2020年12月21日月曜日

Jupyter notebooks + Kotlin で移動平均を描画する(その3): 移動平均を計算する

このエントリは Fintalk Advent Calendar 2020 の21日目です。
今年は3つも割当たっているので、Covid-19 のデータで件数とその移動平均をグラフに描画する、というのを3回シリーズでやりたいと思います。


移動平均(Moving Average)は簡単に言うと、時系列データで平均をとって滑らかにする方法です。

Wikipedia にはこう書いてあります。

移動平均()は、時系列データ(より一般的には時系列に限らず系列データ)を平滑化する手法である。音声や画像等のデジタル信号処理に留まらず、金融(特にテクニカル分析)分野、気象、水象を含む計測分野等、広い技術分野で使われる。有限インパルス応答に対するローパスフィルタ(デジタルフィルタ)の一種であり、分野によっては移動積分とも呼ばれる。

主要なものは、単純移動平均と加重移動平均と指数移動平均の3種類である。普通、移動平均といえば、単純移動平均のことをいう。

by https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87


ここに書いてあるとおり、主なものとして
  • 単純移動平均
  • 加重移動平均
  • 指数移動平均
があります。

それぞれ計算してみましょう。
前回CSVデータを操作して得られた日本の新規感染者の最初の20日間のデータを使ってみます。
  1. A DataFrame: 318 x 2  
  2.            date   new_cases_double  
  3.  1   2020-01-23                  0  
  4.  2   2020-01-24                  0  
  5.  3   2020-01-25                  0  
  6.  4   2020-01-26                  2  
  7.  5   2020-01-27                  0  
  8.  6   2020-01-28                  3  
  9.  7   2020-01-29                  0  
  10.  8   2020-01-30                  4  
  11.  9   2020-01-31                  4  
  12. 10   2020-02-01                  5  
  13. 11   2020-02-02                  0  
  14. 12   2020-02-03                  0  
  15. 13   2020-02-04                  2  
  16. 14   2020-02-05                  1  
  17. 15   2020-02-06                  0  
  18. 16   2020-02-07                  0  
  19. 17   2020-02-08                  1  
  20. 18   2020-02-09                  0  
  21. 19   2020-02-10                  2  
  22. 20   2020-02-11                  1  
わかりやすいように4日間の移動平均(4日移動平均)を計算してみます。

単純移動平均(Simple Moving Average)

単純移動平均は値をそのまま足して平均を計算する方法です。
例えば 01-26 の単純移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)を足して4で割るので 0.5 です。

01-26 の単純移動平均 = (01-23 + 01-24 + 01-25 + 01-26) / 4 = (0 + 0 + 0 + 2) / 4 = 0.5

01-27 の単純移動平均を計算するとき、01-24 〜 01-27 までのデータ(0,0,2,0)を足して4で割ってもいいのですが、すでに01-26 の単純移動平均が計算してあるなら

01-27 の単純移動平均
= (01-24 + 01-25 + 01-26 + 01-27) / 4
= (- 01-23 + (01-23 + 01-24 + 01-25 + 01-26) + 01-27) / 4
= - 01-23 / 4 + (01-23 + 01-24 + 01-25 + 01-26) / 4 + 01-27 / 4
= - 01-23 / 4 + 01-26 の単純移動平均 + 01-27 / 4
= 01-26 の単純移動平均 - 01-23 / 4 + 01-27 / 4

このように、01-26 の単純移動平均から古い値(01-23)を4で割った数を引いて、新しい値(01-27)を4で割った値を足せば求めることができます。

01-23 〜 01-25 の単純移動平均は4日間分のデータがないので計算できません。

  1. A DataFrame: 318 x 2  
  2.            date   new_cases_double   simple_moving_average  
  3.  1   2020-01-23                  0  
  4.  2   2020-01-24                  0  
  5.  3   2020-01-25                  0  
  6.  4   2020-01-26                  2                    0.50  
  7.  5   2020-01-27                  0                    0.50  
  8.  6   2020-01-28                  3                    1.25  
  9.  7   2020-01-29                  0                    1.25  
  10.  8   2020-01-30                  4                    1.75  
  11.  9   2020-01-31                  4                    2.75  
  12. 10   2020-02-01                  5                    3.25  
  13. 11   2020-02-02                  0                    3.25  
  14. 12   2020-02-03                  0                    2.25  
  15. 13   2020-02-04                  2                    1.75  
  16. 14   2020-02-05                  1                    0.75  
  17. 15   2020-02-06                  0                    0.75  
  18. 16   2020-02-07                  0                    0.75  
  19. 17   2020-02-08                  1                    0.50  
  20. 18   2020-02-09                  0                    0.25  
  21. 19   2020-02-10                  2                    0.75  
  22. 20   2020-02-11                  1                    1.00  
  1. val newCases3 = newCases2.head(20)  
  2.   
  3. val values = newCases3["new_cases_double"].asDoubles()  
  4. val size = values.size  
  5.   
  6. var sma = arrayOfNulls<Double?>(size)  
  7. val n = 4  
  8.   
  9. // calculate 01-26  
  10. var sum = 0.0  
  11. for(i in 0 until n) {  
  12.     sum += values[i]!!  
  13. }  
  14. sma[n-1] = sum/n  
  15.   
  16. // calculate 01-27 ~  
  17. for(i in n until values.size) {  
  18.     sma[i] = sma[i-1]!! - values[i-n]!!/n + values[i]!!/n  
  19. }  
  20.   
  21. val newCases4 = newCases3.addColumn("simple_moving_average") { sma }  
赤の単純移動平均の線が滑らかになっているのがわかります。


加重移動平均(Weighted Moving Average)

加重移動平均は各値に重みをつけたものを足して平均を計算する方法です。例えば線形加重移動平均(Linear Weighted Moving Average)だと、現在に最も近い日の重みが一番大きくなり、そこから過去に行くほど線形に(一定量ずつ)重みが減っていきます。

例えば 01-26 の加重移動平均は 01-23 〜 01-26 までのデータ(0,0,0,2)から次のように計算します。

現在に最も近い日の 01-26 のデータには 4 を掛けます。次に近い日の 01-25 のデータには 4 から 1 を引いた値を掛けます。その前の日は 4-2、その前の日は 4-3 を掛けます。
それを 4 + 3 + 2 + 1 = 10 で割ります。

01-26 の加重移動平均 = (01-23 * (4-3) + 01-24* (4-2) + 01-25 * (4-1) + 01-26 * (4-0)) / (4 + 3 + 2 + 1)
= (0 * 1 + 0 * 2 + 0 * 3 + 2 * 4) / 10 = 0.8
= (0 + 0 + 0 + 8) / 10 = 0.8

01-27 の加重移動平均には、単純移動平均と同じように 01-26 の加重移動平均を利用します。

01-27 の加重移動平均
= (01-24 * (4-3) + 01-25* (4-2) + 01-26 * (4-1) + 01-27 * (4-0)) / 10
= (- 01-23 - 01-24 - 01-25 - 01-26 + (01-23 * (4-3) + 01-24 * (4-2) + 01-25* (4-1) + 01-26 * (4-0)) + 01-27 * (4-0)) / 10
= (- (01-23 ~ 01-26の総和)/10) + 01-26 の加重移動平均 + 01-27 * 4 / 10
= 01-26 の加重移動平均 - (01-23 ~ 01-26の総和) / 10 + 01-27 * 4 / 10


このように、01-26 の加重移動平均に、01-23 ~ 01-26の総和を10で割った数を引いて、新しい値(01-27)に4を掛けて10で割った値を足せば求めることができます。

01-23 〜 01-25 の加重移動平均は4日間分のデータがないので計算できません。

  1. val newCases3 = newCases2.head(20)  
  2.   
  3. val values = newCases3["new_cases_double"].asDoubles()  
  4. val size = values.size  
  5.   
  6. var wma = arrayOfNulls<Double?>(size)  
  7. var sums = arrayOfNulls<Double?>(size)  
  8. val n = 4  
  9. val n2 = n*(n + 1)/2  
  10.   
  11. // calculate 01-26  
  12. var sum = 0.0  
  13. var sum_wma = 0.0  
  14. for(i in 0 until n) {  
  15.     sum += values[i]!!  
  16.     sum_wma += values[i]!! * (i + 1)  
  17. }  
  18. sums[n-1] = sum  
  19. wma[n-1] = sum_wma/n2  
  20.   
  21. // calculate 01-27 ~  
  22. for(i in n until values.size) {  
  23.     sums[i] = sums[i-1]!! - values[i-n]!! + values[i]!!  
  24.     wma[i] = wma[i-1]!! - sums[i-1]!!/n2 + n * values[i]!!/n2  
  25. }  
  26.   
  27. val newCases5 = newCases4.addColumn("weighted_moving_average") { wma }  
赤が単純移動平均、緑が加重移動平均です。


指数移動平均(Exponential Moving Average)

指数移動平均は加重移動平均のように各値に重みをつけたものを足して平均を計算する方法です。各値につける重みが指数関数的に減っていきます。

重みの減少度合いは平滑化係数と呼ばれる0~1の間の値をとる定数 α で決まり、αを時系列区間 N で表した場合 α = 2 / (N+1) となります。
最初の値での EMA は定義しません。2番目の値での EMA をどう設定するかにはいくつかの手法があるそうですが、ここでは単純に2番目の値とします。

3番目以降の場合の EMA の計算式はこうなります。

EMA_t = α * value_t + (1 - α) * EMA_(t-1)



α = 2 / (N + 1) = 2 / (4 + 1) = 0.4 として計算すると
01-24 の指数移動平均 : 0
01-25 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0 = 0
01-26 の指数移動平均 : 0.4 * 2 + (1 - 0.4) * 0 = 0.8
01-27 の指数移動平均 : 0.4 * 0 + (1 - 0.4) * 0.8 = 0.48


  1. val newCases3 = newCases2.head(20)  
  2.   
  3. val values = newCases3["new_cases_double"].asDoubles()  
  4. val size = values.size  
  5.   
  6. var ema = arrayOfNulls<Double?>(size)  
  7. var sums = arrayOfNulls<Double?>(size)  
  8. val n = 4  
  9. val alpha = 2.0 / (n + 1)  
  10.   
  11. ema[0] = null  
  12. ema[1] = values[1]  
  13.   
  14. // calculate 01-25 ~  
  15. for(i in 2 until values.size) {  
  16.     ema[i] = alpha * values[i]!! + (1 - alpha) * ema[i - 1]!!  
  17. }  
  18.   
  19. val newCases6 = newCases5.addColumn("exponential_moving_average") { ema }  
赤が単純移動平均、緑が加重移動平均、灰色が指数移動平均です。


移動平均は自分で実際に計算してグラフにするとよくわかると思うので、Kotlin じゃなくても、好きな言語でぜひやってみてください。


0 件のコメント:

コメントを投稿