Skip to content

Commit 9004414

Browse files
authored
REG: quantile with IntegerArray/FloatingArray (#41428)
1 parent b117ab5 commit 9004414

File tree

3 files changed

+70
-27
lines changed

3 files changed

+70
-27
lines changed

pandas/core/array_algos/quantile.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,18 @@ def quantile_compat(values: ArrayLike, qs: np.ndarray, interpolation: str) -> Ar
3737
mask = isna(values)
3838
return _quantile_with_mask(values, mask, fill_value, qs, interpolation)
3939
else:
40-
return _quantile_ea_compat(values, qs, interpolation)
40+
# In general we don't want to import from arrays here;
41+
# this is temporary pending discussion in GH#41428
42+
from pandas.core.arrays import BaseMaskedArray
43+
44+
if isinstance(values, BaseMaskedArray):
45+
# e.g. IntegerArray, does not implement _from_factorized
46+
out = _quantile_ea_fallback(values, qs, interpolation)
47+
48+
else:
49+
out = _quantile_ea_compat(values, qs, interpolation)
50+
51+
return out
4152

4253

4354
def _quantile_with_mask(
@@ -144,3 +155,31 @@ def _quantile_ea_compat(
144155

145156
# error: Incompatible return value type (got "ndarray", expected "ExtensionArray")
146157
return result # type: ignore[return-value]
158+
159+
160+
def _quantile_ea_fallback(
161+
values: ExtensionArray, qs: np.ndarray, interpolation: str
162+
) -> ExtensionArray:
163+
"""
164+
quantile compatibility for ExtensionArray subclasses that do not
165+
implement `_from_factorized`, e.g. IntegerArray.
166+
167+
Notes
168+
-----
169+
We assume that all impacted cases are 1D-only.
170+
"""
171+
mask = np.atleast_2d(np.asarray(values.isna()))
172+
npvalues = np.atleast_2d(np.asarray(values))
173+
174+
res = _quantile_with_mask(
175+
npvalues,
176+
mask=mask,
177+
fill_value=values.dtype.na_value,
178+
qs=qs,
179+
interpolation=interpolation,
180+
)
181+
assert res.ndim == 2
182+
assert res.shape[0] == 1
183+
res = res[0]
184+
out = type(values)._from_sequence(res, dtype=values.dtype)
185+
return out

pandas/core/internals/blocks.py

-1
Original file line numberDiff line numberDiff line change
@@ -1316,7 +1316,6 @@ def quantile(
13161316
assert is_list_like(qs) # caller is responsible for this
13171317

13181318
result = quantile_compat(self.values, np.asarray(qs._values), interpolation)
1319-
13201319
return new_block(result, placement=self._mgr_locs, ndim=2)
13211320

13221321

pandas/tests/frame/methods/test_quantile.py

+30-25
Original file line numberDiff line numberDiff line change
@@ -550,31 +550,36 @@ class TestQuantileExtensionDtype:
550550
),
551551
pd.period_range("2016-01-01", periods=9, freq="D"),
552552
pd.date_range("2016-01-01", periods=9, tz="US/Pacific"),
553-
pytest.param(
554-
pd.array(np.arange(9), dtype="Int64"),
555-
marks=pytest.mark.xfail(reason="doesn't implement from_factorized"),
556-
),
557-
pytest.param(
558-
pd.array(np.arange(9), dtype="Float64"),
559-
marks=pytest.mark.xfail(reason="doesn't implement from_factorized"),
560-
),
553+
pd.array(np.arange(9), dtype="Int64"),
554+
pd.array(np.arange(9), dtype="Float64"),
561555
],
562556
ids=lambda x: str(x.dtype),
563557
)
564558
def index(self, request):
559+
# NB: not actually an Index object
565560
idx = request.param
566561
idx.name = "A"
567562
return idx
568563

564+
@pytest.fixture
565+
def obj(self, index, frame_or_series):
566+
# bc index is not always an Index (yet), we need to re-patch .name
567+
obj = frame_or_series(index).copy()
568+
569+
if frame_or_series is Series:
570+
obj.name = "A"
571+
else:
572+
obj.columns = ["A"]
573+
return obj
574+
569575
def compute_quantile(self, obj, qs):
570576
if isinstance(obj, Series):
571577
result = obj.quantile(qs)
572578
else:
573579
result = obj.quantile(qs, numeric_only=False)
574580
return result
575581

576-
def test_quantile_ea(self, index, frame_or_series):
577-
obj = frame_or_series(index).copy()
582+
def test_quantile_ea(self, obj, index):
578583

579584
# result should be invariant to shuffling
580585
indexer = np.arange(len(index), dtype=np.intp)
@@ -585,13 +590,14 @@ def test_quantile_ea(self, index, frame_or_series):
585590
result = self.compute_quantile(obj, qs)
586591

587592
# expected here assumes len(index) == 9
588-
expected = Series([index[4], index[0], index[-1]], index=qs, name="A")
589-
expected = frame_or_series(expected)
593+
expected = Series(
594+
[index[4], index[0], index[-1]], dtype=index.dtype, index=qs, name="A"
595+
)
596+
expected = type(obj)(expected)
590597

591598
tm.assert_equal(result, expected)
592599

593-
def test_quantile_ea_with_na(self, index, frame_or_series):
594-
obj = frame_or_series(index).copy()
600+
def test_quantile_ea_with_na(self, obj, index):
595601

596602
obj.iloc[0] = index._na_value
597603
obj.iloc[-1] = index._na_value
@@ -605,15 +611,15 @@ def test_quantile_ea_with_na(self, index, frame_or_series):
605611
result = self.compute_quantile(obj, qs)
606612

607613
# expected here assumes len(index) == 9
608-
expected = Series([index[4], index[1], index[-2]], index=qs, name="A")
609-
expected = frame_or_series(expected)
614+
expected = Series(
615+
[index[4], index[1], index[-2]], dtype=index.dtype, index=qs, name="A"
616+
)
617+
expected = type(obj)(expected)
610618
tm.assert_equal(result, expected)
611619

612620
# TODO: filtering can be removed after GH#39763 is fixed
613621
@pytest.mark.filterwarnings("ignore:Using .astype to convert:FutureWarning")
614-
def test_quantile_ea_all_na(self, index, frame_or_series):
615-
616-
obj = frame_or_series(index).copy()
622+
def test_quantile_ea_all_na(self, obj, index, frame_or_series):
617623

618624
obj.iloc[:] = index._na_value
619625

@@ -630,13 +636,12 @@ def test_quantile_ea_all_na(self, index, frame_or_series):
630636
result = self.compute_quantile(obj, qs)
631637

632638
expected = index.take([-1, -1, -1], allow_fill=True, fill_value=index._na_value)
633-
expected = Series(expected, index=qs)
634-
expected = frame_or_series(expected)
639+
expected = Series(expected, index=qs, name="A")
640+
expected = type(obj)(expected)
635641
tm.assert_equal(result, expected)
636642

637-
def test_quantile_ea_scalar(self, index, frame_or_series):
643+
def test_quantile_ea_scalar(self, obj, index):
638644
# scalar qs
639-
obj = frame_or_series(index).copy()
640645

641646
# result should be invariant to shuffling
642647
indexer = np.arange(len(index), dtype=np.intp)
@@ -646,8 +651,8 @@ def test_quantile_ea_scalar(self, index, frame_or_series):
646651
qs = 0.5
647652
result = self.compute_quantile(obj, qs)
648653

649-
expected = Series({"A": index[4]}, name=0.5)
650-
if frame_or_series is Series:
654+
expected = Series({"A": index[4]}, dtype=index.dtype, name=0.5)
655+
if isinstance(obj, Series):
651656
expected = expected["A"]
652657
assert result == expected
653658
else:

0 commit comments

Comments
 (0)