오늘은 시계열 데이터 2일차로 기준선 모형과 ARIMA 모형에 대해서 배웠다. 이론적으로 상당히 어려운 하루였다.
| 구분 | 공식 | 상세 내용 |
|---|---|---|
| 단순 기법 Naive Method | 단순 기법의 기본 개념은 '내일은 오늘과 같다' 랜덤 워크 데이터에서는 단순 기법이 이론적으로 최적 | |
| 이동 평균 Moving Average | 이동 평균 예측은 최근 k일의 평균을 사용 전체 평균 예측은 추세가 있는 데이터에서 적절하지 않음 | |
| 단순 회귀 Linear Regression | 단순 회귀 예측은 시간에 따라 직선의 추세가 있다고 가정하고 시간을 숫자로 간주하여 회귀 모형을 학습 | |
| 표류 기법 Drift Method | 표류 기법은 단순 기법에 평균적인 변화 방향을 더하는 예측 방법 |
std_date = '2025-01-01' # 기준일자 설정
train = df.loc[:std_date].iloc[:-1]
test = df.loc[std_date:]
n_train = len(train) # 훈련셋 크기 생성
train_idx = np.arange(n_train).reshape(-1, 1) # 훈련셋 인덱스를 수치형으로 변환
n_test = len(test) # 시험셋 크기 생성
test_idx = np.arange(n_train, n_train + n_test).reshape(-1, 1) # 시험셋 인덱스를 수치형으로 변환
nm_pred = pd.concat(objs=[train, test])['value'].shift(1).loc[test.index]
nm_pred.head()
# date
# 2025-01-01 112.18
# 2025-01-02 111.60
# 2025-01-03 101.77
# 2025-01-04 111.94
# 2025-01-05 81.24
# Freq: D, Name: value, dtype: float64
ma_pred = pd.concat(objs=[train, test])['value'].rolling(window=7).mean().shift(1).loc[test.index]
ma_pred.head()
# date
# 2025-01-01 103.960714
# 2025-01-02 104.509286
# 2025-01-03 103.600714
# 2025-01-04 103.499286
# 2025-01-05 101.275714
# Freq: D, Name: value, dtype: float64
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X=train_idx, y=train['value'])
lr_pred = pd.Series(data=model.predict(X=test_idx), index=test.index)
lr_pred.head()
# date
# 2025-01-01 121.615984
# 2025-01-02 121.670329
# 2025-01-03 121.724673
# 2025-01-04 121.779017
# 2025-01-05 121.833362
# Freq: D, dtype: float64
y_1, y_t = train['value'].iloc[[0, -1]] # 훈련셋 첫 번째 값과 마지막 값 생성
drift = (y_t - y_1) / (n_train - 1) # 전체 기간의 평균 일별 변화량 계산
h = np.arange(1, n_test + 1) # 각 시점까지 일수 차이 계산
dm_pred = pd.Series(data=y_t + h * drift, index=test.index)
dm_pred
# date
# 2025-01-01 112.227425
# 2025-01-02 112.274849
# 2025-01-03 112.322274
# 2025-01-04 112.369699
# 2025-01-05 112.417123
# ...
# 2025-12-27 129.300301
# 2025-12-28 129.347726
# 2025-12-29 129.395151
# 2025-12-30 129.442575
# 2025-12-31 129.490000
# Freq: D, Length: 365, dtype: float64
sns.lineplot(x=test.index, y=test['value'], label='Actual', color='0')
sns.lineplot(x=test.index, y=nm_pred, label='Naive Method', color='0.8')
sns.lineplot(x=test.index, y=ma_pred, label='Moving Average', color='blue')
sns.lineplot(x=test.index, y=lr_pred, label='Linear Regression', color='red')
sns.lineplot(x=test.index, y=dm_pred, label='Drift Method', color='green')
plt.title(label='기준선 모형 비교', fontweight='bold')
plt.legend()
plt.show()

| 데이터 특징 | 예시 데이터 | 공식 |
|---|---|---|
| 랜덤 워크 | 주가 데이터 (기업별, 시점별로 크게 변함) | 단순 기법 Naive Method |
| 잡음 많음 | 센서 데이터 (측정 오차, 노후화 등) | 이동 평균 Moving Average |
| 추세 있음 | 고객 수, 매출액 등 (장기 변화 데이터) | 단순 회귀 Linear Regression |
| 추세 + 랜덤 워크 | 환율 데이터 (장기 상승 또는 하락 + 충격) | 표류 기법 Drift Method |
hds.stat.regmetrics(y_true=test['value'], y_pred=nm_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
# metric score
# 0 RMSE 16.595267
# 1 MAPE 0.090108
hds.stat.regmetrics(y_true=test['value'], y_pred=ma_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
# metric score
# 0 RMSE 13.931236
# 1 MAPE 0.100994
hds.stat.regmetrics(y_true=test['value'], y_pred=lr_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
# metric score
# 0 RMSE 19.784532
# 1 MAPE 0.131419
hds.stat.regmetrics(y_true=test['value'], y_pred=dm_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
# metric score
# 0 RMSE 22.509184
# 1 MAPE 0.137104
df['RENT_DT'] = pd.to_datetime(arg=df['RENT_DT'], errors='coerce') # 날짜시간형으로 변환
df = df.set_index('RENT_DT').sort_index() # 날짜시간형 컬럼은 인덱스로 설정
df.index.duplicated(False).sum() # 중복건 확인
# 0
full_idx = pd.date_range(df.index.min(), df.index.max(), freq='D') # 인덱스의 최솟값과 최댓값으로 기대 인덱스 생성
full_idx.difference(df.index).size # 누락 인덱스 개수 확인
# 6
df = df.reindex(full_idx).rename_axis(index='RENT_DT') # 누락 인덱스를 행으로 추가
df.loc[df['USE_CNT'].isna()] # 결측인 행 확인
# USE_CNT
# RENT_DT
# 2021-06-25 NaN
# 2021-06-26 NaN
# 2021-06-27 NaN
# 2021-06-28 NaN
# 2021-06-29 NaN
# 2021-06-30 NaN
df = df.dropna() # 결측인 행 모두 삭제
sns.lineplot(data=df, x=df.index, y='USE_CNT', color='royalblue', linewidth=0.5)
plt.show()

from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(x=df['USE_CNT'], model='additive', period=7)
result.plot();

std_date = '2025-01-01' # 훈련셋과 시험셋으로 분할하는 기준일자 설정
train = df.loc[:std_date].iloc[:-1]
test = df.loc[std_date:]
n_train = len(train) # 훈련셋 크기 생성
train_idx = np.arange(n_train).reshape(-1, 1) # 인덱스를 수치형으로 변환
n_test = len(test) # 시험셋 크기 생성
test_idx = np.arange(n_train, n_train + n_test).reshape(-1, 1) # 인덱스를 수치형으로 변환
nm_pred = pd.concat(objs=[train, test])['USE_CNT'].shift(1).loc[test.index]
ma_pred = pd.concat(objs=[train, test])['USE_CNT'].rolling(window=7).mean().shift(1).loc[test.index]
from sklearn.linear_model import LinearRegression
model = LinearRegression() # 단순 선형 회귀 모형 생성
model.fit(X=train_idx, y=train['USE_CNT']) # 훈련셋으로 모형 학습
lr_pred = pd.Series(data=model.predict(X=test_idx), index=test.index) # 단순 회귀 기반 예측
y_1, y_t = train['USE_CNT'].iloc[[0, -1]] # 훈련셋의 첫 번째 값과 마지막 값을 생성
drift = (y_t - y_1) / (n_train - 1) # 평균 일별 변화량 계산
h = np.arange(1, n_test + 1) # ㅣ험셋의 일수 차이 계산
dm_pred = pd.Series(data=y_t + h * drift, index=test.index) # 시험셋으로 표퓨 기법 예측값 생성
sns.lineplot(x=test.index, y=test['USE_CNT'], label='Actual', color='0')
sns.lineplot(x=test.index, y=nm_pred, label='Naive Method', color='0.8')
sns.lineplot(x=test.index, y=ma_pred, label='Moving Average', color='blue')
sns.lineplot(x=test.index, y=lr_pred, label='Linear Regression', color='red')
sns.lineplot(x=test.index, y=dm_pred, label='Drift Method', color='green')
plt.title(label='기준선 모형 비교', fontweight='bold')
plt.legend(loc='upper left')
plt.show()

hds.stat.regmetrics(y_true=test['USE_CNT'], y_pred=nm_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
hds.stat.regmetrics(y_true=test['USE_CNT'], y_pred=ma_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
hds.stat.regmetrics(y_true=test['USE_CNT'], y_pred=lr_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
hds.stat.regmetrics(y_true=test['USE_CNT'], y_pred=dm_pred).loc[[2, 6], ['metric', 'score']].reset_index(drop=True)
train = train.asfreq('D')
test = test.asfreq('D')
| 구분 | ACF | PACF |
|---|---|---|
| AR(p) | 서서히 감소(Tail-off) | p 이후 단절 |
| MA(q) | q 이후 단절 | 서서히 감소(Tail-off) |
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
plot_acf(x=df, lags=30);

plot_pacf(x=df, lags=30);

use_diff_1 = df.diff(1).dropna()
plot_acf(x=use_diff_1, lags=30);

plot_pacf(x=use_diff_1, lags=30);

from statsmodels.tsa.statespace.sarimax import SARIMAX
arima_model = SARIMAX(
endog=train,
order=(0, 1, 1),
enforce_stationarity=False,
enforce_invertibility=False
)
arima_result = arima_model.fit()
arima_result.summary()

from statsmodels.stats.diagnostic import acorr_ljungbox
arima_resid = arima_result.resid.dropna() # ARIMA 모형의 잔차를 생성
acorr_ljungbox(x=arima_resid) # 룽 박스 검정을 통해 잔차의 자기상관 여부 확인
# lb_stat lb_pvalue
# 1 87.246095 9.582431e-21
# 2 104.626705 1.908094e-23
# 3 138.091505 9.748678e-30
# 4 155.442875 1.387080e-32
# 5 163.480199 1.793442e-33
# 6 168.112718 1.130396e-33
# 7 201.420868 5.740250e-40
# 8 201.434694 3.185462e-39
# 9 212.201067 9.118374e-41
# 10 214.662629 1.398402e-40
arima_result.aic # ARIMA 모형의 AIC 확인
# 34400.26888872458
from statsmodels.graphics.tsaplots import plot_acf
plot_acf(x=arima_resid, lags=30);

n_test = len(test) # 시험셋 크기 설정
arima_pred = arima_result.get_forecast(steps=n_test) # 시험셋 크기만큼 ARIMA 모형의 예측값 생성
arima_pred_avg = arima_pred.predicted_mean # ARIMA 모형의 예측값 평균 생성
sns.lineplot(x=test.index, y=test['USE_CNT'], color='0.8', linewidth=0.5)
sns.lineplot(x=test.index, y=arima_pred_avg, color='red')
plt.show()

def rolling_1step(model, test, exog=None):
preds = pd.Series() # ARIMA 모형 예측값을 저장할 빈 시리즈 생성
for t in test.index: # test 매개변수의 인덱스를 바꿔가면서 반복문 실행
if exog is None: # 외생변수가 없는 모형으로 1-step Rolling 예측 실행
y_hat = model.forecast(1) # 다음 시점 예측값 생성
model = model.append(endog=test.loc[[t]], refit=False)
else: # 외생변수가 있는 모형으로 예측 실행
y_hat = model.forecast(1, exog=exog.loc[[t]])
model = model.append(endog=test.loc[[t]], exog=exog.loc[[t]], refit=False)
preds = pd.concat(objs=[preds, y_hat]) # preds에 y_hat 추가
return preds
arima_roll = rolling_1step(model=arima_result, test=test) # ARIMA 모형에 대한 예측값 생성
sns.lineplot(x=test.index, y=test['USE_CNT'], color='0.8', linewidth=0.5)
sns.lineplot(x=test.index, y=arima_roll, color='red')
plt.show()

| 전제 조건 | 현실 데이터의 특징 |
|---|---|
| • 시계열은 자기 자신의 과거 값과 과거 오차로 설명할 수 있음 | • 명확한 주기적 패턴이 반복(요일 효과, 월별 패턴, 연간 계절성 등) |
| • 비정상성은 차분으로 제거할 수 있음 | • 차분만으로 충분히 설명되지 않는 경우가 많음 |
| • 계절성은 없거나 약함 | • 기온, 강수량, 이벤트, 정책 등 외생변수의 영향이 큼 |
| • 외부 요인의 영향은 고려하지 않음 | • ARIMA로는 잔차에 구조적인 패턴이 남음 |
| 비계절 ARIMA | 계절 ARIMA |
|---|---|
| AR(p) : 직전 1~p 시점의 값 영향 | SAR(P) : s 간격으로 떨어진 값의 영향 |
| I(d) : 전체 추세 제거용 차분 → | SD(D) : 계절 효과 제거용 차분 → |
| MA(q) : 직전 1~q 시점의 오차 영향 | SMA(Q) : s 간격으로 떨어진 오차의 영향 |
| 상황 | 추천 모형 |
|---|---|
| 계절성 없는 단변량 데이터 예측 | ARIMA |
| 강한 주기성이 있는 데이터 예측 | SARIMA |
| 외생변수가 중요한 데이터 예측 | ARIMAX |
| 계절성과 외생변수를 모두 고려 | SARIMAX |
| 추세, 계절성, 이벤트 등 구조가 자주 바뀜 | Prophet / ML |
sarima_model = SARIMAX(
endog=train,
order=(0, 1, 1),
seasonal_order=(0, 1, 1, 7),
enforce_stationarity=False,
enforce_invertibility=False
)
sarima_result = sarima_model.fit()
sarima_result.summary()
sarima_resid = sarima_result.resid.dropna() # SARIMA 모형의 잔차 생성
acorr_ljungbox(sarima_resid)
sarima_pred = sarima_result.get_forecast(steps=n_test)
sarima_pred_avg = sarima_pred.predicted_mean
sns.lineplot(x=test.index, y=test['USE_CNT'], color='0.8', linewidth=0.5)
sns.lineplot(x=test.index, y=sarima_pred_avg, color='red')
plt.show()

sarima_roll = rolling_1step(model=sarima_result, test=test)
sns.lineplot(x=test.index, y=test['USE_CNT'], color='0.8', linewidth=0.5)
sns.lineplot(x=test.index, y=sarima_roll, color='red')
plt.show()

sarimax_model = SARIMAX(
endog=train,
exog=exog_train,
order=(0, 1, 1),
seasonal_order=(0, 1, 1, 7),
enforce_stationarity=False,
enforce_invertibility=False
)
sarimax_result = sarimax_model.fit()
sarimax_result.summary()
sarimax_resid = sarimax_result.resid.dropna()
plot_acf(x=sarimax_resid, lags=30);
sarimax_pred = sarimax_result.get_forecast(steps=n_test, exog=exog_test)
sarimax_pred_avg = sarimax_pred.predicted_mean
sns.lineplot(x=test.index, y=test['USE_CNT'], color='0.8', linewidth=0.5)
sns.lineplot(x=test.index, y=sarimax_pred_avg, color='red')
plt.show()

sarimax_roll = rolling_1step(model=sarimax_result, test=test, exog=exog_test)
sns.lineplot(x=test.index, y=test['USE_CNT'], color='0.8', linewidth=0.5)
sns.lineplot(x=test.index, y=sarimax_roll, color='red')
plt.show()

내일은 머신러닝, 딥러닝 파트의 마지막 날이다. Prophet부터 LSTM까지 간단하게 다뤄보는 것으로 알고 있는데 마지막 마무리까지 잘하면 좋겠다.