peeksafe 0.3.0 → 0.4.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 +51 -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 +11 -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 +12 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peeksafe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Statistically valid gates for non-deterministic eval suites. Stop early without losing error control.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"evals",
|
|
@@ -10,7 +10,16 @@
|
|
|
10
10
|
"false-discovery-rate",
|
|
11
11
|
"llm",
|
|
12
12
|
"statistics",
|
|
13
|
-
"ci"
|
|
13
|
+
"ci",
|
|
14
|
+
"eval",
|
|
15
|
+
"llm-evals",
|
|
16
|
+
"regression-testing",
|
|
17
|
+
"flaky-tests",
|
|
18
|
+
"ab-testing",
|
|
19
|
+
"benjamini-hochberg",
|
|
20
|
+
"e-bh",
|
|
21
|
+
"early-stopping",
|
|
22
|
+
"significance"
|
|
14
23
|
],
|
|
15
24
|
"license": "Apache-2.0",
|
|
16
25
|
"author": "Alexis Rico",
|
package/src/check.ts
CHANGED
|
@@ -50,16 +50,20 @@ export interface CertifyProbabilityOptions extends Omit<TypeOneErrorOptions, "ra
|
|
|
50
50
|
* baseline at a horizon of 600 takes tens of milliseconds.
|
|
51
51
|
*/
|
|
52
52
|
export function certifyProbability(options: CertifyProbabilityOptions): number {
|
|
53
|
+
return certify(options, "certifyProbability");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function certify(options: CertifyProbabilityOptions, where: string): number {
|
|
53
57
|
const { baselineRate, candidateRate: pc, baselineTrials: nb, alpha, horizon } = options;
|
|
54
58
|
const mde = options.mde ?? DEFAULT_GATE_OPTIONS.mde;
|
|
55
59
|
const concentration = options.altConcentration ?? DEFAULT_GATE_OPTIONS.altConcentration;
|
|
56
60
|
const evidence = options.evidence ?? DEFAULT_GATE_OPTIONS.evidence;
|
|
57
|
-
requireEvidence(evidence,
|
|
58
|
-
requireOpenProbability(baselineRate, "baselineRate",
|
|
59
|
-
requireOpenProbability(pc, "candidateRate",
|
|
60
|
-
requireOpenProbability(alpha, "alpha",
|
|
61
|
-
requireOpenProbability(mde, "mde",
|
|
62
|
-
requirePositiveConfig(concentration, "altConcentration",
|
|
61
|
+
requireEvidence(evidence, where);
|
|
62
|
+
requireOpenProbability(baselineRate, "baselineRate", where);
|
|
63
|
+
requireOpenProbability(pc, "candidateRate", where);
|
|
64
|
+
requireOpenProbability(alpha, "alpha", where);
|
|
65
|
+
requireOpenProbability(mde, "mde", where);
|
|
66
|
+
requirePositiveConfig(concentration, "altConcentration", where);
|
|
63
67
|
for (const [name, v] of [
|
|
64
68
|
["baselineTrials", nb],
|
|
65
69
|
["horizon", horizon],
|
|
@@ -67,7 +71,7 @@ export function certifyProbability(options: CertifyProbabilityOptions): number {
|
|
|
67
71
|
if (!Number.isInteger(v) || v < 1) {
|
|
68
72
|
throw new PeeksafeError(
|
|
69
73
|
"PEEKSAFE_E_CONFIG",
|
|
70
|
-
|
|
74
|
+
`${where}: ${name} must be a positive integer, got ${v}`,
|
|
71
75
|
{
|
|
72
76
|
detail: { [name]: v },
|
|
73
77
|
},
|
|
@@ -164,7 +168,7 @@ function universalFromBayes(rawLog: number, s: number, n: number, sb: number, nb
|
|
|
164
168
|
export function typeOneError(options: TypeOneErrorOptions): number {
|
|
165
169
|
const { rate, ...rest } = options;
|
|
166
170
|
requireOpenProbability(rate, "rate", "typeOneError");
|
|
167
|
-
return
|
|
171
|
+
return certify({ ...rest, baselineRate: rate, candidateRate: rate }, "typeOneError");
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
export interface PairedCertifyProbabilityOptions {
|
package/src/cluster.ts
CHANGED
|
@@ -135,12 +135,16 @@ function requireLevel(level: number, where: string): void {
|
|
|
135
135
|
|
|
136
136
|
/** Group observations by cluster, preserving first-seen order. */
|
|
137
137
|
export function groupByCluster(obs: ClusterObservation[]): Map<string, number[]> {
|
|
138
|
+
return group(obs, "groupByCluster");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function group(obs: ClusterObservation[], where: string): Map<string, number[]> {
|
|
138
142
|
const m = new Map<string, number[]>();
|
|
139
143
|
for (const o of obs) {
|
|
140
144
|
if (!Number.isFinite(o.value)) {
|
|
141
145
|
throw new PeeksafeError(
|
|
142
146
|
"PEEKSAFE_E_STAT_DOMAIN",
|
|
143
|
-
|
|
147
|
+
`${where}: value for cluster "${o.cluster}" is not finite`,
|
|
144
148
|
{
|
|
145
149
|
detail: { cluster: o.cluster, value: o.value },
|
|
146
150
|
},
|
|
@@ -220,7 +224,7 @@ export function iidMean(values: number[], level = 0.95): Interval {
|
|
|
220
224
|
*/
|
|
221
225
|
export function clusterRobustMean(obs: ClusterObservation[], level = 0.95): ClusteredMean {
|
|
222
226
|
requireLevel(level, "clusterRobustMean");
|
|
223
|
-
const groups =
|
|
227
|
+
const groups = group(obs, "clusterRobustMean");
|
|
224
228
|
const n = obs.length;
|
|
225
229
|
const G = groups.size;
|
|
226
230
|
const sizes = [...groups.values()].map((xs) => xs.length);
|
|
@@ -355,7 +359,7 @@ export interface RandomEffectsMean extends Interval {
|
|
|
355
359
|
*/
|
|
356
360
|
export function randomEffectsMean(obs: ClusterObservation[], level = 0.95): RandomEffectsMean {
|
|
357
361
|
requireLevel(level, "randomEffectsMean");
|
|
358
|
-
const groups =
|
|
362
|
+
const groups = group(obs, "randomEffectsMean");
|
|
359
363
|
const n = obs.length;
|
|
360
364
|
const G = groups.size;
|
|
361
365
|
const grand = n > 0 ? obs.reduce((a, o) => a + o.value, 0) / n : 0;
|
|
@@ -529,6 +533,9 @@ const fmtPts = (x: number) => `${x >= 0 ? "+" : ""}${(x * 100).toFixed(1)}`;
|
|
|
529
533
|
* a reader would conclude.
|
|
530
534
|
*/
|
|
531
535
|
export function clusteredEffect(obs: ClusterObservation[], level = 0.95): ClusteredEffect {
|
|
536
|
+
// Checked before the estimators below, which would refuse the same input in their own names.
|
|
537
|
+
requireLevel(level, "clusteredEffect");
|
|
538
|
+
const groups = group(obs, "clusteredEffect");
|
|
532
539
|
const cr2 = clusterRobustMean(obs, level);
|
|
533
540
|
const re = randomEffectsMean(obs, level);
|
|
534
541
|
const naive = iidMean(
|
|
@@ -536,7 +543,6 @@ export function clusteredEffect(obs: ClusterObservation[], level = 0.95): Cluste
|
|
|
536
543
|
level,
|
|
537
544
|
);
|
|
538
545
|
|
|
539
|
-
const groups = groupByCluster(obs);
|
|
540
546
|
const families: FamilyEffect[] = [...groups.entries()]
|
|
541
547
|
.map(([family, xs]) => {
|
|
542
548
|
const m = xs.reduce((a, x) => a + x, 0) / xs.length;
|
|
@@ -741,6 +747,8 @@ export function clusterKeyDiagnostic(
|
|
|
741
747
|
},
|
|
742
748
|
);
|
|
743
749
|
}
|
|
750
|
+
group(declared, "clusterKeyDiagnostic");
|
|
751
|
+
group(fallback, "clusterKeyDiagnostic");
|
|
744
752
|
const d = summarise("declared", declared);
|
|
745
753
|
const f = summarise("case-id", fallback);
|
|
746
754
|
|
package/src/gate.ts
CHANGED
|
@@ -189,6 +189,39 @@ export interface GateResult {
|
|
|
189
189
|
headline: string;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/** A number as JSON can hold it: finite as is, the rest as the string `String` gives, which `Number` reads back. */
|
|
193
|
+
export type JsonNumber = number | "Infinity" | "-Infinity" | "NaN";
|
|
194
|
+
|
|
195
|
+
/** A `CaseVerdict` as `gateReport` writes it. A paired case's `ceiling` is `"Infinity"`. */
|
|
196
|
+
export type CaseReport = Omit<CaseVerdict, "ceiling" | "logE"> & {
|
|
197
|
+
ceiling: JsonNumber;
|
|
198
|
+
logE: JsonNumber;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** A `GateResult` as `gateReport` writes it, and as `JSON.stringify(result)` does. */
|
|
202
|
+
export type GateReport = Omit<GateResult, "cases" | "regressed"> & {
|
|
203
|
+
cases: CaseReport[];
|
|
204
|
+
regressed: CaseReport[];
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const jsonSafe = (v: unknown): unknown => {
|
|
208
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : String(v);
|
|
209
|
+
if (Array.isArray(v)) return v.map(jsonSafe);
|
|
210
|
+
if (v !== null && typeof v === "object") {
|
|
211
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, jsonSafe(x)]));
|
|
212
|
+
}
|
|
213
|
+
return v;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A copy of `result` that survives JSON: `JSON.stringify` writes `Infinity` as
|
|
218
|
+
* `null`, which a reader of a CI artifact takes for "no ceiling" where the truth
|
|
219
|
+
* is "no limit". Every non-finite number becomes a string instead.
|
|
220
|
+
*/
|
|
221
|
+
export function gateReport(result: GateResult): GateReport {
|
|
222
|
+
return jsonSafe(result) as GateReport;
|
|
223
|
+
}
|
|
224
|
+
|
|
192
225
|
/**
|
|
193
226
|
* Decide which cases regressed.
|
|
194
227
|
*
|
|
@@ -220,8 +253,19 @@ export function gate(
|
|
|
220
253
|
continue;
|
|
221
254
|
}
|
|
222
255
|
requireCounts(c.successes, c.trials, `gate(${c.id})`);
|
|
223
|
-
if (c.baseline)
|
|
224
|
-
|
|
256
|
+
if (!c.baseline) continue;
|
|
257
|
+
// Only the counts are read, so a baseline joined to the wrong case was gated against without a word.
|
|
258
|
+
if (c.baseline.caseId !== c.id) {
|
|
259
|
+
throw new PeeksafeError(
|
|
260
|
+
"PEEKSAFE_E_CONFIG",
|
|
261
|
+
`gate: case ${c.id} carries the baseline of case ${c.baseline.caseId}`,
|
|
262
|
+
{
|
|
263
|
+
detail: { id: c.id, baselineCaseId: c.baseline.caseId },
|
|
264
|
+
hint: "join baselines to cases by id, e.g. toBaselineMap(baselines).get(c.id)",
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
requireCounts(c.baseline.successes, c.baseline.trials, `gate(${c.id}).baseline`);
|
|
225
269
|
}
|
|
226
270
|
|
|
227
271
|
const hasBaseline = (c: GateCase | PairedGateCase): boolean =>
|
|
@@ -350,7 +394,7 @@ export function gate(
|
|
|
350
394
|
.join(", ")}${regressed.length > 3 ? ", …" : ""})`) +
|
|
351
395
|
(caveats.length > 0 ? `. Read with care: ${caveats.join("; ")}.` : "");
|
|
352
396
|
|
|
353
|
-
|
|
397
|
+
const result: GateResult = {
|
|
354
398
|
verdict: regressed.length === 0 ? "PASS" : "FAIL",
|
|
355
399
|
cases: verdicts,
|
|
356
400
|
regressed,
|
|
@@ -360,4 +404,7 @@ export function gate(
|
|
|
360
404
|
options: opts,
|
|
361
405
|
headline,
|
|
362
406
|
};
|
|
407
|
+
// Not enumerable, so equality checks and spreads see only the fields.
|
|
408
|
+
Object.defineProperty(result, "toJSON", { value: () => gateReport(result) });
|
|
409
|
+
return result;
|
|
363
410
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* ## 1. The decision
|
|
12
12
|
*
|
|
13
13
|
* gate(cases, options) which cases regressed, valid at any stopping time
|
|
14
|
+
* gateReport(result) the same verdict with Infinity kept through JSON
|
|
14
15
|
*
|
|
15
16
|
* You bring counts. It brings e-values and e-BH. Nothing here knows or cares how
|
|
16
17
|
* you ran the evals, and nothing here will gate a case that has no baseline.
|
|
@@ -109,12 +110,16 @@ export {
|
|
|
109
110
|
} from "./frontier.js";
|
|
110
111
|
/* ── 1. the decision ─────────────────────────────────────────────────────── */
|
|
111
112
|
export {
|
|
113
|
+
type CaseReport,
|
|
112
114
|
type CaseVerdict,
|
|
113
115
|
DEFAULT_GATE_OPTIONS,
|
|
114
116
|
type GateCase,
|
|
115
117
|
type GateOptions,
|
|
118
|
+
type GateReport,
|
|
116
119
|
type GateResult,
|
|
117
120
|
gate,
|
|
121
|
+
gateReport,
|
|
122
|
+
type JsonNumber,
|
|
118
123
|
type PairedGateCase,
|
|
119
124
|
} from "./gate.js";
|
|
120
125
|
/* ── 2. the budget ───────────────────────────────────────────────────────── */
|
package/src/plan.ts
CHANGED
|
@@ -162,9 +162,10 @@ export interface PlanCase {
|
|
|
162
162
|
*/
|
|
163
163
|
design: "unpaired" | "paired" | null;
|
|
164
164
|
/**
|
|
165
|
-
*
|
|
166
|
-
* work, or null if it already works.
|
|
167
|
-
*
|
|
165
|
+
* The smallest total baseline size (not an increment over the current one)
|
|
166
|
+
* at which the *unpaired* design would work, or null if it already works.
|
|
167
|
+
* A floor, not a threshold: see `baselineRunsToClear`. This is the
|
|
168
|
+
* actionable half of an "undetectable" verdict.
|
|
168
169
|
*/
|
|
169
170
|
baselineRunsNeeded: number | null;
|
|
170
171
|
/**
|
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);
|