Metadata-Version: 2.4
Name: greenwood
Version: 0.1.0
Summary: Modern survival analysis for Python: Narwhals-native, R-validated, beautifully visualized.
Author-email: Richard Iannone <riannone@me.com>
Maintainer-email: Richard Iannone <riannone@me.com>
License: MIT License
        
        Copyright (c) 2026 Richard Iannone
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/rich-iannone/greenwood
Project-URL: Repository, https://github.com/rich-iannone/greenwood
Project-URL: Documentation, https://rich-iannone.github.io/greenwood/
Project-URL: Bug Tracker, https://github.com/rich-iannone/greenwood/issues
Keywords: survival analysis,time to event,kaplan-meier,cox regression,proportional hazards,competing risks,hazard,censoring,epidemiology,biostatistics,reliability
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: narwhals>=2.0
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.11
Requires-Dist: typing_extensions>=4.0
Provides-Extra: pd
Requires-Dist: pandas>=2.0; extra == "pd"
Provides-Extra: pl
Requires-Dist: polars>=1.0; extra == "pl"
Provides-Extra: pa
Requires-Dist: pyarrow>=15; extra == "pa"
Provides-Extra: formula
Requires-Dist: formulaic>=1.0; extra == "formula"
Provides-Extra: plotnine
Requires-Dist: plotnine>=0.13; extra == "plotnine"
Provides-Extra: altair
Requires-Dist: altair>=6; extra == "altair"
Requires-Dist: vl-convert-python>=1.9; extra == "altair"
Provides-Extra: tables
Requires-Dist: great-tables>=0.17; extra == "tables"
Provides-Extra: fast
Requires-Dist: numba>=0.59; extra == "fast"
Provides-Extra: all
Requires-Dist: greenwood[altair,formula,pa,pd,pl,plotnine,tables]; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest-snapshot; extra == "dev"
Requires-Dist: pytest-randomly; extra == "dev"
Requires-Dist: hypothesis>=6.100; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: pyright; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: pandas>=2.0; extra == "dev"
Requires-Dist: polars>=1.0; extra == "dev"
Requires-Dist: pyarrow>=15; extra == "dev"
Requires-Dist: formulaic>=1.0; extra == "dev"
Requires-Dist: plotnine>=0.13; extra == "dev"
Requires-Dist: altair>=6; extra == "dev"
Requires-Dist: vl-convert-python>=1.9; extra == "dev"
Provides-Extra: docs
Requires-Dist: jupyter; extra == "docs"
Requires-Dist: nbclient; extra == "docs"
Requires-Dist: nbformat; extra == "docs"
Requires-Dist: pandas>=2.0; extra == "docs"
Requires-Dist: polars>=1.0; extra == "docs"
Requires-Dist: pyarrow>=15; extra == "docs"
Requires-Dist: altair>=6; extra == "docs"
Requires-Dist: vl-convert-python>=1.9; extra == "docs"
Requires-Dist: plotnine>=0.13; extra == "docs"
Requires-Dist: formulaic>=1.0; extra == "docs"
Dynamic: license-file

# Greenwood

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

## 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](https://narwhals-dev.github.io/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](https://posit-co.github.io/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**: 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's `survreg`.
- **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's `survival` package.

## Get started

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

```python
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](user_guide/02-quick-start.qmd) 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:

```python
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])
```

## License

MIT (c) Richard Iannone. See the [`LICENSE`](LICENSE) file.
