실제 중고차 매물 426,880건을 정제해 187,707대의 차로 시세 예측 모델을 만들고, 점수가 부풀려지지 않는 평가 방식으로 시세표·선형 모델·Random Forest·XGBoost를 비교했습니다. 예측값과 함께 80% 시세 구간, 가격을 움직인 요인까지 돌려주는 API로 마무리했습니다.
Craigslist에 올라온 미국 중고차 매물 426,880건입니다. 가격·연식·주행거리·제조사·모델·차종·상태 등이 매물마다 판매자가 직접 입력한 값이라, 시세를 배우기 전에 '시세가 아닌 값'부터 걸러야 했습니다. 트럭·픽업이 정제 후 매물의 15%를 차지해, 트럭 시세를 따로 살펴볼 만큼 충분합니다.
| 단계 | 남은 매물 | 제외 | 기준 |
|---|---|---|---|
| 원본 매물 | 426,880 | ||
| 가격 범위 | 380,369 | -46,511 | 1,000달러 미만(0·1달러 등 미기재)과 150,000달러 초과(입력 오류·수집차) 제외 |
| 연식 범위 | 364,247 | -16,122 | 연식 미기재와 1995년 이전 클래식카 제외 |
| 주행거리 | 358,266 | -5,981 | 미기재, 400,000마일 초과, 2년 넘은 차의 100마일 미만(자리표시) 제외 |
| 제조사·모델 | 344,338 | -13,928 | 제조사나 모델명이 없는 매물 제외 |
| 모델명 해석 | 344,336 | -2 | 모델명이 기호뿐이라 해석할 수 없는 매물 제외 |
| 중복 매물 | 187,707 | -156,629 | 같은 차(VIN, 없으면 제조사·모델·연식·주행거리)를 반복 게시한 매물은 최신 1건만 |
모델명도 자유 입력이라 'F150 XLT', 'f-150 lariat', 'super duty f-250'처럼 제각각이었습니다. 표기를 통일하고 트림을 떼어 기본 모델명으로 정리했고, 트럭은 1500·2500·3500 같은 적재 등급이 가격을 크게 가르기 때문에 'silverado 2500'처럼 등급까지 남기고 별도 변수(적재 등급)로도 뽑았습니다. 30건 이상 나온 378개 모델을 그대로 쓰고, 나머지는 제조사별 '기타'로 묶었습니다.
같은 차가 학습 데이터와 평가 데이터에 함께 들어가면, 모델은 시세를 배운 것이 아니라 그 차의 가격을 외운 것으로도 좋은 점수를 받습니다. 같은 XGBoost 설정과 같은 절차로, 중복을 지웠는지만 다르게 해서 두 방식을 비교했습니다. 이 점검도 최종 평가 데이터에 속한 차는 빼고 나머지 데이터로만 했습니다.
중복을 남긴 채 매물 단위로 무작위로 나누면 평가 매물의 56%가 학습 데이터에 이미 있는 차였고, ±20% 적중률이 80.8%로 나왔습니다. 중복을 지우고 차 단위로 나눈 정직한 평가에서는 68.9%입니다. 실제 서비스에서 만나는 것은 처음 보는 차이므로 이 프로젝트의 모든 수치는 뒤의 방식으로 쟀습니다.
머신러닝 없이 만들 수 있는 기준선부터 세웠습니다. 같은 모델·연식 매물 가격의 중앙값을 쓰는 '시세표' 방식으로, 표본이 적으면 모델 → 제조사 → 전체 순으로 넓혔습니다. 모든 모델은 log(가격)을 예측하고, 오차는 달러로 되돌려 쟀습니다. 아래 표와 그림은 검증 데이터 점수이며, 이 점수로 최종 모델을 골랐습니다.
| 모델 (검증 데이터) | 오차율 중앙값 | ±10% 적중 | ±20% 적중 | 평균 절대 오차 | R² (log) | 학습 시간 |
|---|---|---|---|---|---|---|
| XGBoost (튜닝) | 12.4% | 42.0% | 69.5% | $2,454 | 0.868 | 50초 |
| XGBoost (기본 설정) | 12.7% | 40.9% | 69.3% | $2,476 | 0.863 | 19초 |
| Random Forest (평균가 인코딩) | 15.2% | 34.8% | 61.5% | $2,993 | 0.826 | 25초 |
| Ridge 회귀 | 15.5% | 33.6% | 61.0% | $3,242 | 0.821 | 2초 |
| Random Forest | 16.8% | 31.7% | 57.0% | $3,280 | 0.803 | 23초 |
| 기준선: 시세표 | 18.7% | 29.3% | 52.5% | $3,808 | 0.705 | 0초 |
| 기준선: 전체 중앙값 | 53.1% | 8.7% | 16.1% | $9,157 | -0.004 | 0초 |
처음에는 Random Forest가 선형 모델(Ridge)보다도 못했습니다(오차율 16.8% 대 15.5%). 원인을 보니 378개 모델을 정수 코드로 넣어서, 나무가 '코드 번호가 큰 쪽과 작은 쪽'으로만 나눌 수 있어 모델 정보를 거의 쓰지 못하고 있었습니다. 모델·제조사·주의 평균 가격을 숫자로 바꿔 넣자(5겹 교차 방식의 타깃 인코딩) 15.2%로 좋아졌습니다. 모델을 비교할 때 한쪽만 불리한 입력을 쓰고 있지 않은지 먼저 확인해야 공정한 비교가 됩니다.
XGBoost는 Optuna로 25번 후보를 탐색했습니다. 시간을 아끼려고 학습 데이터 일부(6만 건)와 높은 학습률로 후보를 고르고, 고른 설정을 전체 학습 데이터와 낮은 학습률로 다시 학습했습니다. 탐색의 기준도 검증 데이터였습니다. 튜닝으로 줄어든 검증 오차율은 0.3%p로 작았습니다. 기본 설정도 이미 충분히 좋았다는 뜻이고, 큰 차이는 튜닝보다 데이터 정제와 모델 종류에서 나왔습니다.
모든 실행(기준선, 비교 모델, 튜닝 25회, 평가 방식 점검, 최종 평가)의 설정과 지표를 MLflow에 기록했고, 최종 모델은 모델 레지스트리에 'used-vehicle-price'로 등록했습니다.
XGBoost가 계산하는 SHAP 값으로 각 변수가 예측 가격을 얼마나 움직였는지 봤습니다. 이는 모델이 가격을 어떻게 판단했는지를 보여주는 것이고, 변수가 가격을 '일으킨' 원인이라는 뜻은 아닙니다.
다른 조건이 같을 때 차령 2년인 차는 예측 가격을 모델의 평균 예측보다 약 95% 끌어올리고, 차령이 늘수록 영향이 줄어 10년 전후에서 0이 된 뒤 15년차에는 약 33% 끌어내립니다. 감가는 처음 10년 동안 가파르고 그 뒤로 완만해집니다. 주행거리도 비슷하게 늘수록 가격을 내리는데, 차령과 주행거리를 함께 넣었기 때문에 '같은 연식이라면 더 많이 탄 차가 얼마나 싼가'를 따로 볼 수 있습니다.
차종별로 오차를 나눠 봤습니다. 트럭·픽업은 시세표 방식의 오차가 20.4%로 큰 편이었는데, 모델은 이를 12.6%로 줄여 차종 가운데 가장 크게 개선했습니다(38% 감소). 같은 모델·연식 안에서도 적재 등급, 구동 방식, 디젤 여부에 따라 가격이 크게 달라지는데, 시세표는 이 차이를 담지 못하기 때문으로 보입니다.
| 차종 | 평가 매물 | 시세표 오차율 | 모델 오차율 | ±20% 적중 | 80% 구간 포함률 |
|---|---|---|---|---|---|
| 트럭·픽업 | 5,539 | 20.4% | 12.6% | 69.9% | 79.9% |
| SUV | 7,973 | 15.6% | 10.8% | 75.3% | 80.4% |
| 세단 | 8,438 | 17.2% | 11.6% | 72.9% | 81.1% |
| 기타 차종 | 6,773 | 19.8% | 12.4% | 69.6% | 80.1% |
| 차종 미기재 | 8,818 | 21.4% | 15.0% | 61.1% | 79.6% |
트럭만으로 따로 학습한 모델도 만들어 봤습니다. 운영 방식을 정하는 판단이라 평가 데이터가 아닌 검증 데이터로 비교했습니다. 검증 데이터의 트럭 2,738대에서 트럭 전용 모델의 오차율은 12.85%, 전체 데이터로 학습한 모델은 12.49%였습니다. 전체 모델이 0.4%p 낮았습니다. 다른 차종에서 배운 감가 패턴이 트럭에도 도움이 되는 것으로 보여, 하나의 모델로 운영하는 편이 낫다고 판단했습니다.
유지율은 차종 구성이 다른 효과를 피하려고, 모델마다 '차령 1~3년 가격 중앙값' 대비 비율을 먼저 구한 뒤 차종별로 모델들의 중앙값을 냈습니다. 매물 가격 기준이라 실제 거래 가격과는 다를 수 있습니다.
시세는 한 숫자보다 '대략 얼마에서 얼마 사이'로 보여주는 것이 쓸모 있습니다. XGBoost 분위수 회귀로 10%·90% 분위수를 함께 학습해 80% 구간을 만들었는데, 평가 데이터에서 실제 가격이 구간에 든 비율이 75.1%로 목표에 미치지 못했습니다. 그래서 보정용 데이터에서 구간을 벗어난 정도를 재고 그만큼 구간을 조정하는 CQR(Conformalized Quantile Regression)을 적용해 80.3%로 맞췄습니다. 구간 폭의 중앙값은 예측 가격의 54%입니다.
평가 매물 중 실제 가격이 예측의 0.4배 미만인 매물이 492건(1.3%), 2.5배를 넘는 매물이 138건(0.4%)이었습니다. 낮은 쪽 중 62건은 5년 이내 차인데 가격이 5천 달러 미만이었습니다. 예를 들어 2020년식 캐딜락 에스컬레이드가 1,093달러로 올라와 있었는데, 할부 선납금이나 계약금을 가격 칸에 적은 광고일 가능성이 큽니다. 정제 단계에서 1,000달러 미만은 걸러냈지만 1,000달러를 살짝 넘긴 이런 값은 규칙으로 잡히지 않았습니다. 높은 쪽에서는 111,111달러, 123,456달러처럼 같은 숫자를 반복하거나 순서대로 친 가격이 보였습니다. 매물 설명 원문이 데이터에 없어 확인할 수는 없지만, 이런 매물은 모델 오차라기보다 매물 정보의 문제로 보입니다. 규칙만으로 거르기 어려운 이상 가격을 모델이 '시세 대비 이상 매물'로 짚어 검수 대상으로 넘길 수 있다는 뜻이기도 합니다.
| 매물 | 주행거리 | 등록 가격 | 예측 시세 | 가격/시세 |
|---|---|---|---|---|
| 2020 cadillac escalade platinum | 10,031 | $1,093 | $76,867 | 0.01배 |
| 2018 cadillac escalade esv | 49,342 | $1,091 | $61,194 | 0.02배 |
| 2020 lincoln navigator | 19,646 | $1,312 | $64,563 | 0.02배 |
| 2019 ram 2500 | 6,130 | $1,116 | $41,532 | 0.03배 |
| 2020 chevrolet silverado 2500hd ltz | 23,500 | $1,175 | $34,948 | 0.03배 |
| 2017 chevrolet camaro | 31,000 | $1,111 | $27,783 | 0.04배 |
| 2009 ford escape | 182,415 | $150,000 | $4,418 | 33.95배 |
| 2009 volkswagen gti | 111,111 | $111,111 | $4,900 | 22.68배 |
| 2008 ford explorer xlt 4x4 | 185,000 | $123,456 | $5,771 | 21.39배 |
| 1998 chevrolet camaro | 100 | $49,000 | $3,464 | 14.15배 |
| 실제 가격대 | 매물 | 오차율 | 평균 절대 오차 |
|---|---|---|---|
| 5천 달러 미만 | 6,489 | 21.6% | $1,367 |
| 5천~1만 | 10,016 | 13.3% | $1,317 |
| 1만~2만 | 11,027 | 10.7% | $2,084 |
| 2만~4만 | 8,099 | 9.9% | $3,658 |
| 4만 달러 이상 | 1,910 | 12.1% | $9,203 |
| 차령 | 매물 | 오차율 | ±20% 적중 |
|---|---|---|---|
| 0~3년 | 2,760 | 9.6% | 81.1% |
| 4~7년 | 9,892 | 9.7% | 80.6% |
| 8~12년 | 11,563 | 11.6% | 73.5% |
| 13~18년 | 9,412 | 15.6% | 60.5% |
| 19년 이상 | 3,914 | 23.1% | 44.2% |
5천 달러 미만 저가 차는 오차율이 가장 큽니다. 같은 금액 차이도 저가 차에서는 비율이 커지고, 7절에서 본 자리표시 가격도 이 구간에 몰려 있습니다. 차령이 19년을 넘으면 ±20% 적중률이 절반 아래로 떨어집니다. 오래된 차일수록 관리 상태가 가격을 크게 가를 텐데, 상태 정보는 판매자의 자기 평가이고 절반 가까이 비어 있어 모델이 이 차이를 담기 어려운 것으로 보입니다.
학습한 모델을 FastAPI로 감싸, 매물 정보를 보내면 예측 시세와 80% 구간, 가격을 가장 크게 움직인 요인을 돌려주게 했습니다. 판매자에게 '왜 이 가격인지'를 함께 보여줄 수 있습니다.
POST /predict
{"year": 2016, "manufacturer": "ford", "model": "F-150 XLT",
"odometer": 98000, "type": "pickup", "drive": "4wd", "state": "tx"}
→ {"estimate_usd": 30030, "range_80_usd": [24570, 38260],
"model_key": "ford f-150",
"top_factors": [{"feature": "차령", "effect_pct": 50.1},
{"feature": "모델", "effect_pct": 39.3}, …]}
정제 규칙과 API는 pytest로 검증합니다. 처음 보는 모델명도 제조사별 '기타'로 처리해 예측하고, 범위를 벗어난 입력(1995년 이전 연식 등)은 거부합니다. 학습 데이터에 주(state)가 빠진 매물이 없었기 때문에, 주는 필수 입력으로 받습니다.