Repository: scipy/scipy

Issue:
Sparse implementation of argmax would be useful
Currently, there is no argmax implementation for sparse matrices, and after studying the code of the dense argmax implementation, I couldn't figure out a non-trivial way to implement it myself. 

I asked this question on StackOverflow about half a year ago, which lead to a potentially useful answer: http://stackoverflow.com/questions/30742572/argmax-of-each-row-or-column-in-scipy-sparse-matrix

Thanks for Scipy, by the way!



Discussion:
I agree that a sparse argmax would be useful. The SO responses look like fairly good starting points.


Resolving patch:
diff --git a/scipy/sparse/data.py b/scipy/sparse/data.py
--- a/scipy/sparse/data.py
+++ b/scipy/sparse/data.py
@@ -123,6 +123,18 @@ def method(self):
     setattr(_data_matrix, name, _create_method(npfunc))
 
 
+def _find_missing_index(ind, n):
+    for k, a in enumerate(ind):
+        if k != a:
+            return k
+
+    k += 1
+    if k < n:
+        return k
+    else:
+        return -1
+
+
 class _minmax_mixin(object):
     """Mixin for min and max methods.
 
@@ -181,6 +193,80 @@ def _min_or_max(self, axis, out, min_or_max):
         else:
             raise ValueError("axis out of range")
 
+    def _arg_min_or_max_axis(self, axis, op, compare):
+        if self.shape[axis] == 0:
+            raise ValueError("Can't apply the operation along a zero-sized "
+                             "dimension.")
+
+        if axis < 0:
+            axis += 2
+
+        zero = self.dtype.type(0)
+
+        mat = self.tocsc() if axis == 0 else self.tocsr()
+        mat.sum_duplicates()
+
+        ret_size, line_size = mat._swap(mat.shape)
+        ret = np.zeros(ret_size, dtype=int)
+
+        nz_lines, = np.nonzero(np.diff(mat.indptr))
+        for i in nz_lines:
+            p, q = mat.indptr[i:i + 2]
+            data = mat.data[p:q]
+            indices = mat.indices[p:q]
+            am = op(data)
+            m = data[am]
+            if compare(m, zero) or q - p == line_size:
+                ret[i] = indices[am]
+            else:
+                zero_ind = _find_missing_index(indices, line_size)
+                if m == zero:
+                    ret[i] = min(am, zero_ind)
+                else:
+                    ret[i] = zero_ind
+
+        if axis == 1:
+            ret = ret.reshape(-1, 1)
+
+        return np.asmatrix(ret)
+
+    def _arg_min_or_max(self, axis, out, op, compare):
+        if out is not None:
+            raise ValueError("Sparse matrices do not support "
+                             "an 'out' parameter.")
+
+        validateaxis(axis)
+
+        if axis is None:
+            if 0 in self.shape:
+                raise ValueError("Can't apply the operation to "
+                                 "an empty matrix.")
+
+            if self.nnz == 0:
+                return 0
+            else:
+                zero = self.dtype.type(0)
+                mat = self.tocoo()
+                mat.sum_duplicates()
+                am = op(mat.data)
+                m = mat.data[am]
+
+                if compare(m, zero):
+                    return mat.row[am] * mat.shape[1] + mat.col[am]
+                else:
+                    size = np.product(mat.shape)
+                    if size == mat.nnz:
+                        return am
+                    else:
+                        ind = mat.row * mat.shape[1] + mat.col
+                        zero_ind = _find_missing_index(ind, size)
+                        if m == zero:
+                            return min(zero_ind, am)
+                        else:
+                            return zero_ind
+
+        return self._arg_min_or_max_axis(axis, op, compare)
+
     def max(self, axis=None, out=None):
         """
         Return the maximum of the matrix or maximum along an axis.
@@ -244,3 +330,49 @@ def min(self, axis=None, out=None):
 
         """
         return self._min_or_max(axis, out, np.minimum)
+
+    def argmax(self, axis=None, out=None):
+        """Return indices of minimum elements along an axis.
+
+        Implicit zero elements are also taken into account. If there are
+        several maximum values, the index of the first occurrence is returned.
+
+        Parameters
+        ----------
+        axis : {-2, -1, 0, 1, None}, optional
+            Axis along which the argmax is computed. If None (default), index
+            of the maximum element in the flatten data is returned.
+        out : None, optional
+            This argument is in the signature *solely* for NumPy
+            compatibility reasons. Do not pass in anything except for
+            the default value, as this argument is not used.
+
+        Returns
+        -------
+        ind : np.matrix or int
+            Indices of maximum elements. If matrix, its size along `axis` is 1.
+        """
+        return self._arg_min_or_max(axis, out, np.argmax, np.greater)
+
+    def argmin(self, axis=None, out=None):
+        """Return indices of minimum elements along an axis.
+
+        Implicit zero elements are also taken into account. If there are
+        several minimum values, the index of the first occurrence is returned.
+
+        Parameters
+        ----------
+        axis : {-2, -1, 0, 1, None}, optional
+            Axis along which the argmin is computed. If None (default), index
+            of the minimum element in the flatten data is returned.
+        out : None, optional
+            This argument is in the signature *solely* for NumPy
+            compatibility reasons. Do not pass in anything except for
+            the default value, as this argument is not used.
+
+        Returns
+        -------
+         ind : np.matrix or int
+            Indices of minimum elements. If matrix, its size along `axis` is 1.
+        """
+        return self._arg_min_or_max(axis, out, np.argmin, np.less)


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
@@ -3233,6 +3233,45 @@ def test_numpy_minmax(self):
             assert_array_equal(np.min(datsp), np.min(dat))
             assert_array_equal(np.max(datsp), np.max(dat))
 
+    def test_argmax(self):
+        D1 = np.array([
+            [-1, 5, 2, 3],
+            [0, 0, -1, -2],
+            [-1, -2, -3, -4],
+            [1, 2, 3, 4],
+            [1, 2, 0, 0],
+        ])
+        D2 = D1.transpose()
+
+        for D in [D1, D2]:
+            mat = csr_matrix(D)
+
+            assert_equal(mat.argmax(), np.argmax(D))
+            assert_equal(mat.argmin(), np.argmin(D))
+
+            assert_equal(mat.argmax(axis=0),
+                         np.asmatrix(np.argmax(D, axis=0)))
+            assert_equal(mat.argmin(axis=0),
+                         np.asmatrix(np.argmin(D, axis=0)))
+
+            assert_equal(mat.argmax(axis=1),
+                         np.asmatrix(np.argmax(D, axis=1).reshape(-1, 1)))
+            assert_equal(mat.argmin(axis=1),
+                         np.asmatrix(np.argmin(D, axis=1).reshape(-1, 1)))
+
+        D1 = np.empty((0, 5))
+        D2 = np.empty((5, 0))
+
+        for axis in [None, 0]:
+            mat = self.spmatrix(D1)
+            assert_raises(ValueError, mat.argmax, axis=axis)
+            assert_raises(ValueError, mat.argmin, axis=axis)
+
+        for axis in [None, 1]:
+            mat = self.spmatrix(D2)
+            assert_raises(ValueError, mat.argmax, axis=axis)
+            assert_raises(ValueError, mat.argmin, axis=axis)
+
 
 class _TestGetNnzAxis(object):
     def test_getnnz_axis(self):