Repository: scipy/scipy

Issue:
Can no longer slice csr_matrix in 1.3.0
I have a code example that works in scipy 1.1.0, 1.2.0, 1.2.1 and has a stack trace in 1.3.0, 1.3.1, 1.3.2.

#### Reproducing code example:

```
import pandas as pd
from scipy.sparse import csr_matrix

df = pd.DataFrame({'thing': [1, 1, 2, 2, 2],
                   'score': [0.12, 0.13, 0.14, 0.15, 0.17]})

row_index = df['thing'] == 1
print(type(row_index), row_index)
print(df[row_index])
sdf = csr_matrix(df)
print(sdf[row_index])
```

#### Error message:

```
Traceback (most recent call last):
  File "./foo.py", line 13, in <module>
    print(sdf[row_index])
  File "root/.venv/lib/python3.7/site-packages/scipy/sparse/_index.py", line 59, in __getitem__
    return self._get_arrayXslice(row, col)
  File "root/.venv/lib/python3.7/site-packages/scipy/sparse/csr.py", line 325, in _get_arrayXslice
    return self._major_index_fancy(row)._get_submatrix(minor=col)
  File "root/.venv/lib/python3.7/site-packages/scipy/sparse/compressed.py", line 690, in _major_index_fancy
    np.cumsum(row_nnz[idx], out=res_indptr[1:])
  File "<__array_function__ internals>", line 6, in cumsum
  File "root/.venv/lib/python3.7/site-packages/numpy/core/fromnumeric.py", line 2423, in cumsum
    return _wrapfunc(a, 'cumsum', axis=axis, dtype=dtype, out=out)
  File "root/.venv/lib/python3.7/site-packages/numpy/core/fromnumeric.py", line 61, in _wrapfunc
    return bound(*args, **kwds)
ValueError: provided out is the wrong size for the reduction
```

#### Output of working version (on earlier versions of scipy):

```
<class 'pandas.core.series.Series'> 0     True
1     True
2    False
3    False
4    False
Name: thing, dtype: bool
   thing  score
0      1   0.12
1      1   0.13
  (0, 0)	1.0
  (0, 1)	0.13
  (1, 0)	1.0
  (1, 1)	0.13
  (2, 0)	1.0
  (2, 1)	0.12
  (3, 0)	1.0
  (3, 1)	0.12
  (4, 0)	1.0
  (4, 1)	0.12
```

#### Scipy/Numpy/Python version information:
```
1.3.0 1.17.3 sys.version_info(major=3, minor=7, micro=4, releaselevel='final', serial=0)
```

See the same info on stackoverflow [here](https://stackoverflow.com/questions/59273855/how-to-select-only-some-rows-of-a-scipy-sparse-csr-matrix).

I'm running in OS X, tried pandas 0.25 and 0.25.3, though I don't know that those matter.


Discussion:
[This issue](https://github.com/scipy/scipy/issues/9639) may or may not be related.
I can confirm this issue is present on master (1.5.0.dev0+0a7b39a).
The issue here stems from the use of a pandas Series object as a boolean/logical index array. We check for boolean indices here:

https://github.com/scipy/scipy/blob/0a7b39a3973bf0ed95a497764445ccbaed7e990b/scipy/sparse/_index.py#L279

Since `pandas.Series` is not a subclass of `numpy.ndarray`, the indices are not converted to integers, breaking the assumptions in the code that follows.

You can see this in the posted example:

```
In [29]: sdf[row_index.values]                                                  
Out[29]: 
<2x2 sparse matrix of type '<class 'numpy.float64'>'
	with 4 stored elements in Compressed Sparse Row format>
```

To see that the change is taking place in index validation:

```
In [30]: sdf._validate_indices((row_index, slice(None)))                        
Out[30]: (array([ True,  True, False, False, False]), slice(None, None, None))

In [31]: sdf._validate_indices((row_index.values, slice(None)))                 
Out[31]: (array([0, 1]), slice(None, None, None))
```

So for the time being, you can work around the issue by changing index arrays from Series to plain numpy arrays. A fix for the index validation to be less strict about types when checking for logical index arrays would solve this for good, though.
Thanks!
Instead of checking index array for `numpy.ndarray` wouldn't it be better if check for the presence of attribute `ndim`?  Pandas Series object already has attribute `ndim`. We may get the desired leniency in array type by making this change.

Resolving patch:
diff --git a/scipy/sparse/_index.py b/scipy/sparse/_index.py
--- a/scipy/sparse/_index.py
+++ b/scipy/sparse/_index.py
@@ -266,8 +266,13 @@ def _unpack_index(index):
         else:
             raise IndexError('invalid number of indices')
     else:
-        row, col = index, slice(None)
-
+        idx = _compatible_boolean_index(index)
+        if idx is None:
+            row, col = index, slice(None)
+        elif idx.ndim < 2:
+            return _boolean_index_to_array(idx), slice(None)
+        elif idx.ndim == 2:
+            return idx.nonzero()
     # Next, check for validity and transform the index as needed.
     if isspmatrix(row) or isspmatrix(col):
         # Supporting sparse boolean indexing with both row and col does
@@ -276,10 +281,12 @@ def _unpack_index(index):
             'Indexing with sparse matrices is not supported '
             'except boolean indexing where matrix and index '
             'are equal shapes.')
-    if isinstance(row, np.ndarray) and row.dtype.kind == 'b':
-        row = _boolean_index_to_array(row)
-    if isinstance(col, np.ndarray) and col.dtype.kind == 'b':
-        col = _boolean_index_to_array(col)
+    bool_row = _compatible_boolean_index(row)
+    bool_col = _compatible_boolean_index(col)
+    if bool_row is not None:
+        row = _boolean_index_to_array(bool_row)
+    if bool_col is not None:
+        col = _boolean_index_to_array(bool_col)
     return row, col
 
 
@@ -322,7 +329,41 @@ def _check_ellipsis(index):
     return index[:first_ellipsis] + (slice(None),)*nslice + tuple(tail)
 
 
+def _maybe_bool_ndarray(idx):
+    """Returns a compatible array if elements are boolean.
+    """
+    idx = np.asanyarray(idx)
+    if idx.dtype.kind == 'b':
+        return idx
+    return None
+
+
+def _first_element_bool(idx, max_dim=2):
+    """Returns True if first element of the incompatible
+    array type is boolean.
+    """
+    if max_dim < 1:
+        return None
+    try:
+        first = next(iter(idx), None)
+    except TypeError:
+        return None
+    if isinstance(first, bool):
+        return True
+    return _first_element_bool(first, max_dim-1)
+
+
+def _compatible_boolean_index(idx):
+    """Returns a boolean index array that can be converted to
+    integer array. Returns None if no such array exists.
+    """
+    # Presence of attribute `ndim` indicates a compatible array type.
+    if hasattr(idx, 'ndim') or _first_element_bool(idx):
+        return _maybe_bool_ndarray(idx)
+    return None
+
+
 def _boolean_index_to_array(idx):
     if idx.ndim > 1:
         raise IndexError('invalid index shape')
-    return idx.nonzero()[0]
+    return np.where(idx)[0]


Test patch:
diff --git a/scipy/sparse/tests/test_csr.py b/scipy/sparse/tests/test_csr.py
--- a/scipy/sparse/tests/test_csr.py
+++ b/scipy/sparse/tests/test_csr.py
@@ -95,3 +95,23 @@ def test_csr_empty_slices(matrix_input, axis, expected_shape):
 
     assert actual_shape_1 == expected_shape
     assert actual_shape_1 == actual_shape_2
+
+
+def test_csr_bool_indexing():
+    data = csr_matrix([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
+    list_indices1 = [False, True, False]
+    array_indices1 = np.array(list_indices1)
+    list_indices2 = [[False, True, False], [False, True, False], [False, True, False]]
+    array_indices2 = np.array(list_indices2)
+    list_indices3 = ([False, True, False], [False, True, False])
+    array_indices3 = (np.array(list_indices3[0]), np.array(list_indices3[1]))
+    slice_list1 = data[list_indices1].toarray()
+    slice_array1 = data[array_indices1].toarray()
+    slice_list2 = data[list_indices2]
+    slice_array2 = data[array_indices2]
+    slice_list3 = data[list_indices3]
+    slice_array3 = data[array_indices3]
+    assert (slice_list1 == slice_array1).all()
+    assert (slice_list2 == slice_array2).all()
+    assert (slice_list3 == slice_array3).all()
+