Source code for p2p.models.constrained_elasticnet

"""Constrained ElasticNet model."""

from __future__ import annotations

import cvxpy as cp
import numpy as np
import pandas as pd

from p2p.models.constrained_model import ConstrainedModel


[docs] class ConstrainedElasticNet(ConstrainedModel): """ElasticNet regression with constraints and explicit solver choice.""" def __init__( self, alpha: float = 1.0, l1_ratio: float = 0.5, zero_threshold: float = 1e-5, solver: str | None = None, **kwargs, ): """ Initialize constrained ElasticNet regression. Args: alpha: Overall regularization strength. l1_ratio: Mixing ratio between L1 and L2 penalties. zero_threshold: Coefficients below this absolute value are set to zero after fitting. solver: Optional CVXPY solver name. When omitted, CVXPY selects its default solver. **kwargs: Arguments passed to ``ConstrainedModel``. """ super().__init__(**kwargs) if solver is not None and ( not isinstance(solver, str) or not solver.strip() ): raise ValueError("solver must be a nonempty string or None") self.alpha = alpha self.l1_ratio = l1_ratio self.zero_threshold = zero_threshold self.solver = solver def fit(self, A: pd.DataFrame, y: np.ndarray): X = self.safe_df_to_numpy(A) n, p = X.shape beta = cp.Variable(p) intercept = self._intercept_variable() loss = cp.sum_squares(self._linear_predictor(X, beta, intercept) - y) / (2 * n) l1_penalty = self.l1_ratio * cp.norm1(beta) l2_penalty = ( (1 - self.l1_ratio) * cp.sum_squares(beta) / 2 ) # The intercept is not penalised. penalty = self.alpha * (l1_penalty + l2_penalty) constraints, penalty_soft = self._build_constraints(X, A, beta, intercept) objective = cp.Minimize(loss + penalty + penalty_soft) problem = cp.Problem(objective, constraints) self.solve_with_fallback(problem, beta, "constrained ElasticNet") self._store_intercept(intercept) self.beta_raw_ = beta.value self.beta_ = beta.value.copy() self.beta_[np.abs(self.beta_) < self.zero_threshold] = 0.0 self.status_ = problem.status self.objective_value_ = problem.value self.feature_names_ = A.columns.tolist() self.X_ = X self.A_ = A self.y_ = y self.n_obs_, self.n_features_ = X.shape self.log_fit_summary() return self def count_nonzero_weights(self) -> int: return int(np.sum(self.beta_ != 0)) def get_support(self) -> np.ndarray: return self.beta_ != 0