K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

外生変数付き SARIMAX

上級

04.7.5

外生変数付き SARIMAX

最終更新 2020-07-15 読了時間 3 分
まとめ
外生変数付き SARIMAX は、季節 ARIMA の式に「プロモーションの有無」「その週の気温」のような説明変数を回帰項として足し、その係数を季節・トレンドと同時に推定するモデルです。SARIMAX(train, order=..., seasonal_order=..., exog=train_exog) と exog に DataFrame を渡すだけで、result.params に各変数の効果が数値で現れます。このページでは、プロモーション週に +10、天候指数 1 あたり +3 の効果を埋め込んだ 208 週(4 年)の売上系列を作り、先頭 196 週で学習して残り 12 週を予測します。読み終えると、推定された係数を正解の 10 と 3 と比べて検算でき、「予測時には未来の exog が必要」という制約の意味が分かります。

SARIMAX モデル で order と seasonal_order の 7 つの数の役割を先に押さえておくと、このページは「そこへ exog を 1 つ足すだけ」として読めます。

直感 #

週次の売上には、年に 1 回の季節の山、ゆるい上昇トレンド、そして「セールをした週は +10 売れる」「暖かい週ほど売れる」という外部要因が混ざっています。季節 ARIMA だけで追うと、セール週の跳ねは説明できないノイズとして残差に残り、予測もセールを知らない水平な線になります。exog にセールの有無(0/1)と天候指数を渡すと、モデルは「セール週は +10、天候 1 単位で +3」という係数を季節やトレンドと切り分けて推定し、来月セールを打つ予定なら予測線にもその分の山が立ちます。代償として、予測したい 12 週分のセール予定と天候の見込みを先に用意する必要があります。

何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:水準 → 傾き → 季節 と要素を足すほど、未来への伸ばし方が豊かになる。外生変数はこの上に「セール週だけ +10」のような段差を足す 4 つ目の要素と考えると分かりやすい。

式で書くと外生変数はどこに入るか #

外生変数付きの SARIMAX は、観測値から回帰部分を引いた残りに季節 ARIMA を当てる形で

$$ y_t = \beta_1\, \text{promo}_t + \beta_2\, \text{weather}_t + u_t, \qquad u_t \sim \text{SARIMA}(1,1,1)(0,1,1)_{52} $$

と書けます。このページのデータは \(\beta_1 = 10,\ \beta_2 = 3\) で作っているので、result.params["promo"] と result.params["weather"] がそれぞれ 10 と 3 に近づけば推定は成功です。\(u_t\) 側の seasonal_order=(0, 1, 1, 52) は「52 週前との差分を取り、52 週前の誤差を 1 つ使う」指定で、年次季節を季節差分と季節 MA だけで表しています。

Python で係数を推定し 12 週先を予測する #

次のコードは、プロモーション(8 週に 2 週だけ 1)と天候指数(標準正規)を外生変数として 208 週の売上を作り、先頭 196 週で学習して残り 12 週を predict(exog=test_exog) で予測します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.statespace.sarimax import SARIMAX

rng = np.random.default_rng(31)
dates = pd.date_range("2019-01-01", periods=4 * 52, freq="W")
promo = (np.sin(2 * np.pi * np.arange(len(dates)) / 8) > 0.7).astype(int)
weather = rng.normal(0, 1, len(dates))

baseline = 120 + 0.2 * np.arange(len(dates)) + 15 * np.sin(2 * np.pi * np.arange(len(dates)) / 52)
series = baseline + 10 * promo + 3 * weather + rng.normal(0, 4, len(dates))
series = pd.Series(series, index=dates)

exog = pd.DataFrame({"promo": promo, "weather": weather}, index=dates)
train = series.iloc[:-12]
test_index = series.index[-12:]
train_exog = exog.iloc[:-12]
test_exog = exog.iloc[-12:]

model = SARIMAX(train, order=(1, 1, 1), seasonal_order=(0, 1, 1, 52), exog=train_exog)
result = model.fit(disp=False)
forecast = result.predict(start=test_index[0], end=test_index[-1], exog=test_exog)

fig, ax = plt.subplots(figsize=(7.5, 4))
ax.plot(series.index, series, color="#cbd5f5", label="実測値(全体)")
ax.plot(train.index, train, color="#2563eb", linewidth=1.2, label="学習区間")
ax.plot(test_index, forecast, color="#f97316", linewidth=1.6, label="SARIMAX 予測")
ax.set_title("外生変数付き SARIMAX の例")
ax.set_xlabel("週")
ax.set_ylabel("売上指数")
ax.legend()
ax.grid(alpha=0.3)

fig.tight_layout()
fig.savefig("static/images/timeseries/sarimax_exog.svg")

外生変数付き SARIMAXの図

実行結果の読み方 #

青の学習区間は 120 付近から 4 年で約 160(120 + 0.2 × 195)まで上がりながら、周期 52 週の山を描き、8 週ごとに 2 週だけ +10 跳ねるセール週が点々と乗っています。橙の 12 週の予測はトレンドと季節の続きに、test_exog に含まれるセール週の +10 と天候の ±3 程度の上下を足した形で、薄い青の実測と標準偏差 4 のノイズの範囲で重なります。result.params を表示すると promo が 10 前後、weather が 3 前後の推定値になり、標準誤差は result.bse で確認できます。推定値が正解から大きく外れるなら、学習区間の短さ(季節差分で 52 週失う)や seasonal_order の不足を疑います。

使いどころと落とし穴 #

外生変数の係数は「施策 1 回あたりの効果」をそのまま表すので、promo の係数 10 を使って「来月 2 回セールを打てば +20」といった意思決定の材料にできます。ただし予測時点でその変数の未来の値が手に入るかを先に確認してください。カレンダーや確定した施策は使えますが、気温のように予報に頼る変数は予報誤差がそのまま予測誤差に乗ります。外生変数と季節が同じ周期で動く(毎年 12 月だけセール、など)と季節項と係数が分離できず推定が不安定になるので、result.bse の標準誤差が係数と同じ桁なら変数を減らします。複数系列が互いに影響し合う場合は、片方を exog にするのではなく VAR モデルで同時に扱う方が自然です。