고전적 분해 :: 시계열 분석 - mindscale
Skip to content

고전적 분해

이번 시간에는 시계열 분해 방법의 일종인 고전적 분해법에 대해서 알아보도록 하겠습니다. 고전적 분해법은 1920년대에 나온 방법입니다. 1920년이면 굉장히 옛날 방법이란 얘기죠. 그래서 옛날에 나온 방법들이 다 그렇지만 굉장히 단순한 방법입니다. 그래서 아주 옛날에 이제 1920년 되면 컴퓨터도 없던 시절이니까 손으로 계산해서 하던 시절이죠.

그렇지만은 이렇게 단순한 방법들이 사실은 굉장히 많은 경우에 이제 잘 적용이 되는 것을 볼 수 있는데 왜냐하면 복잡한 방법은 데이터가 좀 많아야 된다든지 이런저런 조건들이 붙는 경우가 많은데 옛날 방법들은 단순하기 때문에 또 바꾸면 어디에나 적용할 수 있는 그런 장점들이 있습니다.

그리고 이제 고전적 분해법에서 여러 가지 분해법들이 파생되어서 나왔기 때문에 다른 시계열 분해 방법의 기초가 된다라고 얘기를 할 수가 있습니다.

시계열 분해의 단계

분해를 하는 방법은 첫번째는 이동평균을 구해 가지고 추세 성분을 계산을 합니다. 이동평균을 적당해서 추세 성분을 계산한 다음에 시계열에서 일괄적으로 추세 성분을 제거를 하는 거죠.

그러면 이제 추세가 제거된, 만약에 이렇게 올라가는 데이터가 있는데 이런 추세를 찾아낸 다음에 추세를 빼면 수평으로 가는 데이터가 될 겁니다. 그러면 여기에서 계절 성분을 구하게 됩니다.

추세 구했고, 계절 성분을 구했으면 그 다음에 이 두 개를 빼면 나머지 성분이 되는 거죠. 시계열을 이렇게 순차적으로 분해를 하게 됩니다.

곱셈 분해

곱셈분해를 하는 경우는 똑같이 하는데 성분을 제거할 때, 뺄셈 대신에 나눗셈을 하면 되겠죠. 아니면 시계열에 로그를 취한 후, 덧셈분해를 하면 곱셈 분해를 한 것과 똑같이 됩니다.

전기 장비 주문 데이터

데이터는 행은 연도, 열은 월 형식으로 되어 있습니다.

import pandas as pd
df = pd.read_excel('elecequip.xlsx')
df.head()
year Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
0 1996 79.35 75.78 86.32 72.60 74.86 83.81 79.80 62.41 85.41 83.11 84.21 89.70
1 1997 78.64 77.42 89.86 81.27 78.68 89.51 83.67 69.80 91.09 89.43 91.04 92.87
2 1998 81.87 85.36 92.98 81.09 85.64 91.14 83.46 66.37 93.34 85.93 86.81 93.30
3 1999 81.59 81.77 91.24 79.45 86.99 96.60 97.99 79.13 103.56 100.89 99.40 111.80
4 2000 95.30 97.77 116.23 100.98 104.07 114.64 107.62 96.12 123.50 116.12 116.86 128.61

이를 연-월별로 한 행씩이 되도록 변환해줍니다. pd.meltid_vars로 지정된 열은 그대로 가져오고, 나머지 컬럼의 이름은 var_name 컬럼의 값으로 바꿔줍니다. 해당 컬럼의 값들은 value_name 컬럼의 값으로 변환됩니다.

ym = pd.melt(df, 
             id_vars=['year'], 
             var_name='month', 
             value_name='demand')
ym.head()
year month demand
0 1996 Jan 79.35
1 1997 Jan 78.64
2 1998 Jan 81.87
3 1999 Jan 81.59
4 2000 Jan 95.30

이제 날짜순 정렬을 위해서 month의 순서를 지정해주고, ym['month']를 이 순서대로 정의된 범주형 변수로 변환합니다.

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
ym['month'] = pd.Categorical(
    ym['month'], categories=months, ordered=True)

ymyearmonth 순으로 정렬하고, 인덱스를 재설정합니다.

ym = ym.sort_values(['year', 'month']).dropna().reset_index()
ym.head()
index year month demand
0 0 1996 Jan 79.35
1 17 1996 Feb 75.78
2 34 1996 Mar 86.32
3 51 1996 Apr 72.60
4 68 1996 May 74.86

추세 성분: 이동평균의 이동평균

이제 4개월 단위로 이동평균을 구합니다.

ma4 = ym.demand.rolling(4, center=True).mean()
ma4
0          NaN
1          NaN
2      78.5125
3      77.3900
4      79.3975
        ...   
190    94.6300
191    92.4800
192    90.7975
193    91.9050
194        NaN
Name: demand, Length: 195, dtype: float64

4개월 이동 평균의 2번 값은 78.5125입니다.

v1 = ma4.iloc[2]
v1
78.51249999999999

첫 4개월을 평균 내도 78.5125입니다.

v2 = ym.demand[:4].mean()
v2
78.51249999999999

이 둘은 거의 같은 값인 것을 알 수 있습니다. 컴퓨터에서 실수 계산은 약간의 오차가 있기 때문에 ==으로 비교하면 다르다고 나올 수 있습니다. 이때는 np.isclose로 비교합니다.

import numpy as np
np.isclose(v1, v2)
True

이동 평균을 시각화합니다.

import matplotlib.pyplot as plt
ym.demand.plot(color='lightgray')
ma4.plot(color='red', label='4-MA')
plt.legend()
<matplotlib.legend.Legend at 0x185d857da50>

4개월 이동 평균은 좌우 대칭이 아니므로 이를 다시 2개씩 묶어서 이동 평균을 구합니다. 그러면 5개월의 데이터를 각각 1/8, 1/4, 1/4, 1/4, 1/8 씩 반영한 것과 같음을 알 수 있습니다.

ma2x4 = ma4.rolling(2).mean()
v1 = ma2x4.iloc[3]
v2 = (ym.demand[0:5] * [1/8, 1/4, 1/4, 1/4, 1/8]).sum()
np.isclose(v1, v2)
True

이를 시각화합니다.

ym.demand.plot(color='lightgray')
ma4.plot(color='blue', label='4-MA')
ma2x4.plot(color='red', label='2x4-MA')
plt.legend(loc='upper left')
<matplotlib.legend.Legend at 0x185d89aaef0>

계절성 성분

이제 이동 평균을 추세 성분으로서 제거해줍니다. 데이터에 추세가 없어진 것을 볼 수 있습니다.

y2 = ym.demand - ma2x4
y2.plot()
<Axes: >

다음으로 계절성분을 제거하기 위해 월별 평균을 구합니다.

monthly = y2.groupby(ym.month).mean()
monthly.plot()
<Axes: xlabel='month'>

계절 성분을 총기간만큼 반복해줍니다.

total = len(ym)  # 총 개월 수
years = total // len(monthly) + 1  # 총 년수
seasonal = np.tile(monthly, years)[:total]

나머지 성분

이제 계절 성분도 제거하면 나머지 성분이 남습니다.

remainder = y2 - seasonal
remainder.plot()
plt.xticks(remainder.index[::12], ym.year[::12], rotation=90);