Kaplan-Meier product-limit estimator of the survival function.
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)| time | n_risk | n_event | n_censor | estimate | std_error | conf_low | conf_high |
|---|
| f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| 5.0 | 228.0 | 1.0 | 0.0 | 0.995614 | 0.004376 | 0.987073 | 1.0 |
| 11.0 | 227.0 | 3.0 | 0.0 | 0.982456 | 0.008695 | 0.965562 | 0.999646 |
| 12.0 | 224.0 | 1.0 | 0.0 | 0.97807 | 0.009699 | 0.959244 | 0.997266 |
| 13.0 | 223.0 | 2.0 | 0.0 | 0.969298 | 0.011425 | 0.947163 | 0.991951 |
| 15.0 | 221.0 | 1.0 | 0.0 | 0.964912 | 0.012186 | 0.941322 | 0.989094 |
| … | … | … | … | … | … | … | … |
| 840.0 | 5.0 | 0.0 | 1.0 | 0.067127 | 0.023506 | 0.033793 | 0.133343 |
| 883.0 | 4.0 | 1.0 | 0.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| 965.0 | 3.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| 1010.0 | 2.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| 1022.0 | 1.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.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.
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)
predict()
Evaluate the survival or cumulative hazard curve at specified times.
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.
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)
rmrl()
Restricted mean residual life at time s, over the window \((s, \tau]\).
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).
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.
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)| time | n_risk | n_event | n_censor | estimate | std_error | conf_low | conf_high |
|---|
| f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| 5.0 | 228.0 | 1.0 | 0.0 | 0.995614 | 0.004376 | 0.987073 | 1.0 |
| 11.0 | 227.0 | 3.0 | 0.0 | 0.982456 | 0.008695 | 0.965562 | 0.999646 |
| 12.0 | 224.0 | 1.0 | 0.0 | 0.97807 | 0.009699 | 0.959244 | 0.997266 |
| 13.0 | 223.0 | 2.0 | 0.0 | 0.969298 | 0.011425 | 0.947163 | 0.991951 |
| 15.0 | 221.0 | 1.0 | 0.0 | 0.964912 | 0.012186 | 0.941322 | 0.989094 |
| … | … | … | … | … | … | … | … |
| 840.0 | 5.0 | 0.0 | 1.0 | 0.067127 | 0.023506 | 0.033793 | 0.133343 |
| 883.0 | 4.0 | 1.0 | 0.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| 965.0 | 3.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| 1010.0 | 2.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| 1022.0 | 1.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.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