Repository: numpy/numpy

Issue:
min/max of empty arrays 
A similar issue was raised in #2670, however the edge case of (0, ) shape is still not covered. I can go around it with checking for non-zero length in an `if` statement, but it would be more practical if `np.min` could handle this, too.

```
In [24]: [np.min(param[x][1]) for x in fields]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-24-076aa74e3684> in <module>()
----> 1 [np.min(param[x][1]) for x in fields]

/data/wts/pipeline64/python/lib/python2.6/site-packages/numpy/core/fromnumeric.pyc in amin(a, axis, out)
   1893     except AttributeError:
   1894         return _wrapit(a, 'min', axis, out)
-> 1895     return amin(axis, out)
   1896 
   1897 

ValueError: zero-size array to minimum.reduce without identity

In [25]: param[x]
Out[25]: 
{1: array([], dtype=float64),
 2: array([], dtype=float64),
 3: array([], dtype=float64),
 4: array([], dtype=float64)}

In [26]: param[x][1].shape
Out[26]: (0,)
```



Discussion:
gh-2670 is a very different thing. There you calculate the min/max over 2 elements, you just happen to do it 0 times. Here you want to calculate the min/max over 0 elements and that is not well defined.

What is `min([])`? We can define `sum([])` as `0` easily. But `-Inf`/`+Inf` for min/max does not seem elegant/clear to be correct. So we raise an error instead at the moment.

I will close this for now, but please feel free to ask/post more and convince me otherwise :).

OK, I see your point, however can't see why it behaves differently for different empty arrays (e.g. getting the minimum for `axis=1` and `axis=0` in #2670's example). Both are empty, still the minimum will be either an empty array or a `ValueError`.  
To be honest, I would be most happy with a `nan` as a result to `min([])`, at least it seems more reasonable than an error.

```
In [12]: a = numpy.zeros((0,2))

In [13]: a
Out[13]: array([], shape=(0, 2), dtype=float64)

In [14]: np.min(a, axis=1)
Out[14]: array([], dtype=float64)

In [15]: np.min(a, axis=0)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-15-c8666bcd2a35> in <module>()
----> 1 np.min(a, axis=0)

/data/wts/pipeline64/python/lib/python2.6/site-packages/numpy/core/fromnumeric.pyc in amin(a, axis, out)
   1893     except AttributeError:
   1894         return _wrapit(a, 'min', axis, out)
-> 1895     return amin(axis, out)
   1896 
   1897 

ValueError: zero-size array to minimum.reduce without identity
```

As noted above, the difference is that axis=1 computes minimum over two elements, whereas axis=0 computes it over zero elements. Only the latter has to invent numbers with undefined values as the results.

In principle, `inf` is the identity for `minimum` and `-inf` the identity for `maximum`, but `min([]) == inf` may be surprising.

Maybe to make the difference more clear, compare it with `sum`:

```
In [2]: np.ones((2, 0)).sum(0)
Out[2]: array([], dtype=float64)

In [3]: np.ones((2, 0)).sum(1)
Out[3]: array([ 0.,  0.])
```

In one case, the result is empty itself, in the other case it is not. What happens if you got `np.ones((0, 0)).min(0)` is philosophical.

Certainly not raising an error but returning something like NaN or Inf might be a reasonable choice, but I think it would be a rather largish change and changing it would have to follow a discussion the mailing list.
Though to be clear, personally I prefer raising the error here, inconvenient for some, but not as surprising for others, and I doubt there will be much will to change it.

-10 on returning Nan (that's just wrong!), -1 on Inf (what do you do
for dtypes that don't have inf?)

and anyway all of the reduction operations behave like this, e.g.:
```
In [2]: np.divide.reduce([])
ValueError: zero-size array to reduction operation divide which has no identity
```
It seems to me that only thing you could really return is an empty array.
But that ship sailed long ago. (And doesn't really seem useful).
I believe it'd be perfectly fine to provide an option to manually specify meaningful identity in a min/max/reduce call.
Like `np.zeros((0,0)).min(identity=np.inf)` yields infty,
while `np.zeros((0,0)).min()` can still throw a `ValueError` or whatever.
@RerumNovarum : Before anything like that can be implemented, #8955 needs to be merged
Repeating from #8955:

I would suggest adding identities to `np.minimum` and `np.maximum`, which would be `+inf` and `-inf` respectively. This is intuitively and logically true, as `max(a, -inf) = a` and `min(a, +inf) = a`.

I agree this probably doesn't match Python conventions but does make sense mathematically. Not to mention, `np.nanmin(all-nan)` and `np.min([])` should be `np.inf` (Mathematically speaking).

I agree it depends on how you define it, but this is probably the most widely accepted view.
Your argument for signed `inf` is valid (indeed, it is probably the best choice other than erroring), but there are valid arguments against it too:

* `min(np.array(data, int16)).dtype == np.int16` must be true even when `len(data) == 0` - so returning inf is not possible for integer types. We could return `np.iinfo(int16).min`, but that would probably be surprising.
* `max(abs(complex_vals))` has a natural identity of `0`, not `-infinity`, but there's no way for `max` to distinguish that case. Elsewhere a `max_abs` ufunc has been proposed to solve that problem.

It's worth noting that core python has decided that `max([])` should error, and that `max([], default=-inf)` is the way to spell the behavior you want. Perhaps we should aim for something similar.
> min(np.array(data, int16)).dtype must return an int16 even when len(data) == 0 - so returning inf is not possible

I guess you could cast and then return whatever is the maximum of `int16`, which would be valid too (as the universal set is the set of all `int16`). I guess it comes down to whether we want the maximum or the supremum (the only difference for finite sets being that the supremum would be what I say and and the maximum not well defined for empty sets).

> max(abs(complex_vals)) has a natural identity of 0, not -infinity, but there's no way for max to distinguish that case. Elsewhere a max_abs ufunc has been proposed to solve that problem.

I would still argue that the identity value should be `-inf` in this case, and we should aim for `np.max(np.abs(complex_vals), identity=0)`.

Edit: Or maybe `np.maximum(np.max(np.abs(complex_vals)), 0)` would also work. I'd argue against adding `identity=` everywhere because it requires core changes.
> I would still argue that the identity value should be -inf in this case

I think this would be too surprising to be useful. Summarizing the problem:

* Let X be some set of values
* Let Y be N samples from X
* Expectation: min(Y) is in X

The only way this can hold when N == 0, and still satisfy the property of being an identity, is if `min([]) == min(X)` &mdash; but `min` has no way of know what `min(X)` is. When X is ℝ, then the answer is `-inf`. But in general, numpy shouldn't assume the invariants of the set of values the user is passing, even if the assumption is often correct.

To quote the zen of python

> In the face of ambiguity, refuse the temptation to guess.

So I'm also -1 on a default for empty arrays.

I'm +1 on allowing `np.max(np.abs(complex_vals), identity=0)`. I though that was blocked by gh-8955, but actually I think it was only blocked by gh-8952 (now merged), so a patch would be welcome!

That patch would look something like adding an `identity` argument to the C `PyUFunc_Reduce` function, and using it in place of `_get_identity(ufunc)` if provided.

I'll look into it. I'm not too deep into the annals of core Numpy, if you could point me towards relevant files that'd be a big help. As long as they're Cython/Pure C++ I should be able to do it, but if they're Python API C++... I'm not entirely sure.

Edit: I'm talking about implementing `identity=`
The function I mention is defined here

https://github.com/numpy/numpy/blob/e6956dde2299fb9980eb00437a1cd3af97da5958/numpy/core/src/umath/ufunc_object.c#L2833-L2898

And called here:

https://github.com/numpy/numpy/blob/e6956dde2299fb9980eb00437a1cd3af97da5958/numpy/core/src/umath/ufunc_object.c#L3632-L3914

You'd need to:
* Parse a `identity` kwarg for `reduce` in one of the `PyArg_ParseTupleAndKeywords` calls, and call `_get_identity` if the argument is missing.
* Pass it to `PyUFunc_Reduce`, rather than having PyUFunc_Reduce compute it internally
* Add tests and documentation
Why leave it at `reduce` when `reduceat`, `accumulate` and `accumulateat`, and NaN-aggregations would also need it? This is rather big, but I'm willing to take it on. 😄 
* `reduceat` doesn't even use it's identity - don't open that can of worms. There's another issue about that elsewhere
* `accumulate` perhaps doesn't need it? `np.minimum.accumulate([])` is already well-defined
* `accumulateat` doesn't exist
Can you reopen the issue?

Resolving patch:
diff --git a/numpy/add_newdocs.py b/numpy/add_newdocs.py
--- a/numpy/add_newdocs.py
+++ b/numpy/add_newdocs.py
@@ -5850,7 +5850,7 @@ def luf(lamdaexpr, *args, **kwargs):
 
 add_newdoc('numpy.core', 'ufunc', ('reduce',
     """
-    reduce(a, axis=0, dtype=None, out=None, keepdims=False)
+    reduce(a, axis=0, dtype=None, out=None, keepdims=False, initial)
 
     Reduces `a`'s dimension by one, by applying ufunc along one axis.
 
@@ -5906,6 +5906,14 @@ def luf(lamdaexpr, *args, **kwargs):
         the result will broadcast correctly against the original `arr`.
 
         .. versionadded:: 1.7.0
+    initial : scalar, optional
+        The value with which to start the reduction.
+        If the ufunc has no identity or the dtype is object, this defaults
+        to None - otherwise it defaults to ufunc.identity.
+        If ``None`` is given, the first element of the reduction is used,
+        and an error is thrown if the reduction is empty.
+        
+        .. versionadded:: 1.15.0
 
     Returns
     -------
@@ -5937,7 +5945,24 @@ def luf(lamdaexpr, *args, **kwargs):
     >>> np.add.reduce(X, 2)
     array([[ 1,  5],
            [ 9, 13]])
-
+           
+    You can use the ``initial`` keyword argument to initialize the reduction with a
+    different value.
+    
+    >>> np.add.reduce([10], initial=5)
+    15
+    >>> np.add.reduce(np.ones((2, 2, 2)), axis=(0, 2), initializer=10)
+    array([14., 14.])
+    
+    Allows reductions of empty arrays where they would normally fail, i.e.
+    for ufuncs without an identity.
+    
+    >>> np.minimum.reduce([], initial=np.inf)
+    inf
+    >>> np.minimum.reduce([])
+    Traceback (most recent call last):
+        ...
+    ValueError: zero-size array to reduction operation minimum which has no identity
     """))
 
 add_newdoc('numpy.core', 'ufunc', ('accumulate',
diff --git a/numpy/core/_methods.py b/numpy/core/_methods.py
--- a/numpy/core/_methods.py
+++ b/numpy/core/_methods.py
@@ -11,6 +11,7 @@
 from numpy.core import umath as um
 from numpy.core.numeric import asanyarray
 from numpy.core import numerictypes as nt
+from numpy._globals import _NoValue
 
 # save those O(100) nanoseconds!
 umr_maximum = um.maximum.reduce
@@ -22,17 +23,21 @@
 
 # avoid keyword arguments to speed up parsing, saves about 15%-20% for very
 # small reductions
-def _amax(a, axis=None, out=None, keepdims=False):
-    return umr_maximum(a, axis, None, out, keepdims)
+def _amax(a, axis=None, out=None, keepdims=False,
+          initial=_NoValue):
+    return umr_maximum(a, axis, None, out, keepdims, initial)
 
-def _amin(a, axis=None, out=None, keepdims=False):
-    return umr_minimum(a, axis, None, out, keepdims)
+def _amin(a, axis=None, out=None, keepdims=False,
+          initial=_NoValue):
+    return umr_minimum(a, axis, None, out, keepdims, initial)
 
-def _sum(a, axis=None, dtype=None, out=None, keepdims=False):
-    return umr_sum(a, axis, dtype, out, keepdims)
+def _sum(a, axis=None, dtype=None, out=None, keepdims=False,
+         initial=_NoValue):
+    return umr_sum(a, axis, dtype, out, keepdims, initial)
 
-def _prod(a, axis=None, dtype=None, out=None, keepdims=False):
-    return umr_prod(a, axis, dtype, out, keepdims)
+def _prod(a, axis=None, dtype=None, out=None, keepdims=False,
+          initial=_NoValue):
+    return umr_prod(a, axis, dtype, out, keepdims, initial)
 
 def _any(a, axis=None, dtype=None, out=None, keepdims=False):
     return umr_any(a, axis, dtype, out, keepdims)
diff --git a/numpy/core/fromnumeric.py b/numpy/core/fromnumeric.py
--- a/numpy/core/fromnumeric.py
+++ b/numpy/core/fromnumeric.py
@@ -1812,7 +1812,7 @@ def clip(a, a_min, a_max, out=None):
     return _wrapfunc(a, 'clip', a_min, a_max, out=out)
 
 
-def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
+def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue, initial=np._NoValue):
     """
     Sum of array elements over a given axis.
 
@@ -1851,6 +1851,10 @@ def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
         `ndarray`, however any non-default value will be.  If the
         sub-class' method does not implement `keepdims` any
         exceptions will be raised.
+    initial : scalar, optional
+        Starting value for the sum. See `~numpy.ufunc.reduce` for details.
+
+        .. versionadded:: 1.15.0
 
     Returns
     -------
@@ -1898,6 +1902,10 @@ def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
     >>> np.ones(128, dtype=np.int8).sum(dtype=np.int8)
     -128
 
+    You can also start the sum with a value other than zero:
+
+    >>> np.sum([10], initial=5)
+    15
     """
     if isinstance(a, _gentype):
         # 2018-02-25, 1.15.0
@@ -1912,7 +1920,8 @@ def sum(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
             return out
         return res
 
-    return _wrapreduction(a, np.add, 'sum', axis, dtype, out, keepdims=keepdims)
+    return _wrapreduction(a, np.add, 'sum', axis, dtype, out, keepdims=keepdims,
+                          initial=initial)
 
 
 def any(a, axis=None, out=None, keepdims=np._NoValue):
@@ -2209,7 +2218,7 @@ def ptp(a, axis=None, out=None, keepdims=np._NoValue):
     return _methods._ptp(a, axis=axis, out=out, **kwargs)
 
 
-def amax(a, axis=None, out=None, keepdims=np._NoValue):
+def amax(a, axis=None, out=None, keepdims=np._NoValue, initial=np._NoValue):
     """
     Return the maximum of an array or maximum along an axis.
 
@@ -2241,6 +2250,13 @@ def amax(a, axis=None, out=None, keepdims=np._NoValue):
         sub-class' method does not implement `keepdims` any
         exceptions will be raised.
 
+    initial : scalar, optional
+        The minimum value of an output element. Must be present to allow
+        computation on empty slice. See `~numpy.ufunc.reduce` for details.
+
+        .. versionadded:: 1.15.0
+
+
     Returns
     -------
     amax : ndarray or scalar
@@ -2293,11 +2309,26 @@ def amax(a, axis=None, out=None, keepdims=np._NoValue):
     >>> np.nanmax(b)
     4.0
 
+    You can use an initial value to compute the maximum of an empty slice, or
+    to initialize it to a different value:
+
+    >>> np.max([[-50], [10]], axis=-1, initial=0)
+    array([ 0, 10])
+
+    Notice that the initial value is used as one of the elements for which the
+    maximum is determined, unlike for the default argument Python's max
+    function, which is only used for empty iterables.
+
+    >>> np.max([5], initial=6)
+    6
+    >>> max([5], default=6)
+    5
     """
-    return _wrapreduction(a, np.maximum, 'max', axis, None, out, keepdims=keepdims)
+    return _wrapreduction(a, np.maximum, 'max', axis, None, out, keepdims=keepdims,
+                          initial=initial)
 
 
-def amin(a, axis=None, out=None, keepdims=np._NoValue):
+def amin(a, axis=None, out=None, keepdims=np._NoValue, initial=np._NoValue):
     """
     Return the minimum of an array or minimum along an axis.
 
@@ -2329,6 +2360,12 @@ def amin(a, axis=None, out=None, keepdims=np._NoValue):
         sub-class' method does not implement `keepdims` any
         exceptions will be raised.
 
+    initial : scalar, optional
+        The maximum value of an output element. Must be present to allow
+        computation on empty slice. See `~numpy.ufunc.reduce` for details.
+
+        .. versionadded:: 1.15.0
+
     Returns
     -------
     amin : ndarray or scalar
@@ -2381,8 +2418,22 @@ def amin(a, axis=None, out=None, keepdims=np._NoValue):
     >>> np.nanmin(b)
     0.0
 
+    >>> np.min([[-50], [10]], axis=-1, initial=0)
+    array([-50,   0])
+
+    Notice that the initial value is used as one of the elements for which the
+    minimum is determined, unlike for the default argument Python's max
+    function, which is only used for empty iterables.
+
+    Notice that this isn't the same as Python's ``default`` argument.
+
+    >>> np.min([6], initial=5)
+    5
+    >>> min([6], default=5)
+    6
     """
-    return _wrapreduction(a, np.minimum, 'min', axis, None, out, keepdims=keepdims)
+    return _wrapreduction(a, np.minimum, 'min', axis, None, out, keepdims=keepdims,
+                          initial=initial)
 
 
 def alen(a):
@@ -2418,7 +2469,7 @@ def alen(a):
         return len(array(a, ndmin=1))
 
 
-def prod(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
+def prod(a, axis=None, dtype=None, out=None, keepdims=np._NoValue, initial=np._NoValue):
     """
     Return the product of array elements over a given axis.
 
@@ -2458,6 +2509,10 @@ def prod(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
         `ndarray`, however any non-default value will be.  If the
         sub-class' method does not implement `keepdims` any
         exceptions will be raised.
+    initial : scalar, optional
+        The starting value for this product. See `~numpy.ufunc.reduce` for details.
+
+        .. versionadded:: 1.15.0
 
     Returns
     -------
@@ -2515,8 +2570,13 @@ def prod(a, axis=None, dtype=None, out=None, keepdims=np._NoValue):
     >>> np.prod(x).dtype == int
     True
 
+    You can also start the product with a value other than one:
+
+    >>> np.prod([1, 2], initial=5)
+    10
     """
-    return _wrapreduction(a, np.multiply, 'prod', axis, dtype, out, keepdims=keepdims)
+    return _wrapreduction(a, np.multiply, 'prod', axis, dtype, out, keepdims=keepdims,
+                          initial=initial)
 
 
 def cumprod(a, axis=None, dtype=None, out=None):


Test patch:
diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py
--- a/numpy/core/tests/test_ufunc.py
+++ b/numpy/core/tests/test_ufunc.py
@@ -494,6 +494,17 @@ def test_sum_complex(self):
             d += d
             assert_almost_equal(d, 2. + 2j)
 
+    def test_sum_initial(self):
+        # Integer, single axis
+        assert_equal(np.sum([3], initial=2), 5)
+
+        # Floating point
+        assert_almost_equal(np.sum([0.2], initial=0.1), 0.3)
+
+        # Multiple non-adjacent axes
+        assert_equal(np.sum(np.ones((2, 3, 5), dtype=np.int64), axis=(0, 2), initial=2),
+                     [12, 12, 12])
+
     def test_inner1d(self):
         a = np.arange(6).reshape((2, 3))
         assert_array_equal(umt.inner1d(a, a), np.sum(a*a, axis=-1))
@@ -844,6 +855,7 @@ def test_object_array_reduction(self):
         assert_equal(np.min(a), False)
         assert_equal(np.array([[1]], dtype=object).sum(), 1)
         assert_equal(np.array([[[1, 2]]], dtype=object).sum((0, 1)), [1, 2])
+        assert_equal(np.array([1], dtype=object).sum(initial=1), 2)
 
     def test_object_array_accumulate_inplace(self):
         # Checks that in-place accumulates work, see also gh-7402
@@ -987,7 +999,7 @@ def test_where_param_alloc(self):
         assert_equal(np.sqrt(a, where=m), [1])
 
     def check_identityless_reduction(self, a):
-        # np.minimum.reduce is a identityless reduction
+        # np.minimum.reduce is an identityless reduction
 
         # Verify that it sees the zero at various positions
         a[...] = 1
@@ -1056,6 +1068,35 @@ def test_identityless_reduction_noncontig_unaligned(self):
         a = a[1:, 1:, 1:]
         self.check_identityless_reduction(a)
 
+    def test_initial_reduction(self):
+        # np.minimum.reduce is an identityless reduction
+
+        # For cases like np.maximum(np.abs(...), initial=0)
+        # More generally, a supremum over non-negative numbers.
+        assert_equal(np.maximum.reduce([], initial=0), 0)
+
+        # For cases like reduction of an empty array over the reals.
+        assert_equal(np.minimum.reduce([], initial=np.inf), np.inf)
+        assert_equal(np.maximum.reduce([], initial=-np.inf), -np.inf)
+
+        # Random tests
+        assert_equal(np.minimum.reduce([5], initial=4), 4)
+        assert_equal(np.maximum.reduce([4], initial=5), 5)
+        assert_equal(np.maximum.reduce([5], initial=4), 5)
+        assert_equal(np.minimum.reduce([4], initial=5), 4)
+
+        # Check initial=None raises ValueError for both types of ufunc reductions
+        assert_raises(ValueError, np.minimum.reduce, [], initial=None)
+        assert_raises(ValueError, np.add.reduce, [], initial=None)
+
+        # Check that np._NoValue gives default behavior.
+        assert_equal(np.add.reduce([], initial=np._NoValue), 0)
+
+        # Check that initial kwarg behaves as intended for dtype=object
+        a = np.array([10], dtype=object)
+        res = np.add.reduce(a, initial=5)
+        assert_equal(res, 15)
+
     def test_identityless_reduction_nonreorderable(self):
         a = np.array([[8.0, 2.0, 2.0], [1.0, 0.5, 0.25]])
 
@@ -1407,15 +1448,18 @@ def test_reduce_arguments(self):
         assert_equal(f(d, 0, None, None), r)
         assert_equal(f(d, 0, None, None, keepdims=False), r)
         assert_equal(f(d, 0, None, None, True), r.reshape((1,) + r.shape))
+        assert_equal(f(d, 0, None, None, False, 0), r)
+        assert_equal(f(d, 0, None, None, False, initial=0), r)
         # multiple keywords
         assert_equal(f(d, axis=0, dtype=None, out=None, keepdims=False), r)
         assert_equal(f(d, 0, dtype=None, out=None, keepdims=False), r)
         assert_equal(f(d, 0, None, out=None, keepdims=False), r)
+        assert_equal(f(d, 0, None, out=None, keepdims=False, initial=0), r)
 
         # too little
         assert_raises(TypeError, f)
         # too much
-        assert_raises(TypeError, f, d, 0, None, None, False, 1)
+        assert_raises(TypeError, f, d, 0, None, None, False, 0, 1)
         # invalid axis
         assert_raises(TypeError, f, d, "invalid")
         assert_raises(TypeError, f, d, axis="invalid")
diff --git a/numpy/core/tests/test_umath.py b/numpy/core/tests/test_umath.py
--- a/numpy/core/tests/test_umath.py
+++ b/numpy/core/tests/test_umath.py
@@ -1759,7 +1759,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
 
         # reduce, kwargs
         res = np.multiply.reduce(a, axis='axis0', dtype='dtype0', out='out0',
-                                 keepdims='keep0')
+                                 keepdims='keep0', initial='init0')
         assert_equal(res[0], a)
         assert_equal(res[1], np.multiply)
         assert_equal(res[2], 'reduce')
@@ -1767,7 +1767,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         assert_equal(res[4], {'dtype':'dtype0',
                               'out': ('out0',),
                               'keepdims': 'keep0',
-                              'axis': 'axis0'})
+                              'axis': 'axis0',
+                              'initial': 'init0'})
 
         # reduce, output equal to None removed, but not other explicit ones,
         # even if they are at their default value.
@@ -1777,6 +1778,14 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
         assert_equal(res[4], {'axis': 0, 'keepdims': True})
         res = np.multiply.reduce(a, None, out=(None,), dtype=None)
         assert_equal(res[4], {'axis': None, 'dtype': None})
+        res = np.multiply.reduce(a, 0, None, None, False, 2)
+        assert_equal(res[4], {'axis': 0, 'dtype': None, 'keepdims': False, 'initial': 2})
+        # np._NoValue ignored for initial.
+        res = np.multiply.reduce(a, 0, None, None, False, np._NoValue)
+        assert_equal(res[4], {'axis': 0, 'dtype': None, 'keepdims': False})
+        # None kept for initial.
+        res = np.multiply.reduce(a, 0, None, None, False, None)
+        assert_equal(res[4], {'axis': 0, 'dtype': None, 'keepdims': False, 'initial': None})
 
         # reduce, wrong args
         assert_raises(ValueError, np.multiply.reduce, a, out=())