Modern survival analysis for Python: Narwhals-native, R-validated, beautifully visualized.
Greenwood is a Python library for survival analysis, the statistical study of time-to-event outcomes. Greenwood gives you lots of powerful features for extracting insights from incomplete data where some observations are censored (i.e., we don't know if the event happened yet).
- it works with your dataframe library: Pandas, Polars, PyArrow, or anything supported by Narwhals
- it is rigorously validated, where every statistic is tested to tolerance against R's gold-standard
survivalpackage - you get beautiful, interactive survival visualizations, with a choice of plotting backends so you can use whichever you prefer
- you also get publication-ready tables through integration with the Great Tables library
- batteries are included: from simple Kaplan-Meier curves to Cox proportional hazards, competing risks, and beyond
Descriptive statistics:
Survresponse object: Handle right-, left-, and interval-censored data; counting-process form; left truncation; weights; and multi-state endpoints with built-in validation.- Kaplan-Meier estimation (
KaplanMeier): Survival curves with Greenwood confidence intervals, median/quantile survival, restricted mean survival time, and step-function predictions. - Nelson-Aalen estimator (
NelsonAalen): Cumulative hazard curves. - Visualization (
plot_survival): Interactive survival curves with confidence bands, censoring marks, and aligned at-risk tables, with a choice of plotting backends.
Hypothesis testing:
- Log-rank tests (
logrank_test): Standard log-rank test and the G-rho (Fleming-Harrington) family for 2+ groups.
Regression models:
- Cox proportional hazards (
CoxPH): Model covariates as hazard ratios with stratification, robust sandwich variance, clustering, baseline hazard prediction, and model diagnostics (residuals, proportional-hazards test, concordance). - Accelerated failure time (
AFT): Parametric models (Weibull, exponential, log-normal, log-logistic) validated against R'ssurvreg. - Competing risks: Cumulative incidence functions (
AalenJohansen), subdistribution hazards (FineGray), and multi-state transition probabilities.
Model performance:
- Prediction metrics: Concordance index (Harrell's C) and inverse-probability censoring weighted (IPCW) Brier score / integrated Brier score.
Tidy & reproducible:
- Tidy layer (
greenwood.tidy): Broom-compatible summaries aligned with Great Summaries for consistent reporting. - Built-in datasets (
lung,veteran,ovarian,pbc,colon) and R-parity test harness: Every statistic is validated to tolerance against R'ssurvivalpackage.
Here's a simple example that loads survival data, estimates a survival curve, and visualizes it.
import greenwood as gw
# Load the data and represent it as a survival object
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
# Estimate the Kaplan-Meier survival curve
km = gw.KaplanMeier().fit(y)
# Visualize it
gw.plot_survival(km)
# Fit a Cox proportional hazards model
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])That's it! See the user guide for more details on each step, and scroll down for a comprehensive example covering more of Greenwood's capabilities.
Here's a comprehensive example showcasing more of Greenwood's capabilities:
import greenwood as gw
df = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(df["time"], event=(df["status"] == 2))
# Kaplan-Meier with stratification and detailed summaries
km = gw.KaplanMeier(conf_type="log-log").fit(y, by=df["sex"])
km.to_frame(format="polars") # tidy: strata, time, n_risk, n_event, estimate, conf_low, conf_high
km.median(ci=True) # median survival with confidence limits, per stratum
km.rmst(365, ci=True) # restricted mean survival time up to 365 days
km.predict([180, 365]) # survival probability at specific times
# Statistical tests
gw.logrank_test(y, group=df["sex"]) # standard log-rank test
gw.logrank_test(y, group=df["sex"], rho=1) # Peto-Peto (G-rho) test
# Visualization with risk tables
gw.plot_survival(km, risk_table=True)
# Cox proportional hazards regression
cox = gw.CoxPH().fit(y, df[["age", "sex"]])
gw.tidy(cox, exponentiate=True, format="polars") # hazard ratios with confidence intervals
cox.cox_zph() # proportional-hazards test
cox.concordance() # C-statistic
cox.predict(df[["age", "sex"]].head(), type="survival", times=[180, 365], format="polars")
# Parametric accelerated failure time models
aft = gw.AFT("weibull").fit(y, df[["age", "sex"]])
gw.tidy(aft, format="polars") # coefficients on the log-time scale
# Competing risks: cumulative incidence per cause
# (mgus2 loaded with pandas here for the Series `.where` construction below)
mg = gw.load_dataset("mgus2", backend="pandas")
etime = mg["ptime"].where(mg["pstat"] == 1, mg["futime"])
cause = mg["pstat"].where(mg["pstat"] == 1, 2 * mg["death"])
cr = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))
gw.AalenJohansen().fit(cr).to_frame(format="polars")
gw.FineGray("pcm").fit(cr, mg[["age", "sex"]]).to_frame(format="polars")
# Model performance and prediction
gw.concordance_index(y, cox.predict(type="lp"))
S = cox.predict(df[["age", "sex"]], type="survival", times=[180, 365], format="pandas").iloc[:, 1:].to_numpy().T
gw.brier_score(y, S, times=[180, 365])MIT (c) Richard Iannone. See the LICENSE file.