Skip to content

DEPR: argmin/argmax with all-NA or skipa=False #54170

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 9 commits into from
Jul 20, 2023
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 @@ -368,6 +368,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; 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
12 changes: 10 additions & 2 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -2530,7 +2530,11 @@ 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():
# 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 +2605,11 @@ 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():
# 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