Skip to content

Commit e295a4e

Browse files
committed
FIX KMeansSMOTE: uniform weights for degenerate clusters
When every sample within each valid cluster is identical, all cluster sparsities are 0, so dividing by their (zero) sum produced NaN weights and crashed with 'ValueError: cannot convert float NaN to integer' in math.ceil. Fall back to uniform weighting across the valid clusters. Closes #1186 (issue 1).
1 parent 8504e95 commit e295a4e

3 files changed

Lines changed: 40 additions & 1 deletion

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+
- Fix a bug in :class:`~imblearn.over_sampling.KMeansSMOTE` where degenerate
15+
clusters -- every sample identical, hence zero sparsity -- produced ``NaN``
16+
cluster weights and crashed with ``ValueError: cannot convert float NaN to
17+
integer``. Such clusters are now weighted uniformly.
18+
:pr:`__PRNUM__` by :user:`chaoz23 <chaoz23>`.
19+
1420
Enhancements
1521
............
1622

imblearn/over_sampling/_smote/cluster.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ def _fit_resample(self, X, y):
264264
cluster_sparsities.append(self._find_cluster_sparsity(X_cluster_class))
265265

266266
cluster_sparsities = np.array(cluster_sparsities)
267-
cluster_weights = cluster_sparsities / cluster_sparsities.sum()
268267

269268
if not valid_clusters:
270269
raise RuntimeError(
@@ -274,6 +273,19 @@ def _fit_resample(self, X, y):
274273
"clusters."
275274
)
276275

276+
sparsity_sum = cluster_sparsities.sum()
277+
if sparsity_sum == 0:
278+
# All valid clusters are degenerate: every sample within each
279+
# cluster is identical, so the pairwise distances -- and hence
280+
# the sparsities -- are all zero. Dividing by the (zero) sum
281+
# would yield NaN weights and later crash ``math.ceil``. Fall
282+
# back to weighting the valid clusters uniformly.
283+
cluster_weights = np.full(
284+
len(cluster_sparsities), 1 / len(cluster_sparsities)
285+
)
286+
else:
287+
cluster_weights = cluster_sparsities / sparsity_sum
288+
277289
for valid_cluster_idx, valid_cluster in enumerate(valid_clusters):
278290
X_cluster = _safe_indexing(X, valid_cluster)
279291
y_cluster = _safe_indexing(y, valid_cluster)

imblearn/over_sampling/_smote/tests/test_kmeans_smote.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ def test_sample_kmeans_not_enough_clusters(data):
9393
smote.fit_resample(X, y)
9494

9595

96+
def test_sample_kmeans_degenerate_clusters():
97+
"""Check that identical samples within every valid cluster do not raise.
98+
99+
Non-regression test for
100+
https://github.com/scikit-learn-contrib/imbalanced-learn/issues/1186:
101+
when all valid clusters are degenerate (every point identical), their
102+
sparsities are all zero and the weights used to be ``NaN``, crashing
103+
``math.ceil`` with ``ValueError: cannot convert float NaN to integer``.
104+
"""
105+
X = np.array([[1.0, 1.0]] * 4 + [[2.0, 2.0]] * 4 + [[3.0, 3.0]] * 4)
106+
y = np.array([1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0])
107+
smote = KMeansSMOTE(
108+
kmeans_estimator=KMeans(n_clusters=2, random_state=42),
109+
random_state=42,
110+
cluster_balance_threshold=0.1,
111+
)
112+
X_res, y_res = smote.fit_resample(X, y)
113+
# the minority class has been oversampled up to the majority count
114+
assert (y_res == 1).sum() == (y_res == 0).sum()
115+
116+
96117
@pytest.mark.parametrize("density_exponent", ["auto", 10])
97118
@pytest.mark.parametrize("cluster_balance_threshold", ["auto", 0.1])
98119
def test_sample_kmeans_density_estimation(density_exponent, cluster_balance_threshold):

0 commit comments

Comments
 (0)