02-power_results.R 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. library(tidyverse)
  2. library(broom)
  3. library(patchwork)
  4. library(grid)
  5. ggplot2::theme_set(
  6. theme_classic() +
  7. theme(
  8. legend.position=c(0.99, 0.01),
  9. legend.justification = c(1, 0),
  10. text=element_text(size=12),
  11. plot.title = element_text(hjust=0.5),
  12. legend.direction = "vertical"
  13. )
  14. )
  15. # colours <- c("red", "blue")
  16. colours <- c("#EA5515", "#15AAEA")
  17. ci_level <- 0.99
  18. ci_level_norm_sd <- qnorm(ci_level + (1-ci_level)/2) # how many multiples of sd to get ci_level of normal distribution
  19. geom_ribbon_ci_level <- 0.99
  20. # function to take vector of 0s and 1s and get lower and upper bounds of confidence interval in tidy format
  21. tidy_binom_ci <- function(x, level=0.95, min_obs=1) {
  22. x <- as.numeric(x)
  23. if (length(x)>min_obs) {
  24. prop.test(sum(x), length(x), conf.level=level, correct = TRUE) %>%
  25. broom::tidy() %>%
  26. select(conf.low, conf.high)
  27. } else {
  28. tibble(conf.low = NA, conf.high = NA)
  29. }
  30. }
  31. # power <- list.files("simulations", full.names = TRUE) %>%
  32. # map_df(
  33. # read_csv,
  34. # col_types = cols(
  35. # sim_nr = col_factor(),
  36. # re_corr = col_double(),
  37. # nS = col_double(),
  38. # estimate = col_double(),
  39. # chisq = col_double(),
  40. # p = col_double(),
  41. # is_sig = col_logical(),
  42. # slopes_removed = col_logical()
  43. # )
  44. # ) %>%
  45. # mutate(re_corr = factor(round(re_corr, 1), levels=seq(0, 0.8, 0.2)))
  46. # write_csv(power, "power.csv")
  47. # import simulations results
  48. power <- read_csv("power.csv", col_types = cols(
  49. sim_nr = col_factor(),
  50. re_corr = col_double(),
  51. nS = col_double(),
  52. estimate = col_double(),
  53. chisq = col_double(),
  54. p = col_double(),
  55. is_sig = col_logical(),
  56. corrs_removed = col_logical()
  57. )) %>%
  58. mutate(re_corr = factor(round(re_corr, 1), levels=seq(0, 0.8, 0.2))) %>%
  59. group_by(nS, re_corr) %>%
  60. mutate(iter_id = row_number()) %>%
  61. ungroup()
  62. # get one and two-tailed significance results in long format
  63. power_summ <- power %>%
  64. mutate(sig_type = "One-Tailed", is_sig = as.numeric(p<0.1 & estimate>0)) %>%
  65. bind_rows(
  66. .,
  67. mutate(., sig_type = "Two-Tailed", is_sig = as.numeric(p<0.05))
  68. )
  69. power_summ_recorr_0 <- filter(power_summ, re_corr==0)
  70. # for the plot looking at random effect correlations, sample a subset of the
  71. power_summ_all_recorrs <- power %>%
  72. group_by(nS, re_corr) %>%
  73. slice_sample(prop=1) %>%
  74. mutate(sig_type = "One-Tailed", is_sig = as.numeric(p<0.1 & estimate>0)) %>%
  75. bind_rows(
  76. .,
  77. mutate(., sig_type = "Two-Tailed", is_sig = as.numeric(p<0.05))
  78. )
  79. # plot power curve
  80. plt_power_summ <- power_summ_recorr_0 %>%
  81. group_by(nS, sig_type) %>%
  82. summarise(
  83. power = mean(is_sig),
  84. ci_low = pull(tidy_binom_ci(is_sig, level=ci_level), "conf.low"),
  85. ci_high = pull(tidy_binom_ci(is_sig, level=ci_level), "conf.high"),
  86. .groups = "drop"
  87. ) %>%
  88. ggplot(aes(nS, power, colour=sig_type, fill=sig_type)) +
  89. geom_hline(yintercept=0.8, linetype="dashed") +
  90. geom_ribbon(
  91. data=power_summ_recorr_0,
  92. aes(y=is_sig, group=sig_type),
  93. stat="smooth", method="glm", formula = y ~ log(x), method.args=list(family="binomial"),
  94. alpha=0.2, level=geom_ribbon_ci_level
  95. ) +
  96. # geom_line(
  97. # data=power_summ_recorr_0,
  98. # aes(y=is_sig),
  99. # stat="smooth", method="glm", formula = y ~ log(x), method.args=list(family="binomial"),
  100. # size = 0.25, alpha=1
  101. # ) +
  102. geom_point(size=1, position=position_dodge(width=1)) +
  103. geom_errorbar(aes(ymin=ci_low, ymax=ci_high), width=0, position=position_dodge(width=1), key_glyph="vpath") +
  104. scale_x_continuous(breaks = seq(0, 100, by = 10)) +
  105. scale_y_continuous(breaks = seq(0, 1, by = 0.2), limits = c(0, 1), expand=c(0,0)) +
  106. labs(
  107. x = "Number of Participants",
  108. y = "Power"
  109. ) +
  110. scale_color_manual(name = "Significance Test", values = colours) +
  111. scale_fill_manual(name = "Significance Test", values = colours) +
  112. theme(plot.margin=unit(c(0.75,0.75,0.75,0.75), "lines"))
  113. plt_power_summ
  114. ggsave("power.png", plt_power_summ, device="png", type="cairo", width=3.5, height=3, dpi=600)
  115. ggsave("power.pdf", plt_power_summ, device="pdf", width=3.5, height=3)
  116. # plot power curve
  117. draw_key_curve <- function(data, params, size, order=2.5) {
  118. x <- seq(0.1, 0.9, 0.001)
  119. y <- 1 - rev(
  120. 0.1 + (x**order - min(x**order)) /
  121. (1/0.8 * (max(x**order) - min(x**order)))
  122. )
  123. grobTree(
  124. linesGrob(
  125. x=x,
  126. y=y
  127. ),
  128. gp = gpar(
  129. col = data$colour %||% "grey20",
  130. fill = alpha(data$fill %||% "white", data$alpha),
  131. lwd = (data$linewidth %||% 0.5) * .pt,
  132. lty = data$linetype %||% 1
  133. )
  134. )
  135. }
  136. plt_power_summ_ot <- power_summ_recorr_0 |>
  137. filter(sig_type=="One-Tailed") |>
  138. group_by(nS) %>%
  139. summarise(
  140. power = mean(is_sig),
  141. ci_low = pull(tidy_binom_ci(is_sig, level=ci_level), "conf.low"),
  142. ci_high = pull(tidy_binom_ci(is_sig, level=ci_level), "conf.high"),
  143. .groups = "drop"
  144. ) %>%
  145. ggplot(aes(nS, power)) +
  146. geom_ribbon(
  147. data=filter(power_summ_recorr_0, sig_type=="One-Tailed"),
  148. aes(y=is_sig, group=sig_type, fill=sprintf("Loglinear %s%% CI", geom_ribbon_ci_level*100)),
  149. stat="smooth", method="glm", formula = y ~ log(x), method.args=list(family="binomial"),
  150. alpha=1, level=geom_ribbon_ci_level,
  151. key_glyph = draw_key_curve, colour = "#F09B0F"
  152. ) +
  153. geom_hline(yintercept=0.8, linetype="dashed") +
  154. geom_point(aes(colour="Point Estimates"), size=0.75) +
  155. geom_errorbar(aes(ymin=ci_low, ymax=ci_high, colour="Point Estimates"), width=0, position=position_dodge(width=1), key_glyph="vpath", linewidth=0.4) +
  156. scale_x_continuous(breaks = seq(0, 100, by = 20)) +
  157. scale_y_continuous(breaks = seq(0, 1, by = 0.2), limits = c(0, 1), expand=c(0,0)) +
  158. scale_fill_manual(values = "#F09B0F", guide=guide_legend(override.aes=list(linewidth=1))) +
  159. scale_colour_manual(values = "#000000") +
  160. labs(
  161. x = "N (Participants)",
  162. y = "Power",
  163. fill = NULL,
  164. colour = NULL
  165. ) +
  166. theme_classic() +
  167. theme(
  168. legend.position = c(1, 0),
  169. legend.justification = c(1, 0),
  170. legend.background = element_blank(),
  171. legend.spacing = unit(0, "pt"),
  172. legend.key.height = unit(10, "pt"),
  173. legend.key.width = unit(15, "pt"),
  174. legend.spacing.y = unit(-4, "pt"),
  175. legend.spacing.x = unit(2, "pt"),
  176. legend.key = element_blank()
  177. )
  178. ggsave("power_onetailed.png", plt_power_summ_ot, width=2, height=2, units="in", device="png", type="cairo")
  179. ggsave("power_onetailed.pdf", plt_power_summ_ot, width=2, height=2, units="in")
  180. plt_power_summ_corr_vals <- power_summ_all_recorrs %>%
  181. group_by(nS, re_corr, sig_type) %>%
  182. summarise(
  183. power = mean(is_sig),
  184. ci_low = pull(tidy_binom_ci(is_sig, level=ci_level), "conf.low"),
  185. ci_high = pull(tidy_binom_ci(is_sig, level=ci_level), "conf.high"),
  186. .groups = "drop"
  187. ) %>%
  188. ggplot(aes(nS, power, colour=re_corr, group=re_corr)) +
  189. geom_hline(yintercept=0.8, linetype="dashed") +
  190. geom_line(
  191. data=power_summ_all_recorrs,
  192. aes(y=is_sig),
  193. stat="smooth", method="glm", formula = y ~ log(x), method.args=list(family="binomial"),
  194. size = 0.5
  195. ) +
  196. scale_x_continuous(breaks = seq(0, 100, by = 20)) +
  197. scale_y_continuous(breaks = seq(0, 1, by = 0.2), limits = c(0, 1), expand=c(0,0)) +
  198. scale_colour_viridis_d() +
  199. guides(colour = guide_legend(override.aes = list(size = 1.5))) +
  200. labs(
  201. x = "Number of Participants",
  202. y = "Power",
  203. colour = "Random\nEffects\nCorrelation"
  204. ) +
  205. facet_wrap(vars(sig_type)) +
  206. theme(
  207. legend.position = "right",
  208. plot.margin=unit(c(0.75,0.75,0.75,0.75), "lines"),
  209. strip.background = element_rect(fill = "white")
  210. )
  211. plt_power_summ_corr_vals
  212. ggsave("power_corr_vals.png", plt_power_summ_corr_vals, device="png", type="cairo", width=6.5, height=2.75, dpi=600)
  213. ggsave("power_corr_vals.pdf", plt_power_summ_corr_vals, device="pdf", width=6.5, height=2.75)
  214. # fit the same models as plotted for to get predictions and results
  215. tails <- unique(power_summ$sig_type)
  216. re_corrs <- unique(power_summ$re_corr)
  217. power_m <- map(tails, function(tail) {
  218. map(re_corrs, function(re_corr_i) {
  219. power_summ %>%
  220. filter(sig_type==tail & re_corr==re_corr_i) %>%
  221. mutate(power=as.numeric(is_sig)) %>%
  222. glm(
  223. power ~ log(nS),
  224. family=binomial("logit"),
  225. data = .
  226. )
  227. }) %>%
  228. set_names(re_corrs)
  229. }) %>%
  230. set_names(tails)
  231. # get number of subjects needed for desired power
  232. poss_nS <- seq(4, 100, 4) # divisible by 4, so balanced between stimulus groups and response groups
  233. # confint(power_m[["One-Tailed"]][["0"]], level=ci_level)
  234. all_logit <- predict(power_m[["One-Tailed"]][["0"]], newdata=tibble(nS = poss_nS), se.fit=TRUE)
  235. all_logit$se.fit <- all_logit$se.fit * ci_level_norm_sd # make the standard errors into confidence intervals of ci_level
  236. all_p <- exp(all_logit$fit) / (1+exp(all_logit$fit))
  237. all_intervals_lower <- exp(all_logit$fit - all_logit$se.fit) / (1+exp(all_logit$fit - all_logit$se.fit))
  238. all_intervals_upper <- exp(all_logit$fit + all_logit$se.fit) / (1+exp(all_logit$fit + all_logit$se.fit))
  239. req_power <- 0.8
  240. dist_targ <- all_intervals_lower-req_power
  241. req_nS <- poss_nS[dist_targ == min(dist_targ[dist_targ>0])]
  242. pred_power <- all_p[poss_nS==req_nS]
  243. pred_ci <- c(all_intervals_lower[poss_nS==req_nS], all_intervals_upper[poss_nS==req_nS])
  244. # print a summary of the results
  245. power %>%
  246. filter(re_corr==0) %>%
  247. group_by(nS, re_corr) %>%
  248. summarise(
  249. n = n(),
  250. raw_onetailed = mean(p<0.1 & estimate>0),
  251. raw_twotailed = mean(p<0.05),
  252. m_estimate = mean(estimate),
  253. sd_estimate = sd(estimate),
  254. .groups = "drop"
  255. ) %>%
  256. rowwise() %>%
  257. mutate(
  258. re_corr = as.character(re_corr),
  259. logit_onetail = predict(power_m[["One-Tailed"]][[re_corr]], newdata=tibble(nS=nS)),
  260. power_onetail = exp(logit_onetail) / (1 + exp(logit_onetail)),
  261. logit_twotail = predict(power_m[["Two-Tailed"]][[re_corr]], newdata=tibble(nS=nS)),
  262. power_twotail = exp(logit_twotail) / (1 + exp(logit_twotail))
  263. )
  264. message(sprintf("We decided that we need %s participants to reach >=%s%% power. Specifically, the model predicted that at this number of subjects, we would have %s%% power (%s%% CI = [%s%%, %s%%]) to detect the predicted effect.\n", req_nS, req_power*100, round(pred_power*100, 3), ci_level*100, round(pred_ci[1]*100, 3), round(pred_ci[2]*100, 3)))