Mímisbrunnr知恵の泉

← 時系列分析 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:ランダムウォークと単位根 | 関連:訓練・検証・テストと交差検証(機械学習)

要点(BLUF)

1. 予測の評価指標

予測 y^t\hat y_t と実測 yty_t の誤差 et=y^tyte_t=\hat y_t-y_t を、3通りに集約します。

RMSE=1ntet2,MAE=1ntet,MAPE=100ntetyt(%)\mathrm{RMSE}=\sqrt{\tfrac1n\textstyle\sum_t e_t^2},\quad \mathrm{MAE}=\tfrac1n\textstyle\sum_t|e_t|,\quad \mathrm{MAPE}=\tfrac{100}{n}\textstyle\sum_t\left|\tfrac{e_t}{y_t}\right|\,(\%)
import numpy as np

y_true = np.array([100, 102, 101, 105, 110, 108.0])
y_pred = np.array([ 98, 103, 104, 102, 109, 111.0])
e = y_pred - y_true
rmse = np.sqrt(np.mean(e**2)); mae = np.mean(np.abs(e)); mape = np.mean(np.abs(e/y_true))*100
print(f"RMSE={rmse:.3f}(大外れに敏感)  MAE={mae:.3f}(平均的ズレ)  MAPE={mape:.2f}%(割合)")

出力:

RMSE=2.345(大外れに敏感)  MAE=2.167(平均的ズレ)  MAPE=2.08%(割合)

出力の意味:RMSE 2.352.35 が MAE 2.172.17 より大きいのは、二乗が大きめの誤差を強く効かせるから。両者の差が開くほど「たまに大きく外す」傾向を示します。MAPE 2.08%2.08\% は「平均して実測の約2%ズレる」とスケール非依存に言えます。目的(大外れを嫌うか・割合で見たいか)で指標を選びます。

2. なぜ普通の k-fold はダメか:未来の情報漏洩

時系列は順序が本質。ところがふつうの k-fold(行をシャッフル/ランダム分割)は、テスト時点より未来のデータを訓練に入れてしまいます。「未来を見て過去を当てる」のは現実の予測ではありえず、評価が楽観的になります。正しくは TimeSeriesSplit(拡大窓)で、訓練を常にテストより過去に限ります。

import numpy as np
from sklearn.model_selection import TimeSeriesSplit, KFold

idx = np.arange(12)
print("TimeSeriesSplit(正しい): 訓練は常にテストより前")
for k, (tr, te) in enumerate(TimeSeriesSplit(n_splits=4).split(idx)):
    print(f"  fold{k}: train={tr.min()}..{tr.max()}  test={te.min()}..{te.max()}")

print("KFold(誤り): テストより未来が訓練に混入=情報漏洩")
leak = 0
for k, (tr, te) in enumerate(KFold(n_splits=4).split(idx)):
    future_in_train = int((tr > te.min()).sum()); leak += future_in_train
    print(f"  fold{k}: test={te.min()}..{te.max()}  未来なのに訓練にある点数={future_in_train}")
print(f"  → 未来漏洩した点の総数={leak}")

出力:

TimeSeriesSplit(正しい): 訓練は常にテストより前
  fold0: train=0..3  test=4..5
  fold1: train=0..5  test=6..7
  fold2: train=0..7  test=8..9
  fold3: train=0..9  test=10..11
KFold(誤り): テストより未来が訓練に混入=情報漏洩
  fold0: test=0..2  未来なのに訓練にある点数=9
  fold1: test=3..5  未来なのに訓練にある点数=6
  fold2: test=6..8  未来なのに訓練にある点数=3
  fold3: test=9..11  未来なのに訓練にある点数=0
  → 未来漏洩した点の総数=18

出力の意味:TimeSeriesSplit は訓練範囲が常にテストより前(拡大窓で 0..3 → 0..5 → …)。一方 KFold は、たとえば fold0 でテストが時点 0022 なのに、訓練に未来の 9 点(時点3以降)が混ざります——合計 18 点の未来漏洩。これで測った精度は本番より良く見えるだけの幻です。同じ理由で、標準化や特徴量も訓練データだけで作ります(全データで作ると未来情報が漏れる)。

3. ウォークフォワード・バックテストとベースライン

実務の検証はバックテスト:過去のある時点に立ち、そこまでの情報だけで次を予測 → 時間を1歩進めて繰り返す。そして必ず素朴予測をベースラインに置きます。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib  # 日本語ラベル用

rng = np.random.default_rng(3)
n, period = 120, 12; t = np.arange(n)
y = 50 + 0.05*t + 8*np.sin(2*np.pi*t/period) + rng.normal(0, 1.0, n)   # 季節が強い系列

start = 60
pred_naive, pred_snaive, actual, times = [], [], [], []
for i in range(start, n):
    hist = y[:i]                                # 過去だけで予測(未来は見ない)
    pred_naive.append(hist[-1])                 # 素朴:前回値
    pred_snaive.append(hist[-period])           # 季節素朴:1周期前
    actual.append(y[i]); times.append(i)
actual = np.array(actual)
rmse_naive  = np.sqrt(np.mean((np.array(pred_naive)  - actual)**2))
rmse_snaive = np.sqrt(np.mean((np.array(pred_snaive) - actual)**2))
print(f"1期先予測 RMSE: 素朴(前回値)={rmse_naive:.3f}  季節素朴(1周期前)={rmse_snaive:.3f}")

plt.figure(figsize=(9, 4))
plt.plot(t, y, color="gray", lw=1, label="実測")
plt.plot(times, pred_snaive, "o-", ms=3, color="C1", label="季節素朴予測(1周期前)")
plt.axvline(start, ls=":", color="k"); plt.xlabel("時点 t"); plt.legend()
plt.title("ウォークフォワード・バックテスト(過去だけで1期先を予測)")
plt.tight_layout(); plt.show()

出力:

1期先予測 RMSE: 素朴(前回値)=3.315  季節素朴(1周期前)=1.710

出力の意味:季節が強いこの系列では、季節素朴(RMSE 1.71)が素朴(3.32)の半分の誤差。季節を無視した「前回値」は、毎ステップ季節の波を跨いで外すからです。どちらが勝つかはデータ次第(トレンドが強ければ前回値が勝つことも)——だからこそ複数のベースラインを必ず試し、新しいモデルはベースラインを上回って初めて価値があると判断します。図では季節素朴の予測(橙)が実測(灰)の波によく追随しています。

まとめ(Phase 1)

第1章で時系列の土台が揃いました——分解で構造を見て(時系列データと分解)、定常性と ACF/PACF で依存を測り(定常性と自己相関)、単位根を見極めて差分で定常化し(ランダムウォークと単位根)、順序を保った CV とベースラインで honest に評価する(本ノート)。次章では、この ACF/PACF の指紋を持つ ARIMA モデルで、依存構造を実際にモデル化して予測します。

⚠️ よくある誤解

関連ノート