Mímisbrunnr知恵の泉

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

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

📎 前提:ボトルネックとキャパシティ(拘束制約=ボトルネック・資源キャパシティ) | 最適化理論:凸最適化の基礎(凸性・双対・KKT。LP はその線形な特殊例) | 次:スケジューリングとジョブショップ(順序の最適化)

要点(BLUF)

1. 製品ミックス問題を立式する

工場が製品A・製品Bをつくっています。それぞれ1個あたりの利益(貢献利益)は cA,cBc_A,c_B。生産には資源(機械加工の時間、組立の時間、原料)が要り、各製品が1個あたり消費する量と、資源の月間上限が決まっています。利益を最大にする生産量の組 (xA,xB)(x_A,x_B) を求めたい——これが製品ミックス問題です。

要素は3つだけです。

目的も制約もすべて1次(線形)なので「線形計画」。EOQ や M/M/1 のように閉形式の公式は出ませんが、その代わり変数や制約がいくつあってもソルバが頂点を探し当てます。

flowchart LR
  X["決定変数<br/>各製品の生産量 x_i >= 0"] --> OBJ["目的<br/>利益最大 max Σ c_i x_i"]
  X --> CON["資源制約<br/>Σ a_ij x_i <= b_j"]
  OBJ --> LP["線形計画 LP"]
  CON --> LP
  LP --> V["最適は実行可能領域の頂点<br/>拘束制約 slack=0 がボトルネック"]

2. なぜ最適は「頂点」なのか・シャドープライスとは

実行可能領域は、各制約 iaijxibj\sum_i a_{ij}x_i\le b_j が定める半空間の共通部分です。半空間は凸で、凸集合の共通部分も凸なので、実行可能領域は凸多面体(角のある多角形・多面体)になります。

目的関数 Z=cxZ=c^\top x は線形なので、Z=一定Z=\text{一定} の等高線(等利益線)は平行な直線(超平面)の束。これを利益が増える向きへ平行移動していくと、領域から出る直前に触れる点が最適です。平らな直線が凸多面体から離れる瞬間に触れているのは頂点(まれに辺全体)——だから線形計画の最適解は必ず頂点(端点)に存在しますnn 変数なら、頂点は「nn 本の制約が同時に等号で交わる点」。最適化とは「どの制約を等号で効かせるか(どの角か)」の組合せ選びに帰着します。

行列で書くと標準形は次の通りです。

maxx cxs.t.Axb,  x0\max_{x}\ c^\top x \quad \text{s.t.}\quad Ax\le b,\ \ x\ge 0

各資源制約に双対変数 yj0y_j\ge0 を対応させた双対問題を考えると、

miny bys.t.Ayc,  y0\min_{y}\ b^\top y \quad \text{s.t.}\quad A^\top y\ge c,\ \ y\ge 0

強双対定理により、最適では主問題の最大利益と双対問題の最小値が一致します。この最適な yjy_j が資源 jjシャドープライスで、定義は

yj=Z\*bjy_j=\frac{\partial Z^\*}{\partial b_j}

——資源 jj の上限 bjb_j を1単位ゆるめたときの最適利益の増分です。さらに補完スラック条件 yj(slackj)=0y_j\cdot(\text{slack}_j)=0 が成り立ちます。すなわち、

これが「どの資源に投資すれば効くか」の答えになります。凸性・双対・KKT条件の証明と一般論は 凸最適化の基礎 へ(LP はそこで扱う凸最適化を線形に特殊化したもの)。本ノートはこの結果を linprog で計算し、増資源の再求解で裏取りします。

3. linprog で製品ミックスを解く(コード)

scipy.optimize.linprog最小化ソルバです。利益最大化は目的係数の符号を反転(ccc\to-c)して最小化に直し、得られた最小値の符号を戻します。2製品・3資源の例を解いて、最適生産量・最大利益・各資源の余り(slack)からボトルネックを判定し、シャドープライスを取り出します。

import numpy as np
from scipy.optimize import linprog

# 製品ミックス:製品A・製品B を何個つくると利益が最大か
# 利益(千円/個):A=40, B=30
c_profit = np.array([40.0, 30.0])

# 資源制約 A_ub x <= b_ub
#   機械加工(時間/個):2A + 1B <= 100
#   組立    (時間/個):1A + 2B <=  80
#   原料    (kg/個)  :1A + 1B <=  70
A_ub = np.array([
    [2.0, 1.0],   # 機械加工
    [1.0, 2.0],   # 組立
    [1.0, 1.0],   # 原料
])
b_ub = np.array([100.0, 80.0, 70.0])
res_names = ["機械加工(時間)", "組立(時間)", "原料(kg)"]

# linprog は「最小化」。利益最大化は目的を負にして最小化する(c -> -c)
res = linprog(c=-c_profit, A_ub=A_ub, b_ub=b_ub, bounds=[(0, None), (0, None)],
              method="highs")

x_opt = res.x
profit_opt = -res.fun                     # 符号を戻す
used = A_ub @ x_opt                        # 各資源の使用量
slack = b_ub - used                        # 余り(slack)
shadow = -res.ineqlin.marginals           # シャドープライス=利益増/資源1単位

print("=== 最適生産計画 ===")
print(f"製品A の生産量 x_A = {x_opt[0]:.4f} 個")
print(f"製品B の生産量 x_B = {x_opt[1]:.4f} 個")
print(f"最大利益          = {profit_opt:.4f} 千円")
print()
print("=== 資源の使用状況(slack=0 が拘束制約=ボトルネック)===")
print(f"{'資源':<14}{'使用量':>10}{'上限':>8}{'余りslack':>12}{'シャドープライス':>18}")
for i, name in enumerate(res_names):
    tag = "  <-拘束(ボトルネック)" if slack[i] < 1e-6 else "  (余裕あり)"
    print(f"{name:<14}{used[i]:>10.2f}{b_ub[i]:>8.1f}{slack[i]:>12.4f}{shadow[i]:>18.4f}{tag}")

出力:

=== 最適生産計画 ===
製品A の生産量 x_A = 40.0000 個
製品B の生産量 x_B = 20.0000 個
最大利益          = 2200.0000 千円

=== 資源の使用状況(slack=0 が拘束制約=ボトルネック)===
資源                   使用量      上限     余りslack          シャドープライス
機械加工(時間)          100.00   100.0      0.0000           16.6667  <-拘束(ボトルネック)
組立(時間)             80.00    80.0      0.0000            6.6667  <-拘束(ボトルネック)
原料(kg)             60.00    70.0     10.0000            0.0000  (余裕あり)

出力の意味:最適は 製品A を40個・製品B を20個、最大利益 2200千円。注目は資源の使い方です——機械加工と組立は使用量が上限ぴったり(slack=0=0で、ここが拘束制約=ボトルネック。最適解はこの2本の制約が交わる頂点 (40,20)(40,20) に来ています(2変数なので2本が等号で効く)。一方原料は60kg使用で上限70kg、10kg余っています(slack>0>0)=律速していません。シャドープライスを見ると、拘束している機械加工は 16.67千円/時間、組立は 6.67千円/時間 と正の値で、余っている原料は0。つまり「もし生産能力を増やすなら、原料を買い増しても1円も儲からない。効くのは機械加工の時間を増やすこと(1時間あたり16.67千円の利益増)」と読めます。ボトルネックがどこで、そこを1単位広げると利益がいくら伸びるか——LP はこれを同時に教えてくれます。

4. シャドープライスを再求解で検算し、実行可能領域を図示する(コード)

シャドープライスは「資源を1単位増やすと利益が yjy_j 増える」という主張でした。本当かどうかは、上限 bjb_j を実際に +1+1 して解き直し、利益の増分を見れば確かめられます。あわせて2製品なので、実行可能領域と最適頂点・等利益線を図示します。

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

c_profit = np.array([40.0, 30.0])
A_ub = np.array([[2.0, 1.0], [1.0, 2.0], [1.0, 1.0]])
b_ub = np.array([100.0, 80.0, 70.0])
res_names = ["機械加工", "組立", "原料"]

def solve(b):
    r = linprog(c=-c_profit, A_ub=A_ub, b_ub=b, bounds=[(0, None), (0, None)],
                method="highs")
    return -r.fun, r.x

base_profit, x_opt = solve(b_ub)
shadow = -linprog(c=-c_profit, A_ub=A_ub, b_ub=b_ub,
                  bounds=[(0, None), (0, None)], method="highs").ineqlin.marginals

print("=== シャドープライスの検算(資源を1単位増やして再求解)===")
print(f"基準の最大利益 = {base_profit:.4f} 千円")
print(f"{'資源':<8}{'シャドープライス':>16}{'+1単位後の利益':>16}{'実際の利益増':>14}")
for i, name in enumerate(res_names):
    b2 = b_ub.copy()
    b2[i] += 1.0
    p2, _ = solve(b2)
    print(f"{name:<8}{shadow[i]:>16.4f}{p2:>16.4f}{p2 - base_profit:>14.4f}")
print("-> 拘束資源では『利益増 = シャドープライス』、非拘束(原料)は増やしても利益不変")

# 図:実行可能領域(2製品)と最適頂点
xx = np.linspace(0, 60, 400)
plt.figure(figsize=(8.5, 7))
colors = ["#1f77b4", "#2ca02c", "#9467bd"]
for i, name in enumerate(res_names):
    a1, a2 = A_ub[i]
    yy = (b_ub[i] - a1 * xx) / a2          # 制約境界 a1*A + a2*B = b
    plt.plot(xx, yy, color=colors[i], lw=1.8,
             label=f"{name}: {a1:.0f}A+{a2:.0f}B<={b_ub[i]:.0f}")

# 実行可能領域(全制約を満たすグリッド点)を塗る
gx, gy = np.meshgrid(np.linspace(0, 60, 600), np.linspace(0, 60, 600))
feasible = ((A_ub[0, 0] * gx + A_ub[0, 1] * gy <= b_ub[0]) &
            (A_ub[1, 0] * gx + A_ub[1, 1] * gy <= b_ub[1]) &
            (A_ub[2, 0] * gx + A_ub[2, 1] * gy <= b_ub[2]))
plt.contourf(gx, gy, feasible.astype(float), levels=[0.5, 1.5],
             colors=["#ffd54f"], alpha=0.35)

# 等利益線(最適利益を通る)
P = base_profit
yiso = (P - c_profit[0] * xx) / c_profit[1]
plt.plot(xx, yiso, "--", color="#d62728", lw=1.6,
         label=f"等利益線 40A+30B={P:.0f}")

plt.plot(x_opt[0], x_opt[1], "*", color="black", ms=20,
         label=f"最適頂点 ({x_opt[0]:.0f}, {x_opt[1]:.0f})")
plt.xlabel("製品A の生産量"); plt.ylabel("製品B の生産量")
plt.title("線形計画:実行可能領域の頂点が最適(拘束2本の交点)")
plt.xlim(0, 60); plt.ylim(0, 60)
plt.legend(loc="upper right", fontsize=9)
plt.grid(alpha=0.3); plt.tight_layout()
plt.show()

出力:

=== シャドープライスの検算(資源を1単位増やして再求解)===
基準の最大利益 = 2200.0000 千円
資源              シャドープライス        +1単位後の利益        実際の利益増
機械加工             16.6667       2216.6667       16.6667
組立                6.6667       2206.6667        6.6667
原料                0.0000       2200.0000        0.0000
-> 拘束資源では『利益増 = シャドープライス』、非拘束(原料)は増やしても利益不変

出力の意味:機械加工を +1+1 時間すると利益は 22002216.672200\to2216.67+16.67+16.67——シャドープライス 16.67 とぴたり一致。組立 +1+1+6.67+6.67 でこれも一致。そして余っている原料を +1+1kg しても利益は2200のまま(増分0)で、補完スラック条件(非拘束資源のシャドープライス=0=0)が数値で確かめられました。シャドープライスは机上の双対値ではなく、「ここを1単位広げたら本当にこれだけ儲かる」という運用上の限界価値なのです。図では、3本の制約境界が囲む黄色い実行可能領域の右上の角(機械加工=青と組立=緑の交点)で赤い等利益線が領域に接し、そこに最適頂点 (40,20)(40,20) の★が乗っています。原料(紫)はこの角より外側を通り、領域を縛っていない=余っていることが視覚的にもわかります。利益を増やす向きへ平行移動した等利益線が、最後に触れるのは頂点——LP の幾何学的な正体です。

⚠️ よくある誤解

関連ノート