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
2 changes: 1 addition & 1 deletion feature_engine/_base_transformers/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def get_feature_names_out(
else:
raise ValueError(
"input_features must be a list or an array. "
"Got {input_features} instead."
f"Got {input_features} instead."
)

feature_names = self.feature_names_in_
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
def _check_param_missing_values(missing_values):
if missing_values not in ["raise", "ignore"]:
if not isinstance(missing_values, str) or missing_values not in [
"raise",
"ignore",
]:
raise ValueError(
"missing_values takes only values 'raise' or 'ignore'. "
f"Got {missing_values} instead."
Expand Down
20 changes: 19 additions & 1 deletion feature_engine/encoding/_helper_functions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import narwhals as nw
import narwhals.dependencies as nwd

TARGET_NAME = "__feature_engine_target__"


def check_parameter_unseen(unseen, accepted_values):
if not isinstance(accepted_values, list) or not all(
isinstance(item, str) for item in accepted_values
Expand All @@ -6,8 +12,20 @@ def check_parameter_unseen(unseen, accepted_values):
"accepted_values should be a list of strings. "
f" Got {accepted_values} instead."
)
if unseen not in accepted_values:
if not isinstance(unseen, str) or unseen not in accepted_values:
raise ValueError(
f"Parameter `unseen` takes only values {', '.join(accepted_values)}."
f" Got {unseen} instead."
)


def add_target_to_X(nw_X, y):
"""Add y to X as the column TARGET_NAME, pairing rows by position.

y can be a series, list or array. With pandas, the column takes the index of X.
"""
if nwd.is_into_series(y):
y_nw = nw.from_native(y, series_only=True)
else:
y_nw = nw.new_series(name=TARGET_NAME, values=y, backend=nw_X.implementation)
return nw_X.with_columns(y_nw.alias(TARGET_NAME))
5 changes: 4 additions & 1 deletion feature_engine/encoding/base_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ def __init__(
ignore_format: bool = False,
) -> None:

if missing_values not in ["raise", "ignore"]:
if not isinstance(missing_values, str) or missing_values not in [
"raise",
"ignore",
]:
raise ValueError(
"missing_values takes only values 'raise' or 'ignore'. "
f"Got {missing_values} instead."
Expand Down
5 changes: 4 additions & 1 deletion feature_engine/encoding/count_frequency.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ def __init__(
unseen: str = "ignore",
) -> None:

if encoding_method not in ["count", "frequency"]:
if not isinstance(encoding_method, str) or encoding_method not in [
"count",
"frequency",
]:
raise ValueError(
"encoding_method takes only values 'count' and 'frequency'. "
f"Got {encoding_method} instead."
Expand Down
26 changes: 11 additions & 15 deletions feature_engine/encoding/mean_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
)
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._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 @@ -225,16 +229,8 @@ def fit(self, X: IntoDataFrame, y: IntoSeries):

self.encoder_dict_ = {}

# pair y with X by position, so list, array and series targets all work
target_name = "__feature_engine_mean_target__"
if nwd.is_into_series(y):
y_nw = nw.from_native(y, series_only=True).alias(target_name)
else:
y_nw = nw.new_series(
name=target_name, values=y, backend=nw_X.implementation
)
nw_Xy = nw_X.with_columns(y_nw)

nw_Xy = add_target_to_X(nw_X, y)
y_nw = nw_Xy[TARGET_NAME]
y_prior = y_nw.mean()

if self.unseen == "encode":
Expand All @@ -246,7 +242,7 @@ def fit(self, X: IntoDataFrame, y: IntoSeries):
# pandas is faster than narwhals.
if nwd.is_pandas_dataframe(X):
# pandas series with the index of X
y = nw_Xy[target_name].to_native()
y = y_nw.to_native()
for var in variables_:
if self.smoothing == "auto":
damping = y.groupby(X[var]).var(ddof=0) / y_var
Expand All @@ -262,9 +258,9 @@ def fit(self, X: IntoDataFrame, y: IntoSeries):
else:
for var in variables_:
stats = nw_Xy.group_by(var, drop_null_keys=True).agg(
nw.col(target_name).mean().alias("__mean__"),
nw.col(target_name).len().alias("__count__"),
nw.col(target_name).var(ddof=0).alias("__var__"),
nw.col(TARGET_NAME).mean().alias("__mean__"),
nw.col(TARGET_NAME).len().alias("__count__"),
nw.col(TARGET_NAME).var(ddof=0).alias("__var__"),
)
if self.smoothing == "auto":
damping = nw.col("__var__") / y_var
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

import numpy as np
import pandas as pd
import polars as pl
Expand Down Expand Up @@ -155,10 +157,12 @@ def test_raise_error_when_input_feature_non_permitted():
with pytest.raises(ValueError, match="feature_names_in_"):
transformer.get_feature_names_out(input_features=np.array(["Name", "Age"]))

with pytest.raises(ValueError, match="list or an array"):
msg = "input_features must be a list or an array. Got var1 instead."
with pytest.raises(ValueError, match=re.escape(msg)):
transformer.get_feature_names_out(input_features="var1")

with pytest.raises(ValueError, match="list or an array"):
msg = "input_features must be a list or an array. Got True instead."
with pytest.raises(ValueError, match=re.escape(msg)):
transformer.get_feature_names_out(input_features=True)


Expand Down
18 changes: 15 additions & 3 deletions tests/test_check_init_parameters/test_check_init_input_params.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

import pytest

from feature_engine._check_init_parameters.check_init_input_params import (
Expand All @@ -6,13 +8,23 @@
)


@pytest.mark.parametrize("missing_vals", [None, ["Hola"], True, "Hola"])
@pytest.mark.parametrize(
"missing_vals", [None, ["Hola"], ["raise"], ("ignore",), True, 1, "Hola", "Raise"]
)
def test_check_param_missing_values(missing_vals):
with pytest.raises(ValueError):
msg = (
"missing_values takes only values 'raise' or 'ignore'. "
f"Got {missing_vals} instead."
)
with pytest.raises(ValueError, match=re.escape(msg)):
_check_param_missing_values(missing_vals)


@pytest.mark.parametrize("drop_orig", [None, ["Hola"], 10, "Hola"])
def test_check_param_drop_original(drop_orig):
with pytest.raises(ValueError):
msg = (
"drop_original takes only boolean values True and False. "
f"Got {drop_orig} instead."
)
with pytest.raises(ValueError, match=re.escape(msg)):
_check_param_drop_original(drop_orig)
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import re

import pytest

from feature_engine.encoding.base_encoder import CategoricalInitMixin


@pytest.mark.parametrize("param", [1, "hola", [1, 2, 0], (True, False)])
def test_raises_error_when_ignore_format_not_permitted(param):
with pytest.raises(ValueError) as record:
CategoricalInitMixin(ignore_format=param)
msg = f"ignore_format takes only booleans True and False. Got {param} instead."
assert str(record.value) == msg
with pytest.raises(ValueError, match=re.escape(msg)):
CategoricalInitMixin(ignore_format=param)


@pytest.mark.parametrize("param", [True, False])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import re

import pytest

from feature_engine.encoding.base_encoder import CategoricalInitMixinNA


@pytest.mark.parametrize("param", [1, "hola", [1, 2, 0], (True, False)])
def test_raises_error_when_ignore_format_not_permitted(param):
with pytest.raises(ValueError) as record:
CategoricalInitMixinNA(ignore_format=param)
msg = f"ignore_format takes only booleans True and False. Got {param} instead."
assert str(record.value) == msg
with pytest.raises(ValueError, match=re.escape(msg)):
CategoricalInitMixinNA(ignore_format=param)


@pytest.mark.parametrize("param", [1, "hola", [1, 2, 0], (True, False)])
@pytest.mark.parametrize(
"param", [1, "hola", "Raise", None, [1, 2, 0], ["raise"], (True, False)]
)
def test_raises_error_when_missing_values_not_permitted(param):
with pytest.raises(ValueError) as record:
CategoricalInitMixinNA(missing_values=param)
msg = f"missing_values takes only values 'raise' or 'ignore'. Got {param} instead."
assert str(record.value) == msg
with pytest.raises(ValueError, match=re.escape(msg)):
CategoricalInitMixinNA(missing_values=param)


@pytest.mark.parametrize("param", [(True, "ignore"), (False, "raise")])
Expand Down
22 changes: 17 additions & 5 deletions tests/test_encoding/test_count_frequency_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@


# init parameters
@pytest.mark.parametrize("enc_method", ["arbitrary", False, 1])
@pytest.mark.parametrize(
"enc_method",
["arbitrary", "Count", "", False, 1, None, ["count"], ("frequency",)],
)
def test_error_if_encoding_method_not_permitted_value(enc_method):
msg = (
"encoding_method takes only values 'count' and 'frequency'. "
f"Got {enc_method} instead."
)
with pytest.raises(ValueError, match=msg):
with pytest.raises(ValueError, match=re.escape(msg)):
CountEncoder(encoding_method=enc_method)


Expand Down Expand Up @@ -337,18 +340,27 @@ def test_inverse_transform_when_encode_unseen(make_df):
def test_inverse_transform_raises_non_fitted_error(make_df):
df1 = make_df({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]})
enc = CountEncoder()
msg = (
"This CountEncoder instance is not fitted yet. Call 'fit' with "
"appropriate arguments before using this estimator."
)
msg_na = (
"Some of the variables in the dataset contain NaN. Check and "
"remove those before using this transformer or set the parameter "
"`missing_values='ignore'` when initialising this transformer."
)

# Test when fit is not called prior to transform.
with pytest.raises(NotFittedError):
with pytest.raises(NotFittedError, match=re.escape(msg)):
enc.inverse_transform(df1)

df1_na = make_df({"words": ["dog", "dog", "cat", "cat", "cat", None]})

with pytest.raises(ValueError):
with pytest.raises(ValueError, match=re.escape(msg_na)):
enc.fit(df1_na)

# Test when fit is not called prior to transform.
with pytest.raises(NotFittedError):
with pytest.raises(NotFittedError, match=re.escape(msg)):
enc.inverse_transform(df1_na)


Expand Down
53 changes: 40 additions & 13 deletions tests/test_encoding/test_helper_functions.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,49 @@
import re

import narwhals as nw
import numpy as np
import pandas as pd
import pytest

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 tests.backend_helpers import frame_to_dict, make_series


@pytest.mark.parametrize("accepted", ["one", False, [1, 2], ("one", "two"), 1])
def test_raises_error_when_accepted_values_not_permitted(accepted):
with pytest.raises(ValueError) as record:
check_parameter_unseen("zero", accepted)
msg = "accepted_values should be a list of strings. " f" Got {accepted} instead."
assert str(record.value) == msg
with pytest.raises(ValueError, match=re.escape(msg)):
check_parameter_unseen("zero", accepted)


@pytest.mark.parametrize("accepted", [["one", "two"], ["three", "four"]])
def test_raises_error_when_error_not_in_accepted_values(accepted):
with pytest.raises(ValueError) as record:
check_parameter_unseen("zero", accepted)
msg = (
f"Parameter `unseen` takes only values {', '.join(accepted)}."
" Got zero instead."
)
assert str(record.value) == msg
@pytest.mark.parametrize("unseen", ["zero", "One", "", 1, None, ["one"], ("one",)])
def test_raises_error_when_unseen_not_in_accepted_values(unseen):
msg = f"Parameter `unseen` takes only values one, two. Got {unseen} instead."
with pytest.raises(ValueError, match=re.escape(msg)):
check_parameter_unseen(unseen, ["one", "two"])


@pytest.mark.parametrize("to_target", [list, np.array, "series"])
def test_add_target_to_X_pairs_rows_by_position(make_df, to_target):
X = make_df({"var_A": ["a", "b", "c"]})
values = [1, 0, 1]
if to_target == "series":
y = make_series(make_df, values)
else:
y = to_target(values)

Xy = add_target_to_X(nw.from_native(X), y).to_native()

assert isinstance(Xy, make_df)
assert frame_to_dict(Xy) == {"var_A": ["a", "b", "c"], TARGET_NAME: values}


def test_add_target_to_X_keeps_the_pandas_index():
X = pd.DataFrame({"var_A": ["a", "b", "c"]}, index=[12, 10, 11])
Xy = add_target_to_X(nw.from_native(X), np.array([1, 0, 1])).to_native()
assert Xy.index.tolist() == [12, 10, 11]
assert Xy[TARGET_NAME].tolist() == [1, 0, 1]
Loading