Skip to contents

Fit right-censored Cox proportional hazards models with Breslow ties, adaptive L0 selection, an optional relaxed refit, and automatic LLT/PCG routing.

Details

Use family=mgcv::cox.ph() as family metadata and supply survival::Surv(time,status) or a two-column time/status matrix. Status is zero for censoring and one for an observed event; it belongs in the response, not in weights. No intercept is identifiable. Constant columns must be removed. Delayed entry, strata, time-dependent covariates, multistate responses and Efron/exact tie methods are not supported.

The hazard is \(h_i(t)=h_0(t)\exp(x_i^T\beta+o_i)\). For event times \(t\), define the risk set \(R_t=\{r:T_r\ge t\}\), \(d_t=\sum_{i:T_i=t}w_i\delta_i\), and \(S_t=\sum_{r\in R_t}w_r\exp(\eta_r)\). The weighted Breslow partial log likelihood is $$\ell(\beta)=\sum_t\left\{\sum_{i:T_i=t}w_i\delta_i\eta_i -d_t\log S_t\right\}.$$ Censored observations remain in risk sets. Weights follow the weighted Breslow case-weight definition, including fractional weights.

Quadratic IRLS and matrix-free products

Let \(u=\partial\ell/\partial\beta\) and \(H=-\partial^2\ell/\partial\beta\partial\beta^T\). With ridge diagonal \(D\) and center \(a\), a working update solves $$(H+D)\Delta=u-D(\beta-a),\qquad \beta^+=\beta+\Delta.$$ The center \(a\) is prior.mean, supplied in original predictor units as a scalar, length-\(p\) vector or \(p\) by \(1\) matrix, without an intercept. Normalization is handled internally; NULL uses zero centers. These are targets for log-relative-hazard coefficients, not the baseline hazard. Nonzero targets cannot reactivate predictors excluded by explicit zero starts; their rows remain zero in the original sparse coefficient dimensions. Unpenalized mandatory terms are unaffected by their targets. The final relaxed refit removes the ridge penalty on retained support. An exact partial-likelihood line search safeguards the current ridge subproblem. For selectable terms, default adaptive weights are \(D_j=\lambda/(4\beta_j^2)\). Mandatory terms have zero ridge penalty. The heuristic target is \(-2\ell(\beta)+\lambda\|\beta\|_0\), excluding mandatory terms from the L0 count. This does not certify a global optimum.

For \(e_i=w_i\exp(\eta_i)\) and \(q_i=e_i\sum_{t\le T_i}d_t/S_t\), the observation information satisfies $$(Wv)_i=q_iv_i-e_i\sum_{t\le T_i}{d_t\over S_t^2} \sum_{r\in R_t}e_rv_r,\qquad H=X^TWX.$$ After sorting times once, two cumulative sums evaluate this expression. Thus a product \((H+D)v\) costs a design multiplication, its transpose, and linear risk-set work, without forming an observation or coefficient Hessian. Time ordering, tie groups, event weights and risk membership are cached once per fit; fitted likelihood, baseline hazard and training scores reuse the cache. The public data and predictions retain their original row order.

PCG uses diagonal preconditioning \(M_{jj}=\sum_iq_ix_{ij}^2+D_j\), an inexpensive positive curvature bound. Before accepting convergence it recomputes the actual residual and requires $$\|M^{-1/2}(A\Delta-b)\|_2/ \|M^{-1/2}b\|_2\le\texttt{cox.cg.tol},\qquad A=H+D.$$ For small direct systems, exact information is assembled using $$H=X^T\operatorname{diag}(q)X-\sum_t d_t m_tm_t^T,\qquad m_t=\sum_{r\in R_t}e_rx_r/S_t,$$ and solved with Cholesky LLT.

Initialization and solver controls

Automatic initialization retains the weak-ridge pilot $$(X^T\operatorname{diag}(q_0)X+\kappa I)\beta_0=X^Ts_0, \qquad \kappa=10^{-3},$$ where the score and weights are evaluated at zero coefficients, including offsets. The pilot uses matrix-free PCG too. Wide designs solve its equivalent observation-space Woodbury system without forming that matrix. The resulting step is safeguarded against the pilot's exact penalized likelihood. fit$initialization records its space, iterations and scaled residual. Explicit starts bypass this pilot. Zero starts lock selectable coefficients out; intercept/mandatory eligibility conventions are retained.

control.family accepts cox="auto", "direct", or "pcg"; cox.llt.max=c(dense=1024L,sparse=0L); cox.cg.tol=1e-10; and cox.cg.maxit=2000L. Automatic routing uses LLT for dense active systems up to the threshold when the active count does not exceed the observation count. Sparse and wide systems use PCG. A scalar cox.llt.max overrides both storage thresholds. The default reflects measured systems, not a universal timing crossover. Automatic LLT also limits the risk-mean temporary to ten million elements. max.working=2000L guards direct working dimensions; matrix-free solves are not restricted by it.

PCG requires unrestricted coefficients and solver="eigen". Automatic routing retains the direct path for constrained/non-Eigen solvers. Eigen nonnegative solves use the package's clipping approximation; use OSQP for formal bounds. A failed automatic PCG solve warns and falls back to a permitted small direct system. Forced PCG or a large system fails explicitly; it never silently accepts an inaccurate inner solve. Increase cox.cg.maxit for difficult near-interpolating designs. fit$history records each backend, inner iteration count and residual. These loops are serial. Check fit$convergence for both fitting passes.

Prediction, scoring and cross-validation

Breslow increments estimate \(d\widehat H_0(t)=d_t/S_t\). predict(type="survival",times=...) returns \(\exp\{-\widehat H_0(t)\exp(\eta)\}\) using log cumulative hazards. type="link" returns \(\eta\); type="risk" and the default type="response" return \(\exp(\eta)\). New-data offsets must be supplied explicitly when needed. metrics$deviance is \(-2\ell\), not saturated-model GLM deviance. Presets use weighted event count as sample size and selectable predictor count as dimension. Cox CV evaluates \(-2\{\ell_{\rm full}(\widehat\beta_{-k})- \ell_{-k}(\widehat\beta_{-k})\}\) per held-out event weight. This retains full risk sets while estimating coefficients on training data only. fit_best and fit_1se are full-data refits.

References

Simon, N., Friedman, J., Hastie, T. and Tibshirani, R. (2011). Regularization paths for Cox's proportional hazards model via coordinate descent. Journal of Statistical Software 39(5), 1–13. The comparison example uses data from the glmnet Coxnet vignette.

Verweij, P. J. M. and van Houwelingen, H. C. (1993). Cross-validation in survival analysis. Statistics in Medicine 12, 2305–2314.

Examples

if (requireNamespace("mgcv", quietly = TRUE) &&
    requireNamespace("survival", quietly = TRUE) &&
    requireNamespace("glmnet", quietly = TRUE)) {
  # Same right-censored example data as the glmnet Coxnet vignette.
  data("CoxExample", package = "glmnet")
  X <- CoxExample$x
  y <- survival::Surv(CoxExample$y[, "time"], CoxExample$y[, "status"])
  set.seed(203)
  train <- sample(seq_len(nrow(X)), floor(0.7 * nrow(X)))
  Xtr <- X[train, , drop = FALSE]
  ytr <- y[train, , drop = FALSE]
  foldid <- sample(rep(1:3, length.out = length(train)))
  time_l0 <- system.time(tune_cox <- L0glm.tune(Xtr, ytr,
    family = mgcv::cox.ph(), tune = "cv", foldid = foldid,
    nlambda = 6, plot = FALSE, printbest = FALSE, relaxed = TRUE,
    maxit = 200, tol = 1e-8))[["elapsed"]]
  fit_cox <- tune_cox$fit_best
  # Recent glmnet releases offer an explicit tie-method option.
  ties <- if ("cox.ties" %in% names(formals(glmnet::glmnet)))
    list(cox.ties = "breslow") else list()
  time_lasso <- system.time(lasso_cox <- do.call(glmnet::cv.glmnet,
    c(list(x = Xtr, y = ytr, family = "cox", alpha = 1,
           foldid = foldid, grouped = TRUE), ties)))[["elapsed"]]
  eta_l0 <- as.numeric(predict(fit_cox, X[-train, , drop = FALSE], type = "link"))
  eta_lasso <- as.numeric(predict(lasso_cox, X[-train, , drop = FALSE],
                                   s = "lambda.min", type = "link"))
  ytest <- y[-train, , drop = FALSE]
  concordance <- function(eta) survival::concordance(ytest ~ eta, reverse = TRUE)$concordance
  comparison_cox <- data.frame(method = c("L0 CV", "LASSO CV"),
    selected = c(fit_cox$metrics$selected,
                 sum(as.numeric(coef(lasso_cox, s = "lambda.min")) != 0)),
    heldout_C = c(concordance(eta_l0), concordance(eta_lasso)),
    seconds = c(time_l0, time_lasso))
  print(comparison_cox)
  # Penalty values differ in definition and scale between L0 and LASSO.
  fit_cox$convergence
  survival_first_three <- predict(fit_cox, X[1:3, , drop = FALSE],
    type = "survival", times = c(1, 2, 4))
  survival_first_three
}