Skip to content

Commit 4edca32

Browse files
authored
Merge pull request #719 from bishtashish708/fix/cblof-n-jobs-attribute
fix(cblof): store n_jobs so get_params() and clone() carry it over
2 parents 57a4688 + e988564 commit 4edca32

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

pyod/models/cblof.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# License: BSD 2 clause
77

88

9+
import inspect
910
import warnings
1011

1112
import numpy as np
@@ -92,10 +93,14 @@ class CBLOF(BaseDetector):
9293
RandomState instance used by `np.random`.
9394
9495
n_jobs : int, optional (default=1)
95-
Accepted for API compatibility but currently unused: the value is
96-
neither stored on the estimator nor forwarded to the clustering
97-
step, so ``get_params()`` reports ``None`` for it and ``clone()``
98-
does not carry it over. See issue #713.
96+
Number of parallel jobs for the default KMeans backend.
97+
Only forwarded to ``KMeans`` when set to a value other than 1
98+
*and* the installed scikit-learn version supports it (the
99+
parameter was deprecated in 0.23 and removed in 0.25).
100+
On sklearn >= 0.25, or when left at the default of 1, this value
101+
is stored for ``get_params()`` / ``clone()`` compatibility only.
102+
Has no effect when a custom ``clustering_estimator`` is provided;
103+
set ``n_jobs`` on that estimator directly.
99104
100105
Attributes
101106
----------
@@ -149,6 +154,7 @@ def __init__(self, n_clusters=8, contamination=0.1,
149154
self.use_weights = use_weights
150155
self.check_estimator = check_estimator
151156
self.random_state = random_state
157+
self.n_jobs = n_jobs
152158

153159
# noinspection PyIncorrectDocstring
154160
def fit(self, X, y=None):
@@ -175,9 +181,17 @@ def fit(self, X, y=None):
175181

176182
# check parameters
177183
# number of clusters are default to 8
178-
self._validate_estimator(default=KMeans(
179-
n_clusters=self.n_clusters,
180-
random_state=self.random_state))
184+
_kmeans_kwargs = dict(
185+
n_clusters=self.n_clusters, random_state=self.random_state)
186+
# n_jobs was removed from KMeans in sklearn 0.25; on 0.23/0.24 it is
187+
# deprecated and emits FutureWarning for any concrete value, including
188+
# the default 1. Only forward when the user explicitly requested more
189+
# than one job so that plain CBLOF().fit(X) stays warning-free.
190+
if (self.clustering_estimator is None
191+
and self.n_jobs != 1
192+
and "n_jobs" in inspect.signature(KMeans.__init__).parameters):
193+
_kmeans_kwargs["n_jobs"] = self.n_jobs
194+
self._validate_estimator(default=KMeans(**_kmeans_kwargs))
181195

182196
self.clustering_estimator_.fit(X=X, y=y)
183197
# Get the labels of the clustering results

pyod/test/test_cblof.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,28 @@ def test_predict_rank_normalized(self):
166166
def test_model_clone(self):
167167
clone_clf = clone(self.clf)
168168

169+
def test_n_jobs_stored(self):
170+
# n_jobs must be stored so get_params() and clone() carry it over
171+
clf = CBLOF(n_jobs=4)
172+
assert clf.n_jobs == 4
173+
assert clf.get_params()['n_jobs'] == 4
174+
175+
def test_n_jobs_clone(self):
176+
clf = CBLOF(n_jobs=4)
177+
cloned = clone(clf)
178+
assert cloned.n_jobs == 4
179+
180+
def test_n_jobs_fit(self):
181+
# CBLOF(n_jobs=4) must fit and produce results identical to n_jobs=1
182+
clf_single = CBLOF(contamination=self.contamination,
183+
random_state=42, n_jobs=1)
184+
clf_multi = CBLOF(contamination=self.contamination,
185+
random_state=42, n_jobs=4)
186+
clf_single.fit(self.X_train)
187+
clf_multi.fit(self.X_train)
188+
assert clf_multi.n_jobs == 4
189+
assert_equal(len(clf_multi.decision_scores_), self.X_train.shape[0])
190+
169191
def tearDown(self):
170192
pass
171193

0 commit comments

Comments
 (0)