Thanks to visit codestin.com
Credit goes to github.com

Skip to content

BUG: fix ma.diff not preserving mask when using append/prepend #22776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 18, 2023
6 changes: 6 additions & 0 deletions doc/release/upcoming_changes/22776.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix ``np.ma.diff`` not preserving the mask when called with arguments prepend/append.
-------------------------------------------------------------------------------------
Calling ``np.ma.diff`` with arguments prepend and/or append now returns a
``MaskedArray`` with the input mask preserved.

Previously, a ``MaskedArray`` without the mask was returned.
141 changes: 135 additions & 6 deletions numpy/ma/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7341,6 +7341,141 @@ def size(obj, axis=None):
size.__doc__ = np.size.__doc__


def diff(a, /, n=1, axis=-1, prepend=np._NoValue, append=np._NoValue):
"""
Calculate the n-th discrete difference along the given axis.
The first difference is given by ``out[i] = a[i+1] - a[i]`` along
the given axis, higher differences are calculated by using `diff`
recursively.
Preserves the input mask.

Parameters
----------
a : array_like
Input array
n : int, optional
The number of times values are differenced. If zero, the input
is returned as-is.
axis : int, optional
The axis along which the difference is taken, default is the
last axis.
prepend, append : array_like, optional
Values to prepend or append to `a` along axis prior to
performing the difference. Scalar values are expanded to
arrays with length 1 in the direction of axis and the shape
of the input array in along all other axes. Otherwise the
dimension and shape must match `a` except along axis.

Returns
-------
diff : MaskedArray
The n-th differences. The shape of the output is the same as `a`
except along `axis` where the dimension is smaller by `n`. The
type of the output is the same as the type of the difference
between any two elements of `a`. This is the same as the type of
`a` in most cases. A notable exception is `datetime64`, which
results in a `timedelta64` output array.

See Also
--------
numpy.diff : Equivalent function in the top-level NumPy module.

Notes
-----
Type is preserved for boolean arrays, so the result will contain
`False` when consecutive elements are the same and `True` when they
differ.

For unsigned integer arrays, the results will also be unsigned. This
should not be surprising, as the result is consistent with
calculating the difference directly:

>>> u8_arr = np.array([1, 0], dtype=np.uint8)
>>> np.ma.diff(u8_arr)
masked_array(data=[255],
mask=False,
fill_value=999999,
dtype=uint8)
>>> u8_arr[1,...] - u8_arr[0,...]
255

If this is not desirable, then the array should be cast to a larger
integer type first:

>>> i16_arr = u8_arr.astype(np.int16)
>>> np.ma.diff(i16_arr)
masked_array(data=[-1],
mask=False,
fill_value=999999,
dtype=int16)

Examples
--------
>>> a = np.array([1, 2, 3, 4, 7, 0, 2, 3])
>>> x = np.ma.masked_where(a < 2, a)
>>> np.ma.diff(x)
masked_array(data=[--, 1, 1, 3, --, --, 1],
mask=[ True, False, False, False, True, True, False],
fill_value=999999)

>>> np.ma.diff(x, n=2)
masked_array(data=[--, 0, 2, --, --, --],
mask=[ True, False, False, True, True, True],
fill_value=999999)

>>> a = np.array([[1, 3, 1, 5, 10], [0, 1, 5, 6, 8]])
>>> x = np.ma.masked_equal(a, value=1)
>>> np.ma.diff(x)
masked_array(
data=[[--, --, --, 5],
[--, --, 1, 2]],
mask=[[ True, True, True, False],
[ True, True, False, False]],
fill_value=1)

>>> np.ma.diff(x, axis=0)
masked_array(data=[[--, --, --, 1, -2]],
mask=[[ True, True, True, False, False]],
fill_value=1)

"""
if n == 0:
return a
if n < 0:
raise ValueError("order must be non-negative but got " + repr(n))

a = np.ma.asanyarray(a)
if a.ndim == 0:
Copy link
Member

Choose a reason for hiding this comment

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

Sorry for not looking earlier. I am happy to do this to keep it identical to the original code (will open an issue about it, I don't think n == 0 is correct, it should check for prepend/append to be None).

Doing a.ndim == 0 may fail on e.g. list inputs when it currently didn't fail, so we probably also need that np.ma.asanyarray() call first that the old function has.

Copy link
Contributor Author

@markopacak markopacak Jan 18, 2023

Choose a reason for hiding this comment

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

Added np.ma.asanyarray to keep it identical to BASE diff.

Pls ping me when you open that issue, I'd also like to take a look.

raise ValueError(
"diff requires input that is at least one dimensional"
)

combined = []
if prepend is not np._NoValue:
prepend = np.ma.asanyarray(prepend)
if prepend.ndim == 0:
shape = list(a.shape)
shape[axis] = 1
prepend = np.broadcast_to(prepend, tuple(shape))
combined.append(prepend)

combined.append(a)

if append is not np._NoValue:
append = np.ma.asanyarray(append)
if append.ndim == 0:
shape = list(a.shape)
shape[axis] = 1
append = np.broadcast_to(append, tuple(shape))
combined.append(append)

if len(combined) > 1:
a = np.ma.concatenate(combined, axis)

# GH 22465 np.diff without prepend/append preserves the mask
return np.diff(a, n, axis)


##############################################################################
# Extra functions #
##############################################################################
Expand Down Expand Up @@ -8281,12 +8416,6 @@ def __call__(self, *args, **params):
np_ret='clipped_array : ndarray',
np_ma_ret='clipped_array : MaskedArray',
)
diff = _convert2ma(
'diff',
params=dict(fill_value=None, hardmask=False),
np_ret='diff : ndarray',
np_ma_ret='diff : MaskedArray',
)
empty = _convert2ma(
'empty',
params=dict(fill_value=None, hardmask=False),
Expand Down
2 changes: 1 addition & 1 deletion numpy/ma/core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ from numpy import (
amin as amin,
bool_ as bool_,
expand_dims as expand_dims,
diff as diff,
clip as clip,
indices as indices,
ones_like as ones_like,
Expand Down Expand Up @@ -429,6 +428,7 @@ def resize(x, new_shape): ...
def ndim(obj): ...
def shape(obj): ...
def size(obj, axis=...): ...
def diff(a, /, n=..., axis=..., prepend=..., append=...): ...
def where(condition, x=..., y=...): ...
def choose(indices, choices, out=..., mode=...): ...
def round_(a, decimals=..., out=...): ...
Expand Down
40 changes: 40 additions & 0 deletions numpy/ma/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4091,6 +4091,46 @@ def test_mean_overflow(self):
mask=np.zeros((10000, 10000)))
assert_equal(a.mean(), 65535.0)

def test_diff_with_prepend(self):
# GH 22465
x = np.array([1, 2, 2, 3, 4, 2, 1, 1])

a = np.ma.masked_equal(x[3:], value=2)
a_prep = np.ma.masked_equal(x[:3], value=2)
diff1 = np.ma.diff(a, prepend=a_prep, axis=0)

b = np.ma.masked_equal(x, value=2)
diff2 = np.ma.diff(b, axis=0)

assert_(np.ma.allequal(diff1, diff2))

def test_diff_with_append(self):
# GH 22465
x = np.array([1, 2, 2, 3, 4, 2, 1, 1])

a = np.ma.masked_equal(x[:3], value=2)
a_app = np.ma.masked_equal(x[3:], value=2)
diff1 = np.ma.diff(a, append=a_app, axis=0)

b = np.ma.masked_equal(x, value=2)
diff2 = np.ma.diff(b, axis=0)

assert_(np.ma.allequal(diff1, diff2))

def test_diff_with_dim_0(self):
with pytest.raises(
ValueError,
match="diff requires input that is at least one dimensional"
):
np.ma.diff(np.array(1))

def test_diff_with_n_0(self):
a = np.ma.masked_equal([1, 2, 2, 3, 4, 2, 1, 1], value=2)
diff = np.ma.diff(a, n=0, axis=0)

assert_(np.ma.allequal(a, diff))


class TestMaskedArrayMathMethodsComplex:
# Test class for miscellaneous MaskedArrays methods.
def setup_method(self):
Expand Down