
미래를 보고 있지 않은데도, 검증은 여전히 컨닝을 한다
요약
시계열 데이터 검증 시 발생하는 특징량 누수(feature leakage)의 위험성과 해결 방법을 다룹니다. 데이터 전처리 과정에서 전체 데이터를 사용하여 통계량을 계산하거나, 레이블 정보를 특징량에 직접 포함할 때 발생하는 오류를 경고합니다.
핵심 포인트
- 전처리(스케일링, PCA 등)는 반드시 학습 데이터로만 fit 해야 함
- scikit-learn의 Pipeline을 사용하여 데이터 누수를 방지할 것
- 타겟 인코딩 시 전체 레이블을 사용하면 검증 성적이 왜곡됨
- 모든 전처리는 모델의 일부로서 분할 내부에서 수행되어야 함
이 기사는 Zenn에 게시했던 내용을 재게시한 것입니다. 초출: 미래를 보고 있지 않은데도, 검증은 여전히 컨닝을 한다
시계열 트레이드(series trade) 검증에서 빠지기 쉬운 함정을 다루는 연재의 제2회입니다. 제1회(미래 참조 버그)의 후속편으로 작성되었습니다.
지난번에는 미래 참조(look-ahead) 버그에 대해 이야기했습니다. 특징량(feature)도 레이블(label)도, 해당 시점까지 확정된 정보만으로 만든다——point-in-time을 철저히 지킨다면, 검증은 신뢰할 수 있다.
그렇게 믿고 싶지만, 현실은 한 단계 더 심술궂습니다.
시간의 순서를 완벽하게 지켜도, 검증은 여전히 컨닝을 합니다.
미래 참조가 '시간을 가로지르는 누수'라면, 이번에 다룰 것은 '학습과 검증의 경계를 가로지르는 누수'와 '레이블로부터의 누수'입니다. 이를 총칭하여 **특징량 누수(feature leakage)**라고 합니다. 까다로운 점은, 어떤 특징량도 미래의 봉(bar)에는 전혀 닿지 않았음에도 불구하고 검증 성적이 부풀려진다는 점입니다. 그래서 미래 참조를 해결한 사람일수록 "코드는 완벽한데, 왜 라이브(live)에서는 손실이 나는가"라며 오랫동안 고민하게 됩니다.
누수되는 통로는 크게 세 가지입니다.
가장 많고, 가장 눈에 보이지 않습니다. 정규화(normalization)·스케일링(scaling)·특징 선택(feature selection)·결측치 보완(imputation)·PCA와 같은 전처리를, 분할하기 전에 전체 데이터로 fit 해버리는 경우입니다.
# 흔히 하는 실수: 전체 데이터로 스케일러를 fit 한 후 분할한다
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # ← 검증·테스트의 통계 정보까지 엿보고 있음
...
fit_transform(X)
를 수행하는 시점에, 스케일러는 검증 구간의 평균이나 분산을 학습하고 있습니다. 특징 선택을 전체 데이터로 수행하면 '검증 구간에서 유효한 특징'을 미리 알고 선택하는 꼴이 됩니다. 미래의 봉은 보고 있지 않지만, 검증 구간의 정보는 보고 있는 것입니다. 이것이 leakage입니다.
해결 방법은 "전처리를 fold 안에서 fit 시키는 것"입니다. scikit-learn이라면 Pipeline에 담아 CV(교차 검증)에 전달하면, 각 분할에서 학습 측만 fit → 검증 측은 transform 하는 과정이 자동으로 지켜집니다.
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
pipe = make_pipeline(StandardScaler(), model)
...
전처리를 "데이터 전체에 대한 일회성 작업"이 아니라 "학습 데이터로부터 배워서 검증 데이터에 적용하는 모델의 일부"로 취급하는 것. 이 발상의 전환이 핵심입니다.
특징량을 만들 때, 레이블 그 자체(=정답) 유래의 정보를 섞어버리는 케이스입니다. 전형적인 예는 타겟 인코딩(target encoding)입니다.
# 종목별 레이블 평균을 특징량으로 만든다
X["symbol_target"] = y.groupby(symbol).transform("mean") # ← 전체 데이터의 레이블을 사용
이것은 미래의 봉을 보고 있지 않습니다. 하지만 각 행의 특징량이 해당 행 자신의 레이블을 포함하는 집계로부터 만들어지고 있습니다. 검증 행의 정답이 특징량이라는 형태로 희석되어 스며듭니다. 학습 시에 "정답의 냄새"를 맡을 수 있기 때문에 검증 성적은 올라가지만, 실전에서는 사라집니다.
인코딩이나 집계에서 레이블을 사용한다면, 학습 fold의 내부에서만 계산하고, 검증 행에는 out-of-fold 값을 적용해야 합니다. "레이블에 닿는 특징량은 반드시 분할 내부에서 만든다"라고 정해두는 것이 안전합니다.
이것이 시계열 트레이드 특유의, 가장 알아차리기 어려운 누수입니다.
레이블을 "앞으로의 20개 봉 리턴"과 같이 **앞을 향하는 윈도우(window)**로 만들면, 인접한 봉의 레이블은 시간적으로 중첩됩니다. 봉 i의 레이블은 i+1..i+20이고, 봉 i+3의 레이블은 i+4..i+23입니다. 16개 분량이 공통됩니다.
여기서 봉 i가 학습 측이고, 봉 i+3이 검증 측으로 나뉘었다고 가정해 봅시다. 양자의 레이블은 중첩된 미래로부터 만들어졌기 때문에, 학습 샘플과 검증 샘플이 정보적으로 이어져 있게 됩니다. 랜덤 분할은 물론, 단순한 walk-forward 방식에서도 분할 경계에서 이러한 번짐 현상이 일어납니다.
대책은 두 가지 작업입니다.
- purging (제거): 학습 샘플 중, 레이블 윈도우가 검증 구간과 중첩되는 것을 학습 데이터에서 제거한다.
- embargo (금지): 검증 구간 직후에 **완충 구역(buffer gap)**을 비워둔 뒤, 학습 데이터 이용을 재개한다.
개념: 레이블이 i+1..i+20의 윈도우(window)에 의존할 때
1) 검증 구간과 윈도우가 겹치는 학습 샘플을 제외 (purge)
2) 검증 구간 뒤에 몇 개의 embargo를 배치하여, 그 사이의 데이터는 학습에 사용하지 않음
scikit-learn의 TimeSeriesSplit은 purging까지는 수행해주지 않으므로, 레이블의 윈도우 폭에 맞춰 직접 제거(purge)와 embargo를 추가해야 합니다 (López de Prado의 purged K-fold 개념을 참고하면 좋습니다). 윈도우를 가진 레이블을 사용하는 이상, 이 과정은 피할 수 없습니다.
leakage (누수) 탐지는 미래 참조와 동일한 발상입니다. 올바르게 분리하는 순간 검증 성적이 무너지는가를 확인합니다.
강력한 방법 중 하나는 **레이블 셔플 테스트 (label shuffle test)**입니다.
# 레이블을 셔플해도 검증 성적이 나온다면, 파이프라인에 누수가 있는 것임
y_shuffled = y.sample(frac=1.0, random_state=0).reset_index(drop=True)
# 올바르게 분리되어 있다면, 셔플 후에는 랜덤 수준으로 떨어져야 함
레이블과 특성량(feature)의 대응 관계를 깨뜨렸음에도 여전히 맞춘다면, 정답이 어딘가에서 새어 나오고 있다는 증거입니다 (특히 구멍 1, 구멍 2에 유효). 구멍 3의 중첩이 의심될 때는 purge와 embargo를 적용하기 전후의 검증 성적을 비교하여, 적용하는 순간 성적이 크게 떨어지는지를 확인합니다. 성적이 떨어진 만큼이 누수에 의한 부풀려진 결과였다는 뜻입니다.
-
전처리는 '전체에 대한 일회성 작업'이 아니라 '학습을 통해 배우고 검증에 적용하는 모델의 일부'이다.
-
Pipeline에 포함시켜 fold 내부에서 fit 시킨다. -
레이블에 접촉하는 특성량은 반드시 분할(split) 내부에서 생성한다. 인코딩이나 집계 과정에서 레이블을 사용했다면 그 시점에서 의심해야 한다.
-
미래 방향의 윈도우로 레이블을 만들었다면, purge + embargo를 세트로 사용한다. 윈도우 폭을 변경하면 제거 및 embargo의 폭도 함께 재검토한다.
-
'건전하다면 완만하게 저하되고, 누수가 있다면 붕괴한다'를 판정의 척도로 삼는다. 검증 성적이 갑자기 좋아진 변경이야말로 leakage를 의심해야 할 변경이다.
-
미래 참조는 '시간의 누수'이며, leakage는 '학습/검증의 경계와 레이블로부터의 누수'이다.
-
시간을 지켜도 발생할 수 있다 - 누수 경로(구멍)는 주로 세 가지: ① 전처리의 전체 기간 fit, ② 레이블 유래 특성량, ③ 미래 방향 윈도우 레이블의 중첩.
-
③에는 purge + embargo가 필수적이다.
TimeSeriesSplit에만 의존해서는 막을 수 없다. -
탐지는 '올바르게 분리하는 순간 검증 성적이 무너지는가'로 한다. 레이블 셔플이 효과적이다.
-
척도는 '갑자기 좋아진 변경을 의심하는 것'이다.
여기까지 시간의 누수(지난 회차)와 정보의 누수(이번 회차)를 차단했습니다. 데이터도 코드도 이제 올바릅니다.
그럼에도 불구하고——검증이 올바르게 되어도, 라이브(실전)는 여전히 다릅니다. 다음 회차는 버그가 아닌 괴리, 즉 집행(execution)의 현실과 어떻게 마주할지에 대한 이야기입니다.
질문이나 "우리 회사는 이 구멍으로 누수가 있었다"라는 이야기가 있다면 댓글로 남겨주세요. 연재를 통해 검증이 거짓말을 하는 패턴을 차례대로 격파해 나가겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기