Repository: pandas-dev/pandas
Pull request: REGR: Series.nlargest with masked arrays
- [x] closes #42816
- [x] tests added / passed
- [x] Ensure all linting tests pass, see [here](https://pandas.pydata.org/pandas-docs/dev/development/contributing.html#code-standards) for how to run them
- [x] whatsnew entry


Problem statement:
BUG: nlargest raises TypeError "No matching signature found" on Float64Dtype Series, versions >1.3.0
- [yes] I have checked that this issue has not already been reported.

- [yes] I have confirmed this bug exists on the latest version of pandas.

#### Code Sample, a copy-pastable example

```python
# this works:
pandas.Series(numpy.random.random(10)).nlargest(5)

# this works on pandas 1.2.5 but fails on 1.3.0 and 1.3.1.  All Float64DType Series seem to have the same issue
pandas.Series(numpy.random.random(10)).astype('Float64').nlargest(5)

```

#### Software/hardware
python version: 3.8.10
Ubuntu 20.04
(Intel Tiger Lake CPU)

#### Stack trace

```
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_213893/2592114079.py in <module>
----> 1 pandas.Series(numpy.random.random(10)).astype('Float64').nlargest(5)

~/.pyenv/versions/3.8.10/envs/myvenv/lib/python3.8/site-packages/pandas/core/series.py in nlargest(self, n, keep)
   3764         dtype: int64
   3765         """
-> 3766         return algorithms.SelectNSeries(self, n=n, keep=keep).nlargest()
   3767 
   3768     def nsmallest(self, n: int = 5, keep: str = "first") -> Series:

~/.pyenv/versions/3.8.10/envs/myvenv/lib/python3.8/site-packages/pandas/core/algorithms.py in nlargest(self)
   1217 
   1218     def nlargest(self):
-> 1219         return self.compute("nlargest")
   1220 
   1221     def nsmallest(self):

~/.pyenv/versions/3.8.10/envs/myvenv/lib/python3.8/site-packages/pandas/core/algorithms.py in compute(self, method)
   1285         # arr passed into kth_smallest must be contiguous. We copy
   1286         # here because kth_smallest will modify its input
-> 1287         kth_val = algos.kth_smallest(arr.copy(order="C"), n - 1)
   1288         (ns,) = np.nonzero(arr <= kth_val)
   1289         inds = ns[arr[ns].argsort(kind="mergesort")]

~/.pyenv/versions/3.8.10/envs/myvenv/lib/python3.8/site-packages/pandas/_libs/algos.pyx in pandas._libs.algos.__pyx_fused_cpdef()

TypeError: No matching signature found
```

Thanks!

Hints from issue discussion:
Thanks @hottwaj for the report.

> this works on pandas 1.2.5 but fails on 1.3.0 and 1.3.1.  All Float64DType Series seem to have the same issue

first bad commit: [b4375a40dee6295718f535cbd3e91b83e976035c] REF: avoid unnecessary casting in algorithms (#41256)

cc @jbrockmendel 





It's easy to make a (kludgy) shim that fixes cases without any NAs, need to give some thought to the general case
xref #42737
3 options here

1) make nsmallest/nlargest an EA method for authors to override
2) for our masked arrays, in cases where there are no NAs, we can kludge _ensure_data to work
3) make libalgos.kth_smallest support object dtype

Commit message: REGR: Series.nlargest with masked arrays

Patch to review:
diff --git a/doc/source/whatsnew/v1.3.2.rst b/doc/source/whatsnew/v1.3.2.rst
index 116bdd6e1d98f..c2f9046c96108 100644
--- a/doc/source/whatsnew/v1.3.2.rst
+++ b/doc/source/whatsnew/v1.3.2.rst
@@ -21,7 +21,7 @@ Fixed regressions
 - Regression in :meth:`DataFrame.agg` when the ``func`` argument returned lists and ``axis=1`` (:issue:`42727`)
 - Fixed regression where :meth:`pandas.read_csv` raised a ``ValueError`` when parameters ``names`` and ``prefix`` were both set to None (:issue:`42387`)
 - Fixed regression in comparisons between :class:`Timestamp` object and ``datetime64`` objects outside the implementation bounds for nanosecond ``datetime64`` (:issue:`42794`)
--
+- Regression in :meth:`Series.nlargest` and :meth:`Series.nsmallest` with nullabe integer or float dtype (:issue:`41816`)
 
 .. ---------------------------------------------------------------------------
 
diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py
index df9f3d07ce7fd..572fd36e4ce25 100644
--- a/pandas/core/algorithms.py
+++ b/pandas/core/algorithms.py
@@ -11,6 +11,7 @@
     Literal,
     Union,
     cast,
+    final,
 )
 from warnings import warn
 
@@ -1211,12 +1212,15 @@ def __init__(self, obj, n: int, keep: str):
     def compute(self, method: str) -> DataFrame | Series:
         raise NotImplementedError
 
+    @final
     def nlargest(self):
         return self.compute("nlargest")
 
+    @final
     def nsmallest(self):
         return self.compute("nsmallest")
 
+    @final
     @staticmethod
     def is_valid_dtype_n_method(dtype: DtypeObj) -> bool:
         """
@@ -1255,6 +1259,18 @@ def compute(self, method: str) -> Series:
 
         dropped = self.obj.dropna()
 
+        if is_extension_array_dtype(dropped.dtype):
+            # GH#41816 bc we have dropped NAs above, MaskedArrays can use the
+            #  numpy logic.
+            from pandas.core.arrays import BaseMaskedArray
+
+            arr = dropped._values
+            if isinstance(arr, BaseMaskedArray):
+                ser = type(dropped)(arr._data, index=dropped.index, name=dropped.name)
+
+                result = type(self)(ser, n=self.n, keep=self.keep).compute(method)
+                return result.astype(arr.dtype)
+
         # slow method
         if n >= len(self.obj):
             ascending = method == "nsmallest"
diff --git a/pandas/tests/series/methods/test_nlargest.py b/pandas/tests/series/methods/test_nlargest.py
index 3af06145b9fcd..c20831e6832a4 100644
--- a/pandas/tests/series/methods/test_nlargest.py
+++ b/pandas/tests/series/methods/test_nlargest.py
@@ -211,3 +211,14 @@ def test_nlargest_boolean(self, data, expected):
         result = ser.nlargest(1)
         expected = Series(expected)
         tm.assert_series_equal(result, expected)
+
+    def test_nlargest_nullable(self, any_nullable_numeric_dtype):
+        # GH#42816 NB so far we only handles cases without NAs
+        dtype = any_nullable_numeric_dtype
+        arr = np.random.randn(10).astype(dtype.lower(), copy=False)
+
+        ser = Series(arr, dtype=dtype)
+        result = ser.nlargest(5)
+
+        expected = Series(arr).nlargest(5).astype(dtype)
+        tm.assert_series_equal(result, expected)


Code review comments:
diff_hunk: @@ -211,3 +211,14 @@ def test_nlargest_boolean(self, data, expected):
         result = ser.nlargest(1)
         expected = Series(expected)
         tm.assert_series_equal(result, expected)
+
+    def test_nlargest_nullable(self, any_nullable_numeric_dtype):
+        # GH#42816 NB so far we only handles cases without NAs
path: pandas/tests/series/methods/test_nlargest.py
text: @user1:
hmm, so this PR does not close the issue since this worked in 1.2.5?

@author:
comment is out of date, this should now handle cases with NAs, though a test case needs to be added