FA-74366 / Experiment statistics / Open access
Conversion rate z-test: The pooled rate averages the two arm rates · case 01
With unequal arm sizes the null variance is wrong and p-values drift.
ROOT CAUSE
pool is (p_a + p_b) / 2 instead of total conversions over total users.
VERIFIED REPAIR
Pool conversions and users across arms.
Unsuccessful approach: Using the control rate as the pool ignores treatment data under the null.
Case contract
Pooled two-proportion z-test: pool = (conv_a + conv_b) / (n_a + n_b), se = sqrt(pool(1 - pool)(1/n_a + 1/n_b)), z = (p_b - p_a) / se, two-sided p = erfc(|z| / sqrt 2). Nonpositive n -> None; se = 0 -> [0.0, 1.0]. Return [round(z, 6), round(p, 6)].
Why this case matters
Online experiment readouts drive launch decisions; a silent formula slip flips conclusions.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(conv_a, n_a, conv_b, n_b):
if n_a <= 0 or n_b <= 0:
return None
pa, pb = conv_a / n_a, conv_b / n_b
pool = (pa + pb) / 2
se = math.sqrt(pool * (1 - pool) * (1 / n_a + 1 / n_b))
if se == 0:
return [0.0, 1.0]
z = (pb - pa) / se
p = math.erfc(abs(z) / math.sqrt(2))
return [round(z, 6), round(p, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('conversion sample 1', [88, 1000, 75, 500], [3.637163, 0.000276]),
('conversion sample 2', [446, 4000, 43, 2500], [-14.022988, 0.0]),
('conversion sample 3', [86, 500, 18, 100], [0.192927, 0.847016])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('conversion sample 5', [14, 100, 17, 100], [0.586154, 0.557772]),
('conversion sample 9', [18, 100, 81, 500], [-0.442687, 0.657992])],
[('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 11', [263, 4000, 78, 1000], [1.374446, 0.169303]),
('conversion sample 12', [5, 100, 238, 1000], [4.320765, 1.6e-05]),
('conversion sample 18', [24, 100, 97, 500], [-1.046545, 0.295309])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 16', [200, 1000, 1, 100], [-4.687832, 3e-06]),
('conversion sample 19', [211, 1000, 115, 1000], [-5.811653, 0.0]),
('conversion sample 28', [69, 1000, 8, 100], [0.411061, 0.681028])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 21', [20, 100, 104, 500], [0.180358, 0.856871]),
('conversion sample 26', [128, 4000, 22, 100], [9.890895, 0.0]),
('conversion sample 38', [323, 4000, 231, 1000], [13.539102, 0.0])]]
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 arm sizes weight the pooled rate | [-2.795085, 0.005189] | [-2.98032, 0.002879] | Failed |
| treatment better gives positive z | [2.102741, 0.035488] | [2.102741, 0.035488] | Passed |
| treatment worse gives negative z | [-2.102741, 0.035488] | [-2.102741, 0.035488] | Passed |
| no conversions anywhere | [0.0, 1.0] | [0.0, 1.0] | Passed |
| all conversions everywhere | [0.0, 1.0] | [0.0, 1.0] | Passed |
| conversion sample 1 | [3.495986, 0.000472] | [3.637163, 0.000276] | Failed |
| conversion sample 2 | [-15.073844, 0.0] | [-14.022988, 0.0] | Failed |
| conversion sample 3 | [0.191769, 0.847923] | [0.192927, 0.847016] | Failed |
SHA-256 / aa3901c51a0c4b15c7d8764f8cbf58dcda2bfef7ae36a6a29930067618ca40e7
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(conv_a, n_a, conv_b, n_b):
if n_a <= 0 or n_b <= 0:
return None
pa, pb = conv_a / n_a, conv_b / n_b
pool = pa
se = math.sqrt(pool * (1 - pool) * (1 / n_a + 1 / n_b))
if se == 0:
return [0.0, 1.0]
z = (pb - pa) / se
p = math.erfc(abs(z) / math.sqrt(2))
return [round(z, 6), round(p, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('conversion sample 1', [88, 1000, 75, 500], [3.637163, 0.000276]),
('conversion sample 2', [446, 4000, 43, 2500], [-14.022988, 0.0]),
('conversion sample 3', [86, 500, 18, 100], [0.192927, 0.847016])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('conversion sample 5', [14, 100, 17, 100], [0.586154, 0.557772]),
('conversion sample 9', [18, 100, 81, 500], [-0.442687, 0.657992])],
[('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 11', [263, 4000, 78, 1000], [1.374446, 0.169303]),
('conversion sample 12', [5, 100, 238, 1000], [4.320765, 1.6e-05]),
('conversion sample 18', [24, 100, 97, 500], [-1.046545, 0.295309])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 16', [200, 1000, 1, 100], [-4.687832, 3e-06]),
('conversion sample 19', [211, 1000, 115, 1000], [-5.811653, 0.0]),
('conversion sample 28', [69, 1000, 8, 100], [0.411061, 0.681028])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 21', [20, 100, 104, 500], [0.180358, 0.856871]),
('conversion sample 26', [128, 4000, 22, 100], [9.890895, 0.0]),
('conversion sample 38', [323, 4000, 231, 1000], [13.539102, 0.0])]]
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 arm sizes weight the pooled rate | [-2.513123, 0.011967] | [-2.98032, 0.002879] | Failed |
| treatment better gives positive z | [2.236068, 0.025347] | [2.102741, 0.035488] | Failed |
| treatment worse gives negative z | [-1.994688, 0.046077] | [-2.102741, 0.035488] | Failed |
| no conversions anywhere | [0.0, 1.0] | [0.0, 1.0] | Passed |
| all conversions everywhere | [0.0, 1.0] | [0.0, 1.0] | Passed |
| conversion sample 1 | [3.995695, 6.5e-05] | [3.637163, 0.000276] | Failed |
| conversion sample 2 | [-11.75138, 0.0] | [-14.022988, 0.0] | Failed |
| conversion sample 3 | [0.193517, 0.846554] | [0.192927, 0.847016] | Failed |
SHA-256 / 22114c19ec76bc3f4ad97b053dd489b7e2b40fb4d5601660be80d8add5ac06ce
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(conv_a, n_a, conv_b, n_b):
if n_a <= 0 or n_b <= 0:
return None
pa, pb = conv_a / n_a, conv_b / n_b
pool = (conv_a + conv_b) / (n_a + n_b)
se = math.sqrt(pool * (1 - pool) * (1 / n_a + 1 / n_b))
if se == 0:
return [0.0, 1.0]
z = (pb - pa) / se
p = math.erfc(abs(z) / math.sqrt(2))
return [round(z, 6), round(p, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('conversion sample 1', [88, 1000, 75, 500], [3.637163, 0.000276]),
('conversion sample 2', [446, 4000, 43, 2500], [-14.022988, 0.0]),
('conversion sample 3', [86, 500, 18, 100], [0.192927, 0.847016])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('conversion sample 5', [14, 100, 17, 100], [0.586154, 0.557772]),
('conversion sample 9', [18, 100, 81, 500], [-0.442687, 0.657992])],
[('treatment worse gives negative z', [130, 1000, 100, 1000], [-2.102741, 0.035488]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 11', [263, 4000, 78, 1000], [1.374446, 0.169303]),
('conversion sample 12', [5, 100, 238, 1000], [4.320765, 1.6e-05]),
('conversion sample 18', [24, 100, 97, 500], [-1.046545, 0.295309])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('no conversions anywhere', [0, 500, 0, 500], [0.0, 1.0]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 16', [200, 1000, 1, 100], [-4.687832, 3e-06]),
('conversion sample 19', [211, 1000, 115, 1000], [-5.811653, 0.0]),
('conversion sample 28', [69, 1000, 8, 100], [0.411061, 0.681028])],
[('unequal arm sizes weight the pooled rate', [50, 1000, 90, 3000], [-2.98032, 0.002879]),
('treatment better gives positive z', [100, 1000, 130, 1000], [2.102741, 0.035488]),
('all conversions everywhere', [200, 200, 300, 300], [0.0, 1.0]),
('identical rates', [40, 400, 80, 800], [0.0, 1.0]),
('empty arm', [0, 0, 5, 10], None),
('conversion sample 21', [20, 100, 104, 500], [0.180358, 0.856871]),
('conversion sample 26', [128, 4000, 22, 100], [9.890895, 0.0]),
('conversion sample 38', [323, 4000, 231, 1000], [13.539102, 0.0])]]
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 arm sizes weight the pooled rate | [-2.98032, 0.002879] | [-2.98032, 0.002879] | Passed |
| treatment better gives positive z | [2.102741, 0.035488] | [2.102741, 0.035488] | Passed |
| treatment worse gives negative z | [-2.102741, 0.035488] | [-2.102741, 0.035488] | Passed |
| no conversions anywhere | [0.0, 1.0] | [0.0, 1.0] | Passed |
| all conversions everywhere | [0.0, 1.0] | [0.0, 1.0] | Passed |
| conversion sample 1 | [3.637163, 0.000276] | [3.637163, 0.000276] | Passed |
| conversion sample 2 | [-14.022988, 0.0] | [-14.022988, 0.0] | Passed |
| conversion sample 3 | [0.192927, 0.847016] | [0.192927, 0.847016] | Passed |
SHA-256 / e153251384bc7ff227848c8166d7c3c9589f98d7a8046f90ef6d732ce72cdf46
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:56.008627+00:00.
Case digest / 75cb767e904d046d34d289b40ecfb97f5abf0c44ec031849d844ce28b2772082