Source code for modeva.models.wrappers.builtin.linear_model

from copy import deepcopy

import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression, ElasticNet
from sklearn.preprocessing import OneHotEncoder

from ..sklearn import MoSKLearnRegressor, MoSKLearnClassifier
from ....testsuite.interpret.linear_model.base import InterpretLinearModel
from ....utils.constants import NUMERICAL, CATEGORICAL
from ....utils.helper import limit_function_for_trial_license


def wrapper_func(encoders):
    def transform(data):
        transformed_data = []
        for fn, item in encoders.items():
            feature_names_out = item["feature_names_out"]
            if item["encoder"] is None:
                transformed_data.append(pd.DataFrame(data[:, [item["fidx"]]], columns=feature_names_out))
            else:
                dt = item["encoder"].transform(data[:, [item["fidx"]]]).toarray()
                dt[:, 0] = np.zeros((dt.shape[0],))
                transformed_data.append(pd.DataFrame(dt, columns=feature_names_out))
        return pd.concat(transformed_data, axis=1)

    return transform


class MoLinearModelBase:

    def __init__(self, feature_names=None, feature_types=None):

        self.feature_names = feature_names
        self.feature_types = feature_types

    def _encode_categorical(self, X):

        if self.feature_names is None:
            self.feature_names_ = ["X" + str(idx) for idx in range(self.n_features_in_)]
        else:
            self.feature_names_ = deepcopy(self.feature_names)
        if self.feature_types is None:
            self.feature_types_ = [NUMERICAL for _ in range(self.n_features_in_)]
        else:
            self.feature_types_ = deepcopy(self.feature_types)

        encoders = {}
        categories_list = []
        feature_names_out_list = []
        for fidx, (fn, ft) in enumerate(zip(self.feature_names_, self.feature_types_)):
            if ft == CATEGORICAL:
                encoder = OneHotEncoder(handle_unknown="ignore")
                encoder.fit(pd.DataFrame(X[:, [fidx]], columns=[fn]))
                feature_names_out = encoder.get_feature_names_out().tolist()
                categories = encoder.categories_[0].tolist()
            else:
                encoder = None
                categories = None
                feature_names_out = [fn]
            encoders[fn] = {"fidx": fidx,
                            "encoder": encoder,
                            "feature_names_out": feature_names_out}

            categories_list.append(categories)
            feature_names_out_list.append(feature_names_out)
        return encoders, feature_names_out_list, categories_list

    def predict_effect(self, fidx, X):

        XX = np.zeros_like(X)
        XX[:, fidx] = X[:, fidx]
        enfunc = wrapper_func(self.encoders_)
        return np.dot(enfunc(XX), self.coef_)

    def extract_model_info(self, X):

        idx = 0
        main_effects = {}
        self.main_effect_mean_ = np.zeros((X.shape[1],))
        for fidx, fn in enumerate(self.feature_names_):
            n_categories = len(self.feature_names_out_list_[fidx])
            item = dict()
            item["fidx"] = [fidx]
            item["type"] = self.feature_types_[fidx]
            item["categories"] = self.categories_list_[fidx]
            item["coefs"] = self.coef_[idx: idx + n_categories]
            item["feature_names_out"] = self.feature_names_out_list_[fidx]
            main_effects[fn] = item
            idx += n_categories
            self.main_effect_mean_[fidx] = self.predict_effect(fidx, X).mean()

        self.modeva_effects_ = {"main_effect": main_effects}

    def interpret(self, dataset):

        if not dataset.is_built_in():
            limit_function_for_trial_license()
        self.extract_model_info(X=dataset.train_x)
        return InterpretLinearModel(model=self, dataset=dataset)


[docs] class MoElasticNet(MoSKLearnRegressor, MoLinearModelBase): """ A lightweight wrapper of :class:`sklearn.linear_model.ElasticNet`. Note that categorical features are preprocessed by one-hot encoding in this wrapper. Parameters ---------- name : str, default=None Identifier for the model instance. feature_names : list or None, default=None The list of feature names. feature_types : list or None, default=None The list of feature types. Available types include "numerical" and "categorical". *args Variable length argument list passed to the underlying ElasticNet model. **kwargs Arbitrary keyword arguments passed to the underlying ElasticNet model. """ def __init__(self, name: str = None, feature_names=None, feature_types=None, *args, **kwargs): MoLinearModelBase.__init__(self, feature_names=feature_names, feature_types=feature_types) super().__init__(name=name, estimator=ElasticNet(*args, **kwargs))
[docs] def get_params(self, deep=True): """ Get parameters for this estimator. Parameters ---------- deep : bool, default=True If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns ------- params : dict Parameter names mapped to their values. """ return self.estimator.get_params(deep=deep)
[docs] def fit(self, X, y, sample_weight=None): """ Fits the estimator to the provided data. Parameters ---------- X : array-like, shape (n_samples, n_features) Training data. y : array-like, shape (n_samples,) or (n_samples, n_outputs) Target values. sample_weight : array-like, shape (n_samples,), optional Sample weights. Returns ------- self : object Fitted model instance. """ self.n_features_in_ = X.shape[1] self.encoders_, self.feature_names_out_list_, self.categories_list_ = self._encode_categorical(X) enfunc = wrapper_func(self.encoders_) self.estimator.fit(enfunc(X), y, sample_weight) self.coef_ = self.coef_.ravel() self.intercept_ = self.intercept_.ravel() return self
def _predict(self, X): """ Makes predictions using the fitted model. Parameters ---------- X : array-like Input features. Returns ------- array-like The predicted values. """ enfunc = wrapper_func(self.encoders_) return self.estimator.predict(enfunc(X))
[docs] class MoLogisticRegression(MoSKLearnClassifier, MoLinearModelBase): """ A lightweight wrapper of :class:`sklearn.linear_model.LogisticRegression`. Note that categorical features are preprocessed by one-hot encoding in this wrapper. Parameters ---------- name : str, default=None Identifier for the model instance. feature_names : list or None, default=None The list of feature names. feature_types : list or None, default=None The list of feature types. Available types include "numerical" and "categorical". *args Variable length argument list passed to the underlying LogisticRegression model. **kwargs Arbitrary keyword arguments passed to the underlying LogisticRegression model. """ def __init__(self, name: str = None, feature_names=None, feature_types=None, *args, **kwargs): MoLinearModelBase.__init__(self, feature_names=feature_names, feature_types=feature_types) super().__init__(name=name, estimator=LogisticRegression(*args, **kwargs))
[docs] def get_params(self, deep=True): """ Get parameters for this estimator. Parameters ---------- deep : bool, default=True If True, will return the parameters for this estimator and contained subobjects that are estimators. Returns ------- params : dict Parameter names mapped to their values. """ return self.estimator.get_params(deep=deep)
[docs] def fit(self, X, y, sample_weight=None): """ Fits the estimator to the provided data. Parameters ---------- X : array-like, shape (n_samples, n_features) Training data. y : array-like, shape (n_samples,) or (n_samples, n_outputs) Target values. sample_weight : array-like, shape (n_samples,), optional Sample weights. Returns ------- self : object Fitted model instance. """ self.n_features_in_ = X.shape[1] self.encoders_, self.feature_names_out_list_, self.categories_list_ = self._encode_categorical(X) enfunc = wrapper_func(self.encoders_) self.estimator.fit(enfunc(X), y, sample_weight) self.coef_ = self.coef_.ravel() self.intercept_ = self.intercept_.ravel() return self
def _decision_function(self, X): """ Computes the decision function for the given input data. Parameters ---------- X : array-like, shape (n_samples, n_features) Input data for prediction. Returns ------- logit_prediction : array, shape (n_samples,) Array of logit predictions. """ enfunc = wrapper_func(self.encoders_) return self.estimator.decision_function(enfunc(X)) def _predict_proba(self, X): """ Predicts class probabilities for the given input data. Parameters ---------- X : array-like, shape (n_samples, n_features) Input data for prediction. Returns ------- proba : array, shape (n_samples, n_classes) Array of predicted probabilities for each class. """ enfunc = wrapper_func(self.encoders_) return self.estimator.predict_proba(enfunc(X))