Kaplan-Meier product-limit estimator of the survival function.
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.
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)| strata | time | n_risk | n_event | n_censor | estimate | std_error | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| "all" | 5.0 | 228.0 | 1.0 | 0.0 | 0.995614 | 0.004376 | 0.987073 | 1.0 |
| "all" | 11.0 | 227.0 | 3.0 | 0.0 | 0.982456 | 0.008695 | 0.965562 | 0.999646 |
| "all" | 12.0 | 224.0 | 1.0 | 0.0 | 0.97807 | 0.009699 | 0.959244 | 0.997266 |
| "all" | 13.0 | 223.0 | 2.0 | 0.0 | 0.969298 | 0.011425 | 0.947163 | 0.991951 |
| "all" | 15.0 | 221.0 | 1.0 | 0.0 | 0.964912 | 0.012186 | 0.941322 | 0.989094 |
| … | … | … | … | … | … | … | … | … |
| "all" | 840.0 | 5.0 | 0.0 | 1.0 | 0.067127 | 0.023506 | 0.033793 | 0.133343 |
| "all" | 883.0 | 4.0 | 1.0 | 0.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| "all" | 965.0 | 3.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| "all" | 1010.0 | 2.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| "all" | 1022.0 | 1.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.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_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).
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.
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)
predict()
Read the survival or cumulative hazard curve at given times, for each curve.
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)| strata | time | estimate |
|---|
| str | f64 | f64 |
| "all" | 180.0 | 0.721671 |
| "all" | 365.0 | 0.409242 |
| "all" | 730.0 | 0.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)| strata | time | estimate |
|---|
| str | f64 | f64 |
| "all" | 180.0 | 0.324828 |
| "all" | 365.0 | 0.888325 |
| "all" | 730.0 | 2.125043 |
quantile()
Return survival-time quantiles, with confidence limits, for each curve.
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)| strata | prob | time | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 |
| "all" | 0.25 | 170.0 | 145.0 | 197.0 |
| "all" | 0.5 | 310.0 | 285.0 | 363.0 |
| "all" | 0.75 | 550.0 | 460.0 | 654.0 |
rmrl()
Restricted mean residual life at time s, over the window (s, \tau].
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)| strata | s | tau | estimate | std_error | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 |
| "all" | 180.0 | 730.0 | 275.702771 | 16.862242 | 242.653384 | 308.752158 |
rmst()
Restricted mean survival time up to tau (the area under the survival curve).
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)| strata | tau | estimate | std_error | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 |
| "all" | 365.0 | 263.221866 | 7.798859 | 247.936384 | 278.507349 |
to_frame()
Return the fitted survival curve(s) as a DataFrame.
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)| strata | time | n_risk | n_event | n_censor | estimate | std_error | conf_low | conf_high |
|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| "all" | 5.0 | 228.0 | 1.0 | 0.0 | 0.995614 | 0.004376 | 0.987073 | 1.0 |
| "all" | 11.0 | 227.0 | 3.0 | 0.0 | 0.982456 | 0.008695 | 0.965562 | 0.999646 |
| "all" | 12.0 | 224.0 | 1.0 | 0.0 | 0.97807 | 0.009699 | 0.959244 | 0.997266 |
| "all" | 13.0 | 223.0 | 2.0 | 0.0 | 0.969298 | 0.011425 | 0.947163 | 0.991951 |
| "all" | 15.0 | 221.0 | 1.0 | 0.0 | 0.964912 | 0.012186 | 0.941322 | 0.989094 |
| … | … | … | … | … | … | … | … | … |
| "all" | 840.0 | 5.0 | 0.0 | 1.0 | 0.067127 | 0.023506 | 0.033793 | 0.133343 |
| "all" | 883.0 | 4.0 | 1.0 | 0.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| "all" | 965.0 | 3.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| "all" | 1010.0 | 2.0 | 0.0 | 1.0 | 0.050346 | 0.022848 | 0.020685 | 0.122534 |
| "all" | 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:
# 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