From eb67d473b95c9709c45876c9c8bf0252fe2d1021 Mon Sep 17 00:00:00 2001 From: Gaetan Date: Mon, 22 Sep 2025 10:03:13 +0200 Subject: [PATCH 01/10] base logic and test --- sklearn/feature_selection/_base.py | 19 ++++++++-- sklearn/feature_selection/_rfe.py | 1 + sklearn/feature_selection/tests/test_rfe.py | 40 +++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/sklearn/feature_selection/_base.py b/sklearn/feature_selection/_base.py index 3c12cd035d5c8..02dcc2e25abef 100644 --- a/sklearn/feature_selection/_base.py +++ b/sklearn/feature_selection/_base.py @@ -3,6 +3,7 @@ # Authors: The scikit-learn developers # SPDX-License-Identifier: BSD-3-Clause +import inspect import warnings from abc import ABCMeta, abstractmethod from operator import attrgetter @@ -196,7 +197,9 @@ def get_feature_names_out(self, input_features=None): return input_features[self.get_support()] -def _get_feature_importances(estimator, getter, transform_func=None, norm_order=1): +def _get_feature_importances( + estimator, getter, feature_indices=None, transform_func=None, norm_order=1 +): """ Retrieve and aggregate (ndim > 1) the feature importances from an estimator. Also optionally applies transformation. @@ -215,6 +218,14 @@ def _get_feature_importances(estimator, getter, transform_func=None, norm_order= The transform to apply to the feature importances. By default (`None`) no transformation is applied. + feature_indices : ndarray of shape (n_features,), default=None + The indices of features from the full dataset whose importance are currently + evaluated. These are passed to `getter` when it can accept them which allows + using RFE with permutation importance, as shown in this documentation example: + + #TODO: create and link here a documentation example showing how to use + # `permutation_importance` with RFE(CV). + norm_order : int, default=1 The norm order to apply when `transform_func="norm"`. Only applied when `importances.ndim > 1`. @@ -243,7 +254,11 @@ def _get_feature_importances(estimator, getter, transform_func=None, norm_order= elif not callable(getter): raise ValueError("`importance_getter` has to be a string or `callable`") - importances = getter(estimator) + param_names = list(inspect.signature(getter).parameters.keys()) + if "feature_indices" in param_names: + importances = getter(estimator, feature_indices) + else: + importances = getter(estimator) if transform_func is None: return importances diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index 056bb0203b187..07e18664b1c44 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -346,6 +346,7 @@ def _fit(self, X, y, step_score=None, **fit_params): importances = _get_feature_importances( estimator, self.importance_getter, + features, transform_func="square", ) ranks = np.argsort(importances) diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index 1f5672545874c..c707cda566705 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -14,12 +14,13 @@ from sklearn.compose import TransformedTargetRegressor from sklearn.cross_decomposition import CCA, PLSCanonical, PLSRegression from sklearn.datasets import load_iris, make_classification, make_friedman1 -from sklearn.ensemble import RandomForestClassifier +from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor from sklearn.feature_selection import RFE, RFECV from sklearn.impute import SimpleImputer +from sklearn.inspection import permutation_importance from sklearn.linear_model import LinearRegression, LogisticRegression from sklearn.metrics import get_scorer, make_scorer, zero_one_loss -from sklearn.model_selection import GroupKFold, cross_val_score +from sklearn.model_selection import GroupKFold, cross_val_score, train_test_split from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC, SVR, LinearSVR @@ -753,3 +754,38 @@ def test_results_per_cv_in_rfecv(global_random_seed): assert len(rfecv.cv_results_["split1_ranking"]) == len( rfecv.cv_results_["split2_ranking"] ) + + +def test_rfe_with_permutation_importance(global_random_seed): + """ + Ensure that using permutation_importance as a importance_getter returns the right + amount of features. + """ + X, y = make_friedman1(n_samples=100, n_features=7, random_state=global_random_seed) + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.5, random_state=global_random_seed + ) + + reg = RandomForestRegressor(random_state=global_random_seed, n_estimators=2) + + def permutation_importance_getter( + model, feature_indices, X_test, y_test, global_random_seed + ): + return permutation_importance( + model, + X_test[:, feature_indices], + y_test, + n_repeats=2, + random_state=global_random_seed, + ).importances_mean + + rfe = RFE( + estimator=reg, + importance_getter=lambda model, feature_indices: permutation_importance_getter( + model, feature_indices, X_test, y_test, global_random_seed + ), + n_features_to_select=5, + ).fit(X_train, y_train) + + assert rfe.n_features_ == 5 From 55db7fb528c0ce104c88f7bb9f3ce5c76aac45ec Mon Sep 17 00:00:00 2001 From: Gaetan Date: Mon, 22 Sep 2025 16:51:46 +0200 Subject: [PATCH 02/10] dont inspect signature for attrgetter to avoid python3.10 bug --- sklearn/feature_selection/_base.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sklearn/feature_selection/_base.py b/sklearn/feature_selection/_base.py index 02dcc2e25abef..444c7d4be5d0c 100644 --- a/sklearn/feature_selection/_base.py +++ b/sklearn/feature_selection/_base.py @@ -254,11 +254,14 @@ def _get_feature_importances( elif not callable(getter): raise ValueError("`importance_getter` has to be a string or `callable`") - param_names = list(inspect.signature(getter).parameters.keys()) - if "feature_indices" in param_names: - importances = getter(estimator, feature_indices) - else: + if isinstance(getter, attrgetter): importances = getter(estimator) + else: + param_names = list(inspect.signature(getter).parameters.keys()) + if "feature_indices" in param_names: + importances = getter(estimator, feature_indices) + else: + importances = getter(estimator) if transform_func is None: return importances From e9a58bcf0c5f3729840029d0c00eab090e33f2b7 Mon Sep 17 00:00:00 2001 From: Gaetan Date: Thu, 25 Sep 2025 10:21:56 +0200 Subject: [PATCH 03/10] add changelog --- .../sklearn.feature_selection/32251.feature.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst diff --git a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst new file mode 100644 index 0000000000000..903b759a94816 --- /dev/null +++ b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst @@ -0,0 +1,9 @@ +- :class:`feature_selection.RFE` and :class:`feature_selection.RFECV` + now support the use of :func:`permutation_importance` as an :attr:`importance_getter`. + When a callable, and when it can accept it, the :attr:`importance_getter` will be passed + :attr:`feature_indices` along the fitted estimator. + The attribute :attr:`feature_indices` stores the index of the features from the full dataset + that have not been eliminated yet. + This allows methods like :func:`permutation_importance` to extract the relevant features + from its test set. + By :user:`Gaétan de Castellane `. From 5777a4ad0ff5f7b10b3447dd786c09f13baaa73f Mon Sep 17 00:00:00 2001 From: Gaetan Date: Thu, 25 Sep 2025 10:36:31 +0200 Subject: [PATCH 04/10] add test from code review --- sklearn/feature_selection/tests/test_rfe.py | 41 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index c707cda566705..2ea0e258ac52d 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -758,8 +758,8 @@ def test_results_per_cv_in_rfecv(global_random_seed): def test_rfe_with_permutation_importance(global_random_seed): """ - Ensure that using permutation_importance as a importance_getter returns the right - amount of features. + Ensure that using permutation_importance as a importance_getter returns the amount + features set with `n_features_to_select`. """ X, y = make_friedman1(n_samples=100, n_features=7, random_state=global_random_seed) @@ -789,3 +789,40 @@ def permutation_importance_getter( ).fit(X_train, y_train) assert rfe.n_features_ == 5 + + +def test_rfecv_with_permutation_importance(global_random_seed): + """ + Ensure that using permutation_importance as a importance_getter in `RFECV` returns + exactly the five informative features of `make_friedman1`. + """ + X, y = make_friedman1( + n_samples=1_000, n_features=7, random_state=global_random_seed + ) + + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.5, random_state=global_random_seed + ) + + reg = RandomForestRegressor(random_state=global_random_seed, n_estimators=15) + + def permutation_importance_getter( + model, feature_indices, X_test, y_test, global_random_seed + ): + return permutation_importance( + model, + X_test[:, feature_indices], + y_test, + n_repeats=2, + random_state=global_random_seed, + ).importances_mean + + rfe = RFECV( + estimator=reg, + importance_getter=lambda model, feature_indices: permutation_importance_getter( + model, feature_indices, X_test, y_test, global_random_seed + ), + ).fit(X_train, y_train) + + assert rfe.n_features_ == 5 + assert_array_equal(rfe.support_, np.array(([True] * 5) + ([False] * 2))) From f2bf623e3ebcdb012bdc005d052d46376a5c41b0 Mon Sep 17 00:00:00 2001 From: Gaetan Date: Thu, 25 Sep 2025 11:58:28 +0200 Subject: [PATCH 05/10] doc example and docstrings --- .../32251.feature.rst | 8 +- .../plot_rfe_with_cross_validation.py | 75 +++++++++++++++++++ sklearn/feature_selection/_base.py | 6 +- sklearn/feature_selection/_rfe.py | 16 +++- sklearn/feature_selection/tests/test_rfe.py | 14 ++-- 5 files changed, 101 insertions(+), 18 deletions(-) diff --git a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst index 903b759a94816..c156fafc75bd1 100644 --- a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst +++ b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst @@ -1,9 +1,9 @@ - :class:`feature_selection.RFE` and :class:`feature_selection.RFECV` now support the use of :func:`permutation_importance` as an :attr:`importance_getter`. - When a callable, and when it can accept it, the :attr:`importance_getter` will be passed - :attr:`feature_indices` along the fitted estimator. + When a callable, and when it can accept it, the :attr:`importance_getter` is passed + :attr:`feature_indices` along with the fitted estimator. The attribute :attr:`feature_indices` stores the index of the features from the full dataset that have not been eliminated yet. - This allows methods like :func:`permutation_importance` to extract the relevant features - from its test set. + This allows methods that need a test set, like :func:`permutation_importance`, to know which + features of to use in their predictions. By :user:`Gaétan de Castellane `. diff --git a/examples/feature_selection/plot_rfe_with_cross_validation.py b/examples/feature_selection/plot_rfe_with_cross_validation.py index 307707c5aa069..5f39167547518 100644 --- a/examples/feature_selection/plot_rfe_with_cross_validation.py +++ b/examples/feature_selection/plot_rfe_with_cross_validation.py @@ -113,3 +113,78 @@ # In the five folds, the selected features are consistent. This is good news, # it means that the selection is stable across folds, and it confirms that # these features are the most informative ones. + +# %% +# Using `permutation_importance` to select features +# ------------------------------------------------- +# Under the hood, `RFECV` uses importance scores derived from the coefficients of the +# linear model we used to choose which feature to eliminate. We show here how to use +# `permutation_importance` as an alternative way to measure the importance of features. +# For that, we need to feed the `importance_getter` parameter of RFECV a callable +# that accepts a fitted model and an array containing the indices of the features that +# have not been eliminated yet. + +# %% +from sklearn.inspection import permutation_importance + +# Permutation importance need test data to produce reliable importance measures. +X_test, y_test = make_classification( + n_samples=500, + n_features=n_features, + n_informative=3, + n_redundant=2, + n_repeated=0, + n_classes=8, + n_clusters_per_class=1, + class_sep=0.8, + random_state=0, +) + + +# Use `feature_indices` to extract the features that have not been eliminated yet from +# test set. +def permutation_importance_getter(model, feature_indices, X_test, y_test, random_state): + return permutation_importance( + model, + X_test[:, feature_indices], + y_test, + random_state=random_state, + ).importances_mean + + +rfecv = RFECV( + estimator=clf, + step=1, + cv=cv, + scoring="accuracy", + min_features_to_select=min_features_to_select, + n_jobs=2, + importance_getter=lambda model, feature_indices: permutation_importance_getter( + model, feature_indices, X_test, y_test, random_state=0 + ), +) +rfecv.fit(X, y) + +print(f"Optimal number of features: {rfecv.n_features_}") + +# %% +data = { + key: value + for key, value in rfecv.cv_results_.items() + if key in ["n_features", "mean_test_score", "std_test_score"] +} +cv_results = pd.DataFrame(data) +plt.figure() +plt.xlabel("Number of features selected") +plt.ylabel("Mean test accuracy") +plt.errorbar( + x=cv_results["n_features"], + y=cv_results["mean_test_score"], + yerr=cv_results["std_test_score"], +) +plt.title("Recursive Feature Elimination \nwith correlated features") +plt.show() + +# %% +# We see that we obtain very similar results with this model agnostic feature importance +# method. diff --git a/sklearn/feature_selection/_base.py b/sklearn/feature_selection/_base.py index 444c7d4be5d0c..63eeaf532af4d 100644 --- a/sklearn/feature_selection/_base.py +++ b/sklearn/feature_selection/_base.py @@ -220,11 +220,7 @@ def _get_feature_importances( feature_indices : ndarray of shape (n_features,), default=None The indices of features from the full dataset whose importance are currently - evaluated. These are passed to `getter` when it can accept them which allows - using RFE with permutation importance, as shown in this documentation example: - - #TODO: create and link here a documentation example showing how to use - # `permutation_importance` with RFE(CV). + evaluated. These are passed to `getter` when it can accept them. norm_order : int, default=1 The norm order to apply when `transform_func="norm"`. Only applied diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index 07e18664b1c44..f74e3495649a7 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -124,7 +124,13 @@ class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator): If `callable`, overrides the default feature importance getter. The callable is passed with the fitted estimator and it should - return importance for each feature. + return importance for each feature. When it accepts it, the callable is passed + `feature_indices` which stores the index of the features in the full dataset + that have not been eliminated yet. + + `feature_indices` allows RFE to be used with permutation importance, as + shown on RFECV at the end of + :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`. .. versionadded:: 0.24 @@ -647,7 +653,13 @@ class RFECV(RFE): If `callable`, overrides the default feature importance getter. The callable is passed with the fitted estimator and it should - return importance for each feature. + return importance for each feature. When it accepts it, the callable is passed + `feature_indices` which stores the index of the features in the full dataset + that have not been eliminated yet. + + `feature_indices` allows RFECV to be used with permutation importance, as + shown at the end of + :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`. .. versionadded:: 0.24 diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index 2ea0e258ac52d..d5a5dd3cf9c99 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -770,14 +770,14 @@ def test_rfe_with_permutation_importance(global_random_seed): reg = RandomForestRegressor(random_state=global_random_seed, n_estimators=2) def permutation_importance_getter( - model, feature_indices, X_test, y_test, global_random_seed + model, feature_indices, X_test, y_test, random_state ): return permutation_importance( model, X_test[:, feature_indices], y_test, n_repeats=2, - random_state=global_random_seed, + random_state=random_state, ).importances_mean rfe = RFE( @@ -807,22 +807,22 @@ def test_rfecv_with_permutation_importance(global_random_seed): reg = RandomForestRegressor(random_state=global_random_seed, n_estimators=15) def permutation_importance_getter( - model, feature_indices, X_test, y_test, global_random_seed + model, feature_indices, X_test, y_test, random_state ): return permutation_importance( model, X_test[:, feature_indices], y_test, n_repeats=2, - random_state=global_random_seed, + random_state=random_state, ).importances_mean - rfe = RFECV( + rfecv = RFECV( estimator=reg, importance_getter=lambda model, feature_indices: permutation_importance_getter( model, feature_indices, X_test, y_test, global_random_seed ), ).fit(X_train, y_train) - assert rfe.n_features_ == 5 - assert_array_equal(rfe.support_, np.array(([True] * 5) + ([False] * 2))) + assert rfecv.n_features_ == 5 + assert_array_equal(rfecv.support_, np.array(([True] * 5) + ([False] * 2))) From 701e6a6ec6442935f3c92671f2d908ca66470b5c Mon Sep 17 00:00:00 2001 From: Gaetan Date: Thu, 25 Sep 2025 15:01:59 +0200 Subject: [PATCH 06/10] typo and reformulation --- .../plot_rfe_with_cross_validation.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/feature_selection/plot_rfe_with_cross_validation.py b/examples/feature_selection/plot_rfe_with_cross_validation.py index 5f39167547518..0b1eff3ee464d 100644 --- a/examples/feature_selection/plot_rfe_with_cross_validation.py +++ b/examples/feature_selection/plot_rfe_with_cross_validation.py @@ -118,16 +118,16 @@ # Using `permutation_importance` to select features # ------------------------------------------------- # Under the hood, `RFECV` uses importance scores derived from the coefficients of the -# linear model we used to choose which feature to eliminate. We show here how to use +# linear model we used, to choose which feature to eliminate. We show here how to use # `permutation_importance` as an alternative way to measure the importance of features. -# For that, we need to feed the `importance_getter` parameter of RFECV a callable -# that accepts a fitted model and an array containing the indices of the features that -# have not been eliminated yet. +# For that, we use a callable in the `importance_getter` parameter of RFECV. +# This callable accepts a fitted model and an array containing the indices of the +# features that have not been eliminated yet. # %% from sklearn.inspection import permutation_importance -# Permutation importance need test data to produce reliable importance measures. +# Permutation importance needs test data to produce reliable importance measures. X_test, y_test = make_classification( n_samples=500, n_features=n_features, @@ -141,8 +141,8 @@ ) -# Use `feature_indices` to extract the features that have not been eliminated yet from -# test set. +# Use `feature_indices` to extract from the test set the features that have not been +# eliminated yet. def permutation_importance_getter(model, feature_indices, X_test, y_test, random_state): return permutation_importance( model, From a7897d32038028c09761a1b8611e3b86ca4dd998 Mon Sep 17 00:00:00 2001 From: Gaetan Date: Thu, 25 Sep 2025 18:33:24 +0200 Subject: [PATCH 07/10] add missing backtick and colon --- sklearn/feature_selection/_rfe.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index f74e3495649a7..a1aa088c4651a 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -120,7 +120,7 @@ class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator): For example, give `regressor_.coef_` in case of :class:`~sklearn.compose.TransformedTargetRegressor` or `named_steps.clf.feature_importances_` in case of - class:`~sklearn.pipeline.Pipeline` with its last step named `clf`. + :class:`~sklearn.pipeline.Pipeline` with its last step named `clf`. If `callable`, overrides the default feature importance getter. The callable is passed with the fitted estimator and it should @@ -128,8 +128,8 @@ class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator): `feature_indices` which stores the index of the features in the full dataset that have not been eliminated yet. - `feature_indices` allows RFE to be used with permutation importance, as - shown on RFECV at the end of + `feature_indices` allows `RFE` to be used with permutation importance, as + shown on `RFECV` at the end of :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`. .. versionadded:: 0.24 @@ -657,7 +657,7 @@ class RFECV(RFE): `feature_indices` which stores the index of the features in the full dataset that have not been eliminated yet. - `feature_indices` allows RFECV to be used with permutation importance, as + `feature_indices` allows `RFECV` to be used with permutation importance, as shown at the end of :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`. From db66df65daef9aca83c1ccc469670a9c1e09414d Mon Sep 17 00:00:00 2001 From: Gaetan Date: Fri, 26 Sep 2025 09:42:42 +0200 Subject: [PATCH 08/10] typo --- .../sklearn.feature_selection/32251.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst index c156fafc75bd1..509ed1a81d1e7 100644 --- a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst +++ b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst @@ -5,5 +5,5 @@ The attribute :attr:`feature_indices` stores the index of the features from the full dataset that have not been eliminated yet. This allows methods that need a test set, like :func:`permutation_importance`, to know which - features of to use in their predictions. + features to use in their predictions. By :user:`Gaétan de Castellane `. From 38549548e1332c8b9942d5d53788ce9832f8720c Mon Sep 17 00:00:00 2001 From: Gaetan Date: Fri, 26 Sep 2025 15:54:07 +0200 Subject: [PATCH 09/10] improve docstring and fix example --- .../plot_rfe_with_cross_validation.py | 4 +++- sklearn/feature_selection/_rfe.py | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/examples/feature_selection/plot_rfe_with_cross_validation.py b/examples/feature_selection/plot_rfe_with_cross_validation.py index 0b1eff3ee464d..78c38caf78c01 100644 --- a/examples/feature_selection/plot_rfe_with_cross_validation.py +++ b/examples/feature_selection/plot_rfe_with_cross_validation.py @@ -137,7 +137,7 @@ n_classes=8, n_clusters_per_class=1, class_sep=0.8, - random_state=0, + random_state=1, ) @@ -148,6 +148,8 @@ def permutation_importance_getter(model, feature_indices, X_test, y_test, random model, X_test[:, feature_indices], y_test, + n_repeats=10, + n_jobs=2, random_state=random_state, ).importances_mean diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index a1aa088c4651a..cbd9a745ec8b1 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -124,9 +124,9 @@ class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator): If `callable`, overrides the default feature importance getter. The callable is passed with the fitted estimator and it should - return importance for each feature. When it accepts it, the callable is passed + return importance for each feature. When it accepts it, the callable is passed `feature_indices` which stores the index of the features in the full dataset - that have not been eliminated yet. + that have not yet been eliminated in previous iterations. `feature_indices` allows `RFE` to be used with permutation importance, as shown on `RFECV` at the end of @@ -134,6 +134,10 @@ class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator): .. versionadded:: 0.24 + .. versionchanged:: 1.8 + Add support for passing `feature_indices` to the callable when part of its + signature. + Attributes ---------- classes_ : ndarray of shape (n_classes,) @@ -655,14 +659,18 @@ class RFECV(RFE): The callable is passed with the fitted estimator and it should return importance for each feature. When it accepts it, the callable is passed `feature_indices` which stores the index of the features in the full dataset - that have not been eliminated yet. + that have not yet been eliminated in previous iterations. - `feature_indices` allows `RFECV` to be used with permutation importance, as - shown at the end of + `feature_indices` allows `RFE` to be used with permutation importance, as + shown on `RFECV` at the end of :ref:`sphx_glr_auto_examples_feature_selection_plot_rfe_with_cross_validation.py`. .. versionadded:: 0.24 + .. versionchanged:: 1.8 + Add support for passing `feature_indices` to the callable when part of its + signature. + Attributes ---------- classes_ : ndarray of shape (n_classes,) From a6ed7216f9b52b2dc0e85d851a1050536ef7a93e Mon Sep 17 00:00:00 2001 From: Gaetan Date: Fri, 26 Sep 2025 17:49:33 +0200 Subject: [PATCH 10/10] improve documentation --- .../32251.feature.rst | 2 +- .../plot_rfe_with_cross_validation.py | 33 ++++++++----------- sklearn/feature_selection/_rfe.py | 12 +++---- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst index 509ed1a81d1e7..7fb6e8806ac51 100644 --- a/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst +++ b/doc/whats_new/upcoming_changes/sklearn.feature_selection/32251.feature.rst @@ -1,6 +1,6 @@ - :class:`feature_selection.RFE` and :class:`feature_selection.RFECV` now support the use of :func:`permutation_importance` as an :attr:`importance_getter`. - When a callable, and when it can accept it, the :attr:`importance_getter` is passed + When a callable, and when possible, the :attr:`importance_getter` is passed :attr:`feature_indices` along with the fitted estimator. The attribute :attr:`feature_indices` stores the index of the features from the full dataset that have not been eliminated yet. diff --git a/examples/feature_selection/plot_rfe_with_cross_validation.py b/examples/feature_selection/plot_rfe_with_cross_validation.py index 78c38caf78c01..13e92598a0116 100644 --- a/examples/feature_selection/plot_rfe_with_cross_validation.py +++ b/examples/feature_selection/plot_rfe_with_cross_validation.py @@ -21,12 +21,13 @@ # features are non-informative as they are drawn at random. from sklearn.datasets import make_classification +from sklearn.model_selection import train_test_split n_features = 15 feat_names = [f"feature_{i}" for i in range(15)] X, y = make_classification( - n_samples=500, + n_samples=1_000, n_features=n_features, n_informative=3, n_redundant=2, @@ -36,6 +37,9 @@ class_sep=0.8, random_state=0, ) +X_train, X_test, y_train, y_test = train_test_split( + X, y, train_size=0.5, shuffle=False, random_state=0 +) # %% # Model training and selection @@ -60,7 +64,7 @@ min_features_to_select=min_features_to_select, n_jobs=2, ) -rfecv.fit(X, y) +rfecv.fit(X_train, y_train) print(f"Optimal number of features: {rfecv.n_features_}") @@ -117,29 +121,18 @@ # %% # Using `permutation_importance` to select features # ------------------------------------------------- -# Under the hood, `RFECV` uses importance scores derived from the coefficients of the -# linear model we used, to choose which feature to eliminate. We show here how to use -# `permutation_importance` as an alternative way to measure the importance of features. -# For that, we use a callable in the `importance_getter` parameter of RFECV. +# The `importance_getter` parameter in RFE and RFECV uses by default the `coef_` (e.g. +# in linear models) or the `feature_importances_` attributes of an estimator to derive +# feature importance. These importance measures are used to choose which features to +# eliminate first. +# +# We show here how to use a callable to compute the `permutation_importance` instead. # This callable accepts a fitted model and an array containing the indices of the -# features that have not been eliminated yet. +# features that remain after elimination. # %% from sklearn.inspection import permutation_importance -# Permutation importance needs test data to produce reliable importance measures. -X_test, y_test = make_classification( - n_samples=500, - n_features=n_features, - n_informative=3, - n_redundant=2, - n_repeated=0, - n_classes=8, - n_clusters_per_class=1, - class_sep=0.8, - random_state=1, -) - # Use `feature_indices` to extract from the test set the features that have not been # eliminated yet. diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index cbd9a745ec8b1..053ff76e67840 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -124,9 +124,9 @@ class RFE(SelectorMixin, MetaEstimatorMixin, BaseEstimator): If `callable`, overrides the default feature importance getter. The callable is passed with the fitted estimator and it should - return importance for each feature. When it accepts it, the callable is passed - `feature_indices` which stores the index of the features in the full dataset - that have not yet been eliminated in previous iterations. + return importance for each feature. When the callable also accepts + `feature_indices` in its signature, it will be passed the index of the features + of the full dataset that remain after elimination in previous iterations. `feature_indices` allows `RFE` to be used with permutation importance, as shown on `RFECV` at the end of @@ -657,9 +657,9 @@ class RFECV(RFE): If `callable`, overrides the default feature importance getter. The callable is passed with the fitted estimator and it should - return importance for each feature. When it accepts it, the callable is passed - `feature_indices` which stores the index of the features in the full dataset - that have not yet been eliminated in previous iterations. + return importance for each feature. When the callable also accepts + `feature_indices` in its signature, it will be passed the index of the features + of the full dataset that remain after elimination in previous iterations. `feature_indices` allows `RFE` to be used with permutation importance, as shown on `RFECV` at the end of