Logistic regression and the odds ratio
Which ratio an OR actually is, why it necessarily overstates the effect when the outcome is not rare, how an adjusted OR differs from a crude one for reasons other than confounding, what separation looks like and how to fix it, and where the "at least ten events per variable" rule came from and why it is now disputed.
What this model is for
The second large family of clinical outcomes is binary: low birth weight or not, hospital-acquired infection or not, readmission within thirty days or not, a postoperative complication or not.
Fitting a 0/1 outcome with linear regression breaks in two places: fitted values leave the 0–1 range (a negative probability means nothing), and the residual variance changes with the fitted probability (the variance of a 0/1 variable is , which is not constant by construction).
Logistic regression fixes this by changing the scale first. Turn the probability into the odds , then take the logarithm; the range opens from out to the whole real line, and the linear model is built there:
So each means “with the other variables held constant, a one-unit increase in raises the log-odds by ”, and exponentiating gives , the odds ratio (OR). This model produces an OR rather than an RR not because anyone chose it, but because of how the model is built.
The example on this page
We keep MASS::birthwt from the linear regression page and switch the outcome to the binary low: whether the newborn weighed under 2500 g. Of 189 mothers, 59 had a low-birth-weight baby, a prevalence of 31.2%.
That prevalence is the reason this example was chosen. It is nowhere near rare, so the OR and the RR will visibly part company in the same table — which is what the next section is about.
library(MASS)
data(birthwt, package = "MASS")
bw <- birthwt
bw$race_f <- factor(bw$race, levels = 1:3, labels = c("White", "Black", "Other"))
bw$smoke_f <- factor(bw$smoke, levels = 0:1, labels = c("No", "Yes"))
bw$ptl_any <- as.integer(bw$ptl > 0)
# Crude OR: one predictor
fit_c <- glm(low ~ smoke_f, data = bw, family = binomial)
exp(cbind(OR = coef(fit_c), confint.default(fit_c)))
# The RR from the same 2x2 -- you compute it yourself, glm will not give it
tb <- table(bw$smoke_f, bw$low)
(tb[2, 2] / sum(tb[2, ])) / (tb[1, 2] / sum(tb[1, ]))
# Adjusted OR
fit <- glm(low ~ smoke_f + age + lwt + race_f + ht + ui + ptl_any,
data = bw, family = binomial)
summary(fit)
exp(cbind(OR = coef(fit), confint.default(fit))) # Wald, the format papers use
# What separation looks like: treat ptl as a four-level factor
summary(glm(low ~ smoke_f + factor(ptl), data = bw, family = binomial))Verified with R 4.6.0 and MASS 7.3.65. For a glm, confint() returns profile-likelihood intervals; the Wald intervals papers usually report come from confint.default().
import numpy as np
import statsmodels.api as sm
import statsmodels.formula.api as smf
bw = sm.datasets.get_rdataset("birthwt", "MASS").data
bw["race_f"] = bw["race"].map({1: "White", 2: "Black", 3: "Other"})
bw["smoke_f"] = bw["smoke"].map({0: "No", 1: "Yes"})
bw["ptl_any"] = (bw["ptl"] > 0).astype(int)
fit = smf.logit(
"low ~ C(smoke_f) + age + lwt + C(race_f) + ht + ui + ptl_any", data=bw).fit()
print(fit.summary())
print(np.exp(fit.params)) # OR
print(np.exp(fit.conf_int())) # 95% CI, Wald
# On separation statsmodels raises PerfectSeparationWarning, which beats R's silencestatsmodels Logit and GLM(family=Binomial()) give the same fit; the Logit summary is the one that looks closest to R's summary().
Crude OR and RR: one 2×2 table, two answers
Before fitting anything, do it by hand. Smoking against low birth weight:
| Low birth weight | Normal | Total | Risk | Odds | |
|---|---|---|---|---|---|
| Smoked | 30 | 44 | 74 | 40.5% | 0.682 |
| Did not smoke | 29 | 86 | 115 | 25.2% | 0.337 |
- Risk ratio (RR) = one risk divided by the other = 1.608 (1.06–2.44)
- Odds ratio (OR) = one odds divided by the other = 2.022 (1.08–3.78)
Same table, same people, and the OR comes out about 26% larger than the RR. Read that OR aloud as “the risk doubled” and you have overstated the effect. What actually happened is that the risk went from 25.2% to 40.5%.
figures/scripts/B2-02-logistic.RThis is algebra, not coincidence. Given an OR and an unexposed risk , the exposed risk is
Holding this page’s OR fixed and varying the baseline risk:
| Risk in the unexposed | RR implied by this OR |
|---|---|
| 1% | 2.001 |
| 5% | 1.924 |
| 20% | 1.679 |
| 40% | 1.435 |
The adjusted OR
figures/scripts/B2-02-logistic.R| Variable | Adjusted OR | 95% CI | p |
|---|---|---|---|
| Smoking during pregnancy | 2.33 | 1.048–5.187 | 0.038 |
| Age, per year | 0.96 | 0.894–1.037 | 0.318 |
| Mother's weight, per lb | 0.99 | 0.972–0.999 | 0.034 |
| Race: Black vs White | 3.36 | 1.184–9.548 | 0.023 |
| Race: Other vs White | 2.23 | 0.928–5.382 | 0.073 |
| History of hypertension | 6.29 | 1.585–24.954 | 0.009 |
| Uterine irritability | 2.04 | 0.822–5.047 | 0.125 |
| Previous preterm labour | 3.39 | 1.369–8.408 | 0.008 |
The OR for smoking moves from a crude 2.022 to an adjusted 2.331.
Most textbooks will tell you the difference is confounding. Half of it is; the other half is not, and the other half is almost never mentioned:
The remaining columns read exactly as in linear regression: an OR on a continuous variable must carry its unit (the OR of 0.963 for age is per year); a variable whose confidence interval crosses 1 should be written up as “this study did not detect an association”, not as “no association”; and the global model test (likelihood ratio 37.8, df = 8, p < 0.001) asks whether all the coefficients are simultaneously 0.
Separation
Sometimes glm() returns a dramatic-looking coefficient attached to an absurd standard error. That is usually not strong evidence; it is separation — some variable splits the outcome perfectly, or nearly perfectly.
This dataset contains a ready-made example, no fabrication needed. Treat the number of previous preterm labours, ptl, as a four-level factor:
| Previous preterm labours | Normal weight | Low birth weight |
|---|---|---|
| 0 | 118 | 41 |
| 1 | 8 | 16 |
| 2 | 3 | 2 |
| 3 | 1 | 0 |
The last row contains 1 mother, and her baby was not low birth weight. That cell is 0, so within these data the level “three previous preterm labours” is synonymous with “low birth weight cannot happen”. The model responds like this:
| Term | Coefficient | SE | p |
|---|---|---|---|
| (Intercept) | -1.28 | 0.23 | < 0.001 |
| smoke_fYes | 0.58 | 0.34 | 0.087 |
| ptl_f1 | 1.65 | 0.48 | < 0.001 |
| ptl_f2 | 0.52 | 0.94 | 0.582 |
| ptl_f3 | -13.86 | 882.74 | 0.987 |
EPV: at least ten events per variable
How many variables a logistic model can carry depends on the number of events, not the sample size. The model on this page has 59 events and 8 parameters, so EPV (events per variable) = 7.38.
Common misuses
| Misuse | Why it is wrong |
|---|---|
| Reading an OR as an RR when the outcome is not rare | The OR is always further from 1, and the gap widens as the outcome becomes more common |
| Reporting only an OR from a cohort study or RCT that has denominators | An RR or risk difference is available directly; the OR demands an extra assumption |
| Attributing the whole crude-to-adjusted change in an OR to confounding | The OR is non-collapsible; a conditional OR sits further from 1 by construction |
| Comparing the size of ORs between two papers | Different adjustment sets mean the two ORs define different contrasts |
| Using a model’s OR to compute an NNT or a population-level impact | That needs a marginal estimate (standardisation / G-computation), not a conditional OR |
| Reporting an OR on a continuous variable without its unit | Per year and per decade are entirely different numbers |
| Reporting a huge OR produced by separation | The estimate does not exist; collapse categories or use Firth |
Dismissing fitted probabilities numerically 0 or 1 as noise | It is usually the only warning of separation you will get |
| Loading many covariates into a model with few events | Overfitting; neither the coefficients nor the intervals can be trusted |
| Choosing variables by looking at significance first | Post-selection p-values and confidence intervals are invalid |
| Ranking variables by “importance” using the size of their ORs | The scale depends on each variable’s unit and distribution, not on importance |
| Fitting a matched case-control study with ordinary logistic regression | Use conditional logistic regression; see case-control studies |
| Describing an effect size when the confidence interval crosses 1 | Write “no significant association was detected” and give the interval |
Reproducing every number on this page
/opt/homebrew/bin/Rscript figures/scripts/B2-02-logistic.RRead the figure
The answer comes from the same statistical output that produced this page's figures, not from a number typed in beside them.
Low birth weight is not rare in these data, and the crude odds ratio is 2.02. Can you say smoking doubles the risk?
Show the answer and why
Correct answer: No - the risk ratio is 1.61, and when an outcome is common the OR sits further from one than the RR
The risk ratio is 1.61 against an odds ratio of 2.02, a wide gap because this outcome is anything but rare. The two converge only when events are uncommon; the higher the prevalence the further they separate, and the OR is always the one further from one. Reading an OR as "risk multiplied by" overstates the effect, which is how abstracts routinely phrase it. 3.78 is the OR's upper confidence bound, describing uncertainty rather than a second effect measure.
After adjusting for age, maternal weight and race, smoking's odds ratio moves away from the crude 2.02. What has adjustment done?
Show the answer and why
Correct answer: It becomes 2.33 - adjustment can move an estimate in any direction, and here it moves outward
The adjusted OR is 2.33, further from one than the crude 2.02. "Adjustment pulls estimates towards the null" is a common intuition and it is simply not true: the direction depends on how each covariate relates to the exposure and to the outcome, and an estimate can move outward, inward, or across. 3.36 is the race row of the same model, not smoking's. Faced with a crude and an adjusted figure, the question is which variables entered and why, not which way it should have gone.
30 of the 74 smokers had a low-birth-weight baby. What is the risk in the smoking group?
Show the answer and why
Correct answer: 0.405 - denominator is everyone in that group
Risk divides by everyone, giving 0.405. 0.682 is the same group's odds, whose denominator excludes those who had the event, which is why odds always exceed risk. When events are common the two diverge widely - and logistic regression hands you the odds side, which is why talking about risk from a logistic model requires converting first. 0.252 is the non-smokers' risk, read off the wrong row.
Chapters that use this method
Watch next
StatQuest: Logistic Regression
Logistic Regression Details Pt1: Coefficients
醫學統計 EP14 羅吉斯迴歸
醫學統計 EP15 RR vs OR
【Hands-on】L9 R: Logistic RegressionSources and licences
This page is original writing