混むと片付けが詰まる理由を、Unity のスタッフの仕事の選び方から読んで調べる（読むだけ・2026-10-30 08:23〜08:56・やり直し）

■ まとめ
・仕事を選ぶ決まり（優先0／1／2、同じ優先なら先に頼まれた順、持ち場なし、途中で割り込まれない）は、Unity と試算で同じ。
・ずれは主に二つから来ている。どちらも「片付けの仕事が入ってくる速さ」と「片付けが済む速さ」の差になる。
  (ア) 会計の頼みを出す時刻が違う（大）。試算はレジが空いてからスタッフを頼むので、ほかの優先1の仕事の後ろに並び、
       そのあいだレジが止まる（レジを持っていた時間のうち、会計していたのは ③ 88%）。Unity は会計の人がレジの並ぶ点に
       着いた時に板に出すので、板の優先1の中で古くなり、前の会計が終わるとすぐ取られる（会計のスタッフ 0.97 人＝ほぼいつも1人）。
       その分、Unity は会計が速く回る（③ 60〜160分 38.7 組/時、試算 34.8）→ 片付けの仕事も速く入ってくる。
  (イ) 片付け1回が Unity のほうが長い（中）。③ 135 秒、試算 123 秒。片付けに付く人数はほぼ同じ（③ 1.17〜1.26 人／試算 1.15〜1.22 人）
       なので、済む速さは Unity が遅い（③ 32.4 卓/時、試算 35.1）。
  ③で、入ってくる − 済む ＝ Unity 1時間に＋6.3 卓、試算 −0.3 卓。60〜160分の100分で Unity は約10卓たまる。記録の汚れた卓（12前後）と合う。
  試算では、たまる分が「会計を卓で待つ」（pay_wait 15 分）に出て、卓はふさがったまま（＝満席で待つ）。
  Unity では、たまる分が「汚れた卓」に出る（片付け待ち）。来た組が待つ長さは同じくらいでも、待ちの名前が入れ替わる。
・② 定番の郊外型は、Unity も試算も入ってくる−済むが 0 に近く（0.3／0.6）、ずれが小さい（358 秒／192 秒）のと合う。

■ 始める前の確かめ（08:20 ごろ）
・ServiceSim の保留：0（cm status。/main@ServiceSim cs:45 head、保留の行なし）
・台帳 /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv：1032 行・md5 52e433713d4f7d41dfa8ccfb889492cd（最後に書かれたのは 10/30 06:04。07:38 の作業より前）
・unity_ledger_v1.lock：なし
・Assets・tools・ProjectSettings・Packages に 10/30 06:10 より後に変わったファイル：なし
・前の作業の途中のファイル（20261030_0744_staff_job_choice_files/）：compare_timeline.py と空の compare_timeline.txt が残っていた。
  → 前の作業で変わったファイルはないと判断して始めた。

■ 前の作業（07:44 の compare_timeline.py）について
・今回も同じものを走らせたら、10 分たっても何も出さずに止められた。
・理由：試算の window() を走らせていない。試算の「全員が帰った」の合図（finished）は window() の中で出るので、
  合図が来ないまま、1分ごとに数える sampler が回り続ける（試算の run_once は window() を走らせている）。
  07:44 の作業が空の txt のまま止まっていたのも、Mac の再起動の前にこれで回り続けていたためと思われる。
・07:44 のファイルはそのまま残し、window() を足した写しを今回のフォルダに置いて走らせた（2.5 秒で終わった）。
・試算を走らせる simpy はこの Mac の python に入っていなかった。uv の一時の環境（uv run --no-project --with simpy、
  入るのは ~/.cache/uv の中だけ）で走らせた。プロジェクト・設定は変えていない。

■ 1. スタッフの仕事の選び方
読んだところ：
  Assets/ServiceSim/People/StaffAgent.cs 7-16（決まりの説明）、92-101（待つ点）、143-198（Update）、201-216（Idle）、
    233-291（Begin：仕事ごとの行き先）、467-503（GiveUpIfLate）
  Assets/ServiceSim/Service/ServiceOps.cs 77-110（StaffJob）、1658-1686（PostJob／PostJobFront）、1692-1713（TakeNextJob）、
    1716-1747（FinishJob／AbandonJob）、1266-1276（PostRescue）、1594-1647（戻された皿を板の先頭に出す）
  Assets/ServiceSim/Service/GroupManager.cs 411-434（案内を出す）、536（呼び出し）、577-587（片付けを出す）、684・710（会計を出す）
  haizen_sim.py（v2.8）279-288（staff_job）、414-458（bus）、474-543（group）
答え：
・優先：0＝立ち往生のロボットを動かす、1＝案内・料理・呼び出し・会計、2＝片付け。試算と同じ（StaffJob の priority。
  ServiceOps.cs 78、GroupManager.cs 432・536・584・684・710）。
・同じ優先の中の順番：板（_jobs）の並び順＝頼まれた順。近い順ではない（TakeNextJob 1692-1713 は「優先が小さいもの、
  同じなら前のもの」）。例外は二つ：立ち往生は板の先頭に入れる（PostRescue 1272）、ロボットから戻された料理も先頭に入れる
  （PostJobFront 1647）。料理は誰かが持った・届いたものは飛ばす。会計は、レジの内側に誰かがいる間は飛ばす（RegisterBackBusy、1700）。
・どこで待つか：手が空いたら、スタッフごとに1つ決まった「スタッフが待つ点」へ歩く（StaffAgent.cs 92-101、210）。③は
  受け渡し口の横の6点（x 13.75、z 6.8〜9.8）。待つ点へ歩いている途中でも、毎コマ板を見て仕事を取る（Idle 201-216）。
・どこから仕事を取るか：場所を見ない。いまいる所から、その仕事の最初の行き先へ歩き出す。持ち場はない。決まった人だけの仕事もない
  （会計は1人ずつ、というだけ）。
・仕事の終わる場所が試算と違う：試算はどの仕事も受け渡し口から出て戻る（往復）。Unity は戻らない。
  案内は卓で、呼び出しは卓で、片付けは卓で（最後に拭くため）、料理は受け渡し口で、会計はレジの内側で終わる。
  次の仕事はそこから歩く（StaffAgent.cs 243-286）。
・割り込み：ない。取った仕事は終わるまで続ける。止まるのは二つだけ：ロボットをよけている間（182）、1つの行き先に 60 秒着けない
  ときにあきらめて板に戻す（3回目はその場所へ移して続ける。467-503）。③0台の種0では、スタッフのあきらめ・移したは 0 件。

■ 2. 片付けの中身
読んだところ：GroupManager.cs 723-735（OnPaid）、577-587（Left で片付けを出す）、749-755（OnCleared）、
  StaffAgent.cs 267-278、ServiceOps.cs 1208-1215（ReleaseTable）、SiteValues.cs 111-112・136-137、ServiceSpec.cs 118-124、
  Assets/ServiceSim/Store/Layouts/p1_efficient_corner.json の places
答え：
・頼む人・時：会計が済んだ時（OnPaid で left_s を入れ、状態を Left にする）。次のコマに GroupManager が片付けの仕事を
  優先2で板に出す（584）。客が歩いて店を出るのを待たない。試算も会計の staff_job が終わったらすぐ dirty を1足して bus に入る（534-536）。同じ。
・歩く道（bus_trips＝2 のとき）：いた所 → 卓（clear_s 40 秒）→ 返却口 → 卓 → 返却口 → 卓（wipe_s 40 秒）で終わり。
  歩くのは「いた所 → 卓」と、卓と返却口のあいだを4本。試算は 受け渡し口と卓のあいだを4本（2 ×（d＋dish_extra_m）× bus_trips）。
  Unity は最初の1本が多く、最後に戻る1本がない。③は返却口と受け渡し口が同じ場所（rect [14.3, 6.5, 16.3, 6.9]）。
  なお StaffAgent.cs 268 の説明文は「返却口へ運び、戻って拭いて、もう一度運ぶ」だが、コードは「運ぶ→戻る→運ぶ→戻って拭く」。
・往復の回数・時間：params の clear_s・wipe_s（サイトの決まり）と、店の bus_trips（family＝2）を使っている（SiteValues.cs 111-112・136-137）。
・洗い場：返却口はただの点。待つことも、置ける量の上限もない。試算も同じ（dish_extra_m＝0、待ちなし）。
・1回の時間（スタッフの手がふさがった秒、中央値）：③ Unity 135／試算 123、⑤ 141／130、⑥ 131／124、② 123／114。

■ 3. 数え方
読んだところ：GroupManager.cs 322-343（CountBlockedWait）、450-465（OnGuided）、ServiceOps.cs 545-565（RecordJobWait）・
  1226-1243（RecordEntryWait）・1249-1258（ClearingTableCount）、TimelineLogger.cs 12・118-145・168-183・199-213、
  LedgerRow.cs 227-238、params の meta.metrics、haizen_sim.py 241-248（_acc）・485-504
答え：
・clear_wait_mean_s（Unity だけの欄。params の meta.metrics には無い）：片付けの仕事が板に出た時刻（postedS＝会計が済んだ次のコマ）から、
  スタッフが取った時刻まで。取られるたびに1回数え、あきらめて戻った仕事は取り直すたびに、最初に出た時刻からもう一度数える。
  卓を空けて板から外した仕事は入らない。慣らしの組も外さず、店が開いてから全員が帰るまでの全部。
  比べた試算の 20 秒は、staff_breakdown の clear_wait_check.py が優先2の staff_job の「頼んでから手が空くまで」を数えたもの。0台では同じものを数えている。
・blocked_wait_mean：毎コマ、片付け待ちの卓（客が帰ってから拭き終わるまで。clearing）の数だけ、入口の列の前から dt を足す。
  卓に着いた時（案内が済んだ時）に1組ずつ残す。試算の _acc と同じ数え方（列の前から、dirty の数まで）。
  ちがいは一つ：Unity は 2人・4人・6人席があり、列の先頭が入れない卓だと飛ばして次の組を座らせるが、blocked は席の大きさを見ずに前から数える（322-335 の注にあるとおり）。
・staff_wait_mean：blocked_wait ＋ guide_wait（案内の仕事が板に出てからスタッフが取るまで）。meta.metrics の「席は空いているのに片付け・案内が追いつかず待った時間」と同じ。
・turn_mean（meta.metrics「客が帰ってから卓が使えるまで」）：Unity は ReleaseTable の時刻 − left_s。試算は bus の終わり − 始まり。同じ。
・timeline の tables_dirty_untaken／tables_clearing：60 秒ごとのその瞬間の数（平均にしない）。clearing の卓を、その卓の片付けの仕事が
  取られていれば tables_clearing、まだなら tables_dirty_untaken に入れる。0 秒から全員が帰るまで。サイトの指標ではない。
  依頼の表の「汚れた卓の平均の数」は、Unity が timeline の行の平均、試算が dirty の時間の重みつき平均（どちらも全部の時間）。
・まとめ：数え方の違いで 3〜6 倍は出ない。

■ 4. 記録で追う（③ 混んでいる・0台・種0：20261106_night_a54_cs44_fam_p1_r0_s0_passon_sbret）
記録に「取った時刻」「終わった時刻」は無いので、割り出した（やり方は trace_clear.py の頭に書いた）：
  ・片付けはみな優先2で、出た順に取られる。この回にスタッフのあきらめ・卓を空けたは 0 件なので、順番は崩れていない。
    → t までに取られた数 ＝ t までに頼まれた数 − tables_dirty_untaken(t)。60 秒ごとの幅で出る。
  ・終わった時刻は、同じ卓の次の組の案内が始まった時刻を上の端にした（入口で待つ組がいれば、卓が空いたコマに案内が始まる）。
  ・確かめ：割り出した待ちの平均 555 秒、summary.json の片付けの job_wait_mean_s 548 秒。
時刻は店が開いてからの分：
  卓  | 席 | 組 | 客が帰った（会計済み）| スタッフが取った | 終わった（次の組の案内が始まった。上の端）| そのとき入口で待つ組 | 帰ってから取るまで | 取ってから終わりまで
  T31 | 6 | 42 | 111.0 | 131.0〜132.0 | 133.1 | 2 | 20.0〜21.0分 | 1.1〜2.1分
  T29 | 4 | 38 | 105.0 | 125.0〜126.0 | 127.0 | 7 | 20.0〜21.0分 | 1.0〜2.0分
  T36 | 2 | 41 | 115.5 | 134.0〜135.0 | 分からない（この卓に次の組は来なかった） | - | 18.5〜19.5分 | 分からない
  T01 | 4 | 45 | 112.5 | 131.0〜132.0 | 133.7 | 2 | 18.5〜19.5分 | 1.7〜2.7分
  T30 | 4 | 39 | 106.5 | 125.0〜126.0 | 128.0 | 7 | 18.5〜19.5分 | 2.0〜3.0分
そのあいだスタッフ4人がしていたこと：
  ・1人ずつの動きは記録に無い（timeline は仕事ごとの人数だけ。StaffAgent に1人ずつの記録はない）。分からない。
  ・人数で見ると（帰ってから取られるまでの行の平均）：待機 0.00、案内 0.55〜0.70、料理 1.10〜1.15、呼び出し 0.15〜0.30、
    会計 0.90〜0.95、片付け 1.05〜1.15。板には優先1の仕事（会計を除く）が 1.4〜1.9、片付けが 11.5〜12.0 残っていた。
  ・つまり4人とも手がふさがっていて、1人はほぼいつもレジ。残り3人のうち約2人が優先1を、約1人が片付けをしていた。
    片付けは、板に優先1が無くなった瞬間にまとめて取られている（例：115〜116分に3人、127分に4人、132分に3人が片付け）。
  ・T31 の1分ごとの表は trace_clear.txt にある。前に頼まれてまだ取られていない片付けが 10 → 0 になるまで 16 分かかった。

■ 5. ロボットがいるとスタッフの仕事が増える理由（④ 効率重視・小さめの店・混んでいる・2台、種10個の中央値）
読んだところ：StaffAgent.cs 147-153・182・467-503、PersonRobotYield.cs 100-125、PeopleSpec.cs 205-222、
  ServingRobotController.cs 1765-1785（EnterStuck → PostRescue）、各回の summary.json・give_ups.csv・yields.csv
・Unity のスタッフの時間は「仕事を持っている間」を仕事の名前ごとに足す。よけて止まった時間・あきらめるまでの 60 秒も、その仕事の時間に入る。
・案内1回：0台 68.5 秒 → 2台 93.2 秒（＋24.7。試算は 0台も2台も 1.03 分＝約 62 秒のまま）
    あきらめるまでの 60 秒：＋11.9 秒（1組あたり 0.20 回あきらめた。0.05 回は3回目で移した）
    ロボットをよけて止まった：＋4.1 秒（按分。どの仕事のときかは記録に無いので、歩いている時間の割合で分けた）
    そのほか：＋8.7 秒（あきらめたあと最初から歩き直す分・遠回り・人どうしのつかえ・ゆっくり歩く。記録からは分けられない）
・片付け1回：0台 122.9 秒 → 2台 148.5 秒（＋25.6。試算は 1.89〜1.91 分のまま）
    あきらめるまでの 60 秒：＋11.8 秒（0.20 回/組）、よけて止まった（按分）：＋5.3 秒、そのほか：＋8.5 秒
・案内・片付けの外で増える分：
    ロボットを動かしに行く（立ち往生）：0 → 30.6 秒/組（1組あたり 0.84 回。試算は 0.00 分）。4つの中でいちばん大きい。
    2分待って代わりに運ぶ：0.29 皿/組。これは料理の時間に入り、案内・片付けには入らない。
    スタッフがよけて止まった秒の合計：18.0 秒/組（仕事を持っている時間の 3.8%）
    人のつかえ（客も入る）：0.6 → 38.6 秒/組。ほとんどがロボットに道をふさがれたつかえ（種0で 98%）
    スタッフが歩いた距離：127 → 98 m/組（料理をロボットが運ぶ分へる）。遠回りは、距離の合計からは見えない。
・③ 2台（参考）：案内 ＋5.2 秒、片付け ＋27.5 秒（あきらめ 9.4・よけ 13.7（按分）・そのほか 4.4）、立ち往生 3.7 秒/組。

■ 試算との違いの表
項目 | 試算（v2.8） | Unity | 片付け待ちのずれに効いていそうか
会計のスタッフを頼む時刻 | レジが空いてから（ほかの優先1の後ろに並ぶ。レジが止まる時間がある：③ 12%） | レジの並ぶ点に着いた時に板に出す（古い順で前にいるので、前の会計が終わるとすぐ取られる。レジの並ぶ点は3つ） | 大（③ 会計 38.7／34.8 組/時。片付けが入ってくる速さの差 3.9 卓/時）
片付け1回の道 | 受け渡し口⇔卓を4本 | いた所→卓＋卓⇔返却口を4本、卓で終わる | 中（③ 135／123 秒。済む速さの差 2.7 卓/時）
仕事を始める・終える場所 | 毎回 受け渡し口から出て戻る | 前の仕事が終わった所から歩く。戻らない | 中（上の片付けの差と、案内の差に入っている）
案内の道 | 受け渡し口→卓（着いたら席に着く）→説明45秒→戻る | いた所→待合（45秒）→卓 | 小（③ 1.21／1.11 分）
会計の道 | 歩かない | いた所→レジの内側 | 小（93／90 秒）
優先順・同じ優先の順番 | 0／1／2、先に頼まれた順 | 同じ（立ち往生と戻された料理は先頭） | 小（0台では違いなし）
持ち場・待つ点 | 場所なし | 待つ点が1人に1つ。持ち場なし | 小
割り込み | なし | なし（あきらめ60秒で板に戻す。③0台は0件） | 小（0台）
洗い場 | 上限・待ちなし | 上限・待ちなし | 小（違いなし）
卓の大きさ | 無い | 2・4・6人席。入れない組は飛ばす | 分からない
数え方（片付け待ち・blocked） | _acc | 同じ形。clear_wait は取り直しも数える | 小
ロボットがいるときの人のよけ・あきらめ・立ち往生 | 入っていない（stuck_rate 0.01 だけ） | 入る（④2台で案内＋25秒・片付け＋26秒・立ち往生 31 秒/組） | ロボットありの条件で大

■ 直すとしたら考えられる形（並べるだけ。どれを選ぶかは書かない）
サイトの試算の側に足すもの：
  ・会計のスタッフを、レジの並ぶ点に着いた時（レジを取る前）に頼む形にする。または、会計の頼みだけ先に頼まれた順を前にする
  ・スタッフの居場所を持たせ、次の仕事は前の仕事が終わった所から歩く（戻りの1本を入れない）
  ・片付けの道を「卓まで1本＋卓⇔返却口を 2×bus_trips 本」にする
  ・案内を「待合へ迎えに行く→卓」の道にする。会計にレジまで歩く時間を足す
  ・洗い場（返却口）の受け入れの上限や置ける量、待ちを足す（今はどちらにも無い。足すなら両方に要る）
  ・ロボットがいるときの、スタッフのよけ・あきらめ・歩き直し・立ち往生の時間を unity_measures として戻して入れる
  ・片付けの優先を、待った時間で上げる（待つほど前に出る）などの決まりを足す（サイトの決まりを変えることになる）
Unity を試算の決まりに合わせるもの：
  ・会計の仕事を板に出すのを、レジの内側が空いてからにする（＝試算と同じく、ほかの優先1の後ろに並ぶ）
  ・どの仕事も終わったら受け渡し口（または待つ点）に戻ってから次を取る
  ・片付けを「受け渡し口⇔卓を 2×bus_trips 本」の道にし、受け渡し口で終える
  ・案内を「受け渡し口→卓→説明45秒→戻る」の形にする（待合に迎えに行かない）
  ・会計のスタッフをレジの内側から動かさない（歩きを 0 にする）
  ・clear_wait_mean_s を、取り直しを数えない形にそろえる（0台ではもともと同じ）

■ 置いたファイル（どれも claude_code_reports/ の中だけ）
  20261030_0823_staff_job_choice.txt（この報告）
  20261030_0823_staff_job_choice_files/
    compare_timeline.py（07:44 の版に window() を足した写し）／compare_timeline.txt（その出力）
    pay_flow_check.py／pay_flow_check.txt（会計と片付けの流れ）
    trace_clear.py／trace_clear.txt（4 の卓の時刻）
    robot_staff_extra.py／robot_staff_extra.txt（5 の ④）、robot_staff_extra_p1.txt（同じスクリプトで ③）
  07:44 のフォルダ（20261030_0744_staff_job_choice_files/）は消していない。中の compare_timeline.txt は、今回の1回目の
  走らせ（止められた）で、空のまま書き直された。

■ 変えたファイルがないこと（終わりの確かめ 08:56）
  ・ServiceSim の保留：0（cm status、cs:45 head、保留の行なし）。始めと同じ
  ・台帳：1032 行・md5 52e433713d4f7d41dfa8ccfb889492cd。始めと同じ
  ・Assets・tools・ProjectSettings・Packages：10/30 06:10 より後に変わったファイルなし
  ・Unity エディタは開いていない。新しく回していない。check-in していない
  ・一つだけ、報告の場所の外にファイルができた：simulation/v2_8/__pycache__/haizen_sim.cpython-312.pyc（08:23）。
    1回目の走らせで python -I を使ったため、PYTHONDONTWRITEBYTECODE が効かず、試算を読み込んだ時に python が自動で書いた
    キャッシュ。haizen_sim.py そのものは変わっていない（10/25 08:58 のまま）。前からあったかは分からないので、消していない。
    2回目からは -B をつけて書かせていない。

■ 書いた集計のスクリプトの全文

----- pay_flow_check.py（置いた場所：claude_code_reports/20261030_0823_staff_job_choice_files/pay_flow_check.py）-----
"""会計の流れが、片付けの仕事の入ってくる速さを決めているかを見る（2026-10-30・読むだけ）
片付けは会計が済むと頼まれる。会計が速く回れば、片付けの仕事も速く入ってくる。
  Unity：会計の仕事は、会計の人がレジの並ぶ点に着いた時に板に出る（GroupManager.SendPayerIfPossible）。
         前の会計が終わるより前に出ているので、板の優先1の中で古く、前の会計が終わるとすぐ取られる
  試算：会計のスタッフの頼みは、レジが空いてから出す（haizen_sim.py group の register.request のあと staff_job）。
         ほかの優先1の頼みの後ろに並ぶので、そのあいだレジは止まる
試算は中を変えず、staff_job を外からくるんで数えるだけ。Unity は groups.csv・timeline.csv を読むだけ。
使い方: python3 pay_flow_check.py <haizen_sim.py のフォルダ> <service_sim/unity のフォルダ>
"""
import csv, glob, os, statistics, sys
SIM_DIR, UNITY_DIR = sys.argv[1], sys.argv[2]
sys.path.insert(0, SIM_DIR)
import haizen_sim as h

W0, W1 = 60 * 60, 160 * 60      # 混んでいる時間（60〜160分）で数える
med = statistics.median


class S(h.Store):
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self.pay_wait_staff = []    # レジを取ってから、会計のスタッフが付くまで（秒）
        self.pay_done = []          # 会計が済んだ時刻
        self.clear_done = []        # 片付けが済んだ時刻
        self.reg_busy = 0.0         # W0〜W1 にレジを持っていた秒（スタッフ待ちも入る）
        self.reg_paying = 0.0       # そのうち、スタッフが付いて会計していた秒

    def staff_job(self, priority, walk_m, work_s, waited=None):
        is_pay = priority == 1 and walk_m == 0 and work_s == self.p["pay_s"]
        if not is_pay:
            yield from super().staff_job(priority, walk_m, work_s, waited)
            if priority == 2:
                self.clear_done.append(self.env.now)
            return
        w = []
        yield from super().staff_job(priority, walk_m, work_s, w)
        now = self.env.now
        t_reg = now - work_s - w[0]
        self.pay_wait_staff.append(w[0])
        self.pay_done.append(now)
        self.reg_busy += max(0.0, min(now, W1) - max(t_reg, W0))
        self.reg_paying += max(0.0, min(now, W1) - max(now - work_s, W0))


def rate(ts):
    return sum(1 for t in ts if W0 <= t < W1) / ((W1 - W0) / 3600)


print("混んでいる・0台。60〜160分のあいだで数える（Unity 種10個・試算 40回の中央値）")
print("店 | 会計が済んだ組/時 U/S | 片付けが済んだ卓/時 U/S | 入ってきた−済んだ 卓/時 U/S | "
      "レジで会計のスタッフが付くまで（秒）U/S | レジを持っていた時間のうち会計していた割合 S | Unity 会計のスタッフ人数の平均")
for st in ["fam_p1", "fam_p3", "fam_p4", "fam_p0"]:
    s = h.STORES[st]
    arr, staff = s["arrivals"][2], s["staff"][1]
    cond = {"store": st, "staff": staff, "robots": 0, "arrivals_per_h": arr, "mode": "serve",
            "trays": 1, "fragile_by": "any", "registers": 1}
    sp, sc, sw, sr = [], [], [], []
    for seed in range(h.REPS):
        p = {**h.COMMON, **s, **cond}
        env = h.simpy.Environment()
        x = S(env, h.random.Random(seed), p)
        env.process(x.arrivals())
        env.process(x.window())
        env.run()
        sp.append(rate(x.pay_done)); sc.append(rate(x.clear_done))
        sw.append(statistics.mean(x.pay_wait_staff)); sr.append(x.reg_paying / max(1.0, x.reg_busy))
    up, uc, uw, us = [], [], [], []
    for f in sorted(glob.glob(os.path.join(UNITY_DIR, f"20261106_night_a{arr}_cs44_{st}_r0_s*_passon_sbret"))):
        g = list(csv.DictReader(open(os.path.join(f, "groups.csv"), encoding="utf-8")))
        tl = list(csv.DictReader(open(os.path.join(f, "timeline.csv"), encoding="utf-8")))
        pays = [float(r["pay_s"]) for r in g if r["pay_s"] not in ("", "-1")]
        up.append(rate(pays))
        # 片付けが済んだ数 ＝ 頼まれた数 − 汚れた卓（未着手＋片付け中）の増え方
        def dirty_at(t):
            prev = [r for r in tl if float(r["t_s"]) <= t]
            r = prev[-1]
            return int(r["tables_dirty_untaken"]) + int(r["tables_clearing"])
        posted = sum(1 for t in pays if W0 < t <= W1)
        uc.append((posted - (dirty_at(W1) - dirty_at(W0))) / ((W1 - W0) / 3600))
        rows = [r for r in tl if W0 <= float(r["t_s"]) < W1]
        us.append(statistics.mean(int(r["staff_pay"]) for r in rows))
    # Unity の「会計の仕事が板に出てから取られるまで」は台帳の pay_job_wait_mean_s
    L = {r["run_folder"]: r for r in csv.DictReader(open(os.path.join(UNITY_DIR, "unity_ledger_v1.csv"), encoding="utf-8"))}
    uw = [float(L[os.path.basename(f)]["pay_job_wait_mean_s"]) for f in
          sorted(glob.glob(os.path.join(UNITY_DIR, f"20261106_night_a{arr}_cs44_{st}_r0_s*_passon_sbret")))]
    print(f"{s['label']} | {med(up):.1f}/{med(sp):.1f} | {med(uc):.1f}/{med(sc):.1f} | "
          f"{med(up)-med(uc):.1f}/{med(sp)-med(sc):.1f} | {med(uw):.0f}（板に出てから。並ぶ点に着いた時から）/{med(sw):.0f}（レジを取ってから） | "
          f"{med(sr):.2f} | {med(us):.2f}")

----- trace_clear.py（置いた場所：claude_code_reports/20261030_0823_staff_job_choice_files/trace_clear.py）-----
"""③混んでいる0台の1回で、片付けを長く待った卓を追う（2026-10-30・読むだけ）
今までの groups.csv・timeline.csv・give_ups.csv を読むだけ。何も書き換えない。

記録に「片付けをスタッフが取った時刻」「終わった時刻」は無いので、次のように割り出す。
  頼んだ時刻   … groups.csv の left_s（会計が済んだ時刻。GroupManager.OnPaid で left_s を入れ、
                 次のコマで片付けの仕事を板に出す）
  取った時刻   … 片付けの仕事はみな優先2で、板の後ろに足され（ServiceOps.PostJob）、
                 同じ優先の中では出た順に取られる（ServiceOps.TakeNextJob）。
                 あきらめて板に戻った片付けも、卓を空けた片付けも、この回には無い（give_ups.csv で確かめる）。
                 だから t の時点で取られた片付けの数 ＝ t までに頼まれた数 − tables_dirty_untaken(t)。
                 k 番目に頼まれた片付けは、その数が k 以上になった最初の行（60秒ごと）までに取られた。
                 → 「前の行の時刻 〜 その行の時刻」の幅で出す
  終わった時刻 … 卓が空いたら、待っている組がいればそのコマで案内が始まる（GroupManager.SeatWaiting）。
                 だから同じ卓の次の組の guide_start_s が上の端。そのとき入口で待つ組がいれば、ほぼ終わった時刻そのもの
スタッフ4人それぞれの動きは記録に無い。timeline.csv の、仕事ごとのスタッフの人数（60秒ごと）を並べる。

使い方: python3 trace_clear.py <回のフォルダ> [卓の数=5]
"""
import csv, os, sys

RUN = sys.argv[1]
TOP = int(sys.argv[2]) if len(sys.argv) > 2 else 5

groups = list(csv.DictReader(open(os.path.join(RUN, "groups.csv"), encoding="utf-8")))
tl = list(csv.DictReader(open(os.path.join(RUN, "timeline.csv"), encoding="utf-8")))
gu = list(csv.DictReader(open(os.path.join(RUN, "give_ups.csv"), encoding="utf-8")))

staff_gu = [r for r in gu if r["who"] == "スタッフ"]
freed = [r for r in gu if r["action"] == "卓を空けた"]
print(f"回: {os.path.basename(RUN)}")
print(f"スタッフのあきらめ・移した: {len(staff_gu)} 件　卓を空けた: {len(freed)} 件"
      "（どちらも 0 なら、片付けは頼まれた順に取られたと見てよい）")

F = lambda x: float(x) if x not in ("", None) else -1.0
for g in groups:
    for k in ("arrive_s", "guide_start_s", "seated_s", "pay_s", "left_s"):
        g[k] = F(g[k])
done = sorted([g for g in groups if g["left_s"] >= 0], key=lambda g: (g["left_s"], int(g["group_id"])))
lefts = [g["left_s"] for g in done]

rows = [{k: (float(v) if k not in ("condition_id",) else v) for k, v in r.items()} for r in tl]


def posted_by(t):
    return sum(1 for x in lefts if x <= t)


# 各行で、取られた片付けの数
taken_n = [posted_by(r["t_s"]) - int(r["tables_dirty_untaken"]) for r in rows]

res = []
for k, g in enumerate(done, start=1):
    hi = next((i for i, n in enumerate(taken_n) if n >= k and rows[i]["t_s"] >= g["left_s"]), None)
    if hi is None:
        continue
    t_hi = rows[hi]["t_s"]
    t_lo = max(g["left_s"], rows[hi - 1]["t_s"]) if hi > 0 else g["left_s"]
    # 同じ卓の次の組
    nxt = [h for h in groups if h["table_id"] == g["table_id"] and h["guide_start_s"] > g["left_s"]]
    nxt = min(nxt, key=lambda h: h["guide_start_s"]) if nxt else None
    end = nxt["guide_start_s"] if nxt else None
    # 終わった時刻のころ、入口で待つ組がいたか
    q = None
    if end is not None:
        prev = [r for r in rows if r["t_s"] <= end]
        q = int(prev[-1]["waiting_groups"]) if prev else None
    res.append(dict(k=k, g=g, t_lo=t_lo, t_hi=t_hi, end=end, nxt=nxt, q=q, wait=(t_lo + t_hi) / 2 - g["left_s"]))

res.sort(key=lambda r: -r["wait"])
pick = res[:TOP]

m = lambda s: f"{s/60:6.1f}"
print(f"\n片付けを頼んだ数: {len(done)}　割り出せた数: {len(res)}")
print(f"片付けに付くまで（割り出し・幅の真ん中）の平均: {sum(r['wait'] for r in res)/len(res):.0f} 秒"
      "（summary.json の job_wait_mean_s 片付け と比べる）")

print(f"\n片付けを長く待った卓 {TOP} つ（時刻は店が開いてからの分）")
print("順 | 卓 | 席 | 組 | 客が帰った（会計済み） | スタッフが取った（幅） | 次の組の案内が始まった＝終わった時刻の上の端 | そのとき入口で待つ組 | 帰ってから取るまで（分） | 取ってから次の案内まで（分）")
for r in pick:
    g = r["g"]
    end = f"{m(r['end'])}" if r["end"] is not None else "  なし"
    span = f"{(r['end'] - r['t_hi'])/60:.1f}〜{(r['end'] - r['t_lo'])/60:.1f}" if r["end"] is not None else "-"
    print(f"{r['k']:3d} | T{int(g['table_id']):02d} | {g['table_seats']} | {g['group_id']} | {m(g['left_s'])} | "
          f"{m(r['t_lo'])}〜{m(r['t_hi'])} | {end} | {r['q']} | {(r['t_lo']-g['left_s'])/60:.1f}〜{(r['t_hi']-g['left_s'])/60:.1f} | {span}")

# そのあいだスタッフが何をしていたか（60秒ごとの人数の平均）
cols = [("staff_idle", "待機"), ("staff_guide", "案内"), ("staff_serve", "料理"), ("staff_call", "呼び出し"),
        ("staff_pay", "会計"), ("staff_clear", "片付け"), ("staff_rescue", "立ち往生")]
bcols = [("board_guide", "案内"), ("board_serve", "料理"), ("board_call", "呼び出し"), ("board_pay", "会計"), ("board_clear", "片付け")]
print("\n帰ってから取られるまでのあいだ、スタッフ4人が持っていた仕事（60秒ごとの人数の平均）と、板に残っていた仕事の数の平均")
print("卓 | 行の数 | " + " ".join(n for _, n in cols) + " || 板: " + " ".join(n for _, n in bcols) + " | 汚れた卓（未着手+片付け中） | 入口で待つ組")
for r in pick:
    g = r["g"]
    rr = [x for x in rows if g["left_s"] <= x["t_s"] <= r["t_hi"]]
    av = lambda c: sum(x[c] for x in rr) / len(rr)
    print(f"T{int(g['table_id']):02d} | {len(rr)} | " + " ".join(f"{av(c):.2f}" for c, _ in cols) + " || " +
          " ".join(f"{av(c):.2f}" for c, _ in bcols) +
          f" | {av('tables_dirty_untaken'):.1f}+{av('tables_clearing'):.1f} | {av('waiting_groups'):.1f}")

# 1分ごとの並び（いちばん長く待った卓）
r = pick[0]; g = r["g"]
print(f"\nいちばん長く待った卓 T{int(g['table_id']):02d} の、帰ってから取られるまでの1分ごとの様子")
print("分 | 待機 案内 料理 呼出 会計 片付け | 板: 案内 料理 呼出 会計 片付け | 汚れた卓 未着手/片付け中 | 入口で待つ組 | この卓より前に頼まれてまだ取られていない片付け")
for x in rows:
    if not (g["left_s"] - 60 <= x["t_s"] <= r["t_hi"]):
        continue
    ahead = sum(1 for j, lt in enumerate(lefts, start=1) if j < r["k"] and lt <= x["t_s"]) - \
            max(0, posted_by(x["t_s"]) - int(x["tables_dirty_untaken"]))
    print(f"{x['t_s']/60:5.0f} | " + " ".join(f"{int(x[c])}" for c, _ in cols[:6]) + " | " +
          " ".join(f"{int(x[c])}" for c, _ in bcols) +
          f" | {int(x['tables_dirty_untaken'])}/{int(x['tables_clearing'])} | {int(x['waiting_groups'])} | {max(0, ahead)}")

----- robot_staff_extra.py（置いた場所：claude_code_reports/20261030_0823_staff_job_choice_files/robot_staff_extra.py）-----
"""ロボットがいると案内・片付けが長くなる分を、記録から分ける（2026-10-30・読むだけ）
今までの summary.json・give_ups.csv・yields.csv・groups.csv を読むだけ。何も書き換えない。

Unity のスタッフの時間（summary.json の staff_time_s）は、仕事を持っているあいだの秒数を仕事の名前ごとに足したもの
（StaffAgent.Update → ServiceOps.AddStaffTime）。ロボットをよけて止まっている間・あきらめるまでの 60 秒も、
その仕事の時間に入る。
  案内 1回 ＝ いた所 → 待合まで歩く ＋ guide_s 45 秒 ＋ 待合 → 卓まで歩く
  片付け 1回 ＝ いた所 → 卓 ＋ clear_s 40 秒 ＋（卓 → 返却口 → 卓）× bus_trips … ＋ wipe_s 40 秒
なので「歩いている分」＝ 1回の時間 − 決まった作業の秒数。ロボットがいると増える分をこう分ける：
  あきらめ … give_ups.csv のスタッフの「あきらめた」1件につき staffGiveUpSec（60 秒）。そのあと仕事は板に戻り、
             取り直した人は最初の行き先から歩き直す（StaffAgent.Begin で行き先を組み直す）。歩き直しの分は下の「そのほか」に入る
  よけて止まった … yields.csv のスタッフの waited_s の合計。どの仕事のときかは記録に無いので、
             仕事ごとの「歩いている分」の割合で按分する（按分であって、記録ではない）
  そのほか … 残り。遠回り・歩き直し・人どうしのつかえ・ゆっくり歩く などで、記録からは分けられない
使い方: python3 robot_staff_extra.py <service_sim/unity のフォルダ> [店=fam_p2] [来客=36]
"""
import csv, glob, json, os, statistics, sys

U = sys.argv[1]
ST = sys.argv[2] if len(sys.argv) > 2 else "fam_p2"
ARR = sys.argv[3] if len(sys.argv) > 3 else "36"
GIVEUP_S = 60.0
FIXED = {"案内": 45.0, "片付け": 80.0}   # guide_s、clear_s + wipe_s（その店の bus_trips は歩く回数だけに効く）
med = statistics.median


def one(f):
    sm = json.load(open(os.path.join(f, "summary.json"), encoding="utf-8"))
    g = list(csv.DictReader(open(os.path.join(f, "groups.csv"), encoding="utf-8")))
    gu = list(csv.DictReader(open(os.path.join(f, "give_ups.csv"), encoding="utf-8")))
    yd = list(csv.DictReader(open(os.path.join(f, "yields.csv"), encoding="utf-8")))
    n = len(g)
    t = sm["staff_time_s"]
    out = {"groups": n}
    for job in ("案内", "片付け"):
        per = t.get(job, 0.0) / n
        gave = sum(1 for r in gu if r["who"] == "スタッフ" and r["job"] == job and r["action"] == "あきらめた")
        warp = sum(1 for r in gu if r["who"] == "スタッフ" and r["job"] == job and r["action"] == "移した")
        out[job] = per
        out[job + "_walk"] = per - FIXED[job]
        out[job + "_giveup_s"] = gave * GIVEUP_S / n
        out[job + "_gave"] = gave / n
        out[job + "_warp"] = warp / n
    # よけて止まった秒を、仕事ごとの「歩いている分」の割合で按分する。
    # 歩いている分の合計 ＝ 仕事を持っている時間 − 決まった作業の秒数
    #   （案内 45・片付け 80・会計 90 は1組に1回、料理は皿ごとに staff_place_s 10、立ち往生は1回 rescue_s 15。
    #    呼び出しの回数は summary に無いので、呼び出しの 30 秒は引かない＝歩いている分に入れたまま）
    staff_wait = sum(float(r["waited_s"]) for r in yd if r["who"].startswith("スタッフ"))
    busy = sum(v for k, v in t.items() if k != "待機")
    dish = sm["sim_like"].get("staff_dishes", 0)
    resc = sm["site_run"].get("rescues_done", 0)
    walk_busy = busy - (45.0 + 80.0 + 90.0) * n - 10.0 * dish - 15.0 * resc
    share = staff_wait / max(1.0, walk_busy)
    out["yield_total_per_group"] = staff_wait / n
    out["yield_share_busy"] = staff_wait / busy if busy else 0.0
    out["yield_share_walk"] = share
    for job in ("案内", "片付け"):
        out[job + "_yield_s"] = out[job + "_walk"] * share
    out["rescue_per_group"] = t.get("立ち往生", 0.0) / n
    out["walk_km_per_staff"] = sm["site_metrics"]["walk_km_per_staff"]
    out["staff_util"] = sm["site_metrics"]["staff_util"]
    out["robot_jams_s"] = sm["sim_like"].get("people_jam_total_s", 0.0) / n
    out["late_staff_dishes"] = sm["sim_like"].get("late_staff_dishes", 0) / n
    out["rescues"] = sm["site_run"].get("rescues_done", 0) / n
    out["staff_n"] = sm["timeline_check"]["staff"]
    out["walk_m_per_group"] = out["walk_km_per_staff"] * 1000 * out["staff_n"] / n
    return out


res = {}
for r in (0, 1, 2):
    fs = sorted(glob.glob(os.path.join(U, f"20261106_night_a{ARR}_cs44_{ST}_r{r}_s*_passon_sbret")))
    res[r] = [one(f) for f in fs]

keys = [("groups", "組の数", "{:.0f}"),
        ("案内", "案内1回（秒）", "{:.1f}"), ("案内_walk", "　うち歩いている分（秒）", "{:.1f}"),
        ("案内_giveup_s", "　あきらめた60秒の分（秒/組）", "{:.1f}"), ("案内_gave", "　あきらめた回数（回/組）", "{:.2f}"),
        ("案内_warp", "　移した回数（回/組）", "{:.2f}"), ("案内_yield_s", "　よけて止まった分（按分・秒/組）", "{:.1f}"),
        ("片付け", "片付け1回（秒）", "{:.1f}"), ("片付け_walk", "　うち歩いている分（秒）", "{:.1f}"),
        ("片付け_giveup_s", "　あきらめた60秒の分（秒/組）", "{:.1f}"), ("片付け_gave", "　あきらめた回数（回/組）", "{:.2f}"),
        ("片付け_warp", "　移した回数（回/組）", "{:.2f}"), ("片付け_yield_s", "　よけて止まった分（按分・秒/組）", "{:.1f}"),
        ("yield_total_per_group", "スタッフがよけて止まった秒（全部の仕事・秒/組）", "{:.1f}"),
        ("yield_share_busy", "　仕事を持っている時間のうちの割合", "{:.3f}"),
        ("yield_share_walk", "　歩いている分のうちの割合（按分に使う）", "{:.3f}"),
        ("rescue_per_group", "立ち往生を動かしに行った時間（秒/組）", "{:.1f}"),
        ("rescues", "立ち往生を動かした回数（回/組）", "{:.2f}"),
        ("late_staff_dishes", "2分待ってスタッフが運んだ皿（皿/組）", "{:.2f}"),
        ("robot_jams_s", "人のつかえの時間（秒/組・客も入る）", "{:.1f}"),
        ("walk_m_per_group", "スタッフが歩いた距離（m/組）", "{:.0f}"),
        ("staff_util", "スタッフの稼働率", "{:.3f}")]
print(f"店 {ST}・来客 {ARR} 組/時・各台数 種 {len(res[0])}/{len(res[1])}/{len(res[2])} 個の中央値")
print("項目 | 0台 | 1台 | 2台 | 2台−0台")
for k, name, fmt in keys:
    v = [med([x[k] for x in res[r]]) for r in (0, 1, 2)]
    print(f"{name} | " + " | ".join(fmt.format(a) for a in v) + f" | {fmt.format(v[2]-v[0])}")

# 2台−0台の増えた分を分ける
print("\n2台で1回あたり増えた分の分け方（中央値どうしの差。秒）")
for job in ("案内", "片付け"):
    d = med([x[job] for x in res[2]]) - med([x[job] for x in res[0]])
    gu = med([x[job + "_giveup_s"] for x in res[2]]) - med([x[job + "_giveup_s"] for x in res[0]])
    yl = med([x[job + "_yield_s"] for x in res[2]]) - med([x[job + "_yield_s"] for x in res[0]])
    print(f"{job}: 増えた {d:.1f} ＝ あきらめるまでの60秒 {gu:.1f} ＋ よけて止まった（按分） {yl:.1f} ＋ そのほか {d-gu-yl:.1f}")

----- compare_timeline.py（置いた場所：claude_code_reports/20261030_0823_staff_job_choice_files/compare_timeline.py）-----
"""混んでいる・0台で、スタッフの手と片付け待ちの時間の流れを、Unity と試算で並べる（2026-10-30・読むだけ）
試算（haizen_sim.py v2.8）は中を変えず、外から1分ごとに数えるだけ。Unity は今までの timeline.csv・summary.json を読むだけ。
07:44 の版（20261030_0744_staff_job_choice_files）を写して1か所直した：試算の run_once と同じく window() も走らせる。
  07:44 の版は window() を走らせていなかったので、試算の「全員が帰った」の合図（finished）が来ず、
  1分ごとに数える sampler が止まらないまま回り続けていた（compare_timeline.txt が空のままだった理由）。
使い方: python compare_timeline.py <haizen_sim.py のフォルダ> <service_sim/unity のフォルダ>
"""
import csv, glob, json, os, statistics, sys
SIM_DIR, UNITY_DIR = sys.argv[1], sys.argv[2]
sys.path.insert(0, SIM_DIR)
import haizen_sim as h

STEP = 60          # Unity の timeline と同じ 60 秒ごと
BIN = 20 * 60      # 表は 20 分ごとにまとめる


class S(h.Store):
    """試算の店。決まりは変えず、1分ごとの様子と片付けの待ちを外から数える"""
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self.rows = []
        self.cw = []            # 片付けの依頼からスタッフが付くまで（秒）
        self.clear_dur = []     # 片付けでスタッフの手がふさがった秒数
        self.env.process(self.sampler())

    def sampler(self):
        env = self.env
        while not self.finished.triggered:
            busy = len(self.staff.users)
            busy2 = sum(1 for u in self.staff.users if u.priority == 2)
            q1 = sum(1 for r in self.staff.queue if r.priority <= 1)
            q2 = sum(1 for r in self.staff.queue if r.priority == 2)
            self.rows.append({"t": env.now, "busy": busy, "busy_clear": busy2, "q_p1": q1, "q_clear": q2,
                              "dirty": self.dirty, "dirty_untaken": max(0, self.dirty - busy2),
                              "waiting": len(self.tables.queue), "reg_q": len(self.register.queue)})
            yield env.timeout(STEP)

    def staff_job(self, priority, walk_m, work_s, waited=None):
        if priority == 2:
            w = []
            t0 = self.env.now
            yield from super().staff_job(priority, walk_m, work_s, w)
            self.cw.append(w[0])
            self.clear_dur.append(self.env.now - t0 - w[0])
        else:
            yield from super().staff_job(priority, walk_m, work_s, waited)


def sim_runs(st, arr, staff):
    s = h.STORES[st]
    cond = {"store": st, "staff": staff, "robots": 0, "arrivals_per_h": arr, "mode": "serve",
            "trays": 1, "fragile_by": "any", "registers": 1}
    out = []
    for seed in range(h.REPS):
        p = {**h.COMMON, **s, **cond}
        env = h.simpy.Environment()
        x = S(env, h.random.Random(seed), p)
        env.process(x.arrivals())
        env.process(x.window())      # run_once と同じ。これが無いと finished が来ない
        env.run()
        out.append(x)
    return out


def unity_runs(st, arr):
    out = []
    for f in sorted(glob.glob(os.path.join(UNITY_DIR, f"20261106_night_a{arr}_cs44_{st}_r0_s*_passon_sbret"))):
        tl = list(csv.DictReader(open(os.path.join(f, "timeline.csv"), encoding="utf-8")))
        sm = json.load(open(os.path.join(f, "summary.json"), encoding="utf-8"))
        g = list(csv.DictReader(open(os.path.join(f, "groups.csv"), encoding="utf-8")))
        out.append((tl, sm, g))
    return out


def binned(rows_per_run, key, tmax):
    """20 分ごとの平均（全部の回をまとめる）"""
    res = []
    for b0 in range(0, tmax, BIN):
        v = [r[key] for rows in rows_per_run for r in rows if b0 <= r["t"] < b0 + BIN]
        res.append(statistics.mean(v) if v else float("nan"))
    return res


m = statistics.median
for st in ["fam_p1", "fam_p3", "fam_p4", "fam_p0"]:
    s = h.STORES[st]
    arr, staff = s["arrivals"][2], s["staff"][1]
    sims = sim_runs(st, arr, staff)
    uns = unity_runs(st, arr)
    # Unity の timeline を同じ形にそろえる
    urows = []
    for tl, sm, g in uns:
        rr = []
        for t in tl:
            idle = int(t["staff_idle"]); clr = int(t["staff_clear"])
            rr.append({"t": float(t["t_s"]), "busy": staff - idle, "busy_clear": clr,
                       "q_p1": int(t["board_guide"]) + int(t["board_serve"]) + int(t["board_call"]) + int(t["board_pay"]),
                       "q_clear": int(t["board_clear"]),
                       "dirty": int(t["tables_dirty_untaken"]) + int(t["tables_clearing"]),
                       "dirty_untaken": int(t["tables_dirty_untaken"]),
                       "waiting": int(t["waiting_groups"]), "pay_line": int(t["pay_in_line"]) + int(t["pay_waiting_seat"]),
                       "busy_pay": int(t["staff_pay"])})
        urows.append(rr)
    srows = [x.rows for x in sims]
    tmax = 4 * 3600
    print(f"\n=== {s['label']}（{st}・来客 {arr} 組/時・スタッフ {staff} 人・0台）　Unity 種{len(uns)}個 / 試算 {len(sims)}回 ===")
    print("20分ごと | 手がふさがった人数 U/S | うち片付け U/S | 板の優先1の仕事 U/S | 板の片付け U/S | 汚れた卓 U/S | 入口で待つ組 U/S")
    cols = {k: (binned(urows, k, tmax), binned(srows, k, tmax)) for k in ["busy", "busy_clear", "q_p1", "q_clear", "dirty", "waiting"]}
    for i, b0 in enumerate(range(0, tmax, BIN)):
        line = f"{b0//60:3d}-{(b0+BIN)//60:3d}分"
        for k in ["busy", "busy_clear", "q_p1", "q_clear", "dirty", "waiting"]:
            u, sv = cols[k][0][i], cols[k][1][i]
            line += f" | {u:4.2f}/{sv:4.2f}"
        print(line)
    # 全員がふさがっていた分の割合（来客がある 0〜120 分）
    def full_share(rows_per_run):
        v = [r["busy"] >= staff for rows in rows_per_run for r in rows if r["t"] < 7200]
        return sum(v) / len(v)
    def last_minute(rows_per_run):
        return m([max(r["t"] for r in rows) / 60 for rows in rows_per_run])
    u_clear_job = m([sm["staff_time_s"].get("片付け", 0) / max(1, len(g)) for tl, sm, g in uns])
    s_clear_job = m([statistics.mean(x.clear_dur) for x in sims])
    u_pay_job = m([sm["staff_time_s"].get("会計", 0) / max(1, len(g)) for tl, sm, g in uns])
    print(f"来客中（0〜120分）に全員の手がふさがっていた分の割合 U/S: {full_share(urows):.2f}/{full_share(srows):.2f}")
    print(f"片付け1回でスタッフの手がふさがった秒数（中央値） U/S: {u_clear_job:.0f}/{s_clear_job:.0f}")
    print(f"会計1回でスタッフの手がふさがった秒数（中央値） U/S: {u_pay_job:.0f}/90")
    print(f"片付けに付くまで（秒・中央値） U/S: {m([sm['job_wait_mean_s'].get('片付け', 0) for tl, sm, g in uns]):.0f}/{m([statistics.mean(x.cw) for x in sims]):.0f}")
    print(f"組の数（中央値） U/S: {m([len(g) for tl, sm, g in uns])}/{m([x.done for x in sims])}　最後の客が出た分（中央値） U/S: {last_minute(urows):.0f}/{last_minute(srows):.0f}")
    up = [statistics.mean(r["busy_pay"] for r in rows if 60*60 <= r["t"] < 120*60) for rows in urows]
    print(f"Unity：60〜120分に会計の仕事を持っていたスタッフの人数（平均の中央値）: {m(up):.2f}")
