【问题标题】:Calculating the Moving Average of a List计算列表的移动平均线
【发布时间】:2009-08-23 23:41:52
【问题描述】:

这个周末我决定尝试一些 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)

现在用命令式风格很容易做到这一点,但我终生无法弄清楚如何用函数式表达它。

【问题讨论】:

    标签: scala functional-programming clojure


    【解决方案1】:

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

    从 Scala 2.8 开始,以下将通过使用 sliding 来获得 n &gt;= period 的结果以获取列表的滑动窗口:

    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 的滑动窗口。如果大于,它可能是偶数或奇数。

    如果奇数,我们将每个元素添加到下一个 (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(period),而不是 O(values.size)。使用尾递归函数可以提高效率。当然,我提供的“滑动”定义在性能方面是可怕的,但在 Scala 2.8 上会有更好的定义。请注意,我们无法在 List 上创建有效的 sliding 方法,但我们可以在 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
    

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

     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
     }
    

    【讨论】:

    • 丹尼尔,太棒了。我也感谢您对思维过程的解释。对我来说,这更像是一种优雅的函数式编程练习,而不是找到绝对最有效的方法。你的例子给了我灵感,这是可能的!非常感谢。
    • 从现在开始,我称您为 Sobral 教授。这将成为一个很好的讲座主题,尤其是在您展示的漂亮的逐步转换中。做得很好!
    • 对于 Scala 2.9.1 simpleMovingAverage 变为: def simpleMovingAverage(values: List[Double], period: Int): List[Double] = List.make(period - 1, 0.0) ++ (values滑动周期图 (_ sum) 图 (_ / period))。 List.make(period - 1, 0.0) 返回一个 List[Double] 和 (values slip period map (_ sum) map (_ / period) 返回一个 Iterator[Double。++ 用于连接 List 和 Iterator .
    • @Brian 你会丢失所有先前计算的值并一直计算period 元素的总和。我认为你没有抓住重点。
    • @Ivan 是的,这个答案是为 Scala 2.7 编写的。我现在已经为现代 Scala 版本重写了它。可能还有一些问题,但至少最终代码应该可以正常工作。
    【解决方案2】:

    我比 Scala 更了解 Clojure,所以就这样吧。在我写这篇文章时,这里的另一个 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,以集合的自然方式附加——列表的开头和向量的结尾。

    【讨论】:

    • +1 表示递归解决方案;现在让它尾递归;-)
    • 好吧,惰性的好处是(除非你给惰性序列一个名字)你不会用完堆栈——前面的值会被清理掉。 (至少据我所知。)
    • 谢谢詹姆斯,这正是我正在寻找的东西。简单、优雅且易于阅读。
    • @James Cunningham:Clojure 默认是惰性的吗?我不知道。很高兴知道。
    • @Daniel:默认不是懒惰的;这就是为什么我必须在惰性序列宏中包装对 cons 的调用。然而,大多数对序列进行操作的函数都是惰性的。
    【解决方案3】:

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

    (定义平均 [coll] (/ (减少 + coll) (科尔伯爵))) (defn ma [句号 coll] (地图平均(分区周期 1 coll)))

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

    【讨论】:

    • 给分区第三个参数 (repeat 0) 以提供缺少的参数到最后,如果你想包含它们。
    • 要在开头得到零,你可以像这样连接它们:(defn ma [period coll] (lazy-cat (repeat period 0) (map avarage (partition period 1 (repeat 0) coll))))
    【解决方案4】:

    这是 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))))))
    

    【讨论】:

    • 我决定添加到这个旧 Q 中,因为这个话题又出现了 (stackoverflow.com/questions/2359821/…),我发现在添加我自己的观点的同时指出这个可能的解决方案的集合更可取 (这与 Clojure 中的先前版本不同,如 A) 中所述。也许我们可以构建网络上最完整的函数式 mov-avg 实现库! ;-)
    【解决方案5】:

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

    ma p = reverse . map ((/ (fromIntegral p)) . sum . take p) . (drop p) . reverse . 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 . take p) 映射到列表上。因此,对于列表中的每个列表,它采用第一个“p”元素,对它们求和,然后将它们除以“p”。然后我们只需使用“reverse”再次将列表翻转回来。

    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”不会在物理上颠倒列表的顺序,它只是将其放在堆栈上(好 ol'lazy 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" 相同,但有一个额外的运行状态/累加器参数,当 map 遍历列表时,它会从前一个 "mapping" 传递到下一个 "mapping"。我们使用累加器作为移动平均线,并且由于我们的列表由刚刚离开滑动窗口的元素和刚刚进入的元素(我们刚刚压缩的列表)组成,我们的滑动函数采用第一个数字“x”远离平均值并添加第二个数字“y”。然后我们传递新的's'并返回's'除以'p'。 "snd" (second) 只取一对(元组)的第二个成员,用于取 mapAccumL 的第二个返回值,因为 mapAccumL 将返回累加器以及映射列表。

    对于那些不熟悉$ symbol 的人来说,它是“应用程序运算符”。它实际上并没有做任何事情,但它具有“低右关联绑定优先级”,因此这意味着您可以省略括号(注意 LISPers),即 (f x) 与 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”来编译任一解决方案。

    【讨论】:

      【解决方案6】:

      这里还有 2 种在 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)
      

      【讨论】:

      • 这一次自动格式化真的把代码搞砸了。对于任何阅读它的人,“#”不是评论标记,而是运算符“#::”和“#:::”的一部分,它们是“::”和“:::”的流等效项.
      • 小子,这段代码不错!在增加另一个列表的同时使用一个元组减少一个列表是非常聪明的。但是请解释一下您在做什么,以使答案更有用。
      • @Daniel 谢谢!编写代码比解释它容易得多;-) 你已经描述了它的要点。在这两个函数中都维护了两个列表/流,并在每次迭代期间将它们的“头”取下来。一个 List/Stream 用作迭代的主集合,而另一个 List/Stream 是相同的集合,只是去掉了“周期”减去 Doubles,用于计算新的移动平均线。
      【解决方案7】:

      J 编程语言促进了诸如移动平均线之类的程序。实际上,(+/ % #)\ 中的字符比其标签“移动平均线”中的字符少。

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

         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 中发生了什么。 +/ 表示求和 (Σ),% 表示除法(如经典符号 ÷)。计算项目的计数(计数)由 # 完成。因此,整个程序是值的总和除以值的总数:+/ % #

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

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

      【讨论】:

        【解决方案8】:

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

        通常我将集合或列表参数放在最后,以使函数更容易柯里化。但是在 Clojure 中...

        (partial moving-average 4)
        

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

        #(moving-average 4 %)
        

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

        【讨论】:

        • 嗨乔纳森,我对这个函数式编程很陌生,你能解释一下这是如何尾递归的吗?谢谢
        • 递归发生在if 语句上,其中任一选项都基于recur。这将首先计算每个参数,然后才递归。答案将是recur 的结果。由于结果与递归返回的结果相同,没有其他计算,这是尾递归。
        • 正如丹尼尔所说,在对每个 recur 的调用返回后,就没有什么可做的了。不再需要“栈帧”,loop 变量可以重新绑定。 recur 是 Clojure 中的一个特殊构造;编译器实际上会检查它是否处于尾部位置。
        【解决方案9】:

        这是一个 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)))
        

        【讨论】:

          【解决方案10】:

          这个例子使用了状态,因为对我来说这是一个实用的解决方案,并且是一个创建窗口平均函数的闭包:

          (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 之上,因此都允许在必要时进行状态管理。

          【讨论】:

            【解决方案11】:

            这个解决方案在我比较熟悉的 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)
            

            【讨论】:

            • 我喜欢使用 match 语句。我尝试做类似的事情,但无法做到。
            【解决方案12】:

            一个简短的 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])然后减去两个数字,偏移量等于您的周期。

            【讨论】:

              【解决方案13】:

              看起来您正在寻找递归解决方案。在这种情况下,我建议稍微改变问题并以 (4.75, 5.0, 6.0, 7.25, 8.0, 8.25, 6.5, 0.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】:

                我知道如何在 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]
                

                【讨论】:

                  【解决方案15】:

                  聚会迟到了,而且对函数式编程也很陌生,我用一个内部函数来解决这个问题:

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

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

                  【讨论】:

                    【解决方案16】:

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

                    【讨论】:

                      【解决方案17】:

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

                      【讨论】:

                        【解决方案18】:

                        我对在我看来最惯用的 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 的解决方案与 @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 的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"
                        

                        【讨论】:

                          猜你喜欢
                          • 2014-11-18
                          • 2021-05-30
                          • 1970-01-01
                          • 2014-12-24
                          • 1970-01-01
                          相关资源
                          最近更新 更多