| Copyright | (c) 2026 Jared Tobin |
|---|---|
| License | MIT |
| Maintainer | Jared Tobin <jared@ppad.tech> |
| Safe Haskell | None |
| Language | Haskell2010 |
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
Test configuration and state
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.).
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 | null mean |
| -> Double | sample lower bound |
| -> Double | sample upper bound |
| -> Double | significance level |
| -> 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). Sincexcan dip tolo,x - mcan reachlo - m(the most negative value), so we needlambda_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). Sincexcan rise tohi,x - mcan reachhi - m, so we needlambda_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
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 s0Continue
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 s00.0