Categorical features and missing values

LightGBM · Essentials · 10 min

Native handling is faster and often more accurate than one-hot encoding or imputing.

Categorical features. LightGBM splits categorical features natively: for a categorical column it sorts categories by their gradient statistics and finds the best partition into two groups, which is much better than one-hot encoding for high-cardinality features. Two ways to declare them:

  • pandas columns with category dtype are detected automatically (categorical_feature="auto"),
  • or pass categorical_feature=["city", ...] (names or indices) to fit / lgb.Dataset; those columns must contain non-negative integers.

Category codes are stored with the model, and at prediction time pandas categories are mapped consistently as long as the column has the same categories; re-use the training CategoricalDtype to be safe. Controls against overfitting rare categories: min_data_per_group (default 100), cat_smooth, cat_l2 and max_cat_threshold. With very many rare categories, consider grouping them first.

Missing values. NaN is treated as missing by default (use_missing=True): each split learns which side missing values go to. Zeros can also be treated as missing with zero_as_missing=True, useful for sparse data where absent means unknown. Don't impute by reflex: the learned direction can use missingness as information.

Object (string) columns are rejected; convert with astype("category").

Example

import lightgbm as lgb
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
n = 5000
df = pd.DataFrame({"store": rng.choice([f"s{i}" for i in range(50)], n),
                   "temp": rng.normal(20, 8, n)})
effect = {f"s{i}": rng.normal(0, 10) for i in range(50)}
y = df["store"].map(effect) + 0.5 * df["temp"] + rng.normal(0, 2, n)
df.loc[rng.random(n) < 0.1, "temp"] = np.nan          # 10% missing
df["store"] = df["store"].astype("category")           # native categorical

model = lgb.LGBMRegressor(n_estimators=300, learning_rate=0.05, verbose=-1).fit(df, y)
new = pd.DataFrame({"store": pd.Categorical(["s3", "s7"], categories=df["store"].cat.categories),
                    "temp": [25.0, np.nan]})
print(model.predict(new).round(1))                    # [6.1 1.2]
print(round(effect["s3"] + 0.5 * 25, 1))              # 5.7 (true mean for the first row)

Remember

  • pandas category columns are used as native categorical features
  • Each split learns where NaN goes; avoid imputing by default
  • Keep the same categories at prediction time

Watch out for

  • Passing integer-coded categories without declaring them, so they are split as ordered numbers.

Check yourself

  1. How should you pass a 'city' column with 500 distinct names to LightGBM?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: lightgbm.readthedocs.io/en/stable/Advanced-Topics.html