on aaa775b help sign in
353 lines 11 kB history raw
1-- |
2-- Module : Streamly.Internal.Data.Fold.Window
3-- Copyright : (c) 2020 Composewell Technologies
4-- License : Apache-2.0
5-- Maintainer : streamly@composewell.com
6-- Stability : experimental
7-- Portability : GHC
8--
9-- Simple incremental statistical measures over a stream of data. All
10-- operations use numerically stable floating point arithmetic.
11--
12-- Measurements can be performed over the entire input stream or on a sliding
13-- window of fixed or variable size. Where possible, measures are computed
14-- online without buffering the input stream.
15--
16-- Currently there is no overflow detection.
17--
18-- For more advanced statistical measures see the @streamly-statistics@
19-- package.
21module Streamly.Internal.Data.Fold.Window
22 (
23 -- * Incremental Folds
24 -- | Folds of type @Fold m (a, Maybe a) b@ are incremental sliding window
25 -- folds. An input of type @(a, Nothing)@ indicates that the input element
26 -- @a@ is being inserted in the window without ejecting an old value
27 -- increasing the window size by 1. An input of type @(a, Just a)@
28 -- indicates that the first element is being inserted in the window and the
29 -- second element is being removed from the window, the window size remains
30 -- the same. The window size can only increase and never decrease.
31 --
32 -- You can compute the statistics over the entire stream using sliding
33 -- window folds by keeping the second element of the input tuple as
34 -- @Nothing@.
35 --
36 lmap
37 , cumulative
39 -- ** Sums
40 , length
41 , sum
42 , sumInt
43 , powerSum
44 , powerSumFrac
46 -- ** Location
47 , minimum
48 , maximum
49 , range
50 , mean
51 )
52where
54import Data.Bifunctor(bimap)
55import Data.Function ((&))
56import Data.Maybe (fromMaybe)
58import Streamly.Internal.Data.Fold.Type (Fold(..), Step(..))
59import Streamly.Internal.Data.Tuple.Strict (Tuple'(..), Tuple3'(..))
61import Prelude hiding (length, sum, minimum, maximum)
63import qualified Deque.Strict as DQ
64import qualified Streamly.Data.Fold as Fold
66-- $setup
67-- >>> import Data.Bifunctor(bimap)
68-- >>> import qualified Streamly.Data.Fold as Fold
69-- >>> import Prelude hiding (length, sum, minimum, maximum)
71-------------------------------------------------------------------------------
72-- Utilities
73-------------------------------------------------------------------------------
75-- | Map a function on the incoming as well as outgoing element of a rolling
76-- window fold.
77--
78-- >>> lmap f = Fold.lmap (bimap f (f <$>))
79--
80{-# INLINE lmap #-}
81lmap :: (c -> a) -> Fold m (a, Maybe a) b -> Fold m (c, Maybe c) b
82lmap f = Fold.lmap (bimap f (f <$>))
84-- | Convert an incremental fold to a cumulative fold using the entire input
85-- stream as a single window.
86--
87-- >>> cumulative f = Fold.lmap (\x -> (x, Nothing)) f
88--
89{-# INLINE cumulative #-}
90cumulative :: Fold m (a, Maybe a) b -> Fold m a b
91cumulative = Fold.lmap (, Nothing)
93-------------------------------------------------------------------------------
94-- Sum
95-------------------------------------------------------------------------------
97-- XXX Overflow.
98--
99-- | The sum of all the elements in a rolling window. The input elements are
100-- required to be intergal numbers.
101--
102-- This was written in the hope that it would be a tiny bit faster than 'sum'
103-- for 'Integral' values. But turns out that 'sum' is 2% faster than this even
104-- for intergal values!
105--
106-- /Internal/
107--
108{-# INLINE sumInt #-}
109sumInt :: forall m a. (Monad m, Integral a) => Fold m (a, Maybe a) a
110sumInt = Fold step initial extract
112 where
114 initial = return $ Partial (0 :: a)
116 step s (a, ma) =
117 return
118 $ Partial
119 $ case ma of
120 Nothing -> s + a
121 Just old -> s + a - old
123 extract = return
125-- XXX Overflow.
126--
127-- | Sum of all the elements in a rolling window:
128--
129-- \(S = \sum_{i=1}^n x_{i}\)
130--
131-- This is the first power sum.
132--
133-- >>> sum = powerSum 1
134--
135-- Uses Kahan-Babuska-Neumaier style summation for numerical stability of
136-- floating precision arithmetic.
137--
138-- /Space/: \(\mathcal{O}(1)\)
139--
140-- /Time/: \(\mathcal{O}(n)\)
141--
142{-# INLINE sum #-}
143sum :: forall m a. (Monad m, Num a) => Fold m (a, Maybe a) a
144sum = Fold step initial extract
146 where
148 initial =
149 return
150 $ Partial
151 $ Tuple'
152 (0 :: a) -- running sum
153 (0 :: a) -- accumulated rounding error
155 step (Tuple' total err) (new, mOld) =
156 let incr =
157 case mOld of
158 -- XXX new may be large and err may be small we may lose it
159 Nothing -> new - err
160 -- XXX if (new - old) is large we may lose err
161 Just old -> (new - old) - err
162 -- total is large and incr may be small, we may round incr here but
163 -- we will accumulate the rounding error in err1 in the next step.
164 total1 = total + incr
165 -- Accumulate any rounding error in err1
166 -- XXX In the Nothing case above we may lose err, therefore we
167 -- should use ((total1 - total) - new) + err here.
168 -- Or even in the just case if (new - old) is large we may lose
169 -- err, so we should use ((total1 - total) + (old - new)) + err.
170 err1 = (total1 - total) - incr
171 in return $ Partial $ Tuple' total1 err1
173 extract (Tuple' total _) = return total
175-- | The number of elements in the rolling window.
176--
177-- This is the \(0\)th power sum.
178--
179-- >>> length = powerSum 0
180--
181{-# INLINE length #-}
182length :: (Monad m, Num b) => Fold m (a, Maybe a) b
183length = Fold.foldl' step 0
185 where
187 step w (_, Nothing) = w + 1
188 step w _ = w
190-- | Sum of the \(k\)th power of all the elements in a rolling window:
191--
192-- \(S_k = \sum_{i=1}^n x_{i}^k\)
193--
194-- >>> powerSum k = lmap (^ k) sum
195--
196-- /Space/: \(\mathcal{O}(1)\)
197--
198-- /Time/: \(\mathcal{O}(n)\)
199{-# INLINE powerSum #-}
200powerSum :: (Monad m, Num a) => Int -> Fold m (a, Maybe a) a
201powerSum k = lmap (^ k) sum
203-- | Like 'powerSum' but powers can be negative or fractional. This is slower
204-- than 'powerSum' for positive intergal powers.
205--
206-- >>> powerSumFrac p = lmap (** p) sum
207--
208{-# INLINE powerSumFrac #-}
209powerSumFrac :: (Monad m, Floating a) => a -> Fold m (a, Maybe a) a
210powerSumFrac p = lmap (** p) sum
212-------------------------------------------------------------------------------
213-- Location
214-------------------------------------------------------------------------------
216-- Theoretically, we can approximate minimum in a rolling window by using a
217-- 'powerMean' with sufficiently large negative power.
218--
219-- XXX If we need to know the minimum in the window only once in a while then
220-- we can use linear search when it is extracted and not pay the cost all the
221-- time.
222--
223-- | The minimum element in a rolling window.
224--
225-- If you want to compute the minimum of the entire stream Fold.minimum from
226-- streamly package would be much faster.
227--
228-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
229--
230{-# INLINE minimum #-}
231minimum :: (Monad m, Ord a) => Fold m (a, Maybe a) a
232minimum = Fold step initial extract
234 where
236 initial = return $ Partial $ Tuple3' (0 :: Int) (0 :: Int)
237 (mempty :: DQ.Deque (Int, a))
239 step (Tuple3' i w q) (a, ma) =
240 case ma of
241 Nothing ->
242 return $ Partial $ Tuple3' (i + 1) (w + 1)
243 (headCheck i q (w + 1) & dqloop (i, a))
244 Just _ ->
245 return $ Partial $ Tuple3' (i + 1) w
246 (headCheck i q w & dqloop (i,a))
248 {-# INLINE headCheck #-}
249 headCheck i q w =
250 case DQ.uncons q of
251 Nothing -> q
252 Just (ia', q') ->
253 if fst ia' <= i - w
254 then q'
255 else q
257 dqloop ia q =
258 case DQ.unsnoc q of
259 Nothing -> DQ.snoc ia q
260 -- XXX This can be improved for the case of `=`
261 Just (ia', q') ->
262 if snd ia <= snd ia'
263 then dqloop ia q'
264 else DQ.snoc ia q
266 extract (Tuple3' _ _ q) = return $ snd
267 $ fromMaybe (0, error "min: Empty stream")
268 $ DQ.head q
270-- Theoretically, we can approximate maximum in a rolling window by using a
271-- 'powerMean' with sufficiently large positive power.
272--
273-- | The maximum element in a rolling window.
274--
275-- If you want to compute the maximum of the entire stream Fold.maximum from
276-- streamly package would be much faster.
277--
278-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
279--
280{-# INLINE maximum #-}
281maximum :: (Monad m, Ord a) => Fold m (a, Maybe a) a
282maximum = Fold step initial extract
284 where
286 initial = return $ Partial $ Tuple3' (0 :: Int) (0 :: Int)
287 (mempty :: DQ.Deque (Int, a))
289 step (Tuple3' i w q) (a, ma) =
290 case ma of
291 Nothing ->
292 return $ Partial $ Tuple3' (i + 1) (w + 1)
293 (headCheck i q (w + 1) & dqloop (i, a))
294 Just _ ->
295 return $ Partial $ Tuple3' (i + 1) w
296 (headCheck i q w & dqloop (i,a))
298 {-# INLINE headCheck #-}
299 headCheck i q w =
300 case DQ.uncons q of
301 Nothing -> q
302 Just (ia', q') ->
303 if fst ia' <= i - w
304 then q'
305 else q
307 dqloop ia q =
308 case DQ.unsnoc q of
309 Nothing -> DQ.snoc ia q
310 -- XXX This can be improved for the case of `=`
311 Just (ia', q') ->
312 if snd ia >= snd ia'
313 then dqloop ia q'
314 else DQ.snoc ia q
316 extract (Tuple3' _ _ q) =
317 return
318 $ snd
319 $ fromMaybe (0, error "max: Empty stream")
320 $ DQ.head q
322-- | Arithmetic mean of elements in a sliding window:
323--
324-- \(\mu = \frac{\sum_{i=1}^n x_{i}}{n}\)
325--
326-- This is also known as the Simple Moving Average (SMA) when used in the
327-- sliding window and Cumulative Moving Avergae (CMA) when used on the entire
328-- stream.
329--
330-- >>> mean = Fold.teeWith (/) sum length
331--
332-- /Space/: \(\mathcal{O}(1)\)
333--
334-- /Time/: \(\mathcal{O}(n)\)
335{-# INLINE mean #-}
336mean :: forall m a. (Monad m, Fractional a) => Fold m (a, Maybe a) a
337mean = Fold.teeWith (/) sum length
340-- | The difference between the maximum and minimum elements of a rolling window.
341--
342-- >>> range = Fold.teeWith (-) maximum minimum
343--
344-- If you want to compute the range of the entire stream @Fold.teeWith (-)
345-- Fold.maximum Fold.min@ from the streamly package would be much faster.
346--
347-- /Space/: \(\mathcal{O}(n)\) where @n@ is the window size.
348--
349-- /Time/: \(\mathcal{O}(n*w)\) where \(w\) is the window size.
350--
351{-# INLINE range #-}
352range :: (Monad m, Num a, Ord a) => Fold m (a, Maybe a) a
353range = Fold.teeWith (-) maximum minimum