packages feed

monoid-statistics-1.1.0: Data/Monoid/Statistics/Class.hs

{-# LANGUAGE BangPatterns               #-}
{-# LANGUAGE DefaultSignatures          #-}
{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE DeriveFoldable             #-}
{-# LANGUAGE DeriveFunctor              #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE DeriveTraversable          #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
-- |
-- Module     : Data.Monoid.Statistics
-- Copyright  : Copyright (c) 2010,2017, Alexey Khudyakov <alexey.skladnoy@gmail.com>
-- License    : BSD3
-- Maintainer : Alexey Khudyakov <alexey.skladnoy@gmail.com>
-- Stability  : experimental
--
module Data.Monoid.Statistics.Class
  ( -- * Monoid Type class and helpers
    StatMonoid(..)
  , reduceSample
  , reduceSampleVec
    -- * Ad-hoc type classes for select statistics
    -- $adhoc
  , CalcCount(..)
  , CalcMean(..)
  , HasMean(..)
  , CalcVariance(..)
  , HasVariance(..)
  , CalcViaHas(..)
    -- * Exception handling
  , Partial(..)
  , partial
  , SampleError(..)
    -- * Data types
  , Pair(..)
  ) where

import           Control.Exception
import           Control.Monad.Catch (MonadThrow(..))
import           Data.Data           (Typeable,Data)
import           Data.Monoid
import           Data.Vector.Unboxed          (Unbox)
import           Data.Vector.Unboxed.Deriving (derivingUnbox)
import qualified Data.Foldable       as F
import qualified Data.Vector.Generic as G
import           Numeric.Sum
import GHC.Stack    (HasCallStack)
import GHC.Generics (Generic)


-- | This type class is used to express parallelizable constant space
--   algorithms for calculation of statistics. /Statistic/ is function
--   of type @[a]→b@ which does not depend on order of elements. (for
--   example: mean, sum, number of elements, variance, etc).
--
--   For many statistics it's possible to possible to construct
--   constant space algorithm which is expressed as fold. Additionally
--   it's usually possible to write function which combine state of
--   fold accumulator to get statistic for union of two samples.
--
--   Thus for such algorithm we have value which corresponds to empty
--   sample, function which which corresponds to merging of two
--   samples, and single step of fold. Last one allows to evaluate
--   statistic given data sample and first two form a monoid and allow
--   parallelization: split data into parts, build estimate for each
--   by folding and then merge them using mappend.
--
--   Instance must satisfy following laws. If floating point
--   arithmetics is used then equality should be understood as
--   approximate.
--
--   > 1. addValue (addValue y mempty) x  == addValue mempty x <> addValue mempty y
--   > 2. x <> y == y <> x
class Monoid m => StatMonoid m a where
  -- | Add one element to monoid accumulator. It's step of fold.
  addValue :: m -> a -> m
  addValue m a = m <> singletonMonoid a
  {-# INLINE addValue #-}
  -- | State of accumulator corresponding to 1-element sample.
  singletonMonoid :: a -> m
  singletonMonoid = addValue mempty
  {-# INLINE singletonMonoid #-}
  {-# MINIMAL addValue | singletonMonoid #-}

-- | Calculate statistic over 'Foldable'. It's implemented in terms of
--   foldl'. Note that in cases when accumulator is immediately
--   consumed by polymorphic function such as 'callMeam' its type
--   becomes ambiguous. @TypeApplication@ then could be used to
--   disambiguate.
--
-- >>> reduceSample @Mean [1,2,3,4]
-- MeanKBN 4 (KBNSum 10.0 0.0)
-- >>> calcMean $ reduceSample @Mean [1,2,3,4] :: Maybe Double
-- Just 2.5
reduceSample :: forall m a f. (StatMonoid m a, F.Foldable f) => f a -> m
reduceSample = F.foldl' addValue mempty

-- | Calculate statistic over vector. Works in same was as
-- 'reduceSample' but works for vectors.
reduceSampleVec :: forall m a v. (StatMonoid m a, G.Vector v a) => v a -> m
reduceSampleVec = G.foldl' addValue mempty
{-# INLINE reduceSampleVec #-}

instance ( StatMonoid m1 a
         , StatMonoid m2 a
         ) => StatMonoid (m1,m2) a where
  addValue (!m1, !m2) a =
    let !m1' = addValue m1 a
        !m2' = addValue m2 a
    in (m1', m2')
  singletonMonoid a = ( singletonMonoid a
                      , singletonMonoid a
                      )

instance ( StatMonoid m1 a
         , StatMonoid m2 a
         , StatMonoid m3 a
         ) => StatMonoid (m1,m2,m3) a where
  addValue (!m1, !m2, !m3) a =
    let !m1' = addValue m1 a
        !m2' = addValue m2 a
        !m3' = addValue m3 a
    in (m1', m2', m3')
  singletonMonoid a = ( singletonMonoid a
                      , singletonMonoid a
                      , singletonMonoid a
                      )

instance ( StatMonoid m1 a
         , StatMonoid m2 a
         , StatMonoid m3 a
         , StatMonoid m4 a
         ) => StatMonoid (m1,m2,m3,m4) a where
  addValue (!m1, !m2, !m3, !m4) a =
    let !m1' = addValue m1 a
        !m2' = addValue m2 a
        !m3' = addValue m3 a
        !m4' = addValue m4 a
    in (m1', m2', m3', m4')
  singletonMonoid a = ( singletonMonoid a
                      , singletonMonoid a
                      , singletonMonoid a
                      , singletonMonoid a
                      )

instance (Num a, a ~ a') => StatMonoid (Sum a) a' where
  singletonMonoid = Sum

instance (Num a, a ~ a') => StatMonoid (Product a) a' where
  singletonMonoid = Product

instance Real a => StatMonoid KahanSum a where
  addValue m x = add m (realToFrac x)
  {-# INLINE addValue #-}

instance Real a => StatMonoid KBNSum a where
  addValue m x = add m (realToFrac x)
  {-# INLINE addValue #-}

instance Real a => StatMonoid KB2Sum a where
  addValue m x = add m (realToFrac x)
  {-# INLINE addValue #-}


----------------------------------------------------------------
-- Ad-hoc type class
----------------------------------------------------------------

-- $adhoc
--
-- Type classes defined here allows to extract common statistics from
-- estimators. it's assumed that quantities in question are already
-- computed so extraction is cheap.
--
--
-- ==== Error handling
--
-- Computation of statistics may fail. For example mean is not defined
-- for an empty sample. @Maybe@ could be seen as easy way to handle
-- this situation. But in many cases most convenient way to handle
-- failure is to throw an exception. So failure is encoded by using
-- polymorphic function of type @MonadThrow m ⇒ a → m X@.
--
-- Maybe types has instance, such as 'Maybe', 'Either'
-- 'Control.Exception.SomeException', 'IO' and most transformers
-- wrapping it. Notably this library defines 'Partial' monad which
-- allows to convert failures to exception in pure setting.
--
-- >>> calcMean $ reduceSample @Mean []
-- *** Exception: EmptySample "Data.Monoid.Statistics.Numeric.MeanKBN: calcMean"
--
-- >>> calcMean $ reduceSample @Mean [] :: Maybe Double
-- Nothing
--
-- >>> import Control.Exception
-- >>> calcMean $ reduceSample @Mean [] :: Either SomeException Double
-- Left (EmptySample "Data.Monoid.Statistics.Numeric.MeanKBN: calcMean")
--
-- Last example uses IO
--
-- >>> calcMean $ reduceSample @Mean []
-- *** Exception: EmptySample "Data.Monoid.Statistics.Numeric.MeanKBN: calcMean"
--
--
-- ==== Deriving instances
--
-- Type classes come in two variants, one that allow failure and one
-- for use in cases when quantity is always defined. This is not the
-- case for estimators, but true for distributions and intended for
-- such use cases. In that case 'CalcViaHas' could be used to derive
-- necessary instances.
--
-- >>> :{
-- data NormalDist = NormalDist !Double !Double
--   deriving (CalcMean,CalcVariance) via CalcViaHas NormalDist
-- instance HasMean NormalDist where
--   getMean (NormalDist mu _) = mu
-- instance HasVariance NormalDist where
--   getVariance   (NormalDist _ s) = s
--   getVarianceML (NormalDist _ s) = s
-- :}


-- | Value from which we can efficiently extract number of elements in
--   sample it represents.
class CalcCount a where
  -- | /Assumed O(1)/. Number of elements in sample.
  calcCount :: a -> Int

-- | Value from which we can efficiently calculate mean of sample or
--   distribution.
class CalcMean a where
  -- | /Assumed O(1)/ Returns @Nothing@ if there isn't enough data to
  --   make estimate or distribution doesn't have defined mean.
  --
  --   \[ \bar{x} = \frac{1}{N}\sum_{i=1}^N{x_i} \]
  calcMean :: MonadThrow m => a -> m Double

-- | Same as 'CalcMean' but should never fail
class CalcMean a => HasMean a where
  getMean :: a -> Double


-- | Values from which we can efficiently compute estimate of sample
--   variance or distribution variance. It has two methods: one which
--   applies bias correction to estimate and another that returns
--   maximul likelyhood estimate. For distribution they should return
--   same value.
class CalcVariance a where
  -- | /Assumed O(1)/ Calculate unbiased estimate of variance:
  --
  --   \[ \sigma^2 = \frac{1}{N-1}\sum_{i=1}^N(x_i - \bar{x})^2 \]
  calcVariance :: MonadThrow m => a -> m Double
  calcVariance = fmap (\x->x*x) . calcStddev
  -- | /Assumed O(1)/ Calculate maximum likelihood estimate of variance:
  --
  --   \[ \sigma^2 = \frac{1}{N}\sum_{i=1}^N(x_i - \bar{x})^2 \]
  calcVarianceML :: MonadThrow m => a -> m Double
  calcVarianceML = fmap (\x->x*x) . calcStddevML
  -- | Calculate sample standard deviation from unbiased estimation of
  --   variance.
  calcStddev :: MonadThrow m => a -> m Double
  calcStddev = fmap sqrt . calcVariance
  -- | Calculate sample standard deviation from maximum likelihood
  --   estimation of variance.
  calcStddevML :: (MonadThrow m) => a -> m Double
  calcStddevML = fmap sqrt . calcVarianceML
  {-# MINIMAL (calcVariance,calcVarianceML) | (calcStddev,calcStddevML) #-}

-- | Same as 'CalcVariance' but never fails
class CalcVariance a => HasVariance a where
  getVariance   :: a -> Double
  getVariance   = (\x -> x*x) . getStddev
  getVarianceML :: a -> Double
  getVarianceML = (\x -> x*x) . getStddevML
  getStddev     :: a -> Double
  getStddev     = sqrt . getVariance
  getStddevML   :: a -> Double
  getStddevML   = sqrt . getVarianceML
  {-# MINIMAL (getVariance,getVarianceML) | (getStddev,getStddevML) #-}




newtype CalcViaHas a = CalcViaHas a
  deriving newtype (HasMean, HasVariance)

instance HasMean a => CalcMean (CalcViaHas a) where
  calcMean = pure . getMean

instance HasVariance a => CalcVariance (CalcViaHas a) where
  calcVariance   = pure . getVariance
  calcVarianceML = pure . getVarianceML

----------------------------------------------------------------
-- Exceptions
----------------------------------------------------------------

-- | Identity monad which is used to encode partial functions for
--   'MonadThrow' based error handling. Its @MonadThrow@ instance
--   just throws normal exception.
newtype Partial a = Partial a
  deriving (Show, Read, Eq, Ord, Typeable, Data, Generic)

-- | Convert error to IO exception. This way one could for example
--   convert case when some statistics is not defined to an exception:
--
-- >>> calcMean $ reduceSample @Mean []
-- *** Exception: EmptySample "Data.Monoid.Statistics.Numeric.MeanKBN: calcMean"
partial :: HasCallStack => Partial a -> a
partial (Partial x) = x

instance Functor Partial where
  fmap f (Partial a) = Partial (f a)

instance Applicative Partial where
  pure = Partial
  Partial f <*> Partial a = Partial (f a)
  (!_) *> a   = a
  a   <* (!_) = a
instance Monad Partial where
  return = pure
  Partial a >>= f = f a
  (>>) = (*>)

instance MonadThrow Partial where
  throwM = throw

-- | Exception which is thrown when we can't compute some value
data SampleError
  = EmptySample String
  -- ^ @EmptySample function@: We're trying to compute quantity that
  --   is undefined for empty sample.
  | InvalidSample String String
  -- ^ @InvalidSample function descripton@ quantity in question could
  --   not be computed for some other reason
  deriving Show

instance Exception SampleError


----------------------------------------------------------------
-- Generic monoids
----------------------------------------------------------------

-- | Strict pair. It allows to calculate two statistics in parallel
data Pair a b = Pair !a !b
              deriving (Show,Eq,Ord,Typeable,Data,Generic)

instance (Semigroup a, Semigroup b) => Semigroup (Pair a b) where
  Pair x y <> Pair x' y' = Pair (x <> x') (y <> y')
  {-# INLINABLE (<>) #-}

instance (Monoid a, Monoid b) => Monoid (Pair a b) where
  mempty  = Pair mempty mempty
  mappend = (<>)
  {-# INLINABLE mempty  #-}
  {-# INLINABLE mappend #-}

instance (StatMonoid a x, StatMonoid b x) => StatMonoid (Pair a b) x where
  addValue (Pair a b) !x = Pair (addValue a x) (addValue b x)
  singletonMonoid x = Pair (singletonMonoid x) (singletonMonoid x)
  {-# INLINE addValue        #-}
  {-# INLINE singletonMonoid #-}


-- | Strict pair for parallel accumulation
data PPair a b = PPair !a !b

instance (Semigroup a, Semigroup b) => Semigroup (PPair a b) where
  PPair x y <> PPair x' y' = PPair (x <> x') (y <> y')
  {-# INLINABLE (<>) #-}

instance (Monoid a, Monoid b) => Monoid (PPair a b) where
  mempty  = PPair mempty mempty
  mappend = (<>)
  {-# INLINABLE mempty  #-}
  {-# INLINABLE mappend #-}

instance (StatMonoid a x, StatMonoid b y) => StatMonoid (PPair a b) (x,y) where
  addValue (PPair a b) (!x,!y) = PPair (addValue a x) (addValue b y)
  singletonMonoid (!x,!y) = PPair (singletonMonoid x) (singletonMonoid y)
  {-# INLINE addValue        #-}
  {-# INLINE singletonMonoid #-}





derivingUnbox "Pair"
  [t| forall a b. (Unbox a, Unbox b) => Pair a b -> (a,b) |]
  [| \(Pair a b) -> (a,b) |]
  [| \(a,b) -> Pair a b   |]

-- $setup
--
-- >>> :set -XDerivingVia
-- >>> import Data.Monoid.Statistics.Numeric