Source code for p2p.models.base

import numpy as np
import cvxpy as cp
import pandas as pd
from typing import Optional
from abc import ABC, abstractmethod
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)  # Default level, can be overridden

# Optional: add handler if not already configured by user
if not logger.hasHandlers():
    handler = logging.StreamHandler()
    formatter = logging.Formatter('[%(levelname)s] %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)

[docs] class Model(ABC): def __init__(self, verbose: bool = False): self.verbose = verbose self.beta_ = None self.status_ = None self.feature_names_ = None self.objective_value_ = None # A constant added to every prediction; 0.0 unless a model fits one. self.intercept_ = 0.0 @abstractmethod def fit(self, A: pd.DataFrame, y: np.ndarray): self.feature_names_ = A.columns.tolist() pass def predict(self, A: pd.DataFrame) -> np.ndarray: if self.beta_ is None: raise ValueError("Model has not been fit yet.") predictions = Model.safe_df_to_numpy(A) @ self.beta_ intercept = getattr(self, "intercept_", 0.0) return predictions + intercept if intercept else predictions
[docs] def score(self, A: pd.DataFrame, y: np.ndarray) -> float: """Return mean squared error.""" y_pred = self.predict(A) return np.mean((y - y_pred) ** 2)
def r2(self, A: pd.DataFrame, y: np.ndarray) -> float: y_pred = self.predict(A) return 1 - np.sum((y - y_pred)**2) / np.sum((y - np.mean(y))**2) def get_params(self) -> dict: return {"verbose": self.verbose} def set_params(self, **params): for key, value in params.items(): setattr(self, key, value) @property def coef_(self) -> np.ndarray: if self.beta_ is None: raise ValueError("Model has not been fit yet.") return self.beta_ @property def is_fitted(self) -> bool: return self.beta_ is not None @property def coef_dict_(self) -> dict: if not self.is_fitted or self.feature_names_ is None: raise ValueError("Model must be fit before accessing coef_dict_.") return dict(zip(self.feature_names_, self.beta_)) @property def num_nonzero_(self) -> int: return int(np.sum(self.coef_ != 0))
[docs] @staticmethod def safe_df_to_numpy(df: pd.DataFrame) -> np.ndarray: """ Safely converts a DataFrame to a float64 NumPy array after ensuring all values are numeric. Args: df: Pandas DataFrame to convert. Returns: A NumPy ndarray of float64 values. Raises: ValueError if any value cannot be converted or if NaNs are found. """ df_clean = df.apply(pd.to_numeric, errors='raise') # will raise if non-numeric arr = df_clean.to_numpy(dtype=np.float64) if np.isnan(arr).any(): raise ValueError("DataFrame contains NaNs after conversion.") return arr
def log_fit_summary(self): if self.verbose: logger.info(f"Fit status: {self.status_}") logger.info(f"Objective value: {self.objective_value_:.4f}") logger.info(f"Num nonzero coefficients: {np.sum(self.beta_ != 0)}") def to_dict(self): if not self.is_fitted: raise ValueError("Model has not been fit yet.") return { "beta": self.beta_.tolist(), "feature_names": self.feature_names_, "coef_dict": self.coef_dict_, "status": self.status_, "objective": self.objective_value_ }