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))