Repository: NVIDIA-Merlin/NVTabular

Issue:
[FEA] apply joint TE on multiple columns after another op
**Is your feature request related to a problem? Please describe.**
We'd like to the the following pipeline with LambdaOp and Target Encoding. 

```
unixReviewTime = ColumnGroup(['unixReviewTime'])
unixReviewTime_toDatetime = unixReviewTime >> (lambda col: cudf.to_datetime(col, unit='s')) >> Rename(postfix="_toDatetime")
unixReviewTime_toDatetime_year = unixReviewTime_toDatetime >> (lambda col: col.dt.year) >> Rename(postfix="_year")

columns_1 = [[['userID'] + unixReviewTime_toDatetime_year]]
te_features = columns_1 >> nvt.ops.TargetEncoding("label", kfold=1, p_smooth=20)
```
However this does not work. 

We also tried `columns_1 = ColumnGroup(['user_id']) +  unixReviewTime_toDatetime_year` but this is not providing us with the joint multi-column TE as we want.

**Describe the solution you'd like**
We want to be able to do  joint multi-column TE on `userID` (from the input dataset) and `unixReviewTime_toDatetime_year` that is calculated from a lambda op. So our final output column should be `TE_userID_unixReviewTime_toDatetime_year_label`.

**Additional context**
We are using new api and conda environment with the latest NVT main branch.



Discussion:

Resolving patch:
diff --git a/nvtabular/column_group.py b/nvtabular/column_group.py
--- a/nvtabular/column_group.py
+++ b/nvtabular/column_group.py
@@ -35,16 +35,36 @@ class ColumnGroup:
     """
 
     def __init__(self, columns):
-        if isinstance(columns, str):
-            self.columns = [columns]
-        else:
-            self.columns = [_convert_col(col) for col in columns]
         self.parents = []
         self.children = []
         self.op = None
         self.kind = None
         self.dependencies = None
 
+        if isinstance(columns, str):
+            columns = [columns]
+
+        # if any of the values we're passed are a columngroup
+        # we have to ourselves as a childnode in the graph.
+        if any(isinstance(col, ColumnGroup) for col in columns):
+            self.columns = []
+            self.kind = "[...]"
+            for col in columns:
+                if not isinstance(col, ColumnGroup):
+                    col = ColumnGroup(col)
+                else:
+                    # we can't handle nesting arbitrarily deep here
+                    # only accept non-nested (str) columns here
+                    if any(not isinstance(c, str) for c in col.columns):
+                        raise ValueError("Can't handle more than 1 level of nested columns")
+
+                col.children.append(self)
+                self.parents.append(col)
+                self.columns.append(tuple(col.columns))
+
+        else:
+            self.columns = [_convert_col(col) for col in columns]
+
     def __rshift__(self, operator):
         """Transforms this ColumnGroup by applying an Operator
 


Test patch:
diff --git a/tests/unit/test_column_group.py b/tests/unit/test_column_group.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/test_column_group.py
@@ -0,0 +1,43 @@
+import cudf
+import pytest
+
+from nvtabular import ColumnGroup, Dataset, Workflow
+from nvtabular.ops import Categorify, Rename
+
+
+def test_nested_column_group(tmpdir):
+    df = cudf.DataFrame(
+        {
+            "geo": ["US>CA", "US>NY", "CA>BC", "CA>ON"],
+            "user": ["User_A", "User_A", "User_A", "User_B"],
+        }
+    )
+
+    country = (
+        ColumnGroup(["geo"]) >> (lambda col: col.str.slice(0, 2)) >> Rename(postfix="_country")
+    )
+
+    # make sure we can do a 'combo' categorify (cross based) of country+user
+    # as well as categorifying the country and user columns on their own
+    cats = [country + "user"] + country + "user" >> Categorify(encode_type="combo")
+
+    workflow = Workflow(cats)
+    df_out = workflow.fit_transform(Dataset(df)).to_ddf().compute(scheduler="synchronous")
+
+    geo_country = df_out["geo_country"]
+    assert geo_country[0] == geo_country[1]  # rows 0,1 are both 'US'
+    assert geo_country[2] == geo_country[3]  # rows 2,3 are both 'CA'
+
+    user = df_out["user"]
+    assert user[0] == user[1] == user[2]
+    assert user[3] != user[2]
+
+    geo_country_user = df_out["geo_country_user"]
+    assert geo_country_user[0] == geo_country_user[1]  # US / userA
+    assert geo_country_user[2] != geo_country_user[0]  # same user but in canada
+
+    # make sure we get an exception if we nest too deeply (can't handle arbitrarily deep
+    # nested column groups - and the exceptions we would get in operators like Categorify
+    # are super confusing for users)
+    with pytest.raises(ValueError):
+        cats = [[country + "user"] + country + "user"] >> Categorify(encode_type="combo")