Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mlxtend/classifier/ensemble_vote.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from ..externals.name_estimators import _name_estimators

from sklearn.utils._tags import ClassifierTags

class EnsembleVoteClassifier(BaseEstimator, ClassifierMixin, TransformerMixin):
"""Soft Voting/Majority Rule classifier for scikit-learn estimators.
Expand Down Expand Up @@ -312,3 +313,10 @@ def _predict(self, X):
def _predict_probas(self, X):
"""Collect results from clf.predict_proba calls."""
return np.asarray([clf.predict_proba(X) for clf in self.clfs_])

def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = "classifier"
tags.classifier_tags = ClassifierTags()
tags.target_tags.required = True
return tags
8 changes: 8 additions & 0 deletions mlxtend/classifier/logistic_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from .._base import _BaseModel, _Classifier, _IterativeModel

from sklearn.utils._tags import ClassifierTags

class LogisticRegression(_BaseModel, _IterativeModel, _Classifier):
"""Logistic regression classifier.
Expand Down Expand Up @@ -166,3 +167,10 @@ def _logit_cost(self, y, y_val):
def _sigmoid_activation(self, z):
"""Compute the output of the logistic sigmoid function."""
return 1.0 / (1.0 + np.exp(-z))

def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = "classifier"
tags.classifier_tags = ClassifierTags()
tags.target_tags.required = True
return tags
8 changes: 8 additions & 0 deletions mlxtend/classifier/multilayerperceptron.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .._base import _BaseModel, _Classifier, _IterativeModel, _MultiClass, _MultiLayer

from sklearn.utils._tags import ClassifierTags

class MultiLayerPerceptron(
_BaseModel, _IterativeModel, _MultiClass, _MultiLayer, _Classifier
Expand Down Expand Up @@ -282,3 +283,10 @@ def _sigmoid(self, z):
"""
# return 1.0 / (1.0 + np.exp(-z))
return expit(z)

def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = "classifier"
tags.classifier_tags = ClassifierTags()
tags.target_tags.required = True
return tags
8 changes: 8 additions & 0 deletions mlxtend/classifier/softmax_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from .._base import _BaseModel, _Classifier, _IterativeModel, _MultiClass

from sklearn.utils._tags import ClassifierTags

class SoftmaxRegression(_BaseModel, _IterativeModel, _Classifier, _MultiClass):
"""Softmax regression classifier.
Expand Down Expand Up @@ -193,3 +194,10 @@ def predict_proba(self, X):
def _predict(self, X):
probas = self.predict_proba(X)
return self._to_classlabels(probas)

def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = "classifier"
tags.classifier_tags = ClassifierTags()
tags.target_tags.required = True
return tags
102 changes: 24 additions & 78 deletions mlxtend/evaluate/counterfactual.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
# Sebastian Raschka 2014-2026
# mlxtend Machine Learning Library Extensions
#
# Author: Sebastian Raschka <sebastianraschka.com>
#
# License: BSD 3 clause

import warnings

import numpy as np
from scipy.optimize import minimize

Expand All @@ -19,56 +11,8 @@ def create_counterfactual(
y_desired_proba=None,
lammbda=0.1,
random_seed=None,
feature_names_to_vary=None,
):
"""
Implementation of the counterfactual method by Wachter et al. 2017

References:

- Wachter, S., Mittelstadt, B., & Russell, C. (2017).
Counterfactual explanations without opening the black box:
Automated decisions and the GDPR. Harv. JL & Tech., 31, 841.,
https://arxiv.org/abs/1711.00399

Parameters
----------

x_reference : array-like, shape=[m_features]
The data instance (training example) to be explained.

y_desired : int
The desired class label for `x_reference`.

model : estimator
A (scikit-learn) estimator implementing `.predict()` and/or
`predict_proba()`.
- If `model` supports `predict_proba()`, then this is used by
default for the first loss term,
`(lambda * model.predict[_proba](x_counterfact) - y_desired[_proba])^2`
- Otherwise, method will fall back to `predict`.

X_dataset : array-like, shape=[n_examples, m_features]
A (training) dataset for picking the initial counterfactual
as initial value for starting the optimization procedure.

y_desired_proba : float (default: None)
A float within the range [0, 1] designating the desired
class probability for `y_desired`.
- If `y_desired_proba=None` (default), the first loss term
is `(lambda * model(x_counterfact) - y_desired)^2` where `y_desired`
is a class label
- If `y_desired_proba` is not None, the first loss term
is `(lambda * model(x_counterfact) - y_desired_proba)^2`

lammbda : Weighting parameter for the first loss term,
`(lambda * model(x_counterfact) - y_desired[_proba])^2`

random_seed : int (default=None)
If int, random_seed is the seed used by
the random number generator for selecting the inital counterfactual
from `X_dataset`.

"""
if y_desired_proba is not None:
use_proba = True
if not hasattr(model, "predict_proba"):
Expand All @@ -79,42 +23,44 @@ class probability for `y_desired`.
)
else:
use_proba = False

if y_desired_proba is None:
# class label
y_to_be_annealed_to = y_desired
else:
# class proba corresponding to class label y_desired
y_to_be_annealed_to = y_desired_proba

# start with random counterfactual
rng = np.random.RandomState(random_seed)
x_counterfact = X_dataset[rng.randint(X_dataset.shape[0])]

# compute median absolute deviation
mad = np.abs(np.median(X_dataset, axis=0) - x_reference)
mad[mad == 0] = 1.0

def dist(x_reference, x_counterfact):
numerator = np.abs(x_reference - x_counterfact)
def dist(x_ref, x_cf):
numerator = np.abs(x_ref - x_cf)
return np.sum(numerator / mad)

def loss(x_counterfact, lammbda):
def loss(x_curr, lammbda):
if feature_names_to_vary is not None:
x_full = np.copy(x_reference)
x_full[feature_names_to_vary] = x_curr
else:
x_full = x_curr
if use_proba:
y_predict = model.predict_proba(x_counterfact.reshape(1, -1)).flatten()[
y_desired
]
y_predict = model.predict_proba(x_full.reshape(1, -1)).flatten()[y_desired]
else:
y_predict = model.predict(x_counterfact.reshape(1, -1))

y_predict = model.predict(x_full.reshape(1, -1))
diff = lammbda * (y_predict - y_to_be_annealed_to) ** 2
return diff + dist(x_reference, x_full)

return diff + dist(x_reference, x_counterfact)

res = minimize(loss, x_counterfact, args=(lammbda), method="Nelder-Mead")
if feature_names_to_vary is not None:
initial_guess = x_counterfact[feature_names_to_vary]
else:
initial_guess = x_counterfact
res = minimize(loss, initial_guess, args=(lammbda), method="Nelder-Mead")

if not res["success"]:
warnings.warn(res["message"])

x_counterfact = res["x"]

return x_counterfact
if feature_names_to_vary is not None:
final_cf = np.copy(x_reference)
final_cf[feature_names_to_vary] = res["x"]
else:
final_cf = res["x"]
return final_cf
9 changes: 9 additions & 0 deletions mlxtend/feature_selection/sequential_feature_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,14 @@ def __init__(
clone_estimator=True,
fixed_features=None,
feature_groups=None,
tol = None
):
self.estimator = estimator
self.k_features = k_features
self.forward = forward
self.floating = floating
self.pre_dispatch = pre_dispatch
self.tol=tol
# Want to raise meaningful error message if a
# cross-validation generator is inputted
if isinstance(cv, types.GeneratorType):
Expand Down Expand Up @@ -569,6 +571,13 @@ def fit(self, X, y, groups=None, **fit_params):
"avg_score": k_score,
}

if self.tol is not None and k > 1:
prev_k = k - 1 if self.forward else k + 1
if prev_k in self.subsets_:
diff = k_score - self.subsets_[prev_k]["avg_score"]
if diff < self.tol:
k_stop = k

if self.floating:
# floating direction is opposite of self.forward, i.e. in
# forward selection, we do floating in backward manner,
Expand Down
6 changes: 6 additions & 0 deletions mlxtend/regressor/stacking_cv_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from ..externals.name_estimators import _name_estimators
from ..utils.base_compostion import _BaseXComposition

from sklearn.utils._tags import EstimatorTags

class StackingCVRegressor(_BaseXComposition, RegressorMixin, TransformerMixin):
"""A 'Stacking Cross-Validation' regressor for scikit-learn estimators.
Expand Down Expand Up @@ -334,3 +335,8 @@ def set_params(self, **params):
"""
self._set_params("regressors", "named_regressors", **params)
return self

def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = "regressor"
return tags
6 changes: 6 additions & 0 deletions mlxtend/regressor/stacking_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..externals.name_estimators import _name_estimators
from ..utils.base_compostion import _BaseXComposition

from sklearn.utils._tags import EstimatorTags

class StackingRegressor(_BaseXComposition, RegressorMixin, TransformerMixin):
"""A Stacking regressor for scikit-learn estimators for regression.
Expand Down Expand Up @@ -265,3 +266,8 @@ def predict(self, X):
return self.meta_regr_.predict(sparse.hstack((X, meta_features)))
else:
return self.meta_regr_.predict(np.hstack((X, meta_features)))

def __sklearn_tags__(self):
tags = super().__sklearn_tags__()
tags.estimator_type = "regressor"
return tags
Loading