OSCR

Structural Brain Correlates of Poor Reading Comprehension.

Code ↔ Paper

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

The 2 matches
  1. [1] § MATERIALS AND METHODS › Group Classification › Cutoff classification model ↔ Code Files/Mahaffy_Masters_Real_DK__KM_20251223 (1).Rmd, lines 458–565 · score 0.66 · MatchIt, standard score, outlier, IQ, PC, age
  2. [2] § RESULTS › White Matter › Regression classification method ↔ Code Files/Mahaffy_Masters_Real_DK__KM_20251223 (1).Rmd, lines 1130–1237 · score 0.53 · left AF, right ILF, quadratic, linear, FA, residuals

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 · 1,237 lines · 68 KB · no license · 2 matches

  1. ---
  2. title: "Mahaffy_MA_Publication"
  3. author: "Kelly_Mahaffy"
  4. date: "`r Sys.Date()`"
  5. output: word_document
  6. ---
  7. # questions from Dan:
  8. # 1) Is it correct that no models are fit to the matched data?
  9. # 2) p-value adjustments: What should be grouped? (I don't have access to the folder with those outputs.)
  10. ```{r setup, include=FALSE}
  11. knitr::opts_chunk$set(echo = TRUE)
  12. library(psych)
  13. library(ggplot2)
  14. library(readxl)
  15. library(memisc)
  16. library(MatchIt)
  17. library(lm.beta)
  18. library(ggplot2)
  19. library(ggforce) # for geom_sina()
  20. library(gghalves)
  21. library(ggpubr)
  22. library(gridExtra) # for grid.arrange()
  23. # added by DK
  24. library(reshape2) # for melt()
  25. library(dplyr) # for rename()
  26. library(viridisLite) # for easier access to viridis color scale
  27. ```
  28. ```{r define dirs, include=FALSE}
  29. #super_dir <- "~/Mahaffy_PoorComprehenders_Pub"
  30. super_dir <- "/Users/dank/Documents/Haskins/Other people's projects/Kelly Mahaffy/MA_paper/Code_2024-12-17/"
  31. #
  32. # super_dir <- validate_dir(super_dir) # in case the final file separator character was omitted
  33. #
  34. data_dir <- paste0(super_dir, "Data_Files/")
  35. pvals_dir <- paste0(data_dir , "P_Values/" )
  36. dirs <- list()
  37. # file structure assumes this script is stored in a separate folder at the same level as Data_Files
  38. dirs[["code" ]] <- paste0(super_dir, "Code_Files/"); setwd(dirs[["code"]]) # set wd here!
  39. dirs[["data" ]] <- paste0(".." , .Platform$file.sep, "Data_Files", .Platform$file.sep)
  40. dirs[["input_brain" ]] <- paste0(dirs[["data"]], "Input_files" , .Platform$file.sep, "Brain_data", .Platform$file.sep)
  41. dirs[["input_pvals" ]] <- paste0(dirs[["data"]], "Input_files" , .Platform$file.sep, "P_values" , .Platform$file.sep)
  42. dirs[["input_other" ]] <- paste0(dirs[["data"]], "Input_files" , .Platform$file.sep, "Other" , .Platform$file.sep)
  43. dirs[["output_data" ]] <- paste0(dirs[["data"]], "Output_files", .Platform$file.sep, "Data" , .Platform$file.sep)
  44. dirs[["output_models"]] <- paste0(dirs[["data"]], "Output_files", .Platform$file.sep, "Models" , .Platform$file.sep)
  45. # create output directories that don't already exist
  46. for(next_output_dir in names(dirs)[grepl("^output_",names(dirs))]) {
  47. dir.create(dirs[[next_output_dir]], recursive=TRUE, showWarnings=FALSE)
  48. }
  49. output_file_prefix <- "Mahaffy_MA_"
  50. ```
  51. ```{r define functions, include=FALSE}
  52. # function which takes a filepath for a .csv, .xls or .xlsx file
  53. # and returns the data in that file
  54. read_data_file <- function(file_path, na="NA") {
  55. file_ext <- tail(strsplit(file_path, "\\.")[[1]], n=1)
  56. if(file_ext %in% c('xls', 'xlsx')) {
  57. file_data <- read_excel(file_path, na=na)
  58. } else if(file_ext %in% 'csv') {
  59. file_data <- read.csv(file_path, na=na)
  60. } else {
  61. error(paste0(file_path, ' file extension must be .csv, .xls, or .xlsx!'))
  62. }
  63. return(file_data)
  64. }
  65. # function which takes a list of data frames (data_list), a data frame to merge with the others (merge_with_others),
  66. # and (optionally) a column by which to join them (uses "id" by default).
  67. # returns a list with all data frames in the list merged with the to-be-merged data frame.
  68. merge_one_with_all <- function(data_list, merge_with_others, join_by="id") {
  69. merged_list <- sapply(data_list, function(next_file_data) {merge(next_file_data, merge_with_others, by=join_by)})
  70. return(merged_list)
  71. }
  72. # function which takes a list of data frames (data_list), the name of a data frame in that list to merge with the others (merge_with_others),
  73. # and (optionally) a column by which to join them (uses "id" by default).
  74. # returns a list with all data frames except the named one merged with the named data frame.
  75. # (builds on merge_one_with_all() by breaking out the to-be-merged data frame from the list, then passing them as separate args.)
  76. merge_one_with_others <- function(data_list, merge_with_others, join_by="id") {
  77. names_to_merge <- setdiff(names(data_list), merge_with_others)
  78. merged_list <- merge_one_with_all(data_list[names_to_merge], data_list[[merge_with_others]], join_by)
  79. return(merged_list)
  80. }
  81. # function which takes a list of data frames (e.g., brain_data[["grey"]]), a column to center (e.g., "cnr"), and the name of an output centered column ("cnr_c").
  82. # returns the list with a centered column in each data frame
  83. center_col_by_matter_type <- function(df_list, col_to_center, centered_col_name) {
  84. for(next_file_data in names(df_list)) {
  85. df_list[[next_file_data]][[centered_col_name]] <- scale(df_list[[next_file_data]][[col_to_center]], scale=F)
  86. }
  87. return(df_list)
  88. }
  89. ##KM attempt at scaling
  90. scale_col_by_matter_type <- function(df_list, col_to_scale, scaled_col_name) {
  91. for(next_file_data in names(df_list)) {
  92. df_list[[next_file_data]][[scaled_col_name]] <- scale(df_list[[next_file_data]][[col_to_scale]])
  93. }
  94. return(df_list)
  95. }
  96. # function that takes a list of data frames, a list of data frame names to write,
  97. # and a directory to write them in, and then writes those data frames as CSVs
  98. # in that dir. by default, output directory is dirs[["output_data"]]
  99. write_df_as_csv <- function(df_list, dfs_to_write, write_dir=dirs[["output_data"]]) {
  100. for(next_df_to_write in dfs_to_write) {
  101. write.csv(df_list[[next_df_to_write]], paste0(write_dir, next_df_to_write, ".csv"))
  102. }
  103. }
  104. # function that takes a data frame as input and a dependent variable (as a character),
  105. # then calls matchit() 2x and creates some summary output (which may not be output
  106. # in a usable way, though this could be easily changed). it returns a data frame
  107. # with the appropriate columns for matching.
  108. custom_matchit_calls <- function(df, dv) {
  109. matchit_formula <- formula(paste0(dv, " ~ age + sex + WISC_NVI_ss"))
  110. summary(m.out.1 <- matchit(matchit_formula, data=df, method=NULL , distance="glm" ))
  111. m.out.2 <- matchit(matchit_formula, data=df, method="optimal", distance="mahalanobis", replace=FALSE)
  112. summary(m.out.2)
  113. plot(m.out.2, type="qq")
  114. return(match.data(m.out.2))
  115. }
  116. # function to combine a PCPD and a PCTD dataset rather than doing this outside R
  117. merge_pcpd_pctd <- function(pcpd, pctd) {
  118. # not an rbind, because a given participant may appear in both, and we only want them to have one row.
  119. # for a given sub who appears in both, all values are identical (though the _Match col is called PC_PD_Match in one and PC_TD_Match in the other)
  120. # rename cols
  121. colnames(pctd)[colnames(pctd)=="PC_TD_Match"] <- "PC_TD"
  122. colnames(pcpd)[colnames(pcpd)=="PC_PD_Match"] <- "PC_PD"
  123. # create a df with one sub per row if they appear in at least one of the two matched data sets
  124. # remove X col, if present -- would need to be treated differently because vals would be different for PCPD and PCTD, and I don't think it's used later anyway
  125. merged_data <- unique.data.frame(rbind(
  126. as.data.frame(pctd[,setdiff(colnames(pctd), c("PC_TD", "X"))]),
  127. as.data.frame(pcpd[,setdiff(colnames(pcpd), c("PC_PD", "X"))])
  128. ))
  129. # merge PC_PD and PC_TD col headers
  130. merged_headers <- merge(pctd[,c("id","PC_TD")], pcpd[,c("id","PC_PD")], all.x=TRUE, all.y=TRUE)
  131. # add PC_All col headers: 1 if both !is.na(PC_TD) and !is.na(PC_PD), and 0 otherwise
  132. merged_headers[["PC_All"]] <- 1-apply(sapply(merged_headers[,c("PC_TD","PC_PD")], is.na), 1, sum)
  133. # merge headers and data
  134. merged_data <- merge(merged_headers, merged_data)
  135. # sort: PC_All (descending), PC_TD (descending), PC_PD (descending), id (ascending)
  136. merged_data <- merged_data[with(merged_data, order(-PC_All, -PC_TD, -PC_PD, id)),]
  137. # to do (maybe): add X col
  138. # NOTE: There is an error in the way this is handled in the previous code --
  139. # X is nested within matched dataset, so a given participant should have
  140. # either 1 or 2 values (potentially one each for PC_PD and PC_TD).
  141. # However, X is never used again, so it doesn't really matter. Safe to
  142. # just omit it for now.
  143. return(merged_data)
  144. }
  145. # function which takes various pieces of information about a set of lm() models to fit,
  146. # then writes the appropriate files with results and p-values, and returns
  147. # an object containing the fitted models and p-values
  148. # df = the data frame to be used in all models
  149. # dvs = a character vector of DVs (1 per model)
  150. # pred_pval = the string name of the variable for which to save the p-value
  151. # preds_other = a character vector of other predictors (not pred_pval) to include in the model
  152. # id_text = an identifying string to be prepended to the names of files that are written
  153. # df = get(next_model_df)[[paste0(output_file_prefix, next_model_info[["df"]])]]
  154. # dvs = dvs[[models_to_fit[next_model_idx,"dvs"]]]
  155. # pred_pval = next_model_info[["pred_pval"]]
  156. # preds_other = strsplit(next_model_info[["preds_other"]], " \\+ ")[[1]]
  157. # id_text = next_model_info[["id_text"]]
  158. fit_lms <- function(df, dvs, pred_pval, preds_other, id_text) {
  159. all_preds <- setdiff(c(pred_pval, preds_other), "") # exclude empty preds
  160. # convert data from wide to long
  161. df.long <- melt(df[,c(dvs,all_preds)], id.vars=all_preds, variable.name="region", value.name="dv")
  162. # construct formula -- will be the same for all models
  163. lm_formula_baseline <- formula(paste0("dv ~ ", paste(setdiff(preds_other, ""), collapse=" + ")))
  164. lm_formula <- formula(paste0("dv ~ ", paste( all_preds , collapse=" + ")))
  165. # fit all models
  166. all_baseline_lms <- lapply(dvs, function(x) {next_lm <- lm.beta(lm(lm_formula_baseline, df.long[df.long[["region"]]==x,]))})
  167. names(all_baseline_lms) <- dvs
  168. all_lms <- lapply(dvs, function(x) {next_lm <- lm.beta(lm(lm_formula, df.long[df.long[["region"]]==x,]))})
  169. names(all_lms) <- dvs
  170. # get r^2 vals
  171. rsq_vals <- do.call(rbind.data.frame, lapply(dvs, function(x) {
  172. rsq <- c(summary(all_lms[[x]])[["r.squared"]], summary(all_baseline_lms[[x]])[["r.squared"]]);
  173. rsq <- c(rsq, rsq[1]-rsq[2])}))
  174. rsq.headers <- c("rsq.linear", "rsq.intercept", "rsq.linear.partial")
  175. colnames(rsq_vals) <- rsq.headers
  176. #names(all_lms) <- paste0("lm", 1:length(all_lms)) # necessary for mtable() call to work
  177. # generate and save mtable
  178. # note: only works if the models are not standardized (due to extra column in model summary)
  179. # lm_mtable <- do.call(mtable, all_lms)
  180. # write.mtable(lm_mtable, file=paste0(dirs[["output_models"]], output_file_prefix, id_text, "_Results.csv"), format=c("delim"))
  181. # make our own output
  182. all_lms_for_output <- do.call(rbind.data.frame, lapply(1:length(all_lms), function(x) {data.frame(model_num=x, dv=dvs[[x]], effect=rownames(coef(summary(all_lms[[x]]))), coef(summary(all_lms[[x]])), rsq_vals[x,])}))
  183. rownames(all_lms_for_output) <- NULL
  184. colnames(all_lms_for_output) <- c("model_num","dv","effect","Estimate","Standardized","SE","t","p",rsq.headers)
  185. write.table(all_lms_for_output, file=paste0(dirs[["output_models"]], output_file_prefix, id_text, "_Results.csv"), sep=",")
  186. # extract and save p-values from specified predictor
  187. lm_pvals <- sapply(1:length(all_lms), function(next_lm_idx) {coef(summary(all_lms[[next_lm_idx]]))[pred_pval,"Pr(>|t|)"]})
  188. write.csv(lm_pvals, paste0(dirs[["output_models"]], output_file_prefix, id_text, "_PValue.csv"))
  189. objects_to_return <- list(model=all_lms, rsq.linear=data.frame(dv=dvs, rsq_vals), p=lm_pvals)
  190. # quad formula if needed
  191. unique_pred_vals <- unique(df.long[[pred_pval]])
  192. unique_pred_vals <- unique_pred_vals[!is.na(unique_pred_vals)] # exclude NAs
  193. pred_is_continuous <- length(unique_pred_vals) > 2
  194. if(pred_is_continuous) {
  195. pred_linear_and_quad <- as.data.frame(poly(df.long[[pred_pval]], 2))
  196. colnames(pred_linear_and_quad) <- paste0(pred_pval, c(".lin", ".quad"))
  197. df.long <- data.frame(df.long, pred_linear_and_quad)
  198. # construct quadratic formula
  199. lm_formula.quad <- formula(paste0("dv ~ ", paste(c(all_preds, paste0(pred_pval, ".quad")), collapse=" + ")))
  200. all_lms.quad <- lapply(dvs, function(x) {next_lm <- lm(lm_formula.quad, df.long[df.long[["region"]]==x,])})
  201. names(all_lms.quad) <- dvs
  202. rsq_vals.quad <- do.call(rbind.data.frame, lapply(1:length(all_lms), function(x) {
  203. rsq <- summary(all_lms.quad[[x]])[["r.squared"]]
  204. rsq <- c(rsq, rsq - rsq_vals[x,"rsq.linear"])
  205. }))
  206. rsq.quad.headers <- c("rsq.quad", "rsq.quad.partial")
  207. colnames(rsq_vals.quad) <- rsq.quad.headers
  208. # compare p-values for each one
  209. all_quad_stats <- do.call(rbind.data.frame, lapply(1:length(all_lms), function(x) {
  210. next_quad_comparison <- anova(all_lms[[x]], all_lms.quad[[x]])
  211. next_quad_comparison.stats <- data.frame(with(next_quad_comparison, data.frame(F=F, df=Df, p=`Pr(>F)`)[2,]), rsq_vals.quad[x,])
  212. return(next_quad_comparison.stats)
  213. }))
  214. objects_to_return[["quad_models" ]] <- all_lms.quad
  215. objects_to_return[["quad_results"]] <- data.frame(dv=dvs, all_quad_stats)
  216. }
  217. return(objects_to_return)
  218. }
  219. # function which takes a data frame containing brain data (df) and a data frame
  220. # (var_info) containing two cols: "match" and "store". for each row in var_info,
  221. # it averages across all values in columns named "slf[digit(s)]_x" (where x is
  222. # the value of "match") and stores the results in "slfavg_y" (where y is the
  223. # value of "store").
  224. create_slf_avg_vars <- function(df, var_info) {
  225. # average over appropriately named cols
  226. new_cols <- sapply(var_info[["match"]], function(next_string) {
  227. rowMeans(df[,grepl(paste0("^slf\\d+_", next_string, "$"), colnames(df))], na.rm=TRUE)
  228. })
  229. # add new cols to df and return
  230. df[,paste0("slfavg_", var_info[["store"]])] <- new_cols
  231. return(df)
  232. }
  233. create_slf_avg_vars_fake <- function(df, var_info) { # for printing only
  234. # average over appropriately named cols
  235. summary_strings <- sapply(1:nrow(var_info), function(next_row_idx) {
  236. next_match_string <- var_info[next_row_idx,"match"]
  237. cols_to_combine <- colnames(df)[grepl(paste0("^slf\\d+_", next_match_string, "$"), colnames(df))]
  238. next_store_string <- paste0("slfavg_", var_info[next_row_idx,"store"], " = mean(", paste(cols_to_combine, collapse=", "), ")")
  239. return(next_store_string)
  240. })
  241. return(summary_strings)
  242. }
  243. get_model_resids <- function(fitted_lm, pred_var) {
  244. outputs <- list()
  245. outputs[["fitted_lm.formula_split"]] <- as.character(formula(fitted_lm))[c(2,3)]
  246. outputs[["fitted_lm.dv"]] <- outputs[["fitted_lm.formula_split"]][1]
  247. outputs[["fitted_lm.preds"]] <- strsplit(outputs[["fitted_lm.formula_split"]][2], ' \\+ ')[[1]]
  248. outputs[["subset_lm.preds"]] <- setdiff(outputs[["fitted_lm.preds"]], pred_var)
  249. outputs[["subset_lm.formula"]] <- formula(paste0(outputs[["fitted_lm.dv"]], " ~ ", paste(outputs[["subset_lm.preds"]], collapse=" + ")))
  250. outputs[["subset_lm"]] <- lm(outputs[["subset_lm.formula"]], data=fitted_lm[["model"]])
  251. outputs[["subset_lm.resids"]] <- resid(outputs[["subset_lm"]])
  252. outputs[["pred_lm.formula"]] <- formula(paste0(pred_var, " ~ ", paste(outputs[["subset_lm.preds"]], collapse=" + ")))
  253. outputs[["pred_lm"]] <- lm(outputs[["pred_lm.formula"]], data=fitted_lm[["model"]])
  254. outputs[["pred.resids"]] <- resid(outputs[["pred_lm"]])
  255. return(outputs)
  256. }
  257. plot_grouped_vs_continuous <- function(fitted_lm, pred_var, num_quantiles=5, highlighted_quantiles=c(1,3,5), degree=c(1), linetype=NULL) {
  258. resid_models <- get_model_resids(fitted_lm, pred_var)
  259. # get plot information, colors, quantiles, etc.
  260. plot_data <- fitted_lm[["model"]]
  261. plot.y.label <- paste0(resid_models[["fitted_lm.dv"]], ".resid")
  262. plot_data[[plot.y.label]] <- resid_models[["subset_lm.resids"]]
  263. plot.x.label <- paste0(pred_var, ".resid")
  264. plot_data[[plot.x.label]] <- resid_models[["pred.resids"]]
  265. plot.quantiles <- quantile(plot_data[[plot.x.label]], probs=(1:(num_quantiles-1))/num_quantiles)
  266. plot_data[["quantile"]] <- as.factor(as.character(sapply(plot_data[[plot.x.label]], function(x) {1 + sum(x > plot.quantiles)})))
  267. # compute quantile boundaries as mean of mins/maxes of each quantile
  268. quantile_boundaries <- lapply(c(min,max), function(x) {aggregate(formula(paste0(plot.x.label, " ~ quantile")), data=plot_data, FUN=x)})
  269. quantile_boundaries <- data.frame(max=quantile_boundaries[[2]][1:(num_quantiles-1),plot.x.label], min=quantile_boundaries[[1]][2:num_quantiles,plot.x.label])
  270. quantile_boundaries[["mean"]] <- apply(quantile_boundaries, 1, mean)
  271. # create color map: grey for non-highlighted quantiles, viridis for highlighted quantiles
  272. plot_colors <- gray.colors(num_quantiles)
  273. plot_colors[highlighted_quantiles] <- viridis(num_quantiles)[highlighted_quantiles]
  274. # get means by quantile (there is SO a better way to do this...)
  275. summary_stats <- do.call(rbind.data.frame, lapply(c(plot.x.label, plot.y.label), function(y) {
  276. next_summary_stats <- do.call(cbind, lapply(c(mean,sd,length), function(x) {aggregate(formula(paste0(y, " ~ quantile")), data=plot_data, FUN=x)[,2]}))
  277. colnames(next_summary_stats) <- c("mean","sd","n")
  278. #next_summary_stats <- data.frame(var=rep(c("x","y"),each=num_quantiles), next_summary_stats)
  279. next_summary_stats <- data.frame(quantile=1:num_quantiles, next_summary_stats)
  280. next_summary_stats
  281. }))
  282. summary_stats <- data.frame(var=rep(c("x","y"), each=num_quantiles), summary_stats)
  283. summary_stats[["se" ]] <- with(summary_stats, sd/sqrt(n))
  284. summary_stats[["start"]] <- with(summary_stats, mean-se)
  285. summary_stats[["end" ]] <- with(summary_stats, mean+se)
  286. # oy
  287. summary_stats <- do.call(merge, lapply(c("x","y"), function(x) {
  288. subset_summary_stats <- summary_stats[summary_stats[["var"]]==x,]
  289. subset_col_names <- setdiff(colnames(subset_summary_stats), c("var","quantile"))
  290. colnames(subset_summary_stats)[colnames(subset_summary_stats) %in% subset_col_names] <- paste0(subset_col_names, ".", x)
  291. subset(subset_summary_stats, select=-var)
  292. }))
  293. # set figure x-limits at [range] + 10% on each end
  294. x.range <- range(plot_data[[plot.x.label]])
  295. x.range <- x.range + (diff(x.range) * .10) * c(-1,1)
  296. # make plot
  297. resid_plot <- ggplot(plot_data, aes_string(x=plot.x.label, y=plot.y.label, color="quantile")) +
  298. geom_point() +
  299. scale_color_manual(values=plot_colors) +
  300. theme_bw() +
  301. scale_x_continuous(limits=x.range)
  302. for(next_degree_idx in 1:length(degree)) {
  303. next_degree <- degree[next_degree_idx]
  304. if(next_degree > 1) {
  305. stat_smooth.formula <- formula(paste0("y ~ x + ", paste(paste0("I(x^", 2:next_degree, ")"), collapse=" + ")))
  306. } else {
  307. stat_smooth.formula <- formula("y ~ x")
  308. }
  309. if(is.null(linetype) || length(linetype) != length(degree)) {
  310. next_linetype <- next_degree
  311. } else {
  312. next_linetype <- linetype[next_degree_idx]
  313. }
  314. resid_plot <- resid_plot + stat_smooth(aes(group=1), color="black", linetype=next_linetype, method="lm", formula=stat_smooth.formula)
  315. }
  316. # add quantile boundaries
  317. for(next_quantile_boundary_idx in 1:nrow(quantile_boundaries)) {
  318. resid_plot <- resid_plot + geom_vline(xintercept=quantile_boundaries[next_quantile_boundary_idx,"mean"], color="black", linetype="dashed", alpha=0.2)
  319. }
  320. # just add plus signs rather than bars with +/- 1 SEM
  321. resid_plot <- resid_plot +
  322. geom_point(data=summary_stats, aes(x=mean.x, y=mean.y), color="black", size=5, shape=3)
  323. # for(next_quantile_mean_idx in (1:num_quantiles)) {
  324. # resid_plot <- resid_plot +
  325. # geom_segment(color=plot_colors[next_quantile_mean_idx], linewidth=1.5, # shows variability in X
  326. # x =summary_stats[next_quantile_mean_idx,"start.x"],
  327. # xend=summary_stats[next_quantile_mean_idx,"end.x" ],
  328. # y =summary_stats[next_quantile_mean_idx,"mean.y" ],
  329. # yend=summary_stats[next_quantile_mean_idx,"mean.y" ]) +
  330. # geom_segment(color=plot_colors[next_quantile_mean_idx], linewidth=1.5, # shows variability in Y
  331. # x =summary_stats[next_quantile_mean_idx,"mean.x" ],
  332. # xend=summary_stats[next_quantile_mean_idx,"mean.x" ],
  333. # y =summary_stats[next_quantile_mean_idx,"start.y"],
  334. # yend=summary_stats[next_quantile_mean_idx,"end.y" ])
  335. # }
  336. return(resid_plot)
  337. }
  338. ```
  339. ## Describe
  340. ```{r}
  341. Mahaffy_MA_Final <- read_data_file(paste0(dirs[["input_other"]], "Mahaffy_Masters_Final.xlsx"),na = "NA")
  342. #objects(Mahaffy_MA_Final)
  343. Mahaffy_MA_Final$age_round<-round(Mahaffy_MA_Final$age,1)
  344. describe(Mahaffy_MA_Final$age_round)
  345. sum(Mahaffy_MA_Final$sex)
  346. describe(Mahaffy_MA_Final$wiat_rc_stand)
  347. describe(Mahaffy_MA_Final$wiat_rc_raw)
  348. describe(Mahaffy_MA_Final$wiat_lcrv_ss)
  349. describe(Mahaffy_MA_Final$wiat_lcrv_raw)
  350. describe(Mahaffy_MA_Final$towre_pde_ss)
  351. describe(Mahaffy_MA_Final$towre_pde_raw)
  352. describe(Mahaffy_MA_Final$WISC_NVI_ss)
  353. ```
  354. ## Regression Model
  355. ```{r}
  356. BehavioralRegression<-lm(wiat_rc_raw ~ wiat_lcrv_raw + towre_pde_raw + age_round + sex + WISC_NVI_raw, data=Mahaffy_MA_Final)
  357. summary(BehavioralRegression)
  358. BehavioralRegression_Interactions<-lm(wiat_rc_raw ~ (wiat_lcrv_raw + towre_pde_raw + age_round + sex + WISC_NVI_raw)^2 , data=Mahaffy_MA_Final)
  359. summary(BehavioralRegression_Interactions)
  360. anova(BehavioralRegression, BehavioralRegression_Interactions)
  361. Mahaffy_MA_Final$Residuals_BehavioralRegression<-resid(BehavioralRegression_Interactions)
  362. plot(Mahaffy_MA_Final$Residuals_BehavioralRegression)
  363. hist(Mahaffy_MA_Final$Residuals_BehavioralRegression)
  364. ```
  365. ##Preparing Data
  366. ```{r}
  367. Mahaffy_MA_Final$age_c<-scale(Mahaffy_MA_Final$age_round, scale=FALSE)
  368. Mahaffy_MA_Final$word_c<-scale(Mahaffy_MA_Final$towre_pde_raw, scale=FALSE)
  369. Mahaffy_MA_Final$vocab_c<-scale(Mahaffy_MA_Final$wiat_lcrv_raw, scale=FALSE)
  370. Mahaffy_MA_Final$iq_c<-scale(Mahaffy_MA_Final$WISC_NVI_raw, scale=FALSE)
  371. #Import Brain Data
  372. brain_data <- list()
  373. #Grey Matter
  374. # grey matter files
  375. files.grey <- c("FS7_compiled_lh_area_2009", "FS7_compiled_lh_thickness_2009", "FS7_compiled_rh_area_2009", "FS7_compiled_rh_thicnkess_2009", "FS7_Compiled_Vol_Seg", "T1w_QC_Full")
  376. # read in files to list
  377. brain_data[["grey"]] <- lapply(files.grey, function(next_file) {read_data_file(paste0(dirs[["input_brain"]], next_file, ".xlsx"), na = "NA")})
  378. names(brain_data[["grey"]]) <- files.grey
  379. #White Matter
  380. # white matter files
  381. files.white <- c("WM_FA_MD_Tract_All", "WM_NODDI_All", "WM_QC_All")
  382. # read in files to list
  383. brain_data[["white"]] <- lapply(files.white, function(next_file) {read_data_file(paste0(dirs[["input_brain"]], next_file, ".xlsx"), na = "0")})
  384. names(brain_data[["white"]]) <- files.white
  385. #Combining Data
  386. brain_data[["grey" ]] <- merge_one_with_others(brain_data[["grey" ]], "T1w_QC_Full")
  387. brain_data[["white"]] <- merge_one_with_others(brain_data[["white"]], "WM_QC_All" )
  388. # rename dfs
  389. df_names <- c(
  390. LH_Area = "FS7_compiled_lh_area_2009",
  391. LH_Thickness = "FS7_compiled_lh_thickness_2009",
  392. RH_Area = "FS7_compiled_rh_area_2009",
  393. RH_Thickness = "FS7_compiled_rh_thicnkess_2009",
  394. Vol = "FS7_Compiled_Vol_Seg",
  395. Noddi = "WM_NODDI_All",
  396. MDFA = "WM_FA_MD_Tract_All"
  397. )
  398. names(brain_data[["grey" ]]) <- paste0(output_file_prefix, names(df_names)[match(names(brain_data[["grey" ]]), unname(df_names))])
  399. names(brain_data[["white"]]) <- paste0(output_file_prefix, names(df_names)[match(names(brain_data[["white"]]), unname(df_names))])
  400. #Centering Data
  401. # center CNR
  402. brain_data[["grey" ]] <- center_col_by_matter_type(brain_data[["grey" ]], "cnr" , "cnr_c")
  403. brain_data[["white"]] <- center_col_by_matter_type(brain_data[["white"]], "Avg _CNR", "cnr_c")
  404. #Scaling Data
  405. brain_data[["grey" ]] <- scale_col_by_matter_type(brain_data[["grey" ]], "cnr" , "cnr_c")
  406. brain_data[["white"]] <- scale_col_by_matter_type(brain_data[["white"]], "Avg _CNR", "cnr_c")
  407. #Combining Brain and Behavioral Data
  408. brain_data[["grey" ]] <- merge_one_with_all(brain_data[["grey" ]], Mahaffy_MA_Final)
  409. brain_data[["white"]] <- merge_one_with_all(brain_data[["white"]], Mahaffy_MA_Final)
  410. #Making spreadsheets for matching
  411. #Only using one GM metric here because I know the participants are the same across all GM data
  412. brain_data[["grey"]][["Mahaffy_GM_PC" ]] <- subset(brain_data[["grey"]][[paste0(output_file_prefix, "LH_Area")]], wiat_rc_stand <= 90 & towre_pde_ss >= 100)
  413. brain_data[["grey"]][["Mahaffy_GM_Possible_PD"]] <- subset(brain_data[["grey"]][[paste0(output_file_prefix, "LH_Area")]], wiat_rc_stand >= 100 & towre_pde_ss <= 90 & wiat_lcrv_ss >= 100)
  414. brain_data[["grey"]][["Mahaffy_GM_Possible_TD"]] <- subset(brain_data[["grey"]][[paste0(output_file_prefix, "LH_Area")]], wiat_rc_stand >= 100 & towre_pde_ss >= 100 & wiat_lcrv_ss >= 100)
  415. write_df_as_csv(brain_data[["grey"]], c("Mahaffy_GM_PC", "Mahaffy_GM_Possible_PD", "Mahaffy_GM_Possible_TD"))
  416. #Outside of R, I checked for and removed duplicate values, combined all there of the above into a single spreadsheet, and dummy coded both PC_PD and PC_TD variables, with poor comprehenders always being 1. Spreadsheet re-uploaded as Mahaffy_MatchGroups. We also identified one PC who was an outlier (a RC standard score more that 10 points from the next lowest SS) and created a second spreadsheet for group matching without this outlier. Spreadsheets were split into a PC_PD and PC_TD spreadsheet (with all poor comprehenders and possible PDs and TDs, respectively, for compliance with MatchIt! package used below.)
  417. # matched data files
  418. match_pool_files.grey <- c("Mahaffy_MatchGroups_PCPD", "Mahaffy_MatchGroups_PCTD", "Mahaffy_MatchGroups_NoOut_PCPD", "Mahaffy_MatchGroups_NoOut_PCTD")
  419. # read in files to list
  420. match_pool_data <- list()
  421. match_pool_data[["grey"]] <- lapply(match_pool_files.grey, function(next_file) {read_data_file(paste0(dirs[["input_other"]], next_file, ".xlsx"), na = "NA")})
  422. names(match_pool_data[["grey"]]) <- match_pool_files.grey
  423. #Repeating the Process for White Matter
  424. #Here, doing this process once for WM metrics from FSL and once for NODDI because the participants are not all the same
  425. brain_data[["white"]][["Mahaffy_WM_PC" ]] <- subset(brain_data[["white"]][[paste0(output_file_prefix, "MDFA" )]], wiat_rc_stand <= 90 & towre_pde_ss >= 100)
  426. brain_data[["white"]][["Mahaffy_WM_Possible_PD" ]] <- subset(brain_data[["white"]][[paste0(output_file_prefix, "MDFA" )]], wiat_rc_stand >= 100 & towre_pde_ss <= 90 & wiat_lcrv_ss >= 100)
  427. brain_data[["white"]][["Mahaffy_WM_Possible_TD" ]] <- subset(brain_data[["white"]][[paste0(output_file_prefix, "MDFA" )]], wiat_rc_stand >= 100 & towre_pde_ss >= 100 & wiat_lcrv_ss >= 100)
  428. brain_data[["white"]][["Mahaffy_Noddi_PC" ]] <- subset(brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]], wiat_rc_stand <= 90 & towre_pde_ss >= 100)
  429. brain_data[["white"]][["Mahaffy_Noddi_Possible_PD"]] <- subset(brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]], wiat_rc_stand >= 100 & towre_pde_ss <= 90 & wiat_lcrv_ss >= 100)
  430. brain_data[["white"]][["Mahaffy_Noddi_Possible_TD"]] <- subset(brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]], wiat_rc_stand >= 100 & towre_pde_ss >= 100 & wiat_lcrv_ss >= 100)
  431. write_df_as_csv(brain_data[["white"]], c("Mahaffy_WM_PC" , "Mahaffy_WM_Possible_PD" , "Mahaffy_WM_Possible_TD" ,
  432. "Mahaffy_Noddi_PC", "Mahaffy_Noddi_Possible_PD", "Mahaffy_Noddi_Possible_TD"))
  433. match_pool_files.white <- c("Mahaffy_WM_MatchGroups_PCPD", "Mahaffy_WM_MatchGroups_PCTD", "Mahaffy_Noddi_MatchGroups_PCPD", "Mahaffy_Noddi_MatchGroups_PCTD")
  434. match_pool_data[["white"]] <- lapply(match_pool_files.white, function(next_file) {read_data_file(paste0(dirs[["input_other"]], next_file, ".csv"), na = "NA")})
  435. names(match_pool_data[["white"]]) <- match_pool_files.white
  436. ```
  437. ##Matching For Cutoff
  438. ```{r}
  439. # create a data frame with all parameters needed, then call custom_matchit_calls() via an apply function to match everything at once
  440. data_to_match <- data.frame(rbind(
  441. c("grey" , "Mahaffy_MatchGroups_PCPD" , "PC_PD_Match", "Matched_PCPD_GM" )
  442. , c("grey" , "Mahaffy_MatchGroups_PCTD" , "PC_TD_Match", "Matched_PCTD_GM" )
  443. , c("grey" , "Mahaffy_MatchGroups_NoOut_PCPD", "PC_PD_Match", "Matched_PCPD_NoOut")
  444. , c("grey" , "Mahaffy_MatchGroups_NoOut_PCTD", "PC_TD_Match", "Matched_PCTD_NoOut")
  445. , c("white", "Mahaffy_WM_MatchGroups_PCPD" , "PC_PD_Match", "Matched_PCPD_WM" )
  446. , c("white", "Mahaffy_WM_MatchGroups_PCTD" , "PC_TD_Match", "Matched_PCTD_WM" )
  447. , c("white", "Mahaffy_Noddi_MatchGroups_PCPD", "PC_PD_Match", "Matched_PCPD_Noddi")
  448. , c("white", "Mahaffy_Noddi_MatchGroups_PCTD", "PC_TD_Match", "Matched_PCTD_Noddi")
  449. ))
  450. names(data_to_match) <- c("matter_type", "df_name", "dv", "id_text")
  451. # run all models in data_to_match
  452. matched_group_data <- lapply(1:nrow(data_to_match), function(next_data_to_match_idx) {
  453. next_match_info <- data_to_match[next_data_to_match_idx,]
  454. next_matched_data <- custom_matchit_calls(match_pool_data[[next_match_info[["matter_type"]]]][[next_match_info[["df_name"]]]], next_match_info[["dv"]])
  455. return(next_matched_data)
  456. })
  457. names(matched_group_data) <- paste0(output_file_prefix, data_to_match[["id_text"]])
  458. # write CSVs
  459. write_df_as_csv(matched_group_data, data_to_match[["id_text"]])
  460. #Outside of R, I combined each of these into a single spreadsheet with all contrasts of interest, which resulted in three spreadsheets- one for GM, one for WM, and one for Noddi
  461. ## we can do that in R!
  462. # define the four file groupings
  463. matched_group_data.groupings <- c("GM","WM","Noddi","NoOut")
  464. # for each grouping, combine the two dfs
  465. matched_group_data.merged <- lapply(matched_group_data.groupings, function(next_data) {
  466. # algorithmically construct the names of the fields to pull
  467. ordered_pcpd_pctd <- c("PCPD","PCTD")
  468. matched_data_names <- paste0(output_file_prefix, "Matched_", ordered_pcpd_pctd, "_", next_data)
  469. names(matched_data_names) <- ordered_pcpd_pctd
  470. # run custom function which combines the data
  471. merged_data <- merge_pcpd_pctd(pcpd=matched_group_data[[matched_data_names["PCPD"]]], pctd=matched_group_data[[matched_data_names["PCTD"]]])
  472. return(merged_data)
  473. })
  474. # name the fields in the resulting list
  475. names(matched_group_data.merged) <- paste0(output_file_prefix, "Matched_", matched_group_data.groupings)
  476. #Writing a CSV to prepare groups for mixed regression (save the lowest, middle, and highest 20% of residual scores with the lowest group being dummy coded as 1 and all others as 0) and create a list to merge with other spreadsheets like for previous. Done separately for GM, WM, and Noddi.
  477. write_df_as_csv(c(brain_data[["grey"]], brain_data[["white"]]), paste0(output_file_prefix, c("LH_Area", "MDFA", "Noddi"))) # mix of GM & WM
  478. # reading these in from the output directory (?) because they were just created
  479. brain_data[["grey" ]][[paste0(output_file_prefix, "LH_Area")]] <- read_data_file(paste0(dirs[["output_data"]], output_file_prefix, "LH_Area.csv"), na = "NA")
  480. brain_data[["white"]][[paste0(output_file_prefix, "MDFA" )]] <- read_data_file(paste0(dirs[["output_data"]], output_file_prefix, "MDFA.csv" ), na = "NA")
  481. brain_data[["white"]][[paste0(output_file_prefix, "Noddi" )]] <- read_data_file(paste0(dirs[["output_data"]], output_file_prefix, "Noddi.csv" ), na = "NA")
  482. #Reading in lists to merge so that group contrasts can be merged with other data files
  483. merged_data_files <- data.frame(rbind( # keep all info about each file together
  484. c("GM_Matched_ToMerge.xlsx" , "GM_Cutoff_Matched_Merge", "_Cutoff" , 1)
  485. , c("Mixed_Merge.csv" , "Mixed_Merge" , "" , 2)
  486. , c("Matched_NoOut_ToMerge.csv", "Matched_NoOut_Merge" , "_Cutoff_NoOut", 1)
  487. ))
  488. names(merged_data_files) <- c("filename", "field_name", "suffix", "merge_order")
  489. merged_data_files[["merge_order"]] <- as.numeric(merged_data_files[["merge_order"]])
  490. data_for_merging <- lapply(merged_data_files[["filename"]],
  491. function(next_filename) {read_data_file(paste0(dirs[["input_other"]], output_file_prefix, next_filename), na="NA")}
  492. )
  493. names(data_for_merging) <- paste0(output_file_prefix, merged_data_files[["field_name"]])
  494. ## comprehensively merge brain data files with matching files
  495. data_for_analysis <- list()
  496. for(df_1_name in paste0(output_file_prefix, c("LH_Thickness","RH_Thickness","LH_Area","RH_Area","Vol"))) {
  497. df_1 <- brain_data[["grey"]][[df_1_name]]
  498. for(df_2_idx in 1:nrow(merged_data_files)) {
  499. df_2 <- data_for_merging[[paste0(output_file_prefix, merged_data_files[df_2_idx,"field_name"])]]
  500. next_merge_order <- merged_data_files[df_2_idx,"merge_order"]
  501. if(next_merge_order==1) {
  502. next_merged_df <- merge(df_2, df_1, by="id")
  503. } else if(next_merge_order==2) {
  504. next_merged_df <- merge(df_1, df_2, by="id")
  505. } else {
  506. next_merged_df <- NULL
  507. }
  508. data_for_analysis[[paste0(df_1_name, merged_data_files[df_2_idx,"suffix"])]] <- next_merged_df
  509. }
  510. }
  511. #WM
  512. #Creating SLF Avg variables
  513. # store info about col names to match ("match" col) and the name of the column
  514. # in which averages should be stored ("stored")
  515. slf_avg_var_ids <- list()
  516. slf_avg_var_ids[["wm"]] <- data.frame(rbind(
  517. c("l_FA" , "l_FA" )
  518. , c("l_MD" , "l_MD" )
  519. , c("r_FA" , "r_FA" )
  520. , c("r_MD" , "r_MD" )
  521. ))
  522. slf_avg_var_ids[["noddi"]] <- data.frame(rbind(
  523. c("l_Neurite_density", "l_Neurite_density" )
  524. , c("l_ODI" , "l_ODI")
  525. , c("r_Neurite_density", "r_Neurite_density" )
  526. , c("r_ODI" , "r_ODI")
  527. ))
  528. for(next_df_name in names(slf_avg_var_ids)) {
  529. colnames(slf_avg_var_ids[[next_df_name]]) <- c("match","store")
  530. }
  531. ##MD and FA
  532. brain_data[["white"]][[paste0(output_file_prefix, "MDFA")]] <- create_slf_avg_vars(brain_data[["white"]][[paste0(output_file_prefix, "MDFA")]] , slf_avg_var_ids[["wm"]] )
  533. matched_group_data.merged[[paste0(output_file_prefix, "Matched_WM")]] <- create_slf_avg_vars(matched_group_data.merged[[paste0(output_file_prefix, "Matched_WM")]] , slf_avg_var_ids[["wm"]] )
  534. ##ND and ODI
  535. brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]] <- create_slf_avg_vars(brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]] , slf_avg_var_ids[["noddi"]])
  536. matched_group_data.merged[[paste0(output_file_prefix, "Matched_Noddi")]] <- create_slf_avg_vars(matched_group_data.merged[[paste0(output_file_prefix, "Matched_Noddi")]], slf_avg_var_ids[["noddi"]])
  537. ## FOR DEBUGGING PURPOSES:
  538. ## just printing out the col names being averaged here:
  539. slf_summary_str <- c()
  540. slf_summary_str <- c(slf_summary_str, paste0(paste(c("brain_data", "white", paste0(output_file_prefix, "MDFA")), collapse="$"), "$",
  541. create_slf_avg_vars_fake(brain_data[["white"]][[paste0(output_file_prefix, "MDFA")]], slf_avg_var_ids[["wm"]])))
  542. slf_summary_str <- c(slf_summary_str, paste0(paste(c("matched_group_data.merged", paste0(output_file_prefix, "Matched_WM")), collapse="$"), "$",
  543. create_slf_avg_vars_fake(matched_group_data.merged[[paste0(output_file_prefix, "Matched_WM")]], slf_avg_var_ids[["wm"]])))
  544. slf_summary_str <- c(slf_summary_str, paste0(paste(c("brain_data", "white", paste0(output_file_prefix, "Noddi")), collapse="$"), "$",
  545. create_slf_avg_vars_fake(brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]], slf_avg_var_ids[["noddi"]])))
  546. slf_summary_str <- c(slf_summary_str, paste0(paste(c("matched_group_data.merged", paste0(output_file_prefix, "Matched_Noddi")), collapse="$"), "$",
  547. create_slf_avg_vars_fake(matched_group_data.merged[[paste0(output_file_prefix, "Matched_Noddi")]], slf_avg_var_ids[["noddi"]])))
  548. slf_summary_str <- do.call(rbind.data.frame, strsplit(slf_summary_str, "="))
  549. colnames(slf_summary_str) <- c("store","avg")
  550. # slf_summary_str[c(2,4,1,3,9,11,10,12),] # order of rowMeans averaging
  551. # slf_summary_str[c(1,3,2,4,9,11,10,12),] # order of mod1-8
  552. data_for_merging[[paste0(output_file_prefix, "MDFA_Mixed_Merge")]] <- read_data_file(paste0(dirs[["input_other"]], output_file_prefix, "MDFA_Mixed_ToMerge.csv"), na = "NA")
  553. data_for_analysis[[paste0(output_file_prefix, "MDFA" )]] <- merge(brain_data[["white"]][[paste0(output_file_prefix, "MDFA" )]], data_for_merging[[paste0(output_file_prefix, "MDFA_Mixed_Merge")]])
  554. data_for_analysis[[paste0(output_file_prefix, "Noddi")]] <- merge(brain_data[["white"]][[paste0(output_file_prefix, "Noddi")]], data_for_merging[[paste0(output_file_prefix, "MDFA_Mixed_Merge")]])
  555. ```
  556. ## Preparing Brain Data
  557. ```{r}
  558. dvs <- list()
  559. # Most of these (all except Vol) can be determined algorithmically from column names in data files
  560. dvs[["lh_area" ]] <- data_for_analysis[[paste0(output_file_prefix, "LH_Area" )]] %>% colnames %>% .[grepl("^lh_" , .)]
  561. dvs[["rh_area" ]] <- data_for_analysis[[paste0(output_file_prefix, "RH_Area" )]] %>% colnames %>% .[grepl("^rh_" , .)]
  562. dvs[["lh_thick"]] <- data_for_analysis[[paste0(output_file_prefix, "LH_Thickness")]] %>% colnames %>% .[grepl("^lh_.*_thickness$", .)]
  563. dvs[["rh_thick"]] <- data_for_analysis[[paste0(output_file_prefix, "RH_Thickness")]] %>% colnames %>% .[grepl("^rh_.*_thickness$", .)]
  564. # can't determine this one algorithmically
  565. dvs[["vol" ]] <- c("Brain_Stem", "CC_Anterior", "CC_Central", "CC_Mid_Anterior", "CC_Mid_Posterior", "CC_Posterior", "Left_Accumbens_area" , "Left_Amygdala", "Left_Caudate", "Left_Cerebellum_Cortex" , "Left_Cerebellum_White_Matter", "Left_choroid_plexus", "Left_Hippocampus", "Left_Pallidum" , "Left_Putamen" , "Left_Thalamus", "Optic_Chiasm", "Right_Accumbens_area" , "Right_Amygdala" , "Right_Caudate" , "Right_Cerebellum_Cortex" , "Right_Cerebellum_White_Matter", "Right_choroid_plexus", "Right_Hippocampus" , "Right_Lateral_Ventricle" , "Right_Pallidum", "Right_Putamen", "Right_Thalamus", "TotalGrayVol")
  566. dvs[["mdfa" ]] <- data_for_analysis[[paste0(output_file_prefix, "MDFA" )]] %>% colnames %>% .[(grepl("_FA$" , .) | grepl("_MD$" , .)) & !grepl("^slf[0-9]", .)]
  567. dvs[["noddi" ]] <- data_for_analysis[[paste0(output_file_prefix, "Noddi" )]] %>% colnames %>% .[(grepl("_Neurite_density$", .) | grepl("_ODI$", .)) & !grepl("^slf[0-9]", .)]
  568. ```
  569. ```{r All Models}
  570. # define predictors which are used as pred_others
  571. cnr_pred <- "cnr_c"
  572. control_preds <- c("age_c","sex","iq_c")
  573. all_preds <- paste(c(control_preds, cnr_pred), collapse=" + ")
  574. # create a data frame with all parameters needed, then call fit.lm() via an apply function to call everything at once!
  575. models_to_fit <- data.frame(rbind(
  576. # df dvs pred_pval pred_others id_text
  577. ## grey matter
  578. c("LH_Area" , "lh_area" , "Residuals_BehavioralRegression", cnr_pred , "LH_Area_Regression" )
  579. , c("Matched_GM" , "lh_area" , "PC_TD" , all_preds , "LH_Area_PC_TD" )
  580. , c("LH_Area_Cutoff_NoOut" , "lh_area" , "PC_TD_NoOut" , all_preds , "LH_Area_PC_TD_NoOut" )
  581. , c("Matched_GM" , "lh_area" , "PC_PD" , all_preds , "LH_Area_PC_PD" )
  582. , c("LH_Area_Cutoff_NoOut" , "lh_area" , "PC_PD_NoOut" , all_preds , "LH_Area_PC_PD_NoOut" )
  583. , c("Matched_GM" , "lh_area" , "PC_All" , all_preds , "LH_Area_PC_All" )
  584. , c("LH_Area_Cutoff_NoOut" , "lh_area" , "PC_All_NoOut" , all_preds , "LH_Area_PC_All_NoOut" )
  585. , c("LH_Area" , "lh_area" , "UPC_UGC" , all_preds , "LH_Area_UPC_UGC" )
  586. , c("LH_Area" , "lh_area" , "UPC_EC" , all_preds , "LH_Area_UPC_EC" )
  587. , c("LH_Area" , "lh_area" , "UPC_All" , all_preds , "LH_Area_UPC_All" )
  588. , c("RH_Area" , "rh_area" , "Residuals_BehavioralRegression", cnr_pred , "RH_Area_Regression" )
  589. , c("RH_Area_Cutoff" , "rh_area" , "PC_TD" , all_preds , "RH_Area_PC_TD" )
  590. , c("RH_Area_Cutoff_NoOut" , "rh_area" , "PC_TD_NoOut" , all_preds , "RH_Area_PC_TD_NoOut" )
  591. , c("RH_Area_Cutoff" , "rh_area" , "PC_PD" , all_preds , "RH_Area_PC_PD" )
  592. , c("RH_Area_Cutoff_NoOut" , "rh_area" , "PC_PD_NoOut" , all_preds , "RH_Area_PC_PD_NoOut" )
  593. , c("RH_Area_Cutoff" , "rh_area" , "PC_All" , all_preds , "RH_Area_PC_All" )
  594. , c("RH_Area_Cutoff_NoOut" , "rh_area" , "PC_All_NoOut" , all_preds , "RH_Area_PC_All_NoOut" )
  595. , c("RH_Area" , "rh_area" , "UPC_UGC" , all_preds , "RH_Area_UPC_UGC" )
  596. , c("RH_Area" , "rh_area" , "UPC_EC" , all_preds , "RH_Area_UPC_EC" )
  597. , c("RH_Area" , "rh_area" , "UPC_All" , all_preds , "RH_Area_UPC_All" )
  598. , c("LH_Thickness" , "lh_thick", "Residuals_BehavioralRegression", cnr_pred , "LH_Thick_Regression" )
  599. , c("LH_Thickness_Cutoff" , "lh_thick", "PC_TD" , all_preds , "LH_Thick_PC_TD" )
  600. , c("LH_Thickness_Cutoff_NoOut", "lh_thick", "PC_TD_NoOut" , all_preds , "LH_Thick_PC_TD_NoOut" )
  601. , c("LH_Thickness_Cutoff" , "lh_thick", "PC_PD" , all_preds , "LH_Thick_PC_PD" )
  602. , c("LH_Thickness_Cutoff_NoOut", "lh_thick", "PC_PD_NoOut" , all_preds , "LH_Thick_PC_PD_NoOut" )
  603. , c("LH_Thickness_Cutoff" , "lh_thick", "PC_All" , all_preds , "LH_Thick_PC_All" )
  604. , c("LH_Thickness_Cutoff_NoOut", "lh_thick", "PC_All_NoOut" , all_preds , "LH_Thick_PC_All_NoOut")
  605. , c("LH_Thickness" , "lh_thick", "UPC_UGC" , all_preds , "LH_Thick_UPC_UGC" )
  606. , c("LH_Thickness" , "lh_thick", "UPC_EC" , all_preds , "LH_Thick_UPC_EC" )
  607. , c("LH_Thickness" , "lh_thick", "UPC_All" , all_preds , "LH_Thick_UPC_All" )
  608. , c("RH_Thickness" , "rh_thick", "Residuals_BehavioralRegression", cnr_pred , "RH_Thick_Regression" )
  609. , c("RH_Thickness_Cutoff" , "rh_thick", "PC_TD" , all_preds , "RH_Thick_PC_TD" )
  610. , c("RH_Thickness_Cutoff_NoOut", "rh_thick", "PC_TD_NoOut" , all_preds , "RH_Thick_PC_TD_NoOut" )
  611. , c("RH_Thickness_Cutoff" , "rh_thick", "PC_PD" , all_preds , "RH_Thick_PC_PD" )
  612. , c("RH_Thickness_Cutoff_NoOut", "rh_thick", "PC_PD_NoOut" , all_preds , "RH_Thick_PC_PD_NoOut" )
  613. , c("RH_Thickness_Cutoff" , "rh_thick", "PC_All" , all_preds , "RH_Thick_PC_All" )
  614. , c("RH_Thickness_Cutoff_NoOut", "rh_thick", "PC_All_NoOut" , all_preds , "RH_Thick_PC_All_NoOut")
  615. , c("RH_Thickness" , "rh_thick", "UPC_UGC" , all_preds , "RH_Thick_UPC_UGC" )
  616. , c("RH_Thickness" , "rh_thick", "UPC_EC" , all_preds , "RH_Thick_UPC_EC" )
  617. , c("RH_Thickness" , "rh_thick", "UPC_All" , all_preds , "RH_Thick_UPC_All" )
  618. , c("Vol" , "vol" , "Residuals_BehavioralRegression", cnr_pred , "Vol_Regression" )
  619. , c("Vol_Cutoff" , "vol" , "PC_TD" , all_preds , "Vol_PC_TD" )
  620. , c("Vol_Cutoff_NoOut" , "vol" , "PC_TD_NoOut" , all_preds , "Vol_PC_TD_NoOut" )
  621. , c("Vol_Cutoff" , "vol" , "PC_PD" , all_preds , "Vol_PC_PD" )
  622. , c("Vol_Cutoff_NoOut" , "vol" , "PC_PD_NoOut" , all_preds , "Vol_PC_PD_NoOut" )
  623. , c("Vol_Cutoff" , "vol" , "PC_All" , all_preds , "Vol_PC_All" )
  624. , c("Vol_Cutoff_NoOut" , "vol" , "PC_All_NoOut" , all_preds , "Vol_PC_All_NoOut" )
  625. , c("Vol" , "vol" , "UPC_UGC" , all_preds , "Vol_UPC_UGC" )
  626. , c("Vol" , "vol" , "UPC_EC" , all_preds , "Vol_UPC_EC" )
  627. , c("Vol" , "vol" , "UPC_All" , all_preds , "Vol_UPC_All" )
  628. ## white matter
  629. , c("Matched_WM" , "mdfa" , "PC_PD" , all_preds , "MDFA_PC_PD" )
  630. , c("Matched_WM" , "mdfa" , "PC_TD" , all_preds , "MDFA_PC_TD" )
  631. , c("Matched_WM" , "mdfa" , "PC_All" , all_preds , "MDFA_PC_All" ) # NOT THE SAME DATA AS KELLY'S
  632. , c("MDFA" , "mdfa" , "UPC_EC" , all_preds , "MDFA_UPC_EC" )
  633. , c("MDFA" , "mdfa" , "UPC_UGC" , all_preds , "MDFA_UPC_UGC" )
  634. , c("MDFA" , "mdfa" , "UPC_All" , all_preds , "MDFA_UPC_All" )
  635. , c("MDFA" , "mdfa" , "Residuals_BehavioralRegression", cnr_pred , "MDFA_Regression" )
  636. ## NODDI
  637. , c("Matched_Noddi" , "noddi" , "PC_PD" , all_preds , "Noddi_PC_PD" )
  638. , c("Matched_Noddi" , "noddi" , "PC_TD" , all_preds , "Noddi_PC_TD" )
  639. , c("Matched_Noddi" , "noddi" , "PC_All" , all_preds , "Noddi_PC_All" )
  640. , c("Noddi" , "noddi" , "UPC_EC" , all_preds , "Noddi_UPC_EC" )
  641. , c("Noddi" , "noddi" , "UPC_UGC" , all_preds , "Noddi_UPC_UGC" )
  642. , c("Noddi" , "noddi" , "UPC_All" , all_preds , "Noddi_UPC_All" )
  643. , c("Noddi" , "noddi" , "Residuals_BehavioralRegression", cnr_pred , "Noddi_Regression" )
  644. ))
  645. names(models_to_fit) <- c("df", "dvs", "pred_pval", "preds_other", "id_text")
  646. ## COMMENTED OUT FOR DEBUGGING
  647. # # run all models in models_to_fit
  648. # all_models <- lapply(1:nrow(models_to_fit), function(next_model_idx) {
  649. # # extract info about next model
  650. # next_model_info <- models_to_fit[next_model_idx,]
  651. # # if next df name contains the word "Matched", look for it in matched_group_data.merged; otherwise, look for it in data_for_analysis
  652. # next_model_df <- ifelse(grepl("Matched", next_model_info[["df"]]), "matched_group_data.merged", "data_for_analysis")
  653. # # fit next set of models
  654. # next_fit_models <- fit_lms(
  655. # df = get(next_model_df)[[paste0(output_file_prefix, next_model_info[["df"]])]],
  656. # dvs = dvs[[models_to_fit[next_model_idx,"dvs"]]],
  657. # pred_pval = next_model_info[["pred_pval"]],
  658. # preds_other = strsplit(next_model_info[["preds_other"]], " \\+ ")[[1]],
  659. # id_text = next_model_info[["id_text"]]
  660. # )
  661. # return(next_fit_models)
  662. # })
  663. # names(all_models) <- models_to_fit[["id_text"]]
  664. # ENABLED FOR DEBUGGING
  665. all_models <- list()
  666. for(next_model_idx in 1:nrow(models_to_fit)) {
  667. print(paste0(next_model_idx, " / ", nrow(models_to_fit), "..."))
  668. # extract info about next model
  669. next_model_info <- models_to_fit[next_model_idx,]
  670. # if next df name contains the word "Matched", look for it in matched_group_data.merged; otherwise, look for it in data_for_analysis
  671. next_model_df <- ifelse(grepl("Matched", next_model_info[["df"]]), "matched_group_data.merged", "data_for_analysis")
  672. # fit next set of models
  673. next_fit_models <- fit_lms(
  674. df = get(next_model_df)[[paste0(output_file_prefix, next_model_info[["df"]])]],
  675. dvs = dvs[[models_to_fit[next_model_idx,"dvs"]]],
  676. pred_pval = next_model_info[["pred_pval"]],
  677. preds_other = strsplit(next_model_info[["preds_other"]], " \\+ ")[[1]],
  678. id_text = next_model_info[["id_text"]]
  679. )
  680. all_models[[next_model_info[["id_text"]]]] <- next_fit_models
  681. }
  682. # extract key features of linear results into a single df
  683. all_linear_results <- do.call(rbind.data.frame, lapply(names(all_models), function(model) {
  684. data.frame(model, all_models[[model]][["rsq.linear"]], p=all_models[[model]][["p"]])
  685. }))
  686. # extract all quadratic results into a single df
  687. models_with_quad_results_binary <- sapply(names(all_models), function(x) {"quad_results" %in% names(all_models[[x]])})
  688. all_quad_results <- do.call(rbind.data.frame, lapply(names(all_models)[models_with_quad_results_binary], function(model) {
  689. data.frame(model, all_models[[model]][["quad_results"]])
  690. }))
  691. all_rsq_results <- do.call(rbind.data.frame, lapply(names(all_models), function(model) {
  692. data.frame(model, all_models[[model]][["rsq.linear"]])
  693. }))
  694. all_rsq_results <- merge(all_rsq_results, all_quad_results[,colnames(all_quad_results) %in% colnames(all_rsq_results) | grepl("^rsq\\.",colnames(all_quad_results))], all.x=TRUE)
  695. #all_quad_results # contains all p-values from comparing linear & quadratic models
  696. # shows proportion of p-values <= .05 separately for each model
  697. sapply(unique(all_quad_results[["model"]]), function(model) {mean(all_quad_results[all_quad_results[["model"]]==model,"p"] <= .05)})
  698. # figure of same
  699. all_quad_results[["model"]] <- factor(all_quad_results[["model"]], unique(all_quad_results[["model"]]))
  700. all_quad_results[["p_thresh"]] <- as.factor(ifelse(all_quad_results[["p"]] <= .05, 1, 0))
  701. # for MDFA: subdivide based on l vs. r (or neither), and MD vs. FA
  702. # for NODDI: subdivide based on l vs. r (or neither), and ODI vs. NeuriteDensity
  703. all_quad_results.for_plotting <- all_quad_results
  704. all_quad_results.for_plotting[["model_group"]] <- gsub("^LH_", "l_", gsub("^RH_", "r_", gsub("_Regression$","",all_quad_results.for_plotting[["model"]])))
  705. # reorder measure and hemisphere designations...
  706. all_quad_results.for_plotting[["model_group"]] <- sapply(all_quad_results.for_plotting[["model_group"]], function(x) {paste(rev(strsplit(x, "_")[[1]]), collapse="_")})
  707. # oy!
  708. mdfa_split <- sapply(all_quad_results.for_plotting[all_quad_results.for_plotting[["model_group"]]=="MDFA","dv"], function(x) {strsplit(x, "_")})
  709. mdfa_num_fields <- sapply(mdfa_split, function(x) {length(x)})
  710. mdfa_groups <- sapply(1:length(mdfa_split), function(y) {paste(mdfa_split[[y]][mdfa_num_fields[y]:2], collapse="_")})
  711. all_quad_results.for_plotting[all_quad_results.for_plotting[["model_group"]]=="MDFA","model_group"] <- mdfa_groups
  712. noddi_split <- sapply(gsub("Neurite_density","NeuriteDensity",all_quad_results.for_plotting[all_quad_results.for_plotting[["model_group"]]=="Noddi","dv"]), function(x) {strsplit(x, "_")})
  713. noddi_num_fields <- sapply(noddi_split, function(x) {length(x)})
  714. noddi_groups <- sapply(1:length(noddi_split), function(y) {paste(noddi_split[[y]][noddi_num_fields[y]:2], collapse="_")})
  715. all_quad_results.for_plotting[all_quad_results.for_plotting[["model_group"]]=="Noddi","model_group"] <- noddi_groups
  716. ordered_model_groups <- c("Area_l", "Area_r", "Thick_l", "Thick_r", "Vol", "FA_l", "FA", "FA_r", "MD_l", "MD", "MD_r", "NeuriteDensity_l", "NeuriteDensity", "NeuriteDensity_r", "ODI_l", "ODI", "ODI_r")
  717. all_quad_results.for_plotting[["model_group"]] <- factor(all_quad_results.for_plotting[["model_group"]], ordered_model_groups)
  718. # determine where to add vertical lines for visual breaks in the more complex figure
  719. model_group_prefixes <- sapply(strsplit(ordered_model_groups, "_"), function(x) {x[[1]]})
  720. model_group_breaks <- which(model_group_prefixes[2:length(model_group_prefixes)]!=model_group_prefixes[1:(length(model_group_prefixes)-1)])+0.5
  721. # plot p-values by model (simpler)
  722. quad_p.plot <- ggplot(all_quad_results, aes(x=model, y=p, color=p_thresh)) +
  723. ggforce::geom_sina(size=1, scale=F, method="density", maxwidth=0.3, position=position_identity()) +
  724. scale_color_manual(values=c("black","red")) +
  725. geom_hline(yintercept=0.05, linetype="dashed", color="black") +
  726. theme_bw() +
  727. xlab("Model") +
  728. ylab("p")
  729. quartz(width=11, height=5); quad_p.plot
  730. # plot p-values by model, measure, hemisphere... (more complex)
  731. quad_p.for_plotting.plot <- ggplot(all_quad_results.for_plotting, aes(x=model_group, y=p, color=p_thresh)) +
  732. ggforce::geom_sina(size=1, scale=F, method="density", maxwidth=0.2, position=position_identity()) +
  733. scale_color_manual(values=c("black","red")) +
  734. geom_hline(yintercept=0.05, linetype="dashed", color="black") +
  735. geom_vline(xintercept=model_group_breaks, linetype="dashed", color="black") +
  736. theme_bw() +
  737. xlab("Model") +
  738. ylab("p")
  739. quartz(width=18, height=5); quad_p.for_plotting.plot
  740. ```
  741. ## How to access stored models
  742. ```{r Access model objects}
  743. # all_models is a list with 64 objects:
  744. # LH_Area_Regression, LH_Area_PCTD, ... RH_Area_Regression, RH_Area_PCTD, ...
  745. # LH_Thick_Regression, LH_Thick_PCTD, ... RH_Thick_Regression, RH_Thick_PCTD, ...
  746. # Vol_Regression, Vol_PCTD, ... MDFA_PCPD, MDFA_PCTD, ... Noddi_PCPD, Noddi_PCTD, ...
  747. # One of those can be accessed via (e.g.) all_models[["LH_Area_Regression"]]
  748. # That is itself a list with 3 objects:
  749. # "model" (a list of 75 fitted models); can be accessed via all_models[["LH_Area_Regression"]][["model"]]
  750. # "p" (a vector of 75 p-values, 1 per model); can be accessed via all_models[["LH_Area_Regression"]][["p"]]
  751. # "quad_results" (a data frame of results from quadratic tests); can be accessed via all_models[["LH_Area_Regression"]][["quad_results"]]
  752. # Unfortunately, the names of the model fields had to be changed to work with mtable(). So, at the moment, they're all "lm1", "lm2", ... "lm75".
  753. # You can see this via names(all_models[["LH_Area_Regression"]][["model"]])
  754. # So, in order to match those up with names which are actually useful, you can grab the list of dependent variables which they correspond to.
  755. # Those can be found in dvs[["lh_area"]] which returns a list of 75 area names. This means that, for example,
  756. # all_models[["LH_Area_Regression"]][["model"]][[1]] (or, equivalently, all_models[["LH_Area_Regression"]][["model"]][["lm1"]] )
  757. # will return the LH area regression model for dvs[["lh_area"]][[1]] , which is "lh_G_and_S_frontomargin_area" .
  758. # You can see the list of different sets of DVs via names(dvs) ,
  759. # and the list of everything that was matched up for model fitting is in models_to_fit
  760. # We can also make an enormous df of model p-values, as follows...
  761. all_model_results.flat <- suppressWarnings(lapply(1:nrow(models_to_fit), function(idx) {data.frame(subset(models_to_fit[idx,], select=-dvs), dv=dvs[[models_to_fit[idx,"dvs"]]], p=all_models[[idx]][["p"]])})) # warnings regard row names
  762. all_model_results.flat <- do.call(rbind.data.frame, all_model_results.flat)
  763. # all_model_results.flat now has everything in one place!
  764. all_rsq_results <- merge(all_model_results.flat, all_rsq_results, by.x=c("id_text","dv"), by.y=c("model","dv"))
  765. ```
  766. #FDR Correction for Multiple Comparisons
  767. ```{r MultipleComparisonsCorrection}
  768. dv_pcorr_groupings <- unique.data.frame(rbind(all_linear_results[,c("model","dv")],all_quad_results[,c("model","dv")])) # quad should be a subset, but just in case...
  769. model_suffixes <- c(
  770. "Regression"
  771. , "UPC_UGC"
  772. , "UPC_EC"
  773. , "UPC_All"
  774. , "PC_PD"
  775. , "PC_TD"
  776. , "PC_All"
  777. , "PC_PD_NoOut"
  778. , "PC_TD_NoOut"
  779. , "PC_All_NoOut"
  780. )
  781. dv_pcorr_groupings[["model_type"]] <- sapply(dv_pcorr_groupings[["model"]], function(x) {
  782. match_idx <- which(sapply(model_suffixes, function(y) { grepl(paste0("_", y, "$"), x) }))
  783. ifelse(length(match_idx)==1, return(model_suffixes[match_idx]), stop(paste0("Not exactly 1 match! : ", x, " has ", length(match_idx), " matches")))
  784. })
  785. dv_pcorr_groupings[["dv_type"]] <- sapply(strsplit(dv_pcorr_groupings[["dv"]], "_"), function(x) {x[length(x)]})
  786. dv_pcorr_groupings[["dv_type"]][!dv_pcorr_groupings[["dv_type"]] %in% c("area","thickness","MD","FA","ODI","density")] <- "volume"
  787. dv_pcorr_groupings[["pcorr_group_id"]] <- with(dv_pcorr_groupings, paste0(model_type, "_", dv_type))
  788. # count how many models are in each group
  789. unique_model_groups <- unique.data.frame(dv_pcorr_groupings[,c("model_type","dv_type","pcorr_group_id")])
  790. unique_model_groups[["num_models_in_group"]] <- sapply(unique_model_groups[["pcorr_group_id"]], function(x) {sum(dv_pcorr_groupings[["pcorr_group_id"]]==x)})
  791. # show # models per group and # of groups
  792. dplyr::rename(aggregate(pcorr_group_id ~ dv_type + num_models_in_group, data=unique_model_groups, FUN=length), num_groups="pcorr_group_id")
  793. ```
  794. ```{r Explore sig. quadratic models}
  795. # quad correction
  796. all_quad_results <- merge(all_quad_results, dv_pcorr_groupings[,c("model","dv","pcorr_group_id")])
  797. quad_summary <- do.call(rbind.data.frame, lapply(unique(all_quad_results[["pcorr_group_id"]]), function(pcorr_group_id) {
  798. matching_rows <- all_quad_results[all_quad_results[["pcorr_group_id"]]==pcorr_group_id,]
  799. c(nrow(matching_rows), sum(matching_rows[["p"]] < .05))}))
  800. colnames(quad_summary) <- c("n","sig")
  801. # correct p-values in groups
  802. for(next_group in unique(all_quad_results[["pcorr_group_id"]])) {
  803. temp_p_list <- all_quad_results[all_quad_results[["pcorr_group_id"]]==next_group,"p"]
  804. all_quad_results[all_quad_results[["pcorr_group_id"]]==next_group,"p.corr"] <- p.adjust(temp_p_list, method="fdr")
  805. all_quad_results[all_quad_results[["pcorr_group_id"]]==next_group,"n.corr"] <- length(temp_p_list)
  806. }
  807. sig_quad_results <- all_quad_results[all_quad_results[["p.corr"]] < .05,] # sig. post-correction
  808. # plot post-correction sig. p-values by model
  809. all_sig_quad_plots <- list()
  810. for(next_sig_quad_result_idx in 1:nrow(sig_quad_results)) {
  811. next_sig_quad_model_info <- sig_quad_results[next_sig_quad_result_idx,]
  812. next_sig_quad_model <- with(next_sig_quad_model_info, all_models[[as.character(model)]][["model"]][[dv]]) # (1) this is the linear model, not the quad model; (2) do I need this?
  813. #next_sig_quad_model <- with(next_sig_quad_model_info, all_models[[as.character(model)]][["quad_models"]][[as.character(dv)]]) # oy
  814. #next_sig_quad_plot <- plot_grouped_vs_continuous(fitted_lm=next_sig_quad_model, pred_var=c("Residuals_BehavioralRegression", "Residuals_BehavioralRegression.quad"))
  815. next_sig_quad_plot <- plot_grouped_vs_continuous(fitted_lm=next_sig_quad_model, pred_var="Residuals_BehavioralRegression", degree=c(1,2))
  816. next_sig_quad_plot <- next_sig_quad_plot + ggtitle(next_sig_quad_model_info[["dv"]])
  817. all_sig_quad_plots[[next_sig_quad_result_idx]] <- next_sig_quad_plot
  818. }
  819. all_sig_quad_plots.together <- grid.arrange(grobs=all_sig_quad_plots, ncol=5)
  820. ggsave(filename=paste0(super_dir, "all_sig_quad_plots.png"), plot=all_sig_quad_plots.together, width=30, height=15, units="in")
  821. all_sig_quad_plots.quad_models <- list()
  822. for(next_sig_quad_result_idx in 1:nrow(sig_quad_results)) {
  823. next_sig_quad_model_info <- sig_quad_results[next_sig_quad_result_idx,]
  824. # next_sig_quad_model <- with(next_sig_quad_model_info, all_models[[as.character(model)]][["model"]][[dv]]) # (1) this is the linear model, not the quad model; (2) do I need this?
  825. next_sig_quad_model <- with(next_sig_quad_model_info, all_models[[as.character(model)]][["quad_models"]][[as.character(dv)]]) # oy
  826. all_sig_quad_plots.quad_models[[next_sig_quad_result_idx]] <- next_sig_quad_model
  827. }
  828. all_sig_quad_plots.quad_models <- lapply(1:length(all_sig_quad_plots.quad_models), function(x) {coef(summary(all_sig_quad_plots.quad_models[[x]]))})
  829. sig_quad_results[["quad_coeff"]] <- sapply(1:length(all_sig_quad_plots.quad_models), function(x) {all_sig_quad_plots.quad_models[[x]][grepl("\\.quad$", rownames(all_sig_quad_plots.quad_models[[x]])),"Estimate"]})
  830. sig_quad_results
  831. ```
  832. ```{r Explore sig. linear models}
  833. # linear correction
  834. all_linear_results <- merge(all_linear_results, dv_pcorr_groupings[,c("model","dv","pcorr_group_id")])
  835. linear_summary <- do.call(rbind.data.frame, lapply(unique(all_linear_results[["pcorr_group_id"]]), function(pcorr_group_id) {
  836. matching_rows <- all_linear_results[all_linear_results[["pcorr_group_id"]]==pcorr_group_id,]
  837. c(nrow(matching_rows), sum(matching_rows[["p"]] < .05))}))
  838. colnames(linear_summary) <- c("n","sig")
  839. # correct p-values in groups
  840. for(next_group in unique(all_linear_results[["pcorr_group_id"]])) {
  841. temp_p_list <- all_linear_results[all_linear_results[["pcorr_group_id"]]==next_group,"p"]
  842. all_linear_results[all_linear_results[["pcorr_group_id"]]==next_group,"p.corr"] <- p.adjust(temp_p_list, method="fdr")
  843. all_linear_results[all_linear_results[["pcorr_group_id"]]==next_group,"n.corr"] <- length(temp_p_list)
  844. }
  845. sig_linear_results <- all_linear_results[all_linear_results[["p.corr"]] < .05,] # sig. post-correction
  846. # get p-values for quadratic plots for these same models, to identify models that only show linear but not quadratic components...
  847. compare_linear_and_quad <- merge(rename(sig_linear_results, p.linear="p", p.corr.linear="p.corr", n.corr.linear="n.corr"), subset(rename(all_quad_results, p.quad="p", p.corr.quad="p.corr", n.corr.quad="n.corr"), select=-c(F,df,p_thresh)), by.all=c("model","dv","group"), all.x=TRUE) # lol
  848. # remove models for which a quadratic model was not fit
  849. compare_linear_and_quad <- compare_linear_and_quad[!is.na(compare_linear_and_quad[["rsq.quad"]]),] # only 6!
  850. # remove models with sig. quad fits
  851. compare_linear_and_quad <- compare_linear_and_quad[compare_linear_and_quad[["p.corr.quad"]] > .05,]
  852. # plot quadratic fits for these models (not a typo!)
  853. all_sig_linear_nonsig_quad_plots <- list()
  854. for(next_result_idx in 1:nrow(compare_linear_and_quad)) {
  855. next_model_info <- compare_linear_and_quad[next_result_idx,]
  856. next_model <- with(next_model_info, all_models[[as.character(model)]][["model"]][[dv]])
  857. next_plot <- plot_grouped_vs_continuous(fitted_lm=next_model, pred_var="Residuals_BehavioralRegression", degree=c(1,2))
  858. next_plot <- next_plot + ggtitle(next_model_info[["dv"]])
  859. all_sig_linear_nonsig_quad_plots[[next_result_idx]] <- next_plot
  860. }
  861. all_sig_linear_nonsig_quad_plots <- grid.arrange(grobs=all_sig_linear_nonsig_quad_plots, ncol=3)
  862. ggsave(filename=paste0(super_dir, "all_sig_linear_nonsig_quad_plots.png"), plot=all_sig_linear_nonsig_quad_plots, width=18, height=10, units="in")
  863. ```
  864. ```{r Figs for paper}
  865. # added 12/23/25
  866. # First: Sinaplots for the AF, SLF, and R ILF for the UPC vs EC contrast (that one has the largest effect sizes) with mean lines by group (all MD)
  867. regions_for_sinaplots <- c(af_l_MD="MD - Left AF", af_r_MD="MD - Right AF", ilf_r_MD="MD - Right ILF", slfavg_l_MD="MD - Left SLF", slfavg_r_MD="MD - Right SLF")
  868. models_for_sinaplots <- data.frame(model="MDFA_UPC_EC", dv=names(regions_for_sinaplots), desc=unname(regions_for_sinaplots), pred.crit="UPC_EC")
  869. # overly complicated function for computing multiple stats on a given df
  870. agg_df <- function(char_funs=c("min","max","mean","sd"), df, x.col, y.col) {
  871. # uses aggregate() to apply each of the functions listed (in character format) in char_funs
  872. # to formula(y.col ~ [all of the vars in x.cols]) in data frame df, then returns the merged result
  873. resids_by_group <- Reduce(function(x,y) merge(x, y, by="group", all=TRUE), lapply(char_funs, function(fun) {agg <- aggregate(formula(paste0(y.col, " ~ ", paste(x.col, collapse=" + "))), FUN=eval(parse(text=fun)), data=df); colnames(agg) <- c(x.col,fun); return(agg) })) # !!!
  874. return(resids_by_group)
  875. }
  876. group_colors <- viridis(4)[2:3] # group colors are defined here -- first for UPC, second for EC
  877. names(group_colors) <- c("UPC","EC")
  878. x_margin <- 0.15
  879. all_sinaplots <- list()
  880. set.seed(123) # sinaplots involve random jitter; setting the seed for reproducibility
  881. for(next_model_for_sinaplot_idx in 1:nrow(models_for_sinaplots)) {
  882. next_model <- with(models_for_sinaplots[next_model_for_sinaplot_idx,], all_models[[model]][["model"]][[dv]]) # extract appropriate model
  883. # fit subset models to get residualized predictor & DV
  884. next_model.resids <- get_model_resids(next_model, models_for_sinaplots[next_model_for_sinaplot_idx,"pred.crit"])
  885. # combine df with residualized DV
  886. next_model.for_plotting <- data.frame(next_model[["model"]], dv.resid=next_model.resids[["subset_lm.resids"]], pred.resids=next_model.resids[["pred.resids"]])
  887. # this hard-codes in something which is earlier assumed to be a variable... the key col may not be UPC_EC
  888. next_model.for_plotting[["group"]] <- as.factor(with(next_model.for_plotting, ifelse(UPC_EC==0, "UPC", "EC"))) # it's either 0 or 1
  889. next_model.for_plotting[["group"]] <- factor(next_model.for_plotting[["group"]], c("UPC", "EC"))
  890. # compute means for separate plotting
  891. next_model.for_plotting.means <- dplyr::rename(agg_df(char_funs=c("mean","sd","length"), df=next_model.for_plotting, x.col=c("group"), y="dv.resid"), n="length")
  892. next_model.for_plotting.means[["se"]] <- with(next_model.for_plotting.means, sd/sqrt(n))
  893. next_model.for_plotting.means <- next_model.for_plotting.means[order(next_model.for_plotting.means[["group"]]),] # order (important!)
  894. next_plot <- ggplot(next_model.for_plotting, aes(x=group, y=dv.resid, color=group)) +
  895. ggforce::geom_sina(size=1, scale=F, method="density", maxwidth=0.3, position=position_identity()) +
  896. scale_color_manual(values=group_colors) +
  897. theme_bw() +
  898. xlab("Group") +
  899. ylab(paste0(models_for_sinaplots[next_model_for_sinaplot_idx,"desc"], " (resid.)")) +
  900. theme(panel.grid.major.x=element_blank(), legend.position="none") # no vertical lines
  901. # add means
  902. for(next_group_mean_idx in 1:nrow(next_model.for_plotting.means)) {
  903. next_plot <- next_plot +
  904. geom_segment(color="black", linewidth=0.5, # horizontal line at y-mean
  905. x = with(next_model.for_plotting.means[next_group_mean_idx,], next_group_mean_idx-x_margin),
  906. xend = with(next_model.for_plotting.means[next_group_mean_idx,], next_group_mean_idx+x_margin),
  907. y = with(next_model.for_plotting.means[next_group_mean_idx,], mean ),
  908. yend = with(next_model.for_plotting.means[next_group_mean_idx,], mean )) +
  909. geom_segment(color="black", linewidth=1.5, # vertical line to show +/- 1 SE variability
  910. x = next_group_mean_idx, xend=next_group_mean_idx,
  911. y = with(next_model.for_plotting.means[next_group_mean_idx,], mean-se),
  912. yend = with(next_model.for_plotting.means[next_group_mean_idx,], mean+se))
  913. }
  914. # store plot in list
  915. all_sinaplots[[next_model_for_sinaplot_idx]] <- next_plot
  916. }
  917. # combine plots and save
  918. all_sinaplots.together <- grid.arrange(grobs=all_sinaplots, ncol=3)
  919. ggsave(filename=paste0(super_dir, "all_sinaplots.png"), plot=all_sinaplots.together, width=9, height=6, units="in")
  920. # Second: Scatterplots with a nonlinear fit line for thel AF (af_l_fa) and the right SLF (slf_r_Neurite_density) with colors from the groups
  921. # note: the second one is not sig. (p = .051)
  922. models_for_quadplots <- data.frame(rbind(c("MDFA_Regression" , "af_l_FA" , "FA - Left AF" , "Residuals_BehavioralRegression"),
  923. c("Noddi_Regression", "slfavg_r_Neurite_density", "NODDI - Right SLF", "Residuals_BehavioralRegression")))
  924. colnames(models_for_quadplots) <- c("model","dv","desc","pred.crit")
  925. all_quad_results[(all_quad_results[["dv"]] %in% models_for_quadplots[["dv"]]),] # show info for these two models
  926. # generate plots for these quadratic models
  927. all_paper_quad_plots <- list()
  928. for(next_paper_quad_result_idx in 1:nrow(models_for_quadplots)) {
  929. next_paper_quad_model_info <- models_for_quadplots[next_paper_quad_result_idx,]
  930. next_paper_quad_model <- with(next_paper_quad_model_info, all_models[[as.character(model)]][["model"]][[dv]])
  931. next_paper_quad_plot <- plot_grouped_vs_continuous(fitted_lm=next_paper_quad_model, pred_var=next_paper_quad_model_info[["pred.crit"]], degree=c(2), linetype=c(1)) # to show both linear AND quad, change degree to c(1,2)
  932. next_paper_quad_plot <- next_paper_quad_plot +
  933. xlab("Comprehension regression residuals (resid.)") +
  934. ylab(paste0(next_paper_quad_model_info[["desc"]], " (resid.)"))
  935. all_paper_quad_plots[[next_paper_quad_result_idx]] <- next_paper_quad_plot
  936. }
  937. all_paper_quad_plots.together <- grid.arrange(grobs=all_paper_quad_plots, ncol=2)
  938. ggsave(filename=paste0(super_dir, "all_paper_quad_plots.png"), plot=all_paper_quad_plots.together, width=12, height=5, units="in")
  939. ```

Mahaffy_Masters_Real_DK__KM_20251223 (1).Rmd, no license · at the source

Overview

  1. Department of Psychological Sciences, University of Connecticut, Storrs, CT, USA
  2. Child Study Center, Yale School of Medicine, New Haven, CT, USA
  3. Brain Imaging Research Core, University of Connecticut, Storrs, CT, USA
  4. The Nathan S. Klein Institute for Psychiatric Research, Orangeburg, NY, USA
Institutions: University of Connecticut (United States); Yale University (United States); Nathan Kline Institute for Psychiatric Research (United States)
Journal: Neurobiology of language (Cambridge, Mass.), volume 7, article NOL.a.265
Dates: received 7 May 2025; accepted 14 April 2026; published online 1 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1162/nol.a.265 · PMID 42492006 · PMCID PMC13379302 · OpenAlex W4412638000
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), cognitive (subfield)
Methods: Statistics, Connectivity, fMRI & imaging, Machine learning, Preprocessing
Keywords: educational neuroscience, NODDI (neurite orientation dispersion and density imaging), poor comprehension, reading comprehension
Topic: Reading and Literacy Development (Developmental and Educational Psychology, Psychology), according to OpenAlex
Funding: NIDCD NIH HHS (T32 DC017703)
Citations: not cited yet (Europe PMC); 102 references in the paper

Abstract

Poor comprehenders (PCs) have typical word reading skill and intelligence but poorer than expected reading comprehension. While the prevalence of PCs is similar to that of poor decoders (individuals who struggle fluently converting written text into spoken language), less is known about the neurobiological substrates of poor comprehension. Extant studies have found small differences in gray matter volume between PCs and poor or typically reading peers. However, a detailed quantification of cortical morphometric features and white matter integrity remains unexplored. Data from 2,100 children (1,200 with imaging data), aged 8–16 years were analyzed to determine if there is a distinct neuroanatomy associated with poor reading comprehension across three common methods for classifying PCs. We computed gray matter volume, cortical thickness, and surface area, as well as white matter measures (mean diffusivity, fractional anisotropy, neurite orientation, and neurite density) for PCs and compared these measures with those of poor decoders and typical readers. Results revealed small but widespread white matter differences, but no gray matter differences between PCs and other readers. PCs showed decreased white matter integrity (increased mean diffusivity, decreased neurite density) in tracts previously associated with reading, including the superior longitudinal fasciculus and inferior longitudinal fasciculus, and in tracts that have been associated with cognitive performance such as the uncinate fasciculus. These results suggest that diffuse structural connectivity differences may underlie reading comprehension weaknesses in the face of intact decoding skills. This is consistent with the behavioral profile of PCs who exhibit a broad pattern of subclinical impairments in language and integrative cognitive processes.

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 2 matches between paragraphs and lines of code.

OSF 2ng57

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Languages: R (1)
Size: 14 files, 1 script
Software Heritage: not checked
Found in: “DATA AVAILABILITY STATEMENT”
Holds: 1 notebook
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file), ggpubr (1 file), psych (1 file), reshape2 (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
1 file
At the source: osf.io/2ng57/overview

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;
  • 1 script, each with its path and the digest of its content;
  • 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

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

Data

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

Data availability statement

All data and code used for this project, along with project preregistration, are publicly available in an OSF repository: https://osf.io/2ng57/overview.

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, 4 authors, 4 keywords, 1 funder, 96 references.

Cite

This paper

Mahaffy, K., Koirala, N., Kleinman, D., & Landi, N. (2026). Structural Brain Correlates of Poor Reading Comprehension. Neurobiology of language (Cambridge, Mass.), 7, NOL.a.265. https://doi.org/10.1162/nol.a.265

BibTeX

@article{mahaffy2026structural,
author = {Mahaffy, Kelly and Koirala, Nabin and Kleinman, Daniel and Landi, Nicole},
title = {{Structural Brain Correlates of Poor Reading Comprehension}},
journal = {Neurobiology of language (Cambridge, Mass.)},
year = {2026},
month = jul,
volume = {7},
pages = {NOL.a.265},
publisher = {MIT Press},
issn = {2641-4368},
doi = {10.1162/nol.a.265},
url = {https://doi.org/10.1162/nol.a.265},
pmid = {42492006},
pmcid = {PMC13379302}
}

RIS

TY - JOUR
AU - Mahaffy, Kelly
AU - Koirala, Nabin
AU - Kleinman, Daniel
AU - Landi, Nicole
TI - Structural Brain Correlates of Poor Reading Comprehension
T2 - Neurobiology of language (Cambridge, Mass.)
J2 - Neurobiol Lang (Camb)
PY - 2026
DA - 2026/07/01
VL - 7
SP - NOL.a.265
SN - 2641-4368
PB - MIT Press
DO - 10.1162/nol.a.265
UR - https://doi.org/10.1162/nol.a.265
LA - en
ER -

CSL-JSON

{
"id": "10.1162/nol.a.265",
"type": "article-journal",
"title": "Structural Brain Correlates of Poor Reading Comprehension",
"container-title": "Neurobiology of language (Cambridge, Mass.)",
"author": [
{
"family": "Mahaffy",
"given": "Kelly"
},
{
"family": "Koirala",
"given": "Nabin"
},
{
"family": "Kleinman",
"given": "Daniel"
},
{
"family": "Landi",
"given": "Nicole"
}
],
"container-title-short": "Neurobiol Lang (Camb)",
"volume": "7",
"page": "NOL.a.265",
"DOI": "10.1162/nol.a.265",
"PMID": "42492006",
"PMCID": "PMC13379302",
"ISSN": "2641-4368",
"publisher": "MIT Press",
"URL": "https://doi.org/10.1162/nol.a.265",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
1
]
]
}
}

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.1162/imag.a.1303
Composite reaction time and variability correlate with whole-brain white-matter characteristics.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: structural MRI / diffusion, 14 references
[2] doi:10.3389/frai.2026.1771088 [code]
Few-shot deployment of pretrained MRI transformers in brain imaging tasks.
Journal: Frontiers in artificial intelligence
In common: structural MRI / diffusion, 13 references
[3] doi:10.3389/fpain.2026.1850836 [code]
Diffusion tensor imaging in chronic tension-type headache.
Journal: Frontiers in pain research (Lausanne, Switzerland)
In common: structural MRI / diffusion, 11 references
[4] doi:10.1038/s41593-026-02359-0 [code]
The cross-site reproducibility of MRI morphometric phenotypes in psychiatric disorders.
Journal: Nature neuroscience
In common: structural MRI / diffusion, 9 references
[5] doi:10.1038/s41467-026-73262-2 [code]
Robust but independent sex differences in human brain function, structure, and behavior.
Journal: Nature communications
In common: reshape2, ggpubr, ggplot2, 1 other tool, structural MRI / diffusion, 4 references
[6] doi:10.1016/j.nicl.2026.104012 [code]
Structural-functional multilayer brain network properties and outcome of combined repetitive transcranial magnetic stimulation and psychotherapy for obsessive-compulsive disorder.
Journal: NeuroImage. Clinical
In common: psych, reshape2, ggpubr, 2 other tools, structural MRI / diffusion, 2 references
[7] doi:10.7554/elife.108109 [code]
Multimodal MRI marker of cognition explains the association between cognition and mental health in the UK Biobank.
Journal: eLife
In common: psych, ggplot2, structural MRI / diffusion, cognitive, 5 references
[8] doi:10.1162/netn.a.536 [code]
Increased sensitivity in identifying language-related functional connectivity using jackknife resampling analyses.
Journal: Network neuroscience (Cambridge, Mass.)
In common: psych, reshape2, ggpubr, 2 other tools, 2 references
[9] doi:10.1038/s41593-026-02363-4 [code]
Cortical thickness changes precede high levels of amyloid by at least 7 years.
Journal: Nature neuroscience
In common: ggpubr, ggplot2, tidyverse, structural MRI / diffusion, 4 references
[10] doi:10.1007/s00415-026-14034-2
Similarities and differences in the late-onset GM2 gangliosidoses: Tay-Sachs and Sandhoff diseases.
Journal: Journal of neurology
In common: structural MRI / diffusion, 7 references

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.