peeksafe 0.1.0 → 0.2.0
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 +180 -1
- package/README.md +3 -5
- package/dist/cluster.d.ts.map +1 -1
- package/dist/cluster.js +8 -0
- package/dist/cluster.js.map +1 -1
- package/dist/errors.d.ts +24 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +52 -3
- package/dist/errors.js.map +1 -1
- package/dist/frontier.d.ts +5 -3
- package/dist/frontier.d.ts.map +1 -1
- package/dist/frontier.js +78 -33
- package/dist/frontier.js.map +1 -1
- package/dist/gate.d.ts +2 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +5 -1
- package/dist/gate.js.map +1 -1
- package/dist/plan.d.ts +42 -10
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +176 -32
- package/dist/plan.js.map +1 -1
- package/dist/rand.d.ts.map +1 -1
- package/dist/rand.js +16 -4
- package/dist/rand.js.map +1 -1
- package/dist/stats.d.ts +7 -3
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +101 -42
- package/dist/stats.js.map +1 -1
- package/dist/stop.d.ts +1 -0
- package/dist/stop.d.ts.map +1 -1
- package/dist/stop.js +13 -5
- package/dist/stop.js.map +1 -1
- package/package.json +10 -11
- package/src/cluster.ts +12 -0
- package/src/errors.ts +70 -3
- package/src/frontier.ts +102 -47
- package/src/gate.ts +6 -1
- package/src/plan.ts +236 -46
- package/src/rand.ts +20 -4
- package/src/stats.ts +132 -48
- package/src/stop.ts +17 -5
package/src/stats.ts
CHANGED
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
import {
|
|
24
24
|
PeeksafeError,
|
|
25
25
|
requireCounts,
|
|
26
|
+
requireErrorRates,
|
|
26
27
|
requireOpenProbability,
|
|
28
|
+
requirePairedCounts,
|
|
27
29
|
requireProbability,
|
|
28
30
|
} from "./errors.js";
|
|
29
31
|
|
|
@@ -36,6 +38,16 @@ const LANCZOS = [
|
|
|
36
38
|
|
|
37
39
|
/** Lanczos approximation to log Γ(x), x > 0. ~15 significant digits. */
|
|
38
40
|
export function logGamma(x: number): number {
|
|
41
|
+
// Below 0 the reflection answered finite values at the poles, 37.78 at x = -1, and Infinity answered NaN.
|
|
42
|
+
if (!(x > 0) || x === Infinity) {
|
|
43
|
+
throw new PeeksafeError(
|
|
44
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
45
|
+
`logGamma: x must be finite and positive, got ${x}`,
|
|
46
|
+
{
|
|
47
|
+
detail: { x },
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}
|
|
39
51
|
if (x < 0.5) {
|
|
40
52
|
// reflection: Γ(x)Γ(1-x) = π / sin(πx)
|
|
41
53
|
return Math.log(Math.PI / Math.abs(Math.sin(Math.PI * x))) - logGamma(1 - x);
|
|
@@ -86,7 +98,14 @@ function gammaQContinued(a: number, x: number): number {
|
|
|
86
98
|
|
|
87
99
|
/** Regularized lower incomplete gamma P(a,x), accurate to ~1e-15. */
|
|
88
100
|
export function gammaP(a: number, x: number): number {
|
|
101
|
+
// A shape at or below 0 answered a negative probability, and x = Infinity fed Infinity - Infinity to exp.
|
|
102
|
+
if (!(a > 0) || !Number.isFinite(a) || Number.isNaN(x)) {
|
|
103
|
+
throw new PeeksafeError("PEEKSAFE_E_STAT_DOMAIN", "gammaP: need finite a > 0 and a number x", {
|
|
104
|
+
detail: { a, x },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
89
107
|
if (x <= 0) return 0;
|
|
108
|
+
if (x === Infinity) return 1;
|
|
90
109
|
return x < a + 1 ? gammaPSeries(a, x) : 1 - gammaQContinued(a, x);
|
|
91
110
|
}
|
|
92
111
|
|
|
@@ -106,6 +125,16 @@ export const normalCdf = (x: number): number => 0.5 * (1 + erf(x / Math.SQRT2));
|
|
|
106
125
|
|
|
107
126
|
/** Φ⁻¹(p): Acklam's inverse normal CDF, refined by one Halley step. */
|
|
108
127
|
export function normalQuantile(p: number): number {
|
|
128
|
+
// Every branch below compares false for NaN, so it fell through to the tail formula and came back NaN.
|
|
129
|
+
if (Number.isNaN(p)) {
|
|
130
|
+
throw new PeeksafeError(
|
|
131
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
132
|
+
"normalQuantile: p must be a number, got NaN",
|
|
133
|
+
{
|
|
134
|
+
detail: { p },
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
109
138
|
if (p <= 0) return -Infinity;
|
|
110
139
|
if (p >= 1) return Infinity;
|
|
111
140
|
const a = [
|
|
@@ -244,6 +273,20 @@ export interface Interval {
|
|
|
244
273
|
observations?: number;
|
|
245
274
|
}
|
|
246
275
|
|
|
276
|
+
// A negative z swapped the bounds and a non-finite one made them NaN; checked before the empty-arm returns.
|
|
277
|
+
function requireZ(z: number, where: string): void {
|
|
278
|
+
if (!Number.isFinite(z) || z < 0) {
|
|
279
|
+
throw new PeeksafeError(
|
|
280
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
281
|
+
`${where}: z must be finite and ≥ 0, got ${z}`,
|
|
282
|
+
{
|
|
283
|
+
detail: { z, where },
|
|
284
|
+
hint: "a standard normal quantile, such as 1.96 for a 95% interval",
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
247
290
|
/**
|
|
248
291
|
* Wilson score interval for a binomial proportion.
|
|
249
292
|
* Correct at the boundaries (0/n and n/n) where Wald is degenerate, which is
|
|
@@ -251,6 +294,7 @@ export interface Interval {
|
|
|
251
294
|
*/
|
|
252
295
|
export function wilsonInterval(successes: number, n: number, z = 1.959963984540054): Interval {
|
|
253
296
|
requireCounts(successes, n, "wilsonInterval");
|
|
297
|
+
requireZ(z, "wilsonInterval");
|
|
254
298
|
// No data is not "p = 0.5". The interval is the whole line and the point
|
|
255
299
|
// estimate is NaN-free only because callers must check `n`, which is why
|
|
256
300
|
// `n` is echoed back in `observations`.
|
|
@@ -321,6 +365,12 @@ export function betaQuantile(alpha: number, beta: number, q: number): number {
|
|
|
321
365
|
},
|
|
322
366
|
);
|
|
323
367
|
}
|
|
368
|
+
// The clamps below are kept, but a NaN fails every comparison in the bisection and came back near 0.
|
|
369
|
+
if (Number.isNaN(q)) {
|
|
370
|
+
throw new PeeksafeError("PEEKSAFE_E_STAT_DOMAIN", "betaQuantile: q must be a number, got NaN", {
|
|
371
|
+
detail: { alpha, beta, q },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
324
374
|
if (q <= 0) return 0;
|
|
325
375
|
if (q >= 1) return 1;
|
|
326
376
|
let lo = 0,
|
|
@@ -376,6 +426,8 @@ export function tQuantile(p: number, df: number): number {
|
|
|
376
426
|
|
|
377
427
|
/** Equal-tailed credible interval of a Beta posterior. */
|
|
378
428
|
export function betaCredibleInterval(post: BetaPosterior, level = 0.95): Interval {
|
|
429
|
+
// betaQuantile clamps q to [0, 1], so a level of 95 or -1 came back as [0, 1] or [1, 0].
|
|
430
|
+
requireOpenProbability(level, "level", "betaCredibleInterval");
|
|
379
431
|
const tail = (1 - level) / 2;
|
|
380
432
|
return {
|
|
381
433
|
low: betaQuantile(post.alpha, post.beta, tail),
|
|
@@ -416,6 +468,7 @@ export function diffInterval(
|
|
|
416
468
|
): Interval {
|
|
417
469
|
requireCounts(s1, n1, "diffInterval(arm 1)");
|
|
418
470
|
requireCounts(s2, n2, "diffInterval(arm 2)");
|
|
471
|
+
requireZ(z, "diffInterval");
|
|
419
472
|
// With an empty arm there is no difference to estimate. Saying so beats
|
|
420
473
|
// reporting `p2 − 0` as if the missing arm had scored zero.
|
|
421
474
|
if (n1 === 0 || n2 === 0) return { low: -1, high: 1, point: 0, observations: 0 };
|
|
@@ -551,8 +604,9 @@ export function sprtDecision(opts: SprtOpts): SprtResult {
|
|
|
551
604
|
// against a wall that is not a number. An alpha of 0 puts `upper` at
|
|
552
605
|
// Infinity, which no evidence ever crosses. Open at both ends: an error rate
|
|
553
606
|
// of 0 is a test that never decides and one of 1 is a test that always does.
|
|
554
|
-
|
|
555
|
-
|
|
607
|
+
// At `alpha + beta >= 1` the walls meet or cross, so a test at 0 runs and a
|
|
608
|
+
// log likelihood ratio of 0 already sat on the H1 wall and answered ACCEPT_H1.
|
|
609
|
+
requireErrorRates(alpha, beta, "sprtDecision");
|
|
556
610
|
const e = 1e-12;
|
|
557
611
|
const cl = (x: number) => Math.min(1 - e, Math.max(e, x));
|
|
558
612
|
const q0 = cl(p0),
|
|
@@ -630,8 +684,9 @@ export function sprtExpectedN(p: number, p0: number, p1: number, alpha = 0.05, b
|
|
|
630
684
|
// and quote `maxTrials` when that fails, which both answers do, so a plan
|
|
631
685
|
// built on an out-of-range error rate priced every case at the per-case cap
|
|
632
686
|
// and reported it as a budget, with nothing to say it was not a real answer.
|
|
633
|
-
|
|
634
|
-
|
|
687
|
+
// At `alpha + beta >= 1` the test decides before its first run, and this
|
|
688
|
+
// answered 0 or a small positive number instead of saying so.
|
|
689
|
+
requireErrorRates(alpha, beta, "sprtExpectedN");
|
|
635
690
|
const A = Math.log((1 - beta) / alpha);
|
|
636
691
|
const B = Math.log(beta / (1 - alpha));
|
|
637
692
|
const logR1 = Math.log(p1 / p0);
|
|
@@ -641,16 +696,6 @@ export function sprtExpectedN(p: number, p0: number, p1: number, alpha = 0.05, b
|
|
|
641
696
|
// p0 === p1 leaves every trial uninformative, so the drift and the curvature
|
|
642
697
|
// are both 0 and no number of runs ends the test.
|
|
643
698
|
if (second === 0) return Infinity;
|
|
644
|
-
// `A === B` is `alpha + beta === 1`: both walls sit at a log likelihood ratio
|
|
645
|
-
// of 0, which is where the test starts, so it decides before it has seen
|
|
646
|
-
// anything and the expected sample number is 0. The general form is
|
|
647
|
-
// `(L1·(A − B) + B) / drift`, which is 0 whatever `L1` is, but it computes
|
|
648
|
-
// `L1` as `(1 − B^h) / (A^h − B^h)`, a genuine 0/0 here, and so returned NaN
|
|
649
|
-
// for in-domain error rates: (0.5, 0.5) and (0.25, 0.75) both landed on it.
|
|
650
|
-
// The neighbouring configurations already converge to 0 from both sides.
|
|
651
|
-
// Only the pairs where both logs round to exactly 0 were affected, which is
|
|
652
|
-
// why this survived the error-rate guards above.
|
|
653
|
-
if (A === B) return 0;
|
|
654
699
|
// The ASN is 0/0 where the tilt vanishes, and that limit is the one place
|
|
655
700
|
// Wald's formula needs a different expression. What decides whether the
|
|
656
701
|
// quotient is really 0/0 is the *tilt*, not the drift: near the zero
|
|
@@ -791,6 +836,41 @@ function requireConcentration(k: number, name: string, where: string): void {
|
|
|
791
836
|
}
|
|
792
837
|
}
|
|
793
838
|
|
|
839
|
+
/**
|
|
840
|
+
* The most runs a sample-size search may try. Below 1, or NaN, the doubling
|
|
841
|
+
* loop never ran and the search answered Infinity, "no number of runs will
|
|
842
|
+
* do", for a design that needs a few hundred.
|
|
843
|
+
*/
|
|
844
|
+
function requireSearchCap(cap: number, name: string, where: string): void {
|
|
845
|
+
if (!(cap >= 1) || !Number.isFinite(cap)) {
|
|
846
|
+
throw new PeeksafeError(
|
|
847
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
848
|
+
`${where}: ${name} must be finite and at least 1, got ${cap}`,
|
|
849
|
+
{
|
|
850
|
+
detail: { [name]: cap, where },
|
|
851
|
+
},
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* The log e-value bar a sample-size search sizes a study to. A NaN bar loses
|
|
858
|
+
* every comparison the searches make, so one answered a single pair and the
|
|
859
|
+
* other Infinity, "no number of runs will do", for the same mistake.
|
|
860
|
+
*/
|
|
861
|
+
function requireLogThreshold(logThreshold: number, where: string): void {
|
|
862
|
+
if (!Number.isFinite(logThreshold)) {
|
|
863
|
+
throw new PeeksafeError(
|
|
864
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
865
|
+
`${where}: logThreshold must be finite, got ${logThreshold}`,
|
|
866
|
+
{
|
|
867
|
+
detail: { logThreshold },
|
|
868
|
+
hint: "this is a log e-value bar, such as Math.log(ebhSoloThreshold(m, fdr))",
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
794
874
|
// Below 1 a shape piles alternative mass onto 0 or 1, which pushed type I error past alpha for thin baselines.
|
|
795
875
|
const ALT_SHAPE_FLOOR = 1;
|
|
796
876
|
|
|
@@ -907,8 +987,25 @@ export function ebhCorrect(evalues: number[], q = 0.05): EbhResult {
|
|
|
907
987
|
return { rejected, discoveries: kStar, threshold: kStar > 0 ? m / (q * kStar) : m / q };
|
|
908
988
|
}
|
|
909
989
|
|
|
910
|
-
/**
|
|
911
|
-
|
|
990
|
+
/**
|
|
991
|
+
* The e-value a case must reach to be certain of rejection whatever the others do.
|
|
992
|
+
*
|
|
993
|
+
* `q` is checked as `ebhCorrect` checks it, so a level of 0 answers Infinity.
|
|
994
|
+
*/
|
|
995
|
+
export function ebhSoloThreshold(m: number, q = 0.05): number {
|
|
996
|
+
if (!Number.isInteger(m) || m < 1) {
|
|
997
|
+
throw new PeeksafeError(
|
|
998
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
999
|
+
`ebhSoloThreshold: m must be a positive integer, got ${m}`,
|
|
1000
|
+
{
|
|
1001
|
+
detail: { m },
|
|
1002
|
+
hint: "m is the number of cases in the e-BH family",
|
|
1003
|
+
},
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
requireProbability(q, "q", "ebhSoloThreshold");
|
|
1007
|
+
return m / q;
|
|
1008
|
+
}
|
|
912
1009
|
|
|
913
1010
|
/* ──────────────────────────────── planning ──────────────────────────── */
|
|
914
1011
|
|
|
@@ -937,6 +1034,16 @@ export function sampleSizeTwoProportion(
|
|
|
937
1034
|
// naive design costs.
|
|
938
1035
|
requireOpenProbability(alpha, "alpha", "sampleSizeTwoProportion");
|
|
939
1036
|
requireOpenProbability(beta, "beta", "sampleSizeTwoProportion");
|
|
1037
|
+
// Both comparisons below are false for NaN, so it fell through to the formula and came back NaN.
|
|
1038
|
+
if (Number.isNaN(delta)) {
|
|
1039
|
+
throw new PeeksafeError(
|
|
1040
|
+
"PEEKSAFE_E_STAT_DOMAIN",
|
|
1041
|
+
"sampleSizeTwoProportion: delta must be a number, got NaN",
|
|
1042
|
+
{
|
|
1043
|
+
detail: { p0, delta },
|
|
1044
|
+
},
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
940
1047
|
if (delta <= 0 || delta >= p0) return Infinity;
|
|
941
1048
|
const p1 = p0 - delta;
|
|
942
1049
|
const pbar = (p0 + p1) / 2;
|
|
@@ -1230,6 +1337,8 @@ export function samplesForEvidence(
|
|
|
1230
1337
|
maxN = 100_000,
|
|
1231
1338
|
altConcentration = 8,
|
|
1232
1339
|
): number {
|
|
1340
|
+
requireSearchCap(maxN, "maxN", "samplesForEvidence");
|
|
1341
|
+
requireLogThreshold(logThreshold, "samplesForEvidence");
|
|
1233
1342
|
const ceiling = evidenceCeilingLogE(
|
|
1234
1343
|
pTrue,
|
|
1235
1344
|
baselineSuccesses,
|
|
@@ -1373,7 +1482,10 @@ export interface PairedCounts {
|
|
|
1373
1482
|
bothFail: number;
|
|
1374
1483
|
}
|
|
1375
1484
|
|
|
1376
|
-
export
|
|
1485
|
+
export function discordant(p: PairedCounts): number {
|
|
1486
|
+
requirePairedCounts(p, "discordant");
|
|
1487
|
+
return p.worse + p.better;
|
|
1488
|
+
}
|
|
1377
1489
|
|
|
1378
1490
|
/**
|
|
1379
1491
|
* Phi coefficient of the 2×2 pair table, how well the pairing actually
|
|
@@ -1383,23 +1495,8 @@ export const discordant = (p: PairedCounts): number => p.worse + p.better;
|
|
|
1383
1495
|
* not assumed.
|
|
1384
1496
|
*/
|
|
1385
1497
|
export function pairPhi(p: PairedCounts): number {
|
|
1498
|
+
requirePairedCounts(p, "pairPhi");
|
|
1386
1499
|
const { bothPass: a, worse: b, better: cc, bothFail: d } = p;
|
|
1387
|
-
for (const [name, v] of [
|
|
1388
|
-
["bothPass", a],
|
|
1389
|
-
["worse", b],
|
|
1390
|
-
["better", cc],
|
|
1391
|
-
["bothFail", d],
|
|
1392
|
-
] as const) {
|
|
1393
|
-
if (!Number.isInteger(v) || v < 0) {
|
|
1394
|
-
throw new PeeksafeError(
|
|
1395
|
-
"PEEKSAFE_E_STAT_DOMAIN",
|
|
1396
|
-
`pairPhi: ${name} must be a non-negative integer, got ${v}`,
|
|
1397
|
-
{
|
|
1398
|
-
detail: { counts: p },
|
|
1399
|
-
},
|
|
1400
|
-
);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
1500
|
const n = a + b + cc + d;
|
|
1404
1501
|
if (n === 0) return 0;
|
|
1405
1502
|
const den = Math.sqrt((a + b) * (cc + d) * (a + cc) * (b + d));
|
|
@@ -1488,21 +1585,8 @@ export function mcnemarSamplesForEvidence(
|
|
|
1488
1585
|
concentration = 6,
|
|
1489
1586
|
): number {
|
|
1490
1587
|
requireConcentration(concentration, "concentration", "mcnemarSamplesForEvidence");
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
// not fire, and the bisection returned its own starting point. The answer
|
|
1494
|
-
// came back as one pair, where the same design at a real 15pt effect needs
|
|
1495
|
-
// 172. `samplesForEvidence` writes its guard negated and so refuses instead.
|
|
1496
|
-
if (!Number.isFinite(logThreshold)) {
|
|
1497
|
-
throw new PeeksafeError(
|
|
1498
|
-
"PEEKSAFE_E_STAT_DOMAIN",
|
|
1499
|
-
`mcnemarSamplesForEvidence: logThreshold must be finite, got ${logThreshold}`,
|
|
1500
|
-
{
|
|
1501
|
-
detail: { logThreshold },
|
|
1502
|
-
hint: "this is a log e-value bar, such as Math.log(ebhSoloThreshold(m, fdr))",
|
|
1503
|
-
},
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1588
|
+
requireSearchCap(maxPairs, "maxPairs", "mcnemarSamplesForEvidence");
|
|
1589
|
+
requireLogThreshold(logThreshold, "mcnemarSamplesForEvidence");
|
|
1506
1590
|
const { rate, theta } = pairedDiscordance(pBaseline, mde, rho);
|
|
1507
1591
|
if (rate <= 0 || theta <= 0.5) return Infinity;
|
|
1508
1592
|
const a1 = Math.max(0.35, concentration * theta);
|
package/src/stop.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
requireEvidence,
|
|
61
61
|
toEvalue,
|
|
62
62
|
} from "./evidence.js";
|
|
63
|
-
import { cannotDropBy, DEFAULT_GATE_OPTIONS } from "./gate.js";
|
|
63
|
+
import { cannotDropBy, DEFAULT_GATE_OPTIONS, definedOptions } from "./gate.js";
|
|
64
64
|
import {
|
|
65
65
|
discordant,
|
|
66
66
|
ebhSoloThreshold,
|
|
@@ -160,7 +160,19 @@ const zFor = (confidence: number): number => {
|
|
|
160
160
|
},
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
const z = normalQuantile(1 - (1 - confidence) / 2);
|
|
164
|
+
// At the largest double below 1 the upper tail rounds to 1, and an infinite z reached the ceiling as NaN.
|
|
165
|
+
if (!Number.isFinite(z)) {
|
|
166
|
+
throw new PeeksafeError(
|
|
167
|
+
"PEEKSAFE_E_CONFIG",
|
|
168
|
+
`shouldStop: futilityConfidence ${confidence} is too close to 1 for a finite interval`,
|
|
169
|
+
{
|
|
170
|
+
detail: { futilityConfidence: confidence },
|
|
171
|
+
hint: "a two-sided confidence level such as 0.95; higher makes futility fire later",
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return z;
|
|
164
176
|
};
|
|
165
177
|
|
|
166
178
|
/**
|
|
@@ -180,7 +192,7 @@ export function shouldStop(
|
|
|
180
192
|
baseline: BaselineStat,
|
|
181
193
|
options: StopOptions,
|
|
182
194
|
): StopDecision {
|
|
183
|
-
const opts = { ...DEFAULT_STOP_OPTIONS, ...options };
|
|
195
|
+
const opts = { ...DEFAULT_STOP_OPTIONS, ...definedOptions(options) };
|
|
184
196
|
requireCounts(observed.successes, observed.trials, "shouldStop");
|
|
185
197
|
requireCounts(baseline.successes, baseline.trials, "shouldStop.baseline");
|
|
186
198
|
requireOpenProbability(opts.mde, "mde", "shouldStop");
|
|
@@ -363,7 +375,7 @@ export function shouldStopPaired(
|
|
|
363
375
|
counts: PairedCounts,
|
|
364
376
|
options: PairedStopOptions,
|
|
365
377
|
): PairedStopDecision {
|
|
366
|
-
const opts = { ...DEFAULT_STOP_OPTIONS, ...options };
|
|
378
|
+
const opts = { ...DEFAULT_STOP_OPTIONS, ...definedOptions(options) };
|
|
367
379
|
requirePairedCounts(counts, "shouldStopPaired");
|
|
368
380
|
requireOpenProbability(opts.mde, "mde", "shouldStopPaired");
|
|
369
381
|
requireOpenProbability(opts.fdr, "fdr", "shouldStopPaired");
|
|
@@ -437,7 +449,7 @@ export function shouldStopPaired(
|
|
|
437
449
|
}
|
|
438
450
|
|
|
439
451
|
// Mean of prod(1 - lambda (D - mde)) over fixed bets, D = +1 worse, -1 better; a supermartingale whenever E[D] >= mde.
|
|
440
|
-
function settleLogE(c: PairedCounts, mde: number): number {
|
|
452
|
+
export function settleLogE(c: PairedCounts, mde: number): number {
|
|
441
453
|
const terms: number[] = [];
|
|
442
454
|
for (let k = 1; k <= 9; k++) {
|
|
443
455
|
const lambda = k / 10 / (1 - mde);
|