From 35151ccd9568430251a3a07daf484cd814ad05fc Mon Sep 17 00:00:00 2001 From: solegalli Date: Wed, 15 Jan 2025 10:58:46 -0300 Subject: [PATCH 01/36] expand parametrize with tests to all current classes --- ...check_estimators_with_parametrize_tests.py | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/tests/check_estimators_with_parametrize_tests.py b/tests/check_estimators_with_parametrize_tests.py index 209fa29db..62323273f 100644 --- a/tests/check_estimators_with_parametrize_tests.py +++ b/tests/check_estimators_with_parametrize_tests.py @@ -7,6 +7,12 @@ from sklearn.linear_model import LogisticRegression from sklearn.utils.estimator_checks import parametrize_with_checks +from feature_engine.creation import ( + CyclicalFeatures, + DecisionTreeFeatures, + MathFeatures, + RelativeFeatures, +) from feature_engine.encoding import ( CountFrequencyEncoder, DecisionTreeEncoder, @@ -14,6 +20,7 @@ OneHotEncoder, OrdinalEncoder, RareLabelEncoder, + StringSimilarityEncoder, WoEEncoder, ) from feature_engine.imputation import ( @@ -27,20 +34,28 @@ ) from feature_engine.outliers import ArbitraryOutlierCapper, OutlierTrimmer, Winsorizer from feature_engine.selection import ( + MRMR, DropConstantFeatures, DropCorrelatedFeatures, DropDuplicateFeatures, DropFeatures, DropHighPSIFeatures, + ProbeFeatureSelection, RecursiveFeatureAddition, RecursiveFeatureElimination, + SelectByInformationValue, SelectByShuffling, SelectBySingleFeaturePerformance, SelectByTargetMeanPerformance, SmartCorrelatedSelection, ) -from feature_engine.timeseries.forecasting import LagFeatures +from feature_engine.timeseries.forecasting import ( + ExpandingWindowFeatures, + LagFeatures, + WindowFeatures, +) from feature_engine.transformation import ( + ArcsinTransformer, BoxCoxTransformer, LogTransformer, PowerTransformer, @@ -48,11 +63,22 @@ YeoJohnsonTransformer, ) from feature_engine.wrappers import SklearnTransformerWrapper -from feature_engine.creation import DecisionTreeFeatures, CyclicalFeatures # creation -@parametrize_with_checks([DecisionTreeFeatures(regression=False), CyclicalFeatures()]) +@parametrize_with_checks( + [ + DecisionTreeFeatures(regression=False), + CyclicalFeatures(), + MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore"), + RelativeFeatures( + variables=["x0", "x1"], + reference=["x0"], + func=["add"], + missing_values="ignore", + ), + ] +) def test_sklearn_compatible_creator(estimator, check): check(estimator) @@ -88,6 +114,7 @@ def test_sklearn_compatible_imputer(estimator, check): ignore_format=True, ), WoEEncoder(ignore_format=True), + StringSimilarityEncoder(ignore_format=True), ] ) def test_sklearn_compatible_encoder(estimator, check): @@ -97,7 +124,7 @@ def test_sklearn_compatible_encoder(estimator, check): # outliers @parametrize_with_checks( [ - ArbitraryOutlierCapper(max_capping_dict={"0": 10}), + ArbitraryOutlierCapper(max_capping_dict={"x0": 10}), OutlierTrimmer(), Winsorizer(), ] @@ -109,6 +136,7 @@ def test_sklearn_compatible_outliers(estimator, check): # transformers @parametrize_with_checks( [ + ArcsinTransformer(), BoxCoxTransformer(), LogTransformer(), PowerTransformer(), @@ -123,7 +151,7 @@ def test_sklearn_compatible_transformer(estimator, check): # selectors @parametrize_with_checks( [ - DropFeatures(features_to_drop=["0"]), + DropFeatures(features_to_drop=["x0"]), DropConstantFeatures(missing_values="ignore"), DropDuplicateFeatures(), DropCorrelatedFeatures(), @@ -144,6 +172,9 @@ def test_sklearn_compatible_transformer(estimator, check): threshold=-100, ), SelectByTargetMeanPerformance(scoring="roc_auc", bins=3, regression=False), + SelectByInformationValue(), + MRMR(), + ProbeFeatureSelection(estimator=LogisticRegression()), ] ) def test_sklearn_compatible_selectors(estimator, check): @@ -157,6 +188,12 @@ def test_sklearn_compatible_wrapper(estimator, check): # test_forecasting -@parametrize_with_checks([LagFeatures(missing_values="ignore")]) +@parametrize_with_checks( + [ + LagFeatures(missing_values="ignore"), + WindowFeatures(missing_values="ignore"), + ExpandingWindowFeatures(missing_values="ignore"), + ] +) def test_sklearn_compatible_forecasters(estimator, check): check(estimator) From 9d145a30d7a26c097df2bf361ce588e77f01ab7d Mon Sep 17 00:00:00 2001 From: solegalli Date: Thu, 16 Jan 2025 08:36:47 -0300 Subject: [PATCH 02/36] reorder creation imports, remove estimator checks --- feature_engine/_base_transformers/base_numerical.py | 6 +++++- feature_engine/creation/base_creation.py | 6 +++++- feature_engine/creation/decision_tree_features.py | 2 +- tests/test_creation/test_check_estimator_creation.py | 6 +++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/feature_engine/_base_transformers/base_numerical.py b/feature_engine/_base_transformers/base_numerical.py index 9c1967b0d..8753c6f78 100644 --- a/feature_engine/_base_transformers/base_numerical.py +++ b/feature_engine/_base_transformers/base_numerical.py @@ -22,7 +22,7 @@ class BaseNumericalTransformer( - BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin + TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin ): """Shared set-up procedures across numerical transformers, i.e., variable transformers, discretisers, math combination. @@ -122,3 +122,7 @@ def _more_tags(self): tags_dict = _return_tags() tags_dict["variables"] = "numerical" return tags_dict + +def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/creation/base_creation.py b/feature_engine/creation/base_creation.py index 7e6a501d8..a7d5105bc 100644 --- a/feature_engine/creation/base_creation.py +++ b/feature_engine/creation/base_creation.py @@ -22,7 +22,7 @@ ) -class BaseCreation(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class BaseCreation(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """Shared set-up, checks and methods across creation transformers.""" def __init__( @@ -128,3 +128,7 @@ def _more_tags(self): ] = "this transformer works with datasets that contain at least 2 variables. \ Otherwise, there is nothing to combine" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/creation/decision_tree_features.py b/feature_engine/creation/decision_tree_features.py index f17c344fb..1ad6298ad 100644 --- a/feature_engine/creation/decision_tree_features.py +++ b/feature_engine/creation/decision_tree_features.py @@ -58,7 +58,7 @@ transform=_transform_creation_docstring, fit_transform=_fit_transform_docstring, ) -class DecisionTreeFeatures(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class DecisionTreeFeatures(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """ `DecisionTreeFeatures()` adds new variables to the data that result of the output of decision trees trained with one or more features. diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index 2d7b12cdc..91762171b 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -21,9 +21,9 @@ ] -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +# @pytest.mark.parametrize("estimator", _estimators) +# def test_check_estimator_from_sklearn(estimator): +# return check_estimator(estimator, expected_failed_checks=estimator._more_tags()['_xfail_checks']) _estimators = [ From e6531d6e68cd3e02cbb6fe2c80b20663825e2083 Mon Sep 17 00:00:00 2001 From: solegalli Date: Thu, 16 Jan 2025 08:39:13 -0300 Subject: [PATCH 03/36] reorder inheritance datetime module --- feature_engine/datetime/datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature_engine/datetime/datetime.py b/feature_engine/datetime/datetime.py index bd158ae73..cc1d93d75 100644 --- a/feature_engine/datetime/datetime.py +++ b/feature_engine/datetime/datetime.py @@ -45,7 +45,7 @@ fit=_fit_not_learn_docstring, fit_transform=_fit_transform_docstring, ) -class DatetimeFeatures(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class DatetimeFeatures(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """ DatetimeFeatures extracts date and time features from datetime variables, adding new columns to the dataset. DatetimeFeatures can extract datetime information from From bd08d78c47732109d30d800eb80ebf8318f3d893 Mon Sep 17 00:00:00 2001 From: solegalli Date: Thu, 16 Jan 2025 23:53:26 -0300 Subject: [PATCH 04/36] makes module creation compatible with sklearn 1.6 --- .../_base_transformers/base_numerical.py | 6 ++-- feature_engine/creation/base_creation.py | 5 +++ .../creation/decision_tree_features.py | 4 +++ feature_engine/tags.py | 16 ++++++++- tests/parametrize_with_checks_creation_v16.py | 32 +++++++++++++++++ .../test_check_estimator_creation.py | 36 ++++++++++++------- 6 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 tests/parametrize_with_checks_creation_v16.py diff --git a/feature_engine/_base_transformers/base_numerical.py b/feature_engine/_base_transformers/base_numerical.py index 8753c6f78..f0e5d4542 100644 --- a/feature_engine/_base_transformers/base_numerical.py +++ b/feature_engine/_base_transformers/base_numerical.py @@ -123,6 +123,6 @@ def _more_tags(self): tags_dict["variables"] = "numerical" return tags_dict -def __sklearn_tags__(self): - tags = super().__sklearn_tags__() - return tags \ No newline at end of file + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/creation/base_creation.py b/feature_engine/creation/base_creation.py index a7d5105bc..587ae9de6 100644 --- a/feature_engine/creation/base_creation.py +++ b/feature_engine/creation/base_creation.py @@ -21,6 +21,10 @@ find_numerical_variables, ) +try: + from sklearn.utils import Tags as _sklearn_Tags +except ImportError: + _sklearn_Tags = object class BaseCreation(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """Shared set-up, checks and methods across creation transformers.""" @@ -131,4 +135,5 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True return tags \ No newline at end of file diff --git a/feature_engine/creation/decision_tree_features.py b/feature_engine/creation/decision_tree_features.py index 1ad6298ad..f1040e468 100644 --- a/feature_engine/creation/decision_tree_features.py +++ b/feature_engine/creation/decision_tree_features.py @@ -471,3 +471,7 @@ def _more_tags(self): tags_dict["requires_y"] = True tags_dict["variables"] = "numerical" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/tags.py b/feature_engine/tags.py index 6dc3647b7..78a025311 100644 --- a/feature_engine/tags.py +++ b/feature_engine/tags.py @@ -1,5 +1,10 @@ +import sklearn +from sklearn.utils.fixes import parse_version + +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + def _return_tags(): - return { + tags = { "preserves_dtype": [], "_xfail_checks": { # Complex data in math terms, are values like 4i (imaginary numbers @@ -25,3 +30,12 @@ def _return_tags(): "only work with dataframes.", }, } + + if sklearn_version > parse_version("1.6"): + all_fail = { + "check_do_not_raise_errors_in_init_or_set_params": "against feature engine design", + "check_transformer_preserve_dtypes": "our transformers do not preserve dtype", + "check_n_features_in_after_fitting":"not sure why it fails, we do check" + } + tags['_xfail_checks'].update(all_fail) + return tags diff --git a/tests/parametrize_with_checks_creation_v16.py b/tests/parametrize_with_checks_creation_v16.py new file mode 100644 index 000000000..2fd04930a --- /dev/null +++ b/tests/parametrize_with_checks_creation_v16.py @@ -0,0 +1,32 @@ +from sklearn.utils.estimator_checks import parametrize_with_checks + +from feature_engine.creation import ( + CyclicalFeatures, + DecisionTreeFeatures, + MathFeatures, + RelativeFeatures, +) + +dtf = DecisionTreeFeatures(regression=False) +cf = CyclicalFeatures() +mf = MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore") +rf = RelativeFeatures( + variables=["x0", "x1"], + reference=["x0"], + func=["add"], + missing_values="ignore", +) + +EXPECTED_FAILED_CHECKS = { + "DecisionTreeFeatures": dtf._more_tags()['_xfail_checks'], + "CyclicalFeatures": cf._more_tags()['_xfail_checks'], + "MathFeatures": mf._more_tags()['_xfail_checks'], + "RelativeFeatures": rf._more_tags()['_xfail_checks'], +} +# creation +@parametrize_with_checks( + estimators = [dtf, cf, mf, rf], + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get(est.__class__.__name__, {}) +) +def test_sklearn_compatible_creator(estimator, check): + check(estimator) diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index 91762171b..99daca479 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -1,7 +1,12 @@ import pandas as pd +import sklearn import pytest + from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version + +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) from feature_engine.creation import ( CyclicalFeatures, @@ -11,20 +16,27 @@ ) from tests.estimator_checks.estimator_checks import check_feature_engine_estimator -_estimators = [ - MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore"), - RelativeFeatures( - variables=["x0", "x1"], reference=["x0"], func=["add"], missing_values="ignore" - ), - CyclicalFeatures(), - DecisionTreeFeatures(regression=False), -] - +mf = MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore") +rf = RelativeFeatures( + variables=["x0", "x1"], reference=["x0"], func=["add"], missing_values="ignore" +) +cf = CyclicalFeatures() +dtf = DecisionTreeFeatures(regression=False) -# @pytest.mark.parametrize("estimator", _estimators) -# def test_check_estimator_from_sklearn(estimator): -# return check_estimator(estimator, expected_failed_checks=estimator._more_tags()['_xfail_checks']) +if sklearn_version > parse_version("1.6"): + @pytest.mark.parametrize("estimator, failed_tests", [ + (mf, mf._more_tags()['_xfail_checks']), + (rf, rf._more_tags()['_xfail_checks']), + (dtf, dtf._more_tags()['_xfail_checks']), + (cf, cf._more_tags()['_xfail_checks']), + ]) + def test_check_estimator_from_sklearn(estimator, failed_tests): + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) +else: + @pytest.mark.parametrize("estimator", [mf, rf, cf, dtf]) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) _estimators = [ MathFeatures(variables=["var_1", "var_2", "var_3"], func="mean"), From 684b8bc2f82b6df4293f452cb44fcb75949ea8fd Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 00:01:07 -0300 Subject: [PATCH 05/36] improves code style --- feature_engine/creation/base_creation.py | 5 ----- ...check_estimators_with_parametrize_tests.py | 3 ++- .../test_check_estimator_creation.py | 21 ++++++++++++------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/feature_engine/creation/base_creation.py b/feature_engine/creation/base_creation.py index 587ae9de6..a9df2dadb 100644 --- a/feature_engine/creation/base_creation.py +++ b/feature_engine/creation/base_creation.py @@ -21,11 +21,6 @@ find_numerical_variables, ) -try: - from sklearn.utils import Tags as _sklearn_Tags -except ImportError: - _sklearn_Tags = object - class BaseCreation(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """Shared set-up, checks and methods across creation transformers.""" diff --git a/tests/check_estimators_with_parametrize_tests.py b/tests/check_estimators_with_parametrize_tests.py index 62323273f..c0e213d61 100644 --- a/tests/check_estimators_with_parametrize_tests.py +++ b/tests/check_estimators_with_parametrize_tests.py @@ -1,6 +1,7 @@ """ This file is only intended to help understand check_estimator tests on Feature-engine -transformers. It is not run as part of the battery of acceptance tests. +transformers. It is not run as part of the battery of acceptance tests. Works up to +sklearn < 1.6. """ from sklearn.impute import SimpleImputer diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index 99daca479..ac1288a66 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -1,7 +1,6 @@ import pandas as pd -import sklearn import pytest - +import sklearn from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator from sklearn.utils.fixes import parse_version @@ -24,20 +23,26 @@ dtf = DecisionTreeFeatures(regression=False) if sklearn_version > parse_version("1.6"): - @pytest.mark.parametrize("estimator, failed_tests", [ - (mf, mf._more_tags()['_xfail_checks']), - (rf, rf._more_tags()['_xfail_checks']), - (dtf, dtf._more_tags()['_xfail_checks']), - (cf, cf._more_tags()['_xfail_checks']), - ]) + + @pytest.mark.parametrize( + "estimator, failed_tests", + [ + (mf, mf._more_tags()["_xfail_checks"]), + (rf, rf._more_tags()["_xfail_checks"]), + (dtf, dtf._more_tags()["_xfail_checks"]), + (cf, cf._more_tags()["_xfail_checks"]), + ], + ) def test_check_estimator_from_sklearn(estimator, failed_tests): return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) else: + @pytest.mark.parametrize("estimator", [mf, rf, cf, dtf]) def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) + _estimators = [ MathFeatures(variables=["var_1", "var_2", "var_3"], func="mean"), RelativeFeatures(variables=["var_1", "var_2"], reference=["var_3"], func=["add"]), From 20eb5eb112a2b6352b3e6e79bcd996c39d4eb3dc Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 00:08:10 -0300 Subject: [PATCH 06/36] add sklearn tag to dateimte transformer --- feature_engine/datetime/datetime.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/feature_engine/datetime/datetime.py b/feature_engine/datetime/datetime.py index cc1d93d75..acb096fb3 100644 --- a/feature_engine/datetime/datetime.py +++ b/feature_engine/datetime/datetime.py @@ -394,3 +394,7 @@ def _check_index_contains_na(self, index: pd.Index): def _more_tags(self): tags_dict = {"variables": "datetime"} return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags From 7a57b8bae56d89522a994ede955a93c64584da52 Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 00:27:30 -0300 Subject: [PATCH 07/36] makes discretization compatible with sklearn 1.6 --- feature_engine/discretisation/arbitrary.py | 4 ++ .../discretisation/decision_tree.py | 4 ++ tests/parametrize_with_checks_creation_v16.py | 22 +++++++--- ...ametrize_with_checks_discretization_v16.py | 41 +++++++++++++++++++ .../test_check_estimator_discretisers.py | 32 +++++++++++++-- 5 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 tests/parametrize_with_checks_discretization_v16.py diff --git a/feature_engine/discretisation/arbitrary.py b/feature_engine/discretisation/arbitrary.py index fc201dd3b..44d35ecdf 100644 --- a/feature_engine/discretisation/arbitrary.py +++ b/feature_engine/discretisation/arbitrary.py @@ -208,3 +208,7 @@ def _more_tags(self): "check_parameters_default_constructible" ] = "transformer has 1 mandatory parameter" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/discretisation/decision_tree.py b/feature_engine/discretisation/decision_tree.py index 13822650f..3e5d71ce5 100644 --- a/feature_engine/discretisation/decision_tree.py +++ b/feature_engine/discretisation/decision_tree.py @@ -345,3 +345,7 @@ def _more_tags(self): tags_dict["variables"] = "numerical" tags_dict["requires_y"] = True return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/tests/parametrize_with_checks_creation_v16.py b/tests/parametrize_with_checks_creation_v16.py index 2fd04930a..e1449f35f 100644 --- a/tests/parametrize_with_checks_creation_v16.py +++ b/tests/parametrize_with_checks_creation_v16.py @@ -1,3 +1,9 @@ +""" +File intended to help understand check_estimator tests for the module creation of +Feature-engine. It is not run as part of the battery of acceptance tests. Works from +sklearn > 1.6. +""" + from sklearn.utils.estimator_checks import parametrize_with_checks from feature_engine.creation import ( @@ -18,15 +24,19 @@ ) EXPECTED_FAILED_CHECKS = { - "DecisionTreeFeatures": dtf._more_tags()['_xfail_checks'], - "CyclicalFeatures": cf._more_tags()['_xfail_checks'], - "MathFeatures": mf._more_tags()['_xfail_checks'], - "RelativeFeatures": rf._more_tags()['_xfail_checks'], + "DecisionTreeFeatures": dtf._more_tags()["_xfail_checks"], + "CyclicalFeatures": cf._more_tags()["_xfail_checks"], + "MathFeatures": mf._more_tags()["_xfail_checks"], + "RelativeFeatures": rf._more_tags()["_xfail_checks"], } + + # creation @parametrize_with_checks( - estimators = [dtf, cf, mf, rf], - expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get(est.__class__.__name__, {}) + estimators=[dtf, cf, mf, rf], + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( + est.__class__.__name__, {} + ), ) def test_sklearn_compatible_creator(estimator, check): check(estimator) diff --git a/tests/parametrize_with_checks_discretization_v16.py b/tests/parametrize_with_checks_discretization_v16.py new file mode 100644 index 000000000..3ca7cdd0a --- /dev/null +++ b/tests/parametrize_with_checks_discretization_v16.py @@ -0,0 +1,41 @@ +""" +File intended to help understand check_estimator tests for Feature-engine's +discretization module. It is not run as part of the battery of acceptance tests. +Works from sklearn > 1.6. +""" + +import numpy as np +from sklearn.utils.estimator_checks import parametrize_with_checks + +from feature_engine.discretisation import ( + ArbitraryDiscretiser, + DecisionTreeDiscretiser, + EqualFrequencyDiscretiser, + EqualWidthDiscretiser, + GeometricWidthDiscretiser, +) + +dtd = DecisionTreeDiscretiser(regression=False) +efd = EqualFrequencyDiscretiser() +ewd = EqualWidthDiscretiser() +ad = ArbitraryDiscretiser(binning_dict={"x0": [-np.inf, 0, np.inf]}) +gd = GeometricWidthDiscretiser() + +EXPECTED_FAILED_CHECKS = { + "DecisionTreeDiscretiser": dtd._more_tags()["_xfail_checks"], + "EqualFrequencyDiscretiser": efd._more_tags()["_xfail_checks"], + "EqualWidthDiscretiser": ewd._more_tags()["_xfail_checks"], + "ArbitraryDiscretiser": ad._more_tags()["_xfail_checks"], + "GeometricWidthDiscretiser": gd._more_tags()["_xfail_checks"], +} + + +# creation +@parametrize_with_checks( + estimators=[dtd, efd, ewd, ad, gd], + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( + est.__class__.__name__, {} + ), +) +def test_sklearn_compatible_creator(estimator, check): + check(estimator) diff --git a/tests/test_discretisation/test_check_estimator_discretisers.py b/tests/test_discretisation/test_check_estimator_discretisers.py index c6a4951ff..60da4a80f 100644 --- a/tests/test_discretisation/test_check_estimator_discretisers.py +++ b/tests/test_discretisation/test_check_estimator_discretisers.py @@ -1,8 +1,10 @@ import numpy as np import pandas as pd import pytest +import sklearn from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.discretisation import ( ArbitraryDiscretiser, @@ -13,6 +15,9 @@ ) from tests.estimator_checks.estimator_checks import check_feature_engine_estimator +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + + _estimators = [ DecisionTreeDiscretiser(regression=False), EqualFrequencyDiscretiser(), @@ -21,10 +26,31 @@ GeometricWidthDiscretiser(), ] +if sklearn_version < parse_version("1.6"): -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + dtd = DecisionTreeDiscretiser(regression=False) + efd = EqualFrequencyDiscretiser() + ewd = EqualWidthDiscretiser() + ad = ArbitraryDiscretiser(binning_dict={"x0": [-np.inf, 0, np.inf]}) + gd = GeometricWidthDiscretiser() + + @pytest.mark.parametrize( + "estimator, failed_tests", + [ + (dtd, dtd._more_tags()["_xfail_checks"]), + (efd, efd._more_tags()["_xfail_checks"]), + (ewd, ewd._more_tags()["_xfail_checks"]), + (ad, ad._more_tags()["_xfail_checks"]), + (gd, gd._more_tags()["_xfail_checks"]), + ], + ) + def test_check_estimator_from_sklearn(estimator, failed_tests): + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) @pytest.mark.parametrize("estimator", _estimators) From ef1c257bbaca8101db94a6a3174c581abfd38373 Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 08:54:35 -0300 Subject: [PATCH 08/36] makes encoding module compatible with sklearn 1.6 --- feature_engine/encoding/base_encoder.py | 6 ++- feature_engine/encoding/count_frequency.py | 2 +- feature_engine/encoding/decision_tree.py | 2 +- feature_engine/encoding/mean_encoding.py | 6 ++- feature_engine/encoding/one_hot.py | 2 +- feature_engine/encoding/ordinal.py | 2 +- feature_engine/encoding/rare_label.py | 2 +- feature_engine/encoding/similarity_encoder.py | 2 +- feature_engine/encoding/woe.py | 2 +- ...ametrize_with_checks_discretization_v16.py | 2 +- tests/parametrize_with_checks_encoders_v16.py | 54 +++++++++++++++++++ .../test_check_estimator_encoders.py | 43 +++++++++++++-- 12 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 tests/parametrize_with_checks_encoders_v16.py diff --git a/feature_engine/encoding/base_encoder.py b/feature_engine/encoding/base_encoder.py index 0c9806803..b4ae3478f 100644 --- a/feature_engine/encoding/base_encoder.py +++ b/feature_engine/encoding/base_encoder.py @@ -102,7 +102,7 @@ def __init__( self.missing_values = missing_values -class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class CategoricalMethodsMixin(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """Shared methods across categorical transformers. - BaseEstimator brings methods get_params() and set_params(). @@ -299,3 +299,7 @@ def _more_tags(self): # so we need to leave without this test tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/encoding/count_frequency.py b/feature_engine/encoding/count_frequency.py index 2f81456d8..ae6507627 100644 --- a/feature_engine/encoding/count_frequency.py +++ b/feature_engine/encoding/count_frequency.py @@ -49,7 +49,7 @@ transform=_transform_encoders_docstring, inverse_transform=_inverse_transform_docstring, ) -class CountFrequencyEncoder(CategoricalInitMixinNA, CategoricalMethodsMixin): +class CountFrequencyEncoder(CategoricalMethodsMixin, CategoricalInitMixinNA): """ The CountFrequencyEncoder() replaces categories by either the count or the percentage of observations per category. diff --git a/feature_engine/encoding/decision_tree.py b/feature_engine/encoding/decision_tree.py index b86d4794e..63b5edbac 100644 --- a/feature_engine/encoding/decision_tree.py +++ b/feature_engine/encoding/decision_tree.py @@ -52,7 +52,7 @@ fit_transform=_fit_transform_docstring, inverse_transform=_inverse_transform_docstring, ) -class DecisionTreeEncoder(CategoricalInitMixin, CategoricalMethodsMixin): +class DecisionTreeEncoder(CategoricalMethodsMixin, CategoricalInitMixin): """ The DecisionTreeEncoder() encodes categorical variables with the predictions of a decision tree. diff --git a/feature_engine/encoding/mean_encoding.py b/feature_engine/encoding/mean_encoding.py index 936c7ff01..bdcf160d4 100644 --- a/feature_engine/encoding/mean_encoding.py +++ b/feature_engine/encoding/mean_encoding.py @@ -48,7 +48,7 @@ transform=_transform_encoders_docstring, inverse_transform=_inverse_transform_docstring, ) -class MeanEncoder(CategoricalInitMixinNA, CategoricalMethodsMixin): +class MeanEncoder(CategoricalMethodsMixin, CategoricalInitMixinNA): """ The MeanEncoder() replaces categories by the mean value of the target for each category. @@ -266,3 +266,7 @@ def _more_tags(self): tags_dict = super()._more_tags() tags_dict["requires_y"] = True return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py index de62e44c9..e94432a3d 100644 --- a/feature_engine/encoding/one_hot.py +++ b/feature_engine/encoding/one_hot.py @@ -32,7 +32,7 @@ n_features_in_=_n_features_in_docstring, fit_transform=_fit_transform_docstring, ) -class OneHotEncoder(CategoricalInitMixin, CategoricalMethodsMixin): +class OneHotEncoder(CategoricalMethodsMixin, CategoricalInitMixin): """ The OneHotEncoder() replaces categorical variables by a set of binary variables representing each one of the unique categories in the variable. diff --git a/feature_engine/encoding/ordinal.py b/feature_engine/encoding/ordinal.py index 698b4d6e0..bff179e22 100644 --- a/feature_engine/encoding/ordinal.py +++ b/feature_engine/encoding/ordinal.py @@ -48,7 +48,7 @@ transform=_transform_encoders_docstring, inverse_transform=_inverse_transform_docstring, ) -class OrdinalEncoder(CategoricalInitMixinNA, CategoricalMethodsMixin): +class OrdinalEncoder(CategoricalMethodsMixin, CategoricalInitMixinNA): """ The OrdinalEncoder() replaces categories by ordinal numbers (0, 1, 2, 3, etc). The numbers can be ordered based on the mean of the target diff --git a/feature_engine/encoding/rare_label.py b/feature_engine/encoding/rare_label.py index 90119a9de..8a57f9fa2 100644 --- a/feature_engine/encoding/rare_label.py +++ b/feature_engine/encoding/rare_label.py @@ -35,7 +35,7 @@ n_features_in_=_n_features_in_docstring, fit_transform=_fit_transform_docstring, ) -class RareLabelEncoder(CategoricalInitMixinNA, CategoricalMethodsMixin): +class RareLabelEncoder(CategoricalMethodsMixin, CategoricalInitMixinNA): """ The RareLabelEncoder() groups rare or infrequent categories in a new category called "Rare", or any other name entered by the user. diff --git a/feature_engine/encoding/similarity_encoder.py b/feature_engine/encoding/similarity_encoder.py index 872cf02df..137034ddb 100644 --- a/feature_engine/encoding/similarity_encoder.py +++ b/feature_engine/encoding/similarity_encoder.py @@ -38,7 +38,7 @@ def _gpm_fast(x1: str, x2: str) -> float: n_features_in_=_n_features_in_docstring, fit_transform=_fit_transform_docstring, ) -class StringSimilarityEncoder(CategoricalInitMixin, CategoricalMethodsMixin): +class StringSimilarityEncoder(CategoricalMethodsMixin, CategoricalInitMixin): """ The StringSimilarityEncoder() replaces categorical variables with a set of float variables that capture the similarity between the category names. The new variables diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py index 78069b300..e46f35708 100644 --- a/feature_engine/encoding/woe.py +++ b/feature_engine/encoding/woe.py @@ -94,7 +94,7 @@ def _calculate_woe( transform=_transform_encoders_docstring, inverse_transform=_inverse_transform_docstring, ) -class WoEEncoder(CategoricalInitMixin, CategoricalMethodsMixin, WoE): +class WoEEncoder(CategoricalMethodsMixin, CategoricalInitMixin, WoE): """ The WoEEncoder() replaces categories by the weight of evidence (WoE). The WoE was used primarily in the financial sector to create credit risk diff --git a/tests/parametrize_with_checks_discretization_v16.py b/tests/parametrize_with_checks_discretization_v16.py index 3ca7cdd0a..dd46349f4 100644 --- a/tests/parametrize_with_checks_discretization_v16.py +++ b/tests/parametrize_with_checks_discretization_v16.py @@ -30,7 +30,7 @@ } -# creation +# discretization @parametrize_with_checks( estimators=[dtd, efd, ewd, ad, gd], expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( diff --git a/tests/parametrize_with_checks_encoders_v16.py b/tests/parametrize_with_checks_encoders_v16.py new file mode 100644 index 000000000..5fa3b689a --- /dev/null +++ b/tests/parametrize_with_checks_encoders_v16.py @@ -0,0 +1,54 @@ +""" +File intended to help understand check_estimator tests for Feature-engine's +encoding module. It is not run as part of the battery of acceptance tests. +Works from sklearn > 1.6. +""" + +from sklearn.utils.estimator_checks import parametrize_with_checks + +from feature_engine.encoding import ( + CountFrequencyEncoder, + MeanEncoder, + OneHotEncoder, + OrdinalEncoder, + RareLabelEncoder, + StringSimilarityEncoder, + WoEEncoder, +) +from feature_engine.tags import _return_tags + +ce = CountFrequencyEncoder(ignore_format=True) +me = MeanEncoder(ignore_format=True) +ohe = OneHotEncoder(ignore_format=True) +oe = OrdinalEncoder(ignore_format=True) +re = RareLabelEncoder( + tol=0.00000000001, + n_categories=100000000000, + replace_with=10, + ignore_format=True, +) +woe = WoEEncoder(ignore_format=True) +sse = StringSimilarityEncoder(ignore_format=True) + +FAILED_CHECKS = _return_tags()["_xfail_checks"] +FAILED_CHECKS.update({"check_estimators_nan_inf": "transformer allows NA"}) + +EXPECTED_FAILED_CHECKS = { + "CountFrequencyEncoder": FAILED_CHECKS, + "MeanEncoder": FAILED_CHECKS, + "OneHotEncoder": FAILED_CHECKS, + "OrdinalEncoder": FAILED_CHECKS, + "RareLabelEncoder": FAILED_CHECKS, + "StringSimilarityEncoder": FAILED_CHECKS, +} + + +# encoding +@parametrize_with_checks( + estimators=[ce, me, ohe, oe, re, woe, sse], + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( + est.__class__.__name__, {} + ), +) +def test_sklearn_compatible_creator(estimator, check): + check(estimator) diff --git a/tests/test_encoding/test_check_estimator_encoders.py b/tests/test_encoding/test_check_estimator_encoders.py index 920f3c4d2..fd82e70ea 100644 --- a/tests/test_encoding/test_check_estimator_encoders.py +++ b/tests/test_encoding/test_check_estimator_encoders.py @@ -1,10 +1,12 @@ import pandas as pd import pytest +import sklearn from numpy import nan from sklearn import clone from sklearn.exceptions import NotFittedError from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.encoding import ( CountFrequencyEncoder, @@ -16,11 +18,14 @@ StringSimilarityEncoder, WoEEncoder, ) +from feature_engine.tags import _return_tags from tests.estimator_checks.estimator_checks import ( check_feature_engine_estimator, test_df, ) +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + _estimators = [ CountFrequencyEncoder(ignore_format=True), # breaks with sklearn 1.4.1 - check and fix? @@ -39,9 +44,41 @@ ] -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +if sklearn_version < parse_version("1.6"): + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + ce = CountFrequencyEncoder(ignore_format=True) + me = MeanEncoder(ignore_format=True) + ohe = OneHotEncoder(ignore_format=True) + oe = OrdinalEncoder(ignore_format=True) + re = RareLabelEncoder( + tol=0.00000000001, + n_categories=100000000000, + replace_with=10, + ignore_format=True, + ) + woe = WoEEncoder(ignore_format=True) + sse = StringSimilarityEncoder(ignore_format=True) + + expected_fails = _return_tags()["_xfail_checks"] + expected_fails.update({"check_estimators_nan_inf": "transformer allows NA"}) + + @pytest.mark.parametrize( + "estimator, failed_tests", + [ + (ce, expected_fails), + (me, expected_fails), + (ohe, expected_fails), + (oe, expected_fails), + (re, expected_fails), + ], + ) + def test_check_estimator_from_sklearn(estimator, failed_tests): + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) _estimators = [ From 62b8f065c73bfbc7f744ec348eeac27fd3d2abc2 Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 14:26:49 -0300 Subject: [PATCH 09/36] make imputation module compatible --- feature_engine/imputation/base_imputer.py | 7 +++- feature_engine/imputation/categorical.py | 5 +++ .../imputation/drop_missing_data.py | 5 +++ .../imputation/missing_indicator.py | 5 +++ feature_engine/imputation/random_sample.py | 5 +++ .../test_check_estimator_imputers.py | 35 +++++++++++++++++-- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/feature_engine/imputation/base_imputer.py b/feature_engine/imputation/base_imputer.py index 522536b57..6ab856e7a 100644 --- a/feature_engine/imputation/base_imputer.py +++ b/feature_engine/imputation/base_imputer.py @@ -7,7 +7,7 @@ from feature_engine.tags import _return_tags -class BaseImputer(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class BaseImputer(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """shared set-up checks and methods across imputers""" def _transform(self, X: pd.DataFrame) -> pd.DataFrame: @@ -78,3 +78,8 @@ def _more_tags(self): tags_dict["allow_nan"] = True tags_dict["variables"] = "numerical" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags diff --git a/feature_engine/imputation/categorical.py b/feature_engine/imputation/categorical.py index d0300f7d0..8c4000a0c 100644 --- a/feature_engine/imputation/categorical.py +++ b/feature_engine/imputation/categorical.py @@ -257,3 +257,8 @@ def _more_tags(self): tags_dict["allow_nan"] = True tags_dict["variables"] = "categorical" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags diff --git a/feature_engine/imputation/drop_missing_data.py b/feature_engine/imputation/drop_missing_data.py index 8d568fb4d..07c6f3e75 100644 --- a/feature_engine/imputation/drop_missing_data.py +++ b/feature_engine/imputation/drop_missing_data.py @@ -223,3 +223,8 @@ def _more_tags(self): tags_dict["allow_nan"] = True tags_dict["variables"] = "all" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags diff --git a/feature_engine/imputation/missing_indicator.py b/feature_engine/imputation/missing_indicator.py index 9e827f0f2..323b6542c 100644 --- a/feature_engine/imputation/missing_indicator.py +++ b/feature_engine/imputation/missing_indicator.py @@ -179,3 +179,8 @@ def _more_tags(self): tags_dict["allow_nan"] = True tags_dict["variables"] = "all" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags \ No newline at end of file diff --git a/feature_engine/imputation/random_sample.py b/feature_engine/imputation/random_sample.py index bac661631..d05aeaac8 100644 --- a/feature_engine/imputation/random_sample.py +++ b/feature_engine/imputation/random_sample.py @@ -275,3 +275,8 @@ def _more_tags(self): tags_dict["allow_nan"] = True tags_dict["variables"] = "all" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags diff --git a/tests/test_imputation/test_check_estimator_imputers.py b/tests/test_imputation/test_check_estimator_imputers.py index 9496ce489..04abc9c0a 100644 --- a/tests/test_imputation/test_check_estimator_imputers.py +++ b/tests/test_imputation/test_check_estimator_imputers.py @@ -1,7 +1,9 @@ import pandas as pd import pytest +import sklearn from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.imputation import ( AddMissingIndicator, @@ -24,10 +26,37 @@ DropMissingData(), ] +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +if sklearn_version < parse_version("1.6"): + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + mi = MeanMedianImputer() + ai = ArbitraryNumberImputer() + ci = CategoricalImputer(fill_value=0, ignore_format=True) + eti = EndTailImputer() + ami = AddMissingIndicator() + rsi = RandomSampleImputer() + dmd = DropMissingData() + + @pytest.mark.parametrize( + "estimator, failed_tests", + [ + (mi, mi._more_tags()["_xfail_checks"]), + (ai, ai._more_tags()["_xfail_checks"]), + (ci, ci._more_tags()["_xfail_checks"]), + (eti, eti._more_tags()["_xfail_checks"]), + (ami, ami._more_tags()["_xfail_checks"]), + (rsi, rsi._more_tags()["_xfail_checks"]), + (dmd, dmd._more_tags()["_xfail_checks"]), + ], + ) + def test_check_estimator_from_sklearn(estimator, failed_tests): + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) @pytest.mark.parametrize("estimator", _estimators) From 06bbb2612a72274af00165163b47d941dbf77824 Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 14:45:42 -0300 Subject: [PATCH 10/36] make outlier module compatible --- feature_engine/outliers/artbitrary.py | 4 ++ feature_engine/outliers/base_outlier.py | 13 ++++- tests/parametrize_with_checks_outliers_v16.py | 46 ++++++++++++++++++ .../test_check_estimator_outliers.py | 48 +++++++++++++++++-- 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 tests/parametrize_with_checks_outliers_v16.py diff --git a/feature_engine/outliers/artbitrary.py b/feature_engine/outliers/artbitrary.py index 7f06a70fa..87ec4a709 100644 --- a/feature_engine/outliers/artbitrary.py +++ b/feature_engine/outliers/artbitrary.py @@ -204,3 +204,7 @@ def _more_tags(self): "check_parameters_default_constructible" ] = "transformer has 1 mandatory parameter" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/outliers/base_outlier.py b/feature_engine/outliers/base_outlier.py index 73dfedffc..267680e7a 100644 --- a/feature_engine/outliers/base_outlier.py +++ b/feature_engine/outliers/base_outlier.py @@ -21,7 +21,7 @@ ) -class BaseOutlier(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class BaseOutlier(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """shared set-up checks and methods across outlier transformers""" def _check_transform_input_and_state(self, X: pd.DataFrame) -> pd.DataFrame: @@ -96,6 +96,11 @@ def _more_tags(self): tags_dict["variables"] = "numerical" return tags_dict + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags + + class WinsorizerBase(BaseOutlier): @@ -289,7 +294,7 @@ def _more_tags(self): tags_dict = _return_tags() tags_dict["variables"] = "numerical" # ======= this tests fail because the transformers throw an error - # when variance of the any input feature is 0. + # when variance of any input feature is 0. # Nothing to do with the test itself but # mostly with the data created and used in the test msg = ( @@ -298,3 +303,7 @@ def _more_tags(self): ) tags_dict["_xfail_checks"]["check_fit2d_1sample"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/tests/parametrize_with_checks_outliers_v16.py b/tests/parametrize_with_checks_outliers_v16.py new file mode 100644 index 000000000..bc74e41f0 --- /dev/null +++ b/tests/parametrize_with_checks_outliers_v16.py @@ -0,0 +1,46 @@ +""" +File intended to help understand check_estimator tests for Feature-engine's +encoding module. It is not run as part of the battery of acceptance tests. +Works from sklearn > 1.6. +""" + +from sklearn.utils.estimator_checks import parametrize_with_checks + +from feature_engine.outliers import ArbitraryOutlierCapper, OutlierTrimmer, Winsorizer +from feature_engine.tags import _return_tags + +aoc = ArbitraryOutlierCapper(max_capping_dict={"x0": 10}) +ot = OutlierTrimmer() +wz = Winsorizer() + +FAILED_CHECKS = _return_tags()["_xfail_checks"] +FAILED_CHECKS_AOC = _return_tags()["_xfail_checks"] + +msg1 = "transformers raise errors when data variation is low, " "thus this check fails" + +msg2 = "transformer has 1 mandatory parameter" + +FAILED_CHECKS.update({"check_fit2d_1sample": msg1}) +FAILED_CHECKS_AOC.update( + { + "check_fit2d_1sample": msg1, + "check_parameters_default_constructible": msg2, + } +) + +EXPECTED_FAILED_CHECKS = { + "ArbitraryOutlierCapper": FAILED_CHECKS_AOC, + "OutlierTrimmer": FAILED_CHECKS, + "Winsorizer": FAILED_CHECKS, +} + + +# encoding +@parametrize_with_checks( + estimators=[aoc, ot, wz], + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( + est.__class__.__name__, {} + ), +) +def test_sklearn_compatible_creator(estimator, check): + check(estimator) diff --git a/tests/test_outliers/test_check_estimator_outliers.py b/tests/test_outliers/test_check_estimator_outliers.py index 243df1567..1b41e9879 100644 --- a/tests/test_outliers/test_check_estimator_outliers.py +++ b/tests/test_outliers/test_check_estimator_outliers.py @@ -1,10 +1,14 @@ import pandas as pd import pytest +import sklearn from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.outliers import ArbitraryOutlierCapper, OutlierTrimmer, Winsorizer +from feature_engine.tags import _return_tags from tests.estimator_checks.estimator_checks import check_feature_engine_estimator +from tests.parametrize_with_checks_discretization_v16 import EXPECTED_FAILED_CHECKS _estimators = [ ArbitraryOutlierCapper(max_capping_dict={"x0": 10}), @@ -12,10 +16,46 @@ Winsorizer(), ] - -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + +if sklearn_version < parse_version("1.6"): + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + aoc = ArbitraryOutlierCapper(max_capping_dict={"x0": 10}) + ot = OutlierTrimmer() + wz = Winsorizer() + + FAILED_CHECKS = _return_tags()["_xfail_checks"] + FAILED_CHECKS_AOC = _return_tags()["_xfail_checks"] + + msg1 = ( + "transformers raise errors when data variation is low, " "thus this check fails" + ) + + msg2 = "transformer has 1 mandatory parameter" + + FAILED_CHECKS.update({"check_fit2d_1sample": msg1}) + FAILED_CHECKS_AOC.update( + { + "check_fit2d_1sample": msg1, + "check_parameters_default_constructible": msg2, + } + ) + + @pytest.mark.parametrize( + "estimator, failed_tests", + [ + (aoc, FAILED_CHECKS_AOC), + (ot, FAILED_CHECKS), + (wz, FAILED_CHECKS), + ], + ) + def test_check_estimator_from_sklearn(estimator, failed_tests): + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) @pytest.mark.parametrize("estimator", _estimators) From a2f71fba60954f30fbaa84538789ef3e7e771605 Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 16:39:11 -0300 Subject: [PATCH 11/36] make provisory fix for prediction module --- feature_engine/_prediction/base_predictor.py | 4 +++ .../_prediction/target_mean_classifier.py | 7 +++- .../_prediction/target_mean_regressor.py | 2 +- tests/parametrize_with_checks_outliers_v16.py | 2 +- .../parametrize_with_checks_prediction_v16.py | 32 +++++++++++++++++++ .../test_check_estimator_prediction.py | 15 ++++++--- 6 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/parametrize_with_checks_prediction_v16.py diff --git a/feature_engine/_prediction/base_predictor.py b/feature_engine/_prediction/base_predictor.py index 2909de985..c7e2618fd 100644 --- a/feature_engine/_prediction/base_predictor.py +++ b/feature_engine/_prediction/base_predictor.py @@ -298,3 +298,7 @@ def _predict(self, X: pd.DataFrame) -> np.ndarray: def _more_tags(self): return _return_tags() + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/_prediction/target_mean_classifier.py b/feature_engine/_prediction/target_mean_classifier.py index 3338f3f46..bc88b0c2f 100644 --- a/feature_engine/_prediction/target_mean_classifier.py +++ b/feature_engine/_prediction/target_mean_classifier.py @@ -6,7 +6,7 @@ from feature_engine._prediction.base_predictor import BaseTargetMeanEstimator -class TargetMeanClassifier(BaseTargetMeanEstimator, ClassifierMixin): +class TargetMeanClassifier(ClassifierMixin, BaseTargetMeanEstimator): """ The TargetMeanClassifier() estimates target values based on the average of the mean target value per category or bin of a group of categorical and numerical variables. @@ -185,3 +185,8 @@ def predict(self, X: pd.DataFrame) -> np.ndarray: """ y_pred = np.where(self._predict(X) > 0.5, self.classes_[1], self.classes_[0]) return y_pred + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.classifier_tags.multi_class = False + return tags diff --git a/feature_engine/_prediction/target_mean_regressor.py b/feature_engine/_prediction/target_mean_regressor.py index b220fc2f3..26fa27875 100644 --- a/feature_engine/_prediction/target_mean_regressor.py +++ b/feature_engine/_prediction/target_mean_regressor.py @@ -6,7 +6,7 @@ from feature_engine._prediction.base_predictor import BaseTargetMeanEstimator -class TargetMeanRegressor(BaseTargetMeanEstimator, RegressorMixin): +class TargetMeanRegressor(RegressorMixin, BaseTargetMeanEstimator): """ The TargetMeanRegressor() outputs a target estimation based on the mean target value per category or bin, across a group of categorical or numerical variables. diff --git a/tests/parametrize_with_checks_outliers_v16.py b/tests/parametrize_with_checks_outliers_v16.py index bc74e41f0..0dd4d06c2 100644 --- a/tests/parametrize_with_checks_outliers_v16.py +++ b/tests/parametrize_with_checks_outliers_v16.py @@ -1,6 +1,6 @@ """ File intended to help understand check_estimator tests for Feature-engine's -encoding module. It is not run as part of the battery of acceptance tests. +outliers module. It is not run as part of the battery of acceptance tests. Works from sklearn > 1.6. """ diff --git a/tests/parametrize_with_checks_prediction_v16.py b/tests/parametrize_with_checks_prediction_v16.py new file mode 100644 index 000000000..fdb884c11 --- /dev/null +++ b/tests/parametrize_with_checks_prediction_v16.py @@ -0,0 +1,32 @@ +""" +File intended to help understand check_estimator tests for Feature-engine's +prediction module. It is not run as part of the battery of acceptance tests. +Works from sklearn > 1.6. +""" + +from sklearn.utils.estimator_checks import parametrize_with_checks + +from feature_engine._prediction.base_predictor import BaseTargetMeanEstimator +from feature_engine._prediction.target_mean_classifier import TargetMeanClassifier +from feature_engine._prediction.target_mean_regressor import TargetMeanRegressor +from feature_engine.tags import _return_tags + +_estimators = [BaseTargetMeanEstimator(), TargetMeanClassifier(), TargetMeanRegressor()] + +FAILED_CHECKS = _return_tags()["_xfail_checks"] + +EXPECTED_FAILED_CHECKS = { + "BaseTargetMeanEstimator": FAILED_CHECKS, + "TargetMeanClassifier": FAILED_CHECKS, + "TargetMeanRegressor": FAILED_CHECKS, +} + + +@parametrize_with_checks( + estimators=_estimators, + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( + est.__class__.__name__, {} + ), +) +def test_sklearn_compatible_creator(estimator, check): + check(estimator) diff --git a/tests/test_prediction/test_check_estimator_prediction.py b/tests/test_prediction/test_check_estimator_prediction.py index 62a8ae5f2..e3762f265 100644 --- a/tests/test_prediction/test_check_estimator_prediction.py +++ b/tests/test_prediction/test_check_estimator_prediction.py @@ -1,9 +1,11 @@ import numpy as np import pandas as pd import pytest +import sklearn from sklearn.base import clone from sklearn.exceptions import NotFittedError from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine._prediction.base_predictor import BaseTargetMeanEstimator from feature_engine._prediction.target_mean_classifier import TargetMeanClassifier @@ -16,14 +18,17 @@ from tests.estimator_checks.dataframe_for_checks import test_df from tests.estimator_checks.fit_functionality_checks import check_error_if_y_not_passed +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + _estimators = [BaseTargetMeanEstimator(), TargetMeanClassifier(), TargetMeanRegressor()] _predictors = [TargetMeanRegressor(), TargetMeanClassifier()] - -# sklearn check_estimator -@pytest.mark.parametrize("estimator", [BaseTargetMeanEstimator()]) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +if sklearn_version < parse_version("1.6"): + # In sklearn version 1.6, changes into the developer api were introduced + # that break the tests. Need to dig further into it. + @pytest.mark.parametrize("estimator", [BaseTargetMeanEstimator()]) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) @pytest.mark.parametrize("estimator", _estimators) From fcb1c7247fed16281107c104063bbc9fb5683d5c Mon Sep 17 00:00:00 2001 From: solegalli Date: Fri, 17 Jan 2025 16:50:40 -0300 Subject: [PATCH 12/36] make preprocessing module compatible --- .../preprocessing/match_categories.py | 2 +- feature_engine/preprocessing/match_columns.py | 6 ++- .../test_check_estimator_outliers.py | 1 - .../test_check_estimator_preprocessing.py | 40 +++++++++++++++++-- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/feature_engine/preprocessing/match_categories.py b/feature_engine/preprocessing/match_categories.py index e4be96251..a41c02852 100644 --- a/feature_engine/preprocessing/match_categories.py +++ b/feature_engine/preprocessing/match_categories.py @@ -31,7 +31,7 @@ n_features_in_=_n_features_in_docstring, ) class MatchCategories( - CategoricalInitMixinNA, CategoricalMethodsMixin, GetFeatureNamesOutMixin + CategoricalMethodsMixin, CategoricalInitMixinNA, GetFeatureNamesOutMixin ): """ MatchCategories() ensures that categorical variables are encoded as pandas diff --git a/feature_engine/preprocessing/match_columns.py b/feature_engine/preprocessing/match_columns.py index b39f5e07a..c5321b6c3 100644 --- a/feature_engine/preprocessing/match_columns.py +++ b/feature_engine/preprocessing/match_columns.py @@ -10,7 +10,7 @@ from feature_engine.tags import _return_tags -class MatchVariables(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class MatchVariables(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """ MatchVariables() ensures that the same variables observed in the train set are present in the test set. If the dataset to transform contains variables that @@ -297,3 +297,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/tests/test_outliers/test_check_estimator_outliers.py b/tests/test_outliers/test_check_estimator_outliers.py index 1b41e9879..a5c117355 100644 --- a/tests/test_outliers/test_check_estimator_outliers.py +++ b/tests/test_outliers/test_check_estimator_outliers.py @@ -8,7 +8,6 @@ from feature_engine.outliers import ArbitraryOutlierCapper, OutlierTrimmer, Winsorizer from feature_engine.tags import _return_tags from tests.estimator_checks.estimator_checks import check_feature_engine_estimator -from tests.parametrize_with_checks_discretization_v16 import EXPECTED_FAILED_CHECKS _estimators = [ ArbitraryOutlierCapper(max_capping_dict={"x0": 10}), diff --git a/tests/test_preprocessing/test_check_estimator_preprocessing.py b/tests/test_preprocessing/test_check_estimator_preprocessing.py index 044fb5dbe..378091840 100644 --- a/tests/test_preprocessing/test_check_estimator_preprocessing.py +++ b/tests/test_preprocessing/test_check_estimator_preprocessing.py @@ -1,23 +1,57 @@ import pandas as pd import pytest +import sklearn from numpy import nan from sklearn import clone from sklearn.exceptions import NotFittedError from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.preprocessing import MatchCategories, MatchVariables +from feature_engine.tags import _return_tags from tests.estimator_checks.estimator_checks import ( check_feature_engine_estimator, test_df, ) +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + _estimators = [MatchCategories(ignore_format=True), MatchVariables()] +if sklearn_version < parse_version("1.6"): -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + FAILED_CHECKS = _return_tags()["_xfail_checks"] + FAILED_CHECKS_MATCHCOLS = _return_tags()["_xfail_checks"] + + msg1 = "input shape of dataframes in fit and transform can differ" + msg2 = ( + "transformer takes categorical variables, and inf cannot be determined" + "on these variables. Thus, check is not implemented" + ) + + FAILED_CHECKS.update({"check_estimators_nan_inf": msg2}) + FAILED_CHECKS_MATCHCOLS.update( + { + "check_transformer_general": msg1, + "check_estimators_nan_inf": msg2, + } + ) + + @pytest.mark.parametrize( + "estimator, failed_tests", + [ + (_estimators[0], FAILED_CHECKS), + (_estimators[1], FAILED_CHECKS_MATCHCOLS), + ], + ) + def test_check_estimator_from_sklearn(estimator, failed_tests): + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) @pytest.mark.parametrize("estimator", [MatchCategories(), MatchVariables()]) From d1b5c9e81add081dd4c6a3773a5cc42ceb97e5ce Mon Sep 17 00:00:00 2001 From: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:00:12 +0100 Subject: [PATCH 13/36] make transformation module compatible with sklearn_tags (#836) * make transformation module compatible * Remove positive only tag from more_tags * Revert adding input tag positive only --- feature_engine/transformation/arcsin.py | 3 ++ feature_engine/transformation/boxcox.py | 4 ++ feature_engine/transformation/log.py | 7 +++ feature_engine/transformation/reciprocal.py | 3 ++ feature_engine/transformation/yeojohnson.py | 3 ++ .../test_check_estimator_transformers.py | 51 +++++++++++++++++-- 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/feature_engine/transformation/arcsin.py b/feature_engine/transformation/arcsin.py index 95ace06df..059df813e 100644 --- a/feature_engine/transformation/arcsin.py +++ b/feature_engine/transformation/arcsin.py @@ -206,3 +206,6 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_n_features_in"] = msg return tags_dict + + def __sklearn_tags__(self): + return super().__sklearn_tags__() diff --git a/feature_engine/transformation/boxcox.py b/feature_engine/transformation/boxcox.py index d50319563..1541ff8b5 100644 --- a/feature_engine/transformation/boxcox.py +++ b/feature_engine/transformation/boxcox.py @@ -218,3 +218,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1sample"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index 66c60f3df..f7a47a72f 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -215,6 +215,10 @@ def _more_tags(self): return tags_dict + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags + @Substitution( variables_=_variables_attribute_docstring, @@ -434,3 +438,6 @@ def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame: X.loc[:, self.variables_] = 10 ** X.loc[:, self.variables_] - self.C_ return X + + def __sklearn_tags__(self): + return super().__sklearn_tags__() diff --git a/feature_engine/transformation/reciprocal.py b/feature_engine/transformation/reciprocal.py index dad2c1840..d51557331 100644 --- a/feature_engine/transformation/reciprocal.py +++ b/feature_engine/transformation/reciprocal.py @@ -188,3 +188,6 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_transformer_general"] = msg return tags_dict + + def __sklearn_tags__(self): + return super().__sklearn_tags__() diff --git a/feature_engine/transformation/yeojohnson.py b/feature_engine/transformation/yeojohnson.py index f87d09917..f8d938e4a 100644 --- a/feature_engine/transformation/yeojohnson.py +++ b/feature_engine/transformation/yeojohnson.py @@ -216,3 +216,6 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1sample"] = msg return tags_dict + + def __sklearn_tags__(self): + return super().__sklearn_tags__() diff --git a/tests/test_transformation/test_check_estimator_transformers.py b/tests/test_transformation/test_check_estimator_transformers.py index 4f230dd6a..7db0088f8 100644 --- a/tests/test_transformation/test_check_estimator_transformers.py +++ b/tests/test_transformation/test_check_estimator_transformers.py @@ -1,7 +1,9 @@ import pandas as pd import pytest +import sklearn from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.transformation import ( ArcsinTransformer, @@ -24,10 +26,53 @@ YeoJohnsonTransformer(), ] +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +if sklearn_version < parse_version("1.6"): + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + checks_with_negative_values = [ + "check_readonly_memmap_input", + "check_fit_score_takes_y", + "check_dont_overwrite_parameters", + "check_estimators_nan_inf", + "check_f_contiguous_array_estimator", + "check_fit2d_1feature", + "check_fit2d_1sample", + "check_dict_unchanged", + "check_fit_check_is_fitted", + "check_n_features_in", + "check_positive_only_tag_during_fit", + "check_methods_subset_invariance", + ] + estimators_not_supporting_negative_values = [ + "BoxCoxTransformer", + "LogTransformer", + "ArcsinTransformer", + ] + extra_failing_checks = { + estimator_name: { + check: "this checks passes a negative value which is not supported by the " + "transformer" + for check in checks_with_negative_values + } + for estimator_name in estimators_not_supporting_negative_values + } + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + expected_failed_checks = estimator._more_tags()["_xfail_checks"] + expected_failed_checks.update( + extra_failing_checks.get(estimator.__class__.__name__, {}) + ) + return check_estimator( + estimator=estimator, + expected_failed_checks=expected_failed_checks, + ) @pytest.mark.parametrize("estimator", _estimators[4:]) From f9145d61665c3181732576a8153cc69e7696b644 Mon Sep 17 00:00:00 2001 From: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:03:52 +0100 Subject: [PATCH 14/36] make time_series module compatible (#834) * make time_series module compatible * fix typo * refactor test code --- .../forecasting/base_forecast_transformers.py | 6 ++++- .../test_check_estimator_forecasting.py | 27 ++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/feature_engine/timeseries/forecasting/base_forecast_transformers.py b/feature_engine/timeseries/forecasting/base_forecast_transformers.py index 487a297ab..c8fcee7f7 100644 --- a/feature_engine/timeseries/forecasting/base_forecast_transformers.py +++ b/feature_engine/timeseries/forecasting/base_forecast_transformers.py @@ -42,7 +42,7 @@ n_features_in_=_n_features_in_docstring, ) class BaseForecastTransformer( - BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin, TransformXyMixin + TransformerMixin, GetFeatureNamesOutMixin, TransformXyMixin, BaseEstimator ): """ Shared methods across time-series forecasting transformers. @@ -234,3 +234,7 @@ def _more_tags(self): "check_methods_subset_invariance" ] = "LagFeatures is not invariant when applied to a subset. Not sure why yet" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py b/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py index 2ac81edad..f9905a4d0 100644 --- a/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py +++ b/tests/test_time_series/test_forecasting/test_check_estimator_forecasting.py @@ -1,9 +1,11 @@ import numpy as np import pandas as pd import pytest +import sklearn from sklearn.base import clone from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.timeseries.forecasting import ( ExpandingWindowFeatures, @@ -19,9 +21,28 @@ ] -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + +if sklearn_version < parse_version("1.6"): + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + extra_failing_checks = { + "check_estimators_nan_inf": "Time Series transformers do not handle NaNs " + "or infinity." + } + return check_estimator( + estimator=estimator, + expected_failed_checks={ + **extra_failing_checks, + **estimator._more_tags()["_xfail_checks"], + }, + ) @pytest.mark.parametrize("estimator", _estimators) From 8cb2c21221c39efc99578b6a48966dee4da945bb Mon Sep 17 00:00:00 2001 From: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:11:16 +0100 Subject: [PATCH 15/36] make wrappers module compatible (#835) * make wrappers module compatible * Add missing new line * Sort imports in test file --- feature_engine/wrappers/wrappers.py | 5 ++++- .../test_check_estimator_wrappers.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/feature_engine/wrappers/wrappers.py b/feature_engine/wrappers/wrappers.py index e6bbc8cb7..6787ede9e 100644 --- a/feature_engine/wrappers/wrappers.py +++ b/feature_engine/wrappers/wrappers.py @@ -70,7 +70,7 @@ ] -class SklearnTransformerWrapper(BaseEstimator, TransformerMixin): +class SklearnTransformerWrapper(TransformerMixin, BaseEstimator): """ Wrapper to apply Scikit-learn transformers to a selected group of variables. It supports the following transformers: @@ -448,3 +448,6 @@ def _more_tags(self): "check_parameters_default_constructible" ] = "transformer has 1 mandatory parameter" return tags_dict + + def __sklearn_tags__(self): + return super().__sklearn_tags__() diff --git a/tests/test_wrappers/test_check_estimator_wrappers.py b/tests/test_wrappers/test_check_estimator_wrappers.py index ba0ef2e24..f6506342b 100644 --- a/tests/test_wrappers/test_check_estimator_wrappers.py +++ b/tests/test_wrappers/test_check_estimator_wrappers.py @@ -1,7 +1,9 @@ import pytest +import sklearn from sklearn.impute import SimpleImputer from sklearn.preprocessing import OrdinalEncoder, StandardScaler from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.wrappers import SklearnTransformerWrapper from tests.estimator_checks.estimator_checks import ( @@ -14,9 +16,22 @@ check_numerical_variables_assignment, ) +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) -def test_sklearn_transformer_wrapper(): - check_estimator(SklearnTransformerWrapper(transformer=SimpleImputer())) +if sklearn_version < parse_version("1.6"): + + def test_sklearn_transformer_wrapper(): + check_estimator(SklearnTransformerWrapper(transformer=SimpleImputer())) + +else: + + def test_sklearn_transformer_wrapper(): + check_estimator( + estimator=SklearnTransformerWrapper(transformer=SimpleImputer()), + expected_failed_checks=SklearnTransformerWrapper( + transformer=SimpleImputer() + )._more_tags()["_xfail_checks"], + ) @pytest.mark.parametrize( From 30c81261fc951100e1cc86e19dabf291f45cd467 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 11:24:59 -0300 Subject: [PATCH 16/36] make selection module compatible --- .../selection/base_recursive_selector.py | 4 + feature_engine/selection/base_selector.py | 6 +- .../selection/drop_constant_features.py | 5 ++ .../selection/drop_duplicate_features.py | 5 ++ feature_engine/selection/drop_features.py | 5 ++ feature_engine/selection/drop_psi_features.py | 4 + feature_engine/selection/mrmr.py | 4 + .../selection/probe_feature_selection.py | 4 + feature_engine/selection/shuffle_features.py | 4 + .../selection/single_feature_performance.py | 4 + .../selection/target_mean_selection.py | 6 +- .../parametrize_with_checks_selection_v16.py | 85 +++++++++++++++++++ .../test_check_estimator_selectors.py | 41 +++++++-- 13 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 tests/parametrize_with_checks_selection_v16.py diff --git a/feature_engine/selection/base_recursive_selector.py b/feature_engine/selection/base_recursive_selector.py index 79bb628cf..a2d95fb41 100644 --- a/feature_engine/selection/base_recursive_selector.py +++ b/feature_engine/selection/base_recursive_selector.py @@ -209,3 +209,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags diff --git a/feature_engine/selection/base_selector.py b/feature_engine/selection/base_selector.py index 541b9d0be..3437bad66 100644 --- a/feature_engine/selection/base_selector.py +++ b/feature_engine/selection/base_selector.py @@ -8,7 +8,7 @@ from feature_engine.tags import _return_tags -class BaseSelector(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin): +class BaseSelector(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """ Shared set-up checks and methods across selectors. @@ -124,3 +124,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/selection/drop_constant_features.py b/feature_engine/selection/drop_constant_features.py index 760c1652d..a3c7722c7 100644 --- a/feature_engine/selection/drop_constant_features.py +++ b/feature_engine/selection/drop_constant_features.py @@ -228,3 +228,8 @@ def _more_tags(self): "check_fit2d_1sample" ] = "the transformer raises an error when dropping all columns, ok to fail" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags \ No newline at end of file diff --git a/feature_engine/selection/drop_duplicate_features.py b/feature_engine/selection/drop_duplicate_features.py index c0a4785da..a62218a6b 100644 --- a/feature_engine/selection/drop_duplicate_features.py +++ b/feature_engine/selection/drop_duplicate_features.py @@ -187,3 +187,8 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags \ No newline at end of file diff --git a/feature_engine/selection/drop_features.py b/feature_engine/selection/drop_features.py index 3420ee043..862621572 100644 --- a/feature_engine/selection/drop_features.py +++ b/feature_engine/selection/drop_features.py @@ -118,3 +118,8 @@ def _more_tags(self): "check_fit2d_1feature" ] = "the transformer raises an error when removing the only column, ok to fail" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + tags.input_tags.allow_nan = True + return tags \ No newline at end of file diff --git a/feature_engine/selection/drop_psi_features.py b/feature_engine/selection/drop_psi_features.py index 3e87adbdb..c43768d8b 100644 --- a/feature_engine/selection/drop_psi_features.py +++ b/feature_engine/selection/drop_psi_features.py @@ -775,3 +775,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/selection/mrmr.py b/feature_engine/selection/mrmr.py index b11e24318..662af71e1 100644 --- a/feature_engine/selection/mrmr.py +++ b/feature_engine/selection/mrmr.py @@ -476,3 +476,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1sample"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/selection/probe_feature_selection.py b/feature_engine/selection/probe_feature_selection.py index 985f4acb4..6fb1d8298 100644 --- a/feature_engine/selection/probe_feature_selection.py +++ b/feature_engine/selection/probe_feature_selection.py @@ -343,3 +343,7 @@ def _more_tags(self): # tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/selection/shuffle_features.py b/feature_engine/selection/shuffle_features.py index 35acc656d..5c9d8091a 100644 --- a/feature_engine/selection/shuffle_features.py +++ b/feature_engine/selection/shuffle_features.py @@ -320,3 +320,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/selection/single_feature_performance.py b/feature_engine/selection/single_feature_performance.py index 439a09699..2c32e195c 100644 --- a/feature_engine/selection/single_feature_performance.py +++ b/feature_engine/selection/single_feature_performance.py @@ -264,3 +264,7 @@ def _more_tags(self): tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/feature_engine/selection/target_mean_selection.py b/feature_engine/selection/target_mean_selection.py index c5e36ea8a..310ed8947 100644 --- a/feature_engine/selection/target_mean_selection.py +++ b/feature_engine/selection/target_mean_selection.py @@ -129,7 +129,7 @@ class SelectByTargetMeanPerformance(BaseSelector): {groups} - regression: boolean, default=True + regression: boolean, default=False Indicates whether the target is one for regression or a classification. {confirm_variables} @@ -352,3 +352,7 @@ def _more_tags(self): msg = "transformers need more than 1 feature to work" tags_dict["_xfail_checks"]["check_fit2d_1feature"] = msg return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags \ No newline at end of file diff --git a/tests/parametrize_with_checks_selection_v16.py b/tests/parametrize_with_checks_selection_v16.py new file mode 100644 index 000000000..ddee4f7f6 --- /dev/null +++ b/tests/parametrize_with_checks_selection_v16.py @@ -0,0 +1,85 @@ +""" +File intended to help understand check_estimator tests for Feature-engine's +selection module. It is not run as part of the battery of acceptance tests. +Works from sklearn > 1.6. +""" + +from sklearn.linear_model import LogisticRegression +from sklearn.utils.estimator_checks import parametrize_with_checks + +from feature_engine.selection import ( + MRMR, + DropConstantFeatures, + DropCorrelatedFeatures, + DropDuplicateFeatures, + DropFeatures, + DropHighPSIFeatures, + ProbeFeatureSelection, + RecursiveFeatureAddition, + RecursiveFeatureElimination, + SelectByInformationValue, + SelectByShuffling, + SelectBySingleFeaturePerformance, + SelectByTargetMeanPerformance, + SmartCorrelatedSelection, +) + +_logreg = LogisticRegression(C=0.0001, max_iter=2, random_state=1) + +df = DropFeatures(features_to_drop=["x0"]) +dcf = DropConstantFeatures(missing_values="ignore") +ddf = DropDuplicateFeatures() +dcf = DropCorrelatedFeatures() +dpsi = DropHighPSIFeatures(bins=5) +sms = SmartCorrelatedSelection() +sbs = SelectByShuffling(estimator=_logreg, scoring="accuracy") +sbtm = SelectByTargetMeanPerformance(bins=3, regression=True, scoring="r2") +sbsfp = SelectBySingleFeaturePerformance(estimator=_logreg, scoring="accuracy") +rfa = RecursiveFeatureAddition(estimator=_logreg, scoring="accuracy") +rfe = RecursiveFeatureElimination(estimator=_logreg, scoring="accuracy", threshold=-100) +sbiv = SelectByInformationValue(bins=2) +pfs = ProbeFeatureSelection(estimator=_logreg, scoring="accuracy") +mrmr = MRMR(regression=False) + +EXPECTED_FAILED_CHECKS = { + "DropFeatures": df._more_tags()["_xfail_checks"], + "DropConstantFeatures": dcf._more_tags()["_xfail_checks"], + "DropDuplicateFeatures": ddf._more_tags()["_xfail_checks"], + "DropCorrelatedFeatures": dcf._more_tags()["_xfail_checks"], + "DropHighPSIFeatures": dpsi._more_tags()["_xfail_checks"], + "SmartCorrelatedSelection": sms._more_tags()["_xfail_checks"], + "SelectByShuffling": sbs._more_tags()["_xfail_checks"], + "SelectByTargetMeanPerformance": sbtm._more_tags()["_xfail_checks"], + "SelectBySingleFeaturePerformance": sbsfp._more_tags()["_xfail_checks"], + "RecursiveFeatureAddition": rfa._more_tags()["_xfail_checks"], + "RecursiveFeatureElimination": rfe._more_tags()["_xfail_checks"], + "SelectByInformationValue": sbiv._more_tags()["_xfail_checks"], + "ProbeFeatureSelection": pfs._more_tags()["_xfail_checks"], + "MRMR": mrmr._more_tags()["_xfail_checks"], +} + + +# encoding +@parametrize_with_checks( + estimators=[ + df, + dcf, + ddf, + dcf, + dpsi, + sms, + sbs, + sbtm, + sbsfp, + rfa, + rfe, + sbiv, + pfs, + mrmr, + ], + expected_failed_checks=lambda est: EXPECTED_FAILED_CHECKS.get( + est.__class__.__name__, {} + ), +) +def test_sklearn_compatible_creator(estimator, check): + check(estimator) diff --git a/tests/test_selection/test_check_estimator_selectors.py b/tests/test_selection/test_check_estimator_selectors.py index 7815f3c11..2f834e063 100644 --- a/tests/test_selection/test_check_estimator_selectors.py +++ b/tests/test_selection/test_check_estimator_selectors.py @@ -1,10 +1,13 @@ import pandas as pd import pytest +import sklearn from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline from sklearn.utils.estimator_checks import check_estimator +from sklearn.utils.fixes import parse_version from feature_engine.selection import ( + MRMR, DropConstantFeatures, DropCorrelatedFeatures, DropDuplicateFeatures, @@ -18,7 +21,6 @@ SelectBySingleFeaturePerformance, SelectByTargetMeanPerformance, SmartCorrelatedSelection, - MRMR, ) from tests.estimator_checks.estimator_checks import check_feature_engine_estimator from tests.estimator_checks.init_params_triggered_functionality_checks import ( @@ -26,6 +28,8 @@ check_raises_error_if_only_1_variable, ) +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + _logreg = LogisticRegression(C=0.0001, max_iter=2, random_state=1) _estimators = [ @@ -77,10 +81,37 @@ ProbeFeatureSelection(estimator=_logreg, scoring="accuracy"), ] - -@pytest.mark.parametrize("estimator", _estimators) -def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator) +if sklearn_version < parse_version("1.6"): + + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + +else: + # In sklearn 1.6. the API changes break the tests for the target mean selector. + # We need to investigate further. + est = [ + DropFeatures(features_to_drop=["x0"]), + DropConstantFeatures(missing_values="ignore"), + DropDuplicateFeatures(), + DropCorrelatedFeatures(), + DropHighPSIFeatures(bins=5), + SmartCorrelatedSelection(), + SelectByShuffling(estimator=_logreg, scoring="accuracy"), + SelectBySingleFeaturePerformance(estimator=_logreg, scoring="accuracy"), + RecursiveFeatureAddition(estimator=_logreg, scoring="accuracy"), + RecursiveFeatureElimination( + estimator=_logreg, scoring="accuracy", threshold=-100 + ), + SelectByInformationValue(bins=2), + ProbeFeatureSelection(estimator=_logreg, scoring="accuracy"), + MRMR(regression=False), + ] + + @pytest.mark.parametrize("estimator", est) + def test_check_estimator_from_sklearn(estimator): + failed_tests = estimator._more_tags()["_xfail_checks"] + return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) @pytest.mark.parametrize("estimator", _univariate_estimators) From 0e57c9c25ef54ac905a036770481175e76ee3694 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 11:29:57 -0300 Subject: [PATCH 17/36] fix style in feature engine module --- feature_engine/_base_transformers/base_numerical.py | 2 +- feature_engine/creation/base_creation.py | 3 ++- feature_engine/discretisation/decision_tree.py | 2 +- feature_engine/imputation/missing_indicator.py | 2 +- feature_engine/outliers/base_outlier.py | 1 - feature_engine/selection/base_selector.py | 2 +- feature_engine/selection/drop_constant_features.py | 2 +- feature_engine/selection/drop_duplicate_features.py | 2 +- feature_engine/selection/drop_features.py | 2 +- feature_engine/selection/drop_psi_features.py | 2 +- feature_engine/selection/mrmr.py | 2 +- feature_engine/selection/probe_feature_selection.py | 2 +- feature_engine/selection/shuffle_features.py | 2 +- feature_engine/selection/single_feature_performance.py | 2 +- feature_engine/selection/target_mean_selection.py | 2 +- feature_engine/tags.py | 9 ++++++--- 16 files changed, 21 insertions(+), 18 deletions(-) diff --git a/feature_engine/_base_transformers/base_numerical.py b/feature_engine/_base_transformers/base_numerical.py index f0e5d4542..60212f3d6 100644 --- a/feature_engine/_base_transformers/base_numerical.py +++ b/feature_engine/_base_transformers/base_numerical.py @@ -125,4 +125,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/creation/base_creation.py b/feature_engine/creation/base_creation.py index a9df2dadb..c294045f4 100644 --- a/feature_engine/creation/base_creation.py +++ b/feature_engine/creation/base_creation.py @@ -21,6 +21,7 @@ find_numerical_variables, ) + class BaseCreation(TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin): """Shared set-up, checks and methods across creation transformers.""" @@ -131,4 +132,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() tags.input_tags.allow_nan = True - return tags \ No newline at end of file + return tags diff --git a/feature_engine/discretisation/decision_tree.py b/feature_engine/discretisation/decision_tree.py index 3e5d71ce5..af691e4aa 100644 --- a/feature_engine/discretisation/decision_tree.py +++ b/feature_engine/discretisation/decision_tree.py @@ -348,4 +348,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/imputation/missing_indicator.py b/feature_engine/imputation/missing_indicator.py index 323b6542c..7976aa749 100644 --- a/feature_engine/imputation/missing_indicator.py +++ b/feature_engine/imputation/missing_indicator.py @@ -183,4 +183,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() tags.input_tags.allow_nan = True - return tags \ No newline at end of file + return tags diff --git a/feature_engine/outliers/base_outlier.py b/feature_engine/outliers/base_outlier.py index 267680e7a..8f296bcff 100644 --- a/feature_engine/outliers/base_outlier.py +++ b/feature_engine/outliers/base_outlier.py @@ -101,7 +101,6 @@ def __sklearn_tags__(self): return tags - class WinsorizerBase(BaseOutlier): _intro_docstring = """The extreme values beyond which an observation is considered diff --git a/feature_engine/selection/base_selector.py b/feature_engine/selection/base_selector.py index 3437bad66..cfa8f1c95 100644 --- a/feature_engine/selection/base_selector.py +++ b/feature_engine/selection/base_selector.py @@ -127,4 +127,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/drop_constant_features.py b/feature_engine/selection/drop_constant_features.py index a3c7722c7..ba3fad490 100644 --- a/feature_engine/selection/drop_constant_features.py +++ b/feature_engine/selection/drop_constant_features.py @@ -232,4 +232,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() tags.input_tags.allow_nan = True - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/drop_duplicate_features.py b/feature_engine/selection/drop_duplicate_features.py index a62218a6b..87bd9e44b 100644 --- a/feature_engine/selection/drop_duplicate_features.py +++ b/feature_engine/selection/drop_duplicate_features.py @@ -191,4 +191,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() tags.input_tags.allow_nan = True - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/drop_features.py b/feature_engine/selection/drop_features.py index 862621572..028527e0b 100644 --- a/feature_engine/selection/drop_features.py +++ b/feature_engine/selection/drop_features.py @@ -122,4 +122,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() tags.input_tags.allow_nan = True - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/drop_psi_features.py b/feature_engine/selection/drop_psi_features.py index c43768d8b..65d90b413 100644 --- a/feature_engine/selection/drop_psi_features.py +++ b/feature_engine/selection/drop_psi_features.py @@ -778,4 +778,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/mrmr.py b/feature_engine/selection/mrmr.py index 662af71e1..7ed189212 100644 --- a/feature_engine/selection/mrmr.py +++ b/feature_engine/selection/mrmr.py @@ -479,4 +479,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/probe_feature_selection.py b/feature_engine/selection/probe_feature_selection.py index 6fb1d8298..3cea4c598 100644 --- a/feature_engine/selection/probe_feature_selection.py +++ b/feature_engine/selection/probe_feature_selection.py @@ -346,4 +346,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/shuffle_features.py b/feature_engine/selection/shuffle_features.py index 5c9d8091a..4fe1cfa66 100644 --- a/feature_engine/selection/shuffle_features.py +++ b/feature_engine/selection/shuffle_features.py @@ -323,4 +323,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/single_feature_performance.py b/feature_engine/selection/single_feature_performance.py index 2c32e195c..5630642ab 100644 --- a/feature_engine/selection/single_feature_performance.py +++ b/feature_engine/selection/single_feature_performance.py @@ -267,4 +267,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/selection/target_mean_selection.py b/feature_engine/selection/target_mean_selection.py index 310ed8947..913783dc6 100644 --- a/feature_engine/selection/target_mean_selection.py +++ b/feature_engine/selection/target_mean_selection.py @@ -355,4 +355,4 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() - return tags \ No newline at end of file + return tags diff --git a/feature_engine/tags.py b/feature_engine/tags.py index 78a025311..f21b02fd5 100644 --- a/feature_engine/tags.py +++ b/feature_engine/tags.py @@ -3,6 +3,7 @@ sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + def _return_tags(): tags = { "preserves_dtype": [], @@ -32,10 +33,12 @@ def _return_tags(): } if sklearn_version > parse_version("1.6"): + msg1 = "against feature engine design" + msg2 = "our transformers do not preserve dtype" all_fail = { - "check_do_not_raise_errors_in_init_or_set_params": "against feature engine design", - "check_transformer_preserve_dtypes": "our transformers do not preserve dtype", - "check_n_features_in_after_fitting":"not sure why it fails, we do check" + "check_do_not_raise_errors_in_init_or_set_params": msg1, + "check_transformer_preserve_dtypes": msg2, + "check_n_features_in_after_fitting": "not sure why it fails, we do check" } tags['_xfail_checks'].update(all_fail) return tags From 15b253081d6f36c7f6d057dc26690ec96c14d19b Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 11:31:55 -0300 Subject: [PATCH 18/36] fix style in tests module --- tests/test_creation/test_check_estimator_creation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index ac1288a66..0b3b86dd9 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -5,8 +5,6 @@ from sklearn.utils.estimator_checks import check_estimator from sklearn.utils.fixes import parse_version -sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) - from feature_engine.creation import ( CyclicalFeatures, DecisionTreeFeatures, @@ -15,6 +13,8 @@ ) from tests.estimator_checks.estimator_checks import check_feature_engine_estimator +sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) + mf = MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore") rf = RelativeFeatures( variables=["x0", "x1"], reference=["x0"], func=["add"], missing_values="ignore" From 4115c71072281570f8a8c573da80a0f3a175140c Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 11:35:52 -0300 Subject: [PATCH 19/36] fix mypy check --- feature_engine/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature_engine/tags.py b/feature_engine/tags.py index f21b02fd5..aed5daf2c 100644 --- a/feature_engine/tags.py +++ b/feature_engine/tags.py @@ -40,5 +40,5 @@ def _return_tags(): "check_transformer_preserve_dtypes": msg2, "check_n_features_in_after_fitting": "not sure why it fails, we do check" } - tags['_xfail_checks'].update(all_fail) + tags['_xfail_checks'].update(all_fail) # type: ignore return tags From 3ef2c66ebe066d93c5433676156bf1b6248e23bc Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 12:13:46 -0300 Subject: [PATCH 20/36] add tags to woe --- feature_engine/encoding/woe.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py index e46f35708..0b64f617e 100644 --- a/feature_engine/encoding/woe.py +++ b/feature_engine/encoding/woe.py @@ -289,3 +289,8 @@ def _more_tags(self): # are not suitable tags_dict["_skip_test"] = True return tags_dict + + def __sklearn_tags__(self): + tags = super().__sklearn_tags__() + return tags + From 237809815b917e519a8cf0f0cad4c08a71bef4fc Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 12:19:30 -0300 Subject: [PATCH 21/36] fix style --- feature_engine/encoding/woe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py index 0b64f617e..338998315 100644 --- a/feature_engine/encoding/woe.py +++ b/feature_engine/encoding/woe.py @@ -293,4 +293,3 @@ def _more_tags(self): def __sklearn_tags__(self): tags = super().__sklearn_tags__() return tags - From 3a4aa7f44c6289ba8ac948e8df39e4d36ece5f2a Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 12:22:22 -0300 Subject: [PATCH 22/36] add test for sklearn 1.5 --- .circleci/config.yml | 12 ++++++++++++ tox.ini | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b03548e69..e5fb09730 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -72,6 +72,18 @@ jobs: name: Run tests with Python 3.12 command: | tox -e py312 + test_feature_engine_sklearn15: + docker: + - image: cimg/python:3.12.1 + working_directory: ~/project + steps: + - checkout: + path: ~/project + - *prepare_tox + - run: + name: Run tests with Sklearn 1.5 + command: | + tox -e sklearn15 test_style: docker: - image: cimg/python:3.10.0 diff --git a/tox.ini b/tox.ini index 5ceeb7d6a..aafd1d536 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312, codecov, docs, stylechecks, typechecks +envlist = py39, py310, py311, py312, sklearn15, codecov, docs, stylechecks, typechecks skipsdist = true [testenv] @@ -29,6 +29,13 @@ deps = deps = -rtest_requirements.txt +[testenv:sklearn15] +deps = + -rtest_requirements.txt +commands = + pip install -U scikit-learn==1.5.0 + pytest tests + [testenv:codecov] deps = -rtest_requirements.txt From 9c35fd637fbe649a123b8b2d693f51eb38d89250 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 13:21:42 -0300 Subject: [PATCH 23/36] remove circleci update --- .circleci/config.yml | 12 ------------ tox.ini | 9 +-------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e5fb09730..b03548e69 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -72,18 +72,6 @@ jobs: name: Run tests with Python 3.12 command: | tox -e py312 - test_feature_engine_sklearn15: - docker: - - image: cimg/python:3.12.1 - working_directory: ~/project - steps: - - checkout: - path: ~/project - - *prepare_tox - - run: - name: Run tests with Sklearn 1.5 - command: | - tox -e sklearn15 test_style: docker: - image: cimg/python:3.10.0 diff --git a/tox.ini b/tox.ini index aafd1d536..5ceeb7d6a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312, sklearn15, codecov, docs, stylechecks, typechecks +envlist = py39, py310, py311, py312, codecov, docs, stylechecks, typechecks skipsdist = true [testenv] @@ -29,13 +29,6 @@ deps = deps = -rtest_requirements.txt -[testenv:sklearn15] -deps = - -rtest_requirements.txt -commands = - pip install -U scikit-learn==1.5.0 - pytest tests - [testenv:codecov] deps = -rtest_requirements.txt From 969c008a74b1035fc5e15767414a7014da4c4ca3 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 13:26:51 -0300 Subject: [PATCH 24/36] make some rewording on tags --- feature_engine/tags.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/feature_engine/tags.py b/feature_engine/tags.py index aed5daf2c..3be830279 100644 --- a/feature_engine/tags.py +++ b/feature_engine/tags.py @@ -33,12 +33,13 @@ def _return_tags(): } if sklearn_version > parse_version("1.6"): - msg1 = "against feature engine design" - msg2 = "our transformers do not preserve dtype" + msg1 = "against Feature-engines design." + msg2 = "Our transformers do not preserve dtype." all_fail = { "check_do_not_raise_errors_in_init_or_set_params": msg1, "check_transformer_preserve_dtypes": msg2, - "check_n_features_in_after_fitting": "not sure why it fails, we do check" + #TODO: investigate this test further. + "check_n_features_in_after_fitting": "not sure why it fails, we do check." } tags['_xfail_checks'].update(all_fail) # type: ignore return tags From 3b0156b81208771c75041811202367de081b7960 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 13:30:24 -0300 Subject: [PATCH 25/36] remove redundant tag, reorder inheritance --- .../timeseries/forecasting/base_forecast_transformers.py | 2 +- feature_engine/transformation/log.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/feature_engine/timeseries/forecasting/base_forecast_transformers.py b/feature_engine/timeseries/forecasting/base_forecast_transformers.py index c8fcee7f7..f6edc95c0 100644 --- a/feature_engine/timeseries/forecasting/base_forecast_transformers.py +++ b/feature_engine/timeseries/forecasting/base_forecast_transformers.py @@ -42,7 +42,7 @@ n_features_in_=_n_features_in_docstring, ) class BaseForecastTransformer( - TransformerMixin, GetFeatureNamesOutMixin, TransformXyMixin, BaseEstimator + TransformerMixin, BaseEstimator, GetFeatureNamesOutMixin, TransformXyMixin ): """ Shared methods across time-series forecasting transformers. diff --git a/feature_engine/transformation/log.py b/feature_engine/transformation/log.py index f7a47a72f..91a7c7b1f 100644 --- a/feature_engine/transformation/log.py +++ b/feature_engine/transformation/log.py @@ -438,6 +438,3 @@ def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame: X.loc[:, self.variables_] = 10 ** X.loc[:, self.variables_] - self.C_ return X - - def __sklearn_tags__(self): - return super().__sklearn_tags__() From 74a723ca62548275de569297727c9a158b468124 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 13:33:57 -0300 Subject: [PATCH 26/36] fix style --- feature_engine/tags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature_engine/tags.py b/feature_engine/tags.py index 3be830279..ad36b030a 100644 --- a/feature_engine/tags.py +++ b/feature_engine/tags.py @@ -38,8 +38,8 @@ def _return_tags(): all_fail = { "check_do_not_raise_errors_in_init_or_set_params": msg1, "check_transformer_preserve_dtypes": msg2, - #TODO: investigate this test further. - "check_n_features_in_after_fitting": "not sure why it fails, we do check." + # TODO: investigate this test further. + "check_n_features_in_after_fitting": "not sure why it fails, we do check.", } - tags['_xfail_checks'].update(all_fail) # type: ignore + tags["_xfail_checks"].update(all_fail) # type: ignore return tags From 9d8c73b4ec6c55747a03086a5c3b21300411884a Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 21 Jan 2025 13:41:49 -0300 Subject: [PATCH 27/36] Update tests/test_creation/test_check_estimator_creation.py Co-authored-by: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> --- tests/test_creation/test_check_estimator_creation.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index 0b3b86dd9..1beb05b96 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -24,17 +24,9 @@ if sklearn_version > parse_version("1.6"): - @pytest.mark.parametrize( - "estimator, failed_tests", - [ - (mf, mf._more_tags()["_xfail_checks"]), - (rf, rf._more_tags()["_xfail_checks"]), - (dtf, dtf._more_tags()["_xfail_checks"]), - (cf, cf._more_tags()["_xfail_checks"]), - ], - ) + @pytest.mark.parametrize("estimator", [mf, rf, cf, dtf]) def test_check_estimator_from_sklearn(estimator, failed_tests): - return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) + return check_estimator(estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"]) else: From dc10fa39fbc5cf11c407f65f2e68ed0e25b3caf2 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 21 Jan 2025 13:44:26 -0300 Subject: [PATCH 28/36] Update tests/test_discretisation/test_check_estimator_discretisers.py Co-authored-by: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> --- .../test_check_estimator_discretisers.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/tests/test_discretisation/test_check_estimator_discretisers.py b/tests/test_discretisation/test_check_estimator_discretisers.py index 60da4a80f..dfa25e7b4 100644 --- a/tests/test_discretisation/test_check_estimator_discretisers.py +++ b/tests/test_discretisation/test_check_estimator_discretisers.py @@ -33,24 +33,9 @@ def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) else: - dtd = DecisionTreeDiscretiser(regression=False) - efd = EqualFrequencyDiscretiser() - ewd = EqualWidthDiscretiser() - ad = ArbitraryDiscretiser(binning_dict={"x0": [-np.inf, 0, np.inf]}) - gd = GeometricWidthDiscretiser() - - @pytest.mark.parametrize( - "estimator, failed_tests", - [ - (dtd, dtd._more_tags()["_xfail_checks"]), - (efd, efd._more_tags()["_xfail_checks"]), - (ewd, ewd._more_tags()["_xfail_checks"]), - (ad, ad._more_tags()["_xfail_checks"]), - (gd, gd._more_tags()["_xfail_checks"]), - ], - ) - def test_check_estimator_from_sklearn(estimator, failed_tests): - return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"]) @pytest.mark.parametrize("estimator", _estimators) From 3be01fcc9663d21e6f6feece7ea1a056dc3d31fe Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 21 Jan 2025 13:45:45 -0300 Subject: [PATCH 29/36] Update tests/test_encoding/test_check_estimator_encoders.py Co-authored-by: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> --- .../test_check_estimator_encoders.py | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/tests/test_encoding/test_check_estimator_encoders.py b/tests/test_encoding/test_check_estimator_encoders.py index fd82e70ea..3c3ba41e5 100644 --- a/tests/test_encoding/test_check_estimator_encoders.py +++ b/tests/test_encoding/test_check_estimator_encoders.py @@ -51,34 +51,12 @@ def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) else: - ce = CountFrequencyEncoder(ignore_format=True) - me = MeanEncoder(ignore_format=True) - ohe = OneHotEncoder(ignore_format=True) - oe = OrdinalEncoder(ignore_format=True) - re = RareLabelEncoder( - tol=0.00000000001, - n_categories=100000000000, - replace_with=10, - ignore_format=True, - ) - woe = WoEEncoder(ignore_format=True) - sse = StringSimilarityEncoder(ignore_format=True) - expected_fails = _return_tags()["_xfail_checks"] expected_fails.update({"check_estimators_nan_inf": "transformer allows NA"}) - @pytest.mark.parametrize( - "estimator, failed_tests", - [ - (ce, expected_fails), - (me, expected_fails), - (ohe, expected_fails), - (oe, expected_fails), - (re, expected_fails), - ], - ) - def test_check_estimator_from_sklearn(estimator, failed_tests): - return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator=estimator, expected_failed_checks=expected_fails) _estimators = [ From 849f7d82619e32dcb5d4ed844980e984db8397b3 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 21 Jan 2025 13:46:42 -0300 Subject: [PATCH 30/36] Update tests/test_imputation/test_check_estimator_imputers.py Co-authored-by: Claudio Salvatore Arcidiacono <22871978+ClaudioSalvatoreArcidiacono@users.noreply.github.com> --- .../test_check_estimator_imputers.py | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/tests/test_imputation/test_check_estimator_imputers.py b/tests/test_imputation/test_check_estimator_imputers.py index 04abc9c0a..d987ce64e 100644 --- a/tests/test_imputation/test_check_estimator_imputers.py +++ b/tests/test_imputation/test_check_estimator_imputers.py @@ -35,28 +35,9 @@ def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) else: - mi = MeanMedianImputer() - ai = ArbitraryNumberImputer() - ci = CategoricalImputer(fill_value=0, ignore_format=True) - eti = EndTailImputer() - ami = AddMissingIndicator() - rsi = RandomSampleImputer() - dmd = DropMissingData() - - @pytest.mark.parametrize( - "estimator, failed_tests", - [ - (mi, mi._more_tags()["_xfail_checks"]), - (ai, ai._more_tags()["_xfail_checks"]), - (ci, ci._more_tags()["_xfail_checks"]), - (eti, eti._more_tags()["_xfail_checks"]), - (ami, ami._more_tags()["_xfail_checks"]), - (rsi, rsi._more_tags()["_xfail_checks"]), - (dmd, dmd._more_tags()["_xfail_checks"]), - ], - ) - def test_check_estimator_from_sklearn(estimator, failed_tests): - return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"]) @pytest.mark.parametrize("estimator", _estimators) From e2f84d69f140f48d9f37671f2ef3f8f29ef609b2 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 13:50:13 -0300 Subject: [PATCH 31/36] fix style: --- tests/test_creation/test_check_estimator_creation.py | 5 ++++- .../test_check_estimator_discretisers.py | 6 +++++- tests/test_encoding/test_check_estimator_encoders.py | 4 +++- tests/test_imputation/test_check_estimator_imputers.py | 6 +++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index 1beb05b96..d5505c364 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -26,7 +26,10 @@ @pytest.mark.parametrize("estimator", [mf, rf, cf, dtf]) def test_check_estimator_from_sklearn(estimator, failed_tests): - return check_estimator(estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"]) + return check_estimator( + estimator=estimator, + expected_failed_checks=estimator._more_tags()["_xfail_checks"], + ) else: diff --git a/tests/test_discretisation/test_check_estimator_discretisers.py b/tests/test_discretisation/test_check_estimator_discretisers.py index dfa25e7b4..87e175eac 100644 --- a/tests/test_discretisation/test_check_estimator_discretisers.py +++ b/tests/test_discretisation/test_check_estimator_discretisers.py @@ -33,9 +33,13 @@ def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) else: + @pytest.mark.parametrize("estimator", _estimators) def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"]) + return check_estimator( + estimator=estimator, + expected_failed_checks=estimator._more_tags()["_xfail_checks"], + ) @pytest.mark.parametrize("estimator", _estimators) diff --git a/tests/test_encoding/test_check_estimator_encoders.py b/tests/test_encoding/test_check_estimator_encoders.py index 3c3ba41e5..8aabc3828 100644 --- a/tests/test_encoding/test_check_estimator_encoders.py +++ b/tests/test_encoding/test_check_estimator_encoders.py @@ -56,7 +56,9 @@ def test_check_estimator_from_sklearn(estimator): @pytest.mark.parametrize("estimator", _estimators) def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator=estimator, expected_failed_checks=expected_fails) + return check_estimator( + estimator=estimator, expected_failed_checks=expected_fails + ) _estimators = [ diff --git a/tests/test_imputation/test_check_estimator_imputers.py b/tests/test_imputation/test_check_estimator_imputers.py index d987ce64e..0091c7bf7 100644 --- a/tests/test_imputation/test_check_estimator_imputers.py +++ b/tests/test_imputation/test_check_estimator_imputers.py @@ -35,9 +35,13 @@ def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) else: + @pytest.mark.parametrize("estimator", _estimators) def test_check_estimator_from_sklearn(estimator): - return check_estimator(estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"]) + return check_estimator( + estimator=estimator, + expected_failed_checks=estimator._more_tags()["_xfail_checks"], + ) @pytest.mark.parametrize("estimator", _estimators) From feb43b8e618f55e34e6af03c6f968e290ac87b5e Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 13:56:28 -0300 Subject: [PATCH 32/36] refactor check creation --- .../test_check_estimator_creation.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_creation/test_check_estimator_creation.py b/tests/test_creation/test_check_estimator_creation.py index d5505c364..fd39bfc57 100644 --- a/tests/test_creation/test_check_estimator_creation.py +++ b/tests/test_creation/test_check_estimator_creation.py @@ -15,17 +15,19 @@ sklearn_version = parse_version(parse_version(sklearn.__version__).base_version) -mf = MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore") -rf = RelativeFeatures( - variables=["x0", "x1"], reference=["x0"], func=["add"], missing_values="ignore" -) -cf = CyclicalFeatures() -dtf = DecisionTreeFeatures(regression=False) +_estimators = [ + MathFeatures(variables=["x0", "x1"], func="mean", missing_values="ignore"), + RelativeFeatures( + variables=["x0", "x1"], reference=["x0"], func=["add"], missing_values="ignore" + ), + CyclicalFeatures(), + DecisionTreeFeatures(regression=False), +] if sklearn_version > parse_version("1.6"): - @pytest.mark.parametrize("estimator", [mf, rf, cf, dtf]) - def test_check_estimator_from_sklearn(estimator, failed_tests): + @pytest.mark.parametrize("estimator", _estimators) + def test_check_estimator_from_sklearn(estimator): return check_estimator( estimator=estimator, expected_failed_checks=estimator._more_tags()["_xfail_checks"], @@ -33,7 +35,7 @@ def test_check_estimator_from_sklearn(estimator, failed_tests): else: - @pytest.mark.parametrize("estimator", [mf, rf, cf, dtf]) + @pytest.mark.parametrize("estimator", _estimators) def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) From 7fad36674f4330e905082124ce3c7870aa5fe345 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 14:00:15 -0300 Subject: [PATCH 33/36] add todo --- tests/test_prediction/test_check_estimator_prediction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_prediction/test_check_estimator_prediction.py b/tests/test_prediction/test_check_estimator_prediction.py index e3762f265..bf19059b0 100644 --- a/tests/test_prediction/test_check_estimator_prediction.py +++ b/tests/test_prediction/test_check_estimator_prediction.py @@ -26,6 +26,7 @@ if sklearn_version < parse_version("1.6"): # In sklearn version 1.6, changes into the developer api were introduced # that break the tests. Need to dig further into it. + # TODO: add tests for sklearn version > 1.6 @pytest.mark.parametrize("estimator", [BaseTargetMeanEstimator()]) def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) From f046eaa80bdc79f20dd63a1740640fa89d5d63a1 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 14:06:34 -0300 Subject: [PATCH 34/36] refactor check estimator selection --- .../test_check_estimator_selectors.py | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/tests/test_selection/test_check_estimator_selectors.py b/tests/test_selection/test_check_estimator_selectors.py index 2f834e063..6331573ca 100644 --- a/tests/test_selection/test_check_estimator_selectors.py +++ b/tests/test_selection/test_check_estimator_selectors.py @@ -90,28 +90,14 @@ def test_check_estimator_from_sklearn(estimator): else: # In sklearn 1.6. the API changes break the tests for the target mean selector. # We need to investigate further. - est = [ - DropFeatures(features_to_drop=["x0"]), - DropConstantFeatures(missing_values="ignore"), - DropDuplicateFeatures(), - DropCorrelatedFeatures(), - DropHighPSIFeatures(bins=5), - SmartCorrelatedSelection(), - SelectByShuffling(estimator=_logreg, scoring="accuracy"), - SelectBySingleFeaturePerformance(estimator=_logreg, scoring="accuracy"), - RecursiveFeatureAddition(estimator=_logreg, scoring="accuracy"), - RecursiveFeatureElimination( - estimator=_logreg, scoring="accuracy", threshold=-100 - ), - SelectByInformationValue(bins=2), - ProbeFeatureSelection(estimator=_logreg, scoring="accuracy"), - MRMR(regression=False), - ] - - @pytest.mark.parametrize("estimator", est) + # TODO: investigate checks for target mean selector. + @pytest.mark.parametrize("estimator", _estimators) def test_check_estimator_from_sklearn(estimator): - failed_tests = estimator._more_tags()["_xfail_checks"] - return check_estimator(estimator=estimator, expected_failed_checks=failed_tests) + if estimator.__class__.__name__ != "SelectByTargetMeanPerformance": + failed_tests = estimator._more_tags()["_xfail_checks"] + return check_estimator( + estimator=estimator, expected_failed_checks=failed_tests + ) @pytest.mark.parametrize("estimator", _univariate_estimators) From bc66eefc4b0c2945f862892b05b961fc8c3c1682 Mon Sep 17 00:00:00 2001 From: solegalli Date: Tue, 21 Jan 2025 14:32:35 -0300 Subject: [PATCH 35/36] remove woe from testing --- feature_engine/encoding/woe.py | 2 +- tests/test_encoding/test_check_estimator_encoders.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py index 338998315..2a803eebc 100644 --- a/feature_engine/encoding/woe.py +++ b/feature_engine/encoding/woe.py @@ -284,7 +284,7 @@ def _more_tags(self): # in the current format, the tests are performed using continuous np.arrays # this means that when we encode some of the values, the denominator is 0 # and this the transformer raises an error, and the test fails. - # For this reason, most sklearn transformers will fail. And it has nothing to + # For this reason, most sklearn tests will fail. And it has nothing to # do with the class not being compatible, it is just that the inputs passed # are not suitable tags_dict["_skip_test"] = True diff --git a/tests/test_encoding/test_check_estimator_encoders.py b/tests/test_encoding/test_check_estimator_encoders.py index 8aabc3828..5c96b6baf 100644 --- a/tests/test_encoding/test_check_estimator_encoders.py +++ b/tests/test_encoding/test_check_estimator_encoders.py @@ -56,9 +56,10 @@ def test_check_estimator_from_sklearn(estimator): @pytest.mark.parametrize("estimator", _estimators) def test_check_estimator_from_sklearn(estimator): - return check_estimator( - estimator=estimator, expected_failed_checks=expected_fails - ) + if estimator.__class__.__name__ != "WoEEncoder": + return check_estimator( + estimator=estimator, expected_failed_checks=expected_fails + ) _estimators = [ From 7fa78ca36385823f0c13bff5d344d57de474f0ab Mon Sep 17 00:00:00 2001 From: solegalli Date: Wed, 22 Jan 2025 07:58:26 -0300 Subject: [PATCH 36/36] remove dup code --- tests/test_outliers/test_check_estimator_outliers.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/test_outliers/test_check_estimator_outliers.py b/tests/test_outliers/test_check_estimator_outliers.py index a5c117355..f49382088 100644 --- a/tests/test_outliers/test_check_estimator_outliers.py +++ b/tests/test_outliers/test_check_estimator_outliers.py @@ -24,10 +24,6 @@ def test_check_estimator_from_sklearn(estimator): return check_estimator(estimator) else: - aoc = ArbitraryOutlierCapper(max_capping_dict={"x0": 10}) - ot = OutlierTrimmer() - wz = Winsorizer() - FAILED_CHECKS = _return_tags()["_xfail_checks"] FAILED_CHECKS_AOC = _return_tags()["_xfail_checks"] @@ -48,9 +44,9 @@ def test_check_estimator_from_sklearn(estimator): @pytest.mark.parametrize( "estimator, failed_tests", [ - (aoc, FAILED_CHECKS_AOC), - (ot, FAILED_CHECKS), - (wz, FAILED_CHECKS), + (_estimators[0], FAILED_CHECKS_AOC), + (_estimators[1], FAILED_CHECKS), + (_estimators[2], FAILED_CHECKS), ], ) def test_check_estimator_from_sklearn(estimator, failed_tests):