计算列表的移动平均值

Jam*_*s P 56 functional-programming scala 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)和period4,函数应该返回:(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
}
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

现在,这将很容易以一种命令式的方式完成,但我不能为我的生活弄清楚如何在功能上表达.

Dan*_*ral 49

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

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

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

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

假设我们写这个:

values sliding 2 map sum
Run Code Online (Sandbox Code Playgroud)

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

from d1, d2, d3, d4, d5, d6, ...
to (d1+d2), (d2+d3), (d3+d4), (d4+d5), (d5+d6), ...
Run Code Online (Sandbox Code Playgroud)

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

(d1+d2)+(d3+d4), (d2+d3)+(d4+d5), (d3+d4)+(d5+d6), ...
Run Code Online (Sandbox Code Playgroud)

我们可以这样做:

res zip (res drop 2) map Function.tupled(_+_)
Run Code Online (Sandbox Code Playgroud)

然后我们可以计算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)
}
Run Code Online (Sandbox Code Playgroud)

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

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(_+_)
}
Run Code Online (Sandbox Code Playgroud)

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

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

如果是偶数,我们计算movingSumfor n / 2,然后将每个元素添加到n / 2一步之后.

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

def simpleMovingAverage(values: List[Double], period: Int): List[Double] =
  List.fill(period - 1)(0.0) ::: (movingSum(values, period) map (_ / period))
Run Code Online (Sandbox Code Playgroud)

关于使用的效率略有低效:::,但它是O(期间),而不是O(values.size).使用尾递归函数可以提高效率.当然,我提供的"滑动"定义在性能方面是可怕的,但在Scala 2.8上会有更好的定义.请注意,我们无法sliding在a上创建有效的方法List,但我们可以在a 上进行Iterable.

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

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

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

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

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

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

   val subtract = values map (_ / period)
   val add = subtract drop period
Run Code Online (Sandbox Code Playgroud)

我们可以使用添加这两个列表zip.这种方法只会产生与较小列表一样多的元素,从而避免了subtract大于必要的问题:

   val addAndSubtract = add zip subtract map Function.tupled(_ - _)
Run Code Online (Sandbox Code Playgroud)

我们通过折叠组合结果完成:

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

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

 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
 }
Run Code Online (Sandbox Code Playgroud)

  • 丹尼尔太棒了.我也很感谢你对思考过程的解释.对我而言,它更像是一种优雅的函数式编程,而不是找到绝对最有效的方法.你的例子给了我灵感,这是可能的!非常感谢. (2认同)

Jam*_*ham 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))))))
Run Code Online (Sandbox Code Playgroud)

在测试数据上运行此操作会产生

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)
Run Code Online (Sandbox Code Playgroud)

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

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

(defn moving-average [values period]
  (map #(/ (reduce + %) period) (partition period 1 values))
Run Code Online (Sandbox Code Playgroud)

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

(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))))
Run Code Online (Sandbox Code Playgroud)

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

  • 好吧,关于懒惰的好处是(除非你给懒惰的序列一个名字)你不会用完堆栈---之前的值将被清除.(据我所知,至少.) (5认同)

Jon*_*nas 15

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

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

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

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

  • 给分区一个第三个参数(重复0),如果你想包含它们,就可以为末尾提供缺少的参数. (2认同)

Mic*_*zyk 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))))))
Run Code Online (Sandbox Code Playgroud)


Wil*_*ill 9

这是一个部分无点的一行Haskell解决方案:

ma p = reverse . map ((/ (fromIntegral p)) . sum . take p) . (drop p) . reverse . tails
Run Code Online (Sandbox Code Playgroud)

首先,它将尾部应用于列表以获取"尾巴"列表,因此:

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],[]]
Run Code Online (Sandbox Code Playgroud)

反转它并删除第一个'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]]
Run Code Online (Sandbox Code Playgroud)

如果您不熟悉(.)点/乳头符号,它是"功能组合"的操作符,这意味着它将一个函数的输出作为另一个函数的输入传递,将它们"组合"成一个函数.(g.f)表示"在一个值上运行f然后将输出传递给g",因此((f.g)x)与(g(fx))相同.通常它的使用导致更清晰的编程风格.

然后它将函数((/(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]
Run Code Online (Sandbox Code Playgroud)

这一切看起来都比它效率低得多; 在计算列表之前,"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))
Run Code Online (Sandbox Code Playgroud)

首先,我们将列表分成两部分"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])
Run Code Online (Sandbox Code Playgroud)

求和第一位:

Prelude List> sum [2.0, 4.0]
6.0
Run Code Online (Sandbox Code Playgroud)

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

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)]
Run Code Online (Sandbox Code Playgroud)

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

对于那些不熟悉$符号的人来说,它是"应用程序运算符".它没有真正做任何事情,但它有一个"低,右关联的绑定优先",所以它意味着你可以省略括号(注意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].

哦,你需要导入模块"List"来编译任一解决方案.


Wal*_*ang 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)
Run Code Online (Sandbox Code Playgroud)

  • 男孩,这个代码很好!使用元组减少列表同时增加另一个是非常聪明的.但要解释你在做什么,使答案更有用. (2认同)

kal*_*dic 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
Run Code Online (Sandbox Code Playgroud)

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

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

   periods average moving values
4.75 5 6 7.25 8 8.25 6.5
Run Code Online (Sandbox Code Playgroud)

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

让我们进一步了解子程序中发生的事情average.+/表示求和(Σ)并%表示除法(如经典符号÷).计算项目的计数(计数)是通过#.那么整个程序是值之和除以值的总和:+/ % #

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

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


Jon*_*ran 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]))))
Run Code Online (Sandbox Code Playgroud)

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

(partial moving-average 4)
Run Code Online (Sandbox Code Playgroud)

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

#(moving-average 4 %)
Run Code Online (Sandbox Code Playgroud)

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

  • 递归发生在`if`语句中,其中任一选项都基于`recur`.这将首先计算每个参数,然后才递归.答案将是'recur`的结果.结果是递归返回相同的结果,没有其他计算,这是尾递归. (2认同)