Skip to main content

Greenwood

Modern survival analysis for Python: Narwhals-native, R-validated, beautifully visualized.

PyPI Python versions Downloads MIT License CI

Repo Status

Documentation Contributors Contributor Covenant


What is Greenwood?

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).

Why Greenwood?

  • 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 survival package
  • 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

What's included

Descriptive statistics:

  • Surv response object: One Surv() call that mirrors R's survival::Surv() handles right-, left-, and interval-censored data, the counting-process form with left truncation, and multi-state endpoints. event_time() offers a compact alternative with one-letter status codes.
  • Formulas and Outcome: Describe a response by column names and R-style expressions ("Surv(time, status == 2) ~ age", or Outcome.surv(time="time", event="status == 2")), evaluated against data= at fit. The same description works on Pandas, Polars, PyArrow, DuckDB, and lazy frames.
  • 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(), plot_forest(), plot_cif()): Interactive survival curves with confidence bands and censoring marks, publication-ready forest plots with aligned at-risk tables, and cumulative incidence plots for competing risks (all with a choice of plotting backends and Great Tables integration).

Hypothesis testing:

  • Log-rank tests (logrank_test(), pairwise_logrank_test()): Standard log-rank test and the G-rho (Fleming-Harrington) family for 2+ groups with p-value adjustment for multiple comparisons.
  • Linear trend tests (trend_test()): Test for linear trends across ordered groups with support for Fleming-Harrington weights and stratification.
  • RMST comparisons (rmst_test(), pairwise_rmst_test(), rmst_diff()): Restricted mean survival time hypothesis tests, pairwise comparisons, and difference estimation.

Regression models:

  • Cox proportional hazards (CoxPH): Model covariates as hazard ratios with stratification, robust sandwich variance, clustering, baseline hazard prediction with confidence intervals, shared gamma and log-normal frailty by cluster with frailty-variance LR testing, and model diagnostics (residuals, proportional-hazards test, concordance).
  • Penalized Cox (CoxNet, cv_coxnet()): Elastic-net regularized Cox regression (lasso, ridge, and the full elastic-net continuum) with cross-validated penalty selection — for high-dimensional covariate settings or automatic variable selection.
  • Accelerated failure time (AFT): Parametric models (Weibull, exponential, log-normal, log-logistic) with survival prediction confidence intervals, validated against R's survreg.
  • Flexible parametric models (RoystonParmar): Spline-based proportional-hazards model that fits smooth baseline hazard shapes — more flexible than Weibull while preserving interpretable covariate effects.
  • Competing risks: Cumulative incidence functions (AalenJohansen), subdistribution hazards (FineGray), and multi-state transition probabilities (MultiState).

Distribution fitting:

  • Univariate parametric fitting (Parametric): Fit Weibull, exponential, log-normal, or log-logistic distributions to censored data by MLE, with AIC/BIC for model selection.
  • Distribution comparison (compare_distributions()): Rank all parametric families side-by-side by log-likelihood, AIC, and BIC in a single call.

Model performance:

  • Prediction metrics: Concordance index (Harrell's C), IPCW Brier score / integrated Brier score, time-dependent AUC (time_dependent_auc(), integrated_auc()), and calibration assessment (calibration()).
  • Cross-validation (cross_validate()): K-fold cross-validation with stratification support for balanced outcome distributions.

Study design:

  • Power analysis (logrank_n_events(), logrank_power(), logrank_sample_size()): Compute required events, achievable power, and total enrollment for log-rank test designs under the proportional-hazards assumption.

Tidy & reproducible:

  • Tidy layer (tidy(), glance(), augment()): Broom-compatible summaries — coefficient tables, model-level statistics, and observation-level augmentation — aligned with Great Summaries for consistent reporting.
  • Built-in datasets (lung, veteran, ovarian, pbc, colon, mgus2) and R-parity test harness: Every statistic is validated to tolerance against R's survival package.

Get started

Here's a simple example that loads survival data, estimates a survival curve, and visualizes it.

import greenwood as gw

# Load the data and describe the response by column names (a status of 2 marks a death)
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")

# Estimate the Kaplan-Meier survival curve, reading the columns from the frame
km = gw.KaplanMeier().fit(death, data=lung)

# Visualize it
gw.plot_survival(km)

# Fit a Cox proportional hazards model, written as an R-style formula
cox = gw.CoxPH().fit("Surv(time, status) ~ age + sex", data=lung)

The same calls work on Pandas, Polars, PyArrow, DuckDB, and lazy frames, because the response is described by column names and R-style expressions (such as status == 2) that Greenwood evaluates against data=, rather than by data frame code. When the values are already in hand as lists or arrays, gw.Surv(time=..., event=...) builds the response from them directly.

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.

See more

Here's a comprehensive example showcasing more of Greenwood's capabilities:

import greenwood as gw

df = gw.load_dataset("lung", backend="polars")

# Describe the endpoint once, then fit anything from the frame
death = gw.Outcome.surv(time="time", event="status")

# Kaplan-Meier with stratification and detailed summaries
km = gw.KaplanMeier(conf_type="log-log").fit(death, by="sex", data=df)
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(tau=365, ci=True)      # restricted mean survival time up to 365 days
km.predict(times=[180, 365])     # survival probability at specific times

# Statistical tests
gw.logrank_test("Surv(time, status) ~ sex", data=df)          # standard log-rank test
gw.logrank_test("Surv(time, status) ~ sex", data=df, 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("Surv(time, status) ~ age + sex", data=df)
gw.tidy(cox, exponentiate=True, format="polars")  # hazard ratios with confidence intervals
gw.glance(cox, format="polars")              # model-level stats (loglik, AIC, concordance)
cox.cox_zph()                                # proportional-hazards test
cox.concordance()                            # C-statistic
cox.predict(df.head(), type="survival", times=[180, 365], format="polars")

# Penalized Cox: elastic-net with cross-validated lambda (rows with missing values are dropped)
coxnet = gw.CoxNet(l1_ratio=1.0).fit("Surv(time, status) ~ age + sex + ph.ecog + wt.loss", data=df)
cv_result = gw.cv_coxnet("Surv(time, status) ~ age + sex + ph.ecog + wt.loss", data=df)
cv_result.best_penalizer_   # penalty with the best cross-validated score

# Flexible parametric model (spline-based, proportional hazards)
rp = gw.RoystonParmar(df=3).fit("Surv(time, status) ~ age + sex", data=df)
gw.tidy(rp, format="polars")

# Parametric accelerated failure time models
aft = gw.AFT(dist="weibull").fit("Surv(time, status) ~ age + sex", data=df)
gw.tidy(aft, format="polars")           # coefficients on the log-time scale

# Univariate distribution fitting and comparison
gw.compare_distributions(
    death, data=df, format="polars"
)  # rank Weibull / exponential / lognormal / loglogistic by AIC

# Study design: events and sample size for an 80% powered log-rank test
gw.logrank_n_events(hazard_ratio=0.7)          # events needed
gw.logrank_sample_size(hazard_ratio=0.7, prob_event=0.6)  # total enrollment

# Competing risks: cumulative incidence per cause, where the first observed endpoint wins
mg = gw.load_dataset("mgus2", backend="polars")
cr = gw.Outcome.first_event(endpoints={"pcm": ("ptime", "pstat"), "death": ("futime", "death")})
gw.AalenJohansen().fit(cr, data=mg).to_frame(format="polars")
gw.FineGray(cause="pcm").fit(cr, covariates=["age", "sex"], data=mg).to_frame(format="polars")

# Model performance and prediction
gw.concordance_index(death, risk=cox.predict(type="lp"), data=df)
# One row per time, one column per subject: drop the time column and transpose
S = cox.predict(df, type="survival", times=[180, 365], format="polars").drop("time").to_numpy().T
gw.brier_score(death, survival_prob=S, times=[180, 365], data=df)
gw.time_dependent_auc(death, marker=cox.predict(type="lp"), times=[180, 365], data=df)

License

MIT (c) Richard Iannone. See the LICENSE file.

Metadata

Release files for greenwood 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for greenwood 0.8.0
File Size Uploaded
greenwood-0.8.0.tar.gz 2.8 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for greenwood 0.8.0
File Interpreter ABI Platform
greenwood-0.8.0-py3-none-any.whl Python 3 none any Details

Total release size: 3.2 MB

Release files / greenwood-0.8.0.tar.gz

Download URL greenwood-0.8.0.tar.gz
Size 2.8 MB
Tags Source
SHA-256 checksum
How to use checksums
ee951d7d61b06bcc1a84c7a282601a8ba963d3a5cf507f816ba79ff317ce661f
BLAKE2b-256 checksum
How to use checksums
b0530459a2a08e527dc052fe113b9f6d4e21205ff548f490f64705277ab93e1d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / greenwood-0.8.0-py3-none-any.whl

Download URL greenwood-0.8.0-py3-none-any.whl
Size 456.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62d7e03bbdd7d034965c13265870c2dabea2a1d87ccd742a2f12664b28aac2b1
BLAKE2b-256 checksum
How to use checksums
336275e21139fc1d6f857012a0cb388603f51247a9d622c5f439d00a50a194ee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.8.0 This release

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page