diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst index 52fc8512c9db3..370cad535564f 100644 --- a/doc/source/whatsnew/v2.1.0.rst +++ b/doc/source/whatsnew/v2.1.0.rst @@ -413,10 +413,10 @@ Groupby/resample/rolling or :class:`PeriodIndex`, and the ``groupby`` method was given a function as its first argument, the function operated on the whole index rather than each element of the index. (:issue:`51979`) - Bug in :meth:`DataFrameGroupBy.apply` causing an error to be raised when the input :class:`DataFrame` was subset as a :class:`DataFrame` after groupby (``[['a']]`` and not ``['a']``) and the given callable returned :class:`Series` that were not all indexed the same. (:issue:`52444`) +- Bug in :meth:`DataFrameGroupBy.apply` raising a ``TypeError`` when selecting multiple columns and providing a function that returns ``np.ndarray`` results (:issue:`18930`) - Bug in :meth:`GroupBy.groups` with a datetime key in conjunction with another key produced incorrect number of group keys (:issue:`51158`) - Bug in :meth:`GroupBy.quantile` may implicitly sort the result index with ``sort=False`` (:issue:`53009`) - Bug in :meth:`GroupBy.var` failing to raise ``TypeError`` when called with datetime64, timedelta64 or :class:`PeriodDtype` values (:issue:`52128`, :issue:`53045`) -- Reshaping ^^^^^^^^^ diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index d26448dffc11a..c66aaf3c82c70 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -51,6 +51,7 @@ CategoricalDtype, IntervalDtype, ) +from pandas.core.dtypes.inference import is_hashable from pandas.core.dtypes.missing import ( isna, notna, @@ -1474,9 +1475,16 @@ def _wrap_applied_output( # fall through to the outer else clause # TODO: sure this is right? we used to do this # after raising AttributeError above - return self.obj._constructor_sliced( - values, index=key_index, name=self._selection - ) + # GH 18930 + if not is_hashable(self._selection): + # error: Need type annotation for "name" + name = tuple(self._selection) # type: ignore[var-annotated, arg-type] + else: + # error: Incompatible types in assignment + # (expression has type "Hashable", variable + # has type "Tuple[Any, ...]") + name = self._selection # type: ignore[assignment] + return self.obj._constructor_sliced(values, index=key_index, name=name) elif not isinstance(first_not_none, Series): # values are not series or array-like but scalars # self._selection not passed through to Series as the diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index b52b708de50a6..0cdb11cfbf6e0 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -1404,3 +1404,15 @@ def test_apply_inconsistent_output(group_col): ) tm.assert_series_equal(result, expected) + + +def test_apply_array_output_multi_getitem(): + # GH 18930 + df = DataFrame( + {"A": {"a": 1, "b": 2}, "B": {"a": 1, "b": 2}, "C": {"a": 1, "b": 2}} + ) + result = df.groupby("A")[["B", "C"]].apply(lambda x: np.array([0])) + expected = Series( + [np.array([0])] * 2, index=Index([1, 2], name="A"), name=("B", "C") + ) + tm.assert_series_equal(result, expected)