Skip to content

Commit a811388

Browse files
authored
BUG(string dtype): groupby/resampler.min/max returns float on all NA strings (#60985)
* BUG(string dtype): groupby/resampler.min/max returns float on all NA strings * Merge cleanup * whatsnew * Add type-ignore * Remove condition
1 parent c27a309 commit a811388

File tree

4 files changed

+96
-13
lines changed

4 files changed

+96
-13
lines changed

doc/source/whatsnew/v2.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ Conversion
119119

120120
Strings
121121
^^^^^^^
122+
- Bug in :meth:`.DataFrameGroupBy.min`, :meth:`.DataFrameGroupBy.max`, :meth:`.Resampler.min`, :meth:`.Resampler.max` on string input of all NA values would return float dtype; now returns string (:issue:`60810`)
122123
- Bug in :meth:`DataFrame.sum` with ``axis=1``, :meth:`.DataFrameGroupBy.sum` or :meth:`.SeriesGroupBy.sum` with ``skipna=True``, and :meth:`.Resampler.sum` on :class:`StringDtype` with all NA values resulted in ``0`` and is now the empty string ``""`` (:issue:`60229`)
123124
- Bug in :meth:`Series.__pos__` and :meth:`DataFrame.__pos__` did not raise for :class:`StringDtype` with ``storage="pyarrow"`` (:issue:`60710`)
124125
- Bug in :meth:`Series.rank` for :class:`StringDtype` with ``storage="pyarrow"`` incorrectly returning integer results in case of ``method="average"`` and raising an error if it would truncate results (:issue:`59768`)

pandas/core/groupby/groupby.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class providing the base-class of operations.
8181
is_numeric_dtype,
8282
is_object_dtype,
8383
is_scalar,
84+
is_string_dtype,
8485
needs_i8_conversion,
8586
pandas_dtype,
8687
)
@@ -1724,8 +1725,13 @@ def _agg_py_fallback(
17241725
# preserve the kind of exception that raised
17251726
raise type(err)(msg) from err
17261727

1727-
if ser.dtype == object:
1728+
dtype = ser.dtype
1729+
if dtype == object:
17281730
res_values = res_values.astype(object, copy=False)
1731+
elif is_string_dtype(dtype):
1732+
# mypy doesn't infer dtype is an ExtensionDtype
1733+
string_array_cls = dtype.construct_array_type() # type: ignore[union-attr]
1734+
res_values = string_array_cls._from_sequence(res_values, dtype=dtype)
17291735

17301736
# If we are DataFrameGroupBy and went through a SeriesGroupByPath
17311737
# then we need to reshape

pandas/tests/groupby/test_reductions.py

+88-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
isna,
2121
)
2222
import pandas._testing as tm
23+
from pandas.tests.groupby import get_groupby_method_args
2324
from pandas.util import _test_decorators as td
2425

2526

@@ -956,17 +957,95 @@ def test_min_empty_string_dtype(func, string_dtype_no_object):
956957

957958

958959
@pytest.mark.parametrize("min_count", [0, 1])
959-
def test_string_dtype_empty_sum(string_dtype_no_object, skipna, min_count):
960-
# https://github.com/pandas-dev/pandas/issues/60229
960+
@pytest.mark.parametrize("test_series", [True, False])
961+
def test_string_dtype_all_na(
962+
string_dtype_no_object, reduction_func, skipna, min_count, test_series
963+
):
964+
# https://github.com/pandas-dev/pandas/issues/60985
965+
if reduction_func == "corrwith":
966+
# corrwith is deprecated.
967+
return
968+
961969
dtype = string_dtype_no_object
970+
971+
if reduction_func in [
972+
"any",
973+
"all",
974+
"idxmin",
975+
"idxmax",
976+
"mean",
977+
"median",
978+
"std",
979+
"var",
980+
]:
981+
kwargs = {"skipna": skipna}
982+
elif reduction_func in ["kurt"]:
983+
kwargs = {"min_count": min_count}
984+
elif reduction_func in ["count", "nunique", "quantile", "sem", "size"]:
985+
kwargs = {}
986+
else:
987+
kwargs = {"skipna": skipna, "min_count": min_count}
988+
989+
expected_dtype, expected_value = dtype, pd.NA
990+
if reduction_func in ["all", "any"]:
991+
expected_dtype = "bool"
992+
# TODO: For skipna=False, bool(pd.NA) raises; should groupby?
993+
expected_value = not skipna if reduction_func == "any" else True
994+
elif reduction_func in ["count", "nunique", "size"]:
995+
# TODO: Should be more consistent - return Int64 when dtype.na_value is pd.NA?
996+
if (
997+
test_series
998+
and reduction_func == "size"
999+
and dtype.storage == "pyarrow"
1000+
and dtype.na_value is pd.NA
1001+
):
1002+
expected_dtype = "Int64"
1003+
else:
1004+
expected_dtype = "int64"
1005+
expected_value = 1 if reduction_func == "size" else 0
1006+
elif reduction_func in ["idxmin", "idxmax"]:
1007+
expected_dtype, expected_value = "float64", np.nan
1008+
elif not skipna or min_count > 0:
1009+
expected_value = pd.NA
1010+
elif reduction_func == "sum":
1011+
# https://github.com/pandas-dev/pandas/pull/60936
1012+
expected_value = ""
1013+
9621014
df = DataFrame({"a": ["x"], "b": [pd.NA]}, dtype=dtype)
963-
gb = df.groupby("a")
964-
result = gb.sum(skipna=skipna, min_count=min_count)
965-
value = "" if skipna and min_count == 0 else pd.NA
966-
expected = DataFrame(
967-
{"b": value}, index=pd.Index(["x"], name="a", dtype=dtype), dtype=dtype
968-
)
969-
tm.assert_frame_equal(result, expected)
1015+
obj = df["b"] if test_series else df
1016+
args = get_groupby_method_args(reduction_func, obj)
1017+
gb = obj.groupby(df["a"])
1018+
method = getattr(gb, reduction_func)
1019+
1020+
if reduction_func in [
1021+
"mean",
1022+
"median",
1023+
"kurt",
1024+
"prod",
1025+
"quantile",
1026+
"sem",
1027+
"skew",
1028+
"std",
1029+
"var",
1030+
]:
1031+
msg = f"dtype '{dtype}' does not support operation '{reduction_func}'"
1032+
with pytest.raises(TypeError, match=msg):
1033+
method(*args, **kwargs)
1034+
return
1035+
elif reduction_func in ["idxmin", "idxmax"] and not skipna:
1036+
msg = f"{reduction_func} with skipna=False encountered an NA value."
1037+
with pytest.raises(ValueError, match=msg):
1038+
method(*args, **kwargs)
1039+
return
1040+
1041+
result = method(*args, **kwargs)
1042+
index = pd.Index(["x"], name="a", dtype=dtype)
1043+
if test_series or reduction_func == "size":
1044+
name = None if not test_series and reduction_func == "size" else "b"
1045+
expected = Series(expected_value, index=index, dtype=expected_dtype, name=name)
1046+
else:
1047+
expected = DataFrame({"b": expected_value}, index=index, dtype=expected_dtype)
1048+
tm.assert_equal(result, expected)
9701049

9711050

9721051
def test_max_nan_bug():

pandas/tests/resample/test_resampler_grouper.py

-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import numpy as np
44
import pytest
55

6-
from pandas._config import using_string_dtype
7-
86
from pandas.compat import is_platform_windows
97

108
import pandas as pd
@@ -462,7 +460,6 @@ def test_empty(keys):
462460
tm.assert_frame_equal(result, expected)
463461

464462

465-
@pytest.mark.xfail(using_string_dtype(), reason="TODO(infer_string)")
466463
@pytest.mark.parametrize("consolidate", [True, False])
467464
def test_resample_groupby_agg_object_dtype_all_nan(consolidate):
468465
# https://github.com/pandas-dev/pandas/issues/39329

0 commit comments

Comments
 (0)