# Competing risks

So far every method has assumed a single kind of event. Often that is not realistic. A patient with a precancerous condition might progress to cancer, but they might also die of something else first, and death from another cause prevents the progression from ever being observed. These are competing risks: mutually exclusive event types where the occurrence of one removes the possibility of the others. Analyzing them with ordinary survival methods, one event type at a time, gives biased and even nonsensical results. This page explains why, and shows the two standard tools Greenwood provides.


*\[Rich HTML output -- view on the documentation site\]*


Figure 1: A competing-risks model: from an initial state, a subject can move to exactly one of several absorbing event states.

We use the bundled `mgus2` dataset, which follows patients with monoclonal gammopathy. The competing events are progression to a plasma-cell malignancy and death without progression. We first build the competing-risks endpoint from two intermediate arrays: a single event time and a cause code that is 0 for censored, 1 for progression, and 2 for death.


``` python
import numpy as np
import greenwood as gw

mg = gw.load_dataset("mgus2", backend="polars")

etime = np.where(mg["pstat"] == 1, mg["ptime"], mg["futime"])
cause = np.where(mg["pstat"] == 1, 1, 2 * mg["death"])   # 0 censor, 1 pcm, 2 death
```


The `etime` array picks each subject's event time: the progression time `ptime` when the subject progressed (`pstat == 1`), and otherwise the follow-up time `futime`. Because a subject can experience only the first of the competing events, one time per subject is all we need. Here are the first several values, in months.


``` python
etime[:8]
```


    array([ 30,  25,  46,  92,   8,   4, 151,   2])


The `cause` array records which event that time refers to, using the coding noted in the comment above. Where a subject progressed it is 1 (`pcm`); otherwise it is `2 * death`, which is 2 when the subject died without progressing and 0 when the subject was censored. Reading `etime` and `cause` position by position gives each subject's (time, cause) pair.


``` python
cause[:8]
```


    array([2, 2, 2, 2, 2, 2, 2, 2])


We pass both arrays to `gw.Surv.multistate`, mapping cause code 1 to `pcm` and code 2 to `death`, with code 0 left as censoring. The resulting response object shows the event times alongside their decoded states.


``` python
y = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))
y
```


    Surv(type=right, n=1384, events=975, states=('pcm', 'death'))


# Cumulative incidence, not one-minus-survival

The intuitive but wrong approach is to treat one cause as the event, censor the other, run Kaplan-Meier, and report one minus the survival curve as the probability of that cause. This overestimates the probability, sometimes badly, because it treats subjects who died of the other cause as if they could still progress. The correct quantity is the cumulative incidence function, which gives the probability of experiencing a specific cause by a given time while accounting for the competing cause.

The Aalen-Johansen estimator computes it. You fit it to the multi-state response, and it returns a curve for every cause.


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

etime = np.where(mg["pstat"] == 1, mg["ptime"], mg["futime"])
cause = np.where(mg["pstat"] == 1, 1, 2 * mg["death"])   # 0 censor, 1 pcm, 2 death
y = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))

aj = gw.AalenJohansen().fit(y)
aj
```


    AalenJohansen (Aalen-Johansen cumulative incidence)

    states: pcm, death
    n = 1384

           final CIF
    pcm       0.1613
    death     0.8387


The fitted estimator holds a cumulative incidence curve for each cause. To inspect it we call [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame) and take the first couple of rows within each cause, so both curves are visible side by side.


``` python
aj.to_frame(format="polars").group_by("cause").head(2)
```


<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>Rows4Columns7
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="cause" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

cause

<em>str</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="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: 55px">pcm</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 63px">1384</td>
<td class="gt_row gt_right" style="max-width: 131px">0</td>
<td class="gt_row gt_right" style="max-width: 131px">0</td>
<td class="gt_row gt_right" style="max-width: 124px">0</td>
<td class="gt_row gt_right" style="max-width: 131px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 55px">pcm</td>
<td class="gt_row gt_right" style="max-width: 50px">2</td>
<td class="gt_row gt_right" style="max-width: 63px">1341</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00144616432392</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00102185289691</td>
<td class="gt_row gt_right" style="max-width: 124px">0</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00344895919936</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 55px">death</td>
<td class="gt_row gt_right" style="max-width: 50px">1</td>
<td class="gt_row gt_right" style="max-width: 63px">1384</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0303468208092</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00461101747295</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0213093926302</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0393842489883</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_left" style="max-width: 55px">death</td>
<td class="gt_row gt_right" style="max-width: 50px">2</td>
<td class="gt_row gt_right" style="max-width: 63px">1341</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0505931213442</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00589210798256</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0390448019053</td>
<td class="gt_row gt_right" style="max-width: 131px">0.062141440783</td>
</tr>
</tbody>
</table>


Each cause has its own cumulative incidence estimate, standard error, and confidence interval at every event time. Because the causes are competing, their cumulative incidences plus the event-free probability sum to one at every time, which is exactly the accounting that one-minus-Kaplan-Meier gets wrong.

> **Warning: Do not use one-minus-Kaplan-Meier for a competing cause**
>
> Censoring the competing event and applying Kaplan-Meier treats those subjects as if they remained at risk for the cause of interest, which they do not. Always use the cumulative incidence function for competing-risks probabilities.

You can stratify by a grouping variable, just as with Kaplan-Meier, to compare cumulative incidence across groups.


``` python
aj_sex = gw.AalenJohansen().fit(y, by=mg["sex"])
aj_sex
```


    AalenJohansen (Aalen-Johansen cumulative incidence)

    states: pcm, death
    strata: 2


The stratified fit carries a separate set of curves for each level of `sex`. Here we pull the `pcm` cause and keep the last row within each stratum, which is the cumulative incidence of progression at the final event time in that group.


``` python
import polars as pl

aj_sex.to_frame(format="polars").filter(pl.col("cause") == "pcm").group_by("strata").tail(1)
```


<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>Rows2Columns8
</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>str</em>

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

cause

<em>str</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="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">M</td>
<td class="gt_row gt_left" style="max-width: 55px">pcm</td>
<td class="gt_row gt_right" style="max-width: 50px">424</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 117px">0.104460229977</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0158156196773</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0734621850159</td>
<td class="gt_row gt_right" style="max-width: 117px">0.135458274937</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 63px">F</td>
<td class="gt_row gt_left" style="max-width: 55px">pcm</td>
<td class="gt_row gt_right" style="max-width: 50px">394</td>
<td class="gt_row gt_right" style="max-width: 63px">1</td>
<td class="gt_row gt_right" style="max-width: 117px">0.198554321073</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0399976113701</td>
<td class="gt_row gt_right" style="max-width: 124px">0.12016044332</td>
<td class="gt_row gt_right" style="max-width: 117px">0.276948198826</td>
</tr>
</tbody>
</table>


# Modeling with the Fine-Gray model

The cumulative incidence function describes groups. To model how covariates affect the incidence of a specific cause, use the Fine-Gray model. It is a regression on the subdistribution hazard, constructed so that its coefficients describe the effect of covariates on the cumulative incidence itself, which is usually what you want to report.


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

etime = np.where(mg["pstat"] == 1, mg["ptime"], mg["futime"])
cause = np.where(mg["pstat"] == 1, 1, 2 * mg["death"])   # 0 censor, 1 pcm, 2 death
y = gw.Surv.multistate(etime, event=cause, states=("pcm", "death"))

fg = gw.FineGray("pcm").fit(y, mg[["age", "sex"]])
fg
```


    FineGray (Fine-Gray subdistribution hazard model, cause='pcm')

             coef  exp(coef)  se(coef)       z         p
    age   -0.0173     0.9828  0.005701  -3.035  0.002408
    sexM  -0.2597     0.7713    0.1856  -1.399    0.1617

    n = 1384, events = 115
    Standard errors: robust (clustered)


We name `pcm` as the target cause and fit against age and sex. To read the fitted model we tidy it into a coefficient table, asking for exponentiated estimates so the numbers are on the hazard-ratio scale rather than the log scale.


``` python
gw.tidy(fg, exponentiate=True, 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>Rows2Columns7
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="term" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

term

<em>str</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="statistic" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

statistic

<em>f64</em>

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

p_value

<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: 50px">age</td>
<td class="gt_row gt_right" style="max-width: 117px">0.982848064921</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00570095802298</td>
<td class="gt_row gt_right" style="max-width: 117px">-3.03470633443</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00240769998927</td>
<td class="gt_row gt_right" style="max-width: 117px">0.9719271695</td>
<td class="gt_row gt_right" style="max-width: 117px">0.993891671139</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 50px">sexM</td>
<td class="gt_row gt_right" style="max-width: 117px">0.771282230528</td>
<td class="gt_row gt_right" style="max-width: 131px">0.185579308546</td>
<td class="gt_row gt_right" style="max-width: 117px">-1.39940662934</td>
<td class="gt_row gt_right" style="max-width: 131px">0.161691080051</td>
<td class="gt_row gt_right" style="max-width: 117px">0.536102622861</td>
<td class="gt_row gt_right" style="max-width: 117px">1.1096313537</td>
</tr>
</tbody>
</table>


The exponentiated coefficients are subdistribution hazard ratios. A value above 1 means the covariate increases the cumulative incidence of the target cause, `pcm` here, over time. The model reports clustered robust standard errors by default, which are appropriate given the inverse-probability weighting it uses internally.


## Modeling multiple competing causes

In a competing-risks study with more than one cause of interest, you can fit separate Fine-Gray models, one for each cause, to compare the covariate effects across causes.


``` python
import pandas as pd

# Fit FineGray models for both competing causes
fg_pcm = gw.FineGray("pcm").fit(y, mg[["age", "sex"]])
fg_death = gw.FineGray("death").fit(y, mg[["age", "sex"]])

# Compare the hazard ratios side-by-side
pcm_tidy = gw.tidy(fg_pcm, exponentiate=True, format="pandas")
death_tidy = gw.tidy(fg_death, exponentiate=True, format="pandas")

pcm_tidy["cause"] = "PCM"
death_tidy["cause"] = "Death"

pd.concat([pcm_tidy, death_tidy])
```


<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: #150458; color: #FFFFFF; border: 1px solid #150458; margin-right: 8px;">Pandas</span>Rows4Columns8
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="term" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

term

<em>str</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="statistic" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

statistic

<em>f64</em>

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

p_value

<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="cause" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

cause

<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_left" style="max-width: 50px">age</td>
<td class="gt_row gt_right" style="max-width: 117px">0.982848064921</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00570095802298</td>
<td class="gt_row gt_right" style="max-width: 117px">-3.03470633443</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00240769998927</td>
<td class="gt_row gt_right" style="max-width: 117px">0.9719271695</td>
<td class="gt_row gt_right" style="max-width: 117px">0.993891671139</td>
<td class="gt_row gt_left" style="max-width: 55px">PCM</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 50px">sexM</td>
<td class="gt_row gt_right" style="max-width: 117px">0.771282230528</td>
<td class="gt_row gt_right" style="max-width: 131px">0.185579308546</td>
<td class="gt_row gt_right" style="max-width: 117px">-1.39940662934</td>
<td class="gt_row gt_right" style="max-width: 138px">0.161691080051</td>
<td class="gt_row gt_right" style="max-width: 117px">0.536102622861</td>
<td class="gt_row gt_right" style="max-width: 117px">1.1096313537</td>
<td class="gt_row gt_left" style="max-width: 55px">PCM</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 50px">age</td>
<td class="gt_row gt_right" style="max-width: 117px">1.06035605364</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0036793490261</td>
<td class="gt_row gt_right" style="max-width: 117px">15.9280217679</td>
<td class="gt_row gt_right" style="max-width: 138px">4.04967555101e-57</td>
<td class="gt_row gt_right" style="max-width: 117px">1.05273691623</td>
<td class="gt_row gt_right" style="max-width: 117px">1.06803033421</td>
<td class="gt_row gt_left" style="max-width: 55px">Death</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_left" style="max-width: 50px">sexM</td>
<td class="gt_row gt_right" style="max-width: 117px">1.44900400068</td>
<td class="gt_row gt_right" style="max-width: 131px">0.0667809170816</td>
<td class="gt_row gt_right" style="max-width: 117px">5.55362879898</td>
<td class="gt_row gt_right" style="max-width: 138px">2.79799326688e-08</td>
<td class="gt_row gt_right" style="max-width: 117px">1.27123419034</td>
<td class="gt_row gt_right" style="max-width: 117px">1.65163320019</td>
<td class="gt_row gt_left" style="max-width: 55px">Death</td>
</tr>
</tbody>
</table>


This comparison reveals how covariates affect each cause differently. A covariate might increase the incidence of progression to PCM while decreasing the incidence of death from other causes (or vice versa) which both tell important clinical stories.

> **Note: Two kinds of hazard, two kinds of question**
>
> A cause-specific Cox model answers "what drives the rate of this event among those still at risk?", while the Fine-Gray model answers "what drives the cumulative probability of this event?". They can point in different directions for the same covariate, and both can be correct. Decide which question you are asking before choosing the model.


# Next steps

You can now estimate and model competing-risks data correctly.

- [Multi-state models](multi-state.md) generalize competing risks to settings where subjects move between several states over time.
- [Visualizing survival](visualization.md) shows the grammar-of-graphics approach you can reuse to plot cumulative incidence curves.
- Revisit [Survival data](survival-data.md) for the details of the multi-state response.
