v3 · Colab run · part 1 of 7
Setup & data
The notebook exactly as it ran on Colab's T4 GPU, with its real outputs.
Download the executed notebook
Workspace price tier prediction: v3
v1 → v2 recap. Kaggle split train/test by operator (only ~1% of test rows come from an operator seen in train). v1's random folds let the model see each operator's other listings, so its CV (0.758) was inflated. Operator-grouped CV gives 0.697, which matches the leaderboard (0.70). From v2 on, every decision is made on the grouped CV.
v3 = an experiment notebook. Everything below is a hypothesis that gets measured; the notebook keeps only what wins.
| # | hypothesis | why it might help |
|---|---|---|
| H1 | demand relative to comparable listings (booked less / more open days / fewer reviews than similar spaces nearby) | for the same kind of space in the same zone, the pricier ones get booked less. Data check: corr −0.085 within comparables. Trees can't compute "vs. the group average" on their own |
| H2 | facility price effects (each facility's average tier, after removing the metro effect) | 150+ one-hot columns are hard to use; one "how premium are these amenities" score is easier |
| H3 | more model families (LightGBM, XGBoost, CatBoost) + blending | different algorithms make different mistakes |
| H4 | ordinal decoding (expected-tier score + tuned cut-points) | tiers are ordered; cut-points can be tuned directly for macro F1 |
| H5 | operator smoothing (stacker using the operator's other listings' predictions) | an operator's listings are usually in the same tier, and all of a test operator's listings are in test |
| H6 | test-distribution weighting (metro mix differs: e.g. MTR-15 is 4% of train, 10% of test) | macro F1 depends on the class mix; tune decisions for the test's metro mix |
| H7 | per-metro cut-points | tiers mean very different things in different metros |
Decoders are evaluated with cross-fitting (tuned on 4 folds' predictions, scored on the 5th), so the scenario table isn't fooled by tuning on the same rows.
How to run
- Settings → Accelerator → GPU T4 (XGBoost and CatBoost are ~10× faster). It also works on CPU, but CatBoost is then skipped.
- Optional: set
QUICK_TEST = Trueand Run All (~10 min) to check that everything works, then set it back toFalse. - Save Version → "Save & Run All (Commit)". This runs on Kaggle's servers: closing the browser or losing internet doesn't stop it.
- If a run is ever cut off: Add Input → Notebook Output → this notebook's last version, then run again.
It finds the old
ckpt/folder and continues where it stopped. Every finished fold is saved.
0. Colab setup (skipped automatically on Kaggle)
Runtime → Change runtime type → T4 GPU first. This cell installs CatBoost and downloads the competition data from the course Drive link.
import sys, subprocess, zipfile from pathlib import Path as _P if "google.colab" in sys.modules: subprocess.run([sys.executable, "-m", "pip", "install", "-q", "catboost", "gdown"], check=True) if not any(_P("/content").rglob("train.csv")): import gdown gdown.download(id="1v6Ce6HLFvByXzDdcHQD1NMgA-XnJf5kf", output="/content/data_download", quiet=False) z = _P("/content/data_download") if zipfile.is_zipfile(z): zipfile.ZipFile(z).extractall("/content/data") for inner in list(_P("/content/data").rglob("*.zip")): # zip inside a zip zipfile.ZipFile(inner).extractall(inner.parent) print("train.csv found at:", [str(p) for p in _P("/content").rglob("train.csv")])
Downloading... From (original): https://drive.google.com/uc?id=1v6Ce6HLFvByXzDdcHQD1NMgA-XnJf5kf From (redirected): https://drive.google.com/uc?id=1v6Ce6HLFvByXzDdcHQD1NMgA-XnJf5kf&confirm=t&uuid=a761f33a-75fa-4c7c-8029-abe08af30049 To: /content/data_download 100%|██████████| 53.7M/53.7M [00:01<00:00, 33.5MB/s]
train.csv found at: ['/content/data/kaggle_ready_dataset/train.csv']
import json import os import shutil import subprocess import time import warnings from pathlib import Path import numpy as np import pandas as pd import lightgbm as lgb import xgboost as xgb from catboost import CatBoostClassifier from scipy import sparse from scipy.optimize import minimize from sklearn.cluster import MiniBatchKMeans from sklearn.metrics import log_loss from sklearn.model_selection import StratifiedGroupKFold from sklearn.neighbors import NearestNeighbors warnings.filterwarnings("ignore") # ======================= SETTINGS ======================= QUICK_TEST = bool(int(os.environ.get("PT_QUICK", "0"))) # True = 10% of operators + few rounds (pipeline check only) USE_LIGHTGBM = False # LightGBM runs on CPU only (its GPU mode is slower on Kaggle): ~8 min/fold RUN_ABLATION = False # test H1/H2 (feature blocks) before the big training FOLDS, SEED, K = 5, 42, 4 MAX_ROUNDS = 300 if QUICK_TEST else 6000 LOG_EVERY = 100 if QUICK_TEST else 250 # print training progress every N rounds # ========================================================= TAG = f"v3b{'q' if QUICK_TEST else ''}_f{FOLDS}" # checkpoint names (quick-test checkpoints never mix with full ones) WORK = Path("/kaggle/working") if Path("/kaggle/working").exists() else Path(".") CKPT = WORK / "ckpt" CKPT.mkdir(exist_ok=True) SEARCH = [CKPT] + (sorted(p for p in Path("/kaggle/input").glob("**/ckpt") if p.is_dir()) if Path("/kaggle/input").exists() else []) def gpu_available(): if shutil.which("nvidia-smi") is None: return False try: return subprocess.run(["nvidia-smi"], capture_output=True, timeout=10).returncode == 0 except Exception: return False GPU = gpu_available() if GPU: # all-GPU models; LightGBM only if asked for MODELS = ["xgb", "xgb_reg", "cat"] + (["lgb_multi", "lgb_reg"] if USE_LIGHTGBM else []) else: MODELS = ["lgb_multi", "lgb_reg", "xgb", "xgb_reg"] + (["cat"] if os.environ.get("PT_FORCE_CAT") else []) print("No GPU found: running on CPU and skipping CatBoost. Turn on GPU T4 for a much faster run.") N_THREADS = os.cpu_count() print(f"GPU: {GPU} | CPU threads: {N_THREADS} | models: {MODELS} | quick test: {QUICK_TEST}") print("checkpoint folders searched:", [str(p) for p in SEARCH])
GPU: True | CPU threads: 2 | models: ['xgb', 'xgb_reg', 'cat'] | quick test: False checkpoint folders searched: ['ckpt']
Helpers: checkpoints, progress bar, fast macro F1
Checkpoints are plain .npz files (NumPy arrays only, no pickle). They are written to a temp file and then renamed,
so a crash during a save never leaves a broken checkpoint.
def ck_load(name): for d in SEARCH: p = d / f"{name}.npz" if p.exists(): with np.load(p, allow_pickle=False) as z: return {k: z[k] for k in z.files} return None def ck_save(name, **arrays): tmp = CKPT / f"{name}.tmp.npz" np.savez(tmp, **arrays) tmp.replace(CKPT / f"{name}.npz") class Progress: """Overall progress bar over all big steps. ETA = remaining steps x the measured time of the same kind of step (e.g. each remaining XGBoost fold is estimated from finished XGBoost folds, not from the slower LightGBM ones).""" def __init__(self, plan): self.plan, self.total, self.done, self.t0, self.kind_times = plan, len(plan), 0, time.time(), {} def bar(self, width=30): full = int(width * self.done / self.total) return "█" * full + "░" * (width - full) + f" {self.done}/{self.total} ({100 * self.done / self.total:.0f}%)" def start(self, label): self.label, self.ts = label, time.time() print(f"\n▶ {label} [{self.bar()}]", flush=True) def finish(self, from_checkpoint=False): self.done += 1 took = time.time() - self.ts if not from_checkpoint: self.kind_times.setdefault(self.label.split(":")[0], []).append(took) left = [s.split(":")[0] for s in self.plan[self.done:]] timed = sum(np.mean(self.kind_times[k]) for k in left if k in self.kind_times) untimed = sum(k not in self.kind_times for k in left) eta = "-" if not left else f"{timed / 60:.0f} min" + (f" + {untimed} steps not timed yet" if untimed else "") how = "loaded from checkpoint" if from_checkpoint else f"took {took / 60:.1f} min" print(f"✓ {self.label}: {how} | [{self.bar()}] | elapsed {(time.time() - self.t0) / 60:.1f} min | ETA ~{eta}", flush=True) def conf(yt, yp, w=None): return np.bincount(yt * K + yp, weights=w, minlength=K * K).reshape(K, K) def f1_from_conf(cm): tp = np.diag(cm) return float(np.mean(2 * tp / np.maximum(cm.sum(0) + cm.sum(1), 1e-12))) def fast_f1(yt, yp, w=None): """macro F1 (optionally sample-weighted), ~50× faster than sklearn; used inside the tuning loops""" return f1_from_conf(conf(yt, yp, w)) PLAN = (["load data + grouped folds", "build features", "ablation of feature blocks"] + [f"{m}: fold {f + 1}/{FOLDS}" for m in MODELS for f in range(FOLDS)] + ["level-2 operator stacker", "scenario table", "write submissions"]) prog = Progress(PLAN)
1. Data, operator id, grouped folds
prog.start("load data + grouped folds") DATA_DIR = "" # optional: paste the folder that contains train.csv, e.g. "/kaggle/input/workstation-rental-price-tier-prediction" def find_data(): if DATA_DIR: return Path(DATA_DIR) if "PT_DATA" in os.environ: return Path(os.environ["PT_DATA"]) for root in ["/kaggle/input", "/content", "."]: for dirpath, _, files in os.walk(root, followlinks=True): # followlinks: Kaggle mounts inputs as symlinks if "train.csv" in files and "test.csv" in files: return Path(dirpath) found = [str(q) for q in Path("/kaggle/input").glob("*")] if Path("/kaggle/input").exists() else [] raise FileNotFoundError( "train.csv / test.csv not found. The competition data is not attached to this notebook. " "Fix: right panel -> 'Add Input' -> 'Competitions' -> search 'workstation-rental-price-tier-prediction' -> Add. " f"Currently in /kaggle/input: {found}") DATA = find_data() train = pd.read_csv(DATA / "train.csv") test = pd.read_csv(DATA / "test.csv") # operator_ref alone isn't unique (different operators share names); these columns together identify one operator HOST = ["operator_ref", "operator_platform_age_years", "operator_platform_age_months", "operator_active_years", "operator_active_months", "operator_class", "operator_portfolio_size"] host = pd.concat([train[HOST], test[HOST]], ignore_index=True).groupby(HOST, dropna=False, sort=False).ngroup().to_numpy() if QUICK_TEST: # keep 10% of operators (whole operators, so the split logic stays the same) keep = (host * 2654435761 % 1000) < 100 n0 = len(train) train, test = train[keep[:n0]].reset_index(drop=True), test[keep[n0:]].reset_index(drop=True) host = host[keep] n_tr = len(train) y = train["price_tier"].to_numpy() - 1 folds = list(StratifiedGroupKFold(FOLDS, shuffle=True, random_state=SEED).split(np.zeros(n_tr), y, host[:n_tr])) fold_id = np.zeros(n_tr, int) for i, (_, v) in enumerate(folds): fold_id[v] = i print(train.shape, test.shape, "| test rows whose operator is in train:", round(np.isin(host[n_tr:], host[:n_tr]).mean(), 4)) prog.finish()
▶ load data + grouped folds [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0/21 (0%)] (279746, 60) (114538, 59) | test rows whose operator is in train: 0.0098 ✓ load data + grouped folds: took 1.0 min | [█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1/21 (5%)] | elapsed 1.0 min | ETA ~0 min + 20 steps not timed yet