Repository: numpy/numpy

Issue:
DEP: Deprecate `np.masked_array.mini`
It's not at all clear why this exists, or how `MaskedArray.mini` is different from `MaskedArray.min`

This was introduced in 80f6f5062022104f3a57de05cb119df23c2b5de4 by @stefanv, in a patch claimed to be authored by Pierre ( @pierregm? )


Discussion:
@stefanv @pv Any recollection of why this method exists? It doesn't appear to be used anywhere in numpy or scipy.
@eric-wieser I'll guess there is something special about the return value.
I guess this was an attempt to avoid the decision on what happens in the cases where: a) the array is reduced to a scalar because no axis was specified and b) the case where the entire array is masked.

E.g.:

```
n [11]: x = np.ma.MaskedArray(np.ones((3, 3)), mask=np.ones((3, 3)))

In [12]: x.min()
Out[12]: masked

In [13]: x.mini()
Out[13]: 
masked_array(data = --,
             mask = True,
       fill_value = 1e+20)
```

In the first case, returns `np.ma.core.MaskedContent`, in the second an MaskedArray scalar that is masked.

```
In [16]: x = np.ma.MaskedArray([[1,2,3], [4,5,6]], mask=[[True,False,True], [False, True, False]])

In [17]: x.min()
Out[17]: 2

In [18]: x.mini()
Out[18]: 
masked_array(data = 2,
             mask = False,
       fill_value = 999999)
```

An int64 in the first case, a MaskedArray scalar in the second.
So `.mini()` is the same as `.min(keepdims=True, axis=axis).squeeze(axis=axis)`?

Seems pretty odd that whoever added `mini` didn't also add `maxi`
Yes, and given the one-liner you've shown, I would certainly deprecate and remove it.

Resolving patch:
diff --git a/numpy/ma/core.py b/numpy/ma/core.py
--- a/numpy/ma/core.py
+++ b/numpy/ma/core.py
@@ -5621,6 +5621,14 @@ def mini(self, axis=None):
         """
         Return the array minimum along the specified axis.
 
+        .. deprecated:: 1.13.0
+           This function is identical to both:
+
+            * ``self.min(keepdims=True, axis=axis).squeeze(axis=axis)``
+            * ``np.ma.minimum.reduce(self, axis=axis)``
+
+           Typically though, ``self.min(axis=axis)`` is sufficient.
+
         Parameters
         ----------
         axis : int, optional
@@ -5650,11 +5658,22 @@ def mini(self, axis=None):
         >>> print(x.mini(axis=1))
         [0 2 4]
 
+        There is a small difference between `mini` and `min`:
+
+        >>> x[:,1].mini(axis=0)
+        masked_array(data = --,
+                     mask = True,
+               fill_value = 999999)
+        >>> x[:,1].min(axis=0)
+        masked
         """
-        if axis is None:
-            return minimum(self)
-        else:
-            return minimum.reduce(self, axis)
+
+        # 2016-04-13, 1.13.0, gh-8764
+        warnings.warn(
+            "`mini` is deprecated; use the `min` method or "
+            "`np.ma.minimum.reduce instead.",
+            DeprecationWarning, stacklevel=2)
+        return minimum.reduce(self, axis)
 
     def max(self, axis=None, out=None, fill_value=None, keepdims=np._NoValue):
         """
@@ -6348,31 +6367,52 @@ class _extrema_operation(object):
       `_minimum_operation`.
 
     """
+    def __init__(self, ufunc, compare, fill_value):
+        self.ufunc = ufunc
+        self.compare = compare
+        self.fill_value_func = fill_value
+        self.__doc__ = ufunc.__doc__
+        self.__name__ = ufunc.__name__
 
     def __call__(self, a, b=None):
         "Executes the call behavior."
         if b is None:
+            # 2016-04-13, 1.13.0
+            warnings.warn(
+                "Single-argument form of np.ma.{0} is deprecated. Use "
+                "np.ma.{0}.reduce instead.".format(self.__name__),
+                DeprecationWarning, stacklevel=2)
             return self.reduce(a)
         return where(self.compare(a, b), a, b)
 
-    def reduce(self, target, axis=None):
+    def reduce(self, target, axis=np._NoValue):
         "Reduce target along the given axis."
         target = narray(target, copy=False, subok=True)
         m = getmask(target)
-        if axis is not None:
-            kargs = {'axis': axis}
+
+        if axis is np._NoValue and target.ndim > 1:
+            # 2017-05-06, Numpy 1.13.0: warn on axis default
+            warnings.warn(
+                "In the future the default for ma.{0}.reduce will be axis=0, "
+                "not the current None, to match np.{0}.reduce. "
+                "Explicitly pass 0 or None to silence this warning.".format(
+                    self.__name__
+                ),
+                MaskedArrayFutureWarning, stacklevel=2)
+            axis = None
+
+        if axis is not np._NoValue:
+            kwargs = dict(axis=axis)
         else:
-            kargs = {}
-            target = target.ravel()
-            if not (m is nomask):
-                m = m.ravel()
+            kwargs = dict()
+
         if m is nomask:
-            t = self.ufunc.reduce(target, **kargs)
+            t = self.ufunc.reduce(target, **kwargs)
         else:
             target = target.filled(
                 self.fill_value_func(target)).view(type(target))
-            t = self.ufunc.reduce(target, **kargs)
-            m = umath.logical_and.reduce(m, **kargs)
+            t = self.ufunc.reduce(target, **kwargs)
+            m = umath.logical_and.reduce(m, **kwargs)
             if hasattr(t, '_mask'):
                 t._mask = m
             elif m:
@@ -6395,34 +6435,6 @@ def outer(self, a, b):
         result._mask = m
         return result
 
-
-class _minimum_operation(_extrema_operation):
-
-    "Object to calculate minima"
-
-    def __init__(self):
-        """minimum(a, b) or minimum(a)
-In one argument case, returns the scalar minimum.
-        """
-        self.ufunc = umath.minimum
-        self.afunc = amin
-        self.compare = less
-        self.fill_value_func = minimum_fill_value
-
-
-class _maximum_operation(_extrema_operation):
-
-    "Object to calculate maxima"
-
-    def __init__(self):
-        """maximum(a, b) or maximum(a)
-           In one argument case returns the scalar maximum.
-        """
-        self.ufunc = umath.maximum
-        self.afunc = amax
-        self.compare = greater
-        self.fill_value_func = maximum_fill_value
-
 def min(obj, axis=None, out=None, fill_value=None, keepdims=np._NoValue):
     kwargs = {} if keepdims is np._NoValue else {'keepdims': keepdims}
 
@@ -6518,9 +6530,9 @@ def __call__(self, a, *args, **params):
 diagonal = _frommethod('diagonal')
 harden_mask = _frommethod('harden_mask')
 ids = _frommethod('ids')
-maximum = _maximum_operation()
+maximum = _extrema_operation(umath.maximum, greater, maximum_fill_value)
 mean = _frommethod('mean')
-minimum = _minimum_operation()
+minimum = _extrema_operation(umath.minimum, less, minimum_fill_value)
 nonzero = _frommethod('nonzero')
 prod = _frommethod('prod')
 product = _frommethod('prod')


Test patch:
diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py
--- a/numpy/ma/tests/test_core.py
+++ b/numpy/ma/tests/test_core.py
@@ -1049,8 +1049,8 @@ def test_minmax_func(self):
         xr = np.ravel(x)
         xmr = ravel(xm)
         # following are true because of careful selection of data
-        assert_equal(max(xr), maximum(xmr))
-        assert_equal(min(xr), minimum(xmr))
+        assert_equal(max(xr), maximum.reduce(xmr))
+        assert_equal(min(xr), minimum.reduce(xmr))
 
         assert_equal(minimum([1, 2, 3], [4, 0, 9]), [1, 0, 3])
         assert_equal(maximum([1, 2, 3], [4, 0, 9]), [4, 2, 9])
@@ -1060,12 +1060,12 @@ def test_minmax_func(self):
         y[0] = masked
         assert_equal(minimum(x, y), where(less(x, y), x, y))
         assert_equal(maximum(x, y), where(greater(x, y), x, y))
-        assert_(minimum(x) == 0)
-        assert_(maximum(x) == 4)
+        assert_(minimum.reduce(x) == 0)
+        assert_(maximum.reduce(x) == 4)
 
         x = arange(4).reshape(2, 2)
         x[-1, -1] = masked
-        assert_equal(maximum(x), 2)
+        assert_equal(maximum.reduce(x, axis=None), 2)
 
     def test_minimummaximum_func(self):
         a = np.ones((2, 2))
diff --git a/numpy/ma/tests/test_deprecations.py b/numpy/ma/tests/test_deprecations.py
--- a/numpy/ma/tests/test_deprecations.py
+++ b/numpy/ma/tests/test_deprecations.py
@@ -6,35 +6,68 @@
 import numpy as np
 from numpy.testing import TestCase, run_module_suite, assert_warns
 from numpy.ma.testutils import assert_equal
-
+from numpy.ma.core import MaskedArrayFutureWarning
 
 class TestArgsort(TestCase):
-	""" gh-8701 """
-	def _test_base(self, argsort, cls):
-		arr_0d = np.array(1).view(cls)
-		argsort(arr_0d)
+    """ gh-8701 """
+    def _test_base(self, argsort, cls):
+        arr_0d = np.array(1).view(cls)
+        argsort(arr_0d)
+
+        arr_1d = np.array([1, 2, 3]).view(cls)
+        argsort(arr_1d)
+
+        # argsort has a bad default for >1d arrays
+        arr_2d = np.array([[1, 2], [3, 4]]).view(cls)
+        result = assert_warns(
+            np.ma.core.MaskedArrayFutureWarning, argsort, arr_2d)
+        assert_equal(result, argsort(arr_2d, axis=None))
+
+        # should be no warnings for explictly specifiying it
+        argsort(arr_2d, axis=None)
+        argsort(arr_2d, axis=-1)
+
+    def test_function_ndarray(self):
+        return self._test_base(np.ma.argsort, np.ndarray)
+
+    def test_function_maskedarray(self):
+        return self._test_base(np.ma.argsort, np.ma.MaskedArray)
+
+    def test_method(self):
+        return self._test_base(np.ma.MaskedArray.argsort, np.ma.MaskedArray)
+
+
+class TestMinimumMaximum(TestCase):
+    def test_minimum(self):
+        assert_warns(DeprecationWarning, np.ma.minimum, np.ma.array([1, 2]))
+
+    def test_maximum(self):
+        assert_warns(DeprecationWarning, np.ma.maximum, np.ma.array([1, 2]))
+
+    def test_axis_default(self):
+        # NumPy 1.13, 2017-05-06
 
-		arr_1d = np.array([1, 2, 3]).view(cls)
-		argsort(arr_1d)
+        data1d = np.ma.arange(6)
+        data2d = data1d.reshape(2, 3)
 
-		# argsort has a bad default for >1d arrays
-		arr_2d = np.array([[1, 2], [3, 4]]).view(cls)
-		result = assert_warns(
-			np.ma.core.MaskedArrayFutureWarning, argsort, arr_2d)
-		assert_equal(result, argsort(arr_2d, axis=None))
+        ma_min = np.ma.minimum.reduce
+        ma_max = np.ma.maximum.reduce
 
-		# should be no warnings for explictly specifiying it
-		argsort(arr_2d, axis=None)
-		argsort(arr_2d, axis=-1)
+        # check that the default axis is still None, but warns on 2d arrays
+        result = assert_warns(MaskedArrayFutureWarning, ma_max, data2d)
+        assert_equal(result, ma_max(data2d, axis=None))
 
-	def test_function_ndarray(self):
-		return self._test_base(np.ma.argsort, np.ndarray)
+        result = assert_warns(MaskedArrayFutureWarning, ma_min, data2d)
+        assert_equal(result, ma_min(data2d, axis=None))
 
-	def test_function_maskedarray(self):
-		return self._test_base(np.ma.argsort, np.ma.MaskedArray)
+        # no warnings on 1d, as both new and old defaults are equivalent
+        result = ma_min(data1d)
+        assert_equal(result, ma_min(data1d, axis=None))
+        assert_equal(result, ma_min(data1d, axis=0))
 
-	def test_method(self):
-		return self._test_base(np.ma.MaskedArray.argsort, np.ma.MaskedArray)
+        result = ma_max(data1d)
+        assert_equal(result, ma_max(data1d, axis=None))
+        assert_equal(result, ma_max(data1d, axis=0))
 
 
 if __name__ == "__main__":
diff --git a/numpy/ma/tests/test_old_ma.py b/numpy/ma/tests/test_old_ma.py
--- a/numpy/ma/tests/test_old_ma.py
+++ b/numpy/ma/tests/test_old_ma.py
@@ -182,8 +182,8 @@ def test_testMinMax(self):
         xmr = ravel(xm)
 
         # true because of careful selection of data
-        self.assertTrue(eq(max(xr), maximum(xmr)))
-        self.assertTrue(eq(min(xr), minimum(xmr)))
+        self.assertTrue(eq(max(xr), maximum.reduce(xmr)))
+        self.assertTrue(eq(min(xr), minimum.reduce(xmr)))
 
     def test_testAddSumProd(self):
         # Test add, sum, product.
@@ -444,8 +444,8 @@ def test_testMinMax2(self):
         y[0] = masked
         assert_(eq(minimum(x, y), where(less(x, y), x, y)))
         assert_(eq(maximum(x, y), where(greater(x, y), x, y)))
-        assert_(minimum(x) == 0)
-        assert_(maximum(x) == 4)
+        assert_(minimum.reduce(x) == 0)
+        assert_(maximum.reduce(x) == 4)
 
     def test_testTakeTransposeInnerOuter(self):
         # Test of take, transpose, inner, outer products