時系列データの分解
「時系列データをそのまま眺めていても、何が起きているかは分からない。分解して初めて構造が見える。」
田中VPoEがStore Salesの売上グラフを表示する。波打つような複雑なパターンが見える。
「トレンド、季節性、残差。この3つに分けることで、需要の本質的な動きが浮かび上がる。STL分解を使おう。」
時系列分解の基本
時系列データは、以下の成分に分解できる。
加法モデル: Y(t) = T(t) + S(t) + R(t)
乗法モデル: Y(t) = T(t) × S(t) × R(t)
T(t): トレンド成分(長期的な方向性)
S(t): 季節成分(周期的な変動)
R(t): 残差成分(ノイズ)
加法モデル vs 乗法モデル
| 特徴 | 加法モデル | 乗法モデル |
|---|---|---|
| 季節変動 | 一定の振幅 | トレンドに比例して振幅が変化 |
| 使用場面 | 売上が安定している場合 | 売上が成長している場合 |
| 変換 | そのまま使用 | 対数変換で加法モデルに変換可能 |
STL分解
STL(Seasonal and Trend decomposition using Loess)は、局所回帰(LOESS)を使った柔軟な時系列分解手法である。
from statsmodels.tsa.seasonal import STL
import pandas as pd
import matplotlib.pyplot as plt
# GROCERY Iカテゴリの全店舗合計売上
grocery = train[train['family'] == 'GROCERY I'].groupby('date')['sales'].sum()
grocery = grocery.asfreq('D').fillna(method='ffill')
# STL分解
stl = STL(grocery, period=7, robust=True) # 週次季節性
result = stl.fit()
# 可視化
fig = result.plot()
fig.set_size_inches(14, 10)
plt.tight_layout()
plt.show()
# 各成分の確認
print("トレンド(最初の5日):")
print(result.trend.head())
print("\n季節成分(最初の7日):")
print(result.seasonal.head(7))
print("\n残差の標準偏差:")
print(result.resid.std())
STLのパラメータ
| パラメータ | 説明 | 推奨値 |
|---|---|---|
| period | 季節周期 | 日次→7(週)or 365(年) |
| seasonal | 季節成分の平滑度 | 奇数、大きいほど滑らか |
| trend | トレンドの平滑度 | Noneで自動設定 |
| robust | 外れ値への頑健性 | Trueを推奨 |
古典的分解との比較
from statsmodels.tsa.seasonal import seasonal_decompose
# 古典的分解(移動平均ベース)
classic_result = seasonal_decompose(grocery, model='additive', period=7)
classic_result.plot()
# 比較
# 古典的分解の問題点:
# 1. 端部(最初と最後のperiod/2期間)が欠損になる
# 2. 季節成分が年間を通じて一定と仮定される
# 3. 外れ値に弱い
#
# STLの利点:
# 1. 端部も推定可能
# 2. 季節成分が時間とともに変化できる
# 3. robust=Trueで外れ値に頑健
複数の季節性
Store Salesデータには、週次と年次の両方の季節性がある。
# 週次パターンの確認
weekly = train.groupby(train['date'].dt.dayofweek)['sales'].mean()
print("曜日別平均売上:")
print(weekly)
# 日曜(0)が最も高い → エクアドルでは日曜が買い物日
# 月次パターンの確認
monthly = train.groupby(train['date'].dt.month)['sales'].mean()
print("\n月別平均売上:")
print(monthly)
# 12月がピーク(クリスマス需要)
# 4月も高い(Semana Santa: 聖週間)
MSTL: 複数季節性の分解
from statsmodels.tsa.seasonal import MSTL
# 週次(7)と年次(365)の2つの季節性を同時に分解
mstl = MSTL(grocery, periods=[7, 365])
result = mstl.fit()
# 成分: trend, seasonal(7), seasonal(365), resid
print("週次季節成分の振幅:", result.seasonal['seasonal_7'].std())
print("年次季節成分の振幅:", result.seasonal['seasonal_365'].std())
トレンドの分析
# トレンド成分から成長率を計算
trend = result.trend
yearly_growth = (trend.iloc[-365:].mean() / trend.iloc[:365].mean() - 1) * 100
print(f"年間成長率: {yearly_growth:.1f}%")
# トレンドの変化点検出
# 2016年4月の地震の影響を確認
earthquake_date = pd.Timestamp('2016-04-16')
before_eq = trend[trend.index < earthquake_date].tail(30).mean()
after_eq = trend[trend.index >= earthquake_date].head(30).mean()
print(f"地震前30日平均: {before_eq:.0f}")
print(f"地震後30日平均: {after_eq:.0f}")
print(f"変化率: {(after_eq / before_eq - 1) * 100:.1f}%")
まとめ
| 項目 | ポイント |
|---|---|
| 時系列分解 | トレンド + 季節性 + 残差に分離 |
| STL分解 | LOESSベースの柔軟な分解手法 |
| 加法 vs 乗法 | 季節振幅が一定なら加法、比例なら乗法 |
| 複数季節性 | Store Salesでは週次と年次の2層 |
チェックリスト
- 時系列分解の3成分を説明できる
- 加法モデルと乗法モデルの違いを理解した
- STL分解を実装し結果を解釈できる
- Store Salesデータの週次・年次季節性を確認した
次のステップへ
時系列の分解ができたところで、次は自己相関分析(ACF/PACF)と定常性検定を学び、ARIMAモデルの前準備を行おう。
推定読了時間: 30分