peeksafe 0.0.0-stage → 0.1.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 +146 -0
- package/LICENSE +190 -0
- package/NOTICE +78 -0
- package/README.md +492 -2
- package/RESEARCH-NOTES.md +274 -0
- package/dist/baseline.d.ts +45 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +49 -0
- package/dist/baseline.js.map +1 -0
- package/dist/check.d.ts +64 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +172 -0
- package/dist/check.js.map +1 -0
- package/dist/cluster.d.ts +328 -0
- package/dist/cluster.d.ts.map +1 -0
- package/dist/cluster.js +598 -0
- package/dist/cluster.js.map +1 -0
- package/dist/errors.d.ts +87 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +143 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence.d.ts +15 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +30 -0
- package/dist/evidence.js.map +1 -0
- package/dist/frontier.d.ts +269 -0
- package/dist/frontier.d.ts.map +1 -0
- package/dist/frontier.js +460 -0
- package/dist/frontier.js.map +1 -0
- package/dist/gate.d.ts +173 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +196 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/plan.d.ts +248 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +482 -0
- package/dist/plan.js.map +1 -0
- package/dist/rand.d.ts +17 -0
- package/dist/rand.d.ts.map +1 -0
- package/dist/rand.js +92 -0
- package/dist/rand.js.map +1 -0
- package/dist/stats.d.ts +397 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +1245 -0
- package/dist/stats.js.map +1 -0
- package/dist/stop.d.ts +163 -0
- package/dist/stop.d.ts.map +1 -0
- package/dist/stop.js +262 -0
- package/dist/stop.js.map +1 -0
- package/package.json +64 -4
- package/src/baseline.ts +72 -0
- package/src/check.ts +238 -0
- package/src/cluster.ts +830 -0
- package/src/errors.ts +208 -0
- package/src/evidence.ts +62 -0
- package/src/frontier.ts +782 -0
- package/src/gate.ts +358 -0
- package/src/index.ts +206 -0
- package/src/plan.ts +778 -0
- package/src/rand.ts +121 -0
- package/src/stats.ts +1579 -0
- package/src/stop.ts +452 -0
package/src/gate.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate.ts: the decision.
|
|
3
|
+
*
|
|
4
|
+
* You bring counts. This decides which cases regressed, at a false discovery
|
|
5
|
+
* rate you name, in a way that stays valid however many times you looked at the
|
|
6
|
+
* data on the way here.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this is safe to peek at
|
|
9
|
+
*
|
|
10
|
+
* The usual construction is: run the suite, stop a case when it looks
|
|
11
|
+
* significant, compute a p-value from the counts, hand the p-values to
|
|
12
|
+
* Benjamini-Hochberg. That is invalid in two independent places.
|
|
13
|
+
*
|
|
14
|
+
* A p-value computed at a stopping boundary is not a p-value. `P(p_n <= a) <= a`
|
|
15
|
+
* holds for each fixed `n`; a procedure that adds runs and re-tests until the
|
|
16
|
+
* result looks significant reports `min` over the peeks, and that minimum is not
|
|
17
|
+
* bounded by `a`. Benjamini-Hochberg takes `P(p <= t) <= t` as its input
|
|
18
|
+
* assumption, so it inherits nothing and controls nothing.
|
|
19
|
+
*
|
|
20
|
+
* This module never computes a p-value. It computes an e-value, which is a
|
|
21
|
+
* non-negative statistic with mean at most 1 under the null. Ville's inequality
|
|
22
|
+
* bounds `P(sup_n E_n >= 1/a) <= a` over the whole trajectory, so an e-value is
|
|
23
|
+
* valid at any stopping time, including one chosen after looking. (The mean is
|
|
24
|
+
* exactly 1 averaged over the shared rate, not at every fixed rate; the README
|
|
25
|
+
* section "What that guarantee rests on" says where the bound is checked.) The
|
|
26
|
+
* multiplicity correction is e-BH (Wang and Ramdas), which takes e-values
|
|
27
|
+
* directly and controls FDR under arbitrary dependence between cases.
|
|
28
|
+
*
|
|
29
|
+
* The practical consequence: you may stop a case whenever you like, for any
|
|
30
|
+
* reason, including "it already looks bad". Nothing here cares when you stopped.
|
|
31
|
+
*
|
|
32
|
+
* ## What it will refuse to do
|
|
33
|
+
*
|
|
34
|
+
* Gate a case with no baseline. There is no null to test against, and the
|
|
35
|
+
* failure mode of inventing one is not a wrong number, it is a confident wrong
|
|
36
|
+
* number: a missing baseline read as `0/0` has a Beta(1,1) posterior median of
|
|
37
|
+
* 0.5, which gates the case against "it passes half the time" and reports a
|
|
38
|
+
* large improvement against nothing at all. Cases without a baseline come back
|
|
39
|
+
* in `newCases` and take no part in the verdict or the e-BH family.
|
|
40
|
+
*
|
|
41
|
+
* ## Paired cases
|
|
42
|
+
*
|
|
43
|
+
* A case may instead carry `paired` counts from re-running the baseline next to
|
|
44
|
+
* the candidate, one baseline run per candidate run. Its e-value is
|
|
45
|
+
* `pairedLogE` on the discordant pairs, which is valid at every rate by
|
|
46
|
+
* construction and has no ceiling, whichever `evidence` the unpaired cases use.
|
|
47
|
+
* The pairs need not share a seed: independent runs paired in order are still
|
|
48
|
+
* valid, and a shared seed only makes discordances more informative.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import type { BaselineStat } from "./baseline.js";
|
|
52
|
+
import {
|
|
53
|
+
PeeksafeError,
|
|
54
|
+
requireAltConcentration,
|
|
55
|
+
requireCounts,
|
|
56
|
+
requireOpenProbability,
|
|
57
|
+
requirePairedCounts,
|
|
58
|
+
} from "./errors.js";
|
|
59
|
+
import {
|
|
60
|
+
ceilingLogEvidence,
|
|
61
|
+
type Evidence,
|
|
62
|
+
logEvidence,
|
|
63
|
+
requireEvidence,
|
|
64
|
+
toEvalue,
|
|
65
|
+
} from "./evidence.js";
|
|
66
|
+
import {
|
|
67
|
+
discordant,
|
|
68
|
+
ebhCorrect,
|
|
69
|
+
ebhSoloThreshold,
|
|
70
|
+
type PairedCounts,
|
|
71
|
+
pairedLogE,
|
|
72
|
+
} from "./stats.js";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* True when the baseline's observed rate leaves no room for an `mde`-sized drop.
|
|
76
|
+
* `makePlan` calls the same cases IMPOSSIBLE, by the same rule.
|
|
77
|
+
*/
|
|
78
|
+
export const cannotDropBy = (
|
|
79
|
+
baseline: { successes: number; trials: number },
|
|
80
|
+
mde: number,
|
|
81
|
+
): boolean => baseline.successes / baseline.trials <= mde;
|
|
82
|
+
|
|
83
|
+
/** One case's candidate result, and the baseline it is measured against. */
|
|
84
|
+
export interface GateCase {
|
|
85
|
+
id: string;
|
|
86
|
+
/** Successes observed for the revision under test. */
|
|
87
|
+
successes: number;
|
|
88
|
+
/** Trials run for the revision under test. Stop whenever you like. */
|
|
89
|
+
trials: number;
|
|
90
|
+
/** The reference revision's recorded counts, or undefined if there are none. */
|
|
91
|
+
baseline?: BaselineStat;
|
|
92
|
+
paired?: undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One case run as pairs: each candidate run next to a fresh baseline run. */
|
|
96
|
+
export interface PairedGateCase {
|
|
97
|
+
id: string;
|
|
98
|
+
/** The pair table so far. Stop whenever you like. */
|
|
99
|
+
paired: PairedCounts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface GateOptions {
|
|
103
|
+
/** Minimum detectable effect: the drop, in rate, that counts as a regression. */
|
|
104
|
+
mde?: number;
|
|
105
|
+
/** Target false discovery rate across the suite. */
|
|
106
|
+
fdr?: number;
|
|
107
|
+
/**
|
|
108
|
+
* Concentration of the alternative prior. Lower spreads the alternative over
|
|
109
|
+
* "anything materially worse", which is closer to how a broken case behaves
|
|
110
|
+
* than a point mass exactly `mde` below the baseline. At most
|
|
111
|
+
* `MAX_ALT_CONCENTRATION` (16) with the default `bayes` statistic.
|
|
112
|
+
*/
|
|
113
|
+
altConcentration?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Which e-value decides. `bayes` has more power and error control checked
|
|
116
|
+
* numerically; `universal` is valid at every rate by construction and needs
|
|
117
|
+
* more runs. See the README section "What that guarantee rests on".
|
|
118
|
+
*/
|
|
119
|
+
evidence?: Evidence;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const DEFAULT_GATE_OPTIONS = {
|
|
123
|
+
mde: 0.15,
|
|
124
|
+
fdr: 0.05,
|
|
125
|
+
altConcentration: 8,
|
|
126
|
+
evidence: "bayes",
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
129
|
+
export interface CaseVerdict {
|
|
130
|
+
id: string;
|
|
131
|
+
/** True when e-BH certifies this case as regressed at the suite's FDR. */
|
|
132
|
+
regressed: boolean;
|
|
133
|
+
/** The e-value. Larger is more evidence against the null. */
|
|
134
|
+
evalue: number;
|
|
135
|
+
/** log of the above, which is what the arithmetic actually uses. */
|
|
136
|
+
logE: number;
|
|
137
|
+
/**
|
|
138
|
+
* The largest e-value this case could ever reach against its current
|
|
139
|
+
* baseline, however many candidate runs you add. When this is below
|
|
140
|
+
* `threshold`, the case is undetectable at any candidate budget and the only
|
|
141
|
+
* remedy is more baseline runs.
|
|
142
|
+
*/
|
|
143
|
+
ceiling: number;
|
|
144
|
+
/** True when `ceiling` is below the bar the case has to clear. */
|
|
145
|
+
undetectable: boolean;
|
|
146
|
+
/**
|
|
147
|
+
* True when the baseline rate is at or below `mde`, so an `mde`-sized drop
|
|
148
|
+
* cannot happen. The case is not tested: its e-value is fixed at 1, which
|
|
149
|
+
* keeps it in the e-BH family without it ever being certified.
|
|
150
|
+
*/
|
|
151
|
+
impossible: boolean;
|
|
152
|
+
/** For a paired case, each arm's passes over the number of pairs. */
|
|
153
|
+
observed: { successes: number; trials: number };
|
|
154
|
+
baseline: { successes: number; trials: number };
|
|
155
|
+
design: "unpaired" | "paired";
|
|
156
|
+
/** The pair table, for a paired case. */
|
|
157
|
+
paired?: PairedCounts;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface GateResult {
|
|
161
|
+
/** PASS when nothing was certified as regressed. */
|
|
162
|
+
verdict: "PASS" | "FAIL";
|
|
163
|
+
cases: CaseVerdict[];
|
|
164
|
+
/** Cases certified as regressed, in the order given. */
|
|
165
|
+
regressed: CaseVerdict[];
|
|
166
|
+
/**
|
|
167
|
+
* Cases with no baseline. These are reported, not gated: they have no null to
|
|
168
|
+
* be tested against and they are excluded from the e-BH family.
|
|
169
|
+
*/
|
|
170
|
+
newCases: string[];
|
|
171
|
+
/** The e-value a case had to beat, given how many discoveries were made. */
|
|
172
|
+
threshold: number;
|
|
173
|
+
/** The e-value a case must reach to be certified whatever the others do. */
|
|
174
|
+
soloThreshold: number;
|
|
175
|
+
options: Required<GateOptions>;
|
|
176
|
+
/**
|
|
177
|
+
* One line an operator can read without knowing what an e-value is.
|
|
178
|
+
*
|
|
179
|
+
* Names the two things a passing suite can hide: cases with no baseline,
|
|
180
|
+
* which were not gated at all, and cases whose baseline is too thin to
|
|
181
|
+
* certify an `mde`-sized drop however many runs you add. A PASS that is
|
|
182
|
+
* really "nothing was checked" should not read like a PASS.
|
|
183
|
+
*/
|
|
184
|
+
headline: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Decide which cases regressed.
|
|
189
|
+
*
|
|
190
|
+
* Valid at any stopping time. Run every case to a fixed count, or stop each one
|
|
191
|
+
* the moment it looks decided, or anything in between; the guarantee does not
|
|
192
|
+
* depend on the rule you used.
|
|
193
|
+
*/
|
|
194
|
+
export function gate(
|
|
195
|
+
cases: readonly (GateCase | PairedGateCase)[],
|
|
196
|
+
options: GateOptions = {},
|
|
197
|
+
): GateResult {
|
|
198
|
+
const opts: Required<GateOptions> = { ...DEFAULT_GATE_OPTIONS, ...options };
|
|
199
|
+
requireOpenProbability(opts.mde, "mde", "gate");
|
|
200
|
+
requireOpenProbability(opts.fdr, "fdr", "gate");
|
|
201
|
+
requireEvidence(opts.evidence, "gate");
|
|
202
|
+
requireAltConcentration(opts.altConcentration, opts.evidence, "gate");
|
|
203
|
+
|
|
204
|
+
const seen = new Set<string>();
|
|
205
|
+
for (const c of cases) {
|
|
206
|
+
if (seen.has(c.id)) {
|
|
207
|
+
throw new PeeksafeError("PEEKSAFE_E_CASE_DUPLICATE", `gate: duplicate case id ${c.id}`, {
|
|
208
|
+
detail: { id: c.id },
|
|
209
|
+
hint: "two cases sharing an id would be merged into one baseline entry and counted once",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
seen.add(c.id);
|
|
213
|
+
if (c.paired) {
|
|
214
|
+
requirePairedCounts(c.paired, `gate(${c.id}).paired`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
requireCounts(c.successes, c.trials, `gate(${c.id})`);
|
|
218
|
+
if (c.baseline)
|
|
219
|
+
requireCounts(c.baseline.successes, c.baseline.trials, `gate(${c.id}).baseline`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const hasBaseline = (c: GateCase | PairedGateCase): boolean =>
|
|
223
|
+
c.paired !== undefined || (c.baseline !== undefined && c.baseline.trials > 0);
|
|
224
|
+
const newCases = cases.filter((c) => !hasBaseline(c)).map((c) => c.id);
|
|
225
|
+
const gated = cases.filter(hasBaseline);
|
|
226
|
+
|
|
227
|
+
if (gated.length === 0) {
|
|
228
|
+
throw new PeeksafeError(
|
|
229
|
+
"PEEKSAFE_E_BASELINE_MISSING",
|
|
230
|
+
"gate: no case has a baseline, so there is nothing to test against",
|
|
231
|
+
{
|
|
232
|
+
detail: { cases: cases.length, newCases: newCases.length },
|
|
233
|
+
hint: 'record a baseline on the reference revision first; gating without one compares against a Beta(1,1) prior, which reads as "this case passes half the time"',
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const m = gated.length;
|
|
239
|
+
// Testing these anyway clamps the alternative onto a rate near zero, which is
|
|
240
|
+
// where the case already sits, and the false positive rate then exceeds `fdr`.
|
|
241
|
+
// A paired case has an exact null and never needs this.
|
|
242
|
+
const impossible = gated.map((c) => !c.paired && cannotDropBy(c.baseline!, opts.mde));
|
|
243
|
+
const logEs = gated.map((c, i) => {
|
|
244
|
+
if (c.paired) return pairedLogE(c.paired.worse, discordant(c.paired));
|
|
245
|
+
return impossible[i]
|
|
246
|
+
? 0
|
|
247
|
+
: logEvidence(
|
|
248
|
+
opts.evidence,
|
|
249
|
+
c.successes,
|
|
250
|
+
c.trials,
|
|
251
|
+
c.baseline!.successes,
|
|
252
|
+
c.baseline!.trials,
|
|
253
|
+
opts.mde,
|
|
254
|
+
opts.altConcentration,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
const evalues = logEs.map(toEvalue);
|
|
258
|
+
const ebh = ebhCorrect(evalues, opts.fdr);
|
|
259
|
+
const solo = ebhSoloThreshold(m, opts.fdr);
|
|
260
|
+
|
|
261
|
+
const verdicts: CaseVerdict[] = gated.map((c, i) => {
|
|
262
|
+
const shared = { id: c.id, regressed: ebh.rejected[i]!, evalue: evalues[i]!, logE: logEs[i]! };
|
|
263
|
+
if (c.paired) {
|
|
264
|
+
const p = c.paired;
|
|
265
|
+
const pairs = p.bothPass + p.worse + p.better + p.bothFail;
|
|
266
|
+
return {
|
|
267
|
+
...shared,
|
|
268
|
+
ceiling: Infinity,
|
|
269
|
+
undetectable: false,
|
|
270
|
+
impossible: false,
|
|
271
|
+
observed: { successes: p.bothPass + p.better, trials: pairs },
|
|
272
|
+
baseline: { successes: p.bothPass + p.worse, trials: pairs },
|
|
273
|
+
design: "paired",
|
|
274
|
+
paired: { ...p },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// The ceiling is evaluated at the rate a regression would actually sit at,
|
|
278
|
+
// one MDE below the baseline's own rate. That is the alternative the gate is
|
|
279
|
+
// trying to detect, so it is the right place to ask "could this ever clear
|
|
280
|
+
// the bar". Matches how plan.ts prices the same case.
|
|
281
|
+
const bRate = c.baseline!.successes / c.baseline!.trials;
|
|
282
|
+
const pAlt = Math.max(1e-6, Math.min(1 - 1e-6, bRate - opts.mde));
|
|
283
|
+
const ceiling = impossible[i]
|
|
284
|
+
? 1
|
|
285
|
+
: Math.exp(
|
|
286
|
+
ceilingLogEvidence(
|
|
287
|
+
opts.evidence,
|
|
288
|
+
pAlt,
|
|
289
|
+
c.baseline!.successes,
|
|
290
|
+
c.baseline!.trials,
|
|
291
|
+
opts.mde,
|
|
292
|
+
opts.altConcentration,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
return {
|
|
296
|
+
...shared,
|
|
297
|
+
ceiling,
|
|
298
|
+
undetectable: !impossible[i] && ceiling < solo,
|
|
299
|
+
impossible: impossible[i]!,
|
|
300
|
+
observed: { successes: c.successes, trials: c.trials },
|
|
301
|
+
baseline: { successes: c.baseline!.successes, trials: c.baseline!.trials },
|
|
302
|
+
design: "unpaired",
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const regressed = verdicts.filter((v) => v.regressed);
|
|
307
|
+
const blind = verdicts.filter((v) => v.undetectable && !v.regressed);
|
|
308
|
+
const untested = verdicts.filter((v) => v.impossible);
|
|
309
|
+
|
|
310
|
+
const caveats: string[] = [];
|
|
311
|
+
if (blind.length > 0) {
|
|
312
|
+
caveats.push(
|
|
313
|
+
`${blind.length} of ${m} gated case(s) have a baseline too thin to certify a ` +
|
|
314
|
+
`${(opts.mde * 100).toFixed(0)}pt drop at any candidate budget (${blind
|
|
315
|
+
.slice(0, 3)
|
|
316
|
+
.map((v) => v.id)
|
|
317
|
+
.join(", ")}` +
|
|
318
|
+
`${blind.length > 3 ? ", …" : ""}), which needs more baseline runs, not more candidate runs`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if (untested.length > 0) {
|
|
322
|
+
caveats.push(
|
|
323
|
+
`${untested.length} case(s) pass at or below ${(opts.mde * 100).toFixed(0)}% and cannot drop by ` +
|
|
324
|
+
`${(opts.mde * 100).toFixed(0)}pts, so they were not tested (${untested
|
|
325
|
+
.slice(0, 3)
|
|
326
|
+
.map((v) => v.id)
|
|
327
|
+
.join(", ")}` +
|
|
328
|
+
`${untested.length > 3 ? ", …" : ""})`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (newCases.length > 0) {
|
|
332
|
+
caveats.push(
|
|
333
|
+
`${newCases.length} case(s) have no baseline and were not gated ` +
|
|
334
|
+
`(${newCases.slice(0, 3).join(", ")}${newCases.length > 3 ? ", …" : ""})`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const headline =
|
|
339
|
+
(regressed.length === 0
|
|
340
|
+
? `PASS: no case cleared the bar of ${solo.toFixed(0)} at ${(opts.fdr * 100).toFixed(0)}% FDR over ${m} gated case(s)`
|
|
341
|
+
: `FAIL: ${regressed.length} of ${m} gated case(s) certified as regressed at ${(opts.fdr * 100).toFixed(0)}% FDR ` +
|
|
342
|
+
`(${regressed
|
|
343
|
+
.slice(0, 3)
|
|
344
|
+
.map((v) => v.id)
|
|
345
|
+
.join(", ")}${regressed.length > 3 ? ", …" : ""})`) +
|
|
346
|
+
(caveats.length > 0 ? `. Read with care: ${caveats.join("; ")}.` : "");
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
verdict: regressed.length === 0 ? "PASS" : "FAIL",
|
|
350
|
+
cases: verdicts,
|
|
351
|
+
regressed,
|
|
352
|
+
newCases,
|
|
353
|
+
threshold: ebh.threshold,
|
|
354
|
+
soloThreshold: solo,
|
|
355
|
+
options: opts,
|
|
356
|
+
headline,
|
|
357
|
+
};
|
|
358
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peeksafe: statistically valid gates for non-deterministic eval suites.
|
|
3
|
+
*
|
|
4
|
+
* The problem this exists for: you run an eval suite, watch results arrive, stop
|
|
5
|
+
* a case when it looks decided, then correct for multiplicity with
|
|
6
|
+
* Benjamini-Hochberg. That construction has no error control. A p-value computed
|
|
7
|
+
* at a stopping boundary is not a p-value, and BH assumes it is.
|
|
8
|
+
*
|
|
9
|
+
* The library is organised in four layers, and most callers only need the first.
|
|
10
|
+
*
|
|
11
|
+
* ## 1. The decision
|
|
12
|
+
*
|
|
13
|
+
* gate(cases, options) which cases regressed, valid at any stopping time
|
|
14
|
+
*
|
|
15
|
+
* You bring counts. It brings e-values and e-BH. Nothing here knows or cares how
|
|
16
|
+
* you ran the evals, and nothing here will gate a case that has no baseline.
|
|
17
|
+
*
|
|
18
|
+
* ## 2. The budget
|
|
19
|
+
*
|
|
20
|
+
* makePlan(...) how many runs each case needs, and which are impossible
|
|
21
|
+
* computeFrontier(...) what is certifiable inside a budget
|
|
22
|
+
* affordabilityGrid(...) the same question as a grid over two axes
|
|
23
|
+
*
|
|
24
|
+
* These answer "how many runs do I need" before you spend anything, including
|
|
25
|
+
* the case where the honest answer is that no candidate budget will do and only
|
|
26
|
+
* more baseline runs will.
|
|
27
|
+
*
|
|
28
|
+
* ## 3. The check
|
|
29
|
+
*
|
|
30
|
+
* typeOneError(...) the error rate the gate delivers at rates you name
|
|
31
|
+
* certifyProbability(...) the same computation with separate baseline and candidate rates
|
|
32
|
+
* pairedCertifyProbability(...) the power of a paired design
|
|
33
|
+
*
|
|
34
|
+
* These are exact, not simulated, so a caller can check the gate on their own
|
|
35
|
+
* rates before trusting a verdict.
|
|
36
|
+
*
|
|
37
|
+
* ## 4. The statistics
|
|
38
|
+
*
|
|
39
|
+
* Everything the first two layers are built from, exported so a caller can check
|
|
40
|
+
* the arithmetic, build a different gate, or use one piece on its own: special
|
|
41
|
+
* functions, intervals, the SPRT, e-values, e-BH, power, paired designs, and the
|
|
42
|
+
* cluster-robust suite summary.
|
|
43
|
+
*
|
|
44
|
+
* Cases in a suite are not independent draws. Several cases generated from one
|
|
45
|
+
* source file share a failure mode, so a change that breaks the file moves all
|
|
46
|
+
* of them together. `clusteredEffect` and the rest of `cluster` are what stop
|
|
47
|
+
* the suite-level interval from being too narrow for that reason.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
type BaselineStat,
|
|
52
|
+
baselineNullRate,
|
|
53
|
+
type CaseRef,
|
|
54
|
+
toBaselineMap,
|
|
55
|
+
} from "./baseline.js";
|
|
56
|
+
export {
|
|
57
|
+
type CertifyProbabilityOptions,
|
|
58
|
+
certifyProbability,
|
|
59
|
+
type PairedCertifyProbabilityOptions,
|
|
60
|
+
pairedCertifyProbability,
|
|
61
|
+
type TypeOneErrorOptions,
|
|
62
|
+
typeOneError,
|
|
63
|
+
} from "./check.js";
|
|
64
|
+
export {
|
|
65
|
+
type ClusteredEffect,
|
|
66
|
+
type ClusteredMean,
|
|
67
|
+
type ClusterKeyDiagnostic,
|
|
68
|
+
type ClusterObservation,
|
|
69
|
+
caseFamily,
|
|
70
|
+
clusteredEffect,
|
|
71
|
+
clusterKey,
|
|
72
|
+
clusterKeyDiagnostic,
|
|
73
|
+
clusterRobustMean,
|
|
74
|
+
compareEstimators,
|
|
75
|
+
type FamilyEffect,
|
|
76
|
+
type GroupingSummary,
|
|
77
|
+
groupByCluster,
|
|
78
|
+
iidMean,
|
|
79
|
+
MIN_TRUSTWORTHY_CLUSTERS,
|
|
80
|
+
type RandomEffectsMean,
|
|
81
|
+
randomEffectsMean,
|
|
82
|
+
} from "./cluster.js";
|
|
83
|
+
/* ── errors and determinism ──────────────────────────────────────────────── */
|
|
84
|
+
export {
|
|
85
|
+
MAX_ALT_CONCENTRATION,
|
|
86
|
+
PeeksafeError,
|
|
87
|
+
type PeeksafeErrorCode,
|
|
88
|
+
type PeeksafeErrorOptions,
|
|
89
|
+
} from "./errors.js";
|
|
90
|
+
export type { Evidence } from "./evidence.js";
|
|
91
|
+
export {
|
|
92
|
+
computeFrontier,
|
|
93
|
+
costShares,
|
|
94
|
+
DEFAULT_AXES,
|
|
95
|
+
DEFAULT_FRONTIER,
|
|
96
|
+
enumerateFrontier,
|
|
97
|
+
evaluatePoint,
|
|
98
|
+
type FrontierAxes,
|
|
99
|
+
type FrontierBasis,
|
|
100
|
+
type FrontierCell,
|
|
101
|
+
type FrontierConfig,
|
|
102
|
+
type FrontierCost,
|
|
103
|
+
type FrontierDesign,
|
|
104
|
+
type FrontierPoint,
|
|
105
|
+
type FrontierResult,
|
|
106
|
+
type FrontierScreen,
|
|
107
|
+
fmtRate,
|
|
108
|
+
typicalObservationsPerCase,
|
|
109
|
+
} from "./frontier.js";
|
|
110
|
+
/* ── 1. the decision ─────────────────────────────────────────────────────── */
|
|
111
|
+
export {
|
|
112
|
+
type CaseVerdict,
|
|
113
|
+
DEFAULT_GATE_OPTIONS,
|
|
114
|
+
type GateCase,
|
|
115
|
+
type GateOptions,
|
|
116
|
+
type GateResult,
|
|
117
|
+
gate,
|
|
118
|
+
type PairedGateCase,
|
|
119
|
+
} from "./gate.js";
|
|
120
|
+
/* ── 2. the budget ───────────────────────────────────────────────────────── */
|
|
121
|
+
export {
|
|
122
|
+
type AffordabilityCell,
|
|
123
|
+
affordabilityGrid,
|
|
124
|
+
DEFAULT_PLAN,
|
|
125
|
+
type Detectability,
|
|
126
|
+
expectedSequentialSamples,
|
|
127
|
+
makePlan,
|
|
128
|
+
type Plan,
|
|
129
|
+
type PlanCase,
|
|
130
|
+
type PlanConfig,
|
|
131
|
+
type PlanTotals,
|
|
132
|
+
planCase,
|
|
133
|
+
} from "./plan.js";
|
|
134
|
+
export { hash32, makeRand, type Rand, streamFor } from "./rand.js";
|
|
135
|
+
/* ── 3. the statistics ───────────────────────────────────────────────────── */
|
|
136
|
+
export {
|
|
137
|
+
type BetaPosterior,
|
|
138
|
+
type BhResult,
|
|
139
|
+
bernoulliEntropy,
|
|
140
|
+
betaCredibleInterval,
|
|
141
|
+
betaMassBetween,
|
|
142
|
+
betaPosterior,
|
|
143
|
+
betaQuantile,
|
|
144
|
+
/* multiplicity */
|
|
145
|
+
bhCorrect,
|
|
146
|
+
cohensH,
|
|
147
|
+
diffInterval,
|
|
148
|
+
discordant,
|
|
149
|
+
type EbhResult,
|
|
150
|
+
ebhCorrect,
|
|
151
|
+
ebhSoloThreshold,
|
|
152
|
+
erf,
|
|
153
|
+
evidenceCeilingAsymptotic,
|
|
154
|
+
evidenceCeilingLogE,
|
|
155
|
+
evidenceCeilingSlope,
|
|
156
|
+
expectedLogE,
|
|
157
|
+
expectedLogEExact,
|
|
158
|
+
fisherExact2x2,
|
|
159
|
+
gammaP,
|
|
160
|
+
type Interval,
|
|
161
|
+
ibeta,
|
|
162
|
+
logBeta,
|
|
163
|
+
logBetaPdf,
|
|
164
|
+
/* special functions */
|
|
165
|
+
logGamma,
|
|
166
|
+
/* e-values */
|
|
167
|
+
logMarginalBetaBinomial,
|
|
168
|
+
mcnemarSamplesForEvidence,
|
|
169
|
+
normalCdf,
|
|
170
|
+
normalQuantile,
|
|
171
|
+
type PairedCounts,
|
|
172
|
+
pairedDiscordance,
|
|
173
|
+
/* paired designs */
|
|
174
|
+
pairedLogE,
|
|
175
|
+
pairPhi,
|
|
176
|
+
probabilityMoved,
|
|
177
|
+
type SprtDecision,
|
|
178
|
+
type SprtOpts,
|
|
179
|
+
type SprtResult,
|
|
180
|
+
/* power */
|
|
181
|
+
sampleSizeTwoProportion,
|
|
182
|
+
samplesForEvidence,
|
|
183
|
+
/* sequential */
|
|
184
|
+
sprtDecision,
|
|
185
|
+
sprtExpectedN,
|
|
186
|
+
type TestResult,
|
|
187
|
+
/* fixed-sample tests, for comparison rather than for gating */
|
|
188
|
+
twoProportionZTest,
|
|
189
|
+
twoSampleLogE,
|
|
190
|
+
twoSamplePriors,
|
|
191
|
+
universalCeilingLogE,
|
|
192
|
+
universalTwoSampleLogE,
|
|
193
|
+
/* intervals and posteriors */
|
|
194
|
+
wilsonInterval,
|
|
195
|
+
} from "./stats.js";
|
|
196
|
+
export {
|
|
197
|
+
DEFAULT_STOP_OPTIONS,
|
|
198
|
+
type PairedStopDecision,
|
|
199
|
+
type PairedStopOptions,
|
|
200
|
+
type PairedStopReason,
|
|
201
|
+
type StopDecision,
|
|
202
|
+
type StopOptions,
|
|
203
|
+
type StopReason,
|
|
204
|
+
shouldStop,
|
|
205
|
+
shouldStopPaired,
|
|
206
|
+
} from "./stop.js";
|