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% |
No.051:到着率をモデル化する
実務での意味
到着率は単位時間当たりに検査へ流入するロット数です。日平均だけでなく時間帯別に見ると、混雑を作る山が分かります。
分析・モデル化の考え方
は到着数、 は観測時間です。ポアソン到着では単位時間内の件数をポアソン分布で表しますが、実務では時間帯・曜日・品種で率が変わる非定常性を確認します。
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 |
全時間平均到着率: 7.71件/時
結果の読み取り
平均到着率よりピーク時間帯の到着率が高く、混雑が時間帯に偏っています。増員だけでなく、前工程の完了時刻や搬送便をずらす平準化が候補です。
短い観測期間では率が不安定なため、曜日・月末・品種切替を層別化します。
No.052:処理率をモデル化する
実務での意味
処理率は検査員1名が単位時間に完了できる件数です。標準時間だけでなく実績分布から、能力とばらつきを把握します。
分析・モデル化の考え方
平均処理時間を 時間とすると、 です。平均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 |
平均検査時間: 8.06分 / 1名当たり処理率 μ: 7.44件/時
結果の読み取り
処理率は平均能力を示しますが、分布の右裾にある長時間案件が後続を待たせます。品種・検査項目・再検査で層別化し、標準作業や専用窓口を検討します。
No.053:待ち時間をモデル化する
実務での意味
平均待ちだけでは出荷遅延リスクを表せません。中央値、90・95パーセンタイル、待ち発生率を併記します。
分析・モデル化の考え方
ロット の待ち時間は 、工程滞在時間は です。サービス水準は「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 | % |
結果の読み取り
平均より95%点が大きければ、一部ロットの長い待ちが隠れています。出荷締切がある工程では上位分位点を管理し、長時間案件の発生条件を調べます。
No.054:稼働率をモデル化する
実務での意味
稼働率は到着負荷を処理能力で割った値です。高すぎると遅れを吸収する余裕がなくなります。
分析・モデル化の考え方
窓口数 のとき、 です。 なら長期的に行列が発散します。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% |
全時間平均稼働率: 51.8%
結果の読み取り
全日平均に余裕があってもピーク帯は高負荷です。要員を1日固定で増やす前に、ピークのみ応援、休憩時刻調整、到着平準化を比較します。
No.055:窓口数と処理能力の関係を整理する
実務での意味
検査員数を増やすと総能力は増えますが、待ち時間の改善は人数に比例しません。Erlang C式で複数窓口の待ちを比較します。
分析・モデル化の考え方
M/M/cモデルではポアソン到着、指数処理時間、共通行列を仮定します。待ち確率 と平均待ち を計算します。
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 |
結果の読み取り
1名では到着率が能力を上回り不安定です。2名から3名の改善は大きく、その後の限界効果は小さくなります。理論仮定と実績分布の差はシミュレーションで確認します。
No.056:単一窓口の待ち行列をモデル化する
実務での意味
単一の測定器・承認者・特殊検査員はM/M/1の基本例です。能力ぎりぎりの専用工程がなぜ滞留するかを説明できます。
分析・モデル化の考え方
M/M/1では 、平均待ち時間は 、工程内時間は です。
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 |
結果の読み取り
稼働率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 |
結果の読み取り
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% |
結果の読み取り
総件数と受付人数が同じでも、予約平準化で待ちが減ります。増員、予約、便指定、書類事前登録の費用を比較できます。
No.059:製造ラインの滞留をモデル化する
実務での意味
検査待ちロットは工程内仕掛です。Littleの法則により、到着率・工程内時間・平均滞留数をつなげられます。
分析・モデル化の考え方
安定系では 、待ち行列だけなら です。単位をそろえ、件/時×時間で件数を得ます。
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件 |
結果の読み取り
Littleの法則とイベント時間積分が一致します。到着率を変えずに工程内仕掛を半減したいなら、工程内時間を半減する必要があります。仕掛上限だけでなく検査待ちの原因を改善します。
No.060:待ち時間と人員配置のトレードオフを整理する
実務での意味
増員は待ち損失を減らしますが人件費を増やします。納期制約を満たす候補の中で総費用を比較します。
分析・モデル化の考え方
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 |
総費用最小: 2名 / 平均待ち 2.95分 / ¥94,267/日
結果の読み取り
人数増加で待ち損失は急減し、人件費は直線的に増えます。総費用最小は一つの候補ですが、出荷締切や95%待ち時間の上限を満たすことを先に確認します。
ピーク時間だけの応援、到着平準化、処理時間短縮も同じ費用尺度で比較します。
対象ノックを通して見える実務上の示唆
- 到着率と処理率は同じ時間単位で定義する
- 日平均だけでなくピーク時間帯を評価する
- 平均待ちと上位分位点を併記する
- 稼働率が1へ近づくと待ち時間は非線形に増える
- 窓口追加の限界効果は逓減する
- 専用設備・承認者は単一窓口としてボトルネックになりやすい
- 予約・平準化は増員の代替策になる
- Littleの法則で仕掛数とリードタイムを接続する
- 人員費、待ち損失、納期制約で配置を評価する
待ち時間は現場の努力不足ではなく、到着・能力・ばらつきの構造から生じます。構造を変える施策を比較することが重要です。
実務導入する場合に必要なこと
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 まずはお気軽にご相談ください。