Skip to content

figures: pooled-mean column on the by-split heatmap, rows sorted by it - #262

Merged
jonfroehlich merged 5 commits into
mainfrom
figures/scoreboard-heatmap-aggregate
Oct 21, 2026
Merged

jonfroehlich merged 5 commits into
mainfrom
figures/scoreboard-heatmap-aggregate

Conversation

@jonfroehlich

Copy link
Copy Markdown
Member

Stacked on #261. Base is claude-newest-ids; merge this after #261. (When #261 merges, GitHub retargets this to main.)

Jon asked (2026-10-20) for an aggregate column on the "F1 by model and split" heatmap (docs/figures/scoreboard_by_split.png) that the whole table is sorted by.

What changed

  • Pooled mean column, at the right end of the pooled-city block and before the gutter. It is framed, set in bold, and has a bold "Pooled mean ▼" header. The value is the board's own pooled F1, the headline number (RampNet 0.793, Opus 5.5 0.712). It is read from the summary rather than recomputed, and by_split_rows() asserts that it equals the mean of the row's per-split cells (tolerance 2e-6, which covers the six-place rounding in scoreboard.json). It prints three decimals to match the headline. Cells keep two.
  • Rows sorted by it. The 14 legs with 9/9 pooled coverage sort descending, and RampNet comes first because it sorts there, not because it is pinned. The 8 partial-coverage legs form a separate group below a labelled rule and sort by the mean over the cities they ran, shown with coverage, e.g. 0.533 (5/9). So a one-city leg is never ranked against a nine-city mean.
  • Column-group labels ("Pooled US cities (nine)", "Pooled mean ▼", "Held out †") with hairlines above the grid, so the gutter reads as a boundary rather than a missing column.
  • Colour ramp unchanged and values printed in every cell. The aggregate column is marked by its frame, bold type and position, not by a new hue.
  • A footer line defines the pooled mean and the sort.

Row order, full group: RampNet 0.793, Claude Opus 5.5 0.712, Claude Fable 5.1 0.607, YOLO11l 0.583, Gemini 3.1 Pro 0.571, YOLO11x 0.551, YOLO26 0.547, Gemini 3.7 Flash 0.538, Gemini 3.6 Flash 0.525, Molmo2-8B 0.422, Qwen3-VL-32B 0.322, Qwen3-VL-8B 0.314, OWLv2 0.065, Grounding DINO 0.053.
Partial group: Claude Fable 5 0.611 (1/9), Claude Opus 5 low 0.568 (8/9), Mask2Former 1024 0.533 (5/9), Claude Opus 5 high 0.520 (1/9), Mask2Former curb cut 0.517 (1/9), Claude Sonnet 5 low 0.463 (1/9), Claude Sonnet 5 high 0.456 (1/9), Mask2Former +curb 0.210 (1/9).

Only the heatmap changes. The other four scoreboard figures regenerate byte-identical, and the markdown by-split table keeps its class-grouped order. docs/model_scoreboard.md gets a paragraph under the figure that explains the different row order.

Tests

  • test_by_split_heatmap_rows_sort_by_pooled_mean_in_two_groups checks the grouping, the sort within each group, that the aggregate equals the board's pooled F1, and that RampNet is first (0.793). It reads the committed analysis_out/scoreboard.json and is CPU-only.
  • test_by_split_rows_rejects_an_aggregate_that_disagrees_with_its_cells checks that a pooled F1 that drifts from its cells fails the assert.
  • python -m pytest -q: 3354 passed, 7 skipped. scripts/check_all.py --ci: 22 passed, 0 failed, 3 skipped. scoreboard.py --check: current.

To regenerate: python scripts/analysis/scoreboard.py

🤖 Generated with Claude Code — Opus 5.5, claude-opus-5-5

jonfroehlich and others added 2 commits October 20, 2026 12:11
Jon (2026-10-20) asked for an aggregate column to sort the F1-by-split heatmap
by. The column is the board's own pooled F1 (the headline number), read from
the summary and asserted equal to the mean of the row's per-split cells.

- Full-coverage legs (9/9 pooled cities) sort by it, descending; RampNet
  comes first by sorting, not by being pinned.
- Partial-coverage legs form a separate group below a labelled rule, sorted
  by the mean over the pooled cities they ran, shown as e.g. "0.533 (5/9)".
- The column sits at the right of the pooled block, framed and bold, before
  the gutter; group labels "Pooled US cities (nine)" / "Pooled mean" /
  "Held out" sit above the columns so the gutter reads as a boundary.
- Same sequential ramp, values printed in every cell.

by_split_rows() is the ordering logic, tested against the committed
scoreboard.json. Only scoreboard_by_split.png changes; the other four
figures regenerate byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s recorded as lost

The partial-coverage group now sorts by pooled cities run, then by mean, so
a one-city leg no longer sits above Opus 5 low's 8/9 (Jon, 2026-10-20).

The gemini-3.1-pro-preview / gemini-3.6-flash manual_gold run did happen
(2026-07-25 sweep, b148f71, counts kept in model_comparison.md) but its
detections were searched for on 2026-10-20 on the Windows workstation,
makelab2, klone and Jon's Mac and are not there. 0.568 / 0.540 are now marked
historical; not re-run, by decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jonfroehlich
jonfroehlich changed the base branch from claude-newest-ids to main October 20, 2026 23:11

@jonfroehlich jonfroehlich left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Independent review (head 589a50c): Not ready, one factual fix

The code is sound. Verified:

  • scoreboard.py --check exits 0.
  • tests/test_scoreboard.py: 104 passed.
  • A fresh render of the figure is pixel-identical to the committed PNG.
  • The pooled-mean tolerance has margin: the largest drift is 5.6e-7 against a 2e-6 tolerance.
  • Edge cases render correctly: a 0/9 leg, a partial-only board, a full-only board, and Mask2Former forced to 9/9 (simulating #267).
  • Nothing breaks when #267 or #259 regenerates the figure.

Blocker

  1. docs/model_scoreboard.md, Gemini note. The note says "the sweep used klone's ckpt partition, so the likeliest home was purged scratch". The repo's own log contradicts this. model_comparison.md (manual_gold "Cost and runtime", from b148f71) says "Gemini needs no GPU at all — both models ran from a desktop against Vertex"; only the GPU legs used ckpt. The cache was therefore desktop-local, and the two desktops (the Windows workstation and the Mac) are both among the machines searched. Fix: cite that line and drop the scratch guess.

Should-fix

  1. The prose example 0.533 (5/9) will go stale. It is Mask2Former 1024, which #267 moves to full coverage. Use a leg that stays partial (0.568 (8/9)) or a generic form.
  2. The new ordering test needs three changes:
    • mean_f1 == m["f1"] is tautological; drop it.
    • The 0.793 pin is a data value that breaks whenever the pool changes; drop it or derive it from the board.
    • The None / 0-coverage path is untested. Add a synthetic-board test covering a partial leg with a higher mean but lower coverage, a 0/9 leg, and a tie in the full group, and check that fig_by_split renders it.

Nits

  1. Coverage assert format. The assert hard-codes the "n/N" coverage string format. Compare counts instead.
  2. Fixed width. The figure width is fixed at 13 in. Scale it with the column count, as the height already scales with rows.
  3. Partial-only caption. On a partial-only board the caption says "not ranked against the rows above" when there are no rows above. Guard the clause.
  4. Held-out footer. It is still hard-coded and says "budapest" where the data says budapest_district5. Generate it from result["held_out"].
  5. [-0.0, width] → 0.0.
  6. A11y is fine. Every cell carries its number, and the frame and bold text mark the aggregate without using colour. One optional idea: give partial-row means a lighter fill.

Out of scope here, passed to #267: the fig_headline footer says "single-split legs", but the partial legs include 8/9 and 5/9 ones. And the scatter figures have no label nudge for Mask2Former 1024 once it goes full.

Fixes follow, then a re-review.

🤖 Generated with Claude Code — Opus 5.5, claude-opus-5-5

…d_out

Fixes for the PR 262 review (5592994266):

- model_scoreboard.md, "What is missing": the Gemini manual_gold cache was
  desktop-local. model_comparison.md (manual_gold "Cost and runtime", b148f71)
  says both Gemini models ran from a desktop against Vertex; only the GPU legs
  used ckpt. The klone-scratch guess is removed. Both desktops were searched.
- Prose example 0.533 (5/9) -> 0.568 (8/9), a leg that stays partial.
- Tests: dropped the tautological mean_f1 == f1 check and the 0.793 pin
  (RampNet's mean is now checked against the board's best full mean). Added a
  synthetic-board test: a partial leg with a higher mean but lower coverage
  sorts below a wider one, a 0/9 leg (f1 None) sorts last, a tie in the full
  group keeps board order, and fig_by_split renders the board and a
  partial-only board (Agg, tmp_path). Added a held-out footer test.
- by_split_rows checks coverage by count (len(pooled_splits)), not the "n/N"
  string.
- Figure width scales with the column count (3.0 + 0.59 in per column unit;
  12.97 in at fourteen splits, as before).
- The "not ranked against the rows above" clause appears only when there is a
  full-coverage group.
- The dagger footer is generated from result["held_out"] (held_out_footer), so
  it names budapest_district5.
- [-0.0, width] -> [0.0, width].

Only scoreboard_by_split.png changes on regeneration (the footer text).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jonfroehlich

Copy link
Copy Markdown
Member Author

Review fixes pushed in fe76f90. Every finding in the review above is addressed. Checks: scoreboard.py --check passes; tests/test_scoreboard.py 106 passed; check_all.py --ci 22 passed. The only regenerated output that changed is the heatmap's footer text.

  1. Gemini note. The note now says the cache was desktop-local and quotes model_comparison.md: "Gemini needs no GPU at all — both models ran from a desktop against Vertex". Only the GPU legs used ckpt. Both desktops were among the machines searched.
  2. Prose example. Now 0.568 (8/9), which stays partial after Vistas 1024 parity arm on all fourteen splits (stacked on #261) #267.
  3. Tests. The tautological check is dropped. The 0.793 pin is replaced by "RampNet is the best full-coverage mean". A new synthetic-board test covers coverage-first ordering, a 0/3 leg with None sorting last, and a tie keeping board order. It also renders fig_by_split for that board and for a partial-only board. A footer test is added too.
  4. Coverage assert. It now compares counts, not the n/N string.
  5. Figure width. It scales with the column count. At 14 splits it is unchanged: 12.97 in against 13.
  6. Partial-only caption. The "not ranked against the rows above" clause appears only when full-coverage rows exist.
  7. Held-out footer. It is generated from result["held_out"] and names budapest_district5.
  8. -0.0. Now 0.0.

Not done: the lighter fill for partial means. The fill encodes magnitude, so lightening it would misstate the number. Partial rows are already marked without colour, by their separate group and the coverage label.

Next: a final independent re-review.

🤖 Generated with Claude Code — Opus 5.5, claude-opus-5-5

…ut note

The synthetic board now lists rows out of order and has two one-city partial
legs, so every ordering rule is exercised without the committed data. Drops
the literal budapest_district5 pin (the loop covers it). The Gemini note
says why a desktop search can come up empty: compare.py's cache lives inside
the checkout it ran from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jonfroehlich

Copy link
Copy Markdown
Member Author

Final re-review (head fe76f90): Ready to merge

Every finding is verified as fixed:

  • Gemini note. The quote matches model_comparison.md:2046 word for word and matches b148f71.
  • Tests. Eight sort-key mutations each fail at least one test.
  • Figure. A fresh render is pixel-identical to the committed figure.
  • Held-out footer. Every generated reason is accurate for the five held-out splits.
  • Checks. scoreboard.py --check passes; tests/test_scoreboard.py 106 passed; check_all.py --ci 22 passed.

Its three optional nits are applied in 10c93b4:

  • The synthetic board now lists rows out of order and has two one-city partial legs, so the ordering test no longer depends on committed data.
  • The literal budapest_district5 pin is dropped.
  • The Gemini note now says why a desktop search can come up empty: compare.py's cache lives inside the checkout it ran from.

#267 also regenerates these figures. Whichever of the two merges second will merge main in and regenerate.

🤖 Generated with Claude Code — Opus 5.5, claude-opus-5-5

tests/test_scoreboard.py: both sides added tests at the same place; kept both.
scoreboard_by_split.png regenerated from the merged board (y11x_tiles lands in
the partial group at 0.667, 7/9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jonfroehlich
jonfroehlich merged commit 7e6d557 into main Oct 21, 2026
3 checks passed
jonfroehlich added a commit that referenced this pull request Oct 21, 2026
…#267

Conflicts, and how each was resolved:

Generated artifacts (regenerated, never hand-merged):
- analysis_out/scoreboard.json, docs/figures/scoreboard_by_split.png,
  docs/figures/scoreboard_f1.png, and the generated blocks of
  docs/model_scoreboard.md (partial table, by-split matrix) and
  docs/model_comparison.md (per-split "Also scored" lines): scoreboard.py.
  The board is 23 legs: 15 full-coverage (Vistas 1024 now among them, 0.519)
  and 8 partial (y11x_tiles 7/9 at 0.667, claude-opus-5 low 8/9, six one-split
  legs). The six y11x_tiles seed legs stay off_scoreboard.
- analysis_out/eval_protocol_150/reproduction.json: eval_protocol_150.py.
  233 scoreboard cells (main's 224 + the 9 Vistas cells), 23 legs, 2,330
  fields; 30 benchmark_eval cells; 0 differing, 0 unchecked.
- analysis_out/cascade_cost_35/summary.json: cascade_cost_35.py --summary.
  The gaps list now has both sides' "pair was not run" rows (Vistas 1024 on its
  new splits, and the seven y11x_tiles legs).

Hand-written prose and tests, with counts recomputed from the regenerated board:
- docs/model_scoreboard.md: "Eight legs have not run every pooled split - six of
  them only one" (main said nine with Vistas at 5/9; the PR said seven without
  y11x_tiles), in the section opener and the "What is missing" bullet. That
  bullet keeps the PR's wording (Vistas 1024 is in the headline) and main's
  y11x_tiles/seed-leg prose. The Vistas budapest sentence now quotes the four
  YOLO arms' 0.221-0.319, since y11x_tiles is at the top of that range.
- docs/replication.md: 273 files, 31.3 MB (MiB, the doc's convention) in
  benchmark/model_detections/ (main's 264 + the 9 Vistas files). The breakdown
  table already sums to 273.
- tests/test_scoreboard.py: kept both sides. The PR's
  test_the_vistas_parity_arm_is_pooled and test_partial_footer_counts_the_board,
  and main's by_split_rows/held_out_footer tests and
  test_seed_legs_are_named_but_not_scored. The coverage map had already
  auto-merged correctly (y11x_tiles 7/9, no Vistas 1024).

Stale counts outside the conflicts, fixed the same way:
- docs/eval_protocol_150.md: 224 -> 233 cells, 2,240 -> 2,330 fields, and the
  PR's update note says 233 (was 224) with 23 legs (it said 223 and 22).
- docs/README.md: twenty-two -> twenty-three model legs.
- docs/claude_newest_ids.md: Opus 5.5 is no longer "the only" full-coverage model
  besides the open detectors with pooled recall above RampNet's. Vistas 1024
  (0.836) joined the pool. "Highest pooled recall apart from the two open
  detectors" in model_scoreboard.md still holds.
- docs/vistas_transfer_126.md: the YOLO budapest range is 0.221-0.319.

#262's Gemini manual_gold note in model_scoreboard.md is unchanged.

Checks: scoreboard.py --check, eval_protocol_150.py --check, pytest
(3360 passed, 7 skipped), check_all.py --ci (22 passed, 3 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jonfroehlich added a commit that referenced this pull request Oct 21, 2026
eval_protocol_150.md: keep both dated notes; the laurens_gsv note quotes the
post-merge counts (224 cells, 13 op_cache cells). All checks re-run clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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