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

Numeric.Eproc.Bounded

Description

Two-sided bounded-mean anytime-valid test.

For samples x_t in [lo, hi], tests H_0: E[x] = m against H_1: E[x] /= m.

Internally two one-sided e-processes are run in parallel: a positive-direction process betting against the alternative E[x] > m (using centred observations z = x - m), and a negative-direction process betting against E[x] < m (using -z). Each maintains its own log-wealth and bettor state. The test rejects when either side's wealth crosses 2 / alpha; the factor of 2 is the Bonferroni adjustment for the two-sided union.

The test is anytime-valid: under H_0 the wealth process is a nonnegative supermartingale, so by Ville's inequality the probability of ever crossing the threshold is at most alpha, regardless of when the user decides to stop streaming samples.

Example

Test H_0: E[x] = 0.5 for x in [0, 1] at level alpha = 1e-3 against a stream with empirical mean 0.8:

>>> let cfg = config 0.5 0.0 1.0 1.0e-3 Newton
>>> let xs  = concat (replicate 30 [1, 1, 0, 1, 1, 0, 1, 1, 1, 1])
>>> decide cfg (foldl' (update cfg) (initial cfg) xs)
Reject
Synopsis

Test configuration and state

data Config Source #

Bounded-mean test configuration. Build with config.

Carries the bettor strategy, the null mean, the significance level, the precomputed Bonferroni-adjusted log-wealth threshold, and the per-direction safe-bet ceilings (see config for how the latter are derived from the sample bounds).

data State Source #

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

The two log-wealth fields track the running log-wealth of the positive- and negative-direction e-processes separately; decide compares each to the threshold and log_wealth returns the larger of the two. The per-direction bettor states carry whatever the chosen Bettor needs (running sums, current bet, etc.).

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

null mean m

-> Double

sample lower bound lo

-> Double

sample upper bound hi

-> Double

significance level alpha

-> Bettor

bettor strategy

-> Config 

Build a Config for the bounded-mean test.

Each per-direction safe-bet ceiling lambda_max is set so that the wealth factor stays nonnegative for every admissible observation:

  • The positive-direction factor is 1 + lambda_p * (x - m). Since x can dip to lo, x - m can reach lo - m (the most negative value), so we need lambda_p <= 1 / (m - lo). The ceiling stored is half this to leave numerical margin -- the WSR safety recommendation.
  • The negative-direction factor is 1 - lambda_n * (x - m). Since x can rise to hi, x - m can reach hi - m, so we need lambda_n <= 1 / (hi - m); again the ceiling is set to half this.

The log-wealth rejection threshold is precomputed as log(2 / alpha); the 2 is the Bonferroni union-bound adjustment for the two one-sided e-processes.

>>> let cfg = config 0.5 0.0 1.0 1.0e-3 Newton

initial :: Config -> State Source #

The initial State for a fresh streaming test.

Both directional log-wealths start at 0 (i.e., wealth 1) and both bettors start in the per-strategy initial state appropriate for the Bettor chosen in the Config.

>>> let s0 = initial cfg

Streaming

update :: Config -> State -> Double -> State Source #

Fold one observation into the running State.

Computes the centred observation z = x - m, queries the two directional bettors for their predictable bets, accumulates per-direction log-wealth via

log_w' = log_w + log (1 + lambda * z)

(with the symmetric -lambda for the negative direction), and then steps the bettor states given the newly observed z. The per-step wealth factor is floored at a tiny positive value to keep the log finite when a marginal bet drives the factor to (or below) zero.

>>> let s1 = update cfg s0 0.7

decide :: Config -> State -> Verdict Source #

Compute the current Verdict from the running State.

Reject iff either directional log-wealth has crossed the Bonferroni-adjusted threshold log(2 / alpha); equivalently, the wealth process on either side has exceeded 2 / 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, taken as the maximum of the two directional processes.

This is the natural "test statistic": it is monotone in the evidence against H_0 accumulated so far, and the test rejects exactly when it crosses log(2 / alpha).

>>> log_wealth s0
0.0

samples :: State -> Int Source #

The number of samples consumed so far.

>>> samples s0
0