javelin 0.1.0.0 → 0.1.1.0
raw patch · 29 files changed
+7586/−7630 lines, 29 filesdep −ieee754dep −statisticsPVP ok
version bump matches the API change (PVP)
Dependencies removed: ieee754, statistics
API changes (from Hackage documentation)
+ Data.Series.Index: indexed :: Index k -> Index (Int, k)
Files
- CHANGELOG.md +10/−5
- LICENSE +20/−20
- benchmarks/Comparison.hs +200/−200
- benchmarks/Operations.hs +69/−69
- javelin.cabal +136/−141
- scripts/bench-report.hs +99/−99
- src/Data/Series.hs +1360/−1360
- src/Data/Series/Generic.hs +98/−86
- src/Data/Series/Generic/Aggregation.hs +325/−325
- src/Data/Series/Generic/Definition.hs +832/−832
- src/Data/Series/Generic/Internal.hs +26/−26
- src/Data/Series/Generic/Numeric.hs +0/−7
- src/Data/Series/Generic/Scans.hs +112/−112
- src/Data/Series/Generic/View.hs +336/−333
- src/Data/Series/Generic/Zip.hs +463/−463
- src/Data/Series/Index.hs +108/−107
- src/Data/Series/Index/Definition.hs +517/−503
- src/Data/Series/Index/Internal.hs +39/−39
- src/Data/Series/Tutorial.hs +770/−770
- src/Data/Series/Unboxed.hs +1291/−1291
- test/Main.hs +19/−21
- test/Test/Data/Series.hs +6/−6
- test/Test/Data/Series/Generic/Aggregation.hs +133/−133
- test/Test/Data/Series/Generic/Definition.hs +205/−205
- test/Test/Data/Series/Generic/Numeric.hs +0/−62
- test/Test/Data/Series/Generic/View.hs +142/−142
- test/Test/Data/Series/Generic/Zip.hs +147/−147
- test/Test/Data/Series/Index.hs +123/−113
- test/Test/Utils.hs +0/−13
CHANGELOG.md view
@@ -1,5 +1,10 @@-# Revision history for javelin--## Release 0.1.0.0--* This is the first version of `javelin` and associated packages.+# Revision history for javelin + +## Release 0.1.1.0 + +* Added the `Data.Series.Index.indexed` function +* Replace all INLINE pragmas for INLINABLE, which will improve compilation speed and performance. + +## Release 0.1.0.0 + +* This is the first version of `javelin` and associated packages.
LICENSE view
@@ -1,20 +1,20 @@-Copyright (c) Laurent P. René de Cotret--Permission is hereby granted, free of charge, to any person obtaining-a copy of this software and associated documentation files (the-"Software"), to deal in the Software without restriction, including-without limitation the rights to use, copy, modify, merge, publish,-distribute, sublicense, and/or sell copies of the Software, and to-permit persons to whom the Software is furnished to do so, subject to-the following conditions:--The above copyright notice and this permission notice shall be included-in all copies or substantial portions of the Software.--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.+Copyright (c) Laurent P. René de Cotret + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
benchmarks/Comparison.hs view
@@ -1,201 +1,201 @@--- This benchmarking script is forked from--- https://github.com/haskell-perf/dictionaries/blob/master/Time.hs-{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# LANGUAGE BangPatterns #-}-{-# LANGUAGE ExistentialQuantification #-}--module Main (main) where--import Control.DeepSeq ( NFData, force )-import qualified Control.Foldl as Fold-import Control.Monad ( when )-import Criterion.Main ( defaultMainWith, defaultConfig, bench, bgroup, env, nf )-import Criterion.Types ( Config(csvFile) )-import Data.List ( foldl' )-import qualified Data.Map.Lazy-import qualified Data.Map.Strict-import Data.MonoTraversable ( ofoldlUnwrap )-import Data.Set ( Set )-import qualified Data.Set as Set -import qualified Data.Series-import qualified Data.Series.Unboxed-import qualified Data.Series.Index as Index-import qualified Data.Vector-import qualified Data.Vector.Unboxed-import System.Directory ( doesFileExist, removeFile )-import System.Random ( mkStdGen, Random(randoms) )--data Lookup =- forall f. (NFData (f Int)) =>- Lookup String- ([(Int, Int)] -> f Int)- (Int -> f Int -> Maybe Int)--data Sum =- forall f. (NFData (f Int)) =>- Sum String ([(Int, Int)] -> f Int) (f Int -> Int)--data Fold =- forall f. (NFData (f Double)) =>- Fold String ([(Int, Double)] -> f Double) (f Double -> Double)--data Mappend = - forall f. (NFData (f Int), Monoid (f Int)) =>- Mappend String - ([(Int, Int)] -> f Int)--data SliceByKeys =- forall f. (NFData (f Int), Monoid (f Int)) =>- SliceByKeys String - ([(Int, Int)] -> f Int)- (Set Int -> f Int -> f Int)-----main :: IO ()-main = do- let fp = "out.csv"- exists <- doesFileExist fp- when exists (removeFile fp)- defaultMainWith- defaultConfig {csvFile = Just fp}- [ bgroup- "Lookup Int (Randomized)"- (lookupRandomized- [ Lookup "Data.Map.Lazy" Data.Map.Lazy.fromList Data.Map.Lazy.lookup- , Lookup- "Data.Map.Strict"- Data.Map.Strict.fromList- Data.Map.Strict.lookup- , Lookup- "Data.Series"- Data.Series.fromList- (flip Data.Series.at)- , Lookup - "Data.Vector"- (Data.Vector.fromList . map fst)- (\ix -> Data.Vector.find (==ix))- , Lookup- "Data.Series.Unboxed"- Data.Series.Unboxed.fromList- (flip Data.Series.Unboxed.at)- , Lookup - "Data.Vector.Unboxed"- (Data.Vector.Unboxed.fromList . map fst)- (\ix -> Data.Vector.Unboxed.find (==ix))- ])- , bgroup- "Sum Int (Randomized)"- (sumRandomized- [ Sum "Data.Map.Lazy" Data.Map.Lazy.fromList sum- , Sum "Data.Map.Strict" Data.Map.Strict.fromList sum- , Sum "Data.Series" Data.Series.fromList sum- , Sum "Data.Vector" (Data.Vector.fromList . map snd) sum- , Sum "Data.Series.Unboxed" Data.Series.Unboxed.fromList Data.Series.Unboxed.sum- , Sum "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) Data.Vector.Unboxed.sum- ])- , bgroup- "Fold mean (Randomized)"- (foldRandomized- [ Fold "Data.Map.Lazy" Data.Map.Lazy.fromList (Fold.fold Fold.mean)- , Fold "Data.Map.Strict" Data.Map.Strict.fromList (Fold.fold Fold.mean)- , Fold "Data.Series" Data.Series.fromList (Data.Series.fold Fold.mean)- , Fold "Data.Vector" (Data.Vector.fromList . map snd) (Fold.fold Fold.mean)- , Fold "Data.Series.Unboxed" Data.Series.Unboxed.fromList (Data.Series.Unboxed.fold Fold.mean)- , Fold "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) (Fold.purely ofoldlUnwrap Fold.mean)- ])- , bgroup- "Mappend Int (Randomized)"- ( mappendRandomized - [ Mappend "Data.Map.Lazy" Data.Map.Lazy.fromList- , Mappend "Data.Map.Strict" Data.Map.Strict.fromList- , Mappend "Data.Series" Data.Series.fromList- , Mappend "Data.Vector" (Data.Vector.fromList . map snd)- , Mappend "Data.Series.Unboxed" Data.Series.Unboxed.fromList- , Mappend "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd)- ])- , bgroup- "Slice by keys (Randomized)"- ( sliceByKeyRandomized - [ SliceByKeys "Data.Map.Lazy" - Data.Map.Lazy.fromList- (flip Data.Map.Lazy.restrictKeys)- , SliceByKeys "Data.Map.Strict" - Data.Map.Strict.fromList- (flip Data.Map.Strict.restrictKeys)- , SliceByKeys "Data.Series" - Data.Series.fromList- (\ks xs -> xs `Data.Series.select` Index.fromSet ks)- , SliceByKeys "Data.Series.Unboxed" - Data.Series.Unboxed.fromList- (\ks xs -> xs `Data.Series.Unboxed.select` Index.fromSet ks)- ])- ]-- where- lookupRandomized funcs =- [ env- (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])- !elems = force (fromList list)- in pure (list, elems))- (\(~(list, elems)) ->- bench (title ++ ":" ++ show i) $- nf- (foldl'- (\_ k ->- case func k elems of- Just !v -> v- Nothing -> 0)- 0)- (map fst list))- | i <- [10, 100, 1000, 10000]- , Lookup title fromList func <- funcs- ]- sumRandomized funcs =- [ env- (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])- !elems = force (fromList list)- in pure (list, elems))- (\(~(_, elems)) ->- bench (title ++ ":" ++ show i) $- nf func elems)- | i <- [10, 100, 1000, 10000, 100000, 1000000]- , Sum title fromList func <- funcs- ]- foldRandomized funcs =- [ env- (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])- !elems = force (fromList list)- in pure (list, elems))- (\(~(_, elems)) ->- bench (title ++ ":" ++ show i) $- nf func elems)- | i <- [10, 100, 1000, 10000, 100000, 1000000]- , Fold title fromList func <- funcs- ]- mappendRandomized funcs =- [ env- (let list1 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])- list2 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])- !elems1 = force (fromList list1)- !elems2 = force (fromList list2)- in pure (elems1, elems2))- (\(~(elems1, elems2)) ->- bench (title ++ ":" ++ show i) $- nf mconcat [elems1, elems2])- | i <- [10, 100, 1000, 10000, 100000, 1000000]- , Mappend title fromList <- funcs- ]- sliceByKeyRandomized funcs = - [ env- (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..])- keys = Set.fromList $ take (round ((fromIntegral i / 10) :: Double)) (randoms (mkStdGen 0) :: [Int])- !elems = force (fromList list)- in pure (keys, elems))- (\(~(keys, elems)) ->- bench (title ++ ":" ++ show i) $- nf (slice keys) elems)- | i <- [10, 100, 1000, 10000, 100000, 1000000]- , SliceByKeys title fromList slice <- funcs+-- This benchmarking script is forked from +-- https://github.com/haskell-perf/dictionaries/blob/master/Time.hs +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE ExistentialQuantification #-} + +module Main (main) where + +import Control.DeepSeq ( NFData, force ) +import qualified Control.Foldl as Fold +import Control.Monad ( when ) +import Criterion.Main ( defaultMainWith, defaultConfig, bench, bgroup, env, nf ) +import Criterion.Types ( Config(csvFile) ) +import Data.List ( foldl' ) +import qualified Data.Map.Lazy +import qualified Data.Map.Strict +import Data.MonoTraversable ( ofoldlUnwrap ) +import Data.Set ( Set ) +import qualified Data.Set as Set +import qualified Data.Series +import qualified Data.Series.Unboxed +import qualified Data.Series.Index as Index +import qualified Data.Vector +import qualified Data.Vector.Unboxed +import System.Directory ( doesFileExist, removeFile ) +import System.Random ( mkStdGen, Random(randoms) ) + +data Lookup = + forall f. (NFData (f Int)) => + Lookup String + ([(Int, Int)] -> f Int) + (Int -> f Int -> Maybe Int) + +data Sum = + forall f. (NFData (f Int)) => + Sum String ([(Int, Int)] -> f Int) (f Int -> Int) + +data Fold = + forall f. (NFData (f Double)) => + Fold String ([(Int, Double)] -> f Double) (f Double -> Double) + +data Mappend = + forall f. (NFData (f Int), Monoid (f Int)) => + Mappend String + ([(Int, Int)] -> f Int) + +data SliceByKeys = + forall f. (NFData (f Int), Monoid (f Int)) => + SliceByKeys String + ([(Int, Int)] -> f Int) + (Set Int -> f Int -> f Int) + + + + +main :: IO () +main = do + let fp = "out.csv" + exists <- doesFileExist fp + when exists (removeFile fp) + defaultMainWith + defaultConfig {csvFile = Just fp} + [ bgroup + "Lookup Int (Randomized)" + (lookupRandomized + [ Lookup "Data.Map.Lazy" Data.Map.Lazy.fromList Data.Map.Lazy.lookup + , Lookup + "Data.Map.Strict" + Data.Map.Strict.fromList + Data.Map.Strict.lookup + , Lookup + "Data.Series" + Data.Series.fromList + (flip Data.Series.at) + , Lookup + "Data.Vector" + (Data.Vector.fromList . map fst) + (\ix -> Data.Vector.find (==ix)) + , Lookup + "Data.Series.Unboxed" + Data.Series.Unboxed.fromList + (flip Data.Series.Unboxed.at) + , Lookup + "Data.Vector.Unboxed" + (Data.Vector.Unboxed.fromList . map fst) + (\ix -> Data.Vector.Unboxed.find (==ix)) + ]) + , bgroup + "Sum Int (Randomized)" + (sumRandomized + [ Sum "Data.Map.Lazy" Data.Map.Lazy.fromList sum + , Sum "Data.Map.Strict" Data.Map.Strict.fromList sum + , Sum "Data.Series" Data.Series.fromList sum + , Sum "Data.Vector" (Data.Vector.fromList . map snd) sum + , Sum "Data.Series.Unboxed" Data.Series.Unboxed.fromList Data.Series.Unboxed.sum + , Sum "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) Data.Vector.Unboxed.sum + ]) + , bgroup + "Fold mean (Randomized)" + (foldRandomized + [ Fold "Data.Map.Lazy" Data.Map.Lazy.fromList (Fold.fold Fold.mean) + , Fold "Data.Map.Strict" Data.Map.Strict.fromList (Fold.fold Fold.mean) + , Fold "Data.Series" Data.Series.fromList (Data.Series.fold Fold.mean) + , Fold "Data.Vector" (Data.Vector.fromList . map snd) (Fold.fold Fold.mean) + , Fold "Data.Series.Unboxed" Data.Series.Unboxed.fromList (Data.Series.Unboxed.fold Fold.mean) + , Fold "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) (Fold.purely ofoldlUnwrap Fold.mean) + ]) + , bgroup + "Mappend Int (Randomized)" + ( mappendRandomized + [ Mappend "Data.Map.Lazy" Data.Map.Lazy.fromList + , Mappend "Data.Map.Strict" Data.Map.Strict.fromList + , Mappend "Data.Series" Data.Series.fromList + , Mappend "Data.Vector" (Data.Vector.fromList . map snd) + , Mappend "Data.Series.Unboxed" Data.Series.Unboxed.fromList + , Mappend "Data.Vector.Unboxed" (Data.Vector.Unboxed.fromList . map snd) + ]) + , bgroup + "Slice by keys (Randomized)" + ( sliceByKeyRandomized + [ SliceByKeys "Data.Map.Lazy" + Data.Map.Lazy.fromList + (flip Data.Map.Lazy.restrictKeys) + , SliceByKeys "Data.Map.Strict" + Data.Map.Strict.fromList + (flip Data.Map.Strict.restrictKeys) + , SliceByKeys "Data.Series" + Data.Series.fromList + (\ks xs -> xs `Data.Series.select` Index.fromSet ks) + , SliceByKeys "Data.Series.Unboxed" + Data.Series.Unboxed.fromList + (\ks xs -> xs `Data.Series.Unboxed.select` Index.fromSet ks) + ]) + ] + + where + lookupRandomized funcs = + [ env + (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..]) + !elems = force (fromList list) + in pure (list, elems)) + (\(~(list, elems)) -> + bench (title ++ ":" ++ show i) $ + nf + (foldl' + (\_ k -> + case func k elems of + Just !v -> v + Nothing -> 0) + 0) + (map fst list)) + | i <- [10, 100, 1000, 10000] + , Lookup title fromList func <- funcs + ] + sumRandomized funcs = + [ env + (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..]) + !elems = force (fromList list) + in pure (list, elems)) + (\(~(_, elems)) -> + bench (title ++ ":" ++ show i) $ + nf func elems) + | i <- [10, 100, 1000, 10000, 100000, 1000000] + , Sum title fromList func <- funcs + ] + foldRandomized funcs = + [ env + (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..]) + !elems = force (fromList list) + in pure (list, elems)) + (\(~(_, elems)) -> + bench (title ++ ":" ++ show i) $ + nf func elems) + | i <- [10, 100, 1000, 10000, 100000, 1000000] + , Fold title fromList func <- funcs + ] + mappendRandomized funcs = + [ env + (let list1 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..]) + list2 = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..]) + !elems1 = force (fromList list1) + !elems2 = force (fromList list2) + in pure (elems1, elems2)) + (\(~(elems1, elems2)) -> + bench (title ++ ":" ++ show i) $ + nf mconcat [elems1, elems2]) + | i <- [10, 100, 1000, 10000, 100000, 1000000] + , Mappend title fromList <- funcs + ] + sliceByKeyRandomized funcs = + [ env + (let list = take i (zip (randoms (mkStdGen 0) :: [Int]) [1 ..]) + keys = Set.fromList $ take (round ((fromIntegral i / 10) :: Double)) (randoms (mkStdGen 0) :: [Int]) + !elems = force (fromList list) + in pure (keys, elems)) + (\(~(keys, elems)) -> + bench (title ++ ":" ++ show i) $ + nf (slice keys) elems) + | i <- [10, 100, 1000, 10000, 100000, 1000000] + , SliceByKeys title fromList slice <- funcs ]
benchmarks/Operations.hs view
@@ -1,70 +1,70 @@--import Control.DeepSeq ( rnf )-import Control.Exception ( evaluate )-import Criterion.Main ( bench, whnf, defaultMain )--import Data.Foldable ( Foldable(foldl') )-import Data.Set ( Set ) -import qualified Data.Set as Set-import Data.Series ( Series )-import qualified Data.Series as Series-import qualified Data.Series.Index as Index---main :: IO ()-main = do- let srs1 = Series.fromList $ zip [0..] [1::Int .. 2^(12::Int)]- srs2 = Series.fromList $ zip [0,2..] [1::Int .. 2^(12::Int)]- elems = Index.toSet $ Series.index srs1- small = Set.fromAscList [1::Int .. 2^(8::Int)]- elems_even = Set.fromDistinctAscList [2::Int, 4..2^(12::Int)]- elems_odd = Set.fromDistinctAscList [1::Int, 3..2^(12::Int)]- evaluate $ rnf [elems, small, elems_even, elems_odd]- evaluate $ rnf [srs1, srs2]- defaultMain- [ bench "at" $ whnf (at elems_even) srs1- , bench "iat" $ whnf (iat elems_even) srs1- , bench "select" $ whnf (select elems_odd) srs1- , bench "mappend" $ whnf (mappend' srs1) srs2- , bench "zipWithMatched" $ whnf (zipWithMatched srs1) srs1- , bench "group by ... aggregate with ..." $ whnf (groupbyagg small) srs1- , bench "group by ... fold with ..." $ whnf (groupbyfold small) srs1- ]--at :: Set Int -> Series Int Int -> Int-at xs s = foldl' go 0 xs- where- go n x = case s `Series.at` x of - Just _ -> n + 1- Nothing -> n--iat :: Set Int -> Series Int Int -> Int-iat xs s = foldl' go 0 xs- where- go n x = case s `Series.iat` x of - Just _ -> n + 1- Nothing -> n- -select :: Set Int -> Series Int Int -> Int-select ks s = foldl' go 0 ks- where- go n k = n + length (s `Series.select` ((k-100) `Series.to` (k+100)))---mappend' :: Series Int Int -> Series Int Int -> Int-mappend' xs ys = sum $ xs <> ys---zipWithMatched :: Series Int Int -> Series Int Int -> Int-zipWithMatched xs ys = length $ Series.zipWithMatched (+) xs ys---groupbyagg :: Set Int -> Series Int Int -> Int-groupbyagg ks s = foldl' go 0 ks- where- go n k = n + product (s `Series.groupBy` (`mod` (k + 1)) `Series.aggregateWith` sum)--groupbyfold :: Set Int -> Series Int Int -> Int-groupbyfold ks s = foldl' go 0 ks- where+ +import Control.DeepSeq ( rnf ) +import Control.Exception ( evaluate ) +import Criterion.Main ( bench, whnf, defaultMain ) + +import Data.Foldable ( Foldable(foldl') ) +import Data.Set ( Set ) +import qualified Data.Set as Set +import Data.Series ( Series ) +import qualified Data.Series as Series +import qualified Data.Series.Index as Index + + +main :: IO () +main = do + let srs1 = Series.fromList $ zip [0..] [1::Int .. 2^(12::Int)] + srs2 = Series.fromList $ zip [0,2..] [1::Int .. 2^(12::Int)] + elems = Index.toSet $ Series.index srs1 + small = Set.fromAscList [1::Int .. 2^(8::Int)] + elems_even = Set.fromDistinctAscList [2::Int, 4..2^(12::Int)] + elems_odd = Set.fromDistinctAscList [1::Int, 3..2^(12::Int)] + evaluate $ rnf [elems, small, elems_even, elems_odd] + evaluate $ rnf [srs1, srs2] + defaultMain + [ bench "at" $ whnf (at elems_even) srs1 + , bench "iat" $ whnf (iat elems_even) srs1 + , bench "select" $ whnf (select elems_odd) srs1 + , bench "mappend" $ whnf (mappend' srs1) srs2 + , bench "zipWithMatched" $ whnf (zipWithMatched srs1) srs1 + , bench "group by ... aggregate with ..." $ whnf (groupbyagg small) srs1 + , bench "group by ... fold with ..." $ whnf (groupbyfold small) srs1 + ] + +at :: Set Int -> Series Int Int -> Int +at xs s = foldl' go 0 xs + where + go n x = case s `Series.at` x of + Just _ -> n + 1 + Nothing -> n + +iat :: Set Int -> Series Int Int -> Int +iat xs s = foldl' go 0 xs + where + go n x = case s `Series.iat` x of + Just _ -> n + 1 + Nothing -> n + +select :: Set Int -> Series Int Int -> Int +select ks s = foldl' go 0 ks + where + go n k = n + length (s `Series.select` ((k-100) `Series.to` (k+100))) + + +mappend' :: Series Int Int -> Series Int Int -> Int +mappend' xs ys = sum $ xs <> ys + + +zipWithMatched :: Series Int Int -> Series Int Int -> Int +zipWithMatched xs ys = length $ Series.zipWithMatched (+) xs ys + + +groupbyagg :: Set Int -> Series Int Int -> Int +groupbyagg ks s = foldl' go 0 ks + where + go n k = n + product (s `Series.groupBy` (`mod` (k + 1)) `Series.aggregateWith` sum) + +groupbyfold :: Set Int -> Series Int Int -> Int +groupbyfold ks s = foldl' go 0 ks + where go n k = n + product (s `Series.groupBy` (`mod` (k + 1)) `Series.foldWith` (+))
javelin.cabal view
@@ -1,141 +1,136 @@-cabal-version: 3.0-name: javelin-version: 0.1.0.0-synopsis: Labeled one-dimensional arrays-license: MIT-license-file: LICENSE-author: Laurent P. René de Cotret-maintainer: laurent.decotret@outlook.com-category: Data, Data Structures, Data Science-build-type: Simple-extra-doc-files: CHANGELOG.md- files/aapl.txt-tested-with: GHC ==9.8.1 - || ==9.6.3- || ==9.4.7 -description:- - This package implements 'Series', labeled one-dimensional arrays- combining properties from maps and arrays.- - To get started, the important modules are:- - ["Data.Series"] Boxed series of arbitrary types.- - ["Data.Series.Unboxed"] Series of unboxed data types for better performance, at the cost of flexibility.- - ["Data.Series.Generic"] Generic interface to manipulate any type of 'Series'.- - ["Data.Series.Index"] Index containing series keys.- - To get started, please take a look at the tutorial ("Data.Series.Tutorial").- --common common- default-language: GHC2021- ghc-options: -Wall- -Wcompat- -Widentities- -Wincomplete-uni-patterns- -Wincomplete-record-updates- -Wredundant-constraints- -fhide-source-paths- -Wpartial-fields--library- import: common- hs-source-dirs: src- exposed-modules: Data.Series- Data.Series.Generic- Data.Series.Generic.Internal- Data.Series.Index- Data.Series.Index.Internal- Data.Series.Tutorial- Data.Series.Unboxed- other-modules: Data.Series.Generic.Aggregation- Data.Series.Generic.Definition- Data.Series.Generic.Numeric- Data.Series.Generic.Scans- Data.Series.Generic.View- Data.Series.Generic.Zip- Data.Series.Index.Definition- build-depends: base >=4.15.0.0 && <4.20,- containers >=0.6 && <0.8,- deepseq >=1.4 && <1.6,- foldl ^>=1.4,- indexed-traversable ^>=0.1,- vector >=0.12.3.0 && <0.14,- vector-algorithms ^>=0.9--test-suite javelin-test- import: common- type: exitcode-stdio-1.0- hs-source-dirs: test- main-is: Main.hs- other-modules: Test.Data.Series- Test.Data.Series.Index- Test.Data.Series.Generic.Aggregation- Test.Data.Series.Generic.Definition- Test.Data.Series.Generic.Numeric- Test.Data.Series.Generic.View- Test.Data.Series.Generic.Zip- Test.Utils- build-depends: base,- containers,- foldl,- hedgehog,- HUnit,- ieee754,- javelin,- statistics,- tasty,- tasty-hedgehog,- tasty-hspec,- tasty-hunit,- vector----- Running the 'comparison-containers' benchmark is expected--- to be done in conjunction with the cabal.project.profiling project file:--- > cabal bench comparison-containers --project=cabal.project.profiling-benchmark comparison-containers- import: common- type: exitcode-stdio-1.0- ghc-options: -rtsopts- hs-source-dirs: benchmarks- main-is: Comparison.hs- build-depends: base,- containers,- foldl,- mono-traversable,- javelin,- vector, - criterion, - deepseq, - random, - directory----- Running the 'operations' benchmark is expected--- to be done in conjunction with the cabal.project.profiling project file:--- > cabal bench operations --project=cabal.project.profiling-benchmark operations- import: common- type: exitcode-stdio-1.0- ghc-options: -rtsopts- hs-source-dirs: benchmarks- main-is: Operations.hs- build-depends: base,- containers,- deepseq,- foldl,- javelin,- criterion---executable bench-report- import: common- main-is: bench-report.hs- hs-source-dirs: scripts- build-depends: base, - csv ^>=0.1+cabal-version: 3.0 +name: javelin +version: 0.1.1.0 +synopsis: Labeled one-dimensional arrays +license: MIT +license-file: LICENSE +author: Laurent P. René de Cotret +maintainer: laurent.decotret@outlook.com +category: Data, Data Structures, Data Science +build-type: Simple +extra-doc-files: CHANGELOG.md + files/aapl.txt +tested-with: GHC ==9.8.1 + || ==9.6.3 + || ==9.4.7 +description: + + This package implements 'Series', labeled one-dimensional arrays + combining properties from maps and arrays. + + To get started, the important modules are: + + ["Data.Series"] Boxed series of arbitrary types. + + ["Data.Series.Unboxed"] Series of unboxed data types for better performance, at the cost of flexibility. + + ["Data.Series.Generic"] Generic interface to manipulate any type of 'Series'. + + ["Data.Series.Index"] Index containing series keys. + + To get started, please take a look at the tutorial ("Data.Series.Tutorial"). + + +common common + default-language: GHC2021 + ghc-options: -Wall + -Wcompat + -Widentities + -Wincomplete-uni-patterns + -Wincomplete-record-updates + -Wredundant-constraints + -fhide-source-paths + -Wpartial-fields + +library + import: common + hs-source-dirs: src + exposed-modules: Data.Series + Data.Series.Generic + Data.Series.Generic.Internal + Data.Series.Index + Data.Series.Index.Internal + Data.Series.Tutorial + Data.Series.Unboxed + other-modules: Data.Series.Generic.Aggregation + Data.Series.Generic.Definition + Data.Series.Generic.Scans + Data.Series.Generic.View + Data.Series.Generic.Zip + Data.Series.Index.Definition + build-depends: base >=4.15.0.0 && <4.20, + containers >=0.6 && <0.8, + deepseq >=1.4 && <1.6, + foldl ^>=1.4, + indexed-traversable ^>=0.1, + vector >=0.12.3.0 && <0.14, + vector-algorithms ^>=0.9 + +test-suite javelin-test + import: common + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + other-modules: Test.Data.Series + Test.Data.Series.Index + Test.Data.Series.Generic.Aggregation + Test.Data.Series.Generic.Definition + Test.Data.Series.Generic.View + Test.Data.Series.Generic.Zip + build-depends: base, + containers, + foldl, + hedgehog, + HUnit, + javelin, + tasty, + tasty-hedgehog, + tasty-hspec, + tasty-hunit, + vector + + +-- Running the 'comparison-containers' benchmark is expected +-- to be done in conjunction with the cabal.project.profiling project file: +-- > cabal bench comparison-containers --project=cabal.project.profiling +benchmark comparison-containers + import: common + type: exitcode-stdio-1.0 + ghc-options: -rtsopts + hs-source-dirs: benchmarks + main-is: Comparison.hs + build-depends: base, + containers, + foldl, + mono-traversable, + javelin, + vector, + criterion, + deepseq, + random, + directory + + +-- Running the 'operations' benchmark is expected +-- to be done in conjunction with the cabal.project.profiling project file: +-- > cabal bench operations --project=cabal.project.profiling +benchmark operations + import: common + type: exitcode-stdio-1.0 + ghc-options: -rtsopts + hs-source-dirs: benchmarks + main-is: Operations.hs + build-depends: base, + containers, + deepseq, + foldl, + javelin, + criterion + + +executable bench-report + import: common + main-is: bench-report.hs + hs-source-dirs: scripts + build-depends: base, + csv ^>=0.1
scripts/bench-report.hs view
@@ -1,100 +1,100 @@--- This script has been forked from:--- https://github.com/haskell-perf/sets/blob/master/Report.hs-{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}-module Main (main) where--import Data.Function ( on )-import Data.List ( groupBy, intercalate, nub )-import System.Environment ( getArgs )-import Text.CSV ( parseCSVFromFile )-import Text.Printf ( printf )--main :: IO ()-main = do- from:to:_ <- getArgs- reportFromCsv from to--reportFromCsv :: FilePath -> FilePath -> IO ()-reportFromCsv from to = do- result <- parseCSVFromFile from- case result of- Right (_:rows) -> do- writeFile to- (unlines- (map- format- (filter- (not . all (all null))- (groupBy (on (==) (takeWhile (/= '/') . concat . take 1)) rows))))- _ -> error "Couldn't parse csv"--format :: [[String]] -> String-format rows =- ("## " ++ takeWhile (/= '/') (concat (concat (take 1 (drop 1 rows))))) ++- "\n\n" ++- unlines- [ "|Name|" ++ intercalate "|" scales ++ "|"- , "|" ++ concat (replicate (1 + length scales) "---|")- ] ++- unlines- (map- (\name ->- "|" ++ name ++ "|" ++ intercalate "|" (valuesByName name) ++ "|")- names)- where- valuesByName name =- map- (\row@(_:avg:_) ->- let scale = rowScale row- in float (valuesByScale scale) (read avg))- (filter ((== name) . rowName) rows)- valuesByScale scale =- map (\(_:avg:_) -> read avg) (filter ((== scale) . rowScale) rows)- names = nub (map rowName rows)- scales = nub (map rowScale rows)- rowName row =- let s =- takeWhile- (/= ':')- (dropWhile (== '/') (dropWhile (/= '/') (concat (take 1 row))))- in s- rowScale row =- let scale = dropWhile (== ':') (dropWhile (/= ':') (concat (take 1 row)))- in scale--float :: [Double] -> Double -> String-float others x = let (scale, ext) = secs (mean others)- in with (x * scale) ext---- | Convert a number of seconds to a string. The string will consist--- of four decimal places, followed by a short description of the time--- units.-secs :: Double -> (Double, String)-secs k- | k >= 1 = 1 `pair` "s"- | k >= 1e-3 = 1e3 `pair` "ms"- | k >= 1e-6 = 1e6 `pair` "μs"- | k >= 1e-9 = 1e9 `pair` "ns"- | k >= 1e-12 = 1e12 `pair` "ps"- | k >= 1e-15 = 1e15 `pair` "fs"- | k >= 1e-18 = 1e18 `pair` "as"- | otherwise = error "Bad scale"- where pair= (,)--with :: Double -> String -> String-with (t :: Double) (u :: String)- | t >= 1e9 = printf "%.4g %s" t u- | t >= 1e3 = printf "%.0f %s" t u- | t >= 1e2 = printf "%.1f %s" t u- | t >= 1e1 = printf "%.2f %s" t u- | otherwise = printf "%.3f %s" t u---- | Simple rolling average.-mean :: [Double] -> Double-mean =- snd .- foldr- (\x (cnt,avg) ->- ( cnt + 1- , (x + avg * cnt) / (cnt + 1)))+-- This script has been forked from: +-- https://github.com/haskell-perf/sets/blob/master/Report.hs +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +module Main (main) where + +import Data.Function ( on ) +import Data.List ( groupBy, intercalate, nub ) +import System.Environment ( getArgs ) +import Text.CSV ( parseCSVFromFile ) +import Text.Printf ( printf ) + +main :: IO () +main = do + from:to:_ <- getArgs + reportFromCsv from to + +reportFromCsv :: FilePath -> FilePath -> IO () +reportFromCsv from to = do + result <- parseCSVFromFile from + case result of + Right (_:rows) -> do + writeFile to + (unlines + (map + format + (filter + (not . all (all null)) + (groupBy (on (==) (takeWhile (/= '/') . concat . take 1)) rows)))) + _ -> error "Couldn't parse csv" + +format :: [[String]] -> String +format rows = + ("## " ++ takeWhile (/= '/') (concat (concat (take 1 (drop 1 rows))))) ++ + "\n\n" ++ + unlines + [ "|Name|" ++ intercalate "|" scales ++ "|" + , "|" ++ concat (replicate (1 + length scales) "---|") + ] ++ + unlines + (map + (\name -> + "|" ++ name ++ "|" ++ intercalate "|" (valuesByName name) ++ "|") + names) + where + valuesByName name = + map + (\row@(_:avg:_) -> + let scale = rowScale row + in float (valuesByScale scale) (read avg)) + (filter ((== name) . rowName) rows) + valuesByScale scale = + map (\(_:avg:_) -> read avg) (filter ((== scale) . rowScale) rows) + names = nub (map rowName rows) + scales = nub (map rowScale rows) + rowName row = + let s = + takeWhile + (/= ':') + (dropWhile (== '/') (dropWhile (/= '/') (concat (take 1 row)))) + in s + rowScale row = + let scale = dropWhile (== ':') (dropWhile (/= ':') (concat (take 1 row))) + in scale + +float :: [Double] -> Double -> String +float others x = let (scale, ext) = secs (mean others) + in with (x * scale) ext + +-- | Convert a number of seconds to a string. The string will consist +-- of four decimal places, followed by a short description of the time +-- units. +secs :: Double -> (Double, String) +secs k + | k >= 1 = 1 `pair` "s" + | k >= 1e-3 = 1e3 `pair` "ms" + | k >= 1e-6 = 1e6 `pair` "μs" + | k >= 1e-9 = 1e9 `pair` "ns" + | k >= 1e-12 = 1e12 `pair` "ps" + | k >= 1e-15 = 1e15 `pair` "fs" + | k >= 1e-18 = 1e18 `pair` "as" + | otherwise = error "Bad scale" + where pair= (,) + +with :: Double -> String -> String +with (t :: Double) (u :: String) + | t >= 1e9 = printf "%.4g %s" t u + | t >= 1e3 = printf "%.0f %s" t u + | t >= 1e2 = printf "%.1f %s" t u + | t >= 1e1 = printf "%.2f %s" t u + | otherwise = printf "%.3f %s" t u + +-- | Simple rolling average. +mean :: [Double] -> Double +mean = + snd . + foldr + (\x (cnt,avg) -> + ( cnt + 1 + , (x + avg * cnt) / (cnt + 1))) (0, 0)
src/Data/Series.hs view
@@ -1,1361 +1,1361 @@--------------------------------------------------------------------------------- |--- Module : Data.Series--- Copyright : (c) Laurent P. René de Cotret--- License : MIT--- Maintainer : laurent.decotret@outlook.com--- Portability : portable------ This module contains data structures and functions to work with 'Series' capable of holding any Haskell value. --- For better performance, at the cost of less flexibility, see the "Data.Series.Unboxed".------ = Introduction to series------ A 'Series' of type @Series k a@ is a labeled array of values of type @a@,--- indexed by keys of type @k@.------ Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient:------ * random access by key ( \(O(\log n)\) );--- * slice by key ( \(O(\log n)\) ).------ Like `Data.Vector.Vector`, they support efficient:------ * random access by index ( \(O(1)\) );--- * slice by index ( \(O(1)\) );--- * numerical operations.------ This module re-exports most of the content of "Data.Series.Generic", with type signatures --- specialized to the boxed container type `Data.Vector.Vector`.------ For better performance (at the cost of more constraints), especially when it comes to numerical calculations, prefer to--- use "Data.Series.Unboxed", which contains an implementation of series specialized to the unboxed container type `Data.Vector.Unboxed.Vector`.- -module Data.Series (- Series, index, values,-- -- * Building/converting 'Series'- singleton, fromIndex,- -- ** Lists- fromList, toList,- -- ** Vectors- fromVector, toVector,- -- ** Handling duplicates- Occurrence, fromListDuplicates, fromVectorDuplicates,- -- ** Strict Maps- fromStrictMap, toStrictMap,- -- ** Lazy Maps- fromLazyMap, toLazyMap,- -- ** Ad-hoc conversion with other data structures- IsSeries(..),- -- ** Conversion between 'Series' types- G.convert,-- -- * Mapping and filtering- map, mapWithKey, mapIndex, concatMap,- take, takeWhile, drop, dropWhile, filter, filterWithKey,- -- ** Mapping with effects- mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey,-- -- * Combining series- zipWith, zipWithMatched, zipWithKey,- zipWith3, zipWithMatched3, zipWithKey3,- ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,- zipWithMonoid, esum, eproduct, unzip, unzip3,-- -- * Index manipulation- require, catMaybes, dropIndex,-- -- * Accessors- -- ** Bulk access- select, selectWhere, Range, to, from, upto, Selection, - -- ** Single-element access- at, iat,-- -- * Replacing values- replace, (|->), (<-|),-- -- * Scans- forwardFill,-- -- * Grouping and windowing operations- groupBy, Grouping, aggregateWith, foldWith, - windowing, expanding,-- -- * Folds- fold, foldM, foldWithKey, foldMWithKey, foldMapWithKey,- -- ** Specialized folds- G.mean, G.variance, G.std,- length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn, - argmin, argmax,-- -- * Scans- postscanl, prescanl,-- -- * Displaying 'Series'- display, displayWith,- noLongerThan,- DisplayOptions(..), G.defaultDisplayOptions-) where--import Control.Foldl ( Fold, FoldM )-import qualified Data.Map.Lazy as ML-import qualified Data.Map.Strict as MS-import Data.Series.Index ( Index )-import Data.Series.Generic ( IsSeries(..), Range, Selection, ZipStrategy, Occurrence, DisplayOptions(..)- , to, from, upto, skipStrategy, mapStrategy, constStrategy, noLongerThan- )-import qualified Data.Series.Generic as G-import Data.Vector ( Vector )--import Prelude hiding ( map, concatMap, zipWith, zipWith3, filter, take, takeWhile, drop, dropWhile, last, unzip, unzip3- , length, null, all, any, and, or, sum, product, maximum, minimum, - )---- $setup--- >>> import qualified Data.Series as Series--- >>> import qualified Data.Series.Index as Index--infixl 1 `select` -infix 6 |->, <-|---- | A series is a labeled array of values of type @a@,--- indexed by keys of type @k@.------ Like @Data.Map@ and @Data.HashMap@, they support efficient:------ * random access by key ( \(O(\log n)\) );--- * slice by key ( \(O(\log n)\) ).------ Like @Data.Vector.Vector@, they support efficient:------ * random access by index ( \(O(1)\) );--- * slice by index ( \(O(1)\) );--- * numerical operations.-type Series = G.Series Vector---index :: Series k a -> Index k-{-# INLINE index #-}-index = G.index---values :: Series k a -> Vector a-{-# INLINE values #-}-values = G.values----- | Create a 'Series' with a single element.-singleton :: k -> a -> Series k a-{-# INLINE singleton #-}-singleton = G.singleton----- | \(O(n)\) Generate a 'Series' by mapping every element of its index.------ >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d']--- index | values--- ----- | --------- 'a' | 0--- 'b' | 0--- 'c' | 0--- 'd' | 0-fromIndex :: (k -> a) -> Index k -> Series k a-{-# INLINE fromIndex #-}-fromIndex = G.fromIndex----- | Construct a series from a list of key-value pairs. There is no--- condition on the order of pairs.------ >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ]--- >>> xs--- index | values--- ----- | --------- 'a' | 5--- 'b' | 0--- 'd' | 1------ If you need to handle duplicate keys, take a look at `fromListDuplicates`.-fromList :: Ord k => [(k, a)] -> Series k a-{-# INLINE fromList #-}-fromList = G.fromList----- | Construct a series from a list of key-value pairs.--- Contrary to `fromList`, values at duplicate keys are preserved. To keep each--- key unique, an `Occurrence` number counts up.------ >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]--- >>> xs--- index | values--- ----- | --------- ('a',0) | 5--- ('b',0) | 0--- ('d',0) | 1--- ('d',1) | -4--- ('d',2) | 7-fromListDuplicates :: Ord k => [(k, a)] -> Series (k, Occurrence) a-{-# INLINE fromListDuplicates #-}-fromListDuplicates = G.fromListDuplicates----- | Construct a list from key-value pairs. The elements are in order sorted by key:------ >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ]--- >>> xs--- index | values--- ----- | --------- 'a' | 5--- 'b' | 0--- 'd' | 1--- >>> toList xs--- [('a',5),('b',0),('d',1)]-toList :: Series k a -> [(k, a)]-{-# INLINE toList #-}-toList = G.toList----- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. -toVector :: Series k a -> Vector (k, a)-{-# INLINE toVector #-}-toVector = G.toVector----- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no--- condition on the order of pairs. Duplicate keys are silently dropped. If you--- need to handle duplicate keys, see 'fromVectorDuplicates'.------ Note that due to differences in sorting,--- @'Series.fromList'@ and @'Series.fromVector' . 'Vector.fromList'@ --- may not be equivalent if the input list contains duplicate keys.-fromVector :: Ord k => Vector (k, a) -> Series k a-{-# INLINE fromVector #-}-fromVector = G.fromVector----- | Construct a series from a 'Vector' of key-value pairs.--- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each--- key unique, an 'Occurrence' number counts up.------ >>> import qualified Data.Vector as Vector--- >>> let xs = fromVectorDuplicates $ Vector.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]--- >>> xs--- index | values--- ----- | --------- ('a',0) | 5--- ('b',0) | 0--- ('d',0) | 1--- ('d',1) | -4--- ('d',2) | 7-fromVectorDuplicates :: Ord k => Vector (k, a) -> Series (k, Occurrence) a-{-# INLINE fromVectorDuplicates #-}-fromVectorDuplicates = G.fromVectorDuplicates----- | Convert a series into a lazy @Map@.-toLazyMap :: Series k a -> ML.Map k a-{-# INLINE toLazyMap #-}-toLazyMap = G.toLazyMap----- | Construct a series from a lazy @Map@.-fromLazyMap :: ML.Map k a -> Series k a-{-# INLINE fromLazyMap #-}-fromLazyMap = G.fromLazyMap----- | Convert a series into a strict @Map@.-toStrictMap :: Series k a -> MS.Map k a-{-# INLINE toStrictMap #-}-toStrictMap = G.toStrictMap----- | Construct a series from a strict @Map@.-fromStrictMap :: MS.Map k a -> Series k a-{-# INLINE fromStrictMap #-}-fromStrictMap = G.fromStrictMap----- | \(O(n)\) Map every element of a 'Series'.-map :: (a -> b) -> Series k a -> Series k b-{-# INLINE map #-}-map = G.map----- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.-mapWithKey :: (k -> a -> b) -> Series k a -> Series k b-{-# INLINE mapWithKey #-}-mapWithKey = G.mapWithKey----- | \(O(n \log n)\).--- Map each key in the index to another value. Note that the resulting series--- may have less elements, because each key must be unique.------ In case new keys are conflicting, the first element is kept.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> import qualified Data.List--- >>> xs `mapIndex` (Data.List.take 1)--- index | values--- ----- | --------- "L" | 4--- "P" | 1-mapIndex :: (Ord k, Ord g) => Series k a -> (k -> g) -> Series g a-{-# INLINE mapIndex #-}-mapIndex = G.mapIndex----- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.-concatMap :: Ord k - => (a -> Series k b) - -> Series k a - -> Series k b-{-# INLINE concatMap #-}-concatMap = G.concatMap----- | \(O(n)\) Apply the monadic action to every element of a series and its--- index, yielding a series of results.-mapWithKeyM :: (Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b)-{-# INLINE mapWithKeyM #-}-mapWithKeyM = G.mapWithKeyM----- | \(O(n)\) Apply the monadic action to every element of a series and its--- index, discarding the results.-mapWithKeyM_ :: Monad m => (k -> a -> m b) -> Series k a -> m ()-{-# INLINE mapWithKeyM_ #-}-mapWithKeyM_ = G.mapWithKeyM_----- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, --- yielding a series of results.-forWithKeyM :: (Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b)-{-# INLINE forWithKeyM #-}-forWithKeyM = G.forWithKeyM----- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, --- discarding the results.-forWithKeyM_ :: Monad m => Series k a -> (k -> a -> m b) -> m ()-{-# INLINE forWithKeyM_ #-}-forWithKeyM_ = G.forWithKeyM_----- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. -traverseWithKey :: (Applicative t, Ord k)- => (k -> a -> t b) - -> Series k a - -> t (Series k b)-{-# INLINE traverseWithKey #-}-traverseWithKey = G.traverseWithKey----- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5--- >>> take 2 xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2-take :: Int -> Series k a -> Series k a-{-# INLINE take #-}-take = G.take----- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5---- >>> takeWhile (>1) xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2-takeWhile :: (a -> Bool) -> Series k a -> Series k a-takeWhile = G.takeWhile----- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5--- >>> drop 2 xs--- index | values--- ----- | --------- "Paris" | 1--- "Vienna" | 5-drop :: Int -> Series k a -> Series k a-{-# INLINE drop #-}-drop = G.drop----- | \(O(n)\) Returns the complement of `takeWhile`.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5---- >>> dropWhile (>1) xs--- index | values--- ----- | --------- "Paris" | 1--- "Vienna" | 5-dropWhile :: (a -> Bool) -> Series k a -> Series k a-dropWhile = G.dropWhile----- | Apply a function elementwise to two series, matching elements--- based on their keys. For keys present only in the left or right series, --- the value 'Nothing' is returned.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWith (+) xs ys--- index | values--- ----- | --------- "alpha" | Just 10--- "beta" | Just 12--- "delta" | Nothing--- "gamma" | Nothing------ To only combine elements where keys are in both series, see 'zipWithMatched'.-zipWith :: (Ord k) - => (a -> b -> c) -> Series k a -> Series k b -> Series k (Maybe c)-zipWith = G.zipWith -{-# INLINE zipWith #-}------ | Apply a function elementwise to three series, matching elements--- based on their keys. For keys present only in the left or right series, --- the value 'Nothing' is returned.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]--- >>> zipWith3 (\x y z -> x + y + z) xs ys zs--- index | values--- ----- | --------- "alpha" | Just 30--- "beta" | Nothing--- "delta" | Nothing--- "epsilon" | Nothing--- "gamma" | Nothing------ To only combine elements where keys are in all series, see 'zipWithMatched3'-zipWith3 :: (Ord k) - => (a -> b -> c -> d) - -> Series k a - -> Series k b - -> Series k c - -> Series k (Maybe d)-{-# INLINE zipWith3 #-}-zipWith3 = G.zipWith3----- | Apply a function elementwise to two series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWithMatched (+) xs ys--- index | values--- ----- | --------- "alpha" | 10--- "beta" | 12------ To combine elements where keys are in either series, see 'zipWith'.-zipWithMatched :: Ord k => (a -> b -> c) -> Series k a -> Series k b -> Series k c-{-# INLINE zipWithMatched #-}-zipWithMatched = G.zipWithMatched----- | Apply a function elementwise to three series, matching elements--- based on their keys. Keys not present in all three series are dropped.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]--- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs--- index | values--- ----- | --------- "alpha" | 30-zipWithMatched3 :: (Ord k) - => (a -> b -> c -> d) - -> Series k a - -> Series k b - -> Series k c- -> Series k d-{-# INLINE zipWithMatched3 #-}-zipWithMatched3 = G.zipWithMatched3----- | Apply a function elementwise to two series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.------ To combine elements where keys are in either series, see 'zipWith'-zipWithKey :: (Ord k) - => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c-{-# INLINE zipWithKey #-}-zipWithKey = G.zipWithKey----- | Apply a function elementwise to three series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.------ To combine elements where keys are in any series, see 'zipWith3'-zipWithKey3 :: (Ord k) - => (k -> a -> b -> c -> d) - -> Series k a - -> Series k b - -> Series k c- -> Series k d-{-# INLINE zipWithKey3 #-}-zipWithKey3 = G.zipWithKey3----- | Zip two 'Series' with a combining function, applying a `ZipStrategy` when one key is present in one of the 'Series' but not both.------ In the example below, we want to set the value to @-100@ (via @`constStrategy` (-100)@) for keys which are only present --- in the left 'Series', and drop keys (via `skipStrategy`) which are only present in the `right 'Series' ------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy xs ys--- index | values--- ----- | --------- "alpha" | 10--- "beta" | 12--- "gamma" | -100------ Note that if you want to drop keys missing in either 'Series', it is faster to use @`zipWithMatched` f@ --- than using @`zipWithStrategy` f skipStrategy skipStrategy@.-zipWithStrategy :: (Ord k) - => (a -> b -> c) -- ^ Function to combine values when present in both series- -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right- -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left- -> Series k a- -> Series k b - -> Series k c-{-# INLINE zipWithStrategy #-}-zipWithStrategy = G.zipWithStrategy----- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is --- present in one of the 'Series' but not all of the others.------ Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ --- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.-zipWithStrategy3 :: (Ord k) - => (a -> b -> c -> d) -- ^ Function to combine values when present in all series- -> ZipStrategy k a d -- ^ Strategy for when the key is in the left series but not in all the others- -> ZipStrategy k b d -- ^ Strategy for when the key is in the center series but not in all the others- -> ZipStrategy k c d -- ^ Strategy for when the key is in the right series but not in all the others- -> Series k a- -> Series k b - -> Series k c- -> Series k d-{-# INLINE zipWithStrategy3 #-}-zipWithStrategy3 = G.zipWithStrategy3----- | Zip two 'Series' with a combining function. The value for keys which are missing from--- either 'Series' is replaced with the appropriate `mempty` value.------ >>> import Data.Monoid ( Sum(..) )--- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]--- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]--- >>> Series.zipWith (<>) xs ys--- index | values--- ----- | --------- "2023-01-01" | Just (Sum {getSum = 6})--- "2023-01-02" | Nothing--- "2023-01-03" | Nothing--- >>> zipWithMonoid (<>) xs ys--- index | values--- ----- | --------- "2023-01-01" | Sum {getSum = 6}--- "2023-01-02" | Sum {getSum = 2}--- "2023-01-03" | Sum {getSum = 7}-zipWithMonoid :: ( Monoid a, Monoid b, Ord k) - => (a -> b -> c)- -> Series k a- -> Series k b - -> Series k c-zipWithMonoid = G.zipWithMonoid-{-# INLINE zipWithMonoid #-}----- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. ------ >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]--- >>> xs `esum` ys--- index | values--- ----- | --------- "2023-01-01" | 6--- "2023-01-02" | 2--- "2023-01-03" | 7-esum :: (Ord k, Num a) - => Series k a - -> Series k a- -> Series k a-esum = G.esum-{-# INLINE esum #-}----- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. ------ >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]--- >>> xs `eproduct` ys--- index | values--- ----- | --------- "2023-01-01" | 10--- "2023-01-02" | 3--- "2023-01-03" | 7-eproduct :: (Ord k, Num a) - => Series k a - -> Series k a- -> Series k a-eproduct = G.eproduct-{-# INLINE eproduct #-}----- | \(O(n)\) Unzip a 'Series' of 2-tuples.-unzip :: Series k (a, b)- -> ( Series k a- , Series k b- )-unzip = G.unzip-{-# INLINE unzip #-}----- | \(O(n)\) Unzip a 'Series' of 3-tuples.-unzip3 :: Series k (a, b, c)- -> ( Series k a- , Series k b- , Series k c- )-unzip3 = G.unzip3-{-# INLINE unzip3 #-}----- | Require a series to have a specific `Index`.--- Contrary to @select@, all keys in the `Index` will be present in the resulting series.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> xs `require` Index.fromList ["Paris", "Lisbon", "Taipei"]--- index | values--- ----- | --------- "Lisbon" | Just 4--- "Paris" | Just 1--- "Taipei" | Nothing-require :: Ord k => Series k a -> Index k -> Series k (Maybe a)-{-# INLINE require #-}-require = G.require ----- | Drop the index of a series by replacing it with an `Int`-based index. Values will--- be indexed from 0.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> dropIndex xs--- index | values--- ----- | --------- 0 | 4--- 1 | 2--- 2 | 1-dropIndex :: Series k a -> Series Int a-{-# INLINE dropIndex #-}-dropIndex = G.dropIndex----- | Filter elements. Only elements for which the predicate is @True@ are kept. --- Notice that the filtering is done on the values, not on the keys.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> filter (>2) xs--- index | values--- ----- | --------- "Lisbon" | 4------ See also 'filterWithKey'.-filter :: Ord k => (a -> Bool) -> Series k a -> Series k a-{-# INLINE filter #-}-filter = G.filter----- | Filter elements, taking into account the corresponding key. Only elements for which --- the predicate is @True@ are kept. -filterWithKey :: Ord k - => (k -> a -> Bool) - -> Series k a - -> Series k a-{-# INLINE filterWithKey #-}-filterWithKey = G.filterWithKey----- | Drop elements which are not available (NA). ------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> let ys = xs `require` Index.fromList ["Paris", "London", "Lisbon", "Toronto"]--- >>> ys--- index | values--- ----- | --------- "Lisbon" | Just 4--- "London" | Just 2--- "Paris" | Just 1--- "Toronto" | Nothing--- >>> catMaybes ys--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1-catMaybes :: Ord k => Series k (Maybe a) -> Series k a-{-# INLINE catMaybes #-}-catMaybes = G.catMaybes----- | Select a subseries. There are a few ways to do this.------ The first way to do this is to select a sub-series based on random keys. For example,--- selecting a subseries from an `Index`:------ >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]--- >>> xs `select` Index.fromList ['a', 'd']--- index | values--- ----- | --------- 'a' | 10--- 'd' | 40------ The second way to select a sub-series is to select all keys in a range:------ >>> xs `select` 'b' `to` 'c'--- index | values--- ----- | --------- 'b' | 20--- 'c' | 30------ Note that with `select`, you'll always get a sub-series; if you ask for a key which is not--- in the series, it'll be ignored:------ >>> xs `select` Index.fromList ['a', 'd', 'e']--- index | values--- ----- | --------- 'a' | 10--- 'd' | 40------ See `require` if you want to ensure that all keys are present.-select :: (Selection s, Ord k) => Series k a -> s k -> Series k a-select = G.select----- | Select a sub-series from a series matching a condition.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> xs `selectWhere` (fmap (>1) xs)--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2-selectWhere :: Ord k => Series k a -> Series k Bool -> Series k a-{-# INLINE selectWhere #-}-selectWhere = G.selectWhere----- | \(O(\log n)\). Extract a single value from a series, by key.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs `at` "Paris"--- Just 1--- >>> xs `at` "Sydney"--- Nothing-at :: Ord k => Series k a -> k -> Maybe a-{-# INLINE at #-}-at = G.at----- | \(O(1)\). Extract a single value from a series, by index.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> xs `iat` 0--- Just 4--- >>> xs `iat` 3--- Nothing-iat :: Series k a -> Int -> Maybe a-{-# INLINE iat #-}-iat = G.iat----- | Replace values in the right series from values in the left series at matching keys.--- Keys not in the right series are unaffected.--- --- See `(|->)` and `(<-|)`, which might be more readable.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> let ys = Series.singleton "Paris" (99::Int)--- >>> ys `replace` xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 99-replace :: Ord k => Series k a -> Series k a -> Series k a-{-# INLINE replace #-}-replace = G.replace----- | Replace values in the right series from values in the left series at matching keys.--- Keys not in the right series are unaffected.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> let ys = Series.singleton "Paris" (99::Int)--- >>> ys |-> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 99-(|->) :: (Ord k) => Series k a -> Series k a -> Series k a-{-# INLINE (|->) #-}-(|->) = (G.|->)----- | Replace values in the left series from values in the right series at matching keys.--- Keys not in the left series are unaffected.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> let ys = Series.singleton "Paris" (99::Int)--- >>> xs <-| ys--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 99-(<-|) :: (Ord k) => Series k a -> Series k a -> Series k a-{-# INLINE (<-|) #-}-(<-|) = (G.<-|)----- | \(O(n)\) Replace all instances of 'Nothing' with the last previous--- value which was not 'Nothing'.------ >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int)--- >>> xs--- index | values--- ----- | --------- 0 | Just 1--- 1 | Just 2--- 2 | Nothing--- 3 | Just 3--- >>> forwardFill 0 xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 2--- 3 | 3------ If the first entry of the series is missing, the first input to 'forwardFill' will be used:------ >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int)--- >>> ys--- index | values--- ----- | --------- 0 | Nothing--- 1 | Just 2--- 2 | Nothing--- 3 | Just 3--- >>> forwardFill 0 ys--- index | values--- ----- | --------- 0 | 0--- 1 | 2--- 2 | 2--- 3 | 3-forwardFill :: a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value.- -> Series v (Maybe a)- -> Series v a-{-# INLINE forwardFill #-}-forwardFill = G.forwardFill----- | \(O(n)\) Execute a 'Fold' over a 'Series'.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double--- >>> xs--- index | values--- ----- | --------- 0 | 1.0--- 1 | 2.0--- 2 | 3.0--- 3 | 4.0--- >>> import Control.Foldl (variance)--- >>> fold variance xs--- 1.25------ See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into--- account while folding.-fold :: Fold a b -> Series k a -> b-fold = G.fold-{-# INLINE fold #-}----- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.------ See also 'fold' for pure folds, and 'foldMWithKey' to take keys into--- account while folding.-foldM :: (Monad m) - => FoldM m a b - -> Series k a - -> m b-foldM = G.foldM-{-# INLINE foldM #-}----- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account.-foldWithKey :: Fold (k, a) b -> Series k a -> b-foldWithKey = G.foldWithKey-{-# INLINE foldWithKey #-}----- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.-foldMWithKey :: (Monad m) - => FoldM m (k, a) b - -> Series k a - -> m b-foldMWithKey = G.foldMWithKey-{-# INLINE foldMWithKey #-}----- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine--- the results.-foldMapWithKey :: Monoid m => (k -> a -> m) -> Series k a -> m-{-# INLINE foldMapWithKey #-}-foldMapWithKey = G.foldMapWithKey----- | 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 'aggregateWith':--- --- >>> 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 k a -- ^ Grouping function- ->(k -> g) -- ^ Input series- -> Grouping k g a -- ^ Grouped series-{-# INLINE groupBy #-}-groupBy = G.groupBy---- | Representation of a 'Series' being grouped.-type Grouping k g a = G.Grouping k g Vector a----- | 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) - => Grouping k g a - -> (Series k a -> b) - -> Series g b-{-# INLINE aggregateWith #-}-aggregateWith = G.aggregateWith----- | Aggregate 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.-foldWith :: Ord g - => Grouping k g a- -> (a -> a -> a)- -> Series g a-{-# INLINE foldWith #-}-foldWith = G.foldWith----- | 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 :: Series k a -- ^ Series vector- -> (Series k a -> b) -- ^ Aggregation function- -> Series k b -- ^ Resulting vector-{-# INLINE expanding #-}-expanding = G.expanding----- | General-purpose 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 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- => (k -> Range k)- -> (Series k a -> b)- -> Series k a- -> Series k b-{-# INLINE windowing #-}-windowing = G.windowing----- | \(O(1)\) Test whether a 'Series' is empty.-null :: Series k a -> Bool-{-# INLINE null #-}-null = G.null----- |\(O(1)\) Extract the length of a 'Series'.-length :: Series k a -> Int-{-# INLINE length #-}-length = G.length----- | \(O(n)\) Check if all elements satisfy the predicate.-all :: (a -> Bool) -> Series k a -> Bool-{-# INLINE all #-}-all = G.all----- | \(O(n)\) Check if any element satisfies the predicate.-any :: (a -> Bool) -> Series k a -> Bool-{-# INLINE any #-}-any = G.any----- | \(O(n)\) Check if all elements are 'True'.-and :: Series k Bool -> Bool-{-# INLINE and #-}-and = G.and----- | \(O(n)\) Check if any element is 'True'.-or :: Series k Bool -> Bool-{-# INLINE or #-}-or = G.or----- | \(O(n)\) Compute the sum of the elements.-sum :: (Num a) => Series k a -> a-{-# INLINE sum #-}-sum = G.sum----- | \(O(n)\) Compute the product of the elements.-product :: (Num a) => Series k a -> a-{-# INLINE product #-}-product = G.product----- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.--- If the 'Series' is empty, @Nothing@ is returned.------ See also 'argmax'.-maximum :: (Ord a) => Series k a -> Maybe a-{-# INLINE maximum #-}-maximum = G.maximum----- | \(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) => (a -> b) -> Series k a -> Maybe a-{-# INLINE maximumOn #-}-maximumOn = G.maximumOn----- | \(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.------ See also 'argmin'.-minimum :: (Ord a) => Series k a -> Maybe a-{-# INLINE minimum #-}-minimum = G.minimum----- | \(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) => (a -> b) -> Series k a -> Maybe a-{-# INLINE minimumOn #-}-minimumOn = G.minimumOn----- | \(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.------ >>> :{ --- let (xs :: 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 => Series k a -> Maybe k-argmax = G.argmax-{-# INLINE argmax #-}----- | \(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.--- >>> :{ --- let (xs :: 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 => Series k a -> Maybe k-argmin = G.argmin-{-# INLINE argmin #-}----- | \(O(n)\) Left-to-right postscan.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int--- >>> xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- 3 | 4--- >>> postscanl (+) 0 xs--- index | values--- ----- | --------- 0 | 1--- 1 | 3--- 2 | 6--- 3 | 10-postscanl :: (a -> b -> a) -> a -> Series k b -> Series k a-{-# INLINE postscanl #-}-postscanl = G.postscanl----- | \(O(n)\) Left-to-right prescan.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int--- >>> xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- 3 | 4--- >>> prescanl (+) 0 xs--- index | values--- ----- | --------- 0 | 0--- 1 | 1--- 2 | 3--- 3 | 6-prescanl :: (a -> b -> a) -> a -> Series k b -> Series k a-{-# INLINE prescanl #-}-prescanl = G.prescanl----- | Display a 'Series' using default 'DisplayOptions'.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int--- >>> putStrLn $ display xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- ... | ...--- 4 | 5--- 5 | 6--- 6 | 7-display :: (Show k, Show a) - => Series k a - -> String-display = G.display----- | Display a 'Series' using customizable 'DisplayOptions'.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int--- >>> import Data.List (replicate)--- >>> :{--- let opts = DisplayOptions { maximumNumberOfRows = 4--- , indexHeader = "keys"--- , valuesHeader = "vals"--- , keyDisplayFunction = (\i -> replicate i 'x') `noLongerThan` 5--- , valueDisplayFunction = (\i -> replicate i 'o') --- }--- in putStrLn $ displayWith opts xs--- :}--- keys | vals--- ----- | --------- | o--- x | oo--- ... | ...--- xxxxx | oooooo--- xxx... | ooooooo-displayWith :: DisplayOptions k a- -> Series k a - -> String+----------------------------------------------------------------------------- +-- | +-- Module : Data.Series +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT +-- Maintainer : laurent.decotret@outlook.com +-- Portability : portable +-- +-- This module contains data structures and functions to work with 'Series' capable of holding any Haskell value. +-- For better performance, at the cost of less flexibility, see the "Data.Series.Unboxed". +-- +-- = Introduction to series +-- +-- A 'Series' of type @Series k a@ is a labeled array of values of type @a@, +-- indexed by keys of type @k@. +-- +-- Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient: +-- +-- * random access by key ( \(O(\log n)\) ); +-- * slice by key ( \(O(\log n)\) ). +-- +-- Like `Data.Vector.Vector`, they support efficient: +-- +-- * random access by index ( \(O(1)\) ); +-- * slice by index ( \(O(1)\) ); +-- * numerical operations. +-- +-- This module re-exports most of the content of "Data.Series.Generic", with type signatures +-- specialized to the boxed container type `Data.Vector.Vector`. +-- +-- For better performance (at the cost of more constraints), especially when it comes to numerical calculations, prefer to +-- use "Data.Series.Unboxed", which contains an implementation of series specialized to the unboxed container type `Data.Vector.Unboxed.Vector`. + +module Data.Series ( + Series, index, values, + + -- * Building/converting 'Series' + singleton, fromIndex, + -- ** Lists + fromList, toList, + -- ** Vectors + fromVector, toVector, + -- ** Handling duplicates + Occurrence, fromListDuplicates, fromVectorDuplicates, + -- ** Strict Maps + fromStrictMap, toStrictMap, + -- ** Lazy Maps + fromLazyMap, toLazyMap, + -- ** Ad-hoc conversion with other data structures + IsSeries(..), + -- ** Conversion between 'Series' types + G.convert, + + -- * Mapping and filtering + map, mapWithKey, mapIndex, concatMap, + take, takeWhile, drop, dropWhile, filter, filterWithKey, + -- ** Mapping with effects + mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey, + + -- * Combining series + zipWith, zipWithMatched, zipWithKey, + zipWith3, zipWithMatched3, zipWithKey3, + ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3, + zipWithMonoid, esum, eproduct, unzip, unzip3, + + -- * Index manipulation + require, catMaybes, dropIndex, + + -- * Accessors + -- ** Bulk access + select, selectWhere, Range, to, from, upto, Selection, + -- ** Single-element access + at, iat, + + -- * Replacing values + replace, (|->), (<-|), + + -- * Scans + forwardFill, + + -- * Grouping and windowing operations + groupBy, Grouping, aggregateWith, foldWith, + windowing, expanding, + + -- * Folds + fold, foldM, foldWithKey, foldMWithKey, foldMapWithKey, + -- ** Specialized folds + G.mean, G.variance, G.std, + length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn, + argmin, argmax, + + -- * Scans + postscanl, prescanl, + + -- * Displaying 'Series' + display, displayWith, + noLongerThan, + DisplayOptions(..), G.defaultDisplayOptions +) where + +import Control.Foldl ( Fold, FoldM ) +import qualified Data.Map.Lazy as ML +import qualified Data.Map.Strict as MS +import Data.Series.Index ( Index ) +import Data.Series.Generic ( IsSeries(..), Range, Selection, ZipStrategy, Occurrence, DisplayOptions(..) + , to, from, upto, skipStrategy, mapStrategy, constStrategy, noLongerThan + ) +import qualified Data.Series.Generic as G +import Data.Vector ( Vector ) + +import Prelude hiding ( map, concatMap, zipWith, zipWith3, filter, take, takeWhile, drop, dropWhile, last, unzip, unzip3 + , length, null, all, any, and, or, sum, product, maximum, minimum, + ) + +-- $setup +-- >>> import qualified Data.Series as Series +-- >>> import qualified Data.Series.Index as Index + +infixl 1 `select` +infix 6 |->, <-| + +-- | A series is a labeled array of values of type @a@, +-- indexed by keys of type @k@. +-- +-- Like @Data.Map@ and @Data.HashMap@, they support efficient: +-- +-- * random access by key ( \(O(\log n)\) ); +-- * slice by key ( \(O(\log n)\) ). +-- +-- Like @Data.Vector.Vector@, they support efficient: +-- +-- * random access by index ( \(O(1)\) ); +-- * slice by index ( \(O(1)\) ); +-- * numerical operations. +type Series = G.Series Vector + + +index :: Series k a -> Index k +{-# INLINABLE index #-} +index = G.index + + +values :: Series k a -> Vector a +{-# INLINABLE values #-} +values = G.values + + +-- | Create a 'Series' with a single element. +singleton :: k -> a -> Series k a +{-# INLINABLE singleton #-} +singleton = G.singleton + + +-- | \(O(n)\) Generate a 'Series' by mapping every element of its index. +-- +-- >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d'] +-- index | values +-- ----- | ------ +-- 'a' | 0 +-- 'b' | 0 +-- 'c' | 0 +-- 'd' | 0 +fromIndex :: (k -> a) -> Index k -> Series k a +{-# INLINABLE fromIndex #-} +fromIndex = G.fromIndex + + +-- | Construct a series from a list of key-value pairs. There is no +-- condition on the order of pairs. +-- +-- >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- 'a' | 5 +-- 'b' | 0 +-- 'd' | 1 +-- +-- If you need to handle duplicate keys, take a look at `fromListDuplicates`. +fromList :: Ord k => [(k, a)] -> Series k a +{-# INLINABLE fromList #-} +fromList = G.fromList + + +-- | Construct a series from a list of key-value pairs. +-- Contrary to `fromList`, values at duplicate keys are preserved. To keep each +-- key unique, an `Occurrence` number counts up. +-- +-- >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- ('a',0) | 5 +-- ('b',0) | 0 +-- ('d',0) | 1 +-- ('d',1) | -4 +-- ('d',2) | 7 +fromListDuplicates :: Ord k => [(k, a)] -> Series (k, Occurrence) a +{-# INLINABLE fromListDuplicates #-} +fromListDuplicates = G.fromListDuplicates + + +-- | Construct a list from key-value pairs. The elements are in order sorted by key: +-- +-- >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- 'a' | 5 +-- 'b' | 0 +-- 'd' | 1 +-- >>> toList xs +-- [('a',5),('b',0),('d',1)] +toList :: Series k a -> [(k, a)] +{-# INLINABLE toList #-} +toList = G.toList + + +-- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. +toVector :: Series k a -> Vector (k, a) +{-# INLINABLE toVector #-} +toVector = G.toVector + + +-- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no +-- condition on the order of pairs. Duplicate keys are silently dropped. If you +-- need to handle duplicate keys, see 'fromVectorDuplicates'. +-- +-- Note that due to differences in sorting, +-- @'Series.fromList'@ and @'Series.fromVector' . 'Vector.fromList'@ +-- may not be equivalent if the input list contains duplicate keys. +fromVector :: Ord k => Vector (k, a) -> Series k a +{-# INLINABLE fromVector #-} +fromVector = G.fromVector + + +-- | Construct a series from a 'Vector' of key-value pairs. +-- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each +-- key unique, an 'Occurrence' number counts up. +-- +-- >>> import qualified Data.Vector as Vector +-- >>> let xs = fromVectorDuplicates $ Vector.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- ('a',0) | 5 +-- ('b',0) | 0 +-- ('d',0) | 1 +-- ('d',1) | -4 +-- ('d',2) | 7 +fromVectorDuplicates :: Ord k => Vector (k, a) -> Series (k, Occurrence) a +{-# INLINABLE fromVectorDuplicates #-} +fromVectorDuplicates = G.fromVectorDuplicates + + +-- | Convert a series into a lazy @Map@. +toLazyMap :: Series k a -> ML.Map k a +{-# INLINABLE toLazyMap #-} +toLazyMap = G.toLazyMap + + +-- | Construct a series from a lazy @Map@. +fromLazyMap :: ML.Map k a -> Series k a +{-# INLINABLE fromLazyMap #-} +fromLazyMap = G.fromLazyMap + + +-- | Convert a series into a strict @Map@. +toStrictMap :: Series k a -> MS.Map k a +{-# INLINABLE toStrictMap #-} +toStrictMap = G.toStrictMap + + +-- | Construct a series from a strict @Map@. +fromStrictMap :: MS.Map k a -> Series k a +{-# INLINABLE fromStrictMap #-} +fromStrictMap = G.fromStrictMap + + +-- | \(O(n)\) Map every element of a 'Series'. +map :: (a -> b) -> Series k a -> Series k b +{-# INLINABLE map #-} +map = G.map + + +-- | \(O(n)\) Map every element of a 'Series', possibly using the key as well. +mapWithKey :: (k -> a -> b) -> Series k a -> Series k b +{-# INLINABLE mapWithKey #-} +mapWithKey = G.mapWithKey + + +-- | \(O(n \log n)\). +-- Map each key in the index to another value. Note that the resulting series +-- may have less elements, because each key must be unique. +-- +-- In case new keys are conflicting, the first element is kept. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> import qualified Data.List +-- >>> xs `mapIndex` (Data.List.take 1) +-- index | values +-- ----- | ------ +-- "L" | 4 +-- "P" | 1 +mapIndex :: (Ord k, Ord g) => Series k a -> (k -> g) -> Series g a +{-# INLINABLE mapIndex #-} +mapIndex = G.mapIndex + + +-- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'. +concatMap :: Ord k + => (a -> Series k b) + -> Series k a + -> Series k b +{-# INLINABLE concatMap #-} +concatMap = G.concatMap + + +-- | \(O(n)\) Apply the monadic action to every element of a series and its +-- index, yielding a series of results. +mapWithKeyM :: (Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b) +{-# INLINABLE mapWithKeyM #-} +mapWithKeyM = G.mapWithKeyM + + +-- | \(O(n)\) Apply the monadic action to every element of a series and its +-- index, discarding the results. +mapWithKeyM_ :: Monad m => (k -> a -> m b) -> Series k a -> m () +{-# INLINABLE mapWithKeyM_ #-} +mapWithKeyM_ = G.mapWithKeyM_ + + +-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, +-- yielding a series of results. +forWithKeyM :: (Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b) +{-# INLINABLE forWithKeyM #-} +forWithKeyM = G.forWithKeyM + + +-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, +-- discarding the results. +forWithKeyM_ :: Monad m => Series k a -> (k -> a -> m b) -> m () +{-# INLINABLE forWithKeyM_ #-} +forWithKeyM_ = G.forWithKeyM_ + + +-- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. +traverseWithKey :: (Applicative t, Ord k) + => (k -> a -> t b) + -> Series k a + -> t (Series k b) +{-# INLINABLE traverseWithKey #-} +traverseWithKey = G.traverseWithKey + + +-- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 +-- >>> take 2 xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +take :: Int -> Series k a -> Series k a +{-# INLINABLE take #-} +take = G.take + + +-- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 + +-- >>> takeWhile (>1) xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +takeWhile :: (a -> Bool) -> Series k a -> Series k a +takeWhile = G.takeWhile + + +-- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 +-- >>> drop 2 xs +-- index | values +-- ----- | ------ +-- "Paris" | 1 +-- "Vienna" | 5 +drop :: Int -> Series k a -> Series k a +{-# INLINABLE drop #-} +drop = G.drop + + +-- | \(O(n)\) Returns the complement of `takeWhile`. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 + +-- >>> dropWhile (>1) xs +-- index | values +-- ----- | ------ +-- "Paris" | 1 +-- "Vienna" | 5 +dropWhile :: (a -> Bool) -> Series k a -> Series k a +dropWhile = G.dropWhile + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. For keys present only in the left or right series, +-- the value 'Nothing' is returned. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWith (+) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | Just 10 +-- "beta" | Just 12 +-- "delta" | Nothing +-- "gamma" | Nothing +-- +-- To only combine elements where keys are in both series, see 'zipWithMatched'. +zipWith :: (Ord k) + => (a -> b -> c) -> Series k a -> Series k b -> Series k (Maybe c) +zipWith = G.zipWith +{-# INLINABLE zipWith #-} + + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. For keys present only in the left or right series, +-- the value 'Nothing' is returned. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ] +-- >>> zipWith3 (\x y z -> x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- "alpha" | Just 30 +-- "beta" | Nothing +-- "delta" | Nothing +-- "epsilon" | Nothing +-- "gamma" | Nothing +-- +-- To only combine elements where keys are in all series, see 'zipWithMatched3' +zipWith3 :: (Ord k) + => (a -> b -> c -> d) + -> Series k a + -> Series k b + -> Series k c + -> Series k (Maybe d) +{-# INLINABLE zipWith3 #-} +zipWith3 = G.zipWith3 + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWithMatched (+) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 10 +-- "beta" | 12 +-- +-- To combine elements where keys are in either series, see 'zipWith'. +zipWithMatched :: Ord k => (a -> b -> c) -> Series k a -> Series k b -> Series k c +{-# INLINABLE zipWithMatched #-} +zipWithMatched = G.zipWithMatched + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. Keys not present in all three series are dropped. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ] +-- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- "alpha" | 30 +zipWithMatched3 :: (Ord k) + => (a -> b -> c -> d) + -> Series k a + -> Series k b + -> Series k c + -> Series k d +{-# INLINABLE zipWithMatched3 #-} +zipWithMatched3 = G.zipWithMatched3 + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- To combine elements where keys are in either series, see 'zipWith' +zipWithKey :: (Ord k) + => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c +{-# INLINABLE zipWithKey #-} +zipWithKey = G.zipWithKey + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- To combine elements where keys are in any series, see 'zipWith3' +zipWithKey3 :: (Ord k) + => (k -> a -> b -> c -> d) + -> Series k a + -> Series k b + -> Series k c + -> Series k d +{-# INLINABLE zipWithKey3 #-} +zipWithKey3 = G.zipWithKey3 + + +-- | Zip two 'Series' with a combining function, applying a `ZipStrategy` when one key is present in one of the 'Series' but not both. +-- +-- In the example below, we want to set the value to @-100@ (via @`constStrategy` (-100)@) for keys which are only present +-- in the left 'Series', and drop keys (via `skipStrategy`) which are only present in the `right 'Series' +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 10 +-- "beta" | 12 +-- "gamma" | -100 +-- +-- Note that if you want to drop keys missing in either 'Series', it is faster to use @`zipWithMatched` f@ +-- than using @`zipWithStrategy` f skipStrategy skipStrategy@. +zipWithStrategy :: (Ord k) + => (a -> b -> c) -- ^ Function to combine values when present in both series + -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right + -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left + -> Series k a + -> Series k b + -> Series k c +{-# INLINABLE zipWithStrategy #-} +zipWithStrategy = G.zipWithStrategy + + +-- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is +-- present in one of the 'Series' but not all of the others. +-- +-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ +-- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@. +zipWithStrategy3 :: (Ord k) + => (a -> b -> c -> d) -- ^ Function to combine values when present in all series + -> ZipStrategy k a d -- ^ Strategy for when the key is in the left series but not in all the others + -> ZipStrategy k b d -- ^ Strategy for when the key is in the center series but not in all the others + -> ZipStrategy k c d -- ^ Strategy for when the key is in the right series but not in all the others + -> Series k a + -> Series k b + -> Series k c + -> Series k d +{-# INLINABLE zipWithStrategy3 #-} +zipWithStrategy3 = G.zipWithStrategy3 + + +-- | Zip two 'Series' with a combining function. The value for keys which are missing from +-- either 'Series' is replaced with the appropriate `mempty` value. +-- +-- >>> import Data.Monoid ( Sum(..) ) +-- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ] +-- >>> Series.zipWith (<>) xs ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | Just (Sum {getSum = 6}) +-- "2023-01-02" | Nothing +-- "2023-01-03" | Nothing +-- >>> zipWithMonoid (<>) xs ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | Sum {getSum = 6} +-- "2023-01-02" | Sum {getSum = 2} +-- "2023-01-03" | Sum {getSum = 7} +zipWithMonoid :: ( Monoid a, Monoid b, Ord k) + => (a -> b -> c) + -> Series k a + -> Series k b + -> Series k c +zipWithMonoid = G.zipWithMonoid +{-# INLINABLE zipWithMonoid #-} + + +-- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. +-- +-- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ] +-- >>> xs `esum` ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | 6 +-- "2023-01-02" | 2 +-- "2023-01-03" | 7 +esum :: (Ord k, Num a) + => Series k a + -> Series k a + -> Series k a +esum = G.esum +{-# INLINABLE esum #-} + + +-- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. +-- +-- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ] +-- >>> xs `eproduct` ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | 10 +-- "2023-01-02" | 3 +-- "2023-01-03" | 7 +eproduct :: (Ord k, Num a) + => Series k a + -> Series k a + -> Series k a +eproduct = G.eproduct +{-# INLINABLE eproduct #-} + + +-- | \(O(n)\) Unzip a 'Series' of 2-tuples. +unzip :: Series k (a, b) + -> ( Series k a + , Series k b + ) +unzip = G.unzip +{-# INLINABLE unzip #-} + + +-- | \(O(n)\) Unzip a 'Series' of 3-tuples. +unzip3 :: Series k (a, b, c) + -> ( Series k a + , Series k b + , Series k c + ) +unzip3 = G.unzip3 +{-# INLINABLE unzip3 #-} + + +-- | Require a series to have a specific `Index`. +-- Contrary to @select@, all keys in the `Index` will be present in the resulting series. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> xs `require` Index.fromList ["Paris", "Lisbon", "Taipei"] +-- index | values +-- ----- | ------ +-- "Lisbon" | Just 4 +-- "Paris" | Just 1 +-- "Taipei" | Nothing +require :: Ord k => Series k a -> Index k -> Series k (Maybe a) +{-# INLINABLE require #-} +require = G.require + + +-- | \(O(n)\) Drop the index of a series by replacing it with an `Int`-based index. Values will +-- be indexed from 0. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> dropIndex xs +-- index | values +-- ----- | ------ +-- 0 | 4 +-- 1 | 2 +-- 2 | 1 +dropIndex :: Series k a -> Series Int a +{-# INLINABLE dropIndex #-} +dropIndex = G.dropIndex + + +-- | Filter elements. Only elements for which the predicate is @True@ are kept. +-- Notice that the filtering is done on the values, not on the keys. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> filter (>2) xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- +-- See also 'filterWithKey'. +filter :: Ord k => (a -> Bool) -> Series k a -> Series k a +{-# INLINABLE filter #-} +filter = G.filter + + +-- | Filter elements, taking into account the corresponding key. Only elements for which +-- the predicate is @True@ are kept. +filterWithKey :: Ord k + => (k -> a -> Bool) + -> Series k a + -> Series k a +{-# INLINABLE filterWithKey #-} +filterWithKey = G.filterWithKey + + +-- | Drop elements which are not available (NA). +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> let ys = xs `require` Index.fromList ["Paris", "London", "Lisbon", "Toronto"] +-- >>> ys +-- index | values +-- ----- | ------ +-- "Lisbon" | Just 4 +-- "London" | Just 2 +-- "Paris" | Just 1 +-- "Toronto" | Nothing +-- >>> catMaybes ys +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +catMaybes :: Ord k => Series k (Maybe a) -> Series k a +{-# INLINABLE catMaybes #-} +catMaybes = G.catMaybes + + +-- | Select a subseries. There are a few ways to do this. +-- +-- The first way to do this is to select a sub-series based on random keys. For example, +-- selecting a subseries from an `Index`: +-- +-- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)] +-- >>> xs `select` Index.fromList ['a', 'd'] +-- index | values +-- ----- | ------ +-- 'a' | 10 +-- 'd' | 40 +-- +-- The second way to select a sub-series is to select all keys in a range: +-- +-- >>> xs `select` 'b' `to` 'c' +-- index | values +-- ----- | ------ +-- 'b' | 20 +-- 'c' | 30 +-- +-- Note that with `select`, you'll always get a sub-series; if you ask for a key which is not +-- in the series, it'll be ignored: +-- +-- >>> xs `select` Index.fromList ['a', 'd', 'e'] +-- index | values +-- ----- | ------ +-- 'a' | 10 +-- 'd' | 40 +-- +-- See `require` if you want to ensure that all keys are present. +select :: (Selection s, Ord k) => Series k a -> s k -> Series k a +select = G.select + + +-- | Select a sub-series from a series matching a condition. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> xs `selectWhere` (fmap (>1) xs) +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +selectWhere :: Ord k => Series k a -> Series k Bool -> Series k a +{-# INLINABLE selectWhere #-} +selectWhere = G.selectWhere + + +-- | \(O(\log n)\). Extract a single value from a series, by key. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs `at` "Paris" +-- Just 1 +-- >>> xs `at` "Sydney" +-- Nothing +at :: Ord k => Series k a -> k -> Maybe a +{-# INLINABLE at #-} +at = G.at + + +-- | \(O(1)\). Extract a single value from a series, by index. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> xs `iat` 0 +-- Just 4 +-- >>> xs `iat` 3 +-- Nothing +iat :: Series k a -> Int -> Maybe a +{-# INLINABLE iat #-} +iat = G.iat + + +-- | Replace values in the right series from values in the left series at matching keys. +-- Keys not in the right series are unaffected. +-- +-- See `(|->)` and `(<-|)`, which might be more readable. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> let ys = Series.singleton "Paris" (99::Int) +-- >>> ys `replace` xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 99 +replace :: Ord k => Series k a -> Series k a -> Series k a +{-# INLINABLE replace #-} +replace = G.replace + + +-- | Replace values in the right series from values in the left series at matching keys. +-- Keys not in the right series are unaffected. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> let ys = Series.singleton "Paris" (99::Int) +-- >>> ys |-> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 99 +(|->) :: (Ord k) => Series k a -> Series k a -> Series k a +{-# INLINABLE (|->) #-} +(|->) = (G.|->) + + +-- | Replace values in the left series from values in the right series at matching keys. +-- Keys not in the left series are unaffected. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> let ys = Series.singleton "Paris" (99::Int) +-- >>> xs <-| ys +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 99 +(<-|) :: (Ord k) => Series k a -> Series k a -> Series k a +{-# INLINABLE (<-|) #-} +(<-|) = (G.<-|) + + +-- | \(O(n)\) Replace all instances of 'Nothing' with the last previous +-- value which was not 'Nothing'. +-- +-- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int) +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | Just 1 +-- 1 | Just 2 +-- 2 | Nothing +-- 3 | Just 3 +-- >>> forwardFill 0 xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 2 +-- 3 | 3 +-- +-- If the first entry of the series is missing, the first input to 'forwardFill' will be used: +-- +-- >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series Int (Maybe Int) +-- >>> ys +-- index | values +-- ----- | ------ +-- 0 | Nothing +-- 1 | Just 2 +-- 2 | Nothing +-- 3 | Just 3 +-- >>> forwardFill 0 ys +-- index | values +-- ----- | ------ +-- 0 | 0 +-- 1 | 2 +-- 2 | 2 +-- 3 | 3 +forwardFill :: a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value. + -> Series v (Maybe a) + -> Series v a +{-# INLINABLE forwardFill #-} +forwardFill = G.forwardFill + + +-- | \(O(n)\) Execute a 'Fold' over a 'Series'. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1.0 +-- 1 | 2.0 +-- 2 | 3.0 +-- 3 | 4.0 +-- >>> import Control.Foldl (variance) +-- >>> fold variance xs +-- 1.25 +-- +-- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into +-- account while folding. +fold :: Fold a b -> Series k a -> b +fold = G.fold +{-# INLINABLE fold #-} + + +-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'. +-- +-- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into +-- account while folding. +foldM :: (Monad m) + => FoldM m a b + -> Series k a + -> m b +foldM = G.foldM +{-# INLINABLE foldM #-} + + +-- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account. +foldWithKey :: Fold (k, a) b -> Series k a -> b +foldWithKey = G.foldWithKey +{-# INLINABLE foldWithKey #-} + + +-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account. +foldMWithKey :: (Monad m) + => FoldM m (k, a) b + -> Series k a + -> m b +foldMWithKey = G.foldMWithKey +{-# INLINABLE foldMWithKey #-} + + +-- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine +-- the results. +foldMapWithKey :: Monoid m => (k -> a -> m) -> Series k a -> m +{-# INLINABLE foldMapWithKey #-} +foldMapWithKey = G.foldMapWithKey + + +-- | 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 'aggregateWith': +-- +-- >>> 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 k a -- ^ Grouping function + ->(k -> g) -- ^ Input series + -> Grouping k g a -- ^ Grouped series +{-# INLINABLE groupBy #-} +groupBy = G.groupBy + +-- | Representation of a 'Series' being grouped. +type Grouping k g a = G.Grouping k g Vector a + + +-- | 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) + => Grouping k g a + -> (Series k a -> b) + -> Series g b +{-# INLINABLE aggregateWith #-} +aggregateWith = G.aggregateWith + + +-- | Aggregate 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. +foldWith :: Ord g + => Grouping k g a + -> (a -> a -> a) + -> Series g a +{-# INLINABLE foldWith #-} +foldWith = G.foldWith + + +-- | 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 :: Series k a -- ^ Series vector + -> (Series k a -> b) -- ^ Aggregation function + -> Series k b -- ^ Resulting vector +{-# INLINABLE expanding #-} +expanding = G.expanding + + +-- | General-purpose 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 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 + => (k -> Range k) + -> (Series k a -> b) + -> Series k a + -> Series k b +{-# INLINABLE windowing #-} +windowing = G.windowing + + +-- | \(O(1)\) Test whether a 'Series' is empty. +null :: Series k a -> Bool +{-# INLINABLE null #-} +null = G.null + + +-- |\(O(1)\) Extract the length of a 'Series'. +length :: Series k a -> Int +{-# INLINABLE length #-} +length = G.length + + +-- | \(O(n)\) Check if all elements satisfy the predicate. +all :: (a -> Bool) -> Series k a -> Bool +{-# INLINABLE all #-} +all = G.all + + +-- | \(O(n)\) Check if any element satisfies the predicate. +any :: (a -> Bool) -> Series k a -> Bool +{-# INLINABLE any #-} +any = G.any + + +-- | \(O(n)\) Check if all elements are 'True'. +and :: Series k Bool -> Bool +{-# INLINABLE and #-} +and = G.and + + +-- | \(O(n)\) Check if any element is 'True'. +or :: Series k Bool -> Bool +{-# INLINABLE or #-} +or = G.or + + +-- | \(O(n)\) Compute the sum of the elements. +sum :: (Num a) => Series k a -> a +{-# INLINABLE sum #-} +sum = G.sum + + +-- | \(O(n)\) Compute the product of the elements. +product :: (Num a) => Series k a -> a +{-# INLINABLE product #-} +product = G.product + + +-- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins. +-- If the 'Series' is empty, @Nothing@ is returned. +-- +-- See also 'argmax'. +maximum :: (Ord a) => Series k a -> Maybe a +{-# INLINABLE maximum #-} +maximum = G.maximum + + +-- | \(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) => (a -> b) -> Series k a -> Maybe a +{-# INLINABLE maximumOn #-} +maximumOn = G.maximumOn + + +-- | \(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. +-- +-- See also 'argmin'. +minimum :: (Ord a) => Series k a -> Maybe a +{-# INLINABLE minimum #-} +minimum = G.minimum + + +-- | \(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) => (a -> b) -> Series k a -> Maybe a +{-# INLINABLE minimumOn #-} +minimumOn = G.minimumOn + + +-- | \(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. +-- +-- >>> :{ +-- let (xs :: 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 => Series k a -> Maybe k +argmax = G.argmax +{-# INLINABLE argmax #-} + + +-- | \(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. +-- >>> :{ +-- let (xs :: 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 => Series k a -> Maybe k +argmin = G.argmin +{-# INLINABLE argmin #-} + + +-- | \(O(n)\) Left-to-right postscan. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- 3 | 4 +-- >>> postscanl (+) 0 xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 3 +-- 2 | 6 +-- 3 | 10 +postscanl :: (a -> b -> a) -> a -> Series k b -> Series k a +{-# INLINABLE postscanl #-} +postscanl = G.postscanl + + +-- | \(O(n)\) Left-to-right prescan. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- 3 | 4 +-- >>> prescanl (+) 0 xs +-- index | values +-- ----- | ------ +-- 0 | 0 +-- 1 | 1 +-- 2 | 3 +-- 3 | 6 +prescanl :: (a -> b -> a) -> a -> Series k b -> Series k a +{-# INLINABLE prescanl #-} +prescanl = G.prescanl + + +-- | Display a 'Series' using default 'DisplayOptions'. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int +-- >>> putStrLn $ display xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- ... | ... +-- 4 | 5 +-- 5 | 6 +-- 6 | 7 +display :: (Show k, Show a) + => Series k a + -> String +display = G.display + + +-- | Display a 'Series' using customizable 'DisplayOptions'. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int +-- >>> import Data.List (replicate) +-- >>> :{ +-- let opts = DisplayOptions { maximumNumberOfRows = 4 +-- , indexHeader = "keys" +-- , valuesHeader = "vals" +-- , keyDisplayFunction = (\i -> replicate i 'x') `noLongerThan` 5 +-- , valueDisplayFunction = (\i -> replicate i 'o') +-- } +-- in putStrLn $ displayWith opts xs +-- :} +-- keys | vals +-- ----- | ------ +-- | o +-- x | oo +-- ... | ... +-- xxxxx | oooooo +-- xxx... | ooooooo +displayWith :: DisplayOptions k a + -> Series k a + -> String displayWith = G.displayWith
src/Data/Series/Generic.hs view
@@ -1,86 +1,98 @@-{-# LANGUAGE NoImplicitPrelude #-}-module Data.Series.Generic (- -- * Definition- Series(index, values),- convert,-- -- * Building/converting 'Series'- singleton, fromIndex,- -- ** Lists- fromList, toList,- -- ** Vectors- fromVector, toVector,- -- ** Handling duplicates- Occurrence, fromListDuplicates, fromVectorDuplicates,- -- ** Strict Maps- fromStrictMap, toStrictMap,- -- ** Lazy Maps- fromLazyMap, toLazyMap,- -- ** Ad-hoc conversion with other data structures- IsSeries(..),-- -- * Mapping and filtering- map, mapWithKey, mapIndex, concatMap, filter, filterWithKey, - take, takeWhile, drop, dropWhile,- -- ** Mapping with effects- mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey,-- -- * Folding- fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMapWithKey,- -- ** Specialized folds- mean, variance, std, - length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,- argmax, argmin,-- -- * Scans- postscanl, prescanl, forwardFill,-- -- * Combining series- zipWith, zipWithMatched, zipWithKey,- zipWith3, zipWithMatched3, zipWithKey3,- ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,- zipWithMonoid, esum, eproduct, unzip, unzip3,-- -- * Index manipulation- require, requireWith, catMaybes, dropIndex,-- -- * Accessors- -- ** Bulk access- select, selectWhere, Range, to, from, upto, Selection, - -- ** Single-element access- at, iat,-- -- * Replacement- replace, (|->), (<-|),-- -- * Grouping and windowing operations- groupBy, Grouping, aggregateWith, foldWith, - windowing, expanding,-- -- * Displaying 'Series'- display, displayWith,- noLongerThan,- DisplayOptions(..), defaultDisplayOptions-) where--import Data.Series.Generic.Aggregation ( groupBy, Grouping, aggregateWith, foldWith- , windowing, expanding, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn- , argmax, argmin,- )-import Data.Series.Generic.Definition ( Series(index, values), IsSeries(..), Occurrence, convert, singleton, fromIndex, fromStrictMap- , toStrictMap, fromLazyMap, toLazyMap, fromList, fromListDuplicates, toList- , fromVector, fromVectorDuplicates, toVector- , map, mapWithKey, mapIndex, concatMap, length, null, take, takeWhile, drop, dropWhile- , mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey, fold, foldM- , foldWithKey, foldMWithKey, foldMap, foldMapWithKey- , display, displayWith, noLongerThan, DisplayOptions(..), defaultDisplayOptions- )-import Data.Series.Generic.Numeric ( mean, variance, std )-import Data.Series.Generic.Scans ( postscanl, prescanl, forwardFill )-import Data.Series.Generic.View ( Range, Selection, at, iat, select, selectWhere, to, from, upto, filter, filterWithKey, require, requireWith- , catMaybes, dropIndex,- )-import Data.Series.Generic.Zip ( zipWith, zipWithMatched, zipWithKey, zipWith3, zipWithMatched3, zipWithKey3, replace- , (|->), (<-|), zipWithStrategy, zipWithStrategy3, ZipStrategy, skipStrategy, mapStrategy, constStrategy- , zipWithMonoid, esum, eproduct, unzip, unzip3- )+{-# LANGUAGE NoImplicitPrelude #-} +----------------------------------------------------------------------------- +-- | +-- Module : Data.Series.Generic +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT +-- Maintainer : laurent.decotret@outlook.com +-- Portability : portable +-- +-- This module contains data structures and functions to work with any type of 'Series', +-- including boxed and unboxed types. +-- +-- Use the definitions in this module if you want to support all types of 'Series' at once. +module Data.Series.Generic ( + -- * Definition + Series(index, values), + convert, + + -- * Building/converting 'Series' + singleton, fromIndex, + -- ** Lists + fromList, toList, + -- ** Vectors + fromVector, toVector, + -- ** Handling duplicates + Occurrence, fromListDuplicates, fromVectorDuplicates, + -- ** Strict Maps + fromStrictMap, toStrictMap, + -- ** Lazy Maps + fromLazyMap, toLazyMap, + -- ** Ad-hoc conversion with other data structures + IsSeries(..), + + -- * Mapping and filtering + map, mapWithKey, mapIndex, concatMap, filter, filterWithKey, + take, takeWhile, drop, dropWhile, + -- ** Mapping with effects + mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey, + + -- * Folding + fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMapWithKey, + -- ** Specialized folds + mean, variance, std, + length, null, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn, + argmax, argmin, + + -- * Scans + postscanl, prescanl, forwardFill, + + -- * Combining series + zipWith, zipWithMatched, zipWithKey, + zipWith3, zipWithMatched3, zipWithKey3, + ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3, + zipWithMonoid, esum, eproduct, unzip, unzip3, + + -- * Index manipulation + require, requireWith, catMaybes, dropIndex, + + -- * Accessors + -- ** Bulk access + select, selectWhere, Range, to, from, upto, Selection, + -- ** Single-element access + at, iat, + + -- * Replacement + replace, (|->), (<-|), + + -- * Grouping and windowing operations + groupBy, Grouping, aggregateWith, foldWith, + windowing, expanding, + + -- * Displaying 'Series' + display, displayWith, + noLongerThan, + DisplayOptions(..), defaultDisplayOptions +) where + +import Control.Foldl ( mean, variance, std ) +import Data.Series.Generic.Aggregation ( groupBy, Grouping, aggregateWith, foldWith + , windowing, expanding, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn + , argmax, argmin, + ) +import Data.Series.Generic.Definition ( Series(index, values), IsSeries(..), Occurrence, convert, singleton, fromIndex, fromStrictMap + , toStrictMap, fromLazyMap, toLazyMap, fromList, fromListDuplicates, toList + , fromVector, fromVectorDuplicates, toVector + , map, mapWithKey, mapIndex, concatMap, length, null, take, takeWhile, drop, dropWhile + , mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, traverseWithKey, fold, foldM + , foldWithKey, foldMWithKey, foldMap, foldMapWithKey + , display, displayWith, noLongerThan, DisplayOptions(..), defaultDisplayOptions + ) +import Data.Series.Generic.Scans ( postscanl, prescanl, forwardFill ) +import Data.Series.Generic.View ( Range, Selection, at, iat, select, selectWhere, to, from, upto, filter, filterWithKey, require, requireWith + , catMaybes, dropIndex, + ) +import Data.Series.Generic.Zip ( zipWith, zipWithMatched, zipWithKey, zipWith3, zipWithMatched3, zipWithKey3, replace + , (|->), (<-|), zipWithStrategy, zipWithStrategy3, ZipStrategy, skipStrategy, mapStrategy, constStrategy + , zipWithMonoid, esum, eproduct, unzip, unzip3 + )
src/Data/Series/Generic/Aggregation.hs view
@@ -1,326 +1,326 @@-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-{-# INLINE 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-{-# INLINE aggregateWith #-}-aggregateWith (MkGrouping xs by) f- = GSeries.fromStrictMap - $ 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 (<>) (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-{-# INLINE 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-{-# INLINE 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-{-# INLINE 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-{-# INLINE 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-{-# INLINE any #-}-any f = Vector.any f . values----- | \(O(n)\) Check if all elements are 'True'.-and :: Vector v Bool => Series v k Bool -> Bool-{-# INLINE and #-}-and = Vector.and . values----- | \(O(n)\) Check if any element is 'True'.-or :: Vector v Bool => Series v k Bool -> Bool-{-# INLINE or #-}-or = Vector.or . values----- | \(O(n)\) Compute the sum of the elements.-sum :: (Num a, Vector v a) => Series v k a -> a-{-# INLINE sum #-}-sum = Vector.sum . values----- | \(O(n)\) Compute the product of the elements.-product :: (Num a, Vector v a) => Series v k a -> a-{-# INLINE 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-{-# INLINE 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-{-# INLINE 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-{-# INLINE 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-{-# INLINE 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-{-# INLINE 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-{-# INLINE argmin #-}+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 + $ 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 (<>) (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
src/Data/Series/Generic/Definition.hs view
@@ -1,832 +1,832 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE QuantifiedConstraints #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}--module Data.Series.Generic.Definition ( - Series(..),-- convert,-- -- * Basic interface- singleton,- headM, lastM, map, mapWithKey, mapIndex, concatMap, fold, foldM, - foldWithKey, foldMWithKey, foldMap, bifoldMap, foldMapWithKey, - length, null, take, takeWhile, drop, dropWhile,- mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_,- traverseWithKey,-- fromIndex,- -- * Conversion to/from Series- IsSeries(..),- -- ** Conversion to/from Maps- fromStrictMap,- toStrictMap,- fromLazyMap,- toLazyMap,- -- ** Conversion to/from list- fromList,- toList,- -- *** Unsafe construction- fromDistinctAscList,- -- ** Conversion to/from vectors- fromVector,- toVector,- -- *** Unsafe construction- fromDistinctAscVector,- -- ** Handling duplicates- Occurrence, fromListDuplicates, fromVectorDuplicates,-- -- * Displaying 'Series'- display, displayWith,- noLongerThan,- DisplayOptions(..), defaultDisplayOptions-) where--import Control.DeepSeq ( NFData(rnf) )-import Control.Foldl ( Fold(..), FoldM(..) )-import Control.Monad.ST ( runST )-import Data.Bifoldable ( Bifoldable )-import qualified Data.Bifoldable as Bifoldable-import qualified Data.Foldable as Foldable-import Data.Foldable.WithIndex ( FoldableWithIndex(..))-import Data.Function ( on )-import Data.Functor.WithIndex ( FunctorWithIndex(imap) )--import Data.IntMap.Strict ( IntMap )-import qualified Data.IntMap.Strict as IntMap-import qualified Data.List as List-import qualified Data.Map.Lazy as ML-import Data.Map.Strict ( Map )-import qualified Data.Map.Strict as MS-import Data.Sequence ( Seq )-import qualified Data.Sequence as Seq-import Data.Semigroup ( Semigroup(..) )-import Data.Series.Index ( Index )-import qualified Data.Series.Index as Index-import qualified Data.Series.Index.Internal as Index.Internal-import Data.Set ( Set )-import qualified Data.Set as Set-import Data.Traversable.WithIndex ( TraversableWithIndex(..) )-import qualified Data.Vector as Boxed-import Data.Vector.Algorithms.Intro ( sortUniqBy, sortBy )-import Data.Vector.Generic ( Vector )-import qualified Data.Vector.Generic as Vector-import qualified Data.Vector.Generic.Mutable as GM-import qualified Data.Vector.Unboxed as U-import qualified Data.Vector.Unboxed.Mutable as UM- -import Prelude hiding ( take, takeWhile, drop, dropWhile, map, concatMap, foldMap, sum, length, null )-import qualified Prelude as P------ | A @Series v k a@ is a labeled array of type @v@ filled with values of type @a@,--- indexed by keys of type @k@.------ Like 'Data.Map.Strict.Map', they support efficient:------ * random access by key ( \(O(\log n)\) );--- * slice by key ( \(O(\log n)\) ).------ Like 'Data.Vector.Vector', they support efficient:------ * random access by index ( \(O(1)\) );--- * slice by index ( \(O(1)\) );--- * numerical operations.----data Series v k a - -- The reason the index is a set of keys is that we *want* keys to be ordered.- -- This allows for efficient slicing of the underlying values, because- -- if @k1 < k2@, then the values are also at indices @ix1 < ix2@.- = MkSeries { index :: Index k -- ^ The 'Index' of a series, which contains its (unique) keys in ascending order.- , values :: v a -- ^ The values of a series, in the order of its (unique) keys.- }----- | \(O(n)\) Convert between two types of 'Series'.-convert :: (Vector v1 a, Vector v2 a) => Series v1 k a -> Series v2 k a-{-# INLINE convert #-}-convert (MkSeries ix vs) = MkSeries ix $ Vector.convert vs ----- | \(O(1)\) Create a 'Series' with a single element.-singleton :: Vector v a => k -> a -> Series v k a-{-# INLINE singleton #-}-singleton k v = MkSeries (Index.singleton k) $ Vector.singleton v----- | \(O(n)\) Generate a 'Series' by mapping every element of its index.-fromIndex :: (Vector v a) - => (k -> a) -> Index k -> Series v k a-{-# INLINE fromIndex #-}-fromIndex f ix = MkSeries ix $ Vector.convert - $ Boxed.map f -- Using boxed vector to prevent a (Vector v k) constraint- $ Index.toAscVector ix----- | The 'IsSeries' typeclass allow for ad-hoc definition--- of conversion functions, converting to / from 'Series'.-class IsSeries t v k a where- -- | Construct a 'Series' from some container of key-values pairs. There is no- -- condition on the order of pairs. Duplicate keys are silently dropped. If you- -- need to handle duplicate keys, see 'fromListDuplicates' or 'fromVectorDuplicates'.- toSeries :: t -> Series v k a-- -- | Construct a container from key-value pairs of a 'Series'. - -- The elements are returned in ascending order of keys. - fromSeries :: Series v k a -> t---instance (Ord k, Vector v a) => IsSeries [(k, a)] v k a where- -- | Construct a series from a list of key-value pairs. There is no- -- condition on the order of pairs.- --- -- >>> let xs = toSeries [('b', 0::Int), ('a', 5), ('d', 1) ]- -- >>> xs- -- index | values- -- ----- | ------- -- 'a' | 5- -- 'b' | 0- -- 'd' | 1- --- -- If you need to handle duplicate keys, take a look at `fromListDuplicates`.- toSeries :: [(k, a)] -> Series v k a- toSeries = toSeries . MS.fromList- {-# INLINE toSeries #-}-- -- | Construct a list from key-value pairs. The elements are in order sorted by key:- --- -- >>> let xs = Series.toSeries [ ('b', 0::Int), ('a', 5), ('d', 1) ]- -- >>> xs- -- index | values- -- ----- | ------- -- 'a' | 5- -- 'b' | 0- -- 'd' | 1- -- >>> fromSeries xs- -- [('a',5),('b',0),('d',1)]- fromSeries :: Series v k a -> [(k, a)]- fromSeries (MkSeries ks vs)= zip (Index.toAscList ks) (Vector.toList vs)- {-# INLINE fromSeries #-}----- | Construct a 'Series' from a list of key-value pairs. There is no--- condition on the order of pairs. Duplicate keys are silently dropped. If you--- need to handle duplicate keys, see 'fromListDuplicates'.-fromList :: (Vector v a, Ord k) => [(k, a)] -> Series v k a-{-# INLINE fromList #-}-fromList = toSeries----- | \(O(n)\) Build a 'Series' from a list of pairs, where the first elements of the pairs (the keys)--- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked.-fromDistinctAscList :: (Vector v a) => [(k, a)] -> Series v k a-fromDistinctAscList xs - = let (!ks, !vs) = unzip xs - in MkSeries (Index.Internal.fromDistinctAscList ks) (Vector.fromListN (List.length vs) vs)----- | Integer-like, non-negative number that specifies how many occurrences--- of a key is present in a 'Series'.------ The easiest way to convert from an 'Occurrence' to another integer-like type--- is the 'fromIntegral' function.-newtype Occurrence = MkOcc Int- deriving (Eq, Enum, Num, Ord, Integral, Real)- deriving newtype (Show, U.Unbox) ---- Occurrence needs to be an 'U.Unbox' type--- so that 'fromVectorDuplicates' works with unboxed vectors--- and series.-newtype instance UM.MVector s Occurrence = MV_Occ (UM.MVector s Int)-newtype instance U.Vector Occurrence = V_Occ (U.Vector Int)-deriving instance GM.MVector UM.MVector Occurrence-deriving instance Vector U.Vector Occurrence ----- | Construct a series from a list of key-value pairs.--- Contrary to 'fromList', values at duplicate keys are preserved. To keep each--- key unique, an 'Occurrence' number counts up.-fromListDuplicates :: (Vector v a, Ord k) => [(k, a)] -> Series v (k, Occurrence) a-{-# INLINE fromListDuplicates #-}-fromListDuplicates = convert . fromVectorDuplicates . Boxed.fromList----- | Construct a list from key-value pairs. The elements are in order sorted by key. -toList :: Vector v a => Series v k a -> [(k, a)]-{-# INLINE toList #-}-toList (MkSeries ks vs) = zip (Index.toAscList ks) (Vector.toList vs)---instance (Ord k) => IsSeries (Boxed.Vector (k, a)) Boxed.Vector k a where- toSeries = fromVector- {-# INLINE toSeries #-}-- fromSeries = toVector- {-# INLINE fromSeries #-}---instance (Ord k, U.Unbox a, U.Unbox k) => IsSeries (U.Vector (k, a)) U.Vector k a where- toSeries :: U.Vector (k, a) -> Series U.Vector k a- toSeries = fromVector- {-# INLINE toSeries #-}-- fromSeries :: Series U.Vector k a -> U.Vector (k, a)- fromSeries = toVector- {-# INLINE fromSeries #-}----- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no--- condition on the order of pairs. Duplicate keys are silently dropped. If you--- need to handle duplicate keys, see 'fromVectorDuplicates'.------ Note that due to differences in sorting,--- 'Series.fromList' and @'Series.fromVector' . 'Vector.fromList'@--- may not be equivalent if the input list contains duplicate keys.-fromVector :: (Ord k, Vector v k, Vector v a, Vector v (k, a))- => v (k, a) -> Series v k a-{-# INLINE fromVector #-}-fromVector vec = let (indexVector, valuesVector) = Vector.unzip $ runST $ do- mv <- Vector.thaw vec- -- Note that we're using this particular flavor of `sortUniqBy`- -- because it both sorts AND removes duplicate keys- destMV <- sortUniqBy (compare `on` fst) mv- v <- Vector.freeze destMV- pure (Vector.force v)- in MkSeries (Index.Internal.fromDistinctAscVector indexVector) valuesVector----- | \(O(n)\) Build a 'Series' from a vector of pairs, where the first elements of the pairs (the keys)--- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked.-fromDistinctAscVector :: (Vector v k, Vector v a, Vector v (k, a))- => v (k, a) -> Series v k a-fromDistinctAscVector xs - = let (ks, vs) = Vector.unzip xs - in MkSeries (Index.Internal.fromDistinctAscVector ks) vs----- | Construct a 'Series' from a 'Vector' of key-value pairs, where there may be duplicate keys. --- There is no condition on the order of pairs.-fromVectorDuplicates :: (Ord k, Vector v k, Vector v a, Vector v (k, a), Vector v (k, Occurrence))- => v (k, a) -> Series v (k, Occurrence) a-{-# INLINE fromVectorDuplicates #-}-fromVectorDuplicates vec - = let (indexVector, valuesVector) - = Vector.unzip $ runST $ do- mv <- Vector.thaw vec- sortBy (compare `on` fst) mv- v <- Vector.freeze mv- pure (Vector.force v)- in MkSeries (Index.Internal.fromDistinctAscVector (occurences indexVector)) valuesVector- where- occurences vs - | Vector.null vs = Vector.empty- | Vector.length vs == 1 = Vector.map (,0) vs- | otherwise = Vector.scanl f (Vector.head vs, 0) (Vector.tail vs)- where- f (lastKey, lastOcc) newKey - | lastKey == newKey = (newKey, lastOcc + 1)- | otherwise = (newKey, 0)----- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. -toVector :: (Vector v a, Vector v k, Vector v (k, a)) - => Series v k a -> v (k, a)-{-# INLINE toVector #-}-toVector (MkSeries ks vs) = Vector.zip (Index.toAscVector ks) vs---instance (Vector v a) => IsSeries (Map k a) v k a where- toSeries :: Map k a -> Series v k a- toSeries mp = MkSeries - { index = Index.fromSet $ MS.keysSet mp- , values = Vector.fromListN (MS.size mp) $ MS.elems mp- }- {-# INLINE toSeries #-}-- fromSeries :: Series v k a -> Map k a- fromSeries (MkSeries ks vs)- = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)- {-# INLINE fromSeries #-}---toLazyMap :: (Vector v a) => Series v k a -> Map k a-{-# INLINE toLazyMap #-}-toLazyMap = fromSeries----- | Construct a series from a lazy 'Data.Map.Lazy.Map'.-fromLazyMap :: (Vector v a) => ML.Map k a -> Series v k a-{-# INLINE fromLazyMap #-}-fromLazyMap = toSeries----- | Convert a series into a strict 'Data.Map.Strict.Map'.-toStrictMap :: (Vector v a) => Series v k a -> Map k a-{-# INLINE toStrictMap #-}-toStrictMap (MkSeries ks vs) = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)----- | Construct a series from a strict 'Data.Map.Strict.Map'.-fromStrictMap :: (Vector v a) => MS.Map k a -> Series v k a-{-# INLINE fromStrictMap #-}-fromStrictMap mp = MkSeries { index = Index.toIndex $ MS.keysSet mp- , values = Vector.fromListN (MS.size mp) $ MS.elems mp- }---instance (Vector v a) => IsSeries (IntMap a) v Int a where- toSeries :: IntMap a -> Series v Int a- toSeries im = MkSeries - { index = Index.toIndex $ IntMap.keysSet im- , values = Vector.fromListN (IntMap.size im) $ IntMap.elems im - }- {-# INLINE toSeries #-}-- fromSeries :: Series v Int a -> IntMap a- fromSeries (MkSeries ks vs) - = IntMap.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs)- {-# INLINE fromSeries #-}---instance (Ord k, Vector v a) => IsSeries (Seq (k, a)) v k a where- toSeries :: Seq (k, a) -> Series v k a- toSeries = toSeries . Foldable.toList- {-# INLINE toSeries #-}-- fromSeries :: Series v k a -> Seq (k, a)- fromSeries = Seq.fromList . fromSeries- {-# INLINE fromSeries #-}---instance (Vector v a) => IsSeries (Set (k, a)) v k a where- toSeries :: Set (k, a) -> Series v k a- toSeries = fromDistinctAscList . Set.toAscList- {-# INLINE toSeries #-}-- fromSeries :: Series v k a -> Set (k, a)- fromSeries = Set.fromDistinctAscList . toList- {-# INLINE fromSeries #-}----- | Get the first value of a 'Series'. If the 'Series' is empty,--- this function returns 'Nothing'.-headM :: Vector v a => Series v k a -> Maybe a-{-# INLINE headM #-}-headM (MkSeries _ vs) = Vector.headM vs----- | Get the last value of a 'Series'. If the 'Series' is empty,--- this function returns 'Nothing'.-lastM :: Vector v a => Series v k a -> Maybe a-{-# INLINE lastM #-}-lastM (MkSeries _ vs) = Vector.lastM vs----- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.-take :: Vector v a => Int -> Series v k a -> Series v k a-{-# INLINE take #-}-take n (MkSeries ks vs) - -- Index.take is O(log n) while Vector.take is O(1)- = MkSeries (Index.take n ks) (Vector.take n vs)----- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.-drop :: Vector v a => Int -> Series v k a -> Series v k a-{-# INLINE drop #-}-drop n (MkSeries ks vs) - -- Index.drop is O(log n) while Vector.drop is O(1)- = MkSeries (Index.drop n ks) (Vector.drop n vs)----- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.-takeWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a-{-# INLINE takeWhile #-}-takeWhile f (MkSeries ix vs) = let taken = Vector.takeWhile f vs- in MkSeries { index = Index.take (Vector.length taken) ix- , values = taken - }----- | \(O(n)\) Returns the complement of 'takeWhile'.-dropWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a-{-# INLINE dropWhile #-}-dropWhile f (MkSeries ix vs) = let dropped = Vector.dropWhile f vs- in MkSeries { index = Index.drop (Index.size ix - Vector.length dropped) ix- , values = dropped- }----- | \(O(n)\) Map every element of a 'Series'.-map :: (Vector v a, Vector v b) - => (a -> b) -> Series v k a -> Series v k b-{-# INLINE map #-}-map f (MkSeries ix xs) = MkSeries ix $ Vector.map f xs----- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.-mapWithKey :: (Vector v a, Vector v b) - => (k -> a -> b) -> Series v k a -> Series v k b-{-# INLINE mapWithKey #-}-mapWithKey f (MkSeries ix xs) - -- We're using boxed vectors to map because we don't want any restrictions- -- on the index type, i.e. we don't want the constraint Vector v k- = let vs = Boxed.zipWith f (Index.toAscVector ix) (Vector.convert xs)- in MkSeries ix (Vector.convert vs)----- | \(O(n \log n)\).--- Map each key in the index to another value. Note that the resulting series--- may have less elements, because each key must be unique.------ In case new keys are conflicting, the first element is kept.-mapIndex :: (Vector v a, Ord k, Ord g) => Series v k a -> (k -> g) -> Series v g a-{-# INLINE mapIndex #-}-mapIndex (MkSeries index values) f- -- Note that the order in which items are kept appears to be backwards;- -- See the examples for Data.Map.Strict.fromListWith- = let mapping = MS.fromListWith (\_ x -> x) $ [(f k, k) | k <- Index.toAscList index]- newvalues = fmap (\k -> values Vector.! Index.Internal.findIndex k index) mapping- in toSeries newvalues----- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.-concatMap :: (Vector v a, Vector v k, Vector v b, Vector v (k, a), Vector v (k, b), Ord k) - => (a -> Series v k b) - -> Series v k a - -> Series v k b-{-# INLINE concatMap #-}-concatMap f = fromVector - . Vector.concatMap (toVector . f . snd) - . toVector---instance (Vector v a, Ord k) => Semigroup (Series v k a) where- {-# INLINE (<>) #-}- (<>) :: Series v k a -> Series v k a -> Series v k a- -- Despite all my effort, merging via conversion to Map remains fastest.- xs <> ys = toSeries $ toStrictMap xs <> toStrictMap ys-- {-# INLINE sconcat #-}- sconcat = toSeries . sconcat . fmap toStrictMap---instance (Vector v a, Ord k) => Monoid (Series v k a) where- {-# INLINE mempty #-}- mempty :: Series v k a- mempty = MkSeries mempty Vector.empty-- {-# INLINE mappend #-}- mappend :: Series v k a -> Series v k a -> Series v k a- mappend = (<>)-- {-# INLINE mconcat #-}- mconcat :: [Series v k a] -> Series v k a- mconcat = toSeries . mconcat . fmap toStrictMap---instance (Vector v a, Eq k, Eq a) => Eq (Series v k a) where- {-# INLINE (==) #-}- (==) :: Series v k a -> Series v k a -> Bool- (MkSeries ks1 vs1) == (MkSeries ks2 vs2) = (ks1 == ks2) && (vs1 `Vector.eq` vs2)---instance (Vector v a, Ord (v a), Ord k, Ord a) => Ord (Series v k a) where- {-# INLINE compare #-}- compare :: Series v k a -> Series v k a -> Ordering- compare (MkSeries ks1 vs1) (MkSeries ks2 vs2) = compare (ks1, vs1) (ks2, vs2)---instance (Functor v) => Functor (Series v k) where- {-# INLINE fmap #-}- fmap :: (a -> b) -> Series v k a -> Series v k b- fmap f (MkSeries ks vs) = MkSeries ks (fmap f vs)---instance (forall a. Vector v a, Functor v) => FunctorWithIndex k (Series v k) where- {-# INLINE imap #-}- imap :: (k -> a -> b) -> Series v k a -> Series v k b- imap = mapWithKey----- Inlining all methods in 'Foldable'--- is important in order for folds over a boxed--- Series to have performance characteristics--- be as close as possible to boxed vectors -instance (Foldable v) => Foldable (Series v k) where- {-# INLINE fold #-}- fold :: Monoid m => Series v k m -> m- fold = Foldable.fold . values-- {-# INLINE foldMap #-}- foldMap :: (Monoid m) => (a -> m) -> Series v k a -> m- foldMap f = Foldable.foldMap f . values-- {-# INLINE foldMap' #-}- foldMap' :: (Monoid m) => (a -> m) -> Series v k a -> m- foldMap' f = Foldable.foldMap f . values-- {-# INLINE foldr #-}- foldr :: (a -> b -> b) -> b -> Series v k a -> b- foldr f i = Foldable.foldr f i . values-- {-# INLINE foldr' #-}- foldr' :: (a -> b -> b) -> b -> Series v k a -> b- foldr' f i = Foldable.foldr' f i . values-- {-# INLINE foldl #-}- foldl :: (b -> a -> b) -> b -> Series v k a -> b- foldl f i = Foldable.foldl f i . values-- {-# INLINE foldl' #-}- foldl' :: (b -> a -> b) -> b -> Series v k a -> b- foldl' f i = Foldable.foldl' f i . values-- {-# INLINE foldr1 #-}- foldr1 :: (a -> a -> a) -> Series v k a -> a- foldr1 f = Foldable.foldr1 f . values-- {-# INLINE foldl1 #-}- foldl1 :: (a -> a -> a) -> Series v k a -> a- foldl1 f = Foldable.foldl1 f . values-- {-# INLINE toList #-}- toList :: Series v k a -> [a]- toList = Foldable.toList . values-- {-# INLINE null #-}- null :: Series v k a -> Bool- null = Foldable.null . values-- {-# INLINE length #-}- length :: Series v k a -> Int- length = Foldable.length . values-- {-# INLINE elem #-}- elem :: Eq a => a -> Series v k a -> Bool- elem e = Foldable.elem e . values-- {-# INLINE maximum #-}- maximum :: Ord a => Series v k a -> a- maximum = Foldable.maximum . values-- {-# INLINE minimum #-}- minimum :: Ord a => Series v k a -> a- minimum = Foldable.minimum . values-- {-# INLINE sum #-}- sum :: Num a => Series v k a -> a- sum = Foldable.sum . values-- {-# INLINE product #-}- product :: Num a => Series v k a -> a- product = Foldable.product . values---instance (forall a. Vector v a, Vector v k, Foldable v, Functor v) => FoldableWithIndex k (Series v k) where- {-# INLINE ifoldMap #-}- ifoldMap :: Monoid m => (k -> a -> m) -> Series v k a -> m- ifoldMap = foldMapWithKey---instance (Foldable v) => Bifoldable (Series v) where- {-# INLINE bifoldMap #-}- bifoldMap :: Monoid m => (k -> m) -> (a -> m) -> Series v k a -> m- bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Foldable.foldMap fv vs---instance (Traversable v) => Traversable (Series v k) where- {-# INLINE traverse #-}- traverse :: Applicative f- => (a -> f b) -> Series v k a -> f (Series v k b)- traverse f (MkSeries ix vs) = MkSeries ix <$> traverse f vs---instance (forall a. Vector v a, Functor v, Foldable v, Ord k, Traversable v) => TraversableWithIndex k (Series v k) where- {-# INLINE itraverse #-}- itraverse :: Applicative f => (k -> a -> f b) -> Series v k a -> f (Series v k b)- itraverse = traverseWithKey----- | \(O(n)\) Execute a 'Fold' over a 'Series'.------ See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into--- account while folding.-fold :: Vector v a - => Fold a b - -> Series v k a - -> b-fold (Fold step init' extract) - = extract . Vector.foldl' step init' . values-{-# INLINE fold #-}----- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.------ See also 'fold' for pure folds, and 'foldMWithKey' to take keys into--- account while folding.-foldM :: (Monad m, Vector v a)- => FoldM m a b - -> Series v k a - -> m b-foldM (FoldM step init' extract) xs- = init' >>= \i -> Vector.foldM' step i (values xs) >>= extract-{-# INLINE foldM #-}----- | \(O(n)\) Execute a 'Fold' over a 'Series', where the 'Fold' takes keys into account.-foldWithKey :: (Vector v a, Vector v k, Vector v (k, a)) - => Fold (k, a) b - -> Series v k a - -> b-foldWithKey (Fold step init' extract) - = extract . Vector.foldl' step init' . toVector-{-# INLINE foldWithKey #-}----- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.-foldMWithKey :: (Monad m, Vector v a, Vector v k, Vector v (k, a)) - => FoldM m (k, a) b- -> Series v k a - -> m b-foldMWithKey (FoldM step init' extract) xs- = init' >>= \i -> Vector.foldM' step i (toVector xs) >>= extract-{-# INLINE foldMWithKey #-}----- | \(O(n)\) Fold over elements in a 'Series'.-foldMap :: (Monoid m, Vector v a) => (a -> m) -> Series v k a -> m-{-# INLINE foldMap #-}-foldMap f = Vector.foldMap f . values----- | \(O(n)\) Fold over pairs of keys and elements in a 'Series'.--- See also 'bifoldMap'.-foldMapWithKey :: (Monoid m, Vector v a, Vector v k, Vector v (k, a)) => (k -> a -> m) -> Series v k a -> m-{-# INLINE foldMapWithKey #-}-foldMapWithKey f = Vector.foldMap (uncurry f) . toVector----- | \(O(n)\) Fold over keys and elements separately in a 'Series'.--- See also 'foldMapWithKey'.-bifoldMap :: (Vector v a, Monoid m) => (k -> m) -> (a -> m) -> Series v k a -> m-{-# INLINE bifoldMap #-}-bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Vector.foldMap fv vs----- | \(O(1)\) Extract the length of a 'Series'.-length :: Vector v a => Series v k a -> Int-{-# INLINE length #-}-length = Vector.length . values----- | \(O(1)\) Test whether a 'Series' is empty.-null :: Vector v a => Series v k a -> Bool-{-# INLINE null #-}-null = Vector.null . values----- | \(O(n)\) Apply the monadic action to every element of a series and its--- index, yielding a series of results.-mapWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) - => (k -> a -> m b) -> Series v k a -> m (Series v k b)-{-# INLINE mapWithKeyM #-}-mapWithKeyM f xs = let f' (key, val) = (key,) <$> f key val- in fmap fromList $ traverse f' $ toList xs----- | \(O(n)\) Apply the monadic action to every element of a series and its--- index, discarding the results.-mapWithKeyM_ :: (Vector v a, Monad m) - => (k -> a -> m b) -> Series v k a -> m ()-{-# INLINE mapWithKeyM_ #-}-mapWithKeyM_ f xs = let f' (key, val) = (key,) <$> f key val- in mapM_ f' $ toList xs----- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, --- yielding a series of results.-forWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) => Series v k a -> (k -> a -> m b) -> m (Series v k b)-{-# INLINE forWithKeyM #-}-forWithKeyM = flip mapWithKeyM----- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, --- discarding the results.-forWithKeyM_ :: (Vector v a, Monad m) => Series v k a -> (k -> a -> m b) -> m ()-{-# INLINE forWithKeyM_ #-}-forWithKeyM_ = flip mapWithKeyM_----- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. -traverseWithKey :: (Applicative t, Ord k, Traversable v, Vector v a, Vector v b, Vector v k, Vector v (k, a), Vector v (k, b))- => (k -> a -> t b) - -> Series v k a - -> t (Series v k b)-{-# INLINE traverseWithKey #-}-traverseWithKey f = fmap fromVector - . traverse (\(k, x) -> (k,) <$> f k x) - . toVector---instance (NFData (v a), NFData k) => NFData (Series v k a) where- rnf :: Series v k a -> ()- rnf (MkSeries ks vs) = rnf ks `seq` rnf vs---instance (Vector v a, Ord k, Show k, Show a) => Show (Series v k a) where- show :: Series v k a -> String- show = display----- | Options controlling how to display 'Series' in the 'displayWith' function.--- Default options are provided by 'defaultDisplayOptions'.------ To help with creating 'DisplayOptions', see 'noLongerThan'.-data DisplayOptions k a- = DisplayOptions- { maximumNumberOfRows :: Int- -- ^ Maximum number of rows shown. These rows will be distributed evenly- -- between the start of the 'Series' and the end. - , indexHeader :: String- -- ^ Header of the index column.- , valuesHeader :: String- -- ^ Header of the values column.- , keyDisplayFunction :: k -> String- -- ^ Function used to display keys from the 'Series'. Use 'noLongerThan'- -- to control the width of the index column.- , valueDisplayFunction :: a -> String- -- ^ Function used to display values from the 'Series'. Use 'noLongerThan'- -- to control the width of the values column.- }----- | Default 'Series' display options.-defaultDisplayOptions :: (Show k, Show a) => DisplayOptions k a-defaultDisplayOptions - = DisplayOptions { maximumNumberOfRows = 6- , indexHeader = "index"- , valuesHeader = "values"- , keyDisplayFunction = show- , valueDisplayFunction = show- }----- | This function modifies existing functions to limit the width of its result.------ >>> let limit7 = (show :: Int -> String) `noLongerThan` 7--- >>> limit7 123456789--- "123456..."-noLongerThan :: (a -> String) -> Int -> (a -> String)-noLongerThan f len x - = let raw = f x- in if List.length raw <= max 0 len- then raw- else List.take (List.length raw - 3) raw <> "..."----- | Display a 'Series' using default 'DisplayOptions'.-display :: (Vector v a, Show k, Show a) - => Series v k a - -> String-display = displayWith defaultDisplayOptions----- | Display a 'Series' using customizable 'DisplayOptions'.-displayWith :: (Vector v a) - => DisplayOptions k a- -> Series v k a - -> String-displayWith DisplayOptions{..} xs- = formatGrid $ if length xs > max 0 maximumNumberOfRows- then let headlength = max 0 maximumNumberOfRows `div` 2- taillength = max 0 maximumNumberOfRows - headlength- in mconcat [ [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ take headlength xs]- , [ ("...", "...") ]- , [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ drop (length xs - taillength) xs]- ] - else [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList xs ]-- where- -- | Format a grid represented by a list of rows, where every row is a list of items- -- All columns will have a fixed width- formatGrid :: [ (String, String) ] -- List of rows- -> String- formatGrid rows = mconcat $ List.intersperse "\n" - $ [ pad indexWidth k <> " | " <> pad valuesWidth v - | (k, v) <- rows'- ] - where- rows' = [ (indexHeader, valuesHeader) ] <> [ ("-----", "------")] <> rows- (indexCol, valuesCol) = unzip rows'- width col = maximum (P.length <$> col)- indexWidth = width indexCol- valuesWidth = width valuesCol-- -- | Pad a string to a minimum of @n@ characters wide.- pad :: Int -> String -> String - pad n s- | n <= P.length s = s- | otherwise = replicate (n - P.length s) ' ' <> s+{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE QuantifiedConstraints #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +module Data.Series.Generic.Definition ( + Series(..), + + convert, + + -- * Basic interface + singleton, + headM, lastM, map, mapWithKey, mapIndex, concatMap, fold, foldM, + foldWithKey, foldMWithKey, foldMap, bifoldMap, foldMapWithKey, + length, null, take, takeWhile, drop, dropWhile, + mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, + traverseWithKey, + + fromIndex, + -- * Conversion to/from Series + IsSeries(..), + -- ** Conversion to/from Maps + fromStrictMap, + toStrictMap, + fromLazyMap, + toLazyMap, + -- ** Conversion to/from list + fromList, + toList, + -- *** Unsafe construction + fromDistinctAscList, + -- ** Conversion to/from vectors + fromVector, + toVector, + -- *** Unsafe construction + fromDistinctAscVector, + -- ** Handling duplicates + Occurrence, fromListDuplicates, fromVectorDuplicates, + + -- * Displaying 'Series' + display, displayWith, + noLongerThan, + DisplayOptions(..), defaultDisplayOptions +) where + +import Control.DeepSeq ( NFData(rnf) ) +import Control.Foldl ( Fold(..), FoldM(..) ) +import Control.Monad.ST ( runST ) +import Data.Bifoldable ( Bifoldable ) +import qualified Data.Bifoldable as Bifoldable +import qualified Data.Foldable as Foldable +import Data.Foldable.WithIndex ( FoldableWithIndex(..)) +import Data.Function ( on ) +import Data.Functor.WithIndex ( FunctorWithIndex(imap) ) + +import Data.IntMap.Strict ( IntMap ) +import qualified Data.IntMap.Strict as IntMap +import qualified Data.List as List +import qualified Data.Map.Lazy as ML +import Data.Map.Strict ( Map ) +import qualified Data.Map.Strict as MS +import Data.Sequence ( Seq ) +import qualified Data.Sequence as Seq +import Data.Semigroup ( Semigroup(..) ) +import Data.Series.Index ( Index ) +import qualified Data.Series.Index as Index +import qualified Data.Series.Index.Internal as Index.Internal +import Data.Set ( Set ) +import qualified Data.Set as Set +import Data.Traversable.WithIndex ( TraversableWithIndex(..) ) +import qualified Data.Vector as Boxed +import Data.Vector.Algorithms.Intro ( sortUniqBy, sortBy ) +import Data.Vector.Generic ( Vector ) +import qualified Data.Vector.Generic as Vector +import qualified Data.Vector.Generic.Mutable as GM +import qualified Data.Vector.Unboxed as U +import qualified Data.Vector.Unboxed.Mutable as UM + +import Prelude hiding ( take, takeWhile, drop, dropWhile, map, concatMap, foldMap, sum, length, null ) +import qualified Prelude as P + + + +-- | A @Series v k a@ is a labeled array of type @v@ filled with values of type @a@, +-- indexed by keys of type @k@. +-- +-- Like 'Data.Map.Strict.Map', they support efficient: +-- +-- * random access by key ( \(O(\log n)\) ); +-- * slice by key ( \(O(\log n)\) ). +-- +-- Like 'Data.Vector.Vector', they support efficient: +-- +-- * random access by index ( \(O(1)\) ); +-- * slice by index ( \(O(1)\) ); +-- * numerical operations. +-- +data Series v k a + -- The reason the index is a set of keys is that we *want* keys to be ordered. + -- This allows for efficient slicing of the underlying values, because + -- if @k1 < k2@, then the values are also at indices @ix1 < ix2@. + = MkSeries { index :: Index k -- ^ The 'Index' of a series, which contains its (unique) keys in ascending order. + , values :: v a -- ^ The values of a series, in the order of its (unique) keys. + } + + +-- | \(O(n)\) Convert between two types of 'Series'. +convert :: (Vector v1 a, Vector v2 a) => Series v1 k a -> Series v2 k a +{-# INLINABLE convert #-} +convert (MkSeries ix vs) = MkSeries ix $ Vector.convert vs + + +-- | \(O(1)\) Create a 'Series' with a single element. +singleton :: Vector v a => k -> a -> Series v k a +{-# INLINABLE singleton #-} +singleton k v = MkSeries (Index.singleton k) $ Vector.singleton v + + +-- | \(O(n)\) Generate a 'Series' by mapping every element of its index. +fromIndex :: (Vector v a) + => (k -> a) -> Index k -> Series v k a +{-# INLINABLE fromIndex #-} +fromIndex f ix = MkSeries ix $ Vector.convert + $ Boxed.map f -- Using boxed vector to prevent a (Vector v k) constraint + $ Index.toAscVector ix + + +-- | The 'IsSeries' typeclass allow for ad-hoc definition +-- of conversion functions, converting to / from 'Series'. +class IsSeries t v k a where + -- | Construct a 'Series' from some container of key-values pairs. There is no + -- condition on the order of pairs. Duplicate keys are silently dropped. If you + -- need to handle duplicate keys, see 'fromListDuplicates' or 'fromVectorDuplicates'. + toSeries :: t -> Series v k a + + -- | Construct a container from key-value pairs of a 'Series'. + -- The elements are returned in ascending order of keys. + fromSeries :: Series v k a -> t + + +instance (Ord k, Vector v a) => IsSeries [(k, a)] v k a where + -- | Construct a series from a list of key-value pairs. There is no + -- condition on the order of pairs. + -- + -- >>> let xs = toSeries [('b', 0::Int), ('a', 5), ('d', 1) ] + -- >>> xs + -- index | values + -- ----- | ------ + -- 'a' | 5 + -- 'b' | 0 + -- 'd' | 1 + -- + -- If you need to handle duplicate keys, take a look at `fromListDuplicates`. + toSeries :: [(k, a)] -> Series v k a + toSeries = toSeries . MS.fromList + {-# INLINABLE toSeries #-} + + -- | Construct a list from key-value pairs. The elements are in order sorted by key: + -- + -- >>> let xs = Series.toSeries [ ('b', 0::Int), ('a', 5), ('d', 1) ] + -- >>> xs + -- index | values + -- ----- | ------ + -- 'a' | 5 + -- 'b' | 0 + -- 'd' | 1 + -- >>> fromSeries xs + -- [('a',5),('b',0),('d',1)] + fromSeries :: Series v k a -> [(k, a)] + fromSeries (MkSeries ks vs)= zip (Index.toAscList ks) (Vector.toList vs) + {-# INLINABLE fromSeries #-} + + +-- | Construct a 'Series' from a list of key-value pairs. There is no +-- condition on the order of pairs. Duplicate keys are silently dropped. If you +-- need to handle duplicate keys, see 'fromListDuplicates'. +fromList :: (Vector v a, Ord k) => [(k, a)] -> Series v k a +{-# INLINABLE fromList #-} +fromList = toSeries + + +-- | \(O(n)\) Build a 'Series' from a list of pairs, where the first elements of the pairs (the keys) +-- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked. +fromDistinctAscList :: (Vector v a) => [(k, a)] -> Series v k a +fromDistinctAscList xs + = let (!ks, !vs) = unzip xs + in MkSeries (Index.Internal.fromDistinctAscList ks) (Vector.fromListN (List.length vs) vs) + + +-- | Integer-like, non-negative number that specifies how many occurrences +-- of a key is present in a 'Series'. +-- +-- The easiest way to convert from an 'Occurrence' to another integer-like type +-- is the 'fromIntegral' function. +newtype Occurrence = MkOcc Int + deriving (Eq, Enum, Num, Ord, Integral, Real) + deriving newtype (Show, U.Unbox) + +-- Occurrence needs to be an 'U.Unbox' type +-- so that 'fromVectorDuplicates' works with unboxed vectors +-- and series. +newtype instance UM.MVector s Occurrence = MV_Occ (UM.MVector s Int) +newtype instance U.Vector Occurrence = V_Occ (U.Vector Int) +deriving instance GM.MVector UM.MVector Occurrence +deriving instance Vector U.Vector Occurrence + + +-- | Construct a series from a list of key-value pairs. +-- Contrary to 'fromList', values at duplicate keys are preserved. To keep each +-- key unique, an 'Occurrence' number counts up. +fromListDuplicates :: (Vector v a, Ord k) => [(k, a)] -> Series v (k, Occurrence) a +{-# INLINABLE fromListDuplicates #-} +fromListDuplicates = convert . fromVectorDuplicates . Boxed.fromList + + +-- | Construct a list from key-value pairs. The elements are in order sorted by key. +toList :: Vector v a => Series v k a -> [(k, a)] +{-# INLINABLE toList #-} +toList (MkSeries ks vs) = zip (Index.toAscList ks) (Vector.toList vs) + + +instance (Ord k) => IsSeries (Boxed.Vector (k, a)) Boxed.Vector k a where + toSeries = fromVector + {-# INLINABLE toSeries #-} + + fromSeries = toVector + {-# INLINABLE fromSeries #-} + + +instance (Ord k, U.Unbox a, U.Unbox k) => IsSeries (U.Vector (k, a)) U.Vector k a where + toSeries :: U.Vector (k, a) -> Series U.Vector k a + toSeries = fromVector + {-# INLINABLE toSeries #-} + + fromSeries :: Series U.Vector k a -> U.Vector (k, a) + fromSeries = toVector + {-# INLINABLE fromSeries #-} + + +-- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no +-- condition on the order of pairs. Duplicate keys are silently dropped. If you +-- need to handle duplicate keys, see 'fromVectorDuplicates'. +-- +-- Note that due to differences in sorting, +-- 'Series.fromList' and @'Series.fromVector' . 'Vector.fromList'@ +-- may not be equivalent if the input list contains duplicate keys. +fromVector :: (Ord k, Vector v k, Vector v a, Vector v (k, a)) + => v (k, a) -> Series v k a +{-# INLINABLE fromVector #-} +fromVector vec = let (indexVector, valuesVector) = Vector.unzip $ runST $ do + mv <- Vector.thaw vec + -- Note that we're using this particular flavor of `sortUniqBy` + -- because it both sorts AND removes duplicate keys + destMV <- sortUniqBy (compare `on` fst) mv + v <- Vector.freeze destMV + pure (Vector.force v) + in MkSeries (Index.Internal.fromDistinctAscVector indexVector) valuesVector + + +-- | \(O(n)\) Build a 'Series' from a vector of pairs, where the first elements of the pairs (the keys) +-- are distinct elements in ascending order. The precondition that the keys be unique and sorted is not checked. +fromDistinctAscVector :: (Vector v k, Vector v a, Vector v (k, a)) + => v (k, a) -> Series v k a +fromDistinctAscVector xs + = let (ks, vs) = Vector.unzip xs + in MkSeries (Index.Internal.fromDistinctAscVector ks) vs + + +-- | Construct a 'Series' from a 'Vector' of key-value pairs, where there may be duplicate keys. +-- There is no condition on the order of pairs. +fromVectorDuplicates :: (Ord k, Vector v k, Vector v a, Vector v (k, a), Vector v (k, Occurrence)) + => v (k, a) -> Series v (k, Occurrence) a +{-# INLINABLE fromVectorDuplicates #-} +fromVectorDuplicates vec + = let (indexVector, valuesVector) + = Vector.unzip $ runST $ do + mv <- Vector.thaw vec + sortBy (compare `on` fst) mv + v <- Vector.freeze mv + pure (Vector.force v) + in MkSeries (Index.Internal.fromDistinctAscVector (occurences indexVector)) valuesVector + where + occurences vs + | Vector.null vs = Vector.empty + | Vector.length vs == 1 = Vector.map (,0) vs + | otherwise = Vector.scanl f (Vector.head vs, 0) (Vector.tail vs) + where + f (lastKey, lastOcc) newKey + | lastKey == newKey = (newKey, lastOcc + 1) + | otherwise = (newKey, 0) + + +-- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. +toVector :: (Vector v a, Vector v k, Vector v (k, a)) + => Series v k a -> v (k, a) +{-# INLINABLE toVector #-} +toVector (MkSeries ks vs) = Vector.zip (Index.toAscVector ks) vs + + +instance (Vector v a) => IsSeries (Map k a) v k a where + toSeries :: Map k a -> Series v k a + toSeries mp = MkSeries + { index = Index.fromSet $ MS.keysSet mp + , values = Vector.fromListN (MS.size mp) $ MS.elems mp + } + {-# INLINABLE toSeries #-} + + fromSeries :: Series v k a -> Map k a + fromSeries (MkSeries ks vs) + = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs) + {-# INLINABLE fromSeries #-} + + +toLazyMap :: (Vector v a) => Series v k a -> Map k a +{-# INLINABLE toLazyMap #-} +toLazyMap = fromSeries + + +-- | Construct a series from a lazy 'Data.Map.Lazy.Map'. +fromLazyMap :: (Vector v a) => ML.Map k a -> Series v k a +{-# INLINABLE fromLazyMap #-} +fromLazyMap = toSeries + + +-- | Convert a series into a strict 'Data.Map.Strict.Map'. +toStrictMap :: (Vector v a) => Series v k a -> Map k a +{-# INLINABLE toStrictMap #-} +toStrictMap (MkSeries ks vs) = MS.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs) + + +-- | Construct a series from a strict 'Data.Map.Strict.Map'. +fromStrictMap :: (Vector v a) => MS.Map k a -> Series v k a +{-# INLINABLE fromStrictMap #-} +fromStrictMap mp = MkSeries { index = Index.toIndex $ MS.keysSet mp + , values = Vector.fromListN (MS.size mp) $ MS.elems mp + } + + +instance (Vector v a) => IsSeries (IntMap a) v Int a where + toSeries :: IntMap a -> Series v Int a + toSeries im = MkSeries + { index = Index.toIndex $ IntMap.keysSet im + , values = Vector.fromListN (IntMap.size im) $ IntMap.elems im + } + {-# INLINABLE toSeries #-} + + fromSeries :: Series v Int a -> IntMap a + fromSeries (MkSeries ks vs) + = IntMap.fromDistinctAscList $ zip (Index.toAscList ks) (Vector.toList vs) + {-# INLINABLE fromSeries #-} + + +instance (Ord k, Vector v a) => IsSeries (Seq (k, a)) v k a where + toSeries :: Seq (k, a) -> Series v k a + toSeries = toSeries . Foldable.toList + {-# INLINABLE toSeries #-} + + fromSeries :: Series v k a -> Seq (k, a) + fromSeries = Seq.fromList . fromSeries + {-# INLINABLE fromSeries #-} + + +instance (Vector v a) => IsSeries (Set (k, a)) v k a where + toSeries :: Set (k, a) -> Series v k a + toSeries = fromDistinctAscList . Set.toAscList + {-# INLINABLE toSeries #-} + + fromSeries :: Series v k a -> Set (k, a) + fromSeries = Set.fromDistinctAscList . toList + {-# INLINABLE fromSeries #-} + + +-- | Get the first value of a 'Series'. If the 'Series' is empty, +-- this function returns 'Nothing'. +headM :: Vector v a => Series v k a -> Maybe a +{-# INLINABLE headM #-} +headM (MkSeries _ vs) = Vector.headM vs + + +-- | Get the last value of a 'Series'. If the 'Series' is empty, +-- this function returns 'Nothing'. +lastM :: Vector v a => Series v k a -> Maybe a +{-# INLINABLE lastM #-} +lastM (MkSeries _ vs) = Vector.lastM vs + + +-- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@. +take :: Vector v a => Int -> Series v k a -> Series v k a +{-# INLINABLE take #-} +take n (MkSeries ks vs) + -- Index.take is O(log n) while Vector.take is O(1) + = MkSeries (Index.take n ks) (Vector.take n vs) + + +-- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@. +drop :: Vector v a => Int -> Series v k a -> Series v k a +{-# INLINABLE drop #-} +drop n (MkSeries ks vs) + -- Index.drop is O(log n) while Vector.drop is O(1) + = MkSeries (Index.drop n ks) (Vector.drop n vs) + + +-- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate. +takeWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a +{-# INLINABLE takeWhile #-} +takeWhile f (MkSeries ix vs) = let taken = Vector.takeWhile f vs + in MkSeries { index = Index.take (Vector.length taken) ix + , values = taken + } + + +-- | \(O(n)\) Returns the complement of 'takeWhile'. +dropWhile :: Vector v a => (a -> Bool) -> Series v k a -> Series v k a +{-# INLINABLE dropWhile #-} +dropWhile f (MkSeries ix vs) = let dropped = Vector.dropWhile f vs + in MkSeries { index = Index.drop (Index.size ix - Vector.length dropped) ix + , values = dropped + } + + +-- | \(O(n)\) Map every element of a 'Series'. +map :: (Vector v a, Vector v b) + => (a -> b) -> Series v k a -> Series v k b +{-# INLINABLE map #-} +map f (MkSeries ix xs) = MkSeries ix $ Vector.map f xs + + +-- | \(O(n)\) Map every element of a 'Series', possibly using the key as well. +mapWithKey :: (Vector v a, Vector v b) + => (k -> a -> b) -> Series v k a -> Series v k b +{-# INLINABLE mapWithKey #-} +mapWithKey f (MkSeries ix xs) + -- We're using boxed vectors to map because we don't want any restrictions + -- on the index type, i.e. we don't want the constraint Vector v k + = let vs = Boxed.zipWith f (Index.toAscVector ix) (Vector.convert xs) + in MkSeries ix (Vector.convert vs) + + +-- | \(O(n \log n)\). +-- Map each key in the index to another value. Note that the resulting series +-- may have less elements, because each key must be unique. +-- +-- In case new keys are conflicting, the first element is kept. +mapIndex :: (Vector v a, Ord k, Ord g) => Series v k a -> (k -> g) -> Series v g a +{-# INLINABLE mapIndex #-} +mapIndex (MkSeries index values) f + -- Note that the order in which items are kept appears to be backwards; + -- See the examples for Data.Map.Strict.fromListWith + = let mapping = MS.fromListWith (\_ x -> x) $ [(f k, k) | k <- Index.toAscList index] + newvalues = fmap (\k -> values Vector.! Index.Internal.findIndex k index) mapping + in toSeries newvalues + + +-- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'. +concatMap :: (Vector v a, Vector v k, Vector v b, Vector v (k, a), Vector v (k, b), Ord k) + => (a -> Series v k b) + -> Series v k a + -> Series v k b +{-# INLINABLE concatMap #-} +concatMap f = fromVector + . Vector.concatMap (toVector . f . snd) + . toVector + + +instance (Vector v a, Ord k) => Semigroup (Series v k a) where + {-# INLINABLE (<>) #-} + (<>) :: Series v k a -> Series v k a -> Series v k a + -- Despite all my effort, merging via conversion to Map remains fastest. + xs <> ys = toSeries $ toStrictMap xs <> toStrictMap ys + + {-# INLINABLE sconcat #-} + sconcat = toSeries . sconcat . fmap toStrictMap + + +instance (Vector v a, Ord k) => Monoid (Series v k a) where + {-# INLINABLE mempty #-} + mempty :: Series v k a + mempty = MkSeries mempty Vector.empty + + {-# INLINABLE mappend #-} + mappend :: Series v k a -> Series v k a -> Series v k a + mappend = (<>) + + {-# INLINABLE mconcat #-} + mconcat :: [Series v k a] -> Series v k a + mconcat = toSeries . mconcat . fmap toStrictMap + + +instance (Vector v a, Eq k, Eq a) => Eq (Series v k a) where + {-# INLINABLE (==) #-} + (==) :: Series v k a -> Series v k a -> Bool + (MkSeries ks1 vs1) == (MkSeries ks2 vs2) = (ks1 == ks2) && (vs1 `Vector.eq` vs2) + + +instance (Vector v a, Ord (v a), Ord k, Ord a) => Ord (Series v k a) where + {-# INLINABLE compare #-} + compare :: Series v k a -> Series v k a -> Ordering + compare (MkSeries ks1 vs1) (MkSeries ks2 vs2) = compare (ks1, vs1) (ks2, vs2) + + +instance (Functor v) => Functor (Series v k) where + {-# INLINABLE fmap #-} + fmap :: (a -> b) -> Series v k a -> Series v k b + fmap f (MkSeries ks vs) = MkSeries ks (fmap f vs) + + +instance (forall a. Vector v a, Functor v) => FunctorWithIndex k (Series v k) where + {-# INLINABLE imap #-} + imap :: (k -> a -> b) -> Series v k a -> Series v k b + imap = mapWithKey + + +-- Inlining all methods in 'Foldable' +-- is important in order for folds over a boxed +-- Series to have performance characteristics +-- be as close as possible to boxed vectors +instance (Foldable v) => Foldable (Series v k) where + {-# INLINABLE fold #-} + fold :: Monoid m => Series v k m -> m + fold = Foldable.fold . values + + {-# INLINABLE foldMap #-} + foldMap :: (Monoid m) => (a -> m) -> Series v k a -> m + foldMap f = Foldable.foldMap f . values + + {-# INLINABLE foldMap' #-} + foldMap' :: (Monoid m) => (a -> m) -> Series v k a -> m + foldMap' f = Foldable.foldMap f . values + + {-# INLINABLE foldr #-} + foldr :: (a -> b -> b) -> b -> Series v k a -> b + foldr f i = Foldable.foldr f i . values + + {-# INLINABLE foldr' #-} + foldr' :: (a -> b -> b) -> b -> Series v k a -> b + foldr' f i = Foldable.foldr' f i . values + + {-# INLINABLE foldl #-} + foldl :: (b -> a -> b) -> b -> Series v k a -> b + foldl f i = Foldable.foldl f i . values + + {-# INLINABLE foldl' #-} + foldl' :: (b -> a -> b) -> b -> Series v k a -> b + foldl' f i = Foldable.foldl' f i . values + + {-# INLINABLE foldr1 #-} + foldr1 :: (a -> a -> a) -> Series v k a -> a + foldr1 f = Foldable.foldr1 f . values + + {-# INLINABLE foldl1 #-} + foldl1 :: (a -> a -> a) -> Series v k a -> a + foldl1 f = Foldable.foldl1 f . values + + {-# INLINABLE toList #-} + toList :: Series v k a -> [a] + toList = Foldable.toList . values + + {-# INLINABLE null #-} + null :: Series v k a -> Bool + null = Foldable.null . values + + {-# INLINABLE length #-} + length :: Series v k a -> Int + length = Foldable.length . values + + {-# INLINABLE elem #-} + elem :: Eq a => a -> Series v k a -> Bool + elem e = Foldable.elem e . values + + {-# INLINABLE maximum #-} + maximum :: Ord a => Series v k a -> a + maximum = Foldable.maximum . values + + {-# INLINABLE minimum #-} + minimum :: Ord a => Series v k a -> a + minimum = Foldable.minimum . values + + {-# INLINABLE sum #-} + sum :: Num a => Series v k a -> a + sum = Foldable.sum . values + + {-# INLINABLE product #-} + product :: Num a => Series v k a -> a + product = Foldable.product . values + + +instance (forall a. Vector v a, Vector v k, Foldable v, Functor v) => FoldableWithIndex k (Series v k) where + {-# INLINABLE ifoldMap #-} + ifoldMap :: Monoid m => (k -> a -> m) -> Series v k a -> m + ifoldMap = foldMapWithKey + + +instance (Foldable v) => Bifoldable (Series v) where + {-# INLINABLE bifoldMap #-} + bifoldMap :: Monoid m => (k -> m) -> (a -> m) -> Series v k a -> m + bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Foldable.foldMap fv vs + + +instance (Traversable v) => Traversable (Series v k) where + {-# INLINABLE traverse #-} + traverse :: Applicative f + => (a -> f b) -> Series v k a -> f (Series v k b) + traverse f (MkSeries ix vs) = MkSeries ix <$> traverse f vs + + +instance (forall a. Vector v a, Functor v, Foldable v, Ord k, Traversable v) => TraversableWithIndex k (Series v k) where + {-# INLINABLE itraverse #-} + itraverse :: Applicative f => (k -> a -> f b) -> Series v k a -> f (Series v k b) + itraverse = traverseWithKey + + +-- | \(O(n)\) Execute a 'Fold' over a 'Series'. +-- +-- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into +-- account while folding. +fold :: Vector v a + => Fold a b + -> Series v k a + -> b +fold (Fold step init' extract) + = extract . Vector.foldl' step init' . values +{-# INLINABLE fold #-} + + +-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'. +-- +-- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into +-- account while folding. +foldM :: (Monad m, Vector v a) + => FoldM m a b + -> Series v k a + -> m b +foldM (FoldM step init' extract) xs + = init' >>= \i -> Vector.foldM' step i (values xs) >>= extract +{-# INLINABLE foldM #-} + + +-- | \(O(n)\) Execute a 'Fold' over a 'Series', where the 'Fold' takes keys into account. +foldWithKey :: (Vector v a, Vector v k, Vector v (k, a)) + => Fold (k, a) b + -> Series v k a + -> b +foldWithKey (Fold step init' extract) + = extract . Vector.foldl' step init' . toVector +{-# INLINABLE foldWithKey #-} + + +-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account. +foldMWithKey :: (Monad m, Vector v a, Vector v k, Vector v (k, a)) + => FoldM m (k, a) b + -> Series v k a + -> m b +foldMWithKey (FoldM step init' extract) xs + = init' >>= \i -> Vector.foldM' step i (toVector xs) >>= extract +{-# INLINABLE foldMWithKey #-} + + +-- | \(O(n)\) Fold over elements in a 'Series'. +foldMap :: (Monoid m, Vector v a) => (a -> m) -> Series v k a -> m +{-# INLINABLE foldMap #-} +foldMap f = Vector.foldMap f . values + + +-- | \(O(n)\) Fold over pairs of keys and elements in a 'Series'. +-- See also 'bifoldMap'. +foldMapWithKey :: (Monoid m, Vector v a, Vector v k, Vector v (k, a)) => (k -> a -> m) -> Series v k a -> m +{-# INLINABLE foldMapWithKey #-} +foldMapWithKey f = Vector.foldMap (uncurry f) . toVector + + +-- | \(O(n)\) Fold over keys and elements separately in a 'Series'. +-- See also 'foldMapWithKey'. +bifoldMap :: (Vector v a, Monoid m) => (k -> m) -> (a -> m) -> Series v k a -> m +{-# INLINABLE bifoldMap #-} +bifoldMap fk fv (MkSeries ks vs) = P.foldMap fk ks <> Vector.foldMap fv vs + + +-- | \(O(1)\) Extract the length of a 'Series'. +length :: Vector v a => Series v k a -> Int +{-# INLINABLE length #-} +length = Vector.length . values + + +-- | \(O(1)\) Test whether a 'Series' is empty. +null :: Vector v a => Series v k a -> Bool +{-# INLINABLE null #-} +null = Vector.null . values + + +-- | \(O(n)\) Apply the monadic action to every element of a series and its +-- index, yielding a series of results. +mapWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) + => (k -> a -> m b) -> Series v k a -> m (Series v k b) +{-# INLINABLE mapWithKeyM #-} +mapWithKeyM f xs = let f' (key, val) = (key,) <$> f key val + in fmap fromList $ traverse f' $ toList xs + + +-- | \(O(n)\) Apply the monadic action to every element of a series and its +-- index, discarding the results. +mapWithKeyM_ :: (Vector v a, Monad m) + => (k -> a -> m b) -> Series v k a -> m () +{-# INLINABLE mapWithKeyM_ #-} +mapWithKeyM_ f xs = let f' (key, val) = (key,) <$> f key val + in mapM_ f' $ toList xs + + +-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, +-- yielding a series of results. +forWithKeyM :: (Vector v a, Vector v b, Monad m, Ord k) => Series v k a -> (k -> a -> m b) -> m (Series v k b) +{-# INLINABLE forWithKeyM #-} +forWithKeyM = flip mapWithKeyM + + +-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, +-- discarding the results. +forWithKeyM_ :: (Vector v a, Monad m) => Series v k a -> (k -> a -> m b) -> m () +{-# INLINABLE forWithKeyM_ #-} +forWithKeyM_ = flip mapWithKeyM_ + + +-- | \(O(n)\) Traverse a 'Series' with an Applicative action, taking into account both keys and values. +traverseWithKey :: (Applicative t, Ord k, Traversable v, Vector v a, Vector v b, Vector v k, Vector v (k, a), Vector v (k, b)) + => (k -> a -> t b) + -> Series v k a + -> t (Series v k b) +{-# INLINABLE traverseWithKey #-} +traverseWithKey f = fmap fromVector + . traverse (\(k, x) -> (k,) <$> f k x) + . toVector + + +instance (NFData (v a), NFData k) => NFData (Series v k a) where + rnf :: Series v k a -> () + rnf (MkSeries ks vs) = rnf ks `seq` rnf vs + + +instance (Vector v a, Ord k, Show k, Show a) => Show (Series v k a) where + show :: Series v k a -> String + show = display + + +-- | Options controlling how to display 'Series' in the 'displayWith' function. +-- Default options are provided by 'defaultDisplayOptions'. +-- +-- To help with creating 'DisplayOptions', see 'noLongerThan'. +data DisplayOptions k a + = DisplayOptions + { maximumNumberOfRows :: Int + -- ^ Maximum number of rows shown. These rows will be distributed evenly + -- between the start of the 'Series' and the end. + , indexHeader :: String + -- ^ Header of the index column. + , valuesHeader :: String + -- ^ Header of the values column. + , keyDisplayFunction :: k -> String + -- ^ Function used to display keys from the 'Series'. Use 'noLongerThan' + -- to control the width of the index column. + , valueDisplayFunction :: a -> String + -- ^ Function used to display values from the 'Series'. Use 'noLongerThan' + -- to control the width of the values column. + } + + +-- | Default 'Series' display options. +defaultDisplayOptions :: (Show k, Show a) => DisplayOptions k a +defaultDisplayOptions + = DisplayOptions { maximumNumberOfRows = 6 + , indexHeader = "index" + , valuesHeader = "values" + , keyDisplayFunction = show + , valueDisplayFunction = show + } + + +-- | This function modifies existing functions to limit the width of its result. +-- +-- >>> let limit7 = (show :: Int -> String) `noLongerThan` 7 +-- >>> limit7 123456789 +-- "123456..." +noLongerThan :: (a -> String) -> Int -> (a -> String) +noLongerThan f len x + = let raw = f x + in if List.length raw <= max 0 len + then raw + else List.take (List.length raw - 3) raw <> "..." + + +-- | Display a 'Series' using default 'DisplayOptions'. +display :: (Vector v a, Show k, Show a) + => Series v k a + -> String +display = displayWith defaultDisplayOptions + + +-- | Display a 'Series' using customizable 'DisplayOptions'. +displayWith :: (Vector v a) + => DisplayOptions k a + -> Series v k a + -> String +displayWith DisplayOptions{..} xs + = formatGrid $ if length xs > max 0 maximumNumberOfRows + then let headlength = max 0 maximumNumberOfRows `div` 2 + taillength = max 0 maximumNumberOfRows - headlength + in mconcat [ [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ take headlength xs] + , [ ("...", "...") ] + , [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList $ drop (length xs - taillength) xs] + ] + else [ (keyDisplayFunction k, valueDisplayFunction v) | (k, v) <- toList xs ] + + where + -- | Format a grid represented by a list of rows, where every row is a list of items + -- All columns will have a fixed width + formatGrid :: [ (String, String) ] -- List of rows + -> String + formatGrid rows = mconcat $ List.intersperse "\n" + $ [ pad indexWidth k <> " | " <> pad valuesWidth v + | (k, v) <- rows' + ] + where + rows' = [ (indexHeader, valuesHeader) ] <> [ ("-----", "------")] <> rows + (indexCol, valuesCol) = unzip rows' + width col = maximum (P.length <$> col) + indexWidth = width indexCol + valuesWidth = width valuesCol + + -- | Pad a string to a minimum of @n@ characters wide. + pad :: Int -> String -> String + pad n s + | n <= P.length s = s + | otherwise = replicate (n - P.length s) ' ' <> s
src/Data/Series/Generic/Internal.hs view
@@ -1,27 +1,27 @@--------------------------------------------------------------------------------- |--- Module : Data.Series.Generic.Internal--- Copyright : (c) Laurent P. René de Cotret--- License : MIT--- Maintainer : laurent.decotret@outlook.com--- Portability : portable------ = WARNING------ This module is considered __internal__. Using the 'Series' constructor--- directly may result in loss or corruption of data if not handled carefully.------ The Package Versioning Policy still applies.--module Data.Series.Generic.Internal ( - -- * Constructor- Series(..),- -- * Unsafe construction- fromDistinctAscList,- fromDistinctAscVector,- -- * Unsafe selection- selectSubset-) where--import Data.Series.Generic.Definition ( Series(..), fromDistinctAscList, fromDistinctAscVector )+----------------------------------------------------------------------------- +-- | +-- Module : Data.Series.Generic.Internal +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT +-- Maintainer : laurent.decotret@outlook.com +-- Portability : portable +-- +-- = WARNING +-- +-- This module is considered __internal__. Using the 'Series' constructor +-- directly may result in loss or corruption of data if not handled carefully. +-- +-- The Package Versioning Policy still applies. + +module Data.Series.Generic.Internal ( + -- * Constructor + Series(..), + -- * Unsafe construction + fromDistinctAscList, + fromDistinctAscVector, + -- * Unsafe selection + selectSubset +) where + +import Data.Series.Generic.Definition ( Series(..), fromDistinctAscList, fromDistinctAscVector ) import Data.Series.Generic.View ( selectSubset )
− src/Data/Series/Generic/Numeric.hs
@@ -1,7 +0,0 @@-module Data.Series.Generic.Numeric ( - module Control.Foldl-) where--import Control.Foldl ( sum, product, mean, variance, std )--
src/Data/Series/Generic/Scans.hs view
@@ -1,112 +1,112 @@--module Data.Series.Generic.Scans (- postscanl,- prescanl,-- -- * Filling missing data- forwardFill,-) where--import Data.Series.Generic.Definition ( Series(..) )--import Data.Vector.Generic ( Vector )-import qualified Data.Vector.Generic as Vector ---- $setup--- >>> import qualified Data.Series.Generic ( Series )--- >>> import qualified Data.Series.Generic as Series--- >>> import qualified Data.Series.Index as Index---- | \(O(n)\) Left-to-right postscan.------ >>> import qualified Data.Vector as V --- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int--- >>> xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- 3 | 4--- >>> postscanl (+) 0 xs--- index | values--- ----- | --------- 0 | 1--- 1 | 3--- 2 | 6--- 3 | 10-postscanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a-{-# INLINE postscanl #-}-postscanl f s (MkSeries ix vs) = MkSeries ix $ Vector.postscanl f s vs----- | \(O(n)\) Left-to-right prescan.------ >>> import qualified Data.Vector as V --- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int--- >>> xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- 3 | 4--- >>> prescanl (+) 0 xs--- index | values--- ----- | --------- 0 | 0--- 1 | 1--- 2 | 3--- 3 | 6-prescanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a-{-# INLINE prescanl #-}-prescanl f s (MkSeries ix vs) = MkSeries ix $ Vector.prescanl f s vs----- | \(O(n)\) Replace all instances of 'Nothing' with the last previous--- value which was not 'Nothing'.------ >>> import qualified Data.Vector as V --- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int)--- >>> xs--- index | values--- ----- | --------- 0 | Just 1--- 1 | Just 2--- 2 | Nothing--- 3 | Just 3--- >>> forwardFill 0 xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 2--- 3 | 3------ If the first entry of the series is missing, the first input to 'forwardFill' will be used:------ >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int)--- >>> ys--- index | values--- ----- | --------- 0 | Nothing--- 1 | Just 2--- 2 | Nothing--- 3 | Just 3--- >>> forwardFill 0 ys--- index | values--- ----- | --------- 0 | 0--- 1 | 2--- 2 | 2--- 3 | 3-forwardFill :: (Vector v a, Vector v (Maybe a))- => a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value.- -> Series v k (Maybe a)- -> Series v k a-{-# INLINE forwardFill #-}-forwardFill = postscanl go- where- go :: a -> Maybe a -> a- go lastValid Nothing = lastValid- go _ (Just v) = v+ +module Data.Series.Generic.Scans ( + postscanl, + prescanl, + + -- * Filling missing data + forwardFill, +) where + +import Data.Series.Generic.Definition ( Series(..) ) + +import Data.Vector.Generic ( Vector ) +import qualified Data.Vector.Generic as Vector + +-- $setup +-- >>> import qualified Data.Series.Generic ( Series ) +-- >>> import qualified Data.Series.Generic as Series +-- >>> import qualified Data.Series.Index as Index + +-- | \(O(n)\) Left-to-right postscan. +-- +-- >>> import qualified Data.Vector as V +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- 3 | 4 +-- >>> postscanl (+) 0 xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 3 +-- 2 | 6 +-- 3 | 10 +postscanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a +{-# INLINABLE postscanl #-} +postscanl f s (MkSeries ix vs) = MkSeries ix $ Vector.postscanl f s vs + + +-- | \(O(n)\) Left-to-right prescan. +-- +-- >>> import qualified Data.Vector as V +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series V.Vector Int Int +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- 3 | 4 +-- >>> prescanl (+) 0 xs +-- index | values +-- ----- | ------ +-- 0 | 0 +-- 1 | 1 +-- 2 | 3 +-- 3 | 6 +prescanl :: (Vector v a, Vector v b) => (a -> b -> a) -> a -> Series v k b -> Series v k a +{-# INLINABLE prescanl #-} +prescanl f s (MkSeries ix vs) = MkSeries ix $ Vector.prescanl f s vs + + +-- | \(O(n)\) Replace all instances of 'Nothing' with the last previous +-- value which was not 'Nothing'. +-- +-- >>> import qualified Data.Vector as V +-- >>> let xs = Series.fromList (zip [0..] [Just 1, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int) +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | Just 1 +-- 1 | Just 2 +-- 2 | Nothing +-- 3 | Just 3 +-- >>> forwardFill 0 xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 2 +-- 3 | 3 +-- +-- If the first entry of the series is missing, the first input to 'forwardFill' will be used: +-- +-- >>> let ys = Series.fromList (zip [0..] [Nothing, Just 2,Nothing, Just 3]) :: Series V.Vector Int (Maybe Int) +-- >>> ys +-- index | values +-- ----- | ------ +-- 0 | Nothing +-- 1 | Just 2 +-- 2 | Nothing +-- 3 | Just 3 +-- >>> forwardFill 0 ys +-- index | values +-- ----- | ------ +-- 0 | 0 +-- 1 | 2 +-- 2 | 2 +-- 3 | 3 +forwardFill :: (Vector v a, Vector v (Maybe a)) + => a -- ^ Until the first non-'Nothing' is found, 'Nothing' will be filled with this value. + -> Series v k (Maybe a) + -> Series v k a +{-# INLINABLE forwardFill #-} +forwardFill = postscanl go + where + go :: a -> Maybe a -> a + go lastValid Nothing = lastValid + go _ (Just v) = v
src/Data/Series/Generic/View.hs view
@@ -1,333 +1,336 @@-module Data.Series.Generic.View (- -- * Accessing a single element- (!),- at,- iat,-- -- * Bulk access- select,- slice,- selectWhere,- selectSubset,- Selection,-- -- * Resizing- require,- requireWith,- filter,- filterWithKey,- catMaybes,- dropIndex,-- -- * Creating and accessing ranges- Range(..),- to,- from,- upto,-) where---import Data.Series.Index ( Index )-import qualified Data.Series.Index as Index-import qualified Data.Series.Index.Internal as Index.Internal-import Data.Maybe ( fromJust, isJust )-import Data.Series.Generic.Definition ( Series(..) )-import qualified Data.Series.Generic.Definition as G-import Data.Set ( Set )-import qualified Data.Set as Set-import qualified Data.Vector as Boxed-import Data.Vector.Generic ( Vector )-import qualified Data.Vector.Generic as Vector--import Prelude hiding ( filter )---- $setup--- >>> import qualified Data.Series as Series--- >>> import qualified Data.Series.Index as Index --infixr 9 `to` -- Ensure that @to@ binds strongest-infixl 1 `select` ----- | \(O(1)\). Extract a single value from a series, by index. --- An exception is thrown if the index is out-of-bounds.------ A safer alternative is @iat@, which returns 'Nothing' if the index is--- out-of-bounds.-(!) :: Vector v a => Series v k a -> Int -> a-(MkSeries _ vs) ! ix = (Vector.!) vs ix----- | \(O(\log n)\). Extract a single value from a series, by key.-at :: (Vector v a, Ord k) => Series v k a -> k -> Maybe a-at (MkSeries ks vs) k = do- ix <- Index.lookupIndex k ks- pure $ Vector.unsafeIndex vs ix -{-# INLINE at #-}----- | \(O(1)\). Extract a single value from a series, by index.-iat :: Vector v a => Series v k a -> Int -> Maybe a-iat (MkSeries _ vs) = (Vector.!?) vs-{-# INLINE iat #-}----- | require a series with a new index.--- Contrary to 'select', all keys in @'Set' k@ will be present in the re-indexed series.-require :: (Vector v a, Vector v (Maybe a), Ord k) - => Series v k a -> Index k -> Series v k (Maybe a)-{-# INLINE require #-}-require = requireWith (const Nothing) Just----- | Generalization of 'require', which maps missing keys to values.--- This is particularly useful for 'Vector' instances which don't support 'Maybe', like "Data.Vector.Unboxed".-requireWith :: (Vector v a, Vector v b, Ord k)- => (k -> b) -- ^ Function to apply to keys which are missing from the input series, but required in the input index- -> (a -> b) -- ^ Function to apply to values which are in the input series and input index.- -> Series v k a - -> Index k - -> Series v k b-{-# INLINE requireWith #-}-requireWith replacement f xs ss - = let existingKeys = index xs `Index.intersection` ss- newKeys = ss `Index.difference` existingKeys- in G.map f (xs `selectSubset` existingKeys) <> MkSeries newKeys (Vector.fromListN (Index.size newKeys) (replacement <$> Index.toAscList newKeys))----- | Drop the index of a series by replacing it with an @Int@-based index. Values will--- be indexed from 0.-dropIndex :: Series v k a -> Series v Int a-{-# INLINE dropIndex #-}-dropIndex (MkSeries ks vs) = MkSeries (Index.Internal.fromDistinctAscList [0..Index.size ks - 1]) vs----- | Filter elements. Only elements for which the predicate is @True@ are kept. --- Notice that the filtering is done on the values, not on the keys; see 'filterWithKey'--- to filter while taking keys into account.-filter :: (Vector v a, Vector v Int, Ord k) - => (a -> Bool) -> Series v k a -> Series v k a-{-# INLINE filter #-}-filter predicate xs@(MkSeries ks vs) - = let indicesToKeep = Vector.findIndices predicate vs- keysToKeep = Index.Internal.fromDistinctAscList [Index.Internal.elemAt ix ks | ix <- Vector.toList indicesToKeep]- in xs `select` keysToKeep----- | Filter elements, taking into account the corresponding key. Only elements for which --- the predicate is @True@ are kept. -filterWithKey :: (Vector v a, Vector v Int, Vector v Bool, Ord k) - => (k -> a -> Bool) - -> Series v k a - -> Series v k a-{-# INLINE filterWithKey #-}-filterWithKey predicate xs = xs `selectWhere` G.mapWithKey predicate xs----- | \(O(n)\) Only keep elements which are @'Just' v@. -catMaybes :: (Vector v a, Vector v (Maybe a), Vector v Int, Ord k) - => Series v k (Maybe a) -> Series v k a-{-# INLINE catMaybes #-}-catMaybes = G.map fromJust . filter isJust----- | Datatype representing an /inclusive/ range of keys, which can either be bounded--- or unbounded. The canonical ways to construct a 'Range' are to use 'to', 'from', and 'upto':------ >>> 'a' `to` 'z'--- Range (from 'a' to 'z')--- >>> from 'd'--- Range (from 'd')--- >>> upto 'q'--- Range (up to 'q')------ A 'Range' can be used to efficiently select a sub-series with 'select'.-data Range k - = BoundedRange k k- | From k- | UpTo k- deriving (Eq)---instance Show k => Show (Range k) where- show :: Range k -> String- show (BoundedRange start stop) = mconcat ["Range (from ", show start, " to ", show stop, ")"]- show (From start) = mconcat ["Range (from ", show start, ")"]- show (UpTo stop) = mconcat ["Range (up to ", show stop, ")"]----- | Find the keys which are in range. In case of an empty 'Series',--- the returned value is 'Nothing'.-keysInRange :: Ord k => Series v k a -> Range k -> Maybe (k, k)-{-# INLINE keysInRange #-}-keysInRange (MkSeries ks _) rng- = let inrange = inRange rng- in if Set.null inrange - then Nothing- else Just (Set.findMin inrange, Set.findMax inrange)- where- inRange (BoundedRange start stop) = Set.takeWhileAntitone (<= stop) - $ Set.dropWhileAntitone (< start) $ Index.toSet ks- inRange (From start) = Set.dropWhileAntitone (< start) $ Index.toSet ks- inRange (UpTo stop) = Set.takeWhileAntitone (<= stop) $ Index.toSet ks----- | Create a bounded 'Range' which can be used for slicing. This function--- is expected to be used in conjunction with 'select'.------ For unbound ranges, see 'from' and 'upto'.-to :: Ord k => k -> k -> Range k-to k1 k2 = BoundedRange (min k1 k2) (max k1 k2)----- | Create an unbounded 'Range' which can be used for slicing. --- This function is expected to be used in conjunction with 'select'. ------ For bound ranges, see 'to'.-from :: k -> Range k-from = From----- | Create an unbounded 'Range' which can be used for slicing. This function--- is expected to be used in conjunction with 'select'. ------ For bound ranges, see 'to'.-upto :: k -> Range k-upto = UpTo----- | Class for datatypes which can be used to select sub-series using 'select'.------ There are two use-cases for 'select':------ * Bulk random-access (selecting from an 'Index' of keys);--- * Bulk ordered access (selecting from a 'Range' of keys).------ See the documentation for 'select'.-class Selection s where- -- | Select a subseries. There are two main ways to do this.- --- -- The first way to do this is to select a sub-series based on keys:- --- -- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]- -- >>> xs `select` Index.fromList ['a', 'd']- -- index | values- -- ----- | ------- -- 'a' | 10- -- 'd' | 40- --- -- The second way to select a sub-series is to select all keys in a range:- --- -- >>> xs `select` 'b' `to` 'c'- -- index | values- -- ----- | ------- -- 'b' | 20- -- 'c' | 30- --- -- Such ranges can also be unbounded. (i.e. all keys smaller or larger than some key), like so:- --- -- >>> xs `select` upto 'c'- -- index | values- -- ----- | ------- -- 'a' | 10- -- 'b' | 20- -- 'c' | 30- -- >>> xs `select` from 'c'- -- index | values- -- ----- | ------- -- 'c' | 30- -- 'd' | 40- --- -- Note that with 'select', you'll always get a sub-series; if you ask for a key which is not- -- in the series, it'll be ignored:- --- -- >>> xs `select` Index.fromList ['a', 'd', 'e']- -- index | values- -- ----- | ------- -- 'a' | 10- -- 'd' | 40- --- -- See 'require' if you want to ensure that all keys are present.- select :: (Vector v a, Ord k) => Series v k a -> s k -> Series v k a---instance Selection Index where- -- | Select all keys in 'Index' from a series. Keys which are not- -- in the series are ignored.- select :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a- {-# INLINE select #-}- select xs ss- = let selectedKeys = index xs `Index.intersection` ss- -- Surprisingly, using `Vector.backpermute` does not- -- perform as well as `Vector.map (Vector.unsafeIndex vs)`- -- for large Series- in xs `selectSubset` selectedKeys---- | Selecting a sub-series from a 'Set' is a convenience--- function. Internally, the 'Set' is converted to an index first.-instance Selection Set where- select :: (Vector v a, Ord k) => Series v k a -> Set k -> Series v k a- {-# INLINE select #-}- select xs = select xs . Index.fromSet----- | Selecting a sub-series from a list is a convenience--- function. Internally, the list is converted to an index first.-instance Selection [] where- select :: (Vector v a, Ord k) => Series v k a -> [k] -> Series v k a- {-# INLINE select #-}- select xs = select xs . Index.fromList----- | Selecting a sub-series based on a @Range@ is most performant.--- Constructing a @Range@ is most convenient using the 'to' function.-instance Selection Range where- select :: (Vector v a, Ord k) => Series v k a -> Range k -> Series v k a- {-# INLINE select #-}- select series rng = case keysInRange series rng of - Nothing -> mempty- Just (kstart, kstop) -> let indexOf xs k = Index.Internal.findIndex k (index xs)- in slice (series `indexOf` kstart) (1 + indexOf series kstop) series----- | Select a sub-series from a series matching a condition.-selectWhere :: (Vector v a, Vector v Int, Vector v Bool, Ord k) => Series v k a -> Series v k Bool -> Series v k a-{-# INLINE selectWhere #-}-selectWhere xs ys = xs `select` Index.fromSet keysWhereTrue- where- (MkSeries _ cond) = ys `select` index xs- whereValuesAreTrue = Set.fromDistinctAscList $ Vector.toList (Vector.findIndices id cond)- keysWhereTrue = Set.mapMonotonic (`Index.Internal.elemAt` index xs) whereValuesAreTrue----- | Implementation of `select` where the selection keys are known--- to be a subset of the series. This precondition is NOT checked.------ This is a performance optimization and therefore is not normally exposed.-selectSubset :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a-{-# INLINE selectSubset #-}-selectSubset (MkSeries ks vs) ss- -- TODO: - -- Is it possible to scan over the series once- -- while filtering away on keys? Initial attempts did not lead- -- to performance improvements, but I can't imagine that calling- -- `Index.Internal.findIndex` repeatedly is efficient- = MkSeries ss $ Boxed.convert- $ Boxed.map (Vector.unsafeIndex vs . (`Index.Internal.findIndex` ks))- $ Index.toAscVector ss----- | Yield a subseries based on indices. The end index is not included.-slice :: Vector v a- => Int -- ^ Start index- -> Int -- ^ End index, which is not included- -> Series v k a - -> Series v k a-{-# INLINE slice #-}-slice start stop (MkSeries ks vs) - = let stop' = min (Vector.length vs) stop- in MkSeries { index = Index.take (stop' - start) $ Index.drop start ks- , values = Vector.slice start (stop' - start) vs- }--+module Data.Series.Generic.View ( + -- * Accessing a single element + (!), + at, + iat, + + -- * Bulk access + select, + slice, + selectWhere, + selectSubset, + Selection, + + -- * Resizing + require, + requireWith, + filter, + filterWithKey, + catMaybes, + dropIndex, + + -- * Creating and accessing ranges + Range(..), + to, + from, + upto, +) where + + +import Data.Functor ( (<&>) ) +import Data.Series.Index ( Index ) +import qualified Data.Series.Index as Index +import qualified Data.Series.Index.Internal as Index.Internal +import Data.Maybe ( fromJust, isJust ) +import Data.Series.Generic.Definition ( Series(..) ) +import qualified Data.Series.Generic.Definition as G +import Data.Set ( Set ) +import qualified Data.Set as Set +import qualified Data.Vector as Boxed +import Data.Vector.Generic ( Vector ) +import qualified Data.Vector.Generic as Vector + +import Prelude hiding ( filter ) + +-- $setup +-- >>> import qualified Data.Series as Series +-- >>> import qualified Data.Series.Index as Index + +infixr 9 `to` -- Ensure that @to@ binds strongest +infixl 1 `select` + + +-- | \(O(1)\). Extract a single value from a series, by index. +-- An exception is thrown if the index is out-of-bounds. +-- +-- A safer alternative is @iat@, which returns 'Nothing' if the index is +-- out-of-bounds. +(!) :: Vector v a => Series v k a -> Int -> a +(MkSeries _ vs) ! ix = (Vector.!) vs ix + + +-- | \(O(\log n)\). Extract a single value from a series, by key. +at :: (Vector v a, Ord k) => Series v k a -> k -> Maybe a +at (MkSeries ks vs) k = Index.lookupIndex k ks <&> Vector.unsafeIndex vs +{-# INLINABLE at #-} + + +-- | \(O(1)\). Extract a single value from a series, by index. +iat :: Vector v a => Series v k a -> Int -> Maybe a +iat (MkSeries _ vs) = (Vector.!?) vs +{-# INLINABLE iat #-} + + +-- | Require a series with a new index. +-- Contrary to 'select', all keys in @'Index' k@ will be present in the re-indexed series. +require :: (Vector v a, Vector v (Maybe a), Ord k) + => Series v k a -> Index k -> Series v k (Maybe a) +{-# INLINABLE require #-} +require = requireWith (const Nothing) Just + + +-- | Generalization of 'require', which maps missing keys to values. +-- This is particularly useful for 'Vector' instances which don't support 'Maybe', like "Data.Vector.Unboxed". +requireWith :: (Vector v a, Vector v b, Ord k) + => (k -> b) -- ^ Function to apply to keys which are missing from the input series, but required in the input index + -> (a -> b) -- ^ Function to apply to values which are in the input series and input index. + -> Series v k a + -> Index k + -> Series v k b +{-# INLINABLE requireWith #-} +requireWith replacement f xs ss + = let existingKeys = index xs `Index.intersection` ss + newKeys = ss `Index.difference` existingKeys + in G.map f (xs `selectSubset` existingKeys) <> MkSeries newKeys (Vector.fromListN (Index.size newKeys) (replacement <$> Index.toAscList newKeys)) + + +-- | \(O(n)\) Drop the index of a series by replacing it with an @Int@-based index. Values will +-- be indexed from 0. +dropIndex :: Series v k a -> Series v Int a +{-# INLINABLE dropIndex #-} +dropIndex (MkSeries ks vs) = MkSeries (Index.Internal.fromDistinctAscList [0..Index.size ks - 1]) vs + + +-- | Filter elements. Only elements for which the predicate is @True@ are kept. +-- Notice that the filtering is done on the values, not on the keys; see 'filterWithKey' +-- to filter while taking keys into account. +filter :: (Vector v a, Vector v Int, Ord k) + => (a -> Bool) -> Series v k a -> Series v k a +{-# INLINABLE filter #-} +filter predicate xs@(MkSeries ks vs) + = let indicesToKeep = Vector.findIndices predicate vs + keysToKeep = Index.Internal.fromDistinctAscList [Index.Internal.elemAt ix ks | ix <- Vector.toList indicesToKeep] + in xs `select` keysToKeep + + +-- | Filter elements, taking into account the corresponding key. Only elements for which +-- the predicate is @True@ are kept. +filterWithKey :: (Vector v a, Vector v Int, Vector v Bool, Ord k) + => (k -> a -> Bool) + -> Series v k a + -> Series v k a +{-# INLINABLE filterWithKey #-} +filterWithKey predicate xs = xs `selectWhere` G.mapWithKey predicate xs + + +-- | \(O(n)\) Only keep elements which are @'Just' v@. +catMaybes :: (Vector v a, Vector v (Maybe a), Vector v Int, Ord k) + => Series v k (Maybe a) -> Series v k a +{-# INLINABLE catMaybes #-} +catMaybes = G.map fromJust . filter isJust + + +-- | Datatype representing an /inclusive/ range of keys, which can either be bounded +-- or unbounded. The canonical ways to construct a 'Range' are to use 'to', 'from', and 'upto': +-- +-- >>> 'a' `to` 'z' +-- Range (from 'a' to 'z') +-- >>> from 'd' +-- Range (from 'd') +-- >>> upto 'q' +-- Range (up to 'q') +-- +-- A 'Range' can be used to efficiently select a sub-series with 'select'. +data Range k + = BoundedRange k k + | From k + | UpTo k + deriving (Eq) + + +instance Show k => Show (Range k) where + show :: Range k -> String + show (BoundedRange start stop) = mconcat ["Range (from ", show start, " to ", show stop, ")"] + show (From start) = mconcat ["Range (from ", show start, ")"] + show (UpTo stop) = mconcat ["Range (up to ", show stop, ")"] + + +-- | Find the keys which are in range. In case of an empty 'Series', +-- the returned value is 'Nothing'. +keysInRange :: Ord k => Series v k a -> Range k -> Maybe (k, k) +{-# INLINABLE keysInRange #-} +keysInRange (MkSeries ks _) rng + = let inrange = inRange rng + in if Set.null inrange + then Nothing + else Just (Set.findMin inrange, Set.findMax inrange) + where + inRange (BoundedRange start stop) = Set.takeWhileAntitone (<= stop) + $ Set.dropWhileAntitone (< start) $ Index.toSet ks + inRange (From start) = Set.dropWhileAntitone (< start) $ Index.toSet ks + inRange (UpTo stop) = Set.takeWhileAntitone (<= stop) $ Index.toSet ks + + +-- | Create a bounded 'Range' which can be used for slicing. This function +-- is expected to be used in conjunction with 'select'. +-- +-- For unbound ranges, see 'from' and 'upto'. +to :: Ord k => k -> k -> Range k +to k1 k2 = BoundedRange (min k1 k2) (max k1 k2) + + +-- | Create an unbounded 'Range' which can be used for slicing. +-- This function is expected to be used in conjunction with 'select'. +-- +-- For bound ranges, see 'to'. +from :: k -> Range k +from = From + + +-- | Create an unbounded 'Range' which can be used for slicing. This function +-- is expected to be used in conjunction with 'select'. +-- +-- For bound ranges, see 'to'. +upto :: k -> Range k +upto = UpTo + + +-- | Class for datatypes which can be used to select sub-series using 'select'. +-- +-- There are two use-cases for 'select': +-- +-- * Bulk random-access (selecting from an 'Index' of keys); +-- * Bulk ordered access (selecting from a 'Range' of keys). +-- +-- See the documentation for 'select'. +class Selection s where + -- | Select a subseries. There are two main ways to do this. + -- + -- The first way to do this is to select a sub-series based on keys: + -- + -- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)] + -- >>> xs `select` Index.fromList ['a', 'd'] + -- index | values + -- ----- | ------ + -- 'a' | 10 + -- 'd' | 40 + -- + -- The second way to select a sub-series is to select all keys in a range: + -- + -- >>> xs `select` 'b' `to` 'c' + -- index | values + -- ----- | ------ + -- 'b' | 20 + -- 'c' | 30 + -- + -- Such ranges can also be unbounded. (i.e. all keys smaller or larger than some key), like so: + -- + -- >>> xs `select` upto 'c' + -- index | values + -- ----- | ------ + -- 'a' | 10 + -- 'b' | 20 + -- 'c' | 30 + -- >>> xs `select` from 'c' + -- index | values + -- ----- | ------ + -- 'c' | 30 + -- 'd' | 40 + -- + -- Note that with 'select', you'll always get a sub-series; if you ask for a key which is not + -- in the series, it'll be ignored: + -- + -- >>> xs `select` Index.fromList ['a', 'd', 'e'] + -- index | values + -- ----- | ------ + -- 'a' | 10 + -- 'd' | 40 + -- + -- See 'require' if you want to ensure that all keys are present. + select :: (Vector v a, Ord k) => Series v k a -> s k -> Series v k a + + +instance Selection Index where + -- | Select all keys in 'Index' from a series. Keys which are not + -- in the series are ignored. + select :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a + {-# INLINABLE select #-} + select xs ss + = let selectedKeys = index xs `Index.intersection` ss + -- Surprisingly, using `Vector.backpermute` does not + -- perform as well as `Vector.map (Vector.unsafeIndex vs)` + -- for large Series + in xs `selectSubset` selectedKeys + + +-- | Selecting a sub-series from a 'Set' is a convenience +-- function. Internally, the 'Set' is converted to an index first. +instance Selection Set where + select :: (Vector v a, Ord k) => Series v k a -> Set k -> Series v k a + {-# INLINABLE select #-} + select xs = select xs . Index.fromSet + + +-- | Selecting a sub-series from a list is a convenience +-- function. Internally, the list is converted to an index first. +instance Selection [] where + select :: (Vector v a, Ord k) => Series v k a -> [k] -> Series v k a + {-# INLINABLE select #-} + select xs = select xs . Index.fromList + + +-- | Selecting a sub-series based on a @Range@ is most performant. +-- Constructing a @Range@ is most convenient using the 'to' function. +instance Selection Range where + select :: (Vector v a, Ord k) => Series v k a -> Range k -> Series v k a + {-# INLINABLE select #-} + select series rng = case keysInRange series rng of + Nothing -> mempty + Just (kstart, kstop) -> let indexOf xs k = Index.Internal.findIndex k (index xs) + in slice (series `indexOf` kstart) (1 + series `indexOf` kstop) series + + +-- | Select a sub-series from a series matching a condition. +selectWhere :: (Vector v a, Vector v Int, Vector v Bool, Ord k) => Series v k a -> Series v k Bool -> Series v k a +{-# INLINABLE selectWhere #-} +selectWhere xs ys = xs `select` Index.fromSet keysWhereTrue + where + (MkSeries _ cond) = ys `select` index xs + whereValuesAreTrue = Set.fromDistinctAscList $ Vector.toList (Vector.findIndices id cond) + keysWhereTrue = Set.mapMonotonic (`Index.Internal.elemAt` index xs) whereValuesAreTrue + + +-- | Implementation of `select` where the selection keys are known +-- to be a subset of the series. This precondition is NOT checked. +-- +-- This is a performance optimization and therefore is not normally exposed. +selectSubset :: (Vector v a, Ord k) => Series v k a -> Index k -> Series v k a +{-# INLINABLE selectSubset #-} +selectSubset (MkSeries ks vs) ss + -- TODO: + -- Is it possible to scan over the series once + -- while filtering away on keys? Initial attempts did not lead + -- to performance improvements, but I can't imagine that calling + -- `Index.Internal.findIndex` repeatedly is efficient + -- + -- Maybe use Data.Series.Index.indexed to traverse the index once? + = MkSeries ss $ Boxed.convert + $ Boxed.map (Vector.unsafeIndex vs . (`Index.Internal.findIndex` ks)) + $ Index.toAscVector ss + + +-- | \(O(\log n)\) Yield a subseries based on integer indices. The end index is not included. +slice :: Vector v a + => Int -- ^ Start index + -> Int -- ^ End index, which is not included + -> Series v k a + -> Series v k a +{-# INLINABLE slice #-} +slice start stop (MkSeries ks vs) + = let stop' = min (Vector.length vs) stop + -- Index.take is O(log n) while Vector.slice is O(1) + in MkSeries { index = Index.take (stop' - start) $ Index.drop start ks + , values = Vector.slice start (stop' - start) vs + } + +
src/Data/Series/Generic/Zip.hs view
@@ -1,463 +1,463 @@-module Data.Series.Generic.Zip (- zipWith, zipWithMatched, zipWithKey,- zipWith3, zipWithMatched3, zipWithKey3,- replace, (|->), (<-|),- - -- * Generalized zipping with strategies- zipWithStrategy,- zipWithStrategy3,- ZipStrategy,- skipStrategy,- mapStrategy,- constStrategy,-- -- * Special case of zipping monoids- zipWithMonoid,- esum, eproduct,-- -- * Unzipping- unzip, unzip3,-) where--import qualified Data.Map.Strict as Map-import Data.Monoid ( Sum(..), Product(..) )-import Data.Series.Generic.Definition ( Series(MkSeries, index, values) )-import qualified Data.Series.Generic.Definition as G-import Data.Series.Generic.View ( selectSubset, requireWith )-import Data.Vector.Generic ( Vector )-import qualified Data.Vector.Generic as Vector-import qualified Data.Series.Index as Index-import qualified Data.Series.Index.Internal as Index.Internal-import Prelude hiding ( zipWith, zipWith3, unzip, unzip3 ) ---- $setup--- >>> import qualified Data.Series as Series--infix 6 |->, <-|---- | Apply a function elementwise to two series, matching elements--- based on their keys. For keys present only in the left or right series, --- the value 'Nothing' is returned.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWith (+) xs ys--- index | values--- ----- | --------- "alpha" | Just 10--- "beta" | Just 12--- "delta" | Nothing--- "gamma" | Nothing------ To only combine elements where keys are in both series, see 'zipWithMatched'-zipWith :: (Vector v a, Vector v b, Vector v c, Vector v (Maybe c), Ord k) - => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k (Maybe c)-zipWith f left right- = let matched = zipWithMatched f left right- matchedKeys = index matched- allKeys = index left `Index.union` index right- unmatchedKeys = allKeys `Index.difference` matchedKeys- unmatched = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing)- in G.map Just matched <> unmatched-{-# INLINE zipWith #-}----- | Apply a function elementwise to three series, matching elements--- based on their keys. For keys present only in the left or right series, --- the value 'Nothing' is returned.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]--- >>> zipWith3 (\x y z -> x + y + z) xs ys zs--- index | values--- ----- | --------- "alpha" | Just 30--- "beta" | Nothing--- "delta" | Nothing--- "epsilon" | Nothing--- "gamma" | Nothing------ To only combine elements where keys are in all series, see 'zipWithMatched3'-zipWith3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v (Maybe d), Ord k) - => (a -> b -> c -> d) - -> Series v k a - -> Series v k b - -> Series v k c - -> Series v k (Maybe d)-zipWith3 f left center right- = let matched = zipWithMatched3 f left center right- matchedKeys = index matched- allKeys = index left `Index.union` index center `Index.union` index right- unmatchedKeys = allKeys `Index.difference` matchedKeys- unmatched = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing)- in G.map Just matched <> unmatched-{-# INLINE zipWith3 #-}------ | Apply a function elementwise to two series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWithMatched (+) xs ys--- index | values--- ----- | --------- "alpha" | 10--- "beta" | 12------ To combine elements where keys are in either series, see 'zipWith'. To combine--- three series, see 'zipWithMatched3'.-zipWithMatched :: (Vector v a, Vector v b, Vector v c, Ord k) - => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k c-zipWithMatched f left right- = let matchedKeys = index left `Index.intersection` index right- -- Recall that `selectSubset` is a performance optimization- -- and is generally unsafe to use; however, in this case, we know- -- that `matchedKeys` are subsets of the index of both series- (MkSeries _ !xs) = left `selectSubset` matchedKeys- (MkSeries _ !ys) = right `selectSubset` matchedKeys- -- The following construction relies on the fact that keys are always sorted- in MkSeries matchedKeys $ Vector.zipWith f xs ys-{-# INLINE zipWithMatched #-}----- | Apply a function elementwise to three series, matching elements--- based on their keys. Keys not present in all three series are dropped.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ]--- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs--- index | values--- ----- | --------- "alpha" | 30-zipWithMatched3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) - => (a -> b -> c -> d) - -> Series v k a - -> Series v k b - -> Series v k c- -> Series v k d-zipWithMatched3 f left center right- = let matchedKeys = index left `Index.intersection` index center `Index.intersection` index right- -- Recall that `selectSubset` is a performance optimization- -- and is generally unsafe to use; however, in this case, we know- -- that `matchedKeys` are subsets of the index of all series- (MkSeries _ !xs) = left `selectSubset` matchedKeys- (MkSeries _ !ys) = center `selectSubset` matchedKeys- (MkSeries _ !zs) = right `selectSubset` matchedKeys- -- The following construction relies on the fact that keys are always sorted- in MkSeries matchedKeys $ Vector.zipWith3 f xs ys zs-{-# INLINE zipWithMatched3 #-}----- | Apply a function elementwise to two series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.--- --- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWithKey (\k x y -> length k + x + y) xs ys--- index | values--- ----- | --------- "alpha" | 15--- "beta" | 16------ To combine elements where keys are in either series, see 'zipWith'-zipWithKey :: (Vector v a, Vector v b, Vector v c, Vector v k, Ord k) - => (k -> a -> b -> c) -> Series v k a -> Series v k b -> Series v k c-zipWithKey f left right- = let matchedKeys = index left `Index.intersection` index right- -- Recall that `selectSubset` is a performance optimization- -- and is generally unsafe to use; however, in this case, we know- -- that `matchedKeys` are subsets of the index of both series- (MkSeries _ xs) = left `selectSubset` matchedKeys- (MkSeries _ ys) = right `selectSubset` matchedKeys- ks = Index.toAscVector matchedKeys- -- The following construction relies on the fact that keys are always sorted- in MkSeries matchedKeys $ Vector.zipWith3 f ks xs ys-{-# INLINE zipWithKey #-}----- | Apply a function elementwise to three series, matching elements--- based on their keys. Keys not present in all series are dropped.--- --- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("beta", 7), ("delta", 5) ]--- >>> zipWithKey3 (\k x y z -> length k + x + y + z) xs ys zs--- index | values--- ----- | --------- "alpha" | 35--- "beta" | 23--zipWithKey3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v k, Ord k) - => (k -> a -> b -> c -> d) - -> Series v k a - -> Series v k b - -> Series v k c- -> Series v k d-zipWithKey3 f left center right- = let matchedKeys = index left `Index.intersection` index right- -- Recall that `selectSubset` is a performance optimization- -- and is generally unsafe to use; however, in this case, we know- -- that `matchedKeys` are subsets of the index of all series- (MkSeries _ xs) = left `selectSubset` matchedKeys- (MkSeries _ ys) = center `selectSubset` matchedKeys- (MkSeries _ zs) = right `selectSubset` matchedKeys- ks = Index.toAscVector matchedKeys- -- The following construction relies on the fact that keys are always sorted- in MkSeries matchedKeys $ Vector.zipWith4 f ks xs ys zs-{-# INLINE zipWithKey3 #-}----- | Replace values from the right series with values from the left series at matching keys.--- Keys in the right series but not in the right series are unaffected.-replace :: (Vector v a, Vector v Int, Ord k) - => Series v k a -> Series v k a -> Series v k a-{-# INLINE replace #-}-xs `replace` ys - = let keysToReplace = index xs `Index.intersection` index ys- iixs = Index.toAscVector $ Index.Internal.mapMonotonic (\k -> Index.Internal.findIndex k (index ys)) keysToReplace- in MkSeries (index ys) $ Vector.update_ (values ys) iixs (values (xs `selectSubset` keysToReplace))----- | Infix version of 'replace'-(|->) :: (Vector v a, Vector v Int, Ord k)- => Series v k a -> Series v k a -> Series v k a-{-# INLINE (|->) #-}-(|->) = replace----- | Flipped version of '|->',-(<-|) :: (Vector v a, Vector v Int, Ord k) - => Series v k a -> Series v k a -> Series v k a-{-# INLINE (<-|) #-}-(<-|) = flip replace----- | A 'ZipStrategy' is a function which is used to decide what to do when a key is missing from one--- of two 'Series' being zipped together with 'zipWithStrategy'.------ If a 'ZipStrategy' returns 'Nothing', the key is dropped.--- If a 'ZipStrategy' returns @'Just' v@ for key @k@, then the value @v@ is inserted at key @k@.------ For example, the most basic 'ZipStrategy' is to skip over any key which is missing from the other series.--- Such a strategy can be written as @skip key value = 'Nothing'@ (see 'skipStrategy').-type ZipStrategy k a b = (k -> a -> Maybe b)----- | This 'ZipStrategy' drops keys which are not present in both 'Series'.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWithStrategy (+) skipStrategy skipStrategy xs ys--- index | values--- ----- | --------- "alpha" | 10--- "beta" | 12-skipStrategy :: ZipStrategy k a b-skipStrategy _ _ = Nothing-{-# INLINE skipStrategy #-}----- | This 'ZipStrategy' sets the value at keys which are not present in both 'Series' --- to the some mapping from the value present in one of the series. See the example below.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 5::Int), ("beta", 6), ("delta", 7) ]--- >>> zipWithStrategy (+) (mapStrategy id) (mapStrategy (*10)) xs ys--- index | values--- ----- | --------- "alpha" | 5--- "beta" | 7--- "delta" | 70--- "gamma" | 2-mapStrategy :: (a -> b) -> ZipStrategy k a b-mapStrategy f _ x = Just (f x)-{-# INLINE mapStrategy #-}----- | This 'ZipStrategy' sets a constant value at keys which are not present in both 'Series'.------ >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ]--- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ]--- >>> zipWith (+) xs ys--- index | values--- ----- | --------- "alpha" | Just 10--- "beta" | Just 12--- "delta" | Nothing--- "gamma" | Nothing--- >>> zipWithStrategy (+) (constStrategy (-100)) (constStrategy 200) xs ys--- index | values--- ----- | --------- "alpha" | 10--- "beta" | 12--- "delta" | 200--- "gamma" | -100-constStrategy :: b -> ZipStrategy k a b-constStrategy v = mapStrategy (const v)-{-# INLINE constStrategy #-}----- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both.------ Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ --- than using @'zipWithStrategy' f skipStrategy skipStrategy@.-zipWithStrategy :: (Vector v a, Vector v b, Vector v c, Ord k) - => (a -> b -> c) -- ^ Function to combine values when present in both series- -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right- -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left- -> Series v k a- -> Series v k b - -> Series v k c-zipWithStrategy f whenLeft whenRight left right - = let onlyLeftKeys = index left `Index.difference` index right- onlyRightKeys = index right `Index.difference` index left- -- Recall that `selectSubset` is a performance optimization- -- and is generally unsafe to use; however, in this case, we know- -- that `matchedKeys` are subsets of the index of both series- leftZip = applyStrategy whenLeft $ left `selectSubset` onlyLeftKeys- rightZip = applyStrategy whenRight $ right `selectSubset` onlyRightKeys- - in zipWithMatched f left right <> leftZip <> rightZip- where- -- Application of the 'ZipStrategy' is done on a `Map` rather than- -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to- -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`. - applyStrategy strat = G.toSeries - . Map.mapMaybeWithKey strat- . G.fromSeries-{-# INLINE zipWithStrategy #-}----- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is --- present in one of the 'Series' but not all of the others.------ Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ --- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.-zipWithStrategy3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) - => (a -> b -> c -> d) -- ^ Function to combine values when present in all series- -> ZipStrategy k a d -- ^ Strategy for when the key is in the left series but not in all the others- -> ZipStrategy k b d -- ^ Strategy for when the key is in the center series but not in all the others- -> ZipStrategy k c d -- ^ Strategy for when the key is in the right series but not in all the others- -> Series v k a- -> Series v k b - -> Series v k c- -> Series v k d-zipWithStrategy3 f whenLeft whenCenter whenRight left center right - = let onlyLeftKeys = index left `Index.difference` (index center `Index.union` index right)- onlyCenterKeys = index center `Index.difference` (index left `Index.union` index right)- onlyRightKeys = index right `Index.difference` (index center `Index.union` index left)- -- Recall that `selectSubset` is a performance optimization- -- and is generally unsafe to use; however, in this case, we know- -- that `matchedKeys` are subsets of the index of all series- leftZip = applyStrategy whenLeft $ left `selectSubset` onlyLeftKeys- centerZip = applyStrategy whenCenter $ center `selectSubset` onlyCenterKeys- rightZip = applyStrategy whenRight $ right `selectSubset` onlyRightKeys- - in zipWithMatched3 f left center right <> leftZip <> centerZip <> rightZip- where- -- Application of the 'ZipStrategy' is done on a `Map` rather than- -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to- -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`. - applyStrategy strat = G.toSeries - . Map.mapMaybeWithKey strat- . G.fromSeries-{-# INLINE zipWithStrategy3 #-}----- | Zip two 'Series' with a combining function. The value for keys which are missing from--- either 'Series' is replaced with the appropriate 'mempty' value.------ >>> import Data.Monoid ( Sum(..) )--- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]--- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]--- >>> zipWith (<>) xs ys--- index | values--- ----- | --------- "2023-01-01" | Just (Sum {getSum = 6})--- "2023-01-02" | Nothing--- "2023-01-03" | Nothing--- >>> zipWithMonoid (<>) xs ys--- index | values--- ----- | --------- "2023-01-01" | Sum {getSum = 6}--- "2023-01-02" | Sum {getSum = 2}--- "2023-01-03" | Sum {getSum = 7}-zipWithMonoid :: ( Monoid a, Monoid b- , Vector v a, Vector v b, Vector v c- , Ord k- ) - => (a -> b -> c)- -> Series v k a- -> Series v k b - -> Series v k c-zipWithMonoid f left right - = let fullindex = index left `Index.union` index right- (MkSeries ix ls) = requireWith (const mempty) id left fullindex- (MkSeries _ rs) = requireWith (const mempty) id right fullindex - in MkSeries ix $ Vector.zipWith f ls rs-{-# INLINE zipWithMonoid #-}----- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. ------ >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]--- >>> xs `esum` ys--- index | values--- ----- | --------- "2023-01-01" | 6--- "2023-01-02" | 2--- "2023-01-03" | 7-esum :: (Ord k, Num a, Vector v a, Vector v (Sum a)) - => Series v k a - -> Series v k a- -> Series v k a-esum ls rs = G.map getSum $ zipWithMonoid (<>) (G.map Sum ls) (G.map Sum rs)-{-# INLINE esum #-}----- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. ------ >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]--- >>> xs `eproduct` ys--- index | values--- ----- | --------- "2023-01-01" | 10--- "2023-01-02" | 3--- "2023-01-03" | 7-eproduct :: (Ord k, Num a, Vector v a, Vector v (Product a)) - => Series v k a - -> Series v k a- -> Series v k a-eproduct ls rs = G.map getProduct $ zipWithMonoid (<>) (G.map Product ls) (G.map Product rs)-{-# INLINE eproduct #-}----- | \(O(n)\) Unzip a 'Series' of 2-tuples.-unzip :: (Vector v a, Vector v b, Vector v (a, b)) - => Series v k (a, b)- -> ( Series v k a- , Series v k b- )-unzip (MkSeries ix vs) - = let (left, right) = Vector.unzip vs- in (MkSeries ix left, MkSeries ix right)-{-# INLINE unzip #-}----- | \(O(n)\) Unzip a 'Series' of 3-tuples.-unzip3 :: (Vector v a, Vector v b, Vector v c, Vector v (a, b, c)) - => Series v k (a, b, c)- -> ( Series v k a- , Series v k b- , Series v k c- )-unzip3 (MkSeries ix vs) - = let (left, center, right) = Vector.unzip3 vs- in (MkSeries ix left, MkSeries ix center, MkSeries ix right)-{-# INLINE unzip3 #-}+module Data.Series.Generic.Zip ( + zipWith, zipWithMatched, zipWithKey, + zipWith3, zipWithMatched3, zipWithKey3, + replace, (|->), (<-|), + + -- * Generalized zipping with strategies + zipWithStrategy, + zipWithStrategy3, + ZipStrategy, + skipStrategy, + mapStrategy, + constStrategy, + + -- * Special case of zipping monoids + zipWithMonoid, + esum, eproduct, + + -- * Unzipping + unzip, unzip3, +) where + +import qualified Data.Map.Strict as Map +import Data.Monoid ( Sum(..), Product(..) ) +import Data.Series.Generic.Definition ( Series(MkSeries, index, values) ) +import qualified Data.Series.Generic.Definition as G +import Data.Series.Generic.View ( selectSubset, requireWith ) +import Data.Vector.Generic ( Vector ) +import qualified Data.Vector.Generic as Vector +import qualified Data.Series.Index as Index +import qualified Data.Series.Index.Internal as Index.Internal +import Prelude hiding ( zipWith, zipWith3, unzip, unzip3 ) + +-- $setup +-- >>> import qualified Data.Series as Series + +infix 6 |->, <-| + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. For keys present only in the left or right series, +-- the value 'Nothing' is returned. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWith (+) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | Just 10 +-- "beta" | Just 12 +-- "delta" | Nothing +-- "gamma" | Nothing +-- +-- To only combine elements where keys are in both series, see 'zipWithMatched' +zipWith :: (Vector v a, Vector v b, Vector v c, Vector v (Maybe c), Ord k) + => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k (Maybe c) +zipWith f left right + = let matched = zipWithMatched f left right + matchedKeys = index matched + allKeys = index left `Index.union` index right + unmatchedKeys = allKeys `Index.difference` matchedKeys + unmatched = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing) + in G.map Just matched <> unmatched +{-# INLINABLE zipWith #-} + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. For keys present only in the left or right series, +-- the value 'Nothing' is returned. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ] +-- >>> zipWith3 (\x y z -> x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- "alpha" | Just 30 +-- "beta" | Nothing +-- "delta" | Nothing +-- "epsilon" | Nothing +-- "gamma" | Nothing +-- +-- To only combine elements where keys are in all series, see 'zipWithMatched3' +zipWith3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v (Maybe d), Ord k) + => (a -> b -> c -> d) + -> Series v k a + -> Series v k b + -> Series v k c + -> Series v k (Maybe d) +zipWith3 f left center right + = let matched = zipWithMatched3 f left center right + matchedKeys = index matched + allKeys = index left `Index.union` index center `Index.union` index right + unmatchedKeys = allKeys `Index.difference` matchedKeys + unmatched = MkSeries unmatchedKeys (Vector.replicate (Index.size unmatchedKeys) Nothing) + in G.map Just matched <> unmatched +{-# INLINABLE zipWith3 #-} + + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWithMatched (+) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 10 +-- "beta" | 12 +-- +-- To combine elements where keys are in either series, see 'zipWith'. To combine +-- three series, see 'zipWithMatched3'. +zipWithMatched :: (Vector v a, Vector v b, Vector v c, Ord k) + => (a -> b -> c) -> Series v k a -> Series v k b -> Series v k c +zipWithMatched f left right + = let matchedKeys = index left `Index.intersection` index right + -- Recall that `selectSubset` is a performance optimization + -- and is generally unsafe to use; however, in this case, we know + -- that `matchedKeys` are subsets of the index of both series + (MkSeries _ !xs) = left `selectSubset` matchedKeys + (MkSeries _ !ys) = right `selectSubset` matchedKeys + -- The following construction relies on the fact that keys are always sorted + in MkSeries matchedKeys $ Vector.zipWith f xs ys +{-# INLINABLE zipWithMatched #-} + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. Keys not present in all three series are dropped. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("delta", 13), ("epsilon", 6) ] +-- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- "alpha" | 30 +zipWithMatched3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) + => (a -> b -> c -> d) + -> Series v k a + -> Series v k b + -> Series v k c + -> Series v k d +zipWithMatched3 f left center right + = let matchedKeys = index left `Index.intersection` index center `Index.intersection` index right + -- Recall that `selectSubset` is a performance optimization + -- and is generally unsafe to use; however, in this case, we know + -- that `matchedKeys` are subsets of the index of all series + (MkSeries _ !xs) = left `selectSubset` matchedKeys + (MkSeries _ !ys) = center `selectSubset` matchedKeys + (MkSeries _ !zs) = right `selectSubset` matchedKeys + -- The following construction relies on the fact that keys are always sorted + in MkSeries matchedKeys $ Vector.zipWith3 f xs ys zs +{-# INLINABLE zipWithMatched3 #-} + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWithKey (\k x y -> length k + x + y) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 15 +-- "beta" | 16 +-- +-- To combine elements where keys are in either series, see 'zipWith' +zipWithKey :: (Vector v a, Vector v b, Vector v c, Vector v k, Ord k) + => (k -> a -> b -> c) -> Series v k a -> Series v k b -> Series v k c +zipWithKey f left right + = let matchedKeys = index left `Index.intersection` index right + -- Recall that `selectSubset` is a performance optimization + -- and is generally unsafe to use; however, in this case, we know + -- that `matchedKeys` are subsets of the index of both series + (MkSeries _ xs) = left `selectSubset` matchedKeys + (MkSeries _ ys) = right `selectSubset` matchedKeys + ks = Index.toAscVector matchedKeys + -- The following construction relies on the fact that keys are always sorted + in MkSeries matchedKeys $ Vector.zipWith3 f ks xs ys +{-# INLINABLE zipWithKey #-} + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. Keys not present in all series are dropped. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> let zs = Series.fromList [ ("alpha", 20::Int), ("beta", 7), ("delta", 5) ] +-- >>> zipWithKey3 (\k x y z -> length k + x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- "alpha" | 35 +-- "beta" | 23 + +zipWithKey3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Vector v k, Ord k) + => (k -> a -> b -> c -> d) + -> Series v k a + -> Series v k b + -> Series v k c + -> Series v k d +zipWithKey3 f left center right + = let matchedKeys = index left `Index.intersection` index right + -- Recall that `selectSubset` is a performance optimization + -- and is generally unsafe to use; however, in this case, we know + -- that `matchedKeys` are subsets of the index of all series + (MkSeries _ xs) = left `selectSubset` matchedKeys + (MkSeries _ ys) = center `selectSubset` matchedKeys + (MkSeries _ zs) = right `selectSubset` matchedKeys + ks = Index.toAscVector matchedKeys + -- The following construction relies on the fact that keys are always sorted + in MkSeries matchedKeys $ Vector.zipWith4 f ks xs ys zs +{-# INLINABLE zipWithKey3 #-} + + +-- | Replace values from the right series with values from the left series at matching keys. +-- Keys in the right series but not in the right series are unaffected. +replace :: (Vector v a, Vector v Int, Ord k) + => Series v k a -> Series v k a -> Series v k a +{-# INLINABLE replace #-} +xs `replace` ys + = let keysToReplace = index xs `Index.intersection` index ys + iixs = Index.toAscVector $ Index.Internal.mapMonotonic (\k -> Index.Internal.findIndex k (index ys)) keysToReplace + in MkSeries (index ys) $ Vector.update_ (values ys) iixs (values (xs `selectSubset` keysToReplace)) + + +-- | Infix version of 'replace' +(|->) :: (Vector v a, Vector v Int, Ord k) + => Series v k a -> Series v k a -> Series v k a +{-# INLINABLE (|->) #-} +(|->) = replace + + +-- | Flipped version of '|->', +(<-|) :: (Vector v a, Vector v Int, Ord k) + => Series v k a -> Series v k a -> Series v k a +{-# INLINABLE (<-|) #-} +(<-|) = flip replace + + +-- | A 'ZipStrategy' is a function which is used to decide what to do when a key is missing from one +-- of two 'Series' being zipped together with 'zipWithStrategy'. +-- +-- If a 'ZipStrategy' returns 'Nothing', the key is dropped. +-- If a 'ZipStrategy' returns @'Just' v@ for key @k@, then the value @v@ is inserted at key @k@. +-- +-- For example, the most basic 'ZipStrategy' is to skip over any key which is missing from the other series. +-- Such a strategy can be written as @skip key value = 'Nothing'@ (see 'skipStrategy'). +type ZipStrategy k a b = (k -> a -> Maybe b) + + +-- | This 'ZipStrategy' drops keys which are not present in both 'Series'. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWithStrategy (+) skipStrategy skipStrategy xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 10 +-- "beta" | 12 +skipStrategy :: ZipStrategy k a b +skipStrategy _ _ = Nothing +{-# INLINABLE skipStrategy #-} + + +-- | This 'ZipStrategy' sets the value at keys which are not present in both 'Series' +-- to the some mapping from the value present in one of the series. See the example below. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 5::Int), ("beta", 6), ("delta", 7) ] +-- >>> zipWithStrategy (+) (mapStrategy id) (mapStrategy (*10)) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 5 +-- "beta" | 7 +-- "delta" | 70 +-- "gamma" | 2 +mapStrategy :: (a -> b) -> ZipStrategy k a b +mapStrategy f _ x = Just (f x) +{-# INLINABLE mapStrategy #-} + + +-- | This 'ZipStrategy' sets a constant value at keys which are not present in both 'Series'. +-- +-- >>> let xs = Series.fromList [ ("alpha", 0::Int), ("beta", 1), ("gamma", 2) ] +-- >>> let ys = Series.fromList [ ("alpha", 10::Int), ("beta", 11), ("delta", 13) ] +-- >>> zipWith (+) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | Just 10 +-- "beta" | Just 12 +-- "delta" | Nothing +-- "gamma" | Nothing +-- >>> zipWithStrategy (+) (constStrategy (-100)) (constStrategy 200) xs ys +-- index | values +-- ----- | ------ +-- "alpha" | 10 +-- "beta" | 12 +-- "delta" | 200 +-- "gamma" | -100 +constStrategy :: b -> ZipStrategy k a b +constStrategy v = mapStrategy (const v) +{-# INLINABLE constStrategy #-} + + +-- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both. +-- +-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ +-- than using @'zipWithStrategy' f skipStrategy skipStrategy@. +zipWithStrategy :: (Vector v a, Vector v b, Vector v c, Ord k) + => (a -> b -> c) -- ^ Function to combine values when present in both series + -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right + -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left + -> Series v k a + -> Series v k b + -> Series v k c +zipWithStrategy f whenLeft whenRight left right + = let onlyLeftKeys = index left `Index.difference` index right + onlyRightKeys = index right `Index.difference` index left + -- Recall that `selectSubset` is a performance optimization + -- and is generally unsafe to use; however, in this case, we know + -- that `matchedKeys` are subsets of the index of both series + leftZip = applyStrategy whenLeft $ left `selectSubset` onlyLeftKeys + rightZip = applyStrategy whenRight $ right `selectSubset` onlyRightKeys + + in zipWithMatched f left right <> leftZip <> rightZip + where + -- Application of the 'ZipStrategy' is done on a `Map` rather than + -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to + -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`. + applyStrategy strat = G.toSeries + . Map.mapMaybeWithKey strat + . G.fromSeries +{-# INLINABLE zipWithStrategy #-} + + +-- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is +-- present in one of the 'Series' but not all of the others. +-- +-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ +-- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@. +zipWithStrategy3 :: (Vector v a, Vector v b, Vector v c, Vector v d, Ord k) + => (a -> b -> c -> d) -- ^ Function to combine values when present in all series + -> ZipStrategy k a d -- ^ Strategy for when the key is in the left series but not in all the others + -> ZipStrategy k b d -- ^ Strategy for when the key is in the center series but not in all the others + -> ZipStrategy k c d -- ^ Strategy for when the key is in the right series but not in all the others + -> Series v k a + -> Series v k b + -> Series v k c + -> Series v k d +zipWithStrategy3 f whenLeft whenCenter whenRight left center right + = let onlyLeftKeys = index left `Index.difference` (index center `Index.union` index right) + onlyCenterKeys = index center `Index.difference` (index left `Index.union` index right) + onlyRightKeys = index right `Index.difference` (index center `Index.union` index left) + -- Recall that `selectSubset` is a performance optimization + -- and is generally unsafe to use; however, in this case, we know + -- that `matchedKeys` are subsets of the index of all series + leftZip = applyStrategy whenLeft $ left `selectSubset` onlyLeftKeys + centerZip = applyStrategy whenCenter $ center `selectSubset` onlyCenterKeys + rightZip = applyStrategy whenRight $ right `selectSubset` onlyRightKeys + + in zipWithMatched3 f left center right <> leftZip <> centerZip <> rightZip + where + -- Application of the 'ZipStrategy' is done on a `Map` rather than + -- the 'Series' directly to keep the type contraints of `zipWithStrategy` to + -- a minimum. Recall that unboxed 'Series' cannot contain `Maybe a`. + applyStrategy strat = G.toSeries + . Map.mapMaybeWithKey strat + . G.fromSeries +{-# INLINABLE zipWithStrategy3 #-} + + +-- | Zip two 'Series' with a combining function. The value for keys which are missing from +-- either 'Series' is replaced with the appropriate 'mempty' value. +-- +-- >>> import Data.Monoid ( Sum(..) ) +-- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ] +-- >>> zipWith (<>) xs ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | Just (Sum {getSum = 6}) +-- "2023-01-02" | Nothing +-- "2023-01-03" | Nothing +-- >>> zipWithMonoid (<>) xs ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | Sum {getSum = 6} +-- "2023-01-02" | Sum {getSum = 2} +-- "2023-01-03" | Sum {getSum = 7} +zipWithMonoid :: ( Monoid a, Monoid b + , Vector v a, Vector v b, Vector v c + , Ord k + ) + => (a -> b -> c) + -> Series v k a + -> Series v k b + -> Series v k c +zipWithMonoid f left right + = let fullindex = index left `Index.union` index right + (MkSeries ix ls) = requireWith (const mempty) id left fullindex + (MkSeries _ rs) = requireWith (const mempty) id right fullindex + in MkSeries ix $ Vector.zipWith f ls rs +{-# INLINABLE zipWithMonoid #-} + + +-- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. +-- +-- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ] +-- >>> xs `esum` ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | 6 +-- "2023-01-02" | 2 +-- "2023-01-03" | 7 +esum :: (Ord k, Num a, Vector v a, Vector v (Sum a)) + => Series v k a + -> Series v k a + -> Series v k a +esum ls rs = G.map getSum $ zipWithMonoid (<>) (G.map Sum ls) (G.map Sum rs) +{-# INLINABLE esum #-} + + +-- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. +-- +-- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ] +-- >>> xs `eproduct` ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | 10 +-- "2023-01-02" | 3 +-- "2023-01-03" | 7 +eproduct :: (Ord k, Num a, Vector v a, Vector v (Product a)) + => Series v k a + -> Series v k a + -> Series v k a +eproduct ls rs = G.map getProduct $ zipWithMonoid (<>) (G.map Product ls) (G.map Product rs) +{-# INLINABLE eproduct #-} + + +-- | \(O(n)\) Unzip a 'Series' of 2-tuples. +unzip :: (Vector v a, Vector v b, Vector v (a, b)) + => Series v k (a, b) + -> ( Series v k a + , Series v k b + ) +unzip (MkSeries ix vs) + = let (left, right) = Vector.unzip vs + in (MkSeries ix left, MkSeries ix right) +{-# INLINABLE unzip #-} + + +-- | \(O(n)\) Unzip a 'Series' of 3-tuples. +unzip3 :: (Vector v a, Vector v b, Vector v c, Vector v (a, b, c)) + => Series v k (a, b, c) + -> ( Series v k a + , Series v k b + , Series v k c + ) +unzip3 (MkSeries ix vs) + = let (left, center, right) = Vector.unzip3 vs + in (MkSeries ix left, MkSeries ix center, MkSeries ix right) +{-# INLINABLE unzip3 #-}
src/Data/Series/Index.hs view
@@ -1,107 +1,108 @@--------------------------------------------------------------------------------- |--- Module : $header--- Copyright : (c) Laurent P. René de Cotret--- License : MIT-style--- Maintainer : Laurent P. René de Cotret--- Portability : portable------ This module contains the definition of 'Index', a sequence of /unique/ and /sorted/--- keys which can be used to efficient index a 'Data.Series.Series'.------ = Construction------ Constructing an 'Index' can be done from the usual list using `fromList`. Note that --- the 'Index' length could be smaller than the input list, due to the requirement that--- an 'Index' be a sequence of unique keys. A better way to construct an 'Index' is --- to use a 'Data.Set' (`fromSet`)------ For quick inline definitions of an 'Index', you can also make use of the @OverloadedLists@ extension:--- --- >>> :set -XOverloadedLists--- >>> let (ix :: Index Int) = [1,2,3,4,5,5,5]--- >>> ix--- Index [1,2,3,4,5] ------ Another useful function to construct an 'Index' is `range`. This allows to build an 'Index'--- from a starting value up to an ending value, with a custom step function. For example,--- here's an 'Index' with values from 1 to 10, in steps of 3:------ >>> range (+3) (1 :: Int) 10--- Index [1,4,7,10]------ Note that `range` is a special case of the `unfoldr` function, which is also provided in this module.------ = Set operations--- --- Just like a 'Data.Set', 'Index' supports efficient `member`, `notMember`, `union`, `intersection`, and `difference` operations.--- Like 'Data.Set', the `Semigroup` and `Monoid` instance of 'Index' are defined using the `union` operation:------ >>> fromList ['a', 'b', 'c'] <> fromList ['b', 'c', 'd']--- Index "abcd"------ = Mapping------ Because of the restriction that all keys be unique, an 'Index' is not a true `Functor`; you can't use--- `fmap` to map elements of an index. Instead, you can use the general-purpose function 'map'. If you want--- to map elements of an 'Index' with a monotonic function (i.e. a function which will not re-order elements and won't--- create duplicate elements), you can use the 'Data.Series.mapMonotonic' function which operates faster.------ = Indexing------ One of the key operations for 'Data.Series.Series' is to find the integer index of an element in an 'Index'. For this purpose, you--- can use `lookupIndex`:------ >>> lookupIndex 'b' $ fromList ['a', 'b', 'c']--- Just 1--- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c']--- Nothing--module Data.Series.Index (- Index,-- -- * Creation and Conversion- singleton,- unfoldr,- range,- IsIndex(..),- fromSet,- fromList,- fromVector,- toSet,- toAscList,- toAscVector,-- -- * Set-like operations- null,- member,- notMember,- union,- intersection,- difference,- symmetricDifference,- contains,- size,- take,- drop,-- -- * Mapping and filtering- map,- filter,- traverse,- - -- * Indexing- lookupIndex,-- -- * Insertion and deletion- insert,- delete,-) where--import Data.Series.Index.Definition ( Index, IsIndex(..), singleton, unfoldr, range, fromSet, fromList, fromVector, toSet- , toAscList, toAscVector, null, member, notMember, union, intersection- , difference, symmetricDifference, contains, size, take, drop, map- , filter, traverse, lookupIndex, insert, delete - )-import Prelude hiding ( null, take, drop, map, filter, traverse )-+----------------------------------------------------------------------------- +-- | +-- Module : $header +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT-style +-- Maintainer : Laurent P. René de Cotret +-- Portability : portable +-- +-- This module contains the definition of 'Index', a sequence of /unique/ and /sorted/ +-- keys which can be used to efficient index a 'Data.Series.Series'. +-- +-- = Construction +-- +-- Constructing an 'Index' can be done from the usual list using `fromList`. Note that +-- the 'Index' length could be smaller than the input list, due to the requirement that +-- an 'Index' be a sequence of unique keys. A better way to construct an 'Index' is +-- to use a 'Data.Set' (`fromSet`) +-- +-- For quick INLINABLE definitions of an 'Index', you can also make use of the @OverloadedLists@ extension: +-- +-- >>> :set -XOverloadedLists +-- >>> let (ix :: Index Int) = [1,2,3,4,5,5,5] +-- >>> ix +-- Index [1,2,3,4,5] +-- +-- Another useful function to construct an 'Index' is `range`. This allows to build an 'Index' +-- from a starting value up to an ending value, with a custom step function. For example, +-- here's an 'Index' with values from 1 to 10, in steps of 3: +-- +-- >>> range (+3) (1 :: Int) 10 +-- Index [1,4,7,10] +-- +-- Note that `range` is a special case of the `unfoldr` function, which is also provided in this module. +-- +-- = Set operations +-- +-- Just like a 'Data.Set', 'Index' supports efficient `member`, `notMember`, `union`, `intersection`, and `difference` operations. +-- Like 'Data.Set', the `Semigroup` and `Monoid` instance of 'Index' are defined using the `union` operation: +-- +-- >>> fromList ['a', 'b', 'c'] <> fromList ['b', 'c', 'd'] +-- Index "abcd" +-- +-- = Mapping +-- +-- Because of the restriction that all keys be unique, an 'Index' is not a true `Functor`; you can't use +-- `fmap` to map elements of an index. Instead, you can use the general-purpose function 'map'. If you want +-- to map elements of an 'Index' with a monotonic function (i.e. a function which will not re-order elements and won't +-- create duplicate elements), you can use the 'Data.Series.mapMonotonic' function which operates faster. +-- +-- = Indexing +-- +-- One of the key operations for 'Data.Series.Series' is to find the integer index of an element in an 'Index'. For this purpose, you +-- can use `lookupIndex`: +-- +-- >>> lookupIndex 'b' $ fromList ['a', 'b', 'c'] +-- Just 1 +-- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c'] +-- Nothing + +module Data.Series.Index ( + Index, + + -- * Creation and Conversion + singleton, + unfoldr, + range, + IsIndex(..), + fromSet, + fromList, + fromVector, + toSet, + toAscList, + toAscVector, + + -- * Set-like operations + null, + member, + notMember, + union, + intersection, + difference, + symmetricDifference, + contains, + size, + take, + drop, + + -- * Mapping and filtering + map, + indexed, + filter, + traverse, + + -- * Indexing + lookupIndex, + + -- * Insertion and deletion + insert, + delete, +) where + +import Data.Series.Index.Definition ( Index, IsIndex(..), singleton, unfoldr, range, fromSet, fromList, fromVector, toSet + , toAscList, toAscVector, null, member, notMember, union, intersection + , difference, symmetricDifference, contains, size, take, drop, map, indexed + , filter, traverse, lookupIndex, insert, delete + ) +import Prelude hiding ( null, take, drop, map, filter, traverse ) +
src/Data/Series/Index/Definition.hs view
@@ -1,503 +1,517 @@-{-# LANGUAGE TypeFamilies #-}-{-# OPTIONS_GHC -Wno-redundant-constraints #-}---------------------------------------------------------------------------------- |--- Module : $header--- Copyright : (c) Laurent P. René de Cotret--- License : MIT-style--- Maintainer : Laurent P. René de Cotret--- Portability : portable------ This module contains the definition of 'Index', a sequence of /unique/ and /sorted/--- keys which can be used to efficient index a 'Series'.---module Data.Series.Index.Definition (- Index(..),-- -- * Creation and Conversion- singleton,- unfoldr,- range,- fromSet, toSet,- fromList, toAscList,- fromAscList, fromDistinctAscList,- fromVector, toAscVector,- fromAscVector, fromDistinctAscVector,- -- ** Ad-hoc conversion with other data structures- IsIndex(..),- - -- * Set-like operations- null,- member,- notMember,- union,- intersection,- difference,- symmetricDifference,- cartesianProduct,- contains,- size,- take,- drop,-- -- * Mapping and filtering- map,- mapMonotonic,- filter,- traverse,- - -- * Indexing- findIndex,- lookupIndex,- elemAt,-- -- * Insertion and deletion- insert,- delete,-) where--import Control.DeepSeq ( NFData )-import Control.Monad ( guard )-import Control.Monad.ST ( runST )-import Data.Coerce ( coerce )-import qualified Data.Foldable as Foldable-import Data.Functor ( ($>) )-import Data.IntSet ( IntSet )-import qualified Data.IntSet as IntSet-import qualified Data.List as List-import Data.Sequence ( Seq )-import qualified Data.Sequence as Seq-import Data.Set ( Set )-import qualified Data.Set as Set-import qualified Data.Traversable as Traversable-import qualified Data.Vector as Boxed-import Data.Vector.Algorithms.Intro ( sortUniq )-import Data.Vector.Generic ( Vector )-import qualified Data.Vector.Generic as Vector-import qualified Data.Vector.Generic.Mutable as M-import qualified Data.Vector.Unboxed as Unboxed-import GHC.Exts ( IsList )-import qualified GHC.Exts as Exts-import GHC.Stack ( HasCallStack )-import Prelude as P hiding ( null, take, drop, map, filter, traverse, product )---- $setup--- >>> import Data.Series.Index--- >>> import qualified Data.Vector as Vector----- | Representation of the index of a series.--- An index is a sequence of sorted elements. All elements are unique, much like a 'Set'.------ You can construct an 'Index' from a set ('fromSet'), from a list ('fromList'), or from a vector ('fromVector'). You can --- also make use of the @OverloadedLists@ extension:------ >>> :set -XOverloadedLists--- >>> let (ix :: Index Int) = [1, 2, 3]--- >>> ix--- Index [1,2,3]------ Since keys in an 'Index' are always sorted and unique, 'Index' is not a 'Functor'. To map a function--- over an 'Index', use 'map'.-newtype Index k = MkIndex (Set k)- deriving (Eq, Ord, Semigroup, Monoid, Foldable, NFData)---instance Ord k => IsList (Index k) where- type Item (Index k) = k- fromList :: [k] -> Index k- fromList = fromList- toList :: Index k -> [Exts.Item (Index k)]- toList = toAscList---instance Show k => Show (Index k) where- show :: Index k -> String- show (MkIndex s) = "Index " ++ show (Set.toList s)----- | \(O(1)\) Create a singleton 'Index'.-singleton :: k -> Index k-singleton = MkIndex . Set.singleton-{-# INLINE singleton #-}----- | \(O(n \log n)\) Create an 'Index' from a seed value. --- Note that the order in which elements are generated does not matter; elements are stored--- in order. See the example below.------ >>> unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int)--- Index [1,2,3,4,5,6,7]-unfoldr :: Ord a => (b -> Maybe (a, b)) -> b -> Index a-unfoldr f = fromList . List.unfoldr f-{-# INLINE unfoldr #-}----- | \(O(n \log n)\) Create an 'Index' as a range of values. @range f start end@ will generate --- an 'Index' with values @[start, f start, f (f start), ... ]@ such that the largest element--- less or equal to @end@ is included. See examples below.------ >>> range (+3) (1 :: Int) 10--- Index [1,4,7,10]--- >>> range (+3) (1 :: Int) 11--- Index [1,4,7,10]-range :: Ord a - => (a -> a) -- ^ Function to generate the next element in the index- -> a -- ^ Starting value of the 'Index'- -> a -- ^ Ending value of the 'Index', which may or may not be contained- -> Index a-range next start end - = unfoldr (\x -> guard (x <= end) $> (x, next x)) start-{-# INLINE range #-}----- | The 'IsIndex' typeclass allow for ad-hoc definition--- of conversion functions, converting to / from 'Index'.-class IsIndex t k where- -- | Construct an 'Index' from some container of keys. There is no- -- condition on the order of keys. Duplicate keys are silently dropped.- toIndex :: t -> Index k-- -- | Construct a container from keys of an 'Index'. - -- The elements are returned in ascending order of keys.- fromIndex :: Index k -> t---instance IsIndex (Set k) k where- -- | \(O(1)\) Build an 'Index' from a 'Set'.- toIndex :: Set k -> Index k- toIndex = coerce- {-# INLINE toIndex #-}-- -- | \(O(1)\) Build an 'Index' from a 'Set'.- fromIndex :: Index k -> Set k- fromIndex = coerce- {-# INLINE fromIndex #-}---instance Ord k => IsIndex [k] k where- -- | \(O(n \log n)\) Build an 'Index' from a list.- toIndex :: [k] -> Index k- toIndex = fromList- {-# INLINE toIndex #-}-- -- | \(O(n)\) Convert an 'Index' to a list.- fromIndex :: Index k -> [k]- fromIndex = toAscList- {-# INLINE fromIndex #-}---instance Ord k => IsIndex (Seq k) k where- -- | \(O(n \log n)\) Build an 'Index' from a 'Seq'.- toIndex :: Seq k -> Index k- toIndex = fromList . Foldable.toList- {-# INLINE toIndex #-}-- -- | \(O(n)\) Convert an 'Index' to a 'Seq'.- fromIndex :: Index k -> Seq k- fromIndex = Seq.fromList . toAscList- {-# INLINE fromIndex #-}---instance IsIndex IntSet Int where- -- | \(O(n \min(n,W))\), where \W\ is the number of bits in an 'Int' on your platform (32 or 64).- toIndex :: IntSet -> Index Int- toIndex = fromDistinctAscList . IntSet.toList- {-# INLINE toIndex #-}- - -- | \(O(n)\) Convert an 'Index' to an 'IntSet.- fromIndex :: Index Int -> IntSet- fromIndex = IntSet.fromDistinctAscList . toAscList- {-# INLINE fromIndex #-}---instance (Ord k) => IsIndex (Boxed.Vector k) k where- toIndex :: Boxed.Vector k -> Index k- toIndex = fromVector- {-# INLINE toIndex #-} -- fromIndex :: Index k -> Boxed.Vector k- fromIndex = toAscVector- {-# INLINE fromIndex #-}---instance (Ord k, Unboxed.Unbox k) => IsIndex (Unboxed.Vector k) k where- toIndex :: Unboxed.Vector k -> Index k- toIndex = fromVector- {-# INLINE toIndex #-} -- fromIndex :: Index k -> Unboxed.Vector k- fromIndex ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze- {-# INLINE fromIndex #-}----- | \(O(1)\) Build an 'Index' from a 'Set'.-fromSet :: Set k -> Index k-fromSet = toIndex-{-# INLINE fromSet #-}----- | \(O(n \log n)\) Build an 'Index' from a list. Note that since an 'Index' is--- composed of unique elements, the length of the index may not be--- the same as the length of the input list:------ >>> fromList ['c', 'a', 'b', 'b']--- Index "abc"------ If the list is already sorted, `fromAscList` is generally faster.-fromList :: Ord k => [k] -> Index k-fromList = fromSet . Set.fromList-{-# INLINE fromList #-}----- | \(O(n)\) Build an 'Index' from a list of elements in ascending order. The precondition--- that elements already be sorted is not checked.--- --- Note that since an 'Index' is composed of unique elements, the length of --- the index may not be the same as the length of the input list.-fromAscList :: Eq k => [k] -> Index k-fromAscList = toIndex . Set.fromAscList-{-# INLINE fromAscList #-}----- | \(O(n)\) Build an 'Index' from a list of distinct elements in ascending order. The precondition--- that elements be unique and sorted is not checked.-fromDistinctAscList :: [k] -> Index k-fromDistinctAscList = MkIndex . Set.fromDistinctAscList-{-# INLINE fromDistinctAscList #-}----- | \(O(n \log n)\) Build an 'Index' from a 'Vector'. Note that since an 'Index' is--- composed of unique elements, the length of the index may not be--- the same as the length of the input vector:------ >>> import Data.Vector as V--- >>> fromVector $ V.fromList ['c', 'a', 'b', 'b']--- Index "abc"------ If the 'Vector' is already sorted, 'fromAscVector' is generally faster.-fromVector :: (Vector v k, Ord k) => v k -> Index k-fromVector vs = fromDistinctAscVector $ runST $ Vector.thaw vs >>= sortUniq >>= Vector.freeze-{-# INLINE fromVector #-}----- | \(O(n \log n)\) Build an 'Index' from a 'Vector' of elements in ascending order. The precondition--- that elements already be sorted is not checked. ------ Note that since an 'Index' is composed of unique elements, --- the length of the index may not be the same as the length of the input vector:------ >>> import Data.Vector as V--- >>> fromAscVector $ V.fromList ['a', 'b', 'b', 'c']--- Index "abc"-fromAscVector :: (Vector v k, Ord k) => v k -> Index k-fromAscVector = fromAscList . Vector.toList-{-# INLINE fromAscVector #-}----- | \(O(n)\) Build an 'Index' from a 'Vector' of unique elements in ascending order. The precondition--- that elements already be unique and sorted is not checked.-fromDistinctAscVector :: Vector v k => v k -> Index k-fromDistinctAscVector = fromDistinctAscList . Vector.toList-{-# INLINE fromDistinctAscVector #-}----- | \(O(1)\) Convert an 'Index' to a 'Set'.-toSet :: Index k -> Set k-toSet = fromIndex-{-# INLINE toSet #-}----- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order.-toAscList :: Index k -> [k]-toAscList (MkIndex s) = Set.toAscList s-{-# INLINE toAscList #-}----- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order.-toAscVector :: Vector v k => Index k -> v k-toAscVector ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze-{-# INLINE toAscVector #-}----- | \(O(1)\) Returns 'True' for an empty 'Index', and @False@ otherwise.-null :: Index k -> Bool-null (MkIndex ix) = Set.null ix-{-# INLINE null #-}----- | \(O(n \log n)\) Check whether the element is in the index.-member :: Ord k => k -> Index k -> Bool-member k (MkIndex ix) = k `Set.member` ix-{-# INLINE member #-}----- | \(O(n \log n)\) Check whether the element is NOT in the index.-notMember :: Ord k => k -> Index k -> Bool-notMember k = not . member k-{-# INLINE notMember #-}----- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Union of two 'Index', containing--- elements either in the left index, right right index, or both.-union :: Ord k => Index k -> Index k -> Index k-union = (<>)-{-# INLINE union #-}----- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Intersection of two 'Index', containing--- elements which are in both the left index and the right index.-intersection :: Ord k => Index k -> Index k -> Index k-intersection (MkIndex ix) (MkIndex jx) = MkIndex $ ix `Set.intersection` jx-{-# INLINE intersection #-}----- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Returns the elements of the first index --- which are not found in the second index.------ >>> difference (fromList ['a', 'b', 'c']) (fromList ['b', 'c', 'd'])--- Index "a"-difference :: Ord k => Index k -> Index k -> Index k-difference (MkIndex ix) (MkIndex jx) = MkIndex $ Set.difference ix jx-{-# INLINE difference #-}----- | \(O(n+m)\). The symmetric difference of two 'Index'.--- The first element of the tuple is an 'Index' containing all elements which--- are only found in the left 'Index', while the second element of the tuple is an 'Index' containing--- all elements which are only found in the right 'Index':------ >>> left = fromList ['a', 'b', 'c']--- >>> right = fromList ['c', 'd', 'e']--- >>> left `symmetricDifference` right--- (Index "ab",Index "de")-symmetricDifference :: Ord k => Index k -> Index k -> (Index k, Index k)-symmetricDifference left right = (left `difference` right, right `difference` left)-{-# INLINE symmetricDifference #-}----- | \(O(n m)\) Take the cartesian product of two 'Index':------ >>> (range (+1) (1 :: Int) 2) `cartesianProduct` (range (+1) (3 :: Int) 4)--- Index [(1,3),(1,4),(2,3),(2,4)]-cartesianProduct :: Index k -> Index g -> Index (k, g)-cartesianProduct (MkIndex xs) (MkIndex ys) - = MkIndex $ Set.cartesianProduct xs ys-{-# INLINE cartesianProduct #-}----- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\).--- @(ix1 \'contains\' ix2)@ indicates whether all keys in @ix2@ are also in @ix1@.-contains :: Ord k => Index k -> Index k -> Bool-contains (MkIndex ix1) (MkIndex ix2)= ix2 `Set.isSubsetOf` ix1-{-# INLINE contains #-}----- | \(O(1)\) Returns the number of keys in the index.-size :: Index k -> Int-size (MkIndex ix) = Set.size ix-{-# INLINE size #-}----- | \(O(\log n)\). Take @n@ elements from the index, in ascending order.--- Taking more than the number of elements in the index is a no-op:------ >>> take 10 $ fromList [1::Int,2,3]--- Index [1,2,3]-take :: Int -> Index k -> Index k-take n (MkIndex ix) = MkIndex (Set.take n ix)-{-# INLINE take #-}----- | \(O(\log n)\). Drop @n@ elements from the index, in ascending order.-drop :: Int -> Index k -> Index k-drop n (MkIndex ix) = MkIndex (Set.drop n ix)-{-# INLINE drop #-}----- | \(O(n \log n)\) Map a function over keys in the index.--- Note that since keys in an 'Index' are unique, the length of the resulting--- index may not be the same as the input:------ >>> map (\x -> if even x then 0::Int else 1) $ fromList [0::Int,1,2,3,4]--- Index [0,1]------ If the mapping is monotonic, see 'mapMonotonic', which has better performance--- characteristics.-map :: Ord g => (k -> g) -> Index k -> Index g-map f (MkIndex ix) = MkIndex $ Set.map f ix-{-# INLINE map #-}----- | \(O(n)\) Map a monotonic function over keys in the index. /Monotonic/ means that if @a < b@, then @f a < f b@.--- Using 'mapMonononic' can be much faster than using 'map' for a large 'Index'.--- Note that the precondiction that the function be monotonic is not checked.------ >>> mapMonotonic (+1) $ fromList [0::Int,1,2,3,4,5]--- Index [1,2,3,4,5,6]-mapMonotonic :: (k -> g) -> Index k -> Index g-mapMonotonic f (MkIndex ix) = MkIndex $ Set.mapMonotonic f ix-{-# INLINE mapMonotonic #-}----- | \(O(n)\) Filter elements satisfying a predicate.------ >>> filter even $ fromList [1::Int,2,3,4,5]--- Index [2,4]-filter :: (k -> Bool) -> Index k -> Index k-filter p (MkIndex ix) = MkIndex $ Set.filter p ix-{-# INLINE filter #-}----- | \(O(\log n)\). Returns the integer /index/ of a key. This function raises an exception--- if the key is not in the 'Index'; see 'lookupIndex' for a safe version.------ >>> findIndex 'b' $ fromList ['a', 'b', 'c']--- 1-findIndex :: HasCallStack => Ord k => k -> Index k -> Int-findIndex e (MkIndex ix) = Set.findIndex e ix -{-# INLINE findIndex #-}----- | \(O(\log n)\). Returns the integer /index/ of a key, if the key is in the index.------ >>> lookupIndex 'b' $ fromList ['a', 'b', 'c']--- Just 1--- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c']--- Nothing-lookupIndex :: Ord k => k -> Index k -> Maybe Int-lookupIndex e (MkIndex ix) = Set.lookupIndex e ix-{-# INLINE lookupIndex #-}----- | \(O(\log n)\) Returns the element at some integer index. This function raises--- an exception if the integer index is out-of-bounds.-elemAt :: HasCallStack => Int -> Index k -> k-elemAt n (MkIndex ix) = Set.elemAt n ix-{-# INLINE elemAt #-}----- | \(O(\log n)\). Insert a key in an 'Index'. If the key is already --- present, the 'Index' will not change.-insert :: Ord k => k -> Index k -> Index k-insert k (MkIndex ix) = MkIndex $ k `Set.insert` ix-{-# INLINE insert #-}----- | \(O(\log n)\). Delete a key from an 'Index', if this key is present--- in the index.-delete :: Ord k => k -> Index k -> Index k-delete k (MkIndex ix) = MkIndex $ k `Set.delete` ix-{-# INLINE delete #-}----- | \(O(n \log n)\). Map each element of an 'Index' to an applicative action, --- evaluate these actions from left to right, and collect the results.------ Note that the data type 'Index' is not a member of 'Traversable'--- because it is not a 'Functor'.-traverse :: (Applicative f, Ord b) => (k -> f b) -> Index k -> f (Index b)-traverse f = fmap fromList . Traversable.traverse f . toAscList-{-# INLINE traverse #-}+{-# LANGUAGE TypeFamilies #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + +----------------------------------------------------------------------------- +-- | +-- Module : $header +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT-style +-- Maintainer : Laurent P. René de Cotret +-- Portability : portable +-- +-- This module contains the definition of 'Index', a sequence of /unique/ and /sorted/ +-- keys which can be used to efficient index a 'Series'. + + +module Data.Series.Index.Definition ( + Index(..), + + -- * Creation and Conversion + singleton, + unfoldr, + range, + fromSet, toSet, + fromList, toAscList, + fromAscList, fromDistinctAscList, + fromVector, toAscVector, + fromAscVector, fromDistinctAscVector, + -- ** Ad-hoc conversion with other data structures + IsIndex(..), + + -- * Set-like operations + null, + member, + notMember, + union, + intersection, + difference, + symmetricDifference, + cartesianProduct, + contains, + size, + take, + drop, + + -- * Mapping and filtering + map, + mapMonotonic, + indexed, + filter, + traverse, + + -- * Indexing + findIndex, + lookupIndex, + elemAt, + + -- * Insertion and deletion + insert, + delete, +) where + +import Control.DeepSeq ( NFData ) +import Control.Monad ( guard ) +import Control.Monad.ST ( runST ) +import Data.Coerce ( coerce ) +import qualified Data.Foldable as Foldable +import Data.Functor ( ($>) ) +import Data.IntSet ( IntSet ) +import qualified Data.IntSet as IntSet +import qualified Data.List as List +import Data.Sequence ( Seq ) +import qualified Data.Sequence as Seq +import Data.Set ( Set ) +import qualified Data.Set as Set +import qualified Data.Traversable as Traversable +import qualified Data.Vector as Boxed +import Data.Vector.Algorithms.Intro ( sortUniq ) +import Data.Vector.Generic ( Vector ) +import qualified Data.Vector.Generic as Vector +import qualified Data.Vector.Generic.Mutable as M +import qualified Data.Vector.Unboxed as Unboxed +import GHC.Exts ( IsList ) +import qualified GHC.Exts as Exts +import GHC.Stack ( HasCallStack ) +import Prelude as P hiding ( null, take, drop, map, filter, traverse, product ) + +-- $setup +-- >>> import Data.Series.Index +-- >>> import qualified Data.Vector as Vector + + +-- | Representation of the index of a series. +-- An index is a sequence of sorted elements. All elements are unique, much like a 'Set'. +-- +-- You can construct an 'Index' from a set ('fromSet'), from a list ('fromList'), or from a vector ('fromVector'). You can +-- also make use of the @OverloadedLists@ extension: +-- +-- >>> :set -XOverloadedLists +-- >>> let (ix :: Index Int) = [1, 2, 3] +-- >>> ix +-- Index [1,2,3] +-- +-- Since keys in an 'Index' are always sorted and unique, 'Index' is not a 'Functor'. To map a function +-- over an 'Index', use 'map'. +newtype Index k = MkIndex (Set k) + deriving (Eq, Ord, Semigroup, Monoid, Foldable, NFData) + + +instance Ord k => IsList (Index k) where + type Item (Index k) = k + fromList :: [k] -> Index k + fromList = fromList + toList :: Index k -> [Exts.Item (Index k)] + toList = toAscList + + +instance Show k => Show (Index k) where + show :: Index k -> String + show (MkIndex s) = "Index " ++ show (Set.toList s) + + +-- | \(O(1)\) Create a singleton 'Index'. +singleton :: k -> Index k +singleton = MkIndex . Set.singleton +{-# INLINABLE singleton #-} + + +-- | \(O(n \log n)\) Create an 'Index' from a seed value. +-- Note that the order in which elements are generated does not matter; elements are stored +-- in order. See the example below. +-- +-- >>> unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int) +-- Index [1,2,3,4,5,6,7] +unfoldr :: Ord a => (b -> Maybe (a, b)) -> b -> Index a +unfoldr f = fromList . List.unfoldr f +{-# INLINABLE unfoldr #-} + + +-- | \(O(n \log n)\) Create an 'Index' as a range of values. @range f start end@ will generate +-- an 'Index' with values @[start, f start, f (f start), ... ]@ such that the largest element +-- less or equal to @end@ is included. See examples below. +-- +-- >>> range (+3) (1 :: Int) 10 +-- Index [1,4,7,10] +-- >>> range (+3) (1 :: Int) 11 +-- Index [1,4,7,10] +range :: Ord a + => (a -> a) -- ^ Function to generate the next element in the index + -> a -- ^ Starting value of the 'Index' + -> a -- ^ Ending value of the 'Index', which may or may not be contained + -> Index a +range next start end + = unfoldr (\x -> guard (x <= end) $> (x, next x)) start +{-# INLINABLE range #-} + + +-- | The 'IsIndex' typeclass allow for ad-hoc definition +-- of conversion functions, converting to / from 'Index'. +class IsIndex t k where + -- | Construct an 'Index' from some container of keys. There is no + -- condition on the order of keys. Duplicate keys are silently dropped. + toIndex :: t -> Index k + + -- | Construct a container from keys of an 'Index'. + -- The elements are returned in ascending order of keys. + fromIndex :: Index k -> t + + +instance IsIndex (Set k) k where + -- | \(O(1)\) Build an 'Index' from a 'Set'. + toIndex :: Set k -> Index k + toIndex = coerce + {-# INLINABLE toIndex #-} + + -- | \(O(1)\) Build an 'Index' from a 'Set'. + fromIndex :: Index k -> Set k + fromIndex = coerce + {-# INLINABLE fromIndex #-} + + +instance Ord k => IsIndex [k] k where + -- | \(O(n \log n)\) Build an 'Index' from a list. + toIndex :: [k] -> Index k + toIndex = fromList + {-# INLINABLE toIndex #-} + + -- | \(O(n)\) Convert an 'Index' to a list. + fromIndex :: Index k -> [k] + fromIndex = toAscList + {-# INLINABLE fromIndex #-} + + +instance Ord k => IsIndex (Seq k) k where + -- | \(O(n \log n)\) Build an 'Index' from a 'Seq'. + toIndex :: Seq k -> Index k + toIndex = fromList . Foldable.toList + {-# INLINABLE toIndex #-} + + -- | \(O(n)\) Convert an 'Index' to a 'Seq'. + fromIndex :: Index k -> Seq k + fromIndex = Seq.fromList . toAscList + {-# INLINABLE fromIndex #-} + + +instance IsIndex IntSet Int where + -- | \(O(n \min(n,W))\), where \W\ is the number of bits in an 'Int' on your platform (32 or 64). + toIndex :: IntSet -> Index Int + toIndex = fromDistinctAscList . IntSet.toList + {-# INLINABLE toIndex #-} + + -- | \(O(n)\) Convert an 'Index' to an 'IntSet. + fromIndex :: Index Int -> IntSet + fromIndex = IntSet.fromDistinctAscList . toAscList + {-# INLINABLE fromIndex #-} + + +instance (Ord k) => IsIndex (Boxed.Vector k) k where + toIndex :: Boxed.Vector k -> Index k + toIndex = fromVector + {-# INLINABLE toIndex #-} + + fromIndex :: Index k -> Boxed.Vector k + fromIndex = toAscVector + {-# INLINABLE fromIndex #-} + + +instance (Ord k, Unboxed.Unbox k) => IsIndex (Unboxed.Vector k) k where + toIndex :: Unboxed.Vector k -> Index k + toIndex = fromVector + {-# INLINABLE toIndex #-} + + fromIndex :: Index k -> Unboxed.Vector k + fromIndex ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze + {-# INLINABLE fromIndex #-} + + +-- | \(O(1)\) Build an 'Index' from a 'Set'. +fromSet :: Set k -> Index k +fromSet = toIndex +{-# INLINABLE fromSet #-} + + +-- | \(O(n \log n)\) Build an 'Index' from a list. Note that since an 'Index' is +-- composed of unique elements, the length of the index may not be +-- the same as the length of the input list: +-- +-- >>> fromList ['c', 'a', 'b', 'b'] +-- Index "abc" +-- +-- If the list is already sorted, `fromAscList` is generally faster. +fromList :: Ord k => [k] -> Index k +fromList = fromSet . Set.fromList +{-# INLINABLE fromList #-} + + +-- | \(O(n)\) Build an 'Index' from a list of elements in ascending order. The precondition +-- that elements already be sorted is not checked. +-- +-- Note that since an 'Index' is composed of unique elements, the length of +-- the index may not be the same as the length of the input list. +fromAscList :: Eq k => [k] -> Index k +fromAscList = toIndex . Set.fromAscList +{-# INLINABLE fromAscList #-} + + +-- | \(O(n)\) Build an 'Index' from a list of distinct elements in ascending order. The precondition +-- that elements be unique and sorted is not checked. +fromDistinctAscList :: [k] -> Index k +fromDistinctAscList = MkIndex . Set.fromDistinctAscList +{-# INLINABLE fromDistinctAscList #-} + + +-- | \(O(n \log n)\) Build an 'Index' from a 'Vector'. Note that since an 'Index' is +-- composed of unique elements, the length of the index may not be +-- the same as the length of the input vector: +-- +-- >>> import Data.Vector as V +-- >>> fromVector $ V.fromList ['c', 'a', 'b', 'b'] +-- Index "abc" +-- +-- If the 'Vector' is already sorted, 'fromAscVector' is generally faster. +fromVector :: (Vector v k, Ord k) => v k -> Index k +fromVector vs = fromDistinctAscVector $ runST $ Vector.thaw vs >>= sortUniq >>= Vector.freeze +{-# INLINABLE fromVector #-} + + +-- | \(O(n \log n)\) Build an 'Index' from a 'Vector' of elements in ascending order. The precondition +-- that elements already be sorted is not checked. +-- +-- Note that since an 'Index' is composed of unique elements, +-- the length of the index may not be the same as the length of the input vector: +-- +-- >>> import Data.Vector as V +-- >>> fromAscVector $ V.fromList ['a', 'b', 'b', 'c'] +-- Index "abc" +fromAscVector :: (Vector v k, Ord k) => v k -> Index k +fromAscVector = fromAscList . Vector.toList +{-# INLINABLE fromAscVector #-} + + +-- | \(O(n)\) Build an 'Index' from a 'Vector' of unique elements in ascending order. The precondition +-- that elements already be unique and sorted is not checked. +fromDistinctAscVector :: Vector v k => v k -> Index k +fromDistinctAscVector = fromDistinctAscList . Vector.toList +{-# INLINABLE fromDistinctAscVector #-} + + +-- | \(O(1)\) Convert an 'Index' to a 'Set'. +toSet :: Index k -> Set k +toSet = fromIndex +{-# INLINABLE toSet #-} + + +-- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order. +toAscList :: Index k -> [k] +toAscList (MkIndex s) = Set.toAscList s +{-# INLINABLE toAscList #-} + + +-- | \(O(n)\) Convert an 'Index' to a list. Elements will be produced in ascending order. +toAscVector :: Vector v k => Index k -> v k +toAscVector ix = runST $ M.generate (size ix) (`elemAt` ix) >>= Vector.freeze +{-# INLINABLE toAscVector #-} + + +-- | \(O(1)\) Returns 'True' for an empty 'Index', and @False@ otherwise. +null :: Index k -> Bool +null (MkIndex ix) = Set.null ix +{-# INLINABLE null #-} + + +-- | \(O(n \log n)\) Check whether the element is in the index. +member :: Ord k => k -> Index k -> Bool +member k (MkIndex ix) = k `Set.member` ix +{-# INLINABLE member #-} + + +-- | \(O(n \log n)\) Check whether the element is NOT in the index. +notMember :: Ord k => k -> Index k -> Bool +notMember k = not . member k +{-# INLINABLE notMember #-} + + +-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Union of two 'Index', containing +-- elements either in the left index, right right index, or both. +union :: Ord k => Index k -> Index k -> Index k +union = (<>) +{-# INLINABLE union #-} + + +-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Intersection of two 'Index', containing +-- elements which are in both the left index and the right index. +intersection :: Ord k => Index k -> Index k -> Index k +intersection (MkIndex ix) (MkIndex jx) = MkIndex $ ix `Set.intersection` jx +{-# INLINABLE intersection #-} + + +-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\) Returns the elements of the first index +-- which are not found in the second index. +-- +-- >>> difference (fromList ['a', 'b', 'c']) (fromList ['b', 'c', 'd']) +-- Index "a" +difference :: Ord k => Index k -> Index k -> Index k +difference (MkIndex ix) (MkIndex jx) = MkIndex $ Set.difference ix jx +{-# INLINABLE difference #-} + + +-- | \(O(n+m)\). The symmetric difference of two 'Index'. +-- The first element of the tuple is an 'Index' containing all elements which +-- are only found in the left 'Index', while the second element of the tuple is an 'Index' containing +-- all elements which are only found in the right 'Index': +-- +-- >>> left = fromList ['a', 'b', 'c'] +-- >>> right = fromList ['c', 'd', 'e'] +-- >>> left `symmetricDifference` right +-- (Index "ab",Index "de") +symmetricDifference :: Ord k => Index k -> Index k -> (Index k, Index k) +symmetricDifference left right = (left `difference` right, right `difference` left) +{-# INLINABLE symmetricDifference #-} + + +-- | \(O(n m)\) Take the cartesian product of two 'Index': +-- +-- >>> (range (+1) (1 :: Int) 2) `cartesianProduct` (range (+1) (3 :: Int) 4) +-- Index [(1,3),(1,4),(2,3),(2,4)] +cartesianProduct :: Index k -> Index g -> Index (k, g) +cartesianProduct (MkIndex xs) (MkIndex ys) + = MkIndex $ Set.cartesianProduct xs ys +{-# INLINABLE cartesianProduct #-} + + +-- | \(O\bigl(m \log\bigl(\frac{n+1}{m+1}\bigr)\bigr), \; m \leq n\). +-- @(ix1 \'contains\' ix2)@ indicates whether all keys in @ix2@ are also in @ix1@. +contains :: Ord k => Index k -> Index k -> Bool +contains (MkIndex ix1) (MkIndex ix2)= ix2 `Set.isSubsetOf` ix1 +{-# INLINABLE contains #-} + + +-- | \(O(1)\) Returns the number of keys in the index. +size :: Index k -> Int +size (MkIndex ix) = Set.size ix +{-# INLINABLE size #-} + + +-- | \(O(\log n)\). Take @n@ elements from the index, in ascending order. +-- Taking more than the number of elements in the index is a no-op: +-- +-- >>> take 10 $ fromList [1::Int,2,3] +-- Index [1,2,3] +take :: Int -> Index k -> Index k +take n (MkIndex ix) = MkIndex (Set.take n ix) +{-# INLINABLE take #-} + + +-- | \(O(\log n)\). Drop @n@ elements from the index, in ascending order. +drop :: Int -> Index k -> Index k +drop n (MkIndex ix) = MkIndex (Set.drop n ix) +{-# INLINABLE drop #-} + + +-- | \(O(n \log n)\) Map a function over keys in the index. +-- Note that since keys in an 'Index' are unique, the length of the resulting +-- index may not be the same as the input: +-- +-- >>> map (\x -> if even x then 0::Int else 1) $ fromList [0::Int,1,2,3,4] +-- Index [0,1] +-- +-- If the mapping is monotonic, see 'mapMonotonic', which has better performance +-- characteristics. +map :: Ord g => (k -> g) -> Index k -> Index g +map f (MkIndex ix) = MkIndex $ Set.map f ix +{-# INLINABLE map #-} + + +-- | \(O(n)\) Map a monotonic function over keys in the index. /Monotonic/ means that if @a < b@, then @f a < f b@. +-- Using 'mapMonononic' can be much faster than using 'map' for a large 'Index'. +-- Note that the precondiction that the function be monotonic is not checked. +-- +-- >>> mapMonotonic (+1) $ fromList [0::Int,1,2,3,4,5] +-- Index [1,2,3,4,5,6] +mapMonotonic :: (k -> g) -> Index k -> Index g +mapMonotonic f (MkIndex ix) = MkIndex $ Set.mapMonotonic f ix +{-# INLINABLE mapMonotonic #-} + + +-- | \(O(n)\) Pair each key in the index with its position in the index, starting with 0: +-- +-- @since 0.1.1.0 +-- +-- >>> indexed (fromList ['a', 'b', 'c', 'd']) +-- Index [(0,'a'),(1,'b'),(2,'c'),(3,'d')] +indexed :: Index k -> Index (Int, k) +indexed = fromDistinctAscList + . zip [0..] + . toAscList +{-# INLINABLE indexed #-} + + +-- | \(O(n)\) Filter elements satisfying a predicate. +-- +-- >>> filter even $ fromList [1::Int,2,3,4,5] +-- Index [2,4] +filter :: (k -> Bool) -> Index k -> Index k +filter p (MkIndex ix) = MkIndex $ Set.filter p ix +{-# INLINABLE filter #-} + + +-- | \(O(\log n)\). Returns the integer /index/ of a key. This function raises an exception +-- if the key is not in the 'Index'; see 'lookupIndex' for a safe version. +-- +-- >>> findIndex 'b' $ fromList ['a', 'b', 'c'] +-- 1 +findIndex :: HasCallStack => Ord k => k -> Index k -> Int +findIndex e (MkIndex ix) = Set.findIndex e ix +{-# INLINABLE findIndex #-} + + +-- | \(O(\log n)\). Returns the integer /index/ of a key, if the key is in the index. +-- +-- >>> lookupIndex 'b' $ fromList ['a', 'b', 'c'] +-- Just 1 +-- >>> lookupIndex 'd' $ fromList ['a', 'b', 'c'] +-- Nothing +lookupIndex :: Ord k => k -> Index k -> Maybe Int +lookupIndex e (MkIndex ix) = Set.lookupIndex e ix +{-# INLINABLE lookupIndex #-} + + +-- | \(O(\log n)\) Returns the element at some integer index. This function raises +-- an exception if the integer index is out-of-bounds. +elemAt :: HasCallStack => Int -> Index k -> k +elemAt n (MkIndex ix) = Set.elemAt n ix +{-# INLINABLE elemAt #-} + + +-- | \(O(\log n)\). Insert a key in an 'Index'. If the key is already +-- present, the 'Index' will not change. +insert :: Ord k => k -> Index k -> Index k +insert k (MkIndex ix) = MkIndex $ k `Set.insert` ix +{-# INLINABLE insert #-} + + +-- | \(O(\log n)\). Delete a key from an 'Index', if this key is present +-- in the index. +delete :: Ord k => k -> Index k -> Index k +delete k (MkIndex ix) = MkIndex $ k `Set.delete` ix +{-# INLINABLE delete #-} + + +-- | \(O(n \log n)\). Map each element of an 'Index' to an applicative action, +-- evaluate these actions from left to right, and collect the results. +-- +-- Note that the data type 'Index' is not a member of 'Traversable' +-- because it is not a 'Functor'. +traverse :: (Applicative f, Ord b) => (k -> f b) -> Index k -> f (Index b) +traverse f = fmap fromList . Traversable.traverse f . toAscList +{-# INLINABLE traverse #-}
src/Data/Series/Index/Internal.hs view
@@ -1,39 +1,39 @@-{-# LANGUAGE TypeFamilies #-}---------------------------------------------------------------------------------- |--- Module : Data.Series.Generic.Internal--- Copyright : (c) Laurent P. René de Cotret--- License : MIT--- Maintainer : laurent.decotret@outlook.com--- Portability : portable------ = WARNING------ This module is considered __internal__. It contains functions--- which may be unsafe to use in general, for example requiring --- the data to be pre-sorted like 'fromDistinctAscList'.------ The Package Versioning Policy still applies.--module Data.Series.Index.Internal(- Index(..),-- -- * Unsafe construction- fromAscList,- fromDistinctAscList,- fromAscVector,- fromDistinctAscVector,-- -- * Functions with unchecked pre-conditions- mapMonotonic,-- -- * Unsafe indexing- elemAt,- findIndex,--) where--import Data.Series.Index.Definition ( Index(..), fromAscList, fromDistinctAscList, fromAscVector- , fromDistinctAscVector, mapMonotonic, elemAt, findIndex- )+{-# LANGUAGE TypeFamilies #-} + +----------------------------------------------------------------------------- +-- | +-- Module : Data.Series.Generic.Internal +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT +-- Maintainer : laurent.decotret@outlook.com +-- Portability : portable +-- +-- = WARNING +-- +-- This module is considered __internal__. It contains functions +-- which may be unsafe to use in general, for example requiring +-- the data to be pre-sorted like 'fromDistinctAscList'. +-- +-- The Package Versioning Policy still applies. + +module Data.Series.Index.Internal( + Index(..), + + -- * Unsafe construction + fromAscList, + fromDistinctAscList, + fromAscVector, + fromDistinctAscVector, + + -- * Functions with unchecked pre-conditions + mapMonotonic, + + -- * Unsafe indexing + elemAt, + findIndex, + +) where + +import Data.Series.Index.Definition ( Index(..), fromAscList, fromDistinctAscList, fromAscVector + , fromDistinctAscVector, mapMonotonic, elemAt, findIndex + )
src/Data/Series/Tutorial.hs view
@@ -1,770 +1,770 @@-{-# OPTIONS_GHC -fno-warn-unused-imports #-}--module Data.Series.Tutorial (- -- * Introduction- -- $introduction-- -- * Construction- -- $construction-- -- * Index- -- $index-- -- * Selections- -- ** Single-key selection- -- $singlekey-- -- ** Bulk selections- -- $multikey-- -- * Filtering and mapping- -- $filteringandmapping-- -- * Folding- -- $folding-- -- * Grouping- -- $grouping-- -- * Window aggregation- -- $windowing-- -- * Combining 'Series' together- -- $zipping-- -- * Conclusion- -- $conclusion and further reading- - -- * Advanced topics- -- ** Handling duplicate keys- -- $duplicates-- -- ** Unboxed and generic series- -- $unboxed-- -- ** Replacing values- -- $replacement-- -- ** Comparison with other data structures- -- $comparison--) where--import Control.Foldl ( Fold )-import Data.Series ( IsSeries(..), Series, Occurrence, at, iat, select, to, from, upto, require- , groupBy, aggregateWith, (<-|), (|->), Range, windowing- )-import qualified Data.Series as Series-import qualified Data.Series.Generic-import Data.Series.Index ( Index )-import qualified Data.Series.Index as Index-import qualified Data.Series.Unboxed-import Data.Set ( Set )-import qualified Data.Set-import Data.Map.Strict ( Map )-import qualified Data.Map.Strict-import qualified Data.Map.Merge.Strict-import Numeric.Natural ( Natural)-import qualified Data.List-import qualified Data.Vector-import qualified Data.Vector.Unboxed--{- $introduction--This is a short user guide on how to get started using @javelin@ and its various modules.--The central data structure at the heart of this package is the 'Series'. A @'Series' k a@ -is a labeled array of type @v@ filled with values of type @a@, indexed by keys of type @k@.--Like 'Data.Map.Strict.Map', 'Series' support efficient:--* random access by key ( \(O(\log n)\) );-* slice by key ( \(O(\log n)\) ).--Like 'Data.Vector.Vector', 'Series' support efficient:--* numerical operations.-* random access by index ( \(O(1)\) );-* slice by index ( \(O(1)\) ); --To follow along this tutorial, the following imports are expected:-->>> import Data.Series as Series--}--{- $construction --The easiest way to create a 'Series' is to do it from a list using 'Data.Series.fromList':-->>> Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ]-index | values------ | ------- 'a' | 1- 'b' | 2- 'c' | 3- 'd' | 4--Note what happens when we have the same key (@\'a\'@) attached to multiple values:-->>> Series.fromList [ ('a', 1::Int), ('a', 0), ('b', 2), ('c', 3), ('d', 4) ]-index | values------ | ------- 'a' | 0- 'b' | 2- 'c' | 3- 'd' | 4--'Series', like 'Map's, have unique keys; therefore, the output series may -not be the same length as the input series. See further below for an -explanation of how to handle duplicate keys. --Since 'Series' are like 'Map', it's easy to convert between the two:-->>> let mp = Data.Map.Strict.fromList [ ('a', 0::Int), ('a', 1), ('b', 2), ('c', 3), ('d', 4) ]->>> mp-fromList [('a',1),('b',2),('c',3),('d',4)]->>> Series.fromStrictMap mp-index | values------ | ------- 'a' | 1- 'b' | 2- 'c' | 3- 'd' | 4--Of course, 'Series.fromLazyMap' is also available. In fact, conversion to/from 'Series' is supported for-many types; see the 'IsSeries' typeclass and its methods, 'toSeries' and 'fromSeries'.---}--{- $index--'Series' have two components: values and an index.--The index (of type @'Index' k@) is an ordered set of unique elements which allows to determine -where are each values in the series. Since all keys in an 'Index' are unique and sorted, it-is fast to find the value associated to any random key.--As we'll see soon, 'Index' is an important data structure which can be used to slice through a 'Series', -so let's get comfortable with them.-->>> import qualified Data.Series.Index as Index--An 'Index' can be constructed from a list:-->>> Index.fromList [5::Int,5,4,3,2,1,5,5,5]-Index [1,2,3,4,5]--As you see above, repeated elements (in this case, @5@) won't be repeated in the 'Index'. Therefore, it often makes -more sense to construct an 'Index' using 'Index.fromSet' from a 'Set' from "Data.Set".--One common way to construct an 'Index' is to programmatically __unfold__ a seed value using -'Index.unfoldr'. Below, we want to generate numbers from 7 down to 1:-->>> Index.unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int)-Index [1,2,3,4,5,6,7]--This task is so common that there is a convenience function to create ranges, 'Index.range'. -For example, if you want to create an 'Index' of values starting at 1 and ending at 10, in -steps of 3:-->>> Index.range (+3) (1 :: Int) 10-Index [1,4,7,10]--An 'Index' is very much like a 'Set', so you can --* check for membership using 'Index.member';-* combine two 'Index' using 'Index.union', 'Index.intersection', and 'Index.difference';-* find the integer index of a key using 'Index.lookupIndex';--and more.---}--{- $singlekey --Single-element selections are performed using 'at', which selects a single element by key. 'at' is safe;-if the key is missing, 'Nothing' is returned:-->>> let xs = Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ]->>> xs-index | values------ | ------- 'a' | 1- 'b' | 2- 'c' | 3- 'd' | 4->>> xs `at` 'a'-Just 1->>> xs `at` 'z'-Nothing---}--{- $multikey --Bulk selection, also known as *slicing*, is the method by which we extract a sub-series from a series.-In the examples below, we'll assume that we have the series @aapl_close@ is available in-scope, which represents-the closing price of Apple stock:-->>> :{-let aapl_close = Series.fromList [ ("2010-01-04", 6.5522 :: Double)- , ("2010-01-05", 6.5636)- , ("2010-01-06", 6.4592)- , ("2010-01-07", 6.4472)- , ("2010-01-08", 6.4901)- -- No prices during the weekend- , ("2010-01-11", 6.5152)- , ("2010-01-12", 6.4047)- , ("2010-01-13", 6.3642)- , ("2010-01-14", 6.4328)- , ("2010-01-15", 6.4579)- ]- :}--Bulk selection is done via the 'select' function. 'select' works with many types of inputs. -For example, we can query for a contiguous range of keys by using 'to':-->>> aapl_close `select` "2010-01-04" `to` "2010-01-08"- index | values- ----- | -------"2010-01-04" | 6.5522-"2010-01-05" | 6.5636-"2010-01-06" | 6.4592-"2010-01-07" | 6.4472-"2010-01-08" | 6.4901--You can also request unbounded ranges. For example all dates up to @"2010-01-08"@ using 'upto':-->>> aapl_close `select` upto "2010-01-08"- index | values- ----- | -------"2010-01-04" | 6.5522-"2010-01-05" | 6.5636-"2010-01-06" | 6.4592-"2010-01-07" | 6.4472-"2010-01-08" | 6.4901--There's also the other unbound range, 'from':-->>> aapl_close `select` from "2010-01-11"- index | values- ----- | -------"2010-01-11" | 6.5152-"2010-01-12" | 6.4047-"2010-01-13" | 6.3642-"2010-01-14" | 6.4328-"2010-01-15" | 6.4579--Note that the bounds may contain less data than you think! For example, -let's look at a 5-day range:-->>> aapl_close `select` "2010-01-08" `to` "2010-01-12"- index | values- ----- | -------"2010-01-08" | 6.4901-"2010-01-11" | 6.5152-"2010-01-12" | 6.4047--We've requested a range of 5 days (@"2010-01-08"@, @"2010-01-09"@, @"2010-01-10"@, @"2010-01-11"@, @"2010-01-12"@), -but there's no data in our series with the keys @"2010-01-09"@ and @"2010-01-10"@, because it was the week-end -(stock markets are usually closed on week-ends). --Sometimes you want to be more specific than a contiguous range of data; 'select' -also supports bulk *random* access like so:-->>> aapl_close `select` ["2010-01-08", "2010-01-10", "2010-01-12"]- index | values- ----- | -------"2010-01-08" | 6.4901-"2010-01-12" | 6.4047--Note above that we've requested data for the date @"2010-01-10"@, but it's missing. Therefore, -the data isn't returned. If you want to get a sub-series which has the exact index that -you've asked for, you can use 'require' in combination with an 'Index':-->>> import qualified Data.Series.Index as Index->>> aapl_close `require` Index.fromList ["2010-01-08", "2010-01-10", "2010-01-12"]- index | values- ----- | -------"2010-01-08" | Just 6.4901-"2010-01-10" | Nothing-"2010-01-12" | Just 6.4047--Using 'require' or 'select' in conjunction with 'Index.range' is very powerful.---}--{- $filteringandmapping --'Series' support operations on both their index and their values. To illustrate -this, let's load some latitude and longitude data for some cities.--We'll assume that the following types are in scope:-->>> import Data.Fixed (Centi)->>> data Position = Pos { latitude :: Centi, longitude :: Centi } deriving (Show)->>> :{- let cities = Series.fromList [ ("Paris"::String , Pos 48.86 2.35)- , ("New York City" , Pos 40.71 (-74.01))- , ("Taipei" , Pos 25.04 121.56)- , ("Buenos Aires" , Pos (-34.60) (-58.38)) - ]- :}--We can easily filter for data just like you would filter a list. -In this example, let's find cities in the western hemisphere (i.e. cities -which have negative longitudes), using 'Series.filter':-->>> Series.filter (\pos -> longitude pos < 0) cities- index | values- ----- | ------- "Buenos Aires" | Pos {latitude = -34.60, longitude = -58.38}-"New York City" | Pos {latitude = 40.71, longitude = -74.01}--We can transform the values of a 'Series' using 'Series.map'. In this example, -let's isolate the latitude of cities in the western hemisphere:-->>> let western_cities = Series.filter (\pos -> longitude pos < 0) cities->>> Series.map latitude western_cities- index | values- ----- | ------- "Buenos Aires" | -34.60-"New York City" | 40.71--Finally, we can summarize the 'Series' by reducing all its values. -Let's average the latitude of cities in the western hemisphere:-->>> import Data.Series ( mean )->>> let latitudes = Series.map latitude western_cities->>> Series.fold mean latitudes-3.05--The next section introduces 'Series.fold' more generally.--}--{- $folding--Folding refers to the action of aggregating values in a 'Series' to a single value.-Folding 'Series' is done through the 'Series.fold' function. Its type signature is:-->>> :t Series.fold-Series.fold :: Fold a b -> Series k a -> b--Here, @'Fold' a b@ represents a calculation which takes in values of type @a@, and will ultimately produce a-final value of type b. Such calculations are provided by the @foldl@ package (see 'Control.Foldl'), although-some of its functions are re-exported by "Data.Series" (and "Data.Series.Unboxed"), such as 'Data.Series.mean'.--Let's look at an example. First, we'll need some data. We'll use end-of-day stock prices for Apple Inc:-->>> import Data.Fixed ( Centi )->>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt"->>> aapl_closing - index | values- ----- | -------"1980-12-12" | 0.1007-"1980-12-15" | 9.54e-2-"1980-12-16" | 8.84e-2- ... | ...-"2022-01-05" | 174.92-"2022-01-06" | 172.0-"2022-01-07" | 172.17--Normally we would use an appropriate datetime type for the index of @aapl_closing@, -for example from the @time@ package, but we're keeping it simple for this tutorial. --Prices have changed a lot over the years, so we'll restrict ourselves to 2021:-->>> let aapl_closing_2021 = aapl_closing `select` "2021-01-01" `to` "2021-12-31"->>> aapl_closing_2021- index | values- ----- | -------"2021-01-04" | 128.6174-"2021-01-05" | 130.2076-"2021-01-06" | 125.8246- ... | ...-"2021-12-29" | 179.38-"2021-12-30" | 178.2-"2021-12-31" | 177.57--To calculate the average closing price over the year 2021, we use 'Data.Series.fold' in conjunction with-'Data.Series.mean':-->>> Series.fold Series.mean aapl_closing_2021-140.61256349206354--One of the magic things about 'Fold' is that it's possible to combine them in such a way that you can -traverse a 'Series' only once, which is important for good performance. As an example, we'll calculate-both the mean closing price AND the standard deviation of closing prices.-->>> let meanAndStdDev = (,) <$> Data.Series.mean <*> Data.Series.std->>> Series.fold meanAndStdDev aapl_closing_2021-(140.61256349206354,14.811663837435361)--See 'Control.Foldl' from the @foldl@ package for more information on 'Fold'.--}--{- $grouping--One important feature of 'Series' is the ability to efficiently group values -together based on their keys.--Let's load some stock price data again for this part:-->>> import Data.Fixed ( Centi )->>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt"->>> aapl_closing - index | values- ----- | -------"1980-12-12" | 0.1007-"1980-12-15" | 9.54e-2-"1980-12-16" | 8.84e-2- ... | ...-"2022-01-05" | 174.92-"2022-01-06" | 172.0-"2022-01-07" | 172.17--Grouping involves two steps:-- (1) Grouping keys in some way using 'groupBy';- (2) Aggregating the values in each group using 'aggregateWith' or other variants.--Let's find the highest closing price of each month. First, we need to define-our grouping function:-->>> :{ - -- | Extract the year and month from a date like XXXX-YY-ZZ. For example:- -- - -- >>> month "2023-01-01"- -- "2023-01"- month :: String -> String- month = take 7- :}--Then, we can group keys by month and take the 'maximum' of each group:-->>> aapl_closing `groupBy` month `aggregateWith` maximum- index | values- ----- | -------"1980-12" | 0.1261-"1981-01" | 0.1208-"1981-02" | 0.1007- ... | ...-"2021-11" | 165.3-"2021-12" | 180.33-"2022-01" | 182.01--This means, for example, that the maximum closing price for Apple stock in the -month of November 2021 was $165.30 per share. This library also contains -numerical aggregation functions such as 'Data.Series.mean' and 'Data.Series.std'. Therefore, in order -to find the monthly average Apple closing price, rounded to the nearest cent:-->>> import Data.Series (mean)->>> let (roundToCent :: Double -> Double) = \x -> fromIntegral ((round $ x * 100) :: Int) / 100->>> aapl_closing `groupBy` month `aggregateWith` (roundToCent . Series.fold mean)- index | values- ----- | -------"1980-12" | 0.11-"1981-01" | 0.11-"1981-02" | 9.0e-2- ... | ...-"2021-11" | 154.21-"2021-12" | 173.55-"2022-01" | 176.16---}--{- $windowing--Windowing aggregation refers to the practice of aggregating values in a window around every key.--General-purpose windowing is done using the 'windowing' function. Let's look at its-type signature:-->>> :t windowing-windowing- :: Ord k =>- (k -> Range k) -> (Series k a -> b) -> Series k a -> Series k b--Here, @`windowing` window aggfunc xs@ is a new series @'Series' k b@ where-for every key @k@, the values in the range @window k@ are aggregated by @aggfunc@-and placed in the resulting series at key @k@. Here's an example where-for every key @k@, we add the values at @k@ and @k+1@:-->>> :{ -let (xs :: Series Int Int) - = Series.fromList [ (1, 0)- , (2, 1)- , (3, 2)- , (4, 3)- , (5, 4)- , (6, 5)- ]-in windowing (\k -> k `to` (k + 1)) sum xs-:}-index | values------ | ------- 1 | 1- 2 | 3- 3 | 5- 4 | 7- 5 | 9- 6 | 5--'windowing' can be used to compute so-called rolling aggregations. An example of-this is to compute the rolling mean of the last 3 keys:-->>> import Data.Series ( mean )->>> :{ -let rollingMean = windowing (\k -> (k-3) `to` k) (Series.fold mean)- (xs :: Series Int Double) - = Series.fromList [ (1, 0)- , (2, 1)- , (3, 2)- , (4, 3)- , (5, 4)- , (6, 5)- ]- in (rollingMean xs) :: Series Int Double-:}-index | values------ | ------- 1 | 0.0- 2 | 0.5- 3 | 1.0- 4 | 1.5- 5 | 2.5- 6 | 3.5---}--{- $zipping --An important class of operations are combining two 'Series' together, also known as *zipping*. -For lists, Haskell has 'Data.List.zipWith'. 'Series' also have 'Series.zipWith' and variants:--* 'Series.zipWith', which combines two series with some elementwise function;-* 'Series.zipWithMatched', which combines two series with some elementwise function - on keys which are in *both* maps;-* 'Series.zipWithStrategy', which combines two series with some elementwise - function and supports custom operations to deal with missing keys;--To illustrate the differences between the various zipping functions, -consider the following two series. There's population:-->>> :set -XNumericUnderscores->>> import Data.Fixed (Centi)->>> :{ - -- Most recent population estimate rounded to the nearest million- let population = Series.fromList [ ("Canada"::String, 40_000_000::Centi)- , ("Kenya" , 56_000_000)- , ("Poland" , 38_000_000)- , ("Singapore" , 6_000_000)- ]- :}--and there's total land mass:-->>> :{ - -- Land mass in square kilometer- let landmass = Series.fromList [ ("Brazil"::String, 8_520_000::Centi)- , ("Canada", 9_990_000)- , ("Kenya", 580_000)- , ("Poland", 313_000)- ] - :}--@'Series.zipWith' f left right@ combines the series @left@ and @right@ using the -function @f@ which admits two arguments, for all keys one-by-one. If a key -is missing from either @left@ or @right@, 'Series.zipWith' returns 'Nothing'. For example, -the population density per country would be:-->>> Series.zipWith (/) population landmass- index | values- ----- | ------- "Brazil" | Nothing- "Canada" | Just 4.00- "Kenya" | Just 96.55- "Poland" | Just 121.40-"Singapore" | Nothing--Since we don't have population estimates for Brazil and no land mass -information for Singapore, we can't calculate their population densities.--Sometimes, we only care about the results of @'Series.zipWith' f@ where keys are -in both series. In this case, we can use 'Series.zipWithMatched':-->>> Series.zipWithMatched (/) population landmass- index | values- ----- | -------"Canada" | 4.00- "Kenya" | 96.55-"Poland" | 121.40--Finally, in case we want full control over what to do when a key is missing, -we can use @Series.zipWithStrategy'. For example, consider the case where:--* If population numbers are missing, I want to set the density to 0;-* If land mass information is missing, I wait to skip calculating the density of this country. -->>> import Data.Series (skipStrategy, constStrategy)->>> let noPopulationStrategy = Series.constStrategy 0->>> let noLandmassStrategy = Series.skipStrategy->>> Series.zipWithStrategy (/) noPopulationStrategy noLandmassStrategy population landmass- index | values- ----- | ------- "Canada" | 4.00- "Kenya" | 96.55- "Poland" | 121.40-"Singapore" | 0.00--As you can imagine, 'Series.zipWithStrategy' is the most general and gives the most control, but is less easy -to use than 'Series.zipWith' and 'Series.zipWithMatched'.---}--{- $conclusion--This section concludes the introductory tutorial to the @javelin@ package and its "Data.Series" module.--For a more in-depth look at this package, you can read the full documentation for each module:--* "Data.Series"-* "Data.Series.Index"-* "Data.Series.Unboxed"-* "Data.Series.Generic"---}--{- $duplicates--If you must build a 'Series' with duplicate keys, you can use the 'Data.Series.fromListDuplicates' or -'Data.Series.fromVectorDuplicates' functions. -In the example below, the key @\'d\'@ is repeated three times:-->>> Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]- index | values- ----- | -------('a',0) | 5-('b',0) | 0-('d',0) | 1-('d',1) | -4-('d',2) | 7--Note that the 'Series' produced by 'Data.Series.fromListDuplicates' still has unique keys, but each key is a -composite of a character and an occurrence. This is reflected in the type:-->>> :t Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]-Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]- :: Series (Char, Occurrence) Int--Here, 'Data.Series.Occurrence' is a non-negative number, and can be converted to -other integer-like numbers using 'fromIntegral'. In practice, you should aim to aggregate your 'Series' to remove duplicate keys, for example-using 'Data.Series.groupBy' and grouping on the first element of the key ('fst'):-->>> let xs = Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]->>> xs `groupBy` fst `aggregateWith` sum-index | values------ | ------- 'a' | 5- 'b' | 0- 'd' | 4---}--{- $unboxed --The 'Data.Series.Series' defined in "Data.Series" are based on 'Data.Vector.Vector' from "Data.Vector". -This implementation is nice because such 'Series' can hold _any_ Haskell type. However, because-Haskell types can be arbitrarily complex, numerical operations on 'Series' may not be as fast-as could be.--For simpler types such as 'Double' and 'Int', a different kind of series can be used to-speed up numerical calculations: 'Data.Series.Unboxed.Series' from the "Data.Series.Unboxed" module.-Such 'Data.Series.Unboxed.Series' are much more limited: they can only contain datatypes which are-instances of 'Data.Vector.Unboxed.Unbox'. --This then brings the question: how can you write software which supports both ordinary 'Data.Series.Series'-__and__ unboxed 'Data.Series.Unboxed.Series'? The answer is to use functions from the "Data.Series.Generic".--For example, we could implement the dot product of two series as:-->>> import qualified Data.Series.Generic as G->>> import Data.Vector.Generic ( Vector )->>> :{- dot :: (Ord k, Num a, Vector v a) => G.Series v k a -> G.Series v k a -> a- dot v1 v2 = G.sum $ G.zipWithMatched (*) v1 v2- :}--You can convert between the two types of series using the 'Data.Series.Generic.convert' function.---}--{- $replacement --'Series.map' allows to map every value of a series. How about replacing *some* -values in a series? The function 'Data.Series.replace' (and its infix variant, '|->') replaces values in the right operand -which have an analogue in the left operand:-->>> import Data.Series ( (|->) )->>> let nan = (0/0) :: Double->>> let right = Series.fromList [('a', 1), ('b', nan), ('c', 3), ('d', nan)]->>> right-index | values------ | ------- 'a' | 1.0- 'b' | NaN- 'c' | 3.0- 'd' | NaN->>> let left = Series.fromList [('b', 0::Double), ('d', 0), ('e', 0)]->>> left-index | values------ | ------- 'b' | 0.0- 'd' | 0.0- 'e' | 0.0->>> left |-> right-index | values------ | ------- 'a' | 1.0- 'b' | 0.0- 'c' | 3.0- 'd' | 0.0--In the example above, the key @\'e\'@ is ignored since it was not in the @right@ -series to begin with.--The flipped version, '<-|', is also available.---}--{- $comparison --Below is a table showing which operations on "Data.Series" have analogues for -other data structures.--+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Action | "Data.Series" | "Data.Map.Strict" | "Data.List" | "Data.Vector" |-+=================================+================================+=================================+===================+======================+-| Mapping values | 'Data.Series.map' | 'Data.Map.Strict.map' | 'map' | 'Data.Vector.map' |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Mapping index | 'Data.Series.mapIndex' | 'Data.Map.Strict.mapKeys' | | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Mapping values with key | 'Data.Series.mapWithKey' | 'Data.Map.Strict.mapWithKey' | | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Filtering values | 'Data.Series.filter' | 'Data.Map.Strict.filter' | 'filter' | 'Data.Vector.filter' |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Filtering index | 'Data.Series.select', | 'Data.Map.Strict.filterWithKey' | | |-| | 'Data.Series.filterWithKey' | | | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Indexing by key | 'Data.Series.at' | 'Data.Map.Strict.lookup' | | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Indexing by position | 'Data.Series.iat' | | 'Data.List.!' | 'Data.Vector.!' |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Combine two structures key-wise | 'Data.Series.zipWith' | 'Data.Map.Merge.Strict.merge' | | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Union | 'Data.Series.<>' | 'Data.Map.Strict.union' | 'Data.List.union' | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+-| Group keys | 'Data.Series.groupBy' | | | |-+---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+---}+{-# OPTIONS_GHC -fno-warn-unused-imports #-} + +module Data.Series.Tutorial ( + -- * Introduction + -- $introduction + + -- * Construction + -- $construction + + -- * Index + -- $index + + -- * Selections + -- ** Single-key selection + -- $singlekey + + -- ** Bulk selections + -- $multikey + + -- * Filtering and mapping + -- $filteringandmapping + + -- * Folding + -- $folding + + -- * Grouping + -- $grouping + + -- * Window aggregation + -- $windowing + + -- * Combining 'Series' together + -- $zipping + + -- * Conclusion + -- $conclusion and further reading + + -- * Advanced topics + -- ** Handling duplicate keys + -- $duplicates + + -- ** Unboxed and generic series + -- $unboxed + + -- ** Replacing values + -- $replacement + + -- ** Comparison with other data structures + -- $comparison + +) where + +import Control.Foldl ( Fold ) +import Data.Series ( IsSeries(..), Series, Occurrence, at, iat, select, to, from, upto, require + , groupBy, aggregateWith, (<-|), (|->), Range, windowing + ) +import qualified Data.Series as Series +import qualified Data.Series.Generic +import Data.Series.Index ( Index ) +import qualified Data.Series.Index as Index +import qualified Data.Series.Unboxed +import Data.Set ( Set ) +import qualified Data.Set +import Data.Map.Strict ( Map ) +import qualified Data.Map.Strict +import qualified Data.Map.Merge.Strict +import Numeric.Natural ( Natural) +import qualified Data.List +import qualified Data.Vector +import qualified Data.Vector.Unboxed + +{- $introduction + +This is a short user guide on how to get started using @javelin@ and its various modules. + +The central data structure at the heart of this package is the 'Series'. A @'Series' k a@ +is a labeled array of type @v@ filled with values of type @a@, indexed by keys of type @k@. + +Like 'Data.Map.Strict.Map', 'Series' support efficient: + +* random access by key ( \(O(\log n)\) ); +* slice by key ( \(O(\log n)\) ). + +Like 'Data.Vector.Vector', 'Series' support efficient: + +* numerical operations. +* random access by index ( \(O(1)\) ); +* slice by index ( \(O(1)\) ); + +To follow along this tutorial, the following imports are expected: + +>>> import Data.Series as Series +-} + +{- $construction + +The easiest way to create a 'Series' is to do it from a list using 'Data.Series.fromList': + +>>> Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ] +index | values +----- | ------ + 'a' | 1 + 'b' | 2 + 'c' | 3 + 'd' | 4 + +Note what happens when we have the same key (@\'a\'@) attached to multiple values: + +>>> Series.fromList [ ('a', 1::Int), ('a', 0), ('b', 2), ('c', 3), ('d', 4) ] +index | values +----- | ------ + 'a' | 0 + 'b' | 2 + 'c' | 3 + 'd' | 4 + +'Series', like 'Map's, have unique keys; therefore, the output series may +not be the same length as the input series. See further below for an +explanation of how to handle duplicate keys. + +Since 'Series' are like 'Map', it's easy to convert between the two: + +>>> let mp = Data.Map.Strict.fromList [ ('a', 0::Int), ('a', 1), ('b', 2), ('c', 3), ('d', 4) ] +>>> mp +fromList [('a',1),('b',2),('c',3),('d',4)] +>>> Series.fromStrictMap mp +index | values +----- | ------ + 'a' | 1 + 'b' | 2 + 'c' | 3 + 'd' | 4 + +Of course, 'Series.fromLazyMap' is also available. In fact, conversion to/from 'Series' is supported for +many types; see the 'IsSeries' typeclass and its methods, 'toSeries' and 'fromSeries'. + +-} + +{- $index + +'Series' have two components: values and an index. + +The index (of type @'Index' k@) is an ordered set of unique elements which allows to determine +where are each values in the series. Since all keys in an 'Index' are unique and sorted, it +is fast to find the value associated to any random key. + +As we'll see soon, 'Index' is an important data structure which can be used to slice through a 'Series', +so let's get comfortable with them. + +>>> import qualified Data.Series.Index as Index + +An 'Index' can be constructed from a list: + +>>> Index.fromList [5::Int,5,4,3,2,1,5,5,5] +Index [1,2,3,4,5] + +As you see above, repeated elements (in this case, @5@) won't be repeated in the 'Index'. Therefore, it often makes +more sense to construct an 'Index' using 'Index.fromSet' from a 'Set' from "Data.Set". + +One common way to construct an 'Index' is to programmatically __unfold__ a seed value using +'Index.unfoldr'. Below, we want to generate numbers from 7 down to 1: + +>>> Index.unfoldr (\x -> if x < 1 then Nothing else Just (x, x-1)) (7 :: Int) +Index [1,2,3,4,5,6,7] + +This task is so common that there is a convenience function to create ranges, 'Index.range'. +For example, if you want to create an 'Index' of values starting at 1 and ending at 10, in +steps of 3: + +>>> Index.range (+3) (1 :: Int) 10 +Index [1,4,7,10] + +An 'Index' is very much like a 'Set', so you can + +* check for membership using 'Index.member'; +* combine two 'Index' using 'Index.union', 'Index.intersection', and 'Index.difference'; +* find the integer index of a key using 'Index.lookupIndex'; + +and more. + +-} + +{- $singlekey + +Single-element selections are performed using 'at', which selects a single element by key. 'at' is safe; +if the key is missing, 'Nothing' is returned: + +>>> let xs = Series.fromList [ ('a', 1::Int), ('b', 2), ('c', 3), ('d', 4) ] +>>> xs +index | values +----- | ------ + 'a' | 1 + 'b' | 2 + 'c' | 3 + 'd' | 4 +>>> xs `at` 'a' +Just 1 +>>> xs `at` 'z' +Nothing + +-} + +{- $multikey + +Bulk selection, also known as *slicing*, is the method by which we extract a sub-series from a series. +In the examples below, we'll assume that we have the series @aapl_close@ is available in-scope, which represents +the closing price of Apple stock: + +>>> :{ +let aapl_close = Series.fromList [ ("2010-01-04", 6.5522 :: Double) + , ("2010-01-05", 6.5636) + , ("2010-01-06", 6.4592) + , ("2010-01-07", 6.4472) + , ("2010-01-08", 6.4901) + -- No prices during the weekend + , ("2010-01-11", 6.5152) + , ("2010-01-12", 6.4047) + , ("2010-01-13", 6.3642) + , ("2010-01-14", 6.4328) + , ("2010-01-15", 6.4579) + ] + :} + +Bulk selection is done via the 'select' function. 'select' works with many types of inputs. +For example, we can query for a contiguous range of keys by using 'to': + +>>> aapl_close `select` "2010-01-04" `to` "2010-01-08" + index | values + ----- | ------ +"2010-01-04" | 6.5522 +"2010-01-05" | 6.5636 +"2010-01-06" | 6.4592 +"2010-01-07" | 6.4472 +"2010-01-08" | 6.4901 + +You can also request unbounded ranges. For example all dates up to @"2010-01-08"@ using 'upto': + +>>> aapl_close `select` upto "2010-01-08" + index | values + ----- | ------ +"2010-01-04" | 6.5522 +"2010-01-05" | 6.5636 +"2010-01-06" | 6.4592 +"2010-01-07" | 6.4472 +"2010-01-08" | 6.4901 + +There's also the other unbound range, 'from': + +>>> aapl_close `select` from "2010-01-11" + index | values + ----- | ------ +"2010-01-11" | 6.5152 +"2010-01-12" | 6.4047 +"2010-01-13" | 6.3642 +"2010-01-14" | 6.4328 +"2010-01-15" | 6.4579 + +Note that the bounds may contain less data than you think! For example, +let's look at a 5-day range: + +>>> aapl_close `select` "2010-01-08" `to` "2010-01-12" + index | values + ----- | ------ +"2010-01-08" | 6.4901 +"2010-01-11" | 6.5152 +"2010-01-12" | 6.4047 + +We've requested a range of 5 days (@"2010-01-08"@, @"2010-01-09"@, @"2010-01-10"@, @"2010-01-11"@, @"2010-01-12"@), +but there's no data in our series with the keys @"2010-01-09"@ and @"2010-01-10"@, because it was the week-end +(stock markets are usually closed on week-ends). + +Sometimes you want to be more specific than a contiguous range of data; 'select' +also supports bulk *random* access like so: + +>>> aapl_close `select` ["2010-01-08", "2010-01-10", "2010-01-12"] + index | values + ----- | ------ +"2010-01-08" | 6.4901 +"2010-01-12" | 6.4047 + +Note above that we've requested data for the date @"2010-01-10"@, but it's missing. Therefore, +the data isn't returned. If you want to get a sub-series which has the exact index that +you've asked for, you can use 'require' in combination with an 'Index': + +>>> import qualified Data.Series.Index as Index +>>> aapl_close `require` Index.fromList ["2010-01-08", "2010-01-10", "2010-01-12"] + index | values + ----- | ------ +"2010-01-08" | Just 6.4901 +"2010-01-10" | Nothing +"2010-01-12" | Just 6.4047 + +Using 'require' or 'select' in conjunction with 'Index.range' is very powerful. + +-} + +{- $filteringandmapping + +'Series' support operations on both their index and their values. To illustrate +this, let's load some latitude and longitude data for some cities. + +We'll assume that the following types are in scope: + +>>> import Data.Fixed (Centi) +>>> data Position = Pos { latitude :: Centi, longitude :: Centi } deriving (Show) +>>> :{ + let cities = Series.fromList [ ("Paris"::String , Pos 48.86 2.35) + , ("New York City" , Pos 40.71 (-74.01)) + , ("Taipei" , Pos 25.04 121.56) + , ("Buenos Aires" , Pos (-34.60) (-58.38)) + ] + :} + +We can easily filter for data just like you would filter a list. +In this example, let's find cities in the western hemisphere (i.e. cities +which have negative longitudes), using 'Series.filter': + +>>> Series.filter (\pos -> longitude pos < 0) cities + index | values + ----- | ------ + "Buenos Aires" | Pos {latitude = -34.60, longitude = -58.38} +"New York City" | Pos {latitude = 40.71, longitude = -74.01} + +We can transform the values of a 'Series' using 'Series.map'. In this example, +let's isolate the latitude of cities in the western hemisphere: + +>>> let western_cities = Series.filter (\pos -> longitude pos < 0) cities +>>> Series.map latitude western_cities + index | values + ----- | ------ + "Buenos Aires" | -34.60 +"New York City" | 40.71 + +Finally, we can summarize the 'Series' by reducing all its values. +Let's average the latitude of cities in the western hemisphere: + +>>> import Data.Series ( mean ) +>>> let latitudes = Series.map latitude western_cities +>>> Series.fold mean latitudes +3.05 + +The next section introduces 'Series.fold' more generally. +-} + +{- $folding + +Folding refers to the action of aggregating values in a 'Series' to a single value. +Folding 'Series' is done through the 'Series.fold' function. Its type signature is: + +>>> :t Series.fold +Series.fold :: Fold a b -> Series k a -> b + +Here, @'Fold' a b@ represents a calculation which takes in values of type @a@, and will ultimately produce a +final value of type b. Such calculations are provided by the @foldl@ package (see 'Control.Foldl'), although +some of its functions are re-exported by "Data.Series" (and "Data.Series.Unboxed"), such as 'Data.Series.mean'. + +Let's look at an example. First, we'll need some data. We'll use end-of-day stock prices for Apple Inc: + +>>> import Data.Fixed ( Centi ) +>>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt" +>>> aapl_closing + index | values + ----- | ------ +"1980-12-12" | 0.1007 +"1980-12-15" | 9.54e-2 +"1980-12-16" | 8.84e-2 + ... | ... +"2022-01-05" | 174.92 +"2022-01-06" | 172.0 +"2022-01-07" | 172.17 + +Normally we would use an appropriate datetime type for the index of @aapl_closing@, +for example from the @time@ package, but we're keeping it simple for this tutorial. + +Prices have changed a lot over the years, so we'll restrict ourselves to 2021: + +>>> let aapl_closing_2021 = aapl_closing `select` "2021-01-01" `to` "2021-12-31" +>>> aapl_closing_2021 + index | values + ----- | ------ +"2021-01-04" | 128.6174 +"2021-01-05" | 130.2076 +"2021-01-06" | 125.8246 + ... | ... +"2021-12-29" | 179.38 +"2021-12-30" | 178.2 +"2021-12-31" | 177.57 + +To calculate the average closing price over the year 2021, we use 'Data.Series.fold' in conjunction with +'Data.Series.mean': + +>>> Series.fold Series.mean aapl_closing_2021 +140.61256349206354 + +One of the magic things about 'Fold' is that it's possible to combine them in such a way that you can +traverse a 'Series' only once, which is important for good performance. As an example, we'll calculate +both the mean closing price AND the standard deviation of closing prices. + +>>> let meanAndStdDev = (,) <$> Data.Series.mean <*> Data.Series.std +>>> Series.fold meanAndStdDev aapl_closing_2021 +(140.61256349206354,14.811663837435361) + +See 'Control.Foldl' from the @foldl@ package for more information on 'Fold'. +-} + +{- $grouping + +One important feature of 'Series' is the ability to efficiently group values +together based on their keys. + +Let's load some stock price data again for this part: + +>>> import Data.Fixed ( Centi ) +>>> (aapl_closing :: Series String Double) <- (Series.fromList . read) <$> readFile "files/aapl.txt" +>>> aapl_closing + index | values + ----- | ------ +"1980-12-12" | 0.1007 +"1980-12-15" | 9.54e-2 +"1980-12-16" | 8.84e-2 + ... | ... +"2022-01-05" | 174.92 +"2022-01-06" | 172.0 +"2022-01-07" | 172.17 + +Grouping involves two steps: + + (1) Grouping keys in some way using 'groupBy'; + (2) Aggregating the values in each group using 'aggregateWith' or other variants. + +Let's find the highest closing price of each month. First, we need to define +our grouping function: + +>>> :{ + -- | Extract the year and month from a date like XXXX-YY-ZZ. For example: + -- + -- >>> month "2023-01-01" + -- "2023-01" + month :: String -> String + month = take 7 + :} + +Then, we can group keys by month and take the 'maximum' of each group: + +>>> aapl_closing `groupBy` month `aggregateWith` maximum + index | values + ----- | ------ +"1980-12" | 0.1261 +"1981-01" | 0.1208 +"1981-02" | 0.1007 + ... | ... +"2021-11" | 165.3 +"2021-12" | 180.33 +"2022-01" | 182.01 + +This means, for example, that the maximum closing price for Apple stock in the +month of November 2021 was $165.30 per share. This library also contains +numerical aggregation functions such as 'Data.Series.mean' and 'Data.Series.std'. Therefore, in order +to find the monthly average Apple closing price, rounded to the nearest cent: + +>>> import Data.Series (mean) +>>> let (roundToCent :: Double -> Double) = \x -> fromIntegral ((round $ x * 100) :: Int) / 100 +>>> aapl_closing `groupBy` month `aggregateWith` (roundToCent . Series.fold mean) + index | values + ----- | ------ +"1980-12" | 0.11 +"1981-01" | 0.11 +"1981-02" | 9.0e-2 + ... | ... +"2021-11" | 154.21 +"2021-12" | 173.55 +"2022-01" | 176.16 + +-} + +{- $windowing + +Windowing aggregation refers to the practice of aggregating values in a window around every key. + +General-purpose windowing is done using the 'windowing' function. Let's look at its +type signature: + +>>> :t windowing +windowing + :: Ord k => + (k -> Range k) -> (Series k a -> b) -> Series k a -> Series k b + +Here, @`windowing` window aggfunc xs@ is a new series @'Series' k b@ where +for every key @k@, the values in the range @window k@ are aggregated by @aggfunc@ +and placed in the resulting series at key @k@. Here's an example where +for every key @k@, we add the values at @k@ and @k+1@: + +>>> :{ +let (xs :: Series Int Int) + = Series.fromList [ (1, 0) + , (2, 1) + , (3, 2) + , (4, 3) + , (5, 4) + , (6, 5) + ] +in windowing (\k -> k `to` (k + 1)) sum xs +:} +index | values +----- | ------ + 1 | 1 + 2 | 3 + 3 | 5 + 4 | 7 + 5 | 9 + 6 | 5 + +'windowing' can be used to compute so-called rolling aggregations. An example of +this is to compute the rolling mean of the last 3 keys: + +>>> import Data.Series ( mean ) +>>> :{ +let rollingMean = windowing (\k -> (k-3) `to` k) (Series.fold mean) + (xs :: Series Int Double) + = Series.fromList [ (1, 0) + , (2, 1) + , (3, 2) + , (4, 3) + , (5, 4) + , (6, 5) + ] + in (rollingMean xs) :: Series Int Double +:} +index | values +----- | ------ + 1 | 0.0 + 2 | 0.5 + 3 | 1.0 + 4 | 1.5 + 5 | 2.5 + 6 | 3.5 + +-} + +{- $zipping + +An important class of operations are combining two 'Series' together, also known as *zipping*. +For lists, Haskell has 'Data.List.zipWith'. 'Series' also have 'Series.zipWith' and variants: + +* 'Series.zipWith', which combines two series with some elementwise function; +* 'Series.zipWithMatched', which combines two series with some elementwise function + on keys which are in *both* maps; +* 'Series.zipWithStrategy', which combines two series with some elementwise + function and supports custom operations to deal with missing keys; + +To illustrate the differences between the various zipping functions, +consider the following two series. There's population: + +>>> :set -XNumericUnderscores +>>> import Data.Fixed (Centi) +>>> :{ + -- Most recent population estimate rounded to the nearest million + let population = Series.fromList [ ("Canada"::String, 40_000_000::Centi) + , ("Kenya" , 56_000_000) + , ("Poland" , 38_000_000) + , ("Singapore" , 6_000_000) + ] + :} + +and there's total land mass: + +>>> :{ + -- Land mass in square kilometer + let landmass = Series.fromList [ ("Brazil"::String, 8_520_000::Centi) + , ("Canada", 9_990_000) + , ("Kenya", 580_000) + , ("Poland", 313_000) + ] + :} + +@'Series.zipWith' f left right@ combines the series @left@ and @right@ using the +function @f@ which admits two arguments, for all keys one-by-one. If a key +is missing from either @left@ or @right@, 'Series.zipWith' returns 'Nothing'. For example, +the population density per country would be: + +>>> Series.zipWith (/) population landmass + index | values + ----- | ------ + "Brazil" | Nothing + "Canada" | Just 4.00 + "Kenya" | Just 96.55 + "Poland" | Just 121.40 +"Singapore" | Nothing + +Since we don't have population estimates for Brazil and no land mass +information for Singapore, we can't calculate their population densities. + +Sometimes, we only care about the results of @'Series.zipWith' f@ where keys are +in both series. In this case, we can use 'Series.zipWithMatched': + +>>> Series.zipWithMatched (/) population landmass + index | values + ----- | ------ +"Canada" | 4.00 + "Kenya" | 96.55 +"Poland" | 121.40 + +Finally, in case we want full control over what to do when a key is missing, +we can use @Series.zipWithStrategy'. For example, consider the case where: + +* If population numbers are missing, I want to set the density to 0; +* If land mass information is missing, I wait to skip calculating the density of this country. + +>>> import Data.Series (skipStrategy, constStrategy) +>>> let noPopulationStrategy = Series.constStrategy 0 +>>> let noLandmassStrategy = Series.skipStrategy +>>> Series.zipWithStrategy (/) noPopulationStrategy noLandmassStrategy population landmass + index | values + ----- | ------ + "Canada" | 4.00 + "Kenya" | 96.55 + "Poland" | 121.40 +"Singapore" | 0.00 + +As you can imagine, 'Series.zipWithStrategy' is the most general and gives the most control, but is less easy +to use than 'Series.zipWith' and 'Series.zipWithMatched'. + +-} + +{- $conclusion + +This section concludes the introductory tutorial to the @javelin@ package and its "Data.Series" module. + +For a more in-depth look at this package, you can read the full documentation for each module: + +* "Data.Series" +* "Data.Series.Index" +* "Data.Series.Unboxed" +* "Data.Series.Generic" + +-} + +{- $duplicates + +If you must build a 'Series' with duplicate keys, you can use the 'Data.Series.fromListDuplicates' or +'Data.Series.fromVectorDuplicates' functions. +In the example below, the key @\'d\'@ is repeated three times: + +>>> Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] + index | values + ----- | ------ +('a',0) | 5 +('b',0) | 0 +('d',0) | 1 +('d',1) | -4 +('d',2) | 7 + +Note that the 'Series' produced by 'Data.Series.fromListDuplicates' still has unique keys, but each key is a +composite of a character and an occurrence. This is reflected in the type: + +>>> :t Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] +Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] + :: Series (Char, Occurrence) Int + +Here, 'Data.Series.Occurrence' is a non-negative number, and can be converted to +other integer-like numbers using 'fromIntegral'. In practice, you should aim to aggregate your 'Series' to remove duplicate keys, for example +using 'Data.Series.groupBy' and grouping on the first element of the key ('fst'): + +>>> let xs = Series.fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] +>>> xs `groupBy` fst `aggregateWith` sum +index | values +----- | ------ + 'a' | 5 + 'b' | 0 + 'd' | 4 + +-} + +{- $unboxed + +The 'Data.Series.Series' defined in "Data.Series" are based on 'Data.Vector.Vector' from "Data.Vector". +This implementation is nice because such 'Series' can hold _any_ Haskell type. However, because +Haskell types can be arbitrarily complex, numerical operations on 'Series' may not be as fast +as could be. + +For simpler types such as 'Double' and 'Int', a different kind of series can be used to +speed up numerical calculations: 'Data.Series.Unboxed.Series' from the "Data.Series.Unboxed" module. +Such 'Data.Series.Unboxed.Series' are much more limited: they can only contain datatypes which are +instances of 'Data.Vector.Unboxed.Unbox'. + +This then brings the question: how can you write software which supports both ordinary 'Data.Series.Series' +__and__ unboxed 'Data.Series.Unboxed.Series'? The answer is to use functions from the "Data.Series.Generic". + +For example, we could implement the dot product of two series as: + +>>> import qualified Data.Series.Generic as G +>>> import Data.Vector.Generic ( Vector ) +>>> :{ + dot :: (Ord k, Num a, Vector v a) => G.Series v k a -> G.Series v k a -> a + dot v1 v2 = G.sum $ G.zipWithMatched (*) v1 v2 + :} + +You can convert between the two types of series using the 'Data.Series.Generic.convert' function. + +-} + +{- $replacement + +'Series.map' allows to map every value of a series. How about replacing *some* +values in a series? The function 'Data.Series.replace' (and its infix variant, '|->') replaces values in the right operand +which have an analogue in the left operand: + +>>> import Data.Series ( (|->) ) +>>> let nan = (0/0) :: Double +>>> let right = Series.fromList [('a', 1), ('b', nan), ('c', 3), ('d', nan)] +>>> right +index | values +----- | ------ + 'a' | 1.0 + 'b' | NaN + 'c' | 3.0 + 'd' | NaN +>>> let left = Series.fromList [('b', 0::Double), ('d', 0), ('e', 0)] +>>> left +index | values +----- | ------ + 'b' | 0.0 + 'd' | 0.0 + 'e' | 0.0 +>>> left |-> right +index | values +----- | ------ + 'a' | 1.0 + 'b' | 0.0 + 'c' | 3.0 + 'd' | 0.0 + +In the example above, the key @\'e\'@ is ignored since it was not in the @right@ +series to begin with. + +The flipped version, '<-|', is also available. + +-} + +{- $comparison + +Below is a table showing which operations on "Data.Series" have analogues for +other data structures. + ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Action | "Data.Series" | "Data.Map.Strict" | "Data.List" | "Data.Vector" | ++=================================+================================+=================================+===================+======================+ +| Mapping values | 'Data.Series.map' | 'Data.Map.Strict.map' | 'map' | 'Data.Vector.map' | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Mapping index | 'Data.Series.mapIndex' | 'Data.Map.Strict.mapKeys' | | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Mapping values with key | 'Data.Series.mapWithKey' | 'Data.Map.Strict.mapWithKey' | | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Filtering values | 'Data.Series.filter' | 'Data.Map.Strict.filter' | 'filter' | 'Data.Vector.filter' | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Filtering index | 'Data.Series.select', | 'Data.Map.Strict.filterWithKey' | | | +| | 'Data.Series.filterWithKey' | | | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Indexing by key | 'Data.Series.at' | 'Data.Map.Strict.lookup' | | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Indexing by position | 'Data.Series.iat' | | 'Data.List.!' | 'Data.Vector.!' | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Combine two structures key-wise | 'Data.Series.zipWith' | 'Data.Map.Merge.Strict.merge' | | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Union | 'Data.Series.<>' | 'Data.Map.Strict.union' | 'Data.List.union' | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ +| Group keys | 'Data.Series.groupBy' | | | | ++---------------------------------+--------------------------------+---------------------------------+-------------------+----------------------+ + +-}
src/Data/Series/Unboxed.hs view
@@ -1,1291 +1,1291 @@--------------------------------------------------------------------------------- |--- Module : Data.Series.Unboxed--- Copyright : (c) Laurent P. René de Cotret--- License : MIT--- Maintainer : laurent.decotret@outlook.com--- Portability : portable------ This module contains data structures and functions to work with 'Series' capable of holding unboxed values,--- i.e. values of types which are instances of `Unbox`.------ = Why use unboxed series?------ Unboxed series can have much better performance, at the cost of less flexibility. For example,--- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of --- `Functor` or `Foldable`.------ If you are hesitating, you should prefer the series implementation in the "Data.Series" module.------ = Introduction to series------ A 'Series' of type @Series k a@ is a labeled array of values of type @a@,--- indexed by keys of type @k@.------ Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient:------ * random access by key ( \(O(\log n)\) );--- * slice by key ( \(O(\log n)\) ).------ Like `Data.Vector.Vector`, they support efficient:------ * random access by index ( \(O(1)\) );--- * slice by index ( \(O(1)\) );--- * numerical operations.------ This module re-exports most of the content of "Data.Series.Generic", with type signatures --- specialized to the unboxed vector type `Data.Vector.Unboxed.Vector`.- -module Data.Series.Unboxed (- Series, index, values,-- -- * Building/converting 'Series'- singleton, fromIndex,- -- ** Lists- fromList, toList,- -- ** Vectors- fromVector, toVector,- -- ** Handling duplicates- Occurrence, fromListDuplicates, fromVectorDuplicates,- -- ** Strict Maps- fromStrictMap, toStrictMap,- -- ** Lazy Maps- fromLazyMap, toLazyMap,- -- ** Ad-hoc conversion with other data structures- IsSeries(..),- -- ** Conversion between 'Series' types- G.convert,-- -- * Mapping and filtering- map, mapWithKey, mapIndex, concatMap,- take, takeWhile, drop, dropWhile, filter, filterWithKey,- -- ** Mapping with effects- mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_,-- -- * Combining series- zipWithMatched, zipWithKey,- zipWithMatched3, zipWithKey3,- ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3,- zipWithMonoid, esum, eproduct, unzip, unzip3,-- -- * Index manipulation- require, dropIndex,-- -- * Accessors- -- ** Bulk access- select, selectWhere, Range, to, from, upto, Selection, - -- ** Single-element access- at, iat,-- -- * Replacement- replace, (|->), (<-|),-- -- * Grouping and windowing operations- groupBy, Grouping, aggregateWith, foldWith, - windowing, expanding,-- -- * Folds- -- ** General folds- fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMap', foldMapWithKey,- -- ** Specialized folds- G.mean, G.variance, G.std,- null, length, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn,- argmin, argmax,-- -- * Scans- postscanl, prescanl,-- -- * Displaying 'Series'- display, displayWith,- noLongerThan,- DisplayOptions(..), G.defaultDisplayOptions-) where--import Control.Foldl ( Fold, FoldM )-import qualified Data.Map.Lazy as ML-import qualified Data.Map.Strict as MS-import Data.Series.Index ( Index )-import Data.Series.Generic.View - ( Range, Selection, to, from, upto )-import Data.Series.Generic ( IsSeries(..), ZipStrategy, Occurrence, DisplayOptions(..), skipStrategy, mapStrategy, constStrategy- , noLongerThan - )-import qualified Data.Series.Generic as G-import Data.Vector.Unboxed ( Vector, Unbox )-import qualified Data.Vector.Unboxed as Vector--import Prelude hiding ( map, concatMap, zipWith, filter, foldMap, null, length, all, any, and, or- , sum, product, maximum, minimum, take, takeWhile, drop, dropWhile- , last, unzip, unzip3- )---- $setup--- >>> import qualified Data.Series.Unboxed as Series--- >>> import qualified Data.Series.Index as Index--infixl 1 `select` -infix 6 |->, <-|---- | A series is a labeled array of values of type @a@,--- indexed by keys of type @k@.------ Like @Data.Map@ and @Data.HashMap@, they support efficient:------ * random access by key ( \(O(\log n)\) );--- * slice by key ( \(O(\log n)\) ).------ Like @Data.Vector.Vector@, they support efficient:------ * random access by index ( \(O(1)\) );--- * slice by index ( \(O(1)\) );--- * numerical operations.-type Series = G.Series Vector---index :: Series k a -> Index k-{-# INLINE index #-}-index = G.index---values :: Series k a -> Vector a-{-# INLINE values #-}-values = G.values----- | Create a 'Series' with a single element.-singleton :: Unbox a => k -> a -> Series k a-{-# INLINE singleton #-}-singleton = G.singleton----- | \(O(n)\) Generate a 'Series' by mapping every element of its index.------ >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d']--- index | values--- ----- | --------- 'a' | 0--- 'b' | 0--- 'c' | 0--- 'd' | 0-fromIndex :: Unbox a- => (k -> a) -> Index k -> Series k a-{-# INLINE fromIndex #-}-fromIndex = G.fromIndex----- | Construct a series from a list of key-value pairs. There is no--- condition on the order of pairs.------ >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ]--- >>> xs--- index | values--- ----- | --------- 'a' | 5--- 'b' | 0--- 'd' | 1------ If you need to handle duplicate keys, take a look at `fromListDuplicates`.-fromList :: (Ord k, Unbox a) => [(k, a)] -> Series k a-{-# INLINE fromList #-}-fromList = G.fromList----- | Construct a series from a list of key-value pairs.--- Contrary to `fromList`, values at duplicate keys are preserved. To keep each--- key unique, an `Occurrence` number counts up.------ >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]--- >>> xs--- index | values--- ----- | --------- ('a',0) | 5--- ('b',0) | 0--- ('d',0) | 1--- ('d',1) | -4--- ('d',2) | 7-fromListDuplicates :: (Ord k, Unbox a) => [(k, a)] -> Series (k, Occurrence) a-{-# INLINE fromListDuplicates #-}-fromListDuplicates = G.fromListDuplicates----- | Construct a list from key-value pairs. The elements are in order sorted by key:------ >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ]--- >>> xs--- index | values--- ----- | --------- 'a' | 5--- 'b' | 0--- 'd' | 1--- >>> toList xs--- [('a',5),('b',0),('d',1)]-toList :: Unbox a => Series k a -> [(k, a)]-{-# INLINE toList #-}-toList = G.toList----- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. -toVector :: (Unbox a, Unbox k) => Series k a -> Vector (k, a)-{-# INLINE toVector #-}-toVector = G.toVector----- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no--- condition on the order of pairs. Duplicate keys are silently dropped. If you--- need to handle duplicate keys, see 'fromVectorDuplicates'.------ Note that due to differences in sorting,--- @Series.fromList@ and @Series.fromVector . Vector.fromList@ --- may not be equivalent if the input list contains duplicate keys.-fromVector :: (Ord k, Unbox k, Unbox a)- => Vector (k, a) -> Series k a-{-# INLINE fromVector #-}-fromVector = G.fromVector----- | Construct a series from a 'Vector' of key-value pairs.--- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each--- key unique, an 'Occurrence' number counts up.------ >>> import qualified Data.Vector.Unboxed as Unboxed--- >>> let xs = fromVectorDuplicates $ Unboxed.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ]--- >>> xs--- index | values--- ----- | --------- ('a',0) | 5--- ('b',0) | 0--- ('d',0) | 1--- ('d',1) | -4--- ('d',2) | 7-fromVectorDuplicates :: (Unbox k, Unbox a, Ord k) => Vector (k, a) -> Series (k, Occurrence) a-{-# INLINE fromVectorDuplicates #-}-fromVectorDuplicates = G.fromVectorDuplicates----- | Convert a series into a lazy @Map@.-toLazyMap :: (Unbox a) => Series k a -> ML.Map k a-{-# INLINE toLazyMap #-}-toLazyMap = G.toLazyMap----- | Construct a series from a lazy @Map@.-fromLazyMap :: (Unbox a) => ML.Map k a -> Series k a-{-# INLINE fromLazyMap #-}-fromLazyMap = G.fromLazyMap----- | Convert a series into a strict @Map@.-toStrictMap :: (Unbox a) => Series k a -> MS.Map k a-{-# INLINE toStrictMap #-}-toStrictMap = G.toStrictMap---- | Construct a series from a strict @Map@.-fromStrictMap :: (Unbox a) => MS.Map k a -> Series k a-{-# INLINE fromStrictMap #-}-fromStrictMap = G.fromStrictMap----- | \(O(n)\) Map every element of a 'Series'.-map :: (Unbox a, Unbox b) => (a -> b) -> Series k a -> Series k b-{-# INLINE map #-}-map = G.map----- | \(O(n)\) Map every element of a 'Series', possibly using the key as well.-mapWithKey :: (Unbox a, Unbox b) => (k -> a -> b) -> Series k a -> Series k b-{-# INLINE mapWithKey #-}-mapWithKey = G.mapWithKey----- | \(O(n \log n)\).--- Map each key in the index to another value. Note that the resulting series--- may have less elements, because each key must be unique.------ In case new keys are conflicting, the first element is kept.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> import qualified Data.List--- >>> xs `mapIndex` (Data.List.take 1)--- index | values--- ----- | --------- "L" | 4--- "P" | 1-mapIndex :: (Unbox a, Ord k, Ord g) => Series k a -> (k -> g) -> Series g a-{-# INLINE mapIndex #-}-mapIndex = G.mapIndex----- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'.-concatMap :: (Unbox a, Unbox k, Unbox b, Ord k) - => (a -> Series k b) - -> Series k a - -> Series k b-{-# INLINE concatMap #-}-concatMap = G.concatMap----- | \(O(n)\) Apply the monadic action to every element of a series and its--- index, yielding a series of results.-mapWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b)-{-# INLINE mapWithKeyM #-}-mapWithKeyM = G.mapWithKeyM----- | \(O(n)\) Apply the monadic action to every element of a series and its--- index, discarding the results.-mapWithKeyM_ :: (Unbox a, Monad m) => (k -> a -> m b) -> Series k a -> m ()-{-# INLINE mapWithKeyM_ #-}-mapWithKeyM_ = G.mapWithKeyM_----- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, --- yielding a series of results.-forWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b)-{-# INLINE forWithKeyM #-}-forWithKeyM = G.forWithKeyM----- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, --- discarding the results.-forWithKeyM_ :: (Unbox a, Monad m) => Series k a -> (k -> a -> m b) -> m ()-{-# INLINE forWithKeyM_ #-}-forWithKeyM_ = G.forWithKeyM_----- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5--- >>> take 2 xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2-take :: Unbox a => Int -> Series k a -> Series k a-{-# INLINE take #-}-take = G.take----- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5---- >>> takeWhile (>1) xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2-takeWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a-takeWhile = G.takeWhile----- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5--- >>> drop 2 xs--- index | values--- ----- | --------- "Paris" | 1--- "Vienna" | 5-drop :: Unbox a => Int -> Series k a -> Series k a-{-# INLINE drop #-}-drop = G.drop----- | \(O(n)\) Returns the complement of `takeWhile`.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- "Vienna" | 5---- >>> dropWhile (>1) xs--- index | values--- ----- | --------- "Paris" | 1--- "Vienna" | 5-dropWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a-dropWhile = G.dropWhile----- | Apply a function elementwise to two series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.------ >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ]--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]--- >>> zipWithMatched (+) xs ys--- index | values--- ----- | --------- 'a' | 10--- 'b' | 12-zipWithMatched :: (Unbox a, Unbox b, Unbox c, Ord k) - => (a -> b -> c) -> Series k a -> Series k b -> Series k c-{-# INLINE zipWithMatched #-}-zipWithMatched = G.zipWithMatched----- | Apply a function elementwise to three series, matching elements--- based on their keys. Keys not present in all three series are dropped.------ >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ]--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]--- >>> let zs = Series.fromList [ ('a', 20::Int), ('d', 13), ('e', 6) ]--- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs--- index | values--- ----- | --------- 'a' | 30-zipWithMatched3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Ord k) - => (a -> b -> c -> d) - -> Series k a - -> Series k b - -> Series k c- -> Series k d-{-# INLINE zipWithMatched3 #-}-zipWithMatched3 = G.zipWithMatched3----- | Apply a function elementwise to two series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.--- ------ >>> import Data.Char ( ord )--- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('c', 2) ]--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]--- >>> zipWithKey (\k x y -> ord k + x + y) xs ys--- index | values--- ----- | --------- 'a' | 107--- 'b' | 110-zipWithKey :: (Unbox a, Unbox b, Unbox c, Unbox k, Ord k) - => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c-{-# INLINE zipWithKey #-}-zipWithKey = G.zipWithKey----- | Apply a function elementwise to three series, matching elements--- based on their keys. Keys present only in the left or right series are dropped.--- --- >>> import Data.Char ( ord )--- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ]--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]--- >>> let zs = Series.fromList [ ('a', 20::Int), ('b', 7), ('d', 5) ]--- >>> zipWithKey3 (\k x y z -> ord k + x + y + z) xs ys zs--- index | values--- ----- | --------- 'a' | 127--- 'b' | 117-zipWithKey3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Unbox k, Ord k) - => (k -> a -> b -> c -> d) - -> Series k a - -> Series k b - -> Series k c- -> Series k d-{-# INLINE zipWithKey3 #-}-zipWithKey3 = G.zipWithKey3----- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both.------ In the example below, we want to set the value to @-100@ (via @'constStrategy' (-100)@) for keys which are only present --- in the left 'Series', and drop keys (via 'skipStrategy') which are only present in the `right 'Series' ------ >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ]--- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ]--- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy xs ys--- index | values--- ----- | --------- 'a' | 10--- 'b' | 12--- 'g' | -100------ Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ --- than using @'zipWithStrategy' f 'skipStrategy' 'skipStrategy'@.-zipWithStrategy :: (Ord k, Unbox a, Unbox b, Unbox c) - => (a -> b -> c) -- ^ Function to combine values when present in both series- -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right- -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left- -> Series k a- -> Series k b - -> Series k c-{-# INLINE zipWithStrategy #-}-zipWithStrategy = G.zipWithStrategy----- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is --- present in one of the 'Series' but not all of the others.------ Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ --- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@.-zipWithStrategy3 :: (Ord k, Unbox a, Unbox b, Unbox c, Unbox d) - => (a -> b -> c -> d) -- ^ Function to combine values when present in all series- -> ZipStrategy k a d -- ^ Strategy for when the key is in the left series but not in all the others- -> ZipStrategy k b d -- ^ Strategy for when the key is in the center series but not in all the others- -> ZipStrategy k c d -- ^ Strategy for when the key is in the right series but not in all the others- -> Series k a- -> Series k b - -> Series k c- -> Series k d-zipWithStrategy3 = G.zipWithStrategy3-{-# INLINE zipWithStrategy3 #-}----- | Zip two 'Series' with a combining function. The value for keys which are missing from--- either 'Series' is replaced with the appropriate `mempty` value.------ >>> import Data.Monoid ( Sum(..) )--- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ]--- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ]--- >>> zipWithMonoid (<>) xs ys--- index | values--- ----- | --------- "2023-01-01" | Sum {getSum = 6}--- "2023-01-02" | Sum {getSum = 2}--- "2023-01-03" | Sum {getSum = 7}-zipWithMonoid :: ( Monoid a, Monoid b- , Unbox a, Unbox b, Unbox c- , Ord k- ) - => (a -> b -> c)- -> Series k a- -> Series k b - -> Series k c-zipWithMonoid = G.zipWithMonoid-{-# INLINE zipWithMonoid #-}----- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. ------ >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ]--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]--- >>> xs `esum` ys--- index | values--- ----- | --------- "2023-01-01" | 6--- "2023-01-02" | 2--- "2023-01-03" | 7-esum :: (Ord k, Num a, Unbox a) - => Series k a - -> Series k a- -> Series k a-esum = G.esum-{-# INLINE esum #-}----- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. ------ >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ]--- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ]--- >>> xs `eproduct` ys--- index | values--- ----- | --------- "2023-01-01" | 10--- "2023-01-02" | 3--- "2023-01-03" | 7-eproduct :: (Ord k, Num a, Unbox a) - => Series k a - -> Series k a- -> Series k a-eproduct = G.eproduct-{-# INLINE eproduct #-}----- | \(O(n)\) Unzip a 'Series' of 2-tuples.-unzip :: (Unbox a, Unbox b) - => Series k (a, b)- -> ( Series k a- , Series k b- )-unzip = G.unzip-{-# INLINE unzip #-}----- | \(O(n)\) Unzip a 'Series' of 3-tuples.-unzip3 :: (Unbox a, Unbox b, Unbox c) - => Series k (a, b, c)- -> ( Series k a- , Series k b- , Series k c- )-unzip3 = G.unzip3-{-# INLINE unzip3 #-}----- | Require a series to have a specific `Index`. --- Contrary to @select@, all keys in the `Index` will be present in the resulting series.------ Note that unlike the implementation for boxed series (`Data.Series.require`), missing keys need to be mapped to some values because unboxed--- series cannot contain values of type @`Maybe` a@. ------ In the example below, the missing value for key @\"Taipei\"@ is mapped to 0:------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> require (const 0) xs (Index.fromList ["Paris", "Lisbon", "Taipei"])--- index | values--- ----- | --------- "Lisbon" | 4--- "Paris" | 1--- "Taipei" | 0-require :: (Unbox a, Ord k) - => (k -> a) -> Series k a -> Index k -> Series k a-{-# INLINE require #-}-require f = G.requireWith f id----- | Drop the index of a series by replacing it with an `Int`-based index. Values will--- be indexed from 0.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> dropIndex xs--- index | values--- ----- | --------- 0 | 4--- 1 | 2--- 2 | 1-dropIndex :: Series k a -> Series Int a-{-# INLINE dropIndex #-}-dropIndex = G.dropIndex----- | Filter elements. Only elements for which the predicate is @True@ are kept. --- Notice that the filtering is done on the values, not on the keys.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> filter (>2) xs--- index | values--- ----- | --------- "Lisbon" | 4------ See also 'filterWithKey'.-filter :: (Unbox a, Ord k) => (a -> Bool) -> Series k a -> Series k a-{-# INLINE filter #-}-filter = G.filter----- | Filter elements, taking into account the corresponding key. Only elements for which --- the predicate is @True@ are kept. -filterWithKey :: (Unbox a, Ord k) - => (k -> a -> Bool) - -> Series k a - -> Series k a-{-# INLINE filterWithKey #-}-filterWithKey = G.filterWithKey----- | Select a subseries. There are a few ways to do this.------ The first way to do this is to select a sub-series based on random keys. For example,--- selecting a subseries from an `Index`:------ >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)]--- >>> xs `select` Index.fromList ['a', 'd']--- index | values--- ----- | --------- 'a' | 10--- 'd' | 40------ The second way to select a sub-series is to select all keys in a range:------ >>> xs `select` 'b' `to` 'c'--- index | values--- ----- | --------- 'b' | 20--- 'c' | 30------ Note that with `select`, you'll always get a sub-series; if you ask for a key which is not--- in the series, it'll be ignored:------ >>> xs `select` Index.fromList ['a', 'd', 'e']--- index | values--- ----- | --------- 'a' | 10--- 'd' | 40------ See `require` if you want to ensure that all keys are present.-select :: (Unbox a, Selection s, Ord k) => Series k a -> s k -> Series k a-select = G.select----- | Select a sub-series from a series matching a condition.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> xs `selectWhere` (Series.map (>1) xs)--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2-selectWhere :: (Unbox a, Ord k) => Series k a -> Series k Bool -> Series k a-{-# INLINE selectWhere #-}-selectWhere = G.selectWhere----- | \(O(\log n)\). Extract a single value from a series, by key.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs `at` "Paris"--- Just 1--- >>> xs `at` "Sydney"--- Nothing-at :: (Unbox a, Ord k) => Series k a -> k -> Maybe a-{-# INLINE at #-}-at = G.at----- | \(O(1)\). Extract a single value from a series, by index.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> xs `iat` 0--- Just 4--- >>> xs `iat` 3--- Nothing-iat :: Unbox a => Series k a -> Int -> Maybe a-{-# INLINE iat #-}-iat = G.iat----- | Replace values in the right series from values in the left series at matching keys.--- Keys not in the right series are unaffected.--- --- See `(|->)` and `(<-|)`, which might be more readable.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> let ys = Series.singleton "Paris" (99::Int)--- >>> ys `replace` xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 99-replace :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a-{-# INLINE replace #-}-replace = G.replace----- | Replace values in the right series from values in the left series at matching keys.--- Keys not in the right series are unaffected.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> let ys = Series.singleton "Paris" (99::Int)--- >>> ys |-> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 99-(|->) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a-{-# INLINE (|->) #-}-(|->) = (G.|->)----- | Replace values in the left series from values in the right series at matching keys.--- Keys not in the left series are unaffected.------ >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)]--- >>> xs--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 1--- >>> let ys = Series.singleton "Paris" (99::Int)--- >>> xs <-| ys--- index | values--- ----- | --------- "Lisbon" | 4--- "London" | 2--- "Paris" | 99-(<-|) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a-{-# INLINE (<-|) #-}-(<-|) = (G.<-|)----- | \(O(n)\) Execute a 'Fold' over a 'Series'.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double--- >>> xs--- index | values--- ----- | --------- 0 | 1.0--- 1 | 2.0--- 2 | 3.0--- 3 | 4.0--- >>> import Control.Foldl (variance)--- >>> fold variance xs--- 1.25------ See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into--- account while folding.-fold :: Unbox a - => Fold a b -> Series k a -> b-fold = G.fold-{-# INLINE fold #-}----- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'.------ See also 'fold' for pure folds, and 'foldMWithKey' to take keys into--- account while folding.-foldM :: (Monad m, Unbox a) - => FoldM m a b - -> Series k a - -> m b-foldM = G.foldM-{-# INLINE foldM #-}----- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account.-foldWithKey :: (Unbox k, Unbox a) - => Fold (k, a) b -> Series k a -> b-foldWithKey = G.foldWithKey-{-# INLINE foldWithKey #-}----- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account.-foldMWithKey :: (Monad m, Unbox a, Unbox k) - => FoldM m (k, a) b - -> Series k a - -> m b-foldMWithKey = G.foldMWithKey-{-# INLINE foldMWithKey #-}----- | \(O(n)\) Map each element of the structure to a monoid and combine--- the results.-foldMap :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m-{-# INLINE foldMap #-}-foldMap = G.foldMap----- | \(O(n)\) Like 'foldMap', but strict in the accumulator. It uses the same--- implementation as the corresponding method of the 'Foldable' type class.-foldMap' :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m-{-# INLINE foldMap' #-}-foldMap' f = Vector.foldMap' f . values----- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine--- the results.-foldMapWithKey :: (Monoid m, Unbox a, Unbox k) => (k -> a -> m) -> Series k a -> m-{-# INLINE foldMapWithKey #-}-foldMapWithKey = G.foldMapWithKey----- | 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 k a -- ^ Grouping function- -> (k -> g) -- ^ Input series- -> Grouping k g a -- ^ Grouped series-{-# INLINE groupBy #-}-groupBy = G.groupBy----- | Representation of a 'Series' being grouped.-type Grouping k g a = G.Grouping k g Vector a----- | 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, Unbox a, Unbox b) - => Grouping k g a - -> (Series k a -> b) - -> Series g b-{-# INLINE aggregateWith #-}-aggregateWith = G.aggregateWith----- | Aggregate 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.-foldWith :: (Ord g, Unbox a) - => Grouping k g a- -> (a -> a -> a)- -> Series g a-{-# INLINE foldWith #-}-foldWith = G.foldWith----- | Expanding window aggregation.------ >>> :{ --- let (xs :: Series Int Int) --- = fromList [ (1, 0)--- , (2, 1)--- , (3, 2)--- , (4, 3)--- , (5, 4)--- , (6, 5)--- ]--- in (xs `expanding` sum) :: Series Int Int --- :}--- index | values--- ----- | --------- 1 | 0--- 2 | 1--- 3 | 3--- 4 | 6--- 5 | 10--- 6 | 15-expanding :: (Unbox a, Unbox b) - => Series k a -- ^ Series vector- -> (Series k a -> b) -- ^ Aggregation function- -> Series k b -- ^ Resulting vector-{-# INLINE expanding #-}-expanding = G.expanding----- | General-purpose window aggregation.------ >>> :{ --- 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, Unbox a, Unbox b)- => (k -> Range k)- -> (Series k a -> b)- -> Series k a- -> Series k b-{-# INLINE windowing #-}-windowing = G.windowing ----- | \(O(1)\) Test whether a 'Series' is empty.-null :: Unbox a => Series k a -> Bool-{-# INLINE null #-}-null = G.null----- |\(O(1)\) Extract the length of a 'Series'.-length :: Unbox a => Series k a -> Int-{-# INLINE length #-}-length = G.length----- | \(O(n)\) Check if all elements satisfy the predicate.-all :: Unbox a => (a -> Bool) -> Series k a -> Bool-{-# INLINE all #-}-all = G.all----- | \(O(n)\) Check if any element satisfies the predicate.-any :: Unbox a => (a -> Bool) -> Series k a -> Bool-{-# INLINE any #-}-any = G.any----- | \(O(n)\) Check if all elements are 'True'.-and :: Series k Bool -> Bool-{-# INLINE and #-}-and = G.and----- | \(O(n)\) Check if any element is 'True'.-or :: Series k Bool -> Bool-{-# INLINE or #-}-or = G.or----- | \(O(n)\) Compute the sum of the elements.-sum :: (Unbox a, Num a) => Series k a -> a-{-# INLINE sum #-}-sum = G.sum----- | \(O(n)\) Compute the product of the elements.-product :: (Unbox a, Num a) => Series k a -> a-{-# INLINE product #-}-product = G.product----- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins.--- If the 'Series' is empty, @Nothing@ is returned.------ See also 'argmax'.-maximum :: (Ord a, Unbox a) => Series k a -> Maybe a-{-# INLINE maximum #-}-maximum = G.maximum----- | \(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, Unbox a) => (a -> b) -> Series k a -> Maybe a-{-# INLINE maximumOn #-}-maximumOn = G.maximumOn----- | \(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.------ See also 'argmin'.-minimum :: (Ord a, Unbox a) => Series k a -> Maybe a-{-# INLINE minimum #-}-minimum = G.minimum----- | \(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, Unbox a) => (a -> b) -> Series k a -> Maybe a-{-# INLINE minimumOn #-}-minimumOn = G.minimumOn----- | \(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.Unboxed 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, Unbox a)- => Series k a- -> Maybe k-argmax = G.argmax-{-# INLINE argmax #-}----- | \(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.Unboxed 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, Unbox a)- => Series k a- -> Maybe k-argmin = G.argmin-{-# INLINE argmin #-}----- | \(O(n)\) Left-to-right postscan.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int--- >>> xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- 3 | 4--- >>> postscanl (+) 0 xs--- index | values--- ----- | --------- 0 | 1--- 1 | 3--- 2 | 6--- 3 | 10-postscanl :: (Unbox a, Unbox b) - => (a -> b -> a) -> a -> Series k b -> Series k a-{-# INLINE postscanl #-}-postscanl = G.postscanl----- | \(O(n)\) Left-to-right prescan.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int--- >>> xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- 3 | 4--- >>> prescanl (+) 0 xs--- index | values--- ----- | --------- 0 | 0--- 1 | 1--- 2 | 3--- 3 | 6-prescanl :: (Unbox a, Unbox b) - => (a -> b -> a) -> a -> Series k b -> Series k a-{-# INLINE prescanl #-}-prescanl = G.prescanl----- | Display a 'Series' using default 'DisplayOptions'.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int--- >>> putStrLn $ display xs--- index | values--- ----- | --------- 0 | 1--- 1 | 2--- 2 | 3--- ... | ...--- 4 | 5--- 5 | 6--- 6 | 7-display :: (Unbox a, Show k, Show a) - => Series k a - -> String-display = G.display----- | Display a 'Series' using customizable 'DisplayOptions'.------ >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int--- >>> import Data.List (replicate)--- >>> :{--- let opts = DisplayOptions { maximumNumberOfRows = 4--- , indexHeader = "keys"--- , valuesHeader = "vals"--- , keyDisplayFunction = (\i -> replicate i 'x') `noLongerThan` 5--- , valueDisplayFunction = (\i -> replicate i 'o') --- }--- in putStrLn $ displayWith opts xs--- :}--- keys | vals--- ----- | --------- | o--- x | oo--- ... | ...--- xxxxx | oooooo--- xxx... | ooooooo-displayWith :: (Unbox a) - => DisplayOptions k a- -> Series k a - -> String-displayWith = G.displayWith+----------------------------------------------------------------------------- +-- | +-- Module : Data.Series.Unboxed +-- Copyright : (c) Laurent P. René de Cotret +-- License : MIT +-- Maintainer : laurent.decotret@outlook.com +-- Portability : portable +-- +-- This module contains data structures and functions to work with 'Series' capable of holding unboxed values, +-- i.e. values of types which are instances of `Unbox`. +-- +-- = Why use unboxed series? +-- +-- Unboxed series can have much better performance, at the cost of less flexibility. For example, +-- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of +-- `Functor` or `Foldable`. +-- +-- If you are hesitating, you should prefer the series implementation in the "Data.Series" module. +-- +-- = Introduction to series +-- +-- A 'Series' of type @Series k a@ is a labeled array of values of type @a@, +-- indexed by keys of type @k@. +-- +-- Like `Data.Map.Strict.Map` from the @containers@ package, 'Series' support efficient: +-- +-- * random access by key ( \(O(\log n)\) ); +-- * slice by key ( \(O(\log n)\) ). +-- +-- Like `Data.Vector.Vector`, they support efficient: +-- +-- * random access by index ( \(O(1)\) ); +-- * slice by index ( \(O(1)\) ); +-- * numerical operations. +-- +-- This module re-exports most of the content of "Data.Series.Generic", with type signatures +-- specialized to the unboxed vector type `Data.Vector.Unboxed.Vector`. + +module Data.Series.Unboxed ( + Series, index, values, + + -- * Building/converting 'Series' + singleton, fromIndex, + -- ** Lists + fromList, toList, + -- ** Vectors + fromVector, toVector, + -- ** Handling duplicates + Occurrence, fromListDuplicates, fromVectorDuplicates, + -- ** Strict Maps + fromStrictMap, toStrictMap, + -- ** Lazy Maps + fromLazyMap, toLazyMap, + -- ** Ad-hoc conversion with other data structures + IsSeries(..), + -- ** Conversion between 'Series' types + G.convert, + + -- * Mapping and filtering + map, mapWithKey, mapIndex, concatMap, + take, takeWhile, drop, dropWhile, filter, filterWithKey, + -- ** Mapping with effects + mapWithKeyM, mapWithKeyM_, forWithKeyM, forWithKeyM_, + + -- * Combining series + zipWithMatched, zipWithKey, + zipWithMatched3, zipWithKey3, + ZipStrategy, skipStrategy, mapStrategy, constStrategy, zipWithStrategy, zipWithStrategy3, + zipWithMonoid, esum, eproduct, unzip, unzip3, + + -- * Index manipulation + require, dropIndex, + + -- * Accessors + -- ** Bulk access + select, selectWhere, Range, to, from, upto, Selection, + -- ** Single-element access + at, iat, + + -- * Replacement + replace, (|->), (<-|), + + -- * Grouping and windowing operations + groupBy, Grouping, aggregateWith, foldWith, + windowing, expanding, + + -- * Folds + -- ** General folds + fold, foldM, foldWithKey, foldMWithKey, foldMap, foldMap', foldMapWithKey, + -- ** Specialized folds + G.mean, G.variance, G.std, + null, length, all, any, and, or, sum, product, maximum, maximumOn, minimum, minimumOn, + argmin, argmax, + + -- * Scans + postscanl, prescanl, + + -- * Displaying 'Series' + display, displayWith, + noLongerThan, + DisplayOptions(..), G.defaultDisplayOptions +) where + +import Control.Foldl ( Fold, FoldM ) +import qualified Data.Map.Lazy as ML +import qualified Data.Map.Strict as MS +import Data.Series.Index ( Index ) +import Data.Series.Generic.View + ( Range, Selection, to, from, upto ) +import Data.Series.Generic ( IsSeries(..), ZipStrategy, Occurrence, DisplayOptions(..), skipStrategy, mapStrategy, constStrategy + , noLongerThan + ) +import qualified Data.Series.Generic as G +import Data.Vector.Unboxed ( Vector, Unbox ) +import qualified Data.Vector.Unboxed as Vector + +import Prelude hiding ( map, concatMap, zipWith, filter, foldMap, null, length, all, any, and, or + , sum, product, maximum, minimum, take, takeWhile, drop, dropWhile + , last, unzip, unzip3 + ) + +-- $setup +-- >>> import qualified Data.Series.Unboxed as Series +-- >>> import qualified Data.Series.Index as Index + +infixl 1 `select` +infix 6 |->, <-| + +-- | A series is a labeled array of values of type @a@, +-- indexed by keys of type @k@. +-- +-- Like @Data.Map@ and @Data.HashMap@, they support efficient: +-- +-- * random access by key ( \(O(\log n)\) ); +-- * slice by key ( \(O(\log n)\) ). +-- +-- Like @Data.Vector.Vector@, they support efficient: +-- +-- * random access by index ( \(O(1)\) ); +-- * slice by index ( \(O(1)\) ); +-- * numerical operations. +type Series = G.Series Vector + + +index :: Series k a -> Index k +{-# INLINABLE index #-} +index = G.index + + +values :: Series k a -> Vector a +{-# INLINABLE values #-} +values = G.values + + +-- | Create a 'Series' with a single element. +singleton :: Unbox a => k -> a -> Series k a +{-# INLINABLE singleton #-} +singleton = G.singleton + + +-- | \(O(n)\) Generate a 'Series' by mapping every element of its index. +-- +-- >>> fromIndex (const (0::Int)) $ Index.fromList ['a','b','c','d'] +-- index | values +-- ----- | ------ +-- 'a' | 0 +-- 'b' | 0 +-- 'c' | 0 +-- 'd' | 0 +fromIndex :: Unbox a + => (k -> a) -> Index k -> Series k a +{-# INLINABLE fromIndex #-} +fromIndex = G.fromIndex + + +-- | Construct a series from a list of key-value pairs. There is no +-- condition on the order of pairs. +-- +-- >>> let xs = fromList [('b', 0::Int), ('a', 5), ('d', 1) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- 'a' | 5 +-- 'b' | 0 +-- 'd' | 1 +-- +-- If you need to handle duplicate keys, take a look at `fromListDuplicates`. +fromList :: (Ord k, Unbox a) => [(k, a)] -> Series k a +{-# INLINABLE fromList #-} +fromList = G.fromList + + +-- | Construct a series from a list of key-value pairs. +-- Contrary to `fromList`, values at duplicate keys are preserved. To keep each +-- key unique, an `Occurrence` number counts up. +-- +-- >>> let xs = fromListDuplicates [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- ('a',0) | 5 +-- ('b',0) | 0 +-- ('d',0) | 1 +-- ('d',1) | -4 +-- ('d',2) | 7 +fromListDuplicates :: (Ord k, Unbox a) => [(k, a)] -> Series (k, Occurrence) a +{-# INLINABLE fromListDuplicates #-} +fromListDuplicates = G.fromListDuplicates + + +-- | Construct a list from key-value pairs. The elements are in order sorted by key: +-- +-- >>> let xs = Series.fromList [ ('b', 0::Int), ('a', 5), ('d', 1) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- 'a' | 5 +-- 'b' | 0 +-- 'd' | 1 +-- >>> toList xs +-- [('a',5),('b',0),('d',1)] +toList :: Unbox a => Series k a -> [(k, a)] +{-# INLINABLE toList #-} +toList = G.toList + + +-- | Construct a 'Vector' of key-value pairs. The elements are in order sorted by key. +toVector :: (Unbox a, Unbox k) => Series k a -> Vector (k, a) +{-# INLINABLE toVector #-} +toVector = G.toVector + + +-- | Construct a 'Series' from a 'Vector' of key-value pairs. There is no +-- condition on the order of pairs. Duplicate keys are silently dropped. If you +-- need to handle duplicate keys, see 'fromVectorDuplicates'. +-- +-- Note that due to differences in sorting, +-- @Series.fromList@ and @Series.fromVector . Vector.fromList@ +-- may not be equivalent if the input list contains duplicate keys. +fromVector :: (Ord k, Unbox k, Unbox a) + => Vector (k, a) -> Series k a +{-# INLINABLE fromVector #-} +fromVector = G.fromVector + + +-- | Construct a series from a 'Vector' of key-value pairs. +-- Contrary to 'fromVector', values at duplicate keys are preserved. To keep each +-- key unique, an 'Occurrence' number counts up. +-- +-- >>> import qualified Data.Vector.Unboxed as Unboxed +-- >>> let xs = fromVectorDuplicates $ Unboxed.fromList [('b', 0::Int), ('a', 5), ('d', 1), ('d', -4), ('d', 7) ] +-- >>> xs +-- index | values +-- ----- | ------ +-- ('a',0) | 5 +-- ('b',0) | 0 +-- ('d',0) | 1 +-- ('d',1) | -4 +-- ('d',2) | 7 +fromVectorDuplicates :: (Unbox k, Unbox a, Ord k) => Vector (k, a) -> Series (k, Occurrence) a +{-# INLINABLE fromVectorDuplicates #-} +fromVectorDuplicates = G.fromVectorDuplicates + + +-- | Convert a series into a lazy @Map@. +toLazyMap :: (Unbox a) => Series k a -> ML.Map k a +{-# INLINABLE toLazyMap #-} +toLazyMap = G.toLazyMap + + +-- | Construct a series from a lazy @Map@. +fromLazyMap :: (Unbox a) => ML.Map k a -> Series k a +{-# INLINABLE fromLazyMap #-} +fromLazyMap = G.fromLazyMap + + +-- | Convert a series into a strict @Map@. +toStrictMap :: (Unbox a) => Series k a -> MS.Map k a +{-# INLINABLE toStrictMap #-} +toStrictMap = G.toStrictMap + +-- | Construct a series from a strict @Map@. +fromStrictMap :: (Unbox a) => MS.Map k a -> Series k a +{-# INLINABLE fromStrictMap #-} +fromStrictMap = G.fromStrictMap + + +-- | \(O(n)\) Map every element of a 'Series'. +map :: (Unbox a, Unbox b) => (a -> b) -> Series k a -> Series k b +{-# INLINABLE map #-} +map = G.map + + +-- | \(O(n)\) Map every element of a 'Series', possibly using the key as well. +mapWithKey :: (Unbox a, Unbox b) => (k -> a -> b) -> Series k a -> Series k b +{-# INLINABLE mapWithKey #-} +mapWithKey = G.mapWithKey + + +-- | \(O(n \log n)\). +-- Map each key in the index to another value. Note that the resulting series +-- may have less elements, because each key must be unique. +-- +-- In case new keys are conflicting, the first element is kept. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> import qualified Data.List +-- >>> xs `mapIndex` (Data.List.take 1) +-- index | values +-- ----- | ------ +-- "L" | 4 +-- "P" | 1 +mapIndex :: (Unbox a, Ord k, Ord g) => Series k a -> (k -> g) -> Series g a +{-# INLINABLE mapIndex #-} +mapIndex = G.mapIndex + + +-- | Map a function over all the elements of a 'Series' and concatenate the result into a single 'Series'. +concatMap :: (Unbox a, Unbox k, Unbox b, Ord k) + => (a -> Series k b) + -> Series k a + -> Series k b +{-# INLINABLE concatMap #-} +concatMap = G.concatMap + + +-- | \(O(n)\) Apply the monadic action to every element of a series and its +-- index, yielding a series of results. +mapWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => (k -> a -> m b) -> Series k a -> m (Series k b) +{-# INLINABLE mapWithKeyM #-} +mapWithKeyM = G.mapWithKeyM + + +-- | \(O(n)\) Apply the monadic action to every element of a series and its +-- index, discarding the results. +mapWithKeyM_ :: (Unbox a, Monad m) => (k -> a -> m b) -> Series k a -> m () +{-# INLINABLE mapWithKeyM_ #-} +mapWithKeyM_ = G.mapWithKeyM_ + + +-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, +-- yielding a series of results. +forWithKeyM :: (Unbox a, Unbox b, Monad m, Ord k) => Series k a -> (k -> a -> m b) -> m (Series k b) +{-# INLINABLE forWithKeyM #-} +forWithKeyM = G.forWithKeyM + + +-- | \(O(n)\) Apply the monadic action to all elements of the series and their associated keys, +-- discarding the results. +forWithKeyM_ :: (Unbox a, Monad m) => Series k a -> (k -> a -> m b) -> m () +{-# INLINABLE forWithKeyM_ #-} +forWithKeyM_ = G.forWithKeyM_ + + +-- | \(O(\log n)\) @'take' n xs@ returns at most @n@ elements of the 'Series' @xs@. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 +-- >>> take 2 xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +take :: Unbox a => Int -> Series k a -> Series k a +{-# INLINABLE take #-} +take = G.take + + +-- | \(O(n)\) Returns the longest prefix (possibly empty) of the input 'Series' that satisfy a predicate. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 + +-- >>> takeWhile (>1) xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +takeWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a +takeWhile = G.takeWhile + + +-- | \(O(\log n)\) @'drop' n xs@ drops at most @n@ elements from the 'Series' @xs@. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 +-- >>> drop 2 xs +-- index | values +-- ----- | ------ +-- "Paris" | 1 +-- "Vienna" | 5 +drop :: Unbox a => Int -> Series k a -> Series k a +{-# INLINABLE drop #-} +drop = G.drop + + +-- | \(O(n)\) Returns the complement of `takeWhile`. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4), ("Vienna", 5)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- "Vienna" | 5 + +-- >>> dropWhile (>1) xs +-- index | values +-- ----- | ------ +-- "Paris" | 1 +-- "Vienna" | 5 +dropWhile :: Unbox a => (a -> Bool) -> Series k a -> Series k a +dropWhile = G.dropWhile + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ] +-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ] +-- >>> zipWithMatched (+) xs ys +-- index | values +-- ----- | ------ +-- 'a' | 10 +-- 'b' | 12 +zipWithMatched :: (Unbox a, Unbox b, Unbox c, Ord k) + => (a -> b -> c) -> Series k a -> Series k b -> Series k c +{-# INLINABLE zipWithMatched #-} +zipWithMatched = G.zipWithMatched + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. Keys not present in all three series are dropped. +-- +-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ] +-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ] +-- >>> let zs = Series.fromList [ ('a', 20::Int), ('d', 13), ('e', 6) ] +-- >>> zipWithMatched3 (\x y z -> x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- 'a' | 30 +zipWithMatched3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Ord k) + => (a -> b -> c -> d) + -> Series k a + -> Series k b + -> Series k c + -> Series k d +{-# INLINABLE zipWithMatched3 #-} +zipWithMatched3 = G.zipWithMatched3 + + +-- | Apply a function elementwise to two series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- +-- >>> import Data.Char ( ord ) +-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('c', 2) ] +-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ] +-- >>> zipWithKey (\k x y -> ord k + x + y) xs ys +-- index | values +-- ----- | ------ +-- 'a' | 107 +-- 'b' | 110 +zipWithKey :: (Unbox a, Unbox b, Unbox c, Unbox k, Ord k) + => (k -> a -> b -> c) -> Series k a -> Series k b -> Series k c +{-# INLINABLE zipWithKey #-} +zipWithKey = G.zipWithKey + + +-- | Apply a function elementwise to three series, matching elements +-- based on their keys. Keys present only in the left or right series are dropped. +-- +-- >>> import Data.Char ( ord ) +-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ] +-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ] +-- >>> let zs = Series.fromList [ ('a', 20::Int), ('b', 7), ('d', 5) ] +-- >>> zipWithKey3 (\k x y z -> ord k + x + y + z) xs ys zs +-- index | values +-- ----- | ------ +-- 'a' | 127 +-- 'b' | 117 +zipWithKey3 :: (Unbox a, Unbox b, Unbox c, Unbox d, Unbox k, Ord k) + => (k -> a -> b -> c -> d) + -> Series k a + -> Series k b + -> Series k c + -> Series k d +{-# INLINABLE zipWithKey3 #-} +zipWithKey3 = G.zipWithKey3 + + +-- | Zip two 'Series' with a combining function, applying a 'ZipStrategy' when one key is present in one of the 'Series' but not both. +-- +-- In the example below, we want to set the value to @-100@ (via @'constStrategy' (-100)@) for keys which are only present +-- in the left 'Series', and drop keys (via 'skipStrategy') which are only present in the `right 'Series' +-- +-- >>> let xs = Series.fromList [ ('a', 0::Int), ('b', 1), ('g', 2) ] +-- >>> let ys = Series.fromList [ ('a', 10::Int), ('b', 11), ('d', 13) ] +-- >>> zipWithStrategy (+) (constStrategy (-100)) skipStrategy xs ys +-- index | values +-- ----- | ------ +-- 'a' | 10 +-- 'b' | 12 +-- 'g' | -100 +-- +-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched' f@ +-- than using @'zipWithStrategy' f 'skipStrategy' 'skipStrategy'@. +zipWithStrategy :: (Ord k, Unbox a, Unbox b, Unbox c) + => (a -> b -> c) -- ^ Function to combine values when present in both series + -> ZipStrategy k a c -- ^ Strategy for when the key is in the left series but not the right + -> ZipStrategy k b c -- ^ Strategy for when the key is in the right series but not the left + -> Series k a + -> Series k b + -> Series k c +{-# INLINABLE zipWithStrategy #-} +zipWithStrategy = G.zipWithStrategy + + +-- | Zip three 'Series' with a combining function, applying a 'ZipStrategy' when one key is +-- present in one of the 'Series' but not all of the others. +-- +-- Note that if you want to drop keys missing in either 'Series', it is faster to use @'zipWithMatched3' f@ +-- than using @'zipWithStrategy3' f skipStrategy skipStrategy skipStrategy@. +zipWithStrategy3 :: (Ord k, Unbox a, Unbox b, Unbox c, Unbox d) + => (a -> b -> c -> d) -- ^ Function to combine values when present in all series + -> ZipStrategy k a d -- ^ Strategy for when the key is in the left series but not in all the others + -> ZipStrategy k b d -- ^ Strategy for when the key is in the center series but not in all the others + -> ZipStrategy k c d -- ^ Strategy for when the key is in the right series but not in all the others + -> Series k a + -> Series k b + -> Series k c + -> Series k d +zipWithStrategy3 = G.zipWithStrategy3 +{-# INLINABLE zipWithStrategy3 #-} + + +-- | Zip two 'Series' with a combining function. The value for keys which are missing from +-- either 'Series' is replaced with the appropriate `mempty` value. +-- +-- >>> import Data.Monoid ( Sum(..) ) +-- >>> let xs = Series.fromList [ ("2023-01-01", Sum (1::Int)), ("2023-01-02", Sum 2) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", Sum (5::Int)), ("2023-01-03", Sum 7) ] +-- >>> zipWithMonoid (<>) xs ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | Sum {getSum = 6} +-- "2023-01-02" | Sum {getSum = 2} +-- "2023-01-03" | Sum {getSum = 7} +zipWithMonoid :: ( Monoid a, Monoid b + , Unbox a, Unbox b, Unbox c + , Ord k + ) + => (a -> b -> c) + -> Series k a + -> Series k b + -> Series k c +zipWithMonoid = G.zipWithMonoid +{-# INLINABLE zipWithMonoid #-} + + +-- | Elementwise sum of two 'Series'. Elements missing in one or the other 'Series' is considered 0. +-- +-- >>> let xs = Series.fromList [ ("2023-01-01", (1::Int)), ("2023-01-02", 2) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ] +-- >>> xs `esum` ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | 6 +-- "2023-01-02" | 2 +-- "2023-01-03" | 7 +esum :: (Ord k, Num a, Unbox a) + => Series k a + -> Series k a + -> Series k a +esum = G.esum +{-# INLINABLE esum #-} + + +-- | Elementwise product of two 'Series'. Elements missing in one or the other 'Series' is considered 1. +-- +-- >>> let xs = Series.fromList [ ("2023-01-01", (2::Int)), ("2023-01-02", 3) ] +-- >>> let ys = Series.fromList [ ("2023-01-01", (5::Int)), ("2023-01-03", 7) ] +-- >>> xs `eproduct` ys +-- index | values +-- ----- | ------ +-- "2023-01-01" | 10 +-- "2023-01-02" | 3 +-- "2023-01-03" | 7 +eproduct :: (Ord k, Num a, Unbox a) + => Series k a + -> Series k a + -> Series k a +eproduct = G.eproduct +{-# INLINABLE eproduct #-} + + +-- | \(O(n)\) Unzip a 'Series' of 2-tuples. +unzip :: (Unbox a, Unbox b) + => Series k (a, b) + -> ( Series k a + , Series k b + ) +unzip = G.unzip +{-# INLINABLE unzip #-} + + +-- | \(O(n)\) Unzip a 'Series' of 3-tuples. +unzip3 :: (Unbox a, Unbox b, Unbox c) + => Series k (a, b, c) + -> ( Series k a + , Series k b + , Series k c + ) +unzip3 = G.unzip3 +{-# INLINABLE unzip3 #-} + + +-- | Require a series to have a specific `Index`. +-- Contrary to @select@, all keys in the `Index` will be present in the resulting series. +-- +-- Note that unlike the implementation for boxed series (`Data.Series.require`), missing keys need to be mapped to some values because unboxed +-- series cannot contain values of type @`Maybe` a@. +-- +-- In the example below, the missing value for key @\"Taipei\"@ is mapped to 0: +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> require (const 0) xs (Index.fromList ["Paris", "Lisbon", "Taipei"]) +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "Paris" | 1 +-- "Taipei" | 0 +require :: (Unbox a, Ord k) + => (k -> a) -> Series k a -> Index k -> Series k a +{-# INLINABLE require #-} +require f = G.requireWith f id + + +-- | \(O(n)\) Drop the index of a series by replacing it with an `Int`-based index. Values will +-- be indexed from 0. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> dropIndex xs +-- index | values +-- ----- | ------ +-- 0 | 4 +-- 1 | 2 +-- 2 | 1 +dropIndex :: Series k a -> Series Int a +{-# INLINABLE dropIndex #-} +dropIndex = G.dropIndex + + +-- | Filter elements. Only elements for which the predicate is @True@ are kept. +-- Notice that the filtering is done on the values, not on the keys. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> filter (>2) xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- +-- See also 'filterWithKey'. +filter :: (Unbox a, Ord k) => (a -> Bool) -> Series k a -> Series k a +{-# INLINABLE filter #-} +filter = G.filter + + +-- | Filter elements, taking into account the corresponding key. Only elements for which +-- the predicate is @True@ are kept. +filterWithKey :: (Unbox a, Ord k) + => (k -> a -> Bool) + -> Series k a + -> Series k a +{-# INLINABLE filterWithKey #-} +filterWithKey = G.filterWithKey + + +-- | Select a subseries. There are a few ways to do this. +-- +-- The first way to do this is to select a sub-series based on random keys. For example, +-- selecting a subseries from an `Index`: +-- +-- >>> let xs = Series.fromList [('a', 10::Int), ('b', 20), ('c', 30), ('d', 40)] +-- >>> xs `select` Index.fromList ['a', 'd'] +-- index | values +-- ----- | ------ +-- 'a' | 10 +-- 'd' | 40 +-- +-- The second way to select a sub-series is to select all keys in a range: +-- +-- >>> xs `select` 'b' `to` 'c' +-- index | values +-- ----- | ------ +-- 'b' | 20 +-- 'c' | 30 +-- +-- Note that with `select`, you'll always get a sub-series; if you ask for a key which is not +-- in the series, it'll be ignored: +-- +-- >>> xs `select` Index.fromList ['a', 'd', 'e'] +-- index | values +-- ----- | ------ +-- 'a' | 10 +-- 'd' | 40 +-- +-- See `require` if you want to ensure that all keys are present. +select :: (Unbox a, Selection s, Ord k) => Series k a -> s k -> Series k a +select = G.select + + +-- | Select a sub-series from a series matching a condition. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> xs `selectWhere` (Series.map (>1) xs) +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +selectWhere :: (Unbox a, Ord k) => Series k a -> Series k Bool -> Series k a +{-# INLINABLE selectWhere #-} +selectWhere = G.selectWhere + + +-- | \(O(\log n)\). Extract a single value from a series, by key. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs `at` "Paris" +-- Just 1 +-- >>> xs `at` "Sydney" +-- Nothing +at :: (Unbox a, Ord k) => Series k a -> k -> Maybe a +{-# INLINABLE at #-} +at = G.at + + +-- | \(O(1)\). Extract a single value from a series, by index. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> xs `iat` 0 +-- Just 4 +-- >>> xs `iat` 3 +-- Nothing +iat :: Unbox a => Series k a -> Int -> Maybe a +{-# INLINABLE iat #-} +iat = G.iat + + +-- | Replace values in the right series from values in the left series at matching keys. +-- Keys not in the right series are unaffected. +-- +-- See `(|->)` and `(<-|)`, which might be more readable. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> let ys = Series.singleton "Paris" (99::Int) +-- >>> ys `replace` xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 99 +replace :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a +{-# INLINABLE replace #-} +replace = G.replace + + +-- | Replace values in the right series from values in the left series at matching keys. +-- Keys not in the right series are unaffected. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> let ys = Series.singleton "Paris" (99::Int) +-- >>> ys |-> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 99 +(|->) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a +{-# INLINABLE (|->) #-} +(|->) = (G.|->) + + +-- | Replace values in the left series from values in the right series at matching keys. +-- Keys not in the left series are unaffected. +-- +-- >>> let xs = Series.fromList [("Paris", 1 :: Int), ("London", 2), ("Lisbon", 4)] +-- >>> xs +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 1 +-- >>> let ys = Series.singleton "Paris" (99::Int) +-- >>> xs <-| ys +-- index | values +-- ----- | ------ +-- "Lisbon" | 4 +-- "London" | 2 +-- "Paris" | 99 +(<-|) :: (Unbox a, Ord k) => Series k a -> Series k a -> Series k a +{-# INLINABLE (<-|) #-} +(<-|) = (G.<-|) + + +-- | \(O(n)\) Execute a 'Fold' over a 'Series'. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Double +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1.0 +-- 1 | 2.0 +-- 2 | 3.0 +-- 3 | 4.0 +-- >>> import Control.Foldl (variance) +-- >>> fold variance xs +-- 1.25 +-- +-- See also 'foldM' for monadic folds, and 'foldWithKey' to take keys into +-- account while folding. +fold :: Unbox a + => Fold a b -> Series k a -> b +fold = G.fold +{-# INLINABLE fold #-} + + +-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series'. +-- +-- See also 'fold' for pure folds, and 'foldMWithKey' to take keys into +-- account while folding. +foldM :: (Monad m, Unbox a) + => FoldM m a b + -> Series k a + -> m b +foldM = G.foldM +{-# INLINABLE foldM #-} + + +-- | \(O(n)\) Execute a 'Fold' over a 'Series', taking keys into account. +foldWithKey :: (Unbox k, Unbox a) + => Fold (k, a) b -> Series k a -> b +foldWithKey = G.foldWithKey +{-# INLINABLE foldWithKey #-} + + +-- | \(O(n)\) Execute a monadic 'FoldM' over a 'Series', where the 'FoldM' takes keys into account. +foldMWithKey :: (Monad m, Unbox a, Unbox k) + => FoldM m (k, a) b + -> Series k a + -> m b +foldMWithKey = G.foldMWithKey +{-# INLINABLE foldMWithKey #-} + + +-- | \(O(n)\) Map each element of the structure to a monoid and combine +-- the results. +foldMap :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m +{-# INLINABLE foldMap #-} +foldMap = G.foldMap + + +-- | \(O(n)\) Like 'foldMap', but strict in the accumulator. It uses the same +-- implementation as the corresponding method of the 'Foldable' type class. +foldMap' :: (Monoid m, Unbox a) => (a -> m) -> Series k a -> m +{-# INLINABLE foldMap' #-} +foldMap' f = Vector.foldMap' f . values + + +-- | \(O(n)\) Map each element and associated key of the structure to a monoid and combine +-- the results. +foldMapWithKey :: (Monoid m, Unbox a, Unbox k) => (k -> a -> m) -> Series k a -> m +{-# INLINABLE foldMapWithKey #-} +foldMapWithKey = G.foldMapWithKey + + +-- | 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 k a -- ^ Grouping function + -> (k -> g) -- ^ Input series + -> Grouping k g a -- ^ Grouped series +{-# INLINABLE groupBy #-} +groupBy = G.groupBy + + +-- | Representation of a 'Series' being grouped. +type Grouping k g a = G.Grouping k g Vector a + + +-- | 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, Unbox a, Unbox b) + => Grouping k g a + -> (Series k a -> b) + -> Series g b +{-# INLINABLE aggregateWith #-} +aggregateWith = G.aggregateWith + + +-- | Aggregate 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. +foldWith :: (Ord g, Unbox a) + => Grouping k g a + -> (a -> a -> a) + -> Series g a +{-# INLINABLE foldWith #-} +foldWith = G.foldWith + + +-- | Expanding window aggregation. +-- +-- >>> :{ +-- let (xs :: Series Int Int) +-- = fromList [ (1, 0) +-- , (2, 1) +-- , (3, 2) +-- , (4, 3) +-- , (5, 4) +-- , (6, 5) +-- ] +-- in (xs `expanding` sum) :: Series Int Int +-- :} +-- index | values +-- ----- | ------ +-- 1 | 0 +-- 2 | 1 +-- 3 | 3 +-- 4 | 6 +-- 5 | 10 +-- 6 | 15 +expanding :: (Unbox a, Unbox b) + => Series k a -- ^ Series vector + -> (Series k a -> b) -- ^ Aggregation function + -> Series k b -- ^ Resulting vector +{-# INLINABLE expanding #-} +expanding = G.expanding + + +-- | General-purpose window aggregation. +-- +-- >>> :{ +-- 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, Unbox a, Unbox b) + => (k -> Range k) + -> (Series k a -> b) + -> Series k a + -> Series k b +{-# INLINABLE windowing #-} +windowing = G.windowing + + +-- | \(O(1)\) Test whether a 'Series' is empty. +null :: Unbox a => Series k a -> Bool +{-# INLINABLE null #-} +null = G.null + + +-- |\(O(1)\) Extract the length of a 'Series'. +length :: Unbox a => Series k a -> Int +{-# INLINABLE length #-} +length = G.length + + +-- | \(O(n)\) Check if all elements satisfy the predicate. +all :: Unbox a => (a -> Bool) -> Series k a -> Bool +{-# INLINABLE all #-} +all = G.all + + +-- | \(O(n)\) Check if any element satisfies the predicate. +any :: Unbox a => (a -> Bool) -> Series k a -> Bool +{-# INLINABLE any #-} +any = G.any + + +-- | \(O(n)\) Check if all elements are 'True'. +and :: Series k Bool -> Bool +{-# INLINABLE and #-} +and = G.and + + +-- | \(O(n)\) Check if any element is 'True'. +or :: Series k Bool -> Bool +{-# INLINABLE or #-} +or = G.or + + +-- | \(O(n)\) Compute the sum of the elements. +sum :: (Unbox a, Num a) => Series k a -> a +{-# INLINABLE sum #-} +sum = G.sum + + +-- | \(O(n)\) Compute the product of the elements. +product :: (Unbox a, Num a) => Series k a -> a +{-# INLINABLE product #-} +product = G.product + + +-- | \(O(n)\) Yield the maximum element of the series. In case of a tie, the first occurrence wins. +-- If the 'Series' is empty, @Nothing@ is returned. +-- +-- See also 'argmax'. +maximum :: (Ord a, Unbox a) => Series k a -> Maybe a +{-# INLINABLE maximum #-} +maximum = G.maximum + + +-- | \(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, Unbox a) => (a -> b) -> Series k a -> Maybe a +{-# INLINABLE maximumOn #-} +maximumOn = G.maximumOn + + +-- | \(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. +-- +-- See also 'argmin'. +minimum :: (Ord a, Unbox a) => Series k a -> Maybe a +{-# INLINABLE minimum #-} +minimum = G.minimum + + +-- | \(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, Unbox a) => (a -> b) -> Series k a -> Maybe a +{-# INLINABLE minimumOn #-} +minimumOn = G.minimumOn + + +-- | \(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.Unboxed 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, Unbox a) + => Series k a + -> Maybe k +argmax = G.argmax +{-# INLINABLE argmax #-} + + +-- | \(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.Unboxed 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, Unbox a) + => Series k a + -> Maybe k +argmin = G.argmin +{-# INLINABLE argmin #-} + + +-- | \(O(n)\) Left-to-right postscan. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- 3 | 4 +-- >>> postscanl (+) 0 xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 3 +-- 2 | 6 +-- 3 | 10 +postscanl :: (Unbox a, Unbox b) + => (a -> b -> a) -> a -> Series k b -> Series k a +{-# INLINABLE postscanl #-} +postscanl = G.postscanl + + +-- | \(O(n)\) Left-to-right prescan. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4]) :: Series Int Int +-- >>> xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- 3 | 4 +-- >>> prescanl (+) 0 xs +-- index | values +-- ----- | ------ +-- 0 | 0 +-- 1 | 1 +-- 2 | 3 +-- 3 | 6 +prescanl :: (Unbox a, Unbox b) + => (a -> b -> a) -> a -> Series k b -> Series k a +{-# INLINABLE prescanl #-} +prescanl = G.prescanl + + +-- | Display a 'Series' using default 'DisplayOptions'. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int +-- >>> putStrLn $ display xs +-- index | values +-- ----- | ------ +-- 0 | 1 +-- 1 | 2 +-- 2 | 3 +-- ... | ... +-- 4 | 5 +-- 5 | 6 +-- 6 | 7 +display :: (Unbox a, Show k, Show a) + => Series k a + -> String +display = G.display + + +-- | Display a 'Series' using customizable 'DisplayOptions'. +-- +-- >>> let xs = Series.fromList (zip [0..] [1,2,3,4,5,6,7]) :: Series Int Int +-- >>> import Data.List (replicate) +-- >>> :{ +-- let opts = DisplayOptions { maximumNumberOfRows = 4 +-- , indexHeader = "keys" +-- , valuesHeader = "vals" +-- , keyDisplayFunction = (\i -> replicate i 'x') `noLongerThan` 5 +-- , valueDisplayFunction = (\i -> replicate i 'o') +-- } +-- in putStrLn $ displayWith opts xs +-- :} +-- keys | vals +-- ----- | ------ +-- | o +-- x | oo +-- ... | ... +-- xxxxx | oooooo +-- xxx... | ooooooo +displayWith :: (Unbox a) + => DisplayOptions k a + -> Series k a + -> String +displayWith = G.displayWith
test/Main.hs view
@@ -1,21 +1,19 @@-module Main (main) where--import qualified Test.Data.Series-import qualified Test.Data.Series.Generic.Aggregation-import qualified Test.Data.Series.Generic.Definition-import qualified Test.Data.Series.Index-import qualified Test.Data.Series.Generic.Numeric-import qualified Test.Data.Series.Generic.View-import qualified Test.Data.Series.Generic.Zip--import Test.Tasty ( defaultMain, testGroup )--main :: IO ()-main = defaultMain $ testGroup "Test suite" [ Test.Data.Series.tests- , Test.Data.Series.Index.tests- , Test.Data.Series.Generic.Aggregation.tests- , Test.Data.Series.Generic.Definition.tests- , Test.Data.Series.Generic.Numeric.tests- , Test.Data.Series.Generic.View.tests- , Test.Data.Series.Generic.Zip.tests- ]+module Main (main) where + +import qualified Test.Data.Series +import qualified Test.Data.Series.Generic.Aggregation +import qualified Test.Data.Series.Generic.Definition +import qualified Test.Data.Series.Index +import qualified Test.Data.Series.Generic.View +import qualified Test.Data.Series.Generic.Zip + +import Test.Tasty ( defaultMain, testGroup ) + +main :: IO () +main = defaultMain $ testGroup "Test suite" [ Test.Data.Series.tests + , Test.Data.Series.Index.tests + , Test.Data.Series.Generic.Aggregation.tests + , Test.Data.Series.Generic.Definition.tests + , Test.Data.Series.Generic.View.tests + , Test.Data.Series.Generic.Zip.tests + ]
test/Test/Data/Series.hs view
@@ -1,7 +1,7 @@--module Test.Data.Series (tests) where--import Test.Tasty ( testGroup, TestTree ) --tests :: TestTree+ +module Test.Data.Series (tests) where + +import Test.Tasty ( testGroup, TestTree ) + +tests :: TestTree tests = testGroup "Data.Series" []
test/Test/Data/Series/Generic/Aggregation.hs view
@@ -1,134 +1,134 @@--module Test.Data.Series.Generic.Aggregation (tests) where--import qualified Data.Map.Strict as MS-import qualified Data.Series.Generic as Series-import Data.Series.Generic ( Series, fromStrictMap, groupBy, aggregateWith, foldWith, windowing, to, expanding)-import Data.Vector ( Vector )--import Hedgehog ( property, forAll, (===) )-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--import Prelude hiding ( zipWith )--import Test.Tasty ( testGroup, TestTree )-import Test.Tasty.Hedgehog ( testProperty )-import Test.Tasty.HUnit ( testCase, assertEqual )--tests :: TestTree-tests = testGroup "Data.Series.Generic.Aggregation" [ testGroupBy- , testWindowing- , testWindowingRollingForwards- , testWindowingRollingBackwards- , testPropAggregateVsfoldWith- , testExpanding- ]---testGroupBy :: TestTree-testGroupBy = testGroup "Data.Series.Generic.groupBy" [ testGroupBy1, testGroupBy2 ]- where- testGroupBy1 = testCase "groupBy" $ do- let (series :: Series Vector String Int) = fromStrictMap $ MS.fromList [("aa", 1), ("ab", 2), ("c", 3), ("dc", 4), ("ae", 5)]- expectation = fromStrictMap $ MS.fromList [(1, 3), (2, 1+2+4+5)]- - assertEqual mempty expectation $ series `groupBy` length `aggregateWith` (Series.sum :: Series Vector String Int -> Int)-- testGroupBy2 = testCase "groupBy" $ do- let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList $ zip [0,1,2,3] [0,1,2,3]- expectation = fromStrictMap $ MS.fromList [(True, 0+2), (False, 1+3)]- - assertEqual mempty expectation $ series `groupBy` even `aggregateWith` (Series.sum :: Series Vector Int Int -> Int)----testWindowing :: TestTree-testWindowing = testCase "Data.Series.Generic.windowing" $ do-- let (xs :: Series Vector Int Int) - = Series.fromList [ (1, 0)- , (2, 1)- , (3, 2)- , (4, 3)- , (5, 4)- , (6, 5)- ]- expectation = Series.fromList [ (1, 3)- , (2, 6)- , (3, 9)- , (4, 12)- , (5, 9)- , (6, 5)- ]- assertEqual mempty expectation $ windowing (\k -> k `to` (k+2)) sum xs---testWindowingRollingForwards :: TestTree-testWindowingRollingForwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ]- where- test1 = testCase "rollingForwards" $ do- let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]- expectation = fromStrictMap $ MS.fromList [ (1, 1+2)- , (2, 2+3)- , (3, 3+4)- , (4, 4+5)- , (5, 5)- ]- - assertEqual mempty expectation $ windowing (\k -> k `to` (k + 1)) (Series.sum :: Series Vector Int Int -> Int) series-- test2 = testCase "rollingForwards" $ do- let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]- expectation = fromStrictMap $ MS.fromList [ (1, 1+2+3)- , (2, 2+3+4)- , (3, 3+4+5)- , (4, 4+5)- , (5, 5)- ]- - assertEqual mempty expectation $ windowing (\k -> k `to` (k + 2)) (Series.sum :: Series Vector Int Int -> Int) series---testWindowingRollingBackwards :: TestTree-testWindowingRollingBackwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ]- where- test1 = testCase "rollingForwards" $ do- let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]- expectation = fromStrictMap $ MS.fromList [ (1, 1)- , (2, 1+2)- , (3, 2+3)- , (4, 3+4)- , (5, 4+5)- ]- - assertEqual mempty expectation $ windowing (\k -> (k-1) `to` k) (Series.sum :: Series Vector Int Int -> Int) series-- test2 = testCase "rollingForwards" $ do- let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]- expectation = fromStrictMap $ MS.fromList [ (1, 1)- , (2, 1+2)- , (3, 1+2+3)- , (4, 2+3+4)- , (5, 3+4+5)- ]- - assertEqual mempty expectation $ windowing (\k -> (k-2) `to` k) (Series.sum :: Series Vector Int Int -> Int) series---testPropAggregateVsfoldWith :: TestTree-testPropAggregateVsfoldWith - = testProperty "check that groupBy and testWindowingRollingForwards are equivalent" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.int $ Range.linear (-500) 500) - let (xs :: Series Vector Int Int) = Series.fromList (zip [0::Int ..] ms)-- xs `groupBy` (`mod` 5) `aggregateWith` (Series.sum :: Series Vector Int Int -> Int) === xs `groupBy` (`mod` 5) `foldWith` (+)---testExpanding :: TestTree-testExpanding = testCase "expanding" $ do- let (xs :: Series Vector Char Int) = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1::Int,2,3,4]- rs = xs `expanding` Series.sum- expectation = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1,1+2,1+2+3,1+2+3+4]- + +module Test.Data.Series.Generic.Aggregation (tests) where + +import qualified Data.Map.Strict as MS +import qualified Data.Series.Generic as Series +import Data.Series.Generic ( Series, fromStrictMap, groupBy, aggregateWith, foldWith, windowing, to, expanding) +import Data.Vector ( Vector ) + +import Hedgehog ( property, forAll, (===) ) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +import Prelude hiding ( zipWith ) + +import Test.Tasty ( testGroup, TestTree ) +import Test.Tasty.Hedgehog ( testProperty ) +import Test.Tasty.HUnit ( testCase, assertEqual ) + +tests :: TestTree +tests = testGroup "Data.Series.Generic.Aggregation" [ testGroupBy + , testWindowing + , testWindowingRollingForwards + , testWindowingRollingBackwards + , testPropAggregateVsfoldWith + , testExpanding + ] + + +testGroupBy :: TestTree +testGroupBy = testGroup "Data.Series.Generic.groupBy" [ testGroupBy1, testGroupBy2 ] + where + testGroupBy1 = testCase "groupBy" $ do + let (series :: Series Vector String Int) = fromStrictMap $ MS.fromList [("aa", 1), ("ab", 2), ("c", 3), ("dc", 4), ("ae", 5)] + expectation = fromStrictMap $ MS.fromList [(1, 3), (2, 1+2+4+5)] + + assertEqual mempty expectation $ series `groupBy` length `aggregateWith` (Series.sum :: Series Vector String Int -> Int) + + testGroupBy2 = testCase "groupBy" $ do + let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList $ zip [0,1,2,3] [0,1,2,3] + expectation = fromStrictMap $ MS.fromList [(True, 0+2), (False, 1+3)] + + assertEqual mempty expectation $ series `groupBy` even `aggregateWith` (Series.sum :: Series Vector Int Int -> Int) + + + +testWindowing :: TestTree +testWindowing = testCase "Data.Series.Generic.windowing" $ do + + let (xs :: Series Vector Int Int) + = Series.fromList [ (1, 0) + , (2, 1) + , (3, 2) + , (4, 3) + , (5, 4) + , (6, 5) + ] + expectation = Series.fromList [ (1, 3) + , (2, 6) + , (3, 9) + , (4, 12) + , (5, 9) + , (6, 5) + ] + assertEqual mempty expectation $ windowing (\k -> k `to` (k+2)) sum xs + + +testWindowingRollingForwards :: TestTree +testWindowingRollingForwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ] + where + test1 = testCase "rollingForwards" $ do + let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] + expectation = fromStrictMap $ MS.fromList [ (1, 1+2) + , (2, 2+3) + , (3, 3+4) + , (4, 4+5) + , (5, 5) + ] + + assertEqual mempty expectation $ windowing (\k -> k `to` (k + 1)) (Series.sum :: Series Vector Int Int -> Int) series + + test2 = testCase "rollingForwards" $ do + let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] + expectation = fromStrictMap $ MS.fromList [ (1, 1+2+3) + , (2, 2+3+4) + , (3, 3+4+5) + , (4, 4+5) + , (5, 5) + ] + + assertEqual mempty expectation $ windowing (\k -> k `to` (k + 2)) (Series.sum :: Series Vector Int Int -> Int) series + + +testWindowingRollingBackwards :: TestTree +testWindowingRollingBackwards = testGroup "Data.Series.Generic.windowing" [ test1, test2 ] + where + test1 = testCase "rollingForwards" $ do + let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] + expectation = fromStrictMap $ MS.fromList [ (1, 1) + , (2, 1+2) + , (3, 2+3) + , (4, 3+4) + , (5, 4+5) + ] + + assertEqual mempty expectation $ windowing (\k -> (k-1) `to` k) (Series.sum :: Series Vector Int Int -> Int) series + + test2 = testCase "rollingForwards" $ do + let (series :: Series Vector Int Int) = fromStrictMap $ MS.fromList [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] + expectation = fromStrictMap $ MS.fromList [ (1, 1) + , (2, 1+2) + , (3, 1+2+3) + , (4, 2+3+4) + , (5, 3+4+5) + ] + + assertEqual mempty expectation $ windowing (\k -> (k-2) `to` k) (Series.sum :: Series Vector Int Int -> Int) series + + +testPropAggregateVsfoldWith :: TestTree +testPropAggregateVsfoldWith + = testProperty "check that groupBy and testWindowingRollingForwards are equivalent" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.int $ Range.linear (-500) 500) + let (xs :: Series Vector Int Int) = Series.fromList (zip [0::Int ..] ms) + + xs `groupBy` (`mod` 5) `aggregateWith` (Series.sum :: Series Vector Int Int -> Int) === xs `groupBy` (`mod` 5) `foldWith` (+) + + +testExpanding :: TestTree +testExpanding = testCase "expanding" $ do + let (xs :: Series Vector Char Int) = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1::Int,2,3,4] + rs = xs `expanding` Series.sum + expectation = Series.fromList $ zip ['a', 'b', 'c', 'd'] [1,1+2,1+2+3,1+2+3+4] + assertEqual mempty expectation rs
test/Test/Data/Series/Generic/Definition.hs view
@@ -1,206 +1,206 @@--module Test.Data.Series.Generic.Definition (tests) where--import qualified Control.Foldl as Fold-import Data.Function ( on )-import Data.Functor.Identity ( Identity(..))-import Data.List ( nubBy, sortOn )-import qualified Data.Map.Strict as MS-import qualified Data.Map.Lazy as ML-import Data.Series.Generic ( Series, Occurrence, fromStrictMap, toStrictMap, fromLazyMap, toLazyMap, fromList, toList, fromVector, toVector )-import qualified Data.Series.Generic as Series-import Data.Vector ( Vector )-import qualified Data.Vector as Vector--import Hedgehog ( property, forAll, (===), tripping )-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--import Test.Tasty ( testGroup, TestTree ) -import Test.Tasty.Hedgehog ( testProperty )-import Test.Tasty.HUnit ( testCase, assertEqual )--tests :: TestTree-tests = testGroup "Data.Series.Generic.Definition" - [ testMappend- , testPropMappendLikeMap- , testPropShow- , testFromStrictMap- , testToStrictMap- , testPropRoundtripConversionWithStrictMap- , testPropRoundtripConversionWithLazyMap- , testPropRoundtripConversionWithList- , testPropFromListDuplicatesNeverDrops- , testPropFromVectorDuplicatesNeverDrops- , testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder- , testPropRoundtripConversionWithVector- , testPropVectorVsList- , testFromLazyMap- , testToLazyMap- , testTakeWhile- , testDropWhile- , testFold- ]---testMappend :: TestTree-testMappend = testCase "(<>)" $ do- let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)]- (s2 :: Series Vector Char Int) = fromList [('b', 10), ('x', 25)]- expectation = fromList [('a', 1), ('b', 5), ('x', 25)]- - assertEqual mempty expectation (s1 <> s2)---testPropMappendLikeMap :: TestTree-testPropMappendLikeMap - = testProperty "Mappend property similar to Data.Map.Strict" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.alpha)- m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 500 1500) <*> Gen.alpha)-- (fromStrictMap :: MS.Map Int Char -> Series Vector Int Char) (m1 <> m2) === fromStrictMap m1 <> fromStrictMap m2---testPropShow :: TestTree-testPropShow- = testProperty "Show is never too long" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.alpha)-- let (xs :: Series Vector Int Char) = fromStrictMap m1- ls = lines $ show xs- if Series.length xs > 6- then length ls === 2 + 6 + 1- else length ls === 2 + Series.length xs---testFromStrictMap :: TestTree-testFromStrictMap = testCase "fromStrictMap" $ do- -- Note the duplicate input at key 'a', which should disappear- let input = MS.fromList [('b', 2), ('a', 1), ('a', 1)]- (series :: Series Vector Char Int) = fromStrictMap input- expectation = fromList [('a', 1), ('b', 2)]- - assertEqual mempty series expectation---testToStrictMap :: TestTree-testToStrictMap = testCase "toStrictMap" $ do- let input = MS.fromList [('b', 2), ('a', 1)]- (series :: Series Vector Char Int) = fromStrictMap input- - assertEqual mempty (toStrictMap series) input---testPropRoundtripConversionWithStrictMap :: TestTree-testPropRoundtripConversionWithStrictMap - = testProperty "Roundtrip property with Data.Map.Strict" $ property $ do- ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- tripping ms (fromStrictMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toStrictMap)---testPropRoundtripConversionWithLazyMap :: TestTree-testPropRoundtripConversionWithLazyMap - = testProperty "Roundtrip property with Data.Map.Lazy" $ property $ do- ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- tripping (ML.fromDistinctAscList $ MS.toAscList ms) (fromLazyMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toLazyMap)---testPropRoundtripConversionWithList :: TestTree-testPropRoundtripConversionWithList - = testProperty "Roundtrip property with List" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)-- -- The property below needs some explanation.- -- In case of conflicting keys, a Series will be biased like a Map. Therefore,- -- the expected List won't have duplicated (hence the use of nubBy), but the elements which- -- are kept are in the order of `reverse xs`.- (toList :: Series Vector Int Char -> [(Int, Char)] ) (fromList xs) === sortOn fst (nubBy (\left right -> fst left == fst right) (reverse xs))---testPropFromListDuplicatesNeverDrops :: TestTree-testPropFromListDuplicatesNeverDrops- = testProperty "fromListDuplicates never drops elements" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)- Series.length (Series.fromListDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs---testPropFromVectorDuplicatesNeverDrops :: TestTree-testPropFromVectorDuplicatesNeverDrops- = testProperty "fromVectorDuplicates never drops elements" $ property $ do- xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)- Series.length (Series.fromVectorDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs---testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder :: TestTree-testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder- = testProperty "fromVectorDuplicates and fromListDuplicates are equivalent" $ property $ do- xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha)- Series.fromVectorDuplicates xs === Series.fromListDuplicates (Vector.toList xs)---testPropRoundtripConversionWithVector :: TestTree-testPropRoundtripConversionWithVector - = testProperty "Roundtrip property with Vector" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)-- let (srs :: Series Vector Int Char) = fromList xs- tripping srs toVector (Just . fromVector)---testPropVectorVsList :: TestTree-testPropVectorVsList - = testProperty "building from a list or vector yields the same results" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha)- -- Note that due to differences in sorting,- -- Series.fromList and Series.fromVector . Vector.fromList - -- are not equivalent if the input list contains duplicate keys.- let unique = nubBy ((==) `on` fst) xs - (fromList unique :: Series Vector Int Char) === fromVector (Vector.fromList unique)---testFromLazyMap :: TestTree-testFromLazyMap = testCase "fromLazyMap" $ do- let input = ML.fromList [('b', 2), ('a', 1)]- (series :: Series Vector Char Int) = fromLazyMap input- expectation = fromList [('a', 1), ('b', 2)]- - assertEqual mempty series expectation---testToLazyMap :: TestTree-testToLazyMap = testCase "toLazyMap" $ do- let input = ML.fromList [('b', 2), ('a', 1)]- (series :: Series Vector Char Int) = fromLazyMap input- - assertEqual mempty (toLazyMap series) input---testTakeWhile :: TestTree-testTakeWhile = testProperty "takeWhile behaves like lists" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50))- let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs-- n <- forAll $ Gen.int (Range.linear 1 10)- Series.takeWhile (\v -> v `mod` n == 0) ys === Series.fromList (takeWhile (\(_, v) -> v `mod` n == 0) $ Series.toList ys)---testDropWhile :: TestTree-testDropWhile = testProperty "dropWhile behaves like lists" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50))- let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs-- n <- forAll $ Gen.int (Range.linear 1 10)- Series.dropWhile (\v -> v `mod` n /= 0) ys === Series.fromList (dropWhile (\(_, v) -> v `mod` n /= 0) $ Series.toList ys)---testFold :: TestTree-testFold = testGroup "fold"- [ testProperty "Series.sum and Control.Foldl.sum should be equivalent" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50))- let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs- Series.fold Fold.sum ys === Series.sum ys- , testProperty "FoldM Identity should be equivalent to a pure fold" $ property $ do- xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50))- let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs- runIdentity (Series.foldM (Fold.generalize Fold.sum) ys) === Series.sum ys+ +module Test.Data.Series.Generic.Definition (tests) where + +import qualified Control.Foldl as Fold +import Data.Function ( on ) +import Data.Functor.Identity ( Identity(..)) +import Data.List ( nubBy, sortOn ) +import qualified Data.Map.Strict as MS +import qualified Data.Map.Lazy as ML +import Data.Series.Generic ( Series, Occurrence, fromStrictMap, toStrictMap, fromLazyMap, toLazyMap, fromList, toList, fromVector, toVector ) +import qualified Data.Series.Generic as Series +import Data.Vector ( Vector ) +import qualified Data.Vector as Vector + +import Hedgehog ( property, forAll, (===), tripping ) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +import Test.Tasty ( testGroup, TestTree ) +import Test.Tasty.Hedgehog ( testProperty ) +import Test.Tasty.HUnit ( testCase, assertEqual ) + +tests :: TestTree +tests = testGroup "Data.Series.Generic.Definition" + [ testMappend + , testPropMappendLikeMap + , testPropShow + , testFromStrictMap + , testToStrictMap + , testPropRoundtripConversionWithStrictMap + , testPropRoundtripConversionWithLazyMap + , testPropRoundtripConversionWithList + , testPropFromListDuplicatesNeverDrops + , testPropFromVectorDuplicatesNeverDrops + , testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder + , testPropRoundtripConversionWithVector + , testPropVectorVsList + , testFromLazyMap + , testToLazyMap + , testTakeWhile + , testDropWhile + , testFold + ] + + +testMappend :: TestTree +testMappend = testCase "(<>)" $ do + let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)] + (s2 :: Series Vector Char Int) = fromList [('b', 10), ('x', 25)] + expectation = fromList [('a', 1), ('b', 5), ('x', 25)] + + assertEqual mempty expectation (s1 <> s2) + + +testPropMappendLikeMap :: TestTree +testPropMappendLikeMap + = testProperty "Mappend property similar to Data.Map.Strict" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.alpha) + m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 500 1500) <*> Gen.alpha) + + (fromStrictMap :: MS.Map Int Char -> Series Vector Int Char) (m1 <> m2) === fromStrictMap m1 <> fromStrictMap m2 + + +testPropShow :: TestTree +testPropShow + = testProperty "Show is never too long" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.alpha) + + let (xs :: Series Vector Int Char) = fromStrictMap m1 + ls = lines $ show xs + if Series.length xs > 6 + then length ls === 2 + 6 + 1 + else length ls === 2 + Series.length xs + + +testFromStrictMap :: TestTree +testFromStrictMap = testCase "fromStrictMap" $ do + -- Note the duplicate input at key 'a', which should disappear + let input = MS.fromList [('b', 2), ('a', 1), ('a', 1)] + (series :: Series Vector Char Int) = fromStrictMap input + expectation = fromList [('a', 1), ('b', 2)] + + assertEqual mempty series expectation + + +testToStrictMap :: TestTree +testToStrictMap = testCase "toStrictMap" $ do + let input = MS.fromList [('b', 2), ('a', 1)] + (series :: Series Vector Char Int) = fromStrictMap input + + assertEqual mempty (toStrictMap series) input + + +testPropRoundtripConversionWithStrictMap :: TestTree +testPropRoundtripConversionWithStrictMap + = testProperty "Roundtrip property with Data.Map.Strict" $ property $ do + ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + tripping ms (fromStrictMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toStrictMap) + + +testPropRoundtripConversionWithLazyMap :: TestTree +testPropRoundtripConversionWithLazyMap + = testProperty "Roundtrip property with Data.Map.Lazy" $ property $ do + ms <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + tripping (ML.fromDistinctAscList $ MS.toAscList ms) (fromLazyMap :: MS.Map Char Char -> Series Vector Char Char) (Just . toLazyMap) + + +testPropRoundtripConversionWithList :: TestTree +testPropRoundtripConversionWithList + = testProperty "Roundtrip property with List" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha) + + -- The property below needs some explanation. + -- In case of conflicting keys, a Series will be biased like a Map. Therefore, + -- the expected List won't have duplicated (hence the use of nubBy), but the elements which + -- are kept are in the order of `reverse xs`. + (toList :: Series Vector Int Char -> [(Int, Char)] ) (fromList xs) === sortOn fst (nubBy (\left right -> fst left == fst right) (reverse xs)) + + +testPropFromListDuplicatesNeverDrops :: TestTree +testPropFromListDuplicatesNeverDrops + = testProperty "fromListDuplicates never drops elements" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha) + Series.length (Series.fromListDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs + + +testPropFromVectorDuplicatesNeverDrops :: TestTree +testPropFromVectorDuplicatesNeverDrops + = testProperty "fromVectorDuplicates never drops elements" $ property $ do + xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha) + Series.length (Series.fromVectorDuplicates xs :: Series Vector (Int, Occurrence) Char) === length xs + + +testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder :: TestTree +testPropFromVectorDuplicatesAndFromListDuplicatesHaveSameOrder + = testProperty "fromVectorDuplicates and fromListDuplicates are equivalent" $ property $ do + xs <- fmap Vector.fromList $ forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-10) 10) <*> Gen.alpha) + Series.fromVectorDuplicates xs === Series.fromListDuplicates (Vector.toList xs) + + +testPropRoundtripConversionWithVector :: TestTree +testPropRoundtripConversionWithVector + = testProperty "Roundtrip property with Vector" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha) + + let (srs :: Series Vector Int Char) = fromList xs + tripping srs toVector (Just . fromVector) + + +testPropVectorVsList :: TestTree +testPropVectorVsList + = testProperty "building from a list or vector yields the same results" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 100) ((,) <$> Gen.int (Range.linear (-50) 50) <*> Gen.alpha) + -- Note that due to differences in sorting, + -- Series.fromList and Series.fromVector . Vector.fromList + -- are not equivalent if the input list contains duplicate keys. + let unique = nubBy ((==) `on` fst) xs + (fromList unique :: Series Vector Int Char) === fromVector (Vector.fromList unique) + + +testFromLazyMap :: TestTree +testFromLazyMap = testCase "fromLazyMap" $ do + let input = ML.fromList [('b', 2), ('a', 1)] + (series :: Series Vector Char Int) = fromLazyMap input + expectation = fromList [('a', 1), ('b', 2)] + + assertEqual mempty series expectation + + +testToLazyMap :: TestTree +testToLazyMap = testCase "toLazyMap" $ do + let input = ML.fromList [('b', 2), ('a', 1)] + (series :: Series Vector Char Int) = fromLazyMap input + + assertEqual mempty (toLazyMap series) input + + +testTakeWhile :: TestTree +testTakeWhile = testProperty "takeWhile behaves like lists" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50)) + let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs + + n <- forAll $ Gen.int (Range.linear 1 10) + Series.takeWhile (\v -> v `mod` n == 0) ys === Series.fromList (takeWhile (\(_, v) -> v `mod` n == 0) $ Series.toList ys) + + +testDropWhile :: TestTree +testDropWhile = testProperty "dropWhile behaves like lists" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 100) (Gen.int (Range.linear (-50) 50)) + let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs + + n <- forAll $ Gen.int (Range.linear 1 10) + Series.dropWhile (\v -> v `mod` n /= 0) ys === Series.fromList (dropWhile (\(_, v) -> v `mod` n /= 0) $ Series.toList ys) + + +testFold :: TestTree +testFold = testGroup "fold" + [ testProperty "Series.sum and Control.Foldl.sum should be equivalent" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50)) + let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs + Series.fold Fold.sum ys === Series.sum ys + , testProperty "FoldM Identity should be equivalent to a pure fold" $ property $ do + xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-50) 50)) + let (ys :: Series Vector Int Int) = Series.fromList $ zip [0..] xs + runIdentity (Series.foldM (Fold.generalize Fold.sum) ys) === Series.sum ys ]
− test/Test/Data/Series/Generic/Numeric.hs
@@ -1,62 +0,0 @@-module Test.Data.Series.Generic.Numeric (tests) where--import Data.Series.Generic ( Series, fromList, mean, variance, std)-import qualified Data.Series.Generic as Series-import Data.Vector ( Vector )-import qualified Data.Vector as Vector --import Hedgehog ( property, forAll, (===), assert )-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--import qualified Statistics.Sample as Stats--import Test.Tasty ( testGroup, TestTree ) -import Test.Tasty.Hedgehog ( testProperty )-import Test.Utils ( approx )---tests :: TestTree-tests = testGroup "Data.Series.Generic.Numeric" [ testPropMean- , testPropVariance- , testPropStdDev- ]---testPropMean :: TestTree-testPropMean - = testProperty "mean" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.double $ Range.linearFrac (-500) 500) - let (xs :: Series Vector Int Double) = fromList (zip [0::Int ..] ms)- Series.length xs === length ms - let m :: Double = Series.fold mean xs- -- Stats.mean of an empty vector is NaN, but is 0 for Control.Foldl.mean- case Series.length xs of- 0 -> m === 0 - _ -> m `approx` Stats.mean (Vector.fromList ms)---testPropVariance :: TestTree-testPropVariance- = testProperty "population variance" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.double $ Range.linearFrac (-500) 500) - let (xs :: Series Vector Int Double) = fromList (zip [0::Int ..] ms)- Series.length xs === length ms - let v :: Double = Series.fold variance xs- -- IEEE 754 specifies that NaN != NaN...- case Series.length xs of- 0 -> assert $ isNaN v- _ -> v `approx` Stats.fastVariance (Vector.fromList ms)---testPropStdDev :: TestTree-testPropStdDev- = testProperty "population standard deviation" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 100) (Gen.double $ Range.linearFrac (-500) 500) - let (xs :: Series Vector Int Double) = fromList (zip [0::Int ..] ms)- Series.length xs === length ms - let d :: Double = Series.fold std xs- -- IEEE 754 specifies that NaN != NaN...- case Series.length xs of- 0 -> assert $ isNaN d- _ -> d `approx` Stats.fastStdDev (Vector.fromList ms)
test/Test/Data/Series/Generic/View.hs view
@@ -1,143 +1,143 @@-module Test.Data.Series.Generic.View (tests) where--import qualified Data.Map.Strict as MS-import Data.Series.Generic ( Series, index, fromStrictMap, fromList, to, from, upto, select- , selectWhere, require, mapIndex, argmax, argmin, )-import qualified Data.Series.Index as Index-import Data.Vector ( Vector )--import Hedgehog ( property, forAll, (===), assert )-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--import Test.Tasty ( testGroup, TestTree )-import Test.Tasty.Hedgehog ( testProperty )-import Test.Tasty.HUnit ( testCase, assertEqual )--tests :: TestTree-tests = testGroup "Data.Series.Generic.View" [ testSelectRange- , testSelectUnboundedRange- , testSelectUnboundedRangeEquivalence- , testSelectRangeEmptyRange- , testPropSelectRangeSubseries- , testSelectSet - , testPropSelectSetSubseries- , testSelectWhere- , testPropRequire- , testMapIndex- , testArgmax- , testArgmin- ]---testSelectRange :: TestTree-testSelectRange = testCase "from ... to ..." $ do- let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]- subSeries = series `select` ('b' `to` 'd')- expectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4)]- assertEqual mempty expectation subSeries---testSelectUnboundedRange :: TestTree-testSelectUnboundedRange = testCase "from and upto" $ do- let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]- openLeftsubSeries = series `select` from 'b'- openLeftExpectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4), ('e', 5)]- assertEqual mempty openLeftExpectation openLeftsubSeries-- let openRightsubSeries = series `select` upto 'b'- openRightExpectation = fromStrictMap $ MS.fromList [('a', 1), ('b', 2)]- assertEqual mempty openRightExpectation openRightsubSeries---testSelectUnboundedRangeEquivalence :: TestTree-testSelectUnboundedRangeEquivalence - = testProperty "Combining unbounded ranges is equivalent to a bounded range" - $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))- (b1, b2) <- (,) <$> forAll Gen.alpha <*> forAll Gen.alpha- let start = min b1 b2- end = max b1 b2- (xs :: Series Vector Char Int) = fromStrictMap m1-- (xs `select` start `to` end) === ( (xs `select` from start) `select` upto end)---testPropSelectRangeSubseries :: TestTree-testPropSelectRangeSubseries = testProperty "xs `select` <x> `to` <y> always returns a proper subseries" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))- start <- forAll Gen.alpha- end <- forAll Gen.alpha- let (xs :: Series Vector Char Int) = fromStrictMap m1- ys = xs `select` start `to` end- - assert $ index xs `Index.contains` index ys---testSelectRangeEmptyRange :: TestTree-testSelectRangeEmptyRange = testCase "from ... to ... on an empty `Range``" $ do- let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]- subSeries = series `select` ('f' `to` 'z')- assertEqual mempty mempty subSeries---testSelectSet :: TestTree-testSelectSet = testCase "select" $ do- let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]- subSeries = series `select` Index.fromList ['a', 'd', 'x']- expectation = fromStrictMap $ MS.fromList [('a', 1), ('d', 4)]- - assertEqual mempty expectation subSeries---testPropSelectSetSubseries :: TestTree-testPropSelectSetSubseries = testProperty "xs `select` <some set> always returns a proper subseries" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))- selection <- forAll $ Gen.set (Range.linear 0 10) Gen.alpha- let (xs :: Series Vector Char Int) = fromStrictMap m1- ys = xs `select` selection- - assert $ index xs `Index.contains` index ys---testSelectWhere :: TestTree-testSelectWhere = testCase "selectWhere" $ do- let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]- subSeries = series `selectWhere` fmap (>3) series - expectation = fromStrictMap $ MS.fromList [('d', 4), ('e', 5)]- - assertEqual mempty expectation subSeries---testPropRequire :: TestTree-testPropRequire = testProperty "require always returns a series with the expected index" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.int (Range.linear 0 1000))- ss <- forAll $ Gen.set (Range.linear 0 100) (Gen.int (Range.linear (-100) 100))- - let (xs :: Series Vector Int Int) = fromStrictMap m1- ix = Index.fromSet ss- index (xs `require` ix) === ix ---testMapIndex :: TestTree-testMapIndex = testCase "mapIndex" $ do- let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 3), ("bc", 4), ("c", 5)]- subSeries = series `mapIndex` take 1- expectation = fromList [("a", 1), ("b", 3), ("c", 5)]- - assertEqual mempty expectation subSeries---testArgmax :: TestTree-testArgmax = testCase "argmax" $ do- let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 10), ("bc", 4), ("c", 5)]- expectation = Just "bb"- - assertEqual mempty expectation (argmax series)--testArgmin :: TestTree-testArgmin = testCase "argmin" $ do- let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", -10), ("bc", 4), ("c", 5)]- expectation = Just "bb"- +module Test.Data.Series.Generic.View (tests) where + +import qualified Data.Map.Strict as MS +import Data.Series.Generic ( Series, index, fromStrictMap, fromList, to, from, upto, select + , selectWhere, require, mapIndex, argmax, argmin, ) +import qualified Data.Series.Index as Index +import Data.Vector ( Vector ) + +import Hedgehog ( property, forAll, (===), assert ) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +import Test.Tasty ( testGroup, TestTree ) +import Test.Tasty.Hedgehog ( testProperty ) +import Test.Tasty.HUnit ( testCase, assertEqual ) + +tests :: TestTree +tests = testGroup "Data.Series.Generic.View" [ testSelectRange + , testSelectUnboundedRange + , testSelectUnboundedRangeEquivalence + , testSelectRangeEmptyRange + , testPropSelectRangeSubseries + , testSelectSet + , testPropSelectSetSubseries + , testSelectWhere + , testPropRequire + , testMapIndex + , testArgmax + , testArgmin + ] + + +testSelectRange :: TestTree +testSelectRange = testCase "from ... to ..." $ do + let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + subSeries = series `select` ('b' `to` 'd') + expectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4)] + assertEqual mempty expectation subSeries + + +testSelectUnboundedRange :: TestTree +testSelectUnboundedRange = testCase "from and upto" $ do + let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + openLeftsubSeries = series `select` from 'b' + openLeftExpectation = fromStrictMap $ MS.fromList [('b', 2), ('c', 3), ('d', 4), ('e', 5)] + assertEqual mempty openLeftExpectation openLeftsubSeries + + let openRightsubSeries = series `select` upto 'b' + openRightExpectation = fromStrictMap $ MS.fromList [('a', 1), ('b', 2)] + assertEqual mempty openRightExpectation openRightsubSeries + + +testSelectUnboundedRangeEquivalence :: TestTree +testSelectUnboundedRangeEquivalence + = testProperty "Combining unbounded ranges is equivalent to a bounded range" + $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000)) + (b1, b2) <- (,) <$> forAll Gen.alpha <*> forAll Gen.alpha + let start = min b1 b2 + end = max b1 b2 + (xs :: Series Vector Char Int) = fromStrictMap m1 + + (xs `select` start `to` end) === ( (xs `select` from start) `select` upto end) + + +testPropSelectRangeSubseries :: TestTree +testPropSelectRangeSubseries = testProperty "xs `select` <x> `to` <y> always returns a proper subseries" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000)) + start <- forAll Gen.alpha + end <- forAll Gen.alpha + let (xs :: Series Vector Char Int) = fromStrictMap m1 + ys = xs `select` start `to` end + + assert $ index xs `Index.contains` index ys + + +testSelectRangeEmptyRange :: TestTree +testSelectRangeEmptyRange = testCase "from ... to ... on an empty `Range``" $ do + let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + subSeries = series `select` ('f' `to` 'z') + assertEqual mempty mempty subSeries + + +testSelectSet :: TestTree +testSelectSet = testCase "select" $ do + let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + subSeries = series `select` Index.fromList ['a', 'd', 'x'] + expectation = fromStrictMap $ MS.fromList [('a', 1), ('d', 4)] + + assertEqual mempty expectation subSeries + + +testPropSelectSetSubseries :: TestTree +testPropSelectSetSubseries = testProperty "xs `select` <some set> always returns a proper subseries" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000)) + selection <- forAll $ Gen.set (Range.linear 0 10) Gen.alpha + let (xs :: Series Vector Char Int) = fromStrictMap m1 + ys = xs `select` selection + + assert $ index xs `Index.contains` index ys + + +testSelectWhere :: TestTree +testSelectWhere = testCase "selectWhere" $ do + let (series :: Series Vector Char Int) = fromStrictMap $ MS.fromList [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] + subSeries = series `selectWhere` fmap (>3) series + expectation = fromStrictMap $ MS.fromList [('d', 4), ('e', 5)] + + assertEqual mempty expectation subSeries + + +testPropRequire :: TestTree +testPropRequire = testProperty "require always returns a series with the expected index" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.int (Range.linear 0 1000) <*> Gen.int (Range.linear 0 1000)) + ss <- forAll $ Gen.set (Range.linear 0 100) (Gen.int (Range.linear (-100) 100)) + + let (xs :: Series Vector Int Int) = fromStrictMap m1 + ix = Index.fromSet ss + index (xs `require` ix) === ix + + +testMapIndex :: TestTree +testMapIndex = testCase "mapIndex" $ do + let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 3), ("bc", 4), ("c", 5)] + subSeries = series `mapIndex` take 1 + expectation = fromList [("a", 1), ("b", 3), ("c", 5)] + + assertEqual mempty expectation subSeries + + +testArgmax :: TestTree +testArgmax = testCase "argmax" $ do + let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", 10), ("bc", 4), ("c", 5)] + expectation = Just "bb" + + assertEqual mempty expectation (argmax series) + +testArgmin :: TestTree +testArgmin = testCase "argmin" $ do + let (series :: Series Vector String Int) = fromList [("aa", 1), ("ab", 2), ("bb", -10), ("bc", 4), ("c", 5)] + expectation = Just "bb" + assertEqual mempty expectation (argmin series)
test/Test/Data/Series/Generic/Zip.hs view
@@ -1,147 +1,147 @@--module Test.Data.Series.Generic.Zip ( tests ) where---import Control.Monad ( forM_ )--import Data.Maybe ( fromJust, isNothing )-import Data.Monoid ( Sum(..) )-import Data.Series.Generic ( Series(index), mapStrategy- , fromStrictMap, fromList, zipWith, select, at, replace, (|->), (<-|)- )-import qualified Data.Series.Generic as Series-import qualified Data.Series.Index as Index -import Data.Vector ( Vector )--import Hedgehog ( property, forAll, (===), assert )-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range--import Prelude hiding ( zipWith )--import Test.Tasty ( testGroup, TestTree ) -import Test.Tasty.Hedgehog ( testProperty )-import Test.Tasty.HUnit ( testCase, assertEqual )--tests :: TestTree-tests = testGroup "Data.Series.Generic.Zip" [ testZipWith- , testPropZipWithMatched- , testPropZipWithMatchedAndZipWithMonoid- , testPropZipWith- , testPropReplace- , testPropReplaceInfix- , testPropZipWithStrategySkipStrategy- , testMapStrategy- ]---testZipWith :: TestTree-testZipWith = testCase "zipWith" $ do- let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)]- (s2 :: Series Vector Char Int) = fromList [('x', 25), ('b', 10)]- expectation = fromList [('a', Nothing), ('b', Just 15), ('x', Nothing)]- - assertEqual mempty expectation (zipWith (+) s1 s2)---testPropZipWithMatched :: TestTree-testPropZipWithMatched - = testProperty "zipWith when keys all match" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))- let (xs :: Series Vector Char Int) = fromStrictMap m1- zipWith (+) xs xs === fmap (Just . (*2)) xs---testPropZipWithMatchedAndZipWithMonoid :: TestTree-testPropZipWithMatchedAndZipWithMonoid - = testProperty "zipWithMonoid and zipWithStrategy give compatible results" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))- m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000))- let (xs :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m1- (ys :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m2-- expectation = Series.zipWithStrategy (<>) (mapStrategy id) (mapStrategy id) xs ys- - expectation === Series.zipWithMonoid (<>) xs ys----testPropZipWith :: TestTree-testPropZipWith - = testProperty "zipWith when keys all match" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))- m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))- let (x1 :: Series Vector String Int) = fromStrictMap m1- x2 = fromStrictMap m2- common = index x1 `Index.intersection` index x2- symdiff = (index x1 `Index.union` index x2) `Index.difference` common- comb = zipWith (+) x1 x2-- forM_ common $ \k -> do- let left = fromJust $ x1 `at` k- right = fromJust $ x2 `at` k- fromJust (comb `at` k) === Just (left + right)- - assert $ all isNothing $ Series.values (comb `select` symdiff)---testPropReplace :: TestTree-testPropReplace - = testProperty "replace" $ property $ do- ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) - ns <- forAll $ Gen.list (Range.linear 0 10) (Gen.int $ Range.linear (-500) 500) - ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150)- let (xs :: Series Vector Int Int) = fromList (zip ixs ms)- ys = fromList (zip [0..] ns)- rs = ys `replace` xs-- index rs === index xs-- let commonKeys = index xs `Index.intersection` index ys-- (rs `select` commonKeys) === (ys `select` commonKeys)---testPropReplaceInfix :: TestTree-testPropReplaceInfix - = testProperty "(|->) and (<-|)" $ property $ do- ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) - ns <- forAll $ Gen.list (Range.linear 0 10) (Gen.int $ Range.linear (-500) 500) - ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150)- let (xs :: Series Vector Int Int) = fromList (zip ixs ms)- ys = fromList (zip [0..] ns)- rs = ys `replace` xs- - ys |-> xs === rs - ys |-> xs === xs <-| ys ---testPropZipWithStrategySkipStrategy :: TestTree-testPropZipWithStrategySkipStrategy - = testProperty "zipWithStrategy f skipStrategy skipStrategy is equivalent to zipWithMatched" $ property $ do- m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))- m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000))-- let (xs :: Series Vector String Int) = fromStrictMap m1- ys = fromStrictMap m2-- expectation = Series.zipWithMatched (+) xs ys- - expectation === Series.zipWithStrategy (+) Series.skipStrategy Series.skipStrategy xs ys---testMapStrategy :: TestTree-testMapStrategy - = testCase "mapStrategy works as expected" $ do- let (xs :: Series Vector Int Int) = Series.fromList $ zip [0..] [1,2,3,4,5]- ys = Series.fromList $ zip [3..] [3,4,5]- - expected = Series.fromList [ (0, 1+1)- , (1, 2+1)- , (2, 3+1)- , (3, 4+3)- , (4, 5+4)- , (5, 5*2)- ]-- assertEqual mempty expected $ Series.zipWithStrategy (+) (mapStrategy (+1)) (mapStrategy (*2)) xs ys+ +module Test.Data.Series.Generic.Zip ( tests ) where + + +import Control.Monad ( forM_ ) + +import Data.Maybe ( fromJust, isNothing ) +import Data.Monoid ( Sum(..) ) +import Data.Series.Generic ( Series(index), mapStrategy + , fromStrictMap, fromList, zipWith, select, at, replace, (|->), (<-|) + ) +import qualified Data.Series.Generic as Series +import qualified Data.Series.Index as Index +import Data.Vector ( Vector ) + +import Hedgehog ( property, forAll, (===), assert ) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +import Prelude hiding ( zipWith ) + +import Test.Tasty ( testGroup, TestTree ) +import Test.Tasty.Hedgehog ( testProperty ) +import Test.Tasty.HUnit ( testCase, assertEqual ) + +tests :: TestTree +tests = testGroup "Data.Series.Generic.Zip" [ testZipWith + , testPropZipWithMatched + , testPropZipWithMatchedAndZipWithMonoid + , testPropZipWith + , testPropReplace + , testPropReplaceInfix + , testPropZipWithStrategySkipStrategy + , testMapStrategy + ] + + +testZipWith :: TestTree +testZipWith = testCase "zipWith" $ do + let (s1 :: Series Vector Char Int) = fromList [('a', 1), ('b', 5)] + (s2 :: Series Vector Char Int) = fromList [('x', 25), ('b', 10)] + expectation = fromList [('a', Nothing), ('b', Just 15), ('x', Nothing)] + + assertEqual mempty expectation (zipWith (+) s1 s2) + + +testPropZipWithMatched :: TestTree +testPropZipWithMatched + = testProperty "zipWith when keys all match" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000)) + let (xs :: Series Vector Char Int) = fromStrictMap m1 + zipWith (+) xs xs === fmap (Just . (*2)) xs + + +testPropZipWithMatchedAndZipWithMonoid :: TestTree +testPropZipWithMatchedAndZipWithMonoid + = testProperty "zipWithMonoid and zipWithStrategy give compatible results" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000)) + m2 <- forAll $ Gen.map (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.int (Range.linear 0 1000)) + let (xs :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m1 + (ys :: Series Vector Char (Sum Int)) = Series.map Sum $ fromStrictMap m2 + + expectation = Series.zipWithStrategy (<>) (mapStrategy id) (mapStrategy id) xs ys + + expectation === Series.zipWithMonoid (<>) xs ys + + + +testPropZipWith :: TestTree +testPropZipWith + = testProperty "zipWith when keys all match" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000)) + m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000)) + let (x1 :: Series Vector String Int) = fromStrictMap m1 + x2 = fromStrictMap m2 + common = index x1 `Index.intersection` index x2 + symdiff = (index x1 `Index.union` index x2) `Index.difference` common + comb = zipWith (+) x1 x2 + + forM_ common $ \k -> do + let left = fromJust $ x1 `at` k + right = fromJust $ x2 `at` k + fromJust (comb `at` k) === Just (left + right) + + assert $ all isNothing $ Series.values (comb `select` symdiff) + + +testPropReplace :: TestTree +testPropReplace + = testProperty "replace" $ property $ do + ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) + ns <- forAll $ Gen.list (Range.linear 0 10) (Gen.int $ Range.linear (-500) 500) + ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150) + let (xs :: Series Vector Int Int) = fromList (zip ixs ms) + ys = fromList (zip [0..] ns) + rs = ys `replace` xs + + index rs === index xs + + let commonKeys = index xs `Index.intersection` index ys + + (rs `select` commonKeys) === (ys `select` commonKeys) + + +testPropReplaceInfix :: TestTree +testPropReplaceInfix + = testProperty "(|->) and (<-|)" $ property $ do + ms <- forAll $ Gen.list (Range.linear 10 100) (Gen.int $ Range.linear (-500) 500) + ns <- forAll $ Gen.list (Range.linear 0 10) (Gen.int $ Range.linear (-500) 500) + ixs <- forAll $ Gen.list (Range.singleton $ length ns) (Gen.int $ Range.linear 0 150) + let (xs :: Series Vector Int Int) = fromList (zip ixs ms) + ys = fromList (zip [0..] ns) + rs = ys `replace` xs + + ys |-> xs === rs + ys |-> xs === xs <-| ys + + +testPropZipWithStrategySkipStrategy :: TestTree +testPropZipWithStrategySkipStrategy + = testProperty "zipWithStrategy f skipStrategy skipStrategy is equivalent to zipWithMatched" $ property $ do + m1 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000)) + m2 <- forAll $ Gen.map (Range.linear 0 100) ((,) <$> Gen.string (Range.singleton 2) Gen.alpha <*> Gen.int (Range.linear 0 1000)) + + let (xs :: Series Vector String Int) = fromStrictMap m1 + ys = fromStrictMap m2 + + expectation = Series.zipWithMatched (+) xs ys + + expectation === Series.zipWithStrategy (+) Series.skipStrategy Series.skipStrategy xs ys + + +testMapStrategy :: TestTree +testMapStrategy + = testCase "mapStrategy works as expected" $ do + let (xs :: Series Vector Int Int) = Series.fromList $ zip [0..] [1,2,3,4,5] + ys = Series.fromList $ zip [3..] [3,4,5] + + expected = Series.fromList [ (0, 1+1) + , (1, 2+1) + , (2, 3+1) + , (3, 4+3) + , (4, 5+4) + , (5, 5*2) + ] + + assertEqual mempty expected $ Series.zipWithStrategy (+) (mapStrategy (+1)) (mapStrategy (*2)) xs ys
test/Test/Data/Series/Index.hs view
@@ -1,113 +1,123 @@--module Test.Data.Series.Index (tests) where--import qualified Data.Series.Index as Index-import qualified Data.Series.Index.Internal as Index.Internal-import qualified Data.Set as Set-import qualified Data.Vector as Vector--import Hedgehog ( property, forAll, tripping, assert, (===) )-import qualified Hedgehog.Gen as Gen-import qualified Hedgehog.Range as Range---import Test.Tasty ( testGroup, TestTree ) -import Test.Tasty.Hedgehog ( testProperty )---tests :: TestTree-tests = testGroup "Data.Series.Index" [ testPropRange- , testPropFromToSet- , testPropFromToList- , testPropFromToAscList- , testPropFromToVector- , testPropFromToAscVector- , testPropMemberNotMember- , testPropFilter- ]---testPropRange :: TestTree-testPropRange = testProperty "range always includes the start, and all elements less than/equal to end" $ property $ do- start <- forAll $ Gen.int (Range.linear 0 50)- end <- forAll $ Gen.int (Range.linear 51 100)- step <- forAll $ Gen.int (Range.linear 1 5)-- let ix = Index.range (+step) start end -- assert $ start `Index.member` ix- assert $ maximum ix <= end-- if (end - start) `mod` step == 0- then assert (end `Index.member` ix)- else assert (end `Index.notMember` ix)---testPropFromToSet :: TestTree-testPropFromToSet = testGroup "conversion to/from Set" - [ testProperty "fromSet / toSet" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- tripping (Set.fromList ms) Index.fromSet (Just . Index.toSet)- , testProperty "toIndex / fromIndex" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- tripping (Set.fromList ms) (Index.toIndex :: Set.Set (Char, Char) -> Index.Index (Char, Char)) (Just . Index.fromIndex)- ]---testPropFromToList :: TestTree-testPropFromToList = testGroup "conversion to/from list" - [ testProperty "fromList / toAscList" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- let index = Index.fromList ms- tripping index (reverse . Index.toAscList) (Just . Index.fromList)- , testProperty "toIndex / fromIndex" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- let index = Index.toIndex ms :: Index.Index (Char, Char)- tripping index (reverse . Index.fromIndex) (Just . (Index.toIndex :: [(Char, Char)] -> Index.Index (Char, Char)))- ]---testPropFromToAscList :: TestTree-testPropFromToAscList = testProperty "fromAscList / toAscList" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- let index = Index.fromList ms- tripping index Index.toAscList (Just . Index.Internal.fromAscList)---testPropFromToVector :: TestTree-testPropFromToVector = testGroup "conversion to/from Vector"- [ testProperty "fromVector / toAscVector" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- let index = Index.fromList ms- tripping index (Vector.reverse . Index.toAscVector) (Just . Index.fromVector)- , testProperty "toIndex / fromIndex" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- let index = Index.toIndex ms :: Index.Index (Char, Char)- tripping index (Vector.reverse . Index.fromIndex) (Just . (Index.toIndex :: Vector.Vector (Char, Char) -> Index.Index (Char, Char)))- ]---testPropFromToAscVector :: TestTree-testPropFromToAscVector = testProperty "fromAscVector / toAscVector" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha)- let index = Index.fromList ms- tripping index (Index.toAscVector :: Index.Index (Char, Char) -> Vector.Vector (Char, Char)) (Just . Index.Internal.fromAscVector)---testPropMemberNotMember :: TestTree-testPropMemberNotMember = testProperty "elements are either a member or not a member of the index" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))- k <- forAll $ Gen.int (Range.linear (-100) 100)-- let ix = Index.fromList ms- assert $ (k `Index.member` ix) /= (k `Index.notMember` ix)---testPropFilter :: TestTree-testPropFilter = testProperty "filter works just like for Sets" $ property $ do- ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100))-- let ss = Set.fromList ms- ix = Index.fromSet ss- - Index.fromSet (Set.filter even ss) === Index.filter even ix+ +module Test.Data.Series.Index (tests) where + +import qualified Data.Series.Index as Index +import qualified Data.Series.Index.Internal as Index.Internal +import qualified Data.Set as Set +import qualified Data.Vector as Vector + +import Hedgehog ( property, forAll, tripping, assert, (===) ) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + + +import Test.Tasty ( testGroup, TestTree ) +import Test.Tasty.Hedgehog ( testProperty ) + + +tests :: TestTree +tests = testGroup "Data.Series.Index" [ testPropRange + , testPropFromToSet + , testPropFromToList + , testPropFromToAscList + , testPropFromToVector + , testPropFromToAscVector + , testPropMemberNotMember + , testPropIndexed + , testPropFilter + ] + + +testPropRange :: TestTree +testPropRange = testProperty "range always includes the start, and all elements less than/equal to end" $ property $ do + start <- forAll $ Gen.int (Range.linear 0 50) + end <- forAll $ Gen.int (Range.linear 51 100) + step <- forAll $ Gen.int (Range.linear 1 5) + + let ix = Index.range (+step) start end + + assert $ start `Index.member` ix + assert $ maximum ix <= end + + if (end - start) `mod` step == 0 + then assert (end `Index.member` ix) + else assert (end `Index.notMember` ix) + + +testPropFromToSet :: TestTree +testPropFromToSet = testGroup "conversion to/from Set" + [ testProperty "fromSet / toSet" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + tripping (Set.fromList ms) Index.fromSet (Just . Index.toSet) + , testProperty "toIndex / fromIndex" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + tripping (Set.fromList ms) (Index.toIndex :: Set.Set (Char, Char) -> Index.Index (Char, Char)) (Just . Index.fromIndex) + ] + + +testPropFromToList :: TestTree +testPropFromToList = testGroup "conversion to/from list" + [ testProperty "fromList / toAscList" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + let index = Index.fromList ms + tripping index (reverse . Index.toAscList) (Just . Index.fromList) + , testProperty "toIndex / fromIndex" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + let index = Index.toIndex ms :: Index.Index (Char, Char) + tripping index (reverse . Index.fromIndex) (Just . (Index.toIndex :: [(Char, Char)] -> Index.Index (Char, Char))) + ] + + +testPropFromToAscList :: TestTree +testPropFromToAscList = testProperty "fromAscList / toAscList" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + let index = Index.fromList ms + tripping index Index.toAscList (Just . Index.Internal.fromAscList) + + +testPropFromToVector :: TestTree +testPropFromToVector = testGroup "conversion to/from Vector" + [ testProperty "fromVector / toAscVector" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + let index = Index.fromList ms + tripping index (Vector.reverse . Index.toAscVector) (Just . Index.fromVector) + , testProperty "toIndex / fromIndex" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + let index = Index.toIndex ms :: Index.Index (Char, Char) + tripping index (Vector.reverse . Index.fromIndex) (Just . (Index.toIndex :: Vector.Vector (Char, Char) -> Index.Index (Char, Char))) + ] + + +testPropFromToAscVector :: TestTree +testPropFromToAscVector = testProperty "fromAscVector / toAscVector" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) ((,) <$> Gen.alpha <*> Gen.alpha) + let index = Index.fromList ms + tripping index (Index.toAscVector :: Index.Index (Char, Char) -> Vector.Vector (Char, Char)) (Just . Index.Internal.fromAscVector) + + +testPropMemberNotMember :: TestTree +testPropMemberNotMember = testProperty "elements are either a member or not a member of the index" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100)) + k <- forAll $ Gen.int (Range.linear (-100) 100) + + let ix = Index.fromList ms + assert $ (k `Index.member` ix) /= (k `Index.notMember` ix) + + +testPropIndexed :: TestTree +testPropIndexed = testProperty "indexed works just like for Vectors" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100)) + + let ix = Index.fromList ms + + Index.toAscVector (Index.indexed ix) === Vector.indexed (Index.toAscVector ix) + + +testPropFilter :: TestTree +testPropFilter = testProperty "filter works just like for Sets" $ property $ do + ms <- forAll $ Gen.list (Range.linear 0 50) (Gen.int (Range.linear (-100) 100)) + + let ss = Set.fromList ms + ix = Index.fromSet ss + + Index.fromSet (Set.filter even ss) === Index.filter even ix
− test/Test/Utils.hs
@@ -1,13 +0,0 @@--module Test.Utils ( approx ) where--import Data.AEq ( AEq((~==)))-import Hedgehog ( MonadTest, diff )-import Hedgehog.Internal.Source ( HasCallStack, withFrozenCallStack ) ----- | Fails the test if the two arguments provided are not equal to within `epsilon`.-approx :: (MonadTest m, AEq a, Show a, HasCallStack) => a -> a -> m ()-approx x y =- withFrozenCallStack $- diff x (~==) y