Nグループからk個をテスト

T個の時系列標本を、シャッフルせずN個の連続グループG1,…,GNへ分けます。各splitではkグループをテスト、残りN−kを学習にします。

θtrain = 1 − k/N

kを増やすとテストの多様性は増えますが、各モデルの学習量は減ります。通常はkN/2とし、学習・テストの取引数が十分かを先に確認します。

分割数と経路数

Nグループからk個を選ぶため、train/test split数は二項係数です。

S = C(N,k) = N! / [k!(N−k)!]

すべてのsplitにはk個のテストグループがあるため、テストグループ出現総数はkS。各完全経路はNグループを1回ずつ使うので、構成可能な経路数φは次です。

φ(N,k) = (k/N) C(N,k) = C(N−1,k−1)

例としてN=6、k=2なら、splitはC(6,2)=15、経路はφ=5、学習比率は4/6です。

splitG1G2G3G4G5G6S1S2S3S4緑=TEST、青灰=TRAIN。実際は15通りすべてを作る。
N=6、k=2の組合せの一部。各グループは同じ回数だけテストになります。

CPCVのアルゴリズム

  1. 履歴をシャッフルせずN個の連続グループへ分ける。
  2. テストグループk個の全組合せを列挙する。
  3. 各splitでラベル区間がテストと重なる学習標本をpurgeする。
  4. 各テスト境界へ事前定義のembargoを置く。
  5. 標準化、特徴選択、モデルfitをtrainだけで行う。
  6. テスト予測とコスト後損益を、元の時刻とsplit ID付きで保存する。
  7. テストグループを割り当て、φ本の時系列OOS経路へつなぐ。
Python風擬似コード:split列挙
from itertools import combinations

groups = range(N)
for test_groups in combinations(groups, k):
    train_groups = set(groups) - set(test_groups)
    train_idx = purge_and_embargo(train_groups, test_groups, intervals)
    model = fit_pipeline(X[train_idx], y[train_idx])
    pred = model.predict(X[index_of(test_groups)])
    save_oos(pred, test_groups)

同じテストグループでも学習組合せが違えば予測が変わるため、グループとsplitの組を保存します。

経路を組み立てる

各グループはφ回テストに登場します。各登場を1〜φの経路へ一度ずつ割り当て、各経路がG1からGNまでを時系列順に1回ずつ含むようにします。

Pathp = OOS(G1,s1p) ⊕ … ⊕ OOS(GN,sNp)

⊕は時系列連結です。経路の各区間は異なる学習モデルから生まれます。これは実際の一回の運用履歴を完全再現するものではなく、分割感度を見る再構成です。

分布の読み方

φ本それぞれについて、同じ定義で指標を出します。

pfail = (1/φ) Σp=1φ 1[J(Pathp) ≤ J0]

平均だけでなく、中央値、10%点、最悪経路を報告します。採用基準を結果分布を見た後で作りません。

限界

ウォークフォワードを捨てない

ウォークフォワードは実際の更新順序を再現しやすく、CPCVは分割から生じる成績分布を調べます。目的が違うため、どちらか一方を万能としません。

ミニテスト

4問で確認します。

Q1. N=6、k=2のsplit数は?

C(6,2)=15です。

Q2. N=6、k=2の経路数φは?

(2/6)×15=5です。

Q3. 各splitで必要なことは?

テスト情報を学習へ混ぜません。

Q4. CPCV経路の正しい理解は?

共有履歴から構成され、独立ではありません。

参考資料

Marcos López de Prado, Combinatorial Purged Cross-Validation および Advances in Financial Machine Learning Chapter 12。