计算列表的移动平均值

时间:2009-08-23 23:41:52

标签: scala functional-programming clojure

本周末,我决定尝试一些Scala和Clojure。我精通面向对象的编程,因此Scala很容易学习语言,但想尝试函数式编程。这是它变得艰难的地方。

我似乎无法进入编写函数的模式。作为一名专业的函数式程序员,您如何处理问题?

给定一个值列表和一个定义的求和周期,您将如何生成列表中简单移动平均值的新列表?

例如:给定列表values(2.0,4.0,7.0,6.0,3.0,8.0,12.0,9.0,4.0,1.0)和period 4,该函数应返回: (0.0,0.0,0.0,4.75,5.0,6.0,7.25,8.0,8.25,6.5)

花了一天的时间仔细考虑之后,我在Scala中想到的最好的就是:

def simpleMovingAverage(values: List[Double], period: Int): List[Double] = {
  (for (i <- 1 to values.length)
    yield
    if (i < period) 0.00
    else values.slice(i - period, i).reduceLeft(_ + _) / period).toList
}

我知道这是非常低效的,我宁愿做类似的事情:

where n < period: ma(n) = 0
where n = period: ma(n) = sum(value(1) to value(n)) / period
where n > period: man(n) = ma(n -1) - (value(n-period) / period) + (value(n) / period)

现在这很容易以一种命令式的方式完成,但我不能为我的生活找到如何在功能上表达的。

18 个答案:

答案 0 :(得分:49)

有趣的问题。我可以想到许多解决方案,效率各不相同。必须重复添加内容并不是真正的性能问题,但让我们假设它是。此外,开头的零可以在以后加上,所以我们不用担心它们的产生。如果算法自然地提供它们,很好;如果没有,我们稍后会更正。

从Scala 2.8开始,通过使用n >= period获取List的滑动窗口,以下内容将为sliding提供结果:

def simpleMovingAverage(values: List[Double], period: Int): List[Double] =
  List.fill(period - 1)(0.0) ::: (values sliding period map (_.sum) map (_ / period))

尽管如此,虽然这是相当优雅的,但它没有尽可能好的性能,因为它没有利用已经计算过的加法。所以,谈到它们,我们怎样才能得到它们?

假设我们写下这个:

values sliding 2 map sum

我们列出了每两对的总和。让我们尝试使用此结果来计算4个元素的移动平均值。上述公式进行了以下计算:

from d1, d2, d3, d4, d5, d6, ...
to (d1+d2), (d2+d3), (d3+d4), (d4+d5), (d5+d6), ...

因此,如果我们将每个元素添加到第二个下一个元素,我们得到4个元素的移动平均值:

(d1+d2)+(d3+d4), (d2+d3)+(d4+d5), (d3+d4)+(d5+d6), ...

我们可以这样做:

res zip (res drop 2) map Function.tupled(_+_)

然后我们可以计算8个元素的移动平均值,依此类推。嗯,有一个众所周知的算法来计算遵循这种模式的东西。它以计算数字的力量而闻名。它是这样的:

def power(n: Int, e: Int): Int = e match {
  case 0 => 1
  case 1 => n
  case 2 => n * n
  case odd if odd % 2 == 1 => power(n, (odd - 1)) * n
  case even => power(power(n, even / 2), 2)
}

所以,我们在这里应用它:

def movingSum(values: List[Double], period: Int): List[Double] = period match {
  case 0 => throw new IllegalArgumentException
  case 1 => values
  case 2 => values sliding 2 map (_.sum)
  case odd if odd % 2 == 1 => 
    values zip movingSum(values drop 1, (odd - 1)) map Function.tupled(_+_)
  case even =>
    val half = even / 2
    val partialResult = movingSum(values, half)
    partialResult zip (partialResult drop half) map Function.tupled(_+_)
}

所以,这是逻辑。周期0无效,周期1等于输入,周期2是大小为2的滑动窗口。如果大于,则可以是偶数或奇数。

如果奇怪,我们将每个元素添加到下一个movingSum元素的(odd - 1)。例如,如果为3,我们将每个元素添加到接下来的2个元素的movingSum中。

如果是偶数,我们会计算movingSum的{​​{1}},然后将每个元素添加到n / 2个步骤中。

根据该定义,我们可以回到问题并执行此操作:

n / 2

使用def simpleMovingAverage(values: List[Double], period: Int): List[Double] = List.fill(period - 1)(0.0) ::: (movingSum(values, period) map (_ / period)) 时效率略低,但它是O(句点),而不是O(values.size)。使用尾递归函数可以提高效率。当然,我提供的“滑动”定义在性能方面是可怕的,但在Scala 2.8上会有更好的定义。请注意,我们无法在:::上制作有效的sliding方法,但我们可以在List上进行此操作。

说了这么多之后,我会选择第一个定义,并且只有在关键路径分析指出这是一个大问题时才进行优化。

总而言之,让我们考虑一下我是如何解决这个问题的。我们有一个均线问题。移动平均值是列表上移动的“窗口”的总和除以该窗口的大小。所以,首先,我尝试获得一个滑动窗口,对其上的所有内容求和,然后除以大小。

下一个问题是避免重复已计算的加法。在这种情况下,我尽可能地进行了最小的添加,并试图找出如何计算更大的总和来重复使用这些结果。

最后,让我们尝试通过添加和减去前一个结果来解决问题。获得第一次平均值很简单:

Iterable

现在我们制作两个清单。首先,要减去的元素列表。接下来,要添加的元素列表:

 def movingAverage(values: List[Double], period: Int): List[Double] = {
   val first = (values take period).sum / period

我们可以使用 val subtract = values map (_ / period) val add = subtract drop period 添加这两个列表。这个方法只产生与较小列表一样多的元素,这避免了zip大于必要的问题:

subtract

我们通过用折叠构成结果来完成:

   val addAndSubtract = add zip subtract map Function.tupled(_ - _)

这是要回复的答案。整个函数看起来像这样:

   val res = (addAndSubtract.foldLeft(first :: List.fill(period - 1)(0.0)) { 
     (acc, add) => (add + acc.head) :: acc 
   }).reverse

答案 1 :(得分:29)

我知道Clojure比Scala更好,所以这里有。在我写这篇文章时,其他Clojure条目势在必行;这不是你真正想要的(并不是惯用的Clojure)。我想到的第一个算法是重复从序列中获取所需数量的元素,删除第一个元素,然后重复出现。

以下适用于任何类型的序列(向量或列表,惰性或非懒惰),并给出一个懒惰的平均序列 - 如果您正在处理无限大小的列表,这可能会有所帮助。请注意,如果列表中没有足够的元素可供使用,它会通过隐式返回nil来处理基本情况。

(defn moving-average [values period]
  (let [first (take period values)]
    (if (= (count first) period)
      (lazy-seq 
        (cons (/ (reduce + first) period)
              (moving-average (rest values) period))))))

在测试数据上运行此产生

user> (moving-average '(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0) 4)
(4.75 5.0 6.0 7.25 8.0 8.25 6.5)

对于序列中的前几个元素,它不会给出“0”,尽管这可以很容易地处理(有点人为)。

最简单的事情就是看模式,并能够想到一个适合账单的可用功能。 partition给出了序列部分的惰性视图,然后我们可以将其映射到:

(defn moving-average [values period]
  (map #(/ (reduce + %) period) (partition period 1 values))

有人要求尾递归版;尾递归与懒惰是一种权衡。当你的工作正在建立一个列表然后使你的函数尾递归通常非常简单,这也不例外 - 只需将列表构建为子函数的参数。我们将积累到向量而不是列表,否则列表将向后构建,并且最后需要反转。

(defn moving-average [values period]
  (loop [values values, period period, acc []]
    (let [first (take period values)]
      (if (= (count first) period)
        (recur (rest values) period (conj acc (/ (reduce + first) period)))
        acc))))

loop是一种创建匿名内部函数的方法(类似于Scheme的名为let);必须在Clojure中使用recur来消除尾调用。 conj是一个广义的cons,以收集的自然方式附加 - 列表的开头和向量的结尾。

答案 2 :(得分:15)

这是另一个(功能性)Clojure解决方案:

(defn avarage [coll]
  (/ (reduce + coll)
     (count coll)))

(defn ma [period coll]
  (map avarage (partition period 1 coll)))

如果需要,仍必须添加序列开头的零。

答案 3 :(得分:13)

这是Clojure中的纯功能解决方案。比那些已经提供的更复杂,但懒惰只调整每一步的平均值,而不是从头开始重新计算。它实际上比一个简单的解决方案慢,如果周期很小,它会计算每一步的新平均值;然而,对于较大的时期,它几乎没有经历减速,而(/ (take period ...) period)的某些事情会在较长时期内表现更差。

(defn moving-average
  "Calculates the moving average of values with the given period.
  Returns a lazy seq, works with infinite input sequences.
  Does not include initial zeros in the output."
  [period values]
  (let [gen (fn gen [last-sum values-old values-new]
              (if (empty? values-new)
                nil
                (let [num-out (first values-old)
                      num-in  (first values-new)
                      new-sum (+ last-sum (- num-out) num-in)]
                  (lazy-seq
                    (cons new-sum
                          (gen new-sum
                               (next values-old)
                               (next values-new)))))))]
    (if (< (count (take period values)) period)
      nil
      (map #(/ % period)
           (gen (apply + (take (dec period) values))
                (cons 0 values)
                (drop (dec period) values))))))

答案 4 :(得分:9)

这是一个部分point-free一行Haskell解决方案:

ma p = reverse . map ((/ (fromIntegral p)) . sum . take p) . (drop p) . reverse . tails

首先,它将tails应用于列表以获取“tails”列表,因此:

Prelude List> tails [2.0, 4.0, 7.0, 6.0, 3.0]
[[2.0,4.0,7.0,6.0,3.0],[4.0,7.0,6.0,3.0],[7.0,6.0,3.0],[6.0,3.0],[3.0],[]]

反转它并删除第一个'p'条目(在此处将p作为2):

Prelude List> (drop 2 . reverse . tails) [2.0, 4.0, 7.0, 6.0, 3.0]
[[6.0,3.0],[7.0,6.0,3.0],[4.0,7.0,6.0,3.0],[2.0,4.0,7.0,6.0,3.0]]

如果你不熟悉(.) dot/nipple符号,它就是'功能组合'的操作符,这意味着它将一个函数的输出作为另一个函数的输入传递,将它们“组合”成一个功能。 (g。f)表示“在一个值上运行f然后将输出传递给g”,因此((f.g)x)与(g(f x))相同。一般来说,它的使用会带来更清晰的编程风格。

然后将函数((/(fromIntegral p))。sum。取p)映射到列表中。因此,对于列表中的每个列表,它采用第一个'p'元素,对它们求和,然后将它们除以'p'。然后我们用“反向”再次翻转列表。

Prelude List> map ((/ (fromIntegral 2)) . sum . take 2) [[6.0,3.0],[7.0,6.0,3.0]
,[4.0,7.0,6.0,3.0],[2.0,4.0,7.0,6.0,3.0]]
[4.5,6.5,5.5,3.0]

这一切看起来效率都低于它;在对列表进行评估之前,“reverse”不会在物理上颠倒列表的顺序,它只是将它放到堆栈上(好的'懒惰的Haskell)。 “tails”也不会创建所有这些单独的列表,它只是引用原始列表的不同部分。它仍然不是一个很好的解决方案,但它只有一行:)

这是一个稍好但更长的解决方案,它使用mapAccum进行滑动减法和添加:

ma p l = snd $ mapAccumL ma' a l'
    where
        (h, t) = splitAt p l
        a = sum h
        l' = (0, 0) : (zip l t)
        ma' s (x, y) = let s' = (s - x) + y in (s', s' / (fromIntegral p))

首先,我们将列表分成两部分“p”,所以:

Prelude List> splitAt 2 [2.0, 4.0, 7.0, 6.0, 3.0]
([2.0,4.0],[7.0,6.0,3.0])

总结第一位:

Prelude List> sum [2.0, 4.0]
6.0

使用原始列表压缩第二位(这只是从两个列表中按顺序配对项目)。原始列表显然更长,但我们失去了这个额外的位:

Prelude List> zip [2.0, 4.0, 7.0, 6.0, 3.0] [7.0,6.0,3.0]
[(2.0,7.0),(4.0,6.0),(7.0,3.0)]

现在我们为mapAccum(ulator)定义一个函数。 mapAccumL与“map”相同,但有一个额外的运行状态/累加器参数,当地图在列表中运行时,该参数从前一个“映射”传递到下一个。我们使用累加器作为移动平均值,并且由于我们的列表由刚刚离开滑动窗口的元素和刚刚输入的元素(我们刚刚压缩的列表)组成,我们的滑动函数采用第一个数字'x'远离平均值并添加第二个数字'y'。然后我们传递新的's'并返回's'除以'p'。 “snd”(第二个)只取一对(元组)的第二个成员,用于获取mapAccumL的第二个返回值,因为mapAccumL将返回累加器以及映射列表。

对于那些不熟悉$ symbol的人,它是“应用程序运营商”。它没有真正做任何事情,但它有一个“低,右关联绑定优先”,所以它意味着你可以省略括号(注意LISPers),即(fx)与f $ x相同

对于任一解决方案,运行(ma 4 [2.0,4.0,7.0,6.0,3.0,8.0,12.0,9.0,4.0,1.0])得到[4.75,5.0,6.0,7.25,8.0,8.25,6.5]。< / p>

哦,你需要导入模块“List”来编译任何一个解决方案。

答案 5 :(得分:7)

以下是Scala 2.8.0中另外两种移动平均值的方法(一种是严格的,一种是懒惰的)。两者都假设 vs 中至少有 p 双打。

// 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)

答案 6 :(得分:6)

J编程语言有助于移动平均等程序。实际上,(+/ % #)\中的字符数少于其标签中的字符数,即“移动平均线”。

对于此问题中指定的值(包括名称'values'),这是一种直接的编码方式:

   values=: 2 4 7 6 3 8 12 9 4 1
   4 (+/ % #)\ values
4.75 5 6 7.25 8 8.25 6.5

我们可以通过使用组件标签来描述这一点。

   periods=: 4
   average=: +/ % #
   moving=: \

   periods average moving values
4.75 5 6 7.25 8 8.25 6.5

两个示例都使用完全相同的程序。唯一的区别是在第二种形式中使用更多名称。这些名字可以帮助那些不了解J初选的读者。

让我们进一步了解子程序average中发生的事情。 +/表示求和(Σ),%表示除法(如经典符号÷)。计算项目的计数(计数)由#完成。然后,整个程序是值的总和除以值的计数:+/ % #

此处写入的移动平均值计算的结果不包括原始问题中预期的前导零。这些零可以说不是预期计算的一部分。

这里使用的技术叫做默会编程。它与函数式编程的无点式风格几乎相同。

答案 7 :(得分:5)

这是Clojure假装是一种更具功能性的语言。这是完全尾递归的,顺便说一句,包括前导零。

(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]))))

通常我会将collection或list参数放在最后,以使函数更容易理解。但是在Clojure ......

(partial moving-average 4)

......太麻烦了,我通常最终会这样做......

#(moving-average 4 %)

......在这种情况下,参数的顺序并不重要。

答案 8 :(得分:3)

这是一个clojure版本:

由于lazy-seq,它是完全通用的,不会打击堆栈

(defn partialsums [start lst]
  (lazy-seq
    (if-let [lst (seq lst)] 
          (cons start (partialsums (+ start (first lst)) (rest lst)))
          (list start))))

(defn sliding-window-moving-average [window lst]
  (map #(/ % window)
       (let [start   (apply + (take window lst))
             diffseq (map   - (drop window lst) lst)]
         (partialsums start diffseq))))

;;为了帮助了解它正在做什么:

(sliding-window-moving-average 5 '(1 2 3 4 5 6 7 8 9 10 11))

start = (+ 1 2 3 4 5) = 15

diffseq = - (6 7 8 9 10 11)
            (1 2 3 4  5  6 7 8 9 10 11)

        =   (5 5 5 5  5  5)

(partialsums 15 '(5 5 5 5 5 5) ) = (15 20 25 30 35 40 45)

(map #(/ % 5) (20 25 30 35 40 45)) = (3 4 5 6 7 8 9)

;;实施例

(take 20 (sliding-window-moving-average 5 (iterate inc 0)))

答案 9 :(得分:2)

这个例子使用了state,因为在这种情况下它是一个实用的解决方案,并且是一个用于创建窗口平均函数的闭包:

(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)

在使用第一类功能的意义上它仍然是有用的,尽管它不是无副作用的。您提到的两种语言都运行在JVM之上,因此在必要时都允许进行状态管理。

答案 10 :(得分:2)

这个解决方案在Haskell中,对我来说比较熟悉:

slidingSums :: Num t => Int -> [t] -> [t]
slidingSums n list = case (splitAt (n - 1) list) of
                      (window, []) -> [] -- list contains less than n elements
                      (window, rest) -> slidingSums' list rest (sum window)
  where
    slidingSums' _ [] _ = []
    slidingSums' (hl : tl) (hr : tr) sumLastNm1 = sumLastN : slidingSums' tl tr (sumLastN - hl)
      where sumLastN = sumLastNm1 + hr

movingAverage :: Fractional t => Int -> [t] -> [t]
movingAverage n list = map (/ (fromIntegral n)) (slidingSums n list)

paddedMovingAverage :: Fractional t => Int -> [t] -> [t]
paddedMovingAverage n list = replicate (n - 1) 0 ++ movingAverage n list

Scala翻译:

def slidingSums1(list: List[Double], rest: List[Double], n: Int, sumLastNm1: Double): List[Double] = rest match {
    case Nil => Nil
    case hr :: tr => {
        val sumLastN = sumLastNm1 + hr
        sumLastN :: slidingSums1(list.tail, tr, n, sumLastN - list.head)
    }
}

def slidingSums(list: List[Double], n: Int): List[Double] = list.splitAt(n - 1) match {
    case (_, Nil) => Nil
    case (firstNm1, rest) => slidingSums1(list, rest, n, firstNm1.reduceLeft(_ + _))
}

def movingAverage(list: List[Double], n: Int): List[Double] = slidingSums(list, n).map(_ / n)

def paddedMovingAverage(list: List[Double], n: Int): List[Double] = List.make(n - 1, 0.0) ++ movingAverage(list, n)

答案 11 :(得分:2)

一个简短的Clojure版本,无论您的期间如何,都具有O(列表长度)的优势:

(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))))

这利用了这样一个事实:您可以通过创建序列的累积和(例如[1 2 3 4 5] - > [0 1 3 6 10 15])然后减去来计算一系列数字的总和两个数字的偏移量等于你的期间。

答案 12 :(得分:1)

我知道如何在python中做到这一点(注意:没有返回值为0.0的前3个元素,因为这实际上不是表示移动平均线的合适方式)。我想在Scala中可以使用类似的技术。这有多种方法可以做到。

data = (2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0)
terms = 4
expected = (4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5)

# Method 1 : Simple. Uses slices
assert expected == \
    tuple((sum(data[i:i+terms])/terms for i in range(len(data)-terms+1)))

# Method 2 : Tracks slots each of terms elements
# Note: slot, and block mean the same thing.
# Block is the internal tracking deque, slot is the final output
from collections import deque
def slots(data, terms):
    block = deque()
    for datum in data :
        block.append(datum)
        if len(block) > terms : block.popleft()
        if len(block) == terms :
            yield block

assert expected == \
    tuple(sum(slot)/terms for slot in slots(data, terms))

# Method 3 : Reads value one at a time, computes the sums and throws away read values
def moving_average((avgs, sums),val):
    sums = tuple((sum + val) for sum in sums)
    return (avgs + ((sums[0] / terms),), sums[1:] + (val,))

assert expected == reduce(
    moving_average,
    tuple(data[terms-1:]),
    ((),tuple(sum(data[i:terms-1]) for i in range(terms-1))))[0]

# Method 4 : Semantically same as method 3, intentionally obfuscates just to fit in a lambda
assert expected == \
    reduce(
        lambda (avgs, sums),val: tuple((avgs + ((nsum[0] / terms),), nsum[1:] + (val,)) \
                                for nsum in (tuple((sum + val) for sum in sums),))[0], \
           tuple(data[terms-1:]),
           ((),tuple(sum(data[i:terms-1]) for i in range(terms-1))))[0]

答案 13 :(得分:1)

看起来您正在寻找递归解决方案。在这种情况下,我建议稍微改变问题,并寻求获得(4.75,5.0,6.0,7.25,8.0,8.25,6.0,0.0,0.0)作为解决方案。

在这种情况下,您可以在Scala中编写以下优雅的递归解决方案:

def mavg(values: List[Double], period: Int): List[Double] = {
  if (values.size < period) List.fill(values.size)(0.0) else
    if (values.size == period) (values.sum / values.size) :: List.fill(period - 1)(0.0) else {
      val rest: List[Double] = mavg(values.tail, period)
      (rest.head + ((values.head - values(period))/period)):: rest
  }
}

答案 14 :(得分:0)

在聚会上迟到,也是功能性编程的新手,我带着内在的功能来到这个解决方案:

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)

我采纳了这个想法,提前将整个清单划分为期间(len)。 然后我为len-first-elements生成总和。 我生成了第一个无效元素(0.0,0.0,...)。

然后我递归地减去第一个并添加最后一个值。 最后,我整理了整个事情。

答案 15 :(得分:0)

在Haskell伪代码中:

group4 (a:b:c:d:xs) = [a,b,c,d] : group4 (b:c:d:xs)
group4 _ = []

avg4 xs = sum xs / 4

running4avg nums = (map avg4 (group4 nums))

或pointfree

runnig4avg = map avg4 . group4

(现在真的应该抽出4个......)

答案 16 :(得分:0)

使用Haskell:

movingAverage :: Int -> [Double] -> [Double]
movingAverage n xs = catMaybes . (fmap avg . take n) . tails $ xs
  where avg list = case (length list == n) -> Just . (/ (fromIntegral n)) . (foldl (+) 0) $ list
                        _                  -> Nothing

关键是tails函数,它将列表映射到原始列表的副本列表,其属性是结果的第n个元素缺少前n-1个元素。

所以

[1,2,3,4,5] -> [[1,2,3,4,5], [2,3,4,5], [3,4,5], [4,5], [5], []]

我们将fmap(avg.取n)应用于结果,这意味着我们从子列表中获取n长度前缀,并计算其平均值。如果我们平均的列表长度不是n,那么我们不计算平均值(因为它是未定义的)。在那种情况下,我们返回Nothing。如果是,我们这样做,并将其包装在“Just”中。最后,我们对fmap(avg.take n)的结果运行“catMaybes”,以摆脱Maybe类型。

答案 17 :(得分:-1)

对于我认为最具惯用性的Clojure解决方案@JamesCunningham lazy-seq solutions的表现,我感到很惊讶和失望。

(def integers (iterate inc 0))
(def coll (take 10000 integers))
(def n 1000)
(time (doall (moving-average-james-1 coll n)))
# "Elapsed time: 3022.862 msecs"
(time (doall (moving-average-james-2 coll n)))
# "Elapsed time: 3433.988 msecs"

所以这是James&#39;的组合。 @ DanielC.Sobral idea使fast-exponentiation适应移动总和的解决方案:

(defn moving-average
  [coll n]
  (letfn [(moving-sum [coll n]
            (lazy-seq
              (cond
                (= n 1)  coll
                (= n 2)  (map + coll (rest coll))
                (odd? n) (map + coll (moving-sum (rest coll) (dec n)))
                :else    (let [half (quot n 2)
                               hcol (moving-sum coll half)]
                           (map + hcol (drop half hcol))))))]
    (cond
      (< n 1) nil
      (= n 1) coll
      :else   (map #(/ % n) (moving-sum coll n)))))


(time (doall (moving-average coll n)))
# "Elapsed time: 42.034 msecs"

编辑:这个基于@mikera&#39; s solution - 更快。

(defn moving-average
  [coll n]
  (cond
    (< n 1) nil
    (= n 1) coll
    :else   (let [sums (reductions + 0 coll)]
              (map #(/ (- %1 %2) n) (drop n sums) sums))))

(time (doall (moving-average coll n)))
# "Elapsed time: 9.184 msecs"