LangChef
Reference

Agreement and kappa

How LangChef decides whether your judge can be trusted, worked from real numbers, with the code and the papers behind each step. No statistics background assumed.

The worked example used throughout this page

From the project's own dogfood run. You labelled 40 examples by hand; the judge had already scored the same 40. Every number on this page comes from these four counts.

                judge fail   judge pass
  you fail         12 (tp)       3 (fn)    = 15
  you pass          3 (fp)      22 (tn)    = 25
                  = 15         = 25         40

Twelve times you both said bad. Twenty-two times you both said fine. Six times you disagreed: three the judge missed, three it cried wolf on.

Why accuracy is not the answer

You agreed on 34 of 40, so the judge is 85% accurate. That sounds usable.

Now take a judge that does nothing but say "pass" to everything. On this same set it scores 62.5%. On a realistic production suite where only 5% of answers are bad, that same do-nothing judge scores 95%.

Accuracy rewards guessing the common answer. It cannot separate a judge that works from one that has noticed most things are fine. That is the whole reason this page exists.

Kappa: subtract the luck

You flagged 37.5% of examples. The judge flagged 37.5%. If both of you were flipping weighted coins at those rates and never reading the answers, you would still agree 53.1% of the time.

So the question is not how often you agreed, but how much of the room above luck you closed.

  room above chance:   100%  -  53.1%   =  46.9%
  you actually closed:  85%  -  53.1%   =  31.9%

  kappa = 31.9 / 46.9 = 0.68

In one line: kappa = (agreement observed − agreement by luck) / (1 − agreement by luck). One is perfect, zero is no better than coin-flipping. The do-nothing judge above scores exactly 0.00, which is the point.

Computed in src/langchef/core/agreement.py
confusion() counts the four cells from paired verdicts
cohen_kappa() the formula above
kappa_interval() its uncertainty, see below

How we read it, and where the thresholds came from

KappaWe sayWhat to doLandis & Koch call it
0.8 and upstrongtrust the numbers downstreamalmost perfect
0.6 to 0.8usableact on it, quote the interval toosubstantial
0.4 to 0.6weakfix the rubric before experimentingmoderate
below 0.4not usablestop; do not report pass ratesfair or worse

These bands are a convention, not a law. They descend from Landis and Koch (1977), who proposed them for observer agreement in medical data and said plainly that the divisions were arbitrary. We kept the shape because it is the one most readers already know, and collapsed the lower three into "not usable" because for our purpose the difference between fair and slight does not change what you do. If your domain has its own convention, use that instead.

The two numbers you act on

Kappa says whether the judge is worth anything overall. These say how it fails, which is what changes a rubric.

HereMeans
Catch rate (TPR, recall, sensitivity)12/15 = 80%Of the problems you found, the judge caught 12. Three shipped past it.
False alarm rate (FPR)3/25 = 12%Of the answers that were fine, it flagged three anyway.

They trade off. Push a judge to catch more and it cries wolf more. A judge with a respectable kappa can still be unusable in the direction you happen to care about, which is why the report gives you all three rather than a single score.

The report also carries PPV (when it flags something, how often it is right) and NPV (when it passes something, how often it is right). Those are the ones to read if you are deciding how much to trust an individual verdict rather than the judge as a whole.

The interval, and why Wilson

12/15 is not "80%". It is 80%, measured on fifteen examples, and those are different claims. So every rate is printed with the range the true value probably sits in.

The textbook interval, the one most people are taught, would say:

  0.8 ± 1.96 × √(0.8 × 0.2 / 15)   =   [59.8%, 100.2%]

100.2%. It runs off the end of the scale, because that formula assumes a bell curve and a proportion near a boundary is not one. At 15/15 it collapses to [100%, 100%], claiming certainty from fifteen examples. At 1/15 it goes negative.

The Wilson interval does not do that. It stays inside 0 to 1, goes asymmetric near the edges, and holds its coverage down to single-digit counts. Your actual catch-rate interval is [54.8%, 93.0%].

That width is the honest content of the measurement. The judge's true catch rate could be 55%, and forty labels cannot rule it out.

Read the kappa interval too

Kappa here is 0.68, interval [0.44, 0.92]. By the table above, 0.44 is "weak, fix the rubric" and 0.92 is "strong, trust it". Forty labels cannot tell those apart. The point estimate reads usable; the interval says you do not actually know yet. That is not a flaw in the measurement, it is the measurement.

wilson() the score interval for every rate
kappa_interval() Fleiss, Cohen and Everitt's asymptotic variance
Checked against scipy's own Wilson implementation and, for kappa, against a bootstrap that knows nothing about the formula. See tests/test_agreement.py.

Matthews correlation, in one line

0.68 here. A single correlation between the two raters' verdicts, from −1 to +1, which unlike accuracy degrades honestly when one class dominates. It is there for when someone wants one number, and it is a better one number than accuracy.

Where you disagreed

Six disagreements is a count. It does not tell you what to fix, so the taxonomy groups them by which rubric criterion the judge cited and which slice of your traffic they fell in, then refuses to report a slice whose interval does not clear the base rate.

In this run topic=returns disagreed 33% of the time against a 15% base rate, a 2.2x lift, on nine examples, with an interval of [12.1%, 64.6%]. The bottom of that interval sits below the base rate, so it is not reported. That guard is the most important thing in the codebase and it has its own page: disagreement taxonomy.

Did the rubric change help?

The taxonomy tells you where the judge disagrees. You act on it by rewriting the rubric — and then you have two calibrations and one question: did that help, or did it just move the noise around? langchef calibrate diff answers it. It re-scores the revised rubric against the same labels and reports the change in kappa and in both rates, each with an interval.

The worked example below uses a larger labelled set — sixty rather than the forty above — for a reason worth knowing before you run this. On forty labels, a revision that repairs two false alarms lands at +0.10, interval [0.00, 0.25]: inconclusive. Forty labels are enough to measure a judge and not enough to resolve a small change to one.

$ langchef calibrate diff --rubric answer-quality-v2
rubric delta on 60 labelled example(s) from run base
  answer-quality@290335165c70  ->  answer-quality-v2@8ad25d2ca381
  kappa  +0.57 -> +0.84   +0.27 [+0.11, +0.45]   IMPROVED
  TPR    80.0% -> 80.0%   +0.0% [+0.0%, +0.0%]   p=1.0000  inconclusive
         (nothing under 25.1% was in reach on these labels)
  TNR    80.0% -> 100.0%   +20.0% [+7.5%, +32.5%]   p=0.0156  improved
  moved  0 miss(es) fixed, 0 introduced; 8 false alarm(s) fixed, 0 introduced

In plain words: dropping the criterion the judge was reading as word-containment stopped all eight false alarms and cost nothing in catch rate. The moved line is the actionable one — a revision that fixes four false alarms by introducing four misses is not the same change as this, and kappa alone cannot tell them apart.

Why this is not two reports side by side

Both rubrics scored the same sixty examples against the same sixty labels. A rubric revision changes the instrument, not the ground truth. So the two kappas are two measurements on one sample, not two samples — and they move together, because the eight repaired examples are the only thing separating them.

The two calibrations on their own read 0.57 [0.36, 0.78] and 0.84 [0.69, 0.99]. Those intervals overlap, and "they overlap, so nothing changed" is the mistake. Do the same thing arithmetically — add the two variances as though the calibrations were independent — and you get [+0.01, +0.53]: more than twice as wide, and on data one repaired example lighter it straddles zero and the revision gets thrown away.

The paired interval is [+0.11, +0.45]. It is not a tighter answer to the same question; it is the answer to the right one.

This is the same defect as the minimum detectable effect computed with an unpaired formula on paired data, which reported 15.6% where the truth was 6.0%. It does not crash and it does not look wrong. It quietly turns real improvements into shrugs, which is how a rubric-iteration loop stops being worth running.

The two rates get an exact test, not a bootstrap

Kappa is not an average of anything, so its interval is a bootstrap that resamples examples and carries the human label and both verdicts on every draw. The catch rate and the false-alarm rate are simpler than that. Because the human labels do not move, the human-fail examples are literally the same examples under both rubrics: the denominator is fixed and only the numerator can move. That is exactly McNemar's setting, so only the examples where the two rubrics disagree with each other carry information — the same arithmetic, and the same code, that compare uses on two arms.

Two more things the output commits to. Kappa is the headline and the two rates are its halves, so their p-values are Holm-corrected across that family of two and a direction is named only when the corrected p clears alpha and the whole interval agrees with it — never one without the other. And a delta needs the same judge model at both ends: move the model between the two and the command exits 5 rather than reporting a rubric change that is partly a model change.

The revised rubric does not need approving first, and deliberately so. Gate one stops a rubric nobody has read from scoring a suite for real; this command produces the evidence that reading is supposed to rest on. The output records that the candidate is unapproved, so nothing downstream can mistake it for a signed-off instrument.

Computed in src/langchef/core/delta.py
kappa_delta() the paired percentile bootstrap over examples
delta() both rates, exact McNemar, Holm-corrected across the pair
The unpaired interval is written out once, in tests/test_delta.py, purely so the paired one can be measured against it. It appears nowhere under src/.

What the rubric has to do

Each ### heading in your rubric is one criterion, and the judge must name the one it failed on. That is what makes the taxonomy possible at all: without an attributed failure there is nothing to group by, and a regression in a retrieval app cannot be told apart from a regression in the generator.

It is also why editing a rubric revokes its approval. The criteria are the axis the taxonomy is reported along, so renaming one silently changes what every past number meant.

What we deliberately do not do

Further reading

Ordered by how useful they are if you are actually changing this code.

Citations verified against the publishers' records rather than quoted from memory. Where only a section could be confirmed, a section is what is cited.