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 단일세포 분석

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의 status는 0/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은 위험비이지 위험률·오즈비가 아니라는 점도 기억하세요.

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번 함정.) - ❌
ggsurvplot에data=빠뜨림 — 최신 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()).

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
status는 1=검열·2=사망; HR은exp(coef); PH는 p 클수록 좋음. - ✅ 결과 — 여성 생존 양호(중앙 426 vs 270일, HR≈0.58, log-rank p=0.001).
9. 다음 학습 추천
- 📊 DESeq2로 벌크 RNA-seq 차등발현 — 같은 R 실전, 발현 데이터 분석 (실전)
- 🧬 Seurat 단일세포 분석 — R로 세포 하나하나 분석 (실전)
- 💉 췌장암 mRNA 백신 autogene cevumeran 리뷰 — 이 생존곡선이 실제로 쓰인 임상 논문 (비교)
References
- Therneau, T. M. survival: Survival Analysis (R package, base-R recommended). cran.r-project.org.
- Kassambara, A., et al. survminer: Drawing Survival Curves using 'ggplot2' (CRAN; v0.5.2 — ggplot2 4.0 대응).
- 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.
- 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.
'Code Bench > R Snippets' 카테고리의 다른 글
| 주성분분석(PCA)으로 RNA-seq 샘플 QC하기 — vst·plotPCA·prcomp·배치효과 (R 실전) (0) | 2026.06.11 |
|---|---|
| ComplexHeatmap으로 발현 히트맵 그리기 — z-score부터 주석·분할까지 (R 실전) (0) | 2026.06.10 |
| Seurat로 시작하는 단일세포 RNA-seq 분석 — pbmc3k로 QC부터 셀타입 주석까지 (R 실전) (0) | 2026.06.05 |
| ClusterProfiler로 GO·KEGG·GSEA 농축분석하기 — DEG 목록에 생물학적 의미 입히기 (R 실전) (0) | 2026.06.04 |
| DESeq2로 RNA-seq 차등발현(DEG) 분석하기 — 카운트부터 볼케이노까지 (R 실전) (0) | 2026.06.02 |