Site icon Ampersand Tutorials

Survival Analysis: Kaplan-Meier Curves and Cox Regression, Worked Through

Two descending step curves with small tick marks, representing Kaplan-Meier survival curves

A study follows patients for five years and records when each one dies, relapses or is discharged. Halfway through, some participants move away; others are still healthy when the funding ends. You know only that they survived at least until their last observation. This is censoring, and it is what makes time-to-event data a different statistical problem rather than a t-test with a stopwatch.

Survival analysis exists to use that partial information properly. Ignoring censored subjects biases survival downward; dropping them biases it upward. Neither is acceptable, and both are common in drafts that reach reviewers.

Why time-to-event data breaks ordinary statistics

Three features make standard methods fail.

If you analysed this with a plain logistic regression on ever/never, you would silently assume that everyone followed for one day and someone followed for five years carries equal information. That is why survival methods weight each event by how many people were still at risk at that moment.

Censoring, precisely

Right censoring means the event happened after the last observation, or was never observed. It is uninformative only if the reason for censoring is unrelated to prognosis — someone relocating for work is fine; someone dropping out because they felt too ill is not, and that is informative censoring, which no standard method repairs.

TypeWhat you observeConsequence
EventExact time of eventFull information
Right-censoredSurvived until time t, outcome unknown afterHandled by KM and Cox
Left-censoredEvent occurred before tNeeds specialised methods
Interval-censoredEvent between two visitsNeeds interval methods; KM is biased

Interval censoring is the one people miss. If patients are seen annually, the true event time is somewhere in a twelve-month window, and pretending it happened at the visit date biases the curve. If your data are visit-based, say so.

Kaplan-Meier: what the curve is doing

The Kaplan-Meier estimator multiplies conditional survival probabilities across every event time. At each event, survival drops by the fraction of those still at risk who experienced it. Subjects censored before that point leave the risk set without causing a drop, which is exactly the correction ordinary proportions lack.

library(survival)

fit <- survfit(Surv(time, event) ~ group, data = trial)
summary(fit)              # survival at each event time
summary(fit, times = c(12, 36, 60))   # survival at chosen timepoints

Reading it correctly takes three habits. First, the curve is a step function, not a smooth trend — it changes only at event times. Second, the number at risk falls over time, so the right-hand end of a curve is estimated from very few people and is correspondingly uncertain. Third, the curve never reaches zero unless every subject had the event.

Confidence bands and the tail

The shaded band is a pointwise confidence interval, which is not a simultaneous band: the chance that the whole curve lies inside it is lower than 95%. Near the end of follow-up the band widens dramatically because the risk set has shrunk. Truncating curves at a stated median follow-up, with numbers-at-risk printed beneath, is standard practice precisely because it prevents the misleading tail.

library(survminer)
ggsurvplot(fit, data = trial, risk.table = TRUE,
           conf.int = TRUE, xlim = c(0, 60),
           break.time.by = 12,
           legend.title = "Group")

The risk table is not decoration. A curve that separates only after month 48, when twelve people remain at risk, is a very different claim from one that separates at month 6 with 300 at risk — and the table is how a reader tells which they are looking at.

The log-rank test, and its one job

The log-rank test compares entire survival curves between groups. It asks whether the observed number of events in each group differs from what you would expect if survival were identical, summed across all event times.

survdiff(Surv(time, event) ~ group, data = trial)

It is a test of the whole curve, not of any single timepoint, and it has low power when curves cross. If one treatment is better early and worse later, the log-rank test can return a large p-value while both directions matter clinically. In that case report the curves, consider a time-varying or restricted-mean analysis, and do not claim equivalence.

Cox regression and hazard ratios

Cox proportional hazards models the hazard — the instantaneous risk of the event given survival so far — as a baseline hazard multiplied by an exponential function of the covariates. The baseline hazard is left unspecified, which is why it is called semi-parametric and why it is so widely used.

cox <- coxph(Surv(time, event) ~ treatment + age + stage, data = trial)
summary(cox)
exp(coef(cox))              # hazard ratios
exp(confint(cox))           # 95% CI

A hazard ratio of 0.72 for treatment means: at any given moment, among those still at risk, the treated group’s instantaneous risk is 72% of the control group’s. It does not mean 28% fewer events overall, and it does not mean a 28% longer survival time. Converting a hazard ratio into a difference in median survival requires the baseline hazard, which is why those two numbers rarely match.

StatementCorrect?
Treated patients had 28% lower hazard at any given timeYes
Treated patients were 28% less likely to have the eventNo — that needs absolute risks
Median survival was 28% longerNo — HR and median ratio differ
The treatment delayed events proportionallyOnly if proportional hazards holds

The proportional hazards assumption

Cox regression assumes the hazard ratio is constant over time. If treatment helps most in the first six months and not afterwards, the model averages two different realities into one number. This assumption is testable and routinely untested.

cox.zph(cox)                # Schoenfeld residuals test
plot(cox.zph(cox))          # look for a trend, not just the p-value

A significant result means the proportional hazards assumption is doubtful — but the test is sensitive with large samples and underpowered with small ones, so inspect the plot too. When the assumption fails, the standard options are stratification on the offending variable, a time-varying coefficient, an interaction with time, or reporting separate hazard ratios for prespecified periods.

# Stratify by stage rather than modelling it proportionally
cox_strat <- coxph(Surv(time, event) ~ treatment + age + strata(stage), data = trial)

Competing risks

If patients can die of something unrelated before the event of interest, a standard Kaplan-Meier curve overestimates the incidence of the event, because it treats death as censoring rather than as a competing outcome that removes the possibility of the event. Cumulative incidence functions are the right tool, and the difference becomes material in older populations.

# Cumulative incidence with competing risks
library(cmprsk)
cuminc(trial$time, trial$status, trial$group)

What to report

Report the number of events, not just the sample size — power in survival analysis is driven by events. Give median follow-up with its method, curves truncated at a defensible time with numbers at risk, hazard ratios with confidence intervals, the proportional hazards check, and explicit mention of any competing risks. Avoid reporting a hazard ratio without stating whether the assumption held.

Time-to-event reasoning connects directly to study design: how many events you need, and how long you must follow people to get them. If you want to build that planning skill properly, Ampersand Academy covers the design-to-analysis sequence.

Practice

  1. Plot a Kaplan-Meier curve with the risk table and state the median follow-up rather than the maximum.
  2. Run the log-rank test, then describe a scenario where it would mislead.
  3. Fit a Cox model, interpret one hazard ratio in a sentence that does not mention probability of the event.
  4. Test proportional hazards and try one remedy if the assumption fails.

What is censoring in survival analysis?

Censoring means you know the subject survived at least until a certain time but not what happened afterwards, typically because follow-up ended or they withdrew. Kaplan-Meier and Cox models use that partial information rather than discarding it.

What does a hazard ratio actually mean?

It compares instantaneous event rates at any given time among those still at risk. A hazard ratio of 0.75 means the treated group experiences 75% of the control group’s hazard at each moment. It is not a risk ratio and not a ratio of median survival times.

Can I use a Cox model when proportional hazards fails?

Yes, with adjustments: stratify on the variable whose effect changes over time, add a time interaction, fit a time-varying coefficient, or split follow-up into periods and report period-specific hazard ratios. Simply ignoring the failed test is not one of the options.

Why is the end of my Kaplan-Meier curve so unstable?

Because the risk set has shrunk to very few subjects, so each remaining event moves the curve by a large step. That is why curves are usually truncated at a time with an adequate number at risk and accompanied by a numbers-at-risk table.

What is the difference between median survival and mean survival?

Median survival is the time at which the curve crosses 50%, and it is estimable as long as the curve reaches that level. Mean survival requires the whole curve, including the tail, and is often not estimable when follow-up is short — report median survival instead.

Do I need survival analysis if everyone in my study had the event?

Not necessarily — if follow-up is complete and the timing is the question, you can model time directly. But the moment some subjects are censored, ordinary regression on time is biased and survival methods are required.

How many events do I need for a Cox model?

A common guideline is at least ten events per predictor variable, and events, not participants, drive precision. A study with 500 patients and 40 events supports roughly four predictors, not twenty.

Exit mobile version