Repository: feature-engine/feature_engine

Issue:
`OrdinalEncoder` could output -1 for unseen categories
The OrdinalEncoder has an errors argument which can either raise an error or output NaNs when encountering new categories. For this particular class, it'd make sense to output -1 when a new category is encoded instead of generating NaNs.


Discussion:
solegalli: FYI @GLevV 

Patch:
diff --git a/README.md b/README.md
index 0461775..c87d135 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,7 @@ transforming parameters from the data and then transform it.
 * PRatioEncoder
 * RareLabelEncoder
 * DecisionTreeEncoder
+* StringSimilarityEncoder
 
 ### Discretisation methods
 * EqualFrequencyDiscretiser
diff --git a/docs/api_doc/encoding/StringSimilarityEncoder.rst b/docs/api_doc/encoding/StringSimilarityEncoder.rst
new file mode 100644
index 0000000..e45d1d6
--- /dev/null
+++ b/docs/api_doc/encoding/StringSimilarityEncoder.rst
@@ -0,0 +1,6 @@
+StringSimilarityEncoder
+=======================
+
+.. autoclass:: feature_engine.encoding.StringSimilarityEncoder
+    :members:
+
diff --git a/docs/api_doc/encoding/index.rst b/docs/api_doc/encoding/index.rst
index f187632..b288d55 100644
--- a/docs/api_doc/encoding/index.rst
+++ b/docs/api_doc/encoding/index.rst
@@ -27,6 +27,7 @@ and make the transformers also accept numerical variables as input.
    PRatioEncoder
    DecisionTreeEncoder
    RareLabelEncoder
+   StringSimilarityEncoder
 
 Other categorical encoding libraries
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/index.rst b/docs/index.rst
index 1e4ad97..d66646b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -130,6 +130,7 @@ Categorical Encoders: Encoders
 - :doc:`api_doc/encoding/PRatioEncoder`: replaces categories by a ratio of probabilities
 - :doc:`api_doc/encoding/DecisionTreeEncoder`: replaces categories by predictions of a decision tree
 - :doc:`api_doc/encoding/RareLabelEncoder`: groups infrequent categories
+- :doc:`api_doc/encoding/StringSimilarityEncoder`: encodes categories based on string similarity
 
 Variable Discretisation: Discretisers
 -------------------------------------
diff --git a/docs/user_guide/encoding/StringSimilarityEncoder.rst b/docs/user_guide/encoding/StringSimilarityEncoder.rst
new file mode 100644
index 0000000..1c64554
--- /dev/null
+++ b/docs/user_guide/encoding/StringSimilarityEncoder.rst
@@ -0,0 +1,278 @@
+.. _string_similarity:
+
+.. currentmodule:: feature_engine.encoding
+
+
+StringSimilarityEncoder
+=======================
+
+The :class:`StringSimilarityEncoder()` replaces categorical variables with a set of float
+variables that capture the similarity between the category names. The new variables
+have values between 0 and 1, where 0 indicates no similarity and 1 is an exact
+match between the names of the categories.
+
+To calculate the similarity between the categories, :class:`StringSimilarityEncoder()`
+uses Gestalt pattern matching. Under the hood, :class:`StringSimilarityEncoder()` uses
+the `quick_ratio` method from the `SequanceMatcher()` from `difflib`.
+
+The similarity is calculated as:
+
+.. math::
+
+    GPM = 2 M / T
+
+where T is the total number of elements in both sequences and M is the number of matches.
+
+For example, the similarity between the categories "dog" and "dig" is 0.66. T is the
+total number of elements in both categories, that is 6. There are 2 matches between
+the words, the letters d and g, so: 2 * M / T = 2 * 2 / 6 = 0.66.
+
+Output of the :class:`StringSimilarityEncoder()`
+------------------------------------------------
+
+Let's create a dataframe with the categories "dog", "dig" and "cat":
+
+.. code:: python
+
+    import pandas as pd
+    from feature_engine.encoding import StringSimilarityEncoder
+
+    df = pd.DataFrame({"words": ["dog", "dig", "cat"]})
+    df
+
+We see the dataframe in the following output:
+
+.. code:: python
+
+      words
+    0   dog
+    1   dig
+    2   cat
+
+Let's now encode the variable:
+
+.. code:: python
+
+    encoder =  StringSimilarityEncoder()
+    dft = encoder.fit_transform(df)
+    dft
+
+We see the encoded variables below:
+
+
+.. code:: python
+
+       words_dog  words_dig  words_cat
+    0   1.000000   0.666667        0.0
+    1   0.666667   1.000000        0.0
+    2   0.000000   0.000000        1.0
+
+
+Note that :class:`StringSimilarityEncoder()` replaces the original variables by the
+distance variables.
+
+:class:`StringSimilarityEncoder()` vs One-hot encoding
+------------------------------------------------------
+
+String similarity encoding is similar to one-hot encoding, in the sense that each category is
+encoded as a new variable. But the values, instead of 1 or 0, are the similarity
+between the observation's category and the dummy variable. It is suitable for poorly
+defined (or 'dirty') categorical variables.
+
+Encoding only popular categories
+--------------------------------
+
+The :class:`StringSimilarityEncoder()` can also create similarity variables for the *n* most popular
+categories, *n* being determined by the user. For example, if we encode only the 6 more popular categories, by
+setting the parameter `top_categories=6`, the transformer will add variables only
+for the 6 most frequent categories. The most frequent categories are those with the largest
+number of observations. This behaviour is useful when the categorical variables are highly cardinal,
+to control the expansion of the feature space.
+
+Specifying how :class:`StringSimilarityEncoder()` should deal with missing values
+---------------------------------------------------------------------------------
+
+The :class:`StringSimilarityEncoder()` has three options for dealing with missing values, which can be
+specified with the parameter `missing_values`:
+
+  1. Ignore NaNs (option `ignore`) - will leave the NaN in the resulting dataframe after transformation.
+     Could be useful, if the next step in the pipeline is imputation or if the machine learning algorithm
+     can handle missing data out-of-the-box.
+  2. Impute NaNs (option `impute`) - will impute NaN with an empty string, and then calculate the similarity
+     between the empty string and the variable's categories. Most of the time, the similarity value will be
+     0 in resulting dataframe. This is the default option.
+  3. Raise an error (option `raise`) - will raise an error if NaN is present during `fit`, `transform` or
+     `fit_transform`. Could be useful for debugging and monitoring purposes.
+
+
+Important
+---------
+
+:class:`StringSimilarityEncoder()` will encode unseen categories by out-of-the-box, by measuring the
+string similarity to the seen categories.
+
+No text preprocessing is applied by :class:`StringSimilarityEncoder()`. Be mindful of preparing
+string categorical variables if needed.
+
+:class:`StringSimilarityEncoder()` works with categorical variables by default. And it has the option to
+encode numerical variables as well. This is useful, when the values of the numerical variables are more
+useful as strings, than as numbers. For example, for variables like barcode.
+
+Examples
+--------
+
+Let's look at an example using the Titanic Dataset. First we load the data and divide it
+into a train and a test set:
+
+.. code:: python
+
+    import string
+    import numpy as np
+    import pandas as pd
+    from sklearn.model_selection import train_test_split
+
+    from feature_engine.encoding import StringSimilarityEncoder
+
+    # Helper function for loading and preprocessing data
+    def load_titanic() -> pd.DataFrame:
+        translate_table = str.maketrans('' , '', string.punctuation)
+        data = pd.read_csv('https://www.openml.org/data/get_csv/16826755/phpMYEkMl')
+        data = data.replace('?', np.nan)
+        data['home.dest'] = (
+        data['home.dest']
+        .str.strip()
+        .str.translate(translate_table)
+        .str.replace('  ', ' ')
+        .str.lower()
+        )
+        data['name'] = (
+        data['name']
+        .str.strip()
+        .str.translate(translate_table)
+        .str.replace('  ', ' ')
+        .str.lower()
+        )
+        data['ticket'] = (
+        data['ticket']
+        .str.strip()
+        .str.translate(translate_table)
+        .str.replace('  ', ' ')
+        .str.lower()
+        )
+        return data
+
+    data = load_titanic()
+
+    # Separate into train and test sets
+    X_train, X_test, y_train, y_test = train_test_split(
+        data.drop(['survived', 'sex', 'cabin', 'embarked'], axis=1),
+        data['survived'],
+        test_size=0.3,
+        random_state=0
+    )
+
+    X_train.head()
+
+Below, we see the first rows of the dataset:
+
+.. code:: python
+
+          pclass                             name  age  sibsp  parch  \
+    501        2  mellinger miss madeleine violet   13      0      1
+    588        2                  wells miss joan    4      1      1
+    402        2     duran y more miss florentina   30      1      0
+    1193       3                 scanlan mr james  NaN      0      0
+    686        3       bradley miss bridget delia   22      0      0
+
+                ticket     fare boat body  \
+    501         250644     19.5   14  NaN
+    588          29103       23   14  NaN
+    402   scparis 2148  13.8583   12  NaN
+    1193         36209    7.725  NaN  NaN
+    686         334914    7.725   13  NaN
+
+                                                home.dest
+    501                             england bennington vt
+    588                                 cornwall akron oh
+    402                       barcelona spain havana cuba
+    1193                                              NaN
+    686   kingwilliamstown co cork ireland glens falls ny
+
+
+Now, we set up the encoder to encode only the 2 most frequent categories of each of the
+3 indicated categorical variables:
+
+.. code:: python
+
+	# set up the encoder
+	encoder = StringSimilarityEncoder(
+            top_categories=2,
+            variables=['name', 'home.dest', 'ticket'],
+            )
+
+	# fit the encoder
+	encoder.fit(X_train)
+
+With `fit()` the encoder will learn the most popular categories of the variables, which
+are stored in the attribute `encoder_dict_`.
+
+.. code:: python
+
+	encoder.encoder_dict_
+
+.. code:: python
+
+    {
+      'name': ['mellinger miss madeleine violet', 'barbara mrs catherine david'],
+      'home.dest': ['', 'new york ny'],
+      'ticket': ['ca 2343', 'ca 2144']
+    }
+
+The `encoder_dict_` contains the categories that will derive similarity variables for each
+categorical variable.
+
+With transform, we go ahead and encode the variables. Note that the
+:class:`StringSimilarityEncoder()` will drop the original variables.
+
+.. code:: python
+
+    # transform the data
+    train_t = encoder.transform(X_train)
+    test_t = encoder.transform(X_test)
+
+    test_t.head()
+
+Below, we see the resulting dataframe:
+
+.. code:: python
+
+          pclass  age  sibsp  parch    fare boat body  \
+    1139       3   38      0      0  7.8958  NaN  NaN
+    533        2   21      0      1      21   12  NaN
+    459        2   42      1      0      27  NaN  NaN
+    1150       3  NaN      0      0    14.5  NaN  NaN
+    393        2   25      0      0    31.5  NaN  NaN
+
+          name_mellinger miss madeleine violet  name_barbara mrs catherine david  \
+    1139                              0.454545                          0.550000
+    533                               0.615385                          0.524590
+    459                               0.596491                          0.603774
+    1150                              0.641509                          0.693878
+    393                               0.408163                          0.666667
+
+          home.dest_nan  home.dest_new york ny  ticket_ca 2343  ticket_ca 2144
+    1139            1.0               0.000000        0.461538        0.461538
+    533             0.0               0.370370        0.307692        0.307692
+    459             0.0               0.352941        0.461538        0.461538
+    1150            1.0               0.000000        0.307692        0.307692
+    393             0.0               0.437500        0.666667        0.666667
+
+
+More details
+^^^^^^^^^^^^
+
+For more details into :class:`StringSimilarityEncoder()`'s functionality visit:
+
+- `Jupyter notebook <https://nbviewer.org/github/feature-engine/feature-engine-examples/blob/main/encoding/StringSimilarityEncoder.ipynb>`_
+
+All notebooks can be found in a `dedicated repository <https://github.com/feature-engine/feature-engine-examples>`_.
diff --git a/docs/user_guide/encoding/index.rst b/docs/user_guide/encoding/index.rst
index 561d017..afbd8c6 100644
--- a/docs/user_guide/encoding/index.rst
+++ b/docs/user_guide/encoding/index.rst
@@ -61,6 +61,7 @@ defeat the purpose of these encoding techniques.
    PRatioEncoder
    DecisionTreeEncoder
    RareLabelEncoder
+   StringSimilarityEncoder
 
 
 Additional categorical encoding transformations ara available in the open-source package
diff --git a/feature_engine/encoding/__init__.py b/feature_engine/encoding/__init__.py
index 5ee96bc..d6dfd4d 100644
--- a/feature_engine/encoding/__init__.py
+++ b/feature_engine/encoding/__init__.py
@@ -9,6 +9,7 @@ from .one_hot import OneHotEncoder
 from .ordinal import OrdinalEncoder
 from .probability_ratio import PRatioEncoder
 from .rare_label import RareLabelEncoder
+from .similarity_encoder import StringSimilarityEncoder
 from .woe import WoEEncoder
 
 __all__ = [
@@ -17,7 +18,8 @@ __all__ = [
     "MeanEncoder",
     "OneHotEncoder",
     "OrdinalEncoder",
+    "PRatioEncoder",
     "RareLabelEncoder",
+    "StringSimilarityEncoder",
     "WoEEncoder",
-    "PRatioEncoder",
 ]
diff --git a/feature_engine/encoding/base_encoder.py b/feature_engine/encoding/base_encoder.py
index c04ccd3..f11f2d5 100644
--- a/feature_engine/encoding/base_encoder.py
+++ b/feature_engine/encoding/base_encoder.py
@@ -8,7 +8,6 @@ from sklearn.utils.validation import check_is_fitted
 from feature_engine._base_transformers.mixins import GetFeatureNamesOutMixin
 from feature_engine._docstrings.init_parameters import (
     _ignore_format_docstring,
-    _unseen_docstring,
     _variables_categorical_docstring,
 )
 from feature_engine._docstrings.substitute import Substitution
@@ -24,7 +23,6 @@ from feature_engine.dataframe_checks import (
     _check_X_matches_training_df,
     check_X,
 )
-from feature_engine.encoding._helper_functions import check_parameter_unseen
 from feature_engine.tags import _return_tags
 
 
@@ -58,43 +56,16 @@ class CategoricalInitMixin:
         self.ignore_format = ignore_format
 
 
-@Substitution(
-    ignore_format=_ignore_format_docstring,
-    variables=_variables_categorical_docstring,
-    unseen=_unseen_docstring,
-)
-class CategoricalInitExpandedMixin(CategoricalInitMixin):
-    """Shared initialization parameters across transformers. Contains additional
-    initialization parameters respect to the parent class.
-
-    Parameters
-    ----------
-    {variables}.
-
-    {ignore_format}
-
-    {unseen}
-    """
-
-    def __init__(
-        self,
-        variables: Union[None, int, str, List[Union[str, int]]] = None,
-        ignore_format: bool = False,
-        unseen: str = "ignore",
-    ) -> None:
-        check_parameter_unseen(unseen, ["raise", "ignore"])
-        super().__init__(variables, ignore_format)
-        self.unseen = unseen
-
-
 @Substitution(
     ignore_format=_ignore_format_docstring,
     variables=_variables_categorical_docstring,
 )
 class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOutMixin):
     """Shared methods across categorical transformers.
-    BaseEstimator brings methods get_params() and set_params().
-    TransformerMixin brings method fit_transform()
+
+    - BaseEstimator brings methods get_params() and set_params().
+    - TransformerMixin brings method fit_transform()
+    - GetFeatureNamesOutMixin brings method get_feature_names_out().
     """
 
     def _fit(self, X: pd.DataFrame):
@@ -119,7 +90,7 @@ class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOu
             If there are no categorical variables in the df or the df is empty
             If the variable(s) contain null values
         """
-        if not self.ignore_format:
+        if self.ignore_format is False:
             # find categorical variables or check variables entered by user are object
             self.variables_: List[
                 Union[str, int]
@@ -129,6 +100,10 @@ class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOu
             self.variables_ = _find_all_variables(X, self.variables)
 
     def _get_feature_names_in(self, X: pd.DataFrame):
+        """
+        Returns attributes `featrure_names_in_` and `n_feature_names_in_`, which are
+        standard for all transformers in the library.
+        """
 
         # save input features
         self.feature_names_in_ = X.columns.tolist()
@@ -173,19 +148,6 @@ class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOu
 
         return X
 
-    def _check_encoding_dictionary(self):
-        """After fit(), the encoders should return a dictionary with the original values
-        to numerical mappings as key, values. This function checks that the dictionary
-        was created and is not empty.
-        """
-
-        # check that dictionary is not empty
-        if len(self.encoder_dict_) == 0:
-            raise ValueError(
-                "Encoder could not be fitted. Check the parameters and the variables "
-                "in your dataframe."
-            )
-
     def transform(self, X: pd.DataFrame) -> pd.DataFrame:
         """Replace categories with the learned parameters.
 
@@ -217,8 +179,20 @@ class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOu
                 else:
                     X[feature] = X[feature].astype("float")
 
+        if self.unseen == "encode":
+            X[self.variables_] = X[self.variables_].fillna(
+                self._unseen, downcast="infer"
+            )
+        else:
+            # check if nan values were introduced by the transformation
+            self._check_nan_values_after_transformation(X)
+
+        return X
+
+    def _check_nan_values_after_transformation(self, X):
+
         # check if NaN values were introduced by the encoding
-        if X[self.encoder_dict_.keys()].isnull().sum().sum() > 0:
+        if X[self.variables_].isnull().sum().sum() > 0:
 
             # obtain the name(s) of the columns have null values
             nan_columns = (
@@ -243,8 +217,6 @@ class CategoricalMethodsMixin(BaseEstimator, TransformerMixin, GetFeatureNamesOu
                     f"{nan_columns_str}."
                 )
 
-        return X
-
     def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
         """Convert the encoded variable back to the original values.
 
diff --git a/feature_engine/encoding/count_frequency.py b/feature_engine/encoding/count_frequency.py
index 66fee61..645ea58 100644
--- a/feature_engine/encoding/count_frequency.py
+++ b/feature_engine/encoding/count_frequency.py
@@ -1,7 +1,6 @@
 # Authors: Soledad Galli <solegalli@protonmail.com>
 # License: BSD 3 clause
 
-from collections import defaultdict
 from typing import List, Optional, Union
 
 import pandas as pd
@@ -159,18 +158,17 @@ class CountFrequencyEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
         self._get_feature_names_in(X)
 
         self.encoder_dict_ = {}
-        dct_init = defaultdict(lambda: 0) if self.unseen == "encode" else {}
 
         # learn encoding maps
         for var in self.variables_:
             if self.encoding_method == "count":
-                self.encoder_dict_[var] = X[var].value_counts().to_dict(dct_init)
+                self.encoder_dict_[var] = X[var].value_counts().to_dict()
 
             elif self.encoding_method == "frequency":
-                self.encoder_dict_[var] = (
-                    X[var].value_counts(normalize=True).to_dict(dct_init)
-                )
+                self.encoder_dict_[var] = X[var].value_counts(normalize=True).to_dict()
 
-        self._check_encoding_dictionary()
+        # unseen categories are replaced by 0
+        if self.unseen == "encode":
+            self._unseen = 0
 
         return self
diff --git a/feature_engine/encoding/decision_tree.py b/feature_engine/encoding/decision_tree.py
index 03a45bf..25a53e6 100644
--- a/feature_engine/encoding/decision_tree.py
+++ b/feature_engine/encoding/decision_tree.py
@@ -267,7 +267,9 @@ class DecisionTreeEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
 
     def inverse_transform(self, X: pd.DataFrame):
         """inverse_transform is not implemented for this transformer."""
-        return self
+        raise NotImplementedError(
+            "inverse_transform is not implemented for this transformer."
+        )
 
     def _more_tags(self):
         tags_dict = _return_tags()
diff --git a/feature_engine/encoding/mean_encoding.py b/feature_engine/encoding/mean_encoding.py
index 7a7465a..265d284 100644
--- a/feature_engine/encoding/mean_encoding.py
+++ b/feature_engine/encoding/mean_encoding.py
@@ -1,7 +1,6 @@
 # Authors: Soledad Galli <solegalli@protonmail.com>
 # License: BSD 3 clause
 from typing import List, Union
-from collections import defaultdict
 
 import pandas as pd
 
@@ -21,10 +20,10 @@ from feature_engine._docstrings.methods import (
     _transform_encoders_docstring,
 )
 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.dataframe_checks import check_X_y
 from feature_engine.encoding.base_encoder import (
-    CategoricalInitExpandedMixin,
+    CategoricalInitMixin,
     CategoricalMethodsMixin,
 )
 
@@ -46,7 +45,7 @@ _unseen_docstring = (
     transform=_transform_encoders_docstring,
     inverse_transform=_inverse_transform_docstring,
 )
-class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
+class MeanEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
     """
     The MeanEncoder() replaces categories by the mean value of the target for each
     category.
@@ -158,9 +157,10 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
     ) -> None:
         super().__init__(variables, ignore_format)
         if (
-            (isinstance(smoothing, str) and (smoothing != 'auto')) or
-            (isinstance(smoothing, (float, int)) and smoothing < 0)
-        ):
+            not isinstance(smoothing, (str, float, int))
+            or isinstance(smoothing, str)
+            and (smoothing != "auto")
+        ) or (isinstance(smoothing, (float, int)) and smoothing < 0):
             raise ValueError(
                 f"smoothing must be greater than 0 or 'auto'. "
                 f"Got {smoothing} instead."
@@ -190,9 +190,9 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
         self.encoder_dict_ = {}
 
         y_prior = y.mean()
-        dct_init = defaultdict(
-            lambda: y_prior
-        ) if self.unseen == "encode" else {}  # type: Union[dict, defaultdict]
+
+        if self.unseen == "encode":
+            self._unseen = y_prior
 
         if self.smoothing == "auto":
             y_var = y.var(ddof=0)
@@ -204,14 +204,35 @@ class MeanEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
             counts = X[var].value_counts()
             _lambda = counts / (counts + damping)
             self.encoder_dict_[var] = (
-                _lambda * y.groupby(X[var]).mean() +
-                (1. - _lambda) * y_prior
-            ).to_dict(dct_init)
-
-        self._check_encoding_dictionary()
+                _lambda * y.groupby(X[var]).mean() + (1.0 - _lambda) * y_prior
+            ).to_dict()
 
         return self
 
+    def inverse_transform(self, X: pd.DataFrame) -> pd.DataFrame:
+        """Convert the encoded variable back to the original values.
+
+        Note that if unseen was set to 'encode', then this method is not implemented.
+
+        Parameters
+        ----------
+        X: pandas dataframe of shape = [n_samples, n_features].
+            The transformed dataframe.
+
+        Returns
+        -------
+        X_tr: pandas dataframe of shape = [n_samples, n_features].
+            The un-transformed dataframe, with the categorical variables containing the
+            original values.
+        """
+
+        if self.unseen == "encode":
+            raise NotImplementedError(
+                "inverse_transform is not implemented for this transformer."
+            )
+        else:
+            return super().inverse_transform(X)
+
     def _more_tags(self):
         tags_dict = super()._more_tags()
         tags_dict["requires_y"] = True
diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py
index f5fe5d0..cc3bcd5 100644
--- a/feature_engine/encoding/one_hot.py
+++ b/feature_engine/encoding/one_hot.py
@@ -223,8 +223,6 @@ class OneHotEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
                 category = X[var].unique()[0]
                 self.encoder_dict_[var] = [category]
 
-        self._check_encoding_dictionary()
-
         return self
 
     def transform(self, X: pd.DataFrame) -> pd.DataFrame:
@@ -251,9 +249,7 @@ class OneHotEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
 
         for feature in self.variables_:
             for category in self.encoder_dict_[feature]:
-                X[f"{feature}_{category}"] = np.where(
-                    X[feature] == category, 1, 0
-                )
+                X[f"{feature}_{category}"] = np.where(X[feature] == category, 1, 0)
 
         # drop the original non-encoded variables.
         X.drop(labels=self.variables_, axis=1, inplace=True)
@@ -262,7 +258,9 @@ class OneHotEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
 
     def inverse_transform(self, X: pd.DataFrame):
         """inverse_transform is not implemented for this transformer."""
-        return self
+        raise NotImplementedError(
+            "inverse_transform is not implemented for this transformer."
+        )
 
     def _get_new_features_name(self) -> List:
         """Return names of the created features."""
diff --git a/feature_engine/encoding/ordinal.py b/feature_engine/encoding/ordinal.py
index 6f508bf..c767d4f 100644
--- a/feature_engine/encoding/ordinal.py
+++ b/feature_engine/encoding/ordinal.py
@@ -21,12 +21,17 @@ from feature_engine._docstrings.methods import (
     _transform_encoders_docstring,
 )
 from feature_engine._docstrings.substitute import Substitution
+from feature_engine.encoding._helper_functions import check_parameter_unseen
 from feature_engine.dataframe_checks import check_X, check_X_y
 from feature_engine.encoding.base_encoder import (
-    CategoricalInitExpandedMixin,
+    CategoricalInitMixin,
     CategoricalMethodsMixin,
 )
 
+_unseen_docstring = (
+    _unseen_docstring + """ If `'encode'`, unseen categories will be encoded as -1."""
+)
+
 
 @Substitution(
     ignore_format=_ignore_format_docstring,
@@ -39,7 +44,7 @@ from feature_engine.encoding.base_encoder import (
     transform=_transform_encoders_docstring,
     inverse_transform=_inverse_transform_docstring,
 )
-class OrdinalEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
+class OrdinalEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
     """
     The OrdinalEncoder() replaces categories by ordinal numbers
     (0, 1, 2, 3, etc). The numbers can be ordered based on the mean of the target
@@ -133,9 +138,10 @@ class OrdinalEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
                 "encoding_method takes only values 'ordered' and 'arbitrary'"
             )
 
-        super().__init__(variables, ignore_format, unseen)
-
+        check_parameter_unseen(unseen, ["ignore", "raise", "encode"])
+        super().__init__(variables, ignore_format)
         self.encoding_method = encoding_method
+        self.unseen = unseen
 
     def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
         """Learn the numbers to be used to replace the categories in each
@@ -174,6 +180,7 @@ class OrdinalEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
 
             self.encoder_dict_[var] = {k: i for i, k in enumerate(t, 0)}
 
-        self._check_encoding_dictionary()
+        if self.unseen == "encode":
+            self._unseen = -1
 
         return self
diff --git a/feature_engine/encoding/probability_ratio.py b/feature_engine/encoding/probability_ratio.py
index 79c61d0..8341431 100644
--- a/feature_engine/encoding/probability_ratio.py
+++ b/feature_engine/encoding/probability_ratio.py
@@ -5,6 +5,7 @@ from typing import List, Union
 
 import numpy as np
 import pandas as pd
+from sklearn.utils import deprecated
 
 from feature_engine._docstrings.fit_attributes import (
     _feature_names_in_docstring,
@@ -22,9 +23,10 @@ from feature_engine._docstrings.methods import (
     _transform_encoders_docstring,
 )
 from feature_engine._docstrings.substitute import Substitution
+from feature_engine.encoding._helper_functions import check_parameter_unseen
 from feature_engine.dataframe_checks import check_X_y
 from feature_engine.encoding.base_encoder import (
-    CategoricalInitExpandedMixin,
+    CategoricalInitMixin,
     CategoricalMethodsMixin,
 )
 from feature_engine.tags import _return_tags
@@ -41,7 +43,11 @@ from feature_engine.tags import _return_tags
     transform=_transform_encoders_docstring,
     inverse_transform=_inverse_transform_docstring,
 )
-class PRatioEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
+@deprecated(
+    "PRatioEncoder() is deprecated in version 1.5 and will be removed in "
+    "version 1.6. Use WoEEncoder() instead for a similar approach."
+)
+class PRatioEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
     """
     The PRatioEncoder() replaces categories by the ratio of the probability of the
     target = 1 and the probability of the target = 0.
@@ -140,10 +146,10 @@ class PRatioEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
             raise ValueError(
                 "encoding_method takes only values 'ratio' and 'log_ratio'"
             )
-
-        super().__init__(variables, ignore_format, unseen)
-
+        super().__init__(variables, ignore_format)
         self.encoding_method = encoding_method
+        check_parameter_unseen(unseen, ["ignore", "raise"])
+        self.unseen = unseen
 
     def fit(self, X: pd.DataFrame, y: pd.Series):
         """
@@ -207,8 +213,6 @@ class PRatioEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
                 else:
                     self.encoder_dict_[var] = (t.p1 / t.p0).to_dict()
 
-        self._check_encoding_dictionary()
-
         return self
 
     def _more_tags(self):
diff --git a/feature_engine/encoding/rare_label.py b/feature_engine/encoding/rare_label.py
index afc040b..577fce6 100644
--- a/feature_engine/encoding/rare_label.py
+++ b/feature_engine/encoding/rare_label.py
@@ -128,7 +128,11 @@ class RareLabelEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
             raise ValueError("n_categories takes only positive integer numbers")
 
         if max_n_categories is not None:
-            if max_n_categories < 0 or not isinstance(max_n_categories, int):
+            if (
+                not isinstance(max_n_categories, int)
+                or isinstance(max_n_categories, int)
+                and max_n_categories < 0
+            ):
                 raise ValueError("max_n_categories takes only positive integer numbers")
 
         super().__init__(variables, ignore_format)
@@ -182,8 +186,6 @@ class RareLabelEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
                 )
                 self.encoder_dict_[var] = X[var].unique()
 
-        self._check_encoding_dictionary()
-
         return self
 
     def transform(self, X: pd.DataFrame) -> pd.DataFrame:
@@ -218,4 +220,6 @@ class RareLabelEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
 
     def inverse_transform(self, X: pd.DataFrame):
         """inverse_transform is not implemented for this transformer."""
-        return self
+        raise NotImplementedError(
+            "inverse_transform is not implemented for this transformer."
+        )
diff --git a/feature_engine/encoding/similarity_encoder.py b/feature_engine/encoding/similarity_encoder.py
new file mode 100644
index 0000000..01283e8
--- /dev/null
+++ b/feature_engine/encoding/similarity_encoder.py
@@ -0,0 +1,301 @@
+from difflib import SequenceMatcher
+from typing import List, Optional, Union
+
+import numpy as np
+import pandas as pd
+from sklearn.utils.validation import check_is_fitted
+
+from feature_engine._docstrings.fit_attributes import (
+    _feature_names_in_docstring,
+    _n_features_in_docstring,
+    _variables_attribute_docstring,
+)
+from feature_engine._docstrings.init_parameters import (
+    _ignore_format_docstring,
+    _variables_categorical_docstring,
+)
+from feature_engine._docstrings.methods import _fit_transform_docstring
+from feature_engine._docstrings.substitute import Substitution
+from feature_engine.dataframe_checks import _check_contains_na, check_X
+from feature_engine.encoding.base_encoder import (
+    CategoricalInitMixin,
+    CategoricalMethodsMixin,
+)
+
+
+def _gpm_fast(x1: str, x2: str) -> float:
+    return SequenceMatcher(None, str(x1), str(x2)).quick_ratio()
+
+
+_gpm_fast_vec = np.vectorize(_gpm_fast)
+
+
+@Substitution(
+    ignore_format=_ignore_format_docstring,
+    variables=_variables_categorical_docstring,
+    variables_=_variables_attribute_docstring,
+    feature_names_in_=_feature_names_in_docstring,
+    n_features_in_=_n_features_in_docstring,
+    fit_transform=_fit_transform_docstring,
+)
+class StringSimilarityEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
+    """
+    The StringSimilarityEncoder() replaces categorical variables with a set of float
+    variables that capture the similarity between the category names. The new variables
+    have values between 0 and 1, where 0 indicates no similarity and 1 is an exact
+    match between the names of the categories.
+
+    The similarity measure is a float in the range [0, 1]. It is defined as 2 * M / T,
+    where T is the total number of elements in both categories being compared, and M is
+    the number of matches. Note that this is 1 if the sequences are identical, and 0 if
+    they have nothing in common.
+
+    For example, the similarity between the categories "dog" and "dig" is 0.66. T is the
+    total number of elements in both categories, that is 6. There are 2 matches between
+    the words, the letters d and g, so: 2 * M / T = 2 * 2 / 6 = 0.66.
+
+    This encoding is similar to one-hot encoding, in the sense that each category is
+    encoded as a new variable. But the values, instead of 1 or 0, are the similarity
+    between the observation's category and the dummy variable.
+
+    For example, if a variable has 3 categories, dog, dig and cat,
+    StringSimilarityEncoder() will create 3 new variables, var_dog, var_dig and var_cat
+    and the values would be for the observation dog: 1, 0.66 , 0. For the observation
+    dig they would be 0.66, 1, 0. And for cat, they would be 0, 0, 1.
+
+    The encoder has the option to generate similarity variables only  for the most
+    popular categories, that is, the categories present in most observations. This
+    behaviour can be specified with the parameter `top_categories`.
+
+    **Missing values**
+
+    StringSimilarityEncoder() will rreplace missing data with an empty string and
+    then return the similarity to the remaining variables by default. Alternatively,
+    it can be set to return an error if the variable has missing values, or to ignore
+    them.
+
+    **Unseen categories**
+
+    StringSimilarityEncoder() handles unseen categories out-of-the-box by assigning a
+    similarity measure to the other categories that were seen during `fit()`.
+
+    **Categorical variables**
+
+    The encoder will encode only categorical variables by default (type 'object' or
+    'categorical'). You can pass a list of variables to encode. Alternatively, the
+    encoder will find and encode all categorical variables.
+
+    **Numerical variables**
+
+    With `ignore_format=True` you have the option to encode numerical variables as well.
+    Encoding numerical variables with similarity measures make sense for example for
+    variables like barcodes. In this case, you can either enter the list of variables
+    to encode (recommended), or the transformer will automatically select all variables.
+
+    More details in the :ref:`User Guide <string_similarity>`.
+
+    Parameters
+    ----------
+    top_categories: int, default=None
+        If None, dummy variables will be created for each unique category of the
+        variable. Alternatively, we can indicate in the number of most frequent
+        categories to encode. In this case, similarity variables will be created
+        only for those popular categories.
+
+    missing_values : str, default='impute'
+        Indicates if missing values should be ignored, raised or imputed. If 'raise' the
+        transformer will return an error if the datasets to `fit` or `transform`
+        contain missing values. If 'ignore', missing data will be ignored when learning
+        parameters or performing the transformation. If 'impute', the transformer will
+        replace missing values with an empty string, '', and then return the similarity
+        measures.
+
+    {variables}
+
+    {ignore_format}
+
+    Attributes
+    ----------
+    encoder_dict_:
+        Dictionary with the categories for which dummy variables will be created.
+
+    {variables_}
+
+    {feature_names_in_}
+
+    {n_features_in_}
+
+    Methods
+    -------
+    fit:
+        Learn the unique categories per variable.
+
+    {fit_transform}
+
+    transform:
+        Replace the categorical variables by the distance variables.
+
+    Notes
+    -----
+    This encoder will encode unseen categories by measuring string similarity between
+    seen and unseen categories.
+
+    No text preprocessing is applied before calculating the similarity.
+
+    The original categorical variables are removed from the returned dataset after the
+    transformation. In their place, the binary variables are returned.
+
+    See Also
+    --------
+    feature_engine.encoding.OneHotEncoder
+    dirty_cat.SimilarityEncoder
+
+    References
+    ----------
+    .. [1] Cerda P, Varoquaux G, Kégl B. "Similarity encoding for learning with dirty
+       categorical variables". Machine Learning, Springer Verlag, 2018.
+    .. [2] Cerda P, Varoquaux G. "Encoding high-cardinality string categorical
+       variables". IEEE Transactions on Knowledge & Data Engineering, 2020.
+    """
+
+    def __init__(
+        self,
+        top_categories: Union[None, int] = None,
+        missing_values: str = "impute",
+        variables: Union[None, int, str, List[Union[str, int]]] = None,
+        ignore_format: bool = False,
+    ):
+        if top_categories and not isinstance(top_categories, int):
+            raise ValueError(
+                f"top_categories takes only integers. Got {top_categories!r} instead."
+            )
+        if missing_values not in ("raise", "impute", "ignore"):
+            raise ValueError(
+                "missing_values should be one of 'raise', 'impute' or 'ignore'."
+                f" Got {missing_values!r} instead."
+            )
+        super().__init__(variables, ignore_format)
+        self.top_categories = top_categories
+        self.missing_values = missing_values
+
+    def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
+        """
+        Learns the unique categories per variable. If top_categories is indicated,
+        it will learn the most popular categories. Alternatively, it learns all
+        unique categories per variable.
+
+        Parameters
+        ----------
+
+        X: pandas dataframe of shape = [n_samples, n_features]
+            The training input samples.
+            Can be the entire dataframe, not just the variables to encode.
+
+        y: pandas series, default=None
+            Target. It is not needed in this encoded. You can pass y or None.
+        """
+
+        X = check_X(X)
+        self._check_or_select_variables(X)
+        self._get_feature_names_in(X)
+        self.encoder_dict_ = {}
+
+        if self.missing_values == "raise":
+            _check_contains_na(X, self.variables_)
+            for var in self.variables_:
+                self.encoder_dict_[var] = (
+                    X[var]
+                    .astype(str)
+                    .value_counts()
+                    .head(self.top_categories)
+                    .index.tolist()
+                )
+        elif self.missing_values == "impute":
+            for var in self.variables_:
+                self.encoder_dict_[var] = (
+                    X[var]
+                    .astype(str)
+                    .replace("nan", "")
+                    .value_counts()
+                    .head(self.top_categories)
+                    .index.tolist()
+                )
+        elif self.missing_values == "ignore":
+            for var in self.variables_:
+                self.encoder_dict_[var] = (
+                    X[var]
+                    .astype(str)
+                    .value_counts(dropna=True)
+                    .head(self.top_categories)
+                    .index.tolist()
+                )
+
+        self._check_encoding_dictionary()
+
+        return self
+
+    def transform(self, X: pd.DataFrame) -> pd.DataFrame:
+        """
+        Replaces the categorical variables with the similarity variables.
+
+        Parameters
+        ----------
+        X: pandas dataframe of shape = [n_samples, n_features]
+            The data to transform.
+
+        Returns
+        -------
+        X_new: pandas dataframe.
+            The transformed dataframe. The shape of the dataframe will be different from
+            the original as it includes the similarity variables in place of the
+            original categorical ones.
+        """
+
+        check_is_fitted(self)
+        X = self._check_transform_input_and_state(X)
+        if self.missing_values == "raise":
+            _check_contains_na(X, self.variables_)
+
+        new_values = []
+        for var in self.variables_:
+            if self.missing_values == "impute":
+                X[var] = X[var].astype(str).replace("nan", "")
+            categories = X[var].dropna().astype(str).unique()
+            column_encoder_dict = {
+                x: _gpm_fast_vec(x, self.encoder_dict_[var]) for x in categories
+            }
+            column_encoder_dict["nan"] = [np.nan] * len(self.encoder_dict_[var])
+            encoded = np.vstack(X[var].astype(str).map(column_encoder_dict).values)
+            if self.missing_values == "ignore":
+                encoded[X[var].isna(), :] = np.nan
+            new_values.append(encoded)
+
+        new_features = self._get_new_features_name()
+        X.loc[:, new_features] = np.hstack(new_values)
+
+        return X.drop(self.variables_, axis=1)
+
+    def _get_new_features_name(self) -> List:
+        """Return names of the created features."""
+        feature_names = []
+        for feature in self.variables_:
+            for category in self.encoder_dict_[feature]:
+                if category == "":
+                    feature_names.append(f"{feature}_nan")
+                else:
+                    feature_names.append(f"{feature}_{category}")
+
+        return feature_names
+
+    def _add_new_feature_names(self, feature_names) -> List:
+        """Creates new features names and removes original categorical variables."""
+        feature_names = feature_names + self._get_new_features_name()
+        feature_names = [f for f in feature_names if f not in self.variables_]
+
+        return feature_names
+
+    def inverse_transform(self, X: pd.DataFrame):
+        """inverse_transform is not implemented for this transformer."""
+        raise NotImplementedError(
+            "inverse_transform is not implemented for this transformer."
+        )
diff --git a/feature_engine/encoding/woe.py b/feature_engine/encoding/woe.py
index 5204c07..7079578 100644
--- a/feature_engine/encoding/woe.py
+++ b/feature_engine/encoding/woe.py
@@ -22,9 +22,10 @@ from feature_engine._docstrings.methods import (
     _transform_encoders_docstring,
 )
 from feature_engine._docstrings.substitute import Substitution
+from feature_engine.encoding._helper_functions import check_parameter_unseen
 from feature_engine.dataframe_checks import check_X_y
 from feature_engine.encoding.base_encoder import (
-    CategoricalInitExpandedMixin,
+    CategoricalInitMixin,
     CategoricalMethodsMixin,
 )
 from feature_engine.tags import _return_tags
@@ -41,7 +42,7 @@ from feature_engine.tags import _return_tags
     transform=_transform_encoders_docstring,
     inverse_transform=_inverse_transform_docstring,
 )
-class WoEEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
+class WoEEncoder(CategoricalInitMixin, CategoricalMethodsMixin):
     """
     The WoEEncoder() replaces categories by the weight of evidence
     (WoE). The WoE was used primarily in the financial sector to create credit risk
@@ -126,7 +127,9 @@ class WoEEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
         unseen: str = "ignore",
     ) -> None:
 
-        super().__init__(variables, ignore_format, unseen)
+        super().__init__(variables, ignore_format)
+        check_parameter_unseen(unseen, ["ignore", "raise"])
+        self.unseen = unseen
 
     def fit(self, X: pd.DataFrame, y: pd.Series):
         """
@@ -179,8 +182,6 @@ class WoEEncoder(CategoricalInitExpandedMixin, CategoricalMethodsMixin):
 
             self.encoder_dict_[var] = woe.to_dict()
 
-        self._check_encoding_dictionary()
-
         return self
 
     def _more_tags(self):


Test patch:
diff --git a/tests/test_encoding/test_base_encoder.py b/tests/test_encoding/test_base_encoder.py
new file mode 100644
index 0000000..c613e25
--- /dev/null
+++ b/tests/test_encoding/test_base_encoder.py
@@ -0,0 +1,105 @@
+import numpy as np
+import pandas as pd
+import pytest
+
+from feature_engine.encoding.base_encoder import (
+    CategoricalInitMixin,
+    CategoricalMethodsMixin,
+)
+
+
+@pytest.mark.parametrize("param", [1, "hola", [1, 2, 0], (True, False)])
+def test_categorical_init_mixin_raises_error(param):
+    with pytest.raises(ValueError):
+        CategoricalInitMixin(ignore_format=param)
+
+
+class MockClass(CategoricalMethodsMixin):
+    def __init__(self, unseen=None):
+        self.encoder_dict_ = {"words": {"dog": 1, "dig": 0.66, "cat": 0}}
+        self.n_features_in_ = 1
+        self.feature_names_in_ = ["words"]
+        self.variables_ = ["words"]
+        self.unseen = unseen
+        self._unseen = -1
+
+    def fit(self):
+        return self
+
+
+def test_categorical_methods_mixin_transform_no_unseen():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "cat"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, 0]})
+    enc = MockClass()
+    pd.testing.assert_frame_equal(enc.transform(input_df), output_df)
+
+
+def test_categorical_methods_mixin_transform_ignore_unseen():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "bird"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, np.nan]})
+    enc = MockClass(unseen="ignore")
+    pd.testing.assert_frame_equal(enc.transform(input_df), output_df)
+
+
+def test_categorical_methods_mixin_transform_raise_unseen():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "bird"]})
+    enc = MockClass(unseen="raise")
+    with pytest.raises(ValueError):
+        enc.transform(input_df)
+
+
+def test_categorical_methods_mixin_transform_encode_unseen():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "bird"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, -1]})
+    enc = MockClass(unseen="encode")
+    pd.testing.assert_frame_equal(enc.transform(input_df), output_df)
+
+
+def test_categorical_methods_mixin_raises_error_when_nan_introduced():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "bird"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, np.nan]})
+    enc = MockClass(unseen="raise")
+    msg = "During the encoding, NaN values were introduced in the feature(s) words."
+
+    with pytest.raises(ValueError) as record:
+        enc._check_nan_values_after_transformation(output_df)
+    assert str(record.value) == msg
+
+    with pytest.raises(ValueError) as record:
+        enc.transform(input_df)
+    assert str(record.value) == msg
+
+
+def test_categorical_methods_mixin_raises_warning_when_nan_introduced():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "bird"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, np.nan]})
+    enc = MockClass(unseen="ignore")
+    msg = "During the encoding, NaN values were introduced in the feature(s) words."
+
+    with pytest.warns(UserWarning) as record:
+        enc.transform(input_df)
+    assert record[0].message.args[0] == msg
+
+    with pytest.warns(UserWarning) as record:
+        enc._check_nan_values_after_transformation(output_df)
+    assert record[0].message.args[0] == msg
+
+
+def test_categorical_methods_mixin_inverse_transform_no_unseen_categories():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "cat"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, 0]})
+
+    # when no unseen categories
+    enc = MockClass()
+    pd.testing.assert_frame_equal(enc.inverse_transform(output_df), input_df)
+
+
+def test_categorical_methods_mixin_inverse_transform_when_ignore_unseen():
+    input_df = pd.DataFrame({"words": ["dog", "dig", "bird"]})
+    output_df = pd.DataFrame({"words": [1, 0.66, np.nan]})
+    inverse_df = pd.DataFrame({"words": ["dog", "dig", np.nan]})
+
+    # when no unseen categories
+    enc = MockClass(unseen="ignore")
+    pd.testing.assert_frame_equal(enc.transform(input_df), output_df)
+    pd.testing.assert_frame_equal(enc.inverse_transform(output_df), inverse_df)
diff --git a/tests/test_encoding/test_check_estimator_encoders.py b/tests/test_encoding/test_check_estimator_encoders.py
index 0111916..8f9c64b 100644
--- a/tests/test_encoding/test_check_estimator_encoders.py
+++ b/tests/test_encoding/test_check_estimator_encoders.py
@@ -11,6 +11,7 @@ from feature_engine.encoding import (
     PRatioEncoder,
     RareLabelEncoder,
     WoEEncoder,
+    StringSimilarityEncoder
 )
 from tests.estimator_checks.estimator_checks import check_feature_engine_estimator
 
@@ -28,6 +29,7 @@ _estimators = [
     ),
     WoEEncoder(ignore_format=True),
     PRatioEncoder(ignore_format=True),
+    StringSimilarityEncoder(ignore_format=True),
 ]
 
 
@@ -45,6 +47,7 @@ _estimators = [
     RareLabelEncoder(),
     WoEEncoder(),
     PRatioEncoder(),
+    StringSimilarityEncoder(),
 ]
 
 
diff --git a/tests/test_encoding/test_count_frequency_encoder.py b/tests/test_encoding/test_count_frequency_encoder.py
index 62071c0..6d2280c 100644
--- a/tests/test_encoding/test_count_frequency_encoder.py
+++ b/tests/test_encoding/test_count_frequency_encoder.py
@@ -1,5 +1,6 @@
 import warnings
 
+import numpy as np
 import pandas as pd
 import pytest
 from numpy import nan
@@ -363,3 +364,31 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
 def test_exception_if_unseen_gets_not_permitted_value(errors):
     with pytest.raises(ValueError):
         CountFrequencyEncoder(unseen=errors)
+
+
+def test_inverse_transform_when_no_unseen():
+    df = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    enc = CountFrequencyEncoder()
+    enc.fit(df)
+    dft = enc.transform(df)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df)
+
+
+def test_inverse_transform_when_ignore_unseen():
+    df1 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    df2 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]})
+    df3 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", np.nan]})
+    enc = CountFrequencyEncoder(unseen="ignore")
+    enc.fit(df1)
+    dft = enc.transform(df2)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df3)
+
+
+def test_inverse_transform_when_encode_unseen():
+    df1 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    df2 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]})
+    df3 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", np.nan]})
+    enc = CountFrequencyEncoder(unseen="encode")
+    enc.fit(df1)
+    dft = enc.transform(df2)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df3)
diff --git a/tests/test_encoding/test_decision_tree_encoder.py b/tests/test_encoding/test_decision_tree_encoder.py
index a92b7b7..70dd20a 100644
--- a/tests/test_encoding/test_decision_tree_encoder.py
+++ b/tests/test_encoding/test_decision_tree_encoder.py
@@ -131,3 +131,11 @@ def test_error_when_regression_is_false_and_target_is_continuous(df_enc):
     with pytest.raises(ValueError):
         encoder = DecisionTreeEncoder(regression=False)
         encoder.fit(df_enc[["var_A", "var_B"]], y)
+
+
+def test_inverse_transform_raises_not_implemented_error(df_enc):
+    random = np.random.RandomState(42)
+    y = random.normal(0, 10, len(df_enc))
+    encoder = DecisionTreeEncoder(regression=True).fit(df_enc[["var_A", "var_B"]], y)
+    with pytest.raises(NotImplementedError):
+        encoder.inverse_transform(df_enc[["var_A", "var_B"]])
diff --git a/tests/test_encoding/test_mean_encoder.py b/tests/test_encoding/test_mean_encoder.py
index edb9e4e..ab6abf1 100644
--- a/tests/test_encoding/test_mean_encoder.py
+++ b/tests/test_encoding/test_mean_encoder.py
@@ -1,3 +1,4 @@
+import numpy as np
 import pandas as pd
 import pytest
 
@@ -300,17 +301,21 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
 
 
 def test_auto_smoothing(df_enc):
-    encoder = MeanEncoder(smoothing='auto')
+    encoder = MeanEncoder(smoothing="auto")
     encoder.fit(df_enc[["var_A", "var_B"]], df_enc["target"])
     X = encoder.transform(df_enc[["var_A", "var_B"]])
 
     # expected output
     transf_df = df_enc.copy()
     var_A_dict = {
-        'A': 0.328335832083958, 'B': 0.20707964601769913, 'C': 0.4541284403669725
+        "A": 0.328335832083958,
+        "B": 0.20707964601769913,
+        "C": 0.4541284403669725,
     }
     var_B_dict = {
-        'A': 0.20707964601769913, 'B': 0.328335832083958, 'C': 0.4541284403669725
+        "A": 0.20707964601769913,
+        "B": 0.328335832083958,
+        "C": 0.4541284403669725,
     }
     transf_df["var_A"] = transf_df["var_A"].map(var_A_dict)
     transf_df["var_B"] = transf_df["var_B"].map(var_B_dict)
@@ -336,10 +341,14 @@ def test_value_smoothing(df_enc):
     # expected output
     transf_df = df_enc.copy()
     var_A_dict = {
-        'A': 0.3018867924528302, 'B': 0.2909090909090909, 'C': 0.30769230769230765
+        "A": 0.3018867924528302,
+        "B": 0.2909090909090909,
+        "C": 0.30769230769230765,
     }
     var_B_dict = {
-        'A': 0.2909090909090909, 'B': 0.3018867924528302, 'C': 0.30769230769230765
+        "A": 0.2909090909090909,
+        "B": 0.3018867924528302,
+        "C": 0.30769230769230765,
     }
     transf_df["var_A"] = transf_df["var_A"].map(var_A_dict)
     transf_df["var_B"] = transf_df["var_B"].map(var_B_dict)
@@ -368,3 +377,40 @@ def test_encoding_new_categories(df_enc):
     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)
+
+
+def test_inverse_transform_when_no_unseen():
+    df = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    y = [1, 0, 1, 0, 1, 0]
+    enc = MeanEncoder()
+    enc.fit(df, y)
+    dft = enc.transform(df)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df)
+
+
+def test_inverse_transform_when_ignore_unseen():
+    df1 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    df2 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]})
+    df3 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", np.nan]})
+    y = [1, 0, 1, 0, 1, 0]
+    enc = MeanEncoder(unseen="ignore")
+    enc.fit(df1, y)
+    dft = enc.transform(df2)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df3)
+
+
+def test_inverse_transform_when_encode_unseen():
+    df1 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    df2 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]})
+    y = [1, 0, 1, 0, 1, 0]
+    enc = MeanEncoder(unseen="encode")
+    enc.fit(df1, y)
+    dft = enc.transform(df2)
+    with pytest.raises(NotImplementedError):
+        enc.inverse_transform(dft)
+
+
+@pytest.mark.parametrize("smoothing", ["hello", ["auto"], -1])
+def test_raises_error_when_not_allowed_smoothing_param_in_init(smoothing):
+    with pytest.raises(ValueError):
+        MeanEncoder(smoothing=smoothing)
diff --git a/tests/test_encoding/test_onehot_encoder.py b/tests/test_encoding/test_onehot_encoder.py
index e227bc4..e3c8725 100644
--- a/tests/test_encoding/test_onehot_encoder.py
+++ b/tests/test_encoding/test_onehot_encoder.py
@@ -493,3 +493,15 @@ def test_get_feature_names_out_from_pipeline(df_enc_binary):
 
     assert tr.get_feature_names_out(input_features=None) == feat_out
     assert tr.get_feature_names_out(input_features=input_features) == feat_out
+
+
+@pytest.mark.parametrize("drop_binary", ["hello", ["auto"], -1, 100, 0.5])
+def test_raises_error_when_not_allowed_smoothing_param_in_init(drop_binary):
+    with pytest.raises(ValueError):
+        OneHotEncoder(drop_last_binary=drop_binary)
+
+
+def test_inverse_transform_raises_not_implemented_error(df_enc_binary):
+    enc = OneHotEncoder().fit(df_enc_binary)
+    with pytest.raises(NotImplementedError):
+        enc.inverse_transform(df_enc_binary)
diff --git a/tests/test_encoding/test_ordinal_encoder.py b/tests/test_encoding/test_ordinal_encoder.py
index 48e39ff..9ebf0af 100644
--- a/tests/test_encoding/test_ordinal_encoder.py
+++ b/tests/test_encoding/test_ordinal_encoder.py
@@ -1,3 +1,4 @@
+import numpy as np
 import pandas as pd
 import pytest
 
@@ -171,3 +172,39 @@ def test_variables_cast_as_category(df_enc_category_dtypes):
 def test_error_if_rare_labels_not_permitted_value():
     with pytest.raises(ValueError):
         OrdinalEncoder(unseen="empanada")
+
+
+def test_inverse_transform_when_no_unseen():
+    df = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    enc = OrdinalEncoder(encoding_method="arbitrary")
+    enc.fit(df)
+    dft = enc.transform(df)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df)
+
+
+def test_inverse_transform_when_ignore_unseen():
+    df1 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    df2 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]})
+    df3 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", np.nan]})
+    enc = OrdinalEncoder(encoding_method="arbitrary", unseen="ignore")
+    enc.fit(df1)
+    dft = enc.transform(df2)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df3)
+
+
+def test_inverse_transform_when_encode_unseen():
+    df1 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
+    df2 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]})
+    df3 = pd.DataFrame({"words": ["dog", "dog", "cat", "cat", "cat", np.nan]})
+    enc = OrdinalEncoder(encoding_method="arbitrary", unseen="encode")
+    enc.fit(df1)
+    dft = enc.transform(df2)
+    pd.testing.assert_frame_equal(enc.inverse_transform(dft), df3)
+
+
+def test_encoding_new_categories(df_enc):
+    df_unseen = pd.DataFrame({"var_A": ["D"], "var_B": ["D"]})
+    encoder = OrdinalEncoder(encoding_method="arbitrary", unseen="encode")
+    encoder.fit(df_enc[["var_A", "var_B"]])
+    df_transformed = encoder.transform(df_unseen)
+    assert (df_transformed == -1).all(axis=None)
diff --git a/tests/test_encoding/test_probability_ratio_encoder.py b/tests/test_encoding/test_probability_ratio_encoder.py
index b76dfec..f9e73fb 100644
--- a/tests/test_encoding/test_probability_ratio_encoder.py
+++ b/tests/test_encoding/test_probability_ratio_encoder.py
@@ -258,11 +258,6 @@ def test_warn_if_transform_df_contains_categories_not_seen_in_fit(df_enc, df_enc
         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(unseen="raise")
diff --git a/tests/test_encoding/test_rare_label_encoder.py b/tests/test_encoding/test_rare_label_encoder.py
index 96bae1d..0b57029 100644
--- a/tests/test_encoding/test_rare_label_encoder.py
+++ b/tests/test_encoding/test_rare_label_encoder.py
@@ -212,3 +212,15 @@ def test_variables_cast_as_category(df_enc_big):
     assert encoder.n_features_in_ == 3
     # test transform output
     pd.testing.assert_frame_equal(X, df)
+
+
+@pytest.mark.parametrize("max_n_categories", ["hello", ["auto"], -1, 0.5])
+def test_raises_error_when_not_allowed_smoothing_param_in_init(max_n_categories):
+    with pytest.raises(ValueError):
+        RareLabelEncoder(max_n_categories=max_n_categories)
+
+
+def test_inverse_transform_raises_not_implemented_error(df_enc_big):
+    enc = RareLabelEncoder().fit(df_enc_big)
+    with pytest.raises(NotImplementedError):
+        enc.inverse_transform(df_enc_big)
diff --git a/tests/test_encoding/test_similarity_encoder.py b/tests/test_encoding/test_similarity_encoder.py
new file mode 100644
index 0000000..6f88bb6
--- /dev/null
+++ b/tests/test_encoding/test_similarity_encoder.py
@@ -0,0 +1,215 @@
+from difflib import SequenceMatcher
+
+import pandas as pd
+import pytest
+
+from feature_engine.encoding import StringSimilarityEncoder
+from feature_engine.encoding.similarity_encoder import _gpm_fast
+
+
+@pytest.mark.parametrize(
+    "strings", [("hola", "chau"), ("hi there", "hi here"), (100, 1000)]
+)
+def test_gpm_fast(strings):
+    str1, str2 = strings
+    assert SequenceMatcher(None, str(str1), str(str2)).quick_ratio() == _gpm_fast(
+        str1, str2
+    )
+
+
+def test_encode_top_categories():
+    df = pd.DataFrame(
+        {
+            "var_A": ["A"] * 5
+            + ["B"] * 11
+            + ["C"] * 4
+            + ["D"] * 9
+            + ["E"] * 2
+            + ["F"] * 2
+            + ["G"] * 7,
+            "var_B": ["A"] * 11
+            + ["B"] * 7
+            + ["C"] * 4
+            + ["D"] * 9
+            + ["E"] * 2
+            + ["F"] * 2
+            + ["G"] * 5,
+            "var_C": ["A"] * 4
+            + ["B"] * 5
+            + ["C"] * 11
+            + ["D"] * 9
+            + ["E"] * 2
+            + ["F"] * 2
+            + ["G"] * 7,
+        }
+    )
+
+    encoder = StringSimilarityEncoder(top_categories=4)
+    X = encoder.fit_transform(df)
+
+    # test init params
+    assert encoder.top_categories == 4
+    # test fit attr
+    transf = {
+        "var_A_D": 9,
+        "var_A_B": 11,
+        "var_A_A": 5,
+        "var_A_G": 7,
+        "var_B_A": 11,
+        "var_B_D": 9,
+        "var_B_G": 5,
+        "var_B_B": 7,
+        "var_C_D": 9,
+        "var_C_C": 11,
+        "var_C_G": 7,
+        "var_C_B": 5,
+    }
+
+    # test fit attr
+    assert encoder.variables_ == ["var_A", "var_B", "var_C"]
+    assert encoder.n_features_in_ == 3
+    assert encoder.encoder_dict_ == {
+        "var_A": ["B", "D", "G", "A"],
+        "var_B": ["A", "D", "B", "G"],
+        "var_C": ["C", "D", "G", "B"],
+    }
+    # test transform output
+    for col in transf.keys():
+        assert X[col].sum() == transf[col]
+    assert "var_B" not in X.columns
+    assert "var_B_F" not in X.columns
+
+
+@pytest.mark.parametrize("top_cat", ["hello", 0.5, [1]])
+def test_error_if_top_categories_not_integer(top_cat):
+    with pytest.raises(ValueError):
+        StringSimilarityEncoder(top_categories=top_cat)
+
+
+@pytest.mark.parametrize(
+    "handle_missing", ["error", "propagate", ["raise"], 1, 0.1, False]
+)
+def test_error_if_handle_missing_invalid(handle_missing):
+    with pytest.raises(ValueError):
+        StringSimilarityEncoder(missing_values=handle_missing)
+
+
+def test_nan_behaviour_error_fit(df_enc_big_na):
+    encoder = StringSimilarityEncoder(missing_values="raise")
+    with pytest.raises(ValueError):
+        encoder.fit(df_enc_big_na)
+
+
+def test_nan_behaviour_error_transform(df_enc_big, df_enc_big_na):
+    encoder = StringSimilarityEncoder(missing_values="raise")
+    encoder.fit(df_enc_big)
+    with pytest.raises(ValueError):
+        encoder.transform(df_enc_big_na)
+
+
+def test_nan_behaviour_impute(df_enc_big_na):
+    encoder = StringSimilarityEncoder(missing_values="impute")
+    X = encoder.fit_transform(df_enc_big_na)
+    assert (X.isna().sum() == 0).all(axis=None)
+
+
+def test_nan_behaviour_ignore(df_enc_big_na):
+    encoder = StringSimilarityEncoder(missing_values="ignore")
+    X = encoder.fit_transform(df_enc_big_na)
+    assert (X.isna().any(1) == df_enc_big_na.isna().any(1)).all()
+
+
+def test_inverse_transform_error(df_enc_big):
+    encoder = StringSimilarityEncoder()
+    X = encoder.fit_transform(df_enc_big)
+    with pytest.raises(NotImplementedError):
+        encoder.inverse_transform(X)
+
+
+def test_get_feature_names_out(df_enc_big):
+    input_features = df_enc_big.columns.tolist()
+
+    tr = StringSimilarityEncoder()
+    tr.fit(df_enc_big)
+
+    # sort by popularity within variable
+    out = [
+        "var_A_B",
+        "var_A_D",
+        "var_A_A",
+        "var_A_G",
+        "var_A_C",
+        "var_A_E",
+        "var_A_F",
+        "var_B_A",
+        "var_B_D",
+        "var_B_B",
+        "var_B_G",
+        "var_B_C",
+        "var_B_E",
+        "var_B_F",
+        "var_C_C",
+        "var_C_D",
+        "var_C_B",
+        "var_C_G",
+        "var_C_A",
+        "var_C_E",
+        "var_C_F",
+    ]
+
+    assert tr.get_feature_names_out(input_features=None) == out
+    assert tr.get_feature_names_out(input_features=input_features) == out
+
+    tr = StringSimilarityEncoder(top_categories=1)
+    tr.fit(df_enc_big)
+
+    out = ["var_A_B", "var_B_A", "var_C_C"]
+
+    assert tr.get_feature_names_out(input_features=None) == out
+    assert tr.get_feature_names_out(input_features=input_features) == out
+
+    with pytest.raises(ValueError):
+        tr.get_feature_names_out("var_A")
+
+    with pytest.raises(ValueError):
+        tr.get_feature_names_out(["var_A", "hola"])
+
+
+def test_get_feature_names_out_na(df_enc_big_na):
+    input_features = df_enc_big_na.columns.tolist()
+
+    tr = StringSimilarityEncoder()
+    tr.fit(df_enc_big_na)
+
+    out = [
+        "var_A_B",
+        "var_A_D",
+        "var_A_G",
+        "var_A_A",
+        "var_A_C",
+        "var_A_E",
+        "var_A_F",
+        "var_A_nan",
+        "var_B_A",
+        "var_B_D",
+        "var_B_B",
+        "var_B_G",
+        "var_B_C",
+        "var_B_E",
+        "var_B_F",
+        "var_C_C",
+        "var_C_D",
+        "var_C_B",
+        "var_C_G",
+        "var_C_A",
+        "var_C_E",
+        "var_C_F",
+    ]
+
+    assert tr.encoder_dict_ == {
+        "var_A": ["B", "D", "G", "A", "C", "E", "F", ""],
+        "var_B": ["A", "D", "B", "G", "C", "E", "F"],
+        "var_C": ["C", "D", "B", "G", "A", "E", "F"],
+    }
+    assert tr.get_feature_names_out(input_features=None) == out
+    assert tr.get_feature_names_out(input_features=input_features) == out