packages feed

javelin-0.1.2.0: src/Data/Series/Generic/Aggregation.hs

module Data.Series.Generic.Aggregation ( 
    -- * Grouping
    Grouping,
    groupBy,
    aggregateWith,
    foldWith,

    -- * Windowing
    expanding,
    windowing,

    -- * Folding
    all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,
    argmax, argmin,
) where

import qualified Data.List 
import qualified Data.Map.Strict                as Map
import           Data.Ord                       ( Down(..) )
import           Data.Series.Generic.Definition ( Series(..) )
import qualified Data.Series.Generic.Definition as GSeries
import           Data.Series.Generic.View       ( Range, slice, select )
import qualified Data.Vector                    as Boxed
import           Data.Vector.Generic            ( Vector )
import qualified Data.Vector.Generic            as Vector
import           Prelude                        hiding ( last, null, length, all, any, and, or, sum, product, maximum, minimum )

-- $setup
-- >>> import qualified Data.Series as Series
-- >>> import qualified Data.Set as Set

-- | Group values in a 'Series' by some grouping function (@k -> g@).
-- The provided grouping function is guaranteed to operate on a non-empty 'Series'.
--
-- This function is expected to be used in conjunction with @aggregate@:
-- 
-- >>> import Data.Maybe ( fromMaybe )
-- >>> type Date = (Int, String)
-- >>> month :: (Date -> String) = snd
-- >>> :{ 
--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
--                              , ((2021, "January"), -5)
--                              , ((2020, "June")   , 20)
--                              , ((2021, "June")   , 25) 
--                              ]
--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
-- :}
--     index | values
--     ----- | ------
-- "January" |     -5
--    "June" |     20
groupBy :: Series v k a       -- ^ Input series
        -> (k -> g)           -- ^ Grouping function
        -> Grouping k g v a   -- ^ Grouped series
{-# INLINABLE groupBy #-}
groupBy = MkGrouping


-- | Representation of a 'Series' being grouped.
data Grouping k g v a 
    = MkGrouping (Series v k a)  (k -> g)


-- | Aggregate groups resulting from a call to 'groupBy':
-- 
-- >>> import Data.Maybe ( fromMaybe )
-- >>> type Date = (Int, String)
-- >>> month :: (Date -> String) = snd
-- >>> :{ 
--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
--                              , ((2021, "January"), -5)
--                              , ((2020, "June")   , 20)
--                              , ((2021, "June")   , 25) 
--                              ]
--      in xs `groupBy` month `aggregateWith` (fromMaybe 0 . minimum)
-- :}
--     index | values
--     ----- | ------
-- "January" |     -5
--    "June" |     20
--
-- If you want to aggregate groups using a binary function, see 'foldWith' which
-- may be much faster.
aggregateWith :: (Ord g, Vector v a, Vector v b) 
              => Grouping k g v a 
              -> (Series v k a -> b) 
              -> Series v g b
{-# INLINABLE aggregateWith #-}
aggregateWith (MkGrouping xs by) f
    = GSeries.fromStrictMap 
    -- Using `fromDistinctAscList` is predicated on a particular structure
    -- created by the `acc` function below.
    -- This is rather unsafe, and has been the source of bugs in the past
    $ fmap (f . GSeries.fromDistinctAscList)
    -- We're using a list fold to limit the number of 
    -- type constraints. This is about as fast as it is 
    -- with a Vector fold
    $ Data.List.foldl' acc mempty 
    $ GSeries.toList xs
    where
        acc !m (key, val) = Map.insertWith (flip (<>)) -- Flipping arguments to ensure that keys are ordered as expected
                                           (by key) 
                                           (Data.List.singleton (key, val)) 
                                           m


-- | Fold over each group in a 'Grouping' using a binary function.
-- While this is not as expressive as 'aggregateWith', users looking for maximum
-- performance should use 'foldWith' as much as possible.
--
-- >>> type Date = (Int, String)
-- >>> month :: (Date -> String) = snd
-- >>> :{ 
--     let xs = Series.fromList [ ((2020, "January") :: Date,  0 :: Int)
--                              , ((2021, "January"), -5)
--                              , ((2020, "June")   , 20)
--                              , ((2021, "June")   , 25) 
--                              ]
--      in xs `groupBy` month `foldWith` min
-- :}
--     index | values
--     ----- | ------
-- "January" |     -5
--    "June" |     20
foldWith :: (Ord g, Vector v a) 
         => Grouping k g v a
         -> (a -> a -> a)
         -> Series v g a
{-# INLINABLE foldWith #-}
foldWith (MkGrouping xs by) f 
    = GSeries.fromStrictMap 
    -- We're using a list fold to limit the number of 
    -- type constraints. This is about as fast as it is 
    -- with a Vector fold
    $ Data.List.foldl' acc mempty 
    $ GSeries.toList xs
    where
        acc !m (key, val) = Map.insertWith f (by key) val m


-- | Expanding window aggregation.
--
-- >>> import qualified Data.Series as Series 
-- >>> :{ 
--     let (xs :: Series.Series Int Int) 
--          = Series.fromList [ (1, 0)
--                            , (2, 1)
--                            , (3, 2)
--                            , (4, 3)
--                            , (5, 4)
--                            , (6, 5)
--                            ]
--     in (xs `expanding` sum) :: Series.Series Int Int 
-- :}
-- index | values
-- ----- | ------
--     1 |      0
--     2 |      1
--     3 |      3
--     4 |      6
--     5 |     10
--     6 |     15
expanding :: (Vector v a, Vector v b) 
          => Series v k a        -- ^ Series vector
          -> (Series v k a -> b) -- ^ Aggregation function
          -> Series v k b        -- ^ Resulting vector
{-# INLINABLE expanding #-}
expanding vs f = MkSeries (index vs) $ Vector.unfoldrExactN (GSeries.length vs) go 0
    where
        -- Recall that `slice` does NOT include the right index
        go ix = (f $ slice 0 (ix + 1) vs, ix + 1)


-- | General-purpose window aggregation.
--
-- >>> import qualified Data.Series as Series 
-- >>> import           Data.Series ( to )
-- >>> :{ 
--     let (xs :: Series.Series Int Int) 
--          = Series.fromList [ (1, 0)
--                            , (2, 1)
--                            , (3, 2)
--                            , (4, 3)
--                            , (5, 4)
--                            , (6, 5)
--                            ]
--     in windowing (\k -> k `to` (k + 2)) sum xs
-- :}
-- index | values
-- ----- | ------
--     1 |      3
--     2 |      6
--     3 |      9
--     4 |     12
--     5 |      9
--     6 |      5
windowing :: (Ord k, Vector v a, Vector v b)
          => (k -> Range k)
          -> (Series v k a -> b)
          -> Series v k a
          -> Series v k b
{-# INLINABLE windowing #-}
windowing range agg series 
    = GSeries.mapWithKey (\k _ -> agg $ series `select` range k) series


-- | \(O(n)\) Check if all elements satisfy the predicate.
all :: Vector v a => (a -> Bool) -> Series v k a -> Bool
{-# INLINABLE all #-}
all f = Vector.all f . values


-- | \(O(n)\) Check if any element satisfies the predicate.
any :: Vector v a => (a -> Bool) -> Series v k a -> Bool
{-# INLINABLE any #-}
any f = Vector.any f . values


-- | \(O(n)\) Check if all elements are 'True'.
and :: Vector v Bool => Series v k Bool -> Bool
{-# INLINABLE and #-}
and = Vector.and . values


-- | \(O(n)\) Check if any element is 'True'.
or :: Vector v Bool => Series v k Bool -> Bool
{-# INLINABLE or #-}
or = Vector.or . values


-- | \(O(n)\) Compute the sum of the elements.
sum :: (Num a, Vector v a) => Series v k a -> a
{-# INLINABLE sum #-}
sum = Vector.sum . values


-- | \(O(n)\) Compute the product of the elements.
product :: (Num a, Vector v a) => Series v k a -> a
{-# INLINABLE product #-}
product = Vector.product . values


nothingIfEmpty :: Vector v a 
               => (Series v k a -> b) -> (Series v k a -> Maybe b)
nothingIfEmpty f xs = if GSeries.null xs then Nothing else Just (f xs) 


-- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.
maximum :: (Ord a, Vector v a) => Series v k a -> Maybe a
{-# INLINABLE maximum #-}
maximum = nothingIfEmpty $ Vector.maximum . values


-- | \(O(n)\) @'maximumOn' f xs@ teturns the maximum element of the series @xs@, as determined by the function @f@.
-- In case of a tie, the first occurrence wins.
-- If the 'Series' is empty, @Nothing@ is returned.
maximumOn :: (Ord b, Vector v a) => (a -> b) -> Series v k a -> Maybe a
{-# INLINABLE maximumOn #-}
maximumOn f = nothingIfEmpty $ Vector.maximumOn f . values


-- | \(O(n)\) Yield the minimum element of the series. In case of a tie, the first occurrence wins.
-- If the 'Series' is empty, @Nothing@ is returned.
minimum :: (Ord a, Vector v a) => Series v k a -> Maybe a
{-# INLINABLE minimum #-}
minimum = nothingIfEmpty $ Vector.minimum . values


-- | \(O(n)\) @'minimumOn' f xs@ teturns the minimum element of the series @xs@, as determined by the function @f@.
-- In case of a tie, the first occurrence wins.
-- If the 'Series' is empty, @Nothing@ is returned.
minimumOn :: (Ord b, Vector v a) => (a -> b) -> Series v k a -> Maybe a
{-# INLINABLE minimumOn #-}
minimumOn f = nothingIfEmpty $ Vector.minimumOn f . values


-- | \(O(n)\) Find the index of the maximum element in the input series.
-- If the input series is empty, 'Nothing' is returned.
--
-- The index of the first occurrence of the maximum element is returned.
--
-- >>> import qualified Data.Series as Series 
-- >>> :{ 
--     let (xs :: Series.Series Int Int) 
--          = Series.fromList [ (1, 0)
--                            , (2, 1)
--                            , (3, 2)
--                            , (4, 7)
--                            , (5, 4)
--                            , (6, 5)
--                            ]
--     in argmax xs 
-- :}
-- Just 4
argmax :: (Ord a, Vector v a)
       => Series v k a
       -> Maybe k
{-# INLINABLE argmax #-}
argmax xs | GSeries.null xs = Nothing
          | otherwise = Just 
                      . fst 
                      -- We're forcing the use of boxed vectors in order to
                      -- reduce the constraints on the vector instance
                      . Boxed.maximumOn snd 
                      . GSeries.toVector
                      . GSeries.convert
                      $ xs


-- | \(O(n)\) Find the index of the minimum element in the input series.
-- If the input series is empty, 'Nothing' is returned.
--
-- The index of the first occurrence of the minimum element is returned.
--
-- >>> import qualified Data.Series as Series 
-- >>> :{ 
--     let (xs :: Series.Series Int Int) 
--          = Series.fromList [ (1, 1)
--                            , (2, 1)
--                            , (3, 2)
--                            , (4, 0)
--                            , (5, 4)
--                            , (6, 5)
--                            ]
--     in argmin xs 
-- :}
-- Just 4
argmin :: (Ord a, Vector v a, Vector v (Down a))
       => Series v k a
       -> Maybe k
{-# INLINABLE argmin #-}
argmin = argmax . GSeries.map Down