Spaces:
Running
Running
| /** | |
| * Wilson score interval for a binomial proportion. | |
| * | |
| * Re-expressed from CRISPResso2's allele-frequency CI columns | |
| * (docs/research/perception-bio-synthesis-2026.md §2). No external | |
| * dependency; numerical kernel is pure. | |
| * | |
| * Wilson is preferred over the Wald (normal-approximation) interval | |
| * because it stays inside [0, 1] for small counts and small/large | |
| * proportions — the exact regime where edit-call tables live. | |
| */ | |
| /** z critical value for a two-sided confidence level. */ | |
| const Z = { | |
| '0.90': 1.6448536269514722, | |
| '0.95': 1.959963984540054, | |
| '0.99': 2.5758293035489004, | |
| } as const; | |
| export type ConfidenceLevel = keyof typeof Z; | |
| export interface WilsonInterval { | |
| /** Point estimate `successes / trials`. */ | |
| readonly p: number; | |
| /** Lower bound ∈ [0, 1]. */ | |
| readonly ciLower: number; | |
| /** Upper bound ∈ [0, 1]. */ | |
| readonly ciUpper: number; | |
| /** Confidence level used. */ | |
| readonly level: ConfidenceLevel; | |
| } | |
| /** | |
| * Wilson score interval. `trials` must be ≥ 0, `successes` must be in | |
| * `[0, trials]`. A degenerate `trials === 0` returns `[0, 1]` — the | |
| * widest honest interval — rather than `NaN`. This mirrors the | |
| * CRISPResso2 lesson that absence-of-data must not be silently | |
| * collapsed. | |
| */ | |
| export function wilsonInterval( | |
| successes: number, | |
| trials: number, | |
| level: ConfidenceLevel = '0.95', | |
| ): WilsonInterval { | |
| if (!Number.isFinite(successes) || !Number.isFinite(trials) || trials < 0 || successes < 0 || successes > trials) { | |
| throw new Error(`wilsonInterval: invalid inputs successes=${successes} trials=${trials}`); | |
| } | |
| if (trials === 0) { | |
| return { p: 0, ciLower: 0, ciUpper: 1, level }; | |
| } | |
| const z = Z[level]; | |
| const n = trials; | |
| const p = successes / n; | |
| const z2 = z * z; | |
| const denom = 1 + z2 / n; | |
| const centre = (p + z2 / (2 * n)) / denom; | |
| const half = (z * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n))) / denom; | |
| return { | |
| p, | |
| ciLower: Math.max(0, centre - half), | |
| ciUpper: Math.min(1, centre + half), | |
| level, | |
| }; | |
| } | |