From bff0d92437881ec2116cd72bb7d86c5f677e00ee Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Sun, 6 Jul 2025 13:20:04 +0200 Subject: [PATCH 1/8] add compat.py for sklearn v1.7 --- .../integrations/sklearn/_compat.py | 69 +++++++++++++++++++ .../integrations/sklearn/best_estimator.py | 2 +- .../sklearn/hyperactive_search_cv.py | 40 ++++++++++- 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/hyperactive/integrations/sklearn/_compat.py diff --git a/src/hyperactive/integrations/sklearn/_compat.py b/src/hyperactive/integrations/sklearn/_compat.py new file mode 100644 index 00000000..4289e35c --- /dev/null +++ b/src/hyperactive/integrations/sklearn/_compat.py @@ -0,0 +1,69 @@ +""" +Internal helpers that bridge behavioural differences between +scikit-learn versions. Import *private* scikit-learn symbols **only** +here and nowhere else. + +Copyright: Hyperactive contributors +License: MIT +""" + +from __future__ import annotations + +import warnings +from typing import Dict, Any + +import sklearn +from packaging import version + +_SK_VERSION = version.parse(sklearn.__version__) + + +# ------------------------------------------------------------------ +# A) Replacement for `_deprecate_Xt_in_inverse_transform` +# ------------------------------------------------------------------ +if _SK_VERSION < version.parse("1.7"): + # Still exists → re-export + from sklearn.utils.deprecation import _deprecate_Xt_in_inverse_transform +else: + # Removed in 1.7 → provide drop-in replacement + def _deprecate_Xt_in_inverse_transform( # noqa: N802 keep sklearn’s name + X: Any | None, + Xt: Any | None, + ): + """ + scikit-learn ≤1.6 accepted both the old `Xt` parameter and the new + `X` parameter for `inverse_transform`. When only `Xt` is given we + return `Xt` and raise a deprecation warning (same behaviour that + scikit-learn had before 1.7); otherwise we return `X`. + """ + if Xt is not None: + warnings.warn( + "'Xt' was deprecated in scikit-learn 1.2 and has been " + "removed in 1.7; use the positional argument 'X' instead.", + FutureWarning, + stacklevel=2, + ) + return Xt + return X + + +# ------------------------------------------------------------------ +# B) Replacement for `_check_method_params` +# (still present in 1.7, but could be removed later) +# ------------------------------------------------------------------ +try: + from sklearn.utils.validation import _check_method_params # noqa: F401 +except ImportError: # fallback for future releases + + def _check_method_params( # type: ignore[override] # noqa: N802 + X, + params: Dict[str, Any], + ): + # passthrough – rely on estimator & indexable for validation + return params + + +__all__ = [ + "_deprecate_Xt_in_inverse_transform", + "_check_method_params", +] diff --git a/src/hyperactive/integrations/sklearn/best_estimator.py b/src/hyperactive/integrations/sklearn/best_estimator.py index def5f828..11d61e7b 100644 --- a/src/hyperactive/integrations/sklearn/best_estimator.py +++ b/src/hyperactive/integrations/sklearn/best_estimator.py @@ -4,11 +4,11 @@ from sklearn.utils.metaestimators import available_if -from sklearn.utils.deprecation import _deprecate_Xt_in_inverse_transform from sklearn.exceptions import NotFittedError from sklearn.utils.validation import check_is_fitted from .utils import _estimator_has +from ._compat import _deprecate_Xt_in_inverse_transform # NOTE Implementations of following methods from: diff --git a/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py b/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py index 2acb414f..2d3b943b 100644 --- a/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py +++ b/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py @@ -7,7 +7,7 @@ from sklearn.base import BaseEstimator, clone from sklearn.metrics import check_scoring -from sklearn.utils.validation import indexable, _check_method_params +from sklearn.utils.validation import indexable from sklearn.base import BaseEstimator as SklearnBaseEstimator @@ -18,6 +18,8 @@ from ...optimizers import RandomSearchOptimizer from hyperactive.experiment.integrations.sklearn_cv import SklearnCvExperiment +from ._compat import _check_method_params + class HyperactiveSearchCV(BaseEstimator, _BestEstimator_, Checks): """ @@ -85,7 +87,7 @@ def _refit(self, X, y=None, **fit_params): self.best_estimator_.fit(X, y, **fit_params) return self - def _check_data(self, X, y): + def _check_data_(self, X, y): X, y = indexable(X, y) if hasattr(self, "_validate_data"): validate_data = self._validate_data @@ -94,6 +96,33 @@ def _check_data(self, X, y): return validate_data(X, y) + def _check_data(self, X, y): + """ + Wrapper around scikit-learn’s input validation that: + • makes X 2-D (as required for estimators) + • lets y stay 1-D + The implementation follows scikit-learn 1.7+ guidelines and is + still accepted by 1.6, thanks to the validate_separately API + introduced in 1.3. + """ + X, y = indexable(X, y) + + if hasattr(self, "_validate_data"): + # Use BaseEstimator’s helper but ask it to treat X and y separately + return self._validate_data( + X, + y, + validate_separately=( + {"ensure_2d": True}, # for X + {"ensure_2d": False}, # for y + ), + ) + + # Fallback – should rarely be used + from sklearn.utils.validation import check_X_y + + return check_X_y(X, y, ensure_2d=True) + @Checks.verify_fit def fit(self, X, y, **fit_params): """ @@ -144,6 +173,13 @@ def fit(self, X, y, **fit_params): if self.refit: self._refit(X, y, **fit_params) + # make the wrapper itself expose n_features_in_ + if hasattr(self.best_estimator_, "n_features_in_"): + self.n_features_in_ = self.best_estimator_.n_features_in_ + else: + # Even when `refit=False` we must satisfy the contract + self.n_features_in_ = X.shape[1] + return self def score(self, X, y=None, **params): From 4e2adf0cebd33f2654f12155de115f45d2f85f15 Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Sun, 6 Jul 2025 13:54:27 +0200 Subject: [PATCH 2/8] add '_safe_refit'-method and '_safe_validate_X_y'-method in search_cv and opt_cv --- .../integrations/sklearn/_compat.py | 40 +++++++++++++++ .../sklearn/hyperactive_search_cv.py | 49 ++----------------- .../integrations/sklearn/opt_cv.py | 16 ++---- 3 files changed, 49 insertions(+), 56 deletions(-) diff --git a/src/hyperactive/integrations/sklearn/_compat.py b/src/hyperactive/integrations/sklearn/_compat.py index 4289e35c..99f8d358 100644 --- a/src/hyperactive/integrations/sklearn/_compat.py +++ b/src/hyperactive/integrations/sklearn/_compat.py @@ -14,10 +14,50 @@ import sklearn from packaging import version +from sklearn.utils.validation import indexable _SK_VERSION = version.parse(sklearn.__version__) +def _safe_validate_X_y(estimator, X, y): + """ + Version-independent replacement for naive validate_data(X, y). + + • Ensures X is 2-D. + • Allows y to stay 1-D (required by scikit-learn >=1.7 checks). + • Uses BaseEstimator._validate_data when available so that + estimator tags and sample-weight checks keep working. + """ + X, y = indexable(X, y) + + if hasattr(estimator, "_validate_data"): + return estimator._validate_data( + X, + y, + validate_separately=( + {"ensure_2d": True}, # parameters for X + {"ensure_2d": False}, # parameters for y + ), + ) + + # Fallback for very old scikit-learn versions (<0.23) + from sklearn.utils.validation import check_X_y + + return check_X_y(X, y, ensure_2d=True) + + +def _safe_refit(estimator, X, y, fit_params): + if estimator.refit: + estimator._refit(X, y, **fit_params) + + # make the wrapper itself expose n_features_in_ + if hasattr(estimator.best_estimator_, "n_features_in_"): + estimator.n_features_in_ = estimator.best_estimator_.n_features_in_ + else: + # Even when `refit=False` we must satisfy the contract + estimator.n_features_in_ = X.shape[1] + + # ------------------------------------------------------------------ # A) Replacement for `_deprecate_Xt_in_inverse_transform` # ------------------------------------------------------------------ diff --git a/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py b/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py index 2d3b943b..cf7fdd61 100644 --- a/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py +++ b/src/hyperactive/integrations/sklearn/hyperactive_search_cv.py @@ -7,7 +7,7 @@ from sklearn.base import BaseEstimator, clone from sklearn.metrics import check_scoring -from sklearn.utils.validation import indexable + from sklearn.base import BaseEstimator as SklearnBaseEstimator @@ -18,7 +18,7 @@ from ...optimizers import RandomSearchOptimizer from hyperactive.experiment.integrations.sklearn_cv import SklearnCvExperiment -from ._compat import _check_method_params +from ._compat import _check_method_params, _safe_validate_X_y, _safe_refit class HyperactiveSearchCV(BaseEstimator, _BestEstimator_, Checks): @@ -87,41 +87,8 @@ def _refit(self, X, y=None, **fit_params): self.best_estimator_.fit(X, y, **fit_params) return self - def _check_data_(self, X, y): - X, y = indexable(X, y) - if hasattr(self, "_validate_data"): - validate_data = self._validate_data - else: - from sklearn.utils.validation import validate_data - - return validate_data(X, y) - def _check_data(self, X, y): - """ - Wrapper around scikit-learn’s input validation that: - • makes X 2-D (as required for estimators) - • lets y stay 1-D - The implementation follows scikit-learn 1.7+ guidelines and is - still accepted by 1.6, thanks to the validate_separately API - introduced in 1.3. - """ - X, y = indexable(X, y) - - if hasattr(self, "_validate_data"): - # Use BaseEstimator’s helper but ask it to treat X and y separately - return self._validate_data( - X, - y, - validate_separately=( - {"ensure_2d": True}, # for X - {"ensure_2d": False}, # for y - ), - ) - - # Fallback – should rarely be used - from sklearn.utils.validation import check_X_y - - return check_X_y(X, y, ensure_2d=True) + return _safe_validate_X_y(self, X, y) @Checks.verify_fit def fit(self, X, y, **fit_params): @@ -170,15 +137,7 @@ def fit(self, X, y, **fit_params): self.best_score_ = hyper.best_score(objective_function) self.search_data_ = hyper.search_data(objective_function) - if self.refit: - self._refit(X, y, **fit_params) - - # make the wrapper itself expose n_features_in_ - if hasattr(self.best_estimator_, "n_features_in_"): - self.n_features_in_ = self.best_estimator_.n_features_in_ - else: - # Even when `refit=False` we must satisfy the contract - self.n_features_in_ = X.shape[1] + _safe_refit(self, X, y, fit_params) return self diff --git a/src/hyperactive/integrations/sklearn/opt_cv.py b/src/hyperactive/integrations/sklearn/opt_cv.py index eb83cf90..3e535885 100644 --- a/src/hyperactive/integrations/sklearn/opt_cv.py +++ b/src/hyperactive/integrations/sklearn/opt_cv.py @@ -4,14 +4,15 @@ from typing import Union from sklearn.base import BaseEstimator, clone -from sklearn.utils.validation import indexable, _check_method_params from hyperactive.experiment.integrations.sklearn_cv import SklearnCvExperiment from hyperactive.integrations.sklearn.best_estimator import ( - BestEstimator as _BestEstimator_ + BestEstimator as _BestEstimator_, ) from hyperactive.integrations.sklearn.checks import Checks +from ._compat import _check_method_params, _safe_validate_X_y, _safe_refit + class OptCV(BaseEstimator, _BestEstimator_, Checks): """Tuning via any optimizer in the hyperactive API. @@ -92,13 +93,7 @@ def _refit(self, X, y=None, **fit_params): return self def _check_data(self, X, y): - X, y = indexable(X, y) - if hasattr(self, "_validate_data"): - validate_data = self._validate_data - else: - from sklearn.utils.validation import validate_data - - return validate_data(X, y) + return _safe_validate_X_y(self, X, y) @Checks.verify_fit def fit(self, X, y, **fit_params): @@ -138,8 +133,7 @@ def fit(self, X, y, **fit_params): self.best_params_ = best_params self.best_estimator_ = clone(self.estimator).set_params(**best_params) - if self.refit: - self._refit(X, y, **fit_params) + _safe_refit(self, X, y, fit_params) return self From 87c3a5284ce70d953b0eadb686810b9c1a744d3f Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Sun, 6 Jul 2025 15:08:23 +0200 Subject: [PATCH 3/8] remove comments --- src/hyperactive/integrations/sklearn/_compat.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/hyperactive/integrations/sklearn/_compat.py b/src/hyperactive/integrations/sklearn/_compat.py index 99f8d358..528af594 100644 --- a/src/hyperactive/integrations/sklearn/_compat.py +++ b/src/hyperactive/integrations/sklearn/_compat.py @@ -58,9 +58,7 @@ def _safe_refit(estimator, X, y, fit_params): estimator.n_features_in_ = X.shape[1] -# ------------------------------------------------------------------ -# A) Replacement for `_deprecate_Xt_in_inverse_transform` -# ------------------------------------------------------------------ +# Replacement for `_deprecate_Xt_in_inverse_transform` if _SK_VERSION < version.parse("1.7"): # Still exists → re-export from sklearn.utils.deprecation import _deprecate_Xt_in_inverse_transform @@ -87,10 +85,7 @@ def _deprecate_Xt_in_inverse_transform( # noqa: N802 keep sklearn’s name return X -# ------------------------------------------------------------------ -# B) Replacement for `_check_method_params` -# (still present in 1.7, but could be removed later) -# ------------------------------------------------------------------ +# Replacement for `_check_method_params` try: from sklearn.utils.validation import _check_method_params # noqa: F401 except ImportError: # fallback for future releases From 2e94c6bf9f7aeeee468aba56a82e0c43985c9a2e Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Mon, 7 Jul 2025 15:33:31 +0200 Subject: [PATCH 4/8] add test for sklearn versions --- .github/workflows/test.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 116878f3..bf2a6e3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,3 +84,30 @@ jobs: - name: Test with pytest run: | python -m pytest src/hyperactive -p no:warnings + + test-sklearn-versions: + name: test-sklearn-${{ matrix.sklearn-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + sklearn-version: ["1.5", "1.6", "1.7"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies for scikit-learn ${{ matrix.sklearn-version }} + run: | + python -m pip install --upgrade pip + python -m pip install build pytest + python -m pip install scikit-learn==${{ matrix.sklearn-version }} + + - name: Run sklearn integration tests for ${{ matrix.sklearn-version }} + run: | + python -m pytest -x -p no:warnings tests/integrations/sklearn/ \ No newline at end of file From bad1f4f2b0f358157ebd36ff14722c3f95c117d5 Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Mon, 7 Jul 2025 15:36:39 +0200 Subject: [PATCH 5/8] add make install --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf2a6e3d..01b12aae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -106,6 +106,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install build pytest + make install python -m pip install scikit-learn==${{ matrix.sklearn-version }} - name: Run sklearn integration tests for ${{ matrix.sklearn-version }} From 9efe2d6cc534c3f1eb4ba94b4061a424f0f7fa1a Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Mon, 7 Jul 2025 16:52:46 +0200 Subject: [PATCH 6/8] add python version test matrix --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 01b12aae..bc6145ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,6 +93,7 @@ jobs: fail-fast: false matrix: sklearn-version: ["1.5", "1.6", "1.7"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 @@ -100,7 +101,7 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} - name: Install dependencies for scikit-learn ${{ matrix.sklearn-version }} run: | From 8ef5a7c98945d116bf12a8f357092a78f5b1e480 Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Mon, 7 Jul 2025 17:49:50 +0200 Subject: [PATCH 7/8] remove python v3.9 and v3.10 from sklearn testing --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc6145ac..3ff7ca21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,7 +93,7 @@ jobs: fail-fast: false matrix: sklearn-version: ["1.5", "1.6", "1.7"] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 From 882eaa218f975ce6372b6ad8523d49f113e94384 Mon Sep 17 00:00:00 2001 From: Simon Blanke Date: Mon, 7 Jul 2025 18:16:14 +0200 Subject: [PATCH 8/8] add python version to test name --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ff7ca21..6b5d0002 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,7 +86,7 @@ jobs: python -m pytest src/hyperactive -p no:warnings test-sklearn-versions: - name: test-sklearn-${{ matrix.sklearn-version }} + name: test-sklearn-${{ matrix.sklearn-version }} python-${{ matrix.python-version }} runs-on: ubuntu-latest strategy: