Winning Kaggle the Reproducible Way

Interactive chapter

This page shows the chapter notebook with its saved results. Constructed examples do not measure competition gains.

Chapter 14: What Doesn't Work

Try the decision first

When the hidden grouping matters to the label, how much of that value does a K-means column recover for a boosted-tree model?

Write your expectation before opening the worked results. Use the chapter activity to compare supported inputs. To change the calculation itself, run the notebook on GitHub.

Your answers stay on this page until you download them. They are not sent to a server.

Open the chapter activity

Worked notebook and saved results

When the hidden grouping matters to the label, how much of that value does a K-means column recover for a boosted-tree model?

The chapter says K-means geometry need not align with the target, and asks for the addition to be compared with the original features and checked for stable assignments. This measures how that comparison comes out when the answer is known.

Constructed data. The K-means gain is at most about 0.01 AUC, a few standard errors above zero at the middle settings, so the ordering between settings is not claimed; only that it stays far below the value of the true grouping.

The experiment

Constructed binary task: six hidden groups in eight dimensions whose centres sit a distance set by the control, and a label that depends on three features and on the group (group effect standard deviation 1.5 log odds). One scikit-learn histogram gradient boosting model scores AUC on 6,000 new rows with the original features, with a K-means column and distances (scaling and clusters fitted on the 800 training rows, six clusters), and with the true group id supplied directly; both group ids enter as native categorical columns. Stability is the agreement (adjusted Rand index) between clusterings fitted on two halves of the training rows; recovery is the agreement with the true groups. Sixteen independent datasets are averaged.

Calculation code
import numpy as np
from sklearn.cluster import KMeans
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import adjusted_rand_score, roc_auc_score
from sklearn.preprocessing import StandardScaler

from kaggle_companion.activities._common import clean

SEED = 14
REPLICATES = 16              # independent constructed datasets; every estimate is their mean
GROUPS, DIM = 6, 8
N_TRAIN, N_NEW = 800, 6000
EFFECT_SD = 1.5              # how strongly the true group moves the log odds


def generate(spread, seed):
    """Six hidden groups in eight dimensions. The label depends on three features and on the hidden group."""
    rng = np.random.default_rng(seed)
    centers = rng.normal(0.0, spread, (GROUPS, DIM))               # `spread` sets how far apart the groups sit
    effect = rng.normal(size=GROUPS)
    effect = (effect - effect.mean()) / effect.std() * EFFECT_SD   # group effect on the log odds
    weights = np.array([0.7, -0.5, 0.4])

    def rows(n):
        group = rng.integers(0, GROUPS, n)
        X = centers[group] + rng.normal(size=(n, DIM))
        y = (rng.random(n) < 1 / (1 + np.exp(-(X[:, :3] @ weights + effect[group])))).astype(int)
        return X, y, group

    return rows(N_TRAIN), rows(N_NEW)


def booster(**extra):
    return HistGradientBoostingClassifier(max_iter=60, learning_rate=0.1, max_leaf_nodes=8, min_samples_leaf=15,
                                          random_state=0, **extra)


def cluster_features(X_fit, *others):
    """Scale and cluster on training rows only; return the training columns and the same columns for other rows."""
    scaler = StandardScaler().fit(X_fit)
    km = KMeans(GROUPS, n_init=5, random_state=0).fit(scaler.transform(X_fit))
    def build(X):
        Z = scaler.transform(X)
        return np.column_stack([X, km.predict(Z), km.transform(Z)])   # raw columns, cluster id, distance to each centre
    return [build(X_fit)] + [build(X) for X in others], km, scaler


def run(spread):
    gain_learned, gain_known, recovery, stability, base_auc = [], [], [], [], []
    for rep in range(REPLICATES):
        (X, y, g), (X_new, y_new, g_new) = generate(spread, SEED * 100 + rep)
        score = lambda model, a, b: roc_auc_score(y_new, model.fit(a, y).predict_proba(b)[:, 1])
        base = score(booster(), X, X_new)
        (F, F_new), km, scaler = cluster_features(X, X_new)
        gain_learned.append(score(booster(categorical_features=[DIM]), F, F_new) - base)   # K-means id learned from training rows
        gain_known.append(score(booster(categorical_features=[DIM]),               # the true group id, supplied directly
                                np.column_stack([X, g]), np.column_stack([X_new, g_new])) - base)
        recovery.append(adjusted_rand_score(g_new, km.predict(scaler.transform(X_new))))   # known only because the data are constructed
        half = N_TRAIN // 2                                                         # observable: do two halves agree on new rows?
        labels = []
        for part in (slice(0, half), slice(half, N_TRAIN)):
            half_scaler = StandardScaler().fit(X[part])
            half_km = KMeans(GROUPS, n_init=5, random_state=0).fit(half_scaler.transform(X[part]))
            labels.append(half_km.predict(half_scaler.transform(X_new)))
        stability.append(adjusted_rand_score(labels[0], labels[1]))
        base_auc.append(base)
    return clean({
        "spread": spread, "baseline_auc": float(np.mean(base_auc)),
        "gain_learned": float(np.mean(gain_learned)), "gain_known": float(np.mean(gain_known)),
        "recovery": float(np.mean(recovery)), "stability": float(np.mean(stability)),
        "standard_error_learned": float(np.std(gain_learned) / np.sqrt(REPLICATES)),
        "per_replicate": {"gain_learned": gain_learned, "gain_known": gain_known},
        "replicates": REPLICATES, "train_rows": N_TRAIN, "new_rows": N_NEW,
    })

Measure it across the control

The control is spread of the group centres (standard deviations of the within-group noise). Each value below runs the whole experiment from the generator up.

Calculation code
import pandas as pd
from kaggle_companion.activities.ch14 import explain

VALUES = [0.4, 0.8, 1.2, 1.8]
results = {value: run(value) for value in VALUES}
table = pd.DataFrame({str(v): explain(results[v], v)['metrics'] for v in VALUES})
print(table.to_string())
                        0.4     0.8     1.2     1.8
Baseline AUC          0.685   0.768   0.815   0.855
K-means column gain  -0.001  +0.004  +0.010  +0.003
True group id gain   +0.143  +0.069  +0.028  +0.008
Stability (ARI)        0.16    0.62    0.88    0.98
Recovery (ARI)         0.10    0.49    0.80    0.96

Look at it

The figure shows the default setting, spread of the group centres (standard deviations of the within-group noise) = 0.8.

Calculation code
import matplotlib.pyplot as plt
from kaggle_companion.activities.ch14 import draw, NCOLS, HEIGHT

DEFAULT = 0.8
fig, axes = plt.subplots(1, NCOLS, figsize=(6.6 if NCOLS == 1 else 10.4, HEIGHT), layout='constrained')
draw(axes, results[DEFAULT], DEFAULT)
plt.show()
Saved figure from this chapter calculation

What the result says

With centre spread 0.8, the true group id adds +0.069 AUC to the baseline of 0.768. The K-means column adds +0.004 (standard error 0.001), which is 7% of it, so 0.069 - 0.004 = 0.065 of the value stays out of reach. Two halves of the training rows agree on the clusters at ARI 0.62, and the clusters match the true groups at 0.49.

Apply this to your competition

Book location: Chapter 14, K-Means Clustering as a Feature.

Technical corrections

Apply a lesson from this chapter

Select a lesson whose conditions match your project. State how you would test it and what would make you reject it.

Retain unsuccessful outlier removal as a local finding

Conditions

  • housing extremes tempt automatic deletion

Action

  • Proposed adaptation of source observation: Compare keeping extreme transactions with an explicitly documented deletion rule before dropping them.

Check

  • Proposed verification: Evaluate high-price cases and original-unit MAE on an untouched set.

Limits

  • Threshold/method are unspecified and extreme values may be legitimate.

Find source links and value grades in the lesson atlas

Keep unsuccessful PLS comparisons scoped to their setup

Conditions

  • PLS is a plausible alternative in a spectroscopic regression task.

Action

  • Proposed conditional application: Include PLS as a comparison, rather than assuming it will improve the existing setup.

Check

  • Proposed check: Tune PLScomponents and preprocessing on identical training folds and assess against the current model using the same untouched device-wise metric.

Limits

  • Negative evidence is setup-specific; source omits PLShyperparameters and exact score.

Find source links and value grades in the lesson atlas