100本ノック / 数理モデリング / 数理モデリング100本ノック
生産計画の曖昧な相談を意思決定モデルへ:精密部品工場で学ぶ数理モデリングの基本
生産計画の曖昧な相談を意思決定モデルへ
精密部品工場で学ぶ数理モデリングの基本 No.001〜No.010
数理モデリング100本ノックについて
本記事は、数理モデリング100本ノックの第1回です。この100本ノックは、現場の曖昧な課題を数理的な問いへ変換し、データ・数式・シミュレーション・最適化を使って、説明可能な意思決定へつなげる力を段階的に身につけることを目的としています。
全体は、次の10章で構成されています。
| 章 | No. | 主なテーマ |
|---|---|---|
| 第1章 | 001〜010 | 数理モデリングの基本 |
| 第2章 | 011〜020 | データを数式で表す |
| 第3章 | 021〜030 | 業務KPIのモデル化 |
| 第4章 | 031〜040 | 需要・売上のモデル化 |
| 第5章 | 041〜050 | 在庫・生産のモデル化 |
| 第6章 | 051〜060 | 待ち行列・混雑のモデル化 |
| 第7章 | 061〜070 | 不確実性とリスクのモデル化 |
| 第8章 | 071〜080 | 状態遷移モデル |
| 第9章 | 081〜090 | 最適化のためのモデル化 |
| 第10章 | 091〜100 | シミュレーションと意思決定 |
今回はその土台となるNo.001〜010を扱います。精密部品工場の生産計画を題材に、現場の相談を数理的な問いへ変換し、変数・パラメータ・状態・仮定・単位・粒度・制約・目的関数を整理して、モデルの妥当性を確かめるまでを一続きで確認します。
高度な最適化手法を使う前に必要なのは、**「何を決めたいのか」「何を固定して考えるのか」「どの条件を守るのか」**を関係者が同じ言葉で説明できる状態です。Pythonで架空データを作り、表・グラフ・簡単なシミュレーションを通して、数理モデリングを製造業の意思決定へ接続します。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。 掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
はじめに:この記事で扱う製造業の実務課題
架空の精密部品工場では、営業部から「来月は需要が増えるので、欠品を避けつつ在庫も増やしすぎない計画を作ってほしい」と依頼されました。生産管理には、計画数、実績良品数、需要、設備停止時間、不良数、期末在庫の記録があります。
しかし、この依頼のままでは計算できません。「欠品を避ける」は欠品ゼロを意味するのか、一定のサービス水準でよいのか。「在庫を増やしすぎない」は何個までか。増産には設備時間・作業時間・材料の上限もあります。
今回のゴールは、唯一の正解を出すことではありません。現場の言葉を、検証可能で更新可能な意思決定モデルに翻訳する基本動作を身につけることです。
現場でよくある状況
- 需要予測、生産計画、設備実績、品質実績が別々のExcelで管理されている
- 「余裕を持つ」「極力減らす」など、判断基準が定性的なままである
- 日次・週次・月次の数字が混在し、同じKPIでも部署ごとに定義が異なる
- 平均能力だけで計画し、日々の停止や品種切替による山谷を見落とす
- 試算結果は出せても、どの仮定が結果に効いたのか説明できない
数理モデルは、現場を完全に再現する精密模型ではありません。意思決定に必要な構造だけを残し、入力と判断結果の関係を明示するための道具です。
なぜこの問題は判断が難しいのか
生産計画では、複数の指標が同時に動きます。生産量を増やせば欠品は減りやすくなりますが、残業費、仕掛在庫、完成品在庫が増える可能性があります。設備停止や不良率は日によって変動するため、平均値だけでは繁忙日のリスクを表現できません。
また、次の3種類を混同すると議論が不安定になります。
- 事実:過去の良品数、停止時間、需要など、観測された値
- 仮定:将来も歩留まりが同程度、特急注文は通常需要に含めない、などの置き方
- 意思決定:製品別生産量、残業時間、在庫目標など、変更可能な値
数理モデリングでは、これらを分離し、単位・集計粒度・制約・評価軸をそろえてから計算します。
今回扱うノックの全体像
| No. | テーマ | 生産管理での問い |
|---|---|---|
| 001 | 数理モデリングとは何か | 現場の一部をどこまで数式で表すか |
| 002 | 業務課題を数理的な問いに変換 | 「欠品を避けたい」を何で測るか |
| 003 | 変数・パラメータ・定数 | 変える値、推定する値、固定する値は何か |
| 004 | 入力・出力・状態 | 在庫が日々どう引き継がれるか |
| 005 | 仮定を明確にする | 停止・歩留まりの置き方で結論は変わるか |
| 006 | 単位・スケール・粒度 | 秒・時間・個、日次・週次をどうそろえるか |
| 007 | モデルの粒度を決める | 月平均では見えない不足をどこまで追うか |
| 008 | 制約条件を整理する | 設備・作業・材料の上限内か |
| 009 | 目的関数を理解する | 利益、欠品、在庫をどう一つの評価値にするか |
| 010 | 妥当性を確認する | 過去データに対して使える精度か |
No.001〜010は独立した小技ではなく、課題設定 → 定式化 → 評価 → 検証という一連の流れです。
Python 環境の準備
外部データは使いません。NumPyで乱数を固定して架空データを生成し、pandasで集計、matplotlibで可視化します。グラフはMarkdown変換後も読みやすいようSVG形式で表示します。
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import platform
import sys
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from IPython.display import display
SEED = 42
rng = np.random.default_rng(SEED)
plt.rcParams["font.family"] = "Hiragino Sans"
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["figure.figsize"] = (9, 4.8)
print(f"Python : {sys.version.split()[0]}")
print(f"NumPy : {np.__version__}")
print(f"pandas : {pd.__version__}")
print(f"matplotlib : {matplotlib.__version__}")
print(f"platform : {platform.platform()}")
print(f"random seed: {SEED}")
Python : 3.13.1
NumPy : 2.5.1
pandas : 3.0.3
matplotlib : 3.11.0
platform : macOS-26.3-arm64-arm-64bit-Mach-O
random seed: 42
架空データの作成
対象は精密ポンプ部品を製造する架空工場です。製品はA・B・Cの3種類、期間は2025年4月1日から8週間(56日)とします。1行が「1日×1製品」の記録です。
planned_units:当日の計画数量demand_units:当日の需要数量downtime_min:設備停止時間(分)defect_rate:実績不良率good_units:生産できた良品数ending_inventory:需要充当後の期末在庫
需要の曜日性、設備停止、歩留まり、製品別の能力差を入れています。すべて架空値で、乱数シードは 42 に固定します。
dates = pd.date_range("2025-04-01", periods=56, freq="D")
products = pd.DataFrame({
"product": ["A", "B", "C"],
"daily_capacity": [520, 430, 340],
"standard_defect_rate": [0.018, 0.025, 0.032],
"cycle_time_sec": [48, 62, 78],
"material_kg_per_unit": [0.42, 0.55, 0.68],
"unit_margin_yen": [850, 1100, 1450],
})
rows = []
for day_no, date in enumerate(dates):
weekday_factor = 0.88 if date.dayofweek >= 5 else 1.04
trend = 1 + 0.0018 * day_no
for spec in products.itertuples(index=False):
demand = max(0, rng.normal(spec.daily_capacity * 0.82 * weekday_factor * trend, 28))
planned = max(0, rng.normal(spec.daily_capacity * 0.86, 18))
downtime = np.clip(rng.gamma(shape=1.8, scale=16), 0, 150)
defect_rate = np.clip(rng.normal(spec.standard_defect_rate, 0.006), 0.004, 0.07)
available_ratio = max(0, 1 - downtime / (16 * 60))
produced = min(planned, spec.daily_capacity * available_ratio)
good_units = np.floor(produced * (1 - defect_rate))
rows.append({
"date": date,
"product": spec.product,
"planned_units": int(round(planned)),
"demand_units": int(round(demand)),
"downtime_min": round(float(downtime), 1),
"defect_rate": float(defect_rate),
"good_units": int(good_units),
})
production = pd.DataFrame(rows).merge(products, on="product", how="left")
production["week"] = ((production["date"] - production["date"].min()).dt.days // 7) + 1
inventory = {"A": 650, "B": 520, "C": 420}
ending_inventory, shortage_units = [], []
for row in production.itertuples(index=False):
available = inventory[row.product] + row.good_units
shipped = min(available, row.demand_units)
shortage = row.demand_units - shipped
inventory[row.product] = available - shipped
ending_inventory.append(inventory[row.product])
shortage_units.append(shortage)
production["ending_inventory"] = ending_inventory
production["shortage_units"] = shortage_units
print(f"レコード数: {len(production):,} 行({production['date'].nunique()}日 × {production['product'].nunique()}製品)")
display(production.head(9).style.format({"defect_rate": "{:.2%}"}))
レコード数: 168 行(56日 × 3製品)
| date | product | planned_units | demand_units | downtime_min | defect_rate | good_units | daily_capacity | standard_defect_rate | cycle_time_sec | material_kg_per_unit | unit_margin_yen | week | ending_inventory | shortage_units | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2025-04-01 00:00:00 | A | 428 | 452 | 41.200000 | 0.63% | 425 | 520 | 0.018000 | 48 | 0.420000 | 850 | 1 | 623 | 0 |
| 1 | 2025-04-01 00:00:00 | B | 372 | 330 | 17.900000 | 1.99% | 364 | 430 | 0.025000 | 62 | 0.550000 | 1100 | 1 | 554 | 0 |
| 2 | 2025-04-01 00:00:00 | C | 306 | 315 | 24.800000 | 3.48% | 295 | 340 | 0.032000 | 78 | 0.680000 | 1450 | 1 | 400 | 0 |
| 3 | 2025-04-02 00:00:00 | A | 454 | 420 | 9.400000 | 1.77% | 445 | 520 | 0.018000 | 48 | 0.420000 | 850 | 1 | 648 | 0 |
| 4 | 2025-04-02 00:00:00 | B | 358 | 362 | 56.000000 | 2.24% | 349 | 430 | 0.025000 | 62 | 0.550000 | 1100 | 1 | 541 | 0 |
| 5 | 2025-04-02 00:00:00 | C | 302 | 281 | 31.300000 | 3.46% | 291 | 340 | 0.032000 | 78 | 0.680000 | 1450 | 1 | 410 | 0 |
| 6 | 2025-04-03 00:00:00 | A | 440 | 505 | 14.900000 | 2.17% | 430 | 520 | 0.018000 | 48 | 0.420000 | 850 | 1 | 573 | 0 |
| 7 | 2025-04-03 00:00:00 | B | 368 | 400 | 10.700000 | 2.89% | 357 | 430 | 0.025000 | 62 | 0.550000 | 1100 | 1 | 498 | 0 |
| 8 | 2025-04-03 00:00:00 | C | 302 | 312 | 12.800000 | 3.27% | 292 | 340 | 0.032000 | 78 | 0.680000 | 1450 | 1 | 390 | 0 |
overview = production.groupby("product", as_index=False).agg(
計画数=("planned_units", "sum"),
良品数=("good_units", "sum"),
需要数=("demand_units", "sum"),
欠品数=("shortage_units", "sum"),
平均不良率=("defect_rate", "mean"),
停止時間_分=("downtime_min", "sum"),
最終在庫=("ending_inventory", "last"),
)
display(overview.style.format({"平均不良率": "{:.2%}", "停止時間_分": "{:,.1f}"}))
print(
f"全期間合計 | 良品 {production['good_units'].sum():,}個 / "
f"需要 {production['demand_units'].sum():,}個 / 欠品 {production['shortage_units'].sum():,}個"
)
weekly = production.groupby("week", as_index=False).agg(
良品数=("good_units", "sum"), 需要数=("demand_units", "sum"), 欠品数=("shortage_units", "sum")
)
ax = weekly.plot(x="week", y=["良品数", "需要数"], marker="o", linewidth=2)
ax.set_title("週別の良品数と需要数(全製品合計)")
ax.set_xlabel("週")
ax.set_ylabel("数量(個)")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
| product | 計画数 | 良品数 | 需要数 | 欠品数 | 平均不良率 | 停止時間_分 | 最終在庫 | |
|---|---|---|---|---|---|---|---|---|
| 0 | A | 24805 | 24330 | 24823 | 0 | 1.71% | 1,531.5 | 157 |
| 1 | B | 20776 | 20241 | 20547 | 0 | 2.43% | 1,427.9 | 214 |
| 2 | C | 16272 | 15722 | 16205 | 66 | 3.20% | 1,767.8 | 3 |
全期間合計 | 良品 60,293個 / 需要 61,575個 / 欠品 66個
No.001:数理モデリングとは何かを理解する
実務での意味
数理モデリングとは、現場のすべてを再現することではなく、判断に関係する要素を選び、数量間の関係として表すことです。たとえば良品数を考えるなら、計画数、設備の利用可能割合、歩留まりを残し、作業者名や伝票番号は今回の判断に不要なら省きます。
分析・モデル化の考え方
単純化した良品数モデルを次のように置きます。
- :日 ・製品 の予測良品数
- :計画数
- :1日当たり能力
- :16時間(960分)に対する設備利用可能割合
- :標準不良率
この式は、計画数と停止後能力の小さい方に標準歩留まりを掛けるモデルです。現実の要因を絞っているため、誤差は残ります。その誤差が意思決定上許容できるかはNo.010で確認します。
Pythonで確認する
production["modeled_good_units"] = np.floor(
np.minimum(
production["planned_units"],
production["daily_capacity"] * (1 - production["downtime_min"] / 960),
) * (1 - production["standard_defect_rate"])
).astype(int)
model_example = production.loc[:8, [
"date", "product", "planned_units", "downtime_min",
"good_units", "modeled_good_units"
]].copy()
model_example["error_units"] = model_example["modeled_good_units"] - model_example["good_units"]
display(model_example)
| date | product | planned_units | downtime_min | good_units | modeled_good_units | error_units | |
|---|---|---|---|---|---|---|---|
| 0 | 2025-04-01 | A | 428 | 41.2 | 425 | 420 | -5 |
| 1 | 2025-04-01 | B | 372 | 17.9 | 364 | 362 | -2 |
| 2 | 2025-04-01 | C | 306 | 24.8 | 295 | 296 | 1 |
| 3 | 2025-04-02 | A | 454 | 9.4 | 445 | 445 | 0 |
| 4 | 2025-04-02 | B | 358 | 56.0 | 349 | 349 | 0 |
| 5 | 2025-04-02 | C | 302 | 31.3 | 291 | 292 | 1 |
| 6 | 2025-04-03 | A | 440 | 14.9 | 430 | 432 | 2 |
| 7 | 2025-04-03 | B | 368 | 10.7 | 357 | 358 | 1 |
| 8 | 2025-04-03 | C | 302 | 12.8 | 292 | 292 | 0 |
結果の読み取り
予測値は実績良品数と完全には一致しません。標準不良率だけを使い、日々の微小停止や品質変動を省略しているためです。一方、計画数・能力・停止・歩留まりから良品数が決まるという骨格は説明できます。
実務では「精密さ」だけでなく、どの判断に使うモデルかで必要精度を決めます。月次の概算能力を見るモデルと、当日の投入順序を決めるモデルでは、必要な粒度が異なります。
No.002:業務課題を数理的な問いに変換する
実務での意味
「欠品を避けたい」だけでは達成判定ができません。対象期間、対象製品、需要の定義、許容欠品量を決めることで、関係者が同じ問いを検討できます。
分析・モデル化の考え方
今回は業務課題を次の問いに変換します。
各週・各製品について、期首在庫と予測良品数で需要を満たせるか。満たせない場合、何個不足するか。
週 ・製品 の需給差を とすると、
不足量は です。この定義により、「危ない」を不足個数というKPIへ置き換えられます。
Pythonで確認する
weekly_product = production.groupby(["week", "product"], as_index=False).agg(
予測良品数=("modeled_good_units", "sum"),
需要数=("demand_units", "sum"),
)
weekly_product["期首在庫"] = weekly_product["product"].map({"A": 650, "B": 520, "C": 420})
weekly_product.loc[weekly_product["week"] > 1, "期首在庫"] = 0
weekly_product["需給差"] = (
weekly_product["期首在庫"] + weekly_product["予測良品数"] - weekly_product["需要数"]
)
weekly_product["不足見込"] = (-weekly_product["需給差"]).clip(lower=0)
display(weekly_product.pivot(index="week", columns="product", values="需給差").style
.format("{:+,.0f}").background_gradient(cmap="RdYlGn", vmin=-250, vmax=250))
print(f"単純週次判定で不足が見込まれる組み合わせ: {(weekly_product['不足見込'] > 0).sum()}件 / {len(weekly_product)}件")
| product | A | B | C |
|---|---|---|---|
| week | |||
| 1 | +677 | +540 | +483 |
| 2 | +136 | -44 | -17 |
| 3 | +61 | +59 | -105 |
| 4 | -98 | -97 | +53 |
| 5 | -212 | +14 | -120 |
| 6 | -116 | -241 | -142 |
| 7 | -183 | +6 | -99 |
| 8 | -128 | -36 | -114 |
単純週次判定で不足が見込まれる組み合わせ: 15件 / 24件
結果の読み取り
同じ「欠品懸念」でも、製品と週に分解すると優先順位が見えます。マイナスの週は、前週からの繰越を無視した簡易判定では需要が予測良品数を上回る週です。
ここで大切なのは、計算前に問いを固定したことです。実運用では「期末在庫を翌週へ繰り越す」「受注残を翌日に持ち越す」「安全在庫を需要に上乗せする」など、業務ルールを追加します。
No.003:変数・パラメータ・定数の違いを理解する
実務での意味
モデルの数値をすべて同じように扱うと、責任の所在が曖昧になります。生産数量は会議で決める値、歩留まりは実績から推定する値、1時間=60分は定義上固定する値です。
分析・モデル化の考え方
| 種類 | 意味 | 今回の例 |
|---|---|---|
| 意思決定変数 | 担当者が選ぶ値 | 製品別の生産計画数 |
| パラメータ | データや見積りから与える値 | 不良率 、需要 、能力 |
| 定数 | 定義上固定して扱う値 | 1時間=60分、対象期間=7日 |
単純な予測良品数 でも、変数 とパラメータ の区別がつけば、「増産」と「品質改善」の効果を別々に試算できます。
Pythonで確認する
base_plan = 3_000 # 意思決定変数 x
defect_candidates = [0.015, 0.025, 0.035] # パラメータ r の想定
plan_candidates = [2_800, 3_000, 3_200]
sensitivity = pd.DataFrame([
{"計画数_x": x, "不良率_r": r, "予測良品数_Q": round(x * (1 - r))}
for x in plan_candidates for r in defect_candidates
])
pivot = sensitivity.pivot(index="計画数_x", columns="不良率_r", values="予測良品数_Q")
pivot.columns = [f"不良率 {r:.1%}" for r in pivot.columns]
display(pivot)
| 不良率 1.5% | 不良率 2.5% | 不良率 3.5% | |
|---|---|---|---|
| 計画数_x | |||
| 2800 | 2758 | 2730 | 2702 |
| 3000 | 2955 | 2925 | 2895 |
| 3200 | 3152 | 3120 | 3088 |
結果の読み取り
同じ3,000個の計画でも、不良率の想定が1.5%から3.5%へ変われば良品数は変わります。逆に、不良率を固定すれば、計画数を増やす効果を比較できます。
実務では、意思決定変数には承認権限、パラメータには更新元と更新頻度を付けます。たとえば「標準不良率は品質管理が毎月更新」「生産計画は生産管理が週次決定」と決めると、モデルが属人化しにくくなります。
No.004:入力・出力・状態を整理する
実務での意味
在庫や仕掛品は、前日の結果が翌日に引き継がれます。日ごとに独立した集計ではなく、状態を持つモデルが必要です。
分析・モデル化の考え方
製品 の在庫状態を次式で更新します。
- 入力:当日良品数 、需要
- 状態:期首・期末在庫
- 出力:出荷可能数、欠品数
今回は失注型として欠品を翌日に持ち越しません。受注残として持ち越す業務なら、バックオーダー状態を追加します。
Pythonで確認する
sample_a = production.query("product == 'A'").head(14).copy()
state = 650
states = []
for row in sample_a.itertuples(index=False):
begin = state
available = begin + row.good_units
shortage = max(0, row.demand_units - available)
state = max(0, available - row.demand_units)
states.append((begin, row.good_units, row.demand_units, state, shortage))
state_table = pd.DataFrame(states, columns=["期首在庫", "良品数", "需要数", "期末在庫", "欠品数"])
state_table.insert(0, "日付", sample_a["date"].dt.strftime("%m-%d").to_numpy())
display(state_table)
ax = state_table.plot(x="日付", y="期末在庫", marker="o", color="#2c7fb8", legend=False)
ax.axhline(300, color="#d95f0e", linestyle="--", label="管理下限 300個")
ax.set_title("製品A:在庫状態の推移(最初の14日)")
ax.set_xlabel("日付")
ax.set_ylabel("期末在庫(個)")
ax.grid(True, alpha=0.3)
ax.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
| 日付 | 期首在庫 | 良品数 | 需要数 | 期末在庫 | 欠品数 | |
|---|---|---|---|---|---|---|
| 0 | 04-01 | 650 | 425 | 452 | 623 | 0 |
| 1 | 04-02 | 623 | 445 | 420 | 648 | 0 |
| 2 | 04-03 | 648 | 430 | 505 | 573 | 0 |
| 3 | 04-04 | 573 | 454 | 452 | 575 | 0 |
| 4 | 04-05 | 575 | 431 | 331 | 675 | 0 |
| 5 | 04-06 | 675 | 432 | 398 | 709 | 0 |
| 6 | 04-07 | 709 | 441 | 467 | 683 | 0 |
| 7 | 04-08 | 683 | 426 | 443 | 666 | 0 |
| 8 | 04-09 | 666 | 440 | 421 | 685 | 0 |
| 9 | 04-10 | 685 | 436 | 422 | 699 | 0 |
| 10 | 04-11 | 699 | 420 | 447 | 672 | 0 |
| 11 | 04-12 | 672 | 455 | 357 | 770 | 0 |
| 12 | 04-13 | 770 | 441 | 420 | 791 | 0 |
| 13 | 04-14 | 791 | 450 | 429 | 812 | 0 |
結果の読み取り
在庫は、その日の良品数と需要だけでなく、前日の期末在庫に左右されます。したがって、需要が生産を上回った日が直ちに欠品日になるとは限りません。一方、在庫が連続して減ると、後日の小さな需要増でも欠品につながります。
状態を明示すると、「いつ補充判断をするか」「管理下限を何個にするか」という運用ルールへ接続できます。
No.005:モデル化に必要な仮定を明確にする
実務での意味
将来の停止時間や不良率は確定していません。仮定を隠した単一予測より、仮定を変えた複数シナリオを示す方が、意思決定者はリスクを理解できます。
分析・モデル化の考え方
製品Bの翌週計画について、次の仮定を比較します。
- 楽観:停止時間10分/日、不良率2.0%
- 基準:停止時間30分/日、不良率2.5%
- 慎重:停止時間75分/日、不良率4.0%
各シナリオで、 を計算します。需要は3,000個、期首在庫は200個と仮定します。仮定値は予測ではなく、判断の幅を見るための入力です。
Pythonで確認する
scenario = pd.DataFrame({
"シナリオ": ["楽観", "基準", "慎重"],
"停止時間_分日": [10, 30, 75],
"不良率": [0.020, 0.025, 0.040],
})
days = 7
daily_plan_b = 430
demand_b = 3_000
begin_inventory_b = 200
scenario["予測良品数"] = np.floor(
np.minimum(daily_plan_b, 430 * (1 - scenario["停止時間_分日"] / 960))
* (1 - scenario["不良率"]) * days
).astype(int)
scenario["需給余力"] = begin_inventory_b + scenario["予測良品数"] - demand_b
display(scenario.style.format({"不良率": "{:.1%}", "需給余力": "{:+,}"}))
| シナリオ | 停止時間_分日 | 不良率 | 予測良品数 | 需給余力 | |
|---|---|---|---|---|---|
| 0 | 楽観 | 10 | 2.0% | 2919 | +119 |
| 1 | 基準 | 30 | 2.5% | 2843 | +43 |
| 2 | 慎重 | 75 | 4.0% | 2663 | -137 |
結果の読み取り
同じ計画数量でも、停止時間と不良率の仮定により需給余力が変わります。基準シナリオだけで余裕が出ても、慎重シナリオで不足するなら、保全予定の調整、応援要員、外注、事前在庫などを検討する根拠になります。
実務では仮定一覧に、根拠、責任者、更新日、有効期間を持たせます。「仮定が外れたら再計算する」運用まで含めてモデルです。
No.006:単位・スケール・粒度をそろえる
実務での意味
製造データでは、サイクルタイムは秒/個、停止時間は分、稼働枠は時間、材料はkgで記録されることがあります。単位をそろえずに加減乗除すると、計算は動いても意味が崩れます。
分析・モデル化の考え方
生産時間は次式で時間へ変換します。
は生産個数、 は秒/個、 は時間です。分母の3,600は、秒を時間へ変換する定数です。また、比較するときは日次同士、週次同士のように期間粒度をそろえます。
Pythonで確認する
unit_check = products[["product", "cycle_time_sec", "material_kg_per_unit"]].copy()
unit_check["計画数量_個"] = [3_200, 2_700, 2_100]
unit_check["必要設備時間_時間"] = (
unit_check["計画数量_個"] * unit_check["cycle_time_sec"] / 3_600
)
unit_check["必要材料_kg"] = (
unit_check["計画数量_個"] * unit_check["material_kg_per_unit"]
)
display(unit_check.style.format({"必要設備時間_時間": "{:,.1f}", "必要材料_kg": "{:,.1f}"}))
daily_total = production.groupby("date", as_index=False)[["good_units", "demand_units"]].sum()
weekly_total = production.groupby("week", as_index=False)[["good_units", "demand_units"]].sum()
print(f"日次平均需要: {daily_total['demand_units'].mean():,.1f} 個/日")
print(f"週次平均需要: {weekly_total['demand_units'].mean():,.1f} 個/週")
print(f"日次平均 × 7: {daily_total['demand_units'].mean() * 7:,.1f} 個/週")
| product | cycle_time_sec | material_kg_per_unit | 計画数量_個 | 必要設備時間_時間 | 必要材料_kg | |
|---|---|---|---|---|---|---|
| 0 | A | 48 | 0.420000 | 3200 | 42.7 | 1,344.0 |
| 1 | B | 62 | 0.550000 | 2700 | 46.5 | 1,485.0 |
| 2 | C | 78 | 0.680000 | 2100 | 45.5 | 1,428.0 |
日次平均需要: 1,099.6 個/日
週次平均需要: 7,696.9 個/週
日次平均 × 7: 7,696.9 個/週
結果の読み取り
秒/個に個数を掛けた値は秒なので、3,600で割って初めて時間単位の設備枠と比較できます。同様に、kg/個に個数を掛けると必要材料kgになります。
データ辞書には列名だけでなく、単位、時間粒度、税込・税抜、良品・総生産などの定義を記載します。単位を列名へ含めることも、事故を減らす実務的な方法です。
No.007:モデルの粒度を決める
実務での意味
週合計で需給が合っていても、週前半に需要が集中すれば日次欠品が起こり得ます。細かいモデルほど情報を残せますが、データ整備・計算・説明の負担も増えます。
分析・モデル化の考え方
粒度は「細かいほど良い」のではなく、判断の周期とリスクに合わせます。
- 月次:中期能力や予算の検討
- 週次:生産量・人員・材料の調整
- 日次:在庫と納期の管理
- 時間帯別:投入順序、段取り、ボトルネック管理
ここでは第6週の製品Cについて、日次不足と週合計の需給差を比較します。
Pythonで確認する
granularity = production.query("week == 6 and product == 'C'").copy()
granularity["日次需給差"] = granularity["good_units"] - granularity["demand_units"]
granularity_view = granularity[["date", "good_units", "demand_units", "日次需給差"]]
display(granularity_view.style.format({"日次需給差": "{:+,}"}))
weekly_gap = granularity["日次需給差"].sum()
daily_negative = (-granularity["日次需給差"]).clip(lower=0).sum()
print(f"週合計の需給差 : {weekly_gap:+,} 個")
print(f"日次の不足側だけの合計 : {daily_negative:,} 個")
print(f"比較する行数(日次/週次): {len(granularity)} 行 / 1 行")
fig, ax = plt.subplots()
colors = np.where(granularity["日次需給差"] >= 0, "#2ca25f", "#de2d26")
ax.bar(granularity["date"].dt.strftime("%m-%d"), granularity["日次需給差"], color=colors)
ax.axhline(0, color="black", linewidth=0.8)
ax.set_title("製品C・第6週:日次需給差(良品数-需要数)")
ax.set_xlabel("日付")
ax.set_ylabel("需給差(個/日)")
ax.grid(True, axis="y", alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
| date | good_units | demand_units | 日次需給差 | |
|---|---|---|---|---|
| 107 | 2025-05-06 00:00:00 | 298 | 264 | +34 |
| 110 | 2025-05-07 00:00:00 | 286 | 327 | -41 |
| 113 | 2025-05-08 00:00:00 | 294 | 334 | -40 |
| 116 | 2025-05-09 00:00:00 | 242 | 320 | -78 |
| 119 | 2025-05-10 00:00:00 | 275 | 227 | +48 |
| 122 | 2025-05-11 00:00:00 | 264 | 278 | -14 |
| 125 | 2025-05-12 00:00:00 | 275 | 325 | -50 |
週合計の需給差 : -141 個
日次の不足側だけの合計 : 223 個
比較する行数(日次/週次): 7 行 / 1 行
結果の読み取り
週合計は、週内のプラスとマイナスを相殺します。期首在庫が十分なら相殺で問題ありませんが、在庫が少ない、当日納入が必要、前倒し生産ができない場合には日次の不足が重要です。
必要な粒度は業務ルールで決まります。今回なら、週次で粗い能力計画を作り、欠品リスクが高い製品だけ日次へ掘り下げる二段階設計が現実的です。
No.008:制約条件を整理する
実務での意味
「たくさん作る」という案が、設備時間、作業時間、材料、保管場所を超えていれば実行できません。制約は、候補案を実行可能・不可能に分ける条件です。
分析・モデル化の考え方
翌週の製品A・Bの生産量を とします。簡略化のため、次の制約を置きます。
さらに , とします。ここでは候補を100個刻みで全探索し、制約を満たす計画を確認します。
Pythonで確認する
plans = []
for x_a in range(0, 3_601, 100):
for x_b in range(0, 3_001, 100):
machine_h = (48 * x_a + 62 * x_b) / 3_600
labor_h = 0.030 * x_a + 0.040 * x_b
material_kg = 0.42 * x_a + 0.55 * x_b
feasible = machine_h <= 86 and labor_h <= 190 and material_kg <= 2_550
plans.append((x_a, x_b, machine_h, labor_h, material_kg, feasible))
plans = pd.DataFrame(plans, columns=[
"x_A", "x_B", "設備時間", "作業時間", "材料kg", "実行可能"
])
print(f"候補数: {len(plans):,}件 / 実行可能: {plans['実行可能'].sum():,}件")
display(plans.query("実行可能").sort_values(["x_A", "x_B"], ascending=False).head(10)
.style.format({"設備時間": "{:.1f}", "作業時間": "{:.1f}", "材料kg": "{:.1f}"}))
fig, ax = plt.subplots()
for feasible, group in plans.groupby("実行可能"):
ax.scatter(group["x_A"], group["x_B"], s=13, alpha=0.55,
label="実行可能" if feasible else "制約違反",
color="#2ca25f" if feasible else "#bdbdbd")
ax.set_title("製品A・Bの生産計画候補と実行可能領域")
ax.set_xlabel("製品A 生産量 x_A(個/週)")
ax.set_ylabel("製品B 生産量 x_B(個/週)")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
候補数: 1,147件 / 実行可能: 1,052件
| x_A | x_B | 設備時間 | 作業時間 | 材料kg | 実行可能 | |
|---|---|---|---|---|---|---|
| 1134 | 3600 | 1800 | 79.0 | 180.0 | 2502.0 | True |
| 1133 | 3600 | 1700 | 77.3 | 176.0 | 2447.0 | True |
| 1132 | 3600 | 1600 | 75.6 | 172.0 | 2392.0 | True |
| 1131 | 3600 | 1500 | 73.8 | 168.0 | 2337.0 | True |
| 1130 | 3600 | 1400 | 72.1 | 164.0 | 2282.0 | True |
| 1129 | 3600 | 1300 | 70.4 | 160.0 | 2227.0 | True |
| 1128 | 3600 | 1200 | 68.7 | 156.0 | 2172.0 | True |
| 1127 | 3600 | 1100 | 66.9 | 152.0 | 2117.0 | True |
| 1126 | 3600 | 1000 | 65.2 | 148.0 | 2062.0 | True |
| 1125 | 3600 | 900 | 63.5 | 144.0 | 2007.0 | True |
結果の読み取り
灰色の候補は、少なくとも一つの制約に違反します。緑色の範囲が現状条件で実行可能な計画です。制約を可視化すると、「設備を増やすべきか」「材料調達を増やすべきか」「作業改善が効くか」を議論しやすくなります。
本番モデルでは、段取り回数、最低ロット、整数数量、製品Cとの共用設備、保全時間なども制約候補です。最初からすべてを入れず、計画の実行可能性を左右する条件から追加します。
No.009:目的関数の考え方を理解する
実務での意味
実行可能な計画は一つとは限りません。候補の中から「どれが望ましいか」を比較する基準が目的関数です。利益最大化だけでなく、欠品、残業、在庫、納期遅延を含める場合があります。
分析・モデル化の考え方
需要を , 、初期在庫を各100個とし、評価利益を次のように置きます。
- :販売できた1個当たり限界利益
- :欠品数、:欠品ペナルティ
- :期末在庫、:在庫保有費
- :80時間を超える設備時間、:追加稼働費
ペナルティは「真の損失」ではなく、経営上の優先度を数値化したものです。値を変えた感度分析が必要です。
Pythonで確認する
demand = {"A": 3_100, "B": 2_500}
initial_inventory = {"A": 100, "B": 100}
margin = {"A": 850, "B": 1_100}
shortage_penalty = 1_800
holding_cost = 90
overtime_cost_per_h = 18_000
candidate = plans.query("実行可能").copy()
for p in ["A", "B"]:
available = candidate[f"x_{p}"] + initial_inventory[p]
candidate[f"販売_{p}"] = np.minimum(available, demand[p])
candidate[f"欠品_{p}"] = np.maximum(0, demand[p] - available)
candidate[f"在庫_{p}"] = np.maximum(0, available - demand[p])
candidate["追加稼働時間"] = np.maximum(0, candidate["設備時間"] - 80)
candidate["評価利益"] = (
candidate["販売_A"] * margin["A"] + candidate["販売_B"] * margin["B"]
- (candidate["欠品_A"] + candidate["欠品_B"]) * shortage_penalty
- (candidate["在庫_A"] + candidate["在庫_B"]) * holding_cost
- candidate["追加稼働時間"] * overtime_cost_per_h
)
best_plans = candidate.nlargest(8, "評価利益")[[
"x_A", "x_B", "設備時間", "作業時間", "欠品_A", "欠品_B",
"在庫_A", "在庫_B", "評価利益"
]]
display(best_plans.style.format({
"設備時間": "{:.1f}", "作業時間": "{:.1f}", "評価利益": "¥{:,.0f}"
}))
best = best_plans.iloc[0]
print(
f"推奨候補: A={int(best['x_A']):,}個、B={int(best['x_B']):,}個、"
f"評価利益=¥{best['評価利益']:,.0f}"
)
| x_A | x_B | 設備時間 | 作業時間 | 欠品_A | 欠品_B | 在庫_A | 在庫_B | 評価利益 | |
|---|---|---|---|---|---|---|---|---|---|
| 923 | 2900 | 2400 | 80.0 | 183.0 | 100 | 0 | 0 | 0 | ¥5,120,000 |
| 953 | 3000 | 2300 | 79.6 | 182.0 | 0 | 100 | 0 | 0 | ¥5,095,000 |
| 892 | 2800 | 2400 | 78.7 | 180.0 | 200 | 0 | 0 | 0 | ¥4,855,000 |
| 922 | 2900 | 2300 | 78.3 | 179.0 | 100 | 100 | 0 | 0 | ¥4,830,000 |
| 952 | 3000 | 2200 | 77.9 | 178.0 | 0 | 200 | 0 | 0 | ¥4,805,000 |
| 983 | 3100 | 2200 | 79.2 | 181.0 | 0 | 200 | 100 | 0 | ¥4,796,000 |
| 861 | 2700 | 2400 | 77.3 | 177.0 | 300 | 0 | 0 | 0 | ¥4,590,000 |
| 862 | 2700 | 2500 | 79.1 | 181.0 | 300 | 0 | 0 | 100 | ¥4,581,000 |
推奨候補: A=2,900個、B=2,400個、評価利益=¥5,120,000
結果の読み取り
実行可能な候補を目的関数で順位付けすると、推奨計画と近接候補を比較できます。首位だけを提示するのではなく、2位・3位との差、欠品内訳、制約の余裕も示すと、現場が採用可否を判断しやすくなります。
目的関数は経営方針を数式へ落とす場所です。重要顧客向け欠品を一般在庫より重くする、残業の上限をペナルティではなく制約にするなど、合意形成が必要です。
No.010:モデルの妥当性を確認する観点を整理する
実務での意味
数式が正しく動いても、現場判断に使えるとは限りません。過去データで予測誤差を測り、製品別の偏りや大きな外れを確認します。平均誤差だけでなく、過大予測・過小予測の方向も重要です。
分析・モデル化の考え方
No.001の予測良品数を実績と比較し、次を確認します。
MAEは平均的な個数誤差、MAPEは規模に対する誤差率、Biasは過大・過小予測の方向を示します。さらに、入力定義、極端条件、業務ルール、説明可能性も妥当性確認の対象です。
Pythonで確認する
validation = production.copy()
validation["error"] = validation["modeled_good_units"] - validation["good_units"]
validation["abs_error"] = validation["error"].abs()
validation["ape"] = validation["abs_error"] / validation["good_units"].clip(lower=1)
metrics = validation.groupby("product", as_index=False).agg(
件数=("error", "size"),
MAE_個=("abs_error", "mean"),
MAPE=("ape", "mean"),
Bias_個=("error", "mean"),
最大絶対誤差_個=("abs_error", "max"),
)
overall = pd.DataFrame({
"product": ["全体"],
"件数": [len(validation)],
"MAE_個": [validation["abs_error"].mean()],
"MAPE": [validation["ape"].mean()],
"Bias_個": [validation["error"].mean()],
"最大絶対誤差_個": [validation["abs_error"].max()],
})
metrics_display = pd.concat([metrics, overall], ignore_index=True)
display(metrics_display.style.format({
"MAE_個": "{:.2f}", "MAPE": "{:.2%}", "Bias_個": "{:+.2f}", "最大絶対誤差_個": "{:.0f}"
}))
print(
f"全体指標 | MAE={validation['abs_error'].mean():.2f}個、"
f"MAPE={validation['ape'].mean():.2%}、Bias={validation['error'].mean():+.2f}個"
)
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
for product, group in validation.groupby("product"):
axes[0].scatter(group["good_units"], group["modeled_good_units"], s=24, alpha=0.6, label=product)
low = min(validation["good_units"].min(), validation["modeled_good_units"].min())
high = max(validation["good_units"].max(), validation["modeled_good_units"].max())
axes[0].plot([low, high], [low, high], "k--", linewidth=1)
axes[0].set_title("実績良品数とモデル予測の比較")
axes[0].set_xlabel("実績良品数(個/日)")
axes[0].set_ylabel("予測良品数(個/日)")
axes[0].grid(True, alpha=0.3)
axes[0].legend(title="製品")
axes[1].hist(validation["error"], bins=17, color="#756bb1", edgecolor="white")
axes[1].axvline(0, color="black", linestyle="--", linewidth=1)
axes[1].set_title("予測誤差の分布(予測-実績)")
axes[1].set_xlabel("誤差(個/日)")
axes[1].set_ylabel("件数")
axes[1].grid(True, axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
| product | 件数 | MAE_個 | MAPE | Bias_個 | 最大絶対誤差_個 | |
|---|---|---|---|---|---|---|
| 0 | A | 56 | 2.43 | 0.56% | -0.36 | 8 |
| 1 | B | 56 | 1.80 | 0.49% | -0.23 | 5 |
| 2 | C | 56 | 1.14 | 0.41% | +0.04 | 4 |
| 3 | 全体 | 168 | 1.79 | 0.49% | -0.18 | 8 |
全体指標 | MAE=1.79個、MAPE=0.49%、Bias=-0.18個
結果の読み取り
散布図が45度線に近いほど、予測と実績が近いことを示します。Biasが正なら平均的に過大予測、負なら過小予測です。製品別に誤差を出すことで、全体平均では隠れる偏りを確認できます。
このモデルは日々の実績不良率を予測前には知らないため、標準不良率を使っています。誤差が許容範囲を超えるなら、温度、作業班、段取り、材料ロットなどの説明変数を追加する候補になります。ただし、変数追加による保守負担と説明の難しさも同時に評価します。
対象ノックを通して見える実務上の示唆
今回の10本から得られる重要な示唆は、アルゴリズム選定より前に意思決定の設計があるということです。
- 業務課題を、対象・期間・KPI・許容範囲を持つ数理的な問いへ変える
- 意思決定変数、推定パラメータ、定数を分け、更新責任を決める
- 在庫のように前時点から引き継ぐ量は、状態としてモデル化する
- 仮定は隠さず、楽観・基準・慎重のシナリオで結論の頑健性を見る
- 単位と粒度をそろえ、判断周期に必要な細かさを選ぶ
- 制約で実行可能性を担保し、目的関数で望ましさを比較する
- 実績との誤差、偏り、例外条件を確認し、運用中も監視する
小さなモデルでも、前提と評価軸が共有されれば、経験知を否定せずに会議の論点を整理できます。
実務導入する場合に必要なこと
1. 意思決定と利用者を先に定義する
週次生産会議で製品別数量を決めるのか、日次朝会で投入順序を変えるのかを明確にします。判断周期がモデルの粒度と更新時刻を決めます。
2. データ定義と品質を整える
計画数・実績数・良品数・需要・出荷・欠品・在庫の定義、単位、締め時刻を統一します。欠測、後日修正、設備コード変更の扱いも必要です。
3. 仮定・制約・目的関数を関係部署で合意する
生産管理、製造、品質、保全、営業、経理で、能力、不良率、重要顧客、残業費、欠品影響を確認します。数式自体より、数式へ入れる業務ルールの合意が重要です。
4. 現行判断との並行評価から始める
最初から自動決定せず、モデル案と現行計画を並行して比較します。差が出た日をレビューし、未反映の制約や例外を洗い出します。
5. 監視と更新の仕組みを作る
MAE、Bias、欠品率、在庫、計画変更回数などを定期監視します。パラメータ更新頻度、再学習・再推定の条件、障害時の手動運用、モデル責任者も決めます。
6. 小さく導入し、効果を金額と運用指標で測る
一製品・一ラインから始め、欠品削減、残業削減、在庫削減、計画作成時間の短縮を測ります。効果と保守負担を確認してから対象を広げます。
まとめ
No.001〜010では、精密部品工場の架空データを使い、数理モデリングの土台を確認しました。
- モデルは現実のコピーではなく、意思決定に必要な構造の表現である
- 曖昧な課題は、対象・期間・KPIを持つ数理的な問いへ変換する
- 変数・パラメータ・定数、入力・出力・状態を区別する
- 仮定、単位、時間粒度を明示する
- 制約で実行可能性、目的関数で望ましさを表す
- 過去実績との誤差・偏りを確認し、利用目的に対する妥当性を判断する
数理モデリングの価値は、複雑な数式を使うことではなく、判断の前提とトレードオフを見える形にすることにあります。
法人向けのご相談
数理工房では、製造業における次のようなテーマを支援しています。
- 生産計画・要員配置・設備割当のモデル設計
- 在庫・欠品・納期・残業のトレードオフ整理
- 現場データの定義、KPI設計、可視化
- 数理最適化・シミュレーション・予測モデルのPoC
- 現場担当者・管理者向けの実データ型研修
- 既存Excel業務を起点とした意思決定支援の仕組み化
「課題はあるが、何を数式にすべきかわからない」という段階から、業務ヒアリング、データ確認、小規模な検証、実運用設計までご相談いただけます。
📩 お問い合わせ: surikobo.co.jp/contact まずはお気軽にご相談ください。