OSCR

Emergent blink rate in early childhood is associated with neural origins of executive function.

Code ↔ Paper

6 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 6 matches
  1. [1] § Methods › Analysis plan ↔ code/MainAnalysis.R, lines 396–441 · score 0.88 · Residual diagnostics, residual variance, design cells, random intercept, SD ratio, fit
  2. [2] § Methods › Analysis plan ↔ code/MainAnalysis.R, lines 275–312 · score 0.76 · confidence intervals, Tukey adjustment, multiplicity adjustment, post hoc contrasts, pairwise, oxy Hb
  3. [3] § Results › Spontaneous eye-blink rate is associated with switching performance ↔ code/MainAnalysis.R, lines 146–211 · score 0.76 · post hoc Wilcoxon, rank biserial, signed rank, Friedman, Kendall, Bonferroni
  4. [4] § Methods › fNIRS recordings and analysis ↔ code/Fig4_plot.R, lines 1–58 · score 0.75 · lDLPFC, rRLPFC, lRLPFC, rDLPFC, oxy Hb, fNIRS
  5. [5] § Methods › Analysis plan ↔ code/Fig4_plot.R, lines 1–58 · score 0.74 · lDLPFC, rRLPFC, lRLPFC, sEBR, rDLPFC, switching accuracy
  6. [6] § Methods › fNIRS recordings and analysis ↔ code/MainAnalysis.R, lines 508–549 · score 0.65 · lDLPFC, rRLPFC, lRLPFC, rDLPFC, neural, correlated

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

R · 767 lines · 41 KB · no license · 4 matches

  1. # ==============================================================================
  2. # MainAnalysis.R
  3. # Emergent blink rate in early childhood is associated with neural origins
  4. # of executive function
  5. # Kuwamizu, Yamamoto, Otani & Moriguchi
  6. #
  7. # Copyright (c) 2026 the authors. Released under the MIT License; see code/LICENSE.
  8. # The accompanying data are released under CC BY 4.0; see LICENSE.md.
  9. #
  10. # Integrated analysis pipeline. Reproduces every statistic reported in the
  11. # manuscript and Supplementary Information, and exports figure source data.
  12. #
  13. # Section 1 Behavioural correlations Fig 2a-c; Supp Fig 1a-d
  14. # Section 2 fNIRS linear mixed models Fig 3a-f; Supp Table 3
  15. # Section 3 Neural correlations Fig 4a-b; Supp Table 4; Supp Fig 5
  16. # Section 4 Robustness and sensitivity Supp Notes 1 and 4; Methods
  17. # Section 5 Summary CSVs and session information
  18. #
  19. # Input : data/SourceData.csv
  20. # Output: outputs_summary/results_summary_generated.csv
  21. # source_data_figures/*.csv
  22. # R >= 4.3
  23. # ==============================================================================
  24. ## ====== 0. Setup, data, helpers ==============================================
  25. if (!require("pacman")) install.packages("pacman")
  26. pacman::p_load(tidyverse, lme4, lmerTest, emmeans, ppcor, rstatix,
  27. effectsize, performance, mediation)
  28. options(scipen = 999)
  29. set.seed(365)
  30. OUTDIR <- "outputs_summary"
  31. FIGDIR <- "source_data_figures"
  32. dir.create(OUTDIR, showWarnings = FALSE, recursive = TRUE)
  33. dir.create(FIGDIR, showWarnings = FALSE, recursive = TRUE)
  34. # ---- single data path (override here if needed) ----
  35. candidate_paths <- c(
  36. file.path("data", "SourceData.csv"),
  37. "SourceData.csv",
  38. file.path("..", "data", "SourceData.csv")
  39. )
  40. data_path <- candidate_paths[file.exists(candidate_paths)][1]
  41. if (is.na(data_path)) stop("SourceData.csv not found; set data_path manually.")
  42. cat("Loading data from:", data_path, "\n")
  43. raw_data <- read.csv(data_path, na.strings = c("NA", "NaN", ""),
  44. fileEncoding = "UTF-8", stringsAsFactors = FALSE) %>%
  45. mutate(ID = as.factor(ID))
  46. fnirs_cols <- grep("^(r|l)_(DL|RL)_(pre|post|mix|sw)$", names(raw_data), value = TRUE)
  47. count_cols <- c("sw_corr","sw_rep","Pre_corr","Pre_rep",
  48. "Post_corr","Post_rep","Mix_corr","Mix_rep")
  49. target_cols <- c("age","sEBR","sw_acc","pre_acc","post_acc","mix_acc",
  50. count_cols, fnirs_cols)
  51. raw_data <- raw_data %>%
  52. mutate(across(any_of(target_cols), ~ as.numeric(as.character(.))),
  53. sex = if ("sex" %in% names(.)) factor(sex) else NA,
  54. site = if ("site" %in% names(.)) factor(site) else NA)
  55. cat("N participants:", nrow(raw_data), "\n")
  56. # ---- helper functions ----
  57. # Spearman with N and 95% CI.
  58. # CI from Fisher's z transform with the Bonett & Wright (2000) standard error
  59. # for Spearman's rho: SE_z = sqrt((1 + rho^2/2)/(n - 3)).
  60. sp <- function(x, y, conf = 0.95) {
  61. d <- na.omit(data.frame(x, y))
  62. n <- nrow(d)
  63. ct <- cor.test(d$x, d$y, method = "spearman", exact = FALSE)
  64. rho <- unname(ct$estimate)
  65. if (n > 3 && abs(rho) < 1) {
  66. z <- atanh(rho)
  67. se <- sqrt((1 + rho^2 / 2) / (n - 3))
  68. zc <- qnorm(1 - (1 - conf) / 2)
  69. lo <- tanh(z - zc * se); hi <- tanh(z + zc * se)
  70. } else { lo <- NA_real_; hi <- NA_real_ }
  71. c(rho = rho, p = ct$p.value, n = n, ci_low = lo, ci_high = hi)
  72. }
  73. # Partial Spearman via ppcor (df = n - 2 - k); covariates are ranked.
  74. # CI from Fisher's z transform with SE_z = 1/sqrt(n - k - 3) (k = # covariates).
  75. pspear <- function(x, y, Z, conf = 0.95) {
  76. Z <- as.data.frame(Z)
  77. d <- na.omit(data.frame(x = x, y = y, Z))
  78. zc <- setdiff(names(d), c("x", "y"))
  79. k <- length(zc)
  80. n <- nrow(d)
  81. zr <- as.data.frame(lapply(d[zc], function(v) rank(xtfrm(v))))
  82. pc <- ppcor::pcor.test(rank(d$x), rank(d$y), zr, method = "pearson")
  83. rho <- unname(pc$estimate)
  84. if ((n - k - 3) > 0 && abs(rho) < 1) {
  85. z <- atanh(rho)
  86. se <- 1 / sqrt(n - k - 3)
  87. zcrit <- qnorm(1 - (1 - conf) / 2)
  88. lo <- tanh(z - zcrit * se); hi <- tanh(z + zcrit * se)
  89. } else { lo <- NA_real_; hi <- NA_real_ }
  90. c(rho = rho, p = pc$p.value, n = n, ci_low = lo, ci_high = hi)
  91. }
  92. winsorize <- function(v, p = 0.05) {
  93. lo <- quantile(v, p, na.rm = TRUE); hi <- quantile(v, 1-p, na.rm = TRUE)
  94. pmin(pmax(v, lo), hi)
  95. }
  96. # ---- running summary collector (-> results_summary_generated.csv) ----
  97. # Every value written to the CSV is also echoed to the console (prefixed "[CSV]"),
  98. # so the console log and the CSV contain the same numbers.
  99. SUMMARY <- list()
  100. # `stat` : NAME of the test statistic or estimate type (e.g. "t", "V", "F")
  101. # `stat_value` : VALUE of that test statistic, when it differs from `estimate`
  102. # (e.g. estimate = Cohen's d while stat_value = the t statistic).
  103. # Left empty when `estimate` already IS the statistic.
  104. # `stat_value` is the LAST argument so that every pre-existing positional call
  105. # in this script keeps its original meaning.
  106. add_row <- function(loc, analysis, estimate, stat = "", df = "",
  107. p = "", n = "", model = "", note = "",
  108. ci_low = "", ci_high = "", stat_value = "") {
  109. row <- data.frame(
  110. ms_location = loc, analysis = analysis, estimate = as.character(estimate),
  111. stat = stat, stat_value = as.character(stat_value),
  112. df = as.character(df), p = as.character(p),
  113. n = as.character(n),
  114. ci_low = as.character(ci_low), ci_high = as.character(ci_high),
  115. model = model, note = note, stringsAsFactors = FALSE)
  116. SUMMARY[[length(SUMMARY) + 1]] <<- row
  117. # ---- console echo of the same numbers ----
  118. e <- as.character(estimate); st <- as.character(stat); d <- as.character(df)
  119. sv <- as.character(stat_value)
  120. pp <- as.character(p); nn <- as.character(n)
  121. cl <- as.character(ci_low); ch <- as.character(ci_high)
  122. md <- as.character(model); nt <- as.character(note)
  123. part <- function(lab, v) if (length(v) && !is.na(v) && nzchar(v)) paste0(" ", lab, v) else ""
  124. ci_str <- if (length(cl) && length(ch) && !is.na(cl) && !is.na(ch) && nzchar(cl) && nzchar(ch))
  125. sprintf(" 95%%CI=[%s, %s]", cl, ch) else ""
  126. # when a statistic value is present, echo it as e.g. " t=3.214"
  127. st_str <- if (length(sv) && !is.na(sv) && nzchar(sv))
  128. paste0(" ", st, "=", sv) else part("", st)
  129. cat(sprintf("[CSV] %-18s | %-46s | est=%s%s%s%s%s%s%s%s\n",
  130. loc, analysis, e,
  131. st_str, part("df=", d), part("p=", pp), part("n=", nn), ci_str,
  132. part("model=", md), part("note=", nt)))
  133. }
  134. fmtp <- function(p) ifelse(p < 0.001, "<0.001", signif(p, 3))
  135. # 95% CI as a printable string, e.g. "[0.12, 0.34]"
  136. fmtci <- function(lo, hi, d = 3) ifelse(is.na(lo) | is.na(hi), "",
  137. sprintf(paste0("[%.", d, "f, %.", d, "f]"), lo, hi))
  138. ## =============================================================================
  139. ## SECTION 1 -- Behavioural correlations [Fig 2a-c; Supp Fig 1a-d]
  140. ## =============================================================================
  141. cat("\n\n========== SECTION 1: Behaviour (Fig 2; Supp Fig 1) ==========\n")
  142. ## 1-1 Friedman + post-hoc [Supp Fig 1a]
  143. beh_long <- raw_data %>%
  144. dplyr::select(ID, pre_acc, post_acc, mix_acc) %>%
  145. pivot_longer(c(pre_acc, post_acc, mix_acc), names_to = "Condition", values_to = "Accuracy") %>%
  146. mutate(Condition = factor(Condition, levels = c("pre_acc","post_acc","mix_acc"))) %>%
  147. drop_na()
  148. fr <- friedman.test(Accuracy ~ Condition | ID, data = beh_long); print(fr)
  149. # The omnibus Friedman test is reported in full as chi2(df) and an exact p.
  150. # (No separate effect size is added here: the editor's effect-size + CI
  151. # requirement targets t-tests and ANOVAs; the rank-based post-hoc comparisons
  152. # below carry the effect sizes and CIs.)
  153. n_blk <- nrow(raw_data %>% dplyr::select(pre_acc, post_acc, mix_acc) %>% tidyr::drop_na())
  154. add_row("SuppFig1a","Pre/Post/Mix accuracy (Friedman)", round(unname(fr$statistic),2),
  155. "chi2", fr$parameter, fmtp(fr$p.value), n_blk)
  156. # Effect size for the Friedman test: Kendall's W with 95% CI.
  157. kw <- as.data.frame(suppressWarnings(
  158. effectsize::kendalls_w(Accuracy ~ Condition | ID, data = beh_long,
  159. ci = 0.95, alternative = "two.sided")))
  160. cat(sprintf("Friedman effect size: Kendall's W = %.3f, 95%% CI [%.3f, %.3f]\n",
  161. kw$Kendalls_W, kw$CI_low, kw$CI_high))
  162. add_row("SuppFig1a","Friedman effect size (Kendall's W)", round(kw$Kendalls_W,3),
  163. "Kendall W", "", "", n_blk, "", "",
  164. round(kw$CI_low,3), round(kw$CI_high,3))
  165. # Post-hoc paired Wilcoxon signed-rank tests, reported in full and separately.
  166. # Statistic V; p from the normal approximation with continuity correction
  167. # (exact = FALSE, appropriate here because accuracy values are tied), then
  168. # Bonferroni-adjusted across the 2 comparisons; matched-pairs rank-biserial
  169. # correlation r (+95% CI) as the effect size.
  170. # NOTE: the signed-rank statistic V has no degrees of freedom, so V is
  171. # written to `stat_value` and the `df` column is left empty.
  172. ph_pairs <- list(c("pre_acc","post_acc","Pre vs Post"),
  173. c("pre_acc","mix_acc","Pre vs Mix"))
  174. ph_raw_p <- sapply(ph_pairs, function(pr)
  175. wilcox.test(raw_data[[pr[1]]], raw_data[[pr[2]]], paired = TRUE, exact = FALSE)$p.value)
  176. ph_adj_p <- p.adjust(ph_raw_p, method = "bonferroni")
  177. for (i in seq_along(ph_pairs)) {
  178. pr <- ph_pairs[[i]]
  179. d2 <- na.omit(data.frame(a = raw_data[[pr[1]]], b = raw_data[[pr[2]]]))
  180. wt <- wilcox.test(d2$a, d2$b, paired = TRUE, exact = FALSE)
  181. rb <- tryCatch(as.data.frame(effectsize::rank_biserial(d2$a, d2$b, paired = TRUE, ci = 0.95)),
  182. error = function(e) data.frame(r_rank_biserial = NA, CI_low = NA, CI_high = NA))
  183. cat(sprintf("Post-hoc %-11s V = %.0f, p_adj = %s, r_rb = %.3f, 95%% CI %s, n = %d\n",
  184. pr[3], unname(wt$statistic), fmtp(ph_adj_p[i]),
  185. rb$r_rank_biserial, fmtci(rb$CI_low, rb$CI_high), nrow(d2)))
  186. add_row("SuppFig1a", paste0("Post-hoc Wilcoxon ", pr[3]),
  187. estimate = round(rb$r_rank_biserial, 3),
  188. stat = "V",
  189. df = "", # V has no df
  190. p = fmtp(ph_adj_p[i]),
  191. n = nrow(d2),
  192. note = paste0("two-sided, normal approximation; Bonferroni-adjusted p; ",
  193. "estimate = matched-pairs rank-biserial r (95% CI)"),
  194. ci_low = round(rb$CI_low, 3),
  195. ci_high = round(rb$CI_high, 3),
  196. stat_value = round(unname(wt$statistic), 1))
  197. }
  198. ## 1-2 Primary correlations [Fig 2a-c]
  199. for (nm in list(c("age","sw_acc","Fig2a","age ~ switching accuracy"),
  200. c("age","sEBR","Fig2b","age ~ sEBR"),
  201. c("sEBR","sw_acc","Fig2c","sEBR ~ switching accuracy"))) {
  202. r <- sp(raw_data[[nm[1]]], raw_data[[nm[2]]])
  203. cat(sprintf("%-28s rho=%.2f p=%s 95%% CI %s n=%d\n",
  204. nm[4], r["rho"], fmtp(r["p"]), fmtci(r["ci_low"], r["ci_high"]), r["n"]))
  205. add_row(nm[3], nm[4], round(r["rho"],3), "Spearman rho", "", fmtp(r["p"]), r["n"],
  206. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  207. }
  208. r <- pspear(raw_data$sEBR, raw_data$sw_acc, raw_data["age"])
  209. cat(sprintf("sEBR ~ switching acc | age rho=%.2f p=%s 95%% CI %s n=%d\n",
  210. r["rho"], fmtp(r["p"]), fmtci(r["ci_low"], r["ci_high"]), r["n"]))
  211. add_row("Fig2c","sEBR ~ switching accuracy | age", round(r["rho"],3),
  212. "partial Spearman", "", fmtp(r["p"]), r["n"],
  213. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  214. ## 1-3 Phase-wise sEBR x accuracy + age x phase-accuracy [Supp Fig 1b-d]
  215. for (v in c("pre_acc","post_acc","mix_acc")) {
  216. rs <- sp(raw_data$sEBR, raw_data[[v]]); rp <- pspear(raw_data$sEBR, raw_data[[v]], raw_data["age"])
  217. add_row("SuppFig1", paste0("sEBR ~ ", v), round(rs["rho"],3), "Spearman rho","", fmtp(rs["p"]), rs["n"],
  218. "", "", round(rs["ci_low"],3), round(rs["ci_high"],3))
  219. add_row("SuppFig1", paste0("sEBR ~ ", v, " | age"), round(rp["rho"],3), "partial Spearman","", fmtp(rp["p"]), rp["n"],
  220. "", "", round(rp["ci_low"],3), round(rp["ci_high"],3))
  221. }
  222. for (v in c("pre_acc","post_acc","mix_acc")) {
  223. ra <- sp(raw_data$age, raw_data[[v]])
  224. add_row("SuppFig1", paste0("age ~ ", v), round(ra["rho"],3), "Spearman rho","", fmtp(ra["p"]), ra["n"],
  225. "", "", round(ra["ci_low"],3), round(ra["ci_high"],3))
  226. }
  227. ## =============================================================================
  228. ## SECTION 2 -- fNIRS LMM [Fig 3a-f; Supp Table 3]
  229. ## =============================================================================
  230. cat("\n\n========== SECTION 2: fNIRS LMM (Fig 3; Supp Table 3) ==========\n")
  231. df_long <- raw_data %>%
  232. pivot_longer(matches("^(r|l)_(DL|RL)_(pre|post|mix)$"),
  233. names_to = c("Hemisphere","Region","Phase"),
  234. names_pattern = "^([rl])_([A-Z]{2})_([a-z]+)", values_to = "OxyHb") %>%
  235. mutate(Subject = ID,
  236. Hemisphere = factor(Hemisphere, levels = c("l","r"), labels = c("Left","Right")),
  237. Region = factor(Region, levels = c("RL","DL"), labels = c("RLPFC","DLPFC")),
  238. Phase = factor(Phase, levels = c("pre","mix","post")),
  239. PhaseRep = dplyr::case_when(Phase=="pre"~Pre_rep, Phase=="mix"~Mix_rep, Phase=="post"~Post_rep)) %>%
  240. drop_na(OxyHb)
  241. cat("df_long rows:", nrow(df_long), "\n")
  242. ## 2-1 Base model [Fig 3b-d]
  243. model_base <- lmer(OxyHb ~ Phase*Hemisphere*Region + (1|Subject), data = df_long)
  244. print(anova(model_base))
  245. ab <- anova(model_base)
  246. for (eff in c("Phase","Hemisphere","Region")) {
  247. add_row("Fig3", paste0(eff," main effect"), round(ab[eff,"F value"],2), "F",
  248. paste0(ab[eff,"NumDF"], ",", sprintf("%.1f", ab[eff,"DenDF"])), fmtp(ab[eff,"Pr(>F)"]), "", "base")
  249. }
  250. # Interaction terms reported in full (F, df, exact p) even though non-significant.
  251. for (eff in c("Phase:Hemisphere","Phase:Region","Hemisphere:Region","Phase:Hemisphere:Region")) {
  252. add_row("Fig3", paste0(eff," interaction"), round(ab[eff,"F value"],2), "F",
  253. paste0(ab[eff,"NumDF"], ",", sprintf("%.1f", ab[eff,"DenDF"])), fmtp(ab[eff,"Pr(>F)"]), "", "base")
  254. }
  255. ## Base-model post-hoc EMM contrasts [Fig 3c,d]
  256. ## Reported in full: EMM difference with 95% CI, t(df), p.
  257. ## >>> SIGN: contrasts are (first level - second level); all five come out
  258. ## >>> NEGATIVE here. The manuscript quotes magnitudes. See the header block.
  259. ## `infer = c(TRUE, TRUE)` is added so that the confidence interval is returned
  260. ## alongside the test; emmeans applies the SAME multiplicity adjustment to the
  261. ## interval as to the p value, so both carry the adjustment named below.
  262. ## Phase has three levels -> Tukey adjustment is active.
  263. ## Hemisphere and Region have two levels -> a single contrast, no adjustment.
  264. ## The corresponding standardised effect sizes (Cohen's d) are written
  265. ## separately by report_effsizes() above.
  266. emm_contrast_rows <- function(model, fac, adjust, loc, tag) {
  267. ct <- as.data.frame(summary(
  268. emmeans(model, as.formula(paste0("pairwise ~ ", fac)), adjust = adjust)$contrasts,
  269. infer = c(TRUE, TRUE)))
  270. cat(sprintf("\n-- %s post-hoc contrasts (adjust = %s) --\n", fac, adjust))
  271. print(ct)
  272. for (j in seq_len(nrow(ct))) {
  273. add_row(loc, paste0(fac, " post-hoc (", ct$contrast[j], ")"),
  274. estimate = round(ct$estimate[j], 4),
  275. stat = "t",
  276. df = sprintf("%.1f", ct$df[j]),
  277. p = fmtp(ct$p.value[j]),
  278. n = "",
  279. model = tag,
  280. note = paste0("estimate = UNSTANDARDISED EMM difference in oxy-Hb units ",
  281. "(95% CI on that difference, NOT on Cohen's d); ",
  282. "sign follows the contrast label (first - second level); ",
  283. "p and CI adjustment: ", adjust,
  284. " (", nrow(ct), " contrast(s)); ",
  285. "the standardised effect size for this contrast is reported ",
  286. "separately as a 'Cohen d' row"),
  287. ci_low = round(ct$lower.CL[j], 4),
  288. ci_high = round(ct$upper.CL[j], 4),
  289. stat_value = round(ct$t.ratio[j], 3))
  290. }
  291. ct
  292. }
  293. cph <- emm_contrast_rows(model_base, "Phase", "tukey", "Fig3c", "base")
  294. chh <- emm_contrast_rows(model_base, "Hemisphere", "none", "Fig3d", "base")
  295. crr <- emm_contrast_rows(model_base, "Region", "none", "Fig3d", "base")
  296. ## 2-2 Effect sizes for BASE and AGE models
  297. report_effsizes <- function(model, label, tag) {
  298. cat(sprintf("\n--- Effect sizes [%s] ---\n", label))
  299. cat("partial eta^2:\n"); es <- eta_squared(anova(model, type = 3), partial = TRUE, ci = 0.95, alternative = "two.sided"); print(es)
  300. cat("R^2 (Nakagawa):\n"); r2 <- r2_nakagawa(model); print(r2)
  301. sig <- sigma(model); edf <- df.residual(model)
  302. for (fac in c("Phase","Hemisphere","Region")) {
  303. cat(sprintf(" Cohen d [%s]:\n", fac))
  304. esz <- eff_size(emmeans(model, as.formula(paste0("~ ", fac))), sigma = sig, edf = edf)
  305. print(esz)
  306. ed <- as.data.frame(esz)
  307. for (j in seq_len(nrow(ed))) {
  308. add_row(paste0("Fig3 effsize [",tag,"]"),
  309. paste0("Cohen d ", fac, ": ", ed$contrast[j]),
  310. round(ed$effect.size[j],3), "Cohen d", round(ed$df[j],1), "", "",
  311. tag, paste0("post-hoc EMM contrast (sign = first - second level); ",
  312. "eff_size() calls contrast(adjust = \"none\"), so this 95% CI is ",
  313. "UNADJUSTED for multiplicity even where the corresponding p value ",
  314. "is Tukey-adjusted; sigma = residual SD of the LMM, edf = df.residual"),
  315. round(ed$lower.CL[j],3), round(ed$upper.CL[j],3))
  316. }
  317. }
  318. es <- as.data.frame(es)
  319. for (i in seq_len(nrow(es))) {
  320. add_row(paste0("Fig3 effsize [",tag,"]"), paste0("partial eta2 ", es$Parameter[i]),
  321. signif(es$Eta2_partial[i],3), "eta2_p","","","", tag, "",
  322. round(es$CI_low[i],3), round(es$CI_high[i],3))
  323. }
  324. add_row(paste0("Fig3 effsize [",tag,"]"),"Marginal R2", round(r2$R2_marginal,3), "R2","","","", tag)
  325. add_row(paste0("Fig3 effsize [",tag,"]"),"Conditional R2",round(r2$R2_conditional,3), "R2","","","", tag)
  326. }
  327. ## 2-3 Age model [Fig 3e,f]
  328. model_age <- lmer(OxyHb ~ (Phase*Hemisphere*Region)*age + (1|Subject), data = df_long)
  329. cat("\nBase vs Age LRT:\n"); print(anova(model_base, model_age))
  330. print(anova(model_age))
  331. aa <- anova(model_age)
  332. ## Every term of the age-model ANOVA is written to the summary CSV, not only the
  333. ## three significant age interactions: the manuscript reports F, df and P for the
  334. ## non-significant higher-order interactions as well.
  335. for (eff in rownames(aa)) {
  336. add_row("Fig3e/f", eff, round(aa[eff,"F value"],2), "F",
  337. paste0(aa[eff,"NumDF"], ",", sprintf("%.1f", aa[eff,"DenDF"])),
  338. fmtp(aa[eff,"Pr(>F)"]), "", "age",
  339. "Type III ANOVA, Satterthwaite df")
  340. }
  341. ## Post-hoc slope contrasts decomposing the age interactions (Fig 3e,f).
  342. ## Reported in full: estimate (difference in age slopes) with 95% CI, t(df), p.
  343. ## >>> SIGN: (first level - second level), as for the base-model contrasts above.
  344. ## >>> See the sign-convention block in the file header before transcribing.
  345. for (fac in c("Hemisphere","Region","Phase")) {
  346. ct <- as.data.frame(summary(
  347. emtrends(model_age, as.formula(paste0("pairwise ~ ", fac)), var = "age")$contrasts,
  348. infer = c(TRUE, TRUE)))
  349. cat(sprintf("\n-- age-slope contrasts by %s --\n", fac)); print(ct)
  350. for (j in seq_len(nrow(ct))) {
  351. # emmeans applies a Tukey adjustment to BOTH the p values and the CIs of
  352. # pairwise contrasts by default. Hemisphere and Region have two levels
  353. # (a single contrast, so the adjustment is inert); Phase has three levels,
  354. # where the adjustment is active. This is recorded in the note.
  355. add_row("Fig3e/f", paste0(fac, ":age contrast (", ct$contrast[j], ")"),
  356. estimate = round(ct$estimate[j], 4),
  357. stat = "t",
  358. df = sprintf("%.1f", ct$df[j]),
  359. p = fmtp(ct$p.value[j]),
  360. n = "",
  361. model = "age",
  362. note = paste0("estimate = difference in age slopes (95% CI); ",
  363. "Tukey-adjusted p and CI across the ", nrow(ct),
  364. " contrast(s) of ", fac),
  365. ci_low = round(ct$lower.CL[j], 4),
  366. ci_high = round(ct$upper.CL[j], 4),
  367. stat_value = round(ct$t.ratio[j], 3))
  368. }
  369. }
  370. report_effsizes(model_base, "BASE model: Phase x Hemisphere x Region", "BASE")
  371. report_effsizes(model_age, "AGE model: (Phase x Hemisphere x Region) x age", "AGE")
  372. ## 2-3b Residual diagnostics for the mixed-effects models [Methods]
  373. ## Normality of residuals and of the random intercepts, dependence of the
  374. ## residual variance on the fitted values, and equality of residual variance
  375. ## across the design cells.
  376. lmm_diagnostics <- function(model, tag) {
  377. mf <- model@frame
  378. r <- residuals(model)
  379. f <- fitted(model)
  380. sw <- shapiro.test(r)
  381. sk <- mean((r - mean(r))^3) / sd(r)^3
  382. ku <- mean((r - mean(r))^4) / sd(r)^4 - 3
  383. u <- r^2 / mean(r^2)
  384. LM <- 0.5 * sum((fitted(lm(u ~ f)) - 1)^2)
  385. bp <- pchisq(LM, 1, lower.tail = FALSE)
  386. cell <- interaction(mf$Phase, mf$Hemisphere, mf$Region, drop = TRUE)
  387. z <- abs(r - ave(r, cell, FUN = median))
  388. bf <- anova(lm(z ~ cell))
  389. sds <- tapply(r, cell, sd)
  390. re <- ranef(model)$Subject[, 1]
  391. swr <- shapiro.test(re)
  392. cat(sprintf("\n--- Residual diagnostics [%s] ---\n", tag))
  393. cat(sprintf("Residual normality: W = %.4f, p = %s (skewness %.3f, excess kurtosis %.3f)\n",
  394. sw$statistic, fmtp(sw$p.value), sk, ku))
  395. cat(sprintf("Residual variance vs fitted values: LM(1) = %.3f, p = %s\n", LM, fmtp(bp)))
  396. cat(sprintf("Residual variance across design cells: F(%d, %d) = %.3f, p = %s\n",
  397. bf$Df[1], bf$Df[2], bf$`F value`[1], fmtp(bf$`Pr(>F)`[1])))
  398. cat(sprintf("Residual SD by cell: %.4f to %.4f (max/min = %.2f)\n",
  399. min(sds), max(sds), max(sds) / min(sds)))
  400. cat(sprintf("Random-intercept normality: W = %.4f, p = %s\n",
  401. swr$statistic, fmtp(swr$p.value)))
  402. add_row("Methods", paste0("Residual normality [", tag, "]"),
  403. round(unname(sw$statistic), 4), "Shapiro-Wilk W", "", fmtp(sw$p.value), length(r), tag)
  404. add_row("Methods", paste0("Residual skewness / excess kurtosis [", tag, "]"),
  405. sprintf("%.3f / %.3f", sk, ku), "desc", "", "", length(r), tag)
  406. add_row("Methods", paste0("Residual variance vs fitted [", tag, "]"),
  407. round(LM, 3), "Breusch-Pagan LM", 1, fmtp(bp), length(r), tag)
  408. add_row("Methods", paste0("Residual variance across cells [", tag, "]"),
  409. round(bf$`F value`[1], 3), "Brown-Forsythe F",
  410. paste0(bf$Df[1], ",", bf$Df[2]), fmtp(bf$`Pr(>F)`[1]), length(r), tag)
  411. add_row("Methods", paste0("Residual SD ratio across cells [", tag, "]"),
  412. round(max(sds) / min(sds), 2), "max/min", "", "", length(sds), tag)
  413. add_row("Methods", paste0("Random-intercept normality [", tag, "]"),
  414. round(unname(swr$statistic), 4), "Shapiro-Wilk W", "", fmtp(swr$p.value), length(re), tag)
  415. }
  416. lmm_diagnostics(model_base, "BASE")
  417. lmm_diagnostics(model_age, "AGE")
  418. ## 2-3c Effect size for the base-vs-age model comparison [Fig 3e,f]
  419. r2_base <- r2_nakagawa(model_base)
  420. r2_age <- r2_nakagawa(model_age)
  421. d_marg <- r2_age$R2_marginal - r2_base$R2_marginal
  422. d_cond <- r2_age$R2_conditional - r2_base$R2_conditional
  423. lrt_ba <- anova(model_base, model_age)
  424. aic_diff <- lrt_ba$AIC[2] - lrt_ba$AIC[1]
  425. cat(sprintf("\nBase vs Age model comparison: change in marginal R2 = %.3f, ", d_marg))
  426. cat(sprintf("change in conditional R2 = %.3f, AIC difference = %.1f\n", d_cond, aic_diff))
  427. add_row("Fig3e/f","Base vs Age: likelihood ratio test", round(lrt_ba$Chisq[2],2), "chi2",
  428. lrt_ba$Df[2], fmtp(lrt_ba$`Pr(>Chisq)`[2]), "", "age",
  429. "LRT of the base model against the model adding age and all its interactions")
  430. add_row("Fig3e/f","Base vs Age: change in marginal R2", round(d_marg,3), "dR2","","","","age")
  431. add_row("Fig3e/f","Base vs Age: change in conditional R2", round(d_cond,3), "dR2","","","","age")
  432. add_row("Fig3e/f","Base vs Age: AIC difference", round(aic_diff,1), "dAIC","","","","age",
  433. "AIC(age) - AIC(base); equals 2*Df - Chisq of the LRT above")
  434. ## 2-4 One-sample t-tests vs baseline [Supp Table 3]
  435. ## Full reporting per cell: t(df), exact p (+Bonferroni), Cohen's d with 95% CI.
  436. st3 <- df_long %>% group_by(Phase, Hemisphere, Region) %>%
  437. group_modify(~{
  438. x <- .x$OxyHb
  439. tt <- t.test(x, mu = 0)
  440. dd <- tryCatch(as.data.frame(effectsize::cohens_d(x, mu = 0, ci = 0.95)),
  441. error = function(e) data.frame(Cohens_d = mean(x)/sd(x),
  442. CI_low = NA, CI_high = NA))
  443. tibble(N = length(x), Mean_Oxy = mean(x, na.rm = TRUE),
  444. Mean_CI_low = unname(tt$conf.int[1]), Mean_CI_high = unname(tt$conf.int[2]),
  445. t_value = unname(tt$statistic), df = unname(tt$parameter),
  446. p_raw = tt$p.value,
  447. Cohen_d = dd$Cohens_d, d_CI_low = dd$CI_low, d_CI_high = dd$CI_high)
  448. }) %>%
  449. ungroup() %>%
  450. mutate(p_bonf = p.adjust(p_raw, method = "bonferroni")) %>%
  451. arrange(Phase, Hemisphere, Region)
  452. print(as.data.frame(st3))
  453. write.csv(st3, file.path(FIGDIR, "source_SuppTable3_onesample.csv"), row.names = FALSE)
  454. for (i in seq_len(nrow(st3))) {
  455. add_row("SuppTable3",
  456. paste0("one-sample t vs 0: ", st3$Phase[i], " ", st3$Hemisphere[i], " ", st3$Region[i]),
  457. estimate = round(st3$Cohen_d[i], 3),
  458. stat = "t",
  459. df = st3$df[i],
  460. p = fmtp(st3$p_raw[i]),
  461. n = st3$N[i],
  462. note = paste0("two-sided; p shown is raw, p_bonf=", fmtp(st3$p_bonf[i]),
  463. "; estimate = Cohen d (95% CI)"),
  464. ci_low = round(st3$d_CI_low[i], 3),
  465. ci_high = round(st3$d_CI_high[i], 3),
  466. stat_value = round(st3$t_value[i], 3))
  467. ## The Oxy-Hb column of Supplementary Table 3 is the cell mean, which is a
  468. ## different quantity from Cohen's d above and is reported in mM*mm. Written
  469. ## as its own row so that the table can be reproduced from this file alone.
  470. add_row("SuppTable3",
  471. paste0("mean oxy-Hb: ", st3$Phase[i], " ", st3$Hemisphere[i], " ", st3$Region[i]),
  472. estimate = round(st3$Mean_Oxy[i], 3),
  473. stat = "mean",
  474. n = st3$N[i],
  475. note = "cell mean oxy-Hb in mM*mm with 95% CI; Oxy-Hb column of Supp Table 3",
  476. ci_low = round(st3$Mean_CI_low[i], 3),
  477. ci_high = round(st3$Mean_CI_high[i], 3))
  478. }
  479. ## 2-5 Fig 3c-f source-data export (EMMs for Prism) [Fig 3c,d,e,f]
  480. write.csv(as.data.frame(summary(emmeans(model_base, ~ Phase))),
  481. file.path(FIGDIR, "Fig3c_phase_emm.csv"), row.names = FALSE)
  482. write.csv(as.data.frame(summary(emmeans(model_base, ~ Hemisphere*Region))),
  483. file.path(FIGDIR, "Fig3d_hemi_region_emm.csv"), row.names = FALSE)
  484. age_seq <- seq(floor(min(df_long$age)), ceiling(max(df_long$age)), by = 1)
  485. write.csv(as.data.frame(summary(emmeans(model_age, ~ Hemisphere | age, at = list(age = age_seq)), infer = TRUE)),
  486. file.path(FIGDIR, "Fig3e_hemisphere_age.csv"), row.names = FALSE)
  487. write.csv(as.data.frame(summary(emmeans(model_age, ~ Region | age, at = list(age = age_seq)), infer = TRUE)),
  488. file.path(FIGDIR, "Fig3f_region_age.csv"), row.names = FALSE)
  489. ## =============================================================================
  490. ## SECTION 3 -- Neural correlations [Fig 4a-b; Supp Table 4; Supp Fig 5]
  491. ## =============================================================================
  492. cat("\n\n========== SECTION 3: Neural correlations (Fig 4; Supp Table 4) ==========\n")
  493. rois <- c(r_DL_sw="rDLPFC", l_DL_sw="lDLPFC", r_RL_sw="rRLPFC", l_RL_sw="lRLPFC")
  494. outcomes <- c(sEBR="sEBR", sw_acc="Switching accuracy")
  495. forest_df <- purrr::map_dfr(names(rois), function(r) purrr::map_dfr(names(outcomes), function(ov) {
  496. raw <- sp(raw_data[[r]], raw_data[[ov]])
  497. adj <- pspear(raw_data[[r]], raw_data[[ov]], raw_data["age"])
  498. tibble(Panel = rois[[r]], Series = outcomes[[ov]],
  499. rho_raw = raw["rho"], p_raw = raw["p"], n_raw = raw["n"],
  500. ci_low_raw = raw["ci_low"], ci_high_raw = raw["ci_high"],
  501. rho_age = adj["rho"], p_age = adj["p"], n_age = adj["n"],
  502. ci_low_age = adj["ci_low"], ci_high_age = adj["ci_high"])
  503. }))
  504. print(as.data.frame(forest_df %>% mutate(across(starts_with("rho"), ~round(.,3)),
  505. across(starts_with("p_"), ~signif(.,3)),
  506. across(starts_with("ci_"), ~round(.,3)))))
  507. write.csv(forest_df, file.path(FIGDIR, "Fig4_SuppTable4_correlations.csv"), row.names = FALSE)
  508. for (i in seq_len(nrow(forest_df))) {
  509. fr <- forest_df[i,]
  510. add_row("Fig4/SuppTable4", paste0(fr$Panel," ~ ", fr$Series), round(fr$rho_raw,3),
  511. "Spearman rho","", fmtp(fr$p_raw), fr$n_raw,
  512. "", "", round(fr$ci_low_raw,3), round(fr$ci_high_raw,3))
  513. if (fr$Panel == "rDLPFC")
  514. add_row("Fig4", paste0(fr$Panel," ~ ", fr$Series, " | age"), round(fr$rho_age,3),
  515. "partial Spearman","", fmtp(fr$p_age), fr$n_age,
  516. "", "", round(fr$ci_low_age,3), round(fr$ci_high_age,3))
  517. }
  518. ## 3-2 Mediation: sEBR -> rDLPFC -> switching accuracy [Supp Fig 5]
  519. med_df <- raw_data %>% dplyr::select(sEBR, sw_acc, r_DL_sw) %>% drop_na()
  520. cat("\n-- Mediation (Supp Fig 5): N =", nrow(med_df), "--\n")
  521. q75 <- quantile(med_df$sEBR, 0.75); q25 <- quantile(med_df$sEBR, 0.25)
  522. ## The sEBR "effect" in this mediation is the interquartile-range increase, so
  523. ## the two quartiles define the contrast being estimated and are reported in the
  524. ## Supplementary Information. Written to the summary CSV as their own rows.
  525. cat(sprintf("sEBR interquartile range used as the treatment contrast: %.2f -> %.2f blinks/min\n",
  526. q25, q75))
  527. add_row("SuppFig5","sEBR 25th percentile (control value)", round(unname(q25),2), "percentile",
  528. "","", nrow(med_df), "", "blinks/min; lower end of the interquartile-range contrast")
  529. add_row("SuppFig5","sEBR 75th percentile (treatment value)", round(unname(q75),2), "percentile",
  530. "","", nrow(med_df), "", "blinks/min; upper end of the interquartile-range contrast")
  531. model.m <- lm(r_DL_sw ~ sEBR, data = med_df)
  532. model.y <- lm(sw_acc ~ sEBR + r_DL_sw, data = med_df)
  533. set.seed(365)
  534. med.out <- mediate(model.m, model.y, treat = "sEBR", mediator = "r_DL_sw",
  535. treat.value = q75, control.value = q25,
  536. boot = FALSE, sims = 5000, robustSE = TRUE)
  537. print(summary(med.out))
  538. ms <- summary(med.out)
  539. add_row("SuppFig5","ACME (indirect via rDLPFC)", round(ms$d0,4), "quasi-Bayes","", fmtp(ms$d0.p), nrow(med_df), "", "exploratory",
  540. round(ms$d0.ci[1],4), round(ms$d0.ci[2],4))
  541. add_row("SuppFig5","ADE (direct)", round(ms$z0,4), "quasi-Bayes","", fmtp(ms$z0.p), nrow(med_df), "", "exploratory",
  542. round(ms$z0.ci[1],4), round(ms$z0.ci[2],4))
  543. add_row("SuppFig5","Proportion mediated", round(ms$n0,4), "quasi-Bayes","", "", nrow(med_df), "",
  544. "exploratory; kept to 4 dp because the manuscript quotes this as a percentage to 2 dp",
  545. round(ms$n0.ci[1],4), round(ms$n0.ci[2],4))
  546. sens.out <- medsens(med.out, rho.by = 0.1, sims = 1000)
  547. print(summary(sens.out))
  548. ## Sensitivity to violations of sequential ignorability. `err.cr.d` is the value
  549. ## of the residual correlation rho at which the ACME is driven to zero -- the
  550. ## quantity quoted in the Supplementary Information. It has length 2 only when
  551. ## the ACME differs between treatment and control groups, which is not the case
  552. ## here; the guard keeps the row well defined either way. Note that the grid is
  553. ## stepped by rho.by = 0.1, so this threshold is resolved to that granularity.
  554. rho0 <- unname(sens.out$err.cr.d)
  555. add_row("SuppFig5","Sensitivity: rho at which ACME = 0",
  556. paste(round(rho0, 2), collapse = "; "), "rho",
  557. "","", nrow(med_df), "",
  558. paste0("residual correlation between the mediator and outcome errors at which ",
  559. "the ACME reaches zero; evaluated on a grid with rho.by = 0.1"))
  560. ## =============================================================================
  561. ## SECTION 4 -- Supp Note 1 (sex/site/trials), Supp Note 4 (outliers),
  562. ## Methods (low pre-switch accuracy exclusion)
  563. ## =============================================================================
  564. cat("\n\n========== SECTION 4: Robustness / sensitivity ==========\n")
  565. ## 4-1 sex / site adjusted associations [Supp Note 1]
  566. pairs <- list(c("sEBR","r_DL_sw","sEBR-rDLPFC"),
  567. c("sEBR","sw_acc","sEBR-sw_acc"),
  568. c("r_DL_sw","sw_acc","rDLPFC-sw_acc"))
  569. for (cv in c("sex","site")) {
  570. if (all(is.na(raw_data[[cv]]))) next
  571. cat(sprintf("\n-- adjust for %s --\n", cv))
  572. for (pr in pairs) {
  573. r <- pspear(raw_data[[pr[1]]], raw_data[[pr[2]]], raw_data[cv])
  574. cat(sprintf("%-14s rho=%.2f p=%s n=%d\n", pr[3], r["rho"], fmtp(r["p"]), r["n"]))
  575. add_row("SuppNote1", paste0(pr[3]," | ", cv), round(r["rho"],3), "partial Spearman","", fmtp(r["p"]), r["n"],
  576. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  577. }
  578. }
  579. ## 4-2 LME robustness: add sex / site / phase-matched trial count [Supp Note 1]
  580. dlx <- df_long %>% mutate(sex = raw_data$sex[match(Subject, raw_data$ID)],
  581. site = raw_data$site[match(Subject, raw_data$ID)])
  582. fit_lrt <- function(extra, sub) {
  583. d <- dlx %>% drop_na(OxyHb, age, all_of(sub))
  584. m0 <- lmer(OxyHb ~ (Phase*Hemisphere*Region)*age + (1|Subject), d, REML = FALSE)
  585. m1 <- lmer(as.formula(paste0("OxyHb ~ (Phase*Hemisphere*Region)*age + ", extra, " + (1|Subject)")),
  586. d, REML = FALSE)
  587. an <- anova(m0, m1); list(chi = an$Chisq[2], df = an$Df[2], p = an$`Pr(>Chisq)`[2])
  588. }
  589. for (s in list(c("sex","sex"), c("site","site"))) {
  590. r <- fit_lrt(s[1], s[2]); cat(sprintf("LME +%s: chi2(%d)=%.3f p=%s\n", s[1], r$df, r$chi, fmtp(r$p)))
  591. add_row("SuppNote1", paste0("LME age vs age+", s[1]), round(r$chi,3), "chi2", r$df, fmtp(r$p), "", "age")
  592. }
  593. dl2 <- df_long %>% drop_na(OxyHb, age, PhaseRep)
  594. m_a <- lmer(OxyHb ~ (Phase*Hemisphere*Region)*age + (1|Subject), dl2, REML = FALSE)
  595. m_r <- lmer(OxyHb ~ (Phase*Hemisphere*Region)*age + PhaseRep + (1|Subject), dl2, REML = FALSE)
  596. m_rm <- lmer(OxyHb ~ (Phase*Hemisphere*Region)*age + PhaseRep*age + (1|Subject), dl2, REML = FALSE)
  597. # Step 1: does the PhaseRep main effect improve fit over the age model? (1 df)
  598. # Step 2: does the PhaseRep x age interaction improve fit over the model that
  599. # ALREADY contains the PhaseRep main effect? (1 df)
  600. # Step 2 must compare m_r with m_rm, not m_a with m_rm: the latter would test the
  601. # main effect and the interaction jointly on 2 df. The comparison below isolates
  602. # the interaction on 1 df, as the label states.
  603. lr1 <- anova(m_a, m_r) # m_a -> m_a + PhaseRep
  604. lr2 <- anova(m_r, m_rm) # m_r -> m_r + PhaseRep:age
  605. stopifnot(lr1$Df[2] == 1, lr2$Df[2] == 1) # both steps must be 1 df
  606. cat(sprintf("LME +trial: chi2(%d)=%.3f p=%s | +trial x age: chi2(%d)=%.3f p=%s\n",
  607. lr1$Df[2], lr1$Chisq[2], fmtp(lr1$`Pr(>Chisq)`[2]),
  608. lr2$Df[2], lr2$Chisq[2], fmtp(lr2$`Pr(>Chisq)`[2])))
  609. add_row("SuppNote1","LME age vs age+PhaseRep", round(lr1$Chisq[2],3),"chi2",lr1$Df[2],
  610. fmtp(lr1$`Pr(>Chisq)`[2]),"","age+rep",
  611. "LRT of the PhaseRep main effect (m_a vs m_r)")
  612. add_row("SuppNote1","LME PhaseRep x age", round(lr2$Chisq[2],3),"chi2",lr2$Df[2],
  613. fmtp(lr2$`Pr(>Chisq)`[2]),"","age+rep",
  614. "LRT of the PhaseRep x age interaction over the main-effect model (m_r vs m_rm)")
  615. # Reference only: the joint 2-df test of both terms together, retained for
  616. # traceability. The 1-df test above is the one reported.
  617. lr2_joint <- anova(m_a, m_rm)
  618. cat(sprintf(" [reference, joint 2-df test] +trial and trial x age: chi2(%d)=%.3f p=%s\n",
  619. lr2_joint$Df[2], lr2_joint$Chisq[2], fmtp(lr2_joint$`Pr(>Chisq)`[2])))
  620. add_row("SuppNote1","LME PhaseRep + PhaseRep x age (joint)", round(lr2_joint$Chisq[2],3),
  621. "chi2", lr2_joint$Df[2], fmtp(lr2_joint$`Pr(>Chisq)`[2]),"","age+rep",
  622. "joint 2-df test of both terms; reference only, superseded by the 1-df test above")
  623. arp <- anova(m_r)
  624. for (eff in c("Phase:age","Hemisphere:age","Region:age"))
  625. add_row("SuppNote1", paste0(eff," (with trial)"), round(arp[eff,"F value"],2),"F",
  626. paste0(arp[eff,"NumDF"],",",round(arp[eff,"DenDF"],0)), fmtp(arp[eff,"Pr(>F)"]),"","age+rep")
  627. ## Effect sizes for the trial-adjusted model and for the model comparisons.
  628. es_rep <- as.data.frame(eta_squared(anova(m_r, type = 3), partial = TRUE,
  629. ci = 0.95, alternative = "two.sided"))
  630. for (eff in c("Phase:age","Hemisphere:age","Region:age")) {
  631. i <- match(eff, es_rep$Parameter)
  632. add_row("SuppNote1", paste0("partial eta2 ", eff, " (with trial)"),
  633. signif(es_rep$Eta2_partial[i],3), "eta2_p","","","", "age+rep","",
  634. round(es_rep$CI_low[i],3), round(es_rep$CI_high[i],3))
  635. }
  636. for (s in c("sex","site")) {
  637. if (all(is.na(raw_data[[s]]))) next
  638. d <- dlx %>% drop_na(OxyHb, age, all_of(s))
  639. q0 <- lmer(OxyHb ~ (Phase*Hemisphere*Region)*age + (1|Subject), d, REML = FALSE)
  640. q1 <- lmer(as.formula(paste0("OxyHb ~ (Phase*Hemisphere*Region)*age + ", s, " + (1|Subject)")),
  641. d, REML = FALSE)
  642. dm <- r2_nakagawa(q1)$R2_marginal - r2_nakagawa(q0)$R2_marginal
  643. cat(sprintf("Change in marginal R2 when adding %s: %.4f\n", s, dm))
  644. add_row("SuppNote1", paste0("Change in marginal R2 with ", s), round(dm,4), "dR2","","","","age")
  645. }
  646. dm_rep <- r2_nakagawa(m_r)$R2_marginal - r2_nakagawa(m_a)$R2_marginal
  647. cat(sprintf("Change in marginal R2 when adding trial count: %.4f\n", dm_rep))
  648. add_row("SuppNote1","Change in marginal R2 with trial count", round(dm_rep,4),
  649. "dR2","","","","age+rep")
  650. ## 4-3 rep-corrected partial correlations [Supp Note 1]
  651. for (pr in list(c("sEBR","r_DL_sw","sEBR-rDLPFC"), c("sEBR","sw_acc","sEBR-sw_acc"))) {
  652. for (z in list(list("sw_rep","| sw_rep"), list(c("age","sw_rep"),"| age+sw_rep"))) {
  653. r <- pspear(raw_data[[pr[1]]], raw_data[[pr[2]]], raw_data[z[[1]]])
  654. add_row("SuppNote1", paste0(pr[3]," ", z[[2]]), round(r["rho"],3), "partial Spearman","", fmtp(r["p"]), r["n"],
  655. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  656. }
  657. }
  658. ## 4-4 completed-trial descriptives + age x trials [Methods / Supp Note 1]
  659. desc <- raw_data %>% summarise(across(c(Pre_rep, Post_rep, Mix_rep),
  660. list(m = ~mean(.,na.rm=TRUE), s = ~sd(.,na.rm=TRUE))))
  661. print(desc)
  662. for (ph in c("Pre_rep","Post_rep","Mix_rep")) {
  663. add_row("Methods/SuppNote1", paste0("completed trials ", ph),
  664. sprintf("%.1f+/-%.1f", mean(raw_data[[ph]],na.rm=TRUE), sd(raw_data[[ph]],na.rm=TRUE)), "desc","","",nrow(raw_data))
  665. ra <- sp(raw_data$age, raw_data[[ph]])
  666. add_row("SuppNote1", paste0("age ~ ", ph), round(ra["rho"],3), "Spearman rho","", fmtp(ra["p"]), ra["n"],
  667. "", "", round(ra["ci_low"],3), round(ra["ci_high"],3))
  668. }
  669. ## 4-5 Outlier robustness [Supp Note 4]
  670. raw_data$sEBR_w <- winsorize(raw_data$sEBR, 0.05)
  671. thr <- mean(raw_data$sEBR, na.rm = TRUE) + 3*sd(raw_data$sEBR, na.rm = TRUE)
  672. trim <- raw_data %>% filter(is.na(sEBR) | sEBR <= thr)
  673. for (z in list(list("sEBR_w","sw_acc","winsorized","sEBR-sw_acc"),
  674. list("sEBR_w","r_DL_sw","winsorized","sEBR-rDLPFC"))) {
  675. r <- sp(raw_data[[z[[1]]]], raw_data[[z[[2]]]])
  676. add_row("SuppNote4", paste0(z[[4]]," ", z[[3]]), round(r["rho"],3),"Spearman rho","", fmtp(r["p"]), r["n"],
  677. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  678. }
  679. for (z in list(list("sw_acc","trimmed(n-3)","sEBR-sw_acc"), list("r_DL_sw","trimmed(n-3)","sEBR-rDLPFC"))) {
  680. r <- sp(trim$sEBR, trim[[z[[1]]]])
  681. add_row("SuppNote4", paste0(z[[3]]," ", z[[2]]), round(r["rho"],3),"Spearman rho","", fmtp(r["p"]), r["n"],
  682. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  683. }
  684. ## 4-6 Low pre-switch accuracy exclusion [Methods]
  685. excl <- raw_data %>% filter(pre_acc <= 0.5)
  686. cat("\nExcluded (pre_acc<=0.5): n =", nrow(excl), "| IDs:", paste(excl$ID, collapse=","), "\n")
  687. de <- raw_data %>% filter(pre_acc > 0.5)
  688. for (z in list(list("sEBR","sw_acc","sEBR-sw_acc"), list("sEBR","r_DL_sw","sEBR-rDLPFC"),
  689. list("age","sw_acc","age-sw_acc"), list("age","sEBR","age-sEBR"))) {
  690. r <- sp(de[[z[[1]]]], de[[z[[2]]]])
  691. add_row("Methods", paste0("after excl: ", z[[3]]), round(r["rho"],3),"Spearman rho","", fmtp(r["p"]), r["n"],
  692. "", "", round(r["ci_low"],3), round(r["ci_high"],3))
  693. }
  694. ## =============================================================================
  695. ## SECTION 5 -- Write summary CSVs
  696. ## =============================================================================
  697. cat("\n\n========== SECTION 5: Writing summaries ==========\n")
  698. summary_df <- dplyr::bind_rows(SUMMARY)
  699. write.csv(summary_df, file.path(OUTDIR, "results_summary_generated.csv"), row.names = FALSE)
  700. cat("Wrote:", file.path(OUTDIR, "results_summary_generated.csv"),
  701. "(", nrow(summary_df), "rows )\n")
  702. cat("Source-data CSVs in:", FIGDIR, "\n")
  703. ## ---- session information -----------------------------------------------
  704. ## Written to outputs_summary/sessionInfo.txt as well as echoed to the console.
  705. ## The script name below is the single source of truth for that file's header;
  706. ## keep it in sync with the filename if this script is revised again.
  707. SCRIPT_NAME <- "MainAnalysis.R"
  708. si_path <- file.path(OUTDIR, "sessionInfo.txt")
  709. si_con <- file(si_path, open = "wt")
  710. writeLines(c(
  711. paste0(SCRIPT_NAME, " -- session information"),
  712. paste0("Generated: ", format(Sys.time(), "%Y-%m-%d %H:%M:%S %Z")),
  713. paste0("Data file: ", data_path),
  714. ""), si_con)
  715. capture.output(sessionInfo(), file = si_con)
  716. close(si_con)
  717. cat("Wrote:", si_path, "\n")
  718. cat("\n=== Session info ===\n"); print(sessionInfo())
  719. # ==============================================================================
  720. # END
  721. # ==============================================================================

MainAnalysis.R, no license · at the source

Overview

Authors: Ryuta Kuwamizu1,2, Nozomi Yamamoto2, Kota Otani1,2, Yusuke Moriguchi2
  1. Institute of Health and Sport Sciences, University of Tsukuba, Ibaraki, Japan
  2. Graduate School of Letters, Kyoto University, Kyoto, Japan
Institutions: University of Tsukuba (Japan); Kyoto University (Japan)
Journal: Communications psychology, volume 4, issue 1, article 126
Dates: received 9 March 2026; accepted 4 August 2026; published online 15 September 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s44271-026-00524-6 · PMID 42744968 · PMCID PMC13578739 · OpenAlex W7213256404
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), cognitive (subfield)
Methods: Statistics, Connectivity, fMRI & imaging, Smoothing, state filtering, decompositions, Physiology & signal measures
Keywords: Cognitive control, Human behaviour
Topic: Ocular Surface and Contact Lens (Public Health, Environmental and Occupational Health, Medicine), according to OpenAlex
Funding: MEXT | Japan Society for the Promotion of Science (JSPS) (23H04830, 24K00486, 23KJ1169, 24K20598)
Citations: not cited yet (Europe PMC); 82 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

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

OSF mrz89

License: none: the authors keep all their rights
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Languages: R (2)
Size: 7 files, 2 scripts
Software Heritage: not checked
Found in: “Code availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: tidyverse (2 files), easystats (1 file), emmeans (1 file), ggplot2 (1 file), lme4 (1 file), lmerTest (1 file), patchwork (1 file), rstatix (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers (HTTP 200)
  • 26 September 2026: the link answers (HTTP 200)
3 files
At the source:

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:

  • it points to the authors' code: OSF mrz89

Read it in the paper: doi.org/10.1038/s44271-026-00524-6.

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:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • 6 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

No dataset and no data link were found in the paper.

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/s44271-026-00524-6.

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 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 2 keywords, 1 funder, 80 references.

Cite

This paper

Kuwamizu, R., Yamamoto, N., Otani, K., & Moriguchi, Y. (2026). Emergent blink rate in early childhood is associated with neural origins of executive function. Communications psychology, 4(1), 126. https://doi.org/10.1038/s44271-026-00524-6

BibTeX

@article{kuwamizu2026emergent,
author = {Kuwamizu, Ryuta and Yamamoto, Nozomi and Otani, Kota and Moriguchi, Yusuke},
title = {{Emergent blink rate in early childhood is associated with neural origins of executive function}},
journal = {Communications psychology},
year = {2026},
month = sep,
volume = {4},
number = {1},
pages = {126},
publisher = {Nature Publishing Group},
issn = {2731-9121},
doi = {10.1038/s44271-026-00524-6},
url = {https://doi.org/10.1038/s44271-026-00524-6},
pmid = {42744968},
pmcid = {PMC13578739}
}

RIS

TY - JOUR
AU - Kuwamizu, Ryuta
AU - Yamamoto, Nozomi
AU - Otani, Kota
AU - Moriguchi, Yusuke
TI - Emergent blink rate in early childhood is associated with neural origins of executive function
T2 - Communications psychology
J2 - Commun Psychol
PY - 2026
DA - 2026/09/15
VL - 4
IS - 1
SP - 126
SN - 2731-9121
PB - Nature Publishing Group
DO - 10.1038/s44271-026-00524-6
UR - https://doi.org/10.1038/s44271-026-00524-6
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s44271-026-00524-6",
"type": "article-journal",
"title": "Emergent blink rate in early childhood is associated with neural origins of executive function",
"container-title": "Communications psychology",
"author": [
{
"family": "Kuwamizu",
"given": "Ryuta"
},
{
"family": "Yamamoto",
"given": "Nozomi"
},
{
"family": "Otani",
"given": "Kota"
},
{
"family": "Moriguchi",
"given": "Yusuke"
}
],
"container-title-short": "Commun Psychol",
"volume": "4",
"issue": "1",
"page": "126",
"DOI": "10.1038/s44271-026-00524-6",
"PMID": "42744968",
"PMCID": "PMC13578739",
"ISSN": "2731-9121",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s44271-026-00524-6",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
15
]
]
}
}

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-69950-8 [code]
Dopaminergic processes predict temporal distortions in event memory.
Journal: Nature communications
In common: rstatix, easystats, emmeans, 4 other tools, cognitive, 8 references
[2] doi:10.1073/pnas.2603114123 [code]
The human hippocampus can pattern separate memories by meaning.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: rstatix, easystats, emmeans, 5 other tools, cognitive
[3] doi:10.1016/j.neuroimage.2026.122115 [code]
Midfrontal theta power relates to response speeding following frustrative nonreward.
Journal: NeuroImage
In common: rstatix, easystats, emmeans, 5 other tools, cognitive
[4] doi:10.1038/s41467-026-74753-y [code]
A human-specific microRNA controls the timing of excitatory synaptogenesis.
Journal: Nature communications
In common: rstatix, easystats, emmeans, 5 other tools
[5] doi:10.1038/s41398-026-04010-9 [code]
Bullying victimization and brain development: a longitudinal structural magnetic resonance imaging study from adolescence to early adulthood.
Journal: Translational psychiatry
In common: rstatix, easystats, emmeans, 5 other tools
[6] doi:10.64898/2026.05.08.26348885 [code]
Insights from nine nights of self-applied, low-density sleep EEG during sleep restriction therapy: a proof-of-concept evaluation
Journal: medRxiv (preprint)
In common: rstatix, easystats, emmeans, 5 other tools
[7] doi:10.1126/sciadv.aeb8106 [code]
A thyroid hormone-mediated opsin switch initiates metamorphosis in a proto-vertebrate.
Journal: Science advances
In common: rstatix, easystats, emmeans, 5 other tools
[8] doi:10.1016/j.isci.2026.116047 [code]
Deciding to simulate: Cognitive mechanisms of predicting the decisions of others.
Journal: iScience
In common: rstatix, easystats, emmeans, 4 other tools, cognitive
[9] doi:10.1038/s41467-026-73865-9 [code]
Histamine shapes the neurocomputational dynamics of human learning.
Journal: Nature communications
In common: rstatix, easystats, emmeans, 4 other tools, cognitive
[10] doi:10.1038/s42003-026-10040-2 [code]
Functional dissociation of language and theory of mind in the developing superior temporal lobe.
Journal: Communications biology
In common: rstatix, emmeans, lmerTest, 3 other tools, cognitive, 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.