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.expgives 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'.