Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions numpy/ma/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,39 @@ class MaskError(MAError):
default_filler["M8[" + v + "]"] = np.datetime64("NaT", v)
default_filler["m8[" + v + "]"] = np.timedelta64("NaT", v)


class _UniversalInfinity:
def __init__(self, neg=False):
self.neg = neg

def __le__(self, other):
return self.neg

def __lt__(self, other):
return self.neg and self != other

def __ge__(self, other):
return not self.neg

def __gt__(self, other):
return not self.neg and self != other

def __eq__(self, other):
return isinstance(other, _UniversalInfinity) and self.neg == other.neg

def __hash__(self):
return hash(self.neg)

def __neg__(self):
return _UniversalInfinity(not self.neg)

def __pos__(self):
return self

def __repr__(self):
return "-uinf" if self.neg else "uinf"


float_types_list = [np.half, np.single, np.double, np.longdouble,
np.csingle, np.cdouble, np.clongdouble]

Expand All @@ -211,6 +244,8 @@ class MaskError(MAError):
min_val, max_val = info.min, info.max
elif scalar_dtype.kind == "b":
min_val, max_val = 0, 1
elif scalar_dtype.kind == "O":
min_val, max_val = -_UniversalInfinity(), _UniversalInfinity()
else:
min_val, max_val = None, None

Expand Down Expand Up @@ -5942,8 +5977,9 @@ def min(self, axis=None, out=None, fill_value=None, keepdims=np._NoValue):
# No explicit output
if out is None:
result = self.filled(fill_value).min(
axis=axis, out=out, **kwargs).view(type(self))
if result.ndim:
axis=axis, out=out, **kwargs)
if isinstance(result, ndarray) and result.ndim:
Copy link
Author

@benburrill benburrill Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an (extreme) edge case I am aware of here that my code fails to address: ideally, I think that

x = np.array([1, 2])
assert np.fromiter([x], dtype=object).view(np.ma.MaskedArray).min() is x

should pass, but with my change, x will get erroneously converted to a masked array when returned by min.

This is identical to the old behavior, so it's not necessarily a problem, but it is a failure of my assumptions: initially when working on this PR, I thought I only needed to worry about arrays containing 0-D array objects, since arrays of higher dimension are not orderable (and so you'd just get an error when calling min(), as you should). But of course I forgot to consider that min() will succeed regardless if there is only one item. Also, as another similar edge case, higher dimensional arrays actually are orderable if they themselves only contain one item (not just if they are 0-D).

I'm not sure if this is actually worth fixing though. Thoughts?

result = result.view(type(self))
# Set the mask
result.__setmask__(newmask)
# Get rid of Infs
Expand Down Expand Up @@ -6047,8 +6083,9 @@ def max(self, axis=None, out=None, fill_value=None, keepdims=np._NoValue):
# No explicit output
if out is None:
result = self.filled(fill_value).max(
axis=axis, out=out, **kwargs).view(type(self))
if result.ndim:
axis=axis, out=out, **kwargs)
if isinstance(result, ndarray) and result.ndim:
result = result.view(type(self))
# Set the mask
result.__setmask__(newmask)
# Get rid of Infs
Expand Down
14 changes: 14 additions & 0 deletions numpy/ma/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,20 @@ def test_minmax_dtypes(self):
assert masked_array([-cmax, 0], mask=[0, 1]).max() == -cmax
assert masked_array([cmax, 0], mask=[0, 1]).min() == cmax

@pytest.mark.parametrize("mask", [nomask, [False, True, False]])
@pytest.mark.parametrize("arg1, arg2, arg3", [
(1, 2, 3),
(np.array(1), np.array(2), np.array(3)),
])
def test_minmax_object_dtype(self, arg1, arg2, arg3, mask):
a = masked_array([arg1, arg2, arg3], mask=mask, dtype=object)
assert a.min() is arg1
assert a.max() is arg3

def test_gh_9103(self):
arr = masked_array([1, 2, 3], dtype='object', mask=[True, False, False])
assert arr.min() == 2

@pytest.mark.parametrize("dtype", "bBiIqQ")
@pytest.mark.parametrize("mask", [
[False, False, False, True, True], # masked min/max
Expand Down