K_DM Book
用語集
対象者 標準
対象者別の入口 →

対象者の設定はこのブラウザの中にだけ保存されます。

言語 JA

SES

入門

04.12.1

SES

最終更新 2020-01-29 読了時間 5 分
まとめ
単純指数平滑法(SES)は、直近の観測値ほど大きな重みを付けて平均し、その値を次の期の予測にする手法です。重みは平滑係数 \(\alpha\) で決まり、\(\alpha = 0.1\) なら最新の値を 1 割、それまでの平滑値を 9 割で混ぜるので、過去を長く引きずるなだらかな線になります。このページでは米国の天然ガス消費量(月次 BCF、2013〜2022 年)を 2020 年 1 月で学習と検証に分け、SimpleExpSmoothing(data_train).fit(smoothing_level=0.1) で 33 か月先を予測します。SES は水準しか持たないため予測は水平線になり、冬に 3000 を超え夏に 2000 近くまで落ちる季節の波を表せないことが図で分かります。

直感 #

毎月の天然ガス消費量から来月の値を見積もるとき、2013 年の値と先月の値を同じ重みで平均するのは不自然です。SES は「先月を \(\alpha\)、それ以前の見積もりを \(1 - \alpha\)」で混ぜる更新を毎月繰り返すので、2 か月前の重みは \(\alpha(1-\alpha)\)、3 か月前は \(\alpha(1-\alpha)^2\) と指数的に小さくなります。\(\alpha\) を 0.9 にすれば直近の値をほぼそのまま予測に使い、0.1 にすれば 20 か月以上前の値も効く滑らかな線になります。更新式は \(\ell_t = \alpha y_t + (1 - \alpha)\ell_{t-1}\) の 1 本だけで、予測はこの最終値 \(\ell_T\) を未来のすべての月に置くので、SES の予測線は必ず水平になります。

1. ライブラリ #

1
2
3
4
5
6
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from statsmodels.tsa.holtwinters import SimpleExpSmoothing

何が起きているか(ステップで見る) #

コードを読む前に、この手法が図の上で何をしているかを追ってみましょう。「再生」を押すか、スライダーを動かすと 1 手順ずつ進みます。

見るポイント:α は「新しい値をどれだけ信じるか」。小さいほど滑らか、大きいほど直近に追従。

2. サンプルデータ #

Natural Gas Consumption(NATURALGAS) の月次データを使います。数値の単位は BCF(10 億立方フィート)で、2013 年 1 月から 2022 年 9 月までの 117 か月分を辞書で直接埋め込み、日付 index の DataFrame に整えます。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
data = {
    "value": {
        "2013-01-01": 2878.8,
        "2013-02-01": 2567.2,
        "2013-03-01": 2521.1,
        "2013-04-01": 1967.5,
        "2013-05-01": 1752.5,
        "2013-06-01": 1742.9,
        "2013-07-01": 1926.3,
        "2013-08-01": 1927.4,
        "2013-09-01": 1767.0,
        "2013-10-01": 1866.8,
        "2013-11-01": 2316.9,
        "2013-12-01": 2920.8,
        "2014-01-01": 3204.1,
        "2014-02-01": 2741.2,
        "2014-03-01": 2557.9,
        "2014-04-01": 1961.7,
        "2014-05-01": 1810.2,
        "2014-06-01": 1745.4,
        "2014-07-01": 1881.0,
        "2014-08-01": 1933.1,
        "2014-09-01": 1809.3,
        "2014-10-01": 1912.8,
        "2014-11-01": 2357.5,
        "2014-12-01": 2679.2,
        "2015-01-01": 3115.0,
        "2015-02-01": 2925.2,
        "2015-03-01": 2591.3,
        "2015-04-01": 2007.9,
        "2015-05-01": 1858.2,
        "2015-06-01": 1899.9,
        "2015-07-01": 2067.7,
        "2015-08-01": 2052.7,
        "2015-09-01": 1901.3,
        "2015-10-01": 1987.3,
        "2015-11-01": 2249.1,
        "2015-12-01": 2588.2,
        "2016-01-01": 3091.7,
        "2016-02-01": 2652.3,
        "2016-03-01": 2356.3,
        "2016-04-01": 2083.9,
        "2016-05-01": 1965.8,
        "2016-06-01": 2000.7,
        "2016-07-01": 2186.6,
        "2016-08-01": 2208.4,
        "2016-09-01": 1947.8,
        "2016-10-01": 1925.2,
        "2016-11-01": 2159.5,
        "2016-12-01": 2866.3,
        "2017-01-01": 2913.8,
        "2017-02-01": 2340.2,
        "2017-03-01": 2523.3,
        "2017-04-01": 1932.0,
        "2017-05-01": 1892.0,
        "2017-06-01": 1910.4,
        "2017-07-01": 2141.6,
        "2017-08-01": 2093.8,
        "2017-09-01": 1920.5,
        "2017-10-01": 2031.5,
        "2017-11-01": 2357.3,
        "2017-12-01": 3086.0,
        "2018-01-01": 3340.9,
        "2018-02-01": 2710.7,
        "2018-03-01": 2796.7,
        "2018-04-01": 2350.5,
        "2018-05-01": 2055.0,
        "2018-06-01": 2063.1,
        "2018-07-01": 2350.7,
        "2018-08-01": 2313.8,
        "2018-09-01": 2156.1,
        "2018-10-01": 2285.9,
        "2018-11-01": 2715.9,
        "2018-12-01": 2999.5,
        "2019-01-01": 3424.3,
        "2019-02-01": 3019.1,
        "2019-03-01": 2927.8,
        "2019-04-01": 2212.4,
        "2019-05-01": 2134.0,
        "2019-06-01": 2119.3,
        "2019-07-01": 2393.9,
        "2019-08-01": 2433.9,
        "2019-09-01": 2206.3,
        "2019-10-01": 2306.5,
        "2019-11-01": 2783.8,
        "2019-12-01": 3170.7,
        "2020-01-01": 3320.6,
        "2020-02-01": 3058.5,
        "2020-03-01": 2722.0,
        "2020-04-01": 2256.9,
        "2020-05-01": 2072.2,
        "2020-06-01": 2127.9,
        "2020-07-01": 2464.1,
        "2020-08-01": 2399.5,
        "2020-09-01": 2151.2,
        "2020-10-01": 2315.9,
        "2020-11-01": 2442.0,
        "2020-12-01": 3182.8,
        "2021-01-01": 3343.9,
        "2021-02-01": 3099.2,
        "2021-03-01": 2649.4,
        "2021-04-01": 2265.1,
        "2021-05-01": 2117.4,
        "2021-06-01": 2238.4,
        "2021-07-01": 2412.2,
        "2021-08-01": 2433.8,
        "2021-09-01": 2142.3,
        "2021-10-01": 2262.6,
        "2021-11-01": 2693.3,
        "2021-12-01": 3007.3,
        "2022-01-01": 3612.1,
        "2022-02-01": 3064.2,
        "2022-03-01": 2785.4,
        "2022-04-01": 2379.3,
        "2022-05-01": 2247.8,
        "2022-06-01": 2326.9,
        "2022-07-01": 2597.9,
        "2022-08-01": 2566.1,
        "2022-09-01": 2263.3,
    }
}

data = pd.DataFrame(data)
data.rename(columns={"value": "Natural Gas Consumption(BCF)"}, inplace=True)
data.index = pd.to_datetime(data.index)
data = data.asfreq("MS")
data.head()

Natural Gas Consumption(BCF)
2013-01-012878.8
2013-02-012567.2
2013-03-012521.1
2013-04-011967.5
2013-05-011752.5

全期間を折れ線で描くと、暖房需要で 1 月に 3000 BCF 前後の山、夏に 2000 BCF 前後の谷を作る 12 か月周期と、10 年で山が 2900 から 3600 へ上がるゆるやかな上昇トレンドが見えます。

1
2
3
4
plt.figure(figsize=(12, 6))
sns.lineplot(data=data)
plt.grid()
plt.show()

Sample Dataの図

3. データの分割 #

予測性能を評価するため、2020 年 1 月より前の 84 か月を学習用、2020 年 1 月以降の 33 か月を検証用に分けます。時系列ではランダムに分けず、必ず時間順に切ります。

1
2
3
4
5
6
7
8
data_train = data[data.index < "2020-1-1"]
data_test = data[data.index >= "2020-1-1"]

plt.figure(figsize=(12, 6))
plt.plot(data_train.index, data_train.values, label="train", linewidth=2)
plt.plot(data_test.index, data_test.values, "-.", label="test")
plt.legend()
plt.grid()

To evaluate the predictive performance, the data is split su…の図

4. モデルの学習と予測 #

学習データ 84 か月に SES を当て、\(\alpha = 0.1\) を固定して 33 か月先まで予測します。fit は学習期間の各月で水準 \(\ell_t\) を更新し、forecast はその最終値を未来に置くだけです。

1
2
3
ses = SimpleExpSmoothing(data_train)
ses = ses.fit(smoothing_level=0.1)
pred = ses.forecast(33)
1
2
3
4
5
6
plt.figure(figsize=(12, 6))
plt.plot(data_train.index, data_train.values, label="train", linewidth=2)
plt.plot(data_test.index, data_test.values, "-.", label="test")
plt.plot(pred.index, pred.values, "-x", label="pred")
plt.legend()
plt.grid()

Model Training and Predictionの図

実行結果の読み方 #

× の予測線は 2400 BCF 前後の水平線になり、検証データの冬の山(3300〜3600)を 1000 以上下回り、夏の谷(2100 前後)は 300 ほど上回ります。これは SES が水準 \(\ell_t\) しか持たず、学習期間の最後の値を未来に置くだけだからです。\(\alpha = 0.1\) と小さいため、平滑水準は 2019 年 12 月の 3170.7 という高い値にはほとんど引っぱられず、過去 2〜3 年の平均に近い水準で落ち着いています。ses.params["smoothing_level"] で指定した \(\alpha\)、ses.fittedvalues で学習期間内の平滑線を取り出すと、観測の山谷より 1〜2 か月遅れて小さく波打つ線が確認できます。

使いどころと落とし穴 #

SES が向くのは、水準がゆっくり変わるだけでトレンドも季節性もない系列、たとえば在庫回転率や故障率の月次推移です。天然ガス消費量のように 12 か月周期の山谷が水準の 40% もある系列では、どんな \(\alpha\) を選んでも水平線しか出せないので、季節を持つ Holt-Winters 法 に進む必要があります。\(\alpha\) を手で決めるときは、0.1〜0.3 なら平滑化(ノイズ除去)寄り、0.7 以上なら直近追従寄りと覚え、fit() に何も渡さずに最尤推定した値と比べて判断します。最尤推定の \(\alpha\) が 0.9 を超えたら「前月の値をそのまま使う」ナイーブ予測とほぼ同じで、SES を使う意味は薄いという合図です。