본문 바로가기

공부 일지 #38 | 머신러닝: 시계열 데이터

@studying:)2025. 9. 14. 23:27

학습날짜: 2025.09.11


1. 시계열데이터의 특징

  • 시계열 데이터?
    : 연도별, 월별, 일별, 시별, 분별, 초별 등 균등한 간격으로 관측된 자료 의미함
  • 분석 이유
    : 시간 흐름에 따른 변화를 분석해서 미래 값 예측, 추세(Trend), 주기(Cyclic), 계절성(Seasonality) 파악하려는 것임
  • 일반 데이터와 차이
    • 시계열은 시간 자체가 주요 영향 요인
    • 예) 통장에 백만원 넣어두면 날짜가 지남에 따라 이자가 붙음 → 어제와 오늘 데이터가 밀접한 상관성 가짐
  • 데이터 형태
    • POS 구매자료(불규칙 시차)
    • 일별/주별/월별/분기별 등 규칙적 시차 데이터
  • 구성요소 (S(t) = T(t) + S(t) + C(t) + R(t))
    • 추세(Trend, T) : 시간 따라 상승/하락 경향성
    • 계절성(Seasonality, S) : 일·주·월·계절 단위로 반복되는 변동
    • 순환(Cyclic, C) : 수년 단위의 장기 변동 (이론적 개념)
    • 불규칙성(Random, R) : 설명 불가능한 우연 요인

  • 분석 목적
    1. 과거 데이터의 패턴을 이용해 미래 예측
    2. 시계열 데이터는 독립적이지 않고, 가까운 시점일수록 상관성 강함
    3. 단, 정상성(stationarity) 확보가 필요함
  • 정상성과 비정상성
    • 대부분의 시계열 데이터는 비정상적임
    • 정상성 데이터 = 평균, 분산 일정 → 백색잡음 특성 가짐
    • 비정상 데이터는 그대로 모델에 넣으면 의미 없음 → 변환 필요

  • 정상성 확인 방법
    • ACF(자기상관) : 모든 상관성 확인
    • PACF(부분자기상관) : 직접 경로 상관성만 확인
    • 자기상관이 높으면 비정상 시계열 데이터로 판단
    • Lagged value(지연값) 이용해 패턴 확인

2. 분석 방법

2.1. EDA

  • 평활법(smoothing)
    • 이동평균(SMA): window 이동하며 평균 → 단점: 시간 영향력 상쇄
    • 지수이동평균(EWMA): 최근 데이터에 더 큰 가중치 → 트렌드 반영 상대적으로 빠름
  • 분해법(decomposition)
    • 데이터 = trend + seasonality + random 으로 분해
    • trend 성분은 회귀분석 표본데이터로 활용 가능

2.2. 시계열 모형

2.2.1. AR(자기회귀모형)

  • 현재 값 = 과거 값들의 함수
  • lagged value 사용
  • 입력 데이터는 정상시계열 필요

2.2.2. MA(이동평균모형)

  • 현재 값 = 과거 오차들의 선형결합
  • 백색잡음의 선형결합이므로, 항상 정상성 만족

2.2.3. ARIMA (Autoregressive Integrated Moving Average) 

  • AR + MA 결합 모형
  • 대부분의 데이터는 비정상 → 차분(differencing) 통해 정상화 후 사용
  • ARIMA(p, d, q) 형태
    • p: AR 차수
    • d: 차분 횟수
    • q: MA 차수
  • 차분 예시
    • 1차 차분: Xt – Xt-1
    • 2차 차분: Xt – Xt-2 (잘 안 씀, 데이터 왜곡 심함)
  • 모형 해석 예시
    • ARIMA(0, 1, 0) → 단순 1차 차분
    • ARIMA(0, 1, 2) → IMA(1, 2) 모형에서 1차 차분 후 MA(2) 적용
    • ARIMA(2, 1, 0) → ARI(2, 1) 모형에서 1차 차분 후 AR(2) 적용
# 1. 라이브러리 불러오기
!pip install pmdarima

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import pickle

from statsmodels.tsa.stattools import adfuller  # ADF 검정
from pmdarima.arima import auto_arima  # ARIMA 자동 파라미터 탐색

# 2. 데이터 불러오기
shampoo_df = pd.read_csv(path)
shampoo_df.head(2)
'''
Month   Sales
0  1-01  266.0
1  1-02  145.9
'''

# 3. 정상성 검정 (ADF Test)
# 정상성 확인
def check_stationary(ts):
  print("H0: 시계열이 정상성을 가지지 않는다.")
  print("H1: 시계열이 정상성을 갖는다.")

  adft = adfuller(ts, autolag="AIC")
  print(f"통계량: {adft[0]}, p-value: {adft[1]}")

# 원 데이터 확인
check_stationary(shampoo_df['Sales'])
'''
H0: 시계열이 정상성을 가지지 않는다.
H1: 시계열이 정상성을 갖는다.
통계량: 3.060142083641181, p-value: 1.0
=> H0 채택 → 정상성 없음
'''

# 4. 차분으로 정상화
# 1차 차분
shampoo_df['Sales_diff'] = shampoo_df['Sales'].diff()

# 맨 처음 값(첫 번째 row)은 비교할 이전 값이 없으므로 결과가 하나가 NaN이 되기 때문에.
shampoo_df.dropna(inplace=True) 

# 다시 정상성 검정
check_stationary(shampoo_df['Sales_diff'])
'''
H0: 시계열이 정상성을 가지지 않는다.
H1: 시계열이 정상성을 갖는다.
통계량: -7.249074055553854, p-value: 1.7998574141687034e-10
=> H1 채택 → 정상성 확보
'''

5. ARIMA 모델 적합 (auto_arima)
'''
p = [1,2,3]
d = [1,2,3]
q = [1,2,3]

for a,b,c in zip(p,d,q):
  aic = arima(p,d,q)

# 이렇게 일일히 해야할 것을 auto_arima쓰면 알아서 해줌
'''
model = auto_arima(shampoo_df['Sales'],
                   start_p=0,
                   start_q = 0,
                   max_p=3,
                   max_q=3,
                   test='adf',
                   d=None, # 알아서 해달라는 의미
                   m = 1, # 1년 단위로 분석해라
                   seasonal=False,
                   trace = True, # 값 추적 허용
                   stepwise=True) # p,q 조합 허용
print(model.summary())
'''
Performing stepwise search to minimize aic
 ARIMA(0,1,0)(0,0,0)[0] intercept   : AIC=418.150, Time=0.02 sec
 ARIMA(1,1,0)(0,0,0)[0] intercept   : AIC=393.068, Time=0.04 sec
 ARIMA(0,1,1)(0,0,0)[0] intercept   : AIC=394.655, Time=0.12 sec
 ARIMA(0,1,0)(0,0,0)[0]             : AIC=416.790, Time=0.01 sec
 ARIMA(2,1,0)(0,0,0)[0] intercept   : AIC=389.136, Time=0.15 sec
 ARIMA(3,1,0)(0,0,0)[0] intercept   : AIC=389.780, Time=0.38 sec
 ARIMA(2,1,1)(0,0,0)[0] intercept   : AIC=389.277, Time=0.85 sec
 ARIMA(1,1,1)(0,0,0)[0] intercept   : AIC=387.783, Time=0.57 sec
 ARIMA(1,1,2)(0,0,0)[0] intercept   : AIC=inf, Time=0.97 sec
 ARIMA(0,1,2)(0,0,0)[0] intercept   : AIC=inf, Time=1.55 sec
 ARIMA(2,1,2)(0,0,0)[0] intercept   : AIC=inf, Time=1.02 sec
 ARIMA(1,1,1)(0,0,0)[0]             : AIC=394.574, Time=0.10 sec

Best model:  ARIMA(1,1,1)(0,0,0)[0] intercept
Total fit time: 5.817 seconds

Note: AIC 가장 작은게 가장 잘 학습된 것!
'''

6. 모델 진단
model.plot_diagnostics(figsize=(12,8))
plt.show()
'''
1. 표준화 잔차 → 평균·분산 일정?
2. 잔차 히스토그램 → 정규성 확인
3. Q-Q plot → 정규성 확인
4. ACF plot → 잔차의 독립성 확인
'''

# 7. Train/Test 분리 & 예측
triaing_rate = 0.80

train = shampoo_df[:int(len(shampoo_df)*triaing_rate)]
test = shampoo_df[int(len(shampoo_df)*triaing_rate) :]

# train 데이터로 다시 모델 학습
model = auto_arima(train['Sales'],
                   start_p=0,
                   start_q = 0,
                   max_p=3,
                   max_q=3,
                   test='adf',
                   d=None, # 알아서 해달라는 의미
                   m = 1, # 1년 단위로 분석해라
                   seasonal=False,
                   trace = True, # 값 추적 허용
                   with_intercept='auto', # 상수항
                   stepwise=True) # p,q 조합 허용
'''
Note:
- seasonal=True 줘도 결과 비슷함 → ARIMA 한계일수도
- 선형 특성 때문에 예측이 단순 직선화됨
- 이를 보완하기 위해서는 sine + cosine을 이용해서 만들어진 trigomatric 모델을 쓸 수도 있음
	- holt-winters model: 이게 훨씬 잘 맞음
'''

# test 구간 예측
y_pred = model.predict(n_periods=len(test))
pred_df = pd.DataFrame(y_pred, index=test.index, columns=['y_pred'])

# 결과 병합
result_df = pd.concat([test, pred_df], axis=1)
result_df.columns = ['Month', 'y', 'Sales_diff', 'y_pred']

# 8. 성능 평가 (RMSE)
# 모델 성능 평가: 1/n * (y-yhat) ^2
def rmse(y, ypred):
  return np.sqrt(np.mean(np.square(y-ypred)))

rmse(result_df['y'], result_df['y_pred'])
# 35.58399697627928

# 9. 예측 결과 시각화
plt.figure(figsize=(20,6))
plt.grid(True)
plt.xlabel("Dates")
plt.ylabel("Sales_diff")
plt.plot(result_df["Month"], result_df["y"], c = "red")
plt.plot(result_df["Month"], result_df["y_pred"], c = 'blue')
plt.show()

# 10. 모델 저장 & 로드
# 저장
filename = "/content/drive/MyDrive/ML/auto_arima_model.save"
pickle.dump(model, open(filename, 'wb'))

# 불러오기
loaded_model = pickle.load(open(filename, 'rb'))
y_pred = loaded_model.predict(n_periods=len(test))

'''
- 저장한 학습된 모델 load -> predict
- 이렇게 해서 모델을 서비스에 적용시킬 수 있음!
'''
studying:)
@studying:) :: what i studied

studying:) 님의 학습 여정을 기록하는 블로그입니다.

목차