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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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