この週末、いくつかの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)
1日かけて熟考した後、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 2.8)から開始すると、次のようにsliding
を使用してリストのスライディングウィンドウを取得することにより、n >= 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
各2つのペアの合計のリストがあります。この結果を使用して、4つの要素の移動平均を計算してみましょう。上記の式は次の計算を行いました。
from d1, d2, d3, d4, d5, d6, ...
to (d1+d2), (d2+d3), (d3+d4), (d4+d5), (d5+d6), ...
したがって、各要素を取得して2番目の次の要素に追加すると、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 / 2
のmovingSum
を計算し、その後、各要素を1つのn / 2
ステップに追加します。
その定義を使用して、問題に戻ってこれを行うことができます。
def simpleMovingAverage(values: List[Double], period: Int): List[Double] =
List.fill(period - 1)(0.0) ::: (movingSum(values, period) map (_ / period))
:::
の使用に関してはわずかな非効率性がありますが、O(values.size)ではなくO(period)です。末尾再帰関数を使用すると、より効率的にすることができます。そして、もちろん、私が提供した「スライディング」の定義はパフォーマンスの面では恐ろしいものですが、Scala 2.8。 sliding
で効率的なList
メソッドが、Iterable
で実行できます。
そうは言っても、最初の定義に進み、クリティカルパス分析でこれが大したことであると特定された場合にのみ最適化します。
最後に、私がどのように問題に取り組んだかを考えてみましょう。移動平均の問題があります。移動平均は、リスト上の移動する「ウィンドウ」の合計をそのウィンドウのサイズで割ったものです。だから、最初に、スライディングウィンドウを取得し、その上のすべてを合計してから、サイズで割ろうとします。
次の問題は、既に計算された加算の繰り返しを避けることでした。この場合、可能な限り最小の加算を行い、そのような結果を再利用してより大きな合計を計算する方法を見つけようとしました。
最後に、前の結果に加算および減算することにより、問題を考えた方法で解決してみましょう。最初の平均を取得するのは簡単です:
def movingAverage(values: List[Double], period: Int): List[Double] = {
val first = (values take period).sum / period
次に、2つのリストを作成します。最初に、減算する要素のリスト。次に、追加する要素のリスト:
val subtract = values map (_ / period)
val add = subtract drop period
Zip
を使用して、これら2つのリストを追加できます。このメソッドは、小さいリストにある要素をできるだけ多く生成するため、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
}
私は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という名前のようなもの)を作成する方法です。 recur
は、末尾呼び出しを排除するためにClojureで使用する必要があります。 conj
は一般化されたcons
であり、コレクションに自然な方法で追加されます。リストの始まりとベクトルの終わりです。
別の(機能的な)Clojureソリューションを次に示します。
(defn avarage [coll] (/(reduce + coll) (count coll))) (defn ma [period coll ] (マップ平均(パーティション期間1 coll)))
必要な場合は、シーケンスの先頭にゼロを追加する必要があります。
Clojureの純粋に機能的なソリューションを次に示します。既に提供されているものよりも複雑ですが、lazyおよびゼロから再計算するのではなく、各ステップで平均を調整するだけですです。実際には、期間が短い場合に各ステップで新しい平均を計算する単純なソリューションよりも遅くなります。ただし、より長い期間、実質的に速度低下はありませんが、何かが(/ (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))))))
以下は部分的に point-free 1行の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」で除算します。次に、「逆」でリストを元に戻します。
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]
これはすべて、実際よりもはるかに非効率に見えます。 「逆」は、リストが評価されるまでリストの順序を物理的に逆にするのではなく、単にスタックに配置するだけです(いいやつは怠zyなHaskell)。 「tails」もこれらの個別のリストをすべて作成するのではなく、元のリストの異なるセクションを参照するだけです。それはまだ素晴らしい解決策ではありませんが、1行の長さです:)
以下は、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」で2つの部分に分割します。
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
2番目のビットを元のリストで圧縮します(これにより、2つのリストから順番に項目がペアになります)。元のリストは明らかに長いですが、この余分なビットは失われます。
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」と同じですが、追加の実行状態/アキュムレーターパラメーターがあり、マップがリストを実行するときに前の「mapping」から次の「mapping」に渡されます。アキュムレータを移動平均として使用し、リストはスライドウィンドウを出たばかりの要素と入力したばかりの要素(圧縮したばかりのリスト)で構成されるため、スライド関数は最初の数字「x」を取ります平均から離れ、2番目の数字「y」を追加します。次に、新しい「s」を渡し、「s」を「p」で割った値を返します。 「snd」(2番目)は、mapAccumLがマップされたリストとともにアキュムレータを返すため、mapAccumLの2番目の戻り値を取得するために使用されるペアの2番目のメンバー(Tuple)を取得します。
$シンボル に慣れていない人にとっては、「アプリケーション演算子」です。それは実際には何もしませんが、「低い、右結合バインディングの優先順位」を持っているので、括弧を省くことができることを意味します(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]を生成します。
ああ、どちらかのソリューションをコンパイルするには、モジュール「リスト」をインポートする必要があります。
Scala 2.8.0(1つは厳密で、もう1つは遅延)で移動平均を行う2つの方法があります。両方とも少なくともがあると仮定しますpvsの倍になります。
// 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プログラミング言語は、移動平均などのプログラムを容易にします。実際、(+/ % #)\
の文字はラベルよりも少なく、「移動平均」です。
この質問で指定された値(名前 '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
どちらの例もまったく同じプログラムを使用しています。唯一の違いは、2番目の形式でより多くの名前を使用することです。このような名前は、J原色を知らない読者に役立ちます。
サブプログラムaverage
で行われていることをもう少し見てみましょう。 +/
は合計(Σ)を表し、%
は除算(古典的な記号÷のような)を表します。アイテムの集計(カウント)の計算は#
によって行われます。したがって、プログラム全体は、値の合計を値の集計で割ったものです:+/ % #
ここに記述された移動平均計算の結果には、元の質問で予想される先行ゼロは含まれていません。これらのゼロは、おそらく意図した計算の一部ではありません。
ここで使用される手法は、暗黙プログラミングと呼ばれます。これは、関数プログラミングのポイントフリースタイルとほとんど同じです。
これは、より機能的な言語のふりをした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 %)
...その場合、パラメーターの順序は関係ありません。
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)))
期間に関係なくO(リストの長さ)であるという利点があるClojureの短いバージョン:
(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]など)を作成し、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)
この例では状態を利用します。なぜなら私にとっては、この場合の実用的な解決策であり、ウィンドウ平均関数を作成するクロージャーだからです。
(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)
副作用がないわけではありませんが、ファーストクラスの機能を利用するという意味ではまだ機能的です。前述の2つの言語は両方ともJVMの上で実行されるため、必要に応じて状態管理が可能になります。
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]
再帰的な解決策を探しているようです。その場合、問題をわずかに変更し、(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
}
}
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タイプを取り除きます。
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つを実際に抽象化する必要があります....)
パーティーに遅刻し、関数型プログラミングも初めてであるため、内部ソリューションを使用してこのソリューションに取り組みました。
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、...)を生成します。
次に、最初の値を再帰的に減算し、最後の値を追加します。最後に、すべてをリストアップします。