日時でラグ特徴量を作る（日付でシフトと集計）

テーブルデータにおける時系列予測では, ラグ特徴量 (shift & rolling)を作る事がよくある. 
これまでは, 単純に行数でシフトすればよい場合のみを扱ってきたが, 日付でシフトして日付で集計する需要が出てきた. 
これが案外苦労したのでメモとして残しておく. 

次のデータを例に説明する. 

```python
df = pd.DataFrame({
        "date": ["2021-04-01 10:04", "2021-04-01 12:03", "2021-04-02 14:40", "2021-04-03 2:01"
                , "2021-04-05 5:02", "2021-04-07 1:16", "2021-04-10 11:20", "2021-04-14 23:30"],
        "target": [1,5,7,2,3,7,9,3],
})
df["date"]=pd.to_datetime(df["date"])
```

![table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/404acf9c-6426-9a0d-c2d5-427cf2eb8726.png)

これまでよく使ってきたのは, `df["target_shift_rows"]=df["target"].shift(2).rolling(3).mean()`みたいな行単位での処理だった. 
行ごとのシフトであるため, indexが共通であり代入が簡単である. 

しかしここでやりたいのは, 2日シフト, 3日間集計だ. 

**1. 日付でシフトして日付で集計する**

```python
shift_rolling = df.set_index("date").shift(freq="2D").rolling("3D")[["target"]].mean()
```
![shift_rolling.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/7d44163d-7d36-dedc-a409-959f2758e4e0.png)

- indexを日付にすると`.shift(freq="2D")`で2日シフトできる
    - 通常の行のシフトのイメージと異なり, **単純に日付が2日ずれるだけ**
    - 事前に`pd.Timedelta(days=2)`を引いておいても同じことを実現できる
- `.rolling("3D")`で3日間の単位で集計できる
    - indexが日付のときのみ可能
    - indexが日付でない場合, `.rolling("3D", on="date")`のように`on`で指定も可能


**2. 無理やりマージする**
先程のテーブルを元のテーブルにマージしたいのだが, 日付がバラバラなので完全一致することはなく, 通常のマージではどうにもならない. 
[`merge_asof`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.merge_asof.html)という関数を使うと, これを解消できる. 

```python
df = pd.merge_asof(df, shift_rolling, on="date", direction="backward", suffixes=("","_shift"))
```

![merge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/fab5bcec-e203-f452-8f65-5855f1a5003e.png)

- direction
    - `"backward"`: 後ろ方向の近い値でマージ
    - `"forward"`: 前方向の近い値でマージ
    - `"nearest"`: 最も近い値でマージする

今回の場合, 未来情報を参照しないように`"backward"`を使う. 

See https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.merge_asof.html

# groupbyで使いたい場合
ユーザーIDや商品IDごとに日付で処理したい場合, さらに複雑になるのでメモ. 

```python
df = pd.DataFrame({
        "date": ["2021-04-01 10:04", "2021-04-01 12:03", "2021-04-02 14:40", "2021-04-03 2:01"
                , "2021-04-05 5:02", "2021-04-07 1:16", "2021-04-10 11:20", "2021-04-14 23:30"],
        "group": ["a","a","b","a","b","a","b","b"],
        "target": [1,5,7,2,3,7,9,3],
})
df["date"]=pd.to_datetime(df["date"])
```
![table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/b50054a1-01a9-729f-4bf5-b635e379fee9.png)

今回は, groupごとに, 1日シフト, 2日平均を取りたいとする. 

```python
tmp = df.set_index("date").groupby("group")[["target"]].apply(
    lambda x: pd.merge_asof(pd.DataFrame(x.index), 
                            x.shift(freq="1D").rolling("2D").mean(),
                            on="date",
                            direction="backward",
                           )
)
```
![table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/6bd86885-646a-d243-f11a-c1dbcd365af9.png)

`pd.merge_asof`が複数列でのマージに対応していないため, 事前にマージ用にシフトされた`"date"`列を, 元の`"date"`列に無理やりマージしておく. 
追記：`by`という引数でonの前に通常マージする項目を設定できるっぽい. 

```python
df = df.merge(tmp, on=["group","date"], suffixes=("","_shift"))
```
![table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294661/48a2b868-f654-7773-3ddf-211204ae00c1.png)

最後に, 複数列でマージすればOK. 


# まとめ
- `pd.merge_asof`の存在を知っているかどうかという問題