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/CHANGELOG.md +59 -0
- package/README.md +31 -6
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +11 -8
- package/dist/check.js.map +1 -1
- package/dist/cluster.d.ts.map +1 -1
- package/dist/cluster.js +24 -4
- package/dist/cluster.js.map +1 -1
- package/dist/gate.d.ts +18 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +32 -3
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/plan.d.ts +4 -3
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js.map +1 -1
- package/dist/stats.d.ts +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +54 -3
- package/dist/stats.js.map +1 -1
- package/package.json +11 -2
- package/src/check.ts +12 -8
- package/src/cluster.ts +29 -4
- package/src/gate.ts +50 -3
- package/src/index.ts +5 -0
- package/src/plan.ts +4 -3
- package/src/stats.ts +68 -3
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
|
|
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
|
-
|
|
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);
|