시간의 흐름에 따라 관측된 데이터
수집된 데이터의 변수의 개수에 따라 단변량 시계열 데이터와 다변량 시계열 데이터로 나눈다
단변량 시계열 데이터 : 동일한 간격의 사간 증가에 대해 순차적으로 기록되며 한 개의 변수 관측치로 구성된 데이터를 의미
다변량 시계열 데이터 : 동일한 간격의 시간 증가에 대해 순차적으로 기록되지만 두 개 이상의 변수 관측치로 구성된 데이터를 의미
시계열 데이터는 계절성(S), 추세성(T), 순환성(C), 불규칙성(I) 이라는 특징이 존재
시계열 분석을 위해서는 시계열이 정상성을 만족해야 함
정상성 (stationary)
시간에 관계없이 데이터가 일정한 상태를 유지하는 것을 의미
시계열 데이터의 평균과 분산에 체계적인 변화 및 주기적 변동이 없다는 것
미래는 확률적으로 과거와 동일하다는 것
정상 시계열의 조건
평균은 모든 시점(시간 t)에 대해 일정하다
분산은 모든 시점(시간 t)에 대해 일정하다
자기 공분산은 시점(시간 t)에 의존하지 않고, 단지 시차에만 의존한다
차분: 현 시점 값 - 이전 시점 값
정상 시계열로 전환
평균이 일정하지 않은 경우: 원계열에 차분 사용
계절성을 갖는 비정상시계열: 계절 차분 사용
분산이 일정하지 않은 경우: 원계열에 자연로그(변환) 사용
pct_change(): 퍼센트 변화율
rooling(): 이동 평균선
df=pd.read_csv(CSV_PATH, parse_dates=['date'])
df['return'] = df['close'].pct_change() # pct_change() 는 이전 행 대비 퍼센트 변화율(%) 을 계산. (현재값 - 이전값) / 이전값 계산 결과를 반환
df['roll_mean_5'] = df['close'].rolling(5).mean() # 이동 평균선 이전 n개의 평균
'''
rolling(window, min_periods=None, center=False)
window: 개수
min_periods: 최소 유효값 개수(기본=None -> window가 꽉차야 값 산출)
center=True면 윈도우를 현재 행을 중심으로 좌우에 펼침. 미래 예측함. 금지

feature_cols = [c for c in df.columns if c not in ["date", "close", "target_next"]]
cut = int(len(df) * 0.8)
train, valid = df.iloc[:cut].copy(), df.iloc[cut:].copy()
X_tr, y_tr = train[feature_cols], train["target_next"]
X_va, y_va = valid[feature_cols], valid["target_next"]
def smape(y_true, y_pred): # 대칭 평균 절대 백분율 오차
y_true = np.array(y_true)
y_pred = np.array(y_pred)
denom = (np.abs(y_true) + np.abs(y_pred))
denom = np.where(denom == 0, 1e-9, denom) # 0으로 나눔 방지
return 100 * np.mean(np.abs(y_true - y_pred) / denom)
yhat_naive = valid["close"].values # 오늘의 종가를 내일의 종가 예측으로 사용
mae_naive = mean_absolute_error(y_va, yhat_naive)
rmse_naive = np.sqrt(mean_squared_error(y_va, yhat_naive))
smape_naive = smape(y_va, yhat_naive)
print(f"BaseLine - Naive: MAE={mae_naive:.4f}, RMSE={rmse_naive:.4f}, sMAPE={smape_naive:.4f}")
shift_k = 5
yhat_seas = valid["close"].shift(1 - shift_k) # 4일 후의 종가를 내일의 종가로 예측
# print(yhat_seas.isna().sum())
mask = ~yhat_seas.isna()
mae_naive = mean_absolute_error(y_va[mask], yhat_seas[mask])
rmse_naive = np.sqrt(mean_squared_error(y_va[mask], yhat_seas[mask]))
smape_naive = smape(y_va[mask], yhat_seas[mask])
print(f"BaseLine - Naive: MAE={mae_naive:.4f}, RMSE={rmse_naive:.4f}, sMAPE={smape_naive:.4f}")
TimeSeriesSplit
▪ 시계열 전용 교차검증
▪ 과거 → 현재 순서를 유지한 채, 점점 늘어나는(또는 고정 길이의) 학습 구간 + 미래 테스트 구간으로 나눠서 5번 평가
▪ 랜덤 셔플이 없고, 과거 정보로 미래를 예측하는 구조라 누수 방지에 적합
분할 방식
- 총 샘플 수가 N인 테스트 구간 크기는 대략 N // (n_splits + 1)
- Fold 1: train = [0 ... i1-1], test = [i1 ... i2-1]
- Fold 2: train = [0 ... i2-1], test = [i2 ... i3-1]
- …
- Fold 5: train = [0 ... i5-1], test = [i5 ... i6-1]
▪ 과거 데이터가 누적되면서 테스트 블록을 앞으로 한 칸씩 밀어가며 평가
지수 평활법(Exponential Smoothing)
▪ 이동 평균법의 종류로, 최근관측치에 더 높은 가중치를 부여하는
방법으로 최신의 변동을 민감하게 반영
▪ 최근 시점에 큰 가중치, 과거 시점으로 갈수록 가중치를 지수적으
로 줄여 나감
◼ 지수 평활법을 사용하는 모델
▪ SimpleExpSmoothing (SES)
▪ Holt (Holt’s Linear Trend)
▪ ExponentialSmoothing (Holt–Winters)
Simple Exponential Smoothing (SES, 단순지수평활)
개념
- 하나의 시계열 데이터(단변량) 만을 사용한다.
- 추세(trend) 나 계절성(seasonality) 이 없다고 가정한다.
- 최근 관측값일수록 더 큰 가중치(α, 알파)를 주어 평균 수준(level) 을 추정하는 방식이다.
- 즉, 오래된 데이터보다는 최신 데이터에 더 민감하게 반응하는 형태의 이동평균이라고 보면 된다.
사용 목적 및 적합한 상황
- 데이터가 크게 변하지 않고 비교적 안정적일 때, 즉 변화가 완만한 경우.
- 추세나 계절성 패턴이 뚜렷하지 않은 시계열에서 사용한다.
- “내일의 값은 오늘과 거의 비슷할 것”이라고 보는 나이브 예측보다,
조금 더 부드럽게 변화 추세를 반영하고 싶을 때 적합하다. - 단기적인 노이즈를 줄이고 최근 데이터의 방향성을 반영한 예측을 원할 때 사용한다.
핵심 파라미터
initialization_method
- 초기값(level의 시작점)을 정하는 방법.
- 보통 "estimated" 옵션을 사용한다. (초기값도 함께 최적화됨)
- 즉, 모델이 학습 과정에서 가장 오차가 작아지도록 초기값을 스스로 찾는다.
smoothing_level (α, 알파, 0~1 사이 값)
- “현재 관측값 yty_t을 얼마나 강하게 반영할 것인가”를 결정하는 계수.
- 값의 의미
- α가 1에 가까울수록 → 최근 값에 매우 민감하게 반응 (변화에 빠르게 따라감)
- α가 0에 가까울수록 → 이전 평균을 더 많이 신뢰 (예측이 부드럽고 느리게 반응)
- 지정하지 않으면 보통 내부에서 최적값을 자동 추정한다.

# 이동평균
k = 5
hist = pd.concat([train['close'], valid['close']])
hist_ma = hist.rolling(k).mean()
hist_roll_aligned = hist_ma.loc[valid.index]
mae = mean_absolute_error(y_va, hist_roll_aligned)
rmse = np.sqrt(mean_squared_error(y_va, hist_roll_aligned))
smape_rf = smape(y_va, hist_roll_aligned)
print(f"roll 5 mean: MAE={mae:.4f}, RMSE={rmse:.4f}, sMAPE={smape_rf:.4f}")
# 단순 지수 평활
ses_fit = SimpleExpSmoothing(train['close'], initialization_method="estimated").fit()
print(valid.index[0], valid.index[-1])
ses_pred = ses_fit.predict(start=valid.index[0], end=valid.index[-1])
ses_pred_aligned = ses_pred.shift(-1)
mask = ~ses_pred_aligned.isna()
mae = mean_absolute_error(y_va[mask], hist_roll_aligned[mask])
rmse = np.sqrt(mean_squared_error(y_va[mask], hist_roll_aligned[mask]))
smape_rf = smape(y_va[mask], hist_roll_aligned[mask])
print(f"roll 5 mean : MAE={mae:.4f}, RMSE={rmse:.4f}, sMAPE={smape_rf:.4f}")
Holt (Holt’s Linear Trend)
개념 요약
- **Simple Exponential Smoothing(SES)**에서 한 단계 발전된 형태.
- 추세(Trend) 를 함께 고려하는 지수평활법.
- “레벨(Level)” + “추세(Trend)” 두 가지 상태값을 동시에 갱신하면서 예측을 수행.
- 간단히 말해,
“최근 데이터의 평균적인 수준 + 상승·하락하는 추세”를 모두 반영해서 미래를 예측함. - 가끔 Holt-Winters의 추세만 쓰는 형태라고도 부름
(Holt-Winters는 추세 + 계절성까지 포함하지만, Holt는 계절성은 없음).
사용 예시
- 추세가 뚜렷한 시계열 데이터 (예: 지속적으로 오르거나 내리는 주가, 판매량 등)에 적합.
- 계절성(주기적인 패턴)은 없거나 약할 때 사용.
- 단순히 “최근값을 평활화(SES)” 하는 것보다,
시간에 따라 증가·감소하는 경향까지 예측에 반영하고 싶을 때 좋음. - 하지만 추세를 너무 민감하게 반영하면 과적합이 될 수 있어서→ damped=True 옵션으로 완만하게 조정하는 경우가 많음.
(이걸 “댐핑(damping)”이라고 함 — 추세가 장기적으로 폭주하지 않게 완화시킴.)
핵심 파라미터
| damped_trend=True | 추세를 완만하게 만들어 장기 폭주 방지. 예를 들어 상승세가 끝없이 커지지 않게 조절함. |
| smoothing_level (0–1) | 관측값(실제값)을 얼마나 강하게 반영할지 조절. 1에 가까울수록 최근값 반영이 강함, 0에 가까울수록 과거 평균 반영이 강함. |
| smoothing_trend (0–1) | 추세(상승/하락)를 얼마나 강하게 반영할지 조절. 1이면 추세 변화에 즉각 반응, 0이면 추세를 거의 고정. |

Exponential Smoothing (Holt–Winters)
개념 요약
- Holt-Winters는 지수평활법(Exponential Smoothing)의 가장 일반적인 형태야.
- 하나의 시계열을 구성하는 세 가지 요소를 모두 다룸:
레벨(Level) + 추세(Trend) + 계절성(Seasonality) - 즉, 단순한 평균 변화뿐 아니라 시간적 패턴과 주기적 반복까지 반영함.
- 계절성(Seasonality) 표현 방식은 두 가지 중 선택:
- additive(덧셈형) → 변화 폭이 일정할 때
- multiplicative(곱셈형) → 변화 폭이 비율로 커질 때
사용 예시
- 분기별, 월별, 주별 등 주기적으로 반복되는 패턴(계절성) 이 뚜렷할 때 적합.
- 예시
- 월별 수요 변화
- 주중/주말에 따라 다른 방문자 수
- 분기별 매출 패턴 등
즉, 단순한 추세(상승·하락)뿐 아니라
반복적인 계절 패턴까지 고려해야 할 때 사용.
핵심 파라미터
| trend | 추세 반영 여부. "add"는 선형추세, None은 추세 없음. |
| seasonal | 계절성 반영 방식. "add"(덧셈형), "mul"(곱셈형), 또는 None. |
| seasonal_periods | 계절 주기의 길이. 예: 월별 데이터에서 1년 주기면 12. 주간 데이터에서 1개월 주기면 4 정도. |
| damped_trend=True | 추세를 완만하게 조정해서 장기 폭주를 막음. |
| initialization_method="estimated" | 초기값(level, trend, seasonality)을 자동으로 최적화해서 학습 시작. |
실습
DAILY_SALES_PATH = '/content/drive/MyDrive/multicampus/시각화/data/daily_sales.csv'
# 이동 평균선으로 이전 n개 평균 구하기
df=pd.read_csv(DAILY_SALES_PATH, parse_dates=['date'])
df["revenue"] = df["sales"] * df["price"]
df
df['return'] = df['revenue'].pct_change() # pct_change() 는 이전 행 대비 퍼센트 변화율(%) 을 계산. (현재값 - 이전값) / 이전값 계산 결과를 반환
df['roll_mean_5'] = df['revenue'].rolling(5).mean() # 이동 평균선 이전 n개의 평균
df['roll_mean_20'] = df['revenue'].rolling(20).mean() # 이동 평균선 이전 n개의 평균
print(df.head(10))
# 추세 시각화
plt.figure(figsize=(9, 5))
plt.plot(df["date"], df["revenue"], label="revenue Price")
plt.plot(df["date"], df["roll_mean_5"], label="5-day Mean")
plt.plot(df["date"], df["roll_mean_20"], label="20-day Mean")
plt.legend()
plt.tight_layout()
plt.show()

#
df["dow"] = df["date"].dt.dayofweek
df["month"] = df["date"].dt.month
df["ret_1"] = df["revenue"].pct_change(1)
df["ret_5"] = df["revenue"].pct_change(5) # 5일전 대비 퍼센트
LAGS = [1, 2, 3, 5, 7, 9]
for i in LAGS:
df[f"revenue_lag{i}"] = df["revenue"].shift(i) # 이전 값 추가. 시간 지연
df["roll_mean_5"] = df["revenue"].rolling(5).mean().shift(1) # 직전 5일 평균 revenue. shift(1)은 미래 데이터 누출 방지
df["roll_std_5"] = df["revenue"].rolling(5).std().shift(1) # 직전 5일 revenue의 표준편차
df["roll_mean_20"] = df["revenue"].rolling(20).mean().shift(1)
df["target_next"] = df["revenue"].shift(-1)
df = df.dropna().reset_index(drop=True)
df.head()

# 대칭 평균 절대 백분율 오차로 BaseLine 2개 만들어보기
feature_cols = [c for c in df.columns if c not in ["date", "revenue", "target_next"]]
cut = int(len(df) * 0.8)
train, valid = df.iloc[:cut].copy(), df.iloc[cut:].copy() # train이 80퍼, valid가 20퍼
X_tr, y_tr = train[feature_cols], train["target_next"]
X_va, y_va = valid[feature_cols], valid["target_next"]
def smape(y_true, y_pred): # 대칭 평균 절대 백분율 오차
y_true = np.array(y_true)
y_pred = np.array(y_pred)
denom = (np.abs(y_true) + np.abs(y_pred))
denom = np.where(denom == 0, 1e-9, denom) # 0으로 나눔 방지. np.where(조건, 참일때, 거짓일때)
return 100 * np.mean(np.abs(y_true - y_pred) / denom)
yhat_naive = valid["revenue"].values # 오늘의 종가를 내일의 종가 예측으로 사용
mae_naive = mean_absolute_error(y_va, yhat_naive)
rmse_naive = np.sqrt(mean_squared_error(y_va, yhat_naive))
smape_naive = smape(y_va, yhat_naive)
print(f"BaseLine - Naive: MAE={mae_naive:.4f}, RMSE={rmse_naive:.4f}, sMAPE={smape_naive:.4f}")
shift_k = 5
yhat_seas = valid["revenue"].shift(1 - shift_k) # 4일 후의 종가를 오늘의 종가 예측으로 본다
# print(yhat_seas.isna().sum())
mask = ~yhat_seas.isna()
mae_naive = mean_absolute_error(y_va[mask], yhat_seas[mask])
rmse_naive = np.sqrt(mean_squared_error(y_va[mask], yhat_seas[mask]))
smape_naive = smape(y_va[mask], yhat_seas[mask])
print(f"BaseLine - Naive: MAE={mae_naive:.4f}, RMSE={rmse_naive:.4f}, sMAPE={smape_naive:.4f}")

# 랜덤포레스트
rf = RandomForestRegressor(
n_estimators=400,
bootstrap=False,
max_features='sqrt',
random_state=RANDOM_STATE,
n_jobs=-1
)
rf.fit(X_tr, y_tr)
pred_va = rf.predict(X_va)
mae = mean_absolute_error(y_va, pred_va)
rmse = np.sqrt(mean_squared_error(y_va, pred_va))
smape_rf = smape(y_va, pred_va)
print(f"RandomForestRegressor : MAE={mae:.4f}, RMSE={rmse:.4f}, sMAPE={smape_rf:.4f}")
plt.figure(figsize=(13, 5))
plt.plot(valid["date"], y_va, label="True")
plt.plot(valid["date"], pred_va, label="Pred")
plt.legend()
plt.tight_layout()
plt.show()

# 시계열 데이터를 여러 구간으로 나눠 랜덤포레스트로 예측 성능을 평가
def evaluate_window(df_in, feature_cols, date_col, y_col):
X = df_in[feature_cols]
y = df_in[y_col]
tscv = TimeSeriesSplit(n_splits=5)
fold_metrics = []
for k, (tr, te) in enumerate(tscv.split(X)):
X_tr, X_te = X.iloc[tr], X.iloc[te]
y_tr, y_te = y.iloc[tr], y.iloc[te]
rf = RandomForestRegressor(
n_estimators=400,
bootstrap=False,
max_features='sqrt',
random_state=RANDOM_STATE,
n_jobs=-1
)
rf.fit(X_tr, y_tr)
pred = rf.predict(X_te)
mae = mean_absolute_error(y_te, pred)
rmse = np.sqrt(mean_squared_error(y_te, pred))
smape_rf = smape(y_te, pred)
fold_metrics.append({"fold": k + 1, "MAE": mae, "RMSE": rmse, "sMAPE": smape_rf})
return pd.DataFrame(fold_metrics)
bt = evaluate_window(df, feature_cols, "date", "target_next")
print("MAE mean+-std:", bt["MAE"].mean())

# 이동평균
k = 5
hist = pd.concat([train['revenue'], valid['revenue']])
hist_ma = hist.rolling(k).mean()
hist_roll_aligned = hist_ma.loc[valid.index]
mae = mean_absolute_error(y_va, hist_roll_aligned)
rmse = np.sqrt(mean_squared_error(y_va, hist_roll_aligned))
smape_rf = smape(y_va, hist_roll_aligned)
print(f"roll 5 mean: MAE={mae:.4f}, RMSE={rmse:.4f}, sMAPE={smape_rf:.4f}")

# 단순 지수 평활
ses_fit = SimpleExpSmoothing(train["revenue"], initialization_method="estimated").fit()
print(valid.index[0], valid.index[-1])
ses_pred = ses_fit.predict(start=valid.index[0], end=valid.index[-1])
ses_pred_aligned = ses_pred.shift(-1)
mask = ~ses_pred_aligned.isna()
mae = mean_absolute_error(y_va[mask], ses_pred_aligned[mask])
rmse = np.sqrt(mean_squared_error(y_va[mask], ses_pred_aligned[mask]))
smape_rf = smape(y_va[mask], ses_pred_aligned[mask])
print(f"ses : MAE={mae:.4f}, RMSE={rmse:.4f}, sMAPE={smape_rf:.4f}")

# Exponential Smoothing (Holt–Winters)
s = 90
hw_fit = ExponentialSmoothing(
train['revenue'],
trend='add',
seasonal='add',
seasonal_periods=s,
initialization_method='estimated'
).fit()
hw_pred = hw_fit.forecast(steps=len(valid))
hw_pred.index = y_va.index
mask = ~hw_pred.isna()
mae = mean_absolute_error(y_va[mask], hw_pred[mask])
rmse = np.sqrt(mean_squared_error(y_va[mask], hw_pred[mask]))
smape_rf = smape(y_va[mask], hw_pred[mask])
print(f"Holt_Winters : MAE={mae:.4f}, RMSE={rmse:.4f}, sMAPE={smape_rf:.4f}")

'데이터 분석 > 머신러닝, 딥러닝' 카테고리의 다른 글
| 인공신경망(ANN : Artificial Neural Network) (0) | 2025.11.17 |
|---|---|
| 딥러닝 (0) | 2025.11.14 |
| 규제(Regularization) (0) | 2025.11.11 |
| 회귀 분석(Regression Analysis) (0) | 2025.11.10 |
| Gradient Boosting (0) | 2025.11.07 |