Generalised Cochran-Mantel-Haenszel tests
Menu location: Analysis_Crosstabs.
Three generalised tests for association between row and column classes are offered for stratified r by c tables produced in the crosstabs function when you specify a third (stratum, controlling for) classifier (Agresti, 2002; Landis et al., 1978, 1979).
The first test (ordinal association) assumes that there is meaningful order to both the columns and rows of each r by c table.
The second test (ordinal columns vs. nominal rows) assumes that there is meaningful order in the columns of each r by c table.
The third test (nominal association) does not assume any order in rows or columns; it provides a general test of association between the row and column classifiers.
The reliability of the tests increases with sample size, but unlike the Pearson chi-square statistic for single r by c tables, small counts in a few cells are unlikely to invalidate the tests.
You could control for more than one factor by making a stratum variable consisting of several factors (e.g. UK male, US male, UK female, US female to control for gender and country of residence).
Note that there are other approaches to these analyses, namely ordinal and nominal logistic regression. You should consult with a statistician before using these methods in important studies.
Data entry
When you give a third classifier and the table is not 2 by 2, StatsDirect asks for scores in a grid with a column headed by the name of each classifier: enter one score for each category of the row classifier under its name, and one score for each category of the column classifier under its name, in the order in which the categories appear in the table. The report repeats the scores used.
Example
From Agresti (2002).
The data can be found in the Tables worksheet of the Test workbook. Use the menu item Analysis_Crosstabs to generate a cross tabulation with income as the first classifier (rows), job satisfaction as the second classifier (columns) and gender as the third classifier (strata). Use the row (income) scores as 3, 10, 20 and 35. Use the column (job satisfaction) scores as 1, 3, 4 and 5.
For this example:
Generalised Cochran-Mantel-Haenszel tests
Row variable (first classifier): Income
Column variable (second classifier): Job Satisfaction
Stratum variable (third classifier, controlling for): Gender
Income scores: 3, 10, 20, 35
Job Satisfaction scores: 1, 3, 4, 5
| Alternative hypothesis | Statistic | DF | Probability |
| Ordinal association | 6.156301 | 1 | P = 0.0131 |
| Nominal rows vs. ordinal columns association | 9.034222 | 3 | P = 0.0288 |
| Nominal association | 10.200089 | 9 | P = 0.3345 |
Sample size = 104
From the results above you can see that the strongest effect detected is ordinal association (i.e. association between greater job satisfaction with greater income), after controlling for gender.
R code
This R code reproduces the example above. It needs no packages and was checked with R 4.6.1. It reads the data from job_satisfaction.csv, the test workbook's columns saved with their headings as a csv file (see the first comment in the code). Paste it into R, or save it as a script and run it.
# Generalised Cochran-Mantel-Haenszel tests: the StatsDirect help example (Agresti
# 2002, job satisfaction by income in 104 people, controlling for gender) in R
# The data are the Income, Job Satisfaction and Gender columns of the Tables
# worksheet of the StatsDirect test workbook. Save those columns, with their
# headings, as job_satisfaction.csv in R's working directory first.
d <- read.csv("job_satisfaction.csv")
tab <- table(Income = d$Income, Satisfaction = d$Job.Satisfaction, Gender = d$Gender)
print(tab) # one income by satisfaction table per gender
u <- c(3, 10, 20, 35) # scores for the income categories (the rows)
v <- c(1, 3, 4, 5) # scores for the satisfaction categories (columns)
# R's mantelhaen.test gives the third of the report's tests, the generalised
# Cochran-Mantel-Haenszel test of nominal association for a stratified r by c
# table (Landis, Heyman and Koch 1978); its continuity correction applies only to
# 2 by 2 by k tables, so none is made here
print(mantelhaen.test(tab))
six <- function(x) formatC(x, digits = 6, format = "f", drop0trailing = TRUE)
pv <- function(p) {
if (p < 0.0001) "P < 0.0001" else
paste("P =", formatC(p, digits = 4, format = "f", drop0trailing = TRUE))
}
# All three tests are the same quadratic form (Landis et al. 1978, Agresti 2002):
# within each stratum the cell counts, stacked column by column, are compared with
# their expected values given the margins, and the differences are summed over the
# strata along with their covariance under the multiple hypergeometric distribution.
# A linear transformation A chooses what is compared: the counts themselves (nominal
# association), the row totals of the column scores (nominal rows, ordinal columns)
# or the sum of the products of the row and column scores (ordinal association).
cmh <- function(A) {
s <- 0
V <- 0
for (k in seq_len(dim(tab)[3])) {
n <- tab[, , k]
N <- sum(n)
if (N < 2) next # a stratum of one observation adds nothing
rs <- rowSums(n)
cs <- colSums(n)
m <- as.vector(outer(rs, cs) / N)
cov <- kronecker(N * diag(cs) - cs %*% t(cs), N * diag(rs) - rs %*% t(rs)) /
(N^2 * (N - 1))
s <- s + A %*% (as.vector(n) - m)
V <- V + A %*% cov %*% t(A)
}
as.numeric(t(s) %*% solve(V) %*% s) # solve() fails if V is singular
}
r <- dim(tab)[1]
cc <- dim(tab)[2]
drop_last <- function(k) cbind(diag(k - 1), 0) # the last category is redundant
A_nominal <- kronecker(drop_last(cc), drop_last(r))
A_rows <- kronecker(matrix(v, 1), drop_last(r))
A_ordinal <- kronecker(matrix(v, 1), matrix(u, 1))
show <- function(label, q, df) {
cat(label, "=", six(q), " DF =", df, " ", pv(pchisq(q, df, lower.tail = FALSE)), "\n")
}
cat("Income scores:", paste(u, collapse = ", "), "\n")
cat("Job Satisfaction scores:", paste(v, collapse = ", "), "\n")
show("Ordinal association", cmh(A_ordinal), 1)
show("Nominal rows vs. ordinal columns association", cmh(A_rows), r - 1)
show("Nominal association", cmh(A_nominal), (r - 1) * (cc - 1))
cat("Sample size =", sum(tab), "\n")