iBetter Books
수정

의료 데이터 분석 (생존 분석 기초)

임상 연구에서 자주 묻는 질문이 있습니다. "이 치료를 받은 환자는 얼마나 오래 생존했는가. 다른 치료와 비교하면 어느 쪽이 더 효과적인가." 이런 질문에 답하는 도구가 생존 분석입니다.

생존 분석의 특별한 점은 "검열(censoring)"을 다룬다는 것입니다. 연구 종료 시점까지 사망하지 않은 환자, 연구 도중 탈락한 환자처럼 "아직 결과가 나오지 않은" 데이터가 섞여 있습니다. 이 불완전한 정보를 버리지 않고 올바르게 활용하는 것이 생존 분석의 핵심입니다.

분석 목표와 가설 설정

R에 내장된 lung 데이터셋을 사용합니다. 폐암 환자 228명의 생존 데이터로, ECOG 점수와 체중 감소 등의 임상 정보가 포함되어 있습니다.

연구 질문

  • 폐암 환자의 중앙 생존시간은 얼마인가.
  • 성별(남/여)에 따라 생존율에 유의미한 차이가 있는가.
  • ECOG 기능 점수에 따라 생존 곡선이 다른가.

가설

  • H1: 남성과 여성의 생존 분포가 동일하다 (귀무가설).
  • H2: ECOG 점수가 낮을수록(기능이 좋을수록) 더 오래 생존한다 (연구 가설).

데이터 탐색

# install.packages("survival")
library(survival)
library(ggplot2)
library(dplyr)

# lung 데이터 불러오기
data(lung)

# 구조 확인
str(lung)

주요 변수를 파악합니다.

변수 설명
time 생존 기간 (일)
status 결과 (1 = 검열, 2 = 사망)
sex 성별 (1 = 남, 2 = 여)
ph.ecog ECOG 기능 점수 (0~4, 낮을수록 기능 좋음)
age 나이
wt.loss 체중 감소량 (파운드)
# 기본 현황
cat("전체 환자 수:", nrow(lung), "\n")
cat("사망 환자 수:", sum(lung$status == 2, na.rm = TRUE), "\n")
cat("검열 환자 수:", sum(lung$status == 1, na.rm = TRUE), "\n")
cat("생존 기간 범위:", range(lung$time, na.rm = TRUE), "일\n")

# 성별 분포
table(lung$sex)

# 결측치 확인
colSums(is.na(lung))

Surv() 객체 만들기

생존 분석의 첫 단계는 Surv() 객체를 만드는 것입니다. 시간과 결과(사망 여부)를 묶어서 하나의 반응 변수로 취급합니다.

# Surv 객체 생성
# status == 2이면 사건(사망) 발생, 나머지는 검열
surv_obj <- Surv(time = lung$time, event = lung$status == 2)

# 처음 10개 값 확인
# + 기호가 붙은 값은 검열(아직 살아 있음)
head(surv_obj, 10)

출력에서 숫자 뒤에 +가 붙은 것은 검열된 관측치입니다. 연구 종료 시점까지 사망이 확인되지 않은 환자들입니다.

Kaplan-Meier 생존 곡선

Kaplan-Meier 방법은 시점마다 "이 순간까지 살아남을 확률"을 계단식으로 계산합니다. 수식은 복잡하지만 R에서는 survfit() 한 줄로 해결됩니다.

# 전체 환자 KM 곡선
km_overall <- survfit(surv_obj ~ 1, data = lung)
summary(km_overall)

# 주요 요약 통계
print(km_overall)

median이 중앙 생존시간입니다. 환자의 50%가 이 기간 내에 사망했다는 뜻입니다.

# 성별에 따른 KM 곡선 비교
lung$sex_label <- factor(lung$sex, labels = c("남성", "여성"))
km_sex <- survfit(surv_obj ~ sex_label, data = lung)
print(km_sex)

# 시각화
# install.packages("survminer")
library(survminer)

ggsurvplot(
  km_sex,
  data          = lung,
  conf.int      = TRUE,         # 신뢰구간 표시
  pval          = TRUE,         # 로그순위 검정 p값 자동 표시
  risk.table    = TRUE,         # 위험군 수 표 추가
  legend.labs   = c("남성", "여성"),
  palette       = c("#2E9FDF", "#E7B800"),
  xlab          = "생존 시간 (일)",
  ylab          = "생존 확률",
  title         = "폐암 환자 성별 생존 곡선 (Kaplan-Meier)",
  ggtheme       = theme_minimal(base_family = "AppleGothic")
)

생존 곡선에서 여성 곡선이 남성 곡선보다 높은 위치에 있다면, 같은 시점에서 여성의 생존율이 더 높다는 뜻입니다.

로그순위 검정 — survdiff()

두 생존 곡선이 시각적으로 달라 보여도, 통계적으로 유의미한 차이인지 검정이 필요합니다. 로그순위 검정(log-rank test)이 이 역할을 합니다.

# 성별 로그순위 검정
logrank_sex <- survdiff(surv_obj ~ sex_label, data = lung)
print(logrank_sex)

# p값 추출
p_value <- 1 - pchisq(logrank_sex$chisq, df = length(logrank_sex$n) - 1)
cat("로그순위 검정 p값:", round(p_value, 4), "\n")

if (p_value < 0.05) {
  cat("결론: 성별에 따른 생존 분포에 유의미한 차이가 있습니다.\n")
} else {
  cat("결론: 성별에 따른 생존 분포에 유의미한 차이가 없습니다.\n")
}

ECOG 점수별 생존 분석

# ECOG 점수가 0, 1, 2인 환자만 분석 (3, 4는 소수)
lung_ecog <- lung %>%
  filter(ph.ecog %in% 0:2) %>%
  mutate(ecog_label = factor(ph.ecog,
    levels = 0:2,
    labels = c("ECOG 0 (정상)", "ECOG 1 (경증)", "ECOG 2 (중증)")
  ))

surv_ecog <- Surv(lung_ecog$time, lung_ecog$status == 2)
km_ecog   <- survfit(surv_ecog ~ ecog_label, data = lung_ecog)

# 중앙 생존시간 비교
print(km_ecog)

# 시각화
ggsurvplot(
  km_ecog,
  data       = lung_ecog,
  conf.int   = FALSE,
  pval       = TRUE,
  risk.table = TRUE,
  palette    = c("#00BA38", "#619CFF", "#F8766D"),
  xlab       = "생존 시간 (일)",
  ylab       = "생존 확률",
  title      = "ECOG 점수별 생존 곡선",
  ggtheme    = theme_minimal(base_family = "AppleGothic")
)

# ECOG별 로그순위 검정
survdiff(surv_ecog ~ ecog_label, data = lung_ecog)

중앙 생존시간 비교 정리

# 각 집단의 중앙 생존시간 테이블
median_table <- data.frame(
  집단   = c("전체", "남성", "여성"),
  중앙생존시간 = c(
    summary(km_overall)$table["median"],
    summary(km_sex)$table[1, "median"],
    summary(km_sex)$table[2, "median"]
  ),
  단위 = "일"
)

print(median_table)

# 1년(365일) 생존율 추정
km_365 <- summary(km_sex, times = 365)
cat("\n1년 생존율\n")
cat("남성:", round(km_365$surv[1] * 100, 1), "%\n")
cat("여성:", round(km_365$surv[2] * 100, 1), "%\n")

결과 해석과 보고서 요약

생존 분석 결과를 논문이나 보고서에 쓸 때 필수 요소는 세 가지입니다.

첫째, 중앙 생존시간과 95% 신뢰구간입니다. "폐암 환자의 중앙 생존시간은 310일(95% CI: 285~363일)이었다"처럼 씁니다.

둘째, 집단 비교 결과입니다. "여성의 중앙 생존시간은 426일로 남성 270일보다 유의미하게 길었다(로그순위 검정 p = 0.001)"처럼 통계값을 포함합니다.

셋째, Kaplan-Meier 곡선 그림입니다. 위험군 수 표(risk table)를 함께 표시하면 각 시점에서 분석에 포함된 환자 수를 독자가 파악할 수 있습니다.

# 최종 보고서용 그림
final_plot <- ggsurvplot(
  km_sex,
  data          = lung,
  conf.int      = TRUE,
  pval          = TRUE,
  pval.method   = TRUE,
  risk.table    = "abs_pct",   # 절대 수치와 비율 함께
  risk.table.col= "strata",
  legend.labs   = c("남성", "여성"),
  palette       = c("#2E9FDF", "#E7B800"),
  surv.median.line = "hv",     # 중앙 생존시간 선 표시
  xlab          = "생존 시간 (일)",
  ylab          = "생존 확률",
  title         = "성별 생존 곡선 (Kaplan-Meier)",
  ggtheme       = theme_bw(base_family = "AppleGothic")
)

print(final_plot)

정리

생존 분석은 "언제 사건이 일어났는가"를 분석합니다. Surv() 객체로 시간과 검열을 묶고, survfit()으로 Kaplan-Meier 곡선을 그리고, survdiff()로 집단 차이를 검정합니다. 이 세 함수가 생존 분석의 핵심입니다. 더 나아가면 여러 변수를 동시에 고려하는 Cox 비례위험 모형이 있지만, 그것은 고급 과정에서 다룹니다.