eproc

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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:
MREADME.md | 2+-
Mbench/Main.hs | 14+++++++-------
Mbench/Weight.hs | 14+++++++-------
Alib/Numeric/Eproc/Test.hs | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/Numeric/Eproc/TwoSample.hs | 132-------------------------------------------------------------------------------
Mppad-eproc.cabal | 2+-
Mtest/Main.hs | 32++++++++++++++++----------------
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 ]