Repository: pandas-dev/pandas

Issue:
Transform with Count Agg against DateTime returns DateTime
#### Code Sample, a copy-pastable example if possible

```python
df = pd.DataFrame({'a': pd.date_range('2018-01-01', periods=3), 'b': range(3)})
df.groupby('b')['a'].transform('count') 

0   1970-01-01 00:00:00.000000001
1   1970-01-01 00:00:00.000000001
2   1970-01-01 00:00:00.000000001
Name: a, dtype: datetime64[ns]
```

#### Expected Output
The value 1.0 broadcasted with a float dtype, not a datetime64

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: 78c3ff97a8795c2de9861b6b0a40a5fa4b8e331f
python: 3.6.1.final.0
python-bits: 64
OS: Darwin
OS-release: 17.3.0
machine: x86_64
processor: i386
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.23.0.dev0+101.g78c3ff97a
pytest: None
pip: 9.0.1
setuptools: 27.2.0
Cython: None
numpy: 1.13.1
scipy: 0.19.1
pyarrow: None
xarray: None
IPython: 6.1.0
sphinx: 1.6.5
patsy: None
dateutil: 2.6.1
pytz: 2017.2
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 2.1.1
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: 4.6.0
html5lib: 0.999999999
sqlalchemy: 1.1.13
pymysql: None
psycopg2: 2.7.3.2 (dt dec pq3 ext lo64)
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
This appears to be a result of the following code:

https://github.com/pandas-dev/pandas/blob/8347ff8c9d8876fa46f73dbc48919a672caef1d2/pandas/core/groupby.py#L3380

It affects other aggregation functions like ``size``, ``rank`` and ``cumcount`` that I would expect should always return a number and not a datetime64.

One solution I could think of is to blacklist those particular aggregations from being cast, although I'd have to think through where exactly to apply that blacklist. 

``_transform_fast`` in the linked code only receives a lambda function as an argument to execute aggregation. I'm not sure if there's a way to inspect that lambda to see the exact agg function being used, if that's even the approach we would want to take
thought we had an issue about this. https://github.com/pandas-dev/pandas/issues/15562 is related.

On certain operations we should *not* try to cast (count, size, rank). These are normally dispatched to specific methods for non-transforming groupbys (they just calll the method on Series/FrameGroupBy), this is not a problem. However transform calls a cython function and then does the casting. 

So need to wade into this and make a better method of doing this. Could certainly pass the name of the function into ``_transform_fast`` (we may at a higher level).

Resolving patch:
diff --git a/pandas/core/groupby.py b/pandas/core/groupby.py
--- a/pandas/core/groupby.py
+++ b/pandas/core/groupby.py
@@ -345,6 +345,8 @@
 _cython_transforms = frozenset(['cumprod', 'cumsum', 'shift',
                                 'cummin', 'cummax'])
 
+_cython_cast_blacklist = frozenset(['rank', 'count', 'size'])
+
 
 class Grouper(object):
     """
@@ -965,6 +967,21 @@ def _try_cast(self, result, obj, numeric_only=False):
 
         return result
 
+    def _transform_should_cast(self, func_nm):
+        """
+        Parameters:
+        -----------
+        func_nm: str
+            The name of the aggregation function being performed
+
+        Returns:
+        --------
+        bool
+            Whether transform should attempt to cast the result of aggregation
+        """
+        return (self.size().fillna(0) > 0).any() and (func_nm not in
+                                                      _cython_cast_blacklist)
+
     def _cython_transform(self, how, numeric_only=True):
         output = collections.OrderedDict()
         for name, obj in self._iterate_slices():
@@ -3333,7 +3350,7 @@ def transform(self, func, *args, **kwargs):
             else:
                 # cythonized aggregation and merge
                 return self._transform_fast(
-                    lambda: getattr(self, func)(*args, **kwargs))
+                    lambda: getattr(self, func)(*args, **kwargs), func)
 
         # reg transform
         klass = self._selected_obj.__class__
@@ -3364,7 +3381,7 @@ def transform(self, func, *args, **kwargs):
         result.index = self._selected_obj.index
         return result
 
-    def _transform_fast(self, func):
+    def _transform_fast(self, func, func_nm):
         """
         fast version of transform, only applicable to
         builtin/cythonizable functions
@@ -3373,7 +3390,7 @@ def _transform_fast(self, func):
             func = getattr(self, func)
 
         ids, _, ngroup = self.grouper.group_info
-        cast = (self.size().fillna(0) > 0).any()
+        cast = self._transform_should_cast(func_nm)
         out = algorithms.take_1d(func().values, ids)
         if cast:
             out = self._try_cast(out, self.obj)
@@ -4127,15 +4144,15 @@ def transform(self, func, *args, **kwargs):
         if not result.columns.equals(obj.columns):
             return self._transform_general(func, *args, **kwargs)
 
-        return self._transform_fast(result, obj)
+        return self._transform_fast(result, obj, func)
 
-    def _transform_fast(self, result, obj):
+    def _transform_fast(self, result, obj, func_nm):
         """
         Fast transform path for aggregations
         """
         # if there were groups with no observations (Categorical only?)
         # try casting data to original dtype
-        cast = (self.size().fillna(0) > 0).any()
+        cast = self._transform_should_cast(func_nm)
 
         # for each col, reshape to to size of original frame
         # by take operation


Test patch:
diff --git a/pandas/tests/groupby/test_transform.py b/pandas/tests/groupby/test_transform.py
--- a/pandas/tests/groupby/test_transform.py
+++ b/pandas/tests/groupby/test_transform.py
@@ -582,3 +582,28 @@ def test_transform_with_non_scalar_group(self):
                                'group.*',
                                df.groupby(axis=1, level=1).transform,
                                lambda z: z.div(z.sum(axis=1), axis=0))
+
+    @pytest.mark.parametrize('cols,exp,comp_func', [
+        ('a', pd.Series([1, 1, 1], name='a'), tm.assert_series_equal),
+        (['a', 'c'], pd.DataFrame({'a': [1, 1, 1], 'c': [1, 1, 1]}),
+         tm.assert_frame_equal)
+    ])
+    @pytest.mark.parametrize('agg_func', [
+        'count', 'rank', 'size'])
+    def test_transform_numeric_ret(self, cols, exp, comp_func, agg_func):
+        if agg_func == 'size' and isinstance(cols, list):
+            pytest.xfail("'size' transformation not supported with "
+                         "NDFrameGroupy")
+
+        # GH 19200
+        df = pd.DataFrame(
+            {'a': pd.date_range('2018-01-01', periods=3),
+             'b': range(3),
+             'c': range(7, 10)})
+
+        result = df.groupby('b')[cols].transform(agg_func)
+
+        if agg_func == 'rank':
+            exp = exp.astype('float')
+
+        comp_func(result, exp)