CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_class_weight.py335 linesDownload Raw Back to tests
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