Repository: scipy/scipy

Issue:
sparse matrix element-wise multiplying dense matrix returns dense matrix
When a scipy sparse matrix element-wise multiples a dense ndarray, the returned matrix is of type matrix, which seems very inefficient, given that it is obviously a sparse matrix.

```
In [90]: A = scipy.sparse.csr_matrix((5, 5))

In [93]: B = np.random.randn(5, 5)

In [96]: C = A.multiply(B)

In [98]: type(C)
Out[98]: numpy.matrixlib.defmatrix.matrix

In [100]: C
Out[100]: 
matrix([[-0., -0.,  0.,  0.,  0.],
        [ 0.,  0., -0., -0., -0.],
        [ 0., -0.,  0.,  0., -0.],
        [-0., -0.,  0.,  0.,  0.],
        [ 0.,  0.,  0., -0., -0.]])
```

Is there any reason behind this that I am missing? 



Discussion:
I had a pull request aimed at solving this problem, but I forgot to follow up on it: #4133.

I'll look into fixing it now.

Ok, PR #5057 is pending with a fix.

That's awesome. Thanks a lot!

What I mean is element-wise multiplication which is obviously sparse, not matrix multiplication. 

Ah! Sorry for the noise, missed that in your original post.

PR #5057 merged, so this issue can be closed now.

Great. Thanks for the help!

I don't think this issue has been fixed when broadcasting occurs. I'm running SciPy 0.18.1 and I get the following outputs:

```
>>> from scipy import sparse

>>> sparse.eye(4,3).multiply(np.ones((4,3)))
<4x3 sparse matrix of type '<class 'numpy.float64'>'
    with 3 stored elements in COOrdinate format>

# Broadcasting in sparse
>>> sparse.eye(4,1).multiply(np.ones((4,3)))
matrix([[ 1.,  1.,  1.],
        [ 0.,  0.,  0.],
        [ 0.,  0.,  0.],
        [ 0.,  0.,  0.]])

# Broadcasting in both
>>> sparse.eye(4,1).multiply(np.ones((1,3)))
matrix([[ 1.,  1.,  1.],
        [ 0.,  0.,  0.],
        [ 0.,  0.,  0.],
        [ 0.,  0.,  0.]])

# Broadcasting in dense
>>> sparse.eye(4,3).multiply(np.ones((4,1)))
matrix([[ 1.,  0.,  0.],
        [ 0.,  1.,  0.],
        [ 0.,  0.,  1.],
        [ 0.,  0.,  0.]])

```

I think all these should return sparse matrix results.


Resolving patch:
diff --git a/scipy/sparse/compressed.py b/scipy/sparse/compressed.py
--- a/scipy/sparse/compressed.py
+++ b/scipy/sparse/compressed.py
@@ -400,44 +400,87 @@ def multiply(self, other):
             elif self.shape == (1,1):
                 return other._mul_scalar(self.toarray()[0, 0])
             # A row times a column.
-            elif self.shape[1] == other.shape[0] and self.shape[1] == 1:
+            elif self.shape[1] == 1 and other.shape[0] == 1:
                 return self._mul_sparse_matrix(other.tocsc())
-            elif self.shape[0] == other.shape[1] and self.shape[0] == 1:
+            elif self.shape[0] == 1 and other.shape[1] == 1:
                 return other._mul_sparse_matrix(self.tocsc())
             # Row vector times matrix. other is a row.
             elif other.shape[0] == 1 and self.shape[1] == other.shape[1]:
                 other = dia_matrix((other.toarray().ravel(), [0]),
-                                    shape=(other.shape[1], other.shape[1]))
+                                   shape=(other.shape[1], other.shape[1]))
                 return self._mul_sparse_matrix(other)
             # self is a row.
             elif self.shape[0] == 1 and self.shape[1] == other.shape[1]:
                 copy = dia_matrix((self.toarray().ravel(), [0]),
-                                    shape=(self.shape[1], self.shape[1]))
+                                  shape=(self.shape[1], self.shape[1]))
                 return other._mul_sparse_matrix(copy)
             # Column vector times matrix. other is a column.
             elif other.shape[1] == 1 and self.shape[0] == other.shape[0]:
                 other = dia_matrix((other.toarray().ravel(), [0]),
-                                    shape=(other.shape[0], other.shape[0]))
+                                   shape=(other.shape[0], other.shape[0]))
                 return other._mul_sparse_matrix(self)
             # self is a column.
             elif self.shape[1] == 1 and self.shape[0] == other.shape[0]:
                 copy = dia_matrix((self.toarray().ravel(), [0]),
-                                    shape=(self.shape[0], self.shape[0]))
+                                  shape=(self.shape[0], self.shape[0]))
                 return copy._mul_sparse_matrix(other)
             else:
                 raise ValueError("inconsistent shapes")
-        # Dense matrix.
-        if isdense(other):
-            if self.shape == other.shape:
-                ret = self.tocoo()
-                ret.data = np.multiply(ret.data, other[ret.row, ret.col]
-                                       ).view(np.ndarray).ravel()
-                return ret
-            # Single element.
-            elif other.size == 1:
-                return self._mul_scalar(other.flat[0])
-        # Anything else.
-        return np.multiply(self.todense(), other)
+
+        # Assume other is a dense matrix/array, which produces a single-item
+        # object array if other isn't convertible to ndarray.
+        other = np.atleast_2d(other)
+
+        if other.ndim != 2:
+            return np.multiply(self.toarray(), other)
+        # Single element / wrapped object.
+        if other.size == 1:
+            return self._mul_scalar(other.flat[0])
+        # Fast case for trivial sparse matrix.
+        elif self.shape == (1, 1):
+            return np.multiply(self.toarray()[0,0], other)
+
+        from .coo import coo_matrix
+        ret = self.tocoo()
+        # Matching shapes.
+        if self.shape == other.shape:
+            data = np.multiply(ret.data, other[ret.row, ret.col])
+        # Sparse row vector times...
+        elif self.shape[0] == 1:
+            if other.shape[1] == 1:  # Dense column vector.
+                data = np.multiply(ret.data, other)
+            elif other.shape[1] == self.shape[1]:  # Dense matrix.
+                data = np.multiply(ret.data, other[:, ret.col])
+            else:
+                raise ValueError("inconsistent shapes")
+            row = np.repeat(np.arange(other.shape[0]), len(ret.row))
+            col = np.tile(ret.col, other.shape[0])
+            return coo_matrix((data.view(np.ndarray).ravel(), (row, col)),
+                              shape=(other.shape[0], self.shape[1]),
+                              copy=False)
+        # Sparse column vector times...
+        elif self.shape[1] == 1:
+            if other.shape[0] == 1:  # Dense row vector.
+                data = np.multiply(ret.data[:, None], other)
+            elif other.shape[0] == self.shape[0]:  # Dense matrix.
+                data = np.multiply(ret.data[:, None], other[ret.row])
+            else:
+                raise ValueError("inconsistent shapes")
+            row = np.repeat(ret.row, other.shape[1])
+            col = np.tile(np.arange(other.shape[1]), len(ret.col))
+            return coo_matrix((data.view(np.ndarray).ravel(), (row, col)),
+                              shape=(self.shape[0], other.shape[1]),
+                              copy=False)
+        # Sparse matrix times dense row vector.
+        elif other.shape[0] == 1 and self.shape[1] == other.shape[1]:
+            data = np.multiply(ret.data, other[:, ret.col].ravel())
+        # Sparse matrix times dense column vector.
+        elif other.shape[1] == 1 and self.shape[0] == other.shape[0]:
+            data = np.multiply(ret.data, other[ret.row].ravel())
+        else:
+            raise ValueError("inconsistent shapes")
+        ret.data = data.view(np.ndarray).ravel()
+        return ret
 
     ###########################
     # Multiplication handlers #


Test patch:
diff --git a/scipy/sparse/tests/test_base.py b/scipy/sparse/tests/test_base.py
--- a/scipy/sparse/tests/test_base.py
+++ b/scipy/sparse/tests/test_base.py
@@ -1301,8 +1301,9 @@ def test_elementwise_multiply_broadcast(self):
         H = np.ones((3, 4))
         J = H.T
         K = array([[0]])
+        L = array([[[1,2],[0,1]]])
 
-        # Rank 1 arrays can't be cast as spmatrices (A and C) so leave
+        # Some arrays can't be cast as spmatrices (A,C,L) so leave
         # them out.
         Bsp = self.spmatrix(B)
         Dsp = self.spmatrix(D)
@@ -1314,7 +1315,7 @@ def test_elementwise_multiply_broadcast(self):
         Jspp = self.spmatrix(J[:,0,None])
         Ksp = self.spmatrix(K)
 
-        matrices = [A, B, C, D, E, F, G, H, J, K]
+        matrices = [A, B, C, D, E, F, G, H, J, K, L]
         spmatrices = [Bsp, Dsp, Esp, Fsp, Hsp, Hspp, Jsp, Jspp, Ksp]
 
         # sparse/sparse
@@ -1326,16 +1327,15 @@ def test_elementwise_multiply_broadcast(self):
                     assert_raises(ValueError, i.multiply, j)
                     continue
                 sp_mult = i.multiply(j)
-                if isspmatrix(sp_mult):
-                    assert_almost_equal(sp_mult.todense(), dense_mult)
-                else:
-                    assert_almost_equal(sp_mult, dense_mult)
+                assert_almost_equal(sp_mult.todense(), dense_mult)
 
         # sparse/dense
         for i in spmatrices:
             for j in matrices:
                 try:
                     dense_mult = np.multiply(i.todense(), j)
+                except TypeError:
+                    continue
                 except ValueError:
                     assert_raises(ValueError, i.multiply, j)
                     continue