Repository: scikit-learn-contrib/imbalanced-learn (language: python)

Issue:
[BUG] Test issues with sklearn 1.4
#### Describe the bug
I've updated the Debian package of sklearn to 1.4 which is supposed to work with imbalanced-learn 0.12.0.  Unfortunately the build on Debian fails as you can see in the [build log in our CI](https://salsa.debian.org/med-team/imbalanced-learn/-/jobs/5331921)

#### Steps/Code to Reproduce
Install sklearn 1.4 and do

     python3 -m pytest -k "not test_rusboost"

on a Debian sid.

#### Expected Results
Passing its own test suite.

#### Actual Results

The errors in the log are starting with

    =================================== FAILURES ===================================
    _______________ test_fit_predict_on_pipeline_without_fit_predict _______________
    self = <sklearn.utils._available_if._AvailableIfDescriptor object at 0x7fe089a69950>
    obj = Pipeline(steps=[('scaler', StandardScaler()), ('pca', PCA(svd_solver='full'))])
    owner = <class 'imblearn.pipeline.Pipeline'>
        def _check(self, obj, owner):
            attr_err_msg = (
                f"This {repr(owner.__name__)} has no attribute {repr(self.attribute_name)}"
            )
            try:
    >           check_result = self.check(obj)
    /usr/lib/python3/dist-packages/sklearn/utils/_available_if.py:29:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    self = Pipeline(steps=[('scaler', StandardScaler()), ('pca', PCA(svd_solver='full'))])
        def check(self):
            # raise original `AttributeError` if `attr` does not exist
    >       getattr(self._final_estimator, attr)
    E       AttributeError: 'PCA' object has no attribute 'fit_predict'
    /usr/lib/python3/dist-packages/sklearn/pipeline.py:53: AttributeError
    The above exception was the direct cause of the following exception:
        def test_fit_predict_on_pipeline_without_fit_predict():
            # tests that a pipeline does not have fit_predict method when final
            # step of pipeline does not have fit_predict defined
            scaler = StandardScaler()
            pca = PCA(svd_solver="full")
            pipe = Pipeline([("scaler", scaler), ("pca", pca)])
            error_regex = "'PCA' object has no attribute 'fit_predict'"
            with raises(AttributeError, match=error_regex):
    >           getattr(pipe, "fit_predict")
    imblearn/tests/test_pipeline.py:415:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    /usr/lib/python3/dist-packages/sklearn/utils/_available_if.py:40: in __get__
        self._check(obj, owner=owner)
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    self = <sklearn.utils._available_if._AvailableIfDescriptor object at 0x7fe089a69950>
    obj = Pipeline(steps=[('scaler', StandardScaler()), ('pca', PCA(svd_solver='full'))])
    owner = <class 'imblearn.pipeline.Pipeline'>
        def _check(self, obj, owner):
            attr_err_msg = (
                f"This {repr(owner.__name__)} has no attribute {repr(self.attribute_name)}"
            )
            try:
                check_result = self.check(obj)
            except Exception as e:
    >           raise AttributeError(attr_err_msg) from e
    E           AttributeError: This 'Pipeline' has no attribute 'fit_predict'
    /usr/lib/python3/dist-packages/sklearn/utils/_available_if.py:31: AttributeError
    During handling of the above exception, another exception occurred:
        def test_fit_predict_on_pipeline_without_fit_predict():
            # tests that a pipeline does not have fit_predict method when final
            # step of pipeline does not have fit_predict defined
            scaler = StandardScaler()
            pca = PCA(svd_solver="full")
            pipe = Pipeline([("scaler", scaler), ("pca", pca)])
            error_regex = "'PCA' object has no attribute 'fit_predict'"
    >       with raises(AttributeError, match=error_regex):
    E       AssertionError: Regex pattern did not match.
    E        Regex: "'PCA' object has no attribute 'fit_predict'"
    E        Input: "This 'Pipeline' has no attribute 'fit_predict'"
    imblearn/tests/test_pipeline.py:414: AssertionError
    _____________ test_score_samples_on_pipeline_without_score_samples _____________
    ...

#### Versions

Python 3.11.8 (main, Feb  7 2024, 21:52:08) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sklearn; sklearn.show_versions()

System:
    python: 3.11.8 (main, Feb  7 2024, 21:52:08) [GCC 13.2.0]
executable: /usr/bin/python3
   machine: Linux-6.6.13-amd64-x86_64-with-glibc2.37

Python dependencies:
      sklearn: 1.4.1.post1
          pip: 24.0
   setuptools: 68.1.2
        numpy: 1.24.2
        scipy: 1.10.1
       Cython: 0.29.37
       pandas: 2.1.4+dfsg
   matplotlib: 3.6.3
       joblib: 1.3.2
threadpoolctl: 3.1.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
         prefix: libopenblas
       filepath: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so
        version: 0.3.26
threading_layer: pthreads
   architecture: Haswell
    num_threads: 4

       user_api: openmp
   internal_api: openmp
         prefix: libgomp
       filepath: /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0
        version: None
    num_threads: 4

>>> import sys; print("Python", sys.version)
Python 3.11.8 (main, Feb  7 2024, 21:52:08) [GCC 13.2.0]

>>> import numpy; print("NumPy", numpy.__version__)
NumPy 1.24.2

>>> import scipy; print("SciPy", scipy.__version__)
SciPy 1.11.4

>>> import sklearn; print("Scikit-Learn", sklearn.__version__)
Scikit-Learn 1.4.1.post1




Discussion / hints:
I was able to fix this for Debian by adapting the tests for the new error message format:

``` diff
--- imbalanced-learn.orig/imblearn/tests/test_pipeline.py
+++ imbalanced-learn/imblearn/tests/test_pipeline.py
@@ -410,7 +410,7 @@
     scaler = StandardScaler()
     pca = PCA(svd_solver="full")
     pipe = Pipeline([("scaler", scaler), ("pca", pca)])
-    error_regex = "'PCA' object has no attribute 'fit_predict'"
+    error_regex = "This 'Pipeline' has no attribute 'fit_predict'"
     with raises(AttributeError, match=error_regex):
         getattr(pipe, "fit_predict")
 
@@ -1219,7 +1219,7 @@
     pipe.fit(X, y)
     with pytest.raises(
         AttributeError,
-        match="'LogisticRegression' object has no attribute 'score_samples'",
+        match="This 'Pipeline' has no attribute 'score_samples'",
     ):
         pipe.score_samples(X)
```
Yep the regex has changed for the error match. Not big deal for user facing code. 

I'll fix it in main
> I'll fix it in main

Has that happened yet? I was looking for a relevant commit on that file, but couldn't find one. If not, I can apply @mr-c's patch. We are facing the same issue now sklearn has been updated to 1.4 in Fedora.

Fix patch:
diff --git a/examples/applications/porto_seguro_keras_under_sampling.py b/examples/applications/porto_seguro_keras_under_sampling.py
index ee8a6e2f0..9175427fa 100644
--- a/examples/applications/porto_seguro_keras_under_sampling.py
+++ b/examples/applications/porto_seguro_keras_under_sampling.py
@@ -151,7 +151,7 @@ def wrapper(*args, **kwds):
 # mini-batches.
 import tensorflow
 from sklearn.metrics import roc_auc_score
-from sklearn.utils import parse_version
+from sklearn.utils.fixes import parse_version
 
 tf_version = parse_version(tensorflow.__version__)
 
diff --git a/imblearn/_config.py b/imblearn/_config.py
index 4c093db09..ef98e7305 100644
--- a/imblearn/_config.py
+++ b/imblearn/_config.py
@@ -7,7 +7,7 @@
 from contextlib import contextmanager as contextmanager
 
 import sklearn
-from sklearn.utils import parse_version
+from sklearn.utils.fixes import parse_version
 
 sklearn_version = parse_version(sklearn.__version__)
 
diff --git a/imblearn/base.py b/imblearn/base.py
index e529fead6..0b2d94e84 100644
--- a/imblearn/base.py
+++ b/imblearn/base.py
@@ -17,7 +17,7 @@
     from sklearn.base import _OneToOneFeatureMixin as OneToOneFeatureMixin
 
 from sklearn.preprocessing import label_binarize
-from sklearn.utils import parse_version
+from sklearn.utils.fixes import parse_version
 from sklearn.utils.multiclass import check_classification_targets
 
 from .utils import check_sampling_strategy, check_target_type
diff --git a/imblearn/ensemble/_bagging.py b/imblearn/ensemble/_bagging.py
index afcf3fd3a..2808239a7 100644
--- a/imblearn/ensemble/_bagging.py
+++ b/imblearn/ensemble/_bagging.py
@@ -16,7 +16,7 @@
 from sklearn.ensemble._base import _partition_estimators
 from sklearn.exceptions import NotFittedError
 from sklearn.tree import DecisionTreeClassifier
-from sklearn.utils import parse_version
+from sklearn.utils.fixes import parse_version
 from sklearn.utils.validation import check_is_fitted
 
 try:
diff --git a/imblearn/ensemble/_easy_ensemble.py b/imblearn/ensemble/_easy_ensemble.py
index db3c6cbb7..1da81d93c 100644
--- a/imblearn/ensemble/_easy_ensemble.py
+++ b/imblearn/ensemble/_easy_ensemble.py
@@ -15,8 +15,8 @@
 from sklearn.ensemble._bagging import _parallel_decision_function
 from sklearn.ensemble._base import _partition_estimators
 from sklearn.exceptions import NotFittedError
-from sklearn.utils import parse_version
 from sklearn.utils._tags import _safe_tags
+from sklearn.utils.fixes import parse_version
 from sklearn.utils.validation import check_is_fitted
 
 try:
diff --git a/imblearn/ensemble/_forest.py b/imblearn/ensemble/_forest.py
index a7c8f9beb..5f8d08e91 100644
--- a/imblearn/ensemble/_forest.py
+++ b/imblearn/ensemble/_forest.py
@@ -22,7 +22,8 @@
 )
 from sklearn.exceptions import DataConversionWarning
 from sklearn.tree import DecisionTreeClassifier
-from sklearn.utils import _safe_indexing, check_random_state, parse_version
+from sklearn.utils import _safe_indexing, check_random_state
+from sklearn.utils.fixes import parse_version
 from sklearn.utils.multiclass import type_of_target
 from sklearn.utils.validation import _check_sample_weight
 
diff --git a/imblearn/ensemble/_weight_boosting.py b/imblearn/ensemble/_weight_boosting.py
index 539b7824f..9da02255e 100644
--- a/imblearn/ensemble/_weight_boosting.py
+++ b/imblearn/ensemble/_weight_boosting.py
@@ -8,7 +8,8 @@
 from sklearn.ensemble import AdaBoostClassifier
 from sklearn.ensemble._base import _set_random_states
 from sklearn.tree import DecisionTreeClassifier
-from sklearn.utils import _safe_indexing, parse_version
+from sklearn.utils import _safe_indexing
+from sklearn.utils.fixes import parse_version
 from sklearn.utils.validation import has_fit_parameter
 
 from ..base import _ParamsValidationMixin
diff --git a/imblearn/utils/_available_if.py b/imblearn/utils/_available_if.py
index 9b2c5e6db..bca75e735 100644
--- a/imblearn/utils/_available_if.py
+++ b/imblearn/utils/_available_if.py
@@ -7,7 +7,7 @@
 from types import MethodType
 
 import sklearn
-from sklearn.utils import parse_version
+from sklearn.utils.fixes import parse_version
 
 sklearn_version = parse_version(sklearn.__version__)
 


Test patch:
diff --git a/imblearn/keras/tests/test_generator.py b/imblearn/keras/tests/test_generator.py
index f49ecd0aa..a073d846d 100644
--- a/imblearn/keras/tests/test_generator.py
+++ b/imblearn/keras/tests/test_generator.py
@@ -70,7 +70,7 @@ def test_balanced_batch_generator_class(data, sampler, sample_weight):
         batch_size=10,
         random_state=42,
     )
-    model.fit_generator(generator=training_generator, epochs=10)
+    model.fit(training_generator, epochs=10)
 
 
 @pytest.mark.parametrize("keep_sparse", [True, False])
@@ -122,8 +122,8 @@ def test_balanced_batch_generator_function(data, sampler, sample_weight):
         batch_size=10,
         random_state=42,
     )
-    model.fit_generator(
-        generator=training_generator,
+    model.fit(
+        training_generator,
         steps_per_epoch=steps_per_epoch,
         epochs=10,
     )
diff --git a/imblearn/tests/test_pipeline.py b/imblearn/tests/test_pipeline.py
index c39758d9f..409dbce41 100644
--- a/imblearn/tests/test_pipeline.py
+++ b/imblearn/tests/test_pipeline.py
@@ -410,7 +410,7 @@ def test_fit_predict_on_pipeline_without_fit_predict():
     scaler = StandardScaler()
     pca = PCA(svd_solver="full")
     pipe = Pipeline([("scaler", scaler), ("pca", pca)])
-    error_regex = "'PCA' object has no attribute 'fit_predict'"
+    error_regex = "has no attribute 'fit_predict'"
     with raises(AttributeError, match=error_regex):
         getattr(pipe, "fit_predict")
 
@@ -1219,7 +1219,7 @@ def test_score_samples_on_pipeline_without_score_samples():
     pipe.fit(X, y)
     with pytest.raises(
         AttributeError,
-        match="'LogisticRegression' object has no attribute 'score_samples'",
+        match="has no attribute 'score_samples'",
     ):
         pipe.score_samples(X)