## KaplanMeier


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


Usage

``` python
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()](AFT.md#greenwood.AFT.fit) with a right-censored [Surv](Surv.md#greenwood.Surv) response (built with [Surv.right()](Surv.md#greenwood.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()](KaplanMeier.md#greenwood.KaplanMeier.median), [quantile()](KaplanMeier.md#greenwood.KaplanMeier.quantile), and [predict()](AFT.md#greenwood.AFT.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()](AFT.md#greenwood.AFT.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](AFT.md#greenwood.AFT.fit) with a [Surv](Surv.md#greenwood.Surv) response. Results are exposed as aligned arrays (`time_`, `survival_`, `std_error_`, `conf_low_`, `conf_high_`, `strata_`), as tidy frames via [to_frame()](AFT.md#greenwood.AFT.to_frame) (optionally `format=`), and through [median](KaplanMeier.md#greenwood.KaplanMeier.median), [quantile](KaplanMeier.md#greenwood.KaplanMeier.quantile), and [predict](AFT.md#greenwood.AFT.predict).


## Examples

Build a [Surv](Surv.md#greenwood.Surv) response from the bundled `lung` dataset and fit the estimator. Printing the fitted object reports the median survival and its confidence interval.


``` python
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](AFT.md#greenwood.AFT.to_frame); pass `format=` to choose the backend (here, Polars):


``` python
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) | Fit the Kaplan-Meier estimator to survival data. |
| [median()](#median) | Median survival time per stratum (the 0.5-quantile). |
| [predict()](#predict) | Evaluate the survival or cumulative hazard curve at specified times. |
| [quantile()](#quantile) | Return the `p`-quantile survival time per stratum. |
| [rmrl()](#rmrl) | Restricted mean residual life at time `s`, over the window \\(s, \tau\]\\. |
| [rmst()](#rmst) | Restricted mean survival time up to `tau` (area under the survival curve). |
| [to_frame()](#to_frame) | Return the fitted survival curve(s) as a DataFrame. |

------------------------------------------------------------------------


#### fit()


Fit the Kaplan-Meier estimator to survival data.


Usage

``` python
fit(surv, *, by=None, weights=None)
```


Computes the product-limit survival estimate from a [Surv](Surv.md#greenwood.Surv) response (time-to-event data, possibly right-censored). The estimator remains in the fitted object after calling [fit()](AFT.md#greenwood.AFT.fit); access it via attributes like `surv`, `time`, `n_risk`, etc., or access raw tables with [to_frame()](AFT.md#greenwood.AFT.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()](plot_survival.md#greenwood.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](Surv.md#greenwood.Surv) response (typically right-censored, but supports counting-process and other forms). Built from data using [Surv.right()](Surv.md#greenwood.Surv.right), [Surv.interval()](Surv.md#greenwood.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()](AFT.md#greenwood.AFT.to_frame), or visualized as separate curves via [plot_survival()](plot_survival.md#greenwood.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:


``` python
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:


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

``` python
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:


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

``` python
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:


``` python
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:


``` python
km.predict([180, 365, 730], what="cumhaz")
```


    array([0.32482809, 0.88832457, 2.1250428 ])


------------------------------------------------------------------------


#### quantile()


Return the `p`-quantile survival time per stratum.


Usage

``` python
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:


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

``` python
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:


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

``` python
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:


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

``` python
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:


``` python
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:


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