Skip to content

Commit 4412800

Browse files
authored
BUG: value_counts returning incorrect dtype for string dtype (#55627)
1 parent 43c7d40 commit 4412800

File tree

4 files changed

+68
-2
lines changed

4 files changed

+68
-2
lines changed

doc/source/whatsnew/v2.1.2.rst

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Fixed regressions
2525
Bug fixes
2626
~~~~~~~~~
2727
- Fixed bug in :class:`.DataFrameGroupBy` reductions not preserving object dtype when ``infer_string`` is set (:issue:`55620`)
28+
- Fixed bug in :meth:`.SeriesGroupBy.value_counts` returning incorrect dtype for string columns (:issue:`55627`)
2829
- Fixed bug in :meth:`Categorical.equals` if other has arrow backed string dtype (:issue:`55364`)
2930
- Fixed bug in :meth:`DataFrame.__setitem__` not inferring string dtype for zero-dimensional array with ``infer_string=True`` (:issue:`55366`)
3031
- Fixed bug in :meth:`DataFrame.idxmin` and :meth:`DataFrame.idxmax` raising for arrow dtypes (:issue:`55368`)

pandas/core/groupby/groupby.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ class providing the base-class of operations.
108108
SparseArray,
109109
)
110110
from pandas.core.arrays.string_ import StringDtype
111+
from pandas.core.arrays.string_arrow import (
112+
ArrowStringArray,
113+
ArrowStringArrayNumpySemantics,
114+
)
111115
from pandas.core.base import (
112116
PandasObject,
113117
SelectionMixin,
@@ -2854,7 +2858,9 @@ def _value_counts(
28542858
result_series.name = name
28552859
result_series.index = index.set_names(range(len(columns)))
28562860
result_frame = result_series.reset_index()
2857-
result_frame.columns = columns + [name]
2861+
orig_dtype = self.grouper.groupings[0].obj.columns.dtype # type: ignore[union-attr] # noqa: E501
2862+
cols = Index(columns, dtype=orig_dtype).insert(len(columns), name)
2863+
result_frame.columns = cols
28582864
result = result_frame
28592865
return result.__finalize__(self.obj, method="value_counts")
28602866

@@ -3006,7 +3012,12 @@ def size(self) -> DataFrame | Series:
30063012
dtype_backend: None | Literal["pyarrow", "numpy_nullable"] = None
30073013
if isinstance(self.obj, Series):
30083014
if isinstance(self.obj.array, ArrowExtensionArray):
3009-
dtype_backend = "pyarrow"
3015+
if isinstance(self.obj.array, ArrowStringArrayNumpySemantics):
3016+
dtype_backend = None
3017+
elif isinstance(self.obj.array, ArrowStringArray):
3018+
dtype_backend = "numpy_nullable"
3019+
else:
3020+
dtype_backend = "pyarrow"
30103021
elif isinstance(self.obj.array, BaseMaskedArray):
30113022
dtype_backend = "numpy_nullable"
30123023
# TODO: For DataFrames what if columns are mixed arrow/numpy/masked?

pandas/tests/groupby/methods/test_size.py

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import numpy as np
22
import pytest
33

4+
import pandas.util._test_decorators as td
5+
46
from pandas.core.dtypes.common import is_integer_dtype
57

68
from pandas import (
@@ -104,3 +106,25 @@ def test_size_series_masked_type_returns_Int64(dtype):
104106
result = ser.groupby(level=0).size()
105107
expected = Series([2, 1], dtype="Int64", index=["a", "b"])
106108
tm.assert_series_equal(result, expected)
109+
110+
111+
@pytest.mark.parametrize(
112+
"dtype",
113+
[
114+
object,
115+
pytest.param("string[pyarrow_numpy]", marks=td.skip_if_no("pyarrow")),
116+
pytest.param("string[pyarrow]", marks=td.skip_if_no("pyarrow")),
117+
],
118+
)
119+
def test_size_strings(dtype):
120+
# GH#55627
121+
df = DataFrame({"a": ["a", "a", "b"], "b": "a"}, dtype=dtype)
122+
result = df.groupby("a")["b"].size()
123+
exp_dtype = "Int64" if dtype == "string[pyarrow]" else "int64"
124+
expected = Series(
125+
[2, 1],
126+
index=Index(["a", "b"], name="a", dtype=dtype),
127+
name="b",
128+
dtype=exp_dtype,
129+
)
130+
tm.assert_series_equal(result, expected)

pandas/tests/groupby/methods/test_value_counts.py

+30
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import numpy as np
1010
import pytest
1111

12+
import pandas.util._test_decorators as td
13+
1214
from pandas import (
1315
Categorical,
1416
CategoricalIndex,
@@ -369,6 +371,14 @@ def test_against_frame_and_seriesgroupby(
369371
tm.assert_frame_equal(result, expected)
370372

371373

374+
@pytest.mark.parametrize(
375+
"dtype",
376+
[
377+
object,
378+
pytest.param("string[pyarrow_numpy]", marks=td.skip_if_no("pyarrow")),
379+
pytest.param("string[pyarrow]", marks=td.skip_if_no("pyarrow")),
380+
],
381+
)
372382
@pytest.mark.parametrize("normalize", [True, False])
373383
@pytest.mark.parametrize(
374384
"sort, ascending, expected_rows, expected_count, expected_group_size",
@@ -386,7 +396,10 @@ def test_compound(
386396
expected_rows,
387397
expected_count,
388398
expected_group_size,
399+
dtype,
389400
):
401+
education_df = education_df.astype(dtype)
402+
education_df.columns = education_df.columns.astype(dtype)
390403
# Multiple groupby keys and as_index=False
391404
gp = education_df.groupby(["country", "gender"], as_index=False, sort=False)
392405
result = gp["education"].value_counts(
@@ -395,11 +408,17 @@ def test_compound(
395408
expected = DataFrame()
396409
for column in ["country", "gender", "education"]:
397410
expected[column] = [education_df[column][row] for row in expected_rows]
411+
expected = expected.astype(dtype)
412+
expected.columns = expected.columns.astype(dtype)
398413
if normalize:
399414
expected["proportion"] = expected_count
400415
expected["proportion"] /= expected_group_size
416+
if dtype == "string[pyarrow]":
417+
expected["proportion"] = expected["proportion"].convert_dtypes()
401418
else:
402419
expected["count"] = expected_count
420+
if dtype == "string[pyarrow]":
421+
expected["count"] = expected["count"].convert_dtypes()
403422
tm.assert_frame_equal(result, expected)
404423

405424

@@ -1146,3 +1165,14 @@ def test_value_counts_time_grouper(utc):
11461165
)
11471166
expected = Series(1, index=index, name="count")
11481167
tm.assert_series_equal(result, expected)
1168+
1169+
1170+
def test_value_counts_integer_columns():
1171+
# GH#55627
1172+
df = DataFrame({1: ["a", "a", "a"], 2: ["a", "a", "d"], 3: ["a", "b", "c"]})
1173+
gp = df.groupby([1, 2], as_index=False, sort=False)
1174+
result = gp[3].value_counts()
1175+
expected = DataFrame(
1176+
{1: ["a", "a", "a"], 2: ["a", "a", "d"], 3: ["a", "b", "c"], "count": 1}
1177+
)
1178+
tm.assert_frame_equal(result, expected)

0 commit comments

Comments
 (0)