From 810a86af186be151fde5c08992682cd17765c373 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Wed, 26 Aug 2026 20:51:21 +0200 Subject: [PATCH 1/8] Migrate OneHotEncoder to narwhals, add polars support Uses narwhals' to_dummies() for the actual expansion rather than a manual numpy/dict loop, since it's a real vectorized one-hot op on both backends. Handles two edge cases to_dummies() doesn't cover directly: a fixed-length prefix placeholder ("__ohe_tmp__") swapped back out by slicing rather than by using the real column name, since to_dummies() only prefixes with the Series name when it's truthy - a falsy real name (e.g. an int column literally named 0) would otherwise silently drop the prefix; and learned categories absent from (or present-but-unlearned in) a given transform batch, filled with an explicit all-0 column so unseen categories are encoded as 0 across the board, matching the pre-narwhals behavior exactly. fit()'s value_counts()/unique() calls and transform()'s reassembly are a single unified narwhals path - no pandas/polars split needed, verified directly on both backends (identical dummy columns/values for identical input). Rewrote tests/test_encoding/test_onehot_encoder.py to the single cross-backend-parametrized-test convention: local dict fixtures (dropping the pandas-only global df_enc_big/df_enc_numeric/df_enc_binary fixtures) parametrized over make_df in [pd.DataFrame, pl.DataFrame], with narwhals- based column/sum assertions replacing pd.testing.assert_frame_equal. test_variables_cast_as_category stays pandas-only (pandas category dtype has no polars equivalent under test there). Verified: 43/43 own tests, full encoding suite 340 passed/17 pre-existing failures (matches the narwhals-encoding-base baseline exactly), flake8 and mypy clean, sphinx -W build clean (only the pre-existing unrelated linkcode_resolve warning), no pandas import in this file itself. Co-Authored-By: Claude Sonnet 5 --- docs/user_guide/encoding/OneHotEncoder.rst | 33 ++ feature_engine/encoding/one_hot.py | 81 +++-- tests/test_encoding/test_onehot_encoder.py | 380 ++++++++++----------- 3 files changed, 261 insertions(+), 233 deletions(-) diff --git a/docs/user_guide/encoding/OneHotEncoder.rst b/docs/user_guide/encoding/OneHotEncoder.rst index 02901671e..94cc1a284 100644 --- a/docs/user_guide/encoding/OneHotEncoder.rst +++ b/docs/user_guide/encoding/OneHotEncoder.rst @@ -521,6 +521,39 @@ We see the names of the columns below: 'embarked_S', 'embarked_C'] +With polars +----------- + +:class:`OneHotEncoder()` works the same way with a polars dataframe: + +.. code:: python + + import polars as pl + from feature_engine.encoding import OneHotEncoder + + X = pl.DataFrame({"x1": ["b", "b", "b", "a", "a"], "x2": [1, 2, 3, 4, 5]}) + + ohe = OneHotEncoder(variables=["x1"]) + ohe.fit(X) + + print(ohe.transform(X)) + +.. code:: text + + shape: (5, 3) + ┌─────┬──────┬──────┐ + │ x2 ┆ x1_b ┆ x1_a │ + │ --- ┆ --- ┆ --- │ + │ i64 ┆ i8 ┆ i8 │ + ╞═════╪══════╪══════╡ + │ 1 ┆ 1 ┆ 0 │ + │ 2 ┆ 1 ┆ 0 │ + │ 3 ┆ 1 ┆ 0 │ + │ 4 ┆ 0 ┆ 1 │ + │ 5 ┆ 0 ┆ 1 │ + └─────┴──────┴──────┘ + + Considerations -------------- diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py index 9d028475b..1212a25b1 100644 --- a/feature_engine/encoding/one_hot.py +++ b/feature_engine/encoding/one_hot.py @@ -3,8 +3,8 @@ from typing import List, Optional, Union -import numpy as np -import pandas as pd +import narwhals as nw +from narwhals.typing import IntoDataFrame from feature_engine._docstrings.fit_attributes import ( _feature_names_in_docstring, @@ -196,7 +196,7 @@ def __init__( self.drop_last = drop_last self.drop_last_binary = drop_last_binary - def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): + def fit(self, X: IntoDataFrame, y: Optional[IntoDataFrame] = None): """ Learns the unique categories per variable. If top_categories is indicated, it will learn the most popular categories. Alternatively, it learns all @@ -205,7 +205,7 @@ def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): Parameters ---------- - X: pandas dataframe of shape = [n_samples, n_features] + X: pandas or polars dataframe of shape = [n_samples, n_features] The training input samples. Can be the entire dataframe, not just selected variables. @@ -218,56 +218,56 @@ def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None): variables_ = self._check_or_select_variables(X) _check_contains_na(X, variables_) + nw_X = nw.from_native(X, eager_only=True) self.encoder_dict_ = {} for var in variables_: + col = nw_X.get_column(var) # make dummies only for the most popular categories if self.top_categories: - self.encoder_dict_[var] = [ - x - for x in X[var] - .value_counts() - .sort_values(ascending=False) - .head(self.top_categories) - .index - ] + top = col.value_counts(sort=True, name="count").head( + self.top_categories + ) + self.encoder_dict_[var] = top.get_column(var).to_list() else: - category_ls = list(X[var].unique()) + category_ls = col.unique(maintain_order=True).to_list() # return k-1 dummies - if self.drop_last: + if self.drop_last is True: self.encoder_dict_[var] = category_ls[:-1] # return k dummies else: self.encoder_dict_[var] = category_ls - self.variables_binary_ = [var for var in variables_ if X[var].nunique() == 2] + self.variables_binary_ = [ + var for var in variables_ if nw_X.get_column(var).n_unique() == 2 + ] # automatically encode binary variables as 1 dummy - if self.drop_last_binary: + if self.drop_last_binary is True: for var in self.variables_binary_: - category = X[var].unique()[0] + category = nw_X.get_column(var).unique(maintain_order=True)[0] self.encoder_dict_[var] = [category] self.variables_ = variables_ self._get_feature_names_in(X) return self - def transform(self, X: pd.DataFrame) -> pd.DataFrame: + def transform(self, X: IntoDataFrame) -> IntoDataFrame: """ Replaces the categorical variables by the binary variables. Parameters ---------- - X: pandas dataframe of shape = [n_samples, n_features] + X: pandas or polars dataframe of shape = [n_samples, n_features] The data to transform. Returns ------- - X_new: pandas dataframe. + X_new: pandas or polars dataframe. The transformed dataframe. The shape of the dataframe will be different from the original as it includes the dummy variables in place of the original categorical ones. @@ -278,20 +278,43 @@ def transform(self, X: pd.DataFrame) -> pd.DataFrame: # check if dataset contains na _check_contains_na(X, self.variables_) + nw_X = nw.from_native(X, eager_only=True) + dummy_frames = [] + # a placeholder Series name, swapped back out below by a fixed-length + # prefix slice (never by parsing the category suffix): to_dummies() + # only prefixes with the Series name when it's truthy, so a falsy + # real name (e.g. an int column literally named 0) would otherwise + # silently drop the prefix and make every dummy column look "missing". + tmp_name = "__ohe_tmp__" for feature in self.variables_: - for category in self.encoder_dict_[feature]: - dummy_df = pd.DataFrame( - {f"{feature}_{category}": np.where(X[feature] == category, 1, 0)}, - index=X.index, + desired = [ + f"{feature}_{category}" for category in self.encoder_dict_[feature] + ] + dummies = ( + nw_X.get_column(feature).alias(tmp_name).to_dummies(separator="_") + ) + dummies = dummies.rename( + {c: f"{feature}{c[len(tmp_name):]}" for c in dummies.columns} + ) + # categories learned in fit() but absent from, or unseen categories + # present in, this particular transform batch: to_dummies() only + # creates columns for values it actually finds, so any learned + # category missing here is filled with an all-0 column, and + # selecting just `desired` drops any column for a category that + # wasn't learned (unseen categories are encoded as 0 across the + # board, matching the pre-narwhals behaviour). + missing = [c for c in desired if c not in dummies.columns] + if len(missing) > 0: + dummies = dummies.with_columns( + **{c: nw.lit(0, dtype=nw.Int8) for c in missing} ) - X = pd.concat([X, dummy_df], axis=1) + dummy_frames.append(dummies.select(desired)) - # drop the original non-encoded variables. - X.drop(labels=self.variables_, axis=1, inplace=True) + nw_X = nw.concat([nw_X.drop(*self.variables_), *dummy_frames], how="horizontal") - return X + return nw_X.to_native() - def inverse_transform(self, X: pd.DataFrame): + def inverse_transform(self, X: IntoDataFrame): """inverse_transform is not implemented for this transformer.""" raise NotImplementedError( "inverse_transform is not implemented for this transformer." diff --git a/tests/test_encoding/test_onehot_encoder.py b/tests/test_encoding/test_onehot_encoder.py index aca3448be..726843e0a 100644 --- a/tests/test_encoding/test_onehot_encoder.py +++ b/tests/test_encoding/test_onehot_encoder.py @@ -1,33 +1,73 @@ +import narwhals as nw import pandas as pd +import polars as pl import pytest from sklearn.pipeline import Pipeline from feature_engine.encoding import OneHotEncoder +DF_ENC_BIG = { + "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, +} +DF_ENC_NUMERIC = { + "var_A": [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3], + "var_B": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3], +} + +DF_ENC_BINARY = { + "var_A": ["A"] * 6 + ["B"] * 10 + ["C"] * 4, + "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, + "var_C": ["AHA"] * 12 + ["UHU"] * 8, + "var_D": ["OHO"] * 5 + ["EHE"] * 15, + "var_num": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], +} + + +def _columns(X): + return list(nw.from_native(X, eager_only=True).columns) + + +def _colsum(X, col): + return sum(nw.from_native(X, eager_only=True).get_column(col).to_list()) + + +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) @pytest.mark.parametrize("index_", [[1, 2, 3], [3, 2, 1], [4, 9, 2]]) -def test_concat_with_non_ordered_index(index_): - df = pd.DataFrame({"varA": ["a", "b", "c"], "varB": ["d", "d", "a"]}, index=index_) +def test_concat_with_non_ordered_index(make_df, index_): + data = {"varA": ["a", "b", "c"], "varB": ["d", "d", "a"]} + if make_df is pd.DataFrame: + df = make_df(data, index=index_) + else: + df = make_df(data) encoder = OneHotEncoder() dft = encoder.fit_transform(df) - df_expected = pd.DataFrame( - { - "varA_a": [1, 0, 0], - "varA_b": [0, 1, 0], - "varA_c": [0, 0, 1], - "varB_d": [1, 1, 0], - "varB_a": [0, 0, 1], - }, - index=index_, - ) - pd.testing.assert_frame_equal(dft, df_expected, check_dtype=False) + + expected = { + "varA_a": [1, 0, 0], + "varA_b": [0, 1, 0], + "varA_c": [0, 0, 1], + "varB_d": [1, 1, 0], + "varB_a": [0, 0, 1], + } + result = nw.from_native(dft, eager_only=True).to_dict(as_series=False) + assert list(result.keys()) == list(expected.keys()) + for col, values in expected.items(): + assert list(result[col]) == values -def test_encode_categories_in_k_binary_plus_select_vars_automatically(df_enc_big): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_categories_in_k_binary_plus_select_vars_automatically(make_df): # test case 1: encode all categories into k binary variables, select variables # automatically + df = make_df(DF_ENC_BIG) encoder = OneHotEncoder(top_categories=None, variables=None, drop_last=False) - X = encoder.fit_transform(df_enc_big) + X = encoder.fit_transform(df) # test init params assert encoder.top_categories is None @@ -35,26 +75,10 @@ def test_encode_categories_in_k_binary_plus_select_vars_automatically(df_enc_big assert encoder.drop_last is False # test fit attr transf = { - "var_A_A": 6, - "var_A_B": 10, - "var_A_C": 4, - "var_A_D": 10, - "var_A_E": 2, - "var_A_F": 2, - "var_A_G": 6, - "var_B_A": 10, - "var_B_B": 6, - "var_B_C": 4, - "var_B_D": 10, - "var_B_E": 2, - "var_B_F": 2, - "var_B_G": 6, - "var_C_A": 4, - "var_C_B": 6, - "var_C_C": 10, - "var_C_D": 10, - "var_C_E": 2, - "var_C_F": 2, + "var_A_A": 6, "var_A_B": 10, "var_A_C": 4, "var_A_D": 10, "var_A_E": 2, + "var_A_F": 2, "var_A_G": 6, "var_B_A": 10, "var_B_B": 6, "var_B_C": 4, + "var_B_D": 10, "var_B_E": 2, "var_B_F": 2, "var_B_G": 6, "var_C_A": 4, + "var_C_B": 6, "var_C_C": 10, "var_C_D": 10, "var_C_E": 2, "var_C_F": 2, "var_C_G": 6, } @@ -67,17 +91,20 @@ def test_encode_categories_in_k_binary_plus_select_vars_automatically(df_enc_big "var_C": ["A", "B", "C", "D", "E", "F", "G"], } # test transform output - assert X.sum().to_dict() == transf - assert "var_A" not in X.columns + for col, expected_sum in transf.items(): + assert _colsum(X, col) == expected_sum + assert "var_A" not in _columns(X) -def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables(df_enc_big): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables(make_df): # test case 2: encode all categories into k-1 binary variables, # pass list of variables + df = make_df(DF_ENC_BIG) encoder = OneHotEncoder( top_categories=None, variables=["var_A", "var_B"], drop_last=True ) - X = encoder.fit_transform(df_enc_big) + X = encoder.fit_transform(df) # test init params assert encoder.top_categories is None @@ -85,18 +112,9 @@ def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables(df_enc_big assert encoder.drop_last is True # test fit attr transf = { - "var_A_A": 6, - "var_A_B": 10, - "var_A_C": 4, - "var_A_D": 10, - "var_A_E": 2, - "var_A_F": 2, - "var_B_A": 10, - "var_B_B": 6, - "var_B_C": 4, - "var_B_D": 10, - "var_B_E": 2, - "var_B_F": 2, + "var_A_A": 6, "var_A_B": 10, "var_A_C": 4, "var_A_D": 10, "var_A_E": 2, + "var_A_F": 2, "var_B_A": 10, "var_B_B": 6, "var_B_C": 4, "var_B_D": 10, + "var_B_E": 2, "var_B_F": 2, } assert encoder.variables_ == ["var_A", "var_B"] @@ -107,41 +125,26 @@ def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables(df_enc_big "var_B": ["A", "B", "C", "D", "E", "F"], } # test transform output + columns = _columns(X) for col in transf.keys(): - assert X[col].sum() == transf[col] - assert "var_B" not in X.columns - assert "var_B_G" not in X.columns - assert "var_C" in X.columns + assert _colsum(X, col) == transf[col] + assert "var_B" not in columns + assert "var_B_G" not in columns + assert "var_C" in columns -def test_encode_top_categories(): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_top_categories(make_df): # test case 3: encode only the most popular 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, - } - ) + data = { + "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, + } + df = make_df(data) encoder = OneHotEncoder(top_categories=4, variables=None, drop_last=False) X = encoder.fit_transform(df) @@ -150,18 +153,9 @@ def test_encode_top_categories(): 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, + "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 @@ -174,10 +168,11 @@ def test_encode_top_categories(): "var_C": ["C", "D", "G", "B"], } # test transform output + columns = _columns(X) 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 + assert _colsum(X, col) == transf[col] + assert "var_B" not in columns + assert "var_B_F" not in columns # init params @@ -199,28 +194,33 @@ def test_raises_error_when_not_allowed_smoothing_param_in_init(drop_binary): OneHotEncoder(drop_last_binary=drop_binary) -def test_raises_error_if_df_contains_na(df_enc_big, df_enc_big_na): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_raises_error_if_df_contains_na(make_df): # test case 4: when dataset contains na, fit method + data_na = dict(DF_ENC_BIG) + data_na["var_A"] = [None] + list(DF_ENC_BIG["var_A"][1:]) + df_na = make_df(data_na) + df = make_df(DF_ENC_BIG) + msg = ( "Some of the variables in the dataset contain NaN. Check and " "remove those before using this transformer." ) encoder = OneHotEncoder() - with pytest.raises(ValueError) as record: - encoder.fit(df_enc_big_na) - - assert str(record.value) == msg + with pytest.raises(ValueError, match=msg): + encoder.fit(df_na) # test case 4: when dataset contains na, transform method encoder = OneHotEncoder() - encoder.fit(df_enc_big) - with pytest.raises(ValueError): - encoder.transform(df_enc_big_na) - assert str(record.value) == msg + encoder.fit(df) + with pytest.raises(ValueError, match=msg): + encoder.transform(df_na) -def test_encode_numerical_variables(df_enc_numeric): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_numerical_variables(make_df): + df = make_df(DF_ENC_NUMERIC) encoder = OneHotEncoder( top_categories=None, variables=None, @@ -228,7 +228,7 @@ def test_encode_numerical_variables(df_enc_numeric): ignore_format=True, ) - X = encoder.fit_transform(df_enc_numeric[["var_A", "var_B"]]) + X = encoder.fit_transform(df) # test fit attr transf = { @@ -240,31 +240,30 @@ def test_encode_numerical_variables(df_enc_numeric): "var_B_3": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], } - transf = pd.DataFrame(transf).astype("int32") - X = pd.DataFrame(X).astype("int32") - assert encoder.variables_ == ["var_A", "var_B"] assert encoder.variables_binary_ == [] assert encoder.n_features_in_ == 2 assert encoder.encoder_dict_ == {"var_A": [1, 2, 3], "var_B": [1, 2, 3]} # test transform output - pd.testing.assert_frame_equal(X, transf) + result = nw.from_native(X, eager_only=True).to_dict(as_series=False) + for col, values in transf.items(): + assert list(result[col]) == values + +def test_variables_cast_as_category(): + # pandas-specific: category dtype has no polars equivalent behavior + # under test here (encoding categorical-dtype columns). + df = pd.DataFrame(DF_ENC_NUMERIC) + df[["var_A", "var_B"]] = df[["var_A", "var_B"]].astype("category") -def test_variables_cast_as_category(df_enc_numeric): encoder = OneHotEncoder( top_categories=None, variables=None, drop_last=False, ignore_format=True, ) + X = encoder.fit_transform(df) - df = df_enc_numeric.copy() - df[["var_A", "var_B"]] = df[["var_A", "var_B"]].astype("category") - - X = encoder.fit_transform(df[["var_A", "var_B"]]) - - # test fit attr transf = { "var_A_1": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "var_A_2": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], @@ -284,26 +283,13 @@ def test_variables_cast_as_category(df_enc_numeric): pd.testing.assert_frame_equal(X, transf) -@pytest.fixture(scope="module") -def df_enc_binary(): - df = { - "var_A": ["A"] * 6 + ["B"] * 10 + ["C"] * 4, - "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, - "var_C": ["AHA"] * 12 + ["UHU"] * 8, - "var_D": ["OHO"] * 5 + ["EHE"] * 15, - "var_num": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], - } - df = pd.DataFrame(df) - - return df - - -def test_encode_into_k_dummy_plus_drop_binary(df_enc_binary): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_into_k_dummy_plus_drop_binary(make_df): + df = make_df(DF_ENC_BINARY) encoder = OneHotEncoder( top_categories=None, variables=None, drop_last=False, drop_last_binary=True ) - X = encoder.fit_transform(df_enc_binary) - X = X.astype("int32") + X = encoder.fit_transform(df) # test fit attr transf = { @@ -317,7 +303,6 @@ def test_encode_into_k_dummy_plus_drop_binary(df_enc_binary): "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], } - transf = pd.DataFrame(transf).astype("int32") assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] assert encoder.variables_binary_ == ["var_C", "var_D"] @@ -329,16 +314,20 @@ def test_encode_into_k_dummy_plus_drop_binary(df_enc_binary): "var_D": ["OHO"], } # test transform output - pd.testing.assert_frame_equal(X, transf) - assert "var_C_B" not in X.columns + result = nw.from_native(X, eager_only=True).to_dict(as_series=False) + assert list(result.keys()) == list(transf.keys()) + for col, values in transf.items(): + assert list(result[col]) == values + assert "var_C_B" not in result.keys() -def test_encode_into_kminus1_dummyy_plus_drop_binary(df_enc_binary): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_into_kminus1_dummyy_plus_drop_binary(make_df): + df = make_df(DF_ENC_BINARY) encoder = OneHotEncoder( top_categories=None, variables=None, drop_last=True, drop_last_binary=True ) - X = encoder.fit_transform(df_enc_binary) - X = X.astype("int32") + X = encoder.fit_transform(df) # test fit attr transf = { @@ -350,7 +339,6 @@ def test_encode_into_kminus1_dummyy_plus_drop_binary(df_enc_binary): "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], } - transf = pd.DataFrame(transf).astype("int32") assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] assert encoder.variables_binary_ == ["var_C", "var_D"] @@ -362,17 +350,21 @@ def test_encode_into_kminus1_dummyy_plus_drop_binary(df_enc_binary): "var_D": ["OHO"], } # test transform output - pd.testing.assert_frame_equal(X, transf) - assert "var_C_B" not in X.columns + result = nw.from_native(X, eager_only=True).to_dict(as_series=False) + assert list(result.keys()) == list(transf.keys()) + for col, values in transf.items(): + assert list(result[col]) == values + assert "var_C_B" not in result.keys() -def test_encode_into_top_categories_plus_drop_binary(df_enc_binary): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_encode_into_top_categories_plus_drop_binary(make_df): + df = make_df(DF_ENC_BINARY) # top_categories = 1 encoder = OneHotEncoder( top_categories=1, variables=None, drop_last=False, drop_last_binary=True ) - X = encoder.fit_transform(df_enc_binary) - X = X.astype("int32") + X = encoder.fit_transform(df) # test fit attr transf = { @@ -382,7 +374,6 @@ def test_encode_into_top_categories_plus_drop_binary(df_enc_binary): "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], } - transf = pd.DataFrame(transf).astype("int32") assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] assert encoder.variables_binary_ == ["var_C", "var_D"] @@ -394,15 +385,17 @@ def test_encode_into_top_categories_plus_drop_binary(df_enc_binary): "var_D": ["OHO"], } # test transform output - pd.testing.assert_frame_equal(X, transf) - assert "var_C_B" not in X.columns + result = nw.from_native(X, eager_only=True).to_dict(as_series=False) + assert list(result.keys()) == list(transf.keys()) + for col, values in transf.items(): + assert list(result[col]) == values + assert "var_C_B" not in result.keys() # top_categories = 2 encoder = OneHotEncoder( top_categories=2, variables=None, drop_last=False, drop_last_binary=True ) - X = encoder.fit_transform(df_enc_binary) - X = X.astype("int32") + X = encoder.fit_transform(df) # test fit attr transf = { @@ -414,7 +407,6 @@ def test_encode_into_top_categories_plus_drop_binary(df_enc_binary): "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], } - transf = pd.DataFrame(transf).astype("int32") assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] assert encoder.variables_binary_ == ["var_C", "var_D"] @@ -426,28 +418,25 @@ def test_encode_into_top_categories_plus_drop_binary(df_enc_binary): "var_D": ["OHO"], } # test transform output - pd.testing.assert_frame_equal(X, transf) - assert "var_C_B" not in X.columns + result = nw.from_native(X, eager_only=True).to_dict(as_series=False) + assert list(result.keys()) == list(transf.keys()) + for col, values in transf.items(): + assert list(result[col]) == values + assert "var_C_B" not in result.keys() -def test_get_feature_names_out(df_enc_binary): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_get_feature_names_out(make_df): + df = make_df(DF_ENC_BINARY) original_features = ["var_num"] - input_features = df_enc_binary.columns + input_features = list(DF_ENC_BINARY.keys()) tr = OneHotEncoder() - tr.fit(df_enc_binary) + tr.fit(df) out = [ - "var_A_A", - "var_A_B", - "var_A_C", - "var_B_A", - "var_B_B", - "var_B_C", - "var_C_AHA", - "var_C_UHU", - "var_D_OHO", - "var_D_EHE", + "var_A_A", "var_A_B", "var_A_C", "var_B_A", "var_B_B", "var_B_C", + "var_C_AHA", "var_C_UHU", "var_D_OHO", "var_D_EHE", ] feat_out = original_features + out @@ -456,33 +445,20 @@ def test_get_feature_names_out(df_enc_binary): assert tr.get_feature_names_out(input_features=input_features) == feat_out tr = OneHotEncoder(drop_last=True) - tr.fit(df_enc_binary) + tr.fit(df) - out = [ - "var_A_A", - "var_A_B", - "var_B_A", - "var_B_B", - "var_C_AHA", - "var_D_OHO", - ] + out = ["var_A_A", "var_A_B", "var_B_A", "var_B_B", "var_C_AHA", "var_D_OHO"] feat_out = original_features + out assert tr.get_feature_names_out(input_features=None) == feat_out assert tr.get_feature_names_out(input_features=input_features) == feat_out tr = OneHotEncoder(drop_last_binary=True) - tr.fit(df_enc_binary) + tr.fit(df) out = [ - "var_A_A", - "var_A_B", - "var_A_C", - "var_B_A", - "var_B_B", - "var_B_C", - "var_C_AHA", - "var_D_OHO", + "var_A_A", "var_A_B", "var_A_C", "var_B_A", "var_B_B", "var_B_C", + "var_C_AHA", "var_D_OHO", ] feat_out = original_features + out @@ -490,7 +466,7 @@ def test_get_feature_names_out(df_enc_binary): assert tr.get_feature_names_out(input_features=input_features) == feat_out tr = OneHotEncoder(top_categories=1) - tr.fit(df_enc_binary) + tr.fit(df) out = ["var_A_B", "var_B_A", "var_C_AHA", "var_D_EHE"] feat_out = original_features + out @@ -505,24 +481,18 @@ def test_get_feature_names_out(df_enc_binary): tr.get_feature_names_out(["var_A", "hola"]) -def test_get_feature_names_out_from_pipeline(df_enc_binary): +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_get_feature_names_out_from_pipeline(make_df): + df = make_df(DF_ENC_BINARY) original_features = ["var_num"] - input_features = df_enc_binary.columns + input_features = list(DF_ENC_BINARY.keys()) tr = Pipeline([("transformer", OneHotEncoder())]) - tr.fit(df_enc_binary) + tr.fit(df) out = [ - "var_A_A", - "var_A_B", - "var_A_C", - "var_B_A", - "var_B_B", - "var_B_C", - "var_C_AHA", - "var_C_UHU", - "var_D_OHO", - "var_D_EHE", + "var_A_A", "var_A_B", "var_A_C", "var_B_A", "var_B_B", "var_B_C", + "var_C_AHA", "var_C_UHU", "var_D_OHO", "var_D_EHE", ] feat_out = original_features + out @@ -530,7 +500,9 @@ def test_get_feature_names_out_from_pipeline(df_enc_binary): assert tr.get_feature_names_out(input_features=input_features) == feat_out -def test_inverse_transform_raises_not_implemented_error(df_enc_binary): - enc = OneHotEncoder().fit(df_enc_binary) +@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) +def test_inverse_transform_raises_not_implemented_error(make_df): + df = make_df(DF_ENC_BINARY) + enc = OneHotEncoder().fit(df) with pytest.raises(NotImplementedError): - enc.inverse_transform(df_enc_binary) + enc.inverse_transform(df) From 0771d5e6a8eda82dadb476f740b4cd0aac17194f Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Mon, 31 Aug 2026 00:10:07 +0200 Subject: [PATCH 2/8] Adapt OneHotEncoder to narwhals-returning check_X Bind check_X / _check_transform_input_and_state results to nw_X and keep the original native X for _check_or_select_variables, _check_contains_na and _get_feature_names_in (those helpers still expect native input, matching the CategoricalImputer migration on narwhals-migration). Drop the now-redundant nw.from_native(X) round-trips in fit() and transform(); they reuse the narwhals frame returned by check_X / _check_transform_input_and_state. Co-Authored-By: Claude Sonnet 5 --- feature_engine/encoding/one_hot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py index 1212a25b1..945f70248 100644 --- a/feature_engine/encoding/one_hot.py +++ b/feature_engine/encoding/one_hot.py @@ -214,11 +214,10 @@ def fit(self, X: IntoDataFrame, y: Optional[IntoDataFrame] = None): None. """ - X = check_X(X) + nw_X = check_X(X) variables_ = self._check_or_select_variables(X) _check_contains_na(X, variables_) - nw_X = nw.from_native(X, eager_only=True) self.encoder_dict_ = {} for var in variables_: @@ -273,12 +272,11 @@ def transform(self, X: IntoDataFrame) -> IntoDataFrame: original categorical ones. """ - X = self._check_transform_input_and_state(X) + nw_X = self._check_transform_input_and_state(X) # check if dataset contains na _check_contains_na(X, self.variables_) - nw_X = nw.from_native(X, eager_only=True) dummy_frames = [] # a placeholder Series name, swapped back out below by a fixed-length # prefix slice (never by parsing the category suffix): to_dummies() From 41e75be5ebdbec589521ce0d00b547f095f79fe7 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Mon, 14 Sep 2026 22:23:24 +0200 Subject: [PATCH 3/8] Use shared backend test fixtures and helpers in OneHotEncoder tests Replace the file-local data dicts and _columns/_colsum helpers with the shared test structure: make_df and data_enc* fixtures, isinstance(X, make_df) plus to_dict() checks (keeping the column-order assertions, which are part of this encoder's output contract), and pytest.raises(match=re.escape(msg)). Co-Authored-By: Claude Opus 5 --- tests/test_encoding/test_onehot_encoder.py | 269 ++++++++------------- 1 file changed, 106 insertions(+), 163 deletions(-) diff --git a/tests/test_encoding/test_onehot_encoder.py b/tests/test_encoding/test_onehot_encoder.py index 726843e0a..55ec0a2bf 100644 --- a/tests/test_encoding/test_onehot_encoder.py +++ b/tests/test_encoding/test_onehot_encoder.py @@ -1,26 +1,13 @@ -import narwhals as nw +import re + import pandas as pd -import polars as pl import pytest from sklearn.pipeline import Pipeline from feature_engine.encoding import OneHotEncoder +from tests.backend_helpers import to_dict -DF_ENC_BIG = { - "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, -} - -DF_ENC_NUMERIC = { - "var_A": [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3], - "var_B": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3], -} - -DF_ENC_BINARY = { +DATA_ENC_BINARY = { "var_A": ["A"] * 6 + ["B"] * 10 + ["C"] * 4, "var_B": ["A"] * 10 + ["B"] * 6 + ["C"] * 4, "var_C": ["AHA"] * 12 + ["UHU"] * 8, @@ -29,18 +16,10 @@ } -def _columns(X): - return list(nw.from_native(X, eager_only=True).columns) - - -def _colsum(X, col): - return sum(nw.from_native(X, eager_only=True).get_column(col).to_list()) - - -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) @pytest.mark.parametrize("index_", [[1, 2, 3], [3, 2, 1], [4, 9, 2]]) def test_concat_with_non_ordered_index(make_df, index_): data = {"varA": ["a", "b", "c"], "varB": ["d", "d", "a"]} + # only pandas has a row index to scramble if make_df is pd.DataFrame: df = make_df(data, index=index_) else: @@ -55,19 +34,18 @@ def test_concat_with_non_ordered_index(make_df, index_): "varB_d": [1, 1, 0], "varB_a": [0, 0, 1], } - result = nw.from_native(dft, eager_only=True).to_dict(as_series=False) - assert list(result.keys()) == list(expected.keys()) - for col, values in expected.items(): - assert list(result[col]) == values + assert isinstance(dft, make_df) + assert list(dft.columns) == list(expected) + assert to_dict(dft) == expected -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_encode_categories_in_k_binary_plus_select_vars_automatically(make_df): +def test_encode_categories_in_k_binary_plus_select_vars_automatically( + make_df, data_enc_big +): # test case 1: encode all categories into k binary variables, select variables # automatically - df = make_df(DF_ENC_BIG) encoder = OneHotEncoder(top_categories=None, variables=None, drop_last=False) - X = encoder.fit_transform(df) + X = encoder.fit_transform(make_df(data_enc_big)) # test init params assert encoder.top_categories is None @@ -91,20 +69,21 @@ def test_encode_categories_in_k_binary_plus_select_vars_automatically(make_df): "var_C": ["A", "B", "C", "D", "E", "F", "G"], } # test transform output - for col, expected_sum in transf.items(): - assert _colsum(X, col) == expected_sum - assert "var_A" not in _columns(X) + assert isinstance(X, make_df) + result = to_dict(X) + assert {col: sum(result[col]) for col in transf} == transf + assert "var_A" not in result -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables(make_df): +def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables( + make_df, data_enc_big +): # test case 2: encode all categories into k-1 binary variables, # pass list of variables - df = make_df(DF_ENC_BIG) encoder = OneHotEncoder( top_categories=None, variables=["var_A", "var_B"], drop_last=True ) - X = encoder.fit_transform(df) + X = encoder.fit_transform(make_df(data_enc_big)) # test init params assert encoder.top_categories is None @@ -125,29 +104,18 @@ def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables(make_df): "var_B": ["A", "B", "C", "D", "E", "F"], } # test transform output - columns = _columns(X) - for col in transf.keys(): - assert _colsum(X, col) == transf[col] - assert "var_B" not in columns - assert "var_B_G" not in columns - assert "var_C" in columns + assert isinstance(X, make_df) + result = to_dict(X) + assert {col: sum(result[col]) for col in transf} == transf + assert "var_B" not in result + assert "var_B_G" not in result + assert result["var_C"] == data_enc_big["var_C"] -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_encode_top_categories(make_df): +def test_encode_top_categories(make_df, data_enc_top): # test case 3: encode only the most popular categories - data = { - "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, - } - df = make_df(data) - encoder = OneHotEncoder(top_categories=4, variables=None, drop_last=False) - X = encoder.fit_transform(df) + X = encoder.fit_transform(make_df(data_enc_top)) # test init params assert encoder.top_categories == 4 @@ -168,11 +136,11 @@ def test_encode_top_categories(make_df): "var_C": ["C", "D", "G", "B"], } # test transform output - columns = _columns(X) - for col in transf.keys(): - assert _colsum(X, col) == transf[col] - assert "var_B" not in columns - assert "var_B_F" not in columns + assert isinstance(X, make_df) + result = to_dict(X) + assert {col: sum(result[col]) for col in transf} == transf + assert "var_B" not in result + assert "var_B_F" not in result # init params @@ -194,33 +162,25 @@ def test_raises_error_when_not_allowed_smoothing_param_in_init(drop_binary): OneHotEncoder(drop_last_binary=drop_binary) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_raises_error_if_df_contains_na(make_df): - # test case 4: when dataset contains na, fit method - data_na = dict(DF_ENC_BIG) - data_na["var_A"] = [None] + list(DF_ENC_BIG["var_A"][1:]) - df_na = make_df(data_na) - df = make_df(DF_ENC_BIG) - +def test_raises_error_if_df_contains_na(make_df, data_enc_big, data_enc_big_na): msg = ( "Some of the variables in the dataset contain NaN. Check and " "remove those before using this transformer." ) + # test case 4: when dataset contains na, fit method encoder = OneHotEncoder() - with pytest.raises(ValueError, match=msg): - encoder.fit(df_na) + with pytest.raises(ValueError, match=re.escape(msg)): + encoder.fit(make_df(data_enc_big_na)) # test case 4: when dataset contains na, transform method encoder = OneHotEncoder() - encoder.fit(df) - with pytest.raises(ValueError, match=msg): - encoder.transform(df_na) + encoder.fit(make_df(data_enc_big)) + with pytest.raises(ValueError, match=re.escape(msg)): + encoder.transform(make_df(data_enc_big_na)) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) -def test_encode_numerical_variables(make_df): - df = make_df(DF_ENC_NUMERIC) +def test_encode_numerical_variables(make_df, data_enc_numeric): encoder = OneHotEncoder( top_categories=None, variables=None, @@ -228,16 +188,16 @@ def test_encode_numerical_variables(make_df): ignore_format=True, ) - X = encoder.fit_transform(df) + X = encoder.fit_transform(make_df(data_enc_numeric)[["var_A", "var_B"]]) # test fit attr transf = { - "var_A_1": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_A_2": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_A_3": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], - "var_B_1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_B_2": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_B_3": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + "var_A_1": [1] * 6 + [0] * 14, + "var_A_2": [0] * 6 + [1] * 10 + [0] * 4, + "var_A_3": [0] * 16 + [1] * 4, + "var_B_1": [1] * 10 + [0] * 10, + "var_B_2": [0] * 10 + [1] * 6 + [0] * 4, + "var_B_3": [0] * 16 + [1] * 4, } assert encoder.variables_ == ["var_A", "var_B"] @@ -245,15 +205,14 @@ def test_encode_numerical_variables(make_df): assert encoder.n_features_in_ == 2 assert encoder.encoder_dict_ == {"var_A": [1, 2, 3], "var_B": [1, 2, 3]} # test transform output - result = nw.from_native(X, eager_only=True).to_dict(as_series=False) - for col, values in transf.items(): - assert list(result[col]) == values + assert isinstance(X, make_df) + assert to_dict(X) == transf -def test_variables_cast_as_category(): +def test_variables_cast_as_category(df_enc_numeric): # pandas-specific: category dtype has no polars equivalent behavior # under test here (encoding categorical-dtype columns). - df = pd.DataFrame(DF_ENC_NUMERIC) + df = df_enc_numeric[["var_A", "var_B"]].copy() df[["var_A", "var_B"]] = df[["var_A", "var_B"]].astype("category") encoder = OneHotEncoder( @@ -265,12 +224,12 @@ def test_variables_cast_as_category(): X = encoder.fit_transform(df) transf = { - "var_A_1": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_A_2": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_A_3": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], - "var_B_1": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_B_2": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_B_3": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + "var_A_1": [1] * 6 + [0] * 14, + "var_A_2": [0] * 6 + [1] * 10 + [0] * 4, + "var_A_3": [0] * 16 + [1] * 4, + "var_B_1": [1] * 10 + [0] * 10, + "var_B_2": [0] * 10 + [1] * 6 + [0] * 4, + "var_B_3": [0] * 16 + [1] * 4, } transf = pd.DataFrame(transf).astype("int32") @@ -283,25 +242,23 @@ def test_variables_cast_as_category(): pd.testing.assert_frame_equal(X, transf) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_encode_into_k_dummy_plus_drop_binary(make_df): - df = make_df(DF_ENC_BINARY) encoder = OneHotEncoder( top_categories=None, variables=None, drop_last=False, drop_last_binary=True ) - X = encoder.fit_transform(df) + X = encoder.fit_transform(make_df(DATA_ENC_BINARY)) # test fit attr transf = { - "var_num": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], - "var_A_A": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_A_B": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_A_C": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], - "var_B_A": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_B_B": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_B_C": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], - "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "var_num": DATA_ENC_BINARY["var_num"], + "var_A_A": [1] * 6 + [0] * 14, + "var_A_B": [0] * 6 + [1] * 10 + [0] * 4, + "var_A_C": [0] * 16 + [1] * 4, + "var_B_A": [1] * 10 + [0] * 10, + "var_B_B": [0] * 10 + [1] * 6 + [0] * 4, + "var_B_C": [0] * 16 + [1] * 4, + "var_C_AHA": [1] * 12 + [0] * 8, + "var_D_OHO": [1] * 5 + [0] * 15, } assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] @@ -314,30 +271,26 @@ def test_encode_into_k_dummy_plus_drop_binary(make_df): "var_D": ["OHO"], } # test transform output - result = nw.from_native(X, eager_only=True).to_dict(as_series=False) - assert list(result.keys()) == list(transf.keys()) - for col, values in transf.items(): - assert list(result[col]) == values - assert "var_C_B" not in result.keys() + assert isinstance(X, make_df) + assert list(X.columns) == list(transf) + assert to_dict(X) == transf -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_encode_into_kminus1_dummyy_plus_drop_binary(make_df): - df = make_df(DF_ENC_BINARY) encoder = OneHotEncoder( top_categories=None, variables=None, drop_last=True, drop_last_binary=True ) - X = encoder.fit_transform(df) + X = encoder.fit_transform(make_df(DATA_ENC_BINARY)) # test fit attr transf = { - "var_num": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], - "var_A_A": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_A_B": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_B_A": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_B_B": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "var_num": DATA_ENC_BINARY["var_num"], + "var_A_A": [1] * 6 + [0] * 14, + "var_A_B": [0] * 6 + [1] * 10 + [0] * 4, + "var_B_A": [1] * 10 + [0] * 10, + "var_B_B": [0] * 10 + [1] * 6 + [0] * 4, + "var_C_AHA": [1] * 12 + [0] * 8, + "var_D_OHO": [1] * 5 + [0] * 15, } assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] @@ -350,16 +303,13 @@ def test_encode_into_kminus1_dummyy_plus_drop_binary(make_df): "var_D": ["OHO"], } # test transform output - result = nw.from_native(X, eager_only=True).to_dict(as_series=False) - assert list(result.keys()) == list(transf.keys()) - for col, values in transf.items(): - assert list(result[col]) == values - assert "var_C_B" not in result.keys() + assert isinstance(X, make_df) + assert list(X.columns) == list(transf) + assert to_dict(X) == transf -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_encode_into_top_categories_plus_drop_binary(make_df): - df = make_df(DF_ENC_BINARY) + df = make_df(DATA_ENC_BINARY) # top_categories = 1 encoder = OneHotEncoder( top_categories=1, variables=None, drop_last=False, drop_last_binary=True @@ -368,11 +318,11 @@ def test_encode_into_top_categories_plus_drop_binary(make_df): # test fit attr transf = { - "var_num": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], - "var_A_B": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_B_A": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "var_num": DATA_ENC_BINARY["var_num"], + "var_A_B": [0] * 6 + [1] * 10 + [0] * 4, + "var_B_A": [1] * 10 + [0] * 10, + "var_C_AHA": [1] * 12 + [0] * 8, + "var_D_OHO": [1] * 5 + [0] * 15, } assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] @@ -385,11 +335,9 @@ def test_encode_into_top_categories_plus_drop_binary(make_df): "var_D": ["OHO"], } # test transform output - result = nw.from_native(X, eager_only=True).to_dict(as_series=False) - assert list(result.keys()) == list(transf.keys()) - for col, values in transf.items(): - assert list(result[col]) == values - assert "var_C_B" not in result.keys() + assert isinstance(X, make_df) + assert list(X.columns) == list(transf) + assert to_dict(X) == transf # top_categories = 2 encoder = OneHotEncoder( @@ -399,13 +347,13 @@ def test_encode_into_top_categories_plus_drop_binary(make_df): # test fit attr transf = { - "var_num": [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0], - "var_A_B": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_A_A": [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_B_A": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - "var_B_B": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], - "var_C_AHA": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - "var_D_OHO": [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "var_num": DATA_ENC_BINARY["var_num"], + "var_A_B": [0] * 6 + [1] * 10 + [0] * 4, + "var_A_A": [1] * 6 + [0] * 14, + "var_B_A": [1] * 10 + [0] * 10, + "var_B_B": [0] * 10 + [1] * 6 + [0] * 4, + "var_C_AHA": [1] * 12 + [0] * 8, + "var_D_OHO": [1] * 5 + [0] * 15, } assert encoder.variables_ == ["var_A", "var_B", "var_C", "var_D"] @@ -418,18 +366,15 @@ def test_encode_into_top_categories_plus_drop_binary(make_df): "var_D": ["OHO"], } # test transform output - result = nw.from_native(X, eager_only=True).to_dict(as_series=False) - assert list(result.keys()) == list(transf.keys()) - for col, values in transf.items(): - assert list(result[col]) == values - assert "var_C_B" not in result.keys() + assert isinstance(X, make_df) + assert list(X.columns) == list(transf) + assert to_dict(X) == transf -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_get_feature_names_out(make_df): - df = make_df(DF_ENC_BINARY) + df = make_df(DATA_ENC_BINARY) original_features = ["var_num"] - input_features = list(DF_ENC_BINARY.keys()) + input_features = list(DATA_ENC_BINARY) tr = OneHotEncoder() tr.fit(df) @@ -481,11 +426,10 @@ def test_get_feature_names_out(make_df): tr.get_feature_names_out(["var_A", "hola"]) -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_get_feature_names_out_from_pipeline(make_df): - df = make_df(DF_ENC_BINARY) + df = make_df(DATA_ENC_BINARY) original_features = ["var_num"] - input_features = list(DF_ENC_BINARY.keys()) + input_features = list(DATA_ENC_BINARY) tr = Pipeline([("transformer", OneHotEncoder())]) tr.fit(df) @@ -500,9 +444,8 @@ def test_get_feature_names_out_from_pipeline(make_df): assert tr.get_feature_names_out(input_features=input_features) == feat_out -@pytest.mark.parametrize("make_df", [pd.DataFrame, pl.DataFrame]) def test_inverse_transform_raises_not_implemented_error(make_df): - df = make_df(DF_ENC_BINARY) + df = make_df(DATA_ENC_BINARY) enc = OneHotEncoder().fit(df) with pytest.raises(NotImplementedError): enc.inverse_transform(df) From 6d2a1f439d7b5d3775b57c8be8990d5638b5f6a9 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 12:01:33 +0200 Subject: [PATCH 4/8] Use frame_to_dict after the shared helper rename in #1045 Co-Authored-By: Claude Opus 5 --- tests/test_encoding/test_onehot_encoder.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_encoding/test_onehot_encoder.py b/tests/test_encoding/test_onehot_encoder.py index 55ec0a2bf..915788585 100644 --- a/tests/test_encoding/test_onehot_encoder.py +++ b/tests/test_encoding/test_onehot_encoder.py @@ -5,7 +5,7 @@ from sklearn.pipeline import Pipeline from feature_engine.encoding import OneHotEncoder -from tests.backend_helpers import to_dict +from tests.backend_helpers import frame_to_dict DATA_ENC_BINARY = { "var_A": ["A"] * 6 + ["B"] * 10 + ["C"] * 4, @@ -36,7 +36,7 @@ def test_concat_with_non_ordered_index(make_df, index_): } assert isinstance(dft, make_df) assert list(dft.columns) == list(expected) - assert to_dict(dft) == expected + assert frame_to_dict(dft) == expected def test_encode_categories_in_k_binary_plus_select_vars_automatically( @@ -70,7 +70,7 @@ def test_encode_categories_in_k_binary_plus_select_vars_automatically( } # test transform output assert isinstance(X, make_df) - result = to_dict(X) + result = frame_to_dict(X) assert {col: sum(result[col]) for col in transf} == transf assert "var_A" not in result @@ -105,7 +105,7 @@ def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables( } # test transform output assert isinstance(X, make_df) - result = to_dict(X) + result = frame_to_dict(X) assert {col: sum(result[col]) for col in transf} == transf assert "var_B" not in result assert "var_B_G" not in result @@ -137,7 +137,7 @@ def test_encode_top_categories(make_df, data_enc_top): } # test transform output assert isinstance(X, make_df) - result = to_dict(X) + result = frame_to_dict(X) assert {col: sum(result[col]) for col in transf} == transf assert "var_B" not in result assert "var_B_F" not in result @@ -206,7 +206,7 @@ def test_encode_numerical_variables(make_df, data_enc_numeric): assert encoder.encoder_dict_ == {"var_A": [1, 2, 3], "var_B": [1, 2, 3]} # test transform output assert isinstance(X, make_df) - assert to_dict(X) == transf + assert frame_to_dict(X) == transf def test_variables_cast_as_category(df_enc_numeric): @@ -273,7 +273,7 @@ def test_encode_into_k_dummy_plus_drop_binary(make_df): # test transform output assert isinstance(X, make_df) assert list(X.columns) == list(transf) - assert to_dict(X) == transf + assert frame_to_dict(X) == transf def test_encode_into_kminus1_dummyy_plus_drop_binary(make_df): @@ -305,7 +305,7 @@ def test_encode_into_kminus1_dummyy_plus_drop_binary(make_df): # test transform output assert isinstance(X, make_df) assert list(X.columns) == list(transf) - assert to_dict(X) == transf + assert frame_to_dict(X) == transf def test_encode_into_top_categories_plus_drop_binary(make_df): @@ -337,7 +337,7 @@ def test_encode_into_top_categories_plus_drop_binary(make_df): # test transform output assert isinstance(X, make_df) assert list(X.columns) == list(transf) - assert to_dict(X) == transf + assert frame_to_dict(X) == transf # top_categories = 2 encoder = OneHotEncoder( @@ -368,7 +368,7 @@ def test_encode_into_top_categories_plus_drop_binary(make_df): # test transform output assert isinstance(X, make_df) assert list(X.columns) == list(transf) - assert to_dict(X) == transf + assert frame_to_dict(X) == transf def test_get_feature_names_out(make_df): From 6cd4618a6077f1957031ee448d7e669536cfb9ce Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 13:38:51 +0200 Subject: [PATCH 5/8] Group OneHotEncoder init tests, match errors, shorten comments Co-Authored-By: Claude Opus 5 --- feature_engine/encoding/one_hot.py | 16 +--- tests/test_encoding/test_onehot_encoder.py | 86 +++++++++++++--------- 2 files changed, 57 insertions(+), 45 deletions(-) diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py index 945f70248..6c17a7c76 100644 --- a/feature_engine/encoding/one_hot.py +++ b/feature_engine/encoding/one_hot.py @@ -278,11 +278,8 @@ def transform(self, X: IntoDataFrame) -> IntoDataFrame: _check_contains_na(X, self.variables_) dummy_frames = [] - # a placeholder Series name, swapped back out below by a fixed-length - # prefix slice (never by parsing the category suffix): to_dummies() - # only prefixes with the Series name when it's truthy, so a falsy - # real name (e.g. an int column literally named 0) would otherwise - # silently drop the prefix and make every dummy column look "missing". + # to_dummies() skips the prefix for falsy names (e.g. a column called 0), so + # use a placeholder name and swap in the real one by slicing its length tmp_name = "__ohe_tmp__" for feature in self.variables_: desired = [ @@ -294,13 +291,8 @@ def transform(self, X: IntoDataFrame) -> IntoDataFrame: dummies = dummies.rename( {c: f"{feature}{c[len(tmp_name):]}" for c in dummies.columns} ) - # categories learned in fit() but absent from, or unseen categories - # present in, this particular transform batch: to_dummies() only - # creates columns for values it actually finds, so any learned - # category missing here is filled with an all-0 column, and - # selecting just `desired` drops any column for a category that - # wasn't learned (unseen categories are encoded as 0 across the - # board, matching the pre-narwhals behaviour). + # add all-0 columns for learned categories missing in X, and drop the + # columns of unseen categories, so these are encoded as 0 missing = [c for c in desired if c not in dummies.columns] if len(missing) > 0: dummies = dummies.with_columns( diff --git a/tests/test_encoding/test_onehot_encoder.py b/tests/test_encoding/test_onehot_encoder.py index 915788585..a925dcab6 100644 --- a/tests/test_encoding/test_onehot_encoder.py +++ b/tests/test_encoding/test_onehot_encoder.py @@ -16,6 +16,53 @@ } +# init parameters +@pytest.mark.parametrize("top_cat", ["empanada", [1], 0.5, -1]) +def test_error_if_top_categories_not_integer(top_cat): + msg = f"top_categories takes only positive integers. Got {top_cat} instead" + with pytest.raises(ValueError, match=re.escape(msg)): + OneHotEncoder(top_categories=top_cat) + + +@pytest.mark.parametrize("drop_last", ["empanada", [1], 0.5, -1, 1, None]) +def test_error_if_drop_last_not_bool(drop_last): + msg = f"drop_last takes only True or False. Got {drop_last} instead." + with pytest.raises(ValueError, match=re.escape(msg)): + OneHotEncoder(drop_last=drop_last) + + +@pytest.mark.parametrize("drop_binary", ["hello", ["auto"], -1, 100, 0.5, None]) +def test_error_if_drop_last_binary_not_bool(drop_binary): + msg = f"drop_last_binary takes only True or False. Got {drop_binary} instead." + with pytest.raises(ValueError, match=re.escape(msg)): + OneHotEncoder(drop_last_binary=drop_binary) + + +@pytest.mark.parametrize( + "top_categories, drop_last, drop_last_binary, ignore_format", + [ + (None, False, False, False), + (1, True, False, True), + (10, False, True, False), + (0, True, True, True), + ], +) +def test_init_param_assignment( + top_categories, drop_last, drop_last_binary, ignore_format +): + encoder = OneHotEncoder( + top_categories=top_categories, + drop_last=drop_last, + drop_last_binary=drop_last_binary, + ignore_format=ignore_format, + ) + assert encoder.top_categories == top_categories + assert encoder.drop_last is drop_last + assert encoder.drop_last_binary is drop_last_binary + assert encoder.ignore_format is ignore_format + + +# fit and transform @pytest.mark.parametrize("index_", [[1, 2, 3], [3, 2, 1], [4, 9, 2]]) def test_concat_with_non_ordered_index(make_df, index_): data = {"varA": ["a", "b", "c"], "varB": ["d", "d", "a"]} @@ -47,10 +94,6 @@ def test_encode_categories_in_k_binary_plus_select_vars_automatically( encoder = OneHotEncoder(top_categories=None, variables=None, drop_last=False) X = encoder.fit_transform(make_df(data_enc_big)) - # test init params - assert encoder.top_categories is None - assert encoder.variables is None - assert encoder.drop_last is False # test fit attr transf = { "var_A_A": 6, "var_A_B": 10, "var_A_C": 4, "var_A_D": 10, "var_A_E": 2, @@ -85,10 +128,6 @@ def test_encode_categories_in_k_minus_1_binary_plus_list_of_variables( ) X = encoder.fit_transform(make_df(data_enc_big)) - # test init params - assert encoder.top_categories is None - assert encoder.variables == ["var_A", "var_B"] - assert encoder.drop_last is True # test fit attr transf = { "var_A_A": 6, "var_A_B": 10, "var_A_C": 4, "var_A_D": 10, "var_A_E": 2, @@ -117,9 +156,6 @@ def test_encode_top_categories(make_df, data_enc_top): encoder = OneHotEncoder(top_categories=4, variables=None, drop_last=False) X = encoder.fit_transform(make_df(data_enc_top)) - # 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, @@ -143,25 +179,6 @@ def test_encode_top_categories(make_df, data_enc_top): assert "var_B_F" not in result -# init params -@pytest.mark.parametrize("top_cat", ["empanada", [1], 0.5, -1]) -def test_error_if_top_categories_not_integer(top_cat): - with pytest.raises(ValueError): - OneHotEncoder(top_categories=top_cat) - - -@pytest.mark.parametrize("drop_last", ["empanada", [1], 0.5, -1, 1]) -def test_error_if_drop_last_not_bool(drop_last): - with pytest.raises(ValueError): - OneHotEncoder(drop_last=drop_last) - - -@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_raises_error_if_df_contains_na(make_df, data_enc_big, data_enc_big_na): msg = ( "Some of the variables in the dataset contain NaN. Check and " @@ -419,10 +436,12 @@ def test_get_feature_names_out(make_df): assert tr.get_feature_names_out(input_features=None) == feat_out assert tr.get_feature_names_out(input_features=input_features) == feat_out - with pytest.raises(ValueError): + msg = "input_features must be a list or an array. Got {input_features} instead." + with pytest.raises(ValueError, match=re.escape(msg)): tr.get_feature_names_out("var_A") - with pytest.raises(ValueError): + msg = "input_features is not equal to feature_names_in_" + with pytest.raises(ValueError, match=re.escape(msg)): tr.get_feature_names_out(["var_A", "hola"]) @@ -447,5 +466,6 @@ def test_get_feature_names_out_from_pipeline(make_df): def test_inverse_transform_raises_not_implemented_error(make_df): df = make_df(DATA_ENC_BINARY) enc = OneHotEncoder().fit(df) - with pytest.raises(NotImplementedError): + msg = "inverse_transform is not implemented for this transformer." + with pytest.raises(NotImplementedError, match=re.escape(msg)): enc.inverse_transform(df) From db497cb29070dbd2bcb50458d34194d16b77ab75 Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 13:47:51 +0200 Subject: [PATCH 6/8] Match the fixed get_feature_names_out error message Co-Authored-By: Claude Opus 5 --- tests/test_encoding/test_onehot_encoder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_encoding/test_onehot_encoder.py b/tests/test_encoding/test_onehot_encoder.py index a925dcab6..5ff6f1a84 100644 --- a/tests/test_encoding/test_onehot_encoder.py +++ b/tests/test_encoding/test_onehot_encoder.py @@ -436,7 +436,7 @@ def test_get_feature_names_out(make_df): assert tr.get_feature_names_out(input_features=None) == feat_out assert tr.get_feature_names_out(input_features=input_features) == feat_out - msg = "input_features must be a list or an array. Got {input_features} instead." + msg = "input_features must be a list or an array. Got var_A instead." with pytest.raises(ValueError, match=re.escape(msg)): tr.get_feature_names_out("var_A") From fad196331e860e2ca50745b4f52c733fa288159a Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 19:13:55 +0200 Subject: [PATCH 7/8] refactor ohe --- feature_engine/encoding/one_hot.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py index 6c17a7c76..d1e6e5aae 100644 --- a/feature_engine/encoding/one_hot.py +++ b/feature_engine/encoding/one_hot.py @@ -218,28 +218,21 @@ def fit(self, X: IntoDataFrame, y: Optional[IntoDataFrame] = None): variables_ = self._check_or_select_variables(X) _check_contains_na(X, variables_) - self.encoder_dict_ = {} + encoder_dict_ = {} for var in variables_: col = nw_X.get_column(var) - # make dummies only for the most popular categories - if self.top_categories: + if self.top_categories is not None: top = col.value_counts(sort=True, name="count").head( self.top_categories ) - self.encoder_dict_[var] = top.get_column(var).to_list() + encoder_dict_[var] = top.get_column(var).to_list() else: category_ls = col.unique(maintain_order=True).to_list() - - # return k-1 dummies - if self.drop_last is True: - self.encoder_dict_[var] = category_ls[:-1] - - # return k dummies - else: - self.encoder_dict_[var] = category_ls + # return k-1 vs k dummies + encoder_dict_[var] = category_ls[:-1] if self.drop_last is True else category_ls self.variables_binary_ = [ var for var in variables_ if nw_X.get_column(var).n_unique() == 2 @@ -249,9 +242,10 @@ def fit(self, X: IntoDataFrame, y: Optional[IntoDataFrame] = None): if self.drop_last_binary is True: for var in self.variables_binary_: category = nw_X.get_column(var).unique(maintain_order=True)[0] - self.encoder_dict_[var] = [category] + encoder_dict_[var] = [category] self.variables_ = variables_ + self.encoder_dict_ = encoder_dict_ self._get_feature_names_in(X) return self From ea5343441a7a6513de13e65199e9a8e5e4c7d06a Mon Sep 17 00:00:00 2001 From: Soledad Galli Date: Tue, 15 Sep 2026 19:19:29 +0200 Subject: [PATCH 8/8] fix code style --- feature_engine/encoding/one_hot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/feature_engine/encoding/one_hot.py b/feature_engine/encoding/one_hot.py index d1e6e5aae..2fae8cfae 100644 --- a/feature_engine/encoding/one_hot.py +++ b/feature_engine/encoding/one_hot.py @@ -232,7 +232,9 @@ def fit(self, X: IntoDataFrame, y: Optional[IntoDataFrame] = None): else: category_ls = col.unique(maintain_order=True).to_list() # return k-1 vs k dummies - encoder_dict_[var] = category_ls[:-1] if self.drop_last is True else category_ls + encoder_dict_[var] = ( + category_ls[:-1] if self.drop_last is True else category_ls + ) self.variables_binary_ = [ var for var in variables_ if nw_X.get_column(var).n_unique() == 2 @@ -279,9 +281,7 @@ def transform(self, X: IntoDataFrame) -> IntoDataFrame: desired = [ f"{feature}_{category}" for category in self.encoder_dict_[feature] ] - dummies = ( - nw_X.get_column(feature).alias(tmp_name).to_dummies(separator="_") - ) + dummies = nw_X.get_column(feature).alias(tmp_name).to_dummies(separator="_") dummies = dummies.rename( {c: f"{feature}{c[len(tmp_name):]}" for c in dummies.columns} )