[Python] トイレ待ち時間シミュレーション ～ SimPyで検証する各対策の効果

## はじめに
ネット上では男女トイレの待ち時間に関する議論が絶えません。さまざまな対策が提案される中で、実際にどの程度効果があるのかを数値で示すのは難しいものです。そこで、本記事ではシンプルなモデルを用い、Pythonのシミュレーションライブラリ「SimPy」を使ってトイレの待ち時間を再現し、各対策がどのような影響を及ぼすかを検証しました。

## 確かめたいこと
本シミュレーションでは、以下の点をモデル化しています。

- 利用者の到着
利用者は指数分布に基づくランダムな間隔で到着します。これにより、現実に近い利用パターンを再現しています。

- サービス時間
大便・小便の所要時間はガウス分布を用いて計算し、各利用者ごとの変動を取り入れています。

- 設備の利用
トイレは「個室」と「小便器」という2種類の設備を持ち、利用形態（大便か小便か）に応じて適切なリソースが割り当てられます。

## シナリオ
- オリジナル
    何の追加対策も行わない場合
- スマホ禁止案
    個室内でのスマホ使用を全面的に禁止する対策（スマホがあることでサービス時間が延びると仮定）
- 時間による強制退出案
    利用時間が「平均排便時間＋2σ」を超えた場合、強制的に退出させる対策（約2.8%の利用者が対象）
- 男女共用案
    男性トイレにある小便器を撤去し、個室を男女で共用する対策
- 男女共用＋時間による強制退出案
    男女共用の対策に加え、利用時間の上限を設ける組み合わせ

## コードの概要
シミュレーションの実装は、以下の主要な関数で構成されています。

- get_duration
ガウス分布に基づいてサービスにかかる時間を算出。指定した最小時間を下回らないように調整します。

- user
各利用者の行動をシミュレーション。到着時刻からリソースの確保、そして実際の利用時間を計測します。大便の場合は個室を、小便の場合は小便器が利用可能ならそれを、なければ個室を使います。

- arrival_process
利用者がランダムな間隔で到着するプロセスを再現し、各利用者のプロセスを生成します。

- run_simulation
各シナリオごとにシミュレーションを実行し、待ち時間（大便利用・小便利用別および全体）の平均を算出します。

- main
シナリオごとのパラメータを設定し、シミュレーションを順次実行。結果は待ち時間（秒単位）としてコンソールに出力されます。

## 実装コード
```python:wc_sim.py
import simpy
import random
import statistics

# シミュレーション実行時間（分）
SIM_DURATION = 2e6


def get_duration(min_time, mean, std):
    """
    ガウス分布に従う所要時間を計算し、最小時間を下回らない値を返す。

    Args:
        min_time (float): 最小所要時間
        mean (float): 平均所要時間
        std (float): 標準偏差

    Returns:
        float: 所要時間
    """
    return max(min_time, random.gauss(mean, std))


def user(
    env, name, urinal, private_room, sim_params, waiting_times_poop, waiting_times_pee
):
    """
    利用者プロセス

    - 到着時刻を記録し、リソース確保までの待ち時間を計測
    - 大便利用の場合は個室を利用し、大便時間と追加時間の合計がサービス時間となる
    - 小便のみの場合:
        - urinal が存在すれば小便器を利用し、小便時間がサービス時間となる
        - urinal がなければ個室を利用し、小便時間と追加時間の合計がサービス時間となる
    - サービス時間が max_private_time を超える場合は、上限時間でタイムアウトする
    """
    arrival_time = env.now

    # 利用形態の判定（大便利用かどうか）
    if random.random() < sim_params["defecation_rate"]:
        # 大便利用：個室リソースを利用
        with private_room.request() as req:
            yield req
            waiting_times_poop.append(env.now - arrival_time)

            # 大便時間と追加時間を計算
            poop_duration = get_duration(
                sim_params["poop_min"], sim_params["poop_mean"], sim_params["poop_std"]
            )
            extra_duration = get_duration(
                sim_params["private_extra_min"],
                sim_params["private_extra_mean"],
                sim_params["private_extra_std"],
            )
            service_time = poop_duration + extra_duration

            # 強制退出の上限時間を超えた場合は上限でタイムアウト
            yield env.timeout(min(service_time, sim_params["max_private_time"]))
    else:
        # 小便利用
        if urinal is not None:
            # 小便器がある場合：小便器リソースを利用
            with urinal.request() as req:
                yield req
                waiting_times_pee.append(env.now - arrival_time)
                pee_duration = get_duration(
                    sim_params["pee_min"], sim_params["pee_mean"], sim_params["pee_std"]
                )
                yield env.timeout(pee_duration)
        else:
            # 小便器がない場合：個室リソースを利用
            with private_room.request() as req:
                yield req
                waiting_times_pee.append(env.now - arrival_time)
                pee_duration = get_duration(
                    sim_params["pee_min"], sim_params["pee_mean"], sim_params["pee_std"]
                )
                extra_duration = get_duration(
                    sim_params["private_extra_min"],
                    sim_params["private_extra_mean"],
                    sim_params["private_extra_std"],
                )
                service_time = pee_duration + extra_duration
                yield env.timeout(min(service_time, sim_params["max_private_time"]))


def arrival_process(
    env, urinal, private_room, sim_params, waiting_times_poop, waiting_times_pee
):
    """
    利用者到着プロセス

    利用者は指数分布に従うインターバルで到着する。
    平均インターバルは 60 / total_users_per_hour（分）となる。
    """
    user_count = 0
    total_users_per_hour = sim_params["total_users_per_hour"]

    while True:
        interarrival = random.expovariate(total_users_per_hour / 60.0)
        yield env.timeout(interarrival)
        user_count += 1
        env.process(
            user(
                env,
                f"User {user_count}",
                urinal,
                private_room,
                sim_params,
                waiting_times_poop,
                waiting_times_pee,
            )
        )


def run_simulation(sim_params):
    """
    シミュレーション実行関数

    sim_params は以下のキーを持つ辞書:
      - name: シミュレーション名
      - total_users_per_hour: 1時間あたりの利用者数
      - num_private_rooms: 個室数
      - num_urinals: 小便器数
      - defecation_rate: 大便利用の割合（0～1）
      - pee_mean, pee_std, pee_min: 小便の所要時間パラメータ
      - poop_mean, poop_std, poop_min: 大便の所要時間パラメータ
      - private_extra_mean, private_extra_std, private_extra_min: 個室利用時の追加時間パラメータ
      - max_private_time: 強制退出の上限時間
    """
    env = simpy.Environment()
    waiting_times_poop = []
    waiting_times_pee = []

    # 小便器リソースは、利用可能な場合のみ生成
    urinal = (
        simpy.Resource(env, capacity=sim_params["num_urinals"])
        if sim_params["num_urinals"] > 0
        else None
    )
    private_room = simpy.Resource(env, capacity=sim_params["num_private_rooms"])

    # 利用者到着プロセスを開始
    env.process(
        arrival_process(
            env, urinal, private_room, sim_params, waiting_times_poop, waiting_times_pee
        )
    )
    env.run(until=SIM_DURATION)

    total_waiting_times = waiting_times_poop + waiting_times_pee
    avg_wait = statistics.mean(total_waiting_times) if total_waiting_times else 0
    avg_wait_poop = statistics.mean(waiting_times_poop) if waiting_times_poop else 0
    avg_wait_pee = statistics.mean(waiting_times_pee) if waiting_times_pee else 0

    return avg_wait, avg_wait_poop, avg_wait_pee


def main():
    # 各シミュレーション条件の定義
    simulations = [
        {
            "name": "男子トイレ",
            "total_users_per_hour": 100,
            "num_private_rooms": 4,
            "num_urinals": 7,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 1.0,
            "private_extra_std": 1.0,
            "private_extra_min": 0.0,
            "max_private_time": 1e3,
        },
        {
            "name": "女子トイレ",
            "total_users_per_hour": 100,
            "num_private_rooms": 6,
            "num_urinals": 0,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 1.0,
            "private_extra_std": 1.0,
            "private_extra_min": 0.0,
            "max_private_time": 1e3,
        },
        {
            "name": "男子トイレ(スマホ禁止)",
            "total_users_per_hour": 100,
            "num_private_rooms": 4,
            "num_urinals": 7,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 0.0,
            "private_extra_std": 0.0,
            "private_extra_min": 0.0,
            "max_private_time": 1e3,
        },
        {
            "name": "女子トイレ(スマホ禁止)",
            "total_users_per_hour": 100,
            "num_private_rooms": 6,
            "num_urinals": 0,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 0.0,
            "private_extra_std": 0.0,
            "private_extra_min": 0.0,
            "max_private_time": 1e3,
        },
        {
            "name": "男子トイレ(平均排便時間 + 2σで退出)",
            "total_users_per_hour": 100,
            "num_private_rooms": 4,
            "num_urinals": 7,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 1.0,
            "private_extra_std": 1.0,
            "private_extra_min": 0.0,
            "max_private_time": 6.0 + 1.5 * 2,
        },
        {
            "name": "女子トイレ(平均排便時間 + 2σで退出)",
            "total_users_per_hour": 100,
            "num_private_rooms": 6,
            "num_urinals": 0,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 1.0,
            "private_extra_std": 1.0,
            "private_extra_min": 0.0,
            "max_private_time": 6.0 * 1.5 * 2,
        },
        {
            "name": "男女共用トイレ",
            "total_users_per_hour": 200,
            "num_private_rooms": 12,
            "num_urinals": 0,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 1.0,
            "private_extra_std": 1.0,
            "private_extra_min": 0.0,
            "max_private_time": 1e3,
        },
        {
            "name": "男女共用トイレ(平均排便時間 + 2σで退出)",
            "total_users_per_hour": 200,
            "num_private_rooms": 12,
            "num_urinals": 0,
            "defecation_rate": 0.25,
            "pee_mean": 1.0,
            "pee_std": 0.2,
            "pee_min": 0.3,
            "poop_mean": 6.0,
            "poop_std": 1.5,
            "poop_min": 1.0,
            "private_extra_mean": 1.0,
            "private_extra_std": 1.0,
            "private_extra_min": 0.0,
            "max_private_time": 6.0 + 1.5 * 2,
        },
    ]

    # 各シミュレーションの実行と結果の出力
    for sim in simulations:
        avg_wait, avg_wait_poop, avg_wait_pee = run_simulation(sim)

        # 待ち時間は分単位なので秒単位に変換
        total_wait_seconds = int(avg_wait * 60)
        poop_wait_seconds = int(avg_wait_poop * 60)
        pee_wait_seconds = int(avg_wait_pee * 60)

        print(
            f"{sim['name']} - 平均待ち時間: {total_wait_seconds}秒 "
            f"(大便: {poop_wait_seconds}秒, 小便: {pee_wait_seconds}秒)"
        )


if __name__ == "__main__":
    main()

```

## 実験結果
シミュレーションの結果、以下のような待ち時間が観測されました（単位：秒）。

|                                               | 男平均待ち時間[s]  | 女平均待ち時間[s] | 男女平均待ち時間[s] | 
| --------------------------------------------- | ------------------ | ----------------- | ------------------- | 
| オリジナル                                    | 27(大:110, 小:0) | 283               | 155      | 
| スマホ完全禁止                                | 10(大:43, 小:0)  | 13                | 11.5      | 
| 時間による強制退出案                   | 24(大:99, 小:0)  | 277               | 150.5    | 
| 男女共用トイレ                                | -                  | -                 | 128                 | 
| 男女共用トイレ+時間による強制退出案 | -                  | -                 | 107                 | 

※ 数値は設定したパラメータに基づく概算です。

## まとめ
今回のシミュレーションから、トイレの運用方法を変更するだけで待ち時間に大きな差が出ることが分かりました。特に、スマホ使用を禁止する対策は、個室を多く利用する女性トイレにおいて待ち時間を大幅に短縮する効果があることが確認されました。一方、男女共用や強制退出の対策は、スマホ禁止案ほどの効果は見られませんでした。

なお、使用したパラメータはあくまで概算のため、結果はあくまで傾向を示すものです。また、プライバシーに関する価値観は個人差があるため、どの対策が最適かを断定するものではありません。

## 参考文献
- ![なぜ女性トイレだけ行列？　706カ所調べてみたら…見えた男女格差](https://www.asahi.com/articles/AST1X0FS7T1XULLI00ZM.html)
- ![女性トイレだけ行列、なぜ？「男性便器は1.76倍」記者も調べると](https://withnews.jp/article/f0250212003qq000000000000000W0j410101qq000027772A)