👏
Purged KFold, Combinatorial Purged KFoldとは何か(+Python実装)
Purged KFoldのPythonでの実装等が見つからないので書いた。
用語の説明
Purged Kfold → 時系列データにおいて、trainとtestの間に一定期間trainでもtestでもない間を開けたKFold。時系列の変数に過去データを使うことが多いため、単純にk-foldするとleakしてしまう対策。
Combinatorial Purged KFold → Purged KFoldに加えて、testを更に分割すること。なぜかこっちの方が解説がたくさんある[1][2]。
Purged KFold実装①
richmanbtcさんによるもの[3]。scikit-learn likeではないものの、使いたいデータの数を入れるとそのときのindexを返してくれるので非常に使いやすい
# このときは非常に大きいデータセットであり、purgeの初期値がかなり大きいので注意
def my_purge_kfold(n, n_splits=5, purge=3750*14):
idx = np.arange(n)
cv = []
for i in range(n_splits):
val_start = i * n // n_splits
val_end = (i + 1) * n // n_splits
val_idx = idx[val_start:val_end]
train_idx = idx[(idx < val_start - purge) | (val_end + purge <= idx)]
cv.append((
train_idx,
val_idx,
))
return cv
Combinatorial Purged KFold実装
ymdさんの実装が一番scikit-learn likeで使いやすかった[1:1]。ただし、実装は超長いのでここには書かない。
Purged KFold と Combinatorial Purged KFoldの使用、および可視化
完全版のソースコードはGoogleColabにも投げました[4]。
Combinatorial Purged KFold
組み合わせなので、n_splits * n_test_splits ÷ 2 通りの組み合わせが生まれることに注意。
(あと実装はかなり汚いですがご容赦を…)
import pandas as pd
import matplotlib.pylab as plt
df = pd.DataFrame(index=range(1000))
# time_gapがpurgeに相当する。
# time_gapもembargo_tdもintで指定し、その幅は使用しないことになっているみたい。ソースコードがpd.Timedeltaになっているのは元ソースコードのせい。
cv = CombPurgedKFoldCV(n_splits=4, n_test_splits=2, time_gap=50, embargo_td=5)
for i, (trn_idx, val_idx) in enumerate(cv.split(df), start=1):
if i == 1:
plt.scatter(range(1000), np.repeat(i, 1000), color="gray", label="not used")
plt.scatter(trn_idx, np.repeat(i, len(trn_idx)), color="red", label="train")
plt.scatter(val_idx, np.repeat(i, len(val_idx)), color="blue", label="valid")
plt.scatter(range(1000), np.repeat(i, 1000), color="gray")
plt.scatter(trn_idx, np.repeat(i, len(trn_idx)), color="red")
plt.scatter(val_idx, np.repeat(i, len(val_idx)), color="blue")
print(len(trn_idx) + len(val_idx))
plt.legend(bbox_to_anchor=(1.05, 1))
plt.savefig("CombinatorialPurgedKFolld.png")
Purged KFold
import pandas as pd
import matplotlib.pylab as plt
df = pd.DataFrame(index=range(1000))
cv = my_purge_kfold(1000, n_splits=5, purge=100)
for i, (trn_idx, val_idx) in enumerate(cv, start=1):
if i == 1:
plt.scatter(range(1000), np.repeat(i, 1000), color="gray", label="not used")
plt.scatter(trn_idx, np.repeat(i, len(trn_idx)), color="red", label="train")
plt.scatter(val_idx, np.repeat(i, len(val_idx)), color="blue", label="valid")
plt.scatter(range(1000), np.repeat(i, 1000), color="gray")
plt.scatter(trn_idx, np.repeat(i, len(trn_idx)), color="red")
plt.scatter(val_idx, np.repeat(i, len(val_idx)), color="blue")
print(len(trn_idx) + len(val_idx))
plt.legend(bbox_to_anchor=(1.05, 1))
plt.savefig("PurgedKFolld.png")
感想
理論的にはCPCVがいいようだが、実務的にはPurgedKFoldでも問題ないとのこと。組み合わせで結構計算量多くなるし、使われなくなるデータも結構多いしで大変な印象は持ちました。NumeraiではCPCVを、KaggleコンペではPurgedKFoldを使っています。
Discussion