Relative Risk (Risk Ratio) Meta-analysis
Menu location: Analysis_Meta-Analysis_Relative Risk.
Cohort studies of dichotomous outcomes (e.g. dead or alive) can by represented by arranging the observed counts into fourfold (2 by 2) tables. Meta-analysis may be used to investigate the combination or interaction of a group of independent studies, for example a series of fourfold tables from similar studies conducted at different centres. This StatsDirect function examines the relative risk for each stratum (a single fourfold table) and for the group of studies as a whole.
For a single stratum relative risk is defined as follows:
| EXPOSURE | |||
| Exposed | Non-Exposed | ||
| OUTCOME: | Cases: | a | b |
| Non-cases: | c | d | |
Relative risk = [a/(a+c)] / [b/(b+d)]
For each table the observed relative risk is displayed with a confidence interval. The likelihood score-based method of Koopman (1984) recommended by Gart and Nam is used to construct the confidence interval (Gart and Nam 1988; Sahai and Khurshid, 1996). If the ’try exact’ option is not selected then a normal approximation to the confidence interval is given instead.
The Mantel-Haenszel type method of Rothman and Boice (Rothman, 1998) is used to estimate the pooled risk ratio for all strata under the assumption of a fixed effects model:
- where ni = ai+bi+ci+di.
A confidence interval for the pooled relative risk is calculated using the Greenland-Robins variance formula(Greenland and Robins, 1985). A chi-square test statistic is given with associated probability of the pooled relative risk being equal to one. Note that some packages give a z statistic; this is equal to the square root of the chi-square statistic with one degree of freedom, i.e. it gives the same P.
If any cell count in a table is zero then a continuity correction is applied to each cell in that table - if you have selected the 'delay continuity correction' option then the pooled relative risk, its confidence interval and the fixed effects weights are calculated from the counts as they are, provided that there is an event in each group among the studies. The type of continuity correction used is set in the options. A table is left out of the pooling, and marked as excluded, if it has no cases in either group or no subjects in one of the groups.
The inconsistency of results across studies is summarised in the I² statistic, which is the percentage of variation across studies that is due to heterogeneity rather than chance – see the heterogeneity section for more information.
Note that the results from StatsDirect may differ slightly from other software or from those quoted in papers; this is due to differences in the variance formulae. StatsDirect employs the most robust practical approaches to variance according to accepted statistical literature.
DATA INPUT:
Observed frequencies should be entered in a workbook as follows:
| Exposed/Experimental | Non-exposed/Non-experimental | ||
| Total number | Number of cases | Total number | Number of cases |
...where total number = cases + non-cases
Example
From Fleiss and Gross (1991).
Test workbook (Meta-analysis worksheet: Exposed total, Exposed cases, Non-exposed total, Non-exposed cases, Study).
The following data combine seven placebo-controlled randomized trials of the effect of aspirin in preventing death after myocardial infarction:
| Aspirin | Placebo | |||
| Trial | patients | deaths | patients | deaths |
| MRC-1 | 615 | 49 | 624 | 67 |
| CDP | 758 | 44 | 771 | 64 |
| MRC-2 | 832 | 102 | 850 | 126 |
| GASP | 317 | 32 | 309 | 38 |
| PARIS | 810 | 85 | 406 | 52 |
| AMIS | 2267 | 246 | 2257 | 219 |
| ISIS-2 | 8587 | 1570 | 8600 | 1720 |
To analyse these data in StatsDirect first prepare them in four workbook columns and label these columns appropriately. Alternatively, open the test workbook using the file open function of the file menu. Then select relative risk from the meta-analysis section of the analysis menu. Select the columns marked "Exposed total", "Exposed cases", "Non-exposed total" and "Non-exposed cases" when prompted for data. Note that "exposed" and "experimental" groups are the same.
For this example:
| Stratum | Relative risk | 95% CI (Koopman) | ||
| 1 | 0.742046 | 0.522928 | 1.051866 | MRC-1 |
| 2 | 0.699291 | 0.483538 | 1.010302 | CDP |
| 3 | 0.827038 | 0.648842 | 1.053557 | MRC-2 |
| 4 | 0.820853 | 0.528416 | 1.273886 | GASP |
| 5 | 0.819326 | 0.594653 | 1.133252 | PARIS |
| 6 | 1.118333 | 0.941378 | 1.3287 | AMIS |
| 7 | 0.914173 | 0.859614 | 0.97217 | ISIS-2 |
| Stratum | Standardized effect | Variance | % Weights (fixed, random) | ||
| 1 | -0.298344 | 0.032105 | 2.891172 | 7.835723 | MRC-1 |
| 2 | -0.357688 | 0.035736 | 2.758272 | 7.176709 | CDP |
| 3 | -0.189905 | 0.015362 | 5.418302 | 13.589974 | MRC-2 |
| 4 | -0.197411 | 0.051175 | 1.672876 | 5.286301 | GASP |
| 5 | -0.199274 | 0.027298 | 3.011273 | 8.920134 | PARIS |
| 6 | 0.111839 | 0.007747 | 9.540439 | 20.405362 | AMIS |
| 7 | -0.089736 | 0.000986 | 74.707666 | 36.785796 | ISIS-2 |
Fixed effects (Mantel-Haenszel, Rothman-Boice)
Pooled relative risk = 0.913608 (95% CI = 0.8657 to 0.964168)
Chi² (test relative risk differs from 1) = 10.809386 (df = 1) P = 0.001
Non-combinability of studies
Cochran Q = 9.928487 (df = 6) P = 0.1277
Moment-based estimate of between studies variance = 0.007437
I² (inconsistency) = 39.6% (95% CI = 0% to 76.6%)
Random effects (DerSimonian-Laird)
Pooled relative risk = 0.892922 (95% CI = 0.800632 to 0.995851)
Chi² (test relative risk differs from 1) = 4.139819 (df = 1) P = 0.0419
Bias indicators
Begg-Mazumdar: Kendall's tau = -0.428571 P = 0.2389 (low power)
Egger: bias = -0.730996 (90% CI = -2.227324 to 0.765332) P = 0.3701
Harbord-Egger: bias = -0.734002 (90% CI = -2.233589 to 0.765585) P = 0.3693
Here we can say with 95% confidence, assuming a random effects model, that for those given aspirin the true population risk of dying in the specified interval after a heart attack is at most 0.996 of the risk for those not given aspirin. Assuming a fixed effects model a stronger inference could be made about a relative risk of .96 (the upper confidence limit) but the high inter-study variation makes the fixed effects model less appropriate.
R code
This R code reproduces the example above. It needs no packages and was checked with R 4.6.1. Paste it into R, or save it as a script and run it.
# Relative risk meta-analysis: the StatsDirect help example (Fleiss and Gross 1991,
# deaths after myocardial infarction in seven placebo-controlled trials of aspirin;
# the test workbook's Meta-analysis worksheet columns A to E) in R
study <- c("MRC-1", "CDP", "MRC-2", "GASP", "PARIS", "AMIS", "ISIS-2")
exposed_total <- c(615, 758, 832, 317, 810, 2267, 8587)
exposed_cases <- c(49, 44, 102, 32, 85, 246, 1570)
control_total <- c(624, 771, 850, 309, 406, 2257, 8600)
control_cases <- c(67, 64, 126, 38, 52, 219, 1720)
# Each study's fourfold table in the report's order: a = exposed cases, b = control
# cases, cx = exposed non-cases and d = control non-cases (cx, because c is R's
# function for making a vector)
a <- exposed_cases
b <- control_cases
cx <- exposed_total - exposed_cases
d <- control_total - control_cases
n <- a + b + cx + d
k <- length(a)
z <- qnorm(0.975)
six <- function(x) formatC(x, digits = 6, format = "f", drop0trailing = TRUE)
one <- function(x) formatC(x, digits = 1, 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))
}
# Base R has no meta-analysis function, so the report is built from the formulae.
# The relative risk of each study, with Koopman's (1984) likelihood score interval:
# at a trial value of the ratio the two risks are estimated under the constraint
# that they stand in that ratio (the root of a quadratic in the control risk), and
# the limits are the ratios at which the score statistic for the exposed count
# reaches plus and minus the normal deviate. This holds when every cell is filled;
# the program adjusts a total equal to its count by a half before solving.
rr <- (a / (a + cx)) / (b / (b + d))
koopman <- function(x1, n1, x0, n0) {
score <- function(theta) {
qa <- (n0 + n1) * theta
qb <- -((x0 + n1) * theta + x1 + n0)
qc <- x0 + x1
p0 <- (-qb - sqrt(qb^2 - 4 * qa * qc)) / (2 * qa)
p1 <- p0 * theta
v <- 1 / ((1 - p0) / (n0 * p0) + (1 - p1) / (n1 * p1))
(x1 - n1 * p1) / (1 - p1) / sqrt(v)
}
est <- (x1 / n1) / (x0 / n0)
c(uniroot(function(t) score(t) - z, c(est / 100, est), tol = 1e-12)$root,
uniroot(function(t) score(t) + z, c(est, est * 100), tol = 1e-12)$root)
}
ci <- t(mapply(koopman, a, a + cx, b, b + d))
cat("Stratum Relative risk 95% CI (Koopman)\n")
for (i in 1:k) cat(i, six(rr[i]), six(ci[i, 1]), six(ci[i, 2]), study[i], "\n")
# Fixed effects: the Mantel-Haenszel type pooled risk ratio of Rothman and Boice,
# with the Greenland-Robins variance of its log for the interval and chi-square
w_mh <- b * (a + cx) / n
rr_mh <- sum(a * (b + d) / n) / sum(w_mh)
v_mh <- sum(((a + b) * (a + cx) * (b + d) - a * b * n) / n^2) /
(sum(a * (b + d) / n) * sum(w_mh))
x2 <- log(rr_mh)^2 / v_mh
# Cochran's Q about the pooled log risk ratio above, each study weighted by the
# inverse of the usual variance of its log relative risk; the DerSimonian-Laird
# moment estimate of the between studies variance then inflates each variance for
# the random effects weights
lrr <- log(rr)
v_i <- 1 / a + 1 / b - 1 / (a + cx) - 1 / (b + d)
w_i <- 1 / v_i
q <- sum(w_i * (lrr - log(rr_mh))^2)
tau2 <- max(0, (q - (k - 1)) / (sum(w_i) - sum(w_i^2) / sum(w_i)))
w_dl <- 1 / (tau2 + v_i)
rr_dl <- exp(sum(w_dl * lrr) / sum(w_dl))
dl_ci <- exp(sum(w_dl * lrr) / sum(w_dl) + c(-1, 1) * z / sqrt(sum(w_dl)))
x2_dl <- sum(w_dl * lrr)^2 / sum(w_dl)
# The report's second table: the log relative risk with its variance, and each
# study's share of the fixed and random weights
cat("Stratum Standardized effect Variance % Weights (fixed, random)\n")
for (i in 1:k) {
cat(i, six(lrr[i]), six(v_i[i]), six(100 * w_mh[i] / sum(w_mh)),
six(100 * w_dl[i] / sum(w_dl)), study[i], "\n")
}
cat("Fixed effects (Mantel-Haenszel, Rothman-Boice)\n")
cat("Pooled relative risk = ", six(rr_mh), " (95% CI = ",
six(exp(log(rr_mh) - z * sqrt(v_mh))), " to ",
six(exp(log(rr_mh) + z * sqrt(v_mh))), ")\n", sep = "")
cat("Chi2 (test relative risk differs from 1) =", six(x2), " (df = 1) ",
pv(pchisq(x2, 1, lower.tail = FALSE)), "\n")
# I-squared = (Q - df) / Q, with the interval that the heterogeneity topic attributes
# to Hedges and Pigott (2001): Q is treated as non-central chi-square and its
# distribution function at the observed Q is inverted for the non-centrality parameter
# lambda, the lower limit where that probability is 0.975 (0 when even the central
# distribution gives less) and the upper where it is 0.025; each limit is converted to
# I-squared as lambda / (df + lambda)
cat("Non-combinability of studies\n")
cat("Cochran Q = ", six(q), " (df = ", k - 1, ") ",
pv(pchisq(q, k - 1, lower.tail = FALSE)), "\n", sep = "")
cat("Moment-based estimate of between studies variance =", six(tau2), "\n")
i2 <- max(0, 100 * (q - (k - 1)) / q)
lambda <- function(p) {
if (pchisq(q, k - 1) < p) 0 else
uniroot(function(l) pchisq(q, k - 1, ncp = l) - p, c(0, 10 * q + 100),
tol = 1e-10)$root
}
lim <- c(lambda(0.975), lambda(0.025))
cat("I2 (inconsistency) = ", one(i2), "% (95% CI = ",
one(100 * lim[1] / (k - 1 + lim[1])), "% to ",
one(100 * lim[2] / (k - 1 + lim[2])), "%)\n", sep = "")
cat("Random effects (DerSimonian-Laird)\n")
cat("Pooled relative risk = ", six(rr_dl), " (95% CI = ", six(dl_ci[1]), " to ",
six(dl_ci[2]), ")\n", sep = "")
cat("Chi2 (test relative risk differs from 1) =", six(x2_dl), " (df = 1) ",
pv(pchisq(x2_dl, 1, lower.tail = FALSE)), "\n")
# Bias indicators. Begg and Mazumdar's rank correlation is Kendall's tau between
# each study's deviation from the inverse variance pooled log relative risk,
# standardised by its variance less the pooled variance, and that variance; with
# no ties among seven studies cor.test gives the exact two sided P, and fewer than
# eleven studies give the test little power
se_i <- sqrt(v_i)
dev <- (lrr - sum(w_i * lrr) / sum(w_i)) / sqrt(v_i - 1 / sum(w_i))
kt <- cor.test(dev, v_i, method = "kendall")
cat("Bias indicators\n")
cat("Begg-Mazumdar: Kendall's tau =", six(kt$estimate), "", pv(kt$p.value),
"(low power)\n")
# Egger's test regresses each study's standardised effect (log relative risk over
# its standard error) on its precision (one over that standard error); the bias
# is the intercept, judged by a t test on k - 2 degrees of freedom with a 90%
# interval, the level the test is conventionally reported at
egger <- function(y, x, label) {
fit <- lm(y ~ x)
bias <- coef(fit)[1]
se <- sqrt(vcov(fit)[1, 1])
t <- bias / se
cat(label, ": bias = ", six(bias), " (90% CI = ", six(bias - qt(0.95, k - 2) * se),
" to ", six(bias + qt(0.95, k - 2) * se), ") ",
pv(2 * pt(-abs(t), k - 2)), "\n", sep = "")
}
egger(lrr / se_i, 1 / se_i, "Egger")
# Harbord's modification (Harbord, Egger and Sterne 2006) uses the efficient score
# for the log relative risk and its Fisher information in place of the estimate and
# its standard error, which are far less correlated than the estimate and its standard
# error when effects are large or events few
score <- (a * n - (a + b) * (a + cx)) / (cx + d)
info <- (b + d) * (a + cx) * (a + b) / (n * (cx + d))
egger(score / sqrt(info), sqrt(info), "Harbord-Egger")
# The report's L'Abbe plot: the risk in the exposed group of each study against
# the risk in its control group, the symbol growing with the study size, with the
# line of equal risks and the line of the pooled relative risk
plot(100 * b / (b + d), 100 * a / (a + cx), cex = 0.5 + 2 * sqrt(n / max(n)),
xlim = c(0, 25), ylim = c(0, 25), xlab = "control percent",
ylab = "experimental percent",
main = "L'Abbe plot (symbol size represents sample size)")
abline(0, 1)
abline(0, rr_mh, lty = 3)
# The two forest plots: each study's relative risk (the symbol growing with its
# weight) with its interval on a log scale, the line of no effect at 1 and the
# pooled estimate as a diamond at the foot
forest <- function(weights, pooled, lower, upper, title, label) {
y <- rev(seq_len(k)) + 1
par(mar = c(5, 8, 4, 8))
plot(rr, y, log = "x", xlim = range(ci, lower, upper), ylim = c(0.5, k + 1.5),
pch = 15, cex = 0.5 + 2 * sqrt(weights / max(weights)), yaxt = "n", ylab = "",
xlab = "relative risk (95% confidence interval)", main = title)
segments(ci[, 1], y, ci[, 2], y)
polygon(c(lower, pooled, upper, pooled), c(1, 1.3, 1, 0.7), col = "grey")
segments(pooled, 1, pooled, k + 1, lty = 3)
abline(v = 1)
axis(2, at = c(y, 1), labels = c(study, label), las = 1, tick = FALSE)
axis(4, at = c(y, 1), las = 1, tick = FALSE,
labels = sprintf("%.2f (%.2f, %.2f)", c(rr, pooled), c(ci[, 1], lower),
c(ci[, 2], upper)))
}
forest(w_mh, rr_mh, exp(log(rr_mh) - z * sqrt(v_mh)), exp(log(rr_mh) + z * sqrt(v_mh)),
"Relative risk meta-analysis plot (fixed effects)", "combined [fixed]")
forest(w_dl, rr_dl, dl_ci[1], dl_ci[2],
"Relative risk meta-analysis plot (random effects)", "combined [random]")