Test for linear trend across ordered groups using the log-rank test family.
trend_test(
surv,
group=None,
*,
data=None,
weights=None,
scores=None,
rho=0.0,
gamma=0.0,
strata=None,
)
A trend test is a one-degree-of-freedom test for whether survival changes linearly across ordered groups (e.g., disease stages I, II, III, IV or dose levels: low, medium, high). It’s more powerful than the multi-degree-of-freedom log-rank test when groups are naturally ordered.
The test assigns numeric scores to each group (default: 0, 1, 2, … in group order) and tests whether a linear relationship exists between group score and survival. It can be combined with Fleming-Harrington weights and stratification, just like logrank_test.
When to use trend tests: Use when groups are naturally ordered and you want a higher-power test for a linear trend, rather than testing all possible differences. For exploratory analysis without assuming order, use logrank_test instead.
Parameters
surv: Surv | Outcome | str
-
A Surv response object representing censored survival times. Supports right-censored data or counting-process format. An Outcome or a formula string such as 'Surv(time, status == 2) ~ sex' is also accepted, with its columns read from data. The right-hand side names the group column(s) and strata(x) terms set strata.
group: Any = None
-
Group labels (typically ordered categories like 0, 1, 2, 3 for dose levels or stages). Can be a Narwhals series, 1-D array, or Python sequence. Must have at least 2 groups. Must have the same length as surv.
scores: Array | None = None
-
Numeric scores assigned to each group to define the linear trend. If None (default), the groups get the scores 0, 1, 2, …, (k-1) in group order, where k is the number of groups. Numbers are in numeric order, text in code-point order, and a pandas Categorical or a Polars Enum keeps its declared order. Provide custom scores as a dictionary mapping group labels to numeric values (e.g., {1: 0, 2: 1, 3: 2} for stage labels 1,2,3) to use different scoring (e.g., unequal spacing). Scores can be any real numbers (including negative).
rho: float = 0.0
-
Fleming-Harrington weight exponent applied to the pooled survival probability. The default of 0 gives a standard trend test with equal weight across all times.
gamma: float = 0.0
-
Fleming-Harrington weight exponent applied to (1 - pooled survival probability). The default of 0; combined with rho=1 gives Peto-Peto (Wilcoxon) trend test emphasizing early events. See logrank_test for more details on Fleming-Harrington weighting.
strata: Any = None
-
Optional stratifying factor. When provided, the trend test is computed separately within each stratum, then combined (stratified trend test). Use to control for confounding while testing a linear trend.
data: Any = None
-
A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) holding the columns named by the response, group, and strata. When surv is an Outcome or a formula, rows with a missing value in any column used are dropped first, along with the matching rows of any arrays passed alongside.
weights: Any = None
-
Case weights, one per row (a column name in
data, or an array). Must be finite and strictly positive. Default is None (all weights 1).
Returns
TestResult
-
A result object with attributes:
statistic: chi-square test statistic (always 1 degree of freedom).
df: always 1 for trend tests.
p_value: upper-tail chi-square p-value.
method: description of the test, e.g., “Linear trend test”, “Stratified linear trend test”, “G-rho trend test (rho=1, gamma=0)”.
observed: dictionary mapping each group to observed event count (unweighted).
expected: dictionary mapping each group to expected event count under null.
Details
The test uses a linear contrast with assigned scores:
- U = \sum_i \text{score}[i] \cdot (O[i] - E[i])
- V = \sum_i \sum_j \text{score}[i] \cdot \text{score}[j] \cdot \text{Cov}[i, j]
- \chi^2 = U^2 / V \sim \chi^2(1)
Here Cov is the covariance matrix of the observed-minus-expected counts. The counts of the groups are correlated, so the off-diagonal terms matter whenever there are more than two groups. Shifting or rescaling the scores leaves the statistic unchanged.
Without tied event times, this is the score test from a Cox model with the group’s score as a numeric covariate.
Examples
Test for linear trend in survival across ordered ECOG performance-status grades:
import greenwood as gw
# Load the data (rows with a missing ECOG grade are dropped when the test runs)
lung = gw.load_dataset("lung", backend="polars")
# Default: ECOG grades are sorted and assigned scores 0,1,2,3
result = gw.trend_test("Surv(time, status) ~ ph.ecog", data=lung)
result
TestResult(method='Linear trend test', statistic=17.8751, df=1, p_value=2.359e-05)
Use custom scores to give different weight to each grade:
# Quadratic scores emphasize the steep decline from ECOG 2 to ECOG 3
scores = {0: 0, 1: 1, 2: 4, 3: 9}
gw.trend_test("Surv(time, status) ~ ph.ecog", scores=scores, data=lung)
TestResult(method='Linear trend test', statistic=20.4597, df=1, p_value=6.09e-06)
Use Peto-Peto weighting to emphasize early differences:
# Peto-Peto: rho=1 gives more weight to early event times
gw.trend_test("Surv(time, status) ~ ph.ecog", rho=1, gamma=0, data=lung)
TestResult(method='G-rho trend test (rho=1, gamma=0)', statistic=20.0581, df=1, p_value=7.512e-06)
Use Tarone-Ware weighting to emphasize late differences (gamma=1):
# Tarone-Ware: gamma=1 gives more weight to late event times
gw.trend_test("Surv(time, status) ~ ph.ecog", rho=0, gamma=1, data=lung)
TestResult(method='G-rho trend test (rho=0, gamma=1)', statistic=7.2171, df=1, p_value=0.007221)
Stratified by sex to control for a confounder:
# Stratify by sex to adjust for a known confounder
gw.trend_test("Surv(time, status) ~ ph.ecog + strata(sex)", data=lung)
TestResult(method='Stratified linear trend test', statistic=18.5515, df=1, p_value=1.654e-05)