Skip to course content
Free Python statistics course

Statistical Data Analytics with Python

Unit 10.03: Multiplies the odds, not the risk

Exponentiating a logistic coefficient gives an odds ratio, which almost everyone then reads as a risk ratio.

Multiplies the odds, not the risk

np.exp(model.params) converts log-odds coefficients into odds ratios, which are at least multiplicative and easier to talk about than log-odds. The interval must be exponentiated too — exponentiate the bounds of model.conf_int(), never the standard error.

An odds ratio of 1 means no association, so an interval crossing 1 is the logistic equivalent of a difference interval crossing 0.

The persistent error is reading 'the odds are 2.5 times higher' as '2.5 times as likely'. Those coincide only when the outcome is rare. When it is common the odds ratio is substantially further from 1 than the risk ratio, so the plain-English reading always overstates the effect.

This block reports the exponentiated model with intervals.

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

rng = np.random.default_rng(704)
n = 600
d = pd.DataFrame({"tenure": rng.uniform(0, 48, n),
                  "support": rng.binomial(1, 0.35, n)})
d["churn"] = rng.binomial(
    1, 1 / (1 + np.exp(-(1.4 - 0.07 * d.tenure + 0.9 * d.support))))

model = smf.logit("churn ~ tenure + support", data=d).fit(disp=False)

table = pd.DataFrame({
    "odds_ratio": np.exp(model.params),
    "ci_low": np.exp(model.conf_int()[0]),
    "ci_high": np.exp(model.conf_int()[1]),
    "p": model.pvalues,
}).round(4)
print(table.to_string())

s = table.loc["support"]
print(f"\nnp.exp() turns log-odds into ODDS RATIOS.")
print(f"Support contact: odds ratio {s.odds_ratio:.3f}"
      f"   95% CI [{s.ci_low:.3f}, {s.ci_high:.3f}]\n")
print(f"Read it as: contacting support multiplies the ODDS of churn by"
      f" {s.odds_ratio:.2f}.")
print(f"NOT: makes churn {s.odds_ratio:.2f} times as likely. The observed churn")
print(f"rate here is {d.churn.mean():.3f} -- far from rare, so the odds ratio")
print("overstates the risk ratio substantially.\n")
print("An odds ratio of 1 means no association; an interval crossing 1 is the")
print("equivalent of a difference interval crossing 0.")

Support contact has an odds ratio of 2.451 with a 95% interval from 1.672 to 3.592 — clearly above 1. Tenure's odds ratio is 0.9325 per month, below 1, so each additional month multiplies the odds of churn by 0.93. Crucially the observed churn rate here is 0.497 — nowhere near rare — so reading 2.45 as 'two and a half times as likely to churn' would substantially overstate the risk.

The mistake this prevents

The mistake is translating an odds ratio into 'times as likely'. With a common outcome that is a different and larger number.

Takeaway

Report odds ratios with their exponentiated intervals and say explicitly that they are odds ratios. Give the baseline rate so a reader can judge how far the odds ratio sits from the risk ratio.