# Survival data and the Surv object

Survival analysis studies the time until an event happens: death, relapse, machine failure, customer churn, or any other well-defined endpoint. What makes this kind of data special, and what separates survival analysis from ordinary regression, is that we usually cannot observe the event time for everyone. Some subjects are still event-free when the study ends, some drop out early, and some enter late. Handling these partially observed times correctly is the entire point of the field, and it starts with how you represent the data.

This page explains the structure of survival data and introduces the [Surv](../reference/Surv.md#greenwood.Surv) object, the response type that every estimator and model in Greenwood consumes.


# Why survival data is different

Imagine a study that follows patients for two years and records the time until relapse. At the end of the study, three situations are possible for any given patient.

The first is a fully observed event: the patient relapsed at a known time, say 8 months. The second is right censoring: the patient was relapse-free at their last visit, say at 20 months, so all we know is that their true relapse time is greater than 20 months. The third is a variation on censoring caused by how subjects enter and leave the study.

Right censoring is by far the most common situation, and it is why we cannot simply average the observed times or run a standard regression on them. A censored time of 20 months is not the same as an event at 20 months, and treating it as one would badly bias every estimate. Survival methods are built to use the partial information in a censored observation without pretending it is a complete one.


<figure class="quarto-float quarto-float-fig figure">

<img src="data:image/svg+xml;base64,PHN2ZyB2aWV3Ym94PSIwIDAgNjgwIDIxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBzdHlsZT0ibWF4LXdpZHRoOjY4MHB4O3dpZHRoOjEwMCU7aGVpZ2h0OmF1dG87Zm9udC1mYW1pbHk6c2Fucy1zZXJpZjtjb2xvcjppbmhlcml0Ij4KPHRleHQgeD0iMTI1IiB5PSI1OSIgdGV4dC1hbmNob3I9ImVuZCIgZm9udC1zaXplPSIxMyIgZmlsbD0iY3VycmVudENvbG9yIj5TdWJqZWN0IEE8L3RleHQ+IDxsaW5lIHgxPSIxNDAiIHkxPSI1NSIgeDI9IjI4MCIgeTI9IjU1IiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIzIj48L2xpbmU+IDxjaXJjbGUgY3g9IjI4MCIgY3k9IjU1IiByPSI2IiBmaWxsPSIjNGM3OGE4Ij48L2NpcmNsZT4gPHRleHQgeD0iMjkyIiB5PSI1OSIgZm9udC1zaXplPSIxMyIgZmlsbD0iY3VycmVudENvbG9yIj5FdmVudCAodCA9IDgpPC90ZXh0PiA8dGV4dCB4PSIxMjUiIHk9IjEwOSIgdGV4dC1hbmNob3I9ImVuZCIgZm9udC1zaXplPSIxMyIgZmlsbD0iY3VycmVudENvbG9yIj5TdWJqZWN0IEI8L3RleHQ+IDxsaW5lIHgxPSIxNDAiIHkxPSIxMDUiIHgyPSI0OTAiIHkyPSIxMDUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjMiPjwvbGluZT4gPGNpcmNsZSBjeD0iNDkwIiBjeT0iMTA1IiByPSI2IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIj48L2NpcmNsZT4gPHRleHQgeD0iNTAyIiB5PSIxMDkiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9ImN1cnJlbnRDb2xvciI+Q2Vuc29yZWQgKHQgPSAyMCk8L3RleHQ+IDxsaW5lIHgxPSIxNDAiIHkxPSIxNTAiIHgyPSI1NjAiIHkyPSIxNTAiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2Utd2lkdGg9IjEiPjwvbGluZT4gPGcgZm9udC1zaXplPSIxMSIgZmlsbD0iY3VycmVudENvbG9yIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj4gPGxpbmUgeDE9IjE0MCIgeTE9IjE1MCIgeDI9IjE0MCIgeTI9IjE1NiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiPjwvbGluZT48dGV4dCB4PSIxNDAiIHk9IjE3MCI+MDwvdGV4dD4gPGxpbmUgeDE9IjIxMCIgeTE9IjE1MCIgeDI9IjIxMCIgeTI9IjE1NiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiPjwvbGluZT48dGV4dCB4PSIyMTAiIHk9IjE3MCI+NDwvdGV4dD4gPGxpbmUgeDE9IjI4MCIgeTE9IjE1MCIgeDI9IjI4MCIgeTI9IjE1NiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiPjwvbGluZT48dGV4dCB4PSIyODAiIHk9IjE3MCI+ODwvdGV4dD4gPGxpbmUgeDE9IjM1MCIgeTE9IjE1MCIgeDI9IjM1MCIgeTI9IjE1NiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiPjwvbGluZT48dGV4dCB4PSIzNTAiIHk9IjE3MCI+MTI8L3RleHQ+IDxsaW5lIHgxPSI0MjAiIHkxPSIxNTAiIHgyPSI0MjAiIHkyPSIxNTYiIHN0cm9rZT0iY3VycmVudENvbG9yIj48L2xpbmU+PHRleHQgeD0iNDIwIiB5PSIxNzAiPjE2PC90ZXh0PiA8bGluZSB4MT0iNDkwIiB5MT0iMTUwIiB4Mj0iNDkwIiB5Mj0iMTU2IiBzdHJva2U9ImN1cnJlbnRDb2xvciI+PC9saW5lPjx0ZXh0IHg9IjQ5MCIgeT0iMTcwIj4yMDwvdGV4dD4gPGxpbmUgeDE9IjU2MCIgeTE9IjE1MCIgeDI9IjU2MCIgeTI9IjE1NiIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiPjwvbGluZT48dGV4dCB4PSI1NjAiIHk9IjE3MCI+MjQ8L3RleHQ+IDwvZz4gPHRleHQgeD0iMzUwIiB5PSIxOTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9ImN1cnJlbnRDb2xvciI+TW9udGhzIG9mIGZvbGxvdy11cDwvdGV4dD4KPC9zdmc+" />

<figcaption>Figure 1: Two subjects observed over a study window. Subject A reaches the event at month 8. Subject B is still event-free at month 20, so their true event time is only known to exceed 20: this is right censoring.</figcaption>
</figure>


Greenwood also supports less common but important patterns: left censoring (the event happened before a known time), interval censoring (the event happened between two known times), and left truncation or late entry (a subject only becomes observable after some delay). All of these are expressed through the same response object.

> **Note: The key idea**
>
> A survival observation carries two pieces of information: a time, and a status that says what the time means (an event, or a censoring point). You must always keep them together. Greenwood does this for you with the [Surv](../reference/Surv.md#greenwood.Surv) object.


# The Surv object

[Surv](../reference/Surv.md#greenwood.Surv) is the response you pass to estimators and models. It bundles the times with their status codes and validates them, so that downstream code can rely on a clean, consistent representation. You build one with a constructor that names the censoring type.

The most common constructor is `gw.Surv.right`, for right-censored data. It takes the observed times and an event indicator, where a truthy value (or `1`) means the event occurred and a falsy value (or `0`) means the observation was censored.


``` python
import greenwood as gw

# Four subjects: events at 5 and 4, censored at 6 and 9.
y = gw.Surv.right(time=[5, 6, 4, 9], event=[1, 0, 1, 0])
y
```


    Surv(type=right, n=4, events=2)


The `repr` gives a quick summary: the censoring type, the number of observations, and how many of them were events. You can also reach the derived quantities directly.


``` python
print("observations:", y.n)
print("events:", y.n_events)
print("censored:", y.n_censored)
```


    observations: 4
    events: 2
    censored: 2


## Other censoring types

When subjects enter the risk set after time zero, or when a covariate changes during follow-up, you use the counting-process form. Each observation is an interval `(start, stop]` with an event indicator at the stop time. This is also how left truncation is expressed.


``` python
gw.Surv.counting(start=[0, 2, 1], stop=[5, 6, 4], event=[1, 0, 1])
```


    Surv(type=counting, n=3, events=2, truncated)


Left censoring, where all you know is that the event had already happened by the time of observation, uses `gw.Surv.left`. It takes the same `(time, event)` arguments as `gw.Surv.right`, but the time is an upper bound on the unknown event time rather than a lower one.


``` python
gw.Surv.left(time=[5, 6, 4], event=[1, 0, 1])
```


    Surv(type=left, n=3, events=2)


Interval censoring, where the event is known only to fall between two times, uses `gw.Surv.interval`. Use `numpy.inf` for the upper bound to mark right censoring.


``` python
import numpy as np

gw.Surv.interval(lower=[1, 2, 3], upper=[2, np.inf, 5])
```


    Surv(type=interval, n=3, events=2)


Competing risks and multi-state endpoints, where more than one kind of event can occur, use `gw.Surv.multistate`. The event codes are `0` for censoring and `1, 2, ...` for the competing causes, and you name the states.


``` python
gw.Surv.multistate(time=[5, 6, 7, 8], event=[1, 2, 0, 1], states=("relapse", "death"))
```


    Surv(type=right, n=4, events=3, states=('relapse', 'death'))


# Building a response from a data frame

In practice your data lives in a data frame. Greenwood is dataframe-agnostic through Narwhals, so you can pass pandas or Polars columns directly. The bundled `lung` dataset, from the North Central Cancer Treatment Group, is a good example.


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


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="11" 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>Rows228Columns10
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="inst" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

inst

<em>i64</em>

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

time

<em>i64</em>

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

status

<em>i64</em>

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

age

<em>i64</em>

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

sex

<em>i64</em>

</div></th>
<th id="ph.ecog" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

ph.ecog

<em>i64</em>

</div></th>
<th id="ph.karno" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

ph.karno

<em>i64</em>

</div></th>
<th id="pat.karno" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

pat.karno

<em>i64</em>

</div></th>
<th id="meal.cal" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

meal.cal

<em>i64</em>

</div></th>
<th id="wt.loss" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

wt.loss

<em>i64</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">3</td>
<td class="gt_row gt_right" style="max-width: 50px">306</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">74</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">90</td>
<td class="gt_row gt_right" style="max-width: 86px">100</td>
<td class="gt_row gt_right" style="max-width: 78px">1175</td>
<td class="gt_row gt_right gd-tbl-missing" style="max-width: 71px">None</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 50px">3</td>
<td class="gt_row gt_right" style="max-width: 50px">455</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">68</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 78px">90</td>
<td class="gt_row gt_right" style="max-width: 86px">90</td>
<td class="gt_row gt_right" style="max-width: 78px">1225</td>
<td class="gt_row gt_right" style="max-width: 71px">15</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 50px">3</td>
<td class="gt_row gt_right" style="max-width: 50px">1010</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">56</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 78px">90</td>
<td class="gt_row gt_right" style="max-width: 86px">90</td>
<td class="gt_row gt_right gd-tbl-missing" style="max-width: 78px">None</td>
<td class="gt_row gt_right" style="max-width: 71px">15</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 50px">5</td>
<td class="gt_row gt_right" style="max-width: 50px">210</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">57</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">90</td>
<td class="gt_row gt_right" style="max-width: 86px">60</td>
<td class="gt_row gt_right" style="max-width: 78px">1150</td>
<td class="gt_row gt_right" style="max-width: 71px">11</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">883</td>
<td class="gt_row gt_right" 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: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
<td class="gt_row gt_right" style="max-width: 78px">100</td>
<td class="gt_row gt_right" style="max-width: 86px">90</td>
<td class="gt_row gt_right gd-tbl-missing" style="max-width: 78px">None</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 50px">12</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: 50px">74</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">50</td>
<td class="gt_row gt_right" style="max-width: 86px">80</td>
<td class="gt_row gt_right" style="max-width: 78px">513</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 50px">7</td>
<td class="gt_row gt_right" style="max-width: 50px">310</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">68</td>
<td class="gt_row gt_right" style="max-width: 50px">2</td>
<td class="gt_row gt_right" style="max-width: 71px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">70</td>
<td class="gt_row gt_right" style="max-width: 86px">60</td>
<td class="gt_row gt_right" style="max-width: 78px">384</td>
<td class="gt_row gt_right" style="max-width: 71px">10</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">11</td>
<td class="gt_row gt_right" style="max-width: 50px">361</td>
<td class="gt_row gt_right" style="max-width: 63px">2</td>
<td class="gt_row gt_right" style="max-width: 50px">71</td>
<td class="gt_row gt_right" style="max-width: 50px">2</td>
<td class="gt_row gt_right" style="max-width: 71px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">60</td>
<td class="gt_row gt_right" style="max-width: 86px">80</td>
<td class="gt_row gt_right" style="max-width: 78px">538</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">225</td>
<td class="gt_row gt_right" style="max-width: 50px">32</td>
<td class="gt_row gt_right" style="max-width: 50px">105</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">75</td>
<td class="gt_row gt_right" style="max-width: 50px">2</td>
<td class="gt_row gt_right" style="max-width: 71px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">60</td>
<td class="gt_row gt_right" style="max-width: 86px">70</td>
<td class="gt_row gt_right" style="max-width: 78px">1025</td>
<td class="gt_row gt_right" style="max-width: 71px">5</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">226</td>
<td class="gt_row gt_right" style="max-width: 50px">6</td>
<td class="gt_row gt_right" style="max-width: 50px">174</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">66</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">90</td>
<td class="gt_row gt_right" style="max-width: 86px">100</td>
<td class="gt_row gt_right" style="max-width: 78px">1075</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">227</td>
<td class="gt_row gt_right" style="max-width: 50px">22</td>
<td class="gt_row gt_right" style="max-width: 50px">177</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 50px">58</td>
<td class="gt_row gt_right" style="max-width: 50px">2</td>
<td class="gt_row gt_right" style="max-width: 71px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">80</td>
<td class="gt_row gt_right" style="max-width: 86px">90</td>
<td class="gt_row gt_right" style="max-width: 78px">1060</td>
<td class="gt_row gt_right" style="max-width: 71px">0</td>
</tr>
</tbody>
</table>


There is a subtlety here that trips up almost everyone at least once. In this dataset, and in several others that come from R, the `status` column is coded `1` for censored and `2` for dead, rather than the `0`/`1` convention. Greenwood does not guess at this coding, and it will raise an error if you pass `1`/`2` values directly. You convert the column explicitly instead.


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


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


> **Warning: Check your event coding**
>
> Greenwood requires the event indicator to be boolean or `0`/`1`, where `1` means the event occurred. If your data uses another convention, such as `1` for censored and `2` for the event, convert it first with a comparison like `status == 2`. This deliberate strictness prevents a silent and serious error where censoring and events are swapped.


# Validation and reproducibility

The [Surv](../reference/Surv.md#greenwood.Surv) constructor validates its inputs immediately. Times must be finite and non-negative, the arrays must have matching lengths, and for the counting-process form each start must be strictly less than its stop. When something is wrong, you get a clear error at construction time rather than a confusing failure deep inside an estimator.


``` python
try:
    gw.Surv.right([5, -1, 3], [1, 1, 1])
except ValueError as error:
    print(error)
```


    `stop` times must be non-negative.


Another common data quality issue is when a subject's entry time equals their exit time (zero follow-up time). This indicates a data problem: if a subject has no time at risk, they cannot experience an event. Greenwood catches this early:


``` python
try:
    # Subject enters at time 1, exits at time 1 (follow-up = 0)
    gw.Surv.counting(start=[1, 0], stop=[1, 2], event=[1, 0])
except ValueError as error:
    print(error)
```


Output:

    Each `start` must be strictly less than its `stop`.

If you encounter this error, check your data:

- **Most common fix**: Exclude subjects with `start >= stop` (they have zero or negative follow-up time)
- **Data quality check**: Why do some subjects have identical entry and exit times? Is this a data entry error?
- **If intentional**: If the event truly occurred instantaneously, add a small epsilon to the stop time: `stop = start + 1e-10`

A response also serializes to and from JSON without loss, which is useful for saving an analysis input or moving it between systems. Calling [to_json()](../reference/Surv.md#greenwood.Surv.to_json) returns a plain string, so you can write it to a file or send it over the wire. Here is the serialized form of the `lung` response, truncated to its first stretch so you can see the shape of it.


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

text = y.to_json(indent=None)
text[:200]
```


    '{"type": "right", "stop": [306.0, 455.0, 1010.0, 210.0, 883.0, 1022.0, 310.0, 361.0, 218.0, 166.0, 170.0, 654.0, 728.0, 71.0, 567.0, 144.0, 613.0, 707.0, 61.0, 88.0, 301.0, 81.0, 624.0, 371.0, 394.0, '


That string carries the times and status codes together, which is exactly what a [Surv](../reference/Surv.md#greenwood.Surv) object holds. Reading it back with [from_json](../reference/Surv.md#greenwood.Surv.from_json) reconstructs an equivalent response, and we can confirm nothing was lost by comparing the JSON of the original and the restored object.


``` python
restored = gw.Surv.from_json(text)
print("round-trips exactly:", restored.to_json() == y.to_json())
```


    round-trips exactly: True


For working in memory rather than as text, [to_dict](../reference/Surv.md#greenwood.Surv.to_dict) and [from_dict](../reference/Surv.md#greenwood.Surv.from_dict) perform the same round-trip with a plain Python dictionary, and [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame) (optionally with a `format=` of `"polars"`, `"pandas"`, or `"pyarrow"`) gives a tidy, one-row-per-subject view that is convenient for inspection or export.


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


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="3" 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>Rows228Columns2
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="stop" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

stop

<em>f64</em>

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

status

<em>i64</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: 226px">306</td>
<td class="gt_row gt_right" style="max-width: 239px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 226px">455</td>
<td class="gt_row gt_right" style="max-width: 239px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 226px">1010</td>
<td class="gt_row gt_right" style="max-width: 239px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 226px">210</td>
<td class="gt_row gt_right" style="max-width: 239px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 226px">883</td>
<td class="gt_row gt_right" style="max-width: 239px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 226px">1022</td>
<td class="gt_row gt_right" style="max-width: 239px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 226px">310</td>
<td class="gt_row gt_right" style="max-width: 239px">1</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: 226px">361</td>
<td class="gt_row gt_right" style="max-width: 239px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">225</td>
<td class="gt_row gt_right" style="max-width: 226px">105</td>
<td class="gt_row gt_right" style="max-width: 239px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">226</td>
<td class="gt_row gt_right" style="max-width: 226px">174</td>
<td class="gt_row gt_right" style="max-width: 239px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">227</td>
<td class="gt_row gt_right" style="max-width: 226px">177</td>
<td class="gt_row gt_right" style="max-width: 239px">0</td>
</tr>
</tbody>
</table>


The dictionary form is the same structure [to_json](../reference/Surv.md#greenwood.Surv.to_json) serializes, so `gw.Surv.from_dict` rebuilds an equivalent response from it.


``` python
gw.Surv.from_dict(gw.Surv.right([5, 6, 4], event=[1, 0, 1]).to_dict())
```


    Surv(type=right, n=3, events=2)


# The risk-set table

Under every non-parametric estimate is one tabulation: at each distinct event time, how many subjects are still at risk, how many have the event, and how many are censored. This risk-set table is what Kaplan-Meier, the log-rank test, and Cox all build on, and you can compute it directly with [event_table](../reference/event_table.md#greenwood.event_table). It matches R's `survfit` tabulation.


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

gw.event_table(y).to_frame(format="polars")
```


<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="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>
</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: 100px">5</td>
<td class="gt_row gt_right" style="max-width: 113px">228</td>
<td class="gt_row gt_right" style="max-width: 121px">1</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 100px">11</td>
<td class="gt_row gt_right" style="max-width: 113px">227</td>
<td class="gt_row gt_right" style="max-width: 121px">3</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 100px">12</td>
<td class="gt_row gt_right" style="max-width: 113px">224</td>
<td class="gt_row gt_right" style="max-width: 121px">1</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 100px">13</td>
<td class="gt_row gt_right" style="max-width: 113px">223</td>
<td class="gt_row gt_right" style="max-width: 121px">2</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 100px">15</td>
<td class="gt_row gt_right" style="max-width: 113px">221</td>
<td class="gt_row gt_right" style="max-width: 121px">1</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 100px">26</td>
<td class="gt_row gt_right" style="max-width: 113px">220</td>
<td class="gt_row gt_right" style="max-width: 121px">1</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 100px">30</td>
<td class="gt_row gt_right" style="max-width: 113px">219</td>
<td class="gt_row gt_right" style="max-width: 121px">1</td>
<td class="gt_row gt_right" style="max-width: 128px">0</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: 100px">31</td>
<td class="gt_row gt_right" style="max-width: 113px">218</td>
<td class="gt_row gt_right" style="max-width: 121px">1</td>
<td class="gt_row gt_right" style="max-width: 128px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">183</td>
<td class="gt_row gt_right" style="max-width: 100px">965</td>
<td class="gt_row gt_right" style="max-width: 113px">3</td>
<td class="gt_row gt_right" style="max-width: 121px">0</td>
<td class="gt_row gt_right" style="max-width: 128px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">184</td>
<td class="gt_row gt_right" style="max-width: 100px">1010</td>
<td class="gt_row gt_right" style="max-width: 113px">2</td>
<td class="gt_row gt_right" style="max-width: 121px">0</td>
<td class="gt_row gt_right" style="max-width: 128px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">185</td>
<td class="gt_row gt_right" style="max-width: 100px">1022</td>
<td class="gt_row gt_right" style="max-width: 113px">1</td>
<td class="gt_row gt_right" style="max-width: 121px">0</td>
<td class="gt_row gt_right" style="max-width: 128px">1</td>
</tr>
</tbody>
</table>


Each row is a distinct time. The `n_risk` column counts the subjects under observation just before that time, `n_event` the events at it, and `n_censor` the censorings. Pass `group=` to tabulate within strata, which adds a `strata` column.


``` python
gw.event_table(y, group=lung["sex"]).to_frame(format="polars")
```


<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>Rows206Columns5
</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>
</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: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">11</td>
<td class="gt_row gt_right" style="max-width: 91px">138</td>
<td class="gt_row gt_right" style="max-width: 99px">3</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">12</td>
<td class="gt_row gt_right" style="max-width: 91px">135</td>
<td class="gt_row gt_right" style="max-width: 99px">1</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">13</td>
<td class="gt_row gt_right" style="max-width: 91px">134</td>
<td class="gt_row gt_right" style="max-width: 99px">2</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">15</td>
<td class="gt_row gt_right" style="max-width: 91px">132</td>
<td class="gt_row gt_right" style="max-width: 99px">1</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">26</td>
<td class="gt_row gt_right" style="max-width: 91px">131</td>
<td class="gt_row gt_right" style="max-width: 99px">1</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">30</td>
<td class="gt_row gt_right" style="max-width: 91px">130</td>
<td class="gt_row gt_right" style="max-width: 99px">1</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">31</td>
<td class="gt_row gt_right" style="max-width: 91px">129</td>
<td class="gt_row gt_right" style="max-width: 99px">1</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr class="gd-tbl-divider">
<td class="gt_row gt_right gd-tbl-rownum">7</td>
<td class="gt_row gt_left" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 78px">53</td>
<td class="gt_row gt_right" style="max-width: 91px">128</td>
<td class="gt_row gt_right" style="max-width: 99px">2</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">203</td>
<td class="gt_row gt_left" style="max-width: 91px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">765</td>
<td class="gt_row gt_right" style="max-width: 91px">3</td>
<td class="gt_row gt_right" style="max-width: 99px">1</td>
<td class="gt_row gt_right" style="max-width: 106px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">204</td>
<td class="gt_row gt_left" style="max-width: 91px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">821</td>
<td class="gt_row gt_right" style="max-width: 91px">2</td>
<td class="gt_row gt_right" style="max-width: 99px">0</td>
<td class="gt_row gt_right" style="max-width: 106px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">205</td>
<td class="gt_row gt_left" style="max-width: 91px">2</td>
<td class="gt_row gt_right" style="max-width: 78px">965</td>
<td class="gt_row gt_right" style="max-width: 91px">1</td>
<td class="gt_row gt_right" style="max-width: 99px">0</td>
<td class="gt_row gt_right" style="max-width: 106px">1</td>
</tr>
</tbody>
</table>


You rarely need this table directly, but it is the shared foundation the estimators are built on, and it is handy when you want to check counts by hand or drive a custom calculation.


# Next steps

You now know how to represent survival data and how to avoid the most common coding mistake. From here you can bring in your own data or start estimating.

- [Data sources and formats](data-sources.md) shows how to load data from pandas, Polars, and other backends, and lists the datasets bundled with the package.
- [Estimating survival with Kaplan-Meier](kaplan-meier.md) shows how to estimate and summarize the survival curve.
- [Comparing groups](comparing-groups.md) covers the log-rank family of tests.
- The [Quick start](quick-start.md) is a fast tour of everything if you prefer to skim first.
