[Python 시계열 자료 분석] 시계열 분해 (Time series Decomposition)
Python 분석과 프로그래밍/Python 통계분석 2020. 1. 2. 23:46지난번 포스팅에서는 시계열 자료의 구성 요인 (time series component factors)으로서 추세 요인, 순환 요인, 계절 요인, 불규칙 요인에 대해서 소개하였습니다.
지난번 포스팅에서는 가법 모형으로 가상의 시계열 자료를 만들었다면(time series composition), ==> 이번 포스팅에서는 반대로 시계열 분해(time series decomposition)를 통해 시계열 자료를 추세(순환)(Trend), 계절성(Seasonality), 잔차(Residual)로 분해를 해보겠습니다.
시계열 분해는 직관적으로 이해하기 쉽고 구현도 쉬워서 시계열 자료 분석의 고전적인 방법론이지만 지금까지도 꾸준히 사용되는 방법론입니다. 시계열 분해의 순서와 방법은 대략 아래와 같습니다.
(1) 시도표 (time series plot)를 보고 시계열의 주기적 반복/계절성이 있는지, 가법 모형(additive model, y = t + s + r)과 승법 모형(multiplicative model, y = t * s * r) 중 무엇이 더 적합할지 판단을 합니다.
(가법 모형을 가정할 시)
(2) 시계열 자료에서 추세(trend)를 뽑아내기 위해서 중심 이동 평균(centered moving average)을 이용합니다.
(3) 원 자료에서 추세 분해값을 빼줍니다(detrend). 그러면 계절 요인과 불규칙 요인만 남게 됩니다.
(4) 다음에 계절 주기 (seasonal period) 로 detrend 이후 남은 값의 합을 나누어주면 계절 평균(average seasonality)을 구할 수 있습니다. (예: 01월 계절 평균 = (2020-01 + 2021-01 + 2022-01 + 2023-01)/4, 02월 계절 평균 = (2020-02 + 2021-02 + 2022-02 + 2023-02)/4).
(5) 원래의 값에서 추세와 계절성 분해값을 빼주면 불규칙 요인(random, irregular factor)이 남게 됩니다.
시계열 분해 후에 추세와 계절성을 제외한 잔차(residual, random/irregular factor) 가 특정 패턴 없이 무작위 분포를 띠고 작은 값이면 추세와 계절성으로 모형화가 잘 되는 것이구요, 시계열 자료의 특성을 이해하고 예측하는데 활용할 수 있습니다. 만약 시계열 분해 후의 잔차에 특정 패턴 (가령, 주기적인 파동을 그린다거나, 분산이 점점 커진다거나 등..) 이 존재한다면 잔차에 대해서만 다른 모형을 추가로 적합할 수도 있겠습니다.
예제로 사용할 시계열 자료로서 '1차 선형 추세 + 4년 주기 순환 + 1년 단위 계절성 + 불규칙 noise' 의 가법 모형 (additive model)으로 시계열 데이터를 만들어보겠습니다.
import numpy as np import pandas as pd import matplotlib.pyplot as plt dates = pd.date_range('2020-01-01', periods=48, freq='M') dates [Out]: DatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31', '2020-04-30', '2020-05-31', '2020-06-30', '2020-07-31', '2020-08-31', '2020-09-30', '2020-10-31', '2020-11-30', '2020-12-31', '2021-01-31', '2021-02-28', '2021-03-31', '2021-04-30', '2021-05-31', '2021-06-30', '2021-07-31', '2021-08-31', '2021-09-30', '2021-10-31', '2021-11-30', '2021-12-31', '2022-01-31', '2022-02-28', '2022-03-31', '2022-04-30', '2022-05-31', '2022-06-30', '2022-07-31', '2022-08-31', '2022-09-30', '2022-10-31', '2022-11-30', '2022-12-31', '2023-01-31', '2023-02-28', '2023-03-31', '2023-04-30', '2023-05-31', '2023-06-30', '2023-07-31', '2023-08-31', '2023-09-30', '2023-10-31', '2023-11-30', '2023-12-31'], dtype='datetime64[ns]', freq='M') # additive model: trend + cycle + seasonality + irregular factor timestamp = np.arange(len(dates)) trend_factor = timestamp*1.1 cycle_factor = 10*np.sin(np.linspace(0, 3.14*2, 48)) seasonal_factor = 7*np.sin(np.linspace(0, 3.14*8, 48)) np.random.seed(2004) irregular_factor = 2*np.random.randn(len(dates)) df = pd.DataFrame({'timeseries': trend_factor + cycle_factor + seasonal_factor + irregular_factor, 'trend': trend_factor, 'cycle': cycle_factor, 'trend_cycle': trend_factor + cycle_factor, 'seasonal': seasonal_factor, 'irregular': irregular_factor}, index=dates) df [Out]:
|
(1) Python을 이용한 시계열 분해 (Time series decomposition using Python) |
Python의 statsmodels 라이브러리를 사용해서 가법 모형(additive model) 가정 하에 시계열 분해를 해보겠습니다.
from statsmodels.tsa.seasonal import seasonal_decompose ts = df.timeseries result = seasonal_decompose(ts, model='additive') plt.rcParams['figure.figsize'] = [12, 8] result.plot() plt.show() |
원래의 시계열 구성요소(추세+순환, 계절성, 불규칙 요인)와 시계열 분해(time series decomposition)를 통해 분리한 추세(&순환), 계절성, 잔차(불규칙 요인)를 겹쳐서 그려보았습니다. (즉, 원래 데이터의 추세요인과 시계열 분해를 통해 분리한 추세를 겹쳐서 그려보고, 원래 데이터의 계절요인과 시계열 분해를 통해 분리한 계절을 겹쳐서 그려보고, 원래 데이터의 불규칙 요인과 시계열 분해를 통해 분리한 잔차를 겹쳐서 그려봄)
원래의 데이터와 얼추 비슷하게, 그럴싸하게 시계열 분해를 한 것처럼 보이지요?
# ground truth & timeseries decompostion all together # -- observed data plt.figure(figsize=(12, 12)) plt.subplot(4,1, 1) result.observed.plot() plt.grid(True) plt.ylabel('Observed', fontsize=14) # -- trend & cycle factor plt.subplot(4, 1, 2) result.trend.plot() # from timeseries decomposition df.trend_cycle.plot() # ground truth plt.grid(True) plt.ylabel('Trend', fontsize=14) # -- seasonal factor plt.subplot(4, 1, 3) result.seasonal.plot() # from timeseries decomposition df.seasonal.plot() # ground truth plt.grid(True) plt.ylabel('Seasonality', fontsize=14) # -- irregular factor (noise) plt.subplot(4, 1, 4) result.resid.plot() # from timeseries decomposition df.irregular.plot() # ground truth plt.grid(True) plt.ylabel('Residual', fontsize=14) plt.show() |
원래의 관측치(observed), 추세(trend), 계절성(seasonal), 잔차(residual) 데이터 아래처럼 시계열 분해한 객체에서 obsered, trend, seasonal, resid 라는 attributes 를 통해서 조회할 수 있습니다.
Observed | Trend ( & Cycle) | ||
print(result.observed) [Out]: 2020-01-31 2.596119 2020-02-29 6.746160 2020-03-31 8.112100 2020-04-30 8.255941 2020-05-31 16.889655 2020-06-30 16.182357 2020-07-31 14.128087 2020-08-31 11.943313 2020-09-30 9.728095 2020-10-31 12.483489 2020-11-30 12.141808 2020-12-31 15.143334 2021-01-31 21.774516 2021-02-28 28.432892 2021-03-31 32.350583 2021-04-30 30.596556 2021-05-31 32.510523 2021-06-30 30.425519 2021-07-31 24.300958 2021-08-31 20.450917 2021-09-30 18.870881 2021-10-31 21.326310 2021-11-30 22.902448 2021-12-31 26.620578 2022-01-31 27.626499 2022-02-28 31.858923 2022-03-31 35.930469 2022-04-30 30.177870 2022-05-31 30.016165 2022-06-30 26.591729 2022-07-31 21.118481 2022-08-31 16.636031 2022-09-30 17.682613 2022-10-31 21.163298 2022-11-30 22.455672 2022-12-31 26.919529 2023-01-31 33.964623 2023-02-28 37.459776 2023-03-31 40.793766 2023-04-30 43.838415 2023-05-31 41.301780 2023-06-30 39.217866 2023-07-31 35.125502 2023-08-31 33.841926 2023-09-30 38.770511 2023-10-31 37.371216 2023-11-30 46.587633 2023-12-31 46.403326 Freq: M, Name: timeseries, dtype: float64 |
print(result.trend) [Out] 2020-01-31 NaN 2020-02-29 NaN 2020-03-31 NaN 2020-04-30 NaN 2020-05-31 NaN 2020-06-30 NaN 2020-07-31 11.994971 2020-08-31 13.697685 2020-09-30 15.611236 2020-10-31 17.552031 2020-11-30 19.133760 2020-12-31 20.378094 2021-01-31 21.395429 2021-02-28 22.173782 2021-03-31 22.909215 2021-04-30 23.658616 2021-05-31 24.475426 2021-06-30 25.402005 2021-07-31 26.124056 2021-08-31 26.510640 2021-09-30 26.802553 2021-10-31 26.934270 2021-11-30 26.812893 2021-12-31 26.549220 2022-01-31 26.256876 2022-02-28 25.965319 2022-03-31 25.756854 2022-04-30 25.700551 2022-05-31 25.675143 2022-06-30 25.668984 2022-07-31 25.945528 2022-08-31 26.442986 2022-09-30 26.878992 2022-10-31 27.650819 2022-11-30 28.690242 2022-12-31 29.686565 2023-01-31 30.796280 2023-02-28 32.096818 2023-03-31 33.692393 2023-04-30 35.246385 2023-05-31 36.927214 2023-06-30 38.744537 2023-07-31 NaN 2023-08-31 NaN 2023-09-30 NaN 2023-10-31 NaN 2023-11-30 NaN 2023-12-31 NaN Freq: M, Name: timeseries, dtype: float64 |
Seasonality | Residual (Noise) |
print(result.seasonal) [Out]: 2020-01-31 1.501630 2020-02-29 5.701170 2020-03-31 8.768065 2020-04-30 6.531709 2020-05-31 5.446174 2020-06-30 2.002476 2020-07-31 -1.643064 2020-08-31 -6.011071 2020-09-30 -7.807785 2020-10-31 -5.858728 2020-11-30 -5.849710 2020-12-31 -2.780867 2021-01-31 1.501630 2021-02-28 5.701170 2021-03-31 8.768065 2021-04-30 6.531709 2021-05-31 5.446174 2021-06-30 2.002476 2021-07-31 -1.643064 2021-08-31 -6.011071 2021-09-30 -7.807785 2021-10-31 -5.858728 2021-11-30 -5.849710 2021-12-31 -2.780867 2022-01-31 1.501630 2022-02-28 5.701170 2022-03-31 8.768065 2022-04-30 6.531709 2022-05-31 5.446174 2022-06-30 2.002476 2022-07-31 -1.643064 2022-08-31 -6.011071 2022-09-30 -7.807785 2022-10-31 -5.858728 2022-11-30 -5.849710 2022-12-31 -2.780867 2023-01-31 1.501630 2023-02-28 5.701170 2023-03-31 8.768065 2023-04-30 6.531709 2023-05-31 5.446174 2023-06-30 2.002476 2023-07-31 -1.643064 2023-08-31 -6.011071 2023-09-30 -7.807785 2023-10-31 -5.858728 2023-11-30 -5.849710 2023-12-31 -2.780867 Freq: M, Name: timeseries, dtype: float64 |
print(result.resid) [Out]: 2020-01-31 NaN 2020-02-29 NaN 2020-03-31 NaN 2020-04-30 NaN 2020-05-31 NaN 2020-06-30 NaN 2020-07-31 3.776179 2020-08-31 4.256699 2020-09-30 1.924644 2020-10-31 0.790186 2020-11-30 -1.142242 2020-12-31 -2.453893 2021-01-31 -1.122544 2021-02-28 0.557940 2021-03-31 0.673303 2021-04-30 0.406231 2021-05-31 2.588922 2021-06-30 3.021039 2021-07-31 -0.180034 2021-08-31 -0.048653 2021-09-30 -0.123887 2021-10-31 0.250769 2021-11-30 1.939265 2021-12-31 2.852225 2022-01-31 -0.132007 2022-02-28 0.192434 2022-03-31 1.405550 2022-04-30 -2.054390 2022-05-31 -1.105152 2022-06-30 -1.079730 2022-07-31 -3.183983 2022-08-31 -3.795884 2022-09-30 -1.388594 2022-10-31 -0.628793 2022-11-30 -0.384861 2022-12-31 0.013830 2023-01-31 1.666713 2023-02-28 -0.338212 2023-03-31 -1.666692 2023-04-30 2.060321 2023-05-31 -1.071608 2023-06-30 -1.529146 2023-07-31 NaN 2023-08-31 NaN 2023-09-30 NaN 2023-10-31 NaN 2023-11-30 NaN 2023-12-31 NaN Freq: M, Name: timeseries, dtype: float64 |
# export to csv file df.to_csv('ts_components.txt', sep=',', index=False) |
(2) R을 이용한 시계열 분해 (Time series Decomposition using R) |
위에서 가법 모형을 적용해서 Python으로 만든 시계열 자료를 text 파일로 내보낸 후, 이를 R에서 읽어서 시계열 분해 (time series decomposition)를 해보겠습니다.
# read time series text file df <- read.table('ts_components.txt', sep=',', header=T) head(df)
|
이렇게 불러와서 만든 df DataFrame의 칼럼 중에서 시계열 분해를 할 'timeseries' 칼럼만을 가져와서 ts() 함수를 사용하여 1년 12개월 이므로 frequency = 12로 설정해 R의 시계열 자료 형태로 변환합니다.
그 다음에 decompose() 함수를 사용하여 시계열 분해를 하는데요, 이때 가법 모형 (additive model)을 적용할 것이므로 decompose(ts, "additive") 라고 설정해줍니다.
시계열 분해를 한 결과를 모아놓은 리스트 ts_decompose 객체를 프린트해보면 원래의 값 $x, 계절 요인 $seasonal, 추세(&순환) 요인 $trend, 불규칙 요인 $random 분해값이 순서대로 저장되어 있음을 알 수 있습니다.
# transforming data to time series with 12 months frequency ts <- ts(df$timeseries, frequency = 12) # 12 months # time series decomposition ts_decompose <- decompose(ts, "additive") # additive model # decomposition results ts_decompose [Out]: $x Jan Feb Mar Apr May Jun Jul 1 2.596119 6.746160 8.112100 8.255941 16.889655 16.182357 14.128087 2 21.774516 28.432892 32.350583 30.596556 32.510523 30.425519 24.300958 3 27.626499 31.858923 35.930469 30.177870 30.016165 26.591729 21.118481 4 33.964623 37.459776 40.793766 43.838415 41.301780 39.217866 35.125502 Aug Sep Oct Nov Dec 1 11.943313 9.728095 12.483489 12.141808 15.143334 2 20.450917 18.870881 21.326310 22.902448 26.620578 3 16.636031 17.682613 21.163298 22.455672 26.919529 4 33.841926 38.770511 37.371216 46.587633 46.403326 $seasonal Jan Feb Mar Apr May Jun Jul 1 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 2 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 3 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 4 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 Aug Sep Oct Nov Dec 1 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 2 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 3 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 4 -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 $trend Jan Feb Mar Apr May Jun Jul Aug 1 NA NA NA NA NA NA 11.99497 13.69769 2 21.39543 22.17378 22.90922 23.65862 24.47543 25.40200 26.12406 26.51064 3 26.25688 25.96532 25.75685 25.70055 25.67514 25.66898 25.94553 26.44299 4 30.79628 32.09682 33.69239 35.24639 36.92721 38.74454 NA NA Sep Oct Nov Dec 1 15.61124 17.55203 19.13376 20.37809 2 26.80255 26.93427 26.81289 26.54922 3 26.87899 27.65082 28.69024 29.68657 4 NA NA NA NA $random Jan Feb Mar Apr May Jun 1 NA NA NA NA NA NA 2 -1.12254398 0.55793990 0.67330316 0.40623129 2.58892205 3.02103851 3 -0.13200705 0.19243403 1.40555039 -2.05439035 -1.10515213 -1.07973023 4 1.66671294 -0.33821202 -1.66669164 2.06032097 -1.07160802 -1.52914637 Jul Aug Sep Oct Nov Dec 1 3.77617915 4.25669908 1.92464365 0.79018590 -1.14224232 -2.45389325 2 -0.18003389 -0.04865275 -0.12388741 0.25076875 1.93926482 2.85222467 3 -3.18398336 -3.79588443 -1.38859434 -0.62879275 -0.38486059 0.01383049 4 NA NA NA NA NA NA $figure [1] 1.501630 5.701170 8.768065 6.531709 5.446174 2.002476 -1.643064 [8] -6.011071 -7.807785 -5.858728 -5.849710 -2.780867 $type [1] "additive" attr(,"class") [1] "decomposed.ts" |
위의 분해 결과가 숫자만 잔뜩 들어있으니 뭐가 뭔지 잘 눈에 안들어오지요? 그러면 이제 원래의 값 (observed)과 시계열 분해된 결과인 trend, seasonal, random 을 plot() 함수를 사용하여 다같이 시각화해보겠습니다.
# change plot in jupyter library(repr) # Change plot size to 12 x 10 options(repr.plot.width=12, repr.plot.height=10) plot(ts_decompose) |
위의 분해 결과를 Trend (ts_decompose$trend), Seasonal (ts_decompose$seasonal), Random (ts_decompose$random) 의 각 요소별로 나누어서 시각화해볼 수도 있습니다.
# change the plot size options(repr.plot.width=12, repr.plot.height=5) # Trend plot(as.ts(ts_decompose$trend)) # Seasonality plot(as.ts(ts_decompose$seasonal)) # Random (Irregular factor) plot(as.ts(ts_decompose$random)) |
많은 도움이 되었기를 바랍니다.
이번 포스팅이 도움이 되었다면 아래의 '공감~
'를 꾹 눌러주세요. :-)