The problem in brief
A car dealer in Limassol wants a fair price for each used car it lists. You get 728 real listings scraped from a Cypriot classifieds site, each with its asking price, and must predict the price of 91 more. Each listing has a free-text title (make, model, engine, year), the location, year, mileage, gearbox, fuel, body type, drive, seats, colour, doors, the date of the next roadworthiness test (MOT), the engine power (missing for more than half of the cars) and a comma-separated list of extras. Prices go from 650 EUR for an old runabout to 650 000 EUR for a new Rolls-Royce.
The test prices are private on Kaggle and we never submitted anything. All scores below are cross-validated on the 728 training listings: 5-fold cross-validation repeated with 3 different shuffles, averaged.
How you are scored
The metric is the mean absolute percentage error,
converted into points out of 100:
So MAPE 0.25 gives 80 points and MAPE 0.15 or less gives the full 100. What to aim for: the public leaderboard (20 teams) has a best MAPE of 0.202 (89.6 points) and a median of 0.219. With only 91 test cars, one or two badly priced cars move those numbers by a few hundredths, so treat them as a rough target.
First look at the data

- Prices are spread over three orders of magnitude. On a raw scale a handful of luxury cars dominate everything; on a log scale the distribution is a well-behaved bell. Since MAPE only cares about relative error, the log scale is the natural one: for small errors.
- The title is the most informative column, but it must be parsed. "BMW 1-Series 1,6L 2014" contains the make (BMW), the model (1-Series), the engine size and the year. There are 52 makes and 217 models; some makes have two words (Land Rover, Alfa Romeo, Rolls Royce).
- The data is messy, as announced. Engine size uses a decimal comma ("1,6L", or "Electric"), power is missing for 56 % of the cars, MOT is a date "11/2027" or "Not applicable" (which means the car is new), extras are a free list of up to 17 items.
A first baseline
The organisers' baseline is a random forest on year, mileage, power, seats and four label-encoded categories, trained on the raw price. In our cross-validation it scores MAPE 0.550 (20 points). It loses for two reasons: it ignores the make and model, and it minimises squared euros, so it spends its effort on the Rolls-Royces and treats a 3 000 EUR error on a 1 000 EUR car as negligible. MAPE thinks the opposite.
Fixing both, by parsing the title and predicting , changes everything: a ridge regression with one-hot make and model, age, mileage and engine size already reaches 0.275, and LightGBM on the full feature set 0.247.
The key idea
Predicting the "typical" price is not what MAPE rewards. Because MAPE punishes over-estimates more than under-estimates, the best guess is systematically below the median, and you should shrink every prediction by a constant factor.
Look at the penalty for a single car (left panel below). If you predict half the true price, MAPE counts an error of 0.5. If you predict double, it counts 1.0. An under-estimate costs at most 1 (predicting 0), an over-estimate has no limit. When the model is uncertain, it is safer to lean low.

We can make this exact. Suppose that, given the features, the price is uncertain with , and we predict a constant . The expected loss is . Its derivative in is
which vanishes when the "weight" is split equally on both sides of . In other words, is the median of the distribution reweighted by . Multiplying the log-normal density by gives, after completing the square, another log-normal with mean . Hence
A model trained on price estimates , so is a median-type prediction and the MAPE-optimal one is times smaller. Our cross-validated log residuals have , so the theory suggests a factor . The empirical optimum (right panel) is 0.93: the residuals are more peaked and heavier-tailed than a Gaussian, but the order of magnitude is right. This single multiplication takes the MAPE from 0.234 to 0.225.
The error analysis confirms the story: the 15 worst errors are all over-estimates of very cheap cars (a 1 000 EUR car priced at 3 700, a 750 EUR car at 2 000). A car listed far below its "book value" is usually damaged or needs urgent sale, and nothing in the listing says so. Each of those costs 1.5 to 3.7 units of error, while an equally wrong under-estimate would cost less than 1.
The full solution
- Parsing. Make and model from the title (with the two-word makes handled), age , , km per year, engine litres (decimal comma), power, seats, doors, "MOT not applicable" flag and months until the MOT, number of extras plus flags for 8 telling ones (leather, panoramic roof, 360 camera, adaptive cruise...), city, and the raw categories.
- Target encoding of make and model: the smoothed mean log price of the category, with pseudo-listings at the global mean, computed out of fold on the training rows so that a car never sees its own price.
- Two models on : LightGBM with an L1 loss (15 leaves, 600 trees, learning rate 0.03, subsampling), whose median-type behaviour suits a metric based on absolute errors; and a ridge regression on one-hot make/model/fuel/body plus standardised age, age², log mileage, engine size, MOT flag and number of extras.
- Blend in log space, 0.7 LightGBM + 0.3 ridge: the two make different mistakes (the ridge extrapolates smoothly for rare models, the trees capture interactions such as "old luxury car").
- Shrink: final price .
pred = SHRINK * np.exp(W_LGB * fit_lgb(X, y, Xt) + (1 - W_LGB) * fit_ridge(X, y, Xt))
The shrink factor was chosen on the out-of-fold predictions. To check that this choice is not overfitted, we also chose it on 4/5 of the cars and applied it to the remaining fifth: the MAPE is the same (0.2252), and the optimum is stable at 0.93 to 0.94 for every model we tried.
Results
| approach (3 x 5-fold CV on the 728 training cars) | MAPE | points |
|---|---|---|
| organisers' baseline: random forest on raw price | 0.550 | 20.0 |
| LightGBM, MAPE objective on raw price (failed) | 0.303 | 69.4 |
| ridge on log price, one-hot make/model | 0.275 | 74.9 |
| random forest on log price | 0.273 | 75.5 |
| LightGBM on log price, L2 loss | 0.247 | 80.6 |
| LightGBM on log price, L1 loss | 0.242 | 81.7 |
| same, model name with 2 words instead of 1 (mixed: ridge got worse) | 0.237 | 82.6 |
| blend 0.7 LightGBM-L1 + 0.3 ridge | 0.234 | 83.2 |
| blend, multiplied by 0.93 (final) | 0.225 | 84.9 |
The final model reaches a cross-validated MAPE of 0.225 (84.9 points), against 0.202 for the best public leaderboard entry. The leaderboard is computed on 91 different cars; with MAPE values whose per-car standard deviation is about 0.3, a 91-car average has a standard error near 0.03, so our estimate and the top of the leaderboard are within noise of each other, but we cannot claim to match it.
A surprise: telling LightGBM to minimise MAPE directly on the raw price is clearly worse (0.303) than learning log prices. The MAPE gradient is , so the expensive cars get almost no weight and the boosting starts from a poor constant; the log transform gives a much better-conditioned problem, and the shrink then corrects the one thing it gets wrong.
What to remember
- Match the target to the metric. For a relative-error metric, learn : errors on cheap and expensive items then count the same, as in the metric.
- The best prediction depends on the loss, not only on the data. Squared error wants the mean, absolute error the median, MAPE a -weighted median that lies below both. For a log-normal uncertainty, that is the median times .
- Post-processing is part of the model. One constant factor, checked with a nested split, was worth as much as all the feature engineering after the first parse.
- Free text hides the best features. Parsing the title into make and model mattered more than any choice of algorithm.
- Small test sets make leaderboards noisy. With 91 cars, differences of 0.02 in MAPE are mostly luck; trust a repeated cross-validation instead.