Skip to content

fix(rescore): handle uncategorised results without dividing by zero - #61

Merged
jaylfc merged 3 commits into
masterfrom
fix/rescore-handles-uncategorised-results
May 1, 2026
Merged

fix(rescore): handle uncategorised results without dividing by zero#61
jaylfc merged 3 commits into
masterfrom
fix/rescore-handles-uncategorised-results

Conversation

@jaylfc

@jaylfc jaylfc commented May 1, 2026

Copy link
Copy Markdown
Owner

Summary

`benchmarks/locomo_rescore_streaming.py` crashed at the very end of every LongMemEval-KU rescore with `ZeroDivisionError: division by zero`. The script bucketed results by LoCoMo's integer category (1-4) and accumulated `tot["count"] += len(rs)` inside the per-category loop. LMEKU results have no category field (always `None`), so the loop ran zero iterations, `tot["count"]` stayed at 0, and `all_f1 = tot["f1"] / n` raised — after all the heavy external-judge work was already done.

This bit LMEKU Phase A Cell 1 (`lmeku_baseline-vector_gemma4_e2b`) earlier today: bench finished, all 78 `judge_rejudged` values landed in the JSON, but no scorecard printed.

What changes

  • Fallback bucket when no LoCoMo categories match: print one Overall row using all results so LMEKU and any future non-LoCoMo benchmark using the same rescore path still produces a usable number.
  • Empty results print `(no results)` instead of crashing.
  • Extracted the per-bucket computation into `_row_stats()` so the LoCoMo and fallback paths share the same code.

What does NOT change

  • LoCoMo per-category breakdown is byte-for-byte unchanged (all 4 category lines plus Overall, same numbers, same formatting).
  • The actual rescore work (LLM calls + checkpoint writes) was already correct — only the final scorecard print path is touched.

Recovered number

Cell 1 already had all 78 rejudge values in its `rescored_v2.json`, so its number is recoverable today:

LongMemEval-KU baseline-vector / gemma4:e2b → 0.5385 ext-judge (qwen3:4b)

The other 5 cells in Phase A weren't run — the chain was killed because the `vector+kg` and `full+supersede` configs were taking ~45-100 min/QA on ingest (the KG-extraction LLM-call volume), which would push Phase A to ~80h+ instead of the original ~4.5h estimate. A redesigned Phase A is for a separate session.

Test plan

  • 4 new regression tests in `tests/test_locomo_rescore_streaming.py`: LoCoMo with categories (positive), LMEKU shape with no categories (regression), empty results, partial rescore coverage
  • Full suite: 127 passing (123 prior + 4 new)
  • Verified Cell 1 number: `statistics.mean` of `judge_rejudged` over the existing `rescored_v2.json` = 0.5385

Summary by CodeRabbit

  • Bug Fixes
    • Scorecard output now includes uncategorized rows in overall totals, avoids errors on empty datasets, and shows correct per-category statistics and coverage counts (e.g., "1/2") even with partial rescoring.
  • Refactor
    • Centralized per-set metric computation to ensure consistent and accurate displayed values and totals.
  • Tests
    • Added tests verifying scorecard formatting and robustness across categorized, uncategorized, mixed, partial-rescore, and empty-result scenarios.

The rescore script's print_scorecard buckets results by LoCoMo's
integer category (1-4) and computed totals as `tot["count"] += len(rs)`
inside the per-category loop. LongMemEval-KU results have no category
field (always None), so the loop ran zero iterations, `tot["count"]`
stayed at 0, and the next line — `all_f1 = tot["f1"] / n` — raised
`ZeroDivisionError` after the script had already done all the actual
rescore work. Result: every LMEKU rescore failed AT THE END with the
heavy lifting wasted, including LMEKU Phase A Cell 1
(`lmeku_baseline-vector_gemma4_e2b`) which finished today with all 78
external-judge values populated but no scorecard.

Now: when no LoCoMo categories matched but results exist, fall back to
a single Overall bucket using all results. The per-category breakdown
stays unchanged for LoCoMo runs. An empty results list prints "no
results" instead of crashing.

Also extracted the per-bucket computation into `_row_stats()` so the
LoCoMo and fallback paths share the same code.

4 new regression tests cover: LoCoMo with categories (positive), LMEKU
shape with no categories (regression), empty results (edge case),
partial rescore coverage. All 127 tests pass.

Recovered Cell 1 number from the existing rescored_v2 JSON:
LongMemEval-KU baseline-vector / gemma4:e2b → 0.5385 ext-judge
(78/78 rescored).
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b4a66e2-9bf3-4ff2-8074-f3a59680a385

📥 Commits

Reviewing files that changed from the base of the PR and between d11b5f5 and 7df4823.

📒 Files selected for processing (1)
  • benchmarks/locomo_rescore_streaming.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • benchmarks/locomo_rescore_streaming.py

📝 Walkthrough

Walkthrough

Refactors per-bucket metric computation into a new _row_stats helper and updates print_scorecard to reuse it and include uncategorised rows in overall totals; adds early handling for empty result sets. Adds a new pytest module exercising these behaviors and edge cases.

Changes

Cohort / File(s) Summary
Scorecard refactor & logic
benchmarks/locomo_rescore_streaming.py
Adds _row_stats(rs: list[dict]) -> dict; centralizes per-category metric computation; per-category loop reads values from _row_stats; aggregates uncategorised/mismatched rows into overall totals; adds early-return handling for empty/no-contributing rows; updates printed header/docstring (no signature change).
Test coverage
tests/test_locomo_rescore_streaming.py
New test module with five tests that import the rescore script as a module and capture stdout to verify: LoCoMo categories output, fallback when categories are unusable, empty-results output, inclusion of uncategorised rows in Overall, and partial-rescore coverage formatting.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble through rows, tidy and neat,
_row_stats hops in, making numbers complete.
Uncategorized bits now counted with care,
Tests thump their paws and cheer in the air. 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main fix: handling uncategorised results and preventing division-by-zero errors when LoCoMo categories are missing.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rescore-handles-uncategorised-results

Review rate limit: 8/10 reviews remaining, refill in 8 minutes and 24 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
benchmarks/locomo_rescore_streaming.py (1)

175-216: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Overall can undercount when results are mixed categorized + uncategorised.

If at least one category in [1,2,3,4] exists, uncategorised rows are excluded from tot and therefore omitted from Overall. This can silently bias aggregate metrics/counts on partially malformed inputs.

Proposed fix (include uncategorised rows in overall aggregation when mixed)
 def print_scorecard(path: Path, data: dict, judge_model: str) -> None:
@@
-    for c in [1, 2, 3, 4]:
+    known_cats = {1, 2, 3, 4}
+    for c in [1, 2, 3, 4]:
         rs = [r for r in results if r.get("category") == c]
         if not rs:
             continue
@@
-    n = tot["count"]
+    # Include uncategorised leftovers in Overall if the dataset is mixed.
+    leftovers = [r for r in results if r.get("category") not in known_cats]
+    if tot["count"] > 0 and leftovers:
+        s = _row_stats(leftovers)
+        m = len(leftovers)
+        tot["count"] += m
+        tot["f1"] += s["f1"] * m
+        tot["orig"] += s["orig"] * m
+        tot["tol"] += s["tol"] * m
+        tot["rj_sum"] += sum(s["rv"])
+        tot["rj_n"] += len(s["rv"])
+
+    n = tot["count"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/locomo_rescore_streaming.py` around lines 175 - 216, The Overall
aggregation currently only accumulates rows in categories 1–4 into tot, so
uncategorised results (category missing or not in 1..4) get omitted when at
least one category exists; to fix, after the for c in [1,2,3,4] loop compute an
"uncat" list = [r for r in results if r.get("category") not in (1,2,3,4)] and if
uncat: compute s = _row_stats(uncat) and add its contributions to tot exactly
like the per-category updates (increment tot["count"], add s["f1"]*len(uncat),
s["orig"]*len(uncat), s["tol"]*len(uncat), tot["rj_sum"] += sum(s["rv"]),
tot["rj_n"] += len(s["rv"])); this ensures n = tot["count"] and the Overall
calculations (all_f1, all_orig, all_tol, all_rj, all_delta, cov_all) include
uncategorised rows when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@benchmarks/locomo_rescore_streaming.py`:
- Around line 175-216: The Overall aggregation currently only accumulates rows
in categories 1–4 into tot, so uncategorised results (category missing or not in
1..4) get omitted when at least one category exists; to fix, after the for c in
[1,2,3,4] loop compute an "uncat" list = [r for r in results if
r.get("category") not in (1,2,3,4)] and if uncat: compute s = _row_stats(uncat)
and add its contributions to tot exactly like the per-category updates
(increment tot["count"], add s["f1"]*len(uncat), s["orig"]*len(uncat),
s["tol"]*len(uncat), tot["rj_sum"] += sum(s["rv"]), tot["rj_n"] +=
len(s["rv"])); this ensures n = tot["count"] and the Overall calculations
(all_f1, all_orig, all_tol, all_rj, all_delta, cov_all) include uncategorised
rows when present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 41767194-f6a4-4488-9362-73d199d190b1

📥 Commits

Reviewing files that changed from the base of the PR and between ec83f67 and c2d5043.

📒 Files selected for processing (2)
  • benchmarks/locomo_rescore_streaming.py
  • tests/test_locomo_rescore_streaming.py

CodeRabbit caught an edge case in the previous fix: when a results
JSON is mixed (some rows have category in {1,2,3,4}, some don't), the
LoCoMo path silently dropped the uncategorised rows from the Overall
aggregate. The pure-LMEKU path (all rows uncategorised) was already
covered; this is the in-between case.

Now: after the per-category loop, any leftover rows whose category is
not in {1,2,3,4} are folded into the running totals before the Overall
line is printed. Empty results still print the (no results) placeholder;
all-uncategorised still falls back to a single Overall bucket.

Adds a regression test that asserts a 2-categorised + 2-uncategorised
input produces an Overall count of 4, not 2.
@jaylfc

jaylfc commented May 1, 2026

Copy link
Copy Markdown
Owner Author

Pushed `d11b5f5` addressing the mixed-categories finding. Real edge case — when the dataset has some rows with `category in {1,2,3,4}` and some without, the previous fix silently dropped the uncategorised rows from Overall. Now folds them in before computing the totals, with a regression test (2 categorised + 2 uncategorised → Overall count = 4, not 2). Pure-LMEKU and pure-LoCoMo paths are unchanged.

Note: a flaky failure in `test_agents.py::test_reflect_gate_disabled_returns_empty` showed up once when running the full suite (passes on isolated re-run, passes on full re-run). That's pre-existing on master — `tests/test_api.py` from #60 holds onto SQLite handles in its fixture cleanup that occasionally leak into `test_agents.py`. Not introduced here. Worth a separate small PR.

@kilo-code-bot

kilo-code-bot Bot commented May 1, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • benchmarks/locomo_rescore_streaming.py - Fixed banner to be benchmark-neutral (changed from "LoCoMo Rescore" to "Rescore"), resolving previous suggestion.
  • tests/test_locomo_rescore_streaming.py - No changes, no issues.

Reviewed by grok-code-fast-1:optimized:free · 120,134 tokens

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@benchmarks/locomo_rescore_streaming.py`:
- Around line 160-166: The printed banner currently hardcodes "LoCoMo Rescore
(streaming)" which is misleading for LongMemEval-KU inputs; change that literal
to a benchmark-neutral heading such as "Rescore (streaming)" or "Rescore Results
(streaming)" where the banner string is emitted (replace the "LoCoMo Rescore
(streaming)" literal used when printing the scorecard in this function/docstring
area) so the output no longer claims a specific benchmark when falling back to
the overall bucket.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7503eecb-d814-4db3-89c0-172ea032a8af

📥 Commits

Reviewing files that changed from the base of the PR and between c2d5043 and d11b5f5.

📒 Files selected for processing (2)
  • benchmarks/locomo_rescore_streaming.py
  • tests/test_locomo_rescore_streaming.py

Comment thread benchmarks/locomo_rescore_streaming.py
@jaylfc
jaylfc merged commit 713f8d6 into master May 1, 2026
2 checks passed
@jaylfc
jaylfc deleted the fix/rescore-handles-uncategorised-results branch May 1, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant