100本ノック / 数理最適化 / 数理最適化100本ノック

製造業の数理最適化入門|生産計画・在庫・人員・設備投資をPythonで実践

工場の意思決定をつなぐ:生産・日程・在庫・投資の最適化10本ノック

架空の精密部品工場を題材に、月次の生産量から日々の作業順、人員、在庫、発注、設備投資までを一貫した意思決定として扱います。対象は No.091〜No.100(製造業DIへの応用) です。

ここでいう DI(Decision Intelligence)は、予測値を眺めるだけで終わらず、制約の下で「何を、いつ、どれだけ実行するか」へ変換し、実績から次の判断を改善する仕組みを指します。

[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。

はじめに:この記事で扱う製造業の実務課題

販売計画は製品別、設備計画は機械別、人員計画は技能別、調達計画は部材別に作られがちです。しかし現場では、増産すれば段取りと残業が増え、在庫を減らせば欠品リスクが高まり、設備を買えば固定費と将来の選択肢が変わります。本稿では、これらを共通のデータと目的指標で接続します。

現場でよくある状況

  • Excelごとに前提となる需要量や能力が異なる
  • 「納期最優先」「在庫最小」など部門KPIが衝突する
  • 熟練者、段取り、保全停止といった実制約が計画に反映されない
  • 最適化結果が一つの数字だけで、代替案や崩れ方を説明できない

なぜこの問題は判断が難しいのか

意思決定変数は相互依存し、整数性と不確実性を含みます。予測誤差もあるため、帳尻の合う計画がそのまま実行可能とは限りません。したがって、目的関数、制約、時間粒度、再計画条件を明示し、現行案との比較で効果を評価する必要があります。

今回扱うノックの全体像

No.テーマ主な意思決定主な評価指標
091生産計画最適化製品別生産量限界利益、能力余裕
092ジョブショップ設備ごとの作業順メイクスパン
093フローショップ全工程共通の投入順完了時刻
094ラインバランシング作業の工程割当サイクルタイム、効率
095人員配置技能別シフト充足率、人件費
096在庫最適化安全在庫・発注点サービス率、保有量
097発注量最適化ロット量発注費+保管費
098設備投資最適化投資案件の組合せNPV、予算消化
099シミュレーション×最適化不確実下の政策総費用分布、欠品率
100意思決定エンジン入力から承認まで価値、実行可能性、監査性

Python 環境の準備

外部データは使いません。NumPy、pandas、SciPy、Matplotlibのみを使い、乱数生成器の seed を固定して再現可能にします。

import platform
import itertools
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import scipy
from scipy.optimize import linprog
from IPython.display import display

rng = np.random.default_rng(42)
plt.rcParams.update({"figure.figsize": (8, 4), "axes.grid": True})
print(f"Python {platform.python_version()}")
print(f"NumPy {np.__version__} / pandas {pd.__version__} / SciPy {scipy.__version__} / Matplotlib {matplotlib.__version__}")
Python 3.13.1
NumPy 2.5.1 / pandas 3.0.3 / SciPy 1.18.0 / Matplotlib 3.11.0

架空データの作成

3製品、複数設備、技能別要員、主要部材を持つ精密部品工場を想定します。金額単位は千円、時間単位は時間、需要・在庫単位は個です。分析ごとに必要な詳細データをこのマスタから派生させます。

products = pd.DataFrame({
    "product": ["A-標準", "B-高精度", "C-短納期"],
    "demand": [420, 300, 260], "contribution": [8.0, 11.0, 9.0],
    "machining_h": [1.0, 1.6, 1.2], "assembly_h": [0.8, 0.7, 1.1],
    "material_kg": [2.0, 2.8, 1.7]
})
capacity = pd.Series({"machining_h": 1150, "assembly_h": 850, "material_kg": 2200})
jobs = pd.DataFrame({"job": list("ABCDEF"), "M1": [4, 7, 3, 6, 5, 4],
                     "M2": [6, 3, 5, 4, 7, 2], "M3": [3, 5, 4, 6, 2, 5]})
display(products)
display(capacity.rename("monthly_capacity").to_frame())
display(jobs)
product demand contribution machining_h assembly_h material_kg
0 A-標準 420 8.0 1.0 0.8 2.0
1 B-高精度 300 11.0 1.6 0.7 2.8
2 C-短納期 260 9.0 1.2 1.1 1.7
monthly_capacity
machining_h 1150
assembly_h 850
material_kg 2200
job M1 M2 M3
0 A 4 6 3
1 B 7 3 5
2 C 3 5 4
3 D 6 4 6
4 E 5 7 2
5 F 4 2 5

No.091:生産計画最適化

実務での意味

需要上限の範囲で、限られた加工・組立・材料をどの製品へ配分するかを決めます。「売上が大きい製品」ではなく、ボトルネック能力1時間を何に使うと利益へ寄与するかが論点です。

分析・モデル化の考え方

製品 ii の生産量を xix_i、限界利益を pip_i、資源使用量を aria_{ri} とすると、

maxxipixis.t.iarixiCr,  0xidi\max_x \sum_i p_i x_i \quad \text{s.t.}\quad \sum_i a_{ri}x_i\le C_r,\;0\le x_i\le d_i

と表せます。ここでは線形計画の連続解を月次目標とし、実運用ではロット単位へ丸めた後に制約を再確認します。

Pythonで確認する

resource_cols = ["machining_h", "assembly_h", "material_kg"]
r91 = linprog(-products["contribution"], A_ub=products[resource_cols].to_numpy().T,
              b_ub=capacity.to_numpy(), bounds=list(zip(np.zeros(3), products["demand"])), method="highs")
plan = products[["product", "demand"]].copy()
plan["optimal_qty"] = r91.x
plan["contribution_kJPY"] = r91.x * products["contribution"]
usage = pd.DataFrame({"capacity": capacity, "used": products[resource_cols].to_numpy().T @ r91.x})
usage["utilization_%"] = 100 * usage["used"] / usage["capacity"]
display(plan.round(1)); display(usage.round(1))
ax = plan.set_index("product")[["demand", "optimal_qty"]].plot.bar()
ax.set(title="Demand and optimized production plan", xlabel="Product", ylabel="Quantity")
ax.grid(axis="y"); plt.tight_layout(); plt.show()
product demand optimal_qty contribution_kJPY
0 A-標準 420 420.0 3360.0
1 B-高精度 300 261.2 2873.8
2 C-短納期 260 260.0 2340.0
capacity used utilization_%
machining_h 1150 1150.0 100.0
assembly_h 850 804.9 94.7
material_kg 2200 2013.5 91.5
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 27161 (\N{CJK UNIFIED IDEOGRAPH-6A19}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 28310 (\N{CJK UNIFIED IDEOGRAPH-6E96}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 39640 (\N{CJK UNIFIED IDEOGRAPH-9AD8}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 31934 (\N{CJK UNIFIED IDEOGRAPH-7CBE}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 24230 (\N{CJK UNIFIED IDEOGRAPH-5EA6}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 30701 (\N{CJK UNIFIED IDEOGRAPH-77ED}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 32013 (\N{CJK UNIFIED IDEOGRAPH-7D0D}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_11861/2709310365.py:12: UserWarning: Glyph 26399 (\N{CJK UNIFIED IDEOGRAPH-671F}) missing from font(s) DejaVu Sans.
  ax.grid(axis="y"); plt.tight_layout(); plt.show()
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 27161 (\N{CJK UNIFIED IDEOGRAPH-6A19}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 28310 (\N{CJK UNIFIED IDEOGRAPH-6E96}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 39640 (\N{CJK UNIFIED IDEOGRAPH-9AD8}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 31934 (\N{CJK UNIFIED IDEOGRAPH-7CBE}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 24230 (\N{CJK UNIFIED IDEOGRAPH-5EA6}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 30701 (\N{CJK UNIFIED IDEOGRAPH-77ED}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 32013 (\N{CJK UNIFIED IDEOGRAPH-7D0D}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 26399 (\N{CJK UNIFIED IDEOGRAPH-671F}) missing from font(s) DejaVu Sans.
  fig.canvas.print_figure(bytes_io, **kw)


png

結果の読み取り

需要をすべて満たすのではなく、制約資源を利益貢献の高い組合せへ配分します。稼働率100%に近い資源が増産のボトルネック候補です。ただし、端数をロットへ丸めると超過し得るため、丸め後の再検証と最低供給量の制約が必要です。

No.092:ジョブショップスケジューリング

実務での意味

ジョブごとに通る設備順が異なる加工現場では、同じ設備で作業が重ならず、各ジョブの工程順を守る計画が必要です。納期遅れだけでなく、仕掛滞留や段取り替えにも影響します。

分析・モデル化の考え方

作業 (j,k)(j,k) の開始時刻を sjks_{jk} とし、工程先行制約と設備上の非重複制約を置き、全作業の完了時刻 CmaxC_{\max} を最小化します。ここでは小規模例を、時刻が早い実行可能作業を優先するディスパッチ規則で解きます。厳密最適とは限らない点も重要です。

Pythonで確認する

routes = {
 "J1": [("M1",4),("M2",3),("M3",2)], "J2": [("M2",2),("M1",5),("M3",4)],
 "J3": [("M1",3),("M3",5),("M2",2)], "J4": [("M3",3),("M2",4),("M1",2)]}
machine_free = {m: 0 for m in ["M1","M2","M3"]}; job_free = {j: 0 for j in routes}; sched=[]
remaining = [(j,k,m,p) for j,ops in routes.items() for k,(m,p) in enumerate(ops)]
while remaining:
    eligible = [x for x in remaining if x[1] == sum(1 for r in sched if r[0] == x[0])]
    j,k,m,p = min(eligible, key=lambda x:(max(job_free[x[0]],machine_free[x[2]]), x[3]))
    start=max(job_free[j],machine_free[m]); end=start+p
    sched.append((j,k,m,start,end)); job_free[j]=end; machine_free[m]=end; remaining.remove((j,k,m,p))
sched_df=pd.DataFrame(sched,columns=["job","operation","machine","start","end"])
display(sched_df.sort_values(["machine","start"]))
fig,ax=plt.subplots()
for y,(m,g) in enumerate(sched_df.groupby("machine")):
    for _,r in g.iterrows(): ax.barh(y,r.end-r.start,left=r.start); ax.text((r.start+r.end)/2,y,r.job,ha="center",va="center")
ax.set_yticks(range(3), sorted(sched_df.machine.unique())); ax.set(title="Job-shop schedule",xlabel="Time",ylabel="Machine")
ax.grid(axis="x"); plt.tight_layout(); plt.show()
print("Makespan:", sched_df.end.max())
job operation machine start end
1 J3 0 M1 0 3
3 J1 0 M1 3 7
6 J4 2 M1 7 9
8 J2 1 M1 9 14
0 J2 0 M2 0 2
4 J4 1 M2 3 7
7 J1 1 M2 7 10
10 J3 2 M2 10 12
2 J4 0 M3 0 3
5 J3 1 M3 3 8
9 J1 2 M3 10 12
11 J2 2 M3 14 18

png

Makespan: 18

結果の読み取り

ガント図に重複はなく、各ジョブの工程順も守られています。一方、設備の空白は前工程待ちでも発生します。実務では納期、優先度、段取り行列、休止時間を含め、現行ルールとメイクスパン・遅延・安定性を比較します。

No.093:フローショップスケジューリング

実務での意味

全品種が同じ工程順を通るラインでは、投入順だけでも後工程の待ち時間が大きく変わります。順番を変えるだけなら設備投資なしで改善できる可能性があります。

分析・モデル化の考え方

順列 π\pi に対し、完了時刻は Ck,m=max(Ck1,m,Ck,m1)+pπk,mC_{k,m}=\max(C_{k-1,m},C_{k,m-1})+p_{\pi_k,m} で再帰計算できます。6ジョブなので全 6!=7206!=720 順列を列挙し、最小メイクスパンを求めます。

Pythonで確認する

def flow_completion(order):
    c=np.zeros((len(order),3))
    for i,j in enumerate(order):
        for m,col in enumerate(["M1","M2","M3"]):
            c[i,m]=max(c[i-1,m] if i else 0,c[i,m-1] if m else 0)+jobs.set_index("job").loc[j,col]
    return c
records=[]
for order in itertools.permutations(jobs.job): records.append((order,flow_completion(order)[-1,-1]))
records.sort(key=lambda x:x[1]); best_order,best_ms=records[0]; base=tuple(jobs.job); base_ms=flow_completion(base)[-1,-1]
display(pd.DataFrame({"plan":["Current","Optimized"],"order":["→".join(base),"→".join(best_order)],"makespan":[base_ms,best_ms]}))
c=flow_completion(best_order); fig,ax=plt.subplots()
for i,j in enumerate(best_order):
 for m in range(3):
  p=jobs.set_index("job").loc[j,f"M{m+1}"]; ax.barh(m,p,left=c[i,m]-p); ax.text(c[i,m]-p/2,m,j,ha="center",va="center",fontsize=8)
ax.set_yticks(range(3),["M1","M2","M3"]); ax.set(title="Optimized flow-shop schedule",xlabel="Time",ylabel="Process")
ax.grid(axis="x"); plt.tight_layout(); plt.show()
plan order makespan
0 Current A→B→C→D→E→F 39.0
1 Optimized A→C→D→F→E→B 37.0

png

結果の読み取り

現行順と最良順のメイクスパン差が、投入順変更の改善余地です。同率最良の順列が複数ある場合は、納期や段取り安定性を副指標に選べます。品種数が増えると全列挙は急増するため、整数計画やヒューリスティクスへ切り替えます。

No.094:ラインバランシング

実務での意味

組立作業を工程へ割り当て、タクトを守りながら工程間の負荷差を減らします。最大負荷工程がライン全体の生産速度を決めるため、平均時間だけでは判断できません。

分析・モデル化の考え方

作業時間合計を TT、工程数を KK、サイクルタイムを CC とすると、ライン効率は E=T/(KC)E=T/(KC) です。先行関係を守る単純な位置重み法で割り当て、遊休時間を可視化します。

Pythonで確認する

tasks=pd.DataFrame({"task":list("ABCDEFGH"),"time":[2.4,3.1,1.8,2.6,3.4,1.5,2.2,2.8]})
cycle=7.0; stations=[]; current=[]; load=0
for _,r in tasks.iterrows():
    if load+r.time>cycle: stations.append((current,load)); current=[]; load=0
    current.append(r.task); load+=r.time
stations.append((current,load))
bal=pd.DataFrame({"station":range(1,len(stations)+1),"tasks":[",".join(s[0]) for s in stations],"load_h":[s[1] for s in stations]})
bal["idle_h"]=cycle-bal.load_h; efficiency=tasks.time.sum()/(len(bal)*cycle)
display(bal.round(1)); print(f"Line efficiency: {efficiency:.1%}")
ax=bal.plot.bar(x="station",y=["load_h","idle_h"],stacked=True)
ax.set(title="Workload and idle time by station",xlabel="Station",ylabel="Hours per cycle"); ax.grid(axis="y"); plt.tight_layout(); plt.show()
station tasks load_h idle_h
0 1 A,B 5.5 1.5
1 2 C,D 4.4 2.6
2 3 E,F 4.9 2.1
3 4 G,H 5.0 2.0
Line efficiency: 70.7%


png

結果の読み取り

負荷の低い工程だけを責めるのではなく、先行関係、作業分割可否、技能・治具制約を確認します。サイクルタイムを短縮すると必要工程数が段階的に増えるため、需要シナリオ別に人員数と効率を評価します。

No.095:人員配置最適化

実務での意味

日ごとの必要人数を満たしつつ、勤務パターンと人件費を決めます。人数だけでなく技能、資格、連続勤務、希望休、公平性が実行可能性を左右します。

分析・モデル化の考え方

勤務パターン pp の採用人数を xpx_p、日 dd をカバーするかを adpa_{dp} として、minpcpxp\min\sum_p c_px_ppadpxprd\sum_p a_{dp}x_p\ge r_d とします。小規模な整数候補を列挙します。

Pythonで確認する

days=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]
patterns={"Weekday":[1,1,1,1,1,0,0],"Early":[1,1,1,0,0,1,1],"Late":[0,0,1,1,1,1,1],"Weekend":[0,0,0,0,0,1,1]}
required=np.array([6,7,8,8,7,5,4]); costs=np.array([210,205,215,90])
A=np.array(list(patterns.values())).T; candidates=[]
for x in itertools.product(range(10),repeat=4):
    cov=A@x
    if np.all(cov>=required): candidates.append((costs@x,*x,*cov))
best=min(candidates); staffing_cost=best[0]; x=np.array(best[1:5]); coverage=A@x
display(pd.DataFrame({"pattern":list(patterns),"staff":x,"weekly_cost_kJPY":x*costs}))
display(pd.DataFrame({"day":days,"required":required,"assigned":coverage,"surplus":coverage-required}))
ax=pd.DataFrame({"required":required,"assigned":coverage},index=days).plot(marker="o")
ax.set(title="Required and assigned staffing",xlabel="Day",ylabel="People"); ax.grid(True); plt.tight_layout(); plt.show()
pattern staff weekly_cost_kJPY
0 Weekday 7 1470
1 Early 0 0
2 Late 1 215
3 Weekend 4 360
day required assigned surplus
0 Mon 6 7 1
1 Tue 7 7 0
2 Wed 8 8 0
3 Thu 8 8 0
4 Fri 7 8 1
5 Sat 5 5 0
6 Sun 4 5 1

png

結果の読み取り

全日の必要数を満たす最小費用のパターン構成です。余剰は整数勤務パターンの副作用で、教育・保全支援へ活用できます。本番では個人名を直接最適化する前に、労務規則、資格の代替可否、公平性の合意と説明責任を整えます。

No.096:在庫最適化

実務での意味

在庫は欠品を防ぐ一方、資金・保管場所・陳腐化リスクを消費します。平均需要だけでなく、リードタイム中の需要変動に対する安全在庫を決めます。

分析・モデル化の考え方

日需要が独立で平均 μd\mu_d、標準偏差 σd\sigma_d、リードタイムが LL 日なら、発注点は ROP=μdL+zσdLROP=\mu_dL+z\sigma_d\sqrt{L}zz は目標サイクルサービス率に対応します。

Pythonで確認する

mu_d,sigma_d,L=40,9,5
service_table=pd.DataFrame({"service_level":[0.90,0.95,0.975,0.99],"z":[1.282,1.645,1.960,2.326]})
service_table["safety_stock"]=service_table.z*sigma_d*np.sqrt(L)
service_table["reorder_point"]=mu_d*L+service_table.safety_stock
display(service_table.round(1))
ax=service_table.plot(x="service_level",y="safety_stock",marker="o")
ax.set(title="Service level and safety stock",xlabel="Cycle service level",ylabel="Safety stock (units)"); ax.grid(True); plt.tight_layout(); plt.show()
service_level z safety_stock reorder_point
0 0.9 1.3 25.8 225.8
1 1.0 1.6 33.1 233.1
2 1.0 2.0 39.4 239.4
3 1.0 2.3 46.8 246.8

png

結果の読み取り

サービス率を上げるほど安全在庫は非線形に増えます。選択は「高いほど良い」ではなく、欠品1回の影響と在庫費の比較です。需要の自己相関、リードタイム変動、欠品時の繰越・失注の違いが大きければ、単純式ではなくシミュレーションで検証します。

No.097:発注量最適化

実務での意味

小口発注は平均在庫を減らしますが、発注・受入・検査の回数を増やします。大口発注はその逆です。このトレードオフから基準ロットを決めます。

分析・モデル化の考え方

年間需要 DD、1回の発注費 SS、単位当たり年間保管費 HH の経済的発注量は Q=2DS/HQ^*=\sqrt{2DS/H}。総関連費は DS/Q+HQ/2DS/Q+HQ/2 です。

Pythonで確認する

D,S,H=12000,18,1.8
q_star=np.sqrt(2*D*S/H); q=np.arange(100,1001,10)
ordering=D/q*S; holding=q/2*H; total=ordering+holding
print(f"EOQ: {q_star:.0f} units / relevant annual cost: {D/q_star*S+q_star/2*H:.1f} kJPY")
fig,ax=plt.subplots(); ax.plot(q,ordering,label="Ordering"); ax.plot(q,holding,label="Holding"); ax.plot(q,total,label="Total"); ax.axvline(q_star,color="black",ls="--",label="EOQ")
ax.set(title="Economic order quantity",xlabel="Order quantity",ylabel="Annual relevant cost (kJPY)"); ax.grid(True); ax.legend(); plt.tight_layout(); plt.show()
EOQ: 490 units / relevant annual cost: 881.8 kJPY


png

結果の読み取り

総費用曲線は最適点近傍で比較的平坦です。したがってEOQを絶対値とせず、梱包単位、最小発注量、車載効率、保管上限へ合わせた近傍候補を比較できます。数量割引がある場合は購入費も含め、価格境界ごとに評価します。

No.098:設備投資最適化

実務での意味

予算内で、能力増強、自動検査、省エネ、搬送改善などの案件を選びます。案件単体の回収年数だけでは、予算競合やセット効果を扱えません。

分析・モデル化の考え方

案件 ii の採否を yi{0,1}y_i\in\{0,1\}、投資額を cic_i、NPVを viv_i とし、maxiviyi\max\sum_i v_iy_iiciyiB\sum_i c_iy_i\le B。さらに「自動搬送は能力増強を採用した場合のみ」の依存制約を加え、全組合せを評価します。

Pythonで確認する

invest=pd.DataFrame({"project":["Capacity","AutoInspection","Energy","AMR","PredictiveMaint"],
 "cost":[55,32,24,28,18],"npv":[78,47,31,44,25]})
budget=90; opts=[]
for y in itertools.product([0,1],repeat=len(invest)):
    y=np.array(y); cost=y@invest.cost; value=y@invest.npv
    if cost<=budget and y[3]<=y[0]: opts.append((value,cost,y))
value,cost,y=max(opts,key=lambda z:z[0])
result=invest.copy(); result["selected"]=y.astype(bool); display(result); print(f"Total cost: {cost} / Budget: {budget} / Total NPV: {value}")
ax=result.plot.bar(x="project",y=["cost","npv"]); ax.set(title="Investment candidates and selected portfolio",xlabel="Project",ylabel="Million JPY");
for i,v in enumerate(y):
    if v: ax.text(i,max(result.loc[i,["cost","npv"]])+2,"SELECT",ha="center")
ax.grid(axis="y"); plt.tight_layout(); plt.show()
project cost npv selected
0 Capacity 55 78 True
1 AutoInspection 32 47 True
2 Energy 24 31 False
3 AMR 28 44 False
4 PredictiveMaint 18 25 False
Total cost: 87 / Budget: 90 / Total NPV: 125


png

結果の読み取り

選択結果は予算と依存関係を同時に満たします。NPVの点推定だけでなく、需要減、立上げ遅延、残存価値のシナリオで順位が変わるかを確認します。未消化予算があっても、離散案件のため必ずしも異常ではありません。

No.099:シミュレーションと最適化の統合

実務での意味

平均需要で最適な在庫政策が、変動下で欠品を多発することがあります。候補政策を多数の同一需要シナリオで比較し、費用とサービスの両面から選びます。

分析・モデル化の考え方

発注点 rr と発注量 QQ を候補化し、日次在庫をモンテカルロシミュレーションします。目的は平均保管費+発注費+欠品ペナルティです。共通乱数を使い、候補間の比較ノイズを抑えます。

Pythonで確認する

scenarios=rng.poisson(40,size=(120,180)); lead=5
def inventory_policy(r,Q):
    costs=[]; stockout_days=[]
    for demand_path in scenarios:
        onhand=r+Q; arrivals={}; cost=0; so=0
        for day,d in enumerate(demand_path):
            onhand+=arrivals.get(day,0); sold=min(onhand,d); lost=d-sold; onhand-=sold; so+=lost>0
            cost+=.003*onhand+.12*lost
            pipeline=sum(v for k,v in arrivals.items() if k>day)
            if onhand+pipeline<=r: arrivals[day+lead]=arrivals.get(day+lead,0)+Q; cost+=18
        costs.append(cost); stockout_days.append(so/len(demand_path))
    return np.mean(costs),np.quantile(costs,.95),np.mean(stockout_days)
rows=[]
for r in [180,200,220,240,260]:
 for Q in [300,450,600,750]: rows.append((r,Q,*inventory_policy(r,Q)))
policies=pd.DataFrame(rows,columns=["reorder_point","order_qty","mean_cost","p95_cost","stockout_day_rate"])
best=policies.loc[policies.mean_cost.idxmin()]; display(policies.sort_values("mean_cost").head(8).round(3))
pivot=policies.pivot(index="reorder_point",columns="order_qty",values="mean_cost")
fig,ax=plt.subplots(); im=ax.imshow(pivot,aspect="auto",origin="lower"); fig.colorbar(im,ax=ax,label="Mean cost")
ax.set_xticks(range(len(pivot.columns)),pivot.columns); ax.set_yticks(range(len(pivot.index)),pivot.index)
ax.set(title="Simulated policy cost",xlabel="Order quantity",ylabel="Reorder point"); ax.grid(False); plt.tight_layout(); plt.show()
reorder_point order_qty mean_cost p95_cost stockout_day_rate
2 180 600 372.401 387.116 0.029
3 180 750 373.321 377.966 0.023
7 200 750 377.349 381.652 0.007
6 200 600 378.014 388.077 0.008
11 220 750 386.794 390.855 0.001
10 220 600 387.471 397.669 0.001
15 240 750 397.487 401.655 0.000
14 240 600 398.173 408.469 0.000

png

結果の読み取り

表の先頭が候補集合内の最小平均費用政策です。ただし平均費用が近いなら、95%費用や欠品日率が低い案を採る余地があります。シミュレーション最適化では、乱数seed、試行数、現行政策との同一シナリオ比較、未経験の外部検証期間を保存します。

No.100:数理工房の意思決定エンジン

実務での意味

価値はアルゴリズム単体ではなく、データ取得、候補生成、制約検証、承認、実行、実績学習が循環して初めて生まれます。人が例外を判断でき、推奨理由を追跡できる仕組みにします。

分析・モデル化の考え方

意思決定エンジンを Observe → Predict → Optimize → Simulate → Approve → Execute → Learn の閉ループとして設計します。推奨案は利益だけでなく、制約違反、リスク、現行案との差、採用・却下理由を一つの記録にまとめます。

Pythonで確認する

decision_record=pd.DataFrame([
 ["Production plan",-r91.fun,"All resource use <= capacity","Approve if rounded plan remains feasible"],
 ["Staffing",-staffing_cost,"Assigned >= required","Supervisor reviews skills"],
 ["Inventory policy",-float(policies.mean_cost.min()),"Stockout-day rate monitored","Approve after shadow operation"],
 ["Investment portfolio",value,"Cost <= budget; dependencies","Board approves scenario assumptions"]],
 columns=["decision","objective_or_value","guardrail","human_gate"])
decision_record["run_id"]="DI-2026-07-001"; decision_record["data_version"]="synthetic-v1"
display(decision_record)
stages=pd.DataFrame({"stage":["Observe","Predict","Optimize","Simulate","Approve","Execute","Learn"],
                     "owner":["Data","Planning","OR","Risk","Manager","Operations","Analytics"]})
fig,ax=plt.subplots(figsize=(10,2.5)); ax.scatter(range(len(stages)),np.zeros(len(stages)),s=900)
for i,r in stages.iterrows(): ax.text(i,0,f"{r.stage}\n({r.owner})",ha="center",va="center",fontsize=8)
ax.plot(range(len(stages)),np.zeros(len(stages))); ax.set(title="Decision Intelligence operating loop",xlabel="Stage",ylabel="Workflow"); ax.set_yticks([]); ax.grid(axis="x"); plt.tight_layout(); plt.show()
decision objective_or_value guardrail human_gate run_id data_version
0 Production plan 8573.7500 All resource use <= capacity Approve if rounded plan remains feasible DI-2026-07-001 synthetic-v1
1 Staffing -2045.0000 Assigned >= required Supervisor reviews skills DI-2026-07-001 synthetic-v1
2 Inventory policy -372.4009 Stockout-day rate monitored Approve after shadow operation DI-2026-07-001 synthetic-v1
3 Investment portfolio 125.0000 Cost <= budget; dependencies Board approves scenario assumptions DI-2026-07-001 synthetic-v1

png

結果の読み取り

同じ run_id で、入力版、目的値、ガードレール、人の承認点を追跡できます。人を排除する自動化ではなく、定型計算を機械へ寄せ、例外・価値判断・説明責任を人が担う設計です。KPIは「最適化の目的値」だけでなく、提案採用率、手修正量、再計画時間、実績差も持ちます。

対象ノックを通して見える実務上の示唆

  • 生産、日程、人員、在庫、投資は共通の需要前提と能力マスタでつなぐ必要があります。
  • 最適値より先に、目的関数の単位、絶対に守る制約、望ましい条件を合意します。
  • 現行案との差分、代替案、感度、制約余裕を示すと現場が判断しやすくなります。
  • 予測誤差が意思決定へ与える影響は、シナリオまたはシミュレーションで検証します。
  • 推奨案の手修正は失敗ではなく、未モデル化制約を発見する重要なログです。

実務導入する場合に必要なこと

論点確認事項成果物
業務定義誰が、いつ、何を決めるか意思決定フロー、RACI
データ需要、能力、BOM、在庫、技能の時点整合データ辞書、品質レポート
数理モデル目的、制約、粒度、計算時間モデル仕様、テストケース
検証現行案、過去実績、ストレス条件との比較効果検証、感度分析
運用再計画条件、承認、障害時の代替手順運用手順、監視指標
統制入力版、推奨、修正、承認の追跡監査ログ、変更管理

導入は小さな意思決定から始め、シャドー運用で人の計画と比較し、制約漏れを直してから適用範囲を広げます。

まとめ

No.091〜No.100では、個別の最適化を製造業の意思決定ループへ統合しました。よいモデルとは、複雑なモデルではなく、現場の制約を守り、結果の理由を説明でき、実績から更新できるモデルです。まず一つの工場・一つの会議体・一つのKPIを対象に、現行判断を再現するところから始めるのが現実的です。

法人向けのご相談

数理工房では、生産計画、スケジューリング、在庫・発注、人員配置、設備投資などについて、課題整理、PoC、モデル開発、既存システム連携、内製化支援までご相談いただけます。

📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。