Source code for machinelens.core.model_interface

"""Core model interface module for the MachineLens library."""

from __future__ import annotations

from typing import Any, Dict, Optional, Union, cast

import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, is_classifier, is_clusterer, is_regressor
from sklearn.exceptions import NotFittedError
from sklearn.utils.validation import check_consistent_length, check_is_fitted

PandasLike = Union[pd.DataFrame, pd.Series]
ArrayLike = Union[np.ndarray, pd.Series]

ModelResults = Dict[str, Union[str, Dict[str, Any]]]


[docs] class ModelInterface: """Encapsulate model data and provide validation utilities. This class standardizes how the MachineLens library interacts with scikit-learn compatible models. It wraps a fitted estimator alongside its training/test splits and predictions, automatically validating inputs and classifying the model's task type and algorithm family. Attributes ---------- model : BaseEstimator The wrapped scikit-learn estimator. X_train : PandasLike or None Training feature matrix. X_test : PandasLike or None Test feature matrix. y_train : ArrayLike or None Training target values. y_test : ArrayLike or None Test target values (ground truth). y_pred : ArrayLike or None Model predictions on the test set. problem_type : str Detected problem type: ``"classification"``, ``"regression"``, ``"clustering"``, or ``"unknown"``. results : ModelResults Dictionary with model metadata and analysis results. """ results: ModelResults problem_type: str def __init__( self, model: BaseEstimator, X_train: Optional[PandasLike] = None, X_test: Optional[PandasLike] = None, y_train: Optional[ArrayLike] = None, y_test: Optional[ArrayLike] = None, y_pred: Optional[ArrayLike] = None, ) -> None: """Initialize the ModelInterface with model, data splits, and predictions. Parameters ---------- model : BaseEstimator The trained scikit-learn machine learning model object. X_train : pandas.DataFrame or pandas.Series or None The training features. X_test : pandas.DataFrame or pandas.Series or None The test features. y_train : numpy.ndarray or pandas.Series or None The training target values. y_test : numpy.ndarray or pandas.Series or None The ground truth target values for the test set. y_pred : numpy.ndarray or pandas.Series, optional The model's predictions on the test set (X_test). Raises ------ TypeError If the model is not an instance of BaseEstimator. """ if not isinstance(model, BaseEstimator): raise TypeError( "The model must be an instance of sklearn.base.BaseEstimator." ) self.model = model self.X_train = X_train self.X_test = X_test self.y_train = y_train self.y_test = y_test self.y_pred = y_pred self.problem_type = "unknown" self.results: ModelResults = { "Model Name": type(model).__name__, "Task Type": "Other/Unsupervised", "Algorithm Family": "Other/Unknown", "Analysis Results": {}, } self._validate_inputs() self._analyze_and_classify_model() if ( self.y_pred is None and self.X_test is not None and hasattr(self.model, "predict") ): try: self.y_pred = self.model.predict(self.X_test) except Exception: pass # ------------------------------------------------------------------ # Validation # ------------------------------------------------------------------ def _validate_inputs(self) -> None: """Validate shapes, types of inputs, and model fitting. Ensures that: - The model has been fitted. - ``X_train`` and ``X_test`` are pandas DataFrames, Series, or numpy arrays. - The lengths of ``X_test`` and ``y_test`` match. - The lengths of ``X_test`` and ``y_pred`` match when both are provided. Raises ------ ValueError If the model is not fitted or if inputs have inconsistent lengths. TypeError If ``X_train`` or ``X_test`` is not a supported type. """ try: check_is_fitted(self.model) except NotFittedError as e: raise ValueError( f"The provided model is not fitted. Execute .fit() before. Error: {e}" ) from e for name, obj in ( ("X_train", self.X_train), ("X_test", self.X_test), ): if obj is not None and not isinstance( obj, (pd.DataFrame, pd.Series, np.ndarray) ): raise TypeError( f"{name} should be a pandas DataFrame/Series or " f"numpy.ndarray, got {type(obj)}" ) if self.X_test is not None and self.y_test is not None: check_consistent_length(self.X_test, self.y_test) if self.X_test is not None and self.y_pred is not None: check_consistent_length(self.X_test, self.y_pred) # ------------------------------------------------------------------ # Model classification # ------------------------------------------------------------------ def _analyze_and_classify_model(self) -> Dict[str, Any]: """Analyze the model to determine its task type and algorithm family. Extracts the scikit-learn submodule automatically to prevent hard-coding every possible module path. Dynamically discovers available fitted attributes. Returns ------- dict The updated ``self.results`` dictionary. """ model = self.model model_module = type(model).__module__ self.results["Model Name"] = type(model).__name__ analysis_results: Dict[str, Any] = cast( Dict[str, Any], self.results["Analysis Results"] ) # --- 0. Extract Feature Names --- if hasattr(model, "feature_names_in_"): analysis_results["Feature Names"] = model.feature_names_in_.tolist() elif isinstance(self.X_train, pd.DataFrame): analysis_results["Feature Names"] = self.X_train.columns.tolist() # --- 1. Determine the TASK TYPE --- if is_classifier(model): self.problem_type = "classification" self.results["Task Type"] = "Supervised Classification" elif is_regressor(model): self.problem_type = "regression" self.results["Task Type"] = "Supervised Regression" elif is_clusterer(model): self.problem_type = "clustering" self.results["Task Type"] = "Unsupervised Clustering" # --- 2. Determine the ALGORITHM FAMILY --- parts = model_module.split(".") if len(parts) >= 2 and parts[0] == "sklearn": submodule = parts[1] pretty_names = { "ensemble": "Ensemble (Forest/Boosting/Bagging)", "linear_model": "Linear Model (Regression/Logit/Ridge)", "naive_bayes": "Probabilistic (Naive Bayes)", "neighbors": "Instance-based (K-Nearest Neighbors)", "svm": "Support Vector Machines", "tree": "Decision Tree", "dummy": "Baseline (Dummy Estimator)", } self.results["Algorithm Family"] = pretty_names.get( submodule, submodule.replace("_", " ").title() ) else: self.results["Algorithm Family"] = "Other/Unknown" # --- 3. Dynamic Attribute Discovery --- if hasattr(model, "feature_importances_"): analysis_results["Feature Importance Available"] = True if hasattr(model, "coef_"): analysis_results["Coefficients Available"] = True if hasattr(model, "class_prior_"): analysis_results["Class Priors Available"] = True if hasattr(model, "n_clusters"): analysis_results["Expected Attribute"] = "n_clusters" return self.results