Adjusted risk ratios: log-binomial and modified Poisson
When the outcome is common an odds ratio overstates the effect, but log-binomial regression — the obvious way to estimate an adjusted risk ratio — very often refuses to run. This page walks the sequence in the order it actually happens: it converges, it fails, it converges with starting values, and then modified Poisson does the job; plus what the robust standard error is really fixing here.
Why this page exists
The logistic regression page concluded that cohort studies and trials have denominators, so a risk ratio can be computed directly, and reporting an OR instead means taking on an extra assumption. It also mentioned, in passing, that an adjusted RR can be had from log-binomial regression or from Poisson regression with robust standard errors.
That sounds like changing one family argument. It is not — log-binomial very often refuses to run, and the way it fails is easy to mistake for your own error the first time you meet it.
So this page does not follow the textbook order (principle, then method, then a footnote saying it sometimes fails to converge). It follows the order you will meet on your own machine: something works, then something breaks, then it is fixed, then a different route is taken. The failure in the middle is one of the points of the page, not an aside.
The example on this page
The same MASS::birthwt: 189 mothers, 59 low-birth-weight babies, a prevalence of 31.2%. Among smokers 30 of 74; among non-smokers 29 of 115 — so the risk in the unexposed group is 25.2%, a number the whole page keeps coming back to.
The adjustment set is age, mother’s weight and race, overlapping with the logistic regression page so the estimates can be compared side by side.
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"))
ADJ <- low ~ smoke_f + age + lwt + race_f
# Step 1: univariable log-binomial, converges straight away
fit_uni <- glm(low ~ smoke_f, data = bw, family = binomial(link = "log"))
exp(coef(fit_uni))["smoke_fYes"]
# Step 2: add covariates and the default starting values fail
# try() is only here so the steps below can run; called directly this
# aborts, which is the point of this step.
try(glm(ADJ, data = bw, family = binomial(link = "log")))
# Step 3: supply starting values. log(0.3) says "baseline risk near 30%",
# every slope starts at 0
fit_lb <- glm(ADJ, data = bw, family = binomial(link = "log"),
start = c(log(0.3), rep(0, 5)))
exp(cbind(RR = coef(fit_lb), confint.default(fit_lb)))
max(fitted(fit_lb)) # after convergence, is anything pinned near 1?
# Step 4: modified Poisson. The sandwich estimator, written out (HC0)
fit_p <- glm(ADJ, data = bw, family = poisson)
robust_vcov <- function(model) {
X <- model.matrix(model)
r <- model$y - fitted(model)
bread <- solve(crossprod(X, X * model$weights)) # (X'WX)^-1
bread %*% crossprod(X, X * r^2) %*% bread # the meat, in between
}
se <- sqrt(diag(robust_vcov(fit_p)))
cbind(RR = exp(coef(fit_p)),
lcl = exp(coef(fit_p) - 1.96 * se),
ucl = exp(coef(fit_p) + 1.96 * se))
summary(fit_p)$coefficients[, 2] # for contrast: the naive Poisson SEs
# Step 5: the logistic OR, for comparison
exp(coef(glm(ADJ, data = bw, family = binomial)))["smoke_fYes"]Verified with R 4.6.0 and MASS 7.3.65. The sandwich package gives robust variances in one line; this site installs no extra packages, so the sandwich estimator is written out below.
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"})
ADJ = "low ~ C(smoke_f) + age + lwt + C(race_f)"
# log-binomial: statsmodels can fail to converge too; pass start_params
lb = smf.glm(ADJ, data=bw,
family=sm.families.Binomial(sm.families.links.Log())
).fit(start_params=np.r_[np.log(0.3), np.zeros(5)])
print(np.exp(lb.params), np.exp(lb.conf_int()))
# modified Poisson: robust standard errors are one argument
mp_ = smf.glm(ADJ, data=bw, family=sm.families.Poisson()).fit(cov_type="HC0")
print(np.exp(mp_.params), np.exp(mp_.conf_int()))
# the logistic OR, for comparison
print(np.exp(smf.logit(ADJ, data=bw).fit().params))In statsmodels, GLM takes cov_type='HC0' and gives robust standard errors directly — no need to write the estimator out.
Step 1: the univariable log-binomial converges
Only one thing changes in the model — the link function goes from logit to log:
so is now the risk ratio directly, with no conversion needed. With smoking as the only predictor, R does not complain at all:
- RR 1.608 (95% CI 1.06–2.44, p 0.026), converged in 6 iterations.
Step 2: adding covariates fails on the default
Add age, mother’s weight and race, write it exactly the same way, and R stops with an error:
| Locale | Message |
|---|---|
| English | no valid set of coefficients has been found: please supply starting values |
The machine that produced these figures (zh_TW) | 找不到有效的係數:請提供初始值 |
The second row is not a different error. R translates its own messages according to the system locale, so the identical failure prints differently on a colleague’s machine. Two practical consequences: search the web with the English wording, because that is what the answers are indexed under; and when you paste an error into an issue or a message to someone, include the English version.
What the message means is that no valid set of starting coefficients could be found. The reason is that a log link does not constrain the fitted values to stay below 1: the inverse logit always lands between 0 and 1, whereas can exceed 1, and that is not a probability. The rule R uses to guess starting values produces, on this data, a set of coefficients that puts some fitted probabilities above 1, and the iteration has nowhere valid to begin.
Step 3: starting values make it converge
Starting values only have to be legal; they do not have to be close to the answer. A generally useful set is: the intercept at the log of a rough baseline risk, and every slope at 0 — which says “assume no association to begin with, and a baseline risk of about this much”.
With that, it converges in 7 iterations:
- Adjusted RR 1.786 (95% CI 1.18–2.71, p 0.006)
Step 4: modified Poisson, with nothing to tune
The other route is less work: use the wrong likelihood on purpose. Fit the 0/1 outcome with Poisson regression and a log link, and the point estimate is still a consistent estimate of the risk ratio; then repair the variance with a robust (sandwich) standard error. That combination is what the literature calls modified Poisson (Zou 2004).
It runs with nothing tuned at all:
- Adjusted RR 1.915 (95% CI 1.26–2.92, p 0.002), 5 iterations, no starting values supplied.
What the robust standard error is actually fixing
The sandwich estimator is two slices of bread around a filling:
The bread is the information matrix under the model’s assumption; the filling is the squared residuals actually observed. So what it does is replace the assumption “the variance is whatever the model says” with “the variance is whatever the data actually showed”.
Step 5: the logistic OR, for contrast
Same data, same covariates, logistic instead:
- Adjusted OR 2.870 (95% CI 1.36–6.05, p 0.006)
figures/scripts/B2-09-adjusted-rr.R| Model | Quantity | Estimate | 95% CI | p |
|---|---|---|---|---|
| log-binomial, smoking only | RR | 1.608 | 1.06–2.44 | 0.026 |
| log-binomial, adjusted | RR | 1.786 | 1.18–2.71 | 0.006 |
| modified Poisson, adjusted | RR | 1.915 | 1.26–2.92 | 0.002 |
| logistic, adjusted | OR | 2.870 | 1.36–6.05 | 0.006 |
At the 31.2% prevalence of this dataset, the OR is about 61% larger than the log-binomial RR (and about 50% larger than the modified Poisson RR).
Where the rare-disease assumption stops working
figures/scripts/B2-09-adjusted-rr.RThe line in the right-hand panel is exactly straight, not coincidentally straight-looking. Rearranging the conversion makes that obvious: given an OR and an unexposed risk ,
In other words, how much bigger the OR is than the RR is proportional to the unexposed risk, with as the constant of proportionality. There is therefore no baseline risk that is a “safe threshold”: the gap grows linearly from zero, it is just that it grows slowly at the low end. As the gap goes to zero, and that limit is all the rare-disease assumption ever says.
Holding the adjusted OR fixed and varying the baseline risk:
| Risk in the unexposed group | RR this OR corresponds to | OR larger by |
|---|---|---|
| 1% | 2.818 | 2% |
| 5% | 2.625 | 9% |
| 10% | 2.418 | 19% |
| 20% | 2.089 | 37% |
| This dataset, 25.2% | 1.950 | 47% |
Which one to use
| Situation | What to do |
|---|---|
| Cohort study or trial, outcome not rare, an RR is wanted | Try log-binomial first; use modified Poisson if it will not converge |
| log-binomial converges but fitted probabilities sit near 1 | Boundary solution — switch to modified Poisson or report a risk difference |
| Case-control study | Only an OR is available; the denominators were chosen by the investigator. See case-control studies |
| A population-level quantity is wanted, or an NNT | Report a marginal risk difference — see marginal estimates and G-computation |
| The outcome really is rare (unexposed risk far below 10%) | Reading the logistic OR as an RR introduces little error, but still label it an OR |
Common mistakes
| Mistake | Why it is wrong |
|---|---|
| Falling back to an OR when log-binomial errors, and saying the RR could not be estimated | Starting values or modified Poisson will both get you one |
| Treating convergence as the end of the check | Look at the largest fitted probability; sitting near 1 is the sign of a boundary solution |
| Reporting a binary-outcome RR with naive Poisson standard errors | A binary outcome has less variance than Poisson assumes, so the interval comes out too wide |
| Applying “without robust standard errors the interval is too narrow” to this page | That statement is about clustered data; here the direction is reversed |
| Treating an OR as a bigger RR | They are different quantities, not different readings on one ruler |
| Judging the OR-as-RR approximation by the overall prevalence | The criterion is the risk in the unexposed group |
| Saying “below 10% they are equal” | It is an approximation, not an identity, and the gap there is already appreciable |
| Reporting a point estimate as “risk reduced by N%” when the interval crosses 1 | Write that no difference was detected, and give the interval |
| Comparing an RR from one paper with an OR from another | Different quantities, and different adjustment sets as well |
| Reading a modified Poisson estimate as an odds ratio | With a log link it estimates a risk ratio |
Further reading
- Logistic regression and the odds ratio — where the OR comes from, and why it necessarily overstates when the outcome is common
- Marginal estimates and G-computation — a risk difference and an NNT without changing model
- Poisson and negative binomial regression — the problem Poisson regression was built for in the first place
- Clustered and repeated measures data — the other use of robust standard errors, pulling the other way
Reproducing every number on this page
/opt/homebrew/bin/Rscript figures/scripts/B2-09-adjusted-rr.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.
Same patients, same covariates: logistic regression gives an adjusted odds ratio of 2.87. Can that be read as the adjusted risk ratio?
Show the answer and why
Correct answer: No - the log-binomial adjusted risk ratio is 1.79, and the odds ratio sits much further from one
The log-binomial adjusted risk ratio is 1.79 against an odds ratio of 2.87 - reading 2.87 as "risk multiplied by 2.87" overstates the effect by roughly half. Low birth weight runs at close to thirty percent in these data, which is nowhere near rare, and OR and RR converge only when the outcome is rare. 1.91 is the modified Poisson adjusted risk ratio: it estimates the same quantity as log-binomial, and the small gap between them comes from model form rather than definition, so it is not a second odds ratio.
If modified Poisson skips the sandwich estimator and takes standard errors straight from the Poisson formula on the log scale, which way do they err?
Show the answer and why
Correct answer: Too large - the naive value is 0.285, because Poisson assumes variance equals the mean and so overstates it for a binary outcome
The naive 0.285 is clearly larger than the HC0 robust standard error of 0.215. Poisson assumes the variance equals the mean, while a binary outcome has variance p(1-p), which is always smaller - so the naive standard error is bound to run large and the interval wide. That direction is conservative, and conservative is not the same as correct: an over-wide interval turns a real effect into a non-significant one just as readily, and nothing in the output flags it. 0.218 is HC1, the small-sample-corrected version, barely different from HC0.
The log-binomial model fails from its default starting values and runs only when they are supplied by hand. Which number best explains why it converges at all?
Show the answer and why
Correct answer: The largest fitted probability is 0.795, still under one
What matters is that the largest fitted probability, 0.795, has not crossed one. A log link places no ceiling on fitted probabilities, and once any row is pushed past one the likelihood is undefined and IRLS has no legal starting point - which is exactly why the default start fails here. 0.405 is the crude risk among smokers and 0.312 the overall prevalence: those describe the observed data, whereas convergence turns on how high the model pushes at the edges of covariate space, which can sit far above any crude risk.
Chapters that use this method
Sources and licences
This page is original writing