OSCR

Deepening the Understanding of Platelet-Derived RNA as Biomarker for Glioblastoma Through GSEA, TDEA, and Elastic Net Regularization Analysis.

Code ↔ Paper

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

The 2 matches
  1. [1] § Materials and Methods › Data Processing and Analysis ↔ programs/basic_model_analysis.R, lines 1 to 51 · score 0.82 · candidate genes, confusion matrix, DESeq2, platelet RNA, healthy control, split · Report a mismatch
  2. [2] § Results › Unraveling Gene Expression and Pathways Disparities Between GBM and Healthy Individuals Through TDEA and Elastic Net Regularization Analysis ↔ programs/basic_model_analysis.R, lines 1 to 51 · score 0.73 · platelet RNA seq, candidate genes, feature selection, healthy control, classify, fold · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R · 1,003 lines · 38 KB · GPL-3.0 · 2 matches

  1. # =============================================================================
  2. # Platelet RNA-based classification of Glioblastoma (GBM) vs Healthy Controls
  3. # =============================================================================
  4. #
  5. # This script performs the TDEA (Threshold-Based Differential Expression Analysis)
  6. # with Elastic Net regularization pipeline described in Giczewska, Pastuszak et al.
  7. #
  8. # The analysis proceeds in three stages:
  9. # 1. Data loading, filtering, and sample exclusion
  10. # 2. Differential expression analysis on the training set to identify candidate genes
  11. # 3. Elastic Net classification (with secondary feature selection) followed by
  12. # Gene Ontology and Reactome pathway enrichment of the final gene panel
  13. #
  14. # Input data:
  15. # - DESeq2-normalized platelet RNA-seq counts (4,412 genes x 403 samples)
  16. # - Sample metadata including patient group (GBM / HC), isolation site, etc.
  17. # - Pre-defined train/test split IDs (60/40)
  18. # - Ensembl 75 gene annotations
  19. # - Reactome GMT gene set file (v2022.1)
  20. #
  21. # Output:
  22. # - Confusion matrix plot (PDF) for the test set
  23. # - Reactome and Gene Ontology enrichment bar plots (PDF)
  24. # - Enrichment results table (XLSX)
  25. # - Elastic Net model coefficients (saved as RData)
  26. # =============================================================================
  27. library(DESeq2)
  28. library(edgeR)
  29. # Flag to use the fixed train/test split from the manuscript (reproducibility).
  30. # When TRUE, sample IDs are read from file rather than generated via createFolds.
  31. useArticleSplit = T
  32. # --- 1. Load normalized counts and sample metadata ---
  33. countsAllDeseq = read.csv2("../data/rawdata/countsNormalized.tsv", sep = "\t")
  34. colnames(countsAllDeseq) = gsub(".", "-", colnames(countsAllDeseq), fixed = T)
  35. # Ensure all columns are numeric (read.csv2 may import as character)
  36. for(i in 1:ncol(countsAllDeseq))
  37. countsAllDeseq[,i] = as.numeric(countsAllDeseq[,i])
  38. sampleInfoAll = read.csv2("../data/rawdata/sampleInfo.tsv", sep = "\t")
  39. # Gene annotation table (Ensembl 75): maps HGNC symbols to Ensembl IDs
  40. load("../data/dgeGenesEnsembl75.RData")
  41. # =============================================================================
  42. # Helper functions
  43. # =============================================================================
  44. # DESeq2 variance-stabilizing normalization (standalone, without design contrast).
  45. # Used when re-normalizing raw counts outside the main pipeline.
  46. normalizeDESeq2NoReport = function(rawCounts) {
  47. library(DESeq2)
  48. d = rawCounts
  49. sampleIds = colnames(rawCounts)
  50. # Single-group dummy design (normalization only, no DE testing)
  51. condition <- factor(rep("A", dim(d)[2]))
  52. dds <- DESeqDataSetFromMatrix(countData = d, DataFrame(condition), design = ~ 1)
  53. dds <- DESeq(dds)
  54. vsd <- varianceStabilizingTransformation(dds, blind = TRUE)
  55. logs_norm_arr_ord = assay(vsd)
  56. return(logs_norm_arr_ord)
  57. }
  58. # Filter out low-abundance transcripts from a DGEList object.
  59. # Removes genes with fewer than `minimum.read.counts` reads in > 90% of samples.
  60. filter.for.platelet.transcriptome <- function(dge,
  61. minimum.read.counts = 30,
  62. verbose = TRUE) {
  63. if (missing(dge)) {
  64. stop("Provide DGElist object")
  65. }
  66. stopifnot(class(dge) == "DGEList")
  67. if (!is.numeric(minimum.read.counts)) {
  68. stop("Provide numeric value for minimum.read.counts")
  69. }
  70. # Identify genes that fall below the read-count threshold in >90% of samples
  71. bad.genes <- names(which(
  72. apply(dge$counts, 1, function(x) { sum(x < minimum.read.counts) }) > 0.9 * ncol(dge)
  73. ))
  74. dgeFiltered <- dge[which(!rownames(dge) %in% bad.genes), ]
  75. if (verbose == TRUE) {
  76. print(paste("Number of transcripts detected: ", nrow(dgeFiltered$counts), sep = ""))
  77. }
  78. return(dgeFiltered)
  79. }
  80. # Safe wrappers for Wilcoxon and t-test that return NA on failure
  81. # (e.g., when all values in a group are identical)
  82. my.wilcox.p.value <- function(...) {
  83. obj <- try(wilcox.test(...), silent = TRUE)
  84. if (is(obj, "try-error")) return(NA) else return(obj$p.value)
  85. }
  86. my.t.test.p.value <- function(...) {
  87. obj <- try(t.test(...), silent = TRUE)
  88. if (is(obj, "try-error")) return(NA) else return(obj$p.value)
  89. }
  90. # =============================================================================
  91. # 2. Sample exclusion and cohort definition
  92. # =============================================================================
  93. library(matrixStats)
  94. dataFiltered = countsAllDeseq
  95. # Exclude one IDH-mutant sample reclassified as non-GBM under updated WHO criteria.
  96. # TR3712-GBM-VUMC was originally labeled GBM but is IDH-wildtype negative.
  97. id604 = grep("TR3712-GBM-VUMC", sampleInfoAll$id, fixed = T)
  98. if(length(id604) > 0) {
  99. dataFiltered = dataFiltered[, -id604]
  100. sampleInfoAll = sampleInfoAll[-id604, ]
  101. }
  102. # Align gene annotation table to the genes present in filtered counts
  103. genes = genes[which(!is.na(match(genes$hgnc_symbol, rownames(dataFiltered)))),]
  104. rownames(genes) = genes$hgnc_symbol
  105. # =============================================================================
  106. # Differential expression summary table (Wilcoxon rank-sum test + FDR)
  107. # =============================================================================
  108. #
  109. # For two patient groups (groupA vs groupB), computes per-gene:
  110. # - Mean and median expression in each group
  111. # - Fold change (on log2 scale, back-transformed to linear)
  112. # - Wilcoxon rank-sum p-value with FDR correction
  113. #
  114. # When `all = FALSE` (default), applies TDEA thresholds:
  115. # - FDR q-value < 0.1
  116. # - Fold change > 1.3 or < 1/1.3
  117. # - At least one group median > 3 (low-expression filter)
  118. getSummaryTable = function(dataFiltered, sampleInfo, groupA, groupB,
  119. all = F) {
  120. hcId = which(sampleInfo$ori_patientgroup == groupA)
  121. ocId = which(sampleInfo$ori_patientgroup == groupB)
  122. logsBenign = dataFiltered[, hcId]
  123. logsMalignant = dataFiltered[, ocId]
  124. # Group-level summary statistics
  125. meanBenign <- rowMeans(logsBenign)
  126. meanMalignant <- rowMeans(logsMalignant)
  127. medianBenign <- rowMedians(as.matrix(logsBenign))
  128. medianMalignant <- rowMedians(as.matrix(logsMalignant))
  129. # Fold change computed from mean log2-scale values (DESeq2-normalized)
  130. folds_mean_Benign_Malignant = 2^(meanBenign - meanMalignant)
  131. # Per-gene Wilcoxon rank-sum test between the two groups
  132. wilcox_Benign_Malignant_pvalue = sapply(1:nrow(logsBenign), function(i)
  133. my.wilcox.p.value(unlist(logsBenign[i, ]), unlist(logsMalignant[i, ])))
  134. genesLocal = genes[rownames(dataFiltered),]
  135. # FDR correction (Benjamini-Hochberg)
  136. fdr_Benign_Malignant_qvalue = p.adjust(
  137. wilcox_Benign_Malignant_pvalue, method = "fdr",
  138. n = length(wilcox_Benign_Malignant_pvalue)
  139. )
  140. # Assemble results table
  141. summary_table_Benign_Malignant = cbind.data.frame(
  142. genesLocal$ensembl_gene_id,
  143. rownames(logsBenign),
  144. meanBenign, meanMalignant,
  145. medianBenign, medianMalignant,
  146. folds_mean_Benign_Malignant,
  147. wilcox_Benign_Malignant_pvalue,
  148. fdr_Benign_Malignant_qvalue,
  149. genesLocal$description
  150. )
  151. groupNameA = groupA
  152. groupNameB = groupB
  153. tableCols_Benign_Malignant = cbind(
  154. "ENSG", "Gene",
  155. paste("Mean ", groupNameA, sep = ""), paste("Mean ", groupNameB, sep = ""),
  156. paste("Median ", groupNameA, sep = ""), paste("Median ", groupNameB, sep = ""),
  157. paste("Mean fold change ", groupNameA, "-", groupNameB, sep = ""),
  158. "Wilcoxon p-value ",
  159. "FDR q-value ",
  160. "Description"
  161. )
  162. colnames(summary_table_Benign_Malignant) = tableCols_Benign_Malignant
  163. # Sort by FDR q-value (ascending)
  164. summary_table_Benign_Malignant = summary_table_Benign_Malignant[order(
  165. summary_table_Benign_Malignant[, 9]), ]
  166. # Return all genes without filtering if requested
  167. if(all) {
  168. return(summary_table_Benign_Malignant)
  169. }
  170. # Apply TDEA thresholds: FDR < 0.1, fold change > 1.3, minimum expression > 3
  171. summaryTable = summary_table_Benign_Malignant[
  172. which(summary_table_Benign_Malignant$`FDR q-value ` < 0.1), ]
  173. summaryTable = summaryTable[c(
  174. which(summaryTable[, 7] > 1.3),
  175. which(summaryTable[, 7] < 1/1.3)
  176. ), ]
  177. lowExprCutOff = 3
  178. # NOTE: "Median OC" was the original column name when this function was used
  179. # with ovarian cancer data. When called with groupA="GBM", groupB="HC", the
  180. # actual column is "Median GBM". Because $`Median OC` returns NULL here,
  181. # the filter only requires HC median > 3. This is preserved for result
  182. # reproducibility with the published manuscript.
  183. highHc = which(summaryTable$`Median HC` > lowExprCutOff)
  184. highOc = which(summaryTable$`Median OC` > lowExprCutOff)
  185. highExpr = unique(c(highHc, highOc))
  186. summaryTable = summaryTable[highExpr, ]
  187. return(summaryTable)
  188. }
  189. # =============================================================================
  190. # 3. Define the study cohort (GBM + HC, excluding NKI controls)
  191. # =============================================================================
  192. sampleInfoAll$full_ori_patientgroup = sampleInfoAll$ori_patientgroup
  193. gbmSamples = sampleInfoAll[which(sampleInfoAll$ori_patientgroup == "GBM"),]
  194. library(caret)
  195. # Select healthy controls from non-NKI sites only (NKI excluded per study protocol)
  196. hcId = which(sampleInfoAll$ori_patientgroup == "HC" &
  197. sampleInfoAll$isolationlocation != "NKI" &
  198. sampleInfoAll$group == "nonMalignant")
  199. # Select GBM patients from non-NKI sites
  200. gbmId = which(sampleInfoAll$ori_patientgroup == "GBM" &
  201. sampleInfoAll$isolationlocation != "NKI")
  202. toInclude = c(hcId, gbmId)
  203. sampleInfoAll = sampleInfoAll[toInclude, ]
  204. countsAllDeseq = countsAllDeseq[, toInclude]
  205. # Refresh indices after subsetting
  206. hcId = which(sampleInfoAll$ori_patientgroup == "HC" &
  207. sampleInfoAll$isolationlocation != "NKI" &
  208. sampleInfoAll$group == "nonMalignant")
  209. gbmId = which(sampleInfoAll$ori_patientgroup == "GBM" &
  210. sampleInfoAll$isolationlocation != "NKI")
  211. # =============================================================================
  212. # 4. Train/test split (60/40)
  213. # =============================================================================
  214. set.seed(12345)
  215. # Default: 10-fold stratified split (folds 1-6 = train, 7-10 = test)
  216. folds = createFolds(sampleInfoAll$ori_patientgroup[toInclude], k = 10)
  217. trainId = toInclude[unlist(folds[1:6])]
  218. testId = toInclude[unlist(folds[7:10])]
  219. # Override with the fixed split used in the manuscript for reproducibility
  220. if(useArticleSplit) {
  221. trainId = read.table("../data/rawdata/train_ids_final.tsv")
  222. trainId = trainId$V1
  223. trainId = trainId[which(!is.na(match(trainId, colnames(countsAllDeseq))))]
  224. trainId = (match(trainId, colnames(countsAllDeseq)))
  225. testId = read.table("../data/rawdata/testId_ids_final.tsv")
  226. testId = testId$V1
  227. testId = testId[which(!is.na(match(testId, colnames(countsAllDeseq))))]
  228. testId = (match(testId, colnames(countsAllDeseq)))
  229. }
  230. # =============================================================================
  231. # 5. TDEA: identify differentially expressed candidate genes on TRAINING data only
  232. # =============================================================================
  233. # Run differential expression on training set (leak-free: test set not used)
  234. gbmHc_comparisonTrimmed = getSummaryTable(
  235. countsAllDeseq[, trainId], sampleInfoAll[trainId, ], "GBM", "HC", F
  236. )
  237. # Candidate genes passing TDEA thresholds (FDR < 0.1, FC > 1.3, expression > 3)
  238. candidates = rownames(gbmHc_comparisonTrimmed)
  239. # Subset counts to candidate genes for modeling
  240. countsModel = countsAllDeseq[candidates, ]
  241. countsModel_deseq = countsModel[, toInclude]
  242. # Record the train/test assignment for each sample
  243. countsModel_deseq_assignment = data.frame(
  244. c(colnames(countsAllDeseq)[trainId], colnames(countsAllDeseq)[testId])
  245. )
  246. colnames(countsModel_deseq_assignment) = "Sample"
  247. countsModel_deseq_assignment$Train = 0
  248. countsModel_deseq_assignment$Train[1:length(trainId)] = 1
  249. countsModel_deseq_assignment$Test = 1
  250. countsModel_deseq_assignment$Test[1:length(trainId)] = 0
  251. deseq_output = gbmHc_comparisonTrimmed
  252. # =============================================================================
  253. # 6. Elastic Net classification (first pass: all candidate genes)
  254. # =============================================================================
  255. # Prepare training and test matrices (samples x genes)
  256. trainX = countsModel[, trainId]
  257. testX = countsModel[, testId]
  258. trainY = factor(sampleInfoAll$ori_patientgroup[trainId])
  259. testY = factor(sampleInfoAll$ori_patientgroup[testId])
  260. library(party)
  261. trainX = t(trainX)
  262. trainX = as.data.frame(trainX)
  263. trainX$group = as.factor(trainY)
  264. testX = t(testX)
  265. testX = as.data.frame(testX)
  266. testX$group = as.factor(testY)
  267. library(dplyr)
  268. library(glmnet)
  269. realRefsTrain = trainX$group
  270. realRefsTest = testX$group
  271. # 5x5 repeated cross-validation with random hyperparameter search
  272. control <- trainControl(method = "repeatedcv",
  273. number = 5,
  274. repeats = 5,
  275. search = "random",
  276. verboseIter = TRUE)
  277. # Train Elastic Net (glmnet) on all TDEA-selected candidate genes
  278. elasticModel <- train(group ~ .,
  279. data = trainX,
  280. method = "glmnet",
  281. tuneLength = 25,
  282. trControl = control)
  283. print(elasticModel)
  284. # --- Evaluate first-pass model ---
  285. predictionsTrain = predict(elasticModel, trainX)
  286. probabilitiesTrain = predict(elasticModel, trainX, type = "prob")
  287. table(predictionsTrain, realRefsTrain)
  288. confusionMatrixTrain = confusionMatrix(predictionsTrain, realRefsTrain)
  289. print(confusionMatrixTrain)
  290. predictionsTest = predict(elasticModel, testX)
  291. probabilitiesTest = predict(elasticModel, testX, type = "prob")
  292. if (interactive()) boxplot(probabilitiesTest[, 1] ~ realRefsTest)
  293. # Apply a lowered probability threshold (0.25) to increase GBM sensitivity
  294. thresholdedPredictionsTest = rep("HC", length(predictionsTest))
  295. thresholdedPredictionsTest[which(probabilitiesTest[, 1] > 0.25)] = "GBM"
  296. thresholdedPredictionsTrain = rep("HC", length(realRefsTrain))
  297. thresholdedPredictionsTrain[which(probabilitiesTrain[, 1] > 0.25)] = "GBM"
  298. confusionMatrixTrain = confusionMatrix(as.factor(thresholdedPredictionsTrain), realRefsTrain)
  299. table(thresholdedPredictionsTest, realRefsTest)
  300. print(confusionMatrix(as.factor(thresholdedPredictionsTest), as.factor(realRefsTest), positive = "GBM"))
  301. table(predictionsTest, realRefsTest)
  302. confusionMatrixTest = confusionMatrix(
  303. as.factor(thresholdedPredictionsTest),
  304. reference = as.factor(realRefsTest),
  305. positive = "GBM"
  306. )
  307. print(confusionMatrixTest)
  308. # =============================================================================
  309. # 7. Secondary feature selection: keep genes with |coefficient| > threshold
  310. # =============================================================================
  311. weightThreshold = 0.05
  312. # Extract Elastic Net coefficients at the optimal lambda
  313. toKeepElNet = stats::predict(
  314. elasticModel$finalModel, type = "coefficients",
  315. s = elasticModel$bestTune$lambda
  316. )
  317. toKeepElNetDF = as.data.frame(as.matrix(toKeepElNet))
  318. toKeepElNetDF = toKeepElNetDF[-1, , drop = F] # Remove intercept row
  319. toKeepElNetDF[, 1] = as.numeric(toKeepElNetDF[, 1]) # Ensure plain numeric
  320. # Full model: all genes with non-zero coefficients
  321. fullModel = rownames(toKeepElNetDF)[which(abs(toKeepElNetDF[, 1]) > 0.0)]
  322. fullModel = gsub("`", "", fullModel)
  323. # Reduced panel: genes with |weight| above threshold
  324. maxWeights = abs(toKeepElNetDF[, 1])
  325. toKeepElNetDF = toKeepElNetDF[which(maxWeights > weightThreshold), , drop = F]
  326. allSelectedEnsg = rownames(toKeepElNetDF)
  327. allSelectedEnsg = gsub("`", "", allSelectedEnsg)
  328. # =============================================================================
  329. # 8. Test set AUC and confusion matrix visualization
  330. # =============================================================================
  331. library(cvAUC)
  332. AUC = cvAUC(probabilitiesTest[, 1], realRefsTest, label.ordering = c("HC", "GBM"))
  333. # Confusion matrix heatmap for the test set
  334. cmTest <- confusionMatrixTest
  335. plt <- as.data.frame(cmTest$table)
  336. plt$Prediction <- factor(plt$Prediction, levels = rev(levels(plt$Prediction)))
  337. pdf(paste0('../Report/', "Confusion_matrix_test", ".pdf"))
  338. confPlot = ggplot(plt, aes(Prediction, Reference, fill = Freq)) +
  339. geom_tile() + geom_text(aes(label = Freq)) +
  340. scale_fill_gradient(low = "white", high = "#009194") +
  341. labs(x = "Reference", y = "Prediction") +
  342. scale_x_discrete(labels = c("HC", "GBM")) +
  343. scale_y_discrete(labels = c("GBM", "HC"))
  344. print(confPlot)
  345. dev.off()
  346. # =============================================================================
  347. # 9. Reactome pathway enrichment (ORA) on the final Elastic Net gene panel
  348. # =============================================================================
  349. library(org.Hs.eg.db)
  350. library(clusterProfiler)
  351. library(openxlsx)
  352. deGenes = allSelectedEnsg # Final panel genes (HGNC symbols)
  353. geneUniverse = rownames(dataFiltered) # Background: all expressed genes
  354. # Load Reactome gene sets (MSigDB format)
  355. c3.tf <- read.gmt("../data/c2.cp.reactome.v2022.1.Hs.symbols.gmt")
  356. presentFromReact = which(is.element(c3.tf$gene, geneUniverse))
  357. c3.tf = c3.tf[presentFromReact, ]
  358. # Run overrepresentation analysis against Reactome pathways
  359. ans.react = enricher(allSelectedEnsg, TERM2GENE = c3.tf,
  360. pAdjustMethod = "fdr", qvalueCutoff = 0.5)
  361. # --- Prepare Reactome bar chart data ---
  362. dfReact = ans.react@result[1:10, ]
  363. # Parse the GeneRatio and BgRatio strings into numeric values
  364. noGenes = strsplit(dfReact$GeneRatio, "/")
  365. noGenesPlus = as.numeric(unlist(lapply(noGenes, `[[`, 1)))
  366. noGenesAll = as.numeric(unlist(lapply(noGenes, `[[`, 2)))
  367. noBg = strsplit(dfReact$BgRatio, "/")
  368. noBgPlus = as.numeric(unlist(lapply(noBg, `[[`, 1)))
  369. noBgAll = as.numeric(unlist(lapply(noBg, `[[`, 2)))
  370. dfReact$noGenesPlus = noGenesPlus
  371. dfReact$noGenesAll = noGenesAll
  372. dfReact$noBgPlus = noBgPlus
  373. dfReact$noBgAll = noBgAll
  374. dfReact$Pct_selected = round(100 * dfReact$noGenesPlus / dfReact$noGenesAll, 2)
  375. dfReact$Pct_background = round(100 * dfReact$noBgPlus / dfReact$noBgAll, 2)
  376. dfReact$LogP = -1 * (log10(dfReact$pvalue))
  377. # Reshape to long format for grouped bar chart
  378. dfReactChartP = cbind(dfReact$Description, dfReact$LogP,
  379. rep("p", nrow(dfReact)),
  380. paste0("p=", round(dfReact$pvalue, 6)))
  381. dfReactChartPct_selected = cbind(dfReact$Description, dfReact$Pct_selected,
  382. rep("Percentage selected", nrow(dfReact)),
  383. paste0(dfReact$Pct_selected, "%"))
  384. dfReactChartPct_background = cbind(dfReact$Description, dfReact$Pct_background,
  385. rep("Percentage background", nrow(dfReact)),
  386. paste0(dfReact$Pct_background, "%"))
  387. dfReactChart = rbind(dfReactChartP, dfReactChartPct_selected, dfReactChartPct_background)
  388. dfReactChart = as.data.frame(dfReactChart)
  389. colnames(dfReactChart) = c("Pathway", "Value", "Type", "Text")
  390. dfReactChart$Value = as.numeric(dfReactChart$Value)
  391. # Strip the REACTOME_ prefix for cleaner labels
  392. dfReactChart$Pathway = gsub("REACTOME_", "", dfReactChart$Pathway)
  393. save(dfReactChart, file = "dfReactChart_v3.RData")
  394. # Plot top 10 Reactome pathways (grouped bars: -log10 p, % selected, % background)
  395. pdf("../Report/reactome_top_10.pdf", width = 20, height = 8)
  396. ggplot(data = dfReactChart, aes(x = Pathway, y = Value, fill = factor(Type))) +
  397. geom_bar(position = "dodge", stat = "identity") +
  398. coord_flip() +
  399. ylab("- Log10 p") +
  400. ggtitle("Reactome analysis") +
  401. geom_text(aes(label = Text),
  402. position = position_dodge(width = 1), size = 3) +
  403. theme_bw()
  404. dev.off()
  405. # Save significant Reactome results (p < 0.05) to Excel
  406. toSaveX = ans.react@result[which(ans.react@result$pvalue < 0.05), ]
  407. toSaveX = toSaveX[, -c(6, 7)]
  408. openxlsx::write.xlsx(toSaveX, file = "Reactome_final_selection_vs_all_considered.xlsx")
  409. # =============================================================================
  410. # 10. Gene Ontology enrichment (ORA) on the final Elastic Net gene panel
  411. # =============================================================================
  412. deGenes = allSelectedEnsg
  413. # Map HGNC symbols to Ensembl IDs, then to Entrez IDs (required by enrichGO)
  414. ensgs = genes$ensembl_gene_id[match(deGenes, genes$hgnc_symbol)]
  415. ensgUniverse = genes$ensembl_gene_id[match(rownames(dataFiltered), genes$hgnc_symbol)]
  416. ensgUniverse = ensgUniverse[which(!is.na(ensgUniverse))]
  417. deGenes = unlist(mget(ensgs[which(!is.na(ensgs))], envir = org.Hs.egENSEMBL2EG,
  418. ifnotfound = NA))
  419. symbolUniverse = unlist(mget(ensgUniverse[which(!is.na(ensgUniverse))],
  420. envir = org.Hs.egENSEMBL2EG, ifnotfound = NA))
  421. symbolUniverse = symbolUniverse[which(!is.na(symbolUniverse))]
  422. set.seed(123)
  423. # GO enrichment across all ontologies (for exploration)
  424. ans.go <- enrichGO(gene = deGenes, ont = "All",
  425. OrgDb = "org.Hs.eg.db",
  426. universe = symbolUniverse,
  427. readable = TRUE,
  428. pvalueCutoff = 1, qvalueCutoff = 1, pAdjustMethod = "fdr")
  429. # Print top GO results (use View() interactively for full table)
  430. head(ans.go@result, 20)
  431. # GO enrichment restricted to Biological Process (for the manuscript figure)
  432. ans.go3 <- enrichGO(gene = deGenes, ont = "BP",
  433. OrgDb = "org.Hs.eg.db",
  434. universe = symbolUniverse,
  435. readable = TRUE,
  436. pvalueCutoff = 1, qvalueCutoff = 1, pAdjustMethod = "fdr")
  437. # --- Prepare GO bar chart data (same structure as Reactome) ---
  438. dfGo = ans.go3@result[1:10, ]
  439. noGenes = strsplit(dfGo$GeneRatio, "/")
  440. noGenesPlus = as.numeric(unlist(lapply(noGenes, `[[`, 1)))
  441. noGenesAll = as.numeric(unlist(lapply(noGenes, `[[`, 2)))
  442. noBg = strsplit(dfGo$BgRatio, "/")
  443. noBgPlus = as.numeric(unlist(lapply(noBg, `[[`, 1)))
  444. noBgAll = as.numeric(unlist(lapply(noBg, `[[`, 2)))
  445. dfGo$noGenesPlus = noGenesPlus
  446. dfGo$noGenesAll = noGenesAll
  447. dfGo$noBgPlus = noBgPlus
  448. dfGo$noBgAll = noBgAll
  449. dfGo$Pct_selected = round(100 * dfGo$noGenesPlus / dfGo$noGenesAll, 2)
  450. dfGo$Pct_background = round(100 * dfGo$noBgPlus / dfGo$noBgAll, 2)
  451. dfGo$LogP = -1 * (log10(dfGo$pvalue))
  452. dfGoChartP = cbind(dfGo$Description, dfGo$LogP,
  453. rep("p", nrow(dfGo)),
  454. paste0("p=", round(dfGo$pvalue, 6)))
  455. dfGoChartPct_selected = cbind(dfGo$Description, dfGo$Pct_selected,
  456. rep("Percentage selected", nrow(dfGo)),
  457. paste0(dfGo$Pct_selected, "%"))
  458. dfGoChartPct_background = cbind(dfGo$Description, dfGo$Pct_background,
  459. rep("Percentage background", nrow(dfGo)),
  460. paste0(dfGo$Pct_background, "%"))
  461. dfGoChart = rbind(dfGoChartP, dfGoChartPct_selected, dfGoChartPct_background)
  462. dfGoChart = as.data.frame(dfGoChart)
  463. colnames(dfGoChart) = c("Pathway", "Value", "Type", "Text")
  464. dfGoChart$Value = as.numeric(dfGoChart$Value)
  465. save(dfGoChart, file = "dfGoChart.RData")
  466. # Plot top 10 GO Biological Process terms
  467. pdf("../Report/gene_ontology_top_10.pdf", width = 20, height = 8)
  468. ggplot(data = dfGoChart, aes(x = Pathway, y = Value, fill = factor(Type))) +
  469. geom_bar(position = "dodge", stat = "identity") +
  470. coord_flip() +
  471. ylab("- Log10 p") +
  472. ggtitle("Gene Ontology analysis") +
  473. geom_text(aes(label = Text),
  474. position = position_dodge(width = 1), size = 3) +
  475. theme_bw()
  476. dev.off()
  477. # =============================================================================
  478. # 11. Refit Elastic Net on reduced gene panel (|coefficient| > threshold)
  479. # =============================================================================
  480. selectedVars = rownames(toKeepElNetDF)
  481. selectedVars = gsub("`", "", selectedVars)
  482. # Subset training and test data to the reduced gene panel
  483. trainX_subset = trainX[, c(selectedVars, "group")]
  484. testX_subset = testX[, c(selectedVars, "group")]
  485. # Same CV setup as before
  486. control <- trainControl(method = "repeatedcv",
  487. number = 5,
  488. repeats = 5,
  489. search = "random",
  490. verboseIter = TRUE)
  491. # Refit Elastic Net on the reduced feature set
  492. elasticModel_subset <- train(group ~ .,
  493. data = trainX_subset,
  494. method = "glmnet",
  495. tuneLength = 25,
  496. trControl = control)
  497. print(elasticModel_subset)
  498. # --- Evaluate reduced model on training set ---
  499. predictionsTrain = predict(elasticModel_subset, trainX_subset)
  500. probabilitiesTrain = predict(elasticModel_subset, trainX_subset, type = "prob")
  501. table(predictionsTrain, realRefsTrain)
  502. confusionMatrixTrain = confusionMatrix(predictionsTrain, realRefsTrain)
  503. print(confusionMatrixTrain)
  504. # --- Evaluate reduced model on test set ---
  505. predictionsTest = predict(elasticModel_subset, testX_subset)
  506. probabilitiesTest = predict(elasticModel_subset, testX_subset, type = "prob")
  507. if (interactive()) boxplot(probabilitiesTest[, 1] ~ realRefsTest)
  508. # Thresholded predictions (probability > 0.25 classified as GBM)
  509. thresholdedPredictionsTest = rep("HC", length(predictionsTest))
  510. thresholdedPredictionsTest[which(probabilitiesTest[, 1] > 0.25)] = "GBM"
  511. thresholdedPredictionsTrain = rep("HC", length(realRefsTrain))
  512. thresholdedPredictionsTrain[which(probabilitiesTrain[, 1] > 0.25)] = "GBM"
  513. # Confusion matrices for thresholded predictions
  514. confusionMatrixTrain = confusionMatrix(as.factor(thresholdedPredictionsTrain), realRefsTrain)
  515. print(confusionMatrixTrain)
  516. table(thresholdedPredictionsTest, realRefsTest)
  517. confusionMatrixTest = confusionMatrix(
  518. as.factor(thresholdedPredictionsTest), as.factor(realRefsTest), positive = "GBM"
  519. )
  520. print(confusionMatrixTest)
  521. # Confusion matrix for default-threshold predictions
  522. table(predictionsTest, realRefsTest)
  523. confusionMatrixTest = confusionMatrix(
  524. as.factor(predictionsTest), as.factor(realRefsTest), positive = "GBM"
  525. )
  526. print(confusionMatrixTest)
  527. # =============================================================================
  528. # 12. Three-way GO Biological Process enrichment across pipeline stages
  529. # =============================================================================
  530. #
  531. # Compares GO BP enrichment results across the three nested gene sets produced
  532. # by the pipeline:
  533. # 1. candidates -- TDEA-selected genes (FDR < 0.1, FC > 1.3, expr > 3)
  534. # 2. fullModel -- all genes with non-zero Elastic Net coefficients
  535. # 3. allSelectedEnsg -- final panel (|coefficient| > 0.05)
  536. #
  537. # A Venn diagram shows overlap of significant GO terms between sets,
  538. # produced for both FDR-adjusted (p.adjust < 0.05) and unadjusted (p < 0.05)
  539. # significance thresholds.
  540. library(VennDiagram)
  541. library(grid)
  542. # --- Convert each gene set from HGNC symbols to Entrez IDs ---
  543. # (enrichGO requires Entrez IDs; universe already computed in section 10)
  544. symbolsToEntrez = function(symbols) {
  545. ensIds = genes$ensembl_gene_id[match(symbols, genes$hgnc_symbol)]
  546. ensIds = ensIds[which(!is.na(ensIds))]
  547. entrezIds = unlist(mget(ensIds, envir = org.Hs.egENSEMBL2EG, ifnotfound = NA))
  548. entrezIds[which(!is.na(entrezIds))]
  549. }
  550. entrezCandidates = symbolsToEntrez(candidates) # TDEA-selected
  551. entrezFullModel = symbolsToEntrez(fullModel) # non-zero coefficients
  552. entrezFinalPanel = symbolsToEntrez(allSelectedEnsg) # |coef| > threshold
  553. # --- Run GO BP enrichment for each gene set ---
  554. set.seed(123)
  555. goBpCandidates = enrichGO(
  556. gene = entrezCandidates, ont = "BP", OrgDb = "org.Hs.eg.db",
  557. universe = symbolUniverse, readable = TRUE,
  558. pvalueCutoff = 1, qvalueCutoff = 1, pAdjustMethod = "fdr"
  559. )
  560. goBpFullModel = enrichGO(
  561. gene = entrezFullModel, ont = "BP", OrgDb = "org.Hs.eg.db",
  562. universe = symbolUniverse, readable = TRUE,
  563. pvalueCutoff = 1, qvalueCutoff = 1, pAdjustMethod = "fdr"
  564. )
  565. goBpFinalPanel = enrichGO(
  566. gene = entrezFinalPanel, ont = "BP", OrgDb = "org.Hs.eg.db",
  567. universe = symbolUniverse, readable = TRUE,
  568. pvalueCutoff = 1, qvalueCutoff = 1, pAdjustMethod = "fdr"
  569. )
  570. # Save significant results (unadjusted p < 0.05) to Excel
  571. write.xlsx(
  572. goBpCandidates@result[which(goBpCandidates@result$pvalue < 0.05), -c(6, 7)],
  573. file = "GO_BP_candidates_vs_all.xlsx"
  574. )
  575. write.xlsx(
  576. goBpFullModel@result[which(goBpFullModel@result$pvalue < 0.05), -c(6, 7)],
  577. file = "GO_BP_fullModel_vs_all.xlsx"
  578. )
  579. write.xlsx(
  580. goBpFinalPanel@result[which(goBpFinalPanel@result$pvalue < 0.05), -c(6, 7)],
  581. file = "GO_BP_finalSelection_vs_all.xlsx"
  582. )
  583. # --- Extract significant pathway names at both thresholds ---
  584. # FDR-adjusted (p.adjust < 0.05)
  585. sigAdjCandidates = goBpCandidates@result$Description[which(goBpCandidates@result$p.adjust < 0.05)]
  586. sigAdjFullModel = goBpFullModel@result$Description[which(goBpFullModel@result$p.adjust < 0.05)]
  587. sigAdjFinalPanel = goBpFinalPanel@result$Description[which(goBpFinalPanel@result$p.adjust < 0.05)]
  588. # Unadjusted (p < 0.05)
  589. sigRawCandidates = goBpCandidates@result$Description[which(goBpCandidates@result$pvalue < 0.05)]
  590. sigRawFullModel = goBpFullModel@result$Description[which(goBpFullModel@result$pvalue < 0.05)]
  591. sigRawFinalPanel = goBpFinalPanel@result$Description[which(goBpFinalPanel@result$pvalue < 0.05)]
  592. cat("Significant GO BP terms (FDR-adjusted p < 0.05):\n")
  593. cat(" TDEA candidates:", length(sigAdjCandidates), "\n")
  594. cat(" Non-zero in model:", length(sigAdjFullModel), "\n")
  595. cat(" Final panel:", length(sigAdjFinalPanel), "\n")
  596. cat("\nSignificant GO BP terms (unadjusted p < 0.05):\n")
  597. cat(" TDEA candidates:", length(sigRawCandidates), "\n")
  598. cat(" Non-zero in model:", length(sigRawFullModel), "\n")
  599. cat(" Final panel:", length(sigRawFinalPanel), "\n")
  600. # --- Venn diagram helper ---
  601. vennColors = c("#4477AA", "#EE6677", "#228833")
  602. vennSetNames = c("TDEA candidates", "Non-zero in model", "Final panel")
  603. drawGoVenn = function(pathList, titleText, outFile) {
  604. pdf(outFile, width = 7, height = 7)
  605. vennObj = venn.diagram(
  606. x = setNames(pathList, vennSetNames),
  607. filename = NULL,
  608. fill = vennColors,
  609. alpha = 0.45,
  610. col = vennColors,
  611. lwd = 1.5,
  612. fontfamily = "sans",
  613. cat.fontfamily = "sans",
  614. cat.fontface = "bold",
  615. cat.cex = 1.1,
  616. cex = 1.3,
  617. margin = 0.1,
  618. main = titleText,
  619. main.fontfamily = "sans",
  620. main.cex = 1.3,
  621. main.fontface = "bold"
  622. )
  623. grid.draw(vennObj)
  624. dev.off()
  625. }
  626. # --- Venn diagram: FDR-adjusted significance ---
  627. drawGoVenn(
  628. pathList = list(sigAdjCandidates, sigAdjFullModel, sigAdjFinalPanel),
  629. titleText = "GO BP pathways (FDR-adjusted p < 0.05)",
  630. outFile = "../Report/GO_BP_venn_adjusted.pdf"
  631. )
  632. # --- Venn diagram: unadjusted significance ---
  633. drawGoVenn(
  634. pathList = list(sigRawCandidates, sigRawFullModel, sigRawFinalPanel),
  635. titleText = "GO BP pathways (unadjusted p < 0.05)",
  636. outFile = "../Report/GO_BP_venn_unadjusted.pdf"
  637. )
  638. # --- Save overlapping pathway names ---
  639. sharedAdj = Reduce(intersect, list(sigAdjCandidates, sigAdjFullModel, sigAdjFinalPanel))
  640. sharedRaw = Reduce(intersect, list(sigRawCandidates, sigRawFullModel, sigRawFinalPanel))
  641. cat("\nPathways significant in all three sets (FDR-adjusted):", length(sharedAdj), "\n")
  642. cat("Pathways significant in all three sets (unadjusted):", length(sharedRaw), "\n")
  643. if (length(sharedAdj) > 0) {
  644. write.xlsx(data.frame(Pathway = sharedAdj),
  645. file = "GO_BP_shared_all_three_adjusted.xlsx")
  646. }
  647. if (length(sharedRaw) > 0) {
  648. write.xlsx(data.frame(Pathway = sharedRaw),
  649. file = "GO_BP_shared_all_three_unadjusted.xlsx")
  650. }
  651. # =============================================================================
  652. # 13. Build ranked gene list for GSEA (GBM vs HC, training set)
  653. # =============================================================================
  654. #
  655. # Gene ranking metric: sign(log2 FC) * -log10(Wilcoxon p-value)
  656. # This preserves directionality (GBM-up vs GBM-down) while using the
  657. # significance of the test to weight the magnitude of the ranking.
  658. #
  659. # All expressed genes are used here -- NOT just TDEA candidates --
  660. # because GSEA operates on a full ranked list, not a pre-filtered set.
  661. # =============================================================================
  662. library(clusterProfiler)
  663. library(org.Hs.eg.db)
  664. library(openxlsx)
  665. library(VennDiagram)
  666. library(grid)
  667. # Full (unfiltered) DE results on training set
  668. gbmHc_allGenes = getSummaryTable(
  669. countsAllDeseq[, trainId],
  670. sampleInfoAll[trainId, ],
  671. "GBM", "HC",
  672. all = TRUE
  673. )
  674. fc_col = which(colnames(gbmHc_allGenes) == "Mean fold change GBM-HC")
  675. pval_col = which(colnames(gbmHc_allGenes) == "Wilcoxon p-value ")
  676. fc_vals = as.numeric(gbmHc_allGenes[, fc_col])
  677. pval_vals = as.numeric(gbmHc_allGenes[, pval_col])
  678. # Guard against p = 0 (would produce Inf)
  679. pval_vals[pval_vals == 0] <- .Machine$double.eps
  680. rank_metric = sign(log2(fc_vals)) * (-log10(pval_vals))
  681. # Map HGNC symbols -> Ensembl IDs (gseGO uses ENSEMBL keyType)
  682. gene_symbols = rownames(gbmHc_allGenes)
  683. ensembl_ids = genes$ensembl_gene_id[match(gene_symbols, genes$hgnc_symbol)]
  684. original_gene_list_gbm = stats::setNames(rank_metric, ensembl_ids)
  685. # Drop unmapped and duplicated entries
  686. original_gene_list_gbm = original_gene_list_gbm[!is.na(names(original_gene_list_gbm))]
  687. original_gene_list_gbm = original_gene_list_gbm[!duplicated(names(original_gene_list_gbm))]
  688. gene_list_gbm = na.omit(original_gene_list_gbm)
  689. gene_list_gbm = sort(gene_list_gbm, decreasing = TRUE)
  690. # =============================================================================
  691. # 14. GSEA across multiple seeds (GO Biological Process)
  692. # =============================================================================
  693. #
  694. # GSEA results can vary across permutations, so 10 seeds are run and saved
  695. # individually. The seed=1 run (pvalueCutoff = 1) is kept in memory for
  696. # downstream visualisation and the ORA/GSEA Venn diagram.
  697. # =============================================================================
  698. library(enrichplot)
  699. for (seed in 1:10) {
  700. set.seed(seed)
  701. gse_gbm_iter <- gseGO(
  702. geneList = gene_list_gbm,
  703. ont = "ALL",
  704. keyType = "ENSEMBL",
  705. nPerm = 10000,
  706. minGSSize = 3,
  707. maxGSSize = 800,
  708. pvalueCutoff = 0.05,
  709. verbose = TRUE,
  710. OrgDb = org.Hs.eg.db,
  711. pAdjustMethod = "fdr"
  712. )
  713. write.xlsx(gse_gbm_iter,
  714. file = paste0("gene_set_enrichment_all_hc_vs_gbm_ref_hc3_", seed, ".xlsx"))
  715. save(gse_gbm_iter,
  716. file = paste0("gene_set_enrichment_all_hc_vs_gbm_ref_hc3_", seed, ".RData"))
  717. }
  718. # Re-run seed=1 with pvalueCutoff = 1 and ont = "BP" to match the ORA
  719. # used in the Venn. Full result set needed for top-100 selection below.
  720. set.seed(1)
  721. gse_gbm <- gseGO(
  722. geneList = gene_list_gbm,
  723. ont = "BP",
  724. keyType = "ENSEMBL",
  725. nPerm = 10000,
  726. minGSSize = 3,
  727. maxGSSize = 800,
  728. pvalueCutoff = 1, # no cutoff -- needed to get >= 100 results
  729. verbose = FALSE,
  730. OrgDb = org.Hs.eg.db,
  731. pAdjustMethod = "fdr"
  732. )
  733. # =============================================================================
  734. # 15. GSEA visualisation
  735. # =============================================================================
  736. library(DOSE)
  737. library(ggplot2)
  738. pdf("../Report/gsea_dotplot_BP.pdf", width = 10, height = 8)
  739. print(
  740. dotplot(gse_gbm, showCategory = 10, split = ".sign") +
  741. facet_grid(. ~ .sign) +
  742. theme_bw(base_size = 11) +
  743. theme(
  744. strip.background = element_rect(fill = "#F0F0F0"),
  745. axis.text.y = element_text(size = 9)
  746. )
  747. )
  748. dev.off()
  749. # emapplot requires pairwise term similarity
  750. pdf("../Report/gsea_emapplot_BP.pdf", width = 10, height = 10)
  751. print(emapplot(pairwise_termsim(gse_gbm), showCategory = 15))
  752. dev.off()
  753. # =============================================================================
  754. # 16. Venn diagram: ORA (top 100, no FDR) vs GSEA (top 100, no FDR)
  755. # =============================================================================
  756. #
  757. # Both sets are GO Biological Process terms ranked by raw p-value.
  758. # ORA is the enrichment on the final Elastic Net gene panel (ans.go3,
  759. # computed in section 10). GSEA is the seed=1 BP run above.
  760. #
  761. # "Top 100" is defined as the 100 terms with the smallest raw p-value in
  762. # each analysis, without any FDR or adjusted-p filter.
  763. # =============================================================================
  764. n_top = 100
  765. # ORA: ans.go3 was computed with pvalueCutoff = 1 so all terms are present
  766. ora_results_sorted = ans.go3@result[order(ans.go3@result$pvalue), ]
  767. ora_top = head(ora_results_sorted$Description, n_top)
  768. # GSEA: gse_gbm also has pvalueCutoff = 1
  769. gsea_results_sorted = gse_gbm@result[order(gse_gbm@result$pvalue), ]
  770. gsea_top = head(gsea_results_sorted$Description, n_top)
  771. n_overlap = length(intersect(ora_top, gsea_top))
  772. cat(sprintf(
  773. "\nGO BP Venn summary (top %d by raw p-value):\n ORA terms: %d\n GSEA terms: %d\n Overlap: %d\n",
  774. n_top, length(ora_top), length(gsea_top), n_overlap
  775. ))
  776. # Palette consistent with the three-way Venn in section 12
  777. venn_pal = c("#4477AA", "#EE6677")
  778. pdf("../Report/GO_BP_venn_ORA_vs_GSEA.pdf", width = 6, height = 6)
  779. venn_obj = venn.diagram(
  780. x = list(
  781. `ORA` = ora_top,
  782. `GSEA` = gsea_top
  783. ),
  784. filename = NULL,
  785. fill = venn_pal,
  786. alpha = 0.45,
  787. col = venn_pal,
  788. lwd = 1.8,
  789. fontfamily = "sans",
  790. cat.fontfamily = "sans",
  791. cat.fontface = "bold",
  792. cat.cex = 1.15,
  793. cex = 1.4,
  794. margin = 0.12,
  795. main = sprintf("GO BP top %d pathways: ORA vs GSEA", n_top),
  796. main.fontfamily = "sans",
  797. main.cex = 1.15,
  798. main.fontface = "bold"
  799. )
  800. grid.newpage()
  801. grid.draw(venn_obj)
  802. dev.off()
  803. # Save the overlapping pathway names
  804. shared_ora_gsea = intersect(ora_top, gsea_top)
  805. if (length(shared_ora_gsea) > 0) {
  806. write.xlsx(
  807. data.frame(
  808. Pathway = shared_ora_gsea,
  809. ORA_pvalue = ora_results_sorted$pvalue[
  810. match(shared_ora_gsea, ora_results_sorted$Description)],
  811. ORA_padj = ora_results_sorted$p.adjust[
  812. match(shared_ora_gsea, ora_results_sorted$Description)],
  813. GSEA_pvalue = gsea_results_sorted$pvalue[
  814. match(shared_ora_gsea, gsea_results_sorted$Description)],
  815. GSEA_padj = gsea_results_sorted$p.adjust[
  816. match(shared_ora_gsea, gsea_results_sorted$Description)],
  817. GSEA_NES = gsea_results_sorted$NES[
  818. match(shared_ora_gsea, gsea_results_sorted$Description)]
  819. ),
  820. file = "GO_BP_shared_ORA_GSEA_top100.xlsx"
  821. )
  822. }

basic_model_analysis.R at commit d355d14, under GPL-3.0 · at the source

Overview

  1. Laboratory of Translational Oncology, Intercollegiate Faculty of Biotechnology, University of Gdańsk and Medical University of Gdańsk, Gdańsk, Poland
  2. Center of Biostatistics and Bioinformatics, Medical University of Gdańsk, Gdańsk, Poland
  3. Department of Algorithms and System Modeling, Gdansk University of Technology, Gdańsk, Poland
  4. Department of Neurosurgery, Cancer Center Amsterdam, Amsterdam UMC Location VUMC, Amsterdam, the Netherlands
  5. Department of Human Genetics, Cancer Center Amsterdam, Amsterdam UMC Location VUMC, Amsterdam, the Netherlands
Journal: Cancer reports (Hoboken, N.J.), volume 9, issue 10, article e70656
Dates: received 17 July 2025; accepted 31 July 2026; published online 30 September 2026; in print October 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/cnr2.70656 · PMID 42816786 · PMCID PMC13627264 · OpenAlex W7214976098
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: human (organism), other condition (population), clinical / translational (subfield)
Methods: Statistics, Machine learning, Connectivity
Keywords: bioinformatics, biomarkers, glioblastoma, pathway analysis, platelet RNA
MeSH: Biomarkers, Tumor*, Blood Platelets*, Brain Neoplasms*, Glioblastoma*, RNA*, Case-Control Studies, Female, Gene Expression Regulation, Neoplastic, Humans, Tumor Microenvironment (* major topic)
Topic: Glioma Diagnosis and Treatment (Oncology, Medicine), according to OpenAlex
Funding: Gdański Uniwersytet Medyczny (MNISW 07/IDUB/2019/94); Health~Holland Grants called AI-IMPACT; Toxicity-Atlas
Citations: not cited yet (Europe PMC); 46 references in the paper

Abstract

Background: Glioblastoma (GBM) is the most aggressive form of primary brain cancer. Blood platelets have emerged as biomarkers where their RNA expression pattern indicates a tumor's presence. Our study aimed to characterize these patterns to identify biological processes associated with GBM and to evaluate their potential as a non‐invasive biomarker.

Methods: We analyzed platelet RNA‐seq expression profiles from 84 GBM patients and 319 healthy individuals using two complementary analytical approaches of Gene Set Enrichment Analysis and Threshold‐based Differential Expression Analysis with Elastic Net regularization. Together, these approaches may offer complementary perspectives on biological pathways associated with GBM and the descriptive functional annotation of a predictive platelet RNA gene signature, providing insight into platelet‐tumor interactions.

Results: Our GSEA broad analysis highlighted platelet‐related biological processes, including coagulation, wound healing, immune regulation, and cytoskeletal organization. The Elastic Net framework additionally suggested pathways related to immune activation, cell signaling, and protein folding. Overall, these findings may suggest that platelet‐derived RNA not only has potential as a non‐invasive biomarker but may also provide insight into the systemic biological processes associated with the GBM tumor microenvironment.

Conclusions: Our data suggests that platelet‐derived RNA may reflect biological processes associated with the GBM tumor microenvironment, where wound repair and immune‐related processes may play important roles. Our findings provide a resource for further research into the mechanisms that may underlie the pathophysiology and progression of glioblastoma.

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

Repository

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

KrzysztofPastuszak/Platelets-GBM

License: GPL-3.0
State: the link answers, verified on 2 November 2026
Evidence: files inventoried
Commit: d355d14795f9a6a7d41b49181cb76265e689adaa, 6 March 2026
Languages: R (1)
Size: 23 files, 1 script
Software Heritage: not archived
Found in: “Data Availability Statement”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: caret (1 file), clusterProfiler (1 file), DESeq2 (1 file), edgeR (1 file), ggplot2 (1 file), glmnet (1 file), tidyverse (1 file)
Availability: 2 checks, the latest on 2 November 2026: the link answers
  • 2 November 2026: the link answers
  • 3 October 2026: the link answers
3 files

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

Tracing map

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

What the map holds:

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

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

Data

Datasets cited

Data Availability Statement

Data and scripts can be found https://github.com/KrzysztofPastuszak/Platelets‐GBM (https://github.com/KrzysztofPastuszak/Platelets-GBM). All analyses were conducted using R version 4.1.2 or higher.

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

Versions

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

Version 1, 3 October 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 5 keywords, 10 MeSH terms, 3 funders, 46 references.

Cite

This paper

Giczewska, A., Pastuszak, K., van Hout, L., Post, E., Ramaker, J., Zwaan, K., Supernat, A., & Westerman, B. A. (2026). Deepening the Understanding of Platelet-Derived RNA as Biomarker for Glioblastoma Through GSEA, TDEA, and Elastic Net Regularization Analysis. Cancer reports (Hoboken, N.J.), 9(10), e70656. https://doi.org/10.1002/cnr2.70656

BibTeX

@article{giczewska2026deepening,
author = {Giczewska, Anna and Pastuszak, Krzysztof and van Hout, Leon and Post, Edward and Ramaker, Jip and Zwaan, Ken and Supernat, Anna and Westerman, Bart A},
title = {{Deepening the Understanding of Platelet-Derived RNA as Biomarker for Glioblastoma Through GSEA, TDEA, and Elastic Net Regularization Analysis}},
journal = {Cancer reports (Hoboken, N.J.)},
year = {2026},
month = oct,
volume = {9},
number = {10},
pages = {e70656},
publisher = {Wiley},
issn = {2573-8348},
doi = {10.1002/cnr2.70656},
url = {https://doi.org/10.1002/cnr2.70656},
pmid = {42816786},
pmcid = {PMC13627264}
}

RIS

TY - JOUR
AU - Giczewska, Anna
AU - Pastuszak, Krzysztof
AU - van Hout, Leon
AU - Post, Edward
AU - Ramaker, Jip
AU - Zwaan, Ken
AU - Supernat, Anna
AU - Westerman, Bart A
TI - Deepening the Understanding of Platelet-Derived RNA as Biomarker for Glioblastoma Through GSEA, TDEA, and Elastic Net Regularization Analysis
T2 - Cancer reports (Hoboken, N.J.)
J2 - Cancer Rep (Hoboken)
PY - 2026
DA - 2026/10/01
VL - 9
IS - 10
SP - e70656
SN - 2573-8348
PB - Wiley
DO - 10.1002/cnr2.70656
UR - https://doi.org/10.1002/cnr2.70656
LA - en
ER -

CSL-JSON

{
"id": "10.1002/cnr2.70656",
"type": "article-journal",
"title": "Deepening the Understanding of Platelet-Derived RNA as Biomarker for Glioblastoma Through GSEA, TDEA, and Elastic Net Regularization Analysis",
"container-title": "Cancer reports (Hoboken, N.J.)",
"author": [
{
"family": "Giczewska",
"given": "Anna"
},
{
"family": "Pastuszak",
"given": "Krzysztof"
},
{
"family": "van Hout",
"given": "Leon"
},
{
"family": "Post",
"given": "Edward"
},
{
"family": "Ramaker",
"given": "Jip"
},
{
"family": "Zwaan",
"given": "Ken"
},
{
"family": "Supernat",
"given": "Anna"
},
{
"family": "Westerman",
"given": "Bart A"
}
],
"container-title-short": "Cancer Rep (Hoboken)",
"volume": "9",
"issue": "10",
"page": "e70656",
"DOI": "10.1002/cnr2.70656",
"PMID": "42816786",
"PMCID": "PMC13627264",
"ISSN": "2573-8348",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/cnr2.70656",
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
1
]
]
}
}

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

Similar papers

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

[1] doi:10.1093/noajnl/vdad134 [code]
Longitudinal drug synergy assessment using convolutional neural network image-decoding of glioblastoma single-spheroid cultures.
Journal: Neuro-oncology advances
In common: caret, clinical / translational, other condition, 3 authors
[2] doi:10.1016/j.xcrm.2026.102682 [code]
TET CpG sequence-context-specific DNA demethylation shapes progression of IDH-mutant gliomas.
Journal: Cell reports. Medicine
In common: glmnet, edgeR, clusterProfiler, 3 other tools, other condition, 1 reference, author Bart A Westerman
[3] doi:10.1093/noajnl/vdad073
Screening of predicted synergistic multi-target therapies in glioblastoma identifies new treatment strategies.
Journal: Neuro-oncology advances
In common: clinical / translational, other condition, 2 authors
[4] doi:10.3390/genes16050473 [code]
Enhancing Prognostic Signatures in Glioblastoma with Feature Selection and Regularised Cox Regression.
Journal: Genes
In common: glmnet, caret, edgeR, 3 other tools, clinical / translational, other condition, 1 reference
[5] doi:10.1093/gigascience/giaf093 [code]
The Open Pediatric Cancer Project.
Journal: GigaScience
In common: glmnet, caret, edgeR, 3 other tools, other condition, 2 references
[6] doi:10.3390/ijms27136068 [code]
Loss of Neuropeptide Y Signaling Accompanies the Neural-to-Mesenchymal Transcriptional Transition in Glioblastoma: A Multi-Scale Transcriptomic Analysis.
Journal: International journal of molecular sciences
In common: glmnet, edgeR, clusterProfiler, 2 other tools, other condition, 3 references
[7] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: glmnet, caret, edgeR, 4 other tools
[8] doi:10.1038/s41467-025-58675-9 [code]
Multimodal fusion of radio-pathology and proteogenomics identify integrated glioma subtypes with prognostic and therapeutic opportunities.
Journal: Nature communications
In common: glmnet, caret, clusterProfiler, 2 other tools, clinical / translational, other condition, 2 references
[9] doi:10.1093/brain/awag014 [code]
Study of blood linear RNA nominates CD55 and DLD as early-stage biomarkers for Parkinson's sisease.
Journal: Brain : a journal of neurology
In common: glmnet, caret, clusterProfiler, 3 other tools, clinical / translational, 1 reference
[10] doi:10.1371/journal.pcbi.1012857 [code]
An integrative network approach for longitudinal stratification in Parkinson's disease.
Journal: PLoS computational biology
In common: glmnet, edgeR, clusterProfiler, 3 other tools, clinical / translational, 1 reference

Contribute

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

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

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1002/cnr2.70656.svg)](https://openscicode.org/badge/doi/10.1002/cnr2.70656)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1002/cnr2.70656.svg
   :target: https://openscicode.org/badge/doi/10.1002/cnr2.70656
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1002/cnr2.70656"><img src="https://openscicode.org/badge/doi/10.1002/cnr2.70656.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

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