Skip to content

MAINT compatibility sklearn 1.5 #1074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/whats_new/0.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Bug fixes
Compatibility
.............

- Compatibility with scikit-learn 1.5
:pr:`1074` by :user:`Guillaume Lemaitre <glemaitre>`.

Deprecations
............

Expand Down
2 changes: 1 addition & 1 deletion imblearn/ensemble/_bagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def _fit(self, X, y, max_samples=None, max_depth=None, sample_weight=None):
self.sampler_ = clone(self.sampler)
# RandomUnderSampler is not supporting sample_weight. We need to pass
# None.
return super()._fit(X, y, self.max_samples, sample_weight=None)
return super()._fit(X, y, self.max_samples)

# TODO: remove when minimum supported version of scikit-learn is 1.1
@available_if(_estimator_has("decision_function"))
Expand Down
14 changes: 8 additions & 6 deletions imblearn/ensemble/_easy_ensemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def _fit(self, X, y, max_samples=None, max_depth=None, sample_weight=None):
check_target_type(y)
# RandomUnderSampler is not supporting sample_weight. We need to pass
# None.
return super()._fit(X, y, self.max_samples, sample_weight=None)
return super()._fit(X, y, self.max_samples)

# TODO: remove when minimum supported version of scikit-learn is 1.1
@available_if(_estimator_has("decision_function"))
Expand Down Expand Up @@ -365,9 +365,11 @@ def base_estimator_(self):
raise error
raise error

def _more_tags(self):
def _get_estimator(self):
if self.estimator is None:
estimator = AdaBoostClassifier(algorithm="SAMME")
else:
estimator = self.estimator
return {"allow_nan": _safe_tags(estimator, "allow_nan")}
return AdaBoostClassifier(algorithm="SAMME")
return self.estimator

# TODO: remove when minimum supported version of scikit-learn is 1.5
def _more_tags(self):
return {"allow_nan": _safe_tags(self._get_estimator(), "allow_nan")}
9 changes: 8 additions & 1 deletion imblearn/over_sampling/_smote/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
import warnings

import numpy as np
import sklearn
from scipy import sparse
from sklearn.base import clone
from sklearn.exceptions import DataConversionWarning
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
from sklearn.utils import (
_get_column_indices,
_safe_indexing,
check_array,
check_random_state,
)
from sklearn.utils.fixes import parse_version
from sklearn.utils.sparsefuncs_fast import (
csr_mean_variance_axis0,
)
Expand All @@ -34,6 +35,12 @@
from ...utils.fixes import _is_pandas_df, _mode
from ..base import BaseOverSampler

sklearn_version = parse_version(sklearn.__version__).base_version
if parse_version(sklearn_version) < parse_version("1.5"):
from sklearn.utils import _get_column_indices
else:
from sklearn.utils._indexing import _get_column_indices


class BaseSMOTE(BaseOverSampler):
"""Base class for the different SMOTE algorithms."""
Expand Down
10 changes: 9 additions & 1 deletion imblearn/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
# Christos Aridas
# Guillaume Lemaitre <g.lemaitre58@gmail.com>
# License: BSD
import sklearn
from sklearn import pipeline
from sklearn.base import clone
from sklearn.utils import Bunch, _print_elapsed_time
from sklearn.utils import Bunch
from sklearn.utils.fixes import parse_version
from sklearn.utils.metaestimators import available_if
from sklearn.utils.validation import check_memory

Expand All @@ -34,6 +36,12 @@

__all__ = ["Pipeline", "make_pipeline"]

sklearn_version = parse_version(sklearn.__version__).base_version
if parse_version(sklearn_version) < parse_version("1.5"):
from sklearn.utils import _print_elapsed_time
else:
from sklearn.utils._user_interface import _print_elapsed_time


class Pipeline(_ParamsValidationMixin, pipeline.Pipeline):
"""Pipeline of transforms and resamples with a final estimator.
Expand Down
38 changes: 29 additions & 9 deletions imblearn/utils/_metadata_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,9 +1086,12 @@ def _serialize(self):

def __iter__(self):
if self._self_request:
yield "$self_request", RouterMappingPair(
mapping=MethodMapping.from_str("one-to-one"),
router=self._self_request,
yield (
"$self_request",
RouterMappingPair(
mapping=MethodMapping.from_str("one-to-one"),
router=self._self_request,
),
)
for name, route_mapping in self._route_mappings.items():
yield (name, route_mapping)
Expand Down Expand Up @@ -1234,7 +1237,7 @@ def __init__(self, name, keys, validate_keys=True):

def __get__(self, instance, owner):
# we would want to have a method which accepts only the expected args
def func(**kw):
def func(*args, **kw):
"""Updates the request for provided parameters

This docstring is overwritten below.
Expand All @@ -1253,15 +1256,32 @@ def func(**kw):
f"arguments are: {set(self.keys)}"
)

requests = instance._get_metadata_request()
# This makes it possible to use the decorated method as an unbound
# method, for instance when monkeypatching.
# https://github.com/scikit-learn/scikit-learn/issues/28632
if instance is None:
_instance = args[0]
args = args[1:]
else:
_instance = instance

# Replicating python's behavior when positional args are given other
# than `self`, and `self` is only allowed if this method is unbound.
if args:
raise TypeError(
f"set_{self.name}_request() takes 0 positional argument but"
f" {len(args)} were given"
)

requests = _instance._get_metadata_request()
method_metadata_request = getattr(requests, self.name)

for prop, alias in kw.items():
if alias is not UNCHANGED:
method_metadata_request.add_request(param=prop, alias=alias)
instance._metadata_request = requests
_instance._metadata_request = requests

return instance
return _instance

# Now we set the relevant attributes of the function so that it seems
# like a normal method to the end user, with known expected arguments.
Expand Down Expand Up @@ -1525,13 +1545,13 @@ def process_routing(_obj, _method, /, **kwargs):
metadata to corresponding methods or corresponding child objects. The object
names are those defined in `obj.get_metadata_routing()`.
"""
if not _routing_enabled() and not kwargs:
if not kwargs:
# If routing is not enabled and kwargs are empty, then we don't have to
# try doing any routing, we can simply return a structure which returns
# an empty dict on routed_params.ANYTHING.ANY_METHOD.
class EmptyRequest:
def get(self, name, default=None):
return default if default else {}
return Bunch(**{method: dict() for method in METHODS})

def __getitem__(self, name):
return Bunch(**{method: dict() for method in METHODS})
Expand Down