Monotonic and interaction constraints

XGBoost · Advanced · 11 min

Constraints encode domain knowledge, improve generalisation and make models acceptable to regulators.

Sometimes you know how a feature should act: risk should never decrease as debt increases; price should not fall as size grows. An unconstrained model may still learn small reversals from noise, which looks wrong to users and can generalise badly.

Monotonic constraints: monotone_constraints gives each feature 1 (non-decreasing), -1 (non-increasing) or 0 (unconstrained). With a DataFrame you can pass a dict: monotone_constraints={"debt": 1, "income": -1}. XGBoost then only accepts splits whose child values respect the order, and propagates bounds down the tree. The constraint holds for every prediction, holding the other features fixed.

Interaction constraints: interaction_constraints=[["age", "income"], ["region", "channel"]] lets features interact only within each group: a tree branch that split on age may later split only on age or income. That makes the model closer to an additive structure that is easier to explain, and can reduce overfitting.

Constraints usually cost a little training accuracy and can gain test accuracy when they reflect reality. Verify them with partial dependence plots, or by sweeping one feature over a grid on many rows and checking that predictions are monotone.

Both work with the hist tree method used by default, and with the native and scikit-learn APIs.

Example

import numpy as np
import pandas as pd
from xgboost import XGBRegressor

rng = np.random.default_rng(2)
n = 2000
X = pd.DataFrame({"size": rng.uniform(30, 200, n), "age": rng.uniform(0, 50, n)})
y = 2.0 * X["size"] - 0.5 * X["age"] + rng.normal(0, 40, n)     # noisy

free = XGBRegressor(n_estimators=300, max_depth=5).fit(X, y)
mono = XGBRegressor(n_estimators=300, max_depth=5,
                    monotone_constraints={"size": 1, "age": -1}).fit(X, y)

grid = pd.DataFrame({"size": np.linspace(30, 200, 200), "age": 10.0})
for m in (free, mono):
    pred = m.predict(grid)
    print((np.diff(pred) < -1e-6).sum())   # number of decreases as size grows
# 76 (unconstrained model wiggles down and up)
# 0  (monotone in size)

Remember

  • monotone_constraints with 1/-1/0 per feature (dict for DataFrames)
  • interaction_constraints lists groups of features allowed to interact
  • Verify constraints with a sweep or partial dependence

Watch out for

  • Constraining a feature whose true relationship is not monotonic, which costs accuracy.

Check yourself

  1. What does monotone_constraints={"debt": 1} guarantee?

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

Official docs: xgboost.readthedocs.io/en/stable/tutorials/monotonic.html

Builds on

Unlocks

Nothing builds on this yet.