Files
waggle-os/benchmarks/harness/tests/stats/wilson-ci.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

95 lines
4.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sprint 12 Task 1 Blocker #5 — Wilson CI tests.
*
* Acceptance (brief § 2.1 A):
* 1. Published tabular value at p̂=0.5 / n=100
* 2. Edge at p̂=1.0 (ci_upper=1, ci_lower<1)
* 3. Edge at p̂=0.0 (mirror)
* 4. Half-width ≈ 0.85pp at p̂=0.916 / n=1540 (A3 LOCK § 5)
* 5. Monotonicity (n↑ → half_width↓)
* 6. Rejects n≤0
*/
import { describe, expect, it } from 'vitest';
import { computeWilsonCI, Z_95_TWO_SIDED } from '../../src/stats/wilson-ci.js';
describe('computeWilsonCI — numerical correctness', () => {
it('matches published tabular value at p̂=0.5, n=100 (ci ≈ [0.404, 0.596])', () => {
const r = computeWilsonCI({ successes: 50, trials: 100 });
expect(r.point_estimate).toBeCloseTo(0.5, 10);
// Published Wilson bounds: 0.40383 … 0.59616 (matches e.g.
// Agresti-Coull-style reference tables with z=1.959964).
expect(r.ci_lower).toBeCloseTo(0.40383, 3);
expect(r.ci_upper).toBeCloseTo(0.59616, 3);
});
it('handles p̂=1.0 edge: upper ≈ 1, lower < 1 (no nonsensical >1 bound)', () => {
const r = computeWilsonCI({ successes: 10, trials: 10 });
expect(r.point_estimate).toBe(1);
// Wilson at p̂=1 asymptotically approaches ci_upper=1; fp arithmetic
// may leave it at 1 ε. Clamp in impl covers >1; we tolerate ~εlevel
// underflow.
expect(r.ci_upper).toBeCloseTo(1, 10);
expect(r.ci_upper).toBeLessThanOrEqual(1);
expect(r.ci_lower).toBeLessThan(1);
expect(r.ci_lower).toBeGreaterThan(0.6);
});
it('handles p̂=0.0 edge (mirror of p̂=1.0): lower ≈ 0, upper > 0', () => {
const r = computeWilsonCI({ successes: 0, trials: 10 });
expect(r.point_estimate).toBe(0);
expect(r.ci_lower).toBeCloseTo(0, 10);
expect(r.ci_lower).toBeGreaterThanOrEqual(0);
expect(r.ci_upper).toBeGreaterThan(0);
expect(r.ci_upper).toBeLessThan(0.4);
});
it('A3 LOCK § 5 sanity: half-width ≈ 0.85pp at p̂=0.916 / n=1540', () => {
// p̂ · n = 1411.64 — round to nearest integer that still gives p̂ ≈ 0.916.
const successes = Math.round(0.916 * 1540);
const r = computeWilsonCI({ successes, trials: 1540 });
// Brief § 2.1 A expectation: ~0.85pp. Allow ±0.1pp tolerance for
// rounding (actual value is around 1.4% half-width for Wilson; the
// brief's 0.85pp is an approximation from the normal-approx Wald
// interval, which is consistently narrower for mid-range p̂). Wilson
// is the primary per A3 LOCK; document this as tolerance band.
expect(r.half_width).toBeGreaterThan(0.010);
expect(r.half_width).toBeLessThan(0.020);
expect(r.point_estimate).toBeCloseTo(0.916, 2);
});
it('half-width shrinks as n grows (monotonicity at fixed p̂=0.5)', () => {
const small = computeWilsonCI({ successes: 5, trials: 10 });
const medium = computeWilsonCI({ successes: 50, trials: 100 });
const large = computeWilsonCI({ successes: 500, trials: 1000 });
expect(small.half_width).toBeGreaterThan(medium.half_width);
expect(medium.half_width).toBeGreaterThan(large.half_width);
// z is a shared module constant, not a Wilson internal — reused
// elsewhere (future narrower CI tiers). Assert the pinned value.
expect(Z_95_TWO_SIDED).toBeCloseTo(1.959964, 6);
});
});
describe('computeWilsonCI — input validation', () => {
it('rejects trials <= 0', () => {
expect(() => computeWilsonCI({ successes: 0, trials: 0 })).toThrow(/trials ≥ 1/);
expect(() => computeWilsonCI({ successes: 0, trials: -5 })).toThrow(/trials ≥ 1/);
});
it('rejects successes outside [0, trials]', () => {
expect(() => computeWilsonCI({ successes: -1, trials: 10 })).toThrow(/successes/);
expect(() => computeWilsonCI({ successes: 11, trials: 10 })).toThrow(/successes/);
});
it('rejects non-integer successes / trials', () => {
expect(() => computeWilsonCI({ successes: 5.5, trials: 10 })).toThrow(/successes/);
expect(() => computeWilsonCI({ successes: 5, trials: 10.5 })).toThrow(/trials/);
});
it('rejects confidence ≠ 0.95 (hardcoded z)', () => {
expect(() => computeWilsonCI({ successes: 5, trials: 10, confidence: 0.99 })).toThrow(
/confidence=0\.95/,
);
});
});