Mímisbrunnr知恵の泉

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

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

📎 前提:指数平滑と季節指数による需要予測(季節需要をどう読むか) | 線形計画による生産計画(LP・在庫収支を制約に総コスト最小化) | 在庫費用:経済的発注量EOQ(保管コスト HH の考え方)

要点(BLUF)

1. 集約計画と2つの戦略

季節商品(冷暖房・飲料・行楽用品)の需要は月ごとに大きく上下します。一方、生産能力は人員・設備で決まり、急には変えられません。集約計画は「向こう数ヶ月〜1年、毎月どれだけ生産し、人をどう増減し、在庫をどれだけ持つか」を、需要予測(指数平滑と季節指数による需要予測)を入力に、総コスト最小で決める中期の意思決定です。

打ち手は大きく2つの極にまとめられます。

flowchart TB
  D["季節変動する需要 D_t"] --> S{"供給をどう合わせる?"}
  S -->|"生産を需要に追従 P_t=D_t"| C["追跡 chase<br/>在庫ゼロ・欠品なし<br/>でも生産調整コスト大"]
  S -->|"生産一定 P_t=一定"| L["平準化 level<br/>雇用安定・生産調整ゼロ<br/>でも在庫+欠品コスト大"]
  C --> M["実務は両者の中間<br/>混合戦略を LP で最適化"]
  L --> M

現実にはこの両極の**中間(混合)**が最適なことがほとんど。どこで折り合うかを勘で決めず、LP で総コスト最小の生産レートを月ごとに求めるのが本ノートの主題です。

2. 在庫収支と総コスト・LP への線形化

計画期間を t=1,,Tt=1,\dots,T 月とし、各月の需要 DtD_t(予測値)を与件とします。決めるのは各月の生産量 PtP_t。在庫は次の**在庫収支(バランス式)**で1ヶ月ずつ繰り上がります。

It=It1+PtDtI_t=I_{t-1}+P_t-D_t

——前月末在庫に今月の生産を足し、需要を引いたのが今月末在庫。It>0I_t>0 なら手持ち在庫、It<0I_t<0 なら**欠品(バックオーダー=積み残し)**です。総コストは3つの和で、これを最小化します。

min t=1T[hIt++sIt+cPtPt1]\min\ \sum_{t=1}^{T}\Big[\,h\,I_t^{+}+s\,I_t^{-}+c\,\lvert P_t-P_{t-1}\rvert\,\Big]

ここで hh は保管コスト(経済的発注量EOQHH と同じ性格・円/個/月)、ss は欠品コスト、cc は生産1単位を増減する調整コスト(雇用調整・残業の代理)。It+=max(It,0)I_t^{+}=\max(I_t,0) は在庫、It=max(It,0)I_t^{-}=\max(-I_t,0) は欠品の大きさです。

このままでは絶対値 \lvert\cdot\rvertmax\max があって線形ではありませんが、非負の補助変数に分ければ LP になります。生産の増減を ut,vt0u_t,v_t\ge0(増やした量・減らした量)に、在庫を It+,It0I_t^{+},I_t^{-}\ge0 に分けて、

PtPt1=utvt,It+It=It1+It1+PtDtP_t-P_{t-1}=u_t-v_t,\qquad I_t^{+}-I_t^{-}=I_{t-1}^{+}-I_{t-1}^{-}+P_t-D_t

と等式制約で結べば、目的 t[hIt++sIt+c(ut+vt)]\sum_t[h\,I_t^{+}+s\,I_t^{-}+c\,(u_t+v_t)]すべて線形。最小化なので、It+,ItI_t^{+},I_t^{-} の片方は自然に0になり(両方正だと無駄にコストが乗る)、ut,vtu_t,v_t も同様です。これで scipy.optimize.linprog にそのまま渡せます。線形計画そのものの幾何(頂点が最適・シャドープライス)は 線形計画による生産計画 を参照。

3. 追跡戦略と平準化戦略を数値で比べる(コード)

12ヶ月の季節需要(夏ピーク・冬の谷)に対し、**追跡(chase)平準化(level)**の総コストを、生産変更・在庫保管・欠品に分けて比べます。期初の生産レートは現状=平均需要としておきます(平準化はこの平均をそのまま続けるので生産変更ゼロ)。

import numpy as np
import pandas as pd

# 12ヶ月の季節需要(夏にピーク・冬に谷)。単位=個
D = np.array([800, 700, 900, 1200, 1500, 1800, 2000, 1900, 1500, 1100, 900, 700])
T = len(D)
mean_D = D.mean()            # 平均需要=平準化(level)の生産レート

# コスト係数
h = 2.0    # 在庫保管コスト(円/個/月)
s = 4.0    # 欠品(backorder)コスト(円/個/月)
c = 8.0    # 生産調整コスト(円/個・増減1単位あたり=雇用調整の代理)
I0 = 0.0   # 期初在庫
P0 = mean_D  # 期初(前月)の生産レート=現状は平均で生産していたとする

def costs(P):
    """生産計画 P(長さT)の総コストを 生産調整・在庫・欠品 に分けて返す"""
    P = np.asarray(P, float)
    I = I0 + np.cumsum(P - D)               # 各月末の正味在庫(負なら欠品)
    holding = h * np.maximum(I, 0).sum()    # 在庫保管コスト
    stockout = s * np.maximum(-I, 0).sum()  # 欠品コスト
    change = c * (np.abs(P[0] - P0) + np.abs(np.diff(P)).sum())  # 生産調整コスト
    total = holding + stockout + change
    return I, holding, stockout, change, total

# 追跡戦略(chase):毎月需要ぴったり生産 -> 在庫ゼロだが生産が需要に追従して変動
P_chase = D.astype(float)
# 平準化戦略(level):毎月一定=平均需要 -> 生産は不変だが在庫が大きく振れる
P_level = np.full(T, mean_D)

rows = []
for name, P in [("追跡 chase", P_chase), ("平準化 level", P_level)]:
    I, hold, stock, chg, tot = costs(P)
    rows.append({"戦略": name, "生産調整": chg, "在庫保管": hold,
                 "欠品": stock, "総コスト": tot,
                 "最大在庫": I.max(), "最大欠品": max(0, -I.min())})
df = pd.DataFrame(rows)
print(f"平均需要 = {mean_D:.1f} 個/月、コスト係数 h={h} s={s} c={c}")
print(df.to_string(index=False, float_format=lambda x: f"{x:.1f}"))
print()
print(f"追跡 chase  : 在庫・欠品コストは {rows[0]['在庫保管']+rows[0]['欠品']:.0f} 円だが"
      f"生産調整が {rows[0]['生産調整']:.0f} 円と大きい")
print(f"平準化 level: 生産調整は {rows[1]['生産調整']:.0f} 円(不変)だが"
      f"在庫+欠品が {rows[1]['在庫保管']+rows[1]['欠品']:.0f} 円と大きい")

出力:

平均需要 = 1250.0 個/月、コスト係数 h=2.0 s=4.0 c=8.0
       戦略    生産調整    在庫保管      欠品    総コスト   最大在庫   最大欠品
 追跡 chase 25200.0     0.0     0.0 25200.0    0.0    0.0
平準化 level     0.0 11900.0 13800.0 25700.0 1400.0 1050.0

追跡 chase  : 在庫・欠品コストは 0 円だが生産調整が 25200 円と大きい
平準化 level: 生産調整は 0 円(不変)だが在庫+欠品が 25700 円と大きい

出力の意味:2戦略のコストの内訳が見事に裏返しになっています。追跡(chase)は毎月需要ぴったり作るので在庫も欠品もゼロ、しかし生産量が 8007009002000800\to700\to900\to\dots\to2000\to\dots と波打つため、生産調整コストが25200円とすべてをそこで払います。平準化(level)は逆に生産調整ゼロ(毎月1250で一定)だが、谷の月に在庫を積み(最大在庫1400個)山の月に積み残す(最大欠品1050個)ので、在庫11900+欠品13800=25700円かかる。総コストは chase 25200・level 25700 とほぼ拮抗——この需要とコストでは「どちらの極も似たり寄ったりで、決め手に欠ける」。だからこそ中間に最適があるはずだ、と次のLPに繋がります。

4. LP で最適な集約計画を解く(コード)

在庫収支を制約に、生産変更・在庫・欠品の総コストを最小化する集約計画を linprog で解きます。第2節の線形化(生産増減 u,vu,v・在庫の正負 I+,II^{+},I^{-})をそのまま実装し、期末在庫を0に縛って(総生産=総需要の閉じた計画期間に)、chase・level と同じ土俵で比べます。月次の需要・生産・在庫を図示します。

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

D = np.array([800, 700, 900, 1200, 1500, 1800, 2000, 1900, 1500, 1100, 900, 700], float)
T = len(D)
h, s, c = 2.0, 4.0, 8.0
I0, P0 = 0.0, D.mean()

# 変数:各月 t に [P_t, Ipos_t, Ineg_t, up_t, dn_t] の5つ。総数 5T
nvar = 5 * T
def idx(t, k): return 5 * t + k   # k: 0=P,1=Ipos,2=Ineg,3=up,4=dn

# 目的(最小化):保管 h*Ipos + 欠品 s*Ineg + 生産調整 c*(up+dn)
cobj = np.zeros(nvar)
for t in range(T):
    cobj[idx(t, 1)] = h
    cobj[idx(t, 2)] = s
    cobj[idx(t, 3)] = c
    cobj[idx(t, 4)] = c

# 等式制約:在庫収支・生産調整・期末在庫ゼロ
A_eq, b_eq = [], []
for t in range(T):
    # 在庫収支:Ipos_t - Ineg_t - Ipos_{t-1} + Ineg_{t-1} - P_t = -D_t(t=0 は +I0)
    row = np.zeros(nvar)
    row[idx(t, 1)] = 1.0
    row[idx(t, 2)] = -1.0
    row[idx(t, 0)] = -1.0
    rhs = -D[t]
    if t == 0:
        rhs += I0
    else:
        row[idx(t - 1, 1)] = -1.0
        row[idx(t - 1, 2)] = 1.0
    A_eq.append(row); b_eq.append(rhs)
    # 生産調整:P_t - P_{t-1} - up_t + dn_t = 0(t=0 は P_{-1}=P0 を右辺へ)
    row = np.zeros(nvar)
    row[idx(t, 0)] = 1.0
    row[idx(t, 3)] = -1.0
    row[idx(t, 4)] = 1.0
    rhs = P0 if t == 0 else 0.0
    if t > 0:
        row[idx(t - 1, 0)] = -1.0
    A_eq.append(row); b_eq.append(rhs)
# 期末在庫=0(総生産=総需要の閉じた計画期間。chase/level と同条件にそろえる)
row = np.zeros(nvar)
row[idx(T - 1, 1)] = 1.0
row[idx(T - 1, 2)] = -1.0
A_eq.append(row); b_eq.append(0.0)

res = linprog(c=cobj, A_eq=np.array(A_eq), b_eq=np.array(b_eq),
              bounds=[(0, None)] * nvar, method="highs")

P_lp = np.array([res.x[idx(t, 0)] for t in range(T)])
Ipos = np.array([res.x[idx(t, 1)] for t in range(T)])
Ineg = np.array([res.x[idx(t, 2)] for t in range(T)])
I_lp = Ipos - Ineg
hold_lp = h * Ipos.sum()
stock_lp = s * Ineg.sum()
chg_lp = c * (np.abs(P_lp[0] - P0) + np.abs(np.diff(P_lp)).sum())

# 比較用:chase と level の総コスト(コード1と同じ評価関数)
def total_cost(P):
    P = np.asarray(P, float)
    I = I0 + np.cumsum(P - D)
    return (h * np.maximum(I, 0).sum() + s * np.maximum(-I, 0).sum()
            + c * (np.abs(P[0] - P0) + np.abs(np.diff(P)).sum()))

tot_chase = total_cost(D)
tot_level = total_cost(np.full(T, P0))

print("=== 最適集約計画(線形計画 linprog)===")
print(f"生産調整 {chg_lp:.0f} + 在庫保管 {hold_lp:.0f} + 欠品 {stock_lp:.0f}")
print(f"LP 総コスト = {res.fun:.0f} 円(内訳の合計と一致:{chg_lp+hold_lp+stock_lp:.0f})")
print(f"総生産 {P_lp.sum():.0f} = 総需要 {D.sum():.0f}(期末在庫0)")
print()
print(f"追跡 chase 総コスト   = {tot_chase:.0f} 円")
print(f"平準化 level 総コスト = {tot_level:.0f} 円")
print(f"LP 最適 総コスト      = {res.fun:.0f} 円"
      f"  -> chase比 {(1-res.fun/tot_chase)*100:.1f}% / level比 {(1-res.fun/tot_level)*100:.1f}% 削減")
print()
print("月別の最適生産・在庫:")
print(" 月  需要    生産P    在庫I")
for t in range(T):
    Iv = 0.0 if abs(I_lp[t]) < 1e-6 else I_lp[t]
    print(f"{t+1:>3} {D[t]:>5.0f} {P_lp[t]:>8.0f} {Iv:>8.0f}")

# 図:上=需要と3つの生産計画、下=LPの在庫推移
months = np.arange(1, T + 1)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9.5, 7), sharex=True,
                               gridspec_kw={"height_ratios": [2, 1]})
ax1.bar(months, D, color="#cccccc", label="需要 D")
ax1.plot(months, D, "o-", color="#d62728", lw=1.5, label="追跡 chase(=需要)")
ax1.axhline(P0, color="#2ca02c", lw=2, ls="--", label=f"平準化 level(={P0:.0f})")
ax1.plot(months, P_lp, "s-", color="#1f77b4", lw=2.2, label="LP 最適生産")
ax1.set_ylabel("個/月"); ax1.legend(loc="upper left", fontsize=9)
ax1.set_title("集約計画:追跡・平準化・LP最適の生産レート")

ax2.bar(months, I_lp, color=["#1f77b4" if v >= -1e-6 else "#d62728" for v in I_lp])
ax2.axhline(0, color="black", lw=0.8)
ax2.set_ylabel("LP在庫 I"); ax2.set_xlabel("月")
ax2.set_title("LP最適計画の月末在庫(正=在庫・負=欠品)")
ax2.set_xticks(months)
plt.tight_layout()
plt.show()

出力:

=== 最適集約計画(線形計画 linprog)===
生産調整 15600 + 在庫保管 3800 + 欠品 1200
LP 総コスト = 20600 円(内訳の合計と一致:20600)
総生産 15000 = 総需要 15000(期末在庫0)

追跡 chase 総コスト   = 25200 円
平準化 level 総コスト = 25700 円
LP 最適 総コスト      = 20600 円  -> chase比 18.3% / level比 19.8% 削減

月別の最適生産・在庫:
 月  需要    生産P    在庫I
  1   800      950      150
  2   700      950      400
  3   900      950      450
  4  1200      950      200
  5  1500     1700      400
  6  1800     1700      300
  7  2000     1700        0
  8  1900     1700     -200
  9  1500     1700        0
 10  1100     1100        0
 11   900      800     -100
 12   700      800        0

出力の意味:LP の最適計画は、冬(1〜4月)は950個・夏(5〜9月)は1700個・秋(10〜12月)は1100〜800個という3〜4ブロックの階段でした。chase のように毎月細かく追従もせず、level のように完全一定でもない——需要の季節に合わせて生産レートを数回だけ大きく切り替える混合戦略です。総コストは 20600円で、純 chase より18.3%・純 level より19.8%安い。内訳は生産調整15600・在庫保管3800・欠品1200で、h<sh<s(保管が欠品より安い)を活かして冬に少し作りだめ(在庫を150〜450個積む)、ピーク月にわずかに積み残す(8月−200・11月−100の小さなバックオーダー)ことで、生産調整の回数を抑えています。図の上段では、LP の青い階段が赤い chase(需要そのまま)と緑の level(一定1250)のちょうど間を通り、下段の在庫は春までプラス(作りだめ)・夏の山で2回だけ小さくマイナスに沈むのが見えます。両極を勘で混ぜるのでなく、コスト最小の混合点をLPが自動で見つける——これが集約計画の実務的な使い方です。

⚠️ よくある誤解

関連ノート