100本ノック / 確率統計 / 確率・統計マーケティング応用100本ノック

製造業の生産計画をPythonで学ぶ|ラインバランシングからデジタルツインまで10本ノック

変動に強い生産計画をつくる:多品種部品工場のライン設計・能力評価10本ノック

本記事では、架空の精密部品工場を題材に、ラインバランシング、ジョブショップ、フローショップ、スケジューリング、制約の定式化、シミュレーション最適化、ボトルネック、タクトタイム、生産能力、デジタルツインを一続きの意思決定として扱います。

個々の手法を計算して終えるのではなく、「納期を守れるか」「どこへ人・設備を配分するか」「需要変動にどこまで耐えられるか」を、再現可能な数値で説明することが目的です。掲載するデータは Python で生成し、外部データには依存しません。

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

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

対象は、複数品種を切削・研削・検査して出荷する工場です。受注量は日々変わり、品種ごとに工程順序と処理時間が異なります。生産管理者は、日々の差立てだけでなく、要員配置、設備増強、仕掛在庫、納期回答を同時に判断しなければなりません。

本記事では、決定を「計画値」と「現実の変動」に分けます。まず負荷と順序を静的に計算し、その後、停止や処理時間のばらつきをシミュレーションに入れ、最後にデジタルツインの小さな原型へつなげます。

現場でよくある状況

  • 月間計画では能力内だったのに、日々の順序と段取り替えで納期遅延が起きる
  • 各工程の稼働率を高めた結果、ボトルネック前の仕掛が増える
  • 標準時間は一つの値だが、設備停止・手直し・作業者差で実績が散らばる
  • 「設備を1台追加すべきか」「残業で吸収できるか」を平均値だけで決めてしまう
  • 現場実績を収集しても、計画モデルへ戻す仕組みがなく、マスタが陳腐化する

これらは別々の問題に見えますが、共通する原因は、工程の依存関係、有限能力、変動を同じ尺度で捉えていないことです。

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

生産時間の合計が勤務時間以下でも、納期を守れるとは限りません。前工程が終わらないと後工程を始められず、同じ設備を複数ジョブが同時利用できないためです。また、高い平均能力があっても、能力の分布の下側で需要を割り込めば欠品が発生します。

代表的な評価軸は、完了時刻(makespan)、総遅れ時間、仕掛在庫、スループット、稼働率、需要達成確率です。単一KPIの最大化では局所最適になりやすいため、納期・流動性・コスト・頑健性を併記します。

今回扱うノックの全体像

No.テーマ主な問い主な出力
041ラインバランシング作業を何工程へ分けるか工程負荷、編成効率
042ジョブショップ異なる経路の仕事をどう競合解消するかガントチャート、完了時刻
043フローショップ共通経路で投入順をどう選ぶか最短 makespan
044スケジューリング納期優先と短時間優先をどう使い分けるか遅れ時間比較
045制約条件の定式化計画案が有限能力を守るか制約余力
046シミュレーション最適化変動下でどの要員案を選ぶか費用、達成確率
047ボトルネック分析改善対象はどこか負荷率、余力
048タクトタイム需要から必要ペースをどう決めるかタクト、必要人数
049生産能力分析需要を何%の確率で満たせるか能力分布、下方分位点
050デジタルツイン実績で計画をどう更新するか状態推定、シナリオ比較

Python 環境の準備

NumPy は数値計算、pandas は工程・受注データの表現、matplotlib は可視化に利用します。乱数生成器は default_rng(42) に固定し、同じ条件なら同じ結果を再現できるようにします。

import itertools
import platform

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import japanize_matplotlib
from IPython.display import display

SEED = 42
rng = np.random.default_rng(SEED)
pd.set_option("display.max_columns", 20)
pd.set_option("display.float_format", lambda x: f"{x:,.2f}")

print(f"Python: {platform.python_version()}")
print(f"NumPy: {np.__version__} / pandas: {pd.__version__}")
print(f"乱数seed: {SEED}")
Python: 3.13.1
NumPy: 2.5.1 / pandas: 3.0.3
乱数seed: 42

架空データの作成

3製品(A・B・C)を、切削・研削・検査の3工程で生産する工場を想定します。標準時間は良品1個当たりの正味作業分、需要は1日当たりです。別途、組立ラインの要素作業と、当日受注ジョブも用意します。

実務では、標準時間の定義(正味時間か、余裕率込みか)とデータ粒度を揃えることが重要です。ここでは説明を明瞭にするため、単位をすべて「分」に統一します。

products = pd.DataFrame({
    "製品": ["A", "B", "C"],
    "日需要": [72, 48, 36],
    "切削_分": [3.2, 4.5, 2.8],
    "研削_分": [2.5, 3.0, 4.2],
    "検査_分": [1.4, 1.8, 1.6],
}).set_index("製品")

assembly_tasks = pd.DataFrame({
    "作業": ["部品取出", "圧入", "締結", "塗布", "調整", "通電検査", "外観検査", "梱包"],
    "標準時間_秒": [28, 46, 38, 32, 55, 44, 29, 36],
})

orders = pd.DataFrame({
    "ジョブ": ["J1", "J2", "J3", "J4", "J5", "J6"],
    "処理時間_分": [80, 35, 55, 25, 65, 40],
    "納期_分": [170, 100, 210, 85, 230, 150],
})

print("製品別の需要・標準時間")
display(products)
print("組立の要素作業")
display(assembly_tasks)
print("当日受注ジョブ")
display(orders)
製品別の需要・標準時間
日需要 切削_分 研削_分 検査_分
製品
A 72 3.20 2.50 1.40
B 48 4.50 3.00 1.80
C 36 2.80 4.20 1.60
組立の要素作業
作業 標準時間_秒
0 部品取出 28
1 圧入 46
2 締結 38
3 塗布 32
4 調整 55
5 通電検査 44
6 外観検査 29
7 梱包 36
当日受注ジョブ
ジョブ 処理時間_分 納期_分
0 J1 80 170
1 J2 35 100
2 J3 55 210
3 J4 25 85
4 J5 65 230
5 J6 40 150

No.041:ラインバランシング — タクト内に要素作業を配分する

実務での意味

ラインバランシングは、前後関係を守りながら要素作業を工程へ配分し、作業待ちと過負荷を抑える設計です。繁忙時の増員判断や、新製品立上げ時の標準作業設計に直結します。

分析・モデル化の考え方

稼働可能時間を TT、必要数量を DD とするとサイクルタイムは C=T/DC=T/D です。全作業時間を iti\sum_i t_i、工程数を mm としたとき、編成効率は

E=itimCE=\frac{\sum_i t_i}{mC}

です。ここでは工程順を維持する逐次割付を使います。厳密最適化ではありませんが、現場検討の基準案を素早く作れます。

Pythonで確認する

available_sec = 7.5 * 60 * 60
daily_demand = 320
cycle_sec = available_sec / daily_demand

stations, current, load = [], [], 0
for row in assembly_tasks.itertuples(index=False):
    if current and load + row.標準時間_秒 > cycle_sec:
        stations.append((current, load))
        current, load = [], 0
    current.append(row.作業)
    load += row.標準時間_秒
stations.append((current, load))

balance = pd.DataFrame({
    "工程": [f"ST{i+1}" for i in range(len(stations))],
    "割付作業": [" → ".join(x[0]) for x in stations],
    "負荷_秒": [x[1] for x in stations],
})
balance["余裕_秒"] = cycle_sec - balance["負荷_秒"]
efficiency = assembly_tasks["標準時間_秒"].sum() / (len(balance) * cycle_sec)
display(balance)
print(f"サイクルタイム: {cycle_sec:.1f}秒/個")
print(f"編成効率: {efficiency:.1%}")

ax = balance.plot.bar(x="工程", y="負荷_秒", color="#4472C4", legend=False, figsize=(7, 3.5))
ax.axhline(cycle_sec, color="#C00000", linestyle="--", label="サイクルタイム")
ax.set_title("工程別負荷とサイクルタイム")
ax.set_xlabel("工程")
ax.set_ylabel("負荷(秒)")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
工程 割付作業 負荷_秒 余裕_秒
0 ST1 部品取出 → 圧入 74 10.38
1 ST2 締結 → 塗布 70 14.38
2 ST3 調整 55 29.38
3 ST4 通電検査 → 外観検査 73 11.38
4 ST5 梱包 36 48.38
サイクルタイム: 84.4秒/個
編成効率: 73.0%


png

結果の読み取り

各工程の棒が赤いサイクルタイム線を下回るため、標準時間上は日需要を処理できます。ただし工程間の余裕には差があり、負荷最大工程が停止するとライン全体へ波及します。編成効率は設備投資の結論ではなく、歩行、供給、品質確認を含む実測で再評価するための基準値です。

No.042:ジョブショップ — 工程経路が異なる仕事を差し立てる

実務での意味

ジョブショップでは、品種ごとに工程経路が異なります。試作、金型、補修部品のような多品種少量生産では、設備競合を解消しながら各ジョブを流す差立てが納期を左右します。

分析・モデル化の考え方

各作業の開始時刻は「そのジョブの前作業完了」と「対象設備が空く時刻」の最大値です。

sj,k=max(cj,k1, am(j,k))s_{j,k}=\max(c_{j,k-1},\ a_{m(j,k)})

ここでは固定されたジョブ優先順で、最早開始時刻へ割り付けるディスパッチングを行います。

Pythonで確認する

routes = {
    "J1": [("切削", 55), ("研削", 35), ("検査", 20)],
    "J2": [("研削", 45), ("切削", 30), ("検査", 18)],
    "J3": [("切削", 40), ("検査", 22), ("研削", 30)],
    "J4": [("検査", 15), ("切削", 48), ("研削", 25)],
}
machine_ready = {m: 0 for m in ["切削", "研削", "検査"]}
job_ready = {j: 0 for j in routes}
schedule = []
for op_no in range(3):
    for job in routes:
        machine, duration = routes[job][op_no]
        start = max(job_ready[job], machine_ready[machine])
        end = start + duration
        schedule.append([job, op_no + 1, machine, start, end, duration])
        job_ready[job] = end
        machine_ready[machine] = end

jobshop = pd.DataFrame(schedule, columns=["ジョブ", "工程順", "設備", "開始", "終了", "時間"])
display(jobshop.sort_values(["開始", "設備"]))
print(f"全ジョブ完了時刻(makespan): {jobshop['終了'].max()}分")

fig, ax = plt.subplots(figsize=(9, 4))
machines = ["切削", "研削", "検査"]
colors = dict(zip(routes, ["#4472C4", "#ED7D31", "#70AD47", "#A5A5A5"]))
for row in jobshop.itertuples():
    y = machines.index(row.設備)
    ax.barh(y, row.時間, left=row.開始, color=colors[row.ジョブ], edgecolor="white")
    ax.text((row.開始 + row.終了) / 2, y, row.ジョブ, ha="center", va="center", color="white")
ax.set_yticks(range(len(machines)), machines)
ax.set_title("ジョブショップの設備別ガントチャート")
ax.set_xlabel("時刻(分)")
ax.set_ylabel("設備")
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.show()
ジョブ 工程順 設備 開始 終了 時間
0 J1 1 切削 0 55 55
3 J4 1 検査 0 15 15
1 J2 1 研削 0 45 45
2 J3 1 切削 55 95 40
4 J1 2 研削 55 90 35
5 J2 2 切削 95 125 30
6 J3 2 検査 95 117 22
8 J1 3 検査 117 137 20
10 J3 3 研削 117 147 30
7 J4 2 切削 125 173 48
9 J2 3 検査 137 155 18
11 J4 3 研削 173 198 25
全ジョブ完了時刻(makespan): 198分


png

結果の読み取り

ガントチャートの空白には、前工程待ちと設備待ちが混在します。空白をすべて「設備の無駄」とみなすのではなく、どちらの制約で生じたかを区別します。優先順を変えれば makespan やジョブ別納期は変わるため、緊急品を割り込ませる際は全体影響を再計算すべきです。

No.043:フローショップ — 共通工程を通る製品の投入順を選ぶ

実務での意味

全製品が同じ順に設備を通るフローショップでは、投入順だけでも滞留と終業時刻が変わります。塗装、熱処理、包装など、共通経路をもつ工程群の順序決定に使えます。

分析・モデル化の考え方

順列 π\pi に対し、ジョブ ii、工程 kk の完了時刻は

Ci,k=max(Ci1,k,Ci,k1)+pπi,kC_{i,k}=\max(C_{i-1,k}, C_{i,k-1})+p_{\pi_i,k}

です。5ジョブなら 5!=1205!=120 通りなので全列挙し、makespan 最小の順序を確認します。

Pythonで確認する

flow_times = pd.DataFrame(
    [[42, 35, 24], [30, 46, 20], [38, 28, 32], [25, 40, 27], [45, 22, 30]],
    index=["F1", "F2", "F3", "F4", "F5"], columns=["切削", "研削", "検査"]
)

def flowshop_completion(sequence, times=flow_times):
    completion = np.zeros((len(sequence), len(times.columns)), dtype=int)
    for i, job in enumerate(sequence):
        for k, machine in enumerate(times.columns):
            prev_job = completion[i - 1, k] if i > 0 else 0
            prev_machine = completion[i, k - 1] if k > 0 else 0
            completion[i, k] = max(prev_job, prev_machine) + times.loc[job, machine]
    return completion

results = []
for seq in itertools.permutations(flow_times.index):
    c = flowshop_completion(seq)
    results.append((" → ".join(seq), int(c[-1, -1])))
flow_result = pd.DataFrame(results, columns=["投入順", "makespan_分"]).sort_values("makespan_分")
display(flow_result.head(10))
best_seq = flow_result.iloc[0, 0].split(" → ")
baseline = flowshop_completion(list(flow_times.index))[-1, -1]
best = flow_result.iloc[0, 1]
print(f"元の順序: {baseline}分 / 最良順序: {best}分 / 短縮: {baseline-best}分")

ax = flow_result["makespan_分"].plot.hist(bins=12, color="#70AD47", edgecolor="white", figsize=(7, 3.5))
ax.axvline(best, color="#C00000", linestyle="--", label=f"最良 {best}分")
ax.set_title("投入順120通りのmakespan分布")
ax.set_xlabel("makespan(分)")
ax.set_ylabel("順列数")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
投入順 makespan_分
80 F4 → F2 → F3 → F1 → F5 232
86 F4 → F3 → F2 → F1 → F5 232
38 F2 → F4 → F3 → F1 → F5 233
74 F4 → F1 → F3 → F2 → F5 233
32 F2 → F3 → F4 → F1 → F5 233
78 F4 → F2 → F1 → F3 → F5 236
24 F2 → F1 → F3 → F4 → F5 236
72 F4 → F1 → F2 → F3 → F5 238
84 F4 → F3 → F1 → F2 → F5 238
57 F3 → F2 → F4 → F5 → F1 239
元の順序: 248分 / 最良順序: 232分 / 短縮: 16分


png

結果の読み取り

設備能力を増やさなくても、投入順の変更だけで終了時刻を短縮できることが分かります。一方、最短 makespan の順序が個別納期を必ず守るとは限りません。現場適用時は段取り時間、材料到着、納期優先度も目的関数・制約へ追加します。

No.044:スケジューリング — EDDとSPTを納期KPIで比較する

実務での意味

単一設備でも、どの仕事から処理するかで納期遵守率が変わります。代表ルールである EDD(納期が早い順)と SPT(処理時間が短い順)を比較すると、現場ルールの狙いを明文化できます。

分析・モデル化の考え方

ジョブ jj の遅れは Tj=max(0,Cjdj)T_j=\max(0,C_j-d_j) です。EDD は最大遅れを抑えやすく、SPT は平均滞留時間を抑えやすい性質があります。評価指標を総遅れ、遅延件数、平均完了時間に分けます。

Pythonで確認する

def evaluate_rule(df, sort_col):
    x = df.sort_values(sort_col).copy()
    x["完了_分"] = x["処理時間_分"].cumsum()
    x["遅れ_分"] = (x["完了_分"] - x["納期_分"]).clip(lower=0)
    return x

edd = evaluate_rule(orders, "納期_分")
spt = evaluate_rule(orders, "処理時間_分")
comparison = pd.DataFrame({
    "ルール": ["EDD(納期順)", "SPT(短時間順)"],
    "総遅れ_分": [edd["遅れ_分"].sum(), spt["遅れ_分"].sum()],
    "遅延件数": [(edd["遅れ_分"] > 0).sum(), (spt["遅れ_分"] > 0).sum()],
    "平均完了_分": [edd["完了_分"].mean(), spt["完了_分"].mean()],
})
display(comparison)
print("EDDの順序:", " → ".join(edd["ジョブ"]))
print("SPTの順序:", " → ".join(spt["ジョブ"]))

ax = comparison.set_index("ルール")[["総遅れ_分", "平均完了_分"]].plot.bar(
    color=["#C00000", "#4472C4"], figsize=(7, 3.8), rot=0
)
ax.set_title("ディスパッチングルール別KPI")
ax.set_xlabel("ルール")
ax.set_ylabel("時間(分)")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
ルール 総遅れ_分 遅延件数 平均完了_分
0 EDD(納期順) 105 3 150.00
1 SPT(短時間順) 130 1 143.33
EDDの順序: J4 → J2 → J6 → J1 → J3 → J5
SPTの順序: J4 → J2 → J6 → J3 → J5 → J1


png

結果の読み取り

どちらが優れるかはKPIで変わります。納期遵守を重視するなら遅れ指標、仕掛削減を重視するなら平均完了時間を優先します。「昔から納期順」のような暗黙ルールを数値比較し、受注区分ごとにルールを使い分ける材料になります。

No.045:制約条件の定式化 — 生産計画の実行可能性を確認する

実務での意味

販売計画をそのまま製造指示へ落とすと、特定設備だけ能力超過することがあります。制約式にすると、計画未達の原因と必要な残業・外注時間を工程別に説明できます。

分析・モデル化の考え方

製品 pp の数量を xpx_p、工程 mm の標準時間を ampa_{mp}、利用可能時間を bmb_m とすると、能力制約は

pampxpbm(m)\sum_p a_{mp}x_p \le b_m \quad (\forall m)

です。加えて xp0x_p\ge0、必要なら整数条件、需要上限、材料制約を設定します。ここでは需要どおり作る案の制約余力を診断します。

Pythonで確認する

machines = ["切削", "研削", "検査"]
available = pd.Series({"切削": 450, "研削": 450, "検査": 420}, name="利用可能_分")
time_cols = [f"{m}_分" for m in machines]
load = pd.Series(
    products[time_cols].to_numpy().T @ products["日需要"].to_numpy(),
    index=machines, name="必要負荷_分"
)
constraints = pd.concat([load, available], axis=1)
constraints["余力_分"] = constraints["利用可能_分"] - constraints["必要負荷_分"]
constraints["負荷率"] = constraints["必要負荷_分"] / constraints["利用可能_分"]
constraints["実行可能"] = constraints["余力_分"] >= 0
display(constraints)
print("計画全体の実行可能性:", "実行可能" if constraints["実行可能"].all() else "能力制約に違反")

ax = constraints[["必要負荷_分", "利用可能_分"]].plot.bar(
    color=["#ED7D31", "#A5A5A5"], figsize=(7, 3.5), rot=0
)
ax.set_title("工程別の必要負荷と利用可能時間")
ax.set_xlabel("工程")
ax.set_ylabel("時間(分/日)")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
必要負荷_分 利用可能_分 余力_分 負荷率 実行可能
切削 547.20 450 -97.20 1.22 False
研削 475.20 450 -25.20 1.06 False
検査 244.80 420 175.20 0.58 True
計画全体の実行可能性: 能力制約に違反


png

結果の読み取り

制約違反の工程が、日次計画をそのまま実行できない理由です。負荷率が100%未満でも、変動吸収余力が小さければ実務上は危険です。まず標準時間と停止を分け、残業・外注・数量調整のどれで余力を確保するかを費用と納期で比較します。

No.046:シミュレーション最適化 — 変動下で要員案を選ぶ

実務での意味

平均標準時間だけで要員を決めると、日々のばらつきで計画未達が起こります。候補案ごとに多くの「仮想の一日」を発生させ、達成確率と費用を比較します。

分析・モデル化の考え方

各案 xx の期待損失を

J(x)=Clabor(x)+CshortE[max(0,DQ(x,ω))]J(x)=C_{\mathrm{labor}}(x)+C_{\mathrm{short}}\,\mathbb{E}[\max(0,D-Q(x,\omega))]

とします。ω\omega は処理時間や停止の乱数、QQ は日産量です。ここでは2〜5人案を共通乱数で比較し、費用最小案を選びます。

Pythonで確認する

sim_rng = np.random.default_rng(SEED)
n_sim = 5000
demand = 320
base_cycle = sim_rng.lognormal(mean=np.log(70), sigma=0.10, size=n_sim)
downtime = np.clip(sim_rng.normal(35, 18, size=n_sim), 0, 120)

sim_rows = []
for workers in range(2, 6):
    # 並行作業の効果は逓減すると仮定
    effective_lines = 1 + 0.72 * (workers - 2)
    available_seconds = (450 - downtime) * 60
    output = np.floor(available_seconds / base_cycle * effective_lines)
    shortage = np.maximum(0, demand - output)
    daily_cost = workers * 18000 + shortage.mean() * 2500
    sim_rows.append([workers, output.mean(), np.quantile(output, 0.05), (output >= demand).mean(), daily_cost])

sim_options = pd.DataFrame(sim_rows, columns=["要員数", "平均生産数", "5%分位生産数", "需要達成確率", "期待総費用_円"])
display(sim_options)
best_worker = int(sim_options.loc[sim_options["期待総費用_円"].idxmin(), "要員数"])
print(f"期待総費用が最小の案: {best_worker}人")

fig, ax1 = plt.subplots(figsize=(7.5, 3.8))
ax1.plot(sim_options["要員数"], sim_options["需要達成確率"], marker="o", color="#4472C4")
ax1.set_xlabel("要員数")
ax1.set_ylabel("需要達成確率", color="#4472C4")
ax1.set_ylim(0, 1.05)
ax2 = ax1.twinx()
ax2.plot(sim_options["要員数"], sim_options["期待総費用_円"], marker="s", color="#C00000")
ax2.set_ylabel("期待総費用(円/日)", color="#C00000")
ax1.set_title("要員案の需要達成確率と期待総費用")
ax1.grid(alpha=0.3)
plt.tight_layout()
plt.show()
要員数 平均生産数 5%分位生産数 需要達成確率 期待総費用_円
0 2 357.49 298.00 0.84 43,167.00
1 3 615.25 513.00 1.00 54,000.00
2 4 873.01 728.00 1.00 72,000.00
3 5 1,130.76 943.00 1.00 90,000.00
期待総費用が最小の案: 2人


png

結果の読み取り

人を増やすほど達成確率は上がりますが、人件費も増えます。期待費用最小案は仮定した欠品単価に依存するため、単価を一つに固定せず感度分析するのが実務的です。また、平均生産数だけでなく5%分位点を併記すると、悪い日の供給力を把握できます。

No.047:ボトルネック分析 — 全体スループットを制約する工程を特定する

実務での意味

すべての工程を均等に改善しても、全体の出荷量は同じ比率では増えません。制約理論(TOC)の考え方では、最も逼迫した工程を特定し、そこを止めない運用を優先します。

分析・モデル化の考え方

工程負荷率 um=Lm/Bmu_m=L_m/B_m を比較し、最大の工程を暫定ボトルネックとします。ただし、実績ではブロッキング、材料待ち、故障によって移動するため、負荷率と待ち行列を併用します。

Pythonで確認する

bottleneck = constraints.copy()
bottleneck["余力率"] = 1 - bottleneck["負荷率"]
bn_name = bottleneck["負荷率"].idxmax()
display(bottleneck.sort_values("負荷率", ascending=False))
print(f"計画上のボトルネック候補: {bn_name}")

colors = ["#C00000" if x == bn_name else "#4472C4" for x in bottleneck.index]
ax = (bottleneck["負荷率"] * 100).plot.bar(color=colors, figsize=(7, 3.5), rot=0)
ax.axhline(100, color="black", linestyle="--", label="能力上限")
ax.set_title("工程別負荷率によるボトルネック候補")
ax.set_xlabel("工程")
ax.set_ylabel("負荷率(%)")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
必要負荷_分 利用可能_分 余力_分 負荷率 実行可能 余力率
切削 547.20 450 -97.20 1.22 False -0.22
研削 475.20 450 -25.20 1.06 False -0.06
検査 244.80 420 175.20 0.58 True 0.42
計画上のボトルネック候補: 切削


png

結果の読み取り

赤い工程が最初の改善候補です。短期には休憩交替、材料先行供給、段取り外段取り化で停止を減らし、中期には加工条件や治具、外注、増設を検討します。非ボトルネックを高稼働にすると仕掛が増えるため、「全設備の稼働率最大化」は避けます。

No.048:タクトタイム — 需要を満たす生産ペースと要員数を求める

実務での意味

タクトタイムは顧客需要が要求する生産ペースです。標準時間との比較により、必要な並列数・要員数や、時間帯別の応援判断を共通言語にできます。

分析・モデル化の考え方

正味稼働時間 TT、需要 DD に対し、

Takt=TD,nmin=総作業時間Takt\text{Takt}=\frac{T}{D},\qquad n_{\min}=\left\lceil\frac{\text{総作業時間}}{\text{Takt}}\right\rceil

です。タクトは実績サイクルタイムではありません。前者は需要起点、後者は工程実力です。

Pythonで確認する

demands = np.array([260, 300, 320, 360, 400])
net_available_sec = 7.5 * 3600
work_content_sec = assembly_tasks["標準時間_秒"].sum()
takt_table = pd.DataFrame({"日需要": demands})
takt_table["タクト_秒"] = net_available_sec / takt_table["日需要"]
takt_table["理論最小人数"] = np.ceil(work_content_sec / takt_table["タクト_秒"]).astype(int)
display(takt_table)

fig, ax1 = plt.subplots(figsize=(7, 3.8))
ax1.plot(takt_table["日需要"], takt_table["タクト_秒"], marker="o", color="#4472C4")
ax1.set_xlabel("日需要(個)")
ax1.set_ylabel("タクトタイム(秒/個)")
ax2 = ax1.twinx()
ax2.step(takt_table["日需要"], takt_table["理論最小人数"], where="mid", color="#ED7D31")
ax2.set_ylabel("理論最小人数")
ax1.set_title("需要変化に対するタクトタイムと必要人数")
ax1.grid(alpha=0.3)
plt.tight_layout()
plt.show()
日需要 タクト_秒 理論最小人数
0 260 103.85 3
1 300 90.00 4
2 320 84.38 4
3 360 75.00 5
4 400 67.50 5

png

結果の読み取り

需要増加でタクトは短くなり、ある境界で必要人数が段階的に増えます。理論最小人数は、編成ロス、疲労余裕、応援スキルを含まない下限です。S&OPでは需要シナリオごとの境界を示し、採用・多能工化・残業を決める先行指標にします。

No.049:生産能力分析 — OEEの変動を含めて需要達成確率を測る

実務での意味

公称能力が需要を上回っていても、停止、速度低下、不良で実効能力は減ります。能力を確率分布として扱うと、「平均的には足りる」から「何%の確率で足りる」へ判断を進められます。

分析・モデル化の考え方

OEEを可動率 AA、性能 PP、良品率 QQ の積 A×P×QA\times P\times Q とし、日産能力を

K=Kideal×A×P×QK=K_{\mathrm{ideal}}\times A\times P\times Q

で計算します。各要素を0〜1の乱数としてモンテカルロ評価します。相関が強い実データでは、独立乱数ではなく同日の組を再標本化します。

Pythonで確認する

cap_rng = np.random.default_rng(SEED)
n = 10000
availability = cap_rng.beta(45, 5, n)
performance = cap_rng.beta(38, 4, n)
quality = cap_rng.beta(98, 2, n)
ideal_capacity = 390
oee = availability * performance * quality
capacity = ideal_capacity * oee
demand_target = 320

capacity_kpi = pd.Series({
    "平均OEE": oee.mean(),
    "平均良品能力": capacity.mean(),
    "能力5%分位点": np.quantile(capacity, 0.05),
    "需要達成確率": (capacity >= demand_target).mean(),
})
display(capacity_kpi.to_frame("値"))

fig, ax = plt.subplots(figsize=(7, 3.5))
ax.hist(capacity, bins=35, color="#4472C4", edgecolor="white")
ax.axvline(demand_target, color="#C00000", linestyle="--", label=f"需要 {demand_target}個")
ax.axvline(np.quantile(capacity, 0.05), color="#ED7D31", linestyle=":", label="能力5%分位点")
ax.set_title("OEE変動を反映した日産良品能力の分布")
ax.set_xlabel("良品能力(個/日)")
ax.set_ylabel("シミュレーション回数")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
平均OEE 0.80
平均良品能力 311.13
能力5%分位点 272.48
需要達成確率 0.36

png

結果の読み取り

需要線より左側の面積が未達確率です。平均能力だけでなく5%分位点を見ると、厳しい日の供給力を在庫政策や納期回答へ反映できます。OEEの積だけでは損失理由が隠れるため、可動率・性能・品質を個別に改善管理します。

No.050:デジタルツイン — 現場実績で状態を更新し将来を比較する

実務での意味

デジタルツインは、きれいな3D表示そのものではありません。設備・仕掛・標準時間・計画と現場イベントを結び、現在状態を再現し、施策の結果を現実に影響を与える前に比較する仕組みです。

分析・モデル化の考え方

最小構成は、(1) 対象とKPI、(2) 状態データ、(3) 更新規則、(4) 予測・シミュレーション、(5) 意思決定、(6) 実績フィードバックです。今回は直近完了イベントから工程別サイクル時間を更新し、現状案と停止短縮案の完了見込みを比較します。

観測 yty_t で推定値 μ^t\hat\mu_t を更新する指数平滑は

μ^t=αyt+(1α)μ^t1\hat\mu_t=\alpha y_t+(1-\alpha)\hat\mu_{t-1}

です。古い標準を急に捨てず、最近の変化を反映できます。

Pythonで確認する

twin_rng = np.random.default_rng(SEED)
event_rows = []
standards = {"切削": 3.5, "研削": 3.2, "検査": 1.6}
for machine, standard in standards.items():
    observed = twin_rng.lognormal(np.log(standard), 0.12, 30)
    for i, value in enumerate(observed, 1):
        event_rows.append([f"E-{machine}-{i:02d}", machine, i, value])
events = pd.DataFrame(event_rows, columns=["event_id", "工程", "完了順", "実績CT_分"])

alpha = 0.25
state_rows = []
for machine, group in events.groupby("工程", sort=False):
    estimate = standards[machine]
    for value in group["実績CT_分"]:
        estimate = alpha * value + (1 - alpha) * estimate
    state_rows.append([machine, standards[machine], estimate])
twin_state = pd.DataFrame(state_rows, columns=["工程", "標準CT_分", "更新CT_分"]).set_index("工程")
twin_state["乖離率"] = twin_state["更新CT_分"] / twin_state["標準CT_分"] - 1
display(twin_state)

remaining = 100
scenario = pd.DataFrame({
    "シナリオ": ["現状継続", "停止時間20%短縮", "研削CTを8%改善"],
    "停止見込_分": [45, 36, 45],
    "研削CT係数": [1.0, 1.0, 0.92],
})
base_ct = twin_state["更新CT_分"].max()
scenario["完了見込_分"] = scenario["停止見込_分"] + remaining * np.maximum(
    [base_ct, base_ct, twin_state.loc["研削", "更新CT_分"] * 0.92]
, twin_state.drop(index="研削")["更新CT_分"].max())
display(scenario)

ax = scenario.plot.bar(x="シナリオ", y="完了見込_分", color="#4472C4", legend=False, figsize=(8, 3.8), rot=0)
ax.set_title("デジタルツインによる残り100個のシナリオ比較")
ax.set_xlabel("シナリオ")
ax.set_ylabel("完了見込時間(分)")
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
標準CT_分 更新CT_分 乖離率
工程
切削 3.50 3.61 0.03
研削 3.20 3.28 0.03
検査 1.60 1.62 0.01
シナリオ 停止見込_分 研削CT係数 完了見込_分
0 現状継続 45 1.00 406.42
1 停止時間20%短縮 36 1.00 397.42
2 研削CTを8%改善 45 0.92 406.42

png

結果の読み取り

標準と更新値の乖離は、計画マスタを見直すシグナルです。シナリオ表は「停止短縮」と「サイクル改善」の効果を同じ完了見込時間で比較します。本番では、イベント欠損、時刻ずれ、設備状態コードを監視し、モデル値と実績の誤差を継続記録します。モデル更新を自動化しても、製造指示の最終権限と異常時の手動復帰手順は明確にします。

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

  1. 需要から逆算する:タクトと必要能力を先に定義し、個別設備の高稼働を目的化しません。
  2. 順序も能力である:ジョブショップとフローショップでは、設備追加なしでも差立て改善の余地があります。
  3. 制約を明文化する:時間、材料、要員、前後関係を式にすると、計画不成立の理由を関係部門で共有できます。
  4. 平均値から確率へ進む:需要達成確率と下方分位点を使うと、変動に対する安全余力を設計できます。
  5. ボトルネックへ集中する:非制約工程の部分最適ではなく、出荷を制約する工程の保護・改善を優先します。
  6. 実績をモデルへ戻す:デジタルツインは一度作って終わりではなく、予測誤差を使って標準とロジックを更新する運用です。

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

1. 意思決定とKPIを先に決める

「何を可視化するか」ではなく、「誰が、いつ、どの選択肢を決めるか」を定義します。日次差立てなら納期遵守・仕掛・残業、設備投資なら能力下方分位・需要シナリオ・投資回収を主KPIにします。

2. マスタと実績の意味を揃える

品目、工程順、設備能力、カレンダー、段取り、良品・手直しの定義を統一します。標準時間の版管理と、停止理由コードの粒度が分析精度を左右します。

3. 小さな閉ループから始める

1製品群・1ラインで「実績収集→状態更新→翌日案作成→現場確認→結果評価」を回します。表計算やNotebookで価値を検証した後、MES・ERP・設備データとの連携を段階的に増やします。

4. 制約と責任分界を運用に埋め込む

凍結期間、緊急品、保全予定、品質隔離をモデルへ反映し、自動提案を採用・却下した理由を残します。モデル停止時の代替手順、承認権限、監査ログも設計対象です。

まとめ

No.041〜No.050では、作業配分、工程競合、投入順、納期ルール、有限能力、変動評価、制約工程、需要ペース、確率的能力、実績フィードバックを扱いました。

生産計画の品質は、最適化アルゴリズムだけで決まりません。正しい工程マスタ、現実的な制約、複数KPI、変動の扱い、現場が更新できる運用が揃って初めて、計画が意思決定に使えるようになります。最初は小さな対象で基準値を作り、予測と実績の差を測りながら精度と適用範囲を広げることが堅実です。

法人向けのご相談

数理工房では、製造業における生産計画・スケジューリング、シミュレーション、数理最適化、デジタルツインの構想策定から実装・内製化支援までご相談を承ります。

  • 工程・設備・要員制約を反映した生産計画モデルの設計
  • ボトルネック分析、能力評価、在庫・納期KPIの整備
  • 現場データを用いたシミュレーションと投資効果検証
  • ERP・MES・設備データをつなぐ意思決定基盤の構築
  • Python・統計・最適化を扱う法人研修

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