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|)
절댓값 기준 중요도 순위:
- Duration (주행시간)
- SOC_Consumed (배터리 사용량)
- 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이라는, 데이터 성질에서 비롯된 명확한 패턴이라 다음에 뭘 해야 할지가 자연스럽게 정해졌다. 최적의 모델과 개선 후도 결정되어 추후에는 이러한 데이터를 통해 대시보드를 제작할 예정이다.
'실전 프로젝트 대시보드' 카테고리의 다른 글
| Chapter.7 실전 프로젝트 대시보드(8) (0) | 2026.06.09 |
|---|---|
| Chapter.7 실전 프로젝트 대시보드(7) (0) | 2026.06.08 |
| Chapter.7 실전 프로젝트 대시보드(5) (0) | 2026.06.04 |
| Chapter.7 실전 프로젝트 대시보드(4) (0) | 2026.06.02 |
| Chapter.7 실전 프로젝트 대시보드(3) (0) | 2026.06.01 |