Workspace Price Tiers the competition, step by step

Lesson 5 of 10

Models

Logistic Regression baseline vs XGBoost, XGBoost regression, CatBoost. Pros, cons, importance.

Download the final notebook

Outputs will appear here after the full run. The code is final.

Left: what & whyRight: the code and its real output

5. Models (each fold checkpointed)

model output why it's here
logreg Logistic Regression (impute → scale → LR pipeline) 4 probabilities simple, fast, interpretable baseline; shows what non-linear models add
lgb_multi LightGBM multiclass 4 probabilities strong default for tabular data
lgb_reg LightGBM regression on tier 1–4 one score uses the order of the tiers (H4)
xgb XGBoost multiclass 4 probabilities a different boosting implementation, so its mistakes differ
xgb_reg XGBoost regression on tier 1–4 one score a second ordered model, on GPU
cat CatBoost multiclass 4 probabilities symmetric trees, strong regularisation, a third opinion
lgb_ord (optional) 4 probabilities v1's ordinal model: 3 binary models P(tier > k)

Every model uses early stopping on its validation fold; test predictions are averaged over the 5 fold models.

def pick_lgb_device():
    """LightGBM's GPU mode (OpenCL) isn't always available or faster: time 30 rounds on 60k rows on each device, keep the faster."""
    if not GPU or not any(m.startswith("lgb") for m in MODELS):
        return "cpu"
    rows_ = np.random.RandomState(0).choice(n_tr, min(60000, n_tr), replace=False)
    times = {}
    for dev in ("cpu", "gpu"):
        try:
            pa = dict(objective="multiclass", num_class=K, learning_rate=0.05, num_leaves=127, max_bin=63, feature_fraction=0.5,
                      verbose=-1, num_threads=N_THREADS, device_type=dev)
            t0 = time.time()
            lgb.train(pa, lgb.Dataset(X[rows_], y[rows_], categorical_feature=CAT_IDX), 30)
            times[dev] = time.time() - t0
        except Exception as e:
            print(f"LightGBM on {dev} not available: {str(e)[:150]}")
    print("LightGBM speed test (30 rounds, 60k rows):", {k: f"{v:.1f}s" for k, v in times.items()})
    return min(times, key=times.get) if times else "cpu"


LGB_DEVICE = pick_lgb_device()
print("LightGBM device:", LGB_DEVICE, "| XGBoost device:", "cuda" if GPU else "cpu", "| CatBoost:", "GPU" if GPU else "CPU")
LGB_BASE = dict(learning_rate=0.05, num_leaves=127, min_child_samples=40, feature_fraction=0.5, bagging_fraction=0.8,
                bagging_freq=1, lambda_l2=1.0, max_bin=63, verbose=-1, seed=SEED, num_threads=N_THREADS, device_type=LGB_DEVICE)
XGB_PARAMS = dict(objective="multi:softprob", num_class=K, eta=0.05, max_depth=8, min_child_weight=5, subsample=0.8,
                  colsample_bytree=0.5, max_bin=128, tree_method="hist", eval_metric="mlogloss", seed=SEED,
                  device="cuda" if GPU else "cpu", nthread=N_THREADS)
CAT_PARAMS = dict(loss_function="MultiClass", iterations=MAX_ROUNDS, learning_rate=0.08, depth=8, border_count=128,
                  random_seed=SEED, task_type="GPU" if GPU else "CPU", thread_count=N_THREADS, od_type="Iter", od_wait=100)


def train_lgb(params, Xtr, ytr, Xva, yva, Xte):
    dtr = lgb.Dataset(Xtr, ytr, categorical_feature=CAT_IDX)
    dva = lgb.Dataset(Xva, yva, categorical_feature=CAT_IDX, reference=dtr)
    m = lgb.train(params, dtr, MAX_ROUNDS, valid_sets=[dva],
                  callbacks=[lgb.early_stopping(100, verbose=False), lgb.log_evaluation(LOG_EVERY)])
    print(f"   best iteration: {m.best_iteration}")
    return m.predict(Xva, num_iteration=m.best_iteration), m.predict(Xte, num_iteration=m.best_iteration)


def cumulative_to_classes(p_above):
    p_above = np.minimum.accumulate(p_above, axis=1)
    edges = np.column_stack([np.ones(len(p_above)), p_above, np.zeros(len(p_above))])
    return np.clip(edges[:, :-1] - edges[:, 1:], 1e-6, 1)


def fit_lgb_multi(Xtr, ytr, Xva, yva, Xte):
    return train_lgb({**LGB_BASE, "objective": "multiclass", "num_class": K}, Xtr, ytr, Xva, yva, Xte)


def fit_lgb_reg(Xtr, ytr, Xva, yva, Xte):
    pv, pt = train_lgb({**LGB_BASE, "objective": "regression"}, Xtr, ytr + 1.0, Xva, yva + 1.0, Xte)
    return pv[:, None], pt[:, None]


def fit_lgb_ord(Xtr, ytr, Xva, yva, Xte):
    av, at = [], []
    for k in range(K - 1):
        v, t = train_lgb({**LGB_BASE, "objective": "binary"}, Xtr, (ytr > k).astype(int), Xva, (yva > k).astype(int), Xte)
        av.append(v); at.append(t)
    return cumulative_to_classes(np.column_stack(av)), cumulative_to_classes(np.column_stack(at))


from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler


def fit_logreg(Xtr, ytr, Xva, yva, Xte):
    """baseline: impute -> scale -> multinomial logistic regression (a reusable sklearn pipeline)"""
    clean = lambda A: np.where(np.isfinite(A), A, np.nan)
    m = make_pipeline(SimpleImputer(strategy="median"), StandardScaler(), LogisticRegression(max_iter=300, C=0.5))
    m.fit(clean(Xtr), ytr)
    return m.predict_proba(clean(Xva)), m.predict_proba(clean(Xte))


LAST_IMP = {}


def fit_xgb(Xtr, ytr, Xva, yva, Xte):
    dtr, dva = xgb.DMatrix(Xtr, label=ytr), xgb.DMatrix(Xva, label=yva)
    b = xgb.train(XGB_PARAMS, dtr, MAX_ROUNDS, evals=[(dva, "valid")], early_stopping_rounds=100, verbose_eval=LOG_EVERY)
    LAST_IMP.clear(); LAST_IMP.update(b.get_score(importance_type="gain"))
    it = (0, b.best_iteration + 1)
    print(f"   best iteration: {b.best_iteration}")
    return b.predict(dva, iteration_range=it), b.predict(xgb.DMatrix(Xte), iteration_range=it)


XGB_REG = {**{k: v for k, v in XGB_PARAMS.items() if k not in ("objective", "num_class", "eval_metric")},
           "objective": "reg:squarederror", "eval_metric": "rmse"}


def fit_xgb_reg(Xtr, ytr, Xva, yva, Xte):
    dtr, dva = xgb.DMatrix(Xtr, label=ytr + 1.0), xgb.DMatrix(Xva, label=yva + 1.0)
    b = xgb.train(XGB_REG, dtr, MAX_ROUNDS, evals=[(dva, "valid")], early_stopping_rounds=100, verbose_eval=LOG_EVERY)
    it = (0, b.best_iteration + 1)
    print(f"   best iteration: {b.best_iteration}")
    return b.predict(dva, iteration_range=it)[:, None], b.predict(xgb.DMatrix(Xte), iteration_range=it)[:, None]


def fit_cat(Xtr, ytr, Xva, yva, Xte):
    m = CatBoostClassifier(**CAT_PARAMS)
    m.fit(Xtr, ytr, eval_set=(Xva, yva), verbose=LOG_EVERY, use_best_model=True)
    return m.predict_proba(Xva), m.predict_proba(Xte)


TRAINERS = {"logreg": fit_logreg, "lgb_multi": fit_lgb_multi, "lgb_reg": fit_lgb_reg, "lgb_ord": fit_lgb_ord, "xgb": fit_xgb, "xgb_reg": fit_xgb_reg,
            "cat": fit_cat}
OOF, TEST = {}, {}
for key in MODELS:
    for f, (tr, va) in enumerate(folds):
        name = f"{TAG}_{SET_TAG}_{key}_fold{f}"
        prog.start(f"{key}: fold {f + 1}/{FOLDS}")
        ck = ck_load(name)
        if ck is None:
            pv, pt = TRAINERS[key](X[tr], y[tr], X[va], y[va], Xt)
            ck = {"oof": np.asarray(pv, np.float32), "test": np.asarray(pt, np.float32)}
            if key == "xgb":  # keep feature importance (gain) for section 5c
                ck["imp"] = np.array([LAST_IMP.get(f"f{i}", 0.0) for i in range(X.shape[1])], np.float32)
            ck_save(name, **ck)
            prog.finish()
        else:
            prog.finish(from_checkpoint=True)
        if key not in OOF:
            OOF[key] = np.zeros((n_tr, ck["oof"].shape[1]), np.float32)
            TEST[key] = np.zeros((len(Xt), ck["test"].shape[1]), np.float32)
        OOF[key][va] = ck["oof"]
        if "imp" in ck:
            XGB_IMP = XGB_IMP + ck["imp"] if "XGB_IMP" in globals() else ck["imp"].copy()
        TEST[key] += ck["test"] / FOLDS
        if ck["oof"].shape[1] == K:
            print(f"   fold macro F1: {fast_f1(y[va], ck['oof'].argmax(1)):.4f}")
for key in OOF:
    if OOF[key].shape[1] == K:
        print(f"{key:10s} grouped CV macro F1 (plain argmax): {fast_f1(y, OOF[key].argmax(1)):.4f}")

5b. Model comparison

Every model is scored on the same operator-grouped folds. Probability models use the most likely tier; regression models round to the nearest tier.

cmp_rows = []
for key in OOF:
    pred_ = OOF[key].argmax(1) if OOF[key].shape[1] == K else np.clip(np.rint(OOF[key][:, 0]) - 1, 0, K - 1).astype(int)
    cmp_rows.append({"model": key, "grouped CV macro F1": round(fast_f1(y, pred_), 4)})
display(pd.DataFrame(cmp_rows).sort_values("grouped CV macro F1", ascending=False).reset_index(drop=True))
model advantages disadvantages
Logistic Regression fast, stable, coefficients are interpretable, little overfitting only linear boundaries; needs scaling/imputation; can't use interactions like "zone × size" unless we build them
XGBoost strong on tabular data, handles missing values, captures interactions, fast on GPU many hyperparameters; can overfit without early stopping
XGBoost regression uses the order of tiers (predicting 2.6 is "between 2 and 3") needs cut-points to turn a number into a tier
CatBoost symmetric trees regularise well; robust with default settings slower on CPU; less flexible trees
LightGBM (optional) very fast on CPU with many cores, leaf-wise growth its GPU mode was slower than CPU here; on Kaggle's 4 cores it is the bottleneck

5c. Which features matter? (XGBoost gain, summed over folds)

import matplotlib.pyplot as plt
if "XGB_IMP" in globals():
    imp = pd.Series(XGB_IMP, index=COLS).sort_values(ascending=False)
    fig, ax = plt.subplots(figsize=(8, 7))
    imp.head(25)[::-1].plot.barh(ax=ax, title="Top 25 features by XGBoost gain")
    plt.tight_layout(); plt.show()
    groups = {"target encodings (te_/res_)": ("te_", "res_"), "geo neighbours (geo*/knn*)": ("geo", "knn"),
              "operator profile (h_/b_)": ("h_", "b_"), "facilities (f_)": ("f_",)}
    share = {g: imp[[c for c in imp.index if c.startswith(p)]].sum() / imp.sum() for g, p in groups.items()}
    share["listing's own columns"] = 1 - sum(share.values())
    display(pd.Series(share, name="share of total gain").round(3).sort_values(ascending=False).to_frame())