Repository: feature-engine/feature_engine
Pull request #338 (2021-12-07 01:27:13)

base_encoder NaN Detection
Identifies which columns in the dataframe used in the base_encoder have null values and informs the user of those columns.

closes #351 

Morgan-Sell: To test a warning, I suspect I use an `assert` function to determine whether the warning returns the expected string. In this case, the expected string is the warning message w/ the columns that contain NaN values.  Is this correct?

Do I create a sample dataframe that is used in the unit test? If so, where do I create the dataframe? Do I create in the `test_count_frequency_encoder.py`, the test_encoding `__init__.py` or somewhere else?
 
solegalli: > 

Hi @Morgan-Sell 

Maybe this link helps: https://stackoverflow.com/questions/33920322/how-do-i-test-exceptions-and-errors-using-pytest
Morgan-Sell: Hi @solegalli,

Thanks for sharing the Stackoverflow article! I implemented the `pytest.raises()` and `assert` statement for `test_base_categorical_transformer_detect_nan()`.

Is it appropriate to add a `UserWarning` label to the warning text? It seems that `pytest.raises()` requires a label for the warning.

On the topic of project mgmt, is it ok for me to select "Resolve conversation" after I implement your comments? Or, are you, as the reviewer, the only person who should select "Resolve conversation"?
solegalli: You are welcome to resolve conversations :)
solegalli: Hi @Morgan-Sell 

I made a [PR](https://github.com/Morgan-Sell/feature_engine/pull/1) to your repo with the necessary changes.

Could you please have a look and merge?

Thank you!

solegalli: Note: the decision tree encoder does not raise the warning, because the transformer raises an error when the decision tree discretizer introduces nan. 

We need to see if we are stuck here, or we can add a more informative error for the users. Probably becomes clearer after working on issue #295 
solegalli: Hi @Morgan-Sell 

Thinking it further, we will need the encoders to be able to raise an error as well as a warning, when introducing NaN.

Do you think you could do the following:

- [ ] revise and make the PR I made to your repo
- [ ] add an init param in the encoders: rare_labels='ignore'
- [ ] extend the tests to accommodate the new functionality

If rare_labels is 'ignore', then the encoders will raise a warning as they do now. But when rare_labels is 'raise', instead of a warning, they should raise an error.

The init param needs to be added to the CountFrequencyEncoder, WoEEncoder, PRatioEncoder, MeanEncoder and OrdinalEncoder. The functionality needs to be extended in the BaseCategoricalEncoder. The tests to be extended are the ones we modified in this PR, to test for a warning or for the error.

Let me know.

Cheers


Morgan-Sell: Hola @solegalli,

Yes, I can work on those revisions. I've already started working on issue # [295](https://github.com/feature-engine/feature_engine/issues/295). May I complete that issue before returning to this one?

Also, I noticed that the code failed one of the tests. I see the error states `ERROR:   typechecks: commands failed`. I'm unclear as to why.


solegalli: Hi @Morgan-Sell ,

You can work in whichever order you prefer. 

Having said this, this PR is almost ready, and it has the logic that you need to implement in the discretizer. So If you finished this one first, you would already know what you have to do in the other PR.

But up to you. I don't really mind.

Regarding the typecheck error, it comes with the new release of mypy and we fixed it in a different PR #333 
Morgan-Sell: Hi @solegalli,

In that case, I'll finish this task first. ;)

After merging your PR, shouldn't I see your edits in my codebase (in PyCharm)? I don't see those revisions when I have the `encoder-nan-detection` branch checked out.

I tried `git pull` to pull in your changes. However, I received the following message, **"There is no tracking information for the current branch. Please specify which branch you want to merge with."**

solegalli: Hi @Morgan-Sell 

Did you try searching for the error in a search engine or stackoverflow? That will allow you to troubleshoot much faster than waiting for me to reply. Also, I don't always have the right answer ;)

In this case, most likely you need to check out your branch locally:
`git checkout mybranch`

and then pull the changes you have in that branch remotely
`git pull origin mybranch`

Hope that helps
Cheers
Morgan-Sell: @solegalli, gracias! I don't know why I just didn't Google the error message. Google/Stackoverflow are great friends. Que boludo!

I started implementing the code to raise the error if `self.rare_labels == "raise"`. I only revised the `CountFrequencyEncoder()` test b/c I thought it would be best to approve/disapprove of the code prior to replicating the code for the other four encoding classes.
Morgan-Sell: @solegalli, que bueno! da mis saludos a BsAs y disfruta un vaso de Malbec para mi. O mas, si prefieres ;)

I moved the init params - `self.variables` and `self.ignore_format` - and corresponding docstrings to the parent class BaseCategoricalTransformer. I made the following revisions to all of the child Encoding classes docstrings (hopefully they follow the appropriate standards):

- Deleted the mentioning of `ignore_format` in the docstring.
- Deleted `variables` and `ignore_format` from the "Parameters" section.
- Added a note to reference BaseCategoricalTransformer to read the definition of `variables` and `ignore_format`.

With regards to `self.rare_labels`, the parameter does **not** exist in DecisionTreeEncoder, OneHotEncoder, and RareLabelEncoder. Should we still include `rare_labels` in the parent class BaseCategoricalTransformer? 

I have not made revisions to `rare_labels`.

And, yes, I will work on the other tasks ;)

solegalli: > 

Hi @Morgan-Sell 

Good call about the new parameter not being present in OHE, tree discretizer and rare labels!

In fact, for just 2 parameters I think it is not worth it to migrate them to the baseclass. 

Could we then undo the last 3 commits, and add the  param value check on each relevant transformer and its respective docstring?

I think you probably need to check out the relevant commit, make the changes, and if push does not do it, then force push.

Sorry, my bad :/

I apologize.

btw, I don't like red wine. Is white ok?
solegalli: Hi @Morgan-Sell 

Actually, this refactoring wasn't so straightforward, so I gave it a go and sent a [PR to your repo](https://github.com/Morgan-Sell/feature_engine/pull/2)

pls have a look and merge if you are happy

Thank you!
Morgan-Sell: @solegalli, I merged the other PR. Thanks for the help!

Are all the edits that you requested now complete?

I'm unclear on the status of a task from one of your prior posts - "Could you add 1 test in each class to ensure that if the user passes something else in the param rare_labels, say "hello", the transformer raises an error?"

Is this task addressed by the following code?
```

if rare_labels not in ["raise", "ignore"]:
     raise ValueError(
         "rare_labels takes only values 'raise' and 'ignore ."
         f"Got {rare_labels} instead."
     )
```

Given that the `rare_labels` check is in the `__init__` method, do we still need to create a `rare_labels` check in the `test_error_if_input_df_contains_categories_not_present_in_fit_df` function?


Y, si vino blanco esta bien! En realidad, malbec es el vino rojo unico que bebo. Y, come un alfajor tambien! 
Morgan-Sell: Hi @solegalli,

I was reviewing the code and I noticed an inconsistency.

The check/warning for the NaN values is in the `transform()` method in the BaseCategoricalTransformer class.

However, the test for NaN values in the `transform()` method is in the `test_error_if_input_df_contains_categories_not_present_in_fit_df` fcn on the **test_count_frequency_encoder.py** file.

Should the code below be moved to the `test_transform_raises_error_if_df_contains_na` fcn (test case 4)?
```
msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
    encoder = CountFrequencyEncoder()

    if encoder.rare_labels == "ignore":
        with pytest.warns(UserWarning) as record:
            encoder.fit(df_enc)
            encoder.transform(df_enc_rare)

        # check that only one warning was raised
        assert len(record) == 1
        # check that the message matches
        assert record[0].message.args[0] == msg

    elif encoder.rare_labels == "raise":
        with pytest.raises(ValueError) as record:
            encoder.fit(df_enc)
            encoder.transform(df_enc_rare)

        # check that only one error was raised
        assert len(record) == 1
        # check that the error message matches
        assert record[0].message.args[0] == msg
```

`df_enc_rare` should be replaced w/ `df_enc_na`.

Meanwhile, the `test_error_if_input_df_contains_categories_not_present_in_fit_df` fcn should have code similar to the following?

```
        with pytest.raises(ValueError) as record:
            encoder = CountFrequencyEncoder()
            encoder.fit(df_enc)
            encoder.transform(df_enc_rare)
```
solegalli: > Hi @solegalli,
> 
> I was reviewing the code and I noticed an inconsistency.
> 
> The check/warning for the NaN values is in the `transform()` method in the BaseCategoricalTransformer class.
> 
> However, the test for NaN values in the `transform()` method is in the `test_error_if_input_df_contains_categories_not_present_in_fit_df` fcn on the **test_count_frequency_encoder.py** file.
> 
> Should the code below be moved to the `test_transform_raises_error_if_df_contains_na` fcn (test case 4)?
> 
> ```
> msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
>     encoder = CountFrequencyEncoder()
> 
>     if encoder.rare_labels == "ignore":
>         with pytest.warns(UserWarning) as record:
>             encoder.fit(df_enc)
>             encoder.transform(df_enc_rare)
> 
>         # check that only one warning was raised
>         assert len(record) == 1
>         # check that the message matches
>         assert record[0].message.args[0] == msg
> 
>     elif encoder.rare_labels == "raise":
>         with pytest.raises(ValueError) as record:
>             encoder.fit(df_enc)
>             encoder.transform(df_enc_rare)
> 
>         # check that only one error was raised
>         assert len(record) == 1
>         # check that the error message matches
>         assert record[0].message.args[0] == msg
> ```
> 
> `df_enc_rare` should be replaced w/ `df_enc_na`.
> 
> Meanwhile, the `test_error_if_input_df_contains_categories_not_present_in_fit_df` fcn should have code similar to the following?
> 
> ```
>         with pytest.raises(ValueError) as record:
>             encoder = CountFrequencyEncoder()
>             encoder.fit(df_enc)
>             encoder.transform(df_enc_rare)
> ```

test case 3: tests the situation where the dataframe to transform contains categories that were not present in the train set. Then, when the transformer encoders those variables, it introduces NA, and here is where you changed the functionality, so you added the new tests.

test case 4: tests the situation where the dataframe to transform contains NA. That is a different story, the error will be raised before doing the encoding.

Bottom line, we do not need to re-organise the tests. This is OK. 

We just need to add a few tests missing. 

Thank you for checking and bring it this up.
Morgan-Sell: Hi @solegalli,

I implemented the tests that you mentioned. I'll resolve the failed tests later. I thought they were caused by the `assert` statement for the `ValueError` check when `rare_labels=raise`.
Morgan-Sell: Hola @solegalli,

I believe I implemented all of the suggested tests. Let me know if I overlooked something.

Hopefully, the "test string" for the `test_error_if_rare_labels_not_permitted_value()` function is OK. If you would like, I am happy to change it ;)
solegalli: Hi @Morgan-Sell 

This is OK to merge. Thank you so much for adding all the tests!

There is one detail though. Something went wrong in the rebase, because now all the files that were modified in other PRs appear in this PR.

We have 2 options. 

The first one is that you try to resolve the issue, so that we commit this PR with all your work, and we can close it here, end to end.

I think the following should work:

`
git checkout main
git pull -rebase upstream main
git checkout -yourfeaturebranch
git rebase main 
`

and now try

`git push -f origin yourfeaturebranch`

That worked for me.

If you still see all 59 files in the PR, then we can use the second approach, which is merging #351 which is basically your changes, but then it looks like it is my commit.

Let me  know if you would like to try the above and / or if I go ahead and merge #351.

Thank you!
Morgan-Sell: Thanks, @solegalli! It seemed to work.

I don't why all the other PR changes were merged into this one. 

I also changed the BaseCategorical init param from `rare_labels` to `errors`. The nomenclature for the encoders' param now matches the discretizers' param.
solegalli: Hi @Morgan-Sell. Glad you could remove the changes from other PRs. Well done.

We now have a lot of files that seem to come from your local environment.

Would you be able to remove those as well please? 
Morgan-Sell: Hi @solegalli, I removed the appropriate directories/files. Lmk if there's anything else!
solegalli: Masterpiece! Thank you @Morgan-Sell 

Patch:
diff --git a/feature_engine/encoding/base_encoder.py b/feature_engine/encoding/base_encoder.py
index eb5b814..b2ebebb 100644
--- a/feature_engine/encoding/base_encoder.py
+++ b/feature_engine/encoding/base_encoder.py
@@ -14,11 +14,41 @@ from feature_engine.validation import _return_tags
 from feature_engine.variable_manipulation import (
     _find_all_variables,
     _find_or_check_categorical_variables,
+    _check_input_parameter_variables,
 )
 
 
 class BaseCategoricalTransformer(BaseEstimator, TransformerMixin):
-    """shared set-up checks and methods across categorical transformers"""
+    """shared set-up checks and methods across categorical transformers
+
+    Parameters
+    ----------
+    variables: list, default=None
+        The list of categorical variables that will be encoded. If None, the
+        encoder will find and transform all variables of type object or categorical by
+        default. You can also make the transformer accept numerical variables, see the
+        next parameter.
+
+    ignore_format: bool, default=False
+        Whether the format in which the categorical variables are cast should be
+        ignored. If False, the encoder will automatically select variables of type
+        object or categorical, or check that the variables entered by the user are of
+        type object or categorical. If True, the encoder will select all variables or
+        accept all variables entered by the user, including those cast as numeric.
+    """
+
+    def __init__(
+        self,
+        variables: Union[None, int, str, List[Union[str, int]]] = None,
+        ignore_format: bool = False,
+    ) -> None:
+
+        if not isinstance(ignore_format, bool):
+            raise ValueError("ignore_format takes only booleans True and False. "
+                             f"Got {ignore_format} instead.")
+
+        self.variables = _check_input_parameter_variables(variables)
+        self.ignore_format = ignore_format
 
     def _check_fit_input_and_variables(self, X: pd.DataFrame) -> pd.DataFrame:
         """
@@ -144,14 +174,23 @@ class BaseCategoricalTransformer(BaseEstimator, TransformerMixin):
 
         # check if NaN values were introduced by the encoding
         if X[self.encoder_dict_.keys()].isnull().sum().sum() > 0:
-            warnings.warn(
-                "NaN values were introduced in the returned dataframe by the encoder."
-                "This means that some of the categories in the input dataframe were "
-                "not present in the training set used when the fit method was called. "
-                "Thus, mappings for those categories do not exist. Try using the "
-                "RareLabelCategoricalEncoder to remove infrequent categories before "
-                "calling this encoder."
-            )
+            # obtain the name(s) of the columns have null values
+            nan_columns = X.columns[X.isnull().any()].tolist()
+            if len(nan_columns) > 1:
+                nan_columns_str = ", ".join(nan_columns)
+            else:
+                nan_columns_str = nan_columns[0]
+
+            if self.errors == "ignore":
+                warnings.warn(
+                    "During the encoding, NaN values were introduced in the feature(s) "
+                    f"{nan_columns_str}."
+                )
+            elif self.errors == "raise":
+                raise ValueError(
+                    "During the encoding, NaN values were introduced in the feature(s) "
+                    f"{nan_columns_str}."
+                )
 
         return X
 
@@ -186,3 +225,47 @@ class BaseCategoricalTransformer(BaseEstimator, TransformerMixin):
         # so we need to leave without this test
         tags_dict["_xfail_checks"]["check_estimators_nan_inf"] = "transformer allows NA"
         return tags_dict
+
+
+class BaseCategorical(BaseCategoricalTransformer):
+    """
+    BaseCategorical() is the parent class to some of the encoders.
+    It shares set-up checks of init parameters.
+
+    Parameters
+    ----------
+    variables: list, default=None
+        The list of categorical variables that will be encoded. If None, the
+        encoder will find and transform all variables of type object or categorical by
+        default. You can also make the transformer accept numerical variables, see the
+        next parameter.
+
+    ignore_format: bool, default=False
+        Whether the format in which the categorical variables are cast should be
+        ignored. If False, the encoder will automatically select variables of type
+        object or categorical, or check that the variables entered by the user are of
+        type object or categorical. If True, the encoder will select all variables or
+        accept all variables entered by the user, including those cast as numeric.
+
+    errors: string, default='ignore'
+        Indicates what to do, when categories not present in the train set are
+        encountered during transform. If 'raise', then rare categories will raise an
+        error. If 'ignore', then rare categories will be set as NaN and a warning will
+        be raised instead.
+    """
+
+    def __init__(
+        self,
+        variables: Union[None, int, str, List[Union[str, int]]] = None,
+        ignore_format: bool = False,
+        errors: str = "ignore",
+    ) -> None:
+
+        if errors not in ["raise", "ignore"]:
+            raise ValueError(
+                "errors takes only values 'raise' and 'ignore ."
+                f"Got {errors} instead."
+            )
+
+        super().__init__(variables, ignore_format)
+        self.errors = errors
diff --git a/feature_engine/encoding/count_frequency.py b/feature_engine/encoding/count_frequency.py
index 4cd43b7..e1f4cc9 100644
--- a/feature_engine/encoding/count_frequency.py
+++ b/feature_engine/encoding/count_frequency.py
@@ -5,11 +5,10 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
-from feature_engine.variable_manipulation import _check_input_parameter_variables
+from feature_engine.encoding.base_encoder import BaseCategorical
 
 
-class CountFrequencyEncoder(BaseCategoricalTransformer):
+class CountFrequencyEncoder(BaseCategorical):
     """
     The CountFrequencyEncoder() replaces categories by either the count or the
     percentage of observations per category.
@@ -55,6 +54,12 @@ class CountFrequencyEncoder(BaseCategoricalTransformer):
         type object or categorical. If True, the encoder will select all variables or
         accept all variables entered by the user, including those cast as numeric.
 
+    errors: string, default='ignore'
+        Indicates what to do when categories not present in the train set are
+        encountered during transform. If 'raise', then rare categories will raise an
+        error. If 'ignore', then rare categories will be set as NaN and a warning will
+        be raised instead.
+
     Attributes
     ----------
     encoder_dict_:
@@ -97,18 +102,16 @@ class CountFrequencyEncoder(BaseCategoricalTransformer):
         encoding_method: str = "count",
         variables: Union[None, int, str, List[Union[str, int]]] = None,
         ignore_format: bool = False,
+        errors: str = "ignore"
     ) -> None:
 
         if encoding_method not in ["count", "frequency"]:
             raise ValueError(
                 "encoding_method takes only values 'count' and 'frequency'"
             )
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
+        super().__init__(variables, ignore_format, errors)
 
         self.encoding_method = encoding_method
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """
@@ -149,11 +152,11 @@ class CountFrequencyEncoder(BaseCategoricalTransformer):
 
         return X
 
-    transform.__doc__ = BaseCategoricalTransformer.transform.__doc__
+    transform.__doc__ = BaseCategorical.transform.__doc__
 
     def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
         X = super().inverse_transform(X)
 
         return X
 
-    inverse_transform.__doc__ = BaseCategoricalTransformer.inverse_transform.__doc__
+    inverse_transform.__doc__ = BaseCategorical.inverse_transform.__doc__
diff --git a/feature_engine/encoding/decision_tree.py b/feature_engine/encoding/decision_tree.py
index 5a097e3..d5d288d 100644
--- a/feature_engine/encoding/decision_tree.py
+++ b/feature_engine/encoding/decision_tree.py
@@ -9,7 +9,6 @@ from sklearn.pipeline import Pipeline
 from feature_engine.discretisation import DecisionTreeDiscretiser
 from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
 from feature_engine.encoding.ordinal import OrdinalEncoder
-from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
 class DecisionTreeEncoder(BaseCategoricalTransformer):
@@ -139,14 +138,13 @@ class DecisionTreeEncoder(BaseCategoricalTransformer):
         ignore_format: bool = False,
     ) -> None:
 
+        super().__init__(variables, ignore_format)
         self.encoding_method = encoding_method
         self.cv = cv
         self.scoring = scoring
         self.regression = regression
         self.param_grid = param_grid
         self.random_state = random_state
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """
@@ -176,6 +174,7 @@ class DecisionTreeEncoder(BaseCategoricalTransformer):
             encoding_method=self.encoding_method,
             variables=self.variables_,
             ignore_format=self.ignore_format,
+            errors="raise",
         )
 
         # initialize decision tree discretiser
diff --git a/feature_engine/encoding/mean_encoding.py b/feature_engine/encoding/mean_encoding.py
index ba99efd..83ef8c6 100644
--- a/feature_engine/encoding/mean_encoding.py
+++ b/feature_engine/encoding/mean_encoding.py
@@ -5,11 +5,10 @@ from typing import List, Union
 
 import pandas as pd
 
-from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
-from feature_engine.variable_manipulation import _check_input_parameter_variables
+from feature_engine.encoding.base_encoder import BaseCategorical
 
 
-class MeanEncoder(BaseCategoricalTransformer):
+class MeanEncoder(BaseCategorical):
     """
     The MeanEncoder() replaces categories by the mean value of the target for each
     category.
@@ -47,6 +46,12 @@ class MeanEncoder(BaseCategoricalTransformer):
         type object or categorical. If True, the encoder will select all variables or
         accept all variables entered by the user, including those cast as numeric.
 
+    errors: string, default='ignore'
+        Indicates what to do when categories not present in the train set are
+        encountered during transform. If 'raise', then rare categories will raise an
+        error. If 'ignore', then rare categories will be set as NaN and a warning will
+        be raised instead.
+
     Attributes
     ----------
     encoder_dict_:
@@ -95,13 +100,10 @@ class MeanEncoder(BaseCategoricalTransformer):
         self,
         variables: Union[None, int, str, List[Union[str, int]]] = None,
         ignore_format: bool = False,
+        errors: str = "ignore"
     ) -> None:
 
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
-
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
+        super().__init__(variables, ignore_format, errors)
 
     def fit(self, X: pd.DataFrame, y: pd.Series):
         """
@@ -142,11 +144,11 @@ class MeanEncoder(BaseCategoricalTransformer):
 
         return X
 
-    transform.__doc__ = BaseCategoricalTransformer.transform.__doc__
+    transform.__doc__ = BaseCategorical.transform.__doc__
 
     def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
         X = super().inverse_transform(X)
 
         return X
 
-    inverse_transform.__doc__ = BaseCategoricalTransformer.inverse_transform.__doc__
+    inverse_transform.__doc__ = BaseCategorical.inverse_transform.__doc__
diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py
index eb8bfe4..6db7f34 100644
--- a/feature_engine/encoding/one_hot.py
+++ b/feature_engine/encoding/one_hot.py
@@ -7,7 +7,6 @@ import numpy as np
 import pandas as pd
 
 from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
-from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
 class OneHotEncoder(BaseCategoricalTransformer):
@@ -146,14 +145,10 @@ class OneHotEncoder(BaseCategoricalTransformer):
         if not isinstance(drop_last_binary, bool):
             raise ValueError("drop_last_binary takes only True or False")
 
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
-
+        super().__init__(variables, ignore_format)
         self.top_categories = top_categories
         self.drop_last = drop_last
         self.drop_last_binary = drop_last_binary
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """
diff --git a/feature_engine/encoding/ordinal.py b/feature_engine/encoding/ordinal.py
index b1dd6d7..29ff95d 100644
--- a/feature_engine/encoding/ordinal.py
+++ b/feature_engine/encoding/ordinal.py
@@ -5,11 +5,10 @@ from typing import List, Optional, Union
 
 import pandas as pd
 
-from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
-from feature_engine.variable_manipulation import _check_input_parameter_variables
+from feature_engine.encoding.base_encoder import BaseCategorical
 
 
-class OrdinalEncoder(BaseCategoricalTransformer):
+class OrdinalEncoder(BaseCategorical):
     """
     The OrdinalCategoricalEncoder() replaces categories by ordinal numbers
     (0, 1, 2, 3, etc). The numbers can be ordered based on the mean of the target
@@ -52,6 +51,12 @@ class OrdinalEncoder(BaseCategoricalTransformer):
         type object or categorical. If True, the encoder will select all variables or
         accept all variables entered by the user, including those cast as numeric.
 
+    errors: string, default='ignore'
+        Indicates what to do when categories not present in the train set are
+        encountered during transform. If 'raise', then rare categories will raise an
+        error. If 'ignore', then rare categories will be set as NaN and a warning will
+        be raised instead.
+
     Attributes
     ----------
     encoder_dict_:
@@ -102,6 +107,7 @@ class OrdinalEncoder(BaseCategoricalTransformer):
         encoding_method: str = "ordered",
         variables: Union[None, int, str, List[Union[str, int]]] = None,
         ignore_format: bool = False,
+        errors: str = "ignore"
     ) -> None:
 
         if encoding_method not in ["ordered", "arbitrary"]:
@@ -109,12 +115,9 @@ class OrdinalEncoder(BaseCategoricalTransformer):
                 "encoding_method takes only values 'ordered' and 'arbitrary'"
             )
 
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
+        super().__init__(variables, ignore_format, errors)
 
         self.encoding_method = encoding_method
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """Learn the numbers to be used to replace the categories in each
@@ -174,11 +177,11 @@ class OrdinalEncoder(BaseCategoricalTransformer):
 
         return X
 
-    transform.__doc__ = BaseCategoricalTransformer.transform.__doc__
+    transform.__doc__ = BaseCategorical.transform.__doc__
 
     def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
         X = super().inverse_transform(X)
 
         return X
 
-    inverse_transform.__doc__ = BaseCategoricalTransformer.inverse_transform.__doc__
+    inverse_transform.__doc__ = BaseCategorical.inverse_transform.__doc__
diff --git a/feature_engine/encoding/probability_ratio.py b/feature_engine/encoding/probability_ratio.py
index c4e3d8f..24483e8 100644
--- a/feature_engine/encoding/probability_ratio.py
+++ b/feature_engine/encoding/probability_ratio.py
@@ -6,12 +6,11 @@ from typing import List, Union
 import numpy as np
 import pandas as pd
 
-from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
+from feature_engine.encoding.base_encoder import BaseCategorical
 from feature_engine.validation import _return_tags
-from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
-class PRatioEncoder(BaseCategoricalTransformer):
+class PRatioEncoder(BaseCategorical):
     """
     The PRatioEncoder() replaces categories by the ratio of the probability of the
     target = 1 and the probability of the target = 0.
@@ -72,6 +71,12 @@ class PRatioEncoder(BaseCategoricalTransformer):
         type object or categorical. If True, the encoder will select all variables or
         accept all variables entered by the user, including those cast as numeric.
 
+    errors: string, default='ignore'
+        Indicates what to do when categories not present in the train set are
+        encountered during transform. If 'raise', then rare categories will raise an
+        error. If 'ignore', then rare categories will be set as NaN and a warning will
+        be raised instead.
+
     Attributes
     ----------
     encoder_dict_:
@@ -110,6 +115,7 @@ class PRatioEncoder(BaseCategoricalTransformer):
         encoding_method: str = "ratio",
         variables: Union[None, int, str, List[Union[str, int]]] = None,
         ignore_format: bool = False,
+        errors: str = "ignore"
     ) -> None:
 
         if encoding_method not in ["ratio", "log_ratio"]:
@@ -117,12 +123,9 @@ class PRatioEncoder(BaseCategoricalTransformer):
                 "encoding_method takes only values 'ratio' and 'log_ratio'"
             )
 
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
+        super().__init__(variables, ignore_format, errors)
 
         self.encoding_method = encoding_method
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
 
     def fit(self, X: pd.DataFrame, y: pd.Series):
         """
@@ -198,14 +201,14 @@ class PRatioEncoder(BaseCategoricalTransformer):
 
         return X
 
-    transform.__doc__ = BaseCategoricalTransformer.transform.__doc__
+    transform.__doc__ = BaseCategorical.transform.__doc__
 
     def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
         X = super().inverse_transform(X)
 
         return X
 
-    inverse_transform.__doc__ = BaseCategoricalTransformer.inverse_transform.__doc__
+    inverse_transform.__doc__ = BaseCategorical.inverse_transform.__doc__
 
     def _more_tags(self):
         tags_dict = _return_tags()
diff --git a/feature_engine/encoding/rare_label.py b/feature_engine/encoding/rare_label.py
index 2e882dc..22bbced 100644
--- a/feature_engine/encoding/rare_label.py
+++ b/feature_engine/encoding/rare_label.py
@@ -8,7 +8,6 @@ import numpy as np
 import pandas as pd
 
 from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
-from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
 class RareLabelEncoder(BaseCategoricalTransformer):
@@ -117,15 +116,11 @@ class RareLabelEncoder(BaseCategoricalTransformer):
             if max_n_categories < 0 or not isinstance(max_n_categories, int):
                 raise ValueError("max_n_categories takes only positive integer numbers")
 
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
-
+        super().__init__(variables, ignore_format)
         self.tol = tol
         self.n_categories = n_categories
         self.max_n_categories = max_n_categories
         self.replace_with = replace_with
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """
diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py
index 1186af8..1d319ab 100644
--- a/feature_engine/encoding/woe.py
+++ b/feature_engine/encoding/woe.py
@@ -6,12 +6,11 @@ from typing import List, Union
 import numpy as np
 import pandas as pd
 
-from feature_engine.encoding.base_encoder import BaseCategoricalTransformer
+from feature_engine.encoding.base_encoder import BaseCategorical
 from feature_engine.validation import _return_tags
-from feature_engine.variable_manipulation import _check_input_parameter_variables
 
 
-class WoEEncoder(BaseCategoricalTransformer):
+class WoEEncoder(BaseCategorical):
     """
     The WoEEncoder() replaces categories by the weight of evidence
     (WoE). The WoE was used primarily in the financial sector to create credit risk
@@ -55,6 +54,12 @@ class WoEEncoder(BaseCategoricalTransformer):
         type object or categorical. If True, the encoder will select all variables or
         accept all variables entered by the user, including those cast as numeric.
 
+    errors: string, default='ignore'
+        Indicates what to do when categories not present in the train set are
+        encountered during transform. If 'raise', then rare categories will raise an
+        error. If 'ignore', then rare categories will be set as NaN and a warning will
+        be raised instead.
+
     Attributes
     ----------
     encoder_dict_:
@@ -100,13 +105,10 @@ class WoEEncoder(BaseCategoricalTransformer):
         self,
         variables: Union[None, int, str, List[Union[str, int]]] = None,
         ignore_format: bool = False,
+        errors: str = "ignore"
     ) -> None:
 
-        if not isinstance(ignore_format, bool):
-            raise ValueError("ignore_format takes only booleans True and False")
-
-        self.variables = _check_input_parameter_variables(variables)
-        self.ignore_format = ignore_format
+        super().__init__(variables, ignore_format, errors)
 
     def fit(self, X: pd.DataFrame, y: pd.Series):
         """
@@ -178,14 +180,14 @@ class WoEEncoder(BaseCategoricalTransformer):
 
         return X
 
-    transform.__doc__ = BaseCategoricalTransformer.transform.__doc__
+    transform.__doc__ = BaseCategorical.transform.__doc__
 
     def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
         X = super().inverse_transform(X)
 
         return X
 
-    inverse_transform.__doc__ = BaseCategoricalTransformer.inverse_transform.__doc__
+    inverse_transform.__doc__ = BaseCategorical.inverse_transform.__doc__
 
     def _more_tags(self):
         tags_dict = _return_tags()


Test patch:
diff --git a/tests/test_encoding/check_estimator_encoders.py b/tests/test_encoding/test_check_estimator_encoders.py
similarity index 100%
rename from tests/test_encoding/check_estimator_encoders.py
rename to tests/test_encoding/test_check_estimator_encoders.py
diff --git a/tests/test_encoding/test_count_frequency_encoder.py b/tests/test_encoding/test_count_frequency_encoder.py
index e8ef049..cb101ae 100644
--- a/tests/test_encoding/test_count_frequency_encoder.py
+++ b/tests/test_encoding/test_count_frequency_encoder.py
@@ -122,11 +122,30 @@ def test_error_if_input_df_contains_categories_not_present_in_fit_df(
 ):
     # test case 3: when dataset to be transformed contains categories not present in
     # training dataset
-    with pytest.warns(UserWarning):
-        encoder = CountFrequencyEncoder()
+
+    msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
+
+    # check for warning when rare_labels equals 'ignore'
+    with pytest.warns(UserWarning) as record:
+        encoder = CountFrequencyEncoder(errors="ignore")
+        encoder.fit(df_enc)
+        encoder.transform(df_enc_rare)
+
+    # check that only one warning was raised
+    assert len(record) == 1
+    # check that the message matches
+    assert record[0].message.args[0] == msg
+
+    # check for error when rare_labels equals 'raise'
+    with pytest.raises(ValueError) as record:
+        encoder = CountFrequencyEncoder(errors="raise")
+
         encoder.fit(df_enc)
         encoder.transform(df_enc_rare)
 
+    # check that the error message matches
+    assert str(record.value) == msg
+
 
 def test_fit_raises_error_if_df_contains_na(df_enc_na):
     # test case 4: when dataset contains na, fit method
@@ -238,3 +257,8 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
     encoder = CountFrequencyEncoder(encoding_method="frequency", variables=["var_A"])
     X = encoder.fit_transform(df_enc_category_dtypes)
     assert X["var_A"].dtypes == float
+
+
+def test_error_if_rare_labels_not_permitted_value():
+    with pytest.raises(ValueError):
+        CountFrequencyEncoder(errors="empanada")
diff --git a/tests/test_encoding/test_mean_encoder.py b/tests/test_encoding/test_mean_encoder.py
index 706997a..144883d 100644
--- a/tests/test_encoding/test_mean_encoder.py
+++ b/tests/test_encoding/test_mean_encoder.py
@@ -126,11 +126,29 @@ def test_warning_if_transform_df_contains_categories_not_present_in_fit_df(
 ):
     # test case 4: when dataset to be transformed contains categories not present
     # in training dataset
-    with pytest.warns(UserWarning):
-        encoder = MeanEncoder()
+
+    msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
+
+    # check for warning when rare_labels equals 'ignore'
+    with pytest.warns(UserWarning) as record:
+        encoder = MeanEncoder(errors="ignore")
         encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
         encoder.transform(df_enc_rare[["var_A", "var_B"]])
 
+    # check that only one warning was raised
+    assert len(record) == 1
+    # check that the message matches
+    assert record[0].message.args[0] == msg
+
+    # check for error when rare_labels equals 'raise'
+    with pytest.raises(ValueError) as record:
+        encoder = MeanEncoder(errors="raise")
+        encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+        encoder.transform(df_enc_rare[["var_A", "var_B"]])
+
+    # check that the error message matches
+    assert str(record.value) == msg
+
 
 def test_fit_raises_error_if_df_contains_na(df_enc_na):
     # test case 4: when dataset contains na, fit method
@@ -293,3 +311,8 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
 
     pd.testing.assert_frame_equal(X, transf_df[["var_A", "var_B"]], check_dtype=False)
     assert X["var_A"].dtypes == float
+
+
+def test_error_if_rare_labels_not_permitted_value():
+    with pytest.raises(ValueError):
+        MeanEncoder(errors="empanada")
diff --git a/tests/test_encoding/test_ordinal_encoder.py b/tests/test_encoding/test_ordinal_encoder.py
index c1ab2e7..394cbb9 100644
--- a/tests/test_encoding/test_ordinal_encoder.py
+++ b/tests/test_encoding/test_ordinal_encoder.py
@@ -67,10 +67,27 @@ def test_error_if_input_df_contains_categories_not_present_in_training_df(
 ):
     # test case 4: when dataset to be transformed contains categories not present
     # in training dataset
-    with pytest.warns(UserWarning):
-        encoder = OrdinalEncoder(encoding_method="arbitrary")
-        encoder.fit(df_enc)
-        encoder.transform(df_enc_rare)
+    msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
+
+    # check for warning when rare_labels equals 'ignore'
+    with pytest.warns(UserWarning) as record:
+        encoder = OrdinalEncoder(errors="ignore")
+        encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+        encoder.transform(df_enc_rare[["var_A", "var_B"]])
+
+    # check that only one warning was raised
+    assert len(record) == 1
+    # check that the message matches
+    assert record[0].message.args[0] == msg
+
+    # check for error when rare_labels equals 'raise'
+    with pytest.raises(ValueError) as record:
+        encoder = OrdinalEncoder(errors="raise")
+        encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+        encoder.transform(df_enc_rare[["var_A", "var_B"]])
+
+    # check that the error message matches
+    assert str(record.value) == msg
 
 
 def test_non_fitted_error(df_enc):
@@ -156,3 +173,8 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
     # test transform output
     pd.testing.assert_frame_equal(X, transf_df[["var_A", "var_B"]], check_dtype=False)
     assert X["var_A"].dtypes == int
+
+
+def test_error_if_rare_labels_not_permitted_value():
+    with pytest.raises(ValueError):
+        OrdinalEncoder(errors="empanada")
diff --git a/tests/test_encoding/test_probability_ratio_encoder.py b/tests/test_encoding/test_probability_ratio_encoder.py
index fb98fa2..bee9bec 100644
--- a/tests/test_encoding/test_probability_ratio_encoder.py
+++ b/tests/test_encoding/test_probability_ratio_encoder.py
@@ -253,3 +253,34 @@ def test_logratio_on_numerical_variables(df_enc_numeric):
     # test error raise
     with pytest.raises(ValueError):
         PRatioEncoder(encoding_method="other")
+
+
+def test_warn_if_transform_df_contains_categories_not_seen_in_fit(df_enc, df_enc_rare):
+    # test case 3: when dataset to be transformed contains categories not present
+    # in training dataset
+    msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
+
+    # check for warning when rare_labels equals 'ignore'
+    with pytest.warns(UserWarning) as record:
+        encoder = PRatioEncoder(errors="ignore")
+        encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+        encoder.transform(df_enc_rare[["var_A", "var_B"]])
+
+    # check that only one warning was raised
+    assert len(record) == 1
+    # check that the message matches
+    assert record[0].message.args[0] == msg
+
+    # check for error when rare_labels equals 'raise'
+    with pytest.raises(ValueError) as record:
+        encoder = PRatioEncoder(errors="raise")
+        encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+        encoder.transform(df_enc_rare[["var_A", "var_B"]])
+
+    # check that the error message matches
+    assert str(record.value) == msg
+
+
+def test_error_if_rare_labels_not_permitted_value():
+    with pytest.raises(ValueError):
+        PRatioEncoder(errors="empanada")
diff --git a/tests/test_encoding/test_woe_encoder.py b/tests/test_encoding/test_woe_encoder.py
index 535dfc2..d2df729 100644
--- a/tests/test_encoding/test_woe_encoder.py
+++ b/tests/test_encoding/test_woe_encoder.py
@@ -90,11 +90,28 @@ def test_error_target_is_not_passed(df_enc):
 def test_warn_if_transform_df_contains_categories_not_seen_in_fit(df_enc, df_enc_rare):
     # test case 3: when dataset to be transformed contains categories not present
     # in training dataset
-    encoder = WoEEncoder(variables=None)
-    with pytest.warns(UserWarning):
+    msg = "During the encoding, NaN values were introduced in the feature(s) var_A."
+
+    # check for error when rare_labels equals 'raise'
+    with pytest.warns(UserWarning) as record:
+        encoder = WoEEncoder(errors="ignore")
+        encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+        encoder.transform(df_enc_rare[["var_A", "var_B"]])
+
+    # check that only one warning was raised
+    assert len(record) == 1
+    # check that the message matches
+    assert record[0].message.args[0] == msg
+
+    # check for error when rare_labels equals 'raise'
+    with pytest.raises(ValueError) as record:
+        encoder = WoEEncoder(errors="raise")
         encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
         encoder.transform(df_enc_rare[["var_A", "var_B"]])
 
+    # check that the error message matches
+    assert str(record.value) == msg
+
 
 def test_error_if_target_not_binary():
     # test case 4: the target is not binary
@@ -292,3 +309,8 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
 
     pd.testing.assert_frame_equal(X, transf_df[["var_A", "var_B"]], check_dtype=False)
     assert X["var_A"].dtypes == float
+
+
+def test_error_if_rare_labels_not_permitted_value():
+    with pytest.raises(ValueError):
+        WoEEncoder(errors="empanada")