From 49863eb95544a89ffa7f40b411944a845877455d Mon Sep 17 00:00:00 2001 From: ChrisW09 <50968720+ChrisW09@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:45:58 +0200 Subject: [PATCH] fix(preprocessor): raise a clear error on duplicate column names Fitting on a DataFrame with a repeated column label failed with AttributeError: 'DataFrame' object has no attribute 'dtype' from inside ``_detect_column_types``, with nothing pointing at the cause: ``X[col]`` returns a DataFrame rather than a Series for a duplicated label, so the dtype inspection has nothing to read. (``nunique()`` on the line above happens to work on a frame, which is why it surfaced one line later.) Check for duplicates up front and raise ``PretabDataError`` naming them. The ColumnTransformer this builds keys its transformers by column name, so duplicates could not be routed unambiguously even if detection coped with them -- rejecting them early is the honest outcome. Closes #37 Co-Authored-By: Claude Opus 5 (1M context) --- pretab/preprocessor.py | 13 +++++++++++++ tests/test_exceptions.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/pretab/preprocessor.py b/pretab/preprocessor.py index fcf228a..f11bbc0 100644 --- a/pretab/preprocessor.py +++ b/pretab/preprocessor.py @@ -9,6 +9,7 @@ from .core.exceptions import ( IncompatibleParamsError, + PretabDataError, invalid_param_error, ) from .core.logging import configure_logging, get_logger @@ -285,6 +286,18 @@ def _detect_column_types(self, X): elif isinstance(X, np.ndarray): X = pd.DataFrame(X, columns=[f"feature_{i}" for i in range(X.shape[1])]) + # ``X[col]`` returns a DataFrame rather than a Series for a duplicated + # label, so the dtype inspection below fails with an opaque + # ``AttributeError``. The ColumnTransformer this builds also keys its + # transformers by column name, so duplicates could not be routed even if + # detection coped with them. + duplicated = X.columns[X.columns.duplicated()].unique().tolist() + if duplicated: + raise PretabDataError( + f"Duplicate column names are not supported: {duplicated}.\n" + "Fix: rename the columns so every name is unique." + ) + for col in X.columns: num_unique_values = X[col].nunique() total_samples = len(X[col]) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 909456a..a9930bb 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -249,3 +249,36 @@ def test_preprocessor_unknown_categorical_method(): with pytest.raises(InvalidParamError) as exc: Preprocessor(categorical_method="bogus").fit(df, y) assert isinstance(exc.value, ValueError) + + +# --------------------------------------------------------------------------- # +# Duplicate column names must fail with a clear, actionable error. +# +# ``X[col]`` returns a DataFrame rather than a Series for a duplicated label, so +# detection died on ``.dtype`` with an opaque AttributeError from pandas. +# --------------------------------------------------------------------------- # +def test_duplicate_column_names_raise_a_clear_error(): + rng = np.random.default_rng(0) + frame = pd.DataFrame(np.column_stack([rng.normal(size=50)] * 2), columns=pd.Index(["a", "a"])) + + with pytest.raises(PretabDataError, match=r"Duplicate column names are not supported: \['a'\]"): + Preprocessor(numerical_method="minmax").fit(frame, rng.normal(size=50)) + + +def test_duplicate_column_names_lists_every_offender(): + rng = np.random.default_rng(0) + frame = pd.DataFrame(rng.normal(size=(50, 4)), columns=pd.Index(["a", "a", "b", "b"])) + + with pytest.raises(PretabDataError) as excinfo: + Preprocessor(numerical_method="minmax").fit(frame, rng.normal(size=50)) + + assert "'a'" in str(excinfo.value) and "'b'" in str(excinfo.value) + + +def test_unique_column_names_are_unaffected(): + rng = np.random.default_rng(0) + frame = pd.DataFrame(rng.normal(size=(50, 2)), columns=pd.Index(["a", "b"])) + + pre = Preprocessor(numerical_method="minmax").fit(frame, rng.normal(size=50)) + + assert sorted(pre.output_dims_) == ["a", "b"]