diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 607a14c696578..eb9e31be0772e 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -302,8 +302,9 @@ Sparse ExtensionArray ^^^^^^^^^^^^^^ + - Bug in :meth:`DataFrame.where` when ``other`` is a :class:`Series` with ExtensionArray dtype (:issue:`38729`) -- +- Fixed bug where :meth:`Series.idxmax`, :meth:`Series.idxmin` and ``argmax/min`` fail when the underlying data is :class:`ExtensionArray` (:issue:`32749`, :issue:`33719`, :issue:`36566`) - Other diff --git a/pandas/core/base.py b/pandas/core/base.py index 54dec90c08aa2..afc22a8446dce 100644 --- a/pandas/core/base.py +++ b/pandas/core/base.py @@ -715,9 +715,17 @@ def argmax(self, axis=None, skipna: bool = True, *args, **kwargs) -> int: the minimum cereal calories is the first element, since series is zero-indexed. """ + delegate = self._values nv.validate_minmax_axis(axis) - nv.validate_argmax_with_skipna(skipna, args, kwargs) - return nanops.nanargmax(self._values, skipna=skipna) + skipna = nv.validate_argmax_with_skipna(skipna, args, kwargs) + + if isinstance(delegate, ExtensionArray): + if not skipna and delegate.isna().any(): + return -1 + else: + return delegate.argmax() + else: + return nanops.nanargmax(delegate, skipna=skipna) def min(self, axis=None, skipna: bool = True, *args, **kwargs): """ @@ -765,9 +773,17 @@ def min(self, axis=None, skipna: bool = True, *args, **kwargs): @doc(argmax, op="min", oppose="max", value="smallest") def argmin(self, axis=None, skipna=True, *args, **kwargs) -> int: + delegate = self._values nv.validate_minmax_axis(axis) - nv.validate_argmax_with_skipna(skipna, args, kwargs) - return nanops.nanargmin(self._values, skipna=skipna) + skipna = nv.validate_argmin_with_skipna(skipna, args, kwargs) + + if isinstance(delegate, ExtensionArray): + if not skipna and delegate.isna().any(): + return -1 + else: + return delegate.argmin() + else: + return nanops.nanargmin(delegate, skipna=skipna) def tolist(self): """ diff --git a/pandas/core/series.py b/pandas/core/series.py index cb753e887b0f8..7f2039c998f53 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2076,8 +2076,7 @@ def idxmin(self, axis=0, skipna=True, *args, **kwargs): >>> s.idxmin(skipna=False) nan """ - skipna = nv.validate_argmin_with_skipna(skipna, args, kwargs) - i = nanops.nanargmin(self._values, skipna=skipna) + i = self.argmin(None, skipna=skipna) if i == -1: return np.nan return self.index[i] @@ -2147,8 +2146,7 @@ def idxmax(self, axis=0, skipna=True, *args, **kwargs): >>> s.idxmax(skipna=False) nan """ - skipna = nv.validate_argmax_with_skipna(skipna, args, kwargs) - i = nanops.nanargmax(self._values, skipna=skipna) + i = self.argmax(None, skipna=skipna) if i == -1: return np.nan return self.index[i] diff --git a/pandas/tests/extension/base/methods.py b/pandas/tests/extension/base/methods.py index 1cc03d4f4f2bd..472e783c977f0 100644 --- a/pandas/tests/extension/base/methods.py +++ b/pandas/tests/extension/base/methods.py @@ -107,6 +107,27 @@ def test_argmin_argmax_all_na(self, method, data, na_value): with pytest.raises(ValueError, match=err_msg): getattr(data_na, method)() + @pytest.mark.parametrize( + "op_name, skipna, expected", + [ + ("idxmax", True, 0), + ("idxmin", True, 2), + ("argmax", True, 0), + ("argmin", True, 2), + ("idxmax", False, np.nan), + ("idxmin", False, np.nan), + ("argmax", False, -1), + ("argmin", False, -1), + ], + ) + 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. + ser = pd.Series(data_missing_for_sorting) + result = getattr(ser, op_name)(skipna=skipna) + tm.assert_almost_equal(result, expected) + @pytest.mark.parametrize( "na_position, expected", [