R—生存分析—Kaplan-Meier + Log-rank test + Cox回归

参考教程:http://www.sthda.com/english/wiki/survival-analysis-basics
假设有228个被诊断患有肺癌的病人开始长期的随访,如果病人死亡;则随访结束,并记录生存时间(从确诊到死亡的时间)。而生存分析Survival Analysis 主要回答三个方面的问题:
(1)病人的第5年生存率是多少?病人总体的中位生存率的时间可以到多久?
(2)若根据病人信息(例如性别),将病人分为两组,那么这两组的生存率是否有显著差异?
(3)若有很多病人的临床信息或者分子特征,其中哪些属性会显著影响病人的生存?

1、基本概念

1.1 event事件与survival time生存时间

  • 在生存分析中,这里的事件一般是指死亡,在广义的生存分析中,事件可以引申至许多其它含义,例如疾病复发等;

如果处理TCGA的数据,会发现有各种各样的event的定义,可以参考笔记https://www.jianshu.com/p/f07588bcf22e。本文笔记全篇采用的是死亡作为事件。

  • 而生存时间是指在事件发生的前提下,从病人确诊到死亡的时间。

1.2 Censor 删失值

  • 在随访的过程中,可能无法全部病人都可以收集到准确地生存时间。
  • 一方面由于随访项目时间限制,直至随访结束,病人仍在世;一方面可能在随访期间,病人失联、或者因其它原因去世等,无法继续随访。
  • 这样的数据就称之为Censor,而此时的生存时间即为所能记录到的最长生存时间。病人的status要么是事件发生了,要么就是Censor。


2、Kaplan-Meier与Log-rank test

2.1方法简介

  • Kaplan-Meier简单来说是根据病人的status与survival time计算、预估病人在确诊后时间t时刻的生存率(survival probability)如何。

survivor function关注的是event事件不发生的概率;与之相反,之后会提到的hazard function则主要关注event事件发生的概率

  • Log-rank test是指将病人分为两组或者多组的情况下,组间的生存率是否有显著差异。

一般上述二者同时分析

2.2 分析R包与示例数据

  • R包
# install.packages(c("survival", "survminer"))

library("survival") #生存分析
library("survminer") #结果可视化
  • 示例数据 lung {survival}
head(lung)
#     inst time status age sex ph.ecog ph.karno pat.karno meal.cal wt.loss
# 1    3  306      2  74   1       1       90       100     1175      NA
# 2    3  455      2  68   1       0       90        90     1225      15
# 3    3 1010      1  56   1       0       90        90       NA      15
# 4    5  210      2  57   1       1       90        60     1150      11
# 5    1  883      2  60   1       0      100        90       NA       0
# 6   12 1022      1  74   1       1       50        80      513       0

# inst: Institution code
# time: Survival time in days(*) 生存时间
# status: censoring status 1=censored, 2=dead(*) 事件是否发生
# age: Age in years
# sex: Male=1 Female=2
# ph.ecog: ECOG performance score (0=good 5=dead)
# ph.karno: Karnofsky performance score (bad=0-good=100) rated by physician
# pat.karno: Karnofsky performance score as rated by patient
# meal.cal: Calories consumed at meals
# wt.loss: Weight loss in last six months

如果挖掘TCGA的数据,通常0表示存活,1表示死亡

2.3 survival分析

  • 开始分析:按性别将病人分为两组
fit <- survfit(Surv(time, status) ~ sex, data = lung)
print(fit)
# Call: survfit(formula = Surv(time, status) ~ sex, data = lung)
# 
# n events median 0.95LCL 0.95UCL
# sex=1 138    112    270     212     310
# sex=2  90     53    426     348     550

如上,n表示每组的病人数;events表示每组有多少病人死亡;median表示中位生存率所对应的生存时间;最后两列表示第三列的95%置信区间。

res.sum <- surv_summary(fit)
head(res.sum[res.sum$sex==1,])
#   time n.risk n.event n.censor      surv    std.err     upper     lower strata sex
# 1   11    138       3        0 0.9782609 0.01268978 1.0000000 0.9542301  sex=1   1
# 2   12    135       1        0 0.9710145 0.01470747 0.9994124 0.9434235  sex=1   1
# 3   13    134       2        0 0.9565217 0.01814885 0.9911586 0.9230952  sex=1   1
# 4   15    132       1        0 0.9492754 0.01967768 0.9866017 0.9133612  sex=1   1
# 5   26    131       1        0 0.9420290 0.02111708 0.9818365 0.9038355  sex=1   1
# 6   30    130       1        0 0.9347826 0.02248469 0.9768989 0.8944820  sex=1   1
head(res.sum[res.sum$sex==2,])
#     time n.risk n.event n.censor      surv    std.err     upper     lower strata sex
# 120    5     90       1        0 0.9888889 0.01117336 1.0000000 0.9674682  sex=2   2
# 121   60     89       1        0 0.9777778 0.01589104 1.0000000 0.9477934  sex=2   2
# 122   61     88       1        0 0.9666667 0.01957401 1.0000000 0.9302835  sex=2   2
# 123   62     87       1        0 0.9555556 0.02273314 0.9990942 0.9139143  sex=2   2
# 124   79     86       1        0 0.9444444 0.02556550 0.9929738 0.8982868  sex=2   2
# 125   81     85       1        0 0.9333333 0.02817181 0.9863173 0.8831956  sex=2   2
  • 如上好像没有看到 log-rank test的结果,即组间生存率差异的显著性。其实参看下面的可视化结果是有的,如果想提取出来P值的内容,可以如下操作:
surv_diff <- survdiff(Surv(time, status) ~ sex, data = lung)
# Call:
#   survdiff(formula = Surv(time, status) ~ sex, data = lung)
# 
# N Observed Expected (O-E)^2/E (O-E)^2/V
# sex=1 138      112     91.6      4.55      10.3
# sex=2  90       53     73.4      5.68      10.3
# 
# Chisq= 10.3  on 1 degrees of freedom, p= 0.001

#虽然如上结果是有P值的,但是尝试之后发现从对象中只能提取chisq的值,然后再进一步转换
p.val = 1 - pchisq(surv_diff$chisq, length(surv_diff$n) - 1)
p.val
# [1] 0.001311165

2.4 survminer结果可视化

(1)生存曲线
ggsurvplot(fit,
           pval = TRUE, conf.int = TRUE,
           risk.table = TRUE)
图释

上图表示:时间对应的生存率的生存曲线图,曲线中的短竖线表示在时间点有缺失值病人。不同颜色表示不同分组,阴影部分表示95%置信区间;左下角p值表示基于log-rank test计算得到的P值,对于本图的结果p=0.0013,即不同性别的生存率有显著差异。
下面的表:不同时间点对应每组尚未发生事件(死亡)的数目。

图形参数(结合具体代码理解)
p1=ggsurvplot(fit,
           pval = TRUE, conf.int = TRUE,
           risk.table = TRUE, # Add risk table
           risk.table.col = "strata", # Change risk table color by groups
           linetype = "strata", # Change line type by groups
           surv.median.line = "hv", # Specify median survival
           ggtheme = theme_bw(), # Change ggplot2 theme
           palette = c("#E7B800", "#2E9FDF"))
p2=ggsurvplot(
  fit,                     # survfit object with calculated statistics.
  pval = TRUE,             # show p-value of log-rank test.
  conf.int = TRUE,         # show confidence intervals for 
  # point estimaes of survival curves.
  conf.int.style = "step",  # customize style of confidence intervals
  xlab = "Time in days",   # customize X axis label.
  break.time.by = 200,     # break X axis in time intervals by 200.
  ggtheme = theme_light(), # customize plot and risk table with a theme.
  risk.table = "abs_pct",  # absolute number and percentage at risk.
  risk.table.y.text.col = T,# colour risk table text annotations.
  risk.table.y.text = FALSE,# show bars instead of names in text annotations
  # in legend of risk table.
  ncensor.plot = TRUE,      # plot the number of censored subjects at time t
  surv.median.line = "hv",  # add the median survival pointer.
  legend.labs = 
    c("Male", "Female"),    # change legend labels.
  palette = 
    c("#E7B800", "#2E9FDF") # custom color palettes.
)
arrange_ggsurvplots(list(p1,p2))
(2)其它类型曲线
  • 参数fun="event",表示cumulative event事件累计发生率
p3=ggsurvplot(fit,
           conf.int = TRUE,
           risk.table.col = "strata", # Change risk table color by groups
           ggtheme = theme_bw(), # Change ggplot2 theme
           palette = c("#E7B800", "#2E9FDF"),
           fun = "event")
  • 参数fun="cumhaz",表示cummulative hazard表示累计风险水平(在时刻t,事件发生的可能性)
p4=ggsurvplot(fit,
              conf.int = TRUE,
              risk.table.col = "strata", # Change risk table color by groups
              ggtheme = theme_bw(), # Change ggplot2 theme
              palette = c("#E7B800", "#2E9FDF"),
              fun = "cumhaz")
arrange_ggsurvplots(list(p3,p4))

3、Cox Proportional-Hazards Model比例风险模型

3.1 简单理解

  • 上面了解的log-rank test用于比较两个分组的生存率是否有显著差异,从而反映出这个分组因子的意义。但由于必须要分组,所以必须使用分类变量,或者将连续变量(基因表达)转为分组变量(中位数划分)。
  • 而这里的Cox Proportional-Hazards Model可以理解为回归模型。其中回归模型的响应变量Y是Hazard rate(风险因子,可以类比死亡率);而预测变量就是想验证是否会影响病人生存率的临床特征、基因表达等变量(分类变量、连续变量均可)。通过P值反映变量是否有显著影响;通过变量的系数coefficient表征如何影响Hazard rate,更常用的形式为:Hazard ratio(HR)=exp(coef);如果HR<1(coef<0),则表明该变量越大,风险越低,生存率越高;HR>1(coef>0),则表明该变量越大,风险越高,生存率越小。

3.2 单变量Cox回归

res.cox <- coxph(Surv(time, status) ~ sex, data = lung)
res.cox
# Call:
#   coxph(formula = Surv(time, status) ~ sex, data = lung)
# 
# coef exp(coef) se(coef)      z       p
# sex -0.5310    0.5880   0.1672 -3.176 0.00149
# 
# Likelihood ratio test=10.63  on 1 df, p=0.001111
# n= 228, number of events= 165
exp(res.cox$coefficients)
# sex 
# 0.5880028
  • 对多个变量分别进行单变量Cox回归
#分别回归
covariates <- c("age", "sex", "ph.karno", "ph.ecog", "wt.loss")
univ_formulas <- sapply(covariates,
function(x) as.formula(paste('Surv(time, status)~', x)))
univ_models <- lapply( univ_formulas, function(x){coxph(x, data = lung)})

#提取各个变量的回归结果
univ_results <- lapply(univ_models,
                       function(x){
                         x <- summary(x)
                         p.value<-signif(x$wald["pvalue"], digits=2)
                         wald.test<-signif(x$wald["test"], digits=2)
                         beta<-signif(x$coef[1], digits=2);#coeficient beta
                         HR <-signif(x$coef[2], digits=2);#exp(beta)
                         HR.confint.lower <- signif(x$conf.int[,"lower .95"], 2)
                         HR.confint.upper <- signif(x$conf.int[,"upper .95"],2)
                         HR <- paste0(HR, " (",
                                      HR.confint.lower, "-", HR.confint.upper, ")")
                         res<-c(beta, HR, wald.test, p.value)
                         names(res)<-c("beta", "HR (95% CI for HR)", "wald.test",
                                       "p.value")
                         return(res)
                         #return(exp(cbind(coef(x),confint(x))))
                       })
res <- t(as.data.frame(univ_results, check.names = FALSE))
as.data.frame(res)
#             beta HR (95% CI for HR) wald.test p.value
# age       0.019            1 (1-1)       4.1   0.042
# sex       -0.53   0.59 (0.42-0.82)        10  0.0015
# ph.karno -0.016      0.98 (0.97-1)       7.9   0.005
# ph.ecog    0.48        1.6 (1.3-2)        18 2.7e-05
# wt.loss  0.0013         1 (0.99-1)      0.05    0.83

3.3 多变量Cox回归(多元回归)

res.cox <- coxph(Surv(time, status) ~ age + sex + ph.ecog, data = lung)
summary(res.cox)
# Call:
#   coxph(formula = Surv(time, status) ~ age + sex + ph.ecog, data = lung)
# 
# n= 227, number of events= 164 
# (因为不存在,1个观察量被删除了)
# 
# coef exp(coef)  se(coef)      z Pr(>|z|)    
# age      0.011067  1.011128  0.009267  1.194 0.232416    
# sex     -0.552612  0.575445  0.167739 -3.294 0.000986 ***
#   ph.ecog  0.463728  1.589991  0.113577  4.083 4.45e-05 ***
#   ---
#   Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
# 
# exp(coef) exp(-coef) lower .95 upper .95
# age        1.0111     0.9890    0.9929    1.0297
# sex        0.5754     1.7378    0.4142    0.7994
# ph.ecog    1.5900     0.6289    1.2727    1.9864
# 
# Concordance= 0.637  (se = 0.025 )
# Likelihood ratio test= 30.5  on 3 df,   p=1e-06
# Wald test            = 29.93  on 3 df,   p=1e-06
# Score (logrank) test = 30.5  on 3 df,   p=1e-06
ggforest(res.cox, data = lung, 
         main = "Hazard ratio",
         cpositions = c(0.10, 0.22, 0.4),
         fontsize = 1.0) 

3.4 注意点

(1)如果多变量cox回归结果证明多个变量均影响Hazard rate,那么当想要观察其中一个变量所造成分组的生存率差异(KM plot)时,需要其它变量的值拉到统一水平(均值)
(2)Cox回归一个重要的假设,就是变量对于hazard rate的影响不随时间的变化而变化(等比例),可通过统计方法检验变量数据是否适合Cox回归分析。详见 http://www.sthda.com/english/wiki/cox-model-assumptions

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容