Skip to content

Commit 7f97dba

Browse files
authored
FIX SMOTENC: clear error for column-dropping categorical encoders (#1035) (#1177)
1 parent de97df0 commit 7f97dba

3 files changed

Lines changed: 59 additions & 0 deletions

File tree

doc/whats_new/v0.15.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ Changelog
1111
Bug fixes
1212
.........
1313

14+
- Raise an informative error in :class:`~imblearn.over_sampling.SMOTENC` when the
15+
provided `categorical_encoder` does not keep one column per category (e.g.
16+
`OneHotEncoder(drop=...)` or an encoder merging infrequent categories), instead
17+
of failing with an opaque ``zero-size array`` NumPy error.
18+
:issue:`1035` by :user:`Imran Ahamed <immu4989>`.
19+
1420
Enhancements
1521
............
1622

imblearn/over_sampling/_smote/base.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,26 @@ def _fit_resample(self, X, y):
619619
if not sparse.issparse(X_ohe):
620620
X_ohe = sparse.csr_matrix(X_ohe, dtype=dtype_ohe)
621621

622+
# SMOTENC reconstructs the categorical features by activating exactly one
623+
# column per categorical feature (see `_generate_samples`). This requires
624+
# a complete one-hot encoding with one column per category. Encoders that
625+
# emit fewer columns -- e.g. ``OneHotEncoder(drop=...)`` or one merging
626+
# infrequent categories -- break this assumption and previously raised an
627+
# opaque "zero-size array" error or silently produced wrong categories.
628+
n_categories = sum(
629+
categories.size for categories in self.categorical_encoder_.categories_
630+
)
631+
if X_ohe.shape[1] != n_categories:
632+
raise ValueError(
633+
"SMOTENC requires a one-hot encoding with one column per category "
634+
"for the categorical features. The provided `categorical_encoder` "
635+
f"produced {X_ohe.shape[1]} columns for {n_categories} categories. "
636+
"This happens when the encoder drops columns (e.g. "
637+
"`OneHotEncoder(drop=...)`) or merges infrequent categories, which "
638+
"is not supported. Pass an encoder that keeps all categories, such "
639+
"as `OneHotEncoder(handle_unknown='ignore')`."
640+
)
641+
622642
X_encoded = sparse.hstack((X_continuous, X_ohe), format="csr", dtype=dtype_ohe)
623643
X_resampled = [X_encoded.copy()]
624644
y_resampled = [y.copy()]

imblearn/over_sampling/_smote/tests/test_smote_nc.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,39 @@ def test_smotenc_categorical_encoder():
277277
assert getattr(smote.categorical_encoder_, "sparse_output") is False
278278

279279

280+
@pytest.mark.parametrize("drop", ["first", "if_binary"])
281+
def test_smotenc_categorical_encoder_dropped_columns(drop):
282+
"""Check that a clear error is raised when the categorical encoder does not
283+
keep one column per category (e.g. ``OneHotEncoder(drop=...)``).
284+
285+
Non-regression test for:
286+
https://github.com/scikit-learn-contrib/imbalanced-learn/issues/1035
287+
"""
288+
rng = np.random.RandomState(0)
289+
n_samples = 200
290+
X = np.hstack(
291+
[
292+
rng.randn(n_samples, 2),
293+
rng.randint(0, 2, size=(n_samples, 1)), # binary categorical
294+
rng.randint(0, 4, size=(n_samples, 1)),
295+
rng.randint(0, 3, size=(n_samples, 1)),
296+
]
297+
).astype(object)
298+
y = np.array([1] * 40 + [0] * (n_samples - 40))
299+
rng.shuffle(y)
300+
301+
encoder = OneHotEncoder(drop=drop, handle_unknown="ignore")
302+
smote = SMOTENC(
303+
categorical_features=[2, 3, 4],
304+
categorical_encoder=encoder,
305+
sampling_strategy="minority",
306+
random_state=0,
307+
)
308+
err_msg = "SMOTENC requires a one-hot encoding with one column per category"
309+
with pytest.raises(ValueError, match=err_msg):
310+
smote.fit_resample(X, y)
311+
312+
280313
def test_smotenc_bool_categorical():
281314
"""Check that we don't try to early convert the full input data to numeric when
282315
handling a pandas dataframe.

0 commit comments

Comments
 (0)