Skip to content

Medical AI · Machine learning · Lesson 5 of 9

Survival analysis: time-to-event models

Reviewed by Dr. Ram Kumar G, MBBS, MD (Internal Medicine), MPH (USA), on · Last updated

  • Intermediate
  • Technical focus
  • About 35 minutes
  • Includes exercise

Before this lesson: A logistic-regression risk score

In short

Survival analysis models the time until an event, such as kidney failure, while correctly handling patients whose follow-up ended before any event (censoring). Simply counting events among everyone underestimates risk because some patients have not been followed long enough. Kaplan-Meier curves estimate risk over time, Cox models relate risk to patient characteristics, and both can give a predicted risk at a chosen time, such as five years.

You will learn to

  • Explain censoring and why it makes simple event counts misleading
  • Estimate cumulative risk with a Kaplan-Meier curve
  • Fit and interpret a Cox proportional-hazards model and its hazard ratios
  • Recognise competing risks and when the standard methods overestimate risk

Why time matters

Many clinical outcomes unfold over years: kidney failure, heart attack, death. Patients join a study at different times and are followed for different lengths of time; some move away, some reach the end of the study period. For them, we know only that the event had not happened by their last contact. This is called censoring.

A logistic-regression model of 'event within five years' must either drop censored patients (losing information and biasing results) or treat them as event-free (underestimating risk). Survival methods use each patient's follow-up for as long as it lasts.

TermMeaning
EventThe outcome of interest, such as kidney failure
CensoringFollow-up ended before the event; the event time is known only to be later
Survival function S(t)Probability of remaining event-free beyond time t
HazardThe instantaneous rate of the event at time t among those still at risk
Hazard ratioHow much a characteristic multiplies the hazard, assumed constant over time in a Cox model

A synthetic kidney-failure cohort

The simulation follows 3,000 synthetic adults with diabetes for between one and eight years. The predictors and their effects are invented for teaching; they are loosely inspired by known kidney risk factors, not taken from a real model.

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(21)
n = 3000
df = pd.DataFrame({
    "age": rng.integers(40, 80, n),
    "egfr": rng.normal(60, 18, n).clip(15, 120).round(),   # mL/min/1.73m²
    "uacr_high": rng.binomial(1, 0.35, n),                  # albuminuria
    "sglt2i": rng.binomial(1, 0.4, n),
})
hazard = 0.02 * np.exp(0.02 * (df.age - 60) - 0.04 * (df.egfr - 60)
                       + 0.8 * df.uacr_high - 0.3 * df.sglt2i)
event_time = rng.exponential(1 / hazard)
censor_time = rng.uniform(1, 8, n)           # end of follow-up varies by patient
df["years"] = np.minimum(event_time, censor_time).round(2)
df["kidney_failure"] = (event_time <= censor_time).astype(int)
print(f"{df.kidney_failure.sum()} events among {n} patients; "
      f"{(1 - df.kidney_failure.mean()):.0%} censored")

# 368 events among 3000 patients; 88% censored
Synthetic time to kidney failure with censoring at varying follow-up times.

Kaplan-Meier: risk over time

The Kaplan-Meier estimator steps through time. At each event, it reduces the survival estimate by the proportion of people still at risk who had the event. Censored patients contribute for as long as they were followed and then leave the risk set.

python
from lifelines import KaplanMeierFitter

naive = df[df.years <= 5].kidney_failure.sum() / n
print(f"Naive 'events / everyone' by 5 years: {naive:.1%}")

kmf = KaplanMeierFitter().fit(df.years, df.kidney_failure)
print(f"Kaplan-Meier risk by 5 years:        {1 - kmf.predict(5):.1%}")

# Naive 'events / everyone' by 5 years: 10.9%
# Kaplan-Meier risk by 5 years:        13.8%
Continuing from the previous block. Install lifelines first if needed: pip install lifelines
  • The naive count treats everyone followed for less than five years as if they were event-free for the full five, so it underestimates risk (10.9% versus 13.8%).
  • kmf.plot_survival_function() draws the curve with a confidence band; plotting groups separately (for example by albuminuria) is a common first look at the data.

Cox proportional-hazards regression

The Cox model relates the hazard to patient characteristics without assuming any particular shape for the baseline risk over time. Each exponentiated coefficient is a hazard ratio.

python
from lifelines import CoxPHFitter

cph = CoxPHFitter().fit(df, duration_col="years", event_col="kidney_failure")
print(cph.summary[["exp(coef)", "exp(coef) lower 95%", "exp(coef) upper 95%"]].round(2))
print(f"\nConcordance index {cph.concordance_index_:.3f}")

#            exp(coef)  exp(coef) lower 95%  exp(coef) upper 95%
# covariate
# age             1.02                 1.01                 1.02
# egfr            0.96                 0.95                 0.96
# uacr_high       2.77                 2.25                 3.41
# sglt2i          0.72                 0.58                 0.89
#
# Concordance index 0.737
Continuing from the previous block: fit a Cox model and show hazard ratios with 95% confidence intervals.
  • The hazard ratios are close to the values built into the simulation (age 1.02, eGFR 0.96 per mL/min/1.73m², SGLT2 inhibitor 0.74). For albuminuria the estimate (2.77) is somewhat above the true 2.23, whose value falls just below the 95% interval: with one sample, an interval misses the truth about 1 time in 20.
  • The concordance index (C-index) is the survival version of AUROC: the probability that, of two patients, the one who has the event first had the higher predicted risk.
  • In real observational data, the SGLT2-inhibitor hazard ratio would not be a reliable treatment effect: who receives a medicine is confounded, as the 'Prediction or causation?' lesson explains. Treatment effects come from trials.

From hazard ratios to individual risk

Clinicians and patients need absolute risk, not hazard ratios. The Cox model can predict each patient's survival curve, and hence their risk by a chosen time.

python
patients = pd.DataFrame({"age": [55, 70], "egfr": [80, 35],
                         "uacr_high": [0, 1], "sglt2i": [0, 0]})
risk_5y = 1 - cph.predict_survival_function(patients, times=[5]).iloc[0]
print(risk_5y.round(3).to_string())

# 0    0.034
# 1    0.559
Continuing from the previous block: predicted 5-year risk for two synthetic patients.
  • A 55-year-old with good kidney function and no albuminuria has a predicted 3.4% five-year risk in this synthetic model; a 70-year-old with eGFR 35 and albuminuria has 55.9%.
  • Real, validated tools exist for this task, for example the Kidney Failure Risk Equation, which uses age, sex, eGFR and albuminuria. Teaching models like this one must not be used for patient care.

Competing risks and other cautions

Many people with advanced kidney disease die before reaching kidney failure. Death is a competing risk: it prevents the event of interest from ever happening. Kaplan-Meier treats such patients as if they could still have the event later, which overestimates the real-world risk of kidney failure. The Aalen-Johansen estimator (cumulative incidence) and Fine-Gray models handle competing risks.

  • Check the proportional-hazards assumption (lifelines: cph.check_assumptions(df)); effects that change over time need other approaches.
  • Censoring must be unrelated to the risk of the event. If sicker patients are lost to follow-up more often, estimates are biased.
  • Machine-learning survival methods exist, such as random survival forests and gradient-boosted survival models (for example in scikit-survival), and are evaluated with the C-index and time-dependent calibration.

When to use which method

  • Fixed, short follow-up that everyone completes (for example 30-day readmission): logistic regression is fine.
  • Variable follow-up or long time horizons: use survival methods.
  • A competing event that is common, such as death in older or very ill populations: use competing-risk methods.

Exercise

Analyse a synthetic kidney-failure cohort with survival methods, and see how censoring and competing risks change the answer.

Data: Synthetic cohort generated in the notebook (no real patient data) (Generated in the notebook)

  1. Reproduce the naive and Kaplan-Meier five-year risks, and plot Kaplan-Meier curves by albuminuria status.
  2. Fit the Cox model, check the proportional-hazards assumption, and report hazard ratios with confidence intervals.
  3. Add a competing risk of death to the simulation and compare 1 − Kaplan-Meier with the Aalen-Johansen cumulative incidence of kidney failure.
  4. Calculate the C-index on a held-out test set and draw a calibration plot of predicted versus observed five-year risk.
  5. Fit a random survival forest (scikit-survival) and compare its C-index with the Cox model.

The runnable notebook and worked solution will be published soon. You can work through the steps with the code on this page in the meantime.

Key takeaways

  • Censoring makes simple event counts underestimate risk; survival methods use each patient's follow-up.
  • Kaplan-Meier estimates risk over time; Cox models relate the hazard to characteristics.
  • Report absolute risk at a chosen time, not only hazard ratios.
  • Common competing events, such as death, need competing-risk methods.

Terms used: C-index (concordance index), Censoring, Competing risks, Hazard ratio

Related: A logistic-regression risk score, Prediction or causation?, Diabetic kidney disease

FAQ

What is censoring?

When a patient's follow-up ends before the event has happened, for example because the study ended or they moved away. We know only that their event time is later than their last follow-up.

What is a hazard ratio?

The factor by which a characteristic multiplies the event rate at any time. A hazard ratio of 2 means the event rate is twice as high, assumed constant over follow-up in a Cox model.

Can I use logistic regression instead of survival analysis?

Only when every patient is followed for the full time window or has the event, such as 30-day outcomes with complete follow-up. Otherwise censoring biases the results.

What is the C-index?

A measure of discrimination for survival models: the probability that the patient who has the event earlier was given the higher risk. It is the survival equivalent of AUROC.

What is a competing risk?

An event that prevents the outcome of interest from occurring, such as death before kidney failure. Ignoring it overestimates the risk of the outcome of interest.

Educational content only. This material teaches methods; it is not clinical advice. Code, models and examples here are not validated medical devices and must not be used to make decisions about real patients without proper clinical validation, governance and any required regulatory approval. Examples use synthetic or openly licensed de-identified data only. Never paste identifiable patient information into public AI tools. Mentions of models, tools or vendors are for identification, not endorsement.