Skip to content

Commit 314f4ed

Browse files
Skits0098claude
andcommitted
Add the writeup
Lives in docs/, linked from the README by one line rather than expanded into a section. The README's job is to make someone want to use the tool; this piece's job is to explain why it exists. Mixing them is how a real artifact starts reading as a demo. Leads with the thing anyone can check for themselves: a 50-case suite going 82% to 78% produces a 95% interval spanning [-0.16, +0.00] at p = 0.63, which is no evidence in either direction, and most teams would have reverted on it. The core of it is section 2, the cluster undercoverage, because it is the one claim here that could not have been produced by argument. The pairs cluster bootstrap is the obvious method, I built it, and calibration measured it at 89.5% coverage for a nominal 95%. It needs ~50 clusters to be honest and real eval suites have eight. That is a finding, not a design opinion, and the measured table is in the piece. Every quoted output is verbatim from the real code, re-run and diffed rather than transcribed from memory. Every number cross-checks against inference.py. The AI-assistance note is deliberately in this file and not in the README, is specific enough to check against the commit trailers and the diff, and does not claim more for me than is true: the code was largely generated, the scope decision was mine, and the bug in section 2 was caught by a simulation rather than by anyone reading anything. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cd7f9dd commit 314f4ed

2 files changed

Lines changed: 337 additions & 2 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,17 @@ statisticians.
193193
20-50 real-failure cases as a starting suite; regression evals sit near 100%
194194
and exist to detect backsliding.
195195

196+
## Reading
197+
198+
- [**Most of your eval regressions aren't real**](docs/most-eval-regressions-arent-real.md)
199+
- why a 50-case suite cannot answer the question you are asking it, and what the
200+
simulations caught that code review would not have.
201+
- [DESIGN.md](DESIGN.md) - the full design, and decision records for the two
202+
choices I got wrong first.
203+
196204
## Status
197205

198-
Pre-release, v0.1 in progress. See [DESIGN.md](DESIGN.md) for the full design and
199-
the reasoning behind each decision, including the ones I got wrong first.
206+
Pre-release, v0.1 in progress.
200207

201208
## License
202209

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# Most of your eval regressions aren't real
2+
3+
You change a prompt. Your eval suite goes from 82% to 78%.
4+
5+
What do you do?
6+
7+
Most people revert. Some ship anyway and feel bad about it. Almost nobody asks the
8+
only question that matters, which is whether 78% and 82% are different at all.
9+
10+
Here is that exact comparison, run through the tool I will describe below:
11+
12+
```
13+
verdict: INCONCLUSIVE
14+
effect: -0.0400 (0.8200 -> 0.7800)
15+
95% CI: [-0.1600, +0.0000]
16+
p: 0.625 (exact McNemar)
17+
paired: 50 cases
18+
```
19+
20+
Four of your fifty cases moved. The interval spans everything from a sixteen point
21+
regression to no change whatsoever. McNemar puts it at p = 0.63. There is no
22+
evidence here in either direction. You learned nothing, and if you reverted, you
23+
reverted on a coin flip.
24+
25+
This is not a contrived example. It is what a 50-case suite looks like, and 50
26+
cases is what Anthropic's own guidance suggests you start with.
27+
28+
## Nothing in your toolchain can tell you this
29+
30+
I went looking for a tool that would. promptfoo, DeepEval, Ragas, Braintrust
31+
autoevals, Langfuse, Phoenix, pydantic-evals. Every one of them computes a mean
32+
and compares it to a threshold you hardcoded.
33+
34+
`assert score > 0.8`
35+
36+
That comparison is close to meaningless on a suite this size, and it is the actual
37+
mechanism behind the thing everyone complains about: flaky eval CI. Your suite
38+
reports 82% today and 78% tomorrow, the tool says REGRESSION with total
39+
confidence, and you go hunting for a bug that was never there. Then it happens
40+
often enough that you stop believing the tool, which is worse than not having it.
41+
42+
The field has good data collection. OpenTelemetry tracing is solved, and every
43+
observability vendor will happily store your spans. It has good scoring
44+
primitives: judges, assertions, rubrics. What it does not have is any statistics
45+
between "we ran an eval" and "we know whether this change is real."
46+
47+
## The fix is twenty months old and nobody implemented it
48+
49+
Evan Miller, an econometrician at Anthropic, published
50+
[*Adding Error Bars to Evals*](https://arxiv.org/abs/2411.00640) in November 2024.
51+
It is not a research breakthrough. It is a patient explanation that eval scores
52+
are a *sample*, that samples have standard errors, and that undergraduate
53+
experimental design applies. Report standard errors. Cluster them when your
54+
questions come in groups. Compare paired, not unpaired. Do a power analysis.
55+
56+
As of July 2026, essentially zero mainstream eval tools implement any of it.
57+
58+
I do not think that is because it is hard. The methods are standard. I think it is
59+
because eval tooling is built by product engineers and this is a statistics
60+
problem, and those are different people. Miller saw it because he is an
61+
econometrician looking at a field that had reinvented A/B testing without
62+
noticing.
63+
64+
So I implemented it. The result is [OpenParity](https://github.com/Skits0098/OpenParity),
65+
which does exactly one thing: you hand it per-case results from two runs, and it
66+
tells you whether the difference is real.
67+
68+
It does not run your evals. It does not define graders. It never makes a network
69+
call. That constraint is what lets its entire test suite run in ten seconds with no
70+
API key, which matters more than it sounds, and I will come back to it.
71+
72+
Three things I learned building it. The second one is the one I did not expect.
73+
74+
## 1. The gate everyone would build rewards you for having a worse eval suite
75+
76+
The obvious upgrade to `assert score > 0.8` is a significance test. Instead of
77+
comparing to a threshold, ask: is the candidate significantly worse than baseline?
78+
79+
This is better. It is also broken, and the way it is broken is instructive.
80+
81+
A significance test asks "can I reject the hypothesis that nothing changed?" With
82+
a small suite, you cannot reject anything. There is not enough evidence to reject
83+
with. So a small suite passes. Always.
84+
85+
Shrink your eval suite and your build goes green.
86+
87+
That is a catastrophic incentive to bake into a regression tool, and it is a
88+
completely natural thing to build. You would not notice for months, because the
89+
failure mode is silence: your CI is green, and it is green because you are not
90+
looking hard enough to see anything.
91+
92+
The fix is to ask the opposite question. Not "is it worse?" but "can I *prove* it
93+
is close enough?" That is an equivalence test, TOST, and it is standard in
94+
pharmacology, where "we failed to detect a difference between this generic and the
95+
real drug" is obviously not the same claim as "this generic works."
96+
97+
Now an underpowered suite *fails*, because it cannot establish anything either.
98+
The incentive inverts. To get a green build you need a suite capable of supporting
99+
a claim. You declare a margin, which is your honest statement of what you do not
100+
care about, and the tool proves the change stays inside it or admits it cannot.
101+
102+
This gives you four verdicts instead of two:
103+
104+
| | |
105+
|---|---|
106+
| `equivalent` | Proven within your margin. Ship. |
107+
| `regressed` | Significantly worse. Block. |
108+
| `improved` | Significantly better. |
109+
| `inconclusive` | Your suite cannot answer. **Not a pass.** |
110+
111+
That fourth one is the whole product. Every other tool has two states, so whenever
112+
the honest answer is "you don't have enough evidence," it has to pick one and lie.
113+
114+
## 2. The obvious way to handle grouped cases is silently wrong
115+
116+
This is the part I got wrong, and I only know I got it wrong because I wrote the
117+
tests first.
118+
119+
Eval cases are usually not independent. You have five questions about the same
120+
document, or eight turns in the same scenario, or twelve prompts against the same
121+
customer record. When cases share a parent, they move together. Treating them as
122+
independent understates your uncertainty, and the literature puts the
123+
understatement above 3x.
124+
125+
Everyone who knows this reaches for the same fix: resample whole clusters instead
126+
of individual cases. The pairs cluster bootstrap. It is the obvious move, it is
127+
what I planned, and it is what I built.
128+
129+
Then I ran the calibration test. A 95% confidence interval makes a falsifiable
130+
promise: across repeated experiments, it contains the true value 95% of the time.
131+
So generate data where you *know* the true answer, build a thousand intervals, and
132+
count.
133+
134+
Mine contained the truth 89.5% of the time.
135+
136+
Nominal 95%, actual 89.5%. My intervals were too narrow, which means my tool would
137+
call regressions that were not there, which is the exact failure it exists to
138+
prevent. If I had shipped on "the method is standard and my code looks right," I
139+
would have shipped a tool that lies.
140+
141+
So I measured coverage against the number of clusters:
142+
143+
```
144+
Continuous data, nominal 95%:
145+
146+
clusters pairs bootstrap CRVE + t(G-1)
147+
6 0.845 0.945
148+
12 0.902 0.945
149+
20 0.917 0.937
150+
30 0.923 0.947
151+
50 0.950 0.960
152+
```
153+
154+
And on binary pass/fail data, which is what most eval suites actually produce:
155+
156+
```
157+
clusters pairs bootstrap CRVE + t(G-1)
158+
5 0.840 0.953
159+
8 0.890 0.949
160+
12 0.931 0.960
161+
20 0.948 0.958
162+
```
163+
164+
This is the documented "few clusters" problem, and there is the shape of it. The
165+
pairs cluster bootstrap needs roughly fifty clusters before it delivers the
166+
coverage it advertises.
167+
168+
**Real eval suites do not have fifty clusters.** Group your cases by source
169+
document and you have eight. At eight, the method quietly gives you 89% coverage
170+
and prints "95%" on the label.
171+
172+
The fix is a cluster-robust standard error with a t(G-1) critical value, which
173+
holds ~95% down to five clusters. What does the work there is not the robust
174+
variance, it is the t correction: at six clusters, t(5) puts the critical value at
175+
2.57 instead of 1.96, and that gap is the entire difference between 84% and 95%
176+
actual coverage.
177+
178+
I also measured a wild cluster bootstrap with Rademacher weights. It is
179+
statistically indistinguishable from CRVE here. CRVE won on engineering grounds:
180+
a third of the code, analytic rather than resampled, and deterministic, so CI can
181+
never see the same data produce different bounds.
182+
183+
The general lesson is not about clusters. It is that statistical code fails
184+
silently. It does not throw. It does not crash. It returns a plausible number with
185+
a confidence interval attached, and the interval is a lie, and nothing anywhere
186+
tells you. The only defense is to generate data where you know the answer and
187+
check that your tool finds it.
188+
189+
Which is why the "no network calls" constraint earns its keep. Because the core is
190+
pure functions over arrays, I can run ten thousand simulated comparisons in
191+
seconds for free, on every commit. An eval tool that needs a model to test itself
192+
cannot do this, and so mostly does not.
193+
194+
## 3. You cannot compute power from one run
195+
196+
I wanted a command that says "your suite is too small, here is how many cases you
197+
need." Everyone wants this.
198+
199+
You cannot build it from a baseline run. The power of a paired test depends on the
200+
standard deviation of the per-case *differences*, and that depends on the
201+
correlation between two runs. One run does not contain that information. Any tool
202+
that offers power analysis from a single eval set is either guessing sigma or
203+
silently assuming your runs are independent, which they are not.
204+
205+
What it needs is a **null pair**: run your baseline twice, changing nothing. Same
206+
prompt, same model, same config. The only difference is sampling randomness. That
207+
measures your actual noise floor.
208+
209+
This turns out to be the cheapest useful thing you can do to an eval suite. No
210+
labelling, no judging, no new cases. Just run it twice. And almost nobody has.
211+
212+
Here is what it said about that 50-case suite:
213+
214+
```
215+
suite: 50 cases
216+
noise: SE of the mean difference = 0.0896 (+/-10%, from your null pair)
217+
goal: prove equivalence within +/-0.0300 at alpha=0.05
218+
power: 0.0% (target 80%)
219+
220+
NOT adequately powered. You need about 3822 cases (+3772) to reach 80% power.
221+
```
222+
223+
Three thousand eight hundred cases. That number is brutal, and it is not a bug.
224+
Proving a change stays within three points when your run-to-run noise is nine
225+
points genuinely takes thousands of cases. That is arithmetic, not pessimism.
226+
227+
The useful response is not to go label 3,772 cases. It is one of:
228+
229+
- **Widen the margin.** Three points was probably a number you picked because it
230+
sounded rigorous, not because you can tell the difference between a 2% and a 4%
231+
regression in production. Pick the margin you actually care about.
232+
- **Cut the noise floor.** Sample each case several times and average. This costs
233+
inference, not labelling, and it is usually far cheaper than new cases. It also
234+
converts binary outcomes into continuous ones, which carry strictly more
235+
information per case.
236+
- **Accept that you are shipping on judgment.** Which is fine. It is what you are
237+
doing right now. The difference is knowing it.
238+
239+
What you should not do is keep making ship decisions from a 50-case suite and
240+
believe them.
241+
242+
## The uncomfortable part
243+
244+
If you run this on your own eval suite, the first thing it will tell you is that
245+
your suite cannot resolve the question you have been asking it for months. Most
246+
suites return `INCONCLUSIVE` on their first real comparison. That is the tool
247+
working, and it is not a pleasant experience.
248+
249+
It also means some of the prompt changes you shipped were noise you read as
250+
signal, and some you reverted were improvements you threw away. There is no way to
251+
find that out that feels good.
252+
253+
But the alternative is what everyone is doing now: eyeballing a handful of outputs
254+
and calling it engineering, while a dashboard reports 78% with three significant
255+
figures and no error bar. The number is not wrong. It is just not evidence, and
256+
the two are easy to confuse when one of them is rendered in a nice font.
257+
258+
## Limits, honestly
259+
260+
- **It does not run your evals.** It consumes per-case results from whatever
261+
produced them. That is on purpose, so it composes with your existing tooling
262+
instead of replacing it, but it means you have to get results out of your runner
263+
in a format it can read.
264+
- **It cannot fix a bad eval.** If your judge is miscalibrated, OpenParity will
265+
give you a beautifully rigorous confidence interval around a meaningless number.
266+
Statistics do not rescue construct validity.
267+
- **The power estimate inherits your noise floor's error.** The relative standard
268+
error of an SE estimate is about `1/sqrt(2n)`, so about 16% from a 20-case null
269+
pair. "You need 355 cases" from a small null pair means "a few hundred," not
270+
three significant figures. The tool says so rather than printing false precision.
271+
- **Below about five clusters, everything is doing heavy lifting.** Coverage held
272+
in simulation, but the intervals get wide fast and lean hard on the t
273+
correction. Treat borderline verdicts with suspicion.
274+
275+
## Try it
276+
277+
```bash
278+
pip install openparity
279+
openparity compare runs/main.json runs/pr.json --margin 0.03
280+
```
281+
282+
Exit 0 to ship, 1 to block, 2 for "your suite cannot answer this," which also
283+
blocks, because treating no evidence as success is the failure the whole thing
284+
exists to prevent.
285+
286+
Source and the full design notes, including the decisions I got wrong first:
287+
[github.com/Skits0098/OpenParity](https://github.com/Skits0098/OpenParity)
288+
289+
The statistics are verified rather than asserted. Coverage, false alarm rates,
290+
clustering, pairing, and power are each checked against data whose true answer is
291+
known by construction, on every commit. If those tests fail, everything above is
292+
worthless. They are the first thing I would look at, and you should too.
293+
294+
---
295+
296+
### On how this was built
297+
298+
This was written with heavy AI assistance, in one session, using Claude. Every
299+
commit carries a `Co-Authored-By` trailer, and you can check that against the
300+
history rather than take my word for it.
301+
302+
I want to be precise about what that does and does not mean, because "AI wrote it"
303+
is doing a lot of unearned work in both directions right now.
304+
305+
The code was largely generated. What I chose was the scope, and on this project
306+
the scope was most of the work. This started as something much larger and much
307+
worse: an open-source "AI workflow control plane" with a dashboard, a durable
308+
execution engine, approval gates, and traces. That idea is dead for good reasons.
309+
The orchestration category is saturated, the human-approval piece is a graveyard
310+
of near-identical abandoned projects, and a broad platform is now the cheapest
311+
artifact anyone can generate, which is exactly why it signals nothing. Killing it
312+
and keeping the one piece nobody had built was the decision that mattered.
313+
314+
The other choice worth naming is writing the tests before the implementation, and
315+
it paid off in a way I did not predict. The cluster bug in section 2 was not
316+
caught by review or by reasoning. It was caught because a simulation made a
317+
falsifiable claim and the claim failed. Reading the code would never have found
318+
it: the code was *correct* for the method it implemented. The method was wrong for
319+
the data, and the only thing that can tell you that is measurement.
320+
321+
Which is the honest summary of what AI assistance is and is not good for here. It
322+
will write you a clean, well-documented, confidently wrong statistical library.
323+
It will not tell you the method is wrong, because it does not know either. The
324+
simulation knows.
325+
326+
If you want to evaluate this repo, do not evaluate the prose. Run
327+
`pytest tests/test_calibration.py`, then go break something in `inference.py` and
328+
watch what happens.

0 commit comments

Comments
 (0)