Skip to content

Commit

Permalink
DEPR: argmin/argmax with all-NA or skipa=False (pandas-dev#54170)
Browse files Browse the repository at this point in the history
* DEPR: argmax/min with all-NAs or any-NAs and skipna=False

* Warn in numpy cases too

* update test_argreduce_series

* mypy fixup

* Update doc/source/whatsnew/v2.1.0.rst

Co-authored-by: Matthew Roeschke <[email protected]>

* comment

---------

Co-authored-by: Matthew Roeschke <[email protected]>
  • Loading branch information
jbrockmendel and mroeschke authored Jul 20, 2023
1 parent beb4dc4 commit 8f1c56d
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 23 deletions.
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ Deprecations
- Deprecated the ``method`` and ``limit`` keywords in :meth:`DataFrame.replace` and :meth:`Series.replace` (:issue:`33302`)
- Deprecated the use of non-supported datetime64 and timedelta64 resolutions with :func:`pandas.array`. Supported resolutions are: "s", "ms", "us", "ns" resolutions (:issue:`53058`)
- Deprecated values "pad", "ffill", "bfill", "backfill" for :meth:`Series.interpolate` and :meth:`DataFrame.interpolate`, use ``obj.ffill()`` or ``obj.bfill()`` instead (:issue:`53581`)
- Deprecated the behavior of :meth:`Index.argmax`, :meth:`Index.argmin`, :meth:`Series.argmax`, :meth:`Series.argmin` with either all-NAs and skipna=True or any-NAs and skipna=False returning -1; in a future version this will raise ``ValueError`` (:issue:`33941`, :issue:`33942`)
-

.. ---------------------------------------------------------------------------
Expand Down
42 changes: 36 additions & 6 deletions pandas/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
final,
overload,
)
import warnings

import numpy as np

Expand All @@ -36,6 +37,7 @@
cache_readonly,
doc,
)
from pandas.util._exceptions import find_stack_level

from pandas.core.dtypes.cast import can_hold_element
from pandas.core.dtypes.common import (
Expand Down Expand Up @@ -735,15 +737,29 @@ def argmax(

if isinstance(delegate, ExtensionArray):
if not skipna and delegate.isna().any():
warnings.warn(
f"The behavior of {type(self).__name__}.argmax/argmin "
"with skipna=False and NAs, or with all-NAs is deprecated. "
"In a future version this will raise ValueError.",
FutureWarning,
stacklevel=find_stack_level(),
)
return -1
else:
return delegate.argmax()
else:
result = nanops.nanargmax(delegate, skipna=skipna)
if result == -1:
warnings.warn(
f"The behavior of {type(self).__name__}.argmax/argmin "
"with skipna=False and NAs, or with all-NAs is deprecated. "
"In a future version this will raise ValueError.",
FutureWarning,
stacklevel=find_stack_level(),
)
# error: Incompatible return value type (got "Union[int, ndarray]", expected
# "int")
return nanops.nanargmax( # type: ignore[return-value]
delegate, skipna=skipna
)
return result # type: ignore[return-value]

@doc(argmax, op="min", oppose="max", value="smallest")
def argmin(
Expand All @@ -755,15 +771,29 @@ def argmin(

if isinstance(delegate, ExtensionArray):
if not skipna and delegate.isna().any():
warnings.warn(
f"The behavior of {type(self).__name__}.argmax/argmin "
"with skipna=False and NAs, or with all-NAs is deprecated. "
"In a future version this will raise ValueError.",
FutureWarning,
stacklevel=find_stack_level(),
)
return -1
else:
return delegate.argmin()
else:
result = nanops.nanargmin(delegate, skipna=skipna)
if result == -1:
warnings.warn(
f"The behavior of {type(self).__name__}.argmax/argmin "
"with skipna=False and NAs, or with all-NAs is deprecated. "
"In a future version this will raise ValueError.",
FutureWarning,
stacklevel=find_stack_level(),
)
# error: Incompatible return value type (got "Union[int, ndarray]", expected
# "int")
return nanops.nanargmin( # type: ignore[return-value]
delegate, skipna=skipna
)
return result # type: ignore[return-value]

def tolist(self):
"""
Expand Down
14 changes: 14 additions & 0 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7271,6 +7271,13 @@ def argmin(self, axis=None, skipna: bool = True, *args, **kwargs) -> int:
# Take advantage of cache
mask = self._isnan
if not skipna or mask.all():
warnings.warn(
f"The behavior of {type(self).__name__}.argmax/argmin "
"with skipna=False and NAs, or with all-NAs is deprecated. "
"In a future version this will raise ValueError.",
FutureWarning,
stacklevel=find_stack_level(),
)
return -1
return super().argmin(skipna=skipna)

Expand All @@ -7283,6 +7290,13 @@ def argmax(self, axis=None, skipna: bool = True, *args, **kwargs) -> int:
# Take advantage of cache
mask = self._isnan
if not skipna or mask.all():
warnings.warn(
f"The behavior of {type(self).__name__}.argmax/argmin "
"with skipna=False and NAs, or with all-NAs is deprecated. "
"In a future version this will raise ValueError.",
FutureWarning,
stacklevel=find_stack_level(),
)
return -1
return super().argmax(skipna=skipna)

Expand Down
14 changes: 12 additions & 2 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2530,7 +2530,12 @@ def idxmin(self, axis: Axis = 0, skipna: bool = True, *args, **kwargs) -> Hashab
nan
"""
axis = self._get_axis_number(axis)
i = self.argmin(axis, skipna, *args, **kwargs)
with warnings.catch_warnings():
# TODO(3.0): this catching/filtering can be removed
# ignore warning produced by argmin since we will issue a different
# warning for idxmin
warnings.simplefilter("ignore")
i = self.argmin(axis, skipna, *args, **kwargs)
if i == -1:
# GH#43587 give correct NA value for Index.
return self.index._na_value
Expand Down Expand Up @@ -2601,7 +2606,12 @@ def idxmax(self, axis: Axis = 0, skipna: bool = True, *args, **kwargs) -> Hashab
nan
"""
axis = self._get_axis_number(axis)
i = self.argmax(axis, skipna, *args, **kwargs)
with warnings.catch_warnings():
# TODO(3.0): this catching/filtering can be removed
# ignore warning produced by argmax since we will issue a different
# warning for argmax
warnings.simplefilter("ignore")
i = self.argmax(axis, skipna, *args, **kwargs)
if i == -1:
# GH#43587 give correct NA value for Index.
return self.index._na_value
Expand Down
7 changes: 6 additions & 1 deletion pandas/tests/extension/base/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,13 @@ def test_argreduce_series(
self, data_missing_for_sorting, op_name, skipna, expected
):
# data_missing_for_sorting -> [B, NA, A] with A < B and NA missing.
warn = None
if op_name.startswith("arg") and expected == -1:
warn = FutureWarning
msg = "The behavior of Series.argmax/argmin"
ser = pd.Series(data_missing_for_sorting)
result = getattr(ser, op_name)(skipna=skipna)
with tm.assert_produces_warning(warn, match=msg):
result = getattr(ser, op_name)(skipna=skipna)
tm.assert_almost_equal(result, expected)

def test_argmax_argmin_no_skipna_notimplemented(self, data_missing_for_sorting):
Expand Down
50 changes: 36 additions & 14 deletions pandas/tests/reductions/test_reductions.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,17 @@ def test_nanargminmax(self, opname, index_or_series):
# GH#7261
klass = index_or_series
arg_op = "arg" + opname if klass is Index else "idx" + opname
warn = FutureWarning if klass is Index else None

obj = klass([NaT, datetime(2011, 11, 1)])
assert getattr(obj, arg_op)() == 1
result = getattr(obj, arg_op)(skipna=False)

msg = (
"The behavior of (DatetimeIndex|Series).argmax/argmin with "
"skipna=False and NAs"
)
with tm.assert_produces_warning(warn, match=msg):
result = getattr(obj, arg_op)(skipna=False)
if klass is Series:
assert np.isnan(result)
else:
Expand All @@ -125,7 +132,8 @@ def test_nanargminmax(self, opname, index_or_series):
obj = klass([NaT, datetime(2011, 11, 1), NaT])
# check DatetimeIndex non-monotonic path
assert getattr(obj, arg_op)() == 1
result = getattr(obj, arg_op)(skipna=False)
with tm.assert_produces_warning(warn, match=msg):
result = getattr(obj, arg_op)(skipna=False)
if klass is Series:
assert np.isnan(result)
else:
Expand Down Expand Up @@ -155,26 +163,40 @@ def test_argminmax(self):
obj = Index([np.nan, 1, np.nan, 2])
assert obj.argmin() == 1
assert obj.argmax() == 3
assert obj.argmin(skipna=False) == -1
assert obj.argmax(skipna=False) == -1
msg = "The behavior of Index.argmax/argmin with skipna=False and NAs"
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmin(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmax(skipna=False) == -1

obj = Index([np.nan])
assert obj.argmin() == -1
assert obj.argmax() == -1
assert obj.argmin(skipna=False) == -1
assert obj.argmax(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmin() == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmax() == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmin(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmax(skipna=False) == -1

msg = "The behavior of DatetimeIndex.argmax/argmin with skipna=False and NAs"
obj = Index([NaT, datetime(2011, 11, 1), datetime(2011, 11, 2), NaT])
assert obj.argmin() == 1
assert obj.argmax() == 2
assert obj.argmin(skipna=False) == -1
assert obj.argmax(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmin(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmax(skipna=False) == -1

obj = Index([NaT])
assert obj.argmin() == -1
assert obj.argmax() == -1
assert obj.argmin(skipna=False) == -1
assert obj.argmax(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmin() == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmax() == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmin(skipna=False) == -1
with tm.assert_produces_warning(FutureWarning, match=msg):
assert obj.argmax(skipna=False) == -1

@pytest.mark.parametrize("op, expected_col", [["max", "a"], ["min", "b"]])
def test_same_tz_min_max_axis_1(self, op, expected_col):
Expand Down

0 comments on commit 8f1c56d

Please sign in to comment.