🎓 レベル:標準 | 重要度:A(必須)
📎 前提:時系列データと分解 | 関連:ARMA・ARIMAモデル(別系統の予測)・予測の評価指標と時系列CV
要点(BLUF)
- 指数平滑法=過去の観測を指数的に減衰する重みで平均し、水準(と必要ならトレンド・季節)を逐次更新して外挿する予測法。直近を重く、遠い過去を軽く見ます。
- 単純指数平滑 SES(水準のみ)→ Holt(水準+トレンド)→ Holt-Winters(水準+トレンド+季節、加法/乗法)と部品を足して発展します。
- 平滑化パラメータ は滑らかさと追従の速さのトレードオフ。本ノートでは Holt-Winters が季節素朴予測比で RMSE を 67.6% 改善し、 が小さいほど滑らか・大きいほど追従が速いことをコードで実証します。
1. 単純指数平滑(SES):水準だけを追う
トレンドも季節もない系列で、水準 だけを推定する最も単純な指数平滑です。更新式は
新しい観測 を重み で、これまでの水準 を重み で混ぜます。SES の 期先予測は水平(フラット)で、。
なぜ「指数」かは、漸化式を展開すると見えます。
にかかる重みは で、 が大きい(古い)ほど で幾何級数的に小さくなります。これが「過去を指数的に重み付けする」の正体で、重みの総和は に収束します。 が大きいほど直近に集中(追従が速い)、小さいほど広く均す(滑らか)。
2. Holt:トレンドを足す(二重指数平滑)
水準に傾き を加え、線形の外挿ができるようにしたのが Holt 法です。
水準は「前回の水準+傾き」を基準に更新し、傾きは「水準の増分」を平滑化して更新します。予測は から傾き で直線的に伸ばす( 期先は )。遠い未来へ直線外挿しすぎる弊害を抑えるため、傾きを で減衰させる減衰トレンド(damped)もよく使われます(ETSモデルと状態空間表現)。
3. Holt-Winters:季節を足す(三重指数平滑)
周期 の季節成分 を加えると Holt-Winters。季節の入れ方で加法と乗法があります。加法版は
ここで は直近の季節指標を使い回すための添字。季節変動の幅が水準に比例して拡大するなら乗法版(・)を使います(時系列データと分解 の加法/乗法と同じ考え方)。statsmodels の命名では smoothing_level、 smoothing_trend、 smoothing_seasonal です。
flowchart LR
Y["観測 y_t"] --> L["水準 ℓ_t を更新(α)"]
Y --> B["傾き b_t を更新(β)"]
Y --> S["季節 s_t を更新(γ)"]
L --> F["予測 ℓ_t + h·b_t + s(季節)"]
B --> F
S --> F
コード①:Holt-Winters で季節+トレンドを捉えて予測(季節素朴比でRMSE比較)
確率的にドリフトする水準(適応的な平滑化が効くよう、ランダムウォーク的にゆらぐ)+加法季節(周期12・振幅8)を仕込んだ月次系列を作り、ExponentialSmoothing(trend="add", seasonal="add", seasonal_periods=12) を当てます。時間順ホールドアウトで最後の24ヶ月を検証に取り、季節素朴予測(12ヶ月前の値)と RMSE を比べます。
import numpy as np
import pandas as pd
from statsmodels.tsa.holtwinters import ExponentialSmoothing
# 真の構造:確率的にドリフトする水準 + 加法季節(周期12,振幅8) + 観測ノイズ
# 水準 mu_t = mu_{t-1} + 0.4(ドリフト) + 揺れ → 適応的な平滑化が効く
np.random.seed(3)
n, period = 132, 12
mu = np.zeros(n); mu[0] = 50.0
for k in range(1, n):
mu[k] = mu[k-1] + 0.4 + np.random.normal(0, 0.8)
t = np.arange(n)
seasonal_true = 8 * np.sin(2 * np.pi * t / period)
y = mu + seasonal_true + np.random.normal(0, 2.0, n)
idx = pd.date_range("2014-01-01", periods=n, freq="MS")
y = pd.Series(y, index=idx)
# 時間順ホールドアウト:最後の24ヶ月を検証に
h = 24
train, test = y.iloc[:-h], y.iloc[-h:]
# Holt-Winters(加法トレンド+加法季節, 周期12)を訓練データに当てる
hw = ExponentialSmoothing(train, trend="add", seasonal="add",
seasonal_periods=period).fit()
fc_hw = hw.forecast(h)
# 季節素朴予測:12ヶ月前の値をそのまま使う
season_naive = train.iloc[-period:].values
fc_naive = np.resize(season_naive, h)
rmse_hw = np.sqrt(np.mean((fc_hw.values - test.values) ** 2))
rmse_naive = np.sqrt(np.mean((fc_naive - test.values) ** 2))
print(f"推定 alpha(水準) = {hw.params['smoothing_level']:.3f}")
print(f"推定 beta(トレンド) = {hw.params['smoothing_trend']:.3f}")
print(f"推定 gamma(季節) = {hw.params['smoothing_seasonal']:.3f}")
print(f"Holt-Winters RMSE = {rmse_hw:.3f}")
print(f"季節素朴予測 RMSE = {rmse_naive:.3f}")
print(f"改善率 = {(1 - rmse_hw/rmse_naive)*100:.1f}%(季節素朴比)")
出力:
推定 alpha(水準) = 0.388
推定 beta(トレンド) = 0.000
推定 gamma(季節) = 0.000
Holt-Winters RMSE = 2.704
季節素朴予測 RMSE = 8.341
改善率 = 67.6%(季節素朴比)
出力の意味:推定された は水準を適応的に更新することを意味します(水準が確率的にドリフトするので、直近に重みを置く必要がある)。一方 は「ドリフトの傾きと季節パターンは安定なので、ほとんど更新しなくてよい」という最尤推定の判断です。Holt-Winters の RMSE は で、季節素朴予測の を 67.6% 改善しました。トレンドと季節を同時に捉えるモデルが、単純なベースラインを明確に上回っています(予測の評価指標と時系列CV)。
⚠️ 古典的な
ExponentialSmoothingの.forecast()は点予測のみで、予測区間は出ません。区間が要るなら、これを状態空間モデルとして定式化した ETS(ETSモデルと状態空間表現 のETSModel)か、.simulate()でのシミュレーションを使います。予測区間つきの実演は次ノートで行います。
4. αの効果:滑らかさ vs 追従の速さ
コード②:SES の漸化式を手実装してαを変える
指数平滑の核心は前述の漸化式 だけです。これを自分で実装し、途中で水準が階段状にジャンプするノイズ系列に をかけて、滑らかさ(平滑後系列の差分の標準偏差)とジャンプへの追従速度を比べます。
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib # 日本語ラベル用
# 途中で水準が階段状に変わるノイズ系列(追従の速さの違いを見る)
np.random.seed(5)
n = 120
level = np.where(np.arange(n) < 60, 10.0, 16.0) # 60期で 10 -> 16 にジャンプ
y = level + np.random.normal(0, 1.2, n)
# SES の漸化式を手実装: l_t = alpha*y_t + (1-alpha)*l_{t-1}
def ses_level(y, alpha):
l = np.zeros(len(y)); l[0] = y[0]
for k in range(1, len(y)):
l[k] = alpha * y[k] + (1 - alpha) * l[k-1]
return l
alphas = [0.1, 0.5, 0.9]
print("alpha 平滑度(差分SD) ジャンプ後10期の平均到達度")
for a in alphas:
l = ses_level(y, a)
smooth = np.std(np.diff(l)) # 小さいほど滑らか
reach = l[69] # ジャンプ(60期)から10期後の水準
print(f"{a:>4} {smooth:>10.3f} {reach:>8.2f}(真の新水準=16)")
# 図:同じ系列に異なる alpha の SES をかける
plt.figure(figsize=(9, 4.5))
plt.plot(y, color="lightgray", lw=1, label="観測値(ノイズあり)")
for a, c in zip(alphas, ["C0", "C1", "C2"]):
plt.plot(ses_level(y, a), color=c, lw=1.8, label=f"SES α={a}")
plt.axvline(60, ls=":", color="k", label="水準ジャンプ")
plt.xlabel("時点 t"); plt.ylabel("値")
plt.title("単純指数平滑(SES):α が小さいほど滑らか・大きいほど追従")
plt.legend(); plt.tight_layout(); plt.show()
出力:
alpha 平滑度(差分SD) ジャンプ後10期の平均到達度
0.1 0.164 13.94(真の新水準=16)
0.5 0.761 15.69(真の新水準=16)
0.9 1.564 16.54(真の新水準=16)
出力の意味: は平滑後の差分 SD が と最も滑らかですが、水準が にジャンプしても 10 期後にまだ までしか追いつけません(追従が遅い)。 は逆に差分 SD でノイズまで拾う代わりに、10 期後には と新水準にほぼ到達(追従が速い・わずかに行き過ぎ)。 は「滑らかさ」と「変化への追従」のトレードオフを一本のつまみで決める——だから実務では をデータから最尤推定するのが標準です(コード①では自動推定された)。
5. 数式の直観
- SES = 指数加重移動平均:直近を重く、過去を で軽くした加重平均。新情報をどれだけ取り込むかが で、これはAR・MAモデル の MA() 表現とも通じます(実際 SES の予測は ARIMA(0,1,1) と等価)。
- Holt = 水準+傾きの二重平滑:水準と傾きを別々の漸化式で更新し、直線で外挿。傾きを減衰させると遠未来の外挿が穏やかになります。
- Holt-Winters = さらに季節を漸化:季節成分も で更新するので、季節の形がゆっくり変わってよい(古典分解の固定季節より柔軟)。重みが小さければほぼ固定季節、大きければ季節が機敏に変化します。
⚠️ よくある誤解
- 「指数平滑は点予測だけでよい」ではない:古典的指数平滑の
.forecast()は点のみ。意思決定には予測区間が要ります。区間は ETS(ETSモデルと状態空間表現)か.simulate()で出します。 - 「季節周期は自動で決まる」ではない:
seasonal_periods(12・7・24 など)は分析者がドメイン知識で与えます。誤った周期は予測を壊します(時系列データと分解)。 - 「乗法でも加法でもどちらでもよい」ではない:季節振幅が水準とともに拡大するなら乗法、一定なら加法。間違えると外挿で季節の山谷がずれます。迷えば対数変換して加法で扱う手も。
- 「 は大きいほど賢い」ではない: が大きいとノイズに過敏になり、平滑の意味が薄れます。最尤推定に任せるか、検証データ(時間順)で選ぶのが安全。
- 「Holt の直線外挿は遠未来も当たる」ではない:傾きをそのまま伸ばすので、長期では過大/過小になりがち。減衰トレンドで外挿を抑えるのが定石です。
関連ノート
- 時系列データと分解(トレンド・季節・加法/乗法の土台)
- ETSモデルと状態空間表現(指数平滑の状態空間化・予測区間が出せる)
- STL分解(ロバストな分解・季節調整)
- ARMA・ARIMAモデル(別系統の予測・SES=ARIMA(0,1,1) との等価)
- 予測の評価指標と時系列CV(時間順ホールドアウト・ベースライン)
- 第3章 指数平滑と分解 目次
- 時系列分析・予測テキスト 全体目次