AdvancedIndependently reviewed, not yet spot-checked by a human

The proportional hazards assumption and Schoenfeld residuals

What proportional hazards actually assumes, how to read the cox.zph table, what Schoenfeld residual plots and log-log plots each show you, and what the four repairs — stratification, a split time axis, a time-varying coefficient, RMST — each cost once the assumption fails.

What the assumption actually assumes

The Cox model says: a baseline hazard, multiplied by a number.

h(tx)=h0(t)exp(βx)h(t \mid x) = h_0(t) \cdot \exp(\beta x)

Notice that β\beta carries no tt. That means the multiplier is the same number for the whole of follow-up. In the lung model below, the hazard ratio for women is 0.602, and what the model is claiming is that on day 30, on day 300 and on day 900, a woman’s hazard of death is 0.602 times a man’s — every time. That claim is the proportional hazards assumption.

It is not a technical detail you check on the side. It is the precondition for that hazard ratio meaning anything at all. When it fails, coxph() still returns a number, but the number is an average of effects that differed in size — sometimes in direction — across periods, weighted by where the events happened to fall. It does not correspond to the true hazard ratio at any single moment, and its magnitude moves with the length of follow-up: the same treatment followed for three years and for ten years yields different hazard ratios even when nothing about the underlying reality has changed.

Clinical situations where the assumption predictably breaks:

SituationHow the hazard ratio moves over time
Surgery vs conservative managementEarly surgical hazard is high (perioperative complications), later hazard is lower, so the ratio starts above 1 and falls below it — the curves cross
Immunotherapy vs chemotherapyNothing separates for the first few months, then a gap opens — the ratio starts near 1 and shrinks
Prognostic factors such as performance status or ageThe effect is concentrated early; by the time follow-up is long, the frail patients have already died and the survivors differ less — the ratio drifts toward 1
Vaccine protectionWanes over time — the ratio drifts toward 1

How to check it: Schoenfeld residuals

The idea behind Schoenfeld residuals is direct. At every event time, the model uses everyone’s covariates to predict who is most likely to be the person having the event; the residual is the covariate value of the person who actually had the event, minus the model’s prediction at that moment.

The key property: if hazards really are proportional, these residuals should have no relationship with time — early and late residuals should both scatter around zero. A systematic trend over time means β\beta is in fact moving.

In practice you use scaled Schoenfeld residuals, which have a very convenient property: plot them against time and the smoothed curve is an estimate of β^(t)\hat{\beta}(t) — you are looking directly at where the coefficient goes. The formal test in cox.zph() asks whether the slope of that line is zero.

Run it yourself

library(survival)
data(cancer, package = "survival")
lung$sex_f <- factor(lung$sex, levels = c(1, 2),
                     labels = c("Male", "Female"))

fit <- coxph(Surv(time, status) ~ age + sex_f + ph.karno + wt.loss,
             data = lung)

zph <- cox.zph(fit)
zph                      # one row per variable, plus GLOBAL
par(mfrow = c(2, 2)); plot(zph)   # the smoothed line is beta(t)

# log(-log) plot: the graphical check for categorical variables,
# parallel lines mean the assumption is holding
plot(survfit(Surv(time, status) ~ sex_f, data = lung),
     fun = "cloglog", col = c("#4d6a8c", "#c44d4d"), lwd = 2)

# Repair 1: split the time axis, estimate one coefficient per period
sp <- survSplit(Surv(time, status) ~ ., data = lung, cut = 180,
                episode = "period")
coxph(Surv(tstart, time, status) ~ age + sex_f + wt.loss +
        ph.karno:strata(period), data = sp)

# Repair 2: make the coefficient a function of time, beta(t) = b0 + b1*log(t)
coxph(Surv(time, status) ~ age + sex_f + wt.loss + ph.karno + tt(ph.karno),
      data = lung, tt = function(x, t, ...) x * log(t))

# Repair 3: stratify -- each stratum gets its own baseline hazard
lung$kstrata <- cut(lung$ph.karno, c(-Inf, 70, 80, Inf))
coxph(Surv(time, status) ~ age + sex_f + wt.loss + strata(kstrata), data = lung)

# Repair 4 (replace the effect measure) has its code on B3-07, the page that
# states RMST in full

Verified with R 4.6.0 and survival 3.8.6

How to read the cox.zph() table

Here is the four-variable model on survival::lung. The dataset holds 228 patients; after complete-case analysis, 213 enter the model and 151 events are observed:

VariableHR95% CIp
Age, per year1.0160.996–1.0360.113
Female vs male0.6020.428–0.8480.004
Karnofsky score, per point0.9880.976–1.0000.048
Weight loss, per kg0.9970.985–1.0100.657

And what cox.zph() says about it:

Variableχ²dfp
Age, per year0.9310.335
Female vs male3.0610.080
Karnofsky score, per point7.3810.007
Weight loss, per kg0.0410.842
GLOBAL10.1640.038

The null hypothesis here is that proportional hazards holds, so a small p is the bad news. For the Karnofsky score p = 0.007 — a clear violation — and the GLOBAL test reaches significance too, at p = 0.038. Sex sits on the boundary at p = 0.080; age and weight loss give no sign of trouble.

Four panels of scaled Schoenfeld residuals plotted against time, one per covariate. In the Karnofsky score panel the smoothed line rises clearly from negative values toward zero; in the sex panel it rises gently from negative toward zero, a trend visible to the eye but not statistically significant; the age and weight-loss panels are roughly flat.
Scaled Schoenfeld residuals for the four covariates. The solid red line is the estimate of β(t), the dashed lines are its confidence band, and the blue dotted line is the single coefficient the model reported. In the Karnofsky panel the red line climbs from negative values toward zero — the coefficient is shrinking over time, and that is what a violation looks like.Plotting script figures/scripts/B3-04-ph-assumption.R

The log-log plot: the graphical version for categorical variables

The other classical check plots log(logS^(t))\log(-\log \hat S(t)) against logt\log t. The logic: if two groups do satisfy proportional hazards with hazard ratio θ\theta, then

log(logS1(t))=log(logS0(t))+logθ\log(-\log S_1(t)) = \log(-\log S_0(t)) + \log \theta

so the two curves are a constant vertical distance apart — parallel. Curves that converge, diverge or cross are telling you the assumption is in trouble.

Two log-log plots side by side. On the left, patients split into two groups by Karnofsky score: the two curves converge noticeably in the later part of follow-up. On the right, patients split by sex: the two curves stay roughly parallel.
Left: split by Karnofsky score, the two curves converge late in follow-up — the same phenomenon the Schoenfeld plot showed as a coefficient drifting toward zero, drawn a different way. Right: split by sex, roughly parallel. The value in each title's parentheses is the cox.zph p value for that variable.Plotting script figures/scripts/B3-04-ph-assumption.R

What to do when it is violated

Start with one question: is the violation in the variable you care about, or in some covariate?

If it is only a covariate — you want a treatment effect and it is age that misbehaves — stratify it away and the cost is small. If the violation is in the exposure or treatment variable itself, you can no longer report a single hazard ratio, because that number has no clear meaning. Four routes, each with a price.

Route 1: stratified Cox

Move the offending variable from covariate to stratum. Each stratum gets its own baseline hazard h0(t)h_0(t), and nothing forces strata to be proportional to one another.

Splitting Karnofsky into three strata takes the cox.zph() GLOBAL test from p = 0.038 to p = 0.441 — the assumption is recovered. The hazard ratio for sex becomes 0.572 (0.405–0.809).

The price: the stratifying variable no longer has a hazard ratio. You bought back the assumption and gave up the estimate. Stratification therefore suits variables you think of as “something I need to control for, whose effect size I do not care about”.

Route 2: split the time axis and estimate one hazard ratio per period

Use survSplit() to cut everyone’s follow-up in two at day 180, so the Karnofsky coefficient is estimated separately before and after:

PeriodHR per Karnofsky point95% CIp
Day 0-1800.9660.947–0.9870.001
Day 180+0.9980.983–1.0130.756

Now the story is legible: the protective effect of the Karnofsky score is concentrated in the first 180 days (HR 0.966 per point, interval clear of 1), and after day 180 no association is detected (HR 0.998, interval crossing 1). That carries far more information than the single hazard ratio of 0.988 the original model gave, which was in fact a weighted average of these two periods.

The price: you chose the cut point. Looking at the data first and then picking the cut that makes the p value attractive is data dredging. The cut point should come from a clinical rationale — 30 days after surgery, the end of a treatment cycle — and the paper should say how it was decided.

Route 3: make the coefficient a function of time

tt() lets you assume β(t)=β0+β1logt\beta(t) = \beta_0 + \beta_1 \log t outright and estimate β1\beta_1. Here β1\beta_1 = 0.011 (SE 0.006, p = 0.062) — positive, meaning the coefficient climbs over time, in the same direction the Schoenfeld plot showed.

The price: the logt\log t form is also an assumption you made. Use tt or t\sqrt t instead and you get different answers. And the output is now two coefficients, which clinical readers find hard to interpret.

Route 4: switch to an effect measure that does not need proportional hazards

The first three routes still repair the Cox model. The fourth replaces the effect measure itself. Restricted mean survival time (RMST) is the area under the Kaplan-Meier curve between 0 and τ\tau — “on average, how long did people live during the first τ\tau of follow-up”. It assumes no proportionality of any kind, its unit is days, and it does not ask the two groups to hold a fixed hazard ratio across the whole of follow-up.

RMST is no longer only a repair for a failed assumption. In oncology and heart-failure trials it is now a routine secondary analysis. The full account — where the area comes from, why τ\tau has to be fixed in advance, how much the conclusion moves when τ\tau changes, and how to say it as “lived about this many months longer” — is on restricted mean survival time (RMST).

How to read this in a paper

Everything relevant to this page is usually one sentence buried in the Statistical analysis section. Look for three things:

  1. Whether the assumption was checked at all. No mention, with a hazard ratio as the headline result, is a substantive methodological gap. The sentence usually reads “The proportional hazards assumption was assessed using scaled Schoenfeld residuals”.
  2. What was done about the result. “The assumption held” is fine; “the assumption was violated, so we used stratification / period-specific estimates / RMST” is fine; “the assumption was violated but we report a single hazard ratio anyway” means you should discount the number yourself.
  3. Whether the Kaplan-Meier curves cross. This is the check you can do without reading the Methods at all. Two curves that clearly cross mid-follow-up and stay apart afterwards are strong evidence against proportional hazards — if the true hazard ratio were fixed at any value other than 1, the true survival curves could not cross. Remember, though, that what you are looking at is an estimate: once only a handful of people remain at risk, two Kaplan-Meier curves can cross on sampling noise alone, and a crossing in that tail is not evidence of anything. Read the crossing against the numbers-at-risk table — where it happens and how wide it is — then go back to the Methods for the Schoenfeld residual test or a time interaction that confirms it. Once confirmed, the single hazard ratio is an average of two opposing effects, and the log-rank test loses power as well.

Common misuses

MisuseWhy it is wrong
Fitting a Cox model and never checking proportional hazardsWhen it fails, that hazard ratio corresponds to no real hazard ratio at any timepoint
Reading a non-significant cox.zph() p as “the assumption holds”The test has limited power, especially in small samples; judge the shape and size of the departure from the residual plot as well
Overhauling the model the moment a p value turns significant in a large databaseWith a big enough sample, clinically negligible departures are significant; look at how much β(t) actually moves
Reporting a single hazard ratio when the curves crossThe number is a weighted average of two effects pointing in opposite directions, with no clean interpretation
Drawing a log-log plot for a continuous variableIt has to be cut into groups first, you chose the cuts, and the picture follows them
Switching to the log-rank test because the assumption failedThe log-rank test is equally sensitive to the same problem, and less powerful when curves cross
Truncating follow-up until the assumption holdsLets the data set the scope of the analysis, and may discard exactly the period in which the effect appears
Choosing the time cut point after seeing the dataPicking cuts post hoc is data dredging; the cut should be fixed in advance on clinical grounds
Using RMST but deciding τ afterwardsτ must be pre-specified in the protocol, otherwise it amounts to selecting a favourable result — see RMST
Quoting a hazard ratio for a stratifying variableA stratified Cox model does not estimate the effect of the stratifying variable — that number does not exist

Reproducing every number on this page

/opt/homebrew/bin/Rscript figures/scripts/B3-04-ph-assumption.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.

Each row of cox.zph output is one covariate, and the last row is GLOBAL. To judge whether Karnofsky score violates proportional hazards, which p-value should you read?

Show the answer and why

Correct answer: 0.007 - Karnofsky's own row, which tests whether its effect varies with time

Each term is judged on its own row; Karnofsky's is 0.007. 0.080 belongs to female versus male. GLOBAL's 0.038 is the one most often misused: significance there says only that something in the model varies with time, without saying what, and non-significance guarantees nothing about individual terms, because pooling lets one strong violation be diluted by several clean ones. Reading the GLOBAL p-value as a covariate's p-value is the commonest misreading of this table.

Splitting follow-up at day 180 gives Karnofsky two different hazard ratios. So what does the single hazard ratio from the unsplit model represent?

Show the answer and why

Correct answer: 0.988 - it is a weighted average of the two periods and equals neither

When proportional hazards fails, the single hazard ratio does not become a wrong number so much as a number corresponding to no period at all: it averages the period-specific effects with weights set by the event counts, and those weights depend on how long this particular study followed people, so a longer study would report something different. 0.966 covers days 0 to 180 and 0.998 covers day 180 onward. The awkward part is that 0.988 is the figure that reaches the abstract and gets compared across papers.

Same data, same clinical question: replacing Karnofsky with ECOG makes the cox.zph GLOBAL test non-significant. What does that tell you?

Show the answer and why

Correct answer: GLOBAL becomes 0.314 - whether proportional hazards holds depends on how the model is specified, not on the data alone

All that changed was how performance status is coded; the clinical question is identical, and the test reverses: 0.314 against the original 0.038. So "does this violate PH" is really asking whether the model as written violates it. 0.201 is ECOG's own row in the new model, not the global test. This is why cox.zph should not be treated as a pass-or-fail gate - it turns with the specification, while the questions worth asking are how much the effect varies with time and whether that variation changes the conclusion.

Watch next

The Cox proportional hazards model explained
ENTileStats· 14 minTakes the word "proportional" apart one picture at a time. The best visual companion to this page's first section.
Survival Analysis Part 9 | Cox Proportional Hazards Model
ENMarinStatsLectures· 14 minDerives the model and shows where the assumption enters. Watch it to see that this is not an add-on check but part of the model's definition.
Cox Proportional Hazard Models
ENEpidemiology Stuff· 10 minAn epidemiologist's framing, focused on how conclusions bend when the assumption fails. Pairs with the last two sections here.
【Lecture】L20 Survival Analysis (2)
繁中MeDA(臺大公衛洪弘教授)· 51 minIn Mandarin, from National Taiwan University's public health programme — the only full lecture in the site's inventory that covers assumption diagnostics and residuals. Go here for the mathematics.

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.