peeksafe 0.2.0 → 0.3.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 +40 -0
- package/README.md +1 -1
- package/dist/frontier.d.ts +5 -4
- package/dist/frontier.d.ts.map +1 -1
- package/dist/frontier.js +30 -31
- package/dist/frontier.js.map +1 -1
- package/dist/plan.d.ts +33 -17
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +167 -73
- package/dist/plan.js.map +1 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +2 -3
- package/dist/stats.js.map +1 -1
- package/dist/stop.d.ts +0 -1
- package/dist/stop.d.ts.map +1 -1
- package/dist/stop.js +1 -1
- package/dist/stop.js.map +1 -1
- package/package.json +1 -1
- package/src/frontier.ts +39 -34
- package/src/plan.ts +174 -75
- package/src/stats.ts +2 -3
- package/src/stop.ts +1 -1
package/src/plan.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* it.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import type { BaselineStat, CaseRef } from "./baseline.js";
|
|
36
36
|
import {
|
|
37
37
|
PeeksafeError,
|
|
38
38
|
requireCounts,
|
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
requireUniqueCaseIds,
|
|
45
45
|
} from "./errors.js";
|
|
46
46
|
import {
|
|
47
|
-
betaQuantile,
|
|
48
47
|
ebhSoloThreshold,
|
|
49
48
|
evidenceCeilingLogE,
|
|
50
49
|
mcnemarSamplesForEvidence,
|
|
@@ -52,9 +51,8 @@ import {
|
|
|
52
51
|
pairPhi,
|
|
53
52
|
sampleSizeTwoProportion,
|
|
54
53
|
samplesForEvidence,
|
|
55
|
-
sprtExpectedN,
|
|
56
54
|
} from "./stats.js";
|
|
57
|
-
import {
|
|
55
|
+
import { shouldStop, shouldStopPaired } from "./stop.js";
|
|
58
56
|
|
|
59
57
|
export interface PlanConfig {
|
|
60
58
|
/** the drop we want to be able to detect, in probability units (0.15 = 15 points) */
|
|
@@ -169,6 +167,13 @@ export interface PlanCase {
|
|
|
169
167
|
* "undetectable" verdict.
|
|
170
168
|
*/
|
|
171
169
|
baselineRunsNeeded: number | null;
|
|
170
|
+
/**
|
|
171
|
+
* Expected runs this case costs in a pull request in which nothing moved, at
|
|
172
|
+
* the design the expected bill prices it at: `config.design`, else `design`,
|
|
173
|
+
* else the suite's `recommendedDesign` (unpaired when `planCase` is called on
|
|
174
|
+
* its own). The cases of a plan sum to `totals.typicalRuns`; 0 with no baseline.
|
|
175
|
+
*/
|
|
176
|
+
typicalRuns: number;
|
|
172
177
|
/** what a fixed-N two-proportion design would spend per arm, for comparison */
|
|
173
178
|
fixedNPerArm: number;
|
|
174
179
|
reason: string;
|
|
@@ -330,6 +335,9 @@ export function planCase(
|
|
|
330
335
|
if (b) requireCounts(b.successes, b.trials, `planCase(${c.id}).baseline`);
|
|
331
336
|
requireErrorRates(cfg.alpha, cfg.beta, "planCase");
|
|
332
337
|
requirePrices(cfg, "planCase");
|
|
338
|
+
// The expected bill walks the batch grid at the stopping rule's level; a zero batch never ends.
|
|
339
|
+
requireTrialGrid(cfg, "planCase");
|
|
340
|
+
requireSettleConfidence(cfg, "planCase");
|
|
333
341
|
const barLogE = Math.log(ebhSoloThreshold(m, cfg.fdr));
|
|
334
342
|
const trials = b?.trials ?? 0;
|
|
335
343
|
const successes = b?.successes ?? 0;
|
|
@@ -357,6 +365,7 @@ export function planCase(
|
|
|
357
365
|
detectability: "IMPOSSIBLE",
|
|
358
366
|
design: null,
|
|
359
367
|
baselineRunsNeeded: null,
|
|
368
|
+
typicalRuns: 0,
|
|
360
369
|
reason: "no baseline: nothing to compare against, record one first",
|
|
361
370
|
};
|
|
362
371
|
}
|
|
@@ -370,6 +379,8 @@ export function planCase(
|
|
|
370
379
|
detectability: "IMPOSSIBLE",
|
|
371
380
|
design: null,
|
|
372
381
|
baselineRunsNeeded: null,
|
|
382
|
+
// Undecidable, but the gate still runs it until it stops, so the expected bill pays for it.
|
|
383
|
+
typicalRuns: typicalCaseRuns(b, cfg.design ?? "unpaired", cfg, m),
|
|
373
384
|
reason:
|
|
374
385
|
`baseline rate ${(rate * 100).toFixed(0)}% cannot lose ${(cfg.mde * 100).toFixed(0)} points, ` +
|
|
375
386
|
`the effect is arithmetically impossible, not merely expensive`,
|
|
@@ -445,24 +456,40 @@ export function planCase(
|
|
|
445
456
|
detectability,
|
|
446
457
|
design,
|
|
447
458
|
baselineRunsNeeded,
|
|
459
|
+
typicalRuns: typicalCaseRuns(b, cfg.design ?? design ?? "unpaired", cfg, m),
|
|
448
460
|
reason,
|
|
449
461
|
};
|
|
450
462
|
}
|
|
451
463
|
|
|
464
|
+
// Each design is counted under the rule that stops it: an unpaired case in the runs `shouldStop` takes, a paired one in the pairs `shouldStopPaired` takes.
|
|
465
|
+
function typicalCaseRuns(
|
|
466
|
+
b: BaselineStat | undefined,
|
|
467
|
+
design: "unpaired" | "paired",
|
|
468
|
+
cfg: PlanConfig,
|
|
469
|
+
m: number,
|
|
470
|
+
): number {
|
|
471
|
+
if (!b || b.trials === 0) return 0;
|
|
472
|
+
const rate = b.successes / b.trials;
|
|
473
|
+
return design === "paired"
|
|
474
|
+
? 2 * expectedSettlePairs(rate, cfg.pairCoupling, cfg.mde, m, cfg)
|
|
475
|
+
: expectedStopRuns(b, rate, m, cfg);
|
|
476
|
+
}
|
|
477
|
+
|
|
452
478
|
/**
|
|
453
|
-
* Expected total samples for the sequential design,
|
|
454
|
-
* expected
|
|
455
|
-
* the gate's own
|
|
479
|
+
* Expected total samples for the sequential design: per case, the exact
|
|
480
|
+
* expected runs `shouldStop` takes to stop it when its candidate passes at
|
|
481
|
+
* `trueRate`, in a suite of `cases.length`, on the gate's own batch grid
|
|
482
|
+
* (`expectedStopRuns`). At the default rate this is the figure the plan bills
|
|
483
|
+
* an unpaired case at, `PlanCase.typicalRuns`.
|
|
456
484
|
*
|
|
457
|
-
* `makePlan` no longer bills from it: an unpaired case there is counted in
|
|
458
|
-
* the runs `shouldStop` takes, which this figure misses either way (#105).
|
|
459
485
|
* `samplesForEvidence`, the one behind `PlanCase.unpaired.runs`, answers a
|
|
460
486
|
* different question: how many runs it takes to *certify a regression*. A case
|
|
461
|
-
* that did not regress never gets there; it
|
|
462
|
-
*
|
|
487
|
+
* that did not regress never gets there; it stops as futile or settled in a
|
|
488
|
+
* few batches. Pricing every case at the certification cost over-predicts the
|
|
463
489
|
* bill.
|
|
464
490
|
*
|
|
465
|
-
* `trueRate` defaults to the case's own baseline rate, i.e. "nothing moved",
|
|
491
|
+
* `trueRate` defaults to the case's own baseline rate, i.e. "nothing moved",
|
|
492
|
+
* which is what almost every pull request looks like.
|
|
466
493
|
*/
|
|
467
494
|
export function expectedSequentialSamples(
|
|
468
495
|
cases: Array<CaseRef>,
|
|
@@ -472,6 +499,10 @@ export function expectedSequentialSamples(
|
|
|
472
499
|
b.trials > 0 ? b.successes / b.trials : 0,
|
|
473
500
|
): { samples: number; perCase: Map<string, number> } {
|
|
474
501
|
requireTrialGrid(cfg, "expectedSequentialSamples");
|
|
502
|
+
// Checked here so a bad value is refused under this name, not under the stopping rule's.
|
|
503
|
+
requireOpenProbability(cfg.mde, "mde", "expectedSequentialSamples");
|
|
504
|
+
requireOpenProbability(cfg.fdr, "fdr", "expectedSequentialSamples");
|
|
505
|
+
requireSettleConfidence(cfg, "expectedSequentialSamples");
|
|
475
506
|
// A repeated id was added to `samples` once per listing but kept once in `perCase`, so the two disagreed.
|
|
476
507
|
requireUniqueCaseIds(cases, "expectedSequentialSamples");
|
|
477
508
|
const perCase = new Map<string, number>();
|
|
@@ -479,16 +510,12 @@ export function expectedSequentialSamples(
|
|
|
479
510
|
for (const c of cases) {
|
|
480
511
|
const b = baseline.get(c.id);
|
|
481
512
|
if (!b || b.trials === 0) continue;
|
|
482
|
-
const p0 = baselineNullRate(b, cfg.nullQuantile);
|
|
483
|
-
const p1 = Math.max(0.005, p0 - cfg.mde);
|
|
484
513
|
const p = Math.max(0, Math.min(1, trueRate(c.id, b)));
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
perCase.set(c.id, capped);
|
|
491
|
-
total += capped;
|
|
514
|
+
// Wald's figure fell back to the cap on a NaN rate; the dynamic program would answer NaN.
|
|
515
|
+
requireProbability(p, "trueRate", "expectedSequentialSamples");
|
|
516
|
+
const runs = expectedStopRuns(b, p, cases.length, cfg);
|
|
517
|
+
perCase.set(c.id, runs);
|
|
518
|
+
total += runs;
|
|
492
519
|
}
|
|
493
520
|
return { samples: total, perCase };
|
|
494
521
|
}
|
|
@@ -505,38 +532,101 @@ export function requireSettleConfidence(
|
|
|
505
532
|
}
|
|
506
533
|
}
|
|
507
534
|
|
|
508
|
-
// The
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
cfg.minTrials + Math.ceil(Math.max(0, clamped - cfg.minTrials) / cfg.batchSize) * cfg.batchSize;
|
|
513
|
-
return Math.min(cfg.maxTrials, batched);
|
|
514
|
-
}
|
|
535
|
+
// The frontier prices the same paired cell once per baseline and screen, so a sweep would rerun one program dozens of times.
|
|
536
|
+
const settlePairsMemo = new Map<string, number>();
|
|
537
|
+
// Every case of a suite that shares a baseline, and every planCase call makePlan makes, runs the same program.
|
|
538
|
+
const stopRunsMemo = new Map<string, number>();
|
|
515
539
|
|
|
516
540
|
/**
|
|
517
|
-
* Expected pairs
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* sets how often an unmoved pair disagrees.
|
|
541
|
+
* Expected pairs `shouldStopPaired` takes to stop a paired case that did not
|
|
542
|
+
* move, at coupling `rho`, checked at `minTrials` and then every `batchSize`
|
|
543
|
+
* pairs up to `maxTrials`, in a suite of `suiteSize`. Exact, as
|
|
544
|
+
* `expectedStopRuns` is: the decision depends only on the worse and better
|
|
545
|
+
* counts at a given pair count, so a dynamic program carries their joint
|
|
546
|
+
* distribution from one check to the next. Rounded up to a whole pair.
|
|
524
547
|
*/
|
|
525
548
|
export function expectedSettlePairs(
|
|
526
549
|
rate: number,
|
|
527
550
|
rho: number,
|
|
528
551
|
mde: number,
|
|
529
|
-
|
|
552
|
+
suiteSize: number,
|
|
553
|
+
cfg: TrialGrid & Pick<PlanConfig, "fdr" | "futilityConfidence">,
|
|
530
554
|
): number {
|
|
555
|
+
const key = [
|
|
556
|
+
rate,
|
|
557
|
+
rho,
|
|
558
|
+
mde,
|
|
559
|
+
suiteSize,
|
|
560
|
+
cfg.fdr,
|
|
561
|
+
cfg.futilityConfidence,
|
|
562
|
+
cfg.minTrials,
|
|
563
|
+
cfg.maxTrials,
|
|
564
|
+
cfg.batchSize,
|
|
565
|
+
].join("|");
|
|
566
|
+
const known = settlePairsMemo.get(key);
|
|
567
|
+
if (known !== undefined) return known;
|
|
568
|
+
const options = {
|
|
569
|
+
suiteSize,
|
|
570
|
+
mde,
|
|
571
|
+
fdr: cfg.fdr,
|
|
572
|
+
futilityConfidence: cfg.futilityConfidence,
|
|
573
|
+
};
|
|
531
574
|
// Unmoved arms disagree each way only when uncoupled: (1 - rho) p (1 - p).
|
|
532
575
|
const each = (1 - rho) * rate * (1 - rate);
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
576
|
+
const same = 1 - 2 * each;
|
|
577
|
+
// live[w * (top + 1) + b]: probability of w worse and b better pairs with the case still running.
|
|
578
|
+
let live = new Float64Array([1]);
|
|
579
|
+
// Mass under 1e-18 is dropped so the square stays as small as the spread of the counts; it cannot move the rounded answer.
|
|
580
|
+
let top = 0;
|
|
581
|
+
let n = 0;
|
|
582
|
+
let expected = 0;
|
|
583
|
+
for (let step = cfg.minTrials; ; step = Math.min(cfg.batchSize, cfg.maxTrials - n)) {
|
|
584
|
+
for (let i = 0; i < step; i++) {
|
|
585
|
+
const side = top + 2;
|
|
586
|
+
const next = new Float64Array(side * side);
|
|
587
|
+
for (let w = 0; w <= top; w++) {
|
|
588
|
+
for (let b = 0; b <= top; b++) {
|
|
589
|
+
const q = live[w * (top + 1) + b]!;
|
|
590
|
+
if (q === 0) continue;
|
|
591
|
+
next[w * side + b]! += q * same;
|
|
592
|
+
next[(w + 1) * side + b]! += q * each;
|
|
593
|
+
next[w * side + b + 1]! += q * each;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
let reach = 0;
|
|
597
|
+
for (let at = 0; at < next.length; at++) {
|
|
598
|
+
if (next[at]! < 1e-18) next[at] = 0;
|
|
599
|
+
else reach = Math.max(reach, Math.floor(at / side), at % side);
|
|
600
|
+
}
|
|
601
|
+
live = new Float64Array((reach + 1) * (reach + 1));
|
|
602
|
+
for (let w = 0; w <= reach; w++) {
|
|
603
|
+
for (let b = 0; b <= reach; b++) live[w * (reach + 1) + b] = next[w * side + b]!;
|
|
604
|
+
}
|
|
605
|
+
top = reach;
|
|
606
|
+
n++;
|
|
607
|
+
}
|
|
608
|
+
let running = 0;
|
|
609
|
+
for (let w = 0; w <= top; w++) {
|
|
610
|
+
for (let b = 0; b <= top; b++) {
|
|
611
|
+
const at = w * (top + 1) + b;
|
|
612
|
+
const q = live[at]!;
|
|
613
|
+
if (q === 0) continue;
|
|
614
|
+
const counts = { bothPass: n - w - b, worse: w, better: b, bothFail: 0 };
|
|
615
|
+
if (n >= cfg.maxTrials || shouldStopPaired(counts, options).stop) {
|
|
616
|
+
expected += q * n;
|
|
617
|
+
live[at] = 0;
|
|
618
|
+
} else {
|
|
619
|
+
running += q;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (running === 0) break;
|
|
538
624
|
}
|
|
539
|
-
|
|
625
|
+
// Float residue must not push an exact whole number up a pair.
|
|
626
|
+
const pairs = Math.ceil(expected - 1e-9);
|
|
627
|
+
if (settlePairsMemo.size >= 1024) settlePairsMemo.clear();
|
|
628
|
+
settlePairsMemo.set(key, pairs);
|
|
629
|
+
return pairs;
|
|
540
630
|
}
|
|
541
631
|
|
|
542
632
|
/**
|
|
@@ -547,12 +637,26 @@ export function expectedSettlePairs(
|
|
|
547
637
|
* program carries the distribution of the success count from one check to the
|
|
548
638
|
* next. Rounded up to a whole run; an expectation is not a grid point.
|
|
549
639
|
*/
|
|
550
|
-
function expectedStopRuns(
|
|
640
|
+
export function expectedStopRuns(
|
|
551
641
|
baseline: BaselineStat,
|
|
552
642
|
rate: number,
|
|
553
643
|
suiteSize: number,
|
|
554
644
|
cfg: TrialGrid & Pick<PlanConfig, "mde" | "fdr" | "futilityConfidence">,
|
|
555
645
|
): number {
|
|
646
|
+
const key = [
|
|
647
|
+
baseline.successes,
|
|
648
|
+
baseline.trials,
|
|
649
|
+
rate,
|
|
650
|
+
suiteSize,
|
|
651
|
+
cfg.mde,
|
|
652
|
+
cfg.fdr,
|
|
653
|
+
cfg.futilityConfidence,
|
|
654
|
+
cfg.minTrials,
|
|
655
|
+
cfg.maxTrials,
|
|
656
|
+
cfg.batchSize,
|
|
657
|
+
].join("|");
|
|
658
|
+
const known = stopRunsMemo.get(key);
|
|
659
|
+
if (known !== undefined) return known;
|
|
556
660
|
const options = {
|
|
557
661
|
suiteSize,
|
|
558
662
|
mde: cfg.mde,
|
|
@@ -586,7 +690,10 @@ function expectedStopRuns(
|
|
|
586
690
|
if (running === 0) break;
|
|
587
691
|
}
|
|
588
692
|
// Float residue must not push an exact whole number up a run.
|
|
589
|
-
|
|
693
|
+
const runs = Math.ceil(expected - 1e-9);
|
|
694
|
+
if (stopRunsMemo.size >= 1024) stopRunsMemo.clear();
|
|
695
|
+
stopRunsMemo.set(key, runs);
|
|
696
|
+
return runs;
|
|
590
697
|
}
|
|
591
698
|
|
|
592
699
|
/**
|
|
@@ -669,6 +776,13 @@ export function makePlan(
|
|
|
669
776
|
// Per case, like `bestRuns`: one suite-wide design billed a mixed suite as if every case ran it.
|
|
670
777
|
const runsPerObs = (p: PlanCase) =>
|
|
671
778
|
(cfg.design ?? p.design ?? recommendedDesign) === "paired" ? 2 : 1;
|
|
779
|
+
// `planCase` cannot see the suite, so a case with no design was priced unpaired there.
|
|
780
|
+
for (const p of planned) {
|
|
781
|
+
if (p.design === null && runsPerObs(p) === 2) {
|
|
782
|
+
const b = baseline.get(p.caseId);
|
|
783
|
+
p.typicalRuns = typicalCaseRuns(b, "paired", { ...cfg, pairCoupling: p.pairCoupling }, m);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
672
786
|
// `screenRuns` counts observations, so each case's pass costs its own design's runs.
|
|
673
787
|
const screenPassRuns = planned.reduce((a, p) => a + cfg.screenRuns * runsPerObs(p), 0);
|
|
674
788
|
|
|
@@ -694,27 +808,14 @@ export function makePlan(
|
|
|
694
808
|
// set from.
|
|
695
809
|
// Both counts are *observations*. A paired observation is two runs, the
|
|
696
810
|
// candidate and the reference revision on the same seed, and so is a paired
|
|
697
|
-
// screening run. Each case is counted under the rule that stops it
|
|
698
|
-
// one in pairs to settle, an unpaired one
|
|
699
|
-
// Wald's SPRT figure, used for both before,
|
|
700
|
-
// the coupling and the suite size, and
|
|
701
|
-
|
|
702
|
-
const observations = (p: PlanCase) => {
|
|
703
|
-
const b = baseline.get(p.caseId);
|
|
704
|
-
if (!b || b.trials === 0) return 0;
|
|
705
|
-
if (runsPerObs(p) === 2) {
|
|
706
|
-
return expectedSettlePairs(p.baseline.rate, p.pairCoupling, cfg.mde, cfg);
|
|
707
|
-
}
|
|
708
|
-
const key = `${b.successes}/${b.trials}`;
|
|
709
|
-
const known = stopRuns.get(key);
|
|
710
|
-
if (known !== undefined) return known;
|
|
711
|
-
const runs = expectedStopRuns(b, b.successes / b.trials, m, cfg);
|
|
712
|
-
stopRuns.set(key, runs);
|
|
713
|
-
return runs;
|
|
714
|
-
};
|
|
811
|
+
// screening run. Each case is counted under the rule that stops it
|
|
812
|
+
// (`PlanCase.typicalRuns`): a paired one in pairs to settle, an unpaired one
|
|
813
|
+
// in the runs `shouldStop` takes. Wald's SPRT figure, used for both before,
|
|
814
|
+
// runs a different rule, ignores the coupling and the suite size, and
|
|
815
|
+
// mispriced cases either way.
|
|
715
816
|
const priced = planned.filter((p) => (baseline.get(p.caseId)?.trials ?? 0) > 0).length;
|
|
716
|
-
const typicalObservations = planned.reduce((a, p) => a +
|
|
717
|
-
const typicalRuns = planned.reduce((a, p) => a +
|
|
817
|
+
const typicalObservations = planned.reduce((a, p) => a + p.typicalRuns / runsPerObs(p), 0);
|
|
818
|
+
const typicalRuns = planned.reduce((a, p) => a + p.typicalRuns, 0);
|
|
718
819
|
const runsPerObservation =
|
|
719
820
|
typicalObservations > 0
|
|
720
821
|
? typicalRuns / typicalObservations
|
|
@@ -862,9 +963,7 @@ export function affordabilityGrid(
|
|
|
862
963
|
): AffordabilityCell[] {
|
|
863
964
|
requireProbability(baselineRate, "baselineRate", "affordabilityGrid");
|
|
864
965
|
requireErrorRates(cfg.alpha, cfg.beta, "affordabilityGrid");
|
|
865
|
-
//
|
|
866
|
-
// straight from `betaQuantile`, which answers 0 or 1 for a quantile outside
|
|
867
|
-
// (0,1) and about 0 for NaN instead of refusing. `makePlan` refuses it.
|
|
966
|
+
// `makePlan` refuses it, so one options object must not price here and fail there.
|
|
868
967
|
requireOpenProbability(cfg.nullQuantile, "nullQuantile", "affordabilityGrid");
|
|
869
968
|
// makePlan refuses both; unchecked, an fdr of 2 priced a grid cheaper than one at 0.05.
|
|
870
969
|
requireOpenProbability(cfg.fdr, "fdr", "affordabilityGrid");
|
|
@@ -936,16 +1035,16 @@ export function affordabilityGrid(
|
|
|
936
1035
|
// The ceiling above prices a pull request in which every case regressed.
|
|
937
1036
|
// This prices one in which nothing did, which is what a pull request
|
|
938
1037
|
// almost always is.
|
|
939
|
-
|
|
940
|
-
const p1 = Math.max(0.005, p0 - mde);
|
|
941
|
-
const rawN = sprtExpectedN(baselineRate, p0, p1, cfg.alpha, cfg.beta);
|
|
942
|
-
// A paired cell counts the pairs `shouldStopPaired` takes to settle, as `makePlan` does; the SPRT figure ignores the coupling.
|
|
1038
|
+
// Each design is counted under the rule that stops it, as `makePlan` does; Wald's SPRT figure ignores the coupling and the suite size.
|
|
943
1039
|
const perCaseTypical =
|
|
944
1040
|
runsPerObs === 2
|
|
945
|
-
? expectedSettlePairs(baselineRate, cfg.pairCoupling, mde, cfg)
|
|
946
|
-
:
|
|
947
|
-
|
|
948
|
-
|
|
1041
|
+
? expectedSettlePairs(baselineRate, cfg.pairCoupling, mde, m, cfg)
|
|
1042
|
+
: expectedStopRuns(
|
|
1043
|
+
{ caseId: "typical", successes: s, trials: baselineTrials },
|
|
1044
|
+
baselineRate,
|
|
1045
|
+
m,
|
|
1046
|
+
{ ...cfg, mde },
|
|
1047
|
+
);
|
|
949
1048
|
// Priced for the same design as the ceiling above (`runsPerObs`), or the
|
|
950
1049
|
// cell quotes an unpaired price for a plan only pairing can execute.
|
|
951
1050
|
const typicalRuns = Math.ceil(
|
package/src/stats.ts
CHANGED
|
@@ -729,9 +729,8 @@ export function sprtExpectedN(p: number, p0: number, p1: number, alpha = 0.05, b
|
|
|
729
729
|
* outside the two hypotheses, so the two powers are evaluated relative to
|
|
730
730
|
* whichever of them is largest. Written as plain `exp`, a tilt of −662, which is
|
|
731
731
|
* p = 0.0025 against p₀ = 0.999 and p₁ = 0.99, overflows `B^h` to Infinity and
|
|
732
|
-
* the ratio comes back NaN
|
|
733
|
-
*
|
|
734
|
-
* silently prices the case at `maxTrials`.
|
|
732
|
+
* the ratio comes back NaN, which a caller testing the result for `> 0` reads
|
|
733
|
+
* as no estimate at all.
|
|
735
734
|
*/
|
|
736
735
|
function sprtAcceptH1(h: number, A: number, B: number, drift: number): number {
|
|
737
736
|
// No tilt exists at p = 0 or p = 1: one outcome is impossible, so the drift
|
package/src/stop.ts
CHANGED
|
@@ -449,7 +449,7 @@ export function shouldStopPaired(
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
// Mean of prod(1 - lambda (D - mde)) over fixed bets, D = +1 worse, -1 better; a supermartingale whenever E[D] >= mde.
|
|
452
|
-
|
|
452
|
+
function settleLogE(c: PairedCounts, mde: number): number {
|
|
453
453
|
const terms: number[] = [];
|
|
454
454
|
for (let k = 1; k <= 9; k++) {
|
|
455
455
|
const lambda = k / 10 / (1 - mde);
|