내일배움캠프(QA,QC_5기)

[내일배움캠프] QA/QC_5기 ( 61일차 )

lshxkwh 2026. 6. 5. 20:18

EV 주행거리 예측 모델 개발기: CatBoost + Optuna로 RMSE 3.27km 달성

1. Baseline 모델 구축

먼저 아무런 튜닝 없이 대표적인 트리 기반 회귀 모델 4개를 비교했다.

  • Random Forest
  • LightGBM
  • CatBoost
  • XGBoost

타겟 변수는 Distance(주행거리, km) 로 설정하고, 나머지 변수들을 모두 입력 특성으로 사용하였다.

B 데이터셋 단독 결과

모델R²RMSE
CatBoost 0.819 6.50 km
Random Forest 0.799 -
LightGBM 낮음 -

A+B 통합 데이터셋 결과

모델R²RMSE
CatBoost 0.922 4.25 km
XGBoost 0.905 4.72 km
Random Forest 0.873 5.45 km
LightGBM 0.674 8.71 km

데이터를 통합하자 모든 모델의 성능이 향상되었다. 특히 CatBoost가 가장 우수한 결과를 보였다. 반면 LightGBM은 데이터 수가 약 60~70개 수준으로 적어 충분한 학습이 이루어지지 못한 것으로 판단된다.


2. Feature Importance 기반 변수 정리

Baseline CatBoost 모델을 학습한 후 Feature Importance를 분석하였다.

상위 변수들은 주행거리 예측에 강하게 기여했지만, 하위 변수들은 대부분 날씨 및 경로 관련 원-핫 인코딩 변수였다.

예를 들어,

  • Weather_rainy
  • Route_Area_Munich_South

등은 중요도가 매우 낮게 나타났다.

중요도 하위 10개 변수를 제거한 후 재학습을 수행했지만 성능 차이는 거의 없었다. 이를 통해 불필요한 변수를 줄여도 모델 성능에는 영향이 크지 않음을 확인하였다.


3. Optuna 기반 하이퍼파라미터 최적화

각 모델에 대해 Optuna를 이용한 자동 하이퍼파라미터 탐색을 수행하였다.

CatBoost

  • Objective: RMSE 최소화
  • Trial 수: 30회

Optimization History를 확인한 결과 약 20회 이후부터 RMSE 개선 폭이 거의 사라지며 수렴하는 모습을 보였다.

튜닝 결과

RMSE
4.25 km
↓
3.27 km
 

약 23%의 성능 향상이 이루어졌다.

최종 CatBoost 파라미터

iterations = 1250
depth = 3
learning_rate = 0.026
 

Random Forest와 XGBoost 역시 동일하게 Optuna를 적용하여 최적화를 수행하였다.


4. 교차검증 (Repeated K-Fold)

데이터 수가 많지 않기 때문에 단일 Train/Test Split만으로 성능을 평가하는 것은 위험하다.

이를 보완하기 위해

5-Fold × 10 Repeats
 

총 50개의 Fold에 대해 반복 검증을 수행하였다.

평균 RMSE 비교

모델평균 RMSE
CatBoost 3.27 km
XGBoost -
Random Forest -

CatBoost가 가장 낮은 평균 RMSE를 기록하였다.

또한 Fold 간 편차도 상대적으로 작아 안정성이 높은 모델임을 확인할 수 있었다.


5. SHAP 기반 모델 해석

최종 CatBoost 모델에 대해 SHAP 분석을 수행하였다.

SHAP Beeswarm Plot

가장 큰 영향을 미친 변수는 다음과 같았다.

  1. Duration
  2. SOC_Consumed
  3. Velocity_mean

특히 Duration은 SHAP 값 분포가 가장 넓게 나타났으며, 값이 증가할수록 예측 주행거리도 증가하는 경향을 보였다.

SHAP Importance

상위 변수

Duration
SOC_Consumed
Velocity_mean
 

Random Forest와 비교

흥미로운 점은 모델마다 중요하게 보는 변수가 달랐다는 것이다.

CatBoost

속도
가속도
주행 패턴
 

위주로 판단

Random Forest

배터리 온도
HVAC 사용량
 

을 상대적으로 중요하게 평가

다만

Duration
SOC_Consumed
 

는 두 모델 모두 공통적으로 가장 중요한 변수였다.


6. 실제값 vs 예측값 비교

실제값과 예측값을 Scatter Plot으로 시각화하였다.

기준선

y = x
 

에 가까울수록 예측이 정확함을 의미한다.

분석 결과

  • 단거리~중거리 구간에서는 매우 높은 정확도
  • 장거리(40km 이상) 구간에서는 오차 증가

현상이 관찰되었다.

또한 Train과 Test의 성능 차이가 크지 않아 심각한 과적합은 확인되지 않았다.


7. 잔차 분석

잔차

Residual = Actual - Prediction
 

를 분석하였다.

히스토그램

  • 0 근처에 집중
  • 좌우 대칭 형태

즉 특정 방향으로 편향된 예측은 나타나지 않았다.

실제값 vs 잔차

단거리 구간에서는 잔차가 안정적으로 분포하였다.

반면 장거리 구간에서는 잔차의 분산이 커지는 경향이 나타났다.

이는 장거리 트립 데이터 부족의 영향으로 판단된다.


8. Best Fold vs Worst Fold 분석

Repeated K-Fold 결과 중

  • 가장 성능이 좋았던 Fold
  • 가장 성능이 나빴던 Fold

를 비교하였다.

Worst Fold 특징

Elevation_MA3_std

고도 변화가 크고 불규칙

→ 회생제동 패턴 복잡

→ 예측 어려움

SOC_Consumed

Best Fold

0.10 ~ 0.15
 

Worst Fold

0.05 ~ 0.25
 

분산이 훨씬 큼

SoC_lag1_std

배터리 상태 변동성이 큰 트립 집중


반면

Velocity_mean
Battery_Current_max
 

등은 Best/Worst Fold 간 차이가 거의 없었다.

즉 어느 환경에서도 안정적으로 예측에 기여하는 변수임을 확인하였다.


결론

이번 분석에서는 CatBoost 기반 모델에 Optuna 하이퍼파라미터 최적화를 적용하여 평균 RMSE 3.27km 수준까지 성능을 향상시킬 수 있었다.

특히 SHAP 분석과 Fold 분석을 통해 단순히 성능을 높이는 것에 그치지 않고,

  • 어떤 변수가 중요한지
  • 어떤 상황에서 모델이 실패하는지
  • 왜 오차가 발생하는지

를 확인할 수 있었다.

 

오늘 추가한 핵심 작업: Lag / 이동평균 피처의 통계적 정당화
문제의식
기존 파이프라인에서는 시계열 피처를 만들 때 별 고민 없이 모든 변수에 이동평균(_MA3)과 1초 전 값(_lag1)을 붙였다. 그런데 검증 관점에서 보면 이게 약점이었다. "그냥 만들었다"가 아니라 "데이터가 이런 성질을 가지므로 만들었다"가 되어야 설득력이 생긴다.(1) Ljung-Box 검정 — Lag Feature의 근거 (자기상관성)
Lag Feature는 "과거 값이 현재 값을 설명하는가?"를 전제로 한다. 이걸 검증하는 게 자기상관 검정이다.python
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)의 근거 (정상성)
이동평균은 "추세가 있어 불안정한(비정상) 변수"를 매끄럽게 만들어 학습을 돕는다. 따라서 어떤 변수가 비정상인지 확인할 필요가 있다.python
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) 두 검정을 결합한 변수 자동 선정
마지막으로 두 검정 결과를 합쳐, 어느 한쪽이라도 유의성이 검증된 변수만 시계열 피처 대상으로 자동 선정했다.python
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개)로 변환한 뒤, 모델 입력으로 쓰기 전에 데이터가 한쪽으로 쏠려 있지 않은지 점검하는 섹션을 새로 넣었다.python
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개 변수 히스토그램+박스플롯기존
핵심 한 줄: 오늘은 "피처를 만든 행위"에 "통계적 근거"를 붙여, 파이프라인을 설명 가능하고 재현 가능하게 만든 날이었다.
다음 단계는 LassoCV + VIF로 최종 변수를 추리고, 통합 모델 vs TripB 단독 모델의 성능을 비교하는 것이다.