본문으로 건너뛰기

065 시계열 파트 총정리

키워드: 시계열 정리, 종합, 체크리스트

개요

이 글에서는 시계열 파트(051~064번)에서 다룬 내용을 종합적으로 정리합니다. 시계열 예측 프로젝트의 전체 흐름과 핵심 개념을 복습합니다.

시계열 예측 파이프라인

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from flaml import AutoML
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

# 065 전체 파이프라인 예시
np.random.seed(42)
n_days = 730

dates = pd.date_range('2022-01-01', periods=n_days, freq='D')
trend = np.linspace(100, 200, n_days)
seasonal = 30 * np.sin(np.arange(n_days) * 2 * np.pi / 365)
weekly = 10 * np.sin(np.arange(n_days) * 2 * np.pi / 7)
noise = np.random.randn(n_days) * 15

values = trend + seasonal + weekly + noise

df = pd.DataFrame({'date': dates, 'value': values})
print("시계열 데이터 준비 완료")
print(f"기간: {df['date'].min().date()} ~ {df['date'].max().date()}")

1단계: 데이터 이해 (EDA)

def time_series_eda(df, date_col='date', target_col='value'):
"""시계열 EDA 종합"""
print("=" * 50)
print("시계열 데이터 탐색적 분석")
print("=" * 50)

# 기본 통계
print("\n[기본 통계]")
print(df[target_col].describe())

# 결측치
print(f"\n[결측치]")
print(f" 날짜: {df[date_col].isna().sum()}")
print(f" 값: {df[target_col].isna().sum()}")

# 시간 범위
print(f"\n[시간 범위]")
print(f" 시작: {df[date_col].min()}")
print(f" 종료: {df[date_col].max()}")
print(f" 기간: {(df[date_col].max() - df[date_col].min()).days}일")

# 시각화
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 전체 추이
axes[0, 0].plot(df[date_col], df[target_col])
axes[0, 0].set_title('Time Series')

# 분포
axes[0, 1].hist(df[target_col], bins=50, edgecolor='black')
axes[0, 1].set_title('Distribution')

# 자기상관
from pandas.plotting import autocorrelation_plot
autocorrelation_plot(df[target_col], ax=axes[1, 0])
axes[1, 0].set_title('Autocorrelation')

# 월별 박스플롯
df_temp = df.copy()
df_temp['month'] = df_temp[date_col].dt.month
df_temp.boxplot(column=target_col, by='month', ax=axes[1, 1])
axes[1, 1].set_title('Monthly Distribution')

plt.tight_layout()
plt.show()

time_series_eda(df)

2단계: 전처리

def preprocess_time_series(df, date_col='date', target_col='value'):
"""시계열 전처리 파이프라인"""
df = df.copy()

# 1. 정렬
df = df.sort_values(date_col).reset_index(drop=True)

# 2. 결측치 처리
if df[target_col].isna().sum() > 0:
df[target_col] = df[target_col].interpolate(method='linear')

# 3. 이상치 처리 (IQR)
Q1, Q3 = df[target_col].quantile([0.25, 0.75])
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR

outliers = (df[target_col] < lower) | (df[target_col] > upper)
if outliers.sum() > 0:
df.loc[outliers, target_col] = df[target_col].rolling(7, center=True).median()[outliers]
print(f" 이상치 {outliers.sum()}개 처리")

# 4. 날짜 연속성 확인
date_diff = df[date_col].diff().dt.days
gaps = date_diff[date_diff > 1]
if len(gaps) > 0:
print(f" 날짜 갭 {len(gaps)}개 발견")

return df

df_clean = preprocess_time_series(df)
print("전처리 완료")

3단계: 분해 및 정상성 검정

def analyze_components(df, target_col='value', period=365):
"""시계열 분해 및 정상성 분석"""
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller

# 분해
decomposition = seasonal_decompose(df[target_col], model='additive', period=min(period, len(df)//2))

fig, axes = plt.subplots(4, 1, figsize=(14, 12))
decomposition.observed.plot(ax=axes[0], title='Observed')
decomposition.trend.plot(ax=axes[1], title='Trend')
decomposition.seasonal.plot(ax=axes[2], title='Seasonal')
decomposition.resid.plot(ax=axes[3], title='Residual')
plt.tight_layout()
plt.show()

# ADF 테스트
result = adfuller(df[target_col].dropna())
print("\n[정상성 검정 (ADF)]")
print(f" 통계량: {result[0]:.4f}")
print(f" p-value: {result[1]:.4f}")
print(f" 정상성: {'Yes' if result[1] < 0.05 else 'No (차분 필요)'}")

return decomposition

decomp = analyze_components(df_clean)

4단계: 특성 엔지니어링

def create_ts_features(df, date_col='date', target_col='value'):
"""종합 시계열 특성 생성"""
df = df.copy()

# 날짜 특성
df['dayofweek'] = df[date_col].dt.dayofweek
df['month'] = df[date_col].dt.month
df['dayofyear'] = df[date_col].dt.dayofyear
df['weekofyear'] = df[date_col].dt.isocalendar().week.astype(int)
df['quarter'] = df[date_col].dt.quarter

# 주기적 인코딩
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
df['dow_sin'] = np.sin(2 * np.pi * df['dayofweek'] / 7)
df['dow_cos'] = np.cos(2 * np.pi * df['dayofweek'] / 7)

# 비즈니스 특성
df['is_weekend'] = (df['dayofweek'] >= 5).astype(int)
df['is_month_end'] = df[date_col].dt.is_month_end.astype(int)

# Lag 특성
for lag in [1, 7, 14, 28, 30]:
df[f'lag_{lag}'] = df[target_col].shift(lag)

# 이동 통계
for window in [7, 14, 30]:
df[f'rolling_mean_{window}'] = df[target_col].shift(1).rolling(window).mean()
df[f'rolling_std_{window}'] = df[target_col].shift(1).rolling(window).std()

# 차분
df['diff_1'] = df[target_col].diff(1)
df['diff_7'] = df[target_col].diff(7)

# 결측치 제거
df = df.dropna()

return df

df_features = create_ts_features(df_clean)
print(f"특성 생성 완료: {df_features.shape[1]}개 컬럼")

feature_cols = [col for col in df_features.columns
if col not in ['date', 'value']]
print(f"학습 특성: {len(feature_cols)}개")

5단계: 시계열 교차 검증

from sklearn.model_selection import TimeSeriesSplit

def time_series_cv_evaluate(model, X, y, n_splits=5):
"""시계열 교차 검증"""
tscv = TimeSeriesSplit(n_splits=n_splits)

fold_scores = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(X), 1):
X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx]

model.fit(X_tr, y_tr)
y_pred = model.predict(X_val)

mae = mean_absolute_error(y_val, y_pred)
fold_scores.append(mae)
print(f" Fold {fold}: MAE = {mae:.2f}")

print(f"\n 평균 MAE: {np.mean(fold_scores):.2f}{np.std(fold_scores):.2f})")
return fold_scores

# 065 CV 예시
from sklearn.ensemble import RandomForestRegressor
X = df_features[feature_cols]
y = df_features['value']

print("시계열 교차 검증:")
scores = time_series_cv_evaluate(RandomForestRegressor(n_estimators=50, random_state=42), X, y)

6단계: FLAML 모델 학습

# 065 시간 기반 분할
train_end = df_features['date'].max() - pd.Timedelta(days=60)
train = df_features[df_features['date'] <= train_end]
test = df_features[df_features['date'] > train_end]

X_train = train[feature_cols]
y_train = train['value']
X_test = test[feature_cols]
y_test = test['value']

print(f"학습: {len(train)}, 테스트: {len(test)}")

# 065 FLAML 학습
automl = AutoML()
automl.fit(
X_train, y_train,
task="regression",
time_budget=60,
metric="mae",
split_type="time",
n_splits=5,
verbose=1
)

y_pred = automl.predict(X_test)

print(f"\n최적 모델: {automl.best_estimator}")
print(f"MAE: {mean_absolute_error(y_test, y_pred):.2f}")
print(f"RMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.2f}")
print(f"R²: {r2_score(y_test, y_pred):.4f}")

7단계: 평가 및 시각화

def evaluate_forecast(y_true, y_pred, dates=None):
"""종합 예측 평가"""
metrics = {
'MAE': mean_absolute_error(y_true, y_pred),
'RMSE': np.sqrt(mean_squared_error(y_true, y_pred)),
'R²': r2_score(y_true, y_pred),
'MAPE': np.mean(np.abs((y_true - y_pred) / y_true)) * 100,
'Bias': np.mean(y_pred - y_true)
}

print("예측 성능 지표:")
for name, value in metrics.items():
print(f" {name}: {value:.2f}")

# 시각화
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 실제 vs 예측
if dates is not None:
axes[0, 0].plot(dates, y_true, label='Actual')
axes[0, 0].plot(dates, y_pred, label='Predicted', alpha=0.8)
else:
axes[0, 0].plot(y_true.values, label='Actual')
axes[0, 0].plot(y_pred, label='Predicted', alpha=0.8)
axes[0, 0].legend()
axes[0, 0].set_title('Actual vs Predicted')

# 잔차
residuals = y_true - y_pred
axes[0, 1].scatter(y_pred, residuals, alpha=0.5)
axes[0, 1].axhline(y=0, color='red', linestyle='--')
axes[0, 1].set_xlabel('Predicted')
axes[0, 1].set_ylabel('Residual')
axes[0, 1].set_title('Residual Plot')

# 잔차 분포
axes[1, 0].hist(residuals, bins=30, edgecolor='black')
axes[1, 0].set_title('Residual Distribution')

# 실제 vs 예측 산점도
axes[1, 1].scatter(y_true, y_pred, alpha=0.5)
min_val = min(y_true.min(), y_pred.min())
max_val = max(y_true.max(), y_pred.max())
axes[1, 1].plot([min_val, max_val], [min_val, max_val], 'r--')
axes[1, 1].set_xlabel('Actual')
axes[1, 1].set_ylabel('Predicted')
axes[1, 1].set_title('Actual vs Predicted Scatter')

plt.tight_layout()
plt.show()

return metrics

metrics = evaluate_forecast(y_test, y_pred, test['date'])

핵심 개념 체크리스트

checklist = {
'단계': [
'1. 데이터 이해',
'2. 전처리',
'3. 분해/정상성',
'4. 특성 엔지니어링',
'5. 교차 검증',
'6. 모델링',
'7. 평가'
],
'핵심 작업': [
'EDA, 패턴 파악, 이상치 확인',
'결측치, 이상치, 날짜 연속성',
'추세/계절성 분리, ADF 검정',
'Lag, 이동통계, 주기인코딩',
'TimeSeriesSplit (미래누출 방지)',
'FLAML split_type="time"',
'MAE, RMSE, MAPE, 잔차분석'
],
'주의사항': [
'충분한 시간 확보',
'보간 시 미래값 사용 금지',
'비정상성 시 차분 적용',
'shift(1)로 데이터 누출 방지',
'K-Fold 사용 금지',
'시간 순서 유지',
'여러 지표 종합 판단'
]
}

print("\n시계열 예측 체크리스트:")
print(pd.DataFrame(checklist).to_string(index=False))

방법론 선택 가이드

methods = {
'상황': [
'단기 예측 (1-7일)',
'중장기 예측 (1-3개월)',
'강한 계절성',
'다변량 데이터',
'비선형 패턴',
'빠른 프로토타입'
],
'추천 방법': [
'FLAML + Lag 특성',
'Prophet, ARIMA',
'SARIMA, Prophet',
'FLAML + 외생변수',
'FLAML (트리 기반)',
'FLAML AutoML'
],
'핵심 특성': [
'lag_1~7, rolling_mean_7',
'계절성 성분, 추세',
'월/주 주기인코딩',
'다변량 Lag, 상관특성',
'모든 특성 조합',
'기본 날짜/Lag 특성'
]
}

print("\n방법론 선택 가이드:")
print(pd.DataFrame(methods).to_string(index=False))

프로젝트 응용

projects = {
'프로젝트': ['주가 예측', '수요 예측', '에너지 예측'],
'핵심 특성': [
'RSI, MACD, 볼린저밴드',
'제품ID, 프로모션, 계절성',
'시간대, 기온, 요일'
],
'주요 지표': [
'방향 정확도, 수익률',
'MAPE, 안전재고',
'피크 MAE, 비용'
],
'비즈니스 활용': [
'매매 신호, 리스크 관리',
'재고 최적화, 발주량',
'피크 관리, 비용 절감'
]
}

print("\n프로젝트 응용:")
print(pd.DataFrame(projects).to_string(index=False))

종합 파이프라인 코드

def complete_ts_pipeline(df, date_col='date', target_col='value',
test_days=60, time_budget=60):
"""완전한 시계열 예측 파이프라인"""

# 1. 전처리
df = df.sort_values(date_col).reset_index(drop=True)

# 2. 특성 생성
df_feat = create_ts_features(df, date_col, target_col)

feature_cols = [col for col in df_feat.columns
if col not in [date_col, target_col]]

# 3. 분할
train_end = df_feat[date_col].max() - pd.Timedelta(days=test_days)
train = df_feat[df_feat[date_col] <= train_end]
test = df_feat[df_feat[date_col] > train_end]

X_train = train[feature_cols]
y_train = train[target_col]
X_test = test[feature_cols]
y_test = test[target_col]

# 4. FLAML 학습
automl = AutoML()
automl.fit(
X_train, y_train,
task="regression",
time_budget=time_budget,
metric="mae",
split_type="time",
verbose=0
)

# 5. 예측 및 평가
y_pred = automl.predict(X_test)

results = {
'model': automl.best_estimator,
'mae': mean_absolute_error(y_test, y_pred),
'rmse': np.sqrt(mean_squared_error(y_test, y_pred)),
'r2': r2_score(y_test, y_pred),
'predictions': y_pred,
'test_dates': test[date_col]
}

return results, automl

# 065 파이프라인 실행
results, model = complete_ts_pipeline(df, time_budget=30)
print("\n종합 파이프라인 결과:")
print(f" 모델: {results['model']}")
print(f" MAE: {results['mae']:.2f}")
print(f" RMSE: {results['rmse']:.2f}")
print(f" R²: {results['r2']:.4f}")

정리

시계열 파트에서 배운 내용:

  1. 기초 (051-054): 시계열 특성, 전처리, 분해, 정상성
  2. 모델링 (055-057): ARIMA, FLAML, Prophet
  3. 고급 (058-060): 특성 엔지니어링, 교차 검증, 다변량
  4. 프로젝트 (061-063): 주가, 수요, 에너지 예측
  5. 앙상블 (064): 모델 결합 기법

핵심 원칙:

  • 데이터 누출 방지: shift(), split_type="time"
  • 적절한 특성: Lag, 이동통계, 주기 인코딩
  • 올바른 검증: TimeSeriesSplit
  • 종합 평가: MAE, RMSE, MAPE, 잔차 분석

다음 파트 예고

다음 파트에서는 FLAML 고급 기능을 다룹니다. 하이퍼파라미터 튜닝, 커스텀 모델, 분산 학습 등 FLAML의 고급 활용법을 알아보겠습니다.


FLAML AutoML 마스터 시리즈 #065