본문으로 건너뛰기

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