0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

レジは1列に並ぶ方が速いのか? “レジ運”を待ち行列シミュレーションで検証してみた

0
Last updated at Posted at 2026-05-16

はじめに

日常の疑問をシミュレーションで考えるシリーズのまとめ」において、日常の行動を数理モデルを作って考えてきましたが、今回はスーパーやコンビニのレジ待ち時間についてシミュレーションを行いました。
記事もシミュレータも簡単な指示だけして、ほとんどLLMに書かせています。

スーパーやコンビニでレジに並ぶとき、

  • 「1列に並んで空いたレジへ進む方式」
  • 「各レジごとに列が分かれている方式」

があります。

体感的には、

「自分の列だけ異常に進まない」

という“レジ運”を感じることがあります。

そこで今回は、

「レジは1列に並ぶ方が本当に効率的なのか?」

を、Pythonで簡単な待ち行列シミュレーションを作って検証してみます。

今回は、

  • 客の到着
  • 会計時間
  • 長時間かかる「事故客」

などを乱数で生成し、モンテカルロ的に比較してみます。


モデル化

今回は次のような単純化したモデルを考えます。


客の到着

客はランダムに到着するとします。

到着間隔は指数分布で生成します。

平均30秒ごとに1人来店

としました。


会計時間

会計時間もランダムとします。

通常客は平均60秒程度で会計を終えます。

ただし現実には、

  • 小銭で時間がかかる
  • クーポンを使う
  • 公共料金支払い
  • 電子決済エラー

など、「長時間客」が存在します。

そこで今回は、

5%の確率で会計時間が5倍になる

ようにしました。


固定列方式

各レジごとに列が分かれている方式です。

客は、

「並んでいる人数」

だけを見て、一番短い列へ並びます。

重要なのは、

客は未来の会計時間を知らない

という点です。

つまり、

「1人しか並んでいないから速そう」

と思って並んだ結果、その客が長時間客だった、という状況が発生します。


1列方式

全員が共通の1列に並び、空いたレジへ順番に案内される方式です。

この方式では、

  • 特定レジへの偏り
  • 長時間客による事故

が全体へ平均化されます。


シミュレーション結果

実際に5000人分の客をシミュレーションしてみました。

条件:

  • レジ数: 3
  • 客到着平均: 30秒
  • 会計平均時間: 60秒
  • 長時間客: 5%

です。


結果例

================================
固定列方式
================================
平均待ち時間      : 124.30 sec
最大待ち時間      : 2399.67 sec
95パーセンタイル  : 543.68 sec

================================
1列方式
================================
平均待ち時間      : 106.32 sec
最大待ち時間      : 1129.23 sec
95パーセンタイル  : 443.86 sec

(乱数によって多少変動します)


ヒストグラム

待ち時間分布を比較すると、

  • 固定列方式
    → 長時間待ちが多い

  • 1列方式
    → 極端な待ちが減る

という傾向が見えます。

特に、

「最大待ち時間」

「95パーセンタイル」

の差が大きくなりました。

つまり、

「平均」よりも「ハズレの少なさ」

が、1列方式の大きなメリットと言えそうです。


まとめ

今回の簡単なシミュレーションでは、

  • 1列方式の方が待ち時間のばらつきが小さい
  • 「レジ運」が発生しにくい
  • 長時間客の影響を平均化できる

という結果になりました。

特に、

「会計時間のばらつき」

が大きいほど、1列方式が有利になるようです。

逆に、

  • 全員ほぼ同じ会計時間
  • セルフレジ中心

のような状況では、差は小さくなるかもしれません。


おわりに

今回はかなり単純化したモデルでしたが、

  • 列変更可能
  • 商品数を見て列選択
  • セルフレジ
  • 高齢者割合
  • 電子決済トラブル

などを追加すると、さらに現実っぽいシミュレーションができそうです。

「なんとなく感じる日常の不公平感」をシミュレーションで可視化すると、意外と面白いですね。

他の記事も興味があったら見てみてください。


コード

import numpy as np
import matplotlib.pyplot as plt
from collections import deque

# ==========================================
# パラメータ
# ==========================================

NUM_CUSTOMERS = 5000
NUM_REGISTERS = 3

ARRIVAL_MEAN = 30.0     # 客到着間隔平均 [sec]
SERVICE_MEAN = 60.0     # 会計時間平均 [sec]

HEAVY_PROB = 0.05       # 長時間客確率
HEAVY_SCALE = 5.0       # 長時間倍率

np.random.seed(0)

# ==========================================
# 客データ生成
# ==========================================

arrival_intervals = np.random.exponential(
    ARRIVAL_MEAN,
    NUM_CUSTOMERS
)

arrival_times = np.cumsum(arrival_intervals)

service_times = np.random.exponential(
    SERVICE_MEAN,
    NUM_CUSTOMERS
)

# 長時間客を混ぜる
heavy_mask = np.random.rand(NUM_CUSTOMERS) < HEAVY_PROB
service_times[heavy_mask] *= HEAVY_SCALE

# ==========================================
# 固定列方式
# ==========================================

def simulate_fixed_queue(arrivals, services, num_registers):

    # 各レジ:
    # dequeには「未来の終了時刻」が入る
    registers = [
        deque()
        for _ in range(num_registers)
    ]

    wait_times = []

    for arrival, service in zip(arrivals, services):

        # ----------------------------------
        # 過去終了分を削除
        # ----------------------------------

        for q in registers:

            while q and q[0] <= arrival:
                q.popleft()

        # ----------------------------------
        # 一番人数少ない列を選択
        # ----------------------------------

        queue_lengths = [
            len(q)
            for q in registers
        ]

        idx = np.argmin(queue_lengths)

        q = registers[idx]

        # ----------------------------------
        # 会計開始時刻
        # ----------------------------------

        if len(q) == 0:

            start_time = arrival

        else:

            last_finish = q[-1]

            start_time = max(
                arrival,
                last_finish
            )

        # ----------------------------------
        # 終了時刻
        # ----------------------------------

        finish_time = start_time + service

        q.append(finish_time)

        # ----------------------------------
        # 待ち時間記録
        # ----------------------------------

        wait_time = start_time - arrival

        wait_times.append(wait_time)

    return np.array(wait_times)

# ==========================================
# 1列方式
# ==========================================

def simulate_single_queue(arrivals, services, num_registers):

    # 各レジの終了予定時刻
    register_finish_times = np.zeros(num_registers)

    wait_times = []

    for arrival, service in zip(arrivals, services):

        # ----------------------------------
        # 最初に空くレジ
        # ----------------------------------

        idx = np.argmin(register_finish_times)

        # ----------------------------------
        # 会計開始
        # ----------------------------------

        start_time = max(
            arrival,
            register_finish_times[idx]
        )

        finish_time = start_time + service

        register_finish_times[idx] = finish_time

        # ----------------------------------
        # 待ち時間
        # ----------------------------------

        wait_time = start_time - arrival

        wait_times.append(wait_time)

    return np.array(wait_times)

# ==========================================
# 実行
# ==========================================

wait_fixed = simulate_fixed_queue(
    arrival_times,
    service_times,
    NUM_REGISTERS
)

wait_single = simulate_single_queue(
    arrival_times,
    service_times,
    NUM_REGISTERS
)

# ==========================================
# 結果表示
# ==========================================

print("================================")
print("固定列方式")
print("================================")

print(f"平均待ち時間      : {wait_fixed.mean():.2f} sec")
print(f"最大待ち時間      : {wait_fixed.max():.2f} sec")
print(f"95パーセンタイル  : {np.percentile(wait_fixed, 95):.2f} sec")

print()

print("================================")
print("1列方式")
print("================================")

print(f"平均待ち時間      : {wait_single.mean():.2f} sec")
print(f"最大待ち時間      : {wait_single.max():.2f} sec")
print(f"95パーセンタイル  : {np.percentile(wait_single, 95):.2f} sec")

# ==========================================
# ヒストグラム
# ==========================================

plt.figure(figsize=(10, 5))

plt.hist(
    wait_fixed,
    bins=60,
    alpha=0.5,
    label="Fixed Queue"
)

plt.hist(
    wait_single,
    bins=60,
    alpha=0.5,
    label="Single Queue"
)

plt.xlabel("Wait Time [sec]")
plt.ylabel("Count")

plt.title("Checkout Queue Simulation")

plt.legend()

plt.tight_layout()

plt.show()
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?