Mímisbrunnr知恵の泉

← 時系列分析 一覧

🎓 レベル:基礎 | 重要度:A(必須)

📎 数理:確率過程(マルコフ連鎖・ポアソン過程)(統計)

要点(BLUF)

1. 時系列データとは

時系列は、時刻順に並んだ観測 y1,y2,,yTy_1,y_2,\dots,y_T です。普通の表データと決定的に違うのは、順序に意味があり、近い時点が相関すること(自己相関、定常性と自己相関)。だから「行をシャッフルしてよい」前提のふつうの手法(k-fold交差検証など)はそのままでは使えません(予測の評価指標と時系列CV)。まずは系列を眺め、規則的な動き(トレンド・季節)と不規則な動き(残差)に分けて理解します。

2. 3成分への分解

組み合わせ方は2通り:

加法: yt=Tt+St+Rt,乗法: yt=Tt×St×Rt\text{加法:}\ y_t=T_t+S_t+R_t,\qquad \text{乗法:}\ y_t=T_t\times S_t\times R_t

季節変動の幅が水準とともに大きくなるなら乗法。対数を取ると logyt=logTt+logSt+logRt\log y_t=\log T_t+\log S_t+\log R_t と加法に戻るので、実務では「対数変換して加法分解」がよく使われます。

古典的分解の手順はシンプルです:トレンドを中心化移動平均(窓=周期)で推定 → 元系列から引いた残りを季節ごとに平均して季節成分 → 残りが残差。

3. コード:既知の構造を分解して復元する

真のトレンド・季節・ノイズを自分で仕込んだ系列を作り、分解がそれを復元するか確かめます。

import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose

# 真の構造を仕込んだ擬似系列:水準20 + トレンド + 季節(振幅4) + ノイズ(SD0.7)
rng = np.random.default_rng(0)
n, period = 144, 12                       # 12年×月次のイメージ
t = np.arange(n)
trend_true = 0.08 * t
seasonal_true = 4 * np.sin(2*np.pi*t/period)
y = 20 + trend_true + seasonal_true + rng.normal(0, 0.7, n)

# 加法分解(周期12)
res = seasonal_decompose(y, period=period, model="additive")
mask = ~np.isnan(res.trend)
print(f"季節成分の相関(復元 vs 真)= {np.corrcoef(res.seasonal, seasonal_true)[0,1]:.3f}")
print(f"トレンドの相関(復元 vs 真)= {np.corrcoef(res.trend[mask], trend_true[mask])[0,1]:.3f}")
print(f"季節の振幅(復元)= {(res.seasonal.max()-res.seasonal.min())/2:.2f}  (真=4.0)")
print(f"残差のSD = {np.nanstd(res.resid):.3f}  (真のノイズSD=0.7)")

出力:

季節成分の相関(復元 vs 真)= 0.997
トレンドの相関(復元 vs 真)= 0.998
季節の振幅(復元)= 4.01  (真=4.0)
残差のSD = 0.577  (真のノイズSD=0.7)

出力の意味:分解した季節成分・トレンドが、仕込んだ真の成分と相関 0.997/0.998 でほぼ完全に一致。季節の振幅も 4.0 → 4.01 と正確に復元できました。残差 SD は 0.58 で真の 0.7 よりやや小さい——これは古典分解の移動平均トレンドや季節平均が、ノイズの一部を吸収してしまうためで、分解は「記述(構造を見る)には強いが、残差を厳密なノイズ推定とみなしすぎない」のが正しい使い方です。

4. 図:4パネルで分解を見る

原系列・トレンド・季節・残差を並べると、構造が一目で分かります。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib  # 日本語ラベル用
from statsmodels.tsa.seasonal import seasonal_decompose

rng = np.random.default_rng(0)
n, period = 144, 12; t = np.arange(n)
y = 20 + 0.08*t + 4*np.sin(2*np.pi*t/period) + rng.normal(0, 0.7, n)
res = seasonal_decompose(y, period=period, model="additive")

fig, ax = plt.subplots(4, 1, figsize=(9, 7), sharex=True)
for a, data, name in zip(ax, [y, res.trend, res.seasonal, res.resid],
                         ["原系列", "トレンド", "季節性", "残差"]):
    a.plot(t, data, lw=1.2); a.set_ylabel(name)
ax[0].set_title("時系列の加法分解(トレンド+季節+残差)")
ax[-1].set_xlabel("時点 t"); plt.tight_layout(); plt.show()

グラフの意味:原系列(最上段)の右肩上がり+ギザギザが、トレンド(直線的な上昇)・季節(周期12の波)・残差(不規則な揺れ)に分かれます。これで「上昇はトレンドのせい、毎年の波は季節のせい」と読め、次に何をモデル化すべきか(トレンド除去・季節調整)が見えてきます。

5. 加法か乗法か

より頑健な分解として STL(Seasonal-Trend decomposition using Loess、STL分解)があり、外れ値や季節の緩やかな変化に強いです。

⚠️ よくある誤解

関連ノート