From 3a77626c3e616e31c76976586d2646113fc6e566 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Mon, 14 Sep 2026 22:13:12 +0200 Subject: [PATCH 1/5] Add shared backend test fixtures and helpers, use them in CountEncoder tests Tests for narwhals-migrated transformers each defined their own way of building pandas/polars inputs and reading results back (_to_backend, _assert_values, _cols, _to_dict, _to_pandas, ...), which makes the test suite hard to maintain. Standardise on one structure: - tests/conftest.py: `make_df` fixture parametrized over pd.DataFrame and pl.DataFrame (ids "pandas"/"polars"). Tests that request it run once per backend; pandas-only tests simply don't request it. - tests/backend_helpers.py: `to_dict` (contents as {column: values}, NaN normalised to None), `null_count`, and `make_series` (target on the same backend as X). - tests/test_encoding/conftest.py: data shared by the encoder tests, as fixtures returning plain dicts (missing values written as None) that tests build with make_df(data). CountEncoder tests are migrated to this structure: they check the output is of the input backend with isinstance(X, make_df) and compare contents with to_dict(). Co-Authored-By: Claude Opus 5 --- tests/backend_helpers.py | 39 +++++ tests/conftest.py | 11 ++ tests/test_encoding/conftest.py | 73 ++++++++ .../test_count_frequency_encoder.py | 163 ++++++------------ 4 files changed, 176 insertions(+), 110 deletions(-) create mode 100644 tests/backend_helpers.py create mode 100644 tests/test_encoding/conftest.py diff --git a/tests/backend_helpers.py b/tests/backend_helpers.py new file mode 100644 index 000000000..03dc4562f --- /dev/null +++ b/tests/backend_helpers.py @@ -0,0 +1,39 @@ +"""Helpers for tests that run on several dataframe backends. + +Use them together with the ``make_df`` fixture in ``tests/conftest.py``. +""" + +import narwhals as nw +import pandas as pd +import polars as pl + + +def to_dict(X): + """Return the dataframe contents as ``{column: list of values}``. + + pandas represents missing values as NaN and polars as None, so NaN (and + pd.NA) are normalised to None and the same expected values work for both + backends. + """ + result = nw.from_native(X, eager_only=True).to_dict(as_series=False) + return { + col: [_none_if_missing(v) for v in values] for col, values in result.items() + } + + +def null_count(X, col): + """Return the number of missing values in column ``col``.""" + return nw.from_native(X, eager_only=True).get_column(col).null_count() + + +def make_series(make_df, values, name=None): + """Build a Series on the same backend as ``make_df``.""" + if make_df is pd.DataFrame: + return pd.Series(values, name=name) + return pl.Series(name=name or "", values=values) + + +def _none_if_missing(value): + if value is pd.NA or (isinstance(value, float) and value != value): + return None + return value diff --git a/tests/conftest.py b/tests/conftest.py index 721b8b5f3..88fe8f3d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,19 @@ import numpy as np import pandas as pd +import polars as pl import pytest +@pytest.fixture(params=[pd.DataFrame, pl.DataFrame], ids=["pandas", "polars"]) +def make_df(request): + """Dataframe constructor of the backend under test: pandas or polars. + + A test that requests this fixture runs once per backend. Build the input + with ``make_df(data)`` and check the output with ``isinstance(X, make_df)``. + """ + return request.param + + @pytest.fixture(scope="module") def df_vartypes(): data = { diff --git a/tests/test_encoding/conftest.py b/tests/test_encoding/conftest.py new file mode 100644 index 000000000..1ce6fa7df --- /dev/null +++ b/tests/test_encoding/conftest.py @@ -0,0 +1,73 @@ +"""Data shared by the encoder tests. + +Each fixture returns a fresh dict, so tests can build the dataframe on the +backend under test with ``make_df(data)``. Missing values are written as None, +which both pandas and polars read as missing. +""" + +import pytest + +TARGET = [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0] + + +@pytest.fixture +def data_enc(): + return { + "var_A": ["A"] * 6 + ["B"] * 10 + ["C"] * 4, + "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, + "target": list(TARGET), + } + + +@pytest.fixture +def data_enc_rare(): + return { + "var_A": ["B"] * 9 + ["A"] * 6 + ["C"] * 4 + ["D"] * 1, + "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, + "target": list(TARGET), + } + + +@pytest.fixture +def data_enc_na(): + return { + "var_A": [None] + ["B"] * 8 + ["A"] * 6 + ["C"] * 4 + ["D"] * 1, + "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, + "target": list(TARGET), + } + + +@pytest.fixture +def data_enc_numeric(): + return { + "var_A": [1] * 6 + [2] * 10 + [3] * 4, + "var_B": [1] * 10 + [2] * 6 + [3] * 4, + "target": list(TARGET), + } + + +@pytest.fixture +def data_enc_big(): + return { + "var_A": ["A"] * 6 + + ["B"] * 10 + + ["C"] * 4 + + ["D"] * 10 + + ["E"] * 2 + + ["F"] * 2 + + ["G"] * 6, + "var_B": ["A"] * 10 + + ["B"] * 6 + + ["C"] * 4 + + ["D"] * 10 + + ["E"] * 2 + + ["F"] * 2 + + ["G"] * 6, + "var_C": ["A"] * 4 + + ["B"] * 6 + + ["C"] * 10 + + ["D"] * 10 + + ["E"] * 2 + + ["F"] * 2 + + ["G"] * 6, + } diff --git a/tests/test_encoding/test_count_frequency_encoder.py b/tests/test_encoding/test_count_frequency_encoder.py index b4460c0da..c73cbbfa3 100644 --- a/tests/test_encoding/test_count_frequency_encoder.py +++ b/tests/test_encoding/test_count_frequency_encoder.py @@ -1,29 +1,13 @@ import re import warnings -import narwhals as nw import pandas as pd -import polars as pl import pytest from sklearn.exceptions import NotFittedError from feature_engine.encoding import CountEncoder, CountFrequencyEncoder +from tests.backend_helpers import null_count, to_dict -DATA_ENC = { - "var_A": ["A"] * 6 + ["B"] * 10 + ["C"] * 4, - "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, - "target": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], -} -DATA_ENC_RARE = { - "var_A": ["B"] * 9 + ["A"] * 6 + ["C"] * 4 + ["D"] * 1, - "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, - "target": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], -} -DATA_ENC_NA = { - "var_A": [None] + ["B"] * 8 + ["A"] * 6 + ["C"] * 4 + ["D"] * 1, - "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, - "target": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], -} DATA_VARTYPES = { "Name": ["tom", "nick", "krish", "jack"], "City": ["London", "Manchester", "Liverpool", "Bristol"], @@ -33,21 +17,6 @@ } -def _to_dict(X): - # to_dict(as_series=False) is a convenient, backend-agnostic way to read - # values back out for comparison, regardless of pandas vs polars. pandas - # returns NaN where polars returns None, so normalise NaN to None. - result = nw.from_native(X, eager_only=True).to_dict(as_series=False) - return { - c: [None if isinstance(v, float) and v != v else v for v in values] - for c, values in result.items() - } - - -def _null_count(X, col): - return nw.from_native(X, eager_only=True)[col].null_count() - - # init parameters @pytest.mark.parametrize("enc_method", ["arbitrary", False, 1]) def test_error_if_encoding_method_not_permitted_value(enc_method): @@ -80,12 +49,10 @@ def test_init_param_assignment(params): # fit and transform -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_encode_1_variable_with_counts(make_df): +def test_encode_1_variable_with_counts(make_df, data_enc): # test case 1: 1 variable, counts - df_enc = make_df(DATA_ENC) encoder = CountEncoder(encoding_method="count", variables=["var_A"]) - X = encoder.fit_transform(df_enc) + X = encoder.fit_transform(make_df(data_enc)) # init params assert encoder.encoding_method == "count" @@ -96,19 +63,17 @@ def test_encode_1_variable_with_counts(make_df): assert encoder.n_features_in_ == 3 # transform params assert isinstance(X, make_df) - assert _to_dict(X) == { + assert to_dict(X) == { "var_A": [6] * 6 + [10] * 10 + [4] * 4, - "var_B": DATA_ENC["var_B"], - "target": DATA_ENC["target"], + "var_B": data_enc["var_B"], + "target": data_enc["target"], } -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_automatically_select_variables_encode_with_frequency(make_df): +def test_automatically_select_variables_encode_with_frequency(make_df, data_enc): # test case 2: automatically select variables, frequency - df_enc = make_df(DATA_ENC) encoder = CountEncoder(encoding_method="frequency", variables=None) - X = encoder.fit_transform(df_enc) + X = encoder.fit_transform(make_df(data_enc)) # init params assert encoder.encoding_method == "frequency" @@ -122,22 +87,19 @@ def test_automatically_select_variables_encode_with_frequency(make_df): assert encoder.n_features_in_ == 3 # transform params assert isinstance(X, make_df) - assert _to_dict(X) == { + assert to_dict(X) == { "var_A": [0.3] * 6 + [0.5] * 10 + [0.2] * 4, "var_B": [0.5] * 10 + [0.3] * 6 + [0.2] * 4, - "target": DATA_ENC["target"], + "target": data_enc["target"], } -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_encoding_when_nan_in_fit_df(make_df): - df_enc = make_df(DATA_ENC) - +def test_encoding_when_nan_in_fit_df(make_df, data_enc): encoder = CountEncoder( encoding_method="frequency", missing_values="ignore", ) - encoder.fit(df_enc) + encoder.fit(make_df(data_enc)) X = encoder.transform( make_df({"var_A": ["A", None], "var_B": ["A", None], "target": [1, 0]}) @@ -145,13 +107,13 @@ def test_encoding_when_nan_in_fit_df(make_df): # transform params assert isinstance(X, make_df) - assert _to_dict(X) == {"var_A": [0.3, None], "var_B": [0.5, None], "target": [1, 0]} + assert to_dict(X) == {"var_A": [0.3, None], "var_B": [0.5, None], "target": [1, 0]} -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) @pytest.mark.parametrize("enc_method", ["arbitrary", False, 1]) -def test_error_if_encoding_method_not_recognized_in_fit(enc_method, make_df): - df_enc = make_df(DATA_ENC) +def test_error_if_encoding_method_not_recognized_in_fit( + enc_method, make_df, data_enc +): enc = CountEncoder() enc.encoding_method = enc_method msg = ( @@ -159,31 +121,28 @@ def test_error_if_encoding_method_not_recognized_in_fit(enc_method, make_df): f"'frequency'. Got {enc_method} instead." ) with pytest.raises(ValueError, match=re.escape(msg)): - enc.fit(df_enc) + enc.fit(make_df(data_enc)) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_warning_when_df_contains_unseen_categories(make_df): +def test_warning_when_df_contains_unseen_categories( + make_df, data_enc, data_enc_rare +): # dataset to be transformed contains categories not present in # training dataset (unseen categories), unseen set to ignore. - df_enc = make_df(DATA_ENC) - df_enc_rare = make_df(DATA_ENC_RARE) - msg = "During the encoding, NaN values were introduced in the feature(s) var_A." # check for warning when unseen equals 'ignore' encoder = CountEncoder(unseen="ignore") - encoder.fit(df_enc) + encoder.fit(make_df(data_enc)) with pytest.warns(UserWarning, match=re.escape(msg)): - encoder.transform(df_enc_rare) + encoder.transform(make_df(data_enc_rare)) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_error_when_df_contains_unseen_categories(make_df): +def test_error_when_df_contains_unseen_categories(make_df, data_enc, data_enc_rare): # dataset to be transformed contains categories not present in # training dataset (unseen categories), unseen set to raise. - df_enc = make_df(DATA_ENC) - df_enc_rare = make_df(DATA_ENC_RARE) + df_enc = make_df(data_enc) + df_enc_rare = make_df(data_enc_rare) msg = "During the encoding, NaN values were introduced in the feature(s) var_A." @@ -202,28 +161,23 @@ def test_error_when_df_contains_unseen_categories(make_df): encoder.transform(df_enc_rare) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_no_error_triggered_when_df_contains_unseen_categories_and_unseen_is_encode( - make_df, + make_df, data_enc, data_enc_rare ): # dataset to be transformed contains categories not present in # training dataset (unseen categories). - df_enc = make_df(DATA_ENC) - df_enc_rare = make_df(DATA_ENC_RARE) # check for no error and no warning when unseen equals 'encode' warnings.simplefilter("error") encoder = CountEncoder(unseen="encode") - encoder.fit(df_enc) + encoder.fit(make_df(data_enc)) with warnings.catch_warnings(): - encoder.transform(df_enc_rare) + encoder.transform(make_df(data_enc_rare)) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) @pytest.mark.parametrize("errors", ["raise", "ignore", "encode"]) -def test_fit_raises_error_if_df_contains_na(errors, make_df): +def test_fit_raises_error_if_df_contains_na(errors, make_df, data_enc_na): # test case 4: when dataset contains na, fit method - df_enc_na = make_df(DATA_ENC_NA) encoder = CountEncoder(unseen=errors) msg = ( "Some of the variables in the dataset contain NaN. Check and " @@ -231,27 +185,25 @@ def test_fit_raises_error_if_df_contains_na(errors, make_df): "`missing_values='ignore'` when initialising this transformer." ) with pytest.raises(ValueError, match=re.escape(msg)): - encoder.fit(df_enc_na) + encoder.fit(make_df(data_enc_na)) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) @pytest.mark.parametrize("errors", ["raise", "ignore", "encode"]) -def test_transform_raises_error_if_df_contains_na(errors, make_df): +def test_transform_raises_error_if_df_contains_na( + errors, make_df, data_enc, data_enc_na +): # test case 4: when dataset contains na, transform method - df_enc = make_df(DATA_ENC) - df_enc_na = make_df(DATA_ENC_NA) encoder = CountEncoder(unseen=errors) - encoder.fit(df_enc) + encoder.fit(make_df(data_enc)) msg = ( "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." ) with pytest.raises(ValueError, match=re.escape(msg)): - encoder.transform(df_enc_na) + encoder.transform(make_df(data_enc_na)) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_zero_encoding_for_new_categories(make_df): df_fit = make_df( {"col1": ["a", "a", "b", "a", "c"], "col2": ["1", "2", "3", "1", "2"]} @@ -265,14 +217,13 @@ def test_zero_encoding_for_new_categories(make_df): assert isinstance(result, make_df) # check that no NaNs are added - assert _null_count(result, "col1") == 0 - assert _null_count(result, "col2") == 0 + assert null_count(result, "col1") == 0 + assert null_count(result, "col2") == 0 # check that the counts are correct for both new and old - assert _to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} + assert to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): df_fit = make_df( {"col1": ["a", "a", "b", "a", "c"], "col2": ["1", "2", "3", "1", "2"]} @@ -287,11 +238,11 @@ def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): assert isinstance(result, make_df) # check that no NaNs are added - assert _null_count(result, "col1") == 0 - assert _null_count(result, "col2") == 0 + assert null_count(result, "col1") == 0 + assert null_count(result, "col2") == 0 # check that the counts are correct - assert _to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} + assert to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} # with frequency encoder = CountEncoder(encoding_method="frequency", unseen="encode").fit(df_fit) @@ -299,17 +250,16 @@ def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): assert isinstance(result, make_df) # check that no NaNs are added - assert _null_count(result, "col1") == 0 - assert _null_count(result, "col2") == 0 + assert null_count(result, "col1") == 0 + assert null_count(result, "col2") == 0 # check that the frequencies are correct - assert _to_dict(result) == { + assert to_dict(result) == { "col1": [0.6, 0, 0.2, 0.6, 0.2], "col2": [0.4, 0.4, 0.2, 0.4, 0], } -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_nan_encoding_for_new_categories_if_unseen_is_ignore(make_df): df_fit = make_df( {"col1": ["a", "a", "b", "a", "c"], "col2": ["1", "2", "3", "1", "2"]} @@ -322,23 +272,21 @@ def test_nan_encoding_for_new_categories_if_unseen_is_ignore(make_df): assert isinstance(result, make_df) # check that 1 NaN is added per variable - assert _null_count(result, "col1") == 1 - assert _null_count(result, "col2") == 1 + assert null_count(result, "col1") == 1 + assert null_count(result, "col2") == 1 # check that the counts are correct for both new and old - assert _to_dict(result) == { + assert to_dict(result) == { "col1": [3, None, 1, 3, 1], "col2": [2, 2, 1, 2, None], } -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_ignore_variable_format_with_frequency(make_df): - df_vartypes = make_df(DATA_VARTYPES) encoder = CountEncoder( encoding_method="frequency", variables=None, ignore_format=True ) - X = encoder.fit_transform(df_vartypes) + X = encoder.fit_transform(make_df(DATA_VARTYPES)) # init params assert encoder.encoding_method == "frequency" @@ -348,7 +296,7 @@ def test_ignore_variable_format_with_frequency(make_df): assert encoder.n_features_in_ == 5 # transform params assert isinstance(X, make_df) - assert _to_dict(X) == { + assert to_dict(X) == { "Name": [0.25, 0.25, 0.25, 0.25], "City": [0.25, 0.25, 0.25, 0.25], "Age": [0.25, 0.25, 0.25, 0.25], @@ -402,7 +350,6 @@ def test_variables_cast_as_category(df_enc_category_dtypes): assert X["var_A"].dtypes == float -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_inverse_transform_when_no_unseen(make_df): words = ["dog", "dog", "cat", "cat", "cat", "bird"] df = make_df({"words": words}) @@ -411,10 +358,9 @@ def test_inverse_transform_when_no_unseen(make_df): dft = enc.transform(df) X = enc.inverse_transform(dft) assert isinstance(X, make_df) - assert _to_dict(X) == {"words": words} + assert to_dict(X) == {"words": words} -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_inverse_transform_when_ignore_unseen(make_df): df1 = make_df({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]}) df2 = make_df({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]}) @@ -423,10 +369,9 @@ def test_inverse_transform_when_ignore_unseen(make_df): dft = enc.transform(df2) X = enc.inverse_transform(dft) assert isinstance(X, make_df) - assert _to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} + assert to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_inverse_transform_when_encode_unseen(make_df): df1 = make_df({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]}) df2 = make_df({"words": ["dog", "dog", "cat", "cat", "cat", "frog"]}) @@ -435,10 +380,9 @@ def test_inverse_transform_when_encode_unseen(make_df): dft = enc.transform(df2) X = enc.inverse_transform(dft) assert isinstance(X, make_df) - assert _to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} + assert to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_inverse_transform_raises_non_fitted_error(make_df): df1 = make_df({"words": ["dog", "dog", "cat", "cat", "cat", "bird"]}) enc = CountEncoder() @@ -457,7 +401,6 @@ def test_inverse_transform_raises_non_fitted_error(make_df): enc.inverse_transform(df1_na) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_count_frequency_encoder_is_deprecated(make_df): """CountFrequencyEncoder should emit a FutureWarning and still work.""" X = make_df({"var_A": ["A"] * 6 + ["B"] * 2 + ["C"] * 2}) @@ -472,4 +415,4 @@ def test_count_frequency_encoder_is_deprecated(make_df): X_new = enc_new.fit_transform(X) assert isinstance(X_old, make_df) assert isinstance(X_new, make_df) - assert _to_dict(X_old) == _to_dict(X_new) == {"var_A": [6] * 6 + [2] * 2 + [2] * 2} + assert to_dict(X_old) == to_dict(X_new) == {"var_A": [6] * 6 + [2] * 2 + [2] * 2} From 3076a5a87b566bee9bb375865859f622e17f273a Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Mon, 14 Sep 2026 22:21:10 +0200 Subject: [PATCH 2/5] Add data_enc_big_na and data_enc_top encoder test fixtures Shared by the OneHotEncoder, RareLabelEncoder and StringSimilarityEncoder tests, which each defined their own copy of this data. Co-Authored-By: Claude Opus 5 --- tests/test_encoding/conftest.py | 42 +++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/tests/test_encoding/conftest.py b/tests/test_encoding/conftest.py index 1ce6fa7df..3d7ceb0f1 100644 --- a/tests/test_encoding/conftest.py +++ b/tests/test_encoding/conftest.py @@ -46,8 +46,7 @@ def data_enc_numeric(): } -@pytest.fixture -def data_enc_big(): +def _data_enc_big(): return { "var_A": ["A"] * 6 + ["B"] * 10 @@ -71,3 +70,42 @@ def data_enc_big(): + ["F"] * 2 + ["G"] * 6, } + + +@pytest.fixture +def data_enc_big(): + return _data_enc_big() + + +@pytest.fixture +def data_enc_big_na(): + data = _data_enc_big() + data["var_A"][0] = None + return data + + +@pytest.fixture +def data_enc_top(): + return { + "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, + } From 0453201cc780ec7a092c87fdf4c0981dd6422ec1 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 09:25:57 +0200 Subject: [PATCH 3/5] rename functions --- tests/backend_helpers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/backend_helpers.py b/tests/backend_helpers.py index 03dc4562f..44032496f 100644 --- a/tests/backend_helpers.py +++ b/tests/backend_helpers.py @@ -1,6 +1,5 @@ """Helpers for tests that run on several dataframe backends. - -Use them together with the ``make_df`` fixture in ``tests/conftest.py``. +Use them together with the `make_df` fixture in `tests/conftest.py`. """ import narwhals as nw @@ -8,7 +7,7 @@ import polars as pl -def to_dict(X): +def frame_to_dict(X): """Return the dataframe contents as ``{column: list of values}``. pandas represents missing values as NaN and polars as None, so NaN (and @@ -17,7 +16,7 @@ def to_dict(X): """ result = nw.from_native(X, eager_only=True).to_dict(as_series=False) return { - col: [_none_if_missing(v) for v in values] for col, values in result.items() + col: [none_if_missing(v) for v in values] for col, values in result.items() } @@ -33,7 +32,7 @@ def make_series(make_df, values, name=None): return pl.Series(name=name or "", values=values) -def _none_if_missing(value): +def none_if_missing(value): if value is pd.NA or (isinstance(value, float) and value != value): return None return value From 2f2a7a345cb7b98a69b0d60f9200ad839d6e2e2e Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 09:29:23 +0200 Subject: [PATCH 4/5] rename function --- tests/test_encoding/conftest.py | 2 +- .../test_count_frequency_encoder.py | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_encoding/conftest.py b/tests/test_encoding/conftest.py index 3d7ceb0f1..24387fa0c 100644 --- a/tests/test_encoding/conftest.py +++ b/tests/test_encoding/conftest.py @@ -1,7 +1,7 @@ """Data shared by the encoder tests. Each fixture returns a fresh dict, so tests can build the dataframe on the -backend under test with ``make_df(data)``. Missing values are written as None, +backend under test with `make_df(data)`. Missing values are written as None, which both pandas and polars read as missing. """ diff --git a/tests/test_encoding/test_count_frequency_encoder.py b/tests/test_encoding/test_count_frequency_encoder.py index c73cbbfa3..3175c70ba 100644 --- a/tests/test_encoding/test_count_frequency_encoder.py +++ b/tests/test_encoding/test_count_frequency_encoder.py @@ -6,7 +6,7 @@ from sklearn.exceptions import NotFittedError from feature_engine.encoding import CountEncoder, CountFrequencyEncoder -from tests.backend_helpers import null_count, to_dict +from tests.backend_helpers import null_count, frame_to_dict DATA_VARTYPES = { "Name": ["tom", "nick", "krish", "jack"], @@ -63,7 +63,7 @@ def test_encode_1_variable_with_counts(make_df, data_enc): assert encoder.n_features_in_ == 3 # transform params assert isinstance(X, make_df) - assert to_dict(X) == { + assert frame_to_dict(X) == { "var_A": [6] * 6 + [10] * 10 + [4] * 4, "var_B": data_enc["var_B"], "target": data_enc["target"], @@ -87,7 +87,7 @@ def test_automatically_select_variables_encode_with_frequency(make_df, data_enc) assert encoder.n_features_in_ == 3 # transform params assert isinstance(X, make_df) - assert to_dict(X) == { + assert frame_to_dict(X) == { "var_A": [0.3] * 6 + [0.5] * 10 + [0.2] * 4, "var_B": [0.5] * 10 + [0.3] * 6 + [0.2] * 4, "target": data_enc["target"], @@ -107,7 +107,7 @@ def test_encoding_when_nan_in_fit_df(make_df, data_enc): # transform params assert isinstance(X, make_df) - assert to_dict(X) == {"var_A": [0.3, None], "var_B": [0.5, None], "target": [1, 0]} + assert frame_to_dict(X) == {"var_A": [0.3, None], "var_B": [0.5, None], "target": [1, 0]} @pytest.mark.parametrize("enc_method", ["arbitrary", False, 1]) @@ -221,7 +221,7 @@ def test_zero_encoding_for_new_categories(make_df): assert null_count(result, "col2") == 0 # check that the counts are correct for both new and old - assert to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} + assert frame_to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): @@ -242,7 +242,7 @@ def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): assert null_count(result, "col2") == 0 # check that the counts are correct - assert to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} + assert frame_to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} # with frequency encoder = CountEncoder(encoding_method="frequency", unseen="encode").fit(df_fit) @@ -254,7 +254,7 @@ def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): assert null_count(result, "col2") == 0 # check that the frequencies are correct - assert to_dict(result) == { + assert frame_to_dict(result) == { "col1": [0.6, 0, 0.2, 0.6, 0.2], "col2": [0.4, 0.4, 0.2, 0.4, 0], } @@ -276,7 +276,7 @@ def test_nan_encoding_for_new_categories_if_unseen_is_ignore(make_df): assert null_count(result, "col2") == 1 # check that the counts are correct for both new and old - assert to_dict(result) == { + assert frame_to_dict(result) == { "col1": [3, None, 1, 3, 1], "col2": [2, 2, 1, 2, None], } @@ -296,7 +296,7 @@ def test_ignore_variable_format_with_frequency(make_df): assert encoder.n_features_in_ == 5 # transform params assert isinstance(X, make_df) - assert to_dict(X) == { + assert frame_to_dict(X) == { "Name": [0.25, 0.25, 0.25, 0.25], "City": [0.25, 0.25, 0.25, 0.25], "Age": [0.25, 0.25, 0.25, 0.25], @@ -358,7 +358,7 @@ def test_inverse_transform_when_no_unseen(make_df): dft = enc.transform(df) X = enc.inverse_transform(dft) assert isinstance(X, make_df) - assert to_dict(X) == {"words": words} + assert frame_to_dict(X) == {"words": words} def test_inverse_transform_when_ignore_unseen(make_df): @@ -369,7 +369,7 @@ def test_inverse_transform_when_ignore_unseen(make_df): dft = enc.transform(df2) X = enc.inverse_transform(dft) assert isinstance(X, make_df) - assert to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} + assert frame_to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} def test_inverse_transform_when_encode_unseen(make_df): @@ -380,7 +380,7 @@ def test_inverse_transform_when_encode_unseen(make_df): dft = enc.transform(df2) X = enc.inverse_transform(dft) assert isinstance(X, make_df) - assert to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} + assert frame_to_dict(X) == {"words": ["dog", "dog", "cat", "cat", "cat", None]} def test_inverse_transform_raises_non_fitted_error(make_df): @@ -415,4 +415,4 @@ def test_count_frequency_encoder_is_deprecated(make_df): X_new = enc_new.fit_transform(X) assert isinstance(X_old, make_df) assert isinstance(X_new, make_df) - assert to_dict(X_old) == to_dict(X_new) == {"var_A": [6] * 6 + [2] * 2 + [2] * 2} + assert frame_to_dict(X_old) == frame_to_dict(X_new) == {"var_A": [6] * 6 + [2] * 2 + [2] * 2} From bc89b8f5e10daf8dbb67658315d876091337a0fc Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 09:59:38 +0200 Subject: [PATCH 5/5] update tests --- feature_engine/encoding/count_frequency.py | 5 - .../test_count_frequency_encoder.py | 101 +++++------------- 2 files changed, 28 insertions(+), 78 deletions(-) diff --git a/feature_engine/encoding/count_frequency.py b/feature_engine/encoding/count_frequency.py index 1a9825de6..3d063c7dc 100644 --- a/feature_engine/encoding/count_frequency.py +++ b/feature_engine/encoding/count_frequency.py @@ -220,11 +220,6 @@ def fit(self, X: IntoDataFrame, y: Optional[IntoSeries] = None): variables_ = self._check_or_select_variables(X) self._check_na(X, variables_) - if self.encoding_method not in ["count", "frequency"]: - raise ValueError( - "Unrecognized value for encoding_method. It should be 'count' or " - f"'frequency'. Got {self.encoding_method} instead." - ) normalize = self.encoding_method == "frequency" self.encoder_dict_ = {} diff --git a/tests/test_encoding/test_count_frequency_encoder.py b/tests/test_encoding/test_count_frequency_encoder.py index 3175c70ba..b7445f6c7 100644 --- a/tests/test_encoding/test_count_frequency_encoder.py +++ b/tests/test_encoding/test_count_frequency_encoder.py @@ -20,7 +20,11 @@ # init parameters @pytest.mark.parametrize("enc_method", ["arbitrary", False, 1]) def test_error_if_encoding_method_not_permitted_value(enc_method): - with pytest.raises(ValueError): + msg = ( + "encoding_method takes only values 'count' and 'frequency'. " + f"Got {enc_method} instead." + ) + with pytest.raises(ValueError, match=msg): CountEncoder(encoding_method=enc_method) @@ -28,7 +32,11 @@ def test_error_if_encoding_method_not_permitted_value(enc_method): "errors", ["empanada", False, 1, ("raise", "ignore"), ["ignore"]] ) def test_error_if_unseen_gets_not_permitted_value(errors): - with pytest.raises(ValueError): + msg = ( + "Parameter `unseen` takes only values ignore, raise, encode. " + f"Got {errors} instead." + ) + with pytest.raises(ValueError, match=re.escape(msg)): CountEncoder(unseen=errors) @@ -54,9 +62,6 @@ def test_encode_1_variable_with_counts(make_df, data_enc): encoder = CountEncoder(encoding_method="count", variables=["var_A"]) X = encoder.fit_transform(make_df(data_enc)) - # init params - assert encoder.encoding_method == "count" - assert encoder.variables == ["var_A"] # fit params assert encoder.variables_ == ["var_A"] assert encoder.encoder_dict_ == {"var_A": {"A": 6, "B": 10, "C": 4}} @@ -75,9 +80,6 @@ def test_automatically_select_variables_encode_with_frequency(make_df, data_enc) encoder = CountEncoder(encoding_method="frequency", variables=None) X = encoder.fit_transform(make_df(data_enc)) - # init params - assert encoder.encoding_method == "frequency" - assert encoder.variables is None # fit params assert encoder.variables_ == ["var_A", "var_B"] assert encoder.encoder_dict_ == { @@ -107,21 +109,11 @@ def test_encoding_when_nan_in_fit_df(make_df, data_enc): # transform params assert isinstance(X, make_df) - assert frame_to_dict(X) == {"var_A": [0.3, None], "var_B": [0.5, None], "target": [1, 0]} - - -@pytest.mark.parametrize("enc_method", ["arbitrary", False, 1]) -def test_error_if_encoding_method_not_recognized_in_fit( - enc_method, make_df, data_enc -): - enc = CountEncoder() - enc.encoding_method = enc_method - msg = ( - "Unrecognized value for encoding_method. It should be 'count' or " - f"'frequency'. Got {enc_method} instead." - ) - with pytest.raises(ValueError, match=re.escape(msg)): - enc.fit(make_df(data_enc)) + assert frame_to_dict(X) == { + "var_A": [0.3, None], + "var_B": [0.5, None], + "target": [1, 0], + } def test_warning_when_df_contains_unseen_categories( @@ -153,27 +145,6 @@ def test_error_when_df_contains_unseen_categories(make_df, data_enc, data_enc_ra with pytest.raises(ValueError, match=re.escape(msg)): encoder.transform(df_enc_rare) - # check for no error and no warning when unseen equals 'encode' - with warnings.catch_warnings(): - warnings.simplefilter("error") - encoder = CountEncoder(unseen="encode") - encoder.fit(df_enc) - encoder.transform(df_enc_rare) - - -def test_no_error_triggered_when_df_contains_unseen_categories_and_unseen_is_encode( - make_df, data_enc, data_enc_rare -): - # dataset to be transformed contains categories not present in - # training dataset (unseen categories). - - # check for no error and no warning when unseen equals 'encode' - warnings.simplefilter("error") - encoder = CountEncoder(unseen="encode") - encoder.fit(make_df(data_enc)) - with warnings.catch_warnings(): - encoder.transform(make_df(data_enc_rare)) - @pytest.mark.parametrize("errors", ["raise", "ignore", "encode"]) def test_fit_raises_error_if_df_contains_na(errors, make_df, data_enc_na): @@ -204,26 +175,6 @@ def test_transform_raises_error_if_df_contains_na( encoder.transform(make_df(data_enc_na)) -def test_zero_encoding_for_new_categories(make_df): - df_fit = make_df( - {"col1": ["a", "a", "b", "a", "c"], "col2": ["1", "2", "3", "1", "2"]} - ) - df_transf = make_df( - {"col1": ["a", "d", "b", "a", "c"], "col2": ["1", "2", "3", "1", "4"]} - ) - encoder = CountEncoder(unseen="encode").fit(df_fit) - - result = encoder.transform(df_transf) - assert isinstance(result, make_df) - - # check that no NaNs are added - assert null_count(result, "col1") == 0 - assert null_count(result, "col2") == 0 - - # check that the counts are correct for both new and old - assert frame_to_dict(result) == {"col1": [3, 0, 1, 3, 1], "col2": [2, 2, 1, 2, 0]} - - def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): df_fit = make_df( {"col1": ["a", "a", "b", "a", "c"], "col2": ["1", "2", "3", "1", "2"]} @@ -234,7 +185,10 @@ def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): # count encoding encoder = CountEncoder(unseen="encode").fit(df_fit) - result = encoder.transform(df_transform) + # unseen categories are encoded without raising or warning + with warnings.catch_warnings(): + warnings.simplefilter("error") + result = encoder.transform(df_transform) assert isinstance(result, make_df) # check that no NaNs are added @@ -246,7 +200,10 @@ def test_zero_encoding_for_unseen_categories_if_unseen_is_encode(make_df): # with frequency encoder = CountEncoder(encoding_method="frequency", unseen="encode").fit(df_fit) - result = encoder.transform(df_transform) + # unseen categories are encoded without raising or warning + with warnings.catch_warnings(): + warnings.simplefilter("error") + result = encoder.transform(df_transform) assert isinstance(result, make_df) # check that no NaNs are added @@ -288,9 +245,6 @@ def test_ignore_variable_format_with_frequency(make_df): ) X = encoder.fit_transform(make_df(DATA_VARTYPES)) - # init params - assert encoder.encoding_method == "frequency" - assert encoder.variables is None # fit params assert encoder.variables_ == ["Name", "City", "Age", "Marks", "dob"] assert encoder.n_features_in_ == 5 @@ -323,9 +277,6 @@ def test_column_names_are_numbers(df_numeric_columns): transf_df = pd.DataFrame(transf_df) - # init params - assert encoder.encoding_method == "frequency" - assert encoder.variables == [0, 1, 2, 3] # fit params assert encoder.variables_ == [0, 1, 2, 3] assert encoder.n_features_in_ == 5 @@ -415,4 +366,8 @@ def test_count_frequency_encoder_is_deprecated(make_df): X_new = enc_new.fit_transform(X) assert isinstance(X_old, make_df) assert isinstance(X_new, make_df) - assert frame_to_dict(X_old) == frame_to_dict(X_new) == {"var_A": [6] * 6 + [2] * 2 + [2] * 2} + assert ( + frame_to_dict(X_old) + == frame_to_dict(X_new) + == {"var_A": [6] * 6 + [2] * 2 + [2] * 2} + )