Repository: pandas-dev/pandas

Issue:
BUG/EA: groupby on an EA should return the EA type
```
In [1]: df = pd.DataFrame({'Int': pd.Series([1, 2, 3], dtype='Int64'), 'A': [1, 2, 1]})
   ...: df
   ...: 
Out[1]: 
  Int  A
0   1  1
1   2  2
2   3  1

In [2]: df.groupby('A').Int.sum()
Out[2]: 
A
1    4
2    2
Name: Int, dtype: int64
```

[2] should be of type Int64; IOW this aggregation needs to be passed thru to the EA _from_sequence


Discussion:
@jreback Hi working on this issue; did some code walkthrough, learned that here https://github.com/pandas-dev/pandas/blob/master/pandas/core/groupby/groupby.py#L834 in the method `_python_agg_general` at `result, counts = self.grouper.agg_series(obj, f)` the `obj` type is `Int64` but the `result` type is `int64`. So, I checked the `agg_series` method, seems like a Cython converter converts it from object to int64 after calculating the aggregation result. So, I am kind of lost as to where I should put `_from_squence` after checking if it's of type `ExtensionArray`
so look at _wrap_aggregated_output

there are a number of paths here though 

Resolving patch:
diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py
--- a/pandas/core/groupby/groupby.py
+++ b/pandas/core/groupby/groupby.py
@@ -24,7 +24,8 @@ class providing the base-class of operations.
 from pandas.util._validators import validate_kwargs
 
 from pandas.core.dtypes.cast import maybe_downcast_to_dtype
-from pandas.core.dtypes.common import ensure_float, is_numeric_dtype, is_scalar
+from pandas.core.dtypes.common import (
+    ensure_float, is_extension_array_dtype, is_numeric_dtype, is_scalar)
 from pandas.core.dtypes.missing import isna, notna
 
 import pandas.core.algorithms as algorithms
@@ -754,7 +755,18 @@ def _try_cast(self, result, obj, numeric_only=False):
             dtype = obj.dtype
 
         if not is_scalar(result):
-            if numeric_only and is_numeric_dtype(dtype) or not numeric_only:
+            if is_extension_array_dtype(dtype):
+                # The function can return something of any type, so check
+                # if the type is compatible with the calling EA.
+                try:
+                    result = obj.values._from_sequence(result)
+                except Exception:
+                    # https://github.com/pandas-dev/pandas/issues/22850
+                    # pandas has no control over what 3rd-party ExtensionArrays
+                    # do in _values_from_sequence. We still want ops to work
+                    # though, so we catch any regular Exception.
+                    pass
+            elif numeric_only and is_numeric_dtype(dtype) or not numeric_only:
                 result = maybe_downcast_to_dtype(result, dtype)
 
         return result


Test patch:
diff --git a/pandas/tests/arrays/test_integer.py b/pandas/tests/arrays/test_integer.py
--- a/pandas/tests/arrays/test_integer.py
+++ b/pandas/tests/arrays/test_integer.py
@@ -650,9 +650,10 @@ def test_preserve_dtypes(op):
 
     # groupby
     result = getattr(df.groupby("A"), op)()
+
     expected = pd.DataFrame({
         "B": np.array([1.0, 3.0]),
-        "C": np.array([1, 3], dtype="int64")
+        "C": integer_array([1, 3], dtype="Int64")
     }, index=pd.Index(['a', 'b'], name='A'))
     tm.assert_frame_equal(result, expected)
 
@@ -673,9 +674,10 @@ def test_reduce_to_float(op):
 
     # groupby
     result = getattr(df.groupby("A"), op)()
+
     expected = pd.DataFrame({
         "B": np.array([1.0, 3.0]),
-        "C": np.array([1, 3], dtype="float64")
+        "C": integer_array([1, 3], dtype="Int64")
     }, index=pd.Index(['a', 'b'], name='A'))
     tm.assert_frame_equal(result, expected)
 
diff --git a/pandas/tests/sparse/test_groupby.py b/pandas/tests/sparse/test_groupby.py
--- a/pandas/tests/sparse/test_groupby.py
+++ b/pandas/tests/sparse/test_groupby.py
@@ -24,27 +24,39 @@ def test_first_last_nth(self):
         sparse_grouped = self.sparse.groupby('A')
         dense_grouped = self.dense.groupby('A')
 
+        sparse_grouped_first = sparse_grouped.first()
+        sparse_grouped_last = sparse_grouped.last()
+        sparse_grouped_nth = sparse_grouped.nth(1)
+
+        dense_grouped_first = dense_grouped.first().to_sparse()
+        dense_grouped_last = dense_grouped.last().to_sparse()
+        dense_grouped_nth = dense_grouped.nth(1).to_sparse()
+
         # TODO: shouldn't these all be spares or not?
-        tm.assert_frame_equal(sparse_grouped.first(),
-                              dense_grouped.first())
-        tm.assert_frame_equal(sparse_grouped.last(),
-                              dense_grouped.last())
-        tm.assert_frame_equal(sparse_grouped.nth(1),
-                              dense_grouped.nth(1).to_sparse())
+        tm.assert_frame_equal(sparse_grouped_first,
+                              dense_grouped_first)
+        tm.assert_frame_equal(sparse_grouped_last,
+                              dense_grouped_last)
+        tm.assert_frame_equal(sparse_grouped_nth,
+                              dense_grouped_nth)
 
     def test_aggfuncs(self):
         sparse_grouped = self.sparse.groupby('A')
         dense_grouped = self.dense.groupby('A')
 
-        tm.assert_frame_equal(sparse_grouped.mean(),
-                              dense_grouped.mean())
+        result = sparse_grouped.mean().to_sparse()
+        expected = dense_grouped.mean().to_sparse()
+
+        tm.assert_frame_equal(result, expected)
 
         # ToDo: sparse sum includes str column
         # tm.assert_frame_equal(sparse_grouped.sum(),
         #                       dense_grouped.sum())
 
-        tm.assert_frame_equal(sparse_grouped.count(),
-                              dense_grouped.count())
+        result = sparse_grouped.count().to_sparse()
+        expected = dense_grouped.count().to_sparse()
+
+        tm.assert_frame_equal(result, expected)
 
 
 @pytest.mark.parametrize("fill_value", [0, np.nan])
@@ -54,6 +66,5 @@ def test_groupby_includes_fill_value(fill_value):
                        'b': [fill_value, 1, fill_value, fill_value]})
     sdf = df.to_sparse(fill_value=fill_value)
     result = sdf.groupby('a').sum()
-    expected = df.groupby('a').sum()
-    tm.assert_frame_equal(result, expected,
-                          check_index_type=False)
+    expected = df.groupby('a').sum().to_sparse(fill_value=fill_value)
+    tm.assert_frame_equal(result, expected, check_index_type=False)
diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py
--- a/pandas/tests/test_resample.py
+++ b/pandas/tests/test_resample.py
@@ -1576,6 +1576,7 @@ def test_resample_categorical_data_with_timedeltaindex(self):
                               'Group': ['A', 'A']},
                              index=pd.to_timedelta([0, 10], unit='s'))
         expected = expected.reindex(['Group_obj', 'Group'], axis=1)
+        expected['Group'] = expected['Group_obj'].astype('category')
         tm.assert_frame_equal(result, expected)
 
     def test_resample_daily_anchored(self):