웹 서비스의 일별 방문자 수, 쇼핑몰의 월별 주문량, 서버의 시간별 요청 수, 금융 데이터의 일별 가격처럼 시간 순서가 중요한 데이터가 있다.
이런 데이터는 일반적인 표 형태의 데이터와 다르게, 과거 값이 현재 값에 영향을 줄 수 있다는 점이 핵심이다.
즉, 시계열 분석에서는 단순히 값들의 평균이나 분산을 보는 것보다 다음을 함께 살펴봐야 한다.
시간 순서
↓
추세 / 계절성 / 반복 패턴
↓
자기상관
↓
정상성 여부
↓
모델 선택
↓
잔차 진단
↓
예측
이번 글에서는 Python을 사용해 시계열 데이터의 구성 요소, 정상성, ADF 검정, ACF·PACF, AR·MA·ARIMA, 계절성 분해, SARIMA까지 한 흐름으로 정리한다.
시계열 데이터(Time Series Data)는 일정한 시간 순서에 따라 관측된 데이터다.
예를 들어 서비스의 일별 API 요청 건수를 기록했다고 해보자.
2026-09-01 12,340
2026-09-02 13,120
2026-09-03 14,005
2026-09-04 13,870
...
일반적인 회귀분석에서는 각 관측값이 서로 독립적이라고 가정하는 경우가 많다.
하지만 시계열에서는 어제의 값이 오늘의 값과 관련되고, 오늘의 값이 내일의 값과 관련될 수 있다.
이러한 관계를 자기상관(Autocorrelation)이라고 한다.
시계열은 보통 다음 성분으로 나누어 생각한다.
| 요소 | 의미 |
|---|---|
| 추세(Trend) | 장기간에 걸쳐 증가하거나 감소하는 흐름 |
| 계절성(Seasonality) | 일정한 주기를 가지고 반복되는 패턴 |
| 사이클(Cycle) | 일정한 주기가 고정되지 않은 장기적 변동 |
| 잔차(Residual) | 위 요소로 설명되지 않는 불규칙한 변동 |
예를 들어 쇼핑몰 주문 데이터를 생각해보면 다음처럼 해석할 수 있다.
장기적으로 사용자가 증가 → 추세
매주 주말 주문 증가 → 계절성
경기 변화에 따른 장기 변동 → 사이클
예상하기 어려운 일시적 변동 → 잔차
시계열 분해는 이런 성분을 나누어 데이터 구조를 더 쉽게 이해하기 위해 사용한다.
시계열 성분은 크게 두 가지 방식으로 조합해서 생각할 수 있다.
Y = Trend + Seasonality + Residual
계절 변동의 크기가 시간에 따라 크게 달라지지 않을 때 적합하다.
예를 들어 월별 방문자 수가 증가하더라도 계절적 증가 폭이 비슷한 수준이라면 가법 모형을 고려할 수 있다.
Y = Trend × Seasonality × Residual
시계열 수준이 커질수록 계절 변동폭도 함께 커지는 경우에 더 자연스럽다.
예를 들어 서비스 규모가 커질수록 주말 트래픽 증가 폭도 함께 커지는 경우다.
많은 시계열 모델은 정상성(Stationarity)을 중요한 전제로 사용한다.
정상성을 직관적으로 설명하면 다음과 같다.
시간이 지나도 데이터의 통계적 특성이 크게 변하지 않는 상태
대표적으로 다음 특성이 일정해야 한다.
반대로 다음과 같은 데이터는 일반적으로 비정상 시계열에 가깝다.
시간이 지날수록 계속 상승
매년 같은 시기에 반복적으로 급증
시간이 지날수록 변동폭 자체가 커짐
가장 단순한 정상 시계열 예제로 백색잡음(White Noise)을 만들 수 있다.
시간에 따른 뚜렷한 추세나 계절성이 없는 데이터가 어떤 형태인지 확인하기 위해 사용한다.
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
white_noise = np.random.randn(365)
plt.figure(figsize=(10, 3))
plt.plot(white_noise)
plt.title("White Noise")
plt.xlabel("Time")
plt.show()
그래프를 볼 때는 다음을 확인한다.
백색잡음은 대표적인 정상 시계열 예다.
정상 시계열에 인위적으로 추세를 추가하면 차이를 더 쉽게 이해할 수 있다.
trend = np.linspace(0, 15, 365)
non_stationary = white_noise + trend
plt.figure(figsize=(10, 3))
plt.plot(non_stationary)
plt.title("Time Series with Trend")
plt.xlabel("Time")
plt.show()
여기서는 시간이 흐를수록 평균 수준이 올라간다.
즉, 시간에 따라 평균이 달라지기 때문에 정상성이 깨진 상태라고 볼 수 있다.
그래프만 보고 정상성을 판단할 수도 있지만 통계적으로 확인할 수도 있다.
대표적인 방법이 ADF(Augmented Dickey-Fuller) 검정이다.
Python에서는 statsmodels의 adfuller()를 사용할 수 있다.
from statsmodels.tsa.stattools import adfuller
ADF 검정은 보통 다음 가설로 이해하면 된다.
H₀: 단위근이 존재한다 → 비정상 시계열
H₁: 단위근이 없다 → 정상 시계열
따라서 보통 다음과 같이 판단한다.
p-value < 0.05
→ H₀ 기각
→ 정상성에 대한 근거가 있음
p-value >= 0.05
→ H₀를 기각하지 못함
→ 비정상성 가능성을 고려
반복해서 사용할 수 있도록 함수로 만들어두면 편하다.
from statsmodels.tsa.stattools import adfuller
def check_stationarity(series):
result = adfuller(series)
print(f"ADF Statistic: {result[0]:.4f}")
print(f"p-value: {result[1]:.4f}")
print("Critical Values:")
for key, value in result[4].items():
print(f" {key}: {value:.4f}")
백색잡음에 적용해본다.
check_stationarity(white_noise)
추세가 있는 데이터에도 같은 방식으로 적용할 수 있다.
check_stationarity(non_stationary)
이때 중요한 것은 p-value 하나만 보고 끝내지 않는 것이다.
시계열 그래프, ACF, 차분 전후 구조도 같이 확인해야 한다.
시계열에서는 현재 값과 과거 값이 얼마나 관련되어 있는지를 자주 확인한다.
대표적인 도구가 ACF와 PACF다.
ACF(Autocorrelation Function)는 현재 값과 여러 시차의 과거 값 사이 상관관계를 보여준다.
예를 들어 다음과 같은 관계를 확인한다.
현재 값 vs 1일 전
현재 값 vs 2일 전
현재 값 vs 3일 전
...
PACF(Partial Autocorrelation Function)는 중간 시차의 영향을 제거한 뒤 특정 시차와 현재 값 사이의 직접적인 관계를 본다.
직관적으로 표현하면 다음과 같다.
ACF
현재와 과거가 전체적으로 얼마나 연결되어 있는가?
PACF
중간 시차 효과를 제거했을 때 특정 시차가 직접 얼마나 관련되는가?
import statsmodels.api as sm
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(15, 4))
sm.graphics.tsa.plot_acf(
non_stationary,
lags=40,
ax=axes[0]
)
sm.graphics.tsa.plot_pacf(
non_stationary,
lags=40,
ax=axes[1]
)
plt.show()
그래프를 볼 때는 다음을 확인한다.
ACF와 PACF는 ARIMA의 차수 후보를 정하는 데 참고할 수 있지만, 실제 모델 선택은 AIC/BIC, 잔차 진단, 검증 성능까지 함께 봐야 한다.
AR(Autoregressive) 모델은 현재 값을 과거 시계열 값으로 설명한다.
예를 들어 AR(1)은 다음과 같이 표현할 수 있다.
yₜ = c + φ₁yₜ₋₁ + εₜ
AR(2)는 두 시점 전 값까지 사용한다.
yₜ = c + φ₁yₜ₋₁ + φ₂yₜ₋₂ + εₜ
즉 AR(p)는 다음처럼 이해할 수 있다.
현재 값
↑
최근 p개의 과거 값
MA(Moving Average) 모델은 이름 때문에 일반적인 이동평균과 헷갈리기 쉽다.
시계열의 MA 모델은 현재 값을 과거 예측 오차의 조합으로 설명한다.
MA(1)은 다음과 같은 형태다.
yₜ = μ + εₜ + θ₁εₜ₋₁
즉,
과거 실제 값 → AR
과거 예측 오차 → MA
라고 기억하면 이해하기 쉽다.
AR과 MA를 함께 사용하고, 비정상성을 처리하기 위한 차분까지 포함한 모델이 ARIMA다.
ARIMA(p, d, q)
각 파라미터의 의미는 다음과 같다.
| 파라미터 | 의미 |
|---|---|
p | AR 차수 |
d | 차분 횟수 |
q | MA 차수 |
즉,
ARIMA
= AR
+ Differencing
+ MA
추세가 있는 시계열은 평균이 시간에 따라 변하기 때문에 정상성을 만족하지 못하는 경우가 많다.
이때 차분(Differencing)을 이용해 추세를 완화할 수 있다.
1차 차분은 다음과 같다.
Δyₜ = yₜ - yₜ₋₁
Python에서는 간단히 처리할 수 있다.
import pandas as pd
series = pd.Series(non_stationary)
diff_series = series.diff().dropna()
plt.figure(figsize=(10, 3))
plt.plot(diff_series)
plt.title("First Difference")
plt.show()
차분 후에는 다시 ADF 검정을 수행해 정상성이 개선되었는지 확인할 수 있다.
check_stationarity(diff_series)
statsmodels에서는 다음 클래스를 사용한다.
from statsmodels.tsa.arima.model import ARIMA
예를 들어 ARIMA(1, 1, 1) 모델은 다음처럼 만들 수 있다.
model = ARIMA(
non_stationary,
order=(1, 1, 1)
)
result = model.fit()
모델 결과를 확인한다.
print(result.summary())
적합값과 원래 데이터를 비교할 수도 있다.
plt.figure(figsize=(12, 4))
plt.plot(non_stationary, label="Original")
plt.plot(result.fittedvalues, label="Fitted")
plt.legend()
plt.show()
ARIMA 모델을 만들었다고 분석이 끝나는 것은 아니다.
좋은 시계열 모델이라면 모델링 후 남은 잔차가 백색잡음과 비슷해야 한다.
즉, 모델이 설명할 수 있는 시계열 구조를 충분히 제거했다면 잔차에는 눈에 띄는 패턴이 남지 않아야 한다.
statsmodels에서는 다음과 같이 진단 그래프를 확인할 수 있다.
result.plot_diagnostics(
figsize=(12, 8)
)
plt.show()
대표적으로 다음을 확인한다.
잔차 평균이 0 근처인가?
잔차에 추세가 남아 있지 않은가?
잔차의 자기상관이 남아 있지 않은가?
잔차 분포가 지나치게 왜곡되어 있지 않은가?
ARIMA 결과에 등장하는 Ljung-Box 검정은 잔차에 자기상관이 남아 있는지를 확인하는 데 사용한다.
대략 다음과 같이 이해하면 된다.
H₀: 잔차에 자기상관이 없다.
H₁: 잔차에 자기상관이 있다.
좋은 모델이라면 잔차에서 뚜렷한 자기상관이 남지 않는 것이 바람직하다.
실제 시계열 데이터에서는 추세와 계절성이 동시에 존재하는 경우가 많다.
이때 seasonal_decompose()를 사용하면 시계열을 여러 성분으로 나눠볼 수 있다.
예를 들어 월별 서비스 가입자 수가 있다고 가정해보자.
import pandas as pd
import numpy as np
dates = pd.date_range(
"2022-01-01",
periods=48,
freq="MS"
)
np.random.seed(42)
trend = np.linspace(1000, 1800, 48)
seasonal_pattern = np.tile(
[50, 30, 10, -20, -40, -60, -30, 0, 20, 40, 70, 90],
4
)
noise = np.random.normal(
0,
25,
48
)
subscription = pd.Series(
trend + seasonal_pattern + noise,
index=dates
)
먼저 원계열을 확인한다.
subscription.plot(
figsize=(10, 3),
title="Monthly Subscription"
)
plt.show()
from statsmodels.tsa.seasonal import seasonal_decompose
decomp = seasonal_decompose(
subscription,
model="additive",
period=12
)
fig = decomp.plot()
fig.set_size_inches(12, 8)
plt.show()
그래프는 보통 다음 성분으로 나뉜다.
Observed
Trend
Seasonal
Residual
분해 결과에서는 단순히 계절성을 찾는 것뿐 아니라 분해 후 잔차에 구조가 남아 있는지도 확인해야 한다.
시계열 수준이 커질수록 계절 변동도 함께 커지는 구조라면 승법 분해를 고려할 수 있다.
decomp = seasonal_decompose(
subscription,
model="multiplicative",
period=12
)
단, 승법 분해는 0이나 음수 값이 있는 데이터에서는 직접 사용할 수 없기 때문에 데이터 특성을 먼저 확인해야 한다.
일반 ARIMA는 비계절적인 자기상관 구조를 다루는 데 강하지만, 월별·주별처럼 명확한 계절 주기가 있는 경우에는 계절 구조까지 모델에 포함하는 것이 좋다.
이때 사용하는 것이 SARIMA다.
SARIMA는 ARIMA에 계절성 구조를 추가한 모델이다.
보통 다음과 같이 표현한다.
SARIMA(p, d, q)(P, D, Q, s)
앞부분은 일반 ARIMA와 같다.
p: 비계절 AR 차수
d: 비계절 차분 횟수
q: 비계절 MA 차수
계절 부분은 다음 의미다.
P: 계절 AR 차수
D: 계절 차분 횟수
Q: 계절 MA 차수
s: 계절 주기
예를 들어 월별 데이터에서 1년 주기를 사용한다면 일반적으로 다음처럼 생각할 수 있다.
s = 12
statsmodels에서는 SARIMAX 클래스를 이용한다.
외생변수를 사용하지 않는다면 사실상 SARIMA처럼 사용할 수 있다.
import statsmodels.api as sm
model = sm.tsa.statespace.SARIMAX(
subscription,
order=(1, 1, 0),
seasonal_order=(1, 1, 0, 12)
)
result = model.fit()
print(result.summary())
적합 결과는 다음처럼 확인할 수 있다.
plt.figure(figsize=(10, 4))
plt.plot(
subscription,
label="Observed"
)
plt.plot(
result.fittedvalues,
label="Fitted"
)
plt.legend()
plt.show()
SARIMAX에서 X는 외생변수(Exogenous Variable)를 의미한다.
예를 들어 월별 주문량을 예측하면서 다음 변수를 함께 사용하고 싶다면 외생변수를 추가할 수 있다.
광고비
이벤트 여부
프로모션 여부
평균 할인율
즉, SARIMA가 과거 시계열 자체의 패턴을 중심으로 예측한다면 SARIMAX는 외부 설명변수까지 함께 넣을 수 있다.
ARIMA의 p, d, q를 모두 직접 탐색하는 것은 번거로울 수 있다.
pmdarima의 auto_arima()를 사용하면 여러 조합을 비교해 후보 모델을 찾을 수 있다.
import pmdarima as pm
auto_model = pm.auto_arima(
subscription,
d=1,
start_p=0,
max_p=5,
start_q=0,
max_q=5,
D=1,
m=12,
seasonal=True,
stepwise=True,
trace=True
)
여기서 중요한 파라미터는 다음과 같다.
| 파라미터 | 의미 |
|---|---|
d | 비계절 차분 횟수 |
D | 계절 차분 횟수 |
m | 계절 주기 |
start_p, max_p | AR 탐색 범위 |
start_q, max_q | MA 탐색 범위 |
seasonal | 계절 모델 사용 여부 |
stepwise | 효율적인 단계적 탐색 사용 여부 |
auto_arima()가 선택한 모델을 그대로 최종 모델로 확정하기보다는 후보 모델을 빠르게 찾는 도구로 사용하는 것이 좋다.
웹 서비스 운영에서 다음 데이터를 매시간 수집한다고 해보자.
timestamp
request_count
avg_response_ms
error_count
cpu_usage
이 중 request_count는 시간 순서가 매우 중요하다.
예를 들어 다음 질문을 할 수 있다.
최근 몇 시간의 트래픽이 다음 시간 트래픽과 관련 있는가?
매일 같은 시간대에 트래픽이 반복적으로 증가하는가?
주말과 평일에 패턴이 다른가?
장기적으로 서비스 사용량이 증가하는가?
이 경우 분석 흐름은 다음처럼 가져갈 수 있다.
원계열 시각화
↓
추세와 계절성 확인
↓
ADF로 정상성 확인
↓
필요하면 차분
↓
ACF / PACF 확인
↓
ARIMA 또는 SARIMA 후보 설정
↓
모델 적합
↓
잔차 진단
↓
예측 평가
시계열 데이터에서는 일반적인 머신러닝처럼 데이터를 무작위로 섞어서 train/test로 나누면 문제가 생길 수 있다.
미래 데이터가 학습 데이터에 섞이기 때문이다.
잘못된 방식은 다음과 같다.
전체 데이터
↓ random shuffle
train / test
시계열에서는 보통 시간 순서를 유지한다.
과거 -------------------------- 미래
[ Train ][ Test ]
예를 들어 다음처럼 나눌 수 있다.
split_index = int(
len(subscription) * 0.8
)
train = subscription.iloc[:split_index]
test = subscription.iloc[split_index:]
시계열 예측에서는 미래 정보가 학습에 들어가지 않도록 하는 것이 매우 중요하다.
예측 성능은 여러 지표로 확인할 수 있다.
대표적인 지표가 MAE(Mean Absolute Error)다.
MAE
= 실제값과 예측값 차이의 절댓값 평균
Python에서는 다음처럼 계산한다.
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(
actual,
predicted
)
print(mae)
MAE는 원래 데이터와 동일한 단위를 가지기 때문에 해석하기 쉽다.
예를 들어 요청 건수를 예측했다면 MAE도 요청 건수 단위로 해석할 수 있다.
같지 않다.
일반적인 이동평균은 최근 값을 평균내어 데이터를 부드럽게 만드는 방법이다.
반면 ARIMA의 MA(q)는 과거 오차항을 이용하는 확률모형이다.
일반 이동평균
→ 최근 관측값 평균
ARIMA의 MA
→ 과거 예측 오차를 모델링
아니다.
ADF는 정상성 판단의 한 도구일 뿐이다.
다음도 함께 확인해야 한다.
그렇지 않다.
차분은 추세를 제거하는 데 도움이 되지만 너무 많이 하면 원래 데이터의 구조를 잃을 수 있다.
이를 과차분(Over-differencing)이라고 한다.
가능하면 필요한 최소한의 차분을 사용하는 것이 좋다.
ACF와 PACF는 좋은 출발점이지만 최종 선택 기준은 아니다.
실무에서는 여러 모델을 비교하면서 다음을 함께 확인한다.
AIC / BIC
잔차 자기상관
잔차 분포
검증 데이터 예측 성능
모델 복잡도
아니다.
ARIMA는 전통적인 시계열 분석의 중요한 기준 모델이지만 다음과 같은 문제에서는 다른 접근이 더 적합할 수도 있다.
따라서 ARIMA는 시계열 분석의 기본 원리를 이해하고 강력한 기준선을 만드는 모델이라고 보는 것이 좋다.
시계열 데이터를 처음 받았을 때 다음 순서로 확인하면 정리가 쉽다.
1. 시간 인덱스가 올바른가?
2. 시점 간 간격이 일정한가?
3. 결측된 시간 구간이 있는가?
4. 원계열에 추세가 있는가?
5. 계절성이 있는가?
6. 변동폭이 시간에 따라 달라지는가?
7. ADF 결과는 어떠한가?
8. 차분이 필요한가?
9. ACF와 PACF에는 어떤 패턴이 있는가?
10. ARIMA와 SARIMA 중 무엇이 더 적절한가?
11. 모델 적합 후 잔차가 백색잡음에 가까운가?
12. 미래 구간에서 실제 예측 성능은 어떠한가?
seasonal_decompose()를 사용하면 추세·계절성·잔차를 분해해서 볼 수 있다.시계열 분석에서 중요한 것은 모델 이름을 많이 외우는 것이 아니다.
핵심은 시간에 따른 구조를 발견하고, 그 구조를 제거하거나 모델 안에 포함시키는 과정이다.
시간 흐름 이해
↓
추세·계절성 탐색
↓
정상성 확인
↓
차분과 변환
↓
ACF·PACF 탐색
↓
ARIMA / SARIMA 모델링
↓
잔차 진단
↓
미래 구간 예측 평가
이 흐름을 이해하고 나면 시계열 분석은 단순히 ARIMA()를 호출하는 작업이 아니라, 시간에 따라 변화하는 데이터를 구조적으로 이해하는 과정이라는 점이 보이기 시작한다.