🎓 レベル:標準 | 重要度:A(必須)
📎 前提:需要予測の枠組みと移動平均 | 指数平滑の数理:指数平滑法(SES・Holt・Holt-Winters)(時系列分析) | 次:予測誤差の評価と追跡信号
要点(BLUF)
- 指数平滑は、直近を指数的に重く・遠い過去を軽く見る適応的な平滑。更新式 の が反応性と平滑のトレードオフを決めます。本稿は要点だけ——SES/Holt/Holt-Winters の漸化式の導出・ の推定は 指数平滑法(SES・Holt・Holt-Winters) に詳説があり、再導出しません。
- 本稿のOM固有の主役は季節指数法(ratio-to-moving-average)。移動平均(需要予測の枠組みと移動平均)が外挿できなかった季節を、乗法の季節指数として取り出して掛け戻す手法で、中期の集約計画で定番です。
- 流れは6ステップ——①中心化移動平均で水準を推定 → ②季節比 → ③同月平均 → ④合計= に正規化(季節指数) → ⑤季節調整してトレンドを当てる → ⑥将来を予測して季節指数を掛け戻す(再季節化)。
- 3年の擬似需要で、12個の季節指数を真値と平均 0.02 の精度で復元し、翌12ヶ月を予測できることをコードで確かめます。
1. 指数平滑:要点とαのトレードオフ(導出は譲る)
単純指数平滑(SES)の更新式は1本です。新しい観測 を重み で、これまでの予測 を重み で混ぜます。
これを展開すると過去への重みが と指数的に減衰します(だから「指数」平滑)。 が大きいほど直近に強く反応し、小さいほど滑らか——移動平均の窓 と同じトレードオフを、連続的な1パラメータで表したものです。pandas の ewm で実演します(漸化式の手実装は 指数平滑法(SES・Holt・Holt-Winters) にあるので再掲しません)。
import numpy as np
import pandas as pd
rng = np.random.default_rng(1)
# 水準が第20期に 100 -> 120 に上がる系列(ノイズあり)
n = 40
level = np.where(np.arange(n) >= 20, 120.0, 100.0)
y = pd.Series(level + rng.normal(0, 4.0, n))
# 単純指数平滑 F_{t+1}=alpha*y_t+(1-alpha)*F_t を pandas.ewm で実演
# (漸化式の導出・SES/Holt/Holt-Winters・alpha推定は 03-01_指数平滑法 に譲る)
print("alpha 平滑度(差分SD) 水準シフト後118到達まで")
for a in [0.2, 0.6]:
sm = y.ewm(alpha=a, adjust=False).mean()
rough = sm.diff().dropna().std(ddof=1) # 小さいほど滑らか
after = sm.values[20:] # 第20期以降
reach = int(np.argmax(after >= 118)) if (after >= 118).any() else -1
print(f"{a:>4} {rough:>10.2f} {reach:>3d} 期後")
出力:
alpha 平滑度(差分SD) 水準シフト後118到達まで
0.2 1.29 10 期後
0.6 3.56 2 期後
出力の意味: は滑らか(差分SD 1.29)だが水準シフトへの追従が10期と遅い。 は粗い(3.56)が2期で追いつく。移動平均の窓と同じ「反応性 vs 平滑」を、 ひとつで連続調整できるわけです。SES は水準のみ・予測はフラットなので、トレンドには Holt(水準+トレンド)、季節には Holt-Winters(+季節)と部品を足します——その漸化式の導出と最適 の推定は 指数平滑法(SES・Holt・Holt-Winters)、状態空間表現と予測区間は ETSモデルと状態空間表現 にあります。本稿は別アプローチ——季節指数法——に進みます。
2. 季節指数法:ratio-to-moving-average の6ステップ
季節指数法は、需要を乗法分解 と見て、季節成分 を月ごとの倍率(季節指数)として取り出す古典的手法です。Holt-Winters が季節をオンラインで逐次更新するのに対し、季節指数法は履歴から一括で季節パターンを推定し、集約計画やS&OPで「季節調整した素の需要」を見るのに使われます。
flowchart TB Y["原系列 y_t"] --> CMA["(1) 中心化移動平均 CMA_t<br/>(2x12 で水準を推定)"] Y --> RATIO["(2) 季節比 = y_t / CMA_t"] CMA --> RATIO RATIO --> IDX["(3)(4) 同月平均 → 正規化<br/>季節指数 S_j(合計 = m)"] IDX --> DES["(5) 季節調整 d_t = y_t / S_j"] DES --> TR["(5) トレンド/水準を当てる"] TR --> FC["(6) 将来を予測 → S_j を掛け戻す(再季節化)"] IDX --> FC
(1) 中心化移動平均 CMA:周期 の移動平均で季節を均し、トレンド+水準を取り出します。月次( は偶数)は、ちょうど中心の月に揃えるため両端を半分にした2×12移動平均を使います。
(2)(3)(4) 季節比 → 同月平均 → 正規化:実値を CMA で割ると季節×不規則が残ります。同じ月の値を平均して不規則を均し、合計が になるよう正規化(乗法では12ヶ月の指数の合計が 、平均1)。
(5)(6) 季節調整 → トレンド外挿 → 再季節化:原系列を季節指数で割って季節調整済み を作り、そこにトレンド(や水準)を当てて将来を予測し、最後に季節指数を掛け戻します。
3. 季節指数を取り出す(コード)
3年(36ヶ月)の擬似需要——線形トレンド × 乗法季節 × ノイズ——から、ratio-to-moving-average で12個の季節指数を推定し、仕込んだ真値と比べます。
import numpy as np
import pandas as pd
rng = np.random.default_rng(7)
# 3年×12ヶ月 = 36ヶ月の擬似需要:線形トレンド × 乗法季節 × ノイズ
n_years, m = 3, 12
n = n_years * m
t = np.arange(n)
trend = 500.0 + 8.0 * t # 上昇トレンド
true_season = np.array([0.80, 0.75, 0.90, 1.00, 1.10, 1.25,
1.30, 1.20, 1.05, 0.95, 0.85, 0.85]) # 真の季節指数(合計12)
y = trend * true_season[t % m] * rng.normal(1.0, 0.05, n) # 乗法ノイズ
demand = pd.Series(y, name="需要")
# ratio-to-moving-average
# (1) 中心化移動平均 CMA(2x12)で水準を推定。重みは [0.5,1,...,1,0.5]/12
half = m // 2
w = np.ones(m + 1); w[0] = 0.5; w[-1] = 0.5; w = w / m
cma = np.full(n, np.nan)
for i in range(half, n - half):
cma[i] = np.dot(w, y[i - half:i + half + 1])
# (2) 季節比 = 実値 / CMA(季節×不規則)
ratio = y / cma
# (3) 同じ月の季節比を平均 → 生の季節指数
month = t % m
S_raw = np.array([np.nanmean(ratio[month == j]) for j in range(m)])
# (4) 合計が m(=12) になるよう正規化(乗法の制約 sum S_j = m)
S = S_raw * m / np.nansum(S_raw)
tbl = pd.DataFrame({
"月": np.arange(1, m + 1),
"推定季節指数": S,
"真の季節指数": true_season,
})
print(tbl.to_string(index=False, float_format=lambda x: f"{x:.3f}"))
print(f"\n推定季節指数の合計 = {S.sum():.4f} (正規化により m=12 に一致)")
print(f"真の季節指数との平均絶対差 = {np.mean(np.abs(S - true_season)):.4f}")
出力:
月 推定季節指数 真の季節指数
1 0.821 0.800
2 0.744 0.750
3 0.861 0.900
4 1.027 1.000
5 1.087 1.100
6 1.271 1.250
7 1.260 1.300
8 1.224 1.200
9 1.013 1.050
10 0.955 0.950
11 0.854 0.850
12 0.882 0.850
推定季節指数の合計 = 12.0000 (正規化により m=12 に一致)
真の季節指数との平均絶対差 = 0.0225
出力の意味:たった3年・乗法ノイズ込みのデータから、12個の季節指数を真値と平均 0.02 の精度で復元できました。6月・7月に需要が約25〜30%増し(指数 1.27, 1.26)、1月・2月は2割ほど落ち込む(0.82, 0.74)——という季節パターンが、勘ではなく数字で出ています。合計はちょうど 12.0000(正規化の効果)で、季節指数の平均が1=「季節をならすと水準に戻る」ことを保証します。CMA は両端6ヶ月ずつ計算できないので、各月の季節比は実質2年分の平均から推定しており、それでもこの精度です。
4. 季節調整 → トレンド → 再季節化で予測(コード)
季節指数で季節調整し、季節調整済み系列に線形トレンド(np.polyfit)を当て、翌12ヶ月を予測して季節指数を掛け戻します。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
rng = np.random.default_rng(7)
n_years, m = 3, 12
n = n_years * m
t = np.arange(n)
trend = 500.0 + 8.0 * t
true_season = np.array([0.80, 0.75, 0.90, 1.00, 1.10, 1.25,
1.30, 1.20, 1.05, 0.95, 0.85, 0.85])
y = trend * true_season[t % m] * rng.normal(1.0, 0.05, n)
# 季節指数(02-02 コード1と同じ手順)を再計算
half = m // 2
w = np.ones(m + 1); w[0] = 0.5; w[-1] = 0.5; w = w / m
cma = np.full(n, np.nan)
for i in range(half, n - half):
cma[i] = np.dot(w, y[i - half:i + half + 1])
month = t % m
S_raw = np.array([np.nanmean((y / cma)[month == j]) for j in range(m)])
S = S_raw * m / np.nansum(S_raw)
# (1) 季節調整:原系列を季節指数で割る
deseason = y / S[month]
# (2) 季節調整済みに線形トレンドを当てる(np.polyfit)
b1, b0 = np.polyfit(t, deseason, 1) # 傾き b1・切片 b0
print(f"季節調整済みトレンド: 水準 = {b0:.1f} + {b1:.2f} * t")
# (3) 翌12ヶ月を予測 → (4) 季節指数を掛け戻す(再季節化)
t_future = np.arange(n, n + 12)
trend_future = b0 + b1 * t_future
fcast = trend_future * S[t_future % m]
print("\n翌12ヶ月の予測需要(再季節化後):")
for tf, f in zip(t_future, fcast):
print(f" 月{tf + 1:2d} (季節指数 {S[tf % m]:.3f}): {f:7.1f}")
# 図:実績・季節調整済み・トレンド直線・予測
plt.figure(figsize=(11, 5))
plt.plot(t, y, "o-", color="0.5", lw=1, ms=4, label="実績需要")
plt.plot(t, deseason, color="#2ca02c", lw=1.5, label="季節調整済み")
plt.plot(t, b0 + b1 * t, "--", color="#2ca02c", lw=1, label="トレンド直線")
plt.plot(t_future, fcast, "s-", color="#d62728", lw=2, ms=5, label="予測(再季節化)")
plt.axvline(n - 0.5, ls=":", color="gray")
plt.xlabel("月(0 = 1年目1月)"); plt.ylabel("需要")
plt.title("季節指数法:季節調整 → トレンド外挿 → 再季節化")
plt.legend(); plt.tight_layout(); plt.show()
出力:
季節調整済みトレンド: 水準 = 497.8 + 7.25 * t
翌12ヶ月の予測需要(再季節化後):
月37 (季節指数 0.821): 622.5
月38 (季節指数 0.744): 570.0
月39 (季節指数 0.861): 665.9
月40 (季節指数 1.027): 801.6
月41 (季節指数 1.087): 856.2
月42 (季節指数 1.271): 1010.7
月43 (季節指数 1.260): 1010.7
月44 (季節指数 1.224): 991.0
月45 (季節指数 1.013): 827.2
月46 (季節指数 0.955): 787.2
月47 (季節指数 0.854): 710.0
月48 (季節指数 0.882): 739.4
出力の意味:季節調整済み系列はギザギザの季節が消えてほぼ直線になり(図の緑)、そこに当てたトレンドは (真値 をほぼ復元)。これを将来へ伸ばして季節指数を掛け戻すと、予測(赤)はトレンドの上昇と季節の山谷を両方持った系列になります——夏の山(月42・43で約1010)と冬の谷(月38で570)。これは移動平均(需要予測の枠組みと移動平均)のフラットな予測には原理的に出せなかった形です。集約計画では、この「季節調整済みトレンド」で生産を平準化し、季節の山は在庫の積み増しで吸収する、という意思決定につながります。
⚠️ よくある誤解
- 「季節は乗法と決まっている」ではない:振幅が水準に比例して伸びるなら乗法(指数)、水準によらず一定幅なら加法(季節差)を使います。データを見て選びます(加法/乗法の判断は 時系列データと分解)。
- 「季節指数は正規化しなくてよい」ではない:乗法では合計を (平均1)に正規化しないと、季節調整が系全体の水準をずらしてしまいます。加法なら合計を0にそろえます。
- 「指数平滑の は適当でよい」ではない: は誤差最小化でデータから推定するのが本筋です(理論と実装は 指数平滑法(SES・Holt・Holt-Winters))。本稿の は挙動を見せる例示です。
- 「Holt-Winters と季節指数法は同じ」ではない:Holt-Winters は季節係数を逐次オンライン更新、季節指数法は履歴から一括推定。別アプローチで、補完関係にあります(短期の追従はHW、集約計画の季節パターン把握は季節指数法)。
関連ノート
- 需要予測の枠組みと移動平均(前提・移動平均では外挿できなかった季節を扱う)
- 予測誤差の評価と追跡信号(この予測の精度をどう測り監視するか)
- 指数平滑法(SES・Holt・Holt-Winters)(時系列分析・SES/Holt/Holt-Wintersの漸化式の導出と α/β/γ 推定)
- ETSモデルと状態空間表現(時系列分析・状態空間化と予測区間)
- 第3章 在庫管理(季節調整した需要で発注・在庫積み増しを設計)/第5章 集約計画
- オペレーションズ・マネジメント 全体目次