The story: from 0.70 to the final model
Version 1: a strong model with a broken ruler
The first notebook (Lesson 0) was already sophisticated: about 350 features, out-of-fold target encodings, LightGBM multiclass + ordinal models, a blend, and class weights tuned for macro F1.
| macro F1 | |
|---|---|
| its own cross-validation (random 5-fold) | 0.758 |
| Kaggle leaderboard | 0.70 |
A 6-point gap means the validation wasn't measuring the real task.
The diagnosis: Kaggle split by operator
Listings are run by operators, and one operator's listings are usually in the same tier (about 3 in 4 of them).
| share of rows whose operator was seen in training | |
|---|---|
| random K-fold validation | 62% |
| the real test set | ≈1% |
With random folds, the model could learn "operator X is expensive" and get rewarded on X's other listings.
On the real test set every operator is new, so that knowledge is useless. Several of v1's strongest features
(target encodings keyed on operator_ref) were really "recognise the operator" features.
Proof. Same features, same model, on a 35% sample:
| validation | macro F1 |
|---|---|
| random folds | 0.736 |
| folds grouped by operator | 0.690 |
The grouped number matches the leaderboard. From then on, every decision is made on grouped CV.
Version 2: fix the ruler, then improve
- Rebuild an operator id from the operator columns (name + platform age + class + portfolio size).
- Use
StratifiedGroupKFoldwith that id. - Drop operator-name target encodings (on the test set they'd match a different operator who happens to share the name).
- Add label-free operator-profile features and "comparable listings" features.
| feature set (fast model, 50% of operators) | grouped CV |
|---|---|
| v1 features | 0.6973 |
| without operator target encodings | 0.6988 |
| + operator profile | 0.6994 |
| + comparable listings | 0.6997 |
Small, honest gains. The big win of v2 is that the CV now predicts the leaderboard.
Version 3 and the final notebook: experiments, more models, better decoding
- Two more feature hypotheses (demand vs. comparables, facility price effects) were measured. Neither beat noise, so they were dropped. That's a normal outcome.
- More model families (Logistic Regression baseline, XGBoost, XGBoost regression, CatBoost) run on GPU.
- A stacker smooths predictions across each operator's listings.
- Decoders tuned for macro F1 (class weights, cut-points on the expected tier, per-metro cut-points), all cross-fitted.
- A scenario table picks the best combination automatically.
Final result
From the executed final notebook:
submission.csv ← stack | A class weights | CV (test-weighted) 0.7214
submission_rank2.csv ← opt_blend + xgb_reg (a=0.7) | B cut-points | CV (test-weighted) 0.7199
submission_rank3.csv ← opt_blend + xgb_reg (a=0.5) | B cut-points | CV (test-weighted) 0.7198
The lessons that transfer to any project
- Make validation imitate the test set. Ask how the test set was built: random rows? Future dates? New users? New operators?
- A big CV vs. leaderboard gap is a bug report, not bad luck.
- Measure every idea. Plausible ideas often add nothing.
- Use the structure of the problem. Ordered tiers → regression and cut-points. Operator consistency → smoothing within operators.