跳到正文
百沐一下百沐一下
本节目录

预后 signature 与风险模型

跟完这篇,您能把一堆候选基因走完「单因素 Cox 初筛 → LASSO 降维 → 多因素建模」这条主流水线,压成一个能算风险评分(risk score)的预后 signature,据此把样本切成高、低风险两组,出 KM 曲线、时间依赖 ROC 和列线图,并在独立数据集上验证它到底能不能用。

先备好两样输入

整条流程从头到尾只吃两张表,样本一一对应:

形状关键列 / 值
表达矩阵 expr行样本、列基因标准化后的表达量,如 log2(TPM+1)
生存信息表 clin行样本time(随访时长)、event(0/1 结局,1=终点事件发生)

三件事先对齐好,后面才不会错位:

  • 两张表的样本顺序、行名要完全一致expr 第 i 行必须是 clin 第 i 行那个人。
  • timeevent 成对出现:还活着 / 未失访但未发生终点,记 event = 0(删失);发生终点(如死亡、复发)记 event = 1
  • time单位(天、月)自己定,但全程统一——后面时间依赖 ROC 的年份换算全靠它。

矩阵怎么从原始数据整理成这个形状,见 表达矩阵与分组表整理规范

整体思路:三步压出一个 signature

预后建模不是把上百个基因一股脑塞进模型,而是层层过滤、逐步降维,最后落到一个「基因数不多、又能预测预后」的组合。主流做法就三步:

步骤目的关键包 / 函数
1. 单因素 Cox 初筛从全部候选里挑出「单看就和预后相关」的基因survival::coxph
2. LASSO 降维在相关基因里去冗余、防过拟合,进一步缩小到几个glmnet::cv.glmnet
3. 多因素 Cox 建模用留下的基因拟合联合模型,得到系数与 risk scoresurvival::coxph

第一步,单因素 Cox 逐个筛。 对每个候选基因单独跑一次 Cox,看它与生存是否相关,留下达到阈值的:

library(survival)

genes <- colnames(expr)          # 候选基因
uni <- lapply(genes, function(g) {
  fit <- coxph(Surv(time, event) ~ expr[, g], data = clin)
  s   <- summary(fit)
  data.frame(gene = g,
             HR   = s$coefficients[, "exp(coef)"],
             p    = s$coefficients[, "Pr(>|z|)"])
})
uni <- do.call(rbind, uni)
sig <- uni$gene[uni$p < 0.05]    # p<0.05 是常用惯例,非铁律,按数据与候选量调

p < 0.05 只是常见起点。候选基因很多时,为控制假阳性可以收严(如 0.01),或先做多重检验校正——这一步的目的是把明显无关的先剔掉,不是定终局。

第二步,LASSO 进一步降维。 初筛剩下的基因之间常高度共线(一条通路上的基因一起涨一起落),直接全放进多因素模型会不稳。LASSO 通过惩罚把一部分系数压到 0,自动做变量选择:

library(glmnet)

x <- as.matrix(expr[, sig])                 # 只用初筛留下的基因
y <- Surv(clin$time, clin$event)

set.seed(1)                                 # 固定随机种子,交叉验证结果可复现
cvfit <- cv.glmnet(x, y, family = "cox", alpha = 1)   # alpha=1 即 LASSO

coef_min <- coef(cvfit, s = "lambda.min")   # 或 s = "lambda.1se",更稀疏、更保守
keep <- rownames(coef_min)[as.numeric(coef_min) != 0]  # 系数非 0 的基因即入选

两点常被问到:

  • 不用先手动标准化基因。 glmnet 默认 standardize = TRUE,内部会把各基因缩放到可比尺度再惩罚,最后把系数还原到原尺度。您自己再 z-score 一遍反而容易搞乱。
  • lambda.min 还是 lambda.1se 前者给交叉验证误差最小的模型,后者在其一个标准误内挑更简单(基因更少)的模型,通常更抗过拟合。选哪个都要如实报告,别混着说。

第三步,多因素 Cox 得 risk score。 用 LASSO 留下的基因拟合联合模型,每个基因得到一个系数,risk score 就是「系数 × 表达量」的加权和:

multi <- coxph(Surv(time, event) ~ .,
               data = data.frame(expr[, keep], clin[, c("time", "event")]))
risk  <- predict(multi, type = "lp")        # 线性预测值,即 risk score

也可以直接用 LASSO 的系数手算 risk score,两条路都常见。到这里,signature 就成型了:一组基因,加上各自的权重。

分高、低风险两组

有了每个样本的 risk score,下一步是切成两组,好画 KM、讲清楚。最常见、也最好复现的切法是按 risk score 中位数

clin$risk  <- risk
clin$group <- ifelse(clin$risk > median(clin$risk), "high", "low")

几点要留心:

  • 中位数是常用惯例、不是铁律。 它简单、事先确定、不看结局,最好复现。也有人用最佳截断(survminer::surv_cutpoint),但那是从结局里挑切点,天然偏向让 p 变小,容易高估显著性、难以外部重现——用了务必如实说明,并在独立集验证。切点选择的坑,见 生存分析:KM 曲线与 log-rank 检验
  • 切点必须在训练集上定死,原样搬到验证集。 不能到了验证集又按验证集自己的中位数重切,否则就不是「验证同一个模型」了。
  • 高、低风险的中位 risk score 阈值本身要写进论文,别人才能复算。

出三张标配图:KM、时间依赖 ROC、列线图

分好组,接下来是预后模型的三张标配图,各回答一个不同的问题:

回答什么问题关键包 / 函数
KM 曲线 + log-rank高、低两组生存差多少survminer::ggsurvplot
时间依赖 ROCrisk score 在 1/3/5 年排序准不准timeROC::timeROC
列线图 nomogram给单个患者手算某年生存概率rms::nomogram

一、KM 曲线 + log-rank: 直观看两组生存差多少。

library(survminer)
fit <- survfit(Surv(time, event) ~ group, data = clin)
ggsurvplot(fit, data = clin, pval = TRUE, risk.table = TRUE)

pval = TRUE 会标出 log-rank p。怎么读这张图、怎么写中位生存,见 生存分析:KM 曲线与 log-rank 检验

二、时间依赖 ROC: KM 比的是「分组对不对」,ROC 比的是「risk score 排序准不准」。生存数据里结局随时间发生,所以要按时间点(如 1、3、5 年)分别算 AUC:

library(timeROC)
roc <- timeROC(T = clin$time, delta = clin$event,
               marker = clin$risk, cause = 1,
               times = c(365, 1095, 1825))   # 1/3/5 年,单位随您的 time 而定(此处按天)
roc$AUC                                       # 三个时间点各自的 AUC

times 的单位要和 time 列一致:若随访以天计,1/3/5 年就是 365/1095/1825;以月计则是 12/36/60。AUC 越接近 1 越好,0.5 等于瞎猜。

三、列线图(nomogram): 把 risk score(常再联合分期、年龄等临床变量)做成一张能手算生存概率的图,临床上直观好用。

library(rms)
dd <- datadist(clin); options(datadist = "dd")   # rms 要求先注册数据分布
cph_fit <- cph(Surv(time, event) ~ risk + stage + age, data = clin,
               surv = TRUE, x = TRUE, y = TRUE)
surv <- Survival(cph_fit)                         # 取出生存函数,供下面调用
nom <- nomogram(cph_fit,
                fun = function(x) surv(1095, x),   # 例:预测 3 年(1095 天)生存概率
                funlabel = "3-year survival")
plot(nom)

列线图一般要配一张校准曲线rms::calibrate),看预测概率和实际观测生存对不对得上——预测得漂亮但校准差的模型,不能用。

过拟合与验证:训练集建、独立集验

这是整篇最容易翻车、审稿人也盯得最紧的一步。用来建模的那批数据,模型天生就「见过」,在它身上表现好几乎是必然的——那不叫验证,那叫自我印证。真正说明模型有用的,是它在没参与建模的数据上还站得住。

要交代清楚几件事:

  1. 划分要在建模前定好。 常见做法是把一个队列按比例(如 7:3)随机分成训练集、内部验证集,或直接用另一个独立队列(如另一个 GEO 数据集、TCGA 之外的队列)做外部验证。所有筛基因、定系数、定切点的动作,只在训练集上做

  2. 验证集只做一件事:套用现成模型。 用训练集定下的基因、系数、切点,在验证集上算 risk score、分组,再看 KM 是否依然显著、时间依赖 AUC 是否依然像样。

  3. 训练集和验证集的表现要分开、成对地报,读者才能自己判断稳不稳:

    指标训练集验证集怎么读
    KM log-rank p验证集仍显著才算数
    1/3/5 年 AUC掉一点正常,骤降警惕
    C-index两边都报,看掉多少

    C-index(一致性指数)衡量「模型把预后差的排在前面」的能力,coxph 对象直接给:

    summary(multi)$concordance      # 返回 C-index 及其标准误
    

    C-index 0.5 等于随机、1 为完美。多少算「好」没有公认铁定阈值,随癌种、随对照而变——关键是训练集掉到验证集掉多少:掉得少说明稳,从很高骤降到接近 0.5,基本就是过拟合。

  4. 老实交代边界。 单中心、小样本、基因数相对样本数偏多,都会让模型偏乐观;批次效应、平台差异也会让 signature 换个数据集就失灵。这些如实写进局限,比藏着强。

一句话原则:能在独立数据集上重复出来的模型,才值得写;只在自己数据上好看的,多半是过拟合。

结果怎么写进论文

方法和图都齐了,落到笔头。一段规范的预后模型结果,通常把这几件事说全:

  • signature 由哪些基因、什么权重组成:列出最终入选基因及各自 Cox 系数(或直接给出 risk score 公式),别人才能复算。

  • 怎么筛、怎么降维、怎么切:单因素阈值、LASSO 选的是 lambda.min 还是 lambda.1se、按中位数还是别的切点——逐一交代,含随机种子。

  • 训练集与验证集的表现分开报:两边的 KM p 值、1/3/5 年 AUC、C-index 都给。

  • 一句话范式(可直接改用):

    基于 8 个基因构建的风险评分将患者分为高、低风险两组,高风险组总生存显著更差(log-rank p < 0.001);该 signature 在训练集与独立验证集中预测 3 年生存的 AUC 分别为 0.74 与 0.71,C-index 分别为 0.72 与 0.69。

    (句中数字仅为示例格式,请换成您自己的真实结果。)

  • 写清局限:样本量、单 / 多中心、是否外部验证、有无竞争风险等未处理的问题,如实说明。

在平台上跑一遍

从单因素 Cox 到 LASSO、再到 risk score、KM、时间依赖 ROC、列线图和验证,这整条链在百沐一下都不用写代码:上传带 timeevent 的临床表和表达矩阵,用一句话说清目的——「用这批候选基因构建预后风险模型,单因素 Cox 初筛后做 LASSO,建 risk score 并按中位数分高、低风险组,出 KM 和 1/3/5 年时间依赖 ROC,并在验证集上评估 C-index」——平台会按标准流程建模、分组、出图,并给出可复现的 R 代码;想改阈值、切点或验证方式,直接在对话里说明即可。上手见 智能统计使用指南,或直接开始:https://app.baimuyixia.com

相关