SimPy で工場内の滞留在庫をシミュレーションしてみよう

ザ・ゴールとは

かつて2014年 買ってよかった本10選 - たーせる日記でリストアップした奇跡的名著『ザ・ゴール』について、改めてご紹介しよう。

なお、本記事はアフィリエイトやステマではないので安心してお読みいただきたい。

あらすじ

物語の舞台は大企業の地方拠点。 この工場は慢性的な業績不振に悩まされていた。

半年前に所長として赴任したあらろうは、本部長から「3ヶ月以内に業績の立て直しができなければ工場閉鎖」を宣告され、業務改革を決意する。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.17 より

閉鎖の危機から工場を救うには一体何をすればよいのか、目標ザ・ゴールは果たして何なのか──。

窮地に立たされた吾郎は、「完全にバランスの取れた工場」の実現を夢想する。

もしも、すべてのリソースの生産能力が市場の需要と均衡した理想的な工場が実現すれば、コストが減って利益が増えるはず ── だが果たして本当にそうだろうか。

そこで吾郎は自説を検証すべく、ありあわせの道具を使い、工場のモデル*1を作ってシミュレーションを試みる。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.105 より

ところが結果は吾郎の予想を裏切るものであった。 至るところでかかり在庫が滞留し、製品が予定通りに完成しなかったのである。

吾郎の淡い希望はもろくも崩れ去ってしまった。

この現実を目の当たりにした吾郎は、『バランスの取れた工場に近づくほど倒産に近づく』という師・ジョナの言葉を思い出して愕然とするのであった。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.107 より

果たして吾郎は工場を危機から救えるのか!?

続きが気になる方は、ぜひ書店で手に取って読んでいただきたい。

Python で工場のシミュレーション

というわけでようやく今日の本題である。

SimPy というライブラリ*2を使ってミニ工場を作り、どこで何が起こるのかを観察してみたいと思う。 SimPy は標準ライブラリではないため、pipuv等でインストールが必要である。

ちなみに SimPy は工場だけでなく、待ち行列やリソース計画などさまざまな使い途があり、難しい数理知識を必要とせずにさまざまな離散事象をシミュレーションできるので、これを機に知っておいて損はない。

SimPy で超簡単な工場のモデルを作る

まず SimPy が何者なのかを理解するために、これ以上ないくらい簡単な例から始めよう。

開始から完了までに5秒かかる作業プロセスがあるとして、その作業を時刻0に開始したとき、5秒後にどうなっているかをシミュレーションしてみよう。

import simpy

# シンプルなシミュレーション関数
def func(env: simpy.Environment):
    print(env.now, "開始")  # env.nowで現在時刻を取得
    yield env.timeout(5)   # timeoutで5秒、時間を進める
    print(env.now, "終了")

def main():
    env = simpy.Environment()   # シミュレーション環境の作成
    env.process(func(env))      # シミュレーション関数の登録
    env.run()                   # シミュレーションの実行

if __name__ == "__main__":
    main()

実行結果

0 開始
5 終了

プログラムを実行すると、一瞬で結果が表示される。

つまり SimPy を使うと、現実的な時間経過を待つことなく、工場で起こる数時間、数日、数週間分もの出来事を一瞬にして再現できてしまうのである。

よりマシな工場のモデル

次に、もう少しだけリアルな題材を考えてみよう。

ある工場では、1台の加工機械を用いて製品を製造しています。 この機械は一定速度で稼働しており、1つの資材(材料)を加工して1つの製品を完成させるまでに、ちょうど3秒を要します。 また、機械は同時に複数の資材を処理することはできず、常に1つずつ順番に加工を行います。

工場には、新しい資材が2秒ごとに到着します。 搬入された資材は、機械が空いていれば直ちに加工されますが、すでに別の資材を加工中であれば、加工可能になるまで待機しなければなりません。

これは、『ザ・ゴール』の作中で依存的事象と呼ばれている現象であり、たとえば「前工程が完了していないと着手できない」「部品が揃わないと着手できない」「機械が空いていないと着手できない」等を意味する。

エリヤフ・ゴールドラット著『ザ・ゴール』 p.83 より

こうした条件を再現するため、SimPy には以下が用意されている。

env
シミュレーション世界そのもの
process
時間の中で動く処理。工程、作業者、製品などを表す
Resource
機械、人、設備などの限りある資源

これらを用いて先ほどの条件をコードに書き起こし、それを実行した結果を以下に示す。

import simpy

# 資材が工程Aを通過するプロセス
def item(env: simpy.Environment, name: str, machine: simpy.Resource):
    print(f"{env.now}: {name}が工程Aに到着")

    with machine.request() as req:
        yield req                               # リソースの使用を要求

        print(f"{env.now}: {name}が工程Aを開始")
        yield env.timeout(3)                    # 工程Aに3秒かかる
        print(f"{env.now}: {name}が工程Aを終了")

# 資材を生成するプロセス
def source(env: simpy.Environment, machine: simpy.Resource):
    for i in range(5):
        env.process(item(env, f"資材{i}", machine))
        yield env.timeout(2)                    # 2秒ごとに新しい資材が到着

def main():
    env = simpy.Environment()                   # シミュレーション環境の作成
    machine = simpy.Resource(env, capacity=1)   # 工程Aのリソースを作成(1台の機械)
    env.process(source(env, machine))           # シミュレーション関数の登録
    env.run()                                   # シミュレーションの実行

if __name__ == "__main__":
    main()

実行結果

0: 資材0が工程Aに到着
0: 資材0が工程Aを開始
2: 資材1が工程Aに到着
3: 資材0が工程Aを終了
3: 資材1が工程Aを開始
4: 資材2が工程Aに到着
6: 資材1が工程Aを終了
6: 資材3が工程Aに到着
6: 資材2が工程Aを開始
8: 資材4が工程Aに到着
9: 資材2が工程Aを終了
9: 資材3が工程Aを開始
12: 資材3が工程Aを終了
12: 資材4が工程Aを開始
15: 資材4が工程Aを終了

仕掛在庫を見える化する

ここまでで、それぞれの時刻で何が起きたかを確認することができた。

さらに、仕掛在庫の増減を可視化するため、コードを弄ってみよう。

在庫は、工程Aの投入待ちとなっている資材の在庫と、工程Aを経て出荷可能となった製品をそれぞれ数える必要があるため、ここでは辞書を使ってそれを実現している。

import simpy

# 資材が工程Aを通過するプロセス
def item(
    env: simpy.Environment,
    name: str,                                  # 資材の名前
    machine: simpy.Resource,                    # 工程Aで使用する機械
    stock: dict[str, int],                      # 在庫を管理する辞書
):
    stock["before_a"] += 1                      # 工程A前の在庫を1増やす
    print(
        f"{env.now}: {name}が工程Aに到着 "
        f"(工程A前在庫={stock['before_a']}, 工程A後在庫={stock['after_a']})"
    )

    with machine.request() as req:
        yield req                               # リソースの使用を要求

        stock["before_a"] -= 1                  # 工程A前の在庫を1減らす
        print(
            f"{env.now}: {name}が工程Aを開始 "
            f"(工程A前在庫={stock['before_a']}, 工程A後在庫={stock['after_a']})"
        )
        yield env.timeout(3)                    # 工程Aに3秒かかる
        stock["after_a"] += 1                   # 工程A後の在庫を1増やす
        print(
            f"{env.now}: {name}が工程Aを終了 "
            f"(工程A前在庫={stock['before_a']}, 工程A後在庫={stock['after_a']})"
        )

# 資材を生成するプロセス
def source(env: simpy.Environment, machine: simpy.Resource, stock: dict[str, int]):
    for i in range(5):
        env.process(item(env, f"資材{i}", machine, stock))
        yield env.timeout(2)                    # 2秒ごとに新しい資材が到着

def main():
    env = simpy.Environment()                   # シミュレーション環境の作成
    machine = simpy.Resource(env, capacity=1)   # 工程Aのリソースを作成(1台の機械)
    stock = {"before_a": 0, "after_a": 0}       # 在庫の初期化
    env.process(source(env, machine, stock))    # シミュレーション関数の登録
    env.run()                                   # シミュレーションの実行

if __name__ == "__main__":
    main()

実行結果

0: 資材0が工程Aに到着 (工程A前在庫=1, 工程A後在庫=0)
0: 資材0が工程Aを開始 (工程A前在庫=0, 工程A後在庫=0)
2: 資材1が工程Aに到着 (工程A前在庫=1, 工程A後在庫=0)
3: 資材0が工程Aを終了 (工程A前在庫=1, 工程A後在庫=1)
3: 資材1が工程Aを開始 (工程A前在庫=0, 工程A後在庫=1)
4: 資材2が工程Aに到着 (工程A前在庫=1, 工程A後在庫=1)
6: 資材1が工程Aを終了 (工程A前在庫=1, 工程A後在庫=2)
6: 資材3が工程Aに到着 (工程A前在庫=2, 工程A後在庫=2)
6: 資材2が工程Aを開始 (工程A前在庫=1, 工程A後在庫=2)
8: 資材4が工程Aに到着 (工程A前在庫=2, 工程A後在庫=2)
9: 資材2が工程Aを終了 (工程A前在庫=2, 工程A後在庫=3)
9: 資材3が工程Aを開始 (工程A前在庫=1, 工程A後在庫=3)
12: 資材3が工程Aを終了 (工程A前在庫=1, 工程A後在庫=4)
12: 資材4が工程Aを開始 (工程A前在庫=0, 工程A後在庫=4)
15: 資材4が工程Aを終了 (工程A前在庫=0, 工程A後在庫=5)

それぞれの局面で、どこにどれだけ在庫が溜まっているかが分かるようになった。

1工程に3秒かかる一方で、その資材が2秒おきに届くということは、言うまでもなく生産が追いつかない状態である。

実際、資材が到着し続ける限り、投入待ちの在庫が列をなしていくことが観察できるだろう。

依存的事象と統計的変動の再現

さて、いよいよ話を本格化させよう。 まず、先ほどのモデルに対してさらに工程を追加する。 つまり、資材を製品に変えるには 工程A → 工程B を経ないといけない。

そして、各工程の所要時間は一定ではなく、その時々によって多少の揺れがあるものとする。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.84 より

ここまでくると、そろそろ凡人の頭脳では限界が近づいてくる。

今回扱うモデルには、前述の依存的事象に加えて統計的変動という2つの現象を同時に作用させる必要があるからだ。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.73 より

以下はそれらの条件を反映したコードである。 見通しを良くするため、在庫をカウントする処理はオミットしてある。

import simpy
import random

def process_item(env, name, machine_a, machine_b):
    print(f"{env.now}: {name} 到着")

    with machine_a.request() as req:
        yield req
        print(f"{env.now}: {name} 工程A開始")
        yield env.timeout(random.randint(3, 6))
        print(f"{env.now}: {name} 工程A完了")

    with machine_b.request() as req:
        yield req
        print(f"{env.now}: {name} 工程B開始")
        yield env.timeout(random.randint(5, 9))
        print(f"{env.now}: {name} 工程B完了")

def source(env, machine_a, machine_b):
    for i in range(10):
        env.process(process_item(env, f"品目{i}", machine_a, machine_b))
        yield env.timeout(3)

def main():
    env = simpy.Environment()

    machine_a = simpy.Resource(env, capacity=1)   # 工程Aのリソース(1台の機械)
    machine_b = simpy.Resource(env, capacity=1)   # 工程Bのリソース(1台の機械)

    env.process(source(env, machine_a, machine_b))
    env.run()

if __name__ == "__main__":
    main()

実行結果

0: 品目0 到着
0: 品目0 工程A開始
3: 品目1 到着
5: 品目0 工程A完了
5: 品目0 工程B開始
5: 品目1 工程A開始
6: 品目2 到着
8: 品目1 工程A完了
8: 品目2 工程A開始
9: 品目3 到着
11: 品目2 工程A完了
11: 品目3 工程A開始
12: 品目4 到着

……後略……

文字だけだといささか見づらいので、それぞれの工程における仕掛在庫を時系列で視覚化してみよう。

まずは到着した資材の在庫を見ると、在庫の量がじわじわ増えていることが分かる。後半で待ち行列がけているのは単に新しい資材が届かなくなったからに過ぎない。 そうでもなければ延々と手付かずの資材が倉庫に溢れかえり、いずれ現場はしょうけつを極めることになるだろう。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "工程A投入前の在庫数"
    x-axis "時間 t" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
    y-axis "在庫数" 0 --> 10
    line [0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 2, 1, 1, 2, 2, 2, 3, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

続いて、工程Aが完了し、工程Bの投入を待つ仕掛在庫の推移を確認する。こちらは先ほどよりもさらに悲惨で、最大で投入した資材のうち5つ(実に半分)が倉庫で眠ることになる。

滞留している在庫はとっとと製品化して売れば金に換わるので、設備投資をして機械を増やすか外注に出すかを真剣に検討すべきだろう。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "工程B投入前の在庫数"
    x-axis "時間 t" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
    y-axis "在庫数" 0 --> 10
    line [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 2, 2, 3, 3, 2, 2, 2, 3, 3, 3, 3, 4, 3, 3, 3, 3, 4, 4, 4, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

最後に、出荷可能になった製品数の時系列の推移を見てみよう。

これだけだと正直よく分からないので、もう一つ別の見方をこのあと示す。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "出荷可能な製品数"
    x-axis "時間 t" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
    y-axis "在庫数" 0 --> 10
    line [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10]

資材を投入してから完成までの所要時間の分布をヒストグラムにしたものが以下である。

最短で14秒、最長で52秒もかかっている。 重要なのは、機械がどれだけ稼働したかではなくどれだけ売れたかなので、それに対してこのスループットはよろしくない。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "リードタイム分布"
    x-axis "リードタイム帯" ["10-19", "20-29", "30-39", "40-49", "50-59"]
    y-axis "件数" 0 --> 6
    bar [2, 2, 3, 2, 1]

ここから分かるのは、ボトルネックとなる工程の手前では仕掛在庫の滞留が顕著であるという事実である。

言うまでもなく 工程B がボトルネックになっており、これが全工程にわたってスループットを支配していることになる。

ではここで、各品目で何が起きているかをタイムラインにプロットしてみよう。工程A が完了したにもかかわらず、すぐさま 工程B に投入できない「待ち時間」が伸び、在庫がはんらんしていることが一目瞭然だ。

---
displayMode: compact
---
gantt
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S
    tickInterval 5second

    section 品目0
    到着 :milestone, 1970-01-01 00:00:00, 0s
    工程A :1970-01-01 00:00:00, 5s
    工程B :1970-01-01 00:00:05, 9s

    section 品目1
    到着 :milestone, 1970-01-01 00:00:03, 0s
    工程A :1970-01-01 00:00:05, 3s
    工程B :1970-01-01 00:00:14, 7s

    section 品目2
    到着 :milestone, 1970-01-01 00:00:06, 0s
    工程A :1970-01-01 00:00:08, 3s
    工程B :1970-01-01 00:00:21, 6s

    section 品目3
    到着 :milestone, 1970-01-01 00:00:09, 0s
    工程A :1970-01-01 00:00:11, 3s
    工程B :1970-01-01 00:00:27, 8s

    section 品目4
    到着 :milestone, 1970-01-01 00:00:12, 0s
    工程A :1970-01-01 00:00:14, 5s
    工程B :1970-01-01 00:00:35, 9s

    section 品目5
    到着 :milestone, 1970-01-01 00:00:15, 0s
    工程A :1970-01-01 00:00:19, 6s
    工程B :1970-01-01 00:00:44, 5s

    section 品目6
    到着 :milestone, 1970-01-01 00:00:18, 0s
    工程A :1970-01-01 00:00:25, 5s
    工程B :1970-01-01 00:00:49, 6s

    section 品目7
    到着 :milestone, 1970-01-01 00:00:21, 0s
    工程A :1970-01-01 00:00:30, 4s
    工程B :1970-01-01 00:00:55, 6s

    section 品目8
    到着 :milestone, 1970-01-01 00:00:24, 0s
    工程A :1970-01-01 00:00:34, 5s
    工程B :1970-01-01 00:01:01, 9s

    section 品目9
    到着 :milestone, 1970-01-01 00:00:27, 0s
    工程A :1970-01-01 00:00:39, 3s
    工程B :1970-01-01 00:01:10, 9s

今度は少し視点を変えて、各工程を縦軸にしたタイムラインを眺めてみることにしよう。 工程A も 工程B も、資材が届き続ける限り、稼働率はほぼ MAX で張り付いていることがわかる。

---
displayMode: compact
---
gantt
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S
    tickInterval 5second

    section 到着
    品目0 :milestone, 1970-01-01 00:00:00, 0s
    品目1 :milestone, 1970-01-01 00:00:03, 0s
    品目2 :milestone, 1970-01-01 00:00:06, 0s
    品目3 :milestone, 1970-01-01 00:00:09, 0s
    品目4 :milestone, 1970-01-01 00:00:12, 0s
    品目5 :milestone, 1970-01-01 00:00:15, 0s
    品目6 :milestone, 1970-01-01 00:00:18, 0s
    品目7 :milestone, 1970-01-01 00:00:21, 0s
    品目8 :milestone, 1970-01-01 00:00:24, 0s
    品目9 :milestone, 1970-01-01 00:00:27, 0s

    section 工程A
    品目0 :1970-01-01 00:00:00, 5s
    品目1 :1970-01-01 00:00:05, 3s
    品目2 :1970-01-01 00:00:08, 3s
    品目3 :1970-01-01 00:00:11, 3s
    品目4 :1970-01-01 00:00:14, 5s
    品目5 :1970-01-01 00:00:19, 6s
    品目6 :1970-01-01 00:00:25, 5s
    品目7 :1970-01-01 00:00:30, 4s
    品目8 :1970-01-01 00:00:34, 5s
    品目9 :1970-01-01 00:00:39, 3s

    section 工程B
    品目0 :1970-01-01 00:00:05, 9s
    品目1 :1970-01-01 00:00:14, 7s
    品目2 :1970-01-01 00:00:21, 6s
    品目3 :1970-01-01 00:00:27, 8s
    品目4 :1970-01-01 00:00:35, 9s
    品目5 :1970-01-01 00:00:44, 5s
    品目6 :1970-01-01 00:00:49, 6s
    品目7 :1970-01-01 00:00:55, 6s
    品目8 :1970-01-01 00:01:01, 9s
    品目9 :1970-01-01 00:01:10, 9s

これが何を意味するか。 『ザ・ゴール』作中で、ジョナは「稼働率100%の工場は非常に非効率」と断言する。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.65 より

この例がまさしくそうなのだ。 各工程ともに休みなく手を動かしていながら、仕掛在庫の山が積み上がっていることがその証左である。

この状況で、非ボトルネックである 工程A をどうこうすることには何の意味もなく、反対に 工程B に対しては一刻も早くてこれが必要である。

ボトルネック工程への梃入れ

そこで試しに、工程Bの機械の台数を2台に増やしたら何が起こるかをシミュレーションしてみよう。 先ほどのコードに対する修正箇所はたったの1箇所のみである。

import simpy
import random

def process_item(env, name, machine_a, machine_b):
    print(f"{env.now}: {name} 到着")

    with machine_a.request() as req:
        yield req
        print(f"{env.now}: {name} 工程A開始")
        yield env.timeout(random.randint(3, 6))
        print(f"{env.now}: {name} 工程A完了")

    with machine_b.request() as req:
        yield req
        print(f"{env.now}: {name} 工程B開始")
        yield env.timeout(random.randint(5, 9))
        print(f"{env.now}: {name} 工程B完了")

def source(env, machine_a, machine_b):
    for i in range(10):
        env.process(process_item(env, f"品目{i}", machine_a, machine_b))
        yield env.timeout(3)

def main():
    env = simpy.Environment()

    machine_a = simpy.Resource(env, capacity=1)
    machine_b = simpy.Resource(env, capacity=2)   # ★ 修正: 工程Bの機械を2台に

    env.process(source(env, machine_a, machine_b))
    env.run()

if __name__ == "__main__":
    main()

この結果をもとに、先ほどと同様に 工程B の手前の仕掛在庫の推移を視覚化すると、劇的な変化が認められる。 なんと、ボトルネックが解消し、工程B の手前で殆ど詰まることなく仕掛品が通過しているのである。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "工程B投入前の在庫数"
    x-axis "時間 t" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
    y-axis "在庫数" 0 --> 10
    line [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

さらに 工程B が完了し、出荷可能となった製品数の時系列推移を確認しよう。 先ほどは80秒近くかかってようやく全量の製造が完了していたが、ボトルネック(工程B)を解消した結果、20秒以上も短くなったことが読み取れる。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "出荷可能な製品数"
    x-axis "時間 t" [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
    y-axis "在庫数" 0 --> 10
    line [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

製造のリードタイムも劇的に短縮された。 ぜひ先ほどのヒストグラムと見較べていただきたい。

%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1f77b4"}}}}%%
xychart-beta
    title "リードタイム分布"
    x-axis "リードタイム帯" ["10-19", "20-29", "30-39", "40-49", "50-59"]
    y-axis "件数" 0 --> 6
    bar [5, 5, 0, 0, 0]

ここで改めて、品目ごとのタイムラインも見てみよう。

先ほど問題となっていた 工程A と 工程B のタイムラグは跡形もなく解消しており、ひとたび工程A に資材が投入されれば一気通貫に製造を終えることができる事実を物語っている。

---
displayMode: compact
---
gantt
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S
    tickInterval 5second

    section 品目0
    到着 :milestone, 1970-01-01 00:00:00, 0s
    工程A :1970-01-01 00:00:00, 5s
    工程B :1970-01-01 00:00:05, 5s

    section 品目1
    到着 :milestone, 1970-01-01 00:00:03, 0s
    工程A :1970-01-01 00:00:05, 4s
    工程B :1970-01-01 00:00:09, 8s

    section 品目2
    到着 :milestone, 1970-01-01 00:00:06, 0s
    工程A :1970-01-01 00:00:09, 6s
    工程B :1970-01-01 00:00:15, 7s

    section 品目3
    到着 :milestone, 1970-01-01 00:00:09, 0s
    工程A :1970-01-01 00:00:15, 4s
    工程B :1970-01-01 00:00:19, 6s

    section 品目4
    到着 :milestone, 1970-01-01 00:00:12, 0s
    工程A :1970-01-01 00:00:19, 4s
    工程B :1970-01-01 00:00:23, 9s

    section 品目5
    到着 :milestone, 1970-01-01 00:00:15, 0s
    工程A :1970-01-01 00:00:23, 5s
    工程B :1970-01-01 00:00:28, 5s

    section 品目6
    到着 :milestone, 1970-01-01 00:00:18, 0s
    工程A :1970-01-01 00:00:28, 6s
    工程B :1970-01-01 00:00:34, 9s

    section 品目7
    到着 :milestone, 1970-01-01 00:00:21, 0s
    工程A :1970-01-01 00:00:34, 3s
    工程B :1970-01-01 00:00:37, 8s

    section 品目8
    到着 :milestone, 1970-01-01 00:00:24, 0s
    工程A :1970-01-01 00:00:37, 4s
    工程B :1970-01-01 00:00:43, 7s

    section 品目9
    到着 :milestone, 1970-01-01 00:00:27, 0s
    工程A :1970-01-01 00:00:41, 6s
    工程B :1970-01-01 00:00:47, 8s

工程を軸にしたタイムラインも確認しよう。

工程B に機械を導入して並列稼働を可能にしたことで、機械そのものの稼働率は下がっているが、その代わり製造リードタイムは大幅に改善された

---
displayMode: compact
---
gantt
    dateFormat  YYYY-MM-DD HH:mm:ss
    axisFormat  %S
    tickInterval 5second

    section 到着
    品目0 :milestone, 1970-01-01 00:00:00, 0s
    品目1 :milestone, 1970-01-01 00:00:03, 0s
    品目2 :milestone, 1970-01-01 00:00:06, 0s
    品目3 :milestone, 1970-01-01 00:00:09, 0s
    品目4 :milestone, 1970-01-01 00:00:12, 0s
    品目5 :milestone, 1970-01-01 00:00:15, 0s
    品目6 :milestone, 1970-01-01 00:00:18, 0s
    品目7 :milestone, 1970-01-01 00:00:21, 0s
    品目8 :milestone, 1970-01-01 00:00:24, 0s
    品目9 :milestone, 1970-01-01 00:00:27, 0s

    section 工程A
    品目0 :1970-01-01 00:00:00, 5s
    品目1 :1970-01-01 00:00:05, 4s
    品目2 :1970-01-01 00:00:09, 6s
    品目3 :1970-01-01 00:00:15, 4s
    品目4 :1970-01-01 00:00:19, 4s
    品目5 :1970-01-01 00:00:23, 5s
    品目6 :1970-01-01 00:00:28, 6s
    品目7 :1970-01-01 00:00:34, 3s
    品目8 :1970-01-01 00:00:37, 4s
    品目9 :1970-01-01 00:00:41, 6s

    section 工程B
    品目0 :1970-01-01 00:00:05, 5s
    品目1 :1970-01-01 00:00:09, 8s
    品目2 :1970-01-01 00:00:15, 7s
    品目3 :1970-01-01 00:00:19, 6s
    品目4 :1970-01-01 00:00:23, 9s
    品目5 :1970-01-01 00:00:28, 5s
    品目6 :1970-01-01 00:00:34, 9s
    品目7 :1970-01-01 00:00:37, 8s
    品目8 :1970-01-01 00:00:43, 7s
    品目9 :1970-01-01 00:00:47, 8s

設備投資をして機械を増やせば、当然ながら製造コストは跳ね上がるが、ボトルネックを通過できずにいる在庫の山を放置する方が遥かに深刻である。

こんな簡単なプログラムからでも、これだけ様々な洞察が得られるのである。

まとめ

これまで、KKD勘・経験・度胸という意思決定があった。

作中でも忠告されている通り、闇雲な効率化は悪手である。 そうは分かっていても「依存的事象」と「統計的変動」を同時に考慮したシミュレーションはあまり現場に浸透していないのが現状である。

かえりみれば我々が要件定義のときに描く業務フローも、一見すると仕事がよどみなく流れるように見えるが、話はそう簡単ではない。 盲点となるところにボトルネックが潜んでおり、それ以外をどれだけ効率化してもクライアント企業が目標ザ・ゴールに近づくことはない。

そんなわけで今回は、吾郎が試みたアナログな実験をプログラムで再現する手法を紹介した。

難しい数理的な知識を必要とせず、誰でも簡単なコーディングでボトルネックをあぶり出せるので、SimPy は有用なツールである。

ぜひ、あなたにとって何かの参考になれば幸いである。


余談だが、この本の原題には、The Goal と定冠詞がついている。

真・英文法大全』によると、定冠詞とは「全員が『せーの』で指をさせるもの」だという。人によってバラバラであってはならないのだ。

作中で吾郎に工場救済のヒントを与えたジョナも、『どんな会社であっても目標はひとつしかない』と断言している通り、全員が『せーの』で指をさした、たった一つの目標に向かって足並みを揃えねば、全体最適も業務改革も成し得ない。

エリヤフ・ゴールドラット著『ザ・ゴール コミック版』 p.29 より

*1:時系列で状態が確率的に変動する待ち行列ネットワークモデル。

*2:https://simpy.readthedocs.io/en/latest/

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.