Skip to content

Commit 8f1c56d

Browse files
DEPR: argmin/argmax with all-NA or skipa=False (pandas-dev#54170)
* 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]>
1 parent beb4dc4 commit 8f1c56d

File tree

6 files changed

+105
-23
lines changed

6 files changed

+105
-23
lines changed

doc/source/whatsnew/v2.1.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ Deprecations
370370
- Deprecated the ``method`` and ``limit`` keywords in :meth:`DataFrame.replace` and :meth:`Series.replace` (:issue:`33302`)
371371
- Deprecated the use of non-supported datetime64 and timedelta64 resolutions with :func:`pandas.array`. Supported resolutions are: "s", "ms", "us", "ns" resolutions (:issue:`53058`)
372372
- Deprecated values "pad", "ffill", "bfill", "backfill" for :meth:`Series.interpolate` and :meth:`DataFrame.interpolate`, use ``obj.ffill()`` or ``obj.bfill()`` instead (:issue:`53581`)
373+
- 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`)
373374
-
374375

375376
.. ---------------------------------------------------------------------------

pandas/core/base.py

+36-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
final,
1515
overload,
1616
)
17+
import warnings
1718

1819
import numpy as np
1920

@@ -36,6 +37,7 @@
3637
cache_readonly,
3738
doc,
3839
)
40+
from pandas.util._exceptions import find_stack_level
3941

4042
from pandas.core.dtypes.cast import can_hold_element
4143
from pandas.core.dtypes.common import (
@@ -735,15 +737,29 @@ def argmax(
735737

736738
if isinstance(delegate, ExtensionArray):
737739
if not skipna and delegate.isna().any():
740+
warnings.warn(
741+
f"The behavior of {type(self).__name__}.argmax/argmin "
742+
"with skipna=False and NAs, or with all-NAs is deprecated. "
743+
"In a future version this will raise ValueError.",
744+
FutureWarning,
745+
stacklevel=find_stack_level(),
746+
)
738747
return -1
739748
else:
740749
return delegate.argmax()
741750
else:
751+
result = nanops.nanargmax(delegate, skipna=skipna)
752+
if result == -1:
753+
warnings.warn(
754+
f"The behavior of {type(self).__name__}.argmax/argmin "
755+
"with skipna=False and NAs, or with all-NAs is deprecated. "
756+
"In a future version this will raise ValueError.",
757+
FutureWarning,
758+
stacklevel=find_stack_level(),
759+
)
742760
# error: Incompatible return value type (got "Union[int, ndarray]", expected
743761
# "int")
744-
return nanops.nanargmax( # type: ignore[return-value]
745-
delegate, skipna=skipna
746-
)
762+
return result # type: ignore[return-value]
747763

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

756772
if isinstance(delegate, ExtensionArray):
757773
if not skipna and delegate.isna().any():
774+
warnings.warn(
775+
f"The behavior of {type(self).__name__}.argmax/argmin "
776+
"with skipna=False and NAs, or with all-NAs is deprecated. "
777+
"In a future version this will raise ValueError.",
778+
FutureWarning,
779+
stacklevel=find_stack_level(),
780+
)
758781
return -1
759782
else:
760783
return delegate.argmin()
761784
else:
785+
result = nanops.nanargmin(delegate, skipna=skipna)
786+
if result == -1:
787+
warnings.warn(
788+
f"The behavior of {type(self).__name__}.argmax/argmin "
789+
"with skipna=False and NAs, or with all-NAs is deprecated. "
790+
"In a future version this will raise ValueError.",
791+
FutureWarning,
792+
stacklevel=find_stack_level(),
793+
)
762794
# error: Incompatible return value type (got "Union[int, ndarray]", expected
763795
# "int")
764-
return nanops.nanargmin( # type: ignore[return-value]
765-
delegate, skipna=skipna
766-
)
796+
return result # type: ignore[return-value]
767797

768798
def tolist(self):
769799
"""

pandas/core/indexes/base.py

+14
Original file line numberDiff line numberDiff line change
@@ -7271,6 +7271,13 @@ def argmin(self, axis=None, skipna: bool = True, *args, **kwargs) -> int:
72717271
# Take advantage of cache
72727272
mask = self._isnan
72737273
if not skipna or mask.all():
7274+
warnings.warn(
7275+
f"The behavior of {type(self).__name__}.argmax/argmin "
7276+
"with skipna=False and NAs, or with all-NAs is deprecated. "
7277+
"In a future version this will raise ValueError.",
7278+
FutureWarning,
7279+
stacklevel=find_stack_level(),
7280+
)
72747281
return -1
72757282
return super().argmin(skipna=skipna)
72767283

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

pandas/core/series.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -2530,7 +2530,12 @@ def idxmin(self, axis: Axis = 0, skipna: bool = True, *args, **kwargs) -> Hashab
25302530
nan
25312531
"""
25322532
axis = self._get_axis_number(axis)
2533-
i = self.argmin(axis, skipna, *args, **kwargs)
2533+
with warnings.catch_warnings():
2534+
# TODO(3.0): this catching/filtering can be removed
2535+
# ignore warning produced by argmin since we will issue a different
2536+
# warning for idxmin
2537+
warnings.simplefilter("ignore")
2538+
i = self.argmin(axis, skipna, *args, **kwargs)
25342539
if i == -1:
25352540
# GH#43587 give correct NA value for Index.
25362541
return self.index._na_value
@@ -2601,7 +2606,12 @@ def idxmax(self, axis: Axis = 0, skipna: bool = True, *args, **kwargs) -> Hashab
26012606
nan
26022607
"""
26032608
axis = self._get_axis_number(axis)
2604-
i = self.argmax(axis, skipna, *args, **kwargs)
2609+
with warnings.catch_warnings():
2610+
# TODO(3.0): this catching/filtering can be removed
2611+
# ignore warning produced by argmax since we will issue a different
2612+
# warning for argmax
2613+
warnings.simplefilter("ignore")
2614+
i = self.argmax(axis, skipna, *args, **kwargs)
26052615
if i == -1:
26062616
# GH#43587 give correct NA value for Index.
26072617
return self.index._na_value

pandas/tests/extension/base/methods.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,13 @@ def test_argreduce_series(
160160
self, data_missing_for_sorting, op_name, skipna, expected
161161
):
162162
# data_missing_for_sorting -> [B, NA, A] with A < B and NA missing.
163+
warn = None
164+
if op_name.startswith("arg") and expected == -1:
165+
warn = FutureWarning
166+
msg = "The behavior of Series.argmax/argmin"
163167
ser = pd.Series(data_missing_for_sorting)
164-
result = getattr(ser, op_name)(skipna=skipna)
168+
with tm.assert_produces_warning(warn, match=msg):
169+
result = getattr(ser, op_name)(skipna=skipna)
165170
tm.assert_almost_equal(result, expected)
166171

167172
def test_argmax_argmin_no_skipna_notimplemented(self, data_missing_for_sorting):

pandas/tests/reductions/test_reductions.py

+36-14
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,17 @@ def test_nanargminmax(self, opname, index_or_series):
113113
# GH#7261
114114
klass = index_or_series
115115
arg_op = "arg" + opname if klass is Index else "idx" + opname
116+
warn = FutureWarning if klass is Index else None
116117

117118
obj = klass([NaT, datetime(2011, 11, 1)])
118119
assert getattr(obj, arg_op)() == 1
119-
result = getattr(obj, arg_op)(skipna=False)
120+
121+
msg = (
122+
"The behavior of (DatetimeIndex|Series).argmax/argmin with "
123+
"skipna=False and NAs"
124+
)
125+
with tm.assert_produces_warning(warn, match=msg):
126+
result = getattr(obj, arg_op)(skipna=False)
120127
if klass is Series:
121128
assert np.isnan(result)
122129
else:
@@ -125,7 +132,8 @@ def test_nanargminmax(self, opname, index_or_series):
125132
obj = klass([NaT, datetime(2011, 11, 1), NaT])
126133
# check DatetimeIndex non-monotonic path
127134
assert getattr(obj, arg_op)() == 1
128-
result = getattr(obj, arg_op)(skipna=False)
135+
with tm.assert_produces_warning(warn, match=msg):
136+
result = getattr(obj, arg_op)(skipna=False)
129137
if klass is Series:
130138
assert np.isnan(result)
131139
else:
@@ -155,26 +163,40 @@ def test_argminmax(self):
155163
obj = Index([np.nan, 1, np.nan, 2])
156164
assert obj.argmin() == 1
157165
assert obj.argmax() == 3
158-
assert obj.argmin(skipna=False) == -1
159-
assert obj.argmax(skipna=False) == -1
166+
msg = "The behavior of Index.argmax/argmin with skipna=False and NAs"
167+
with tm.assert_produces_warning(FutureWarning, match=msg):
168+
assert obj.argmin(skipna=False) == -1
169+
with tm.assert_produces_warning(FutureWarning, match=msg):
170+
assert obj.argmax(skipna=False) == -1
160171

161172
obj = Index([np.nan])
162-
assert obj.argmin() == -1
163-
assert obj.argmax() == -1
164-
assert obj.argmin(skipna=False) == -1
165-
assert obj.argmax(skipna=False) == -1
173+
with tm.assert_produces_warning(FutureWarning, match=msg):
174+
assert obj.argmin() == -1
175+
with tm.assert_produces_warning(FutureWarning, match=msg):
176+
assert obj.argmax() == -1
177+
with tm.assert_produces_warning(FutureWarning, match=msg):
178+
assert obj.argmin(skipna=False) == -1
179+
with tm.assert_produces_warning(FutureWarning, match=msg):
180+
assert obj.argmax(skipna=False) == -1
166181

182+
msg = "The behavior of DatetimeIndex.argmax/argmin with skipna=False and NAs"
167183
obj = Index([NaT, datetime(2011, 11, 1), datetime(2011, 11, 2), NaT])
168184
assert obj.argmin() == 1
169185
assert obj.argmax() == 2
170-
assert obj.argmin(skipna=False) == -1
171-
assert obj.argmax(skipna=False) == -1
186+
with tm.assert_produces_warning(FutureWarning, match=msg):
187+
assert obj.argmin(skipna=False) == -1
188+
with tm.assert_produces_warning(FutureWarning, match=msg):
189+
assert obj.argmax(skipna=False) == -1
172190

173191
obj = Index([NaT])
174-
assert obj.argmin() == -1
175-
assert obj.argmax() == -1
176-
assert obj.argmin(skipna=False) == -1
177-
assert obj.argmax(skipna=False) == -1
192+
with tm.assert_produces_warning(FutureWarning, match=msg):
193+
assert obj.argmin() == -1
194+
with tm.assert_produces_warning(FutureWarning, match=msg):
195+
assert obj.argmax() == -1
196+
with tm.assert_produces_warning(FutureWarning, match=msg):
197+
assert obj.argmin(skipna=False) == -1
198+
with tm.assert_produces_warning(FutureWarning, match=msg):
199+
assert obj.argmax(skipna=False) == -1
178200

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

0 commit comments

Comments
 (0)