CoxNet

Elastic-net penalized Cox proportional hazards model.

Usage

Source

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() supplying a right-censored or counting-process 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() 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 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 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 drops automatically. Printing the fitted object shows the penalized coefficients and how many were driven to zero.

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 the elastic-net penalized Cox model to survival data.
predict() Predict log-hazard, risk, or survival probabilities from the penalized Cox model.
to_frame() Return the penalized coefficient table as a DataFrame.

fit()

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

Usage

Source

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 response (right-censored or counting-process). Built with Surv.right() or 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:

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):

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

Source

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:

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:

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):

coxnet.predict(lung[cols][:2], type="survival", times=[180, 365], format="polars")
shape: (2, 3)
timesubject_1subject_2
f64f64f64
180.0NaN0.785085
365.0NaN0.482538

to_frame()

Return the penalized coefficient table as a DataFrame.

Usage

Source

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:

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)
termestimatehazard_ratio
strf64f64
"age"0.0061741.006193
"sex"-0.4092620.66414
"ph.ecog"0.3673221.443863
"ph.karno"0.01.0
"wt.loss"-0.0006980.999303

Request a different backend with format=:

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