From c213ea5063776f67a60568714d2a8177721b4004 Mon Sep 17 00:00:00 2001 From: Miguel Silva Date: Sun, 24 Mar 2024 21:31:07 +0000 Subject: [PATCH 1/8] Add n_features to RFECV.cv_results_ --- doc/whats_new/v1.5.rst | 10 ++++- .../plot_rfe_with_cross_validation.py | 9 ++-- sklearn/feature_selection/_rfe.py | 24 ++++++++--- sklearn/feature_selection/tests/test_rfe.py | 43 ++++++++++++++++++- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index bd03cc743f76e..4d41209e1ee8e 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -188,6 +188,14 @@ Changelog :pr:`28085` by :user:`Neto Menoci ` and :user:`Florin Andrei `. +- |Enhancement| :class:`feature_selection.RFE` now have the `n_features_fitted_` + attribute after `_fit`. + :pr:`43745` by :user:`Miguel Silva `. + +- |Feature| :class:`feature_selection.RFECV` adds a new key to its `cv_results_` + `dict`, containing an `np.array` with the number of features fitted at each step. + :pr:`43745` by :user:`Miguel Silva `. + :mod:`sklearn.impute` ..................... @@ -298,7 +306,7 @@ Changelog :func:`preprocessing.quantile_transform` now supports disabling subsampling explicitly. :pr:`27636` by :user:`Ralph Urlus `. - + :mod:`sklearn.tree` ................... diff --git a/examples/feature_selection/plot_rfe_with_cross_validation.py b/examples/feature_selection/plot_rfe_with_cross_validation.py index 693e21fe21787..6e4a8ae0ee8c5 100644 --- a/examples/feature_selection/plot_rfe_with_cross_validation.py +++ b/examples/feature_selection/plot_rfe_with_cross_validation.py @@ -66,15 +66,16 @@ # --------------------------------------------------- import matplotlib.pyplot as plt +import pandas as pd -n_scores = len(rfecv.cv_results_["mean_test_score"]) +cv_results = pd.DataFrame(rfecv.cv_results_) plt.figure() plt.xlabel("Number of features selected") plt.ylabel("Mean test accuracy") plt.errorbar( - range(min_features_to_select, n_scores + min_features_to_select), - rfecv.cv_results_["mean_test_score"], - yerr=rfecv.cv_results_["std_test_score"], + 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() diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index e85dc8f623596..2094a314ed326 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -153,6 +153,9 @@ class RFE(_RoutingNotSupportedMixin, SelectorMixin, MetaEstimatorMixin, BaseEsti support_ : ndarray of shape (n_features,) The mask of selected features. + n_features_fitted_: ndarray of shape (n_features_in_ // step [+1],) + Number of features used for fitting at each step. + See Also -------- RFECV : Recursive feature elimination with built-in cross-validated @@ -294,6 +297,7 @@ def _fit(self, X, y, step_score=None, **fit_params): support_ = np.ones(n_features, dtype=bool) ranking_ = np.ones(n_features, dtype=int) + self.n_features_fitted_ = [] if step_score: self.scores_ = [] @@ -329,6 +333,7 @@ def _fit(self, X, y, step_score=None, **fit_params): # that have not been eliminated yet if step_score: self.scores_.append(step_score(estimator, features)) + self.n_features_fitted_.append(len(features)) support_[features[ranks][:threshold]] = False ranking_[np.logical_not(support_)] += 1 @@ -340,9 +345,11 @@ def _fit(self, X, y, step_score=None, **fit_params): # Compute step score when only n_features_to_select features left if step_score: self.scores_.append(step_score(self.estimator_, features)) + self.n_features_fitted_.append(len(features)) self.n_features_ = support_.sum() self.support_ = support_ self.ranking_ = ranking_ + self.n_features_fitted_ = np.array(self.n_features_fitted_) return self @@ -581,6 +588,9 @@ class RFECV(RFE): std_test_score : ndarray of shape (n_subsets_of_features,) Standard deviation of scores over the folds. + n_features : ndarray of shape (n_subsets_of_features,) + Number of features used at each step. + .. versionadded:: 1.0 n_features_ : int @@ -758,6 +768,7 @@ def fit(self, X, y, groups=None): for train, test in cv.split(X, y, groups) ) + n_features_fitted = rfe.n_features_fitted_ scores = np.array(scores) scores_sum = np.sum(scores, axis=0) scores_sum_rev = scores_sum[::-1] @@ -786,11 +797,10 @@ def fit(self, X, y, groups=None): # reverse to stay consistent with before scores_rev = scores[:, ::-1] - self.cv_results_ = {} - self.cv_results_["mean_test_score"] = np.mean(scores_rev, axis=0) - self.cv_results_["std_test_score"] = np.std(scores_rev, axis=0) - - for i in range(scores.shape[0]): - self.cv_results_[f"split{i}_test_score"] = scores_rev[i] - + self.cv_results_ = { + "mean_test_score": np.mean(scores_rev, axis=0), + "std_test_score": np.std(scores_rev, axis=0), + **{f"split{i}_test_score": scores_rev[i] for i in range(scores.shape[0])}, + "n_features": np.flip(n_features_fitted), + } return self diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index e3edb0e7b5d21..6c9e0e87c96c7 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -11,7 +11,7 @@ from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.compose import TransformedTargetRegressor from sklearn.cross_decomposition import CCA, PLSCanonical, PLSRegression -from sklearn.datasets import load_iris, make_friedman1 +from sklearn.datasets import load_iris, make_classification, make_friedman1 from sklearn.ensemble import RandomForestClassifier from sklearn.feature_selection import RFE, RFECV from sklearn.impute import SimpleImputer @@ -537,7 +537,8 @@ def test_rfecv_std_and_mean(global_random_seed): rfecv = RFECV(estimator=SVC(kernel="linear")) rfecv.fit(X, y) - n_split_keys = len(rfecv.cv_results_) - 2 + n_non_split_keys = 3 + n_split_keys = len(rfecv.cv_results_) - n_non_split_keys split_keys = [f"split{i}_test_score" for i in range(n_split_keys)] cv_scores = np.asarray([rfecv.cv_results_[key] for key in split_keys]) @@ -548,6 +549,44 @@ def test_rfecv_std_and_mean(global_random_seed): assert_allclose(rfecv.cv_results_["std_test_score"], expected_std) +@pytest.mark.parametrize( + ["min_features_to_select", "n_features", "step", "cv_results_n_features"], + [ + [1, 4, 1, np.array([1, 2, 3, 4])], + [1, 5, 1, np.array([1, 2, 3, 4, 5])], + [1, 4, 2, np.array([1, 2, 4])], + [1, 5, 2, np.array([1, 3, 5])], + [1, 4, 3, np.array([1, 4])], + [1, 5, 3, np.array([1, 2, 5])], + [1, 4, 4, np.array([1, 4])], + [1, 5, 4, np.array([1, 5])], + [4, 4, 2, np.array([4])], + [4, 5, 1, np.array([4, 5])], + [4, 5, 2, np.array([4, 5])], + ], +) +def test_rfecv_cv_results_n_features( + min_features_to_select, + n_features, + step, + cv_results_n_features, +): + X, y = make_classification( + n_samples=20, n_features=n_features, n_informative=n_features, n_redundant=0 + ) + rfecv = RFECV( + estimator=SVC(kernel="linear"), + step=step, + min_features_to_select=min_features_to_select, + ) + rfecv.fit(X, y) + assert_array_equal(rfecv.cv_results_["n_features"], cv_results_n_features) + assert all( + len(value) == len(rfecv.cv_results_["n_features"]) + for value in rfecv.cv_results_.values() + ) + + @pytest.mark.parametrize("ClsRFE", [RFE, RFECV]) def test_multioutput(ClsRFE): X = np.random.normal(size=(10, 3)) From 8a57ab3243297f2f31fafd97d68df1a7f467cd62 Mon Sep 17 00:00:00 2001 From: Miguel Silva Date: Mon, 25 Mar 2024 18:03:34 +0000 Subject: [PATCH 2/8] Fix pr in changelog --- doc/whats_new/v1.5.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 4d41209e1ee8e..8d8e3128d969f 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -190,11 +190,11 @@ Changelog - |Enhancement| :class:`feature_selection.RFE` now have the `n_features_fitted_` attribute after `_fit`. - :pr:`43745` by :user:`Miguel Silva `. + :pr:`28670` by :user:`Miguel Silva `. - |Feature| :class:`feature_selection.RFECV` adds a new key to its `cv_results_` `dict`, containing an `np.array` with the number of features fitted at each step. - :pr:`43745` by :user:`Miguel Silva `. + :pr:`28670` by :user:`Miguel Silva `. :mod:`sklearn.impute` ..................... From 2fa460d20902addd72628f732f1867cde3dee10a Mon Sep 17 00:00:00 2001 From: jeremie du boisberranger Date: Tue, 26 Mar 2024 00:02:37 +0100 Subject: [PATCH 3/8] make new attr non public + simplify argmax --- doc/whats_new/v1.5.rst | 9 ++--- sklearn/feature_selection/_rfe.py | 40 +++++++++------------ sklearn/feature_selection/tests/test_rfe.py | 5 +-- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index 4d41209e1ee8e..e79006c3a0840 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -188,12 +188,9 @@ Changelog :pr:`28085` by :user:`Neto Menoci ` and :user:`Florin Andrei `. -- |Enhancement| :class:`feature_selection.RFE` now have the `n_features_fitted_` - attribute after `_fit`. - :pr:`43745` by :user:`Miguel Silva `. - -- |Feature| :class:`feature_selection.RFECV` adds a new key to its `cv_results_` - `dict`, containing an `np.array` with the number of features fitted at each step. +- |Enhancement| The `cv_results_` attribute of :class:`feature_selection.RFECV`has + a new key, `n_features`, containing an array with the number of features selected + at each step. :pr:`43745` by :user:`Miguel Silva `. :mod:`sklearn.impute` diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index 2094a314ed326..bf8dff62dec7a 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -32,7 +32,8 @@ def _rfe_single_fit(rfe, estimator, X, y, train, test, scorer): """ X_train, y_train = _safe_split(estimator, X, y, train) X_test, y_test = _safe_split(estimator, X, y, test, train) - return rfe._fit( + + rfe._fit( X_train, y_train, lambda estimator, features: _score( @@ -43,7 +44,9 @@ def _rfe_single_fit(rfe, estimator, X, y, train, test, scorer): scorer, score_params=None, ), - ).scores_ + ) + + return rfe.scores_, rfe.n_features_selected_ def _estimator_has(attr): @@ -153,9 +156,6 @@ class RFE(_RoutingNotSupportedMixin, SelectorMixin, MetaEstimatorMixin, BaseEsti support_ : ndarray of shape (n_features,) The mask of selected features. - n_features_fitted_: ndarray of shape (n_features_in_ // step [+1],) - Number of features used for fitting at each step. - See Also -------- RFECV : Recursive feature elimination with built-in cross-validated @@ -297,9 +297,9 @@ def _fit(self, X, y, step_score=None, **fit_params): support_ = np.ones(n_features, dtype=bool) ranking_ = np.ones(n_features, dtype=int) - self.n_features_fitted_ = [] if step_score: + self.n_features_selected_ = [] self.scores_ = [] # Elimination @@ -332,8 +332,8 @@ def _fit(self, X, y, step_score=None, **fit_params): # because 'estimator' must use features # that have not been eliminated yet if step_score: + self.n_features_selected_.append(len(features)) self.scores_.append(step_score(estimator, features)) - self.n_features_fitted_.append(len(features)) support_[features[ranks][:threshold]] = False ranking_[np.logical_not(support_)] += 1 @@ -344,12 +344,11 @@ def _fit(self, X, y, step_score=None, **fit_params): # Compute step score when only n_features_to_select features left if step_score: + self.n_features_selected_.append(len(features)) self.scores_.append(step_score(self.estimator_, features)) - self.n_features_fitted_.append(len(features)) self.n_features_ = support_.sum() self.support_ = support_ self.ranking_ = ranking_ - self.n_features_fitted_ = np.array(self.n_features_fitted_) return self @@ -728,12 +727,6 @@ def fit(self, X, y, groups=None): # Initialization cv = check_cv(self.cv, y, classifier=is_classifier(self.estimator)) scorer = check_scoring(self.estimator, scoring=self.scoring) - n_features = X.shape[1] - - if 0.0 < self.step < 1.0: - step = int(max(1, self.step * n_features)) - else: - step = int(self.step) # Build an RFE object, which will evaluate and score each possible # feature count, down to self.min_features_to_select @@ -763,19 +756,18 @@ def fit(self, X, y, groups=None): parallel = Parallel(n_jobs=self.n_jobs) func = delayed(_rfe_single_fit) - scores = parallel( + scores_features = parallel( func(rfe, self.estimator, X, y, train, test, scorer) for train, test in cv.split(X, y, groups) ) + scores, n_features_per_iter = zip(*scores_features) - n_features_fitted = rfe.n_features_fitted_ + n_features_per_iter_rev = np.array(n_features_per_iter[0])[::-1] scores = np.array(scores) - scores_sum = np.sum(scores, axis=0) - scores_sum_rev = scores_sum[::-1] - argmax_idx = len(scores_sum) - np.argmax(scores_sum_rev) - 1 - n_features_to_select = max( - n_features - (argmax_idx * step), self.min_features_to_select - ) + + # Reverse order such that lowest number of features is selected in case of tie. + scores_sum_rev = np.sum(scores, axis=0)[::-1] + n_features_to_select = n_features_per_iter_rev[np.argmax(scores_sum_rev)] # Re-execute an elimination with best_k over the whole set rfe = RFE( @@ -801,6 +793,6 @@ def fit(self, X, y, groups=None): "mean_test_score": np.mean(scores_rev, axis=0), "std_test_score": np.std(scores_rev, axis=0), **{f"split{i}_test_score": scores_rev[i] for i in range(scores.shape[0])}, - "n_features": np.flip(n_features_fitted), + "n_features": n_features_per_iter_rev, } return self diff --git a/sklearn/feature_selection/tests/test_rfe.py b/sklearn/feature_selection/tests/test_rfe.py index 6c9e0e87c96c7..01c6194493ab6 100644 --- a/sklearn/feature_selection/tests/test_rfe.py +++ b/sklearn/feature_selection/tests/test_rfe.py @@ -537,10 +537,7 @@ def test_rfecv_std_and_mean(global_random_seed): rfecv = RFECV(estimator=SVC(kernel="linear")) rfecv.fit(X, y) - n_non_split_keys = 3 - n_split_keys = len(rfecv.cv_results_) - n_non_split_keys - split_keys = [f"split{i}_test_score" for i in range(n_split_keys)] - + split_keys = [key for key in rfecv.cv_results_.keys() if "split" in key] cv_scores = np.asarray([rfecv.cv_results_[key] for key in split_keys]) expected_mean = np.mean(cv_scores, axis=0) expected_std = np.std(cv_scores, axis=0) From 29ff8100c99a59de1a5c73ee00ed43447b7cc09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20du=20Boisberranger?= Date: Tue, 26 Mar 2024 11:13:11 +0100 Subject: [PATCH 4/8] cln --- doc/whats_new/v1.5.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/whats_new/v1.5.rst b/doc/whats_new/v1.5.rst index e487809e7a2b2..32a6fc280d531 100644 --- a/doc/whats_new/v1.5.rst +++ b/doc/whats_new/v1.5.rst @@ -188,7 +188,7 @@ Changelog :pr:`28085` by :user:`Neto Menoci ` and :user:`Florin Andrei `. -- |Enhancement| The `cv_results_` attribute of :class:`feature_selection.RFECV`has +- |Enhancement| The `cv_results_` attribute of :class:`feature_selection.RFECV` has a new key, `n_features`, containing an array with the number of features selected at each step. :pr:`28670` by :user:`Miguel Silva `. From 4fca941eec0748c5c79ff3ea71ae30663211d16f Mon Sep 17 00:00:00 2001 From: jeremie du boisberranger Date: Tue, 26 Mar 2024 11:16:04 +0100 Subject: [PATCH 5/8] add meson-python to requirements --- build_tools/azure/python_nogil_lock.txt | 2 +- build_tools/azure/python_nogil_requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build_tools/azure/python_nogil_lock.txt b/build_tools/azure/python_nogil_lock.txt index 85988263b94b4..9dba84dfb619e 100644 --- a/build_tools/azure/python_nogil_lock.txt +++ b/build_tools/azure/python_nogil_lock.txt @@ -55,7 +55,7 @@ scipy==1.9.3 # via -r /scikit-learn/build_tools/azure/python_nogil_requirements.txt six==1.16.0 # via python-dateutil -threadpoolctl==3.3.0 +threadpoolctl==3.4.0 # via -r /scikit-learn/build_tools/azure/python_nogil_requirements.txt tomli==2.0.1 # via pytest diff --git a/build_tools/azure/python_nogil_requirements.txt b/build_tools/azure/python_nogil_requirements.txt index 970059ede81aa..959b5ace581a8 100644 --- a/build_tools/azure/python_nogil_requirements.txt +++ b/build_tools/azure/python_nogil_requirements.txt @@ -13,3 +13,4 @@ joblib threadpoolctl pytest pytest-xdist +meson-python From b777435116652081c4180bb78457bd665ebc1086 Mon Sep 17 00:00:00 2001 From: Miguel Silva Date: Tue, 26 Mar 2024 21:06:57 +0000 Subject: [PATCH 6/8] Rename private vars --- sklearn/feature_selection/_rfe.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index bf8dff62dec7a..c3ddf168cf5c5 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -46,7 +46,7 @@ def _rfe_single_fit(rfe, estimator, X, y, train, test, scorer): ), ) - return rfe.scores_, rfe.n_features_selected_ + return rfe.step_scores_, rfe.step_n_features_ def _estimator_has(attr): @@ -267,10 +267,9 @@ def fit(self, X, y, **fit_params): return self._fit(X, y, **fit_params) def _fit(self, X, y, step_score=None, **fit_params): - # Parameter step_score controls the calculation of self.scores_ - # step_score is not exposed to users - # and is used when implementing RFECV - # self.scores_ will not be calculated when calling _fit through fit + # Parameter step_score controls the calculation of self.step_scores_ + # step_score is not exposed to users and is used when implementing RFECV + # self.step_scores_ will not be calculated when calling _fit through fit X, y = self._validate_data( X, @@ -299,8 +298,8 @@ def _fit(self, X, y, step_score=None, **fit_params): ranking_ = np.ones(n_features, dtype=int) if step_score: - self.n_features_selected_ = [] - self.scores_ = [] + self.step_n_features_ = [] + self.step_scores_ = [] # Elimination while np.sum(support_) > n_features_to_select: @@ -332,8 +331,8 @@ def _fit(self, X, y, step_score=None, **fit_params): # because 'estimator' must use features # that have not been eliminated yet if step_score: - self.n_features_selected_.append(len(features)) - self.scores_.append(step_score(estimator, features)) + self.step_n_features_.append(len(features)) + self.step_scores_.append(step_score(estimator, features)) support_[features[ranks][:threshold]] = False ranking_[np.logical_not(support_)] += 1 @@ -344,8 +343,8 @@ def _fit(self, X, y, step_score=None, **fit_params): # Compute step score when only n_features_to_select features left if step_score: - self.n_features_selected_.append(len(features)) - self.scores_.append(step_score(self.estimator_, features)) + self.step_n_features_.append(len(features)) + self.step_scores_.append(step_score(self.estimator_, features)) self.n_features_ = support_.sum() self.support_ = support_ self.ranking_ = ranking_ From 3dcbb8ae2960d0b588f194b73a82652faf09496b Mon Sep 17 00:00:00 2001 From: jeremie du boisberranger Date: Wed, 27 Mar 2024 14:57:53 +0100 Subject: [PATCH 7/8] a little bit more renaming --- sklearn/feature_selection/_rfe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sklearn/feature_selection/_rfe.py b/sklearn/feature_selection/_rfe.py index bf8dff62dec7a..34bcd54241d07 100644 --- a/sklearn/feature_selection/_rfe.py +++ b/sklearn/feature_selection/_rfe.py @@ -28,7 +28,7 @@ def _rfe_single_fit(rfe, estimator, X, y, train, test, scorer): """ - Return the score for a fit across one fold. + Return the score and n_features per step for a fit across one fold. """ X_train, y_train = _safe_split(estimator, X, y, train) X_test, y_test = _safe_split(estimator, X, y, test, train) @@ -760,14 +760,14 @@ def fit(self, X, y, groups=None): func(rfe, self.estimator, X, y, train, test, scorer) for train, test in cv.split(X, y, groups) ) - scores, n_features_per_iter = zip(*scores_features) + scores, step_n_features = zip(*scores_features) - n_features_per_iter_rev = np.array(n_features_per_iter[0])[::-1] + step_n_features_rev = np.array(step_n_features[0])[::-1] scores = np.array(scores) # Reverse order such that lowest number of features is selected in case of tie. scores_sum_rev = np.sum(scores, axis=0)[::-1] - n_features_to_select = n_features_per_iter_rev[np.argmax(scores_sum_rev)] + n_features_to_select = step_n_features_rev[np.argmax(scores_sum_rev)] # Re-execute an elimination with best_k over the whole set rfe = RFE( @@ -793,6 +793,6 @@ def fit(self, X, y, groups=None): "mean_test_score": np.mean(scores_rev, axis=0), "std_test_score": np.std(scores_rev, axis=0), **{f"split{i}_test_score": scores_rev[i] for i in range(scores.shape[0])}, - "n_features": n_features_per_iter_rev, + "n_features": step_n_features_rev, } return self From 24be84eb104d4dd665ee70e286ca5ea44e9d9a85 Mon Sep 17 00:00:00 2001 From: jeremie du boisberranger Date: Wed, 27 Mar 2024 15:00:43 +0100 Subject: [PATCH 8/8] oops --- build_tools/azure/python_nogil_lock.txt | 2 +- build_tools/azure/python_nogil_requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build_tools/azure/python_nogil_lock.txt b/build_tools/azure/python_nogil_lock.txt index 9dba84dfb619e..85988263b94b4 100644 --- a/build_tools/azure/python_nogil_lock.txt +++ b/build_tools/azure/python_nogil_lock.txt @@ -55,7 +55,7 @@ scipy==1.9.3 # via -r /scikit-learn/build_tools/azure/python_nogil_requirements.txt six==1.16.0 # via python-dateutil -threadpoolctl==3.4.0 +threadpoolctl==3.3.0 # via -r /scikit-learn/build_tools/azure/python_nogil_requirements.txt tomli==2.0.1 # via pytest diff --git a/build_tools/azure/python_nogil_requirements.txt b/build_tools/azure/python_nogil_requirements.txt index 959b5ace581a8..970059ede81aa 100644 --- a/build_tools/azure/python_nogil_requirements.txt +++ b/build_tools/azure/python_nogil_requirements.txt @@ -13,4 +13,3 @@ joblib threadpoolctl pytest pytest-xdist -meson-python