加重移動平均

移動平均 - uokumuraの日記で作った単純移動平均

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {int n->
        (1..<n).collect {delegate[0..<it].average} +
        (n..size()).collect {i->delegate[(i-n)..<i].average}
    }
}

では過去n件が等しく扱われるため、異常値のn個後まで異常値の影響を受けて高くなり、n+1件目で突如値が変化します。
このため、異常値の後にn件以上平均的な値が続くと、一見何もないところで突然移動平均値が変化したりします。たとえば、次のグラフは2系統の値の棒グラフに対し、n=6の移動平均を線グラフとして出したものですが、

後ろの方にほぼ2で推移しているのに突如移動平均値が下がったり、直前の移動平均値より高い値が出ているにも関わらず移動平均値が下がったりしている部分が有ります。
この不自然な動きを避けるため、直近の値により大きな重み付けをする加重移動平均というものがあるので、これを計算できるよう拡張する事にしました。異常系で無駄に長くなるのもなんなんで、今回は上記の「素直な実装」をベースにします。

リファクタリング

移動平均値の算出の対象になる値は一緒で、その値に対する平均値の算出方法が変わるだけなので、デフォルトではこれまで通りの単純平均のまま、平均の算出方法を必要に応じて平均値の算出方法をクロージャで渡せるようにします。

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {n, average = {it.average}->
        (1..<n).collect{average(delegate[0..<it])} +
        (n..size()).collect {i->average(delegate[(i-n)..<i])}
    }
}

後は、加重平均の算出ロジックをmovingAverageに渡す加重移動平均メソッドを作成するだけです。今回はnから離れるごとに重み付けが1ずつ減っていく線形加重移動平均を作成する事にします。例えばn=3の時には

 [(1*l[0] + 2*l[1] + 3*l[2])/(1+2+3), (1*l[1] + 2*l[2] + 3*l[3])/(1+2+3), …, (1*l[-3] + 2*l[-2] + 3*l[-1])/(1+2+3)]

の様になる加重移動平均です。
これの、移動しつつ平均を出している部分に注目して変形させていくと

(l[0] + l[1] + l[2] + l[1] + l[2] + l[2])/(1+2+3)

[[l[0], l[1], l[2]], [l[1], l[2]], [l[2]]].flatten().average

[l[0..<3], l[1..<3], l[2..<3]].flatten().average

(0..2).collect{ l[it..<l.size()] }.flatten().average

となります。ここまでくれば実装は簡単。

List.metaClass.define {
    getAverage {-> sum() / size() }
    
    movingAverage {n, average = {it.average}->
        (1..<n).collect{average(delegate[0..<it])} +
        (n..size()).collect {i->average(delegate[(i-n)..<i])}
    }
    
    linearWeightedMovingAverage {int n->
        delegate.movingAverage(n) {
            (0..<it.size()).collect{i->it[i..<it.size()]}.flatten().getAverage()
        }
    }
}

…なぜかlinearWeightedMovingAverage中でaverageプロパティが見つけられなかったので、getAverage()と明示的にゲッタを呼んでやる事にしました。
最初のグラフをこの線形加重移動平均で出してみると

単純移動平均のような不自然な変化はなくなりました。ただ、動きが激しくなってます。重み付けの結果最新の値の影響をより大きく受けるようになったんですね。