KaplanMeier

Kaplan-Meier product-limit estimator of the survival function.

Usage

Source

KaplanMeier(
    *,
    conf_type="log",
    conf_level=0.95,
    robust=False,
)

The Kaplan-Meier estimator is a non-parametric method to estimate the survival function from right-censored data. It computes the survival probability at each observed event time as the product of conditional survival probabilities, accounting for subjects still at risk. This is the most widely used method for survival analysis and is the starting point for comparing survival between groups or assessing model fit.

To use this estimator, call fit() with a right-censored response: an Outcome such as gw.Outcome.surv(time="time", event="status") or a formula such as "Surv(time, status) ~ sex" together with data=, or a Surv built from values in hand. The estimator computes survival probabilities, standard errors, and confidence intervals at each unique event time. Results can be accessed as aligned arrays, exported to pandas/polars/pyarrow DataFrames, or queried through methods like median(), quantile(), and predict().

The implementation uses the product-limit formula

S(t) = \prod_{t_i \le t} \frac{n_i - d_i}{n_i}

where n_i is the number at risk and d_i is the number of events at time t_i. Variance uses Greenwood’s formula, and confidence intervals can be constructed on the log, log-log, or identity scale.

Parameters

conf_type: str = "log"

Confidence-interval transform: "log" (default, as in R’s survfit), "plain", or "log-log".

conf_level: float = 0.95

Confidence level for the interval (default 0.95).

robust: bool = False
Use the infinitesimal-jackknife (sandwich) variance instead of Greenwood’s formula (the default is False). When True, standard errors and confidence intervals account for the correlation structure induced by weighted or correlated observations, matching R’s survfit(..., robust = TRUE).

Returns

Fitted estimator
Call fit() to produce a fitted estimator with cached results (time_, surv_, std_error_, conf_low_, conf_high_, n_risk_, n_event_, n_censor_), accessible as aligned arrays or exported to DataFrames.

Details

Call fit with a Surv response. Results are exposed as aligned arrays (time_, survival_, std_error_, conf_low_, conf_high_, strata_), as tidy frames via to_frame() (optionally format=), and through median, quantile, and predict.

Examples

Name the response columns of the bundled lung dataset with gw.Outcome.surv() and fit the estimator with data=. Printing the fitted object reports the median survival and its confidence interval.

import greenwood as gw

# Load data and name the response columns
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")

# Fit the Kaplan-Meier estimator
km = gw.KaplanMeier().fit(death, data=lung)
km
KaplanMeier (Kaplan-Meier survival estimate)

       n  events  median  0.95LCL  0.95UCL
all  228     165     310      285      363

The full step function, one row per event time, is available with to_frame. Pass format= to choose the backend (here, Polars):

# Export the survival curve as a Polars DataFrame
km.to_frame(format="polars")
shape: (186, 9)
stratatimen_riskn_eventn_censorestimatestd_errorconf_lowconf_high
strf64f64f64f64f64f64f64f64
"all"5.0228.01.00.00.9956140.0043760.9870731.0
"all"11.0227.03.00.00.9824560.0086950.9655620.999646
"all"12.0224.01.00.00.978070.0096990.9592440.997266
"all"13.0223.02.00.00.9692980.0114250.9471630.991951
"all"15.0221.01.00.00.9649120.0121860.9413220.989094
………………………
"all"840.05.00.01.00.0671270.0235060.0337930.133343
"all"883.04.01.00.00.0503460.0228480.0206850.122534
"all"965.03.00.01.00.0503460.0228480.0206850.122534
"all"1010.02.00.01.00.0503460.0228480.0206850.122534
"all"1022.01.00.01.00.0503460.0228480.0206850.122534

Attributes

Name Description
conf_high_ Upper confidence limits for the survival estimates.
conf_low_ Lower confidence limits for the survival estimates.
cumhaz_ Nelson–Aalen cumulative hazard estimates at each event time.
std_error_ Standard errors of the survival estimates (Greenwood’s formula).
strata_ The curve label of each row: "all" for one curve, "sex=1" and so on for groups.
survival_ Kaplan–Meier survival estimates at each event time.
time_ Event times at which the survival estimate changes (one entry per step).

conf_high_

Upper confidence limits for the survival estimates.

conf_high_: Array


conf_low_

Lower confidence limits for the survival estimates.

conf_low_: Array


cumhaz_

Nelson–Aalen cumulative hazard estimates at each event time.

cumhaz_: Array


std_error_

Standard errors of the survival estimates (Greenwood’s formula).

std_error_: Array


strata_

The curve label of each row: "all" for one curve, "sex=1" and so on for groups.

strata_: Array


survival_

Kaplan–Meier survival estimates at each event time.

survival_: Array


time_

Event times at which the survival estimate changes (one entry per step).

time_: Array

Methods

Name Description
fit() Fit the Kaplan-Meier estimator to survival data.
median() Median survival time, with confidence limits, for each curve.
predict() Read the survival or cumulative hazard curve at given times, for each curve.
quantile() Return survival-time quantiles, with confidence limits, for each curve.
rmrl() Restricted mean residual life at time s, over the window (s, \tau].
rmst() Restricted mean survival time up to tau (the area under the survival curve).
to_frame() Return the fitted survival curve(s) as a DataFrame.

fit()

Fit the Kaplan-Meier estimator to survival data.

Usage

Source

fit(
    surv,
    *,
    data=None,
    by=None,
    weights=None,
    cluster=None,
)

Computes the product-limit survival estimate from a Surv response (time-to-event data, possibly right-censored). The estimator remains in the fitted object after calling fit(). Access it via attributes like surv, time, n_risk, etc., or access raw tables with to_frame() (optionally format=). Pass by= to produce separate curves per group (stratified analysis). Each group’s fit is stored independently and can be visualized with plot_survival().

The fit is exact and no distributional assumptions are made. Optionally supply weights= (e.g., inverse-probability-of-censoring weights from the survey literature) to adjust for selection bias or survey design. Confidence intervals use the method specified at instantiation (conf_type), typically Greenwood’s variance estimator.

Parameters

surv: Surv | Outcome | str

A Surv response (typically right-censored, but supports counting-process and other forms). Built with Surv(). An Outcome or a formula string such as 'Surv(time, status == 2) ~ sex' is also accepted, with its columns read from data. The right-hand side names the by column(s).

by: Any = None

Optional grouping variable (e.g., a column or array). Produces one fit (one curve) per unique value of by, enabling stratified Kaplan-Meier analysis. Each group’s results are stored and can be accessed separately via to_frame(), or visualized as separate curves via plot_survival(). Default (None) means to fit a single, unstratified curve.

weights: Any = None

Optional weights (e.g., from survey design or inverse-probability-of-censoring adjustments). Must have the same length as surv. Default (None): unit weights.

cluster: Any = None

Optional cluster labels for grouped robust variance estimation. When provided, robust=True is implied. Per-subject influences are summed within each cluster before squaring, accounting for within-cluster correlation. Default (None) means no clustering.

data: Any = None
A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) holding the columns named by the response, by, weights, and cluster. When surv is an Outcome or a formula, rows with a missing value in any column used are dropped before fitting.

Returns

KaplanMeier
The fitted estimator object itself (for method chaining) with cached results (time_, surv_, conf_low_, conf_high_, n_risk_, n_event_, etc. as attributes).

Details

The Kaplan-Meier estimator is a non-parametric maximum likelihood estimator of the survival function S(t). It is defined as the product of (1 - d/n) over all event times up to t, where d is the number of events and n is the number at risk at each time. Confidence intervals are point-wise. They do not guarantee that the true curve lies entirely within the band.

Examples

Fit a single (unstratified) survival curve on the bundled lung dataset:

import greenwood as gw

# Load data and name the response columns
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")

# Fit a single unstratified survival curve
km = gw.KaplanMeier().fit(death, data=lung)
km
KaplanMeier (Kaplan-Meier survival estimate)

       n  events  median  0.95LCL  0.95UCL
all  228     165     310      285      363

Fit stratified curves by sex by passing by="sex". This produces one curve per group, and the formula "Surv(time, status) ~ sex" gives the same fit. The results are stored and can be visualized separately:

# Fit stratified curves by sex and plot them
km_stratified = gw.KaplanMeier().fit(death, by="sex", data=lung)
gw.plot_survival(km_stratified)

median()

Median survival time, with confidence limits, for each curve.

Usage

Source

median(
    *,
    format=None,
)

The median survival time is the time at which the survival curve first drops to 0.5, when half of subjects have had the event. It is a key summary when comparing survival across groups.

Parameters

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per curve, with columns strata, time, conf_low, and conf_high. The layout is the same for one curve (strata is "all") as for several. The median is nan when the curve never drops to 0.5.

Details

This is quantile() at p=0.5, without the prob column. When the curve jumps over 0.5, the first time it reaches or falls below 0.5 is used.

Examples

The median survival time for each sex, with confidence limits:

import greenwood as gw

# Load data and fit one curve per sex
lung = gw.load_dataset("lung", backend="polars")
km = gw.KaplanMeier().fit("Surv(time, status) ~ sex", data=lung)

# Median survival time with confidence limits
km.median(format="polars")
shape: (2, 4)
stratatimeconf_lowconf_high
strf64f64f64
"sex=1"270.0212.0310.0
"sex=2"426.0348.0550.0

predict()

Read the survival or cumulative hazard curve at given times, for each curve.

Usage

Source

predict(
    times,
    *,
    what="survival",
    format=None,
)

Useful for survival probabilities at clinically relevant times (for example, 1-year and 5-year survival) and for the at-risk tables under survival plots.

Parameters

times: Any

One or more times at which to read the curve.

what: str = "survival"

"survival" (default) for the survival probability S(t), or "cumhaz" for the cumulative hazard H(t).

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per curve and time, with columns strata, time, and estimate. The layout is the same for one curve (strata is "all") as for several.

Details

The curves are right-continuous step functions, so the value at time t is the last step at a time \le t. Before the first step it is 1 for survival and 0 for the cumulative hazard.

Examples

Survival probabilities at 180, 365, and 730 days:

import greenwood as gw

# Load data and fit the Kaplan-Meier estimator
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")
km = gw.KaplanMeier().fit(death, data=lung)

# Read survival probabilities at specific time points
km.predict(times=[180, 365, 730], format="polars")
shape: (3, 3)
stratatimeestimate
strf64f64
"all"180.00.721671
"all"365.00.409242
"all"730.00.115693

Pass what="cumhaz" to read the cumulative hazard at the same times:

# Read the cumulative hazard at the same time points
km.predict(times=[180, 365, 730], what="cumhaz", format="polars")
shape: (3, 3)
stratatimeestimate
strf64f64
"all"180.00.324828
"all"365.00.888325
"all"730.02.125043

quantile()

Return survival-time quantiles, with confidence limits, for each curve.

Usage

Source

quantile(
    p,
    *,
    format=None,
)

The p quantile is the time by which a proportion p of subjects have had the event: the time at which the survival curve first drops to 1 - p. For example, p=0.25 gives the first-quartile time. Confidence limits come from inverting the pointwise confidence band, as R’s quantile.survfit() does.

Parameters

p: Any

One or more proportions between 0 and 1. For example, 0.5 is the median, and [0.25, 0.5, 0.75] gives the three quartiles.

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per curve and proportion, with columns strata, prob, time, conf_low, and conf_high. The layout is the same for one curve (strata is "all") as for several. A quantile is nan when the curve (or a confidence limit) never drops to 1 - p.

Details

The quantile is the smallest time t with S(t) \le 1 - p. Its confidence limits are the same crossing times for the lower and upper confidence curves. These are pointwise, not simultaneous, intervals.

Examples

The three quartiles of survival time, with their confidence limits:

import greenwood as gw

# Load data and fit the Kaplan-Meier estimator
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")
km = gw.KaplanMeier().fit(death, data=lung)

# Survival-time quartiles with confidence limits
km.quantile(p=[0.25, 0.5, 0.75], format="polars")
shape: (3, 5)
strataprobtimeconf_lowconf_high
strf64f64f64f64
"all"0.25170.0145.0197.0
"all"0.5310.0285.0363.0
"all"0.75550.0460.0654.0

rmrl()

Restricted mean residual life at time s, over the window (s, \tau].

Usage

Source

rmrl(
    s,
    tau,
    *,
    format=None,
)

The expected additional survival time beyond a landmark s, for subjects who have survived to s, restricted to tau:

\mathrm{RMRL}(s; \tau) = \frac{\int_s^\tau S(u) \, du}{S(s)}

It generalizes the restricted mean survival time (which is rmrl(0, tau)) to a later landmark, for example the remaining life expectancy of patients who reached a milestone.

Parameters

s: float

The landmark time. Must be non-negative.

tau: float

The end of the window. Must be greater than s.

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per curve, with columns strata, s, tau, estimate, std_error, conf_low, and conf_high. The layout is the same for one curve (strata is "all") as for several. The estimate is nan for a curve where everyone has had the event by s.

Details

The variance is the restricted-mean estimator applied to the conditional curve, summed over event times in (s, \tau]. The confidence limits are \text{estimate} \pm z \cdot \text{se}, with the lower limit floored at 0.

Examples

The expected additional survival time at 180 days, over the window out to 730 days:

import greenwood as gw

# Load data and fit the Kaplan-Meier estimator
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")
km = gw.KaplanMeier().fit(death, data=lung)

# Restricted mean residual life at 180 days
km.rmrl(s=180, tau=730, format="polars")
shape: (1, 7)
stratastauestimatestd_errorconf_lowconf_high
strf64f64f64f64f64f64
"all"180.0730.0275.70277116.862242242.653384308.752158

rmst()

Restricted mean survival time up to tau (the area under the survival curve).

Usage

Source

rmst(
    tau,
    *,
    format=None,
)

The restricted mean survival time is the expected survival time over the window [0, \tau]: the area under the survival curve up to tau. Unlike the median, it uses all the follow-up in the window, is defined even when the curve never reaches 0.5, and reads as an average (for example, mean survival over the first year).

Parameters

tau: float

The end of the window. Typically a clinically relevant horizon (for example, 1, 5, or 10 years).

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
One row per curve, with columns strata, tau, estimate, std_error, conf_low, and conf_high. The layout is the same for one curve (strata is "all") as for several.

Details

\mathrm{RMST}(\tau) = \int_0^\tau S(t) \, dt

is computed exactly from the step-function curve. The standard error is R’s survfit restricted-mean estimator, and the confidence limits are \text{estimate} \pm z \cdot \text{se}, with the lower limit floored at 0.

Examples

The mean survival time over the first 365 days, with confidence limits:

import greenwood as gw

# Load data and fit the Kaplan-Meier estimator
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")
km = gw.KaplanMeier().fit(death, data=lung)

# Restricted mean survival time over 365 days
km.rmst(tau=365, format="polars")
shape: (1, 6)
stratatauestimatestd_errorconf_lowconf_high
strf64f64f64f64f64
"all"365.0263.2218667.798859247.936384278.507349

to_frame()

Return the fitted survival curve(s) as a DataFrame.

Usage

Source

to_frame(
    *,
    format=None,
)

Exports the Kaplan-Meier step function with one row per time point, including risk-set counts, the survival estimate, its standard error, confidence limits, and curve labels.

Parameters

format: str | None = None
Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

Returns

pandas.DataFrame, polars.DataFrame, or pyarrow.Table
A tidy table with columns strata, time, n_risk, n_event, n_censor, estimate, std_error, conf_low, and conf_high. The layout is the same for one curve (strata is "all") as for several ("sex=1", "sex=2").

Raises

ImportError
If the requested (or, when auto-detecting, any) DataFrame library is not installed.

Examples

Fit a Kaplan-Meier estimator on the bundled lung dataset, then export the fitted curve as a Polars frame:

import greenwood as gw

# Load data and fit the Kaplan-Meier estimator
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")
km = gw.KaplanMeier().fit(death, data=lung)

# Export the survival curve as a Polars DataFrame
km.to_frame(format="polars")
shape: (186, 9)
stratatimen_riskn_eventn_censorestimatestd_errorconf_lowconf_high
strf64f64f64f64f64f64f64f64
"all"5.0228.01.00.00.9956140.0043760.9870731.0
"all"11.0227.03.00.00.9824560.0086950.9655620.999646
"all"12.0224.01.00.00.978070.0096990.9592440.997266
"all"13.0223.02.00.00.9692980.0114250.9471630.991951
"all"15.0221.01.00.00.9649120.0121860.9413220.989094
………………………
"all"840.05.00.01.00.0671270.0235060.0337930.133343
"all"883.04.01.00.00.0503460.0228480.0206850.122534
"all"965.03.00.01.00.0503460.0228480.0206850.122534
"all"1010.02.00.01.00.0503460.0228480.0206850.122534
"all"1022.01.00.01.00.0503460.0228480.0206850.122534

Pass a different format= for pandas or PyArrow output:

# Export as a pandas DataFrame instead
km.to_frame(format="pandas")
strata time n_risk n_event n_censor estimate std_error conf_low conf_high
0 all 5.0 228.0 1.0 0.0 0.995614 0.004376 0.987073 1.000000
1 all 11.0 227.0 3.0 0.0 0.982456 0.008695 0.965562 0.999646
2 all 12.0 224.0 1.0 0.0 0.978070 0.009699 0.959244 0.997266
3 all 13.0 223.0 2.0 0.0 0.969298 0.011425 0.947163 0.991951
4 all 15.0 221.0 1.0 0.0 0.964912 0.012186 0.941322 0.989094
... ... ... ... ... ... ... ... ... ...
181 all 840.0 5.0 0.0 1.0 0.067127 0.023506 0.033793 0.133343
182 all 883.0 4.0 1.0 0.0 0.050346 0.022848 0.020685 0.122534
183 all 965.0 3.0 0.0 1.0 0.050346 0.022848 0.020685 0.122534
184 all 1010.0 2.0 0.0 1.0 0.050346 0.022848 0.020685 0.122534
185 all 1022.0 1.0 0.0 1.0 0.050346 0.022848 0.020685 0.122534

186 rows × 9 columns