## CoxNet


Elastic-net penalized Cox proportional hazards model.


Usage

``` python
CoxNet(
    penalizer=0.1, l1_ratio=0.5, *, standardize=True, max_iter=1000, tol=1e-07
)
```


When the number of covariates is large relative to the sample size, unpenalized Cox models may overfit or fail to converge due to multicollinearity. CoxNet addresses this by adding a penalty term to the partial likelihood, which shrinks coefficients toward zero and can perform automatic variable selection. The elastic-net penalty combines \\L_1\\ (lasso) and \\L_2\\ (ridge) penalties: \\\lambda(\alpha\\\beta\\\_1 + \tfrac{1-\alpha}{2}\\\beta\\\_2^2)\\, where the mixing parameter \\\alpha\\ controls the trade-off between sparsity and smoothness.

Fit the model with [fit()](AFT.md#greenwood.AFT.fit) supplying a right-censored or counting-process [Surv](Surv.md#greenwood.Surv) response and a design matrix of covariates. The algorithm uses FISTA (Fast Iterative Shrinkage- Thresholding Algorithm) to optimize the penalized partial likelihood. By default, covariates are standardized before penalizing (for fair comparison of penalties across features), but coefficients are returned on the original scale. Ridge (`l1_ratio=0`) encourages small, spread-out coefficients; lasso (`l1_ratio=1`) drives some coefficients exactly to zero.

The implementation follows the glmnet model for elastic-net regularization, using coordinate descent-like optimization with soft-thresholding. Results include penalized coefficients, standard errors, and indicators of which features were selected (non-zero coefficients). Unlike unpenalized Cox, this model does not compute hazard ratios or perform formal hypothesis tests on individual coefficients.


## Parameters


`penalizer: float = ``0.1`  
Overall penalty strength (`lambda`). `0` recovers the unpenalized Breslow Cox fit.

`l1_ratio: float = ``0.5`  
Elastic-net mixing in `[0, 1]`: `1` is lasso, `0` is ridge.

`standardize: bool = ``True`  
Standardize covariates to unit variance before penalizing (default `True`, as in glmnet). Coefficients are returned on the original scale.

`max_iter: int = ``1000`  
Maximum FISTA iterations and the relative-change convergence tolerance.

`tol: int = ``1000`  
Maximum FISTA iterations and the relative-change convergence tolerance.


## Returns


`Fitted estimator`  
Call [fit()](AFT.md#greenwood.AFT.fit) to produce a fitted estimator with cached results (`coef_`, `std_error_`, `n_features_in_`, `feature_names_in_`), accessible as arrays or exported to DataFrames.


## Details

Call `fit(surv, covariates)` with a right-censored or counting-process [Surv](Surv.md#greenwood.Surv) response. `covariates` may be a dataframe, a 2-D array, or a formula string with `data`. Stratified penalized fits are not supported.


## Examples

Build a [Surv](Surv.md#greenwood.Surv) response from the bundled `lung` dataset and fit a lasso (`l1_ratio=1.0`) elastic-net Cox model over several covariates. The `ph.ecog`, `ph.karno`, and `wt.loss` columns have missing values, which [CoxNet](CoxNet.md#greenwood.CoxNet) drops automatically. Printing the fitted object shows the penalized coefficients and how many were driven to zero.


``` python
import greenwood as gw

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"]
coxnet = gw.CoxNet(penalizer=0.05, l1_ratio=1.0).fit(y, lung[cols])
coxnet
```


    CoxNet (elastic-net Cox, penalizer=0.05, l1_ratio=1.0)

                    coef
    age         0.006174
    sex          -0.4093
    ph.ecog       0.3673
    ph.karno           0
    wt.loss   -0.0006976

    n = 213, events = 151, nonzero coefficients = 4


## Methods

| Name | Description |
|----|----|
| [fit()](#fit) | Fit the elastic-net penalized Cox model to survival data. |
| [predict()](#predict) | Predict log-hazard, risk, or survival probabilities from the penalized Cox model. |
| [to_frame()](#to_frame) | Return the penalized coefficient table as a DataFrame. |

------------------------------------------------------------------------


#### fit()


Fit the elastic-net penalized Cox model to survival data.


Usage

``` python
fit(surv, covariates, *, data=None)
```


Fits a Cox proportional-hazards model with elastic-net penalty (L1 + L2 regularization) to a right-censored or counting-process response and covariates. The penalty shrinks coefficients toward zero, selecting a sparse subset of important variables (when L1 dominates) or smoothly shrinking all coefficients (when L2 dominates). An intercept is added automatically.

The CoxNet model is useful for high-dimensional covariate spaces where unpenalized Cox fails to converge or produces unstable estimates. It maintains the proportional-hazards interpretation of hazard ratios while controlling model complexity. Tuning the `penalizer` strength and `l1_ratio` mixing parameter enables variable selection and regularized estimation.


##### Parameters


`surv: Surv`  
A [Surv](Surv.md#greenwood.Surv) response (right-censored or counting-process). Built with [Surv.right()](Surv.md#greenwood.Surv.right) or [Surv.counting()](Surv.md#greenwood.Surv.counting).

`covariates: Any`  
A dataframe (pandas or polars), a 2-D array, or a formula string (e.g., `"age + sex"`) evaluated against the `data` argument.

`data: Any = None`  
A dataframe to evaluate the formula string (ignored if `covariates` is a dataframe or array).


##### Returns


`CoxNet`  
The fitted estimator object itself (for method chaining) with cached coefficient arrays, standard errors, and model metrics.


##### Details

The elastic-net penalty is \\\lambda(\alpha L_1 + (1 - \alpha) L_2)\\, where \\\lambda\\ = `penalizer` and \\\alpha\\ = `l1_ratio`. Setting `l1_ratio=1` gives lasso (\\L_1\\ only, induces sparsity); `l1_ratio=0` gives ridge (\\L_2\\ only, smooth shrinkage); intermediate values blend both effects.

Estimation uses proximal gradient descent (FISTA) to optimize the penalized partial likelihood. Covariates are centered and optionally standardized before fitting; standardization affects the penalty scale but not the fitted hazard ratios.


##### Examples

Fit a ridge-penalized Cox model (L2 penalty, smooth shrinkage) on the bundled `lung` dataset:


``` python
import greenwood as gw

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"]
coxnet_ridge = gw.CoxNet(penalizer=0.05, l1_ratio=0.0).fit(y, lung[cols])
coxnet_ridge
```


    CoxNet (elastic-net Cox, penalizer=0.05, l1_ratio=0.0)

                   coef
    age         0.01397
    sex         -0.5626
    ph.ecog      0.5908
    ph.karno   0.008956
    wt.loss   -0.007546

    n = 213, events = 151, nonzero coefficients = 5


Fit a lasso-penalized Cox model (L1 penalty, sparse selection):


``` python
coxnet_lasso = gw.CoxNet(penalizer=0.05, l1_ratio=1.0).fit(y, lung[cols])
coxnet_lasso
```


    CoxNet (elastic-net Cox, penalizer=0.05, l1_ratio=1.0)

                    coef
    age         0.006174
    sex          -0.4093
    ph.ecog       0.3673
    ph.karno           0
    wt.loss   -0.0006976

    n = 213, events = 151, nonzero coefficients = 4


------------------------------------------------------------------------


#### predict()


Predict log-hazard, risk, or survival probabilities from the penalized Cox model.


Usage

``` python
predict(newdata=None, *, type="lp", times=None, format=None)
```


Generates predictions from a fitted elastic-net penalized Cox model. Pass `newdata=None` to predict for the training data (fitted subjects).

Three prediction types are available:

1.  **Linear predictor** (`type="lp"`): the centered log-hazard \\X\beta\\, a risk score showing how covariates affect hazard. Higher values indicate higher risk. Centered means the baseline is set such that \\\exp(\text{lp}) = 1\\ for an average subject (average covariate values).

2.  **Risk** (`type="risk"`): the relative hazard \\\exp(\text{lp})\\, comparing each subject's hazard to the baseline (average). A value of 2.0 means 2x baseline hazard.

3.  **Survival** (`type="survival"`): survival probabilities \\S(t \mid x)\\ at specified times, returned as a DataFrame. Uses the baseline cumulative hazard from the training data and applies the covariate adjustment via relative risk.


##### Parameters


`newdata: Any = None`  
Covariate values for prediction. A dataframe (Pandas or Polars), 2-D array, or None (default). If `None`, uses the training data (design matrix used at fit time). Must have the same columns/features as the training data. Covariates are centered using the centering from the training data.

`type: str = ``"lp"`  
Prediction type (default `"lp"`):

- `"lp"`: Centered linear predictor \\X\beta\\ (log-hazard). Returns an array.
- `"risk"`: Relative risk \\\exp(\text{lp})\\. Returns an array (always positive).
- `"survival"`: Survival probabilities \\S(t \mid x)\\ at times in `times`. Returns a frame with `time` column and one column per subject.

`times: Any = None`  
Query times for `type="survival"` (ignored for other types). An array-like of floats. If `None` (the default), uses the event times from the training data (baseline cumulative hazard times).

`format: str | None = None`  
Output format for the returned frame (`type="survival"`): `None` (default), `"pandas"`, `"polars"`, or `"pyarrow"`. When `None`, a backend is auto-detected (Polars, then Pandas, then PyArrow). Ignored for `type="lp"` and `type="risk"`.


##### Returns


`ndarray or DataFrame`  
For `type="lp"` or `type="risk"`: an array of shape (n_subjects,) containing centered log-hazard or relative risk values respectively. For `type="survival"`: a DataFrame with columns `time` (query times) and `subject_1`, `subject_2`, etc. containing survival probabilities at each time. Column names match the input row index if `newdata` has a row index.


##### Raises


`ValueError`  
If `type` is not one of `"lp"`, `"risk"`, or `"survival"`.


##### Details

The penalized Cox model estimates \\\exp(\text{lp})\\ as a multiplier on the baseline cumulative hazard: \\H(t \mid x) = H_0(t)\\\exp(\text{lp})\\. Survival is then \\S(t \mid x) = \exp(-H(t \mid x))\\. The baseline cumulative hazard \\H_0(t)\\ is estimated using the Breslow estimator from the training data, and is fixed for new predictions.

Centering ensures that the linear predictor at the average covariate level is 0, making relative risks and survival curves interpretable. Predictions assume the model is well-specified and that the proportional-hazards assumption holds.


##### Examples

The default `type="lp"` returns the centered linear predictor (log-hazard). Here are the values for the first five subjects:


``` python
import greenwood as gw

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"]
coxnet = gw.CoxNet(penalizer=0.05, l1_ratio=1.0).fit(y, lung[cols])
coxnet.predict(lung[cols], type="lp")[:5]
```


    array([        nan, -0.1476714 , -0.22176203,  0.15452507, -0.18660117])


Pass `type="risk"` for the relative risk \\\exp(\text{lp})\\, showing how many times the baseline hazard each subject has:


``` python
coxnet.predict(lung[cols], type="risk")[:5]
```


    array([       nan, 0.86271456, 0.80110598, 1.16710353, 0.82977461])


Pass `type="survival"` for predicted survival curves at specified times or at the event times from training data; pass `format=` to choose the backend (here, Polars):


``` python
coxnet.predict(lung[cols][:2], type="survival", times=[180, 365], format="polars")
```


shape: (2, 3)

| time  | subject_1 | subject_2 |
|-------|-----------|-----------|
| f64   | f64       | f64       |
| 180.0 | NaN       | 0.785085  |
| 365.0 | NaN       | 0.482538  |


------------------------------------------------------------------------


#### to_frame()


Return the penalized coefficient table as a DataFrame.


Usage

``` python
to_frame(*, format=None)
```


Exports one row per term with the penalized coefficient estimate and its hazard ratio. Terms set to zero by the lasso remain in the table with zero estimates.


##### Parameters


`format: str | None = None`  
Output format: `None` (default), `"pandas"`, `"polars"`, or `"pyarrow"`. When `None`, a backend is auto-detected (Polars, then Pandas, then PyArrow).


##### Returns


`pandas.DataFrame, polars.DataFrame, or pyarrow.Table`  
A tidy table with columns `term`, `estimate`, and `hazard_ratio`.


##### Raises


`ImportError`  
If the requested (or, when auto-detecting, any) DataFrame library is not installed.


##### Examples

Fit a lasso-penalized Cox model and export the coefficient table as a Polars frame:


``` python
import greenwood as gw

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"]
coxnet = gw.CoxNet(penalizer=0.05, l1_ratio=1.0).fit(y, lung[cols])
coxnet.to_frame(format="polars")
```


shape: (5, 3)

| term       | estimate  | hazard_ratio |
|------------|-----------|--------------|
| str        | f64       | f64          |
| "age"      | 0.006174  | 1.006193     |
| "sex"      | -0.409262 | 0.66414      |
| "ph.ecog"  | 0.367322  | 1.443863     |
| "ph.karno" | 0.0       | 1.0          |
| "wt.loss"  | -0.000698 | 0.999303     |


Request a different backend with `format=`:


``` python
coxnet.to_frame(format="pandas")
```


|     | term     | estimate  | hazard_ratio |
|-----|----------|-----------|--------------|
| 0   | age      | 0.006174  | 1.006193     |
| 1   | sex      | -0.409262 | 0.664140     |
| 2   | ph.ecog  | 0.367322  | 1.443863     |
| 3   | ph.karno | 0.000000  | 1.000000     |
| 4   | wt.loss  | -0.000698 | 0.999303     |
