ppad-eproc-0.1.0: Anytime-valid sequential testing via e-processes.
Copyright(c) 2026 Jared Tobin
LicenseMIT
MaintainerJared Tobin <jared@ppad.tech>
Safe HaskellSafe-Inferred
LanguageHaskell2010

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

data Config Source #

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.

data State Source #

Streaming test state. Construct with initial and fold observations through update.

Carries the sample count, running log-wealth, and whatever per-step state the chosen Bettor needs.

data Verdict Source #

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).

Constructors

Reject 
Continue 

Instances

Instances details
Show Verdict Source # 
Instance details

Defined in Numeric.Eproc.Common

Eq Verdict Source # 
Instance details

Defined in Numeric.Eproc.Common

Methods

(==) :: Verdict -> Verdict -> Bool #

(/=) :: Verdict -> Verdict -> Bool #

Bettor strategies

data Bettor Source #

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.

  • Fixed always bets the supplied constant lambda. The wager does not respond to observed data; this strategy is useful only as a baseline.
  • Adaptive is the aGRAPA (approximate growth-rate adaptive predictable plug-in) bettor of Waudby-Smith & Ramdas (2024). It tracks the empirical mean mu and variance sigma^2 of centred observations and bets the Kelly-optimal plug-in lambda* = mu / (sigma^2 + mu^2) clipped to [0, lambda_max]. Fast to compute and competitive in practice.
  • Newton is the online Newton step (ONS) bettor. The per-step log-wealth loss -log(1 + lambda * z) is convex in lambda; 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.

Constructors

Fixed !Double 
Adaptive 
Newton 

Instances

Instances details
Show Bettor Source # 
Instance details

Defined in Numeric.Eproc.Common

Eq Bettor Source # 
Instance details

Defined in Numeric.Eproc.Common

Methods

(==) :: Bettor -> Bettor -> Bool #

(/=) :: Bettor -> Bettor -> Bool #

Construction

config Source #

Arguments

:: Double

significance level alpha, in (0, 1)

-> Double

baseline rate p_0, in (0, 1)

-> 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

initial :: Config -> State Source #

The initial State for a fresh streaming test.

Log-wealth starts at 0 (i.e., wealth 1) and the bettor starts in the per-strategy initial state appropriate for the Bettor chosen in the Config.

>>> let s0 = initial cfg

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 s0
Continue

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 s0
0.0

samples :: State -> Int Source #

The number of samples consumed so far.

>>> samples s0
0