pairwise_logrank_test()

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

Usage

Source

pairwise_logrank_test(
    surv,
    group,
    *,
    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

A Surv response object representing censored survival times. Supports right-censored data or counting-process format. Constructed with Surv.right(), Surv.counting(), or Surv.multistate().

group: Any

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).

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

vet = gw.load_dataset("veteran", backend="polars")
y = gw.Surv.right(vet["time"], event=vet["status"])
gw.logrank_test(y, group=vet["celltype"])
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:

pairs = gw.pairwise_logrank_test(y, group=vet["celltype"], 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:

pairs = gw.pairwise_logrank_test(y, group=vet["celltype"], 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:

gw.pairwise_logrank_test(y, group=vet["celltype"], rho=1, 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):

gw.pairwise_logrank_test(y, group=vet["celltype"], correction="bh", 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