Skip to content

Commit 99cfc1b

Browse files
fix(rq1): score() skips aborted/absent runs instead of counting them as recall 0 (#158)
再レビュー待ちの間に、gohan がラウンドごとに指摘してきた『偽 recall』系を先回りで自己監査し、 同種をもう1件発見・修正。score() の run ループが range(runs) 全てを含むため、ABORTED(queue/ phase 失敗)や未実行のランが 04 出力ゼロ→recall 0 として mean を引き下げていた。 - _scorable_run(run_root): 04_PARTIAL があるランのみ採点対象(=Phase04 が実際に出力した run)。 ABORTED/未実行は skip し件数を報告。「04 を走らせたが何も recovered しなかった」は実 04 出力が あるので真の 0 として残る、という区別。 - テスト追加(scorable=04出力あり / aborted・absent=skip)。ALL PASS(7 テスト)。 Refs: #156, #102 Co-authored-by: sururu-k <2009hirotake@gmail.com>
1 parent 6dab3b6 commit 99cfc1b

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

experiments/rq1_baselines/run_arms.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ def _recovered_gt(finding_ids: set[str], finding_to_gt: dict[str, str]) -> set[s
199199
return {finding_to_gt[f] for f in finding_ids if f in finding_to_gt}
200200

201201

202+
def _scorable_run(run_root: Path) -> bool:
203+
"""True only if this (arm, run) actually produced Phase-04 output. A run that
204+
aborted or never ran has no 04_PARTIAL and must be skipped, not scored as
205+
recall 0 (which would spuriously understate the arm's recall)."""
206+
return any(run_root.glob("04_PARTIAL_*.json"))
207+
208+
202209
def score(arms: list[str], runs: int, gt_map_path: str | None) -> int:
203210
"""Per-arm recall on the 15 H/M/L, using an EXTERNAL match map.
204211
@@ -228,16 +235,25 @@ def score(arms: list[str], runs: int, gt_map_path: str | None) -> int:
228235
arm = arm_by_id(arm_id)
229236
per_run_recall: list[float] = []
230237
union: set[str] = set()
238+
skipped = 0
231239
for run_idx in range(runs):
232240
run_root = HERE / "runs" / arm["id"] / f"run{run_idx}"
241+
# Only score a run that actually produced Phase-04 output. A run that
242+
# aborted (ABORTED.txt / no 04 output) or never ran must NOT be counted
243+
# as recall 0 — that is a spurious 0 that understates recall exactly like
244+
# the earlier path/schema bugs. "Ran 04 but recovered nothing" IS a real 0.
245+
if not _scorable_run(run_root):
246+
skipped += 1
247+
continue
233248
rec = _recovered_gt(_confirmed_finding_ids(run_root), f2g)
234249
union |= rec
235250
per_run_recall.append(round(len(rec) / denom, 4))
236251
recovered_union[arm["id"]] = union
237252
by_sev = {s: sum(1 for g in union if sev.get(g) == s) for s in ("High", "Medium", "Low")}
238253
mean = round(sum(per_run_recall) / len(per_run_recall), 4) if per_run_recall else None
239254
rng = (min(per_run_recall), max(per_run_recall)) if per_run_recall else None
240-
print(f"[{arm['id']}] recall mean={mean} min-max={rng} over {runs} run(s) | "
255+
note = f" ({skipped} aborted/absent run(s) skipped)" if skipped else ""
256+
print(f"[{arm['id']}] recall mean={mean} min-max={rng} over {len(per_run_recall)} scored run(s){note} | "
241257
f"union recovered {len(union)}/{denom} (H/M/L={by_sev['High']}/{by_sev['Medium']}/{by_sev['Low']})")
242258

243259
# property-only-recoverable = (B|C) MINUS A, listed by gt id (the decisive set)

experiments/rq1_baselines/test_rq1_baselines.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ def test_confirmed_finding_ids_reads_phase04():
108108
print("ok: confirmed finding ids from Phase 04")
109109

110110

111+
def test_scorable_run_skips_aborted_or_absent():
112+
# A run with no Phase-04 output (aborted / never ran) must NOT be scored as
113+
# recall 0 — score() skips it. Only a run that produced 04 output is scorable
114+
# (a real "ran 04, recovered nothing" is a genuine 0, and DOES have 04 output).
115+
with tempfile.TemporaryDirectory() as td:
116+
ran = Path(td) / "ran"; ran.mkdir()
117+
(ran / "04_PARTIAL_x.json").write_text("{}", encoding="utf-8")
118+
assert run_arms._scorable_run(ran) is True
119+
aborted = Path(td) / "aborted"; aborted.mkdir()
120+
(aborted / "ABORTED.txt").write_text("phase 03 exited 1", encoding="utf-8")
121+
assert run_arms._scorable_run(aborted) is False
122+
assert run_arms._scorable_run(Path(td) / "never_ran") is False
123+
print("ok: _scorable_run skips aborted/absent")
124+
125+
111126
def test_recovered_gt_and_property_only_set():
112127
f2g = {"armA-001": "H1", "armC-005": "H2", "armB-007": "M1"}
113128
assert run_arms._recovered_gt({"armA-001", "armX-999"}, f2g) == {"H1"}

0 commit comments

Comments
 (0)