commit c8c70f93df09bbe173951220453ef661036d33e3
parent f84c84ac03843da025ad73533629af10e6867691
Author: Jared Tobin <jared@jtobin.io>
Date: Wed, 3 Jun 2026 10:22:57 -0230
rename TwoSample to Test
Module Numeric.Eproc.TwoSample becomes Numeric.Eproc.Test. The 'TS'
import alias becomes 'T' to match the single-letter M / B convention.
Local test helpers ('run_ts_paired', 'ts_avg_rate') renamed to
'run_paired' and 'paired_avg_rate' for the same reason. Benchmark
bgroup labels ('TwoSample.update ...') updated to 'Test.update ...'.
No semantic / API changes; tests pass.
Diffstat:
7 files changed, 164 insertions(+), 164 deletions(-)
diff --git a/README.md b/README.md
@@ -46,7 +46,7 @@ A sample GHCi session:
```
For the paired two-sample mean-equality test, see
-`Numeric.Eproc.TwoSample`.
+`Numeric.Eproc.Test`.
## Documentation
diff --git a/bench/Main.hs b/bench/Main.hs
@@ -6,14 +6,14 @@ module Main where
import Control.DeepSeq
import qualified Numeric.Eproc.Bettor as B
import qualified Numeric.Eproc.Mean as M
-import qualified Numeric.Eproc.TwoSample as TS
+import qualified Numeric.Eproc.Test as T
import Criterion.Main
-- all relevant fields are strict (and UNPACK'd for the doubles), so
-- WHNF == NF for these types. orphan instances keep the library API
-- untouched.
instance NFData M.State where rnf !_ = ()
-instance NFData TS.State where rnf !_ = ()
+instance NFData T.State where rnf !_ = ()
instance NFData M.Verdict where rnf !_ = ()
main :: IO ()
@@ -63,11 +63,11 @@ stream =
twosample :: Benchmark
twosample =
let !ps = force (take 1000 (cycle [(0.3, 0.7), (0.7, 0.3)]))
- !cfg_f = TS.config 0.0 1.0 1.0e-3 (B.Fixed 0.5)
- !cfg_a = TS.config 0.0 1.0 1.0e-3 B.Agrapa
- !cfg_o = TS.config 0.0 1.0 1.0e-3 B.Ons
- run_t cfg = foldl' (TS.update cfg) (TS.initial cfg)
- in bgroup "TwoSample.update (1000-sample fold)" [
+ !cfg_f = T.config 0.0 1.0 1.0e-3 (B.Fixed 0.5)
+ !cfg_a = T.config 0.0 1.0 1.0e-3 B.Agrapa
+ !cfg_o = T.config 0.0 1.0 1.0e-3 B.Ons
+ run_t cfg = foldl' (T.update cfg) (T.initial cfg)
+ in bgroup "Test.update (1000-sample fold)" [
bench "fixed" $ nf (run_t cfg_f) ps
, bench "agrapa" $ nf (run_t cfg_a) ps
, bench "ons" $ nf (run_t cfg_o) ps
diff --git a/bench/Weight.hs b/bench/Weight.hs
@@ -6,11 +6,11 @@ module Main where
import Control.DeepSeq
import qualified Numeric.Eproc.Bettor as B
import qualified Numeric.Eproc.Mean as M
-import qualified Numeric.Eproc.TwoSample as TS
+import qualified Numeric.Eproc.Test as T
import Weigh
instance NFData M.State where rnf !_ = ()
-instance NFData TS.State where rnf !_ = ()
+instance NFData T.State where rnf !_ = ()
instance NFData M.Verdict where rnf !_ = ()
-- note that 'weigh' doesn't work properly in a repl
@@ -56,11 +56,11 @@ stream =
twosample :: Weigh ()
twosample =
let !ps = force (take 1000 (cycle [(0.3, 0.7), (0.7, 0.3)]))
- !cfg_f = TS.config 0.0 1.0 1.0e-3 (B.Fixed 0.5)
- !cfg_a = TS.config 0.0 1.0 1.0e-3 B.Agrapa
- !cfg_o = TS.config 0.0 1.0 1.0e-3 B.Ons
- run_t cfg = foldl' (TS.update cfg) (TS.initial cfg)
- in wgroup "TwoSample.update (1000-sample fold)" $ do
+ !cfg_f = T.config 0.0 1.0 1.0e-3 (B.Fixed 0.5)
+ !cfg_a = T.config 0.0 1.0 1.0e-3 B.Agrapa
+ !cfg_o = T.config 0.0 1.0 1.0e-3 B.Ons
+ run_t cfg = foldl' (T.update cfg) (T.initial cfg)
+ in wgroup "Test.update (1000-sample fold)" $ do
func "fixed" (run_t cfg_f) ps
func "agrapa" (run_t cfg_a) ps
func "ons" (run_t cfg_o) ps
diff --git a/lib/Numeric/Eproc/Test.hs b/lib/Numeric/Eproc/Test.hs
@@ -0,0 +1,132 @@
+{-# OPTIONS_HADDOCK prune #-}
+{-# LANGUAGE BangPatterns #-}
+
+-- |
+-- Module: Numeric.Eproc.Test
+-- Copyright: (c) 2026 Jared Tobin
+-- License: MIT
+-- Maintainer: Jared Tobin <jared@ppad.tech>
+--
+-- Paired two-sample anytime-valid mean-equality test.
+--
+-- For paired observations @(a_t, b_t)@ where both samples lie in
+-- @[lo, hi]@, tests @H_0: E[a] = E[b]@ against
+-- @H_1: E[a] /= E[b]@.
+--
+-- The reduction is straightforward: under the null, the differences
+-- @d_t = a_t - b_t@ have mean zero, and differences of @[lo, hi]@
+-- values lie in @[lo - hi, hi - lo]@. So the paired test is just
+-- the bounded-mean test ("Numeric.Eproc.Mean") on @d_t@ with
+-- null mean @0@ and sample bounds @[lo - hi, hi - lo]@.
+--
+-- Pairing is required: independent two-sample testing without
+-- alignment would need to bet against a richer alternative (the
+-- joint distribution rather than the marginal difference) and is
+-- beyond the scope of this module.
+
+module Numeric.Eproc.Test (
+ -- * Test configuration and state
+ Config
+ , State
+ , Verdict(..)
+
+ -- * Construction
+ , config
+ , initial
+
+ -- * Streaming
+ , update
+ , decide
+
+ -- * Inspection
+ , log_wealth
+ , samples
+ ) where
+
+import qualified Numeric.Eproc.Mean as M
+import Numeric.Eproc.Mean (Verdict(..))
+import Numeric.Eproc.Bettor (Bettor)
+
+-- types ----------------------------------------------------------------------
+
+-- | Paired two-sample test configuration. Build with 'config'. Wraps
+-- a 'Numeric.Eproc.Mean.Config' for the underlying
+-- difference test.
+newtype Config = Config M.Config
+
+-- | Streaming paired two-sample test state. Construct with 'initial'
+-- and fold paired observations through 'update'.
+newtype State = State M.State
+
+-- construction ---------------------------------------------------------------
+
+-- | Build a 'Config' for the paired two-sample test.
+--
+-- Bounds @lo@ and @hi@ are the (shared) bounds on the individual
+-- @a@ and @b@ samples; the underlying mean test is then configured
+-- on the differences, which lie in @[lo - hi, hi - lo]@ with null
+-- mean @0@.
+--
+-- >>> import qualified Numeric.Eproc.Bettor as B
+-- >>> let cfg = config 0.0 1.0 1.0e-3 B.Ons
+config
+ :: Double -- ^ sample lower bound @lo@
+ -> Double -- ^ sample upper bound @hi@
+ -> Double -- ^ significance level @alpha@
+ -> Bettor -- ^ bettor strategy
+ -> Config
+config !lo !hi !alpha b =
+ let !d = hi - lo
+ in Config (M.config 0 (negate d) d alpha b)
+{-# INLINE config #-}
+
+-- | The initial 'State' for a fresh streaming test.
+--
+-- >>> let s0 = initial cfg
+initial :: Config -> State
+initial (Config c) = State (M.initial c)
+{-# INLINE initial #-}
+
+-- streaming ------------------------------------------------------------------
+
+-- | Fold one paired observation @(a, b)@ into the running 'State'.
+--
+-- Equivalent to feeding the difference @a - b@ into the underlying
+-- bounded-mean test.
+--
+-- >>> let s1 = update cfg s0 (0.3, 0.7)
+update :: Config -> State -> (Double, Double) -> State
+update (Config c) (State s) (!a, !b) =
+ State (M.update c s (a - b))
+{-# INLINE update #-}
+
+-- | Compute the current 'Verdict' from the running 'State'.
+--
+-- 'Reject' iff either directional log-wealth of the underlying
+-- bounded-mean test on the differences has crossed
+-- @log(2 \/ alpha)@.
+--
+-- >>> decide cfg s0
+-- Continue
+decide :: Config -> State -> Verdict
+decide (Config c) (State s) = M.decide c s
+{-# INLINE decide #-}
+
+-- inspection -----------------------------------------------------------------
+
+-- | The current log-wealth of the underlying bounded-mean test on
+-- the differences.
+--
+-- >>> log_wealth s0
+-- 0.0
+log_wealth :: State -> Double
+log_wealth (State s) = M.log_wealth s
+{-# INLINE log_wealth #-}
+
+-- | The number of paired observations consumed so far.
+--
+-- >>> samples s0
+-- 0
+samples :: State -> Int
+samples (State s) = M.samples s
+{-# INLINE samples #-}
diff --git a/lib/Numeric/Eproc/TwoSample.hs b/lib/Numeric/Eproc/TwoSample.hs
@@ -1,132 +0,0 @@
-{-# OPTIONS_HADDOCK prune #-}
-{-# LANGUAGE BangPatterns #-}
-
--- |
--- Module: Numeric.Eproc.TwoSample
--- Copyright: (c) 2026 Jared Tobin
--- License: MIT
--- Maintainer: Jared Tobin <jared@ppad.tech>
---
--- Paired two-sample anytime-valid mean-equality test.
---
--- For paired observations @(a_t, b_t)@ where both samples lie in
--- @[lo, hi]@, tests @H_0: E[a] = E[b]@ against
--- @H_1: E[a] /= E[b]@.
---
--- The reduction is straightforward: under the null, the differences
--- @d_t = a_t - b_t@ have mean zero, and differences of @[lo, hi]@
--- values lie in @[lo - hi, hi - lo]@. So the paired test is just
--- the bounded-mean test ("Numeric.Eproc.Mean") on @d_t@ with
--- null mean @0@ and sample bounds @[lo - hi, hi - lo]@.
---
--- Pairing is required: independent two-sample testing without
--- alignment would need to bet against a richer alternative (the
--- joint distribution rather than the marginal difference) and is
--- beyond the scope of this module.
-
-module Numeric.Eproc.TwoSample (
- -- * Test configuration and state
- Config
- , State
- , Verdict(..)
-
- -- * Construction
- , config
- , initial
-
- -- * Streaming
- , update
- , decide
-
- -- * Inspection
- , log_wealth
- , samples
- ) where
-
-import qualified Numeric.Eproc.Mean as M
-import Numeric.Eproc.Mean (Verdict(..))
-import Numeric.Eproc.Bettor (Bettor)
-
--- types ----------------------------------------------------------------------
-
--- | Paired two-sample test configuration. Build with 'config'. Wraps
--- a 'Numeric.Eproc.Mean.Config' for the underlying
--- difference test.
-newtype Config = Config M.Config
-
--- | Streaming paired two-sample test state. Construct with 'initial'
--- and fold paired observations through 'update'.
-newtype State = State M.State
-
--- construction ---------------------------------------------------------------
-
--- | Build a 'Config' for the paired two-sample test.
---
--- Bounds @lo@ and @hi@ are the (shared) bounds on the individual
--- @a@ and @b@ samples; the underlying mean test is then configured
--- on the differences, which lie in @[lo - hi, hi - lo]@ with null
--- mean @0@.
---
--- >>> import qualified Numeric.Eproc.Bettor as B
--- >>> let cfg = config 0.0 1.0 1.0e-3 B.Ons
-config
- :: Double -- ^ sample lower bound @lo@
- -> Double -- ^ sample upper bound @hi@
- -> Double -- ^ significance level @alpha@
- -> Bettor -- ^ bettor strategy
- -> Config
-config !lo !hi !alpha b =
- let !d = hi - lo
- in Config (M.config 0 (negate d) d alpha b)
-{-# INLINE config #-}
-
--- | The initial 'State' for a fresh streaming test.
---
--- >>> let s0 = initial cfg
-initial :: Config -> State
-initial (Config c) = State (M.initial c)
-{-# INLINE initial #-}
-
--- streaming ------------------------------------------------------------------
-
--- | Fold one paired observation @(a, b)@ into the running 'State'.
---
--- Equivalent to feeding the difference @a - b@ into the underlying
--- bounded-mean test.
---
--- >>> let s1 = update cfg s0 (0.3, 0.7)
-update :: Config -> State -> (Double, Double) -> State
-update (Config c) (State s) (!a, !b) =
- State (M.update c s (a - b))
-{-# INLINE update #-}
-
--- | Compute the current 'Verdict' from the running 'State'.
---
--- 'Reject' iff either directional log-wealth of the underlying
--- bounded-mean test on the differences has crossed
--- @log(2 \/ alpha)@.
---
--- >>> decide cfg s0
--- Continue
-decide :: Config -> State -> Verdict
-decide (Config c) (State s) = M.decide c s
-{-# INLINE decide #-}
-
--- inspection -----------------------------------------------------------------
-
--- | The current log-wealth of the underlying bounded-mean test on
--- the differences.
---
--- >>> log_wealth s0
--- 0.0
-log_wealth :: State -> Double
-log_wealth (State s) = M.log_wealth s
-{-# INLINE log_wealth #-}
-
--- | The number of paired observations consumed so far.
---
--- >>> samples s0
--- 0
-samples :: State -> Int
-samples (State s) = M.samples s
-{-# INLINE samples #-}
diff --git a/ppad-eproc.cabal b/ppad-eproc.cabal
@@ -36,7 +36,7 @@ library
exposed-modules:
Numeric.Eproc.Bettor
Numeric.Eproc.Mean
- Numeric.Eproc.TwoSample
+ Numeric.Eproc.Test
build-depends:
base >= 4.9 && < 5
diff --git a/test/Main.hs b/test/Main.hs
@@ -6,7 +6,7 @@ import Data.Bits
import Data.Word
import qualified Numeric.Eproc.Bettor as B
import qualified Numeric.Eproc.Mean as M
-import qualified Numeric.Eproc.TwoSample as TS
+import qualified Numeric.Eproc.Test as T
import Test.Tasty
import Test.Tasty.HUnit
@@ -86,38 +86,38 @@ rejection_rate cfg p budget trials seed =
, v == M.Reject ]
in fromIntegral rejects / fromIntegral trials
-run_ts_paired
- :: TS.Config
+run_paired
+ :: T.Config
-> Double
-> Double -- ^ p for A and B
-> Int
-> Gen
- -> (TS.Verdict, Int)
-run_ts_paired cfg pa pb budget g0 = go 0 g0 (TS.initial cfg)
+ -> (T.Verdict, Int)
+run_paired cfg pa pb budget g0 = go 0 g0 (T.initial cfg)
where
go !n !g !st
- | n >= budget = (TS.decide cfg st, n)
- | otherwise = case TS.decide cfg st of
+ | n >= budget = (T.decide cfg st, n)
+ | otherwise = case T.decide cfg st of
M.Reject -> (M.Reject, n)
M.Continue ->
let (a, g1) = bernoulli pa g
(b, g2) = bernoulli pb g1
- st' = TS.update cfg st (a, b)
+ st' = T.update cfg st (a, b)
in go (n + 1) g2 st'
-ts_avg_rate
- :: TS.Config
+paired_avg_rate
+ :: T.Config
-> Double
-> Double
-> Int
-> Int
-> Word64
-> Double
-ts_avg_rate cfg pa pb budget trials seed =
+paired_avg_rate cfg pa pb budget trials seed =
let gens = take trials (gen_seq (mk_gen seed))
rejects = length
[ () | g <- gens
- , let (v, _) = run_ts_paired cfg pa pb budget g
+ , let (v, _) = run_paired cfg pa pb budget g
, v == M.Reject ]
in fromIntegral rejects / fromIntegral trials
@@ -179,12 +179,12 @@ power_tests = testGroup "power" [
two_sample_tests :: TestTree
two_sample_tests = testGroup "two-sample" [
testCase "identical distributions don't reject" $ do
- let cfg = TS.config 0.0 1.0 1.0e-3 B.Ons
- rate = ts_avg_rate cfg 0.5 0.5 2000 100 33333
+ let cfg = T.config 0.0 1.0 1.0e-3 B.Ons
+ rate = paired_avg_rate cfg 0.5 0.5 2000 100 33333
assertBool ("FPR " ++ show rate) $ rate <= 0.05
, testCase "different distributions reject" $ do
- let cfg = TS.config 0.0 1.0 1.0e-3 B.Ons
- rate = ts_avg_rate cfg 0.3 0.7 5000 100 44444
+ let cfg = T.config 0.0 1.0 1.0e-3 B.Ons
+ rate = paired_avg_rate cfg 0.3 0.7 5000 100 44444
assertBool ("power " ++ show rate) $ rate >= 0.95
]