Categories, contrasts and interactions

statsmodels · Practical · 12 min

Most real questions are about groups and about whether an effect differs between groups.

A categorical predictor with k levels becomes k - 1 dummy columns. The missing level is the reference, and each coefficient is the difference from it. By default the reference is the first level alphabetically, which may not be meaningful. Choose it with treatment coding: C(city, Treatment(reference="Rome")). Other contrasts are available, such as Sum (deviations from the grand mean).

An interaction lets the effect of one variable depend on another. price ~ area * C(city) fits a separate area slope per city: the area coefficient is the slope in the reference city, and area:C(city)[T.Oslo] is how much steeper or flatter it is in Oslo. With interactions, main effects alone are no longer "the effect of area"; they are conditional on the reference level (or on zero for numeric variables, which is why centring numeric variables helps).

To test whether a whole categorical factor or interaction matters, look at joint tests rather than individual p-values:

  • res.wald_test_terms() tests each term (all its dummies together),
  • sm.stats.anova_lm(res_small, res_big) compares nested OLS models with an F-test,
  • res.f_test("area:C(city)[T.Oslo] = 0") tests any linear hypothesis.

Too many interactions quickly overfit; add them because you have a reason to expect the effect to differ.

Example

import numpy as np
import pandas as pd
import statsmodels.api as sm
import statsmodels.formula.api as smf

rng = np.random.default_rng(5)
df = pd.DataFrame({"area": rng.uniform(40, 150, 300),
                   "city": rng.choice(["Oslo", "Rome", "Lima"], 300)})
slope = df["city"].map({"Lima": 1.0, "Oslo": 3.0, "Rome": 2.0})
df["price"] = 40 + slope * df["area"] + rng.normal(0, 15, 300)

f = "price ~ area * C(city, Treatment(reference='Rome'))"
res = smf.ols(f, data=df).fit()
print(res.params.filter(like="area").round(2).tolist())   # [2.04, -1.08, 1.0]
# Rome slope 2.0; Lima is 1.0 lower; Oslo is 1.0 higher

small = smf.ols("price ~ area + C(city)", data=df).fit()
print(sm.stats.anova_lm(small, res).round(3)[["F", "Pr(>F)"]].iloc[1].tolist())
# [448.065, 0.0]  (interaction clearly needed)

Remember

  • Each dummy coefficient is a difference from the reference level
  • Set the reference with C(x, Treatment(reference=...))
  • Test whole factors and interactions jointly with F or Wald tests

Watch out for

  • Interpreting a main effect as the overall effect when an interaction with it is in the model.

Check yourself

  1. In `y ~ x * C(group)` with reference 'A', what does the `x` coefficient represent?

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

Official docs: www.statsmodels.org/stable/contrasts.html