Repository: scikit-learn/scikit-learn
Pull request: [MRG] Weights parameter of datasets.make_classification changed to array-like from list only - Issue 14760
####  Fixes #14760

#### What does this implement/fix? 
The weights parameter can be a list or array, not just a list, e.g.  
" `weights` is array-like or None " in docstring.


Problem statement:
datasets :: make_classification() weights parameter should be a sequence (not just a list). 
### `weights` should be passed as list or array (not just list) in `sklearn\datasets\samples_generator.py :: make_classification`:
 If there is a pertinent reason that `weights` must be a list, while *all other iterable parameters are arrays*, then it should be mentioned in the docstring. Otherwise, the docstring should be amended as in `make_blobs`, e.g.  "weights : list of floats or None (default=None)" -> "weights : sequence of floats or None (default=None)", along with amended lines 165 and 171 (see Corrections).

#### Test code to reproduce:
``` 
print('Testing weights type in `datasets.make_classification`:')
# using defaults except for weights (& random_state=1):

w = [0.25, 0.75]
print('  Test 1: weights as List {}'.format(w))
X, y = make_classification(weights=w, random_state=1)
print('  Test 1 result: len(X)={}, len(y)={}'.format(len(X),len(y)))

w = np.array([0.25, 0.75]) 
print('  Test 2: weights as np.array {}'.format(w))
X, y = make_classification(weights=w, random_state=1)
print('  Test 2 result: len(X)={}, len(y)={}, '.format(len(X),len(y)))
```
#### Expected Results:
Should not fail: np.array as valid as a list:
```
Testing weights type in `make_classification`:
  Test 1: weights as List [0.25, 0.75]
  Test 1 result: len(X)=100, len(y)=100
  Test 2: weights as np.array [0.25, 0.75]
  Test 2 result: len(X)=100, len(y)=100
```

#### Actual Results
```
Testing weights type in `make_classification`:
  Test 1: weights as List [0.25, 0.75]
  Test 1 result: len(X)=100, len(y)=100
  Test 2: weights as np.array [0.25, 0.75]
```
```error
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-c297f465db24> in <module>
     13 print('  Test 2: weights as np.array {}'.format(w))
     14 X, y = make_classification(weights=w,
---> 15                            random_state=1)
     16 print('  Test 2 result: len(X)={}, len(y)={}, '.format(len(X),len(y)))

~\Anaconda3\envs\dsml\lib\site-packages\sklearn\datasets\samples_generator.py in make_classification(n_samples, n_features, n_informative, n_redundant, n_repeated, n_classes, n_clusters_per_class, weights, flip_y, class_sep, hypercube, shift, scale, shuffle, random_state)
    163         raise ValueError("n_classes * n_clusters_per_class must"
    164                          " be smaller or equal 2 ** n_informative")
--> 165     if weights and len(weights) not in [n_classes, n_classes - 1]:
    166         raise ValueError("Weights specified but incompatible with number "
    167                          "of classes.")

ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
```

#### Corrections needed (fix ready):
```
165     if all(weights) and (len(weights) not in [n_classes, n_classes - 1]):

171     if all(weights) and len(weights) == (n_classes - 1):
```

#### Versions:
```  
    System:
    python: 3.6.7 (default, Feb 28 2019, 07:28:18) [MSC v.1900 64 bit (AMD64)]
executable: C:\<conda env path>\python.exe
   machine: Windows-10-10.0.18362-SP0 [same outcome with Windows-10-10.0.17134-SP0]

Python deps:
       pip: 19.1.1
setuptools: 41.0.1
   sklearn: 0.21.3
     numpy: 1.16.4
     scipy: 1.3.0
    Cython: None
    pandas: 0.24.2
```
#wimlds

Hints from issue discussion:
sounds good, please open a PR

Commit message: Added coverage for weiths resizing in `test_make_classification_weights_array_or_list_ok` as per @NicolasHug.

Patch to review:
diff --git a/sklearn/datasets/samples_generator.py b/sklearn/datasets/samples_generator.py
index f42eb9d83798c..664692441665f 100644
--- a/sklearn/datasets/samples_generator.py
+++ b/sklearn/datasets/samples_generator.py
@@ -91,7 +91,8 @@ def make_classification(n_samples=100, n_features=20, n_informative=2,
     n_clusters_per_class : int, optional (default=2)
         The number of clusters per class.
 
-    weights : list of floats or None (default=None)
+    weights : array-like of shape (n_classes,) or (n_classes - 1,),
+              (default=None)
         The proportions of samples assigned to each class. If None, then
         classes are balanced. Note that if ``len(weights) == n_classes - 1``,
         then the last class weight is automatically inferred.
@@ -160,22 +161,27 @@ def make_classification(n_samples=100, n_features=20, n_informative=2,
                          " features")
     # Use log2 to avoid overflow errors
     if n_informative < np.log2(n_classes * n_clusters_per_class):
-        raise ValueError("n_classes * n_clusters_per_class must"
-                         " be smaller or equal 2 ** n_informative")
-    if weights and len(weights) not in [n_classes, n_classes - 1]:
-        raise ValueError("Weights specified but incompatible with number "
-                         "of classes.")
+        msg = "n_classes({}) * n_clusters_per_class({}) must be"
+        msg += " smaller or equal 2**n_informative({})={}"
+        raise ValueError(msg.format(n_classes, n_clusters_per_class,
+                                    n_informative, 2**n_informative))
+
+    if weights is not None:
+        if len(weights) not in [n_classes, n_classes - 1]:
+            raise ValueError("Weights specified but incompatible with number "
+                             "of classes.")
+        if len(weights) == n_classes - 1:
+            if isinstance(weights, list):
+                weights = weights + [1.0 - sum(weights)]
+            else:
+                weights = np.resize(weights, n_classes)
+                weights[-1] = 1.0 - sum(weights[:-1])
+    else:
+        weights = [1.0 / n_classes] * n_classes
 
     n_useless = n_features - n_informative - n_redundant - n_repeated
     n_clusters = n_classes * n_clusters_per_class
 
-    if weights and len(weights) == (n_classes - 1):
-        weights = weights + [1.0 - sum(weights)]
-
-    if weights is None:
-        weights = [1.0 / n_classes] * n_classes
-        weights[-1] = 1.0 - sum(weights[:-1])
-
     # Distribute samples among clusters by weight
     n_samples_per_cluster = [
         int(n_samples * weights[k % n_classes] / n_clusters_per_class)
diff --git a/sklearn/datasets/tests/test_samples_generator.py b/sklearn/datasets/tests/test_samples_generator.py
index ecd7e7cba1ef1..e2abe7353243d 100644
--- a/sklearn/datasets/tests/test_samples_generator.py
+++ b/sklearn/datasets/tests/test_samples_generator.py
@@ -146,6 +146,46 @@ def test_make_classification_informative_features():
              n_clusters_per_class=2)
 
 
+@pytest.mark.parametrize(
+    'weights, err_type, err_msg',
+    [
+        ([], ValueError,
+         "Weights specified but incompatible with number of classes."),
+        ([.25, .75, .1], ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.array([]), ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.array([.25, .75, .1]), ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.random.random(3), ValueError,
+         "Weights specified but incompatible with number of classes.")
+    ]
+)
+def test_make_classification_weights_type(weights, err_type, err_msg):
+    with pytest.raises(err_type, match=err_msg):
+        make_classification(weights=weights)
+
+
+def test_make_classification_weights_array_or_list_ok():
+    X1, y1 = make_classification(weights=[.1, .9],
+                                 random_state=0)
+    X2, y2 = make_classification(weights=np.array([.1, .9]),
+                                 random_state=0)
+    assert (X1.all() == X2.all()) and (y1.all() == y2.all())
+
+    # Case where weights length < # classes. n_informative is
+    # also increased to 3 to avoid the informative check error.
+    X1, y1 = make_classification(weights=[.1, .9],
+                                 n_classes=3,
+                                 n_informative=3,
+                                 random_state=0)
+    X2, y2 = make_classification(weights=np.array([.1, .9]),
+                                 n_classes=3,
+                                 n_informative=3,
+                                 random_state=0)
+    assert (X1.all() == X2.all()) and (y1.all() == y2.all())
+
+
 def test_make_multilabel_classification_return_sequences():
     for allow_unlabeled, min_length in zip((True, False), (0, 1)):
         X, Y = make_multilabel_classification(n_samples=100, n_features=20,


Code review comments:
diff_hunk: @@ -146,6 +146,46 @@ def test_make_classification_informative_features():
              n_clusters_per_class=2)
 
 
+@pytest.mark.parametrize(
+    'weights, err_type, err_msg',
+    [
+        ([], ValueError,
+         "Weights specified but incompatible with number of classes."),
+        ([.25, .75, .1], ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.array([]), ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.array([.25, .75, .1]), ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.random.random(3), ValueError,
+         "Weights specified but incompatible with number of classes.")
+    ]
+)
+def test_make_classification_weights_type(weights, err_type, err_msg):
+    with pytest.raises(err_type, match=err_msg):
+        make_classification(weights=weights)
+
+
+def test_make_classification_weights_array_or_list_ok():
+    X1, y1 = make_classification(weights=[.1, .9],
+                                 random_state=0)
+    X2, y2 = make_classification(weights=np.array([.1, .9]),
+                                 random_state=0)
+    assert (X1.all() == X2.all()) and (y1.all() == y2.all())
path: sklearn/datasets/tests/test_samples_generator.py
text: @user1:
`X1.all()` returns True if X1 is all non-zero. Is this assertion to do the following:

```py
assert_almost_equal(X1, X2)
assert_almost_equal(y1, y2)
```

diff_hunk: @@ -146,6 +146,46 @@ def test_make_classification_informative_features():
              n_clusters_per_class=2)
 
 
+@pytest.mark.parametrize(
+    'weights, err_type, err_msg',
+    [
+        ([], ValueError,
+         "Weights specified but incompatible with number of classes."),
+        ([.25, .75, .1], ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.array([]), ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.array([.25, .75, .1]), ValueError,
+         "Weights specified but incompatible with number of classes."),
+        (np.random.random(3), ValueError,
+         "Weights specified but incompatible with number of classes.")
+    ]
+)
+def test_make_classification_weights_type(weights, err_type, err_msg):
+    with pytest.raises(err_type, match=err_msg):
+        make_classification(weights=weights)
+
+
+def test_make_classification_weights_array_or_list_ok():
path: sklearn/datasets/tests/test_samples_generator.py
text: @user2:
This can be parametrized:

```py
@user1.mark.parametrize("kwargs", [{}, {"n_classes": 3, "n_informative": 3}])
def test_make_classification_weights_array_or_list_ok(kwargs):
    X1, y1 = make_classification(weights=[.1, .9],
                                 random_state=0, **kwargs)
    X2, y2 = make_classification(weights=np.array([.1, .9]),
                                 random_state=0, **kwargs)
    assert_almost_equal(X1, X2)
    assert_almost_equal(y1, y2)
```

@author:
Thank you!