Mímisbrunnr知恵の泉

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

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

📎 前提:サプライチェーンの構造とトレードオフ(部分最適≠全体最適)・定量発注と定期発注(order-up-to / 定期発注 SS)・需要予測の枠組みと移動平均(移動平均予測と遅れ) | 関連:在庫集約とリスクプーリング | 次:在庫集約とリスクプーリング

要点(BLUF)

1. ブルウィップ効果とは:上流ほど暴れる

ビールの流通を題材にした有名な「ビールゲーム」(MIT)では、顧客需要がほんの少し増減しただけなのに、小売 → 卸 → 製造と遡るほど発注が乱高下し、在庫が過剰と欠品を激しく往復します。これがブルウィップ効果です。

flowchart LR
  CUS["顧客"] -->|"実需(小さな変動)"| RET["小売"]
  RET -->|"発注(中)"| WHL["卸"]
  WHL -->|"発注(大)"| DST["流通"]
  DST -->|"発注(特大)"| MFG["製造"]

各段は自分の下流から来た発注を需要とみなし、それを予測して自分の発注を決めます。問題は、発注は実需そのものではないこと。下流が予測のために少し上乗せ・前倒しした発注を、上流が「これが需要だ」と受け取ってまた上乗せする——この入れ子の上乗せが、上流へ行くほど積み重なります。

増幅の大きさは分散比で測ります。段 kk が受けた需要の分散を Var(D)\mathrm{Var}(D)、上流へ出した発注の分散を Var(q)\mathrm{Var}(q) とすると、

BW=Var(q)Var(D)\mathrm{BW}=\frac{\mathrm{Var}(q)}{\mathrm{Var}(D)}

1 を超えれば増幅、1 なら素通しです。ブルウィップは「各段の BW\mathrm{BW} が掛け算で積み上がる」現象だと言えます。

2. なぜ起きるか:order-up-to + 移動平均から導く

増幅は気のゆるみではなく方策の数理から出ます。下流の発注を需要とみなす1つの段が、定量発注と定期発注order-up-to 方策で補充するとしましょう。手順は3つだけ:

  1. 予測:直近 pp 期の需要を移動平均して平均需要を見積もる(需要予測の枠組みと移動平均)。
μ^t=1pi=1pDti\hat\mu_t=\frac{1}{p}\sum_{i=1}^{p} D_{t-i}
  1. 目標在庫:リードタイム LL 期間の需要をまかなう order-up-to 水準を St=Lμ^t+(一定の安全在庫)S_t=L\,\hat\mu_t+(\text{一定の安全在庫}) とする。
  2. 発注:売れたぶんを補い、目標水準のズレを埋める。在庫ポジションは前期の St1S_{t-1} から実需 Dt1D_{t-1} だけ減っているので、
qt=Dt1+(StSt1)q_t=D_{t-1}+\bigl(S_t-S_{t-1}\bigr)

ここで目標水準の変化は、予測の変化だけで決まります。StSt1=L(μ^tμ^t1)S_t-S_{t-1}=L(\hat\mu_t-\hat\mu_{t-1}) で、移動平均の差は窓から1つ出て1つ入る差なので μ^tμ^t1=1p(Dt1Dt1p)\hat\mu_t-\hat\mu_{t-1}=\dfrac{1}{p}(D_{t-1}-D_{t-1-p})。よって

qt=Dt1+Lp(Dt1Dt1p)=(1+Lp)Dt1LpDt1pq_t=D_{t-1}+\frac{L}{p}\bigl(D_{t-1}-D_{t-1-p}\bigr) =\Bigl(1+\frac{L}{p}\Bigr)D_{t-1}-\frac{L}{p}D_{t-1-p}

発注 qtq_t は、最新需要 Dt1D_{t-1}1+L/p1+L/p 倍に拡大して上流へ渡しています(さらに pp 期前を差し引く)。これが増幅の正体——予測の更新ぶんだけ発注が需要より大きく動くのです。

増幅率の公式(Chen ら)

需要 DD が独立同分布(iid、分散 σ2\sigma^2)なら、Dt1D_{t-1}Dt1pD_{t-1-p} は独立なので分散は足し算で、

Var(q)=[(1+Lp)2+(Lp)2]σ2=[1+2Lp+2L2p2]σ2\mathrm{Var}(q)=\Bigl[\Bigl(1+\frac{L}{p}\Bigr)^2+\Bigl(\frac{L}{p}\Bigr)^2\Bigr]\sigma^2 =\Bigl[1+\frac{2L}{p}+\frac{2L^2}{p^2}\Bigr]\sigma^2

したがって増幅率は

BW=Var(q)Var(D)=1+2Lp+2L2p2  (1)\mathrm{BW}=\frac{\mathrm{Var}(q)}{\mathrm{Var}(D)}=1+\frac{2L}{p}+\frac{2L^2}{p^2}\ \ (\ge 1)

これが Chen, Drezner, Ryan, Simchi-Levi (2000) の結果です。読み取れることは明快——リードタイム LL が長いほど(2L/p2L/p2L2/p22L^2/p^2 が増える)、予測窓 pp が短いほど(分母が小さい)増幅が強いL=0L=0(即納)なら BW=1\mathrm{BW}=1(増幅なし)、pp\to\infty(過去すべてで平均=反応を鈍く)でも BW1\mathrm{BW}\to1速い補充と鈍い反応が増幅を抑えることが式から直接わかります。

3. 単段の増幅率:シミュレーションは公式に一致するか(コード)

理論式 1+2L/p+2L2/p21+2L/p+2L^2/p^2 が正しいか、iid 需要を流して実測の Var(q)/Var(D)\mathrm{Var}(q)/\mathrm{Var}(D) と突き合わせます。発注は導いた qt=Dt1+(L/p)(Dt1Dt1p)q_t=D_{t-1}+(L/p)(D_{t-1}-D_{t-1-p}) をそのまま使い、(L,p)(L,p) をいろいろ変えます。

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)

def bullwhip_sim(L, p, n=2_000_000, mu=100.0, sigma=20.0):
    """単段 order-up-to(移動平均 MA(p) 予測・リードタイム L・iid 需要)。
    発注 q_t = D_{t-1} + (L/p)(D_{t-1} - D_{t-1-p})。増幅率 Var(q)/Var(D) を返す。"""
    D = rng.normal(mu, sigma, size=n)
    i = np.arange(p, n)
    q = D[i] + (L / p) * (D[i] - D[i - p])
    return np.var(q) / np.var(D)

def bullwhip_formula(L, p):
    return 1 + 2 * L / p + 2 * L**2 / p**2

rows = []
for (L, p) in [(1, 4), (2, 4), (4, 4), (8, 4), (4, 2), (4, 8), (4, 16)]:
    sim = bullwhip_sim(L, p)
    f = bullwhip_formula(L, p)
    rows.append({"L": L, "p": p, "シミュ Var(q)/Var(D)": sim,
                 "Chen式 1+2L/p+2L^2/p^2": f, "差": sim - f})
df = pd.DataFrame(rows)
print(df.to_string(index=False, float_format=lambda x: f"{x:.4f}"))

出力:

 L  p  シミュ Var(q)/Var(D)  Chen式 1+2L/p+2L^2/p^2       差
 1  4             1.6253                 1.6250  0.0003
 2  4             2.4991                 2.5000 -0.0009
 4  4             5.0005                 5.0000  0.0005
 8  4            13.0059                13.0000  0.0059
 4  2            13.0075                13.0000  0.0075
 4  8             2.4989                 2.5000 -0.0011
 4 16             1.6249                 1.6250 -0.0001

出力の意味:シミュレーションの実測増幅率は、すべての (L,p)(L,p)Chen の式に小数第3位までほぼ一致しました(差は ±0.008\pm0.008 以内、200万サンプルのばらつきのみ)。読み取れる構造は3つ。①必ず 1 より大きい——どの段も需要より暴れた発注を出す。②リードタイム LL が長いほど増幅大p=4p=4 固定で L=1248L=1\to2\to4\to8 と倍にすると BW=1.632.505.0013.0\mathrm{BW}=1.63\to2.50\to5.00\to13.0 と跳ね上がる。③予測窓 pp が長いほど増幅小L=4L=4 固定で p=24816p=2\to4\to8\to16 と窓を広げると BW=13.05.002.501.63\mathrm{BW}=13.0\to5.00\to2.50\to1.63 と鎮まる。同じ BW=13\mathrm{BW}=13 が「L=8,p=4L=8,p=4」でも「L=4,p=2L=4,p=2」でも出る通り、効くのはリードタイムと予測窓の比です。短いリードタイム(速い補充)と長い予測窓(鈍い反応)が増幅を抑える——緩和策はすべてこの式の上にあります。

4. 多段で複利的に膨らむ:4段直列SC(コード)

単段で BW>1\mathrm{BW}>1 なら、それが段の数だけ掛け算されます。顧客需要(iid)を起点に、小売 → 卸 → 流通 → 製造の4段を直列につなぎ、各段が下流の発注を自分の需要として同じ order-up-to で補充する様子をシミュレートします。各段の発注分散が、顧客需要に対して何倍に膨らむかを見ます。

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

rng = np.random.default_rng(7)

def order_up_to(incoming, L, p, mu=100.0):
    """下流からの受注 incoming を需要として order-up-to(MA(p), リードL) で発注。
    q_t = incoming_{t-1} + (L/p)(incoming_{t-1} - incoming_{t-1-p})。"""
    n = len(incoming)
    q = np.empty(n)
    q[:p + 1] = mu                      # バーンイン(最初の p+1 期は平均で埋める)
    t = np.arange(p + 1, n)
    q[t] = incoming[t - 1] + (L / p) * (incoming[t - 1] - incoming[t - 1 - p])
    return q

n = 300000
mu, sigma = 100.0, 20.0
L, p = 3, 5
burn = 2000
cust = rng.normal(mu, sigma, size=n)   # 顧客需要(iid)
stages = [cust]
for _ in range(4):                      # 4段:小売→卸→流通→製造
    stages.append(order_up_to(stages[-1], L, p, mu))

labels = ["顧客需要", "小売の発注", "卸の発注", "流通の発注", "製造の発注"]
var0 = np.var(cust[burn:])
print(f"設定:4段直列・リードL={L}・予測窓p={p}・iid需要 sigma={sigma}")
print(f"{'段':10s} 発注分散      対顧客比(増幅)")
amp = []
for lab, s in zip(labels, stages):
    v = np.var(s[burn:])
    amp.append(v / var0)
    print(f"{lab:10s} {v:10.2f}    {v / var0:8.3f}")

# 予測窓 p・リード L を変えて「製造(最上流)」の増幅がどう動くか
ps = [2, 3, 5, 8, 12]
amp_grid = {}
grid_rows = []
for Lx in [2, 4]:
    vals = []
    for pp in ps:
        st = [cust]
        for _ in range(4):
            st.append(order_up_to(st[-1], Lx, pp, mu))
        vals.append(np.var(st[-1][burn:]) / var0)
    amp_grid[Lx] = vals
    grid_rows.append({"リードL": Lx, **{f"p={pp}": v for pp, v in zip(ps, vals)}})
print("\n[最上流(製造)の増幅率:予測窓 p とリード L の効果]")
print(pd.DataFrame(grid_rows).to_string(index=False, float_format=lambda x: f"{x:.1f}"))

# 図:左=段ごとの増幅(棒)、右=製造の増幅 vs 予測窓 p(L別の線)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5.2))
colors = ["#7f7f7f", "#9ecae1", "#6baed6", "#3182bd", "#08519c"]
ax1.bar(labels, amp, color=colors, edgecolor="white")
for x, a in enumerate(amp):
    ax1.text(x, a + 0.1, f"{a:.2f}", ha="center", fontsize=10)
ax1.set_ylabel("対・顧客需要の分散比(増幅率)")
ax1.set_title(f"上流ほど発注が暴れる(L={L}, p={p})")
ax1.tick_params(axis="x", rotation=20)

for Lx, mk in [(2, "o-"), (4, "s-")]:
    ax2.plot(ps, amp_grid[Lx], mk, lw=2, label=f"リード L={Lx}")
ax2.set_yscale("log")
ax2.set_xlabel("予測窓 p(移動平均の期間)")
ax2.set_ylabel("最上流(製造)の増幅率(対数軸)")
ax2.set_title("予測窓を長く・リードを短くすると増幅は和らぐ")
ax2.legend(); ax2.grid(alpha=0.3, which="both")
plt.tight_layout(); plt.show()

出力:

設定:4段直列・リードL=3・予測窓p=5・iid需要 sigma=20.0
段          発注分散      対顧客比(増幅)
顧客需要           399.12       1.000
小売の発注         1164.47       2.918
卸の発注          4134.17      10.358
流通の発注        16359.71      40.990
製造の発注        68579.04     171.828

[最上流(製造)の増幅率:予測窓 p とリード L の効果]
 リードL      p=2    p=3   p=5  p=8  p=12
    2   1920.0  271.2  37.7 10.1   4.6
    4 109300.4 9357.1 626.0 83.0  21.4

出力の意味:顧客需要の分散を 1 とすると、上流へ行くごとに増幅が積み上がり、小売 2.92 → 卸 10.4 → 流通 41.0 → 製造 172 と膨らみます。製造段が受け取る発注の分散は、顧客需要の約 172 倍——同じものを売っているのに、製造には嵐のような注文の波が届くわけです。

ここで効くのが第3節との接続です。小売段の増幅 2.918 は、ちょうど Chen の iid 公式に一致します:1+235+23252=1+1.2+0.72=2.921+\dfrac{2\cdot3}{5}+\dfrac{2\cdot3^2}{5^2}=1+1.2+0.72=2.92。小売の入力(顧客需要)は iid なので、公式どおり。ところが上流ほど1段あたりの増幅が大きくなります(卸以降の段間倍率は 3.55,3.96,4.193.55,\,3.96,\,4.19 と増える)。理由は、上流が受け取る発注はもう iid ではなく正に自己相関しているから——移動平均の差分が大きく振れ、2.922.92 より強く増幅するのです。だから多段の総増幅は単純な 2.924722.92^4\approx72 ではなく、それを超える 172 になります。

下段の表は緩和の梃子です。最上流の増幅は、予測窓 pp を長くする(反応を鈍く)と劇的に下がり(L=4L=4p=2p=2109300109300p=12p=1221.421.4)、リードタイム LL を短くすると下がる(p=5p=5L=4L=4626626L=2L=237.737.7)。桁で効くので、ブルウィップ対策は「気合い」ではなくリードタイム短縮と予測の安定化という構造への介入だとわかります。

5. 4つの原因と緩和策

第2〜4節は①需要シグナル処理(予測への過剰反応)だけを数理化しましたが、Lee, Padmanabhan, Whang (1997) は現実の原因を4つ挙げます。いずれも「実需が見えない/反応が過剰になる」点で共通です。

原因中身緩和策
①需要シグナル処理下流の発注を需要と誤認し、予測を過剰更新(本稿の公式)POS(実需)の共有・予測窓を長く・需要の協調計画(CPFR)
②発注のバッチ化固定費 経済的発注量EOQ でまとめ発注 → 上流から見ると断続的な大波小ロット化・混載・発注固定費の削減
③価格変動販促・値引きで前倒し購入(forward buying) → 需要が実需と乖離EDLP(毎日同価格)・販促の抑制
④品薄時の水増し発注供給逼迫時に割当を見越して多めに発注(shortage gaming)過去実績ベースの割当・返品自由の抑制・情報開示

共通する処方箋は 「実需を見せる・反応を鈍くする・ロットを小さくする・価格を安定させる」。とりわけPOS データの上流共有は、各段が「下流の発注」ではなく「最終顧客の実需」を直接予測できるようにし、入れ子の上乗せを根元から断つので効果が大きい。これは サプライチェーンの構造とトレードオフ で触れた部分最適≠全体最適の解——各段の局所最適をやめ、実需という共通情報で全体最適に寄せる打ち手です。

⚠️ よくある誤解

関連ノート