Mímisbrunnr知恵の泉

← オペレーションズマネジメント 一覧

🎓 レベル:基礎 | 重要度:A(必須)

📎 前提:プロセス分析とリトルの法則RR=フローレート) | 深掘り:第8章 制約理論・第4章 待ち行列

要点(BLUF)

1. キャパシティは「min」で決まる

直列プロセスでは、すべてのフローユニットが全ステージを順に通過します。だから工程全体が出せる速さは、どんなに速いステージがあってもいちばん遅いステージに合わせられてしまいます。これがボトルネックです。

flowchart LR
  A["切断<br/>cap=0.50"] --> B["加工<br/>cap=0.40"] --> C["組立<br/>cap=0.50"] --> D["検査<br/>cap=0.33 (最小=律速)"] --> E["梱包<br/>cap=0.67"]

各ステージの資源キャパシティは、並列サーバ数 mim_i(人や機械の台数)を、1ユニットあたりのアクティビティ時間 tit_i で割ったものです。サーバが mim_i 台あれば mim_i 倍の速さで処理できます。

2. 数式:キャパシティ・サイクルタイム・稼働率

capi=miti,capproc=minicapi\text{cap}_i = \frac{m_i}{t_i}, \qquad \text{cap}_\text{proc} = \min_i \text{cap}_i

工程キャパシティが min になるのは、フローレート RR がどのステージのキャパシティも超えられない(RcapiR \le \text{cap}_i がすべての ii で必要)ため、RminicapiR \le \min_i \text{cap}_i だからです。そしてボトルネックを上流が十分に供給できれば、この上限は実際に達成できます。最大速度で流したときの出力どうしの間隔がサイクルタイム、各資源の稼働率は実スループット RR に対する忙しさです。

サイクルタイム=1capproc,ρi=Rcapi\text{サイクルタイム} = \frac{1}{\text{cap}_\text{proc}}, \qquad \rho_i = \frac{R}{\text{cap}_i}

需要が与えられたとき、各資源が需要をさばけるかを表すのがインプライド稼働率です。1 を超える資源があれば、その需要水準ではそこが律速になります(実スループットは需要とキャパシティの小さい方)。

インプライド稼働率i=需要capi\text{インプライド稼働率}_i = \frac{\text{需要}}{\text{cap}_i}

需要の歩調そのものを時間で表したのがタクトタイム(需要を満たすために1ユニットを仕上げるべき間隔)です。サイクルタイム ≤ タクトタイムなら需要を満たせます(これは「ボトルネックのインプライド稼働率 ≤ 1」と同じことです)。

タクトタイム=利用可能時間需要\text{タクトタイム} = \frac{\text{利用可能時間}}{\text{需要}}

3. 工程のキャパシティ分析(コード)

5ステージの工程で、各キャパシティ・ボトルネック・工程キャパシティ・サイクルタイム・各稼働率を計算します。

import numpy as np
import pandas as pd

# 各アクティビティ:1個あたり処理時間 t(分/個・1サーバ)と並列サーバ数 m
stages = pd.DataFrame({
    "ステージ":   ["切断", "加工", "組立", "検査", "梱包"],
    "処理時間_t": [2.0, 5.0, 4.0, 3.0, 1.5],   # 分/個(1サーバあたり)
    "サーバ数_m": [1,   2,   2,   1,   1],       # 並列数
})

# 資源キャパシティ cap_i = m_i / t_i (個/分)
stages["キャパ_個毎分"] = stages["サーバ数_m"] / stages["処理時間_t"]

cap_proc   = stages["キャパ_個毎分"].min()        # 工程キャパ = min(cap_i)
b_idx      = stages["キャパ_個毎分"].idxmin()
bottleneck = stages.loc[b_idx, "ステージ"]
cycle_time = 1.0 / cap_proc                         # サイクルタイム(分/個)

# スループット R(cap_proc 以下)を与えて稼働率 rho_i = R / cap_i
R = 0.30  # 個/分
stages["稼働率_rho"] = R / stages["キャパ_個毎分"]

print(stages.to_string(index=False, float_format=lambda x: f"{x:.4f}"))
print()
print(f"工程キャパシティ = min(cap_i) = {cap_proc:.4f} 個/分")
print(f"ボトルネック   = {bottleneck}")
print(f"サイクルタイム = 1/cap = {cycle_time:.4f} 分/個")
print(f"R = {R} 個/分 のとき 最大稼働率 = {stages['稼働率_rho'].max():.4f}{bottleneck})")

出力:

ステージ  処理時間_t  サーバ数_m  キャパ_個毎分  稼働率_rho
  切断  2.0000       1   0.5000   0.6000
  加工  5.0000       2   0.4000   0.7500
  組立  4.0000       2   0.5000   0.6000
  検査  3.0000       1   0.3333   0.9000
  梱包  1.5000       1   0.6667   0.4500

工程キャパシティ = min(cap_i) = 0.3333 個/分
ボトルネック   = 検査
サイクルタイム = 1/cap = 3.0000 分/個
R = 0.3 個/分 のとき 最大稼働率 = 0.9000(検査)

出力の意味:処理時間が最長なのは加工(5 分)ですが、加工はサーバが 2 台あるため cap=0.40。ボトルネックは検査(1 台・3 分 → cap=0.333)です。キャパシティは時間の長さでなく m/tm/t で決まることに注意してください。工程全体は **0.333 個/分(サイクルタイム 3 分/個)**しか出せません。スループット R=0.3R=0.3 で流すと、ボトルネックの検査が 稼働率 0.90 と最も逼迫し、梱包は 0.45 と余裕——非ボトルネックには遊びがあります。

4. What-if:ボトルネックを動かす(コード+図)

需要が工程キャパシティを超えるとき、インプライド稼働率で律速資源を特定し、そこにサーバを1台足すとどうなるかを before / after で比較します。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

# code1 と同じ工程
names = ["切断", "加工", "組立", "検査", "梱包"]
t = np.array([2.0, 5.0, 4.0, 3.0, 1.5])   # 処理時間(分/個)
m = np.array([1, 2, 2, 1, 1])              # サーバ数

def analyze(m):
    cap = m / t
    return cap, cap.min(), names[int(np.argmin(cap))]

demand = 0.45  # 個/分(工程キャパを超える需要を想定)

# before:現状
cap0, cap_proc0, bn0 = analyze(m)
impl0 = demand / cap0
thru0 = min(demand, cap_proc0)

# after:ボトルネックに並列サーバを1台追加
m2 = m.copy()
m2[int(np.argmin(cap0))] += 1
cap1, cap_proc1, bn1 = analyze(m2)
impl1 = demand / cap1
thru1 = min(demand, cap_proc1)

print(f"需要 = {demand} 個/分")
print("--- before(現状)---")
print(f"  ボトルネック         = {bn0}")
print(f"  工程キャパ           = {cap_proc0:.4f} 個/分")
print(f"  最大インプライド稼働率 = {impl0.max():.4f}{bn0})")
print(f"  達成スループット      = {thru0:.4f} 個/分")
print(f"--- after({bn0}に +1 サーバ)---")
print(f"  ボトルネック         = {bn1}")
print(f"  工程キャパ           = {cap_proc1:.4f} 個/分")
print(f"  最大インプライド稼働率 = {impl1.max():.4f}{bn1})")
print(f"  達成スループット      = {thru1:.4f} 個/分")

# 図:資源別キャパシティ(before/after)。ボトルネックを赤で色分け
fig, axes = plt.subplots(1, 2, figsize=(11, 4), sharey=True)
panels = [
    (axes[0], cap0, cap_proc0, bn0, "before(現状)"),
    (axes[1], cap1, cap_proc1, bn1, f"after({bn0}に +1 サーバ)"),
]
for ax, cap, cap_proc, bn, title in panels:
    colors = ["#d62728" if nm == bn else "#1f77b4" for nm in names]
    ax.bar(names, cap, color=colors)
    ax.axhline(cap_proc, ls="--", color="gray", label=f"工程キャパ={cap_proc:.3f}")
    ax.axhline(demand, ls=":", color="green", label=f"需要={demand}")
    ax.set_title(title)
    ax.legend(fontsize=9)
axes[0].set_ylabel("資源キャパシティ(個/分)")
fig.suptitle("ボトルネックは最小キャパシティの資源。改善すると律速が移動する", fontsize=12)
plt.tight_layout()
plt.show()

出力:

需要 = 0.45 個/分
--- before(現状)---
  ボトルネック         = 検査
  工程キャパ           = 0.3333 個/分
  最大インプライド稼働率 = 1.3500(検査)
  達成スループット      = 0.3333 個/分
--- after(検査に +1 サーバ)---
  ボトルネック         = 加工
  工程キャパ           = 0.4000 個/分
  最大インプライド稼働率 = 1.1250(加工)
  達成スループット      = 0.4000 個/分

出力の意味:需要 0.45 に対し、現状はボトルネック検査のインプライド稼働率が 1.35——需要をさばけず、実際に出せるのは工程キャパシティの 0.333 個/分どまり(タクトタイム 1/0.452.221/0.45 \approx 2.22 分/個に対しサイクルタイム 3 分/個で間に合わない)。検査に 1 台足すと検査の cap は 0.667 に跳ね上がり、今度は加工(0.40)が新しいボトルネックになります。律速が移動し、工程キャパシティは 0.333 → 0.400、達成スループットも同じく改善しました。図の赤い棒が左(検査)から右(加工)へ動き、灰色の破線(工程キャパ)が上がっているのが、ボトルネックの移動とキャパシティ増を表します。さらに増やすには、次は加工に手を入れます——改善は常に「現在のボトルネック」を追いかけるのです。

⚠️ よくある誤解

関連ノート