# Estimating survival with Kaplan-Meier

The Kaplan-Meier estimator is the workhorse of survival analysis. It answers the most basic question you can ask of time-to-event data: what fraction of subjects are still event-free at each point in time? The result is the survival curve, a step function that starts at 1.0 and drops at each observed event. This page shows how to estimate it, quantify its uncertainty, summarize it with medians and restricted means, and compare subgroups.

Every example here uses the bundled `lung` dataset and the [Surv](../reference/Surv.md#greenwood.Surv) response introduced in [Survival data and the Surv object](survival-data.md).


``` python
import greenwood as gw

lung = gw.load_dataset("lung", backend="polars")

y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
y
```


    Surv(type=right, n=228, events=165)


The response `y` is a [Surv](../reference/Surv.md#greenwood.Surv) object holding one entry per subject: the follow-up time and whether that time ended in an event or a censoring. Displaying it shows a compact summary where a trailing `+` marks a censored observation. Everything on this page is estimated from this single object.


# Estimating the survival function

You fit the estimator by calling [fit](../reference/AFT.md#greenwood.AFT.fit) with a response. The [KaplanMeier](../reference/KaplanMeier.md#greenwood.KaplanMeier) object follows the familiar fit-then-inspect pattern, so we fit it first and set it aside.


``` python
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 fitted `km` object holds the whole curve internally. The most direct way to read it is [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame), which lays the estimate out one row per observed time. We show the first few rows.


``` python
km.to_frame(format="polars")
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="9" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows186Columns8
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="time" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

time

<em>f64</em>

</div></th>
<th id="n_risk" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_risk

<em>f64</em>

</div></th>
<th id="n_event" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_event

<em>f64</em>

</div></th>
<th id="n_censor" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_censor

<em>f64</em>

</div></th>
<th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

estimate

<em>f64</em>

</div></th>
<th id="std_error" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

std_error

<em>f64</em>

</div></th>
<th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_low

<em>f64</em>

</div></th>
<th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_high

<em>f64</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_right" style="max-width: 50px">5</td>
<td class="gt_row gt_right" style="max-width: 63px">228</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.995614035088</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00437633599855</td>
<td class="gt_row gt_right" style="max-width: 117px">0.987073416741</td>
<td class="gt_row gt_right" style="max-width: 117px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 50px">11</td>
<td class="gt_row gt_right" style="max-width: 63px">227</td>
<td class="gt_row gt_right" style="max-width: 71px">3</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.982456140351</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00869464259269</td>
<td class="gt_row gt_right" style="max-width: 117px">0.965561897075</td>
<td class="gt_row gt_right" style="max-width: 117px">0.99964597882</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 50px">12</td>
<td class="gt_row gt_right" style="max-width: 63px">224</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.978070175439</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00969918321668</td>
<td class="gt_row gt_right" style="max-width: 117px">0.95924367691</td>
<td class="gt_row gt_right" style="max-width: 117px">0.997266170327</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 50px">13</td>
<td class="gt_row gt_right" style="max-width: 63px">223</td>
<td class="gt_row gt_right" style="max-width: 71px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.969298245614</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0114246495327</td>
<td class="gt_row gt_right" style="max-width: 117px">0.947163003131</td>
<td class="gt_row gt_right" style="max-width: 117px">0.991950789721</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 50px">15</td>
<td class="gt_row gt_right" style="max-width: 63px">221</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.964912280702</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0121858004893</td>
<td class="gt_row gt_right" style="max-width: 117px">0.941321714601</td>
<td class="gt_row gt_right" style="max-width: 117px">0.989094052551</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 50px">26</td>
<td class="gt_row gt_right" style="max-width: 63px">220</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.960526315789</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0128955847988</td>
<td class="gt_row gt_right" style="max-width: 117px">0.935581072627</td>
<td class="gt_row gt_right" style="max-width: 117px">0.986136669838</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 50px">30</td>
<td class="gt_row gt_right" style="max-width: 63px">219</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.956140350877</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0135620698334</td>
<td class="gt_row gt_right" style="max-width: 117px">0.929925266889</td>
<td class="gt_row gt_right" style="max-width: 117px">0.983094451916</td>
</tr>
<tr class="gd-tbl-divider">
<td class="gt_row gt_right gd-tbl-rownum">7</td>
<td class="gt_row gt_right" style="max-width: 50px">31</td>
<td class="gt_row gt_right" style="max-width: 63px">218</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0.951754385965</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0141913574455</td>
<td class="gt_row gt_right" style="max-width: 117px">0.92434233917</td>
<td class="gt_row gt_right" style="max-width: 117px">0.979979357017</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">183</td>
<td class="gt_row gt_right" style="max-width: 50px">965</td>
<td class="gt_row gt_right" style="max-width: 63px">3</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 78px">1</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0503455680708</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0228480489161</td>
<td class="gt_row gt_right" style="max-width: 117px">0.020685460199</td>
<td class="gt_row gt_right" style="max-width: 117px">0.122534195517</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">184</td>
<td class="gt_row gt_right" style="max-width: 50px">1010</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 78px">1</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0503455680708</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0228480489161</td>
<td class="gt_row gt_right" style="max-width: 117px">0.020685460199</td>
<td class="gt_row gt_right" style="max-width: 117px">0.122534195517</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">185</td>
<td class="gt_row gt_right" style="max-width: 50px">1022</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 78px">1</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0503455680708</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0228480489161</td>
<td class="gt_row gt_right" style="max-width: 117px">0.020685460199</td>
<td class="gt_row gt_right" style="max-width: 117px">0.122534195517</td>
</tr>
</tbody>
</table>


Each row corresponds to a distinct observed time. The columns tell a complete story: `n_risk` is the number of subjects still under observation just before that time, `n_event` and `n_censor` count the events and censorings that occurred at it, `estimate` is the Kaplan-Meier survival probability, and `conf_low` and `conf_high` bound it. The estimate only steps down at times where events occur; censorings reduce the risk set but do not move the curve.

The survival probability at any set of times is available through [predict](../reference/AFT.md#greenwood.AFT.predict), which evaluates the step function. It returns a plain array with one probability per requested time, in the same order you asked for them.


``` python
km.predict([180, 365, 730])
```


    array([0.72167065, 0.40924162, 0.1156931 ])


> **Note: Reading the survival probability**
>
> A survival probability of 0.53 at 365 days means the estimator predicts that 53 percent of subjects remain event-free one year in. Because the curve is a step function, the value holds constant between event times.


# Confidence intervals

The confidence band around the curve comes from Greenwood's variance formula. Greenwood supports three transforms for the interval, chosen with `conf_type`. The default is `"log"`, which matches the default in R's survival package. The `"log-log"` transform keeps the limits inside the valid range from 0 to 1 more reliably at the tails, and `"plain"` gives a symmetric interval on the probability scale.

You set the transform when you construct the estimator, so we fit a second [KaplanMeier](../reference/KaplanMeier.md#greenwood.KaplanMeier) with `conf_type="log-log"` and keep it separate from the default fit above.


``` python
lung = gw.load_dataset("lung", backend="polars")

y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))

km_loglog = gw.KaplanMeier(conf_type="log-log").fit(y)
km_loglog
```


    KaplanMeier (Kaplan-Meier survival estimate)

        n  events  median  0.95LCL  0.95UCL
      228     165     310      284      361


Pulling out just the estimate and its interval limits lets you compare this fit against the default one. We select those columns to keep the table narrow.


``` python
km_loglog.to_frame(format="polars")[["time", "estimate", "conf_low", "conf_high"]]
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="5" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows186Columns4
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="time" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

time

<em>f64</em>

</div></th>
<th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

estimate

<em>f64</em>

</div></th>
<th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_low

<em>f64</em>

</div></th>
<th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_high

<em>f64</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_right" style="max-width: 62px">5</td>
<td class="gt_row gt_right" style="max-width: 136px">0.995614035088</td>
<td class="gt_row gt_right" style="max-width: 136px">0.969277036219</td>
<td class="gt_row gt_right" style="max-width: 129px">0.999381011439</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 62px">11</td>
<td class="gt_row gt_right" style="max-width: 136px">0.982456140351</td>
<td class="gt_row gt_right" style="max-width: 136px">0.953935230206</td>
<td class="gt_row gt_right" style="max-width: 129px">0.99337913279</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 62px">12</td>
<td class="gt_row gt_right" style="max-width: 136px">0.978070175439</td>
<td class="gt_row gt_right" style="max-width: 136px">0.948119884093</td>
<td class="gt_row gt_right" style="max-width: 129px">0.990813247959</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 62px">13</td>
<td class="gt_row gt_right" style="max-width: 136px">0.969298245614</td>
<td class="gt_row gt_right" style="max-width: 136px">0.936681974228</td>
<td class="gt_row gt_right" style="max-width: 129px">0.985244433081</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 62px">15</td>
<td class="gt_row gt_right" style="max-width: 136px">0.964912280702</td>
<td class="gt_row gt_right" style="max-width: 136px">0.931066246164</td>
<td class="gt_row gt_right" style="max-width: 129px">0.982296706677</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 62px">26</td>
<td class="gt_row gt_right" style="max-width: 136px">0.960526315789</td>
<td class="gt_row gt_right" style="max-width: 136px">0.925513676153</td>
<td class="gt_row gt_right" style="max-width: 129px">0.979263834158</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 62px">30</td>
<td class="gt_row gt_right" style="max-width: 136px">0.956140350877</td>
<td class="gt_row gt_right" style="max-width: 136px">0.920018759486</td>
<td class="gt_row gt_right" style="max-width: 129px">0.976158015297</td>
</tr>
<tr class="gd-tbl-divider">
<td class="gt_row gt_right gd-tbl-rownum">7</td>
<td class="gt_row gt_right" style="max-width: 62px">31</td>
<td class="gt_row gt_right" style="max-width: 136px">0.951754385965</td>
<td class="gt_row gt_right" style="max-width: 136px">0.914576283437</td>
<td class="gt_row gt_right" style="max-width: 129px">0.972988696334</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">183</td>
<td class="gt_row gt_right" style="max-width: 62px">965</td>
<td class="gt_row gt_right" style="max-width: 136px">0.0503455680708</td>
<td class="gt_row gt_right" style="max-width: 136px">0.0178661710929</td>
<td class="gt_row gt_right" style="max-width: 129px">0.108662176031</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">184</td>
<td class="gt_row gt_right" style="max-width: 62px">1010</td>
<td class="gt_row gt_right" style="max-width: 136px">0.0503455680708</td>
<td class="gt_row gt_right" style="max-width: 136px">0.0178661710929</td>
<td class="gt_row gt_right" style="max-width: 129px">0.108662176031</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">185</td>
<td class="gt_row gt_right" style="max-width: 62px">1022</td>
<td class="gt_row gt_right" style="max-width: 136px">0.0503455680708</td>
<td class="gt_row gt_right" style="max-width: 136px">0.0178661710929</td>
<td class="gt_row gt_right" style="max-width: 129px">0.108662176031</td>
</tr>
</tbody>
</table>


The point estimates are identical across transforms; only the interval limits differ. Choose the transform before fitting, and report which one you used.


## Customizing the confidence level

By default, Greenwood computes 95% confidence intervals. You can adjust this with the [conf_level](../reference/RMSTResult.md#greenwood.RMSTResult.conf_level) parameter, useful for regulatory submissions or sensitivity analyses that require different coverage levels (e.g., 90% or 99%).


``` python
km_90 = gw.KaplanMeier(conf_level=0.90).fit(y)
km_99 = gw.KaplanMeier(conf_level=0.99).fit(y)

# Compare intervals at a few time points
km_90.to_frame(format="polars")[["time", "estimate", "conf_low", "conf_high"]].head(3)
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="5" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows3Columns4
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="time" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

time

<em>f64</em>

</div></th>
<th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

estimate

<em>f64</em>

</div></th>
<th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_low

<em>f64</em>

</div></th>
<th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_high

<em>f64</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_right" style="max-width: 66px">5</td>
<td class="gt_row gt_right" style="max-width: 133px">0.995614035088</td>
<td class="gt_row gt_right" style="max-width: 133px">0.988441563193</td>
<td class="gt_row gt_right" style="max-width: 133px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 66px">11</td>
<td class="gt_row gt_right" style="max-width: 133px">0.982456140351</td>
<td class="gt_row gt_right" style="max-width: 133px">0.968258314093</td>
<td class="gt_row gt_right" style="max-width: 133px">0.996862153069</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 66px">12</td>
<td class="gt_row gt_right" style="max-width: 133px">0.978070175439</td>
<td class="gt_row gt_right" style="max-width: 133px">0.962245848411</td>
<td class="gt_row gt_right" style="max-width: 133px">0.994154736715</td>
</tr>
</tbody>
</table>


Note that narrower confidence levels (0.90) produce tighter bands, while wider levels (0.99) give more conservative coverage. The confidence level affects the median and quantile intervals as well.


## Comparing confidence interval transforms

The three confidence interval transforms give the same point estimates; they differ only in how they construct the interval. Here is a comparison to help you choose:


``` python
import polars as pl

# Fit KM with each transform
km_plain = gw.KaplanMeier(conf_type="plain").fit(y)
km_log = gw.KaplanMeier(conf_type="log").fit(y)
km_loglog = gw.KaplanMeier(conf_type="log-log").fit(y)

# Compare interval widths at selected quantile times
results = []
for km, name in [(km_plain, "plain"), (km_log, "log"), (km_loglog, "log-log")]:
    df = km.to_frame(format="polars")
    # Select every 30th row to show interval differences across follow-up
    df_subset = df[::30].select(["time", "estimate", "conf_low", "conf_high"])
    df_subset = df_subset.with_columns(pl.lit(name).alias("transform"))
    results.append(df_subset)

pl.concat(results)
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="6" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows21Columns5
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="time" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

time

<em>f64</em>

</div></th>
<th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

estimate

<em>f64</em>

</div></th>
<th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_low

<em>f64</em>

</div></th>
<th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_high

<em>f64</em>

</div></th>
<th id="transform" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

transform

<em>str</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_right" style="max-width: 50px">5</td>
<td class="gt_row gt_right" style="max-width: 124px">0.995614035088</td>
<td class="gt_row gt_right" style="max-width: 124px">0.987036574146</td>
<td class="gt_row gt_right" style="max-width: 117px">1</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 50px">135</td>
<td class="gt_row gt_right" style="max-width: 124px">0.815253696589</td>
<td class="gt_row gt_right" style="max-width: 124px">0.76479399977</td>
<td class="gt_row gt_right" style="max-width: 117px">0.865713393409</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 50px">199</td>
<td class="gt_row gt_right" style="max-width: 124px">0.680272862223</td>
<td class="gt_row gt_right" style="max-width: 124px">0.619250223099</td>
<td class="gt_row gt_right" style="max-width: 117px">0.741295501347</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 50px">269</td>
<td class="gt_row gt_right" style="max-width: 124px">0.580686970067</td>
<td class="gt_row gt_right" style="max-width: 124px">0.514745920937</td>
<td class="gt_row gt_right" style="max-width: 117px">0.646628019198</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 50px">353</td>
<td class="gt_row gt_right" style="max-width: 124px">0.434044147155</td>
<td class="gt_row gt_right" style="max-width: 124px">0.363908912208</td>
<td class="gt_row gt_right" style="max-width: 117px">0.504179382101</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 50px">524</td>
<td class="gt_row gt_right" style="max-width: 124px">0.263190302049</td>
<td class="gt_row gt_right" style="max-width: 124px">0.195465597373</td>
<td class="gt_row gt_right" style="max-width: 117px">0.330915006725</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 50px">821</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0671274240944</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0210556916661</td>
<td class="gt_row gt_right" style="max-width: 117px">0.113199156523</td>
<td class="gt_row gt_left" style="max-width: 86px">plain</td>
</tr>
<tr class="gd-tbl-divider">
<td class="gt_row gt_right gd-tbl-rownum">7</td>
<td class="gt_row gt_right" style="max-width: 50px">5</td>
<td class="gt_row gt_right" style="max-width: 124px">0.995614035088</td>
<td class="gt_row gt_right" style="max-width: 124px">0.987073416741</td>
<td class="gt_row gt_right" style="max-width: 117px">1</td>
<td class="gt_row gt_left" style="max-width: 86px">log</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">18</td>
<td class="gt_row gt_right" style="max-width: 50px">353</td>
<td class="gt_row gt_right" style="max-width: 124px">0.434044147155</td>
<td class="gt_row gt_right" style="max-width: 124px">0.36316544851</td>
<td class="gt_row gt_right" style="max-width: 117px">0.502729471467</td>
<td class="gt_row gt_left" style="max-width: 86px">log-log</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">19</td>
<td class="gt_row gt_right" style="max-width: 50px">524</td>
<td class="gt_row gt_right" style="max-width: 124px">0.263190302049</td>
<td class="gt_row gt_right" style="max-width: 124px">0.198161004017</td>
<td class="gt_row gt_right" style="max-width: 117px">0.332592324663</td>
<td class="gt_row gt_left" style="max-width: 86px">log-log</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">20</td>
<td class="gt_row gt_right" style="max-width: 50px">821</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0671274240944</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0307283845558</td>
<td class="gt_row gt_right" style="max-width: 117px">0.123059694935</td>
<td class="gt_row gt_left" style="max-width: 86px">log-log</td>
</tr>
</tbody>
</table>


**Transform summary:**

- log\*\* (default): symmetric on the log scale; matches R's survival package. Good all-purpose choice.
- log-log: bounds intervals more reliably at the tails; best when survival is near 0 or 1.
- plain: symmetric on probability scale; intuitive but can exceed \[0,1\] at extreme tails.

Choose the transform before fitting and report which you used.


# Median survival and quantiles


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


The result is the median together with its confidence limits, obtained by inverting the confidence band. Other quantiles work the same way through [quantile](../reference/KaplanMeier.md#greenwood.KaplanMeier.quantile).


``` python
km.quantile(0.25)
```


    170.0


> **Tip: When the median is not defined**
>
> If the survival curve never falls to 0.5 within the observed follow-up, the median is not estimable and Greenwood returns `nan`. This is not an error; it is an honest statement that the data do not reach the median.


# Restricted mean survival time

The mean survival time is usually not estimable from censored data, because the tail of the curve is unknown. The restricted mean survival time, or RMST, solves this by measuring the area under the survival curve up to a chosen horizon `tau`. It has a direct interpretation as the average event-free time over the first `tau` units, and it is a good summary when survival curves cross, which makes hazard ratios hard to interpret.


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


This reports the average number of event-free days during the first year, with a confidence interval. Choose `tau` based on a clinically or practically meaningful horizon, and use the same `tau` when comparing groups.


# Restricted mean residual life

A related question is how much additional time a subject can expect, given that they have already survived to some landmark time `s`. The restricted mean residual life answers this: it is the area under the conditional survival curve from `s` to `tau`, divided by the survival at `s`. In other words, it is the restricted mean survival measured from `s` onward, among subjects still event-free at `s`. Setting `s=0` recovers the restricted mean survival time.


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


Here the value is the expected number of additional event-free days between 180 and 730 days, for a subject known to be alive at 180 days. This is useful for updating a prognosis partway through follow-up, and it complements the conditional survival curves shown in [Cox model diagnostics](cox-diagnostics.md).


# Comparing subgroups

Passing a grouping variable to `by` fits a separate curve within each stratum, which is the starting point for any subgroup comparison. The tidy output gains a `strata` column.


``` python
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))

km_sex = gw.KaplanMeier().fit(y, by=lung["sex"])
km_sex
```


    KaplanMeier (Kaplan-Meier survival estimate)

         n  events  median  0.95LCL  0.95UCL
    1  138     112     270      212      310
    2   90      53     426      348      550


The grouped fit `km_sex` holds one curve per level of `sex`. Its tidy output gains a `strata` column identifying which curve each row belongs to. We show the first couple of rows within each stratum so you can see both curves side by side without printing every time point.


``` python
km_sex.to_frame(format="polars").group_by("strata").head(2)
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="10" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows4Columns9
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="strata" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

strata

<em>obj</em>

</div></th>
<th id="time" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

time

<em>f64</em>

</div></th>
<th id="n_risk" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_risk

<em>f64</em>

</div></th>
<th id="n_event" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_event

<em>f64</em>

</div></th>
<th id="n_censor" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_censor

<em>f64</em>

</div></th>
<th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

estimate

<em>f64</em>

</div></th>
<th id="std_error" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

std_error

<em>f64</em>

</div></th>
<th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_low

<em>f64</em>

</div></th>
<th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_high

<em>f64</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_left" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">11</td>
<td class="gt_row gt_right" style="max-width: 63px">138</td>
<td class="gt_row gt_right" style="max-width: 71px">3</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 117px">0.978260869565</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0124139182765</td>
<td class="gt_row gt_right" style="max-width: 117px">0.954230116249</td>
<td class="gt_row gt_right" style="max-width: 117px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">12</td>
<td class="gt_row gt_right" style="max-width: 63px">135</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 117px">0.971014492754</td>
<td class="gt_row gt_right" style="max-width: 124px">0.014281169221</td>
<td class="gt_row gt_right" style="max-width: 117px">0.943423496588</td>
<td class="gt_row gt_right" style="max-width: 117px">0.999412404448</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">5</td>
<td class="gt_row gt_right" style="max-width: 63px">90</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 117px">0.988888888889</td>
<td class="gt_row gt_right" style="max-width: 124px">0.011049210289</td>
<td class="gt_row gt_right" style="max-width: 117px">0.967468240209</td>
<td class="gt_row gt_right" style="max-width: 117px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_left" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">60</td>
<td class="gt_row gt_right" style="max-width: 63px">89</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">0</td>
<td class="gt_row gt_right" style="max-width: 117px">0.977777777778</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0155379088618</td>
<td class="gt_row gt_right" style="max-width: 117px">0.947793404603</td>
<td class="gt_row gt_right" style="max-width: 117px">1</td>
</tr>
</tbody>
</table>


Per-stratum summaries follow naturally. [median](../reference/KaplanMeier.md#greenwood.KaplanMeier.median) and [rmst](../reference/KaplanMeier.md#greenwood.KaplanMeier.rmst) return a dictionary keyed by stratum when the fit is grouped.


``` python
km_sex.median()
```


    {1: 270.0, 2: 426.0}


To test whether the difference between groups is statistically significant, rather than just describing it, use the log-rank family covered in [Comparing groups](comparing-groups.md).


## Handling case weights

When your data contains frequency weights (e.g., summarized counts rather than individual records), or case weights for design-based analysis, pass them via the `weights` parameter.


``` python
# Example with case weights: create sample weights for demonstration
import numpy as np

weights = np.random.uniform(0.5, 2.0, len(y))

km_weighted = gw.KaplanMeier().fit(y, weights=weights)
km_weighted
```


    KaplanMeier (Kaplan-Meier survival estimate)

          n  events  median  0.95LCL  0.95UCL
      281.3   204.5     320      286      353


The estimates adjust for the weights using the weighted Kaplan-Meier formula. Confidence intervals and other summaries (median, RMST) also account for the weights automatically.


# Nelson-Aalen cumulative hazard

Closely related to survival is the cumulative hazard, the accumulated risk of the event over time. The Nelson-Aalen estimator provides it directly, and it is often preferred for diagnostics because it is roughly linear when the hazard is constant.

The estimator follows the same fit-then-inspect pattern as [KaplanMeier](../reference/KaplanMeier.md#greenwood.KaplanMeier), so we fit it in its own step.


``` python
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))

na = gw.NelsonAalen().fit(y)
na
```


    NelsonAalen (Nelson-Aalen cumulative hazard estimate)

        n  events  max cumhaz
      228     165       2.889


Reading `na` with [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame) gives the same table layout as before, but now the `estimate` column holds the cumulative hazard rather than the survival probability. Unlike survival, it starts at 0 and climbs as events accumulate.


``` python
na.to_frame(format="polars")
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="8" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows186Columns7
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="time" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

time

<em>f64</em>

</div></th>
<th id="n_risk" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_risk

<em>f64</em>

</div></th>
<th id="n_event" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n_event

<em>f64</em>

</div></th>
<th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

estimate

<em>f64</em>

</div></th>
<th id="std_error" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

std_error

<em>f64</em>

</div></th>
<th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_low

<em>f64</em>

</div></th>
<th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

conf_high

<em>f64</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_right" style="max-width: 50px">5</td>
<td class="gt_row gt_right" style="max-width: 63px">228</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00438596491228</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00438596491228</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000617822342514</td>
<td class="gt_row gt_right" style="max-width: 124px">0.031136278001</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 50px">11</td>
<td class="gt_row gt_right" style="max-width: 63px">227</td>
<td class="gt_row gt_right" style="max-width: 71px">3</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0176018239431</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00880092787832</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00660626714774</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0468985281999</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 50px">12</td>
<td class="gt_row gt_right" style="max-width: 63px">224</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0220661096574</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00986844356817</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00918438227555</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0530153450504</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 50px">13</td>
<td class="gt_row gt_right" style="max-width: 63px">223</td>
<td class="gt_row gt_right" style="max-width: 71px">2</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0310347195229</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0117304799526</td>
<td class="gt_row gt_right" style="max-width: 138px">0.0147948751781</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0651005029965</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 50px">15</td>
<td class="gt_row gt_right" style="max-width: 63px">221</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0355596064007</td>
<td class="gt_row gt_right" style="max-width: 131px">0.012572937651</td>
<td class="gt_row gt_right" style="max-width: 138px">0.0177825713872</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0711081417777</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 50px">26</td>
<td class="gt_row gt_right" style="max-width: 63px">220</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0401050609462</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0133693649138</td>
<td class="gt_row gt_right" style="max-width: 138px">0.020866224206</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0770822692987</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 50px">30</td>
<td class="gt_row gt_right" style="max-width: 63px">219</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0446712709918</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0141276393067</td>
<td class="gt_row gt_right" style="max-width: 138px">0.0240341714416</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0830285519462</td>
</tr>
<tr class="gd-tbl-divider">
<td class="gt_row gt_right gd-tbl-rownum">7</td>
<td class="gt_row gt_right" style="max-width: 50px">31</td>
<td class="gt_row gt_right" style="max-width: 63px">218</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0492584269551</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0148536928813</td>
<td class="gt_row gt_right" style="max-width: 138px">0.0272774597424</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0889522942756</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">183</td>
<td class="gt_row gt_right" style="max-width: 50px">965</td>
<td class="gt_row gt_right" style="max-width: 63px">3</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 131px">2.88926746252</td>
<td class="gt_row gt_right" style="max-width: 131px">0.418689933124</td>
<td class="gt_row gt_right" style="max-width: 138px">2.17489507081</td>
<td class="gt_row gt_right" style="max-width: 124px">3.83828469797</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">184</td>
<td class="gt_row gt_right" style="max-width: 50px">1010</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 131px">2.88926746252</td>
<td class="gt_row gt_right" style="max-width: 131px">0.418689933124</td>
<td class="gt_row gt_right" style="max-width: 138px">2.17489507081</td>
<td class="gt_row gt_right" style="max-width: 124px">3.83828469797</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">185</td>
<td class="gt_row gt_right" style="max-width: 50px">1022</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 131px">2.88926746252</td>
<td class="gt_row gt_right" style="max-width: 131px">0.418689933124</td>
<td class="gt_row gt_right" style="max-width: 138px">2.17489507081</td>
<td class="gt_row gt_right" style="max-width: 124px">3.83828469797</td>
</tr>
</tbody>
</table>


# Next steps

You can now estimate survival curves, summarize them, and split them by group.

- [Comparing groups](comparing-groups.md) tests whether survival differs between strata with the log-rank family.
- [Visualizing survival](visualization.md) turns these curves into publication-quality figures with plotnine, including numbers-at-risk tables.
- [Cox regression](cox-regression.md) moves from describing groups to modeling the effect of continuous and multiple covariates.
