0%
DQDigital Qasas Courses/Medical Statistics with R and Python
Sign inAll courses
  • Why R and Python, and running them in your browser
  • Data frames, import, and tidy medical data
  • Tables and summaries
  • Variables, types, and the question behind the data
  • Displaying data: frequency distributions, histograms, and shape
  • Means, standard deviations and standard errors
  • The normal distribution
  • Confidence interval for a mean
  • Using P-values and confidence intervals
  • Comparison of two means
  • Analysis of variance
  • Linear regression and correlation
  • Multiple regression and diagnostics
  • Transformations
  • Risk, odds, and how to compare them
  • Proportions and the binomial distribution
  • Two proportions: risk ratio, odds ratio, risk difference, and confidence intervals
  • Chi-squared: 2x2 tables, larger tables, trend, and exact tests
  • Confounding and stratification
  • Logistic regression
  • Matched studies
  • Rates and the Poisson distribution
  • Comparing rates and Poisson regression
  • Standardization: direct, indirect, and the SMR
  • Kaplan-Meier survival curves and the log-rank test
  • Cox proportional-hazards regression
  • Likelihood and the generalized linear model
  • Building models, checking assumptions, and clustered data
  • Systematic reviews and meta-analysis
  • Bayesian statistics
  • Linking analysis to study design
  • Sample size and power
  • Measurement error and its consequences
  • Capstone: from a messy dataset to a reported result
9. Confounding and stratification

Confounding and stratification

Last updated 30 June 2026

You compare an exposure against an outcome, you get an odds ratio, and it looks convincing. Before you believe it, ask one question: is something else different between the exposed and the unexposed that also drives the outcome? If yes, your unadjusted estimate is part exposure effect and part the effect of that other thing. That other thing is a confounder, and this lesson is about spotting it, removing it by stratification, and combining the strata into one adjusted estimate with the Mantel-Haenszel method. It assumes you can already read a 2x2 table, an odds ratio, and a chi-squared P-value.

What a confounder is

A confounder is a third variable that gets tangled into an exposure-outcome comparison and distorts it. Write the exposure as E, the outcome (disease) as D, and the candidate confounder as C. For C to confound the E to D association it must meet three conditions at once.

  • It is associated with the exposure. C is distributed differently in the exposed and the unexposed groups.
  • It affects the outcome on its own. C is a risk factor for D even among people who are not exposed.
  • It does not sit on the causal path from E to D. E must not cause C which then causes D.

The third condition is the one people forget. If smoking during pregnancy lowers a baby's birth weight, and you are studying whether low income lowers birth weight, smoking is on the path (low income leads to more smoking leads to lower weight). Adjusting for it would wrongly erase part of the income effect you set out to measure. A confounder distorts; a mediator transmits. They are not the same, and you adjust only for the first.

Key term

A confounder is a variable associated with the exposure and independently linked to the outcome, but not a step in the causal chain between them. Age confounds many exposure-disease links because older people differ in exposure and also carry higher disease risk for separate reasons.

How confounding distorts the crude estimate

Take long-term NSAID use as the exposure and an upper gastrointestinal bleed as the outcome, in 800 arthritis patients followed for a year. The crude table ignores everything else.

NSAID useBleedNo bleedTotal
User50250300
Non-user30470500
Total80720800

The crude odds ratio is (50 × 470) / (250 × 30) = 23500 / 7500 = 3.13. NSAID users look to have more than triple the odds of a bleed. Now split the same 800 patients by age, because older patients both use NSAIDs more (for arthritis) and bleed more (for separate reasons). Age is associated with the exposure and is a risk factor for the outcome, so it is a candidate confounder.

Under 60BleedNo bleedTotal
NSAID user1090100
Non-user20380400
Total30470500
60 and overBleedNo bleedTotal
NSAID user40160200
Non-user1090100
Total50250300

Within the young the odds ratio is (10 × 380) / (90 × 20) = 2.11. Within the old it is (40 × 90) / (160 × 10) = 2.25. Both strata sit near 2.1, yet the crude estimate was 3.13. The gap is confounding. The exposed group is loaded with older patients (200 of 300 NSAID users are 60+, against 100 of 500 non-users), and older patients bleed more whatever they take. That extra age risk leaked into the crude odds ratio and inflated it.

Crude can mislead in any direction

Here confounding inflated the estimate. It can also shrink a real effect to nothing, or flip its sign so the exposure looks protective when it is harmful. You cannot guess the direction from the crude table alone. You have to stratify and look.

Stratify, then combine: the Mantel-Haenszel odds ratio

Stratification means splitting the data into subgroups (strata) that share one value of the confounder, so that within a stratum the confounder cannot vary and cannot distort. You compute a separate odds ratio in each stratum.

Here and are the diseased and healthy counts among the exposed in stratum i, and , the same among the unexposed. If the strata agree, you want one summary number that controls for C. The naive move is to average the stratum odds ratios. The better move is a weighted average that trusts the bigger strata more.

The Mantel-Haenszel method picks the weight , where is the stratum total. That weight is exactly the denominator of the stratum odds ratio, so the messy weighted average collapses into a clean ratio of two sums.

For the NSAID data, work out Q and R term by term. Young: and . Old: and . So Q = 7.6 + 12.0 = 19.6 and R = 3.6 + 5.33 = 8.93. The adjusted odds ratio is 19.6 / 8.93 = 2.19. The age-driven inflation is gone, and the summary lands between the two stratum values, as a weighted average must.

The same trick gives an adjusted risk ratio

If you would rather report a risk ratio, the Mantel-Haenszel risk ratio uses the same stratify-and-combine logic with exposure-group totals in place of the off-diagonal cells: , where and are the exposed and unexposed totals. Use it for cohort and trial data where a risk ratio is the natural measure.

Stack the two age strata and read the Mantel-Haenszel odds ratio, its confidence interval and test.

# each 2x2: rows = NSAID use, cols = bleed / no bleed
young <- matrix(c(10, 20, 90, 380), nrow = 2)
old   <- matrix(c(40, 10, 160, 90), nrow = 2)
arr   <- array(c(young, old), dim = c(2, 2, 2))
mantelhaen.test(arr, correct = FALSE)
mantelhaen.test(arr, correct = FALSE)
# common odds ratio 2.19, 95% CI 1.27 to 3.80
# Mantel-Haenszel X-squared = 8.24, df = 1, p = 0.0041
(50 * 470) / (250 * 30)   # crude, ignoring age: 3.13, inflated

Stack the two age strata with statsmodels and read the pooled odds ratio, interval and test.

import numpy as np, pandas as pd
from statsmodels.stats.contingency_tables import StratifiedTable
young = np.array([[10, 90], [20, 380]])   # rows: NSAID / non, cols: bleed / no
old   = np.array([[40, 160], [10, 90]])
st = StratifiedTable([young, old])
print(round(st.oddsratio_pooled, 3))   # Mantel-Haenszel OR
print(round(st.oddsratio_pooled, 3))                 # 2.194
print(np.round(st.oddsratio_pooled_confint(), 3))    # 1.27 .. 3.80
print(st.test_null_odds(correction=False))           # chi2 8.24, p 0.0041
print((50 * 470) / (250 * 30))                       # crude: 3.13, inflated

Is the adjustment real? The Mantel-Haenszel chi-squared

An adjusted odds ratio of 2.19 still needs a test against the null that the true value is 1. The Mantel-Haenszel chi-squared compares the exposed cases you observed against the number expected under no association, pooled across strata, and divides the squared gap by its variance.

The expected count in each stratum is and the variance is , both built from the margins alone. Young: , . Old: , . Observed exposed cases O = 10 + 40 = 50, expected E = 6.0 + 33.33 = 39.33, variance V = 13.81. So on 1 degree of freedom, P = 0.0041. The test keeps 1 degree of freedom no matter how many strata you pool. The 95% confidence interval for the odds ratio, 1.27 to 3.80, excludes 1 and agrees. After adjusting for age, NSAID use is associated with bleeding.

Plot the four odds ratios so the confounding jumps out: each stratum, the crude, and the adjusted.

ors <- c(Young = 2.11, Old = 2.25, Crude = 3.13, "MH adj" = 2.19)
barplot(ors, ylab = "Odds ratio")
cols <- c("grey70", "grey70", "firebrick", "steelblue")
barplot(ors, col = cols, ylim = c(0, 3.5), ylab = "Odds ratio",
        main = "Crude is inflated; MH sits with the strata")
abline(h = 1, lty = 2)               # the null
abline(h = 2.19, lty = 3, col = "steelblue")

Plot the four odds ratios so the confounding jumps out: each stratum, the crude, and the adjusted.

import numpy as np
import matplotlib.pyplot as plt
labels = ["Young", "Old", "Crude", "MH adj"]
ors = [2.11, 2.25, 3.13, 2.19]
plt.bar(labels, ors)
plt.ylabel("Odds ratio")
plt.show()
cols = ["grey", "grey", "firebrick", "steelblue"]
plt.bar(labels, ors, color=cols)
plt.axhline(1, ls="--", color="black")     # the null
plt.axhline(2.19, ls=":", color="steelblue")
plt.ylabel("Odds ratio")
plt.title("Crude is inflated; MH sits with the strata")
plt.show()

The crude odds ratio for NSAID use and bleeding is 3.13, but both age strata give about 2.1 and the Mantel-Haenszel estimate is 2.19. What does this pattern show?

  • Age confounded the crude estimate: older patients both used NSAIDs more and bled more, inflating the unadjusted odds ratio above the true effect.
  • Age modifies the effect of NSAIDs, because the strata differ from the crude value.
  • The Mantel-Haenszel method is wrong, because a summary should not differ from the crude odds ratio.
The two strata agree with each other (2.11 and 2.25) but both differ from the crude 3.13. That signature, strata together and crude apart, is confounding, not effect modification. The summary is meant to differ from the crude value: that difference is the bias being removed.

Effect modification is not confounding

Pooling with Mantel-Haenszel rests on one assumption: the true odds ratio is the same in every stratum, and the strata differ only by sampling noise. When the exposure genuinely acts differently across levels of C, that assumption fails. Then C is an effect modifier, the effect is said to show interaction or heterogeneity, and a single summary hides the real story rather than cleaning it up.

Consider oral contraceptive (OC) use and venous thromboembolism (VTE), split by smoking. The numbers below are illustrative.

Non-smokersVTENo VTETotal
OC user20480500
Non-user20480500
Total409601000
SmokersVTENo VTETotal
OC user60140200
Non-user15185200
Total75325400

Among non-smokers the odds ratio is (20 × 480) / (480 × 20) = 1.0, with a 95% interval of about 0.53 to 1.88. Among smokers it is (60 × 185) / (140 × 15) = 5.29, interval about 2.88 to 9.70. Those intervals do not overlap. OC use does little on its own but multiplies the clotting risk in smokers. The Mantel-Haenszel summary here is 2.52, a number that describes neither group. Reporting it alone would be the wrong call.

Test the assumption before you pool. The chi-squared test of heterogeneity asks whether the stratum odds ratios scatter further from the pooled value than noise allows. One common form weights each squared log-ratio gap by its precision.

Here and . For these data the statistic is 13.86 on 1 degree of freedom, P = 0.0002. That is strong evidence the OC effect really differs by smoking. So you do not report 2.52. You report each stratum: roughly no effect in non-smokers, a five-fold effect in smokers.

Confounding versus interaction, side by side

Confounding is a nuisance you remove: the strata agree, and you pool them into one cleaner number. Interaction is a finding you keep: the strata disagree, and the disagreement is the result. The heterogeneity test has low power, so a large P does not prove the effect is uniform. Always eyeball the stratum odds ratios and their intervals first, then let the test back up what your eyes already saw.

Pool the smoking strata, then test homogeneity with Woolf's method before trusting the pooled value.

nonsmk <- matrix(c(20, 20, 480, 480), nrow = 2)   # OR = 1.0
smk    <- matrix(c(60, 15, 140, 185), nrow = 2)   # OR = 5.29
arr2   <- array(c(nonsmk, smk), dim = c(2, 2, 2))
mantelhaen.test(arr2, correct = FALSE)$estimate   # pooled OR
mantelhaen.test(arr2, correct = FALSE)$estimate    # 2.52 -- describes neither group
or  <- c((20*480)/(480*20), (60*185)/(140*15))     # 1.0 and 5.29
v   <- c(1/20+1/480+1/20+1/480, 1/60+1/140+1/15+1/185)
w   <- 1 / v
lp  <- sum(w * log(or)) / sum(w)
chi <- sum(w * (log(or) - lp)^2)
c(chi = chi, p = pchisq(chi, df = 1, lower.tail = FALSE))  # 13.86, p 0.0002

Pool the smoking strata, then run the Breslow-Day homogeneity test before trusting the pooled value.

import numpy as np, pandas as pd
from statsmodels.stats.contingency_tables import StratifiedTable
nonsmk = np.array([[20, 480], [20, 480]])   # OR 1.0
smk    = np.array([[60, 140], [15, 185]])   # OR 5.29
st2 = StratifiedTable([nonsmk, smk])
print(round(st2.oddsratio_pooled, 2))   # pooled OR -- check homogeneity first
print(round(st2.oddsratio_pooled, 2))      # 2.52 -- describes neither group
print(st2.test_equal_odds())               # Breslow-Day test, p < 0.001
print((20*480)/(480*20), (60*185)/(140*15))   # report each: 1.0 and 5.29

A study of a new antibiotic and treatment failure stratifies by infection severity. The odds ratio is 0.9 in mild cases and 0.4 in severe cases, and the heterogeneity test gives P = 0.004. What should you report?

  • The two stratum-specific odds ratios separately, because severity modifies the drug's effect and a single pooled value would describe neither group.
  • Only the Mantel-Haenszel pooled odds ratio, because it controls for severity.
  • The crude odds ratio, because stratifying changed the estimate.
A small heterogeneity P with clearly different stratum estimates is effect modification, not confounding. Severity is a finding to keep, so report 0.9 for mild and 0.4 for severe. Pooling them into one Mantel-Haenszel value would hide that the drug helps far more in severe disease. The crude estimate ignores severity entirely and is not the answer either.

When stratification runs out

Stratification is exact, transparent, and easy to defend, which is why it is the right first tool. It also has a hard ceiling. The trouble is the number of strata. Control for age (4 bands), sex (2), and diabetes (2) at once and you already have 4 × 2 × 2 = 16 strata. Add another confounder and the count multiplies again. Spread a few hundred patients across that many tables and most cells go to zero or one. The Mantel-Haenszel odds ratio survives sparse strata, but the stratum-specific estimates and the heterogeneity test fall apart, and you can no longer adjust for a continuous confounder like exact age without chopping it into bands and losing information.

That ceiling is the reason logistic regression exists. A regression model adjusts for several confounders at once, keeps continuous variables continuous, and lets you test interaction terms directly, all without slicing the data into ever-smaller tables. The Mantel-Haenszel odds ratio and a logistic model with one binary exposure and one stratifying variable give nearly the same adjusted estimate, so regression is the natural extension once one or two confounders become several. You meet it in the logistic-regression module next.

Validity check before you trust the test

The Mantel-Haenszel odds ratio is valid even with small strata, but its chi-squared test needs enough spread in the margins. A quick rule: for each table take min() and max(0, ), sum each across strata, and both sums should differ from the total expected count by at least 5. Below that, lean on regression or an exact method instead.

Common mistakes

  • Adjusting for a mediator. If the exposure causes C and C causes the outcome, C is on the causal path. Stratifying on it removes part of the real effect. Confirm C is not a consequence of E before you adjust.
  • Pooling without testing homogeneity. A Mantel-Haenszel summary across heterogeneous strata is a number that fits no one, like the 2.52 above. Run the heterogeneity or Breslow-Day test, and scan the stratum intervals, before you report a pooled value.
  • Reading a similar crude and adjusted estimate as proof of no confounding. Two confounders can pull in opposite directions and cancel in the crude table. Check each candidate, not just the bottom line.
  • Feeding percentages into the calculation. The Mantel-Haenszel sums need the raw cell counts in every stratum. Proportions throw away the stratum sizes that drive the weights.
  • Calling the heterogeneity test negative because P was large. That test has low power. A large P is weak evidence of uniformity, not proof of it. Trust the pattern of estimates, not the P alone.

Tips

  • Decide your confounders from subject knowledge and a causal diagram before you touch the data, not by hunting for whichever variable shifts the estimate.
  • Report the crude and the adjusted estimate together. The size of the move is itself the evidence of how much confounding there was.
  • Use age bands narrow enough that risk is roughly flat within a band. Five-year bands are a common default; ten-year bands can leave residual confounding by age.
  • When you stratify on two confounders, build the full cross-classified strata (every age-by-sex cell), not one variable at a time, or you have not controlled for both jointly.
  • Once you need three or more confounders, or a continuous one, move to logistic regression rather than forcing more strata. Quote the Mantel-Haenszel result as a sanity check against the model.
← PreviousChi-squared: 2x2 tables, larger tables, trend, and exact testsNext →Logistic regression
On this page
  • What a confounder is
  • How confounding distorts the crude estimate
  • Stratify, then combine: the Mantel-Haenszel odds ratio
  • Is the adjustment real? The Mantel-Haenszel chi-squared
  • Effect modification is not confounding
  • When stratification runs out
  • Common mistakes
  • Tips