Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/user_guide/encoding/OrdinalEncoder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,62 @@ might otherwise go unnoticed.
The power of ordinal ordered encoder resides in its intrinsic capacity of finding monotonic relationships.


With polars
~~~~~~~~~~~

:class:`OrdinalEncoder()` works the same way with a polars dataframe. Let's create a toy dataset:

.. code:: python

import polars as pl
from feature_engine.encoding import OrdinalEncoder

X = pl.DataFrame({
"city": ["London", "Manchester", "Liverpool", "London", "Manchester", "Liverpool"],
"price": [500, 300, 250, 520, 310, 260],
})
y = pl.Series("target", [1, 0, 0, 1, 0, 1])

Let's set up :class:`OrdinalEncoder()` to encode `city` with ordered ordinal encoding, and fit it to the data:

.. code:: python

encoder = OrdinalEncoder(encoding_method="ordered", variables=["city"])
encoder.fit(X, y)

encoder.encoder_dict_

We see the resulting mappings from category to integer:

.. code:: python

{'city': {'Manchester': 0, 'Liverpool': 1, 'London': 2}}

Now let's transform the data:

.. code:: python

encoder.transform(X)

We obtain a polars dataframe with the categories in `city` replaced by their ordinal number:

.. code:: text

shape: (6, 2)
┌──────┬───────┐
│ city ┆ price │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞══════╪═══════╡
│ 2 ┆ 500 │
│ 0 ┆ 300 │
│ 1 ┆ 250 │
│ 2 ┆ 520 │
│ 0 ┆ 310 │
│ 1 ┆ 260 │
└──────┴───────┘


Additional resources
--------------------

Expand Down
69 changes: 47 additions & 22 deletions feature_engine/encoding/ordinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

from typing import List, Optional, Union

import pandas as pd
import narwhals as nw
import narwhals.dependencies as nwd
from narwhals.typing import IntoDataFrame, IntoSeries

from feature_engine._check_init_parameters.check_init_input_params import (
_check_return_empty_is_bool,
Expand All @@ -29,7 +31,11 @@
)
from feature_engine._docstrings.substitute import Substitution
from feature_engine.dataframe_checks import check_X, check_X_y
from feature_engine.encoding._helper_functions import check_parameter_unseen
from feature_engine.encoding._helper_functions import (
TARGET_NAME,
add_target_to_X,
check_parameter_unseen,
)
from feature_engine.encoding.base_encoder import (
CategoricalInitMixinNA,
CategoricalMethodsMixin,
Expand Down Expand Up @@ -177,9 +183,13 @@ def __init__(
unseen: str = "ignore",
) -> None:

if encoding_method not in ["ordered", "arbitrary"]:
if not isinstance(encoding_method, str) or encoding_method not in [
"ordered",
"arbitrary",
]:
raise ValueError(
"encoding_method takes only values 'ordered' and 'arbitrary'"
"encoding_method takes only values 'ordered' and 'arbitrary'. "
f"Got {encoding_method} instead."
)

check_parameter_unseen(unseen, ["ignore", "raise", "encode"])
Expand All @@ -190,48 +200,63 @@ def __init__(
self.unseen = unseen
self.return_empty = return_empty

def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None):
def fit(self, X: IntoDataFrame, y: Optional[IntoSeries] = None):
"""Learn the numbers to be used to replace the categories in each
variable.

Parameters
----------
X: pandas dataframe of shape = [n_samples, n_features]
X: dataframe of shape = [n_samples, n_features]
The training input samples. Can be the entire dataframe, not just the
variables to be encoded.

y: pandas series, default=None
y: Series, default=None
The Target. Can be None if `encoding_method='arbitrary'`.
Otherwise, y needs to be passed when fitting the transformer.
"""

if self.encoding_method == "ordered":
X, y = check_X_y(X, y)
nw_X, y = check_X_y(X, y)
nw_Xy = add_target_to_X(nw_X, y)
else:
X = check_X(X)
nw_X = check_X(X)

variables_ = self._check_or_select_variables(X)
self._check_na(X, variables_)

self.encoder_dict_ = {}

for var in variables_:
# pandas is faster than narwhals.
if nwd.is_pandas_dataframe(X):
if self.encoding_method == "ordered":
t = y.groupby(X[var], observed=False).mean() # type: ignore
t = t.sort_values(ascending=True).index

elif self.encoding_method == "arbitrary":
if self.missing_values == "ignore":
# pandas series with the index of X
y_pd = nw_Xy[TARGET_NAME].to_native()
for var in variables_:
if self.encoding_method == "ordered":
t = y_pd.groupby(X[var], observed=False).mean().sort_values().index
elif self.missing_values == "ignore":
t = X[var].dropna().unique()
else:
t = X[var].unique()
else:
raise ValueError(
"Unrecognized value for encoding_method. It should be 'arbitrary' "
f"or 'frequency'. Got {self.encoding_method} instead."
)

self.encoder_dict_[var] = {k: i for i, k in enumerate(t, 0)}
self.encoder_dict_[var] = {k: i for i, k in enumerate(t)}
else:
for var in variables_:
if self.encoding_method == "ordered":
# sort by mean, then category, so ties get the same order
# in every backend
t = (
nw_Xy.group_by(var, drop_null_keys=True)
.agg(nw.col(TARGET_NAME).mean())
.sort([TARGET_NAME, var])
.get_column(var)
.to_list()
)
else:
col = nw_X.get_column(var)
if self.missing_values == "ignore":
col = col.drop_nulls()
t = col.unique(maintain_order=True).to_list()
self.encoder_dict_[var] = {k: i for i, k in enumerate(t)}

if self.unseen == "encode":
self._unseen = -1
Expand Down
Loading