トレンド処理

Timeseries

チェック2: トレンド成分を扱いやすくする

作成日: 最終更新: 読了時間: 2 分
まとめ
  • 人工データを使って、周期+トレンドが混ざった時系列を用意します。
  • 多項式フィットでトレンドを推定し、新しい列として保持します。
  • トレンドを入力特徴に加える or 取り除いた残差で学習する、2 つのスタイルを比較します。
  • XGBoost で単純な回帰を行い、MSE で効果を確認します。

1. ライブラリの準備 #

import japanize_matplotlib
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

2. サンプルデータを作る #

周期(7 日、14 日、28 日)に長期トレンドを重ねた合成データを 2 年分(720 日)作成します。

date_list = pd.date_range("2021-01-01", periods=720, freq="D")
value_list = [
    10
    + np.cos(np.pi * i / 28.0) * (i % 3 > 0)
    + np.cos(np.pi * i / 14.0) * (i % 5 > 0)
    + np.cos(np.pi * i / 7.0)
    + (i / 10) ** 1.1 / 20
    for i, di in enumerate(date_list)
]

df = pd.DataFrame(
    {
        "日付": date_list,
        "観測値": value_list,
    }
)

df.head(10)

折れ線で形状を確認します。

plt.figure(figsize=(10, 5))
sns.lineplot(x=df["日付"], y=df["観測値"])

3. トレンド推定のための特徴量を追加 #

曜日や季節性を説明するダミーを適当に作成します。

df["曜日"] = df["日付"].dt.weekday
df["dayofyear%14"] = df["日付"].dt.dayofyear % 14
df["dayofyear%28"] = df["日付"].dt.dayofyear % 28

4. 多項式でトレンドを推定する関数 #

訓練区間(ここでは 500 日)で 2 次多項式をフィットし、全期間に外挿してトレンド列を得ます。

def get_trend(timeseries, deg=3, trainN=0):
    """時系列のトレンドを多項式で近似する"""
    if trainN == 0:
        trainN = len(timeseries)

    x = list(range(len(timeseries)))
    y = timeseries.values
    coef = np.polyfit(x[:trainN], y[:trainN], deg)
    trend = np.poly1d(coef)(x)
    return pd.Series(data=trend, index=timeseries.index)


trainN = 500
df["Trend"] = get_trend(df["観測値"], trainN=trainN, deg=2)

plt.figure(figsize=(10, 5))
sns.lineplot(x=df["日付"], y=df["観測値"], alpha=0.5, label="観測値")
sns.lineplot(x=df["日付"], y=df["Trend"], label="Trend")

5. 特徴量とターゲットに分ける #

X = df[["曜日", "dayofyear%14", "dayofyear%28"]]
y = df["観測値"]

trainX, trainy = X[:trainN], y[:trainN]
testX, testy = X[trainN:], y[trainN:]
trend_train = df["Trend"][:trainN]
trend_test = df["Trend"][trainN:]

6. ベースライン: トレンドを弄らず XGBoost で学習 #

import xgboost as xgb
from sklearn.metrics import mean_squared_error

regressor = xgb.XGBRegressor(max_depth=5).fit(trainX, trainy)
prediction = regressor.predict(testX)

plt.figure(figsize=(10, 5))
sns.lineplot(x=df["日付"][trainN:], y=prediction)
sns.lineplot(x=df["日付"][trainN:], y=testy)

plt.legend(["予測値", "実測値"], bbox_to_anchor=(0.0, 0.78, 0.28, 0.102))
print(f"MSE = {mean_squared_error(testy, prediction)}")
MSE = 2.8151...

トレンドをそのままにすると周期性だけでは説明できず、誤差が大きいことが分かります。


7. 戦略A: トレンド列を外生説明変数として追加 #

trainX_ext = trainX.assign(Trend=trend_train)
testX_ext = testX.assign(Trend=trend_test)

regressor = xgb.XGBRegressor(max_depth=5).fit(trainX_ext, trainy)
pred_ext = regressor.predict(testX_ext)
print(mean_squared_error(testy, pred_ext))

8. 戦略B: トレンドを取り除いてから学習 #

最も手軽なのは 残差を学習 し、予測時にトレンドを足し戻す方法です。

regressor = xgb.XGBRegressor(max_depth=5).fit(trainX, trainy - trend_train)
prediction = regressor.predict(testX)
prediction = [pred_i + trend_i for pred_i, trend_i in zip(prediction, trend_test)]

plt.figure(figsize=(10, 5))
sns.lineplot(x=df["日付"][trainN:], y=prediction)
sns.lineplot(x=df["日付"][trainN:], y=testy)

plt.legend(["予測値(Trendを戻したもの)", "実測値"], bbox_to_anchor=(0.0, 0.78, 0.28, 0.102))
print(f"MSE = {mean_squared_error(testy, prediction)}")
MSE = 0.4601...

誤差が大幅に改善され、トレンドの扱いが重要であることが分かります。


9. 実務でのヒント #

  • トレンドを複数用意
    多項式だけでなく、移動平均やローカル回帰(LOESS)など複数のトレンド候補を列に持たせると頑健になります。

  • 学習期間とトレンド期間を揃える
    モデルが見ていない期間にトレンドを当てる場合、極端な外挿になるので注意。

  • 共変量シフトの監視
    トレンド列を入れた状態でデプロイするなら、トレンドの更新ロジックも本番環境に実装しておく必要があります。


10. まとめ #

手順目的メモ
トレンド推定(多項式)長期変動を抽出過学習に注意
戦略A: トレンドを説明変数に追加モデルに任せて重み付け学習対象が単純な場合に有効
戦略B: トレンド除去後に学習周期・ノイズのみを学習予測後に足し戻す

トレンドの扱いを変えるだけでエラーは大きく改善できるので、前処理ワークフローに必ず組み込んでおきましょう。