분포가 다른 데이터 간 모델 평가 방법¶
1. 문제 배경¶
- 가구별 전력 소비량 분포가 이질적일 때 단순 MSE/MAE 집계는 고부하 가구에 지배당함
- 본 프로젝트에서 Apt89(MSE=3.58) vs Apt30(MSE=0.09) — 약 87배 차이 발생
- MAPE는 저소비 구간(≈0)에서 폭발 (Apt60: 342%, Apt75: 237%)
2. 평가 방법론¶
2-1. MASE (Mean Absolute Scaled Error) ★ 핵심 대안¶
- naive forecast(lag-1) 대비 상대 오차로 정규화 → 스케일 무관
- 값이 1이면 naive와 동등, <1이면 naive보다 우수
- 시계열 예측 논문 표준 지표 (Hyndman & Koehler, 2006)
import numpy as np
def mase(y_true: np.ndarray, y_pred: np.ndarray) -> float:
"""
MASE: Mean Absolute Scaled Error
y_true, y_pred shape: (T,) — 단일 시계열
분모: in-sample naive(lag-1) MAE
"""
mae = np.mean(np.abs(y_true - y_pred))
naive_mae = np.mean(np.abs(y_true[1:] - y_true[:-1]))
return mae / (naive_mae + 1e-8)
def mase_per_household(
y_true: np.ndarray, # (N_samples, pred_len)
y_pred: np.ndarray,
) -> float:
"""배치 단위 MASE — 전체 샘플을 이어붙여 naive MAE 계산"""
y_flat = y_true.flatten()
p_flat = y_pred.flatten()
mae = np.mean(np.abs(y_flat - p_flat))
naive_mae = np.mean(np.abs(y_flat[1:] - y_flat[:-1]))
return float(mae / (naive_mae + 1e-8))
2-2. nMAE / nRMSE (Normalized MAE / RMSE)¶
- 가구별 range 또는 mean으로 정규화
- range 정규화: 모든 가구를 0~1 스케일로 통일
def nmae(y_true: np.ndarray, y_pred: np.ndarray, mode: str = "range") -> float:
"""
nMAE: Normalized MAE
mode: "range" → MAE / (max - min)
"mean" → MAE / mean(y_true)
"""
mae = np.mean(np.abs(y_true - y_pred))
if mode == "range":
denom = y_true.max() - y_true.min()
else:
denom = np.mean(np.abs(y_true))
return float(mae / (denom + 1e-8))
2-3. 순위 기반 평가 (Win Rate + 상대 개선율)¶
- 분포 가정 없이 baseline 대비 가구별 승/패 집계
- 상대 개선율 Δ%로 방향성 파악
def relative_improvement(
baseline_mse: dict[str, float], # {"Apt6": 0.7753, ...}
model_mse: dict[str, float], # {"Apt6": 0.7286, ...}
) -> dict:
results = {}
for apt in baseline_mse:
if apt not in model_mse:
continue
delta = (model_mse[apt] - baseline_mse[apt]) / baseline_mse[apt] * 100
results[apt] = {
"baseline": baseline_mse[apt],
"model": model_mse[apt],
"delta_pct": round(delta, 2), # 음수 = 개선
"win": model_mse[apt] < baseline_mse[apt],
}
win_rate = sum(v["win"] for v in results.values()) / len(results)
return {"per_household": results, "win_rate": win_rate}
# 사용 예 (DLinear vs GWN N=50, 5가구)
baseline = {"Apt6": 0.7753, "Apt15": 0.1545, "Apt30": 0.0875, "Apt51": 0.6751, "Apt88": 0.9111}
model = {"Apt6": 0.7286, "Apt15": 0.1634, "Apt30": 0.0806, "Apt51": 0.6109, "Apt88": 0.9410}
# → win_rate = 3/5 = 0.6
2-4. 계층화(Stratification) 평가¶
- 소비량 기준으로 그룹 분류 후 그룹 내 별도 집계
- 이상치 가구(Apt89 등)가 다른 그룹 결과를 오염하지 않도록 격리
def stratified_eval(
per_household_mse: dict[str, float],
consumption_mean: dict[str, float], # 가구별 평균 소비량(kW)
thresholds: tuple = (0.3, 0.7), # 저/중/고 분위 기준 (kW)
) -> dict[str, dict]:
"""
소비량 기준 3-tier 계층 분류 후 그룹별 MSE 집계
"""
groups = {"low": [], "mid": [], "high": []}
for apt, mse in per_household_mse.items():
c = consumption_mean.get(apt, 0)
if c < thresholds[0]:
groups["low"].append(mse)
elif c < thresholds[1]:
groups["mid"].append(mse)
else:
groups["high"].append(mse)
return {
g: {"mean_mse": float(np.mean(v)), "n": len(v)}
for g, v in groups.items() if v
}
2-5. 통계적 유의성 검정¶
- 단일 평균 비교 대신 가구별 paired 오차를 검정
- Wilcoxon signed-rank test: 정규분포 가정 없음, 소표본에 적합
- Diebold-Mariano test: 시계열 예측 비교 표준 (예측 오차 자기상관 처리)
from scipy.stats import wilcoxon
def wilcoxon_test(
baseline_errors: list[float], # 가구별 MAE (baseline)
model_errors: list[float], # 가구별 MAE (model)
) -> dict:
"""
H0: 두 모델의 예측 오차 분포가 동일
p < 0.05 → 유의미한 차이
"""
stat, p = wilcoxon(baseline_errors, model_errors, alternative="greater")
return {"statistic": stat, "p_value": p, "significant": p < 0.05}
# 사용 예
baseline_mae = [0.6255, 0.3097, 0.2206, 0.5408, 0.6721] # DLinear 5HH
model_mae = [0.6359, 0.2940, 0.2233, 0.5095, 0.7027] # GWN N=50 5HH
# N=5라 검정력 낮음 — 참고용으로만 사용
3. 본 프로젝트 적용 권고¶
| 현재 문제 | 권고 해결책 |
|---|---|
| MSE가 고부하 가구(Apt89 등)에 지배당함 | MASE 또는 nMAE 도입 |
| MAPE가 저소비 구간에서 폭발 | MASE로 대체, MAPE는 각주 참고용으로 격하 |
| DLinear vs GWN 단순 평균 비교 | 가구별 Win Rate + Δ% 병기 |
| N=5 가구, 단일 seed의 통계 불안정 | Wilcoxon test로 유의성 검증 (참고용) |
| GWN PAPE vs DLinear PAPE 정의 불일치 | 정의 통일 후 PAPE 비교, 또는 MASE로 대체 |
우선순위: MASE 도입 → Win Rate 집계 → 계층화 분석 순으로 적용