Repository: feature-engine/feature_engine (python)

Issue:
add functionality in MeanEncoder to replace unseen by the target prior
How do you feel about this one @GLevV ?

We already added similar functionality in the CountEncoder for inspiration:
https://github.com/feature-engine/feature_engine/blob/main/feature_engine/encoding/count_frequency.py


Pull request description:
replace unseen categories by the target mean prior in MeanEncoder
Will fix #525 

Patch:
diff --git a/feature_engine/encoding/mean_encoding.py b/feature_engine/encoding/mean_encoding.py
index 0d6df2e..7a7465a 100644
--- a/feature_engine/encoding/mean_encoding.py
+++ b/feature_engine/encoding/mean_encoding.py
@@ -1,7 +1,7 @@
 # Authors: Soledad Galli <solegalli@protonmail.com>
 # License: BSD 3 clause
-
 from typing import List, Union
+from collections import defaultdict
 
 import pandas as pd
 
@@ -22,12 +22,19 @@ from feature_engine._docstrings.methods import (
 )
 from feature_engine._docstrings.substitute import Substitution
 from feature_engine.dataframe_checks import check_X_y
+from feature_engine.encoding._helper_functions import check_parameter_unseen
 from feature_engine.encoding.base_encoder import (
     CategoricalInitExpandedMixin,
     CategoricalMethodsMixin,
 )
 
 
+_unseen_docstring = (
+    _unseen_docstring
+    + """ If `'encode'`, unseen categories will be encoded with the prior."""
+)
+
+
 @Substitution(
     ignore_format=_ignore_format_docstring,
     variables=_variables_categorical_docstring,
@@ -149,7 +156,7 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
         unseen: str = "ignore",
         smoothing: Union[int, float, str] = 0.0,
     ) -> None:
-        super().__init__(variables, ignore_format, unseen)
+        super().__init__(variables, ignore_format)
         if (
             (isinstance(smoothing, str) and (smoothing != 'auto')) or
             (isinstance(smoothing, (float, int)) and smoothing < 0)
@@ -159,6 +166,8 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
                 f"Got {smoothing} instead."
             )
         self.smoothing = smoothing
+        check_parameter_unseen(unseen, ["ignore", "raise", "encode"])
+        self.unseen = unseen
 
     def fit(self, X: pd.DataFrame, y: pd.Series):
         """
@@ -181,10 +190,14 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
         self.encoder_dict_ = {}
 
         y_prior = y.mean()
-        if self.smoothing == 'auto':
+        dct_init = defaultdict(
+            lambda: y_prior
+        ) if self.unseen == "encode" else {}  # type: Union[dict, defaultdict]
+
+        if self.smoothing == "auto":
             y_var = y.var(ddof=0)
         for var in self.variables_:
-            if self.smoothing == 'auto':
+            if self.smoothing == "auto":
                 damping = y.groupby(X[var]).var(ddof=0) / y_var
             else:
                 damping = self.smoothing
@@ -193,7 +206,7 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
             self.encoder_dict_[var] = (
                 _lambda * y.groupby(X[var]).mean() +
                 (1. - _lambda) * y_prior
-            ).to_dict()
+            ).to_dict(dct_init)
 
         self._check_encoding_dictionary()
 


Test patch:
diff --git a/tests/test_encoding/test_mean_encoder.py b/tests/test_encoding/test_mean_encoder.py
index a7e89e0..edb9e4e 100644
--- a/tests/test_encoding/test_mean_encoder.py
+++ b/tests/test_encoding/test_mean_encoder.py
@@ -328,7 +328,7 @@ def test_auto_smoothing(df_enc):
     pd.testing.assert_frame_equal(X, transf_df[["var_A", "var_B"]])
 
 
-def test_smoothing(df_enc):
+def test_value_smoothing(df_enc):
     encoder = MeanEncoder(smoothing=100)
     encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
     X = encoder.transform(df_enc[["var_A", "var_B"]])
@@ -360,3 +360,11 @@ def test_smoothing(df_enc):
 def test_error_if_rare_labels_not_permitted_value():
     with pytest.raises(ValueError):
         MeanEncoder(unseen="empanada")
+
+
+def test_encoding_new_categories(df_enc):
+    df_unseen = pd.DataFrame({"var_A": ["D"], "var_B": ["D"]})
+    encoder = MeanEncoder(unseen="encode")
+    encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
+    df_transformed = encoder.transform(df_unseen)
+    assert (df_transformed == df_enc["target"].mean()).all(axis=None)