콘텐츠로 건너뛰기

데이터로 보는 트렌드: 시계열 데이터의 비밀(2)

  • 테크

시계열 예측은 데이터를 분석하고 미래를 예측하는 데 중요한 역할을 합니다. 이번 Part 2에서는 고전적 시계열 모델부터 머신러닝 기반 모델까지 다양한 예측 기법들을 다룰 예정입니다. Part 1에서 시계열 데이터의 구성 요소와 전처리 방법을 익혔다면, 이제 이 데이터를 기반으로 예측을 수행하는 방법을 알보겠습니다.

이전 글 보기: 데이터로 보는 트렌드: 시계열 데이터의 비밀(1)

Part 2: 시계열 예측 모델링 기초

고전적 시계열 모델 이해하기

시계열 예측에서 가장 기본이 되는 고전적 모델들은 여전히 많은 실무 현장에서 활용되고 있습니다. 이들 모델은 간단하면서도 강력한 성능을 자랑하며, 예측의 첫 걸음으로 적합합니다.

기본 시계열 모델
  • 이동평균 (Moving Average): 과거 데이터의 평균을 사용하여 미래를 예측하는 가장 기본적인 방법입니다. 예를 들어, 주간 판매량을 예측할 때 과거 4주간의 평균을 사용할 수 있습니다.
  • 지수평활 (Exponential Smoothing): 최근 데이터에 더 높은 가중치를 부여하여 빠르게 변화하는 패턴을 더 잘 포착하는 방법입니다. α(알파) 파라미터를 통해 최근 데이터의 중요도를 조절할 수 있습니다.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.holtwinters import ExponentialSmoothing

# 샘플 데이터 생성
np.random.seed(42)
dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D')
sales = 100 + np.random.normal(0, 10, len(dates)) + \
        np.sin(np.arange(len(dates)) * 2 * np.pi / 30) * 20  # 계절성 추가
data = pd.Series(sales, index=dates, name='Sales')

def plot_moving_average(data, windows=[7, 14, 30]):
    """이동평균 시각화"""
    plt.figure(figsize=(12, 6))
    plt.plot(data.index, data.values, label='Original', alpha=0.5)
    
    for window in windows:
        ma = data.rolling(window=window).mean()
        plt.plot(data.index, ma, 
                label=f'{window}-day Moving Average', 
                alpha=0.8)
    
    plt.title('Moving Average Example')
    plt.legend()
    plt.grid(True)
    plt.show()

def plot_exponential_smoothing(data, alphas=[0.2, 0.5, 0.8]):
    """지수평활 시각화"""
    plt.figure(figsize=(12, 6))
    plt.plot(data.index, data.values, label='Original', alpha=0.5)
    
    for alpha in alphas:
        exp_smoothed = pd.Series(index=data.index)
        exp_smoothed[0] = data[0]
        
        for t in range(1, len(data)):
            exp_smoothed[t] = alpha * data[t] + (1 - alpha) * exp_smoothed[t-1]
            
        plt.plot(data.index, exp_smoothed, 
                label=f'Exponential Smoothing (α={alpha})', 
                alpha=0.8)
    
    plt.title('Exponential Smoothing Example')
    plt.legend()
    plt.grid(True)
    plt.show()

# 시각화 실행
plot_moving_average(data)
plot_exponential_smoothing(data)

# 성능 비교
def compare_models(data):
    results = pd.DataFrame(index=['RMSE'])
    
    # 7일 이동평균
    ma7 = data.rolling(window=7).mean()
    results['MA(7)'] = np.sqrt(np.mean((data - ma7.shift(1))**2))
    
    # 지수평활 (α=0.3)
    exp_smooth = pd.Series(index=data.index)
    exp_smooth[0] = data[0]
    alpha = 0.3
    for t in range(1, len(data)):
        exp_smooth[t] = alpha * data[t] + (1 - alpha) * exp_smooth[t-1]
    results['Exp Smoothing(0.3)'] = np.sqrt(np.mean((data - exp_smooth.shift(1))**2))
    
    return results.round(2)

print("\nModel Performance Comparison:")
print(compare_models(data))
ARIMA와 SARIMA 모델링

고전적 모델들이 직관적이고 간단하지만, 보다 정교한 예측을 위해서는 ARIMA(AutoRegressive Integrated Moving Average)와 SARIMA(Seasonal ARIMA) 모델이 유용합니다. 이들은 시계열 데이터의 자기상관성과 계절성 패턴을 반영할 수 있어, 더욱 정밀한 예측을 가능하게 합니다.

  • ARIMA: 데이터의 자기회귀(AR), 이동평균(MA), 차분(I)을 결합하여 비정상성을 제거하고 예측을 수행합니다.

ARIMA 모델의 주요 파라미터:

  1. p(자기회귀 차수) : 과거 값들의 영향을 얼마나 고려할지
  2. d(차분 차수) : 시계열을 정상화하기 위한 차분 횟수
  3. q(이동평균 차수) : 과거 오차항의 영향을 얼마나 고려할지
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.stattools import adfuller

def check_stationarity(data):
    """정상성 검정"""
    result = adfuller(data)
    print('ADF Statistic:', result[0])
    print('p-value:', result[1])
    print('\nCritical values:')
    for key, value in result[4].items():
        print(f'\t{key}: {value}')

def fit_arima(data, order=(1,1,1)):
    """ARIMA 모델 적용"""
    model = ARIMA(data, order=order)
    results = model.fit()
    
    # 모델 성능 지표
    print("\nModel Statistics:")
    print(f'AIC: {results.aic:.2f}')
    print(f'BIC: {results.bic:.2f}')
    
    # 예측 시각화
    plt.figure(figsize=(12, 6))
    plt.plot(data.index, data.values, label='Original')
    plt.plot(data.index, results.fittedvalues, label='ARIMA Fitted')
    plt.title(f'ARIMA{order} Model Fitting')
    plt.legend()
    plt.grid(True)
    plt.show()
    
    return results

# ARIMA 모델 적용
print("Stationarity Test:")
check_stationarity(data)
arima_results = fit_arima(data)
  • SARIMA: ARIMA의 확장된 형태로, 계절성을 반영할 수 있는 모델입니다. 연간, 월간, 주간 등의 주기적 패턴이 있는 데이터에 특히 유용합니다.
AIC와 BIC: 모델 선택의 기준

모델을 선택할 때는 객관적인 평가 지표가 필요합니다. AIC와 BIC는 모델의 적합도와 복잡성을 평가하는 데 중요한 역할을 합니다.

  • AIC (Akaike Information Criterion): 모델의 적합도를 평가하면서 과적합을 방지하는 페널티 항을 포함하여, 값이 작을수록 더 좋은 모델을 의미합니다.
  • BIC (Bayesian Information Criterion): AIC와 유사하지만 더 강한 페널티를 부여하여 모델 선택에 있어 더 보수적인 기준을 제공합니다. 샘플 크기를 고려한 보정이 이루어집니다.
import pandas as pd
import numpy as np
from statsmodels.tsa.arima.model import ARIMA

def compare_arima_models(data, p_range, d_range, q_range):
    """
    다양한 ARIMA 모델의 AIC와 BIC를 비교
    
    Parameters:
    -----------
    data : Series
        시계열 데이터
    p_range : range
        AR 차수 범위
    d_range : range
        차분 차수 범위
    q_range : range
        MA 차수 범위
    
    Returns:
    --------
    DataFrame
        각 모델의 파라미터와 평가 지표
    """
    results = []
    
    for p in p_range:
        for d in d_range:
            for q in q_range:
                try:
                    model = ARIMA(data, order=(p, d, q))
                    fitted = model.fit()
                    
                    results.append({
                        'p': p,
                        'd': d,
                        'q': q,
                        'AIC': fitted.aic,
                        'BIC': fitted.bic,
                    })
                except:
                    continue
    
    results_df = pd.DataFrame(results)
    return results_df.sort_values('AIC')

# 사용 예시
if __name__ == "__main__":
    # 샘플 데이터 생성
    np.random.seed(42)
    dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D')
    data = pd.Series(np.random.normal(0, 1, len(dates)) + \
                    np.sin(np.arange(len(dates)) * 2 * np.pi / 30), 
                    index=dates)
    
    # 다양한 ARIMA 모델 비교
    models_comparison = compare_arima_models(
        data,
        p_range=range(0, 3),
        d_range=range(0, 2),
        q_range=range(0, 3)
    )
    
    print("Top 5 models by AIC:")
    print(models_comparison.head())
    
    print("\nBest model parameters:")
    best_model = models_comparison.iloc[0]
    print(f"ARIMA({best_model['p']},{best_model['d']},{best_model['q']})")

    이 두 지표를 함께 고려하면 단순히 데이터에 적합한 모델 뿐 아니라 일반화 가능성이 높은 모델을 선택할 수 있습니다.

    LightGBM을 활용한 시계열 예측

    고전적 시계열 모델들이 유용하지만, 머신러닝 기반 모델은 대규모 데이터셋을 다룰 때 더 큰 효과를 발휘합니다. 특히 LightGBM은 시계열 예측에서도 뛰어난 성능을 보여주고 있습니다.

    LightGBM의 주요 특징

    LightGBM은 그라디언트 부스팅 트리(Gradient Boosting Tree) 기반의 모델로, 다음과 같은 장점을 제공합니다:

    • 빠른 학습 속도와 높은 예측 정확도
    • 대규모 데이터 처리 능력
    • 효율적인 메모리 사용
    시계열 데이터를 위한 특성 엔지니어링

    LightGBM을 시계열 예측에 적용할 때는 다음과 같은 특성들을 활용합니다:

    • 시간 정보 특성: 시간, 요일, 월, 분기, 연도 등
    • 시차(Lag) 특성: 과거 1일, 7일, 14일, 30일 등
    • 통계적 특성: 이동 평균, 이동 표준편차 등
    import lightgbm as lgb
    import pandas as pd
    import numpy as np
    from sklearn.model_selection import TimeSeriesSplit
    from sklearn.metrics import mean_squared_error, mean_absolute_error
    import matplotlib.pyplot as plt
    
    class TimeSeriesMLPredictor:
        def __init__(self, data, target_col='value'):
            self.data = data
            self.target_col = target_col
            self.model = None
            self.feature_importance = None
            
        def create_features(self, df):
            """시계열 특성 생성"""
            df = df.copy()
            
            # 시간 관련 특성
            df['hour'] = df.index.hour
            df['dayofweek'] = df.index.dayofweek
            df['quarter'] = df.index.quarter
            df['month'] = df.index.month
            df['year'] = df.index.year
            df['dayofyear'] = df.index.dayofyear
            
            # 시차 특성
            for lag in [1, 7, 14, 30]:
                df[f'lag_{lag}'] = df[self.target_col].shift(lag)
            
            # 이동 평균
            for window in [7, 14, 30]:
                df[f'rolling_mean_{window}'] = df[self.target_col].rolling(window=window).mean()
                df[f'rolling_std_{window}'] = df[self.target_col].rolling(window=window).std()
            
            return df
        
        def prepare_data(self):
            """데이터 준비"""
            df = self.create_features(self.data)
            
            # NaN 제거
            df = df.dropna()
            
            # 특성과 타겟 분리
            feature_columns = [col for col in df.columns if col != self.target_col]
            X = df[feature_columns]
            y = df[self.target_col]
            
            return X, y
        
        def train(self, params=None):
            """모델 학습"""
            if params is None:
                params = {
                    'objective': 'regression',
                    'metric': 'rmse',
                    'num_leaves': 31,
                    'learning_rate': 0.05,
                    'feature_fraction': 0.9
                }
            
            X, y = self.prepare_data()
            
            # 시계열 교차 검증
            tscv = TimeSeriesSplit(n_splits=5)
            
            models = []
            cv_scores = []
            
            for fold, (train_idx, val_idx) in enumerate(tscv.split(X), 1):
                X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
                y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
                
                train_set = lgb.Dataset(X_train, y_train)
                val_set = lgb.Dataset(X_val, y_val)
                
                model = lgb.train(
                    params,
                    train_set,
                    valid_sets=[train_set, val_set],
                    num_boost_round=1000,
                    early_stopping_rounds=50,
                    verbose_eval=False
                )
                
                models.append(model)
                
                # 검증 세트에 대한 예측
                predictions = model.predict(X_val)
                rmse = np.sqrt(mean_squared_error(y_val, predictions))
                cv_scores.append(rmse)
                
                print(f'Fold {fold} - RMSE: {rmse:.2f}')
            
            print(f'\nMean RMSE: {np.mean(cv_scores):.2f} (+/- {np.std(cv_scores):.2f})')
            
            # 최종 모델 선택 (가장 좋은 성능의 모델)
            best_model_idx = np.argmin(cv_scores)
            self.model = models[best_model_idx]
            
            # 특성 중요도 저장
            self.feature_importance = pd.DataFrame({
                'feature': X.columns,
                'importance': self.model.feature_importance('gain')
            }).sort_values('importance', ascending=False)
            
        def plot_feature_importance(self, top_n=10):
            """특성 중요도 시각화"""
            plt.figure(figsize=(10, 6))
            
            importance_df = self.feature_importance.head(top_n)
            plt.barh(importance_df['feature'], importance_df['importance'])
            
            plt.title(f'Top {top_n} Most Important Features')
            plt.xlabel('Feature Importance (gain)')
            plt.tight_layout()
            plt.show()
    
    # 사용 예시
    if __name__ == "__main__":
        # 샘플 데이터 생성
        dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='H')
        ts = pd.Series(
            np.random.normal(100, 10, len(dates)) + \
            np.sin(np.arange(len(dates)) * 2 * np.pi / 24) * 20,
            index=dates,
            name='value'
        )
        ts = pd.DataFrame(ts)
    
        # 모델 학습 및 시각화
        ml_predictor = TimeSeriesMLPredictor(ts)
        ml_predictor.train()
        ml_predictor.plot_feature_importance()

    마치며

    이번 Part 2에서는 시계열 예측 모델링의 기본적인 기법들을 살펴보았습니다. 고전적인 통계 모델부터 최신 머신러닝 기법까지, 각각의 장단점과 적용 방법을 이해하고, 실무에서 시계열 예측을 어떻게 활용할 수 있을지에 대해 알아보았습니다.

    다음 Part 3에서는 예측 모델의 성능을 지속적으로 관리하는 방법을 다룰 예정입니다. 예측 모델은 시간이 지나면서 정확도가 저하될 수 있기 때문에, 성능 변화를 탐지하고 적절한 재학습 주기를 설정하는 방법을 소개해드리겠습니다.

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다