ExpertIndependently reviewed, not yet spot-checked by a human

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:

U=jwj(OjEj)U = \sum_{j} w_j \,\bigl(O_j - E_j\bigr)

The standard log-rank test sets every wjw_j 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 OjEjO_j - E_j 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 wj=1w_j = 1 handles worst.

A weighted log-rank test does one thing: it replaces wjw_j. 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χ²dfp
Treatment arm0.2110.650
Karnofsky performance score12.8113.4e-4
Tumour cell type14.5330.002
GLOBAL22.8653.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, ρ\rho and γ\gamma:

wj=S^(tj)ρ[1S^(tj)]γw_j = \hat{S}(t_j^-)^{\rho}\,\bigl[1 - \hat{S}(t_j^-)\bigr]^{\gamma}

S^\hat{S} is the Kaplan-Meier estimate from the pooled sample. Three corners of that family:

  • ρ=0, γ=0\rho = 0,\ \gamma = 0: the weight is always 1 — the standard log-rank test.
  • ρ=1, γ=0\rho = 1,\ \gamma = 0: the weight equals S^\hat{S}, so it decreases over time and is early-weighted (the Peto / Gehan idea).
  • ρ=0, γ=1\rho = 0,\ \gamma = 1: the weight equals 1S^1 - \hat{S}, 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 quantilet (days)Ŝ(t⁻)ρ = 0ρ = 1FH(0, 1)
10%10.00.8981.0000.8980.102
30%29.10.7151.0000.7150.285
50%62.00.5311.0000.5310.469
70%125.60.3381.0000.3380.662
90%295.10.1171.0000.1170.883

Read the last row. By day 295, where the latest 10% of events sit, ρ=1\rho = 1 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.

Two panels sharing one horizontal axis running from 0 to 400 days. The upper panel plots the weight applied to observed minus expected, from 0 to 1. Three lines: a blue solid line held flat at 1 for the rho = 0 standard log-rank; a red solid line that starts at 1 and falls steadily, reaching close to 0 at the right edge, for the rho = 1 early-weighted test; and a green dashed mirror image that starts at 0 and climbs to close to 1, for the FH(0, 1) late-weighted test. The red and green lines cross at a weight of 0.5, which is where the pooled survival curve has fallen to one half; the blue line stays above both and crosses neither. Two filled dots with dotted drop lines sit on the red curve, labelled t = 10, w = 0.898 and t = 295, w = 0.117. The lower panel is a rug of the actual event times on the same axis, one short vertical tick per death, visibly dense over the first hundred days and thinning afterwards; its heading states how many of the 128 deaths fall inside this window, which is the large majority of them.
One dataset, one set of event times: all that separates the three tests is this weight curve. The rug underneath explains why ρ = 1 matters so much here — the events are already concentrated early, so pushing the late weights down puts almost all of the statistic's attention on the first stretch.Plotting script figures/scripts/B3-09-weighted-logrank.R

Why 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 wj=1w_j = 1 here is that the long run of early event times, where there is nothing to see, enters UU 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χ²dfpAt α = 0.05Source
rho = 0: standard log-rank25.4031.3e-5significantsurvdiff
rho = 0.5: mildly early-weighted22.7134.6e-5significantsurvdiff
rho = 1: early-weighted19.7131.9e-4significantsurvdiff
FH(0, 1): late-weighted25.7931.1e-5significanthand-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χ²dfpAt α = 0.05Source
rho = 0: standard log-rank0.0110.928not significantsurvdiff
rho = 0.5: mildly early-weighted0.4710.491not significantsurvdiff
rho = 1: early-weighted0.8710.351not significantsurvdiff
FH(0, 1): late-weighted0.8110.369not significanthand-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:

ComponentzDirection
rho = 0: standard log-rank-0.091fewer deaths than expected in the Standard arm
rho = 1: early-weighted-0.933fewer deaths than expected in the Standard arm
FH(0, 1): late-weighted0.898more 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:

Correlationrho0rho1fh01
rho01.0000.8910.855
rho10.8911.0000.526
fh010.8550.5261.000

With the correlation matrix in hand you can integrate the trivariate normal and price the act of taking a maximum:

What is being reportedpCan it go in the paper?
Pre-specified standard log-rank0.928Yes
The best-looking of the three weights0.351No — it was selected
Max-combo, with the selection priced in0.549Yes, 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.

Two Kaplan-Meier step curves overlaid on one plot. The horizontal axis is days since randomisation from 0 to 400 and the vertical axis is survival probability from 0 to 1; both curves carry short tick marks for censoring, and confidence bands are deliberately omitted. The blue curve is the Standard arm (69 patients, 64 deaths) and the red curve is the Test arm (68 patients, 64 deaths). The red curve falls distinctly faster early on, the two cross shortly before the day 180 marker, and from there the red curve runs slightly above the blue one all the way to the right edge. Three vertical dotted lines stand at days 90, 180 and 365, each labelled at the top with the day and the difference in survival there — 16.7, -2.0 and -3.9 percentage points — with a filled dot where each dotted line meets each curve. A line of small text above the panel notes that the curves cross and that the log-rank p of 0.928 cannot show it. Below the panel is a number-at-risk table whose tick marks are those three milestone times plus day 0 and day 270: the blue arm falls from 69 patients to 4 by day 365, and the red arm from 68 to 6.
The two KM curves cross in mid-follow-up. Confidence bands are left off on purpose — they would cover the crossing point, which is the whole point of the figure. The number-at-risk ticks are aligned to the milestone times: a milestone survival rate is only as trustworthy as the number of people still being followed at that instant.Plotting script figures/scripts/B3-09-weighted-logrank.R

The three time points (the difference is Standard minus Test):

DayStandardTestDifference (pp)95% CI (pp)
9054.7% (37 at risk)38.0% (25 at risk)16.70.1 to 33.2
18021.2% (13 at risk)23.3% (14 at risk)-2.0-16.5 to 12.4
3657.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:

ArmSurvivalGreenwood (probability scale)log-log transform
Standard0.0710.005 to 0.1370.023 to 0.155
Test0.1100.030 to 0.1900.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 log[logS^]\log[-\log \hat{S}] 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 transform

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

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 wj=1w_j = 1 version; this one only replaces wjw_j.
  • 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

MisuseWhy it is wrong
The standard log-rank test failed, so try weights until one passesUncorrected multiple testing; pre-specify the weight or use max-combo
Reporting max-combo whose components were chosen afterwardsThe correction prices taking a maximum, not swapping the component list and recomputing
Treating a weighted log-rank p value as evidence of effect sizeIt has no effect measure, just like the standard test; use RMST or a milestone difference
Reporting a single HR when the curves crossThat HR is a weighted average of two opposite effects and has no clear interpretation
Choosing the milestone time point after the analysisSame selective reporting as choosing τ or the weight afterwards
Reporting a milestone rate without the number at risk thereThe reader cannot tell how many people the percentage rests on
Greenwood intervals when survival is near 0 or 1A symmetric interval on the probability scale runs past the boundary; use the log-log transform
Computing both intervals and reporting the narrower oneThe transform must be chosen in advance; choosing afterwards is choosing the result
Using Ŝ(t) rather than Ŝ(t⁻) as the weightThe 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 significantOnly 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.R

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

Sources and licences

This page is original writing

Report a content problem

The statistics on this site are written by AI and reviewed by AI; a human only spot-checks. What you can see may be what we cannot.

The more specific, the more fixable — e.g. which sentence disagrees with which textbook or paper.

Needed only if you want a reply; reports without it are still read.

Sent along with your report

These are attached automatically. You can drop any of them.