analyse_03_main_analysis.R 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. library(lme4)
  2. library(dplyr)
  3. library(purrr)
  4. library(readr)
  5. library(tidyr)
  6. library(ggplot2)
  7. library(scales)
  8. library(patchwork)
  9. library(ggnewscale)
  10. library(ggdist)
  11. library(ggforce)
  12. library(brms)
  13. ggplot2::theme_set(ggplot2::theme_classic() + theme(strip.background = element_rect(fill = "white")))
  14. cong_cols <- c("#E69F00", "#009E73")
  15. cong_cols_light <- sapply(cong_cols, function(x) {
  16. x_rgb <- as.numeric(col2rgb(x))
  17. x_li_rgb <- round(rowMeans(cbind(x_rgb*1.1, c(255, 255, 255)*0.9)))
  18. x_li <- rgb(x_li_rgb[1], x_li_rgb[2], x_li_rgb[3], maxColorValue=255)
  19. x_li
  20. }, USE.NAMES=FALSE)
  21. # function to normalise between 0 and 1
  22. norm01 <- function(x, ...) (x-min(x, ...))/(max(x, ...)-min(x, ...))
  23. # get the stimuli's percentage of name agreement values
  24. stim <- read_csv("boss.csv", col_types = cols(perc_name_agree_denom_fq_inputs = col_number())) %>%
  25. select(filename, perc_name_agree_denom_fq_inputs) %>%
  26. rename(perc_name_agree = perc_name_agree_denom_fq_inputs)
  27. # import the max electrode data from the preprocessing, and set up the variables for the model
  28. d <- list.files("max_elec_data", pattern=".+\\.csv$", full.names=TRUE) %>%
  29. map_dfr(function(f) {
  30. read_csv(
  31. f,
  32. col_types=cols(
  33. date = col_date(format = "%d/%m/%Y"),
  34. trial_save_time = col_time(format = "%H:%M:%S"),
  35. subj_id = col_character(),
  36. stim_grp = col_integer(),
  37. resp_grp = col_integer(),
  38. sex = col_character(),
  39. trl_nr = col_integer(),
  40. item_nr = col_integer(),
  41. condition = col_character(),
  42. eeg_trigg = col_integer(),
  43. image = col_character(),
  44. string = col_character(),
  45. corr_ans = col_character(),
  46. resp = col_character(),
  47. acc = col_integer(),
  48. fix1_jitt_flip = col_integer(),
  49. fix2_jitt_flip = col_integer(),
  50. event = col_integer(),
  51. is_block_start = col_logical(),
  52. is_block_end = col_logical(),
  53. .default = col_double()
  54. )
  55. ) %>%
  56. select(
  57. subj_id, stim_grp, resp_grp, item_nr, condition,
  58. image, string, acc, rt, uV, uV_noise_elec
  59. )
  60. }) %>%
  61. left_join(stim, by=c("image" = "filename")) %>%
  62. mutate(
  63. cong_dev = as.numeric(scale(ifelse(condition=="A2", 0, 1), center=TRUE, scale=FALSE)), # A1=congruent, A2=incongruent
  64. cong_dum_incong = as.numeric(condition=="A1"), # 0 is at incongruent
  65. cong_dum_cong = as.numeric(condition=="A2"), # 0 is at congruent
  66. prop_agree = perc_name_agree/100,
  67. pred_norm = norm01(prop_agree, na.rm=TRUE),
  68. pred_norm_max = pred_norm - 1
  69. )
  70. # fit the pre-registered model ----
  71. m <- lmer(
  72. uV ~ cong_dev * pred_norm +
  73. (cong_dev * pred_norm | subj_id) +
  74. (cong_dev | image) +
  75. (1 | string),
  76. REML=FALSE,
  77. control = lmerControl(optimizer="bobyqa"),
  78. data=d
  79. )
  80. # the effect of the interaction
  81. m_no_interact <- update(m, ~. -cong_dev:pred_norm)
  82. eff_interact <- anova(m, m_no_interact)
  83. # decompose interaction by congruency ----
  84. # effect of predictability in incongruent trials
  85. m_incong <- lmer(
  86. uV ~ cong_dum_incong * pred_norm +
  87. (cong_dum_incong * pred_norm | subj_id) +
  88. (cong_dum_incong | image) +
  89. (1 | string),
  90. REML=FALSE,
  91. control = lmerControl(optimizer="bobyqa"),
  92. data=d
  93. )
  94. m_incong_no_pred <- update(m_incong, ~. -pred_norm)
  95. eff_pred_incong <- anova(m_incong, m_incong_no_pred)
  96. # effect of predictability in congruent trials
  97. m_cong <- lmer(
  98. uV ~ cong_dum_cong * pred_norm +
  99. (cong_dum_cong * pred_norm | subj_id) +
  100. (cong_dum_cong | image) +
  101. (1 | string),
  102. REML=FALSE,
  103. control = lmerControl(optimizer="bobyqa"),
  104. data=d
  105. )
  106. m_cong_no_pred <- update(m_cong, ~. -pred_norm)
  107. eff_pred_cong <- anova(m_cong, m_cong_no_pred)
  108. # decompose interaction by predictability ----
  109. # effect of congruency at minimum level of predictability is in main model
  110. # effect of congruency at maximum predictability
  111. m_maxpred <- lmer(
  112. uV ~ cong_dev * pred_norm_max +
  113. (cong_dev * pred_norm_max | subj_id) +
  114. (cong_dev | image) +
  115. (1 | string),
  116. REML=FALSE,
  117. control = lmerControl(optimizer="bobyqa"),
  118. data=d
  119. )
  120. m_maxpred_no_cong <- update(m_maxpred, ~. -cong_dev)
  121. eff_cong_maxpred <- anova(m_maxpred, m_maxpred_no_cong)
  122. # plot relationship ----
  123. d_cells_pred <- tibble(
  124. pred_norm = rep(range(d$pred_norm), 2),
  125. perc_name_agree = rep(range(d$perc_name_agree), 2),
  126. cong_dev = rep(unique(d$cong_dev), each=2),
  127. condition = ifelse(cong_dev==min(cong_dev), "A2", "A1")
  128. ) %>%
  129. mutate(pred_uV = predict(m, newdata=., re.form=~0))
  130. # get prediction intervals (no random slopes for feasibility)
  131. m_no_rand_s <- lmer(
  132. uV ~ cong_dev * pred_norm +
  133. (1 | subj_id) +
  134. (1 | image) +
  135. (1 | string),
  136. REML=FALSE,
  137. control = lmerControl(optimizer="bobyqa"),
  138. data=d
  139. )
  140. d_preds_boot <- expand_grid(
  141. cong_dev = unique(d$cong_dev),
  142. perc_name_agree = seq(min(d$perc_name_agree), max(d$perc_name_agree), 0.01)
  143. ) %>%
  144. mutate(
  145. prop_agree = perc_name_agree/100,
  146. pred_norm = norm01(prop_agree, na.rm=TRUE),
  147. condition = ifelse(cong_dev==min(cong_dev), "A2", "A1")
  148. )
  149. message("Bootstrapping prediction intervals")
  150. boots <- bootMer(m_no_rand_s, nsim=5000, FUN=function(m_i) {
  151. predict(m_i, newdata=d_preds_boot, re.form=~0)
  152. }, seed=3101, .progress="txt")
  153. ci_norm <- function(x, level=c(0.025, 0.975)) {
  154. qnorm(p=level, mean=mean(x), sd=sd(x))
  155. }
  156. d_preds_boot <- d_preds_boot %>%
  157. mutate(
  158. pred_int_lwr = apply(boots$t, 2, ci_norm, level=.025),
  159. pred_int_upr = apply(boots$t, 2, ci_norm, level=.975)
  160. )
  161. lmm_plot_scatter <- d %>%
  162. ggplot(aes(perc_name_agree, uV, colour = condition)) +
  163. geom_point(shape=1, show.legend=FALSE, alpha=0.25) +
  164. scale_colour_manual(name = NA, labels = NULL, values = cong_cols_light) +
  165. new_scale_colour() +
  166. geom_line(aes(y=pred_uV, colour = condition), data=d_cells_pred, linewidth=0.9) +
  167. scale_colour_manual(name = "Picture-Word Congruency", labels = c("Congruent", "Incongruent"), values = cong_cols) +
  168. scale_y_continuous(breaks=scales::extended_breaks(n=6)) +
  169. guides(colour = "none") +
  170. labs(x = "Predictability (%)", y = "N1 Amplitude (µV)", tag="a")
  171. lmm_plot_lines <- ggplot() +
  172. geom_ribbon(aes(x=perc_name_agree, ymin=pred_int_lwr, ymax=pred_int_upr, colour=condition), data=d_preds_boot, fill=NA, linetype="dashed", show.legend = FALSE) +
  173. geom_line(aes(x=perc_name_agree, y=pred_uV, colour=condition), data=d_cells_pred, linewidth=1.75) +
  174. scale_colour_manual(name = "Picture-Word Congruency", labels = c("Congruent", "Incongruent"), values = cong_cols) +
  175. scale_y_continuous(breaks=scales::extended_breaks(n=6)) +
  176. labs(x = "Predictability (%)", y = "N1 Amplitude (µV)", tag="b") +
  177. theme(legend.position = "bottom")
  178. lmm_plot <- (lmm_plot_scatter | lmm_plot_lines) +
  179. plot_layout(guides = "collect") &
  180. theme(
  181. legend.position = "bottom",
  182. plot.background = element_blank()
  183. ) &
  184. scale_x_continuous(expand=c(0, 0))
  185. ggsave(file.path("figs", "03_lmm_summary_plot.png"), lmm_plot, device="png", type="cairo", width=4.9, height=3, dpi=600)
  186. ggsave(file.path("figs", "03_lmm_summary_plot.pdf"), lmm_plot, device="pdf", width=4.9, height=3)
  187. pred_results <- tibble(
  188. pred_norm = c(0, 1, 0, 1),
  189. perc_name_agree = c(7, 100, 7, 100),
  190. condition = c("A2", "A2", "A1", "A1"),
  191. pred_uV = c(-5, -5, -5, -4.25)
  192. )
  193. pred_plot <- ggplot() +
  194. geom_line(aes(x=perc_name_agree, y=pred_uV, colour=condition), data=pred_results, linewidth=1.75) +
  195. scale_colour_manual(name = "Picture-Word Congruency", labels = c("Congruent", "Incongruent"), values = cong_cols) +
  196. labs(x = "Predictability (%)", y = "N1 Amplitude (µV)", tag="a")
  197. pred_actual_plot <- (
  198. pred_plot + scale_y_continuous(limits=c(-6.5, -3.25), breaks=seq(-6.5, -3.25, 0.5)) + ggtitle("Predicted Results") |
  199. lmm_plot_lines + scale_y_continuous(limits=c(-5, -1.75), breaks=seq(-5, -1.75, 0.5)) + ggtitle("Observed Results")
  200. ) +
  201. plot_layout(guides = "collect") &
  202. theme(
  203. legend.position = "bottom",
  204. plot.title = element_text(hjust=0.5),
  205. plot.background = element_blank()
  206. ) &
  207. scale_x_continuous(expand=c(0, 0))
  208. ggsave(file.path("figs", "03_pred_vs_actual.png"), pred_actual_plot, device="png", type="cairo", width=6.5, height=3.75, dpi=600)
  209. ggsave(file.path("figs", "03_pred_vs_actual.pdf"), pred_actual_plot, device="pdf", width=6.5, height=3.75)
  210. pred_results_b <- tibble(
  211. pred_norm = c(0, 1, 0, 1),
  212. perc_name_agree = c(7, 100, 7, 100),
  213. condition = c("A2", "A2", "A1", "A1"),
  214. pred_uV = c(-5, -5.75, -5, -5)
  215. )
  216. pred_results_c <- tibble(
  217. pred_norm = c(0, 1, 0, 1),
  218. perc_name_agree = c(7, 100, 7, 100),
  219. condition = c("A2", "A2", "A1", "A1"),
  220. pred_uV = c(-5, -5.375, -5, -4.625)
  221. )
  222. pred_plot_b <- ggplot() +
  223. geom_line(aes(x=perc_name_agree, y=pred_uV, colour=condition), data=pred_results_b, linewidth=1.75) +
  224. scale_colour_manual(name = "Picture-Word Congruency", labels = c("Congruent", "Incongruent"), values = cong_cols) +
  225. labs(x = "Predictability (%)", y = "N1 Amplitude (µV)")
  226. pred_plot_c <- ggplot() +
  227. geom_line(aes(x=perc_name_agree, y=pred_uV, colour=condition), data=pred_results_c, linewidth=1.75) +
  228. scale_colour_manual(name = "Picture-Word Congruency", labels = c("Congruent", "Incongruent"), values = cong_cols) +
  229. labs(x = "Predictability (%)", y = "N1 Amplitude (µV)")
  230. pred_actual_plot_abc <- (
  231. pred_plot + scale_y_continuous(limits=c(-6.5, -3.25), breaks=seq(-6.5, -3.25, 0.5)) + labs(title="Expected", x=NULL, tag=NULL) |
  232. pred_plot_b + scale_y_continuous(limits=c(-6.5, -3.25), breaks=seq(-6.5, -3.25, 0.5)) + labs(title="(or)", y=NULL, x=NULL) + theme(axis.ticks.y = element_blank(), axis.text.y=element_blank()) |
  233. pred_plot_c + scale_y_continuous(limits=c(-6.5, -3.25), breaks=seq(-6.5, -3.25, 0.5)) + labs(title="(or)", y=NULL) + theme(axis.ticks.y = element_blank(), axis.text.y=element_blank(), axis.title.x=element_text(hjust=20)) |
  234. lmm_plot_lines + scale_y_continuous(limits=c(-5, -1.75), breaks=seq(-5, -1.75, 0.5)) + labs(title="Observed", y=NULL, x=NULL, tag=NULL)
  235. ) +
  236. plot_annotation(tag_levels = "a") +
  237. plot_layout(guides = "collect") &
  238. theme(
  239. legend.position = "bottom",
  240. plot.title = element_text(hjust=0.5, size=12),
  241. plot.background = element_blank()
  242. ) &
  243. scale_x_continuous(expand=c(0, 0))
  244. ggsave(file.path("figs", "03_pred_vs_actual_all_patterns.png"), pred_actual_plot_abc, device="png", type="cairo", width=6.5, height=2.75, dpi=600)
  245. ggsave(file.path("figs", "03_pred_vs_actual_all_patterns.pdf"), pred_actual_plot_abc, device="pdf", width=6.5, height=2.75)
  246. # evidences ratio for effect -----------------------------------------------
  247. p <- c(
  248. set_prior("normal(-5, 10)", class="Intercept"),
  249. set_prior("normal(0, 5)", class="b", coef="cong_dev"),
  250. set_prior("normal(0, 5)", class="b", coef="pred_norm"),
  251. set_prior("normal(0, 5)", class="b", coef="cong_dev:pred_norm"),
  252. set_prior("lkj_corr_cholesky(1)", class="L")
  253. )
  254. m_b <- brm(
  255. uV ~ cong_dev * pred_norm +
  256. (cong_dev * pred_norm | subj_id) +
  257. (cong_dev | image) +
  258. (1 | string),
  259. data = d,
  260. prior = p,
  261. chains = 5,
  262. cores = 5,
  263. iter = 5000,
  264. control = list(
  265. adapt_delta = 0.8,
  266. max_treedepth = 10
  267. ),
  268. refresh = 25,
  269. file = file.path("mods", "m_main.rds"),
  270. seed = 420
  271. )
  272. m_b_h <- hypothesis(m_b, "cong_dev:pred_norm>0")
  273. m_b_h_hdi <- as_draws_df(m_b, variable="b_cong_dev:pred_norm", regex=FALSE) %>%
  274. rename(int_est = `b_cong_dev:pred_norm`) %>%
  275. median_hdi(int_est, .width=0.89)
  276. m_b_pl <- as_draws_df(m_b, variable="b_cong_dev:pred_norm", regex=FALSE) %>%
  277. rename(int_est = `b_cong_dev:pred_norm`) %>%
  278. ggplot(aes(int_est, fill=after_stat(x>0))) +
  279. stat_slab(slab_colour="black", slab_size=0.4) +
  280. stat_pointinterval(
  281. .width=0.89, point_interval="median_hdi",
  282. point_size=1, size=1, show.legend = FALSE
  283. ) +
  284. geom_vline(xintercept=0, linetype="dashed") +
  285. geom_text(
  286. aes(x = x, y = y),
  287. data = tibble(x=-3, y=1),
  288. label = deparse(bquote(BF["01"]==.(round(1/m_b_h$hypothesis$Evid.Ratio, 2)))),
  289. parse = TRUE,
  290. hjust = 0,
  291. vjust = 1,
  292. size = 4.5
  293. ) +
  294. scale_y_continuous(expand = expansion(0, 0.04), limits = c(0, NA)) +
  295. coord_cartesian() +
  296. labs(
  297. x = "Congruency-Predictability Interaction (µV)",
  298. y = "Posterior Density"
  299. ) +
  300. theme(legend.position = "none") +
  301. scale_fill_manual(
  302. values = c("grey90", "#8B0000"),
  303. labels = c(bquote(H["0"]), bquote(H["1"]))
  304. )
  305. ggsave(file.path("figs", "03_post.png"), width=3.5, height=1.75, device="png", type="cairo")
  306. ggsave(file.path("figs", "03_post.pdf"), width=3.5, height=1.75, device="pdf")
  307. lmm_plot_post <- wrap_plots(
  308. lmm_plot,
  309. (
  310. m_b_pl +
  311. labs(
  312. fill = NULL,
  313. tag = "c",
  314. x = "Congruency-Predictability\nInteraction"
  315. ) +
  316. theme(
  317. plot.background = element_blank(),
  318. legend.position = "bottom"
  319. )
  320. )
  321. ) +
  322. plot_layout(widths = c(2, 1))
  323. ggsave(file.path("figs", "03_lmm_and_post.pdf"), lmm_plot_post, width=6.5, height=3, device="pdf")