E-values
An e-value for a null hypothesis is a nonnegative statistic with
Large values are evidence against : by Markov’s inequality,
. E-values multiply across independent
pieces of evidence, stay valid under optional stopping when built as
e-processes, allow the significance level to be chosen after the fact, and
give false-discovery-rate control under arbitrary dependence. CLI uses
them for drift monitors, batch FDR control, and label-efficient
calibration. All of it is in cli_sdk.stats.evalues.
Betting martingales
A drift monitor watches a stream of 0/1 outcomes : a miss (the returned set did not contain the label) for a coverage monitor, or a loss for a risk monitor. The null hypothesis is that the rate has not risen above its calibrated value :
For a betting fraction , define
Each factor is nonnegative (because and ) and has conditional expectation under , so is a nonnegative supermartingale starting at 1. Read it as the wealth of a gambler who bets a fraction of their wealth, each round, that the next outcome will be a miss: if misses really occur at rate at most , the bet cannot be expected to make money.
CLI does not require you to guess the size of the drift. It averages five bets, with :
An average of nonnegative supermartingales is again one, so keeps
the same guarantee. This is anytime.BettingMartingale.
Ville’s inequality
For any nonnegative supermartingale with ,
The bound is over the whole stream: it holds however long the monitor
runs and however often you look at it. A monitor alarms the first time
, where is the false_alarm_rate (so the
threshold is 20 at the default 0.05). The probability that it ever
alarms while holds is at most .
from cli_sdk.stats.evalues.anytime import BettingMartingale
m = BettingMartingale(null_rate=0.10, false_alarm_rate=0.05)
for miss in [0, 0, 1, 0, 1, 1, 0, 1]:
m.update(miss)
print(m.n, round(m.e_value, 2), m.triggered) # 8 27.85 TrueA classical test controls its error only at one pre-committed sample size.
Re-running it after every new outcome and alarming the first time it
rejects inflates the false-alarm rate far above its nominal level. In a
simulation with 400 in-spec streams of 3,000 outcomes each (miss rate
exactly 0.10), a one-sided z-test at level 0.05, re-run after every
outcome from the 30th on, alarmed on 43% of the streams. LocalMonitor at
false_alarm_rate=0.05 alarmed on 4.25%.
Coverage and risk monitors
| Class | Feeds | Tests |
|---|---|---|
eprocess.CoverageMonitor(target, false_alarm_rate) | update(covered: bool) | miss rate |
eprocess.RiskMonitor(target, false_alarm_rate) | update(is_loss: bool) | loss rate |
cli_sdk.LocalMonitor(type, target, false_alarm_rate) | update(outcome: bool) | either of the above; returns an Alert once |
CoverageMonitor.update and RiskMonitor.update return an alert dict
whenever the e-value is at or above the threshold (and None otherwise);
act on the first one. LocalMonitor wraps them and returns a typed
Alert only the first time.
The null is about the miss rate given the calibration you actually
deployed. Because realized coverage on a fixed calibration set scatters
around the target (see profile.realized_coverage_ci), a monitor set to
exactly the nominal target can eventually and correctly detect a profile
that landed slightly under it. Set the monitor target at or below the
lower end of the profile’s coverage interval if you only want to hear
about drift.
Detection delay
The monitor tests “the rate has been at most since the monitor started”. It is not a change-point detector with a bounded delay: during a long in-spec period the e-value shrinks toward zero and has to be rebuilt after a change. In simulation (coverage target 0.90, miss rate doubling from 0.10 to 0.20), a freshly started monitor fired after a median of 59 labelled outcomes; one that had first watched 1,000 in-spec outcomes fired a median of about 740 outcomes after the change. The further the pre-change rate sits inside the null, the faster the e-value shrinks and the longer the delay: with a 0.88 target and 90% coverage before the change, the median delay was 1,750 outcomes. Start a new monitor with each new profile version, and see the silent model update guide for a worked example and for windowed monitoring.
Post-hoc significance levels
For any e-value and any level ,
so for a level chosen after looking at the data,
.
The ratio of the chance of a false rejection to the level you reported
stays bounded in expectation, which fixed-level p-value procedures cannot
offer. In practice: report as the smallest level at which the
evidence rejects. An Alert carries its e_value for this purpose.
e-BH: FDR control under arbitrary dependence
Given e-values for hypotheses and a target false-discovery rate , sort them in decreasing order and let
(zero if no qualifies). Rejecting the hypotheses with the largest
e-values controls , where is the
number of true nulls, whatever the dependence between the e-values
(Wang & Ramdas, 2022). Benjamini-Hochberg on p-values needs independence or
positive dependence for the same statement. CLI uses e-BH for Gate
batches in "fdr" mode when the decisions may be correlated.
import numpy as np
from cli_sdk.stats.evalues import ebh
e_values = np.array([45.0, 0.3, 120.0, 2.0, 18.0, 0.9, 60.0, 1.1, 0.2, 35.0])
print(ebh.select(e_values, q=0.10)) # [0 2 6 9]Here : the sorted e-values 120, 60, 45, 35 clear , and the fifth (18) misses .
From p-values to e-values
A valid p-value (super-uniform under ) becomes a valid e-value through a calibrator:
Since and is decreasing, . The naive is not a valid e-value (its expectation under a uniform p-value is infinite). With the default , :
from cli_sdk.stats.evalues import ebh
ebh.calibrator_from_p_value(0.01) # 5.0
ebh.calibrator_from_p_value(0.0001) # 50.0Prediction-powered inference
Label-efficient calibration estimates a rate (coverage, error rate, judge-human agreement) from a small human-labelled sample plus a large pool labelled by a judge model. Let be human labels and the judge’s labels on the same items, and the judge’s labels on further items. The prediction-powered estimate of is
The second term is a rectifier: it measures the judge’s bias on the items humans labelled and subtracts it. For a fixed the estimate is unbiased whenever the human-labelled items are a uniformly random subset of the same population as the pool, whatever the judge’s quality. Its standard error is
and the reported interval is
. With
tune_lambda=True (PPI++), CLI sets
,
which is never worse than the human-only estimate () up to
estimation error; tune_lambda=False uses classical PPI ().
from cli_sdk.stats.evalues import ppi
result = ppi.estimate_mean(
labelled_predictions=judge_on_human_sample, # (n,) judge labels on the human-labelled items
labelled_labels=human_labels, # (n,) human labels on the same items
unlabelled_predictions=judge_on_pool, # (N,) judge labels on the pool
alpha=0.05,
)
print(result.estimate, result.ci_lower, result.ci_upper, result.lam, result.standard_error)The PPI interval is a large-sample (central limit theorem) interval, not
a finite-sample or anytime-valid one. It needs the human-labelled items
to be a random sample of the pool. A poor judge makes the interval
wider, never invalid; it just saves fewer labels. With the optimal
and a large pool, each human label is worth about
human-only labels, where is the judge-human
correlation (calibration.judge_quality reports this as
effective_label_multiplier).
See the label-efficient calibration guide for an end-to-end run.