👏

Purged KFold, Combinatorial Purged KFoldとは何か(+Python実装)

2022/02/13に公開

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を使っています。

脚注
  1. https://zenn.dev/ymd/articles/fd08fb46bc868c ↩︎ ↩︎

  2. https://quantcollege.net/financial-machine-learning-cpcv-explained ↩︎

  3. https://www.kaggle.com/richmanbtc/20211103-gresearch-crypto-v1 ↩︎

  4. https://colab.research.google.com/drive/1DDWrSkqbOkBHbdMXPB4x9Uiag7Gyae_1#scrollTo=-QHGFt6_y_uU ↩︎

Discussion