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

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

言語 JA

Holt法

中級

04.12.2

Holt法

最終更新 2020-02-12 読了時間 6 分
まとめ
Holt の線形指数平滑法は、SES の水準 \(\ell_t\) に傾き \(b_t\) を 1 本足し、\(h\) か月先の予測を \(\ell_T + h, b_T\) という直線で伸ばす手法です。水準は \(\alpha\)、傾きは \(\beta\) という 2 つの平滑係数でそれぞれ更新するので、右肩上がりや下降傾向のある系列に SES より追随します。このページでは米国の天然ガス消費量(月次 BCF)を 2020 年 1 月で学習と検証に分け、SimpleExpSmoothing(α=0.1) と Holt(α=0.1, β=0.2) の 33 か月予測を 1 枚に重ねます。SES の水平線に対して Holt の予測は傾いた直線になりますが、12 か月周期の季節の山谷はどちらも表せないことが図で確認できます。

単純指数平滑法 で水準 \(\ell_t\) を \(\alpha\) で更新する仕組みを先に押さえておくと、Holt 法が「同じ更新を傾き \(b_t\) にも行うだけ」だと分かります。

直感 #

天然ガス消費量は 10 年で冬の山が 2900 から 3600 BCF へ上がっており、SES のように「最後の水準を横に伸ばす」と、予測は年を追うごとに実測を下回ります。Holt 法はこの上昇を「毎月およそ +5 BCF」という傾きとして別に覚えておき、予測を 1 か月進めるごとに傾きの分だけ足し上げます。傾きの更新式は \(b_t = \beta(\ell_t - \ell_{t-1}) + (1 - \beta) b_{t-1}\) で、水準の変化量を \(\beta\) の重みで取り込む点は SES の更新と同じ形です。\(\beta = 0.2\) なら直近 1 か月の変化を 2 割だけ信じるので、季節の山谷で水準が月に 500 動いても傾きが急には振れません。

1. ライブラリ #

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

from statsmodels.tsa.holtwinters import Holt
from statsmodels.tsa.holtwinters import SimpleExpSmoothing

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

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

見るポイント:水準 → 傾き → 季節 と要素を足すほど、未来への伸ばし方が豊かになる。

サンプルデータ #

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 月の山が 2900 → 3600 BCF と 10 年で上がる右肩上がりのトレンドと、12 か月周期の季節の波が重なって見えます。Holt 法が狙うのはこのうち前者のトレンドです。

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

サンプルデータの図

データの分割 #

データの末尾(2020 年 1 月以降の 33 か月)を予測性能の検証に使い、2013〜2019 年の 84 か月で学習します。2020 年以降を丸ごと検証に回すのは、未来の値を学習に混ぜないためです。

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()

データの末尾(2020年以降のデータ)を予測性能の検証に使用するためにデータを分割しますの図

モデルの学習と予測 #

同じ学習データに SES と Holt 法を当て、どちらも 33 か月先を予測します。SES は \(\alpha = 0.1\) のみ、Holt は \(\alpha = 0.1\) と \(\beta = 0.2\) を固定し、差がトレンド成分だけから生まれるようにします。

1
2
3
4
5
6
7
ses = SimpleExpSmoothing(data_train)
ses = ses.fit(smoothing_level=0.1)
ses_pred = ses.forecast(33)

holt = Holt(data_train)
holt = holt.fit(smoothing_level=0.1, smoothing_trend=0.2)
holt_pred = holt.forecast(33)
1
2
3
4
5
6
7
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(ses_pred.index, ses_pred.values, "-x", label="prediction(ses)")
plt.plot(holt_pred.index, holt_pred.values, "-x", label="prediction(Holt)")
plt.legend()
plt.grid()

モデルの学習と予測の図

実行結果の読み方 #

SES の予測(水平線)は 2400 BCF 前後に留まり、Holt の予測は学習最終月の水準から傾き \(b_T\) の分だけ毎月上がる直線になります。学習期間の最後の月は 2019 年 12 月(3170.7)で季節の山に向かう途中なので、\(\alpha = 0.1\) で平滑化した水準はそれより低く、傾きは 10 年の上昇トレンド(月 +4 前後)に近い値になります。検証データの 33 か月と比べると、Holt の直線は冬の山(3300〜3600)と夏の谷(2100〜2300)の真ん中あたりを通り、SES より年々の水準の上昇には追随しますが、山谷そのものは表せません。holt.params を表示すると smoothing_level と smoothing_trend のほか、推定された初期水準 initial_level と初期傾き initial_trend が確認できます。

使いどころと落とし穴 #

Holt 法が向くのは、売上や利用者数のように一定の割合で増減し続け、季節の山谷が小さい系列です。天然ガス消費量のように水準の 40% にあたる 12 か月周期の波があるデータでは、トレンドを捉えても誤差の大半は季節が占めるので、次の Holt-Winters 法 で季節成分 \(\gamma\) を足す必要があります。もう一つの落とし穴は傾きを 33 か月も直線で伸ばすことで、学習最終月の傾きがたまたま急だと予測が現実から離れていきます。長期の予測では Holt(data_train, damped_trend=True) で減衰トレンドにし、遠い未来ほど傾きを弱めるのが安全です。\(\beta\) を手で決めるときは 0.1〜0.3 から始め、fit() の最尤推定値と比べて極端に違わないかを確かめます。