R lecture 4: Meta-analysis in R II

SKI3011 · binary and continuous outcomes, published odds ratios, the meta package

How to use this lecture. Download the R script, open it in RStudio and run it line by line (Cmd+Enter on a Mac, Ctrl+Enter on Windows). All explanations on this page are in the script as comments. Check that you get the same output as shown here. Quiz 3 asks you to read and interpret exactly this kind of code and output.

Download the R script

Packages

Today we use two packages. metafor you know from last week. meta does the same with slightly different functions (metabin(), metacont(), metagen()) and makes nice forest plots. Many published reviews use one of the two.

# install.packages("meta")   # run once
library(metafor)
library(meta)
library(metadat)

Binary outcomes: the data

Nine trials on diuretics in pregnancy and the risk of pre-eclampsia (dat.collins1985b). For each trial we need the number of events and the group size in the treatment and the control group.

dat <- dat.collins1985b[, c("author", "year", "pre.xti", "pre.nti", "pre.xci", "pre.nci")]
dat
                   author year pre.xti pre.nti pre.xci pre.nci
1       Weseley & Douglas 1962      14     131      14     136
2          Flowers et al. 1962      21     385      17     134
3                 Menzies 1964      14      57      24      48
4           Fallis et al. 1964       6      38      18      40
5         Cuadros & Tatum 1964      12    1011      35     760
6        Landesman et al. 1965     138    1370     175    1336
7            Kraus et al. 1966      15     506      20     524
8    Tervila & Vartiainen 1971       6     108       2     103
9 Campbell & MacGillivray 1975      65     153      40     102

Binary outcomes with metafor

Instead of the four cells (ai, bi, ci, di) you can give the events and group sizes (ai, n1i, ci, n2i). The odds ratio is pooled on the log scale.

dat <- escalc(measure = "OR",
              ai = pre.xti, n1i = pre.nti,   # events and group size, treatment
              ci = pre.xci, n2i = pre.nci,   # events and group size, control
              data = dat, slab = paste(author, year))
res <- rma(yi, vi, data = dat)
predict(res, transf = exp, digits = 2)

 pred ci.lb ci.ub pi.lb pi.ub 
 0.60  0.38  0.92  0.19  1.90 

Diuretics lower the odds of pre-eclampsia (OR 0.60, 95% CI 0.38 to 0.92), but the prediction interval (0.19 to 1.90) shows that the effect in a new trial could be anything from strong protection to harm: the trials are very heterogeneous.

forest(res, atransf = exp, header = TRUE)

Mantel-Haenszel

The Mantel-Haenszel method is a fixed-effect method that works directly on the counts and handles small numbers of events well.

rma.mh(measure = "OR", ai = pre.xti, n1i = pre.nti, ci = pre.xci, n2i = pre.nci,
       data = dat, digits = 2)

Equal-Effects Model (k = 9)

I^2 (total heterogeneity / total variability):  70.66%
H^2 (total variability / sampling variability): 3.41

Test for Heterogeneity: 
Q(df = 8) = 27.27, p-val < .01

Model Results (log scale):

estimate    se   zval  pval  ci.lb  ci.ub 
   -0.40  0.09  -4.60  <.01  -0.58  -0.23 

Model Results (OR scale):

estimate  ci.lb  ci.ub 
    0.67   0.56   0.79 

Cochran-Mantel-Haenszel Test:    CMH = 21.23, df = 1, p-val < 0.01
Tarone's Test for Heterogeneity: X^2 = 28.79, df = 8, p-val < 0.01

Binary outcomes with meta

metabin() from the meta package gives the fixed-effect (called “common effect”) and the random-effects result in one go. Studies with zero events in one group get a continuity correction of 0.5 (incr).

m_bin <- metabin(event.e = pre.xti, n.e = pre.nti,   # experimental group
                 event.c = pre.xci, n.c = pre.nci,   # control group
                 sm = "OR", data = dat,
                 studlab = paste(author, year))
summary(m_bin)
                                 OR            95%-CI %W(common) %W(random)
Weseley & Douglas 1962       1.0427 [0.4766;  2.2815]        3.9       10.9
Flowers et al. 1962          0.3971 [0.2027;  0.7779]        7.5       11.9
Menzies 1964                 0.3256 [0.1424;  0.7444]        6.2       10.4
Fallis et al. 1964           0.2292 [0.0785;  0.6692]        4.6        8.3
Cuadros & Tatum 1964         0.2488 [0.1283;  0.4827]       12.4       12.0
Landesman et al. 1965        0.7431 [0.5863;  0.9420]       50.1       15.9
Kraus et al. 1966            0.7699 [0.3897;  1.5210]        6.0       11.9
Tervila & Vartiainen 1971    2.9706 [0.5857; 15.0675]        0.6        5.1
Campbell & MacGillivray 1975 1.1449 [0.6871;  1.9078]        8.7       13.6

Number of studies: k = 9
Number of observations: o = 6942 (o.e = 3759, o.c = 3183)
Number of events: e = 636

                         OR           95%-CI     z  p-value
Common effect model  0.6677 [0.5620; 0.7932] -4.60 < 0.0001
Random effects model 0.5956 [0.3843; 0.9233] -2.32   0.0205

Quantifying heterogeneity (with 95%-CIs):
 tau^2 = 0.3008 [0.0723; 2.2027]; tau = 0.5484 [0.2689; 1.4842]
 I^2 = 70.7% [41.8%; 85.2%]; H = 1.85 [1.31; 2.60]

Test of heterogeneity:
     Q d.f. p-value
 27.26    8  0.0006

Details of meta-analysis methods:
- Mantel-Haenszel method (common effect model)
- Inverse variance method (random effects model)
- Restricted maximum-likelihood estimator for tau^2
- Q-Profile method for confidence interval of tau^2 and tau
- Calculation of I^2 based on Q
forest(m_bin)

Published odds ratios with a confidence interval

Often a paper only reports an odds ratio with its 95% CI. Then you calculate the log odds ratio and its standard error yourself: SE = [ln(upper) − ln(lower)] / 3.92. Example: 37 studies on passive smoking and lung cancer in women (dat.hackshaw1998).

smoke <- dat.hackshaw1998[, c("author", "year", "or", "or.lb", "or.ub")]
smoke$log_or <- log(smoke$or)
smoke$se_log_or <- (log(smoke$or.ub) - log(smoke$or.lb)) / 3.92   # SE from the 95% CI
head(smoke, 3)

     author year   or or.lb or.ub    log_or se_log_or 
1 Garfinkel 1981 1.18  0.90  1.54 0.1655144 0.1370263 
2  Hirayama 1984 1.45  1.02  2.08 0.3715636 0.1817769 
3    Butler 1988 2.02  0.48  8.56 0.7030975 0.7349667 

metagen() pools any pre-calculated effect size: TE is the effect (here the log OR) and seTE its standard error. With sm = "OR" the output is shown as odds ratios.

m_gen <- metagen(TE = log_or, seTE = se_log_or, sm = "OR",
                 data = smoke, studlab = paste(author, year))
summary(m_gen)$random    # random-effects result on the log scale
$TE
[1] 0.2188913

$seTE
[1] 0.04939564

$lower
[1] 0.1220777

$upper
[1] 0.315705

$statistic
[1] 4.43139

$p
[1] 9.36276e-06

$level
[1] 0.95

$df
[1] Inf
exp(c(m_gen$TE.random, m_gen$lower.random, m_gen$upper.random))   # as odds ratio
[1] 1.244696 1.129842 1.371226

Women exposed to environmental tobacco smoke have about 24% higher odds of lung cancer (OR 1.24, 95% CI 1.13 to 1.37).

Continuous outcomes: mean difference

Five studies comparing pain scores (NRS, 0 to 10) after two types of spinal fusion surgery, TLIF and PLIF. All studies use the same scale, so we pool the mean difference (MD).

Author   <- c("Yang", "Han", "Liu", "Yan", "Sakeb")
Year     <- c(2015, 2016, 2016, 2008, 2013)
N_TLIF   <- c(32, 36, 101, 91, 50)
NRS_TLIF <- c(1.33, 2.44, 2.84, 2.84, 1.83)
SD_TLIF  <- c(0.89, 1.42, 0.91, 0.91, 0.63)
N_PLIF   <- c(34, 26, 125, 85, 52)
NRS_PLIF <- c(1.26, 2.65, 2.84, 2.84, 2.00)
SD_PLIF  <- c(0.76, 1.26, 0.89, 0.89, 0.67)
pain <- data.frame(Author, Year, N_TLIF, NRS_TLIF, SD_TLIF, N_PLIF, NRS_PLIF, SD_PLIF)

m_cont <- metacont(n.e = N_TLIF, mean.e = NRS_TLIF, sd.e = SD_TLIF,   # TLIF group
                   n.c = N_PLIF, mean.c = NRS_PLIF, sd.c = SD_PLIF,   # PLIF group
                   sm = "MD", data = pain, studlab = paste(Author, Year))
summary(m_cont)
                MD            95%-CI %W(common) %W(random)
Yang 2015   0.0700 [-0.3304; 0.4704]       11.1       11.1
Han 2016   -0.2100 [-0.8806; 0.4606]        4.0        4.0
Liu 2016    0.0000 [-0.2363; 0.2363]       31.9       31.9
Yan 2008    0.0000 [-0.2660; 0.2660]       25.1       25.1
Sakeb 2013 -0.1700 [-0.4223; 0.0823]       27.9       27.9

Number of studies: k = 5
Number of observations: o = 632 (o.e = 310, o.c = 322)

                          MD            95%-CI     z p-value
Common effect model  -0.0481 [-0.1814; 0.0853] -0.71  0.4801
Random effects model -0.0481 [-0.1814; 0.0853] -0.71  0.4801

Quantifying heterogeneity (with 95%-CIs):
 tau^2 = 0 [0.0000; 0.0712]; tau = 0 [0.0000; 0.2669]
 I^2 = 0.0% [0.0%; 79.2%]; H = 1.00 [1.00; 2.19]

Test of heterogeneity:
    Q d.f. p-value
 1.74    4  0.7835

Details of meta-analysis methods:
- Inverse variance method
- Restricted maximum-likelihood estimator for tau^2
- Q-Profile method for confidence interval of tau^2 and tau
- Calculation of I^2 based on Q
forest(m_cont, label.e = "TLIF", label.c = "PLIF", xlab = "Pain (NRS)")

The pain scores hardly differ (MD −0.05, 95% CI −0.18 to 0.09) and there is no heterogeneity (I² = 0%).

Continuous outcomes: standardised mean difference

When studies measure the same outcome on different scales, use the standardised mean difference (SMD, Hedges’ g): the difference in means divided by the pooled standard deviation. Example: length of hospital stay after stroke in specialist versus routine care (dat.normand1999).

stroke <- escalc(measure = "SMD",
                 m1i = m1i, sd1i = sd1i, n1i = n1i,   # specialist care
                 m2i = m2i, sd2i = sd2i, n2i = n2i,   # routine care
                 data = dat.normand1999, slab = source)
res_smd <- rma(yi, vi, data = stroke)
print(res_smd, digits = 2)

Random-Effects Model (k = 9; tau^2 estimator: REML)

tau^2 (estimated amount of total heterogeneity): 0.79 (SE = 0.43)
tau (square root of estimated tau^2 value):      0.89
I^2 (total heterogeneity / total variability):   95.49%
H^2 (total variability / sampling variability):  22.20

Test for Heterogeneity:
Q(df = 8) = 123.73, p-val < .01

Model Results:

estimate    se   zval  pval  ci.lb  ci.ub    
   -0.54  0.31  -1.74  0.08  -1.14   0.07  . 

---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

The pooled SMD is −0.54 (95% CI −1.14 to 0.07): a moderate reduction in length of stay, but not statistically significant, and with very large heterogeneity (I² = 95%).

Saving a forest plot

To put a forest plot in your paper, save it as an image file. This is not run on the website.

png("forest_plot.png", width = 1000, height = 500)
forest(m_cont, label.e = "TLIF", label.c = "PLIF", xlab = "Pain (NRS)")
dev.off()

Check yourself

Go to the concept list and practice quiz of week 4 and try the practice quiz without looking at this page.