Suppose a research team is interested in assessing the effectiveness of a new treatment called Magic Therapy (MT) on mental health which is administered across 50 clinics in North America. In each clinic, a group of 10 patients was randomly selected to begin MT, while another group of 10 served as the control group and continued with traditional therapy. The researchers are particularly interested in evaluating the effectiveness of MT at each individual clinic, rather than relying solely on the overall effect across all clinics.
We begin by simulating data under the null hypothesis that MT does not affect mental health, and that there are no differences between clinics or between each clinic and the overall mean effect.
intercept <- 10 # Grand mean
MTeffect <- 0 # Overall, fixed effect
sd_MT <- 0 # (No) variability in cluster-specific coefficients
sigma <- 8 # within-cluster variability
sd_int <- 7 # intercepts variability
cor <- 0 # No association between slopes and intercepts
n <- 20 # sample size in each cluster
n_clusters <- 50 # number of clusters / clinics
N <- n*n_clusters # total sample size
clinic <- rep(1:n_clusters, each = n) # Defining cluster ID
MT <- rep(c(0, 1), length.out = N) # Setting dichotomous predictor: control vs. treat
varmat <- matrix(c(sd_int^2, cor, cor, sd_MT^2), 2, 2) # Variance-covariance matrix
set.seed(311)
re <- mvtnorm::rmvnorm(n_clusters, sigma = varmat) # Generate cluster-level population information from var-covar
colnames(re) <- c('Intercept', 'MTeffect') # renaming the the columns
# Generate outcome variable:
ment_health <- (intercept + re[clinic, 'Intercept']) +
(MTeffect + re[clinic, 'MTeffect'])*MT + # this line is redundant in the Type I error conditions because it's all 0 in the null condition
rnorm(N, sd = sigma)
# putting it all together
dat <- data.frame(ment_health, MT = factor(MT), clinic)
head(dat)
dp <- ggplot(dat, aes(x=MT, y=ment_health, fill=MT)) +
geom_violin(trim=FALSE)+
geom_boxplot(width=0.1, fill="white")+
labs(title="",x="control vs. MT", y = "Mental Health")
dp + scale_fill_brewer(palette="Set2") + theme_minimal()
## By Clinic
dat %>%
ggplot(aes(x = MT, y = ment_health, color = factor(clinic), group = factor(clinic)) )+
geom_point() + # individual trajectory
geom_smooth(method = "lm", se = F, alpha = 0.8)+
geom_smooth(aes(group = 1), method = "lm", linewidth=2, linetype='dashed', color='black') + # overall trend line
theme_minimal()
One way we might approach this is by conducting 50 one-sample t-tests comparing the treatment and control groups, as seen in the next section.
# Defining a function for convenience
t_test_func <- function(subset_data) {
result <- t.test(ment_health ~ MT, data = subset_data)
c(p.value = result$p.value,
mean_diff = result$estimate[2] - result$estimate[1],
up_ci = -result$conf.int[1],
low_ci = -result$conf.int[2])
}
# Use 'by' to split the data by clinic and apply the function
results <- by(dat, dat$clinic, t_test_func)
# Convert results to a data frame
results_df <- data.frame(do.call(rbind, results))
# Extract values from the data frame
p.vals <- results_df$p.value
mean_difs <- results_df$mean_diff
lowcis <- results_df$low_ci
uppcis <- results_df$up_ci
Without multiple comparison adjustments, 2 out of the 50 t tests were incorrectly rejected.
# Counting how many are significant
(t.test_res<-sum(p.vals<=.05))
## [1] 2
# Type I error rate
(t.test_res/n_clusters)
## [1] 0.04
We can plot this:
no_pooling <- data.frame(clinic = seq(1, n_clusters), mean_difs, lowcis, uppcis) %>%
mutate(significance = ifelse(lowcis <= 0 & uppcis >= 0, "Not sig", "Sig")) %>%
as_tibble()
ggplot(no_pooling, aes(x = clinic, y = mean_difs)) +
geom_point() +
geom_errorbar(aes(ymin = lowcis, ymax = uppcis, color = significance), width = 0.2) +
labs(x = "Site", y = "Treatment Slope") +
ggtitle("Point Estimates from t tests with 95% Confidence Intervals") +
theme_minimal() +
geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
scale_color_manual(values = c("black", "red"))
# Bonferroni adjustment
Bonferroni_ps <- p.adjust(p.vals, method = "bonferroni",
n = length(p.vals))
sum(Bonferroni_ps<=.05) # none are significant
## [1] 0
# FDR adjustment
FDR_ps <-p.adjust(p.vals, method = "fdr",
n = length(p.vals))
sum(FDR_ps<=.05) # none are significant
## [1] 0
mod <- glmmTMB::glmmTMB(ment_health~MT+(1+MT||clinic), data=dat)
randeffs <- mixedup::extract_random_coefs(mod) %>% filter(effect=="MT1")
randCOEF <- randeffs$value
randCIs <- data.frame(randeffs$lower_2.5, randeffs$upper_97.5)
MLM_CI_prod <- apply(randCIs, 1, function(row) row[1] * row[2])
(MLM_prop_sig <- sum(MLM_CI_prod > 0)/n_clusters) # none are significant
## [1] 0
part_pooling <- data.frame(clinic = seq(1, n_clusters), randCOEF, lci = randeffs$lower_2.5, uci = randeffs$upper_97.5) %>%
mutate(significance = ifelse(lci <= 0 & uci >= 0, "Not Sig", "Sig")) %>%
as_tibble()
ggplot(part_pooling, aes(x = clinic, y = randCOEF)) +
geom_point() +
geom_errorbar(aes(ymin = lci, ymax = uci, color = significance), width = 0.2) +
labs(x = "Site", y = "Treatment Slope") +
ggtitle("Point Estimates from Freq. MLM with 95% Confidence Intervals") +
theme_minimal() +
geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
scale_color_manual(values = c("black", "red"))
The plot above shows that the estimated effects of individual clinics
are pulled closer to zero, resulting in less biased estimates with
greater precision (narrower confidence intervals), and no clinic was
mistakenly identified as significantly different from zero. Still, this
is just a single simulated dataset. Next, I present results from a
preliminary simulation study.
The following is a preliminary, pilot Monte Carlo simulation with
1000 iterations using SimDesign.
res.error <- read_csv("/Users/udialter/Library/CloudStorage/GoogleDrive-udi.alter@gmail.com/.shortcut-targets-by-id/1YTLxwhL6sZPwDksLKy12wSf9kHwKL4EQ/Cribbie_Alter/Dissertation (previously Bayes_Multiplicity)/Code/Data/sim_results_TypeIerror_conditions1.csv")
long.error <- res.error %>%
dplyr::select(n:n_clusters, contains("prop")) %>%
pivot_longer(cols=contains("prop"), names_to = 'Method', values_to = 'TypeIerror')
long.error $Method <- factor(long.error $Method)
ggplot(long.error, aes(x = n, y = TypeIerror,
colour = Method,
linetype = as.factor(n_clusters)
)) +
geom_line(linewidth = 2, alpha = 0.7) +
theme_minimal() +
theme(
text = element_text(size = 15), # Increase the size of all text
legend.text = element_text(size = 15),
legend.title = element_text(size = 15, face = "bold"),
axis.text = element_text(size = 15),
axis.title = element_text(size = 15),
strip.text = element_text(size = 15), # Facet label text size
) +
labs(title = "Type I error conditions",y="Type I Error Rate",
color = "Approach", linetype = "Number of clinics", x = "n per-clinic") +
scale_linetype_manual(values = c("5" = "dotted", "10" = "dotdash", "20"="dashed","50" = "longdash", "100" = "solid"))
res.power <- read.csv("~/Library/CloudStorage/GoogleDrive-udi.alter@gmail.com/.shortcut-targets-by-id/1YTLxwhL6sZPwDksLKy12wSf9kHwKL4EQ/Cribbie_Alter/Dissertation (previously Bayes_Multiplicity)/Code/Data/sim_results_power_conditions.csv")
long.power <- res.power %>%
dplyr::select(n:MTeffect, contains("prop")) %>%
pivot_longer(cols=contains("prop"), names_to = 'Method', values_to = 'PowerRate')
long.power$Method <- factor(long.power$Method)
long.power$MTeffect <- factor(long.power$MTeffect)
# Custom labeling function to create expressions
custom_label <- function(MTeffect) {
sapply(MTeffect, function(x) as.expression(bquote(beta[MT] == .(x))))
}
# Plot
ggplot(long.power, aes(x = n, y = PowerRate,
colour = Method,
linetype = as.factor(n_clusters)
)) +
facet_wrap(~MTeffect, labeller = as_labeller(custom_label, label_parsed)) +
geom_line(linewidth = 3, alpha = 0.7) +
theme_minimal() +
theme(
text = element_text(size = 30), # Increase the size of all text
legend.text = element_text(size = 30),
legend.title = element_text(size = 30, face = "bold"),
axis.text = element_text(size = 30),
axis.title = element_text(size = 30),
strip.text = element_text(size = 30), # Facet label text size
) +
labs(title = "Power conditions", y="Power Rate",
color = "Approach", linetype = "Number of clinics", x = "n per-clinic") +
scale_linetype_manual(values = c("5" = "dotted", "10" = "dashed",
"50" = "longdash", "100" = "solid")) +
theme(strip.text = element_text(size = 30, face = "bold")) # Keep the facet labels bold