Skip to the content.

← 目次← 前: 14次: 16 →

15. オペレーションズ・リサーチ入門

テーマ: 経済的発注量(EOQ)モデルと安全在庫・発注点

学習点: 最適化の解析解と数値探索の比較, math, 正規分布のパーセント点

依存: SciPy / 難易度: 中級

実行方法

uv run 15_eoq_inventory.py

スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは uv が自動的に仮想環境へ導入します。事前の pip install は不要です。

解説

何をするプログラムか

在庫管理には相反する 2 つの費用があります。1 回の発注量を増やせば発注回数が減って発注費用は下がりますが、平均在庫が増えて保管費用が上がります。この綱引きの最適点を与えるのが経済的発注量(EOQ: Economic Order Quantity)モデルで、オペレーションズ・リサーチの最も古典的な成果の 1 つです。

このスクリプトは、年間需要 24,000 個・発注費 15,000 円/回・保管費 240 円/個年という設定で最適発注量 Q* を解析解と数値探索の両方で求め、発注量を誤って設定した場合の費用増(モデルの頑健性)、需要の不確実性に備える安全在庫と発注点、そして数量割引がある場合の意思決定までを一気に扱います。

コードの読みどころ

理論的背景

年間総費用は TC(Q) = (D/Q)·K + (Q/2)·h です。第 1 項は発注回数 D/Q × 発注費、第 2 項は平均在庫 Q/2 × 保管費を表します。Q で微分して 0 とおくと Q* = √(2DK/h) が得られます(Harris 1913、いわゆる Wilson 公式)。最適点では 2 つの費用がちょうど等しくなること、そして TC が Q* の近くで平坦な形をしていることがこのモデルの重要な含意です。

需要が不確実な場合は、リードタイム中の需要が平均 μ_L・標準偏差 σ_L の正規分布に従うと仮定し、品切れを起こさない確率(サービス率)に対応する z 値を使って安全在庫 SS = z·σ_L、発注点 = μ_L + SS を決めます。

実行結果の見方

最適発注量は Q* = 1,732.1 個、最小総費用は 415,692 円で、内訳の発注費と保管費がともに 207,846 円と一致しています。1 個刻みの全探索でも Q = 1732 で同じ費用となり、解析解が正しいことを数値で確認できます。頑健性の表では、Q* から ±10% ずれても費用増は 1% 未満、半分や 2 倍にしても +25% にとどまります。EOQ を厳密に守ることより「桁を間違えないこと」が重要だと分かります。

安全在庫の表では、サービス率を 90% から 99.9% へ上げると安全在庫が 40.7 個から 98.1 個へ増え、特に 99% → 99.9% の 1 段で追加保管費が約 5,800 円も増える、つまり高いサービス率の限界費用は逓増することが読み取れます。数量割引の比較では、Q* = 1,732 がもともと割引条件の 1,000 個を超えているため、EOQ のままが総費用最小(28,345,457 円)という結論になります。

ソースコード

# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "scipy",
# ]
# ///
"""15: オペレーションズ・リサーチ入門 -------------------------------------
テーマ: 経済的発注量(EOQ)モデルと安全在庫・発注点
学習点: 最適化の解析解と数値探索の比較, math, 正規分布のパーセント点
根拠: 総費用 TC(Q) = D/Q·K + Q/2·h を Q で微分して 0 とおくと
      Q* = √(2DK/h)(Harris 1913, Wilson 公式)。TC は Q* で最小、
      かつ発注費用と在庫保管費用が一致する。
      安全在庫 SS = z·σ_L(σ_L はリードタイム中の需要標準偏差)。
"""
import math
from scipy import stats


def eoq(D: float, K: float, h: float) -> float:
    """年間需要D, 1回あたり発注費K, 単位年間保管費h に対する最適発注量。"""
    return math.sqrt(2 * D * K / h)


def total_cost(Q: float, D: float, K: float, h: float) -> float:
    return D / Q * K + Q / 2 * h


def main() -> None:
    D, K, h, unit = 24_000, 15_000, 240, 1_200
    q = eoq(D, K, h)
    print(f"年間需要 {D:,}個 / 発注費 {K:,}円/回 / 保管費 {h:,}円/個年")
    print(f"最適発注量 Q* = {q:,.1f} 個")
    print(f"年間発注回数 = {D/q:.2f} 回 / 発注間隔 = {365/(D/q):.1f} 日")
    print(f"最小総費用 = {total_cost(q, D, K, h):,.0f} 円")
    print(f"  内訳: 発注費 {D/q*K:,.0f}円 = 保管費 {q/2*h:,.0f}円 "
          "(EOQでは必ず一致する)\n")

    print("[数値探索による検証]")
    best = min(range(100, 5000, 1), key=lambda x: total_cost(x, D, K, h))
    print(f"  1個刻みの全探索: Q = {best} 個, TC = "
          f"{total_cost(best, D, K, h):,.0f} 円(解析解と一致)\n")

    print("[Qを誤って設定した場合の費用増(EOQの頑健性)]")
    tc_opt = total_cost(q, D, K, h)
    for ratio in [0.5, 0.7, 0.9, 1.0, 1.1, 1.5, 2.0]:
        tc = total_cost(q * ratio, D, K, h)
        print(f"  Q = {ratio:>4.0%}·Q*: TC = {tc:>10,.0f}円 "
              f"({tc/tc_opt - 1:>+6.2%})")
    print("  ※ ±30%程度のずれでも費用増は数%に留まる(TCがQ*近傍で平坦なため)。\n")

    print("[安全在庫と発注点]")
    daily_mu, daily_sd, lead = D / 365, 12.0, 7
    mu_L = daily_mu * lead
    sd_L = daily_sd * math.sqrt(lead)      # 独立を仮定すると分散は加法的
    print(f"  日次需要 平均 {daily_mu:.1f}個 / 標準偏差 {daily_sd}個 / "
          f"リードタイム {lead}日")
    print(f"  リードタイム中需要: 平均 {mu_L:.1f}個 / 標準偏差 {sd_L:.2f}個")
    print(f"\n  {'サービス率':>10}{'z値':>8}{'安全在庫':>10}{'発注点':>10}"
          f"{'追加保管費/年':>14}")
    for sl in [0.90, 0.95, 0.98, 0.99, 0.999]:
        z = stats.norm.ppf(sl)
        ss = z * sd_L
        print(f"  {sl:>10.1%}{z:>8.3f}{ss:>10.1f}{mu_L + ss:>10.1f}"
              f"{ss * h:>14,.0f}")
    print("  ※ サービス率を99%から99.9%へ上げる限界費用は逓増する"
          "(正規分布の裾が薄いため)。")

    print("\n[数量割引の検討] 1000個以上で単価3%引き")
    for Q in [q, 1000, 1500]:
        price = unit * (0.97 if Q >= 1000 else 1.0)
        # 保管費が仕入価格に比例する場合(保管費率 20%)
        hh = price * 0.20
        tc = D * price + D / Q * K + Q / 2 * hh
        print(f"  Q = {Q:>7,.0f}: 単価 {price:,.0f}円 総費用 {tc:>14,.0f}円")
    print("  ※ 購入額まで含めた総費用で比較するのが正しい判断基準。")


if __name__ == "__main__":
    main()

実行結果

年間需要 24,000個 / 発注費 15,000円/回 / 保管費 240円/個年
最適発注量 Q* = 1,732.1 個
年間発注回数 = 13.86 回 / 発注間隔 = 26.3 日
最小総費用 = 415,692 円
  内訳: 発注費 207,846円 = 保管費 207,846円 (EOQでは必ず一致する)

[数値探索による検証]
  1個刻みの全探索: Q = 1732 個, TC = 415,692 円(解析解と一致)

[Qを誤って設定した場合の費用増(EOQの頑健性)]
  Q =  50%·Q*: TC =    519,615円 (+25.00%)
  Q =  70%·Q*: TC =    442,415円 (+6.43%)
  Q =  90%·Q*: TC =    418,002円 (+0.56%)
  Q = 100%·Q*: TC =    415,692円 (+0.00%)
  Q = 110%·Q*: TC =    417,582円 (+0.45%)
  Q = 150%·Q*: TC =    450,333円 (+8.33%)
  Q = 200%·Q*: TC =    519,615円 (+25.00%)
  ※ ±30%程度のずれでも費用増は数%に留まる(TCがQ*近傍で平坦なため)。

[安全在庫と発注点]
  日次需要 平均 65.8個 / 標準偏差 12.0個 / リードタイム 7日
  リードタイム中需要: 平均 460.3個 / 標準偏差 31.75個

       サービス率      z値      安全在庫       発注点       追加保管費/年
       90.0%   1.282      40.7     501.0         9,765
       95.0%   1.645      52.2     512.5        12,533
       98.0%   2.054      65.2     525.5        15,649
       99.0%   2.326      73.9     534.1        17,726
       99.9%   3.090      98.1     558.4        23,547
  ※ サービス率を99%から99.9%へ上げる限界費用は逓増する(正規分布の裾が薄いため)。

[数量割引の検討] 1000個以上で単価3%引き
  Q =   1,732: 単価 1,164円 総費用     28,345,457円
  Q =   1,000: 単価 1,164円 総費用     28,412,400円
  Q =   1,500: 単価 1,164円 総費用     28,350,600円
  ※ 購入額まで含めた総費用で比較するのが正しい判断基準。

← 目次← 前: 14次: 16 →