You know linear regression. You fit a line to data, and the slope tells you how much the outcome changes for each one-unit shift in the predictor. You can standardize those slopes to compare predictors directly. It is clean, linear, and closed-form. Survival analysis is not. The outcome here is time to an event, and that time is frequently censored. The patient is still alive at the last check-up, or the sensor is still running when the study ends. You cannot just regress time on predictors the way you regress a blood pressure reading, because the true event time for censored subjects is unknown.
Cox regression solves this by estimating the effect of predictors on the hazard, not on time itself. The hazard is the instantaneous rate of failure at time t, given survival up to that point. The Cox model assumes that the ratio of hazards between two groups is constant over time. That is the proportional hazards (PH) assumption. When it holds, the model gives you hazard ratios, which are the exponentiated coefficients. A hazard ratio of 0.5 means the risk of the event at any point in time is half as high in the treated group compared to the control group, assuming the baseline hazard is the same for both. For a deeper look at how to interpret these values in clinical or operational contexts, see our explanation of hazard ratios.
This is not a binary pass/fail test. Treating the PH assumption as a gate that either opens or closes the analysis is a mistake. It is a calibration question. How badly does the assumption fail, and does it matter for your decision? If the hazard ratio drifts from 0.8 to 1.2 over the follow-up period, a single point estimate is misleading. But if it hovers between 0.9 and 1.1, the violation is small, and the hazard ratio remains a useful summary of the average effect. You need to pair statistical tests with visual diagnostics to decide if the summary holds.
In Python, the lifelines package makes this concrete. You start by fitting the model. Then you check the proportionality of the hazard assumption using the ph_test method. This returns a p-value, but the p-value alone is not the answer. You need to see the shape of the violation.
from lifelines import CoxPHFitter
# Assume 'df' is a DataFrame with a 'T' column (time), 'E' (event), and 'x' (predictor)
cph = CoxPHFitter()
cph.fit(df, 'T', 'E', 'x')
# Check the PH assumption
ph_test_result = cph.ph_test()
print(ph_test_result.pvalues)
In R, the survival package provides a different workflow. You use survreg or coxph to fit the model. Then you use cox.zph to test proportionality. The function returns a data frame with p-values for each predictor.
library(survival)
library(survminer)
# Assume 'df' is a data frame with 'T', 'E', and 'x'
fit <- coxph(Surv(T, E) ~ x, data = df)
# Test the PH assumption
zph_result <- cox.zph(fit)
print(zph_result)
The cox.zph function also provides a plot option, which is where the real understanding happens. You plot the scaled Schoenfeld residuals against time. If the line is flat, the PH assumption holds. If it trends upward or downward, the effect of the predictor changes over time. A p-value from ph_test or cox.zph tells you that the assumption is violated. It does not tell you how to interpret the results. A trending residual plot does.
Analysts often make the mistake of running the test, seeing a significant p-value, and discarding the Cox model entirely. That is too blunt. If the violation is mild, you might stratify by a variable that causes the time-varying effect. If it is severe, you might switch to an accelerated failure time model or a parametric model that does not require the PH assumption. The goal is not to protect the Cox model. The goal is to pick the model that best represents the underlying risk process.
You are not choosing a model because it is "correct." You are choosing it because it gives you stable, interpretable estimates for the question you care about. A hazard ratio that is valid only in the first month of a two-year study is not a reliable summary. You must check where the violation lives in the time axis. Then you decide if the Cox estimate is still useful. That is the calibration step. It turns a statistical assumption into a practical decision about utility.
