Repository: NVIDIA-Merlin/NVTabular

Issue:
[FEA] Return embedding table shape by feeding colum group to get_embedding_sizes() function
**Is your feature request related to a problem? Please describe.**

In the current case, to get the embedding table shape, we need to do as follows:
```
cat_features = ['col'] >> Cateforify()
workflow = nvt.Workflow(cat_features) 
dataset = nvt.Dataset(gdf)
workflow.fit(dataset)
EMBEDDING_TABLE_SHAPES = nvt.ops.get_embedding_sizes(workflow)
```

but if we do have many other features from other ops, and not want to add `cat_features`  in the final workflow, we cannot get  embedding table shape from `nvt.ops.get_embedding_sizes(workflow)` because it would return {}.  

To avoid that can we change `def get_embedding_sizes(workflow):` to `def get_embedding_sizes(ColumnGroups):`
 or something like that so that we can run `nvt.ops.get_embedding_sizes(cat_features)` instead.

https://github.com/NVIDIA/NVTabular/blob/c7b7e05564ea675f00cad6e784cc360f8bb4ee85/nvtabular/ops/categorify.py#L449



Discussion:
@gabrielspmoreira for viz.
This is possible now, but the api is a little clunky:

```python
cat_features.op.get_embedding_sizes(cat_features.columns)
```

> This is possible now, but the api is a little clunky:
> 
> ```python
> cat_features.op.get_embedding_sizes(cat_features.columns)
> ```

Thanks Ben. will try it out.
Thanks @benfred . I have tried your suggested command and it does work.
`categ_features.op.get_embedding_sizes(categ_features.columns)`

The user could be interested to know only the cardinality of the categorical features and not our recommended embedding sizes. So maybe having a `get_categ_cardinalities()` could be interesting.
Furthermore, that function could be global to the workflow, making it clear that it is only available after the fit() call and returning a dict with the cardinality of all categorical features.

Resolving patch:
diff --git a/nvtabular/ops/categorify.py b/nvtabular/ops/categorify.py
--- a/nvtabular/ops/categorify.py
+++ b/nvtabular/ops/categorify.py
@@ -433,10 +433,33 @@ def _get_embedding_order(cat_names):
     return cat_names
 
 
-def get_embedding_sizes(workflow):
-    """Returns a dictionary of best embedding sizes from the workflow"""
+def get_embedding_sizes(source, output_dtypes=None):
+    """Returns a dictionary of embedding sizes from a workflow or column_group
+
+    Parameters
+    ----------
+    source : Workflow or ColumnGroup
+        Either a nvtabular Workflow or ColumnGroup object that we should use to find
+        embedding sizes
+    output_dtypes : dict, optional
+        Optional dictionary of column_name:dtype. If passing a workflow object dtypes
+        will be read from the workflow. This is used to figure out which columns
+        are multihot-categorical, which are split out by this function. If passed a column_group
+        and this parameter isn't set, you won't have multihot columns returned separately
+    """
     # TODO: do we need to distinguish multihot columns here?  (if so why? )
-    queue = [workflow.column_group]
+
+    # have to lazy import Workflow to avoid circular import errors
+    from nvtabular.workflow import Workflow
+
+    if isinstance(source, Workflow):
+        queue = [source.column_group]
+        output_dtypes = output_dtypes or source.output_dtypes
+    else:
+        # passed in a column group
+        queue = [source]
+        output_dtypes = output_dtypes or {}
+
     output = {}
     multihot_columns = set()
     while queue:
@@ -444,12 +467,13 @@ def get_embedding_sizes(workflow):
         if current.op and hasattr(current.op, "get_embedding_sizes"):
             output.update(current.op.get_embedding_sizes(current.columns))
         elif not current.op:
-
             # only follow parents if its not an operator node (which could
             # transform meaning of the get_embedding_sizes
             queue.extend(current.parents)
+
     for column in output:
-        if _is_list_dtype(workflow.output_dtypes[column]):
+        dtype = output_dtypes.get(column)
+        if dtype and _is_list_dtype(dtype):
             # multi hot so remove from output and add to multihot
             multihot_columns.add(column)
     # TODO: returning differnt return types like this (based off the presence


Test patch:
diff --git a/tests/unit/test_ops.py b/tests/unit/test_ops.py
--- a/tests/unit/test_ops.py
+++ b/tests/unit/test_ops.py
@@ -717,11 +717,16 @@ def test_categorify_max_size(max_emb_size):
     # check encoded values after freq_hashing with fix emb size
     assert new_gdf["Author"].max() <= max_emb_size["Author"]
     assert new_gdf["Engaging_User"].max() <= max_emb_size["Engaging_User"]
+
     # check embedding size is less than max_size after hashing with fix emb size.
-    assert nvt.ops.get_embedding_sizes(processor)["Author"][0] <= max_emb_size["Author"]
-    assert (
-        nvt.ops.get_embedding_sizes(processor)["Engaging_User"][0] <= max_emb_size["Engaging_User"]
-    )
+    embedding_sizes = nvt.ops.get_embedding_sizes(processor)
+    assert embedding_sizes["Author"][0] <= max_emb_size["Author"]
+    assert embedding_sizes["Engaging_User"][0] <= max_emb_size["Engaging_User"]
+
+    # make sure we can also get embedding sizes from the column_group
+    embedding_sizes = nvt.ops.get_embedding_sizes(cat_features)
+    assert embedding_sizes["Author"][0] <= max_emb_size["Author"]
+    assert embedding_sizes["Engaging_User"][0] <= max_emb_size["Engaging_User"]
 
 
 @pytest.mark.parametrize("cpu", [True, False])