Inter-Annotator Agreement › Code Implementation
Language AI HandbookAll chapters Inter-Annotator Agreement › Code ImplementationOne section · Sections in this chapter7 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's implement these calculations in Python, verifying our manual computation and showing library usage. The code also handles a missing-data scenario that illustrates Krippendorff's alpha's unique capabilities.
import numpy as np
# Define our annotation data: 10 items, 3 annotators (A, B, C)
# Categories: 0=Negative(G), 1=Neutral(N), 2=Positive(P)
annotations = np.array(
[
[2, 2, 2], # Item 1: P,P,P
[2, 2, 1], # Item 2: P,P,N
[1, 1, 1], # Item 3: N,N,N
[2, 2, 2], # Item 4: P,P,P
[1, 2, 1], # Item 5: N,P,N
[0, 0, 0], # Item 6: G,G,G
[2, 1, 2], # Item 7: P,N,P
[1, 1, 1], # Item 8: N,N,N
[2, 2, 2], # Item 9: P,P,P
[0, 2, 0], # Item 10: G,P,G
]
)
n_items, n_raters = annotations.shape
categories = np.unique(annotations)
k = len(categories)Now we calculate Cohen's kappa between annotators A (column 0) and B (column 1).
# Cohen's Kappa between A and B
rater_a = annotations[:, 0]
rater_b = annotations[:, 1]
# Using sklearn
kappa_sklearn = cohen_kappa_score(rater_a, rater_b)
# Manual calculation for verification
def cohens_kappa_manual(rater1, rater2, categories):
n = len(rater1)
# Agreement matrix
agreement = np.zeros((len(categories), len(categories)))
for i in range(n):
idx1 = np.where(categories == rater1[i])[0][0]
idx2 = np.where(categories == rater2[i])[0][0]
agreement[idx1, idx2] += 1
agreement = agreement / n # Convert to proportions
# Observed agreement
p_o = np.trace(agreement)
# Expected agreement (marginals)
p_i_plus = np.sum(agreement, axis=1)
p_plus_j = np.sum(agreement, axis=0)
p_e = np.sum(p_i_plus * p_plus_j)
kappa = (p_o - p_e) / (1 - p_e)
return kappa, p_o, p_e
kappa_manual, p_o, p_e = cohens_kappa_manual(rater_a, rater_b, categories)Cohen's Kappa (A vs B) using sklearn: 0.492 Manual calculation: kappa = (0.70 - 0.41) / (1 - 0.41) = 0.492
The manual calculation confirms our earlier arithmetic, showing moderate agreement between annotators A and B.

Next, we implement Fleiss' kappa for all three raters.
def fleiss_kappa(annotations, categories):
"""
Calculate Fleiss' kappa for multiple raters.
annotations: n_items x n_raters array
"""
n_items, n_raters = annotations.shape
k = len(categories)
# Count matrix: n_items x k
count_matrix = np.zeros((n_items, k))
for i in range(n_items):
for j in range(n_raters):
cat_idx = np.where(categories == annotations[i, j])[0][0]
count_matrix[i, cat_idx] += 1
# P_i for each item: proportion of agreeing ordered rater pairs
P_i = (np.sum(count_matrix**2, axis=1) - n_raters) / (
n_raters * (n_raters - 1)
)
P_bar = np.mean(P_i)
# Overall proportion of assignments to each category
P_j = np.sum(count_matrix, axis=0) / (n_items * n_raters)
# Expected agreement
P_e = np.sum(P_j**2)
kappa = (P_bar - P_e) / (1 - P_e) if (1 - P_e) != 0 else 0
return kappa, P_bar, P_e, P_j, P_i, count_matrix
kappa_fleiss, p_bar, p_e_fleiss, cat_props, P_i_values, count_mat = (
fleiss_kappa(annotations, categories)
)Fleiss' Kappa: 0.564
Observed agreement (P_bar): 0.733
Expected agreement (P_e): 0.389
Category proportions: {'G': '0.167', 'N': '0.333', 'P': '0.500'}Fleiss' kappa shows moderate agreement across all three raters, slightly higher than the pairwise Cohen's kappa between A and B. Pooling all three raters raises the observed agreement while producing a slightly lower chance-agreement baseline for this dataset.

Now we implement Krippendorff's alpha, which requires building the coincidence matrix.
def krippendorff_alpha(annotations, categories, metric="nominal"):
"""
Calculate Krippendorff's alpha.
Handles varying numbers of raters per item (missing data via NaN).
"""
n_items, n_raters = annotations.shape
k = len(categories)
# Build coincidence matrix
coincidence = np.zeros((k, k))
for i in range(n_items):
# Get valid annotations for this item (non-NaN if we had missing data)
valid = annotations[i, :]
n_valid = len(valid)
# All ordered pairs of raters
for r1, r2 in itertools.product(range(n_valid), range(n_valid)):
if r1 != r2:
c1 = np.where(categories == valid[r1])[0][0]
c2 = np.where(categories == valid[r2])[0][0]
coincidence[c1, c2] += 1
total_coincidences = np.sum(coincidence)
# Observed disagreement (for nominal: proportion of off-diagonal)
if metric == "nominal":
D_o = (total_coincidences - np.trace(coincidence)) / total_coincidences
else:
raise NotImplementedError("Only nominal metric implemented here")
# Expected disagreement based on marginals
category_counts = np.sum(coincidence, axis=1)
n_total = np.sum(category_counts)
probs = category_counts / n_total
D_e = 1 - np.sum(probs**2)
alpha = 1 - (D_o / D_e) if D_e != 0 else 0
return alpha, D_o, D_e, coincidence
alpha, d_o, d_e, coin_mat = krippendorff_alpha(annotations, categories)Krippendorff's Alpha: 0.564 Observed disagreement (D_o): 0.267 Expected disagreement (D_e): 0.611
Krippendorff's alpha matches Fleiss' kappa in this case because we have complete data, fixed numbers of raters, and nominal categories. The advantage of alpha becomes apparent with missing data or ordinal scales.


Let's demonstrate handling missing data, a key strength of Krippendorff's alpha. We remove two ratings and observe that alpha remains computable and close to the complete-data value.
# Simulate missing data: remove some annotations
annotations_missing = annotations.copy().astype(float)
annotations_missing[2, 1] = np.nan # Item 3, rater B missing
annotations_missing[7, 2] = np.nan # Item 8, rater C missing
def krippendorff_alpha_missing(annotations, categories):
"""Alpha with missing data handling."""
n_items, n_raters = annotations.shape
k = len(categories)
coincidence = np.zeros((k, k))
for i in range(n_items):
# Filter out NaN values
valid = annotations[i, ~np.isnan(annotations[i, :])]
n_valid = len(valid)
if n_valid < 2:
continue
for r1, r2 in itertools.product(range(n_valid), range(n_valid)):
if r1 != r2:
c1 = np.where(categories == valid[r1])[0][0]
c2 = np.where(categories == valid[r2])[0][0]
coincidence[c1, c2] += 1
total = np.sum(coincidence)
D_o = (total - np.trace(coincidence)) / total
probs = np.sum(coincidence, axis=1) / total
D_e = 1 - np.sum(probs**2)
return 1 - (D_o / D_e)
alpha_missing = krippendorff_alpha_missing(annotations_missing, categories)Annotations with missing data (NaN): [[ 2. 2. 2.] [ 2. 2. 1.] [ 1. nan 1.] [ 2. 2. 2.] [ 1. 2. 1.] [ 0. 0. 0.] [ 2. 1. 2.] [ 1. 1. nan] [ 2. 2. 2.] [ 0. 2. 0.]] Krippendorff's Alpha with missing data: 0.467 Original (complete data): 0.564
The alpha coefficient remains stable even with missing annotations. This makes it suitable for real-world annotation projects where not every rater labels every item. Cohen's kappa and Fleiss' kappa would require you to either drop items with missing data or impute values before computation.
Let's now compute pairwise Cohen's kappa for all three annotator pairs to show how agreement varies across pairs.
# Compute pairwise Cohen's kappa for all annotator pairs
pair_labels = ["A vs B", "A vs C", "B vs C"]
pair_kappas = []
pair_p_o = []
pair_p_e = []
for r1, r2 in [(0, 1), (0, 2), (1, 2)]:
kappa_val, po, pe = cohens_kappa_manual(
annotations[:, r1], annotations[:, r2], categories
)
pair_kappas.append(kappa_val)
pair_p_o.append(po)
pair_p_e.append(pe)A vs B: kappa=0.492 (P_o=0.700, P_e=0.410) A vs C: kappa=0.844 (P_o=0.900, P_e=0.360) B vs C: kappa=0.355 (P_o=0.600, P_e=0.380)
