지난번 포스팅에서는 시계열 자료의 구성 요인 (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]:

timeseriestrendcycletrend_cycleseasonalirregular
2020-01-312.5961190.00.0000000.0000000.0000002.596119
2020-02-296.7461601.11.3321982.4321983.5656840.748278
2020-03-318.1121002.22.6406474.8406476.136825-2.865371
2020-04-308.2559413.33.9020217.2020216.996279-5.942358
2020-05-3116.8896554.45.0938349.4938345.9043271.491495
2020-06-3016.1823575.56.19483911.6948393.1655361.321981
2020-07-3114.1280876.67.18540913.785409-0.4561870.798865
2020-08-3111.9433137.78.04788615.747886-3.9506710.146099
2020-09-309.7280958.88.76689217.566892-6.343231-1.495567
2020-10-3112.4834899.99.32961219.229612-6.9665330.220411
2020-11-3012.14180811.09.72601320.726013-5.646726-2.937480
2020-12-3115.14333412.19.94902922.049029-2.751930-4.153764
2021-01-3121.77451613.29.99468423.1946840.910435-2.330604
2021-02-2828.43289214.39.86216424.1621644.318862-0.048134
2021-03-3132.35058315.49.55383224.9538326.5226690.874082
2021-04-3030.59655616.59.07518425.5751846.907169-1.885797
2021-05-3132.51052317.68.43475326.0347535.3651181.110653
2021-06-3030.42551918.77.64395526.3439552.3266241.754939
2021-07-3124.30095819.86.71689026.516890-1.360813-0.855119
2021-08-3120.45091720.95.67008226.570082-4.668691-1.450475
2021-09-3018.87088122.04.52219526.522195-6.674375-0.976939
2021-10-3121.32631023.13.29369026.393690-6.8184381.751059
2021-11-3022.90244824.22.00646926.206469-5.0606991.756678
2021-12-3126.62057825.30.68347825.983478-1.8914262.528526
2022-01-3127.62649926.4-0.65169625.7483041.8054040.072791
2022-02-2831.85892327.5-1.97525325.5247474.9986701.335506
2022-03-3135.93046928.6-3.26359825.3364026.7977043.796363
2022-04-3030.17787029.7-4.49376225.2062386.700718-1.729087
2022-05-3130.01616530.8-5.64381625.1561844.7347640.125217
2022-06-3026.59172931.9-6.69325825.2067421.448187-0.063200
2022-07-3121.11848133.0-7.62337925.376621-2.242320-2.015820
2022-08-3116.63603134.1-8.41759925.682401-5.307397-3.738973
2022-09-3017.68261335.2-9.06175926.138241-6.892132-1.563496
2022-10-3121.16329836.3-9.54437526.755625-6.5545090.962182
2022-11-3022.45567237.4-9.85684427.543156-4.388699-0.698786
2022-12-3126.91952938.5-9.99359528.506405-0.998790-0.588086
2023-01-3133.96462339.6-9.95219129.6478092.6697021.647112
2023-02-2837.45977640.7-9.73336930.9666315.5935590.899586
2023-03-3140.79376641.8-9.34103132.4589696.9572571.377540
2023-04-3043.83841542.9-8.78217134.1178296.3804333.340153
2023-05-3141.30178044.0-8.06675135.9332494.0239751.344556
2023-06-3039.21786645.1-7.20752637.8924740.5451470.780245
2023-07-3135.12550246.2-6.21981339.980187-3.085734-1.768952
2023-08-3133.84192647.3-5.12121942.178781-5.855940-2.480916
2023-09-3038.77051148.4-3.93132944.468671-6.9928031.294643
2023-10-3137.37121649.5-2.67135646.828644-6.179230-3.278198
2023-11-3046.58763350.6-1.36376049.236240-3.6421420.993536
2023-12-3146.40332651.7-0.03185351.668147-0.089186-5.175634





  (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()     # groud 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()        # groud 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()    # groud 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)를 해보겠습니다. 


ts_components.txt



# read time series text file

df <- read.table('ts_components.txt', sep=',', header=T)

head(df)

A data.frame: 6 × 6
timeseriestrendcycletrend_cycleseasonalirregular
<dbl><dbl><dbl><dbl><dbl><dbl>
2.5961190.00.0000000.0000000.0000002.5961193
6.7461601.11.3321982.4321983.5656840.7482781
8.1121002.22.6406474.8406476.136825-2.8653712
8.2559413.33.9020217.2020216.996279-5.9423582
16.8896554.45.0938349.4938345.9043271.4914946
16.1823575.56.19483911.6948393.1655361.3219812

 



이렇게 불러와서 만든 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))




많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend R_Friend

댓글을 달아 주세요