cv_coxnet()

Select the CoxNet penalizer by k-fold cross-validation.

Usage

Source

cv_coxnet(
    surv,
    covariates=None,
    *,
    data=None,
    weights=None,
    l1_ratio=1.0,
    penalizers=None,
    n_penalizers=50,
    eps=0.001,
    k=5,
    metric="concordance",
    times=None,
    standardize=True,
    max_iter=1000,
    tol=1e-07,
    stratified=True,
    seed=None,
)

Evaluates a log-spaced grid of penalizer values (or a user-supplied sequence) with stratified k-fold cross-validation and returns the value that maximises concordance (or minimises the integrated Brier score).

Parameters

surv: Surv | Outcome | str

A right-censored or counting-process Surv response. An Outcome or a formula string such as 'Surv(time, status == 2) ~ age + sex' is also accepted, with its columns read from data. The right-hand side sets covariates.

covariates: Any = None

Covariate design (dataframe, 2-D array, or formula string with data).

data: Any = None

A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) holding the columns named by the response and covariates. When surv is an Outcome or a formula, rows with a missing value in any column used are dropped first, along with the matching rows of any arrays passed alongside.

weights: Any = None

Case weights, one per row (a column name in data, or an array). Must be finite and strictly positive. Default is None (all weights 1).

l1_ratio: float = 1.0

Elastic-net mixing parameter (default 1.0 = lasso). Fixed across the entire path (only the penalizer is varied).

penalizers: Any = None

An explicit sequence of penalizer values to evaluate. When None (the default) a log-spaced path of n_penalizers= values is generated automatically from lambda_max down to eps * lambda_max.

n_penalizers: int = 50

Number of log-spaced values in the auto-generated path (the default is 50). Ignored when penalizers= is supplied.

eps: float = 0.001

Ratio of smallest to largest auto-generated penalizer (default of 1e-3). Smaller values extend the path toward weaker regularisation. Ignored when penalizers= is supplied.

k: int = 5

Number of cross-validation folds (the default is 5).

metric: str = "concordance"

Scoring metric:

  • "concordance" (the default): Harrell’s C-statistic, where higher is better.
  • "brier": integrated IPCW Brier score, which requires times= (here lower is better).
times: Any = None

Evaluation times for metric="brier" (1-D array-like, \ge 2 values).

standardize: bool = True

Standardize covariates before penalising (default is True). Must match the standardize= argument used for the final CoxNet fit.

max_iter: int = 1000

FISTA iteration limit and convergence tolerance for each inner fit.

tol: int = 1000

FISTA iteration limit and convergence tolerance for each inner fit.

stratified: bool = True

Use stratified k-fold to preserve the event rate in every fold (the default is True).

seed: int | None = None
Random seed for reproducible fold assignments.

Returns

CoxNetCVResult
Object exposing best_penalizer_, penalizer_1se_, mean_scores_, std_scores_, n_nonzero_, and a to_frame() method for the path.

Details

Lambda path. When penalizers= is not supplied the path is

\lambda_{\max} = \frac{\max_j \bigl|\nabla_j\,\ell(\beta=0)\bigr|}{n \cdot \max(\alpha,\, 0.01)}, \quad \lambda_i = \lambda_{\max} \cdot \varepsilon^{i/(m-1)}, \quad i = 0,\ldots,m-1

where \nabla\ell(\beta=0) is the score of the partial log-likelihood at the null model, \alpha = l1_ratio, \varepsilon = eps, and m = n_penalizers.

1-SE rule. penalizer_1se_ is the largest penalizer (most regularized) whose mean score lies within one standard error of best_score_. Prefer it when parsimony matters as it yields a sparser model at no significant loss of predictive accuracy.

Fitting the final model. After selecting a penalizer, refit on all data:

result = gw.cv_coxnet(formula, data=df, seed=23)
final = gw.CoxNet(penalizer=result.best_penalizer_).fit(formula, data=df)

Examples

Run cross-validated lasso selection on the lung dataset:

import greenwood as gw

# Load data and write the model formula
lung = gw.load_dataset("lung", backend="polars")
formula = "Surv(time, status) ~ age + sex + ph.ecog + ph.karno + wt.loss"

# Run cross-validated lasso selection with 3 folds
result = gw.cv_coxnet(formula, data=lung, l1_ratio=1.0, n_penalizers=10, k=3, seed=23)
result
CoxNetCV (concordance, ↑ higher is better, l1_ratio=1.0, 3-fold)

  best penalizer : 0.046941  (mean concordance: 0.6377)
  1-SE  penalizer : 0.10113  (mean concordance: 0.6358)

  10 penalizers tested, range [0.000218, 0.218]

Inspect the full penalizer path:

# Inspect the full penalizer path
result.to_frame(format="polars")
shape: (10, 4)
penalizermean_scorestd_scoren_nonzero
f64f64f64f64
0.2178820.516310.028250.333333
0.1011320.6357980.0465192.333333
0.0469410.6377120.0329184.0
0.0217880.6362730.0328884.333333
0.0101130.6329440.0328195.0
0.0046940.6347510.0325185.0
0.0021790.63330.0338555.0
0.0010110.6325270.0346775.0
0.0004690.6325010.0347255.0
0.0002180.6330180.0350035.0

Fit the final model at the best penalizer:

# Refit at the best penalizer on all data
final = gw.CoxNet(penalizer=result.best_penalizer_, l1_ratio=1.0).fit(formula, data=lung)
final
CoxNet (elastic-net Cox, penalizer=0.046941245091417054, l1_ratio=1.0)

               coef
age        0.006586
sex         -0.4198
ph.ecog       0.376
ph.karno          0
wt.loss   -0.001171

n = 213, events = 151, nonzero coefficients = 4 (15 observations deleted due to missingness)

Use the 1-SE rule for a sparser model:

# Refit at the 1-SE penalizer for a sparser model
sparse = gw.CoxNet(penalizer=result.penalizer_1se_, l1_ratio=1.0).fit(formula, data=lung)
sparse
CoxNet (elastic-net Cox, penalizer=0.1011318468182378, l1_ratio=1.0)

             coef
age             0
sex       -0.2499
ph.ecog    0.2646
ph.karno       -0
wt.loss        -0

n = 213, events = 151, nonzero coefficients = 2 (15 observations deleted due to missingness)