# Cox regression

The Kaplan-Meier estimator and the log-rank test describe and compare whole groups. To measure the effect of a continuous predictor, or to adjust for several variables at once, you need a regression model. The Cox proportional hazards model is the most widely used tool for this. It models the hazard, the instantaneous risk of the event among those still at risk, and expresses how covariates multiply that risk without requiring you to specify the shape of the baseline hazard. This page covers fitting the model, reading hazard ratios, and the tests it reports.


``` python
import greenwood as gw

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

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


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


The response `y` is a [Surv](../reference/Surv.md#greenwood.Surv) object that pairs each subject's follow-up time with an event indicator, marking censored times with a `+`. It records 165 events among 228 subjects, and it is the outcome the Cox model below regresses on the covariates.


# The proportional hazards idea

The Cox model assumes that each covariate multiplies the hazard by a constant factor that does not change over time. If being in a treatment group halves the hazard at one month, the model assumes it halves the hazard at every month. That multiplicative factor is the hazard ratio, and estimating it is the point of the model. The word "proportional" refers to this constant-factor assumption, which you should check after fitting; see [Cox model diagnostics](cox-diagnostics.md).

The great convenience of the Cox model is that it estimates these hazard ratios without assuming any particular form for how the baseline risk changes over time. This is why it is called semiparametric, and why it is the default choice for most analyses.


# Fitting a model

You fit the model with a [Surv](../reference/Surv.md#greenwood.Surv) response and a set of covariates. Covariates can be a pandas or Polars data frame; numeric columns are used directly and non-numeric columns are turned into indicator variables automatically. Rows with missing covariate values are dropped, as in a standard complete-case analysis.


``` python
import greenwood as gw

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", "ph.ecog"]])
```


The call returns a fitted [CoxPH](../reference/CoxPH.md#greenwood.CoxPH) estimator, held in `cox`. Printing it gives a compact summary modeled on R's `coxph` output: the coefficient table, the sample size and event count, and the overall likelihood ratio test.


``` python
cox
```


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

                coef  exp(coef)  se(coef)       z          p
    age      0.01107      1.011  0.009267   1.194     0.2324
    sex      -0.5526     0.5754    0.1677  -3.294  0.0009861
    ph.ecog   0.4637       1.59    0.1136   4.083  4.447e-05

    n = 227, events = 164
    Likelihood ratio test = 30.5 on 3 df, p = 1.083e-06


The printed summary is meant for reading. When you want the numbers as data, to filter, join, or plot them, ask for the coefficient table with [to_frame()](../reference/AFT.md#greenwood.AFT.to_frame), which returns a tidy frame.


``` python
cox.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>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="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.0110667645601</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0092674110137</td>
<td class="gt_row gt_right" style="max-width: 117px">1.19415924725</td>
<td class="gt_row gt_right" style="max-width: 138px">0.232415681</td>
<td class="gt_row gt_right" style="max-width: 138px">-0.00709702725671</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0292305563769</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 66px">sex</td>
<td class="gt_row gt_right" style="max-width: 124px">-0.552612395704</td>
<td class="gt_row gt_right" style="max-width: 124px">0.167739053787</td>
<td class="gt_row gt_right" style="max-width: 117px">-3.29447664826</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000986051372138</td>
<td class="gt_row gt_right" style="max-width: 138px">-0.881374899927</td>
<td class="gt_row gt_right" style="max-width: 124px">-0.22384989148</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</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.46372847537</td>
<td class="gt_row gt_right" style="max-width: 124px">0.113577266162</td>
<td class="gt_row gt_right" style="max-width: 117px">4.0829339448</td>
<td class="gt_row gt_right" style="max-width: 138px">4.44706665186e-05</td>
<td class="gt_row gt_right" style="max-width: 138px">0.24112112423</td>
<td class="gt_row gt_right" style="max-width: 124px">0.68633582651</td>
</tr>
</tbody>
</table>


The coefficient table reports, for each covariate, the estimated log hazard ratio (`estimate`), its standard error, a Wald z-statistic and p-value, and a confidence interval. The coefficients are on the log scale, which is convenient for the arithmetic of the model but not for interpretation.


## Handling ties in event times

When multiple subjects experience events at the same time, the Cox model needs a method to handle these "ties". Greenwood supports two tie-handling methods, controlled by the `ties` parameter in the constructor:

- **"efron"** (default): Efron's method. Recommended; matches R's survival package default.
- **"breslow"**: Breslow's method. Computationally simpler but less accurate when ties are common.

The choice usually has minimal impact on results unless ties are very common. When in doubt, stick with the default (Efron).


``` python
import pandas as pd

# Compare methods
cox_efron = gw.CoxPH(ties="efron").fit(y, lung[["age", "sex"]])
cox_breslow = gw.CoxPH(ties="breslow").fit(y, lung[["age", "sex"]])

# Coefficients are nearly identical unless ties are very common
pd.DataFrame({
    "Efron": cox_efron.to_frame(format="polars")["estimate"],
    "Breslow": cox_breslow.to_frame(format="polars")["estimate"],
})
```


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

Efron

<em>f64</em>

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

Breslow

<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: 232px">0.0170453318454</td>
<td class="gt_row gt_right" style="max-width: 232px">0.0170128891984</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 232px">-0.513218517108</td>
<td class="gt_row gt_right" style="max-width: 232px">-0.512564791519</td>
</tr>
</tbody>
</table>


# Hazard ratios and their interpretation

To interpret the model you exponentiate the coefficients, which turns log hazard ratios into hazard ratios. Greenwood does this for you through the tidy layer.


``` 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", "ph.ecog"]])

gw.tidy(cox, 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>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="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: 110px">1.01112822772</td>
<td class="gt_row gt_right" style="max-width: 124px">0.0092674110137</td>
<td class="gt_row gt_right" style="max-width: 117px">1.19415924725</td>
<td class="gt_row gt_right" style="max-width: 138px">0.232415681</td>
<td class="gt_row gt_right" style="max-width: 117px">0.99292809717</td>
<td class="gt_row gt_right" style="max-width: 117px">1.02966196224</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 66px">sex</td>
<td class="gt_row gt_right" style="max-width: 110px">0.57544455619</td>
<td class="gt_row gt_right" style="max-width: 124px">0.167739053787</td>
<td class="gt_row gt_right" style="max-width: 117px">-3.29447664826</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000986051372138</td>
<td class="gt_row gt_right" style="max-width: 117px">0.414213018549</td>
<td class="gt_row gt_right" style="max-width: 117px">0.799435127387</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 66px">ph.ecog</td>
<td class="gt_row gt_right" style="max-width: 110px">1.58999119005</td>
<td class="gt_row gt_right" style="max-width: 124px">0.113577266162</td>
<td class="gt_row gt_right" style="max-width: 117px">4.0829339448</td>
<td class="gt_row gt_right" style="max-width: 138px">4.44706665186e-05</td>
<td class="gt_row gt_right" style="max-width: 117px">1.27267517777</td>
<td class="gt_row gt_right" style="max-width: 117px">1.98642358129</td>
</tr>
</tbody>
</table>


A hazard ratio above 1 means the covariate increases the hazard, and a value below 1 means it decreases it. For a continuous covariate such as `age`, the hazard ratio is the multiplicative change in hazard per one-unit increase; a hazard ratio of 1.02 for age means roughly a 2 percent higher hazard for each additional year. For the `sex` indicator, coded 1 and 2 in this dataset, a hazard ratio below 1 means the higher-coded group has lower risk.

> **Note: Hazard ratio, not risk ratio**
>
> A hazard ratio compares instantaneous rates among those still at risk, not the probability of the event over the whole study. The two are related but not identical, and the difference matters when events are common. Report hazard ratios as such, and consider an absolute measure like a survival difference or a restricted mean difference alongside them.


# Model-fit statistics

The `glance` view gives one-row summary statistics for the whole model, including the log-likelihood, the AIC, and the likelihood-ratio test of the null hypothesis that all coefficients are zero.


``` 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", "ph.ecog"]])

gw.glance(cox, 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>Rows1Columns7
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="n" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>

n

<em>i64</em>

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

nevent

<em>i64</em>

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

loglik

<em>f64</em>

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

aic

<em>f64</em>

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

lr_statistic

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

lr_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_right" style="max-width: 50px">227</td>
<td class="gt_row gt_right" style="max-width: 63px">164</td>
<td class="gt_row gt_right" style="max-width: 117px">-729.230121375</td>
<td class="gt_row gt_right" style="max-width: 110px">1464.46024275</td>
<td class="gt_row gt_right" style="max-width: 110px">30.5006687732</td>
<td class="gt_row gt_right" style="max-width: 50px">3</td>
<td class="gt_row gt_right" style="max-width: 131px">1.0828176992e-06</td>
</tr>
</tbody>
</table>


The model reports three classical global tests, all of which assess whether the covariates jointly improve the fit: the likelihood-ratio test, the Wald test, and the score test. They usually agree closely, and the likelihood-ratio test is generally preferred.


``` python
print("likelihood ratio:", round(cox.lr_stat_, 3))
print("Wald:", round(cox.wald_stat_, 3))
print("score:", round(cox.score_stat_, 3))
```


    likelihood ratio: 30.501
    Wald: 29.929
    score: 30.5


# Handling tied event times

When two or more events occur at exactly the same recorded time, the partial likelihood must account for the tie. Greenwood defaults to the Efron approximation, which is accurate and is also the default in R. The Breslow approximation is available and is faster but slightly less accurate when ties are common.


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

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

gw.CoxPH(ties="breslow").fit(y, lung[["age", "sex"]]).to_frame(format="polars")[["term", "estimate"]]
```


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

estimate

<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: 195px">age</td>
<td class="gt_row gt_right" style="max-width: 269px">0.0170128891984</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 195px">sex</td>
<td class="gt_row gt_right" style="max-width: 269px">-0.512564791519</td>
</tr>
</tbody>
</table>


Unless you have a specific reason to match another tool's Breslow output, the Efron default is the right choice.

> **Tip: Flexibility in tie-handling methods**
>
> Greenwood's support for both Efron and Breslow tie-handling methods gives you flexibility that is useful in several situations:
>
> - matching textbook examples: if you're learning from a textbook that uses SAS (which defaults to Breslow), you can use `ties="breslow"` to get results that match exactly.
> - comparing across tools: if you're validating results across R, SAS, and Python, you can switch the tie method in Greenwood to match any tool's output with a single parameter change, rather than rewriting your analysis in a different language.
> - sensitivity analysis: you can easily compare whether your results change substantially between the two methods, which is a good diagnostic when ties are very common.
>
> Most analyses will stick with the default Efron method (which matches R), but this flexibility means you're never locked into one approach.


# Time-varying covariates

Some covariates change during follow-up: a treatment that starts partway through, a lab value that is remeasured, or a status that switches once. The Cox model handles these through the counting-process form, where each subject contributes one row per interval over which their covariates are constant. Each row records the interval `(start, stop]`, whether the event happened at its end, and the covariate values that held during it.

We build a small illustrative dataset where a treatment switches on for some subjects. The response is a `gw.Surv.counting` object rather than `gw.Surv.right`.


``` python
intervals = pd.DataFrame(
    {
        "subject": [1, 1, 2, 3, 3, 4, 5, 5],
        "start":   [0, 4, 0, 0, 5, 0, 0, 3],
        "stop":    [4, 10, 7, 5, 14, 9, 3, 12],
        "event":   [0, 1, 1, 0, 0, 1, 0, 1],
        "treated": [0, 1, 0, 0, 1, 0, 0, 1],
    }
)

intervals
```


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

subject

<em>i64</em>

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

start

<em>i64</em>

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

stop

<em>i64</em>

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

event

<em>i64</em>

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

treated

<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: 103px">1</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 82px">4</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 103px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_right" style="max-width: 103px">1</td>
<td class="gt_row gt_right" style="max-width: 87px">4</td>
<td class="gt_row gt_right" style="max-width: 82px">10</td>
<td class="gt_row gt_right" style="max-width: 87px">1</td>
<td class="gt_row gt_right" style="max-width: 103px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_right" style="max-width: 103px">2</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 82px">7</td>
<td class="gt_row gt_right" style="max-width: 87px">1</td>
<td class="gt_row gt_right" style="max-width: 103px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_right" style="max-width: 103px">3</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 82px">5</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 103px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_right" style="max-width: 103px">3</td>
<td class="gt_row gt_right" style="max-width: 87px">5</td>
<td class="gt_row gt_right" style="max-width: 82px">14</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 103px">1</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_right" style="max-width: 103px">4</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 82px">9</td>
<td class="gt_row gt_right" style="max-width: 87px">1</td>
<td class="gt_row gt_right" style="max-width: 103px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">6</td>
<td class="gt_row gt_right" style="max-width: 103px">5</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 82px">3</td>
<td class="gt_row gt_right" style="max-width: 87px">0</td>
<td class="gt_row gt_right" style="max-width: 103px">0</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">7</td>
<td class="gt_row gt_right" style="max-width: 103px">5</td>
<td class="gt_row gt_right" style="max-width: 87px">3</td>
<td class="gt_row gt_right" style="max-width: 82px">12</td>
<td class="gt_row gt_right" style="max-width: 87px">1</td>
<td class="gt_row gt_right" style="max-width: 103px">1</td>
</tr>
</tbody>
</table>


Subject 1, for example, is untreated over `(0, 4]` and treated over `(4, 10]`, with the event at day 10. We pass the interval endpoints to `gw.Surv.counting` and fit as usual.


``` python
y_tv = gw.Surv.counting(start=intervals["start"], stop=intervals["stop"], event=intervals["event"])

gw.CoxPH().fit(y_tv, intervals[["treated"]]).to_frame(format="polars")[["term", "estimate", "p_value"]]
```


<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>Rows1Columns3
</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="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: 121px">treated</td>
<td class="gt_row gt_right" style="max-width: 172px">-22.3171831456</td>
<td class="gt_row gt_right" style="max-width: 172px">0.999461642315</td>
</tr>
</tbody>
</table>


The risk set at each event time correctly includes only the intervals that span it, using each subject's covariate values as of that moment. This is exactly R's start-stop `coxph`, and Greenwood matches it to tolerance.

> **Note: One row per interval, not per subject**
>
> The only change from an ordinary Cox fit is the data layout: a subject with a covariate that changes `k` times contributes `k + 1` rows, and the event indicator is 1 only on the interval where the event occurred. Left truncation and delayed entry use the same counting-process response.

> **Important: Subject timelines must start at `0`**
>
> Each subject needs their own timeline starting at 0 (subject-relative time), not calendar time.
>
> **Correct** (subject-relative): Each subject's earliest interval starts at 0
>
> - Subject 1: (0, 4\], (4, 10\] (personal follow-up time)
> - Subject 2: (0, 7\] (personal follow-up time)
>
> **Incorrect** (calendar time): Subjects enter the study at different dates
>
> - Subject 1: (2024-01-01, 2024-01-04\], (2024-01-04, 2024-01-10\]
> - Subject 2: (2024-06-15, 2024-06-22\] (different calendar dates)
>
> If your data uses calendar time (e.g., from a wide dataset where subjects enroll on different dates), subtract each subject's entry date from their start/stop times.
>
> **Pandas**:
>
> <div id="0bcebebd" class="cell" execution_count="13">
>
> ``` python
> # Example data in calendar time (subjects enroll at different dates)
> df = pd.DataFrame({
>     'subject': [1, 1, 2, 2, 3, 3],
>     'start': [0, 10, 365, 375, 730, 740],      # Different enrollment dates
>     'stop': [10, 25, 375, 390, 740, 755],
>     'event': [0, 1, 0, 1, 0, 1]
> })
>
> # Convert calendar time to subject-relative time (pandas)
> df['entry_date'] = df.groupby('subject')['start'].transform('min')
> df['start_relative'] = df['start'] - df['entry_date']
> df['stop_relative'] = df['stop'] - df['entry_date']
>
> # Drop helper column
> df = df.drop(columns=['entry_date'])
> df
> ```
>
> <div class="cell-output cell-output-display" execution_count="13">
>
> <div id="gd-tbl-627c1c72" class="gd-tbl-preview" style="padding-left: 0px; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; margin-bottom: 1em;">
>
> <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>Rows6Columns6
>
> <div id="robust-and-clustered-standard-errors" class="section level1">
>
> # Robust and clustered standard errors
>
> By default, the Cox model uses the model-based (Fisher information) standard errors. When the model may be misspecified, or when observations are not independent (e.g., repeated measures, family data), use the sandwich (robust) variance estimator by setting `robust=True`:
>
> <div id="a7fedf21" class="cell" execution_count="15">
>
> ``` python
> lung = gw.load_dataset("lung", backend="polars")
>
> y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
>
> cox_robust = gw.CoxPH().fit(y, lung[["age", "sex"]], robust=True)
>
> gw.tidy(cox_robust, format="polars")
> ```
>
> <div class="cell-output cell-output-display" execution_count="15">
>
> <div id="gd-tbl-c51b8d65" class="gd-tbl-preview" style="padding-left: 0px; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; margin-bottom: 1em;">
>
> <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>
> <div class="gd-tbl-colname">
> term
> </div>
> <div class="gd-tbl-dtype">
> <em>str</em>
> </div>
> </div></th>
> <th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> estimate
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="std_error" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> std_error
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="statistic" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> statistic
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="p_value" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> p_value
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> conf_low
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> conf_high
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </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: 124px">0.0170453318454</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.00948922108421</td>
> <td class="gt_row gt_right" style="max-width: 117px">1.79628356154</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.0724494308764</td>
> <td class="gt_row gt_right" style="max-width: 138px">-0.00155319972097</td>
> <td class="gt_row gt_right" style="max-width: 124px">0.0356438634118</td>
> </tr>
> <tr>
> <td class="gt_row gt_right gd-tbl-rownum">1</td>
> <td class="gt_row gt_left" style="max-width: 50px">sex</td>
> <td class="gt_row gt_right" style="max-width: 124px">-0.513218517108</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.159919372277</td>
> <td class="gt_row gt_right" style="max-width: 117px">-3.20923293909</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.00133089620495</td>
> <td class="gt_row gt_right" style="max-width: 138px">-0.826654727201</td>
> <td class="gt_row gt_right" style="max-width: 124px">-0.199782307016</td>
> </tr>
> </tbody>
> </table>
>
> </div>
>
> </div>
>
> </div>
>
> The standard errors adjust upward (more conservative) if there is overdispersion, or downward if there is underdispersion. For clustered data (e.g., family members, multiple measurements per subject), pass the cluster variable to ensure standard errors account for within-cluster dependence:
>
> <div id="6786abe6" class="cell" execution_count="16">
>
> ``` python
> # Simulated example: suppose subjects cluster by family
> lung_with_cluster = lung.with_columns(
>     family=((pl.int_range(len(lung)) % 10) + 1)
> )
>
> cox_cluster = gw.CoxPH().fit(y, lung_with_cluster[["age", "sex"]],
>                               robust=True, cluster=lung_with_cluster["family"])
>
> gw.tidy(cox_cluster, format="polars")
> ```
>
> <div class="cell-output cell-output-display" execution_count="16">
>
> <div id="gd-tbl-1880e4c5" class="gd-tbl-preview" style="padding-left: 0px; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; margin-bottom: 1em;">
>
> <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>
> <div class="gd-tbl-colname">
> term
> </div>
> <div class="gd-tbl-dtype">
> <em>str</em>
> </div>
> </div></th>
> <th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> estimate
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="std_error" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> std_error
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="statistic" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> statistic
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="p_value" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> p_value
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="conf_low" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> conf_low
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </div></th>
> <th id="conf_high" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> conf_high
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </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: 124px">0.0170453318454</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.00950511031412</td>
> <td class="gt_row gt_right" style="max-width: 117px">1.79328080181</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.0729280355773</td>
> <td class="gt_row gt_right" style="max-width: 138px">-0.00158434203935</td>
> <td class="gt_row gt_right" style="max-width: 124px">0.0356750057302</td>
> </tr>
> <tr>
> <td class="gt_row gt_right gd-tbl-rownum">1</td>
> <td class="gt_row gt_left" style="max-width: 50px">sex</td>
> <td class="gt_row gt_right" style="max-width: 124px">-0.513218517108</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.158290649924</td>
> <td class="gt_row gt_right" style="max-width: 117px">-3.24225415308</td>
> <td class="gt_row gt_right" style="max-width: 131px">0.00118588214896</td>
> <td class="gt_row gt_right" style="max-width: 138px">-0.823462490048</td>
> <td class="gt_row gt_right" style="max-width: 124px">-0.202974544168</td>
> </tr>
> </tbody>
> </table>
>
> </div>
>
> </div>
>
> </div>
>
> Clustered sandwich standard errors are wider than model-based ones, reflecting the loss of information from within-cluster correlation.
>
> Instead of selecting columns yourself, you can describe the model with a formula, passing the right-hand side as a string and the data frame as `data`. This is convenient for categorical variables, interactions, and transformations, and it mirrors the notation used in R. The formula support uses [formulaic](https://matthewwardrop.github.io/formulaic/), installed with the `formula` extra.
>
> <div id="93292c5c" class="cell" execution_count="17">
>
> ``` python
> gw.CoxPH().fit(y, "age + sex", data=lung).to_frame(format="polars")[["term", "estimate"]]
> ```
>
> <div class="cell-output cell-output-display" execution_count="17">
>
> <div id="gd-tbl-04805357" class="gd-tbl-preview" style="padding-left: 0px; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; margin-bottom: 1em;">
>
> <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>
> <div class="gd-tbl-colname">
> term
> </div>
> <div class="gd-tbl-dtype">
> <em>str</em>
> </div>
> </div></th>
> <th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> estimate
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </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: 195px">age</td>
> <td class="gt_row gt_right" style="max-width: 269px">0.0170453318454</td>
> </tr>
> <tr>
> <td class="gt_row gt_right gd-tbl-rownum">1</td>
> <td class="gt_row gt_left" style="max-width: 195px">sex</td>
> <td class="gt_row gt_right" style="max-width: 269px">-0.513218517108</td>
> </tr>
> </tbody>
> </table>
>
> </div>
>
> </div>
>
> </div>
>
> Categorical columns are expanded into indicator terms automatically, and interactions are written with `*` (which expands to the main effects plus their product) or `:` (the product alone). A term like `C(ph.ecog)` forces a column to be treated as categorical.
>
> <div id="7a397812" class="cell" execution_count="18">
>
> ``` python
> gw.CoxPH().fit(y, "age * sex", data=lung).to_frame(format="polars")[["term", "estimate"]]
> ```
>
> <div class="cell-output cell-output-display" execution_count="18">
>
> <div id="gd-tbl-ae43b487" class="gd-tbl-preview" style="padding-left: 0px; overflow-x: auto; overflow-y: hidden; width: 100%; max-width: 100%; margin-bottom: 1em;">
>
> <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>Rows3Columns2
> </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>
> <div class="gd-tbl-colname">
> term
> </div>
> <div class="gd-tbl-dtype">
> <em>str</em>
> </div>
> </div></th>
> <th id="estimate" class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"><div>
> <div class="gd-tbl-colname">
> estimate
> </div>
> <div class="gd-tbl-dtype">
> <em>f64</em>
> </div>
> </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: 196px">age</td>
> <td class="gt_row gt_right" style="max-width: 268px">0.0300312237831</td>
> </tr>
> <tr>
> <td class="gt_row gt_right gd-tbl-rownum">1</td>
> <td class="gt_row gt_left" style="max-width: 196px">sex</td>
> <td class="gt_row gt_right" style="max-width: 268px">0.0953223158055</td>
> </tr>
> <tr>
> <td class="gt_row gt_right gd-tbl-rownum">2</td>
> <td class="gt_row gt_left" style="max-width: 196px">age:sex</td>
> <td class="gt_row gt_right" style="max-width: 268px">-0.00968855903869</td>
> </tr>
> </tbody>
> </table>
>
> </div>
>
> </div>
>
> </div>
>
> Rows with a missing value in any formula term are dropped, the same complete-case rule used when you pass columns directly. The formula interface is also available on [AFT](../reference/AFT.md#greenwood.AFT).
>
> <div id="penalized-regression" class="section level1">
>
> # Penalized regression
>
> When you have many covariates, or they are collinear, an unpenalized fit can overfit or become unstable. [CoxNet](../reference/CoxNet.md#greenwood.CoxNet) fits an elastic-net penalized Cox model: it shrinks the coefficients and, for the lasso, sets some of them to exactly zero, which selects variables. The `penalizer` argument sets the overall strength, and `l1_ratio` mixes the lasso (`1.0`) and ridge (`0.0`) penalties.
>
> <div id="4f3997b8" class="cell" execution_count="19">
>
> ``` python
> lung = gw.load_dataset("lung", backend="polars")
>
> y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
>
> cols = ["age", "sex", "ph.ecog", "ph.karno", "wt.loss"]
>
> lasso = gw.CoxNet(penalizer=0.05, l1_ratio=1.0).fit(y, lung[cols])
> lasso
> ```
>
> <div class="cell-output cell-output-display" execution_count="19">
>
>     CoxNet (elastic-net Cox, penalizer=0.05, l1_ratio=1.0)
>
>                     coef
>     age         0.006174
>     sex          -0.4093
>     ph.ecog       0.3673
>     ph.karno           0
>     wt.loss   -0.0006976
>
>     n = 213, events = 151, nonzero coefficients = 4
>
> </div>
>
> </div>
>
> The printed summary reports how many coefficients survived: the lasso has driven the weakest ones to exactly zero, leaving a smaller model. A pure ridge penalty (`l1_ratio=0.0`) instead shrinks every coefficient toward zero without removing any. Because penalized estimates are biased on purpose, [CoxNet](../reference/CoxNet.md#greenwood.CoxNet) reports coefficients for prediction and selection but not p-values, and with `penalizer=0` it reduces to the ordinary Cox fit.
>
> The penalty strength is usually chosen by cross-validation. [cross_validate](../reference/cross_validate.md#greenwood.cross_validate) accepts a [CoxNet](../reference/CoxNet.md#greenwood.CoxNet) directly, so you can compare candidate values by held-out concordance.
>
> <div id="ebe76d9b" class="cell" execution_count="20">
>
> ``` python
> for lam in [0.02, 0.05, 0.1]:
>     cv = gw.cross_validate(gw.CoxNet(penalizer=lam, l1_ratio=1.0), y, lung[cols], k=5, seed=1)
>     print(f"penalizer={lam}: CV concordance = {cv['mean']:.4f}")
> ```
>
> <div class="cell-output cell-output-stdout">
>
>     penalizer=0.02: CV concordance = 0.6175
>     penalizer=0.05: CV concordance = 0.6216
>     penalizer=0.1: CV concordance = 0.6096
>
> </div>
>
> </div>
>
> > **Note: Standardization**
> >
> > [CoxNet](../reference/CoxNet.md#greenwood.CoxNet) standardizes covariates to unit variance before applying the penalty, as glmnet does, so that the penalty treats variables on comparable scales. Coefficients are returned on the original scale, so you read and use them exactly as you would from [CoxPH](../reference/CoxPH.md#greenwood.CoxPH).
>
> <div id="next-steps" class="section level1">
>
> # Next steps
>
> You can now fit a Cox model, read hazard ratios, and assess overall fit.
>
> - [Cox model diagnostics](cox-diagnostics.md) checks the proportional hazards assumption, computes residuals, predicts survival curves, and adds robust variance and stratification.
> - [Parametric survival models](parametric-models.md) offers an alternative modeling approach when you are willing to specify the shape of the survival distribution.
> - [Prediction performance](prediction-performance.md) evaluates how well a fitted model discriminates and calibrates.
>
> </div>
>
> </div>
>
> </div>
>
> </div>
>
> </div>
>
> </div>
>
> </div>
