# Comparing groups

Once you have survival curves for two or more groups, the natural question is whether the differences between them are real or could be explained by chance. The log-rank test and its relatives answer this. They compare the observed number of events in each group against the number expected if the groups shared a common survival experience, accumulated across every event time. This page explains the test, its weighted variants, and how to read the results.

We continue with the `lung` dataset, comparing survival between the two sexes.


``` 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 pairing each subject's follow-up time with an event indicator, where a trailing `+` marks a censored observation. The grouping variable `lung["sex"]` splits these same subjects into the two groups we are about to compare.


# The log-rank test

The log-rank test is the standard method for comparing survival across groups. At each event time it forms a small contingency table of who was at risk and who had an event in each group, computes the events expected under the null hypothesis of no difference, and combines the observed-minus-expected discrepancies into a single chi-square statistic.


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

result = gw.logrank_test(y, group=lung["sex"])
```


The call returns a [TestResult](../reference/TestResult.md#greenwood.TestResult) object, which we store in `result`. Displaying it prints a readable summary of the whole test at once.


``` python
result
```


    TestResult(method='Log-rank test', statistic=10.3267, df=1, p_value=0.001311)


The result carries the test statistic, its degrees of freedom (one less than the number of groups), and the p-value. A small p-value is evidence that the survival curves differ. You can also inspect the weighted observed and expected event counts per group, which show the direction of the difference. These live on the `result` object as the `observed` and `expected` attributes, one entry per group.


``` python
print("observed:", result.observed)
print("expected:", result.expected)
```


    observed: {1: 112.0, 2: 53.0}
    expected: {1: 91.58173902957279, 2: 73.41826097042721}


Here one group has more observed events than expected and the other fewer, which is the signature of a survival difference. The test works for any number of groups; with three or more it produces an overall test on the corresponding degrees of freedom.

> **Note: What the log-rank test does and does not tell you**
>
> The test tells you whether the curves differ overall. It does not tell you by how much, at what times, or in which direction beyond the observed-versus-expected counts. Pair it with a Kaplan-Meier plot and with effect measures such as a hazard ratio from [Cox regression](cox-regression.md) or a restricted-mean-survival difference.


# Weighted tests: the G-rho family

The standard log-rank test weights every event time equally, which makes it most sensitive to differences in the tails of the curves where events accumulate. Sometimes you care more about early differences. The Fleming-Harrington, or G-rho, family generalizes the log-rank test by weighting each event time by a power of the pooled survival probability.

The `rho` parameter controls this. With `rho=0` you get the ordinary log-rank test. With `rho=1` you get the Peto-Peto test, a Wilcoxon-type test that gives more weight to early event times and is therefore more sensitive to early separation of the curves.

This returns the same kind of [TestResult](../reference/TestResult.md#greenwood.TestResult) as before, so you can read it the same way. Only the weighting has changed, so compare its statistic and p-value against the unweighted test above to see how much the emphasis on early event times matters here.


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

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

gw.logrank_test(y, group=lung["sex"], rho=1)
```


    TestResult(method='G-rho test (rho=1, gamma=0.0)', statistic=12.7142, df=1, p_value=0.0003629)


Choosing between them is a matter of what alternative you want to detect, and the choice should be made before looking at the results rather than by picking whichever gives the smaller p-value.

> **Warning: Choose the weighting in advance**
>
> Running several weighted tests and reporting the most significant one inflates the false positive rate. Decide on the test, and its `rho`, based on subject-matter reasoning before you see the data.

> **Tip: Robust handling of edge cases**
>
> Greenwood validates input data upfront, so weighted tests handle edge cases robustly. Times can be zero (events at baseline are valid in survival analysis), and weighted tests like Fleming-Harrington work correctly without silent data loss or dimension mismatches. This robustness extends to all censoring patterns and follows the validation rules documented in the [Validation and reproducibility](survival-data.md#validation-and-reproducibility) section.


# Comparing more than two groups

The log-rank test extends to any number of groups, giving one overall test. We switch to the `veteran` dataset, which records a cell type with four levels.


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

y_vet = gw.Surv.right(vet["time"], event=vet["status"])

gw.logrank_test(y_vet, group=vet["celltype"])
```


    TestResult(method='Log-rank test', statistic=25.4037, df=3, p_value=1.271e-05)


A small p-value here says the four cell types do not all share the same survival, but it does not say which ones differ. For that, test each pair and correct for the fact that you are running several tests at once. [pairwise_logrank_test](../reference/pairwise_logrank_test.md#greenwood.pairwise_logrank_test) does both.


``` python
gw.pairwise_logrank_test(y_vet, group=vet["celltype"], format="polars")
```


<table class="gt_table" data-quarto-disable-processing="true" data-quarto-bootstrap="false">
<thead>
<tr class="gt_heading">
<th colspan="6" class="gt_heading gt_title gt_font_normal"><div style="padding-top: 0; padding-bottom: 7px;">
<span class="gd-tbl-badge" style="background-color: #0075FF; color: #FFFFFF; border: 1px solid #0075FF; margin-right: 8px;">Polars</span>Rows6Columns5
</div></th>
</tr>
<tr class="gt_col_headings">
<th class="gt_col_heading gt_columns_bottom_border gt_right" scope="col"></th>
<th id="group1" class="gt_col_heading gt_columns_bottom_border gt_left" scope="col"><div>

group1

<em>str</em>

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

group2

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

p_adjusted

<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: 81px">adeno</td>
<td class="gt_row gt_left" style="max-width: 81px">large</td>
<td class="gt_row gt_right" style="max-width: 117px">17.6693215293</td>
<td class="gt_row gt_right" style="max-width: 138px">2.62831687848e-05</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000157699012709</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 81px">adeno</td>
<td class="gt_row gt_left" style="max-width: 81px">smallcell</td>
<td class="gt_row gt_right" style="max-width: 117px">0.096843191971</td>
<td class="gt_row gt_right" style="max-width: 138px">0.755651328687</td>
<td class="gt_row gt_right" style="max-width: 138px">0.755651328687</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 81px">adeno</td>
<td class="gt_row gt_left" style="max-width: 81px">squamous</td>
<td class="gt_row gt_right" style="max-width: 117px">12.0454836411</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000519180149284</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00259590074642</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_left" style="max-width: 81px">large</td>
<td class="gt_row gt_left" style="max-width: 81px">smallcell</td>
<td class="gt_row gt_right" style="max-width: 117px">9.37090414818</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00220456751896</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00661370255689</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_left" style="max-width: 81px">large</td>
<td class="gt_row gt_left" style="max-width: 81px">squamous</td>
<td class="gt_row gt_right" style="max-width: 117px">0.822593978656</td>
<td class="gt_row gt_right" style="max-width: 138px">0.364422837484</td>
<td class="gt_row gt_right" style="max-width: 138px">0.728845674968</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_left" style="max-width: 81px">smallcell</td>
<td class="gt_row gt_left" style="max-width: 81px">squamous</td>
<td class="gt_row gt_right" style="max-width: 117px">11.57367392</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000668921225041</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00267568490017</td>
</tr>
</tbody>
</table>


Each row is one pair, with its own statistic, the raw `p_value`, and a `p_adjusted` value that accounts for the multiple comparisons. The default correction is Holm's; pass `correction="bh"` for Benjamini-Hochberg, `"bonferroni"`, or `"none"`. Read significance from the adjusted column, not the raw one.


# Restricted mean survival time (RMST) comparisons

While the log-rank test tells you whether groups differ, it does not tell you by how much. The restricted mean survival time (RMST) is an intuitive effect measure: the expected survival time over a fixed time window `tau`, computed as the area under the survival curve. For example, 1-year RMST is the average survival time during the first year. Unlike hazard ratios, RMST is on the survival time scale, making it easy to interpret and explain to non-technical audiences.


## Two-group RMST comparison

Compare RMST between two groups with confidence intervals and hypothesis tests using [rmst_test()](../reference/rmst_test.md#greenwood.rmst_test). Here we compare 1-year RMST between the two sexes in the `lung` dataset.


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

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

# Compare 1-year RMST between groups
result = gw.rmst_test(y, tau=365, group=lung["sex"])
result
```


    RMSTResult(method='RMST difference (tau=365)', estimate=-55.9703, se=14.9581, 95% CI=[-85.2877, -26.6529], p_value=0.0001827)


The result shows the RMST for each group, their difference, standard error, 95% confidence interval, and a two-sided p-value testing equality. A positive difference means group 1 has higher RMST (longer average survival).

You can extract specific components:


``` python
print(f"RMST difference: {result.estimate:.1f} days")
print(f"95% CI: [{result.lower_ci:.1f}, {result.upper_ci:.1f}]")
print(f"P-value: {result.p_value:.4f}")
```


    RMST difference: -56.0 days
    95% CI: [-85.3, -26.7]
    P-value: 0.0002


## Alternative estimands

By default, [rmst_test()](../reference/rmst_test.md#greenwood.rmst_test) returns the RMST difference. You can also compute the ratio or percentage difference using the [estimand](../reference/RMSTResult.md#greenwood.RMSTResult.estimand) parameter.

**Ratio of RMSTs** (group 1 / group 2):


``` python
result_ratio = gw.rmst_test(y, tau=365, group=lung["sex"], estimand="ratio")
result_ratio.estimate  # Ratio
```


    0.8118425788462916


A ratio of `0.8` means group 1 has 80% of the RMST of group 2 (20% shorter average survival).

**Percentage difference** ((group 1 - group 2) / group 2 × 100):


``` python
result_pct = gw.rmst_test(y, tau=365, group=lung["sex"], estimand="percentage_difference")
result_pct.estimate  # Percentage
```


    -18.81574211537084


All three estimands use the delta method to construct standard errors and confidence intervals, and support flexible confidence levels via the `conf_level=` parameter.


## Pairwise RMST comparisons

When you have three or more groups, use [pairwise_rmst_test()](../reference/pairwise_rmst_test.md#greenwood.pairwise_rmst_test) to compare all pairs with multiple-comparison adjustment. Here we test which cell types in the `veteran` dataset differ in 1-year RMST.


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

y_vet = gw.Surv.right(vet["time"], event=vet["status"])

# Pairwise RMST differences with Holm adjustment
pairs = gw.pairwise_rmst_test(y_vet, tau=365, group=vet["celltype"])
pairs
```


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

group1

<em>str</em>

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

group2

<em>str</em>

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

rmst1

<em>f64</em>

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

rmst2

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

se

<em>f64</em>

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

lower_ci

<em>f64</em>

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

upper_ci

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

p_adjusted

<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: 81px">adeno</td>
<td class="gt_row gt_left" style="max-width: 81px">large</td>
<td class="gt_row gt_right" style="max-width: 110px">65.5555555556</td>
<td class="gt_row gt_right" style="max-width: 110px">162.234567901</td>
<td class="gt_row gt_right" style="max-width: 117px">-96.6790123457</td>
<td class="gt_row gt_right" style="max-width: 110px">22.7443674012</td>
<td class="gt_row gt_right" style="max-width: 117px">-141.257153303</td>
<td class="gt_row gt_right" style="max-width: 117px">-52.1008713881</td>
<td class="gt_row gt_right" style="max-width: 124px">-4.25067932821</td>
<td class="gt_row gt_right" style="max-width: 138px">2.13123118245e-05</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000127873870947</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">1</td>
<td class="gt_row gt_left" style="max-width: 81px">adeno</td>
<td class="gt_row gt_left" style="max-width: 81px">smallcell</td>
<td class="gt_row gt_right" style="max-width: 110px">65.5555555556</td>
<td class="gt_row gt_right" style="max-width: 110px">77.6352880658</td>
<td class="gt_row gt_right" style="max-width: 117px">-12.0797325103</td>
<td class="gt_row gt_right" style="max-width: 110px">17.1396326857</td>
<td class="gt_row gt_right" style="max-width: 117px">-45.6727952825</td>
<td class="gt_row gt_right" style="max-width: 117px">21.5133302619</td>
<td class="gt_row gt_right" style="max-width: 124px">-0.704783628202</td>
<td class="gt_row gt_right" style="max-width: 138px">0.480944898525</td>
<td class="gt_row gt_right" style="max-width: 138px">0.961889797051</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">2</td>
<td class="gt_row gt_left" style="max-width: 81px">adeno</td>
<td class="gt_row gt_left" style="max-width: 81px">squamous</td>
<td class="gt_row gt_right" style="max-width: 110px">65.5555555556</td>
<td class="gt_row gt_right" style="max-width: 110px">170.642372598</td>
<td class="gt_row gt_right" style="max-width: 117px">-105.086817043</td>
<td class="gt_row gt_right" style="max-width: 110px">26.1899192619</td>
<td class="gt_row gt_right" style="max-width: 117px">-156.418115554</td>
<td class="gt_row gt_right" style="max-width: 117px">-53.7555185313</td>
<td class="gt_row gt_right" style="max-width: 124px">-4.01249106543</td>
<td class="gt_row gt_right" style="max-width: 138px">6.00813536891e-05</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000300406768445</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">3</td>
<td class="gt_row gt_left" style="max-width: 81px">large</td>
<td class="gt_row gt_left" style="max-width: 81px">smallcell</td>
<td class="gt_row gt_right" style="max-width: 110px">162.234567901</td>
<td class="gt_row gt_right" style="max-width: 110px">77.6352880658</td>
<td class="gt_row gt_right" style="max-width: 117px">84.5992798354</td>
<td class="gt_row gt_right" style="max-width: 110px">24.7760388636</td>
<td class="gt_row gt_right" style="max-width: 117px">36.0391359831</td>
<td class="gt_row gt_right" style="max-width: 117px">133.159423688</td>
<td class="gt_row gt_right" style="max-width: 124px">3.41456034603</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000638850399879</td>
<td class="gt_row gt_right" style="max-width: 138px">0.00255540159952</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">4</td>
<td class="gt_row gt_left" style="max-width: 81px">large</td>
<td class="gt_row gt_left" style="max-width: 81px">squamous</td>
<td class="gt_row gt_right" style="max-width: 110px">162.234567901</td>
<td class="gt_row gt_right" style="max-width: 110px">170.642372598</td>
<td class="gt_row gt_right" style="max-width: 117px">-8.40780469693</td>
<td class="gt_row gt_right" style="max-width: 110px">31.7174551961</td>
<td class="gt_row gt_right" style="max-width: 117px">-70.5728745625</td>
<td class="gt_row gt_right" style="max-width: 117px">53.7572651687</td>
<td class="gt_row gt_right" style="max-width: 124px">-0.265084466738</td>
<td class="gt_row gt_right" style="max-width: 138px">0.790944393171</td>
<td class="gt_row gt_right" style="max-width: 138px">0.961889797051</td>
</tr>
<tr>
<td class="gt_row gt_right gd-tbl-rownum">5</td>
<td class="gt_row gt_left" style="max-width: 81px">smallcell</td>
<td class="gt_row gt_left" style="max-width: 81px">squamous</td>
<td class="gt_row gt_right" style="max-width: 110px">77.6352880658</td>
<td class="gt_row gt_right" style="max-width: 110px">170.642372598</td>
<td class="gt_row gt_right" style="max-width: 117px">-93.0070845323</td>
<td class="gt_row gt_right" style="max-width: 110px">27.9724458036</td>
<td class="gt_row gt_right" style="max-width: 117px">-147.832070867</td>
<td class="gt_row gt_right" style="max-width: 117px">-38.1820981978</td>
<td class="gt_row gt_right" style="max-width: 124px">-3.32495360561</td>
<td class="gt_row gt_right" style="max-width: 138px">0.000884332493635</td>
<td class="gt_row gt_right" style="max-width: 138px">0.0026529974809</td>
</tr>
</tbody>
</table>


Each row compares one pair of groups, showing the RMST estimates, difference, and both raw and adjusted p-values. The adjustment method (default: Holm) controls for multiple testing. Pass `correction="bh"` for Benjamini-Hochberg or `"bonferroni"` for Bonferroni correction.


## Choosing the restriction time tau

The choice of `tau` is critical: RMST at `tau=365` only includes follow-up through 1 year, while `tau=1825` extends to 5 years. Choose `tau` based on clinical relevance before looking at the data. Common choices are clinically meaningful milestones (1 year, 5 years, etc.).


``` python
# Compare 5-year RMST instead
result_5y = gw.rmst_test(y, tau=1825, group=lung["sex"])
result_5y.estimate
```


    -172.70616141861058


> **Note: RMST vs. log-rank: complementary tools**
>
> - log-rank test: answers whether curves differ overall (yes/no).
> - RMST comparison: answers by how much they differ and how long the difference persists.
>
> Use both: run the log-rank test for significance, then estimate RMST difference for the magnitude and direction of the effect. They often agree, but RMST is easier to communicate to stakeholders unfamiliar with hazards.


# Stratified tests

Sometimes you want to compare groups while controlling for a nuisance variable, so that the comparison is made within each level of that variable and then combined. A stratified log-rank does this. Here we compare the two treatments while stratifying by cell type, which removes any cell-type imbalance from the comparison.


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

y_vet = gw.Surv.right(vet["time"], event=vet["status"])

gw.logrank_test(y_vet, group=vet["trt"], strata=vet["celltype"])
```


    TestResult(method='Stratified log-rank test', statistic=0.7017, df=1, p_value=0.4022)


The statistic is pooled across the strata, matching R's `survdiff` with a `strata()` term. Stratification is the test-level counterpart to a stratified Cox model, where each stratum has its own baseline hazard.


# Testing for trend in ordered groups

When your groups are naturally ordered--disease stages (I, II, III, IV), dose levels (low, medium, high), or educational attainment (primary, secondary, tertiary), a general log-rank test asks the right question but it doesn't use the information about ordering. A trend test is more powerful because it looks specifically for a linear relationship across the ordered levels.

The trend test compares a linear contrast across groups to see if survival changes systematically with group ordering. It produces a one-degree-of-freedom chi-square test, which is more powerful than the multi-degree-of-freedom log-rank test when groups are truly ordered.


## Default scoring

By default, groups are sorted and assigned scores 0, 1, 2, …, (*k* - 1). Here we test whether survival differs across cell types in the `veteran` dataset, treating them as an ordered grouping.


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

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

# Test linear trend across ordered cell types
gw.trend_test(y, group=vet["celltype"])
```


    TestResult(method='Linear trend test', statistic=2.3681, df=1, p_value=0.1238)


The result is a [TestResult](../reference/TestResult.md#greenwood.TestResult) with `df=1`, indicating a one-degree-of-freedom test. The `observed` and `expected` event counts per group reveal which groups contribute most to the trend, and the p-value tells you if the trend is statistically significant.


## Custom scoring

If the ordering is not uniform--for instance, some groups have very different prognoses--you can specify custom scores. Scores must be numeric and reflect your belief about the ordering.


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

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

# Custom scores: adeno and large get weight 1, smallcell and squamous get weight 3
scores = {"adeno": 1, "large": 1, "smallcell": 3, "squamous": 3}
gw.trend_test(y, group=vet["celltype"], scores=scores)
```


    TestResult(method='Linear trend test', statistic=0.0263, df=1, p_value=0.8713)


Different scores can change the test statistic, reflecting your scientific hypothesis about how the groups are ordered.


## Weighted trend tests

Like the log-rank test, the trend test can use Fleming-Harrington weights to emphasize early or late differences. With `rho=1`, the Peto-Peto variant emphasizes early event times, which is useful if you suspect early separation of the curves.


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

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

# Peto-Peto trend test: weights early events more heavily
gw.trend_test(y, group=vet["celltype"], rho=1)
```


    TestResult(method='G-rho trend test (rho=1, gamma=0.0)', statistic=0.6858, df=1, p_value=0.4076)


## Stratified trend tests

You can also stratify a trend test to control for confounders while still testing for linear trend across ordered groups. The stratified test computes the trend separately in each stratum and then combines the results.


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

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

# Test trend in celltype while controlling for treatment
gw.trend_test(y, group=vet["celltype"], strata=vet["trt"])
```


    TestResult(method='Stratified linear trend test', statistic=1.7525, df=1, p_value=0.1856)


> **Note: When to use trend tests vs. pairwise tests**
>
> - use [trend_test()](../reference/trend_test.md#greenwood.trend_test) when groups are ordered and you care about detecting a linear trend. It's more powerful for this specific hypothesis.
> - use [pairwise_logrank_test()](../reference/pairwise_logrank_test.md#greenwood.pairwise_logrank_test) when you want to know which specific pairs of unordered groups differ. It tests all pairwise differences, not a linear trend.
> - use [logrank_test()](../reference/logrank_test.md#greenwood.logrank_test) for an overall test of differences without assuming any structure on the groups.


# Study design: sample size and power

The log-rank test also answers a planning question asked before any data are collected: how large must a study be? Under proportional hazards, its power depends on the data only through the number of events, so Greenwood provides Schoenfeld's calculators. To detect a given hazard ratio at a target power, [logrank_n_events](../reference/logrank_n_events.md#greenwood.logrank_n_events) returns the events required.


``` python
gw.logrank_n_events(hazard_ratio=0.5, power=0.9, alpha=0.05)
```


    88


Running it the other way, [logrank_power](../reference/logrank_power.md#greenwood.logrank_power) gives the power a planned study would have for a given number of events, which is useful for checking whether an existing dataset can support a comparison.


``` python
gw.logrank_power(hazard_ratio=0.5, n_events=60)
```


    0.7656462084964221


Events are not the same as subjects. To turn the required events into a sample size, divide by the probability that a subject has the event during the study, which you estimate from the expected survival and the planned follow-up. The [logrank_sample_size()](../reference/logrank_sample_size.md#greenwood.logrank_sample_size) function does this.


``` python
gw.logrank_sample_size(hazard_ratio=0.5, prob_event=0.4, power=0.9)
```


    219


> **Note: What the calculators assume**
>
> These formulas assume proportional hazards, a two-group comparison, and (by default) a balanced, two-sided test at `alpha=0.05`. Use `allocation` for an unbalanced design and `sides=1` for a one-sided test. A balanced design needs the fewest events.


# From tests to models

A group comparison test is the right tool when you have one categorical grouping and want a yes-or-no answer about a difference. When you need to adjust for other variables, handle a continuous predictor, or estimate the size of an effect, you move to regression. The log-rank test has a close connection to the Cox model: the score test from a Cox model with a single group indicator is essentially the log-rank test, which is why the two usually agree.


# Next steps

You can now describe and formally compare survival across groups.

- [Visualizing survival](visualization.md) shows how to present grouped curves clearly.
- [Cox regression](cox-regression.md) estimates hazard ratios and adjusts for multiple covariates.
- Revisit [Kaplan-Meier](kaplan-meier.md) for restricted-mean-survival summaries, which give an interpretable effect size to accompany a significant test.
