Mímisbrunnr知恵の泉

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

🎓 レベル:基礎 | 重要度:A(必須)

📎 前提:プロセス分析とリトルの法則(平均在庫 Q/2 とリトルの法則) | 次:安全在庫と発注点(不確実性への備え) | 発展:第9章 確率的在庫モデル

要点(BLUF)

1. 在庫の2大コストと「のこぎり波」

EOQ が答える問いはシンプルです——1回にいくつ発注すれば、年間の在庫関連コストが最小になるか。登場するコストは2つだけです。

保管コストには平均在庫が要ります。EOQ の標準前提は需要が一定補充は瞬時(リードタイム0で発注したら即届く)。すると在庫は QQ から一定の傾きで減って0になり、0になった瞬間に QQ だけ補充される——のこぎり波を描きます。各サイクルで在庫は QQ と0の間を直線で往復するので、時間平均の在庫は Q2\dfrac{Q}{2}。年間保管コストは Q2H\dfrac{Q}{2}H です。

リトルの法則との接続:平均在庫 Iˉ=Q2\bar I=\dfrac{Q}{2}プロセス分析とリトルの法則I=RTI=R\,T そのものです。需要(フローレート)R=DR=D、1個が倉庫に滞在する平均時間 T=Q/2D×(年)T=\dfrac{Q/2}{D}\times\text{(年)} と見れば I=RT=Q2I=R\,T=\dfrac{Q}{2}。在庫はフローの言葉で読み解けます。

flowchart LR
  Q["1回の発注量 Q を増やす"] --> A["発注回数 D/Q が減る<br/>発注コスト D/Q·S は減少"]
  Q --> B["平均在庫 Q/2 が増える<br/>保管コスト Q/2·H は増加"]
  A --> TC["総コスト TC(Q)"]
  B --> TC
  TC --> OPT["最小にする Q* = EOQ"]

2. EOQ の導出:総コストを微分する

年間総コストは2つの和です。

TC(Q)=DQS発注コスト+Q2H保管コストTC(Q)=\underbrace{\frac{D}{Q}S}_{\text{発注コスト}}+\underbrace{\frac{Q}{2}H}_{\text{保管コスト}}

第1項は QQ に対して減少1/Q1/Q)、第2項は増加QQ に比例)。和は下に凸の谷型になり、谷底が最適です。QQ で微分してゼロと置きます。

dTCdQ=DSQ2+H2=0\frac{dTC}{dQ}=-\frac{DS}{Q^2}+\frac{H}{2}=0 DSQ2=H2        Q2=2DSH        Q\*=2DSH\frac{DS}{Q^2}=\frac{H}{2}\;\;\Longrightarrow\;\; Q^2=\frac{2DS}{H}\;\;\Longrightarrow\;\; \boxed{\,Q^\*=\sqrt{\frac{2DS}{H}}\,}

2階微分は d2TCdQ2=2DSQ3>0\dfrac{d^2TC}{dQ^2}=\dfrac{2DS}{Q^3}>0 なので、確かに最小です(TCTCQ>0Q>0 で凸)。

最適点では発注コストと保管コストが等しい——これは偶然ではありません。dTCdQ=0\dfrac{dTC}{dQ}=0DSQ2=H2\dfrac{DS}{Q^2}=\dfrac{H}{2}、両辺に QQ を掛けると DSQ=QH2\dfrac{DS}{Q}=\dfrac{QH}{2}、すなわち発注コスト=保管コストQ\*=2DS/HQ^\*=\sqrt{2DS/H} を代入すると、どちらも DSH2\sqrt{\dfrac{DSH}{2}} になり、最小総コストは

TC\*=DQ\*S+Q\*2H=2DSH2=2DSHTC^\*=\frac{D}{Q^\*}S+\frac{Q^\*}{2}H=2\sqrt{\frac{DSH}{2}}=\sqrt{2DSH}

ついでに、最適な年間発注回数 N\*=DQ\*=DH2SN^\*=\dfrac{D}{Q^\*}=\sqrt{\dfrac{DH}{2S}}発注サイクル(1回の発注がもつ日数)=Q\*D×365=\dfrac{Q^\*}{D}\times 365 日も決まります。

3. EOQ を計算して数値最適と突き合わせる(コード)

具体的な数値で確かめます。年間需要 D=12000D=12000 個、発注1回 S=80S=80 円、保管 H=6H=6 円/個/年。発注コスト・保管コスト・総コストの3曲線を描き、閉形式 Q\*Q^\*scipy.optimize.minimize_scalar の数値解が一致すること、最適点で発注コスト=保管コストになることを確認します。

import numpy as np
from scipy.optimize import minimize_scalar
import matplotlib.pyplot as plt
import japanize_matplotlib

# パラメータ:年間需要 D、1回あたり発注コスト S、年間・1個あたり保管コスト H
D = 12000.0     # 年間需要(個/年)
S = 80.0        # 1回あたり発注コスト(円/回)
H = 6.0         # 年間・1個あたり保管コスト(円/個/年)

# 総コスト TC(Q) = 発注コスト D/Q*S + 保管コスト Q/2*H
def order_cost(Q):  return D / Q * S
def hold_cost(Q):   return Q / 2.0 * H
def total_cost(Q):  return order_cost(Q) + hold_cost(Q)

# 閉形式の最適発注量 Q* = sqrt(2DS/H)、最小総コスト TC* = sqrt(2DSH)
Q_star = np.sqrt(2 * D * S / H)
TC_star = np.sqrt(2 * D * S * H)

# 数値最適化で裏取り(scipy.optimize.minimize_scalar)
res = minimize_scalar(total_cost, bounds=(1, 5000), method="bounded")
Q_num = res.x

print(f"閉形式  Q* = sqrt(2DS/H)      = {Q_star:.4f} 個")
print(f"数値解  Q* (minimize_scalar) = {Q_num:.4f} 個")
print(f"一致差  |閉形式 - 数値|       = {abs(Q_star - Q_num):.2e}")
print(f"最小総コスト TC* = sqrt(2DSH) = {TC_star:.4f} 円/年")
print(f"  数値解での TC(Q*)           = {total_cost(Q_num):.4f} 円/年")
print()
print(f"最適点での発注コスト D/Q*·S = {order_cost(Q_star):.4f} 円/年")
print(f"最適点での保管コスト Q*/2·H = {hold_cost(Q_star):.4f} 円/年  (発注=保管で一致)")
print(f"年間発注回数 D/Q*           = {D / Q_star:.4f} 回/年")
print(f"発注サイクル Q*/D           = {Q_star / D * 365:.2f} 日")

# 図:発注コスト・保管コスト・総コストの3曲線(交点が最適)
Q = np.linspace(50, 2000, 400)
plt.figure(figsize=(9, 5.5))
plt.plot(Q, order_cost(Q), label="発注コスト D/Q·S", color="#1f77b4")
plt.plot(Q, hold_cost(Q), label="保管コスト Q/2·H", color="#2ca02c")
plt.plot(Q, total_cost(Q), label="総コスト TC(Q)", color="#d62728", lw=2.5)
plt.axvline(Q_star, ls="--", color="gray")
plt.plot(Q_star, TC_star, "*", color="black", ms=18, label=f"最適 Q*={Q_star:.0f}")
plt.xlabel("発注量 Q(個)"); plt.ylabel("年間コスト(円/年)")
plt.title("EOQ:発注コストと保管コストのトレードオフ(交点が最適)")
plt.legend(); plt.ylim(0, 5000); plt.tight_layout(); plt.show()

出力:

閉形式  Q* = sqrt(2DS/H)      = 565.6854 個
数値解  Q* (minimize_scalar) = 565.6854 個
一致差  |閉形式 - 数値|       = 1.01e-05
最小総コスト TC* = sqrt(2DSH) = 3394.1125 円/年
  数値解での TC(Q*)           = 3394.1125 円/年

最適点での発注コスト D/Q*·S = 1697.0563 円/年
最適点での保管コスト Q*/2·H = 1697.0563 円/年  (発注=保管で一致)
年間発注回数 D/Q*           = 21.2132 回/年
発注サイクル Q*/D           = 17.21 日

出力の意味:閉形式 Q\*=21200080/6=565.69Q^\*=\sqrt{2\cdot12000\cdot80/6}=565.69 個と、数値最適化が探し当てた値が小数第4位まで一致(差は 10510^{-5} オーダー=最適化の収束許容誤差)。Q\*Q^\* では発注コストも保管コストもともに 1697.06 円/年でぴたりと等しく、合計が最小総コスト TC\*=2DSH=3394.11TC^\*=\sqrt{2DSH}=3394.11 円/年になります。図でも、青い発注コスト(右下がり)と緑の保管コスト(右上がり)が交わる真上で、赤い総コスト曲線が(★)を打っています。Q\*Q^\* ずつ年21.2回、約17日に1回発注すればよい、と運用に落ちます。「2つのコストが釣り合う点が最適」——これが EOQ の幾何学的な正体です。

4. EOQ は「平ら」:頑健性を導く(コード)

EOQ の隠れた美点は頑健さです。QQQ\*Q^\* から少々ずらしても、総コストはほとんど増えません。これは式で綺麗に言えます。r=Q/Q\*r=Q/Q^\* とおくと、

TC(Q)TC\*=DSQ+QH22DSH=12(Q\*Q+QQ\*)=12(1r+r)\frac{TC(Q)}{TC^\*}=\frac{\frac{DS}{Q}+\frac{QH}{2}}{\sqrt{2DSH}} =\frac{1}{2}\left(\frac{Q^\*}{Q}+\frac{Q}{Q^\*}\right)=\frac{1}{2}\left(\frac{1}{r}+r\right)

(途中:DSQ=1rDSH2\frac{DS}{Q}=\frac1r\sqrt{\frac{DSH}{2}}QH2=rDSH2\frac{QH}{2}=r\sqrt{\frac{DSH}{2}}TC\*=2DSH2TC^\*=2\sqrt{\frac{DSH}{2}} を代入。)D,S,HD,S,H式から消えて、コスト比は比率 rr だけの関数になります。r=1r=1 のまわりで r=1+xr=1+x と展開すると 12(r+1/r)1+x22\frac12(r+1/r)\approx 1+\dfrac{x^2}{2}——コスト増は相対誤差の2乗。だから小さなずれはほぼ無害(1次の項が消える=谷が平ら)です。数値で確かめます。

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

D, S, H = 12000.0, 80.0, 6.0
Q_star = np.sqrt(2 * D * S / H)
TC_star = np.sqrt(2 * D * S * H)

def total_cost(Q):
    return D / Q * S + Q / 2.0 * H

# Q を Q* の r 倍にずらしたときの総コスト比 TC(Q)/TC*
# 理論:TC(Q)/TC* = (1/2)(r + 1/r)   (r = Q/Q*)
ratios = np.array([0.5, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0])
rows = []
for r in ratios:
    Q = r * Q_star
    direct = total_cost(Q) / TC_star          # 直接計算した比
    formula = 0.5 * (r + 1.0 / r)             # 公式 (1/2)(r+1/r)
    rows.append({"Q/Q*": r, "TC/TC* 直接": direct,
                 "TC/TC* 公式": formula, "コスト増(%)": (direct - 1) * 100})
tbl = pd.DataFrame(rows)
print(tbl.to_string(index=False, float_format=lambda x: f"{x:.4f}"))
print()
print(f"+50%発注 (r=1.5): コスト増 {(0.5*(1.5+1/1.5)-1)*100:.2f}%")
print(f"-50%発注 (r=0.5): コスト増 {(0.5*(0.5+1/0.5)-1)*100:.2f}%")
print(f"+-20%発注       : コスト増 {(0.5*(1.2+1/1.2)-1)*100:.2f}% / {(0.5*(0.8+1/0.8)-1)*100:.2f}%")

# 図:正規化コスト曲線 (1/2)(r+1/r) と 2次近似 1+(r-1)^2/2
rr = np.linspace(0.4, 2.2, 300)
plt.figure(figsize=(9, 5.5))
plt.plot(rr, 0.5 * (rr + 1 / rr), color="#d62728", lw=2.5,
         label="正規化総コスト (1/2)(r+1/r)")
plt.plot(rr, 1 + (rr - 1) ** 2 / 2, ls="--", color="#1f77b4",
         label="2次近似 1+(r-1)²/2")
plt.axhline(1.0, color="gray", lw=0.8)
plt.axvline(1.0, color="gray", lw=0.8)
plt.fill_between(rr, 1.0, 1.03, where=(0.5 * (rr + 1 / rr) <= 1.03),
                 color="green", alpha=0.15, label="コスト増 3%以内の帯")
plt.xlabel("発注量比 r = Q/Q*"); plt.ylabel("総コスト比 TC(Q)/TC*")
plt.title("EOQは「平ら」:±20%ずれても総コストは数%しか増えない")
plt.legend(); plt.ylim(0.98, 1.30); plt.tight_layout(); plt.show()

出力:

  Q/Q*  TC/TC* 直接  TC/TC* 公式  コスト増(%)
0.5000     1.2500     1.2500  25.0000
0.7000     1.0643     1.0643   6.4286
0.8000     1.0250     1.0250   2.5000
0.9000     1.0056     1.0056   0.5556
1.0000     1.0000     1.0000   0.0000
1.1000     1.0045     1.0045   0.4545
1.2000     1.0167     1.0167   1.6667
1.5000     1.0833     1.0833   8.3333
2.0000     1.2500     1.2500  25.0000

+50%発注 (r=1.5): コスト増 8.33%
-50%発注 (r=0.5): コスト増 25.00%
+-20%発注       : コスト増 1.67% / 2.50%

出力の意味:「直接計算したコスト比」と「公式 12(r+1/r)\frac12(r+1/r)」は全行ぴたり一致——導出が正しいことの確認です。注目は谷の平らさで、±20%\pm20\% ずらしても総コスト増はわずか 1.67%(+20%)/2.50%(−20%)。発注量を1〜2割読み違えても、コストはほとんど変わりません。さらに +50% も+8.3% にとどまります。ただし完全な対称ではありません——rr1/r1/r が同じコストになる(過剰発注 r=1.5r=1.5 と過少発注 r=0.667r=0.667 が同コスト)ため、−50%(r=0.5r=0.5)は+25% と過少側のほうが急。Q0Q\to0 で発注回数が爆発するからです。図では赤い実線(厳密)と青い破線(2次近似 1+(r1)2/21+(r-1)^2/2)が谷の近くでほぼ重なり、緑の帯(コスト増3%以内)が r0.781.28r\approx0.78\sim1.28幅広いことが見えます。結論:EOQ は桁を合わせる道具D,S,HD,S,H の見積りが多少甘くても、 \sqrt{\ } がならして Q\*Q^\* への影響を  \sqrt{\ } 倍に薄め、さらにコストへの影響は2乗で効くので、実務では端数を丸めて発注ロットに合わせて構いません。

⚠️ よくある誤解

関連ノート