Repository: feature-engine/feature_engine (python)

Issue:
`RareLabelEncoder` with `missing_values`="ignore" does not work properly with `sklearn.compose.ColumnTransformer`
**Describe the bug**
An exception is raised with for no reason.

**To Reproduce**
```python
  import pandas as pd
  from sklearn.compose import ColumnTransformer
  from feature_engine.encoding import RareLabelEncoder


  input_df = pd.DataFrame(
      {
          "num_col1": [1, 2, 3, 4, 5],
          "num_col2": [1, 2, 3, 4, 5],
          "num_col3": ["1.1", "2.2", "3.3", "4.4", "5.5"],
          "cat_col1": ["A", "A", "A", "B", "B"],
          "cat_col2": ["A", "A", None, "B", "B"],
          "cat_col3": [1, 0, 1, 0, 1],
      }
  )

  ct = ColumnTransformer(
      transformers=[
          (
               "categorical_feat_pipeline",
               RareLabelEncoder(missing_values="ignore"),
               ["cat_col1", "cat_col2", "cat_col3"]
           ),
      ],
  )
  ct.fit(input_df)
```
**Expected behavior**

`RareLabelEncoder` should work as usual.

**Screenshots**
```
/lib/python3.10/site-packages/sklearn/compose/_column_transformer.py:693: in fit
    self.fit_transform(X, y=y)
/lib/python3.10/site-packages/sklearn/utils/_set_output.py:142: in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
/lib/python3.10/site-packages/sklearn/compose/_column_transformer.py:726: in fit_transform
    result = self._fit_transform(X, y, _fit_transform_one)
/lib/python3.10/site-packages/sklearn/compose/_column_transformer.py:657: in _fit_transform
    return Parallel(n_jobs=self.n_jobs)(
/lib/python3.10/site-packages/joblib/parallel.py:1085: in __call__
    if self.dispatch_one_batch(iterator):
/lib/python3.10/site-packages/joblib/parallel.py:901: in dispatch_one_batch
    self._dispatch(tasks)
/lib/python3.10/site-packages/joblib/parallel.py:819: in _dispatch
    job = self._backend.apply_async(batch, callback=cb)
/lib/python3.10/site-packages/joblib/_parallel_backends.py:208: in apply_async
    result = ImmediateResult(func)
/lib/python3.10/site-packages/joblib/_parallel_backends.py:597: in __init__
    self.results = batch()
/lib/python3.10/site-packages/joblib/parallel.py:288: in __call__
    return [func(*args, **kwargs)
/lib/python3.10/site-packages/joblib/parallel.py:288: in <listcomp>
    return [func(*args, **kwargs)
/lib/python3.10/site-packages/sklearn/utils/fixes.py:117: in __call__
    return self.function(*args, **kwargs)
/lib/python3.10/site-packages/sklearn/pipeline.py:894: in _fit_transform_one
    res = transformer.fit_transform(X, y, **fit_params)
/lib/python3.10/site-packages/sklearn/utils/_set_output.py:142: in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
/lib/python3.10/site-packages/sklearn/utils/_set_output.py:142: in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
/lib/python3.10/site-packages/sklearn/utils/_set_output.py:142: in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
/lib/python3.10/site-packages/sklearn/base.py:848: in fit_transform
    return self.fit(X, **fit_params).transform(X)
/lib/python3.10/site-packages/sklearn/utils/_set_output.py:142: in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = RareLabelEncoder(missing_values='ignore'), X =   cat_col1 cat_col2  cat_col3
0        A        A         1
1        A        A         0
2        A     None         1
3        B        B         0
4        B        B         1

    def transform(self, X: pd.DataFrame) -> pd.DataFrame:
        \"""
        Group infrequent categories. Replace infrequent categories by the string 'Rare'
        or any other name provided by the user.
    
        Parameters
        ----------
        X: pandas dataframe of shape = [n_samples, n_features]
            The input samples.
    
        Returns
        -------
        X: pandas dataframe of shape = [n_samples, n_features]
            The dataframe where rare categories have been grouped.
        \"""
    
        X = self._check_transform_input_and_state(X)
    
        # check if dataset contains na
        if self.missing_values == "raise":
            _check_optional_contains_na(X, self.variables_)
    
            for feature in self.variables_:
                X[feature] = np.where(
                    X[feature].isin(self.encoder_dict_[feature]),
                    X[feature],
                    self.replace_with,
                )
    
        else:
            for feature in self.variables_:
                X[feature] = np.where(
>                   X[feature].isin(self.encoder_dict_[feature] + [np.nan]),
                    X[feature],
                    self.replace_with,
                )
E               TypeError: can only concatenate str (not "float") to str
```
**Desktop (please complete the following information):**
 - OS: MacOs
 - Version 1.6.0

**Additional context**
Add any other context about the problem here.


Pull request:
fix various bugs in RareLabelEncoder
closes #651

Patch:
diff --git a/feature_engine/encoding/rare_label.py b/feature_engine/encoding/rare_label.py
index 4859b2c..c7afb18 100644
--- a/feature_engine/encoding/rare_label.py
+++ b/feature_engine/encoding/rare_label.py
@@ -218,7 +218,7 @@ class RareLabelEncoder(CategoricalInitMixinNA, CategoricalMethodsMixin):
                     "indicated in n_categories. Thus, all categories will be "
                     "considered frequent".format(var)
                 )
-                self.encoder_dict_[var] = X[var].unique()
+                self.encoder_dict_[var] = list(X[var].unique())
 
         self.variables_ = variables_
         self._get_feature_names_in(X)
@@ -247,19 +247,19 @@ class RareLabelEncoder(CategoricalInitMixinNA, CategoricalMethodsMixin):
             _check_optional_contains_na(X, self.variables_)
 
             for feature in self.variables_:
-                X[feature] = np.where(
-                    X[feature].isin(self.encoder_dict_[feature]),
-                    X[feature],
-                    self.replace_with,
-                )
+                if X[feature].dtype == "category":
+                    X[feature] = X[feature].cat.add_categories(self.replace_with)
+                X.loc[
+                    ~X[feature].isin(self.encoder_dict_[feature]), feature
+                ] = self.replace_with
 
         else:
             for feature in self.variables_:
-                X[feature] = np.where(
-                    X[feature].isin(self.encoder_dict_[feature] + [np.nan]),
-                    X[feature],
-                    self.replace_with,
-                )
+                if X[feature].dtype == "category":
+                    X[feature] = X[feature].cat.add_categories(self.replace_with)
+                X.loc[
+                    ~X[feature].isin(self.encoder_dict_[feature] + [np.nan]), feature
+                ] = self.replace_with
 
         return X
 


Test patch:
diff --git a/tests/test_encoding/test_rare_label_encoder.py b/tests/test_encoding/test_rare_label_encoder.py
index 1a17553..ba85299 100644
--- a/tests/test_encoding/test_rare_label_encoder.py
+++ b/tests/test_encoding/test_rare_label_encoder.py
@@ -54,6 +54,34 @@ def test_defo_params_plus_automatically_find_variables(df_enc_big):
     pd.testing.assert_frame_equal(X, df)
 
 
+def test_when_varnames_are_numbers(df_enc_big):
+    input_df = df_enc_big.copy()
+    input_df.columns = [1, 2, 3]
+
+    encoder = RareLabelEncoder(
+        tol=0.06, n_categories=5, variables=None, replace_with="Rare"
+    )
+    X = encoder.fit_transform(input_df)
+
+    # expected output
+    df = {
+        1: ["A"] * 6 + ["B"] * 10 + ["C"] * 4 + ["D"] * 10 + ["Rare"] * 4 + ["G"] * 6,
+        2: ["A"] * 10 + ["B"] * 6 + ["C"] * 4 + ["D"] * 10 + ["Rare"] * 4 + ["G"] * 6,
+        3: ["A"] * 4 + ["B"] * 6 + ["C"] * 10 + ["D"] * 10 + ["Rare"] * 4 + ["G"] * 6,
+    }
+    df = pd.DataFrame(df)
+
+    frequenc_cat = {
+        1: ["B", "D", "A", "G", "C"],
+        2: ["A", "D", "B", "G", "C"],
+        3: ["C", "D", "B", "G", "A"],
+    }
+
+    assert encoder.variables_ == [1, 2, 3]
+    assert encoder.encoder_dict_ == frequenc_cat
+    pd.testing.assert_frame_equal(X, df)
+
+
 def test_correctly_ignores_nan_in_transform(df_enc_big):
     encoder = RareLabelEncoder(
         tol=0.06,
@@ -102,7 +130,7 @@ def test_correctly_ignores_nan_in_fit(df_enc_big):
         n_categories=3,
         missing_values="ignore",
     )
-    X = encoder.fit_transform(df)
+    encoder.fit(df)
 
     # expected:
     frequenc_cat = {
@@ -134,6 +162,90 @@ def test_correctly_ignores_nan_in_fit(df_enc_big):
     pd.testing.assert_frame_equal(X, tt)
 
 
+def test_correctly_ignores_nan_in_fit_when_var_is_numerical(df_enc_big):
+
+    df = df_enc_big.copy()
+    df["var_C"] = [
+        1,
+        1,
+        1,
+        1,
+        2,
+        2,
+        2,
+        2,
+        2,
+        2,
+        3,
+        3,
+        3,
+        3,
+        3,
+        3,
+        3,
+        3,
+        3,
+        3,
+        4,
+        4,
+        4,
+        4,
+        4,
+        4,
+        4,
+        4,
+        4,
+        4,
+        5,
+        5,
+        6,
+        6,
+        np.nan,
+        np.nan,
+        np.nan,
+        np.nan,
+        np.nan,
+        np.nan,
+    ]
+
+    encoder = RareLabelEncoder(
+        tol=0.06,
+        n_categories=3,
+        missing_values="ignore",
+        ignore_format=True,
+    )
+    encoder.fit(df)
+
+    # expected:
+    frequenc_cat = {
+        "var_A": ["B", "D", "A", "G", "C"],
+        "var_B": ["A", "D", "B", "G", "C"],
+        "var_C": [3, 4, 2, 1],
+    }
+    assert encoder.encoder_dict_ == frequenc_cat
+
+    # input
+    t = pd.DataFrame(
+        {
+            "var_A": ["A", np.nan, "J", "G"],
+            "var_B": ["A", np.nan, "J", "G"],
+            "var_C": [3, np.nan, 9, 10],
+        }
+    )
+
+    # expected
+    tt = pd.DataFrame(
+        {
+            "var_A": ["A", np.nan, "Rare", "G"],
+            "var_B": ["A", np.nan, "Rare", "G"],
+            "var_C": [3.0, np.nan, "Rare", "Rare"],
+        }
+    )
+
+    X = encoder.transform(t)
+    pd.testing.assert_frame_equal(X, tt, check_dtype=False)
+
+
 def test_user_provides_grouping_label_name_and_variable_list(df_enc_big):
     # test case 2: user provides alternative grouping value and variable list
     encoder = RareLabelEncoder(
@@ -316,12 +428,84 @@ def test_variables_cast_as_category(df_enc_big):
         + ["G"] * 6,
     }
     df = pd.DataFrame(df)
+    df["var_B"] = pd.Categorical(df["var_B"])
 
     # test fit attr
     assert encoder.variables_ == ["var_A", "var_B", "var_C"]
     assert encoder.n_features_in_ == 3
     # test transform output
-    pd.testing.assert_frame_equal(X, df)
+    pd.testing.assert_frame_equal(X, df, check_categorical=False)
+
+
+def test_variables_cast_as_category_with_na_in_transform(df_enc_big):
+    encoder = RareLabelEncoder(
+        tol=0.06,
+        n_categories=5,
+        variables=None,
+        replace_with="Rare",
+        missing_values="ignore",
+    )
+
+    df_enc_big = df_enc_big.copy()
+    df_enc_big["var_B"] = df_enc_big["var_B"].astype("category")
+    encoder.fit(df_enc_big)
+
+    # input
+    t = pd.DataFrame(
+        {
+            "var_A": ["A", np.nan, "J", "G"],
+            "var_B": ["A", np.nan, "J", "G"],
+            "var_C": ["A", np.nan, "J", "G"],
+        }
+    )
+    t["var_B"] = pd.Categorical(t["var_B"])
+
+    # expected
+    tt = pd.DataFrame(
+        {
+            "var_A": ["A", np.nan, "Rare", "G"],
+            "var_B": ["A", np.nan, "Rare", "G"],
+            "var_C": ["A", np.nan, "Rare", "G"],
+        }
+    )
+    tt["var_B"] = pd.Categorical(tt["var_B"])
+    pd.testing.assert_frame_equal(encoder.transform(t), tt, check_categorical=False)
+
+
+def test_variables_cast_as_category_with_na_in_fit(df_enc_big):
+
+    df = df_enc_big.copy()
+    df.loc[df["var_C"] == "G", "var_C"] = np.nan
+    df["var_C"] = df["var_C"].astype("category")
+
+    encoder = RareLabelEncoder(
+        tol=0.06,
+        n_categories=3,
+        missing_values="ignore",
+    )
+    encoder.fit(df)
+
+    # input
+    t = pd.DataFrame(
+        {
+            "var_A": ["A", np.nan, "J", "G"],
+            "var_B": ["A", np.nan, "J", "G"],
+            "var_C": ["C", np.nan, "J", "G"],
+        }
+    )
+    t["var_C"] = pd.Categorical(t["var_C"])
+
+    # expected
+    tt = pd.DataFrame(
+        {
+            "var_A": ["A", np.nan, "Rare", "G"],
+            "var_B": ["A", np.nan, "Rare", "G"],
+            "var_C": ["C", np.nan, "Rare", "Rare"],
+        }
+    )
+    tt["var_C"] = pd.Categorical(tt["var_C"])
+
+    pd.testing.assert_frame_equal(encoder.transform(t), tt, check_categorical=False)
 
 
 def test_inverse_transform_raises_not_implemented_error(df_enc_big):