Correlation, regression and survival · Choose a method
Log-rank test vs Cox regression: which to use for survival data
Use the log-rank test to compare whole survival curves between groups and Cox regression for a hazard ratio or covariates. Check proportional hazards and code humane endpoints as events.
On this page
Quick answer. When one group’s risk is expected to stay consistently higher, use the log-rank test to ask whether two groups’ survival differs over the whole study; to say how much it differs, or to adjust for a covariate, fit a Cox regression and report the hazard ratio with its 95% CI. For two groups and no covariates they test the same null hypothesis — the log-rank test is the Cox model’s score test (exactly so when no two events share a day; with tied days, R’s default score test, which uses Efron’s method, differs slightly) — so choose one primary test before unblinding and report the hazard ratio beside it. Both work best when one group’s risk stays consistently higher (proportional hazards); fit the Cox model and check this with
cox.zph(). If you expect the curves to cross, name a measure such as restricted mean survival time as primary in the protocol; if they cross unexpectedly, keep the planned test, describe the curves and add RMST only as a labelled secondary analysis. Code euthanasia at a humane endpoint as the event, not as censored.
Terms used below. Event: the outcome you are timing — here death or euthanasia at a humane endpoint. Censored: an animal whose event was not observed, because it was alive at the end of the study or was removed for a reason unrelated to the outcome. Kaplan–Meier curve: the estimated proportion still event-free over time. Hazard: the event rate among animals still at risk at a given moment. Hazard ratio (HR): the hazard in one group divided by the hazard in another (here treated divided by control), assumed constant over time; it is a ratio of rates, different from a ratio of risks or of survival times. Proportional hazards (PH): the assumption that the hazard ratio stays the same over the study. Restricted mean survival time (RMST): the average event-free time up to a chosen day, equal to the area under the Kaplan–Meier curve up to that day. Wald, likelihood-ratio (LR) and score tests: three ways of testing the same Cox coefficient; they agree in large samples and differ in small ones. Median survival: the day by which half the group has had the event. Covariate: another variable in the model, such as sex or baseline tumour size. Ties: two or more events on the same day; R’s
coxph()handles them with Efron’s method by default, statsmodels’PHRegwith Breslow’s. 95% confidence interval (CI): the range of values, such as hazard ratios, that the data are compatible with at the 95% level. Schoenfeld residuals: per-event differences between what happened and what the Cox model predicted, used to check proportional hazards. Numbers at risk: how many animals are still followed at each time, printed under the curve. Power: the chance that a test detects a real difference. Liberal: a test that gives p < 0.05 more often than 5% of the time when there is no difference; conservative: less often. Permutation test: a test that shuffles the group labels many times to see how often a difference as large as the observed one arises by chance.
Which question are you asking?
| Your question | Use | What you report |
|---|---|---|
| Does survival differ between two or more groups over the whole study? | Log-rank test (survdiff) |
Kaplan–Meier curves, median survival per group, log-rank p |
| How large is the difference? | Cox regression with group as the only term (coxph) |
Hazard ratio with 95% CI, provided cox.zph() and the curves show no clear crossing (otherwise see the crossing row) |
| Does it differ after adjusting for a covariate (e.g. baseline tumour size, sex)? | Cox regression with group and the covariate, only if the protocol named it; with fewer than about 20 events in total, add no covariate the protocol did not name | Adjusted hazard ratio with 95% CI |
| You expect the curves to cross (early harm, later benefit) | Name RMST to a fixed day, or survival at a fixed day, as primary in the protocol | RMST difference with 95% CI; the plotted curves |
| The curves cross or the PH check fails, and the protocol named log-rank or Cox | Keep the planned test; describe the curves; add RMST only as a labelled secondary analysis | The planned result, the curves, the secondary RMST |
| Several animals share a cage and the treatment is given per cage | The cage is the unit — see what counts as n | — |
The log-rank test, in Bland and Altman’s words, “is most likely to detect a difference between groups when the risk of an event is consistently greater for one group than another. It is unlikely to detect a difference when survival curves cross” (Bland & Altman 2004). It gives a p-value only, so pair it with a hazard ratio or medians.
What to do: decide in your protocol which test is primary (log-rank, or the Cox LR test) and which estimate you will report (hazard ratio, or RMST to a stated day if you expect crossing curves).
Code the events before you analyse
Kaplan–Meier curves and the log-rank test assume “that at any time patients who are censored have the same survival prospects as those who continue to be followed” (Bland & Altman 1998). A mouse euthanised because it reached a humane endpoint has worse prospects than the mice still in the study, so censoring it would break that assumption and make survival look better than it is. Count it as the event.
| What happened to the animal | event |
day |
|---|---|---|
| Found dead | 1 | day found |
| Euthanised at a humane endpoint defined in the protocol | 1 | day of euthanasia |
| Alive at the planned end of the study | 0 | last day of the study |
| Removed for a reason unrelated to the outcome (fight wound, technical failure) | 0 | day removed |
ARRIVE 2.0 item 16c asks you to “describe the humane endpoints established for the study, the signs that were monitored, and the frequency of monitoring. If the study did not set humane endpoints, state this” (Percie du Sert et al. 2020). State your event definition next to it.
What to do: write the coding rule in the protocol, fill the event column from it before unblinding, and compute day as the date of the event or censoring minus the start date for that animal (in R, as.numeric(as.Date(end_date) - as.Date(start_date))). Report how many animals in each group were censored and why.
Run it in R and Python
Constructed data, written by hand: 10 control and 10 treated mice followed for 60 days (survival.csv). Control: 8 events (days 19–46), 2 alive at day 60. Treated: 5 events (days 28–55), 4 alive at day 60, and one mouse removed on day 34 with a fight wound (censored). No two events fall on the same day. The file also has a reason column, which the code does not use; save the file in your working directory (getwd() in R shows where that is) before running the code.
# Install once: install.packages("survival")
library(survival) # survival 3.5-8
d <- read.csv("survival.csv") # uses columns group, day, event (1 = died or humane endpoint, 0 = censored)
d$group <- factor(d$group, levels = c("control", "treated"))
km <- survfit(Surv(day, event) ~ group, data = d)
print(km) # medians with 95% CIs
plot(km, col = 1:2, xlab = "Day", ylab = "Proportion event-free")
summary(km, times = c(0, 20, 40, 60)) # numbers at risk (n.risk) to add under the plot
survdiff(Surv(day, event) ~ group, data = d) # log-rank test
fit <- coxph(Surv(day, event) ~ group, data = d)
summary(fit) # HR, 95% CI, and Wald, likelihood-ratio and score tests
cox.zph(fit) # proportional-hazards check; also plot(cox.zph(fit))
# RMST to day 60 (only if your protocol named it): difference with a normal-approximation 95% CI
tab <- summary(km, rmean = 60)$table
dif <- tab["group=treated", "rmean"] - tab["group=control", "rmean"]
se <- sqrt(sum(tab[, "se(rmean)"]^2))
round(c(difference = dif, lower = dif - 1.96 * se, upper = dif + 1.96 * se), 1)
# Install once: pip install numpy pandas scipy statsmodels
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.duration.survfunc import SurvfuncRight, survdiff
from statsmodels.duration.hazard_regression import PHReg
d = pd.read_csv("survival.csv") # uses columns group, day, event (1 = died or humane endpoint, 0 = censored)
for g, sub in d.groupby("group"):
print(g, "median", SurvfuncRight(sub.day, sub.event).quantile(0.5))
chi2, p = survdiff(d.day, d.event, d.group)
print(f"log-rank chi2 = {chi2:.2f}, p = {p:.3f}")
x = (d.group == "treated").astype(float).values[:, None]
fit = PHReg(d.day, x, status=d.event, ties="efron").fit() # statsmodels' default is ties="breslow"
b, se = fit.params[0], fit.bse[0]
print(f"HR = {np.exp(b):.2f} (95% CI {np.exp(b - 1.96 * se):.2f} to {np.exp(b + 1.96 * se):.2f}), "
f"Wald p = {2 * stats.norm.sf(abs(b / se)):.3f}")
The proportional-hazards check and the RMST difference are R-only here: statsmodels 0.15.0 has neither a cox.zph() equivalent nor a built-in RMST function. In R, summary(fit) labels its three tests Likelihood ratio test, Wald test and Score (logrank) test, so you can tell which p-value you have.
| Result | R | Python |
|---|---|---|
| Median survival, control / treated | 35 / 55 days | 37 / 55 days |
| Log-rank test | χ²(1) = 2.99, p = 0.084 | χ²(1) = 2.99, p = 0.084 |
| Cox hazard ratio, treated vs control | 0.38 (95% CI 0.12 to 1.18) | 0.38 (0.12 to 1.18) |
| Cox Wald / LR / score p | 0.095 / 0.089 / 0.084 | 0.095 / 0.089 / — |
PH check (cox.zph) |
p = 0.24 (a weak test with 13 events; does not show that hazards are proportional) | — |
| RMST to day 60, treated − control (not the planned test) | 12.9 days (95% CI 1.8 to 24.0), p = 0.023 | 12.9 days (1.8 to 24.0), from a hand-written Kaplan–Meier area in our check code, not a statsmodels function |
Three things to read from this table:
- The log-rank p equals the Cox score p (0.084) because no two events share a day. R labels the score row “Score (logrank) test”. The Wald and LR p-values differ because they are different approximations to the same question.
- The medians differ by software. In the control group the curve sits at exactly 0.5 between day 33 and day 37; R’s
print(survfit)reports the midpoint (35), and statsmodels reports the end of that flat step (37). Say which software you used. - The RMST test gives p = 0.023 while the planned log-rank test gives p = 0.084. Switching to RMST after seeing this would be choosing the analysis by its result. With 10 per group the normal-approximation standard error behind p = 0.023 is too small, so the test is liberal (8.3% false positives in the simulation below); a permutation test of the same difference, which does not rely on that standard error, gives p = 0.044 (simulation_rmst_permutation_output.txt). If log-rank was the planned test, report p = 0.084, the hazard ratio and its wide interval, and say the data are compatible with anything from a large reduction to a modest increase in hazard.
What to do: report the planned test, the hazard ratio with its 95% CI and the medians, plot the Kaplan–Meier curves with numbers at risk, and run cox.zph().
Which p-value to trust with small groups
The three Cox tests and the log-rank test agree in large samples and drift apart in lab-sized groups. In our simulation (no true difference; exponential survival with median 35 days; follow-up ends at day 60; 20,000 runs per row; seed 20261110 fixed before running), the share of experiments with p < 0.05 was:
| Mice per group | Log-rank (= Cox score) | Cox Wald | Cox likelihood ratio |
|---|---|---|---|
| 6 | 6.1% | 3.7% | 5.6% |
| 8 | 5.7% | 4.2% | 5.4% |
| 10 | 5.7% | 4.6% | 5.3% |
| 20 | 5.3% | 4.7% | 5.0% |
In this simulation (equal groups, no true difference), the log-rank test ran above 5% at every group size (6.1% at 6 per group, 5.3% at 20), the Wald test below 5% (conservative), and the likelihood-ratio test closest to 5%. The simulation code is published as simulation.R with its output, simulation_output.txt.
What to do: with fewer than about 20 animals per group, name either the log-rank test (the usual choice, accepting a slightly liberal rate) or the likelihood-ratio test as primary in the protocol, and quote the hazard ratio’s interval rather than leaning on a borderline p.
Check proportional hazards — and what to do if it fails
cox.zph() tests whether the effect changes over time using scaled Schoenfeld residuals; its documentation states that “if the proportional hazards assumption holds then the true beta(t) function would be a horizontal line” (R survival 3.5-8). The documentation cites Grambsch and Therneau (1994) for the method. With 13 events the test is weak, so the worked example’s p = 0.24 does not show that hazards are proportional; it only fails to show that they are not. Look at plot(cox.zph(fit)) and at the Kaplan–Meier curves as well.
When curves cross, a single hazard ratio averages an early harm with a later benefit, and the log-rank test loses power. Royston and Parmar (2013) found that under non-proportional hazards “the logrank test … is underpowered compared with planned levels”, while a test of the RMST difference “maintains power close to its nominal 90 percent level under both non-PH and PH”. Our own small-group version (treated hazard three times higher before day 20 and a quarter as high after; 5,000 runs per row):
| Mice per group | Share with p < 0.05: log-rank | Share with p < 0.05: RMST to day 60 | False positives: RMST test, no true difference |
|---|---|---|---|
| 10 | 17.8% | 28.8% | 8.3% |
| 20 | 26.8% | 43.6% | 6.2% |
| 40 | 44.3% | 68.8% | 5.5% |
Part of the RMST test’s advantage at 10 per group comes from its liberal false-positive rate (8.3%); by 40 per group it holds 5.5% and still detects the crossing difference far more often. For small groups, a permutation test of the RMST difference (shuffle the group labels, recompute the difference, 999 times; 9,999 for the worked example) holds its rate: 4.8% with no true difference at 10 and at 20 per group, while detecting the crossing difference in 22% and 40% of experiments (2,000 runs per row, seed 20261112 fixed before running; simulation_rmst_permutation.R, simulation_rmst_permutation_output.txt). On the worked example it gives p = 0.044. Code: simulation.R and simulation_rmst_null.R (simulation_rmst_null_output.txt).
What to do: if your treatment could plausibly act early and fade, or harm early and help later, name RMST to a fixed day as the primary measure in the protocol; if a PH check fails unexpectedly, keep the planned analysis, show the curves and add RMST as a labelled secondary analysis, without changing the conclusion on the basis of that secondary test.
Check what an AI assistant ran
Generated survival code goes wrong in a few predictable places:
- Event coding. Is
event1 for death and humane endpoints and 0 for censored? Some code reverses it (1 = alive), which flips every result. - Euthanised animals. Were humane-endpoint animals censored? They should be events.
- Ties. R’s
coxph()uses Efron’s method by default; statsmodels’PHReguses Breslow’s unless you passties="efron". With tied event days the hazard ratio differs slightly. - Which p-value. Did the text quote the Wald p while the protocol named the log-rank test?
- Unit of analysis. Were animals in the same cage treated as independent when the treatment is given per cage?
What to do: ask the assistant to print the first rows of the data with the event column, the tie method and all three Cox tests, and check them against your protocol.
Report it
Survival was analysed with Kaplan–Meier estimates and compared between groups with the log-rank test (primary analysis, two-sided α = 0.05); death and euthanasia at the predefined humane endpoints [describe] counted as events, and animals alive at day 60 or removed for unrelated reasons were censored. The hazard ratio was estimated with Cox regression (Efron ties; R 4.3.3, survival 3.5-8), and proportional hazards were checked with scaled Schoenfeld residuals. Median survival was 35 days in control mice and 55 days in treated mice (log-rank χ²(1) = 2.99, p = 0.084; hazard ratio 0.38, 95% CI 0.12 to 1.18; n = 10 per group, 8 and 5 events).
Replace the bracket with your humane endpoints, and keep “primary analysis” only if your protocol named it; if it named the likelihood-ratio test, replace the log-rank χ² and p with that test’s p (0.089 here). Name your own software and version, and if you used Python, state its median convention (37 days here for control).
References
- Bland JM, Altman DG (1998). Survival probabilities (the Kaplan-Meier method). BMJ 317:1572. doi:10.1136/bmj.317.7172.1572
- Bland JM, Altman DG (2004). The logrank test. BMJ 328:1073. doi:10.1136/bmj.328.7447.1073
- Cox DR (1972). Regression models and life-tables. Journal of the Royal Statistical Society: Series B 34:187–202. doi:10.1111/j.2517-6161.1972.tb00899.x
- Grambsch PM, Therneau TM (1994). Proportional hazards tests and diagnostics based on weighted residuals. Biometrika 81:515–526. doi:10.1093/biomet/81.3.515
- Percie du Sert N, Ahluwalia A, Alam S, et al. (2020). Reporting animal research: Explanation and elaboration for the ARRIVE guidelines 2.0. PLOS Biology 18:e3000411. doi:10.1371/journal.pbio.3000411
- Royston P, Parmar MKB (2013). Restricted mean survival time: an alternative to the hazard ratio for the design and analysis of randomized trials with a time-to-event outcome. BMC Medical Research Methodology 13:152. doi:10.1186/1471-2288-13-152
- R survival package 3.5-8:
survdiff,coxph,cox.zphdocumentation. CRAN
How this page was made
- Written and published by Licklider. Recommendations cite peer-reviewed methods papers, the ARRIVE 2.0 guidelines and the R survival documentation listed under References; cited passages were checked against the source text, and where only a title or abstract was accessible the page states the claim at that level.
- The worked example uses constructed data written by hand. Its numbers and the simulations were produced by stored code (R 4.3.3 with survival 3.5-8; Python with SciPy 1.18.1 and statsmodels 0.15.0), and the two languages agree wherever both compute a value, except for the median-survival convention explained in the results.
- Separate review passes checked the statistics, the citations and readability before publication.