R로 생존 분석하기 — 카플란-마이어 곡선부터 Cox 회귀까지 (survival 패키지 실전)

TL;DR — 신약 임상시험 논문에서 가장 자주 보는 그래프가 계단 모양 생존곡선입니다. "치료군이 더 오래 살았나?"를 보는 생존 분석(survival analysis)으로, 핵심은 '사건이 아직 안 일어난'(검열) 환자도 버리지 않고 쓴다는 점입니다.

이 글은 R 내장 데이터 lung (폐암 환자 228명)으로, 카플란-마이어 곡선 → log-rank 검정 → Cox 회귀(위험비) → 비례위험 가정 점검까지 복붙하면 돌아가는 코드로 따라갑니다. 패키지는 survival (R 기본 포함)과 survminer (ggplot2 그래프) 둘이면 됩니다.

그리고 입문자가 100% 걸리는 첫 함정lung의 status는 0/1이 아니라 1=검열·2=사망입니다. 이걸 모르면 결과가 뒤집힙니다. 마지막으로 가장 중요한 한 가지 — 위험비(HR)는 'coef'가 아니라 exp(coef)이고, 시간 내내 일정하다고 가정(비례위험)한 값이라는 것.

🔗 관련 글  ·  같은 R 분석 실전: DESeq2로 벌크 RNA-seq 차등발현  ·  Seurat 단일세포 분석
그림 1. 카플란-마이어 곡선 — 여성(sex=2)이 남성보다 생존 양호. 검열은 + 표시, 점선은 중앙값(남 270·여 426일), log-rank p=0.001.

0. 준비물 — 패키지와 데이터

survival은 R에 기본 포함된 패키지라 설치가 사실상 필요 없고, survminer만 CRAN에서 받으면 됩니다.

install.packages(c("survival", "survminer"))   # survival은 기본 포함, 설치는 무해
library(survival)
library(survminer)

data(lung, package = "survival")   # ← package= 를 붙이면 안전
str(lung)

데이터 lung은 NCCTG 폐암 환자 228명의 생존 기록입니다(Loprinzi 1994). 주요 열은 — time (생존일수), status (상태), sex (1=남, 2=여), age, ph.ecog (활동도 0~4, 높을수록 나쁨) 등입니다.

1. 핵심 개념 — 사건, 검열, 그리고 세 도구

  • 사건(event)·검열(censoring) — 관심 사건(사망 등)이 일어난 환자와, 추적이 끊겨 '적어도 그 시점까지는 살아 있었다'까지만 아는 환자(우측 검열)를 구분합니다. 생존 분석의 묘는 검열된 환자도 정보로 쓴다는 데 있습니다.
  • 카플란-마이어(Kaplan-Meier) — 비모수 생존곡선(계단 함수). 사건마다 뚝 떨어지고, 검열은 + 표시로 남습니다. 중앙 생존(median)은 곡선이 0.5와 만나는 시점.
  • log-rank 검정 — 두 군의 곡선 전체가 같은지 비교하는 p값.
  • Cox 비례위험 회귀(Cox PH) — 공변량의 효과를 위험비(HR)로 냅니다. HR>1 나쁨, HR<1 좋음, HR=1 무효.

2. ⚠️ 첫 함정 — lung의 status는 1/2다

가장 흔한 실수부터 못 박고 갑니다. lung의 status0/1이 아니라 1=검열, 2=사망입니다(옛 펀치카드 시절 0을 결측과 못 구분하던 관습의 흔적).

Surv()는 숫자 1/2를 받으면 가장 큰 값(2)을 '사건'으로 자동 해석하지만, 헷갈리지 않게 명시적으로 쓰는 걸 권합니다.

# 안전한 명시 형태 — TRUE(=사망)가 사건
Surv(lung$time, lung$status == 2)
⚠️ 다른 데이터를 1=사망으로 알고 1/2를 그대로 넣으면 생존·사망이 뒤집혀 결과가 정반대가 됩니다. 새 데이터를 받으면 table(status)로 코딩부터 확인하세요.

3. 카플란-마이어 곡선 그리기

survfit()으로 군별 생존곡선을 적합하고, ggsurvplot()으로 그립니다.

fit <- survfit(Surv(time, status == 2) ~ sex, data = lung)
print(fit)   # 군별 n·사건수·중앙생존(+95% CI)

ggsurvplot(
  fit, data = lung,              # data= 는 필수
  pval = TRUE,                   # log-rank p를 그래프에
  conf.int = TRUE,               # 신뢰구간 밴드
  risk.table = TRUE,             # 시점별 잔존 수 표
  surv.median.line = "hv",       # 중앙생존 점선
  legend.labs = c("남성", "여성"),
  palette = c("#2E9FDF", "#E7B800"),
  xlab = "시간(일)", ylab = "생존 확률"
)

print(fit) 결과 — 중앙 생존은 남성 270일 / 여성 426일로, 여성이 더 오래 살았습니다.

        n events median 0.95LCL 0.95UCL
sex=1 138    112    270     212     310
sex=2  90     53    426     348     550

4. log-rank 검정 — 곡선 차이가 우연인가

survdiff(Surv(time, status == 2) ~ sex, data = lung)
#  Chisq= 10.3  on 1 degrees of freedom, p= 0.001

χ² = 10.3, p = 0.001 — 두 곡선의 차이는 통계적으로 유의합니다. 단, log-rank 검정은 시점 전체를 비교하므로 "어느 시점에 차이가 났나"는 곡선으로 따로 읽어야 합니다.

5. Cox 회귀 — 위험비(HR)로 효과 크기 보기

여러 변수를 한꺼번에 보정해 위험비를 내는 게 Cox 회귀입니다.

cox <- coxph(Surv(time, status == 2) ~ sex + age + ph.ecog, data = lung)
summary(cox)
             coef exp(coef)  se(coef)      z Pr(>|z|)
sex     -0.552612  0.575445  0.167739 -3.294 0.000986 ***
age      0.011067  1.011128  0.009267  1.194 0.232416
ph.ecog  0.463728  1.589991  0.113577  4.083 4.45e-05 ***

읽는 법 — exp(coef)가 HR입니다(coef는 로그 스케일).

  • sex HR ≈ 0.58 — 여성(sex=2)은 남성 대비 사망 위험이 약 42% 낮음(1−0.58). 95% CI 0.41~0.80, 유의.
  • ph.ecog HR ≈ 1.59 — 활동도가 1 나빠질수록 위험 약 1.6배. 유의.
  • age HR ≈ 1.01 — 나이는 이 모델에서 무의미(p≈0.23).

포레스트 플롯으로 한눈에:

ggforest(cox, data = lung)
⚠️ Cox는 결측을 가진 행을 통째로 제외합니다(ph.ecog에 NA 1개 → "1 observation deleted"). HR은 위험비이지 위험률·오즈비가 아니라는 점도 기억하세요.
그림 2. Cox 포레스트 플롯 — 위험비(HR). sex 0.58 (여성 위험↓), ph.ecog 1.59 (나쁠수록 위험↑), age 1.01 (무의미). CI가 1을 지나면 비유의.

6. 비례위험(PH) 가정 점검 — 빼먹으면 안 되는 단계

Cox는 위험비가 시간 내내 일정하다(비례위험)고 가정합니다. 이걸 안 지키면 HR 해석이 무너집니다. cox.zph()로 검정합니다.

zph <- cox.zph(cox)
print(zph)
ggcoxzph(zph)     # 샨펠트 잔차 플롯
        chisq df    p
sex     2.305  1 0.13
age     0.188  1 0.66
ph.ecog 2.054  1 0.15
GLOBAL  4.464  3 0.22
💡 여기선 p값이 클수록 좋습니다. GLOBAL p=0.22 > 0.05라 비례위험 가정을 만족합니다. 만약 작은 p (위반)가 나오면 — 층화(strata), 시간상호작용 추가, 시간가변 공변량 등으로 대응합니다.

7. 흔한 실수 & 버전 주의

  • status를 0/1로 착각 — lung은 1/2. 새 데이터는 table(status) 먼저. (이 글의 1번 함정.)
  • ggsurvplotdata= 빠뜨림 — 최신 survminer는 필수.
  • HR을 coef로 읽기 — HR은 exp(coef). 위험'비'(상대 위험률)이지 오즈비가 아님.
  • PH 가정 무시cox.zph 항상.
  • 불멸 시간 편향(immortal time bias) — 기저시점 이후에 정해지는 변수(예: '치료 반응함')를 예측인자로 넣으면 생존이 인위적으로 길어 보임.
  • ⚠️ 버전survminer가 ggplot2 4.0과 충돌해 conf.int=TRUE에서 "Aesthetics can not vary along a ribbon" 오류가 났는데, survminer 0.5.2에서 수정됐습니다. packageVersion("survminer")0.5.2 이상인지 확인하세요(낮으면 update.packages()).
그림 3. 해석 가이드 — HR<1 좋음·=1 무효·>1 나쁨; PH는 p 클수록 좋음; lung status는 1=검열·2=사망(0/1 아님).

8. 전체 스크립트 & 핵심 정리

library(survival); library(survminer)
data(lung, package = "survival")

# 1) 카플란-마이어
fit <- survfit(Surv(time, status == 2) ~ sex, data = lung)
print(fit)                                   # 중앙 생존: 남 270, 여 426일
ggsurvplot(fit, data = lung, pval = TRUE, conf.int = TRUE, risk.table = TRUE,
           surv.median.line = "hv", legend.labs = c("남성","여성"),
           xlab = "시간(일)", ylab = "생존 확률")

# 2) log-rank 검정  → Chisq 10.3, p = 0.001
survdiff(Surv(time, status == 2) ~ sex, data = lung)

# 3) Cox 회귀  → sex HR 0.58, ph.ecog HR 1.59
cox <- coxph(Surv(time, status == 2) ~ sex + age + ph.ecog, data = lung)
summary(cox); ggforest(cox, data = lung)

# 4) 비례위험 점검  → GLOBAL p = 0.22 (만족)
cox.zph(cox); ggcoxzph(cox.zph(cox))
  • 생존 분석 = 사건까지의 '시간' + 검열을 함께 쓰는 분석.
  • 카플란-마이어(곡선) → log-rank (군 비교 p) → Cox (위험비 HR) → cox.zph (PH 가정) 순서.
  • ⚠️ 함정 — lung status1=검열·2=사망; HR은 exp(coef); PH는 p 클수록 좋음.
  • ✅ 결과 — 여성 생존 양호(중앙 426 vs 270일, HR≈0.58, log-rank p=0.001).

9. 다음 학습 추천

References

  1. Therneau, T. M. survival: Survival Analysis (R package, base-R recommended). cran.r-project.org.
  2. Kassambara, A., et al. survminer: Drawing Survival Curves using 'ggplot2' (CRAN; v0.5.2 — ggplot2 4.0 대응).
  3. Loprinzi, C. L., et al. (1994). Prospective evaluation of prognostic variables from patient-completed questionnaires (NCCTG; lung 데이터). Journal of Clinical Oncology, 12(3), 601–607.
  4. Cox, D. R. (1972). Regression models and life-tables. Journal of the Royal Statistical Society B, 34(2), 187–220. (Cox 비례위험 모델 원전)

Pipette & Pipeline · A bio portfolio journal — bridging research, data, and industry.

이 글을 쓴 사람 Yumingming

생명융합공학과 박사과정.
Microbiome · Cosmetics · RNA Therapeutics · Bioinformatics를 공부하며,
실험(Wet Lab)과 데이터(Dry Lab)를 잇는 글을 논문(article) 기반으로 씁니다.

About · 더 알아보기 →

⚕️ 이 글은 학습·정보 제공 목적이며, 의학적 진단·치료·조언을 대체하지 않습니다. 건강·질병·치료에 관한 결정은 반드시 의사 등 전문가와 상의하세요. 자세한 내용은 면책조항을 참고해 주세요.