回帰とGBDTのハイブリッドで時系列データの予測精度を上げる

Kaggle公式チュートリアルにて第5回目: [Hybrid Models](https://www.kaggle.com/ryanholbrook/hybrid-models)を扱っていきます。

今までは回帰をメインに予測を行ってきましたが、
今回は回帰とGBDT（Gradient Boosted Decision Tree）をいいとこ取りして精度を高めていこうよ！っていうのが主題です。
（チュートリアルにもありますがstackingモデルのようなイメージです）
# 時系列データを構成する要素と残差
今まで扱ってきたデータとしてトレンド・季節性・周期（不定期含む）があり、これらのアプローチだけでもかなり予測としては成立することがみて取れました。

これらを考慮すると
<B>時系列データ = トレンド + 季節性　+ 周期性 + 残差</B>
という構成要素であると考えることができます(by Kaggle)

公式チュートリアルから引用すると順番としては以下のようなイメージ
![スクリーンショット 2021-11-08 11.21.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865635/45aec8fd-2905-7b07-386c-d476544b2841.png)
[https://www.kaggle.com/ryanholbrook/hybrid-models](https://www.kaggle.com/ryanholbrook/hybrid-models)

トレンドを予測→その残りから周期性を見つけ→・・・→最後に残差だけが残る、と言った具合。

# ハイブリッドの予測
前回まではトレンド・季節性・周期をLinearRegressionで扱って比較的良い予測ができていました（少なくとも公式チュートリアルの中では）

ですが、これらの要素を排除した残差を回帰で予測することは難しいため、ここで別のアルゴリズムを用いてハイブリッドなモデル構築を目指します。

外観をコーディングすると以下。

```
# 1. Train and predict with first model
model_1.fit(X_train_1, y_train)
y_pred_1 = model_1.predict(X_train)

# 2. Train and predict with second model on residuals
model_2.fit(X_train_2, y_train - y_pred_1)
y_pred_2 = model_2.predict(X_train_2)

# 3. Add to get overall predictions
y_pred = y_pred_1 + y_pred_2
```

ひとつめは回帰で予測、残差（y_train - y_pred_1）をmodel_2と別の訓練データで予測し、最終的な予測は2つのモデルの予測値の合計としています。

チュートリアル曰く、3つ以上のモデルを組み合わせることも可能ですがそ、こまで精度向上につながらない（そう）です。
シンプルな回帰モデルと非線形回帰を扱えるGBDT（とかニューラルネット）で組み合わすことが一般的（らしい）。
## Regressionの使い方
回帰モデルを使う上で大きく2つの方向性があり、
特徴量をtransformさせる or 目的変数をtransoformさせるやり方があります。

### 特徴量をtransformさせる
入力として特徴量から数学的な（y=a+bxのような）関数の情報を学習し、その後で目的変数とマッチした出力を訓練データの中で結合しtransformさせる方法
いわゆるLinear Reg。

### 目的変数をtransoformさせる
目的変数をグループ化するために特徴量を用いて、グループの平均によって予測をしていく方法。
いわゆるGBDT（もしくはKNNなど）。
<hr>
グダグダと書いていますが、「与えられた情報以上のことを読み取り、推測する」ことが主題であることには変わりありません。

Kaggle公式のものから引用した以下はLinear Reg とGBDTそれぞれ単一で使った時のグラフです。
![スクリーンショット 2021-11-08 14.05.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865635/2f9c1590-2fb5-f83c-437b-272223aa4a8e.png)
[https://www.kaggle.com/ryanholbrook/hybrid-models](https://www.kaggle.com/ryanholbrook/hybrid-models)

Linear Regは確かにtrendの予測をうまくこなせていますが、GBDTは与えられた範囲での値を上回ることなく予測しています。

つまり、
<B>大まかなトレンドに関しては回帰が有効であり、残差のような回帰では表現することが厳しいものをGBDTで補完する</B>
ことが良さそうであることがわかります

（余談：時系列データを学習する前に時系列データのコンペに参加したのですが、能天気にlightGBMドン！って感じでアプローチしました。そうすると、予測がある程度のところで高止まりして、これ以上訓練を増やしても意味ないよな〜って悩んでいた時に、最初に大きなトレンド予測を回帰で表現してからlightGBM使ったら、めちゃくちゃ誤差が小さくなりました。）
# コーディングで体験してみる
いつものような自作データでは威力が実感できないと思うので、公式のものを使います。
ここでデータの添付などはしませんので、実際に試してみたいという方はぜひ本家のページで学習してみましょう！
(データ読み込みやプロットにおけるパラメータの設定などは割愛した状態で進めます。)

```
# Classでハイブリッドモデルを構築
class BoostedHybrid:
    def __init__(self, model_1, model_2):
        self.model_1 = model_1
        self.model_2 = model_2
        self.y_columns = None  # store column names from fit method


    def fit(self, X_1, X_2, y):
        self.model_1.fit(X_1, y)

        y_fit = pd.DataFrame(
            self.model_1.predict(X_1),
            index=X_1.index, columns=y.columns,
        )

        y_resid = y - y_fit
        y_resid = y_resid.stack().squeeze() # wide to long
        self.model_2.fit(X_2, y_resid)

        # Save column names for predict method
        self.y_columns = y.columns
        # Save data for question checking
        self.y_fit = y_fit
        self.y_resid = y_resid


    def predict(self, X_1, X_2):
        y_pred = pd.DataFrame(
            self.model_1.predict(X_1),
            index=X_1.index, columns=self.y_columns,
        )
        y_pred = y_pred.stack().squeeze()  # wide to long
        y_pred += self.model_2.predict(X_2)

        return y_pred.unstack()  # long to wide

```
このclassを使って、トレンド予測するだけ、という感じですね（今回はがっつりとしたコーディングはいい例がなかったので割愛します。）

## stack()とsqueeze()の補足
あんまりDataFrameでstack, squeezeを使わないので、調べてみました。
### stack
numpyでもhstack, vstackがあるので、それをイメージしてもらえるとわかりやすいと思います。
いわゆる縦に積み上げる（stack）イメージで、簡単な例をみると理解しやすいと思います。

```
df = pd.DataFrame({'A': ['a', 'a', 'a', 'b', 'b', 'b'],
                   'B': ['x', 'y', 'z', 'x', 'y', 'z'],
                   'C': [1, 2, 3, 4, 5, 6],
                   'D': [10, 20, 30, 40, 50, 60]})
```
![スクリーンショット 2021-11-08 14.44.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865635/749efe12-80d9-b559-2a5d-59aa61994a22.png)

```
df.stack()
```
![スクリーンショット 2021-11-08 14.44.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865635/dbd11421-b9eb-b3d6-8e06-44857cab290b.png)
列を行に持ち直して、縦に長いデータフレームができます。

### squeeze
名前は本当に微かな記憶で見たことがある程度の関数でした笑
調べてみると1次元の軸を持つオブジェクトをスカラーにする関数で、普通のデータフレームに割り当てても大した威力は示さないぽいです。

ちょっといくつか例を見てみたので、参考になれば。。

```
display(df.iloc[:, 3].squeeze())
print(type(df.iloc[:, 3].squeeze()))
display(df.iloc[:, 3])
print(type(df.iloc[:, 3]))
display(df.loc[df['D'] < 20, 'D'])
print(type(df.loc[df['D'] < 20, 'D'].squeeze()))
print(type(df.loc[df['D'] < 20, 'D']))
```
![スクリーンショット 2021-11-08 14.58.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1865635/6e639212-9bab-7ce1-e3ad-97c3303bbafe.png)


まず、ilocで該当する列（スカラーの列）を取り出したDataFrameとそれにsqueezeを当てたときは何も変わりませんでした。

唯一違いを見つけたのは、レコードのなかのスカラーを1つだけ取り出してきた時に
- squeezeをしたとき: int
- しなかったとき: Series
になったということでした。

「毎回使える関数ではない」というのが個人的体感で、あくまで今回のclassでは使えた感じでしょうか。。

# 今回参考にさせていただいた記事
[pandasでstack, unstack, pivotを使ってデータを整形](https://note.nkmk.me/python-pandas-stack-unstack-pivot/)
[pandas.DataFrame.squeeze](https://runebook.dev/ja/docs/pandas/reference/api/pandas.dataframe.squeeze)