Spaces:
Running
Running
File size: 2,029 Bytes
518343a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | /**
* 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,
};
}
|