FAILURE MAP
← Case archive

FA-74716 / Experiment statistics / Open access

Cluster randomisation design effect: Required clusters omit the design effect · case 01

Recruitment targets are too small to reach the planned power.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The rounding step rounds to nearest instead of up.

VERIFIED REPAIR

Round the required cluster count up.

Unsuccessful approach: Rounding up without the design effect plans for independent units.

Case contract

For clusters (e.g. companies) randomised as units: mean size m, population variance of sizes / m^2 = cv^2, design effect = 1 + ((cv^2 + 1) m - 1) icc. Effective sample = total units / design effect; clusters needed = ceil(n_required * design effect / m). No clusters or zero mean -> None. Return [round(deff, 6), round(effective n, 6), clusters needed].

Why this case matters

B2B experiments randomise by account; ignoring clustering makes results look far more certain.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cluster_sizes, icc, n_required):
    k = len(cluster_sizes)
    if k == 0:
        return None
    total = sum(cluster_sizes)
    mbar = total / k
    if mbar == 0:
        return None
    var = sum((s - mbar) ** 2 for s in cluster_sizes) / k
    cv2 = var / mbar ** 2
    deff = 1 + ((cv2 + 1) * mbar - 1) * icc
    return [round(deff, 6), round(total / deff, 6), round(n_required * deff / mbar)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 1', [[40, 20, 40, 20, 20, 40], 0.01, 500], [1.323333, 136.020151, 23]),
  ('cluster sample 2', [[3, 10, 10, 20], 0.05, 1000], [1.65814, 25.932679, 155]),
  ('cluster sample 3', [[40, 10, 5], 0.01, 500], [1.303636, 42.189679, 36])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 4', [[20, 10, 20, 5, 5, 3], 0.2, 100], [3.844444, 16.387283, 37]),
  ('cluster sample 6', [[40], 0.2, 1000], [8.8, 4.545455, 220]),
  ('cluster sample 10', [[40], 0.0, 500], [1.0, 40.0, 13])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 11', [[40], 0.01, 500], [1.39, 28.776978, 18]),
  ('cluster sample 17', [[20, 40, 3, 20, 5], 0.05, 100], [2.332955, 37.720409, 14]),
  ('cluster sample 19', [[20, 40, 20], 0.2, 1000], [6.8, 11.764706, 256])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 16', [[3, 20, 10, 5, 10, 40], 0.0, 500], [1.0, 88.0, 35]),
  ('cluster sample 25', [[10, 5, 3, 40], 0.05, 100], [2.444828, 23.723554, 17]),
  ('cluster sample 36', [[5, 10, 20, 3], 0.01, 1000], [1.130526, 33.612663, 120])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 21', [[40, 10, 5], 0.05, 100], [2.518182, 21.841155, 14]),
  ('cluster sample 36', [[5, 10, 20, 3], 0.01, 1000], [1.130526, 33.612663, 120]),
  ('cluster sample 48', [[10, 3, 20, 3, 20], 0.05, 500], [1.769643, 31.644803, 80])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
unequal cluster sizes inflate the design effect[2.54375, 31.449631, 64][2.54375, 31.449631, 64]Passed
equal clusters[1.9, 15.789474, 57][1.9, 15.789474, 57]Passed
zero icc means no inflation[1.0, 43.0, 5][1.0, 43.0, 5]Passed
single cluster[5.8, 4.310345, 23][5.8, 4.310345, 24]Failed
small clusters[1.7, 3.529412, 28][1.7, 3.529412, 29]Failed
cluster sample 1[1.323333, 136.020151, 22][1.323333, 136.020151, 23]Failed
cluster sample 2[1.65814, 25.932679, 154][1.65814, 25.932679, 155]Failed
cluster sample 3[1.303636, 42.189679, 36][1.303636, 42.189679, 36]Passed

SHA-256 / 67c3c497228de63ad4fd83074f525bb173c5f4ac2aab3d340824d7d031edf236

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cluster_sizes, icc, n_required):
    k = len(cluster_sizes)
    if k == 0:
        return None
    total = sum(cluster_sizes)
    mbar = total / k
    if mbar == 0:
        return None
    var = sum((s - mbar) ** 2 for s in cluster_sizes) / k
    cv2 = var / mbar ** 2
    deff = 1 + ((cv2 + 1) * mbar - 1) * icc
    return [round(deff, 6), round(total / deff, 6), math.ceil(n_required / mbar)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 1', [[40, 20, 40, 20, 20, 40], 0.01, 500], [1.323333, 136.020151, 23]),
  ('cluster sample 2', [[3, 10, 10, 20], 0.05, 1000], [1.65814, 25.932679, 155]),
  ('cluster sample 3', [[40, 10, 5], 0.01, 500], [1.303636, 42.189679, 36])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 4', [[20, 10, 20, 5, 5, 3], 0.2, 100], [3.844444, 16.387283, 37]),
  ('cluster sample 6', [[40], 0.2, 1000], [8.8, 4.545455, 220]),
  ('cluster sample 10', [[40], 0.0, 500], [1.0, 40.0, 13])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 11', [[40], 0.01, 500], [1.39, 28.776978, 18]),
  ('cluster sample 17', [[20, 40, 3, 20, 5], 0.05, 100], [2.332955, 37.720409, 14]),
  ('cluster sample 19', [[20, 40, 20], 0.2, 1000], [6.8, 11.764706, 256])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 16', [[3, 20, 10, 5, 10, 40], 0.0, 500], [1.0, 88.0, 35]),
  ('cluster sample 25', [[10, 5, 3, 40], 0.05, 100], [2.444828, 23.723554, 17]),
  ('cluster sample 36', [[5, 10, 20, 3], 0.01, 1000], [1.130526, 33.612663, 120])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 21', [[40, 10, 5], 0.05, 100], [2.518182, 21.841155, 14]),
  ('cluster sample 36', [[5, 10, 20, 3], 0.01, 1000], [1.130526, 33.612663, 120]),
  ('cluster sample 48', [[10, 3, 20, 3, 20], 0.05, 500], [1.769643, 31.644803, 80])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
unequal cluster sizes inflate the design effect[2.54375, 31.449631, 25][2.54375, 31.449631, 64]Failed
equal clusters[1.9, 15.789474, 30][1.9, 15.789474, 57]Failed
zero icc means no inflation[1.0, 43.0, 5][1.0, 43.0, 5]Passed
single cluster[5.8, 4.310345, 4][5.8, 4.310345, 24]Failed
small clusters[1.7, 3.529412, 17][1.7, 3.529412, 29]Failed
cluster sample 1[1.323333, 136.020151, 17][1.323333, 136.020151, 23]Failed
cluster sample 2[1.65814, 25.932679, 94][1.65814, 25.932679, 155]Failed
cluster sample 3[1.303636, 42.189679, 28][1.303636, 42.189679, 36]Failed

SHA-256 / be49e14c21ffa369fd8e5c513a75e2c36d024bfac9b3782fc0ff30081b5db4c8

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cluster_sizes, icc, n_required):
    k = len(cluster_sizes)
    if k == 0:
        return None
    total = sum(cluster_sizes)
    mbar = total / k
    if mbar == 0:
        return None
    var = sum((s - mbar) ** 2 for s in cluster_sizes) / k
    cv2 = var / mbar ** 2
    deff = 1 + ((cv2 + 1) * mbar - 1) * icc
    return [round(deff, 6), round(total / deff, 6), math.ceil(n_required * deff / mbar)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 1', [[40, 20, 40, 20, 20, 40], 0.01, 500], [1.323333, 136.020151, 23]),
  ('cluster sample 2', [[3, 10, 10, 20], 0.05, 1000], [1.65814, 25.932679, 155]),
  ('cluster sample 3', [[40, 10, 5], 0.01, 500], [1.303636, 42.189679, 36])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 4', [[20, 10, 20, 5, 5, 3], 0.2, 100], [3.844444, 16.387283, 37]),
  ('cluster sample 6', [[40], 0.2, 1000], [8.8, 4.545455, 220]),
  ('cluster sample 10', [[40], 0.0, 500], [1.0, 40.0, 13])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 11', [[40], 0.01, 500], [1.39, 28.776978, 18]),
  ('cluster sample 17', [[20, 40, 3, 20, 5], 0.05, 100], [2.332955, 37.720409, 14]),
  ('cluster sample 19', [[20, 40, 20], 0.2, 1000], [6.8, 11.764706, 256])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 16', [[3, 20, 10, 5, 10, 40], 0.0, 500], [1.0, 88.0, 35]),
  ('cluster sample 25', [[10, 5, 3, 40], 0.05, 100], [2.444828, 23.723554, 17]),
  ('cluster sample 36', [[5, 10, 20, 3], 0.01, 1000], [1.130526, 33.612663, 120])],
 [('unequal cluster sizes inflate the design effect', [[5, 10, 20, 45], 0.05, 500], [2.54375, 31.449631, 64]),
  ('equal clusters', [[10, 10, 10], 0.1, 300], [1.9, 15.789474, 57]),
  ('zero icc means no inflation', [[3, 40], 0.0, 100], [1.0, 43.0, 5]),
  ('single cluster', [[25], 0.2, 100], [5.8, 4.310345, 24]),
  ('small clusters', [[2, 4], 0.3, 50], [1.7, 3.529412, 29]),
  ('cluster sample 21', [[40, 10, 5], 0.05, 100], [2.518182, 21.841155, 14]),
  ('cluster sample 36', [[5, 10, 20, 3], 0.01, 1000], [1.130526, 33.612663, 120]),
  ('cluster sample 48', [[10, 3, 20, 3, 20], 0.05, 500], [1.769643, 31.644803, 80])]]
for label, args, expected in fixtures[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
unequal cluster sizes inflate the design effect[2.54375, 31.449631, 64][2.54375, 31.449631, 64]Passed
equal clusters[1.9, 15.789474, 57][1.9, 15.789474, 57]Passed
zero icc means no inflation[1.0, 43.0, 5][1.0, 43.0, 5]Passed
single cluster[5.8, 4.310345, 24][5.8, 4.310345, 24]Passed
small clusters[1.7, 3.529412, 29][1.7, 3.529412, 29]Passed
cluster sample 1[1.323333, 136.020151, 23][1.323333, 136.020151, 23]Passed
cluster sample 2[1.65814, 25.932679, 155][1.65814, 25.932679, 155]Passed
cluster sample 3[1.303636, 42.189679, 36][1.303636, 42.189679, 36]Passed

SHA-256 / 31f4be182df52b773b28d1e79859efae6623534e79a2732dcdec4a690ac29691

Verification & scope

A deterministic toy experiment-analysis model with a stipulated contract; results are rounded and are not a substitute for a validated statistics package. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:48:59.258420+00:00.

Case digest / 061c898699f03b2209fdd7cd007379a50d5213ce8decdc93ed2157ebc9a47b98