Select the CoxNet penalizer by k-fold cross-validation.
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)| 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(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)