시계열 예측에서 점·구간 예측과 백테스트 운영
시계열 예측의 점예측·구간예측·샘플외 백테스트를 중심으로 누수 차단, 평가 지표, 캘리브레이션과 운영 기준을 정리합니다.
2026-08-14 · 최초 발행 2024-04-29
단일 예측값만으로는 운영 판단이 부족하다
수요, 가격, 부하, 트래픽처럼 시간에 따라 달라지는 값을 다룰 때 예측은 사전 대응의 기준이 된다. 다만 모델의 오차만 낮추는 데 그치면, 예측이 빗나갈 가능성과 실제 환경에서의 재현성을 놓치기 쉽다.
시계열 예측은 점예측으로 실행 기준을 만들고, 구간예측으로 불확실성을 전달하며, 샘플외 백테스트로 일반화 성능을 확인하는 방식으로 다룰 수 있다.
점예측(Point forecast)은 특정 시점 또는 기간의 값을 하나로 제시한다. 예를 들어 t+1 시점 매출을 1,234로 예상하는 형태다. 이는 base plan 역할을 하며, MAE, RMSE, MAPE, sMAPE 같은 지표로 오차를 평가한다.
구간예측(Prediction interval)은 신뢰수준(예: 90%, 95%)에 맞춰 예측 범위 [L, U]를 함께 제시한다. 불확실성을 의사결정에 반영할 수 있다는 점이 핵심이다. 관측값까지 포함하는 prediction interval과 평균 추정의 confidence interval은 구분해야 하며, 실무에서는 대부분 prediction interval을 사용한다.
샘플외 백테스트(Out-of-sample backtest)는 미래를 보지 않은 상태에서 모델의 성능을 확인하는 절차다. 시간 순서를 유지해 훈련과 검증을 나누고, 홀드아웃·롤링 오리진·슬라이딩 윈도우 같은 전략을 적용한다. 목적은 데이터 누수를 막고 운영 환경에 가까운 성능을 추정하는 데 있다.
시간 순서를 보존하는 검증 설계
고정 홀드아웃은 과거 데이터를 학습에 쓰고 최근 기간을 테스트 집합으로 분리하는 방식이다. 구성은 단순하지만 특정 테스트 구간에 따라 결과 분산이 커질 수 있다.
롤링 오리진은 훈련 창을 확장하거나 이동시키면서 반복적으로 예측한다. 워크포워드 방식이 여기에 해당하며, 실제 운영에서 마주할 예측 과정을 더 가깝게 재현한다.
피처를 만들 때 미래 정보가 섞이지 않도록 해야 한다. 목표 누수를 점검하고, 이벤트 라벨링에서는 embargo도 고려한다. 분할 방식보다 먼저 확인할 문제는 데이터가 시간적으로 오염되지 않았는지다.
예측 범위는 포함률과 폭을 함께 본다
불확실성은 여러 방법으로 추정할 수 있다. ARIMA/SARIMAX의 해석적 분산, 잔차·블록 부트스트랩, 사전·사후를 활용하는 베이지안 방식, Quantile loss 기반의 분위수 회귀나 딥러닝이 대표적이다.
구간을 만들었다고 해서 바로 사용할 수 있는 것은 아니다. 실제값이 구간에 포함되는 비율인 coverage와 구간 폭(width)의 균형을 확인해야 한다. 폭이 지나치게 넓으면 불확실성을 전달하더라도 실용성이 떨어진다.
점예측에는 MAE, RMSE, sMAPE, MASE를 사용할 수 있다. MAE는 해석이 쉽고, RMSE는 큰 오차에 더 큰 페널티를 준다. sMAPE는 비율형 지표의 0 문제를 완화하며, MASE는 계절성을 고려한 비교에 쓸 수 있다.
구간예측은 포함률, 평균 구간 폭, 핀볼 손실(quantile), 윙클러 스코어처럼 폭과 실제 포함 여부를 함께 반영하는 지표로 평가한다. 샘플외 성능은 롤링 평균과 분산, 추세·계절성 변화에 대한 민감도, 데이터 드리프트 지표까지 함께 확인한다.
데이터와 설명 수준에 맞춰 모델을 고른다
ETS, ARIMA, SARIMA, SARIMAX 같은 통계 모델은 데이터가 적거나 설명 가능성이 중요한 상황에 적합하다. 상호작용, 비선형성, 대규모 피처가 중요한 경우에는 XGBoost, LightGBM, LSTM, TCN, N-BEATS 같은 머신러닝·딥러닝 모델을 선택할 수 있다.
불확실성 표현이 주된 요구라면 Quantile regression, NGBoost, DeepAR/Prophet류의 확률 모델도 후보가 된다. 모델 선택은 정확도만이 아니라 성능 제약과 학습 시간, 설명 가능성, 운영 복잡도를 함께 반영해야 한다.
재학습 주기는 데이터 빈도와 변동성을 기준으로 정한다. 예를 들어 주별·월별 주기를 둘 수 있으며, 드리프트가 감지되면 즉시 재학습한다. MAPE/MASE 임계치, 포함률 경고, 구간 폭 급증 알림을 가드레일로 두고, 예측·구간·커버리지·데이터 유효성을 함께 로깅한다.
예측값과 범위를 함께 사용하는 장면
소매 수요예측에서는 점예측으로 발주량을 산출하고 P90 구간을 안전재고 결정에 활용할 수 있다. 프로모션·시즌 피처를 반영한 롤링 백테스트가 함께 필요하다.
에너지 부하와 발전 계획에서는 P95 상한을 설비·계통 여유 확보에 사용하고, 구간 폭을 비용함수에 직접 반영할 수 있다. 클라우드 오토스케일링은 점예측을 기본 용량에, 상한 구간을 버스트 대응에 연결해 과금과 성능의 트레이드오프를 다룬다.
금융 시그널은 워크포워드 검증에 비용·슬리피지를 반영하고, 방향 정확도·히트레이트·드로다운을 관리한다.
롤링 OOS 기준으로 MAE가 5~15% 개선될 수 있으며, 이는 과최적화 제거 효과로서 도메인에 의존한다. 포함률을 90±3% 수준으로 유지하면 재고 품절률과 과잉재고를 동시에 줄일 수 있다. 구간 폭이 급증하는 상황을 감지하면 이상 징후에 더 일찍 대응할 수 있고, 재학습과 알림 자동화는 운영 비용 절감으로 이어진다.
예측 파이프라인에서 확인할 흐름
입력은 타임스탬프와 값으로 구성된 시계열 원천 데이터, 캘린더·이벤트 같은 메타피처, 데이터 품질 규칙이다. 전처리에서는 결측과 이상치를 처리하고 로그/박스-콕스 변환, 정렬, 중복 제거를 수행한다.
그다음 홀드아웃 또는 롤링 오리진으로 시간을 보존한 분할을 만들고 누수를 차단한다. 모델은 성능과 시간 제약을 고려해 학습하며, 점예측과 분위수·분산 기반 구간예측을 생성한다. 평가 단계에서는 점·구간 지표, 캘리브레이션, 통계적 유의성을 검토한 뒤 배포용 예측, P90/P95 구간, 백테스트 리포트, 운영 알림으로 연결한다.
데이터 예외는 누락 타임스탬프 보간, 극단치 윈저라이징, 시간대와 캘린더 정규화로 다룬다. 모델 수렴에 실패하면 ETS 같은 단순 모델로 폴백하고, enforce_stationarity=False처럼 파라미터 경계를 완화할 수 있다. 0 근처에서 MAPE가 왜곡되면 sMAPE와 MASE를 병행하고, 이상 구간을 자동 제외하는 옵션도 고려한다.
점·구간 예측과 샘플외 검증의 역할
| 항목 | 성능(지표) | 일관성/안정성 | 확장성 | 운영 편의 |
|---|---|---|---|---|
| 점예측 | MAE/RMSE/MASE | 변동성 민감, 단일 값 | 계산 가벼움, 대량 스케일 적합 | 해석·의사결정 용이 |
| 구간예측 | 포함률/평균 폭/핀볼 | 캘리브레이션 중요 | 분위수별 병렬화 가능 | 리스크 커뮤니케이션 우수 |
| 백테스트(OOS) | 롤링 평균/분산 | 누수 차단 시 재현성 높음 | 윈도우 병렬화로 확장 | 배포 성능 근사, QA 필수 |
워크포워드 방식으로 점예측과 구간을 계산하기
예시는 Python 3.10+, pandas 2.x, numpy 1.26+, statsmodels 0.14+를 전제로 한다. 월별 시계열을 합성 데이터로 재현하며, 실제 적용 시에는 주기와 계절성을 맞춰야 한다.
# pip install pandas numpy statsmodels
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_absolute_error, mean_squared_error
# 1) 합성 데이터 생성(월별, 6년)
rng = pd.date_range("2018-01-01", periods=72, freq="MS")
np.random.seed(42)
trend = np.linspace(100, 160, len(rng))
season = 10 * np.sin(2 * np.pi * rng.month / 12)
noise = np.random.normal(0, 3, len(rng))
y = pd.Series(trend + season + noise, index=rng, name="y")
# 2) 워크포워드(1-스텝) 백테스트
def walk_forward_sarimax(
y: pd.Series,
start_test: str,
order=(1,1,1),
seasonal_order=(0,1,1,12),
alpha=0.1 # 90% 구간
):
preds, lowers, uppers, trues, idxs = [], [], [], [], []
test_idx = y.loc[start_test:].index
for t in test_idx:
train = y.loc[:t - pd.offsets.MonthBegin(1)]
# 안전장치: 결측/이상치 간단 처리
train = train.asfreq("MS").interpolate(limit_direction="both")
try:
model = SARIMAX(
train, order=order, seasonal_order=seasonal_order,
enforce_stationarity=False, enforce_invertibility=False
)
res = model.fit(disp=False)
except Exception:
# 폴백: 단순(0,1,1)×(0,1,1,12)
model = SARIMAX(
train, order=(0,1,1), seasonal_order=(0,1,1,12),
enforce_stationarity=False, enforce_invertibility=False
)
res = model.fit(disp=False)
fc = res.get_forecast(steps=1)
mean = fc.predicted_mean.iloc[0]
conf = fc.conf_int(alpha=alpha)
lo, hi = conf.iloc[0, 0], conf.iloc[0, 1]
preds.append(mean)
lowers.append(lo)
uppers.append(hi)
trues.append(y.loc[t])
idxs.append(t)
df = pd.DataFrame({
"y_true": trues, "y_pred": preds,
"lower": lowers, "upper": uppers
}, index=idxs)
return df
# 3) 실행: 최근 24개월 OOS
oos = walk_forward_sarimax(y, start_test="2022-01-01", alpha=0.1)
# 4) 지표 계산
mae = mean_absolute_error(oos.y_true, oos.y_pred)
rmse = mean_squared_error(oos.y_true, oos.y_pred, squared=True) ** 0.5
coverage = np.mean((oos.y_true >= oos.lower) & (oos.y_true <= oos.upper))
avg_width = np.mean(oos.upper - oos.lower)
print(f"MAE: {mae:.3f}")
print(f"RMSE: {rmse:.3f}")
print(f"Coverage(90% target): {coverage:.3f}")
print(f"Avg interval width: {avg_width:.3f}")
# 결과 데이터프레임 oos: 점예측/구간예측/OOS 성능 점검용
print(oos.tail())
다중 호라이즌(h>1)이 필요하면 각 h별 모델을 만드는 직접 방식과 1-스텝 예측을 반복하는 재귀 방식 중에서 선택한다. 성능과 복잡도의 트레이드오프가 판단 기준이다.
포함률이 목표보다 낮으면 분산 스케일업으로 구간을 확대하고, 높으면 축소할 수 있다. 분위수 모델로 전환하는 방법도 있다. 시계열 규모가 커지면 글로벌 모델 공유, 피처 표준화, 배치·스트리밍 예측 파이프라인 구성이 필요하다.