Section 2 · Lesson 7

Blend models and write the submission

Blend probabilities from different model families and turn them into the final submission file.

You'll learn

Why blending works

Two models that make different mistakes can correct each other when you average their probabilities. Model quality isn't the only thing that matters: the ordinal LightGBM model scored only slightly higher than the multiclass one, but it added more to a CatBoost blend than another multiclass model did, because its errors differ.

The model experiments tested blends on the fold-0 holdout. Blend weights came from a 0.1-step grid that minimizes holdout log loss. The "cross-fitted weighted" column applies per-class weights fit on one half and scored on the other half.

BlendArgmax F1Log lossCross-fitted weighted F1
CatBoost d8 + LightGBM (equal)0.74720.58050.7474
CatBoost d8 + CatBoost d6 (equal)0.74560.58440.7459
CatBoost d8 + ordinal LightGBM (equal)0.75020.57810.7498
0.3 CatBoost d8 + 0.3 CatBoost d6 + 0.2 ordinal + 0.2 ordinal (second seed)0.75160.57510.7519

Blends that include a mix of model families beat blends of near-identical models. Two CatBoost models averaged together gained less than a CatBoost plus an ordinal LightGBM.

Blend explorer on real holdout predictions

The three models are the saved fold-0 predictions of LightGBM multiclass, ordinal LightGBM, and CatBoost (15,000-row sample). Change the weights and watch the macro F1 and the log loss. Log loss is a steadier guide than macro F1, because it doesn't depend on a single argmax cut.

–
Blend macro F1
–
Blend log loss

Which blend gains more, two CatBoost models with different depths or one CatBoost and one ordinal LightGBM?

  1. Two CatBoost models, because both are strong.
  2. CatBoost plus ordinal LightGBM (0.7502 against 0.7456), because the models make different errors.
  3. They gain the same.

Diversity matters more than strength. See the blend table in this lesson.

The final recipe

The final notebook is deliberately simpler than the experiments. It trains two LightGBM models per fold and leaves CatBoost out. Both use the same parameters: learning rate 0.05, 127 leaves, 80% feature and row subsampling, and early stopping after 100 rounds without improvement on the fold's validation set.

for i, (fit, val) in enumerate(folds):
    Xf, Xv = X_train.iloc[fit], X_train.iloc[val]
    p_val, p_test = fit_predict({**PARAMS, "objective": "multiclass", "num_class": K},
                                Xf, y[fit], Xv, y[val], X_test)
    oof["multiclass"][val] = p_val
    pred["multiclass"] += p_test / FOLDS            # average test predictions over folds
    # ... three binary models for the ordinal version, same pattern ...

blend_oof = (oof["multiclass"] + oof["ordinal"]) / 2
weights = tune_class_weights(blend_oof)

Every fold model also predicts the test set. The notebook averages those five test predictions, which acts like a five-model ensemble for each model type.

The scores

Stage5-fold CV macro F1
Multiclass LightGBM0.7520
Ordinal LightGBM0.7547
Blend (average)0.7559
Blend with class weights [1, 0.98, 1.15, 0.852]0.7577

The notebook's guide says that these CV scores understate the expected test score by about 0.004 (Lesson 6). The 0.7577 is also slightly optimistic, because the class weights are tuned on the same predictions that report the score. A cross-fitted check put the real gain from the weights at about +0.001.

Line chart of holdout macro F1 across six development setups, rising from 0.730 to 0.762.
Development scores on the same 20% holdout, from NOTEBOOK_GUIDE.md. The weighted blend (0.762) is tuned in-sample.
The same progression as an animation (build/04_videos.py).

Write the submission

The last cell applies the class weights to the averaged test probabilities and writes the CSV with the columns ID and price_tier:

submission = pd.DataFrame({"ID": test["space_id"],
                           "price_tier": (blend_test * weights).argmax(1) + 1})
submission.to_csv("submission.csv", index=False)

Remember the + 1: the models use labels 0 to 3, and the competition wants 1 to 4. The following table shows the predicted tier counts for the 114,538 test listings, next to the training shares.

TierTest predictionsShare of testShare of train
125,35922.1%23.9%
237,55332.8%29.1%
327,16923.7%21.7%
424,45721.4%25.2%

The test mix differs from the training mix, with more tier 2 and fewer tier 4. That might reflect a different test population, in which about a third of listings have unseen operators, or a model bias. The notebook can't tell you which, so treat it as a prompt to check, not as a verdict.

Check these before you submit. The file has one row per test listing and the columns ID and price_tier. The labels run from 1 to 4. The IDs come from the test file's space_id.

The notebook adds 1 to the argmax before it writes the submission. Why?

  1. The models predict 0 to 3, but the competition expects tiers 1 to 4.
  2. It breaks ties between classes.
  3. It applies the class weights.

The first cell converts price_tier to 0-based labels, and the last cell converts predictions back.