본문 바로가기
  • plotly로 바로쓰는 동적시각화 in R & 파이썬
실전에서 바로 쓰는 시계열 데이터 처리와 분석 in R

시계열의 테이블화: ML 기반 예측의 기초

by 아참형인간 2026. 6. 18.
대부분의 머신러닝 튜토리얼은 시계열을 그저 또 하나의 데이터셋으로 취급합니다.하지만 이러한 가정은 눈에 띄지 않게 모델의 성능을 망가뜨릴 수 있습니다. 여기서는 단 하나의 특성(feature)도 생성하기 전에 모든 시계열 프로젝트에서 반드시 수행해야 하는 근본적인 변환을 살펴보겠습니다.

 

핵심 문제

시계열 데이터셋을 열어보면 일반적으로 다음과 같은 형태를 보게 됩니다.

date          sales
2023-01-01    142
2023-01-02    157
2023-01-03    133
2023-01-04    189
...

이것은 순차적인 구조(sequence structure)입니다. 하지만 XGBoost, LightGBM, 선형 회귀(Linear Regression)와 같은 머신러닝 모델은 독립적인 관측치의 행(row)과 특성(feature)으로 구성된 테이블 구조(tabular structure)를 기대합니다.

이 두 구조는 적절하게 연결하지 않으면 근본적으로 호환되지 않습니다.

이 연결 과정을 테이블화(Tabularization) 라고 하며, 이를 올바르게 수행하는 것은 시계열 머신러닝 프로젝트에서 가장 중요한 결정입니다.

테이블화는 실제로 무엇을 하는가

테이블화는 1차원 시계열을 다음과 같은 2차원 행렬로 변환합니다.

  • 각 행은 하나의 예측 시점(forecasting instance)을 나타냅니다.
  • 각 열은 시계열의 과거 정보로부터 생성된 특성을 나타냅니다.

결과는 다음과 같은 형태가 됩니다.

lag_1   lag_2   lag_3   lag_7   target
157     142     NaN     NaN     133
133     157     142     NaN     189
189     133     157     142     201
...

이제 XGBoost는 "7일 전 같은 요일의 매출이 높았다면 내일의 매출도 높을 가능성이 있다"는 패턴을 학습할 수 있습니다.

테이블화가 없다면 이러한 학습은 불가능합니다.

시계열 테이블의 세 가지 특성 범주

1. 목표 변수(Target Variable)로부터 생성된 특성 (내생 변수, Endogenous)

이 특성들은 예측 대상 시계열 자체로부터 생성됩니다. 즉, 목표 변수의 과거 행동을 인코딩합니다.

가장 기본적인 특성은 시차(Lag) 특성입니다.

y(t-1), y(t-2), ..., y(t-k)가 특성 행렬의 열이 됩니다.

import pandas as pd
import numpy as np

def create_lag_features(series: pd.Series, lags: list) -> pd.DataFrame:
    """
    단변량 시계열을 시차 특성을 생성하여
    지도학습 데이터셋으로 변환합니다.

    Parameters
    ----------
    series : DatetimeIndex를 가진 pd.Series
    lags   : 예: [1, 2, 3, 7, 14]

    Returns
    -------
    lag 열과 target 열을 포함한 DataFrame
    """
    df = pd.DataFrame({'target': series})
    for lag in lags:
        df[f'lag_{lag}'] = series.shift(lag)
    return df.dropna()


# 예제: 일별 매출 데이터
np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=100, freq='D')
sales = pd.Series(
    100 + np.cumsum(np.random.randn(100)) + 10 * np.sin(np.arange(100) * 2 * np.pi / 7),
    index=dates,
    name='sales'
)

df = create_lag_features(sales, lags=[1, 2, 3, 7, 14])
print(df.head())

출력:

target      lag_1      lag_2      lag_3      lag_7     lag_14
2023-01-15  105.32     103.81     102.45     108.23      99.67      88.41
2023-01-16  108.91     105.32     103.81     102.45     103.12      91.23
...

왜 이러한 시차를 사용하는 걸까요?

항상 도메인 지식에서 출발해야 합니다.

  • lag_1, lag_2, lag_3 → 단기 자기상관(autocorrelation)
  • lag_7 → 주간 계절성
  • lag_14, lag_28 → 격주 또는 월간 패턴

2. 외생 변수(Exogenous Variables)로부터 생성된 특성

많은 예측 문제는 목표 변수 외에도 추가적인 신호를 포함합니다.

예를 들어 소매 수요 예측에서는 프로모션, 공휴일, 가격 등이 외생 변수입니다.

이들은 매출에 영향을 주지만 매출 자체에서 생성되지는 않습니다.

# 외생 변수를 특성 행렬에 추가
df_exog = pd.DataFrame({
    'target': sales,
    'is_weekend': (sales.index.dayofweek >= 5).astype(int),
    'is_holiday': [0]*90 + [1]*10,  # 단순화된 예시
    'price_index': np.random.uniform(0.9, 1.1, 100),
}, index=dates)

# 목표 변수의 시차 특성 추가
for lag in [1, 7]:
    df_exog[f'lag_{lag}'] = sales.shift(lag)

df_exog = df_exog.dropna()

매우 중요한 구분이 있습니다.

예측 시점 t에서 외생 변수는 다음 두 종류로 나뉩니다.

  • 미래에 이미 알려진 변수(future-known): 내일이 공휴일인지 여부
  • 미래에 알려지지 않은 변수(future-unknown): 변동하는 실제 가격

이 차이는 특성 생성 파이프라인에 매우 큰 영향을 미치지만 많은 실무자가 이를 간과합니다.

3. 과거 이력으로부터 생성되는 윈도우/집계 특성

원시 시차값 대신 최근 이력의 통계적 요약값을 사용할 수도 있습니다.

# 이동 윈도우 통계량
df['rolling_mean_7']  = sales.shift(1).rolling(7).mean()
df['rolling_std_7']   = sales.shift(1).rolling(7).std()
df['rolling_max_7']   = sales.shift(1).rolling(7).max()
df['expanding_mean']  = sales.shift(1).expanding().mean()

모든 윈도우 연산 전에 .shift(1)이 있는 점에 주목하십시오.

이것은 선택 사항이 아닙니다.

이를 생략하면 시점 t의 rolling mean 계산에 y(t)가 포함됩니다.

이러한 타깃 누수(target leakage) 는 훈련 성능을 비정상적으로 높게 만들고 실제 운영 환경에서는 심각한 성능 저하를 유발합니다.

단일 단계 예측(Single-Step Forecasting): 전체 파이프라인

이제 모든 요소를 결합해 보겠습니다.

from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error
import matplotlib.pyplot as plt

# 1. 특성 행렬 생성
def build_feature_matrix(series: pd.Series) -> pd.DataFrame:
    df = pd.DataFrame({'target': series})

    # 시차 특성
    for lag in [1, 2, 3, 7, 14]:
        df[f'lag_{lag}'] = series.shift(lag)

    # 윈도우 특성
    s = series.shift(1)
    df['roll_mean_7']  = s.rolling(7).mean()
    df['roll_std_7']   = s.rolling(7).std()
    df['expand_mean']  = s.expanding().mean()

    # 날짜 특성
    df['dayofweek']    = series.index.dayofweek
    df['month']        = series.index.month

    return df.dropna()

df_features = build_feature_matrix(sales)

X = df_features.drop(columns='target')
y = df_features['target']

# 2. 시계열 분할 (절대 shuffle 금지)
split_idx = int(len(X) * 0.8)
X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:]
y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]

# 3. 모델 학습
model = GradientBoostingRegressor(
    n_estimators=200,
    max_depth=4,
    random_state=42
)
model.fit(X_train, y_train)

# 4. 평가
y_pred = model.predict(X_test)
mae = mean_absolute_error(y_test, y_pred)
print(f"Test MAE: {mae:.2f}")

# 5. 시각화
plt.figure(figsize=(12, 4))
plt.plot(y_test.index, y_test.values,
         label='Actual', linewidth=2)
plt.plot(y_test.index, y_pred,
         label='Predicted',
         linestyle='--',
         linewidth=2)

plt.title('Single-Step Forecast: GBM on Tabularized Time Series')
plt.legend()
plt.tight_layout()
plt.savefig('article1_forecast.png', dpi=150)
plt.show()

가장 치명적인 함정: 시간 누수(Temporal Leakage)

이 실수는 개발 환경에서는 모델을 천재처럼 보이게 만들고 실제 운영 환경에서는 쓸모없게 만듭니다.

누수(leakage)는 미래 정보가 특성에 암묵적으로 포함될 때 발생합니다.

시계열에서 가장 흔한 원인은 다음 세 가지입니다.

  • shift 없이 rolling 통계량 사용
  • 일반적인 train-test split 사용
  • 전체 데이터에 대해 정규화 수행
# ❌ 잘못된 방법
# y(t)가 포함됨
df['roll_mean'] = series.rolling(7).mean()

# ✅ 올바른 방법
# t 시점에서 t-1 ~ t-7만 사용
df['roll_mean'] = series.shift(1).rolling(7).mean()
# ❌ 잘못된 방법
from sklearn.model_selection import train_test_split

X_train, X_test = train_test_split(
    X,
    test_size=0.2,
    shuffle=True
)
# ✅ 올바른 방법
split = int(len(X) * 0.8)

X_train, X_test = X.iloc[:split], X.iloc[split:]

이상 탐지(Anomaly Detection)와의 연결

위에서 설명한 모든 개념은 시계열 이상 탐지에도 그대로 적용됩니다.

차이점은 다음 값을 예측하는 것이 아니라 정상적인 행동을 모델링하고 예측 오차가 임계값을 넘는 지점을 이상치로 판단한다는 점입니다.

# 재구성 오차 기반 이상 탐지
y_pred_train = model.predict(X_train)
residuals_train = y_train - y_pred_train

# 정상 오차 분포 추정
threshold = (
    residuals_train.mean()
    + 3 * residuals_train.std()
)

y_pred_all = model.predict(X)
residuals_all = y - y_pred_all

anomalies = residuals_all[
    residuals_all.abs() > threshold
]

print(f"Detected {len(anomalies)} anomalies")

이러한 단순한 접근법인

학습 → 예측 → 잔차(residual) 임계값 적용

은 많은 실제 운영 환경의 이상 탐지 시스템의 핵심 구조입니다.

핵심 요약

시계열 데이터는 일반적인 머신러닝 알고리즘이 사용할 수 있도록 반드시 테이블화되어야 합니다.

특성 행렬은 시차 특성, 윈도우 특성, 외생 변수로 구성됩니다.

시간 누수는 가장 치명적인 문제입니다. rolling 전에 반드시 shift를 적용하고, 데이터는 반드시 시간 순서대로 분할해야 합니다.

단일 단계 예측은 시작점일 뿐이며, 다중 단계 예측(multi-step forecasting)과 재귀적 예측(recursive forecasting)은 훨씬 더 복잡한 문제를 다룹니다.

동일한 테이블화 프레임워크는 예측(forecasting)뿐 아니라 이상 탐지(anomaly detection)에도 활용됩니다.

 

<출처: https://medium.com/@asidd24/tabularizing-time-series-the-foundation-of-ml-based-forecasting-2070d22651ef>

댓글