
Fast Closed-Form Hazard Ratio Estimation via the Pike-Halley Estimator
Source:R/coxph_fast.R
coxph_fast.RdEstimates the hazard ratio for a two-group parallel trial using the
Pike-Halley Estimator, a pure closed-form approximation to the Cox partial
likelihood maximizer. The function returns the point estimate, its standard
error on the log scale, and a Wald-type confidence interval, using output
names consistent with summary(survival::coxph(...)). The C++ backend
accepts pooled sorted vectors directly, performing group splitting and all
accumulation in a single C++ pass without intermediate R-level vector copies.
Usage
coxph_fast(
time,
event,
group,
control,
side = 2,
conf.level = 0.95,
presorted = FALSE,
strata = NULL,
data = NULL
)Arguments
- time
A numeric vector of follow-up times for all subjects (pooled over both groups). Alternatively, a formula
Surv(time, event) ~ group, optionally withstrata()terms, with the variables indata; seeformula_interface.- event
An integer or numeric vector of event indicators (1 = event, 0 = censored), aligned with
time.- group
A vector of group labels aligned with
time. Any type that supports equality comparison is accepted.- control
A scalar value indicating which level of
grouprepresents the control group. Subjects withgroup != controlare treated as the treatment group. With the formula interface it defaults to the first level of the group variable.- side
1 for a one-sided test in the direction of treatment benefit (hazard ratio below 1, i.e. a negative coefficient) or 2 for a two-sided test (default 2). The returned vector has no p-value:
sideis stored as an attribute, and theprint()method reports the p-value that follows it. The confidence interval is always two-sided atconf.level.- conf.level
A single numeric value in (0, 1) specifying the confidence level for the Wald interval. Defaults to 0.95.
- presorted
A logical value. If
TRUE,time,event, andgroupare assumed to be already sorted in ascending order oftime(withstrata, sorted by stratum and by time within stratum, so that the rows of each stratum are contiguous), and the internalorder()call is skipped. IfFALSE(default), sorting is handled internally. The order is checked, and an error is given when it does not hold.- strata
An optional vector of stratum labels aligned with
time, without missing values. When supplied, the stratified estimator described in Details is computed. Several stratification factors can be combined withinteraction().NULL(default) gives the unstratified estimator.- data
A data frame in which the variables of a formula given as
timeare evaluated. Used only with the formula interface; seeformula_interface.
Value
An object of class "coxph_fast", which is a named numeric
vector of length 5 with elements matching the column names of
summary(coxph(...))$coefficients and
summary(coxph(...))$conf.int:
coefLog hazard ratio log(theta_hat).
exp(coef)Hazard ratio theta_hat (point estimate).
se(coef)Standard error of
coefon the log scale, equal to 1 / sqrt(I_0).lower .95Lower bound of the Wald confidence interval for the hazard ratio. The label reflects
conf.level(e.g.,"lower .90"whenconf.level = 0.90).upper .95Upper bound of the Wald confidence interval.
With strata, the number of strata is stored in the attribute
strata.
Returns a vector of NA_real_ values (still with class
"coxph_fast") when the estimate cannot be computed (e.g., no
events, all events in one group, or I_0 = 0).
The object also has the class "fastsurv", whose
as.data.frame(), coef(), and confint() methods
are described in as.data.frame.fastsurv.
Details
Let t_k (k = 1, ..., K) denote the distinct observed event times in the pooled sample. At each t_k, let n_T_k and n_C_k be the numbers at risk in the treatment and control groups just before t_k, and let O_T_k and O_C_k be the numbers of events in each group, with n_k = n_T_k + n_C_k and O_k = O_T_k + O_C_k. Define E_T = sum n_T_k O_k / n_k and E_C = sum n_C_k O_k / n_k as the log-rank expected event totals.
The Pike-Halley Estimator is obtained in three steps. First, the Pike anchor is computed as theta_0 = (O_T E_C) / (O_C E_T). Second, the score U_0, the observed information I_0, and the third-order curvature term J_0 of the Breslow partial likelihood are evaluated at eta_0 = log(theta_0):
p_k = n_T_k theta_0 / (n_C_k + n_T_k theta_0) U_0 = sum (O_T_k - O_k p_k) I_0 = sum O_k p_k (1 - p_k) J_0 = sum O_k p_k (1 - p_k) (1 - 2 p_k)
Third, a single Halley correction to the score is applied, expanded to second order so that it is in closed form (a third-order correction):
delta_hat = U_0 / I_0 - J_0 U_0^2 / (2 I_0^3) theta_hat = theta_0 exp(delta_hat)
This correction converges cubically, so the residual error |log(theta_hat) - log(theta_Cox)| is of the order of the cube of the error of the Pike anchor. Near the null hypothesis the anchor is already close to the Cox estimate and the residual error is negligible. At a fixed hazard ratio away from 1 the error of the Pike anchor relative to the Cox estimate does not vanish as the sample size grows, so the residual error levels off at a small value that depends on the hazard ratio and the censoring pattern instead of decreasing with the sample size.
The Wald standard error on the log scale is SE = 1 / sqrt(I_0), where I_0
is the observed information evaluated at the Pike anchor. The Wald
confidence interval reported by summary(coxph(...)) uses the
observed information at the maximum likelihood estimate instead. The two
differ by an amount of the order of the error of the Pike anchor, which is
small when the hazard ratio is not far from 1; Berry, Kitchin, and Mock
(1991) found the bias of the Pike estimator minimal for hazard ratios below
3.
With strata, the estimator targets the stratified Cox model, in which
each stratum has its own baseline hazard and the hazard ratio is common to
all strata. The at-risk sets are formed within each stratum, the observed
and expected totals O_T, O_C, E_T, and E_C are summed over strata to give
the stratified Pike anchor, and U_0, I_0, and J_0 are those of the
stratified Breslow partial likelihood (sums of the per-stratum terms). The
Halley correction and the Wald interval are then applied unchanged, so the
result approximates coxph(Surv(time, event) ~ group + strata(s),
ties = "breslow"). With a fixed number of strata the approximation error
shrinks at the same rate as in the unstratified case; it is larger when the
strata are small and the hazard ratio is far from 1, because the stratified
anchor is then further from the maximum likelihood estimate.
The C++ core (pihe_core) accepts the pooled sorted data together
with an integer group indicator and performs group splitting, at-risk
counting, and per-distinct-event-time accumulation in a single left-to-right
scan. This eliminates the rev(cumsum(rev(...))), tapply(),
which(), diff(), and group-split vector copies present in the
pure-R version.
The returned object has class "coxph_fast" and is a named numeric
vector of length 5. A print() method formats the result similarly
to summary(coxph(...)).
References
Cox, D. R. (1972). Regression models and life-tables. Journal of the Royal Statistical Society. Series B (Methodological), 34(2), 187-202.
Berry, G., Kitchin, R. M., & Mock, P. A. (1991). A comparison of two simple hazard ratio estimators based on the logrank test. Statistics in Medicine, 10(5), 749-755.
See also
coxph for the standard iterative Cox estimator.
print.coxph_fast for the print method.
Examples
library(survival)
# Compare coxph_fast with coxph on the ovarian dataset.
# coxph() treats rx as numeric with rx=1 as the reference (control),
# so set control = 1 for a consistent comparison.
fit_fast <- coxph_fast(ovarian$futime, ovarian$fustat, ovarian$rx, control = 1)
fit_fast
#> Pike-Halley estimator for the hazard ratio (two-group)
#>
#> control = 1
#> alternative = two.sided
#>
#> Coefficients:
#> coef exp(coef) se(coef) z Pr(>|z|)
#> group -0.5964 0.5508 0.5868 -1.016 0.31
#>
#> Hazard ratio and 95% Wald confidence interval:
#> exp(coef) exp(-coef) lower .95 upper .95
#> group 0.5508 1.8155 0.1744 1.7399
fit_cox <- summary(coxph(Surv(futime, fustat) ~ rx, data = ovarian))
cat("coxph_fast HR :", fit_fast["exp(coef)"], "\n")
#> coxph_fast HR : 0.5508019
cat("coxph HR :", fit_cox$coefficients[, "exp(coef)"], "\n")
#> coxph HR : 0.5508019
# Stratified by residual disease, compared with coxph(... + strata())
coxph_fast(ovarian$futime, ovarian$fustat, ovarian$rx, control = 1,
strata = ovarian$resid.ds)
#> Stratified Pike-Halley estimator for the hazard ratio (two-group, 2 strata)
#>
#> control = 1
#> alternative = two.sided
#>
#> Coefficients:
#> coef exp(coef) se(coef) z Pr(>|z|)
#> group -0.6626 0.5155 0.5945 -1.115 0.265
#>
#> Hazard ratio and 95% Wald confidence interval:
#> exp(coef) exp(-coef) lower .95 upper .95
#> group 0.5155 1.9399 0.1608 1.6529
coef(coxph(Surv(futime, fustat) ~ rx + strata(resid.ds), data = ovarian,
ties = "breslow"))
#> rx
#> -0.662632
# presorted = TRUE: sort once outside, reuse inside a loop
ord <- order(ovarian$futime)
coxph_fast(ovarian$futime[ord], ovarian$fustat[ord], ovarian$rx[ord],
control = 1, presorted = TRUE)
#> Pike-Halley estimator for the hazard ratio (two-group)
#>
#> control = 1
#> alternative = two.sided
#>
#> Coefficients:
#> coef exp(coef) se(coef) z Pr(>|z|)
#> group -0.5964 0.5508 0.5868 -1.016 0.31
#>
#> Hazard ratio and 95% Wald confidence interval:
#> exp(coef) exp(-coef) lower .95 upper .95
#> group 0.5508 1.8155 0.1744 1.7399
# Formula interface, and the result as a data frame (log hazard ratio)
fit_f <- coxph_fast(Surv(futime, fustat) ~ rx, data = ovarian)
as.data.frame(fit_f)
#> term estimate std.error conf.low conf.high statistic p.value
#> 1 coef -0.59638 0.5868442 -1.746574 0.5538135 -1.016249 0.3095107
exp(confint(fit_f))
#> 2.5 % 97.5 %
#> coef 0.1743704 1.739875
# \donttest{
# Speed comparison against coxph()
if (requireNamespace("microbenchmark", quietly = TRUE)) {
microbenchmark::microbenchmark(
coxph_fast = coxph_fast(ovarian$futime, ovarian$fustat, ovarian$rx, 2),
coxph = coxph(Surv(futime, fustat) ~ rx, data = ovarian),
times = 1000
)
}
#> Unit: microseconds
#> expr min lq mean median uq max neval cld
#> coxph_fast 27.191 31.6875 38.75332 40.871 44.942 94.301 1000 a
#> coxph 686.744 715.5070 752.97335 726.068 739.308 4688.872 1000 b
# }