Judge-comparison core: joint scorer and panel-vs-single-judge analysis - #91
andalibmalit wants to merge 5 commits into
Conversation
Selector fixes (10 findings from the scoped max review): - Partition the severity space: worst / negative / positive_extreme / positive are now mutually exclusive classes; mixed turns (+1.0 alongside a negative cell) are negative-class, so the positive-extreme RTM control contains only clean positive-tail rows - Membership flags use exactly the stratum predicates (single source of truth), fixing flag-vs-predicate denominator mismatches - Zero-safe flag totals (all flag names present even at count 0) - Independent seeded RNG stream per sampled stratum and for the repeat slice — adding or resizing a stratum never changes another's draws - Unified manifest schema per stratum: eligible / residual_pool / labeled / sampling_fraction, with inclusion-probability semantics documented - Between-run file written only when non-empty; convert/judge step for it documented in docs/partner-mode.md - Tests: mixed-extreme overlap case, zero-flag safety, RNG-stream independence, unified schema DRAFT (pending design gate, wired into no launch path): joint-prompt arm C machinery — humanebench/joint_scorer.py (all-8-in-one-call panel scorer mirroring overseer rules/retries), src/partner_rejudge_joint_task.py, convert_partner_results.py --joint mode, tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/compare_panel_vs_single_judge.py implements the pre-committed analysis
rules against arm B's .eval log:
- ordinal-first: per-cell panel median, severity distributions, disagreement
- two Holm-corrected confirmatory contrasts, everything else exploratory:
(a) FHR de-escalation on the worst stratum vs positive-extreme RTM control
(one-sided Fisher on the asymmetry), (b) trivial -0.5 reproduction rate
(one-sided exact binomial vs 0.5)
- denominators from curation.flags, per-stratum only; optional IPW pooled mean
reweighted by the manifest sampling fractions
- NaN attrition per stratum; __rep2 within-run reliability (lower bound),
optional __rep3 between-run log
- reads the nested sample.metadata.metadata shape and scores.overseer
individual_scores verified against the pilot log
tests/test_compare_panel.py: 22 synthetic-fixture tests (ordinal median,
both contrasts incl. symmetric-RTM negative control, flags-based overlapping
strata, NaN attrition, rep2 pairing, IPW, .eval parsing incl. NaN + repeats).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applies the approved arm-C reconciliation delta to the joint scorer: the 8 principles are presented in a per-turn RANDOMIZED order to average out criterion-order/position bias. - principle_order_for(sample_id, salt): deterministic per-turn shuffle, seeded from the sample id so the order is reproducible across runs (never a source of between-run variance) and identical across the ensemble's judges. - build_joint_prompt(prompt, response, patterns=None): the given order drives BOTH the numbered principle list and the JSON output template, so no canonical ordering leaks back through the template. 2-arg calls still work. - joint_overseer records `principle_order` in success- and failure-path metadata for a secondary position-bias analysis. Coverage (all 364 base turns; converter --joint excludes rep2/rep3) and output shape (severity + reasoning only) already matched the approved decisions — no change needed there. 6 new ordering tests (permutation, per-id determinism, cross-id variation, salt sensitivity, prompt-order fidelity). 14 joint tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correctness: - holm(): drop NaN p-values from the family and correct over only the finite ones, so an empty/uncomputable contrast surfaces as n/a instead of forcing a valid contrast's adjusted p to 1.0 (a real pre-registered effect could have been silently declared non-significant). - between-run reliability: filter candidates to repeat_kind=='rep3' (unify with within-run via _pair_reliability), so a mis-supplied --between-run-log can no longer pair base cells against themselves and print a false 100%. - load_cells: skip sample ids without '__' loudly instead of crashing the run. - IPW pooled mean: count and surface cells whose stratum is absent from the manifest (return n_used/n_dropped/dropped_strata; report warns) instead of silently biasing the pooled figure over a non-random subset. - per-stratum original distribution: count off-scale and no-orig cells so the "original dist" column reconciles with the stratum's scored-cell count. Report: empty-stratum p-values render as "n/a (empty stratum)" via _fmt_p. Skipped (cleanup): sharing the ~8-line zipfile samples/ iterator with compute_inter_judge_agreement.py would couple two standalone scripts for no real benefit; kept self-contained. 5 new tests (holm-NaN non-poisoning, rep3 filter, bad-id skip, IPW dropped-cell counting, orig off-scale reconciliation). 27 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The joint arm's parse/invalid/aggregation silently diverged from the per-principle overseer it must mirror, biasing the very C-vs-B contrast arm C exists to make. Reworked to match per-cell granularity: humanebench/joint_scorer.py - _coerce_invalid(): the top-level invalid flag now coerces the JSON string "false"/"no"/"0" to False (raw bool() treated any non-empty string as True, spuriously voiding all 8 principles). Matches the per-principle pydantic bool. - parse_joint_response() is now tolerant per slug: a missing or off-scale severity omits only that principle instead of failing the whole 8-principle parse, so one hard-to-scale principle no longer NaNs the other seven. Raises only on total JSON-extraction failure (still retries a genuine format break). - strict_ensemble_mean(): per-slug mean, NaN unless ALL judges validly scored the slug — the per-principle strict any-judge-fails=>NaN applied per principle. - score(): single return shape; individual_scores/individual_reasoning (NaN/"" for unscored slugs) + judge_invalid are ALWAYS present, so downstream reads no longer KeyError on judge-failure turns. Removed the all-NaN failure path. - joint_pattern_score: documented that the empty-principle 0-fill is deliberate parity with the per-principle metric (not a divergence). scripts/convert_partner_results.py - convert_row_joint validates principle slugs against KNOWN_SLUGS (parity with convert_row) so a typo'd slug fails fast, not after the re-judge run. - --sample-turns without --joint now errors instead of being silently ignored (was emitting the full dataset with a success message). - rep-exclusion coerces sample_id to str (a non-string id crashed only --joint). - accurate turn counts in the completion message (was counting pre-exclusion). scripts/compare_panel_vs_single_judge.py - load_cells rejects a JOINT (arm-C) log loudly instead of silently producing empty/garbage tables; arm C needs its own analyzer. Skipped: _extract_json's //-comment stripping can corrupt URLs in reasoning — real but pre-existing in shared scorer.py used by both arms; out of scope here. 15 new tests (invalid coercion, tolerant per-slug parse, strict ensemble mean, joint-slug validation). 74 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ErikaOnFire
left a comment
There was a problem hiding this comment.
Comment only — no formal verdict yet. My read is approve-worthy once two decisions get made explicitly; neither is a bug. Stack context (mergeability, the absence of CI, and what could and could not be verified mechanically — including that the 675 lines of tests here were read but not run) is in my review on #90.
9 files, +2,008/−102:
humanebench/joint_scorer.py— 343 lines, scores all 8 principles in one call per turn, with per-turn randomized principle order seeded from the sample idscripts/compare_panel_vs_single_judge.py— 741 lines- 675 lines of tests
The contract against humanebench/scorer.py holds
I checked the joint scorer against the single scorer point by point, because a parallel scorer that diverges quietly is the worst kind of bug here:
individual_scoresis a list of per-judge severities in both (scorer.py:226).- The strict any-judge-fails ⇒ NaN rule is mirrored per-slug rather than collapsed to a single global failure.
_extract_jsonandInvalidOverseerResponseare imported rather than reimplemented — so the parsing behaviour cannot drift between the two scorers. This is the right call and it's the one I'd have worried about most.HUMANE_PATTERNSkeys equal the pattern ids, so the prompt's JSON template and the parser agree by construction rather than by convention.load_cellscorrectly treats both NaN-valued scores and the early-return failure path as NaN cells. That second one is easy to miss: the early return atscorer.py:196-204omitsindividual_scoresentirely rather than filling it with NaN, so a reader who only handles the NaN-valued case gets aKeyErroror a silently-dropped cell. You handle it.
Seeding the shuffle from the sample id is also the right choice over a global RNG — it makes the ordering reproducible per sample without coupling to iteration order.
Review point 1 — the confirmatory contrast pairs a one-principle treatment with an all-principles control
contrast_fhr_deescalation builds the treatment arm as c.slug == FHR_SLUG and c.orig_severity == -1.0, but builds the RTM control arm as c.flags.get("is_positive_extreme") and c.orig_severity == 1.0 — with no slug filter. So contrast (a) compares an FHR-only treatment against a pooled, cross-principle control.
The FHR-only control counts (ctrl_fhr_n, ctrl_fhr_regressed) are computed and printed as a descriptive, so the information is there. But the one-sided Fisher test uses the pooled control.
For a claim the PR itself frames as "a one-principle claim and is tested as one," pairing it with a cross-principle control should be an explicit reviewer decision, not an absorbed implementation detail. It may well be the right choice — a pooled control has more power and the regression rate may not vary much by principle — but that argument should be on the record before the number goes in a paper, because it's the first thing a referee will ask. Either filter the control to FHR, or state in the docstring why pooling is defensible here.
Review point 2 — holm() shrinks the pre-registered family from m=2 to m=1 when one contrast is empty
NaN p-values are dropped and the survivor is corrected over the remaining finite tests only. The docstring argues for this well, and the argument is correct as far as it goes: a NaN sorted first would force every valid contrast to 1.0, which is worse. A test pins the behaviour, so it's deliberate rather than incidental.
The thing I'd want said out loud: the effect is weaker FWER control than the pre-registration implies. If you pre-registered a family of two and one comes back empty, correcting the survivor at m=1 means it faces α rather than α/2 — which is a different test than the one that was registered. That's a legitimate position (you can argue the empty contrast was never a test), but it's a reviewer's call and it belongs in the analysis write-up, not only in a docstring.
Both of these are statistical rather than mechanical, and both are documented choices rather than oversights. I'm raising them because they're exactly the two places where "the code does what it says" and "the claim is the one that was registered" can come apart.
Judge-comparison core on top of partner mode: a joint scorer variant and the panel-vs-single-judge analysis.
humanebench/joint_scorer.py: scores all 8 principles in one call per turn (vs. the overseer's one call per principle), with per-slug parsing, invalid-handling, and aggregation mirroring the per-principle overseer so joint-vs-decomposed comparisons aren't confounded by pipeline differences. Principle order is randomized per turn and recorded in score metadata for position-bias analysis.src/partner_rejudge_joint_task.py: replay task using the joint scorer; run without--model.scripts/compare_panel_vs_single_judge.py: compares the 3-judge panel against a dataset's original judgments under pre-committed analysis rules — ordinal-first (per-cell panel medians), flags-based denominators, per-stratum reporting, two Holm-corrected confirmatory contrasts with everything else exploratory, NaN-attrition accounting, and__rep2/__rep3reliability slices.tests/test_joint_scorer.py,tests/test_compare_panel.py(synthetic fixtures throughout).Second in the stack. Part of #90
🤖 Generated with Claude Code