OSCR

Plasma proteomics link menopause timing to brain aging and dementia risk

Code ↔ Paper

The paper beside its authors' code: matches between them have not been computed for this paper yet.

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 · 1,654 lines · 94 KB · MIT

  1. ###################################################################################################################################
  2. ## One-Step GSA FET (R piano implementation) WITH USER PARAMETERS
  3. ## - by Eric Dammer, Divya Nandakumar
  4. ## Nicholas Seyfried Lab Bioinformatics - for the lab -
  5. ## 01/07/2026 version 1.4 -- New: GO.bubblePlot() function for post-processing to GSEA-style bubble plots
  6. ## from the Zscore GOparallel output; and added a GOparallel(..., bubble=TRUE) argument
  7. ## 08/20/2025 version 1.3 -- malloc warning/error for parallelization on some Macs fixed; added Cohen's kappa graph-based
  8. ## pruning within ontology types with option removeRedundantGOterms="kappa"
  9. ## 03/07/2023 version 1.2 -- Outputs include semicolon-separated Genes.Hit goi[goi %in% gs] for each input list/gene set
  10. ## 10/18/2022 version 1.1 -- (inputFile variable previously fileName and other syntax changes; now runs on R v4.2.0)
  11. ## -- cocluster now runs even if removeRedundantGOterms not set or FALSE.
  12. ###################################################################################################################################
  13. GOparallel <- function(dummyVar="",env=.GlobalEnv) {
  14. minHitsPerOntology=5 # ontologies hit by fewer than this number of genes in an input list will not be plotted in Z score barplots.
  15. if(!exists("filePath")) { cat(paste0("- filePath not set. Using current working directory: ",getwd(),"\n")); filePath=getwd(); }
  16. ## Clean out spaces and escaped backslashes from folder paths (folder names with spaces should not be used on non-windows systems with this script)
  17. #filePath=paste0(paste( sapply(do.call(c,strsplit(filePath,"[/\\]")),function(x) { if (grepl(" ",x)) { gsub(x,paste0(substr(gsub(" ","",x),1,6),"~1"),x) } else { x } } ),collapse="/"),"/")
  18. filePath=paste0(paste( sapply(do.call(as.vector,strsplit(filePath,"[/\\]")),function(x) { if (grepl(" ",x)) { gsub(" ","\ ",x) } else { x } } ),collapse="/"),"/")
  19. if(!dir.exists(filePath)) { cat(paste0("- filePath set to ",filePath," ...this path was not found. Using current working directory: ",getwd(),"\n")); filePath=getwd(); }
  20. if(!exists("GMTdatabaseFile")) { cat(paste0("- GMTdatabaseFile variable not specified. Current BaderLab .GMT database file will be downloaded to ",filePath,"\n")); GMTdatabaseFile=paste0(filePath,"nonexistent.file"); }
  21. GMTdatabaseFile=paste0(paste( sapply(do.call(as.vector,strsplit(GMTdatabaseFile,"[/\\]")),function(x) { if (grepl(" ",x)) { gsub(" ","\ ",x) } else { x } } ),collapse="/"),"")
  22. if(!exists("GO.OBOfile")) { cat(paste0("- go.obo file not specified; if needed, current working directory will be checked and if not present, will be downloaded...\n")); GO.OBOfile=paste0(getwd(),"/go.obo"); }
  23. GO.OBOfile=paste0(paste( sapply(do.call(as.vector,strsplit(GO.OBOfile,"[/\\]")),function(x) { if (grepl(" ",x)) { gsub(" ","\ ",x) } else { x } } ),collapse="/"),"")
  24. #pythonPath=paste0(paste( sapply(do.call(c,strsplit(pythonPath,"[/\\]")),function(x) { if (grepl(" ",x)) { gsub(x,paste0(substr(gsub(" ","",x),1,6),"~1"),x) } else { x } } ),collapse="/"),"/")
  25. #GOeliteFolder=paste0(paste( sapply(do.call(c,strsplit(GOeliteFolder,"[/\\]")),function(x) { if (grepl(" ",x)) { gsub(x,paste0(substr(gsub(" ","",x),1,6),"~1"),x) } else { x } } ),collapse="/"),"/")
  26. ## The files we create here are input files for GO-Elite, text files with the gene list as the 1st column, a symbol identified (gene symbol, uniprot etc) as the 2nd column
  27. ## Different accepted inputs are given in the tutorial
  28. ## Commonly used symbols - Gene Symbol - Sy (example of input file below)
  29. ### GeneSymbol SystemCode (Symbol format)
  30. ### GFAP Sy
  31. ### APOE Sy
  32. ## All input files are placed in one folder
  33. ## The background file is prepared similarly and is placed in a separate folder
  34. ## The initial part of the code prepares files for GO-Elite. This can be skipped if the files are being made manually as described above.
  35. ## The second part of the code runs GO-ELite either from R (using the system command) or can be run using the terminal (in mac)
  36. ## The second part requires GO-Elite to be installed and path to the GO-Elite installation site indicated following python
  37. ## The 3rd part of the code plots the results from the GO-Elite results folder. When using the GUI the 1st 2 parts can be skipped and only the 3rd part can be used for plotting
  38. ##-------------------------------##
  39. ## Preparing files for GO-Elite ##
  40. ## Takes in the module assignment file as input with 1st column having gene names, 2nd column having color assignments followed by kME values
  41. if (!exists("filePath")) { cat(paste0(" - filePath variable not specified. Input/Output will take place in the current working directory: ",getwd(),"/ ...\n")); filePath<-paste0(getwd(),"/"); }
  42. if (!exists("outFilename")) { cat(paste0("- outFilename variable not specified. Output files will be saved to: ",getwd(),"/GOparallel/ ...\n")); outFilename="GOparallel"; }
  43. if (!dir.exists(file.path(filePath, outFilename))) dir.create(file.path(filePath, outFilename))
  44. if(!file.exists(GMTdatabaseFile)) {
  45. if (interactive()) {
  46. suppressPackageStartupMessages(require(rvest,quietly=TRUE))
  47. species.links <- html_attr(html_nodes(read_html("http://download.baderlab.org/EM_Genesets/current_release/"), xpath="//a"), "href")
  48. species.links <- species.links[grepl("^[A-z].*\\/$",species.links)]
  49. cat("- GMT File not found: ", GMTdatabaseFile,"\n\n")
  50. print(data.frame(Species=species.links))
  51. input.idx <- readline(paste0("[INTERACTIVE]\nChoose one of the above species from http://download.baderlab.org/EM_Genesets/current_release/ [1-",length(species.links),"]: "))
  52. input.idx <- as.integer(input.idx)
  53. find.symbol.in.links <- html_attr(html_nodes(read_html(paste0("http://download.baderlab.org/EM_Genesets/current_release/",species.links[input.idx])), xpath="//a"), "href")
  54. find.symbol.in.links <- find.symbol.in.links[grepl("[Ss][Yy][Mm][Bb][Oo][Ll]\\/",find.symbol.in.links)]
  55. gmt.links <- html_attr(html_nodes(read_html(paste0("http://download.baderlab.org/EM_Genesets/current_release/",species.links[as.integer(input.idx)],find.symbol.in.links)), xpath="//a"), "href")
  56. file.candidates1<-which(grepl("*\\_GO\\_AllPathways\\_.*\\.[Gg][Mm][Tt]",gmt.links)) # main regEx filter for file to download
  57. file.candidates2<-which(grepl("*\\_noPFOCR.*\\.[Gg][Mm][Tt]",gmt.links)) # files after March 2024 are a subset, excluding PMID-linked lists
  58. file.candidates3<-which(grepl("*\\_with\\_GO\\_iea\\_.*\\.[Gg][Mm][Tt]",gmt.links)) # take files with automated ontologies
  59. this.file.idx=if(length(file.candidates2)>0) { intersect(file.candidates2, intersect(file.candidates1,file.candidates3)) } else { intersect(file.candidates1,file.candidates3) }
  60. if(length(this.file.idx)<1) stop(paste0("Web scraping of the Bader Lab Website could not find an expected GMT filename pattern match.\nDownload and specify a GMTdatabaseFile prior to running this function."))
  61. full.dl.file=gmt.links[this.file.idx[1] ]
  62. GMTtargetPath=gsub("\\/\\/","/", gsub("(.*\\/).*$","\\1",GMTdatabaseFile) )
  63. gmt.url<-paste0("http://download.baderlab.org/EM_Genesets/current_release/",species.links[input.idx],find.symbol.in.links,full.dl.file)
  64. if(file.exists(file.path(GMTtargetPath,full.dl.file))) {
  65. cat(paste0("- Found that the full current GMT file online matches a file name you already have:\n ",full.dl.file," [skipping download]\n"))
  66. GMTdatabaseFile=paste0(GMTtargetPath,full.dl.file)
  67. } else {
  68. cat("Found full current GMT file online: ",gmt.url,"\n")
  69. cat("Download this file to folder: ",GMTtargetPath,"\n")
  70. input.dlYN <- readline("[Y/n]?")
  71. if(input.dlYN == "Y" | input.dlYN == "y" | input.dlYN == "") {
  72. suppressPackageStartupMessages(require(curl,quietly=TRUE))
  73. if (!dir.exists(GMTtargetPath)) dir.create(GMTtargetPath)
  74. curr.dir<-getwd()
  75. setwd(GMTtargetPath)
  76. cat("Downloading .gmt file for ",species.links[input.idx],"...\n")
  77. curl_download(url=gmt.url, destfile=full.dl.file, quiet = TRUE, mode = "w")
  78. setwd(curr.dir)
  79. cat("Using new downloaded .gmt file: ", paste0(GMTtargetPath,full.dl.file),"\n")
  80. GMTdatabaseFile=paste0(GMTtargetPath,full.dl.file)
  81. }
  82. }
  83. } else { stop(paste0("This is not an interactive session and required GMT file not found.\n",GMTdatabaseFile," must be downloaded interactively or prior to running this function.")) }
  84. }
  85. if(!exists("removeRedundantGOterms")) { cat("- removeRedundantGOterms not specified TRUE/FALSE, or 'kappa'. Removing them as the default, using go.obo and ontologyIndex package (you can also try removeRedundantGOterms='kappa').\n"); removeRedundantGOterms=TRUE; } else {
  86. if (removeRedundantGOterms=="Kappa" | removeRedundantGOterms=="KAPPA") removeRedundantGOterms="kappa"
  87. if (removeRedundantGOterms=="kappa") { cat("- removeRedundantGOterms='kappa'; will use Cohen's kappa<0.30 in igraph representation of gene symbol similarity to prune within ontology types.\n") } else {
  88. if(removeRedundantGOterms) {
  89. if (!file.exists(GO.OBOfile)) {
  90. suppressPackageStartupMessages(require(curl,quietly=TRUE))
  91. OBOtargetPath=gsub("(.*\\/).*$","\\1",GO.OBOfile)
  92. if (!dir.exists(OBOtargetPath)) dir.create(OBOtargetPath)
  93. curr.dir<-getwd()
  94. setwd(OBOtargetPath)
  95. cat(paste0("- Downloading go.obo file for main GO term redundancy cleanup...\n...to location: ",OBOtargetPath,"go.obo\n"))
  96. curl_download(url="http://current.geneontology.org/ontology/go.obo", destfile="go.obo", quiet = TRUE, mode = "w")
  97. setwd(curr.dir)
  98. cat("GO.OBOfile set to downloaded file: ", paste0(OBOtargetPath,"go.obo"),"\n")
  99. GO.OBOfile=paste0(OBOtargetPath,"go.obo")
  100. }
  101. }
  102. }
  103. }
  104. ## Check what type of input the user wants. modulesInMemory/ANOVAgroups/file(lists as columns, or kME module membership table)
  105. if (!exists("ANOVAgroups") & !exists("modulesInMemory")) {
  106. if(!exists("inputFile")) {
  107. cat("- No input specified as modulesInMemory=TRUE, ANOVAgroups=TRUE, and no inputFile variable for input either.\n Trying modulesInMemory=TRUE ...\n")
  108. modulesInMemory=TRUE
  109. ANOVAgroups=FALSE
  110. } else {
  111. if(!file.exists(file.path(filePath,inputFile))) stop(paste0("\ninputFile input specified but not found where expected, in: ",paste0(filePath,inputFile),"\nDid you mean to set ANOVAgroups or modulesInMemory=TRUE?\n\n"))
  112. ANOVAgroups=FALSE
  113. modulesInMemory=FALSE
  114. }
  115. } else {
  116. if(exists("ANOVAgroups")) {
  117. if(is.logical(ANOVAgroups)) {
  118. if(ANOVAgroups) {
  119. if(exists("ANOVAout")) { cat("- Found ANOVAgroups=TRUE. Proceeding to process ANOVAout table from memory, using any selections and thresholds set during volcano plotting...\n"); modulesInMemory=FALSE; } else { if (!length(dummyVar)==1) { cat("- ANOVAout not in memory, trying to use input provided to this function (could be ANOVAout or CORout).\n"); ANOVAout=as.data.frame(dummyVar); modulesInMemory=FALSE; } else { stop("Variable ANOVAout not found or no input was provided.\nPlease run parANOVA.dex() function first, and save output to ANOVAout variable or pass its output to this function.\n\n") } }
  120. } else {
  121. if(exists("modulesInMemory")) if(is.logical(modulesInMemory)) if(!modulesInMemory) { # both flags are FALSE
  122. if(!exists("outFilename")) { stop("modulesInMemory=FALSE, ANOVAgroups=FALSE, and no outFilename variable for input either.\nOne of these must be used.\n") }
  123. } else {
  124. cat("- modulesInMemory=TRUE. We will use network module colors vector and symbols found in rownames of cleanDat table for gene lists to check for ontology enrichment.\n")
  125. }
  126. }
  127. } else { #ANOVAgroups not logical TRUE/FALSE
  128. if(exists("modulesInMemory")) if(is.logical(modulesInMemory)) if(!modulesInMemory) { # modulesInMemory is FALSE
  129. if(!exists("inputFile")) { cat("- modulesInMemory=FALSE, ANOVAgroups not TRUE/FALSE, and no inputFile variable for input either.\nOne of these must be used. Trying ANOVAgroups=TRUE ...\n"); ANOVAgroups=TRUE;
  130. if(exists("ANOVAout")) { cat("- Found ANOVAout. Proceeding to process ANOVAout table from memory, using any selections and thresholds set during volcano plotting...\n") } else { stop("\nANOVAout table not found in memory.\n\n") }
  131. } else { # inputFile variable set. Does the file exist?
  132. if(file.exists(file.path(filePath,inputFile))) { cat("- found inputFile as set for input: ",paste0(filePath,inputFile),"\n"); ANOVAgroups=FALSE; } else { stop("\ninputFile as specified not found: ",paste0(filePath,inputFile),"\n\n"); }
  133. }
  134. } else { # modulesInMemory is TRUE
  135. cat("- modulesInMemory=TRUE. We will use network module colors vector and symbols found in rownames of cleanDat table for gene lists to check for ontology enrichment.\n")
  136. ANOVAgroups=FALSE
  137. }
  138. }
  139. } else { # ANOVAgroups does not exist, but modulesInMemory exists. Is it TRUE?
  140. if(exists("modulesInMemory")) {
  141. if(is.logical(modulesInMemory)) {
  142. if(modulesInMemory) {
  143. cat("- modulesInMemory=TRUE.\n We will use network module colors vector and symbols found in rownames of cleanDat table for gene lists to check for ontology enrichment.\n Note rownames of cleanDat table must contain gene symbol, and NETcolors or net$colors vector of module color assignments must be available.\n")
  144. ANOVAgroups=FALSE
  145. } else {
  146. if(!exists("inputFile")) { cat("- modulesInMemory=FALSE, ANOVAgroups not TRUE/FALSE, and no inputFile variable for input either.\nOne of these must be used. Trying ANOVAgroups=TRUE ...\n"); ANOVAgroups=TRUE;
  147. if(exists("ANOVAout")) { cat("- Found ANOVAout. Proceeding to process ANOVAout table from memory, using any selections and thresholds set during volcano plotting...\n") } else { stop("\nANOVAout table not found in memory.\n\n") }
  148. } else { # outFilenName variable set. Does the file exist?
  149. if(file.exists(file.path(filePath,inputFile))) { cat("- found inputFile as set for input: ",paste0(filePath,inputFile),"\n"); ANOVAgroups=FALSE; } else { stop("\ninputFile as specified not found: ",paste0(filePath,inputFile),"\n\n"); }
  150. }
  151. }
  152. } else { #modulesInMemory not logical TRUE/FALSE
  153. if(!exists("inputFile")) {
  154. cat("- ANOVAgroups not set, modulesInMemory not TRUE/FALSE, and no inputFile variable for input either.\nOne of these must be used. Trying modulesInMemory=TRUE ...\n")
  155. modulesInMemory=TRUE;
  156. ANOVAgroups=FALSE;
  157. } else { # inputFile variable set. Does the file exist?
  158. if(file.exists(file.path(filePath,inputFile))) { cat("- found inputFile as set for input: ",paste0(filePath,inputFile),"\n"); ANOVAgroups=FALSE; modulesInMemory=FALSE; } else { stop("\ninputFile as specified not found: ",paste0(filePath,inputFile),"\n\n"); }
  159. }
  160. }
  161. } #else { #both ANOVAgroups and modulesInMemory do not exist; handled first above.
  162. }
  163. }
  164. ## Further checks if modulesInMemory=TRUE for nrow(cleanDat)==NETcolors; and choice of vector for NETcolors
  165. if(modulesInMemory) {
  166. if (!exists("cleanDat")) stop("cleanDat variable must exist, rownames expected to hold gene symbols in the form of 'Symbol' or 'Symbol|...'.\n\n")
  167. if (!exists("NETcolors")) {
  168. if(exists("net")) if ("colors" %in% names(net)) {
  169. NETcolors=net$colors
  170. } else {
  171. if ("NETcolors" %in% colnames(ANOVAout)) {
  172. NETcolors=ANOVAout$NETcolors
  173. } else {
  174. NETcolors=c()
  175. }
  176. }
  177. }
  178. if (!length(NETcolors)==nrow(cleanDat)) { stop("\n\nNetwork color assignment vector not supplied or not of length in rows of cleanDat.\nEvery gene product needs a module (color) assignment when modulesInMemory=TRUE.\n\n") }
  179. }
  180. if (!exists("parallelThreads")) { cat("- parallelThreads variable not set. Attempting to run with 8 threads.\n"); parallelThreads=8; }
  181. if (!exists("outputGOeliteInputs")) outputGOeliteInputs=FALSE
  182. ##1a. Organize input gene lists, of the significant up and down (p<0.05) proteins in the current cleanDat
  183. ### List building from ANOVA-defined categories ###
  184. if(!exists("corVolc")) corVolc=FALSE
  185. if (ANOVAgroups) {
  186. if(corVolc & exists("CORout")) { cat("- corVolc=TRUE so getting gene lists from significant correlations in statistics table stored in variable CORout.\n"); ANOVAout<-CORout; }
  187. if(corVolc & !exists("CORout")) if (!length(dummyVar)==1) { cat("- corVolc=TRUE, but CORout not in memory, using input provided to this function.\n"); ANOVAout=as.data.frame(dummyVar); } else { stop("Variable CORout not found or no input was provided.\nPlease run trait.corStat() function first, and save output to CORout variable or pass its output to this function.\n\n") }
  188. if (!exists("ANOVAout")) if (!length(dummyVar)==1) { cat("- ANOVAout not in memory, trying to use input provided to this function.\n"); ANOVAout=as.data.frame(dummyVar); } else { stop("Variable ANOVAout not found or no input was provided.\nPlease run parANOVA.dex() or trait.corStat() function first, and save output to ANOVAout variable or pass its output to this function.\n\n") }
  189. if (!ncol(ANOVAout)>3) stop("\n\nInput or ANOVAout variable contents are not a data (frame) with at least 4 columns. It is not valid output from the parANOVA.dex() or trait.corStat() function.\n Please run one of these functions first.\n\n")
  190. numberOfNonComparisonColumns=length(colnames(ANOVAout)) - if(!corVolc) { length(which(grepl("^diff ",colnames(ANOVAout))))*2 } else { length(which(grepl("^p ",colnames(ANOVAout))))*2 }
  191. numComp <- (length(colnames(ANOVAout)) - numberOfNonComparisonColumns) / 2 # of columns separating comparisons from matched column of log2(diffs), i.e. # of comparisons
  192. if (!exists("testIndexMasterList")) {
  193. if(exists("selectComps")) { cat("- Volcano (plotVolc) selection of pairwise comparisons in ANOVAout may apply from the variable selectComps.\n"); testIndexMasterList=selectComps; } else { cat("- No comparison p value columns previously selected by running plotVolc(). Using ALL comparisons.\n"); testIndexMasterList="ALL"; }
  194. }
  195. if (max(testIndexMasterList)>numComp+2 | min(testIndexMasterList)<3) {
  196. cat(" - Selected comparison p value columns numbers may not reference valid integer p value column indexes of ANOVAout (or CORout).\n Output will be for all valid comparisons or correlation(s).\n")
  197. testIndexMasterList="ALL"
  198. }
  199. if (testIndexMasterList[1]=="ALL" | testIndexMasterList[1]=="all" | testIndexMasterList[1]=="All") testIndexMasterList=c(3:(numComp+2))
  200. testIndexMasterList=as.integer(testIndexMasterList)
  201. if(corVolc & exists("flip")) { cat("- Significant groups were defined by trait corrrelation. Variable flip will be ignored so positive correlations remain positive.\n"); flip=c(); }
  202. if(!exists("flip")) { cat("- No comparisons selected for flipping numerator and denominator. Variable flip=c().\n"); flip=c(); }
  203. if(!exists("dexComps") | !exists("comparisonIDs")) {
  204. cat("- Extracting names of groups being compared in ANOVAout table from p value column headers...\n")
  205. dexComps <- list()
  206. iter <- length(testIndexMasterList) + 1
  207. comparisonIDs <- data.frame(dfVariable = rep(NA, length(testIndexMasterList)), Comparison = rep(NA, length(testIndexMasterList)))
  208. for (i in testIndexMasterList) {
  209. iter <- iter - 1
  210. if(!corVolc) {
  211. comparisonIDs[iter, ] <- as.vector(c(paste0("dexTargets.", gsub("-", ".", colnames(ANOVAout)[i])), paste0(as.character(gsub("-", " vs ", colnames(ANOVAout)[i])))))
  212. } else {
  213. comparisonIDs[iter, ] <- as.vector(c(paste0("dexTargets.", gsub("^p ", "", colnames(ANOVAout)[i])), paste0(as.character(gsub("^(.*)[' '](.*)\\.(.*)$", "\\1 (\\2 in \\3 samples)", colnames(ANOVAout)[i+numComp])))))
  214. }
  215. dexComps[[comparisonIDs[iter, 1] ]] <- ANOVAout
  216. if (!is.na(match(i, flip))) {
  217. dexComps[[comparisonIDs[iter, 1] ]][, i + numComp] <- -1 * as.numeric(dexComps[[comparisonIDs[iter, 1] ]][, i + numComp])
  218. comparisonIDs[iter, 2] <- gsub("(*.*) vs (*.*)", "\\2 vs \\1", comparisonIDs[iter, 2]) # flip label "vs" in comParisonIDs$Comparison[iter]
  219. }
  220. }
  221. }
  222. # comparisonIDs # list element names and Logical comparisons for those retrievable Dex measurements in the list elements
  223. # ls(dexComps) # list elements are dataframes with the DEX entries for that comparison
  224. ANOVAout$Symbol <- suppressWarnings(do.call("rbind", strsplit(as.character(rownames(ANOVAout)), "[|]"))[, 1])
  225. if(length(which(grepl(";",ANOVAout$Symbol)))>0) {
  226. cat("- *Found some gene symbols have semicolons! Splitting these and keeping only symbol *before* semicolon.\n")
  227. ANOVAout$Symbol<-suppressWarnings(do.call("rbind",strsplit(as.character(ANOVAout$Symbol), "[;]"))[,1])
  228. }
  229. cat("\n")
  230. if (!exists("sigThresh")) if(exists("sigVolcCutoff")) { sigThresh=sigVolcCutoff } else { sigThresh=0.05 }
  231. print(paste0("...Applying a minimum p value cutoff of ",sigThresh," for ",if(corVolc) { "correlation statistics" } else { "ANOVA" }, " lists..."))
  232. if(corVolc & exists("FCmin")) { cat("- Using correlation significance for defining groups. Variable FCmin will be ignored.\n"); FCmin=0; }
  233. if (!exists("FCmin")) FCmin=0
  234. cutoff=log2(1+FCmin)
  235. # shows what your cutoff for log2(FC) calculates as
  236. if(!corVolc) { print(paste0("...Applying a ", FCmin*100,"% minimum fold change threshold at + and - x=", signif(cutoff,2),"...")) }
  237. DEXlistsForGO<-list()
  238. iter=0
  239. for (i in testIndexMasterList) {
  240. iter=iter+1;
  241. j=paste0(gsub(" ",".",comparisonIDs$Comparison[iter]),".down")
  242. k=paste0(gsub(" ",".",comparisonIDs$Comparison[iter]),".up")
  243. if (length(intersect(i,flip))==1) {
  244. #flipped sign (all diffs >cutoff for down)
  245. DEXlistsForGO[[j]]<-ANOVAout$Symbol[which(ANOVAout[,i]<sigThresh & ANOVAout[,i+numComp]> cutoff)]
  246. DEXlistsForGO[[k]]<-ANOVAout$Symbol[which(ANOVAout[,i]<sigThresh & ANOVAout[,i+numComp]< -cutoff)]
  247. } else {
  248. #do not flip sign (all diffs < -cutoff for down)
  249. DEXlistsForGO[[j]]<-ANOVAout$Symbol[which(ANOVAout[,i]<sigThresh & ANOVAout[,i+numComp]< -cutoff )]
  250. DEXlistsForGO[[k]]<-ANOVAout$Symbol[which(ANOVAout[,i]<sigThresh & ANOVAout[,i+numComp]> cutoff)]
  251. }
  252. }
  253. #write lists to GOElite input files, and also the background file
  254. for (i in names(DEXlistsForGO)) {
  255. dfGO<-data.frame(GeneSymbol=DEXlistsForGO[[i]],SystemCode=rep("Sy",length(DEXlistsForGO[[i]])))
  256. if(outputGOeliteInputs) write.table(unique(dfGO),file=paste(filePath,outFilename,"/",i,".txt",sep=""),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  257. }
  258. #write background
  259. background <- unique(ANOVAout$Symbol)
  260. background <- cbind(background,rep("Sy",length=length(background)))
  261. colnames(background) <- c("GeneSymbol","SystemCode")
  262. if(outputGOeliteInputs) dir.create(file.path(paste0(filePath,outFilename),"background"))
  263. if(outputGOeliteInputs) write.table(background,paste0(filePath,outFilename,"/background/background.txt"),row.names=FALSE,col.names=TRUE,quote=FALSE,sep="\t")
  264. nModules=length(names(DEXlistsForGO))
  265. WGCNAinput=FALSE
  266. } else { #NOT creating lists from ANOVA/volcano up & down groups
  267. ##1b. Organize input gene lists from the WGCNA modules, either from specified input file or in the current cleanDat, net, and kME table
  268. ### List-building from WGCNA-defined modules ###
  269. ##use data structures in memory if modulesInMemory=TRUE; otherwise read csv following the template which is written in an earlier R session and edited in excel to produce module membership table saved as .csv:
  270. if (modulesInMemory) {
  271. modulesData <- as.data.frame(cbind(rownames(cleanDat),NETcolors))
  272. WGCNAinput=TRUE
  273. } else {
  274. modulesData <- read.csv(paste(filePath,inputFile,sep=""),header=TRUE, sep=",");
  275. # check if this is a WGCNA modules/kME table containing net.colors, or NETcolors column; (otherwise it is assumed to be simple list input)
  276. if("net.colors" %in% colnames(modulesData)) {
  277. WGCNAinput=TRUE
  278. # .csv column before colors is assumed to contain Unique IDs (Symbol|...) unless colors are in first column; then it's assumed to be in column following colors
  279. NETcolors.idx=which(colnames(modulesData) %in% "net.colors")[1]
  280. if(NETcolors.idx==1) { modulesData <- as.data.frame(modulesData[,c(NETcolors.idx+1,NETcolors.idx)]); } else { modulesData <- as.data.frame(modulesData[,c(NETcolors.idx-1,NETcolors.idx)]); }
  281. } else {
  282. if("NETcolors" %in% colnames(modulesData)) {
  283. WGCNAinput=TRUE
  284. # .csv column before colors is assumed to contain Unique IDs (Symbol|...) unless colors are in first column; then it's assumed to be in column following colors
  285. NETcolors.idx=which(colnames(modulesData) %in% "NETcolors")[1]
  286. if(NETcolors.idx==1) { modulesData <- as.data.frame(modulesData[,c(NETcolors.idx+1,NETcolors.idx)]); } else { modulesData <- as.data.frame(modulesData[,c(NETcolors.idx-1,NETcolors.idx)]); }
  287. } else {
  288. WGCNAinput=FALSE
  289. }
  290. }
  291. }
  292. if (WGCNAinput) {
  293. suppressPackageStartupMessages(require(WGCNA,quietly=TRUE)) #for labels2colors
  294. # Include column with Symbol (if it is gene symbol, if not use appropriate code as given in GO-Elite manual)
  295. modulesData$SystemCode <- rep("Sy",nrow(modulesData))
  296. # Assign Names of First columns, in case they are non standard
  297. colnames(modulesData)[1]<-"Unique.ID" #This should have Symbol|UniprotID
  298. colnames(modulesData)[2]<-"net.colors" #This should have colors
  299. #Split out symbols from UniprotIDs, keep symbols in column 1
  300. rownames(modulesData)<-modulesData$Unique.ID
  301. modulesData$Unique.ID<-suppressWarnings(do.call("rbind",strsplit(as.character(modulesData$Unique.ID), "[|]"))[,1])
  302. if(length(which(grepl(";",modulesData$Unique.ID)))>0) {
  303. cat("- *Found some gene symbols have semicolons! Splitting these and keeping only symbol *before* semicolon.\n")
  304. modulesData$Unique.ID<-suppressWarnings(do.call("rbind",strsplit(as.character(modulesData$Unique.ID), "[;]"))[,1])
  305. }
  306. ## Creating background file for GO Elite analysis
  307. background <- unique(modulesData[,"Unique.ID"])
  308. background <- cbind(background,rep("Sy",length=length(background)))
  309. colnames(background) <- c("GeneSymbol","SystemCode")
  310. if(outputGOeliteInputs) dir.create(file.path(paste0(filePath,outFilename),"background"))
  311. if(outputGOeliteInputs) write.table(background,paste0(filePath,outFilename,"/background/background.txt"),row.names=FALSE,col.names=TRUE,quote=FALSE,sep="\t")
  312. # Separate into independent module txt files for analysis by GO-Elite (CREATE INPUT FILES)
  313. greySubtractor=if(length(which(modulesData$net.colors=="grey"))>0) { 1 } else { 0 } #remove grey from count of modules
  314. nModules <- length(unique(modulesData$net.colors))-greySubtractor
  315. moduleColors <- uniquemodcolors <- labels2colors(c(1:nModules))
  316. for (i in 1:length(moduleColors)) {
  317. moduleName <- moduleColors[i]
  318. ind <- which(colnames(modulesData) == gsub("kMEME","kME",paste("kME",moduleName,sep="")))
  319. moduleInfo <- modulesData[modulesData$net.colors == gsub("ME","",moduleName), c(1,ncol(modulesData),ind)]
  320. colnames(moduleInfo) <- c("GeneSymbol","SystemCode") #,"kME")
  321. if (moduleName == "blue" | moduleName == "brown" | moduleName == "green" | moduleName == "cyan") { if(outputGOeliteInputs) write.table(moduleInfo,file=paste(filePath,outFilename,"/",moduleName,"_2_Module.txt",sep=""),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  322. } else {
  323. if(outputGOeliteInputs) write.table(unique(moduleInfo),file=paste(filePath,outFilename,"/",moduleName,"_Module.txt",sep=""),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  324. }
  325. }
  326. } else { #input is not WGCNA kME table format
  327. ##1c. List building from the columns of a user-specified .csv input file, which must be in a column-wise list format, and including longest such list as background.
  328. # We process the input file as simple lists by column in the CSV (largest list used as background)
  329. #reread the file to a list of gene symbol (or UniqueID) lists
  330. modulesData <- as.list(read.csv(paste(filePath,inputFile, sep=""),sep=",", stringsAsFactors=FALSE,header=T))
  331. nModules <- length(names(modulesData))
  332. semicolonsFound=FALSE
  333. for (a in 1:nModules) {
  334. modulesData[[a]] <- unique(modulesData[[a]][modulesData[[a]] != ""])
  335. modulesData[[a]] <- modulesData[[a]][!is.na(modulesData[[a]])]
  336. modulesData[[a]] <- suppressWarnings(do.call("rbind",strsplit(as.character(modulesData[[a]]), "[|]"))[,1])
  337. if(length(which(grepl(";",modulesData[[a]])))>0) {
  338. modulesData[[a]]<-suppressWarnings(do.call("rbind",strsplit(as.character(modulesData[[a]]), "[;]"))[,1])
  339. }
  340. }
  341. if(semicolonsFound) cat("- *Found some gene symbols have semicolons! Splitting these and keeping only symbol *before* semicolon.\n")
  342. ## Creating background file for GO Elite analysis
  343. background <- modulesData[order(sapply(modulesData,length),decreasing=TRUE)][[1]]
  344. background <- unique(background)
  345. background <- cbind(background,rep("Sy",length=length(background)))
  346. colnames(background) <- c("GeneSymbol","SystemCode")
  347. if(outputGOeliteInputs) dir.create(file.path(paste0(filePath,outFilename),"background"))
  348. if(outputGOeliteInputs) write.table(background,paste0(filePath,outFilename,"/background/background.txt"),row.names=FALSE,col.names=TRUE,quote=FALSE,sep="\t")
  349. # Separate Symbol Lists into independent module txt files for analysis by GO-Elite (not performed by this script) (CREATE INPUT FILES)
  350. modulesData[[ names(modulesData[order(sapply(modulesData,length),decreasing=TRUE)])[1] ]] <- NULL
  351. nModules = nModules -1 #no background
  352. listNames <- uniquemodcolors <- names(modulesData)
  353. for (i in listNames) {
  354. listName <- i
  355. listInfo <- cbind(modulesData[[listName]],rep("Sy",length=length(modulesData[[listName]])))
  356. colnames(listInfo) <- c("GeneSymbol","SystemCode")
  357. if(outputGOeliteInputs) write.table(unique(listInfo),file=paste(filePath,outFilename,"/",listName,".txt",sep=""),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  358. }
  359. } #end if (WGCNAinput)
  360. } #end else for if (ANOVAgroups)
  361. ##2. GSA FET (parallelized within R, must have parallelThreads>1 to work currently)
  362. ####----------------------- piano package and dependencies required ------------------------------------#####
  363. suppressPackageStartupMessages(require(piano,quietly=TRUE))
  364. ## Adapted version of piano::runGSAhyper() function with depletion p value also calculated (for signed Z score if we will use it to cocluster by module, e.g.)
  365. runGSAhyper.twoSided <- function(genes, pvalues, pcutoff, universe, gsc, gsSizeLim = c(1,Inf), adjMethod = "fdr") {
  366. if (length(gsSizeLim) != 2)
  367. stop("argument gsSizeLim should be a vector of length 2")
  368. if (missing(genes)) {
  369. stop("argument genes is required")
  370. } else {
  371. genes <- as.vector(as.matrix(genes))
  372. if (!is(genes, "character"))
  373. stop("argument genes should be a character vector")
  374. if (length(unique(genes)) != length(genes))
  375. stop("argument genes should contain no duplicated entries")
  376. }
  377. if (missing(pvalues)) {
  378. pvalues <- rep(0, length(genes))
  379. } else {
  380. pvalues <- as.vector(as.matrix(pvalues))
  381. if (!is(pvalues, "numeric"))
  382. stop("argument pvalues should be a numeric vector")
  383. if (length(pvalues) != length(genes))
  384. stop("argument pvalues should be the same length as argument genes")
  385. if (max(pvalues) > 1 | min(pvalues) < 0)
  386. stop("pvalues need to lie between 0 and 1")
  387. }
  388. if (missing(pcutoff)) {
  389. if (all(pvalues %in% c(0, 1))) {
  390. pcutoff <- 0
  391. } else {
  392. pcutoff <- 0.05
  393. }
  394. } else {
  395. if (length(pcutoff) != 1 & !is(pcutoff, "numeric"))
  396. stop("argument pcutoff should be a numeric of length 1")
  397. if (max(pcutoff) > 1 | min(pcutoff) < 0)
  398. stop("argument pcutoff needs to lie between 0 and 1")
  399. }
  400. if (missing(gsc)) {
  401. stop("argument gsc needs to be given")
  402. # } else {
  403. # if (!is(gsc, "GSC"))
  404. # stop("argument gsc should be of class GSC, as returned by the loadGSC function") # disabled since the list we create is not of GSC class
  405. }
  406. if (missing(universe)) {
  407. if (!all(pvalues == 0)) {
  408. universe <- genes
  409. message("Using all genes in argument genes as universe.")
  410. } else {
  411. universe <- unique(unlist(gsc$gsc))
  412. message("Using all genes present in argument gsc as universe.")
  413. }
  414. } else {
  415. if (!is(universe, "character"))
  416. stop("argument universe should be a character vector")
  417. if (!all(pvalues == 0))
  418. stop("if universe is given, genes should be only the genes of interest, i.e. pvalues should all be set to 0.")
  419. }
  420. if (!all(unique(unlist(gsc$gsc)) %in% universe))
  421. warning("there are genes in gsc that are not in the universe, these will be removed before analysis")
  422. if (!all(genes %in% universe)) {
  423. warning("not all genes given by argument genes are present in universe, these will be added to universe")
  424. universe <- c(universe, genes[!genes %in% universe])
  425. }
  426. if (length(unique(universe)) != length(universe))
  427. stop("argument universe should contain no duplicated entries")
  428. tmp <- try(adjMethod <- match.arg(adjMethod, c("holm",
  429. "hochberg", "hommel", "bonferroni",
  430. "BH", "BY", "fdr", "none"), several.ok = FALSE),
  431. silent = TRUE)
  432. if (is(tmp, "try-error")) {
  433. stop("argument adjMethod set to unknown method")
  434. }
  435. pvalues[pvalues == 0] <- -1e-10
  436. goi <- genes[pvalues < pcutoff]
  437. if (length(goi) < 1) {
  438. cat("\nrunGSEAhyper: no genes selected due to too strict pcutoff. (no genes of interest made an input list)\n")
  439. res<-list()
  440. res$resTab <- NA
  441. res$gsc <- NA
  442. return(res)
  443. }
  444. bg <- universe[!universe %in% goi]
  445. gsc <- gsc$gsc
  446. delInd <- vector()
  447. for (i in 1:length(gsc)) {
  448. gs <- gsc[[i]]
  449. gs <- gs[gs %in% universe]
  450. if (length(gs) < gsSizeLim[1] | length(gs) > gsSizeLim[2])
  451. delInd <- c(delInd, i)
  452. gsc[[i]] <- gs
  453. }
  454. gsc <- gsc[!c(1:length(gsc)) %in% delInd]
  455. message(paste("Analyzing the overrepresentation of ",
  456. length(goi), " genes of interest in ", length(gsc),
  457. " gene sets, using a background of ", length(bg),
  458. " non-interesting genes.", sep = ""))
  459. p <- p.depletion <- rep(NA, length(gsc))
  460. names(p) <- names(p.depletion) <- names(gsc)
  461. padj <- rep(NA, length(gsc))
  462. names(padj) <- names(gsc)
  463. contTabList <- list()
  464. resTab <- matrix(nrow = length(gsc), ncol = 8) #added 8th column to hold "Genes.Hit"
  465. colnames(resTab) <- c("Pvalue.Enrichment", "Adjusted.Enr.Pvalue", "Pvalue.Depletion",
  466. "Significant (in gene set)", "Non-significant (in gene set)",
  467. "Significant (not in gene set)", "Non-significant (not in gene set)", "Genes.Hit")
  468. rownames(resTab) <- names(gsc)
  469. for (i in 1:length(gsc)) {
  470. gs <- gsc[[i]]
  471. nogs <- universe[!universe %in% gs]
  472. ctab <- rbind(c(sum(goi %in% gs), sum(goi %in% nogs)),
  473. c(sum(bg %in% gs), sum(bg %in% nogs)))
  474. p[i] <- fisher.test(ctab, alternative = "greater")$p.value
  475. p.depletion[i] <- fisher.test(ctab, alternative = "less")$p.value
  476. rownames(ctab) <- c("Significant", "Non-significant")
  477. colnames(ctab) <- c("Genes in gene set", "Genes not in gene set")
  478. contTabList[[i]] <- ctab
  479. resTab[i, ] <- c(p[i], NA, p.depletion[i], sum(goi %in% gs), sum(bg %in%
  480. gs), sum(goi %in% nogs), sum(bg %in% nogs), paste0(goi[goi %in% gs],collapse=";")) #*** added semicolon separated Genes.Hit to 8th column
  481. }
  482. padj.greater <- p.adjust(p, method = adjMethod)
  483. resTab[, 2] <- padj.greater
  484. res <- list()
  485. res$pvalues.greater <- p
  486. res$p.adj.greater <- padj.greater
  487. res$pvalues.depletion <- p.depletion
  488. res$resTab <- resTab #*** includes Genes.Hit in 8th column.
  489. res$contingencyTable <- contTabList
  490. res$gsc <- gsc
  491. return(res)
  492. }
  493. ## Set up parallel backend.
  494. suppressPackageStartupMessages(require("doParallel",quietly=TRUE))
  495. clusterLocal <- makeCluster(c(rep("localhost",parallelThreads)) ) #,type="SOCK") <- may cause malloc error on macOS/non-windows."PSOCK" or leave off...
  496. registerDoParallel(clusterLocal)
  497. ## Load GMT file; Clean UTF-8 characters (since Dec 2023); Write clean.GMT back out
  498. #GMT.df <- read.delim(GMTdatabaseFile, encoding = "utf-8",quote="", sep="\t",header=FALSE)
  499. GMT.df <- readLines(con <- file(GMTdatabaseFile, encoding = "utf-8"))
  500. close(con)
  501. GMT.df <- unlist(sapply(GMT.df, function(x) iconv(gsub("^(PMC\\d*__.+?)\\\t(.*)$","\\1%PMC%\\2",
  502. gsub("\\\"","", gsub("\\x83\\x80.","-",x) )),
  503. "utf-8","ASCII", "")))
  504. names(GMT.df)<-NULL
  505. GMT.df <- lapply(GMT.df, function(x) stringr::str_split_fixed(x, pattern="\t", n=Inf))
  506. # Create list object that is identical to a GSC class object, just not of this class, since not loaded by the loadGSC() function in piano package.
  507. GSCfromGMT<-list()
  508. GSCfromGMT[["addInfo"]]<-do.call(rbind, lapply(GMT.df, function(x) if(grepl("^PMC.*\\%PMC\\%",x[1])) { c(x[1],gsub("^(PMC.*)\\%PMC\\%.*$","\\1",x[1])) } else { x[c(1:2)] } ))
  509. GSCfromGMT[["gsc"]]<-lapply(GMT.df, function(x) if(grepl("^PMC.*\\%PMC\\%",x[1])) { x[c(2:length(x))][!x[c(2:length(x))]==""] } else { x[c(3:length(x))][!x[c(3:length(x))]==""] })
  510. names(GSCfromGMT$gsc)<-GSCfromGMT$addInfo[,1]
  511. # Time and memory overhead are too great to write and read back in a clean.GMT. We process the provided .GMT with UTF-8 and inconsistencies every time this script is run.
  512. #write.table(GMT.df,file="clean.GMT",sep='\t',quote=FALSE, col.names=FALSE, row.names=FALSE)
  513. #GSCfromGMT<-loadGSC(file="clean.GMT") # loadGSC(file=GMTdatabaseFile)
  514. ## Be sure cluster nodes for parallel processing inherit needed variables from both .GlobalEnv and current function environment (error seen in R 4.2.1 in RStudio on Windows).
  515. if(!exists("DEXlistsForGO")) DEXlistsForGO<-list()
  516. parallel::clusterExport(cl=clusterLocal, list("ANOVAgroups","WGCNAinput","background","DEXlistsForGO","GSCfromGMT"), envir=environment()) ## avoid error during foreach below: Error in { : task 1 failed - "object 'ANOVAgroups' not found"
  517. ## Output piano package GSA FET output tables as list assembly
  518. GSA.FET.outlist<-list()
  519. # colnames(modulesData)[3:(ncol(modulesData)-1)]
  520. if (ANOVAgroups) uniquemodcolors=names(DEXlistsForGO) #otherwise, already set above.
  521. # parallelized to speed up.
  522. cat("\nRunning FET overlap statistics in parallel for ",length(uniquemodcolors)," symbol lists using up to ", parallelThreads," threads...\n\n")
  523. #for (this.geneList in uniquemodcolors) {
  524. GSA.FET.outlist <- foreach(this.geneList=uniquemodcolors) %dopar% {
  525. # this.geneList=uniquemodcolors[i]
  526. zeroToKeep.idx= if (WGCNAinput) { which( background[,"GeneSymbol"] %in% unique(modulesData[which(modulesData$net.colors==this.geneList),"Unique.ID"]) ) } else {
  527. if(ANOVAgroups) { which( background[,"GeneSymbol"] %in% DEXlistsForGO[[this.geneList]] ) } else {
  528. which( background[,"GeneSymbol"] %in% unique(modulesData[[this.geneList]]) ) }} #Handles file-based input modulesData
  529. zeroToKeep=rep(1,nrow(background))
  530. zeroToKeep[zeroToKeep.idx]<- 0
  531. cat( paste0("\n",this.geneList, "... ") ) #" (n=",length(zeroToKeep.idx)," gene symbols) now processing: ") )
  532. thislist <- runGSAhyper.twoSided(genes=background[which(zeroToKeep==0),"GeneSymbol"],universe=background[,"GeneSymbol"],gsc=GSCfromGMT,gsSizeLim=c(minHitsPerOntology,Inf),adjMethod="BH",)
  533. #above line runs in time, ~30 sec/list, or 50 sec/list for .twoSided
  534. #list [[this.color]][["pvalues.greater"]] is enrichment p value vector
  535. #list [[this.color]][["padj.greater"]] is FDR vector
  536. #list [[this.color]][["resTab"]] is same-ordered (rows) matrix of: p-value, FDR, ... with rownames equal to the ontology name%ontology type%OntologyID
  537. #list [[this.color]][["pvalues.less"]] is depletion p value vector (relevant for signed Z score calculation), only in customized function
  538. return(list(thislist[["resTab"]], thislist[["gsc"]]))
  539. }
  540. stopCluster(clusterLocal)
  541. # re-combine list elements from two outputs, over all uniquemodcolors
  542. GSA.FET.resTab.list = do.call(list,lapply(GSA.FET.outlist,function(x){x[[1]]}))
  543. GSA.FET.genesByOntology.list = do.call(list,lapply(GSA.FET.outlist,function(x){x[[2]]}))
  544. names(GSA.FET.resTab.list) <- names(GSA.FET.genesByOntology.list) <- uniquemodcolors
  545. #Add signed Zscore, pull out ontologyType, ontology (description, title case)
  546. GSA.FET.outSimple <- lapply(GSA.FET.resTab.list, function(x) {
  547. if(is.na(x[1])) { #*** occurs when "no genes selected due to too strict pcutoff" in GSEA-FET piano
  548. NA
  549. } else {
  550. #PMC\\d*__F\\d* ontologyTypes for PMC gene sets added December 2023 -- collapse to ontologyType "PMC"
  551. #rownames(x)=gsub("^(PMC\\d*__.*)\\t(.*)\\t(.*)",iconv("\\2%PMC%\\1\\t\\3", "latin1","ASCII", ""),rownames(x))
  552. ontology=stringr::str_to_title(gsub("\\%WIKIPATHWAYS_\\d*","", gsub("\\%WP_\\d*","", gsub("\\&(.*);","\\1",gsub("<\\sI>","",gsub("<I>","", gsub("(.*)\\%.*\\%.*","\\1",rownames(x))))))))
  553. ontologyType=gsub("^WP\\d*","WikiPathways", gsub(".*\\%(.*)\\%.*","\\1",rownames(x)))
  554. #force all caps for ontologyType of GObp GOmf GOcc (changed in downloaded GMT files Sept 2022 and/or different in mouse GMT compared to human)
  555. ontologyType=gsub("GObp","GOBP",ontologyType)
  556. ontologyType=gsub("GOmf","GOMF",ontologyType)
  557. ontologyType=gsub("GOcc","GOCC",ontologyType)
  558. ZscoreSign=rep(1,nrow(x))
  559. ZscoreSign[ as.numeric(x[,"Pvalue.Depletion"]) < as.numeric(x[,"Pvalue.Enrichment"]) ] <- -1
  560. Zscore=apply(x, 1, function(p) qnorm(min(as.numeric(p["Pvalue.Enrichment"]), as.numeric(p["Pvalue.Depletion"]))/2, lower.tail=FALSE))
  561. out=as.data.frame(x)
  562. out$Zscore=Zscore*ZscoreSign
  563. out$ontologyType=ontologyType
  564. out$ontology=ontology
  565. out
  566. }
  567. })
  568. #head(GSA.FET.outSimple[[1]])
  569. ## Available Ontology Types
  570. # data.frame(x=table( gsub("^WP\\d*","WikiPathways",gsub(".*\\%(.*)\\%.*","\\1",rownames(GSA.FET.resTab.list[[1]])))))
  571. ##Circa 2022
  572. # x.Var1 x.Freq
  573. #1 BIOCYC 99
  574. #2 GOBP 6432
  575. #3 GOCC 980
  576. #4 GOMF 2015
  577. #5 HUMANCYC 2
  578. #6 IOB 33
  579. #7 MSIGDB_C2 395
  580. #8 PANTHER PATHWAY 92
  581. #9 PATHWAY INTERACTION DATABASE NCI-NATURE CURATED DATA 198
  582. #10 PATHWHIZ 320
  583. #11 REACTOME 744
  584. #12 REACTOME DATABASE ID RELEASE 80 795
  585. #13 SMPDB 371
  586. #14 WikiPathways 529
  587. ##Dec2023
  588. # data.frame(x=table( gsub("^PMC\\d*__.*","PMC", gsub("^WP\\d*","WikiPathways",gsub(".*\\%(.*)\\%.*","\\1",rownames(GSA.FET.resTab.list[[1]]))))))
  589. # data.frame(x=table( GSA.FET.outSimple[[1]]$ontologyType ))
  590. # x.Var1 x.Freq
  591. #1 BIOCYC 94
  592. #2 GOBP 6518
  593. #3 GOCC 784
  594. #4 GOMF 1704
  595. #5 HUMANCYC 3
  596. #6 IOB 34
  597. #7 MSIGDB_C2 415
  598. #8 MSIGDBHALLMARK 50
  599. #9 PANTHER PATHWAY 89
  600. #10 PATHWAY INTERACTION DATABASE NCI-NATURE CURATED DATA 201
  601. #11 PATHWHIZ 260
  602. #12 REACTOME 843
  603. #13 REACTOME DATABASE ID RELEASE 38 802
  604. #14 SMPDB 290
  605. #15 WikiPathways 632
  606. ##Jan2024
  607. # data.frame(x=table( GSA.FET.outSimple[[1]]$ontologyType ))
  608. # note: ontologies kept from input GMT is dependent on background overlap with 'universe' of genes in the GMT -- this is for a smaller background of ~1100 symbols
  609. # x.Var1 x.Freq
  610. #1 BIOCYC 9
  611. #2 GOBP 2426
  612. #3 GOCC 335
  613. #4 GOMF 573
  614. #5 IOB 4
  615. #6 MSIGDB_C2 38
  616. #7 MSIGDBHALLMARK 35
  617. #8 PANTHER PATHWAY 13
  618. #9 PATHWAY INTERACTION DATABASE NCI-NATURE CURATED DATA 53
  619. #10 PATHWHIZ 31
  620. #11 PMC 880
  621. #12 REACTOME 188
  622. #13 REACTOME DATABASE ID RELEASE 65 191
  623. #14 SMPDB 37
  624. #15 WikiPathways 122
  625. ##3. Output Report of Z-Score Barplots, processing all GSA FET output tables
  626. ############################# ----------------------Plotting for modules ------------------------#######################
  627. ######## this script plots the top 3 ontologies for biological process, mol function and cell component for each module
  628. ##color scheme for ontology type key/legend (can be changed in user parameters, editing the "color" vector)
  629. ontologyTypes=c("Biological Process","Molecular Function","Cellular Component","Reactome","WikiPathways","MSIG.C2") # ,"PMC")
  630. if(ANOVAgroups) {
  631. xlabels <- names(DEXlistsForGO)
  632. xlabels.frame <- data.frame(Colors=rep(NA,length(xlabels)),Labels=xlabels)
  633. uniquemodcolors <- names(DEXlistsForGO) #not set above
  634. } else {
  635. xlabels <- uniquemodcolors #labels2colors(c(1:nModules))
  636. xlabels1 <- paste("M",seq(1:nModules),sep="")
  637. xlabels.frame <- as.data.frame(data.frame(Colors=xlabels,Labels=paste0(xlabels1," ",xlabels)))
  638. }
  639. if (!removeRedundantGOterms=="kappa") {
  640. if(removeRedundantGOterms) {
  641. suppressPackageStartupMessages(require(ontologyIndex))
  642. ontology.index<-get_OBO(file=GO.OBOfile,extract_tags="everything")
  643. #below function minimal_set uses ancestors list to dereplicate; what about using synonyms list from our go.obo as ancestors?
  644. }
  645. } else { # removeRedundantGOterms=="kappa"
  646. # --- Cohen's kappa clustering on gene-set membership -------------------------
  647. kappa_cluster_ontologies <- function(gene_sets_present, kappa_cut = 0.30,
  648. method = c("hier", "graph")) {
  649. method <- match.arg(method, c("hier","graph"))
  650. # Normalize & validate
  651. terms <- names(gene_sets_present)
  652. if (is.null(terms)) stop("gene_sets_present must be a *named* list.")
  653. gene_sets_present <- lapply(gene_sets_present, function(x) unique(as.character(x)))
  654. n <- length(gene_sets_present)
  655. if (n == 0L) return(data.frame(term = character(0), cluster = integer(0)))
  656. if (n == 1L) return(data.frame(term = terms, cluster = 1L))
  657. # Universe & encodings
  658. U <- sort(unique(unlist(gene_sets_present)))
  659. N <- length(U)
  660. idx_map <- setNames(seq_along(U), U)
  661. sets_idx <- lapply(gene_sets_present, function(gs) if (length(gs)) sort(idx_map[gs]) else integer(0))
  662. sizes <- vapply(sets_idx, length, integer(1))
  663. # If nothing in universe, just give unique clusters
  664. if (N == 0L || all(sizes == 0L)) {
  665. return(data.frame(term = terms, cluster = seq_len(n)))
  666. }
  667. # Pairwise intersections (upper triangle)
  668. intersec_mat <- matrix(0L, n, n)
  669. for (i in seq_len(n - 1L)) {
  670. gi <- sets_idx[[i]]
  671. for (j in (i + 1L):n) {
  672. intersec_mat[i, j] <- length(intersect(gi, sets_idx[[j]]))
  673. }
  674. }
  675. intersec_mat <- intersec_mat + t(intersec_mat)
  676. diag(intersec_mat) <- sizes
  677. # Cohen’s kappa
  678. A <- intersec_mat
  679. Bi <- matrix(sizes, n, n, byrow = FALSE) - A
  680. Cj <- matrix(sizes, n, n, byrow = TRUE) - A
  681. D <- N - (A + Bi + Cj)
  682. p0 <- (A + D) / max(N, 1L)
  683. pe <- (outer(sizes, sizes, "*") + outer(N - sizes, N - sizes, "*")) / max(N^2, 1L)
  684. denom <- pmax(1 - pe, .Machine$double.eps)
  685. K <- (p0 - pe) / denom
  686. diag(K) <- 1
  687. K[!is.finite(K)] <- 0
  688. if (method == "hier") {
  689. Dmat <- as.dist(pmax(0, 1 - K))
  690. hc <- hclust(Dmat, method = "average")
  691. cl <- cutree(hc, h = 1 - kappa_cut)
  692. data.frame(term = terms, cluster = as.integer(cl))
  693. } else {
  694. if (!requireNamespace("igraph", quietly = TRUE)) {
  695. stop("Please install 'igraph' for graph-based clustering, pruning via 'kappa'.")
  696. }
  697. adj <- (K >= kappa_cut)
  698. diag(adj) <- FALSE
  699. g <- igraph::graph_from_adjacency_matrix(adj, mode = "undirected", diag = FALSE)
  700. comp <- igraph::components(g)$membership
  701. data.frame(term = terms, cluster = as.integer(comp))
  702. }
  703. }
  704. # --- Prune a per-list table (tmp2) to 1 representative per cluster -----------
  705. # tmp2 must have columns: ontology, ontologyType, Zscore, Pvalue.Enrichment, Genes.Hit
  706. prune_ontologies_by_kappa <- function(tmp2, kappa_cut = 0.30, method = c("hier", "graph")) {
  707. method <- match.arg(method, c("hier","graph"))
  708. need <- c("ontology","ontologyType","Zscore","Pvalue.Enrichment","Genes.Hit")
  709. stopifnot(all(need %in% names(tmp2)))
  710. df <- tmp2
  711. df$ontology <- as.character(df$ontology)
  712. df$ontologyType <- as.character(df$ontologyType)
  713. df$Zscore <- suppressWarnings(as.numeric(df$Zscore))
  714. df$Pvalue.Enrichment <- suppressWarnings(as.numeric(df$Pvalue.Enrichment))
  715. # Split “Genes.Hit” into symbol vectors
  716. split_genes <- function(x) {
  717. if (is.na(x) || !nzchar(x)) return(character(0))
  718. parts <- trimws(unlist(strsplit(x, ";", fixed = TRUE)))
  719. parts[nzchar(parts)]
  720. }
  721. genes_list <- lapply(df$Genes.Hit, split_genes)
  722. df$.n_genes <- vapply(genes_list, length, integer(1))
  723. # Cluster **within each ontologyType**; keep 1 best per cluster
  724. keep_blocks <- lapply(split(seq_len(nrow(df)), df$ontologyType), function(ii) {
  725. sub <- df[ii, , drop = FALSE]
  726. gs <- lapply(sub$Genes.Hit, split_genes); names(gs) <- sub$ontology
  727. cl_df <- kappa_cluster_ontologies(gs, kappa_cut = kappa_cut, method = method)
  728. stopifnot(nrow(cl_df) == nrow(sub))
  729. sub$cluster <- cl_df$cluster
  730. reps <- do.call(rbind, lapply(split(seq_len(nrow(sub)), sub$cluster), function(jj) {
  731. block <- sub[jj, , drop = FALSE]
  732. # choose: most significant (lowest p), then fewest genes, then highest Z, then alpha
  733. o <- order(block$Pvalue.Enrichment, block$.n_genes, -block$Zscore, block$ontology, na.last = TRUE)
  734. block[o[1L], , drop = FALSE]
  735. }))
  736. reps
  737. })
  738. kept <- do.call(rbind, keep_blocks)
  739. kept <- kept[, c("ontology","ontologyType","Zscore","Pvalue.Enrichment","Genes.Hit")]
  740. rownames(kept) <- NULL
  741. kept
  742. }
  743. # Precompute kappa-pruned ontology tables for each list in parallel
  744. GSA.KAPPA <- NULL
  745. if (identical(removeRedundantGOterms, "kappa")) {
  746. clusterLocal <- makeCluster(c(rep("localhost",parallelThreads)) )
  747. registerDoParallel(clusterLocal)
  748. # Make sure workers see these
  749. # parallel::clusterExport(
  750. # cl = clusterLocal,
  751. # varlist = c("GSA.FET.outSimple", "minHitsPerOntology",
  752. # "kappa_cluster_ontologies", "prune_ontologies_by_kappa"),
  753. # envir = environment()
  754. # )
  755. cat("\nPerforming Cohen's kappa-based graph clustering (k<0.30) for all output ontology lists in parallel using up to ", parallelThreads," threads...\n")
  756. # igraph is needed (kappa pruning method="graph")
  757. GSA.KAPPA <- foreach(thismod = uniquemodcolors,
  758. .packages = c("igraph"),
  759. .export = c("kappa_cluster_ontologies", "prune_ontologies_by_kappa"),
  760. .errorhandling = "remove") %dopar% {
  761. tmp <- GSA.FET.outSimple[[thismod]]
  762. # Skip empty modules (occur if no genes passed the cutoff)
  763. if (is.null(tmp) || (is.na(tmp[[1]][1]))) return(NULL)
  764. # Keep rows with enough hits
  765. idx <- which(tmp[, "Significant (in gene set)"] >= minHitsPerOntology)
  766. if (!length(idx)) return(NULL)
  767. # Reconstruct tmp2 (the trimmed 5-column data.frame you used before)
  768. tmp2 <- tmp[idx, c("ontology","ontologyType","Zscore","Pvalue.Enrichment","Genes.Hit")]
  769. tmp2 <- tmp2[order(tmp2$Zscore, decreasing = TRUE), ]
  770. tmp2 <- tmp2[order(tmp2$ontologyType, decreasing = TRUE), ]
  771. # Prune via kappa
  772. prune_ontologies_by_kappa(tmp2, kappa_cut = 0.30, method = "graph")
  773. }
  774. names(GSA.KAPPA) <- uniquemodcolors
  775. }
  776. } # end: removeRedundantGOterms=="kappa"
  777. setwd(paste0(filePath,outFilename,"/"))
  778. redundancyRemovalTag=if(is.logical(removeRedundantGOterms)) { if(removeRedundantGOterms) { "-redundancyRemoved.OBOparent" } else { "" } } else if (removeRedundantGOterms=="kappa") { "-redundancyRemoved.Kbest" } else { "" }
  779. filenameFinal=paste0(outFilename,redundancyRemovalTag)
  780. if(!exists("pageDimensions")) pageDimensions=c(8.5,11)
  781. if(!exists("panelDimensions")) panelDimensions=c(3,2)
  782. if(!exists("color")) color=c("darkseagreen3","lightsteelblue1","lightpink4","goldenrod","darkorange","gold")
  783. #colors respectively for ontology Types:
  784. #"Biological Process","Molecular Function","Cellular Component","Reactome","WikiPathways","MSig.C2","PMC"
  785. if(!length(color)==6) color=c("darkseagreen3","lightsteelblue1","lightpink4","goldenrod","darkorange","gold")
  786. if(!exists("maxBarsPerOntology")) maxBarsPerOntology=5
  787. pdf(paste0("GSA-GO-FET_",filenameFinal,".pdf"),height=pageDimensions[2],width=pageDimensions[1])
  788. op <- par(mfrow=panelDimensions,oma=c(0,0,3,0))
  789. frame()
  790. legend(x="topleft",legend = ontologyTypes, fill=color, title=" ",cex=2,horiz=F,xpd=T)
  791. legend(x="topleft",legend = c(" "," "," "), title="Ontology Types",cex=2.5,horiz=F,xpd=T, bty='n', title.adj=1.4)
  792. #frame()
  793. minZ<-qnorm(0.05/2, lower.tail=FALSE)
  794. summary <- list()
  795. for(i in c(1:(length(uniquemodcolors)))){
  796. thismod=uniquemodcolors[i]
  797. tmp=GSA.FET.outSimple[[thismod]]
  798. # cat(unlist(tmp))
  799. if (is.na(tmp[[1]][1])) { #*** occurs when "no genes selected due to too strict pcutoff" in GSEA-FET piano -- error bypass; output empty frame for this comparison.
  800. moduleTitle <- xlabels.frame[i,"Labels"]
  801. frame();
  802. mtext(paste0(moduleTitle,"\n-no genes made it into input"), adj=0.5, line=1, cex=0.85, font=2);
  803. next;
  804. }
  805. filter.minHits.idx<-which(tmp[,"Significant (in gene set)"]>=minHitsPerOntology)
  806. if (length(tmp[,2]) == 0 | length(filter.minHits.idx) == 0) { frame(); next; }
  807. #tmp = tmp[filter.minHits.idx,c(11,10,9,1, 8)] ## Select GO-terms,GO-Type,Z-score,pValues (and previously/again, semicolon-separated gene Lists(8))
  808. tmp2 <- tmp[filter.minHits.idx, c("ontology","ontologyType","Zscore","Pvalue.Enrichment","Genes.Hit")]
  809. tmp2 = tmp2[order(tmp2$Zscore,decreasing=T),]
  810. tmp2 = tmp2[order(tmp2$ontologyType,decreasing=T),]
  811. if(identical(removeRedundantGOterms, "kappa")) {
  812. tmp2 <- GSA.KAPPA[[thismod]]
  813. if (is.null(tmp2) || !nrow(tmp2)) { frame(); next; }
  814. } else if (isTRUE(removeRedundantGOterms)) {
  815. #dim(tmp2[which((tmp2$ontologyType == "GOCC" | tmp2$ontologyType == "GOBP" | tmp2$ontologyType == "GOMF") & tmp2$Zscore>=minZ),])
  816. ##[1] 124 4
  817. go.full.tmp2<-tmp2[which((tmp2$ontologyType == "GOCC" | tmp2$ontologyType == "GOBP" | tmp2$ontologyType == "GOMF") & tmp2$Zscore>=minZ),]
  818. go.full.tmp2$Term=gsub("^.*\\%GO..\\%(GO:\\d*)$","\\1",rownames(go.full.tmp2))
  819. go.minimal.terms<-minimal_set(ontology.index, terms=go.full.tmp2$Term)
  820. #go.pruned.terms<-prune_descendants(ontology.index, roots=?, terms=)
  821. go.minimal.tmp2<-go.full.tmp2[which(go.full.tmp2$Term %in% go.minimal.terms),]
  822. tmp2$Zscore[which(tmp2$ontologyType=="GOCC" | tmp2$ontologyType=="GOBP" | tmp2$ontologyType=="GOMF")] <- 0 #keeps in all GO ontologies, but Z scores zeroed
  823. tmp2$Zscore[match(rownames(go.minimal.tmp2),rownames(tmp2))] <-go.minimal.tmp2$Zscore #put back Z scores of non-redundant ontologies, to be possibly kept below
  824. # tmp2<-rbind(tmp2,go.minimal.tmp2[,-ncol(go.minimal.tmp2)])
  825. tmp2 = tmp2[order(tmp2$Zscore,decreasing=T),]
  826. tmp2 = tmp2[order(tmp2$ontologyType,decreasing=T),]
  827. }
  828. tmp3 = tmp2[tmp2$ontologyType == "GOBP",][c(1:maxBarsPerOntology),]
  829. tmp3 = rbind(tmp3,tmp2[tmp2$ontologyType == "GOMF",][c(1:maxBarsPerOntology),] )
  830. tmp3 = rbind(tmp3,tmp2[tmp2$ontologyType == "GOCC",][c(1:maxBarsPerOntology),] )
  831. tmp3 = rbind(tmp3,tmp2[tmp2$ontologyType == "REACTOME",][c(1:maxBarsPerOntology),] )
  832. tmp3 = rbind(tmp3,tmp2[tmp2$ontologyType == "WikiPathways",][c(1:maxBarsPerOntology),] )
  833. tmp3 = rbind(tmp3,tmp2[tmp2$ontologyType == "MSIGDB_C2",][c(1:maxBarsPerOntology),] )
  834. # tmp3 = rbind(tmp3,tmp2[tmp2$ontologyType == "PMC",][c(1:maxBarsPerOntology),] ) # PMCid(s) of publication-associated gene lists enriched in your input; Bader Lab added these entries to GMT files starting December 2023.
  835. tmp3 <- na.omit(tmp3)
  836. tmp3 <- tmp3[which(tmp3$Zscore>=minZ),]
  837. # tmp3 <- tmp3[order(tmp3$Zscore,decreasing=T),] #added this row, if you want to mix ontology types and sort by Z.Score only
  838. tmp3 <- tmp3[rev(rownames(tmp3)),]
  839. summary[[i]] <- tmp3
  840. moduleTitle <- xlabels.frame[i,"Labels"]
  841. if (is.na(tmp3$ontologyType[1])) { frame(); mtext(paste0(moduleTitle,"\n-no terms hit"), adj=0.5, line=1, cex=0.85, font=2); next; } #*** occurs when "no genes selected due to too strict pcutoff" in GSEA-FET piano -- error bypass; output empty frame for this comparison.
  842. ### To color bars by mol function, cell component or biological process
  843. for (j in 1:nrow(tmp3)){
  844. if (tmp3$ontologyType[j] == "GOMF"){
  845. tmp3$color[j] <- color[2]
  846. } else if (tmp3$ontologyType[j] == "GOCC"){
  847. tmp3$color[j] <- color[3]
  848. } else if (tmp3$ontologyType[j] == "GOBP"){
  849. tmp3$color[j] <- color[1]
  850. } else if (tmp3$ontologyType[j] == "REACTOME"){
  851. tmp3$color[j] <- color[4]
  852. } else if (tmp3$ontologyType[j] == "WikiPathways"){
  853. tmp3$color[j] <- color[5]
  854. } else if (tmp3$ontologyType[j] == "MSIGDB_C2"){
  855. tmp3$color[j] <- color[6]
  856. # } else if (tmp3$ontologyType[j] == "PMC"){
  857. # tmp3$color[j] <- color[7]
  858. }
  859. # tmp3$color[j] <- uniquemodcolors[i] #module color for all bars, instead of different colors by ontology type
  860. }
  861. if (tmp3$Zscore[1] == F) { frame(); mtext(paste0(moduleTitle,"\n-no terms hit"), adj=0.5, line=1, cex=0.85, font=2); next; }
  862. par(mar=c(4,15,4,3))
  863. xlim <- c(0,1.1*max(tmp3$Zscore))
  864. xh <- barplot(tmp3$Zscore,horiz = TRUE,width =0.85,las=1,main=moduleTitle, xlim=xlim,col=tmp3$color,cex.axis=0.7,xlab="Z Score",cex.lab=0.9,cex.main=0.95,ylim=c(0,nrow(tmp3)+0.8))
  865. abline(v=minZ,col="red", cex.axis = 0.5)
  866. axis(2, at=xh, labels = tmp3$ontology, tick=FALSE, las =2, line =-0.5, cex.axis = 0.7)
  867. }
  868. par(op) # Leaves the last plot
  869. dev.off()
  870. ## Master Tables Generation for output to csv and for GO:CC Z score coclustering
  871. for (this.input in names(GSA.FET.outSimple)) {
  872. if (length(GSA.FET.outSimple[[this.input]])==1) { if (is.na(GSA.FET.outSimple[[this.input]][1])) {
  873. cat(paste0("x - ",this.input," list had no genes and is excluded form table output.\n"))
  874. GSA.FET.outSimple[[this.input]]<-NULL
  875. uniquemodcolors<-uniquemodcolors[-which(uniquemodcolors==this.input)]
  876. }}
  877. }
  878. if(length(uniquemodcolors)<1) {
  879. cat("x - no lists had any genes/were enriched in any terms below cutoffs. Skipping output of tables, etc.\n")
  880. } else {
  881. GSA.FET.collapsed.outSimple.Zscore <- cbind(GSA.FET.outSimple[[1]][,c("ontology","ontologyType")], matrix(unlist(lapply(GSA.FET.outSimple, function(x) x[,"Zscore"] )),byrow=FALSE,ncol=length(uniquemodcolors)), matrix(unlist(lapply(GSA.FET.outSimple, function(x) x[,"Genes.Hit"] )),byrow=FALSE,ncol=length(uniquemodcolors)))
  882. GSA.FET.collapsed.outSimple.Pvalue.Enrichment <- cbind(GSA.FET.outSimple[[1]][,c("ontology","ontologyType")], matrix(unlist(lapply(GSA.FET.outSimple, function(x) x[,"Pvalue.Enrichment"] )),byrow=FALSE,ncol=length(uniquemodcolors)), matrix(unlist(lapply(GSA.FET.outSimple, function(x) x[,"Genes.Hit"] )),byrow=FALSE,ncol=length(uniquemodcolors)))
  883. GSA.FET.collapsed.outSimple.Enrichment.FDR.BH <- cbind(GSA.FET.outSimple[[1]][,c("ontology","ontologyType")], matrix(unlist(lapply(GSA.FET.outSimple, function(x) x[,"Adjusted.Enr.Pvalue"] )),byrow=FALSE,ncol=length(uniquemodcolors)), matrix(unlist(lapply(GSA.FET.outSimple, function(x) x[,"Genes.Hit"] )),byrow=FALSE,ncol=length(uniquemodcolors)))
  884. colnames(GSA.FET.collapsed.outSimple.Zscore)[3:(length(uniquemodcolors)*2+2)] <- colnames(GSA.FET.collapsed.outSimple.Pvalue.Enrichment)[3:(length(uniquemodcolors)*2+2)] <- colnames(GSA.FET.collapsed.outSimple.Enrichment.FDR.BH)[3:(length(uniquemodcolors)*2+2)] <- c(names(GSA.FET.outSimple), paste0(names(GSA.FET.outSimple),"_Genes.Hit"))
  885. #write master tables
  886. write.table(GSA.FET.collapsed.outSimple.Zscore,file=paste0(filePath,outFilename,"/GSA-GO-FET_",outFilename,"-Zscores.txt"),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  887. write.table(GSA.FET.collapsed.outSimple.Pvalue.Enrichment,file=paste0(filePath,outFilename,"/GSA-GO-FET_",outFilename,"-Enr.Pvalues.txt"),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  888. write.table(GSA.FET.collapsed.outSimple.Enrichment.FDR.BH,file=paste0(filePath,outFilename,"/GSA-GO-FET_",outFilename,"-Enr.FDR.BH.txt"),row.names=FALSE,col.names=TRUE,sep="\t", quote=FALSE)
  889. if(!exists("cocluster")) cocluster=TRUE
  890. if(cocluster) {
  891. ## For GO:CC Z score coclustering
  892. GSA.FET.GOCC.Zscore <- GSA.FET.collapsed.outSimple.Zscore[which(GSA.FET.collapsed.outSimple.Zscore$ontologyType=="GOCC"), 1:(length(uniquemodcolors)+2)]
  893. temp.rownames=rownames(GSA.FET.GOCC.Zscore)
  894. GSA.FET.GOCC.Zscore<-cbind(GSA.FET.GOCC.Zscore[,1:2], apply(GSA.FET.GOCC.Zscore[,3:ncol(GSA.FET.GOCC.Zscore)],2,as.numeric))
  895. rownames(GSA.FET.GOCC.Zscore)<-temp.rownames
  896. GSA.FET.GOCC.terms <- gsub("^.*\\%GO..\\%(GO:\\d*)$","\\1",rownames(GSA.FET.GOCC.Zscore))
  897. minZ<-qnorm(1e-5/2, lower.tail=FALSE)
  898. if(nrow(GSA.FET.GOCC.Zscore)>1) {
  899. GSA.FET.GOCC.terms.minZreached <- apply(GSA.FET.GOCC.Zscore[,3:ncol(GSA.FET.GOCC.Zscore)],1,function(x) if (max(x)>=minZ) { TRUE } else { FALSE } )
  900. if (removeRedundantGOterms=="kappa") {
  901. ## --- Build gene sets for significant GO:CC terms
  902. sig_idx <- which(GSA.FET.GOCC.terms.minZreached)
  903. sig_terms <- GSA.FET.GOCC.terms[sig_idx]
  904. # Assemble a named list of gene symbols per GO term (using GSCfromGMT$gsc names like "...%GO:####")
  905. gene_sets_present <- setNames(vector("list", length(sig_terms)), sig_terms)
  906. for (i in seq_along(sig_terms)) {
  907. go_id <- sig_terms[i]
  908. hit <- grep(paste0("%", go_id, "$"), names(GSCfromGMT$gsc))
  909. gene_sets_present[[i]] <- if (length(hit)) unique(GSCfromGMT$gsc[[hit[1] ]]) else character(0)
  910. }
  911. ## --- Kappa clustering of the significant terms
  912. # Expecting a data.frame with columns: term, cluster
  913. kc <- kappa_cluster_ontologies(gene_sets_present, kappa_cut = 0.30, method = "graph")
  914. ## --- Pick one representative per cluster: max positive Z anywhere in the row
  915. # Compute per-term max positive Z across all Z-score columns (3:ncol)
  916. row_maxZ <- apply(GSA.FET.GOCC.Zscore[, 3:ncol(GSA.FET.GOCC.Zscore), drop = FALSE],
  917. 1, function(z) suppressWarnings(max(as.numeric(z), na.rm = TRUE)))
  918. names(row_maxZ) <- GSA.FET.GOCC.terms
  919. # Restrict to clustered significant terms
  920. ok <- kc$term %in% names(row_maxZ)
  921. kc <- kc[ok, , drop = FALSE]
  922. # For each cluster, keep the term with the largest maxZ
  923. split_terms <- split(kc$term, kc$cluster)
  924. keep_terms <- unlist(lapply(split_terms, function(tt) tt[which.max(row_maxZ[tt])]), use.names = FALSE)
  925. # Update the logical filter to keep only the chosen representatives
  926. GSA.FET.GOCC.terms.minZreached[] <- FALSE
  927. GSA.FET.GOCC.terms.minZreached[ match(keep_terms, GSA.FET.GOCC.terms) ] <- TRUE
  928. # For downstream convenience, also set the minimal terms vector
  929. GSA.FET.GOCC.minimal.terms <- keep_terms
  930. } else if (removeRedundantGOterms) {
  931. GSA.FET.GOCC.minimal.terms <- minimal_set(ontology.index, terms=GSA.FET.GOCC.terms[which(GSA.FET.GOCC.terms.minZreached)])
  932. } else {
  933. GSA.FET.GOCC.minimal.terms <- GSA.FET.GOCC.terms[which(GSA.FET.GOCC.terms.minZreached)]
  934. cat("- Note: removeRedundantGOterms=FALSE. You can reduce cellular component terms in the coclustering by setting this variable to TRUE, or 'kappa'.\n\n")
  935. }
  936. GSA.FET.GOCC.Zscore.minimal.terms <- GSA.FET.GOCC.Zscore[which(GSA.FET.GOCC.terms %in% GSA.FET.GOCC.minimal.terms),]
  937. dim(GSA.FET.GOCC.Zscore)
  938. # 980 rows of GO:CC terms
  939. length(which(GSA.FET.GOCC.terms.minZreached))
  940. # 596 have at least 1 Z score above minZ set immediately above (with p val max 0.001 used to calc minZ immediately above...)
  941. # 381 at p val max 0.00001
  942. length(GSA.FET.GOCC.minimal.terms)
  943. # 329 kept out of 980 (with p val max 0.001 used to calc minZ immediately above...)
  944. # 191 kept out of 980 (with p val max 0.00001 used to calc minZ immediately above...)
  945. matrixdata <- data <- t(as.matrix(GSA.FET.GOCC.Zscore.minimal.terms[,3:ncol(GSA.FET.GOCC.Zscore.minimal.terms)]))
  946. if(ncol(data)==0) cat(paste0("- No highly significant (Z>",signif(minZ,3),") Cellular Component ontologies found. Skipping GOCC Cluster Heatmap output.\n\n"))
  947. if(ncol(data)==1) cat(paste0("- Only one highly significant (Z>",signif(minZ,3),") Cellular Component ontologies found. Skipping GOCC Cluster Heatmap output.\n\n"))
  948. if(ncol(data)>1) {
  949. data[matrixdata>4]<-4
  950. data[matrixdata< -4] <- -4
  951. #NMF - initial approach
  952. suppressPackageStartupMessages(require(WGCNA,quietly=TRUE))
  953. bw<-colorRampPalette(c("#0058CC", "white"))
  954. wr<-colorRampPalette(c("white", "#CC3300"))
  955. colvec<-c(bw(50),wr(50))
  956. colnames(data)<-gsub("\\%GOCC\\%"," | ",gsub("GOcc","GOCC",colnames(data)))
  957. if(!modulesInMemory) {
  958. uniquemodcolors=labels2colors(1:((ncol(GSA.FET.collapsed.outSimple.Zscore)-2)/2)) #variable reused for color annotation here; meaningless for non-WGCNA lists #/2 because Genes.Hit double the columns.
  959. myRowAnnotation=data.frame(Lists=as.numeric(factor(uniquemodcolors,levels=sort(uniquemodcolors))))
  960. heatmapLegendColors<-list(Lists=factor(sort(uniquemodcolors)))
  961. } else {
  962. heatmapLegendColors<-list(Modules=sort(uniquemodcolors))
  963. myRowAnnotation=data.frame(Modules=as.numeric(factor(uniquemodcolors,levels=sort(uniquemodcolors))))
  964. }
  965. suppressPackageStartupMessages(require(NMF,quietly=TRUE)) # for aheatmap
  966. pdf(file=paste0("GO_cc_clustering_from_GSA_FET_Z-",filenameFinal,".pdf"),width=18.5,height=18,onefile=FALSE)
  967. aheatmap(x=data, ## Numeric Matrix
  968. main="Co-clustering with manhattan distance function, ward metric",
  969. # annCol=metdat, ## Color swatch and legend annotation of columns/samples and rows (not used)
  970. annRow=myRowAnnotation,
  971. annColors=heatmapLegendColors,
  972. border=list(matrix = TRUE),
  973. scale="none", ## row, column, or none
  974. distfun="manhattan",hclustfun="ward", ## Clustering options
  975. cexRow=0.8, ## Character sizes
  976. cexCol=0.8,
  977. col=colvec, #c("white","black"), ## Color map scheme
  978. treeheight=80,
  979. Rowv=TRUE,Colv=TRUE) ## Cluster columns
  980. dev.off()
  981. } # end if(ncol(data)>1)
  982. } else { cat("- None or only one significant Cellular Component ontologies (rows) found. Skipping GOCC Cluster Heatmap output.\n\n") } # end if(nrow(GSA.FET.GOCC.Zscore)>1)
  983. } # end if (cocluster)
  984. } # end if(length(uniquemodcolors)<1)
  985. # Optional (if bubble=TRUE): save a GSEA-style bubble plot to a PDF file for each input list's enriched ontologies.
  986. if (exists("bubble")) if (bubble) {
  987. cat("\nPlotting GSEA-style bubble plots...\n")
  988. GO.bubblePlot(ZinputFile = paste0(filePath,outFilename,"/GSA-GO-FET_",outFilename,"-Zscores.txt"),
  989. GMTfile = if(file.exists(paste0("../", GMTdatabaseFile))) { paste0("../", GMTdatabaseFile) } else {
  990. GMTdatabaseFile }, # direct full path to GMT required if not up one level in the folder structure from the working directory at the time GOparallel was called.
  991. GO.OBOfile = if(file.exists(paste0(filePath,GO.OBOfile))) { paste0(filePath,GO.OBOfile) } else { GO.OBOfile }, # function will fallback to redownload if not found.
  992. keep_ontology_types = c("GOBP", "GOMF", "GOCC", "REACTOME", "WikiPathways", "MSIGDB_C2"),
  993. ontology_to_color = c("GOBP"=color[1], "GOMF"=color[2], "GOCC"=color[3], "REACTOME"=color[4], "WikiPathways"=color[5], "MSIGDB_C2"=color[6]),
  994. MAX_TERMS_PER_ONTOLOGY_TYPE = maxBarsPerOntology,
  995. max_label_width = 50)
  996. }
  997. setwd(filePath)
  998. }
  999. ## GSEA-style Bubble plot PDF output function - stand alone to run after GOparallel on Zscore txt output, or with bubble=TRUE argument within GOparallel()
  1000. GO.bubblePlot <- function(ZinputFile=NULL, GMTfile=NULL, GO.OBOfile="./go.obo", removeRedundantGOterms=TRUE,
  1001. keep_ontology_types = c("GOBP", "GOMF", "GOCC"),
  1002. ontology_to_color = c("GOBP"="darkseagreen3", "GOMF"="lightsteelblue1", "GOCC"="lightpink4", "REACTOME"="goldenrod", "WikiPathways"="darkorange", "MSIGDB_C2"="gold", "MSIGDBHALLMARK"="greenyellow"),
  1003. MAX_TERMS_PER_ONTOLOGY_TYPE = 5,
  1004. max_label_width = 50) {
  1005. # Load required libraries
  1006. suppressPackageStartupMessages(require(ggplot2,quietly=TRUE))
  1007. suppressPackageStartupMessages(require(dplyr,quietly=TRUE))
  1008. suppressPackageStartupMessages(require(tibble,quietly=TRUE))
  1009. suppressPackageStartupMessages(require(stringr,quietly=TRUE))
  1010. suppressPackageStartupMessages(require(scales,quietly=TRUE)) # For rescale function
  1011. suppressPackageStartupMessages(require(grid,quietly=TRUE)) # For grob manipulation - to take out one tick mark!
  1012. suppressPackageStartupMessages(require(gtable,quietly=TRUE))
  1013. minHitsPerOntology <- 5 # Ontologies hit by fewer than this number of genes will not be plotted; 5 is hard coded into GOparallel function and required to get the same ontologies for the 3 ontology types output by GOparallel
  1014. keep_ontology_types <- intersect(keep_ontology_types, c("GOBP", "GOMF", "GOCC", "REACTOME", "WikiPathways", "MSIGDB_C2", "MSIGDBHALLMARK"))
  1015. if(length(keep_ontology_types)<1) stop("Error: Argument 'keep_ontology_types' must specify one or more supported ontologies ('GOBP', 'GOMF', 'GOCC', 'REACTOME', 'WikiPathways', 'MSIGDB_C2', 'MSIGDBHALLMARK')")
  1016. if (is.null(ZinputFile)) stop("Error: No GOparallel Zscore txt file was specified as input.")
  1017. if (!file.exists(ZinputFile)) stop("Error: ZinputFile not found.")
  1018. if (is.null(GMTfile)) stop("Error: No GMT file (ontology database from Bader Lab website) specified. This file must be specified and match the version used to generate the ZinputFile.")
  1019. if (!file.exists(GMTfile)) stop("Error: GMT file (ontology database from Bader Lab website) not found. This file must be specified and match the version used to generate the ZinputFile.")
  1020. # Check if removeRedundantGOterms is specified
  1021. if(!exists("removeRedundantGOterms")) {
  1022. cat("- removeRedundantGOterms not specified TRUE/FALSE. Removing them as the default, using go.obo and ontologyIndex package.\n")
  1023. removeRedundantGOterms <- TRUE
  1024. }
  1025. if(removeRedundantGOterms) {
  1026. if (!exists("GO.OBOfile")) GO.OBOfile <- "go.obo"
  1027. if (!file.exists(GO.OBOfile)) {
  1028. suppressPackageStartupMessages(require(curl, quietly=TRUE))
  1029. OBOtargetPath <- paste0(getwd(),"/")
  1030. # OBOtargetPath <- gsub("(.*\\/).*$", "\\1", GO.OBOfile)
  1031. # if (!dir.exists(OBOtargetPath)) dir.create(OBOtargetPath)
  1032. # curr.dir <- getwd()
  1033. # setwd(OBOtargetPath)
  1034. cat(paste0("- Downloading go.obo file for main GO term redundancy cleanup...\n...to location: ", OBOtargetPath, "go.obo\n"))
  1035. curl_download(url="http://current.geneontology.org/ontology/go.obo", destfile="go.obo", quiet = TRUE, mode = "w")
  1036. # setwd(curr.dir)
  1037. cat("GO.OBOfile set to downloaded file: ", paste0(OBOtargetPath, "go.obo"), "\n")
  1038. GO.OBOfile <- paste0(OBOtargetPath, "go.obo")
  1039. }
  1040. }
  1041. if (removeRedundantGOterms) {
  1042. suppressPackageStartupMessages(require(ontologyIndex))
  1043. ontology.index <- get_OBO(file=GO.OBOfile, extract_tags="everything")
  1044. }
  1045. # Read input files
  1046. df1 <- read.table(ZinputFile, header=TRUE, sep="\t", stringsAsFactors=FALSE, quote = "\"")
  1047. # Ensure the second column is named "OntologyType"
  1048. colnames(df1)[2] <- "OntologyType"
  1049. # Read GMT file
  1050. lines <- readLines(GMTfile)
  1051. df2_list <- strsplit(lines, "\t")
  1052. # Parse the GMT file
  1053. df2_parsed <- lapply(df2_list, function(x) {
  1054. matches <- regmatches(x[1], regexec("^(.*)%(.+)%(.+)$", x[1]))
  1055. if (length(matches[[1]]) >= 4) {
  1056. OntologyType <- matches[[1]][3]
  1057. ontology <- matches[[1]][2]
  1058. term <- matches[[1]][4]
  1059. } else {
  1060. OntologyType <- NA
  1061. ontology <- NA
  1062. term <- NA
  1063. }
  1064. genes <- x[3:length(x)]
  1065. list(OntologyType=OntologyType, ontology=ontology, term=term, genes=genes)
  1066. })
  1067. # Create data frame
  1068. df2_df <- tibble(
  1069. OntologyType = sapply(df2_parsed, function(x) x$OntologyType),
  1070. ontology = sapply(df2_parsed, function(x) stringr::str_to_title(x$ontology)),
  1071. term = sapply(df2_parsed, function(x) x$term),
  1072. genes = lapply(df2_parsed, function(x) x$genes)
  1073. )
  1074. # Copy WP# pathway IDs to term column
  1075. df2_df$term[which(grepl("^WP\\d+",df2_df$OntologyType))]<-df2_df$OntologyType[which(grepl("^WP\\d+",df2_df$OntologyType))]
  1076. # Revert WP# to 'WikiPathways' in OntologyType
  1077. df2_df$OntologyType[which(grepl("^WP\\d+",df2_df$OntologyType))]<-"WikiPathways"
  1078. # Remove '%Wikipathways_YYYYMMDD' from terms
  1079. df2_df$ontology[which(df2_df$OntologyType=="WikiPathways")]<-gsub("%Wikipathways_\\d+$","",df2_df$ontology[which(df2_df$OntologyType=="WikiPathways")])
  1080. df2_df <- df2_df[df2_df$OntologyType %in% keep_ontology_types, ]
  1081. # Get the column names of df1
  1082. colnames_df1 <- colnames(df1)
  1083. # Identify Z score columns (excluding "ontology" and "OntologyType", and columns ending with "_Genes.Hit")
  1084. zscore_cols <- colnames_df1[!(colnames_df1 %in% c("ontology", "OntologyType")) & !grepl("_Genes\\.Hit$", colnames_df1)]
  1085. # Identify Genes.Hit columns
  1086. genes_hit_cols <- paste0(zscore_cols, "_Genes.Hit")
  1087. # Check that the Genes.Hit columns exist
  1088. if (!all(genes_hit_cols %in% colnames_df1)) {
  1089. stop("Some Genes.Hit columns corresponding to Z score columns are missing in input_file1.txt")
  1090. }
  1091. # Loop over each Z score column
  1092. for (i in seq_along(zscore_cols)) {
  1093. zscore_col <- zscore_cols[i]
  1094. genes_hit_col <- genes_hit_cols[i]
  1095. plot_title <- zscore_col
  1096. # Prepare the data
  1097. df_current <- df1[, c("ontology", "OntologyType", zscore_col, genes_hit_col)]
  1098. colnames(df_current) <- c("ontology", "OntologyType", "Zscore", "Genes.Hit")
  1099. # Merge data frames
  1100. df_merged <- merge(df_current, df2_df, by=c("OntologyType", "ontology"))
  1101. # Compute number of genes hit
  1102. df_merged$num_genes_hit <- sapply(df_merged$Genes.Hit, function(x) length(strsplit(x, ";")[[1]]))
  1103. # Compute total number of genes in ontology
  1104. df_merged$num_genes_total <- sapply(df_merged$genes, length)
  1105. # Compute gene ratio
  1106. df_merged$gene_ratio <- df_merged$num_genes_hit / df_merged$num_genes_total
  1107. # Correct p-value calculation: one-tailed test using log.p to handle very small p-values
  1108. # Compute log p-value
  1109. log_pvalue <- pnorm(df_merged$Zscore, lower.tail = FALSE, log.p = TRUE)
  1110. # Compute -log10(p-value) directly from log_pvalue
  1111. df_merged$neg_log10_pvalue <- -log_pvalue / log(10)
  1112. # Handle infinite values by setting a maximum threshold
  1113. max_neg_log10_pvalue <- 50 # Adjust this threshold as needed
  1114. df_merged$neg_log10_pvalue <- pmin(df_merged$neg_log10_pvalue, max_neg_log10_pvalue)
  1115. # Ensure neg_log10_pvalue is at least 1.30
  1116. df_merged$neg_log10_pvalue <- pmax(df_merged$neg_log10_pvalue, 1.30)
  1117. # Filter for Zscore > 1.96
  1118. minZ <- qnorm(0.05/2, lower.tail=FALSE)
  1119. df_filtered <- df_merged[df_merged$Zscore > minZ, ]
  1120. # Filter for minHitsPerOntology
  1121. df_filtered <- df_filtered[df_filtered$num_genes_hit >= minHitsPerOntology, ]
  1122. # Keep only specified OntologyTypes
  1123. df_filtered <- df_filtered[df_filtered$OntologyType %in% keep_ontology_types, ]
  1124. # If no significant ontologies found, skip plotting
  1125. if (nrow(df_filtered) == 0) {
  1126. message(paste("No significant ontologies found for", zscore_col))
  1127. next
  1128. }
  1129. if(removeRedundantGOterms) {
  1130. df_filtered.coreGO <- df_filtered[df_filtered$OntologyType %in% c("GOCC", "GOBP", "GOMF") & df_filtered$Zscore >= minZ, ]
  1131. go.minimal.terms <- minimal_set(ontology.index, terms=df_filtered.coreGO$term)
  1132. df_filtered.minimal.terms <- df_filtered.coreGO[df_filtered.coreGO$term %in% go.minimal.terms, ]
  1133. df_filtered.noCoreGO <- df_filtered[!df_filtered$OntologyType %in% c("GOCC", "GOBP", "GOMF"), ]
  1134. df_filtered <- rbind(df_filtered.noCoreGO, df_filtered.minimal.terms)
  1135. }
  1136. rename_map <- c(
  1137. "GOBP" = "BP",
  1138. "GOMF" = "MF",
  1139. "GOCC" = "CC",
  1140. "REACTOME" = "RE",
  1141. "WikiPathways" = "WP",
  1142. "MSIGDB_C2" = "MS",
  1143. "MSIGDBHALLMARK" = "HM"
  1144. )
  1145. # Select top 5 (or fewer) ontologies per OntologyType
  1146. df_top <- df_filtered %>%
  1147. group_by(OntologyType) %>%
  1148. arrange(desc(Zscore), .by_group = TRUE) %>%
  1149. slice_head(n = MAX_TERMS_PER_ONTOLOGY_TYPE) %>%
  1150. ungroup() %>%
  1151. mutate(
  1152. OntologyType = as.character(OntologyType),
  1153. OntologyType = recode(
  1154. OntologyType,
  1155. !!!rename_map[keep_ontology_types]
  1156. ),
  1157. OntologyType = factor(OntologyType) # optional
  1158. )
  1159. # Create ontology label
  1160. df_top$ontology_label <- df_top$ontology #paste(df_top$OntologyType, df_top$ontology, sep=": ")
  1161. # Reorder the ontology types so that CC follows MF
  1162. df_top$OntologyType <- factor(df_top$OntologyType, levels = c(if ("GOBP" %in% keep_ontology_types) "BP",
  1163. if ("GOMF" %in% keep_ontology_types) "MF",
  1164. if ("GOCC" %in% keep_ontology_types) "CC",
  1165. if ("REACTOME" %in% keep_ontology_types) "RE",
  1166. if ("WikiPathways" %in% keep_ontology_types) "WP",
  1167. if ("MSIGDB_C2" %in% keep_ontology_types) "MS",
  1168. if ("MSIGDBHALLMARK" %in% keep_ontology_types) "HM"))
  1169. # Order ontology_label factor levels after ordering OntologyType
  1170. df_top <- df_top %>%
  1171. arrange(OntologyType, desc(Zscore))
  1172. # Handle duplicates in ontology labels if they exist
  1173. if(length(which(duplicated(df_top$ontology_label)))>0) df_top$ontology_label[which(duplicated(df_top$ontology_label))]<-paste0(".",df_top$ontology_label[which(duplicated(df_top$ontology_label))])
  1174. if(length(which(duplicated(df_top$ontology_label)))>0) df_top$ontology_label[which(duplicated(df_top$ontology_label))]<-paste0("..",df_top$ontology_label[which(duplicated(df_top$ontology_label))])
  1175. df_top$ontology_label <- factor(df_top$ontology_label, levels=rev(unique(df_top$ontology_label)))
  1176. # Map OntologyType to specified colors
  1177. ontology_colors <- c()
  1178. if ("GOBP" %in% keep_ontology_types) ontology_colors <- c(ontology_colors, BP = ontology_to_color[["GOBP"]])
  1179. if ("GOMF" %in% keep_ontology_types) ontology_colors <- c(ontology_colors, "MF" = ontology_to_color[["GOMF"]])
  1180. if ("GOCC" %in% keep_ontology_types) ontology_colors <- c(ontology_colors, "CC" = ontology_to_color[["GOCC"]])
  1181. if ("REACTOME" %in% keep_ontology_types) ontology_colors=c(ontology_colors, "RE" = ontology_to_color[["REACTOME"]])
  1182. if ("WikiPathways" %in% keep_ontology_types) ontology_colors=c(ontology_colors, "WP" = ontology_to_color[["WikiPathways"]])
  1183. if ("MSIGDB_C2" %in% keep_ontology_types) ontology_colors=c(ontology_colors, "MS" = ontology_to_color[["MSIGDB_C2"]])
  1184. if ("MSIGDBHALLMARK" %in% keep_ontology_types) ontology_colors=c(ontology_colors, "HM" = ontology_to_color[["MSIGDBHALLMARK"]])
  1185. # Wrap the ontology labels
  1186. df_top$ontology_label <- stringr::str_wrap(df_top$ontology_label, width = max_label_width)
  1187. # Order ontology_label factor levels (after wrapping)
  1188. df_top$ontology_label <- as.character(df_top$ontology_label)
  1189. df_top$ontology_label <- factor(df_top$ontology_label, levels = rev(unique(df_top$ontology_label)))
  1190. dummy_label <- paste(rep("_", ceiling(max_label_width*0.85)), collapse = "")
  1191. # Adjust the breaks for gene_ratio
  1192. min_gene_ratio <- min(df_top$gene_ratio)
  1193. max_gene_ratio <- max(df_top$gene_ratio)
  1194. # Generate breaks for gene_ratio size scale
  1195. breaks_gene_ratio <- seq(min_gene_ratio, max_gene_ratio, length.out = 5)
  1196. breaks_gene_ratio <- unique(round(breaks_gene_ratio, digits = 4)) # Round to 4 decimals to avoid duplicates
  1197. # Convert breaks to percentages with 1 decimal place
  1198. labels_gene_ratio <- sprintf("%.1f%%", breaks_gene_ratio * 100)
  1199. # Create a dummy row
  1200. dummy_row <- df_top[1, ] # Copy structure
  1201. dummy_row$OntologyType <- NA
  1202. dummy_row$ontology <- NA
  1203. dummy_row$Zscore <- NA
  1204. dummy_row$neg_log10_pvalue <- NA #min(df_top$neg_log10_pvalue)
  1205. dummy_row$gene_ratio <- NA #min(df_top$gene_ratio)
  1206. dummy_row$Genes.Hit <- NA
  1207. dummy_row$num_genes_hit <- NA
  1208. dummy_row$num_genes_total <- NA
  1209. dummy_row$genes <- NA
  1210. dummy_row$ontology_label <- dummy_label
  1211. dummy_row$y_position <- nrow(df_top)+1
  1212. dummy_row$txtColor="#FFFFFF00"
  1213. df_top$y_position<-as.numeric(rownames(df_top)) #c(1:nrow(df_top))
  1214. df_top$txtColor<-"black"
  1215. # Add the dummy row to df_top
  1216. df_top <- rbind(df_top, dummy_row)
  1217. # df_top$OntologyType.label <- paste0(df_top$OntologyType,".",df_top$ontology_label)
  1218. # df_top$OntologyType.label <- factor(df_top$OntologyType.label, levels=rev(unique(df_top$OntologyType.label)))
  1219. # Create a named vector of labels, (the dummy label is set to a long set-length string)
  1220. y_labels <- setNames(as.character(df_top$ontology_label), df_top$ontology_label)
  1221. #y_labels[dummy_label] <- ""
  1222. #all_black_last_white=c(rep("black",length(y_labels)-1),"#FFFFFF00")
  1223. # Get plot height: 342 px = 4.75 in; 252 px = 15.6x; 1x=16.15px
  1224. this.plotHeight=4.75*(16.15*0.6 +(nrow(df_top)-1)*16.15 +90)/342
  1225. # Plot
  1226. pdf(paste0("bubblePlot_", zscore_col, ".pdf"), width=8.8, height=this.plotHeight, bg = "transparent")
  1227. # Create the plot and store it in a variable
  1228. p <- ggplot(df_top, aes(x = neg_log10_pvalue, y = ontology_label, size = gene_ratio, fill = OntologyType)) +
  1229. geom_point(shape = 21, color = "black", stroke = 0.5) +
  1230. scale_size_continuous(
  1231. name = "Gene Ratio",
  1232. breaks = breaks_gene_ratio,
  1233. labels = labels_gene_ratio,
  1234. guide = guide_legend(override.aes = list(fill = "grey70", color = "black")),
  1235. range = c(3, 8)
  1236. ) +
  1237. scale_fill_manual(
  1238. values = ontology_colors,
  1239. guide = "none"
  1240. ) +
  1241. xlab(expression(bold(-log[10](italic(p))))) +
  1242. ylab("Ontology") +
  1243. scale_y_discrete(labels = y_labels) +
  1244. # ggtitle(bquote(bold(.(plot_title)))) +
  1245. theme_bw(base_size = 14, base_family = "Helvetica") +
  1246. theme(
  1247. panel.background = element_rect(fill = "transparent", color = NA),
  1248. plot.background = element_rect(fill = "transparent", color = NA),
  1249. axis.text.y = element_text(size = 9.4, hjust = 1, vjust=0.5, colour=df_top$txtColor),
  1250. axis.title.y = element_blank(),
  1251. # axis.ticks.length.y = c(rep(unit(3,"points"),nrow(df_top)-1),unit(0,"points")),
  1252. panel.grid.major.x = element_blank(),
  1253. panel.grid.minor = element_blank(),
  1254. panel.grid.major.y = element_line(color = "grey", linetype = 2, size = 0.25),
  1255. legend.background = element_rect(fill = "transparent", colour = NA),
  1256. legend.key = element_rect(fill = "transparent", colour = NA),
  1257. plot.margin = unit(c(1, 4, 1, 1), "lines"), # Adjust left margin
  1258. panel.border = element_rect(fill = NA)
  1259. )
  1260. # Compute x-axis limits: data range plus 5% on each extreme
  1261. x_min_data <- min(df_top$neg_log10_pvalue, na.rm = TRUE)
  1262. x_max_data <- max(df_top$neg_log10_pvalue, na.rm = TRUE)
  1263. x_range <- x_max_data - x_min_data
  1264. # Handle the case when x_range is zero
  1265. if (x_range == 0) {
  1266. # Set a small range around the data point
  1267. x_min <- x_min_data - 1
  1268. x_max <- x_max_data + 1
  1269. x_range <- x_max - x_min
  1270. x_min_hbar <- x_min - 0.1
  1271. } else {
  1272. x_min <- x_min_data - 0.05 * x_range
  1273. x_max <- x_max_data + 0.05 * x_range
  1274. x_min_hbar <- x_min_data - (x_range * 0.05) * 2
  1275. }
  1276. # Add coordinate limits and prevent clipping
  1277. p <- p + coord_cartesian(xlim = c(x_min, x_max), clip = "off")
  1278. # Determine the position of the vertical bar outside the plot area
  1279. offset <- x_range * 0.055
  1280. bar_width <- x_range * 0.05 * 2
  1281. # Ensure offset and bar_width have minimum values
  1282. offset <- max(offset, 0.1)
  1283. bar_width <- max(bar_width, 0.2)
  1284. bar_xmin <- x_max + offset
  1285. bar_xmax <- bar_xmin + bar_width
  1286. text_x <- bar_xmin + (bar_xmax - bar_xmin) / 2
  1287. # Create label data with y positions
  1288. df_top <- df_top %>%
  1289. mutate(y_position = as.numeric(ontology_label)) # concatenated Type.label ensures ontologies named identically across ontology types are uniquely numbered!
  1290. # Get the total number of ontologies
  1291. total_terms <- length(unique(df_top$ontology_label))
  1292. # Group labels by OntologyType to get y_min and y_max for each group
  1293. grouped_labels <- df_top %>%
  1294. group_by(OntologyType) %>%
  1295. summarise(
  1296. y_min = min(y_position) - 0.5,
  1297. y_max = max(y_position) + 0.5
  1298. ) %>%
  1299. arrange(y_min)
  1300. # Adjust y_min and y_max to ensure bars are contiguous and cover the entire y-axis
  1301. grouped_labels$y_min[1] <- 0.5
  1302. grouped_labels$y_max[nrow(grouped_labels)] <- total_terms + 0.5
  1303. if (nrow(grouped_labels) > 1) {
  1304. for (ii in 2:nrow(grouped_labels)) {
  1305. grouped_labels$y_min[ii] <- grouped_labels$y_max[ii - 1]
  1306. }
  1307. }
  1308. # Determine the position of the vertical bar outside the plot area
  1309. offset <- x_range * 0.055
  1310. bar_xmin <- x_max + offset
  1311. bar_xmax <- bar_xmin + (x_range * 0.05) * 2
  1312. text_x <- bar_xmin + (bar_xmax - bar_xmin) / 2
  1313. grouped_labels1<-grouped_labels
  1314. grouped_labels1[1,"y_min"]<-floor(grouped_labels1[1,"y_min"])
  1315. grouped_labels1[nrow(grouped_labels1),"y_max"]<-ceiling(grouped_labels1[nrow(grouped_labels1),"y_max"])
  1316. title_box1<-grouped_labels1[nrow(grouped_labels1),]
  1317. grouped_labels1<-grouped_labels1[-nrow(grouped_labels1),]
  1318. # Add geom_rect for the vertical bars
  1319. p <- p + geom_rect(
  1320. data = grouped_labels1,
  1321. aes(
  1322. xmin = bar_xmin,
  1323. xmax = bar_xmax,
  1324. ymin = y_min,
  1325. ymax = y_max,
  1326. fill = OntologyType
  1327. ),
  1328. color = "black",
  1329. size = 0.5,
  1330. inherit.aes = FALSE
  1331. )
  1332. # Add geom_text for the vertical rotated bold text
  1333. p <- p + geom_text(
  1334. data = grouped_labels,
  1335. aes(
  1336. x = text_x,
  1337. y = (y_min + y_max) / 2,
  1338. label = gsub("GO", "", OntologyType)
  1339. ),
  1340. angle = 270,
  1341. fontface = "bold",
  1342. color = "black",
  1343. size = 4,
  1344. inherit.aes = FALSE
  1345. )
  1346. # Add horizontal title bar
  1347. p <- p + geom_rect(
  1348. data = title_box1,
  1349. aes(
  1350. xmin = x_min_hbar,
  1351. xmax = bar_xmin,
  1352. ymin = y_min+(y_max-y_min)*0.1,
  1353. ymax = y_max,
  1354. fill = "grey50"
  1355. ),
  1356. color = "black",
  1357. size = 0,
  1358. inherit.aes = FALSE
  1359. )
  1360. # Add horizontal title bar at the top
  1361. title_bar_height <- 0.5 # Adjust as needed
  1362. title_y_min <- total_terms + 0.5
  1363. title_y_max <- title_y_min + title_bar_height
  1364. title_text1<-data.frame(plot_title=plot_title, x=x_min_hbar, y=max(df_top$y_position))
  1365. # Add geom_text for the plot title within the horizontal bar
  1366. p <- p + geom_text(
  1367. data=title_text1,
  1368. aes(
  1369. x = x,
  1370. y = y,
  1371. label = paste0(" ",plot_title)
  1372. ),
  1373. angle = 0,
  1374. fontface = "bold",
  1375. color = "white",
  1376. size = 5.2,
  1377. hjust = 0,
  1378. vjust = -0.1,
  1379. inherit.aes = FALSE
  1380. )
  1381. # Adjust the left margin to allocate fixed space for y-axis labels
  1382. p <- p + theme(
  1383. plot.margin = unit(c(1, 4, 1, 0.5), "lines"), # Top, right, bottom, left
  1384. axis.text.y = element_text(size = 10, hjust = 1, vjust = 0.5)
  1385. )
  1386. # # Explicitly print the plot
  1387. # print(p)
  1388. # Build the ggplot object
  1389. g <- ggplot_build(p)
  1390. gt <- ggplot_gtable(g)
  1391. # Find the index of the y-axis in the gtable layout
  1392. index <- which(gt$layout$name == "axis-l")
  1393. # Extract the y-axis grob
  1394. axis.grob <- gt$grobs[[index]]
  1395. ## Extract the ticks grob
  1396. ##ticks.grob <- axis.grob$children$axis$children[[which(axis.grob$children$axis$childrenOrder == "ticks")]]
  1397. #
  1398. ## Get the number of ticks
  1399. #n.ticks <- length(ticks.grob)
  1400. #
  1401. ## Set the length of the last tick to zero
  1402. #ticks.grob[n.ticks] <- 2 #ticks.grob$x0[n.ticks]
  1403. #
  1404. ## Assign the modified ticks.grob back to the axis.grob
  1405. #axis.grob$children$axis$grobs[[1]][[4]] <- ticks.grob
  1406. tick.parent.grob.idx<-which(sapply(axis.grob$children$axis$grobs, function(x) inherits(x, "polyline")))
  1407. tick.attr.idx<-which(sapply(axis.grob$children$axis$grobs[[1]], function(x) inherits(x, "gpar")))
  1408. axis.grob$children$axis$grobs[[tick.parent.grob.idx]][[tick.attr.idx]]$col <- c(rep(axis.grob$children$axis$grobs[[tick.parent.grob.idx]][[tick.attr.idx]]$col[1],nrow(df_top)-1),"transparent")
  1409. # Assign the modified axis.grob back to the gtable
  1410. gt$grobs[[index]] <- axis.grob
  1411. grid.draw(gt)
  1412. dev.off()
  1413. }
  1414. }

GOparallel-FET.R at commit f7bb170, under MIT · at the source

Overview

Authors: Madeline Wood Alexander1,2,3, Brendan Wood1,4, Hamilton See-Hwee Oh5,6,7,8, Veronica Augustina Bot9,10,11, Julia Borger12, Francesca Galbiati13,14, Keenan A. Walker15, Susan M. Resnick15,16, Heather M. Ochs-Balcom17, Tony Wyss-Coray9,10,18, Charles Kooperberg19, Alexander P. Reiner20, Emily G. Jacobs21,22,23, Jennifer S. Rabin1,2,3,24, Kaitlin B. Casaletto12,23, Rowan Saloner12
24 affiliations
  1. Hurvitz Brain Sciences Program, Toronto, Ontario, Canada
  2. Rehabilitation Sciences Institute, Temerty Faculty of Medicine, Toronto, Ontario, Canada
  3. Harquail Centre for Neuromodulation, Sunnybrook Health Sciences Centre, Toronto, Ontario, Canada
  4. Department of Medical Biophysics, Toronto, ON, Canada
  5. Nash Family Department of Neuroscience, New York, NY, USA
  6. Brain and Body Research Center of the Friedman Brain Institute, New York, NY, USA
  7. Department of Genetics and Genomic Sciences, New York, NY, USA
  8. Ronald M. Loeb Center for Alzheimer’s Disease, New York, NY, USA
  9. The Phil and Penny Knight Initiative for Brain Resilience, Stanford, CA, USA
  10. Wu Tsai Neurosciences Institute, Stanford, CA, USA
  11. Graduate Program in Bioengineering, Stanford, CA, USA
  12. Edward and Pearl Fein Memory and Aging Center, Department of Neurology, Weill Institute for Neurosciences, San Francisco, California, USA
  13. California Center for Pituitary Disorders, San Francisco, California, USA
  14. Division of Endocrinology, Diabetes, and Metabolism, San Francisco, CA, USA
  15. Laboratory of Behavioral Neuroscience, Baltimore, MD, US
  16. Department of Radiology, Philadelphia, PA, USA
  17. Department of Epidemiology and Environmental Health, at Buffalo, NY, USA
  18. Department of Neurology and Neurological Sciences, School of Medicine, CA, USA
  19. Division of Public Health Sciences, Cancer Center, Seattle, WA, USA
  20. Department of Epidemiology, Seattle, WA, USA
  21. Department of Psychological & Brain Sciences, Santa Barbara, California, USA
  22. Neuroscience Research Institute, California, USA
  23. Ann S. Bowers Women’s Brain Health Initiative, California, USA
  24. Division of Neurology, Department of Medicine, Sunnybrook Health Sciences Centre, Toronto, Ontario, Canada
Dates: published online 4 May 2026
Type: Preprint · Language: English
License: CC BY
Identifiers: DOI 10.21203/rs.3.rs-9499814/v1 · OpenAlex W7160158661
Open access: green, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism), Alzheimer's / dementia (population), cellular / molecular (subfield)
Methods: Statistics
Topic: GDF15 and Related Biomarkers (Rheumatology, Medicine), according to OpenAlex
Funding: U.S. Department of Health and Human Services (75N92021D00002, 75N92021D00005, 75N92021D00004, 75N92021D00001, 75N92021D00003); Alzheimer's Association; Larry L. Hillblom Foundation (2024-A-001-CTR); Ohio State University; Wake Forest University; Alzheimer Society; National Institutes of Health (2024-A-001-CTR, K23AG090757, 5r01ag072475-04, 75N92021D00005, 75N92021D00003, 75N92021D00001, 75N92021D00002, 75N92021D00004, R01AG072475); University of California, Davis; University at Buffalo; Canadian Institutes of Health Research (R01AG072475, 173253, 438475); National Institute on Aging (R01AG072475); National Heart Lung and Blood Institute (75N92021D00005, 75N92021D00004, 75N92021D00003, 75N92021D00002, 75N92021D00001)
Citations: not cited yet (Europe PMC); 71 references in the paper

Abstract

Earlier menopause is a risk factor for several age-related diseases, including dementia. The biological pathways linking menopause timing to later-life brain aging are not understood. Leveraging large-scale plasma proteomics in postmenopausal women from the UK Biobank (N=15,012), earlier menopause was associated with upregulation of pro-inflammatory and extracellular matrix degradation pathways, plus accelerated aging across proteomic clocks of organ and cellular aging, including brain and oligodendrocyte aging. Elevated GDF15, a canonical aging marker, was the top protein correlate of earlier menopause. We observed robust replication of menopause timing proteomic shifts in the Women’s Health Initiative Long Life Study (N=1,210). In UKB, proteins associated with earlier menopause, including GDF15, exhibited concordant associations with incident dementia risk and brain atrophy, cerebral small vessel disease burden, and white matter microstructural integrity. Collectively, our findings identify proteomic signatures linking ovarian aging to brain aging, providing a framework to inform interventions to reduce dementia risk.

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

Repository

Its files are read in the Code ↔ Paper reader above.

edammer/GOparallel

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: f7bb170dcfc6d3a94eac822e1ec56140618ab811, 8 January 2026
Languages: R (1)
Size: 9 files, 1 script
Software Heritage: not archived
Found in: the text, “Analyses”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file), igraph (1 file), tidyverse (1 file), WGCNA (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
3 files

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;
  • no match between paragraphs and code yet;
  • 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.

Versions

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

Version 2, 28 September 2026

  • Language: n/a → en
  • Funding: added U.S. Department of Health and Human Services: 75N92021D00002, 75N92021D00005, 75N92021D00004, 75N92021D00001, 75N92021D00003; Alzheimer's Association; Larry L. Hillblom Foundation: 2024-A-001-CTR; Ohio State University; Wake Forest University; Alzheimer Society; National Institutes of Health: 2024-A-001-CTR, K23AG090757, 5r01ag072475-04, 75N92021D00005, 75N92021D00003, 75N92021D00001, 75N92021D00002, 75N92021D00004, R01AG072475; University of California, Davis; University at Buffalo; Canadian Institutes of Health Research: R01AG072475, 173253, 438475; National Institute on Aging: R01AG072475; National Heart, Lung, and Blood Institute: 75N92021D00005, 75N92021D00004, 75N92021D00003, 75N92021D00002, 75N92021D00001

Version 1, 28 September 2026: the first record

Recorded: type, journal, dates, 16 authors, 71 references.

Cite

This paper

Alexander, M. W., Wood, B., Oh, H. S.-H., Bot, V. A., Borger, J., Galbiati, F., Walker, K. A., Resnick, S. M., Ochs-Balcom, H. M., Wyss-Coray, T., Kooperberg, C., Reiner, A. P., Jacobs, E. G., Rabin, J. S., Casaletto, K. B., & Saloner, R. (2026). Plasma proteomics link menopause timing to brain aging and dementia risk. Research Square (preprint). https://doi.org/10.21203/rs.3.rs-9499814/v1

BibTeX

@article{alexander2026plasma,
author = {Alexander, Madeline Wood and Wood, Brendan and Oh, Hamilton See-Hwee and Bot, Veronica Augustina and Borger, Julia and Galbiati, Francesca and Walker, Keenan A. and Resnick, Susan M. and Ochs-Balcom, Heather M. and Wyss-Coray, Tony and Kooperberg, Charles and Reiner, Alexander P. and Jacobs, Emily G. and Rabin, Jennifer S. and Casaletto, Kaitlin B. and Saloner, Rowan},
title = {{Plasma proteomics link menopause timing to brain aging and dementia risk}},
journal = {Research Square (preprint)},
year = {2026},
month = may,
publisher = {Research Square},
issn = {2693-5015},
doi = {10.21203/rs.3.rs-9499814/v1},
url = {https://doi.org/10.21203/rs.3.rs-9499814/v1}
}

RIS

TY - JOUR
AU - Alexander, Madeline Wood
AU - Wood, Brendan
AU - Oh, Hamilton See-Hwee
AU - Bot, Veronica Augustina
AU - Borger, Julia
AU - Galbiati, Francesca
AU - Walker, Keenan A.
AU - Resnick, Susan M.
AU - Ochs-Balcom, Heather M.
AU - Wyss-Coray, Tony
AU - Kooperberg, Charles
AU - Reiner, Alexander P.
AU - Jacobs, Emily G.
AU - Rabin, Jennifer S.
AU - Casaletto, Kaitlin B.
AU - Saloner, Rowan
TI - Plasma proteomics link menopause timing to brain aging and dementia risk
T2 - Research Square (preprint)
J2 - Res Sq
PY - 2026
DA - 2026/05/04
SN - 2693-5015
PB - Research Square
DO - 10.21203/rs.3.rs-9499814/v1
UR - https://doi.org/10.21203/rs.3.rs-9499814/v1
LA - en
ER -

CSL-JSON

{
"id": "10.21203/rs.3.rs-9499814/v1",
"type": "article",
"title": "Plasma proteomics link menopause timing to brain aging and dementia risk",
"container-title": "Research Square (preprint)",
"author": [
{
"family": "Alexander",
"given": "Madeline Wood"
},
{
"family": "Wood",
"given": "Brendan"
},
{
"family": "Oh",
"given": "Hamilton See-Hwee"
},
{
"family": "Bot",
"given": "Veronica Augustina"
},
{
"family": "Borger",
"given": "Julia"
},
{
"family": "Galbiati",
"given": "Francesca"
},
{
"family": "Walker",
"given": "Keenan A."
},
{
"family": "Resnick",
"given": "Susan M."
},
{
"family": "Ochs-Balcom",
"given": "Heather M."
},
{
"family": "Wyss-Coray",
"given": "Tony"
},
{
"family": "Kooperberg",
"given": "Charles"
},
{
"family": "Reiner",
"given": "Alexander P."
},
{
"family": "Jacobs",
"given": "Emily G."
},
{
"family": "Rabin",
"given": "Jennifer S."
},
{
"family": "Casaletto",
"given": "Kaitlin B."
},
{
"family": "Saloner",
"given": "Rowan"
}
],
"container-title-short": "Res Sq",
"DOI": "10.21203/rs.3.rs-9499814/v1",
"ISSN": "2693-5015",
"publisher": "Research Square",
"URL": "https://doi.org/10.21203/rs.3.rs-9499814/v1",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
4
]
]
}
}

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.64898/2026.04.23.26351500 [code]
Plasma proteomics link menopause timing to brain aging and dementia risk
Journal: medRxiv (preprint)
In common: WGCNA, igraph, ggplot2, 1 other tool, Alzheimer's / dementia, genetics / omics, cellular / molecular, 58 references, 4 authors
[2] doi:10.1093/braincomms/fcag287 [code]
Plasma proteomics reveals molecular overlap between physical activity and dementia risk.
Journal: Brain communications
In common: WGCNA, ggplot2, Alzheimer's / dementia, genetics / omics, 2 references, author Rowan Saloner
[3] doi:10.1038/s41467-026-72091-7 [code]
Coupled cross-sectional and longitudinal non-negative matrix factorization reveals dominant brain aging trajectories in 48,949 individuals.
Journal: Nature communications
In common: Alzheimer's / dementia, 2 authors
[4] doi:10.1523/eneuro.0468-25.2026 [code]
A Multi-Network Approach Identifies Proteins Related to Dendritic Spines in Alzheimer's Disease.
Journal: eNeuro
In common: WGCNA, igraph, ggplot2, 1 other tool, Alzheimer's / dementia, genetics / omics, 2 references
[5] doi:10.64898/2026.03.09.26347914 [code]
Multimodal Ageing Biomarkers and Plasma Proteomic Signatures Associated with All-Cause Mortality
Journal: medRxiv (preprint)
In common: ggplot2, tidyverse, genetics / omics, author Keenan A. Walker
[6] doi:10.34133/csbj.0134 [code]
Integrated Multi-Tissue Transcriptomics Reveals Antagonistic Pleiotropy in Aging and Alzheimer's Disease.
Journal: Computational and structural biotechnology journal
In common: WGCNA, igraph, ggplot2, 1 other tool, Alzheimer's / dementia, genetics / omics, cellular / molecular
[7] doi:10.1038/s41593-026-02367-0 [code]
A reproducible three-dimensional model of human brain tissue to investigate physiological and disease-associated microglia phenotypes.
Journal: Nature neuroscience
In common: WGCNA, igraph, ggplot2, 1 other tool, Alzheimer's / dementia, cellular / molecular
[8] doi:10.1126/sciadv.aeg3223 [code]
The extreme diversity of retinal amacrine cells has deep evolutionary roots.
Journal: Science advances
In common: WGCNA, igraph, ggplot2, 1 other tool, genetics / omics, cellular / molecular
[9] doi:10.1038/s41467-026-75723-0 [code]
Spatial transcriptomics reveals distinct cell type dynamics following opioid dependence in female mice with the common human μ-opioid receptor variant Oprm1 A118G.
Journal: Nature communications
In common: WGCNA, igraph, ggplot2, 1 other tool, genetics / omics, cellular / molecular
[10] doi:10.1038/s41398-026-04200-5 [code]
Postmortem brain single-nucleus and bulk gene expression analyses identify shared and distinct abnormalities in bipolar disorder and major depressive disorder.
Journal: Translational psychiatry
In common: WGCNA, igraph, ggplot2, 1 other tool, genetics / omics, cellular / molecular

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.