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 10: Interactions, Groups, and Text

Try the decision first

As more candidate pairs are screened, does the search score keep describing what the selected features earn on new rows?

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

As more candidate pairs are screened, does the search score keep describing what the selected features earn on new rows?

Every extra pair screened is another chance for noise to look like signal. The search score is computed on the rows that chose the pairs, so it can rise while the recipe gets worse on rows that did not.

Constructed data; one interaction is real by design, and the sizes of the effects are properties of this generator. The noise cost is small per pair and shows only in the accumulation.

The experiment

A constructed binary task with 12 numeric features, three main effects and exactly one real interaction, x0 times x1. The chapter's accept rule (keep a pair if development AUC rises by more than 0.00001) runs over every pairwise product of the first k features, using a logistic regression and 5-fold cross-validation on 1,200 rows. The control is k. The selected recipe is then refitted on the same 1,200 rows and scored once on 20,000 fresh rows from the same generator, which stands in for a reserved assessment partition. Six independent datasets are averaged.

Calculation code
import itertools

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import StratifiedKFold

from kaggle_companion.activities._common import clean

SEED = 10
REPLICATES = 6         # independent constructed datasets; every estimate is their mean
N_SEARCH = 1200        # rows the stepwise search may use
N_FRESH = 20000        # fresh rows from the same generator: the stand-in for a reserved assessment partition
MIN_GAIN = 1e-5        # the chapter's accept rule: keep a pair if the development AUC rises by more than this


def generate(n, rng):
    """Twelve numeric features. One real interaction, x0 * x1, on top of three main effects; the rest is noise."""
    X = rng.normal(size=(n, 12))
    logit = 0.8 * X[:, 0] + 0.5 * X[:, 1] + 0.4 * X[:, 2] + 1.0 * X[:, 0] * X[:, 1] - 0.2
    y = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
    return X, y


def design(X, pairs):
    """Original columns plus one product column per kept pair."""
    return np.column_stack([X] + [X[:, a] * X[:, b] for a, b in pairs])


def cv_auc(X, y, folds):
    """Development score: mean AUC of a logistic regression over the search folds."""
    scores = []
    for fit, val in folds:
        model = LogisticRegression(max_iter=200).fit(X[fit], y[fit])
        scores.append(roc_auc_score(y[val], model.decision_function(X[val])))
    return float(np.mean(scores))


def stepwise_search(X, y, pairs, folds):
    """The chapter's accept rule: try each pair in turn, keep it if the development AUC rises by more than MIN_GAIN."""
    current = X
    base = cv_auc(current, y, folds)
    trace, kept = [base], []
    for a, b in pairs:
        trial = np.column_stack([current, X[:, a] * X[:, b]])
        score = cv_auc(trial, y, folds)
        if score - base > MIN_GAIN:
            base, current = score, trial
            kept.append((a, b))
            trace.append(score)
    return kept, trace


def fresh_auc(X, y, pairs, X_new, y_new):
    """Refit on the search rows with the given pairs, score once on rows the search never saw."""
    model = LogisticRegression(max_iter=200).fit(design(X, pairs), y)
    return roc_auc_score(y_new, model.decision_function(design(X_new, pairs)))


def run(top_features):
    pairs = list(itertools.combinations(range(top_features), 2))
    search_gain, fresh_gain, oracle_gain, noise, found = [], [], [], [], []
    for rep in range(REPLICATES):
        rng = np.random.default_rng(SEED * 100 + rep)
        X, y = generate(N_SEARCH + N_FRESH, rng)
        Xs, ys, Xf, yf = X[:N_SEARCH], y[:N_SEARCH], X[N_SEARCH:], y[N_SEARCH:]
        folds = list(StratifiedKFold(5, shuffle=True, random_state=42).split(Xs, ys))
        kept, trace = stepwise_search(Xs, ys, pairs, folds)
        base_fresh = fresh_auc(Xs, ys, [], Xf, yf)
        search_gain.append(trace[-1] - trace[0])
        fresh_gain.append(fresh_auc(Xs, ys, kept, Xf, yf) - base_fresh)
        oracle_gain.append(fresh_auc(Xs, ys, [(0, 1)], Xf, yf) - base_fresh)   # the one pair that is really there
        noise.append(len([p for p in kept if p != (0, 1)]))
        found.append((0, 1) in kept)
        if rep == 0:   # the path of the first dataset: development AUC and fresh AUC after each accepted pair
            path = [[i, trace[i], fresh_auc(Xs, ys, kept[:i], Xf, yf)] for i in range(len(kept) + 1)]
    return clean({
        "top_features": top_features, "pairs_screened": len(pairs),
        "search_gain": float(np.mean(search_gain)), "fresh_gain": float(np.mean(fresh_gain)),
        "oracle_gain": float(np.mean(oracle_gain)), "noise_pairs_kept": float(np.mean(noise)),
        "true_pair_found": float(np.mean(found)),
        "per_replicate": {"search_gain": search_gain, "fresh_gain": fresh_gain},
        "path": path, "replicates": REPLICATES, "search_rows": N_SEARCH, "fresh_rows": N_FRESH,
    })

Measure it across the control

The control is top features whose pairwise products are screened. Each value below runs the whole experiment from the generator up.

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

VALUES = [4, 6, 9, 12]
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())
                         4       6       9      12
Pairs screened           6      15      36      66
Search score gain   +0.060  +0.061  +0.064  +0.071
Gain on fresh rows  +0.058  +0.056  +0.052  +0.044
True pair only      +0.059  +0.059  +0.059  +0.059
Noise pairs kept       1.7     3.2     6.8    14.3

Look at it

The figure shows the default setting, top features whose pairwise products are screened = 12.

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

DEFAULT = 12
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

Screening 66 pairs, the search reports a gain of 0.071 AUC and the same recipe earns 0.044 on fresh rows, so 0.071 - 0.044 = 0.027 of the reported gain is selection bias (standard error 0.008 over 6 datasets). It kept 14.3 pairs other than the true one on average and found the true pair in 100% of datasets. Adding only the true pair earns 0.059, so the noise pairs cost 0.015 on fresh rows.

Apply this to your competition

Book location: Chapter 10, Interaction Features: The Stepwise Approach.

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.

Compress rare categorical interactions for sparse models

Conditions

  • few categorical columns have high-cardinality interactions

Action

  • Proposed adaptation of source observation: Build sparse categorical interactions, consolidate very rare combinations and compare with tree-friendly categorical features.

Check

  • Proposed verification: Measure sparsity, memory, AUC and interaction ablations on consistent folds.

Limits

  • Rare-value thresholds are unspecified; the repository currently uses fast Naive Bayes and suggests a logistic replacement, but does not establish which estimator configuration produced the original winning submission.

Find source links and value grades in the lesson atlas

Create complementary encodings and search interactions with a bounded hierarchy

Conditions

  • anonymized tabular features lack easy domain interpretations

Action

  • Proposed adaptation of source observation: Compare multiple fold-safe encodings, exhaustively screen feasible pairwise interactions and use bounded sampling for higher orders.

Check

  • Proposed verification: Fit supervised encodings within folds and evaluate interaction families on independent validation.

Limits

  • Interaction search can overfit and grow combinatorially; frequency transforms may shift at deployment.

Find source links and value grades in the lesson atlas

Reject expensive embeddings when structured features suffice

Conditions

  • categorical names tempt semantic embedding features

Action

  • Proposed adaptation of source observation: Compare text embeddings with a structured-data baseline and retain them only if quality justifies cost.

Check

  • Proposed verification: Measure original-unit RMSE, training time and inference resource use on the same split.

Limits

  • The small reported difference is not statistically tested and may not generalize.

Find source links and value grades in the lesson atlas

Fit text vocabulary after the validation split

Conditions

  • text classification with a learned TF-IDF representation

Action

  • Proposed: fit TF-IDF only on training text and transform validation text with the fitted vocabulary.

Check

  • Proposed: assert the vocabulary excludes tokens seen solely in validation; choose entity/group splitting if near-duplicate bugs share context.

Limits

  • This source shows an implementation, without an isolated gain, execution result, or confirmed final-submission provenance. Historical dependencies and dataset assumptions require local checks.

Find source links and value grades in the lesson atlas

Expose text and graph views through separate feature transformers

Conditions

  • article classification includes citation graph and textual metadata

Action

  • Proposed conditional application: Keep abstract/title/author features separate from in-degree, out-degree, neighbor degree and node/word embedding vectors for downstream pipelines.

Check

  • Proposed check: Unit-check each field and missing value, then ablate graph and text views under an edge-availability-respecting split.

Limits

  • Published transformer implementation is not execution proof; downstream fitting and measured benefits are absent.

Find source links and value grades in the lesson atlas

Verify text-row ordering and deduplicate before chasing preprocessing

Conditions

  • text pipelines reorder batches and contain duplicate examples

Action

  • Preserve sample IDs through collation and audit duplicate groups before training and evaluating.

Check

  • Join predictions by ID and validate duplicate-group isolation against a fixed baseline.

Limits

  • Reported deduplication gain lacks split details.
  • Token removal and sequence-length experiments are separate preprocessing choices.

Find source links and value grades in the lesson atlas