Python

【時系列解析】Pythonで移動平均を出してみた

1. はじめに

今回はPythonを使い、移動平均を算出する方法を紹介します。

移動平均とは、主に時系列のデータを平滑化するのによく用いられる手法で、株価のチャートで頻繁に見られるのでご存知の方も多いでしょう(「25日移動平均線」など)。データの長期的なトレンドを追いたいときに、よく用いられます。

2. 移動平均とは

「移動平均」と言ってもいくつかの種類、計算方法があるので、それぞれを見ていきましょう。

1) 単純移動平均(Simple Moving Average; SMA)

単純移動平均とは、直近の n 個のデータの単純な平均値を求めたものです。ある店舗のタピオカミルクティーの販売数の推移(表1)から、5日間の単純移動平均を求めてみましょう。

直近5日間の販売数の移動平均をYとすると、時点でのは、

Y=(Yt+Yt-1+Yt-2+Yt-3+Yt-4)/5

で求めることができます。式を見てわかる通り、直近と昔の販売数のデータが均等に織り込まれています。

表1に3、5、7日間の移動平均を計算した列がありますが、Pythonでは以下のようにrollingメソッドを使うことで計算できます。

import pandas as pd
import matplotlib.pyplot as plt

#表の作成
data = {"月日":["2019-12-01","2019-12-02","2019-12-03","2019-12-04","2019-12-05",
              "2019-12-06","2019-12-07","2019-12-08","2019-12-09","2019-12-10" ,
              "2019-12-11","2019-12-12","2019-12-13","2019-12-14","2019-12-15",
              "2019-12-16","2019-12-17","2019-12-18","2019-12-19","2019-12-20"],
      "販売数":[100, 110, 95, 97, 103, 114, 127, 120, 113, 119, 90, 94, 108, 101, 97, 115, 124, 102, 106, 99]}

df = pd.DataFrame(data)

# 移動平均の計算

df["3日間移動平均"]=df["販売数"].rolling(3).mean().round(1)
df["5日間移動平均"]=df["販売数"].rolling(5).mean().round(1)
df["7日間移動平均"]=df["販売数"].rolling(7).mean().round(1)

print(df)
月日 販売数 3日間移動平均 5日間移動平均 7日間移動平均
0 2019-12-01 100 NaN NaN NaN
1 2019-12-02 110 NaN NaN NaN
2 2019-12-03 95 101.7 NaN NaN
3 2019-12-04 97 100.7 NaN NaN
4 2019-12-05 103 98.3 101.0 NaN
5 2019-12-06 114 104.7 103.8 NaN
6 2019-12-07 127 114.7 107.2 106.6
7 2019-12-08 120 120.3 112.2 109.4
8 2019-12-09 113 120.0 115.4 109.9
9 2019-12-10 119 117.3 118.6 113.3
10 2019-12-11 90 107.3 113.8 112.3
11 2019-12-12 94 101.0 107.2 111.0
12 2019-12-13 108 97.3 104.8 110.1
13 2019-12-14 101 101.0 102.4 106.4
14 2019-12-15 97 102.0 98.0 103.1
15 2019-12-16 115 104.3 103.0 103.4
16 2019-12-17 124 112.0 109.0 104.1
17 2019-12-18 102 113.7 107.8 105.9
18 2019-12-19 106 110.7 108.8 107.6
19 2019-12-20 99 102.3 109.2 106.3

はじめは、移動平均を計算できないためNANが入りますが、それぞれ3、5、7日目以降に計算値が入りきます。

これをグラフにすると、以下のようになります。

plt.plot(df["月日"], df["販売数"], label="daily")
plt.plot(df["月日"], df["3日間移動平均"], "k--", label="SMA(3)")
plt.plot(df["月日"], df["5日間移動平均"], "r--", label="SMA(5)")
plt.plot(df["月日"], df["7日間移動平均"], "g--", label="SMA(7)")
plt.xticks(rotation=90)
plt.xlabel("date")
plt.ylabel("quantity")
plt.legend()

plt.show()

日々の販売量の推移に比べると、移動平均は平滑になっていることが分かります。そして、移動平均を算出する期間が長くなるほど、グラフは滑らかに推移することが分かります。移動平均を算出することで、推移のトレンドを見ることができます。

2) 加重移動平均(Weighted Moving Average; WMA)

指定期間のデータを単純に平均しただけの単純移動平均に対して、直近のデータに比重を置いた移動平均が、加重移動平均と指数移動平均です。

どちらも単純移動平均と比べてデータの変動に素早く反応するので、トレンドの変化を早めに捉えることができます。

直近の価格に徐々に比重を置いていく移動平均が、加重移動平均です。

では、1)と同様に5日間の加重移動平均を求めてみましょう。

直近5日間の販売数の加重移動平均をYとすると、時点tでのYは、

Y=(5Yt+4Yt-1+3Yt-2+2Yt-3+1Yt-4)/(5+4+3+2+1)

で求めることができます。式を見てわかる通り、直近データに比重が置かれています。

表1の販売数のデータを使って求めた3、5、7日の加重移動平均を表2に示します。

Pythonには加重移動平均を求める関数はないため、自分で作る必要があります。

import pandas as pd
import matplotlib.pyplot as plt

#表の作成
data = {"月日":["2019-12-01","2019-12-02","2019-12-03","2019-12-04","2019-12-05",
              "2019-12-06","2019-12-07","2019-12-08","2019-12-09","2019-12-10" ,
              "2019-12-11","2019-12-12","2019-12-13","2019-12-14","2019-12-15",
              "2019-12-16","2019-12-17","2019-12-18","2019-12-19","2019-12-20"],
      "販売数":[100, 110, 95, 97, 103, 114, 127, 120, 113, 119, 90, 94, 108, 101, 97, 115, 124, 102, 106, 99]}

df = pd.DataFrame(data)

# 加重移動平均を求める関数

def wma(milktea):
    
    weight = np.arange(len(milktea)) + 1
    wma = np.sum(weight * milktea) / weight.sum()
    
    return wma

df["加重移動平均(3)"]=df["販売数"].rolling(3).apply(wma, raw=True).round(1)
df["加重移動平均(5)"]=df["販売数"].rolling(5).apply(wma, raw=True).round(1)
df["加重移動平均(7)"]=df["販売数"].rolling(7).apply(wma, raw=True).round(1)

print(df)
           月日 販売数 加重移動平均(3)  加重移動平均(5)  加重移動平均(7)
0   2019-12-01  100        NaN   NaN NaN
1   2019-12-02  110        NaN   NaN NaN
2   2019-12-03   95      100.8        NaN   NaN
3   2019-12-04   97       98.5        NaN   NaN
4   2019-12-05  103       99.7      100.5        NaN
5   2019-12-06  114      107.5      104.9        NaN
6   2019-12-07  127      118.7      112.6      110.0
7   2019-12-08  120      121.3      116.9      113.4
8   2019-12-09  113      117.7      117.1      114.3
9   2019-12-10  119      117.2      118.3      116.6
10  2019-12-11   90      103.5      108.8      110.8
11  2019-12-12   94       96.8      102.2      106.2
12  2019-12-13  108      100.3      102.5      105.4
13  2019-12-14  101      102.2      101.2      103.1
14  2019-12-15   97      100.2       99.4      100.8
15  2019-12-16  115      106.7      105.1      103.8
16  2019-12-17  124      116.5      112.1      108.9
17  2019-12-18  102      111.5      109.7      108.4
18  2019-12-19  106      107.7      109.1      108.4
19  2019-12-20   99      101.8      105.9      106.2

これをグラフにすると、以下のようになります。

plt.plot(df["月日"], df["販売数"], label="daily")
plt.plot(df["月日"], df["加重移動平均(3)"], "k--", label="WMA(3)")
plt.plot(df["月日"], df["加重移動平均(5)"], "r--", label="WMA(5)")
plt.plot(df["月日"], df["加重移動平均(7)"], "g--", label="WMA(7)")
plt.xticks(rotation=90)
plt.xlabel("date")
plt.ylabel("quantity")
plt.legend()

plt.show()

単純移動平均に比べると、販売数のデータの動きに近いことが分かります。これが、直近のデータに比重を置いた効果です。

3) 指数移動平均(Exponential Moving Average; EMA)

加重移動平均よりさらに直近のデータに比重を置き、過去の影響を指数関数的に重みを低くして算出する移動平均が、指数移動平均です。

比重の減少の程度は「平滑化係数」と呼ばれる、0と1の間の値を取る平滑定数αで決めます。

n日間の指数移動平均は、以下のように求められます

1日目:n日間の販売数の平均

2日目以降:前日の指数移動平均+平滑化定数α×{当日の販売数ー前日の指数移動平均}

ただし、α=2÷(n+1)

上記の計算式を用いて、表1の販売数のデータを使って求めた3、5、7日の指数移動平均を表3に示します。

表3. 販売数と指数移動平均推移

加重移動平均と同じく、Pythonには指数移動平均を求める関数はないため、自分で作る必要があります。

#表の作成
data = {"月日":["2019-12-01","2019-12-02","2019-12-03","2019-12-04","2019-12-05",
              "2019-12-06","2019-12-07","2019-12-08","2019-12-09","2019-12-10" ,
              "2019-12-11","2019-12-12","2019-12-13","2019-12-14","2019-12-15",
              "2019-12-16","2019-12-17","2019-12-18","2019-12-19","2019-12-20"],
      "販売数":[100, 110, 95, 97, 103, 114, 127, 120, 113, 119, 90, 94, 108, 101, 97, 115, 124, 102, 106, 99]}

df = pd.DataFrame(data)

# 指数移動平均を求める関数

def ema(milktea, period):
    ema = np.zeros(len(milktea))
    ema[:] = np.nan # NaN で一旦初期化
    ema[period-1] = milktea[:period].mean() # 最初だけ単純移動平均で算出
    
    for day in range(period, len(milktea)):
        ema[day] = ema[day-1] + (milktea[day] - ema[day-1]) / (period + 1) * 2
    
    return ema

df["指数移動平均(3)"]=ema(df["販売数"], 3).round(1)
df["指数移動平均(5)"]=ema(df["販売数"], 5).round(1)
df["指数移動平均(7)"]=ema(df["販売数"], 7).round(1)

print(df)
           月日 販売数 指数移動平均(3)  指数移動平均(5)  指数移動平均(7)
0   2019-12-01  100        NaN   NaN NaN
1   2019-12-02  110        NaN   NaN NaN
2   2019-12-03   95      101.7        NaN   NaN
3   2019-12-04   97       99.3        NaN   NaN
4   2019-12-05  103      101.2      101.0        NaN
5   2019-12-06  114      107.6      105.3        NaN
6   2019-12-07  127      117.3      112.6      106.6
7   2019-12-08  120      118.6      115.0      109.9
8   2019-12-09  113      115.8      114.4      110.7
9   2019-12-10  119      117.4      115.9      112.8
10  2019-12-11   90      103.7      107.3      107.1
11  2019-12-12   94       98.9      102.8      103.8
12  2019-12-13  108      103.4      104.6      104.9
13  2019-12-14  101      102.2      103.4      103.9
14  2019-12-15   97       99.6      101.3      102.2
15  2019-12-16  115      107.3      105.8      105.4
16  2019-12-17  124      115.7      111.9      110.0
17  2019-12-18  102      108.8      108.6      108.0
18  2019-12-19  106      107.4      107.7      107.5
19  2019-12-20   99      103.2      104.8      105.4

これをグラフにすると、以下のようになります。

plt.plot(df["月日"], df["販売数"], label="daily")
plt.plot(df["月日"], df["指数移動平均(3)"], "k--", label="EMA(3)")
plt.plot(df["月日"], df["指数移動平均(5)"], "r--", label="EMA(5)")
plt.plot(df["月日"], df["指数移動平均(7)"], "g--", label="EMA(7)")
plt.xticks(rotation=90)
plt.xlabel("date")
plt.ylabel("quantity")
plt.legend()

plt.show()

加重移動平均と同様に直近の値に比重を置いているため、トレンドは加重移動平均に近いものになります。

3. おわりに

時系列のデータ解析の際によく用いる、3種類の移動平均について見てきました。

Pythonでは単純移動平均は関数が用意されていますが、加重移動平均と指数移動平均は関数を自作する必要があります。

直近のデータに比重を置いている加重移動平均と指数移動平均は、単純移動平均と比べると元データの変化に素早く反応しますので、解析の目的に応じて使い分けるようにしましょう。

4. 参考サイト

移動平均の意味や目的、求め方、注意点

https://toukeigaku-jouhou.info/2015/08/23/moving-average/

株価移動平均線の見方・使い方(1)~移動平均線の基礎

https://www.sevendata.co.jp/shihyou/technical/idouheikin01.html

Pandas 演習としてのテクニカル指標計算 〜 移動平均の巻

http://mariyudu.hatenablog.com/entry/2019/03/30/174547

加重移動平均線の見方・使い方

https://www.sevendata.co.jp/shihyou/technical/kajuuidou.html

指数平滑移動平均線の見方・使い方

https://www.sevendata.co.jp/shihyou/technical/heikatsuidou.html

ABOUT ME
北爪 聖也
ダメ営業マンからデータサイエンティストへキャリアチェンジ。 技術とビジネスサイドの橋渡しが出来るため、ダメ営業マンの経験も役に立ちました。 広告代理店ADKにて3年勤務→データ分析受託の会社DATUM STUDIOにて1.2年勤務後、独立。
データサイエンスの仕事に興味がある方!

株式会社piponではデータサイエンティストの方々へお仕事をご紹介しております。

もし、仕事にご興味ある方がいらっしゃいましたらこちらのフォームよりご連絡頂けると嬉しいです。

また、「未経験から転職して1年で身につけたスキルで月収25万円の副業を得るまで」という記事を有料noteとして出していますので、興味ある方は購入いただけると嬉しいです!