diff --git a/feature_engine/estimator_checks.py b/feature_engine/estimator_checks.py new file mode 100644 index 000000000..76bd9e54c --- /dev/null +++ b/feature_engine/estimator_checks.py @@ -0,0 +1,263 @@ +import pandas as pd +import pytest +from sklearn.base import clone +from sklearn.datasets import make_classification +from sklearn.exceptions import NotFittedError + + +def test_df(numeric=True): + X, y = make_classification( + n_samples=1000, + n_features=12, + n_redundant=4, + n_clusters_per_class=1, + weights=[0.50], + class_sep=2, + random_state=1, + ) + + # trasform arrays into pandas df and series + colnames = ["var_" + str(i) for i in range(12)] + X = pd.DataFrame(X, columns=colnames) + y = pd.Series(y) + + if numeric is False: + X["cat_var"] = ["A"] * 1000 + X["cat_var2"] = ["B"] * 1000 + + return X, y + + +def check_feature_engine_estimator(estimator): + check_raises_non_fitted_error(estimator) + check_raises_error_when_fitting_not_a_df(estimator) + check_raises_error_when_transforming_not_a_df(estimator) + + tags = estimator._more_tags() + if "requires_y" in tags.keys(): + check_error_if_y_not_passed(estimator) + + if hasattr(estimator, "variables"): + if tags["variables"] == "numerical": + check_numerical_variables_assignment(estimator) + elif tags["variables"] == "categorical": + check_categorical_variables_assignment(estimator) + elif tags["variables"] == "all": + check_all_types_variables_assignment(estimator) + + if hasattr(estimator, "cv"): + check_takes_cv_constructor(estimator) + + if hasattr(estimator, "missing_values"): + check_error_param_missing_values(estimator) + + +def check_raises_non_fitted_error(estimator): + X, y = test_df() + transformer = clone(estimator) + # test when fit is not called prior to transform + with pytest.raises(NotFittedError): + transformer.transform(X) + + +def check_raises_error_when_fitting_not_a_df(estimator): + _not_a_df = [ + "not_a_df", + [1, 2, 3, "some_data"], + pd.Series([-2, 1.5, 8.94], name="not_a_df"), + ] + + transformer = clone(estimator) + for not_df in _not_a_df: + # trying to fit not a df + with pytest.raises(TypeError): + transformer.fit(not_df) + + +def check_raises_error_when_transforming_not_a_df(estimator): + X, y = test_df() + + _not_a_df = [ + "not_a_df", + [1, 2, 3, "some_data"], + pd.Series([-2, 1.5, 8.94], name="not_a_df"), + ] + + transformer = clone(estimator) + transformer.fit(X, y) + + for not_df in _not_a_df: + # trying to transform not a df + with pytest.raises(TypeError): + transformer.fit(not_df) + + +def check_error_if_y_not_passed(estimator): + X, y = test_df() + estimator = clone(estimator) + with pytest.raises(TypeError): + estimator.fit(X) + + +def check_numerical_variables_assignment(estimator): + # toy df + X, y = test_df(numeric=False) + + # input variables to test + _input_vars_ls = ["var_1", ["var_2"], ["var_1", "var_2", "var_3", "var_11"], None] + + # the estimator + transformer = clone(estimator) + + for input_vars in _input_vars_ls: + # set the different input var examples + transformer.set_params(variables=input_vars) + + # fit + transformer.fit(X, y) + + if input_vars is not None: + assert transformer.variables == input_vars + + if isinstance(input_vars, list): + assert transformer.variables_ == input_vars + else: + assert transformer.variables_ == [input_vars] + else: + assert transformer.variables is None + assert transformer.variables_ == ["var_" + str(i) for i in range(12)] + + # test raises error if uses passes categorical variable + transformer.set_params(variables=["var_1", "cat_var"]) + with pytest.raises(TypeError): + transformer.fit(X, y) + + +def check_categorical_variables_assignment(estimator): + # toy df + X, y = test_df(numeric=False) + + # cast one variable as category + X[["cat_var2"]] = X[["cat_var2"]].astype("category") + + # input variables to test + _input_vars_ls = ["cat_var", ["cat_var"], ["cat_var", "cat_var2"], None] + + # the estimator + transformer = clone(estimator) + + for input_vars in _input_vars_ls: + # set the different input var examples + transformer.set_params(variables=input_vars) + + # fit + transformer.fit(X, y) + + if input_vars is not None: + assert transformer.variables == input_vars + + if isinstance(input_vars, list): + assert transformer.variables_ == input_vars + else: + assert transformer.variables_ == [input_vars] + else: + assert transformer.variables is None + assert transformer.variables_ == ["cat_var", "cat_var2"] + + # test raises error if uses passes numerical variable + transformer.set_params(variables=["var_1", "cat_var"]) + with pytest.raises(TypeError): + transformer.fit(X, y) + + +def check_all_types_variables_assignment(estimator): + # toy df + X, y = test_df(numeric=False) + + # cast one variable as category + X[["cat_var2"]] = X[["cat_var2"]].astype("category") + + # input variables to test + _input_vars_ls = [ + "var_1", + ["cat_var"], + ["var_1", "var_2", "cat_var", "cat_var2"], + None, + ] + + # the estimator + transformer = clone(estimator) + + for input_vars in _input_vars_ls: + # set the different input var examples + transformer.set_params(variables=input_vars) + + # fit + transformer.fit(X, y) + + if input_vars is not None: + assert transformer.variables == input_vars + + if isinstance(input_vars, list): + assert transformer.variables_ == input_vars + else: + assert transformer.variables_ == [input_vars] + else: + assert transformer.variables is None + assert transformer.variables_ == list(X.columns) + + +def check_takes_cv_constructor(estimator): + from sklearn.model_selection import KFold, StratifiedKFold + + X, y = test_df() + + estimator = clone(estimator) + + cv_constructor_ls = [KFold(n_splits=3), StratifiedKFold(n_splits=3), None] + + for cv_constructor in cv_constructor_ls: + + sel = estimator.set_params(cv=cv_constructor) + sel.fit(X, y) + Xtransformed = sel.transform(X) + + # test fit attrs + if hasattr(sel, "initial_model_performance_"): + assert isinstance(sel.initial_model_performance_, (int, float)) + + assert isinstance(sel.features_to_drop_, list) + assert all([x for x in sel.features_to_drop_ if x in X.columns]) + assert len(sel.features_to_drop_) < X.shape[1] + + assert not Xtransformed.empty + assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) + + if hasattr(sel, "performance_drifts_"): + assert isinstance(sel.performance_drifts_, dict) + assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) + assert all( + [ + isinstance(sel.performance_drifts_[var], (int, float)) + for var in sel.performance_drifts_.keys() + ] + ) + + if hasattr(sel, "feature_performance_"): + assert isinstance(sel.feature_performance_, dict) + assert all([x for x in X.columns if x in sel.feature_performance_.keys()]) + assert all( + [ + isinstance(sel.feature_performance_[var], (int, float)) + for var in sel.feature_performance_.keys() + ] + ) + + +# ======== input param error checks +def check_error_param_missing_values(estimator): + # param takes values "raise" or "ignore" + estimator = clone(estimator) + for value in [2, "hola", False]: + with pytest.raises(ValueError): + estimator.__class__(missing_values=value) diff --git a/feature_engine/imputation/base_imputer.py b/feature_engine/imputation/base_imputer.py index 94f5fcc53..598ec085a 100644 --- a/feature_engine/imputation/base_imputer.py +++ b/feature_engine/imputation/base_imputer.py @@ -62,6 +62,5 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: def _more_tags(self): tags_dict = _return_tags() - # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" + tags_dict["allow_nan"] = True return tags_dict diff --git a/feature_engine/selection/base_recursive_selector.py b/feature_engine/selection/base_recursive_selector.py new file mode 100644 index 000000000..f241d5f0c --- /dev/null +++ b/feature_engine/selection/base_recursive_selector.py @@ -0,0 +1,167 @@ +from typing import List, Union + +import pandas as pd +from sklearn.model_selection import cross_validate + +from feature_engine.dataframe_checks import _is_dataframe +from feature_engine.selection.base_selector import BaseSelector, get_feature_importances +from feature_engine.validation import _return_tags +from feature_engine.variable_manipulation import ( + _check_input_parameter_variables, + _find_or_check_numerical_variables, +) + +Variables = Union[None, int, str, List[Union[str, int]]] + + +class BaseRecursiveSelector(BaseSelector): + """ + Shared functionality for recursive selectors. + + Parameters + ---------- + estimator: object + A Scikit-learn estimator for regression or classification. + The estimator must have either a `feature_importances` or `coef_` attribute + after fitting. + + variables: str or list, default=None + The list of variable to be evaluated. If None, the transformer will evaluate + all numerical features in the dataset. + + scoring: str, default='roc_auc' + Desired metric to optimise the performance of the estimator. Comes from + sklearn.metrics. See the model evaluation documentation for more options: + https://scikit-learn.org/stable/modules/model_evaluation.html + + threshold: float, int, default = 0.01 + The value that defines if a feature will be kept or removed. Note that for + metrics like roc-auc, r2_score and accuracy, the thresholds will be floats + between 0 and 1. For metrics like the mean_square_error and the + root_mean_square_error the threshold can be a big number. + The threshold must be defined by the user. Bigger thresholds will select less + features. + + cv: int, cross-validation generator or an iterable, default=3 + Determines the cross-validation splitting strategy. Possible inputs for cv are: + + - None, to use cross_validate's default 5-fold cross validation + + - int, to specify the number of folds in a (Stratified)KFold, + + - CV splitter + - (https://scikit-learn.org/stable/glossary.html#term-CV-splitter) + + - An iterable yielding (train, test) splits as arrays of indices. + + For int/None inputs, if the estimator is a classifier and y is either binary or + multiclass, StratifiedKFold is used. In all other cases, KFold is used. These + splitters are instantiated with `shuffle=False` so the splits will be the same + across calls. For more details check Scikit-learn's `cross_validate`'s + documentation. + + Attributes + ---------- + initial_model_performance_ : + Performance of the model trained using the original dataset. + + feature_importances_ : + Pandas Series with the feature importance (comes from step 2) + + performance_drifts_: + Dictionary with the performance drift per examined feature (comes from step 5). + + features_to_drop_: + List with the features to remove from the dataset. + + variables_: + The variables that will be considered for the feature selection. + + n_features_in_: + The number of features in the train set used in fit. + + Methods + ------- + fit: + Find the important features. + """ + + def __init__( + self, + estimator, + scoring: str = "roc_auc", + cv=3, + threshold: Union[int, float] = 0.01, + variables: Variables = None, + ): + + if not isinstance(threshold, (int, float)): + raise ValueError("threshold can only be integer or float") + + self.variables = _check_input_parameter_variables(variables) + self.estimator = estimator + self.scoring = scoring + self.threshold = threshold + self.cv = cv + + def fit(self, X: pd.DataFrame, y: pd.Series): + """ + Find initial model performance. Sort features by importance. + + Parameters + ---------- + X: pandas dataframe of shape = [n_samples, n_features] + The input dataframe + + y: array-like of shape (n_samples) + Target variable. Required to train the estimator. + """ + + # check input dataframe + X = _is_dataframe(X) + + # find numerical variables or check variables entered by user + self.variables_ = _find_or_check_numerical_variables(X, self.variables) + + # train model with all features and cross-validation + model = cross_validate( + self.estimator, + X[self.variables_], + y, + cv=self.cv, + scoring=self.scoring, + return_estimator=True, + ) + + # store initial model performance + self.initial_model_performance_ = model["test_score"].mean() + + # Initialize a dataframe that will contain the list of the feature/coeff + # importance for each cross validation fold + feature_importances_cv = pd.DataFrame() + + # Populate the feature_importances_cv dataframe with columns containing + # the feature importance values for each model returned by the cross + # validation. + # There are as many columns as folds. + for i in range(len(model["estimator"])): + m = model["estimator"][i] + feature_importances_cv[i] = get_feature_importances(m) + + # Add the variables as index to feature_importances_cv + feature_importances_cv.index = self.variables_ + + # Aggregate the feature importance returned in each fold + self.feature_importances_ = feature_importances_cv.mean(axis=1) + + return self + + def _more_tags(self): + tags_dict = _return_tags() + tags_dict["variables"] = "numerical" + tags_dict["requires_y"] = True + # add additional test that fails + tags_dict["_xfail_checks"][ + "check_parameters_default_constructible" + ] = "transformer has 1 mandatory parameter" + return tags_dict diff --git a/feature_engine/selection/base_selector.py b/feature_engine/selection/base_selector.py index 3c9d5352b..91ca79a6f 100644 --- a/feature_engine/selection/base_selector.py +++ b/feature_engine/selection/base_selector.py @@ -62,6 +62,7 @@ def transform(self, X: pd.DataFrame): def _more_tags(self): tags_dict = _return_tags() + tags_dict["variables"] = "numerical" # add additional test that fails tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" return tags_dict diff --git a/feature_engine/selection/drop_constant_features.py b/feature_engine/selection/drop_constant_features.py index a3b54048a..67f1dc3ca 100644 --- a/feature_engine/selection/drop_constant_features.py +++ b/feature_engine/selection/drop_constant_features.py @@ -81,7 +81,12 @@ def __init__( self, variables: Variables = None, tol: float = 1, missing_values: str = "raise" ): - if not isinstance(tol, (float, int)) or tol < 0 or tol > 1: + if ( + not isinstance(tol, (float, int)) + or isinstance(tol, bool) + or tol < 0 + or tol > 1 + ): raise ValueError("tol must be a float or integer between 0 and 1") if missing_values not in ["raise", "ignore", "include"]: @@ -160,8 +165,9 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: def _more_tags(self): tags_dict = _return_tags() + tags_dict["allow_nan"] = True + tags_dict["variables"] = "all" # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" tags_dict["_xfail_checks"][ "check_fit2d_1feature" ] = "the transformer needs at least 2 columns to compare, ok to fail" diff --git a/feature_engine/selection/drop_duplicate_features.py b/feature_engine/selection/drop_duplicate_features.py index f9ab09123..5819bcf09 100644 --- a/feature_engine/selection/drop_duplicate_features.py +++ b/feature_engine/selection/drop_duplicate_features.py @@ -145,6 +145,6 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: def _more_tags(self): tags_dict = _return_tags() - # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" + tags_dict["allow_nan"] = True + tags_dict["variables"] = "all" return tags_dict diff --git a/feature_engine/selection/drop_features.py b/feature_engine/selection/drop_features.py index 78a8c0a8d..b2017a84c 100644 --- a/feature_engine/selection/drop_features.py +++ b/feature_engine/selection/drop_features.py @@ -88,8 +88,8 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: def _more_tags(self): tags_dict = _return_tags() + tags_dict["allow_nan"] = True # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" tags_dict["_xfail_checks"][ "check_parameters_default_constructible" ] = "transformer has 1 mandatory parameter" diff --git a/feature_engine/selection/recursive_feature_addition.py b/feature_engine/selection/recursive_feature_addition.py index a93cf7ea7..c979f3e26 100644 --- a/feature_engine/selection/recursive_feature_addition.py +++ b/feature_engine/selection/recursive_feature_addition.py @@ -1,20 +1,10 @@ -from typing import List, Union - import pandas as pd from sklearn.model_selection import cross_validate -from feature_engine.dataframe_checks import _is_dataframe -from feature_engine.selection.base_selector import BaseSelector, get_feature_importances -from feature_engine.validation import _return_tags -from feature_engine.variable_manipulation import ( - _check_input_parameter_variables, - _find_or_check_numerical_variables, -) - -Variables = Union[None, int, str, List[Union[str, int]]] +from feature_engine.selection.base_recursive_selector import BaseRecursiveSelector -class RecursiveFeatureAddition(BaseSelector): +class RecursiveFeatureAddition(BaseRecursiveSelector): """ RecursiveFeatureAddition() selects features following a recursive addition process. @@ -111,24 +101,6 @@ class RecursiveFeatureAddition(BaseSelector): Fit to data, then transform it. """ - def __init__( - self, - estimator, - scoring: str = "roc_auc", - cv=3, - threshold: Union[int, float] = 0.01, - variables: Variables = None, - ): - - if not isinstance(threshold, (int, float)): - raise ValueError("threshold can only be integer or float") - - self.variables = _check_input_parameter_variables(variables) - self.estimator = estimator - self.scoring = scoring - self.threshold = threshold - self.cv = cv - def fit(self, X: pd.DataFrame, y: pd.Series): """ Find the important features. Note that the selector trains various models at @@ -143,42 +115,7 @@ def fit(self, X: pd.DataFrame, y: pd.Series): Target variable. Required to train the estimator. """ - # check input dataframe - X = _is_dataframe(X) - - # find numerical variables or check variables entered by user - self.variables_ = _find_or_check_numerical_variables(X, self.variables) - - # train model with all features and cross-validation - model = cross_validate( - self.estimator, - X[self.variables_], - y, - cv=self.cv, - scoring=self.scoring, - return_estimator=True, - ) - - # store initial model performance - self.initial_model_performance_ = model["test_score"].mean() - - # Initialize a dataframe that will contain the list of the feature/coeff - # importance for each cross validation fold - feature_importances_cv = pd.DataFrame() - - # Populate the feature_importances_cv dataframe with columns containing - # the feature importance values for each model returned by the cross - # validation. - # There are as many columns as folds. - for m in model["estimator"]: - - feature_importances_cv[m] = get_feature_importances(m) - - # Add the variables as index to feature_importances_cv - feature_importances_cv.index = self.variables_ - - # Aggregate the feature importance returned in each fold - self.feature_importances_ = feature_importances_cv.mean(axis=1) + super().fit(X, y) # Sort the feature importance values decreasingly self.feature_importances_.sort_values(ascending=False, inplace=True) @@ -254,13 +191,4 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: return X - transform.__doc__ = BaseSelector.transform.__doc__ - - def _more_tags(self): - tags_dict = _return_tags() - # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" - tags_dict["_xfail_checks"][ - "check_parameters_default_constructible" - ] = "transformer has 1 mandatory parameter" - return tags_dict + transform.__doc__ = BaseRecursiveSelector.transform.__doc__ diff --git a/feature_engine/selection/recursive_feature_elimination.py b/feature_engine/selection/recursive_feature_elimination.py index 3c1834516..91557144a 100644 --- a/feature_engine/selection/recursive_feature_elimination.py +++ b/feature_engine/selection/recursive_feature_elimination.py @@ -1,20 +1,10 @@ -from typing import List, Union - import pandas as pd from sklearn.model_selection import cross_validate -from feature_engine.dataframe_checks import _is_dataframe -from feature_engine.selection.base_selector import BaseSelector, get_feature_importances -from feature_engine.validation import _return_tags -from feature_engine.variable_manipulation import ( - _check_input_parameter_variables, - _find_or_check_numerical_variables, -) - -Variables = Union[None, int, str, List[Union[str, int]]] +from feature_engine.selection.base_recursive_selector import BaseRecursiveSelector -class RecursiveFeatureElimination(BaseSelector): +class RecursiveFeatureElimination(BaseRecursiveSelector): """ RecursiveFeatureElimination() selects features following a recursive elimination process. @@ -111,24 +101,6 @@ class RecursiveFeatureElimination(BaseSelector): Fit to data, then transform it. """ - def __init__( - self, - estimator, - scoring: str = "roc_auc", - cv=3, - threshold: Union[int, float] = 0.01, - variables: Variables = None, - ): - - if not isinstance(threshold, (int, float)): - raise ValueError("threshold can only be integer or float") - - self.variables = _check_input_parameter_variables(variables) - self.estimator = estimator - self.scoring = scoring - self.threshold = threshold - self.cv = cv - def fit(self, X: pd.DataFrame, y: pd.Series): """ Find the important features. Note that the selector trains various models at @@ -142,44 +114,9 @@ def fit(self, X: pd.DataFrame, y: pd.Series): Target variable. Required to train the estimator. """ - # check input dataframe - X = _is_dataframe(X) - - # find numerical variables or check variables entered by user - self.variables_ = _find_or_check_numerical_variables(X, self.variables) - - # train model with all features and cross-validation - model = cross_validate( - self.estimator, - X[self.variables_], - y, - cv=self.cv, - scoring=self.scoring, - return_estimator=True, - ) + super().fit(X, y) - # store initial model performance - self.initial_model_performance_ = model["test_score"].mean() - - # Initialize a dataframe that will contain the list of the feature/coeff - # importance for each cross validation fold - feature_importances_cv = pd.DataFrame() - - # Populate the feature_importances_cv dataframe with columns containing - # the feature importance values for each model returned by the cross - # validation. - # There are as many columns as folds. - for m in model["estimator"]: - - feature_importances_cv[m] = get_feature_importances(m) - - # Add the variables as index to feature_importances_cv - feature_importances_cv.index = self.variables_ - - # Aggregate the feature importance returned in each fold - self.feature_importances_ = feature_importances_cv.mean(axis=1) - - # Sort the feature importance values + # Sort the feature importance values increasingly self.feature_importances_.sort_values(ascending=True, inplace=True) # to collect selected features @@ -251,13 +188,4 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: return X - transform.__doc__ = BaseSelector.transform.__doc__ - - def _more_tags(self): - tags_dict = _return_tags() - # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" - tags_dict["_xfail_checks"][ - "check_parameters_default_constructible" - ] = "transformer has 1 mandatory parameter" - return tags_dict + transform.__doc__ = BaseRecursiveSelector.transform.__doc__ diff --git a/feature_engine/selection/shuffle_features.py b/feature_engine/selection/shuffle_features.py index 13091cdf8..a78adf4e9 100644 --- a/feature_engine/selection/shuffle_features.py +++ b/feature_engine/selection/shuffle_features.py @@ -242,6 +242,8 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: def _more_tags(self): tags_dict = _return_tags() + tags_dict["variables"] = "numerical" + tags_dict["requires_y"] = True # add additional test that fails tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" tags_dict["_xfail_checks"][ diff --git a/feature_engine/selection/single_feature_performance.py b/feature_engine/selection/single_feature_performance.py index c3bbc9b21..e62e889dc 100644 --- a/feature_engine/selection/single_feature_performance.py +++ b/feature_engine/selection/single_feature_performance.py @@ -200,8 +200,10 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: def _more_tags(self): tags_dict = _return_tags() + tags_dict["allow_nan"] = True + tags_dict["variables"] = "numerical" + tags_dict["requires_y"] = True # add additional test that fails - tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA" tags_dict["_xfail_checks"][ "check_parameters_default_constructible" ] = "transformer has 1 mandatory parameter" diff --git a/feature_engine/selection/target_mean_selection.py b/feature_engine/selection/target_mean_selection.py index 6eba35015..9353ffc81 100644 --- a/feature_engine/selection/target_mean_selection.py +++ b/feature_engine/selection/target_mean_selection.py @@ -16,6 +16,7 @@ _check_input_parameter_variables, _find_all_variables, ) +from feature_engine.validation import _return_tags Variables = Union[None, int, str, List[Union[str, int]]] @@ -200,9 +201,7 @@ def fit(self, X: pd.DataFrame, y: pd.Series): # find categorical and numerical variables self.variables_categorical_ = list(X.select_dtypes(include="O").columns) - self.variables_numerical_ = list( - X.select_dtypes(include="number").columns - ) + self.variables_numerical_ = list(X.select_dtypes(include="number").columns) # obtain cross-validation indeces skf = KFold(n_splits=self.cv, shuffle=True, random_state=self.random_state) @@ -312,3 +311,11 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: return X transform.__doc__ = BaseSelector.transform.__doc__ + + def _more_tags(self): + tags_dict = _return_tags() + tags_dict["allow_nan"] = True + tags_dict["variables"] = "all" + tags_dict["requires_y"] = True + + return tags_dict diff --git a/tests/check_estimators_with_parametrize_tests.py b/tests/check_estimators_with_parametrize_tests.py index 5b54efe9a..2f9829664 100644 --- a/tests/check_estimators_with_parametrize_tests.py +++ b/tests/check_estimators_with_parametrize_tests.py @@ -2,8 +2,8 @@ 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. """ -from sklearn.ensemble import RandomForestClassifier from sklearn.impute import SimpleImputer +from sklearn.linear_model import LogisticRegression from sklearn.utils.estimator_checks import parametrize_with_checks from feature_engine.encoding import ( @@ -31,6 +31,7 @@ DropCorrelatedFeatures, DropDuplicateFeatures, DropFeatures, + DropHighPSIFeatures, RecursiveFeatureAddition, RecursiveFeatureElimination, SelectByShuffling, @@ -53,7 +54,7 @@ [ MeanMedianImputer(), ArbitraryNumberImputer(), - CategoricalImputer(ignore_format=True), + CategoricalImputer(fill_value=0, ignore_format=True), EndTailImputer(), AddMissingIndicator(), RandomSampleImputer(), @@ -64,10 +65,11 @@ def test_sklearn_compatible_imputer(estimator, check): check(estimator) +# encoding @parametrize_with_checks( [ CountFrequencyEncoder(ignore_format=True), - DecisionTreeEncoder(ignore_format=True), + DecisionTreeEncoder(regression=False, ignore_format=True), MeanEncoder(ignore_format=True), OneHotEncoder(ignore_format=True), OrdinalEncoder(ignore_format=True), @@ -85,6 +87,7 @@ def test_sklearn_compatible_encoder(estimator, check): check(estimator) +# outliers @parametrize_with_checks( [ ArbitraryOutlierCapper(max_capping_dict={"0": 10}), @@ -96,6 +99,7 @@ def test_sklearn_compatible_outliers(estimator, check): check(estimator) +# transformers @parametrize_with_checks( [ BoxCoxTransformer(), @@ -109,22 +113,26 @@ def test_sklearn_compatible_transformer(estimator, check): check(estimator) +# selectors @parametrize_with_checks( [ DropFeatures(features_to_drop=["0"]), - DropConstantFeatures(), + DropConstantFeatures(missing_values="ignore"), DropDuplicateFeatures(), DropCorrelatedFeatures(), SmartCorrelatedSelection(), - SelectByShuffling(RandomForestClassifier(random_state=1), scoring="accuracy"), + DropHighPSIFeatures(bins=5), + SelectByShuffling( + LogisticRegression(max_iter=2, random_state=1), scoring="accuracy" + ), SelectBySingleFeaturePerformance( - RandomForestClassifier(random_state=1), scoring="accuracy" + LogisticRegression(max_iter=2, random_state=1), scoring="accuracy" ), RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), scoring="accuracy" + LogisticRegression(max_iter=2, random_state=1), scoring="accuracy" ), RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), scoring="accuracy" + LogisticRegression(max_iter=2, random_state=1), scoring="accuracy" ), SelectByTargetMeanPerformance(scoring="r2_score", bins=3), ] @@ -133,6 +141,7 @@ def test_sklearn_compatible_selectors(estimator, check): check(estimator) +# wrappers @parametrize_with_checks([SklearnTransformerWrapper(SimpleImputer())]) def test_sklearn_compatible_wrapper(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 f62757d07..ce4ce5d42 100644 --- a/tests/test_encoding/test_check_estimator_encoders.py +++ b/tests/test_encoding/test_check_estimator_encoders.py @@ -17,7 +17,7 @@ "Estimator", [ CountFrequencyEncoder(ignore_format=True), - DecisionTreeEncoder(ignore_format=True), + DecisionTreeEncoder(regression=False, ignore_format=True), MeanEncoder(ignore_format=True), OneHotEncoder(ignore_format=True), OrdinalEncoder(ignore_format=True), diff --git a/tests/test_imputation/test_check_estimator_imputers.py b/tests/test_imputation/test_check_estimator_imputers.py index 0adcab14a..eaa71baef 100644 --- a/tests/test_imputation/test_check_estimator_imputers.py +++ b/tests/test_imputation/test_check_estimator_imputers.py @@ -17,7 +17,7 @@ [ MeanMedianImputer(), ArbitraryNumberImputer(), - CategoricalImputer(ignore_format=True), + CategoricalImputer(fill_value=0, ignore_format=True), EndTailImputer(), AddMissingIndicator(), RandomSampleImputer(), diff --git a/tests/test_selection/test_check_estimator_selectors.py b/tests/test_selection/test_check_estimator_selectors.py index 8be4bd7de..84ae7b521 100644 --- a/tests/test_selection/test_check_estimator_selectors.py +++ b/tests/test_selection/test_check_estimator_selectors.py @@ -1,7 +1,8 @@ import pytest -from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import LogisticRegression from sklearn.utils.estimator_checks import check_estimator +from feature_engine.estimator_checks import check_feature_engine_estimator from feature_engine.selection import ( DropConstantFeatures, DropCorrelatedFeatures, @@ -12,32 +13,45 @@ RecursiveFeatureElimination, SelectByShuffling, SelectBySingleFeaturePerformance, - SelectByTargetMeanPerformance, + # SelectByTargetMeanPerformance, SmartCorrelatedSelection, ) +_logreg = LogisticRegression(C=0.0001, max_iter=2, random_state=1) -@pytest.mark.parametrize( - "Estimator", - [ - DropFeatures(features_to_drop=["0"]), - DropConstantFeatures(), - DropDuplicateFeatures(), - DropCorrelatedFeatures(), - DropHighPSIFeatures(bins=5), - SmartCorrelatedSelection(), - SelectByShuffling(RandomForestClassifier(random_state=1), scoring="accuracy"), - SelectBySingleFeaturePerformance( - RandomForestClassifier(random_state=1), scoring="accuracy" - ), - RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), scoring="accuracy" - ), - RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), scoring="accuracy" - ), - SelectByTargetMeanPerformance(scoring="r2_score", bins=3), - ], -) -def test_all_transformers(Estimator): - return check_estimator(Estimator) +_estimators = [ + DropFeatures(features_to_drop=["0"]), + DropConstantFeatures(missing_values="ignore"), + DropDuplicateFeatures(), + DropCorrelatedFeatures(), + DropHighPSIFeatures(bins=5), + SmartCorrelatedSelection(), + SelectByShuffling(estimator=_logreg, scoring="accuracy"), + SelectBySingleFeaturePerformance(estimator=_logreg, scoring="accuracy"), + # FIXME: as part of PR 358 + # SelectByTargetMeanPerformance(scoring="r2_score", bins=3), + RecursiveFeatureAddition(estimator=_logreg, scoring="accuracy"), + RecursiveFeatureElimination(estimator=_logreg, scoring="accuracy"), +] + + +# the RFA and RFE fail most tests. I think it has to do +# with the numpy arrays used in sklearn tests. +@pytest.mark.parametrize("estimator", _estimators[:-2]) +def test_check_estimator_from_sklearn(estimator): + return check_estimator(estimator) + + +@pytest.mark.parametrize("estimator", _estimators) +def test_check_estimator_from_feature_engine(estimator): + if estimator.__class__.__name__ == "DropFeatures": + estimator.set_params(features_to_drop=["var_1"]) + return check_feature_engine_estimator(estimator) + + +@pytest.mark.parametrize("estimator", _estimators[7:10]) +def test_raises_error_when_no_estimator_passed(estimator): + # this selectors need an estimator as an input param + # test error otherwise. + with pytest.raises(TypeError): + estimator() diff --git a/tests/test_selection/test_drop_constant_features.py b/tests/test_selection/test_drop_constant_features.py index 9d75f8ab3..a03d9e25e 100644 --- a/tests/test_selection/test_drop_constant_features.py +++ b/tests/test_selection/test_drop_constant_features.py @@ -1,7 +1,6 @@ import numpy as np import pandas as pd import pytest -from sklearn.exceptions import NotFittedError from feature_engine.selection import DropConstantFeatures @@ -42,23 +41,8 @@ def test_drop_constant_features(df_constant_features): } ) - # init params - assert transformer.tol == 1 - assert transformer.variables is None - # fit attributes - assert transformer.variables_ == [ - "Name", - "City", - "Age", - "Marks", - "dob", - "const_feat_num", - "const_feat_cat", - "quasi_feat_num", - "quasi_feat_cat", - ] + # fit attribute assert transformer.features_to_drop_ == ["const_feat_num", "const_feat_cat"] - assert transformer.n_features_in_ == 9 # transform output pd.testing.assert_frame_equal(X, df) @@ -79,29 +63,13 @@ def test_drop_constant_and_quasiconstant_features(df_constant_features): } ) - # init params - assert transformer.tol == 0.7 - assert transformer.variables is None - # fit attr - assert transformer.variables_ == [ - "Name", - "City", - "Age", - "Marks", - "dob", - "const_feat_num", - "const_feat_cat", - "quasi_feat_num", - "quasi_feat_cat", - ] assert transformer.features_to_drop_ == [ "const_feat_num", "const_feat_cat", "quasi_feat_num", "quasi_feat_cat", ] - assert transformer.n_features_in_ == 9 # transform params pd.testing.assert_frame_equal(X, df) @@ -127,49 +95,38 @@ def test_drop_constant_features_with_list_of_variables(df_constant_features): } ) - # init params - assert transformer.tol == 0.7 - assert transformer.variables == ["Name", "const_feat_num", "quasi_feat_num"] - # fit attr assert transformer.features_to_drop_ == ["const_feat_num", "quasi_feat_num"] - assert transformer.n_features_in_ == 9 # transform params pd.testing.assert_frame_equal(X, df) -def test_error_if_fit_input_not_df(): - # test case 4: input is not a dataframe - with pytest.raises(TypeError): - DropConstantFeatures().fit({"Name": ["Karthik"]}) - - -def test_error_if_tol_out_of_range(): +@pytest.mark.parametrize("tol", [2, "hola", False]) +def test_error_if_tol_value_not_allowed(tol): # test case 5: threshold not between 0 and 1 with pytest.raises(ValueError): - DropConstantFeatures(tol=2) + DropConstantFeatures(tol=tol) -def test_error_if_tol_is_string(): - # test case 5: threshold not between 0 and 1 - with pytest.raises(ValueError): - DropConstantFeatures(tol="hola") +@pytest.mark.parametrize("tol", [1, 0, 0.5, 0.7]) +def test_tol_init_param(tol): + sel = DropConstantFeatures(tol=tol) + assert sel.tol == tol -def test_error_if_missing_values_not_permitted(): +@pytest.mark.parametrize("missing", [2, "hola", False]) +def test_error_if_missing_values_not_permitted(missing): # test case 5: threshold not between 0 and 1 with pytest.raises(ValueError): - DropConstantFeatures(missing_values="hola") + DropConstantFeatures(missing_values=missing) -def test_error_if_input_all_constant_features(): +def test_error_if_all_constant_and_quasi_constant_features(): # test case 6: when input contains all constant features with pytest.raises(ValueError): DropConstantFeatures().fit(pd.DataFrame({"col1": [1, 1, 1], "col2": [1, 1, 1]})) - -def test_error_if_all_constant_and_quasi_constant_features(): # test case 7: when input contains all constant and quasi constant features with pytest.raises(ValueError): transformer = DropConstantFeatures(tol=0.7) @@ -185,14 +142,7 @@ def test_error_if_all_constant_and_quasi_constant_features(): ) -def test_non_fitted_error(df_constant_features): - # test case 8: when fit is not called prior to transform - with pytest.raises(NotFittedError): - transformer = DropConstantFeatures() - transformer.transform(df_constant_features) - - -def test_missing_values_param(): +def test_missing_values_param_functionality(): df = { "Name": ["tom", "nick", "krish", "jack"], @@ -208,8 +158,8 @@ def test_missing_values_param(): df = pd.DataFrame(df) # test raises error if there is na + transformer = DropConstantFeatures(missing_values="raise") with pytest.raises(ValueError): - transformer = DropConstantFeatures(missing_values="raise") transformer.fit(df) # test ignores na diff --git a/tests/test_selection/test_drop_correlated_features.py b/tests/test_selection/test_drop_correlated_features.py index 43b5f41d1..943ac8ed7 100644 --- a/tests/test_selection/test_drop_correlated_features.py +++ b/tests/test_selection/test_drop_correlated_features.py @@ -1,7 +1,6 @@ import pandas as pd import pytest from sklearn.datasets import make_classification -from sklearn.exceptions import NotFittedError from feature_engine.selection import DropCorrelatedFeatures @@ -58,17 +57,8 @@ def test_default_params(df_correlated_single): # test init params assert transformer.method == "pearson" assert transformer.threshold == 0.8 - assert transformer.variables is None # test fit attrs - assert transformer.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - ] assert transformer.features_to_drop_ == {"var_2"} assert transformer.correlated_feature_sets_ == [{"var_1", "var_2"}] # test transform output @@ -87,17 +77,8 @@ def test_lower_threshold(df_correlated_single): # test init params assert transformer.method == "pearson" assert transformer.threshold == 0.6 - assert transformer.variables is None # test fit attrs - assert transformer.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - ] assert transformer.features_to_drop_ == {"var_2", "var_4"} assert transformer.correlated_feature_sets_ == [{"var_1", "var_2", "var_4"}] # test transform output @@ -142,19 +123,6 @@ def test_callable_method(df_correlated_double, random_uniform_method): assert transformer.n_features_in_ == len(X.columns) -def test_error_if_fit_input_not_dataframe(): - with pytest.raises(TypeError): - # Next line needs review - DropCorrelatedFeatures().fit({"Name": [1]}) - - -def test_non_fitted_error(df_correlated_single): - # when fit is not called prior to transform - with pytest.raises(NotFittedError): - transformer = DropCorrelatedFeatures() - transformer.transform(df_correlated_single) - - def test_error_method_supplied(df_correlated_double): X = df_correlated_double diff --git a/tests/test_selection/test_drop_duplicate_features.py b/tests/test_selection/test_drop_duplicate_features.py index 25c52828c..d461f507a 100644 --- a/tests/test_selection/test_drop_duplicate_features.py +++ b/tests/test_selection/test_drop_duplicate_features.py @@ -1,7 +1,6 @@ import numpy as np import pandas as pd import pytest -from sklearn.exceptions import NotFittedError from feature_engine.selection import DropDuplicateFeatures @@ -61,13 +60,6 @@ def test_drop_duplicates_features(df_duplicate_features): pd.testing.assert_frame_equal(X, df) -def test_variables_assigned_correctly(df_duplicate_features): - transformer = DropDuplicateFeatures() - transformer.fit(df_duplicate_features) - assert transformer.variables is None - assert transformer.variables_ == (list(df_duplicate_features.columns)) - - def test_fit_attributes(df_duplicate_features): transformer = DropDuplicateFeatures() transformer.fit(df_duplicate_features) @@ -78,7 +70,6 @@ def test_fit_attributes(df_duplicate_features): {"City", "City2"}, {"Age", "Age2"}, ] - assert transformer.n_features_in_ == 9 def test_with_df_with_na(df_duplicate_features_with_na): @@ -103,16 +94,3 @@ def test_with_df_with_na(df_duplicate_features_with_na): {"City", "City2"}, {"Age", "Age2"}, ] - assert transformer.n_features_in_ == 9 - - -def test_error_if_fit_input_not_dataframe(): - with pytest.raises(TypeError): - DropDuplicateFeatures().fit({"Name": ["Karthik"]}) - - -def test_non_fitted_error(df_duplicate_features): - # test case 3: when fit is not called prior to transform - with pytest.raises(NotFittedError): - transformer = DropDuplicateFeatures() - transformer.transform(df_duplicate_features) diff --git a/tests/test_selection/test_drop_features.py b/tests/test_selection/test_drop_features.py index 5d036b12d..d463df0a3 100644 --- a/tests/test_selection/test_drop_features.py +++ b/tests/test_selection/test_drop_features.py @@ -1,6 +1,5 @@ import pandas as pd import pytest -from sklearn.exceptions import NotFittedError from feature_engine.selection import DropFeatures @@ -20,8 +19,7 @@ def test_drop_2_variables(df_vartypes): # init params assert transformer.features_to_drop == ["City", "dob"] - # fit attr - assert transformer.n_features_in_ == 5 + # transform params assert X.shape == (4, 3) assert type(X) == pd.DataFrame @@ -35,13 +33,6 @@ def test_error_if_non_existing_variables(df_vartypes): transformer.fit_transform(df_vartypes) -def test_error_if_fit_input_not_dataframe(): - # test case 3: passing a different input than dataframe - with pytest.raises(TypeError): - transformer = DropFeatures(features_to_drop=["Name"]) - transformer.fit({"Name": ["Karthik"]}) - - def test_error_when_returning_empty_dataframe(df_vartypes): # test case 5: dropping all columns produces warning check with pytest.raises(ValueError): @@ -71,14 +62,5 @@ def test_drop_2_variables_integer_colnames(df_numeric_columns): # init params assert transformer.features_to_drop == [0, 1] - # fit attr - assert transformer.n_features_in_ == 5 # transform params pd.testing.assert_frame_equal(X, df) - - -def test_non_fitted_error(df_numeric_columns): - # test case 8: when fit is not called prior to transform - with pytest.raises(NotFittedError): - transformer = DropFeatures(features_to_drop=[0, 1]) - transformer.transform(df_numeric_columns) diff --git a/tests/test_selection/test_drop_high_psi_features.py b/tests/test_selection/test_drop_high_psi_features.py index d65d9e332..6c6d584fb 100644 --- a/tests/test_selection/test_drop_high_psi_features.py +++ b/tests/test_selection/test_drop_high_psi_features.py @@ -4,7 +4,6 @@ import pandas as pd import pytest from sklearn.datasets import make_classification -from sklearn.exceptions import NotFittedError from feature_engine.selection import DropHighPSIFeatures @@ -89,7 +88,6 @@ def test_fit_attributes(df): ] assert transformer.psi_values_ == pytest.approx(expected_psi, 12) assert transformer.features_to_drop_ == ["drift_1", "drift_2"] - assert transformer.n_features_in_ == 8 # ================ test init parameters ================= @@ -637,10 +635,3 @@ def test_transform_different_number_of_columns(df): with pytest.raises(ValueError): test.transform(data) - - -def test_non_fitted_error(df): - """Error is raised when fit is not called prior to transform.""" - with pytest.raises(NotFittedError): - transformer = DropHighPSIFeatures() - transformer.transform(df) diff --git a/tests/test_selection/test_recursive_feature_addition.py b/tests/test_selection/test_recursive_feature_addition.py index e035f3de6..a22c3a1f7 100644 --- a/tests/test_selection/test_recursive_feature_addition.py +++ b/tests/test_selection/test_recursive_feature_addition.py @@ -1,303 +1,188 @@ -import numpy as np import pandas as pd import pytest from sklearn.ensemble import RandomForestClassifier -from sklearn.exceptions import NotFittedError -from sklearn.linear_model import LinearRegression -from sklearn.model_selection import KFold, StratifiedKFold +from sklearn.linear_model import Lasso, LogisticRegression from sklearn.tree import DecisionTreeRegressor from feature_engine.selection import RecursiveFeatureAddition - -def test_classification_threshold_parameters(df_test): +# tests for classification +_model_and_expectations = [ + ( + RandomForestClassifier(n_estimators=5, random_state=1), + 3, + 0.001, + "roc_auc", + [ + "var_0", + "var_1", + "var_2", + "var_3", + "var_5", + "var_6", + "var_8", + "var_9", + "var_10", + "var_11", + ], + { + "var_4": 0, + "var_7": 0.0241, + "var_6": -0.001, + "var_9": -0.001, + "var_0": -0.0, + "var_8": -0.0011, + "var_10": -0.0011, + "var_11": -0.001, + "var_1": -0.0, + "var_2": -0.0001, + "var_3": -0.0011, + "var_5": -0.0001, + }, + ), + ( + LogisticRegression(random_state=10), + 2, + 0.0001, + "accuracy", + [ + "var_1", + "var_2", + "var_3", + "var_4", + "var_5", + "var_6", + "var_9", + "var_10", + "var_11", + ], + { + "var_7": 0, + "var_8": 0.001, + "var_0": 0.002, + "var_6": -0.001, + "var_4": 0.0, + "var_11": -0.001, + "var_1": -0.001, + "var_5": -0.003, + "var_3": -0.002, + "var_10": 0.0, + "var_9": 0.0, + "var_2": 0.0, + }, + ), +] + + +@pytest.mark.parametrize( + "estimator, cv, threshold, scoring, dropped_features, performances", + _model_and_expectations, +) +def test_classification( + estimator, cv, threshold, scoring, dropped_features, performances, df_test +): X, y = df_test sel = RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), threshold=0.001 + estimator=estimator, cv=cv, threshold=threshold, scoring=scoring ) - sel.fit(X, y) - # expected result - Xtransformed = X[["var_7", "var_10"]].copy() - - # expected ordered features by importance, from most important - # to least important - ordered_features = [ - "var_7", - "var_4", - "var_6", - "var_9", - "var_0", - "var_8", - "var_1", - "var_10", - "var_5", - "var_11", - "var_2", - "var_3", - ] + sel.fit(X, y) - # test init params - assert sel.variables is None - assert sel.threshold == 0.001 - assert sel.cv == 3 - assert sel.scoring == "roc_auc" + Xtransformed = X.copy() + Xtransformed = Xtransformed.drop(labels=dropped_features, axis=1) # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] - assert np.round(sel.initial_model_performance_, 3) == 0.997 - assert sel.features_to_drop_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_8", - "var_9", - "var_11", - ] - assert list(sel.performance_drifts_.keys()) == ordered_features - # test transform output - pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) + assert sel.features_to_drop_ == dropped_features + assert len(sel.performance_drifts_.keys()) == len(X.columns) + assert all([var in sel.performance_drifts_.keys() for var in X.columns]) + rounded_perfs = { + key: round(sel.performance_drifts_[key], 4) for key in sel.performance_drifts_ + } + assert rounded_perfs == performances -def test_regression_cv_3_and_r2(load_diabetes_dataset): - # test for regression using cv=3, and the r2 as metric. - X, y = load_diabetes_dataset - - sel = RecursiveFeatureAddition(estimator=LinearRegression(), scoring="r2", cv=3) - sel.fit(X, y) - - # expected output - Xtransformed = X[[2, 3, 4, 8]].copy() - - # expected ordred features by importance, from most important - # to least important - ordered_features = [4, 8, 2, 5, 3, 1, 7, 6, 9, 0] - - # test init params - assert sel.cv == 3 - assert sel.variables is None - assert sel.scoring == "r2" - assert sel.threshold == 0.01 - # fit params - assert sel.variables_ == list(X.columns) - assert np.round(sel.initial_model_performance_, 3) == 0.489 - assert sel.features_to_drop_ == [0, 1, 5, 6, 7, 9] - assert list(sel.performance_drifts_.keys()) == ordered_features # test transform output pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) -def test_regression_cv_2_and_mse(load_diabetes_dataset): - # test for regression using cv=2, and the neg_mean_squared_error as metric. - # add suitable threshold for regression mse +# tests for regression +_model_and_expectations = [ + ( + Lasso(alpha=0.001, random_state=10), + 3, + 0.1, + "r2", + [0, 1, 3, 4, 5, 6, 7, 9], + { + 8: 0, + 4: 0.0059, + 2: 0.1367, + 5: -0.0026, + 3: 0.0177, + 1: -0.0045, + 7: -0.0035, + 6: 0.0088, + 9: 0.002, + 0: -0.0114, + }, + ), + ( + DecisionTreeRegressor(random_state=10), + 2, + 100, + "neg_mean_squared_error", + [0, 3, 4, 5, 6, 7, 8, 9], + { + 2: 0, + 8: -679.4093, + 5: -943.6299, + 7: 99.315, + 3: -195.385, + 9: -716.6378, + 6: -1701.2939, + 0: -1693.8544, + 4: -781.6593, + 1: 106.9272, + }, + ), +] + + +@pytest.mark.parametrize( + "estimator, cv, threshold, scoring, dropped_features, performances", + _model_and_expectations, +) +def test_regression( + estimator, + cv, + threshold, + scoring, + dropped_features, + performances, + load_diabetes_dataset, +): + # test for regression using cv=3, and the r2 as metric. X, y = load_diabetes_dataset sel = RecursiveFeatureAddition( - estimator=DecisionTreeRegressor(random_state=0), - scoring="neg_mean_squared_error", - cv=2, - threshold=10, + estimator=estimator, cv=cv, threshold=threshold, scoring=scoring ) - # fit transformer - sel.fit(X, y) - - # expected output - Xtransformed = X[[1, 2, 7]].copy() - - # expected ordred features by importance, from most important - # to least important - ordered_features = [2, 8, 5, 7, 3, 9, 6, 4, 0, 1] - - # test init params - assert sel.cv == 2 - assert sel.variables is None - assert sel.scoring == "neg_mean_squared_error" - assert sel.threshold == 10 - # fit params - assert sel.variables_ == list(X.columns) - assert np.round(sel.initial_model_performance_, 0) == -5836.0 - assert sel.features_to_drop_ == [0, 3, 4, 5, 6, 8, 9] - assert list(sel.performance_drifts_.keys()) == ordered_features - # test transform output - pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - - -def test_non_fitted_error(df_test): - # when fit is not called prior to transform - with pytest.raises(NotFittedError): - sel = RecursiveFeatureAddition(RandomForestClassifier(random_state=1)) - sel.transform(df_test) - -def test_raises_threshold_error(): - with pytest.raises(ValueError): - RecursiveFeatureAddition(RandomForestClassifier(random_state=1), threshold=None) - - -def test_automatic_variable_selection(df_test): - X, y = df_test - - # add 2 additional categorical variables, these should not be evaluated by - # the selector - X["cat_1"] = "cat1" - X["cat_2"] = "cat2" - - sel = RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), threshold=0.001 - ) sel.fit(X, y) - # expected result - Xtransformed = X[["var_7", "var_10", "cat_1", "cat_2"]].copy() + Xtransformed = X.copy() + Xtransformed = Xtransformed.drop(labels=dropped_features, axis=1) - # expected ordered features by importance, from most important - # to least important - ordered_features = [ - "var_7", - "var_4", - "var_6", - "var_9", - "var_0", - "var_8", - "var_1", - "var_10", - "var_5", - "var_11", - "var_2", - "var_3", - ] - - # test init params - assert sel.variables is None - assert sel.threshold == 0.001 - assert sel.cv == 3 - assert sel.scoring == "roc_auc" # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] - assert np.round(sel.initial_model_performance_, 3) == 0.997 - assert sel.features_to_drop_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_8", - "var_9", - "var_11", - ] - assert list(sel.performance_drifts_.keys()) == ordered_features - # test transform output - pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - - -def test_KFold_generators(df_test): - - X, y = df_test - - # Kfold - sel = RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), - threshold=0.001, - cv=KFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) + assert sel.features_to_drop_ == dropped_features - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) + assert len(sel.performance_drifts_.keys()) == len(X.columns) + assert all([var in sel.performance_drifts_.keys() for var in X.columns]) + rounded_perfs = { + key: round(sel.performance_drifts_[key], 4) for key in sel.performance_drifts_ + } + assert rounded_perfs == performances - # Stratfied - sel = RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), - threshold=0.001, - cv=StratifiedKFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) - - # None - sel = RecursiveFeatureAddition( - RandomForestClassifier(random_state=1), - threshold=0.001, - cv=None, - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) + # test transform output + pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) diff --git a/tests/test_selection/test_recursive_feature_elimination.py b/tests/test_selection/test_recursive_feature_elimination.py index 7b1870b57..a3ee45909 100644 --- a/tests/test_selection/test_recursive_feature_elimination.py +++ b/tests/test_selection/test_recursive_feature_elimination.py @@ -1,262 +1,188 @@ -import numpy as np import pandas as pd import pytest from sklearn.ensemble import RandomForestClassifier -from sklearn.exceptions import NotFittedError -from sklearn.linear_model import LinearRegression -from sklearn.model_selection import KFold, StratifiedKFold +from sklearn.linear_model import Lasso, LogisticRegression from sklearn.tree import DecisionTreeRegressor from feature_engine.selection import RecursiveFeatureElimination - -def test_classification_threshold_parameters(df_test): +# tests for classification +_model_and_expectations = [ + ( + RandomForestClassifier(n_estimators=5, random_state=1), + 3, + 0.001, + "roc_auc", + [ + "var_1", + "var_2", + "var_3", + "var_5", + "var_6", + "var_7", + "var_8", + "var_9", + "var_10", + "var_11", + ], + { + "var_5": -0.0, + "var_3": 0.0009, + "var_2": -0.0001, + "var_1": -0.002, + "var_11": 0.001, + "var_10": 0.0009, + "var_8": 0.0001, + "var_0": 0.0019, + "var_9": 0.0, + "var_6": -0.0, + "var_7": -0.0015, + "var_4": 0.4149, + }, + ), + ( + LogisticRegression(random_state=10), + 2, + 0.0001, + "accuracy", + [ + "var_1", + "var_2", + "var_3", + "var_4", + "var_5", + "var_6", + "var_9", + "var_10", + "var_11", + ], + { + "var_2": 0.0, + "var_9": 0.0, + "var_10": 0.0, + "var_3": 0.0, + "var_5": -0.001, + "var_1": 0.0, + "var_11": 0.0, + "var_4": -0.001, + "var_6": -0.001, + "var_0": 0.002, + "var_8": 0.002, + "var_7": 0.004, + }, + ), +] + + +@pytest.mark.parametrize( + "estimator, cv, threshold, scoring, dropped_features, performances", + _model_and_expectations, +) +def test_classification( + estimator, cv, threshold, scoring, dropped_features, performances, df_test +): X, y = df_test + sel = RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), threshold=0.001 + estimator=estimator, cv=cv, threshold=threshold, scoring=scoring ) - sel.fit(X, y) - - # expected result - Xtransformed = X[["var_0", "var_6"]].copy() - - # expected ordred features by importance - ordered_features = [ - "var_3", - "var_2", - "var_11", - "var_5", - "var_10", - "var_1", - "var_8", - "var_0", - "var_9", - "var_6", - "var_4", - "var_7", - ] - - # test init params - assert sel.variables is None - assert sel.threshold == 0.001 - assert sel.cv == 3 - assert sel.scoring == "roc_auc" - # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] - assert np.round(sel.initial_model_performance_, 3) == 0.997 - assert sel.features_to_drop_ == [ - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] - assert list(sel.performance_drifts_.keys()) == ordered_features - # test transform output - pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - -def test_regression_cv_3_and_r2(load_diabetes_dataset): - # test for regression using cv=3, and the r2 as metric. - X, y = load_diabetes_dataset - sel = RecursiveFeatureElimination(estimator=LinearRegression(), scoring="r2", cv=3) sel.fit(X, y) - # expected output - Xtransformed = X[[1, 2, 3, 4, 5, 8]].copy() + Xtransformed = X.copy() + Xtransformed = Xtransformed.drop(labels=dropped_features, axis=1) - # expected ordred features by importance - ordered_features = [0, 9, 6, 7, 1, 3, 5, 2, 8, 4] - - # test init params - assert sel.cv == 3 - assert sel.variables is None - assert sel.scoring == "r2" - assert sel.threshold == 0.01 - # fit params - assert sel.variables_ == list(X.columns) - assert np.round(sel.initial_model_performance_, 3) == 0.489 - assert sel.features_to_drop_ == [0, 6, 7, 9] - assert list(sel.performance_drifts_.keys()) == ordered_features - # test transform output - pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - - -def test_regression_cv_2_and_mse(load_diabetes_dataset): - # test for regression using cv=2, and the neg_mean_squared_error as metric. - # add suitable threshold for regression mse - - X, y = load_diabetes_dataset - sel = RecursiveFeatureElimination( - estimator=DecisionTreeRegressor(random_state=0), - scoring="neg_mean_squared_error", - cv=2, - threshold=10, - ) - # fit transformer - sel.fit(X, y) - - # expected output - Xtransformed = X[[0, 2, 3, 5, 6, 7, 8, 9]].copy() + # test fit attrs + assert sel.features_to_drop_ == dropped_features - # expected ordred features by importance - ordered_features = [1, 0, 4, 6, 9, 3, 7, 5, 8, 2] + assert len(sel.performance_drifts_.keys()) == len(X.columns) + assert all([var in sel.performance_drifts_.keys() for var in X.columns]) + rounded_perfs = { + key: round(sel.performance_drifts_[key], 4) for key in sel.performance_drifts_ + } + assert rounded_perfs == performances - # test init params - assert sel.cv == 2 - assert sel.variables is None - assert sel.scoring == "neg_mean_squared_error" - assert sel.threshold == 10 - # fit params - assert sel.variables_ == list(X.columns) - assert np.round(sel.initial_model_performance_, 0) == -5836.0 - assert sel.features_to_drop_ == [1, 4] - assert list(sel.performance_drifts_.keys()) == ordered_features # test transform output pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) -def test_non_fitted_error(df_test): - # when fit is not called prior to transform - with pytest.raises(NotFittedError): - sel = RecursiveFeatureElimination(RandomForestClassifier(random_state=1)) - sel.transform(df_test) - - -def test_raises_threshold_error(): - with pytest.raises(ValueError): - RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), threshold=None - ) - - -def test_automatic_variable_selection(load_diabetes_dataset): +# tests for regression +_model_and_expectations = [ + ( + Lasso(alpha=0.001, random_state=10), + 3, + 0.1, + "r2", + [0, 1, 3, 4, 5, 6, 7, 9], + { + 0: -0.0032, + 9: -0.0003, + 6: -0.0008, + 7: 0.0001, + 1: 0.012, + 3: 0.0199, + 5: 0.0023, + 2: 0.1378, + 4: 0.0069, + 8: 0.115, + }, + ), + ( + DecisionTreeRegressor(random_state=10), + 2, + 100, + "neg_mean_squared_error", + [1, 4], + { + 1: 64.1018, + 4: -199.9864, + 0: 481.6109, + 6: 282.4231, + 9: 699.7964, + 3: 327.1403, + 7: 246.4412, + 5: 436.9751, + 8: 350.4163, + 2: 1340.0226, + }, + ), +] + + +@pytest.mark.parametrize( + "estimator, cv, threshold, scoring, dropped_features, performances", + _model_and_expectations, +) +def test_regression( + estimator, + cv, + threshold, + scoring, + dropped_features, + performances, + load_diabetes_dataset, +): + # test for regression using cv=3, and the r2 as metric. X, y = load_diabetes_dataset - # add 2 additional categorical variables, these should not be evaluated by - # the selector - X["cat_1"] = "cat1" - X["cat_2"] = "cat2" - sel = RecursiveFeatureElimination( - estimator=DecisionTreeRegressor(random_state=0), - scoring="neg_mean_squared_error", - cv=2, - threshold=10, + estimator=estimator, cv=cv, threshold=threshold, scoring=scoring ) - # fit transformer - sel.fit(X, y) - - # expected output - Xtransformed = X[[0, 2, 3, 5, 6, 7, 8, 9, "cat_1", "cat_2"]].copy() - - # expected ordred features by importance - ordered_features = [1, 0, 4, 6, 9, 3, 7, 5, 8, 2] - - # test init params - assert sel.variables is None - # fit params - assert sel.variables_ == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - assert np.round(sel.initial_model_performance_, 0) == -5836.0 - assert sel.features_to_drop_ == [1, 4] - assert list(sel.performance_drifts_.keys()) == ordered_features - # test transform output - pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - -def test_KFold_generators(df_test): - - X, y = df_test - - # Kfold - sel = RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), - threshold=0.001, - cv=KFold(n_splits=3), - ) sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) - # Stratfied - sel = RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), - threshold=0.001, - cv=StratifiedKFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) + Xtransformed = X.copy() + Xtransformed = Xtransformed.drop(labels=dropped_features, axis=1) # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) + assert sel.features_to_drop_ == dropped_features - # None - sel = RecursiveFeatureElimination( - RandomForestClassifier(random_state=1), - threshold=0.001, - cv=None, - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) + assert len(sel.performance_drifts_.keys()) == len(X.columns) + assert all([var in sel.performance_drifts_.keys() for var in X.columns]) + rounded_perfs = { + key: round(sel.performance_drifts_[key], 4) for key in sel.performance_drifts_ + } + assert rounded_perfs == performances - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) + # test transform output + pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) diff --git a/tests/test_selection/test_recursive_feature_selectors.py b/tests/test_selection/test_recursive_feature_selectors.py new file mode 100644 index 000000000..b93ef59ce --- /dev/null +++ b/tests/test_selection/test_recursive_feature_selectors.py @@ -0,0 +1,168 @@ +import numpy as np +import pytest +from sklearn.ensemble import RandomForestClassifier +from sklearn.linear_model import Lasso, LogisticRegression +from sklearn.model_selection import KFold, StratifiedKFold +from sklearn.tree import DecisionTreeRegressor + +from feature_engine.selection import ( + RecursiveFeatureAddition, + RecursiveFeatureElimination, +) +from feature_engine.selection.base_recursive_selector import BaseRecursiveSelector + +_selectors = [ + BaseRecursiveSelector, + RecursiveFeatureElimination, + RecursiveFeatureAddition, +] + +_input_params = [ + (RandomForestClassifier(), "roc_auc", 3, 0.1, None), + (Lasso(), "neg_mean_squared_error", KFold(), 0.01, ["var_a", "var_b"]), + (DecisionTreeRegressor(), "r2", StratifiedKFold(), 0.5, ["var_a"]), + (RandomForestClassifier(), "accuracy", 5, 0.002, "var_a"), +] + + +@pytest.mark.parametrize("_selector", _selectors) +@pytest.mark.parametrize( + "_estimator, _scoring, _cv, _threshold, _variables", _input_params +) +def test_input_params_assignment( + _selector, _estimator, _scoring, _cv, _threshold, _variables +): + sel = _selector( + estimator=_estimator, + scoring=_scoring, + cv=_cv, + threshold=_threshold, + variables=_variables, + ) + + assert sel.estimator == _estimator + assert sel.scoring == _scoring + assert sel.cv == _cv + assert sel.threshold == _threshold + assert sel.variables == _variables + + +_thresholds = [None, [0.1], "a_string"] + + +@pytest.mark.parametrize("_selector", _selectors) +@pytest.mark.parametrize("_thresholds", _thresholds) +def test_raises_threshold_error(_selector, _thresholds): + with pytest.raises(ValueError): + _selector(RandomForestClassifier(), threshold=_thresholds) + + +_estimators_and_results = [ + ( + RandomForestClassifier(random_state=1), + Lasso(alpha=0.01, random_state=1), + 0.9971, + 0.8489, + ), + ( + LogisticRegression(random_state=1), + DecisionTreeRegressor(random_state=1), + 0.9966, + 0.9399, + ), +] + + +@pytest.mark.parametrize("_selector", _selectors) +@pytest.mark.parametrize("_classifier, _regressor, _roc, _r2", _estimators_and_results) +def test_fit_initial_model_performance( + _selector, _classifier, _regressor, _roc, _r2, df_test +): + X, y = df_test + + sel = _selector(_classifier).fit(X, y) + + assert np.round(sel.initial_model_performance_, 4) == _roc + + sel = _selector( + _regressor, + scoring="r2", + ).fit(X, y) + + assert np.round(sel.initial_model_performance_, 4) == _r2 + + +_estimators_importance = [ + ( + RandomForestClassifier(random_state=1), + [ + 0.0238, + 0.0042, + 0.0022, + 0.0021, + 0.2583, + 0.0034, + 0.2012, + 0.38, + 0.0145, + 0.1044, + 0.0035, + 0.0024, + ], + ), + ( + LogisticRegression(random_state=1), + [ + 1.4106, + 0.1924, + 0.0876, + 0.066, + 0.5421, + 0.0825, + 0.5658, + 2.1938, + 1.5259, + 0.1173, + 0.1673, + 0.1792, + ], + ), + ( + Lasso(alpha=0.01, random_state=1), + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2126, 0.0557, 0.0, 0.0, 0.0], + ), + ( + DecisionTreeRegressor(random_state=1), + [ + 0.0016, + 0.0, + 0.002, + 0.002, + 0.0013, + 0.001, + 0.0026, + 0.976, + 0.0106, + 0.0, + 0.0006, + 0.0022, + ], + ), +] + + +@pytest.mark.parametrize("_estimator, _importance", _estimators_importance) +def test_feature_importances(_estimator, _importance, df_test): + X, y = df_test + + # Test Base Recursive Selector + sel = BaseRecursiveSelector(_estimator).fit(X, y) + assert list(np.round(sel.feature_importances_.values, 4)) == _importance + + sel = RecursiveFeatureAddition(_estimator).fit(X, y) + _importance.sort(reverse=True) + assert list(np.round(sel.feature_importances_.values, 4)) == _importance + + sel = RecursiveFeatureElimination(_estimator).fit(X, y) + _importance.sort(reverse=False) + assert list(np.round(sel.feature_importances_.values, 4)) == _importance diff --git a/tests/test_selection/test_shuffle_features.py b/tests/test_selection/test_shuffle_features.py index 7e7c79359..3cbee5f9b 100644 --- a/tests/test_selection/test_shuffle_features.py +++ b/tests/test_selection/test_shuffle_features.py @@ -2,15 +2,13 @@ import pandas as pd import pytest from sklearn.ensemble import RandomForestClassifier -from sklearn.exceptions import NotFittedError from sklearn.linear_model import LinearRegression -from sklearn.model_selection import KFold, StratifiedKFold from sklearn.tree import DecisionTreeRegressor from feature_engine.selection import SelectByShuffling -def test_default_parameters(df_test): +def test_sel_with_default_parameters(df_test): X, y = df_test sel = SelectByShuffling( RandomForestClassifier(random_state=1), threshold=0.01, random_state=1 @@ -21,25 +19,10 @@ def test_default_parameters(df_test): Xtransformed = pd.DataFrame(X["var_7"].copy()) # test init params - assert sel.variables is None assert sel.threshold == 0.01 assert sel.cv == 3 assert sel.scoring == "roc_auc" # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] assert np.round(sel.initial_model_performance_, 3) == 0.997 assert sel.features_to_drop_ == [ "var_0", @@ -71,11 +54,9 @@ def test_regression_cv_3_and_r2(load_diabetes_dataset): # test init params assert sel.cv == 3 - assert sel.variables is None assert sel.scoring == "r2" assert sel.threshold == 0.01 # fit params - assert sel.variables_ == list(X.columns) assert np.round(sel.initial_model_performance_, 3) == 0.489 assert sel.features_to_drop_ == [0, 6, 7, 9] # test transform output @@ -102,24 +83,15 @@ def test_regression_cv_2_and_mse(load_diabetes_dataset): # test init params assert sel.cv == 2 - assert sel.variables is None assert sel.scoring == "neg_mean_squared_error" assert sel.threshold == 5 # fit params - assert sel.variables_ == list(X.columns) assert np.round(sel.initial_model_performance_, 0) == -5836.0 assert sel.features_to_drop_ == [0, 1, 3, 4, 5, 6, 7, 9] # test transform output pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) -def test_non_fitted_error(df_test): - # when fit is not called prior to transform - with pytest.raises(NotFittedError): - sel = SelectByShuffling(RandomForestClassifier(random_state=1)) - sel.transform(df_test) - - def test_raises_threshold_error(): with pytest.raises(ValueError): SelectByShuffling(RandomForestClassifier(random_state=1), threshold="hello") @@ -141,25 +113,10 @@ def test_automatic_variable_selection(df_test): Xtransformed = X[["var_7", "cat_1", "cat_2"]].copy() # test init params - assert sel.variables is None assert sel.threshold == 0.01 assert sel.cv == 3 assert sel.scoring == "roc_auc" # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] assert np.round(sel.initial_model_performance_, 3) == 0.997 assert sel.features_to_drop_ == [ "var_0", @@ -176,86 +133,3 @@ def test_automatic_variable_selection(df_test): ] # test transform output pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - - -def test_KFold_generators(df_test): - - X, y = df_test - - # Kfold - sel = SelectByShuffling( - RandomForestClassifier(random_state=1), - threshold=0.01, - random_state=1, - cv=KFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) - - # Stratfied - sel = SelectByShuffling( - RandomForestClassifier(random_state=1), - threshold=0.01, - random_state=1, - cv=StratifiedKFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) - - # None - sel = SelectByShuffling( - RandomForestClassifier(random_state=1), - threshold=0.01, - random_state=1, - cv=None, - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert sel.initial_model_performance_ > 0.995 - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.performance_drifts_, dict) - assert all([x for x in X.columns if x in sel.performance_drifts_.keys()]) - assert all( - [ - isinstance(sel.performance_drifts_[var], (int, float)) - for var in sel.performance_drifts_.keys() - ] - ) diff --git a/tests/test_selection/test_single_feature_performance_selection.py b/tests/test_selection/test_single_feature_performance_selection.py index 857034009..b84db1258 100644 --- a/tests/test_selection/test_single_feature_performance_selection.py +++ b/tests/test_selection/test_single_feature_performance_selection.py @@ -4,15 +4,13 @@ import pandas as pd import pytest from sklearn.ensemble import RandomForestClassifier -from sklearn.exceptions import NotFittedError from sklearn.linear_model import LinearRegression -from sklearn.model_selection import KFold, StratifiedKFold from sklearn.tree import DecisionTreeRegressor from feature_engine.selection import SelectBySingleFeaturePerformance -def test_default_parameters(df_test): +def test_sel_with_default_parameters(df_test): X, y = df_test sel = SelectBySingleFeaturePerformance( RandomForestClassifier(random_state=1), threshold=0.5 @@ -24,25 +22,10 @@ def test_default_parameters(df_test): Xtransformed.drop(columns=["var_3", "var_10"], inplace=True) # test init params - assert sel.variables is None assert sel.threshold == 0.5 assert sel.cv == 3 assert sel.scoring == "roc_auc" # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] assert sel.features_to_drop_ == ["var_3", "var_10"] assert sel.feature_performance_ == { "var_0": 0.5957642619540211, @@ -88,11 +71,9 @@ def test_regression_cv_3_and_r2(load_diabetes_dataset): # test init params assert sel.cv == 3 - assert sel.variables is None assert sel.scoring == "r2" assert sel.threshold == 0.01 # fit params - assert sel.variables_ == list(X.columns) assert sel.features_to_drop_ == [1] assert all( np.round(sel.feature_performance_[f], 3) == performance_dict[f] @@ -122,11 +103,9 @@ def test_regression_cv_2_and_mse(load_diabetes_dataset): # test init params assert sel.cv == 2 - assert sel.variables is None assert sel.scoring == "neg_mean_squared_error" assert sel.threshold == -6000 # fit params - assert sel.variables_ == list(X.columns) assert sel.features_to_drop_ == [0, 2, 3, 4, 5, 6, 8, 9] assert sel.feature_performance_ == { 0: -7657.154138192973, @@ -157,13 +136,6 @@ def test_raises_warning_if_no_feature_selected(load_diabetes_dataset): warnings.warn(sel.fit(X, y), UserWarning) -def test_non_fitted_error(df_test): - # when fit is not called prior to transform - with pytest.raises(NotFittedError): - sel = SelectBySingleFeaturePerformance(RandomForestClassifier(random_state=1)) - sel.transform(df_test) - - def test_raises_threshold_error(): with pytest.raises(ValueError): SelectBySingleFeaturePerformance( @@ -203,25 +175,10 @@ def test_automatic_variable_selection(df_test): Xtransformed.drop(columns=["var_3", "var_10"], inplace=True) # test init params - assert sel.variables is None assert sel.threshold == 0.5 assert sel.cv == 3 assert sel.scoring == "roc_auc" # test fit attrs - assert sel.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - "var_6", - "var_7", - "var_8", - "var_9", - "var_10", - "var_11", - ] assert sel.features_to_drop_ == ["var_3", "var_10"] assert sel.feature_performance_ == { "var_0": 0.5957642619540211, @@ -239,80 +196,3 @@ def test_automatic_variable_selection(df_test): } # test transform output pd.testing.assert_frame_equal(sel.transform(X), Xtransformed) - - -def test_KFold_generators(df_test): - - X, y = df_test - - # Kfold - sel = SelectBySingleFeaturePerformance( - RandomForestClassifier(random_state=1), - threshold=0.5, - cv=KFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.feature_performance_, dict) - assert all([x for x in X.columns if x in sel.feature_performance_.keys()]) - assert all( - [ - isinstance(sel.feature_performance_[var], float) - for var in sel.feature_performance_.keys() - ] - ) - - # Stratfied - sel = SelectBySingleFeaturePerformance( - RandomForestClassifier(random_state=1), - threshold=0.5, - cv=StratifiedKFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.feature_performance_, dict) - assert all([x for x in X.columns if x in sel.feature_performance_.keys()]) - assert all( - [ - isinstance(sel.feature_performance_[var], float) - for var in sel.feature_performance_.keys() - ] - ) - - # None - sel = SelectBySingleFeaturePerformance( - RandomForestClassifier(random_state=1), - threshold=0.5, - cv=None, - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - assert isinstance(sel.feature_performance_, dict) - assert all([x for x in X.columns if x in sel.feature_performance_.keys()]) - assert all( - [ - isinstance(sel.feature_performance_[var], float) - for var in sel.feature_performance_.keys() - ] - ) diff --git a/tests/test_selection/test_smart_correlation_selection.py b/tests/test_selection/test_smart_correlation_selection.py index 92ce68de8..f29c598c5 100644 --- a/tests/test_selection/test_smart_correlation_selection.py +++ b/tests/test_selection/test_smart_correlation_selection.py @@ -2,8 +2,6 @@ import pytest from sklearn.datasets import make_classification from sklearn.ensemble import RandomForestClassifier -from sklearn.exceptions import NotFittedError -from sklearn.model_selection import KFold, StratifiedKFold from feature_engine.selection import SmartCorrelatedSelection @@ -50,21 +48,12 @@ def test_model_performance_single_corr_group(df_single): # test init params assert transformer.method == "pearson" assert transformer.threshold == 0.8 - assert transformer.variables is None assert transformer.missing_values == "raise" assert transformer.selection_method == "model_performance" assert transformer.scoring == "roc_auc" assert transformer.cv == 3 # test fit attrs - assert transformer.variables_ == [ - "var_0", - "var_1", - "var_2", - "var_3", - "var_4", - "var_5", - ] assert transformer.correlated_feature_sets_ == [{"var_1", "var_2"}] assert transformer.features_to_drop_ == ["var_1"] # test transform output @@ -111,14 +100,9 @@ def test_error_if_select_model_performance_and_y_is_none(df_single): X, y = df_single transformer = SmartCorrelatedSelection( - variables=None, - method="pearson", - threshold=0.8, - missing_values="raise", selection_method="model_performance", estimator=RandomForestClassifier(n_estimators=10, random_state=1), scoring="roc_auc", - cv=3, ) with pytest.raises(ValueError): @@ -239,11 +223,7 @@ def test_callable_method(df_test, random_uniform_method): X, _ = df_test transformer = SmartCorrelatedSelection( - variables=None, method=random_uniform_method, - threshold=0.8, - missing_values="raise", - selection_method="variance", ) Xt = transformer.fit_transform(X) @@ -279,13 +259,7 @@ def test_error_method_supplied(df_test): X, _ = df_test method = "hola" - transformer = SmartCorrelatedSelection( - variables=None, - method=method, - threshold=0.8, - missing_values="raise", - selection_method="variance", - ) + transformer = SmartCorrelatedSelection(method=method) with pytest.raises(ValueError) as errmsg: _ = transformer.fit_transform(X) @@ -297,94 +271,3 @@ def test_error_method_supplied(df_test): == "method must be either 'pearson', 'spearman', 'kendall', or a callable," + f" '{method}' was supplied" ) - - -def test_error_if_fit_input_not_dataframe(): - with pytest.raises(TypeError): - SmartCorrelatedSelection().fit({"Name": [1]}) - - -def test_non_fitted_error(df_single): - X, y = df_single - # when fit is not called prior to transform - with pytest.raises(NotFittedError): - transformer = SmartCorrelatedSelection() - transformer.transform(X) - - transformer = SmartCorrelatedSelection( - variables=None, - method="pearson", - threshold=0.8, - missing_values="raise", - selection_method="model_performance", - estimator=RandomForestClassifier(n_estimators=10, random_state=1), - scoring="roc_auc", - cv=3, - ) - - -def test_KFold_generators(df_test): - X, y = df_test - - # Kfold - sel = SmartCorrelatedSelection( - variables=None, - method="pearson", - threshold=0.8, - missing_values="raise", - selection_method="model_performance", - estimator=RandomForestClassifier(n_estimators=10, random_state=1), - scoring="roc_auc", - cv=KFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - - # Stratfied - sel = SmartCorrelatedSelection( - variables=None, - method="pearson", - threshold=0.8, - missing_values="raise", - selection_method="model_performance", - estimator=RandomForestClassifier(n_estimators=10, random_state=1), - scoring="roc_auc", - cv=StratifiedKFold(n_splits=3), - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) - - # None - sel = SmartCorrelatedSelection( - variables=None, - method="pearson", - threshold=0.8, - missing_values="raise", - selection_method="model_performance", - estimator=RandomForestClassifier(n_estimators=10, random_state=1), - scoring="roc_auc", - cv=None, - ) - sel.fit(X, y) - Xtransformed = sel.transform(X) - - # test fit attrs - assert isinstance(sel.features_to_drop_, list) - assert all([x for x in sel.features_to_drop_ if x in X.columns]) - assert len(sel.features_to_drop_) < X.shape[1] - assert not Xtransformed.empty - assert all([x for x in Xtransformed.columns if x not in sel.features_to_drop_]) diff --git a/tests/test_selection/test_target_mean_selection.py b/tests/test_selection/test_target_mean_selection.py index 2dc722c8f..337adcfa5 100644 --- a/tests/test_selection/test_target_mean_selection.py +++ b/tests/test_selection/test_target_mean_selection.py @@ -1,10 +1,11 @@ # import numpy as np import pandas as pd import pytest -from sklearn.exceptions import NotFittedError from feature_engine.selection import SelectByTargetMeanPerformance +# TODO: we need to expand these tests + def test_numerical_variables_roc_auc(df_test): X, y = df_test @@ -207,26 +208,3 @@ def test_error_wrong_params(): SelectByTargetMeanPerformance(cv="hola") with pytest.raises(ValueError): SelectByTargetMeanPerformance(cv=1) - - -def test_error_if_y_not_passed(df_test): - X, y = df_test - with pytest.raises(TypeError): - SelectByTargetMeanPerformance().fit(X) - - -def test_error_if_input_not_df(df_test): - X, y = df_test - with pytest.raises(TypeError): - SelectByTargetMeanPerformance().fit(X.to_dict(), y) - - -def test_error_if_fit_input_not_dataframe(df_test): - with pytest.raises(TypeError): - SelectByTargetMeanPerformance().fit({"Name": ["Karthik"]}) - - -def test_not_fitted_error(df_test): - with pytest.raises(NotFittedError): - transformer = SelectByTargetMeanPerformance() - transformer.transform(df_test)