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 28: Nearest-Neighbor Features

Try the decision first

How many neighbours does it take before including the row itself stops distorting the estimate, and what does the legitimate feature gain?

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

How many neighbours does it take before including the row itself stops distorting the estimate, and what does the legitimate feature gain?

Neighbour target features look like a free gain, and a self-included version looks like a miracle. Measuring the three constructions against new rows shows where the shine is leakage and what remains once it is removed.

Constructed data with strong local structure; the sizes of these effects are properties of this generator, not a competition result.

The experiment

Six constructed datasets, each with 40 tight clusters in five dimensions with event rates of 0.12 or 0.88, 1,500 training rows and 4,000 new rows from the same generator. The feature is the mean label of the K nearest training rows after a training-fitted scaler. It is built three ways: with the row itself among its neighbours, with the row excluded once on the full training set, and nested inside each of five outer folds. A histogram gradient boosting classifier is scored by AUC, in 5-fold cross-validation and on the new rows, and compared with the same model without the feature.

Calculation code
import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import KFold
from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import StandardScaler

from kaggle_companion.activities._common import clean

SEED = 28
REPLICATES = 6          # independent constructed datasets; every estimate is their mean
DIMS, BLOBS = 5, 40
N_TRAIN, N_NEW = 1500, 4000


def generate(rng):
    """Forty tight clusters in five dimensions, each with its own event rate (0.12 or 0.88).
    The label is a property of the neighbourhood, so neighbours' labels carry real signal."""
    centers = rng.uniform(-3, 3, (BLOBS, DIMS))
    rate = np.where(rng.random(BLOBS) < 0.5, 0.12, 0.88)

    def rows(n):
        b = rng.integers(0, BLOBS, n)
        return centers[b] + rng.normal(0, 0.55, (n, DIMS)), (rng.random(n) < rate[b]).astype(int)

    return rows(N_TRAIN), rows(N_NEW)


def neighbour_mean(X_db, y_db, X_query, k, exclude_self):
    """Mean label of the k nearest database rows. The scaler and the database come from X_db only.
    With exclude_self the query rows ARE the database rows, so each one's first neighbour is itself and is dropped."""
    scaler = StandardScaler().fit(X_db)
    nn = NearestNeighbors(n_neighbors=k + int(exclude_self)).fit(scaler.transform(X_db))
    ids = nn.kneighbors(scaler.transform(X_query))[1]
    return y_db[ids[:, int(exclude_self):]].mean(axis=1)


def auc(X_fit, y_fit, X_eval, y_eval):
    model = HistGradientBoostingClassifier(max_iter=60, max_leaf_nodes=8, random_state=0).fit(X_fit, y_fit)
    return roc_auc_score(y_eval, model.predict_proba(X_eval)[:, 1])


def with_feature(X, f):
    return np.column_stack([X, f])


def one_replicate(k, seed):
    (X, y), (X_new, y_new) = generate(np.random.default_rng(seed))
    outer = list(KFold(5, shuffle=True, random_state=1).split(X))
    cv = lambda col: np.mean([auc(with_feature(X[a], col[a]), y[a], with_feature(X[b], col[b]), y[b]) for a, b in outer])

    baseline = (np.mean([auc(X[a], y[a], X[b], y[b]) for a, b in outer]), auc(X, y, X_new, y_new))
    query_new = neighbour_mean(X, y, X_new, k, exclude_self=False)    # new rows are queried against the training rows

    # 1. Self included: the training column is built from the same rows it describes, each row's own label among them.
    col_self = neighbour_mean(X, y, X, k, exclude_self=False)
    # 2. Self excluded once on the full training set, then ordinary cross-validation on that column.
    col_loo = neighbour_mean(X, y, X, k, exclude_self=True)
    # 3. Nested: inside each outer fold, exclude self among the outer-training rows and query the validation rows from them.
    nested = np.mean([auc(with_feature(X[a], neighbour_mean(X[a], y[a], X[a], k, True)), y[a],
                          with_feature(X[b], neighbour_mean(X[a], y[a], X[b], k, False)), y[b]) for a, b in outer])
    deployed_loo = auc(with_feature(X, col_loo), y, with_feature(X_new, query_new), y_new)
    return {"baseline": baseline,
            "self_included": (cv(col_self), auc(with_feature(X, col_self), y, with_feature(X_new, query_new), y_new)),
            "self_excluded": (cv(col_loo), deployed_loo),
            "nested": (nested, deployed_loo)}      # pipelines 2 and 3 deploy the same model; they differ in the estimate


def run(k):
    reps = [one_replicate(k, SEED * 100 + i) for i in range(REPLICATES)]
    out = {"neighbors": k, "replicates": REPLICATES, "training_rows": N_TRAIN, "new_rows": N_NEW}
    for name in ("baseline", "self_included", "self_excluded", "nested"):
        out[name] = {"cv": float(np.mean([r[name][0] for r in reps])), "new_rows": float(np.mean([r[name][1] for r in reps]))}
    out["per_replicate_cv"] = {n: [r[n][0] for r in reps] for n in ("self_included", "self_excluded", "nested")}
    return clean(out)

Measure it across the control

The control is neighbours k in the target-mean feature. Each value below runs the whole experiment from the generator up.

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

VALUES = [1, 5, 20, 50]
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())
                              1      5     20     50
Self-included CV          1.000  0.921  0.880  0.865
Self-included, new rows   0.754  0.856  0.869  0.867
Nested CV                 0.827  0.853  0.860  0.858
Honest feature, new rows  0.836  0.858  0.865  0.866
No feature, new rows      0.836  0.836  0.836  0.836

Look at it

The figure shows the default setting, neighbours k in the target-mean feature = 5.

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

DEFAULT = 5
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 K = 5: the self-included pipeline reports 0.921 but scores 0.856 on new rows, 0.921 - 0.856 = 0.065 of optimism. Excluding the row once reports 0.853; the nested estimate 0.853 differs from the new-row score 0.858 by -0.005. Against 0.836 without the feature, the honest feature adds 0.022 AUC on new rows. In a self-included feature the row's own label is 1/5 of the mean.

Apply this to your competition

Book location: Chapter 28, A Three-Row Query.

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.

Compute neighborhood statistics with nodata-aware normalization

Conditions

  • raster neighborhoods include missing cells

Action

  • Proposed: filter filled values and validity masks separately then divide to obtain multiscale local means and variances.

Check

  • Proposed: compare hand-computed windows near nodata and boundaries, validate coordinate reference systems and resolution, and assess scales on spatially held-out data.

Limits

  • Boundary mode is nearest and coordinate conversion rounds pixel positions; slope is a pixel-gradient proxy, not a calibrated physical slope.

Find source links and value grades in the lesson atlas

Fit spatial clusters and cyclic dates without target leakage

Conditions

  • housing prices vary by location and season

Action

  • Proposed adaptation of source observation: Fit spatial clusters on training locations and compute fold-safe cluster statistics with cyclic month representations.

Check

  • Proposed verification: Use spatial/time holdouts and compute any target-derived unit-price means only from training folds.

Limits

  • Average unit prices can leak target information; five-hundred centers is not established as optimal.

Find source links and value grades in the lesson atlas

Fit neighbor target summaries without validation-label access

Conditions

  • nearby applicants in a meaningful numeric subspace may share outcome patterns

Action

  • Proposed adaptation of source observation: Proposed safe adaptation: compute neighbor-label summaries using only training-fold labels and exclude the row itself.

Check

  • Proposed verification: Audit label provenance and compare against unsupervised neighbor features and ratios.

Limits

  • The source does not document leakage prevention for this supervised feature; 500 neighbors is not a default.

Find source links and value grades in the lesson atlas

Compare spatial feature complexity on spatially grouped validation

Conditions

  • sonar habitat samples share spatial context

Action

  • Proposed adaptation of source observation: Use spatially grouped folds to compare simple terrain statistics, texture and patch context before increasing derivative complexity.

Check

  • Proposed verification: Measure macro F1 across held-out spatial regions and audit raster patch overlap.

Limits

  • Repository comments are primary observations but supply no numeric derivative ablation or grouping distance.

Find source links and value grades in the lesson atlas