# Outcome


A reusable description of a survival response: column names and expressions, no data.


Usage

``` python
Outcome()
```


An [Outcome](Outcome.md#greenwood.Outcome) is the structured form of a formula's left-hand side. [Outcome.surv()](Outcome.md#greenwood.Outcome.surv) takes the arguments of R's [Surv()](Surv.md#greenwood.Surv) and [Outcome.event_time()](Outcome.md#greenwood.Outcome.event_time) those of `event_time()`, each written as it would be inside a formula: a column name, or an expression such as `"status == 2"`. Pass the [Outcome](Outcome.md#greenwood.Outcome) to any estimator's fit method together with `data=`. The estimator reads every column it needs from that one frame, drops rows with missing values once (as R's `na.omit` does), evaluates the expressions, and builds the response with [Surv()](Surv.md#greenwood.Surv) or `event_time()`.

Because an [Outcome](Outcome.md#greenwood.Outcome) is made of names and expressions rather than DataFrame code, the same description works on any backend: pandas, Polars, PyArrow, DuckDB, or a lazy frame. That makes it, together with the equivalent formula strings, the recommended way to start from a data frame. Use [Surv()](Surv.md#greenwood.Surv) or `event_time()` directly when the values are already in hand.

The expressions understood here and in formulas are:

- a column name (backticks quote unusual names inside an expression),
- a comparison: `status == 2`, `status != 0`, `status in (1, 2)` (R's `%in% c(1, 2)` works), or `status not in (1, 2)`,
- `factor(x, levels, labels)`, R's factor, for a multi-state status whose first level means censored,
- `duration(start, end, unit="days")` for a time computed from two date columns.


## Examples

We'll use the bundled `lung` dataset, from a North Central Cancer Treatment Group trial in advanced lung cancer. `time` is days of follow-up and status is `1` (censored) or `2` (died). Here is what the dataset looks like:


<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Sans:wght@400;600&display=swap');
#gd-tbl-e77b600d .gt_table {
  display: table;
  border-collapse: collapse;
  table-layout: fixed;
  line-height: normal;
  margin-left: auto;
  margin-right: auto;
  color: #333333;
  font-size: 14px;
  font-weight: normal;
  font-style: normal;
  background-color: #FFFFFF;
  width: auto;
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #A8A8A8;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #A8A8A8;
}
#gd-tbl-e77b600d .gt_heading {
  background-color: #FFFFFF;
  text-align: left;
  border-bottom-color: #FFFFFF;
  border-left-style: none;
  border-right-style: none;
}
#gd-tbl-e77b600d .gt_title {
  color: #333333;
  font-size: 125%;
  font-weight: initial;
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  border-bottom-color: #FFFFFF;
  border-bottom-width: 0;
}
#gd-tbl-e77b600d .gt_subtitle {
  color: #333333;
  font-size: 85%;
  font-weight: initial;
  padding-top: 3px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  border-top-color: #FFFFFF;
  border-top-width: 0;
}
#gd-tbl-e77b600d .gt_bottom_border {
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
}
#gd-tbl-e77b600d .gt_col_headings {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
  border-bottom-style: solid;
  border-bottom-width: 2px;
  border-bottom-color: #D3D3D3;
  border-left-style: none;
  border-right-style: none;
}
#gd-tbl-e77b600d .gt_col_heading {
  color: #333333;
  background-color: #FFFFFF;
  font-size: 100%;
  font-weight: normal;
  text-transform: inherit;
  border-left-style: solid;
  border-left-width: 1px;
  border-left-color: #F2F2F2;
  border-right-style: solid;
  border-right-width: 1px;
  border-right-color: #F2F2F2;
  vertical-align: bottom;
  padding-top: 5px;
  padding-bottom: 5px;
  padding-left: 5px;
  padding-right: 5px;
  overflow-x: hidden;
}
#gd-tbl-e77b600d .gt_table_body {
  border-top-style: solid;
  border-top-width: 2px;
  border-top-color: #D3D3D3;
}
#gd-tbl-e77b600d .gt_row {
  padding-top: 4px;
  padding-bottom: 4px;
  padding-left: 5px;
  padding-right: 5px;
  margin: 10px;
  border-top-style: solid;
  border-top-width: 1px;
  border-top-color: #E9E9E9;
  border-left-style: solid;
  border-left-width: 1px;
  border-left-color: #E9E9E9;
  border-right-style: solid;
  border-right-width: 1px;
  border-right-color: #E9E9E9;
  vertical-align: middle;
  overflow-x: hidden;
}
#gd-tbl-e77b600d .gt_left { text-align: left; }
#gd-tbl-e77b600d .gt_center { text-align: center; }
#gd-tbl-e77b600d .gt_right { text-align: right; font-variant-numeric: tabular-nums; }
#gd-tbl-e77b600d .gt_font_normal { font-weight: normal; }
#gd-tbl-e77b600d .gt_font_bold { font-weight: bold; }
#gd-tbl-e77b600d .gt_font_italic { font-style: italic; }
#gd-tbl-e77b600d .gt_striped { background-color: rgba(128,128,128,0.05); }
#gd-tbl-e77b600d .gt_from_md > :first-child { margin-top: 0; }
#gd-tbl-e77b600d .gt_from_md > :last-child { margin-bottom: 0; }
/* Data cell font */
#gd-tbl-e77b600d .gt_table_body .gt_row {
  font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  color: #333333;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  height: 14px;
}
/* Column label font */
#gd-tbl-e77b600d .gt_col_heading {
  font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  color: #333333;
}
/* Row number styling */
#gd-tbl-e77b600d .gd-tbl-rownum {
  color: gray;
  font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10px;
  border-right: 2px solid rgba(102, 153, 204, 0.5) !important;
  text-align: right;
  padding-right: 6px;
  white-space: nowrap;
}
/* Head/tail divider */
#gd-tbl-e77b600d tr.gd-tbl-divider td,
#gd-tbl-e77b600d tr.gd-tbl-divider th {
  border-bottom: 2px solid rgba(102, 153, 204, 0.5);
}
/* Missing values */
#gd-tbl-e77b600d .gd-tbl-missing {
  color: #B22222 !important;
  background-color: rgba(255, 193, 193, 0.35);
}
/* Column label sub-elements */
#gd-tbl-e77b600d .gd-tbl-colname {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  padding-bottom: 0;
  margin-bottom: 0;
}
#gd-tbl-e77b600d .gd-tbl-dtype {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  padding-top: 0;
  margin-top: 0;
  color: #666666;
}
/* Header badge styling */
#gd-tbl-e77b600d .gd-tbl-badge {
  display: inline-block;
  padding: 2px 10px;
  font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  font-size: 10px;
  font-weight: bold;
  text-transform: uppercase;
  position: inherit;
}
#gd-tbl-e77b600d .gd-tbl-badge-rows-label {
  background-color: #eecbff;
  color: #333333;
  border: 1px solid #eecbff;
  margin-left: 5px;
}
#gd-tbl-e77b600d .gd-tbl-badge-rows-value {
  background-color: transparent;
  color: #333333;
  border: 1px solid #eecbff;
  margin-left: -4px;
  margin-right: 3px;
}
#gd-tbl-e77b600d .gd-tbl-badge-cols-label {
  background-color: #BDE7B4;
  color: #333333;
  border: 1px solid #BDE7B4;
}
#gd-tbl-e77b600d .gd-tbl-badge-cols-value {
  background-color: transparent;
  color: #333333;
  border: 1px solid #BDE7B4;
  margin-left: -4px;
}
/* Dark mode */
body.quarto-dark #gd-tbl-e77b600d .gt_table,
html.quarto-dark #gd-tbl-e77b600d .gt_table,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_table {
  background-color: #1a1a2e;
  color: #e0e0e0;
  border-top-color: #555;
  border-bottom-color: #555;
}
body.quarto-dark #gd-tbl-e77b600d .gt_heading,
html.quarto-dark #gd-tbl-e77b600d .gt_heading,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_heading {
  background-color: #1a1a2e;
  border-bottom-color: #1a1a2e;
}
body.quarto-dark #gd-tbl-e77b600d .gt_title,
html.quarto-dark #gd-tbl-e77b600d .gt_title,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_title {
  color: #e0e0e0;
}
body.quarto-dark #gd-tbl-e77b600d .gt_subtitle,
html.quarto-dark #gd-tbl-e77b600d .gt_subtitle,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_subtitle {
  color: #b0b0b0;
}
body.quarto-dark #gd-tbl-e77b600d .gt_col_headings,
html.quarto-dark #gd-tbl-e77b600d .gt_col_headings,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_col_headings {
  border-top-color: #444;
  border-bottom-color: #444;
}
body.quarto-dark #gd-tbl-e77b600d .gt_col_heading,
html.quarto-dark #gd-tbl-e77b600d .gt_col_heading,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_col_heading {
  color: #b0b0b0;
  background-color: #1a1a2e;
  border-left-color: #333;
  border-right-color: #333;
}
body.quarto-dark #gd-tbl-e77b600d .gt_table_body,
html.quarto-dark #gd-tbl-e77b600d .gt_table_body,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_table_body {
  border-top-color: #444;
}
body.quarto-dark #gd-tbl-e77b600d .gt_table_body .gt_row,
html.quarto-dark #gd-tbl-e77b600d .gt_table_body .gt_row,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_table_body .gt_row {
  color: #d0d0d0;
  border-top-color: #4b4b4b;
  border-bottom-color: #4b4b4b;
  border-left-color: #333;
  border-right-color: #333;
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-rownum,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-rownum,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-rownum {
  color: #888;
  border-right-color: rgba(102, 153, 204, 0.4) !important;
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-missing,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-missing,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-missing {
  color: #ff6b6b !important;
  background-color: rgba(60, 17, 24, 0.2);
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-dtype,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-dtype,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-dtype {
  color: #888;
}
body.quarto-dark #gd-tbl-e77b600d .gt_bottom_border,
html.quarto-dark #gd-tbl-e77b600d .gt_bottom_border,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gt_bottom_border {
  border-bottom-color: #555;
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-rows-label,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-rows-label,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-badge-rows-label {
  background-color: #3d2a4d;
  border-color: #3d2a4d;
  color: #e0c0ff;
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-rows-value,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-rows-value,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-badge-rows-value {
  border-color: #3d2a4d;
  color: #d0b0e0;
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-cols-label,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-cols-label,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-badge-cols-label {
  background-color: #2a4d2a;
  border-color: #2a4d2a;
  color: #b0e0b0;
}
body.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-cols-value,
html.quarto-dark #gd-tbl-e77b600d .gd-tbl-badge-cols-value,
:root[data-bs-theme="dark"] #gd-tbl-e77b600d .gd-tbl-badge-cols-value {
  border-color: #2a4d2a;
  color: #a0d0a0;
}
</style>

<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_heading">
<th colspan="11" class="gt_heading gt_subtitle gt_font_normal">lung: NCCTG advanced lung cancer, 228 patients</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 class="gd-tbl-divider">
<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">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>


Describe the endpoint once, with the same arguments [Surv()](Surv.md#greenwood.Surv) takes:


``` python
import greenwood as gw

death = gw.Outcome.surv(time="time", event="status == 2")
death
```


    Outcome.surv(time='time', event='status == 2')


Pass it to estimators along with the frame. Column names work for covariates and `by=` too:


``` python
lung = gw.load_dataset("lung")

cox = gw.CoxPH().fit(death, covariates=["age", "sex"], data=lung)
km = gw.KaplanMeier().fit(death, by="sex", data=lung)
cox
```


    CoxPH (Cox proportional hazards model, ties='efron')

            coef  exp(coef)  se(coef)       z         p
    age  0.01705      1.017  0.009223   1.848   0.06459
    sex  -0.5132     0.5986    0.1675  -3.065  0.002178

    n = 228, events = 165
    Likelihood ratio test = 14.12 on 2 df, p = 0.0008574


The same response written as a formula parses to an equal [Outcome](Outcome.md#greenwood.Outcome):


``` python
gw.Outcome.from_formula(formula="Surv(time, status == 2)") == death
```


    True


## Attributes

| Name | Description |
|----|----|
| [arguments](#arguments) | The arguments of the response, by name, written as they would be in a formula. |
| [column_names](#column_names) | The data columns this response reads, in the order they first appear. |
| [endpoints](#endpoints) | For a `first_event` outcome, each endpoint's time and event expressions. |
| [kind](#kind) | Which function builds the response: `"surv"`, `"event_time"`, or `"first_event"`. |
| [origin](#origin) | The `origin=` passed to [Surv()](Surv.md#greenwood.Surv): a time subtracted from every time when bound. |
| [type](#type) | The `type=` passed to [Surv()](Surv.md#greenwood.Surv), or `None` to let it be inferred when the data is bound. |

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


### arguments


The arguments of the response, by name, written as they would be in a formula.


`arguments: dict[str, str]`


For a [surv](Outcome.md#greenwood.Outcome.surv) outcome the names are those of [Surv()](Surv.md#greenwood.Surv) that were given (`time=`, `time2=`, `event=`), and for an `event_time` outcome those of `event_time()` (`time=`, `status=`, `time_max=`). Each value is a column name or an expression such as `"status == 2"`. As in R, `Surv(time, x)` without `event=` reads `x` as the status, so it is listed as `event=`. For a `first_event` outcome the endpoints are in [endpoints](Outcome.md#greenwood.Outcome.endpoints), and this holds only `censor_at=` and `start=` when they were given. The type and origin have their own properties, [Outcome.type](Outcome.md#greenwood.Outcome.type) and [Outcome.origin](Outcome.md#greenwood.Outcome.origin).


#### Examples


``` python
import greenwood as gw

gw.Outcome.from_formula(formula="Surv(tstart, tstop, status == 2)").arguments
```


    {'time': 'tstart', 'time2': 'tstop', 'event': 'status == 2'}


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


### column_names


The data columns this response reads, in the order they first appear.


`column_names: tuple[str, …]`


These are the columns named by every argument, including both columns of a [duration()](duration.md#greenwood.duration) and the column inside a comparison or `factor()`. When a model is fitted from an [Outcome](Outcome.md#greenwood.Outcome) and `data=`, rows with a missing value in any of these columns are dropped (together with any covariate columns) before the response is built.


#### Examples


``` python
import greenwood as gw

died = gw.Outcome.from_formula(formula="Surv(duration(enroll, exit), outcome == 'died')")
died.column_names
```


    ('enroll', 'exit', 'outcome')


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


### endpoints


For a `first_event` outcome, each endpoint's time and event expressions.


`endpoints: dict[str, tuple[str, str]] | None`


The endpoints are in priority order, which decides ties: when two endpoints happen at the same time, the first one listed wins. Each value is a `(time, event)` pair of expression texts, as passed to [Outcome.first_event()](Outcome.md#greenwood.Outcome.first_event). `None` for other kinds of outcome.


#### Examples


``` python
import greenwood as gw

cr = gw.Outcome.first_event(
    endpoints={"pcm": ("ptime", "pstat"), "death": ("futime", "death == 1")}
)
cr.endpoints
```


    {'pcm': ('ptime', 'pstat'), 'death': ('futime', 'death == 1')}


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


### kind


Which function builds the response: `"surv"`, `"event_time"`, or `"first_event"`.


`kind: str`


An [Outcome](Outcome.md#greenwood.Outcome) from [Outcome.surv()](Outcome.md#greenwood.Outcome.surv) (or a `Surv(...)` formula) is bound with [Surv()](Surv.md#greenwood.Surv). One from [Outcome.event_time()](Outcome.md#greenwood.Outcome.event_time) (or an `event_time(...)` formula) is bound with `event_time()` and then converted with [as_surv()](as_surv.md#greenwood.as_surv). One from [Outcome.first_event()](Outcome.md#greenwood.Outcome.first_event) is bound with `first_event()`. Every kind produces a [Surv](Surv.md#greenwood.Surv) when bound.


#### Examples


``` python
import greenwood as gw

gw.Outcome.from_formula(formula="event_time(time, code)").kind
```


    'event_time'


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


### origin


The `origin=` passed to [Surv()](Surv.md#greenwood.Surv): a time subtracted from every time when bound.


`origin: float`


`0` (the default) leaves the times unchanged. A non-zero origin shifts every time column, as R's `Surv(origin=)` does. Always `0` for `event_time` and `first_event` outcomes.


#### Examples


``` python
import greenwood as gw

gw.Outcome.from_formula(formula="Surv(age_at_exit, died, origin=40)").origin
```


    40.0


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


### type


The `type=` passed to [Surv()](Surv.md#greenwood.Surv), or `None` to let it be inferred when the data is bound.


`type: str | None`


One of R's types: `"right"`, `"left"`, `"interval"`, `"counting"`, `"interval2"`, or `"mstate"`. `None`, the usual case, means [Surv()](Surv.md#greenwood.Surv) infers right-censored or counting-process data from the arguments, or multi-state data from a `factor()` status. The type of the bound response is reported by [Surv.type](Surv.md#greenwood.Surv.type). Always `None` for `event_time` and `first_event` outcomes.


#### Examples


``` python
import greenwood as gw

gw.Outcome.surv(time="lower", time2="upper", type="interval2").type
```


    'interval2'


## Methods

| Name | Description |
|----|----|
| [bind()](#bind) | Build the [Surv](Surv.md#greenwood.Surv) response from the columns of `data`. |
| [event_time()](#event_time) | Describe an `event_time()` response by column names. |
| [first_event()](#first_event) | Describe a `first_event()` competing-risks response by column names. |
| [from_formula()](#from_formula) | Parse a formula response such as `"Surv(time, status == 2)"` into an [Outcome](Outcome.md#greenwood.Outcome). |
| [surv()](#surv) | Describe a [Surv()](Surv.md#greenwood.Surv) response by column names and expressions. |

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


### bind()


Build the [Surv](Surv.md#greenwood.Surv) response from the columns of `data`.


Usage

``` python
bind(data)
```


Each argument is evaluated against `data` on its own backend, and the results go through [Surv()](Surv.md#greenwood.Surv), through `event_time()` and then [as_surv()](as_surv.md#greenwood.as_surv), or through `first_event()`, depending on [kind](Outcome.md#greenwood.Outcome.kind). [bind()](Outcome.md#greenwood.Outcome.bind) is what an estimator does internally when it is given an [Outcome](Outcome.md#greenwood.Outcome) and `data=`, apart from one difference. [bind()](Outcome.md#greenwood.Outcome.bind) keeps rows with missing values, as [Surv()](Surv.md#greenwood.Surv) does, while fitting first drops rows with a missing value in any column the model uses, so the response and the covariates stay aligned.

Use [bind()](Outcome.md#greenwood.Outcome.bind) to inspect the response a description produces, or to pass the [Surv](Surv.md#greenwood.Surv) to a function that takes values rather than an [Outcome](Outcome.md#greenwood.Outcome).


#### Parameters


`data: Any`  
A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) or a mapping of column names to values. Only the columns the [Outcome](Outcome.md#greenwood.Outcome) names are read.


#### Returns


`Surv`  
The response for this data.


#### Raises


`KeyError`  
If a column named by the [Outcome](Outcome.md#greenwood.Outcome) is not in `data`. The message suggests the closest column name.

`TypeError`  
If `data` is not a data frame or a mapping, or [Surv()](Surv.md#greenwood.Surv) or `event_time()` rejects the values (for example, a text status column used without a comparison).

`ValueError`  
If [Surv()](Surv.md#greenwood.Surv), `event_time()`, or `first_event()` rejects the values.


#### Warns


`UserWarning`  
When [Surv()](Surv.md#greenwood.Surv) turns invalid values into missing ones, such as a status code R doesn't accept. The affected rows are missing in the result.


#### Examples

We'll use the bundled `lung` dataset, where status is `1` (censored) or `2` (died). R's `1`/`2` coding passes through:


``` python
import greenwood as gw

lung = gw.load_dataset("lung")
death = gw.Outcome.surv(time="time", event="status")

y = death.bind(lung)
y
```


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


The bound response is an ordinary [Surv](Surv.md#greenwood.Surv), with R's columns:


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


shape: (3, 2)

| time   | status |
|--------|--------|
| f64    | f64    |
| 306.0  | 1.0    |
| 455.0  | 1.0    |
| 1010.0 | 0.0    |


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


### event_time()


Describe an `event_time()` response by column names.


Usage

``` python
event_time(
    time,
    status,
    time_max=None,
)
```


`event_time()` writes each observation as a time and a one-letter status: `"e"` (exact event), `"r"` (right-censored), `"l"` (left-censored), or `"i"` (interval-censored, with the upper bound in a second time column). This constructor names the columns that hold those values. When the [Outcome](Outcome.md#greenwood.Outcome) is bound to data, the columns are passed to `event_time()`, so its validation applies, and the result is converted to a [Surv](Surv.md#greenwood.Surv) with [as_surv()](as_surv.md#greenwood.as_surv).

This is the same description that the formula response `"event_time(time, status)"` gives.


#### Parameters


`time: str | Duration`  
The time column (the lower bound for interval-censored rows), or `duration(...)` for a time computed from two date columns.

`status: str`  
The column of status codes: `"e"`, `"r"`, `"l"`, or `"i"`.

`time_max: str | Duration | None = None`  
The column of interval upper bounds, missing for rows that are not interval-censored. Needed only when some rows are interval-censored.


#### Returns


`Outcome`  
A data-free description of the response.


#### Raises


`TypeError`  
If an argument is not a string or a `gw.duration()` object.


#### Examples

A small study where the event time is known for some subjects and only bracketed for others:


``` python
import greenwood as gw
import polars as pl

visits = pl.DataFrame({
    "time": [7.0, 5.0, 3.0, 2.0],
    "code": ["e", "r", "l", "i"],
    "upper": [None, None, None, 4.0],
})

observed = gw.Outcome.event_time(time="time", status="code", time_max="upper")
observed.bind(visits)
```


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


Interval-censored responses fit with [Turnbull](Turnbull.md#greenwood.Turnbull):


``` python
gw.Turnbull().fit(observed, data=visits)
```


    Turnbull (self-consistent NPMLE for interval-censored data)

         n  atoms  ambiguous  iters  converged
    all  1      1          1      1       True

    (3 observations deleted due to missingness)


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


### first_event()


Describe a `first_event()` competing-risks response by column names.


Usage

``` python
first_event(
    endpoints,
    *,
    censor_at=None,
    start=None,
)
```


Some datasets record each endpoint in its own pair of columns: a time and whether the endpoint was observed. A competing-risks analysis needs one time and one cause per subject: the first endpoint observed, or censoring if none was. This constructor names the column pairs. When the [Outcome](Outcome.md#greenwood.Outcome) is bound to data, the pairs are evaluated and passed to `first_event()`, which keeps the earliest observed event for each row.


#### Parameters


`endpoints: Mapping[str, tuple[str | Duration, str]]`  
A mapping of state name to a `(time, event)` pair, in priority order: when two endpoints happen at the same time, the one listed first wins. The time is a column name (or `duration(...)`), and the event is a column or a comparison such as `"pstat == 1"`.

`censor_at: str | Duration | None = None`  
The column holding the time at which rows with no observed event are censored. The default is the latest of the endpoint times.

`start: str | Duration | None = None`  
The column of entry times, for late entry. Gives a multi-state counting-process response.


#### Returns


`Outcome`  
A data-free description of the response.


#### Raises


`TypeError`  
If a time or event is not a string or a `gw.duration()` object.

`ValueError`  
If [endpoints](Outcome.md#greenwood.Outcome.endpoints) is empty, or an endpoint is not a `(time, event)` pair.


#### Examples

We'll use the bundled `mgus2` dataset, which follows patients with monoclonal gammopathy. `ptime` and `pstat` record progression to plasma-cell malignancy (PCM), and `futime` and `death` record death:


``` python
import greenwood as gw

cr = gw.Outcome.first_event(
    endpoints={"pcm": ("ptime", "pstat"), "death": ("futime", "death")}
)
cr
```


    Outcome.first_event(endpoints={'pcm': ('ptime', 'pstat'), 'death': ('futime', 'death')})


Fit the cumulative incidence of each cause:


``` python
mgus2 = gw.load_dataset("mgus2")

gw.AalenJohansen().fit(cr, data=mgus2)
```


    AalenJohansen (Aalen-Johansen cumulative incidence)

    states: pcm, death
    n = 1384

           final CIF
    pcm       0.1613
    death     0.8387


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


### from_formula()


Parse a formula response such as `"Surv(time, status == 2)"` into an [Outcome](Outcome.md#greenwood.Outcome).


Usage

``` python
from_formula(formula)
```


The response is a call to [Surv()](Surv.md#greenwood.Surv) or `event_time()`, with the arguments R's functions take: `Surv(time, time2, event, type=, origin=)` and `event_time(time, status, time_max)`. Arguments bind by position or by name, as in R. Each one is a column name or one of the expressions listed under [Outcome](Outcome.md#greenwood.Outcome). Column names that aren't valid Python identifiers are quoted with backticks, and dotted R names such as `ph.ecog` work without quoting.

The formula is parsed, not evaluated: only column names, literal values, comparisons, and the functions [duration()](duration.md#greenwood.duration), `factor()`, and `c()` are accepted. Estimators parse full formulas the same way, so `fit("Surv(time, status == 2) ~ age", data=df)` uses exactly this response.


#### Parameters


`formula: str`  
The response, optionally followed by `~ 1`. To include covariates, pass the full formula to an estimator's fit method instead.


#### Returns


`Outcome`  
A data-free description of the response, equal to the one built with [surv()](Outcome.md#greenwood.Outcome.surv) or [event_time()](Outcome.md#greenwood.Outcome.event_time) from the same arguments.


#### Raises


`ValueError`  
If the formula can't be parsed, the response is not a call to [Surv()](Surv.md#greenwood.Surv) or `event_time()`, it has an unknown or repeated argument (such as the removed `event_value=`), it has a right-hand side other than `1`, or an expression is not supported.


#### Examples


``` python
import greenwood as gw

gw.Outcome.from_formula(formula="Surv(time, status == 2)")
```


    Outcome.surv(time='time', event='status == 2')


It equals the [Outcome](Outcome.md#greenwood.Outcome) built from the same arguments:


``` python
gw.Outcome.from_formula(formula="Surv(time, status == 2)") == gw.Outcome.surv(
    time="time", event="status == 2"
)
```


    True


R's other forms work too:


``` python
gw.Outcome.from_formula(formula="Surv(tstart, tstop, status %in% c(1, 2))")
```


    Outcome.surv(time='tstart', time2='tstop', event='status in (1, 2)')


``` python
gw.Outcome.from_formula(formula="event_time(time, code, upper)")
```


    Outcome.event_time(time='time', status='code', time_max='upper')


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


### surv()


Describe a [Surv()](Surv.md#greenwood.Surv) response by column names and expressions.


Usage

``` python
surv(
    time,
    time2=None,
    event=None,
    *,
    type=None,
    origin=0.0,
)
```


The arguments are those of [Surv()](Surv.md#greenwood.Surv) (and of R's `survival::Surv()`), each written as it would be inside a formula: a column name, or an expression such as `"status == 2"`. Nothing is read yet. When the [Outcome](Outcome.md#greenwood.Outcome) is bound to data, at fit time or with [bind()](Outcome.md#greenwood.Outcome.bind), each argument is evaluated on the data's own backend and the results are passed to [Surv()](Surv.md#greenwood.Surv). R's rules then apply unchanged:

- `1`/`2` status coding passes through, and other codings are written as a comparison.
- A second argument without `event=` is read as the status, so `Outcome.surv(time="t", time2="d")` means the same as `Outcome.surv(time="t", event="d")`.
- `type=` selects left-censored or interval data. Without it, the type is inferred: right-censored from a time and a status, counting-process from a start and a stop time, and multi-state from a `factor()` status.

This is the same description that the formula response `"Surv(time, status == 2)"` gives, and the two compare equal.


#### Parameters


`time: str | Duration`  
The time column: the follow-up time for right-censored data, the start time for counting-process data, or the lower bound for interval data. Can also be `duration(...)` (or a `gw.duration()` object) for a time computed from two date columns.

`time2: str | Duration | None = None`  
The stop time for counting-process data, or the upper bound for interval data. Without `event=`, a second argument is the status, as in R.

`event: str | None = None`  
The status: a column name, a comparison such as `"status == 2"` or `"status %in% c(1, 2)"`, or `"factor(cause, c(0, 1, 2), c('censor', 'pcm', 'death'))"` for a multi-state response whose first label means censored.

`type: str | None = None`  
As in [Surv()](Surv.md#greenwood.Surv): `"right"`, `"left"`, `"interval"`, `"counting"`, `"interval2"`, or `"mstate"`. The default (`None`) infers the type from the arguments when the data is bound.

`origin: float = ``0.0`  
A time subtracted from every time when the data is bound, as in [Surv()](Surv.md#greenwood.Surv).


#### Returns


`Outcome`  
A data-free description of the response.


#### Raises


`TypeError`  
If an argument is not a string or a `gw.duration()` object. To build a response from values instead of column names, use [Surv()](Surv.md#greenwood.Surv).

`ValueError`  
If `type=` is not one of the types above, an expression uses an unsupported operator, or the status is a [duration()](duration.md#greenwood.duration).


#### Examples

We'll use the bundled `pbc` dataset, from a Mayo Clinic trial in primary biliary cholangitis. `time` is days of follow-up and status is `0` (censored), `1` (transplant), or `2` (died). Death is the event, so the status is a comparison:


``` python
import greenwood as gw

death = gw.Outcome.surv(time="time", event="status == 2")
death
```


    Outcome.surv(time='time', event='status == 2')


Pass it to an estimator together with the data. Rows with a missing value in any column used are dropped first:


``` python
pbc = gw.load_dataset("pbc")

gw.KaplanMeier().fit(death, by="trt", data=pbc)
```


    KaplanMeier (Kaplan-Meier survival estimate)

             n  events  median  0.95LCL  0.95UCL
    trt=1  158      65    3282     2583       NA
    trt=2  154      60    3428     3090       NA

    (106 observations deleted due to missingness)


A `factor()` status gives a multi-state response, here with transplant and death as competing events:


``` python
competing = gw.Outcome.surv(
    time="time",
    event="factor(status, c(0, 1, 2), c('censor', 'transplant', 'death'))",
)
competing.bind(pbc)
```


    Surv(type=mright, n=418, events=186, states=('transplant', 'death'))
