Motivation
- CV์ ๋ชฉ์ ์ Test Data๋ฅผ ์ต๋ํ ํ์ฉํ์ฌ ML ๋ชจ๋ธ์ ์ผ๋ฐ์ ์ธ ์ค์ฐจ๋ฅผ ์์๋ด์ด ๊ณผ์ ํฉ์ ๋ง๋ ๊ฒ์ด๋ค. K-Fold์ ๊ฒฝ์ฐ Test Data ๊ตฌ๊ฐ์ ์ฌ๋ฌ ๋ฒ ๋์๊ฐ๋ฉฐ ํ๊ฐ ๋ฐ์ดํฐ์ ํธ์ค์ ๋ง์ ๊ณผ์ ํฉ์ ๋ฐฉ์งํ๊ณ ์ฌ๋ฌ ๊ตฌ๊ฐ์ ๋๋์ด ๋ณ๋ ฌ์ ์ผ๋ก ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ์ ๋ถํฌ๊ฐ IIDํด์ผ ํ๋ ํน์ง์ ๊ฐ์ง๊ณ ์๋ค.
- ๊ธ์ต์์ CV๋ฅผ ์ ์ฉํ ๋ ์ด๋ค ์ด์๊ฐ ์๋ ์ง๋ฅผ ์ฐ๊ตฌ
The Goal of Cross-Validation

- ML์ ๋ชฉ์ : ๋ฐ์ดํฐ์ ์ผ๋ฐ ๊ตฌ์กฐ๋ฅผ ์์๋ด์ unseen feature๋ฅผ ๋ณด๊ณ ๋ฏธ๋๋ฅผ ์์ธกํ๊ณ ์ ํจ
- Test Data๋ ๋ถ๋ถ๋ Train์ ์ํจ๋ค๋ฉด ๋ฐ์ดํฐ๋ฅผ ์์ฝํ๋ ๋ฅ๋ ฅ๋ง ์ฆ๊ฐํ ๋ฟ ์์ธก๋ ฅ์ ์ ํ ์๊ฒ ๋จ.
- ๋ฐ์ดํฐ๊ฐ IID ํ๋ค๋ ์ ์ ํ์์ K-Fold๋ ML ๋ชจ๋ธ์ ์ผ๋ฐ์ ์ผ๋ก ๊ฒ์ฆํ๊ธฐ์ ์ข์ Tool์ด ๋จ.
- CV ์ ํ๋๋ ๊ฐ Fold ๋ณ์ Test Metric์ผ๋ก ๊ณ์ฐํ๊ณ ์ด๋, 1/2๋ฅผ ๋๋๋ค๋ฉด ML Model์ด ๋ฌด์์ธ๊ฐ ํ์ตํ๋ค๊ณ ๊ฐ์ฃผํ๋ค.
- ML Model์ CV๋ Hyper Parameter Tuning์ผ๋ก ์ฌ์ฉํ๊ณ ๋ค๋ฅธ ํ๋๋ Backtesting์ด๋ค.
Why K-Fold CV Fails in Finance
- ๋ชจ๋ธ ๊ฐ๋ฐ ๊ด์ : IID Process๋ฅผ ๋ฐ๋ฅธ Data๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๊ฐ ๊ฑฐ์ ์๊ธฐ ๋๋ฌธ
- Data Structures, Fractionally Differentiated Features๋ฅผ ํตํด์ ๋ฐ์ดํฐ๋ฅผ IIDํ ์ํค๋ ค๊ณ '์ด๋ฏธ' ๋
ธ๋ ฅํจ.
- CV์์ Test Data๊ฐ ์ฌ๋ฌ ๊ตฌ๊ฐ์์ ์๊ธฐ๋ฉด์ ์๊ณ์ด ์๊ด์ฑ์ผ๋ก ์ธํด ์ ๋ณด๊ฐ ์ฝ๊ฐ ๋์ถ๋ ์ ์๋ ๋ถ๋ถ์ ๋ฐฉ์งํ๊ณ ์ Purge, Embargo๋ฅผ ์ ์ํจ.
- ๋ฐฑํ
์คํ
๊ด์ : ๋ฐฑํ
์คํธ์ ๋ชฉ์ ์ธ ๋์ ๋ชจ๋ธ์ ํ๊ธฐํ๋ ๊ฒ์ ์ฐ๋ ๊ฒ์ด ์๋ ๋ชจ๋ธ ์์ฒด๋ฅผ ์กฐ์ ํ๊ธฐ ์ํด ์ฌ์ฉํ๊ธฐ ๋๋ฌธ
Information Leakage
- Information Leakage๋ Train ๋ฐ์ดํฐ๊ฐ Test ๋ฐ์ดํฐ์๋ ๋ฑ์ฅํ๋ ์ ๋ณด๋ฅผ ํฌํจํ๋ ๊ฒฝ์ฐ์ ๋ฐ์ํจ. ๋ค๋ง, Train/Test๋ฅผ ๋ฌผ๋ฆฌ์ ์ผ๋ก ์ ํํ๊ฒ ๋๋๊ธฐ ๋๋ฌธ์ ๊ธฐ์ ์ ์ค๋ฅ๊ฐ ์๋๋ผ๋ฉด ์ด๋ฐ ๋ถ๋ถ์ ๋ฐ์ํ์ง ์์. ๋ค๋ง, ๊ณ์ด ์๊ด ํน์ง์ ์ํด ์์ ๊ฐ์ด X_t ~ X_(t+1) , Y_t ~ Y_(t+1)์ธ ๊ฒฝ์ฐ์ ๋ฌผ๋ฆฌ์ ์ธ index์ ๋ง์ง๋ง์ด t๊น์ง๋ผ๋ฉด t+1์ด Test๋ก ๋ถ๋ฅ๋๊ณ ์ด๋ '์ฌ์ค์' Inforamtion Leakage๋ผ๊ณ ๋ณผ ์ ์์.
: X๊ฐ ๋ฌด๊ดํ ํน์ง์ด๋ผ ํ๋๋ผ๋ Expectation ๊ฐ์ด ์ฌ์ค์ Y_(t+1)๋ก ์๋ ดํ๊ฒ ๋๋ ํ์์ด ๋ฐ์ํ๊ฒ ๋จ.
- Information Leakage๋ก ํ๋จํ๋ ค๋ฉด (Xt,Yt) ~ (X_(t+1), Y_(t+1))์ด ๋์ด์ผ ํ๊ณ ํ์ชฝ๋ง ๋ง์์๋ ์ถฉ๋ถํ์ง ์๋ค.
A Solution : Purged K-Fold CV

- Purging(Delete Overlap) : Train/Test ๋ฐ์ดํฐ์ 'Label'์ด ์ค์ฒฉ๋ ๊ฒฝ์ฐ์ Observation์ ๋ชจ๋ ์ ๊ฑฐํ๋ ๊ฒ > ์ด ๋ถ๋ถ์ Labeling์ผ๋ก ์ธํ ์ค์ฒฉ ์์ ์ผ๊ด์ ์ผ๋ก ์ ๊ฑฐ
- Embargo : Purging์ ํ๊ณ ๋์ ๋ถ๊ฐ์ ์ธ ์์
์ผ๋ก ์ด๋ ์๊ณ์ด ์๊ด์ฑ ์ ๋ณด๊ฐ ๋จ์์๊ฒ ๋๋ ๊ฒฝ์ฐ๊ฐ ์์ด์ ์๊ณ์ด ํ๋ฆ ์ ํ
์คํธ ์งํ์ ๋ถ๋ถ์ ๋ํด ์ผ์ ๊ตฌ๊ฐ ๋ณด์์ ์ผ๋ก ์ญ์ ํ๋ ํ์ > ์ข๋ ์ง๋ณด์ ์ผ๋ก ์ด๋๊น์ง๋ฅผ ํ๋ ๋์ง๋ฅผ ๋ฏธ์ธํ๊ฒ ์ฒดํฌํด์ ํ ์๋ ์์๋ฏ.
Purging the Trainig Set
- ๊ฒฐ๊ตญ ๋ชฉ์ ์ Information Set I(Train),J(Test)๊ฐ์ ๊ต์งํฉ์ ์์ ๋ ๊ฒ์ด๊ณ
def getTrainTimes(t1, testTimes):
trn = t1.copy(deep=True)
for i, j in testTimes.iteritems():
df0 = trn[(i <= trn.index) & (trn.index <= j)].index # Train starts within test
df1 = trn[(i <= trn) & (trn <= j)].index # Train ends within test
df2 = trn[(trn.index <= i) & (j <= trn)].index # Train envelops test
trn = trn.drop(df0.union(df1).union(df2))
return trn
Embargo
- Purge๋ก ๋์ ๋ฐฉ์ง ๋ชปํ ๊ฒฝ์ฐ์ ์ฌ์ฉํ๊ณ ์๊ณ์ด ํ๋ฆ์ ์ ๋ณด๊ฐ ๋์ถ๋ ๊ฒฝ์ฐ๋ง ์ ๊ฑฐํ๋ ๊ฒ์ด๋ฏ๋ก Train - (1) - Test - (2) - Train์ผ ๋ (2)์ ํด๋นํ๋ ๊ตฌ๊ฐ์๋ง ์ ์ฉํจ. ๊ตฌ๊ฐ์ ๊ธธ์ด๋ ์์๋ก ์ ํด์ง๋ค. ๋๋ต์ ์ผ๋ก ์์ ๊ฐ 0.01T ์ ๋๋ก ๋งค์ฐ ์ถฉ๋ถํ๋ค๊ณ ํจ.
def getEmbargoTimes(times, pctEmbargo):
step = int(times.shape[0] * pctEmbargo)
if step == 0:
mbrg = pd.Series(times, index=times)
else:
mbrg = pd.Series(times[step:], index=times[:-step])
mbrg = mbrg.append(pd.Series(times[-1], index=times[-step:]))
return mbrg
The Purged K-Fold Class
class PurgedKFold(_BaseKFold):
def __init__(self, cv_config, tl=None):
self._cv_config = cv_config
n_splits = self._cv_config.get('n_splits', 3)
pctEmbargo = self._cv_config.get('pctEmbargo', 0.)
if not isinstance(tl, pd.Series):
raise ValueError('Label Through Dates must be a pd.Series')
super().__init__(n_splits=n_splits, shuffle=False, random_state=None)
self.tl = tl
self.pctEmbargo = pctEmbargo
def split(self, X, y=None, groups=None):
if False in X.index == self.tl.index:
raise ValueError('X and ThruDateValues must have the same index')
indices = np.arange(X.shape[0])
mbrg = int(self.pctEmbargo*X.shape[0])
test_starts = [(i[0], i[-1]+1) for i in np.array_split(indices, self.n_splits)]
for i, j in test_starts:
t0 = self.tl.index[i]
test_indices = indices[i:j]
maxTlIdx = self.tl.index.searchsorted(self.tl[test_indices].max())
train_indices = self.tl.index.searchsorted(self.tl[self.tl <= t0].index)
if maxTlIdx < X.shape[0]:
train_indices = np.concatenate([train_indices, indices[maxTlIdx+mbrg:]])
yield train_indices, test_indices
- self.tl์ pd.Series๋ก index,value๋ ๊ฐ๊ฐ Triple Barrier์ ์์์ ๊ณผ ๋์ ์ ๋ํ๋.
- PurgedKFold ํด๋์ค์ split method ๋ถ์
- mbrg๋ ์ ๋ฐ๊ณ ๋ฅผ ์ ์ฉํ๊ธฐ ์ํ ๊ธธ์ด๋ฅผ ๋ฏธ๋ฆฌ percentage๋ฅผ ์ด์ฉํด ๊ธธ์ด ์ฐ์ถ
- maxTlIdx๋ purging์ผ๋ก **train - (1) - test - (2) - train์์ (2)**ํํธ์ purging ๋ถ๋ถ idx๋ฅผ ๊ณ์ฐ
- train_indices๋ฅผ ๊ณ์ฐํ๋ ๊ณผ์ ์์ ์์ (1) ํํธ์ purging์ ํด๋ฒ๋ฆฐ train_indices๋ฅผ ์ฐ์ถ
- np.concatenate([train_indices, indices[maxTlIdx+mbrg:]]) ์ ํตํด (1),(2)์ purging, embargo๋ฅผ ์ ์ฉํ ์ต์ข
train indices๋ฅผ ์ฐ์ถ
sklearn bug
def cvScore(clf, X, y, sample_weight=None, scoring='neg_log_loss', t1=None, cv=None, cvGen=None, pctEmbargo=None):
if scoring not in ['neg_log_loss', 'accuracy']:
raise Exception('Wrong scoring method')
if cvGen is None:
cvGen = PurgedKFold(n_splits=cv, t1=t1, pctEmbargo=pctEmbargo)
if sample_weight is None:
sample_weight = np.ones(len(X))
score = []
for train, test in cvGen.split(X=X):
fit = clf.fit(
X=X.iloc[train, :], y=y.iloc[train],
sample_weight=sample_weight[train]
)
if scoring == 'neg_log_loss':
prob = fit.predict_proba(X.iloc[test, :])
score_ = -log_loss(y.iloc[test], prob, sample_weight=sample_weight[test], labels=clf.classes_)
else:
pred = fit.predict(X.iloc[test, :])
score_ = accuracy_score(y.iloc[test], pred, sample_weight=sample_weight[test])
score.append(score_)
return np.array(score)
- ์ฝ๋ ์ค๋ช
- clf.fit์ ํตํด ๋ฐ๋ก ๋ชจ๋ธ์ ๋ง๋ค๊ณ scoring input์ ๋ฐ๋ผ predict/predict_proba์ metric function์ธ log_loss, accuracy_score๋ฅผ ํ์ฉํ์ฌ score๋ฅผ ๋ฐ๋ก ๊ณ์ฐ
- model์ ๊ฒ์ฆํ ๋ ์ฃผ๋ก ์ฐ๋ฏ๋ก feature importance ์ชฝ ๋ชจ๋๊ณผ ์ฐ๊ณ