Haskell Moving Average Calculation

I am working on Haskell training, so I tried to implement the moving average function. Here is my code:

mAverage :: Int-> [Int] -> [Float]
mAverage x a = [fromIntegral k / fromIntegral x | k <- rawAverage]
    where
    rawAverage = mAverage' x a a

-- First list contains original values; second list contains moving average computations
mAverage' :: Int -> [Int] -> [Int] -> [Int]
mAverage' 1 a b = b
mAverage' x a b = mAverage' (x - 1) a' b'
    where
    a' = init a
    b' = zipWith (+) a' (tail b)

where the user calls the MEWP with a length for each average and a list of values ​​(for example, mAverage 4 [1,2..100]).

However, when I run the code in the input mAverage 4 [1,2..100000], I understand that it takes 3.6 seconds in ghci (using :set +s) and uses a gigabyte of memory. This seems very inefficient to me, since the equivalent function takes a fraction of a second in Python. Is there a way to make my code more efficient?

+4
source share
5 answers

Here is a direct list-based solution that is idiomatic and fast enough, although it requires more memory.

import Data.List (tails)

mavg :: Fractional b => Int -> [b] -> [b]
mavg k lst = take (length lst-k) $ map average $ tails lst
   where average = (/ fromIntegral k) . sum . take k

average .

, , , .

import Data.List (scanl')

mavg :: Fractional b => Int -> [b] -> [b]
mavg k lst = map (/ fromIntegral k) $ scanl' (+) (sum h) $ zipWith (-) t lst
  where (h, t) = splitAt k lst 

, , , . , .

mavg k lst = map average $ scanl' enq ([], take k lst) $ drop k lst
  where 
    average (l,r) = (sum l + sum r) / fromIntegral k

    enq (l, []) x = enq ([], reverse l) x
    enq (l, (_:r)) x = (x:l, r)

, ghci . , - scanl' ghci.

+5

- , . , . , . - average . , . . , O (k) ( k - ) ( , , ), , State :)

{-# LANGUAGE UnicodeSyntax #-}

module MovingAverage where

import           Control.Monad       (forM)
import           Control.Monad.State (evalState, gets, modify)

moving :: Fractional a ⇒ Int → [a] → [a]
moving n _  | n <= 0 = error "non-positive argument"
moving n xs = evalState (forM xs $ \x → modify ((x:) . take (n-1)) >> gets average) []
  where
    average xs = sum xs / fromIntegral n
+9

.

, , , . , , , . ( , remaining_data sum initial_data .)

, , , .

slidingAverage ( ), centeredSlidingAverage , .

import Data.List (splitAt, replicate)

slidingAverage :: Int -> [Int] -> [Double] -- window size, source list -> list of averages
slidingAverage w xs = map divide $ initial_sum : slidingSum initial_sum xs remaining_data
  where
    divide = (\n -> (fromIntegral n) / (fromIntegral w))  -- divides the sums by window size
    initial_sum = sum initial_data
    (initial_data, remaining_data) = splitAt w xs

centeredSlidingAverage :: Int -> [Int] -> [Double] -- window size, source list -> list of averages
centeredSlidingAverage w xs = slidingAverage w $ left_padding ++ xs ++ right_padding
  where
    left_padding = replicate half_width 0
    right_padding = replicate (w - half_width) 0
    half_width = (w `quot` 2)   -- quot is integer division

slidingSum :: Int -> [Int] -> [Int] -> [Int] -- window_sum before_window after_window -> list of sums
slidingSum _ _ [] = []
slidingSum window_sum before_window after_window = new_sum : slidingSum new_sum new_before new_after
  where
    value_to_go = head before_window
    new_before = tail before_window
    value_to_come = head after_window
    new_after = tail after_window
    new_sum = window_sum - value_to_go + value_to_come

When I try length $ slidingAverage 10 [1..1000000], it takes less than a second for my MBP. Due to laziness , it centeredSlidingAveragetakes about the same time.

+1
source

One easy way to do this, which also uses O (n) complexity

movingAverage :: (Fractional a) => Int -> [a] -> [a]
movingAverage n _ | n <= 0 = error "non-positive argument"
movingAverage n xs = fmap average $ groupBy n xs
  where average xs' = sum xs' / fromIntegral (length xs')

groupBy :: Int -> [a] -> [[a]]
groupBy _ [] = []
groupBy n xs = go [] xs
  where
    go _ []      = []
    go l (x:xs') = (x:t) : go (x:l) xs'
      where t = take (n-1) l
0
source

Another way is to use STUArray.

import           Data.Array.Unboxed
import           Data.Array.ST
import           Data.STRef
import           Control.Monad
import           Control.Monad.ST

movingAverage  :: [Double] -> IO [Double]
movingAverage vals = stToIO $ do
  let end = length vals - 1
  myArray <- newArray (1, end) 0 :: ST s (STArray s Int Double)
  forM_ [1 .. end] $ \i -> do
    let cval = vals !! i
    let lval = vals !! (i-1)
    writeArray myArray i ((cval + lval)/2)
  getElems myArray
0
source

Source: https://habr.com/ru/post/1665035/


All Articles