OSCR

No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus.

Code ↔ Paper

2 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 2 matches
  1. [1] § Methods › Pipeline 3 (Fig. 1e, Theoretical Null) ↔ postprocessing.qmd, lines 651–714 · score 0.55 · theoretical accuracy, confusion matrices, transition matrices, scores, hypothesis, training
  2. [2] § Methods › Pipeline 1 (Fig. 1c & 1e, Replication) ↔ reproduce_and_reorder_omission.py, lines 108–158 · score 0.53 · cross validation, iteration, omission, midminus, midplus, reproduce

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Quarto · 1,980 lines · 61 KB · no license · 1 match

  1. ---
  2. title: "No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus"
  3. subtitle: "A reanalysis of Demarchi et al. (Nature Communications, 2019)"
  4. author:
  5. - name: Abdoun Oussama
  6. orcid: 0000-0001-5123-4956
  7. email: [email hidden]
  8. affiliation:
  9. - name: Centre de Rercherches en Neurosciences de Lyon
  10. city: Providence
  11. - Todorov Dimitrii
  12. - Poublan-couzardot Arnaud
  13. - Tirou Coumarane
  14. - Lutz Antoine
  15. - Vernet Marine
  16. - Quentin Romain
  17. email-obfuscation: javascript
  18. abstract: ""
  19. filters:
  20. - lightbox
  21. lightbox:
  22. match: auto
  23. effect: fade
  24. loop: true
  25. format:
  26. html:
  27. toc: true
  28. toc-depth: 3
  29. toc-location: left
  30. code-fold: true
  31. code-line-numbers: true
  32. code-summary: "Source code"
  33. code-tools:
  34. source: https://github.com/romquentin/predictive_activity/blob/main/postprocessing.qmd
  35. toggle: true
  36. caption: "Code options"
  37. fig-align: center
  38. embed-resources: true
  39. link-external-newwindow: true
  40. link-external-icon: true
  41. other-links:
  42. - text: Github project home page
  43. href: https://github.com/romquentin/predictive_activity
  44. - text: Classification data
  45. href: https://doi.org/10.5281/zenodo.17699784
  46. editor: visual
  47. bibliography: references.bib
  48. ---
  49. # README
  50. **Background information**
  51. See the associated Github project [home page](https://github.com/romquentin/predictive_activity){target="_blank"}.
  52. The present R Quarto document calculates the correlation between decoding accuracy and sequence entropy, performs cluster-based permutation statistical tests and Bayesian statistics on accuracy and correlation time-generalization matrices, and plots all the figures and supplementary figures of the paper.
  53. **Instructions for use**
  54. 1. Obtain the classification data either from [Zenodo](https://doi.org/10.5281/zenodo.17699784){target="_blank"}, or by running the Python scripts on raw MEG data (see the Github project [home page](https://github.com/romquentin/predictive_activity){target="_blank"} for details). Save it in the `./data` folder.
  55. 2. For a first quick run of the script, we recommend that you set the number of cluster permutations to a low value (e.g. 100) in [**Prepare** / Parameters / Global](postprocessing.html#sec-params-global). Set it to 10,000 for stable results matching the ones reported in the publication.
  56. # **Prepare**
  57. ## Libraries
  58. ```{r}
  59. library(expm) # exponentiate matrices
  60. library(patchwork) # combine plots
  61. library(tictoc) # time executions
  62. library(correlation)
  63. library(abind) # combine multi-dimensional arrays
  64. library(BayesFactor)
  65. library(magrittr) # special pipes
  66. library(tidyverse)
  67. library(reticulate)
  68. np <- import("numpy")
  69. os <- import("os")
  70. mne <- import("mne")
  71. ```
  72. ## Parameters
  73. ### Global {#sec-params-global}
  74. ```{r}
  75. path.root <- "./data/results"
  76. list.subj <- list.dirs(path.root, full.names = F, recursive = F)
  77. n.subj <- length(list.subj)
  78. time_offset <- 670
  79. n_permutations <- 100
  80. save <- FALSE
  81. ```
  82. ### Plotting
  83. ```{r}
  84. # Temporal resolution in ms (1000 / sampling frequency)
  85. dt <- 10
  86. # Colors
  87. col.pal.base <- rev(pals::brewer.rdbu(10))
  88. col.pal.diff <- rev(pals::brewer.brbg(10))
  89. col.pal.corr <- pals::ocean.curl(100)
  90. col.lines <- "grey40"
  91. col.clusters <- "grey10"
  92. # Breaks & Labels
  93. # --- function
  94. format_labels_acc <- function(labels) {
  95. labels[seq(2,length(labels),2)] <- ""
  96. labels <- str_replace(labels, "0.", ".")
  97. }
  98. # --- base
  99. acc.breaks.base <- seq(.21,.29, .005)
  100. acc.labels.base <- format_labels_acc(acc.breaks.base)
  101. # --- diff
  102. acc.breaks.diff <- seq(-.02,.02, .005)
  103. acc.labels.diff <- format_labels_acc(acc.breaks.diff)
  104. # --- correlations
  105. corr.breaks <- seq(-1,1,.2)
  106. corr.labels <- str_replace(corr.breaks, "0.", ".")
  107. # GGPLOT theme
  108. theme_set(theme_minimal())
  109. theme_update(text = element_text(family = "Arial"),
  110. plot.tag.position = c(0, 0.95),
  111. plot.tag = element_text(size = 14, hjust = 0, vjust = 1, face = "bold"))
  112. ```
  113. ### Transition matrices
  114. From @demarchi2019
  115. ```{r}
  116. mat.T <- list(RD = matrix(rep(.25, 16), ncol = 4, byrow = T),
  117. MM = matrix(), MP = matrix(), OR = matrix())
  118. mat.T$MM <- matrix(c(c(.25,0,.37,.38),
  119. c(.38,.25,0,.37),
  120. c(.37,.38,.25,0),
  121. c(0,.37,.38,.25)),
  122. ncol = 4, byrow = T)
  123. mat.T$MP <- matrix(c(c(.25,0,.15,.60),
  124. c(.60,.25,0,.15),
  125. c(.15,.60,.25,0),
  126. c(0,.15,.60,.25)),
  127. ncol = 4, byrow = T)
  128. mat.T$OR <- matrix(c(c(.25,0,0,.75),
  129. c(.75,.25,0,0),
  130. c(0,.75,.25,0),
  131. c(0,0,.75,.25)),
  132. ncol = 4, byrow = T)
  133. covCT <- function(mat.C, mat.T) {
  134. covar <- 0
  135. for (i in 1:dim(mat.C)[1]) {
  136. covar <- covar + mean((mat.C[i,]-mean(mat.C[i,]))*(mat.T[i,]-mean(mat.T[i,])))
  137. }
  138. return(covar)
  139. }
  140. ```
  141. ## Functions
  142. ### Matrix to df
  143. ```{r}
  144. matrix_to_df2 <- function(array, row.name, col.name, val.name) {
  145. array %>%
  146. as_tibble() %>%
  147. rownames_to_column(row.name) %>%
  148. pivot_longer(-row.name, names_to = col.name, values_to = val.name) %>%
  149. mutate("{col.name}" := str_remove(!!sym(col.name), "V")) %>%
  150. mutate_all(as.numeric) %>%
  151. return
  152. }
  153. # !!! USE as.data.frame.table() instead
  154. ```
  155. ### Batch data loading
  156. ```{r}
  157. load_loop_subj <- function(root = path.root, folder, condition, diag = F, crop=T) {
  158. # Individual scores
  159. if (folder == "omissions") {
  160. array.scores <- array(dim = c(141,141,n.subj))
  161. } else if (folder == "sounds") {
  162. array.scores <- array(dim = c(33,135,n.subj))
  163. }
  164. i <- 1
  165. for (s in list.subj) {
  166. file <- os$path$join(root, s, folder,
  167. paste0("cv_", condition, "_scores.npy"))
  168. # Proceed to next subject if file doesn't exist
  169. if (!os$path$isfile(file)) {
  170. cat(file, " does not exist !!!\n")
  171. next
  172. }
  173. # Load data otherwise
  174. tmp <- np$load(file)
  175. # Average across CV folds if present
  176. if (length(dim(tmp)) == 3) {
  177. tmp <- tmp %>% apply(c(2,3), mean) # average across CV folds
  178. }
  179. array.scores[,,i] <- tmp
  180. i <- i+1
  181. }
  182. # Average across participants
  183. t_train_offset <- case_when(folder == "omissions" ~ time_offset,
  184. folder == "sounds" ~ 0)
  185. t_test_offset <- time_offset
  186. df.mean <- array.scores %>%
  187. apply(c(1,2), mean) %>%
  188. matrix_to_df2(row.name = "t_train", col.name = "t_test", val.name = "accuracy") %>%
  189. # --- convert indices to time
  190. mutate(t_train = dt*(t_train-1) - t_train_offset,
  191. t_test = dt*(t_test-1) - t_test_offset)
  192. # --- crop time if requested (default)
  193. if (crop) {
  194. df.mean %<>% filter(t_train>=0, (t_train-333) < dt)
  195. }
  196. return(list(array.scores, df.mean))
  197. }
  198. ```
  199. ### Correlations
  200. ```{r}
  201. ## Correlation function between two vectors
  202. correlations_vecs <- function(vec1, vec2) {
  203. cor(as.vector(vec1), as.vector(vec2))
  204. }
  205. # Bind time-generalization matrices from different entropy conditions into a single 3D array
  206. correlations_extract3D <- function(data, idx.lines, idx.subj) {
  207. arr.tmp <- data[[idx.lines[1],"array"]][[1]][,,idx.subj]
  208. for (i in 2:length(idx.lines)) {
  209. arr.tmp <- abind(arr.tmp, data[[idx.lines[i],"array"]][[1]][,,idx.subj], along = 3)
  210. }
  211. return(arr.tmp)
  212. }
  213. # Replace NAs by 0 and extreme values by +/- .99
  214. correlations_fixval <- function(arr.corr) {
  215. arr.corr[is.na(arr.corr)] <- 0
  216. arr.corr[arr.corr == 1] <- .99
  217. arr.corr[arr.corr == -1] <- -.99
  218. return(arr.corr)
  219. }
  220. ```
  221. ### Cluster permutation stats
  222. ```{r}
  223. get_signif_clust2d <- function(X, n_permutations = 2**12, n_jobs = -1) {
  224. # run permutaiton tests
  225. mne$stats$spatio_temporal_cluster_1samp_test(X,
  226. n_permutations = n_permutations,
  227. n_jobs = as.integer(n_jobs),
  228. out_type = "mask",
  229. verbose = FALSE) -> tmp
  230. names(tmp) <- c("t_obs","clusters","cluster_pv","H0")
  231. # Extract clusters: the final output is a 0/1 matrix, 1 indicating belonging to a significant cluster
  232. # --- get ids of significant clusters
  233. idx.signif <- which(tmp$cluster_pv < .05)
  234. # --- aggregate clusters
  235. signif <- array(0, dim = dim(tmp$t_obs))
  236. for (i in idx.signif) {
  237. signif <- signif + tmp$clusters[[i]]
  238. }
  239. return(1*(signif > 0))
  240. }
  241. # Loop through multiple conditions, stored in a dataframe where each row contains a 3D array in an "array" column
  242. # Other columns are appended to the output as descriptors of the condition
  243. cluster_loop_cond <- function(df, H0 = 0, n = 2**12, n_jobs = -1, crop = FALSE) {
  244. # Initialize significant clusters dataframe
  245. df.clusters <- tibble()
  246. # Loop through all conditions
  247. for (i in 1:nrow(df)) {
  248. # Extract array of subject-level time-generalized accuracy
  249. X <- df[[i,"array"]][[1]]
  250. # --- crop to stimulus N-1 if requested
  251. if (crop) {X <- X[,35:68,]}
  252. # --- reshape to how MNE expects it
  253. X <- X %>% np$moveaxis(as.integer(2), as.integer(0))
  254. df.clusters %<>% bind_rows(
  255. get_signif_clust2d(X-H0, n_jobs = n_jobs, n_permutations = n) %>%
  256. matrix_to_df2(row.name = "t_train", col.name = "t_test", val.name = "signif") %>%
  257. bind_cols(select(df,-array)[i,])
  258. )
  259. }
  260. return(df.clusters)
  261. }
  262. ```
  263. ### Difference original-reordered
  264. ```{r}
  265. calculate_diff <- function(df, f.map, values_to) {
  266. df %>%
  267. mutate(manip = case_match(manip, "" ~ "original", "_reord" ~ "reordered")) %>%
  268. pivot_wider(names_from = manip, values_from = values_to) %>%
  269. mutate(diff = f.map(original, reordered, .f=\(x,y){x-y})) %>%
  270. pivot_longer(cols = c(original, reordered, diff), names_to = "manip", values_to = values_to)
  271. }
  272. ```
  273. ### Base plot
  274. ```{r}
  275. plot_base <- function(df.plot,
  276. z = "accuracy",
  277. contour = TRUE,
  278. col.contour = col.clusters,
  279. col.pal = rev(pals::brewer.rdbu(10)),
  280. z.breaks = acc.breaks.base,
  281. z.labels = acc.labels.base,
  282. legend.position = "bottom") {
  283. # Onset lines
  284. lines.v.pos <- 330*seq(round(min(df.plot$t_test)/333), round(max(df.plot$t_test)/333), 1)
  285. lines.v.pos <- lines.v.pos[lines.v.pos!=0]
  286. lines.h.pos <- 330*seq(round(min(df.plot$t_train)/333), round(max(df.plot$t_train)/333), 1)
  287. lines.h.pos <- lines.h.pos[lines.h.pos!=0]
  288. # Plot
  289. g <- df.plot %>%
  290. ggplot(aes(x = t_test, y = t_train)) +
  291. # --- heatmap
  292. geom_tile(aes_string(fill = z)) +
  293. # --- sounds onsets
  294. geom_vline(xintercept = lines.v.pos, linetype = 2, color = col.lines) +
  295. geom_hline(yintercept = lines.h.pos, linetype = 2, color = col.lines) +
  296. geom_vline(xintercept = 0, color = col.lines) +
  297. geom_hline(yintercept = 0, color = col.lines) +
  298. scale_x_continuous(breaks = scales::pretty_breaks(12),
  299. expand = c(0,0)) +
  300. scale_y_continuous(breaks = scales::pretty_breaks(4),
  301. expand = c(0,0)) +
  302. scale_fill_stepsn(colors = col.pal,
  303. breaks = z.breaks,
  304. labels = z.labels,
  305. limits = c(min(z.breaks), max(z.breaks)),
  306. oob = scales::squish) +
  307. labs(x = "Test time (ms)", y = "Train time (ms)") +
  308. coord_equal(clip = "off") +
  309. theme(# Text size
  310. plot.title = element_text(size = 12, hjust = 0.5, face = "bold"),
  311. plot.subtitle = element_text(size = 9, hjust = 0.5),
  312. axis.title = element_text(size = 9),
  313. legend.title = element_text(size = 9),
  314. strip.text = element_text(size = 9),
  315. axis.text = element_text(size = 5),
  316. legend.text = element_text(size = 5.5),
  317. # Time ticks
  318. axis.ticks = element_line(color = "black"),
  319. axis.ticks.length = unit(0.1, "cm"),
  320. # Legend
  321. legend.position = legend.position,
  322. legend.box.margin = margin(0,0,0,0),
  323. legend.box.spacing = unit(0, "pt"),
  324. # no grid
  325. panel.grid = element_blank(),
  326. # don't clip facet titles
  327. strip.clip = "off")
  328. if (legend.position %in% c("bottom","top")) {
  329. g <- g +
  330. guides(fill = guide_colorbar(barwidth = 0.5*length(z.breaks-1), barheight = 0.5,
  331. ticks = F, title.vjust = 1))
  332. } else {
  333. g <- g +
  334. guides(fill = guide_colorbar(barheight = 0.5*length(z.breaks-1), barwidth = 0.5,
  335. ticks = F, title.vjust = 1))
  336. }
  337. # --- significant clusters
  338. if (contour) {
  339. g <- g +
  340. geom_contour(aes(z = signif), size = 0.1, color = col.contour)
  341. }
  342. return(g)
  343. }
  344. ```
  345. # **Replication & Reordering (empirical null)**
  346. ## Load data
  347. ### Sounds
  348. ```{r}
  349. #| cache: true
  350. tic()
  351. # Load subject-level data (used for clustering) and derive grand average (used for visualization)
  352. df.stim.mean <- tibble()
  353. df.stim.subj <- tibble()
  354. for (manip in c("","_reord")) {
  355. for (direction in c("rd_to_rd", "rd_to_mm", "rd_to_mp", "rd_to_or")) {
  356. tmp <- load_loop_subj(folder = "sounds",
  357. condition = paste0(direction, manip))
  358. df.stim.mean %<>% bind_rows(tmp[[2]] %>% mutate(manip = manip, direction = direction))
  359. df.stim.subj %<>% bind_rows(tibble(manip = manip, direction = direction, array = list(tmp[[1]])))
  360. }
  361. }
  362. df.stim.mean %<>% mutate(stim = "sounds")
  363. df.stim.subj %<>% mutate(stim = "sounds") %>%
  364. # --- center decoding accuracy on 0
  365. mutate(array = map(array, .f=\(x){x-0.25}))
  366. toc() # ~1s
  367. ```
  368. ### Omissions
  369. ```{r}
  370. #| cache: true
  371. tic()
  372. df.omit.mean <- tibble()
  373. df.omit.subj <- tibble()
  374. for (manip in c("","_reord")) {
  375. for (direction in c("rd_to_rd", "rd_to_mm", "rd_to_mp", "rd_to_or")) {
  376. tmp <- load_loop_subj(folder = "omissions",
  377. condition = paste0(direction, manip))
  378. df.omit.mean %<>% bind_rows(tmp[[2]] %>% mutate(manip = manip, direction = direction))
  379. df.omit.subj %<>% bind_rows(tibble(manip = manip, direction = direction, array = list(tmp[[1]])))
  380. }
  381. }
  382. df.omit.mean %<>% mutate(stim = "omissions") %>% filter(abs(t_test) <= 670)
  383. df.omit.subj %<>% mutate(stim = "omissions") %>%
  384. # --- center decoding accuracy on 0
  385. mutate(array = map(array, .f=\(x){x-0.25})) %>%
  386. # --- crop subject-level data for cluster analysis
  387. mutate(array = map(array, .f=\(x){x[71:104,4:138,]}))
  388. toc() # ~ 2s
  389. ```
  390. ## Correlations with entropy
  391. ```{r}
  392. #| cache: true
  393. #| warning: false
  394. ## Prepare input data
  395. df.tmp <- bind_rows(df.stim.subj, df.omit.subj) %>%
  396. calculate_diff(f.map = map2, values_to = "array") %>%
  397. mutate(regularity = str_remove(direction, "rd_to_"),
  398. regularity = factor(regularity, levels = c("rd","mm","mp","or"))) %>%
  399. select(-direction) %>%
  400. arrange(as.numeric(regularity))
  401. df.tmp
  402. tic()
  403. df.corr.mean <- tibble()
  404. df.corr.subj <- tibble()
  405. ## Loop through condition "manip" and stimulus type "stim
  406. for (mm in c("original","reordered","diff")) {
  407. for (ss in c("sounds", "omissions")) { #
  408. cat("\n\nProcessing ", str_to_upper(mm), " / ", str_to_upper(ss), "...\n", sep="")
  409. # --- Loop through subjects to derive a 3D array of participant-level time-generalized correlations with entropy, from a 3D array of participant-level accuracy matrix
  410. for (idx.subj in 1:n.subj) {
  411. # --- bind time-generalization matrices for the 3 non-random conditions into a single 3D array
  412. # (add accuracy data from the random condition if we are processing sounds)
  413. if (ss == "sounds") {
  414. idx.lines = 1:4
  415. } else if (ss == "omissions") {
  416. idx.lines = 2:4
  417. }
  418. arr.tmp <- correlations_extract3D(data = filter(df.tmp, manip == mm, stim == ss),
  419. idx.lines = idx.lines, idx.subj)
  420. # --- calculate correlation with entropy level coded as 0,1,2(,3), obtaining a 2D array
  421. if (ss == "sounds") {
  422. arr.corr.tmp <- apply(arr.tmp, 1:2, correlations_vecs, vec2 = 0:3)
  423. } else if (ss == "omissions") {
  424. arr.corr.tmp <- apply(arr.tmp, 1:2, correlations_vecs, vec2 = 1:3)
  425. }
  426. # --- append to already processed subjects in a 3D array
  427. if (idx.subj == 1) {
  428. arr.corr <- arr.corr.tmp
  429. } else {
  430. arr.corr %<>% abind(arr.corr.tmp, along = 3)
  431. }
  432. }
  433. # Postprocessing
  434. arr.corr %<>%
  435. correlations_fixval %>% # replace NAs by 0 and extreme values by +/- .99
  436. atanh # apply Fisher's transformation
  437. # --- store participant-level results in a dataframe
  438. df.corr.subj %<>% bind_rows(tibble(manip = mm, stim = ss, array = list(arr.corr)))
  439. # --- calculate groupe average and store in a dataframe
  440. df.corr.mean %<>% bind_rows(
  441. arr.corr %>%
  442. apply(c(1,2), mean) %>% # average across participants
  443. tanh %>% # convert back to correlation coefficients
  444. matrix_to_df2(row.name = "t_train", col.name = "t_test", val.name = "r") %>%
  445. mutate(manip = mm, stim = ss)
  446. )
  447. }
  448. }
  449. toc() # ~20 s
  450. ```
  451. ## Clusters stats
  452. ### Accuracy
  453. ```{r}
  454. #| cache: true
  455. #| lightbox:
  456. #| group: global
  457. tic()
  458. df.tmp.subj <- bind_rows(df.stim.subj, df.omit.subj)
  459. df.tmp.mean <- bind_rows(df.stim.mean, df.omit.mean)
  460. # Cluster analysis
  461. df.clusters.0 <- df.tmp.subj %>%
  462. calculate_diff(f.map = map2, values_to = "array") %>%
  463. cluster_loop_cond(n = n_permutations) %>%
  464. # --- convert indices to time
  465. mutate(t_test = (dt*(t_test-1)-time_offset)) %>%
  466. mutate(t_train = (dt*(t_train-1)))
  467. # Quick check of clusters with a mask plot
  468. df.clusters.0 %>%
  469. mutate(direction = (direction %>% str_replace("rd_to_", "") %>% str_to_upper)) %>%
  470. ggplot(aes(x = t_test, y = t_train, fill = signif)) +
  471. facet_grid(direction ~ manip) +
  472. geom_tile() +
  473. coord_equal()
  474. # Aggregate all data and clean
  475. df.plot.0 <- df.tmp.mean %>%
  476. calculate_diff(f.map = map2_dbl, values_to = "accuracy") %>%
  477. # --- append cluster significance
  478. left_join(df.clusters.0) %>%
  479. mutate(manip = (recode_factor(manip, original = "ORIGINAL", reordered = "REORDERED",
  480. diff = "ORIGINAL – REORDERED"))) %>%
  481. mutate(direction = toupper(sub("rd_to_", "", direction)),
  482. direction = factor(direction, levels = rev(c("RD","MM","MP","OR"))))
  483. toc() # ~3.5 minutes
  484. ```
  485. ### Correlations
  486. ```{r}
  487. #| cache: true
  488. #| lightbox:
  489. #| group: global
  490. tic()
  491. # Cluster analysis
  492. df.clusters.corr <- df.corr.subj %>%
  493. cluster_loop_cond(n = n_permutations)
  494. # Quick check of clusters with a mask plot
  495. df.clusters.corr %>%
  496. ggplot(aes(x = t_test, y = t_train, fill = signif)) +
  497. facet_grid( ~ manip) +
  498. geom_tile() +
  499. coord_equal()
  500. # --- append cluster significance
  501. df.plot.0.corr <- df.corr.mean %>%
  502. left_join(df.clusters.corr) %>%
  503. mutate(manip = (recode_factor(manip, original = "ORIGINAL", reordered = "REORDERED",
  504. diff = "ORIGINAL – REORDERED"))) %>%
  505. # --- convert indices to time
  506. mutate(t_test = (dt*(t_test-1)-time_offset)) %>%
  507. mutate(t_train = (dt*(t_train-1)))
  508. df.plot.0.corr %>% summary
  509. toc() # ~ 1 min
  510. ```
  511. # **Theoretical null**
  512. ## Load confusion matrices & calculate
  513. ```{r}
  514. #| cache: true
  515. tic()
  516. null_accuracy <- function(mat.confusion, mat.transition) {
  517. # Calculate accuracy under the null hypothesis from the transition and confusion matrices mat.T and mat.C
  518. # The 3/4 factor is used to compensate the unbiased estimator used in cov() by default (see ?cov)
  519. return(0.25 + sum(diag(3/4*cov(t(mat.transition), t(mat.confusion)))))
  520. }
  521. df.maths.subj <- tibble()
  522. df.maths.mean <- tibble()
  523. for (entropy in c("RD","MM","MP","OR")) {
  524. array.scores <- array(0.25, dim = c(33,136,n.subj))
  525. i <- 1
  526. for (s in list.subj) {
  527. # --- load the confusion matrix
  528. tmp.file <- os$path$join(path.root, s, "rd_to_rd_confmats.npz") %>%
  529. np$load()
  530. # --- crop around S0 (decoded stimulus)
  531. mat.C <- tmp.file$f[["arr_0"]][,71:103,71:104,,] %>%
  532. apply(c(2,3,4,5), mean)
  533. # NOTE: because the transition matrix is the same for all subjects, theoretical accuracy can be calculated from the group average of the confusion matrix (faster)
  534. # --- calculate theoretical accuracy
  535. # --- for S-2
  536. array.scores[1:33,1:34,i] <- mat.C %>%
  537. apply(c(1,2), null_accuracy, mat.transition = mat.T[[entropy]]%^%2)
  538. # --- for S-1
  539. array.scores[1:33,35:68,i] <- mat.C %>%
  540. apply(c(1,2), null_accuracy, mat.transition = mat.T[[entropy]]%^%1)
  541. # --- for S
  542. array.scores[1:33,69:102,i] <- mat.C %>%
  543. apply(c(1,2), \(x)mean(diag(x)))
  544. # --- for S+1
  545. array.scores[1:33,103:136,i] <- mat.C %>%
  546. apply(c(1,2), null_accuracy, mat.transition = t(mat.T[[entropy]]))
  547. i <- i+1
  548. }
  549. array.scores <- array.scores[,2:136,]
  550. # Append to other entropy levels in dataframe
  551. # --- subject-level data
  552. df.maths.subj %<>% bind_rows(tibble(array = list(array.scores), entropy = entropy))
  553. # --- group-averaged data
  554. df.maths.mean %<>% bind_rows(array.scores %>%
  555. apply(c(1,2), mean) %>%
  556. matrix_to_df2(row.name = "t_train", col.name = "t_test", val.name = "accuracy") %>%
  557. mutate(entropy = entropy)
  558. )
  559. }
  560. toc() # ~ 1min
  561. ```
  562. ## Append original data & calculate diff
  563. ```{r}
  564. df.maths.all <-
  565. # --- aggregate original, theoretical and difference between the 2
  566. bind_rows(df.stim.subj %>%
  567. # --- select original analysis
  568. filter(manip == "") %>% mutate(manip = "original") %>%
  569. # --- crop to the same time-window than for theoretical analysis
  570. mutate(entropy = toupper(str_replace(direction,"rd_to_",""))),
  571. df.maths.subj %>%
  572. mutate(manip = "maths") %>%
  573. mutate(array = map(array, .f=\(x){x-0.25}))) %>%
  574. select(manip, entropy, array) %>%
  575. pivot_wider(names_from = manip, values_from = "array") %>%
  576. mutate(diff = map2(original, maths, .f=\(x,y){x-y})) %>%
  577. pivot_longer(cols = c(original, maths, diff), names_to = "manip", values_to = "array")
  578. ```
  579. ## Correlations with entropy
  580. ```{r}
  581. #| cache: true
  582. #| warning: false
  583. tic()
  584. df.corr.maths.mean <- tibble()
  585. df.corr.maths.subj <- tibble()
  586. for (mm in c("original","maths","diff")) {
  587. # --- Loop through subjects to derive a 3D array of participant-level time-generalized correlations with entropy, from a 3D array of participant-level accuracy matrix
  588. for (idx.subj in 1:n.subj) {
  589. # --- bind time-generalization matrices of the 4 conditions into a single 3D array
  590. arr.tmp <- correlations_extract3D(data = df.maths.all %>% filter(manip == mm),
  591. idx.lines = 1:4, idx.subj)
  592. # --- calculate correlation with entropy level coded as 0,1,2,3, obtaining a 2D array
  593. arr.corr.tmp <- apply(arr.tmp, 1:2, correlations_vecs, vec2 = 0:3)
  594. # --- append to already processed subjects in a 3D array
  595. if (idx.subj == 1) {
  596. arr.corr <- arr.corr.tmp
  597. } else {
  598. arr.corr %<>% abind(arr.corr.tmp, along = 3)
  599. }
  600. }
  601. # Postprocessing
  602. arr.corr %<>%
  603. correlations_fixval %>% # replace NAs by 0 and extreme values by +/- .99
  604. atanh # apply Fisher's transformation
  605. # --- store participant-level results in a dataframe of arrays
  606. df.corr.maths.subj %<>% bind_rows(tibble(manip = mm, array = list(arr.corr)))
  607. # --- calculate group average and store in a dataframe
  608. df.corr.maths.mean %<>% bind_rows(
  609. arr.corr %>%
  610. apply(c(1,2), mean) %>% # average across participants
  611. tanh %>% # convert back to correlation coefficients
  612. matrix_to_df2(row.name = "t_train", col.name = "t_test", val.name = "r") %>%
  613. mutate(manip = mm)
  614. )
  615. }
  616. toc() # ~ 20s
  617. ```
  618. ## Clusters stats
  619. ### Accuracy
  620. ```{r}
  621. #| cache: true
  622. #| lightbox:
  623. #| group: global
  624. tic()
  625. df.clusters.maths <- df.maths.all %>%
  626. cluster_loop_cond(n = n_permutations) %>%
  627. # --- convert indices to time
  628. mutate(t_test = (dt*(t_test-1)-time_offset)) %>%
  629. mutate(t_train = (dt*(t_train-1)))
  630. toc() # ~ 2 min
  631. # Quick check of clusters with a mask plot
  632. df.clusters.maths %>%
  633. ggplot(aes(x = t_test, y = t_train, fill = signif)) +
  634. facet_grid(entropy ~ manip) +
  635. geom_tile() +
  636. coord_equal()
  637. ```
  638. ### Correlations
  639. ```{r}
  640. #| cache: true
  641. #| lightbox:
  642. #| group: global
  643. tic()
  644. df.clusters.corr.maths <- df.corr.maths.subj %>%
  645. cluster_loop_cond(n = n_permutations)
  646. toc() # ~ 20s
  647. # Quick check of clusters with a mask plot
  648. df.clusters.corr.maths %>%
  649. ggplot(aes(x = t_test, y = t_train, fill = signif)) +
  650. facet_grid( ~ manip) +
  651. geom_tile() +
  652. coord_equal()
  653. # --- append cluster significance
  654. df.plot.maths.corr <- df.corr.maths.mean %>%
  655. left_join(df.clusters.corr.maths) %>%
  656. mutate(manip = (recode_factor(manip, original = "ORIGINAL", maths = "THEORETICAL NULL",
  657. diff = "ORIGINAL – THEORETICAL NULL"))) %>%
  658. # --- convert indices to time
  659. mutate(t_test = (dt*(t_test-1)-time_offset)) %>%
  660. mutate(t_train = (dt*(t_train-1)))
  661. df.plot.maths.corr %>% summary
  662. ```
  663. # **Prediction of the most likely ("simple prediction")**
  664. ## Load data
  665. ```{r}
  666. #| cache: true
  667. tic()
  668. # Load subject-level data and derive grand average
  669. df.stim.ml.mean <- tibble()
  670. df.stim.ml.subj <- tibble()
  671. for (manip in c("","_reord")) {
  672. for (direction in c("rd_to_mm", "rd_to_mp", "rd_to_or")) {
  673. tmp <- load_loop_subj(folder = "sounds",
  674. condition = paste0(direction, manip, "_sp"))
  675. df.stim.ml.mean %<>% bind_rows(tmp[[2]] %>% mutate(manip = manip, direction = direction))
  676. df.stim.ml.subj %<>% bind_rows(tibble(manip = manip, direction = direction, array = list(tmp[[1]])))
  677. }
  678. }
  679. df.stim.ml.mean %<>% mutate(stim = "sounds")
  680. df.stim.ml.subj %<>% mutate(stim = "sounds") %>%
  681. # --- center decoding accuracy on 0
  682. mutate(array = map(array, .f=\(x){x-0.25}))
  683. toc() # ~ 1s
  684. df.stim.ml.mean %>% mutate_if(is.character, as.factor) %>% summary
  685. ```
  686. ## Correlations with entropy
  687. ```{r}
  688. #| cache: true
  689. tic()
  690. ## Prepare input data
  691. df.tmp <- df.stim.ml.subj %>%
  692. calculate_diff(f.map = map2, values_to = "array") %>%
  693. mutate(entropy = str_remove(direction, "rd_to_"),
  694. entropy = factor(entropy, levels = c("rd","mm","mp","or"))) %>%
  695. select(-direction) %>%
  696. arrange(as.numeric(entropy))
  697. ## Calculate correlations
  698. df.corr.ml.mean <- tibble()
  699. df.corr.ml.subj <- tibble()
  700. # --- Loop through condition "manip" and stimulus type "stim"
  701. for (mm in c("original","reordered","diff")) {
  702. for (ss in c("sounds")) {
  703. cat("\n\nProcessing ", str_to_upper(mm), " / ", str_to_upper(ss), "...\n", sep="")
  704. # --- Loop through subjects to derive a 3D array of participant-level time-generalized correlations with entropy, from a 3D array of participant-level accuracy matrix
  705. for (idx.subj in 1:n.subj) {
  706. # --- bind time-generalization matrices for the 3 non-random conditions into a single 3D array
  707. arr.tmp <- correlations_extract3D(data = filter(df.tmp, manip == mm, stim == ss),
  708. idx.lines = 1:3, idx.subj)
  709. # --- calculate correlation with entropy level coded as 0,1,2, obtaining a 2D array
  710. arr.corr.tmp <- apply(arr.tmp, 1:2, correlations_vecs, vec2 = 0:2)
  711. # --- append to already processed subjects in a 3D array
  712. if (idx.subj == 1) {
  713. arr.corr <- arr.corr.tmp
  714. } else {
  715. arr.corr %<>% abind(arr.corr.tmp, along = 3)
  716. }
  717. }
  718. # Post-processing
  719. arr.corr <- arr.corr %>%
  720. correlations_fixval %>% # replace NAs by 0 and extreme values by +/- .99
  721. atanh # apply Fisher's transformation
  722. # --- store participant-level results in a dataframe
  723. df.corr.ml.subj %<>% bind_rows(tibble(manip = mm, stim = ss, array = list(arr.corr)))
  724. # --- calculate groupe average and store in a dataframe
  725. df.corr.ml.mean %<>% bind_rows(
  726. arr.corr %>%
  727. apply(c(1,2), mean) %>% # average across participants
  728. tanh %>% # convert back to correlation coefficients
  729. matrix_to_df2(row.name = "t_train", col.name = "t_test", val.name = "r") %>%
  730. mutate(manip = mm, stim = ss)
  731. )
  732. }
  733. }
  734. toc() # ~ 15s
  735. ```
  736. ## Cluster stats
  737. ### Accuracy
  738. ```{r}
  739. #| cache: true
  740. #| lightbox:
  741. #| group: global
  742. tic()
  743. ## Cluster analysis with subject-level data
  744. # --- calculate original-reordered difference
  745. tmp.diff <- df.stim.ml.subj %>%
  746. calculate_diff(f.map = map2, values_to = "array")
  747. # --- combine & run cluster analysis
  748. df.clusters.ml <- tmp.diff %>% #bind_rows(tmp.diff, tmp.ave) %>%
  749. cluster_loop_cond(n = n_permutations) %>%
  750. # --- convert indices to time
  751. mutate(t_test = (dt*(t_test-1)-time_offset)) %>%
  752. mutate(t_train = (dt*(t_train-1)))
  753. # Quick check of clusters with a mask plot
  754. df.clusters.ml %>%
  755. mutate(direction = (direction %>% str_replace("rd_to_", "") %>% str_to_upper)) %>%
  756. ggplot(aes(x = t_test, y = t_train, fill = signif)) +
  757. facet_grid(direction ~ manip) +
  758. geom_tile() +
  759. coord_equal()
  760. ## Grand average data
  761. # --- calculate original-reordered difference
  762. tmp.diff <- df.stim.ml.mean %>%
  763. calculate_diff(f.map = map2_dbl, values_to = "accuracy")
  764. # --- calculate average of difference across entropy levels
  765. tmp.ave <- tmp.diff %>%
  766. filter(manip == "diff") %>%
  767. group_by(t_train, t_test, manip, stim) %>%
  768. summarise(accuracy = mean(accuracy, na.rm=T)) %>%
  769. mutate(direction = "average of\n(MM,MP,OR)")
  770. # --- combine
  771. df.plot.ml <- tmp.diff #bind_rows(tmp.diff, tmp.ave)
  772. ## Aggregate all data and clean
  773. df.plot.ml %<>%
  774. # --- append cluster significance
  775. left_join(df.clusters.ml) %>%
  776. mutate(manip = (recode_factor(manip, original = "ORIGINAL", reordered = "REORDERED",
  777. diff = "ORIGINAL – REORDERED"))) %>%
  778. mutate(direction = toupper(sub("rd_to_", "", direction))) %>%
  779. mutate(direction = factor(direction, levels = rev(c("AVERAGE OF\n(MM,MP,OR)","MM","MP","OR"))))
  780. toc() # ~1.5min
  781. ```
  782. ### Correlations
  783. ```{r}
  784. #| cache: true
  785. #| lightbox:
  786. #| group: global
  787. tic()
  788. df.clusters.corr.ml <- df.corr.ml.subj %>%
  789. cluster_loop_cond(n = n_permutations)
  790. toc() # ~ 30s
  791. # Quick check of clusters with a mask plot
  792. df.clusters.corr.ml %>%
  793. ggplot(aes(x = t_test, y = t_train, fill = signif)) +
  794. facet_grid( ~ manip) +
  795. geom_tile() +
  796. coord_equal()
  797. # --- append cluster significance
  798. df.plot.ml.corr <- df.corr.ml.mean %>%
  799. filter(between(t_test,35,68)) %>%
  800. left_join(df.clusters.corr.ml) %>%
  801. mutate(manip = (recode_factor(manip, original = "ORIGINAL", reordered = "REORDERED",
  802. diff = "ORIGINAL – REORDERED"))) %>%
  803. # --- convert indices to time
  804. mutate(t_test = (dt*(t_test-1)-time_offset)) %>%
  805. mutate(t_train = (dt*(t_train-1)))
  806. df.plot.ml.corr %>% summary
  807. ```
  808. # **Figures**
  809. ## Methodological plots
  810. ### Transition matrices (fig. 1a)
  811. ```{r}
  812. #| output: false
  813. # Convert & combine transition matrices into a dataframe
  814. df.plot <- tibble()
  815. for (condition in c("RD","MM","MP","OR")) {
  816. df.plot %<>% bind_rows(
  817. mat.T[[condition]] %>%
  818. matrix_to_df2(row.name = "from", col.name = "to", val.name = "p") %>%
  819. mutate(condition = condition)
  820. )
  821. }
  822. df.plot %<>%
  823. mutate(condition = factor(condition, names(mat.T)))
  824. df.plot.pitch <- bind_rows(tibble(x = 0.1, y = 1:4, pitch = 1:4),
  825. tibble(y = 0.1, x = 1:4, pitch = 1:4))
  826. # Plot
  827. df.plot %>%
  828. ggplot(aes(x = to, y = from)) +
  829. facet_wrap(~ condition, nrow = 1, strip.position = "bottom") +
  830. # --- heatmap
  831. geom_tile(aes(fill = p), color = "black") +
  832. # --- display percentages
  833. geom_text(data = . %>% filter(p<=.25),
  834. aes(label = paste0(100*p,"%")),
  835. color = "black", size = 2) +
  836. geom_text(data = . %>% filter(p>.25),
  837. aes(label = paste0(100*p,"%")),
  838. color = "white", size = 2) +
  839. # --- display color-coded pitch in axes
  840. geom_point(data = df.plot.pitch,
  841. aes(x, y, color = pitch), size = 4, show.legend = FALSE) +
  842. geom_text(data = df.plot.pitch,
  843. aes(x, y, label = LETTERS[pitch]), color = "white", size = 2, show.legend = FALSE) +
  844. scale_color_viridis_c(end = 0.8) +
  845. scale_x_continuous(name = "to\n", position = "top") +
  846. scale_y_reverse(name = "from\n") +
  847. scale_fill_gradient(name = "transition probability",
  848. low = "white", high = "black",
  849. labels = ~str_replace(., "^0.","."),
  850. limits = c(0,1)) +
  851. guides(fill = guide_colorbar(barheight = 0.5,
  852. ticks = F, title.vjust = 1)) +
  853. coord_equal(xlim = c(0.5,4.5), ylim = rev(c(0.5,4.5)),
  854. expand = F, clip = "off") +
  855. theme(legend.position = "bottom",
  856. legend.box.margin = margin(t = -20),
  857. # text size
  858. axis.title = element_text(size = 9),
  859. legend.title = element_text(size = 9),
  860. strip.text = element_text(size = 12),
  861. # remove axis text
  862. axis.text = element_blank(),
  863. # Positions & margins
  864. axis.title.x = element_text(hjust = 0.08),
  865. panel.spacing.x = unit(0.8,"cm")) -> g.transitions
  866. g.transitions
  867. ```
  868. ### Reordering (fig. 1b)
  869. ```{r}
  870. #| output: false
  871. # Generate Markov sequences
  872. n.samples <- 12
  873. stims <- list(RD = c(), OR = c())
  874. set.seed(128764) #20476, 99713
  875. for (entropy in c("RD","OR")) {
  876. stims[[entropy]] <- vector("numeric", n.samples)
  877. stims[[entropy]][1] <- sample(1:4, 1)
  878. for (i in 2:n.samples) {
  879. stims[[entropy]][i] <- sample(1:4, 1, prob = mat.T[[entropy]][stims[[entropy]][i-1],])
  880. }
  881. }
  882. # Add global sequence indices, class-specific indices and offset according to pitch
  883. df.plot <- stims %>% as.tibble() %>%
  884. mutate(idx = as.double(1:n.samples)) %>%
  885. pivot_longer(-idx, names_to = "entropy", values_to = "class") %>%
  886. mutate(entropy = factor(entropy, levels = c("OR","RD"))) %>%
  887. group_by(entropy,class) %>% mutate(rank = rank(idx)) %>%
  888. mutate(y = as.double(entropy) + class/16-10/64)
  889. pitch.y <- sort(unique(df.plot$y))
  890. # Plot
  891. df.plot %>%
  892. ggplot(aes(x = idx, y = y, color = class)) +
  893. # --- pitch lines
  894. geom_hline(yintercept = pitch.y, color = "grey90") +
  895. # --- pitch labels
  896. annotate(geom = "text", label = "pitch",
  897. x = 12.5, y = c(mean(head(pitch.y,4)), mean(tail(pitch.y,4))),
  898. hjust = 0.5, vjust = 2.5, size = 2.5, color = "grey50", angle = 90) +
  899. annotate(geom = "text", label = "low",
  900. x = 12.5, y = c(min(head(pitch.y,4)), min(tail(pitch.y,4))),
  901. hjust = 0.74, vjust = 1.2, size = 2, color = "grey50", angle = 90) +
  902. annotate(geom = "text", label = "high",
  903. x = 12.5, y = c(max(head(pitch.y,4)), max(tail(pitch.y,4))),
  904. hjust = 0.25, vjust = 1.2, size = 2, color = "grey50", angle = 90) +
  905. # --- reordering trajectories
  906. ggh4x::geom_pointpath(data = df.plot %>% arrange(desc(entropy)),
  907. aes(x = idx, y = y, group = interaction(class,rank)),
  908. mult = 0.4, color = "black", linewidth = 0.3,
  909. arrow = arrow(type = "closed", angle = 25, length=unit(.15, 'cm'))) +
  910. # --- stimuli
  911. geom_point(size = 4, show.legend = F) +
  912. # --- stimuli ranks
  913. geom_text(aes(label = rank), size = 2, color = "white") +
  914. scale_x_continuous(name = "trial number", breaks = 1:n.samples) +
  915. scale_y_continuous(breaks = c(1,2), labels = c("OR","RD"), position = "left") +
  916. scale_color_viridis_c(end = 0.8) +
  917. coord_cartesian(clip = "off") +
  918. theme(axis.title.y = element_blank(),
  919. axis.text.y = element_text(size = 10, color = "black", margin = margin(r=-2)),
  920. axis.title.x = element_text(size = 9, color = "black"),
  921. panel.grid = element_blank(),
  922. panel.grid.major.x = element_line(color = "grey80", linewidth = 0.2)) -> g.reordering
  923. g.reordering
  924. ```
  925. ### Prediction types (fig. 2a)
  926. Presented vs. most likely
  927. ```{r}
  928. #| output: false
  929. # Generate Markov sequences
  930. n.samples <- 6
  931. stims <- mat.T
  932. set.seed(12871)
  933. for (entropy in names(stims)) {
  934. stims[[entropy]] <- vector("numeric", n.samples)
  935. stims[[entropy]][1] <- 4#sample(1:4, 1)
  936. for (i in 2:n.samples) {
  937. stims[[entropy]][i] <- sample(1:4, 1, prob = mat.T[[entropy]][stims[[entropy]][i-1],])
  938. }
  939. }
  940. # Add sequence index & predictions
  941. df.plot <- stims %>% as.tibble() %>%
  942. mutate(idx = as.double(1:n.samples)) %>%
  943. pivot_longer(-idx, names_to = "entropy", values_to = "class") %>%
  944. mutate(entropy = factor(entropy, levels = rev(names(stims)))) %>%
  945. group_by(entropy) %>% mutate(actual = lead(class),
  946. mostlikely = ifelse(class==1, 4, class-1)) %>%
  947. pivot_longer(c(actual,mostlikely), names_to = "status", values_to = "class.pred") %>%
  948. filter(entropy != "RD")
  949. # Plot
  950. df.plot %>%
  951. ggplot(aes(x = idx, y = class)) +
  952. facet_wrap(~ entropy, ncol = 1, strip.position = "right") +
  953. geom_segment(data = df.plot %>% filter(status == "mostlikely", idx != n.samples),
  954. aes(x = idx, xend = idx+1, y = class, yend = class.pred, linetype = as.factor(sign(class.pred-class))),
  955. #arrow = arrow(type = "closed", length = unit(6,"pt")),
  956. color = "grey50", show.legend = F) +
  957. geom_point(aes(fill = class), size = 3, shape = 21, color = "white", show.legend = T) +
  958. geom_point(data = df.plot %>% filter(status == "mostlikely", idx != n.samples),
  959. aes(x = idx+1, y = class.pred, color = class.pred),
  960. size = 2.5, shape = 8) +
  961. scale_x_continuous(name = "trial number", breaks = 1:n.samples) +
  962. scale_y_continuous(name = "") +
  963. scale_color_viridis_c(end = 0.8, guide = "legend", name = "", labels = c("","","","most likely")) +
  964. scale_fill_viridis_c(end = 0.8, guide = "legend", name = "", labels = c("","","","actually presented")) +
  965. guides(linetype = F) +
  966. coord_cartesian(clip = "off") +
  967. expand_limits(x = c(0.9,6.1), y = c(0.3,4.7)) +
  968. guides(#color = guide_legend(order = 1, override.aes = list(color = "grey50")),
  969. color = guide_legend(order = 1),
  970. fill = guide_legend(order = 2)) +
  971. theme(plot.caption = element_text(size = 6),
  972. axis.text.y = element_blank(),
  973. axis.title.x = element_text(size = 9, color = "black"),
  974. strip.text = element_text(size = 12),
  975. panel.spacing = unit(0.2,"cm"),
  976. panel.grid = element_blank(),
  977. panel.border = element_rect(fill=NA),
  978. legend.position = "top",
  979. legend.box = "vertical",
  980. legend.box.just = "left",
  981. legend.margin = margin(b = -10),
  982. legend.text = element_text(margin = margin(r = -12, l=0, unit = "pt"))
  983. ) -> g.mostlikely
  984. g.mostlikely
  985. ```
  986. ## Figure 1 & Supplementary Figure 2
  987. ### Initialize
  988. ```{r}
  989. stims <- list(RD = c(), OR = c())
  990. g.base <- list(sound = c(), omission = c())
  991. g.diff <- list(sound = c(), omission = c())
  992. # Create ad hoc dataframes for onset' labels
  993. df.labels.onset <- list(
  994. sounds = tibble(label = "0 = sound onset",
  995. manip = factor(c("REPLICATION\n", "EMPIRICAL NULL\n",
  996. "REPLICATION – EMPIRICAL NULL\n")),
  997. direction = factor("OR", levels = names(stims))),
  998. omissions = tibble(label = "0 = omission onset",
  999. manip = factor(c("REPLICATION\n", "EMPIRICAL NULL\n",
  1000. "REPLICATION – EMPIRICAL NULL\n")),
  1001. direction = factor("OR", levels = names(stims)))
  1002. )
  1003. # Function to add onset' labels to a plot
  1004. add_onset <- function(g, df.labels) {
  1005. g <- g + geom_text(data = df.labels %>% filter(manip %in% unique(g$data$manip)),
  1006. aes(label = label, x = 0, y = 350),
  1007. hjust = 0, vjust = -0.5, size = 3, fontface = "italic")
  1008. return(g)
  1009. }
  1010. ```
  1011. ### Original & empirical null: accuracy plots
  1012. ```{r}
  1013. #| warning: false
  1014. #| lightbox:
  1015. #| group: global
  1016. for (s in c("sounds","omissions")) { #
  1017. # ORIGINAL & REORDERED
  1018. df.plot.0 %>%
  1019. filter(stim == s, manip != "ORIGINAL – REORDERED") %>%
  1020. mutate(manip = recode_factor(manip,
  1021. ORIGINAL = "REPLICATION\n",
  1022. REORDERED = "EMPIRICAL NULL\n")) %>%
  1023. plot_base(col.pal = col.pal.base,
  1024. z.breaks = acc.breaks.base,
  1025. z.labels = acc.labels.base) +
  1026. facet_grid(direction ~ manip) -> g
  1027. g.base[["acc"]][[s]] <- g %>% add_onset(df.labels.onset[[s]])
  1028. # DIFFERENCE
  1029. g.diff[["acc"]][[s]] <- df.plot.0 %>%
  1030. filter(stim == s, manip == "ORIGINAL – REORDERED") %>%
  1031. mutate(manip = recode_factor(manip,
  1032. `ORIGINAL – REORDERED` = "REPLICATION – EMPIRICAL NULL\n")) %>%
  1033. plot_base(col.pal = col.pal.diff,
  1034. z.breaks = acc.breaks.diff,
  1035. z.labels = acc.labels.diff) +
  1036. facet_grid(direction ~ manip) +
  1037. theme(axis.title.y = element_blank()) -> g
  1038. g.diff[["acc"]][[s]] <- g %>% add_onset(df.labels.onset[[s]])
  1039. }
  1040. # Assemble Figure 1c
  1041. g.fig1.1.acc <- (
  1042. g.base[["acc"]][["sounds"]] +
  1043. (g.diff[["acc"]][["sounds"]] + plot_layout(tag_level = "new")) +
  1044. plot_layout(widths = c(2, 1)) &
  1045. theme(strip.text.x = element_text(face = "bold"))
  1046. )
  1047. g.fig1.1.acc
  1048. # Assemble Supplementary Figure 1a
  1049. g.supfig2.acc <- (
  1050. g.base[["acc"]][["omissions"]] +
  1051. (g.diff[["acc"]][["omissions"]] + plot_layout(tag_level = "new")) +
  1052. plot_layout(widths = c(2, 1)) &
  1053. theme(strip.text.x = element_text(face = "bold"))
  1054. )
  1055. g.supfig2.acc
  1056. ```
  1057. ### Original & empirical null: correlations plots
  1058. ```{r}
  1059. #| warning: false
  1060. #| lightbox:
  1061. #| group: global
  1062. for (s in c("sounds", "omissions")) {
  1063. # ORIGINAL & REORDERED
  1064. df.plot.0.corr %>%
  1065. filter(stim == s, manip != "ORIGINAL – REORDERED") %>%
  1066. mutate(manip = recode_factor(manip,
  1067. ORIGINAL = "REPLICATION\n", # (original dataset)\n
  1068. REORDERED = "EMPIRICAL NULL\n")) %>% # (reordered RD)\n
  1069. mutate(direction = "") %>%
  1070. plot_base(col.pal = col.pal.corr,
  1071. z = "r",
  1072. z.breaks = corr.breaks,
  1073. z.labels = corr.labels) +
  1074. labs(fill = "Correlation") +
  1075. facet_grid(direction ~ manip) -> g
  1076. g.base[["corr"]][[s]] <- g #%>% add_onset(df.labels.onset[[s]] %>% mutate(direction = ""))
  1077. # DIFFERENCE
  1078. df.plot.0.corr %>%
  1079. filter(stim == s, manip == "ORIGINAL – REORDERED") %>%
  1080. mutate(manip = recode_factor(manip,
  1081. `ORIGINAL – REORDERED` = "REPLICATION – EMPIRICAL NULL\n")) %>%
  1082. mutate(direction = "") %>%
  1083. plot_base(col.pal = col.pal.corr,
  1084. z = "r",
  1085. z.breaks = corr.breaks,
  1086. z.labels = corr.labels) +
  1087. facet_grid(. ~ manip) +
  1088. labs(fill = "Correlation") +
  1089. theme(axis.title.y = element_blank()) -> g
  1090. g.diff[["corr"]][[s]] <- g #%>% add_onset(df.labels.onset[[s]] %>% mutate(direction = ""))
  1091. }
  1092. # Assemble Figure 1d
  1093. g.fig1.1.corr <- (
  1094. g.base[["corr"]][["sounds"]] +
  1095. (g.diff[["corr"]][["sounds"]] + plot_layout(tag_level = "new")) +
  1096. plot_layout(widths = c(2, 1), guides = "collect") &
  1097. theme(strip.text = element_blank(),
  1098. legend.position = "bottom",
  1099. legend.margin = margin(t = -20, l = 60))
  1100. )
  1101. g.fig1.1.corr
  1102. # Assemble Supplementary Figure 1a
  1103. g.supfig2.corr <- (
  1104. g.base[["corr"]][["omissions"]] +
  1105. (g.diff[["corr"]][["omissions"]] + plot_layout(tag_level = "new")) +
  1106. plot_layout(widths = c(2, 1)) &
  1107. theme(strip.text.x = element_text(face = "bold"))
  1108. )
  1109. g.supfig2.corr
  1110. ```
  1111. ### Theoretical null: accuracy plots
  1112. ```{r}
  1113. ## Aggregate all data and clean
  1114. df.plot.maths <- bind_rows(
  1115. # --- make datasets compatible
  1116. df.maths.mean %>% mutate(manip = "maths") %>%
  1117. mutate(t_train = dt*(t_train-1) - 0,
  1118. t_test = dt*(t_test-1) - time_offset),
  1119. df.stim.mean %>%
  1120. filter(manip == "") %>%
  1121. mutate(manip = "original") %>%
  1122. mutate(entropy = toupper(sub("rd_to_", "", direction))) %>%
  1123. # mutate(across(starts_with("t_"), ~(dt*(.-1)-700))) %>%
  1124. select(-stim, -direction)
  1125. ) %>%
  1126. # --- calculate difference
  1127. pivot_wider(names_from = manip, values_from = "accuracy") %>%
  1128. mutate(diff = original - maths) %>%
  1129. pivot_longer(cols = c(original, maths, diff),
  1130. names_to = "manip", values_to = "accuracy") %>%
  1131. # --- crop time window
  1132. filter(t_train>=0, (t_train-333) < dt) %>%
  1133. # --- aggregate results of cluster analysis
  1134. left_join(df.clusters.maths) %>%
  1135. # --- clean labels
  1136. mutate(manip = (recode_factor(manip, original = "REPLICATION\n", maths = "THEORETICAL NULL\n",
  1137. diff = "REPLICATION – THEORETICAL NULL\n")),
  1138. direction = factor(entropy, levels = rev(c("RD","MM","MP","OR"))))
  1139. ```
  1140. ```{r}
  1141. #| warning: false
  1142. #| lightbox:
  1143. #| group: global
  1144. ## PLOT
  1145. # Initialize subplots
  1146. g.maths.base <- list()
  1147. g.maths.diff <- list()
  1148. # --- ORIGINAL & THEORETICAL NULL
  1149. df.plot.maths %>%
  1150. filter(manip != "REPLICATION – THEORETICAL NULL\n") %>%
  1151. plot_base(col.pal = col.pal.base,
  1152. z.breaks = acc.breaks.base,
  1153. z.labels = acc.labels.base) +
  1154. facet_grid(direction ~ manip) -> g
  1155. g.maths.base[["acc"]] <- g %>% add_onset(df.labels.onset[["sounds"]] %>%
  1156. mutate(manip = str_replace(manip, "EMPIRICAL", "THEORETICAL")))
  1157. # --- DIFFERENCE
  1158. df.plot.maths %>%
  1159. filter(manip == "REPLICATION – THEORETICAL NULL\n") %>%
  1160. plot_base(col.pal = col.pal.diff,
  1161. z.breaks = acc.breaks.diff,
  1162. z.labels = acc.labels.diff) +
  1163. facet_grid(direction ~ manip) +
  1164. theme(axis.title.y = element_blank()) -> g
  1165. g.maths.diff[["acc"]] <- g %>% add_onset(df.labels.onset[["sounds"]] %>%
  1166. mutate(manip = str_replace(manip, "EMPIRICAL", "THEORETICAL")))
  1167. g.fig1.2.acc <- (
  1168. g.maths.base[["acc"]] +
  1169. (g.maths.diff[["acc"]] + plot_layout(tag_level = "new")) +
  1170. plot_layout(widths = c(2, 1)) &
  1171. theme(strip.text.x = element_text(face = "bold"))
  1172. )
  1173. g.fig1.2.acc
  1174. ```
  1175. ### Theoretical null: correlations plots
  1176. ```{r}
  1177. #| warning: false
  1178. #| lightbox:
  1179. #| group: global
  1180. df.plot <- df.plot.maths.corr %>%
  1181. mutate(manip = str_replace(manip, "ORIGINAL", "REPLICATION"),
  1182. manip = paste0(manip, "\n"))
  1183. # ORIGINAL & THEORETICAL
  1184. df.plot %>%
  1185. filter(manip != "REPLICATION – THEORETICAL NULL\n") %>%
  1186. mutate(direction = "") %>%
  1187. plot_base(col.pal = col.pal.corr,
  1188. z = "r",
  1189. z.breaks = corr.breaks,
  1190. z.labels = corr.labels) +
  1191. labs(fill = "Correlation") +
  1192. facet_grid(direction ~ manip) -> g
  1193. g.maths.base[["corr"]] <- g
  1194. # DIFFERENCE
  1195. df.plot %>%
  1196. filter(manip == "REPLICATION – THEORETICAL NULL\n") %>%
  1197. mutate(direction = "") %>%
  1198. plot_base(col.pal = col.pal.corr,
  1199. z = "r",
  1200. z.breaks = corr.breaks,
  1201. z.labels = corr.labels) +
  1202. facet_grid(. ~ manip) +
  1203. labs(fill = "Correlation") +
  1204. theme(axis.title.y = element_blank()) -> g
  1205. g.maths.diff[["corr"]] <- g
  1206. g.fig1.2.corr <- (
  1207. g.maths.base[["corr"]] +
  1208. (g.maths.diff[["corr"]] + plot_layout(tag_level = "new")) +
  1209. plot_layout(widths = c(2, 1), guides = "collect") &
  1210. theme(strip.text.x = element_blank(),
  1211. legend.position = "bottom",
  1212. legend.margin = margin(t = -20, l = 60))
  1213. )
  1214. g.fig1.2.corr
  1215. ```
  1216. ### Assemble Figure 1
  1217. ```{r}
  1218. #| fig-width: 7
  1219. #| fig-height: 11
  1220. #| warning: false
  1221. #| lightbox:
  1222. #| group: global
  1223. ((g.transitions + theme(panel.spacing.x = unit(5,"mm"), legend.box.margin = margin(t = -30))) +
  1224. g.reordering + theme(axis.text.x = element_text(margin = margin(t = -15))) +
  1225. plot_layout(widths = c(1.7, 1))) /
  1226. ((g.fig1.1.acc / g.fig1.1.corr & theme(axis.text.x = element_text(angle = 45, hjust = 1))) +
  1227. plot_layout(heights = c(4.7, 1))) /
  1228. ((g.fig1.2.acc / g.fig1.2.corr & theme(axis.text.x = element_text(angle = 45, hjust = 1))) +
  1229. plot_layout(heights = c(4.7, 1))) +
  1230. plot_layout(heights = c(1., 4, 4)) +
  1231. plot_annotation(tag_levels = "a") -> g.fig1
  1232. g.fig1
  1233. if (save) {
  1234. g.fig1 %>% ggsave(filename = "./figures/fig1_MattersArising.pdf", width = 7, height = 11, device = cairo_pdf)
  1235. }
  1236. ```
  1237. ### Assemble Supplementary Figure 2 (omissions)
  1238. ```{r}
  1239. #| fig-width: 7
  1240. #| fig-height: 5.5
  1241. #| warning: false
  1242. ((g.supfig2.acc / g.supfig2.corr & theme(axis.text.x = element_text(angle = 45, hjust = 1))) +
  1243. plot_layout(heights = c(4, 1))) +
  1244. plot_annotation(tag_levels = "a") -> g.supfig2
  1245. g.supfig2
  1246. if (save) {
  1247. g.supfig2 %>% ggsave(filename = "./figures/supfig2_MattersArising.pdf", width = 7, height = 5.5, device = cairo_pdf)
  1248. g.supfig2 %>% ggsave(filename = "./figures/supfig2_MattersArising.png", width = 7, height = 5.5, device = ragg::agg_png(), dpi = 600)
  1249. }
  1250. ```
  1251. ## Figure 2
  1252. ### Initialize
  1253. ```{r}
  1254. g.base.ml <- list()
  1255. g.diff.ml <- list()
  1256. ```
  1257. ### Build accuracy plots
  1258. ```{r}
  1259. #| lightbox:
  1260. #| group: global
  1261. for (s in c("sounds")) {
  1262. # ORIGINAL & REORDERED
  1263. df.plot.ml %>%
  1264. filter(t_test >=-333, t_test <= 0) %>%
  1265. filter(stim == s, manip != "ORIGINAL – REORDERED") %>%
  1266. mutate(manip = recode_factor(manip,
  1267. ORIGINAL = "ORIGINAL",
  1268. REORDERED = "EMPIRICAL NULL")) %>%
  1269. plot_base(col.pal = col.pal.base,
  1270. z.breaks = acc.breaks.base,
  1271. z.labels = acc.labels.base,
  1272. legend.position = "right") +
  1273. facet_grid(direction ~ manip) +
  1274. labs(title = paste0("Decoding MOST LIKELY sounds")) +
  1275. theme(plot.title = element_text(hjust = -0.2)) -> g.base.ml[["acc"]][[s]]
  1276. # DIFFERENCE
  1277. df.plot.ml %>%
  1278. filter(t_test >=-333, t_test <= 0) %>%
  1279. filter(!grepl("AVERAGE",direction)) %>%
  1280. filter(stim == s, manip == "ORIGINAL – REORDERED") %>%
  1281. mutate(manip = "DIFF") %>%
  1282. plot_base(col.pal = col.pal.diff,
  1283. z.breaks = acc.breaks.diff,
  1284. z.labels = acc.labels.diff,
  1285. legend.position = "right") +
  1286. facet_grid(direction ~ manip) +
  1287. theme(axis.title.y = element_blank()) -> g.diff.ml[["acc"]][[s]]
  1288. }
  1289. (g.base.ml[["acc"]][["sounds"]] + (g.diff.ml[["acc"]][["sounds"]] + plot_layout(tag_level = "new")) + plot_layout(widths = c(2, 1))) +
  1290. theme(plot.tag.position = c(0, 1)) -> g.fig2.acc
  1291. g.fig2.acc
  1292. ```
  1293. ### Build correlations plots
  1294. ```{r}
  1295. #| lightbox:
  1296. #| group: global
  1297. for (s in c("sounds")) {
  1298. # ORIGINAL & REORDERED
  1299. df.plot.ml.corr %>%
  1300. filter(t_test >=-333, t_test <= 0) %>%
  1301. filter(stim == s, manip != "ORIGINAL – REORDERED") %>%
  1302. mutate(manip = recode_factor(manip,
  1303. ORIGINAL = "ORIGINAL",
  1304. REORDERED = "EMPIRICAL NULL")) %>%
  1305. mutate(direction = "") %>%
  1306. plot_base(col.pal = col.pal.corr,
  1307. z = "r",
  1308. z.breaks = corr.breaks,
  1309. z.labels = corr.labels,
  1310. legend.position = "right") +
  1311. labs(fill = "Correlation") +
  1312. facet_grid(direction ~ manip) -> g.base.ml[["corr"]][[s]]
  1313. # DIFFERENCE
  1314. df.plot.ml.corr %>%
  1315. filter(t_test >=-333, t_test <= 0) %>%
  1316. filter(stim == s, manip == "ORIGINAL – REORDERED") %>%
  1317. mutate(manip = "DIFF") %>%
  1318. mutate(direction = "") %>%
  1319. plot_base(col.pal = col.pal.corr,
  1320. z = "r",
  1321. z.breaks = corr.breaks,
  1322. z.labels = corr.labels,
  1323. legend.position = "right") +
  1324. facet_grid(. ~ manip) +
  1325. labs(fill = "Correlation") +
  1326. theme(axis.title.y = element_blank()) -> g.diff.ml[["corr"]][[s]]
  1327. }
  1328. (g.base.ml[["corr"]][["sounds"]] + g.diff.ml[["corr"]][["sounds"]] + plot_layout(widths = c(2, 1))) +
  1329. # plot_annotation(tag_levels = list("b.")) &
  1330. theme(plot.tag.position = c(0, 1)) -> g.fig2.corr
  1331. g.fig2.corr
  1332. ```
  1333. ### Assemble figure 2
  1334. ```{r}
  1335. #| fig-width: 8
  1336. #| fig-height: 5.5
  1337. #| warning: false
  1338. #| lightbox:
  1339. #| group: global
  1340. (g.mostlikely + coord_fixed(0.5))+
  1341. plot_spacer() +
  1342. ((g.fig2.acc / (g.fig2.corr + plot_layout(tag_level = 'new'))) +
  1343. plot_layout(heights = c(3, 1)) &
  1344. theme(plot.title = element_text(vjust = -8),
  1345. legend.box.margin = margin(l=-5),
  1346. strip.clip = "off",
  1347. strip.text = element_text(size = 8),
  1348. axis.text.x = element_text(angle = 45, hjust = 1))
  1349. ) +
  1350. plot_layout(widths = c(1, 0.1, 2)) +
  1351. plot_annotation(tag_levels = "a") &
  1352. theme(plot.tag.position = c(0, 1),
  1353. plot.tag = element_text(vjust = 3)) -> g.fig2
  1354. g.fig2
  1355. if (save) {
  1356. g.fig2 %>% ggsave(filename = "./figures/fig2_MattersArising.pdf", width = 8, height = 5.5, device = cairo_pdf)
  1357. }
  1358. ```
  1359. ## Supplementary Figure 1
  1360. ### Prepare data
  1361. ```{r}
  1362. # Load subject-level data (used for clustering) and derive grand average (used for visualization) **without cropping**
  1363. tic()
  1364. df.supfig1.mean <- tibble()
  1365. df.supfig1.subj <- tibble()
  1366. for (manip in c("","_reord")) {
  1367. for (direction in c("rd_to_rd", "rd_to_mm", "rd_to_mp", "rd_to_or")) {
  1368. tmp <- load_loop_subj(folder = "sounds",
  1369. condition = paste0(direction, manip),
  1370. crop = FALSE)
  1371. df.supfig1.mean %<>% bind_rows(tmp[[2]] %>% mutate(manip = manip, direction = direction))
  1372. df.supfig1.subj %<>% bind_rows(tibble(manip = manip, direction = direction, array = list(tmp[[1]])))
  1373. }
  1374. }
  1375. toc() # ~1s
  1376. ```
  1377. ### Plot
  1378. ```{r}
  1379. #| fig-width: 4
  1380. #| fig-height: 2.5
  1381. #| lightbox:
  1382. #| group: global
  1383. # Extract the diagonal elements from the temporal generalization matrix
  1384. tmp <- filter(df.supfig1.subj, direction == "rd_to_rd", manip == "")[[1, "array"]][[1]]
  1385. tmp.diag <- matrix(nrow = dim(tmp)[3], ncol = dim(tmp)[1])
  1386. for (i in 1:dim(tmp)[3]) {
  1387. tmp.diag[i,] <- tmp[,,i] %>% diag
  1388. }
  1389. # Plot
  1390. df.supfig1.mean %>%
  1391. filter(t_train == t_test,
  1392. direction == "rd_to_rd",
  1393. manip == "") %>%
  1394. mutate(sd = tmp.diag %>% apply(MARGIN = 2, FUN = sd) %>% `/`(sqrt(33))) %>%
  1395. filter(t_train >= -300, t_train <= 700) %>%
  1396. ggplot(aes(x = t_train, y = accuracy)) +
  1397. geom_hline(yintercept = 0.25, color = "grey50", linetype = 2) +
  1398. geom_ribbon(aes(ymin = accuracy-sd, ymax = accuracy+sd), alpha = .3) +
  1399. geom_line(size = 0.8) +
  1400. scale_x_continuous(breaks = scales::pretty_breaks(10), name = "Time (ms)") +
  1401. scale_y_continuous(breaks = seq(.20, .40, .02), limits = c(.2,.4), name = "Accuracy") +
  1402. coord_cartesian(expand = F)-> g.supfig1
  1403. g.supfig1
  1404. if (save) {
  1405. g.supfig1 %>% ggsave(filename = "./figures/supfig1_MattersArising.pdf", width = 4, height = 2.5, device = cairo_pdf)
  1406. g.supfig1 %>% ggsave(filename = "./figures/supfig1_MattersArising.png", width = 4, height = 2.5, device = ragg::agg_png(), dpi = 600)
  1407. }
  1408. ```
  1409. ## Supplementary Figure 3
  1410. ### Functions & parameters
  1411. ```{r}
  1412. # Function to calculate and extract BF
  1413. getBF01 <- function(y, prior = "medium") {
  1414. ttestBF(y, rscale = prior) %>% extractBF(onlybf = T)
  1415. }
  1416. # Color palette for BF
  1417. col.pal.bf <- rev(pals::brewer.piyg(7)); col.pal.bf[c(4)] <- "grey50";
  1418. ```
  1419. ### Prepare data
  1420. ```{r}
  1421. tic()
  1422. ## BF for EMPIRICAL null, DIFF
  1423. # --- extract array of subject-level correlation between accuracy and entropy
  1424. X <- df.corr.subj[[3,"array"]][[1]]
  1425. # --- calculate BF for each cell of the TG matrix
  1426. bf.corr.diff.empir <- apply(X, c(1,2), getBF01, simplify = TRUE)
  1427. ## BF for THEORETICAL null, DIFF
  1428. # --- extract array of subject-level correlation between accuracy and entropy
  1429. X <- df.corr.maths.subj[[3,"array"]][[1]]
  1430. # --- calculate BF for each cell of the TG matrix
  1431. bf.corr.diff.maths <- apply(X, c(1,2), getBF01, simplify = TRUE)
  1432. ## BF for MOST LIKELY null, DIFF
  1433. # --- extract array of subject-level correlation between accuracy and entropy
  1434. X <- df.corr.ml.subj[[3,"array"]][[1]]
  1435. # --- calculate BF for each cell of the TG matrix
  1436. bf.corr.diff.ml <- apply(X, c(1,2), getBF01, simplify = TRUE)
  1437. toc() # ~1.5min
  1438. ## Post-processing
  1439. df.plot <- bind_rows(
  1440. # --- aggregate data
  1441. left_join(matrix_to_df2(bf.corr.diff.empir, "t_train", "t_test", "BF"), # BF results
  1442. filter(df.clusters.corr, manip == "original", stim == "sounds")) %>% # append correlation clusters
  1443. mutate(approach = "EMPIRICAL\nNULL"),
  1444. left_join(matrix_to_df2(bf.corr.diff.maths, "t_train", "t_test", "BF"), # BF results
  1445. filter(df.clusters.corr, manip == "original", stim == "sounds")) %>% # append correlation clusters
  1446. mutate(approach = "THEORETICAL\nNULL"),
  1447. left_join(matrix_to_df2(bf.corr.diff.ml, "t_train", "t_test", "BF"), # BF results
  1448. filter(df.clusters.corr, manip == "original", stim == "sounds")) %>% # append correlation clusters
  1449. mutate(approach = "MOST LIKELY")) %>%
  1450. # --- order factor levels
  1451. mutate(approach = factor(approach, levels = c("EMPIRICAL\nNULL", "THEORETICAL\nNULL", "MOST LIKELY"))) %>%
  1452. # --- convert indices to time
  1453. mutate(t_test = (dt * (t_test - 1) - time_offset)) %>%
  1454. mutate(t_train = (dt * (t_train - 1))) %>%
  1455. # --- discretized version of BF
  1456. mutate(BFd = cut(BF,
  1457. breaks = c(0, 1/30, 1/10, 1/3, 3, 10, 30, Inf),
  1458. labels = c("very strong in favor of null (BF < 1/30)",
  1459. "strong in favor of null (1/30 < BF < 1/10)",
  1460. "moderate in favor of null (1/10 < BF < 1/3)",
  1461. "inconclusive (1/3 < BF < 3)",
  1462. "moderate in favor of alt. (3 < BF < 10)",
  1463. "strong in favor of alt. (10 < BF < 30)",
  1464. "very strong in favor of alt. (BF > 30)")),
  1465. .after = 3
  1466. ) %>%
  1467. # --- transform continuous BF for easy symetric display on colorbar
  1468. mutate(BF = case_when(BF < 1/3 ~ -1/BF,
  1469. between(BF, 1/3, 3) ~ NA,
  1470. BF > 3 ~ BF))
  1471. df.plot %>% summary
  1472. ```
  1473. ### Plot
  1474. ```{r}
  1475. #| fig-width: 10
  1476. #| fig-height: 3
  1477. #| lightbox:
  1478. #| group: global
  1479. lines.v.pos <- 330*seq(round(min(df.plot$t_test)/333), round(max(df.plot$t_test)/333), 1)
  1480. lines.v.pos <- lines.v.pos[lines.v.pos!=0]
  1481. lines.h.pos <- 330*seq(round(min(df.plot$t_train)/333), round(max(df.plot$t_train)/333), 1)
  1482. lines.h.pos <- lines.h.pos[lines.h.pos!=0]
  1483. df.plot %>%
  1484. ggplot(aes(x = t_test, y = t_train)) +
  1485. facet_grid(approach ~ .) +
  1486. geom_tile(aes(fill = BFd), show.legend = TRUE) +
  1487. geom_vline(xintercept = lines.v.pos, linetype = 2, color = "black") +
  1488. geom_hline(yintercept = lines.h.pos, linetype = 2, color = "black") +
  1489. geom_vline(xintercept = 0, color = "black") +
  1490. geom_hline(yintercept = 0, color = "black") +
  1491. geom_contour(aes(z = signif), size = 0.2, color = col.clusters) +
  1492. scale_x_continuous(breaks = scales::pretty_breaks(12),
  1493. expand = c(0,0)) +
  1494. scale_y_continuous(breaks = scales::pretty_breaks(4),
  1495. expand = c(0,0)) +
  1496. scale_fill_manual(values = rev(col.pal.bf),
  1497. labels = levels(df.plot$BFd),
  1498. drop = FALSE) +
  1499. labs(fill = expression(BF[10])) +
  1500. coord_equal() +
  1501. labs(x = "Test time (ms)", y = "Train time (ms)") -> g.supfig3
  1502. g.supfig3
  1503. if (save) {
  1504. g.supfig3 %>% ggsave(filename = "./figures/supfig3_MattersArising.pdf", width = 10, height = 3, device = cairo_pdf)
  1505. g.supfig3 %>% ggsave(filename = "./figures/supfig3_MattersArising.png", width = 10, height = 3, device = ragg::agg_png(), dpi = 600)
  1506. }
  1507. ```
  1508. ## Supplementary Figure 4
  1509. ```{r}
  1510. #| fig-width: 7
  1511. #| fig-height: 2.5
  1512. #| lightbox:
  1513. #| group: global
  1514. mat.C.all <- array(dim = c(34,34,4,4,n.subj))
  1515. i <- 1
  1516. for (s in list.subj) {
  1517. # --- load the confusion matrix
  1518. tmp.file <- os$path$join(path.root, s, "rd_to_rd_confmats.npz") %>%
  1519. np$load()
  1520. # --- crop around S0 (decoded stimulus)
  1521. mat.C.all[,,,,i] <- tmp.file$f[["arr_0"]][,71:104,71:104,,] %>%
  1522. apply(c(2,3,4,5), mean)
  1523. i <- i+1
  1524. }
  1525. mat.C.mean <- mat.C.all %>% apply(c(1,2,3,4), mean)
  1526. df.confusion <- matrix_to_df2(mat.C.mean[22,22,,], "from", "to", "p") %>%
  1527. mutate(label = paste0("a[",from,to,"]"))
  1528. df.confusion %>%
  1529. ggplot(aes(x = to, y = from, fill = p)) +
  1530. geom_tile(color = "firebrick3") +
  1531. geom_text(aes(label = label), parse = T, color = "firebrick3") +
  1532. scale_x_continuous(name = "predicted class", position = "top") +
  1533. scale_y_reverse(name = "true class") +
  1534. scale_fill_stepsn(name = "p",
  1535. colors = pals::brewer.greys(100),
  1536. # low = "white", high = "black",
  1537. labels = ~str_replace(., "^0.","."),
  1538. breaks = scales::pretty_breaks(6),
  1539. limits = c(.20,.30), oob = scales::squish) +
  1540. coord_equal(xlim = c(0.5,4.5), ylim = rev(c(0.5,4.5)),
  1541. expand = F, clip = "off") -> g.matC
  1542. matrix_to_df2(mat.T$MP, "from", "to", "p") %>%
  1543. mutate(label = paste0("a[",from,to,"]")) %>%
  1544. ggplot(aes(x = to, y = from, fill = p)) +
  1545. geom_tile(color = "firebrick3") +
  1546. geom_text(aes(label = label), parse = T, color = "firebrick3") +
  1547. scale_x_continuous(name = "to", position = "top") +
  1548. scale_y_reverse(name = "from") +
  1549. scale_fill_stepsn(name = "p",
  1550. colors = pals::brewer.greys(100),
  1551. labels = ~str_replace(., "^0.","."),
  1552. breaks = scales::pretty_breaks(6),
  1553. limits = c(.0,.50), oob = scales::squish) +
  1554. coord_equal(xlim = c(0.5,4.5), ylim = rev(c(0.5,4.5)),
  1555. expand = F, clip = "off") -> g.matT
  1556. ((g.matT + labs(caption = "Transition matrix\n(midplus (MP) sequence)") +
  1557. guides(fill = guide_colorbar(barwidth = 0.5, ticks = F))) +
  1558. (g.matC + labs(caption = "Confusion matrix\n(group average, t=210ms)") +
  1559. guides(fill = guide_colorbar(barwidth = 0.5, ticks = F))) &
  1560. theme(plot.caption = element_text(size = 10),
  1561. axis.title = element_text(face = "italic"),
  1562. legend.box.spacing = unit(2,"mm"),
  1563. )
  1564. ) +
  1565. plot_annotation(tag_levels = "a") -> g.supfig4
  1566. g.supfig4
  1567. if (save) {
  1568. g.supfig4 %>% ggsave(filename = "./figures/supfig4_MattersArising.pdf", width = 7, height = 2.5, device = cairo_pdf)
  1569. g.supfig4 %>% ggsave(filename = "./figures/supfig4_MattersArising.png", width = 7, height = 2.5, device = ragg::agg_png(), dpi = 600)
  1570. }
  1571. ```

postprocessing.qmd at commit a454439, no license · at the source

Overview

Authors: Oussama Abdoun1, Dmitrii Todorov1,2, Arnaud Poublan-couzardot1, Coumarane Tirou1, Antoine Lutz1, Marine Vernet3, Romain Quentin1
  1. Université Claude Bernard Lyon 1, INSERM U1028, CNRS UMR5292, Lyon Neuroscience Research Center (CRNL), EDUWELL team,Lyon, France
  2. NCP Team, The Biomedical Imaging Laboratory (LIB), CNRS (UMR 7371), INSERM (U1146), Sorbonne Université,Paris, France
  3. Université Claude Bernard Lyon 1, INSERM U1028, CNRS UMR5292, Lyon Neuroscience Research Center (CRNL), IMPACT team,Lyon, France
Journal: Nature communications, volume 17, issue 1, article 4639
Dates: received 2 July 2024; accepted 8 May 2026; published online 26 May 2026
Type: Letter · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-73568-1 · PMID 42192112 · PMCID PMC13212915 · OpenAlex W4414358687
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: cognitive (subfield)
Methods: Spectral & time-frequency, Connectivity, Statistics, Machine learning
Keywords: Learning and memory, Computational neuroscience, Auditory system, Perception
Journal subjects: Matters Arising
Topic: Neural and Behavioral Psychology Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: cited by 1 paper (Europe PMC); 12 references in the paper

Abstract

No abstract was found for this paper: the paper, at the publisher.

Repositories

Its files are read in the Code ↔ Paper reader above, with 2 matches between paragraphs and lines of code.

doi:10.5281/zenodo

License: none: the authors keep all their rights
State: the link is dead, verified on 28 September 2026
Evidence: found in the paper
Software Heritage: not checked
Found in: the text, “Data and preprocessing”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 28 September 2026: the link is dead (HTTP 404)
  • 28 September 2026: the link is dead (HTTP 404)
At the source: doi.org/10.5281/zenodo

MEL-Eduwell-lab/predictive_activity

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: a454439aec8936ff9de2199ab255472c7d140b8c, 25 November 2025
Languages: Python (3), JavaScript (1), Quarto (1)
Size: 101 files, 5 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (environment.yml), documentation, 1 notebook
Not found: license file, CITATION.cff, tests, continuous integration
Tools: MNE-Python (3 files), NumPy (3 files), pandas (3 files), scikit-learn (2 files), BayesFactor (1 file), easystats (1 file), patchwork (1 file), reticulate (1 file), seaborn (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
6 files

romquentin/predictive_activity

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: a454439aec8936ff9de2199ab255472c7d140b8c, 25 November 2025
Languages: Python (3), JavaScript (1), Quarto (1)
Size: 101 files, 5 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, environment (environment.yml), documentation, 1 notebook
Not found: license file, CITATION.cff, tests, continuous integration
Tools: MNE-Python (3 files), NumPy (3 files), pandas (3 files), scikit-learn (2 files), BayesFactor (1 file), easystats (1 file), patchwork (1 file), reticulate (1 file), seaborn (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
6 files

Code availability statement

The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-73568-1.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 3 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 10 scripts, each with its path and the digest of its content;
  • 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data availability statement

The paper has a data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

  • no repository, dataset or request procedure was recognized in it

Read it in the paper: doi.org/10.1038/s41467-026-73568-1.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 2, 28 September 2026

  • Funding: added Agence Nationale de la Recherche: ANR-11-LABX-0042; Institut National de la Santé et de la Recherche Médicale; Université Claude Bernard Lyon 1: ED476, ANR-11-LABX-0042

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 4 keywords, 9 references.

Cite

This paper

Abdoun, O., Todorov, D., Poublan-couzardot, A., Tirou, C., Lutz, A., Vernet, M., & Quentin, R. (2026). No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus. Nature communications, 17(1), 4639. https://doi.org/10.1038/s41467-026-73568-1

BibTeX

@article{abdoun2026no,
author = {Abdoun, Oussama and Todorov, Dmitrii and Poublan-couzardot, Arnaud and Tirou, Coumarane and Lutz, Antoine and Vernet, Marine and Quentin, Romain},
title = {{No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus}},
journal = {Nature communications},
year = {2026},
month = may,
volume = {17},
number = {1},
pages = {4639},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-73568-1},
url = {https://doi.org/10.1038/s41467-026-73568-1},
pmid = {42192112},
pmcid = {PMC13212915}
}

RIS

TY - JOUR
AU - Abdoun, Oussama
AU - Todorov, Dmitrii
AU - Poublan-couzardot, Arnaud
AU - Tirou, Coumarane
AU - Lutz, Antoine
AU - Vernet, Marine
AU - Quentin, Romain
TI - No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/05/26
VL - 17
IS - 1
SP - 4639
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-73568-1
UR - https://doi.org/10.1038/s41467-026-73568-1
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-73568-1",
"type": "article-journal",
"title": "No evidence of neural feature-specific pre-activation during the prediction of an upcoming stimulus",
"container-title": "Nature communications",
"author": [
{
"family": "Abdoun",
"given": "Oussama"
},
{
"family": "Todorov",
"given": "Dmitrii"
},
{
"family": "Poublan-couzardot",
"given": "Arnaud"
},
{
"family": "Tirou",
"given": "Coumarane"
},
{
"family": "Lutz",
"given": "Antoine"
},
{
"family": "Vernet",
"given": "Marine"
},
{
"family": "Quentin",
"given": "Romain"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "4639",
"DOI": "10.1038/s41467-026-73568-1",
"PMID": "42192112",
"PMCID": "PMC13212915",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-73568-1",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
26
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41467-026-74824-0 [code]
Learning regularities in noise engages both neural predictive activity and representational changes.
Journal: Nature communications
In common: reticulate, easystats, MNE-Python, 4 other tools, cognitive, 4 references, 3 authors
[2] doi:10.1093/nc/niag029 [code]
A data-driven approach to identifying and evaluating connectivity-based neural correlates of conscious visual perception.
Journal: Neuroscience of consciousness
In common: BayesFactor, easystats, MNE-Python, 6 other tools, cognitive
[3] doi:10.1038/s41597-026-07350-9 [code]
An open multi-center MEG-EEG dataset for studying conscious visual perception.
Journal: Scientific data
In common: BayesFactor, easystats, MNE-Python, 5 other tools, 1 reference
[4] doi:10.1038/s41597-026-07377-y [code]
An open-access multi-site fMRI dataset for investigating conscious visual perception.
Journal: Scientific data
In common: BayesFactor, easystats, MNE-Python, 5 other tools, 1 reference
[5] doi:10.1167/jov.26.8.4 [code]
The neural processes of illusory occlusion in object recognition.
Journal: Journal of vision
In common: BayesFactor, MNE-Python, seaborn, 4 other tools, cognitive, 1 reference
[6] doi:10.1016/j.isci.2026.117285 [code]
Working memory demands modulate memory brain state engagement.
Journal: iScience
In common: BayesFactor, easystats, MNE-Python, 4 other tools, cognitive
[7] doi:10.1038/s41467-026-75662-w [code]
Distinct Roles of Deep and Superficial Cortical Layers in Tone Prediction, Comparison, and Adaptation in Human Auditory Cortices.
Journal: Nature communications
In common: MNE-Python, seaborn, tidyverse, 3 other tools, cognitive, 2 references
[8] doi:10.3390/s26103131 [code]
Neuromagnetism "On the Cheap": Evaluating a Combined Cylindrical Shield and Partial-Coverage OPM-MEG System for Detecting Sensorimotor Responses in Humans.
Journal: Sensors (Basel, Switzerland)
In common: BayesFactor, reticulate, MNE-Python, 4 other tools
[9] doi:10.1002/hbm.70605 [code]
BrainEnrich: Revealing Biological Insights for Imaging-Derived Phenotypes Through Transcriptomic Enrichment.
Journal: Human brain mapping
In common: reticulate, easystats, patchwork, 5 other tools
[10] doi:10.7554/elife.107088 [code]
Development of auditory and spontaneous movement responses to music over the first postnatal year.
Journal: eLife
In common: easystats, MNE-Python, seaborn, 4 other tools, 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.