| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/splitstream/current/scripts/ |
Upload File : |
"""
stats/corpus/stats.json generator.
Produces a deterministic corpus of (sample, expected-result) tuples covering:
- Beta-Binomial conjugate updates at sample sizes from 10 to 1,000,000.
- Posterior CDF + quantile values for credible-interval computation across
Beta(α, β) with α, β ∈ [1, 1e6], including extreme-tail params like
Beta(2, 100000).
- Monte-Carlo P(A > B) for paired Beta distributions with a pinned RNG seed.
- Normal-Normal updates at varying prior strengths.
- Chi-squared p-value for 2x2 and 4x2 contingency tables for SRM.
The PHP analysis runs against the corpus in PHPUnit using math-php; the
TypeScript SDK runs the subset relevant to it in Vitest. Drift fails CI.
Pinned: scipy==1.13.*, numpy==1.26.*, Python 3.12.
"""
import json
import math
from pathlib import Path
import numpy as np
import scipy.stats as stats
def beta_binomial_rows():
"""Conjugate updates: posterior alpha/beta after observing (successes,
trials) on a Beta(1, 1) prior."""
cases = [
(10, 100), (50, 1000), (100, 10000), (500, 5000),
(1, 1), (0, 100), (100, 100),
(5000, 50000), (250000, 500000), (200000, 1000000),
]
rows = []
for successes, trials in cases:
rows.append({
"name": f"BetaBinomial s={successes} n={trials}",
"prior_alpha": 1.0,
"prior_beta": 1.0,
"successes": successes,
"trials": trials,
"expected_alpha": 1.0 + successes,
"expected_beta": 1.0 + (trials - successes),
})
return rows
def beta_quantile_rows():
cases = [
(50, 50, 0.025), (50, 50, 0.975), (50, 50, 0.5),
(1, 1, 0.5),
(1001, 9001, 0.025), (1001, 9001, 0.975),
(2, 100000, 0.5), (2, 100000, 0.975),
(10001, 10001, 0.025), (10001, 10001, 0.975),
(100, 9900, 0.025), (100, 9900, 0.975),
]
rows = []
for a, b, q in cases:
rows.append({
"name": f"Beta({a},{b}).ppf({q})",
"alpha": a,
"beta": b,
"quantile": q,
"expected_value": float(stats.beta.ppf(q, a, b)),
})
return rows
def beta_cdf_rows():
cases = [
(50, 50, 0.4026979166),
(50, 50, 0.5973020834),
(1001, 9001, 0.0942751131),
(1001, 9001, 0.1060363320),
(2, 100000, 0.0000557146),
]
rows = []
for a, b, x in cases:
rows.append({
"name": f"Beta({a},{b}).cdf({x})",
"alpha": a,
"beta": b,
"x": x,
"expected_value": float(stats.beta.cdf(x, a, b)),
})
return rows
def monte_carlo_rows():
"""P(B > A) for paired Beta distributions, computed with a pinned seed."""
rng = np.random.default_rng(42)
cases = [
# (alphaA, betaA, alphaB, betaB) — control vs treatment posteriors
(101, 901, 105, 895), # treatment slight edge
(1001, 9001, 1051, 8951), # treatment moderate edge, n~10k
(501, 501, 505, 497), # near-tie
(101, 9901, 200, 9800), # rare positive, larger treatment
]
rows = []
n_samples = 200_000
for alphaA, betaA, alphaB, betaB in cases:
a_draws = rng.beta(alphaA, betaA, size=n_samples)
b_draws = rng.beta(alphaB, betaB, size=n_samples)
p_b_gt_a = float(np.mean(b_draws > a_draws))
rows.append({
"name": f"MC P(B>A) Beta({alphaA},{betaA}) vs Beta({alphaB},{betaB})",
"alpha_a": alphaA,
"beta_a": betaA,
"alpha_b": alphaB,
"beta_b": betaB,
"expected_p_b_gt_a": p_b_gt_a,
"n_samples": n_samples,
"tolerance_atol": 0.02, # MC inherently has variance; PHP MC
# uses a different RNG and 10k draws
# vs the corpus's 200k → ~5e-3 SE,
# 2e-2 leaves headroom
})
return rows
def chi_squared_rows():
"""SRM chi-squared p-values."""
cases = [
# (observed, expected_weights)
([5050, 4950], [0.5, 0.5]), # near-balanced
([5000, 5000], [0.5, 0.5]), # exactly balanced
([4800, 5200], [0.5, 0.5]), # slight imbalance
([3500, 3500, 3000], [0.33, 0.33, 0.34]), # 3-way
([2500, 2500, 2500, 2500], [0.25, 0.25, 0.25, 0.25]), # 4-way
([5500, 4500], [0.5, 0.5]), # SRM warning territory
]
rows = []
for observed, weights in cases:
n = sum(observed)
weight_sum = sum(weights)
chi2 = 0.0
for obs, w in zip(observed, weights):
expected = (w / weight_sum) * n
if expected > 0:
chi2 += (obs - expected) ** 2 / expected
df = len(observed) - 1
p_value = float(stats.chi2.sf(chi2, df))
rows.append({
"name": f"SRM chi2 {observed} expect {weights}",
"observed": observed,
"expected_weights": weights,
"expected_chi_squared": chi2,
"expected_p_value": p_value,
})
return rows
def main():
corpus = {
"schema_version": 1,
"description": "Cross-implementation stats engine corpus. PHP (math-php) and TS SDK round-trip these tuples in CI. Drift fails the build.",
"generated_with": {
"scipy": stats.__name__ + "==1.13.*",
"numpy": "1.26.*",
"python": "3.12",
"rng_seed": 42,
},
"tolerances": {
"conjugate_update": 1e-9,
"quantile": 1e-7,
"cdf": 1e-9,
"monte_carlo": 2e-2,
"chi_squared": 1e-7,
},
"beta_binomial_updates": beta_binomial_rows(),
"beta_quantiles": beta_quantile_rows(),
"beta_cdfs": beta_cdf_rows(),
"monte_carlo": monte_carlo_rows(),
"chi_squared": chi_squared_rows(),
}
output = Path(__file__).parent.parent / "stats" / "corpus" / "stats.json"
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(corpus, indent=2))
print(f"Wrote {output}")
print(f" beta_binomial_updates: {len(corpus['beta_binomial_updates'])}")
print(f" beta_quantiles: {len(corpus['beta_quantiles'])}")
print(f" beta_cdfs: {len(corpus['beta_cdfs'])}")
print(f" monte_carlo: {len(corpus['monte_carlo'])}")
print(f" chi_squared: {len(corpus['chi_squared'])}")
if __name__ == "__main__":
main()