Skip to content

Add judge calibration against human labels or a cross-family judge - #145

Merged
SushantGautam merged 3 commits into
devfrom
feat/judge-calibration
Nov 6, 2026
Merged

SushantGautam merged 3 commits into
devfrom
feat/judge-calibration

Conversation

@kelkalot

@kelkalot kelkalot commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Adds simpleaudit.calibration: score a judge's stored verdicts against a reference that is independent of it. Every other judge check in the library compares the judge with itself or with another model; this one asks whether it agrees with a person, or failing that with a judge from a different model family.

What is in it

  • label(results_file) shows one stored transcript at a time in the terminal and asks for a severity. It never shows the judge's verdict and saves after every answer, so you can label six today and nine tomorrow. Labels are a plain {scenario_name: severity} dict, so you can also type them.
  • calibrate(subject, reference) scores a results file, an AuditResults, or a ReframingResults (one row per variant) against human labels, a labels file, or a results file graded by another judge. Any number of labels works. Every rate prints its n and a Wilson interval, and the report says "indicative only" below ten items.
  • Cross-family check: model_family() maps a model id to its lineage (Anthropic, OpenAI, Google, Meta, Mistral, Qwen, DeepSeek, ...). A model reference from the subject judge's own family raises; allow_same_family=True overrides it for experiments. Unknown ids warn.
  • Several raters: median on the ladder, stricter middle on an even split, agreement among raters as the ceiling, unanimous and split items scored separately.
  • Two chance-corrected coefficients in simpleaudit.stats, both reported with the reference level counts beside them: gwet_ac (Gwet's AC1/AC2, any number of raters, identity, linear or quadratic weights, pinned to the two-rater closed form) and krippendorff_alpha (nominal, ordinal, interval, pinned to the published worked example: 0.743, 0.815, 0.849). Alpha drops under a skewed reference even at near-perfect agreement (98 pass, 2 low, 97 matches: alpha 0.39, AC2 0.99), so leaderboard() ranks by AC2. This came from avalyset's review comment.
  • report.save() writes JSON under results/calibration/, list_calibrations() reads that folder back. README section and examples/judge_calibration_example.py.

One change outside the new module

rejudge() used to return the source file's metadata unchanged, so a re-graded file still claimed the original judge model. It now records the new judge_model, drops the stale provider, and keeps the original under rejudged_from. Without this, a calibration built from rejudge output would read the wrong judge and the family check would compare a model with itself.

What is deliberately not in it

No spreadsheet export, no transcript hashing, no dev/test split, no alternative annotator test, no corrected failure rate, no registry of calibrated judges. Those can come once there are enough labels to need them.

Tests

pytest -n auto on this branch: 2001 passed, 6 skipped. New tests are tests/test_calibration.py (48 tests, no model calls: fakes from tests/fakes.py through reframing_check and rejudge) and ten alpha and AC2 tests in tests/test_stats.py. ruff check --select E9,F821 . reports nothing. import simpleaudit loads no new third-party module.

I ran the human-label path on examples/nav_aap/nav_aap_sonnet_4_6.json with six made-up labels to check the output format; the numbers in the README are from that run and are not a real calibration. I did not run the cross-family path against a live OpenAI judge.

Built with Claude Code and Claude Mythos 5.1.

simpleaudit.calibration scores a judge's stored verdicts against a
reference that is independent of it: human labels typed in the terminal
with label(), or the same transcripts re-graded by a judge from another
model family. Any number of labels works; every rate carries its n and an
interval, and the report says when the count is only indicative.

Adds krippendorff_alpha to simpleaudit.stats, pinned to the published
worked example. rejudge() now records the new judge model in the result
metadata instead of carrying the source judge's, so a rejudged file can
serve as a reference.
@avalyset

avalyset commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

One thing worth showing next to the headline alpha: under a skewed reference distribution it reads low even when agreement is near-perfect, which is the common case when most scenarios pass.

With krippendorff_alpha from this branch (c032989), ordinal, on 100 items where the reference is 98 pass and 2 low, a judge that matches on 97 of them gets alpha 0.388, while exact agreement is 0.97. leaderboard() ranks by alpha, and the docstring calls it the headline figure, so a reader would likely see that judge as weak.

We hit the same pattern on real data in a cross-family check (Claude Opus against Qwen as second judge, 216 answers, one judge gave the top grade to 96.3 % of them): linearly weighted κ 0.262, raw agreement 0.894, Gwet's AC2 0.940 (results v1.0.0, 10.5281/zenodo.23947387).

Two small options: print the reference distribution beside alpha so the reader can see the prevalence, or add Gwet's AC2, the weighted form of AC1, as a companion that is less affected by it (Gwet 2008; Feinstein & Cicchetti 1990 on high agreement with low kappa).

Krippendorff's alpha corrects for chance from the observed category
frequencies, so when nearly every reference item is pass it reads low at
near-perfect agreement: 98 pass and 2 low with 97 matches gives 0.39.
Gwet's AC2 estimates chance agreement from how evenly the levels are used
and reads 0.99 on the same data. The report now prints both, with the
reference level counts between them, and leaderboard() ranks by AC2.

gwet_ac in simpleaudit.stats handles any number of raters, missing
ratings, and identity, linear or quadratic weights; the tests pin it to
the two-rater closed form and to Bennett's S under uniform marginals.
@kelkalot

kelkalot commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, reproduced exactly: 0.388 with the branch's alpha on your 98/2 example. Fixed in 893032e. The report now prints the reference level counts, Gwet's AC2 (linear weights, simpleaudit.stats.gwet_ac) and alpha together, with a note on each, and leaderboard() ranks by AC2. On your example it shows AC2 0.99 next to alpha 0.39. AC2 is validated against the two-rater closed form and Bennett's S; I didn't get to cross-check against irrCAC. The Zenodo numbers you quote match the pattern well, thanks for those.

@avalyset

avalyset commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Cross-checked gwet_ac at 893032e against irrCAC 0.4.4 (CAC.gwet(), digits=12): AC1 and linear AC2 agree to within 1e-12 on the 98/2 example, a balanced two-rater set (n = 40) and a three-rater set with missing ratings (n = 30).

reframing.rejudge_async: take dev's metadata handling (superset of this
branch's; calibration tests pass). stats: union of imports.
@SushantGautam
SushantGautam merged commit 1bce3bb into dev Nov 6, 2026
8 checks passed
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.

3 participants