# Cox model diagnostics

Fitting a Cox model is only half the work. Before you trust its hazard ratios, you should check that its central assumption holds, look at residuals for influential or poorly fit observations, and understand what the model predicts. This page covers the proportional hazards test, residuals, baseline hazard and survival prediction, stratification, robust standard errors, and the concordance index. Together these turn a fitted model into a defensible one.

We begin with the `lung` outcomes. The response `y` is a [Surv](../reference/Surv.md#greenwood.Surv) object pairing each follow-up time with an event indicator, and it is the target every model on this page is fit against.


``` python
import greenwood as gw

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

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


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


The printed response records 165 events among 228 subjects, with a `+` marking each censored observation. We fit a Cox model to it using `age` and `sex` as covariates. This is the model we diagnose throughout the page, so we fit it once here and reuse it below.


``` python
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])
```


# Checking the proportional hazards assumption

The Cox model assumes each covariate's effect on the hazard is constant over time. When this fails, for example if a treatment helps early but not late, the reported hazard ratio is a misleading average. The Grambsch-Therneau test checks the assumption by looking for a trend in the scaled Schoenfeld residuals against time. A small p-value is evidence that the assumption is violated for that covariate.

Calling [cox_zph](../reference/CoxPH.md#greenwood.CoxPH.cox_zph) returns a [ZPHResult](../reference/ZPHResult.md#greenwood.ZPHResult), an object that bundles the test statistics together. Displaying it directly gives a compact summary of the test.


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

y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

zph = cox.cox_zph()
zph
```


    ZPHResult(transform='identity', age: p=0.7065, sex: p=0.0992, GLOBAL p=0.2425)


For the full per-covariate breakdown, call [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame) on that result. This is the table you will read most often.


``` python
zph.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>Rows3Columns4
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="term" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

term

<em>str</em>

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

chisq

<em>f64</em>

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

df

<em>i64</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>
</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: 87px">age</td>
<td class="gt_row gt_right" style="max-width: 145px">0.141747561345</td>
<td class="gt_row gt_right" style="max-width: 78px">1</td>
<td class="gt_row gt_right" style="max-width: 152px">0.70654984373</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 87px">sex</td>
<td class="gt_row gt_right" style="max-width: 145px">2.71840108815</td>
<td class="gt_row gt_right" style="max-width: 78px">1</td>
<td class="gt_row gt_right" style="max-width: 152px">0.0991973377972</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 87px">GLOBAL</td>
<td class="gt_row gt_right" style="max-width: 145px">2.8333913831</td>
<td class="gt_row gt_right" style="max-width: 78px">2</td>
<td class="gt_row gt_right" style="max-width: 152px">0.242514035592</td>
</tr>
</tbody>
</table>


The table has one row per covariate plus a `GLOBAL` row that tests all covariates jointly. Large p-values, as here, are reassuring: they give no evidence against proportional hazards. The test uses a time transform, which defaults to `"identity"`; `"log"` is also available.

> **Warning: A non-significant test is not proof**
>
> Failing to reject the proportional hazards assumption does not prove it holds, especially in small samples. Complement the test by plotting scaled Schoenfeld residuals or by fitting time-stratified models when you have reason to suspect a time-varying effect.


# Residuals

Residuals reveal how individual observations relate to the fitted model. Martingale residuals, one per subject, are the difference between the observed number of events and the number the model expected; large negative values flag subjects who lived much longer than predicted. Schoenfeld residuals, one per event, underlie the proportional hazards test and are useful for spotting time trends in a covariate's effect.

The martingale residuals come back as a NumPy array with one entry per subject. There are 228 of them, so we look at the first five rather than the whole array.


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

y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

cox.residuals("martingale")[:5]
```


    array([ 0.00438999, -0.50576203, -3.12981924,  0.53275397, -2.35065745])


The Schoenfeld residuals come back as a DataFrame instead, with one row per event and one column per covariate.


``` python
cox.residuals("schoenfeld", 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>Rows165Columns2
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="age" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

age

<em>f64</em>

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

sex

<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: 229px">0.935464711892</td>
<td class="gt_row gt_right" style="max-width: 236px">0.727079613965</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 229px">10.0052299319</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.272302937094</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 229px">17.0052299319</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.272302937094</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 229px">3.00522993189</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.272302937094</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 229px">10.1404536933</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.275789612537</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 229px">12.227706252</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.278411033642</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 229px">1.22770625199</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.278411033642</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: 229px">5.29450657479</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.280979164464</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">162</td>
<td class="gt_row gt_right" style="max-width: 229px">-2.90825326766</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.155341657484</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">163</td>
<td class="gt_row gt_right" style="max-width: 229px">0.611217430947</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.196097246718</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">164</td>
<td class="gt_row gt_right" style="max-width: 229px">-4.65909168016</td>
<td class="gt_row gt_right" style="max-width: 236px">-0.171473016943</td>
</tr>
</tbody>
</table>


# Baseline hazard and predicted survival

Although the Cox model does not assume a shape for the baseline hazard, it can estimate one after fitting. The baseline cumulative hazard, and the survival curve derived from it, describe a reference subject.


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

y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

cox.baseline_hazard(format="polars")
```


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

cumhaz

<em>f64</em>

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

survival

<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: 105px">5</td>
<td class="gt_row gt_right" style="max-width: 186px">0.00295531012895</td>
<td class="gt_row gt_right" style="max-width: 172px">0.997049052501</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 105px">11</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0119063396838</td>
<td class="gt_row gt_right" style="max-width: 172px">0.988164260305</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 105px">12</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0149282203866</td>
<td class="gt_row gt_right" style="max-width: 172px">0.985182653095</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 105px">13</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0210294286393</td>
<td class="gt_row gt_right" style="max-width: 172px">0.979190147912</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 105px">15</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0241081722771</td>
<td class="gt_row gt_right" style="max-width: 172px">0.976180108421</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 105px">26</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0272054191316</td>
<td class="gt_row gt_right" style="max-width: 172px">0.973161315038</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 105px">30</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0303227228765</td>
<td class="gt_row gt_right" style="max-width: 172px">0.970132399105</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: 105px">31</td>
<td class="gt_row gt_right" style="max-width: 186px">0.0334599993722</td>
<td class="gt_row gt_right" style="max-width: 172px">0.967093594809</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">183</td>
<td class="gt_row gt_right" style="max-width: 105px">965</td>
<td class="gt_row gt_right" style="max-width: 186px">2.01305845682</td>
<td class="gt_row gt_right" style="max-width: 172px">0.133579502131</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">184</td>
<td class="gt_row gt_right" style="max-width: 105px">1010</td>
<td class="gt_row gt_right" style="max-width: 186px">2.01305845682</td>
<td class="gt_row gt_right" style="max-width: 172px">0.133579502131</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">185</td>
<td class="gt_row gt_right" style="max-width: 105px">1022</td>
<td class="gt_row gt_right" style="max-width: 186px">2.01305845682</td>
<td class="gt_row gt_right" style="max-width: 172px">0.133579502131</td>
</tr>
</tbody>
</table>


More useful in practice is predicting the survival curve for specific covariate values. First we build a small frame of the subjects we want predictions for: a 50-year-old with `sex` 1 and a 70-year-old with `sex` 2. Showing it confirms the covariate values before we predict from them.


``` python
import pandas as pd

newdata = pd.DataFrame({"age": [50, 70], "sex": [1, 2]})
newdata
```


<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: #150458; color: #FFFFFF; border: 1px solid #150458; margin-right: 8px;">Pandas</span>Rows2Columns2
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></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>
</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: 232px">50</td>
<td class="gt_row gt_right" style="max-width: 232px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 232px">70</td>
<td class="gt_row gt_right" style="max-width: 232px">2</td>
</tr>
</tbody>
</table>


Given that new data and a set of times, [predict](../reference/AFT.md#greenwood.AFT.predict) returns the estimated survival probability for each subject.


``` python
cox.predict(newdata, type="survival", times=[180, 365, 730], format="polars")
```


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

subject_1

<em>f64</em>

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

subject_2

<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: 110px">180</td>
<td class="gt_row gt_right" style="max-width: 177px">0.7297626204</td>
<td class="gt_row gt_right" style="max-width: 177px">0.767074757103</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 110px">365</td>
<td class="gt_row gt_right" style="max-width: 177px">0.417781082587</td>
<td class="gt_row gt_right" style="max-width: 177px">0.479674113531</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 110px">730</td>
<td class="gt_row gt_right" style="max-width: 177px">0.119648956441</td>
<td class="gt_row gt_right" style="max-width: 177px">0.167440538104</td>
</tr>
</tbody>
</table>


Each column is a subject from `newdata`, and each row is a requested time. The `type="lp"` and `type="risk"` options instead return the linear predictor and the relative risk.


## Confidence bands and conditional survival

Pass `ci=True` to add a pointwise confidence band around each predicted curve. Every subject then gains `_lower` and `_upper` columns, built from the standard error of the cumulative hazard and matching R's `survfit` (which combines baseline-hazard and coefficient uncertainty).


``` python
cox.predict(newdata, type="survival", times=[180, 365, 730], ci=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>Rows3Columns7
</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="subject_1" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

subject_1

<em>f64</em>

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

subject_1_lower

<em>f64</em>

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

subject_1_upper

<em>f64</em>

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

subject_2

<em>f64</em>

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

subject_2_lower

<em>f64</em>

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

subject_2_upper

<em>f64</em>

</div></th>
</tr>
</thead>
<tbody class="gt_table_body">
<tr>
<td class="gt_row gt_right gd-tbl-rownum">0</td>
<td class="gt_row gt_right" style="max-width: 50px">180</td>
<td class="gt_row gt_right" style="max-width: 117px">0.7297626204</td>
<td class="gt_row gt_right" style="max-width: 133px">0.65075626184</td>
<td class="gt_row gt_right" style="max-width: 133px">0.818360903092</td>
<td class="gt_row gt_right" style="max-width: 117px">0.767074757103</td>
<td class="gt_row gt_right" style="max-width: 133px">0.696725970364</td>
<td class="gt_row gt_right" style="max-width: 133px">0.844526697744</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 50px">365</td>
<td class="gt_row gt_right" style="max-width: 117px">0.417781082587</td>
<td class="gt_row gt_right" style="max-width: 133px">0.312492619485</td>
<td class="gt_row gt_right" style="max-width: 133px">0.558544497004</td>
<td class="gt_row gt_right" style="max-width: 117px">0.479674113531</td>
<td class="gt_row gt_right" style="max-width: 133px">0.379184415457</td>
<td class="gt_row gt_right" style="max-width: 133px">0.6067951261</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 50px">730</td>
<td class="gt_row gt_right" style="max-width: 117px">0.119648956441</td>
<td class="gt_row gt_right" style="max-width: 133px">0.0574578159532</td>
<td class="gt_row gt_right" style="max-width: 133px">0.249154489079</td>
<td class="gt_row gt_right" style="max-width: 117px">0.167440538104</td>
<td class="gt_row gt_right" style="max-width: 133px">0.0910524175713</td>
<td class="gt_row gt_right" style="max-width: 133px">0.307914216319</td>
</tr>
</tbody>
</table>


You can also predict conditional on a subject having already survived some time, which is useful for updating a prognosis partway through follow-up. With `conditional_after=180`, the returned probabilities are \\P(T \> t \mid T \> 180) = S(t) / S(180)\\.


``` python
cox.predict(newdata, type="survival", times=[365, 730], conditional_after=180, format="polars")
```


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

subject_1

<em>f64</em>

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

subject_2

<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: 110px">365</td>
<td class="gt_row gt_right" style="max-width: 177px">0.572489013425</td>
<td class="gt_row gt_right" style="max-width: 177px">0.625329029654</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 110px">730</td>
<td class="gt_row gt_right" style="max-width: 177px">0.163955994862</td>
<td class="gt_row gt_right" style="max-width: 177px">0.218284510803</td>
</tr>
</tbody>
</table>


# Stratification

Sometimes a variable violates the proportional hazards assumption but is not of direct interest, such as enrolling hospital. Stratification lets each level of that variable have its own baseline hazard while sharing the coefficients of the covariates you do care about. You pass the stratifying variable to `strata`. Here we stratify by `sex` while keeping `age` and `ph.ecog` as covariates.


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

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

stratified = gw.CoxPH().fit(y, lung[["age", "ph.ecog"]], strata=lung["sex"])
```


As with any fitted model, we can print [stratified](../reference/RMSTResult.md#greenwood.RMSTResult.stratified) for a summary, or read its coefficient table as data with [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame).


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


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="8" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>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: 66px">age</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0105662546009</td>
<td class="gt_row gt_right" style="max-width: 131px">0.00924137389309</td>
<td class="gt_row gt_right" style="max-width: 110px">1.14336404123</td>
<td class="gt_row gt_right" style="max-width: 138px">0.252887475744</td>
<td class="gt_row gt_right" style="max-width: 138px">-0.00754650539718</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0286790145991</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 66px">ph.ecog</td>
<td class="gt_row gt_right" style="max-width: 124px">0.462424434358</td>
<td class="gt_row gt_right" style="max-width: 131px">0.114761097855</td>
<td class="gt_row gt_right" style="max-width: 110px">4.0294528634</td>
<td class="gt_row gt_right" style="max-width: 138px">5.59068243623e-05</td>
<td class="gt_row gt_right" style="max-width: 138px">0.237496815737</td>
<td class="gt_row gt_right" style="max-width: 124px">0.68735205298</td>
</tr>
</tbody>
</table>


Notice that the stratifying variable, `sex`, does not appear as a coefficient. Its effect is absorbed into the separate baselines, which is exactly the point of stratification.


# Robust and clustered standard errors

When observations are not independent, for example when the same subject contributes several rows or when subjects are grouped into clusters, the model-based standard errors are too small. The robust, or sandwich, variance corrects for this. Set `robust=True` for the Lin-Wei sandwich estimator, and pass `cluster` to group correlated observations.


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

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

robust = gw.CoxPH().fit(y, lung[["age", "sex"]], robust=True)

robust.to_frame(format="polars")[["term", "std_error"]]
```


<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>Rows2Columns2
</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="std_error" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

std_error

<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: 192px">age</td>
<td class="gt_row gt_right" style="max-width: 273px">0.00948922108421</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 192px">sex</td>
<td class="gt_row gt_right" style="max-width: 273px">0.159919372277</td>
</tr>
</tbody>
</table>


The coefficients are unchanged; only the standard errors, and therefore the p-values and intervals, are adjusted. The model-based standard errors remain available as `naive_std_error_` for comparison.


# The concordance index

The concordance index, or C-statistic, measures how well the model's risk ordering agrees with the observed order of events. It is the probability that, for a random comparable pair of subjects, the one who failed first had the higher predicted risk. A value of 0.5 is no better than chance and 1.0 is perfect discrimination.


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

y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cox = gw.CoxPH().fit(y, lung[["age", "sex"]])

cox.concordance()
```


    0.6028530028979714


# Next steps

You can now validate a Cox model and use it to predict.

- [Prediction performance](prediction-performance.md) extends discrimination and adds the Brier score for calibration, including for external risk scores.
- [Parametric survival models](parametric-models.md) provide an alternative when the proportional hazards assumption is untenable.
- Revisit [Cox regression](cox-regression.md) for the modeling basics if any of this is unfamiliar.
