LightGBMで時系列予測するとき、なぜ目的変数を「変化率」にするのか
売上を当てたいなら、売上を学習させるだけでは、トレンドを持つ非定常性(※後程説明)のデータを上手に予測することはできません。そこで多くの場合はトレンドを持つ非定常性を緩和するために、変化率を目的変数にします
同じところで引っかかる人がいそうなので、記事にしました。
なぜ、トレンドを持つ非定常性のデータは予測できないのか
決定木は、学習データにあった値の範囲でしか予測を出せません。
売上が右肩上がりのデータで売上そのものを予測させると、学習期間の最大値を超える予測が原理的に出せなくなります。変化率に変換すると、この制約に引っかかりにくくなります。
理由を順に見ていきます。
1.木モデルは外挿できない
外挿とは、学習データの範囲の外側を予測することです。
LightGBMの予測は、雑にまとめるとこういう動作をしています。
-
売上 > 1000?のような条件分岐で、データを葉(木の末端)に振り分ける - その葉に入っている学習データの目的変数の平均を返す
返ってくるのは学習データの平均値のどれかでしかなく、それを超える値は絶対に出てきません。
たとえば学習期間の月次売上が800〜1200万円だったとします。このとき、モデルが返せる予測もその範囲に収まります。実際には事業が成長して売上が3000万円になっていても、予測は1200万円あたりで頭打ちです。
線形回帰なら直線を伸ばして範囲外も予測できますが、木にはそれができません。これが「木モデルは外挿できない」と言われる理由です。
定常性という言葉
ここで出てくるのが定常性です。時系列の統計的な性質が時間によらず一定であること、と定義されます。厳密には次の3つが時刻に依存しないことを指します。
- 平均が一定
- 分散が一定
- 自己共分散がラグの大きさだけで決まり、時刻には依存しない
非定常はその逆で、これらが時間とともに変わる状態です。一番よく出会うのはトレンドを持つ系列、つまり平均自体が年々上がっていくケースです。
さっきの売上はまさにこれです。平均が上がり続けているので、未来がどんどん学習データの範囲から出ていきます。
変化率にすると何が変わるか
売上にトレンドがあるとき、構造はだいたいこういう形をしています。
$r_t=\frac{Y_t-Y_{t-1}}{Y_{t-1}}$
掛け算で伸びていくので、Y_t は平均も分散も時間とともに膨らみます。
ここで変化率を取ります。
$r_t = (Y_t − Y_{t-1}) / Y_{t-1}$
成長率がおおむね一定なら、$r_t$は「5%前後」といった水準の周りを上下するだけの系列になります。平均が時間に依存しなくなり、分散も売上の大きさに引きずられなくなる。非定常性の主要因であるトレンドが消えるわけです。
木モデルから見ると、これで「未来の値も学習データの範囲に収まる」状態になります。予測したあとは、直近の実測値に予測した変化率を掛け戻せば元のスケールに復元できます。
pred = y_last * (1 + pred_ratio)
ただし定常過程になるわけではない
誤解しやすいところなので補足します。変化率に変換しても、統計学的に厳密な定常過程にはなりません。
- 株価のボラティリティのように、分散が時期によって大きく変わる(不均一分散)
- コロナ禍のような構造変化があると、前後で性質が変わる
- 変化率自体にトレンドがある(成長が加速している)場合は非定常のまま
- 曜日や季節の周期性は残る
やっているのは「定常過程にすること」ではなく、「トレンドと水準変化という主要因だけ取り除いて、木モデルが扱いやすい形に近づけること」です。ここは正確に理解しておいたほうがいいと思います。
特徴量も同じ変換が必要
目的変数を変換して満足しがちですが、特徴量側も事情は同じです。
時系列でよく使うのが、ラグ特徴量(1期前、7期前の値をそのまま列にしたもの)とローリング統計量(直近N期間の平均や標準偏差)です。どちらも元の系列をそのまま引き継ぐので、トレンドも一緒に持ってきます。
木の分岐は lag_1 > 1000 なら左 のような絶対値の閾値で行われます。学習期間の売上が800〜1200だったなら、「1000で分岐」というルールは売上が3000になった未来では意味をなしません。全部が片側に落ちてしまい、分岐として機能しなくなります。
効くのは、行ごとに基準が変わる変換
df["lag_1_ratio"] = df["lag_1"] / df["rolling_mean_28"]
分母が行ごとに変わるので、サンプル間の順序が実際に入れ替わります。木にとって別の情報になり、分岐の意味が変わります。
この特徴量は「直近28日平均と比べて今どのくらいか」という相対的な位置を表しています。売上800の店でも3000の店でも、平均より2割高ければ等しく1.2になる。学習期間で覚えたルールが、水準の違う未来や別の系列でも通用するようになります。
ほかにもこういう組み合わせが使えます。
df["wow"] = df["lag_1"] / df["lag_7"] # 前週同曜日との比
df["trend"] = df["rolling_mean_7"] / df["rolling_mean_28"] # 短期と長期の比
df["zscore"] = (df["lag_1"] - df["rolling_mean_28"]) / df["rolling_std_28"]
最後のものは紛らわしいのですが、ローリング窓の統計量を使ったz-scoreは有効です。全期間の統計量だと定数なので無効、ローリング窓だと行ごとに値が変わるので有効、という違いです。
特徴量の変化量は水準が時間とともにずれるかで判断
判断基準は「連続値かどうか」ではなく「水準が時間とともにずれるか」です。
相対化したほうがいいもの:
- ラグ特徴量、ローリング統計量(トレンドをそのまま引き継ぐ)
- 価格、来客数など、成長やインフレで水準が動くもの
そのままでいいもの:
- 気温、湿度、降水量(物理的に範囲が決まっていてドリフトしない)
- 曜日、月などの循環する値
気温を相対化しても情報が減るだけです。
カテゴリ変数はそのままでいい
カテゴリ変数にはスケールの概念がないので、水準がずれる問題は起きません。基本はそのままで大丈夫です。LightGBMは categorical_feature で直接扱えるので、ワンホット化も基本的に不要です(高カーディナリティだと木が無駄に深くなって、むしろ不利になります)。
ただし時系列特有の注意点が3つあります。
未知カテゴリ
予測時に新商品や新店舗など、学習時になかった値が出てくると扱えません。低頻度カテゴリを事前に "other" にまとめておくといった対処が必要です。
注意
変化率は分母が前期の値なので、ゼロがあると壊れます。小売の需要予測のように売れない日が多い間欠需要では深刻な問題になります。(本記事では詳細の説明は割愛します)
おわりに
最初は「なぜ変化率にするのか」という素朴な疑問でしたが、掘っていくと木モデルの予測メカニズムと定常性の話に行き着きました。
木は学習データにあった値しか返せないので、値が時間とともにずれていく系列が苦手。そのずれを取り除いてやる作業が変化率変換です。同じことは特徴量側にも当てはまるので、目的変数だけ変換して安心しないよう気をつけたいところです。