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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/core_concepts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ strategy strings is in the [representation comparison](../representations/compar

`Preprocessor` also accepts a plain `numpy.ndarray`, not just a `DataFrame`. Columns are
named `feature_0`, `feature_1`, ... in position order, then detected as numerical or
categorical exactly as they would be for a `DataFrame`.
categorical exactly as they would be for a `DataFrame`. Because array columns are matched by
position, `transform` raises the usual scikit-learn error
(`X has 4 features, but Preprocessor is expecting 3 features as input.`) when an array has a
different number of columns than the one seen at `fit`.

```python
import numpy as np
Expand Down
15 changes: 15 additions & 0 deletions pretab/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,10 +632,25 @@ def transform(self, X, embeddings=None, return_array=None):
(or ``return_array=False``); a SciPy CSR matrix (or CSR blocks) when
``output_format`` resolves to ``"sparse"``; or a pandas / polars DataFrame
when configured via :meth:`set_output`.

Raises
------
PretabDataError
If ``X`` is a NumPy array whose number of columns differs from
``n_features_in_``. Array columns are matched by position, so such an
array can never be routed correctly.
"""

check_is_fitted(self)

# Array columns are matched by position, so a different width can never be
# routed correctly; an extra column would silently shift or drop features.
if isinstance(X, np.ndarray) and X.ndim == 2 and X.shape[1] != self.n_features_in_:
raise PretabDataError(
f"X has {X.shape[1]} features, but {type(self).__name__} is expecting "
f"{self.n_features_in_} features as input."
)

X = to_dataframe(X, copy=True)

if self.missing_policy == "error":
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/test_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,23 @@ def test_unknown_feature_preprocessing_key_raises(sample_data):
X, y = sample_data
with pytest.raises(InvalidParamError, match="feature_preprocessing"):
Preprocessor(feature_preprocessing={"nnum1": "minmax"}).fit(X, y)


# --- ndarray width check at transform (issue #72) ---------------------------------


@pytest.mark.parametrize("width", [2, 4])
def test_transform_rejects_an_ndarray_of_a_different_width(width):
"""Regression guard for issue #72: an extra array column silently shifted features."""
rng = np.random.default_rng(0)
X, y = rng.normal(size=(60, 3)), rng.normal(size=60)
pre = Preprocessor(random_state=0).fit(X, y)
with pytest.raises(ValueError, match=f"X has {width} features, but Preprocessor is expecting 3"):
pre.transform(rng.normal(size=(5, width)))


def test_transform_accepts_an_ndarray_of_the_fitted_width():
rng = np.random.default_rng(0)
X, y = rng.normal(size=(60, 3)), rng.normal(size=60)
pre = Preprocessor(random_state=0).fit(X, y)
assert np.asarray(pre.transform(X)).shape[0] == 60
Loading