From 0ea382b3f4ffb7afff1a24b89abaac58f8675d0e Mon Sep 17 00:00:00 2001 From: Mariam Zakaria <123750992+mariam851@users.noreply.github.com> Date: Sat, 20 Dec 2025 04:11:15 +0200 Subject: [PATCH 1/3] feat: remove comments --- mlxtend/feature_selection/sequential_feature_selector.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mlxtend/feature_selection/sequential_feature_selector.py b/mlxtend/feature_selection/sequential_feature_selector.py index 835d7a3f9..fe5e7d121 100644 --- a/mlxtend/feature_selection/sequential_feature_selector.py +++ b/mlxtend/feature_selection/sequential_feature_selector.py @@ -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): @@ -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, From ce8c2b88b8fd4579a04c57467db91282237edc83 Mon Sep 17 00:00:00 2001 From: Mariam Zakaria <123750992+mariam851@users.noreply.github.com> Date: Sat, 20 Dec 2025 04:29:16 +0200 Subject: [PATCH 2/3] feat: implement feature freezing logic in counterfactual --- mlxtend/evaluate/counterfactual.py | 102 +++++++---------------------- 1 file changed, 24 insertions(+), 78 deletions(-) diff --git a/mlxtend/evaluate/counterfactual.py b/mlxtend/evaluate/counterfactual.py index 0e3981e91..4ff823d91 100644 --- a/mlxtend/evaluate/counterfactual.py +++ b/mlxtend/evaluate/counterfactual.py @@ -1,12 +1,4 @@ -# Sebastian Raschka 2014-2026 -# mlxtend Machine Learning Library Extensions -# -# Author: Sebastian Raschka -# -# License: BSD 3 clause - import warnings - import numpy as np from scipy.optimize import minimize @@ -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"): @@ -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 From 150f96f448454a9d747b1f6e6bd4ddc439473d93 Mon Sep 17 00:00:00 2001 From: Mariam Zakaria <123750992+mariam851@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:33:27 +0200 Subject: [PATCH 3/3] Fix #1129: Add __sklearn_tags__ to classifiers for scikit-learn 1.6+ compatibility --- mlxtend/classifier/ensemble_vote.py | 8 ++++++++ mlxtend/classifier/logistic_regression.py | 8 ++++++++ mlxtend/classifier/multilayerperceptron.py | 8 ++++++++ mlxtend/classifier/softmax_regression.py | 8 ++++++++ mlxtend/regressor/stacking_cv_regression.py | 6 ++++++ mlxtend/regressor/stacking_regression.py | 6 ++++++ 6 files changed, 44 insertions(+) diff --git a/mlxtend/classifier/ensemble_vote.py b/mlxtend/classifier/ensemble_vote.py index 8e6003c58..da55577ae 100644 --- a/mlxtend/classifier/ensemble_vote.py +++ b/mlxtend/classifier/ensemble_vote.py @@ -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. @@ -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 diff --git a/mlxtend/classifier/logistic_regression.py b/mlxtend/classifier/logistic_regression.py index 217625192..5df615d4b 100644 --- a/mlxtend/classifier/logistic_regression.py +++ b/mlxtend/classifier/logistic_regression.py @@ -12,6 +12,7 @@ from .._base import _BaseModel, _Classifier, _IterativeModel +from sklearn.utils._tags import ClassifierTags class LogisticRegression(_BaseModel, _IterativeModel, _Classifier): """Logistic regression classifier. @@ -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 diff --git a/mlxtend/classifier/multilayerperceptron.py b/mlxtend/classifier/multilayerperceptron.py index 0212e4034..3f0f81965 100644 --- a/mlxtend/classifier/multilayerperceptron.py +++ b/mlxtend/classifier/multilayerperceptron.py @@ -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 @@ -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 diff --git a/mlxtend/classifier/softmax_regression.py b/mlxtend/classifier/softmax_regression.py index 2a6d3ef99..b72f7c8e9 100644 --- a/mlxtend/classifier/softmax_regression.py +++ b/mlxtend/classifier/softmax_regression.py @@ -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. @@ -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 diff --git a/mlxtend/regressor/stacking_cv_regression.py b/mlxtend/regressor/stacking_cv_regression.py index d7939b6b2..63e632eb5 100644 --- a/mlxtend/regressor/stacking_cv_regression.py +++ b/mlxtend/regressor/stacking_cv_regression.py @@ -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. @@ -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 diff --git a/mlxtend/regressor/stacking_regression.py b/mlxtend/regressor/stacking_regression.py index 974cff42d..9ea350878 100644 --- a/mlxtend/regressor/stacking_regression.py +++ b/mlxtend/regressor/stacking_regression.py @@ -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. @@ -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