時系列予測システムProphet新機能、外部説明変数を使った時系列予測

2017-9-13にProphetがv0.2へアップグレードされたときに機能がいくつも追加されました。
その追加の中で使えそうな機能の一つとして
『外部説明変数(additional regressor)を追加できるようになった』
があります。これについて使い方の紹介とデモをやってみます。

【予測モデル】

従来のProphetの予測モデルは下記の数式で表されるモデルでした。

y(t) = g(t) + s(t) + h(t) + \epsilon

ただしそれぞれ

です。
Prophetは過去に起こった値の変化を学習して、学習結果を未来に向かって延長するという予測モデルです。
我々の予測問題は既に持っている過去のtについてのy(t)を使って、未来のtについてのy(t)を推定することです。

しかし予測したい数字が、ある時間変化する要因に影響を受けることが分かっており、その変数の未来の値が分かっている場合、その要因を使って予測精度を向上させられる可能性があります。
よくある回帰分析もこの形です。
その場合、prophetに予測項を単純に線型結合したモデル

y(t) = g(t) + s(t) + h(t) + \beta x(t)

になります。ただしx(t)が時刻tでの予測したい値に影響を与える外部要因です。
\betaがモデルパラメータです。いわゆる回帰係数です。
prophetは一般化加法モデルを用いているため、このように必要に応じて要因を足し算で追加するだけでいくらでもモデルを変化させることができるのです。

これが外部説明変数を追加したProphetモデルです。
普通の線形回帰をするように説明変数を追加するだけですので簡単に使えそうです。
繰り返しになりますが、外部説明変数は過去未来のすべての日の値が分かっていることが必要です。

【予測】

予測対象は日経平均株価の終値、そして外部変数は日銀のETF購入金額(累積)とします。
時価総額50〜70兆円の東証に対して、累計20兆円近くの日銀の買いがどれほど影響を与えているのでしょうか。
この影響の程度が分かれば、もし日銀の金融政策の変更があったときに株価への影響を見積もることができるかもしれません。

上でも説明したとおり外部変数として日銀ETF買入金額を使うためには日銀ETF買入れ金額の将来の値が必要です。
ただ幸いなことに日銀様はETF買入金額を予告(年間6兆円)していますし、大きな偏りも無いので将来の日銀ETF買入金額もprophetで予測するというprophet予測の二段構えでいくという試みにチャレンジします。
prophetの統計モデルはスプラインフーリエ変換の合わせ技なので、二段構えにしたとしても誤差の間に妙な相関が発生することは無いのではなかろうと期待します。

日銀ETF買入金額の予測は別記事で行いましたので詳細が見たい方はそちらをご確認ください。
時系列予測Prophetを使って年末の日銀のETF保有額を予測する
見た雰囲気では予測結果に問題はなさそうです。

#R
library(tidyverse)
library(lubridate)
library(prophet)

nikkei <- read.csv("data/nikkei-2017.csv", stringsAsFactors = FALSE)
nichigin_etf <- read.csv("data/nichigin_etf.csv", stringsAsFactors = FALSE)

# 日銀買入れ金額の予測
m <- nichigin_etf %>%
  select(ds = date, y = etf_holding) %>%
  prophet(daily.seasonality = FALSE)
future <- make_future_dataframe(m, periods = 365, include_history = FALSE)
fcst <- predict(m, future) %>%
   transmute(ds = parse_date(ds), nichigin = yhat) %>%
   mutate(flag = "forecast")
nichigin_tbl <- nichigin_etf %>%
   transmute(ds = date, nichigin = etf_holding) %>%
   mutate(flag = "history") %>%
   bind_rows(fcst) %>%
   mutate(nichigin = log(nichigin))

# 日経平均株価取得
nikkei <- read.csv("data/nikkei-2017.csv", stringsAsFactors = FALSE) %>%
   transmute(ds = ymd(date), y = close) %>%
   mutate(y = log(y))

# 外部変数と結合して予測元データセットを作成
df <- left_join(nikkei, nichigin_tbl, by = "ds")
df %>% arrange(ds) %>% head
#          ds        y nichigin    flag
#1 2013-01-04 9.276887 9.594514 history
#2 2013-01-07 9.268516 9.594514 history
#3 2013-01-08 9.259898 9.594514 history
#4 2013-01-09 9.266586 9.594514 history
#5 2013-01-10 9.273563 9.594514 history
#6 2013-01-11 9.287447 9.594514 history

これで予測の準備が完了しました。
予測対象のデータdfに予測対象の値y, 外部変数nichiginが各日付dsごとに入っています。
これを使って予測モデルを作成しましょう。
予測ステップを一つずつ説明します。

# 空の予測モデルを作る。
m <- prophet(weekly.seasonality = FALSE, daily.seasonality = FALSE)

# 外部変数として列名"nichigin"を設定
m <- add_regressor(m, "nichigin")

# prophetモデルの元データとして上で作成したdfを設定
m <- fit.prophet(m, df)

# 予測したい日数をprophetライブラリ同梱のmake_future_dataframe関数を使って作る。
# そしてこちらにも予測に使うべき外部変数として列"nichigin"を設定する。
future <- make_future_dataframe(m, periods = 368) %>%
        mutate(ds = parse_date(ds)) %>%
        left_join(nichigin_tbl, by = "ds")

# 予測
forecast <- predict(m, future)

# 図示
left_join(forecast, m$history, by = "ds") %>% 
   ggplot(aes(ds)) + 
     geom_point(aes(y = exp(y))) + 
     geom_line(aes(y = exp(yhat)), color = "blue")
外部変数として日銀ETF買入累計金額を使った日経平均株価の予測。点が実績で線が予測

どわー!

過去部分のフィッティングは良いです。つまりうまく学習できているようです。
しかし2018年末には日経平均株価は34,000ですか。そうですか。
どえらい予測になってしまいました。もちろんこれは日銀のETF買入れペースが継続する前提です。

学習結果の内容をしておきます。

外部変数として日銀ETF買入累計金額を使った日経平均株価の予測の要因分解。スケールは対数。一番下のextra_regressorsが日銀ETF買入れ累計金額(対数)の影響

一番下のextra_regressorsが日銀ETF買入れの影響です。この数値が株価の予測に加算されます。
驚くべきことにこの数値は時間の流れに対して右肩下がりになっています。
日銀のETF買入れは時間を追うごとに追加されているわけですから、これはなんと日銀がETF買入れを買い入れるほど株価は下がるということを意味しています。
これは直感に反する結論です。

しかし下に示す過去の外部変数無しでの日経平均株価の予測と比較するとトレンド項の複雑な動きが無くなり、学習が改善していると解釈することもできないこともありません。

日経平均株価をデフォルト設定でProphet予測した結果の要因分解

しかしデータの意味を考えれば学習内容が良いとは言えないと思います。
この現象の原因について色々と考えてはみましたが、一つの可能性はtrend項が外部変数の影響を吸収してしまったことではないでしょうか。
trend項に日銀のETF買入れトレンドが織り込まれてしまったため、extra_regressorsの項がおかしな学習結果になってしまったのです。

【モデル調整】

上の予測においてprophetのtrend項がextra_regressorsの項のトレンドを吸収してしまった可能性があります。
もしそうなのであればtrend項の動きを抑制し柔軟性を減少させることで、学習過程で置きてしまった箇条なフィッティングを避けられるかもしれません。
trend項の動きを抑制するためにはchangepoint.prior.scaleを小さくします。

m <- prophet(weekly.seasonality = FALSE, daily.seasonality = FALSE, changepoint.prior.scale = 0.001)
m <- add_regressor(m, "nichigin")
m <- fit.prophet(m, df)
future <- make_future_dataframe(m, periods = 368) %>%
        mutate(ds = parse_date(ds)) %>%
        left_join(nichigin_tbl, by = "ds")
forecast <- predict(m, future)
外部変数として日銀ETF買入累計金額を使った日経平均株価の予測の要因分解。日銀ETF買入れ金額が正の効果を持つようにパラメータを調整。スケールは対数。一番下のextra_regressorsが日銀ETF買入れ累計金額(対数)の影響
外部変数として日銀ETF買入累計金額を使った日経平均株価の予測。日銀ETF買入れ金額が正の効果を持つようにパラメータを調整。点が実績で線が予測

日銀ETF買入れの影響がプラスになりました。
今回得られた結果はトレンド項が2016年前後に激しく減少した後で横ばいになっています。
2016年以降の株価上昇は日銀のETF買入れの影響であって、真のトレンドは横ばいだったのだ、と解釈することができそうです。

【異なるシナリオ】
日銀がETF買入れを止めてしまったらどうなるのかを考えてみます。

上で行った日銀の買入れは現在の買入れが継続すると仮定したときの将来値を使いました。

今の買入れトレンドが継続すると仮定した上での日銀ETF買入れ金額

仮に『2018年は買い入れなし!』と政策が変更された場合には日銀ETF買入れは次のようになると思われます。
日銀が買入れを止めてしまうと仮定したときの日銀ETF買入れ金額

外部変数を2つめに差し替えてprophet予測を行えばもし日銀が買入れを止めてしまったとき株価がどうなるかを予測することができるでしょう。

last_value <- nichigin_tbl %>% filter(flag == "history") %>% pull(nichigin) %>% last()
nichigin_tbl <- nichigin_tbl %>% mutate(nichigin2 = if_else(flag == "forecast", last_value, nichigin))
df2 <- left_join(nikkei, nichigin_tbl, by = "ds")
m2 <- prophet(weekly.seasonality = FALSE, daily.seasonality = FALSE, changepoint.prior.scale = 0.001)
m2 <- add_regressor(m2, "nichigin2")
m2 <- fit.prophet(m2, df2)
future2 <- make_future_dataframe(m2, periods = 368) %>%
        mutate(ds = parse_date(ds)) %>%
        left_join(nichigin_tbl, by = "ds")
forecast2 <- predict(m2, future2)

日銀が買入れを止めてしまうと仮定したときの日経平均株価予測

やはり株価は横ばいになってしまうようです。
日本の未来のために是非とも買入れを続けてほしいものですね。

全く同じ方法で日銀が買入れたETFをすべて市場に放出した場合にはどうなるかを予測することもできます。
私は恐ろしいのでやりませんが。

【まとめ】

外部説明変数は簡単に使えるかと思っていたのですが、実際にやってみて全然そんなことは無いことがよく分かりました。
特に【モデル調整】の節で行ったパラメータ調整がとても恣意的です。
少しパラメータを調整するだけで真逆の学習結果が出ました。
こういう状況で予測を信用するのは少々危険だと感じなければなりません。

外部説明変数もまたトレンドを持っている場合には、予測本体のトレンドの分析と干渉を起こします。
ここから学ぶべき注意点はトレンドを持つ外部変数は使うのは危ないということでしょう。
行ったり来たりと中立な変数の方が無難に使えると思います。
ただあまり良い例が思いつかないので、現実的に使いにくい機能なのかもしれません。
そのうち良い例が思いついたら分析してみたいと思います。

関連記事

参考サイト

免責事項

当ブログの内容をご利用されて何らかの損失が発生しても一切の責任を負いかねます。

コメントを残す