## cv_coxnet()


Select the [CoxNet](CoxNet.md#greenwood.CoxNet) penalizer by k-fold cross-validation.


Usage

``` python
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](Surv.md#greenwood.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](CoxNet.md#greenwood.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()](AFT.md#greenwood.AFT.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:

``` python
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:


``` python
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:


``` python
# 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:


``` python
# 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:


``` python
# 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
