Survival Analysis

Survival Analysis / Time-to-Event Analysis

Modeling the time until an event happens when some observations are incomplete (censored or truncated). This is its own discipline because ordinary regression cannot use a row that says “this customer had not churned yet when we stopped looking” — survival methods extract information from exactly those incomplete rows. Canonical textbooks: Klein & Moeschberger Survival Analysis: Techniques for Censored and Truncated Data (2nd ed, 2003); Therneau & Grambsch Modeling Survival Data (2000). Primary Python tooling: lifelines and scikit-survival; R: survival + survminer.

When to use this skill

When NOT to use this skill


1. Censoring and truncation — the defining feature

The reason survival analysis exists. Get this wrong and every downstream estimate is biased.

Mechanism What it means Handling
Right censoring Event not yet observed at end of follow-up (most common case). You know T > c. Standard; all estimators below assume it.
Left censoring Event already happened before observation began, exact time unknown. T < c. Use models that accept left-censored entries (lifelines KaplanMeierFitter.fit_left_censoring).
Interval censoring Event happened between two inspection times. a < T < b. Turnbull estimator / interval-censored regression.
Left truncation Subjects who had the event before entry never appear at all (delayed entry, e.g. age-as-timescale). Supply an entry/lower_bound; biases ignored if untreated.
Right truncation Only subjects who have had the event are sampled (e.g. registry of completed events). Specialized estimators; rare.

Key distinction: censoring keeps the subject but loses event-time detail; truncation removes the subject from the sample entirely (Stats Ox lecture notes, 2020; NJIT Math 659 Ch.3, 2011; GeeksforGeeks, 2024). The standard estimators assume censoring is non-informative (independent of the event process).

2. The survival, hazard, and cumulative-hazard functions

Three interchangeable views of the same distribution; pick whichever the audience reads best.

The hazard is the modeling target for most methods (lifelines Quickstart v0.30, 2025; Klein & Moeschberger Ch. 2, 2003).

3. Kaplan-Meier & Nelson-Aalen (non-parametric estimators)

The first thing to compute on any survival dataset — assumption-free descriptive curves.

from lifelines import KaplanMeierFitter
kmf = KaplanMeierFitter()
kmf.fit(durations=df["tenure"], event_observed=df["churned"], entry=df.get("entry"))
kmf.median_survival_time_; kmf.plot_survival_function()

Sources: lifelines Quickstart (2025); Klein & Moeschberger Ch. 4 (2003); CPSC 330 Survival lecture (2023).

4. The log-rank test (comparing groups)

Compares two-or-more KM curves; null = equal survival across groups. Chi-square test accumulating observed-minus-expected events at each event time; weights all time points equally (Wilcoxon/Tarone-Ware variants weight early times more). Gives a p-value, not an effect size — for an effect size use Cox.

from lifelines.statistics import logrank_test, multivariate_logrank_test
logrank_test(durA, durB, eventA, eventB).p_value

Sources: lifelines.statistics (2025); STHDA (2018); Klein & Moeschberger Ch. 7 (2003).

5. Cox proportional-hazards model (the workhorse)

Semi-parametric: h(t|x) = h₀(t) · exp(βᵀx). Baseline hazard h₀(t) is unspecified; β estimated via partial likelihood (Cox 1972). exp(βⱼ) is the hazard ratio — multiplicative, time-constant.

from lifelines import CoxPHFitter
cph = CoxPHFitter(penalizer=0.1)
cph.fit(df, duration_col="tenure", event_col="churned")
cph.print_summary()          # coef, exp(coef)=HR, p, CI

Tie handling: Efron (default) or Breslow. Report HRs with CIs. Sources: lifelines CoxPHFitter (2025); Therneau & Grambsch (2000); Researchers’ Guide (2021).

6. The proportional-hazards assumption & diagnostics

Cox is only valid if hazard ratios are constant over time. Always check.

Fixes when violated: stratify (strata=), add a covariate×time interaction, split follow-up into intervals, or switch to AFT. Sources: UCLA OARC (2021); Stata stcox (2015); STHDA (2018).

7. Parametric models: exponential, Weibull, and AFT

For a smooth curve, extrapolation, or a generative model.

from lifelines import WeibullAFTFitter
aft = WeibullAFTFitter().fit(df, duration_col="tenure", event_col="churned")

Sources: AFT model — Wikipedia (2025); CRAN eha (2024); AFT vs Cox PMC4645729 (2015).

8. Competing risks (cause-specific vs. Fine-Gray)

When a subject can fail from mutually exclusive causes, naïve KM/Cox on one cause over-estimates its incidence by treating competing events as censored.

Caveats: separate Fine-Gray per cause → CIFs can sum > 1; avoid multiple Fine-Gray models — prefer cause-specific for multi-event questions. For causal effects, Fine-Gray is discouraged. Sources: Austin & Fine, Stat Med (2017); Austin et al. (2021); Statistical Horizons (2023).

9. Time-varying covariates

When a predictor changes during follow-up, a single baseline value is wrong. Use long (counting-process) format: one row per subject per interval (id, start, stop, event, covariates).

from lifelines import CoxTimeVaryingFitter
ctv = CoxTimeVaryingFitter()
ctv.fit(long_df, id_col="id", start_col="start", stop_col="stop", event_col="event")

Also the standard fix for a time-varying coefficient (a PH violation) — though that needs a covariate×time interaction. Sources: lifelines Time-varying regression (2025); CoxTimeVaryingFitter docs (2025); Therneau & Grambsch Ch. 3 (2000).

10. Discrete-time survival & churn / retention / CLV

When time is naturally binned and many events tie at the same bin, discrete-time survival beats continuous Cox.

Survival beats a static churn classifier: it answers when, uses censored customers correctly, and yields retention curves and CLV directly. Sources: SAS Survival Data Mining (2012); SAS CLV (2003); Springer churn prediction (2025).

11. Machine-learning survival models

When effects are nonlinear/interacting/high-dimensional and accuracy beats interpretability.

from sksurv.ensemble import RandomSurvivalForest
from sksurv.metrics import concordance_index_censored
rsf = RandomSurvivalForest(n_estimators=200).fit(X, y_structured)  # y = (event_bool, time)

Sources: Ishwaran et al., Ann. Appl. Stat. 2(3):841-860 (2008); scikit-survival RSF & boosting guides (2025); Katzman et al., DeepSurv, BMC Med Res Methodol / arXiv 1606.00931 (2018).


Methodology (default workflow)

  1. Define the timeline: t=0 origin, the event, the censoring rule; check for left truncation / delayed entry.
  2. Describe: KM curve + median survival; Nelson-Aalen for cumulative hazard; stratify by key groups.
  3. Compare groups: log-rank (effect size deferred to Cox).
  4. Model effects: Cox PH first; parametric/AFT for extrapolation or a smooth curve.
  5. Check assumptions: Schoenfeld residuals / cox.zph; repair PH violations.
  6. Handle structure: competing risks → cause-specific or Fine-Gray; changing covariates → time-varying; binned time → discrete-time logistic.
  7. Predict at scale: RSF / gradient boosting / DeepSurv.
  8. Validate: C-index, time-dependent AUC, integrated Brier, calibration; never plain accuracy.

Practical patterns

Anti-patterns

Troubleshooting

Symptom Likely cause Fix
KM curves cross PH violated Stratify, time-varying coefficient, or AFT; don’t trust a single HR
cox.zph p-value tiny for a covariate Non-proportional effect strata= that covariate, or add covariate×time interaction
Median survival inf/undefined Curve never reaches 0.5 (heavy censoring) Report RMST or a fixed-horizon survival probability
Cumulative incidence sums > 1 Multiple Fine-Gray models combined Use cause-specific hazards, or one Fine-Gray for the single cause
C-index ≈ 0.5 No signal / wrong outcome encoding Re-check (event, time) pairing and feature leakage
Cox fails to converge / huge CIs Separation, collinearity, too few events penalizer=, drop/merge covariates, ~10 events-per-variable
Suspiciously optimistic effects Informative censoring / immortal-time bias Audit follow-up start/end; align time origin with eligibility

References