Workspace Price Tiers the competition, step by step

Lesson 4 of 10

Feature experiments

Two plausible feature ideas, measured honestly. Neither helped.

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

4. Feature-block experiments

Two extra feature blocks were tested with a fast model on 50% of operators (grouped CV, full data, measured on Kaggle):

feature set features grouped CV macro F1
v2 (base + geo + operator profile + target features) 392 0.7028
v2 + H1 demand relative to comparables (rel_*) 405 0.7016
v2 + H2 facility price effects (facte_*) 398 0.7029
v2 + both 411 0.7026

Neither block beats noise (±0.002), so the simpler v2 set is used (RUN_ABLATION = False skips re-running this). Lesson: a hypothesis that is plausible and even visible in the data can still add nothing once the model already has related features.

Fast settings on 50% of operators and 3 folds (XGBoost on GPU, otherwise LightGBM). Each result is checkpointed. Differences under ~0.002 are within noise, and then the smaller set is kept.

prog.start("ablation of feature blocks")
FEATURE_SETS = {
    "v2": [c for c in ALL_COLS if not c.startswith(("rel_", "facte_"))],
    "v2 + H1 relative": [c for c in ALL_COLS if not c.startswith("facte_")],
    "v2 + H2 facility": [c for c in ALL_COLS if not c.startswith("rel_")],
    "v2 + H1 + H2": ALL_COLS,
}
abl_rows = []
if RUN_ABLATION:
    sub = (host[:n_tr] * 2654435761 % 1000) < 500
    for name, cols in FEATURE_SETS.items():
        idx = [ALL_COLS.index(c) for c in cols]
        scores = []
        for f in range(3):
            ckn = f"{TAG}_abl_{name.replace(' ', '_').replace('+', 'p')}_{f}"
            ck = ck_load(ckn)
            if ck is None:
                tr = np.where(sub & (fold_id != f))[0]
                va = np.where(sub & (fold_id == f))[0]
                if GPU:
                    pa = dict(objective="multi:softprob", num_class=K, eta=0.1, max_depth=8, min_child_weight=5, subsample=0.8,
                              colsample_bytree=0.5, max_bin=128, tree_method="hist", device="cuda", eval_metric="mlogloss", seed=SEED)
                    dva = xgb.DMatrix(X_full[va][:, idx], label=y[va])
                    b = xgb.train(pa, xgb.DMatrix(X_full[tr][:, idx], label=y[tr]), 3000, evals=[(dva, "v")],
                                  early_stopping_rounds=50, verbose_eval=False)
                    p = b.predict(dva, iteration_range=(0, b.best_iteration + 1))
                else:
                    pa = dict(objective="multiclass", num_class=K, learning_rate=0.1, num_leaves=127, min_child_samples=40,
                              feature_fraction=0.5, bagging_fraction=0.8, bagging_freq=1, max_bin=63, verbose=-1, seed=SEED, num_threads=N_THREADS)
                    m = lgb.train(pa, lgb.Dataset(X_full[tr][:, idx], y[tr]), 3000, valid_sets=[lgb.Dataset(X_full[va][:, idx], y[va])],
                                  callbacks=[lgb.early_stopping(50, verbose=False)])
                    p = m.predict(X_full[va][:, idx], num_iteration=m.best_iteration)
                ck = {"f1": np.array([fast_f1(y[va], p.argmax(1))])}
                ck_save(ckn, **ck)
            scores.append(float(ck["f1"][0]))
        abl_rows.append({"feature set": name, "n features": len(cols), "grouped CV macro F1": round(np.mean(scores), 4)})
        print(abl_rows[-1], flush=True)
    abl = pd.DataFrame(abl_rows)
    best = abl["grouped CV macro F1"].max()
    # simplest set within 0.002 of the best (fewer features = faster + less overfitting)
    FINAL_SET = abl[abl["grouped CV macro F1"] >= best - 0.002].sort_values("n features").iloc[0]["feature set"]
    display(abl)
else:
    FINAL_SET = "v2"  # winner of the measured experiments above
print("chosen feature set:", FINAL_SET)
SEL = [ALL_COLS.index(c) for c in FEATURE_SETS[FINAL_SET]]
COLS = FEATURE_SETS[FINAL_SET]
X, Xt = X_full[:, SEL], Xt_full[:, SEL]
CAT_IDX = [COLS.index(c) for c in feat["cat_cols"] if c in COLS]
SET_TAG = FINAL_SET.replace(" ", "").replace("+", "p")
prog.finish()