Cox Proportional Hazards

Michael BrenndoerferOctober 26, 202657 min read

Learn how the Cox proportional hazards model estimates covariate effects on hazards using partial likelihood, with NumPy implementation and diagnostics.

Machine Learning from ScratchAll chapters
Part 6 of 15Survival Analysis
Chapter 50Cox Proportional Hazards
Reading the full chapter · 10 sections · 57 min10 sections · 57 min

All 10 sections · 57 min read

Cox Proportional HazardsFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

Cox Proportional Hazards

Kaplan-Meier and Nelson-Aalen summarize survival and cumulative hazard for a population or stratum. They can describe a mixture of people with different hazards when their censoring assumptions hold; they do not themselves specify how predictors change an individual's hazard. For equipment with different maintenance histories, a regression model lets us ask how the hazard is associated with those histories while retaining censored observations in the appropriate risk sets.

The Cox proportional hazards model, introduced by David Cox in 1972, is the standard regression model for censored outcomes. Its defining feature is simple. Each individual's hazard is written as a shared, unspecified baseline hazard h0(t)h_0(t) multiplied by an individual-specific factor exp⁡(β⊤x)\exp(\beta^\top x) that depends only on that individual's covariates. The covariate factor scales the hazard up or down by the same amount at every point in time, which is precisely the proportional-hazards assumption. Because the baseline hazard is left entirely unspecified, Cox's model is semiparametric: it makes a strong parametric claim about how covariates act, and no parametric claim about the shape of time.

The method's second insight is what makes it practical. Cox showed that β\beta can be estimated by maximizing a partial likelihood that uses only the order in which events occur and who was still at risk when they did. The unknown baseline h0(t)h_0(t) cancels at every event time, so we never have to specify it. That cancellation is the core idea of the chapter. We will derive it from first principles, build the fitting machinery from scratch with NumPy, and reconstruct the baseline and survival curves afterwards. Along the way, you will see how each formula translates into code and how to check the result against a mature library.

A hazard ratio is not a survival-probability ratio, and it is not an effect size on the probability scale. Even when hazards are strictly proportional, the ratio of survival probabilities at time tt depends on tt and on the baseline hazard itself. Holding this distinction straight is essential for correctly communicating results.

Advantages

The Cox model leaves the baseline hazard shape unspecified. A parametric survival model instead chooses a family for that shape, which can add information if the choice is appropriate but can introduce misspecification. The Cox model trades that distributional choice for the proportional-hazards assumption and a model for the covariate effects. Its efficiency relative to a parametric fit depends on the data-generating process and censoring; there is no universal few-percent information-loss bound.

The hazard-ratio interpretation also suits many applied questions well. Clinicians want to know how much a treatment multiplies a patient's hazard at any moment; engineers want to know how much a maintenance schedule multiplies a failure rate; product managers want to know how much a pricing change multiplies a cancellation hazard. In each case, the Cox coefficient reports the log hazard ratio per unit of covariate, and its exponentiated form exp⁡(β)\exp(\beta) reports the multiplicative change in hazard.

Statistical inference for the Cox model is well established. The partial likelihood behaves asymptotically like an ordinary likelihood (Andersen and Gill, 1982), so model-based standard errors, Wald intervals, likelihood-ratio tests and score tests are available under their regularity and sampling assumptions. Clustering, misspecification and sparse events can require additional methods. Time-varying covariates, stratification, and penalization all extend naturally. These tools support a familiar regression workflow, while censoring-aware risk sets, proportional-hazards diagnostics, and survival-specific evaluation remain necessary.

Disadvantages

The most serious disadvantage is the proportional-hazards assumption itself. The entire model depends on the claim that exp⁡(β⊤x)\exp(\beta^\top x) multiplies the hazard by the same amount at every point in time. If that claim fails, for example, if a treatment is protective early and harmful later, a fitted coefficient can become a population- and follow-up-dependent summary, but their interpretation as a constant hazard ratio becomes misleading, and the survival predictions that follow from the model will be systematically off. We will discuss the standard diagnostics in detail later, because in practice it is the assumption that fails, not the mathematics.

Its relative-hazard interpretation must also be separated from absolute survival and from causal effects. An exponentiated Cox coefficient gives the multiplier of the instantaneous hazard for a one-unit covariate increase, holding the other covariates fixed. The hazard is a rate, not a probability. It says nothing directly about how much survival probability changes, because that depends on the cumulative baseline hazard and on how far into the future you are looking. Reporting "a hazard ratio of 0.5" as if it were "half as likely to die" is a common and consequential misinterpretation.

Finally, the model is not free of practical limitations. The simple model assumes noninformative censoring conditional on the included covariates. If dropout still depends on prognosis after conditioning on those covariates, the fitted coefficients can be biased. It assumes the log hazard is linear in the included covariates; nonlinear effects require their own functional-form checks. Our treatment of tied event times uses approximations. Efron accounts for fractional depletion of the tied-event weight in coarsened continuous times. The recording process and risk-set composition also matter when choosing a tie method. It also does not give you an absolute risk prediction unless you re-estimate the baseline hazard, which is an extra step you may be tempted to skip.

Formula

The Cox model makes two intertwined claims at once. First, covariates multiply the hazard through an exponential function of a linear predictor. Second, the effect of every covariate is the same at every time. Both claims appear in the same equation, so let us unpack them separately before we introduce any optimization machinery.

Proportional hazards and covariate effects

We start from the hazard function, which we introduced in the previous chapter. The hazard at time tt is the instantaneous rate of an event among survivors:

h(t∣x)=lim⁡Δt→0P(t≤T<t+Δt∣T≥t,x)Δt.h(t \mid x) = \lim_{\Delta t \to 0} \frac{P(t \le T < t + \Delta t \mid T \ge t, x)}{\Delta t}.

Here TT is the random event time, x∈Rpx \in \mathbb{R}^p is a covariate vector for the individual, and Δt\Delta t is a small time increment. The hazard tells us how many events per unit time occur among those still at risk; the survival function S(t)S(t), in contrast, tells us what fraction survive past tt. They are related by h(t)=−dlog⁡S(t)/dth(t) = -d \log S(t)/dt.

Why model the hazard rather than the survival probability directly? A hazard is a nonnegative rate, so a multiplicative model keeps every fitted hazard nonnegative automatically. It also has a natural "events per unit time among survivors" interpretation, which is often exactly the quantity a clinician, engineer, or product manager wants to reason about. Cox's claim is that this rate factors into a piece that depends only on tt and a piece that depends only on xx:

h(t∣x)=h0(t)exp⁡(β⊤x),β∈Rp.h(t \mid x) = h_0(t) \exp(\beta^\top x), \qquad \beta \in \mathbb{R}^p.

The function h0(t)≥0h_0(t) \ge 0 is the baseline hazard, defined as the hazard of an individual with x=0x = 0, so every new symbol here is accounted for. The exponential does two jobs at once: it maps any real linear predictor to a positive multiplier, and it turns additive changes in xx into multiplicative changes in the hazard. A one-unit increase in covariate xpx_p multiplies the hazard by eβpe^{\beta_p} at every time.

where:

  • tt: time since the start of follow-up, measured in the same units as TT
  • x∈Rpx \in \mathbb{R}^p: the covariate vector for the individual
  • β∈Rp\beta \in \mathbb{R}^p: the coefficient vector, with βp\beta_p the coefficient on covariate xpx_p
  • h0(t)h_0(t): the baseline hazard, the hazard of an individual whose covariates are all zero
  • exp⁡(β⊤x)\exp(\beta^\top x): the individual-specific multiplier that scales the baseline hazard up or down

The model becomes clear when we compare two individuals at the same instant. Taking the ratio of their hazards gives

h(t∣xi)h(t∣xj)=h0(t)exp⁡(β⊤xi)h0(t)exp⁡(β⊤xj)=exp⁡(β⊤(xi−xj)).\frac{h(t \mid x_i)}{h(t \mid x_j)} = \frac{h_0(t) \exp(\beta^\top x_i)}{h_0(t) \exp(\beta^\top x_j)} = \exp\bigl(\beta^\top (x_i - x_j)\bigr).

The baseline h0(t)h_0(t) cancels. That cancellation is the entire reason the Cox model is practical: the ratio of hazards depends only on β\beta and covariate differences, not on time. This is exactly the proportional hazards assumption, and the quantity exp⁡(β⊤(xi−xj))\exp(\beta^\top (x_i - x_j)) is called the hazard ratio.

where:

  • xix_i: the covariate vector for individual ii
  • xjx_j: the covariate vector for individual jj
  • β\beta: the coefficient vector shared by all individuals
  • h(t∣xi)/h(t∣xj)h(t \mid x_i)/h(t \mid x_j): the ratio of the two individuals' hazards at time tt

For fixed covariates and times with positive baseline hazard, for a single binary covariate with β=log⁡0.5\beta = \log 0.5, a unit change in the covariate halves the hazard at every point in time. The "proportional" in the name refers to this ratio: the two hazard curves may have any common shape h0(t)h_0(t), but their vertical ratio is fixed.

Let us be precise about what a hazard ratio is not. For the Cox model the survival function has a very specific form. In the previous chapter we saw that S(t)=exp⁡(−H(t))S(t) = \exp(-H(t)), where H(t)=∫0th(s) dsH(t) = \int_0^t h(s) \, ds is the cumulative hazard. Substituting the Cox hazard gives

S(t∣x)=exp⁡(−H0(t)exp⁡(β⊤x))=S0(t)exp⁡(β⊤x),S(t \mid x) = \exp\left(-H_0(t) \exp(\beta^\top x)\right) = S_0(t)^{\exp(\beta^\top x)},

where S0(t)=exp⁡(−H0(t))S_0(t) = \exp(-H_0(t)) is the baseline survival function and H0(t)=∫0th0(s) dsH_0(t) = \int_0^t h_0(s)\, ds is the baseline cumulative hazard.

where:

  • tt: time since the start of follow-up
  • xx: the covariate vector for the individual
  • h0(s)h_0(s): the baseline hazard at time ss
  • H0(t)=∫0th0(s) dsH_0(t) = \int_0^t h_0(s)\, ds: the baseline cumulative hazard, the total accumulated hazard up to time tt
  • S0(t)=exp⁡(−H0(t))S_0(t) = \exp(-H_0(t)): the baseline survival function, the survival probability when all covariates are zero
  • exp⁡(β⊤x)\exp(\beta^\top x): the individual-specific multiplier that scales the cumulative hazard
  • S(t∣x)S(t \mid x): the survival probability of an individual with covariates xx

The ratio of survival probabilities between two individuals is therefore

S(t∣x1)S(t∣x0)=exp⁡(−H0(t)(eβ⊤x1−eβ⊤x0)),\frac{S(t \mid x_1)}{S(t \mid x_0)} = \exp\Bigl(-H_0(t) \bigl(e^{\beta^\top x_1} - e^{\beta^\top x_0}\bigr)\Bigr),

which depends on tt through H0(t)H_0(t).

where:

  • x1,x0x_1, x_0: the covariate vectors of the two individuals being compared
  • H0(t)H_0(t): the baseline cumulative hazard up to time tt
  • β\beta: the coefficient vector shared by all individuals
  • S(t∣x1)/S(t∣x0)S(t \mid x_1)/S(t \mid x_0): the ratio of the two individuals' survival probabilities at time tt

A hazard ratio of 0.50.5 does not mean "survival is 50% higher" or "the event is twice as likely"; it means "the instantaneous event rate is half as large at every moment." The survival gap can change over time. If the cumulative baseline hazard grows without bound, both survival probabilities tend to zero and their difference eventually vanishes; finite follow-up or a finite limiting cumulative hazard need not show that full pattern. Use the survival curves to explain how a hazard ratio differs from a survival-probability comparison.

Risk sets and partial likelihood

The proportional hazards equation describes the model, but it still contains the unknown function h0(t)h_0(t). If we tried to maximize an ordinary likelihood, we would need to specify and estimate that function at every time. Cox's second insight avoids this entirely by asking a conditional question at each event time. The object that makes the question precise is the risk set. At any time tt, the risk set R(t)R(t) is the set of individuals who have not yet experienced an event and have not yet been censored. Equivalently,

R(t)={ j∈{1,…,n}:T~j≥t }.R(t) = \{\, j \in \{1, \dots, n\} : \tilde T_j \ge t \,\}.

Here T~j=min⁡(Tj,Cj)\tilde T_j=\min(T_j,C_j) is the observed duration, TjT_j is the latent event time and CjC_j is the censoring time. Everyone enters at time zero in this implementation. Individuals censored at an event time are included in the risk set immediately before that time, with events processed before censoring at the same recorded time.

where:

  • tt: a time point at which we evaluate who is still under observation
  • jj: an index running over the nn subjects in the study
  • T~j\tilde T_j: the observed event or censoring duration of subject jj
  • R(t)R(t): the risk set, the set of subjects still at risk (not yet failed and not yet censored) at time tt

A censored subject remains in every risk set up to their censoring time, even though they never contribute a numerator term. This is why survival data cannot be reduced to ordinary classification: dropping censored rows would discard the very information that tells us who was still at risk when later events occurred.

Suppose the observed event times are t(1)<t(2)<⋯<t(K)t_{(1)} < t_{(2)} < \cdots < t_{(K)}, with KK the number of distinct event times (for now we assume no ties). Let iki_k be the individual who failed at t(k)t_{(k)}. Cox's idea is to ask a conditional question at each event time: given that some individual in the risk set failed at t(k)t_{(k)}, what is the probability that it was individual iki_k in particular? The probability is

P(ik∣t(k))=h(t(k)∣xik)h(t(k)∣xik)+∑j∈R(t(k))∖{ik}h(t(k)∣xj).P(i_k \mid t_{(k)}) = \frac{h(t_{(k)} \mid x_{i_k})}{h(t_{(k)} \mid x_{i_k}) + \sum_{j \in R(t_{(k)}) \setminus \{i_k\}} h(t_{(k)} \mid x_j)}.

where:

  • t(k)t_{(k)}: the kk-th distinct event time
  • iki_k: the individual who fails at t(k)t_{(k)}
  • xikx_{i_k}: the covariate vector of the failing individual
  • R(t(k))R(t_{(k)}): the risk set at event time t(k)t_{(k)}
  • h(t(k)∣xj)h(t_{(k)} \mid x_j): the hazard of individual jj at time t(k)t_{(k)}
  • P(ik∣t(k))P(i_k \mid t_{(k)}): the conditional probability that the failing individual was iki_k, given that exactly one individual in the risk set failed at t(k)t_{(k)}

Pulling out the factor h0(t(k))h_0(t_{(k)}), which appears in every term in the numerator and denominator, gives

P(ik∣t(k))=h0(t(k))eβ⊤xikh0(t(k))∑j∈R(t(k))eβ⊤xj=eβ⊤xik∑j∈R(t(k))eβ⊤xj.P(i_k \mid t_{(k)}) = \frac{h_0(t_{(k)}) e^{\beta^\top x_{i_k}}}{h_0(t_{(k)}) \sum_{j \in R(t_{(k)})} e^{\beta^\top x_j}} = \frac{e^{\beta^\top x_{i_k}}}{\sum_{j \in R(t_{(k)})} e^{\beta^\top x_j}}.

The baseline hazard vanished. This is the partial likelihood contribution at time t(k)t_{(k)}. It is a "partial" likelihood because it models only the identity of the failure within the risk set, not the absolute time at which the failure occurred. Multiplying contributions across event times gives the partial likelihood

L(β)=∏k=1Kexp⁡(β⊤xik)∑j∈R(t(k))exp⁡(β⊤xj).L(\beta) = \prod_{k=1}^{K} \frac{\exp(\beta^\top x_{i_k})}{\sum_{j \in R(t_{(k)})} \exp(\beta^\top x_j)}.

where:

  • KK: the number of distinct event times
  • t(k)t_{(k)}: the kk-th distinct event time, with k=1,…,Kk = 1, \dots, K
  • iki_k: the individual who fails at t(k)t_{(k)}
  • xikx_{i_k}: the covariate vector of the failing individual at t(k)t_{(k)}
  • R(t(k))R(t_{(k)}): the risk set at event time t(k)t_{(k)}
  • β\beta: the coefficient vector to be estimated
  • L(β)L(\beta): the partial likelihood, a function of β\beta after the baseline hazard has cancelled

Taking logs turns the product into a sum and the fractions into differences:

ℓ(β)=log⁡L(β)=∑k=1K[β⊤xik−log⁡∑j∈R(t(k))exp⁡(β⊤xj)].\ell(\beta) = \log L(\beta) = \sum_{k=1}^{K} \left[ \beta^\top x_{i_k} - \log \sum_{j \in R(t_{(k)})} \exp(\beta^\top x_j) \right].

Each event contributes a numerator term (the log weight of the individual that failed) minus a denominator term (the log of the total weight of everyone who was at risk). Maximizing ℓ\ell therefore means finding the β\beta that makes the observed failures look as likely as possible given who was at risk when they happened. Increasing βp\beta_p, holding the other coefficients fixed, raises the relative weight of higher-xpx_p individuals. An event’s derivative with respect to this coefficient is positive when the failing individual exceeds the current weighted risk-set mean. Information also depends on covariate spread and weights: a one-subject risk set contributes none, but the number of alternatives alone does not determine the strength of evidence.

The partial likelihood is not the joint density of the observed data: it models the identities of failures conditional on the risk sets, rather than the full distribution of observed event and censoring times. Cox (1975) outlined why partial likelihood supports likelihood-like large-sample inference. Under suitable regularity and sampling conditions, the Cox partial-likelihood estimator is consistent and asymptotically normal (Andersen and Gill, 1982), and inverse observed information provides an approximate model-based covariance. To derive the score equations, treat ℓ\ell exactly as you would treat a log-likelihood.

In matrix form, if we let di∈{0,1}d_i \in \{0,1\} be the event indicator for individual ii and Yj(t)=1{T~j≥t}Y_j(t) = \mathbb{1}\{\tilde T_j \ge t\} the at-risk indicator, the log partial likelihood can be written

ℓ(β)=∑i=1ndi[β⊤xi−log⁡∑j=1nYj(T~i)exp⁡(β⊤xj)].\ell(\beta) = \sum_{i=1}^{n} d_i \left[ \beta^\top x_i - \log \sum_{j=1}^{n} Y_j(\tilde T_i) \exp(\beta^\top x_j) \right].

where:

  • nn: the number of individuals in the study
  • di∈{0,1}d_i \in \{0,1\}: the event indicator for individual ii (1 if the event was observed, 0 if censored)
  • Yj(t)=1{T~j≥t}Y_j(t) = \mathbb{1}\{\tilde T_j \ge t\}: the at-risk indicator for individual jj, equal to 1 if jj is still at risk at time tt
  • T~j\tilde T_j: the observed event or censoring time of individual jj
  • xi,xjx_i, x_j: the covariate vectors of individuals ii and jj
  • β\beta: the coefficient vector to be estimated

This form makes it clear that only individuals who have di=1d_i = 1 contribute numerator terms, while censored individuals still appear in the denominator of every risk set that includes them. The risk-set denominator is the mechanism that lets censored observations speak: they do not need an event to influence the fit, because their presence tells us which covariate profiles were available to fail.

Coefficient fitting, ties, and baseline estimation

To maximize ℓ(β)\ell(\beta) we take derivatives. For the pp-th component of β\beta, differentiate the numerator β⊤xi\beta^\top x_i to get xi,px_{i,p}, and for the log-sum-exp use the chain rule: the derivative of log⁡∑jYjeβ⊤xj\log \sum_j Y_j e^{\beta^\top x_j} is the weighted average of xj,px_{j,p} using the exponential weights. The result is the score function

∂ℓ∂βp=∑k=1K[xik,p−∑j∈R(t(k))xj,pexp⁡(β⊤xj)∑j∈R(t(k))exp⁡(β⊤xj)].\frac{\partial \ell}{\partial \beta_p} = \sum_{k=1}^{K} \left[ x_{i_k,p} - \frac{\sum_{j \in R(t_{(k)})} x_{j,p} \exp(\beta^\top x_j)}{\sum_{j \in R(t_{(k)})} \exp(\beta^\top x_j)} \right].

Each event's contribution is exactly "the observed covariate of the failing individual minus the risk-set average of that covariate." Setting the score to zero is what fitting means in the Cox model, and it is a clean "observed minus expected" condition that mirrors ordinary MLE. An event contributes positively when the failing individual has a larger covariate value than the weighted risk-set mean. A positive total score component means that an infinitesimal increase in that coefficient, holding the others fixed, raises the log partial likelihood. A multivariable Newton update couples coordinates through the Hessian, so its individual coefficient changes need not follow the signs of the score components.

The Hessian is obtained by differentiating once more. Writing xˉ(t(k))=∑j∈R(t(k))wjxj/∑jwj\bar x(t_{(k)}) = \sum_{j \in R(t_{(k)})} w_j x_j / \sum_j w_j for the risk-set weighted mean with weights wj=exp⁡(β⊤xj)w_j = \exp(\beta^\top x_j), we get

∂2ℓ∂βp∂βq=−∑k=1K[(xpxq)‾(t(k))−xˉp(t(k)) xˉq(t(k))].\frac{\partial^2 \ell}{\partial \beta_p \partial \beta_q} = -\sum_{k=1}^{K} \left[ \overline{(x_p x_q)}(t_{(k)}) - \bar x_p(t_{(k)}) \, \bar x_q(t_{(k)}) \right].

where:

  • ℓ(β)\ell(\beta): the log partial likelihood
  • βp,βq\beta_p, \beta_q: the pp-th and qq-th components of the coefficient vector
  • kk: an index running over the KK distinct event times
  • t(k)t_{(k)}: the kk-th distinct event time
  • R(t(k))R(t_{(k)}): the risk set at event time t(k)t_{(k)}
  • wj=exp⁡(β⊤xj)w_j = \exp(\beta^\top x_j): the weight of individual jj in the risk set
  • xˉp(t(k))=∑j∈R(t(k))wjxj,p/∑jwj\bar x_p(t_{(k)}) = \sum_{j \in R(t_{(k)})} w_j x_{j,p} / \sum_j w_j: the risk-set weighted mean of covariate pp
  • (xpxq)‾(t(k))\overline{(x_p x_q)}(t_{(k)}): the risk-set weighted mean of the product xj,pxj,qx_{j,p} x_{j,q}

The bracket is a weighted covariance matrix and is therefore positive semidefinite, which makes the Hessian negative semidefinite. That is, ℓ(β)\ell(\beta) is concave. Concavity rules out a stationary point with a lower objective than an attainable global maximum. Strict concavity can give uniqueness when a finite maximum exists, but full design rank alone does not establish finite estimability. Separation or lack of risk-set variation can prevent a finite unique estimate, and unmodified Newton steps have no arbitrary-start convergence guarantee. Unlike ordinary least squares, this fit generally requires iterative optimization. Concavity does not establish an OLS-style closed-form solution, equal computational cost, or convergence of arbitrary Newton steps.

Real data often produce ties, in which several individuals record an event at the same time. With dkd_k events at time tkt_k and event set DkD_k, the untied expression no longer specifies an ordering within the tied group. Exact tie treatments depend on whether times are modeled as discrete or as coarsened continuous observations and can be expensive. Two approximations are common. The Breslow approximation treats each tied event as if the others were still at risk:

LkBreslow(β)=∏i∈Dkexp⁡(β⊤xi)(∑j∈R(tk)exp⁡(β⊤xj))dk.L_k^{\text{Breslow}}(\beta) = \frac{\prod_{i \in D_k} \exp(\beta^\top x_i)} {\left(\sum_{j \in R(t_k)} \exp(\beta^\top x_j)\right)^{d_k}}.

where:

  • kk: an index over the distinct event times tkt_k that carry ties
  • tkt_k: a distinct event time at which dkd_k events are recorded
  • DkD_k: the set of individuals who fail at time tkt_k, with ∣Dk∣=dk|D_k| = d_k
  • dkd_k: the number of tied events at time tkt_k
  • R(tk)R(t_k): the risk set at time tkt_k
  • xi,xjx_i, x_j: the covariate vectors of individuals ii and jj
  • β\beta: the coefficient vector

The Efron approximation accounts for depletion of the tied event set because it gradually removes events from the denominator as we imagine the tie broken:

LkEfron(β)=∏i∈Dkexp⁡(β⊤xi)∏ℓ=0dk−1[∑j∈R(tk)exp⁡(β⊤xj)−ℓdk∑j∈Dkexp⁡(β⊤xj)].L_k^{\text{Efron}}(\beta) = \frac{\prod_{i \in D_k} \exp(\beta^\top x_i)} {\prod_{\ell=0}^{d_k - 1} \left[ \sum_{j \in R(t_k)} \exp(\beta^\top x_j) - \frac{\ell}{d_k} \sum_{j \in D_k} \exp(\beta^\top x_j) \right]}.

where:

  • kk: an index over the distinct event times tkt_k that carry ties
  • tkt_k: a distinct event time at which dkd_k events are recorded
  • DkD_k: the set of individuals who fail at time tkt_k, with ∣Dk∣=dk|D_k| = d_k
  • dkd_k: the number of tied events at time tkt_k
  • R(tk)R(t_k): the risk set at time tkt_k
  • ℓ\ell: an index running from 00 to dk−1d_k - 1 that progressively removes tied failures from the denominator
  • xi,xjx_i, x_j: the covariate vectors of individuals ii and jj
  • β\beta: the coefficient vector

With one event at a time the two formulas coincide. With multiple tied events they can differ, and the difference depends on the event weights as well as the tie count. Efron is often used for coarsened continuous times; genuinely discrete-time data can call for a different model. We specify the tie method explicitly rather than relying on a software default.

The final ingredient is the baseline hazard. Cox's partial likelihood deliberately hides h0(t)h_0(t), but to turn coefficients into survival curves we need to recover it. The standard estimator is Breslow's:

H^0(t)=∑tk≤tdk∑j∈R(tk)exp⁡(β^⊤xj).\hat H_0(t) = \sum_{t_k \le t} \frac{d_k}{\sum_{j \in R(t_k)} \exp(\hat\beta^\top x_j)}.

Each event time contributes the number of events dkd_k divided by the total at-risk weight at that time. Summing these increments accumulates an estimate of the cumulative baseline hazard. Given H^0(t)\hat H_0(t), individual survival follows from

S^(t∣x)=exp⁡(−H^0(t)exp⁡(β^⊤x)).\hat S(t \mid x) = \exp\Bigl(-\hat H_0(t) \exp(\hat\beta^\top x)\Bigr).

where:

  • tt: time since the start of follow-up
  • xx: the covariate vector for the individual
  • H^0(t)\hat H_0(t): the Breslow estimate of the baseline cumulative hazard up to time tt
  • β^\hat\beta: the fitted coefficient vector
  • S^(t∣x)\hat S(t \mid x): the estimated survival probability for an individual with covariates xx

Because H^0\hat H_0 is a step function, S^\hat S is a step function too; between event times, survival is treated as constant.

Mathematical Properties

Three properties guide the fit. First, the log partial likelihood is concave, but a finite identifiable optimum and adequate risk-set covariate variation still matter; full column rank alone does not guarantee a finite estimate or global Newton convergence. Second, the time-invariant hazard multiplier applies to fixed covariates under proportional hazards. Third, inverse observed information gives an approximate model-based covariance under regularity conditions, rather than an exact finite-sample uncertainty guarantee.

There is also an important invariance we will use implicitly later. Because the partial likelihood depends on covariate values only through differences xi−xjx_i - x_j within each risk set, adding a constant shift to any covariate in XX has no effect on β^\hat\beta, only on the baseline, which absorbs the shift. A constant intercept is not separately identified from the Cox baseline hazard. This differs from ordinary logistic regression, where an intercept usually is identifiable.

Example

We work through a minimal dataset of four subjects so every number can be checked by hand. Each subject has a single covariate x∈{0,1}x \in \{0, 1\}, an observed time, and an event indicator.

Four-subject teaching dataset with a binary covariate, observed time, and event indicator.
SubjectxxTTdd
1021
2141
3061
4180

There are three events, at times 22, 44, and 66. Subject 4 is censored at time 8, so they are at risk at every event time but never contribute a numerator term. The three risk sets are:

  • at t=2t=2: {1,2,3,4}\{1, 2, 3, 4\} (everyone is still at risk)
  • at t=4t=4: {2,3,4}\{2, 3, 4\} (subject 1 has already failed)
  • at t=6t=6: {3,4}\{3, 4\} (subject 2 has already failed)

The partial likelihood is

L(β)=e0e0+eβ+e0+eβ⏟t=2⋅eβeβ+e0+eβ⏟t=4⋅e0e0+eβ⏟t=6.L(\beta) = \underbrace{\frac{e^{0}}{e^{0} + e^{\beta} + e^{0} + e^{\beta}}}_{t=2} \cdot \underbrace{\frac{e^{\beta}}{e^{\beta} + e^{0} + e^{\beta}}}_{t=4} \cdot \underbrace{\frac{e^{0}}{e^{0} + e^{\beta}}}_{t=6}.

Writing q=eβq = e^{\beta} and simplifying,

L(β)=12+2q⋅q1+2q⋅11+q.L(\beta) = \frac{1}{2 + 2q} \cdot \frac{q}{1 + 2q} \cdot \frac{1}{1 + q}.

Taking logs,

ℓ(β)=−log⁡(2+2q)+β−log⁡(1+2q)−log⁡(1+q).\ell(\beta) = -\log(2 + 2q) + \beta - \log(1 + 2q) - \log(1 + q).

Let us evaluate this at three values of β\beta to see the shape of the surface. At β=0\beta = 0 (q=1q=1):

ℓ(0)=−log⁡4+0−log⁡3−log⁡2≈−1.386−1.099−0.693=−3.178.\ell(0) = -\log 4 + 0 - \log 3 - \log 2 \approx -1.386 - 1.099 - 0.693 = -3.178.

At β=−1\beta = -1 (q≈0.368q \approx 0.368):

ℓ(−1)=−log⁡(2.736)−1−log⁡(1.736)−log⁡(1.368)≈−1.006−1−0.552−0.313=−2.871.\ell(-1) = -\log(2.736) - 1 - \log(1.736) - \log(1.368) \approx -1.006 - 1 - 0.552 - 0.313 = -2.871.

At β=−1.5\beta = -1.5 (q≈0.223q \approx 0.223):

ℓ(−1.5)=−log⁡(2.446)−1.5−log⁡(1.446)−log⁡(1.223)≈−2.965.\ell(-1.5) = -\log(2.446) - 1.5 - \log(1.446) - \log(1.223) \approx -2.965.

The displayed evaluations bracket the maximum: ℓ(−1.5)≈−2.965\ell(-1.5) \approx -2.965, ℓ(−1)≈−2.871\ell(-1) \approx -2.871, and ℓ(0)≈−3.178\ell(0) \approx -3.178, so the largest value is at β=−1\beta=-1 and the optimum lies between −1.5-1.5 and 00. The score can be simplified to 1−2q/(1+q)−2q/(1+2q)1-2q/(1+q)-2q/(1+2q). At β=−1\beta=-1 it is positive, so increasing β\beta locally increases the log partial likelihood. Multiplying the zero-score equation 1−2q/(1+q)−2q/(1+2q)=01-2q/(1+q)-2q/(1+2q)=0 by its positive denominators yields 1−q−4q2=01-q-4q^2=0, whose positive root q=(17−1)/8q=(\sqrt{17}-1)/8 determines the finite optimum in this example. We compute it and an approximate Wald interval below; with only three events, the interval is a teaching calculation rather than a precise effect estimate.

PythonIn [3]
CollapseExpand
hand_q = (np.sqrt(17.0) - 1.0) / 8.0
hand_score = 1 - 2 * hand_q / (1 + hand_q) - 2 * hand_q / (1 + 2 * hand_q)
hand_beta = np.log(hand_q)
hand_information = (
    2 * hand_q / (1 + hand_q) ** 2 + 2 * hand_q / (1 + 2 * hand_q) ** 2
)
hand_se = 1 / np.sqrt(hand_information)
hand_hr_interval = np.exp(hand_beta + np.array([-1.96, 1.96]) * hand_se)
ConsoleOut [4]
CollapseExpand
hand optimum beta: -0.940614; hazard ratio: 0.390388
score at the hand optimum: 1.11e-16
approximate model-based 95% Wald hazard-ratio interval: [0.0343374  4.43839458]

The hand calculation also has a visual counterpart. Over a fixed grid of beta values, the log partial likelihood is a smooth concave curve with a single finite maximum at the value derived above.

PythonIn [5]
CollapseExpand
beta_grid = np.linspace(-2.5, 1.0, 400)
hand_q_grid = np.exp(beta_grid)
hand_loglike_grid = (
    -np.log(2.0 + 2.0 * hand_q_grid)
    + beta_grid
    - np.log(1.0 + 2.0 * hand_q_grid)
    - np.log(1.0 + hand_q_grid)
)
hand_loglike_opt = (
    -np.log(2.0 + 2.0 * hand_q)
    + hand_beta
    - np.log(1.0 + 2.0 * hand_q)
    - np.log(1.0 + hand_q)
)
VisualizationOut [6]
CollapseExpand
A concave log partial likelihood curve with a marked maximum near beta equals minus one.
Log partial likelihood for the four-subject hand example over a fixed beta grid. The curve is concave, with the marked point at the finite maximum derived from the score equation. This illustrates the optimization problem behind Cox fitting, not a general guarantee of finite estimability in every dataset.

The lesson from the hand calculation is not the specific coefficient. It is that the optimization operates on observed covariate values within risk sets. At an event where the failing individual has x=1x=1, increasing β\beta raises its numerator weight; when the failing individual has x=0x=0, that numerator stays at one. In either case, increasing β\beta raises the denominator weights of the subjects with x=1x=1 who remain at risk. A positive β\beta helps only when the failing individual has high covariate values relative to those still at risk. Fitting the Cox model means finding the β\beta that balances those two forces across all events.

Implementation in NumPy and SciPy

We now build the partial likelihood from scratch. We use a small equipment-failure dataset with a single binary covariate: maintained equals 1 if the equipment received regular maintenance and 0 otherwise. Our goal is to estimate the effect of maintenance on the failure hazard and then to recover the implied survival curves. The dataset is deliberately small so that the connection between the formulas and the code stays visible.

The hidden setup imports the numerical libraries and shared figure helpers. The table below is a small constructed teaching dataset, with follow-up measured in arbitrary time units; it is not a collected maintenance study.

PythonIn [7]
CollapseExpand
equipment = pd.DataFrame(
    {
        "time": [5, 8, 12, 15, 20, 24, 30, 2, 3, 6, 9, 10, 14, 18, 22],
        "event": [1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1],
        "maintained": [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
    }
)
X = equipment[["maintained"]].to_numpy(dtype=float)
time = equipment["time"].to_numpy(dtype=float)
event = equipment["event"].to_numpy(dtype=int)

The first seven rows are maintained (x=1x=1); the last eight are not (x=0x=0). Unmaintained devices fail earlier on average, which suggests the fitted coefficient should be negative (maintenance reduces the hazard).

Out[8]:
time event maintained
0 5 1 1
1 8 1 1
2 12 0 1
3 15 1 1
4 20 1 1
5 24 1 1
6 30 1 1
7 2 1 0
8 3 1 0
9 6 1 0
10 9 1 0
11 10 0 0
12 14 1 0
13 18 1 0
14 22 1 0

Seven maintained rows precede eight unmaintained rows, and the unmaintained records are visibly concentrated at earlier times, so a negative fitted maintenance coefficient is the expected direction. We next write the log partial likelihood in a numerically stable form. The naive computation log⁡∑jexp⁡(ηj)\log \sum_j \exp(\eta_j) overflows when the linear predictor ηj=β⊤xj\eta_j = \beta^\top x_j is large. We use the log-sum-exp identity

log⁡∑jeηj=m+log⁡∑jeηj−m,m=max⁡jηj,\log \sum_j e^{\eta_j} = m + \log \sum_j e^{\eta_j - m}, \qquad m = \max_j \eta_j,

where:

  • ηj\eta_j: the linear predictor β⊤xj\beta^\top x_j for individual jj
  • mm: the largest linear predictor in the current risk set, used as a shift to keep the exponentials bounded

which shifts the exponential arguments to be nonpositive and keeps the sum bounded. In the code, the loop over np.flatnonzero(event) visits only event times; the risk set for each event is selected by time >= time[i].

PythonIn [9]
CollapseExpand
def cox_loglike_untied(beta, X, time, event):
    """Untied-event Cox partial log-likelihood using log-sum-exp."""
    eta = X @ beta
    total = 0.0
    for i in np.flatnonzero(event):
        at_risk = time >= time[i]
        eta_r = eta[at_risk]
        m = eta_r.max()
        log_denominator = m + np.log(np.exp(eta_r - m).sum())
        total += eta[i] - log_denominator
    return total
PythonIn [10]
CollapseExpand
null_loglike = cox_loglike_untied(np.zeros(X.shape[1]), X, time, event)
ConsoleOut [11]
CollapseExpand
Log partial likelihood at beta = 0: -23.623

At β=0\beta=0, every subject in a risk set has equal relative weight, so this log partial likelihood depends on the risk-set sizes and event pattern. Its raw magnitude is not a measure of covariate discrimination. Comparing a fitted value with the null objective is more informative about the covariate model.

The gradient follows directly from the score function derived in the previous section. The code computes the risk-set weighted average of the covariates and subtracts it from the failing individual's covariate. This is the same "observed minus expected" structure we saw in the Formula section, now expressed as a vector operation over the rows in the risk set.

PythonIn [12]
CollapseExpand
def cox_gradient(beta, X, time, event):
    """Score function: observed covariate minus risk-set weighted average."""
    eta = X @ beta
    p = X.shape[1]
    grad = np.zeros(p)
    for i in np.flatnonzero(event):
        at_risk = time >= time[i]
        eta_r = eta[at_risk]
        w = np.exp(eta_r - eta_r.max())
        w = w / w.sum()
        grad += X[i] - w @ X[at_risk]
    return grad

The Hessian is the negative sum of positive-semidefinite weighted covariance matrices, so the log partial likelihood is concave. We will need it later to compute standard errors. Notice that the Hessian does not depend on the event indicator except through which risk sets are visited; censored observations still enter through their weights in each risk set.

PythonIn [13]
CollapseExpand
def cox_hessian(beta, X, time, event):
    """Negative-semidefinite Hessian for the untied partial log-likelihood."""
    eta = X @ beta
    p = X.shape[1]
    H = np.zeros((p, p))
    for i in np.flatnonzero(event):
        at_risk = time >= time[i]
        eta_r = eta[at_risk]
        w = np.exp(eta_r - eta_r.max())
        w = w / w.sum()
        Xr = X[at_risk]
        mu = w @ Xr
        H -= (Xr.T * w) @ Xr - np.outer(mu, mu)
    return H

With the three functions in hand, we optimize. We minimize the negative log partial likelihood with BFGS, using its analytic gradient, and inspect the returned status and score. Concavity alone does not guarantee a finite optimum or convergence from an arbitrary starting value. We compute standard errors from the inverse information matrix,

Var⁡^(β^)=(−∂2ℓ∂β∂β⊤∣β=β^)−1,\widehat{\operatorname{Var}}(\hat\beta) = \left(-\left.\frac{\partial^2 \ell}{\partial \beta \partial \beta^\top}\right|_{\beta=\hat\beta}\right)^{-1},

where:

  • ℓ(β)\ell(\beta): the Cox partial log-likelihood
  • ∂2ℓ∂β∂β⊤\dfrac{\partial^2 \ell}{\partial \beta \partial \beta^\top}: the Hessian matrix of ℓ\ell with respect to β\beta
  • β^\hat\beta: the fitted coefficient vector

We solve against the observed information to obtain an approximate covariance matrix, then take square roots of its diagonal for standard errors. The fitted coefficient, hazard ratio, and confidence interval are assembled into a table.

PythonIn [14]
CollapseExpand
fit = optimize.minimize(
    lambda b: -cox_loglike_untied(b, X, time, event),
    x0=np.zeros(X.shape[1]),
    jac=lambda b: -cox_gradient(b, X, time, event),
    method="BFGS",
)
beta_hat = fit.x
H_hat = cox_hessian(beta_hat, X, time, event)
covariance_hat = np.linalg.solve(-H_hat, np.eye(X.shape[1]))
se_hat = np.sqrt(np.diag(covariance_hat))
assert (
    fit.success
    and np.max(np.abs(cox_gradient(beta_hat, X, time, event))) < 1e-5
)

summary = pd.DataFrame(
    {
        "covariate": ["maintained"],
        "coef": beta_hat,
        "hazard_ratio": np.exp(beta_hat),
        "se": se_hat,
        "ci_lower": np.exp(beta_hat - 1.96 * se_hat),
        "ci_upper": np.exp(beta_hat + 1.96 * se_hat),
    }
)
optimizer_success = fit.success
Out[15]:
covariate coef hazard_ratio se ci_lower ci_upper
0 maintained -0.882 0.414 0.637 0.119 1.442
ConsoleOut [16]
CollapseExpand
Optimizer success: True

The fitted coefficient is approximately -0.9 with a hazard ratio of about 0.4, which is drawn from the summary table above; the approximate model-based 95 percent Wald hazard-ratio interval spans from its ci_lower to its ci_upper column. Under the fitted fixed-covariate model, maintained equipment has about 41 percent of the reference hazard wherever the baseline hazard is positive. This describes the constructed example's conditional association. The interval includes one, and thirteen events do not give a precise maintenance-effect estimate.

To check that our implementation matches a mature library, we compare with statsmodels' PHReg, which fits the same partial likelihood. It uses default Breslow tie handling here, and we explicitly select Efron in the separate tied comparison. Agreement on this dataset is a useful numerical check. The stabilization identity, risk-set construction, and score formula still need to be checked directly.

PythonIn [17]
CollapseExpand
from statsmodels.duration.hazard_regression import PHReg

reference = PHReg(time, X, status=event).fit()
Out[18]:
our_beta statsmodels_beta our_se statsmodels_se
0 -0.8821 -0.8821 0.6368 0.6368

The two coefficient columns and the two standard-error columns should line up to several decimal places in the displayed table; small residual differences are compatible with optimizer and information-matrix-solve tolerance. Larger discrepancies call for checking the inputs, objective conventions, derivatives, and numerical settings. This is the expected outcome: PHReg and the scratch implementation maximize the same concave log partial likelihood.

Handling tied event times

Our example has no tied event times, so the untied likelihood is exact. Real data often produce ties; exact methods and approximations depend on how those recorded times are modeled. We can demonstrate the difference by coarsening the observation times into four-unit bins, which forces multiple events to land at the same recording time. We implement both the Breslow and Efron approximations and compare them. The Breslow version treats all tied failures as if they occurred with the full risk set available; the Efron version fractionally reduces the total weight of the tied failures across successive denominators.

PythonIn [19]
CollapseExpand
def cox_loglike_breslow(beta, X, time, event):
    """Breslow approximation for tied event times."""
    eta = X @ beta
    total = 0.0
    for t in np.unique(time[event == 1]):
        at_risk = time >= t
        events_t = (time == t) & (event == 1)
        d = events_t.sum()
        eta_r = eta[at_risk]
        m = eta_r.max()
        log_denominator = m + np.log(np.exp(eta_r - m).sum())
        total += eta[events_t].sum() - d * log_denominator
    return total


def cox_loglike_efron(beta, X, time, event):
    """Efron approximation for tied event times."""
    eta = X @ beta
    total = 0.0
    for t in np.unique(time[event == 1]):
        at_risk = time >= t
        events_t = (time == t) & (event == 1)
        d = int(events_t.sum())
        eta_r = eta[at_risk]
        eta_d = eta[events_t]
        m = eta_r.max()
        s0 = np.exp(eta_r - m).sum()
        s_d = np.exp(eta_d - m).sum()
        if d == 1:
            total += eta_d.sum() - (m + np.log(s0))
        else:
            log_denom = 0.0
            for ell in range(d):
                log_denom += m + np.log(s0 - (ell / d) * s_d)
            total += eta_d.sum() - log_denom
    return total

We now coarsen the observation times into bins of four units and fit both approximations.

PythonIn [20]
CollapseExpand
binned_time = np.ceil(time / 4).astype(int) * 4
X_b = X.copy()  # reuse the same covariate matrix without aliasing

fit_breslow = optimize.minimize(
    lambda b: -cox_loglike_breslow(b, X_b, binned_time, event),
    x0=np.zeros(X_b.shape[1]),
    method="BFGS",
)
fit_efron = optimize.minimize(
    lambda b: -cox_loglike_efron(b, X_b, binned_time, event),
    x0=np.zeros(X_b.shape[1]),
    method="BFGS",
)
reference_efron = PHReg(binned_time, X_b, status=event, ties="efron").fit()

tie_comparison = pd.DataFrame(
    {
        "method": [
            "Breslow (from scratch)",
            "Efron (from scratch)",
            "statsmodels Efron",
        ],
        "beta": [fit_breslow.x[0], fit_efron.x[0], reference_efron.params[0]],
        "hazard_ratio": [
            np.exp(fit_breslow.x[0]),
            np.exp(fit_efron.x[0]),
            np.exp(reference_efron.params[0]),
        ],
    }
)
Out[21]:
method beta hazard_ratio
0 Breslow (from scratch) -0.5892 0.5548
1 Efron (from scratch) -0.6364 0.5292
2 statsmodels Efron -0.6364 0.5292

The two tie approximations can also be compared over the whole coefficient path. The display grid is fixed and no additional fitting is performed in the figure cell.

PythonIn [22]
CollapseExpand
tie_beta_grid = np.linspace(-2.0, 1.0, 300)
tie_breslow_grid = np.array(
    [
        cox_loglike_breslow(np.array([b]), X_b, binned_time, event)
        for b in tie_beta_grid
    ]
)
tie_efron_grid = np.array(
    [
        cox_loglike_efron(np.array([b]), X_b, binned_time, event)
        for b in tie_beta_grid
    ]
)
tie_breslow_opt = float(fit_breslow.x[0])
tie_efron_opt = float(fit_efron.x[0])
tie_breslow_opt_value = cox_loglike_breslow(
    fit_breslow.x, X_b, binned_time, event
)
tie_efron_opt_value = cox_loglike_efron(fit_efron.x, X_b, binned_time, event)
VisualizationOut [23]
CollapseExpand
Two concave log partial likelihood curves for Breslow and Efron tied-time approximations.
Breslow and Efron log partial likelihoods for the coarsened tied-time dataset. The two curves are close but not identical, and their maxima sit at the fitted coefficients reported in the table. The gap reflects this coarsened sample rather than a universal rule about tie bias.

The executed tied-data table reports the Breslow and Efron coefficients and their hazard ratios alongside the explicitly selected statsmodels Efron fit. Our Efron coefficient matches the statsmodels Efron coefficient to within 10−510^{-5}. These are different approximations applied to this coarsened dataset, not a universal statement about the direction or size of tie bias.

Baseline hazard and survival curves

Estimating the baseline hazard lets us convert coefficients into absolute survival predictions rather than relative risk scores. We implement Breslow's estimator and predict survival for both groups at a grid of times. The estimator accumulates one increment at each event time: the number of events divided by the total at-risk weight. The prediction function then applies S^(t∣x)=exp⁡(−H^0(t)eβ^⊤x)\hat S(t \mid x) = \exp(-\hat H_0(t)e^{\hat\beta^\top x}).

PythonIn [24]
CollapseExpand
def breslow_baseline_cumhaz(beta, X, time, event):
    """Breslow estimator of the cumulative baseline hazard."""
    eta = X @ beta
    event_times = np.sort(np.unique(time[event == 1]))
    increments = []
    for t in event_times:
        at_risk = time >= t
        d = ((time == t) & (event == 1)).sum()
        increments.append(d / np.exp(eta[at_risk]).sum())
    return event_times, np.cumsum(increments)


def predict_survival(beta, t_grid, H0_times, H0_values, x):
    """Survival curve implied by the Cox model for a given covariate vector."""
    t_grid = np.asarray(t_grid, dtype=float)
    positions = np.searchsorted(H0_times, t_grid, side="right") - 1
    H0 = np.zeros_like(t_grid)
    valid = positions >= 0
    H0[valid] = H0_values[positions[valid]]
    return np.exp(-H0 * np.exp(x @ beta))

We compute the baseline and store predicted curves for the two covariate values.

PythonIn [25]
CollapseExpand
t_grid = np.unique(np.r_[0.0, time])
H0_times, H0_values = breslow_baseline_cumhaz(beta_hat, X, time, event)
surv_unmaintained = predict_survival(
    beta_hat, t_grid, H0_times, H0_values, np.array([0.0])
)
surv_maintained = predict_survival(
    beta_hat, t_grid, H0_times, H0_values, np.array([1.0])
)
event_rug_times = time[event == 1]
event_rug_y = np.full(event_rug_times.shape, -0.02)

The fitted survival curves start at one and are nonincreasing, with the maintained group at or above the unmaintained group over the displayed follow-up. Because the hazard ratio is constant under the fitted model, the vertical gap between the curves changes over time rather than remaining fixed.

We also compute Schoenfeld residuals, which we will use as a diagnostic in the Visualizing Cox Proportional Hazards section. For each event time, the residual is the failing individual's covariate minus the risk-set weighted average. These residuals are the empirical counterpart of the score contributions: they tell us how surprising each failure was relative to the risk set that produced it.

PythonIn [26]
CollapseExpand
def schoenfeld_residuals(beta, X, time, event):
    """Schoenfeld residual for each event and covariate."""
    eta = X @ beta
    p = X.shape[1]
    event_idx = np.flatnonzero(event)
    R = np.zeros((len(event_idx), p))
    for k, i in enumerate(event_idx):
        at_risk = time >= time[i]
        eta_r = eta[at_risk]
        w = np.exp(eta_r - eta_r.max())
        w = w / w.sum()
        R[k] = X[i] - w @ X[at_risk]
    return time[event_idx], R
PythonIn [27]
CollapseExpand
event_times_schoenfeld, schoenfeld = schoenfeld_residuals(
    beta_hat, X, time, event
)
trend = np.polyfit(event_times_schoenfeld, schoenfeld[:, 0], 1)
trend_times = np.sort(event_times_schoenfeld)
trend_values = np.polyval(trend, trend_times)

The fitted quantities are now stored, and the preceding tied-event demonstration uses the same scratch functions with binned times. The next section uses these quantities for survival and diagnostic figures.

Key Parameters

The main choices that affect the fitted Cox model and its predictions are:

  • Tie handling (cox_loglike_untied, cox_loglike_breslow, cox_loglike_efron): selects how tied event times are treated. Use the untied likelihood when times are distinct; use the Efron approximation for coarsened continuous times with ties; Breslow is a useful starting point but can pull estimates toward the null with heavy ties.
  • Optimizer tolerance and starting values (optimize.minimize, method="BFGS"): concavity is not a finite-estimability or convergence guarantee, so inspect the returned status and the score. Tighter gtol and sensible starting values help on harder problems.
  • Censoring/risk-set construction (time >= time[i]): censored individuals remain at risk until their censoring time and must be included in every applicable risk set; dropping them destroys the partial likelihood.
  • Baseline grid (t_grid, breslow_baseline_cumhaz): the survival grid determines where absolute survival is reported; it must cover the horizons of interest.

Key Methods

The methods we built map one-to-one onto the questions a practitioner asks of a Cox model. cox_loglike_untied plus cox_gradient implements estimation; breslow_baseline_cumhaz implements absolute risk reconstruction; schoenfeld_residuals implements diagnostics. Each can be replaced by a more sophisticated version (Efron ties, smoothed baseline, weighted residuals), and the underlying decomposition stays the same.

Visualizing Cox Proportional Hazards

The first visualization shows what the fitted Cox model implies about survival. We plot the baseline-implied survival curves for maintained and unmaintained equipment on a common time axis. The cumulative baseline is evaluated as a right-continuous step function, with each event time contributing an increment and the value staying constant between events. The comparison holds the fitted coefficient and baseline fixed while changing only the maintenance covariate. This is a model-implied association in a constructed dataset, not a causal experiment conclusion.

The residual panels ask a separate question: whether the fixed-coefficient model leaves a pattern over event time. Individual residuals are noisy, and a fitted trend is a descriptive aid rather than a formal proportional-hazards test. We compare the small equipment example with a separate seeded generator whose two groups have nonproportional hazards by construction. The absence of a visible trend in a single small sample cannot validate the assumption.

VisualizationOut [28]
CollapseExpand
Two declining step curves on a shared time axis. The maintained curve lies at or above the unmaintained curve, with a changing gap between them.
Survival curves fitted by the Cox model for maintained and unmaintained equipment. The maintained curve stays at or above the unmaintained curve, and the vertical gap changes with time even though the fitted hazard ratio is constant. This model-implied contrast comes from a constructed teaching dataset and is descriptive, not causal.

Both fitted curves are nonincreasing and start at one before the first event; the first plot cell shows a visible title and uses the shared style. A negative fitted maintenance coefficient places its curve at or above the reference curve. The absolute survival gap changes with cumulative hazard, and it need not reach its maximum or return to zero within the observed follow-up. The model's constant hazard multiplier is therefore a different quantity from a constant survival-probability difference or ratio.

The fitted survival curves imply a survival-probability ratio that changes with time. That ratio is a different quantity from the constant fitted hazard ratio, even though both are derived from the same Cox model.

The accompanying plot contrasts the two quantities directly: the survival-probability ratio rises in steps with accumulated baseline hazard, while the hazard ratio remains flat. This compares a rate ratio with a ratio of survival probabilities; neither plotted ratio is itself a probability.

PythonIn [29]
CollapseExpand
survival_ratio = surv_maintained / surv_unmaintained
fitted_hazard_ratio = float(np.exp(beta_hat[0]))
VisualizationOut [30]
CollapseExpand
A right-continuous rising step curve for the survival-probability ratio, compared with a flat hazard ratio line.
Survival-probability ratio between maintained and unmaintained equipment over time, compared with the constant fitted hazard ratio. The survival ratio rises in steps as the cumulative baseline hazard grows, while the hazard ratio stays fixed under the model. This model-implied contrast comes from a constructed teaching dataset and is descriptive, not causal.

The next visualization is a diagnostic plot. For each event, the Schoenfeld residual is a single number per covariate. Under proportional hazards, the true-parameter conditional residual mean is zero. Estimated residuals remain noisy; a trend is a reason to investigate, not proof of a changing effect.

VisualizationOut [31]
CollapseExpand
A scatter plot of Schoenfeld residuals against event time, with a dashed horizontal line at zero and a dotted straight-line trend. The residuals scatter around zero.
Schoenfeld residuals for the maintenance covariate plotted against event time. The residuals scatter around zero with no strong linear trend, which is consistent with proportional hazards. With only thirteen event residuals, this descriptive plot leaves substantial uncertainty about time-varying effects; an apparently flat trend does not validate proportional hazards.

In our equipment data the residuals are sparse (thirteen events) and the trend line is nearly flat, which is consistent with proportional hazards. Thirteen event residuals give only a sparse view of changes over time. A nearly flat fitted line can miss departures that occur outside the observed event times or have little linear association with time. We will discuss how to interpret residuals rigorously, and how to test the proportional-hazards assumption, in the Practical Implications section.

Proportional-hazards diagnostics and limitations

The Schoenfeld residual is the building block of the standard proportional-hazards diagnostic. Formally, for the pp-th covariate and event time t(k)t_{(k)} where individual iki_k fails, we defined

rik,p=xik,p−xˉp(t(k)),xˉp(t(k))=∑j∈R(t(k))xj,pexp⁡(β^⊤xj)∑j∈R(t(k))exp⁡(β^⊤xj).r_{i_k, p} = x_{i_k, p} - \bar{x}_p(t_{(k)}), \qquad \bar{x}_p(t_{(k)}) = \frac{\sum_{j \in R(t_{(k)})} x_{j, p} \exp(\hat\beta^\top x_j)}{\sum_{j \in R(t_{(k)})} \exp(\hat\beta^\top x_j)}.

At the true coefficient under the proportional-hazards model, conditional on a risk set and one failure, the event's covariate has the weighted risk-set mean. Its unscaled Schoenfeld residual therefore has conditional mean zero. Residuals evaluated at an estimated coefficient are noisy, so a visible trend suggests investigation rather than proving a changing effect. Grambsch and Therneau (1994) develop tests based on scaled Schoenfeld residuals and transformed time. This chapter plots descriptive trends; it does not execute that formal test.

This diagnostic has two limitations. First, sensitivity depends on event count and timing, covariate variation in the risk sets, and the departure being tested. Sparse events can leave little information about time-varying effects, while a precisely estimated departure can be statistically detectable even when its practical effect is small. Interpret effect size and uncertainty alongside the p-value. Second, a test based on a chosen transformation of time can have low power against departures with little association with that transformation. Nonmonotone patterns can therefore be missed, which is why residual plots are useful alongside a formal test.

To make the failure mode concrete, we simulate a dataset in which the hazard ratio is explicitly increasing over time. The covariate is binary; the x=0x = 0 group follows an exponential with constant hazard, and the x=1x = 1 group follows a Weibull with shape 2, which has an increasing hazard. The hazard ratio between the groups therefore grows over time, violating proportional hazards by construction.

PythonIn [32]
CollapseExpand
rng = np.random.default_rng(42)
n_sim = 300
x_sim = rng.integers(0, 2, size=n_sim).astype(float)
t_event_sim = np.where(
    x_sim == 0, rng.exponential(1.0, n_sim), 0.7 * rng.weibull(2.0, n_sim)
)
censor_sim = rng.uniform(0.0, 2.5, n_sim)
time_sim = np.minimum(t_event_sim, censor_sim)
event_sim = (t_event_sim <= censor_sim).astype(int)

X_sim = x_sim.reshape(-1, 1)
fit_sim = optimize.minimize(
    lambda b: -cox_loglike_untied(b, X_sim, time_sim, event_sim),
    x0=np.zeros(1),
    jac=lambda b: -cox_gradient(b, X_sim, time_sim, event_sim),
    method="BFGS",
    options={"gtol": 1e-7},
)
assert (
    fit_sim.success
    and np.max(np.abs(cox_gradient(fit_sim.x, X_sim, time_sim, event_sim)))
    < 1e-5
)
time_sim_events, resid_sim = schoenfeld_residuals(
    fit_sim.x, X_sim, time_sim, event_sim
)
trend_sim = np.polyfit(time_sim_events, resid_sim[:, 0], 1)
trend_sim_times = np.sort(time_sim_events)
trend_sim_values = np.polyval(trend_sim, trend_sim_times)

The following plot displays the individual residuals and their fitted trend for this known nonproportional generator.

VisualizationOut [33]
CollapseExpand
Two bands of binary-covariate Schoenfeld residuals against event time, with a dashed fitted trend rising from negative to positive.
Schoenfeld residuals from a simulated dataset whose hazard ratio increases with time by construction. Individual binary-covariate residuals form two bands, while the fitted descriptive trend rises from negative to positive. A similar trend in real data would prompt investigation rather than establish a violation on its own.

In this constructed example, the fitted residual trend rises over event time, while individual binary-covariate residuals form two bands. A similar trend in a real dataset is a reason to investigate proportional hazards and model specification. If a changing effect is supported, possible responses include a time-varying coefficient, stratification for a categorical covariate, or a spline interaction between the covariate and time.

Practical Implications

Practical use of the Cox model depends on the study design, the event count, and whether proportional hazards is plausible. The subsections below cover when the model is appropriate, best practices, data requirements, common pitfalls, and operational considerations.

When the Cox Model Is Appropriate

The Cox model is most useful when the question concerns relative effects on the hazard: which covariates raise or lower the instantaneous event rate, and by how much, while the shape of the baseline hazard is treated as a nuisance. This covers clinical treatment comparisons, equipment-maintenance studies, customer-churn analyses, and similar settings where follow-up is censored and the main estimand is a multiplicative hazard ratio. Its semiparametric structure helps when the baseline hazard is not well understood but proportional covariate effects are scientifically plausible. The partial likelihood retains censored observations in the risk-set denominators up to their censoring times.

When the main target is absolute risk, such as survival to a fixed horizon, the coefficient fit must be followed by baseline cumulative-hazard estimation and absolute-prediction validation. It is also a poor default when the log hazard ratio is expected to change sign or magnitude over time. Stratification, time-varying coefficients, or an accelerated failure-time model may be more defensible. If events are recorded on a coarse lattice with heavy ties, a discrete-time formulation can be preferable to forcing a continuous-time model through an approximation. These are boundaries of applicability, not defects of the estimator.

A further consideration is causal interpretation. A Cox coefficient describes an association between covariates and the hazard conditional on the included covariates and the censoring mechanism. Calling it a treatment effect requires a design that supports that claim, such as randomization or a defensible adjustment set, plus independent censoring. When those conditions fail, the model can still be useful for prediction or description, but the coefficient should not be presented as an unqualified causal effect.

Best Practices

Begin with the proportional-hazards assumption rather than treating it as an afterthought. Fit the model, inspect Schoenfeld residuals against event time, and use the Grambsch-Therneau test or an equivalent check to detect systematic drift. A residual plot is important even when a formal test is available, because nonmonotone violations can escape a test that is tuned to linear trend. If proportionality fails, choose a remedy that matches the scientific question: stratification for a categorical covariate whose effect is not of direct interest, a time-varying coefficient when the effect itself is the target, an accelerated failure-time model when the baseline shape is central, or a restricted time window when interpretation over the full follow-up is not required.

Choose and report the tie convention explicitly. In the executed statsmodels.PHReg comparison, ties="breslow" and ties="efron" select the two supported approximations; its default is Breslow. Our separate helper cox_loglike_efron implements the latter objective. The Breslow approximation is useful for exposition and for nearly untied data, but it can pull estimates toward the null when many events share a recorded time. Model building should also respect the event count: with few events, adding many covariates or interaction terms produces unstable coefficients and optimistic diagnostics, even if the optimizer converges. Penalization or a pre-specified smaller model is often more reliable than stepwise selection driven by p-values.

Report hazard ratios with confidence intervals and enough context to prevent rate-to-probability confusion. A coefficient table should be accompanied by fitted survival curves, median survival times, or survival probabilities at clinically or operationally meaningful horizons. When the model is used predictively, validate it on held-out data or with bootstrap resampling rather than relying on in-sample fit. Finally, state the tie method, the handling of missing data, and the result of the proportional-hazards check. These details determine whether the reported hazard ratio is reproducible and interpretable.

Data Requirements and Preprocessing

The model requires a nonnegative follow-up time, an event indicator, and a covariate matrix with no perfect collinearity. Continuous covariates should generally be centered and scaled before fitting. For an unpenalized fit, the partial likelihood is invariant to a nonsingular linear transformation of the covariates in the sense that fitted hazard ratios can be mapped back, while centering changes the baseline reference profile and scaling can reduce numerical problems caused by disparate coordinate units. With penalization, scaling also changes how strongly each original-scale coefficient is penalized, so the convention should be reported. Standardized coefficients refer to a chosen covariate scale; neither centering nor scaling removes collinearity or guarantees a stable fit. Categorical covariates need explicit reference levels. In a stratified fit, a covariate constant within every stratum has no within-risk-set variation and contributes no partial-likelihood information. Exact linear dependence among included columns causes design-matrix rank deficiency.

Deleting a row with missing covariates removes that individual from every risk set in which they would otherwise appear. The resulting loss of information and potential bias depend on the missingness mechanism and the study design. Complete-case analysis can therefore change both the composition of the risk sets and the estimand. Imputation is possible, but it must preserve the time and event structure and should be followed by sensitivity analysis. The censoring mechanism also belongs to the data requirements: the partial likelihood assumes that censoring is independent of the event process given the covariates. If dropout is related to prognosis, inverse-probability-of-censoring weighting or a joint modeling strategy may be needed; no routine residual plot from the Cox fit detects this problem.

Event count and covariate variation within the risk sets affect precision; a large raw sample by itself does not guarantee precise coefficient estimates. A historical heuristic suggests roughly ten to twenty events per candidate predictor parameter, counting all parameters considered before any selection. This is not a sample-size guarantee: the required number can be lower or higher depending on the predictors, expected performance, and desired precision. Nonlinear covariate effects should be examined with splines or other flexible terms before the final model is fixed, because the Cox model assumes linearity in the log hazard and will not flag a missed nonlinearity on its own.

Common Pitfalls

The most consequential pitfall is interpreting a hazard ratio as a risk ratio or a survival-probability ratio. A hazard ratio of 0.5 means that the instantaneous event rate is halved at every time under the model; it does not mean that the event is half as likely by any particular horizon. The corresponding survival curves can separate gradually and then reconverge. Presenting a hazard ratio without a survival curve or an absolute-risk summary invites this error, especially when the baseline hazard is steep.

A second pitfall is ignoring informative censoring. Censoring that remains associated with event risk after conditioning on the included covariates can distort risk sets and bias the fit. Marginal differences between dropouts and completers alone do not establish such a violation. The remedy is design-level: document the censoring rule, compare covariate distributions between completers and dropouts, and use sensitivity analysis or weighting when independence is doubtful. A related error is to treat censored observations as non-events and drop them; that destroys the risk-set information that makes the partial likelihood work.

Ties and model complexity are further sources of trouble. Using the Breslow approximation with heavy ties can bias hazard ratios toward the null (i.e., toward a hazard ratio of one), and ignoring ties altogether is not a neutral choice when event times are recorded coarsely. With few events, adding coefficients can leave little information for estimating their effects or changes over time. Inspect coefficient uncertainty and residual patterns together, and keep model complexity consistent with the available information. Finally, failing to check proportional hazards can leave a model that fits adequately on average but misstates the effect over time. The defensive routine is to plot residuals, test proportionality, use Efron ties when appropriate, and report absolute-risk summaries alongside relative effects.

Computational Considerations

The scratch functions compute all linear predictors in O(np)O(np) work, where nn is the number of subjects and pp the number of covariates. For EE untied events, their repeated full-length risk-set masks and scalar sums make one log partial likelihood evaluation cost O(np+En)O(np + En). If rir_i is the risk-set size at event ii, the gradient adds O(p∑iri)O(p\sum_i r_i) work and the Hessian adds O(p2∑iri)O(p^2\sum_i r_i) work. These direct implementations are appropriate for small or teaching datasets. The dense input and largest risk-set copy use O(np)O(np) storage, and the Hessian calculation also needs O(p2)O(p^2) matrix storage, but the repeated risk-set scans become expensive when both the sample size and the event count are large. Sorting by time and maintaining cumulative sums, or using a compiled survival library, reduces the repeated work substantially.

Optimization adds its own cost. Newton-Raphson and quasi-Newton methods require a gradient at each iteration and, for Newton-type methods, a Hessian solve whose cost grows with the cube of the number of covariates. The relative cost of forming derivatives and solving the Newton system depends on the covariate dimension and the risk-set work. Profile those components, and consider a specialized solver when dense derivative calculations are expensive. Penalization can stabilize a model when shrinkage fits the modeling goal; it also changes the objective being fitted. Tied-event approximations increase the work per event time, especially when many events share a recorded time. When repeated Python risk-set scans dominate the fit, use an optimized survival implementation and verify that it supports the required tie convention and model features. The useful crossover depends on the event count, covariate matrix, and computing environment.

Large problems also benefit from reducing the covariate set before fitting, using sparse representations for categorical expansions, and avoiding unnecessary replication of the baseline grid. If the goal is prediction rather than inference, subsampling or a penalized fit may be enough, but any subsampling scheme must preserve the risk-set structure. For the low-dimensional teaching implementation here, repeated risk-set scans can dominate runtime. Profile the actual fit before choosing distributed or out-of-core methods, especially when the covariate matrix or time-dependent data expansion is large.

Performance and Deployment Considerations

Model performance should be assessed on both the relative and absolute scales. The concordance index and time-dependent discrimination measures summarize ranking performance, while the log partial likelihood, likelihood-ratio tests, and Wald tests summarize fit and coefficient evidence. For absolute predictions, calibration plots, Brier scores at chosen horizons, and comparisons of predicted versus observed survival in risk groups are more informative than the coefficient table alone. Internal validation by bootstrap and external validation on a separate cohort are important when the model will be deployed, because in-sample metrics are optimistic, particularly when the event count is small.

Deployment requires storing more than the coefficient vector. Absolute survival prediction needs the fitted baseline cumulative hazard, the covariate preprocessing rules, the reference levels for categorical variables, and the tie method used during fitting. The model should be versioned together with these artifacts so that a later refit does not silently change the meaning of a stored hazard ratio. If the model is used for decision support, the output should include a survival probability or risk at a specified horizon, not only a hazard ratio, because the latter is not directly actionable on the probability scale.

Monitoring should track covariate drift, changes in the event rate, and calibration over calendar time. A model can retain a stable coefficient vector while the baseline hazard shifts, so periodic recalibration of the baseline may be necessary even when the relative effects remain plausible. Proportional hazards should also be rechecked after major changes in population or practice. When the model is updated, preserve a clear audit trail of the fitting sample, censoring rules, and diagnostic results; these are as much part of the deployed model as the coefficients themselves.

Summary

The Cox proportional hazards model is a semiparametric regression model for time-to-event data. It writes each individual's hazard as an unspecified baseline hazard multiplied by an exponential function of the covariates, h(t∣x)=h0(t)exp⁡(β⊤x)h(t \mid x) = h_0(t)\exp(\beta^\top x). The baseline cancels in every hazard ratio, producing the model's central interpretation: exp⁡(β)\exp(\beta) is the multiplicative effect of a unit change in a covariate on the hazard at every moment in time.

Estimation proceeds through the partial likelihood, which is constructed by asking, at each event time, how likely the observed failing individual was given who else was at risk. The baseline hazard factors out of every term and disappears. The log partial likelihood is concave. Its score subtracts the weighted risk-set covariate mean from each failing covariate; its Hessian is a negative sum of weighted covariance matrices. Concavity is useful, but finite estimability, risk-set variation and the actual optimizer diagnostics still need checking; full column rank alone does not establish a finite unique estimate or global Newton convergence. This chapter uses approximations for tied event times; the Efron approximation is generally preferred for coarsened continuous times with moderate tie counts, and the Breslow approximation is a useful starting point.

After fitting we recover the baseline cumulative hazard with Breslow's estimator and predict absolute survival via S^(t∣x)=exp⁡(−H^0(t)exp⁡(β^⊤x))\hat S(t \mid x) = \exp(-\hat H_0(t)\exp(\hat\beta^\top x)). The dimensionless hazard ratio remains a relative rate multiplier; the reconstructed survival function is a separate probability-valued quantity. A hazard ratio of 0.5 does not imply a survival ratio of 2. The survival gap depends on time and on the baseline, which is why the fitted survival curve is an essential companion to any coefficient table.

Finally, the model rests on the proportional-hazards assumption, and any serious application should verify it. Schoenfeld residuals, the Grambsch-Therneau test, and simple residual-versus-time plots detect the common failure modes. When proportionality is implausible, stratification, time-varying coefficients, or a switch to accelerated failure time models (the next chapter) offer principled alternatives. The Cox model is useful, but only when it is used as a model of relative hazards and not as a shortcut to absolute survival probabilities.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about the Cox proportional hazards model, from the partial likelihood derivation to tie handling, baseline reconstruction, and proportional-hazards diagnostics.

Cox Proportional Hazards Quiz

Question 1 of 80 of 8 completed
What functional form does the Cox proportional hazards model assume for an individual's hazard?
Join the discussionAsk a question or share what you learned.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

A different angle
All writing
Machine Learning

Evaluating Survival Predictions

Score censored survival models with concordance and IPCW Brier scores. Learn to compare Cox, Weibull AFT, and Kaplan-Meier forecasts honestly.

77 min read
Machine Learning

Kaplan–Meier and Nelson–Aalen Estimation

Learn Kaplan-Meier and Nelson-Aalen estimation for right-censored data, covering risk sets, ties, Greenwood variance, medians, and NumPy implementation.

39 min read
Machine Learning

Accelerated Failure-Time Models

Learn how AFT models fit censored event-time data, interpret coefficients as time ratios, and compare Weibull and log-normal survival curves.

44 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026coxproportional, author = {Michael Brenndoerfer}, title = {Cox Proportional Hazards}, year = {2026}, url = {https://mbrenndoerfer.com/writing/cox-proportional-hazards-from-scratch}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-28} }
APAAcademic
Michael Brenndoerfer (2026). Cox Proportional Hazards. Retrieved from https://mbrenndoerfer.com/writing/cox-proportional-hazards-from-scratch
MLAAcademic
Michael Brenndoerfer. "Cox Proportional Hazards." 2026. Web. October 28, 2026. <https://mbrenndoerfer.com/writing/cox-proportional-hazards-from-scratch>.
CHICAGOAcademic
Michael Brenndoerfer. "Cox Proportional Hazards." Accessed October 28, 2026. https://mbrenndoerfer.com/writing/cox-proportional-hazards-from-scratch.
HARVARDAcademic
Michael Brenndoerfer (2026) 'Cox Proportional Hazards'. Available at: https://mbrenndoerfer.com/writing/cox-proportional-hazards-from-scratch (Accessed: October 28, 2026).
SimpleBasic
Michael Brenndoerfer (2026). Cox Proportional Hazards. https://mbrenndoerfer.com/writing/cox-proportional-hazards-from-scratch
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy