← All projects

Project 4 of 5

Does Jev's Confidence Mean Anything?

A vendor calls its confidence scores calibrated. I audited them against human annotators for $0.05, then shipped the fix as a one-file tool.

What I didRan the audit, then wrote the tool that fixes what it found.

PythonNumPypandasmatplotlibPlatt scalingisotonic regressioncivil_comments

Every number on this page links to the file it came from, pinned to does-jev-confidence-mean-anything @ def9fac and jevcal @ 468a4df.

Headline results

10%flagged by humans when Jev said about 75%natural comment traffic, best of four wordingsSee where it comes from ↗
0.91AUC, so it ranks wellthe units are wrong, not the decisionsSee where it comes from ↗
96%of the error removed by a two-parameter fitwith the ranking untouchedSee where it comes from ↗
$0.05to run the whole audit8,000 judgments, 4 wordings, 2 base ratesSee where it comes from ↗

The short version

The problem

A vendor’s docs call its confidence scores calibrated. Engineers read that as: threshold at 0.9 and you get 90%. I wanted to know whether that reading survives contact with human-labelled data, and what it costs to find out.

What I did

I used civil_comments as ground truth, because every comment carries the share of a human panel that flagged it. I sent 8,000 judgments to Jev, across four wordings and two base rates, for five cents, and measured ranking (AUC) and calibration (ECE and Brier score) separately.

Jev ranks well. Its probabilities lean toward yes, the error grows as positives get rarer, and a two-parameter fit removes 96% of it without changing a single ranking. Then I turned the check into a tool: jevcal, one file, numpy only, about 100 labelled rows.

What this is not

It is not an argument against the model. An AUC of 0.91 means it understands the task, and as a high-recall pre-filter with a calibration layer on top it works. As if p > 0.9 it does not do what it looks like it does.

What I would do next

Run jevcal on a second model that emits probabilities, such as an LLM judge, to find out whether the claim that nothing in it is Jev-specific holds.

How it works

Pick a request and watch it go through the system. The red ones are the requests I use to attack it. They end where the system is supposed to stop them.

raw scorecivil_commentshuman annotator sharesJev API4 wordings, 8,000 judgmentsPredictionscached JSONL, resumableMeasureECE, Brier, AUCReliability chartstated vs observed rateif p > 0.9a stated 0.9 delivers 28%Fit a correctionPlatt, fitted on half the rowsjevcalcheck, fit, applycal(p) > 0.9p now means a real rate

AUC 0.91, so it ranks well, but ECE is 0.156 to 0.209 and every confidence band sits below the diagonal.

  1. civil_comments
  2. Jev API
  3. Predictions
  4. Measure
  5. Reliability chart

One pipeline, two outcomes. The audit trace measures the problem. The fix trace learns a correction. The red trace is the bet that the audit disproves.

Test results

11 claims, each with the bar I held it to and what I measured. 5 failed and 1 are unclear. They stay in, because a list where everything passes tells you nothing.

Show
ResultClaimExpectedWhat I measuredSource
PASSJev ranks flagged comments above clean onesAUC well above 0.5AUC 0.903 to 0.912 across three runsREADME.md ↗
@ def9fac
FAILStated confidence is calibratedThe vendor's sentence that higher confidence means higher accuracy is true here, since rank correlation is 0.96. What fails is reading 0.9 as a threshold you can bet on.ECE near 0ECE 0.156 to 0.209, and every confidence band sits below the diagonalREADME.md ↗
@ def9fac
FAILA stated 0.75 means about 75%roughly 75% of those comments flaggedannotators flagged 10% of the judgments in the 0.7 to 0.8 band (n=40)README.md ↗
@ def9fac
PASSA two-parameter fit fixes the units without touching the rankingn/aECE 0.157, 0.209 and 0.156 become 0.023, 0.007 and 0.006, and AUC stays 0.918README.md ↗
@ def9fac
PASSThe fix is scored on data it never sawn/afitted on one half of the rows and scored on the other halfREADME.md ↗
@ def9fac
FAILA better prompt wording fixes the calibrationECE near 0 with the right wordingno; across four wordings ECE spans 0.211 to 0.519 and the strictest still said 26% where reality was 5%README.md ↗
@ def9fac
FAILAccuracy at the default threshold shows the model worksThe signal is there and the threshold is in the wrong place. That is the whole argument for measuring calibration.n/a61.0% accuracy, worse than answering no every time (67.8%), while ranking at AUC 0.83README.md ↗
@ def9fac
FAILA low ECE means the model is goodSo jevcal prints that baseline next to your result and makes its own decisions on Brier score.n/ano; a model that ignores its input and returns the base rate scores ECE 0.0000 at AUC 0.5README.md ↗
@ 468a4df
PASSAbout 100 labelled rows are enough to calibraten/a94% of the calibration error removed at 100 rows, against 69% at 25, 90% at 50 and 93% at 800README.md ↗
@ 468a4df
PASSjevcal says when it cannot be trustedn/awarns under 50 rows or 10 positives, and writes no file if the fix does not improve held-out Brier scoreREADME.md ↗
@ 468a4df
UNCLEARThe correction works for models other than JevThe README says nothing in it is Jev-specific. That is a design claim, and I have not run it on a second model.n/anot tested; the repo's only example is the real Jev output from this auditREADME.md ↗
@ 468a4df

The numbers

Each headline is the conclusion. Open “View the data” under a chart for the exact values.

Every confidence band sits below the diagonal. Jev's top band, scored 0.94, was right 33% of the time.

Stated confidence against observed rate

0.00.00.20.20.40.40.60.60.80.81.01.0perfectly calibratedsaid 0.03, actually 0.00 (n=890)said 0.14, actually 0.00 (n=217)said 0.25, actually 0.03 (n=148)said 0.35, actually 0.05 (n=78)said 0.44, actually 0.03 (n=61)said 0.56, actually 0.06 (n=53)said 0.65, actually 0.12 (n=41)said 0.75, actually 0.10 (n=40)said 0.84, actually 0.29 (n=48)said 0.94, actually 0.33 (n=24)Jev said ~75%.Annotators flagged10% of those.what Jev said (stated confidence)how often annotators flagged it

1,600 judgments on natural comment traffic (2.9% positive), tightened wording. Dot size shows how many judgments fall in the band.

Source: results/natural_strict_buckets.csv @ def9fac ↗
View the data
Stated confidenceObserved rateSamples
0.0280.002890
0.1440.000217
0.2470.027148
0.3470.05178
0.4420.03361
0.5560.05753
0.6550.12241
0.7520.10040
0.8400.29248
0.9360.33324

Mirroring the annotators' own definition was the worst wording. No wording fixed it.

Calibration error by question wording

0.000.150.300.450.60Original: 0.3970.397OriginalAnnotator's definition: 0.5190.519Annotator's definitionStrict: 0.2110.211StrictBare: 0.4410.441Bare

Four wordings of one question in a single call, over identical state. True base rate 4.8%. Lower is better.

Source: README.md @ def9fac ↗
View the data
ItemValueNote
Original0.397
Annotator's definition0.519
Strict0.211
Bare0.441

About 100 labelled rows are enough. More does not help.

Labelled rows needed to calibrate

0%25%50%75%100%25 rows: 69%69%25 rows50 rows: 90%90%50 rows100 rows: 94%94%100 rows800 rows: 93%93%800 rows

Share of calibration error removed, measured on real data.

Source: README.md @ 468a4df ↗
View the data
ItemValueNote
25 rows69%
50 rows90%
100 rows94%
800 rows93%

What went wrong

FIXED

My first guess at a better prompt made it worse

How it surfaced
Mirroring the annotators' own published definition, which should have been the fairest wording, gave the highest ECE of four wordings (0.519).
What I did
I ran four wordings of the same question in one call over identical state, so only the phrasing varied, then re-ran the whole pipeline with the best wording instead of the first. The bias survived.
FIXED

Accuracy would have said the model was fine

How it surfaced
At a 2.9% base rate, flagging nothing scores 97.1% accuracy, so accuracy on this data is worse than useless.
What I did
The measuring step prints the do-nothing baseline before anything else and reports precision and recall.
FIXED

ECE can be gamed

How it surfaced
A model that ignores its input and returns the base rate for every row scores a perfect ECE of 0.0000 with AUC 0.5.
What I did
jevcal prints that baseline beside your result and makes its own decisions on Brier score, which cannot be gamed that way.
FIXED

Invented data could be mistaken for real data

How it surfaced
A no-API-key mode needs simulated data to test the plumbing, and simulated data looks exactly like data.
What I did
Simulated output goes to separate SIMULATED files and every chart is stamped, so the two cannot be confused.

Choices I would defend

  • Use human ground truth, not a model's opinion

    civil_comments records what fraction of a panel flagged each comment, so a toxicity of 0.7 means 7 in 10 real people agreed.

  • Test the obvious objection before making the claim

    Someone will say the prompt was badly worded. Step 7 runs four wordings in one call and the headline uses the best, not the first.

  • Ship the fix, not only the finding

    jevcal fits a correction on half the labelled rows, scores it on the other half, and prints the threshold you should actually write.

  • Implement the maths directly

    Platt scaling and isotonic regression are about sixty lines each, and the API client is plain urllib, so every number traces to code in the repo.