DATAFLUCT Tech Blog

データ技術・データサイエンス・MLOps に関するトレンドを追いかけます

時系列分析をお手軽に!機械学習ライブラリDartsの実演

こんにちは!

以前にDartsという時系列分析に特化したpythonライブラリを紹介しました。

前編はこちら

今回は実際にDartsを動かしていきましょう。

Darts内にもデータセットがありますが、公式でも触れられているのであえて、外部のデータを参照してみましょう。導入編でも触れたアイスクリームの生産量の変化を推測したいと思います。

アイスクリームのデータセットはこちら

上記リンクの上部右側Downloadからcsvをダウンロードしてください。

Dartsのインストールは以下の1コマンドです。Windowsではデフォルトのコマンドプロンプトでうまくインストールが終了しなかったので、WSL環境などを推奨します。

$ pip install darts

ARIMAで学習してみる

まずは、導入編で最初に触れたARIMAで学習してみましょう。 ARIMAは回帰モデルと移動平均モデルを和分をとって組み合わせた分析手法です。導入編でも触れたため細かいことは割愛しますが、時系列分析では広く慣れ親しまれた手法です。

必要なツールをインポートし、データセットを用意します。

import pandas as pd
import matplotlib.pyplot as plt


from darts import TimeSeries
from darts.models import ARIMA # 本節で要のARIMAです

df = pd.read_csv('IPN31152N.csv') 
series = TimeSeries.from_dataframe(df, 'DATE', 'IPN31152N') # dartsのTimeSeriesに流し込み

アイスクリームの生産量のデータは以下のようになっています。月ごとの生産量を1972年1月から2021年12月までを記録しています。単位は2017年の生産量を100と見た時のそれぞれの値です。

今回は直近3年分を予測してみましょう。 データが2021年までなので1972年から2018年までを訓練データとします。

train, val = series.split_after(pd.Timestamp('2018-01-01')) # 訓練データと、検証データに分割 

model = ARIMA() # ARIMAモデルのインスタンス作成 
model.fit(train) # 訓練 

predict = model.predict(len(val)) # 予測したい日付ではなく予測したい月数を渡すことに注意

ここまでで予測値の算出が終わりました。 予測の結果がどうなっているかグラフにして見比べてみましょう。

plt.figure(figsize=(12,6)) # 図の描画サイズを決定 

series.plot(label='actual', lw=0.5) # 実際の値を描画 
predict.plot(label='predict', lw=1) # 予測値を描画 

plt.legend() 
plt.xlabel('year') 
plt.ylabel('Index 2017=100')

以上まででARIMAで学習して予測値を算出するところまでを終えました。描画した結果の予測値が青線です。重なって見づらいですが実際の値の黒線と見比べてみましょう。なんとなく同じような波形になっていてそれなりに予測ができていることが分かります。

これ単体ではモデルの評価が難しいので次節でARIMAの分析がどの程度優れていたかを考察していきます。

機械学習をやったことのある方は実際に書くコードの少なさに驚きませんでしたか?

Dartsを含めたAutoMLはこのように短いコードで機械学習の実行ができます。

バックテストでモデルの選定を行う

実際にARIMAで予測を行ってみましたが、これがどの程度いい推測ができているのか、モデルの評価をすることは比較対象がないと難しいですよね。

本節ではいくつかのモデルの精度を評価して先ほどのARIMAと比較してみましょう。

先ほどのARIMAに加えてDartsの導入編で触れたFFTとProphetの3つを用いてバックテストを行ってみます。FFTは波形を三角関数の組み合わせと考え、分解して統計を取る手法です。Prophetは月や週などの季節性や特定の休日の影響などを検出し、それらを組み合わせた加法モデルによって予測値を算出します。

バックテストという手法は少しずつ予測を進めていきながら各地点で、実測値と予測値の誤差をチェックすることを言います。

例えば、2010年1月から2010年12月の値で2011年1月の値を予測する、そのあとに2010年2月から2011年1月の値で2011年2月の値を予測する。 このように少しずつ予測を進めながらモデル全体の精度を評価します。以下にイメージを置きました、一定区間で学習して予測値を出し、少しずらしてまた学習から行うを繰り返しています。

なぜこのような手法をとるのかというと、先ほど1972年から2018年までを訓練データに次の3年間を予測しときたまたま当てはまりがよくて、違う期間の1990年から2000年までを訓練データに次の3年間を予測した場合には、全く予測できていないといった事態を避けるためです。

つまり、バックテストを行うことでいわゆる過学習になっていないかなどを見極めることができます。早速やってみましょう。必要なツールをインポートします。

import pandas as pd 
import datetime as dt 

import matplotlib.pyplot as plt 
from darts import TimeSeries 

from darts.models import (    
    ARIMA,    
    FFT,    
    Prophet 
)

前述したようにバックテストでは様々な区間で何度もモデル作成と予測を行うため、かなりの時間を要します。今回は試しに軽く触りたいので2010年から2021年の11年間の短い区間でバックテストを行いましょう。

df = pd.read_csv('IPN31152N.csv') 

df['DATE'] = pd.to_datetime(df['DATE']) # 日付の比較をしたいためdatetimeに変換 
df = df[df['DATE'] >= dt.datetime(2010,1,1)] # 2010年01月01日以降に絞り込み 

series = TimeSeries.from_dataframe(df, 'DATE', 'IPN31152N')

ではこのシリーズを3つの各モデルに流し込みながら、モデルを評価しましょう。

Dartsの優れている点ですが、ほとんどのモデルを統一的に扱えるようにしているため、それぞれ個別にコードを書かず、同様の手順で訓練を行えます。(深層学習ではデータの正規化が必要など一部のモデルでは工夫が必要なので注意してください)

そのため下記コードのように配列でループして、ループ内で同じ処理でバックテストを回すことが可能です。

models = [   
    ARIMA(),    
    FFT(),
    Prophet() 
] # 上記配列のARIMA, FFT, Prophetからhistorical_forecasts()を実行させていきます 

backtests = [    
    model.historical_forecasts(series=series) for model in models 
]

backtestというメソッドもありますが今回は予測値を描画して比較したいため、各時間の予測値を返却してくれるhistorical_forecastsメソッドを利用してモデル評価を行います。

from darts.metrics import mape 

plt.figure(figsize=(12,6)) 
 
series.plot(label="series", lw=0.5) # historical_forecasts()で得た予測値をそれぞれ描画する 
for i, m in enumerate(models):
    err = mape(backtests[i], series)
    backtests[i].plot(lw=3, label=f'{type(m).__name__}: MAPE={err:.2f}%') 

plt.legend()

dartsのバックテストでの評価指標はデフォルトがMAPEなのでそれに倣ってMAPEを算出して描画してみましょう。MAPEをざっくり説明すると、そのモデルで何パーセント予測を外しているかを表したものです。つまり値が小さいほどモデルの精度が高いといえます。

余談ですがMAPEを利用することで異なる時系列データでもモデルの予測精度を比較することができます。例えば、気温の時系列データと湿度の時系列データでモデルを作って両者のモデル比較をしようとした場合、MAPEでは評価が可能です。

RMSEなど他の評価指標の場合は別空間に対するモデルの精度比較は破綻してしまうので注意です。基本は同時系列上で精度比較することが多いと思うので深くは考えなくて大丈夫です。

結果はこのようになります。今回はバックテストを1レコードずつ進めたので2016年あたりから全ての時間で予測値が取得されています。

右上にモデル名とMAPEの値を描画しています。この中ではARIMAを使うことが最も精度のいい予測ができるといえますね。historical_forecastsのパラメータで描画結果の変更は可能です。

一回の計算のあとに進めるレコード数を変えたければstrideの値を指定してください。さらにデフォルトはstart=0.5なので真ん中の2016年あたりから予測値を算出していますが、もっと前の地点から予測値を見たいのならstart=0.3などにしてください。

ほかのパラメータは公式ドキュメントをご覧ください。

historical_forecastsのパラメータ

あくまでこの比較は今回のデータセットにおける評価だという点、またハイパーパラメータなど何も設定していないので、適切な設定をすれば、ARIMA以外のモデルのほうが精度が高くなるかもしれません。

ちなみにMAPEの値のみ欲しければ以下の記述で取れます。

backtests_only_mape = [
    model.backtest(series=series)    for model in models 
]

RNNで共変量を扱ってみる

導入編では共変量を扱えることがDartsの強みと紹介しました。 導入編で触れたものの今回まだモデルの作成を行っていないRNNにて、共変量を用いた分析を行ってみましょう。

安直ですが、アイスクリームの生産量は温度変化に左右されるということで、未来共変量として気温のデータを入れてみます。(月ごとのデータですので一ヵ月先の気温を未来共変量ととらえるのには無理があるかもしれませんが、厳密な分析を行うことが本稿の目的ではないのでその点は目をつぶってください)

また、共変量部分で使う気候変動のデータセットは以下になります。

気候変動のデータセットはこちら

上部右側にDownloadボタンがあります。Kaggleの登録が必要な点ご注意ください。 複数データがありますが本稿では、GlobalLandTemperaturesByState.csvを利用します。

まずは必要なツールのインポートします。

import pandas as pd 
import datetime as dt 
from darts import TimeSeries 
import matplotlib.pyplot as plt 
from darts.models import RNNModel 
from darts.dataprocessing.transformers import Scaler

次にデータセットを読み込みます。今回共変量に使う気温のデータセットは2013年までなのでキリよく1972年1月から2010年1月までを扱いましょう。

df = pd.read_csv('IPN31152N.csv') 

df['DATE'] = pd.to_datetime(df['DATE']) 
df = df[df['DATE'] <= dt.datetime(2010,1,1)] 

series = TimeSeries.from_dataframe(df, 'DATE', 'IPN31152N')

次に共変量の用意をします。

Dartsでは共変量としてTimeSeriesをセットしたら自動的に必要な時間だけを参照するようになっているので特に加工は必要ないですが、後でグラフとして描画したいので最低限の範囲に切り取ります。RNNは深層学習で数か月分のインプットから予測を繰り返して学習を進めていきます。input_chunk_lengthは24か月分でoutput_chunk_lengthを12か月分にします。

この後設定する値は公式ガイドの同じく深層学習であるN-BEATSの項目を参考にしています。 Darts内のN-BEATSのデモンストレーション

今回扱うアイスのデータセットが1972年からでRNNでinput_chunk_lengthを24で参照したいため24個前までのデータセットを求められます。

つまり1972年1月に対して24か月前の1970年1月から共変量の値が必要ということになります。以下にイメージを置いておきます。

また、未来共変量としてセットするのでアイスの最後のデータ2010年1月に対して、一つ進んだ2010年2月までをデータとして使う必要があります。

cov_df = pd.read_csv('GlobalLandTemperaturesByState.csv')

# アイスはアメリカの生産量なのでアメリカの気温に絞り込み
cov_df = cov_df[cov_df['Country'] == 'United States']
# アメリカは広いので平均値をとりたいですがとりあえずニューヨークを参照
cov_df = cov_df[cov_df['State'] == 'New York']

cov_df['dt'] = pd.to_datetime(cov_df['dt'])
cov_df = cov_df[cov_df['dt'] >= dt.datetime(1970,1,1)]
cov_df = cov_df[cov_df['dt'] <= dt.datetime(2010,2,1)]

cov_series = TimeSeries.from_dataframe(cov_df, 'dt', 'AverageTemperature')

必要箇所に絞ったCSVのイメージは以下になります。

まずは共変量なしのモデルを作成しましょう。RNNの場合、正規化して取りうる値を0 ~ 1に収める必要があります。今回はScalerを使って正規化を行いましょう。

train, val = series.split_after(pd.Timestamp('2007-01-01')) 

scaler = Scaler() 
ts_tr = scaler.fit_transform(series) # 全体の正規化(あとで描画するときに利用) 

train_tr = scaler.transform(train) # 訓練データの正規化 
val_tr = scaler.transform(val) # 検証データの正規化 

model = RNNModel(
    input_chunk_length=24,              # 1回の訓練で読み込むデータ数、今回は24か月分
    output_chunk_length=12,           # 1回の訓練で予測するデータ数、今回は12か月分
    n_epochs=200,                                  # 訓練の繰り返し数
    torch_device_str="cuda",             # GPUを使う場合に指定(使わないならコメントアウト)
    random_state=64,                           # 毎回実行結果が変わるので乱数の固定 
) 

model.fit(train_tr) 

predict = model.predict(n=len(val_tr))

次に共変量ありのモデルを作成します。

scaler_cov = Scaler() 
cov_series_tr = scaler_cov.fit_transform(cov_series) # 共変量の正規化 
model_cov = RNNModel(
    input_chunk_length=24,
    output_chunk_length=12,
    n_epochs=200,
    torch_device_str="cuda",
    random_state=64 
) 

model_cov.fit(
    train_tr,
    future_covariates=cov_series_tr # 未来共変量を設定 
) 

predict_cov = model_cov.predict(
    n=len(val_tr),
    future_covariates=cov_series_tr # 未来共変量を設定 
)

最後に描画して両者を比較しましょう。

plt.figure(figsize=(12,6)) 

ts_tr.plot(label='actual', lw=0.5) 

predict.plot(label='predict', lw=1) 
predict_cov.plot(label='predict_cov', lw=1) 

cov_series_tr.plot(label='cov', lw=0.5) # ついでに共変量も描画 

plt.legend() 
plt.xlabel('year')
plt.ylabel('production')

水色が共変量として設定した気温の変化です。青線が共変量なしで、紫線が共変量ありの予測結果です。 これだけではどちらがすぐれているか分からないので、評価指標を用いて実際の値との差を見てみましょう。

先ほどbacktestではMAPEを使ったので今回はRMSEを使ってみましょう。RMSEは、誤差の二乗から平均をとって、最後にその平方根をとったものです。値が小さいほどモデルの精度が高いといえます。

from darts.metrics import rmse 

print(f'共変量なし:RMSE={rmse(predict, val_tr):.3}'print(f'共変量あり:RMSE={rmse(predict_cov, val_tr):.3}')
  • 共変量なし:RMSE=0.123
  • 共変量あり:RMSE=0.0673

共変量を使った場合のほうがRMSEが小さくなりましたね。今回はたまたま精度が上がりましたが共変量も適切なものを設定しないと精度が悪くなる可能性もあります。共変量として適切かは、吟味して判断しましょう。

Dartsではこのように共変量を用いての時系列分析をサポートしており、その点がほかのAutoMLとは差別化されています。(2022年03月時点)

まとめ

今回はAutoMLのDartsを使って実際に時系列分析を行ってみました。導入編で解説したDartsの強みは簡単に時系列分析を行えること、そして共変量を用いた分析を行えることでした。実際の工程を見ていかがだったでしょうか?

解説したモデル以外にもたくさんのモデルがあり、今回行わなかったパラメータ設定、時系列分析に役立つ便利な機能などなど、Dartsはまだ多くの機能を有しています。興味のある方は公式ドキュメントを参照してみてください。

Dartsの公式ドキュメントはこちら