077 시계열 교차 검증
키워드: 시계열 교차 검증, time series CV
개요
시계열 데이터는 시간 순서가 중요하므로 일반적인 K-Fold 교차 검증을 사용할 수 없습니다. 시계열 교차 검증은 시간 순서를 유지하면서 모델을 평가하는 방법입니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
pycaret[full]>=3.0
일반 CV vs 시계열 CV
일반 K-Fold CV:
┌───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ Fold 1: Train [2,3,4,5], Test [1]
├───┼───┼───┼───┼───┤
│ 1 │ 2 │ 3 │ 4 │ 5 │ Fold 2: Train [1,3,4,5], Test [2]
└───┴───┴───┴───┴───┘
→ 미래 데이터로 과거 예측 = 데이터 누출!
시계열 CV:
┌───┬───┬───┬───┬───┐
│ T │ T │ V │ │ │ Fold 1: Train [1,2], Test [3]
├───┼───┼───┼───┼───┤
│ T │ T │ T │ V │ │ Fold 2: Train [1,2,3], Test [4]
├───┼───┼───┼───┼───┤
│ T │ T │ T │ T │ V │ Fold 3: Train [1,2,3,4], Test [5]
└───┴───┴───┴───┴───┘
→ 항상 과거로 학습, 미래 예측
시계열 CV 전략
1. Expanding Window (누적)
import numpy as np
import matplotlib.pyplot as plt
# 077 Expanding Window 시각화
n_samples = 100
n_splits = 5
test_size = 10
fig, axes = plt.subplots(n_splits, 1, figsize=(14, 8))
for i in range(n_splits):
train_end = n_samples - (n_splits - i) * test_size
test_start = train_end
test_end = test_start + test_size
# 배열 생성
arr = np.zeros(n_samples)
arr[:train_end] = 1 # Train
arr[test_start:test_end] = 2 # Test
axes[i].imshow([arr], aspect='auto', cmap='RdYlBu', vmin=0, vmax=2)
axes[i].set_ylabel(f'Fold {i+1}')
axes[i].set_yticks([])
axes[-1].set_xlabel('Time')
plt.suptitle('Expanding Window CV')
plt.tight_layout()
plt.savefig('expanding_cv.png', dpi=150)
2. Sliding Window (고정)
import numpy as np
import matplotlib.pyplot as plt
# 077 Sliding Window 시각화
n_samples = 100
n_splits = 5
train_size = 50
test_size = 10
fig, axes = plt.subplots(n_splits, 1, figsize=(14, 8))
for i in range(n_splits):
train_start = i * test_size
train_end = train_start + train_size
test_start = train_end
test_end = test_start + test_size
# 배열 생성
arr = np.zeros(n_samples)
arr[train_start:train_end] = 1 # Train
arr[test_start:test_end] = 2 # Test
axes[i].imshow([arr], aspect='auto', cmap='RdYlBu', vmin=0, vmax=2)
axes[i].set_ylabel(f'Fold {i+1}')
axes[i].set_yticks([])
axes[-1].set_xlabel('Time')
plt.suptitle('Sliding Window CV')
plt.tight_layout()
plt.savefig('sliding_cv.png', dpi=150)
PyCaret 시계열 CV
from pycaret.time_series import *
import pandas as pd
import numpy as np
# 077 데이터 준비
np.random.seed(42)
dates = pd.date_range('2020-01-01', periods=365*2, freq='D')
values = (100 + np.linspace(0, 50, len(dates)) +
20 * np.sin(2 * np.pi * np.arange(len(dates)) / 365) +
np.random.normal(0, 5, len(dates)))
data = pd.DataFrame({'date': dates, 'value': values})
data.set_index('date', inplace=True)
# 077 Expanding Window (기본)
ts_expanding = setup(
data=data,
target='value',
fh=30,
fold=5,
fold_strategy='expanding', # 누적
session_id=42,
verbose=False
)
# 077 Sliding Window
ts_sliding = setup(
data=data,
target='value',
fh=30,
fold=5,
fold_strategy='sliding', # 고정 윈도우
session_id=42,
verbose=False
)
CV 전략 비교
from pycaret.time_series import *
import pandas as pd
# 077 Expanding Window
ts1 = setup(data, target='value', fh=30, fold=5,
fold_strategy='expanding', session_id=42, verbose=False)
model1 = create_model('auto_arima')
print("=== Expanding Window ===")
# 077 Sliding Window
ts2 = setup(data, target='value', fh=30, fold=5,
fold_strategy='sliding', session_id=42, verbose=False)
model2 = create_model('auto_arima')
print("\n=== Sliding Window ===")
언제 어떤 전략을?
Expanding Window:
- 데이터가 적을 때
- 전체 이력이 중요할 때
- 패턴이 일관될 때
Sliding Window:
- 데이터가 많을 때
- 최근 데이터가 더 중요할 때
- 패턴이 시간에 따라 변할 때
sklearn TimeSeriesSplit
from sklearn.model_selection import TimeSeriesSplit
import numpy as np
import matplotlib.pyplot as plt
# 077 데이터
X = np.arange(100)
# 077 TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
fig, ax = plt.subplots(figsize=(14, 6))
for i, (train_idx, test_idx) in enumerate(tscv.split(X)):
ax.scatter(train_idx, [i] * len(train_idx), c='blue', marker='s', s=10, label='Train' if i == 0 else '')
ax.scatter(test_idx, [i] * len(test_idx), c='red', marker='s', s=10, label='Test' if i == 0 else '')
ax.set_xlabel('Time Index')
ax.set_ylabel('Fold')
ax.set_title('TimeSeriesSplit')
ax.legend()
plt.savefig('tscv.png', dpi=150)
# 077 사용 예
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
scores = []
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X[train_idx].reshape(-1, 1), X[test_idx].reshape(-1, 1)
y_train, y_test = data['value'].values[train_idx], data['value'].values[test_idx]
model = RandomForestRegressor(n_estimators=50, random_state=42)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
scores.append(mae)
print(f"Fold: Train size={len(train_idx)}, Test size={len(test_idx)}, MAE={mae:.2f}")
print(f"\n평균 MAE: {np.mean(scores):.2f} ± {np.std(scores):.2f}")
Gap 설정
from sklearn.model_selection import TimeSeriesSplit
import numpy as np
# 077 Gap: 학습과 테스트 사이 간격
# 077 실제 예측에서 준비 시간이 필요할 때 유용
tscv_gap = TimeSeriesSplit(n_splits=5, gap=7) # 7일 갭
for i, (train_idx, test_idx) in enumerate(tscv_gap.split(np.arange(100))):
print(f"Fold {i+1}: Train [{train_idx[0]}-{train_idx[-1]}], "
f"Gap [{train_idx[-1]+1}-{test_idx[0]-1}], "
f"Test [{test_idx[0]}-{test_idx[-1]}]")
Blocked Time Series CV
import numpy as np
import matplotlib.pyplot as plt
def blocked_time_series_split(n_samples, n_splits, train_size, test_size, gap=0):
"""블록 기반 시계열 분할"""
indices = np.arange(n_samples)
splits = []
for i in range(n_splits):
# 각 블록의 시작점 계산
block_size = train_size + gap + test_size
offset = i * test_size
train_start = offset
train_end = train_start + train_size
test_start = train_end + gap
test_end = test_start + test_size
if test_end <= n_samples:
train_idx = indices[train_start:train_end]
test_idx = indices[test_start:test_end]
splits.append((train_idx, test_idx))
return splits
# 077 예시
splits = blocked_time_series_split(
n_samples=200,
n_splits=5,
train_size=100,
test_size=20,
gap=5
)
for i, (train_idx, test_idx) in enumerate(splits):
print(f"Fold {i+1}: Train [{train_idx[0]}-{train_idx[-1]}], "
f"Test [{test_idx[0]}-{test_idx[-1]}]")
Walk-Forward Validation
import numpy as np
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error
import matplotlib.pyplot as plt
def walk_forward_validation(data, model_class, train_size, forecast_horizon):
"""Walk-Forward 검증"""
predictions = []
actuals = []
indices = []
n = len(data)
for i in range(train_size, n - forecast_horizon + 1, forecast_horizon):
# 학습 데이터
train_data = data[:i]
# 테스트 데이터
test_data = data[i:i + forecast_horizon]
# 특성 준비 (간단한 예: 인덱스만)
X_train = np.arange(len(train_data)).reshape(-1, 1)
y_train = train_data.values
X_test = np.arange(len(train_data), len(train_data) + len(test_data)).reshape(-1, 1)
y_test = test_data.values
# 모델 학습 및 예측
model = model_class()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
predictions.extend(y_pred)
actuals.extend(y_test)
indices.extend(range(i, i + forecast_horizon))
return np.array(indices), np.array(actuals), np.array(predictions)
# 077 실행
indices, actuals, preds = walk_forward_validation(
data['value'],
lambda: RandomForestRegressor(n_estimators=50, random_state=42),
train_size=365,
forecast_horizon=30
)
# 077 평가
mae = mean_absolute_error(actuals, preds)
print(f"Walk-Forward MAE: {mae:.2f}")
# 077 시각화
plt.figure(figsize=(14, 6))
plt.plot(data.index, data['value'], label='Actual', alpha=0.7)
plt.scatter(data.index[indices], preds, c='red', s=10, label='Walk-Forward Predictions', alpha=0.5)
plt.xlabel('Date')
plt.ylabel('Value')
plt.title('Walk-Forward Validation')
plt.legend()
plt.savefig('walk_forward.png', dpi=150)
PyCaret에서 CV 결과 확인
from pycaret.time_series import *
ts = setup(data, target='value', fh=30, fold=5, session_id=42, verbose=False)
# 077 모델 생성 (CV 자동 실행)
model = create_model('auto_arima')
# 077 CV 결과는 create_model 출력에서 확인
# 077 각 폴드별 MASE, RMSSE, MAE, RMSE, MAPE, SMAPE, R2
CV 폴드 수 선택
from pycaret.time_series import *
import pandas as pd
# 077 폴드 수에 따른 성능 변화
fold_results = []
for n_folds in [3, 5, 7, 10]:
try:
ts = setup(data, target='value', fh=30, fold=n_folds,
session_id=42, verbose=False)
model = create_model('auto_arima')
# 결과 기록 (실제로는 create_model 출력에서 평균값 추출)
fold_results.append({'Folds': n_folds, 'Model': 'auto_arima'})
except Exception as e:
print(f"Fold {n_folds} 오류: {e}")
print(pd.DataFrame(fold_results))
폴드 수 가이드
데이터가 적을 때:
- 3~5 폴드
- Expanding 권장
데이터가 많을 때:
- 5~10 폴드
- Sliding도 가능
폴드가 많을수록:
- 평가 안정적
- 계산 비용 증가
시계열 CV 체크리스트
1. 시간 순서 유지
- 항상 과거 → 미래
- 미래 데이터로 과거 예측 금지
2. Gap 고려
- 실제 배포 시나리오 반영
- 데이터 수집/처리 지연
3. 계절성 고려
- 테스트 기간에 모든 계절 포함
- 최소 1주기 이상 학습
4. 충분한 학습 데이터
- 첫 폴드도 충분한 데이터 필요
- 모델 학습에 필요한 최소량 확보
정리
- 시계열 CV: 시간 순서 유지 필수
- Expanding: 데이터 누적, 데이터 적을 때
- Sliding: 고정 윈도우, 패턴 변화 시
- Gap 설정으로 실제 시나리오 반영
- Walk-Forward로 실전적 검증
다음 글 예고
다음 글에서는 시계열 특성 엔지니어링을 다룹니다.
PyCaret 머신러닝 마스터 시리즈 #077