Logistic regression and odds ratios

statsmodels · Essentials · 11 min

Binary outcomes (converted, churned, defaulted) are everywhere, and the coefficients need special interpretation.

For a 0/1 outcome, smf.logit("converted ~ price + C(channel)", data=df).fit() fits a logistic regression by maximum likelihood. Unlike scikit-learn's LogisticRegression, there is no regularisation by default, so coefficients are the classical maximum likelihood estimates with valid standard errors and p-values.

Coefficients are on the log-odds scale: a one-unit increase in x adds coef to the log-odds of y = 1. Exponentiate them for odds ratios: np.exp(res.params). An odds ratio of 1.5 means the odds are 50% higher per unit; below 1 means lower. Exponentiate the confidence interval bounds too: np.exp(res.conf_int()).

Odds ratios are not probability ratios. For effects on the probability scale use average marginal effects: res.get_margeff().summary_frame() reports how much the predicted probability changes per unit of each predictor, averaged over the data.

Fit statistics differ from OLS: res.prsquared is McFadden's pseudo R², res.llr_pvalue tests the model against an intercept-only model. res.predict(new) returns probabilities.

The fitter prints an optimisation log; pass disp=0 to silence it. If a predictor perfectly separates the classes, the maximum likelihood estimate does not exist and you get huge coefficients or a PerfectSeparationWarning.

Example

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

rng = np.random.default_rng(4)
n = 2000
df = pd.DataFrame({"discount": rng.integers(0, 2, n), "visits": rng.poisson(3, n)})
logit_p = -2 + 0.7 * df["discount"] + 0.3 * df["visits"]
df["bought"] = rng.binomial(1, 1 / (1 + np.exp(-logit_p)))

res = smf.logit("bought ~ discount + visits", data=df).fit(disp=0)
print(res.params.round(2).to_dict())     # {'Intercept': -1.97, 'discount': 0.84, 'visits': 0.29}
print(np.exp(res.params).round(2).to_dict())
# {'Intercept': 0.14, 'discount': 2.32, 'visits': 1.33}  (odds ratios)

me = res.get_margeff().summary_frame()
print(me["dy/dx"].round(3).to_dict())    # {'discount': 0.175, 'visits': 0.06}
print(res.predict(pd.DataFrame({"discount": [1], "visits": [5]})).round(3).tolist())  # [0.578]

Remember

  • Logit coefficients are changes in log-odds; np.exp gives odds ratios
  • Use get_margeff() for effects on the probability scale
  • statsmodels' Logit is unregularised, unlike scikit-learn's default

Watch out for

  • Describing an odds ratio of 2 as 'twice as likely'.

Check yourself

  1. A logit coefficient for 'discount' is 0.69. What is the odds ratio?

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

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