Weighted log-rank, max-combo, and milestone survival
When curves cross or an effect is delayed, the log-rank test adds every event time with equal weight and the two halves of the signal cancel. This page covers the shape of the Fleming-Harrington weights, why a delayed immunotherapy effect needs late weighting, what max-combo charges you for having tried three weights, and why the two confidence-interval transforms for a milestone survival rate disagree so much. Real data does not hand you a test that flips a null result, and that non-finding is on the page too.
The log-rank test treats every event time as equally important
Kaplan-Meier and the log-rank test took the statistic apart: at each event time it computes observed deaths minus the deaths expected under the null, adds those differences up, and divides by a standard error. Written out:
The standard log-rank test sets every to 1. That is a choice, not a law, and it is the optimal choice only when the hazard ratio is constant over the whole of follow-up.
If the two curves cross — one arm worse early, then overtaking — the early terms are positive and the late ones negative, so an unweighted sum cancels them against each other and the statistic collapses towards zero. That is not evidence that the arms are alike; it is this way of adding up being unable to see the difference. Immunotherapy trials in which the two progression-free survival curves overlap for the first six months and separate afterwards have been routine since 2019, and that is exactly the shape handles worst.
A weighted log-rank test does one thing: it replaces . Everything else is unchanged.
The example on this page: data whose proportional-hazards assumption clearly fails
survival::veteran is the US Veterans Administration lung cancer trial: 137 patients, 128 deaths, and only 9 censored observations. It was chosen not
because it produces a dramatic result but because its proportional-hazards assumption genuinely
does not hold:
| Term | χ² | df | p |
|---|---|---|---|
| Treatment arm | 0.21 | 1 | 0.650 |
| Karnofsky performance score | 12.81 | 1 | 3.4e-4 |
| Tumour cell type | 14.53 | 3 | 0.002 |
| GLOBAL | 22.86 | 5 | 3.6e-4 |
The model is coxph(Surv(time, status) ~ trt + karno + celltype) and the check is the
cox.zph() procedure from the assumptions page.
The global p is 3.6e-4, so the violation is not marginal.
What the weights look like
The Fleming-Harrington family sets the weight from two parameters, and :
is the Kaplan-Meier estimate from the pooled sample. Three corners of that family:
- : the weight is always 1 — the standard log-rank test.
- : the weight equals , so it decreases over time and is early-weighted (the Peto / Gehan idea).
- : the weight equals , increases over time, and is late-weighted.
That is enough abstraction. Here is the pooled KM estimate and all three weights at quantiles of the observed event times:
| Event-time quantile | t (days) | Ŝ(t⁻) | ρ = 0 | ρ = 1 | FH(0, 1) |
|---|---|---|---|---|---|
| 10% | 10.0 | 0.898 | 1.000 | 0.898 | 0.102 |
| 30% | 29.1 | 0.715 | 1.000 | 0.715 | 0.285 |
| 50% | 62.0 | 0.531 | 1.000 | 0.531 | 0.469 |
| 70% | 125.6 | 0.338 | 1.000 | 0.338 | 0.662 |
| 90% | 295.1 | 0.117 | 1.000 | 0.117 | 0.883 |
Read the last row. By day 295, where the latest 10% of events sit, assigns a weight of only 0.117 — against 0.898 on day 10. In the same dataset, a late death contributes 13% of what an early death contributes, which is close to not looking at late follow-up at all. The FH(0, 1) column is the mirror image: 0.883 at that late time point against 0.102 on day 10.
figures/scripts/B3-09-weighted-logrank.RWhy a delayed effect needs FH(0, 1)
The immunotherapy shape goes like this: for the first few months the curves are glued together (the drug has not taken effect yet, and some patients will never respond), and then the treated curve flattens while the control curve keeps falling. The real signal is entirely in the late stretch.
The cost of here is that the long run of early event times, where there is nothing to see, enters carrying exactly as much weight as the late ones, diluting the difference. The table above shows how large that problem is: event times are dense early and sparse late, so in practice equal weighting is early weighting. FH(0, 1) simply corrects that implicit tilt in the other direction.
Changing the weight did not change the conclusion — and that is the lesson
First the four-way comparison across tumour cell types (4 groups, df = 3):
| Weight | χ² | df | p | At α = 0.05 | Source |
|---|---|---|---|---|---|
| rho = 0: standard log-rank | 25.40 | 3 | 1.3e-5 | significant | survdiff |
| rho = 0.5: mildly early-weighted | 22.71 | 3 | 4.6e-5 | significant | survdiff |
| rho = 1: early-weighted | 19.71 | 3 | 1.9e-4 | significant | survdiff |
| FH(0, 1): late-weighted | 25.79 | 3 | 1.1e-5 | significant | hand-computed |
Group sizes and event counts are squamous 35 patients with 31 deaths; smallcell 48 patients with 45 deaths; adeno 27 patients with 26 deaths; large 27 patients with 26 deaths. Now the two-arm treatment comparison:
| Weight | χ² | df | p | At α = 0.05 | Source |
|---|---|---|---|---|---|
| rho = 0: standard log-rank | 0.01 | 1 | 0.928 | not significant | survdiff |
| rho = 0.5: mildly early-weighted | 0.47 | 1 | 0.491 | not significant | survdiff |
| rho = 1: early-weighted | 0.87 | 1 | 0.351 | not significant | survdiff |
| FH(0, 1): late-weighted | 0.81 | 1 | 0.369 | not significant | hand-computed |
The cell-type comparison is significant under all four weights, so the conclusion does not move;
the treatment comparison fails to reach significance under all four weights, so that conclusion does not move either.
The machine-readable form of this is the anyConclusionChanged field, which is false and false for the two contrasts.
Max-combo: multiplicity as a number rather than a warning
If you do not know in advance whether the signal will arrive early or late, an honest option is to run several weights at once, take the most extreme statistic, and then charge yourself for having looked at several. That is max-combo.
The three components used here are rho0, rho1, fh01.
Their z values (computed from observed minus expected in the Standard arm, so a
negative sign means fewer deaths than expected in that arm) are:
| Component | z | Direction |
|---|---|---|
| rho = 0: standard log-rank | -0.091 | fewer deaths than expected in the Standard arm |
| rho = 1: early-weighted | -0.933 | fewer deaths than expected in the Standard arm |
| FH(0, 1): late-weighted | 0.898 | more deaths than expected in the Standard arm |
That table explains in full why the log-rank test sees nothing here. The early-weighted z is -0.933 and the late-weighted z is 0.898: opposite signs — early and late point in different directions, which is the crossing shape in the milestone KM plot in the next section. The standard log-rank test adds those two stretches with equal weight and lands on z = -0.091, essentially zero, which is the p = 0.928 in the table above.
The three statistics are strongly correlated, because they are weighted sums over the same event times:
| Correlation | rho0 | rho1 | fh01 |
|---|---|---|---|
rho0 | 1.000 | 0.891 | 0.855 |
rho1 | 0.891 | 1.000 | 0.526 |
fh01 | 0.855 | 0.526 | 1.000 |
With the correlation matrix in hand you can integrate the trivariate normal and price the act of taking a maximum:
| What is being reported | p | Can it go in the paper? |
|---|---|---|
| Pre-specified standard log-rank | 0.928 | Yes |
| The best-looking of the three weights | 0.351 | No — it was selected |
| Max-combo, with the selection priced in | 0.549 | Yes, if the components were declared in advance |
Here multiplicity is not a reminder to correct for something, it is a visible number: 0.351 becomes 0.549. For comparison, a Bonferroni correction over the same three tests would give 1.052 (capped at 1 in practice), far more conservative than max-combo — because Bonferroni assumes the three tests are independent, and the correlation matrix above says they are anything but. The more correlated the tests, the smaller the multiplicity price that is actually due, which is the same point made on multiple comparisons.
Milestone survival: the difference in survival at a stated day
Milestone survival is the version that needs the least statistical training to read — one-year survival was X% against Y%, a difference of Z percentage points — and it depends on no proportionality assumption at all. It reads the height of the KM curve at one vertical slice.
figures/scripts/B3-09-weighted-logrank.RThe three time points (the difference is Standard minus Test):
| Day | Standard | Test | Difference (pp) | 95% CI (pp) |
|---|---|---|---|---|
| 90 | 54.7% (37 at risk) | 38.0% (25 at risk) | 16.7 | 0.1 to 33.2 |
| 180 | 21.2% (13 at risk) | 23.3% (14 at risk) | -2.0 | -16.5 to 12.4 |
| 365 | 7.1% (4 at risk) | 11.0% (6 at risk) | -3.9 | -14.2 to 6.5 |
Note that the sign of the difference flips: 16.7 percentage points on day 90, -2.0 on day 180, and -3.9 on day 365. This is precisely what the log-rank p of 0.928 conceals: a statistic that compresses the whole of follow-up into one number cannot tell you that the sign changed sides along the way.
Note the day 90 row as well: the 95% CI for the difference runs from 0.1 to 33.2 percentage points, so the lower bound only just clears zero. An interval sitting on the edge like that is the one to ask about: was this time point fixed in advance? If it was picked from three after seeing the plot, it cannot be read at face value, for the same reason as in the max-combo section.
The two interval transforms do not agree
There are two common ways to build a confidence interval for a survival probability, and they differ in whether the interval is computed on a transformed scale or directly on the probability scale. Laying out day 365 for both arms:
| Arm | Survival | Greenwood (probability scale) | log-log transform |
|---|---|---|---|
| Standard | 0.071 | 0.005 to 0.137 | 0.023 to 0.155 |
| Test | 0.110 | 0.030 to 0.190 | 0.046 to 0.204 |
The point estimate is the same 0.071 either way, but the lower bound for the Standard arm is 0.005 under one and 0.023 under the other — a factor of 4.7. The reason is that a Greenwood interval is symmetric on the probability scale: once the estimate sits close to 0, the lower bound is pushed towards 0 and a slightly smaller estimate would push it below 0, which a survival probability cannot be. The log-log transform builds the interval on the unbounded scale and maps it back, so the interval always lands between 0 and 1 and is asymmetric. That is why oncology trials report log-log intervals.
Running it yourself
library(survival)
data(cancer, package = "survival")
vet <- veteran
vet$trt_f <- factor(vet$trt, levels = c(1, 2), labels = c("Standard", "Test"))
# --- FH(rho, 0): survdiff takes rho only, weight = S(t-)^rho ---------------
survdiff(Surv(time, status) ~ trt_f, data = vet, rho = 0) # standard log-rank
survdiff(Surv(time, status) ~ trt_f, data = vet, rho = 1) # early-weighted
# --- FH(0, 1): out of reach for survdiff, so build the risk set by hand ----
et <- sort(unique(vet$time[vet$status == 1]))
nj <- sapply(et, function(t) sum(vet$time >= t)) # size of risk set
dj <- sapply(et, function(t) sum(vet$time == t & vet$status == 1))
Sminus <- c(1, cumprod(1 - dj / nj))[seq_along(et)] # LEFT-continuous
w_late <- 1 - Sminus # the FH(0, 1) weight
# U = sum(w * (O - E)), with V weighted by w^2; the full implementation and
# its sanity check live in figures/scripts/B3-09-weighted-logrank.R
# --- milestone survival under both interval transforms --------------------
summary(survfit(Surv(time, status) ~ trt_f, data = vet, conf.type = "plain"),
times = c(90, 180, 365)) # Greenwood, on the probability scale
summary(survfit(Surv(time, status) ~ trt_f, data = vet, conf.type = "log-log"),
times = c(90, 180, 365)) # log-log transformVerified on R 4.6.0 with survival 3.8.6; the trivariate normal integration for max-combo additionally uses mvtnorm 1.4.2. survdiff() accepts only rho, so it cannot produce FH(0, 1).
import pandas as pd
from lifelines import KaplanMeierFitter
from lifelines.statistics import (
logrank_test, survival_difference_at_fixed_point_in_time_test)
# statsmodels' get_rdataset cannot find it -- Rdatasets files it under
# another bundle -- so read the CSV directly.
RD = "https://vincentarelbundock.github.io/Rdatasets/csv/"
vet = pd.read_csv(RD + "survival/veteran.csv")
a, b = vet[vet.trt == 1], vet[vet.trt == 2]
# weightings="fleming-harrington" needs both p and q (i.e. rho and gamma)
logrank_test(a.time, b.time, a.status, b.status) # rho = 0
logrank_test(a.time, b.time, a.status, b.status,
weightings="fleming-harrington", p=1, q=0) # rho = 1
logrank_test(a.time, b.time, a.status, b.status,
weightings="fleming-harrington", p=0, q=1) # FH(0, 1)
# milestone: fit one KM per arm, then compare them at one time point
ka = KaplanMeierFitter().fit(a.time, a.status, label="Standard")
kb = KaplanMeierFitter().fit(b.time, b.status, label="Test")
ka.predict(90), kb.predict(90)
survival_difference_at_fixed_point_in_time_test(90, ka, kb)The fleming-harrington weighting in lifelines also uses the left-continuous KM estimate, and its three test statistics match the R above digit for digit (checked by running both). There is no built-in max-combo; the correlation matrix has to be assembled yourself. Note that survival_difference_at_fixed_point_in_time_test() uses a log(-log) transform, which is not the same thing as the Wald interval on the raw difference reported in the table above.
How this page relates to the others
- The log-rank test itself, and how to read a KM plot — see Kaplan-Meier curves and the log-rank test. That page covers the version; this one only replaces .
- How to tell that proportional hazards has failed, and what else can be done about it — see
the proportional-hazards assumption: stratification, time
splitting, and
tt()for a coefficient that varies with time. That page also explains why truncating follow-up until the assumption holds is not a repair. - When you want an effect measure with units that you can say to a patient — see restricted mean survival time (RMST). A weighted log-rank test still returns only a p value; it carries no effect measure, exactly like the standard one. Under non-proportional hazards, months gained or percentage points at a stated year beats any p value.
- Pricing the selection of a weight or a time point — see multiple comparisons and type I error.
- How the statistics section of a trial hangs together — see the randomised controlled trial.
Common misuse
| Misuse | Why it is wrong |
|---|---|
| The standard log-rank test failed, so try weights until one passes | Uncorrected multiple testing; pre-specify the weight or use max-combo |
| Reporting max-combo whose components were chosen afterwards | The correction prices taking a maximum, not swapping the component list and recomputing |
| Treating a weighted log-rank p value as evidence of effect size | It has no effect measure, just like the standard test; use RMST or a milestone difference |
| Reporting a single HR when the curves cross | That HR is a weighted average of two opposite effects and has no clear interpretation |
| Choosing the milestone time point after the analysis | Same selective reporting as choosing τ or the weight afterwards |
| Reporting a milestone rate without the number at risk there | The reader cannot tell how many people the percentage rests on |
| Greenwood intervals when survival is near 0 or 1 | A symmetric interval on the probability scale runs past the boundary; use the log-log transform |
| Computing both intervals and reporting the narrower one | The transform must be chosen in advance; choosing afterwards is choosing the result |
| Using Ŝ(t) rather than Ŝ(t⁻) as the weight | The convention is left-continuous; getting it wrong raises no error and biases the statistic |
| Saying the arms are equivalent because the weighted test was not significant | Only that no difference was detected; equivalence needs a non-inferiority design and a pre-set margin |
Reproducing every number on this page
/opt/homebrew/bin/Rscript figures/scripts/B3-09-weighted-logrank.RRead the figure
The answer comes from the same statistical output that produced this page's figures, not from a number typed in beside them.
For treatment arm in the veteran data, four weighted log-rank tests give four p-values, none significant. Which is the standard log-rank, and how does it differ from the others?
Show the answer and why
Correct answer: 0.928 - the standard log-rank, weighting every event time equally
The standard log-rank is by definition unweighted: every event time contributes equally, giving 0.928. 0.491 and 0.351 come from versions that shift weight towards later times - more sensitive to curves that separate late, at the cost of being blunt about early differences. Same comparison, same data, and the p-value travels from 0.928 to 0.351. All four are non-significant here, so the conclusion does not move; but if one of them dropped below the threshold and the paper reported only that one, a reader would have no way to see the others.
The max-combo test pools several weighted statistics. Its p-value is larger than the smallest of them. What does that gap represent?
Show the answer and why
Correct answer: 0.549 - the p-value after correcting for having tried several weightings
0.549 is the corrected max-combo p-value and 0.351 is the smallest of the individual tests. Reporting the smallest is selection after the fact: run four weightings, keep the prettiest, and the type I error rate is no longer the nominal one. The gap between the two is exactly the price of that multiplicity, which makes max-combo a way of being honest about having tried several weightings rather than a way of improving the result. 0.928 is the standard log-rank, and there is no reason for max-combo to return to it.
The GLOBAL row of cox.zph is significant for the veteran model. How do you find out which terms violate proportional hazards?
Show the answer and why
Correct answer: Read row by row: karno's chi-square is 12.81, it and one other violate, treatment arm does not
GLOBAL says only that something in the model violates; finding out what means going back to the per-term rows. Karno's chi-square is 12.81 and celltype's is 14.53, so both violate; treatment arm at 0.21 does not. Taking only the largest misses a second term violating just as clearly, and the smallest is neither a global figure nor a summary of anything. This page reaches for weighted log-rank precisely because proportional hazards does not hold in these data - the standard log-rank is not wrong there, it simply weights every time point alike.
Chapters that use this method
Sources and licences
This page is original writing