テーブルデータにおける時系列予測では, ラグ特徴量 (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
の存在を知っているかどうかという問題