Select the CoxNet penalizer by k-fold cross-validation.
cv_coxnet(
surv,
covariates,
*,
data=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
-
A right-censored or counting-process Surv response.
covariates: Any
-
Covariate design (dataframe, 2-D array, or formula string with data).
data: Any = None
-
DataFrame to evaluate a formula covariates string against.
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(y, x, seed=23)
final = gw.CoxNet(penalizer=result.best_penalizer_).fit(y, x)
Examples
Run cross-validated lasso selection on the lung dataset:
import greenwood as gw
# Load data and build a right-censored response
lung = gw.load_dataset("lung", backend="polars")
y = gw.Surv.right(lung["time"], event=(lung["status"] == 2))
cols = ["age", "sex", "ph.ecog", "ph.karno", "wt.loss"]
# Run cross-validated lasso selection with 3 folds
result = gw.cv_coxnet(y, lung[cols], 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)| penalizer | mean_score | std_score | n_nonzero |
|---|
| f64 | f64 | f64 | f64 |
| 0.217882 | 0.51631 | 0.02825 | 0.333333 |
| 0.101132 | 0.635798 | 0.046519 | 2.333333 |
| 0.046941 | 0.637712 | 0.032918 | 4.0 |
| 0.021788 | 0.636273 | 0.032888 | 4.333333 |
| 0.010113 | 0.632944 | 0.032819 | 5.0 |
| 0.004694 | 0.634751 | 0.032518 | 5.0 |
| 0.002179 | 0.6333 | 0.033855 | 5.0 |
| 0.001011 | 0.632527 | 0.034677 | 5.0 |
| 0.000469 | 0.632501 | 0.034725 | 5.0 |
| 0.000218 | 0.633018 | 0.035003 | 5.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(y, lung[cols])
final
CoxNet (elastic-net Cox, penalizer=0.046941245091417096, l1_ratio=1.0)
coef
age 0.006585
sex -0.4198
ph.ecog 0.3761
ph.karno 0
wt.loss -0.001172
n = 213, events = 151, nonzero coefficients = 4
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(y, lung[cols])
sparse
CoxNet (elastic-net Cox, penalizer=0.10113184681823788, 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