100本ノック / 数理モデリング / 数理モデリング100本ノック

検査待ちの滞留を人員配置の判断へ変える:工場の最終検査工程で学ぶ待ち行列・混雑のモデル化

検査待ちの滞留を人員配置の判断へ変える

工場の最終検査工程で学ぶ待ち行列・混雑のモデル化 No.051〜No.060

本記事では、架空の精密部品工場における最終検査工程を題材に、到着率、処理率、待ち時間、稼働率、窓口数、工程内滞留を数理モデルとして整理します。技術サポート窓口や部品受付のシナリオも比較し、最後に待ち時間損失と人員費から配置人数を評価します。

重要なのは平均処理能力だけではありません。需要の山、処理時間のばらつき、稼働率上昇による待ち時間の非線形な増加を考慮して、納期と人員費のバランスを判断します。

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

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

架空工場では、加工を終えたロットが最終検査へ到着します。検査員は2名ですが、午前後半から午後前半にロットが集中し、出荷締切前に検査待ちが増えます。管理者は「検査員を増やすべきか、到着を平準化すべきか」を判断したいと考えています。

20稼働日・1日8時間の到着時刻と検査時間を生成し、現行2名体制を再現します。平均だけでなく、90・95パーセンタイル、ピーク時間帯、仕掛滞留、費用を確認します。

現場でよくある状況

  • 1日合計件数だけを見て、時間帯別の到着集中を見落とす
  • 平均検査時間だけで能力を計算し、長時間案件の影響を無視する
  • 稼働率90%を効率的と評価する一方、待ち時間が急増している
  • 人員を1名増やす効果を、待ち時間や納期遵守率へ換算できない
  • 工程内仕掛数とリードタイムを別KPIとして管理している
  • 混雑対策が増員だけで、予約・平準化・標準作業を比較していない

待ち行列モデルは、到着・処理・窓口数の関係を明示し、改善案を同じ尺度で比較するための道具です。

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

平均到着率が平均処理能力を下回っていても、到着と処理時間にばらつきがあれば待ちます。稼働率が1へ近づくと、少しの遅れを吸収する余裕がなくなり、待ち時間は非線形に増加します。

また、平均待ち時間が短くても、一部のロットが長時間待てば出荷遅延になります。平均、分位点、待ち発生率、最大滞留を併記し、費用だけでなく納期制約も確認します。

今回扱うノックの全体像

No.テーマ実務での問い
051到着率何件がいつ到着するか
052処理率1人が1時間に何件処理できるか
053待ち時間平均と上位何%の待ちを測るか
054稼働率能力余裕はどの時間帯にないか
055窓口数検査員を増やすと待ちはどう変わるか
056単一窓口M/M/1の基本構造は何か
057技術サポート問い合わせ窓口の混雑をどう測るか
058受付予約平準化は増員の代替になるか
059製造ライン滞留仕掛数とリードタイムをどうつなぐか
060人員配置待ち損失と人員費をどう比較するか

Python 環境の準備

外部データは使いません。NumPyで乱数シードを固定し、pandasでイベントデータを集計、matplotlibで可視化します。

%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import math
import platform, sys
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import font_manager
import numpy as np
import pandas as pd
from IPython.display import display

SEED = 42
rng = np.random.default_rng(SEED)
fonts = {f.name for f in font_manager.fontManager.ttflist}
plot_font = next((f for f in ["Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP"] if f in fonts), "sans-serif")
plt.rcParams["font.family"] = plot_font
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["figure.figsize"] = (9, 4.8)
print(f"Python: {sys.version.split()[0]} / NumPy: {np.__version__} / pandas: {pd.__version__}")
print(f"matplotlib: {matplotlib.__version__} / font: {plot_font} / seed: {SEED}")
print(f"platform: {platform.platform()}")
Python: 3.13.1 / NumPy: 2.5.1 / pandas: 3.0.3
matplotlib: 3.11.0 / font: Hiragino Sans / seed: 42
platform: macOS-26.3-arm64-arm-64bit-Mach-O

架空データの作成

20日×8時間について、時間帯別到着率 [5, 7, 10, 12, 11, 9, 7, 5] 件/時からポアソン到着を生成します。検査時間は平均8分のガンマ分布とし、2名のうち早く空く検査員へ割り当てます。

各行は1ロットで、到着、検査開始、終了、待ち時間、工程滞在時間、担当検査員を持ちます。

def simulate_queue(arrival_minutes, service_minutes, servers):
    available = np.zeros(servers)
    rows = []
    for arrival, service in zip(arrival_minutes, service_minutes):
        server = int(np.argmin(available))
        start = max(arrival, available[server])
        end = start + service
        available[server] = end
        rows.append((arrival, service, start, end, start - arrival, end - arrival, server + 1))
    return pd.DataFrame(rows, columns=["arrival_min", "service_min", "start_min", "end_min", "wait_min", "flow_min", "server"])

hourly_rates = np.array([5, 7, 10, 12, 11, 9, 7, 5])
arrivals = []
for day in range(20):
    for hour, rate in enumerate(hourly_rates):
        count = rng.poisson(rate)
        for minute in np.sort(rng.uniform(0, 60, count)):
            arrivals.append(day * 480 + hour * 60 + minute)
arrivals = np.array(sorted(arrivals))
service_times = rng.gamma(shape=4, scale=2, size=len(arrivals))
events = simulate_queue(arrivals, service_times, servers=2)
events["day"] = (events["arrival_min"] // 480).astype(int) + 1
events["hour"] = ((events["arrival_min"] % 480) // 60).astype(int) + 9
events["waited"] = events["wait_min"] > 0

print(f"検査ロット数: {len(events):,}件 / 稼働日: {events['day'].nunique()}日 / 現行検査員: 2名")
display(events.head(10).style.format({c: "{:.1f}" for c in ["arrival_min", "service_min", "start_min", "end_min", "wait_min", "flow_min"]}))
検査ロット数: 1,233件 / 稼働日: 20日 / 現行検査員: 2名
  arrival_min service_min start_min end_min wait_min flow_min server day hour waited
0 13.6 8.0 13.6 21.6 0.0 8.0 1 1 9 False
1 22.2 10.7 22.2 33.0 0.0 10.7 2 1 9 False
2 26.6 5.7 26.6 32.3 0.0 5.7 1 1 9 False
3 27.0 9.3 32.3 41.6 5.3 14.5 1 1 9 True
4 33.3 10.0 33.3 43.3 0.0 10.0 2 1 9 False
5 38.6 10.9 41.6 52.5 2.9 13.9 1 1 9 True
6 49.4 10.8 49.4 60.1 0.0 10.8 2 1 9 False
7 55.6 4.7 55.6 60.3 0.0 4.7 1 1 9 False
8 62.6 15.0 62.6 77.6 0.0 15.0 2 1 10 False
9 69.3 2.5 69.3 71.8 0.0 2.5 1 1 10 False
daily = events.groupby("day", as_index=False).agg(
    到着件数=("arrival_min", "size"), 平均待ち分=("wait_min", "mean"),
    最大待ち分=("wait_min", "max"), 待ち発生率=("waited", "mean"))
display(daily.style.format({"平均待ち分": "{:.1f}", "最大待ち分": "{:.1f}", "待ち発生率": "{:.1%}"}))

fig, axes = plt.subplots(1, 2, figsize=(11, 4.2))
axes[0].bar(daily["day"], daily["到着件数"], color="#6baed6")
axes[0].set_title("日別の検査到着件数")
axes[0].set_xlabel("稼働日")
axes[0].set_ylabel("到着件数(件/日)")
axes[0].grid(True, axis="y", alpha=0.3)
axes[1].plot(daily["day"], daily["平均待ち分"], marker="o", label="平均")
axes[1].plot(daily["day"], daily["最大待ち分"], marker="o", label="最大")
axes[1].set_title("日別の検査待ち時間")
axes[1].set_xlabel("稼働日")
axes[1].set_ylabel("待ち時間(分)")
axes[1].grid(True, alpha=0.3)
axes[1].legend()
plt.tight_layout()
plt.show()
  day 到着件数 平均待ち分 最大待ち分 待ち発生率
0 1 55 1.1 9.3 25.5%
1 2 64 1.6 11.3 29.7%
2 3 72 3.1 19.5 43.1%
3 4 61 2.3 16.5 41.0%
4 5 67 1.0 12.0 29.9%
5 6 66 2.4 13.5 47.0%
6 7 43 0.3 4.6 11.6%
7 8 63 10.3 43.5 58.7%
8 9 65 1.7 17.6 32.3%
9 10 56 1.1 11.6 28.6%
10 11 57 1.5 8.2 36.8%
11 12 59 3.2 15.1 42.4%
12 13 75 7.2 33.2 64.0%
13 14 76 2.3 11.9 52.6%
14 15 55 1.8 15.8 30.9%
15 16 73 2.8 13.9 50.7%
16 17 53 2.0 13.8 39.6%
17 18 65 5.7 23.0 52.3%
18 19 51 0.6 8.9 19.6%
19 20 57 2.2 10.6 40.4%

svg


No.051:到着率をモデル化する

実務での意味

到着率は単位時間当たりに検査へ流入するロット数です。日平均だけでなく時間帯別に見ると、混雑を作る山が分かります。

分析・モデル化の考え方

λ=NT\lambda=\frac{N}{T}

NN は到着数、TT は観測時間です。ポアソン到着では単位時間内の件数をポアソン分布で表しますが、実務では時間帯・曜日・品種で率が変わる非定常性を確認します。

Pythonで確認する

arrival_profile = events.groupby("hour", as_index=False).size().rename(columns={"size": "20日到着数"})
arrival_profile["到着率_件時"] = arrival_profile["20日到着数"] / 20
display(arrival_profile.style.format({"到着率_件時": "{:.2f}"}))

fig, ax = plt.subplots()
ax.bar(arrival_profile["hour"], arrival_profile["到着率_件時"], color="#2c7fb8")
ax.axhline(len(events) / (20 * 8), color="black", linestyle="--", label="全時間平均")
ax.set_title("時間帯別の検査到着率")
ax.set_xlabel("時刻(時)")
ax.set_ylabel("到着率 λ(件/時)")
ax.grid(True, axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
print(f"全時間平均到着率: {len(events)/(20*8):.2f}件/時")
  hour 20日到着数 到着率_件時
0 9 105 5.25
1 10 150 7.50
2 11 178 8.90
3 12 218 10.90
4 13 201 10.05
5 14 164 8.20
6 15 120 6.00
7 16 97 4.85

svg

全時間平均到着率: 7.71件/時

結果の読み取り

平均到着率よりピーク時間帯の到着率が高く、混雑が時間帯に偏っています。増員だけでなく、前工程の完了時刻や搬送便をずらす平準化が候補です。

短い観測期間では率が不安定なため、曜日・月末・品種切替を層別化します。


No.052:処理率をモデル化する

実務での意味

処理率は検査員1名が単位時間に完了できる件数です。標準時間だけでなく実績分布から、能力とばらつきを把握します。

分析・モデル化の考え方

平均処理時間を E[S]E[S] 時間とすると、μ=1/E[S]\mu=1/E[S] です。平均8分なら7.5件/時ですが、長時間案件が待ちへ影響します。

Pythonで確認する

mean_service = events["service_min"].mean()
mu = 60 / mean_service
service_summary = events["service_min"].describe(percentiles=[0.5, 0.9, 0.95]).to_frame("検査時間_分")
display(service_summary.style.format("{:.2f}"))

fig, ax = plt.subplots()
ax.hist(events["service_min"], bins=25, color="#74c476", edgecolor="white")
ax.axvline(mean_service, color="black", linestyle="--", label="平均")
ax.set_title("1ロット当たり検査時間の分布")
ax.set_xlabel("検査時間(分/件)")
ax.set_ylabel("件数")
ax.grid(True, axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
print(f"平均検査時間: {mean_service:.2f}分 / 1名当たり処理率 μ: {mu:.2f}件/時")
  検査時間_分
count 1233.00
mean 8.06
std 3.98
min 0.75
50% 7.45
90% 13.44
95% 15.26
max 28.35

svg

平均検査時間: 8.06分 / 1名当たり処理率 μ: 7.44件/時

結果の読み取り

処理率は平均能力を示しますが、分布の右裾にある長時間案件が後続を待たせます。品種・検査項目・再検査で層別化し、標準作業や専用窓口を検討します。


No.053:待ち時間をモデル化する

実務での意味

平均待ちだけでは出荷遅延リスクを表せません。中央値、90・95パーセンタイル、待ち発生率を併記します。

分析・モデル化の考え方

ロット ii の待ち時間は Wq,i=BiAiW_{q,i}=B_i-A_i、工程滞在時間は Wi=Wq,i+SiW_i=W_{q,i}+S_i です。サービス水準は「95%が何分以内」など分位点で定義します。

Pythonで確認する

wait_kpi = pd.DataFrame({
    "KPI": ["平均", "中央値", "90%点", "95%点", "最大", "待ち発生率"],
    "値": [events["wait_min"].mean(), events["wait_min"].median(), events["wait_min"].quantile(.9),
           events["wait_min"].quantile(.95), events["wait_min"].max(), events["waited"].mean() * 100],
    "単位": ["分", "分", "分", "分", "分", "%"],
})
display(wait_kpi.style.format({"値": "{:.2f}"}))

fig, ax = plt.subplots()
ax.hist(events["wait_min"], bins=30, color="#9ecae1", edgecolor="white")
ax.axvline(events["wait_min"].quantile(.95), color="#de2d26", linestyle="--", label="95%点")
ax.set_title("検査待ち時間の分布")
ax.set_xlabel("待ち時間(分)")
ax.set_ylabel("ロット数")
ax.grid(True, axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
  KPI 単位
0 平均 2.85
1 中央値 0.00
2 90%点 9.21
3 95%点 13.88
4 最大 43.46
5 待ち発生率 40.15 %

svg

結果の読み取り

平均より95%点が大きければ、一部ロットの長い待ちが隠れています。出荷締切がある工程では上位分位点を管理し、長時間案件の発生条件を調べます。


No.054:稼働率をモデル化する

実務での意味

稼働率は到着負荷を処理能力で割った値です。高すぎると遅れを吸収する余裕がなくなります。

分析・モデル化の考え方

窓口数 cc のとき、ρ=λ/(cμ)\rho=\lambda/(c\mu) です。ρ1\rho\ge1 なら長期的に行列が発散します。1未満でも1へ近づくと待ち時間は急増します。

Pythonで確認する

utilization = arrival_profile.copy()
utilization["能力_件時"] = 2 * mu
utilization["稼働率"] = utilization["到着率_件時"] / utilization["能力_件時"]
display(utilization.style.format({"到着率_件時": "{:.2f}", "能力_件時": "{:.2f}", "稼働率": "{:.1%}"}))

fig, ax = plt.subplots()
ax.plot(utilization["hour"], utilization["稼働率"] * 100, marker="o", color="#d95f0e")
ax.axhline(85, color="black", linestyle="--", label="注意目安 85%")
ax.set_title("時間帯別の検査員稼働率")
ax.set_xlabel("時刻(時)")
ax.set_ylabel("稼働率 ρ(%)")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
print(f"全時間平均稼働率: {len(events)/(20*8)/(2*mu):.1%}")
  hour 20日到着数 到着率_件時 能力_件時 稼働率
0 9 105 5.25 14.88 35.3%
1 10 150 7.50 14.88 50.4%
2 11 178 8.90 14.88 59.8%
3 12 218 10.90 14.88 73.2%
4 13 201 10.05 14.88 67.5%
5 14 164 8.20 14.88 55.1%
6 15 120 6.00 14.88 40.3%
7 16 97 4.85 14.88 32.6%

svg

全時間平均稼働率: 51.8%

結果の読み取り

全日平均に余裕があってもピーク帯は高負荷です。要員を1日固定で増やす前に、ピークのみ応援、休憩時刻調整、到着平準化を比較します。


No.055:窓口数と処理能力の関係を整理する

実務での意味

検査員数を増やすと総能力は増えますが、待ち時間の改善は人数に比例しません。Erlang C式で複数窓口の待ちを比較します。

分析・モデル化の考え方

M/M/cモデルではポアソン到着、指数処理時間、共通行列を仮定します。待ち確率 PWP_W と平均待ち Wq=PW/(cμλ)W_q=P_W/(c\mu-\lambda) を計算します。

Pythonで確認する

def erlang_c(lam, mu_value, servers):
    rho = lam / (servers * mu_value)
    if rho >= 1: return {"rho": rho, "wait_probability": 1.0, "wq_hours": np.inf, "lq": np.inf}
    a = lam / mu_value
    base = sum(a**n / math.factorial(n) for n in range(servers))
    tail = a**servers / (math.factorial(servers) * (1 - rho))
    p0 = 1 / (base + tail)
    pw = tail * p0
    wq = pw / (servers * mu_value - lam)
    return {"rho": rho, "wait_probability": pw, "wq_hours": wq, "lq": lam * wq}

lambda_all = len(events) / (20 * 8)
server_table = pd.DataFrame([{"検査員数": c, **erlang_c(lambda_all, mu, c)} for c in range(1, 6)])
server_table["平均待ち_分"] = server_table["wq_hours"] * 60
display(server_table.style.format({"rho": "{:.1%}", "wait_probability": "{:.1%}", "平均待ち_分": "{:.2f}", "lq": "{:.2f}"}))

fig, ax = plt.subplots()
stable = server_table.replace([np.inf], np.nan)
ax.plot(stable["検査員数"], stable["平均待ち_分"], marker="o", color="#2c7fb8")
ax.set_title("検査員数と理論平均待ち時間")
ax.set_xlabel("検査員数(名)")
ax.set_ylabel("平均待ち時間(分)")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
  検査員数 rho wait_probability wq_hours lq 平均待ち_分
0 1 103.5% 100.0% inf inf inf
1 2 51.8% 35.3% 0.049204 0.38 2.95
2 3 34.5% 9.9% 0.006771 0.05 0.41
3 4 25.9% 2.3% 0.001038 0.01 0.06
4 5 20.7% 0.4% 0.000151 0.00 0.01

svg

結果の読み取り

1名では到着率が能力を上回り不安定です。2名から3名の改善は大きく、その後の限界効果は小さくなります。理論仮定と実績分布の差はシミュレーションで確認します。


No.056:単一窓口の待ち行列をモデル化する

実務での意味

単一の測定器・承認者・特殊検査員はM/M/1の基本例です。能力ぎりぎりの専用工程がなぜ滞留するかを説明できます。

分析・モデル化の考え方

M/M/1では ρ=λ/μ\rho=\lambda/\mu、平均待ち時間は Wq=ρ/(μλ)W_q=\rho/(\mu-\lambda)、工程内時間は W=1/(μλ)W=1/(\mu-\lambda) です。

Pythonで確認する

lambda_single, mu_single = 6.0, 7.5
rho_single = lambda_single / mu_single
wq_single = rho_single / (mu_single - lambda_single) * 60
w_single = 1 / (mu_single - lambda_single) * 60
single = pd.DataFrame({"到着率_件時": [lambda_single], "処理率_件時": [mu_single], "稼働率": [rho_single], "平均待ち_分": [wq_single], "工程内時間_分": [w_single]})
display(single.style.format({"稼働率": "{:.1%}", "平均待ち_分": "{:.1f}", "工程内時間_分": "{:.1f}"}))

rho_grid = np.linspace(.1, .95, 18)
wait_grid = rho_grid / (mu_single * (1 - rho_grid)) * 60
fig, ax = plt.subplots()
ax.plot(rho_grid * 100, wait_grid, marker="o", color="#de2d26")
ax.set_title("単一窓口:稼働率と平均待ち時間")
ax.set_xlabel("稼働率(%)")
ax.set_ylabel("平均待ち時間(分)")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
  到着率_件時 処理率_件時 稼働率 平均待ち_分 工程内時間_分
0 6.000000 7.500000 80.0% 32.0 40.0

svg

結果の読み取り

稼働率80%でも平均待ちは32分となり、90%以降は急増します。専用設備では予備能力、優先ルール、前処理の分離を検討します。


No.057:コールセンターの混雑をモデル化する

実務での意味

製造業の技術サポート窓口も複数担当者の待ち行列です。回答待ちは設備復旧の遅れにつながるため、電話本数だけでなく応答時間を管理します。

分析・モデル化の考え方

問い合わせ12件/時、1名5件/時のM/M/cとして、担当者数ごとの稼働率、待ち確率、平均応答待ちを比較します。

Pythonで確認する

support = pd.DataFrame([{"担当者数": c, **erlang_c(12, 5, c)} for c in range(2, 7)])
support["平均応答待ち_分"] = support["wq_hours"] * 60
display(support.style.format({"rho": "{:.1%}", "wait_probability": "{:.1%}", "平均応答待ち_分": "{:.2f}"}))

fig, ax = plt.subplots()
ax.bar(support["担当者数"], support["平均応答待ち_分"].replace(np.inf, np.nan), color="#756bb1")
ax.axhline(2, color="black", linestyle="--", label="目標2分")
ax.set_title("技術サポート担当者数と平均応答待ち")
ax.set_xlabel("担当者数(名)")
ax.set_ylabel("平均応答待ち(分)")
ax.grid(True, axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
  担当者数 rho wait_probability wq_hours lq 平均応答待ち_分
0 2 120.0% 100.0% inf inf inf
1 3 80.0% 64.7% 0.215730 2.588764 12.94
2 4 60.0% 28.7% 0.035880 0.430565 2.15
3 5 48.0% 11.4% 0.008731 0.104773 0.52
4 6 40.0% 4.0% 0.002220 0.026635 0.13

svg

結果の読み取り

2名は能力不足、3名は高稼働で待ちが大きく、目標2分にはさらに余裕が必要です。実務では放棄、折返し、問い合わせ難易度、スキル別ルーティングを追加します。


No.058:店舗・受付の待ち時間をモデル化する

実務での意味

部品受入受付では納入便が特定時刻に集中します。増員せず予約枠で到着を平準化する効果をシミュレーションします。

分析・モデル化の考え方

総到着数と受付2名は同じまま、時間帯率をピーク型 [3,5,12,14,10,7,4,3] から平準型へ変えます。平均能力が同じでも到着のばらつきで待ちが変わります。

Pythonで確認する

def create_scenario(rates, seed):
    local = np.random.default_rng(seed)
    arr = []
    for day in range(20):
        for hour, rate in enumerate(rates):
            arr.extend(day * 480 + hour * 60 + np.sort(local.uniform(0, 60, local.poisson(rate))))
    arr = np.array(sorted(arr))
    service = local.gamma(4, 2.5, len(arr))
    result = simulate_queue(arr, service, 2)
    return {"件数": len(result), "平均待ち": result["wait_min"].mean(), "95%待ち": result["wait_min"].quantile(.95), "待ち発生率": (result["wait_min"] > 0).mean()}

peak_rates = np.array([3, 5, 12, 14, 10, 7, 4, 3])
flat_rates = np.repeat(peak_rates.mean(), 8)
reception = pd.DataFrame([{"方針": "集中到着", **create_scenario(peak_rates, 100)}, {"方針": "予約平準化", **create_scenario(flat_rates, 101)}])
display(reception.style.format({"平均待ち": "{:.1f}分", "95%待ち": "{:.1f}分", "待ち発生率": "{:.1%}"}))

fig, ax = plt.subplots()
x = np.arange(2)
ax.bar(x - .18, reception["平均待ち"], .36, label="平均待ち")
ax.bar(x + .18, reception["95%待ち"], .36, label="95%待ち")
ax.set_xticks(x, reception["方針"])
ax.set_title("部品受付:到着平準化による待ち時間改善")
ax.set_xlabel("到着方針")
ax.set_ylabel("待ち時間(分)")
ax.grid(True, axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
  方針 件数 平均待ち 95%待ち 待ち発生率
0 集中到着 1146 15.8分 50.2分 68.2%
1 予約平準化 1187 4.4分 19.7分 46.9%

svg

結果の読み取り

総件数と受付人数が同じでも、予約平準化で待ちが減ります。増員、予約、便指定、書類事前登録の費用を比較できます。


No.059:製造ラインの滞留をモデル化する

実務での意味

検査待ちロットは工程内仕掛です。Littleの法則により、到着率・工程内時間・平均滞留数をつなげられます。

分析・モデル化の考え方

安定系では L=λWL=\lambda W、待ち行列だけなら Lq=λWqL_q=\lambda W_q です。単位をそろえ、件/時×時間で件数を得ます。

Pythonで確認する

lambda_hour = len(events) / (20 * 8)
mean_flow_hour = events["flow_min"].mean() / 60
mean_wait_hour = events["wait_min"].mean() / 60
little = pd.DataFrame({
    "指標": ["工程内ロット L", "待ちロット Lq"],
    "Littleの法則": [lambda_hour * mean_flow_hour, lambda_hour * mean_wait_hour],
    "イベント時間積分": [events["flow_min"].sum() / (20 * 8 * 60), events["wait_min"].sum() / (20 * 8 * 60)],
})
display(little.style.format({"Littleの法則": "{:.2f}件", "イベント時間積分": "{:.2f}件"}))

fig, ax = plt.subplots()
ax.bar(little["指標"], little["Littleの法則"], color=["#2c7fb8", "#fdae6b"])
ax.set_title("Littleの法則による平均工程内滞留")
ax.set_xlabel("滞留指標")
ax.set_ylabel("平均ロット数(件)")
ax.grid(True, axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
  指標 Littleの法則 イベント時間積分
0 工程内ロット L 1.40件 1.40件
1 待ちロット Lq 0.37件 0.37件

svg

結果の読み取り

Littleの法則とイベント時間積分が一致します。到着率を変えずに工程内仕掛を半減したいなら、工程内時間を半減する必要があります。仕掛上限だけでなく検査待ちの原因を改善します。


No.060:待ち時間と人員配置のトレードオフを整理する

実務での意味

増員は待ち損失を減らしますが人件費を増やします。納期制約を満たす候補の中で総費用を比較します。

分析・モデル化の考え方

C(c)=cClabor+λTWq(c)CwaitC(c)=cC_{\mathrm{labor}}+\lambda T W_q(c)C_{\mathrm{wait}}

1人1日35,000円、待ち1ロット・1時間当たり8,000円、8時間稼働として試算します。

Pythonで確認する

staff_rows = []
for c in range(2, 7):
    q = erlang_c(lambda_all, mu, c)
    labor = c * 35_000
    wait_loss = lambda_all * 8 * q["wq_hours"] * 8_000 if np.isfinite(q["wq_hours"]) else np.inf
    staff_rows.append({"検査員数": c, "稼働率": q["rho"], "平均待ち_分": q["wq_hours"] * 60, "人件費_円日": labor, "待ち損失_円日": wait_loss, "総費用_円日": labor + wait_loss})
staffing = pd.DataFrame(staff_rows)
best_staff = staffing.loc[staffing["総費用_円日"].idxmin()]
display(staffing.style.format({"稼働率": "{:.1%}", "平均待ち_分": "{:.2f}", "人件費_円日": {:,.0f}", "待ち損失_円日": {:,.0f}", "総費用_円日": {:,.0f}"}))

fig, ax = plt.subplots()
ax.plot(staffing["検査員数"], staffing["人件費_円日"], marker="o", label="人件費")
ax.plot(staffing["検査員数"], staffing["待ち損失_円日"], marker="o", label="待ち損失")
ax.plot(staffing["検査員数"], staffing["総費用_円日"], marker="o", linewidth=2, label="総費用")
ax.axvline(best_staff["検査員数"], color="black", linestyle="--", label="総費用最小")
ax.set_title("検査員数と人員費・待ち損失")
ax.set_xlabel("検査員数(名)")
ax.set_ylabel("1日費用(円)")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
print(f"総費用最小: {int(best_staff['検査員数'])}名 / 平均待ち {best_staff['平均待ち_分']:.2f}分 / ¥{best_staff['総費用_円日']:,.0f}/日")
  検査員数 稼働率 平均待ち_分 人件費_円日 待ち損失_円日 総費用_円日
0 2 51.8% 2.95 ¥70,000 ¥24,267 ¥94,267
1 3 34.5% 0.41 ¥105,000 ¥3,339 ¥108,339
2 4 25.9% 0.06 ¥140,000 ¥512 ¥140,512
3 5 20.7% 0.01 ¥175,000 ¥74 ¥175,074
4 6 17.3% 0.00 ¥210,000 ¥10 ¥210,010

svg

総費用最小: 2名 / 平均待ち 2.95分 / ¥94,267/日

結果の読み取り

人数増加で待ち損失は急減し、人件費は直線的に増えます。総費用最小は一つの候補ですが、出荷締切や95%待ち時間の上限を満たすことを先に確認します。

ピーク時間だけの応援、到着平準化、処理時間短縮も同じ費用尺度で比較します。


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

  1. 到着率と処理率は同じ時間単位で定義する
  2. 日平均だけでなくピーク時間帯を評価する
  3. 平均待ちと上位分位点を併記する
  4. 稼働率が1へ近づくと待ち時間は非線形に増える
  5. 窓口追加の限界効果は逓減する
  6. 専用設備・承認者は単一窓口としてボトルネックになりやすい
  7. 予約・平準化は増員の代替策になる
  8. Littleの法則で仕掛数とリードタイムを接続する
  9. 人員費、待ち損失、納期制約で配置を評価する

待ち時間は現場の努力不足ではなく、到着・能力・ばらつきの構造から生じます。構造を変える施策を比較することが重要です。

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

1. イベント時刻を記録する

到着、受付、処理開始、終了、再検査、引渡しをロットIDで結びます。

2. 到着と処理を層別化する

時間帯、曜日、品種、検査種別、担当スキル、再検査を区別します。

3. 待ちKPIと業務影響を接続する

平均、95%点、締切超過、仕掛数、残業、出荷遅延を定義します。

4. 理論式とシミュレーションを使い分ける

Erlang式で概算し、休憩、優先順位、時間変動、故障はイベントシミュレーションで確認します。

5. 増員以外の施策を比較する

予約、搬送便変更、応援、標準作業、専用窓口、前工程平準化を費用と効果で比べます。

6. 小規模な並行運用で検証する

ピーク時間だけ施策を試し、待ち時間・品質・負荷・費用を確認して拡大します。

まとめ

No.051〜060では、工場の最終検査工程を中心に待ち行列モデルを確認しました。

  • 到着率、処理率、窓口数から稼働率を表す
  • 平均・分位点で待ち時間を評価する
  • M/M/1、M/M/cで混雑の非線形性を理解する
  • 到着平準化と増員を比較する
  • Littleの法則で仕掛数と工程内時間をつなぐ
  • 人員費と待ち損失から配置案を評価する

待ち行列モデルの価値は、混雑を可視化するだけでなく、納期と人員配置の判断へ変換できることにあります。

法人向けのご相談

数理工房では、製造業における次のようなテーマを支援しています。

  • 検査・搬送・保全・受付工程の混雑分析
  • イベントログ設計と工程リードタイム可視化
  • 人員・設備数、予約、優先ルールのシミュレーション
  • 仕掛削減と納期短縮の施策評価
  • 現場KPI・ダッシュボード・運用設計
  • 生産技術・品質・DX部門向け実データ型研修

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