Repository: scipy/scipy

Issue:
Bizarre times for `scipy.sparse.rand` function with 'low' density values.
The `scipy.sparse.rand` function is strangely slow for some densities. It seems like it can be made much faster without much effort. The `sparse_random` function below seems to be about 10x faster on the worst case of the `scipy.sparse.rand` function. The timing plots are equally bizarre, showing terrible performance up until around `density=0.4` and then pretty much equivalent timings between both functions.

Judging by the code (https://github.com/scipy/scipy/blob/v1.1.0/scipy/sparse/construct.py#L775), it looks to be caused by this conditional

```
    # Use the algorithm from python's random.sample for k < mn/3.
    if mn < 3*k:
        ind = random_state.choice(mn, size=k, replace=False)
    else:
        ind = np.empty(k, dtype=tp)
        selected = set()
        for i in xrange(k):
            j = random_state.randint(mn)
            while j in selected:
                j = random_state.randint(mn)
            selected.add(j)
```

I'm not sure why this is even there and it is not simply
```
ind = random_state.choice(mn, size=k, replace=False)
```


### Reproducing code example:
```
import scipy as sp
import scipy.sparse
import numpy as np
from contexttimer import Timer
import matplotlib.pyplot as plt

def sparse_random(n_rows, n_cols, density):
    """A faster implementation of scipy.sparse.rand?"""
    if density == 0:
        return sp.sparse.coo_matrix((n_rows, n_cols))
    N = n_rows*n_cols
    nnz = int(N*density)
    idx = np.random.choice(N, nnz, replace=False)
    rows, cols = np.divmod(idx, n_cols)
    data = np.random.rand(nnz)
    return sp.sparse.coo_matrix((data, (rows, cols)), shape=(n_rows,n_cols))

def time_test(n_rows, n_cols):
    densities = np.arange(0, 1.1, 0.1)
    times_sp = np.empty(densities.size)
    times = np.empty(densities.size)
    for i, density in enumerate(densities):
        with Timer() as t:
            sp.sparse.rand(n_rows,n_cols,density)
        times_sp[i] = t.elapsed
        
        with Timer() as t:
            sparse_random(n_rows,n_cols,density)
        times[i] = t.elapsed
    plt.plot(densities, times_sp)
    plt.plot(densities, times)
    plt.legend(['sp.sparse.rand', 'sparse_random'])
    plt.show()

time_test(120, 80)
```

My timings are below:

![bizarre_sparse_rand](https://user-images.githubusercontent.com/3892689/42734765-cf729b3e-8849-11e8-8c51-93b417952b21.png)


### Scipy/Numpy/Python version information:
```
In [1]: import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
1.1.0 1.14.5 sys.version_info(major=3, minor=6, micro=4, releaselevel='final', serial=0)
```




Discussion:
It turns out this is mostly a historical artifact. Here's the commit that added the code in question: https://github.com/scipy/scipy/commit/589c372da5e415c4c0c33171971d3b26102b8b3c

1. We couldn't use `random_state.choice` until numpy 1.7 became the minimum required version.
2. The workaround using `random_state.permutation` was inefficient for small densities, so a special case was added.
3. When the numpy version bump happened in commit https://github.com/scipy/scipy/commit/aa6bc5b79860d9ec8cd3069976c608482ed0d06c, we didn't notice that the special case was no longer needed (and apparently slower, as well).

Feel free to send a PR with the fix, if you'd like!
Ok, I'll fix this.

Resolving patch:
diff --git a/benchmarks/benchmarks/sparse.py b/benchmarks/benchmarks/sparse.py
--- a/benchmarks/benchmarks/sparse.py
+++ b/benchmarks/benchmarks/sparse.py
@@ -423,3 +423,20 @@ def time_toarray(self, format, order):
 
     # Retain old benchmark results (remove this if changing the benchmark)
     time_toarray.version = "2fbf492ec800b982946a62785beda803460b913cc80080043a5d407025893b2b"
+
+
+class Random(Benchmark):
+    params = [
+        np.arange(0, 1.1, 0.1).tolist()
+    ]
+    param_names = ['density']
+
+    def setup(self, density):
+        warnings.simplefilter('ignore', SparseEfficiencyWarning)
+        self.nrows = 1000
+        self.ncols = 1000
+        self.format = 'csr'
+
+    def time_rand(self, density):
+        sparse.rand(self.nrows, self.ncols,
+                    format=self.format, density=density)
diff --git a/scipy/sparse/construct.py b/scipy/sparse/construct.py
--- a/scipy/sparse/construct.py
+++ b/scipy/sparse/construct.py
@@ -711,14 +711,20 @@ def random(m, n, density=0.01, format='coo', dtype=None,
     -------
     res : sparse matrix
 
+    Notes
+    -----
+    Only float types are supported for now.
+
     Examples
     --------
     >>> from scipy.sparse import random
     >>> from scipy import stats
-    >>> class CustomRandomState(object):
+
+    >>> class CustomRandomState(np.random.RandomState):
     ...     def randint(self, k):
     ...         i = np.random.randint(k)
     ...         return i - i % 2
+    >>> np.random.seed(12345)
     >>> rs = CustomRandomState()
     >>> rvs = stats.poisson(25, loc=10).rvs
     >>> S = random(3, 4, density=0.25, random_state=rs, data_rvs=rvs)
@@ -736,13 +742,10 @@ def random(m, n, density=0.01, format='coo', dtype=None,
     >>> Y = X()  # get a frozen version of the distribution
     >>> S = random(3, 4, density=0.25, random_state=2906, data_rvs=Y.rvs)
     >>> S.A
-    array([[ 0.        ,  1.9467163 ,  0.13569738, -0.81205367],
-           [ 0.        ,  0.        ,  0.        ,  0.        ],
+    array([[ 0.        ,  0.        ,  0.        ,  0.        ],
+           [ 0.13569738,  1.9467163 , -0.81205367,  0.        ],
            [ 0.        ,  0.        ,  0.        ,  0.        ]])
 
-    Notes
-    -----
-    Only float types are supported for now.
     """
     if density < 0 or density > 1:
         raise ValueError("density expected to be 0 <= density <= 1")
@@ -768,6 +771,7 @@ def random(m, n, density=0.01, format='coo', dtype=None,
         random_state = np.random
     elif isinstance(random_state, (int, np.integer)):
         random_state = np.random.RandomState(random_state)
+
     if data_rvs is None:
         if np.issubdtype(dtype, np.integer):
             randint = get_randint(random_state)
@@ -781,18 +785,7 @@ def data_rvs(n):
         else:
             data_rvs = random_state.rand
 
-    # Use the algorithm from python's random.sample for k < mn/3.
-    if mn < 3 * k:
-        ind = random_state.choice(mn, size=k, replace=False)
-    else:
-        ind = np.empty(k, dtype=tp)
-        selected = set()
-        for i in xrange(k):
-            j = random_state.randint(mn)
-            while j in selected:
-                j = random_state.randint(mn)
-            selected.add(j)
-            ind[i] = j
+    ind = random_state.choice(mn, size=k, replace=False)
 
     j = np.floor(ind * 1. / m).astype(tp, copy=False)
     i = (ind - j * m).astype(tp, copy=False)
@@ -841,8 +834,9 @@ def rand(m, n, density=0.01, format="coo", dtype=None, random_state=None):
     <3x4 sparse matrix of type '<class 'numpy.float64'>'
        with 3 stored elements in Compressed Sparse Row format>
     >>> matrix.todense()
-    matrix([[ 0.        ,  0.59685016,  0.779691  ,  0.        ],
-            [ 0.        ,  0.        ,  0.        ,  0.44583275],
-            [ 0.        ,  0.        ,  0.        ,  0.        ]])
+    matrix([[0.05641158, 0.        , 0.        , 0.65088847],
+            [0.        , 0.        , 0.        , 0.14286682],
+            [0.        , 0.        , 0.        , 0.        ]])
+
     """
     return random(m, n, density, format, dtype, random_state)


Test patch:
diff --git a/scipy/optimize/tests/test_lsq_linear.py b/scipy/optimize/tests/test_lsq_linear.py
--- a/scipy/optimize/tests/test_lsq_linear.py
+++ b/scipy/optimize/tests/test_lsq_linear.py
@@ -130,13 +130,13 @@ def test_sparse_bounds(self):
         lb = self.rnd.randn(n)
         ub = lb + 1
         res = lsq_linear(A, b, (lb, ub))
-        assert_allclose(res.optimality, 0.0, atol=1e-8)
+        assert_allclose(res.optimality, 0.0, atol=1e-6)
 
         res = lsq_linear(A, b, (lb, ub), lsmr_tol=1e-13)
-        assert_allclose(res.optimality, 0.0, atol=1e-8)
+        assert_allclose(res.optimality, 0.0, atol=1e-6)
 
         res = lsq_linear(A, b, (lb, ub), lsmr_tol='auto')
-        assert_allclose(res.optimality, 0.0, atol=1e-8)
+        assert_allclose(res.optimality, 0.0, atol=1e-6)
 
 
 class TestTRF(BaseMixin, SparseMixin):