☰
生存分析进阶三板斧:时间依赖Cox、竞争风险模型与AFT/RMST
2026/10/25 2:05:03 网站建设 项目流程

前几天帮同事看一个随访数据分析报告,前半段是KM曲线分组,后半段是Cox回归系数表,单看结果都正常。但追问三个问题后,他有点答不上来:Cox模型的比例风险假设到底验没验?患者死亡前如果出现其他严重事件,KM里要不要换一种处理?临床决策时除了风险比,能不能直接给出5年生存时间或受限平均生存时间?这几个问题,恰好是生存分析从入门到进阶的分界线。KM曲线和Cox回归是生存分析最常用的两根柱子,但柱子之外还有一套更贴近真实工作场景的分析思路。真正让我意识到“只会基础版本不够”的,是一次评审反馈:对方问的不再是“HR是多少”,而是“这个效应是不是随时间变化”“事件风险是不是被竞争事件高估了”。这些问题的答案,远远超出KM曲线加Cox回归能覆盖的范围。

只靠KM+Cox,能回答“谁的风险更高”,但不太容易回答“风险是否随时间变化”“除了目标事件还有什么在影响结局”以及“绝对生存时间差多少”。所以如果把生存分析的进阶分为三板斧,我建议从三个方向入手:时间依赖Cox、竞争风险模型、AFT与RMST。这篇内容想做的,不是否定基础方法,而是补上三类基础方法覆盖不到的关键场景。

1. 先搞明白:基础KM和Cox到底卡在哪里

1.1 KM曲线和Cox回归的真正适用边界

KM曲线解决的是“分组生存概率怎么估计”。它能够在存在删失的情况下,用Kaplan-Meier法估计每个时间点的生存概率,然后画成阶梯形曲线。它的优势是直观、非参数、假设少,适合做探索性分析和分组比较。Cox回归解决的是“多个协变量同时影响风险时,每个变量的贡献有多大”。它用比例风险假设把风险函数拆成基线风险乘以协变量指数项,输出的是风险比(HR)。

这个组合在大多数简单场景下是够用的:做随机分组比较、筛选影响预后的因素、控制混杂变量、报告风险比和置信区间。问题是,它有几个默认设定在真实随访数据里并不总是成立。

第一个默认设定是“风险比恒定”。Cox模型要求任意两组在不同时间点的风险比保持不变,也就是比例风险假设。如果治疗组在早期风险下降很快、晚期反而上升,那一个单一的HR会掩盖整个过程。第二个默认设定是“只有一个终点事件”。当个体可能发生多种互斥事件时,比如研究肿瘤复发,患者死亡后不可能再观察复发,如果直接把死亡当作删失,KM曲线和Cox回归都会给出有偏的估计。第三个默认设定是“效应大小可以用相对风险表达”。HR是一个相对估计,但医患沟通和卫生经济学往往需要绝对效应,比如“中位生存时间延长多久”“到第5年时平均多活多少个月”。

1.2 三个让基础方法失效的常见场景

我总结过三个最容易触发进阶需求的现场信号。

第一个信号:Schoenfeld残差检验显著,或者log-log生存曲线出现交叉不平行。这说明不同组的风险比不是常数,这时候一个简单的“治疗组HR=0.7”并不能准确描述整个随访期;可能第一年HR=0.5,第三年HR=0.9,第六年HR=1.3。基础Cox只能给出一个平均磨损后的结果,容易误导结论。

第二个信号:终点事件定义写得不太干净。比如研究“心血管事件”,但患者可能先死于非心血管原因;研究“复发”,但患者可能先死亡。只要竞争事件存在,KM把竞争事件当成删失,实际上就是在假设“这个人以后还可能复发”,这不符合真实过程。

第三个信号:团队或客户真正想要的不是“风险比”,而是“能活多久”。HR告诉你的方向,但不直接告诉月数。尤其是做决策、做患者沟通、做成本效果分析时,需要的是绝对生存时间差异。这时候AFT或受限平均生存时间(RMST)比Cox更贴近问题本身。

这三个信号不是互相排斥的,一个数据里可能同时出现非比例风险、竞争事件、绝对效应需求。这也是为什么“三板斧”要一起学,而不是按需挑一个。

2. 第一板斧:时间依赖Cox,处理“风险比不恒定”

2.1 先判断PH假设是不是真的靠谱

时间依赖Cox不是一种单一模型,而是一类处理“非比例风险”的策略。要判断是否需要它,先做诊断。

常用做法是Schoenfeld残差检验。在R里,用survival包拟合Cox模型后,调用cox.zph()即可。常见写法是:

library(survival) fit_cox <- coxph(Surv(time, status) ~ treatment + age + sex, data = df) # 检查比例风险假设 ph_test <- cox.zph(fit_cox) print(ph_test) plot(ph_test)

如果某个变量的检验p值很小,或者残差图中平滑曲线明显不是水平的,就提示该变量不满足比例风险假设。另一个更直观的方法是画分组log-log生存曲线,也就是对生存概率取两次对数。若曲线大致平行,说明PH假设还可接受;若曲线交叉或明显发散,就要考虑进阶处理。

这里要提醒一句:cox.zph只是诊断工具,不是判决书。小样本下检验功效不足,不做处理也许问题不大;大样本下哪怕偏离很小,也可能被检成显著。更稳妥的判断是“图形+实际效应变化幅度”一起看,不要因为一个p值就急着换模型。

2.2 思路一:按时间分层,让风险比在各段内稳定

处理非比例风险的第一种常用思路是时间分层。它的逻辑是:既然整体风险比不恒定,那就把随访时间切成几段,分别估计不同时间段的风险比,再用一个基线风险分段建模。

一种落地方式是用survSplit()把长格式数据按时间点切开,比如按12个月、24个月切成多个记录,然后对时间分组变量做stratification:

df_split <- survSplit(Surv(time, status) ~ treatment + age + sex, data = df, cut = c(12, 24), episode = "period") fit_strata <- coxph(Surv(time, status) ~ treatment + age + sex + strata(period), data = df_split)

这样做相当于允许不同时间段有不同的基线风险趋势,而且可以进一步加入time_period与treatment的交互项,观察治疗效应在早期、中期、晚期是否真的不同。

时间分层的优点是直观、可解释。缺点是时间段切点需要预先判断,切点不同可能影响结论。我的习惯是先看累计事件数的分布,再结合临床随访节点选择切点,而不是随便等分。比如随访数据如果以年为单位,通常先尝试12个月、24个月;如果事件数在某个点特别密集,也可以根据事件分布调整。

2.3 思路二:加时间交互项,直接估计风险比变化趋势

另一种更平滑的处理方式是在Cox模型中增加协变量与时间函数的交互项。R中的coxph支持tt参数,常见写法是:

fit_tt <- coxph(Surv(time, status) ~ treatment + age + sex + tt(treatment), data = df, tt = function(x, t, ...) x * log(t))

这里的tt(treatment)会在Cox风险模型中把treatment乘以log(t),从而把治疗效应建模为时间的函数。如果tt项的系数显著,就说明治疗的风险比确实随着时间在变化;如果系数不显著,则更倾向于支持恒定HR。

这种思路的优点是避免了人为切时间点,可以更平滑地刻画风险比随时间的演化。它也不是没有代价:如果时间函数选得不对,模型会失去解释力。常见选择有log(t)、t、sqrt(t)等。实际使用时可以做敏感性分析,换几种时间函数看结论是否一致。

需要区分两个概念:时变协变量和时间交互项。时变协变量指的是协变量本身取值随时间变化,比如血压、用药状态;时间交互项指的是协变量固定不变,但它的系数随时间变化。第一板斧主要解决的是“系数随时间变化”,不要把两者混为一谈。如果数据结构本身就是重复测量的时变协变量,还需要处理内因时变协变量和内生性问题,那就比单纯的tt()复杂得多。

3. 第二板斧:竞争风险模型,别再把竞争事件当成删失

3.1 竞争风险为什么不是另一种删失

很多人在处理随访数据时,习惯把所有“没有发生目标事件”的个体都放在删失组。这个习惯在简单分析里问题不大,一旦存在竞争事件就会出错。

删失的真实含义是“从某个时间点之后观测不到这个个体”,但理论上他未来仍有可能发生目标事件。竞争事件的真实含义是“这个事件发生后,目标事件不可能再发生了”。举例来说,研究“肿瘤复发时间”,一名患者在第10个月死亡,死亡后就再也不可能被观察到复发。如果按照KM/Cox的常规处理,把死亡当成删失,相当于默认“如果继续随访,他迟早可能复发”。这会低估复发事件的累积发生率,也会扭曲后续回归模型的估计。

所以,竞争风险模型的第一步不是选方法,而是把事件编码理顺。常见约定是:status=0表示删失,status=1表示目标事件,status=2表示竞争事件。具体数字只是约定,但必须在数据清洗阶段就固定下来,避免后面把竞争事件和普通删失混在一起。

3.2 CIF、原因别风险与子分布风险该怎么理解

KM生存曲线的互补是“1-KM”,它估计的是不考虑竞争事件时的目标事件累计发生率。但在竞争风险存在时,应该用累计发生函数(CIF)来估计目标事件发生概率。CIF的经典估计是Aalen-Johansen估计,它在估计目标事件累计发生率时会把竞争事件的个体从风险集中移出去,但不会简单地把他们当成普通删失。

回归层面有两条路线:原因别风险模型(cause-specific hazard model)和子分布风险模型(subdistribution hazard model)。

原因别风险模型的思路是:把竞争事件当成删失,对目标事件单独拟合一个Cox模型;同时也可以对竞争事件再拟合一个Cox模型。它回答的问题是“在尚未发生任何事件的人里,某个因素如何影响目标事件的发生速率”。适合做病因学解释。

子分布风险模型常与Fine-Gray模型对应,它直接对CIF建模,回答的是“在整个人群里,某个因素如何影响目标事件的累积发生率”。它更适合预测个体风险、做累积发病人数估计,也是很多临床上更喜欢汇报SHR(subdistribution hazard ratio)的原因。

两者并不等价,选择哪一种取决于研究目标。如果想把目标事件的预测概率落在绝对尺度上,Fine-Gray更常用;如果更关心机制解释和公共卫生干预,原因别风险往往更有意义。

3.3 用R快速验证一条完整路径

R里最常用的是cmprsk包。先做累积发生函数和组间比较:

library(cmprsk) # status:0=删失,1=目标事件,2=竞争事件 cuminc_fit <- cuminc(ftime = df$time, fstatus = df$status, group = df$treatment) print(cuminc_fit) plot(cuminc_fit)

Fine-Gray回归可以用crr():

cov <- model.matrix(~ age + sex + treatment, data = df)[, -1] fg_fit <- crr(ftime = df$time, fstatus = df$status, cov1 = cov) print(fg_fit)

crr()默认把status中第一个非零值当作目标事件,其他非零值当作竞争事件。实际操作前最好确认事件编码顺序,避免把目标事件和竞争事件搞反。

如果只想快速比较组间CIF差异,还需要根据事件数量判断是否适合用Gray检验。当某一事件例数很少时,检验功效会很低。结果汇报时,除了SHR和置信区间,最好同时展示随访时间、删失比例、目标事件数、竞争事件数。这些信息直接影响结果可信度。

4. 第三板斧:AFT与RMST,把“风险比”翻译成“时间差”

4.1 为什么只看HR还不够

Cox模型给出的是风险比,描述的是“瞬时风险”的相对倍数。可临床上很多决策需要的是“时间”:患者更想知道“治疗组的中位生存时间是不是更长”“到第3年时平均多活多少个月”。HR=0.7不能说自动等价于“生命延长30%”,因为风险比和生存时间中位数的关系取决于生存分布的形状和基线风险。

另外,Cox模型的半参数特点使得它不需要指定生存时间分布,但代价是它不直接预测生存时间。想要预测生存概率或中位生存时间,还得回到基线生存函数。AFT和RMST提供了更直接的选择。

4.2 AFT模型:直接给生存时间建模

加速失效时间模型(AFT)的思路和Cox相反:它把生存时间取对数后建模为协变量的线性函数,核心输出是时间比(Time Ratio,TR)。TR大于1表示该因素延长生存时间,小于1表示缩短生存时间。常见分布包括Weibull、log-normal、log-logistic。

R中可以用survival包的survreg()拟合:

aft_weibull <- survreg(Surv(time, status) ~ treatment + age + sex, data = df, dist = "weibull") summary(aft_weibull) exp(coef(aft_weibull))

exp(coef())得到的是时间比。例如treatment对应的时间比为1.25,可以解释为“接受该治疗后,生存时间尺度大约延长25%”。如果数据明显是右偏且事件率较高,Weibull模型常常是一个不错的起点;如果风险率是非单调的,log-normal或log-logistic可能更合适。

AFT的优点是直接回答绝对时间问题,且不同分布下可以横向比较。缺点是它依赖分布假设,如果分布选得不好,结果可能有偏。实际使用中建议同时拟合两三种分布,看核心结论是否稳定,不要只用一个Weibull就下结论。

4.3 RMST:指定时间窗内的平均生存时间

受限平均生存时间(Restricted Mean Survival Time, RMST)是指在某个提前设定的时间点tau之前,生存曲线下的面积。它回答的是“到tau时刻为止,平均存活时间是多少”。因为不需要对整个生存曲线积分,避开了尾部数据不稳定和删失过重的问题。

在R里可以用survRM2包实现:

library(survRM2) rmst_result <- rmst2(time = df$time, status = df$status, arm = df$treatment, tau = 60) print(rmst_result)

其中tau需要提前指定,比如60个月或5年。输出会包含各组的RMST估计、差异、置信区间和p值。RMST差值的解释很直接:治疗组比对照组在随访前5年里的平均生存时间多0.8年。

tau的选择很关键,不能只看数据把p值压到最小才选。常见做法是选择有足够随访和事件支持的时间点,最好在分析计划中提前指定,并把不同tau值作为敏感性分析。如果tau远超过大多数实际随访时间,估计会非常不稳;如果tau设得太早,又可能浪费信息。

RMST不是只能用于无竞争风险的数据。在竞争风险存在时,也可以估计考虑竞争事件后的受限平均生存时间。但要注意基础包的默认实现不一定区分竞争事件,使用前需要看文档或改用竞争风险框架下的RMST方法。

5. 三板斧怎么选、怎么排、怎么避坑

5.1 一张决策表:从问题到方法

进阶三板斧学完之后,最容易出现的问题是“每个方法都会跑,但不知道自己该用哪个”。我的建议是建立一张决策表,先把研究问题翻译成方法需求。

核心问题推荐方法关键输出
不同协变量对风险的影响方向是什么,控制混杂后HR是多少基础Cox回归HR和置信区间
风险比随时间变化,单一HR不可靠时间依赖Cox:时间分层或tt交互项各时间段HR或HR随时间变化趋势
存在竞争事件,需要估计目标事件的累计发生率累计发生函数CIF特定时间点累积发生率
存在竞争事件,需要回归效应Fine-Gray或原因别风险模型SHR或cause-specific HR
需要绝对效应,比如中位生存时间、生存时间延长多少AFT模型Time Ratio
需要指定随访期内平均生存时间之差RMSTRMST差值及置信区间
要做个体风险预测,且关注预测精度时间依赖Cox或Fine-Gray的预测版本,也可以尝试机器学习扩展C指数、校准曲线

这张表的重点是:先看问题,再选模型,不要上来就想着“我今天要用Fine-Gray”。基础Cox不是必须要被替换,而是在它不够用的时候才加进阶。

5.2 最容易翻车的六个工程细节

从单次跑通到真正用于项目,有几个细节很容易翻车。

第一,事件编码不一致。同一个项目里,status=1有时表示删失,有时表示死亡,换了包就乱。建议项目开始时就固定一个编码字典,并在分析脚本里用因子标签明确注释。第二,把竞争事件当删失。这个问题会直接污染KM、Cox、AFT和RMST,不能靠后期建模弥补。第三,PH假设检验做了但没看图形。cox.zph的p值只能辅助判断,一定要结合残差图和log-log曲线一起看。第四,时间切点或tau选得太多。一次分析里反复试多个切点、多个tau,会抬高假阳性风险。更稳妥的做法是在分析计划中预设1到2个主要切点,其余作为敏感性分析。第五,随访时间和删失率不报告。只报模型结果不报中位随访时间、风险集人数,别人很难判断结果稳健性。第六,样本量不够还上复杂模型。时间依赖Cox、Fine-Gray、AFT都需要足够的事件数。事件数太少时,参数估计会很不稳定,此时回到KM+基础Cox也许反而更可靠。

5.3 建议的学习路径

如果现在只会KM和Cox,下一步不要急着学各种带“Deep”的生存分析模型。先把这三板斧按顺序练熟。

第一阶段,复现一个只含二分组变量的随访数据,跑KM曲线、Cox回归、cox.zph,画出log-log曲线。第二阶段,构造一个带时间交互项的Cox模型,用log(t)作为时间函数,并比较不同时间函数下的结论变化。第三阶段,给数据加上一个竞争事件,用cuminc和crr重跑一遍,理解CIF和1-KM的差异。第四阶段,用survreg拟合AFT,再用survRM2计算RMST,注意tau的选择和解释方式。

这样的顺序可以让你在每一步都能看到“基础方法哪里不够、进阶方法补了什么”。等你熟练了,再去看多状态模型、动态预测、机器学习生存分析,就不会觉得新名词复杂。它们只是在回答同一类时间到事件问题时的不同尺度和角度而已。

回头再看开头的三个追问,它们其实不是刁难,而是生存分析真正面对真实世界时的必然问题:风险会不会变,事件之间会不会互相干扰,效果能不能用时间说清楚。把时间依赖Cox、竞争风险模型和AFT/RMST练好,再遇到“KM+Cox不够用”的情况,你就有了一套可以落地的回应方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询