pairwise_logrank_test()

Pairwise log-rank tests for all group pairs with multiple-comparison correction.

Usage

Source

pairwise_logrank_test(
    surv,
    group=None,
    *,
    data=None,
    weights=None,
    rho=0.0,
    gamma=0.0,
    strata=None,
    correction="holm",
    format=None,
)

Runs the log-rank test on every pair of groups, then adjusts p-values to control for multiple testing. This answers the question: “Which pairs of groups have significantly different survival?” when you have more than two groups.

After the global log-rank test (via logrank_test) indicates groups differ, this pairwise test reveals which pairs are significantly different and by how much. P-values are adjusted across all pairs using a chosen correction method to control the false discovery rate or family-wise error rate.

Typical workflow: First run logrank_test to test overall group differences. If significant, use pairwise_logrank_test to identify which pairs differ. The adjusted p-values account for testing multiple pairs from the same data.

Parameters

surv: Surv | Outcome | str

A Surv response object representing censored survival times. Supports right-censored data or counting-process format. Constructed with gw.Surv(), for example gw.Surv(time=..., event=...) or gw.Surv(time=..., time2=..., event=...). 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, one per observation. Can be a Narwhals series, 1-D array, or Python sequence. Must have at least 3 unique levels (to create multiple pairs). Must have the same length as surv.

rho: float = 0.0

Fleming-Harrington weight exponents for the log-rank test (same as logrank_test). Default (0, 0) gives standard log-rank; (1, 0) gives Peto-Peto (emphasizes early events).

gamma: float = 0.0

Fleming-Harrington weight exponents for the log-rank test (same as logrank_test). Default (0, 0) gives standard log-rank; (1, 0) gives Peto-Peto (emphasizes early events).

strata: Any = None

Optional stratifying factor. When provided, each pairwise test is stratified by this factor (computed within each stratum, then combined). Use to control for confounding.

correction: str = "holm"

Multiple-comparison adjustment method applied across all pairwise p-values:

  • "holm" (default): Controls family-wise error rate. Conservative; recommended for small numbers of pairs (fewer than ~10).
  • "bh": Benjamini-Hochberg false-discovery rate. Less conservative; recommended for many pairs. Allows more false positives but focuses on their rate.
  • "bonferroni": Bonferroni correction. Very conservative; adjusted p = p \times m, where m is the number of pairs.
  • "none": No adjustment. Use only if you’re testing a single pre-planned pair (though use logrank_test directly in that case).
format: str | None = None

Output format: None (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).

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

pandas.DataFrame, polars.DataFrame, or pyarrow.Table

One row per pair of groups with columns:

  • group1, group2: The pair of group labels being compared.
  • statistic: Chi-square test statistic for the pair.
  • p_value: Raw (unadjusted) log-rank p-value for the pair.
  • p_adjusted: Adjusted p-value after multiple-comparison correction. Use this for significance testing (e.g., p_adjusted < 0.05).

Details

The number of pairs tested is C(k, 2) = k(k-1)/2, where k is the number of groups. For k=3, that’s 3 pairs; for k=5, that’s 10 pairs. Larger numbers of pairs can reduce power per comparison (wider adjusted confidence intervals), so keep the number of groups reasonable when possible.

The adjustment method affects stringency: Holm controls false discovery more strictly (lower type-I error, higher type-II error), while Benjamini-Hochberg is more permissive (higher type-I error rate overall, but controls the proportion of false discoveries).

Examples

Test pairwise survival differences among the four cell types in the veteran dataset. A global log-rank test first shows that cell types differ overall, but doesn’t say which pairs differ:

import greenwood as gw

# Load the data
vet = gw.load_dataset("veteran", backend="polars")

# Run a global log-rank test across all cell types
gw.logrank_test("Surv(time, status) ~ celltype", data=vet)
TestResult(method='Log-rank test', statistic=25.4037, df=3, p_value=1.271e-05)

The pairwise test compares all six pairs of cell types and returns a table with the test statistic, raw p-value, and adjusted p-value for each pair. Pass format= to choose the backend (here, Polars); use p_adjusted for significance testing:

# Compare all pairs of cell types with Holm-adjusted p-values
pairs = gw.pairwise_logrank_test("Surv(time, status) ~ celltype", data=vet, format="polars")
pairs
shape: (6, 5)
group1group2statisticp_valuep_adjusted
strstrf64f64f64
"adeno""large"17.6693220.0000260.000158
"adeno""smallcell"0.0968430.7556510.755651
"adeno""squamous"12.0454840.0005190.002596
"large""smallcell"9.3709040.0022050.006614
"large""squamous"0.8225940.3644230.728846
"smallcell""squamous"11.5736740.0006690.002676

Filter to significant pairs (adjusted p-value < 0.05). Request format="pandas" here so we can use boolean-mask filtering:

# Filter to pairs with statistically significant differences
pairs = gw.pairwise_logrank_test("Surv(time, status) ~ celltype", data=vet, format="pandas")
pairs[pairs["p_adjusted"] < 0.05]
group1 group2 statistic p_value p_adjusted
0 adeno large 17.669322 0.000026 0.000158
2 adeno squamous 12.045484 0.000519 0.002596
3 large smallcell 9.370904 0.002205 0.006614
5 smallcell squamous 11.573674 0.000669 0.002676

Use the Peto-Peto (Wilcoxon) weighting to emphasize early survival differences:

# Use Peto-Peto weighting to emphasize early event times
gw.pairwise_logrank_test("Surv(time, status) ~ celltype", rho=1, data=vet, format="polars")
shape: (6, 5)
group1group2statisticp_valuep_adjusted
strstrf64f64f64
"adeno""large"13.8317370.00020.0012
"adeno""smallcell"0.0418610.8378841.0
"adeno""squamous"6.4849050.0108790.032638
"large""smallcell"12.2927520.0004550.002274
"large""squamous"0.0214840.8834691.0
"smallcell""squamous"7.8879250.0049770.019906

Use Benjamini-Hochberg adjustment (less conservative) if you’re interested in which pairs show evidence of differences (false-discovery rate control rather than family-wise error):

# Use Benjamini-Hochberg correction for false-discovery rate control
gw.pairwise_logrank_test(
    "Surv(time, status) ~ celltype", correction="bh", data=vet, format="polars"
)
shape: (6, 5)
group1group2statisticp_valuep_adjusted
strstrf64f64f64
"adeno""large"17.6693220.0000260.000158
"adeno""smallcell"0.0968430.7556510.755651
"adeno""squamous"12.0454840.0005190.001338
"large""smallcell"9.3709040.0022050.003307
"large""squamous"0.8225940.3644230.437307
"smallcell""squamous"11.5736740.0006690.001338