INEEJI 시계열 예측 프로젝트¶
분석 기간: 2025-05 ~ 2025-06 분야: 시계열 예측, 앙상블 모델링 결과: 예측 대회 상위 성적
개요¶
INEEJI 주최 시계열 예측 대회 참가 프로젝트. 다양한 전처리 기법과 앙상블 모델을 적용하여 시계열 데이터의 미래 값을 예측했다.
데이터 구조¶
학습 데이터¶
| 파일 | 설명 | 크기 |
|---|---|---|
| data_train.csv | 학습용 시계열 데이터 | 158KB |
| data_test.csv | 테스트용 데이터 | 41KB |
| metadata.xlsx | 변수 설명 | 17KB |
주요 변수¶
- target: 예측 대상 시계열 값
- timestamp: 시간 인덱스
- features: 외생 변수들 (기상, 계절 등)
분석 방법론¶
1. 전처리 파이프라인¶
┌─────────────────────────────────────────────────────────────────┐
│ 전처리 파이프라인 │
│ │
│ 결측치 처리 → 이상치 탐지 → 피처 생성 → 정규화 → 분해 │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐│
│ │보간법 │──▶│IQR │──▶│Lag │──▶│MinMax │──▶│CEEMDAN ││
│ │(선형) │ │기반 │ │Rolling │ │Scaler │ │분해 ││
│ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘│
└─────────────────────────────────────────────────────────────────┘
2. CEEMDAN 분해¶
Complete Ensemble Empirical Mode Decomposition with Adaptive Noise:
from PyEMD import CEEMDAN
import numpy as np
def decompose_signal(signal: np.ndarray) -> tuple:
"""CEEMDAN으로 시계열 분해"""
ceemdan = CEEMDAN()
ceemdan.ceemdan(signal)
imfs = ceemdan.get_imfs_and_residue()
# IMF (Intrinsic Mode Functions) + Residue
# 각 IMF는 다른 주기의 진동 성분
return imfs
def reconstruct_from_imfs(imfs: np.ndarray, selected: list = None) -> np.ndarray:
"""선택된 IMF로 신호 재구성"""
if selected is None:
selected = list(range(len(imfs)))
return np.sum(imfs[selected], axis=0)
CEEMDAN의 장점: - 비선형, 비정상 시계열에 효과적 - Mode mixing 문제 완화 - 각 IMF를 별도로 예측 가능
3. 피처 엔지니어링¶
import pandas as pd
import numpy as np
def create_time_features(df: pd.DataFrame, date_col: str) -> pd.DataFrame:
"""시간 기반 피처 생성"""
df = df.copy()
df['date'] = pd.to_datetime(df[date_col])
# 기본 시간 피처
df['hour'] = df['date'].dt.hour
df['dayofweek'] = df['date'].dt.dayofweek
df['month'] = df['date'].dt.month
df['quarter'] = df['date'].dt.quarter
df['dayofyear'] = df['date'].dt.dayofyear
# 주기 인코딩 (순환 특성 보존)
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
df['dow_sin'] = np.sin(2 * np.pi * df['dayofweek'] / 7)
df['dow_cos'] = np.cos(2 * np.pi * df['dayofweek'] / 7)
return df
def create_lag_features(df: pd.DataFrame, target: str, lags: list) -> pd.DataFrame:
"""지연 피처 생성"""
df = df.copy()
for lag in lags:
df[f'{target}_lag_{lag}'] = df[target].shift(lag)
# 롤링 통계
for window in [7, 14, 30]:
df[f'{target}_rolling_mean_{window}'] = df[target].rolling(window).mean()
df[f'{target}_rolling_std_{window}'] = df[target].rolling(window).std()
# 차분
df[f'{target}_diff_1'] = df[target].diff(1)
df[f'{target}_diff_7'] = df[target].diff(7)
return df
모델링¶
1. 단일 모델 비교¶
| 모델 | RMSE | MAE | 특징 |
|---|---|---|---|
| SVR | 0.142 | 0.098 | 비선형 패턴 캡처 |
| LightGBM | 0.156 | 0.108 | 빠른 학습, 피처 중요도 |
| XGBoost | 0.161 | 0.112 | 안정적 성능 |
| LSTM | 0.178 | 0.124 | 장기 의존성 |
2. 앙상블 전략¶
from sklearn.svm import SVR
from sklearn.ensemble import BaggingRegressor
import lightgbm as lgb
def create_ensemble_model():
"""Bagging SVR + LightGBM 앙상블"""
# Bagging SVR
base_svr = SVR(kernel='rbf', C=10, gamma='scale')
bagging_svr = BaggingRegressor(
estimator=base_svr,
n_estimators=10,
max_samples=0.8,
random_state=42
)
# LightGBM
lgbm = lgb.LGBMRegressor(
n_estimators=500,
learning_rate=0.05,
num_leaves=31,
random_state=42
)
return bagging_svr, lgbm
def voting_ensemble(models: list, X: np.ndarray, weights: list = None) -> np.ndarray:
"""가중 평균 앙상블"""
if weights is None:
weights = [1/len(models)] * len(models)
predictions = []
for model, weight in zip(models, weights):
pred = model.predict(X)
predictions.append(pred * weight)
return np.sum(predictions, axis=0)
3. CEEMDAN + Bagging SVR¶
핵심 아이디어: 각 IMF를 개별 모델로 예측 후 합산
def ceemdan_bagging_svr_pipeline(train_series, test_horizon):
"""CEEMDAN 분해 + Bagging SVR 파이프라인"""
# 1. 시계열 분해
imfs = decompose_signal(train_series.values)
predictions_per_imf = []
for i, imf in enumerate(imfs):
# 2. 각 IMF에 대해 피처 생성
df = pd.DataFrame({'target': imf})
df = create_lag_features(df, 'target', lags=[1, 2, 3, 7, 14])
df = df.dropna()
X = df.drop('target', axis=1)
y = df['target']
# 3. Bagging SVR 학습
model = BaggingRegressor(
estimator=SVR(kernel='rbf', C=10),
n_estimators=10,
random_state=42
)
model.fit(X, y)
# 4. 예측 (재귀적)
imf_predictions = recursive_forecast(model, X.iloc[-1:], test_horizon)
predictions_per_imf.append(imf_predictions)
# 5. IMF 예측 합산
final_predictions = np.sum(predictions_per_imf, axis=0)
return final_predictions
def recursive_forecast(model, last_features, horizon):
"""재귀적 다단계 예측"""
predictions = []
current_features = last_features.copy()
for _ in range(horizon):
pred = model.predict(current_features)[0]
predictions.append(pred)
# 피처 업데이트 (lag 시프트)
current_features = update_lag_features(current_features, pred)
return np.array(predictions)
결과¶
최종 성능¶
| 제출 버전 | 모델 | Public Score | Private Score |
|---|---|---|---|
| v1 | SVR 단일 | 0.158 | 0.162 |
| v2 | Bagging SVR | 0.148 | 0.151 |
| v3 | CEEMDAN + Bagging SVR | 0.139 | 0.143 |
| v4 (최종) | Voting (SVR + LGBM) | 0.132 | 0.138 |
피처 중요도 (LightGBM)¶
피처 중요도
────────────────────────────────────────────────
target_lag_1 ████████████████████████ 0.28
target_lag_7 ██████████████████ 0.21
rolling_mean_7 ███████████████ 0.18
hour_sin ████████████ 0.14
target_diff_1 █████████ 0.11
dow_sin ██████ 0.08
────────────────────────────────────────────────
학습 포인트¶
성공 요인¶
- CEEMDAN 분해: 복잡한 시계열을 단순한 성분으로 분리
- Bagging: SVR의 과적합 방지
- 앙상블: SVR + LGBM 조합으로 편향-분산 균형
- 순환 인코딩: 시간 피처의 순환 특성 보존
개선 가능점¶
- 자동 하이퍼파라미터 튜닝: Optuna 활용
- 딥러닝 추가: Transformer 기반 모델
- 확률적 예측: 점추정 → 구간추정
- 외생 변수 활용: 더 많은 외부 데이터
코드¶
전체 파이프라인¶
# INEEJI_Test_EomGyuHyeon_modeling.py
import pandas as pd
import numpy as np
from sklearn.svm import SVR
from sklearn.ensemble import BaggingRegressor
import lightgbm as lgb
from PyEMD import CEEMDAN
def main():
# 1. 데이터 로드
train = pd.read_csv('train_ready.csv')
test = pd.read_csv('test_ready.csv')
# 2. 피처 엔지니어링
train = create_time_features(train, 'timestamp')
train = create_lag_features(train, 'target', [1, 2, 3, 7, 14, 21])
train = train.dropna()
# 3. 학습/검증 분리
train_size = int(len(train) * 0.8)
X_train, X_val = train.iloc[:train_size], train.iloc[train_size:]
# 4. 모델 학습
features = [c for c in train.columns if c not in ['target', 'timestamp', 'date']]
# Bagging SVR
bagging_svr = BaggingRegressor(
estimator=SVR(kernel='rbf', C=10, gamma='scale'),
n_estimators=10,
random_state=42
)
bagging_svr.fit(X_train[features], X_train['target'])
# LightGBM
lgbm = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05)
lgbm.fit(X_train[features], X_train['target'])
# 5. 앙상블 예측
pred_svr = bagging_svr.predict(test[features])
pred_lgbm = lgbm.predict(test[features])
final_pred = 0.6 * pred_svr + 0.4 * pred_lgbm
# 6. 제출 파일 생성
submission = pd.DataFrame({
'id': test['id'],
'target': final_pred
})
submission.to_csv('submission.csv', index=False)
if __name__ == '__main__':
main()
파일 구조¶
ineeji/
├── data_train.csv # 학습 데이터
├── data_test.csv # 테스트 데이터
├── metadata.xlsx # 변수 설명
├── train_ready.csv # 전처리된 학습 데이터
├── test_ready.csv # 전처리된 테스트 데이터
├── INEEJI_Test_EomGyuHyeon.ipynb # 분석 노트북
├── INEEJI_Test_EomGyuHyeon_modeling.py # 모델링 코드
├── INEEJI_Report_EomGyuHyeon.pdf # 보고서
├── submission*.csv # 제출 파일들
└── 엄규현_PT_발표자료.pdf # 발표 자료
참고¶
- 원본 데이터: iCloud/03_Projects/ineeji/
- 주요 라이브러리: PyEMD, scikit-learn, LightGBM
- 참고 논문: "Complete Ensemble EMD with Adaptive Noise" (Torres et al., 2011)
마지막 업데이트: 2026-03-04