Overview
sc.
The model, in one number

Used BMW & Audi prices, predicted within 6.5%.

A LightGBM model trained on 29,988 real Turkish listings, served behind a FastAPI backend on Railway. Every figure below is 5-fold out-of-fold — not a lucky holdout — and read from the same data the analysis reports.

6.49%
MAPE · OOF
mean abs. % error
0.9745
R² · OOF
variance explained
₺110K
MAE
₺75K median abs. error
29,988
listings
deduped · TR-plated
typical error vs. the comparable median — MAE (₺)
Comparable median · baseline
₺191K
LightGBM · TF-IDF+SVD · the model
₺110K

The model's mean error is 43% lower than the median price of the same model and year. Beyond model and year, the gap is closed mostly by mileage and damage.

[01]

How it's served

Two tracks. Offline, listings become a DuckDB file and a model bundle in object storage. Online, FastAPI loads that bundle into memory at boot — the raw 30K rows never leave the backend.

build · offline
Scraped listings
4 snapshots · Jan–Jun 2026
45,159
→
cars.duckdb
dedup · TR-plated · read-only
Railway Volume
→
LightGBM train
TF-IDF+SVD · log1p target
5-fold OOF
→
S3 bundle
serving/…svd.pkl
Railway S3
serve · online · request flow
Next.js client
sadikcoban.com
CORS
→
FastAPI v2.1.0
uvicorn · Railway
APIRouter
→
lgb_service
bundle in memory · thread-locked
boot-loaded
→
predict
expm1(clip) · ±6.6% band
≤15M cap
POST /api/predictGET /api/data-drift · KS + WassersteinGET /api/bi/meta · /aggGET /api/snapshotsGET /docs · OpenAPI

Bundle: {model, tfidf, cat_maps, feat_cols} — 6 categoricals + 8 numerics + free-text model & series → TF-IDF → TruncatedSVD (170 dims). Train and serve can’t drift apart: one pickle carries its own preprocessing.

[02]

Scores

Three variants share one leak-free 5-fold split; the median baseline runs on the same folds.

model comparison · 5-fold out-of-fold
VariantMAPER²MAE ₺MedAE ₺RMSE ₺
LightGBM · TF-IDF+SVD · the model6.49%0.9745109,77675,320176,225
CatBoost · TF-IDF+SVD ★6.44%0.9745110,08574,927176,130
CatBoost · native6.58%0.9739112,92577,660178,312
Comparable median · laddered11.20%0.9235191,224130,000305,050

★ wins the MAPE-only rule; the two TF-IDF+SVD variants are practically tied, and the site and the service use LightGBM. Metrics are out-of-fold (leak-free); final models train on all data. Serving returns a point price plus a fixed ±6.6% band.

[03]

What the data actually says

Findings that change how you'd read a price, not just decorate the model.

01
Brand doesn't matter
Adding brand on top of series+model moves the error by ₺1. Brand is already inside model — a badge, not a price signal.
brand only8.80
series+model6.49
+ brand6.49
Theil's U = 1.00 · model already determines brand
02
km & age, all else equal
In the hedonic regression, at the median car (11 years, 181,000 km): one more year −6.64%, 100,000 more km −15.11%.
km · per 100k−15.11%
age · per year−6.64%
hedonic R² 0.931 · every 95% CI excludes zero
03
The baseline collapses on rare cars
The comparable median prices 97.49% of cars from the same model and year. A car with no comparable steps down to the model's all-year median, then the global one, and the error grows sharply at each step.
model+year₺179K
model fb₺559K
global fb₺1,095K
97.49% with a comparable · 2.51% without, stepping down
04
Dirty data, handled before pricing
45,159 snapshots → 29,988 listings; 16 spec columns go missing together (catalog collapse); the raw "G" segment was a corrupt MPV mislabel, re-derived from series.
raw columns117
kept25
16 spec columns miss together · 98.8% co-missing
[04]

Data & method

Real TR-registered used-car detail pages, four snapshots, one listing per ad. A third of what the scraper returns is the same ad seen again.

scrape → dedup
45,159 raw snapshots
29,988 listings
latest per ad_id

The 15,171 rows between are the same ad re-scraped across four snapshots — scrape residue, not data. Dedup runs on ad_id before the CV split, so the same ad (ad_id) can't straddle folds and inflate the score.

29,988
unique listings
45,159
raw snapshots
25
features used
₺1.54M
median price
17,896
BMW
12,092
Audi
target log1p(price)5-fold OOF · leak-freededup on ad_id before splitP10–P90 ₺0.84M – ₺3.42Mhedonic + LOFO + SHAPdrift KS · PSI · EMD
[05]

Stack & deep dives

FastAPIuvicornLightGBMscikit-learn · TF-IDF+SVDDuckDBboto3 · S3SciPypandas · NumPyRailwayNext.js