Mímisbrunnr知恵の泉

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

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

📎 前提:オペレーションズ・マネジメントとは(L=λW の導入) | 次:ボトルネックとキャパシティ | 数理:確率過程(マルコフ連鎖・ポアソン過程)

要点(BLUF)

1. プロセスを3つの指標で見る

OM では、製品や注文や患者など「系の中を流れる対象」をフローユニットと呼びます。プロセスの状態は、次の3つの指標で過不足なく記述できます。

この3つは独立ではなく、たった1本の式で結ばれています。それがリトルの法則です。01-01 で導入した待ち行列の記号との対応は次の通り——同じ法則の言い換えです。

記号プロセス分析の呼び名待ち行列(01-01)意味
II在庫・仕掛 WIPLL(系内数)系の中の平均数
RRフローレート・スループットλ\lambda(到着率)単位時間に流れる数
TTフロータイムWW(滞在時間)1ユニットが系にいる時間

2. リトルの法則 I=RTI = R\,T を導く

I=RTI = R\,T

なぜこの式が成り立つのか、確率分布をいっさい仮定せずに導けます(これが「分布によらない」ことの理由です)。観測時間 [0,τ][0,\tau] のあいだに NN 個のフローユニットが系を通り、ユニット ii の滞在時間を TiT_i とします。系内数 n(t)n(t)(時刻 tt に系の中にいる数)を時間積分すると、各ユニットは自分が系にいた時間ぶんだけ面積に寄与するので、

0τn(t)dt=i=1NTi\int_0^\tau n(t)\,dt = \sum_{i=1}^{N} T_i

両辺を τ\tau で割ります。左辺は系内数の時間平均=在庫 II。右辺は Nτ1NiTi\dfrac{N}{\tau}\cdot\dfrac{1}{N}\sum_i T_i と分解でき、Nτ\dfrac{N}{\tau} がフローレート RR1NiTi\dfrac{1}{N}\sum_i T_i がフロータイム TT です。よって

I=1τi=1NTi=Nτ1Ni=1NTi=RTI = \frac{1}{\tau}\sum_{i=1}^{N} T_i = \frac{N}{\tau}\cdot\frac{1}{N}\sum_{i=1}^{N} T_i = R\,T

確率も分布も登場しません。定常(入る量と出る量が長期的に釣り合う)でありさえすれば成り立つ恒等式です。これが在庫管理(第3章)でも待ち行列(第4章)でも同じ式が効く理由です。

3. プロセス分析の実際:フロータイム効率(コード)

1件の注文が複数アクティビティを通る工程を考えます。各アクティビティには付加価値時間(実際に価値を生む作業)と、その手前で生じる待ち時間(滞留)があります。総フロータイムに占める付加価値時間の割合がフロータイム効率です。

flowchart LR
  A["受注入力"] --> B["与信審査"] --> C["在庫引当"] --> D["ピッキング"] --> E["梱包"] --> F["出荷検品"]
import numpy as np
import pandas as pd

# あるフローユニット(1件の受注)が通る工程。各アクティビティの
# 付加価値時間(実作業)と、その前後の待ち時間(滞留)を分けて記録(分)
proc = pd.DataFrame({
    "アクティビティ": ["受注入力", "与信審査", "在庫引当", "ピッキング", "梱包", "出荷検品"],
    "付加価値時間": [5, 10, 3, 12, 8, 4],      # 分(実作業)
    "待ち時間":     [0, 120, 30, 45, 15, 20],  # 分(前工程での滞留)
})

proc["工程内時間"] = proc["付加価値時間"] + proc["待ち時間"]

T_va   = proc["付加価値時間"].sum()   # 付加価値時間の合計
T_flow = proc["工程内時間"].sum()     # 総フロータイム T
eff    = T_va / T_flow                  # フロータイム効率

print(proc.to_string(index=False))
print()
print(f"付加価値時間 T_VA = {T_va} 分")
print(f"総フロータイム T  = {T_flow} 分 (= {T_flow/60:.3f} 時間)")
print(f"フロータイム効率   = {eff:.3f}  ({eff*100:.1f} %)")

# リトルの法則 I = R*T:スループット R を与えて仕掛在庫 WIP を算出
R = 30.0                 # 件/時(このプロセスを流れるフローレート)
T_hours = T_flow / 60    # 総フロータイムを時間に換算
I = R * T_hours          # I = R × T(件/時 × 時 = 件)
print()
print(f"スループット R    = {R} 件/時")
print(f"WIP  I = R*T      = {I:.1f} 件")

出力:

アクティビティ  付加価値時間  待ち時間  工程内時間
   受注入力       5     0      5
   与信審査      10   120    130
   在庫引当       3    30     33
  ピッキング      12    45     57
     梱包       8    15     23
   出荷検品       4    20     24

付加価値時間 T_VA = 42 分
総フロータイム T  = 272 分 (= 4.533 時間)
フロータイム効率   = 0.154  (15.4 %)

スループット R    = 30.0 件/時
WIP  I = R*T      = 136.0 件

出力の意味:272 分のフロータイムのうち、実際に価値を生んでいるのは 42 分だけ。フロータイム効率は 15.4 % で、残り 84.6 % は待ち(とくに与信審査の前で 120 分も滞留)です。改善の的は作業の高速化ではなく、この滞留の削減だと一目でわかります。最後にリトルの法則 I=RTI = R\,T を使い、フローレート R=30R = 30 件/時・フロータイム T=4.533T = 4.533 時から、この工程に常時 136 件が仕掛として滞留していると算出しました。フロータイムを縮めれば、RR が同じでも仕掛在庫がそのぶん減ります。

4. リトルの法則は分布によらない(実証コード)

導出(第2節)が示す通り、I=RTI = R\,T は確率分布を仮定しない恒等式です。これを擬似データで確かめます。到着はポアソン過程(到着間隔 ~ Exp)にしつつ、各ユニットの滞在時間 TiT_iあえて非指数——右に重く歪んだ対数正規・ガンマ・一定(決定的)——から生成します。時間軸上の系内数 n(t)n(t) の階段関数から時間平均 LL を直接計算し、λW\lambda W と一致するかを見ます。

これは「待ち行列の理論」ではなく恒等式の実証です。到着・サービスの分布から平均待ち時間を求める理論は第4章と 確率過程(マルコフ連鎖・ポアソン過程) で扱います。

import numpy as np
import pandas as pd

rng = np.random.default_rng(7)

lam = 3.0           # 平均到着率(件/時)
horizon = 20000.0   # 観測時間(時)
mu_T = 2.0          # どの分布でも平均滞在時間は 2.0 時間にそろえる

# 到着=ポアソン過程(到着間隔 ~ Exp(1/λ))。観測窓 [0, horizon) 内の到着のみ
gaps = rng.exponential(1.0/lam, size=int(lam*horizon*1.2))
arrivals = np.cumsum(gaps)
arrivals = arrivals[arrivals < horizon]
N = arrivals.size

def time_average_L(arrivals, T, horizon):
    """系内数 n(t) の階段関数(区分定数)から時間平均 L を直接計算する。"""
    departures = arrivals + T
    ev_t = np.concatenate([arrivals, departures])               # イベント時刻
    ev_d = np.concatenate([np.ones(arrivals.size), -np.ones(arrivals.size)])  # 到着+1/退去-1
    order = np.argsort(ev_t, kind="mergesort")
    ev_t, ev_d = ev_t[order], ev_d[order]
    level = np.cumsum(ev_d)                 # 各イベント直後の系内数
    tc = np.clip(ev_t, 0.0, horizon)        # 観測窓にクリップ
    widths = np.diff(tc)                     # 各区間の幅 dt
    area = float(np.sum(level[:-1] * widths))  # Σ 系内数 × dt(区分定数の積分)
    return area / horizon

# 「あえて非指数」の滞在時間分布。平均はすべて mu_T にそろえる
dists = {
    "lognormal":     lambda n: rng.lognormal(np.log(mu_T) - 0.5, 1.0, n),  # 右に重い裾
    "gamma(k=2)":    lambda n: rng.gamma(2.0, mu_T / 2.0, n),              # CV=0.707
    "deterministic": lambda n: np.full(n, mu_T),                          # 一定(CV=0)
}

rows = []
for name, sampler in dists.items():
    T = sampler(N)
    W = T.mean()                       # 平均滞在時間
    cv = T.std() / T.mean()            # 変動係数(指数分布なら 1)
    L_sim = time_average_L(arrivals, T, horizon)
    lamW = (N / horizon) * W           # λ × W
    rows.append({
        "滞在時間分布": name,
        "CV(T)": cv,
        "W=mean(T)": W,
        "L_sim": L_sim,
        "lambda*W": lamW,
        "相対誤差": abs(L_sim - lamW) / lamW,
    })

res = pd.DataFrame(rows)
res["相対誤差"] = res["相対誤差"].map(lambda x: f"{x:.2e}")
print(f"lambda = {lam} 件/時,  観測内到着数 N = {N}")
print(res.to_string(index=False, float_format=lambda x: f"{x:.4f}"))

出力:

lambda = 3.0 件/時,  観測内到着数 N = 59937
       滞在時間分布  CV(T)  W=mean(T)  L_sim  lambda*W     相対誤差
    lognormal 1.3231     2.0003 5.9944    5.9946 2.97e-05
   gamma(k=2) 0.7068     2.0021 5.9987    5.9999 2.05e-04
deterministic 0.0000     2.0000 5.9934    5.9937 4.55e-05

出力の意味:3つの滞在時間分布は形がまったく違います——変動係数 CV は 1.32(対数正規、指数より暴れる)/0.71(ガンマ)/0.00(一定)。指数分布なら CV=1 ですから、どれも非指数です。それでも時間平均から直接測った LsimL_\text{sim} は、λ×W\lambda \times W(ここでは 3×2=63 \times 2 = 6)といずれも小数第3位まで一致しました。相対誤差は最大でも 2×1042\times10^{-4}(0.02 %)程度で、これは観測窓の端で「まだ系内にいる数件」を取りこぼす有限長効果にすぎず、horizon\text{horizon}\to\infty で 0 に向かいます。分布の形を問わず L=λWL = \lambda W(= I=RTI = R\,T)が成り立つことが、机上ではなくシミュレーションで確認できました。

⚠️ よくある誤解

関連ノート