Repository: NVIDIA-Merlin/NVTabular

Issue:
[BUG] Groupby aggregations on non float64 columns throws an error
**Describe the bug**
When running `Groupby` on columns with a dtype other than `float64`, I receive the following error:

```
TypeError: Dtype discrepancy detected for column measurement_std: operator Groupby reported dtype `float32` but returned dtype `float64`.
```

**Steps/Code to reproduce bug**
![image](https://user-images.githubusercontent.com/2444926/172293775-eed58ba1-d4c4-4b5e-a06e-1a08be97636b.png)

Here is code that can be copied over
```
import cudf
import nvtabular as nvt

gdf = cudf.DataFrame(data={'cat': ['a', 'a', 'b'], 'timestamp': [1, 2, 1], 'measurement': [0.1, 0.2, 0.5]})

gdf.measurement = gdf.measurement.astype('float32')

out = ['measurement', 'cat'] >> nvt.ops.Groupby('cat', aggs=['std'])


ds = nvt.Dataset(gdf)
wf = nvt.Workflow(out)
res = wf.fit_transform(ds)

res.compute()
```

**Expected behavior**
The transformation to work, results to be returned without error.

**Environment details (please complete the following information):**
`merlin-pytorch-training:22.05` image with all Merlin repos updated and pip installed today


Discussion:
@radekosmulski thanks for reporting the issue. As an alternative may be you can use `JoinGroupby`:

```
import cudf
import nvtabular as nvt

gdf = cudf.DataFrame(data={'cat': ['a', 'a', 'b'], 'timestamp': [1, 2, 1], 'measurement': [0.1, 0.2, 0.5]})

gdf.measurement = gdf.measurement.astype('float32')

out = ['cat'] >> nvt.ops.JoinGroupby(stats=['std'], cont_cols=['measurement']) >> nvt.ops.Rename(name = 'measurement_std')

ds = nvt.Dataset(gdf)
wf = nvt.Workflow(out)
res = wf.fit_transform(ds)

res.compute()
```

Resolving patch:
diff --git a/nvtabular/ops/groupby.py b/nvtabular/ops/groupby.py
--- a/nvtabular/ops/groupby.py
+++ b/nvtabular/ops/groupby.py
@@ -12,6 +12,8 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import re
+
 import numpy
 from dask.dataframe.utils import meta_nonempty
 
@@ -184,7 +186,14 @@ def _compute_dtype(self, col_schema, input_schema):
         dtype = col_schema.dtype
         is_list = col_schema.is_list
 
-        dtypes = {"count": numpy.int32, "mean": numpy.float32}
+        dtypes = {
+            "count": numpy.int32,
+            "nunique": numpy.int32,
+            "mean": numpy.float32,
+            "var": numpy.float32,
+            "std": numpy.float32,
+            "median": numpy.float32,
+        }
 
         is_lists = {"list": True}
 
@@ -235,9 +244,9 @@ def _apply_aggs(_df, groupby_cols, _list_aggs, _conv_aggs, name_sep="_", ascendi
             df.drop(columns=[col + f"{name_sep}list"], inplace=True)
 
     for col in df.columns:
-        if col.endswith(f"{name_sep}count"):
+        if re.search(f"{name_sep}(count|nunique)", col):
             df[col] = df[col].astype(numpy.int32)
-        elif col.endswith(f"{name_sep}mean"):
+        elif re.search(f"{name_sep}(mean|median|std|var)", col):
             df[col] = df[col].astype(numpy.float32)
 
     return df


Test patch:
diff --git a/tests/unit/ops/test_groupyby.py b/tests/unit/ops/test_groupyby.py
--- a/tests/unit/ops/test_groupyby.py
+++ b/tests/unit/ops/test_groupyby.py
@@ -163,3 +163,44 @@ def test_groupby_selector_cols():
 
     # If groupby_cols aren't included in the selector, they shouldn't be in the output
     assert "name" not in workflow.output_node.output_schema.column_names
+
+
+@pytest.mark.parametrize("cpu", _CPU)
+def test_groupby_casting_in_aggregations(cpu):
+    # Initial dataset
+    size = 60
+    names = ["Dave", "Zelda"]
+    df1 = make_df(
+        {
+            "name": np.random.choice(names, size=size),
+            "x": np.random.randint(0, 2, size=size).astype(np.int8),
+            "y": np.linspace(0.0, 10.0, num=size),
+            "z": np.linspace(0.0, 10.0, num=size).astype(np.float32),
+            "shuffle": np.random.uniform(low=0.0, high=10.0, size=size),
+        }
+    )
+    df1 = df1.sort_values("shuffle").drop(columns="shuffle").reset_index(drop=True)
+
+    # Create a ddf, and be sure to shuffle by the groupby keys
+    ddf1 = dd.from_pandas(df1, npartitions=3).shuffle("name")
+    dataset = nvt.Dataset(ddf1, cpu=cpu)
+
+    # Define Groupby Workflow
+    aggs_to_test = ["mean", "std", "var", "median", "nunique"]
+    groupby_features = ColumnSelector(["name", "x", "y", "z"]) >> ops.Groupby(
+        groupby_cols="name",
+        aggs={"x": aggs_to_test, "y": aggs_to_test, "z": aggs_to_test},
+        name_sep="-",
+    )
+    processor = nvt.Workflow(groupby_features)
+    processor.fit(dataset)
+    new_gdf = processor.transform(dataset).to_ddf().compute()
+
+    df1.set_index("name", inplace=True)
+    for agg in aggs_to_test:
+        for col in list("xyz"):
+            for name in names:
+                without_agg = getattr(df1.loc[name][col], agg)()
+                with_agg = new_gdf.loc[new_gdf.name == name, f"{col}-{agg}"]
+                with_agg = with_agg.item() if hasattr(with_agg, "item") else with_agg.loc[0][0]
+                assert np.allclose(without_agg, with_agg)