OSCR

Plasma proteomic signatures of early retinal neurodegeneration in diabetes: A multi-cohort study.

A correction to this paper has been published: the notice, 42461828, from Europe PMC.

Code ↔ Paper

1 match 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 1 match
  1. [1] § Methods › Statistical analysis ↔ R code for Pro-DRN.R, lines 693–707 · score 0.52 · age sex, Aspelund, Dagliati, Hippisley, ISDR, Tarasewicz

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,144 lines · 44 KB · no license · 1 match

  1. #———————————————————————————————————————————————————————————————————————————————————————————————————————————####
  2. # 线性回归(RNFL—蛋白)------------------------------------------------------------------------------------------------
  3. ------------------------------------------------------------------------------------------------------------
  4. rm(list = ls())
  5. gc()
  6. library(haven)
  7. library(readr)
  8. library(tidyr)
  9. library(dplyr)
  10. library(readxl)
  11. library(openxlsx)
  12. library(survival)
  13. library(fdrtool)
  14. library(progress)
  15. library(openxlsx)
  16. data_GDES <- read_dta("XXX.dta")
  17. data_pro <- read.xlsx("XXX.xlsx")
  18. # 存储要拟合的蛋白变量名
  19. proteins <- setdiff(names(data_pro), "id")
  20. proname <- read_tsv("XXX.tsv")
  21. proname <- proname %>%
  22. select(-coding) %>% # 删除 coding 列
  23. separate(meaning, into = c("abbreviation", "name"), sep = ";") %>% # 分离 meaning 列
  24. mutate(pro = tolower(abbreviation)) # 增加 pro 列,将 abbreviation 转为小写
  25. panel <- read_excel("XXX.xlsx")
  26. colnames(panel) <- panel[1,]
  27. panel <- panel[-1,-c(5,6)]
  28. colnames(panel) <- c("uniprot","name","pro","panel")
  29. panel <- panel %>%
  30. mutate(
  31. pro = tolower(pro), # 改小写
  32. pro = gsub("-", "_", pro) # "-"改"_"
  33. )
  34. panel <- panel[,c("pro","panel")]
  35. variables <- c("y0_prnfl_aver")
  36. # 年龄、性别、dm_duration、HbA1c、SBP、吸烟 ####
  37. # 创建一个列表,用于存储每个变量的回归结果
  38. lm_results_list <- list()
  39. pb <- progress_bar$new(
  40. format = " Progress [:bar] :percent Elapsed: :elapsed ETA: :eta",
  41. total = length(variables) * length(proteins),
  42. clear = FALSE,
  43. width = 60
  44. )
  45. # 循环进行亚场的线性回归
  46. for (var in variables) {
  47. # 创建一个空的数据框来存储回归结果
  48. lm_result <- data.frame(variable = character(),
  49. coef = numeric(),
  50. tvalue = numeric(),
  51. std = numeric(),
  52. pvalue = numeric(),
  53. stringsAsFactors = FALSE)
  54. # 循环拟合每个模型并存储结果
  55. for (var_pro in proteins) {
  56. formula <- paste0(var,"~", var_pro, "+ age + as.factor(sex) + dmdura_year + hba1c + as.factor(smoking) + sbp")
  57. lm_model <- lm(formula, data = data_GDES)
  58. coef <- as.numeric(summary(lm_model)$coefficients[, 1][2])
  59. std <- as.numeric(summary(lm_model)$coefficients[, 2][2])
  60. tvalue <- as.numeric(summary(lm_model)$coefficients[, 3][2])
  61. pvalue <- as.numeric(summary(lm_model)$coefficients[, 4][2])
  62. lm_result <- rbind(lm_result, c(var_pro, coef, tvalue, std, pvalue))
  63. # ✅ 更新进度条
  64. pb$tick()
  65. }
  66. colnames(lm_result) <- c("pro", "coef", "tvalue", "std", "pvalue")
  67. lm_result$coef <- as.numeric(lm_result$coef)
  68. lm_result$std <- as.numeric(lm_result$std)
  69. lm_result$tvalue <- as.numeric(lm_result$tvalue)
  70. lm_result$pvalue <- as.numeric(lm_result$pvalue)
  71. lm_result$low_limit <- as.numeric(lm_result$coef) - 1.96 * as.numeric(lm_result$std)
  72. lm_result$up_limit <- as.numeric(lm_result$coef) + 1.96 * as.numeric(lm_result$std)
  73. lm_result <- lm_result[c("pro", "coef", "low_limit", "up_limit", "std", "tvalue", "pvalue")]
  74. # 整理结果数据框
  75. lm_result$pvalue_sig <- ifelse(lm_result$pvalue <= 0.05, "significant", "-")
  76. # BH校正
  77. lm_result$BH <- p.adjust(lm_result$pvalue, "BH")
  78. lm_result$BH_sig <- ifelse(lm_result$BH <= 0.05, "significant", "-")
  79. fdr <- fdrtool(lm_result$pvalue, statistic="pvalue", plot=FALSE) # 关闭自动绘图
  80. lm_result$qval <- fdr$qval
  81. lm_result$lfdr <- fdr$lfdr
  82. # Bonferroni 校正与显著性标注
  83. lm_result$bonf <- p.adjust(lm_result$pvalue, method = "bonferroni")
  84. lm_result$bonf_sig <- ifelse(lm_result$bonf <= 0.05, "significant", "-")
  85. lm_result$BY <- p.adjust(lm_result$pvalue, method = "BY")
  86. lm_result$BY_sig <- ifelse(lm_result$BY <= 0.05, "significant", "-")
  87. lm_result$holm <- p.adjust(lm_result$pvalue, method = "holm")
  88. lm_result$holm_sig <- ifelse(lm_result$holm <= 0.05, "significant", "-")
  89. lm_result$hochberg <- p.adjust(lm_result$pvalue, method = "hochberg")
  90. lm_result$hochberg_sig <- ifelse(lm_result$hochberg <= 0.05, "significant", "-")
  91. # 注释蛋白
  92. lm_result <- left_join(lm_result, proname, by = "pro")
  93. lm_result <- left_join(lm_result, panel, by = "pro")
  94. #将每一个lm_result保存到相应的lm_results_list中
  95. lm_results_list[[var]] <- lm_result
  96. }
  97. # 输出结果
  98. lm_results_list
  99. # 写入到Excel
  100. library(openxlsx)
  101. wb <- createWorkbook()
  102. for (var in variables) {
  103. # 创建工作表
  104. addWorksheet(wb, var)
  105. # 写入数据到工作表
  106. writeData(wb, var, lm_results_list[[var]])
  107. }
  108. # Save the Excel file
  109. saveWorkbook(wb, "XXX.xlsx", overwrite = TRUE)
  110. #———————————————————————————————————————————————————————————————————————————————————————————————————————————####
  111. # 线性回归(蛋白-prnfl下降速率)------------------------------------------------------------------------------------------------
  112. ------------------------------------------------------------------------------------------------------------
  113. rm(list = ls())
  114. gc()
  115. library(haven)
  116. library(readr)
  117. library(tidyr)
  118. library(dplyr)
  119. library(readxl)
  120. library(openxlsx)
  121. library(survival)
  122. library(fdrtool)
  123. library(progress)
  124. library(openxlsx)
  125. data_GDES <- read_dta("XXX.dta")
  126. # 存储要拟合的蛋白变量名
  127. proname <- read_tsv("XXX.tsv")
  128. proname <- proname %>%
  129. select(-coding) %>% # 删除 coding 列
  130. separate(meaning, into = c("abbreviation", "name"), sep = ";") %>% # 分离 meaning 列
  131. mutate(pro = tolower(abbreviation)) # 增加 pro 列,将 abbreviation 转为小写
  132. panel <- read_excel("E:/UKB数据/olink数据/3072蛋白列表.xlsx")
  133. colnames(panel) <- panel[1,]
  134. panel <- panel[-1,-c(5,6)]
  135. colnames(panel) <- c("uniprot","name","pro","panel")
  136. panel <- panel %>%
  137. mutate(
  138. pro = tolower(pro), # 改小写
  139. pro = gsub("-", "_", pro) # "-"改"_"
  140. )
  141. panel <- panel[,c("pro","panel")]
  142. data_pro <- read_excel("XXX.xlsx")
  143. proteins <- data_pro$pro
  144. variables <- c("decline_rate")
  145. # 年龄、性别、dm_duration、HbA1c、SBP、吸烟 ####
  146. # 创建一个列表,用于存储每个变量的回归结果
  147. lm_results_list <- list()
  148. pb <- progress_bar$new(
  149. format = " Progress [:bar] :percent Elapsed: :elapsed ETA: :eta",
  150. total = length(variables) * length(proteins),
  151. clear = FALSE,
  152. width = 60
  153. )
  154. # 循环进行亚场的线性回归
  155. for (var in variables) {
  156. # 创建一个空的数据框来存储回归结果
  157. lm_result <- data.frame(variable = character(),
  158. coef = numeric(),
  159. tvalue = numeric(),
  160. std = numeric(),
  161. pvalue = numeric(),
  162. stringsAsFactors = FALSE)
  163. # 循环拟合每个模型并存储结果
  164. for (var_pro in proteins) {
  165. formula <- paste0(var,"~", var_pro, "+ age + as.factor(sex) + dmdura_year + hba1c + as.factor(smoking) + sbp")
  166. lm_model <- lm(formula, data = data_GDES)
  167. coef <- as.numeric(summary(lm_model)$coefficients[, 1][2])
  168. std <- as.numeric(summary(lm_model)$coefficients[, 2][2])
  169. tvalue <- as.numeric(summary(lm_model)$coefficients[, 3][2])
  170. pvalue <- as.numeric(summary(lm_model)$coefficients[, 4][2])
  171. lm_result <- rbind(lm_result, c(var_pro, coef, tvalue, std, pvalue))
  172. # ✅ 更新进度条
  173. pb$tick()
  174. }
  175. colnames(lm_result) <- c("pro", "coef", "tvalue", "std", "pvalue")
  176. lm_result$coef <- as.numeric(lm_result$coef)
  177. lm_result$std <- as.numeric(lm_result$std)
  178. lm_result$tvalue <- as.numeric(lm_result$tvalue)
  179. lm_result$pvalue <- as.numeric(lm_result$pvalue)
  180. lm_result$low_limit <- as.numeric(lm_result$coef) - 1.96 * as.numeric(lm_result$std)
  181. lm_result$up_limit <- as.numeric(lm_result$coef) + 1.96 * as.numeric(lm_result$std)
  182. lm_result <- lm_result[c("pro", "coef", "low_limit", "up_limit", "std", "tvalue", "pvalue")]
  183. # 整理结果数据框
  184. lm_result$pvalue_sig <- ifelse(lm_result$pvalue <= 0.05, "significant", "-")
  185. # BH校正
  186. lm_result$BH <- p.adjust(lm_result$pvalue, "BH")
  187. lm_result$BH_sig <- ifelse(lm_result$BH <= 0.05, "significant", "-")
  188. fdr <- fdrtool(lm_result$pvalue, statistic="pvalue", plot=FALSE) # 关闭自动绘图
  189. lm_result$qval <- fdr$qval
  190. lm_result$lfdr <- fdr$lfdr
  191. # 注释蛋白
  192. lm_result <- left_join(lm_result, proname, by = "pro")
  193. lm_result <- left_join(lm_result, panel, by = "pro")
  194. #将每一个lm_result保存到相应的lm_results_list中
  195. lm_results_list[[var]] <- lm_result
  196. }
  197. # 输出结果
  198. lm_results_list
  199. # 写入到Excel
  200. library(openxlsx)
  201. wb <- createWorkbook()
  202. for (var in variables) {
  203. # 创建工作表
  204. addWorksheet(wb, var)
  205. # 写入数据到工作表
  206. writeData(wb, var, lm_results_list[[var]])
  207. }
  208. # Save the Excel file
  209. saveWorkbook(wb, "XXXX.xlsx", overwrite = TRUE)
  210. # 通路分析 #####
  211. rm(list = ls())
  212. gc()
  213. ####加载包
  214. library(zlibbioc)
  215. library(GO.db)
  216. library(org.Hs.eg.db)
  217. library(clusterProfiler)
  218. library(tidyverse)
  219. library(ggplot2)
  220. library(forcats)
  221. library(readxl)
  222. library(writexl)
  223. ####加载数据
  224. data <- read_excel("XXXX.xlsx")
  225. ##基因ID转换(genesymbol → ENTREZID)
  226. IDs <- bitr(data$abbreviation,
  227. fromType = 'SYMBOL',
  228. toType = c('ENTREZID'),
  229. OrgDb = 'org.Hs.eg.db') #不同的物种对应不同的数据库,详见https://bioconductor.org/packages/release/BiocViews.html#___OrgDb
  230. ##基于ENTREZID进行GO富集分析
  231. enrichGO_result <- enrichGO(IDs$ENTREZID,
  232. OrgDb="org.Hs.eg.db",
  233. qvalueCutoff=0.05,
  234. pvalueCutoff=0.05,
  235. ont="all")
  236. enrichGO_result <- setReadable(enrichGO_result, OrgDb="org.Hs.eg.db", keyType = 'ENTREZID')
  237. enrichGO_result_data <- data.frame(enrichGO_result)
  238. writexl::write_xlsx(enrichGO_result_data,
  239. path = "XXXX.xlsx")
  240. ##基于ENTREZID进行KEGG富集分析
  241. enrichKEGG_result <- enrichKEGG(gene=IDs$ENTREZID,
  242. organism ="hsa",#物种
  243. keyType ="kegg",
  244. pvalueCutoff = 0.05,
  245. qvalueCutoff = 1
  246. )
  247. enrichKEGG_result <- setReadable(enrichKEGG_result,
  248. OrgDb="org.Hs.eg.db",
  249. keyType ="ENTREZID"
  250. )
  251. enrichKEGG_result_data <- data.frame(enrichKEGG_result)
  252. writexl::write_xlsx(enrichKEGG_result_data,
  253. path = "XXXX.xlsx")
  254. #———————————————————————————————————————————————————————————————————————————————————————————————————————————####
  255. # 4. 预测分析 #####
  256. rm(list = ls())
  257. gc()
  258. library(haven)
  259. library(readr)
  260. library(tidyr)
  261. library(dplyr)
  262. library(readxl)
  263. library(openxlsx)
  264. library(survival)
  265. library(fdrtool)
  266. library(progress)
  267. library(openxlsx)
  268. #整理数据
  269. data_GDES <- read_dta("XXXX.dta")
  270. data_train <- read_dta("XXXX.dta")
  271. data_test <- read_dta("XXXX.dta")
  272. # 计算 Q1 阈值
  273. q1_cut <- quantile(data_GDES$decline_rate, 0.25, na.rm = TRUE)
  274. # 根据 Q1 定义 drn
  275. data_GDES <- data_GDES %>%
  276. mutate(
  277. drn = ifelse(decline_rate <= q1_cut, 1, 0)
  278. )
  279. # 蛋白组模型(xgboost) ####
  280. outcome_list <- c("drn")
  281. results_pro_all <- data.frame()
  282. original_seed_results <- list()
  283. seed_list <- createWorkbook()
  284. # 用全量人群 id 作为对齐基准
  285. stopifnot("id" %in% names(data_GDES))
  286. data_score <- data.frame(id = data_GDES$id)
  287. set.seed(2025)
  288. params <- list(
  289. booster = "gbtree",
  290. objective = "binary:logistic",
  291. eval_metric = "auc",
  292. eta = 0.05,
  293. max_depth = 7,
  294. min_child_weight = 1,
  295. gamma = 2,
  296. subsample = 0.9,
  297. colsample_bytree = 1,
  298. colsample_bylevel = 0.5,
  299. lambda = 1,
  300. alpha = 0.5,
  301. max_delta_step = 0,
  302. tree_method = "exact",
  303. scale_pos_weight = 1,
  304. seed = 2025
  305. )
  306. for (outcome in outcome_list) {
  307. # --- 清理NA(保持最小必要清理) ---
  308. stopifnot(all(c("id", outcome) %in% colnames(data_train)))
  309. stopifnot(all(c("id", outcome) %in% colnames(data_test)))
  310. data_train <- data_train[!is.na(data_train[[outcome]]), ]
  311. data_test <- data_test[ !is.na(data_test[[outcome]]), ]
  312. # --- 特征对齐:仅保留在 train/test 都存在的蛋白列 ---
  313. proteins <- intersect(proteins, intersect(colnames(data_train), colnames(data_test)))
  314. stopifnot(length(proteins) > 0)
  315. # --- 标签与特征矩阵 ---
  316. train_y <- as.numeric(data_train[[outcome]])
  317. test_y <- as.numeric(data_test[[outcome]])
  318. train_x <- as.matrix(data_train[, proteins, drop = FALSE])
  319. test_x <- as.matrix(data_test[, proteins, drop = FALSE])
  320. dtrain <- xgb.DMatrix(data = train_x, label = train_y)
  321. dtest <- xgb.DMatrix(data = test_x, label = test_y)
  322. # --- 用训练集做交叉验证,寻找最佳轮次 ---
  323. val.cv <- xgb.cv(
  324. params = params,
  325. data = dtrain,
  326. nfold = 10,
  327. nrounds = 1000,
  328. early_stopping_rounds = 100,
  329. metrics = "auc",
  330. maximize = TRUE,
  331. verbose = 0
  332. )
  333. best_ntree <- val.cv$best_iteration
  334. # --- 用最佳轮次在训练集上拟合最终模型 ---
  335. model <- xgboost(
  336. data = dtrain,
  337. params = params,
  338. nrounds = best_ntree,
  339. verbose = 0
  340. )
  341. # --- 训练/测试集预测与ROC ---
  342. pred_train <- predict(model, dtrain)
  343. pred_test <- predict(model, dtest)
  344. roc_pro_all_train <- pROC::roc(as.factor(train_y) ~ pred_train, ci = TRUE, quiet = TRUE)
  345. roc_pro_all <- pROC::roc(as.factor(test_y) ~ pred_test, ci = TRUE, quiet = TRUE)
  346. # --- cutoff & 混淆矩阵(基于测试集) ---
  347. cutoff <- cutoff::roc(pred_test, as.factor(test_y))$cutoff
  348. prediction <- ifelse(pred_test > cutoff, 1, 0)
  349. cm <- caret::confusionMatrix(
  350. data = factor(prediction, levels = c(0,1)),
  351. reference = factor(test_y, levels = c(0,1)),
  352. positive = "1", mode = "everything"
  353. )
  354. TN <- cm$table[1]; FP <- cm$table[2]; FN <- cm$table[3]; TP <- cm$table[4]
  355. Accuracy <- cm$overall[["Accuracy"]]
  356. Sensitivity <- cm$byClass[["Sensitivity"]]
  357. Specificity <- cm$byClass[["Specificity"]]
  358. PPV <- cm$byClass[["Precision"]]
  359. NPV <- cm$byClass[["Neg Pred Value"]]
  360. Recall <- cm$byClass[["Recall"]]
  361. F1 <- cm$byClass[["F1"]]
  362. DR <- TP/(FN+TP)
  363. FPR <- FP/(TN+FP)
  364. LR <- ifelse(FPR == 0, Inf, DR/FPR)
  365. # --- 结果表 ---
  366. test_results <- data.frame(
  367. outcome = outcome,
  368. seed_pro_all = NA_real_, # 占位
  369. train_roc_pro_all = as.numeric(roc_pro_all_train$auc),
  370. train_low_pro_all = roc_pro_all_train$ci[1],
  371. train_up_pro_all = roc_pro_all_train$ci[3],
  372. test_roc_pro_all = as.numeric(roc_pro_all$auc),
  373. test_low_pro_all = roc_pro_all$ci[1],
  374. test_up_pro_all = roc_pro_all$ci[3],
  375. cutoff = cutoff,
  376. Accuracy = Accuracy, Sensitivity = Sensitivity, Specificity = Specificity,
  377. PPV = PPV, NPV = NPV, Recall = Recall, F1 = F1,
  378. DR = DR, FPR = FPR, LR = LR,
  379. stringsAsFactors = FALSE
  380. )
  381. original_seed_results[[outcome]] <- test_results
  382. addWorksheet(seed_list, sheetName = outcome)
  383. writeData(seed_list, outcome, original_seed_results[[outcome]])
  384. # --- 保存逐个体 score 到全量框架(按 id 对齐;score 列名改为 score_drn) ---
  385. score_col <- "score_drn" # ← 你要求的命名
  386. dset_col <- "dataset_drn" # 数据集标记列,便于区分
  387. data_score_temp <- data_GDES %>%
  388. dplyr::select(id) %>%
  389. dplyr::left_join(data.frame(id = data_test$id, pred = pred_test), by = "id") %>%
  390. dplyr::left_join(data.frame(id = data_train$id, pred_train = pred_train), by = "id") %>%
  391. dplyr::mutate(dataset = dplyr::case_when(!is.na(pred) ~ "test",
  392. !is.na(pred_train) ~ "train",
  393. TRUE ~ NA_character_),
  394. pred_final = ifelse(!is.na(pred), pred, pred_train)) %>%
  395. dplyr::transmute(id, !!score_col := pred_final, !!dset_col := dataset)
  396. data_score <- dplyr::left_join(data_score, data_score_temp, by = "id")
  397. }
  398. saveWorkbook(seed_list, "XXXX.xlsx", overwrite = TRUE)
  399. write.csv(data_score, "XXXXX.csv", row.names = FALSE)
  400. # 保存环境
  401. save.image("XXXXX.RData")
  402. # ——4.2.2 变量重要性 ------------------------------------------------------------------------------------------------
  403. rm(list = ls())
  404. load("XXXXX.RData")
  405. library(openxlsx)
  406. library(DT)
  407. variable_importance <- createWorkbook()
  408. # ———4.2.2.1 xgboost包自带VIP ####
  409. importance <- xgb.importance(feature_names = colnames(train), model = xgb_model)
  410. #1 Gain:使用某个特征进行拆分时,获得的平均训练损失减少量
  411. #2 Cover:首先得到某个特征被用于在所有树中拆分数据的次数,然后要利用经过这些拆分点的训练数据数量赋予权重
  412. #3 frequency:某个特征被用于在所有树中拆分数据的次数
  413. addWorksheet(variable_importance, sheetName = "importance")
  414. writeData(variable_importance, "importance", as.data.frame(importance))
  415. # 图示
  416. xgb.plot.importance(importance, measure = "Gain")
  417. pdf(file="XXXXX.pdf",onefile = FALSE,
  418. width = 8,
  419. height =18)
  420. xgb.plot.importance(importance, measure = "Gain")
  421. dev.off()
  422. # ggplot可视化排名前10变量
  423. library(ggplot2)
  424. xgb.ggplot.importance(importance_matrix = importance, top_n = 10)
  425. # ———4.2.2.2 SHAP ####
  426. library(SHAPforxgboost)
  427. library(dplyr)
  428. # SHAP data (individual-unit)
  429. shap_data <- shap.prep(xgb_model, X_train = train_x)
  430. addWorksheet(variable_importance, sheetName = "SHAP data")
  431. writeData(variable_importance, "SHAP data", as.data.frame(shap_data))
  432. # SHAP summary
  433. shap_summary <- shap_data %>%
  434. group_by(variable) %>%
  435. summarise(AverageMeanValue = mean(mean_value, na.rm = TRUE))
  436. addWorksheet(variable_importance, sheetName = "SHAP summary")
  437. writeData(variable_importance, "SHAP summary", as.data.frame(shap_summary))
  438. # *4.4.3 总人群预测 —— 汇总预测分数 ####
  439. data_GDES <- read_dta("XXXXX.dta")
  440. data_score_temp <- read_csv("XXXXX.csv")
  441. data_GDES <- data_GDES %>%
  442. left_join(
  443. data_score_temp %>%
  444. select(id, score_drn_XGB, dataset_drn),
  445. by = "id"
  446. )
  447. data_train <- data_GDES %>% filter(dataset_drn == "train")
  448. data_test <- data_GDES %>% filter(dataset_drn == "test")
  449. write_dta(data_train, "XXXXX.dta")
  450. write_dta(data_test, "XXXXX.dta")
  451. write_dta(data_GDES, "XXXXX.dta")
  452. # 单独predictor比较 ####
  453. rm(list = ls())
  454. gc()
  455. library(haven)
  456. library(readr)
  457. library(tidyr)
  458. library(dplyr)
  459. library(readxl)
  460. library(openxlsx)
  461. library(survival)
  462. library(fdrtool)
  463. library(progress)
  464. library(pROC)
  465. # 整理数据
  466. data_train <- read_dta("XXXXX.dta")
  467. data_test <- read_dta("XXXXX.dta")
  468. data_GDES <- read_dta("XXXXX.dta")
  469. outcomes <- c("drn")
  470. auc_results <- data.frame(stringsAsFactors = FALSE)
  471. for (outcome in outcomes) {
  472. # -------- 基准:蛋白(XGB 分数) --------
  473. model_pro <- glm(as.formula(paste(outcome, "~ score_drn_XGB")),
  474. data = data_test, family = binomial)
  475. data_test[[paste0(outcome, "_riskscore_pro")]] <-
  476. predict(model_pro, newdata = data_test, type = "response")
  477. roc_result_pro <- pROC::roc(
  478. response = data_test[[outcome]],
  479. predictor = data_test[[paste0(outcome, "_riskscore_pro")]],
  480. ci = TRUE, ci.method = "delong", na.rm = TRUE, quiet = TRUE
  481. )
  482. auc_pro <- as.numeric(roc_result_pro$auc)
  483. ci_lower_pro <- roc_result_pro$ci[1]
  484. ci_upper_pro <- roc_result_pro$ci[3]
  485. fit_and_roc <- function(rhs) {
  486. m <- glm(as.formula(paste(outcome, "~", rhs)), data = data_test, family = binomial)
  487. sc <- predict(m, newdata = data_test, type = "response")
  488. r <- pROC::roc(response = data_test[[outcome]], predictor = sc,
  489. ci = TRUE, ci.method = "delong", na.rm = TRUE, quiet = TRUE)
  490. list(model = m, roc = r, auc = as.numeric(r$auc), ci = r$ci)
  491. }
  492. res_age <- fit_and_roc("age")
  493. res_sex <- fit_and_roc("as.factor(sex)")
  494. res_income <- fit_and_roc("as.factor(income)")
  495. res_smoking <- fit_and_roc("as.factor(smoking)")
  496. res_drinking <- fit_and_roc("as.factor(drinking)")
  497. res_education <- fit_and_roc("as.factor(education)")
  498. res_bmi <- fit_and_roc("as.factor(bmi_c)")
  499. res_statin <- fit_and_roc("as.factor(med_statin)")
  500. res_hbp <- fit_and_roc("as.factor(med_hbp)")
  501. P_age <- pROC::roc.test(roc_result_pro, res_age$roc, method = "delong")$p.value
  502. P_sex <- pROC::roc.test(roc_result_pro, res_sex$roc, method = "delong")$p.value
  503. P_income <- pROC::roc.test(roc_result_pro, res_income$roc, method = "delong")$p.value
  504. P_smoking <- pROC::roc.test(roc_result_pro, res_smoking$roc, method = "delong")$p.value
  505. P_drinking <- pROC::roc.test(roc_result_pro, res_drinking$roc, method = "delong")$p.value
  506. P_education <- pROC::roc.test(roc_result_pro, res_education$roc, method = "delong")$p.value
  507. P_bmi <- pROC::roc.test(roc_result_pro, res_bmi$roc, method = "delong")$p.value
  508. P_statin <- pROC::roc.test(roc_result_pro, res_statin$roc, method = "delong")$p.value
  509. P_hbp <- pROC::roc.test(roc_result_pro, res_hbp$roc, method = "delong")$p.value
  510. lab <- function(pv, auc_other) ifelse(pv >= 0.05, "Comparable",
  511. ifelse(auc_pro > auc_other, "Superior", "-"))
  512. auc_age <- res_age$auc; ci_lower_age <- res_age$ci[1]; ci_upper_age <- res_age$ci[3]
  513. auc_sex <- res_sex$auc; ci_lower_sex <- res_sex$ci[1]; ci_upper_sex <- res_sex$ci[3]
  514. auc_income <- res_income$auc; ci_lower_income <- res_income$ci[1]; ci_upper_income <- res_income$ci[3]
  515. auc_smoking <- res_smoking$auc; ci_lower_smoking <- res_smoking$ci[1]; ci_upper_smoking <- res_smoking$ci[3]
  516. auc_drinking <- res_drinking$auc; ci_lower_drinking <- res_drinking$ci[1]; ci_upper_drinking <- res_drinking$ci[3]
  517. auc_education <- res_education$auc; ci_lower_education <- res_education$ci[1]; ci_upper_education <- res_education$ci[3]
  518. auc_bmi <- res_bmi$auc; ci_lower_bmi <- res_bmi$ci[1]; ci_upper_bmi <- res_bmi$ci[3]
  519. auc_med_statin <- res_statin$auc; ci_lower_med_statin <- res_statin$ci[1]; ci_upper_med_statin <- res_statin$ci[3]
  520. auc_med_hbp <- res_hbp$auc; ci_lower_med_hbp <- res_hbp$ci[1]; ci_upper_med_hbp <- res_hbp$ci[3]
  521. compare_age <- lab(P_age, auc_age)
  522. compare_sex <- lab(P_sex, auc_sex)
  523. compare_income <- lab(P_income, auc_income)
  524. compare_smoking <- lab(P_smoking, auc_smoking)
  525. compare_drinking <- lab(P_drinking, auc_drinking)
  526. compare_education <- lab(P_education, auc_education)
  527. compare_bmi <- lab(P_bmi, auc_bmi)
  528. compare_med_statin<- lab(P_statin, auc_med_statin)
  529. compare_med_hbp <- lab(P_hbp, auc_med_hbp)
  530. auc_results <- rbind(auc_results, c(
  531. outcome,
  532. auc_pro, ci_lower_pro, ci_upper_pro,
  533. auc_age, ci_lower_age, ci_upper_age, P_age, compare_age,
  534. auc_sex, ci_lower_sex, ci_upper_sex, P_sex, compare_sex,
  535. auc_income, ci_lower_income, ci_upper_income, P_income, compare_income,
  536. auc_smoking, ci_lower_smoking, ci_upper_smoking, P_smoking, compare_smoking,
  537. auc_drinking, ci_lower_drinking, ci_upper_drinking, P_drinking, compare_drinking,
  538. auc_education, ci_lower_education, ci_upper_education, P_education, compare_education,
  539. auc_bmi, ci_lower_bmi, ci_upper_bmi, P_bmi, compare_bmi,
  540. auc_med_statin, ci_lower_med_statin, ci_upper_med_statin, P_statin, compare_med_statin,
  541. auc_med_hbp, ci_lower_med_hbp, ci_upper_med_hbp, P_hbp, compare_med_hbp
  542. ))
  543. colnames(auc_results) <- c(
  544. "outcome", "PRO", "pro_lower", "pro_upper",
  545. "AGE", "age_lower", "age_upper", "Pvalue_AGE", "PRO_vs._AGE",
  546. "SEX", "sex_lower", "sex_upper", "Pvalue_SEX", "PRO_vs._SEX",
  547. "INCOME", "income_lower", "income_upper", "Pvalue_INCOME", "PRO_vs._INCOME",
  548. "SMOKING", "smoking_lower", "smoking_upper", "Pvalue_SMOKING", "PRO_vs._SMOKING",
  549. "DRINKING", "drinking_lower", "drinking_upper", "Pvalue_DRINKING", "PRO_vs._DRINKING",
  550. "EDUCATION", "education_lower", "education_upper", "Pvalue_EDUCATION", "PRO_vs._EDUCATION",
  551. "BMI", "bmi_lower", "bmi_upper", "Pvalue_BMI", "PRO_vs._BMI",
  552. "ANTI_LIPID", "anti_lipid_lower", "anti_lipid_upper", "Pvalue_ANTI_LIPID", "PRO_vs._ANTI_LIPID",
  553. "ANTI_BP", "anti_bp_lower", "anti_bp_upper", "Pvalue_ANTI_BP", "PRO_vs._ANTI_BP"
  554. )
  555. }
  556. # 转数值
  557. cmp_cols <- grep("^PRO_vs\\._", colnames(auc_results), value = TRUE) # 比较标签列
  558. num_cols <- setdiff(colnames(auc_results), c("outcome", cmp_cols)) # 其余才转数值
  559. auc_results[cmp_cols] <- lapply(auc_results[cmp_cols], as.character) # 显式设为字符
  560. auc_results[num_cols] <- lapply(auc_results[num_cols], function(x) suppressWarnings(as.numeric(x)))
  561. write.xlsx(auc_results, "XXXXX.xlsx")
  562. # 模型和结局函数 ####
  563. create_models <- function() {
  564. models <- list("Age_Sex" = "age + as.factor(sex)",
  565. "Aspelund_model" = "age + as.factor(sex) + as.factor(sbp_c) + dmdura_year + hba1c",
  566. "Hippisley_model" = "age + as.factor(sex) + as.factor(bmi_c) + as.factor(sbp_c) + TC_divide_HDL + hba1c",
  567. "Dagliati_model" = "age + as.factor(sex) + dmdura_year + as.factor(bmi_c) + hba1c + as.factor(prev_hbp) + as.factor(smoking)",
  568. "ISDR_model" = "age + as.factor(sex) + as.factor(sbp_c) + dmdura_year + hba1c + chol",
  569. "JJR_model" = "age + as.factor(sbp_c) + dmdura_year + hba1c + Egfr",
  570. "Tarasewicz_model" = "age + as.factor(sbp_c) + as.factor(insulin) + hba1c + as.factor(bmi_c) + ldlc + crea + mau",
  571. "All_model" = "age + as.factor(sex) + as.factor(sbp_c) + dmdura_year + hba1c + as.factor(bmi_c) + TC_divide_HDL + chol + Egfr + as.factor(insulin) + ldlc + crea + mau + as.factor(prev_hbp) + as.factor(smoking)"
  572. )
  573. return(list(models = models))
  574. }
  575. models <- create_models()$models
  576. # 人群预测 ####
  577. library(pROC)
  578. library(openxlsx)
  579. library(dplyr)
  580. score_vars <- c(
  581. "score_drn_XGB","score_drn_CART","score_drn_KNN","score_drn_LGBM",
  582. "score_drn_LogReg","score_drn_NN","score_drn_RF","score_drn_SVM"
  583. )
  584. outcomes <- c("drn")
  585. auc_results_list <- list()
  586. wb <- createWorkbook()
  587. for (model_name in names(models)) {
  588. auc_results <- data.frame()
  589. # —— 把 outcome 循环改成 score 循环
  590. for (score_var in score_vars) {
  591. outcome <- outcomes[1]
  592. covariate <- models[[model_name]]
  593. # 传统模型
  594. formula1 <- as.formula(paste(outcome, "~", covariate, sep = ""))
  595. model1 <- glm(formula1, data = data_test, family = binomial)
  596. data_test[[paste(outcome, "_", model_name, "_1", sep = "")]] <- predict(model1, newdata = data_test)
  597. roc_result1 <- roc(
  598. as.formula(paste(outcome, "~", paste(outcome, "_", model_name, "_1", sep = ""))),
  599. data = data_test, print.thres = TRUE, print.auc = TRUE, ci = TRUE, plot = FALSE, smooth = FALSE
  600. )
  601. auc_value1 <- roc_result1$auc
  602. ci_lower1 <- roc_result1$ci[1]
  603. ci_upper1 <- roc_result1$ci[3]
  604. # 加入蛋白(这里用当前 score_var)
  605. formula2 <- as.formula(paste(outcome, "~", covariate, "+", score_var))
  606. model2 <- glm(formula2, data = data_test, family = binomial)
  607. data_test[[paste(outcome, "_", model_name, "_2", sep = "")]] <- predict(model2, newdata = data_test)
  608. roc_result2 <- roc(
  609. as.formula(paste(outcome, "~", paste(outcome, "_", model_name, "_2", sep = ""))),
  610. data = data_test, print.thres = TRUE, print.auc = TRUE, ci = TRUE, plot = FALSE, smooth = FALSE
  611. )
  612. auc_value2 <- roc_result2$auc
  613. ci_lower2 <- roc_result2$ci[1]
  614. ci_upper2 <- roc_result2$ci[3]
  615. test <- roc.test(roc_result1, roc_result2) # DeLong's test
  616. p <- test[["p.value"]]
  617. auc_diff <- auc_value2 - auc_value1
  618. auc_improve_proportion <- auc_diff / auc_value1
  619. sig <- ifelse(p >= 0.05, "",
  620. ifelse(p < 0.001, "***",
  621. ifelse(p >= 0.001 & p < 0.01, "**", "*")))
  622. # 单独蛋白(当前 score_var)
  623. roc_result3 <- roc(
  624. as.formula(paste(outcome, "~", score_var)),
  625. data = data_test, print.thres = TRUE, print.auc = TRUE, ci = TRUE, plot = FALSE, smooth = FALSE
  626. )
  627. auc_value3 <- roc_result3$auc
  628. ci_lower3 <- roc_result3$ci[1]
  629. ci_upper3 <- roc_result3$ci[3]
  630. test_met <- roc.test(roc_result1, roc_result3) # DeLong's test
  631. p_met <- test_met[["p.value"]]
  632. compare_met <- ifelse(p_met >= 0.05, "Comparable", ifelse(auc_value3 > auc_value1, "Superior", "-"))
  633. # 结果入表
  634. auc_results <- rbind(
  635. auc_results,
  636. c(score_var, outcome,
  637. auc_value1, ci_lower1, ci_upper1,
  638. auc_value3, ci_lower3, ci_upper3, p_met, compare_met,
  639. auc_value2, ci_lower2, ci_upper2, auc_diff, auc_improve_proportion, p, sig)
  640. )
  641. colnames(auc_results) <- c("score", "outcome",
  642. "CONV", "conv_lower", "conv_upper",
  643. "PRO", "pro_lower", "pro_upper", "Pvalue_PRO_vs_CONV", "PRO_vs._CONV",
  644. "COMBINED", "combined_lower", "combined_upper", "Diff", "Diff_proportion",
  645. "Pvalue_COMBINED_vs_CONV", "Significance")
  646. }
  647. # 显著标记放在最后一列(保留 score 列)
  648. auc_results <- auc_results %>%
  649. select("score", which(names(auc_results) == "outcome"):which(names(auc_results) == "Pvalue_COMBINED_vs_CONV"), "Significance")
  650. # as.numeric
  651. auc_results$CONV <- as.numeric(auc_results$CONV)
  652. auc_results$conv_lower <- as.numeric(auc_results$conv_lower)
  653. auc_results$conv_upper <- as.numeric(auc_results$conv_upper)
  654. auc_results$PRO <- as.numeric(auc_results$PRO)
  655. auc_results$pro_lower <- as.numeric(auc_results$pro_lower)
  656. auc_results$pro_upper <- as.numeric(auc_results$pro_upper)
  657. auc_results$Pvalue_PRO_vs_CONV <- as.numeric(auc_results$Pvalue_PRO_vs_CONV)
  658. auc_results$COMBINED <- as.numeric(auc_results$COMBINED)
  659. auc_results$combined_lower <- as.numeric(auc_results$combined_lower)
  660. auc_results$combined_upper <- as.numeric(auc_results$combined_upper)
  661. auc_results$Diff <- as.numeric(auc_results$Diff)
  662. auc_results$Diff_proportion <- as.numeric(auc_results$Diff_proportion)
  663. auc_results$Pvalue_COMBINED_vs_CONV <- as.numeric(auc_results$Pvalue_COMBINED_vs_CONV)
  664. # 汇总与写表
  665. auc_results_list[[model_name]] <- auc_results
  666. addWorksheet(wb, sheetName = model_name)
  667. writeData(wb, model_name, auc_results_list[[model_name]])
  668. }
  669. auc_results_list
  670. # ===== 按 score 汇总成“图2样子”的表(每个 score 一个 sheet)=====
  671. # 先把所有模型的结果合并,并带上 model 列
  672. all_res <- dplyr::bind_rows(lapply(names(auc_results_list), function(m) {
  673. df <- auc_results_list[[m]]
  674. df$model <- m
  675. df
  676. }))
  677. # 列顺序
  678. col_order <- c("model","score","outcome",
  679. "CONV","conv_lower","conv_upper",
  680. "PRO","pro_lower","pro_upper","Pvalue_PRO_vs_CONV","PRO_vs._CONV",
  681. "COMBINED","combined_lower","combined_upper",
  682. "Diff","Diff_proportion","Pvalue_COMBINED_vs_CONV","Significance")
  683. # 逐个 score 输出一个 sheet
  684. for (sv in score_vars) {
  685. tag <- sub("^score_drn_", "", sv) # e.g. "XGB"
  686. sheet_name <- substr(paste0("Summary_", tag), 1, 31) # Excel 名称≤31字符
  687. df_out <- all_res %>%
  688. dplyr::filter(score == sv) %>%
  689. # 模型顺序排
  690. dplyr::mutate(model = factor(model, levels = names(models))) %>%
  691. dplyr::arrange(model)
  692. # 只保留需要的列
  693. keep_cols <- intersect(col_order, names(df_out))
  694. df_out <- df_out[, keep_cols, drop = FALSE]
  695. # 写入新工作表
  696. if (sheet_name %in% openxlsx::sheets(wb)) {
  697. openxlsx::removeWorksheet(wb, sheet_name)
  698. }
  699. openxlsx::addWorksheet(wb, sheetName = sheet_name)
  700. openxlsx::writeData(wb, sheet_name, df_out)
  701. }
  702. # 保存 Excel
  703. saveWorkbook(wb, "XXXXX.xlsx", overwrite = TRUE)
  704. # 保存预测label
  705. data_label <- data_test[, 2845:ncol(data_test)]
  706. data_id <- data_test[, 1]
  707. data_predict_label <- cbind(data_id, data_label)
  708. write.xlsx(data_predict_label, "XXXXX.xlsx")
  709. # =========================== RF训练 ===========================
  710. rm(list = ls()); gc()
  711. library(haven)
  712. library(dplyr)
  713. library(caret)
  714. library(ranger)
  715. library(pROC)
  716. library(cutoff)
  717. library(openxlsx)
  718. library(readxl)
  719. library(readr)
  720. library(tidyr)
  721. data_train <- read_dta("XXXXX.dta")
  722. data_test <- read_dta("XXXXX.dta")
  723. data_GDES <- read_dta("XXXXX.dta")
  724. data_pro2 <- read_excel("XXXXX.xlsx")
  725. proteins <- data_pro2$pro
  726. # 传统模型集合
  727. create_models <- function() {
  728. models <- list(
  729. "Age_Sex" = "age + as.factor(sex)",
  730. "Aspelund_model"= "age + as.factor(sex) + as.factor(sbp_c) + dmdura_year + hba1c",
  731. "Hippisley_model"= "age + as.factor(sex) + as.factor(bmi_c) + as.factor(sbp_c) + TC_divide_HDL + hba1c",
  732. "Dagliati_model"= "age + as.factor(sex) + dmdura_year + as.factor(bmi_c) + hba1c + as.factor(prev_hbp) + as.factor(smoking)",
  733. "ISDR_model" = "age + as.factor(sex) + as.factor(sbp_c) + dmdura_year + hba1c + chol",
  734. "JJR_model" = "age + sbp + dmdura_year + hba1c + Egfr",
  735. "Tarasewicz_model" = "age + as.factor(sbp_c) + as.factor(insulin) + hba1c + as.factor(bmi_c) + ldlc + crea + mau",
  736. "All_model" = "age + as.factor(sex) + as.factor(sbp_c) + dmdura_year + hba1c + as.factor(bmi_c) + TC_divide_HDL + chol + Egfr + as.factor(insulin) + ldlc + crea + mau + as.factor(prev_hbp) + as.factor(smoking)"
  737. )
  738. models
  739. }
  740. models <- create_models()
  741. outcome <- "drn"
  742. data_score <- data.frame(id = data_GDES$id)
  743. seed_level_perf <- data.frame()
  744. wb_seed_summary <- createWorkbook()
  745. wb_model_compare <- createWorkbook()
  746. # 仅删除结局缺失(ranger 不接受 y 中有 NA);不做其它预处理/填补
  747. stopifnot(all(c("id","drn") %in% colnames(data_train)))
  748. stopifnot(all(c("id","drn") %in% colnames(data_test)))
  749. data_train <- data_train %>% filter(!is.na(drn))
  750. data_test <- data_test %>% filter(!is.na(drn))
  751. # 与训练/测试集取交集,确保特征存在
  752. proteins <- intersect(proteins, intersect(colnames(data_train), colnames(data_test)))
  753. stopifnot(length(proteins) > 0)
  754. # (沿用你原来做法)仅在训练集上去近零方差,锁定特征清单
  755. nzv <- nearZeroVar(data_train[, proteins, drop = FALSE])
  756. proteins_use <- if (length(nzv) > 0) proteins[-nzv] else proteins
  757. stopifnot(length(proteins_use) > 0)
  758. # 因变量转为 0/1 因子(仅供 ranger 用;不改变 data_train/data_test 中原始 drn)
  759. train_y_fac <- factor(data_train$drn, levels = c(0, 1), labels = c("0", "1"))
  760. test_y_fac <- factor(data_test$drn, levels = c(0, 1), labels = c("0", "1"))
  761. # 训练集必须包含两个类别
  762. stopifnot(length(unique(train_y_fac)) == 2)
  763. # 特征矩阵(不做填补/缩放等)
  764. train_x <- data_train[, proteins_use, drop = FALSE]
  765. test_x <- data_test[, proteins_use, drop = FALSE]
  766. set.seed(2025)
  767. y_fac <- train_y_fac
  768. X <- train_x
  769. # ——1) 构造分层 k 折(类别平衡)——
  770. K <- 5
  771. folds <- caret::createFolds(y_fac, k = K, list = TRUE, returnTrain = FALSE)
  772. # ——2) 参数网格(可按需精简/扩展)——
  773. p <- ncol(X)
  774. grid <- expand.grid(
  775. num.trees = c(500), # 可加 800, 1000
  776. mtry = unique(pmax(1, round(c(sqrt(p), 0.05*p, 0.1*p)))),
  777. min.node.size = c(5, 10, 20),
  778. sample.fraction = c(0.6, 0.8) # <1.0 可缓解过拟合
  779. )
  780. cv_res <- grid
  781. cv_res$mean_auc <- NA_real_
  782. cv_res$sd_auc <- NA_real_
  783. # ——3) 逐参数组合跑 k 折,记录验证 AUC——
  784. for (gi in seq_len(nrow(grid))) {
  785. g <- grid[gi, ]
  786. aucs <- numeric(K)
  787. for (k in seq_len(K)) {
  788. idx_te <- folds[[k]]
  789. idx_tr <- setdiff(seq_len(nrow(X)), idx_te)
  790. rf_k <- ranger::ranger(
  791. formula = as.formula("y ~ ."),
  792. data = data.frame(y = y_fac[idx_tr], X[idx_tr, , drop = FALSE]),
  793. num.trees = g$num.trees,
  794. mtry = g$mtry,
  795. min.node.size = g$min.node.size,
  796. sample.fraction = g$sample.fraction,
  797. replace = TRUE,
  798. probability = TRUE,
  799. importance = "impurity",
  800. oob.error = TRUE
  801. )
  802. pred_val <- predict(rf_k, data = data.frame(X[idx_te, , drop = FALSE]))$predictions[, "1"]
  803. aucs[k] <- as.numeric(pROC::roc(response = y_fac[idx_te], predictor = pred_val, quiet = TRUE)$auc)
  804. }
  805. cv_res$mean_auc[gi] <- mean(aucs, na.rm = TRUE)
  806. cv_res$sd_auc[gi] <- stats::sd(aucs, na.rm = TRUE)
  807. }
  808. # ——4) 选择最佳参数(按平均 AUC 最大;若并列取 sd 最小)——
  809. best_idx <- order(-cv_res$mean_auc, cv_res$sd_auc)[1]
  810. best_par <- cv_res[best_idx, , drop = FALSE]
  811. print(best_par)
  812. # ——5) 用最佳参数在「全部训练集」上重训最终模型,并拿到 OOB 预测/AUC——
  813. rf_model <- ranger::ranger(
  814. formula = as.formula(paste(outcome, "~ .")),
  815. data = data.frame(drn = y_fac, X),
  816. num.trees = best_par$num.trees,
  817. mtry = best_par$mtry,
  818. min.node.size = best_par$min.node.size,
  819. sample.fraction = best_par$sample.fraction,
  820. replace = TRUE,
  821. probability = TRUE,
  822. importance = "impurity"
  823. )
  824. # ——7) 训练/测试集预测与ROC ——
  825. pred_train_prob <- predict(rf_model, data = data.frame(train_x))$predictions[, "1"]
  826. pred_test_prob <- predict(rf_model, data = data.frame(test_x))$predictions[, "1"]
  827. roc_train <- pROC::roc(response = train_y_fac, predictor = pred_train_prob, ci = TRUE, quiet = TRUE)
  828. roc_test <- pROC::roc(response = test_y_fac, predictor = pred_test_prob, ci = TRUE, quiet = TRUE)
  829. # cutoff & 混淆矩阵与派生指标
  830. if (length(unique(test_y_fac)) == 2) {
  831. cutoff_val <- cutoff::roc(pred_test_prob, test_y_fac)$cutoff
  832. pred_label <- ifelse(pred_test_prob > cutoff_val, 1, 0)
  833. cm <- caret::confusionMatrix(
  834. data = factor(pred_label, levels = c(0,1), labels = c("0","1")),
  835. reference = test_y_fac,
  836. positive = "1", mode = "everything"
  837. )
  838. TN <- cm$table[1]; FP <- cm$table[2]; FN <- cm$table[3]; TP <- cm$table[4]
  839. Accuracy <- cm$overall[["Accuracy"]]
  840. Sensitivity <- cm$byClass[["Sensitivity"]]
  841. Specificity <- cm$byClass[["Specificity"]]
  842. PPV <- cm$byClass[["Precision"]]
  843. NPV <- cm$byClass[["Neg Pred Value"]]
  844. Recall <- cm$byClass[["Recall"]]
  845. F1 <- cm$byClass[["F1"]]
  846. DR <- TP/(FN+TP) # detection rate
  847. FPR <- FP/(TN+FP) # false positive rate
  848. LR <- ifelse(FPR == 0, Inf, DR/FPR)
  849. } else {
  850. # 单一类别时占位(只保留AUC;其余置NA)
  851. cutoff_val <- NA
  852. Accuracy <- Sensitivity <- Specificity <- PPV <- NPV <- Recall <- F1 <- DR <- FPR <- LR <- NA
  853. }
  854. seed_level_perf <- rbind(seed_level_perf, data.frame(
  855. method = "RandomForest",
  856. train_auc = as.numeric(roc_train$auc),
  857. train_ci_low = roc_train$ci[1],
  858. train_ci_up = roc_train$ci[3],
  859. test_auc = as.numeric(roc_test$auc),
  860. test_ci_low = roc_test$ci[1],
  861. test_ci_up = roc_test$ci[3],
  862. cutoff = cutoff_val,
  863. Accuracy, Sensitivity, Specificity, PPV, NPV, Recall, F1, DR, FPR, LR
  864. ))
  865. # =========================== 保存逐个体score到全量框架(附dataset标记)
  866. score_col <- "score_drn_RF"
  867. dset_col <- "dataset_drn_RF"
  868. data_score_temp <- data_GDES %>%
  869. select(id) %>%
  870. left_join(data.frame(id = data_test$id, pred = pred_test_prob), by = "id") %>%
  871. left_join(data.frame(id = data_train$id, pred_train = pred_train_prob), by = "id") %>%
  872. mutate(dataset = case_when(!is.na(pred) ~ "test",
  873. !is.na(pred_train) ~ "train",
  874. TRUE ~ NA_character_),
  875. pred_final = ifelse(!is.na(pred), pred, pred_train)) %>%
  876. transmute(id, !!score_col := pred_final, !!dset_col := dataset)
  877. data_score <- left_join(data_score, data_score_temp, by = "id")
  878. # 供下面模型比较使用
  879. data_test[[score_col]] <- pred_test_prob
  880. # =========================== 测试集:传统模型 vs RF score vs 组合模型
  881. auc_rows_all_models <- data.frame()
  882. for (model_name in names(models)) {
  883. covars <- models[[model_name]]
  884. # 传统模型
  885. f1 <- as.formula(paste(outcome, "~", covars))
  886. m1 <- glm(f1, data = data_test, family = binomial)
  887. lp1 <- predict(m1, newdata = data_test) # logit 线性预测器即可;AUC对单调变换不敏感
  888. roc1 <- pROC::roc(response = data_test[[outcome]],
  889. predictor = as.numeric(lp1),
  890. ci = TRUE, quiet = TRUE)
  891. # 单独 RF score
  892. roc3 <- pROC::roc(response = data_test[[outcome]],
  893. predictor = data_test[[score_col]],
  894. ci = TRUE, quiet = TRUE)
  895. # 组合模型(传统 + RF分数)
  896. f2 <- as.formula(paste(outcome, "~", covars, "+", score_col))
  897. m2 <- glm(f2, data = data_test, family = binomial)
  898. lp2 <- predict(m2, newdata = data_test)
  899. roc2 <- pROC::roc(response = data_test[[outcome]],
  900. predictor = as.numeric(lp2),
  901. ci = TRUE, quiet = TRUE)
  902. # DeLong检验(同样显式指向 pROC)
  903. p_met <- as.numeric(pROC::roc.test(roc1, roc3)$p.value) # RF vs CONV
  904. p_comb <- as.numeric(pROC::roc.test(roc1, roc2)$p.value) # COMBINED vs CONV
  905. auc_diff <- as.numeric(roc2$auc) - as.numeric(roc1$auc)
  906. auc_impr <- auc_diff / as.numeric(roc1$auc)
  907. sig <- ifelse(p_comb >= 0.05, "",
  908. ifelse(p_comb < 0.001, "***",
  909. ifelse(p_comb < 0.01, "**", "*")))
  910. compare_met <- ifelse(p_met >= 0.05, "Comparable",
  911. ifelse(as.numeric(roc3$auc) > as.numeric(roc1$auc), "Superior", "-"))
  912. auc_rows_all_models <- rbind(auc_rows_all_models, data.frame(
  913. method = "RandomForest",
  914. outcome = outcome,
  915. model = model_name,
  916. CONV = as.numeric(roc1$auc), conv_lower = roc1$ci[1], conv_upper = roc1$ci[3],
  917. RF = as.numeric(roc3$auc), rf_lower = roc3$ci[1], rf_upper = roc3$ci[3],
  918. Pvalue_RF_vs_CONV = p_met, RF_vs_CONV = compare_met,
  919. COMBINED = as.numeric(roc2$auc), combined_lower = roc2$ci[1], combined_upper = roc2$ci[3],
  920. Diff = auc_diff, Diff_proportion = auc_impr,
  921. Pvalue_COMBINED_vs_CONV = p_comb, Significance = sig,
  922. stringsAsFactors = FALSE
  923. ))
  924. }
  925. # 1) RF汇总
  926. addWorksheet(wb_seed_summary, "RF_Summary")
  927. writeData(wb_seed_summary, "RF_Summary", seed_level_perf)
  928. saveWorkbook(wb_seed_summary, "XXXX.xlsx", overwrite = TRUE)
  929. # 2) 按模型分sheet的AUC对比结果
  930. for (model_name in names(models)) {
  931. sub <- auc_rows_all_models %>% filter(model == model_name)
  932. addWorksheet(wb_model_compare, sheetName = model_name)
  933. writeData(wb_model_compare, model_name, sub)
  934. }
  935. saveWorkbook(wb_model_compare, "XXXX.xlsx", overwrite = TRUE)
  936. # 3) 个体层score(包含score_drn_RF与dataset_drn_RF)
  937. write.csv(data_score, "XXXX.csv", row.names = FALSE)
  938. # # 4) 保存模型与对象
  939. save.image("XXXX.RData")

R code for Pro-DRN.R at commit 459bdfe, no license · at the source

Overview

Authors: Huangdong Li1,2, Ziyu Zhu1,2, Shaopeng Yang1,2, Weijing Cheng1,2, Shaoying Tan3,4,5, Zhuoyao Xin6, Lei Zhang7,8,9, Zhuoting Zhu10, Shida Chen1,2, Wenyong Huang1,2, Wei Wang1,2,11
  1. State Key Laboratory of Ophthalmology, Zhongshan Ophthalmic Center, Sun Yat-Sen University, Guangdong Provincial Key Laboratory of Ophthalmology and Visual Science, Guangdong Provincial Clinical Research Center for Ocular Diseases, Guangzhou, China
  2. Guangdong Basic Research Center of Excellence for Major Blinding Eye Diseases Prevention and Treatment, Guangzhou, China
  3. School of Optometry, The Hong Kong Polytechnic University, Kowloon, Hong Kong SAR, China
  4. Research Centre for SHARP Vision (RCSV), The Hong Kong Polytechnic University, Hong Kong, Hong Kong SAR, China
  5. Centre for Eye and Vision Research (CEVR), Hong Kong, Hong Kong SAR, China
  6. Department of Biomedical Engineering, Columbia University, New York, New York, United States of America
  7. Clinical Medical Research Center, Children’s Hospital of Nanjing Medical University, Nanjing, Jiangsu Province, China
  8. Artificial Intelligence and Modelling in Epidemiology Program, Melbourne Sexual Health Centre, Alfred Health, Melbourne, Australia
  9. Central Clinical School, Faculty of Medicine, Nursing and Health Sciences, Monash University, Melbourne, Australia
  10. Centre for Eye Research Australia, Royal Victorian Eye and Ear Hospital, Melbourne, Australia
  11. Hainan Eye Hospital and Key Laboratory of Ophthalmology, Zhongshan Ophthalmic Center, Sun Yat-sen University, Haikou, China
Journal: PLoS medicine, volume 23, issue 6, article e1004868
Dates: received 30 November 2025; accepted 20 April 2026; published online 2 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pmed.1004868 · PMID 42228639 · PMCID PMC13229346 · OpenAlex W7163180447
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism), other condition (population), clinical / translational (subfield)
Methods: Statistics, Machine learning, Physiology & signal measures
MeSH: Diabetes Mellitus, Type 2*, Diabetic Retinopathy*, Proteomics*, Retinal Degeneration*, Biomarkers, Cohort Studies, Female, Humans, Male, Middle Aged, Prospective Studies, Tomography, Optical Coherence (* major topic)
Journal subjects: Medicine and Health Sciences, Endocrinology, Endocrine Disorders, Diabetes Mellitus, Medical Conditions, Metabolic Disorders, Epidemiology, Medical Risk Factors, Biology and Life Sciences, Biochemistry, Proteomics, Research and Analysis Methods, Database and Informatics Methods, Biological Databases, Proteomic Databases, Proteins, Plasma Proteins, Mathematical and Statistical Techniques, Statistical Methods, Forecasting, Physical Sciences, Mathematics, Statistics, Diagnostic Medicine, Diagnostic Radiology, Tomography, Imaging Techniques, Radiology and Imaging, Biomarkers
Topic: Retinal Diseases and Treatments (Ophthalmology, Medicine), according to OpenAlex
Funding: Guangdong Basic Research Center of Excellence (N/A); Hainan Province Clinical Medical Center (N/A); Guangzhou Municipal Science and Technology Program key projects (2025A04J7150); National Natural Science Foundation of China (82371086, 82301253, 82571271); Natural Science Foundation of Guangdong Province (2026A1515010675); Guangdong Basic and Applied Basic Research Foundation (2022A151511); Projects of Research Center for Sharp Vision at The Hong Kong Polytechnic University (P0057931); Health and Medical Research Fund-Research Fellowship Scheme (07210207); Lumitin Vision to Brightness Research Funding for the Young and middle-aged Ophthalmologists (BCF-KH-YK-20230803-03)
Citations: cited by 2 papers (Europe PMC); 85 references in the paper
Notices: A correction to this paper has been published (42461828, from Europe PMC)

Abstract

Background: Retinal neurodegeneration is an early and independent feature of diabetic retinal disease and has been proposed as a window into the systemic neural consequences of diabetes, yet accessible molecular biomarkers and individualized prediction tools remain scarce. We aimed to identify circulating plasma protein signatures of diabetic retinal neurodegeneration (DRN) and to translate them into a clinically usable risk prediction system.

Methods and findings: In this multi-cohort prospective observational study, we integrated high-throughput plasma proteomics with longitudinal optical coherence tomography (OCT) in two independent populations. The discovery cohort comprised 1,492 participants had baseline plasma proteomics and OCT, and 1,218 were followed with repeated OCT over 6 years in Guangzhou Diabetic Eye Study (GDES). DRN was quantified by the annualized OCT-derived retinal nerve fiber layer thinning rate. In multivariable analyses adjusted for age, sex, smoking, systolic blood pressure, HbA1c, and diabetes duration, we identified 71 plasma proteins associated with development and progression of DRN. These proteins mapped onto pathways governing inflammatory immune recruitment, extracellular matrix remodeling, and microvascular homeostasis, providing a plausible biological basis for DRN. We developed a proteomics-based DRN model (Pro-DRN) using eight machine learning (ML) algorithms, including XGBoost and LightGBM. In the independent test set, Pro-DRN achieved a C-index of 0.860, rising to 0.908 when integrated with clinical variables. Compared with six conventional models, Pro-DRN improved discrimination (ΔC-index 0.137 to 0.159; all P < 0.001), reclassification (IDI 0.212 to 0.245; NRI 0.226 to 0.452; all P < 0.05). In the Hippisley model, the C-index increased from 0.739 (95% CI [0.670, 0.808]) to 0.898 (95% CI [0.858, 0.937]), with IDI 0.245 (95% CI [0.177, 0.318]), NRI 0.452 (95% CI [0.222, 0.673]) (both P < 0.001), and higher net benefit. The proteins most consistently driving model performance included ACTA2, COL6A3, and HSPG2. For clinical translation, we deployed the locked model as an interactive, web-based risk-assessment tool to support early DRN screening and longitudinal monitoring. Cross-ethnic external validation in UK Biobank (n = 502; recruited 2006–2010) reproduced core protein signals and consistent effect directions, confirming robustness across populations. Principal methodological limitation lies in single time point proteomic assessment.

Conclusion: In this multi-cohort study, we present a proteomics- and ML–based precision prediction system for DRN. Pro-DRN substantially enhanced early risk stratification beyond conventional clinical factors and may support targeted screening and timely neuroprotective interventions, advancing molecularly guided strategies for diabetic eye disease prevention.

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 1 match between paragraphs and lines of code.

ZOC-skl/Pro-DRN

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 459bdfe6dacaa07319ae981066700c7caf894863, 24 February 2026
Languages: R (1)
Size: 2 files, 1 script
Software Heritage: not archived
Found in: “Data Availability”
Holds: README
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: caret (1 file), clusterProfiler (1 file), ggplot2 (1 file), pROC (1 file), survival (1 file), tidyverse (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
2 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;
  • 1 match between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

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

Data

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

Data Availability

Data from the Guangzhou Diabetic Eye Study (GDES) are subject to Chinese data privacy regulations and are not publicly available. The data are managed by the Preventive Ophthalmology Data Management Unit, Zhongshan Ophthalmic Center, Sun Yat-sen University. Requests for access should be directed to the data manager via email at . Requests will be reviewed within 90 days according to Center policy, and, if approved, an inter-institutional data use agreement specifying use for non-commercial academic research will be required. UK Biobank data are available through application via the official platform (http://www.ukbiobank.ac.uk) under a material transfer agreement (application number: 105658). All software used in this study is publicly available. The code used in this study can be accessed at https://github.com/ZOC-skl/Pro-DRN.

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

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 11 authors, 12 MeSH terms, 9 funders, 85 references, 1 integrity notice.

Cite

This paper

Li, H., Zhu, Z., Yang, S., Cheng, W., Tan, S., Xin, Z., Zhang, L., Zhu, Z., Chen, S., Huang, W., & Wang, W. (2026). Plasma proteomic signatures of early retinal neurodegeneration in diabetes: A multi-cohort study. PLoS medicine, 23(6), e1004868. https://doi.org/10.1371/journal.pmed.1004868

BibTeX

@article{li2026plasma,
author = {Li, Huangdong and Zhu, Ziyu and Yang, Shaopeng and Cheng, Weijing and Tan, Shaoying and Xin, Zhuoyao and Zhang, Lei and Zhu, Zhuoting and Chen, Shida and Huang, Wenyong and Wang, Wei},
title = {{Plasma proteomic signatures of early retinal neurodegeneration in diabetes: A multi-cohort study}},
journal = {PLoS medicine},
year = {2026},
month = jun,
volume = {23},
number = {6},
pages = {e1004868},
publisher = {PLOS},
issn = {1549-1277},
doi = {10.1371/journal.pmed.1004868},
url = {https://doi.org/10.1371/journal.pmed.1004868},
pmid = {42228639},
pmcid = {PMC13229346}
}

RIS

TY - JOUR
AU - Li, Huangdong
AU - Zhu, Ziyu
AU - Yang, Shaopeng
AU - Cheng, Weijing
AU - Tan, Shaoying
AU - Xin, Zhuoyao
AU - Zhang, Lei
AU - Zhu, Zhuoting
AU - Chen, Shida
AU - Huang, Wenyong
AU - Wang, Wei
TI - Plasma proteomic signatures of early retinal neurodegeneration in diabetes: A multi-cohort study
T2 - PLoS medicine
J2 - PLoS Med
PY - 2026
DA - 2026/06/02
VL - 23
IS - 6
SP - e1004868
SN - 1549-1277
PB - PLOS
DO - 10.1371/journal.pmed.1004868
UR - https://doi.org/10.1371/journal.pmed.1004868
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pmed.1004868",
"type": "article-journal",
"title": "Plasma proteomic signatures of early retinal neurodegeneration in diabetes: A multi-cohort study",
"container-title": "PLoS medicine",
"author": [
{
"family": "Li",
"given": "Huangdong"
},
{
"family": "Zhu",
"given": "Ziyu"
},
{
"family": "Yang",
"given": "Shaopeng"
},
{
"family": "Cheng",
"given": "Weijing"
},
{
"family": "Tan",
"given": "Shaoying"
},
{
"family": "Xin",
"given": "Zhuoyao"
},
{
"family": "Zhang",
"given": "Lei"
},
{
"family": "Zhu",
"given": "Zhuoting"
},
{
"family": "Chen",
"given": "Shida"
},
{
"family": "Huang",
"given": "Wenyong"
},
{
"family": "Wang",
"given": "Wei"
}
],
"container-title-short": "PLoS Med",
"volume": "23",
"issue": "6",
"page": "e1004868",
"DOI": "10.1371/journal.pmed.1004868",
"PMID": "42228639",
"PMCID": "PMC13229346",
"ISSN": "1549-1277",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pmed.1004868",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
2
]
]
}
}

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.1182/bloodadvances.2025019294 [code]
Plasma proteome signatures are predictive of mortality in sickle cell disease.
Journal: Blood advances
In common: genetics / omics, 8 references
[2] doi:10.3390/ijms27156925 [code]
XGBoost-SHAP Interpretable Modeling Identifies and Validates an Eight-Gene Biomarker for Hepatic Encephalopathy Risk Prediction in Cirrhosis.
Journal: International journal of molecular sciences
In common: pROC, survival, caret, 3 other tools, genetics / omics, other condition
[3] doi:10.1073/pnas.2516601123 [code]
Unveiling the glymphatic system's role in brain aging: A comprehensive biomarker and modifiable intervention target.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: pROC, survival, ggplot2, 1 other tool, clinical / translational, 2 references
[4] doi:10.1093/neuonc/noag128 [code]
Spatially-resolved single-cell imaging of melanoma brain metastases identifies localized immune patterns predictive of immune checkpoint blockade response.
Journal: Neuro-oncology
In common: pROC, survival, caret, 2 other tools, clinical / translational, genetics / omics, other condition
[5] doi:10.21037/jtd-2026-0997 [code]
Machine learning models based on XGBoost algorithm to predict prognosis of lung cancer brain metastases.
Journal: Journal of thoracic disease
In common: pROC, survival, caret, 2 other tools, clinical / translational, other condition
[6] doi:10.1038/s41467-026-77170-3 [code]
DNA methylation profiling identifies long-range epigenetic silencing of clustered protocadherins as a key determinant of meningioma progression.
Journal: Nature communications
In common: pROC, survival, caret, 2 other tools, genetics / omics, other condition
[7] doi:10.1016/j.isci.2026.115657 [code]
Integration of machine learning to develop a disulfidptosis model for predicting glioma prognosis, immunotherapy response, and drug.
Journal: iScience
In common: pROC, survival, clusterProfiler, 2 other tools, clinical / translational, other condition
[8] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: pROC, survival, clusterProfiler, 2 other tools, genetics / omics, other condition
[9] doi:10.1080/20002297.2026.2705667 [code]
Oral microbiota dysbiosis related to the cortical thinning and cognitive impairment in cerebral small vessel disease.
Journal: Journal of oral microbiology
In common: pROC, survival, caret, 2 other tools
[10] doi:10.7717/peerj.21426 [code]
Integrated transcriptomic identification and validation reveal key autophagy-associated biomarkers in sleep deprivation.
Journal: PeerJ
In common: pROC, caret, clusterProfiler, 2 other tools, genetics / omics

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.