🎓 レベル:標準 | 重要度:A(必須)
📎 前提:サプライチェーンの構造とトレードオフ(部分最適≠全体最適)・定量発注と定期発注(order-up-to / 定期発注 )・需要予測の枠組みと移動平均(移動平均予測と遅れ) | 関連:在庫集約とリスクプーリング | 次:在庫集約とリスクプーリング
要点(BLUF)
- ブルウィップ効果(bullwhip effect)とは、需要のばらつきがサプライチェーンを上流へ遡るほど増幅される現象です。顧客の実需はわずかしか揺れていないのに、小売の発注はもっと、卸はさらに、製造への発注は激しく振れる——鞭(whip)の手元の小さな動きが先端で大きくしなる様子になぞらえます。
- 増幅は分散比で測ります:。 なら、その段は受けた需要よりも暴れた発注を上流へ出している=増幅している、ということです。
- 怖いのは、各段が誰も間違っていなくても、構造的に起きること。下流の発注しか見えない上流が、移動平均で予測して 定量発注と定期発注 の order-up-to で補充する——この合理的な行動だけで増幅が生まれます。
- 単段の理論増幅率は、移動平均(窓 )・リードタイム の order-up-to で (Chen ら)。リードタイムが長い・予測窓が短いほど増幅が強い。これが多段で複利的に積み上がるので、製造段では桁違いに膨らみます。
- 原因は4つ——①需要シグナル処理(予測への過剰反応)②発注のバッチ化③価格変動(販促)④品薄時の水増し発注。緩和はPOS(実需)共有・リードタイム短縮・小ロット化・EDLP(価格安定)。要は実需を見せ、反応を鈍く、ロットを小さく。
1. ブルウィップ効果とは:上流ほど暴れる
ビールの流通を題材にした有名な「ビールゲーム」(MIT)では、顧客需要がほんの少し増減しただけなのに、小売 → 卸 → 製造と遡るほど発注が乱高下し、在庫が過剰と欠品を激しく往復します。これがブルウィップ効果です。
flowchart LR CUS["顧客"] -->|"実需(小さな変動)"| RET["小売"] RET -->|"発注(中)"| WHL["卸"] WHL -->|"発注(大)"| DST["流通"] DST -->|"発注(特大)"| MFG["製造"]
各段は自分の下流から来た発注を需要とみなし、それを予測して自分の発注を決めます。問題は、発注は実需そのものではないこと。下流が予測のために少し上乗せ・前倒しした発注を、上流が「これが需要だ」と受け取ってまた上乗せする——この入れ子の上乗せが、上流へ行くほど積み重なります。
増幅の大きさは分散比で測ります。段 が受けた需要の分散を 、上流へ出した発注の分散を とすると、
が 1 を超えれば増幅、1 なら素通しです。ブルウィップは「各段の が掛け算で積み上がる」現象だと言えます。
2. なぜ起きるか:order-up-to + 移動平均から導く
増幅は気のゆるみではなく方策の数理から出ます。下流の発注を需要とみなす1つの段が、定量発注と定期発注 の order-up-to 方策で補充するとしましょう。手順は3つだけ:
- 予測:直近 期の需要を移動平均して平均需要を見積もる(需要予測の枠組みと移動平均)。
- 目標在庫:リードタイム 期間の需要をまかなう order-up-to 水準を とする。
- 発注:売れたぶんを補い、目標水準のズレを埋める。在庫ポジションは前期の から実需 だけ減っているので、
ここで目標水準の変化は、予測の変化だけで決まります。 で、移動平均の差は窓から1つ出て1つ入る差なので 。よって
発注 は、最新需要 を 倍に拡大して上流へ渡しています(さらに 期前を差し引く)。これが増幅の正体——予測の更新ぶんだけ発注が需要より大きく動くのです。
増幅率の公式(Chen ら)
需要 が独立同分布(iid、分散 )なら、 と は独立なので分散は足し算で、
したがって増幅率は
これが Chen, Drezner, Ryan, Simchi-Levi (2000) の結果です。読み取れることは明快——リードタイム が長いほど( と が増える)、予測窓 が短いほど(分母が小さい)増幅が強い。(即納)なら (増幅なし)、(過去すべてで平均=反応を鈍く)でも 。速い補充と鈍い反応が増幅を抑えることが式から直接わかります。
3. 単段の増幅率:シミュレーションは公式に一致するか(コード)
理論式 が正しいか、iid 需要を流して実測の と突き合わせます。発注は導いた をそのまま使い、 をいろいろ変えます。
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
出力の意味:シミュレーションの実測増幅率は、すべての で Chen の式に小数第3位までほぼ一致しました(差は 以内、200万サンプルのばらつきのみ)。読み取れる構造は3つ。①必ず 1 より大きい——どの段も需要より暴れた発注を出す。②リードタイム が長いほど増幅大: 固定で と倍にすると と跳ね上がる。③予測窓 が長いほど増幅小: 固定で と窓を広げると と鎮まる。同じ が「」でも「」でも出る通り、効くのはリードタイムと予測窓の比です。短いリードタイム(速い補充)と長い予測窓(鈍い反応)が増幅を抑える——緩和策はすべてこの式の上にあります。
4. 多段で複利的に膨らむ:4段直列SC(コード)
単段で なら、それが段の数だけ掛け算されます。顧客需要(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 公式に一致します:。小売の入力(顧客需要)は iid なので、公式どおり。ところが上流ほど1段あたりの増幅が大きくなります(卸以降の段間倍率は と増える)。理由は、上流が受け取る発注はもう iid ではなく正に自己相関しているから——移動平均の差分が大きく振れ、 より強く増幅するのです。だから多段の総増幅は単純な ではなく、それを超える 172 になります。
下段の表は緩和の梃子です。最上流の増幅は、予測窓 を長くする(反応を鈍く)と劇的に下がり( で の が で )、リードタイム を短くすると下がる( で の が で )。桁で効くので、ブルウィップ対策は「気合い」ではなくリードタイム短縮と予測の安定化という構造への介入だとわかります。
5. 4つの原因と緩和策
第2〜4節は①需要シグナル処理(予測への過剰反応)だけを数理化しましたが、Lee, Padmanabhan, Whang (1997) は現実の原因を4つ挙げます。いずれも「実需が見えない/反応が過剰になる」点で共通です。
| 原因 | 中身 | 緩和策 |
|---|---|---|
| ①需要シグナル処理 | 下流の発注を需要と誤認し、予測を過剰更新(本稿の公式) | POS(実需)の共有・予測窓を長く・需要の協調計画(CPFR) |
| ②発注のバッチ化 | 固定費 経済的発注量EOQ でまとめ発注 → 上流から見ると断続的な大波 | 小ロット化・混載・発注固定費の削減 |
| ③価格変動 | 販促・値引きで前倒し購入(forward buying) → 需要が実需と乖離 | EDLP(毎日同価格)・販促の抑制 |
| ④品薄時の水増し発注 | 供給逼迫時に割当を見越して多めに発注(shortage gaming) | 過去実績ベースの割当・返品自由の抑制・情報開示 |
共通する処方箋は 「実需を見せる・反応を鈍くする・ロットを小さくする・価格を安定させる」。とりわけPOS データの上流共有は、各段が「下流の発注」ではなく「最終顧客の実需」を直接予測できるようにし、入れ子の上乗せを根元から断つので効果が大きい。これは サプライチェーンの構造とトレードオフ で触れた部分最適≠全体最適の解——各段の局所最適をやめ、実需という共通情報で全体最適に寄せる打ち手です。
⚠️ よくある誤解
- 「ブルウィップは誰かのミスで起きる」ではない:各段が合理的に予測し合理的に補充しても、構造的に起きます(第2節の公式は最適に近い order-up-to から出る)。犯人探しではなく、リードタイム・予測窓・ロット・価格・情報共有という構造を変えるのが対策です。
- 「需要予測を精緻にすれば消える」ではない:むしろ予測に敏感に反応するほど増幅は強まります( を小さくすると 増)。精度を上げるより、実需(POS)を上流と共有して各段が同じ実需を見ることと、反応を適度に鈍くすることが効きます。過剰反応こそ根因。
- 「在庫を増やせば対処できる」ではない:在庫の積み増しは欠品を一時しのぎするだけで、変動の増幅そのものは消えません。むしろ過剰在庫と欠品の往復(牛の鞭の振れ)を助長します。効くのは変動を発生源で抑えること——リードタイム短縮・小ロット・EDLP・情報共有。
- 「増幅は一定倍で素直に伝わる」ではない:単段は ですが、多段では上流の入力が自己相関するため、段あたりの増幅が上流ほど大きくなり、総増幅は単段倍率の単純な累乗を超えます(コード4: ではなく )。だから長いチェーンほど末端の暴れは想像以上です。
- 「これは在庫の話で品質や能力とは無関係」ではない:暴れた発注は、製造の能力(ボトルネックとキャパシティ)を過剰/過少に振り回し、残業と遊休、急ぎ替えによる品質の乱れまで波及します。ブルウィップ抑制は在庫だけでなく、能力稼働と品質の安定にも効きます。
関連ノート
- サプライチェーンの構造とトレードオフ(部分最適≠全体最適。情報共有はフロンティアを動かす打ち手)
- 定量発注と定期発注(order-up-to / 定期発注 。本稿の補充方策の土台)
- 需要予測の枠組みと移動平均(移動平均予測と遅れ。 を変えると反応の速さが変わる)
- 経済的発注量EOQ(発注のバッチ化=ブルウィップ原因②の発注固定費)
- 在庫集約とリスクプーリング(次のトピック・ばらつきを束ねて減らす別の打ち手)
- オペレーションズ・マネジメント 全体目次