Cochran-Mantel-Haenszel Test in R: Association Adjusted for a Stratifying Variable

Test the association between two categorical variables while controlling for a third — the stratified analysis that resolves Simpson’s paradox

Biostatistics

Learn to run the Cochran-Mantel-Haenszel (CMH) test in R — test the association between two categorical variables while adjusting for a stratifying (confounding) variable, across a stack of 2×2 tables. Compute it with base mantelhaen.test(), read the common odds ratio, check whether the stratum odds ratios are homogeneous with the Woolf test, and see how stratification resolves Simpson’s paradox.

Published

June 23, 2026

Modified

July 7, 2026

TipKey takeaways
  • The Cochran-Mantel-Haenszel (CMH) test tests the association between two categorical variables while adjusting for a third (stratifying) variable — across a stack of 2×2 tables, one per stratum.
  • Run it with base mantelhaen.test() on a 2×2×k array; it returns a chi-square, a p-value and the common odds ratio (the association after controlling for the stratifier).
  • It resolves Simpson’s paradox — an association that reverses or vanishes once you control for a confounder.
  • Check that the per-stratum odds ratios are homogeneous (similar across strata) with the Woolf test (vcd::woolf_test()); if they differ a lot, a single common odds ratio is misleading.
  • Use it whenever a confounding variable could distort a simple 2×2 association.

Introduction

A simple 2×2 association can be distorted by a confounder — a third variable related to both. The Cochran-Mantel-Haenszel test handles this by stratifying: it splits the data into a 2×2 table for each level of the confounder, then combines the evidence into one test of association adjusted for the stratifier.

The classic example — and the textbook case of Simpson’s paradox — is the Berkeley graduate-admissions data: pooled across departments, men appear admitted at a higher rate than women; but once you look within each department, the bias largely disappears. This lesson runs the CMH test on the built-in UCBAdmissions data.

Note

CMH hypotheses. H₀: the two variables are conditionally independent given the stratifier (common odds ratio = 1). Hₐ: they are associated after adjusting for the stratifier.

The paradox: pooled vs stratified

UCBAdmissions is a 2×2×6 array — Admit × Gender × Department. First, the pooled (collapsed) table ignores department:

pooled <- margin.table(UCBAdmissions, c(1, 2))
pooled
          Gender
Admit      Male Female
  Admitted 1198    557
  Rejected 1493   1278
# pooled odds ratio (admitted, male vs female)
(pooled["Admitted", "Male"] * pooled["Rejected", "Female"]) /
  (pooled["Admitted", "Female"] * pooled["Rejected", "Male"])
[1] 1.84108

The pooled odds ratio is 1.84 — men’s odds of admission look ~84% higher. But this collapses over department, and departments differ both in how many men/women apply and in how selective they are.

Run the Cochran-Mantel-Haenszel test

mantelhaen.test() keeps the departments separate (one 2×2 per department) and tests the Admit × Gender association adjusted for department:

mantelhaen.test(UCBAdmissions)

    Mantel-Haenszel chi-squared test with continuity correction

data:  UCBAdmissions
Mantel-Haenszel X-squared = 1.4269, df = 1, p-value = 0.2323
alternative hypothesis: true common odds ratio is not equal to 1
95 percent confidence interval:
 0.7719074 1.0603298
sample estimates:
common odds ratio 
        0.9046968 

After adjusting for department, the common odds ratio is 0.90 (95% CI ~0.77–1.06) and the test is not significant (p = 0.23) — the apparent gender bias disappears once you control for which department people applied to. That reversal between the pooled (1.84) and adjusted (0.90) odds ratios is Simpson’s paradox.

Check homogeneity of the odds ratios (Woolf test)

The common odds ratio only summarises well if the per-stratum odds ratios are similar. The Woolf test checks that:

library(vcd)

woolf_test(UCBAdmissions)

    Woolf-test on Homogeneity of Odds Ratios (no 3-Way assoc.)

data:  UCBAdmissions
X-squared = 17.902, df = 5, p-value = 0.003072

Here Woolf’s test is significant (p = 0.003) — the admission odds ratio genuinely differs across departments (one department, in fact, favours women strongly).

Warning

A significant Woolf test means the CMH common odds ratio is not an appropriate summary. The Mantel-Haenszel assumption — one shared odds ratio across strata — is violated, so the single 0.90 hides real department-to-department differences. When this happens, don’t lean on the common odds ratio: report each stratum, or test the strata individually (here, with a Fisher exact test per department).

When the strata are heterogeneous, run the test within each department to localise the effect:

# Fisher exact test (gender vs admission) within each department
sapply(seq_len(dim(UCBAdmissions)[3]), function(k) {
  f <- fisher.test(UCBAdmissions[, , k])
  c(odds_ratio = round(unname(f$estimate), 2), p = round(f$p.value, 4))
})
           [,1]   [,2]   [,3]   [,4]   [,5]   [,6]
odds_ratio 0.35 0.8000 1.1300 0.9200 1.2200 0.8300
p          0.00 0.6771 0.3866 0.5995 0.3604 0.5458

Only department A is significant (p < 0.001, odds ratio 0.35) — it admits women at a higher rate. The other five departments show no significant gender effect. So the real story isn’t “a small overall bias” (common OR 0.90) — it’s that one department drives everything, which only the stratified, per-department view reveals.

See the per-stratum odds ratios

library(vcd)

# odds ratio (male vs female admission) within each department
or_by_dept <- sapply(seq_len(dim(UCBAdmissions)[3]), function(k) {
  m <- UCBAdmissions[, , k]
  (m["Admitted", "Male"] * m["Rejected", "Female"]) /
    (m["Admitted", "Female"] * m["Rejected", "Male"])
})
df <- data.frame(dept = LETTERS[1:6], OR = round(or_by_dept, 2))

library(ggpubr)
ggdotchart(df, x = "dept", y = "OR", color = "#3a86d4", dot.size = 5,
           add = "segments", ylab = "Odds ratio (male vs female)", xlab = "Department") +
  geom_hline(yintercept = 1, linetype = "dashed")

A dot plot of the male-vs-female admission odds ratio within each of the six departments, scattered around the no-effect line at 1 — most near or below 1 — unlike the biased pooled estimate of 1.84.

Most departments sit near or below 1 (department A strongly favours women) — nothing like the pooled 1.84. This is why stratified analysis matters.

Report

A Cochran-Mantel-Haenszel test found no significant association between gender and admission after adjusting for department (common odds ratio 0.90, 95% CI 0.77–1.06, p = 0.23), despite a pooled odds ratio of 1.84 — an instance of Simpson’s paradox. A Woolf test indicated the department-specific odds ratios were heterogeneous (p = 0.003), so the per-department results are reported alongside the common estimate.

The CMH statistic combines the 2×2 tables across the \(k\) strata by summing, over strata, the difference between the observed and (under conditional independence) expected count in one cell, then squaring the total and dividing by the summed variance: \(\chi^2_{CMH} = \dfrac{\left(\sum_k (a_k - E[a_k])\right)^2}{\sum_k \text{Var}(a_k)}\). The Mantel-Haenszel common odds ratio is a stratum-weighted average, \(\hat{\theta}_{MH} = \dfrac{\sum_k a_k d_k / n_k}{\sum_k b_k c_k / n_k}\) — robust even when some strata are sparse.

Try it live

Run the CMH test, then collapse over the stratifier to see the paradox. The sandbox boots on first Run (it loads vcd, so give it a moment).

🟢 With an AI agent

Ask Prova “I think a third variable is confounding my 2×2 association — run a Cochran-Mantel-Haenszel test stratified by it and tell me the adjusted odds ratio” — it answers with code you can run on your own stratified data. The runtime is the judge. Ask Prova →

Common issues

You analysed the pooled table. Collapsing over a confounder can create or reverse an association (Simpson’s paradox). Keep the strata separate and use the CMH test to get the adjusted association.

The odds ratios are heterogeneous. If the Woolf test is significant, the strata genuinely differ, and a single common odds ratio over-summarises — report the per-stratum estimates too (or model the interaction).

Your data isn’t a 2×2×k array. mantelhaen.test() wants a three-dimensional table (rows × columns × strata). Build it with xtabs(~ outcome + exposure + stratum, data = mydata) or table().

Frequently asked questions

It tests the association between two categorical variables while adjusting for a third (stratifying) variable, by combining a 2×2 table from each stratum. It returns a chi-square, a p-value, and the common odds ratio — the association after controlling for the confounder — and is the standard way to handle a confounding variable in categorical data.

Simpson’s paradox is when an association in the pooled data reverses or disappears once you control for a third variable. The CMH test gives the adjusted (stratified) association, so it reveals the paradox: in the Berkeley data the pooled odds ratio suggests gender bias, but the CMH (adjusted for department) shows none.

The Woolf test checks whether the odds ratios are homogeneous (similar) across the strata. The CMH common odds ratio is a meaningful summary only when they are. If the Woolf test is significant, the strata differ and you should report the per-stratum odds ratios rather than relying on a single common value.

Test your understanding

  1. Run it. In the live cell, run the CMH test on UCBAdmissions and the pooled table. Fill the blank. How do the adjusted and pooled odds ratios compare?
  2. Conceptual. Your Woolf test is significant. What does that tell you about reporting a single common odds ratio?

Fill the blank with mantelhaen.test. Compare its common odds ratio (~0.90) to the pooled OR (~1.84). For question 2, recall what the common odds ratio assumes about the strata.

mantelhaen.test(UCBAdmissions)   #> common OR ≈ 0.90, p = 0.23 (no bias after adjustment)
margin.table(UCBAdmissions, c(1, 2))   #> pooled OR ≈ 1.84 (apparent bias)

For question 2: a significant Woolf test means the odds ratios differ across strata, so a single common odds ratio over-summarises and can mislead — report the per-stratum odds ratios (or model the stratum × exposure interaction) instead of relying on the pooled CMH estimate alone.

TipWhich test when?

Conclusion

You can now run the Cochran-Mantel-Haenszel test in R: build the 2×2×k array, run mantelhaen.test() for the association and common odds ratio adjusted for the stratifier, and check the Woolf test for homogeneity of the per-stratum odds ratios. It is the tool for controlling a confounder in categorical data — and the cleanest demonstration of why stratified analysis beats a pooled 2×2 (Simpson’s paradox). For adjusting on many variables at once, step up to logistic regression.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Cochran-Mantel-Haenszel {Test} in {R:} {Association}
    {Adjusted} for a {Stratifying} {Variable}},
  date = {2026-06-23},
  url = {https://www.datanovia.com/learn/biostatistics/categorical/cochran-mantel-haenszel-test-in-r},
  langid = {en}
}
For attribution, please cite this work as:
“Cochran-Mantel-Haenszel Test in R: Association Adjusted for a Stratifying Variable.” 2026. June 23. https://www.datanovia.com/learn/biostatistics/categorical/cochran-mantel-haenszel-test-in-r.