Calculating the Moving Average of a List

后端 未结 18 1093
悲哀的现实
悲哀的现实 2020-12-07 09:01

This weekend I decided to try my hand at some Scala and Clojure. I\'m proficient with object oriented programming, and so Scala was easy to pick up as a language, but wante

相关标签:
18条回答
  • 2020-12-07 09:36

    Here is another (functional) Clojure solution:

    (defn avarage [coll]
      (/ (reduce + coll)
         (count coll)))
    
    (defn ma [period coll]
      (map avarage (partition period 1 coll)))
    

    The zeros at the beginning of the sequence must still be added if that is a requirement.

    0 讨论(0)
  • 2020-12-07 09:38

    Here is Clojure pretending to be a more functional language. This is fully tail-recursive, btw, and includes leading zeroes.

    (defn moving-average [period values]
      (loop [[x & xs]  values
             window    []
             ys        []]
    
        (if (and (nil? x) (nil? xs))
          ;; base case
          ys
    
          ;; inductive case
          (if (< (count window) (dec period))
            (recur xs (conj window x) (conj ys 0.0))
            (recur xs
                   (conj (vec (rest window)) x)
                   (conj ys (/ (reduce + x window) period)))))))
    
    (deftest test-moving-average
      (is (= [0.0 0.0 0.0 4.75 5.0 6.0 7.25 8.0 8.25 6.5]
             (moving-average 4 [2.0 4.0 7.0 6.0 3.0 8.0 12.0 9.0 4.0 1.0]))))
    

    Usually I put the collection or list parameter last to make the function easier to curry. But in Clojure...

    (partial moving-average 4)
    

    ... is so cumbersome, I usually end up doing this ...

    #(moving-average 4 %)
    

    ... in which case, it doesn't really matter what order the parameters go.

    0 讨论(0)
  • 2020-12-07 09:41

    Here are 2 more ways to do moving average in Scala 2.8.0(one strict and one lazy). Both assume there are at least p Doubles in vs.

    // strict moving average
    def sma(vs: List[Double], p: Int): List[Double] =
      ((vs.take(p).sum / p :: List.fill(p - 1)(0.0), vs) /: vs.drop(p)) {(a, v) =>
        ((a._1.head - a._2.head / p + v / p) :: a._1, a._2.tail)
      }._1.reverse
    
    // lazy moving average
    def lma(vs: Stream[Double], p: Int): Stream[Double] = {
      def _lma(a: => Double, vs1: Stream[Double], vs2: Stream[Double]): Stream[Double] = {
        val _a = a // caches value of a
        _a #:: _lma(_a - vs2.head / p + vs1.head / p, vs1.tail, vs2.tail)
      }
      Stream.fill(p - 1)(0.0) #::: _lma(vs.take(p).sum / p, vs.drop(p), vs)
    }
    
    scala> sma(List(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0), 4)
    res29: List[Double] = List(0.0, 0.0, 0.0, 4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5)
    
    scala> lma(Stream(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0), 4).take(10).force
    res30: scala.collection.immutable.Stream[Double] = Stream(0.0, 0.0, 0.0, 4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5)
    
    0 讨论(0)
  • 2020-12-07 09:41

    This example makes use of state, since to me it's a pragmatic solution in this case, and a closure to create the windowing averaging function:

    (defn make-averager [#^Integer period]
      (let [buff (atom (vec (repeat period nil)))
            pos (atom 0)]
        (fn [nextval]
          (reset! buff (assoc @buff @pos nextval))
          (reset! pos (mod (+ 1 @pos) period))
          (if (some nil? @buff)
            0
            (/ (reduce + @buff)
               (count @buff))))))
    
    (map (make-averager 4)
         [2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0])
    ;; yields =>
    (0 0 0 4.75 5.0 6.0 7.25 8.0 8.25 6.5)
    

    It is still functional in the sense of making use of first class functions, though it is not side-effect free. The two languages you mentioned both run on top of the JVM and thus both allow for state-management when necessary.

    0 讨论(0)
  • 2020-12-07 09:41

    Being late on the party, and new to functional programming too, I came to this solution with an inner function:

    def slidingAvg (ixs: List [Double], len: Int) = {
        val dxs = ixs.map (_ / len) 
        val start = (0.0 /: dxs.take (len)) (_ + _)
        val head = List.make (len - 1, 0.0)
    
        def addAndSub (sofar: Double, from: Int, to: Int) : List [Double] =  
            if (to >= dxs.length) Nil else {
                val current = sofar - dxs (from) + dxs (to) 
                current :: addAndSub (current, from + 1, to + 1) 
            }
    
        head ::: start :: addAndSub (start, 0, len)
    }
    
    val xs = List(2, 4, 7, 6, 3, 8, 12, 9, 4, 1)
    slidingAvg (xs.map (1.0 * _), 4)
    

    I adopted the idea, to divide the whole list by the period (len) in advance. Then I generate the sum to start with for the len-first-elements. And I generate the first, invalid elements (0.0, 0.0, ...) .

    Then I recursively substract the first and add the last value. In the end I listify the whole thing.

    0 讨论(0)
  • 2020-12-07 09:44

    A short Clojure version that has the advantage of being O(list length) regardless of your period:

    (defn moving-average [list period]
      (let [accums (let [acc (atom 0)] (map #(do (reset! acc (+ @acc %1 ))) (cons 0 list)))
            zeros (repeat (dec period) 0)]
         (concat zeros (map #(/ (- %1 %2) period) (drop period accums) accums))))
    

    This exploits the fact that you can calculate the sum of a range of numbers by creating a cumulative sum of the sequence (e.g. [1 2 3 4 5] -> [0 1 3 6 10 15]) and then subtracting the two numbers with an offset equal to your period.

    0 讨论(0)
提交回复
热议问题