Time Series Cross Validation
[[train-val-test-split|← Train/Val/Test Split]]
## 왜 일반 Cross Validation을 쓰면 안 되는가
일반 k-fold CV는 데이터를 무작위로 k개 구간으로 나눠 돌아가며 검증한다. 시계열에서 이걸 사용하면 미래 데이터로 과거를 예측하는 상황이 생긴다. 시계열에서는 금지다.
## Time Series Split의 원리
시간 순서를 유지하면서 train 구간을 점진적으로 늘려가는 방식이다.
```
fold 1: [train──────────] [val]
fold 2: [train───────────────] [val]
fold 3: [train────────────────────] [val]
fold 4: [train─────────────────────────] [val]
fold 5: [train──────────────────────────────] [val]
```
- val은 항상 train 이후에 온다 → 시간 순서 유지
- fold마다 다른 시간 구간이 val이 된다 → 특정 구간에 치우친 평가 방지
- 각 fold의 MAE를 평균내면 더 신뢰할 수 있는 성능 추정
## 단순 val과의 차이
| | 단순 val | Time Series Split |
|--|--|--|
| val 구간 | 고정 1개 | 5개 이상 |
| 특정 구간 편향 | 있음 | 없음 |
| distribution shift 감지 | 어려움 | fold간 MAE 분산으로 감지 가능 |
| 계산 비용 | 낮음 | n_splits배 |
## 표준편차가 중요한 이유
fold 간 MAE 표준편차가 크면 모델이 시간 구간에 따라 불안정하다는 뜻이다. distribution shift가 특정 fold에서 발생하면 그 fold의 MAE만 크게 튀어나온다. 안정적인 모델은 fold 간 MAE가 균일하다.
## 구현
```python
from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
fold_maes = []
for tr_idx, val_idx in tscv.split(X_tv):
X_tr_f, y_tr_f = X_tv.iloc[tr_idx], y_tv.iloc[tr_idx]
X_val_f, y_val_f = X_tv.iloc[val_idx], y_tv.iloc[val_idx]
model.fit(X_tr_f, y_tr_f)
pred = model.predict(X_val_f)
fold_maes.append(mean_absolute_error(y_val_f, pred))
print(f'평균 MAE: {np.mean(fold_maes):.1f}')
print(f'표준편차: {np.std(fold_maes):.1f}')
```
test는 이 과정에서 절대 사용하지 않는다.
## 실험 결과
| 모델 | fold MAEs | 평균 MAE | 표준편차 |
|------|----------|---------|---------|
| ExtraTrees | [2602.0, 3085.4, 2470.6, 2338.2, 2186.0] | 2536.4 | 307.3 |
| GradientBoosting | [2871.6, 2818.9, 2919.8, 2563.8, 1977.9] | 2630.4 | 348.6 |
ExtraTrees가 평균 MAE, 표준편차 모두 우세 → 더 안정적이고 더 정확함.
## 핵심 관찰
**fold 5가 두 모델 모두 가장 낮은 MAE**
fold 5는 가장 많은 train 데이터 + 가장 최근 val 구간이 겹쳐있다. 앙상블 모델은 데이터가 많을수록 강해지고, 최근 구간일수록 test 분포와 유사하다.
**CV가 단순 val의 한계를 보완**
단순 val 하나에서 ExtraTrees는 10위였지만 CV 5개 fold 평균에서는 1위. 단일 val 구간은 운이 작용할 수 있다. CV가 더 신뢰할 수 있는 모델 선택 기준이다.
**코드 주의**
CV 실험에서 RandomForest를 GradientBoostingRegressor로 잘못 초기화하는 버그 발생 → 결과가 GBT와 동일하게 나옴. 반드시 모델 이름과 클래스가 일치하는지 확인할 것.
## 전체 모델 CV 결과
| 순위 | 모델 | 평균 MAE | 표준편차 |
|------|------|---------|---------|
| 1 | HuberRegressor | 2167.9 | 244.7 |
| 2 | SVR | 2277.0 | 264.1 |
| 3 | ElasticNet | 2408.1 | 230.8 |
| 4 | Ridge | 2413.2 | 230.2 |
| 5 | Lasso | 2413.9 | 230.2 |
| 6 | LinearRegression | 2414.4 | 230.0 |
| 7 | RandomForest | 2483.6 | 323.8 |
| 8 | ExtraTrees | 2536.4 | 307.3 |
| 9 | KNN | 2556.2 | 269.5 |
| 10 | GradientBoosting | 2630.4 | 348.6 |
| 11 | DecisionTree | 3409.6 | 452.9 |
## 단순 val vs CV 순위 역전
| 모델 | 단순 val 순위 | CV 순위 |
|------|------------|--------|
| GradientBoosting | 1위 | 10위 |
| HuberRegressor | 5위 | 1위 |
| LinearRegression | 7위 | 6위 |
| ExtraTrees | 10위 | 8위 |
단순 val 하나는 편향된 평가다. CV가 더 신뢰할 수 있는 기준이다.
## 핵심 인사이트
**Linear 계열이 가장 안정적** — 평균 MAE 2408~2414, std 230 수준. 이 데이터의 판매 패턴이 비선형보다 선형에 더 가깝다는 신호. 복잡한 모델이 오히려 불필요한 패턴까지 학습한 결과.
**Ensemble 계열의 단순 val 강세는 과적합** — RandomForest, ExtraTrees, GradientBoosting이 단순 val에서 강했던 건 특정 val 구간에 맞게 과적합된 것. 여러 구간으로 평가하면 선형 모델보다 오히려 못함.
**ElasticNet 수렴 경고** — 스케일링 미적용 시 발생. 스케일링 적용 시 성능 추가 개선 가능성 있음.