diff --git a/doc/whats_new/v0.7.rst b/doc/whats_new/v0.7.rst index 6018cc5c4..a9bfc9552 100644 --- a/doc/whats_new/v0.7.rst +++ b/doc/whats_new/v0.7.rst @@ -19,6 +19,9 @@ Maintenance - Remove `FutureWarning` issued by `scikit-learn` 0.23. :pr:`710` by :user:`Guillaume Lemaitre `. +- Impose keywords only argument as in `scikit-learn`. + :pr:`721` by :user:`Guillaume Lemaitre `. + Changed models .............. @@ -63,3 +66,7 @@ Deprecation :class:`imblearn.under_sampling.ClusterCentroids` since it was used by :class:`sklearn.cluster.KMeans` which deprecated it. :pr:`710` by :user:`Guillaume Lemaitre `. + +- Deprecation of passing keyword argument by position similarly to + `scikit-learn`. + :pr:`721` by :user:`Guillaume lemaitre `. diff --git a/imblearn/base.py b/imblearn/base.py index e66738a45..86bb53778 100644 --- a/imblearn/base.py +++ b/imblearn/base.py @@ -14,6 +14,7 @@ from .utils import check_sampling_strategy, check_target_type from .utils._validation import ArraysTransformer +from .utils._validation import _deprecate_positional_args class SamplerMixin(BaseEstimator, metaclass=ABCMeta): @@ -213,7 +214,8 @@ class FunctionSampler(BaseSampler): _sampling_type = "bypass" - def __init__(self, func=None, accept_sparse=True, kw_args=None, + @_deprecate_positional_args + def __init__(self, *, func=None, accept_sparse=True, kw_args=None, validate=True): super().__init__() self.func = func diff --git a/imblearn/combine/_smote_enn.py b/imblearn/combine/_smote_enn.py index 92e30e32e..74ca81b9c 100644 --- a/imblearn/combine/_smote_enn.py +++ b/imblearn/combine/_smote_enn.py @@ -15,6 +15,7 @@ from ..utils import Substitution from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution( @@ -85,8 +86,10 @@ class SMOTEENN(BaseSampler): _sampling_type = "over-sampling" + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, smote=None, diff --git a/imblearn/combine/_smote_tomek.py b/imblearn/combine/_smote_tomek.py index 078319da0..f8bbb567b 100644 --- a/imblearn/combine/_smote_tomek.py +++ b/imblearn/combine/_smote_tomek.py @@ -16,6 +16,7 @@ from ..utils import Substitution from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution( @@ -85,8 +86,10 @@ class SMOTETomek(BaseSampler): _sampling_type = "over-sampling" + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, smote=None, diff --git a/imblearn/datasets/_imbalance.py b/imblearn/datasets/_imbalance.py index f761a79e8..b10ec3b57 100644 --- a/imblearn/datasets/_imbalance.py +++ b/imblearn/datasets/_imbalance.py @@ -9,10 +9,12 @@ from ..under_sampling import RandomUnderSampler from ..utils import check_sampling_strategy +from ..utils._validation import _deprecate_positional_args +@_deprecate_positional_args def make_imbalance( - X, y, sampling_strategy=None, random_state=None, verbose=False, **kwargs + X, y, *, sampling_strategy=None, random_state=None, verbose=False, **kwargs ): """Turns a dataset into an imbalanced dataset with a specific sampling strategy. diff --git a/imblearn/datasets/_zenodo.py b/imblearn/datasets/_zenodo.py index 3a180ad78..e8a53423a 100644 --- a/imblearn/datasets/_zenodo.py +++ b/imblearn/datasets/_zenodo.py @@ -57,6 +57,8 @@ from sklearn.utils import Bunch from sklearn.utils import check_random_state +from ..utils._validation import _deprecate_positional_args + URL = ( "https://zenodo.org/record/61452/files/" "benchmark-imbalanced-learn.tar.gz" @@ -101,7 +103,9 @@ MAP_ID_NAME[v + 1] = k +@_deprecate_positional_args def fetch_datasets( + *, data_home=None, filter_data=None, download_if_missing=True, diff --git a/imblearn/datasets/tests/test_imbalance.py b/imblearn/datasets/tests/test_imbalance.py index 4c0893521..58612034d 100644 --- a/imblearn/datasets/tests/test_imbalance.py +++ b/imblearn/datasets/tests/test_imbalance.py @@ -32,14 +32,14 @@ def test_make_imbalance_error(iris, sampling_strategy, err_msg): # cover in the common tests so we will repeat it here X, y = iris with pytest.raises(ValueError, match=err_msg): - make_imbalance(X, y, sampling_strategy) + make_imbalance(X, y, sampling_strategy=sampling_strategy) def test_make_imbalance_error_single_class(iris): X, y = iris y = np.zeros_like(y) with pytest.raises(ValueError, match="needs to have more than 1 class."): - make_imbalance(X, y, {0: 10}) + make_imbalance(X, y, sampling_strategy={0: 10}) @pytest.mark.parametrize( diff --git a/imblearn/ensemble/_bagging.py b/imblearn/ensemble/_bagging.py index a17036306..79916f43a 100644 --- a/imblearn/ensemble/_bagging.py +++ b/imblearn/ensemble/_bagging.py @@ -18,6 +18,7 @@ from ..utils import Substitution, check_target_type, check_sampling_strategy from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution( @@ -175,10 +176,12 @@ class BalancedBaggingClassifier(BaggingClassifier): [ 2 225]] """ + @_deprecate_positional_args def __init__( self, base_estimator=None, n_estimators=10, + *, max_samples=1.0, max_features=1.0, bootstrap=True, diff --git a/imblearn/ensemble/_easy_ensemble.py b/imblearn/ensemble/_easy_ensemble.py index 699f1fc94..443ae0d1f 100644 --- a/imblearn/ensemble/_easy_ensemble.py +++ b/imblearn/ensemble/_easy_ensemble.py @@ -17,6 +17,7 @@ from ..utils import Substitution, check_target_type, check_sampling_strategy from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args from ..pipeline import Pipeline MAX_INT = np.iinfo(np.int32).max @@ -125,10 +126,12 @@ class EasyEnsembleClassifier(BaggingClassifier): [ 2 225]] """ + @_deprecate_positional_args def __init__( self, n_estimators=10, base_estimator=None, + *, warm_start=False, sampling_strategy="auto", replacement=False, diff --git a/imblearn/ensemble/_forest.py b/imblearn/ensemble/_forest.py index 0272629ee..cb2b77dc9 100644 --- a/imblearn/ensemble/_forest.py +++ b/imblearn/ensemble/_forest.py @@ -34,6 +34,7 @@ from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring from ..utils._validation import check_sampling_strategy +from ..utils._validation import _deprecate_positional_args MAX_INT = np.iinfo(np.int32).max @@ -297,9 +298,11 @@ class labels (multi-output problem). [1] """ + @_deprecate_positional_args def __init__( self, n_estimators=100, + *, criterion="gini", max_depth=None, min_samples_split=2, diff --git a/imblearn/ensemble/_weight_boosting.py b/imblearn/ensemble/_weight_boosting.py index 177a8cd02..483f14a8a 100644 --- a/imblearn/ensemble/_weight_boosting.py +++ b/imblearn/ensemble/_weight_boosting.py @@ -12,6 +12,7 @@ from ..pipeline import make_pipeline from ..utils import Substitution, check_target_type from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution( @@ -120,9 +121,11 @@ class RUSBoostClassifier(AdaBoostClassifier): array([...]) """ + @_deprecate_positional_args def __init__( self, base_estimator=None, + *, n_estimators=50, learning_rate=1.0, algorithm="SAMME.R", diff --git a/imblearn/keras/_generator.py b/imblearn/keras/_generator.py index 39f036713..bb241cf6c 100644 --- a/imblearn/keras/_generator.py +++ b/imblearn/keras/_generator.py @@ -41,16 +41,17 @@ def import_from_tensforflow(): ParentClass, HAS_KERAS = import_keras() -from scipy.sparse import issparse +from scipy.sparse import issparse # noqa -from sklearn.base import clone -from sklearn.utils import _safe_indexing -from sklearn.utils import check_random_state +from sklearn.base import clone # noqa +from sklearn.utils import _safe_indexing # noqa +from sklearn.utils import check_random_state # noqa -from ..under_sampling import RandomUnderSampler -from ..utils import Substitution -from ..utils._docstring import _random_state_docstring -from ..tensorflow import balanced_batch_generator as tf_bbg +from ..under_sampling import RandomUnderSampler # noqa +from ..utils import Substitution # noqa +from ..utils._docstring import _random_state_docstring # noqa +from ..tensorflow import balanced_batch_generator as tf_bbg # noqa +from ..utils._validation import _deprecate_positional_args # noqa class BalancedBatchGenerator(*ParentClass): @@ -130,10 +131,12 @@ class BalancedBatchGenerator(*ParentClass): # flag for keras sequence duck-typing use_sequence_api = True + @_deprecate_positional_args def __init__( self, X, y, + *, sample_weight=None, sampler=None, batch_size=32, @@ -199,9 +202,11 @@ def __getitem__(self, index): @Substitution(random_state=_random_state_docstring) +@_deprecate_positional_args def balanced_batch_generator( X, y, + *, sample_weight=None, sampler=None, batch_size=32, diff --git a/imblearn/metrics/_classification.py b/imblearn/metrics/_classification.py index 1f48bbda1..3b8dc7256 100644 --- a/imblearn/metrics/_classification.py +++ b/imblearn/metrics/_classification.py @@ -30,10 +30,14 @@ except ImportError: from sklearn.externals.funcsigs import signature +from ..utils._validation import _deprecate_positional_args + +@_deprecate_positional_args def sensitivity_specificity_support( y_true, y_pred, + *, labels=None, pos_label=1, average=None, @@ -279,9 +283,11 @@ def sensitivity_specificity_support( return sensitivity, specificity, true_sum +@_deprecate_positional_args def sensitivity_score( y_true, y_pred, + *, labels=None, pos_label=1, average="binary", @@ -382,9 +388,11 @@ def sensitivity_score( return s +@_deprecate_positional_args def specificity_score( y_true, y_pred, + *, labels=None, pos_label=1, average="binary", @@ -485,9 +493,11 @@ def specificity_score( return s +@_deprecate_positional_args def geometric_mean_score( y_true, y_pred, + *, labels=None, pos_label=1, average="multiclass", @@ -675,7 +685,8 @@ class is unrecognized by the classifier, G-mean resolves to zero. To return gmean -def make_index_balanced_accuracy(alpha=0.1, squared=True): +@_deprecate_positional_args +def make_index_balanced_accuracy(*, alpha=0.1, squared=True): """Balance any scoring function using the index balanced accuracy This factory function wraps scoring function to express it as the @@ -785,9 +796,11 @@ def compute_score(*args, **kwargs): return decorate +@_deprecate_positional_args def classification_report_imbalanced( y_true, y_pred, + *, labels=None, target_names=None, sample_weight=None, diff --git a/imblearn/over_sampling/_adasyn.py b/imblearn/over_sampling/_adasyn.py index f014243e7..60b1acc1f 100644 --- a/imblearn/over_sampling/_adasyn.py +++ b/imblearn/over_sampling/_adasyn.py @@ -15,6 +15,7 @@ from ..utils import Substitution from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution( @@ -81,8 +82,10 @@ class ADASYN(BaseOverSampler): Resampled dataset shape Counter({{0: 904, 1: 900}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, n_neighbors=5, diff --git a/imblearn/over_sampling/_random_over_sampler.py b/imblearn/over_sampling/_random_over_sampler.py index 4a2cfbf71..cbdaf2179 100644 --- a/imblearn/over_sampling/_random_over_sampler.py +++ b/imblearn/over_sampling/_random_over_sampler.py @@ -14,6 +14,7 @@ from ..utils import check_target_type from ..utils import Substitution from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution( sampling_strategy=BaseOverSampler._sampling_strategy_docstring, @@ -68,7 +69,8 @@ class RandomOverSampler(BaseOverSampler): Resampled dataset shape Counter({{0: 900, 1: 900}}) """ - def __init__(self, sampling_strategy="auto", random_state=None): + @_deprecate_positional_args + def __init__(self, *, sampling_strategy="auto", random_state=None): super().__init__(sampling_strategy=sampling_strategy) self.random_state = random_state diff --git a/imblearn/over_sampling/_smote.py b/imblearn/over_sampling/_smote.py index a141d4b04..f5ce02ad2 100644 --- a/imblearn/over_sampling/_smote.py +++ b/imblearn/over_sampling/_smote.py @@ -30,6 +30,7 @@ from ..utils import Substitution from ..utils._docstring import _n_jobs_docstring from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args class BaseSMOTE(BaseOverSampler): @@ -297,8 +298,10 @@ class BorderlineSMOTE(BaseSMOTE): Resampled dataset shape Counter({{0: 900, 1: 900}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, k_neighbors=5, @@ -495,8 +498,10 @@ class SVMSMOTE(BaseSMOTE): Resampled dataset shape Counter({{0: 900, 1: 900}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, k_neighbors=5, @@ -696,8 +701,10 @@ class SMOTE(BaseSMOTE): Resampled dataset shape Counter({{0: 900, 1: 900}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, k_neighbors=5, @@ -873,9 +880,11 @@ class SMOTENC(SMOTE): _required_parameters = ["categorical_features"] + @_deprecate_positional_args def __init__( self, categorical_features, + *, sampling_strategy="auto", random_state=None, k_neighbors=5, @@ -1142,8 +1151,10 @@ class KMeansSMOTE(BaseSMOTE): More 0 samples: True """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, k_neighbors=2, diff --git a/imblearn/tensorflow/_generator.py b/imblearn/tensorflow/_generator.py index 6c5b32872..669904351 100644 --- a/imblearn/tensorflow/_generator.py +++ b/imblearn/tensorflow/_generator.py @@ -9,12 +9,15 @@ from ..under_sampling import RandomUnderSampler from ..utils import Substitution from ..utils._docstring import _random_state_docstring +from ..utils._validation import _deprecate_positional_args @Substitution(random_state=_random_state_docstring) +@_deprecate_positional_args def balanced_batch_generator( X, y, + *, sample_weight=None, sampler=None, batch_size=32, diff --git a/imblearn/under_sampling/_prototype_generation/_cluster_centroids.py b/imblearn/under_sampling/_prototype_generation/_cluster_centroids.py index 333b1d7cc..ebdbed27b 100644 --- a/imblearn/under_sampling/_prototype_generation/_cluster_centroids.py +++ b/imblearn/under_sampling/_prototype_generation/_cluster_centroids.py @@ -20,6 +20,7 @@ from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring from ...utils._docstring import _random_state_docstring +from ...utils._validation import _deprecate_positional_args VOTING_KIND = ("auto", "hard", "soft") @@ -96,8 +97,10 @@ class ClusterCentroids(BaseUnderSampler): Resampled dataset shape Counter({{...}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, estimator=None, diff --git a/imblearn/under_sampling/_prototype_selection/_condensed_nearest_neighbour.py b/imblearn/under_sampling/_prototype_selection/_condensed_nearest_neighbour.py index 8d79f5758..a3bf1e50e 100644 --- a/imblearn/under_sampling/_prototype_selection/_condensed_nearest_neighbour.py +++ b/imblearn/under_sampling/_prototype_selection/_condensed_nearest_neighbour.py @@ -19,6 +19,7 @@ from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring from ...utils._docstring import _random_state_docstring +from ...utils._validation import _deprecate_positional_args @Substitution( @@ -94,8 +95,10 @@ class CondensedNearestNeighbour(BaseCleaningSampler): Resampled dataset shape Counter({{-1: 268, 1: 227}}) # doctest: +SKIP """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, n_neighbors=None, diff --git a/imblearn/under_sampling/_prototype_selection/_edited_nearest_neighbours.py b/imblearn/under_sampling/_prototype_selection/_edited_nearest_neighbours.py index 41d62c622..6d2de1f3d 100644 --- a/imblearn/under_sampling/_prototype_selection/_edited_nearest_neighbours.py +++ b/imblearn/under_sampling/_prototype_selection/_edited_nearest_neighbours.py @@ -17,6 +17,7 @@ from ...utils import check_neighbors_object from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring +from ...utils._validation import _deprecate_positional_args SEL_KIND = ("all", "mode") @@ -99,8 +100,9 @@ class EditedNearestNeighbours(BaseCleaningSampler): Resampled dataset shape Counter({{1: 887, 0: 100}}) """ + @_deprecate_positional_args def __init__( - self, sampling_strategy="auto", n_neighbors=3, kind_sel="all", + self, *, sampling_strategy="auto", n_neighbors=3, kind_sel="all", n_jobs=None ): super().__init__(sampling_strategy=sampling_strategy) @@ -246,8 +248,10 @@ class RepeatedEditedNearestNeighbours(BaseCleaningSampler): Resampled dataset shape Counter({{1: 887, 0: 100}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", n_neighbors=3, max_iter=100, @@ -423,8 +427,10 @@ class without early stopping. Resampled dataset shape Counter({{1: 887, 0: 100}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", n_neighbors=3, kind_sel="all", diff --git a/imblearn/under_sampling/_prototype_selection/_instance_hardness_threshold.py b/imblearn/under_sampling/_prototype_selection/_instance_hardness_threshold.py index 9edd2ab11..f1e4ae9ec 100644 --- a/imblearn/under_sampling/_prototype_selection/_instance_hardness_threshold.py +++ b/imblearn/under_sampling/_prototype_selection/_instance_hardness_threshold.py @@ -22,6 +22,7 @@ from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring from ...utils._docstring import _random_state_docstring +from ...utils._validation import _deprecate_positional_args @Substitution( @@ -97,8 +98,10 @@ class InstanceHardnessThreshold(BaseUnderSampler): Resampled dataset shape Counter({{1: 5..., 0: 100}}) """ + @_deprecate_positional_args def __init__( self, + *, estimator=None, sampling_strategy="auto", random_state=None, diff --git a/imblearn/under_sampling/_prototype_selection/_nearmiss.py b/imblearn/under_sampling/_prototype_selection/_nearmiss.py index 0dd567a68..bf426ff3a 100644 --- a/imblearn/under_sampling/_prototype_selection/_nearmiss.py +++ b/imblearn/under_sampling/_prototype_selection/_nearmiss.py @@ -15,6 +15,7 @@ from ...utils import check_neighbors_object from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring +from ...utils._validation import _deprecate_positional_args @Substitution( @@ -93,8 +94,10 @@ class NearMiss(BaseUnderSampler): Resampled dataset shape Counter({{0: 100, 1: 100}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", version=1, n_neighbors=3, diff --git a/imblearn/under_sampling/_prototype_selection/_neighbourhood_cleaning_rule.py b/imblearn/under_sampling/_prototype_selection/_neighbourhood_cleaning_rule.py index 6222da83b..01381453d 100644 --- a/imblearn/under_sampling/_prototype_selection/_neighbourhood_cleaning_rule.py +++ b/imblearn/under_sampling/_prototype_selection/_neighbourhood_cleaning_rule.py @@ -16,6 +16,7 @@ from ...utils import check_neighbors_object from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring +from ...utils._validation import _deprecate_positional_args SEL_KIND = ("all", "mode") @@ -101,8 +102,10 @@ class NeighbourhoodCleaningRule(BaseCleaningSampler): Resampled dataset shape Counter({{1: 877, 0: 100}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", n_neighbors=3, kind_sel="all", diff --git a/imblearn/under_sampling/_prototype_selection/_one_sided_selection.py b/imblearn/under_sampling/_prototype_selection/_one_sided_selection.py index 81cf1799e..93c753e97 100644 --- a/imblearn/under_sampling/_prototype_selection/_one_sided_selection.py +++ b/imblearn/under_sampling/_prototype_selection/_one_sided_selection.py @@ -17,6 +17,7 @@ from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring from ...utils._docstring import _random_state_docstring +from ...utils._validation import _deprecate_positional_args @Substitution( @@ -88,8 +89,10 @@ class OneSidedSelection(BaseCleaningSampler): Resampled dataset shape Counter({{1: 496, 0: 100}}) """ + @_deprecate_positional_args def __init__( self, + *, sampling_strategy="auto", random_state=None, n_neighbors=None, diff --git a/imblearn/under_sampling/_prototype_selection/_random_under_sampler.py b/imblearn/under_sampling/_prototype_selection/_random_under_sampler.py index 805e0907e..b5ec14dae 100644 --- a/imblearn/under_sampling/_prototype_selection/_random_under_sampler.py +++ b/imblearn/under_sampling/_prototype_selection/_random_under_sampler.py @@ -13,6 +13,7 @@ from ...utils import check_target_type from ...utils import Substitution from ...utils._docstring import _random_state_docstring +from ...utils._validation import _deprecate_positional_args @Substitution( @@ -71,8 +72,9 @@ class RandomUnderSampler(BaseUnderSampler): Resampled dataset shape Counter({{0: 100, 1: 100}}) """ + @_deprecate_positional_args def __init__( - self, sampling_strategy="auto", random_state=None, replacement=False + self, *, sampling_strategy="auto", random_state=None, replacement=False ): super().__init__(sampling_strategy=sampling_strategy) self.random_state = random_state diff --git a/imblearn/under_sampling/_prototype_selection/_tomek_links.py b/imblearn/under_sampling/_prototype_selection/_tomek_links.py index 4bf9d880a..1105bbbc6 100644 --- a/imblearn/under_sampling/_prototype_selection/_tomek_links.py +++ b/imblearn/under_sampling/_prototype_selection/_tomek_links.py @@ -12,6 +12,7 @@ from ..base import BaseCleaningSampler from ...utils import Substitution from ...utils._docstring import _n_jobs_docstring +from ...utils._validation import _deprecate_positional_args @Substitution( @@ -74,7 +75,8 @@ class TomekLinks(BaseCleaningSampler): Resampled dataset shape Counter({{1: 897, 0: 100}}) """ - def __init__(self, sampling_strategy="auto", n_jobs=None): + @_deprecate_positional_args + def __init__(self, *, sampling_strategy="auto", n_jobs=None): super().__init__(sampling_strategy=sampling_strategy) self.n_jobs = n_jobs diff --git a/imblearn/under_sampling/_prototype_selection/tests/test_instance_hardness_threshold.py b/imblearn/under_sampling/_prototype_selection/tests/test_instance_hardness_threshold.py index 967f2d468..18db03a99 100644 --- a/imblearn/under_sampling/_prototype_selection/tests/test_instance_hardness_threshold.py +++ b/imblearn/under_sampling/_prototype_selection/tests/test_instance_hardness_threshold.py @@ -41,7 +41,8 @@ def test_iht_init(): sampling_strategy = "auto" iht = InstanceHardnessThreshold( - ESTIMATOR, sampling_strategy=sampling_strategy, random_state=RND_SEED + estimator=ESTIMATOR, sampling_strategy=sampling_strategy, + random_state=RND_SEED, ) assert iht.sampling_strategy == sampling_strategy @@ -49,7 +50,9 @@ def test_iht_init(): def test_iht_fit_resample(): - iht = InstanceHardnessThreshold(ESTIMATOR, random_state=RND_SEED) + iht = InstanceHardnessThreshold( + estimator=ESTIMATOR, random_state=RND_SEED + ) X_resampled, y_resampled = iht.fit_resample(X, Y) assert X_resampled.shape == (12, 2) assert y_resampled.shape == (12,) @@ -58,7 +61,8 @@ def test_iht_fit_resample(): def test_iht_fit_resample_half(): sampling_strategy = {0: 3, 1: 3} iht = InstanceHardnessThreshold( - NB(), sampling_strategy=sampling_strategy, random_state=RND_SEED + estimator=NB(), sampling_strategy=sampling_strategy, + random_state=RND_SEED ) X_resampled, y_resampled = iht.fit_resample(X, Y) assert X_resampled.shape == (6, 2) diff --git a/imblearn/utils/_validation.py b/imblearn/utils/_validation.py index 1ebbedcd2..a46918b0c 100644 --- a/imblearn/utils/_validation.py +++ b/imblearn/utils/_validation.py @@ -5,6 +5,8 @@ import warnings from collections import OrderedDict +from functools import wraps +from inspect import signature, Parameter from numbers import Integral, Real import numpy as np @@ -599,3 +601,41 @@ def check_sampling_strategy(sampling_strategy, y, sampling_type, **kwargs): "all": _sampling_strategy_all, "auto": _sampling_strategy_auto, } + + +def _deprecate_positional_args(f): + """Decorator for methods that issues warnings for positional arguments + + Using the keyword-only argument syntax in pep 3102, arguments after the + * will issue a warning when passed as a positional argument. + + Parameters + ---------- + f : function + function to check arguments on + """ + sig = signature(f) + kwonly_args = [] + all_args = [] + + for name, param in sig.parameters.items(): + if param.kind == Parameter.POSITIONAL_OR_KEYWORD: + all_args.append(name) + elif param.kind == Parameter.KEYWORD_ONLY: + kwonly_args.append(name) + + @wraps(f) + def inner_f(*args, **kwargs): + extra_args = len(args) - len(all_args) + if extra_args > 0: + # ignore first 'self' argument for instance methods + args_msg = ['{}={}'.format(name, arg) + for name, arg in zip(kwonly_args[:extra_args], + args[-extra_args:])] + warnings.warn("Pass {} as keyword args. From version 0.9 " + "passing these as positional arguments will " + "result in an error".format(", ".join(args_msg)), + FutureWarning) + kwargs.update({k: arg for k, arg in zip(sig.parameters, args)}) + return f(**kwargs) + return inner_f diff --git a/imblearn/utils/tests/test_validation.py b/imblearn/utils/tests/test_validation.py index 0f4dcb98b..e4f9c01c8 100644 --- a/imblearn/utils/tests/test_validation.py +++ b/imblearn/utils/tests/test_validation.py @@ -18,6 +18,7 @@ from imblearn.utils import check_sampling_strategy from imblearn.utils import check_target_type from imblearn.utils._validation import ArraysTransformer +from imblearn.utils._validation import _deprecate_positional_args multiclass_target = np.array([1] * 50 + [2] * 100 + [3] * 25) binary_target = np.array([1] * 25 + [0] * 100) @@ -366,3 +367,35 @@ def test_arrays_transformer_pandas(): assert isinstance(y_res, pd.Series) assert_array_equal(y_res.name, y_s.name) assert_array_equal(y_res.dtype, y_s.dtype) + + +def test_deprecate_positional_args_warns_for_function(): + + @_deprecate_positional_args + def f1(a, b, *, c=1, d=1): + pass + + with pytest.warns(FutureWarning, + match=r"Pass c=3 as keyword args"): + f1(1, 2, 3) + + with pytest.warns(FutureWarning, + match=r"Pass c=3, d=4 as keyword args"): + f1(1, 2, 3, 4) + + @_deprecate_positional_args + def f2(a=1, *, b=1, c=1, d=1): + pass + + with pytest.warns(FutureWarning, + match=r"Pass b=2 as keyword args"): + f2(1, 2) + + # The * is place before a keyword only argument without a default value + @_deprecate_positional_args + def f3(a, *, b, c=1, d=1): + pass + + with pytest.warns(FutureWarning, + match=r"Pass b=2 as keyword args"): + f3(1, 2)