-
Choose the metric from the business cost FIRST — never optimize bare accuracy on imbalanced data. A 1%-positive fraud set scores 99% accuracy by predicting all-negative and catches zero fraud. Pick before you train:
| Task / situation | Metric | Why |
|---|
| Imbalanced classification, cost of missing a positive high | Recall @ fixed precision, or PR-AUC | accuracy & ROC-AUC look great while recall is ~0 |
| Imbalanced, ranking/threshold-free comparison | PR-AUC (not ROC-AUC) | ROC-AUC is optimistic under heavy imbalance |
| Balanced classification | F1 or ROC-AUC | symmetric cost |
| Asymmetric FP vs FN cost | Expected cost = cFP·FP + cFN·FN, tune threshold | maps directly to money |
| Regression, outliers matter | RMSE | penalizes large errors |
| Regression, robust to outliers | MAE / MAPE | business reads "off by X" |
| Ranking / recsys | NDCG@k, MAP@k | position-aware |
| Forecasting | MASE / sMAPE vs naive | scale-free, must beat seasonal-naive |
| Clustering (no labels) | silhouette + downstream business check | inertia alone is meaningless |
Fix the decision threshold from the cost matrix later — don't ship the default 0.5.
-
Split BEFORE any feature engineering or fitting — this is the #1 leakage source. Three sets: train / validation (or CV folds) / held-out test touched once at the very end. The split strategy is not optional — pick by data structure:
- Random stratified for i.i.d. rows:
train_test_split(X, y, stratify=y, test_size=0.2, random_state=42).
- Time-based for any temporal data — train on past, test on future, never shuffle. Use
TimeSeriesSplit for CV. A random split on time-series leaks the future and inflates every metric.
- Group split when rows share an entity (same user/patient/device across rows):
GroupKFold / StratifiedGroupKFold so the same group never appears in both train and test.
-
Engineer features inside a Pipeline fit only on train, then run a leakage audit. Every transform that learns (imputation means, scaler stats, target/one-hot encoders, feature selection) must .fit on train and only .transform val/test — otherwise test statistics leak in. Wrap it:
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.model_selection import cross_val_score
pre = ColumnTransformer([
("num", Pipeline([("imp", SimpleImputer(strategy="median")),
("sc", StandardScaler())]), num_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
])
pipe = Pipeline([("pre", pre), ("model", model)])
cross_val_score(pipe, X_train, y_train, cv=cv, scoring="average_precision")
Leakage audit checklist — a feature is leaky if it: (a) is derived from the target or post-outcome (e.g. payment_received predicting will_pay); (b) encodes future information unavailable at prediction time; (c) is an ID/timestamp that proxies the label; (d) was computed using full-dataset statistics before the split. If any single feature gives a near-perfect score, it's leakage, not skill.
-
Establish a dumb baseline before any real model. DummyClassifier(strategy="most_frequent") / DummyRegressor(strategy="mean"), then a linear baseline (LogisticRegression / Ridge). This is the bar every model must clear; a fancy model that barely beats majority-class isn't worth the complexity.
-
For tabular data, reach for gradient boosting before deep nets. Default order: linear baseline → gradient boosting (XGBoost / LightGBM / HistGradientBoostingClassifier) → deep net only if GBM plateaus and you have ample data. GBMs win on heterogeneous tabular data, need little preprocessing, and train in minutes. Handle imbalance with class_weight / scale_pos_weight or threshold tuning — not blind SMOTE (and if you oversample, do it inside the CV fold only, never before the split).
-
Tune hyperparameters with CV, search smart not exhaustive. RandomizedSearchCV or Optuna over a sensible space beats GridSearchCV on a huge grid. Always pass scoring= your step-1 metric (not accuracy) and use the same CV object as step 2. Key GBM knobs: n_estimators + learning_rate (trade off), max_depth / num_leaves, min_child_samples, subsample, colsample_bytree, plus early_stopping_rounds on a validation set.
-
Diagnose bias vs variance, then act. Compare train vs validation score:
- Train high, val low (large gap) = overfit/variance → regularize, reduce depth/leaves, add data, drop features, stronger early stopping.
- Train and val both low = underfit/bias → richer model, better features, less regularization.
Plot a learning curve to decide whether more data would even help before collecting it.
-
Track every experiment — params, metric, data version, code version. Log to MLflow / Weights & Biases (or a CSV at minimum): hyperparameters, all CV metrics with std, the data snapshot hash, git commit, and the random seed. An untracked best-run is unreproducible. Pin seeds (random_state) everywhere.
-
Evaluate the held-out test set EXACTLY ONCE, at the end. Report the step-1 metric with a confidence interval (bootstrap), the confusion matrix at your chosen threshold, and the gap vs baseline. Repeatedly peeking at test = overfitting to test by hand.
Done = the held-out test metric (chosen for business cost, evaluated once) beats both baselines beyond its CI, the leakage and split-integrity checks pass, the generalization gap is explained, and the saved pipeline reproduces predictions with no train/serve skew under a pinned, tracked run.