This Preregistration-As-Code uses the Workflow for Open Reproducible Code in Science (Van Lissa et al. 2020) to ensure reproducibility and transparency. All code is available at https://github.com/cjvanlissa/meta_anx_ssri.git.
First, we compute effect sizes using metafor.
dat[["id_experiment"]] <- as.integer(factor(dat$article))
dat[["id_es"]] <- 1:nrow(dat)
dat$paper <- dat$article
dat[c("study", "article")] <- NULL
# Remove cases for sensitivity analysis
if(sensitivity){
dat <- dat[which(!dat$sensitivity == 1), ]
}
dat$Sample <- ordered(dat$test)
samples <- levels(dat$Sample)
dat[["test"]] <- NULL
dat <- escalc(measure = "SMD",
n1i = dat$ncon,
n2i = dat$nexp,
m1i = dat$mcon,
m2i = dat$mexp,
sd1i = dat$sdcon,
sd2i = dat$sdexp,
data = dat)
dat <- dat[!is.na(dat$yi), ]
dat$yi <- dat$yi * dat$multiplier
dat[["multiplier"]] <- NULL
zscores <- scale(dat[["yi"]])
maxz <- max(abs(zscores))
#if(maxz > 3.3) stop("Potential outlier in Y-space")
dat <- dat[!abs(zscores) > 3.3, ]
saveRDS(dat, "dat.RData")
dat <- readRDS("dat.RData")
samples <- levels(dat$Sample)
mods <- c("ssri", "frequency", "disease", "species", "sex",
"hed", "pretested", "sih_test_type", "usv_test_type")
if(!all(mods %in% names(dat))) stop()
cat <- names(dat)[sapply(dat, inherits, "factor")]
cat <- cat[cat %in% mods]
mlm <- lapply(samples, function(p){
df <- dat[dat$Sample == p, ]
model_pubbias <- rma(df$yi, vi = df$vi)
pubbias_selmodel <- metafor::selmodel(model_pubbias, type ="stepfun", steps = c(.05))
png(paste0("funnel_", p, ".png"))
metafor::funnel(model_pubbias)
dev.off()
svg(paste0("funnel_", p, ".svg"))
metafor::funnel(model_pubbias)
dev.off()
pubbias <- regtest(x = df$yi, vi = df$vi)
#Conduct meta-analyses
#model.mods <- rma.mv(yi, vi, random = list(~ 1 | id_experiment, ~ 1 | id_es), data=df)
model.full <- rma.mv(yi, vi, random = list(~ 1 | id_experiment, ~ 1 | id_es), data=df)
model.between_null <- rma.mv(yi, vi, random = list(~ 1 | id_experiment, ~ 1 | id_es), sigma2=c(NA,0), data = df)
model.within_null <- rma.mv(yi, vi, random = list(~ 1 | id_experiment, ~ 1 | id_es), sigma2=c(0,NA), data = df)
model.both_null <- rma.mv(yi, vi, random = list(~ 1 | id_experiment, ~ 1 | id_es), sigma2=c(0,0), data = df)
# Compute I2
W <- diag(1/df$vi)
X <- model.full$X
P <- W - W %*% X %*% solve(t(X) %*% W %*% X) %*% t(X) %*% W
I2 <- 100 * model.full$sigma2 / (sum(model.full$sigma2) + (model.full$k-model.full$p)/sum(diag(P)))
names(I2) <- c("I2_Between", "I2_Within")
#model.mods <- rma.mv(yi, vi, mods = as.formula(paste0("~ ", paste(moderators, collapse = " + "))), random = list(~ 1 | id_experiment, ~ 1 | id_es), data = df)
#ggplot(, aes(x=d, colour=interventioncode))+geom_density()
#anova(model.full,rma.mv(yi, vi, mods = ~interventioncode, random = list(~ 1 | id_experiment, ~ 1 | id_es), data = df) )
aov_within <- anova(model.full,model.within_null)
aov_between <- anova(model.full,model.between_null)
aov_bothnull <- anova(model.full,model.both_null)
aov_table <- data.frame(rbind(
c(aov_between$fit.stats.f[c(3:4, 1)], LRT = NA, p = NA),
c(aov_within$fit.stats.r[c(3:4, 1)], LRT = aov_within$LRT, p = aov_within$pval),
c(aov_between$fit.stats.r[c(3:4, 1)], LRT = aov_between$LRT, p = aov_between$pval),
c(aov_bothnull$fit.stats.r[c(3:4, 1)], LRT = aov_bothnull$LRT, p = aov_bothnull$pval)
))
rownames(aov_table) <- c("Three-level model", "Within-studies variance constrained", "Between-studies variance constrained", "Both variance components constrained")
write.csv(aov_table, paste0("threelevel_ma_", p, ".csv"))
confints <- confint(model.full)
#CHeck convergence of variance components:
#par(mfrow=c(2,1))
#plot.profile1 <- profile(model.full, sigma2=1)
#plot.profile2 <- profile(model.full, sigma2=2)
#Write forest plot to file
xname <- paste0("Hedges' g (", p, ")")
tmp <- df[order(df$vi, decreasing = TRUE), ]
tmp$paperlabel <- paste(tmp$paper, tmp$type_ssri_and_dose)
df_es <- data.frame(
Study = ordered(tmp$paperlabel, levels = unique(tmp$paperlabel)),
y = 1:nrow(tmp),
es = tmp$yi,
lb = tmp$yi - 1.96*sqrt(tmp$vi),
ub = tmp$yi + 1.96*sqrt(tmp$vi),
Sample = p)
dfoverall <- data.frame(x = c(model.full$ci.ub, model.full$b[1,1], model.full$ci.lb, model.full$b[1,1]),
y = c(max(df_es$y)+2, max(df_es$y)+2.5, max(df_es$y)+2, max(df_es$y)+1.5))
ylabsize = 10
# if(p == "Acq retr to ctx") ylabsize =1
pforest <- ggplot(data=df_es)+
geom_point(aes(y=y, x=es))+
geom_errorbarh(aes(y = y, xmin=lb, xmax=ub), height=.1)+
# geom_point(data=data.frame(y = max(df_es$y)+2, es = model.full$b[1,1]), aes(y=y, x=es), size=4, shape = 15, fill = "black")+
# geom_errorbarh(data = data.frame(y = max(df_es$y)+2, lb = model.full$ci.lb, ub = model.full$ci.ub), aes(y = y, xmin=lb, xmax=ub), height=.4, size = 2)+
geom_polygon(data = dfoverall, aes(x = x, y = y))+
scale_x_continuous(name=xname, limits = c(min(df_es$lb), min(c(10,max(df_es$ub)))))+
scale_y_continuous(name = "", breaks=1:max(df_es$y), labels = df_es$Study, trans="reverse")+
#adding a vertical line at the effect = 0 mark
geom_vline(xintercept=0, color="black", linetype="dashed", alpha=.5)+
geom_hline(yintercept=max(df_es$y)+1)+
theme(axis.text.y = element_text(size=ylabsize))+
theme_minimal()
ggsave(paste0("threelevel_ma_forest", p, ".png"), pforest, device = "png", height = (nrow(df)+2)*.2)
res = data.frame(Variance = c("Overall ES", "tau2 between", "tau2 within"),
rbind(c(estimate = model.full$b[1,1], ci.lb = model.full$ci.lb, ci.ub = model.full$ci.ub, model.full$pval),
c(confints[[1]]$random[1,], aov_table$p[3]),
c(confints[[2]]$random[1,], aov_table$p[2])))
res[nrow(res)+c(1:2), ] <- NA
res[[1]][nrow(res)-c(0,1)] <- names(I2)
res[[2]][nrow(res)-c(0,1)] <- I2
list(mod = model.full,
aov_table = aov_table,
res = res,
pubbias = pubbias,
pubbias_sel = pubbias_selmodel)
})
names(mlm) <- samples
saveRDS(mlm, "mlm.RData")
tb <- lapply(1:length(samples), function(i){
tb <- mlm[[i]]$res
tb$Sample <- samples[i]
tb
})
tb <- do.call(rbind, tb)
tb$CI <- tidySEM::conf_int(lb = tb$ci.lb, ub = tb$ci.ub)
tb[c("ci.lb", "ci.ub")] <- NULL
names(tb) <- c("Parameter", "Estimate", "p", "Sample", "CI")
write.csv(tb, "tab_threelevel.csv", row.names = FALSE)
The effect size estimates ranged from -8.46 to 3.76 ().
Several studies reported multiple effect sizes (1 - 16, with most reporting 1 effect size).
We examined publication bias using funnel plots, Egger’s test, and selection models, see Iyengar, S., & Greenhouse, J. B. (1988). Selection models and the file drawer problem. Statistical Science, 3(1), 109–117. https://doi.org/10.1214/ss/1177013012
Note that these methods for detecting publication bias are only valid for random effects models; they thus ignore the multilevel structure of the data.
We conducted likelihood ratio tests to examine whether there was evidence that publications with a p-value < .05 were more likely to be published.
out = NULL
for (i in samples) {
out = c(out, knit_expand('selbias_template.Rmd'))
}
According to the selection model, there was no evidence of publication bias, LRT(2) = 0.72, p = 0.40.
Egger’s test for funnel plot asymmetry showed significant evidence of publication bias, Z = -8.54, p < 0.01.
knitr::include_graphics("funnel_EPM.png")
According to the selection model, there was significant evidence of publication bias, LRT(2) = 6.72, p < 0.01.
Egger’s test for funnel plot asymmetry showed significant evidence of publication bias, Z = -14.95, p < 0.01.
knitr::include_graphics("funnel_MB.png")
According to the selection model, there was no evidence of publication bias, LRT(2) = 1.15, p = 0.28.
Egger’s test for funnel plot asymmetry showed significant evidence of publication bias, Z = -4.84, p < 0.01.
knitr::include_graphics("funnel_SIH.png")
According to the selection model, there was significant evidence of publication bias, LRT(2) = 6.80, p < 0.01.
Egger’s test for funnel plot asymmetry showed significant evidence of publication bias, Z = -7.11, p < 0.01.
knitr::include_graphics("funnel_USV.png")
Meta-analysis was conducted in R (R Core Team 2021) using the R-packages metafor (Viechtbauer et al. 2010), and pema (Van Erp S. 2021).
To estimate overall effects, we used three-level meta-analysis to account for dependent effect sizes within studies (Van den Noortgate et al. 2015).
Let denote the observed effect sizes , originating from studies.
The multi-level model is then given by the following equations:
The first equation indicates that observed effect sizes are equal to the underlying population effect size, plus sampling error . The second equation indicates that population effect sizes within studies are a function of a study-specific true effect size, plus within-study residuals . The third equation indicates that the distribution of study-specific true effect sizes are distributed around an overall mean effect, with between-study residuals .
Separate meta-analyses were conducted for each of the samples. The overall pooled effect sizes were:
tb <- read.csv("tab_threelevel.csv", stringsAsFactors = FALSE)
datatable(tb, rownames= FALSE, options = list(
"pageLength" = nrow(tb))) |>
formatRound(columns=c('Estimate', 'p'), digits=2)
The overall effect size estimate differed significantly from zero for EPM, MB, USV.
The within-studies variance component (between effect sizes) was significant for .
The between-studies variance was significant for .
The forest plots for the aforementioned three-level meta-analyses are presented below. Within each plot, studies are ranked by their sampling variance ; thus, the most precise estimates are at the bottom, near the overall effect.
knitr::include_graphics(paste0("threelevel_ma_forest", samples[1], ".png"))
Figure 3.1: Forest plot for EPM
knitr::include_graphics(paste0("threelevel_ma_forest", samples[2], ".png"))
Figure 3.2: Forest plot for MB
knitr::include_graphics(paste0("threelevel_ma_forest", samples[3], ".png"))
Figure 3.3: Forest plot for SIH
knitr::include_graphics(paste0("threelevel_ma_forest", samples[4], ".png"))
Figure 3.4: Forest plot for USV
The effect of multiple moderators was investigated using meta-regression. For categorical variables, dummies were encoded. As the number of moderators is high relative to the number of studies, there is a risk of overfitting and model non-identification. Addressing this problem requires performing variable selection. Three steps were taken to do so. First, variables and categories that did not occur within one subset of the data were omitted. Secondly, some dummy variables were redundant because some studies had identical values on multiple dummy variables. As such variables are identical, their effects cannot be distinguished. Only one of these redundant dummy variables was retained, and its name was updated to reflect all redundant dummies it represents. Thirdly, despite these measures, many meta-regression models dropped all or some of the predictors, or failed to converge entirely, suggesting the models were empirically non-identified. Although these models are reported below, we advise against their substantive interpretation.
The problems with meta-regression suggests that a technique is required that performs variable selection during analysis.
Such a technique was recently developed: Bayesian penalized meta-regression (BRMA), as implemented in the pema R-package (Van Erp S. (2021)).
By imposing a regularizing (horseshoe) prior on the regression coefficients,
BRMA shrinks all coefficients towards zero, which aids empirical model identification.
Coefficients must overwhelm the prior in order to become significantly different from zero.
Thus, this method also performs variable selection: identifying which moderators are important in predicting the effect size.
The resulting regression coefficients are negatively biased by design, but the estimate of residual heterogeneity is unbiased.
Note that, as this is a Bayesian model, inference is based on credible intervals.
A credible interval is interpreted as follows: The population value falls within this interval with 95% probability (certainty).
This is different from the interpretation of frequentist confidence intervals, which are interpreted as follows: In the long run, 95% of confidence intervals contain the population value.
To examine the effect of a categorical variable, a reference category must be chosen. Dummy variables encode the difference between each remaining category and this reference category. When examining the results, the intercept represents the expected effect size for a study that falls within the reference category for all categorical variables. The effect of dummy variables represents the difference of that category with the reference category. If a dummy variable has a significant effect, that means that that group’s mean differs significantly from the reference category’s mean (i.e., from the intercept).
Note that in penalized regression, predictors are usually standardized. However, the effect of standardized dummies cannot be meaningfully interpreted. Therefore, only continuous predictors were standardized in this analysis. This may give dummy variables a slight advantage, leading them to become significant sooner than continuous ones.
# Refcats now provided in different format
refcats <- read.csv("../reference_categories.csv", stringsAsFactors = FALSE)
refcats <- lapply(samples, function(s){
out <- refcats[refcats$sample == s, -1]
data.frame(Variable = names(out),
Reference = unlist(out))
})
names(refcats) <- samples
modres <- lapply(samples, function(p){
df <- dat[dat$Sample == p, ]
mods_cont <- mods[!mods %in% cat]
constcols <- sapply(df[mods], function(i){length(unique(i)) < 2})
constcolnames <- names(constcols)[constcols]
mods <- mods[!mods %in% constcolnames]
mods <- mods[mods %in% names(df)]
dfmod <- df[, mods, drop = FALSE]
# Find ref cat
# Changed, because reference categories are now provided by researchers
catvars <- sapply(dfmod, class) == "factor"
catvars <- names(catvars)[catvars]
refcat <- as.data.frame(refcats[[p]])
dfmod[catvars] <- lapply(catvars, function(c){
v <- dfmod[[c]]
relevel(v, ref = refcat$Reference[match(tolower(c), refcat$Variable)])
})
completecases <- complete.cases(dfmod)
modmat <- model.matrix(~., dfmod)
constcols <- colSums(modmat) == 0 | colSums(modmat) == nrow(modmat)
modmat <- modmat[, !constcols, drop = FALSE]
modmatlist <- as.list(data.frame(modmat))
dupcols <- duplicated(modmatlist)
modmat <- modmat[, !dupcols, drop = FALSE]
these_dups <- modmatlist[dupcols]
modmatlist <- as.list(data.frame(modmat))
for(i in 1:length(these_dups)){
idnum <- which(duplicated(c(modmatlist, these_dups[i]), fromLast = T))
colnames(modmat)[idnum] <- paste0(colnames(modmat)[idnum], ";", names(these_dups)[i])
}
yi_notcentered <- df$yi[completecases]
res <- try(rma.mv(yi = yi_notcentered, V = df$vi[completecases], random = list(~ 1 | id_experiment, ~ 1 | id_es), data = df[completecases,], intercept = TRUE, mods = modmat))
if(inherits(res, "try-error")){
tabres <- NULL
} else {
vifs <- metafor::vif(res)
confints <- confint(res)
tabres <- data.frame(Parameter = c(rownames(res$b), "Tau2b", "Tau2w"),
Estimate = c(res$b, res$sigma2),
ci.lb = c(res$ci.lb, confints[[1]]$random[1,2], confints[[2]]$random[1,2]),
ci.ub = c(res$ci.ub, confints[[1]]$random[1,3], confints[[2]]$random[1,3]),
p = c(res$pval, NA, NA),
sig = c(c("", "*")[(res$pval < .05)+1], c("", "*")[(c(confints[[1]]$random[1,2], confints[[2]]$random[1,2])> 1e-4)+1]),
VIF = c(NA, vifs$vifs, NA, NA))
tabres$CI <- conf_int(lb = tabres$ci.lb, ub = tabres$ci.ub)
tabres[c("ci.lb", "ci.ub")] <- NULL
tabres$Sample <- p
write.csv(tabres, paste0("rma_mods_", p, ".csv"))
}
moddat <- data.frame(df[completecases, c("yi", "vi")], modmat)
df_cats <- sapply(moddat, function(x){all(x %in% c(0, 1))})
std <- moddat[, !df_cats, drop = FALSE]
std <- scale(std[, -c(1:2), drop = FALSE])
df_cats <- moddat[, df_cats, drop = FALSE]
standardize <- list(
center = c(attr(std,"scaled:center"), rep(0, ncol(df_cats))),
scale = c(attr(std,"scaled:scale"), rep(1, ncol(df_cats)))
)
moddat <- cbind(yi = yi_notcentered, vi = df$vi[completecases], std, df_cats)
moddat$study <- df$id_experiment[completecases]
pma <- brma(yi ~., study = "study", data = moddat, standardize = standardize, iter = 10000)
saveRDS(pma, paste0("pma_", p, ".RData"))
sumpma <- data.frame(summary(pma)$coefficients)
sumpma <- cbind(rownames(sumpma), sumpma)
names(sumpma)[1:3] <- c("Parameter", "Estimate", "se")
sumpma <- sumpma[!sumpma$Parameter == "tau", ]
rownames(sumpma) <- NULL
sumpma$CI <- conf_int(lb = sumpma$X2.5., ub = sumpma$X97.5.)
pstars <- c("*", "")[as.integer(apply(sumpma[, c("X2.5.",
"X97.5.")], 1, function(x) {
sum(sign(x)) == 0
})) + 1]
sumpma$p <- pstars
sumpma[c("sd", grep("^X", names(sumpma), value = T))] <- NULL
write.csv(sumpma, paste0("brma_mods_", p, ".csv"))
list(rma = tabres,
brma = sumpma,
refcat = refcat)
})
names(modres) <- samples
saveRDS(modres, "modres.RData")
modres <- readRDS("modres.RData")
Note that analyses containing VIF values greater than 5 should be regarded as problematic, due to multicolinearity. This applies to nearly all models.
p <- samples[1]
tb <- modres[[1]]$refcat
# knitr::kable(tb, caption = paste0("Reference category for each variable for process ", p))
datatable(
tb,
rownames = FALSE,
caption = paste0("Reference category for each variable for sample ", p))
if(!is.null(modres[[1]][["rma"]])){
p <- samples[1]
tb <- modres[[1]]$rma
tb$format <- as.integer(tb$p < .05)
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 6, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', 'p', "VIF"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
} else {
cat("Model for ", samples[1], " did not converge.")
}
p <- samples[2]
tb <- modres[[2]]$refcat
# knitr::kable(tb, caption = paste0("Reference category for each variable for process ", p))
datatable(
tb,
rownames = FALSE,
caption = paste0("Reference category for each variable for sample ", p))
if(!is.null(modres[[2]][["rma"]])){
p <- samples[2]
tb <- modres[[2]]$rma
tb$format <- as.integer(tb$p < .05)
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 6, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', 'p', "VIF"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
} else {
cat("Model for ", samples[2], " did not converge.")
}
p <- samples[3]
tb <- modres[[3]]$refcat
# knitr::kable(tb, caption = paste0("Reference category for each variable for process ", p))
datatable(
tb,
rownames = FALSE,
caption = paste0("Reference category for each variable for sample ", p))
if(!is.null(modres[[3]][["rma"]])){
p <- samples[3]
tb <- modres[[3]]$rma
tb$format <- as.integer(tb$p < .05)
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 6, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', 'p', "VIF"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
} else {
cat("Model for ", samples[3], " did not converge.")
}
p <- samples[4]
tb <- modres[[4]]$refcat
# knitr::kable(tb, caption = paste0("Reference category for each variable for process ", p))
datatable(
tb,
rownames = FALSE,
caption = paste0("Reference category for each variable for sample ", p))
if(!is.null(modres[[4]][["rma"]])){
p <- samples[4]
tb <- modres[[4]]$rma
tb$format <- as.integer(tb$p < .05)
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 6, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', 'p', "VIF"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
} else {
cat("Model for ", samples[4], " did not converge.")
}
p <- samples[1]
tb <- modres[[1]]$brma
tb$format <- as.integer(tb$p == "*")
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Bayesian regularized meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 7, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', "se", "n_eff", "Rhat"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
p <- samples[2]
tb <- modres[[2]]$brma
tb$format <- as.integer(tb$p == "*")
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Bayesian regularized meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 7, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', "se", "n_eff", "Rhat"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
p <- samples[3]
tb <- modres[[3]]$brma
tb$format <- as.integer(tb$p == "*")
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Bayesian regularized meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 7, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', "se", "n_eff", "Rhat"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)
p <- samples[4]
tb <- modres[[4]]$brma
tb$format <- as.integer(tb$p == "*")
formatStyle(
formatRound(
datatable(
tb,
rownames = FALSE,
caption = paste0("Bayesian regularized meta-regression coefficients for ", p),
options = list(columnDefs = list(list(
targets = 7, visible = FALSE
)),
"pageLength" = nrow(tb))
),
columns = c('Estimate', "se", "n_eff", "Rhat"),
digits = 2
),
"format",
target = "row",
backgroundColor = styleEqual(c(0, 1), c('gray', 'white'))
)