Understanding Krippendorff's Alpha

Understanding Krippendorff's Alpha

The Problem with Raw Agreement

You're building an LLM evaluation pipeline. Four contractors rate 500 model responses as "Good," "Okay," or "Bad." The dashboard shows 87% pairwise agreement. Ship it?

Not so fast. One rater noticed that 70% of responses are "Good" and started marking everything "Good" to speed through the batch. They still agree with the others most of the time--but they're contributing zero signal.

This is the lazy rater problem, and raw agreement can't detect it. If 70% of items are genuinely "Good," two random guessers would agree about 54% of the time just by chance (0.72+0.152+0.1520.540.7^2 + 0.15^2 + 0.15^2 \approx 0.54). We need a number that discounts the agreement chance would have handed us anyway.

The Whole Idea in One Formula

Krippendorff's Alpha measures the reduction in disagreement relative to random labeling:

α=1DoDe\alpha = 1 - \frac{D_o}{D_e}

where DoD_o is the disagreement you actually observed, and DeD_e is the disagreement you'd expect if the same labels had been dealt out at random.

  • α=1\alpha = 1: Perfect agreement--no observed disagreement
  • α=0\alpha = 0: Agreement equals chance--raters might as well flip coins
  • α<0\alpha < 0: Systematic disagreement--raters actively contradict each other

Everything else is careful bookkeeping: how to count observed disagreement fairly, and how to estimate what chance alone would produce. The fastest way to internalize both is to compute them by hand, once, on a dataset small enough to hold in your head.

A Worked Example

Three raters evaluate 5 items using categories A, B, and C:

ItemRater 1Rater 2Rater 3
1AAA
2BBC
3AAA
4BBB
5CCB

We'll follow the same five steps you'll use on real data (and in the practice exercise below).

Step 1: Raw Agreement

First, the simple metric everyone knows--what percentage of rating pairs agree?

For each item with mm raters, there are (m2)=m(m1)2\binom{m}{2} = \frac{m(m-1)}{2} unordered pairs. With 3 raters per item, that's (32)=3\binom{3}{2} = 3 pairs per item. Across 5 items: 5×3=155 \times 3 = 15 pairs total.

ItemRatingsPairsAgreements
1A, A, A(A,A), (A,A), (A,A)3/3
2B, B, C(B,B), (B,C), (B,C)1/3
3A, A, A(A,A), (A,A), (A,A)3/3
4B, B, B(B,B), (B,B), (B,B)3/3
5C, C, B(C,C), (C,B), (C,B)1/3

Raw Agreement = 3+1+3+3+115=1115=73.3%\frac{3+1+3+3+1}{15} = \frac{11}{15} = 73.3\%

Not bad! But is 73% impressive, or would random labeling do nearly as well? Hold that thought.

Step 2: The Coincidence Matrix

The coincidence matrix counts how often each pair of categories co-occurs across all rater pairs. For each item rated by mm raters, we enumerate all m(m1)m(m-1) ordered pairs and weight each by 1m1\frac{1}{m-1}.

With 3 raters per item, each ordered pair gets weight 131=0.5\frac{1}{3-1} = 0.5.

Why the 1/(m-1) weighting?

This ensures each item contributes equally regardless of how many raters labeled it. With mm raters generating m(m1)m(m-1) ordered pairs, the total contribution is m(m1)m1=m\frac{m(m-1)}{m-1} = m--the number of ratings on that item.

Without this, an item with five raters would simply outvote an item with two.

Show item-by-item breakdown

Item 1 (A, A, A): 6 ordered pairs, all (A,A) → contributes 6×0.5=36 \times 0.5 = 3 to cell (A,A)

Item 2 (B, B, C):

  • (B,B): 2 pairs → 2×0.5=12 \times 0.5 = 1
  • (B,C): 2 pairs → 2×0.5=12 \times 0.5 = 1
  • (C,B): 2 pairs → 2×0.5=12 \times 0.5 = 1

Item 3 (A, A, A): Same as Item 1 → adds 3 to (A,A)

Item 4 (B, B, B): All pairs are (B,B) → adds 3 to (B,B)

Item 5 (C, C, B):

  • (C,C): 2 pairs → 2×0.5=12 \times 0.5 = 1
  • (C,B): 2 pairs → 2×0.5=12 \times 0.5 = 1
  • (B,C): 2 pairs → 2×0.5=12 \times 0.5 = 1

The resulting coincidence matrix OO:

ABCRow Total
A6006
B0426
C0213

Matrix total = 15 (equals total ratings across all items: 5 items × 3 raters). The diagonal is where agreement lives; everything off it is disagreement.

Marginal totals ncn_c (row/column sums):

  • nA=6n_A = 6, nB=6n_B = 6, nC=3n_C = 3
  • n=15n = 15 (total pairable values)

Step 3: Observed Disagreement (DoD_o)

For nominal data (unordered categories), the difference function is:

δ(c,k)={0if c=k1if ck\delta(c, k) = \begin{cases} 0 & \text{if } c = k \\ 1 & \text{if } c \neq k \end{cases}

Observed disagreement is the proportion of coincidences that are disagreements (off-diagonal entries):

Do=off-diagonal sumtotal=oAB+oAC+oBA+oBC+oCA+oCBnD_o = \frac{\text{off-diagonal sum}}{\text{total}} = \frac{o_{AB} + o_{AC} + o_{BA} + o_{BC} + o_{CA} + o_{CB}}{n}

From our matrix: Do=0+0+0+2+0+215=415=0.267D_o = \frac{0 + 0 + 0 + 2 + 0 + 2}{15} = \frac{4}{15} = 0.267

This equals 1raw agreement=10.733=0.2671 - \text{raw agreement} = 1 - 0.733 = 0.267. So far, nothing new--we've just reframed agreement as disagreement.

Step 4: Expected Disagreement (DeD_e)

Here's the question Krippendorff asked. If we shuffled all ratings randomly while keeping the same category frequencies, what disagreement would we expect by chance?

The probability that two random draws from our pool are different:

De=1cnc(nc1)n(n1)D_e = 1 - \frac{\sum_c n_c(n_c - 1)}{n(n-1)}

Where ncn_c is the marginal frequency of category cc.

From our data: nA=6n_A = 6, nB=6n_B = 6, nC=3n_C = 3, n=15n = 15

De=16(5)+6(5)+3(2)15(14)=130+30+6210=166210=0.686D_e = 1 - \frac{6(5) + 6(5) + 3(2)}{15(14)} = 1 - \frac{30 + 30 + 6}{210} = 1 - \frac{66}{210} = 0.686

Random raters with these category frequencies would disagree 68.6% of the time. Our actual raters disagreed only 26.7% of the time--much better than chance!

Step 5: Krippendorff's Alpha

α=1DoDe=10.2670.686=10.389=0.611\alpha = 1 - \frac{D_o}{D_e} = 1 - \frac{0.267}{0.686} = 1 - 0.389 = 0.611

Of the agreement chance couldn't explain, our raters delivered 61%. The raw 73% sounded good, but after chance correction we land here:

Worked exampleα = 0.61
0
0.67
0.80
1

Moderate reliability--acceptable for exploratory work, but not for high-stakes decisions.

Summary of the Example

MetricValueMeaning
Raw Agreement73.3%How often raters agreed
Observed Disagreement (DoD_o)26.7%How often raters disagreed
Expected Disagreement (DeD_e)68.6%Disagreement expected by chance
Krippendorff's Alpha0.61Reliability beyond chance

Ways to Fool Yourself

Five mistakes account for most misreadings of Alpha:

1. Confusing the coincidence matrix with a confusion matrix. A confusion matrix compares predictions to ground truth. The coincidence matrix compares raters to each other--there's no "correct" answer involved.

2. Forgetting the 1m1\frac{1}{m-1} weighting. Without it, items with more raters dominate the calculation. The weighting ensures each item contributes equally, not each pair.

3. Thinking high raw agreement means high Alpha. If categories are imbalanced (e.g., 90% of items are "Good"), random guessing produces high agreement. Alpha adjusts for this, so you can have 85% agreement but α=0.3\alpha = 0.3.

4. Treating "agreement" as "accuracy." Alpha measures whether raters agree with each other, not whether they're correct. Three raters who all wrongly label "Bad" responses as "Good" will have perfect Alpha and zero accuracy.

5. Misinterpreting negative Alpha. Negative Alpha means the raters systematically disagree more than chance. That's rare, and usually indicates a coding error, inverted scales, or genuinely adversarial labeling.

Try It Yourself

The playground below simulates raters with controllable accuracy. Each row has a hidden ground truth; drag the sliders to see how rater quality affects Alpha.

Items
12
Rater Skill (P(correct))0% = always wrong | 33% = random | 100% = perfect
Rater 185%
Rater 285%
Rater 385%
Rater 485%
ItemConsensusR1R2R3R4
133333
222221
312131
411111
522222
611111
733333
833333
933333
1022212
1122222
1222222
Categories:123|Matches consensusDiffers
Raw Agreement
85%
Krippendorff's α
0.77
α = 0.77
0
0.67
0.80
1

Experiments to try:

  • "LLM Eval: The Lazy Rater" -- One rater at 33% (random guessing for 3 categories). Watch Alpha drop despite decent raw agreement.
  • "Pure Random" -- All raters at 33%. They agree by chance about 1/3 of the time, but α0\alpha \approx 0.
  • "Expert vs Novice" -- Mixed skill levels. Alpha reflects systematic disagreement.
  • Drag one slider to 0% (always wrong). How does systematic anti-correlation affect Alpha?

Your Turn

Same drill, your numbers. Each problem generates fresh ratings in a new setting, and the five steps mirror the walkthrough exactly. Ask for a hint if a step jams; ask again and it gets more specific.

Practice problem

Science Fair

3 judges rated 5 projects: G Gold, S Silver, B Bronze. Work down to α, one step at a time.
ProjectJudge 1Judge 2Judge 3
#1BSB
#2GGB
#3BBB
#4BBB
#5BBG
Step 1 of 50/5

Raw Agreement

Of all rater pairs, what share gave the same label?

%

Choosing the Difference Function

The choice of δ\delta encodes what "disagreement" means for your data type:

Levelδ(c,k)\delta(c,k)ExampleIntuition
Nominal00 if c=kc=k, else 11Categories (Good/Bad/Okay)All disagreements equally bad
Ordinal(i=ckninc+nk2)2\left(\sum_{i=c}^{k} n_i - \frac{n_c + n_k}{2}\right)^2Likert scales (1-5 stars)Disagreeing 1↔5 worse than 1↔2
Interval(ck)2(c-k)^2Temperature, datesDistance matters, ratios don't
Ratio(ckc+k)2\left(\frac{c-k}{c+k}\right)^2Counts, durations2↔4 same as 20↔40

For most LLM evaluation tasks with categorical labels (Good/Okay/Bad), use nominal. For Likert scales or ranked preferences, use ordinal. You can flip between them in the playground above and watch Alpha move.

How Good Is Good Enough?

Krippendorff (2004, p. 241) recommends:

α\alphaInterpretation
0.80\geq 0.80Reliable for most purposes
0.670.670.800.80Acceptable for tentative conclusions
<0.67< 0.67Treat data with caution

For high-stakes LLM evaluations, aim for α0.80\alpha \geq 0.80. Below 0.67, your "signal" is mostly noise.

How Much Should You Trust That Number?

Alpha is a point estimate computed from a sample of items. A small or lopsided sample can produce an impressive Alpha by luck. The practical remedy is a bootstrap confidence interval: resample your items with replacement, compute Alpha for each sample, and take percentiles.

Show Python bootstrapping code
import numpy as np
import krippendorff
 
def bootstrap_alpha(ratings, n_bootstrap=1000):
    """Compute Alpha with 95% confidence interval."""
    n_items = ratings.shape[0]
    alphas = []
 
    for _ in range(n_bootstrap):
        # Resample items with replacement
        indices = np.random.choice(n_items, n_items, replace=True)
        sample = ratings[indices]
        alphas.append(krippendorff.alpha(sample, level_of_measurement='nominal'))
 
    return {
        'alpha': krippendorff.alpha(ratings, level_of_measurement='nominal'),
        'ci_low': np.percentile(alphas, 2.5),
        'ci_high': np.percentile(alphas, 97.5),
        'std': np.std(alphas)
    }
ScenarioAlpha95% CIInterpretation
Tight CI0.75[0.72, 0.78]Reliable estimate; you can trust this
Wide CI0.75[0.55, 0.90]Uncertain; need more data
CI crosses threshold0.75[0.62, 0.85]Might not meet 0.80 standard

If your 95% CI doesn't include your decision threshold (0.67 or 0.80), you can make a confident claim; if it spans the threshold, get more data. Report CIs whenever you're making go/no-go decisions, comparing reliability across conditions, or publishing results others will build on.

Beyond the overall number, examine disagreement item by item. High-disagreement items are diagnostic gold: they show you exactly where your rubric is failing, and they make the best training examples for calibration sessions.

Show Python code for per-item analysis
def item_disagreement(ratings):
    """Compute disagreement for each item."""
    disagreements = []
    for item in ratings:
        valid = item[~np.isnan(item)]
        if len(valid) < 2:
            disagreements.append(np.nan)
            continue
        # Proportion of pairs that disagree
        pairs = [(valid[i], valid[j]) for i in range(len(valid))
                 for j in range(i+1, len(valid))]
        disagree = sum(1 for a, b in pairs if a != b) / len(pairs)
        disagreements.append(disagree)
    return disagreements

When to Use Alpha (and When Not To)

Alpha earns its keep on annotation quality checks (are your labelers consistent enough to trust?), on codebook validation (can different people apply your categories reliably?), and on benchmarking raters to find who needs more training.

It's the wrong tool when ground truth exists: with gold labels in hand, measure accuracy against them, because how well the raters agree with each other is beside the point. It's also the wrong tool for comparing models, since it measures human consistency rather than model quality -- use head-to-head preference rates or accuracy for that. And some tasks have legitimate ambiguity. When experts genuinely disagree, the disagreement is information, and driving Alpha up would just paper over it.

Why Not Just Use Kappa?

Cohen's Kappa addresses chance correction too, and when you have exactly two raters it's less machinery. But Kappa handles exactly two raters, tolerates no missing data, and (without extensions) only speaks nominal. Alpha takes any number of raters, shrugs off missing ratings, and covers ordinal, interval, and ratio data through the difference function above. For LLM evals with rotating contractor pools and incomplete coverage, Alpha is the practical choice; the Kappa post has a side-by-side table if you want the full comparison.

Improving Low Alpha

Low reliability is feedback you can act on. Start with the rubric, since ambiguous categories cause most of the disagreement: "Is this response helpful?" is vague; "Does this response directly answer the user's question?" is testable. If raters keep splitting between "Good" and "Okay," maybe the scale needs a "Good-ish" middle ground. Calibration helps too -- give raters 20 pre-labeled examples to study before rating, and discuss the edge cases explicitly. Then look for structure in the disagreement itself: compute Alpha with each rater removed (a rater whose removal dramatically improves it needs retraining), and report "clear" and "borderline" cases separately so one blended number doesn't hide the split.

Code

Python example
import krippendorff
import numpy as np
 
# Ratings matrix: rows = items, columns = raters
# Use np.nan for missing values
ratings = np.array([
    [1, 1, 2, 1],
    [2, 2, 2, 2],
    [1, 2, 1, 1],
    # ...
])
 
alpha = krippendorff.alpha(ratings, level_of_measurement='nominal')
print(f"Krippendorff's Alpha: {alpha:.3f}")
R example
library(irr)
kripp.alpha(data_matrix, method="nominal")

Further Reading

  • Krippendorff, K. (2004). Content Analysis: An Introduction to Its Methodology
  • Hayes & Krippendorff (2007). "Answering the call for a standard reliability measure"
  • krippendorff Python package