KaplanMeier

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

Usage

Source

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

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 Surv response (built with Surv.right()). 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).

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

Build a Surv response from the bundled lung dataset and fit the estimator. Printing the fitted object reports the median survival and its confidence interval.

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km
KaplanMeier (Kaplan-Meier survival estimate)

    n  events  median  0.95LCL  0.95UCL
  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):

km.to_frame(format="polars")
shape: (186, 8)
timen_riskn_eventn_censorestimatestd_errorconf_lowconf_high
f64f64f64f64f64f64f64f64
5.0228.01.00.00.9956140.0043760.9870731.0
11.0227.03.00.00.9824560.0086950.9655620.999646
12.0224.01.00.00.978070.0096990.9592440.997266
13.0223.02.00.00.9692980.0114250.9471630.991951
15.0221.01.00.00.9649120.0121860.9413220.989094
840.05.00.01.00.0671270.0235060.0337930.133343
883.04.01.00.00.0503460.0228480.0206850.122534
965.03.00.01.00.0503460.0228480.0206850.122534
1010.02.00.01.00.0503460.0228480.0206850.122534
1022.01.00.01.00.0503460.0228480.0206850.122534

Methods

Name Description
fit() Fit the Kaplan-Meier estimator to survival data.
median() Median survival time per stratum (the 0.5-quantile).
predict() Evaluate the survival or cumulative hazard curve at specified times.
quantile() Return the p-quantile survival time per stratum.
rmrl() Restricted mean residual life at time s, over the window \((s, \tau]\).
rmst() Restricted mean survival time up to tau (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, *, by=None, weights=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

A Surv response (typically right-censored, but supports counting-process and other forms). Built from data using Surv.right(), Surv.interval(), etc.

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

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km
KaplanMeier (Kaplan-Meier survival estimate)

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

Fit stratified curves by sex by passing by=lung["sex"]. This produces one curve per group; the results are stored and can be visualized separately:

km_stratified = gw.KaplanMeier().fit(y, by=lung["sex"])
gw.plot_survival(km_stratified)

median()

Median survival time per stratum (the 0.5-quantile).

Usage

Source

median(*, ci=False)

Computes the median survival time: the time at which the survival curve first drops to 0.5, meaning 50% of subjects have experienced the event. A key clinical summary statistic when comparing survival across groups or evaluating prognosis.

Parameters
ci: bool = False
If True, return (estimate, lower, upper) confidence limits by inverting the survival confidence band. If False (default), return only the point estimate.
Returns
float or tuple or dict
For a single stratum: a float (point estimate) or 3-tuple of floats (estimate, lower, upper) if ci=True. For stratified fits: a dict keyed by stratum label, with values as above. If the survival curve never drops to 0.5, the median is nan.
Details

The median is a convenience wrapper around quantile(0.5, ci=ci). It is the time-to-event value that divides the cohort into two equal halves (in terms of probability of experiencing the event). Unlike parametric models, the non-parametric median may not be uniquely defined if the curve jumps over 0.5; by convention, the first time the curve reaches or falls below 0.5 is returned.

Examples

The median is the time at which the survival curve first drops to 0.5. Pass ci=True for its confidence limits:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km.median(ci=True)
(310.0, 285.0, 363.0)

predict()

Evaluate the survival or cumulative hazard curve at specified times.

Usage

Source

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

Reads the estimated survival function or cumulative hazard off the step-function curve at any set of query times. Useful for extracting survival probabilities or hazard accumulation at clinically relevant time points (e.g., 1-year, 5-year survival).

Parameters
times: Any

Query times at which to evaluate the curve. Can be a scalar or array-like of floats. Results are returned as a scalar or array matching the input shape.

what: str = "survival"
Quantity to evaluate: "survival" (default) for survival probability \(S(t)\), or "cumhaz" for cumulative hazard \(H(t)\). Raises ValueError if any other value.
Returns
ndarray or dict
For a single stratum: an array (or scalar if times is scalar) of estimated values at the query times. For stratified fits: a dict keyed by stratum label, with values as above.
Details

The survival and cumulative hazard curves are step functions defined only at observed event times. Values at times between events are interpolated using the right-continuous step-function convention: the value at time \(t\) is the last step at time \(\le t\). Times before the first event (or after the last observed time with non-zero survival) may return baseline values (1.0 for survival, 0.0 for cumulative hazard) or the last estimated value, respectively.

Examples

Read the survival probability off the curve at any set of times. Here are the estimated survival probabilities at 180, 365, and 730 days:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km.predict([180, 365, 730])
array([0.72167065, 0.40924162, 0.1156931 ])

Pass what="cumhaz" instead to evaluate the cumulative hazard at those same times:

km.predict([180, 365, 730], what="cumhaz")
array([0.32482809, 0.88832457, 2.1250428 ])

quantile()

Return the p-quantile survival time per stratum.

Usage

Source

quantile(p, *, ci=False)

Computes the quantile (percentile) of the survival time distribution, i.e., the time at which the survival curve first drops to (1 - p). For example, p=0.25 returns the 25th percentile (first-quartile time: the time by which 25% of subjects have experienced the event). Useful for reporting clinically meaningful landmarks.

Parameters
p: float

Quantile level between 0 and 1. For example, p=0.5 is the median, p=0.25 is the first quartile, p=0.75 is the third quartile.

ci: bool = False
If True, return (estimate, lower, upper) confidence limits by inverting the survival confidence band (follows R’s quantile.survfit convention). If False (default), return only the point estimate.
Returns
float or tuple or dict
For a single stratum: a float (point estimate) or 3-tuple of floats (estimate, lower, upper) if ci=True. For stratified fits: a dict keyed by stratum label, with values as above. If the survival curve never drops to (1 - p), the quantile is nan.
Details

The quantile is found by inverting the step-function survival curve: the smallest time \(t\) such that \(S(t) \le (1 - p)\). Confidence intervals are obtained by inverting the pointwise confidence band, following R’s convention. These are not simultaneous confidence intervals.

Examples

Any quantile of the survival distribution is available. Here is the first-quartile survival time (the time by which a quarter of subjects have had the event), with its confidence limits:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km.quantile(0.25, ci=True)
(170.0, 145.0, 197.0)

rmrl()

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

Usage

Source

rmrl(s, tau, *, ci=False)

Computes the expected additional survival time beyond a landmark time s, conditional on having survived to s, restricted to an upper time limit tau. Mathematically:

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

This is a generalization of RMST to a later landmark point, useful for assessing prognosis or remaining life expectancy for subjects who have already reached a specific milestone.

Parameters
s: float

The landmark time. Must be non-negative. Represents the time at which subjects are assessed (e.g., time to remission, time at clinic visit, etc.).

tau: float

The upper time limit for the restriction. Must be greater than \(s\). Typically a clinically relevant horizon beyond the landmark (e.g., \(s = 180\) days landmark, \(\tau = 730\) days endpoint).

ci: bool = False
If True, return (estimate, lower, upper) confidence limits using a normal approximation (\(\text{estimate} \pm z \cdot \text{se}\), with lower bound at 0). If False (default), return only the point estimate.
Returns
float or tuple or dict
For a single stratum: a float (point estimate) or 3-tuple of floats (estimate, lower, upper) if ci=True. For stratified fits: a dict keyed by stratum label, with values as above. If everyone has failed by time s (i.e., S(s) = 0), the value is nan.
Details

The restricted mean residual life at a landmark time s measures the expected additional survival time for subjects who have survived to s, restricted to time tau. It generalizes RMST (which is equivalently rmrl(0, tau)). This is useful in clinical follow-up: given that a patient has survived to time s, what is the expected additional survival time? Variance estimation accounts for the conditioning on \(S(s)\).

Examples

Restricted mean residual life is the expected additional survival time for subjects who have already survived to a landmark. Here it is at 180 days, over the window out to 730 days, with confidence limits:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km.rmrl(180, 730, ci=True)
(275.7027711565545, 242.6533843723124, 308.75215794079656)

rmst()

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

Usage

Source

rmst(tau, *, ci=False)

Computes the restricted mean survival time: the expected survival time over a fixed time window [0, tau], calculated as the area under the survival curve up to tau. Unlike median or quantiles, RMST uses all available follow-up information in the window, making it robust and easily interpretable as the average survival time over tau (e.g., 1-year mean survival, 5-year mean survival).

Parameters
tau: float

The upper time limit for the restriction. Must be positive. Typically chosen as a clinically relevant horizon (e.g., 1, 5, or 10 years).

ci: bool = False
If True, return (estimate, lower, upper) confidence limits using a normal approximation (\(\text{estimate} \pm z \cdot \text{se}\), with lower bound at 0). If False (default), return only the point estimate.
Returns
float or tuple or dict
For a single stratum: a float (point estimate) or 3-tuple of floats (estimate, lower, upper) if ci=True. For stratified fits: a dict keyed by stratum label, with values as above.
Details

The restricted mean survival time is computed as the definite integral of \(S(t)\) from 0 to \(\tau\):

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

It is estimated numerically by integrating the step-function survival curve. Unlike the median, RMST is defined even when the survival curve does not reach 0.5, and is easily comparable across groups. Confidence intervals use the normal approximation with Greenwood-style variance estimation.

Examples

The restricted mean survival time is the average survival time over a fixed window, computed as the area under the curve up to tau. Here it is over the first 365 days, with its confidence limits:

import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km.rmst(365, ci=True)
(263.22186648200665, 247.93638355232736, 278.50734941168594)

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 optional strata 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 time, n_risk, n_event, n_censor, estimate, std_error, conf_low, conf_high, and optionally strata.
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

lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
km = gw.KaplanMeier().fit(y)
km.to_frame(format="polars")
shape: (186, 8)
timen_riskn_eventn_censorestimatestd_errorconf_lowconf_high
f64f64f64f64f64f64f64f64
5.0228.01.00.00.9956140.0043760.9870731.0
11.0227.03.00.00.9824560.0086950.9655620.999646
12.0224.01.00.00.978070.0096990.9592440.997266
13.0223.02.00.00.9692980.0114250.9471630.991951
15.0221.01.00.00.9649120.0121860.9413220.989094
840.05.00.01.00.0671270.0235060.0337930.133343
883.04.01.00.00.0503460.0228480.0206850.122534
965.03.00.01.00.0503460.0228480.0206850.122534
1010.02.00.01.00.0503460.0228480.0206850.122534
1022.01.00.01.00.0503460.0228480.0206850.122534

Pass a different format= for pandas or PyArrow output:

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

186 rows × 8 columns