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

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

言語 JA

トレンド処理

入門

04.11.2

トレンド処理

最終更新 2025-11-05 読了時間 5 分
まとめ
周期だけを説明変数にした回帰モデルは、右上がりのトレンドを持つ系列をうまく予測できません。このページでは分母 7・14・28 の 3 本の cos 波にゆるい上昇トレンドを重ねた 720 日の人工データを作り、np.polyfit(deg=2) で推定したトレンド列を XGBRegressor にどう渡すかを 3 通り比べます。トレンドを無視したベースラインの MSE は 2.82 で、トレンドを説明変数に足す戦略 A で改善し、トレンドを引いた残差を学習して予測後に足し戻す戦略 B では 0.46 まで下がります。木モデルは学習範囲の外の値を出せないので、外挿が必要なトレンドは別に扱うのが要点です。

データの状態を把握する で np.polyfit による get_trend 関数と ADF 検定を先に見ておくと、このページの「トレンドを分けて渡す」理由がはっきりします。


直感 #

毎週の曜日パターンと、毎月少しずつ増える売上を同時に持つ店を想像してください。「曜日」だけを入力にしたモデルは「月曜は低く土曜は高い」という形は学べますが、半年後に全体が 10% 上がっていることは入力のどこにも書かれていないので予測できません。特に XGBoost のような木モデルは、学習データで見た値の範囲しか出力できず、右上がりの先を「伸ばす」ことが原理的にできません。そこでトレンドは多項式で別に推定し、入力列として渡すか(戦略 A)、先に引いておいて予測後に足し戻す(戦略 B)かのどちらかにします。

1. ライブラリの準備 #

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

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

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

見るポイント:元データ = トレンド + 季節成分 + 残差。残差に残った大きな山が異常の候補。

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

分母を 7・14・28 にした 3 本の cos 波に長期トレンドを重ねた合成データを 2 年分(720 日)作ります。答えを知っているデータなので、後でモデルが「周期は学べたがトレンドは学べなかった」ことを切り分けて確認できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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)

折れ線で形状を確認します。14 日周期の細かい波の上に、右に行くほど全体が持ち上がる形が見えます。

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

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

周期性を説明する特徴量として、曜日と「通算日を 14・28 で割った余り」を作ります。データを作った周期(14・28・56 日)に対応させた、答えを知っているからこそ選べるダミーです。

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

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

訓練区間(ここでは 500 日)だけで 2 次多項式をフィットし、全 720 日に外挿してトレンド列を得ます。テスト期間を使ってフィットすると「答えを見た」ことになるので、trainN で切るのが重要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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. 特徴量とターゲットに分ける #

周期を表す 3 列を X、観測値を y とし、500 日目で学習とテストに分けます。トレンド列も同じ位置で分けておき、戦略 A・B で使います。

1
2
3
4
5
6
7
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 で学習 #

周期の 3 列だけで XGBRegressor を学習し、テスト 220 日を予測します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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...

トレンドをそのままにすると、周期の 3 列だけでは右上がりを説明できず、MSE 2.82 と大きな誤差が残ります。


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

多項式トレンドを 4 つ目の説明変数として渡し、木モデルに「トレンドが高いときは値も高い」という分岐を作らせます。

1
2
3
4
5
6
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: トレンドを取り除いてから学習 #

もっとも手軽なのは、観測値からトレンドを引いた残差を学習し、予測時にトレンドを足し戻す方法です。木モデルには「周期だけ」を任せ、外挿が必要なトレンドは多項式に任せます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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...

誤差が 2.82 から 0.46 に下がり、同じ XGBRegressor(max_depth=5) でもトレンドの渡し方で結果が 6 倍変わることが分かります。


9. 実務でのヒント #

実データでは、多項式 1 本に頼らず、移動平均やローカル回帰(LOESS、statsmodels の lowess)など複数のトレンド候補を列に持たせておくと、どれか 1 つが外れても全体が崩れにくくなります。学習期間とトレンドのフィット期間は必ず揃え、500 日で学んだモデルに 1,000 日分外挿した多項式を渡すような極端な外挿は避けます。トレンド列を入れた状態でデプロイするなら、日が進むごとに trend_test を再計算する更新ロジックも本番環境に実装しておかないと、固定された古いトレンドを足し続けて予測が徐々にずれます。


10. まとめ #

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

XGBRegressor のような木モデルは学習範囲の外の値を出せないので、右上がりのトレンドを持つ系列では MSE 2.82 のベースラインのように予測が下にずれます。np.polyfit(deg=2) を学習期間 500 日だけで当てたトレンドを、説明変数として足す(戦略 A)か、引いてから学習して足し戻す(戦略 B)かで対処でき、この例では戦略 B が MSE 0.46 まで改善しました。トレンドの扱いは前処理ワークフローの最初に組み込み、外挿の期間と更新の仕組みも一緒に決めておきます。