计算列表的移动平均数

这个周末我决定尝试一下Scala和Clojure。 我精通面向对象的编程,所以Scala很容易被用作语言,但是想要尝试函数式编程。 这是困难的地方。

我似乎无法将自己的头脑变成一种写作function的模式。 作为一个专业的function程序员,你如何解决一个问题?

给定一个值列表和一个确定的求和时间段,你将如何生成一个新的列表中的简单移动平均值?

例如:给定列表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) 

现在很容易做到这一点,但我不能为了我的生活而弄清楚如何在function上expression这一点。

有趣的问题。 我可以想到很多解决scheme,效率不一。 不得不重复添加东西不是真正的性能问题,但我们假设它是。 另外,开头的零可以在后面加上,所以我们不用担心产生它们。 如果algorithm自然而然地提供它们, 如果没有,我们稍后再纠正。

从Scala 2.8开始,通过使用sliding获得List的滑动窗口,以下将给出n >= period的结果:

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

尽pipe如此,虽然这是相当优雅的,但它并没有达到最好的性能,因为它没有利用已经计算出来的附加值。 那么,谈到他们,我们怎么能得到他们呢?

比方说,我们写这个:

 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个元素的移动平均值,依此类推。 那么,有一个众所周知的algorithm来计算遵循这种模式的东西。 它最着名的用途是计算一个数字的强度。 它是这样的:

 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等于input,周期2是大小为2的滑动窗口。如果大于该值,则可能是偶数或奇数。

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

如果偶数,我们计算n / 2movingSum ,然后将每个元素添加到之后的一个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)。 它可以通过尾recursion函数更高效。 当然,我提供的“滑动”的定义是非常糟糕的性能,但是在Scala 2.8上将会有更好的定义。 请注意,我们不能在List上创build有效的sliding方法,但是我们可以在Iterable上进行。

说了这么多,我会按照第一个定义去做,只有在关键path分析指出这是一个大问题的时候,才能进行优化。

总之,让我们考虑一下我是如何处理这个问题的。 我们有一个移动平均的问题。 移动平均值是列表中正在移动的“窗口”的总和,除以该窗口的大小。 所以,首先,我试着弄一个滑动窗口,把所有的东西都加上,然后除以大小。

接下来的问题是为了避免重复已经计算出的加法。 在这种情况下,我尽可能减less了最小化,试图找出如何计算重复使用这些结果的更大的总和。

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

  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 

这是要返回的答案。 整个function如下所示:

  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 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 res } 

我比Scala更了解Clojure,所以在这里。 在我写这篇文章的时候,其他的Clojure条目是必不可less的。 这不是真的你以后(而且不是惯用的Clojure)。 我想到的第一个algorithm是重复从序列中请求数量的元素,放弃第一个元素,并重复。

以下方法适用于任何types的序列(向量或列表,懒惰或不),并给出了一个懒惰的平均序列—如果你正在处理一个无限大小的列表,这可能会有帮助。 请注意,如果列表中没有足够的元素要消耗,则通过隐式返回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)))))) 

在你的testing数据收益上运行这个

 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”,尽pipe这很容易处理(有些人为地)。

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

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

有人要求一个尾recursion版本; 尾recursion与懒惰是有点权衡的。 当你的工作是build立一个列表,然后让你的函数尾recursion通常是非常简单的,这也不例外 – 只是build立列表作为一个子function的参数。 我们将积累到一个向量,而不是一个列表,因为否则列表将被build立倒退,并将需要在最后颠倒。

 (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是一种创build匿名内部函数的方法(有点像Scheme的名为let); 必须在Clojure中使用repeat,以消除尾部调用。 conj是一个广义的cons ,以自然的方式追加—列表的开始和向量的结束。

这是另一个(function)Clojure解决scheme:

 (defave avarage [coll]
   (/(减less+ coll)
      (count coll)))

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

如果这是一个要求,则序列开始处的零必须仍然被添加。

这是Clojure的一个纯function解决scheme。 比已经提供的更复杂,但是它很懒只能调整每一步的平均值,而不是从头开始重新计算 。 它实际上比一个简单的解决scheme要慢,如果周期很短的话,它会在每一步计算一个新的平均值; 然而,在较长时期内,它几乎没有经历减速,而(/ (take period ...) period)某些行为在较长时期内performance更差。

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

这是一个部分无点的 Haskell解决scheme:

 ma p = reverse . map ((/ (fromIntegral p)) . sum . take p) . (drop p) . reverse . 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”条目(在这里取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]] 

如果您不熟悉(。)点/乳头符号,则它是“function组合”的操作符,意思是它将一个function的输出作为另一个function的input,“组合”成一个function。 (g。f)表示“在某个值上运行f,然后将输出传递给g”,所以((f。g)x)与(g(fx))相同。 通常它的使用导致更清晰的编程风格。

然后将函数((/(fromIntegral p))sum。take 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] 

这一切看起来比效率低得多, “反向”在物理上不会逆转列表的顺序,直到列表被评估,它只是把它放在栈上(好的“懒惰的Haskell”)。 “尾巴”也不会创build所有这些单独的列表,它只是引用原始列表的不同部分。 这仍然不是一个很好的解决scheme,但它只有一行:)

这里有一个稍微好一些,但更长的解决scheme,使用mapAccum做一个滑动减法和加法:

 ma pl = snd $ mapAccumL ma' al' where (h, t) = splitAt pl a = sum h l' = (0, 0) : (zip lt) 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”是一样的,但是有一个额外的运行状态/累加器参数,当映射在列表中运行时,会从前一个“映射”传递到下一个。 我们使用累加器作为移动平均值,由于我们的列表是由刚刚离开滑动窗口的元素和刚刚input的元素(我们刚刚压缩的列表)组成的,因此我们的滑动函数将第一个数字“x”远离平均值并加上第二个数字“y”。 然后,我们把新的'一起'并返回'除以'p'。 “snd”(第二个)只需要一对(tuple)的第二个成员,用于获取mapAccumL的第二个返回值,因为mapAccumL将返回累加器以及映射列表。

对于那些不熟悉$符号的人来说 ,这是“应用程序运营商”。 它并没有真正的做什么,但它有一个“低右联合绑定优先级”,所以这意味着你可以省略括号(注意LISPers),即(fx)和f $ x相同

对于任一解决scheme,运行(ma4 [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]。

哦,你需要导入模块“列表”来编译解决scheme。

这里有两种方法可以在Scala 2.8.0(一个严格的,一个懒惰的)中进行移动平均。 两人都假设至less有p双打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) 

J编程语言有助于移动平均等程序。 事实上, (+/ % #)\中的字符数比标签“移动平均数”要less。

对于在这个问题中指定的值(包括名称的值),这里是一个直接的代码:

  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 

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

让我们来看一下子程序中average发生了什么。 +/表示总和(Σ), %表示除法(如经典符号÷)。 计算项目的计数(计数)由#完成。 那么整体计划就是价值除以价值计数的总和: +/ % #

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

这里使用的技术被称为默认编程。 这与函数式编程的无点式风格几乎相同。

这里是Clojure假装是一个更function的语言。 这是完全的尾recursion,顺便说一句,并包括前导零。

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

通常我把collections或列表参数放在最后,以使该function更容易咖喱。 但在Clojure …

 (partial moving-average 4) 

…太麻烦了,我通常会做这个…

 #(moving-average 4 %) 

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

这是一个clojure版本:

由于懒惰,这是完全一般的,不会打击堆栈

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

这个例子使用状态,因为在这种情况下,这是一个实用的解决scheme,并且创build窗口平均函数的闭包:

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

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

这个解决scheme在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 

斯卡拉翻译:

 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) 

一个简短的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)))) 

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

我知道我将如何在Python中做到这一点(注意:值为0.0的前3个元素不会被返回,因为这实际上不是表示移动平均值的适当方式)。 我会想象类似的技术将是可行的在斯卡拉。 这里有多种方式来做到这一点。

 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] 

看起来你正在寻找一个recursion的解决scheme。 在这种情况下,我build议稍微改变这个问题,并且希望得到(4.75,5.0,6.0,7.25,8.0,8.25,6.5,0.0,0.0,0.0)作为解决scheme。

在这种情况下,您可以在Scala中编写下面的优雅的recursion解决scheme:

 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 } } 

在晚会上,也是function编程的新手,我带着内在的function来到这个解决scheme:

 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,…)。

然后我recursion减去第一个并添加最后一个值。 最后,我把整件事情说清楚。

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

或无点

 runnig4avg = map avg4 . group4 

(现在应该把这4个抽象出来……)

使用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 

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

所以

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

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

对于我看来最为惯用的Clojure解决scheme,@JamesCunningham的lazy-seq解决scheme,我感到吃惊和失望。

 (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的解决scheme与DanielC.Sobral的快速取幂适应运动总和的结合:

 (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的解决scheme – 更快。

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