Mímisbrunnr知恵の泉

← オペレーションズマネジメント 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:指数平滑と季節指数による需要予測 | モデル選択・交差検証:バックテストと予測の評価(時系列分析) | 応用:第3章 安全在庫

要点(BLUF)

1. 誤差指標:大きさと偏りは別物

予測誤差 et=AtFte_t = A_t - F_t から、性格の違う指標を作ります。

MAD=1ntet,MSE=1ntet2,RMSE=MSE\text{MAD} = \frac{1}{n}\sum_{t}|e_t|,\qquad \text{MSE} = \frac{1}{n}\sum_{t}e_t^2,\qquad \text{RMSE} = \sqrt{\text{MSE}} MAPE=1ntetAt,MFE=1ntet\text{MAPE} = \frac{1}{n}\sum_{t}\left|\frac{e_t}{A_t}\right|,\qquad \text{MFE} = \frac{1}{n}\sum_{t}e_t

モデル選択 vs 運用監視:「どのモデルが良いか」を過去データで選ぶのは交差検証=バックテストと予測の評価 の仕事。本稿は「採用後の予測が偏っていないかを見張る」運用監視で、目的が違います。

2. 誤差指標を計算する(コード)

ある予測の実績/予測ペア(擬似・無バイアス)から、5つの指標を計算します。

import numpy as np

rng = np.random.default_rng(29)

# 実績 vs 予測(擬似・無バイアス)。誤差指標 MAD/MSE/RMSE/MAPE/MFE を計算
n = 60
actual = 100 + 5 * np.sin(2 * np.pi * np.arange(n) / 12) + rng.normal(0, 6, n)
forecast = actual + rng.normal(0, 8, n)      # 平均0の予測誤差を上乗せ(無バイアス)

e = actual - forecast                        # 予測誤差 e_t = A_t - F_t
MAD = np.mean(np.abs(e))                      # 平均絶対偏差
MSE = np.mean(e ** 2)
RMSE = np.sqrt(MSE)
MAPE = np.mean(np.abs(e / actual)) * 100      # 平均絶対パーセント誤差(%)
MFE = np.mean(e)                              # 平均誤差(バイアス)

print(f"観測数 n   = {n}")
print(f"MAD        = {MAD:.3f}")
print(f"MSE        = {MSE:.3f}")
print(f"RMSE       = {RMSE:.3f}")
print(f"MAPE(%)    = {MAPE:.3f}")
print(f"MFE(bias)  = {MFE:.3f}   (0 近傍なら無バイアス)")
print(f"RMSE/MAD   = {RMSE / MAD:.3f}  (正規誤差なら理論値 約1.25)")

出力:

観測数 n   = 60
MAD        = 6.564
MSE        = 67.745
RMSE       = 8.231
MAPE(%)    = 6.625
MFE(bias)  = -0.013   (0 近傍なら無バイアス)
RMSE/MAD   = 1.254  (正規誤差なら理論値 約1.25)

出力の意味:MAD 6.56・RMSE 8.23・MAPE 6.6%。MFE は −0.013 とほぼ0——この予測は無バイアス(過大も過小も続いていない)と判断できます。一方 RMSE/MAD は 1.254 で、正規誤差の理論値 π/21.2533\sqrt{\pi/2}\approx1.2533 をほぼ言い当てています。この比は第5節の σe1.25MAD\sigma_e\approx1.25\,\text{MAD} と同じ関係で、MAD さえ測れば誤差の標準偏差が出る → 安全在庫が計算できるという橋になります。

3. 追跡信号:予測の管理図

問題は、運用中に予測がだんだん偏っていくことです(市場が構造変化したのにモデルが古いまま、など)。単発の MAD では気づきにくい。そこで累積予測誤差 RSFE(Running Sum of Forecast Errors)を MAD で割った追跡信号を、管理図のように監視します。

RSFEt=s=1tes,TSt=RSFEtMADt\text{RSFE}_t = \sum_{s=1}^{t} e_s,\qquad TS_t = \frac{\text{RSFE}_t}{\text{MAD}_t}

無バイアスなら誤差は正負に散り、RSFE は0近傍をふらつくので TSTS も0付近。ところが予測が一方向にずれ始めると、同符号の誤差が累積して RSFE が一方向に伸び、TSTS が0から離れます。経験則として管理限界 ±4(おおむね「6ヶ月連続で同じ符号」に相当)を超えたら、系統的バイアスありと見て予測を見直します。±4\pm4 は、誤差が独立・無バイアスなら滅多に超えない水準として使われます(要最新確認:限界値は運用方針で ±3〜±6 と幅があります)。

4. バイアスを検知する(コード)

予測を需要水準=100 と見て据え置いたところ、第18期に需要の真の水準が +12 シフトした状況を作ります。予測がこれを取り逃がし、誤差が正に偏り続けます。追跡信号がいつ管理限界を破るかを見ます。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

rng = np.random.default_rng(24)

# 36期。予測は需要水準=100 とみて固定。ところが第18期に需要の真の水準が +12 シフト
# (構造変化)。予測がこれを捉え損ね、実績-予測 が正に偏り続ける
n, bias_start = 36, 18
mu = np.full(n, 100.0)
mu[bias_start:] = 112.0                          # 第18期に需要水準が +12
demand = mu + rng.normal(0, 5, n)                # 実績需要
forecast = np.full(n, 100.0)                     # 予測は100に据え置き

e = demand - forecast                            # 予測誤差 e_t = A_t - F_t
rsfe = np.cumsum(e)                              # 累積予測誤差 RSFE
mad_run = np.array([np.mean(np.abs(e[:i + 1])) for i in range(n)])  # 逐次MAD
ts = rsfe / mad_run                              # 追跡信号 TS_t = RSFE/MAD

limit = 4.0
breach = np.where(np.abs(ts) > limit)[0]
first_breach = int(breach[0]) if breach.size else None
print(f"需要水準シフト = 第{bias_start}期に +12(予測は100で据え置き)")
print(f"追跡信号が +-{limit:.0f} を最初に超えた時点 = 第{first_breach}期")
print(f"その時点の TS = {ts[first_breach]:.2f}")

# 誤差 -> 安全在庫の橋渡し(無バイアスな前半=安定期のMADを使う)
mad_stable = np.mean(np.abs(e[:bias_start]))
sigma_e = 1.25 * mad_stable                      # 正規近似 sigma_e approx 1.25*MAD
z = 1.65                                          # 95%サービス水準(片側)
SS = z * sigma_e                                  # 安全在庫(リード1期)
print(f"\n[誤差 -> 安全在庫] 安定期(前半)のMAD = {mad_stable:.2f}")
print(f"  sigma_e approx 1.25*MAD = {sigma_e:.2f}")
print(f"  安全在庫 SS = z*sigma_e = {z} * {sigma_e:.2f} = {SS:.2f}  (z=1.65, 95%)")

# 図:追跡信号と ±4 の管理限界
fig, ax = plt.subplots(figsize=(11, 5))
ax.plot(np.arange(n), ts, "o-", color="#1f77b4", label="追跡信号 TS = RSFE/MAD")
ax.axhline(limit, ls="--", color="#d62728", label="管理限界 ±4")
ax.axhline(-limit, ls="--", color="#d62728")
ax.axhline(0, color="gray", lw=0.8)
ax.axvline(bias_start, ls=":", color="green", label=f"第{bias_start}期:水準シフト")
if first_breach is not None:
    ax.plot(first_breach, ts[first_breach], "*", color="black", ms=16,
            label=f"検知(第{first_breach}期)")
ax.set_xlabel("期"); ax.set_ylabel("追跡信号 TS")
ax.set_title("追跡信号:累積誤差/MAD が ±4 を超えたら予測バイアスを検知")
ax.legend(); plt.tight_layout(); plt.show()

出力:

需要水準シフト = 第18期に +12(予測は100で据え置き)
追跡信号が +-4 を最初に超えた時点 = 第19期
その時点の TS = 5.96

[誤差 -> 安全在庫] 安定期(前半)のMAD = 3.65
  sigma_e approx 1.25*MAD = 4.56
  安全在庫 SS = z*sigma_e = 1.65 * 4.56 = 7.53  (z=1.65, 95%)

出力の意味:第18期まで追跡信号は±4の内側をふらつき(無バイアス)、需要が +12 シフトした直後の第19期に TS が 5.96 と管理限界を突破しました——構造変化をほぼ1期で検知できています。図では、バイアス注入(緑の点線)後に青い追跡信号が急上昇し、★印(第19期)で赤い限界線を超え、その後も登り続けます。これは「予測が外れ続けているのに気づかず在庫を切らす」事態を防ぐ早期警報です。検知したら予測の水準を上方修正します(あるいは α\alpha を上げて適応を速める → 指数平滑と季節指数による需要予測)。

5. 誤差 → 安全在庫:σ_e ≈ 1.25 MAD(第3章への橋)

予測は必ず外れる以上、OM はその誤差を安全在庫で吸収します。誤差 ete_t が平均0・標準偏差 σe\sigma_e の正規分布なら、絶対値の期待値は

Ee=σe2π0.7979σe        σe=π2  MAD1.25MAD\mathbb{E}|e| = \sigma_e\sqrt{\tfrac{2}{\pi}} \approx 0.7979\,\sigma_e \;\;\Longrightarrow\;\; \sigma_e = \sqrt{\tfrac{\pi}{2}}\;\text{MAD} \approx 1.25\,\text{MAD}

つまりMAD を1.25倍すれば誤差の標準偏差が得られます(第2節で RMSE/MAD≈1.25 を確認した通り)。リードタイム1期の安全在庫は、欲しいサービス水準に対応する正規分位 zz を使って

SS=zσez1.25MADSS = z\,\sigma_e \approx z\cdot 1.25\,\text{MAD}

上のコードでは、安定期の MAD 3.65 から σe4.56\sigma_e \approx 4.56、95%サービス水準(z=1.65z=1.65)で安全在庫 SS ≈ 7.53予測誤差のばらつきがそのまま在庫量を決めるわけです。リードタイムが LL 期に伸びると誤差は σeL\sigma_e\sqrt{L} で効く——この一般化と発注点の設計は第3章で扱います。

⚠️ よくある誤解

関連ノート