Source code for lpspline.spline.base


import abc
import numpy as np
import cvxpy as cp
from typing import List, Optional

[docs] class Spline(abc.ABC): """ Abstract base class for all spline types. """ def __init__(self, term: str, tag: Optional[str] = None): """ Initialize the Spline component. Parameters ---------- term : str The name of the feature column this spline models. tag : Optional[str], default=None An optional tag to identify this specific spline component. """ self._term = term self._tag = tag self._constraints = [] self._penalties = [] self._variables = [] self._by = None # column name of by reference values self._by_classes = None # set of unique by values self._by_int_map = None # map of by values to integer indices @property def by(self) -> Optional[str]: """ Returns the grouping column name used to group spline coefficients. Returns ------- Optional[str] The name of the column used for the `by` argument, or None. """ return self._by @property def variables(self) -> List[cp.Variable]: """ Returns the CVXPY variables representing the spline coefficients. Returns ------- List[cp.Variable] The list of CVXPY variables constructed during fitting. """ return self._variables @property def constraints(self) -> List[cp.Constraint]: """ Returns the CVXPY constraints associated with the spline. Returns ------- List[cp.Constraint] A list of all constraints that apply to this spline. """ return self._constraints @property def penalties(self) -> List[cp.Expression]: """ Returns the penalty expressions associated with the spline. Returns ------- List[cp.Expression] A list of CVXPY expressions representing the penalties. """ return self._penalties @property def term(self) -> str: """ Returns the column name or term this spline models. Returns ------- str The column name or term string. """ return self._term @property def tag(self) -> Optional[str]: """ Returns the custom tag for this spline component. Returns ------- Optional[str] The tag assigned to this spline component, or None. """ return self._tag @property def coefficients(self) -> np.ndarray: """ Returns the fitted coefficients of the spline. Returns ------- np.ndarray The computed coefficient values from the CVXPY variables. """ return np.array(self._variables.value)
[docs] def init_spline(self, x: np.ndarray, by: np.ndarray = None): """ Initialize core parameters to build the spline basis according to the training data. This method is meant to be overridden by subclasses to initialize knot locations, base configurations, or other data-dependent structures. Parameters ---------- x : np.ndarray The 1D input array of training features for this spline. by : np.ndarray, default=None The array of grouping values, if a `by` variable is specified. """ if self._by is not None and by is not None: self.init_by(by)
[docs] def init_by(self, by: np.ndarray): """ Initialize internal tracking for the unique classes in the `by` variable. Parameters ---------- by : np.ndarray A 1D array of the categorical/grouping values found in the data. """ self._by_classes = np.unique(by) self._by_int_map = {c: i for i, c in enumerate(self._by_classes)}
[docs] def add_constraint(self, *constraints): """ Add one or more shape constraints to the spline. Not all splines can accept all constraints. This function validates the compatibility of the constraints with the current spline type. Parameters ---------- *constraints : Constraint One or more Constraint objects to apply (e.g., Monotonic, Convex). Returns ------- Spline Returns the spline instance to allow methodical chaining. Raises ------ ValueError If a given constraint is incompatible with this spline type. """ from ..constraints import Monotonic, Convex, Concave, Bound from .cyclic_spline import CyclicSpline from .factor import Factor from .linear import Linear from .piecewise_linear import PiecewiseLinear for c in constraints: if isinstance(self, (CyclicSpline, Factor)): if isinstance(c, (Monotonic, Convex, Concave)): raise ValueError(f"{type(self).__name__} cannot accept {type(c).__name__} constraint.") if isinstance(self, (Linear)): if isinstance(c, (Convex, Concave)): raise ValueError(f"{type(self).__name__} cannot accept {type(c).__name__} constraint.") self._constraints.append(c) return self
[docs] def add_penalty(self, *penalties): """ Add one or more regularization penalties to the spline coefficients. Parameters ---------- *penalties : Penalty One or more Penalty objects to apply (e.g., Ridge, Lasso). Returns ------- Spline Returns the spline instance to allow methodical chaining. Raises ------ TypeError If the supplied argument is not a Penalty instance. """ from ..penalties import Penalty for p in penalties: if not isinstance(p, Penalty): raise TypeError(f"Expected a Penalty instance, got {type(p).__name__}") self._penalties.append(p) return self
@abc.abstractmethod def _build_basis(self, x: np.ndarray, **kwargs) -> np.ndarray: """ Builds the basis matrix evaluated at the input features. Parameters ---------- x : np.ndarray The 1D input feature array. **kwargs : dict Additional arguments for basis construction. Returns ------- np.ndarray A 2D numpy array of shape `(n_samples, n_basis_funcs)`. """ pass @abc.abstractmethod def _build_variables(self) -> cp.Variable: """ Returns the CVXPY variables associated with this spline. Returns ------- cp.Variable The CVXPY variables matrix or vector used for convex optimization. """ pass def _build_one_hot_matrix(self, by: np.ndarray) -> np.ndarray: """ Returns a one-hot encoded matrix for the given array of categorical values based on `_by_classes`. Parameters ---------- by : np.ndarray A 1D array of group identifiers (strings or integers). Returns ------- np.ndarray A 2D binary numpy array of shape `(n_samples, n_classes)`. """ if getattr(self, '_by_int_map', None) is not None: by_mapped = np.array([self._by_int_map.get(v, -1) for v in by]) else: by_mapped = np.array(by).astype(int) n = len(by_mapped) out = np.zeros((n, len(self._by_classes))) mask = (by_mapped >= 0) & (by_mapped < len(self._by_classes)) out[np.arange(n)[mask], by_mapped[mask]] = 1.0 return out def __call__(self, x: np.ndarray, by: np.ndarray = None) -> cp.Expression: """ Evaluates the symbolic CVXPY spline expression for the given input `x`. Parameters ---------- x : np.ndarray The 1D input feature array for the spline evaluation. by : np.ndarray, default=None The 1D integer-encoded grouping array, if the `by` argument is specified. Returns ------- cp.Expression A CVXPY Expression representing the spline values for optimization. Raises ------ ValueError If no CVXPY variables are defined for the spline prior to evaluation. """ variables = self._build_variables() if not variables: raise ValueError("No variables defined for this spline.") basis = self._build_basis(x) if by is None: return basis @ variables else: onehotby = self._build_one_hot_matrix(by=by) out = basis @ variables out = cp.multiply(out, onehotby) out = cp.sum(out, axis=1) return out
[docs] def eval(self, x: np.ndarray, return_basis: bool = False, by: np.ndarray = None) -> np.ndarray: """ Evaluates the fitted numeric spline values for the given input `x`. Parameters ---------- x : np.ndarray The 1D input feature array to evaluate the spline on. return_basis : bool, default=False Whether to return the raw basis matrix instead of the evaluated spline. by : np.ndarray, default=None The 1D integer-encoded grouping array, if the `by` argument is specified. Returns ------- np.ndarray A 1D numpy array of shape `(n_samples,)` representing the predicted values. Raises ------ AssertionError If the spline has not been fitted and coefficients are not available. """ assert self.coefficients is not None, "Spline has not been fitted." basis = self._build_basis(x) if by is None: return basis @ self.coefficients else: onehotby = self._build_one_hot_matrix(by=by) out = basis @ self.coefficients out = np.multiply(out, onehotby) out = np.sum(out, axis=1) return out
def __add__(self, other): """ Implements addition to allow combining Splines into an `LpRegressor` model. Parameters ---------- other : Spline or LpRegressor The other component to combine with this spline. Returns ------- LpRegressor An LpRegressor model combining the terms: - `Spline + Spline -> LpRegressor` - `Spline + LpRegressor -> LpRegressor` Raises ------ TypeError If the object being added is not a Spline or LpRegressor instance. """ from ..optimizer import LpRegressor if isinstance(other, Spline): return LpRegressor([self, other]) elif isinstance(other, LpRegressor): other.splines.append(self) return other else: raise TypeError(f"Cannot add Spline and {type(other)}") def __pos__(self): """ Unary `+` operator to create an `LpRegressor` with a single spline constraint. Usage: `model = +spline(...)` Returns ------- LpRegressor An initialized regression model containing solely this spline. """ from ..optimizer import LpRegressor return LpRegressor(self) def __repr__(self): return f"{self.__class__.__name__}(term='{self.term}')"