混むと片付けが詰まる理由を、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 """ 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 [店=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 """ 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}")