S1 · Wilson score interval¶
- Author: Krishna Nandimandalam
- Date: 2026-10-09
- Card: #5 S1 · PR: TBD
What I built¶
A Wilson score confidence interval for binomial proportions: wilson_interval(k, n, *, confidence=0.95) → Estimate.
Also created private helpers in src/evalhawk/stats/_checks.py for validation: check_confidence, z_value.
Main signature:
def wilson_interval(k: int, n: int, *, confidence: float = 0.95) -> Estimate:
"""Compute a Wilson score confidence interval for a proportion."""
The idea, simply¶
The Wald interval (p̂ ± z·√(p̂(1−p̂)/n)) has a famous bug: when you observe 0 successes in 10 trials, it gives [0, 0]—false
certainty that the true rate is exactly 0. It can also leave [0, 1], claiming the parameter isn't bounded.
Wilson's fix (1927) pulls the centre of the interval toward 0.5, making it narrower when the data is sparse. The formula involves a z-score based on the confidence level and some algebraic rearrangement. The result is guaranteed to stay in [0, 1] and is more accurate for all proportions and sample sizes.
How I know it's right¶
- Golden values: Four reference cases (82/100, 0/10, 10/10, 500/1000) match published tables to 5 decimals.
- Property tests (Hypothesis, 200+ cases per property):
- Bounds always satisfy 0 ≤ low ≤ high ≤ 1.
- Symmetry: the interval for k successes mirrors the interval for (n−k) successes.
- Narrowing: doubling sample size consistently narrows the interval.
- Error cases: TypeError rejects bools, ValueError rejects floats, out-of-range k, bad confidence (0 or 1 or negative).
- Metadata: method="wilson", n matches input, confidence and unknown_rate defaults are correct.
- String format: matches the spec exactly:
"0.820 [0.733, 0.883] (wilson, n=100, unknown=0.0%)".
What surprised me / gotchas¶
-
Bool rejection comes first. Python treats
Trueas 1 (it's a subclass ofint), so I had to checkisinstance(k, bool)before other int checks. Tests verify the error message names "bool", not just "not int". -
Float noise at bounds. The formula can produce slightly negative low or slightly-over-1 high due to floating-point rounding. I clamp with
max(0.0, ...)andmin(1.0, ...), knowing Estimate's post-init will never fail because the values are within [0, 1]. -
Symmetry property is delicate. Testing
low(k) ≈ 1 - high(n−k)requires usingpytest.approxwith a loose tolerance (1e-10 at the float64 level). The property holds exactly in theory but not in floating-point practice. -
Confidence-level computation. The z-score is computed via
statistics.NormalDist().inv_cdf((1 + confidence) / 2), which is the inverse CDF of the standard normal at the cumulative probability. For confidence=0.95, this gives z ≈ 1.96. I verified this against standard tables.
How AI helped (and where it was wrong)¶
Left for the maintainer to fill in.
Learn more¶
- Wilson, E. B. (1927). "Probable inference, the law of succession, and statistical inference." Journal of the American Statistical Association, 22(158), 209–212.
- Brown, L. D., Cai, T. T., & DasGupta, A. (2001). "Interval estimation for a binomial proportion." Statistical Science, 16(2), 101–133.
- Card #5 S1 in workplan
- Standards §5: Pure functions in stats/
- Architecture §7: Statistics core