Aluode/PerceptionLabPortable
0
1import numpy as np
2import pytest
3from numpy.testing import assert_allclose
4
5from sklearn.datasets import make_blobs
6from sklearn.linear_model import LogisticRegression
7from sklearn.tree import DecisionTreeClassifier
8from sklearn.utils._testing import assert_almost_equal, assert_array_almost_equal
9from sklearn.utils.class_weight import compute_class_weight, compute_sample_weight
10from sklearn.utils.fixes import CSC_CONTAINERS
11
12
13def test_compute_class_weight():
14 # Test (and demo) compute_class_weight.
15 y = np.asarray([2, 2, 2, 3, 3, 4])
16 classes = np.unique(y)
17
18 cw = compute_class_weight("balanced", classes=classes, y=y)
19 # total effect of samples is preserved
20 class_counts = np.bincount(y)[2:]
21 assert_almost_equal(np.dot(cw, class_counts), y.shape[0])
22 assert cw[0] < cw[1] < cw[2]
23
24
25@pytest.mark.parametrize(
26 "y_type, class_weight, classes, err_msg",
27 [
28 (
29 "numeric",
30 "balanced",
31 np.arange(4),
32 "classes should have valid labels that are in y",
33 ),
34 # Non-regression for https://github.com/scikit-learn/scikit-learn/issues/8312
35 (
36 "numeric",
37 {"label_not_present": 1.0},
38 np.arange(4),
39 r"The classes, \[0, 1, 2, 3\], are not in class_weight",
40 ),
41 (
42 "numeric",
43 "balanced",
44 np.arange(2),
45 "classes should include all valid labels",
46 ),
47 (
48 "numeric",
49 {0: 1.0, 1: 2.0},
50 np.arange(2),
51 "classes should include all valid labels",
52 ),
53 (
54 "string",
55 {"dogs": 3, "cat": 2},
56 np.array(["dog", "cat"]),
57 r"The classes, \['dog'\], are not in class_weight",
58 ),
59 ],
60)
61def test_compute_class_weight_not_present(y_type, class_weight, classes, err_msg):
62 # Raise error when y does not contain all class labels
63 y = (
64 np.asarray([0, 0, 0, 1, 1, 2])
65 if y_type == "numeric"
66 else np.asarray(["dog", "cat", "dog"])
67 )
68
69 print(y)
70 with pytest.raises(ValueError, match=err_msg):
71 compute_class_weight(class_weight, classes=classes, y=y)
72
73
74def test_compute_class_weight_dict():
75 classes = np.arange(3)
76 class_weights = {0: 1.0, 1: 2.0, 2: 3.0}
77 y = np.asarray([0, 0, 1, 2])
78 cw = compute_class_weight(class_weights, classes=classes, y=y)
79
80 # When the user specifies class weights, compute_class_weights should just
81 # return them.
82 assert_array_almost_equal(np.asarray([1.0, 2.0, 3.0]), cw)
83
84 # When a class weight is specified that isn't in classes, the weight is ignored
85 class_weights = {0: 1.0, 1: 2.0, 2: 3.0, 4: 1.5}
86 cw = compute_class_weight(class_weights, classes=classes, y=y)
87 assert_allclose([1.0, 2.0, 3.0], cw)
88
89 class_weights = {-1: 5.0, 0: 4.0, 1: 2.0, 2: 3.0}
90 cw = compute_class_weight(class_weights, classes=classes, y=y)
91 assert_allclose([4.0, 2.0, 3.0], cw)
92
93
94def test_compute_class_weight_invariance():
95 # Test that results with class_weight="balanced" is invariant wrt
96 # class imbalance if the number of samples is identical.
97 # The test uses a balanced two class dataset with 100 datapoints.
98 # It creates three versions, one where class 1 is duplicated
99 # resulting in 150 points of class 1 and 50 of class 0,
100 # one where there are 50 points in class 1 and 150 in class 0,
101 # and one where there are 100 points of each class (this one is balanced
102 # again).
103 # With balancing class weights, all three should give the same model.
104 X, y = make_blobs(centers=2, random_state=0)
105 # create dataset where class 1 is duplicated twice
106 X_1 = np.vstack([X] + [X[y == 1]] * 2)
107 y_1 = np.hstack([y] + [y[y == 1]] * 2)
108 # create dataset where class 0 is duplicated twice
109 X_0 = np.vstack([X] + [X[y == 0]] * 2)
110 y_0 = np.hstack([y] + [y[y == 0]] * 2)
111 # duplicate everything
112 X_ = np.vstack([X] * 2)
113 y_ = np.hstack([y] * 2)
114 # results should be identical
115 logreg1 = LogisticRegression(class_weight="balanced").fit(X_1, y_1)
116 logreg0 = LogisticRegression(class_weight="balanced").fit(X_0, y_0)
117 logreg = LogisticRegression(class_weight="balanced").fit(X_, y_)
118 assert_array_almost_equal(logreg1.coef_, logreg0.coef_)
119 assert_array_almost_equal(logreg.coef_, logreg0.coef_)
120
121
122def test_compute_class_weight_balanced_negative():
123 # Test compute_class_weight when labels are negative
124 # Test with balanced class labels.
125 classes = np.array([-2, -1, 0])
126 y = np.asarray([-1, -1, 0, 0, -2, -2])
127
128 cw = compute_class_weight("balanced", classes=classes, y=y)
129 assert len(cw) == len(classes)
130 assert_array_almost_equal(cw, np.array([1.0, 1.0, 1.0]))
131
132
133def test_compute_class_weight_balanced_sample_weight_equivalence():
134 # Test with unbalanced and negative class labels for
135 # equivalence between repeated and weighted samples
136
137 classes = np.array([-2, -1, 0])
138 y = np.asarray([-1, -1, 0, 0, -2, -2])
139 sw = np.asarray([1, 0, 1, 1, 1, 2])
140
141 y_rep = np.repeat(y, sw, axis=0)
142
143 class_weights_weighted = compute_class_weight(
144 "balanced", classes=classes, y=y, sample_weight=sw
145 )
146 class_weights_repeated = compute_class_weight("balanced", classes=classes, y=y_rep)
147 assert len(class_weights_weighted) == len(classes)
148 assert len(class_weights_repeated) == len(classes)
149
150 class_counts_weighted = np.bincount(y + 2, weights=sw)
151 class_counts_repeated = np.bincount(y_rep + 2)
152
153 assert np.dot(class_weights_weighted, class_counts_weighted) == pytest.approx(
154 np.dot(class_weights_repeated, class_counts_repeated)
155 )
156
157 assert_allclose(class_weights_weighted, class_weights_repeated)
158
159
160def test_compute_class_weight_balanced_unordered():
161 # Test compute_class_weight when classes are unordered
162 classes = np.array([1, 0, 3])
163 y = np.asarray([1, 0, 0, 3, 3, 3])
164
165 cw = compute_class_weight("balanced", classes=classes, y=y)
166 class_counts = np.bincount(y)[classes]
167 assert_almost_equal(np.dot(cw, class_counts), y.shape[0])
168 assert_array_almost_equal(cw, [2.0, 1.0, 2.0 / 3])
169
170
171def test_compute_class_weight_default():
172 # Test for the case where no weight is given for a present class.
173 # Current behaviour is to assign the unweighted classes a weight of 1.
174 y = np.asarray([2, 2, 2, 3, 3, 4])
175 classes = np.unique(y)
176 classes_len = len(classes)
177
178 # Test for non specified weights
179 cw = compute_class_weight(None, classes=classes, y=y)
180 assert len(cw) == classes_len
181 assert_array_almost_equal(cw, np.ones(3))
182
183 # Tests for partly specified weights
184 cw = compute_class_weight({2: 1.5}, classes=classes, y=y)
185 assert len(cw) == classes_len
186 assert_array_almost_equal(cw, [1.5, 1.0, 1.0])
187
188 cw = compute_class_weight({2: 1.5, 4: 0.5}, classes=classes, y=y)
189 assert len(cw) == classes_len
190 assert_array_almost_equal(cw, [1.5, 1.0, 0.5])
191
192
193def test_compute_sample_weight():
194 # Test (and demo) compute_sample_weight.
195 # Test with balanced classes
196 y = np.asarray([1, 1, 1, 2, 2, 2])
197 sample_weight = compute_sample_weight("balanced", y)
198 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
199
200 # Test with user-defined weights
201 sample_weight = compute_sample_weight({1: 2, 2: 1}, y)
202 assert_array_almost_equal(sample_weight, [2.0, 2.0, 2.0, 1.0, 1.0, 1.0])
203
204 # Test with column vector of balanced classes
205 y = np.asarray([[1], [1], [1], [2], [2], [2]])
206 sample_weight = compute_sample_weight("balanced", y)
207 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
208
209 # Test with unbalanced classes
210 y = np.asarray([1, 1, 1, 2, 2, 2, 3])
211 sample_weight = compute_sample_weight("balanced", y)
212 expected_balanced = np.array(
213 [0.7777, 0.7777, 0.7777, 0.7777, 0.7777, 0.7777, 2.3333]
214 )
215 assert_array_almost_equal(sample_weight, expected_balanced, decimal=4)
216
217 # Test with `None` weights
218 sample_weight = compute_sample_weight(None, y)
219 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
220
221 # Test with multi-output of balanced classes
222 y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1]])
223 sample_weight = compute_sample_weight("balanced", y)
224 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
225
226 # Test with multi-output with user-defined weights
227 y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1]])
228 sample_weight = compute_sample_weight([{1: 2, 2: 1}, {0: 1, 1: 2}], y)
229 assert_array_almost_equal(sample_weight, [2.0, 2.0, 2.0, 2.0, 2.0, 2.0])
230
231 # Test with multi-output of unbalanced classes
232 y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1], [3, -1]])
233 sample_weight = compute_sample_weight("balanced", y)
234 assert_array_almost_equal(sample_weight, expected_balanced**2, decimal=3)
235
236
237def test_compute_sample_weight_with_subsample():
238 # Test compute_sample_weight with subsamples specified.
239 # Test with balanced classes and all samples present
240 y = np.asarray([1, 1, 1, 2, 2, 2])
241 sample_weight = compute_sample_weight("balanced", y, indices=range(6))
242 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
243
244 # Test with column vector of balanced classes and all samples present
245 y = np.asarray([[1], [1], [1], [2], [2], [2]])
246 sample_weight = compute_sample_weight("balanced", y, indices=range(6))
247 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
248
249 # Test with a subsample
250 y = np.asarray([1, 1, 1, 2, 2, 2])
251 sample_weight = compute_sample_weight("balanced", y, indices=range(4))
252 assert_array_almost_equal(sample_weight, [2.0 / 3, 2.0 / 3, 2.0 / 3, 2.0, 2.0, 2.0])
253
254 # Test with a bootstrap subsample
255 y = np.asarray([1, 1, 1, 2, 2, 2])
256 sample_weight = compute_sample_weight("balanced", y, indices=[0, 1, 1, 2, 2, 3])
257 expected_balanced = np.asarray([0.6, 0.6, 0.6, 3.0, 3.0, 3.0])
258 assert_array_almost_equal(sample_weight, expected_balanced)
259
260 # Test with a bootstrap subsample for multi-output
261 y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1]])
262 sample_weight = compute_sample_weight("balanced", y, indices=[0, 1, 1, 2, 2, 3])
263 assert_array_almost_equal(sample_weight, expected_balanced**2)
264
265 # Test with a missing class
266 y = np.asarray([1, 1, 1, 2, 2, 2, 3])
267 sample_weight = compute_sample_weight("balanced", y, indices=range(6))
268 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0])
269
270 # Test with a missing class for multi-output
271 y = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1], [2, 2]])
272 sample_weight = compute_sample_weight("balanced", y, indices=range(6))
273 assert_array_almost_equal(sample_weight, [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0])
274
275
276@pytest.mark.parametrize(
277 "y_type, class_weight, indices, err_msg",
278 [
279 (
280 "single-output",
281 {1: 2, 2: 1},
282 range(4),
283 "The only valid class_weight for subsampling is 'balanced'.",
284 ),
285 (
286 "multi-output",
287 {1: 2, 2: 1},
288 None,
289 "For multi-output, class_weight should be a list of dicts, or the string",
290 ),
291 (
292 "multi-output",
293 [{1: 2, 2: 1}],
294 None,
295 r"Got 1 element\(s\) while having 2 outputs",
296 ),
297 ],
298)
299def test_compute_sample_weight_errors(y_type, class_weight, indices, err_msg):
300 # Test compute_sample_weight raises errors expected.
301 # Invalid preset string
302 y_single_output = np.asarray([1, 1, 1, 2, 2, 2])
303 y_multi_output = np.asarray([[1, 0], [1, 0], [1, 0], [2, 1], [2, 1], [2, 1]])
304
305 y = y_single_output if y_type == "single-output" else y_multi_output
306 with pytest.raises(ValueError, match=err_msg):
307 compute_sample_weight(class_weight, y, indices=indices)
308
309
310def test_compute_sample_weight_more_than_32():
311 # Non-regression smoke test for #12146
312 y = np.arange(50) # more than 32 distinct classes
313 indices = np.arange(50) # use subsampling
314 weight = compute_sample_weight("balanced", y, indices=indices)
315 assert_array_almost_equal(weight, np.ones(y.shape[0]))
316
317
318def test_class_weight_does_not_contains_more_classes():
319 """Check that class_weight can contain more labels than in y.
320
321 Non-regression test for #22413
322 """
323 tree = DecisionTreeClassifier(class_weight={0: 1, 1: 10, 2: 20})
324
325 # Does not raise
326 tree.fit([[0, 0, 1], [1, 0, 1], [1, 2, 0]], [0, 0, 1])
327
328
329@pytest.mark.parametrize("csc_container", CSC_CONTAINERS)
330def test_compute_sample_weight_sparse(csc_container):
331 """Check that we can compute weight for sparse `y`."""
332 y = csc_container(np.asarray([[0], [1], [1]]))
333 sample_weight = compute_sample_weight("balanced", y)
334 assert_allclose(sample_weight, [1.5, 0.75, 0.75])
335 