"""混んでいるときの「席は空いているのに待った」のずれを、スタッフの時間の内訳で比べる(2026-10-30) Unity:夜 20261106_busy の各回の timeline.csv(60秒ごとに、仕事ごとのスタッフの人数)と groups.csv、台帳の待ちの列 試算:haizen_sim.py(v2.8)を同じ条件で 40 回(試算と同じ種 0〜39)回し、スタッフの手がふさがった時間を仕事ごとに分けて数える (試算のコードは変えない。手がふさがった時間を足す所で、どの仕事かを見分けて別に足すだけ) 使い方: python3 staff_breakdown.py <出力フォルダ> """ import csv, glob, json, os, statistics, sys, collections SIM_DIR, UNITY_DIR, OUT = sys.argv[1], sys.argv[2], sys.argv[3] sys.path.insert(0, SIM_DIR) import haizen_sim as h JOBS = ["guide", "serve", "call", "pay", "clear", "rescue"] JA = {"guide": "案内", "serve": "配膳", "call": "呼び出し", "pay": "会計", "clear": "片付け", "rescue": "ロボット救出"} # ---- 試算:仕事ごとの手がふさがった時間 class S(h.Store): def __init__(self, *a, **k): object.__setattr__(self, "by_job", collections.Counter()) super().__init__(*a, **k) def __setattr__(self, name, val): if name == "staff_busy_s" and hasattr(self, "staff_busy_s"): import inspect fn = inspect.currentframe().f_back.f_code.co_name add = val - self.staff_busy_s if fn == "group": job = "guide" elif fn == "_staff_carry": job = "serve" elif fn == "bus": job = "clear" elif fn == "staff_job": f = inspect.currentframe().f_back work = f.f_locals.get("work_s") job = "pay" if work == self.p["pay_s"] and f.f_locals.get("walk_m") == 0 else ( "call" if work == self.p["call_s"] else "clear") elif fn == "maybe_stop": job = "rescue" else: job = "other:" + fn self.by_job[job] += add object.__setattr__(self, name, val) def site_run(cond, seed): store = h.STORES[cond["store"]] p = {**h.COMMON, **store, **cond} env = h.simpy.Environment() s = S(env, h.random.Random(seed), p) env.process(s.arrivals()) env.run() n = len(s.entry_waits) return {"groups": n, "end_min": env.now / 60, "per_group_min": {j: s.by_job.get(j, 0) / 60 / n for j in JOBS}, "util": s.staff_busy_s / (p["staff"] * env.now), "blocked": statistics.mean(s.blocked_waits), "guide_wait": statistics.mean(s.guide_waits), "entry": statistics.mean(s.entry_waits), "other": {k: v for k, v in s.by_job.items() if k.startswith("other")}} # ---- Unity:夜の各回 def unity_run(folder, staff_n): tl = list(csv.DictReader(open(os.path.join(folder, "timeline.csv"), encoding="utf-8"))) gr = [g for g in csv.DictReader(open(os.path.join(folder, "groups.csv"), encoding="utf-8")) if g["state"] == "Done"] dt = 60.0 n = len(gr) sec = {j: sum(float(r.get("staff_" + j, 0) or 0) for r in tl) * dt for j in JOBS} idle = sum(float(r["staff_idle"]) for r in tl) * dt busy = sum(sec.values()) dirty_untaken = statistics.mean(float(r["tables_dirty_untaken"]) for r in tl) return {"groups": n, "per_group_min": {j: sec[j] / 60 / n for j in JOBS}, "util": busy / (busy + idle) if busy + idle else 0, "dirty_untaken_mean": dirty_untaken, "end_min": float(tl[-1]["t_s"]) / 60} ledger = {r["run_folder"]: r for r in csv.DictReader(open(os.path.join(UNITY_DIR, "unity_ledger_v1.csv"), encoding="utf-8"))} def med(xs): return statistics.median(xs) if xs else float("nan") params = json.load(open(os.path.join(SIM_DIR, "serving-robot-params.json"), encoding="utf-8")) rows = [] for c in params["unity_conditions"][18:]: cond = c["condition"] if cond["arrivals_per_h"] != h.STORES[cond["store"]]["arrivals"][2]: continue # 混んでいるだけ pat = os.path.join(UNITY_DIR, f"20261106_night_a{cond['arrivals_per_h']}_cs44_{cond['store']}_r{cond['robots']}_s*_passon_sbret") us = [] for f in sorted(glob.glob(pat)): u = unity_run(f, cond["staff"]) L = ledger.get(os.path.basename(f), {}) u["blocked"] = float(L.get("blocked_wait_mean") or "nan") u["staff_wait"] = float(L.get("staff_wait_mean") or "nan") u["entry"] = float(L.get("entry_wait_mean") or "nan") us.append(u) ss = [site_run(cond, seed) for seed in range(h.REPS)] row = {"store": cond["store"], "label": h.STORES[cond["store"]]["label"], "staff": cond["staff"], "robots": cond["robots"], "arrivals": cond["arrivals_per_h"], "n_unity": len(us)} for side, xs in (("unity", us), ("site", ss)): row[side] = { "groups": med([x["groups"] for x in xs]), "util": med([x["util"] for x in xs]), "per_group_min": {j: med([x["per_group_min"][j] for x in xs]) for j in JOBS}, "total_per_group_min": med([sum(x["per_group_min"].values()) for x in xs]), "entry": med([x["entry"] for x in xs]), "blocked": med([x["blocked"] for x in xs]), } row["unity"]["staff_wait"] = med([x["staff_wait"] for x in us]) row["unity"]["dirty_untaken_mean"] = med([x["dirty_untaken_mean"] for x in us]) row["site"]["guide_wait"] = med([x["guide_wait"] for x in ss]) row["site_other"] = sorted({k for x in ss for k in x["other"]}) rows.append(row) print(row["label"], cond["robots"], "台 done", flush=True) os.makedirs(OUT, exist_ok=True) json.dump(rows, open(os.path.join(OUT, "staff_breakdown.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1) with open(os.path.join(OUT, "staff_breakdown.txt"), "w", encoding="utf-8") as fo: fo.write("混んでいるとき:1組あたりのスタッフの手がふさがった時間(分、中央値。Unity 種10個/試算 40回)\n") fo.write("店 台数 | 組数 U/S | 稼働率 U/S | " + " | ".join(JA[j] + " U/S" for j in JOBS) + " | 合計 U/S | 入店待ち U/S | 片付け待ち U/S\n") for r in rows: u, s = r["unity"], r["site"] cells = [f"{u['per_group_min'][j]:.2f}/{s['per_group_min'][j]:.2f}" for j in JOBS] fo.write(f"{r['label']} {r['robots']}台 | {u['groups']:.0f}/{s['groups']:.0f} | {u['util']:.2f}/{s['util']:.2f} | " + " | ".join(cells) + f" | {u['total_per_group_min']:.2f}/{s['total_per_group_min']:.2f} | " f"{u['entry']:.2f}/{s['entry']:.2f} | {u['blocked']:.2f}/{s['blocked']:.2f}\n") print("wrote", OUT)