welford-online-mean-variance 0.1.0.4 → 0.2.0.0
raw patch · 3 files changed
+163/−60 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Statistics.Sample.WelfordOnlineMeanVariance: clipValue :: WelfordOnline a => Double -> a -> a
+ Statistics.Sample.WelfordOnlineMeanVariance: constInt :: WelfordOnline a => a -> Int -> a
+ Statistics.Sample.WelfordOnlineMeanVariance: denormaliseFromZeroMeanUnitVariance :: WelfordOnline a => WelfordExistingAggregate a -> a -> a
+ Statistics.Sample.WelfordOnlineMeanVariance: divide :: WelfordOnline a => a -> a -> a
+ Statistics.Sample.WelfordOnlineMeanVariance: infixl 6 `plus`
+ Statistics.Sample.WelfordOnlineMeanVariance: infixl 7 `divideInt`
+ Statistics.Sample.WelfordOnlineMeanVariance: normaliseToZeroMeanUnitVariance :: WelfordOnline a => WelfordExistingAggregate a -> a -> a
+ Statistics.Sample.WelfordOnlineMeanVariance: squareRootMax :: WelfordOnline a => a -> a
Files
- src/Statistics/Sample/WelfordOnlineMeanVariance.hs +152/−58
- test/Statistics/Sample/WelfordOnlineTest.hs +9/−0
- welford-online-mean-variance.cabal +2/−2
src/Statistics/Sample/WelfordOnlineMeanVariance.hs view
@@ -1,4 +1,5 @@ {-# OPTIONS_GHC -Wno-partial-fields #-}+{-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-}@@ -16,6 +17,8 @@ , finalize , nextValue , isWelfordExistingAggregateEmpty+ , normaliseToZeroMeanUnitVariance+ , denormaliseFromZeroMeanUnitVariance , Mean , Variance , SampleVariance@@ -44,85 +47,59 @@ } deriving (Eq, Show, Read, Generic, NFData, Serialize) + -- | Create a new empty Aggreate for the calculation. newWelfordAggregate :: WelfordExistingAggregate a newWelfordAggregate = WelfordExistingAggregateEmpty + -- | Create a new empty Aggreate by specifying an example `a` value. It is safe to use the `*Unsafe` record field selectors from `WelfordExistingAggregate a`, when creating the data structure using -- this fuction. newWelfordAggregateDef :: (WelfordOnline a) => a -> WelfordExistingAggregate a newWelfordAggregateDef a = WelfordExistingAggregate 0 (a `minus` a) (a `minus` a) + -- | Check if it is aggregate is empty isWelfordExistingAggregateEmpty :: WelfordExistingAggregate a -> Bool isWelfordExistingAggregateEmpty WelfordExistingAggregateEmpty = True isWelfordExistingAggregateEmpty _ = False --- | Get counter safely, returns Nothing if `WelfordExistingAggregateEmpty`.++-- | Get counter. welfordCount :: WelfordExistingAggregate a -> Int welfordCount WelfordExistingAggregateEmpty = 0 welfordCount (WelfordExistingAggregate c _ _) = c --- | Get counter safely, returns Nothing if `WelfordExistingAggregateEmpty`.++-- | Get mean safely, returns Nothing if `WelfordExistingAggregateEmpty` as the type of `a` (e.g. length of vector) is unknown. mWelfordMean :: WelfordExistingAggregate a -> Maybe a mWelfordMean WelfordExistingAggregateEmpty = Nothing -- Cannot create value `a` mWelfordMean (WelfordExistingAggregate _ m _) = Just m --- | Get counter with specifying a default value.++-- | Get mean with specifying a default value. welfordMeanDef :: (WelfordOnline a) => a -> WelfordExistingAggregate a -> a welfordMeanDef a WelfordExistingAggregateEmpty = a `minus` a welfordMeanDef _ (WelfordExistingAggregate _ m _) = m --- | Add one value to the current aggregate.-addValue :: (WelfordOnline a) => WelfordExistingAggregate a -> a -> WelfordExistingAggregate a-addValue (WelfordExistingAggregate count mean m2) val =- let count' = count + 1- delta = val `minus` mean- mean' = mean `plus` (delta `divideInt` count')- delta2 = val `minus` mean'- m2' = m2 `plus` (delta `multiply` delta2)- in WelfordExistingAggregate count' mean' m2'-addValue WelfordExistingAggregateEmpty val =- let count' = 1- delta' = val- mean' = delta' `divideInt` count'- delta2' = val `minus` mean'- m2' = delta' `multiply` delta2'- in WelfordExistingAggregate count' mean' m2' --- | Add multiple values to the current aggregate. This is `foldl addValue`.-addValues :: (WelfordOnline a, Foldable f) => WelfordExistingAggregate a -> f a -> WelfordExistingAggregate a-addValues = foldl addValue---- | Calculate mean, variance and sample variance from aggregate. Calls `error` for `WelfordExistingAggregateEmpty`.-finalize :: (WelfordOnline a) => WelfordExistingAggregate a -> (Mean a, Variance a, SampleVariance a)-finalize = fromMaybe err . mFinalize- where err = error "Statistics.Sample.WelfordOnlineMeanVariance.finalize: Emtpy Welford Online Aggreate. Add data first!"---- | Calculate mean, variance and sample variance from aggregate. Safe function.-mFinalize :: (WelfordOnline a) => WelfordExistingAggregate a -> Maybe (Mean a, Variance a, SampleVariance a)-mFinalize (WelfordExistingAggregate count mean m2)- | count < 2 = Just (mean, m2, m2)- | otherwise = Just (mean, m2 `divideInt` count, m2 `divideInt` (count - 1))-mFinalize WelfordExistingAggregateEmpty = Nothing----- | Add a new sample to the aggregate and compute mean and variances.-nextValue :: (WelfordOnline a) => WelfordExistingAggregate a -> a -> (WelfordExistingAggregate a, (Mean a, Variance a, SampleVariance a))-nextValue agg val =- let agg' = addValue agg val- in (agg', finalize agg')----- | Class for all data strucutres that can be used to computer the Welford approximation. For instance, this can be used to compute the Welford algorithm on a `Vector`s of `Fractional`, while only--- requiring to handle one `WelfordExistingAggregate`.+-- | Class for all data structures that can be used to computer the Welford approximation.+-- For instance, this can be used to compute the Welford algorithm on a `Vector`s of `Fractional`,+-- while only requiring to handle one `WelfordExistingAggregate`. class WelfordOnline a where- plus :: a -> a -> a- minus :: a -> a -> a- multiply :: a -> a -> a- divideInt :: a -> Int -> a+ plus :: a -> a -> a -- ^ Addition.+ minus :: a -> a -> a -- ^ Subtraction.+ multiply :: a -> a -> a -- ^ Multiplication.+ divide :: a -> a -> a -- ^ Division.+ divideInt :: a -> Int -> a -- ^ Division by Integer.+ constInt :: a -> Int -> a -- ^ Takes one example vector and a integer value and returns a vector of this integer.+ squareRootMax :: a -> a -- ^ Compute the square root. Ensure the output is >=1e-3. Used for normalisation.+ clipValue :: Double -> a -> a -- ^ Clip the value(s) to the given range. Used for normalisation. +infixl 7 `multiply`, `divideInt`, `divide`+infixl 6 `plus`, `minus` + instance (WelfordOnline a) => WelfordOnline (VB.Vector a) where plus = VB.zipWith plus {-# INLINE plus #-}@@ -130,8 +107,16 @@ {-# INLINE minus #-} multiply = VB.zipWith multiply {-# INLINE multiply #-}+ divide = VB.zipWith divide+ {-# INLINE divide #-} divideInt x i = VB.map (`divideInt` i) x {-# INLINE divideInt #-}+ constInt x n = VB.map (`constInt` n) x+ {-# INLINE constInt #-}+ squareRootMax = VB.map squareRootMax+ {-# INLINE squareRootMax #-}+ clipValue v = VB.map (clipValue v)+ {-# INLINE clipValue #-} instance (WelfordOnline a, VS.Storable a) => WelfordOnline (VS.Vector a) where plus = VS.zipWith plus@@ -140,8 +125,16 @@ {-# INLINE minus #-} multiply = VS.zipWith multiply {-# INLINE multiply #-}+ divide = VS.zipWith divide+ {-# INLINE divide #-} divideInt x i = VS.map (`divideInt` i) x {-# INLINE divideInt #-}+ constInt x n = VS.map (`constInt` n) x+ {-# INLINE constInt #-}+ squareRootMax = VS.map squareRootMax+ {-# INLINE squareRootMax #-}+ clipValue v = VS.map (clipValue v)+ {-# INLINE clipValue #-} instance (WelfordOnline a, VU.Unbox a) => WelfordOnline (VU.Vector a) where@@ -151,36 +144,137 @@ {-# INLINE minus #-} multiply = VU.zipWith multiply {-# INLINE multiply #-}+ divide = VU.zipWith divide+ {-# INLINE divide #-} divideInt x i = VU.map (`divideInt` i) x {-# INLINE divideInt #-}+ constInt x n = VU.map (`constInt` n) x+ {-# INLINE constInt #-}+ squareRootMax = VU.map squareRootMax+ {-# INLINE squareRootMax #-}+ clipValue v = VU.map (clipValue v)+ {-# INLINE clipValue #-} instance WelfordOnline Double where- plus = (+)+ plus = (+) {-# INLINE plus #-}- minus = (-)+ minus = (-) {-# INLINE minus #-}- multiply = (*)+ multiply = (*) {-# INLINE multiply #-}+ divide = (/)+ {-# INLINE divide #-} divideInt x i = x / fromIntegral i {-# INLINE divideInt #-}+ constInt _ n = fromIntegral n+ {-# INLINE constInt #-}+ squareRootMax = max 1e-3 . sqrt+ {-# INLINE squareRootMax #-}+ clipValue v = min v . max (-v)+ {-# INLINE clipValue #-} + instance WelfordOnline Float where- plus = (+)+ plus = (+) {-# INLINE plus #-}- minus = (-)+ minus = (-) {-# INLINE minus #-}- multiply = (*)+ multiply = (*) {-# INLINE multiply #-}+ divide = (/)+ {-# INLINE divide #-} divideInt x i = x / fromIntegral i {-# INLINE divideInt #-}+ constInt _ n = fromIntegral n+ {-# INLINE constInt #-}+ squareRootMax = max 1e-3 . sqrt+ {-# INLINE squareRootMax #-}+ clipValue v = min v' . max (-v')+ where v' = realToFrac v+ {-# INLINE clipValue #-} + instance WelfordOnline Rational where- plus = (+)+ plus = (+) {-# INLINE plus #-}- minus = (-)+ minus = (-) {-# INLINE minus #-}- multiply = (*)+ multiply = (*) {-# INLINE multiply #-}+ divide = (/)+ {-# INLINE divide #-} divideInt x i = x / fromIntegral i {-# INLINE divideInt #-}+ constInt _ n = fromIntegral n+ {-# INLINE constInt #-}+ squareRootMax = toRational . max (1e-3 :: Double) . sqrt . fromRational+ {-# INLINE squareRootMax #-}+ clipValue v = min v' . max (-v')+ where v' = toRational v+ {-# INLINE clipValue #-}+++-- | Add one value to the current aggregate.+addValue :: (WelfordOnline a) => WelfordExistingAggregate a -> a -> WelfordExistingAggregate a+addValue (WelfordExistingAggregate count mean m2) val =+ let count' = count + 1+ delta = val `minus` mean+ mean' = mean `plus` (delta `divideInt` count')+ delta2 = val `minus` mean'+ m2' = m2 `plus` (delta `multiply` delta2)+ in WelfordExistingAggregate count' mean' m2'+addValue WelfordExistingAggregateEmpty val =+ let count' = 1+ delta' = val+ mean' = delta' `divideInt` count'+ delta2' = val `minus` mean'+ m2' = delta' `multiply` delta2'+ in WelfordExistingAggregate count' mean' m2'++-- | Add multiple values to the current aggregate. This is `foldl addValue`.+addValues :: (WelfordOnline a, Foldable f) => WelfordExistingAggregate a -> f a -> WelfordExistingAggregate a+addValues = foldl addValue+++-- | Calculate mean, variance and sample variance from aggregate. Calls `error` for `WelfordExistingAggregateEmpty`.+finalize :: (WelfordOnline a) => WelfordExistingAggregate a -> (Mean a, Variance a, SampleVariance a)+finalize = fromMaybe err . mFinalize+ where err = error "Statistics.Sample.WelfordOnlineMeanVariance.finalize: Emtpy Welford Online Aggreate. Add data first!"+++-- | Calculate mean, variance and sample variance from aggregate. Safe function.+mFinalize :: (WelfordOnline a) => WelfordExistingAggregate a -> Maybe (Mean a, Variance a, SampleVariance a)+mFinalize (WelfordExistingAggregate count mean m2)+ | count < 2 = Just (mean, m2, m2)+ | otherwise = Just (mean, m2 `divideInt` count, m2 `divideInt` (count - 1))+mFinalize WelfordExistingAggregateEmpty = Nothing+++-- | Add a new sample to the aggregate and compute mean and variances.+nextValue :: (WelfordOnline a) => WelfordExistingAggregate a -> a -> (WelfordExistingAggregate a, (Mean a, Variance a, SampleVariance a))+nextValue agg val =+ let agg' = addValue agg val+ in (agg', finalize agg')+++-- | Normalise the given input assuming the learned standard deviation with sample variance to zero mean+-- and unit variance. For the first 100 values the output is clipped to @(-3, 3)@.+normaliseToZeroMeanUnitVariance :: (WelfordOnline a) => WelfordExistingAggregate a -> a -> a+normaliseToZeroMeanUnitVariance WelfordExistingAggregateEmpty x = clipValue 3 x+normaliseToZeroMeanUnitVariance wel x+ | welfordCountUnsafe wel < 100 = clipValue 3 $ (x `minus` mean) `divide` squareRootMax variance+ | otherwise = (x `minus` mean) `divide` squareRootMax variance+ where+ (mean, _, variance) = finalize wel+++-- | Denormalise from a zero mean unit variance normalised value (see `normaliseToZeroMeanUnitVariance`) to+-- the original value(s).+denormaliseFromZeroMeanUnitVariance :: (WelfordOnline a) => WelfordExistingAggregate a -> a -> a+denormaliseFromZeroMeanUnitVariance WelfordExistingAggregateEmpty x = x+denormaliseFromZeroMeanUnitVariance wel x = (x `multiply` squareRootMax variance) `plus` mean+ where+ (mean, _, variance) = finalize wel++
test/Statistics/Sample/WelfordOnlineTest.hs view
@@ -61,3 +61,12 @@ -- print (wMean, wVarSample) eps = min 0.01 $ max 0.001 (0.001 * mean) return $ epsEqWith eps wMean mean .&&. epsEqWith eps wVarSample var+++prop_Normalise :: Gen Property+prop_Normalise = do+ vals <- sized $ \n -> VB.fromList <$> generateNValues (100 + 5 * n)+ let wel = addValues WelfordExistingAggregateEmpty vals :: WelfordExistingAggregate Double+ testVal <- arbitrary+ return $ epsEqWith 0.002 testVal (denormaliseFromZeroMeanUnitVariance wel (normaliseToZeroMeanUnitVariance wel testVal))+
welford-online-mean-variance.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.0.+-- This file has been generated from package.yaml by hpack version 0.35.1. -- -- see: https://github.com/sol/hpack name: welford-online-mean-variance-version: 0.1.0.4+version: 0.2.0.0 synopsis: Online computation of mean and variance using the Welford algorithm. description: Please see the README on GitHub at <https://github.com/githubuser/welford-online-mean-variance#readme> category: Statistics