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)
Pairwise log-rank tests for all group pairs with multiple-comparison correction.
Usage
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.
surv: SurvA 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: AnyGroup 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.0Fleming-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.0Fleming-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 = NoneOptional 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 = NoneNone (default), "pandas", "polars", or "pyarrow". When None, a backend is auto-detected (Polars, then Pandas, then PyArrow).
pandas.DataFrame, polars.DataFrame, or pyarrow.TableOne 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).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).
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:
| group1 | group2 | statistic | p_value | p_adjusted |
|---|---|---|---|---|
| str | str | f64 | f64 | f64 |
| "adeno" | "large" | 17.669322 | 0.000026 | 0.000158 |
| "adeno" | "smallcell" | 0.096843 | 0.755651 | 0.755651 |
| "adeno" | "squamous" | 12.045484 | 0.000519 | 0.002596 |
| "large" | "smallcell" | 9.370904 | 0.002205 | 0.006614 |
| "large" | "squamous" | 0.822594 | 0.364423 | 0.728846 |
| "smallcell" | "squamous" | 11.573674 | 0.000669 | 0.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:
| group1 | group2 | statistic | p_value | p_adjusted |
|---|---|---|---|---|
| str | str | f64 | f64 | f64 |
| "adeno" | "large" | 13.831737 | 0.0002 | 0.0012 |
| "adeno" | "smallcell" | 0.041861 | 0.837884 | 1.0 |
| "adeno" | "squamous" | 6.484905 | 0.010879 | 0.032638 |
| "large" | "smallcell" | 12.292752 | 0.000455 | 0.002274 |
| "large" | "squamous" | 0.021484 | 0.883469 | 1.0 |
| "smallcell" | "squamous" | 7.887925 | 0.004977 | 0.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):
| group1 | group2 | statistic | p_value | p_adjusted |
|---|---|---|---|---|
| str | str | f64 | f64 | f64 |
| "adeno" | "large" | 17.669322 | 0.000026 | 0.000158 |
| "adeno" | "smallcell" | 0.096843 | 0.755651 | 0.755651 |
| "adeno" | "squamous" | 12.045484 | 0.000519 | 0.001338 |
| "large" | "smallcell" | 9.370904 | 0.002205 | 0.003307 |
| "large" | "squamous" | 0.822594 | 0.364423 | 0.437307 |
| "smallcell" | "squamous" | 11.573674 | 0.000669 | 0.001338 |