streamly-statistics 0.1.0 → 0.2.0
raw patch · 6 files changed
+1233/−251 lines, 6 filesdep ~QuickCheckdep ~containersdep ~deepseqnew-uploaderPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: QuickCheck, containers, deepseq, random, streamly-core, tasty, tasty-bench
API changes (from Hackage documentation)
+ Streamly.Statistics.Scanl: ewma :: forall (m :: Type -> Type). Monad m => Double -> Scanl m Double Double
+ Streamly.Statistics.Scanl: ewmaRampUpSmoothing :: forall (m :: Type -> Type). Monad m => Double -> Double -> Scanl m Double Double
+ Streamly.Statistics.Scanl: incrEwma :: forall (m :: Type -> Type). MonadIO m => Double -> Scanl m (Incr Double, RingArray Double) Double
+ Streamly.Statistics.Scanl: incrFrequency :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Scanl m (Incr a) (Map a Int)
+ Streamly.Statistics.Scanl: incrGeometricMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrHarmonicMean :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrKurtosis :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrMaximum :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrMd :: forall (m :: Type -> Type). MonadIO m => Scanl m (Incr Double, RingArray Double) Double
+ Streamly.Statistics.Scanl: incrMinimum :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrPowerMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Int -> Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrPowerMeanFrac :: forall (m :: Type -> Type) a. (Monad m, Floating a) => a -> Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrQuadraticMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrRange :: forall (m :: Type -> Type) a. (Monad m, Num a, Ord a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrRawMoment :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Int -> Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrRawMomentFrac :: forall (m :: Type -> Type) a. (Monad m, Floating a) => a -> Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrSampleStdDev :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrSampleVariance :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrSkewness :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrStdDev :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrStdErrMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrVariance :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Scanl m (Incr a) a
+ Streamly.Statistics.Scanl: incrWelfordMean :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Scanl m (Incr a) a
- Streamly.Statistics: ewma :: Monad m => Double -> Fold m Double Double
+ Streamly.Statistics: ewma :: forall (m :: Type -> Type). Monad m => Double -> Fold m Double Double
- Streamly.Statistics: ewmaAfterMean :: Monad m => Int -> Double -> Fold m Double Double
+ Streamly.Statistics: ewmaAfterMean :: forall (m :: Type -> Type). Monad m => Int -> Double -> Fold m Double Double
- Streamly.Statistics: ewmaRampUpSmoothing :: Monad m => Double -> Double -> Fold m Double Double
+ Streamly.Statistics: ewmaRampUpSmoothing :: forall (m :: Type -> Type). Monad m => Double -> Double -> Fold m Double Double
- Streamly.Statistics: foldResamples :: (MonadIO m, Unbox a) => Int -> Array a -> Fold m a b -> Stream m b
+ Streamly.Statistics: foldResamples :: forall (m :: Type -> Type) a b. (MonadIO m, Unbox a) => Int -> Array a -> Fold m a b -> Stream m b
- Streamly.Statistics: frequency :: (Monad m, Ord a) => Fold m (a, Maybe a) (Map a Int)
+ Streamly.Statistics: frequency :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Fold m (a, Maybe a) (Map a Int)
- Streamly.Statistics: frequency' :: (Monad m, Ord a) => Fold m a (Map a Int)
+ Streamly.Statistics: frequency' :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Fold m a (Map a Int)
- Streamly.Statistics: geometricMean :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: geometricMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: harmonicMean :: (Monad m, Fractional a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: harmonicMean :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
- Streamly.Statistics: histogram :: (Monad m, Ord k) => (a -> k) -> Fold m a (Map k Int)
+ Streamly.Statistics: histogram :: forall (m :: Type -> Type) k a. (Monad m, Ord k) => (a -> k) -> Fold m a (Map k Int)
- Streamly.Statistics: jackKnifeMean :: (Monad m, Fractional a, Unbox a) => Array a -> Stream m a
+ Streamly.Statistics: jackKnifeMean :: forall (m :: Type -> Type) a. (Monad m, Fractional a, Unbox a) => Array a -> Stream m a
- Streamly.Statistics: jackKnifeStdDev :: (Monad m, Unbox a, Floating a) => Array a -> Stream m a
+ Streamly.Statistics: jackKnifeStdDev :: forall (m :: Type -> Type) a. (Monad m, Unbox a, Floating a) => Array a -> Stream m a
- Streamly.Statistics: jackKnifeVariance :: (Monad m, Fractional a, Unbox a) => Array a -> Stream m a
+ Streamly.Statistics: jackKnifeVariance :: forall (m :: Type -> Type) a. (Monad m, Fractional a, Unbox a) => Array a -> Stream m a
- Streamly.Statistics: kurtosis :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: kurtosis :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: maximum :: (Monad m, Ord a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: maximum :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Fold m (a, Maybe a) a
- Streamly.Statistics: mean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: mean :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
- Streamly.Statistics: minimum :: (Monad m, Ord a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: minimum :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Fold m (a, Maybe a) a
- Streamly.Statistics: mode :: (Monad m, Ord a) => Fold m a (Maybe (a, Int))
+ Streamly.Statistics: mode :: forall (m :: Type -> Type) a. (Monad m, Ord a) => Fold m a (Maybe (a, Int))
- Streamly.Statistics: powerMean :: (Monad m, Floating a) => Int -> Fold m (a, Maybe a) a
+ Streamly.Statistics: powerMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Int -> Fold m (a, Maybe a) a
- Streamly.Statistics: powerMeanFrac :: (Monad m, Floating a) => a -> Fold m (a, Maybe a) a
+ Streamly.Statistics: powerMeanFrac :: forall (m :: Type -> Type) a. (Monad m, Floating a) => a -> Fold m (a, Maybe a) a
- Streamly.Statistics: quadraticMean :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: quadraticMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: range :: (Monad m, Num a, Ord a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: range :: forall (m :: Type -> Type) a. (Monad m, Num a, Ord a) => Fold m (a, Maybe a) a
- Streamly.Statistics: rawMoment :: (Monad m, Fractional a) => Int -> Fold m (a, Maybe a) a
+ Streamly.Statistics: rawMoment :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Int -> Fold m (a, Maybe a) a
- Streamly.Statistics: rawMomentFrac :: (Monad m, Floating a) => a -> Fold m (a, Maybe a) a
+ Streamly.Statistics: rawMomentFrac :: forall (m :: Type -> Type) a. (Monad m, Floating a) => a -> Fold m (a, Maybe a) a
- Streamly.Statistics: resample :: (MonadIO m, Unbox a) => Unfold m (Array a) a
+ Streamly.Statistics: resample :: forall (m :: Type -> Type) a. (MonadIO m, Unbox a) => Unfold m (Array a) a
- Streamly.Statistics: sampleStdDev :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: sampleStdDev :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: sampleVariance :: (Monad m, Fractional a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: sampleVariance :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
- Streamly.Statistics: skewness :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: skewness :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: stdDev :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: stdDev :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: stdErrMean :: (Monad m, Floating a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: stdErrMean :: forall (m :: Type -> Type) a. (Monad m, Floating a) => Fold m (a, Maybe a) a
- Streamly.Statistics: variance :: (Monad m, Fractional a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: variance :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
- Streamly.Statistics: welfordMean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
+ Streamly.Statistics: welfordMean :: forall (m :: Type -> Type) a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
Files
- CHANGELOG.md +5/−0
- benchmark/Main.hs +302/−177
- src/Streamly/Statistics.hs +80/−40
- src/Streamly/Statistics/Scanl.hs +787/−0
- streamly-statistics.cabal +22/−15
- test/Main.hs +37/−19
CHANGELOG.md view
@@ -1,5 +1,10 @@ # Changelog +## 0.2.0 (Sep 2025)++* Add a Scanl module+* Support `streamly-core-0.3.0`+ ## 0.1.0 (Apr 2023) * Initial version
benchmark/Main.hs view
@@ -2,14 +2,17 @@ import Control.DeepSeq (NFData) import Streamly.Data.Fold (Fold)+import Streamly.Data.Scanl (Scanl) import Streamly.Data.Stream (Stream) import System.Random (randomRIO) import qualified Streamly.Data.Fold as Fold+import qualified Streamly.Internal.Data.RingArray as Ring+import qualified Streamly.Internal.Data.Scanl as Scanl import qualified Streamly.Data.Stream as Stream import qualified Streamly.Data.Array as Array-import qualified Streamly.Internal.Data.Ring.Unboxed as Ring import qualified Streamly.Statistics as Statistics+import qualified Streamly.Statistics.Scanl as StatScan import Gauge @@ -47,228 +50,350 @@ benchWithFoldInt :: Int -> String -> Fold IO Int Int -> Benchmark benchWithFoldInt len name f = benchWith source len name f +{-# INLINE benchWithScanSrc #-}+benchWithScanSrc :: (Num a) =>+ (Int -> a -> Stream IO a) -> Int -> String -> Scanl IO a a -> Benchmark+benchWithScanSrc src len name f =+ bench name+ $ nfIO+ $ randomRIO (1, 1 :: Int)+ >>= Stream.fold Fold.drain+ . Stream.postscanl f . src len . fromIntegral+ {-# INLINE benchWithPostscan #-}-benchWithPostscan :: Int -> String -> Fold IO Double Double -> Benchmark+benchWithPostscan :: Int -> String -> Scanl IO Double Double -> Benchmark benchWithPostscan len name f = bench name $ nfIO $ randomRIO (1, 1) >>=- Stream.fold Fold.drain . Stream.postscan f . source len+ Stream.fold Fold.drain . Stream.postscanl f . source len {-# INLINE benchWithResample #-} benchWithResample :: Int -> String -> Benchmark benchWithResample len name = bench name $ nfIO $ do i <- randomRIO (1, 1)- arr <- Stream.fold Array.write (source len i :: Stream IO Double)+ arr <- Stream.fold Array.create (source len i :: Stream IO Double) Stream.fold Fold.drain $ Stream.unfold Statistics.resample arr {-# INLINE benchWithFoldResamples #-} benchWithFoldResamples :: Int -> String -> Fold IO Double Double -> Benchmark benchWithFoldResamples len name f = bench name $ nfIO $ do i <- randomRIO (1, 1)- arr <- Stream.fold Array.write (source len i :: Stream IO Double)+ arr <- Stream.fold Array.create (source len i :: Stream IO Double) Stream.fold Fold.drain $ Statistics.foldResamples len arr f {-# INLINE numElements #-} numElements :: Int numElements = 100000 -main :: IO ()-main =- defaultMain- [ bgroup- "fold"- [ benchWithFold numElements "minimum (window size 100)"- (Ring.slidingWindow 100 Statistics.minimum)- , benchWithFold numElements "minimum (window size 1000)"- (Ring.slidingWindow 1000 Statistics.minimum)- , benchWith sourceDescendingInt numElements- "minimum descending (window size 1000)"- (Ring.slidingWindow 1000 Statistics.minimum)+mkBenchmarks ::+ (Int -> String -> Fold IO Double Double -> Benchmark)+ -> [Benchmark]+mkBenchmarks mkBench =+ [+ mkBench numElements "minimum (window size 100)"+ (Ring.slidingWindow 100 Statistics.minimum)+ , mkBench numElements "minimum (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.minimum)+ , benchWith sourceDescendingInt numElements+ "minimum descending (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.minimum) - , benchWithFold numElements "maximum (window size 100)"- (Ring.slidingWindow 100 Statistics.maximum)- , benchWithFold numElements "maximum (window size 1000)"- (Ring.slidingWindow 1000 Statistics.maximum)- , benchWith sourceDescendingInt numElements- "maximum descending (window size 1000)"- (Ring.slidingWindow 1000 Statistics.maximum)+ , mkBench numElements "maximum (window size 100)"+ (Ring.slidingWindow 100 Statistics.maximum)+ , mkBench numElements "maximum (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.maximum)+ , benchWith sourceDescendingInt numElements+ "maximum descending (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.maximum) - , benchWithFold numElements "range (window size 100)"- (Ring.slidingWindow 100 Statistics.range)- , benchWithFold numElements "range (window size 1000)"- (Ring.slidingWindow 1000 Statistics.range)+ , mkBench numElements "range (window size 100)"+ (Ring.slidingWindow 100 Statistics.range)+ , mkBench numElements "range (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.range) - , benchWithFoldInt numElements "sumInt (window size 100)"- (Ring.slidingWindow 100 Statistics.sumInt)- , benchWithFoldInt numElements "sum for Int (window size 100)"- (Ring.slidingWindow 100 Statistics.sum)+ , mkBench numElements "sum (window size 100)"+ (Ring.slidingWindow 100 Statistics.sum)+ , mkBench numElements "sum (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.sum)+ , mkBench numElements "sum (entire stream)"+ (Statistics.cumulative Statistics.sum)+ , mkBench numElements "sum (Data.Fold)" Fold.sum - , benchWithFold numElements "sum (window size 100)"- (Ring.slidingWindow 100 Statistics.sum)- , benchWithFold numElements "sum (window size 1000)"- (Ring.slidingWindow 1000 Statistics.sum)- , benchWithFold numElements "sum (entire stream)"- (Statistics.cumulative Statistics.sum)- , benchWithFold numElements "sum (Data.Fold)"- (Fold.sum)+ , mkBench numElements "mean (window size 100)"+ (Ring.slidingWindow 100 Statistics.mean)+ , mkBench numElements "mean (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.mean)+ , mkBench numElements "mean (entire stream)"+ (Statistics.cumulative Statistics.mean)+ , mkBench numElements "mean (Data.Fold)" Fold.mean - , benchWithFold numElements "mean (window size 100)"- (Ring.slidingWindow 100 Statistics.mean)- , benchWithFold numElements "mean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.mean)- , benchWithFold numElements "mean (entire stream)"- (Statistics.cumulative Statistics.mean)- , benchWithFold numElements "mean (Data.Fold)"- (Fold.mean)+ , mkBench numElements "welfordMean (window size 100)"+ (Ring.slidingWindow 100 Statistics.welfordMean)+ , mkBench numElements "welfordMean (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.welfordMean)+ , mkBench numElements "welfordMean (entire stream)"+ (Statistics.cumulative Statistics.welfordMean) - , benchWithFold- numElements- "welfordMean (window size 100)"- (Ring.slidingWindow 100 Statistics.welfordMean)- , benchWithFold- numElements- "welfordMean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.welfordMean)- , benchWithFold- numElements- "welfordMean (entire stream)"- (Statistics.cumulative Statistics.welfordMean)+ , mkBench numElements "geometricMean (window size 100)"+ (Ring.slidingWindow 100 Statistics.geometricMean)+ , mkBench numElements "geometricMean (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.geometricMean)+ , mkBench numElements "geometricMean (entire stream)"+ (Statistics.cumulative Statistics.geometricMean) - , benchWithFold numElements "geometricMean (window size 100)"- (Ring.slidingWindow 100 Statistics.geometricMean)- , benchWithFold numElements "geometricMean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.geometricMean)- , benchWithFold numElements "geometricMean (entire stream)"- (Statistics.cumulative Statistics.geometricMean)+ , mkBench numElements "harmonicMean (window size 100)"+ (Ring.slidingWindow 100 Statistics.harmonicMean)+ , mkBench numElements "harmonicMean (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.harmonicMean)+ , mkBench numElements "harmonicMean (entire stream)"+ (Statistics.cumulative Statistics.harmonicMean) - , benchWithFold numElements "harmonicMean (window size 100)"- (Ring.slidingWindow 100 Statistics.harmonicMean)- , benchWithFold numElements "harmonicMean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.harmonicMean)- , benchWithFold numElements "harmonicMean (entire stream)"- (Statistics.cumulative Statistics.harmonicMean)+ , mkBench numElements "quadraticMean (window size 100)"+ (Ring.slidingWindow 100 Statistics.quadraticMean)+ , mkBench numElements "quadraticMean (window size 1000)"+ (Ring.slidingWindow 1000 Statistics.quadraticMean)+ , mkBench numElements "quadraticMean (entire stream)"+ (Statistics.cumulative Statistics.quadraticMean) - , benchWithFold numElements "quadraticMean (window size 100)"- (Ring.slidingWindow 100 Statistics.quadraticMean)- , benchWithFold numElements "quadraticMean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.quadraticMean)- , benchWithFold numElements "quadraticMean (entire stream)"- (Statistics.cumulative Statistics.quadraticMean)+ , mkBench numElements "powerSum 2 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerSum 2))+ , mkBench numElements "powerSum 2 (entire stream)"+ (Statistics.cumulative (Statistics.powerSum 2)) - , benchWithFold numElements "powerSum 2 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerSum 2))- , benchWithFold numElements "powerSum 2 (entire stream)"- (Statistics.cumulative (Statistics.powerSum 2))+ , mkBench numElements "rawMoment 2 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerSum 2))+ , mkBench numElements "rawMoment 2 (entire stream)"+ (Statistics.cumulative (Statistics.rawMoment 2)) - , benchWithFold numElements "rawMoment 2 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerSum 2))- , benchWithFold numElements "rawMoment 2 (entire stream)"- (Statistics.cumulative (Statistics.rawMoment 2))+ , mkBench numElements "powerMean 1 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMean 1))+ , mkBench numElements "powerMean 2 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMean 2))+ , mkBench numElements "powerMean 10 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMean 10)) - , benchWithFold numElements "powerMean 1 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMean 1))- , benchWithFold numElements "powerMean 2 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMean 2))- , benchWithFold numElements "powerMean 10 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMean 10))+ , mkBench numElements "powerMeanFrac (-1) (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMeanFrac (-1)))+ , mkBench numElements "powerMeanFrac 1 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMeanFrac 1))+ , mkBench numElements "powerMeanFrac 2 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMeanFrac 2))+ , mkBench numElements "powerMeanFrac 10 (window size 100)"+ (Ring.slidingWindow 100 (Statistics.powerMeanFrac 10)) - , benchWithFold numElements "powerMeanFrac (-1) (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMeanFrac (-1)))- , benchWithFold numElements "powerMeanFrac 1 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMeanFrac 1))- , benchWithFold numElements "powerMeanFrac 2 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMeanFrac 2))- , benchWithFold numElements "powerMeanFrac 10 (window size 100)"- (Ring.slidingWindow 100 (Statistics.powerMeanFrac 10))+ , mkBench numElements "ewma (entire stream)"+ (Statistics.ewma 0.5)+ , mkBench numElements "ewmaAfterMean (entire stream)"+ (Statistics.ewmaAfterMean 10 0.5)+ , mkBench numElements "ewmaRampUpSmoothing (entire stream)"+ (Statistics.ewmaRampUpSmoothing 0.5 0.5) - , benchWithFold numElements "ewma (entire stream)"- (Statistics.ewma 0.5)- , benchWithFold numElements "ewmaAfterMean (entire stream)"- (Statistics.ewmaAfterMean 10 0.5)- , benchWithFold numElements "ewmaRampUpSmoothing (entire stream)"- (Statistics.ewmaRampUpSmoothing 0.5 0.5)+ , mkBench numElements "variance (window size 100)"+ (Ring.slidingWindow 100 Statistics.variance)+ , mkBench numElements "variance (entire stream)"+ (Statistics.cumulative Statistics.variance)+ -- , mkBench numElements "variance (Data.Fold)" Fold.variance - , benchWithFold numElements "variance (window size 100)"- (Ring.slidingWindow 100 (Statistics.variance))- , benchWithFold numElements "variance (entire stream)"- (Statistics.cumulative (Statistics.variance))- -- , benchWithFold numElements "variance (Data.Fold)"- -- (Fold.variance)+ , mkBench numElements "sampleVariance (window size 100)"+ (Ring.slidingWindow 100 Statistics.sampleVariance)+ , mkBench numElements "sampleVariance (entire stream)"+ (Statistics.cumulative Statistics.sampleVariance) - , benchWithFold numElements "sampleVariance (window size 100)"- (Ring.slidingWindow 100 (Statistics.sampleVariance))- , benchWithFold numElements "sampleVariance (entire stream)"- (Statistics.cumulative (Statistics.sampleVariance))+ , mkBench numElements "stdDev (window size 100)"+ (Ring.slidingWindow 100 Statistics.stdDev)+ , mkBench numElements "stdDev (entire stream)"+ (Statistics.cumulative Statistics.stdDev)+ -- , mkBench numElements "stdDev (Data.Fold)" Fold.stdDev - , benchWithFold numElements "stdDev (window size 100)"- (Ring.slidingWindow 100 (Statistics.stdDev))- , benchWithFold numElements "stdDev (entire stream)"- (Statistics.cumulative (Statistics.stdDev))- -- , benchWithFold numElements "stdDev (Data.Fold)"- -- (Fold.stdDev)+ , mkBench numElements "sampleStdDev (window size 100)"+ (Ring.slidingWindow 100 Statistics.sampleStdDev)+ , mkBench numElements "sampleStdDev (entire stream)"+ (Statistics.cumulative Statistics.sampleStdDev) - , benchWithFold numElements "sampleStdDev (window size 100)"- (Ring.slidingWindow 100 (Statistics.sampleStdDev))- , benchWithFold numElements "sampleStdDev (entire stream)"- (Statistics.cumulative (Statistics.sampleStdDev))+ , mkBench numElements "stdErrMean (window size 100)"+ (Ring.slidingWindow 100 Statistics.stdErrMean)+ , mkBench numElements "stdErrMean (entire stream)"+ (Statistics.cumulative Statistics.stdErrMean) - , benchWithFold numElements "stdErrMean (window size 100)"- (Ring.slidingWindow 100 (Statistics.stdErrMean))- , benchWithFold numElements "stdErrMean (entire stream)"- (Statistics.cumulative (Statistics.stdErrMean))+-- These benchmarks take a lot of time/memory with fusion-plugin possibly+-- because of the use of Tee.+#ifndef FUSION_PLUGIN+ , mkBench numElements "skewness (window size 100)"+ (Ring.slidingWindow 100 Statistics.skewness)+ , mkBench numElements "skewness (entire stream)"+ (Statistics.cumulative Statistics.skewness) + , mkBench numElements "kurtosis (window size 100)"+ (Ring.slidingWindow 100 Statistics.kurtosis)+ , mkBench numElements "kurtosis (entire stream)"+ (Statistics.cumulative Statistics.kurtosis)+#endif+ , mkBench numElements "md (window size 100)"+ (Ring.slidingWindowWith 100 Statistics.md)++ ]++mkScans ::+ (Int -> String -> Scanl IO Double Double -> Benchmark)+ -> [Benchmark]+mkScans mkBench =+ [+ mkBench numElements "minimum (window size 100)"+ (Scanl.incrScan 100 StatScan.incrMinimum)+ , mkBench numElements "minimum (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrMinimum)+ , benchWithScanSrc sourceDescendingInt numElements+ "minimum descending (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrMinimum)++ , mkBench numElements "maximum (window size 100)"+ (Scanl.incrScan 100 StatScan.incrMaximum)+ , mkBench numElements "maximum (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrMaximum)+ , benchWithScanSrc sourceDescendingInt numElements+ "maximum descending (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrMaximum)++ , mkBench numElements "range (window size 100)"+ (Scanl.incrScan 100 StatScan.incrRange)+ , mkBench numElements "range (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrRange)++ , mkBench numElements "sum (window size 100)"+ (Scanl.incrScan 100 Scanl.incrSum)+ , mkBench numElements "sum (window size 1000)"+ (Scanl.incrScan 1000 Scanl.incrSum)+ , mkBench numElements "sum (entire stream)"+ (Scanl.cumulativeScan Scanl.incrSum)+ , mkBench numElements "sum (Data.Fold)" Scanl.sum++ , mkBench numElements "mean (window size 100)"+ (Scanl.incrScan 100 Scanl.incrMean)+ , mkBench numElements "mean (window size 1000)"+ (Scanl.incrScan 1000 Scanl.incrMean)+ , mkBench numElements "mean (entire stream)"+ (Scanl.cumulativeScan Scanl.incrMean)+ , mkBench numElements "mean (Data.Fold)" Scanl.mean++ , mkBench numElements "welfordMean (window size 100)"+ (Scanl.incrScan 100 StatScan.incrWelfordMean)+ , mkBench numElements "welfordMean (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrWelfordMean)+ , mkBench numElements "welfordMean (entire stream)"+ (Scanl.cumulativeScan StatScan.incrWelfordMean)++ , mkBench numElements "geometricMean (window size 100)"+ (Scanl.incrScan 100 StatScan.incrGeometricMean)+ , mkBench numElements "geometricMean (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrGeometricMean)+ , mkBench numElements "geometricMean (entire stream)"+ (Scanl.cumulativeScan StatScan.incrGeometricMean)++ , mkBench numElements "harmonicMean (window size 100)"+ (Scanl.incrScan 100 StatScan.incrHarmonicMean)+ , mkBench numElements "harmonicMean (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrHarmonicMean)+ , mkBench numElements "harmonicMean (entire stream)"+ (Scanl.cumulativeScan StatScan.incrHarmonicMean)++ , mkBench numElements "quadraticMean (window size 100)"+ (Scanl.incrScan 100 StatScan.incrQuadraticMean)+ , mkBench numElements "quadraticMean (window size 1000)"+ (Scanl.incrScan 1000 StatScan.incrQuadraticMean)+ , mkBench numElements "quadraticMean (entire stream)"+ (Scanl.cumulativeScan StatScan.incrQuadraticMean)++ , mkBench numElements "powerSum 2 (window size 100)"+ (Scanl.incrScan 100 (Scanl.incrPowerSum 2))+ , mkBench numElements "powerSum 2 (entire stream)"+ (Scanl.cumulativeScan (Scanl.incrPowerSum 2))++ , mkBench numElements "rawMoment 2 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrRawMoment 2))+ , mkBench numElements "rawMoment 2 (entire stream)"+ (Scanl.cumulativeScan (StatScan.incrRawMoment 2))++ , mkBench numElements "powerMean 1 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMean 1))+ , mkBench numElements "powerMean 2 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMean 2))+ , mkBench numElements "powerMean 10 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMean 10))++ , mkBench numElements "powerMeanFrac (-1) (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMeanFrac (-1)))+ , mkBench numElements "powerMeanFrac 1 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMeanFrac 1))+ , mkBench numElements "powerMeanFrac 2 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMeanFrac 2))+ , mkBench numElements "powerMeanFrac 10 (window size 100)"+ (Scanl.incrScan 100 (StatScan.incrPowerMeanFrac 10))++ , mkBench numElements "ewma (entire stream)"+ (StatScan.ewma 0.5)+ , mkBench numElements "ewmaRampUpSmoothing (entire stream)"+ (StatScan.ewmaRampUpSmoothing 0.5 0.5)++ , mkBench numElements "variance (window size 100)"+ (Scanl.incrScan 100 StatScan.incrVariance)+ , mkBench numElements "variance (entire stream)"+ (Scanl.cumulativeScan StatScan.incrVariance)+ -- , mkBench numElements "variance (Data.Fold)" Fold.variance++ , mkBench numElements "sampleVariance (window size 100)"+ (Scanl.incrScan 100 StatScan.incrSampleVariance)+ , mkBench numElements "sampleVariance (entire stream)"+ (Scanl.cumulativeScan StatScan.incrSampleVariance)++ , mkBench numElements "stdDev (window size 100)"+ (Scanl.incrScan 100 StatScan.incrStdDev)+ , mkBench numElements "stdDev (entire stream)"+ (Scanl.cumulativeScan StatScan.incrStdDev)+ -- , mkBench numElements "stdDev (Data.Fold)" Fold.stdDev++ , mkBench numElements "sampleStdDev (window size 100)"+ (Scanl.incrScan 100 StatScan.incrSampleStdDev)+ , mkBench numElements "sampleStdDev (entire stream)"+ (Scanl.cumulativeScan StatScan.incrSampleStdDev)++ , mkBench numElements "stdErrMean (window size 100)"+ (Scanl.incrScan 100 StatScan.incrStdErrMean)+ , mkBench numElements "stdErrMean (entire stream)"+ (Scanl.cumulativeScan StatScan.incrStdErrMean)+ -- These benchmarks take a lot of time/memory with fusion-plugin possibly -- because of the use of Tee. #ifndef FUSION_PLUGIN- , benchWithFold numElements "skewness (window size 100)"- (Ring.slidingWindow 100 (Statistics.skewness))- , benchWithFold numElements "skewness (entire stream)"- (Statistics.cumulative (Statistics.skewness))+ , mkBench numElements "skewness (window size 100)"+ (Scanl.incrScan 100 StatScan.windowSkewness)+ , mkBench numElements "skewness (entire stream)"+ (Scanl.cumulativeScan StatScan.windowSkewness) - , benchWithFold numElements "kurtosis (window size 100)"- (Ring.slidingWindow 100 (Statistics.kurtosis))- , benchWithFold numElements "kurtosis (entire stream)"- (Statistics.cumulative (Statistics.kurtosis))+ , mkBench numElements "kurtosis (window size 100)"+ (Scanl.incrScan 100 StatScan.windowKurtosis)+ , mkBench numElements "kurtosis (entire stream)"+ (Scanl.cumulativeScan StatScan.windowKurtosis) #endif- , benchWithFold numElements "md (window size 100)"- (Ring.slidingWindowWith 100 Statistics.md)- ]- , bgroup- "scan"- [ benchWithPostscan numElements "minimum (window size 100)"- (Ring.slidingWindow 100 Statistics.minimum)- , benchWithPostscan numElements "minimum (window size 1000)"- (Ring.slidingWindow 1000 Statistics.minimum)- , benchWithPostscan numElements "maximum (window size 100)"- (Ring.slidingWindow 100 Statistics.maximum)- , benchWithPostscan numElements "maximum (window size 1000)"- (Ring.slidingWindow 1000 Statistics.maximum)- , benchWithPostscan numElements "range (window size 100)"- (Ring.slidingWindow 100 Statistics.range)- , benchWithPostscan numElements "range (window size 1000)"- (Ring.slidingWindow 1000 Statistics.range)- , benchWithPostscan numElements "sum (window size 100)"+ , mkBench numElements "md (window size 100)"+ (Scanl.incrScanWith 100 StatScan.incrMd)++ ]++main :: IO ()+main =+ defaultMain+ [+ bgroup "fold" $ mkBenchmarks benchWithFold+ , bgroup "fold_Int"+ [ benchWithFoldInt numElements "sumInt (window size 100)"+ (Ring.slidingWindow 100 Statistics.sumInt)+ , benchWithFoldInt numElements "sum for Int (window size 100)" (Ring.slidingWindow 100 Statistics.sum)- , benchWithPostscan numElements "sum (window size 1000)"- (Ring.slidingWindow 1000 Statistics.sum)- , benchWithPostscan numElements "mean (window size 100)"- (Ring.slidingWindow 100 Statistics.mean)- , benchWithPostscan numElements "mean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.mean)- , benchWithPostscan- numElements- "welfordMean (window size 100)"- (Ring.slidingWindow 100 Statistics.welfordMean)- , benchWithPostscan- numElements- "welfordMean (window size 1000)"- (Ring.slidingWindow 1000 Statistics.welfordMean)- , benchWithPostscan- numElements- "md (window size 100)"- (Ring.slidingWindowWith 100 Statistics.md)- -- XXX These benchmarks measure the cost of creating the array as well,- -- we can do that outside the benchmark.- , benchWithResample numElements "Resample"+ ]+ , bgroup "scan" $ mkScans benchWithPostscan+ -- XXX These benchmarks measure the cost of creating the array as well,+ -- we can do that outside the benchmark.+ , bgroup "resample"+ [ benchWithResample numElements "Resample" , benchWithFoldResamples 316 "FoldResamples 316" Fold.mean ] ]
src/Streamly/Statistics.hs view
@@ -22,6 +22,10 @@ -- Resources: --+-- Related:+-- https://hackage.haskell.org/package/foldl-statistics+-- https://hackage.haskell.org/package/foldl-incremental+-- -- This may be another useful resource for incremental (non-windowed) -- computation: --@@ -80,17 +84,17 @@ -- window folds by keeping the second element of the input tuple as -- @Nothing@. --- Window.lmap+ lmap , Window.cumulative -- * Summary Statistics -- | See https://en.wikipedia.org/wiki/Summary_statistics . -- ** Sums- , Window.length- , Window.sum- , Window.sumInt- , Window.powerSum+ , length+ , sum+ , sumInt+ , powerSum -- ** Location -- | See https://en.wikipedia.org/wiki/Location_parameter .@@ -183,25 +187,24 @@ import Data.Functor.Identity (runIdentity, Identity) import Data.Map.Strict (Map) import Data.Maybe (fromMaybe)-import Streamly.Data.Array (Array, length, Unbox)+import Streamly.Data.Array (Array, Unbox) import Streamly.Data.Fold (Tee(..)) import Streamly.Data.Stream (Stream)-import Streamly.Internal.Data.Array.Type (unsafeIndexIO)-import Streamly.Internal.Data.Fold.Type (Fold(..), Step(..))-import Streamly.Internal.Data.Stream.StreamD.Step (Step(..))+import Streamly.Internal.Data.Array (unsafeIndexIO)+import Streamly.Internal.Data.Fold (Fold(..), Step(..))+import Streamly.Internal.Data.Stream (Step(..)) import Streamly.Internal.Data.Tuple.Strict (Tuple'(..), Tuple3'(..))-import Streamly.Internal.Data.Unfold.Type (Unfold(..))+import Streamly.Internal.Data.Unfold (Unfold(..)) import System.Random.MWC (createSystemRandom, uniformRM) import qualified Data.Map.Strict as Map import qualified Deque.Strict as Deque import qualified Streamly.Data.Fold as Fold-import qualified Streamly.Data.Array as Array hiding (read)-import qualified Streamly.Internal.Data.Array as Array (read)+import qualified Streamly.Data.Array as Array import qualified Streamly.Data.MutArray as MA-import qualified Streamly.Internal.Data.Array.Mut as MA- (getIndexUnsafe, putIndexUnsafe, unsafeSwapIndices)-import qualified Streamly.Internal.Data.Fold.Window as Window+import qualified Streamly.Internal.Data.MutArray as MA+ (unsafeSwapIndices)+import qualified Streamly.Internal.Data.Fold as Window import qualified Streamly.Data.Stream as Stream import Prelude hiding (length, sum, minimum, maximum)@@ -216,6 +219,32 @@ -- think about deduplication. -------------------------------------------------------------------------------+-- Re-exports+-------------------------------------------------------------------------------++-- XXX Deprecate these once the streamly functions are released.++-- {-# DEPRECATED lmap "Use Streamly.Data.Fold.windowLmap instead" #-}+lmap :: (c -> a) -> Fold m (a, Maybe a) b -> Fold m (c, Maybe c) b+lmap = Window.windowLmap++-- {-# DEPRECATED length "Use Streamly.Data.Fold.windowLength instead" #-}+length :: (Monad m, Num b) => Fold m (a, Maybe a) b+length = Window.windowLength++-- {-# DEPRECATED sum "Use Streamly.Data.Fold.windowSum instead" #-}+sum :: (Monad m, Num a) => Fold m (a, Maybe a) a+sum = Window.windowSum++-- {-# DEPRECATED sumInt "Use Streamly.Data.Fold.windowSumInt instead" #-}+sumInt :: (Monad m, Integral a) => Fold m (a, Maybe a) a+sumInt = Window.windowSumInt++-- {-# DEPRECATED powerSum "Use Streamly.Data.Fold.windowPowerSum instead" #-}+powerSum :: (Monad m, Num a) => Int -> Fold m (a, Maybe a) a+powerSum = Window.windowPowerSum++------------------------------------------------------------------------------- -- Transforms ------------------------------------------------------------------------------- @@ -305,13 +334,13 @@ let butterfly i | i >= len = flight (j + 1) (a + e) | otherwise = do let i1 = i + l1- xi1 :+ yi1 <- MA.getIndexUnsafe i1 marr+ xi1 :+ yi1 <- MA.unsafeGetIndex i1 marr let !c = cos a !s = sin a d = (c * xi1 - s * yi1) :+ (s * xi1 + c * yi1)- ci <- MA.getIndexUnsafe i marr- MA.putIndexUnsafe i1 marr (ci - d)- MA.putIndexUnsafe i marr (ci + d)+ ci <- MA.unsafeGetIndex i marr+ MA.unsafePutIndex i1 marr (ci - d)+ MA.unsafePutIndex i marr (ci + d) butterfly (i + l2) butterfly j flight 0 0@@ -320,6 +349,10 @@ -- Location ------------------------------------------------------------------------------- +-- XXX prefix window to these folds and implement these using the scans. or+-- just remove these as the corresponding scans can be converted to folds. If+-- we remove these we can just export the scans through this module itself.+ -- Theoretically, we can approximate minimum in a rolling window by using a -- 'powerMean' with sufficiently large negative power. --@@ -329,7 +362,7 @@ -- -- | The minimum element in a rolling window. ----- For smaller window sizes (< 30) Streamly.Data.Fold.Window.minimum performs+-- For smaller window sizes (< 30) Streamly.Internal.Data.Fold.windowMinimum performs -- better. If you want to compute the minimum of the entire stream Fold.min -- from streamly package would be much faster. --@@ -337,7 +370,7 @@ -- {-# INLINE minimum #-} minimum :: (Monad m, Ord a) => Fold m (a, Maybe a) a-minimum = Fold step initial extract+minimum = Fold step initial extract extract where @@ -389,7 +422,7 @@ -- -- | The maximum element in a rolling window. ----- For smaller window sizes (< 30) Streamly.Data.Fold.Window.maximum performs+-- For smaller window sizes (< 30) Streamly.Internal.Data.Fold.windowMaximum performs -- better. If you want to compute the maximum of the entire stream -- Streamly.Data.Fold.maximum from streamly package would be much faster. --@@ -397,7 +430,7 @@ -- {-# INLINE maximum #-} maximum :: (Monad m, Ord a) => Fold m (a, Maybe a) a-maximum = Fold step initial extract+maximum = Fold step initial extract extract where @@ -469,7 +502,7 @@ -- /Time/: \(\mathcal{O}(n)\) {-# INLINE mean #-} mean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a-mean = Window.mean+mean = Window.windowMean -- | Recompute mean from old mean when an item is removed from the sample. {-# INLINE _meanSubtract #-}@@ -511,7 +544,7 @@ -- /Internal/ {-# INLINE welfordMean #-} welfordMean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a-welfordMean = Fold step initial extract+welfordMean = Fold step initial extract extract where @@ -543,7 +576,7 @@ -- -- \(\mu'_k = \frac{\sum_{i=1}^n x_{i}^k}{n}\) ----- >>> rawMoment k = Fold.teeWith (/) (powerSum p) length+-- >>> rawMoment k = Fold.teeWith (/) (Fold.windowPowerSum p) Fold.windowLength -- -- See https://en.wikipedia.org/wiki/Moment_(mathematics) . --@@ -552,16 +585,17 @@ -- /Time/: \(\mathcal{O}(n)\) {-# INLINE rawMoment #-} rawMoment :: (Monad m, Fractional a) => Int -> Fold m (a, Maybe a) a-rawMoment k = Fold.teeWith (/) (Window.powerSum k) Window.length+rawMoment k = Fold.teeWith (/) (Window.windowPowerSum k) Window.windowLength -- | Like 'rawMoment' but powers can be negative or fractional. This is -- slower than 'rawMoment' for positive intergal powers. ----- >>> rawMomentFrac p = Fold.teeWith (/) (powerSumFrac p) length+-- >>> rawMomentFrac p = Fold.teeWith (/) (Fold.windowPowerSumFrac p) Fold.windowLength -- {-# INLINE rawMomentFrac #-} rawMomentFrac :: (Monad m, Floating a) => a -> Fold m (a, Maybe a) a-rawMomentFrac k = Fold.teeWith (/) (Window.powerSumFrac k) Window.length+rawMomentFrac k =+ Fold.teeWith (/) (Window.windowPowerSumFrac k) Window.windowLength -- XXX Overflow can happen when large powers or large numbers are used. We can -- keep a running mean instead of running sum but that won't mitigate the@@ -608,7 +642,9 @@ -- {-# INLINE harmonicMean #-} harmonicMean :: (Monad m, Fractional a) => Fold m (a, Maybe a) a-harmonicMean = Fold.teeWith (/) Window.length (Window.lmap recip Window.sum)+harmonicMean =+ Fold.teeWith (/)+ Window.windowLength (Window.windowLmap recip Window.windowSum) -- | Geometric mean, defined as: --@@ -625,7 +661,7 @@ -- See https://en.wikipedia.org/wiki/Geometric_mean . {-# INLINE geometricMean #-} geometricMean :: (Monad m, Floating a) => Fold m (a, Maybe a) a-geometricMean = exp <$> Window.lmap log mean+geometricMean = exp <$> Window.windowLmap log mean -- | The quadratic mean or root mean square (rms) of the numbers -- \(x_1, x_2, \ldots, x_n\) is defined as:@@ -685,8 +721,9 @@ extract (Tuple' x _) = x --- XXX It can perhaps perform better if implemented as a custom fold?---+-- XXX It can perhaps perform better if implemented as a custom fold? We can+-- also enable this to be used as a scan that way.+ -- | @ewma n k@ is like 'ewma' but uses the mean of the first @n@ values and -- then uses that as the initial value for the @ewma@ of the rest of the -- values.@@ -694,6 +731,7 @@ -- This can be used to reduce the effect of volatility of the initial value -- when k is too small. --+-- Note that this cannot be used as a scan. {-# INLINE ewmaAfterMean #-} ewmaAfterMean :: Monad m => Int -> Double -> Fold m Double Double ewmaAfterMean n k =@@ -897,7 +935,8 @@ -- {-# INLINE sampleVariance #-} sampleVariance :: (Monad m, Fractional a) => Fold m (a, Maybe a) a-sampleVariance = Fold.teeWith (\n s2 -> n * s2 / (n - 1)) Window.length variance+sampleVariance =+ Fold.teeWith (\n s2 -> n * s2 / (n - 1)) Window.windowLength variance -- | Sample standard deviation: --@@ -923,7 +962,8 @@ -- /Time/: \(\mathcal{O}(n)\) {-# INLINE stdErrMean #-} stdErrMean :: (Monad m, Floating a) => Fold m (a, Maybe a) a-stdErrMean = Fold.teeWith (\sd n -> sd / sqrt n) sampleStdDev Window.length+stdErrMean =+ Fold.teeWith (\sd n -> sd / sqrt n) sampleStdDev Window.windowLength ------------------------------------------------------------------------------- -- Resampling@@ -943,7 +983,7 @@ {-# INLINE jackKnifeMean #-} jackKnifeMean :: (Monad m, Fractional a, Unbox a) => Array a -> Stream m a jackKnifeMean arr = do- let len = fromIntegral (length arr - 1)+ let len = fromIntegral (Array.length arr - 1) s = foldArray Fold.sum arr in fmap (\b -> (s - b) / len) $ Array.read arr @@ -955,10 +995,10 @@ jackKnifeVariance :: (Monad m, Fractional a, Unbox a) => Array a -> Stream m a jackKnifeVariance arr = do- let len = fromIntegral $ length arr - 1+ let len = fromIntegral $ Array.length arr - 1 foldSums (s, s2) x = (s + x, s2 + x ^ (2 :: Int))- (sum, sum2) = foldArray (Fold.foldl' foldSums (0.0, 0.0)) arr- var x = (sum2 - x ^ (2 :: Int)) / len - ((sum - x) / len) ^ (2::Int)+ (sum1, sum2) = foldArray (Fold.foldl' foldSums (0.0, 0.0)) arr+ var x = (sum2 - x ^ (2 :: Int)) / len - ((sum1 - x) / len) ^ (2::Int) in fmap var $ Array.read arr -- | Standard deviation computed from 'jackKnifeVariance'.@@ -981,7 +1021,7 @@ inject arr = liftIO $ do g <- createSystemRandom- return $ (g, arr, length arr, 0)+ return $ (g, arr, Array.length arr, 0) chooseOne g arr len = do i <- uniformRM (0, len - 1) g
+ src/Streamly/Statistics/Scanl.hs view
@@ -0,0 +1,787 @@+-- |+-- Module : Streamly.Statistics.Scanl+-- Copyright : (c) 2024 Composewell Technologies+-- License : Apache-2.0+-- Maintainer : streamly@composewell.com+-- Stability : experimental+-- Portability : GHC+--+-- See "Streamly.Statistics" for general information. This module provides+-- scans instead of folds.++{-# LANGUAGE ScopedTypeVariables #-}+module Streamly.Statistics.Scanl+ (+ -- * Incremental Scans+ -- | Scans of type @Scanl m (a, Maybe a) b@ are incremental sliding window+ -- scans. An input of type @(a, Nothing)@ indicates that the input element+ -- @a@ is being inserted in the window without ejecting an old value+ -- increasing the window size by 1. An input of type @(a, Just a)@+ -- indicates that the first element is being inserted in the window and the+ -- second element is being removed from the window, the window size remains+ -- the same. The window size can only increase and never decrease.+ --+ -- You can compute the statistics over the entire stream using sliding+ -- window folds by keeping the second element of the input tuple as+ -- @Nothing@.+ --+ -- Also see "Streamly.Data.Scanl" for some basic window scans.++ -- * Summary Statistics+ -- | See https://en.wikipedia.org/wiki/Summary_statistics .++ -- ** Location+ -- | See https://en.wikipedia.org/wiki/Location_parameter .+ --+ -- See https://en.wikipedia.org/wiki/Central_tendency .+ incrMinimum+ , incrMaximum+ , incrRawMoment+ , incrRawMomentFrac++ -- Pythagorean means (https://en.wikipedia.org/wiki/Pythagorean_means)+ , incrWelfordMean+ , incrGeometricMean+ , incrHarmonicMean++ , incrQuadraticMean++ -- Generalized mean+ , incrPowerMean+ , incrPowerMeanFrac++ -- ** Weighted Means+ -- | Exponential Smoothing.+ , ewma+ , ewmaRampUpSmoothing+ , incrEwma++ -- ** Spread+ -- | Second order central moment is a statistical measure of dispersion.+ -- The \(k\)th moment about the mean (or \(k\)th central moment) is defined+ -- as:+ --+ -- \(\mu_k = \frac{1}{n}\sum_{i=1}^n {(x_{i}-\mu)}^k\)+ --+ -- See https://mathworld.wolfram.com/CentralMoment.html .+ --+ -- See https://en.wikipedia.org/wiki/Statistical_dispersion .+ , incrRange+ , incrMd+ , incrVariance+ , incrStdDev++ -- ** Shape+ -- | Third and fourth order central moments are a measure of shape.+ --+ -- See https://en.wikipedia.org/wiki/Shape_parameter .+ --+ -- See https://en.wikipedia.org/wiki/Standardized_moment .+ , incrSkewness+ , incrKurtosis++ -- XXX Move to Statistics.Sample or Statistics.Estimation module?+ -- ** Estimation+ , incrSampleVariance+ , incrSampleStdDev+ , incrStdErrMean++ -- ** Probability Distribution+ , incrFrequency+ )+where++import Control.Exception (assert)+import Control.Monad (when)+import Control.Monad.IO.Class (MonadIO(..))+import Data.Function ((&))+import Data.Map.Strict (Map)+import Data.Maybe (fromMaybe)+import Streamly.Internal.Data.Fold (Step(..))+import Streamly.Internal.Data.Scanl (Scanl(..), Incr(..))+import Streamly.Internal.Data.Tuple.Strict (Tuple'(..), Tuple3'(..))++import qualified Data.Map.Strict as Map+import qualified Deque.Strict as Deque+import qualified Streamly.Data.Fold as Fold+import qualified Streamly.Internal.Data.RingArray as Ring+import qualified Streamly.Internal.Data.Scanl as Scanl+import qualified Streamly.Data.Stream as Stream++import Prelude hiding (length, sum, minimum, maximum)++-- TODO: Overflow checks. Would be good if we can directly replace the+-- operations with overflow checked operations.+--+-- See https://hackage.haskell.org/package/safe-numeric+-- See https://hackage.haskell.org/package/safeint+--+-- TODO We have many of these functions in Streamly.Data.Fold as well. Need to+-- think about deduplication.++-------------------------------------------------------------------------------+-- Location+-------------------------------------------------------------------------------++-- Theoretically, we can approximate minimum in a rolling window by using a+-- 'powerMean' with sufficiently large negative power.+--+-- XXX If we need to know the minimum in the window only once in a while then+-- we can use linear search when it is extracted and not pay the cost all the+-- time.++-- | The minimum element in a rolling window.+--+-- For smaller window sizes (< 30) Streamly.Internal.Data.Fold.windowMinimum performs+-- better. If you want to compute the minimum of the entire stream Fold.min+-- from streamly package would be much faster.+--+-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.+--+{-# INLINE incrMinimum #-}+incrMinimum :: (Monad m, Ord a) => Scanl m (Incr a) a+incrMinimum = Scanl step initial extract extract++ where++ initial =+ return+ $ Partial+ $ Tuple3' (0 :: Int) (0 :: Int) (mempty :: Deque.Deque (Int, a))++ step (Tuple3' i w q) (Insert a) =+ return+ $ Partial+ $ Tuple3'+ (i + 1)+ (w + 1)+ (headCheck i q (w + 1) & dqloop (i, a))++ step (Tuple3' i w q) (Replace _ new) =+ return+ $ Partial+ $ Tuple3' (i + 1) w (headCheck i q w & dqloop (i, new))++ {-# INLINE headCheck #-}+ headCheck i q w =+ case Deque.uncons q of+ Nothing -> q+ Just (ia', q') ->+ if fst ia' <= i - w+ then q'+ else q++ dqloop ia q =+ case Deque.unsnoc q of+ Nothing -> Deque.snoc ia q+ -- XXX This can be improved for the case of `=`+ Just (ia', q') ->+ if snd ia <= snd ia'+ then dqloop ia q'+ else Deque.snoc ia q++ extract (Tuple3' _ _ q) =+ return+ $ snd+ $ fromMaybe (0, error "minimum: Empty stream")+ $ Deque.head q++-- Theoretically, we can approximate maximum in a rolling window by using a+-- 'powerMean' with sufficiently large positive power.++-- | The maximum element in a rolling window.+--+-- For smaller window sizes (< 30) Streamly.Internal.Data.Fold.windowMaximum+-- performs better. If you want to compute the maximum of the entire stream+-- Streamly.Data.Fold.maximum from streamly package would be much faster.+--+-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.+--+{-# INLINE incrMaximum #-}+incrMaximum :: (Monad m, Ord a) => Scanl m (Incr a) a+incrMaximum = Scanl step initial extract extract++ where++ initial =+ return+ $ Partial+ $ Tuple3' (0 :: Int) (0 :: Int) (mempty :: Deque.Deque (Int, a))++ step (Tuple3' i w q) (Insert a) =+ return+ $ Partial+ $ Tuple3'+ (i + 1)+ (w + 1)+ (headCheck i q (w + 1) & dqloop (i, a))++ step (Tuple3' i w q) (Replace _ new) =+ return+ $ Partial+ $ Tuple3' (i + 1) w (headCheck i q w & dqloop (i, new))++ {-# INLINE headCheck #-}+ headCheck i q w =+ case Deque.uncons q of+ Nothing -> q+ Just (ia', q') ->+ if fst ia' <= i - w+ then q'+ else q++ dqloop ia q =+ case Deque.unsnoc q of+ Nothing -> Deque.snoc ia q+ -- XXX This can be improved for the case of `=`+ Just (ia', q') ->+ if snd ia >= snd ia'+ then dqloop ia q'+ else Deque.snoc ia q++ extract (Tuple3' _ _ q) =+ return+ $ snd+ $ fromMaybe (0, error "maximum: Empty stream")+ $ Deque.head q++-------------------------------------------------------------------------------+-- Mean+-------------------------------------------------------------------------------++-- | Recompute mean from old mean when an item is added to the sample.+{-# INLINE meanAdd #-}+meanAdd :: Fractional a => Int -> a -> a -> a+meanAdd n oldMean newItem =+ let delta = (newItem - oldMean) / fromIntegral (n + 1)+ in oldMean + delta++-- We do not carry rounding errors, therefore, this would be less numerically+-- stable than the kbn mean.++-- | Recompute mean from old mean when an item in the sample is replaced.+{-# INLINE meanReplace #-}+meanReplace :: Fractional a => Int -> a -> a -> a -> a+meanReplace n oldMean oldItem newItem =+ let n1 = fromIntegral n+ -- Compute two deltas instead of a single (newItem - oldItem) because+ -- the latter would be too small causing rounding errors.+ delta1 = (newItem - oldMean) / n1+ delta2 = (oldItem - oldMean) / n1+ in (oldMean + delta1) - delta2++-- | Same as 'mean' but uses Welford's algorithm to compute the mean+-- incrementally.+--+-- It maintains a running mean instead of a running sum and adjusts the mean+-- based on a new value. This is slower than 'mean' because of using the+-- division operation on each step and it is numerically unstable (as of now).+-- The advantage over 'mean' could be no overflow if the numbers are large,+-- because we do not maintain a sum, but that is a highly unlikely corner case.+--+-- /Internal/+{-# INLINE incrWelfordMean #-}+incrWelfordMean :: forall m a. (Monad m, Fractional a) => Scanl m (Incr a) a+incrWelfordMean = Scanl step initial extract extract++ where++ initial =+ return+ $ Partial+ $ Tuple'+ (0 :: a) -- running mean+ (0 :: Int) -- count of items in the window++ step (Tuple' oldMean w) (Insert new) =+ return $ Partial $ Tuple' (meanAdd w oldMean new) (w + 1)++ step (Tuple' oldMean w) (Replace old new) =+ return $ Partial $ Tuple' (meanReplace w oldMean old new) w++ extract (Tuple' x _) = return x++-------------------------------------------------------------------------------+-- Moments+-------------------------------------------------------------------------------++-- XXX We may have chances of overflow if the powers are high or the numbers+-- are large. A limited mitigation could be to use welford style avg+-- computation. Do we need an overflow detection?++-- | Raw moment is the moment about 0. The \(k\)th raw moment is defined as:+--+-- \(\mu'_k = \frac{\sum_{i=1}^n x_{i}^k}{n}\)+--+-- >>> rawMoment k = Fold.teeWith (/) (Fold.windowPowerSum p) Fold.windowLength+--+-- See https://en.wikipedia.org/wiki/Moment_(mathematics) .+--+-- /Space/: \(\mathcal{O}(1)\)+--+-- /Time/: \(\mathcal{O}(n)\)+{-# INLINE incrRawMoment #-}+incrRawMoment :: (Monad m, Fractional a) => Int -> Scanl m (Incr a) a+incrRawMoment k =+ Scanl.teeWith (/) (Scanl.incrPowerSum k) Scanl.incrCount++-- | Like 'rawMoment' but powers can be negative or fractional. This is+-- slower than 'rawMoment' for positive intergal powers.+--+-- >>> rawMomentFrac p = Fold.teeWith (/) (Fold.windowPowerSumFrac p) Fold.windowLength+--+{-# INLINE incrRawMomentFrac #-}+incrRawMomentFrac :: (Monad m, Floating a) => a -> Scanl m (Incr a) a+incrRawMomentFrac k =+ Scanl.teeWith (/) (Scanl.incrPowerSumFrac k) Scanl.incrCount++-- XXX Overflow can happen when large powers or large numbers are used. We can+-- keep a running mean instead of running sum but that won't mitigate the+-- overflow possibility by much. The overflow can still happen when computing+-- the mean incrementally.++-- | The \(k\)th power mean of numbers \(x_1, x_2, \ldots, x_n\) is:+--+-- \(M_k = \left( \frac{1}{n} \sum_{i=1}^n x_i^k \right)^{\frac{1}{k}}\)+--+-- \(powerMean(k) = (rawMoment(k))^\frac{1}{k}\)+--+-- >>> powerMean k = (** (1 / fromIntegral k)) <$> rawMoment k+--+-- All other means can be expressed in terms of power mean. It is also known as+-- the generalized mean.+--+-- See https://en.wikipedia.org/wiki/Generalized_mean+--+{-# INLINE incrPowerMean #-}+incrPowerMean :: (Monad m, Floating a) => Int -> Scanl m (Incr a) a+incrPowerMean k = (** (1 / fromIntegral k)) <$> incrRawMoment k++-- | Like 'powerMean' but powers can be negative or fractional. This is+-- slower than 'powerMean' for positive intergal powers.+--+-- >>> powerMeanFrac k = (** (1 / k)) <$> rawMomentFrac k+--+{-# INLINE incrPowerMeanFrac #-}+incrPowerMeanFrac :: (Monad m, Floating a) => a -> Scanl m (Incr a) a+incrPowerMeanFrac k = (** (1 / k)) <$> incrRawMomentFrac k++-- | The harmonic mean of the positive numbers \(x_1, x_2, \ldots, x_n\) is+-- defined as:+--+-- \(HM = \frac{n}{\frac1{x_1} + \frac1{x_2} + \cdots + \frac1{x_n}}\)+--+-- \(HM = \left(\frac{\sum\limits_{i=1}^n x_i^{-1}}{n}\right)^{-1}\)+--+-- >>> harmonicMean = Fold.teeWith (/) length (lmap recip sum)+-- >>> harmonicMean = powerMeanFrac (-1)+--+-- See https://en.wikipedia.org/wiki/Harmonic_mean .+--+{-# INLINE incrHarmonicMean #-}+incrHarmonicMean :: (Monad m, Fractional a) => Scanl m (Incr a) a+incrHarmonicMean =+ Scanl.teeWith (/)+ Scanl.incrCount (Scanl.lmap (fmap recip) Scanl.incrSum)++-- | Geometric mean, defined as:+--+-- \(GM = \sqrt[n]{x_1 x_2 \cdots x_n}\)+--+-- \(GM = \left(\prod_{i=1}^n x_i\right)^\frac{1}{n}\)+--+-- or, equivalently, as the arithmetic mean in log space:+--+-- \(GM = e ^{{\frac{\sum_{i=1}^{n}\ln a_i}{n}}}\)+--+-- >>> geometricMean = exp <$> lmap log mean+--+-- See https://en.wikipedia.org/wiki/Geometric_mean .+{-# INLINE incrGeometricMean #-}+incrGeometricMean :: (Monad m, Floating a) => Scanl m (Incr a) a+incrGeometricMean = exp <$> Scanl.lmap (fmap log) Scanl.incrMean++-- | The quadratic mean or root mean square (rms) of the numbers+-- \(x_1, x_2, \ldots, x_n\) is defined as:+--+-- \(RMS = \sqrt{ \frac{1}{n} \left( x_1^2 + x_2^2 + \cdots + x_n^2 \right) }.\)+--+-- >>> quadraticMean = powerMean 2+--+-- See https://en.wikipedia.org/wiki/Root_mean_square .+--+{-# INLINE incrQuadraticMean #-}+incrQuadraticMean :: (Monad m, Floating a) => Scanl m (Incr a) a+incrQuadraticMean = incrPowerMean 2++-------------------------------------------------------------------------------+-- Weighted Means+-------------------------------------------------------------------------------++-- XXX Is this numerically stable? We can use the kbn summation here.+-- XXX change the signature to use the newer value first.++-- | ewmaStep smoothing-factor old-value new-value+--+-- >>> ewmaStep k x0 x1 = k * x1 + (1 - k) * x0+--+{-# INLINE ewmaStep #-}+ewmaStep :: Double -> Double -> Double -> Double+ewmaStep k x0 x1 = (1 - k) * x0 + k * x1++-- XXX Compute this in a sliding window?++-- | Exponentially weighted moving average is given by @ewma w@ where @w@ is+-- the smoothing factor varying between 0 and 1. To compute the moving average+-- the newest input is given a weight of @w@ and the running ewma is given a+-- weight of @1 - w@.+--+-- For an empty stream this API returns 0. For a non-empty stream the first+-- value in the stream is used as the initial ewma.+--+-- The higher the smoothing factor the more weight is given to the new value.+-- Consider some interesting special cases, when @w@ is 1 the ewma is always+-- the newest value, when @w@ is 0 then the ewma is always the oldest value. If+-- @w@ is @0.5@ then the new inputs get exponentially weighted by weights+-- @1\/2, 1\/4, 1\/8, ...@, see below for details.+--+-- Mathematically, we define ewma \(s_n\), of \(n\) values, \(x_1,\ldots,x_n\),+-- recursively as follows:+--+-- \(\begin{align} s_0& = x_0, \quad \text{initial value}\\ s_n & = \alpha x_{n} + (1-\alpha)s_{n-1},\quad n>0 \end{align}\)+--+-- If we expand the recursive term it reveals an exponential series:+--+-- \(s_n = \alpha \left[x_n + (1-\alpha)x_{n-1} + (1-\alpha)^2 x_{n-2} + \cdots + (1-\alpha)^{n-1} x_1 \right] + (1-\alpha)^n x_0\)+--+-- where \(\alpha\), the smoothing factor, is in the range \(0 <\alpha < 1\).+-- More the value of \(\alpha\), the more weight is given to newer values. As+-- a special case if it is 0 then the weighted sum would always be the same as+-- the oldest value, if it is 1 then the sum would always be the same as the+-- newest value.+--+-- See https://en.wikipedia.org/wiki/Moving_average+--+-- See https://en.wikipedia.org/wiki/Exponential_smoothing+--+{-# INLINE ewma #-}+ewma :: Monad m => Double -> Scanl m Double Double+ewma k = extract <$> Scanl.mkScanl step (Tuple' 0 1)++ where++ step (Tuple' x0 k1) x = Tuple' (ewmaStep k1 x0 x) k++ extract (Tuple' x _) = x++-- | @ewma n k@ is like 'ewma' but the smoothing factor used is itself+-- exponentially smoothened starting from @1@ to the final value @k@ using @n@+-- as its smoothing factor. In other words, the smoothing factor is derived by+-- smoothing the series @1,k,k,k,...@ using @n@ as the smoothing factor. As+-- time goes on, the smoothing factor gets closer to k.+--+{-# INLINE ewmaRampUpSmoothing #-}+ewmaRampUpSmoothing :: Monad m => Double -> Double -> Scanl m Double Double+ewmaRampUpSmoothing n k1 = extract <$> Scanl.mkScanl step initial++ where++ initial = Tuple' 0 1++ step (Tuple' x0 k0) x1 =+ let x = ewmaStep k0 x0 x1+ k = ewmaStep n k0 k1+ in Tuple' x k++ extract (Tuple' x _) = x++data Ewma = EwmaInit | EwmaGo !Double !Double++-- | @incrEwma w@ computes the ewma of the elements incrementally in a window+-- using @w@ as the smoothing factor.+--+-- ewma can be calculated incrementally as follows. If \(x_i\) is the value+-- entering the window, n is the size of window, \(x_1\) is the second last value+-- in the old window and \(x_0\) is the value exiting the window:+--+-- \(s_{new} = \alpha x_i + (1-\alpha)s_{old} + (1-\alpha)^{n} (x_1 - x_0\)\)+--+-- /Unimplemented/+--+{-# INLINE incrEwma #-}+incrEwma :: MonadIO m =>+ Double -> Scanl m (Incr Double, Ring.RingArray Double) Double+incrEwma w = Scanl step initial extract extract++ where++ initial = do+ when (w < 0 || w > 1)+ $ error "incrEwma: weight must be >= 0 and <= 1"+ return $ Partial EwmaInit++ step EwmaInit (Insert x, rng) = do+ let len = Ring.length rng+ assert (len == 0) (return ())+ return $ Partial (EwmaGo x 1)++ step EwmaInit _ = error "incrEwma: the first window operation must be Insert"++ step (EwmaGo s k) (Insert x, _) = do+ let s1 = w * x + (1 - w) * s+ return $ Partial (EwmaGo s1 (k * (1 - w)))++ step (EwmaGo s k) (Replace x0 x, rng) = do+ x1 <- Ring.unsafeGetIndex 0 rng+ let s1 = w * x + (1 - w) * s + k * (x1 - x0)+ return $ Partial (EwmaGo s1 k)++ extract EwmaInit = return 0+ extract (EwmaGo x _) = return x++-------------------------------------------------------------------------------+-- Spread/Dispersion+-------------------------------------------------------------------------------++-- | The difference between the maximum and minimum elements of a rolling window.+--+-- >>> range = Fold.teeWith (-) maximum minimum+--+-- If you want to compute the range of the entire stream @Fold.teeWith (-)+-- Fold.maximum Fold.minimum@ from the streamly package would be much faster.+--+-- /Space/: \(\mathcal{O}(n)\) where @n@ is the window size.+--+-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.+--+{-# INLINE incrRange #-}+incrRange :: (Monad m, Num a, Ord a) => Scanl m (Incr a) a+incrRange = Scanl.teeWith (-) incrMaximum incrMinimum++-- | @md n@ computes the mean absolute deviation (or mean deviation) in a+-- sliding window of last @n@ elements in the stream.+--+-- The input of the scan is (incr, ring), where incr is the incremental window+-- operation and ring is the contents of the entire window in a ring array.+--+-- The mean absolute deviation of the numbers \(x_1, x_2, \ldots, x_n\) is:+--+-- \(MD = \frac{1}{n}\sum_{i=1}^n |x_i-\mu|\)+--+-- Note: It is expensive to compute MD in a sliding window. We need to+-- maintain a ring buffer of last n elements and maintain a running mean, when+-- the result is extracted we need to compute the difference of all elements+-- from the mean and get the average. Using standard deviation may be+-- computationally cheaper.+--+-- See https://en.wikipedia.org/wiki/Average_absolute_deviation .+--+-- /Pre-release/+{-# INLINE incrMd #-}+incrMd :: MonadIO m =>+ Scanl m (Incr Double, Ring.RingArray Double) Double+incrMd =+ Scanl.rmapM computeMD+ $ Scanl.tee+ (Scanl.lmap fst Scanl.incrMean) (Scanl.lmap snd Scanl.latest)++ where++ computeMD (mn, mRng) =+ case mRng of+ Just rng -> do+ Stream.fold Fold.mean+ $ fmap (\a -> abs (mn - a))+ $ Ring.read rng+ Nothing -> return 0.0++-- | The variance \(\sigma^2\) of a population of \(n\) equally likely values+-- is defined as the average of the squares of deviations from the mean+-- \(\mu\). In other words, second moment about the mean:+--+-- \(\sigma^2 = \frac{1}{n}\sum_{i=1}^n {(x_{i}-\mu)}^2\)+--+-- \(\sigma^2 = rawMoment(2) - \mu^2\)+--+-- \(\mu_2 = -(\mu'_1)^2 + \mu'_2\)+--+-- Note that the variance would be biased if applied to estimate the population+-- variance from a sample of the population. See 'sampleVariance'.+--+-- See https://en.wikipedia.org/wiki/Variance.+--+-- /Space/: \(\mathcal{O}(1)\)+--+-- /Time/: \(\mathcal{O}(n)\)+{-# INLINE incrVariance #-}+incrVariance :: (Monad m, Fractional a) => Scanl m (Incr a) a+incrVariance =+ Scanl.teeWith+ (\p2 m -> p2 - m ^ (2 :: Int)) (incrRawMoment 2) Scanl.incrMean++-- | Standard deviation \(\sigma\) is the square root of 'variance'.+--+-- This is the population standard deviation or uncorrected sample standard+-- deviation.+--+-- >>> stdDev = sqrt <$> variance+--+-- See https://en.wikipedia.org/wiki/Standard_deviation .+--+-- /Space/: \(\mathcal{O}(1)\)+--+-- /Time/: \(\mathcal{O}(n)\)+{-# INLINE incrStdDev #-}+incrStdDev :: (Monad m, Floating a) => Scanl m (Incr a) a+incrStdDev = sqrt <$> incrVariance++-- XXX Need a tee3 operation for better performance.++-- | Skewness \(\gamma\) is the standardized third central moment defined as:+--+-- \(\tilde{\mu}_3 = \frac{\mu_3}{\sigma^3}\)+--+-- The third central moment can be computed in terms of raw moments:+--+-- \(\mu_3 = 2(\mu'_1)^3 - 3\mu'_1\mu'_2 + \mu'_3\)+--+-- Substituting \(\mu'_1 = \mu\), and \(\mu'_2 = \mu^2 + \sigma^2\):+--+-- \(\mu_3 = -\mu^3 - 3\mu\sigma^2 + \mu'_3\)+--+-- Skewness is a measure of symmetry of the probability distribution. It is 0+-- for a symmetric distribution, negative for a distribution that is skewed+-- towards left, positive for a distribution skewed towards right.+--+-- For a normal like distribution the median can be found around+-- \(\mu - \frac{\gamma\sigma}{6}\) and the mode can be found around+-- \(\mu - \frac{\gamma \sigma}{2}\).+--+-- See https://en.wikipedia.org/wiki/Skewness .+--+{-# INLINE incrSkewness #-}+incrSkewness :: (Monad m, Floating a) => Scanl m (Incr a) a+incrSkewness =+ (\rm3 sd mu ->+ rm3 / sd ^ (3 :: Int) - 3 * (mu / sd) - (mu / sd) ^ (3 :: Int)+ )+ <$> incrRawMoment 3+ <*> incrStdDev+ <*> Scanl.incrMean++-- XXX We can compute the 2nd, 3rd, 4th raw moments by repeatedly multiplying+-- instead of computing the powers every time.+-- XXX Need a tee4 operation for better performance.++-- | Kurtosis \(\kappa\) is the standardized fourth central moment, defined as:+--+-- \(\tilde{\mu}_4 = \frac{\mu_4}{\sigma^4}\)+--+-- The fourth central moment can be computed in terms of raw moments:+--+-- \(\mu_4 = -3(\mu'_1)^4 + 6(\mu'_1)^2\mu'_2 - 4\mu'_1\mu'_3\ + \mu'_4\)+--+-- Substituting \(\mu'_1 = \mu\), and \(\mu'_2 = \mu^2 + \sigma^2\):+--+-- \(\mu_4 = 3\mu^4 + 6\mu^2\sigma^2 - 4\mu\mu'_3 + \mu'_4\)+--+-- It is always non-negative. It is 0 for a point distribution, low for light+-- tailed (platykurtic) distributions and high for heavy tailed (leptokurtic)+-- distributions.+--+-- \(\kappa >= \gamma^2 + 1\)+--+-- For a normal distribution \(\kappa = 3\sigma^4\).+--+-- See https://en.wikipedia.org/wiki/Kurtosis .+--+{-# INLINE incrKurtosis #-}+incrKurtosis :: (Monad m, Floating a) => Scanl m (Incr a) a+incrKurtosis =+ (\rm4 rm3 sd mu ->+ ( 3 * mu ^ (4 :: Int)+ + 6 * mu ^ (2 :: Int) * sd ^ (2 :: Int)+ - 4 * mu * rm3+ + rm4) / (sd ^ (4 :: Int))+ )+ <$> incrRawMoment 4+ <*> incrRawMoment 3+ <*> incrStdDev+ <*> Scanl.incrMean++-------------------------------------------------------------------------------+-- Estimation+-------------------------------------------------------------------------------++-- | Unbiased sample variance i.e. the variance of a sample corrected to+-- better estimate the variance of the population, defined as:+--+-- \(s^2 = \frac{1}{n - 1}\sum_{i=1}^n {(x_{i}-\mu)}^2\)+--+-- \(s^2 = \frac{n}{n - 1} \times \sigma^2\).+--+-- See https://en.wikipedia.org/wiki/Bessel%27s_correction.+--+{-# INLINE incrSampleVariance #-}+incrSampleVariance :: (Monad m, Fractional a) => Scanl m (Incr a) a+incrSampleVariance =+ Scanl.teeWith (\n s2 -> n * s2 / (n - 1)) Scanl.incrCount incrVariance++-- | Sample standard deviation:+--+-- \(s = \sqrt{sampleVariance}\)+--+-- >>> sampleStdDev = sqrt <$> sampleVariance+--+-- See https://en.wikipedia.org/wiki/Unbiased_estimation_of_standard_deviation+-- .+--+{-# INLINE incrSampleStdDev #-}+incrSampleStdDev :: (Monad m, Floating a) => Scanl m (Incr a) a+incrSampleStdDev = sqrt <$> incrSampleVariance++-- | Standard error of the sample mean (SEM), defined as:+--+-- \( SEM = \frac{sampleStdDev}{\sqrt{n}} \)+--+-- See https://en.wikipedia.org/wiki/Standard_error .+--+-- /Space/: \(\mathcal{O}(1)\)+--+-- /Time/: \(\mathcal{O}(n)\)+{-# INLINE incrStdErrMean #-}+incrStdErrMean :: (Monad m, Floating a) => Scanl m (Incr a) a+incrStdErrMean =+ Scanl.teeWith (\sd n -> sd / sqrt n) incrSampleStdDev Scanl.incrCount++-------------------------------------------------------------------------------+-- Probability Distribution+-------------------------------------------------------------------------------++-- XXX We can use a Windowed classifyWith operation, that will allow us to+-- express windowed frequency, mode, histograms etc idiomatically.++-- | Count the frequency of elements in a sliding window.+--+-- >>> input = Stream.fromList [1,1,3,4,4::Int]+-- >>> f = Ring.slidingWindow 4 Statistics.frequency+-- >>> Stream.fold f input+-- fromList [(1,1),(3,1),(4,2)]+--+{-# INLINE incrFrequency #-}+incrFrequency :: (Monad m, Ord a) => Scanl m (Incr a) (Map a Int)+incrFrequency = Scanl.mkScanl step Map.empty++ where++ decrement v =+ if v == 1+ then Nothing+ else Just (v - 1)++ step refCountMap (Insert new) =+ Map.insertWith (+) new 1 refCountMap++ step refCountMap (Replace old new) =+ let m1 = Map.insertWith (+) new 1 refCountMap+ in Map.update decrement old m1
streamly-statistics.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: streamly-statistics-version: 0.1.0+version: 0.2.0 synopsis: Statistical measures for finite or infinite data streams. description:@@ -25,14 +25,18 @@ license: Apache-2.0 license-file: LICENSE tested-with:- GHC==8.10.7- , GHC==9.0.2- , GHC==9.2.2+ GHC==8.8.4+ , GHC==8.10.7+ , GHC==9.2.7 , GHC==9.4.4+ , GHC==9.6.3+ , GHC==9.8.1+ , GHC==9.10.1+ , GHC==9.12.1 author: Composewell Technologies maintainer: streamly@composewell.com copyright: 2019 Composewell Technologies-category: Streamly, Statistics+category: Streamly, Statistics, Streaming extra-source-files: CHANGELOG.md@@ -93,6 +97,7 @@ -Wincomplete-uni-patterns -Wredundant-constraints -Wnoncanonical-monad-instances+ -Wno-deprecations -Rghc-timing common optimization-options@@ -107,10 +112,12 @@ library import: ghc-options- exposed-modules: Streamly.Statistics+ exposed-modules:+ Streamly.Statistics+ , Streamly.Statistics.Scanl build-depends: base >= 4.9 && < 5- , streamly-core == 0.1.0- , containers >= 0.5 && < 0.7+ , streamly-core >= 0.3.0 && < 0.4.0+ , containers >= 0.5 && < 0.8 , random >= 1.2 && < 1.3 , mwc-random >= 0.15 && < 0.16 , deque >= 0.4.4 && < 0.4.5@@ -122,13 +129,13 @@ hs-source-dirs: test main-is: Main.hs build-depends: streamly-statistics- , streamly-core == 0.1.0+ , streamly-core >= 0.3.0 , base >= 4.9 && < 5- , QuickCheck >= 2.10 && < 2.15+ , QuickCheck >= 2.10 && < 2.16 , hspec >= 2.0 && < 3 , hspec-core >= 2.0 && < 3 , random >= 1.0.0 && < 2- , containers >= 0.5 && < 0.7+ , containers >= 0.5 && < 0.8 -- XXX Should remove these dependencies , vector >= 0.11 && < 0.14 , statistics >= 0.15 && < 0.17@@ -140,12 +147,12 @@ hs-source-dirs: benchmark main-is: Main.hs build-depends: streamly-statistics- , streamly-core == 0.1.0+ , streamly-core >= 0.3.0 , base >= 4.9 && < 5 , random >= 1.0.0 && < 2- , deepseq >= 1.4.1 && < 1.5- , tasty-bench >= 0.3 && < 0.4- , tasty >= 1.4.1 && < 1.5+ , deepseq >= 1.4.1 && < 1.6+ , tasty-bench >= 0.3 && < 0.5+ , tasty >= 1.4.1 && < 1.6 mixins: tasty-bench (Test.Tasty.Bench as Gauge ,Test.Tasty.Bench as Gauge.Main
test/Main.hs view
@@ -5,6 +5,7 @@ import Data.Functor.Classes (liftEq2) import Streamly.Data.Array (Unbox) import Streamly.Data.Stream (Stream)+import Streamly.Internal.Data.Scanl (Incr(..)) import Test.Hspec.Core.Spec (SpecM) import Test.Hspec.QuickCheck (prop) import Test.QuickCheck@@ -19,9 +20,11 @@ import qualified Streamly.Data.Array as Array import qualified Streamly.Data.Fold as Fold import qualified Streamly.Data.MutArray as MA-import qualified Streamly.Internal.Data.Ring.Unboxed as Ring+import qualified Streamly.Internal.Data.RingArray as Ring import qualified Streamly.Data.Stream as Stream import qualified Streamly.Data.Stream as S+import qualified Streamly.Internal.Data.Scanl as Scanl+import qualified Streamly.Statistics.Scanl as Stat import Prelude hiding (sum, maximum, minimum) @@ -200,19 +203,30 @@ main :: IO () main = hspec $ do describe "Numerical stability while streaming" $ do- let numElem = 80000- winSize = 800- testCaseChunk = [9007199254740992, 1, 1.0 :: Double,- 9007199254740992, 1, 1, 1, 9007199254740992]- testCase = take numElem $ cycle testCaseChunk+ let winSize = 800+ numElem = winSize * 100+ segment =+ [ 9007199254740992+ , 1+ , 1.0 :: Double+ , 9007199254740992+ , 1+ , 1+ , 1+ , 9007199254740992+ ]+ input = take numElem $ cycle segment deviationLimit = 1 testFunc f = do- let c = S.fromList testCase+ let c = S.fromList input a <- runIO $ S.fold (Ring.slidingWindow winSize f) c- b <- runIO $ S.fold f $ S.drop (numElem - winSize)- $ fmap (, Nothing) c+ b <- runIO $ S.fold f $ S.take winSize $ fmap (, Nothing) c let c1 = a - b- it ("should not deviate more than " ++ show deviationLimit)+ it ("deviation " ++ show c1 ++ " should not be more than "+ ++ show deviationLimit+ ++ " one window fold = " ++ show b+ ++ " rolling window fold = " ++ show a+ ) $ c1 >= -1 * deviationLimit && c1 <= deviationLimit describe "Sum" $ testFunc sum@@ -226,9 +240,13 @@ testFunc tc f sI sW = do let c = S.fromList tc- a <- runIO $ S.fold Fold.toList $ S.postscan f $ fmap (, Nothing) c- b <- runIO $ S.fold Fold.toList $ S.postscan- (Ring.slidingWindow winSize f) c+ a <- runIO+ $ S.fold Fold.toList+ $ S.postscanl f+ $ fmap Insert c+ b <- runIO+ $ S.fold Fold.toList+ $ S.postscanl (Scanl.incrScan winSize f) c it "Infinite" $ a == sI it ("Finite " ++ show winSize) $ b == sW @@ -254,27 +272,27 @@ describe "minimum" $ do let scanInf = [31, 31, 31, 26, 26, 26, 26] :: [Double] scanWin = [31, 31, 31, 26, 26, 26, 53] :: [Double]- testFunc testCase1 minimum scanInf scanWin+ testFunc testCase1 Stat.incrMinimum scanInf scanWin describe "maximum" $ do let scanInf = [31, 41, 59, 59, 59, 59, 97] :: [Double] scanWin = [31, 41, 59, 59, 59, 58, 97] :: [Double]- testFunc testCase1 maximum scanInf scanWin+ testFunc testCase1 Stat.incrMaximum scanInf scanWin describe "range" $ do let scanInf = [0, 10, 28, 33, 33, 33, 71] :: [Double] scanWin = [0, 10, 28, 33, 33, 32, 44] :: [Double]- testFunc testCase1 range scanInf scanWin+ testFunc testCase1 Stat.incrRange scanInf scanWin describe "sum" $ do let scanInf = [1, 2, 3, 4, 5, 12] :: [Double] scanWin = [1, 2, 3, 3, 3, 9] :: [Double]- testFunc testCase2 sum scanInf scanWin+ testFunc testCase2 Scanl.incrSum scanInf scanWin describe "mean" $ do let scanInf = [1, 1, 1, 1, 1, 2] :: [Double] scanWin = [1, 1, 1, 1, 1, 3] :: [Double]- testFunc testCase2 mean scanInf scanWin+ testFunc testCase2 Scanl.incrMean scanInf scanWin describe "welfordMean" $ do let scanInf = [1, 1, 1, 1, 1, 2] :: [Double] scanWin = [1, 1, 1, 1, 1, 3] :: [Double]- testFunc testCase2 welfordMean scanInf scanWin+ testFunc testCase2 Stat.incrWelfordMean scanInf scanWin -- Probability Distribution describe "frequency"