🎓 レベル:基礎 | 重要度:A(必須)
📎 前提:プロセス分析とリトルの法則(フローの土台) | 成分分解の理論:時系列データと分解(時系列分析) | 次:指数平滑と季節指数による需要予測
要点(BLUF)
- 需要予測は「当てもの」ではなく、オペレーションズの意思決定への入力です。時間軸の長さで使い道が変わります——短期(発注・スケジューリング)/中期(集約計画・要員)/長期(設備・キャパシティ)。
- 予測対象は需要の時間パス。需要を 水準・トレンド・季節・ノイズの重ね合わせとして捉えます(成分分解そのものの理論は 時系列データと分解 に譲り、本稿は予測への使い方に集中)。
- 最も基本的な平滑が移動平均——単純移動平均 SMA(直近 期の平均)と加重移動平均 WMA(直近を重く)。
- 移動平均は必ず 「平滑(ノイズ除去)」と「遅れ(トレンド・変化への反応)」のトレードオフを持ちます。窓 を大きくすると滑らかになる代わりに、トレンドや水準変化への追従が系統的に遅れます。本稿はこれを数値で実証します。
1. 需要予測はオペレーションズの入力である
OM で需要予測を学ぶ理由は、予測の精度自慢のためではありません。予測はその先の意思決定の入力だからです——何個発注するか(第3章 在庫)、何人配置し何台動かすか(第5章 生産計画)、設備をいつ増やすか(キャパシティ)。予測が変われば在庫も人員も変わります。
flowchart LR D["需要データ(履歴)"] --> M["平滑・分解<br/>(水準/トレンド/季節)"] M --> F["予測 F_t"] F --> INV["在庫・発注(第3章)"] F --> CAP["キャパ・集約計画(第5章)"] F --> ERR["予測誤差の監視<br/>(02-03)"] ERR --> SS["安全在庫 SS=z σ_e(第3章)"]
意思決定の時間軸ごとに、向く手法が変わります。
| 時間軸 | 代表的な意思決定 | 向く手法 |
|---|---|---|
| 短期(日〜数週) | 発注・要員シフト・スケジューリング | 移動平均・指数平滑 |
| 中期(数ヶ月〜1年) | 集約計画(生産平準化・在庫積み増し) | 季節指数法・Holt-Winters |
| 長期(1年〜) | 設備投資・キャパシティ・新規拠点 | 回帰・市場モデル・定性予測 |
OM で繰り返し効く需要予測の3原則を先に押さえます。
- 予測は必ず外れる。点予測だけでなく誤差の幅を持たねば意思決定に使えません(だから誤差を測り → 予測誤差の評価と追跡信号、安全在庫で吸収 → 第3章)。
- 集約するほど当たる。個々のSKU・店舗より、束ねた合計のほうが相対誤差が小さい(リスクプーリング。第3章・第6章 SCM で定量化)。
- 近い未来ほど当たる。ホライズンが延びるほど精度は落ちる。長期予測に短期の精度を期待しない。
2. 需要の構造:水準・トレンド・季節・ノイズ
予測の前に、需要系列がどんな成分でできているかを見ます。基本は加法の重ね合わせ、
変動の大きさが水準に比例して伸びるなら乗法 を使います(対数を取れば加法に戻る)。この成分分解の理論・加法/乗法の使い分け・分解アルゴリズムは 時系列データと分解 に詳説があります。本稿では「需要はこの4成分でできている」という見方だけを使い、移動平均が拾えるのは水準まで——トレンド・季節は別の手当て(指数平滑と季節指数による需要予測)が要る、という線を引きます。
3. 移動平均:数式と「遅れ」の理屈
単純移動平均 SMA:直近 期の単純平均を、次期 の予測 とします。
加重移動平均 WMA:直近を重く、過去を軽く。重みは合計1に正規化します( が最新)。
どちらも次期予測はフラット(その時点の平滑水準をそのまま横に伸ばすだけ)で、トレンドや季節を将来に外挿しません。ここが移動平均の限界です。
なぜトレンドに「遅れる」のか
需要が一定の傾き で増えている()とき、SMA がどれだけ下振れするかは計算できます。直近 期の平均は、その期間の真ん中の時刻 の水準に等しいので、
一方、実現する 。差し引くと、予測の**系統的な下振れ(遅れ)**は
窓 に比例して遅れが大きくなるのがポイントです。WMA で直近を重くすると、平均の「重心」が新しい時刻へ寄るので、同じ窓でも遅れが小さくなります(重心の遅れ が小さくなるため)。次のコードでこの理屈を数値で確かめます。
4. SMA・WMA を当てる:平滑度と遅れ(コード)
水準+トレンド+季節+ノイズに、第18月に一度だけ水準が +40 跳ねるステップ変化を仕込んだ36ヶ月の擬似需要に、SMA(3)・SMA(12)・WMA(直近3期, 重み0.5/0.3/0.2) を当てます。平滑度(平滑後系列の1階差の標準偏差。小さいほど滑らか)と、遅れ(1期先予測の平均誤差。大きいほどトレンドに遅れる)、ステップ応答(純粋なステップへの追従の速さ)を測ります。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
rng = np.random.default_rng(42)
# 擬似月次需要(36ヶ月):水準 + 線形トレンド + 月次季節 + ノイズ。
# さらに第18月に一度だけ水準が +40 跳ねる「ステップ変化」を仕込む
n = 36
t = np.arange(n)
level, slope, samp = 200.0, 2.0, 15.0
season = samp * np.sin(2 * np.pi * (t % 12) / 12.0) # 季節(振幅15)
step = np.where(t >= 18, 40.0, 0.0) # 第18月の水準ジャンプ
signal = level + slope * t + season + step # ノイズなしの真の信号
y = signal + rng.normal(0, 8.0, n) # 観測(ノイズSD=8)
demand = pd.Series(y, name="需要")
# 移動平均:単純SMA(3)・SMA(12)・加重WMA(直近3期, 重み0.5/0.3/0.2)
sma3 = demand.rolling(3).mean()
sma12 = demand.rolling(12).mean()
w = np.array([0.5, 0.3, 0.2]) # 直近→過去の重み(合計1)
wma = demand.rolling(3).apply(lambda x: np.dot(x[::-1], w), raw=True)
# (1) 平滑度=平滑後系列の1階差の標準偏差(小さいほど滑らか)
def diff_sd(s):
return s.dropna().diff().dropna().std(ddof=1)
print("=== 平滑度(1階差の標準偏差・小さいほど滑らか)===")
print(f" 原系列 raw : {diff_sd(demand):6.2f}")
print(f" WMA(3) : {diff_sd(wma):6.2f}")
print(f" SMA(3) : {diff_sd(sma3):6.2f}")
print(f" SMA(12) : {diff_sd(sma12):6.2f}")
# (2) トレンドへの遅れ=1期先予測の平均誤差(符号つき)。
# F_{t+1}=時刻tの移動平均。トレンド上昇局面では予測が下振れ(正の誤差)
def mean_signed_error(sm):
f = sm.shift(1) # 時刻t-1の平滑値が時刻tの予測
e = demand - f
return e.dropna().mean()
print("\n=== トレンドへの遅れ(1期先予測の平均誤差・大きいほど遅れる)===")
print(f" WMA(3) : {mean_signed_error(wma):6.2f}")
print(f" SMA(3) : {mean_signed_error(sma3):6.2f}")
print(f" SMA(12) : {mean_signed_error(sma12):6.2f}")
# (3) ステップ応答:ノイズ・季節を除いた純粋なステップ(100->140)で、
# 新水準の90%(=136)に到達するまでの月数を測る
pure = np.where(np.arange(40) >= 5, 140.0, 100.0)
ps = pd.Series(pure)
def periods_to_90(window):
sm = ps.rolling(window).mean()
reached = np.where(sm.values >= 136.0)[0]
return reached[0] - 5 # ステップ発生(=月5)からの経過
print("\n=== ステップ応答(100->140、新水準の90%=136 到達までの月数) ===")
print(f" SMA(3) : {periods_to_90(3)} ヶ月")
print(f" SMA(12) : {periods_to_90(12)} ヶ月")
# 図:原系列と3つの平滑(ステップ第18月)
plt.figure(figsize=(11, 5))
plt.plot(t, demand, "o-", color="0.6", lw=1, ms=4, label="原系列(観測需要)")
plt.plot(t, sma3, color="#1f77b4", lw=2, label="SMA(3) 反応速い・粗い")
plt.plot(t, wma, color="#2ca02c", lw=2, label="WMA(3) 直近重視")
plt.plot(t, sma12, color="#d62728", lw=2, label="SMA(12) 滑らか・遅れ大")
plt.axvline(18, ls=":", color="gray")
plt.text(18.3, demand.min(), "第18月:水準ジャンプ", color="gray")
plt.xlabel("月"); plt.ylabel("需要")
plt.title("移動平均:窓が大きいほど滑らか、だがトレンド・ステップに遅れる")
plt.legend(); plt.tight_layout(); plt.show()
出力:
=== 平滑度(1階差の標準偏差・小さいほど滑らか)===
原系列 raw : 13.77
WMA(3) : 7.19
SMA(3) : 6.45
SMA(12) : 1.77
=== トレンドへの遅れ(1期先予測の平均誤差・大きいほど遅れる)===
WMA(3) : 4.68
SMA(3) : 5.51
SMA(12) : 24.68
=== ステップ応答(100->140、新水準の90%=136 到達までの月数) ===
SMA(3) : 2 ヶ月
SMA(12) : 10 ヶ月
出力の意味:平滑度は原系列 13.77 → WMA 7.19 → SMA(3) 6.45 → SMA(12) 1.77 の順に滑らかになります。窓が大きいほど滑らか(SMA(12) は原系列の8分の1の凸凹)。ところが代償として遅れが増えます——1期先予測の平均誤差は SMA(12) が 24.68(トレンドの理論遅れ に、第18月の +40 ステップへの追従の鈍さが上乗せされた値)。SMA(3) は 5.51、WMA(3) は 4.68 で、WMA は同じ窓3でも直近を重くするぶん遅れが小さい(重心が新しい)。ステップ応答は決定的で、+40 の段差の90%まで追いつくのに SMA(3) は2ヶ月、SMA(12) は10ヶ月もかかります。図でも赤い SMA(12) が第18月の段差を大きく出遅れて登っていくのが見えます。滑らかさと反応性は同時に手に入りません。
5. 窓 の選択はトレードオフ(コード)
では窓 はいくつが良いのか。水準+ゆるいトレンド+ノイズ(季節なし)の系列で、 を変えて1期先予測の MAD(平均絶対偏差) を比べます。小さい はノイズに過敏(分散大)、大きい はトレンドに遅れる(バイアス大)——その和が最小になる中庸の があるはずです。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
rng = np.random.default_rng(11)
# 水準 + ゆるいトレンド + ノイズ(季節なし)。窓kの選び方を「ノイズ除去 vs 反応性」で見る
n = 120
t = np.arange(n)
level, slope, sigma = 100.0, 1.5, 10.0
y = level + slope * t + rng.normal(0, sigma, n)
demand = pd.Series(y)
# 1期先SMA予測 F_{t+1}=直近k期平均。各kで同じ評価区間でMADを比較
ks = [1, 2, 3, 4, 6, 9, 12, 18, 24]
start = max(ks) # どのkでも予測が出せる位置から評価
rows = []
for k in ks:
fc = demand.rolling(k).mean().shift(1) # 時刻tの予測=t-1までのk期平均
e = (demand - fc).iloc[start:] # 予測誤差(共通区間)
rows.append({"窓k": k, "MAD": e.abs().mean()})
tbl = pd.DataFrame(rows)
best_k = int(tbl.loc[tbl["MAD"].idxmin(), "窓k"])
print(tbl.to_string(index=False, float_format=lambda x: f"{x:.3f}"))
print(f"\nMAD最小の窓 k = {best_k}")
print("小さいk: ノイズに過敏(MAD大)/大きいk: トレンドに遅れる(MAD大)")
# 図:MAD vs 窓k のU字(トレードオフ)
plt.figure(figsize=(8, 5))
plt.plot(tbl["窓k"], tbl["MAD"], "o-", color="#1f77b4")
plt.plot(best_k, tbl["MAD"].min(), "*", color="#d62728", ms=18, label=f"最小 k={best_k}")
plt.xlabel("移動平均の窓 k"); plt.ylabel("1期先予測の MAD")
plt.title("窓kのトレードオフ:小さすぎ=ノイズに過敏/大きすぎ=トレンドに遅れる")
plt.legend(); plt.tight_layout(); plt.show()
出力:
窓k MAD
1 11.289
2 10.815
3 9.882
4 9.317
6 9.679
9 10.789
12 12.092
18 15.182
24 19.106
MAD最小の窓 k = 4
小さいk: ノイズに過敏(MAD大)/大きいk: トレンドに遅れる(MAD大)
出力の意味:MAD は に対してU字を描きます——(=直前値をそのまま予測)は 11.29 とノイズを丸ごと拾い、 は 19.11 とトレンドへの遅れで悪化。**最小は (MAD 9.32)**で、ノイズ除去と反応性のバランスがそこにあります。「窓は大きいほど良い」でも「小さいほど反応的で良い」でもなく、データのノイズとトレンドの強さで最適 が決まるということです。トレンドがもっと急なら最適 は小さく、もっと平坦でノイズだらけなら大きくなります。最適 をデータから選ぶ手続き(時系列クロスバリデーション)は バックテストと予測の評価 に譲ります。
⚠️ よくある誤解
- 「移動平均はトレンドにも当たる」ではない:第3節の通り、SMA はトレンド上昇局面で必ず だけ系統的に下振れします。移動平均が正しく当たるのは**水準が一定(定常)**の区間だけ。トレンドがあるなら指数平滑(Holt)や回帰へ。
- 「窓は大きいほど安定して良い」ではない:大きい窓は確かに滑らかですが、変化への反応が決定的に遅れます(ステップの90%追従に SMA(12) は10ヶ月)。窓の大小はノイズ除去 vs 反応性のトレードオフで、最適点は中庸にあります。
- 「移動平均で季節も予測できる」ではない:移動平均の予測はその時点の平滑水準を横に伸ばすだけ(フラット)で、季節やトレンドを将来へ外挿しません。季節は 指数平滑と季節指数による需要予測 の季節指数法、トレンドは Holt 法で別に手当てします。
関連ノート
- 指数平滑と季節指数による需要予測(次のトピック・トレンド/季節を外挿する)
- 予測誤差の評価と追跡信号(予測の精度をどう測り監視するか)
- 時系列データと分解(時系列分析・成分分解の理論。本稿はその使い方)
- バックテストと予測の評価(時系列分析・窓やモデルの選び方)
- 第3章 在庫管理(予測は発注点・安全在庫の入力)/第5章 生産計画(集約計画の入力)
- オペレーションズ・マネジメント 全体目次