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 36: Categorical Embeddings

Try the decision first

How large can the share of unseen categories get before an integer label code is worse than dropping the column, and what does the unknown rule cost a target encoding?

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 large can the share of unseen categories get before an integer label code is worse than dropping the column, and what does the unknown rule cost a target encoding?

Seen-category rows reward every encoding. The leaderboard may contain new products, users or zip codes, and then the unknown rule decides the score. The chapter asks for a documented dictionary and an explicit unknown code for this reason.

Constructed data; frequency is strong here because unseen categories are rare by construction. The label code falls below the no-feature baseline because of where -1 lands in a popularity-ordered code; in random order it stays above it, and the crossover share moves with the random draw.

The experiment

A constructed binary task with 1,500 Zipf-popular training categories, 10,000 training rows, two numeric features and a category effect plus a rarity effect (rare categories have higher event rates). 6,000 test rows are drawn from the training categories, except for the control's share, which comes from 1,500 new categories. Five gradient-boosted models are compared by pooled AUC: no category feature, an integer label code numbered in order of first appearance, as pandas factorize numbers them (unknown = -1), frequency counts (unknown = 0), nested smoothed target means plus frequency with unknown = the training prior, and the same with unknown = 0. A sixth model numbers the same categories in random order, to show what the label code's result depends on. Results are means over 6 independent worlds.

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 kaggle_companion.activities._common import clean

SEED = 36
REPLICATES = 6                       # independent worlds per control value; results are means over them
SEEN, NEW = 1500, 1500               # categories that can appear in training, and a pool of new ones for test
N_TRAIN, N_TEST = 10_000, 6_000
M = 10.0                             # smoothing strength of the chapter's smoothed mean


def make_world(rng):
    """Zipf popularity for the training categories; a category effect plus a rarity effect (rare categories run hotter)."""
    p = 1 / np.arange(1, SEEN + 1) ** 0.9
    p /= p.sum()
    log_p = np.concatenate([np.log(p), np.full(NEW, np.log(p[-1]))])        # new categories are tail-rare
    rarity = -0.45 * (log_p - np.log(p).mean()) / np.log(p).std()
    return p, rng.normal(0, 0.7, SEEN + NEW) + rarity


def draw_rows(categories, effect, rng):
    x = rng.normal(size=(len(categories), 2))
    logit = -1.2 + 0.7 * x[:, 0] - 0.3 * x[:, 1] + effect[categories]
    return x, (rng.random(len(categories)) < 1 / (1 + np.exp(-logit))).astype(int)


def smoothed_means(cat, y, prior):
    """e_c = (s_c + m*mu) / (n_c + m). A category with n_c = 0 gets exactly the prior mu."""
    s = np.bincount(cat, weights=y, minlength=SEEN + NEW)
    n = np.bincount(cat, minlength=SEEN + NEW)
    return (s + M * prior) / (n + M)


def one_world(unseen_share, seed):
    rng = np.random.default_rng(seed)
    p, effect = make_world(rng)
    cat_tr = rng.choice(SEEN, N_TRAIN, p=p)
    x_tr, y_tr = draw_rows(cat_tr, effect, rng)
    n_new = int(round(unseen_share * N_TEST))
    cat_te = np.concatenate([rng.choice(SEEN, N_TEST - n_new, p=p), SEEN + rng.integers(0, NEW, n_new)])
    x_te, y_te = draw_rows(cat_te, effect, rng)

    known = np.zeros(SEEN + NEW, bool)
    known[cat_tr] = True
    unseen = ~known[cat_te]                                  # unseen means: absent from the training dictionary
    counts = np.bincount(cat_tr, minlength=SEEN + NEW)       # frequency encoding from training rows only; unseen -> 0
    codes = -np.ones(SEEN + NEW, int)                        # label codes in order of first appearance; unseen -> -1
    first = np.unique(cat_tr, return_index=True)
    order = first[0][np.argsort(first[1])]                   # in a shuffled table, first appearance follows popularity
    codes[order] = np.arange(known.sum())
    random_codes = -np.ones(SEEN + NEW, int)                 # the same categories numbered in random order; unseen -> -1
    random_codes[np.random.default_rng([seed, 1]).permutation(order)] = np.arange(known.sum())

    prior = y_tr.mean()
    te_train = np.zeros(N_TRAIN)                             # nested: each training row is encoded from the other folds
    for fit, out in KFold(5, shuffle=True, random_state=0).split(cat_tr):
        te_train[out] = smoothed_means(cat_tr[fit], y_tr[fit], y_tr[fit].mean())[cat_tr[out]]
    full_map = smoothed_means(cat_tr, y_tr, prior)
    te_known = np.where(known[cat_te], full_map[cat_te], prior)    # the documented rule: unknown -> training prior
    te_zero = np.where(known[cat_te], full_map[cat_te], 0.0)       # the shortcut: unknown -> 0

    arms = {                                                 # (training features, test features)
        "none": (x_tr, x_te),
        "label": (np.c_[x_tr, codes[cat_tr]], np.c_[x_te, codes[cat_te]]),
        "label_random": (np.c_[x_tr, random_codes[cat_tr]], np.c_[x_te, random_codes[cat_te]]),
        "frequency": (np.c_[x_tr, counts[cat_tr]], np.c_[x_te, counts[cat_te]]),
        "target_prior": (np.c_[x_tr, te_train, counts[cat_tr]], np.c_[x_te, te_known, counts[cat_te]]),
        "target_zero": (np.c_[x_tr, te_train, counts[cat_tr]], np.c_[x_te, te_zero, counts[cat_te]]),
    }
    scores = {}
    for name, (a, b) in arms.items():
        model = HistGradientBoostingClassifier(max_iter=80, max_leaf_nodes=15, random_state=0).fit(a, y_tr)
        pred = model.predict_proba(b)[:, 1]
        scores[name] = [roc_auc_score(y_te, pred), roc_auc_score(y_te[~unseen], pred[~unseen]),
                        roc_auc_score(y_te[unseen], pred[unseen])]
    return scores, float(unseen.mean())


def run(unseen_share):
    worlds = [one_world(unseen_share, SEED * 100 + r) for r in range(REPLICATES)]
    names = list(worlds[0][0])
    mean = {k: np.mean([w[0][k] for w in worlds], axis=0) for k in names}
    below = {k: float(np.mean([w[0][k][0] < w[0]["none"][0] for w in worlds])) for k in names}
    return clean({
        "unseen_share": unseen_share,
        "measured_unseen_share": float(np.mean([w[1] for w in worlds])),
        "auc": {k: mean[k][0] for k in names}, "auc_seen": {k: mean[k][1] for k in names},
        "auc_unseen": {k: mean[k][2] for k in names}, "below_no_feature": below,
        "replicates": REPLICATES, "test_rows": N_TEST,
    })

Measure it across the control

The control is share of test rows from categories absent in training. Each value below runs the whole experiment from the generator up.

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

VALUES = [0, 0.1, 0.25, 0.5]
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    0.1   0.25    0.5
No categorical feature        0.650  0.660  0.646  0.646
Label code                    0.726  0.679  0.613  0.573
Frequency count               0.762  0.771  0.758  0.738
Target mean, unknown = prior  0.772  0.778  0.764  0.742
Target mean, unknown = 0      0.761  0.739  0.696  0.665
Unseen rows, label code       0.633  0.627  0.607  0.608
Label code, random order      0.721  0.729  0.718  0.698

Look at it

The figure shows the default setting, share of test rows from categories absent in training = 0.25.

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

DEFAULT = 0.25
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 25% of test rows from unseen categories (measured 27%), the label code scores 0.613 against 0.646 with no categorical feature, 0.613 - 0.646 = -0.033: worse than dropping the column. The target mean with the prior scores 0.764; with unknown = 0 it scores 0.696, so the unknown rule alone costs 0.764 - 0.696 = 0.068 AUC. Seen rows score 0.770 under both target rules. Best overall here: target mean with the prior, with frequency alone 0.006 behind the best target rule. Numbered in random order, the label code scores 0.718, so its result depends on where -1 lands.

Apply this to your competition

Book location: Chapter 36, Handling Unseen Categories at Inference.

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.

Filter sparse categorical identities by frequency and information

Conditions

  • recommendation tabular features contain many rare identities

Action

  • Proposed adaptation of source observation: Compare frequency/information-based feature removal with retained high-cardinality features, fitting selection within folds.

Check

  • Proposed verification: Measure held-out quality and category coverage and compare stable imputation with the reported random approach.

Limits

  • No threshold or isolated effect is supplied; random imputation can increase run variance.

Find source links and value grades in the lesson atlas

Keep categorical preprocessing simple for interaction networks

Conditions

  • high-cardinality categorical tables need neural interaction modeling

Action

  • Proposed conditional application: Compare label-encoded categorical inputs with explicit ordinal encodings using a reusable CTR-interaction framework.

Check

  • Proposed check: Compare simple and richer encodings with identical folds/architecture; test unseen-category handling.

Limits

  • Label codes need categorical/embedding semantics; overfitting claim is dataset-specific.

Find source links and value grades in the lesson atlas

Align one-hot feature columns across train and inference

Conditions

  • categorical tabular prediction creates separate encoded matrices

Action

  • Proposed adaptation of source observation: Proposed safe adaptation: learn the category vocabulary on training data and align test columns to it.

Check

  • Proposed verification: Check column identity/order and unseen-category behavior before prediction.

Limits

  • Source does not document alignment handling and its cleaning function description is incomplete.

Find source links and value grades in the lesson atlas

Persist categorical dictionaries across datasets

Conditions

  • categorical integer codes are supplied to a trained tabular model

Action

  • Proposed: reuse a training-fitted category mapping for validation and inference.

Check

  • Proposed: verify the same category has the same code across datasets and define unknown-category behavior.

Limits

  • Persistent dictionaries are proposed correction. Separate title and description tokenizers also share an embedding table in this script, requiring vocabulary identity auditing.

Find source links and value grades in the lesson atlas