생존 분석에서 Kaplan-Meier와 Cox 비례위험 모델 활용법

검열 데이터를 포함한 생존 분석의 핵심 개념과 Kaplan-Meier, Cox 비례위험 모델, 시간 의존 공변량 적용 구조를 정리한다.

2026-08-14 · 최초 발행 2024-04-29

사건이 끝나지 않은 관측값까지 분석에 포함하는 방식

생존 분석은 임상시험의 치료 효과, 구독 서비스의 이탈, 산업 설비의 고장처럼 사건이 일어날 때까지 걸린 시간을 다룬다. 관측이 끝날 때까지 사건이 발생하지 않은 대상도 분석에서 제외하지 않는다는 점이 핵심이다. 이런 관측값을 검열(censoring) 데이터라고 한다.

생존함수는 S(t)=P(T>t)로 표현하며, 시간 t까지 사건 없이 남아 있을 확률을 뜻한다. 위험함수 h(t)는 단위시간당 사건 발생율을 나타낸다. 그룹 간 상대위험은 위험비(Hazard Ratio, HR)로 비교한다.

관측 종료 이전에 사건이 발생하지 않은 경우는 우측 검열이 일반적이다. 좌측 검열과 구간 검열도 가능하며, 분석에서는 비정보성 검열을 가정한다. 분석 방법은 목적에 따라 나뉜다. Kaplan-Meier는 비모수적으로 생존함수를 추정하고, Cox 비례위험 모델은 기준위험을 별도로 규정하지 않는 준모수 모델이다. 시간 의존 공변량은 공변량의 변화 자체를 모델에 반영한다.

분석 전에 정리할 데이터 조건

기본 데이터에는 관측기간을 담는 duration, 사건 여부를 나타내는 event, 그리고 공변량이 필요하다. event는 사건이면 1, 검열이면 0으로 기록한다.

시간 의존 공변량을 다룰 때는 각 대상의 관측 시간을 여러 구간으로 나눈 start–stop–event 카운팅 프로세스 형식을 사용한다. 전처리 단계에서는 검열 비율, 결측치 처리 전략(complete-case/다중대치), 이상치, 동일 시점 사건의 동률(ties) 처리 기준을 함께 정한다.

생존곡선과 집단 차이의 해석

Kaplan-Meier 추정기는 사건이 발생한 시점마다 계단형 생존곡선을 만든다. 신뢰구간은 Greenwood 공식으로 산출하며, 집단 간 생존곡선 차이는 로그랭크(log-rank) 검정으로 확인한다.

다만 Kaplan-Meier만으로는 공변량을 통제할 수 없다. 연령이나 성별처럼 결과에 영향을 줄 수 있는 변수를 조정해야 한다면 Cox 모델을 함께 사용한다.

Cox 모델이 요구하는 가정

Cox 비례위험(Cox Proportional Hazards) 모델은 부분우도(partial likelihood)를 이용해 위험비를 추정한다. 기준위험(baseline hazard)을 특정 분포로 가정하지 않아도 된다는 특징이 있다. 동률을 처리할 때는 Efron, Breslow, Exact 방식 중 하나를 선택한다.

이 모델에서는 시간에 따라 위험비가 일정하다는 비례위험 가정을 검토해야 한다. Schoenfeld 잔차나 변수×log(time) 상호작용으로 위반 여부를 진단할 수 있다. 가정이 맞지 않으면 층화(stratified Cox)나 시간가변 계수를 고려한다.

시간에 따라 바뀌는 노출을 모델에 넣는 법

복용량, 서비스 사용량, 캠페인 노출, 정책 변경처럼 공변량이 관측 도중 바뀌는 경우에는 시간 의존 공변량이 필요하다. 이러한 변수는 내부(internal) 변수와 외부(external) 변수로 구분한다.

구현 시에는 관측 구간을 start–stop–event 형식으로 분할한다. 노출과 사건의 시간 순서를 명확히 기록해야 하며, 지연(lag) 설계를 통해 무불사시간(immortal time) 편향을 피해야 한다. 필요하면 계층화나 시간가변 계수를 적용한다.

임상·구독·설비 운영에서의 적용

의료 임상에서는 Kaplan-Meier로 치료군별 생존곡선과 로그랭크 검정 결과를 확인한 뒤, Cox 모델로 연령과 성별을 조정한 위험비를 추정한다. 비례위험 가정이 깨지면 치료군×log(time) 또는 층화를 적용할 수 있다. 누적용량, 치료 변경, 바이오마커 변화는 시간 의존 공변량으로 반영하며, 지연효과는 lag로 고려한다.

구독 비즈니스에서는 가입부터 이탈까지의 시간을 생존곡선으로 나타내고 코호트 차이를 살핀다. 사용량과 가격 민감도를 조정한 위험비를 Cox 모델로 추정할 수 있다. 최근 30일 활동지표, 접속 빈도, 캠페인 노출 여부는 구간별로 반영하며 고위험 구간은 실시간 알림 트리거에 연결할 수 있다.

산업 설비에서는 장비 타입별 Kaplan-Meier 추정으로 MTBF를 구하고, 온도·진동·부하를 조정한 위험비를 Cox 모델로 추정한다. 고장 모드별 층화도 가능하다. 센서 스트리밍 지표의 구간 평균이나 스파이크 카운트를 시간 의존 공변량으로 넣으면 제어 한계 초과 시 위험 급증 구간을 찾는 데 활용할 수 있다.

분석 흐름

충족위반입력: 사건시각, 검열, 공변량전처리: 결측·검열 확인,start-stop 변환Kaplan-Meier 추정로그랭크 검정: 그룹 차이 평가Cox PH 적합(고정 공변량)PH 가정 검증(Schoenfeld 잔차)해석 HR 리포트대안: 시간 의존공변량/층화/시간가변 계수검증: 재적합·교차검증출력: 생존곡선, HR, 위험도스코어, 정책 의사결정

방법별 선택 기준

방법 성능(추정정확도) 확장성(고차원) 일관성(PH 위반 민감도) 안정성(검열 많음) 운영 편의
Kaplan-Meier 중간 낮음 높음 높음 높음
Cox PH(고정) 높음 중간 중간 중간 중간
Cox PH(시간 의존) 높음 중간 높음 중간 낮음

주: 상대적 비교 기준, 데이터 특성에 따라 상이.

Python으로 적합하기

다음 예시는 Python 3.10+, lifelines 0.27+, pandas 2.x 환경에서 가정 데이터를 생성해 적합한다.

Kaplan-Meier와 고정 공변량 Cox PH

# pip install lifelines==0.27.8 pandas==2.2.2 numpy==1.26.4
import numpy as np, pandas as pd
from lifelines import KaplanMeierFitter, CoxPHFitter
from lifelines.statistics import logrank_test

np.random.seed(42)
n = 400
df = pd.DataFrame({
    "duration": np.random.exponential(scale=12, size=n).astype(float),
    "event": np.random.binomial(1, 0.7, size=n).astype(int),
    "treatment": np.random.binomial(1, 0.5, size=n).astype(int),
    "age": np.random.normal(55, 8, size=n).round(1)
})
# 처리군 HR<1 가정: 위험 감소 효과를 모사
df["duration"] *= np.where(df["treatment"]==1, 1.2, 0.9)

# Kaplan-Meier
km = KaplanMeierFitter()
km.fit(df["duration"], event_observed=df["event"])
print("KM 생존함수(head):")
print(km.survival_function_.head())

# 그룹 간 로그랭크 검정
res = logrank_test(
    df.loc[df.treatment==1,"duration"],
    df.loc[df.treatment==0,"duration"],
    event_observed_A=df.loc[df.treatment==1,"event"],
    event_observed_B=df.loc[df.treatment==0,"event"]
)
print("\n로그랭크 p-value:", res.p_value)

# Cox PH
cph = CoxPHFitter()
cph.fit(df, duration_col="duration", event_col="event", formula="treatment + age")
print("\nCox 요약:")
print(cph.summary[["coef","exp(coef)","p"]])

# PH 가정 점검(텍스트 출력)
cph.check_assumptions(df, p_value_threshold=0.05, show_plots=False)

동률(ties)이 많으면 cph.fit(..., ties="efron") 지정을 권장한다. 다중공선성 경고가 나오면 변수 축소 또는 정규화가 필요하다.

시간 의존 공변량을 사용하는 CoxTimeVaryingFitter

# pip install lifelines==0.27.8
import pandas as pd, numpy as np
from lifelines import CoxTimeVaryingFitter

np.random.seed(7)
m = 150  # 개체 수
rows = []
for i in range(m):
    # 각 개인을 0-3, 3-8, 8-끝 세 구간으로 분할
    t1, t2, t3 = 3, 8, np.random.exponential(6)+8
    # 시간 의존 공변량(예: 최근 사용량 지표) 구간별 변화
    z1, z2, z3 = np.random.poisson(2), np.random.poisson(4), np.random.poisson(6)
    # 마지막 구간에서 사건 발생 여부
    event = np.random.binomial(1, 0.6)
    rows += [
        {"id": i, "start": 0,  "stop": t1, "event": 0,     "usage": z1, "group": np.random.binomial(1,0.5)},
        {"id": i, "start": t1, "stop": t2, "event": 0,     "usage": z2, "group": np.random.binomial(1,0.5)},
        {"id": i, "start": t2, "stop": t3, "event": event, "usage": z3, "group": np.random.binomial(1,0.5)},
    ]
tv = pd.DataFrame(rows)

ctv = CoxTimeVaryingFitter()
ctv.fit(tv, id_col="id", start_col="start", stop_col="stop", event_col="event",
        formula="group + usage")
print(ctv.summary[["coef","exp(coef)","p"]])

노출 시작 이전 구간을 명시하고, 노출 변수는 구간 시작 시점 기준의 이전 상태로 lag 처리해야 무불사시간 편향을 줄일 수 있다. 외생 시계열을 결합할 때는 타임스탬프 정렬과 구간 경계 스냅 규칙도 정해 둔다.

운영 지표로 연결할 때 남는 효과

위험기반 개입을 위한 조기경보 임계값 운영에서는 재입원/이탈률이 515% 상대적으로 감소할 수 있다는 사례가 보고됐다. 정책 A/B 비교에서는 로그랭크 기반 유의성 검증과 Cox 위험비 추정을 함께 사용해, 공변량 조정 대비 표준오차를 1030% 줄이는 정밀도 향상을 기대할 수 있다.

검열 데이터를 포함해 의사결정할 수 있고 편향을 줄일 수 있다는 점도 운영상 이점이다. 시간 의존 정보를 반영하면 동태적 위험 프로파일링을 구현해 운영 최적화에 연결할 수 있다.

Kaplan-Meier는 생존 구조를 비모수적으로 요약하고 로그랭크 검정으로 그룹 차이를 평가하는 출발점이 된다. Cox PH는 공변량을 조정한 위험비를 해석하기에 적합하다. 비례위험 가정이 위반되거나 노출이 변하면 시간 의존 공변량 구조, 카운팅 프로세스 형식, lag 규칙을 포함한 설계가 필요하다.

생존 분석Kaplan-MeierCox 모델검열 데이터통계