Clustered Data and the Sampling Budget

Clustered Data and the Sampling Budget

One Bad Tool Call, Ten Bad Rows

Your agent eval grades 100 conversations, 10 turns each: 1,000 graded rows. The pass rate comes in at 70%, and the naive 95% confidence interval is

± 1.960.211000≈± 2.8 pp\pm\, 1.96\sqrt{\frac{0.21}{1000}} \approx \pm\, 2.8 \text{ pp}

Tight. Shippable. Also wrong.

Turns within a conversation sink or swim together. One bad tool call on turn 2 poisons the next nine rows; a conversation that starts clean tends to stay clean. If the intraclass correlation is ρ=0.5\rho = 0.5 (half of the outcome variance lives between conversations), the design effect is

DEFF=1+(m−1)ρ=1+9×0.5=5.5\mathrm{DEFF} = 1 + (m-1)\rho = 1 + 9 \times 0.5 = 5.5

The effective sample size is 1000/5.5≈1821000 / 5.5 \approx 182, and the honest interval is ± 2.8×5.5≈± 6.7\pm\, 2.8 \times \sqrt{5.5} \approx \pm\, 6.7 pp. Your error bars were 2.3× too tight, and nobody lied. The rows just weren't independent.

This is the third post in the Error Bars for Evals strand. The first put a confidence interval on a single score; the second compared two models on the same questions. This one asks what happens when your rows arrive in clumps, and where the next thousand dollars of eval budget should go.

The Whole Idea in One Formula

Everything runs through the intraclass correlation ρ\rho: the share of outcome variance that lives between clusters rather than within them. ρ=0\rho = 0 means knowing one turn's outcome tells you nothing about its siblings. ρ=1\rho = 1 means a conversation passes or fails as a unit and the turns are photocopies.

Given ρ\rho and a common cluster size mm, Kish's design effect says how much clustering inflates the variance of your pass rate:

DEFF=1+(m−1)ρ\mathrm{DEFF} = 1 + (m - 1)\rho

Two corollaries do all the practical work:

neff=nDEFFSEhonest=SEnaive×DEFFn_{\mathrm{eff}} = \frac{n}{\mathrm{DEFF}} \qquad\qquad \mathrm{SE}_{\mathrm{honest}} = \mathrm{SE}_{\mathrm{naive}} \times \sqrt{\mathrm{DEFF}}

Anchor the formula at its edges:

  • m=1m = 1 → no penalty, ever. One row per cluster is just an independent sample, whatever ρ\rho is.
  • ρ=0\rho = 0 → no penalty, ever. Independent rows are independent rows, however you bundle them.
  • The penalty is depth × stickiness. It grows with how deep each cluster goes (m−1m - 1) multiplied by how much the rows inside it move together (ρ\rho).

The Budget Identity

The second big idea decides how to spend. Fix a budget of n=K⋅mn = K \cdot m rows--KK clusters, mm rows each--and expand the variance of the pooled pass rate:

Var⁡(p^)  ∝  1−ρKm⏟within-cluster noise  +  ρK⏟between-cluster noise\operatorname{Var}(\hat{p}) \;\propto\; \underbrace{\frac{1-\rho}{K m}}_{\text{within-cluster noise}} \;+\; \underbrace{\frac{\rho}{K}}_{\text{between-cluster noise}}

Read it out loud. The first term has the full budget KmKm in its denominator: any spending shrinks it, depth or breadth alike. The second term has only KK. There is no mm in it anywhere; no amount of extra depth touches it. Once ρ\rho is meaningful, that second term becomes the floor under your precision, and past that point extra depth stops paying.

▶Where the two-term split comes from

Start from the design-effect form and split 1+(m−1)ρ=(1−ρ)+mρ1 + (m-1)\rho = (1-\rho) + m\rho:

Var⁡(p^)=p(1−p) 1+(m−1)ρKm=p(1−p)[1−ρKm+mρKm]=p(1−p)[1−ρKm+ρK]\operatorname{Var}(\hat{p}) = p(1-p)\,\frac{1 + (m-1)\rho}{Km} = p(1-p)\left[\frac{1-\rho}{Km} + \frac{m\rho}{Km}\right] = p(1-p)\left[\frac{1-\rho}{Km} + \frac{\rho}{K}\right]

The between-cluster term ρ/K\rho / K is the variance of the cluster-quality lottery itself: with only KK conversations, you only get KK draws of "was this a good conversation," no matter how carefully you grade each one.

A Worked Example

Take the eval from the hook: K=100K = 100 conversations, m=10m = 10 turns, and a pilot estimate of ρ=0.5\rho = 0.5. The same five steps appear in the practice exercise below.

Step 1: count the rows. n=K⋅m=100×10=1,000n = K \cdot m = 100 \times 10 = 1{,}000. This is the nn the report will quote.

Step 2: the design effect. DEFF=1+(10−1)×0.5=5.5\mathrm{DEFF} = 1 + (10 - 1) \times 0.5 = 5.5. Each conversation's ten turns carry the information of roughly two independent rows.

Step 3: effective sample size. neff=1000/5.5≈182n_{\mathrm{eff}} = 1000 / 5.5 \approx 182. A thousand rows doing the work of 182.

Step 4: the bar-widening factor. 5.5≈2.35\sqrt{5.5} \approx 2.35. Every naive standard error in the report needs to stretch by 2.35× before you believe it.

Step 5: spend the next budget. Leadership doubles your spend to 2,000 rows. Two ways to use it:

DesignRowsDEFFneffn_{\mathrm{eff}}
Today: 100 conversations × 10 turns1,0001+9(0.5)=5.51 + 9(0.5) = 5.51000/5.5≈1821000/5.5 \approx 182
(a) 200 conversations × 10 turns2,0001+9(0.5)=5.51 + 9(0.5) = 5.52000/5.5≈3642000/5.5 \approx 364
(b) 100 conversations × 20 turns2,0001+19(0.5)=10.51 + 19(0.5) = 10.52000/10.5≈1902000/10.5 \approx 190

Same money. Doubling depth bought almost nothing (182 → 190 effective samples); doubling clusters doubled the work done (182 → 364). The budget identity said this would happen: option (b) spent its whole raise on the within-cluster term, and the ρ/K\rho/K floor never moved.

Where Clusters Hide in LLM Evals

Conversations are the most familiar case, but the same structure shows up all over LLM evals:

ClusterRows inside itHow it bites
ConversationGraded turnsOne early tool-call failure cascades through every later turn
Promptkk resamples (pass@k, best-of-nn, temperature sampling)Ten generations of the same prompt mostly re-measure that prompt
TemplateInstantiations with different entities/valuesA broken template fails identically a hundred times
DocumentChunks, or questions about the same passageA hard document drags down every question written against it
UserSessions, requestsPower users overweight their own quirks in the average
GraderEvery item they gradeA rubric-happy grader clusters everything they touch

Miller's "Adding Error Bars to Evals" treats this exact structure (question groups drawn from a shared context) and recommends clustered standard errors whenever eval questions arrive in related bundles. The design effect itself is much older: it comes from Kish (1965), built for household surveys where interviewing one more person in the same village was cheap and one more village was expensive. Swap "village" for "conversation" and the arithmetic transfers unchanged.

Ways to Fool Yourself

1. Counting turns (or resamples) as independent nn. The dashboard says n=1000n = 1000; the statistics say neff=182n_{\mathrm{eff}} = 182. Any calculation that feeds the raw row count into n\sqrt{n} (power analyses, CIs, significance tests) inherits the lie. The same applies to kk generations per prompt: pass@k evaluated on 100 prompts is 100 clusters, not 100×k100 \times k observations.

2. Bootstrapping rows instead of clusters. The bootstrap is not automatically honest. Resample individual rows and every resampled dataset scrambles conversations together, preserving none of the within-cluster dependence. The interval that comes out is the naive one with extra steps. Resample whole clusters (keep every turn of each drawn conversation together) and the dependence survives into the replicates.

3. Assuming ρ\rho is small without measuring it. Intuition about ρ\rho is poor and the penalty is linear in it. A pilot run and a one-way ANOVA give you an estimate in five lines: with MSB\mathrm{MS}_B the between-cluster mean square and MSW\mathrm{MS}_W the within, ρ^=(MSB−MSW)/(MSB+(m−1)MSW)\hat\rho = (\mathrm{MS}_B - \mathrm{MS}_W) / (\mathrm{MS}_B + (m-1)\mathrm{MS}_W). Code in the field notes. Agent-style evals routinely land at ρ=0.3\rho = 0.3 to 0.60.6; "it's probably about 0.1" is a hope, not a measurement.

4. Buying ten generations per prompt when the same money buys ten new prompts. Resampling feels rigorous: look how we averaged out the temperature noise. But if prompts differ more than resamples do (they almost always do), the tenth resample of prompt 17 is nearly free of new information while the eleventh prompt would have been full price.

5. Mixing cluster sizes and averaging carelessly. With unequal cluster sizes there are two different answers: pool all rows (big conversations count more) or average the cluster means (every conversation counts once). These are different estimands, and they genuinely differ when cluster size correlates with difficulty--long conversations are often long because they're going badly. Pick the one that matches the question you're asking, and state which one you picked.

Try It Yourself

The playground simulates the whole pipeline: each conversation draws a quality θj\theta_j, each turn is a coin flip around it, and both intervals are computed from the same rows. The dot field is where the intuition comes from: as ρ\rho rises, conversations go solid or hollow, and the sentence under the dots says what the rows are worth.

Interactive

Conversations, not rows

The opening example: 100 conversations × 10 turns at ρ = 0.5. Reshuffle and watch n_eff orbit the 182 computed by hand.

Turns per conversation100 × 10
Intraclass correlation ρ0.50
Budget, graded rows

The sample: 100 conversations × 10 turns

Each cluster of dots is one conversation; each dot is one graded turn, filled for pass, hollow for fail. Showing 64 of 100 conversations.

1,000 graded rows carry about as much information as 216 independent ones.

0tick = formula n ÷ DEFF = 1821,000 rows

Two 95% intervals from the same rows

70%75%80%Naive, every row independent: [69.8%, 75.4%]Naive, every row independent ±2.8ppCluster bootstrap, resampling conversations: [66.3%, 78.8%]Cluster bootstrap, resampling conversations ±6.3pp
Pass rate p̂
72.6%
1,000 rows
Naive SE
1.41
pp · √(p̂(1−p̂)/n)
Clustered SE
3.03
pp · bootstrap
Ratio
×2.15
formula √DEFF = 2.35

Spend the same budget differently

0.02.34.6SE, pp15101520turns per conversation (m), budget fixed at 1,000 rowsyou: m = 10, K = 100 → ±6.5pp

Hover the curve to price other splits. At this ρ, every step right buys rows that repeat each other: more conversations beat deeper ones.

Two comparisons are worth making on purpose:

  • "Wide and shallow" against "Deep and sticky": same budget and roughly the same ρ\rho, very different clustered intervals. Hover the budget curve at the bottom to see where each one sits on it.
  • Hold ρ\rho at 0.5 and sweep turns per conversation from 1 to 20. The marker rides up the budget curve, and every step right is precision you paid for and didn't get.

Your Turn

You've seen my numbers; now run yours. Each problem generates a fresh eval design, and the five steps are the same ones you just watched. Hints escalate if you need them, from a pointer all the way to the worked answer.

Practice problem

Prompt Resampling

Each prompt gets several generations at temperature, and resamples of the same prompt tend to pass or fail together. A pilot run estimated the intraclass correlation at ρ = 0.5. Work out what the correlation costs, then decide how to spend the next budget.
Clusters
K = 100
prompts
Rows per cluster
m = 12
temperature resamples each
Pilot ICC
ρ = 0.5
from a small pilot run
Step 1 of 50/5

Count the rows

How many graded rows does this eval produce?

Field Notes

Estimating ρ\rho from a pilot. Run a slice of the eval (even 30 clusters will do) and fit the one-way ANOVA estimator. With equal cluster sizes mm, grand mean yˉ\bar{y}, and cluster means yˉj\bar{y}_j:

MSB=mK−1∑j(yˉj−yˉ)2MSW=1K(m−1)∑j∑i(yij−yˉj)2ρ^=MSB−MSWMSB+(m−1) MSW\mathrm{MS}_B = \frac{m}{K-1}\sum_j (\bar{y}_j - \bar{y})^2 \qquad \mathrm{MS}_W = \frac{1}{K(m-1)}\sum_j \sum_i (y_{ij} - \bar{y}_j)^2 \qquad \hat\rho = \frac{\mathrm{MS}_B - \mathrm{MS}_W}{\mathrm{MS}_B + (m-1)\,\mathrm{MS}_W}
▶Compact Python: ICC from a pilot
import numpy as np
 
def icc_pilot(cluster_ids, outcomes):
    """One-way ANOVA intraclass correlation from (cluster_id, outcome) pairs."""
    ids, y = np.asarray(cluster_ids), np.asarray(outcomes, dtype=float)
    groups = [y[ids == c] for c in np.unique(ids)]
    K, n = len(groups), len(y)
    m_bar = n / K  # average cluster size
    grand = y.mean()
    ms_b = sum(len(g) * (g.mean() - grand) ** 2 for g in groups) / (K - 1)
    ms_w = sum(((g - g.mean()) ** 2).sum() for g in groups) / (n - K)
    return (ms_b - ms_w) / (ms_b + (m_bar - 1) * ms_w)

A small negative ρ^\hat\rho just means "indistinguishable from zero at this pilot size"--treat it as ρ≈0\rho \approx 0, not as an anti-correlation discovery.

When depth is the point. Everything above assumes the estimand is a population average over clusters. Sometimes it isn't. If you're measuring within-conversation degradation (does turn 15 fail more than turn 3?), then long conversations are the only place that signal lives, and cutting depth destroys the experiment. If pass@k is your metric, the kk resamples aren't a cluster inflating your variance; they're the estimand itself, and you need all kk of them per prompt (see Chen et al. 2021 for the unbiased estimator). The design effect punishes depth only when depth is redundant measurement of the same thing -- so decide what the thing is before you cut.

What to report. KK, mm (or its distribution), ρ^\hat\rho from a pilot, and clustered standard errors--via the cluster bootstrap below or the analytic clustered SEs in Miller (2024). If your CI would embarrass you at neffn_{\mathrm{eff}} instead of nn, the number to fix is KK.

Earlier in this strand. The mechanics of the basic CI live in Error Bars on Eval Scores; comparing two models on the same clustered questions is the subject of Paired Deltas. Clustering composes with both: cluster the bootstrap there the same way you cluster it here.

Code

▶Python: cluster bootstrap CI
import numpy as np
 
def cluster_bootstrap_ci(cluster_ids, outcomes, n_boot=2000, seed=0):
    """95% CI for the pooled mean, resampling whole clusters with replacement."""
    rng = np.random.default_rng(seed)
    ids, y = np.asarray(cluster_ids), np.asarray(outcomes, dtype=float)
    groups = [y[ids == c] for c in np.unique(ids)]
    K = len(groups)
    stats = []
    for _ in range(n_boot):
        draw = rng.integers(0, K, size=K)          # sample K clusters
        sample = np.concatenate([groups[j] for j in draw])
        stats.append(sample.mean())                # pool the rows
    return np.percentile(stats, [2.5, 97.5])
 
# cluster_ids = conversation IDs, one per graded turn
# lo, hi = cluster_bootstrap_ci(df.conversation_id, df.passed)

Two implementation notes. First, the resampling unit is the cluster: a drawn conversation brings all of its turns. Second, sample.mean() pools rows, so bigger clusters weigh more--swap in np.mean([g.mean() for ...]) if your estimand is the mean of cluster means (see fooling-yourself #5).

▶Python: design-effect arithmetic
import numpy as np
 
def design_effect(m, rho):
    return 1 + (m - 1) * rho
 
def effective_n(K, m, rho):
    return K * m / design_effect(m, rho)
 
# The worked example
print(design_effect(10, 0.5))        # 5.5
print(effective_n(100, 10, 0.5))     # 181.8
print(effective_n(200, 10, 0.5))     # 363.6  <- double the clusters
print(effective_n(100, 20, 0.5))     # 190.5  <- double the depth

Further Reading