peeksafe 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/stats.ts CHANGED
@@ -115,13 +115,30 @@ export function gammaP(a: number, x: number): number {
115
115
  * approximation is visible in a p-value and we compare p-values to thresholds.
116
116
  */
117
117
  export function erf(x: number): number {
118
+ requireNumber(x, "x", "erf");
118
119
  if (x === 0) return 0;
119
120
  const p = gammaP(0.5, x * x);
120
121
  return x > 0 ? p : -p;
121
122
  }
122
123
 
123
124
  /** Φ(x): standard normal CDF. */
124
- export const normalCdf = (x: number): number => 0.5 * (1 + erf(x / Math.SQRT2));
125
+ export function normalCdf(x: number): number {
126
+ requireNumber(x, "x", "normalCdf");
127
+ return 0.5 * (1 + erf(x / Math.SQRT2));
128
+ }
129
+
130
+ /** Refuses NaN only: erf and normalCdf answer the infinities as their limits. */
131
+ function requireNumber(x: number, name: string, where: string): void {
132
+ if (Number.isNaN(x)) {
133
+ throw new PeeksafeError(
134
+ "PEEKSAFE_E_STAT_DOMAIN",
135
+ `${where}: ${name} must be a number, got NaN`,
136
+ {
137
+ detail: { [name]: x },
138
+ },
139
+ );
140
+ }
141
+ }
125
142
 
126
143
  /** Φ⁻¹(p): Acklam's inverse normal CDF, refined by one Halley step. */
127
144
  export function normalQuantile(p: number): number {
@@ -330,10 +347,16 @@ export function betaPosterior(
330
347
  prior: { alpha: number; beta: number } = { alpha: 1, beta: 1 },
331
348
  ): BetaPosterior {
332
349
  requireCounts(successes, successes + failures, "betaPosterior");
333
- if (!(prior.alpha > 0) || !(prior.beta > 0)) {
350
+ // An infinite shape passed `> 0` and came back as NaN moments.
351
+ if (
352
+ !(prior.alpha > 0) ||
353
+ !(prior.beta > 0) ||
354
+ !Number.isFinite(prior.alpha) ||
355
+ !Number.isFinite(prior.beta)
356
+ ) {
334
357
  throw new PeeksafeError(
335
358
  "PEEKSAFE_E_STAT_DOMAIN",
336
- "betaPosterior: prior parameters must be positive",
359
+ "betaPosterior: prior parameters must be finite and positive",
337
360
  {
338
361
  detail: { prior },
339
362
  },
@@ -424,8 +447,27 @@ export function tQuantile(p: number, df: number): number {
424
447
  return Math.sqrt((df * (1 - x)) / x);
425
448
  }
426
449
 
450
+ // A posterior built by hand reached betaQuantile or ibeta unchecked and was refused in their name.
451
+ function requirePosteriorShape(post: BetaPosterior, where: string): void {
452
+ if (
453
+ !(post.alpha > 0) ||
454
+ !(post.beta > 0) ||
455
+ !Number.isFinite(post.alpha) ||
456
+ !Number.isFinite(post.beta)
457
+ ) {
458
+ throw new PeeksafeError(
459
+ "PEEKSAFE_E_STAT_DOMAIN",
460
+ `${where}: posterior alpha and beta must be finite and positive`,
461
+ {
462
+ detail: { alpha: post.alpha, beta: post.beta },
463
+ },
464
+ );
465
+ }
466
+ }
467
+
427
468
  /** Equal-tailed credible interval of a Beta posterior. */
428
469
  export function betaCredibleInterval(post: BetaPosterior, level = 0.95): Interval {
470
+ requirePosteriorShape(post, "betaCredibleInterval");
429
471
  // betaQuantile clamps q to [0, 1], so a level of 95 or -1 came back as [0, 1] or [1, 0].
430
472
  requireOpenProbability(level, "level", "betaCredibleInterval");
431
473
  const tail = (1 - level) / 2;
@@ -438,6 +480,7 @@ export function betaCredibleInterval(post: BetaPosterior, level = 0.95): Interva
438
480
 
439
481
  /** P(a < θ < b) under a Beta posterior. */
440
482
  export function betaMassBetween(post: BetaPosterior, a: number, b: number): number {
483
+ requirePosteriorShape(post, "betaMassBetween");
441
484
  requireProbability(a, "a", "betaMassBetween");
442
485
  requireProbability(b, "b", "betaMassBetween");
443
486
  if (a > b) {
@@ -1070,6 +1113,8 @@ export function bernoulliEntropy(p: number): number {
1070
1113
  /** log of the Beta(a,b) density at p. */
1071
1114
  export function logBetaPdf(p: number, a: number, b: number): number {
1072
1115
  requireProbability(p, "p", "logBetaPdf");
1116
+ requireConcentration(a, "a", "logBetaPdf");
1117
+ requireConcentration(b, "b", "logBetaPdf");
1073
1118
  if (p <= 0 || p >= 1) {
1074
1119
  // density is 0 or ∞ at the boundary depending on the shape; the planner
1075
1120
  // only ever evaluates strictly inside, so clamp rather than return ±∞.
@@ -1145,6 +1190,9 @@ export function evidenceCeilingLogE(
1145
1190
  altConcentration = 8,
1146
1191
  ): number {
1147
1192
  requireProbability(pTrue, "pTrue", "evidenceCeilingLogE");
1193
+ requireCounts(baselineSuccesses, baselineTrials, "evidenceCeilingLogE");
1194
+ requireConcentration(altConcentration, "altConcentration", "evidenceCeilingLogE");
1195
+ requireOpenProbability(mde, "mde", "evidenceCeilingLogE");
1148
1196
  const { nullPrior, altPrior } = twoSamplePriors(
1149
1197
  baselineSuccesses,
1150
1198
  baselineTrials,
@@ -1212,6 +1260,8 @@ export function evidenceCeilingAsymptotic(
1212
1260
  ): number {
1213
1261
  requireCounts(baselineSuccesses, baselineTrials, "evidenceCeilingAsymptotic");
1214
1262
  requireProbability(pTrue, "pTrue", "evidenceCeilingAsymptotic");
1263
+ requireConcentration(altConcentration, "altConcentration", "evidenceCeilingAsymptotic");
1264
+ requireOpenProbability(mde, "mde", "evidenceCeilingAsymptotic");
1215
1265
  if (baselineTrials === 0) {
1216
1266
  throw new PeeksafeError(
1217
1267
  "PEEKSAFE_E_STAT_DOMAIN",
@@ -1273,6 +1323,9 @@ export function expectedLogE(
1273
1323
  detail: { n },
1274
1324
  });
1275
1325
  }
1326
+ requireCounts(baselineSuccesses, baselineTrials, "expectedLogE");
1327
+ requireConcentration(altConcentration, "altConcentration", "expectedLogE");
1328
+ requireOpenProbability(mde, "mde", "expectedLogE");
1276
1329
  const { nullPrior: n0, altPrior: a1 } = twoSamplePriors(
1277
1330
  baselineSuccesses,
1278
1331
  baselineTrials,
@@ -1306,6 +1359,9 @@ export function expectedLogEExact(
1306
1359
  { detail: { n } },
1307
1360
  );
1308
1361
  }
1362
+ requireCounts(baselineSuccesses, baselineTrials, "expectedLogEExact");
1363
+ requireConcentration(altConcentration, "altConcentration", "expectedLogEExact");
1364
+ requireOpenProbability(mde, "mde", "expectedLogEExact");
1309
1365
  let acc = 0;
1310
1366
  for (let s = 0; s <= n; s++) {
1311
1367
  const logW =
@@ -1338,6 +1394,10 @@ export function samplesForEvidence(
1338
1394
  ): number {
1339
1395
  requireSearchCap(maxN, "maxN", "samplesForEvidence");
1340
1396
  requireLogThreshold(logThreshold, "samplesForEvidence");
1397
+ requireProbability(pTrue, "pTrue", "samplesForEvidence");
1398
+ requireCounts(baselineSuccesses, baselineTrials, "samplesForEvidence");
1399
+ requireConcentration(altConcentration, "altConcentration", "samplesForEvidence");
1400
+ requireOpenProbability(mde, "mde", "samplesForEvidence");
1341
1401
  const ceiling = evidenceCeilingLogE(
1342
1402
  pTrue,
1343
1403
  baselineSuccesses,
@@ -1406,6 +1466,8 @@ export function universalTwoSampleLogE(
1406
1466
  ): number {
1407
1467
  requireCounts(candidateSuccesses, candidateTrials, "universalTwoSampleLogE(candidate)");
1408
1468
  requireCounts(baselineSuccesses, baselineTrials, "universalTwoSampleLogE(baseline)");
1469
+ requireConcentration(altConcentration, "altConcentration", "universalTwoSampleLogE");
1470
+ requireOpenProbability(mde, "mde", "universalTwoSampleLogE");
1409
1471
  if (baselineTrials === 0) {
1410
1472
  throw new PeeksafeError(
1411
1473
  "PEEKSAFE_E_STAT_DOMAIN",
@@ -1586,6 +1648,9 @@ export function mcnemarSamplesForEvidence(
1586
1648
  requireConcentration(concentration, "concentration", "mcnemarSamplesForEvidence");
1587
1649
  requireSearchCap(maxPairs, "maxPairs", "mcnemarSamplesForEvidence");
1588
1650
  requireLogThreshold(logThreshold, "mcnemarSamplesForEvidence");
1651
+ requireProbability(pBaseline, "pBaseline", "mcnemarSamplesForEvidence");
1652
+ requireProbability(rho, "rho", "mcnemarSamplesForEvidence");
1653
+ requireOpenProbability(mde, "mde", "mcnemarSamplesForEvidence");
1589
1654
  const { rate, theta } = pairedDiscordance(pBaseline, mde, rho);
1590
1655
  if (rate <= 0 || theta <= 0.5) return Infinity;
1591
1656
  const a1 = Math.max(0.35, concentration * theta);