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


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

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

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"])

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

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

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

shift_rolling = df.set_index("date").shift(freq="2D").rolling("3D")[["target"]].mean()

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

2. 無理やりマージする
先程のテーブルを元のテーブルにマージしたいのだが, 日付がバラバラなので完全一致することはなく, 通常のマージではどうにもならない.
merge_asofという関数を使うと, これを解消できる.

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

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

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

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

groupbyで使いたい場合

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

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"])

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

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",
                           )
)

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

df = df.merge(tmp, on=["group","date"], suffixes=("","_shift"))

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

まとめ

  • pd.merge_asofの存在を知っているかどうかという問題