OSCR

Frontoparietal control-default mode connectivity predicts TMS effects on cognitive control.

Code ↔ Paper

5 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 5 matches
  1. [1] § Methods › Behavioral data processing and modeling ↔ 4_DDM_Validation.Rmd, lines 148–218 · score 0.67 · posterior predictive checks, fast dm, Validations, DDM, model
  2. [2] § Methods › Behavioral data processing and modeling ↔ 3_DDMAnalysis.Rmd, lines 421–500 · score 0.56 · Exploratory factor, paran, parallel, psych, factor model, components
  3. [3] § Methods › Behavioral data processing and modeling ↔ 5_Alternative_Models_Raw.Rmd, lines 473–552 · score 0.56 · Exploratory factor, paran, parallel, psych, factor model, components
  4. [4] § Methods › Statistical analysis ↔ 7_Alternative_Model_MRI_SCRUBBED_DDM_Analysis.Rmd, lines 17–88 · score 0.56 · framewise displacement, FD, MRI, head, models
  5. [5] § Results › Validation of factor structure ↔ 5_Alternative_Models_Raw.Rmd, lines 621–633 · score 0.52 · chi square, full model, CFA, CFI, RMSEA, SRMR

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 Markdown · 2,660 lines · 108 KB · no license · 2 matches

  1. ---
  2. title: "Raw_Data_Analysis"
  3. output: html_document
  4. date: "2024-02-19"
  5. ---
  6. ```{r setup, include=FALSE}
  7. knitr::opts_chunk$set(echo = TRUE)
  8. ```
  9. ```{r}
  10. library(tidyverse)
  11. library(dplyr)
  12. library(ggplot2)
  13. ```
  14. # Factor Analysis for RT
  15. ## Load Data
  16. **Note: The Response Time (RT) analysis is followed by an identical automated pipeline for Accuracy (ACC) starting near line ~1420.**
  17. ```{r}
  18. # ---------------- Navon ----------------
  19. behavioral_data_navon <- read.csv("FinalData/Navon_Behavioral_LongFormat_PRISM_final_x4_z_day.csv")
  20. participants_without_FPCN_B_navon <- behavioral_data_navon %>%
  21. group_by(Subj) %>%
  22. summarise(has_FPCN_B = any(Stimulation_Site == "FPCN-B")) %>%
  23. filter(!has_FPCN_B) %>%
  24. pull(Subj)
  25. cleaned_data_navon <- behavioral_data_navon %>%
  26. filter(!Subj %in% participants_without_FPCN_B_navon) %>%
  27. filter(RT >= 0.200 & RT <= 1.5) %>%
  28. mutate(Stimulation_Site = factor(Stimulation_Site, levels = c("Vertex", "FPCN-B", "DAN")))
  29. navon_raw_data <- cleaned_data_navon %>%
  30. group_by(Subj, Stimulation_Site, Timepoint, Task_Low_High, Days) %>%
  31. summarize(rt = mean(RT[correct == 1], na.rm=TRUE), acc = mean(correct, na.rm=TRUE), .groups = "drop")
  32. # ---------------- Stroop ----------------
  33. behavioral_data_stroop <- read.csv("FinalData/Stroop_Behavioral_LongFormat_PRISM_final_x3_z_day.csv")
  34. participants_without_FPCN_B_stroop <- behavioral_data_stroop %>%
  35. group_by(Subj) %>%
  36. summarise(has_FPCN_B = any(Stimulation_Site == "FPCN-B")) %>%
  37. filter(!has_FPCN_B) %>%
  38. pull(Subj)
  39. cleaned_data_stroop <- behavioral_data_stroop %>%
  40. filter(!Subj %in% participants_without_FPCN_B_stroop) %>%
  41. filter(RT >= 0.200 & RT <= 1.5) %>%
  42. mutate(Stimulation_Site = factor(Stimulation_Site, levels = c("Vertex", "DAN", "FPCN-B")))
  43. stroop_raw_data <- cleaned_data_stroop %>%
  44. group_by(Subj, Stimulation_Site, Timepoint, Task_Low_High, Days) %>%
  45. summarize(rt = mean(RT[correct == 1], na.rm=TRUE), acc = mean(correct, na.rm=TRUE), .groups = "drop")
  46. # ---------------- NBack ----------------
  47. behavioral_data_nback <- read.csv("FinalData/n-back_exp_results.csv")
  48. participants_without_FPCN_B_nback <- behavioral_data_nback %>%
  49. group_by(Subj) %>%
  50. summarise(has_FPCN_B = any(Stimulation_Site == "FPCNB")) %>%
  51. filter(!has_FPCN_B) %>%
  52. pull(Subj)
  53. cleaned_data_nback <- behavioral_data_nback %>%
  54. filter(!Subj %in% participants_without_FPCN_B_nback) %>%
  55. filter(RT >= 0.200 & RT <= 1.5)
  56. nback_raw_data <- cleaned_data_nback %>%
  57. mutate(Task_Low_High = case_when(
  58. Condition == 0 ~ "low",
  59. Condition == 1 ~ "medium",
  60. Condition == 2 ~ "high"
  61. ),
  62. Stimulation_Site = case_when(
  63. Stimulation_Site == "vertex" ~ "Vertex",
  64. Stimulation_Site == "FPCNB" ~ "FPCN-B",
  65. TRUE ~ Stimulation_Site
  66. )) %>%
  67. group_by(Subj, Stimulation_Site, Timepoint, Task_Low_High, Days) %>%
  68. summarize(rt = mean(RT[correct == 1], na.rm=TRUE), acc = mean(correct, na.rm=TRUE), .groups = "drop")
  69. ```
  70. ```{r}
  71. # Filter out participants with Days z-score > 3
  72. navon_raw_data <- navon_raw_data %>%
  73. filter(abs((Days - mean(Days, na.rm = TRUE)) / sd(Days, na.rm = TRUE)) <= 2.5)
  74. nback_raw_data <- nback_raw_data %>%
  75. filter(abs((Days - mean(Days, na.rm = TRUE)) / sd(Days, na.rm = TRUE)) <= 2.5)
  76. stroop_raw_data <- stroop_raw_data %>%
  77. filter(abs((Days - mean(Days, na.rm = TRUE)) / sd(Days, na.rm = TRUE)) <= 2.5)
  78. ```
  79. Quick checks on distribution of days for counterbalancing, we want to make sure that there's nothing qualitatively different about DAN
  80. ```{r}
  81. summary_stats <- stroop_raw_data %>%
  82. group_by(Stimulation_Site) %>%
  83. summarise(
  84. mean_Days = mean(Days, na.rm = TRUE),
  85. sd_Days = sd(Days, na.rm = TRUE),
  86. median_Days = median(Days, na.rm = TRUE)
  87. )
  88. # Plot histograms for each statistic in separate facets
  89. ggplot(stroop_raw_data, aes(x = Days)) +
  90. geom_histogram(binwidth = 1, fill = "lightblue", color = "black") +
  91. facet_wrap(~ Stimulation_Site, scales = "free_y") +
  92. theme_minimal() +
  93. labs(
  94. title = "NBack Histograms of Days",
  95. x = "Days",
  96. y = "Count"
  97. )
  98. ```
  99. ```{r}
  100. navon_wide <- navon_raw_data %>%
  101. pivot_wider(
  102. id_cols = c(Subj, Stimulation_Site, Timepoint),
  103. names_from = Task_Low_High,
  104. values_from = c(rt, acc),
  105. names_prefix = ""
  106. )
  107. stroop_wide <- stroop_raw_data %>%
  108. pivot_wider(
  109. id_cols = c(Subj, Stimulation_Site, Timepoint),
  110. names_from = Task_Low_High,
  111. values_from = c(rt, acc),
  112. names_prefix = ""
  113. )
  114. nback_wide <- nback_raw_data %>%
  115. pivot_wider(
  116. id_cols = c(Subj, Stimulation_Site, Timepoint),
  117. names_from = Task_Low_High,
  118. values_from = c(rt, acc),
  119. names_prefix = ""
  120. )
  121. navon_pre <- subset(navon_wide, Timepoint == "pre")
  122. stroop_pre <- subset(stroop_wide, Timepoint == "pre")
  123. nback_pre <- subset(nback_wide, Timepoint == "pre")
  124. navon_post <- subset(navon_wide, Timepoint == "post")
  125. stroop_post <- subset(stroop_wide, Timepoint == "post")
  126. nback_post <- subset(nback_wide, Timepoint == "post")
  127. ```
  128. ```{r}
  129. # Function to rename columns with a prefix
  130. add_prefix <- function(df, prefix) {
  131. # Don't rename key columns (Subj, Stimulation_Site, Timepoint)
  132. non_key_cols <- setdiff(names(df), c("Subj", "Stimulation_Site", "Timepoint"))
  133. # Add the prefix to non-key columns
  134. names(df)[names(df) %in% non_key_cols] <- paste(prefix, names(df)[names(df) %in% non_key_cols], sep = "_")
  135. return(df)
  136. }
  137. # Add custom prefixes to each dataset
  138. navon_wide_prefixed <- add_prefix(navon_wide, "navon")
  139. stroop_wide_prefixed <- add_prefix(stroop_wide, "stroop")
  140. nback_wide_prefixed <- add_prefix(nback_wide, "nback")
  141. # Add custom prefixes to each dataset
  142. navon_pre <- add_prefix(navon_pre, "navon")
  143. stroop_pre <- add_prefix(stroop_pre, "stroop")
  144. nback_pre <- add_prefix(nback_pre, "nback")
  145. # Add custom prefixes to each dataset
  146. navon_post <- add_prefix(navon_post, "navon")
  147. stroop_post <- add_prefix(stroop_post, "stroop")
  148. nback_post <- add_prefix(nback_post, "nback")
  149. # Add custom prefixes to each dataset
  150. navon_pre_c <- add_prefix(navon_pre, "navon_pre")
  151. stroop_pre_c <- add_prefix(stroop_pre, "stroop_pre")
  152. nback_pre_c <- add_prefix(nback_pre, "nback_pre")
  153. # Add custom prefixes to each dataset
  154. navon_post_c <- add_prefix(navon_post, "navon_post")
  155. stroop_post_c <- add_prefix(stroop_post, "stroop_post")
  156. nback_post_c <- add_prefix(nback_post, "nback_post")
  157. ```
  158. ## Handle Missing Data
  159. ```{r}
  160. # Perform the join using merge
  161. combined_data_pre <- Reduce(function(x, y) merge(x, y, by = c("Subj", "Stimulation_Site", "Timepoint"), all = TRUE),
  162. list(navon_pre, stroop_pre, nback_pre)) # Add all your data tables
  163. combined_data_post <- Reduce(function(x, y) merge(x, y, by = c("Subj", "Stimulation_Site", "Timepoint"), all = TRUE),
  164. list(navon_post, stroop_post, nback_post)) # Add all your data tables
  165. # Perform the join using merge
  166. combined_data_pre_with_prefix <- Reduce(function(x, y) merge(x, y, by = c("Subj", "Stimulation_Site", "Timepoint"), all = TRUE),
  167. list(navon_pre_c, stroop_pre_c, nback_pre_c)) # Add all your data tables
  168. combined_data_post_with_prefix <- Reduce(function(x, y) merge(x, y, by = c("Subj", "Stimulation_Site", "Timepoint"), all = TRUE),
  169. list(navon_post_c, stroop_post_c, nback_post_c)) # Add all your data tables
  170. # Perform the join using merge
  171. combined_data_wide <- Reduce(function(x, y) merge(x, y, by = c("Subj", "Stimulation_Site", "Timepoint"), all = TRUE),
  172. list(navon_wide_prefixed, stroop_wide_prefixed, nback_wide_prefixed)) # Add all your data tables
  173. # # Remove rows where Stimulation_Site equals 'DAN'
  174. # combined_data_wide <- combined_data_wide[combined_data_wide$Stimulation_Site != "DAN", ]
  175. ```
  176. ## Combined both pre and post data into a single one
  177. ```{r}
  178. # Remove the Timepoint column from both datasets
  179. combined_data_pre_clean <- combined_data_pre_with_prefix %>% dplyr::select(-Timepoint)
  180. combined_data_post_clean <- combined_data_post_with_prefix %>% dplyr::select(-Timepoint)
  181. # Combine the datasets based on Subj and Stimulation_Site
  182. combined_data_all <- full_join(combined_data_pre_clean, combined_data_post_clean, by = c("Subj", "Stimulation_Site"))
  183. # View the combined dataset
  184. head(combined_data_all)
  185. ```
  186. # Factor Analysis for Response Time
  187. ```{r}
  188. metric_type <- 'RT'
  189. ```
  190. ## Summary Statistics
  191. ```{r}
  192. # Calculate Statistics for Response Times (Mean, Variance, Differences)
  193. library(dplyr)
  194. library(tidyr)
  195. library(stringr)
  196. # Filter for drift rate (v) columns
  197. rt_cols_stats <- grep("_rt_", colnames(combined_data_all), value = TRUE)
  198. # Create long format for statistics
  199. stats_data <- combined_data_all %>%
  200. dplyr::select(Subj, Stimulation_Site, all_of(rt_cols_stats)) %>%
  201. pivot_longer(
  202. cols = all_of(rt_cols_stats),
  203. names_to = "full_name",
  204. values_to = "Value"
  205. ) %>%
  206. mutate(
  207. Task = str_extract(full_name, "^[a-z]+"),
  208. Timepoint = ifelse(grepl("_pre_", full_name), "Pre", "Post"),
  209. Difficulty = str_extract(full_name, "(high|medium|low)$")
  210. ) %>%
  211. filter(!is.na(Value))
  212. # Calculate Summary Stats per Task
  213. task_stats <- stats_data %>%
  214. dplyr::select(Subj, Stimulation_Site, Task, Timepoint, Difficulty, Value) %>%
  215. pivot_wider(names_from = Difficulty, values_from = Value) %>%
  216. group_by(Task) %>%
  217. summarise(
  218. # Low Condition (Across all sites/timepoints)
  219. Mean_Low = mean(low, na.rm = TRUE),
  220. Var_Low = var(low, na.rm = TRUE),
  221. # Medium Condition (Across all sites/timepoints)
  222. Mean_Medium = mean(medium, na.rm = TRUE),
  223. Var_Medium = var(medium, na.rm = TRUE),
  224. # High Condition (Across all sites/timepoints)
  225. Mean_High = mean(high, na.rm = TRUE),
  226. Var_High = var(high, na.rm = TRUE),
  227. # Individual Difference (High - Low)
  228. Mean_Diff = mean(high - low, na.rm = TRUE),
  229. Var_Diff = var(high - low, na.rm = TRUE)
  230. )
  231. print(task_stats)
  232. ```
  233. ```{r}
  234. # 1. Reshape Data for Plotting
  235. library(tidyr)
  236. library(dplyr)
  237. library(ggplot2)
  238. library(stringr)
  239. library(RColorBrewer)
  240. # Filter for drift rate (v) columns
  241. v_cols <- grep("_rt_", colnames(combined_data_all), value = TRUE)
  242. # Pivot generic long format
  243. plot_data_long <- combined_data_all %>%
  244. dplyr::select(Subj, Stimulation_Site, all_of(v_cols)) %>%
  245. pivot_longer(
  246. cols = all_of(v_cols),
  247. names_to = "full_name",
  248. values_to = "Value"
  249. ) %>%
  250. mutate(
  251. # Extract Task: First word before underscore
  252. Task = str_extract(full_name, "^[a-z]+"),
  253. # Extract Timepoint: 'pre' or 'post'
  254. Timepoint = ifelse(grepl("_pre_", full_name), "Pre", "Post"),
  255. # Extract Difficulty: 'low', 'medium', 'high' at the end
  256. Difficulty = str_extract(full_name, "(high|medium|low)$")
  257. ) %>%
  258. mutate(
  259. # Set factor levels for correct ordering
  260. Difficulty = factor(str_to_title(Difficulty), levels = c("Low", "Medium", "High")),
  261. Timepoint = factor(Timepoint, levels = c("Pre", "Post")),
  262. Stimulation_Site = factor(Stimulation_Site, levels = c("Vertex", "FPCN-B", "DAN"), labels = c("Vertex", "L-FPN-B", "D-FPN"))
  263. ) %>%
  264. filter(!is.na(Value))
  265. # 2. Define Plotting Helper Function
  266. draw_task_boxplot <- function(data, task_name) {
  267. # Filter data for specific task
  268. task_df <- data %>% filter(Task == task_name)
  269. p <- ggplot(task_df, aes(x = Difficulty, y = Value)) +
  270. # Facet by Stimulation Site to separate the groups clearly
  271. facet_wrap(~ Stimulation_Site) +
  272. # Boxplots:
  273. # Fill by Stimulation Site (Consistent color within facet)
  274. # Alpha by Timepoint (Distinguish Pre vs Post as 2 bars)
  275. geom_boxplot(
  276. aes(fill = Stimulation_Site, alpha = Timepoint),
  277. position = position_dodge(width = 0.8),
  278. width = 0.6,
  279. outlier.shape = NA
  280. ) +
  281. # Individual Points
  282. geom_point(
  283. aes(shape = Timepoint), # Shape matches Pre/Post
  284. color = "gray25",
  285. position = position_jitterdodge(jitter.width = 0.1, dodge.width = 0.8),
  286. size = 1.5,
  287. alpha = 0.7
  288. ) +
  289. # Aesthetics
  290. scale_fill_brewer(palette = "Set1") +
  291. scale_alpha_manual(values = c("Pre" = 0.4, "Post" = 0.9)) + # Light=Pre, Dark=Post
  292. scale_shape_manual(values = c("Pre" = 16, "Post" = 17)) +
  293. # Override only the alpha legend to show gray fills, keeping plot colors intact
  294. guides(alpha = guide_legend(override.aes = list(fill = "black"))) +
  295. theme_classic(base_size = 18) +
  296. theme(
  297. strip.background = element_rect(fill = "gray95", color = NA),
  298. legend.position = "right",
  299. plot.margin = margin(5.5, 5.5, 5.5, 15, "pt") # Increase left margin to prevent 'Response Time' cutoff
  300. ) +
  301. labs(
  302. title = NULL,
  303. y = "Response Time (s)",
  304. x = "Difficulty Condition",
  305. fill = "Stimulation Site",
  306. alpha = "Timepoint",
  307. shape = "Timepoint"
  308. )
  309. # For N-Back, rotate labels to prevent overlap
  310. if (task_name == "nback") {
  311. p <- p + theme(axis.text.x = element_text(angle = 45, hjust = 1))
  312. }
  313. return(p)
  314. }
  315. # 3. Generate and Display Plots
  316. p_navon <- draw_task_boxplot(plot_data_long, "navon")
  317. print(p_navon)
  318. ggsave("Figures/RT_Navon_v.png", plot = p_navon, width = 8, height = 5, dpi = 300)
  319. p_stroop <- draw_task_boxplot(plot_data_long, "stroop")
  320. print(p_stroop)
  321. ggsave("Figures/RT_Stroop_v.png", plot = p_stroop, width = 8, height = 5, dpi = 300)
  322. p_nback <- draw_task_boxplot(plot_data_long, "nback")
  323. print(p_nback)
  324. ggsave("Figures/RT_NBack_v.png", plot = p_nback, width = 8, height = 5, dpi = 300)
  325. ```
  326. ## Assumption Tests
  327. We should check that our data fits the standard assumptions for a SEM (normality and linearity). Specifically, the relationships with the output should be linear, and the distributions of the input should be multivariate normal.
  328. However, despite our inputs failing tests for normality, the MLR estimator builds robust standard errors that are able to handle non-normality.
  329. ```{r}
  330. # Load necessary libraries
  331. library(ggplot2)
  332. library(MVN)
  333. library(dplyr)
  334. library(tidyr)
  335. # 1) Draw histograms for navon, stroop, and nback columns in multiple subplots
  336. # Reshape data for easy plotting
  337. combined_data_long <- combined_data_wide %>%
  338. dplyr::select(navon_rt_high, navon_rt_low, stroop_rt_high, stroop_rt_low, nback_rt_low, nback_rt_medium, nback_rt_high) %>%
  339. tidyr::pivot_longer(cols = everything(), names_to = "Variable", values_to = "Value")
  340. # Plot histograms with each variable in a separate subplot
  341. ggplot(combined_data_long, aes(x = Value)) +
  342. geom_histogram(aes(fill = Variable), color = "black", bins = 20, alpha = 0.6) +
  343. facet_wrap(~ Variable, scales = "free") +
  344. labs(title = "Histograms for Navon, Stroop, and Nback Variables", x = "Value", y = "Frequency") +
  345. theme_minimal() +
  346. theme(legend.position = "none")
  347. # 2) Multivariate normal distribution test
  348. # Select only the relevant columns for testing
  349. multivariate_data <- combined_data_wide %>%
  350. dplyr::select(navon_rt_high, navon_rt_low, stroop_rt_high, stroop_rt_low, nback_rt_low, nback_rt_medium, nback_rt_high)
  351. # Run Mardia's multivariate normality test
  352. mardia_test <- mvn(multivariate_data, mvnTest = "mardia")
  353. # Print results
  354. print(mardia_test)
  355. # Interpretation:
  356. # If mardia_test$multivariateNormality$pValue.skew > 0.05 and mardia_test$multivariateNormality$pValue.kurt > 0.05,
  357. # the data can be considered to follow a multivariate normal distribution.
  358. ```
  359. ### Linearity Test
  360. We can check for linear relationships with each other. The output variable rt_general shows a general linear relationship with the otehr variables.
  361. ```{r}
  362. # Load necessary packages
  363. library(GGally)
  364. library(dplyr)
  365. # Select only the columns with 'rt_general' and the task-specific variables
  366. plot_data <- combined_data_wide %>%
  367. select(rt_general, navon_rt_high, navon_rt_low, stroop_rt_high, stroop_rt_low, nback_rt_low, nback_rt_medium, nback_rt_high)
  368. # Create scatterplot matrix
  369. p <- ggpairs(plot_data, columns = 1:ncol(plot_data),
  370. title = "Scatterplots of rt_general with Task-specific Variables")
  371. # Save to file with 300 DPI
  372. ggsave("Figures/RT_Fig0_Linearity_Assumption.png", plot = p, dpi = 300, width = 12, height = 10)
  373. ```
  374. ## Run Factor Analysis (With Task Specific)
  375. ### Run Exploratory Factor Analysis with Parallel Analysis
  376. This analysis is to determine whether the single factor or bifactor model makes more sense for our data.
  377. For the parallel analysis, we can see when the unadjusted EV falls under the Random EV, at which point we do not want to use
  378. that many factors. We can see in the output that 1 factor is sufficient, and 2 factors just misses the cutoff.
  379. Therefore, we can justify our decision to use a single factor model.
  380. #### Plot Parallel Analysis Scree Plot
  381. ```{r}
  382. # Load necessary libraries
  383. # install.packages(c("psych", "paran"))
  384. library(psych)
  385. library(paran)
  386. library(dplyr)
  387. # Subset the data to include only columns starting with "navon", "stroop", and "nback"
  388. selected_data <- combined_data_wide[, grepl("^(navon|stroop|nback)_rt_", colnames(combined_data_wide))]
  389. # selected_data$nback_rt_medium <- NULL
  390. # Remove rows with any NA values
  391. selected_data_complete <- na.omit(selected_data)
  392. # Run parallel analysis on the complete dataset
  393. paran_results <- paran(selected_data_complete, iterations = 1000, centile = 95, graph = TRUE)
  394. # print(paran_results)
  395. # Manually Graph Results
  396. # Extract values from paran_results
  397. adj_ev <- paran_results$AdjEv
  398. ev <- paran_results$Ev
  399. rnd_ev <- paran_results$RndEv
  400. # Create data frame
  401. plot_data <- data.frame(
  402. Component = seq_along(ev),
  403. Adjusted_EV = adj_ev,
  404. Unadjusted_EV = ev,
  405. Random_EV = rnd_ev
  406. )
  407. plot_data$Retained <- plot_data$Adjusted_EV > plot_data$Random_EV
  408. # Create the plot
  409. p <- ggplot(plot_data, aes(x = Component)) +
  410. # Lines
  411. geom_line(aes(y = Adjusted_EV, color = "Adjusted EV"), size = 1) +
  412. geom_line(aes(y = Unadjusted_EV, color = "Unadjusted EV"), linetype = "dashed", size = 1) +
  413. geom_line(aes(y = Random_EV, color = "Random EV"), linetype = "dotted", size = 1) +
  414. # Points
  415. geom_point(aes(y = Adjusted_EV, shape = Retained), size = 3, color = "black") +
  416. geom_point(aes(y = Unadjusted_EV), size = 3, color = "red") +
  417. geom_point(aes(y = Random_EV), size = 3, color = "blue") +
  418. # Labels and theme
  419. labs(
  420. title = "Parallel Analysis Scree Plot",
  421. x = "Number of Components",
  422. y = "Eigenvalue",
  423. color = "Legend",
  424. shape = "Retained"
  425. ) +
  426. scale_color_manual(values = c("Adjusted EV" = "black", "Unadjusted EV" = "red", "Random EV" = "blue")) +
  427. scale_shape_manual(values = c(`TRUE` = 16, `FALSE` = 1)) +
  428. theme_minimal(base_size = 14, base_family = "Arial") +
  429. theme(
  430. plot.title = element_text(hjust = 0.5, face = "bold", size = 28),
  431. axis.title = element_text(size = 24),
  432. axis.text = element_text(size = 16),
  433. legend.title = element_text(size = 18), # Legend title font size
  434. legend.text = element_text(size = 16), # Legend item labels
  435. legend.box.background = element_rect(color = "black", fill = NA),
  436. legend.box = "vertical"
  437. )
  438. # Save plot as high-resolution PNG
  439. ggsave("Figures/RT_revised_parallel_analysis_plot.png", plot = p, width = 8, height = 6, dpi = 300, bg="white")
  440. ```
  441. ### Fit Model
  442. Couple notes to keep track of:
  443. Including the nback_rt_medium does cause the fit to get a lot worse. It's the variable with the lowest loading.
  444. Additionally, the loading is not significant. However, removing this variable does not change the results, so I think
  445. it's fine to proceed for now.
  446. ```{r}
  447. library(lavaan)
  448. set.seed(1234)
  449. model <- '
  450. # General factors (pre and post)
  451. rt_general =~ navon_rt_low + navon_rt_high + stroop_rt_low + stroop_rt_high + nback_rt_low + nback_rt_high + nback_rt_medium
  452. '
  453. # Fit the model
  454. fit <- cfa(model, data = combined_data_wide, estimator = "MLR", missing = "fiml", std.lv=TRUE)
  455. # Summarize the results
  456. summary(fit, fit.measures = TRUE, standardized = TRUE)
  457. ```
  458. ```{r}
  459. library(lavaanPlot)
  460. l = lavaanPlot(model = fit, coefs = TRUE)
  461. print(l)
  462. library(semPlot)
  463. # Updated plotting code for top-to-bottom, black-and-white SEM diagram
  464. custom_labels <- c("Navon V Low", "Navon V High", "Stroop V Low",
  465. "Stroop V High", "N-Back V Low", "N-Back V High",
  466. "N-Back V Medium", "V General")
  467. # semPlot::semPaths(fit,
  468. # what = "est", # Plot estimated coefficients
  469. # edge.label.cex = 1.5, # Adjust size of edge labels
  470. # node.label.cex = 1.5, # Adjust size of node labels
  471. # sizeMan = 10,
  472. # sizeLat = 10,
  473. # sizeInt = 5,
  474. # layout = "tree", # Layout style: tree for top-to-bottom
  475. # rotation = 1, # Rotate diagram for top-to-bottom layout
  476. # color = list(lat = "white", man = "white", int = "black"), # Black for edges, white for nodes
  477. # edge.color = "black", # Black arrows
  478. # label.color = "black",
  479. # style = "lisrel", # Style for a clean, structured plot
  480. # exoCov = FALSE, # Remove covariances for exogenous variables
  481. # title = FALSE) # Apply custom variable labels) # Hide residuals
  482. ```
  483. ### Run Measurement Invariance Tests
  484. Test configural invariance (model structure equivalence)
  485. This step checks whether the basic factor structure (number of factors and factor loadings) is the same across groups.
  486. The output seems to verify configural variance.
  487. ```{r}
  488. fit_configural <- cfa(model, data = combined_data_wide, group = "Timepoint")
  489. summary(fit_configural, fit.measures = TRUE)
  490. ```
  491. Test metric invariance (equivalence of factor loadings)
  492. Next, impose constraints on the factor loadings to be the same across groups.
  493. The model does not pass the metric invariance test as clearly as it did the configural invariance test. The CFI and TLI are below ideal thresholds, and the SRMR is above the acceptable cutoff. The RMSEA is within the acceptable range, but the confidence interval suggests some variability in fit quality.
  494. However, the chi-square test indicates that the fit of the model is not significantly worse, so you may choose to move forward with partial metric invariance testing by freeing some factor loadings if necessary or investigating specific sources of misfit.
  495. By removing the nback_medium term, I get 'reasonable metric invariance' with slightly better fit terms. But for now, I will use the full model for completeness.
  496. ```{r}
  497. fit_metric <- cfa(model, data = combined_data_wide, group = "Timepoint", group.equal = "loadings")
  498. summary(fit_metric, fit.measures = TRUE)
  499. ```
  500. ### Compute Fit Metrics
  501. We want to compute the same fit metrics as mentioned in Weigard.
  502. We see that Omega Hierarchical for rt_general: 0.6309885, indicating that the task-general factor explains 63.1% of the data, which isn't perfect but moderate.
  503. The Mean Lambda (λ) for rt_general: 0.456063, which is also a moderate loading, indicating that each model moderately loads onto the factor.
  504. ```{r}
  505. library(psych)
  506. library(lavaan)
  507. # 1. Extract Fit Statistics
  508. # The `summary` function with `fit.measures = TRUE` will provide key fit indices
  509. summary(fit, fit.measures = TRUE, standardized = TRUE)
  510. # 2. Extract standardized loadings to calculate mean λ
  511. # Get the standardized solution
  512. standardized_solution <- standardizedSolution(fit)
  513. # Filter to only loadings for `rt_general` factor
  514. rt_general_loadings <- standardized_solution[standardized_solution$lhs == "rt_general" & standardized_solution$op == "=~", "est.std"]
  515. # Calculate mean λ for the `rt_general` factor
  516. mean_lambda <- mean(rt_general_loadings)
  517. cat("Mean Lambda (λ) for rt_general:", mean_lambda, "\n")
  518. # 3. Omega Calculation for General Factor
  519. # Use the `omega` function from the `psych` package
  520. # First, extract the relevant columns from combined_data_wide for the general factor
  521. rt_general_data <- combined_data_wide[, c("navon_rt_low", "navon_rt_high", "stroop_rt_low", "stroop_rt_high", "nback_rt_low", "nback_rt_high", "nback_rt_medium")]
  522. # Run omega analysis
  523. omega_result <- omega(rt_general_data, nfactors = 1) # Set `nfactors = 1` for one general factor
  524. # Display omega hierarchical for rt_general
  525. cat("Omega Hierarchical for rt_general:", omega_result$omega_h, "\n")
  526. ```
  527. ### Extract Subject Level Factors
  528. ```{r}
  529. # Compute the factor scores for the general factor 'rt_general'
  530. general_rt_rate <- lavPredict(fit, type = "lv") # 'lv' means latent variable
  531. # Extract the 'rt_general' column
  532. general_rt_rate <- as.data.frame(general_rt_rate)$rt_general
  533. combined_data_wide$rt_general <- general_rt_rate
  534. ```
  535. # Run Regressions
  536. ## Attach Network Metrics
  537. ```{r}
  538. library(stringr)
  539. library(dplyr)
  540. # Read the connectivity file
  541. connectivity_data <- read.csv("FinalData/bk_allnets_avg_output_final.csv")
  542. # Extract subject ID
  543. connectivity_data$Subj <- str_extract(connectivity_data$Row, "(?<=sub-)[A-Za-z]+(?=_big_corr)")
  544. connectivity_data$Subj <- str_replace(connectivity_data$Subj, "^([A-Za-z])", "\\1_")
  545. # Rename column to be "FileName"
  546. names(connectivity_data)[names(connectivity_data) == "Row"] <- "FileName"
  547. # Merge data
  548. raw_metrics_full_data <- left_join(combined_data_wide, connectivity_data, by = "Subj")
  549. ```
  550. ## Attach Days Covariate
  551. ```{r}
  552. # Load the necessary library
  553. library(dplyr)
  554. # Read the file
  555. behavioral_data <- read.csv("FinalData/n-back_exp_results.csv")
  556. # Extract unique combinations of Subj, Stimulation_Site, and Days
  557. unique_combinations <- behavioral_data %>%
  558. distinct(Subj, Stimulation_Site, Days, .keep_all = FALSE)
  559. # View the results
  560. print(unique_combinations)
  561. library(data.table)
  562. # Assuming unique_combinations is already a data.table. If not, convert it:
  563. setDT(unique_combinations)
  564. # Update Stimulation_Site values
  565. unique_combinations[Stimulation_Site == "FPCNB", Stimulation_Site := "FPCN-B"]
  566. unique_combinations[Stimulation_Site == "vertex", Stimulation_Site := "Vertex"]
  567. # Merge data
  568. raw_metrics_full_data_days <- left_join(raw_metrics_full_data, unique_combinations, by = c("Subj", "Stimulation_Site"))
  569. ```
  570. ## Look at baseline measures
  571. ```{r}
  572. # Scale Days as well (MAKE SURE THESE METRICS WEREN'T SCALED BEFORE)
  573. raw_metrics_full_data_days$Days = scale(raw_metrics_full_data_days$Days)
  574. raw_metrics_full_data_days$FPCN_B.FPCN_B = scale(raw_metrics_full_data_days$FPCN_B.FPCN_B)
  575. raw_metrics_full_data_days$DMN_Canonical.DMN_Canonical = scale(raw_metrics_full_data_days$DMN_Canonical.DMN_Canonical)
  576. raw_metrics_full_data_days$DMN_Canonical.FPCN_B = scale(raw_metrics_full_data_days$DMN_Canonical.FPCN_B)
  577. ```
  578. ```{r}
  579. library(lmerTest)
  580. ```
  581. ```{r}
  582. # pre_data = filter(raw_metrics_full_data_days, Timepoint == "pre")
  583. # model_0 <- lmer(rt_general ~ (DMN_Canonical.FPCN_B + DMN_Canonical.DMN_Canonical + FPCN_B.FPCN_B) + Days + (1 | Subj), data = pre_data, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  584. # summary(model_0)
  585. pre_data = filter(raw_metrics_full_data_days, Timepoint == "pre")
  586. model_0 <- lmer(rt_general ~ (DMN_Canonical.FPCN_B) + Days + (1 | Subj), data = pre_data, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  587. summary(model_0)
  588. ```
  589. ## Look at Raw Stimulation Effects without Network Moderators
  590. ```{r}
  591. raw_metrics_full_data_days$Stimulation_Site <- factor(raw_metrics_full_data_days$Stimulation_Site)
  592. # Set "Vertex" as the baseline (reference level)
  593. raw_metrics_full_data_days$Stimulation_Site <- relevel(raw_metrics_full_data_days$Stimulation_Site, ref = "Vertex")
  594. model_1a <- lmer(rt_general ~ Stimulation_Site * Timepoint + Days + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  595. summary(model_1a)
  596. ```
  597. ## Look at TMS Network Stimulation Effects
  598. ```{r}
  599. # model_1 <- lmer(rt_general ~ (DMN_Canonical.FPCN_B + DMN_Canonical.DMN_Canonical + FPCN_B.FPCN_B) * Stimulation_Site * Timepoint + Days + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  600. # Ensure Stimulation_Site is a factor
  601. raw_metrics_full_data_days$Stimulation_Site <- factor(raw_metrics_full_data_days$Stimulation_Site)
  602. # Set "Vertex" as the baseline (reference level)
  603. raw_metrics_full_data_days$Stimulation_Site <- relevel(raw_metrics_full_data_days$Stimulation_Site, ref = "Vertex")
  604. model_1 <- lmer(rt_general ~ (DMN_Canonical.FPCN_B) * Stimulation_Site * Timepoint + Days + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  605. summary(model_1)
  606. ```
  607. ### Contrasts
  608. ```{r}
  609. library(emmeans)
  610. ```
  611. ## Look at network moderation of Stimulation Effects
  612. I'm commenting out the code to test out all three networks and only leaving in those for the FPCN-DMN anti-correlation
  613. ```{r}
  614. # # Linear mixed-effects model to regress out 'Days'
  615. # days_effect_rt_rate_model <- lm(rt_general ~ Days, data = raw_metrics_full_data_days)
  616. # # Extract residuals which represent the part of 'v' not explained by 'Days'
  617. # raw_metrics_full_data_days$rt_resid = residuals(days_effect_rt_rate_model)
  618. #
  619. # rt_rate_model_days_resid <- lmer(rt_resid ~ (DMN_Canonical.FPCN_B + DMN_Canonical.DMN_Canonical + FPCN_B.FPCN_B)*Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  620. # Linear mixed-effects model to regress out 'Days'
  621. days_effect_rt_rate_model <- lm(rt_general ~ Days, data = raw_metrics_full_data_days)
  622. # Extract residuals which represent the part of 'v' not explained by 'Days'
  623. raw_metrics_full_data_days$rt_resid = residuals(days_effect_rt_rate_model)
  624. rt_rate_model_days_resid <- lmer(rt_resid ~ (DMN_Canonical.FPCN_B)*Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  625. ```
  626. This is just the stimulation effects without any network moderators
  627. ```{r}
  628. just_stim_model_resid <- lmer(rt_resid ~ Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  629. model_1_emmeans = emmeans(just_stim_model_resid, ~ Stimulation_Site * Timepoint, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  630. # Look at the pairwise comparisons for the interaction
  631. pairs(model_1_emmeans)
  632. # Contrast the change from pre to post for FPCN-B vs Vertex
  633. mdl_1_emmeans = contrast(model_1_emmeans, interaction = c("revpairwise"), adjust = "none")
  634. mdl_1_emmeans
  635. ```
  636. Same analysis but using the full model
  637. ```{r}
  638. just_stim_model_resid <- lmer(rt_resid ~ Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  639. model_1_just_stim_emmeans = emmeans(just_stim_model_resid, ~ Stimulation_Site, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  640. model_1_complete_emmeans = emmeans(rt_rate_model_days_resid, ~ Stimulation_Site, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  641. model_1_complete_emmeans_timepoint = emmeans(rt_rate_model_days_resid, ~ Timepoint, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  642. model_1_complete_emmeans_network = emtrends(rt_rate_model_days_resid, ~ 1, var = "DMN_Canonical.FPCN_B", pbkrtest.limit = 30000, lmerTest.limit = 30000)
  643. # Look at the pairwise comparisons for the interaction
  644. pairs(model_1_complete_emmeans)
  645. # Contrast the change from pre to post for FPCN-B vs Vertex
  646. model_1_complete_emmeans_int = emmeans(rt_rate_model_days_resid, ~ Stimulation_Site * Timepoint, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  647. mdl_1_emmeans = contrast(model_1_complete_emmeans, interaction = c("revpairwise"), adjust = "none")
  648. mdl_1_emmeans
  649. ```
  650. This is our main result below
  651. ```{r}
  652. mdl_4_small = emtrends(rt_rate_model_days_resid, pairwise ~ Timepoint * Stimulation_Site, var="DMN_Canonical.FPCN_B", pbkrtest.limit = 30000, lmerTest.limit = 30000)
  653. mdl_4_small_contrast = contrast(mdl_4_small[[1]], interaction = c("revpairwise"), adjust = "none")
  654. mdl_4_small_contrast
  655. ```
  656. # Draw Plots
  657. ```{r}
  658. library(ggeffects)
  659. library(dplyr)
  660. library(ggplot2)
  661. library(data.table)
  662. # loadfonts(device = "win")
  663. plot_metrics_stim_site = function(model, metric, metric_name, model_name){
  664. # Remove hardcoded overrides to allow function to be generic
  665. # metric_name = "FPCN-B and DMN\nConnectivity"
  666. # model_name = "Response Time"
  667. terms_vec = c(metric, "Stimulation_Site", "Timepoint")
  668. preds <- predict_response(model, terms = terms_vec, interval="confidence", margin="mean_reference", back.transform = FALSE)
  669. preds = as.data.table(preds)
  670. contrast_preds = preds
  671. preds <- preds %>%
  672. mutate(
  673. sem = (conf.high - conf.low) / (2 * 1.96) # approximate SEM from 95% CI
  674. )
  675. preds <- preds %>%
  676. mutate(
  677. ci.low_sem = predicted - sem,
  678. ci.high_sem = predicted + sem
  679. )
  680. filtered_preds_df = preds
  681. # --- Data Preparation for Individual Points (Added) ---
  682. # Extract Random Intercepts
  683. re_intercepts <- as.data.frame(ranef(model)$Subj)
  684. re_intercepts$Subj <- rownames(re_intercepts)
  685. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  686. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  687. names(re_intercepts) <- c("Subj", "RandomIntercept")
  688. # Prepare raw data points with random intercept adjustment
  689. # Note: model should be the residualized model 'rt_rate_model_days_resid'
  690. # ensuring we use 'rt_resid' for consistency with the model response.
  691. raw_data_plot <- raw_metrics_full_data_days %>%
  692. mutate(across(all_of(metric), as.numeric)) %>%
  693. left_join(re_intercepts, by = "Subj") %>%
  694. mutate(
  695. v_adjusted = rt_resid - RandomIntercept,
  696. # Map Timepoint to match prediction labels if necessary
  697. Timepoint = recode(Timepoint, "pre" = "pre", "post" = "post"),
  698. facet = Timepoint # Use 'facet' column to match ggplot logical mapping
  699. )
  700. # Prepare Individual CONTRAST Data (Difference Scores)
  701. subj_contrasts <- raw_data_plot %>%
  702. dplyr::select(Subj, all_of(metric), Stimulation_Site, Timepoint, v_adjusted) %>%
  703. pivot_wider(
  704. id_cols = c(Subj, all_of(metric)),
  705. names_from = c(Stimulation_Site, Timepoint),
  706. values_from = v_adjusted,
  707. names_sep = "_"
  708. ) %>%
  709. mutate(
  710. fpcnb_contrast = `FPCN-B_post` - `FPCN-B_pre`,
  711. vertex_contrast = `Vertex_post` - `Vertex_pre`,
  712. dan_contrast = `DAN_post` - `DAN_pre`,
  713. fpcnb_vertex_contrast = fpcnb_contrast - vertex_contrast,
  714. fpcnb_dan_contrast = fpcnb_contrast - dan_contrast,
  715. dan_vertex_contrast = dan_contrast - vertex_contrast
  716. )
  717. contrast_df = dplyr::select(contrast_preds, -c(conf.low, conf.high)) %>%
  718. pivot_wider(
  719. names_from = c(group, facet),
  720. values_from = c(predicted, std.error),
  721. names_sep = "_"
  722. )
  723. final_contrasts <- contrast_df %>%
  724. mutate(
  725. fpcnb_contrast = `predicted_FPCN-B_post` - `predicted_FPCN-B_pre`,
  726. vertex_contrast = predicted_Vertex_post - predicted_Vertex_pre,
  727. dan_contrast = predicted_DAN_post - predicted_DAN_pre,
  728. fpcnb_contrast.error = sqrt(`std.error_FPCN-B_post`^2 + `std.error_FPCN-B_pre`^2),
  729. vertex_contrast.error = sqrt(std.error_Vertex_post^2 + std.error_Vertex_pre^2),
  730. dan_contrast.error = sqrt(std.error_DAN_post^2 + std.error_DAN_pre^2),
  731. fpcnb_vertex_contrast = fpcnb_contrast - vertex_contrast,
  732. fpcnb_dan_contrast = fpcnb_contrast - dan_contrast,
  733. dan_vertex_contrast = dan_contrast - vertex_contrast,
  734. fpcnb_vertex_std.error = sqrt(fpcnb_contrast.error^2 + vertex_contrast.error^2),
  735. fpcnb_dan_std.error = sqrt(fpcnb_contrast.error^2 + dan_contrast.error^2),
  736. dan_vertex_std.error = sqrt(vertex_contrast.error^2 + dan_contrast.error^2)
  737. ) %>%
  738. mutate(
  739. dan_conf.low = dan_contrast - dan_contrast.error,
  740. dan_conf.high = dan_contrast + dan_contrast.error,
  741. fpcnb_conf.low = fpcnb_contrast - fpcnb_contrast.error,
  742. fpcnb_conf.high = fpcnb_contrast + fpcnb_contrast.error,
  743. vertex_conf.low = vertex_contrast - vertex_contrast.error,
  744. vertex_conf.high = vertex_contrast + vertex_contrast.error,
  745. fpcnb_vertex_conf.low = fpcnb_vertex_contrast - fpcnb_vertex_std.error,
  746. fpcnb_vertex_conf.high = fpcnb_vertex_contrast + fpcnb_vertex_std.error,
  747. fpcnb_dan_conf.low = fpcnb_dan_contrast - fpcnb_dan_std.error,
  748. fpcnb_dan_conf.high = fpcnb_dan_contrast + fpcnb_dan_std.error,
  749. dan_vertex_conf.low = dan_vertex_contrast - dan_vertex_std.error,
  750. dan_vertex_conf.high = dan_vertex_contrast + dan_vertex_std.error
  751. )
  752. ### Plotting
  753. custom_theme <- theme_minimal(base_size = 24) +
  754. theme(plot.title = element_text(size = rel(2.2), hjust = 0.5),
  755. plot.background = element_blank(),
  756. plot.margin = margin(t = 2, r = 1, b = 1, l = 30, unit = "pt"),
  757. panel.background = element_rect(fill = "white"),
  758. panel.grid.major = element_blank(),
  759. panel.grid.minor = element_blank(),
  760. axis.line = element_blank(),
  761. axis.ticks = element_line(color = "black"),
  762. axis.title.x = element_text(size = rel(3.0), margin = margin(t = 10), lineheight = 0.7),
  763. axis.title.y = element_text(size = rel(3.0), margin = margin(r = 10), lineheight = 1.2),
  764. axis.text = element_text(size = rel(2.7)),
  765. strip.text = element_text(size = rel(2.2)),
  766. legend.position = "right",
  767. legend.title = element_blank(),
  768. legend.background = element_rect(color = "black", size = .5),
  769. legend.text = element_text(size = rel(2)),
  770. legend.spacing.y = unit(0.5, "cm"),
  771. panel.border = element_blank(),
  772. text = element_text(family = "Arial"))
  773. clean_metric_name <- gsub(" ", "", "FPCN-BandDMNConnectivity") # Kept consistent with old code behavior if needed, or use metric_name
  774. # Ideally, use: clean_metric_name <- gsub(" ", "", metric_name)
  775. # But assuming "FPCN-BandDMNConnectivity" was important for file naming consistency based on user prompt context "clean up",
  776. # I will use the function argument `metric_name` logic.
  777. clean_metric_name <- gsub("[\n ]", "", metric_name)
  778. # --- Main Interaction Plots with Points ---
  779. # Helper to plot site
  780. plot_site <- function(site_name, file_suffix) {
  781. site_preds <- filter(preds, group == site_name)
  782. site_raw <- filter(raw_data_plot, Stimulation_Site == site_name)
  783. p = ggplot(site_preds, aes(x = x, y = predicted, color = facet)) +
  784. # Add Connecting Lines
  785. geom_line(data = site_raw, aes(x = .data[[metric]], y = v_adjusted, group = Subj),
  786. color = "gray80", alpha = 0.5) +
  787. # Add Adjusted Individual Points
  788. geom_point(data = site_raw, aes(x = .data[[metric]], y = v_adjusted, color = facet),
  789. alpha = 1, size = 3) +
  790. geom_ribbon(aes(ymin = ci.low_sem, ymax = ci.high_sem, fill = facet), linewidth = 0, alpha = 0.2) +
  791. geom_line(size = 1.5) +
  792. labs(y = model_name, x = metric_name) +
  793. custom_theme
  794. ggsave(filename = paste0("Figures/", metric_type, "_FilteredPreds_", file_suffix, "_", clean_metric_name, ".png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  795. }
  796. plot_site("FPCN-B", "FPCNB")
  797. plot_site("Vertex", "Vertex")
  798. plot_site("DAN", "DAN")
  799. # --- Contrast Plots ---
  800. # Helper for contrasts
  801. plot_contrast <- function(y_var, y_low, y_high, file_suffix, extra_theme = NULL) {
  802. p = ggplot(final_contrasts, aes(x = x)) +
  803. # Add Adjusted Individual Points (Contrasts)
  804. geom_point(data = subj_contrasts, aes(x = .data[[metric]], y = .data[[y_var]]),
  805. alpha = 0.4, size = 2.5, color = "black") +
  806. geom_ribbon(aes(ymin = .data[[y_low]], ymax = .data[[y_high]]), linetype = "dotted", linewidth = 1, fill = "gray", alpha = 0.2, color = "gray") +
  807. geom_line(size = 1.5, aes(y = .data[[y_var]]), color='black') +
  808. labs(y = model_name, x = metric_name) +
  809. custom_theme
  810. if (!is.null(extra_theme)) {
  811. p <- p + extra_theme
  812. }
  813. ggsave(filename = paste0("Figures/", metric_type, "_GeneralContrastOf_", file_suffix, "_", clean_metric_name, ".png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  814. }
  815. plot_contrast("fpcnb_contrast", "fpcnb_conf.low", "fpcnb_conf.high", "FPCN")
  816. plot_contrast("vertex_contrast", "vertex_conf.low", "vertex_conf.high", "Vertex")
  817. plot_contrast("dan_contrast", "dan_conf.low", "dan_conf.high", "DAN")
  818. plot_contrast("fpcnb_vertex_contrast", "fpcnb_vertex_conf.low", "fpcnb_vertex_conf.high", "FPCNVertex")
  819. # FPCN-DAN contrast has specific margin
  820. plot_contrast("fpcnb_dan_contrast", "fpcnb_dan_conf.low", "fpcnb_dan_conf.high", "FPCNDAN",
  821. extra_theme = theme(plot.margin = margin(t = 20, r = 1, b = 1, l = 1, unit = "pt")))
  822. plot_contrast("dan_vertex_contrast", "dan_vertex_conf.low", "dan_vertex_conf.high", "DANVertex")
  823. }
  824. ```
  825. ```{r}
  826. plot_metrics_stim_site(rt_rate_model_days_resid, "DMN_Canonical.FPCN_B", "FPCN-B and DMN\nConnectivity", "Response Time")
  827. ```
  828. ## Draw Network emmeans
  829. ```{r}
  830. # Load necessary library
  831. library(ggplot2)
  832. preds <- predict_response(
  833. model_1,
  834. terms = c("DMN_Canonical.FPCN_B"),
  835. interval = "confidence",
  836. margin = "mean_reference",
  837. back.transform = FALSE
  838. )
  839. preds = as.data.table(preds)
  840. preds <- preds %>%
  841. mutate(
  842. sem = (conf.high - conf.low) / (2 * 1.96) # approximate SEM from 95% CI
  843. )
  844. preds <- preds %>%
  845. mutate(
  846. ci.low_sem = predicted - sem,
  847. ci.high_sem = predicted + sem
  848. )
  849. preds_df = preds
  850. # Calculate subject means for scatterplot and adjust for Random Intercepts
  851. # This assumes the large spread is due to subject baseline differences (Random Intercepts).
  852. # By subtracting the Random Intercept, we visualize the "Partial Residuals" - showing the
  853. # relationship between Network and Response Time after controlling for individual baselines.
  854. # 1. Extract Random Intercepts
  855. re_intercepts <- as.data.frame(ranef(model_1)$Subj)
  856. re_intercepts$Subj <- rownames(re_intercepts)
  857. # Select only the intercept (in case there are random slopes) and rename
  858. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  859. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  860. names(re_intercepts) <- c("Subj", "RandomIntercept")
  861. # 2. Calculate adjusted means
  862. subj_means <- raw_metrics_full_data_days %>%
  863. mutate(DMN_Canonical.FPCN_B = as.numeric(DMN_Canonical.FPCN_B)) %>%
  864. group_by(Subj) %>%
  865. summarise(
  866. mean_v = mean(rt_general, na.rm = TRUE),
  867. network_val = mean(DMN_Canonical.FPCN_B, na.rm = TRUE)
  868. ) %>%
  869. left_join(re_intercepts, by = "Subj") %>%
  870. mutate(adjusted_mean_v = mean_v - RandomIntercept)
  871. custom_theme <- theme_minimal(base_size = 20) +
  872. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  873. plot.background = element_blank(), # White background for the plot
  874. panel.background = element_rect(fill = "white"), # White background for the panels
  875. panel.grid.major = element_blank(), # Remove major grid lines
  876. panel.grid.minor = element_blank(), # Remove minor grid lines
  877. axis.line = element_blank(), # Define the axis lines without enclosing
  878. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  879. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  880. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  881. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  882. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  883. legend.position = "right", # Position the legend on the right
  884. legend.title = element_blank(), # Customize the legend title size
  885. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  886. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  887. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  888. panel.border = element_blank(), # Ensure no border around the panels
  889. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  890. # Create the plot
  891. p = ggplot(preds_df, aes(x = x, y = predicted)) +
  892. geom_point(data = subj_means, aes(x = network_val, y = adjusted_mean_v),
  893. color = "black", alpha = 0.6, size = 3) +
  894. geom_line(size = 2, position = position_dodge(width = 0.5)) + # Add points
  895. geom_ribbon(aes(ymin = ci.low_sem, ymax = ci.high_sem), linetype = "dotted", linewidth = 1, fill = "gray", alpha = 0.2) +
  896. # scale_x_continuous(limits = c(-2, 2)) +
  897. labs(title = "Network Connectivity",
  898. y = "", x = "FPCN-B and DMN Connectivity") +
  899. theme_minimal(base_size = 14) +
  900. custom_theme +
  901. theme(legend.position = "none")
  902. # Use ggsave() to save the plot
  903. ggsave(filename = paste0("Figures/RT_EMMeanNetworkDriftRate.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  904. ```
  905. ## Draw Stimulation emmeans (No Timepoint)
  906. ```{r}
  907. # Load necessary library
  908. library(ggplot2)
  909. library(emmeans)
  910. # Create the data frame directly from the emmeans object
  911. model_1_complete_emmeans_df <- as.data.frame(model_1_complete_emmeans)
  912. custom_theme <- theme_minimal(base_size = 20) +
  913. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  914. plot.background = element_blank(), # White background for the plot
  915. panel.background = element_rect(fill = "white"), # White background for the panels
  916. panel.grid.major = element_blank(), # Remove major grid lines
  917. panel.grid.minor = element_blank(), # Remove minor grid lines
  918. axis.line = element_blank(), # Define the axis lines without enclosing
  919. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  920. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  921. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  922. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  923. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  924. legend.position = "right", # Position the legend on the right
  925. legend.title = element_blank(), # Customize the legend title size
  926. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  927. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  928. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  929. panel.border = element_blank(), # Ensure no border around the panels
  930. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  931. # Convert Stimulation_Site to a factor and set the order
  932. model_1_complete_emmeans_df$Stimulation_Site <- factor(model_1_complete_emmeans_df$Stimulation_Site, levels = c("FPCN-B", "Vertex", "DAN"))
  933. # Create the plot
  934. p = ggplot(model_1_complete_emmeans_df, aes(x = Stimulation_Site, y = emmean, color = Stimulation_Site)) +
  935. geom_point(size = 6, position = position_dodge(width = 0.5)) + # Add points
  936. geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = 0.2, size = 2, position = position_dodge(width = 0.5)) + # Error bars
  937. geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # Reference line
  938. scale_color_manual(values = c("DAN" = "blue", "Vertex" = "gray", "FPCN-B" = "red")) + # Set colors for points
  939. labs(title = "Stimulation Site",
  940. y = "Estimated Marginal Mean of \nGeneral Response Time", x = "Stimulation Site") +
  941. theme_minimal(base_size = 14) +
  942. custom_theme +
  943. theme(legend.position = "none")
  944. # Use ggsave() to save the plot
  945. ggsave(filename = paste0("Figures/RT_EMMeanStimSiteDriftRate_Automated.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  946. # Print the plot
  947. print(p)
  948. ```
  949. ### Draw Stimulation Site separated by Timepoint
  950. ```{r}
  951. # Load necessary libraries
  952. library(ggplot2)
  953. library(dplyr)
  954. library(emmeans)
  955. # Convert emmeans object to dataframe
  956. # We use a new variable name to avoid overwriting the original object or the manual dataframe below
  957. model_1_complete_emmeans_int_df <- as.data.frame(model_1_complete_emmeans_int)
  958. # Use SE for error bars to match manual plot (Mean +/- SE)
  959. model_1_complete_emmeans_int_df <- model_1_complete_emmeans_int_df %>%
  960. mutate(
  961. ci.low_sem = emmean - SE,
  962. ci.high_sem = emmean + SE
  963. )
  964. # Capitalize Timepoint labels
  965. model_1_complete_emmeans_int_df <- model_1_complete_emmeans_int_df %>%
  966. mutate(Timepoint = recode(Timepoint, "pre" = "Pre", "post" = "Post"))
  967. # Convert factors and set order for proper plotting
  968. model_1_complete_emmeans_int_df$Stimulation_Site <- factor(
  969. model_1_complete_emmeans_int_df$Stimulation_Site,
  970. levels = c("FPCN-B", "Vertex", "DAN")
  971. )
  972. model_1_complete_emmeans_int_df$Timepoint <- factor(
  973. model_1_complete_emmeans_int_df$Timepoint,
  974. levels = c("Pre", "Post")
  975. )
  976. # Define custom theme
  977. custom_theme <- theme_minimal(base_size = 20) +
  978. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"),
  979. plot.background = element_blank(),
  980. panel.background = element_rect(fill = "white"),
  981. panel.grid.major = element_blank(),
  982. panel.grid.minor = element_blank(),
  983. axis.ticks = element_line(color = "black"),
  984. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2),
  985. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2),
  986. axis.text = element_text(size = rel(1.8)),
  987. strip.text = element_text(size = rel(2.2)),
  988. legend.title = element_blank(),
  989. legend.text = element_text(size = rel(1.5)),
  990. legend.spacing.y = unit(0.5, "cm"),
  991. panel.border = element_blank(),
  992. text = element_text(family = "Arial"))
  993. # Prepare raw data for plotting individual points with Random Intercept subtraction
  994. # Extract Random Intercepts
  995. re_intercepts <- as.data.frame(ranef(model_1)$Subj)
  996. re_intercepts$Subj <- rownames(re_intercepts)
  997. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  998. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  999. names(re_intercepts) <- c("Subj", "RandomIntercept")
  1000. raw_data_plot <- raw_metrics_full_data_days %>%
  1001. left_join(re_intercepts, by = "Subj") %>%
  1002. mutate(
  1003. rt_resid_centered = rt_resid - RandomIntercept,
  1004. Timepoint = recode(Timepoint, "pre" = "Pre", "post" = "Post"),
  1005. Stimulation_Site = factor(Stimulation_Site, levels = c("FPCN-B", "Vertex", "DAN")),
  1006. Timepoint = factor(Timepoint, levels = c("Pre", "Post"))
  1007. )
  1008. # Create the plot
  1009. p <- ggplot(model_1_complete_emmeans_int_df,
  1010. aes(x = Stimulation_Site, y = emmean, group = Timepoint)) +
  1011. # 1. Error bars (Bottom layer)
  1012. geom_errorbar(aes(ymin = ci.low_sem, ymax = ci.high_sem, color = Timepoint),
  1013. width = 0.2,
  1014. position = position_dodge(width = 0.5),
  1015. size = 1.2) +
  1016. # 2. Individual points (Middle layer, alpha increased for visibility)
  1017. geom_point(data = raw_data_plot, aes(y = rt_resid_centered, color = Timepoint),
  1018. position = position_jitterdodge(jitter.width = 0.25, dodge.width = 0.5),
  1019. alpha = 0.4, size = 2.5) +
  1020. # 3. Mean points (Top layer)
  1021. geom_point(size = 6, shape = 16, position = position_dodge(width = 0.5), aes(color = Timepoint)) +
  1022. geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # Reference line
  1023. scale_color_manual(values = c("Pre" = "blue", "Post" = "red")) + # Different colors for error bars
  1024. labs(title = "Stimulation Site",
  1025. y = "General Response Time",
  1026. x = "Stimulation Site") +
  1027. theme_minimal(base_size = 14) +
  1028. custom_theme +
  1029. theme(legend.position = "right") # Keep the legend
  1030. # Save the plot
  1031. ggsave(filename = paste0("Figures/RT_EMMeanStimSiteAndTimepoint_Automated.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1032. print(p)
  1033. ```
  1034. ## Draw Timepoint EMMeans (automated)
  1035. ```{r}
  1036. # Load necessary libraries
  1037. library(ggplot2)
  1038. library(dplyr)
  1039. library(emmeans)
  1040. # Create the data frame directly from the emmeans object
  1041. # Using a new variable name to avoid conflicts
  1042. model_1_complete_emmeans_timepoint_df <- as.data.frame(model_1_complete_emmeans_timepoint)
  1043. # Capitalize Timepoint labels to match manual plot
  1044. model_1_complete_emmeans_timepoint_df <- model_1_complete_emmeans_timepoint_df %>%
  1045. mutate(Timepoint = recode(Timepoint, "pre" = "Pre", "post" = "Post"))
  1046. # Convert Timepoint to a factor and set the order
  1047. model_1_complete_emmeans_timepoint_df$Timepoint <- factor(model_1_complete_emmeans_timepoint_df$Timepoint, levels = c("Pre", "Post"))
  1048. custom_theme <- theme_minimal(base_size = 20) +
  1049. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  1050. plot.background = element_blank(), # White background for the plot
  1051. panel.background = element_rect(fill = "white"), # White background for the panels
  1052. panel.grid.major = element_blank(), # Remove major grid lines
  1053. panel.grid.minor = element_blank(), # Remove minor grid lines
  1054. axis.line = element_blank(), # Define the axis lines without enclosing
  1055. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  1056. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  1057. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  1058. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  1059. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  1060. legend.position = "right", # Position the legend on the right
  1061. legend.title = element_blank(), # Customize the legend title size
  1062. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  1063. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  1064. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  1065. panel.border = element_blank(), # Ensure no border around the panels
  1066. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  1067. # Create the plot
  1068. p = ggplot(model_1_complete_emmeans_timepoint_df, aes(x = Timepoint, y = emmean, color = Timepoint)) +
  1069. geom_point(size = 6, position = position_dodge(width = 0.5)) + # Add points
  1070. geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = 0.2, size = 2, position = position_dodge(width = 0.5)) + # Error bars
  1071. geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # Reference line
  1072. scale_color_manual(values = c("Pre" = "blue", "Post" = "red")) + # Set colors for points
  1073. labs(title = "Timepoint",
  1074. y = "Estimated Marginal Mean of \nGeneral Response Time", x = "Timepoint") +
  1075. theme_minimal(base_size = 14) +
  1076. custom_theme +
  1077. theme(legend.position = "none")
  1078. # Use ggsave() to save the plot
  1079. ggsave(filename = paste0("Figures/RT_EMMeanTimepointDriftRate_Automated.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1080. print(p)
  1081. ```
  1082. ## Draw Days Effects
  1083. ```{r}
  1084. model = model_1
  1085. preds <- predict_response(model,
  1086. terms = c("Days"),
  1087. interval = "confidence",
  1088. margin = "mean_reference",
  1089. back.transform = FALSE)
  1090. preds = as.data.table(preds)
  1091. preds <- preds %>%
  1092. mutate(
  1093. sem = (conf.high - conf.low) / (2 * 1.96) # approximate SEM from 95% CI
  1094. )
  1095. preds <- preds %>%
  1096. mutate(
  1097. ci.low_sem = predicted - sem,
  1098. ci.high_sem = predicted + sem
  1099. )
  1100. filtered_preds_df = preds
  1101. # Create subject-centered data using Random Intercept subtraction
  1102. # Extract Random Intercepts
  1103. re_intercepts <- as.data.frame(ranef(model_1)$Subj)
  1104. re_intercepts$Subj <- rownames(re_intercepts)
  1105. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  1106. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  1107. names(re_intercepts) <- c("Subj", "RandomIntercept")
  1108. metrics_days_centered <- raw_metrics_full_data_days %>%
  1109. left_join(re_intercepts, by = "Subj") %>%
  1110. mutate(rt_general_centered = rt_general - RandomIntercept)
  1111. # Calculate intercepts and slopes for each subject based on raw data
  1112. # (Since the model only has random intercepts, we use individual OLS regressions
  1113. # to visualize the heterogeneity in slopes that exists in the raw data)
  1114. subj_trends <- metrics_days_centered %>%
  1115. group_by(Subj) %>%
  1116. summarise(
  1117. intercept = coef(lm(rt_general_centered ~ Days))[1],
  1118. slope = coef(lm(rt_general_centered ~ Days))[2]
  1119. ) %>%
  1120. mutate(trend_color = ifelse(slope > 0, "green4", "red3"))
  1121. custom_theme <- theme_minimal(base_size = 20) +
  1122. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  1123. plot.background = element_blank(), # White background for the plot
  1124. panel.background = element_rect(fill = "white"), # White background for the panels
  1125. panel.grid.major = element_blank(), # Remove major grid lines
  1126. panel.grid.minor = element_blank(), # Remove minor grid lines
  1127. axis.line = element_blank(), # Define the axis lines without enclosing
  1128. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  1129. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  1130. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  1131. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  1132. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  1133. legend.position = "right", # Position the legend on the right
  1134. legend.title = element_blank(), # Customize the legend title size
  1135. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  1136. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  1137. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  1138. panel.border = element_blank(), # Ensure no border around the panels
  1139. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  1140. p = ggplot(filtered_preds_df, aes(x = x, y = predicted)) +
  1141. # Add individual participant trend lines (subject-centered)
  1142. geom_abline(data = subj_trends, aes(intercept = intercept, slope = slope, group = Subj, color = trend_color),
  1143. alpha = 0.2, size = 0.5) +
  1144. scale_color_identity() +
  1145. geom_ribbon(aes(ymin = ci.low_sem, ymax = ci.high_sem), linetype = "dotted", linewidth = 1, fill = "gray", alpha = 0.2) +
  1146. geom_line(size = 1.5) +
  1147. #Add confidence interval
  1148. labs(title = "Days",y = "", x = "Days") +
  1149. custom_theme
  1150. # Use ggsave() to save the plot
  1151. ggsave(filename = paste0("Figures/RT_DaysLine.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1152. print(p)
  1153. ```
  1154. # Factor Analysis for Accuracy
  1155. ```{r}
  1156. metric_type <- 'ACC'
  1157. ```
  1158. ## Summary Statistics
  1159. ```{r}
  1160. # Calculate Statistics for Accuracys (Mean, Variance, Differences)
  1161. library(dplyr)
  1162. library(tidyr)
  1163. library(stringr)
  1164. # Filter for drift rate (v) columns
  1165. acc_cols_stats <- grep("_acc_", colnames(combined_data_all), value = TRUE)
  1166. # Create long format for statistics
  1167. stats_data <- combined_data_all %>%
  1168. dplyr::select(Subj, Stimulation_Site, all_of(acc_cols_stats)) %>%
  1169. pivot_longer(
  1170. cols = all_of(acc_cols_stats),
  1171. names_to = "full_name",
  1172. values_to = "Value"
  1173. ) %>%
  1174. mutate(
  1175. Task = str_extract(full_name, "^[a-z]+"),
  1176. Timepoint = ifelse(grepl("_pre_", full_name), "Pre", "Post"),
  1177. Difficulty = str_extract(full_name, "(high|medium|low)$")
  1178. ) %>%
  1179. filter(!is.na(Value))
  1180. # Calculate Summary Stats per Task
  1181. task_stats <- stats_data %>%
  1182. dplyr::select(Subj, Stimulation_Site, Task, Timepoint, Difficulty, Value) %>%
  1183. pivot_wider(names_from = Difficulty, values_from = Value) %>%
  1184. group_by(Task) %>%
  1185. summarise(
  1186. # Low Condition (Across all sites/timepoints)
  1187. Mean_Low = mean(low, na.rm = TRUE),
  1188. Var_Low = var(low, na.rm = TRUE),
  1189. # Medium Condition (Across all sites/timepoints)
  1190. Mean_Medium = mean(medium, na.rm = TRUE),
  1191. Var_Medium = var(medium, na.rm = TRUE),
  1192. # High Condition (Across all sites/timepoints)
  1193. Mean_High = mean(high, na.rm = TRUE),
  1194. Var_High = var(high, na.rm = TRUE),
  1195. # Individual Difference (High - Low)
  1196. Mean_Diff = mean(high - low, na.rm = TRUE),
  1197. Var_Diff = var(high - low, na.rm = TRUE)
  1198. )
  1199. print(task_stats)
  1200. ```
  1201. ```{r}
  1202. # 1. Reshape Data for Plotting
  1203. library(tidyr)
  1204. library(dplyr)
  1205. library(ggplot2)
  1206. library(stringr)
  1207. library(RColorBrewer)
  1208. # Filter for drift rate (v) columns
  1209. v_cols <- grep("_acc_", colnames(combined_data_all), value = TRUE)
  1210. # Pivot generic long format
  1211. plot_data_long <- combined_data_all %>%
  1212. dplyr::select(Subj, Stimulation_Site, all_of(v_cols)) %>%
  1213. pivot_longer(
  1214. cols = all_of(v_cols),
  1215. names_to = "full_name",
  1216. values_to = "Value"
  1217. ) %>%
  1218. mutate(
  1219. # Extract Task: First word before underscore
  1220. Task = str_extract(full_name, "^[a-z]+"),
  1221. # Extract Timepoint: 'pre' or 'post'
  1222. Timepoint = ifelse(grepl("_pre_", full_name), "Pre", "Post"),
  1223. # Extract Difficulty: 'low', 'medium', 'high' at the end
  1224. Difficulty = str_extract(full_name, "(high|medium|low)$")
  1225. ) %>%
  1226. mutate(
  1227. # Set factor levels for correct ordering
  1228. Difficulty = factor(str_to_title(Difficulty), levels = c("Low", "Medium", "High")),
  1229. Timepoint = factor(Timepoint, levels = c("Pre", "Post")),
  1230. Stimulation_Site = factor(Stimulation_Site, levels = c("Vertex", "FPCN-B", "DAN"), labels = c("Vertex", "L-FPN-B", "D-FPN"))
  1231. ) %>%
  1232. filter(!is.na(Value))
  1233. # 2. Define Plotting Helper Function
  1234. draw_task_boxplot <- function(data, task_name) {
  1235. # Filter data for specific task
  1236. task_df <- data %>% filter(Task == task_name)
  1237. p <- ggplot(task_df, aes(x = Difficulty, y = Value)) +
  1238. # Facet by Stimulation Site to separate the groups clearly
  1239. facet_wrap(~ Stimulation_Site) +
  1240. # Boxplots:
  1241. # Fill by Stimulation Site (Consistent color within facet)
  1242. # Alpha by Timepoint (Distinguish Pre vs Post as 2 bars)
  1243. geom_boxplot(
  1244. aes(fill = Stimulation_Site, alpha = Timepoint),
  1245. position = position_dodge(width = 0.8),
  1246. width = 0.6,
  1247. outlier.shape = NA
  1248. ) +
  1249. # Individual Points
  1250. geom_point(
  1251. aes(shape = Timepoint), # Shape matches Pre/Post
  1252. color = "gray25",
  1253. position = position_jitterdodge(jitter.width = 0.1, dodge.width = 0.8),
  1254. size = 1.5,
  1255. alpha = 0.7
  1256. ) +
  1257. # Aesthetics
  1258. scale_fill_brewer(palette = "Set1") +
  1259. scale_alpha_manual(values = c("Pre" = 0.4, "Post" = 0.9)) + # Light=Pre, Dark=Post
  1260. scale_shape_manual(values = c("Pre" = 16, "Post" = 17)) +
  1261. # Override only the alpha legend to show gray fills, keeping plot colors intact
  1262. guides(alpha = guide_legend(override.aes = list(fill = "black"))) +
  1263. theme_classic(base_size = 18) +
  1264. theme(
  1265. strip.background = element_rect(fill = "gray95", color = NA),
  1266. legend.position = "right",
  1267. plot.margin = margin(5.5, 5.5, 5.5, 15, "pt") # Increase left margin to prevent 'Accuracy' cutoff
  1268. ) +
  1269. labs(
  1270. title = NULL,
  1271. y = "Accuracy (0-1)",
  1272. x = "Difficulty Condition",
  1273. fill = "Stimulation Site",
  1274. alpha = "Timepoint",
  1275. shape = "Timepoint"
  1276. )
  1277. # For N-Back, rotate labels to prevent overlap
  1278. if (task_name == "nback") {
  1279. p <- p + theme(axis.text.x = element_text(angle = 45, hjust = 1))
  1280. }
  1281. return(p)
  1282. }
  1283. # 3. Generate and Display Plots
  1284. p_navon <- draw_task_boxplot(plot_data_long, "navon")
  1285. print(p_navon)
  1286. ggsave("Figures/ACC_Navon_v.png", plot = p_navon, width = 8, height = 5, dpi = 300)
  1287. p_stroop <- draw_task_boxplot(plot_data_long, "stroop")
  1288. print(p_stroop)
  1289. ggsave("Figures/ACC_Stroop_v.png", plot = p_stroop, width = 8, height = 5, dpi = 300)
  1290. p_nback <- draw_task_boxplot(plot_data_long, "nback")
  1291. print(p_nback)
  1292. ggsave("Figures/ACC_NBack_v.png", plot = p_nback, width = 8, height = 5, dpi = 300)
  1293. ```
  1294. ## Assumption Tests
  1295. We should check that our data fits the standard assumptions for a SEM (normality and linearity). Specifically, the relationships with the output should be linear, and the distributions of the input should be multivariate normal.
  1296. However, despite our inputs failing tests for normality, the MLR estimator builds robust standard errors that are able to handle non-normality.
  1297. ```{r}
  1298. # Load necessary libraries
  1299. library(ggplot2)
  1300. library(MVN)
  1301. library(dplyr)
  1302. library(tidyr)
  1303. # 1) Draw histograms for navon, stroop, and nback columns in multiple subplots
  1304. # Reshape data for easy plotting
  1305. combined_data_long <- combined_data_wide %>%
  1306. dplyr::select(navon_acc_high, navon_acc_low, stroop_acc_high, stroop_acc_low, nback_acc_low, nback_acc_medium, nback_acc_high) %>%
  1307. tidyr::pivot_longer(cols = everything(), names_to = "Variable", values_to = "Value")
  1308. # Plot histograms with each variable in a separate subplot
  1309. ggplot(combined_data_long, aes(x = Value)) +
  1310. geom_histogram(aes(fill = Variable), color = "black", bins = 20, alpha = 0.6) +
  1311. facet_wrap(~ Variable, scales = "free") +
  1312. labs(title = "Histograms for Navon, Stroop, and Nback Variables", x = "Value", y = "Frequency") +
  1313. theme_minimal() +
  1314. theme(legend.position = "none")
  1315. # 2) Multivariate normal distribution test
  1316. # Select only the relevant columns for testing
  1317. multivariate_data <- combined_data_wide %>%
  1318. dplyr::select(navon_acc_high, navon_acc_low, stroop_acc_high, stroop_acc_low, nback_acc_low, nback_acc_medium, nback_acc_high)
  1319. # Run Mardia's multivariate normality test
  1320. mardia_test <- mvn(multivariate_data, mvnTest = "mardia")
  1321. # Print results
  1322. print(mardia_test)
  1323. # Interpretation:
  1324. # If mardia_test$multivariateNormality$pValue.skew > 0.05 and mardia_test$multivariateNormality$pValue.kurt > 0.05,
  1325. # the data can be considered to follow a multivariate normal distribution.
  1326. ```
  1327. ### Linearity Test
  1328. We can check for linear relationships with each other. The output variable acc_general shows a general linear relationship with the otehr variables.
  1329. ```{r}
  1330. # Load necessary packages
  1331. library(GGally)
  1332. library(dplyr)
  1333. # Select only the columns with 'acc_general' and the task-specific variables
  1334. plot_data <- combined_data_wide %>%
  1335. select(acc_general, navon_acc_high, navon_acc_low, stroop_acc_high, stroop_acc_low, nback_acc_low, nback_acc_medium, nback_acc_high)
  1336. # Create scatterplot matrix
  1337. p <- ggpairs(plot_data, columns = 1:ncol(plot_data),
  1338. title = "Scatterplots of acc_general with Task-specific Variables")
  1339. # Save to file with 300 DPI
  1340. ggsave("Figures/ACC_Fig0_Linearity_Assumption.png", plot = p, dpi = 300, width = 12, height = 10)
  1341. ```
  1342. ## Run Factor Analysis (With Task Specific)
  1343. ### Run Exploratory Factor Analysis with Parallel Analysis
  1344. This analysis is to determine whether the single factor or bifactor model makes more sense for our data.
  1345. For the parallel analysis, we can see when the unadjusted EV falls under the Random EV, at which point we do not want to use
  1346. that many factors. We can see in the output that 1 factor is sufficient, and 2 factors just misses the cutoff.
  1347. Therefore, we can justify our decision to use a single factor model.
  1348. #### Plot Parallel Analysis Scree Plot
  1349. ```{r}
  1350. # Load necessary libraries
  1351. # install.packages(c("psych", "paran"))
  1352. library(psych)
  1353. library(paran)
  1354. library(dplyr)
  1355. # Subset the data to include only columns starting with "navon", "stroop", and "nback"
  1356. selected_data <- combined_data_wide[, grepl("^(navon|stroop|nback)_acc_", colnames(combined_data_wide))]
  1357. # selected_data$nback_acc_medium <- NULL
  1358. # Remove rows with any NA values
  1359. selected_data_complete <- na.omit(selected_data)
  1360. # Run parallel analysis on the complete dataset
  1361. paran_results <- paran(selected_data_complete, iterations = 1000, centile = 95, graph = TRUE)
  1362. # print(paran_results)
  1363. # Manually Graph Results
  1364. # Extract values from paran_results
  1365. adj_ev <- paran_results$AdjEv
  1366. ev <- paran_results$Ev
  1367. rnd_ev <- paran_results$RndEv
  1368. # Create data frame
  1369. plot_data <- data.frame(
  1370. Component = seq_along(ev),
  1371. Adjusted_EV = adj_ev,
  1372. Unadjusted_EV = ev,
  1373. Random_EV = rnd_ev
  1374. )
  1375. plot_data$Retained <- plot_data$Adjusted_EV > plot_data$Random_EV
  1376. # Create the plot
  1377. p <- ggplot(plot_data, aes(x = Component)) +
  1378. # Lines
  1379. geom_line(aes(y = Adjusted_EV, color = "Adjusted EV"), size = 1) +
  1380. geom_line(aes(y = Unadjusted_EV, color = "Unadjusted EV"), linetype = "dashed", size = 1) +
  1381. geom_line(aes(y = Random_EV, color = "Random EV"), linetype = "dotted", size = 1) +
  1382. # Points
  1383. geom_point(aes(y = Adjusted_EV, shape = Retained), size = 3, color = "black") +
  1384. geom_point(aes(y = Unadjusted_EV), size = 3, color = "red") +
  1385. geom_point(aes(y = Random_EV), size = 3, color = "blue") +
  1386. # Labels and theme
  1387. labs(
  1388. title = "Parallel Analysis Scree Plot",
  1389. x = "Number of Components",
  1390. y = "Eigenvalue",
  1391. color = "Legend",
  1392. shape = "Retained"
  1393. ) +
  1394. scale_color_manual(values = c("Adjusted EV" = "black", "Unadjusted EV" = "red", "Random EV" = "blue")) +
  1395. scale_shape_manual(values = c(`TRUE` = 16, `FALSE` = 1)) +
  1396. theme_minimal(base_size = 14, base_family = "Arial") +
  1397. theme(
  1398. plot.title = element_text(hjust = 0.5, face = "bold", size = 28),
  1399. axis.title = element_text(size = 24),
  1400. axis.text = element_text(size = 16),
  1401. legend.title = element_text(size = 18), # Legend title font size
  1402. legend.text = element_text(size = 16), # Legend item labels
  1403. legend.box.background = element_rect(color = "black", fill = NA),
  1404. legend.box = "vertical"
  1405. )
  1406. # Save plot as high-resolution PNG
  1407. ggsave("Figures/ACC_revised_parallel_analysis_plot.png", plot = p, width = 8, height = 6, dpi = 300, bg="white")
  1408. ```
  1409. ### Fit Model
  1410. Couple notes to keep track of:
  1411. Including the nback_acc_medium does cause the fit to get a lot worse. It's the variable with the lowest loading.
  1412. Additionally, the loading is not significant. However, removing this variable does not change the results, so I think
  1413. it's fine to proceed for now.
  1414. ```{r}
  1415. library(lavaan)
  1416. set.seed(1234)
  1417. model <- '
  1418. # General factors (pre and post)
  1419. acc_general =~ navon_acc_low + navon_acc_high + stroop_acc_low + stroop_acc_high + nback_acc_low + nback_acc_high + nback_acc_medium
  1420. '
  1421. # Fit the model
  1422. fit <- cfa(model, data = combined_data_wide, estimator = "MLR", missing = "fiml", std.lv=TRUE)
  1423. # Summarize the results
  1424. summary(fit, fit.measures = TRUE, standardized = TRUE)
  1425. ```
  1426. ```{r}
  1427. library(lavaanPlot)
  1428. l = lavaanPlot(model = fit, coefs = TRUE)
  1429. print(l)
  1430. library(semPlot)
  1431. # Updated plotting code for top-to-bottom, black-and-white SEM diagram
  1432. custom_labels <- c("Navon V Low", "Navon V High", "Stroop V Low",
  1433. "Stroop V High", "N-Back V Low", "N-Back V High",
  1434. "N-Back V Medium", "V General")
  1435. # semPlot::semPaths(fit,
  1436. # what = "est", # Plot estimated coefficients
  1437. # edge.label.cex = 1.5, # Adjust size of edge labels
  1438. # node.label.cex = 1.5, # Adjust size of node labels
  1439. # sizeMan = 10,
  1440. # sizeLat = 10,
  1441. # sizeInt = 5,
  1442. # layout = "tree", # Layout style: tree for top-to-bottom
  1443. # rotation = 1, # Rotate diagram for top-to-bottom layout
  1444. # color = list(lat = "white", man = "white", int = "black"), # Black for edges, white for nodes
  1445. # edge.color = "black", # Black arrows
  1446. # label.color = "black",
  1447. # style = "lisrel", # Style for a clean, structured plot
  1448. # exoCov = FALSE, # Remove covariances for exogenous variables
  1449. # title = FALSE) # Apply custom variable labels) # Hide residuals
  1450. ```
  1451. ### Run Measurement Invariance Tests
  1452. Test configural invariance (model structure equivalence)
  1453. This step checks whether the basic factor structure (number of factors and factor loadings) is the same across groups.
  1454. The output seems to verify configural variance.
  1455. ```{r}
  1456. fit_configural <- cfa(model, data = combined_data_wide, group = "Timepoint")
  1457. summary(fit_configural, fit.measures = TRUE)
  1458. ```
  1459. Test metric invariance (equivalence of factor loadings)
  1460. Next, impose constraints on the factor loadings to be the same across groups.
  1461. The model does not pass the metric invariance test as clearly as it did the configural invariance test. The CFI and TLI are below ideal thresholds, and the SRMR is above the acceptable cutoff. The RMSEA is within the acceptable range, but the confidence interval suggests some variability in fit quality.
  1462. However, the chi-square test indicates that the fit of the model is not significantly worse, so you may choose to move forward with partial metric invariance testing by freeing some factor loadings if necessary or investigating specific sources of misfit.
  1463. By removing the nback_medium term, I get 'reasonable metric invariance' with slightly better fit terms. But for now, I will use the full model for completeness.
  1464. ```{r}
  1465. fit_metric <- cfa(model, data = combined_data_wide, group = "Timepoint", group.equal = "loadings")
  1466. summary(fit_metric, fit.measures = TRUE)
  1467. ```
  1468. ### Compute Fit Metrics
  1469. We want to compute the same fit metrics as mentioned in Weigard.
  1470. We see that Omega Hierarchical for acc_general: 0.6309885, indicating that the task-general factor explains 63.1% of the data, which isn't perfect but moderate.
  1471. The Mean Lambda (λ) for acc_general: 0.456063, which is also a moderate loading, indicating that each model moderately loads onto the factor.
  1472. ```{r}
  1473. library(psych)
  1474. library(lavaan)
  1475. # 1. Extract Fit Statistics
  1476. # The `summary` function with `fit.measures = TRUE` will provide key fit indices
  1477. summary(fit, fit.measures = TRUE, standardized = TRUE)
  1478. # 2. Extract standardized loadings to calculate mean λ
  1479. # Get the standardized solution
  1480. standardized_solution <- standardizedSolution(fit)
  1481. # Filter to only loadings for `acc_general` factor
  1482. acc_general_loadings <- standardized_solution[standardized_solution$lhs == "acc_general" & standardized_solution$op == "=~", "est.std"]
  1483. # Calculate mean λ for the `acc_general` factor
  1484. mean_lambda <- mean(acc_general_loadings)
  1485. cat("Mean Lambda (λ) for acc_general:", mean_lambda, "\n")
  1486. # 3. Omega Calculation for General Factor
  1487. # Use the `omega` function from the `psych` package
  1488. # First, extract the relevant columns from combined_data_wide for the general factor
  1489. acc_general_data <- combined_data_wide[, c("navon_acc_low", "navon_acc_high", "stroop_acc_low", "stroop_acc_high", "nback_acc_low", "nback_acc_high", "nback_acc_medium")]
  1490. # Run omega analysis
  1491. omega_result <- omega(acc_general_data, nfactors = 1) # Set `nfactors = 1` for one general factor
  1492. # Display omega hierarchical for acc_general
  1493. cat("Omega Hierarchical for acc_general:", omega_result$omega_h, "\n")
  1494. ```
  1495. ### Extract Subject Level Factors
  1496. ```{r}
  1497. # Compute the factor scores for the general factor 'acc_general'
  1498. general_acc_rate <- lavPredict(fit, type = "lv") # 'lv' means latent variable
  1499. # Extract the 'acc_general' column
  1500. general_acc_rate <- as.data.frame(general_acc_rate)$acc_general
  1501. combined_data_wide$acc_general <- general_acc_rate
  1502. ```
  1503. # Run Regressions
  1504. ## Attach Network Metrics
  1505. ```{r}
  1506. library(stringr)
  1507. library(dplyr)
  1508. # Read the connectivity file
  1509. connectivity_data <- read.csv("FinalData/bk_allnets_avg_output_final.csv")
  1510. # Extract subject ID
  1511. connectivity_data$Subj <- str_extract(connectivity_data$Row, "(?<=sub-)[A-Za-z]+(?=_big_corr)")
  1512. connectivity_data$Subj <- str_replace(connectivity_data$Subj, "^([A-Za-z])", "\\1_")
  1513. # Rename column to be "FileName"
  1514. names(connectivity_data)[names(connectivity_data) == "Row"] <- "FileName"
  1515. # Merge data
  1516. raw_metrics_full_data <- left_join(combined_data_wide, connectivity_data, by = "Subj")
  1517. ```
  1518. ## Attach Days Covariate
  1519. ```{r}
  1520. # Load the necessary library
  1521. library(dplyr)
  1522. # Read the file
  1523. behavioral_data <- read.csv("FinalData/n-back_exp_results.csv")
  1524. # Extract unique combinations of Subj, Stimulation_Site, and Days
  1525. unique_combinations <- behavioral_data %>%
  1526. distinct(Subj, Stimulation_Site, Days, .keep_all = FALSE)
  1527. # View the results
  1528. print(unique_combinations)
  1529. library(data.table)
  1530. # Assuming unique_combinations is already a data.table. If not, convert it:
  1531. setDT(unique_combinations)
  1532. # Update Stimulation_Site values
  1533. unique_combinations[Stimulation_Site == "FPCNB", Stimulation_Site := "FPCN-B"]
  1534. unique_combinations[Stimulation_Site == "vertex", Stimulation_Site := "Vertex"]
  1535. # Merge data
  1536. raw_metrics_full_data_days <- left_join(raw_metrics_full_data, unique_combinations, by = c("Subj", "Stimulation_Site"))
  1537. ```
  1538. ## Look at baseline measures
  1539. ```{r}
  1540. # Scale Days as well (MAKE SURE THESE METRICS WEREN'T SCALED BEFORE)
  1541. # raw_metrics_full_data_days$Days = scale(raw_metrics_full_data_days$Days)
  1542. # raw_metrics_full_data_days$FPCN_B.FPCN_B = scale(raw_metrics_full_data_days$FPCN_B.FPCN_B)
  1543. # raw_metrics_full_data_days$DMN_Canonical.DMN_Canonical = scale(raw_metrics_full_data_days$DMN_Canonical.DMN_Canonical)
  1544. # raw_metrics_full_data_days$DMN_Canonical.FPCN_B = scale(raw_metrics_full_data_days$DMN_Canonical.FPCN_B)
  1545. ```
  1546. ```{r}
  1547. library(lmerTest)
  1548. ```
  1549. ```{r}
  1550. # pre_data = filter(raw_metrics_full_data_days, Timepoint == "pre")
  1551. # model_0 <- lmer(acc_general ~ (DMN_Canonical.FPCN_B + DMN_Canonical.DMN_Canonical + FPCN_B.FPCN_B) + Days + (1 | Subj), data = pre_data, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1552. # summary(model_0)
  1553. pre_data = filter(raw_metrics_full_data_days, Timepoint == "pre")
  1554. model_0 <- lmer(acc_general ~ (DMN_Canonical.FPCN_B) + Days + (1 | Subj), data = pre_data, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1555. summary(model_0)
  1556. ```
  1557. ## Look at Raw Stimulation Effects without Network Moderators
  1558. ```{r}
  1559. raw_metrics_full_data_days$Stimulation_Site <- factor(raw_metrics_full_data_days$Stimulation_Site)
  1560. # Set "Vertex" as the baseline (reference level)
  1561. raw_metrics_full_data_days$Stimulation_Site <- relevel(raw_metrics_full_data_days$Stimulation_Site, ref = "Vertex")
  1562. model_1a <- lmer(acc_general ~ Stimulation_Site * Timepoint + Days + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1563. summary(model_1a)
  1564. ```
  1565. ## Look at TMS Network Stimulation Effects
  1566. ```{r}
  1567. # model_1 <- lmer(acc_general ~ (DMN_Canonical.FPCN_B + DMN_Canonical.DMN_Canonical + FPCN_B.FPCN_B) * Stimulation_Site * Timepoint + Days + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1568. # Ensure Stimulation_Site is a factor
  1569. raw_metrics_full_data_days$Stimulation_Site <- factor(raw_metrics_full_data_days$Stimulation_Site)
  1570. # Set "Vertex" as the baseline (reference level)
  1571. raw_metrics_full_data_days$Stimulation_Site <- relevel(raw_metrics_full_data_days$Stimulation_Site, ref = "Vertex")
  1572. model_1 <- lmer(acc_general ~ (DMN_Canonical.FPCN_B) * Stimulation_Site * Timepoint + Days + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1573. summary(model_1)
  1574. ```
  1575. ### Contrasts
  1576. ```{r}
  1577. library(emmeans)
  1578. ```
  1579. ## Look at network moderation of Stimulation Effects
  1580. I'm commenting out the code to test out all three networks and only leaving in those for the FPCN-DMN anti-correlation
  1581. ```{r}
  1582. # # Linear mixed-effects model to regress out 'Days'
  1583. # days_effect_acc_rate_model <- lm(acc_general ~ Days, data = raw_metrics_full_data_days)
  1584. # # Extract residuals which represent the part of 'v' not explained by 'Days'
  1585. # raw_metrics_full_data_days$acc_resid = residuals(days_effect_acc_rate_model)
  1586. #
  1587. # acc_rate_model_days_resid <- lmer(acc_resid ~ (DMN_Canonical.FPCN_B + DMN_Canonical.DMN_Canonical + FPCN_B.FPCN_B)*Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1588. # Linear mixed-effects model to regress out 'Days'
  1589. days_effect_acc_rate_model <- lm(acc_general ~ Days, data = raw_metrics_full_data_days)
  1590. # Extract residuals which represent the part of 'v' not explained by 'Days'
  1591. raw_metrics_full_data_days$acc_resid = residuals(days_effect_acc_rate_model)
  1592. acc_rate_model_days_resid <- lmer(acc_resid ~ (DMN_Canonical.FPCN_B)*Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1593. ```
  1594. This is just the stimulation effects without any network moderators
  1595. ```{r}
  1596. just_stim_model_resid <- lmer(acc_resid ~ Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1597. model_1_emmeans = emmeans(just_stim_model_resid, ~ Stimulation_Site * Timepoint, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1598. # Look at the pairwise comparisons for the interaction
  1599. pairs(model_1_emmeans)
  1600. # Contrast the change from pre to post for FPCN-B vs Vertex
  1601. mdl_1_emmeans = contrast(model_1_emmeans, interaction = c("revpairwise"), adjust = "none")
  1602. mdl_1_emmeans
  1603. ```
  1604. Same analysis but using the full model
  1605. ```{r}
  1606. just_stim_model_resid <- lmer(acc_resid ~ Stimulation_Site*Timepoint + (1 | Subj), data = raw_metrics_full_data_days, control=lmerControl(optimizer="bobyqa",optCtrl=list(maxfun=100000)),REML=F)
  1607. model_1_just_stim_emmeans = emmeans(just_stim_model_resid, ~ Stimulation_Site, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1608. model_1_complete_emmeans = emmeans(acc_rate_model_days_resid, ~ Stimulation_Site, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1609. model_1_complete_emmeans_timepoint = emmeans(acc_rate_model_days_resid, ~ Timepoint, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1610. model_1_complete_emmeans_network = emtrends(acc_rate_model_days_resid, ~ 1, var = "DMN_Canonical.FPCN_B", pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1611. # Look at the pairwise comparisons for the interaction
  1612. pairs(model_1_complete_emmeans)
  1613. # Contrast the change from pre to post for FPCN-B vs Vertex
  1614. model_1_complete_emmeans_int = emmeans(acc_rate_model_days_resid, ~ Stimulation_Site * Timepoint, pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1615. mdl_1_emmeans = contrast(model_1_complete_emmeans, interaction = c("revpairwise"), adjust = "none")
  1616. mdl_1_emmeans
  1617. ```
  1618. This is our main result below
  1619. ```{r}
  1620. mdl_4_small = emtrends(acc_rate_model_days_resid, pairwise ~ Timepoint * Stimulation_Site, var="DMN_Canonical.FPCN_B", pbkrtest.limit = 30000, lmerTest.limit = 30000)
  1621. mdl_4_small_contrast = contrast(mdl_4_small[[1]], interaction = c("revpairwise"), adjust = "none")
  1622. mdl_4_small_contrast
  1623. ```
  1624. # Draw Plots
  1625. ```{r}
  1626. library(ggeffects)
  1627. library(dplyr)
  1628. library(ggplot2)
  1629. library(data.table)
  1630. # loadfonts(device = "win")
  1631. plot_metrics_stim_site = function(model, metric, metric_name, model_name){
  1632. # Remove hardcoded overrides to allow function to be generic
  1633. # metric_name = "FPCN-B and DMN\nConnectivity"
  1634. # model_name = "Accuracy"
  1635. terms_vec = c(metric, "Stimulation_Site", "Timepoint")
  1636. preds <- predict_response(model, terms = terms_vec, interval="confidence", margin="mean_reference", back.transform = FALSE)
  1637. preds = as.data.table(preds)
  1638. contrast_preds = preds
  1639. preds <- preds %>%
  1640. mutate(
  1641. sem = (conf.high - conf.low) / (2 * 1.96) # approximate SEM from 95% CI
  1642. )
  1643. preds <- preds %>%
  1644. mutate(
  1645. ci.low_sem = predicted - sem,
  1646. ci.high_sem = predicted + sem
  1647. )
  1648. filtered_preds_df = preds
  1649. # --- Data Preparation for Individual Points (Added) ---
  1650. # Extract Random Intercepts
  1651. re_intercepts <- as.data.frame(ranef(model)$Subj)
  1652. re_intercepts$Subj <- rownames(re_intercepts)
  1653. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  1654. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  1655. names(re_intercepts) <- c("Subj", "RandomIntercept")
  1656. # Prepare raw data points with random intercept adjustment
  1657. # Note: model should be the residualized model 'acc_rate_model_days_resid'
  1658. # ensuring we use 'acc_resid' for consistency with the model response.
  1659. raw_data_plot <- raw_metrics_full_data_days %>%
  1660. mutate(across(all_of(metric), as.numeric)) %>%
  1661. left_join(re_intercepts, by = "Subj") %>%
  1662. mutate(
  1663. v_adjusted = acc_resid - RandomIntercept,
  1664. # Map Timepoint to match prediction labels if necessary
  1665. Timepoint = recode(Timepoint, "pre" = "pre", "post" = "post"),
  1666. facet = Timepoint # Use 'facet' column to match ggplot logical mapping
  1667. )
  1668. # Prepare Individual CONTRAST Data (Difference Scores)
  1669. subj_contrasts <- raw_data_plot %>%
  1670. dplyr::select(Subj, all_of(metric), Stimulation_Site, Timepoint, v_adjusted) %>%
  1671. pivot_wider(
  1672. id_cols = c(Subj, all_of(metric)),
  1673. names_from = c(Stimulation_Site, Timepoint),
  1674. values_from = v_adjusted,
  1675. names_sep = "_"
  1676. ) %>%
  1677. mutate(
  1678. fpcnb_contrast = `FPCN-B_post` - `FPCN-B_pre`,
  1679. vertex_contrast = `Vertex_post` - `Vertex_pre`,
  1680. dan_contrast = `DAN_post` - `DAN_pre`,
  1681. fpcnb_vertex_contrast = fpcnb_contrast - vertex_contrast,
  1682. fpcnb_dan_contrast = fpcnb_contrast - dan_contrast,
  1683. dan_vertex_contrast = dan_contrast - vertex_contrast
  1684. )
  1685. contrast_df = dplyr::select(contrast_preds, -c(conf.low, conf.high)) %>%
  1686. pivot_wider(
  1687. names_from = c(group, facet),
  1688. values_from = c(predicted, std.error),
  1689. names_sep = "_"
  1690. )
  1691. final_contrasts <- contrast_df %>%
  1692. mutate(
  1693. fpcnb_contrast = `predicted_FPCN-B_post` - `predicted_FPCN-B_pre`,
  1694. vertex_contrast = predicted_Vertex_post - predicted_Vertex_pre,
  1695. dan_contrast = predicted_DAN_post - predicted_DAN_pre,
  1696. fpcnb_contrast.error = sqrt(`std.error_FPCN-B_post`^2 + `std.error_FPCN-B_pre`^2),
  1697. vertex_contrast.error = sqrt(std.error_Vertex_post^2 + std.error_Vertex_pre^2),
  1698. dan_contrast.error = sqrt(std.error_DAN_post^2 + std.error_DAN_pre^2),
  1699. fpcnb_vertex_contrast = fpcnb_contrast - vertex_contrast,
  1700. fpcnb_dan_contrast = fpcnb_contrast - dan_contrast,
  1701. dan_vertex_contrast = dan_contrast - vertex_contrast,
  1702. fpcnb_vertex_std.error = sqrt(fpcnb_contrast.error^2 + vertex_contrast.error^2),
  1703. fpcnb_dan_std.error = sqrt(fpcnb_contrast.error^2 + dan_contrast.error^2),
  1704. dan_vertex_std.error = sqrt(vertex_contrast.error^2 + dan_contrast.error^2)
  1705. ) %>%
  1706. mutate(
  1707. dan_conf.low = dan_contrast - dan_contrast.error,
  1708. dan_conf.high = dan_contrast + dan_contrast.error,
  1709. fpcnb_conf.low = fpcnb_contrast - fpcnb_contrast.error,
  1710. fpcnb_conf.high = fpcnb_contrast + fpcnb_contrast.error,
  1711. vertex_conf.low = vertex_contrast - vertex_contrast.error,
  1712. vertex_conf.high = vertex_contrast + vertex_contrast.error,
  1713. fpcnb_vertex_conf.low = fpcnb_vertex_contrast - fpcnb_vertex_std.error,
  1714. fpcnb_vertex_conf.high = fpcnb_vertex_contrast + fpcnb_vertex_std.error,
  1715. fpcnb_dan_conf.low = fpcnb_dan_contrast - fpcnb_dan_std.error,
  1716. fpcnb_dan_conf.high = fpcnb_dan_contrast + fpcnb_dan_std.error,
  1717. dan_vertex_conf.low = dan_vertex_contrast - dan_vertex_std.error,
  1718. dan_vertex_conf.high = dan_vertex_contrast + dan_vertex_std.error
  1719. )
  1720. ### Plotting
  1721. custom_theme <- theme_minimal(base_size = 24) +
  1722. theme(plot.title = element_text(size = rel(2.2), hjust = 0.5),
  1723. plot.background = element_blank(),
  1724. plot.margin = margin(t = 2, r = 1, b = 1, l = 30, unit = "pt"),
  1725. panel.background = element_rect(fill = "white"),
  1726. panel.grid.major = element_blank(),
  1727. panel.grid.minor = element_blank(),
  1728. axis.line = element_blank(),
  1729. axis.ticks = element_line(color = "black"),
  1730. axis.title.x = element_text(size = rel(3.0), margin = margin(t = 10), lineheight = 0.7),
  1731. axis.title.y = element_text(size = rel(3.0), margin = margin(r = 10), lineheight = 1.2),
  1732. axis.text = element_text(size = rel(2.7)),
  1733. strip.text = element_text(size = rel(2.2)),
  1734. legend.position = "right",
  1735. legend.title = element_blank(),
  1736. legend.background = element_rect(color = "black", size = .5),
  1737. legend.text = element_text(size = rel(2)),
  1738. legend.spacing.y = unit(0.5, "cm"),
  1739. panel.border = element_blank(),
  1740. text = element_text(family = "Arial"))
  1741. clean_metric_name <- gsub(" ", "", "FPCN-BandDMNConnectivity") # Kept consistent with old code behavior if needed, or use metric_name
  1742. # Ideally, use: clean_metric_name <- gsub(" ", "", metric_name)
  1743. # But assuming "FPCN-BandDMNConnectivity" was important for file naming consistency based on user prompt context "clean up",
  1744. # I will use the function argument `metric_name` logic.
  1745. clean_metric_name <- gsub("[\n ]", "", metric_name)
  1746. # --- Main Interaction Plots with Points ---
  1747. # Helper to plot site
  1748. plot_site <- function(site_name, file_suffix) {
  1749. site_preds <- filter(preds, group == site_name)
  1750. site_raw <- filter(raw_data_plot, Stimulation_Site == site_name)
  1751. p = ggplot(site_preds, aes(x = x, y = predicted, color = facet)) +
  1752. # Add Connecting Lines
  1753. geom_line(data = site_raw, aes(x = .data[[metric]], y = v_adjusted, group = Subj),
  1754. color = "gray80", alpha = 0.5) +
  1755. # Add Adjusted Individual Points
  1756. geom_point(data = site_raw, aes(x = .data[[metric]], y = v_adjusted, color = facet),
  1757. alpha = 1, size = 3) +
  1758. geom_ribbon(aes(ymin = ci.low_sem, ymax = ci.high_sem, fill = facet), linewidth = 0, alpha = 0.2) +
  1759. geom_line(size = 1.5) +
  1760. labs(y = model_name, x = metric_name) +
  1761. custom_theme
  1762. ggsave(filename = paste0("Figures/", metric_type, "_FilteredPreds_", file_suffix, "_", clean_metric_name, ".png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1763. }
  1764. plot_site("FPCN-B", "FPCNB")
  1765. plot_site("Vertex", "Vertex")
  1766. plot_site("DAN", "DAN")
  1767. # --- Contrast Plots ---
  1768. # Helper for contrasts
  1769. plot_contrast <- function(y_var, y_low, y_high, file_suffix, extra_theme = NULL) {
  1770. p = ggplot(final_contrasts, aes(x = x)) +
  1771. # Add Adjusted Individual Points (Contrasts)
  1772. geom_point(data = subj_contrasts, aes(x = .data[[metric]], y = .data[[y_var]]),
  1773. alpha = 0.4, size = 2.5, color = "black") +
  1774. geom_ribbon(aes(ymin = .data[[y_low]], ymax = .data[[y_high]]), linetype = "dotted", linewidth = 1, fill = "gray", alpha = 0.2, color = "gray") +
  1775. geom_line(size = 1.5, aes(y = .data[[y_var]]), color='black') +
  1776. labs(y = model_name, x = metric_name) +
  1777. custom_theme
  1778. if (!is.null(extra_theme)) {
  1779. p <- p + extra_theme
  1780. }
  1781. ggsave(filename = paste0("Figures/", metric_type, "_GeneralContrastOf_", file_suffix, "_", clean_metric_name, ".png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1782. }
  1783. plot_contrast("fpcnb_contrast", "fpcnb_conf.low", "fpcnb_conf.high", "FPCN")
  1784. plot_contrast("vertex_contrast", "vertex_conf.low", "vertex_conf.high", "Vertex")
  1785. plot_contrast("dan_contrast", "dan_conf.low", "dan_conf.high", "DAN")
  1786. plot_contrast("fpcnb_vertex_contrast", "fpcnb_vertex_conf.low", "fpcnb_vertex_conf.high", "FPCNVertex")
  1787. # FPCN-DAN contrast has specific margin
  1788. plot_contrast("fpcnb_dan_contrast", "fpcnb_dan_conf.low", "fpcnb_dan_conf.high", "FPCNDAN",
  1789. extra_theme = theme(plot.margin = margin(t = 20, r = 1, b = 1, l = 1, unit = "pt")))
  1790. plot_contrast("dan_vertex_contrast", "dan_vertex_conf.low", "dan_vertex_conf.high", "DANVertex")
  1791. }
  1792. ```
  1793. ```{r}
  1794. plot_metrics_stim_site(acc_rate_model_days_resid, "DMN_Canonical.FPCN_B", "FPCN-B and DMN\nConnectivity", "Accuracy")
  1795. ```
  1796. ## Draw Network emmeans
  1797. ```{r}
  1798. # Load necessary library
  1799. library(ggplot2)
  1800. preds <- predict_response(
  1801. model_1,
  1802. terms = c("DMN_Canonical.FPCN_B"),
  1803. interval = "confidence",
  1804. margin = "mean_reference",
  1805. back.transform = FALSE
  1806. )
  1807. preds = as.data.table(preds)
  1808. preds <- preds %>%
  1809. mutate(
  1810. sem = (conf.high - conf.low) / (2 * 1.96) # approximate SEM from 95% CI
  1811. )
  1812. preds <- preds %>%
  1813. mutate(
  1814. ci.low_sem = predicted - sem,
  1815. ci.high_sem = predicted + sem
  1816. )
  1817. preds_df = preds
  1818. # Calculate subject means for scatterplot and adjust for Random Intercepts
  1819. # This assumes the large spread is due to subject baseline differences (Random Intercepts).
  1820. # By subtracting the Random Intercept, we visualize the "Partial Residuals" - showing the
  1821. # relationship between Network and Accuracy after controlling for individual baselines.
  1822. # 1. Extract Random Intercepts
  1823. re_intercepts <- as.data.frame(ranef(model_1)$Subj)
  1824. re_intercepts$Subj <- rownames(re_intercepts)
  1825. # Select only the intercept (in case there are random slopes) and rename
  1826. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  1827. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  1828. names(re_intercepts) <- c("Subj", "RandomIntercept")
  1829. # 2. Calculate adjusted means
  1830. subj_means <- raw_metrics_full_data_days %>%
  1831. mutate(DMN_Canonical.FPCN_B = as.numeric(DMN_Canonical.FPCN_B)) %>%
  1832. group_by(Subj) %>%
  1833. summarise(
  1834. mean_v = mean(acc_general, na.rm = TRUE),
  1835. network_val = mean(DMN_Canonical.FPCN_B, na.rm = TRUE)
  1836. ) %>%
  1837. left_join(re_intercepts, by = "Subj") %>%
  1838. mutate(adjusted_mean_v = mean_v - RandomIntercept)
  1839. custom_theme <- theme_minimal(base_size = 20) +
  1840. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  1841. plot.background = element_blank(), # White background for the plot
  1842. panel.background = element_rect(fill = "white"), # White background for the panels
  1843. panel.grid.major = element_blank(), # Remove major grid lines
  1844. panel.grid.minor = element_blank(), # Remove minor grid lines
  1845. axis.line = element_blank(), # Define the axis lines without enclosing
  1846. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  1847. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  1848. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  1849. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  1850. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  1851. legend.position = "right", # Position the legend on the right
  1852. legend.title = element_blank(), # Customize the legend title size
  1853. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  1854. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  1855. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  1856. panel.border = element_blank(), # Ensure no border around the panels
  1857. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  1858. # Create the plot
  1859. p = ggplot(preds_df, aes(x = x, y = predicted)) +
  1860. geom_point(data = subj_means, aes(x = network_val, y = adjusted_mean_v),
  1861. color = "black", alpha = 0.6, size = 3) +
  1862. geom_line(size = 2, position = position_dodge(width = 0.5)) + # Add points
  1863. geom_ribbon(aes(ymin = ci.low_sem, ymax = ci.high_sem), linetype = "dotted", linewidth = 1, fill = "gray", alpha = 0.2) +
  1864. # scale_x_continuous(limits = c(-2, 2)) +
  1865. labs(title = "Network Connectivity",
  1866. y = "", x = "FPCN-B and DMN Connectivity") +
  1867. theme_minimal(base_size = 14) +
  1868. custom_theme +
  1869. theme(legend.position = "none")
  1870. # Use ggsave() to save the plot
  1871. ggsave(filename = paste0("Figures/ACC_EMMeanNetworkDriftRate.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1872. ```
  1873. ## Draw Stimulation emmeans (No Timepoint)
  1874. ```{r}
  1875. # Load necessary library
  1876. library(ggplot2)
  1877. library(emmeans)
  1878. # Create the data frame directly from the emmeans object
  1879. model_1_complete_emmeans_df <- as.data.frame(model_1_complete_emmeans)
  1880. custom_theme <- theme_minimal(base_size = 20) +
  1881. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  1882. plot.background = element_blank(), # White background for the plot
  1883. panel.background = element_rect(fill = "white"), # White background for the panels
  1884. panel.grid.major = element_blank(), # Remove major grid lines
  1885. panel.grid.minor = element_blank(), # Remove minor grid lines
  1886. axis.line = element_blank(), # Define the axis lines without enclosing
  1887. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  1888. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  1889. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  1890. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  1891. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  1892. legend.position = "right", # Position the legend on the right
  1893. legend.title = element_blank(), # Customize the legend title size
  1894. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  1895. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  1896. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  1897. panel.border = element_blank(), # Ensure no border around the panels
  1898. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  1899. # Convert Stimulation_Site to a factor and set the order
  1900. model_1_complete_emmeans_df$Stimulation_Site <- factor(model_1_complete_emmeans_df$Stimulation_Site, levels = c("FPCN-B", "Vertex", "DAN"))
  1901. # Create the plot
  1902. p = ggplot(model_1_complete_emmeans_df, aes(x = Stimulation_Site, y = emmean, color = Stimulation_Site)) +
  1903. geom_point(size = 6, position = position_dodge(width = 0.5)) + # Add points
  1904. geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = 0.2, size = 2, position = position_dodge(width = 0.5)) + # Error bars
  1905. geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # Reference line
  1906. scale_color_manual(values = c("DAN" = "blue", "Vertex" = "gray", "FPCN-B" = "red")) + # Set colors for points
  1907. labs(title = "Stimulation Site",
  1908. y = "Estimated Marginal Mean of \nGeneral Accuracy", x = "Stimulation Site") +
  1909. theme_minimal(base_size = 14) +
  1910. custom_theme +
  1911. theme(legend.position = "none")
  1912. # Use ggsave() to save the plot
  1913. ggsave(filename = paste0("Figures/ACC_EMMeanStimSiteDriftRate_Automated.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  1914. # Print the plot
  1915. print(p)
  1916. ```
  1917. ### Draw Stimulation Site separated by Timepoint
  1918. ```{r}
  1919. # Load necessary libraries
  1920. library(ggplot2)
  1921. library(dplyr)
  1922. library(emmeans)
  1923. # Convert emmeans object to dataframe
  1924. # We use a new variable name to avoid overwriting the original object or the manual dataframe below
  1925. model_1_complete_emmeans_int_df <- as.data.frame(model_1_complete_emmeans_int)
  1926. # Use SE for error bars to match manual plot (Mean +/- SE)
  1927. model_1_complete_emmeans_int_df <- model_1_complete_emmeans_int_df %>%
  1928. mutate(
  1929. ci.low_sem = emmean - SE,
  1930. ci.high_sem = emmean + SE
  1931. )
  1932. # Capitalize Timepoint labels
  1933. model_1_complete_emmeans_int_df <- model_1_complete_emmeans_int_df %>%
  1934. mutate(Timepoint = recode(Timepoint, "pre" = "Pre", "post" = "Post"))
  1935. # Convert factors and set order for proper plotting
  1936. model_1_complete_emmeans_int_df$Stimulation_Site <- factor(
  1937. model_1_complete_emmeans_int_df$Stimulation_Site,
  1938. levels = c("FPCN-B", "Vertex", "DAN")
  1939. )
  1940. model_1_complete_emmeans_int_df$Timepoint <- factor(
  1941. model_1_complete_emmeans_int_df$Timepoint,
  1942. levels = c("Pre", "Post")
  1943. )
  1944. # Define custom theme
  1945. custom_theme <- theme_minimal(base_size = 20) +
  1946. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"),
  1947. plot.background = element_blank(),
  1948. panel.background = element_rect(fill = "white"),
  1949. panel.grid.major = element_blank(),
  1950. panel.grid.minor = element_blank(),
  1951. axis.ticks = element_line(color = "black"),
  1952. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2),
  1953. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2),
  1954. axis.text = element_text(size = rel(1.8)),
  1955. strip.text = element_text(size = rel(2.2)),
  1956. legend.title = element_blank(),
  1957. legend.text = element_text(size = rel(1.5)),
  1958. legend.spacing.y = unit(0.5, "cm"),
  1959. panel.border = element_blank(),
  1960. text = element_text(family = "Arial"))
  1961. # Prepare raw data for plotting individual points with Random Intercept subtraction
  1962. # Extract Random Intercepts
  1963. re_intercepts <- as.data.frame(ranef(model_1)$Subj)
  1964. re_intercepts$Subj <- rownames(re_intercepts)
  1965. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  1966. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  1967. names(re_intercepts) <- c("Subj", "RandomIntercept")
  1968. raw_data_plot <- raw_metrics_full_data_days %>%
  1969. left_join(re_intercepts, by = "Subj") %>%
  1970. mutate(
  1971. acc_resid_centered = acc_resid - RandomIntercept,
  1972. Timepoint = recode(Timepoint, "pre" = "Pre", "post" = "Post"),
  1973. Stimulation_Site = factor(Stimulation_Site, levels = c("FPCN-B", "Vertex", "DAN")),
  1974. Timepoint = factor(Timepoint, levels = c("Pre", "Post"))
  1975. )
  1976. # Create the plot
  1977. p <- ggplot(model_1_complete_emmeans_int_df,
  1978. aes(x = Stimulation_Site, y = emmean, group = Timepoint)) +
  1979. # 1. Error bars (Bottom layer)
  1980. geom_errorbar(aes(ymin = ci.low_sem, ymax = ci.high_sem, color = Timepoint),
  1981. width = 0.2,
  1982. position = position_dodge(width = 0.5),
  1983. size = 1.2) +
  1984. # 2. Individual points (Middle layer, alpha increased for visibility)
  1985. geom_point(data = raw_data_plot, aes(y = acc_resid_centered, color = Timepoint),
  1986. position = position_jitterdodge(jitter.width = 0.25, dodge.width = 0.5),
  1987. alpha = 0.4, size = 2.5) +
  1988. # 3. Mean points (Top layer)
  1989. geom_point(size = 6, shape = 16, position = position_dodge(width = 0.5), aes(color = Timepoint)) +
  1990. geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # Reference line
  1991. scale_color_manual(values = c("Pre" = "blue", "Post" = "red")) + # Different colors for error bars
  1992. labs(title = "Stimulation Site",
  1993. y = "General Accuracy",
  1994. x = "Stimulation Site") +
  1995. theme_minimal(base_size = 14) +
  1996. custom_theme +
  1997. theme(legend.position = "right") # Keep the legend
  1998. # Save the plot
  1999. ggsave(filename = paste0("Figures/ACC_EMMeanStimSiteAndTimepoint_Automated.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  2000. print(p)
  2001. ```
  2002. ## Draw Timepoint EMMeans (automated)
  2003. ```{r}
  2004. # Load necessary libraries
  2005. library(ggplot2)
  2006. library(dplyr)
  2007. library(emmeans)
  2008. # Create the data frame directly from the emmeans object
  2009. # Using a new variable name to avoid conflicts
  2010. model_1_complete_emmeans_timepoint_df <- as.data.frame(model_1_complete_emmeans_timepoint)
  2011. # Capitalize Timepoint labels to match manual plot
  2012. model_1_complete_emmeans_timepoint_df <- model_1_complete_emmeans_timepoint_df %>%
  2013. mutate(Timepoint = recode(Timepoint, "pre" = "Pre", "post" = "Post"))
  2014. # Convert Timepoint to a factor and set the order
  2015. model_1_complete_emmeans_timepoint_df$Timepoint <- factor(model_1_complete_emmeans_timepoint_df$Timepoint, levels = c("Pre", "Post"))
  2016. custom_theme <- theme_minimal(base_size = 20) +
  2017. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  2018. plot.background = element_blank(), # White background for the plot
  2019. panel.background = element_rect(fill = "white"), # White background for the panels
  2020. panel.grid.major = element_blank(), # Remove major grid lines
  2021. panel.grid.minor = element_blank(), # Remove minor grid lines
  2022. axis.line = element_blank(), # Define the axis lines without enclosing
  2023. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  2024. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  2025. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  2026. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  2027. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  2028. legend.position = "right", # Position the legend on the right
  2029. legend.title = element_blank(), # Customize the legend title size
  2030. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  2031. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  2032. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  2033. panel.border = element_blank(), # Ensure no border around the panels
  2034. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  2035. # Create the plot
  2036. p = ggplot(model_1_complete_emmeans_timepoint_df, aes(x = Timepoint, y = emmean, color = Timepoint)) +
  2037. geom_point(size = 6, position = position_dodge(width = 0.5)) + # Add points
  2038. geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = 0.2, size = 2, position = position_dodge(width = 0.5)) + # Error bars
  2039. geom_hline(yintercept = 0, linetype = "dashed", color = "black") + # Reference line
  2040. scale_color_manual(values = c("Pre" = "blue", "Post" = "red")) + # Set colors for points
  2041. labs(title = "Timepoint",
  2042. y = "Estimated Marginal Mean of \nGeneral Accuracy", x = "Timepoint") +
  2043. theme_minimal(base_size = 14) +
  2044. custom_theme +
  2045. theme(legend.position = "none")
  2046. # Use ggsave() to save the plot
  2047. ggsave(filename = paste0("Figures/ACC_EMMeanTimepointDriftRate_Automated.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  2048. print(p)
  2049. ```
  2050. ## Draw Days Effects
  2051. ```{r}
  2052. model = model_1
  2053. preds <- predict_response(model,
  2054. terms = c("Days"),
  2055. interval = "confidence",
  2056. margin = "mean_reference",
  2057. back.transform = FALSE)
  2058. preds = as.data.table(preds)
  2059. preds <- preds %>%
  2060. mutate(
  2061. sem = (conf.high - conf.low) / (2 * 1.96) # approximate SEM from 95% CI
  2062. )
  2063. preds <- preds %>%
  2064. mutate(
  2065. ci.low_sem = predicted - sem,
  2066. ci.high_sem = predicted + sem
  2067. )
  2068. filtered_preds_df = preds
  2069. # Create subject-centered data using Random Intercept subtraction
  2070. # Extract Random Intercepts
  2071. re_intercepts <- as.data.frame(ranef(model_1)$Subj)
  2072. re_intercepts$Subj <- rownames(re_intercepts)
  2073. intercept_col_idx <- grep("Intercept", names(re_intercepts))
  2074. re_intercepts <- re_intercepts[, c(ncol(re_intercepts), intercept_col_idx)]
  2075. names(re_intercepts) <- c("Subj", "RandomIntercept")
  2076. metrics_days_centered <- raw_metrics_full_data_days %>%
  2077. left_join(re_intercepts, by = "Subj") %>%
  2078. mutate(acc_general_centered = acc_general - RandomIntercept)
  2079. # Calculate intercepts and slopes for each subject based on raw data
  2080. # (Since the model only has random intercepts, we use individual OLS regressions
  2081. # to visualize the heterogeneity in slopes that exists in the raw data)
  2082. subj_trends <- metrics_days_centered %>%
  2083. group_by(Subj) %>%
  2084. summarise(
  2085. intercept = coef(lm(acc_general_centered ~ Days))[1],
  2086. slope = coef(lm(acc_general_centered ~ Days))[2]
  2087. ) %>%
  2088. mutate(trend_color = ifelse(slope > 0, "green4", "red3"))
  2089. custom_theme <- theme_minimal(base_size = 20) +
  2090. theme(plot.title = element_text(size = rel(2.8), hjust = 0.5, face = "bold"), # Center the title
  2091. plot.background = element_blank(), # White background for the plot
  2092. panel.background = element_rect(fill = "white"), # White background for the panels
  2093. panel.grid.major = element_blank(), # Remove major grid lines
  2094. panel.grid.minor = element_blank(), # Remove minor grid lines
  2095. axis.line = element_blank(), # Define the axis lines without enclosing
  2096. axis.ticks = element_line(color = "black"), # Define ticks to make them clear
  2097. axis.title.x = element_text(size = rel(2.2), margin = margin(t = 10), lineheight = 1.2), # Add margin on top of x-axis title
  2098. axis.title.y = element_text(size = rel(2.2), margin = margin(r = 10), lineheight = 1.2), # Add margin on the right of y-axis title
  2099. axis.text = element_text(size = rel(1.8)), # Slightly larger axis text
  2100. strip.text = element_text(size = rel(2.2)), # Make facet titles bigger
  2101. legend.position = "right", # Position the legend on the right
  2102. legend.title = element_blank(), # Customize the legend title size
  2103. legend.background = element_rect(color = "black", size = .5), # Add a box around the legend
  2104. legend.text = element_text(size = rel(1.5)), # Customize the legend text size
  2105. legend.spacing.y = unit(0.5, "cm"), # Adjust the spacing between legend items
  2106. panel.border = element_blank(), # Ensure no border around the panels
  2107. text = element_text(family = "Arial")) # Apply Times New Roman (or serif font)
  2108. p = ggplot(filtered_preds_df, aes(x = x, y = predicted)) +
  2109. # Add individual participant trend lines (subject-centered)
  2110. geom_abline(data = subj_trends, aes(intercept = intercept, slope = slope, group = Subj, color = trend_color),
  2111. alpha = 0.2, size = 0.5) +
  2112. scale_color_identity() +
  2113. geom_ribbon(aes(ymin = ci.low_sem, ymax = ci.high_sem), linetype = "dotted", linewidth = 1, fill = "gray", alpha = 0.2) +
  2114. geom_line(size = 1.5) +
  2115. #Add confidence interval
  2116. labs(title = "Days",y = "", x = "Days") +
  2117. custom_theme
  2118. # Use ggsave() to save the plot
  2119. ggsave(filename = paste0("Figures/ACC_DaysLine.png"), plot = p, dpi = 300, width = 12, height = 8, units = "in", bg="white")
  2120. print(p)
  2121. ```
  2122. ## Save Data
  2123. ```{r}
  2124. save(combined_data_all, final_contrasts, subj_contrasts, raw_metrics_full_data_days, file = 'SavedOutputs/Raw_Metrics_Factor_Analysis.RData')
  2125. ```

5_Alternative_Models_Raw.Rmd at commit dcc0852, no license · at the source

Overview

Authors: Brian Kim1, John D. Medaglia1,2
ORCID iDs: Brian Kim
  1. Department of Applied Cognitive and Brain Sciences, Drexel University, Philadelphia, PA, United States
  2. Department of Neurology, University of Pennsylvania, Philadelphia, PA, United States
Institutions: Drexel University (United States); University of Pennsylvania (United States)
Journal: Imaging neuroscience (Cambridge, Mass.), volume 4, article IMAG.a.1298
Dates: received 21 January 2026; accepted 4 June 2026; published online 14 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1162/imag.a.1298 · PMID 42459500 · PMCID PMC13370750 · OpenAlex W7165898388
Open access: diamond, a free copy (OpenAlex)
Status: code verified
Categories: other (modality), human (organism), cognitive (subfield)
Methods: Statistics, fMRI & imaging
Keywords: cognitive control, neuromodulation, TMS, LFPN/MFPN connectivity, DDM, individual differences, latent factor analysis
Topic: Transcranial Magnetic Stimulation Studies (Neurology, Neuroscience), according to OpenAlex
Funding: NIH (1-DP5-OD-021352-01); Starfish Neuroscience
Citations: not cited yet (Europe PMC); 74 references in the paper

Abstract

Transcranial Magnetic Stimulation (TMS) is a promising tool to probe and enhance cognitive control, yet effects are often inconsistent across individuals. These inconsistencies may arise from individual differences in Lateral Frontoparietal (Control) Network (L-FPN) and Medial Frontoparietal (Default) Network (M-FPN) interactions, essential for suppressing internal distraction and facilitating cognitive control. We tested whether baseline connectivity between the L-FPN and M-FPN moderates TMS outcomes in cognitive control tasks. We used the generalized drift rate as a task-general behavioral index of cognitive control, as it overcomes the reliability and interpretability limitations of standard difference measures. Participants completed inhibition (Stroop), working memory (n-back), and flexibility (Navon) tasks before and after intermittent theta-burst stimulation (iTBS) to the L-FPN, dorsal frontoparietal (attention) network (D-FPN), or cranial vertex. Stimulation targets were defined using individualized resting-state parcellations to maximize precision. We found that baseline connectivity moderated stimulation outcomes: individuals with more integrated L-FPN/M-FPN networks benefited most from L-FPN stimulation, whereas those with more segregated networks showed greater improvements from D-FPN stimulation. Notably, stimulation of a single site did not uniformly enhance performance, underscoring the importance of individual network profiles. These findings highlight L-FPN/M-FPN interactions as a basis for individualized TMS interventions and the utility of combining precise network mapping with robust behavioral modeling to optimize neuromodulation interventions.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

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

CogNeW/project_L-FPN_M-FPN_cc_stim

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: dcc0852cf6df646a29ebb848890f5b2d251860a2, 7 July 2026
Languages: R (7)
Size: 1,587 files, 7 scripts
Software Heritage: not archived
Found in: “Data and Code Availability”
Holds: README, 7 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: tidyverse (7 files), ggplot2 (6 files), data.table (4 files), lmerTest (4 files), emmeans (3 files), lavaan (2 files), psych (2 files), patchwork (1 file), reshape2 (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
8 files

The paper's code and data availability statement is in the Data section.

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;
  • 7 scripts, each with its path and the digest of its content;
  • 5 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 and Code Availability

The R Code and raw data files required to reproduce the analyses reported are available at: https://github.com/CogNeW/project_L-FPN_M-FPN_cc_stim

Reproduced under the paper's license (CC BY), from the paper cited above.

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, pages, dates, 2 authors, 7 keywords, 2 funders, 74 references.

Cite

This paper

Kim, B., & Medaglia, J. D. (2026). Frontoparietal control-default mode connectivity predicts TMS effects on cognitive control. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1298. https://doi.org/10.1162/imag.a.1298

BibTeX

@article{kim2026frontoparietal,
author = {Kim, Brian and Medaglia, John D.},
title = {{Frontoparietal control-default mode connectivity predicts TMS effects on cognitive control}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = jul,
volume = {4},
pages = {IMAG.a.1298},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/imag.a.1298},
url = {https://doi.org/10.1162/imag.a.1298},
pmid = {42459500},
pmcid = {PMC13370750}
}

RIS

TY - JOUR
AU - Kim, Brian
AU - Medaglia, John D.
TI - Frontoparietal control-default mode connectivity predicts TMS effects on cognitive control
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/07/14
VL - 4
SP - IMAG.a.1298
SN - 2837-6056
PB - MIT Press
DO - 10.1162/imag.a.1298
UR - https://doi.org/10.1162/imag.a.1298
LA - en
ER -

CSL-JSON

{
"id": "10.1162/imag.a.1298",
"type": "article-journal",
"title": "Frontoparietal control-default mode connectivity predicts TMS effects on cognitive control",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Kim",
"given": "Brian"
},
{
"family": "Medaglia",
"given": "John D."
}
],
"container-title-short": "Imaging Neurosci (Camb)",
"volume": "4",
"page": "IMAG.a.1298",
"DOI": "10.1162/imag.a.1298",
"PMID": "42459500",
"PMCID": "PMC13370750",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://doi.org/10.1162/imag.a.1298",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
14
]
]
}
}

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.1002/jcv2.70135 [code]
Alterations in resting-state functional connectivity relate to psychopathology trajectories during emerging adolescence.
Journal: JCPP advances
In common: psych, emmeans, lmerTest, 3 other tools, 3 references
[2] doi:10.1093/pnasnexus/pgag138 [code]
Regretting a chance to connect: How neural responses to missed social opportunities predict self-disclosure.
Journal: PNAS nexus
In common: lavaan, psych, emmeans, 4 other tools, cognitive, 1 reference
[3] 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: lavaan, psych, emmeans, 5 other tools
[4] doi:10.1371/journal.pbio.3003767 [code]
Ultrasound neuromodulation reveals distinct roles of the dorsal anterior cingulate cortex and anterior insula in learning.
Journal: PLoS biology
In common: psych, emmeans, reshape2, 4 other tools, other, cognitive, 1 reference
[5] doi:10.1371/journal.pone.0353990 [code]
Positive mood enhances accessibility of unrelated concepts in the first language but not in the foreign language.
Journal: PloS one
In common: psych, emmeans, lmerTest, 4 other tools, cognitive, 1 reference
[6] doi:10.1162/imag.a.1303
Composite reaction time and variability correlate with whole-brain white-matter characteristics.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: 7 references
[7] doi:10.1073/pnas.2606871123 [code]
Oxytocin modulates the neurocomputational mechanisms engaged in learning rank relationships in social networks.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: psych, emmeans, lmerTest, 5 other tools
[8] doi:10.1038/s41467-026-73072-6 [code]
Mapping the spatiotemporal continuum of structural connectivity development across the human connectome in youth.
Journal: Nature communications
In common: psych, lmerTest, reshape2, 3 other tools, 2 references
[9] doi:10.1016/j.neuroimage.2026.122115 [code]
Midfrontal theta power relates to response speeding following frustrative nonreward.
Journal: NeuroImage
In common: psych, emmeans, lmerTest, 4 other tools, cognitive
[10] doi:10.1126/sciadv.aec9291 [code]
Computational mechanisms of perception in autism revealed using games inspired by rodent operant tasks.
Journal: Science advances
In common: psych, emmeans, reshape2, 4 other tools, cognitive

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.