실전 프로젝트 대시보드

Chapter.7 실전 프로젝트 대시보드(6)

devjjun 2026. 6. 5. 20:23

TIL(Today I Learned)


1️⃣실전 프로젝트 전처리 및 통계검정(7)

오늘 추가한 핵심 작업: Lag / 이동평균 피처의 통계적 정당화

문제의식

기존 파이프라인에서는 시계열 피처를 만들 때 별 고민 없이 모든 변수에 이동평균(_MA3)과 1초 전 값(_lag1)을 붙였다. 그런데 검증 관점에서 보면 이게 약점이었다. "그냥 만들었다"가 아니라 "데이터가 이런 성질을 가지므로 만들었다"가 되어야 설득력이 생긴다.

(1) Ljung-Box 검정 — Lag Feature의 근거 (자기상관성)

Lag Feature는 "과거 값이 현재 값을 설명하는가?"를 전제로 한다. 이걸 검증하는 게 자기상관 검정이다.

from statsmodels.stats.diagnostic import acorr_ljungbox

lb = acorr_ljungbox(series, lags=[1], return_df=True)

12개 주요 변수(Velocity, Battery Current/Temperature, SoC, Motor Torque, Battery_Power, Elevation, Throttle, Ambient Temperature, Heating/AirCon Power, 가속도)에 대해 lag1 자기상관을 검정한 결과,

모든 변수에서 p-value < 0.05 → 유의미한 자기상관성 확인

주행 데이터는 본질적으로 시간 의존성이 강하다(0.1초 전 속도와 지금 속도가 크게 다를 리 없다). 이 직관이 통계적으로도 확인됐으므로, 과거 시점 정보를 반영하는 Lag Feature 생성이 정당화된다.

(2) ADF 검정 — 이동평균(MA3)의 근거 (정상성)

이동평균은 "추세가 있어 불안정한(비정상) 변수"를 매끄럽게 만들어 학습을 돕는다. 따라서 어떤 변수가 비정상인지 확인할 필요가 있다.

from statsmodels.tsa.stattools import adfuller

adf_stat, p_val, *_ = adfuller(series, autolag='AIC')

ADF 검정은 p-value가 크면 비정상(추세 존재)이다.

  • 비정상 변수 (p > 0.05): Ambient Temperature 등 — 시간 흐름에 따라 추세가 뚜렷해 불안정 → 이동평균 적용 정당화
  • 정상 변수 (p < 0.05): Velocity, Motor Torque 등 대부분 — 이미 안정적이라 추가 가공 없이 그대로 사용해도 무방

(3) 두 검정을 결합한 변수 자동 선정

마지막으로 두 검정 결과를 합쳐, 어느 한쪽이라도 유의성이 검증된 변수만 시계열 피처 대상으로 자동 선정했다.

summary['lag1 포함'] = summary['Ljung-Box p'] < 0.05
summary['MA3 포함']  = summary['ADF p'] > 0.05
summary['선택']      = summary['lag1 포함'] | summary['MA3 포함']

ts_cols = summary[summary['선택']].index.tolist()

이 과정을 거쳐 총 12개 변수가 시계열 파생 대상으로 확정됐다. 손으로 고르던 걸 검정 기반으로 바꾸니, 변수 선택 자체가 재현 가능하고 설명 가능해졌다.


오늘 추가한 시각화 ①: 편향 데이터 확인

요약통계(Trip 1개 = 관측치 1개)로 변환한 뒤, 모델 입력으로 쓰기 전에 데이터가 한쪽으로 쏠려 있지 않은지 점검하는 섹션을 새로 넣었다.

cols_to_plot = ['Velocity [km/h]_mean', 'Battery_Power_mean', 'Battery Temperature [℃]_mean']

for i, col in enumerate(cols_to_plot):
    sns.histplot(trip_summary[col], kde=True, color='skyblue')

Trip 단위 평균값 3종(속도, 배터리 출력, 배터리 온도)의 분포를 KDE 히스토그램으로 확인했다.

결론: 데이터가 특정 값에 편향되지 않고 넓은 범위에 고르게 분포하며, 학습을 방해할 만한 이상치도 보이지 않음.

Trip 단위로 집계하면 표본 수가 확 줄어들기(약 70개) 때문에, 몇몇 Trip에 쏠려 있으면 모델이 그쪽으로 과적합될 위험이 있다. 이 시각화로 그 위험이 낮다는 걸 확인했다.


시각화 ②: 이상치 탐색 (15개 주요 변수)

오늘 새로 그린 건 아니지만 파이프라인의 핵심 시각화라 함께 정리한다. 리샘플링(0.1초 → 1초 평균) 직후, 주요 15개 변수를 각각 히스토그램 + 박스플롯으로 그려 이상치를 검토했다.

대상 변수: Velocity, Elevation, Throttle, Motor Torque, 종방향 가속도, 회생제동 신호, Battery Voltage/Current/Temperature, SoC, Heating/AirCon Power, Ambient Temperature, Heat Exchanger Temperature, Cabin Temperature.

핵심 해석을 몇 가지만 추리면:

  • Velocity: 0km/h에 다수 집중(정차 시간) + 우측 145~150km/h 이상치는 실제 고속 주행 가능 → 이상치 처리 안 함
  • Throttle: 100%를 넘는 값은 물리적으로 불가능 → 센서 오류로 보고 처리 대상
  • Battery Temperature: 다봉형 분포 → 서로 다른 외기 환경/주행 조건이 섞였다는 신호 (TripA/B 혼합의 흔적)
  • Ambient Temperature: -3~33℃ 다봉형 → 여러 계절 데이터가 섞여 있음을 직접 보여줌
  • SoC: 다봉형 → 서로 다른 초기 충전 상태의 Trip이 혼합

이 시각화에서 가장 중요한 판단은 "극값 = 무조건 이상치"가 아니라는 것이다. 고속 주행, 회생제동(-400A 수준 전류, 음수 토크), 저온 초기 난방 같은 값은 도메인상 충분히 정상이다. 그래서 이상치 처리는 다음 원칙으로 정리했다.

변수이상치 범위처리근거
Throttle [%] 100% 초과 clip(0, 100) 물리적 상한 초과 = 센서 오류
AirCon Power [kW] 음수 clip(lower=0) 냉방 전력은 음수 불가
Heating Power CAN [kW] 20kW 초과 보류 5kW 이하는 저온 초기 난방으로 해석 가능
Velocity / Torque / Current 등 우측 극값 처리 안 함 고속·회생제동 등 정상 발생

물리적 제약이 명확한 변수만 클리핑하고, 나머지는 실제 주행에서 발생 가능한 값으로 보고 유지했다.


시각화 ③: 통계 검정 결과 표

코드뿐 아니라 검정 결과를 표로 출력해, 어떤 변수가 어떤 근거로 선택됐는지 한눈에 보이게 했다.

  • Ljung-Box 결과표: 변수별 LB 통계량 / p-value / lag1 포함 여부
  • ADF 결과표: 변수별 ADF 통계량 / p-value / MA3 유효 여부
  • 결합 선정표: 두 검정을 합쳐 최종 선택 여부
  • Mann-Whitney U 검정표: TripA vs TripB 핵심 변수 8개의 중앙값 차이와 유의성

특히 마지막 Mann-Whitney U 검정은 흥미로운 결과를 줬다. 날씨(Weather)와 경로(Route/Area) 그룹 간 주행거리(Distance) 차이는 통계적으로 유의하지 않았다 (p ≈ 0.79, 0.75 / rank-biserial 0.04~0.05).

얼핏 "환경이 주행거리에 영향을 준다"와 반대되는 결과처럼 보이지만, 해석은 이렇다. 환경(날씨·온도·경로)은 거리 자체가 아니라 같은 거리를 가는 데 드는 에너지/SoC 소모에 영향을 준다.

즉 환경 변수는 distance가 아니라 distance당 효율을 좌우하는 요인으로 봐야 한다는 방향이 잡혔다.


그 외 보강한 설명들

블로그/문서화 관점에서, 코드 셀 사이사이에 판단 근거를 적은 markdown을 대거 추가했다.

  • 리샘플링: 0.1초 단위는 노이즈가 크고 용량이 과도 → 1초 평균으로 집계. 단, 순간 이벤트(급가속·급제동) 정보 손실은 변화량(_diff) 피처로 부분 보완하는 trade-off 명시.
  • 결측치 처리: SoC 계열(전체 대비 2~3%)은 핵심 변수라 Trip별 앞뒤 보간으로 유지. Requested Coolant Temperature는 Trip 단위로 통째로 비어 보간 불가 → 삭제.
  • 요약통계 & 변화량: 시계열을 "Trip 1개 = 관측치 1개"로 압축. 평균이 비슷한 두 Trip이라도 변화량 표준편차가 크면 더 역동적인 주행임을 포착.
  • Overview 병합: 센서 데이터에 없는 Trip 메타데이터(날씨, 경로, 시작/종료 SoC)를 결합. 병합 후 중복 변수(외기온 mean/max, 배터리 온도 mean/max, 변별력 없는 Fan)는 재검토 후 제거.

오늘 작업 요약

Ljung-Box 검정 자기상관 확인 → Lag Feature 근거 신규
ADF 검정 정상성 확인 → 이동평균 근거 신규
검정 결합 변수 선정 12개 시계열 변수 자동 선정 신규
편향 데이터 확인 Trip 단위 평균 분포 히스토그램 신규
설명 markdown 보강 리샘플링·결측치·병합 근거 명시 보강
전체 과정 요약 표 11단계 파이프라인 한눈 정리 신규
이상치 탐색 시각화 15개 변수 히스토그램+박스플롯 기존

오늘은 "피처를 만든 행위"에 "통계적 근거"를 붙여, 파이프라인을 설명 가능하고 재현 가능하게 만든 날이었다.


2️⃣실전 프로젝트 머신러닝(7)

Baseline — 일단 4개 모델을 다 돌려봤다

튜닝 없이 RandomForest, LightGBM, CatBoost, XGBoost 네 개를 그대로 돌려 출발점을 잡았다.

B 단독 Baseline

CatBoost 0.81 6.50 km
RandomForest 0.799
LightGBM 낮음

A+B 통합 Baseline

CatBoost 0.92 4.25 km
XGBoost 0.905 4.72 km
RandomForest 0.873 5.45 km
LightGBM 0.674 8.71 km

통합 모델이 B 단독보다 전반적으로 높게 나왔다. 데이터 수가 늘어나니 당연한 흐름이기도 하다. LightGBM은 양쪽 다 아쉬웠는데, Trip 단위 데이터가 60~70개로 적어 충분히 학습되지 못한 것으로 보인다.

CatBoost를 중심


Feature Importance로 하위 변수 정리

Baseline 학습 후 CatBoost Feature Importance를 뽑아 막대 그래프로 시각화했다. 상위 20개는 확실히 중요했고, 하위 변수들은 대부분 날씨·경로 관련 원-핫 변수(Weather_rainy, Route_Area_Munich_South 등)였다.

중요도 하위 10개를 제거하고 재학습해봤더니 성능 차이가 거의 없었다. 불필요한 Feature를 줄이는 게 맞다는 확신이 생긴 지점이다.


Optuna로 하이퍼파라미터 튜닝

각 모델별로 Optuna 30회 탐색을 돌렸다.

  • CatBoost — RMSE minimize 방향으로 탐색. 30번 만에 수렴했고, plot_optimization_history로 보니 Trial 20 이후로 RMSE가 거의 안 떨어지는 게 한눈에 보였다. 최종적으로 4.25 → 3.27 km (약 23% 개선).
  • RandomForest — R² maximize. n_estimators, max_depth, min_samples_split 등 탐색.
  • XGBoost — RMSE minimize. subsample, colsample_bytree 포함 탐색.

CatBoost는 추가로 GridSearch까지 돌려 Optuna 최적값 주변을 한 번 더 파고들었다.

최종 파라미터: iterations=1250, depth=3, learning_rate=0.026
 

depth=3이라는 얕은 트리가 최적으로 나온 건, 역시 데이터가 적어 복잡한 모델이 과적합되기 쉬운 환경이라는 방증이다.


교차검증 — 5-Fold × 10회 반복

단순 train/test split 하나로 판단하기엔 데이터가 너무 적었다. 그래서 RepeatedKFold(5×10 = 50 Folds)로 검증했다.

CatBoost 3.27 km
XGBoost
RandomForest

CatBoost가 평균 RMSE도 낮고 Fold 간 편차도 작았다. 이걸 막대 그래프로 그릴 때, 가장 낮은 Fold(Best)는 초록, 가장 높은 Fold(Worst)는 주황으로 표시하니 모델별 안정성이 한눈에 들어왔다.


SHAP 분석 — 모델이 무엇을 보고 판단하는가

최종 CatBoost 모델에 SHAP TreeExplainer를 돌렸다.

Beeswarm plot

각 변수의 SHAP 값 분포를 보여주는 그래프인데, Duration(주행시간)이 압도적으로 넓게 퍼져 있었다. 값이 높을수록(붉은색) SHAP 값도 크게 양수 → 주행시간이 길수록 예측 거리가 올라가는 관계가 명확했다. 직관과도 맞아떨어진다.

Bar plot (평균 |SHAP|)

절댓값 기준 중요도 순위:

  1. Duration (주행시간)
  2. SOC_Consumed (배터리 사용량)
  3. Velocity_mean (평균속도)

모델 간 관점 차이

RandomForest와 비교하니 보는 눈이 달랐다.

  • CatBoost: 속도·가속도 같은 주행 특성 변수를 더 중요하게 평가
  • RandomForest: 배터리 온도 변화, HVAC 사용량 쪽을 더 중요하게 평가

단, 두 모델 모두 Duration, SOC_Consumed가 공통 1·2위였다. 핵심 변수에 대한 합의가 있다는 건 그만큼 이 두 변수가 신뢰할 만하다는 뜻이다.


정리하면

데이터가 적다는 한계 안에서 CatBoost + Optuna 조합으로 평균 RMSE 3.27 km까지 끌어냈다.


3️⃣모빌리티 데이터 대시보드 초안


💡 느낀점 

 

데이터가 적을수록 검증이 더 중요했다. 데이터가 이러한 성질을 가지고 있기 때문에 이렇게 행하였다는 원인에 따른 결과를 정확히 하기 위해 노력하였다. 통계 검정 뿐 아니라 머신러닝에서 split 한 번이 아니라 50 Fold로 보며 "운 좋게 나온 성능"에 속지 않을 수 있었다. 또한 RMSE 3.27만 보면 끝났을 텐데, 시각화로 들어가니 장거리 구간 약점과 Worst Fold의 진짜 원인이 보였다.모델이 실패하는 지점에는 이유가 있었다. 고도 변화·배터리 변동이 큰 Trip이라는, 데이터 성질에서 비롯된 명확한 패턴이라 다음에 뭘 해야 할지가 자연스럽게 정해졌다. 최적의 모델과 개선 후도 결정되어 추후에는 이러한 데이터를 통해 대시보드를 제작할 예정이다.