| Copyright | (c) 2026 Jared Tobin |
|---|---|
| License | MIT |
| Maintainer | Jared Tobin <jared@ppad.tech> |
| Safe Haskell | Safe-Inferred |
| Language | Haskell2010 |
Numeric.Eproc.Bernoulli
Description
One-sided Bernoulli rate anytime-valid test.
For samples x_t in {0, 1}, tests H_0: E[x] <= p_0 against
H_1: E[x] > p_0.
A single wealth process is run:
W_n = prod_{i=1..n} (1 + lambda_i * (x_i - p_0))where each per-step bet lambda_i is chosen predictably (from
data observed strictly before step i) and clipped to
[0, lambda_max] so that the wealth factor stays nonnegative for
every admissible observation. Under H_0 the wealth process is
a nonnegative supermartingale, so by Ville's inequality the
probability of W_n ever crossing 1 / alpha is at most
alpha, regardless of when the user decides to stop streaming
samples.
Unlike Numeric.Eproc.Bounded, the alternative here is one-sided,
so a single wealth process suffices and no Bonferroni adjustment
is needed -- the rejection threshold is log(1 / alpha).
Example
Test H_0: E[x] <= 0.05 at level alpha = 1e-3 against a stream
with empirical rate ~0.5:
>>>let cfg = config 1.0e-3 0.05 Newton>>>let xs = take 200 (cycle [True, False])>>>decide cfg (foldl' (update cfg) (initial cfg) xs)Reject
Synopsis
Test configuration and state
Bernoulli rate test configuration. Build with config.
Carries the bettor strategy, the baseline rate, the significance
level, the precomputed log-wealth rejection threshold, and the
safe-bet ceiling derived from p_0.
Test outcome at the current sample count.
Reject means the wealth process has crossed the rejection
threshold, so H_0 is rejected at level alpha. Continue
means there is not yet enough evidence; collect more samples
(or stop and report no rejection -- the type-I error guarantee
holds for any stopping rule).
Instances
Bettor strategies
A predictable bettor.
A bettor describes how, given the history of centred
observations z_t (each test module specifies its own centring;
see the per-module documentation), the next predictable bet
lambda_t is chosen. Predictability -- that is, lambda_t
depends only on data observed strictly before step t -- is
what makes the resulting wealth process a nonnegative
supermartingale under H_0.
For Adaptive and Newton, a safe-bet ceiling lambda_max
derived from the test's admissible-observation range is enforced
by clipping lambda to [0, lambda_max], so the wealth factor
stays nonnegative.
Fixedalways bets the supplied constantlambda. The wager does not respond to observed data; this strategy is useful only as a baseline.Adaptiveis the aGRAPA (approximate growth-rate adaptive predictable plug-in) bettor of Waudby-Smith & Ramdas (2024). It tracks the empirical meanmuand variancesigma^2of centred observations and bets the Kelly-optimal plug-inlambda* = mu / (sigma^2 + mu^2)clipped to[0, lambda_max]. Fast to compute and competitive in practice.Newtonis the online Newton step (ONS) bettor. The per-step log-wealth loss-log(1 + lambda * z)is convex inlambda; ONS performs one Newton step per observation, accumulating squared gradients to scale the update. Achieves logarithmic regret against the best constant bet in hindsight and is in practice the strongest of the three bettors under most signal regimes.
Construction
Arguments
| :: Double | significance level |
| -> Double | baseline rate |
| -> Bettor | bettor strategy |
| -> Config |
Build a Config for the Bernoulli rate test.
The safe-bet ceiling lambda_max is set so that the wealth
factor 1 + lambda * (x - p_0) stays nonnegative for both
x = 0 and x = 1. The binding constraint is x = 0, which
requires lambda <= 1 / p_0; the ceiling stored is half this
to leave numerical margin -- the WSR safety recommendation.
p_0 must lie strictly in (0, 1) and alpha strictly in
(0, 1). The degenerate case p_0 = 0 would make lambda_max
infinite (any divergence would reject immediately and the test
becomes uninteresting); the caller is expected to pass a small
positive baseline.
>>>let cfg = config 1.0e-3 0.05 Newton
Streaming
update :: Config -> State -> Bool -> State Source #
Fold one observation into the running State.
True means x_t = 1 (the event of interest occurred -- e.g.,
two readings diverged); False means x_t = 0 (they matched).
The caller decides what "matched" means at the application
level.
Computes the centred observation z = x - p_0, queries the
bettor for its predictable bet, accumulates log-wealth via
log_w' = log_w + log (1 + lambda * z)
and then steps the bettor state given the newly observed z.
>>>let s1 = update cfg s0 True
decide :: Config -> State -> Verdict Source #
Compute the current Verdict from the running State.
Reject iff log-wealth has crossed the threshold
log(1 / alpha); equivalently, wealth has exceeded
1 / alpha. Under H_0, by Ville's inequality, the
probability of this ever happening is at most alpha -- and
crucially this bound holds at every sample size
simultaneously, so the user is free to peek at the verdict as
often as they like and stop on the first Reject.
>>>decide cfg s0Continue
Inspection
log_wealth :: State -> Double Source #
The current log-wealth.
This is the natural "test statistic": it is monotone (in
expectation under H_1) in the evidence against H_0
accumulated so far, and the test rejects exactly when it crosses
log(1 / alpha).
>>>log_wealth s00.0