053 시계열 분해 (Decomposition)
키워드: 분해, decomposition, 트렌드, 계절성
개요
시계열 분해는 시계열 데이터를 트렌드, 계절성, 잔차로 분리하는 기법입니다. 이를 통해 데이터의 패턴을 이해하고 더 나은 예측 모델을 구축할 수 있습니다.
실습 환경
- Python 버전: 3.11 권장
- 필요 패키지:
statsmodels, pandas, numpy
pip install statsmodels pandas numpy matplotlib
시계열 분해 개념
가법 모델 vs 승법 모델
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
# 053 데이터 생성
np.random.seed(42)
dates = pd.date_range('2021-01-01', periods=730, freq='D') # 2년
# 053 가법 모델: Y = T + S + R
trend_additive = np.linspace(100, 200, 730)
season_additive = 20 * np.sin(np.arange(730) * 2 * np.pi / 365)
noise_additive = np.random.randn(730) * 5
y_additive = trend_additive + season_additive + noise_additive
# 053 승법 모델: Y = T × S × R
trend_multiplicative = np.linspace(100, 200, 730)
season_multiplicative = 1 + 0.2 * np.sin(np.arange(730) * 2 * np.pi / 365)
noise_multiplicative = 1 + np.random.randn(730) * 0.05
y_multiplicative = trend_multiplicative * season_multiplicative * noise_multiplicative
# 053 시각화
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
axes[0].plot(dates, y_additive)
axes[0].set_title('Additive Model: Y = Trend + Seasonality + Residual')
axes[0].set_ylabel('Value')
axes[1].plot(dates, y_multiplicative)
axes[1].set_title('Multiplicative Model: Y = Trend × Seasonality × Residual')
axes[1].set_ylabel('Value')
plt.tight_layout()
plt.show()
# 053 모델 선택 가이드
model_guide = {
'특성': ['계절 변동 크기', '적합한 경우', '예시'],
'가법 모델': [
'일정함',
'트렌드와 무관한 계절성',
'온도, 강수량'
],
'승법 모델': [
'트렌드에 비례',
'트렌드에 따라 계절성 증가',
'매출, 주가, 항공 승객'
]
}
print("가법 vs 승법 모델 선택:")
print(pd.DataFrame(model_guide).to_string(index=False))
statsmodels로 분해
가법 분해
# 053 Series 생성
ts_additive = pd.Series(y_additive, index=dates)
# 053 가법 분해
decomposition_add = seasonal_decompose(ts_additive, model='additive', period=365)
# 053 시각화
fig, axes = plt.subplots(4, 1, figsize=(14, 12))
axes[0].plot(decomposition_add.observed)
axes[0].set_title('Observed')
axes[0].set_ylabel('Value')
axes[1].plot(decomposition_add.trend)
axes[1].set_title('Trend')
axes[1].set_ylabel('Value')
axes[2].plot(decomposition_add.seasonal)
axes[2].set_title('Seasonal')
axes[2].set_ylabel('Value')
axes[3].plot(decomposition_add.resid)
axes[3].set_title('Residual')
axes[3].set_ylabel('Value')
plt.tight_layout()
plt.show()
# 053 구성요소 통계
print("가법 분해 결과:")
print(f" 트렌드 범위: {decomposition_add.trend.min():.2f} ~ {decomposition_add.trend.max():.2f}")
print(f" 계절성 범위: {decomposition_add.seasonal.min():.2f} ~ {decomposition_add.seasonal.max():.2f}")
print(f" 잔차 표준편차: {decomposition_add.resid.std():.4f}")
승법 분해
# 053 Series 생성
ts_multiplicative = pd.Series(y_multiplicative, index=dates)
# 053 승법 분해
decomposition_mult = seasonal_decompose(ts_multiplicative, model='multiplicative', period=365)
# 053 시각화
fig = decomposition_mult.plot()
fig.set_size_inches(14, 12)
plt.tight_layout()
plt.show()
print("\n승법 분해 결과:")
print(f" 트렌드 범위: {decomposition_mult.trend.min():.2f} ~ {decomposition_mult.trend.max():.2f}")
print(f" 계절성 범위: {decomposition_mult.seasonal.min():.4f} ~ {decomposition_mult.seasonal.max():.4f}")
print(f" 잔차 표준편차: {decomposition_mult.resid.std():.4f}")
STL 분해
STL (Seasonal and Trend decomposition using Loess)
from statsmodels.tsa.seasonal import STL
# 053 STL 분해 (더 유연함)
stl = STL(ts_additive, period=365, robust=True)
result = stl.fit()
# 053 시각화
fig, axes = plt.subplots(4, 1, figsize=(14, 12))
axes[0].plot(result.observed)
axes[0].set_title('Observed')
axes[1].plot(result.trend)
axes[1].set_title('Trend (STL)')
axes[2].plot(result.seasonal)
axes[2].set_title('Seasonal (STL)')
axes[3].plot(result.resid)
axes[3].set_title('Residual (STL)')
plt.tight_layout()
plt.show()
# 053 STL vs classical 비교
print("\n분해 방법 비교:")
print(f" Classical 잔차 std: {decomposition_add.resid.std():.4f}")
print(f" STL 잔차 std: {result.resid.std():.4f}")
STL 파라미터 조정
# 053 다양한 STL 설정
stl_configs = {
'default': STL(ts_additive, period=365).fit(),
'robust': STL(ts_additive, period=365, robust=True).fit(),
'low_pass': STL(ts_additive, period=365, seasonal=13).fit(),
}
fig, axes = plt.subplots(3, 1, figsize=(14, 9))
for ax, (name, stl_result) in zip(axes, stl_configs.items()):
ax.plot(stl_result.trend, label='Trend')
ax.set_title(f'STL Trend - {name}')
ax.legend()
plt.tight_layout()
plt.show()
분해 결과 활용
계절 조정 (Seasonally Adjusted)
# 053 계절 조정 데이터 = 원본 - 계절성 (가법)
# 053 계절 조정 데이터 = 원본 / 계절성 (승법)
# 053 가법 모델
seasonally_adjusted_add = ts_additive - decomposition_add.seasonal
# 053 승법 모델
seasonally_adjusted_mult = ts_multiplicative / decomposition_mult.seasonal
# 053 시각화
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
axes[0].plot(ts_additive, alpha=0.5, label='Original')
axes[0].plot(seasonally_adjusted_add, label='Seasonally Adjusted')
axes[0].set_title('Additive Model - Seasonal Adjustment')
axes[0].legend()
axes[1].plot(ts_multiplicative, alpha=0.5, label='Original')
axes[1].plot(seasonally_adjusted_mult, label='Seasonally Adjusted')
axes[1].set_title('Multiplicative Model - Seasonal Adjustment')
axes[1].legend()
plt.tight_layout()
plt.show()
트렌드 추출
# 053 트렌드만 분석
trend = decomposition_add.trend.dropna()
# 053 트렌드 기울기 계산
from scipy import stats
x = np.arange(len(trend))
slope, intercept, r_value, p_value, std_err = stats.linregress(x, trend)
print("트렌드 분석:")
print(f" 일일 증가율: {slope:.4f}")
print(f" 연간 증가율: {slope * 365:.2f}")
print(f" R²: {r_value**2:.4f}")
계절성 패턴 분석
# 053 계절성 패턴 (연간 주기)
seasonal = decomposition_add.seasonal
# 053 월별 평균 계절성
seasonal_df = pd.DataFrame({
'value': seasonal.values,
'month': seasonal.index.month
})
monthly_seasonal = seasonal_df.groupby('month')['value'].mean()
# 053 요일별 패턴 (주간 데이터라면)
seasonal_df['dayofweek'] = seasonal.index.dayofweek
daily_seasonal = seasonal_df.groupby('dayofweek')['value'].mean()
# 053 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].bar(monthly_seasonal.index, monthly_seasonal.values)
axes[0].set_xlabel('Month')
axes[0].set_ylabel('Seasonal Effect')
axes[0].set_title('Monthly Seasonal Pattern')
axes[1].bar(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], daily_seasonal.values)
axes[1].set_xlabel('Day of Week')
axes[1].set_ylabel('Seasonal Effect')
axes[1].set_title('Day of Week Pattern')
plt.tight_layout()
plt.show()
print("\n월별 계절성:")
for month, effect in monthly_seasonal.items():
print(f" {month}월: {effect:+.2f}")
분해를 활용한 예측
분해 기반 예측
def decomposition_forecast(series, period, steps, model='additive'):
"""분해 기반 예측"""
# 분해
decomp = seasonal_decompose(series, model=model, period=period)
# 트렌드 예측 (선형 회귀)
trend = decomp.trend.dropna()
x = np.arange(len(trend))
slope, intercept, _, _, _ = stats.linregress(x, trend)
# 미래 트렌드
future_x = np.arange(len(trend), len(trend) + steps)
future_trend = slope * future_x + intercept
# 계절성 (주기적 반복)
seasonal_pattern = decomp.seasonal.iloc[:period].values
future_seasonal = np.tile(seasonal_pattern, (steps // period) + 1)[:steps]
# 예측
if model == 'additive':
forecast = future_trend + future_seasonal
else:
forecast = future_trend * future_seasonal
# 날짜 인덱스
last_date = series.index[-1]
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=steps)
return pd.Series(forecast, index=future_dates)
# 053 예측
forecast_30 = decomposition_forecast(ts_additive, period=365, steps=30)
# 053 시각화
plt.figure(figsize=(14, 6))
plt.plot(ts_additive.iloc[-90:], label='Historical')
plt.plot(forecast_30, 'r-', label='Forecast')
plt.axvline(x=ts_additive.index[-1], color='gray', linestyle='--', alpha=0.5)
plt.title('Decomposition-based Forecast')
plt.legend()
plt.show()
print("30일 예측:")
print(forecast_30.head())
올바른 모델 선택
def select_decomposition_model(series, period):
"""가법/승법 모델 자동 선택"""
# 분해
decomp_add = seasonal_decompose(series, model='additive', period=period)
decomp_mult = seasonal_decompose(series, model='multiplicative', period=period)
# 잔차의 표준편차 비교
resid_add = decomp_add.resid.dropna().std()
resid_mult = decomp_mult.resid.dropna().std()
# 계절성 크기와 트렌드 관계 (상관관계)
trend = decomp_add.trend.dropna()
seasonal_range = series.groupby(series.index.month).apply(lambda x: x.max() - x.min())
# 결정
if resid_add < resid_mult:
recommendation = 'additive'
else:
recommendation = 'multiplicative'
print("모델 선택 분석:")
print(f" 가법 모델 잔차 std: {resid_add:.4f}")
print(f" 승법 모델 잔차 std: {resid_mult:.4f}")
print(f" 권장 모델: {recommendation}")
return recommendation
# 053 테스트
model = select_decomposition_model(ts_additive, 365)
정리
- 시계열 분해: 트렌드 + 계절성 + 잔차로 분리
- 가법 모델: 계절 변동이 일정할 때 (Y = T + S + R)
- 승법 모델: 계절 변동이 트렌드에 비례할 때 (Y = T × S × R)
- STL: 더 유연하고 이상치에 강건한 분해 방법
- 활용: 계절 조정, 트렌드 분석, 예측
다음 글 예고
다음 글에서는 정상성과 차분에 대해 알아보겠습니다. 시계열의 정상성 개념과 차분을 통한 정상화 방법을 다룹니다.
FLAML AutoML 마스터 시리즈 #053