analyse_06_localiser_results_roi.R 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # this script summarises the ERPs from the localiser task
  2. library(lme4)
  3. library(dplyr)
  4. library(purrr)
  5. library(readr)
  6. library(tidyr)
  7. library(ggplot2)
  8. library(parallel)
  9. library(patchwork)
  10. library(svglite)
  11. ggplot2::theme_set(ggplot2::theme_classic() + theme(strip.background = element_rect(fill = "white")))
  12. eff_cols <- c(
  13. "none" = "#000000",
  14. "Words vs. False Font" = "#D81B60",
  15. "Words vs. Phase-Shuffled" = "#1E88E5"
  16. )
  17. cond_cols <- c(
  18. "Words" = "#000000",
  19. "False Font" = "#D81B60",
  20. "Phase-Shuffled" = "#1E88E5"
  21. )
  22. # how many cores to parallelise over
  23. n_cores <- 12
  24. # get alist with the unique values for the times and channel names (taken from the first csv file)
  25. unique_ids <- list.files("localiser_sample_data", pattern=".+\\.csv$", full.names=TRUE) %>%
  26. first() %>%
  27. read_csv() %>%
  28. select(ch_name, time) %>%
  29. as.list() %>%
  30. lapply(unique)
  31. # function to import all data for selected channels at the selected times (exact matches)
  32. get_sample_dat <- function(channels=NA, times=NA) {
  33. list.files("localiser_sample_data", pattern=".+\\.csv$", full.names=TRUE) %>%
  34. map_dfr(function(f) {
  35. read_csv(
  36. f,
  37. col_types=cols(
  38. subj_id = col_character(),
  39. stim_grp = col_integer(),
  40. resp_grp = col_integer(),
  41. trl_nr = col_integer(),
  42. ch_name = col_character(),
  43. condition = col_character(),
  44. string = col_character(),
  45. item_nr = col_integer(),
  46. .default = col_double()
  47. ),
  48. progress = FALSE
  49. ) %>%
  50. when(
  51. is.na(channels) ~ .,
  52. ~ filter(., ch_name %in% channels)
  53. ) %>%
  54. when(
  55. is.na(times) ~ .,
  56. ~ filter(., time %in% times)
  57. ) %>%
  58. # get a unique id for each stimulus and simplify condition column
  59. mutate(
  60. condition = substr(condition, 1, 1),
  61. item_id = paste(item_nr, condition, sep="_")
  62. ) %>%
  63. # remove unused columns to save memory
  64. select(-string, -resp_grp, -stim_grp, -trl_nr)
  65. }) %>%
  66. # deviation-code condition (with word as the null)
  67. mutate(
  68. ff_dev = scale(ifelse(condition=="b", 1, 0), center=TRUE, scale=FALSE),
  69. noise_dev = scale(ifelse(condition=="n", 1, 0), center=TRUE, scale=FALSE)
  70. )
  71. }
  72. rois <- list(
  73. c("TP7", "CP5", "P5", "P7", "P9", "PO3", "PO7", "O1"),
  74. c("TP8", "CP6", "P6", "P8", "P10", "PO4", "PO8", "O2")
  75. )
  76. # get fixed effect results from models for each timepoint, for the ROI
  77. message("Importing data")
  78. d_i_list <- get_sample_dat(channels = c(rois[[1]], rois[[2]])) %>%
  79. mutate(
  80. hemis = ifelse(ch_name %in% rois[[1]], "l", "r"),
  81. hemis_dev = scale(ifelse(hemis == "l", 0, 1), center=TRUE, scale=FALSE)
  82. ) |>
  83. group_split(time)
  84. gc()
  85. message(sprintf("Fitting %g models to timecourse in parallel", length(d_i_list)))
  86. cl <- makeCluster(n_cores)
  87. cl_packages <- clusterEvalQ(cl, {
  88. library(dplyr)
  89. library(lme4)
  90. })
  91. m_fe <- parLapply(cl, d_i_list, function(d_i) {
  92. m <- lme4::lmer(
  93. uV ~ (ff_dev + noise_dev) * hemis_dev +
  94. (1 | subj_id) +
  95. (1 | ch_name:subj_id) +
  96. (1 | item_nr) +
  97. (1 | item_id),
  98. REML = FALSE,
  99. control = lme4::lmerControl(optimizer="bobyqa"),
  100. data = d_i
  101. )
  102. m %>%
  103. summary() %>%
  104. with(coefficients) %>%
  105. as_tibble(rownames = "fe")
  106. }) %>%
  107. reduce(bind_rows) %>%
  108. mutate(time = rep(unique_ids$time, each=6))
  109. stopCluster(cl)
  110. fe_res_tidy <- m_fe %>%
  111. mutate(
  112. stim_eff_lab = factor(case_when(
  113. grepl("ff_dev", fe, fixed=TRUE) ~ "Words vs. False Font",
  114. grepl("noise_dev", fe, fixed=TRUE) ~ "Words vs. Phase-Shuffled",
  115. TRUE ~ "none"
  116. ), levels = c("none", "Words vs. False Font", "Words vs. Phase-Shuffled")),
  117. main_or_int = factor(ifelse(
  118. grepl(":", fe, fixed=TRUE),
  119. "Interaction",
  120. "Main Effect"
  121. ), levels = c("Main Effect", "Interaction")),
  122. eff_lab = factor(case_when(
  123. grepl(":", fe, fixed=TRUE) ~ "Stimulus * Hemisphere",
  124. fe == "hemis_dev" ~ "Hemisphere",
  125. fe == "(Intercept)" ~ "Intercept",
  126. TRUE ~ "Stimulus"
  127. ), levels = c("Intercept", "Hemisphere", "Stimulus", "Stimulus * Hemisphere")),
  128. bound_lower = Estimate - 1.96 * `Std. Error`,
  129. bound_upper = Estimate + 1.96 * `Std. Error`
  130. )
  131. pl_eff <- fe_res_tidy %>%
  132. ggplot(aes(time, Estimate, group=stim_eff_lab, colour=stim_eff_lab, fill=stim_eff_lab)) +
  133. geom_vline(xintercept=0, linetype="dashed") +
  134. geom_hline(yintercept=0, linetype="dashed") +
  135. geom_ribbon(aes(ymin=bound_lower, ymax=bound_upper), size=0.2, alpha=0.5) +
  136. # geom_line(size=0.5) +
  137. facet_wrap(vars(eff_lab), nrow=2) +
  138. labs(x = "Time (ms)", y = "Effect Estimate (µV)", fill="Fixed Effect (95% CI)", colour="Fixed Effect (95% CI)", tag="a") +
  139. scale_x_continuous(breaks = seq(-200, 1000, 200), expand=c(0, 0)) +
  140. scale_y_continuous(breaks = scales::pretty_breaks(n = 5)) +
  141. scale_fill_manual(values = eff_cols, breaks = c("Words vs. False Font", "Words vs. Phase-Shuffled")) +
  142. scale_colour_manual(values = eff_cols, breaks = c("Words vs. False Font", "Words vs. Phase-Shuffled")) +
  143. theme(
  144. legend.position = "top",
  145. legend.key.height = unit(6, "pt"),
  146. strip.background = element_blank(),
  147. axis.line.x = element_blank()
  148. )
  149. preds <- fe_res_tidy %>%
  150. select(time, fe, Estimate) %>%
  151. pivot_wider(names_from = fe, values_from = Estimate) %>%
  152. expand_grid(
  153. condition = c("w", "b", "n"),
  154. hemis = c("l", "r")
  155. ) %>%
  156. left_join(
  157. d_i_list[[1]] %>%
  158. select(condition, hemis, hemis_dev, ff_dev, noise_dev) %>%
  159. distinct() %>%
  160. rename(
  161. ff_dev_val = ff_dev,
  162. noise_dev_val = noise_dev,
  163. hemis_dev_val = hemis_dev
  164. ),
  165. by = c("condition", "hemis")
  166. ) %>%
  167. mutate(
  168. pred_uV = `(Intercept)` +
  169. ff_dev_val * ff_dev +
  170. noise_dev_val * noise_dev +
  171. hemis_dev_val * hemis_dev +
  172. ff_dev_val * hemis_dev_val * `ff_dev:hemis_dev` +
  173. noise_dev_val * hemis_dev_val * `noise_dev:hemis_dev`,
  174. cond_lab = factor(recode(
  175. condition,
  176. w = "Words",
  177. b = "False Font",
  178. n = "Phase-Shuffled"
  179. ), levels = c("Words", "False Font", "Phase-Shuffled")),
  180. hemis_lab = factor(
  181. ifelse(hemis=="l", "Left Hemisphere", "Right Hemisphere"),
  182. levels=c("Left Hemisphere", "Right Hemisphere")
  183. )
  184. )
  185. pl_preds <- preds %>%
  186. ggplot(aes(time, pred_uV, colour=cond_lab)) +
  187. geom_vline(xintercept=0, linetype="dashed") +
  188. geom_hline(yintercept=0, linetype="dashed") +
  189. geom_line() +
  190. scale_x_continuous(breaks = seq(-200, 1000, 200), expand=c(0, 0)) +
  191. scale_y_continuous(breaks = scales::pretty_breaks(n = 5)) +
  192. scale_colour_manual(values = cond_cols) +
  193. labs(
  194. x = "Time (ms)",
  195. y = "Amplitude (µV)",
  196. colour = "Stimulus ERP",
  197. tag = "b"
  198. ) +
  199. theme(
  200. legend.position = "top",
  201. plot.margin = margin(),
  202. strip.background = element_blank(),
  203. axis.line.x = element_blank()
  204. ) +
  205. facet_wrap(vars(hemis_lab))
  206. # function to get a ggplot's legend as a ggplot object that patchwork will accept
  207. get_legend <- function(pl) {
  208. tmp <- ggplot_gtable(ggplot_build(pl))
  209. leg <- which(sapply(tmp$grobs, function(x) x$name) == "guide-box")
  210. legend <- tmp$grobs[[leg]]
  211. legend
  212. }
  213. eff_leg <- get_legend(pl_eff)
  214. stim_leg <- get_legend(pl_preds)
  215. pl_leg <- wrap_plots(list(eff_leg, stim_leg), ncol=1)
  216. pl <- (
  217. (pl_leg) /
  218. (pl_eff + theme(legend.position="none")) /
  219. (pl_preds + theme(legend.position="none"))
  220. ) +
  221. plot_layout(heights = c(0.1, 0.05, 1, 0.4))
  222. ggsave(file.path("figs", "sample_level", "localiser", "roi.svg"), pl, width=6.5, height=6.5)
  223. ggsave(file.path("figs", "sample_level", "localiser", "roi.pdf"), pl, width=6.5, height=6.5)