Skip to content

Commit 4a40b14

Browse files
authored
Feature/sklearn v1.7 (#138)
This PR fixes #128. Compatibility functions for v1.6 and v1.7 of sklearn in hyperactive.integrations.sklearn._compat.py
1 parent 964643b commit 4a40b14

5 files changed

Lines changed: 144 additions & 22 deletions

File tree

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,32 @@ jobs:
8484
- name: Test with pytest
8585
run: |
8686
python -m pytest src/hyperactive -p no:warnings
87+
88+
test-sklearn-versions:
89+
name: test-sklearn-${{ matrix.sklearn-version }} python-${{ matrix.python-version }}
90+
runs-on: ubuntu-latest
91+
92+
strategy:
93+
fail-fast: false
94+
matrix:
95+
sklearn-version: ["1.5", "1.6", "1.7"]
96+
python-version: ["3.11", "3.12", "3.13"]
97+
98+
steps:
99+
- uses: actions/checkout@v4
100+
101+
- name: Set up Python 3.12
102+
uses: actions/setup-python@v5
103+
with:
104+
python-version: ${{ matrix.python-version }}
105+
106+
- name: Install dependencies for scikit-learn ${{ matrix.sklearn-version }}
107+
run: |
108+
python -m pip install --upgrade pip
109+
python -m pip install build pytest
110+
make install
111+
python -m pip install scikit-learn==${{ matrix.sklearn-version }}
112+
113+
- name: Run sklearn integration tests for ${{ matrix.sklearn-version }}
114+
run: |
115+
python -m pytest -x -p no:warnings tests/integrations/sklearn/
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Internal helpers that bridge behavioural differences between
3+
scikit-learn versions. Import *private* scikit-learn symbols **only**
4+
here and nowhere else.
5+
6+
Copyright: Hyperactive contributors
7+
License: MIT
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import warnings
13+
from typing import Dict, Any
14+
15+
import sklearn
16+
from packaging import version
17+
from sklearn.utils.validation import indexable
18+
19+
_SK_VERSION = version.parse(sklearn.__version__)
20+
21+
22+
def _safe_validate_X_y(estimator, X, y):
23+
"""
24+
Version-independent replacement for naive validate_data(X, y).
25+
26+
• Ensures X is 2-D.
27+
• Allows y to stay 1-D (required by scikit-learn >=1.7 checks).
28+
• Uses BaseEstimator._validate_data when available so that
29+
estimator tags and sample-weight checks keep working.
30+
"""
31+
X, y = indexable(X, y)
32+
33+
if hasattr(estimator, "_validate_data"):
34+
return estimator._validate_data(
35+
X,
36+
y,
37+
validate_separately=(
38+
{"ensure_2d": True}, # parameters for X
39+
{"ensure_2d": False}, # parameters for y
40+
),
41+
)
42+
43+
# Fallback for very old scikit-learn versions (<0.23)
44+
from sklearn.utils.validation import check_X_y
45+
46+
return check_X_y(X, y, ensure_2d=True)
47+
48+
49+
def _safe_refit(estimator, X, y, fit_params):
50+
if estimator.refit:
51+
estimator._refit(X, y, **fit_params)
52+
53+
# make the wrapper itself expose n_features_in_
54+
if hasattr(estimator.best_estimator_, "n_features_in_"):
55+
estimator.n_features_in_ = estimator.best_estimator_.n_features_in_
56+
else:
57+
# Even when `refit=False` we must satisfy the contract
58+
estimator.n_features_in_ = X.shape[1]
59+
60+
61+
# Replacement for `_deprecate_Xt_in_inverse_transform`
62+
if _SK_VERSION < version.parse("1.7"):
63+
# Still exists → re-export
64+
from sklearn.utils.deprecation import _deprecate_Xt_in_inverse_transform
65+
else:
66+
# Removed in 1.7 → provide drop-in replacement
67+
def _deprecate_Xt_in_inverse_transform( # noqa: N802 keep sklearn’s name
68+
X: Any | None,
69+
Xt: Any | None,
70+
):
71+
"""
72+
scikit-learn ≤1.6 accepted both the old `Xt` parameter and the new
73+
`X` parameter for `inverse_transform`. When only `Xt` is given we
74+
return `Xt` and raise a deprecation warning (same behaviour that
75+
scikit-learn had before 1.7); otherwise we return `X`.
76+
"""
77+
if Xt is not None:
78+
warnings.warn(
79+
"'Xt' was deprecated in scikit-learn 1.2 and has been "
80+
"removed in 1.7; use the positional argument 'X' instead.",
81+
FutureWarning,
82+
stacklevel=2,
83+
)
84+
return Xt
85+
return X
86+
87+
88+
# Replacement for `_check_method_params`
89+
try:
90+
from sklearn.utils.validation import _check_method_params # noqa: F401
91+
except ImportError: # fallback for future releases
92+
93+
def _check_method_params( # type: ignore[override] # noqa: N802
94+
X,
95+
params: Dict[str, Any],
96+
):
97+
# passthrough – rely on estimator & indexable for validation
98+
return params
99+
100+
101+
__all__ = [
102+
"_deprecate_Xt_in_inverse_transform",
103+
"_check_method_params",
104+
]

src/hyperactive/integrations/sklearn/best_estimator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55

66
from sklearn.utils.metaestimators import available_if
7-
from sklearn.utils.deprecation import _deprecate_Xt_in_inverse_transform
87
from sklearn.exceptions import NotFittedError
98
from sklearn.utils.validation import check_is_fitted
109

1110
from .utils import _estimator_has
11+
from ._compat import _deprecate_Xt_in_inverse_transform
1212

1313

1414
# NOTE Implementations of following methods from:

src/hyperactive/integrations/sklearn/hyperactive_search_cv.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from sklearn.base import BaseEstimator, clone
99
from sklearn.metrics import check_scoring
10-
from sklearn.utils.validation import indexable, _check_method_params
10+
1111

1212
from sklearn.base import BaseEstimator as SklearnBaseEstimator
1313

@@ -18,6 +18,8 @@
1818
from ...optimizers import RandomSearchOptimizer
1919
from hyperactive.experiment.integrations.sklearn_cv import SklearnCvExperiment
2020

21+
from ._compat import _check_method_params, _safe_validate_X_y, _safe_refit
22+
2123

2224
class HyperactiveSearchCV(BaseEstimator, _BestEstimator_, Checks):
2325
"""
@@ -86,13 +88,7 @@ def _refit(self, X, y=None, **fit_params):
8688
return self
8789

8890
def _check_data(self, X, y):
89-
X, y = indexable(X, y)
90-
if hasattr(self, "_validate_data"):
91-
validate_data = self._validate_data
92-
else:
93-
from sklearn.utils.validation import validate_data
94-
95-
return validate_data(X, y)
91+
return _safe_validate_X_y(self, X, y)
9692

9793
@Checks.verify_fit
9894
def fit(self, X, y, **fit_params):
@@ -141,8 +137,7 @@ def fit(self, X, y, **fit_params):
141137
self.best_score_ = hyper.best_score(objective_function)
142138
self.search_data_ = hyper.search_data(objective_function)
143139

144-
if self.refit:
145-
self._refit(X, y, **fit_params)
140+
_safe_refit(self, X, y, fit_params)
146141

147142
return self
148143

src/hyperactive/integrations/sklearn/opt_cv.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
from typing import Union
55

66
from sklearn.base import BaseEstimator, clone
7-
from sklearn.utils.validation import indexable, _check_method_params
87

98
from hyperactive.experiment.integrations.sklearn_cv import SklearnCvExperiment
109
from hyperactive.integrations.sklearn.best_estimator import (
11-
BestEstimator as _BestEstimator_
10+
BestEstimator as _BestEstimator_,
1211
)
1312
from hyperactive.integrations.sklearn.checks import Checks
1413

14+
from ._compat import _check_method_params, _safe_validate_X_y, _safe_refit
15+
1516

1617
class OptCV(BaseEstimator, _BestEstimator_, Checks):
1718
"""Tuning via any optimizer in the hyperactive API.
@@ -92,13 +93,7 @@ def _refit(self, X, y=None, **fit_params):
9293
return self
9394

9495
def _check_data(self, X, y):
95-
X, y = indexable(X, y)
96-
if hasattr(self, "_validate_data"):
97-
validate_data = self._validate_data
98-
else:
99-
from sklearn.utils.validation import validate_data
100-
101-
return validate_data(X, y)
96+
return _safe_validate_X_y(self, X, y)
10297

10398
@Checks.verify_fit
10499
def fit(self, X, y, **fit_params):
@@ -138,8 +133,7 @@ def fit(self, X, y, **fit_params):
138133
self.best_params_ = best_params
139134
self.best_estimator_ = clone(self.estimator).set_params(**best_params)
140135

141-
if self.refit:
142-
self._refit(X, y, **fit_params)
136+
_safe_refit(self, X, y, fit_params)
143137

144138
return self
145139

0 commit comments

Comments
 (0)