CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: 中心化移動平均 (CMA: Centered Moving Average) について

以前から移動平均 (MA: Moving Average) という手法自体は知っていたけど、中心化移動平均 (CMA: Centered Moving Average) というものがあることは知らなかった。 一般的な移動平均である後方移動平均は、データの対応関係が原系列に対して遅れてしまう。 そこで、中心化移動平均という手法を使うことで遅れをなくすらしい。 この手法は、たとえば次のような用途でひとつのやり方として使われているようだ。

  • 不規則変動の除去
  • 季節変動の除去

使った環境は次のとおり。

$ sw_vers  
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G3020
$ python -V
Python 3.7.7

下準備

下準備として、必要なパッケージをインストールしておく。

$ pip install pandas seaborn

インストールできたら Python のインタプリタを起動する。

$ python

起動できたら、1 ~ 12 までの数字が入った Series のオブジェクトを作る。

>>> import pandas as pd
>>> x = pd.Series(range(1, 12 + 1))
>>> x
0      1
1      2
2      3
3      4
4      5
5      6
6      7
7      8
8      9
9     10
10    11
11    12
dtype: int64

上記のオブジェクトを使って移動平均の計算方法について学んでいく。

(後方) 移動平均 (MA: Moving Average)

はじめに、一般的な移動平均である後方移動平均から試す。 Pandas では rolling() メソッドを使うことで移動平均を計算できる。 以下では 3 点の要素で平均をとる 3 点移動平均を計算している。

>>> backward_3p_ma = x.rolling(window=3).mean()
>>> backward_3p_ma
0      NaN
1      NaN
2      2.0
3      3.0
4      4.0
5      5.0
6      6.0
7      7.0
8      8.0
9      9.0
10    10.0
11    11.0
dtype: float64

この計算では、たとえばインデックスで 2 に入る要素は、次のように計算される。

backward_3p_ma[2] = (x[0] + x[1] + x[2]) / 3

つまり、自分よりも後ろの値を使って平均を計算することになる。

この計算方法では、原系列との対応関係を考えたとき次のように 1 つ分の遅れが出る。

>>> pd.concat([x, backward_3p_ma], axis=1)
     0     1
0    1   NaN
1    2   NaN
2    3   2.0
3    4   3.0
4    5   4.0
5    6   5.0
6    7   6.0
7    8   7.0
8    9   8.0
9   10   9.0
10  11  10.0
11  12  11.0

中心化移動平均 (CMA: Centered Moving Average)

そこで、遅れの影響を取り除いたものが中心化移動平均と呼ばれるらしい。 たとえば、平均をとる要素数が奇数のときは、単に rolling() メソッドの center オプションを有効にするだけで良い。

>>> centered_3p_ma = x.rolling(window=3, center=True).mean()
>>> centered_3p_ma
0      NaN
1      2.0
2      3.0
3      4.0
4      5.0
5      6.0
6      7.0
7      8.0
8      9.0
9     10.0
10    11.0
11     NaN
dtype: float64

このオプションが有効だと、たとえばインデックスで 2 に入る要素は、次のように計算される。 つまり、自分の前後にある値を使って平均を計算することになる。

centered_3p_ma[2] = (x[1] + x[2] + x[3]) / 3

現系列との対応関係を確認すると、このやり方では遅れがなくなっている。

>>> pd.concat([x, centered_3p_ma], axis=1)
     0     1
0    1   NaN
1    2   2.0
2    3   3.0
3    4   4.0
4    5   5.0
5    6   6.0
6    7   7.0
7    8   8.0
8    9   9.0
9   10  10.0
10  11  11.0
11  12   NaN

ただし、このオプションは実装的には単に後方移動平均を計算した上で shift() しているだけに過ぎないらしい。 ようするに、こういうこと。

>>> backward_3p_ma_shifted = x.rolling(window=3).mean().shift(-1)
>>> pd.concat([x, backward_3p_ma_shifted], axis=1)
     0     1
0    1   NaN
1    2   2.0
2    3   3.0
3    4   4.0
4    5   5.0
5    6   6.0
6    7   7.0
7    8   8.0
8    9   9.0
9   10  10.0
10  11  11.0
11  12   NaN

そのため、平均をとる要素数が偶数のときに困ったことが起こる。

平均をとる要素数が偶数のときの問題点について

試しに、平均をとる要素数を 4 点に増やして、そのまま計算してみよう。

>>> centerd_4p_ma = x.rolling(window=4, center=True).mean() 
>>> centerd_4p_ma
0      NaN
1      NaN
2      2.5
3      3.5
4      4.5
5      5.5
6      6.5
7      7.5
8      8.5
9      9.5
10    10.5
11     NaN
dtype: float64

すると、計算結果に端数が出ている。

原系列と比較すると、対応関係に 0.5 の遅れがあることがわかる。

>>> pd.concat([x, centerd_4p_ma], axis=1)
     0     1
0    1   NaN
1    2   NaN
2    3   2.5
3    4   3.5
4    5   4.5
5    6   5.5
6    7   6.5
7    8   7.5
8    9   8.5
9   10   9.5
10  11  10.5
11  12   NaN

つまり、中心化移動平均では平均をとる要素数が偶数と奇数のときで計算方法を変えなければいけない。

要素数が偶数のときの計算方法

要素数が偶数のときの中心化移動平均は、計算が 2 段階に分かれている。 はじめに、後方移動平均をそのまま計算する。

>>> backward_4p_ma =x.rolling(window=4).mean()
>>> backward_4p_ma
0      NaN
1      NaN
2      NaN
3      2.5
4      3.5
5      4.5
6      5.5
7      6.5
8      7.5
9      8.5
10     9.5
11    10.5
dtype: float64

その上で、移動平均に使った要素数の半分だけデータをずらし、もう一度要素数 2 で平均を取り直す。

centerd_4p_ma = backward_4p_ma.shift(-2).rolling(window=2).mean()

原系列との対応関係を確認すると、遅れが解消していることがわかる。

>>> pd.concat([x, centerd_4p_ma], axis=1)
     0     1
0    1   NaN
1    2   NaN
2    3   3.0
3    4   4.0
4    5   5.0
5    6   6.0
6    7   7.0
7    8   8.0
8    9   9.0
9   10  10.0
10  11   NaN
11  12   NaN

別のデータで中心化移動平均を計算してみる

もうちょっとちゃんとしたデータでも計算してみよう。 次のサンプルコードでは、旅客機の乗客数の推移に対して 12 点で中心化移動平均を計算している。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from calendar import month_name

import seaborn as sns
from matplotlib import pyplot as plt
import pandas as pd
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()


def main():
    # 航空機の乗客を記録したデータセットを読み込む
    df = sns.load_dataset('flights')
    # 月の名前を数字に直す
    month_name_mappings = {name: str(n).zfill(2) for n, name in
                           enumerate(month_name)}
    df['month'] = df['month'].apply(lambda x: month_name_mappings[x])
    # 年と月を結合したカラムを用意する
    df['year-month'] = df.year.astype(str) + '-' + df.month.astype(str)
    df['year-month'] = pd.to_datetime(df['year-month'], format='%Y-%m')

    # 原系列
    sns.lineplot(data=df, x='year-month', y='passengers', label='original')
    # 中心化移動平均
    df['passengers-cma'] = df['passengers'].rolling(window=12).mean().shift(-6).rolling(2).mean()
    sns.lineplot(data=df, x='year-month', y='passengers-cma', label='CMA')

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記を実行してみる。

$ python flightscma.py

得られるグラフが次のとおり。

f:id:momijiame:20200401184339p:plain
旅客機の乗客数データに対して 12 点中心化移動平均を計算する

ここまでデータ点数が多いと、正直 0.5 の遅れとか言われてもよくわからないけど、これで計算できているはず。