Aluode/PerceptionLabPortable
0
1from re import escape
2
3import numpy as np
4import pytest
5import scipy.sparse as sp
6from numpy.testing import assert_allclose
7
8from sklearn import datasets, svm
9from sklearn.base import BaseEstimator, ClassifierMixin
10from sklearn.datasets import load_breast_cancer
11from sklearn.exceptions import NotFittedError
12from sklearn.impute import SimpleImputer
13from sklearn.linear_model import (
14 ElasticNet,
15 Lasso,
16 LinearRegression,
17 LogisticRegression,
18 Perceptron,
19 Ridge,
20 SGDClassifier,
21)
22from sklearn.metrics import precision_score, recall_score
23from sklearn.model_selection import GridSearchCV, cross_val_score
24from sklearn.multiclass import (
25 OneVsOneClassifier,
26 OneVsRestClassifier,
27 OutputCodeClassifier,
28)
29from sklearn.naive_bayes import MultinomialNB
30from sklearn.neighbors import KNeighborsClassifier
31from sklearn.pipeline import Pipeline, make_pipeline
32from sklearn.svm import SVC, LinearSVC
33from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
34from sklearn.utils import (
35 check_array,
36 shuffle,
37)
38from sklearn.utils._mocking import CheckingClassifier
39from sklearn.utils._testing import assert_almost_equal, assert_array_equal
40from sklearn.utils.fixes import (
41 COO_CONTAINERS,
42 CSC_CONTAINERS,
43 CSR_CONTAINERS,
44 DOK_CONTAINERS,
45 LIL_CONTAINERS,
46)
47from sklearn.utils.multiclass import check_classification_targets, type_of_target
48
49iris = datasets.load_iris()
50rng = np.random.RandomState(0)
51perm = rng.permutation(iris.target.size)
52iris.data = iris.data[perm]
53iris.target = iris.target[perm]
54n_classes = 3
55
56
57def test_ovr_exceptions():
58 ovr = OneVsRestClassifier(LinearSVC(random_state=0))
59
60 # test predicting without fitting
61 with pytest.raises(NotFittedError):
62 ovr.predict([])
63
64 # Fail on multioutput data
65 msg = "Multioutput target data is not supported with label binarization"
66 with pytest.raises(ValueError, match=msg):
67 X = np.array([[1, 0], [0, 1]])
68 y = np.array([[1, 2], [3, 1]])
69 OneVsRestClassifier(MultinomialNB()).fit(X, y)
70
71 with pytest.raises(ValueError, match=msg):
72 X = np.array([[1, 0], [0, 1]])
73 y = np.array([[1.5, 2.4], [3.1, 0.8]])
74 OneVsRestClassifier(MultinomialNB()).fit(X, y)
75
76
77def test_check_classification_targets():
78 # Test that check_classification_target return correct type. #5782
79 y = np.array([0.0, 1.1, 2.0, 3.0])
80 msg = type_of_target(y)
81 with pytest.raises(ValueError, match=msg):
82 check_classification_targets(y)
83
84
85def test_ovr_fit_predict():
86 # A classifier which implements decision_function.
87 ovr = OneVsRestClassifier(LinearSVC(random_state=0))
88 pred = ovr.fit(iris.data, iris.target).predict(iris.data)
89 assert len(ovr.estimators_) == n_classes
90
91 clf = LinearSVC(random_state=0)
92 pred2 = clf.fit(iris.data, iris.target).predict(iris.data)
93 assert np.mean(iris.target == pred) == np.mean(iris.target == pred2)
94
95 # A classifier which implements predict_proba.
96 ovr = OneVsRestClassifier(MultinomialNB())
97 pred = ovr.fit(iris.data, iris.target).predict(iris.data)
98 assert np.mean(iris.target == pred) > 0.65
99
100
101def test_ovr_partial_fit():
102 # Test if partial_fit is working as intended
103 X, y = shuffle(iris.data, iris.target, random_state=0)
104 ovr = OneVsRestClassifier(MultinomialNB())
105 ovr.partial_fit(X[:100], y[:100], np.unique(y))
106 ovr.partial_fit(X[100:], y[100:])
107 pred = ovr.predict(X)
108 ovr2 = OneVsRestClassifier(MultinomialNB())
109 pred2 = ovr2.fit(X, y).predict(X)
110
111 assert_almost_equal(pred, pred2)
112 assert len(ovr.estimators_) == len(np.unique(y))
113 assert np.mean(y == pred) > 0.65
114
115 # Test when mini batches doesn't have all classes
116 # with SGDClassifier
117 X = np.abs(np.random.randn(14, 2))
118 y = [1, 1, 1, 1, 2, 3, 3, 0, 0, 2, 3, 1, 2, 3]
119
120 ovr = OneVsRestClassifier(
121 SGDClassifier(max_iter=1, tol=None, shuffle=False, random_state=0)
122 )
123 ovr.partial_fit(X[:7], y[:7], np.unique(y))
124 ovr.partial_fit(X[7:], y[7:])
125 pred = ovr.predict(X)
126 ovr1 = OneVsRestClassifier(
127 SGDClassifier(max_iter=1, tol=None, shuffle=False, random_state=0)
128 )
129 pred1 = ovr1.fit(X, y).predict(X)
130 assert np.mean(pred == y) == np.mean(pred1 == y)
131
132 # test partial_fit only exists if estimator has it:
133 ovr = OneVsRestClassifier(SVC())
134 assert not hasattr(ovr, "partial_fit")
135
136
137def test_ovr_partial_fit_exceptions():
138 ovr = OneVsRestClassifier(MultinomialNB())
139 X = np.abs(np.random.randn(14, 2))
140 y = [1, 1, 1, 1, 2, 3, 3, 0, 0, 2, 3, 1, 2, 3]
141 ovr.partial_fit(X[:7], y[:7], np.unique(y))
142 # If a new class that was not in the first call of partial fit is seen
143 # it should raise ValueError
144 y1 = [5] + y[7:-1]
145 msg = r"Mini-batch contains \[.+\] while classes must be subset of \[.+\]"
146 with pytest.raises(ValueError, match=msg):
147 ovr.partial_fit(X=X[7:], y=y1)
148
149
150def test_ovr_ovo_regressor():
151 # test that ovr and ovo work on regressors which don't have a decision_
152 # function
153 ovr = OneVsRestClassifier(DecisionTreeRegressor())
154 pred = ovr.fit(iris.data, iris.target).predict(iris.data)
155 assert len(ovr.estimators_) == n_classes
156 assert_array_equal(np.unique(pred), [0, 1, 2])
157 # we are doing something sensible
158 assert np.mean(pred == iris.target) > 0.9
159
160 ovr = OneVsOneClassifier(DecisionTreeRegressor())
161 pred = ovr.fit(iris.data, iris.target).predict(iris.data)
162 assert len(ovr.estimators_) == n_classes * (n_classes - 1) / 2
163 assert_array_equal(np.unique(pred), [0, 1, 2])
164 # we are doing something sensible
165 assert np.mean(pred == iris.target) > 0.9
166
167
168@pytest.mark.parametrize(
169 "sparse_container",
170 CSR_CONTAINERS + CSC_CONTAINERS + COO_CONTAINERS + DOK_CONTAINERS + LIL_CONTAINERS,
171)
172def test_ovr_fit_predict_sparse(sparse_container):
173 base_clf = MultinomialNB(alpha=1)
174
175 X, Y = datasets.make_multilabel_classification(
176 n_samples=100,
177 n_features=20,
178 n_classes=5,
179 n_labels=3,
180 length=50,
181 allow_unlabeled=True,
182 random_state=0,
183 )
184
185 X_train, Y_train = X[:80], Y[:80]
186 X_test = X[80:]
187
188 clf = OneVsRestClassifier(base_clf).fit(X_train, Y_train)
189 Y_pred = clf.predict(X_test)
190
191 clf_sprs = OneVsRestClassifier(base_clf).fit(X_train, sparse_container(Y_train))
192 Y_pred_sprs = clf_sprs.predict(X_test)
193
194 assert clf.multilabel_
195 assert sp.issparse(Y_pred_sprs)
196 assert_array_equal(Y_pred_sprs.toarray(), Y_pred)
197
198 # Test predict_proba
199 Y_proba = clf_sprs.predict_proba(X_test)
200
201 # predict assigns a label if the probability that the
202 # sample has the label is greater than 0.5.
203 pred = Y_proba > 0.5
204 assert_array_equal(pred, Y_pred_sprs.toarray())
205
206 # Test decision_function
207 clf = svm.SVC()
208 clf_sprs = OneVsRestClassifier(clf).fit(X_train, sparse_container(Y_train))
209 dec_pred = (clf_sprs.decision_function(X_test) > 0).astype(int)
210 assert_array_equal(dec_pred, clf_sprs.predict(X_test).toarray())
211
212
213def test_ovr_always_present():
214 # Test that ovr works with classes that are always present or absent.
215 # Note: tests is the case where _ConstantPredictor is utilised
216 X = np.ones((10, 2))
217 X[:5, :] = 0
218
219 # Build an indicator matrix where two features are always on.
220 # As list of lists, it would be: [[int(i >= 5), 2, 3] for i in range(10)]
221 y = np.zeros((10, 3))
222 y[5:, 0] = 1
223 y[:, 1] = 1
224 y[:, 2] = 1
225
226 ovr = OneVsRestClassifier(LogisticRegression())
227 msg = r"Label .+ is present in all training examples"
228 with pytest.warns(UserWarning, match=msg):
229 ovr.fit(X, y)
230 y_pred = ovr.predict(X)
231 assert_array_equal(np.array(y_pred), np.array(y))
232 y_pred = ovr.decision_function(X)
233 assert np.unique(y_pred[:, -2:]) == 1
234 y_pred = ovr.predict_proba(X)
235 assert_array_equal(y_pred[:, -1], np.ones(X.shape[0]))
236
237 # y has a constantly absent label
238 y = np.zeros((10, 2))
239 y[5:, 0] = 1 # variable label
240 ovr = OneVsRestClassifier(LogisticRegression())
241
242 msg = r"Label not 1 is present in all training examples"
243 with pytest.warns(UserWarning, match=msg):
244 ovr.fit(X, y)
245 y_pred = ovr.predict_proba(X)
246 assert_array_equal(y_pred[:, -1], np.zeros(X.shape[0]))
247
248
249def test_ovr_multiclass():
250 # Toy dataset where features correspond directly to labels.
251 X = np.array([[0, 0, 5], [0, 5, 0], [3, 0, 0], [0, 0, 6], [6, 0, 0]])
252 y = ["eggs", "spam", "ham", "eggs", "ham"]
253 Y = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 0, 1], [1, 0, 0]])
254
255 classes = set("ham eggs spam".split())
256
257 for base_clf in (
258 MultinomialNB(),
259 LinearSVC(random_state=0),
260 LinearRegression(),
261 Ridge(),
262 ElasticNet(),
263 ):
264 clf = OneVsRestClassifier(base_clf).fit(X, y)
265 assert set(clf.classes_) == classes
266 y_pred = clf.predict(np.array([[0, 0, 4]]))[0]
267 assert_array_equal(y_pred, ["eggs"])
268
269 # test input as label indicator matrix
270 clf = OneVsRestClassifier(base_clf).fit(X, Y)
271 y_pred = clf.predict([[0, 0, 4]])[0]
272 assert_array_equal(y_pred, [0, 0, 1])
273
274
275def test_ovr_binary():
276 # Toy dataset where features correspond directly to labels.
277 X = np.array([[0, 0, 5], [0, 5, 0], [3, 0, 0], [0, 0, 6], [6, 0, 0]])
278 y = ["eggs", "spam", "spam", "eggs", "spam"]
279 Y = np.array([[0, 1, 1, 0, 1]]).T
280
281 classes = set("eggs spam".split())
282
283 def conduct_test(base_clf, test_predict_proba=False):
284 clf = OneVsRestClassifier(base_clf).fit(X, y)
285 assert set(clf.classes_) == classes
286 y_pred = clf.predict(np.array([[0, 0, 4]]))[0]
287 assert_array_equal(y_pred, ["eggs"])
288 if hasattr(base_clf, "decision_function"):
289 dec = clf.decision_function(X)
290 assert dec.shape == (5,)
291
292 if test_predict_proba:
293 X_test = np.array([[0, 0, 4]])
294 probabilities = clf.predict_proba(X_test)
295 assert 2 == len(probabilities[0])
296 assert clf.classes_[np.argmax(probabilities, axis=1)] == clf.predict(X_test)
297
298 # test input as label indicator matrix
299 clf = OneVsRestClassifier(base_clf).fit(X, Y)
300 y_pred = clf.predict([[3, 0, 0]])[0]
301 assert y_pred == 1
302
303 for base_clf in (
304 LinearSVC(random_state=0),
305 LinearRegression(),
306 Ridge(),
307 ElasticNet(),
308 ):
309 conduct_test(base_clf)
310
311 for base_clf in (MultinomialNB(), SVC(probability=True), LogisticRegression()):
312 conduct_test(base_clf, test_predict_proba=True)
313
314
315def test_ovr_multilabel():
316 # Toy dataset where features correspond directly to labels.
317 X = np.array([[0, 4, 5], [0, 5, 0], [3, 3, 3], [4, 0, 6], [6, 0, 0]])
318 y = np.array([[0, 1, 1], [0, 1, 0], [1, 1, 1], [1, 0, 1], [1, 0, 0]])
319
320 for base_clf in (
321 MultinomialNB(),
322 LinearSVC(random_state=0),
323 LinearRegression(),
324 Ridge(),
325 ElasticNet(),
326 Lasso(alpha=0.5),
327 ):
328 clf = OneVsRestClassifier(base_clf).fit(X, y)
329 y_pred = clf.predict([[0, 4, 4]])[0]
330 assert_array_equal(y_pred, [0, 1, 1])
331 assert clf.multilabel_
332
333
334def test_ovr_fit_predict_svc():
335 ovr = OneVsRestClassifier(svm.SVC())
336 ovr.fit(iris.data, iris.target)
337 assert len(ovr.estimators_) == 3
338 assert ovr.score(iris.data, iris.target) > 0.9
339
340
341def test_ovr_multilabel_dataset():
342 base_clf = MultinomialNB(alpha=1)
343 for au, prec, recall in zip((True, False), (0.51, 0.66), (0.51, 0.80)):
344 X, Y = datasets.make_multilabel_classification(
345 n_samples=100,
346 n_features=20,
347 n_classes=5,
348 n_labels=2,
349 length=50,
350 allow_unlabeled=au,
351 random_state=0,
352 )
353 X_train, Y_train = X[:80], Y[:80]
354 X_test, Y_test = X[80:], Y[80:]
355 clf = OneVsRestClassifier(base_clf).fit(X_train, Y_train)
356 Y_pred = clf.predict(X_test)
357
358 assert clf.multilabel_
359 assert_almost_equal(
360 precision_score(Y_test, Y_pred, average="micro"), prec, decimal=2
361 )
362 assert_almost_equal(
363 recall_score(Y_test, Y_pred, average="micro"), recall, decimal=2
364 )
365
366
367def test_ovr_multilabel_predict_proba():
368 base_clf = MultinomialNB(alpha=1)
369 for au in (False, True):
370 X, Y = datasets.make_multilabel_classification(
371 n_samples=100,
372 n_features=20,
373 n_classes=5,
374 n_labels=3,
375 length=50,
376 allow_unlabeled=au,
377 random_state=0,
378 )
379 X_train, Y_train = X[:80], Y[:80]
380 X_test = X[80:]
381 clf = OneVsRestClassifier(base_clf).fit(X_train, Y_train)
382
383 # Decision function only estimator.
384 decision_only = OneVsRestClassifier(svm.SVR()).fit(X_train, Y_train)
385 assert not hasattr(decision_only, "predict_proba")
386
387 # Estimator with predict_proba disabled, depending on parameters.
388 decision_only = OneVsRestClassifier(svm.SVC(probability=False))
389 assert not hasattr(decision_only, "predict_proba")
390 decision_only.fit(X_train, Y_train)
391 assert not hasattr(decision_only, "predict_proba")
392 assert hasattr(decision_only, "decision_function")
393
394 # Estimator which can get predict_proba enabled after fitting
395 gs = GridSearchCV(
396 svm.SVC(probability=False), param_grid={"probability": [True]}
397 )
398 proba_after_fit = OneVsRestClassifier(gs)
399 assert not hasattr(proba_after_fit, "predict_proba")
400 proba_after_fit.fit(X_train, Y_train)
401 assert hasattr(proba_after_fit, "predict_proba")
402
403 Y_pred = clf.predict(X_test)
404 Y_proba = clf.predict_proba(X_test)
405
406 # predict assigns a label if the probability that the
407 # sample has the label is greater than 0.5.
408 pred = Y_proba > 0.5
409 assert_array_equal(pred, Y_pred)
410
411
412def test_ovr_single_label_predict_proba():
413 base_clf = MultinomialNB(alpha=1)
414 X, Y = iris.data, iris.target
415 X_train, Y_train = X[:80], Y[:80]
416 X_test = X[80:]
417 clf = OneVsRestClassifier(base_clf).fit(X_train, Y_train)
418
419 # Decision function only estimator.
420 decision_only = OneVsRestClassifier(svm.SVR()).fit(X_train, Y_train)
421 assert not hasattr(decision_only, "predict_proba")
422
423 Y_pred = clf.predict(X_test)
424 Y_proba = clf.predict_proba(X_test)
425
426 assert_almost_equal(Y_proba.sum(axis=1), 1.0)
427 # predict assigns a label if the probability that the
428 # sample has the label with the greatest predictive probability.
429 pred = Y_proba.argmax(axis=1)
430 assert not (pred - Y_pred).any()
431
432
433def test_ovr_single_label_predict_proba_zero():
434 """Check that predic_proba returns all zeros when the base estimator
435 never predicts the positive class.
436 """
437
438 class NaiveBinaryClassifier(BaseEstimator, ClassifierMixin):
439 def fit(self, X, y):
440 self.classes_ = np.unique(y)
441 return self
442
443 def predict_proba(self, X):
444 proba = np.ones((len(X), 2))
445 # Probability of being the positive class is always 0
446 proba[:, 1] = 0
447 return proba
448
449 base_clf = NaiveBinaryClassifier()
450 X, y = iris.data, iris.target # Three-class problem with 150 samples
451
452 clf = OneVsRestClassifier(base_clf).fit(X, y)
453 y_proba = clf.predict_proba(X)
454
455 assert_allclose(y_proba, 0.0)
456
457
458def test_ovr_multilabel_decision_function():
459 X, Y = datasets.make_multilabel_classification(
460 n_samples=100,
461 n_features=20,
462 n_classes=5,
463 n_labels=3,
464 length=50,
465 allow_unlabeled=True,
466 random_state=0,
467 )
468 X_train, Y_train = X[:80], Y[:80]
469 X_test = X[80:]
470 clf = OneVsRestClassifier(svm.SVC()).fit(X_train, Y_train)
471 assert_array_equal(
472 (clf.decision_function(X_test) > 0).astype(int), clf.predict(X_test)
473 )
474
475
476def test_ovr_single_label_decision_function():
477 X, Y = datasets.make_classification(n_samples=100, n_features=20, random_state=0)
478 X_train, Y_train = X[:80], Y[:80]
479 X_test = X[80:]
480 clf = OneVsRestClassifier(svm.SVC()).fit(X_train, Y_train)
481 assert_array_equal(clf.decision_function(X_test).ravel() > 0, clf.predict(X_test))
482
483
484def test_ovr_gridsearch():
485 ovr = OneVsRestClassifier(LinearSVC(random_state=0))
486 Cs = [0.1, 0.5, 0.8]
487 cv = GridSearchCV(ovr, {"estimator__C": Cs})
488 cv.fit(iris.data, iris.target)
489 best_C = cv.best_estimator_.estimators_[0].C
490 assert best_C in Cs
491
492
493def test_ovr_pipeline():
494 # Test with pipeline of length one
495 # This test is needed because the multiclass estimators may fail to detect
496 # the presence of predict_proba or decision_function.
497 clf = Pipeline([("tree", DecisionTreeClassifier())])
498 ovr_pipe = OneVsRestClassifier(clf)
499 ovr_pipe.fit(iris.data, iris.target)
500 ovr = OneVsRestClassifier(DecisionTreeClassifier())
501 ovr.fit(iris.data, iris.target)
502 assert_array_equal(ovr.predict(iris.data), ovr_pipe.predict(iris.data))
503
504
505def test_ovo_exceptions():
506 ovo = OneVsOneClassifier(LinearSVC(random_state=0))
507 with pytest.raises(NotFittedError):
508 ovo.predict([])
509
510
511def test_ovo_fit_on_list():
512 # Test that OneVsOne fitting works with a list of targets and yields the
513 # same output as predict from an array
514 ovo = OneVsOneClassifier(LinearSVC(random_state=0))
515 prediction_from_array = ovo.fit(iris.data, iris.target).predict(iris.data)
516 iris_data_list = [list(a) for a in iris.data]
517 prediction_from_list = ovo.fit(iris_data_list, list(iris.target)).predict(
518 iris_data_list
519 )
520 assert_array_equal(prediction_from_array, prediction_from_list)
521
522
523def test_ovo_fit_predict():
524 # A classifier which implements decision_function.
525 ovo = OneVsOneClassifier(LinearSVC(random_state=0))
526 ovo.fit(iris.data, iris.target).predict(iris.data)
527 assert len(ovo.estimators_) == n_classes * (n_classes - 1) / 2
528
529 # A classifier which implements predict_proba.
530 ovo = OneVsOneClassifier(MultinomialNB())
531 ovo.fit(iris.data, iris.target).predict(iris.data)
532 assert len(ovo.estimators_) == n_classes * (n_classes - 1) / 2
533
534
535def test_ovo_partial_fit_predict():
536 temp = datasets.load_iris()
537 X, y = temp.data, temp.target
538 ovo1 = OneVsOneClassifier(MultinomialNB())
539 ovo1.partial_fit(X[:100], y[:100], np.unique(y))
540 ovo1.partial_fit(X[100:], y[100:])
541 pred1 = ovo1.predict(X)
542
543 ovo2 = OneVsOneClassifier(MultinomialNB())
544 ovo2.fit(X, y)
545 pred2 = ovo2.predict(X)
546 assert len(ovo1.estimators_) == n_classes * (n_classes - 1) / 2
547 assert np.mean(y == pred1) > 0.65
548 assert_almost_equal(pred1, pred2)
549
550 # Test when mini-batches have binary target classes
551 ovo1 = OneVsOneClassifier(MultinomialNB())
552 ovo1.partial_fit(X[:60], y[:60], np.unique(y))
553 ovo1.partial_fit(X[60:], y[60:])
554 pred1 = ovo1.predict(X)
555 ovo2 = OneVsOneClassifier(MultinomialNB())
556 pred2 = ovo2.fit(X, y).predict(X)
557
558 assert_almost_equal(pred1, pred2)
559 assert len(ovo1.estimators_) == len(np.unique(y))
560 assert np.mean(y == pred1) > 0.65
561
562 ovo = OneVsOneClassifier(MultinomialNB())
563 X = np.random.rand(14, 2)
564 y = [1, 1, 2, 3, 3, 0, 0, 4, 4, 4, 4, 4, 2, 2]
565 ovo.partial_fit(X[:7], y[:7], [0, 1, 2, 3, 4])
566 ovo.partial_fit(X[7:], y[7:])
567 pred = ovo.predict(X)
568 ovo2 = OneVsOneClassifier(MultinomialNB())
569 pred2 = ovo2.fit(X, y).predict(X)
570 assert_almost_equal(pred, pred2)
571
572 # raises error when mini-batch does not have classes from all_classes
573 ovo = OneVsOneClassifier(MultinomialNB())
574 error_y = [0, 1, 2, 3, 4, 5, 2]
575 message_re = escape(
576 "Mini-batch contains {0} while it must be subset of {1}".format(
577 np.unique(error_y), np.unique(y)
578 )
579 )
580 with pytest.raises(ValueError, match=message_re):
581 ovo.partial_fit(X[:7], error_y, np.unique(y))
582
583 # test partial_fit only exists if estimator has it:
584 ovr = OneVsOneClassifier(SVC())
585 assert not hasattr(ovr, "partial_fit")
586
587
588def test_ovo_decision_function():
589 n_samples = iris.data.shape[0]
590
591 ovo_clf = OneVsOneClassifier(LinearSVC(random_state=0))
592 # first binary
593 ovo_clf.fit(iris.data, iris.target == 0)
594 decisions = ovo_clf.decision_function(iris.data)
595 assert decisions.shape == (n_samples,)
596
597 # then multi-class
598 ovo_clf.fit(iris.data, iris.target)
599 decisions = ovo_clf.decision_function(iris.data)
600
601 assert decisions.shape == (n_samples, n_classes)
602 assert_array_equal(decisions.argmax(axis=1), ovo_clf.predict(iris.data))
603
604 # Compute the votes
605 votes = np.zeros((n_samples, n_classes))
606
607 k = 0
608 for i in range(n_classes):
609 for j in range(i + 1, n_classes):
610 pred = ovo_clf.estimators_[k].predict(iris.data)
611 votes[pred == 0, i] += 1
612 votes[pred == 1, j] += 1
613 k += 1
614
615 # Extract votes and verify
616 assert_array_equal(votes, np.round(decisions))
617
618 for class_idx in range(n_classes):
619 # For each sample and each class, there only 3 possible vote levels
620 # because they are only 3 distinct class pairs thus 3 distinct
621 # binary classifiers.
622 # Therefore, sorting predictions based on votes would yield
623 # mostly tied predictions:
624 assert set(votes[:, class_idx]).issubset(set([0.0, 1.0, 2.0]))
625
626 # The OVO decision function on the other hand is able to resolve
627 # most of the ties on this data as it combines both the vote counts
628 # and the aggregated confidence levels of the binary classifiers
629 # to compute the aggregate decision function. The iris dataset
630 # has 150 samples with a couple of duplicates. The OvO decisions
631 # can resolve most of the ties:
632 assert len(np.unique(decisions[:, class_idx])) > 146
633
634
635def test_ovo_gridsearch():
636 ovo = OneVsOneClassifier(LinearSVC(random_state=0))
637 Cs = [0.1, 0.5, 0.8]
638 cv = GridSearchCV(ovo, {"estimator__C": Cs})
639 cv.fit(iris.data, iris.target)
640 best_C = cv.best_estimator_.estimators_[0].C
641 assert best_C in Cs
642
643
644def test_ovo_ties():
645 # Test that ties are broken using the decision function,
646 # not defaulting to the smallest label
647 X = np.array([[1, 2], [2, 1], [-2, 1], [-2, -1]])
648 y = np.array([2, 0, 1, 2])
649 multi_clf = OneVsOneClassifier(Perceptron(shuffle=False, max_iter=4, tol=None))
650 ovo_prediction = multi_clf.fit(X, y).predict(X)
651 ovo_decision = multi_clf.decision_function(X)
652
653 # Classifiers are in order 0-1, 0-2, 1-2
654 # Use decision_function to compute the votes and the normalized
655 # sum_of_confidences, which is used to disambiguate when there is a tie in
656 # votes.
657 votes = np.round(ovo_decision)
658 normalized_confidences = ovo_decision - votes
659
660 # For the first point, there is one vote per class
661 assert_array_equal(votes[0, :], 1)
662 # For the rest, there is no tie and the prediction is the argmax
663 assert_array_equal(np.argmax(votes[1:], axis=1), ovo_prediction[1:])
664 # For the tie, the prediction is the class with the highest score
665 assert ovo_prediction[0] == normalized_confidences[0].argmax()
666
667
668def test_ovo_ties2():
669 # test that ties can not only be won by the first two labels
670 X = np.array([[1, 2], [2, 1], [-2, 1], [-2, -1]])
671 y_ref = np.array([2, 0, 1, 2])
672
673 # cycle through labels so that each label wins once
674 for i in range(3):
675 y = (y_ref + i) % 3
676 multi_clf = OneVsOneClassifier(Perceptron(shuffle=False, max_iter=4, tol=None))
677 ovo_prediction = multi_clf.fit(X, y).predict(X)
678 assert ovo_prediction[0] == i % 3
679
680
681def test_ovo_string_y():
682 # Test that the OvO doesn't mess up the encoding of string labels
683 X = np.eye(4)
684 y = np.array(["a", "b", "c", "d"])
685
686 ovo = OneVsOneClassifier(LinearSVC())
687 ovo.fit(X, y)
688 assert_array_equal(y, ovo.predict(X))
689
690
691def test_ovo_one_class():
692 # Test error for OvO with one class
693 X = np.eye(4)
694 y = np.array(["a"] * 4)
695
696 ovo = OneVsOneClassifier(LinearSVC())
697 msg = "when only one class"
698 with pytest.raises(ValueError, match=msg):
699 ovo.fit(X, y)
700
701
702def test_ovo_float_y():
703 # Test that the OvO errors on float targets
704 X = iris.data
705 y = iris.data[:, 0]
706
707 ovo = OneVsOneClassifier(LinearSVC())
708 msg = "Unknown label type"
709 with pytest.raises(ValueError, match=msg):
710 ovo.fit(X, y)
711
712
713def test_ecoc_exceptions():
714 ecoc = OutputCodeClassifier(LinearSVC(random_state=0))
715 with pytest.raises(NotFittedError):
716 ecoc.predict([])
717
718
719def test_ecoc_fit_predict():
720 # A classifier which implements decision_function.
721 ecoc = OutputCodeClassifier(LinearSVC(random_state=0), code_size=2, random_state=0)
722 ecoc.fit(iris.data, iris.target).predict(iris.data)
723 assert len(ecoc.estimators_) == n_classes * 2
724
725 # A classifier which implements predict_proba.
726 ecoc = OutputCodeClassifier(MultinomialNB(), code_size=2, random_state=0)
727 ecoc.fit(iris.data, iris.target).predict(iris.data)
728 assert len(ecoc.estimators_) == n_classes * 2
729
730
731def test_ecoc_gridsearch():
732 ecoc = OutputCodeClassifier(LinearSVC(random_state=0), random_state=0)
733 Cs = [0.1, 0.5, 0.8]
734 cv = GridSearchCV(ecoc, {"estimator__C": Cs})
735 cv.fit(iris.data, iris.target)
736 best_C = cv.best_estimator_.estimators_[0].C
737 assert best_C in Cs
738
739
740def test_ecoc_float_y():
741 # Test that the OCC errors on float targets
742 X = iris.data
743 y = iris.data[:, 0]
744
745 ovo = OutputCodeClassifier(LinearSVC())
746 msg = "Unknown label type"
747 with pytest.raises(ValueError, match=msg):
748 ovo.fit(X, y)
749
750
751@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
752def test_ecoc_delegate_sparse_base_estimator(csc_container):
753 # Non-regression test for
754 # https://github.com/scikit-learn/scikit-learn/issues/17218
755 X, y = iris.data, iris.target
756 X_sp = csc_container(X)
757
758 # create an estimator that does not support sparse input
759 base_estimator = CheckingClassifier(
760 check_X=check_array,
761 check_X_params={"ensure_2d": True, "accept_sparse": False},
762 )
763 ecoc = OutputCodeClassifier(base_estimator, random_state=0)
764
765 with pytest.raises(TypeError, match="Sparse data was passed"):
766 ecoc.fit(X_sp, y)
767
768 ecoc.fit(X, y)
769 with pytest.raises(TypeError, match="Sparse data was passed"):
770 ecoc.predict(X_sp)
771
772 # smoke test to check when sparse input should be supported
773 ecoc = OutputCodeClassifier(LinearSVC(random_state=0))
774 ecoc.fit(X_sp, y).predict(X_sp)
775 assert len(ecoc.estimators_) == 4
776
777
778def test_pairwise_indices():
779 clf_precomputed = svm.SVC(kernel="precomputed")
780 X, y = iris.data, iris.target
781
782 ovr_false = OneVsOneClassifier(clf_precomputed)
783 linear_kernel = np.dot(X, X.T)
784 ovr_false.fit(linear_kernel, y)
785
786 n_estimators = len(ovr_false.estimators_)
787 precomputed_indices = ovr_false.pairwise_indices_
788
789 for idx in precomputed_indices:
790 assert (
791 idx.shape[0] * n_estimators / (n_estimators - 1) == linear_kernel.shape[0]
792 )
793
794
795def test_pairwise_n_features_in():
796 """Check the n_features_in_ attributes of the meta and base estimators
797
798 When the training data is a regular design matrix, everything is intuitive.
799 However, when the training data is a precomputed kernel matrix, the
800 multiclass strategy can resample the kernel matrix of the underlying base
801 estimator both row-wise and column-wise and this has a non-trivial impact
802 on the expected value for the n_features_in_ of both the meta and the base
803 estimators.
804 """
805 X, y = iris.data, iris.target
806
807 # Remove the last sample to make the classes not exactly balanced and make
808 # the test more interesting.
809 assert y[-1] == 0
810 X = X[:-1]
811 y = y[:-1]
812
813 # Fitting directly on the design matrix:
814 assert X.shape == (149, 4)
815
816 clf_notprecomputed = svm.SVC(kernel="linear").fit(X, y)
817 assert clf_notprecomputed.n_features_in_ == 4
818
819 ovr_notprecomputed = OneVsRestClassifier(clf_notprecomputed).fit(X, y)
820 assert ovr_notprecomputed.n_features_in_ == 4
821 for est in ovr_notprecomputed.estimators_:
822 assert est.n_features_in_ == 4
823
824 ovo_notprecomputed = OneVsOneClassifier(clf_notprecomputed).fit(X, y)
825 assert ovo_notprecomputed.n_features_in_ == 4
826 assert ovo_notprecomputed.n_classes_ == 3
827 assert len(ovo_notprecomputed.estimators_) == 3
828 for est in ovo_notprecomputed.estimators_:
829 assert est.n_features_in_ == 4
830
831 # When working with precomputed kernels we have one "feature" per training
832 # sample:
833 K = X @ X.T
834 assert K.shape == (149, 149)
835
836 clf_precomputed = svm.SVC(kernel="precomputed").fit(K, y)
837 assert clf_precomputed.n_features_in_ == 149
838
839 ovr_precomputed = OneVsRestClassifier(clf_precomputed).fit(K, y)
840 assert ovr_precomputed.n_features_in_ == 149
841 assert ovr_precomputed.n_classes_ == 3
842 assert len(ovr_precomputed.estimators_) == 3
843 for est in ovr_precomputed.estimators_:
844 assert est.n_features_in_ == 149
845
846 # This becomes really interesting with OvO and precomputed kernel together:
847 # internally, OvO will drop the samples of the classes not part of the pair
848 # of classes under consideration for a given binary classifier. Since we
849 # use a precomputed kernel, it will also drop the matching columns of the
850 # kernel matrix, and therefore we have fewer "features" as result.
851 #
852 # Since class 0 has 49 samples, and class 1 and 2 have 50 samples each, a
853 # single OvO binary classifier works with a sub-kernel matrix of shape
854 # either (99, 99) or (100, 100).
855 ovo_precomputed = OneVsOneClassifier(clf_precomputed).fit(K, y)
856 assert ovo_precomputed.n_features_in_ == 149
857 assert ovr_precomputed.n_classes_ == 3
858 assert len(ovr_precomputed.estimators_) == 3
859 assert ovo_precomputed.estimators_[0].n_features_in_ == 99 # class 0 vs class 1
860 assert ovo_precomputed.estimators_[1].n_features_in_ == 99 # class 0 vs class 2
861 assert ovo_precomputed.estimators_[2].n_features_in_ == 100 # class 1 vs class 2
862
863
864@pytest.mark.parametrize(
865 "MultiClassClassifier", [OneVsRestClassifier, OneVsOneClassifier]
866)
867def test_pairwise_tag(MultiClassClassifier):
868 clf_precomputed = svm.SVC(kernel="precomputed")
869 clf_notprecomputed = svm.SVC()
870
871 ovr_false = MultiClassClassifier(clf_notprecomputed)
872 assert not ovr_false.__sklearn_tags__().input_tags.pairwise
873
874 ovr_true = MultiClassClassifier(clf_precomputed)
875 assert ovr_true.__sklearn_tags__().input_tags.pairwise
876
877
878@pytest.mark.parametrize(
879 "MultiClassClassifier", [OneVsRestClassifier, OneVsOneClassifier]
880)
881def test_pairwise_cross_val_score(MultiClassClassifier):
882 clf_precomputed = svm.SVC(kernel="precomputed")
883 clf_notprecomputed = svm.SVC(kernel="linear")
884
885 X, y = iris.data, iris.target
886
887 multiclass_clf_notprecomputed = MultiClassClassifier(clf_notprecomputed)
888 multiclass_clf_precomputed = MultiClassClassifier(clf_precomputed)
889
890 linear_kernel = np.dot(X, X.T)
891 score_not_precomputed = cross_val_score(
892 multiclass_clf_notprecomputed, X, y, error_score="raise"
893 )
894 score_precomputed = cross_val_score(
895 multiclass_clf_precomputed, linear_kernel, y, error_score="raise"
896 )
897 assert_array_equal(score_precomputed, score_not_precomputed)
898
899
900@pytest.mark.parametrize(
901 "MultiClassClassifier", [OneVsRestClassifier, OneVsOneClassifier]
902)
903# FIXME: we should move this test in `estimator_checks` once we are able
904# to construct meta-estimator instances
905def test_support_missing_values(MultiClassClassifier):
906 # smoke test to check that pipeline OvR and OvO classifiers are letting
907 # the validation of missing values to
908 # the underlying pipeline or classifiers
909 rng = np.random.RandomState(42)
910 X, y = iris.data, iris.target
911 X = np.copy(X) # Copy to avoid that the original data is modified
912 mask = rng.choice([1, 0], X.shape, p=[0.1, 0.9]).astype(bool)
913 X[mask] = np.nan
914 lr = make_pipeline(SimpleImputer(), LogisticRegression(random_state=rng))
915
916 MultiClassClassifier(lr).fit(X, y).score(X, y)
917
918
919@pytest.mark.parametrize("make_y", [np.ones, np.zeros])
920def test_constant_int_target(make_y):
921 """Check that constant y target does not raise.
922
923 Non-regression test for #21869
924 """
925 X = np.ones((10, 2))
926 y = make_y((10, 1), dtype=np.int32)
927 ovr = OneVsRestClassifier(LogisticRegression())
928
929 ovr.fit(X, y)
930 y_pred = ovr.predict_proba(X)
931 expected = np.zeros((X.shape[0], 2))
932 expected[:, 0] = 1
933 assert_allclose(y_pred, expected)
934
935
936def test_ovo_consistent_binary_classification():
937 """Check that ovo is consistent with binary classifier.
938
939 Non-regression test for #13617.
940 """
941 X, y = load_breast_cancer(return_X_y=True)
942
943 clf = KNeighborsClassifier(n_neighbors=8, weights="distance")
944 ovo = OneVsOneClassifier(clf)
945
946 clf.fit(X, y)
947 ovo.fit(X, y)
948
949 assert_array_equal(clf.predict(X), ovo.predict(X))
950
951
952def test_multiclass_estimator_attribute_error():
953 """Check that we raise the proper AttributeError when the final estimator
954 does not implement the `partial_fit` method, which is decorated with
955 `available_if`.
956
957 Non-regression test for:
958 https://github.com/scikit-learn/scikit-learn/issues/28108
959 """
960 iris = datasets.load_iris()
961
962 # LogisticRegression does not implement 'partial_fit' and should raise an
963 # AttributeError
964 clf = OneVsRestClassifier(estimator=LogisticRegression(random_state=42))
965
966 outer_msg = "This 'OneVsRestClassifier' has no attribute 'partial_fit'"
967 inner_msg = "'LogisticRegression' object has no attribute 'partial_fit'"
968 with pytest.raises(AttributeError, match=outer_msg) as exec_info:
969 clf.partial_fit(iris.data, iris.target)
970 assert isinstance(exec_info.value.__cause__, AttributeError)
971 assert inner_msg in str(exec_info.value.__cause__)
972 