Repository: NVIDIA-Merlin/NVTabular

Issue:
[FEA] LogOp for list features
**Is your feature request related to a problem? Please describe.**
As a user, I want to be able to use the LogOp on a columns with list values.

**Describe the solution you'd like**
Example dataset:

```
df = cudf.DataFrame({'userid': ['a', 'b', 'c'], 'col': [[10,100,1000], [10,5], [50,10]]})

log_col = ['col'] >> nvt.ops.LogOp()
```

Log Operation should be applied to every value in the list.



Discussion:

Resolving patch:
diff --git a/nvtabular/dispatch.py b/nvtabular/dispatch.py
--- a/nvtabular/dispatch.py
+++ b/nvtabular/dispatch.py
@@ -242,14 +242,6 @@ def _hash_series(s):
             return s.hash_values()
 
 
-def _natural_log(df):
-    """Natural logarithm of all columns in a DataFrame"""
-    if isinstance(df, pd.DataFrame):
-        return pd.DataFrame(np.log(df.values), columns=df.columns, index=df.index)
-    else:
-        return df.log()
-
-
 def _series_has_nulls(s):
     """Check if Series contains any null values"""
     if isinstance(s, pd.Series):
@@ -285,12 +277,21 @@ def _is_string_dtype(obj):
         return is_string_dtype(obj)
 
 
+def _flatten_list_column_values(s):
+    """returns a flattened list from a list column"""
+    if isinstance(s, pd.Series) or not cudf:
+        return pd.Series(itertools.chain(*s))
+    else:
+        return s.list.leaves
+
+
 def _flatten_list_column(s):
-    """Flatten elements of a list-based column"""
+    """Flatten elements of a list-based column, and return as a DataFrame"""
+    values = _flatten_list_column_values(s)
     if isinstance(s, pd.Series) or not cudf:
-        return pd.DataFrame({s.name: itertools.chain(*s)})
+        return pd.DataFrame({s.name: values})
     else:
-        return cudf.DataFrame({s.name: s.list.leaves})
+        return cudf.DataFrame({s.name: values})
 
 
 def _concat_columns(args: list):
diff --git a/nvtabular/ops/logop.py b/nvtabular/ops/logop.py
--- a/nvtabular/ops/logop.py
+++ b/nvtabular/ops/logop.py
@@ -15,7 +15,13 @@
 #
 import numpy as np
 
-from nvtabular.dispatch import DataFrameType, _natural_log, annotate
+from nvtabular.dispatch import (
+    DataFrameType,
+    _encode_list_column,
+    _flatten_list_column_values,
+    _is_list_dtype,
+    annotate,
+)
 
 from ..tags import Tags
 from .operator import ColumnSelector, Operator
@@ -36,7 +42,14 @@ class LogOp(Operator):
 
     @annotate("LogOp_op", color="darkgreen", domain="nvt_python")
     def transform(self, col_selector: ColumnSelector, df: DataFrameType) -> DataFrameType:
-        return _natural_log(df[col_selector.names].astype(np.float32) + 1)
+        for name in col_selector.names:
+            column = df[name]
+            if _is_list_dtype(column):
+                transformed = np.log(_flatten_list_column_values(column).astype(np.float32) + 1)
+                df[name] = _encode_list_column(column, transformed)
+            else:
+                df[name] = np.log(column.astype(np.float32) + 1)
+        return df
 
     def output_tags(self):
         return [Tags.CONTINUOUS]


Test patch:
diff --git a/tests/unit/ops/test_ops.py b/tests/unit/ops/test_ops.py
--- a/tests/unit/ops/test_ops.py
+++ b/tests/unit/ops/test_ops.py
@@ -51,6 +51,21 @@ def test_log(tmpdir, df, dataset, gpu_memory_frac, engine, op_columns, cpu):
         assert_eq(values, np.log(original.astype(np.float32) + 1))
 
 
+@pytest.mark.parametrize("cpu", _CPU)
+def test_logop_lists(tmpdir, cpu):
+    df = dispatch._make_df(device="cpu" if cpu else "gpu")
+    df["vals"] = [[np.exp(0) - 1, np.exp(1) - 1], [np.exp(2) - 1], []]
+
+    features = ["vals"] >> nvt.ops.LogOp()
+    workflow = nvt.Workflow(features)
+    new_df = workflow.fit_transform(nvt.Dataset(df)).to_ddf().compute()
+
+    expected = dispatch._make_df(device="cpu" if cpu else "gpu")
+    expected["vals"] = [[0.0, 1.0], [2.0], []]
+
+    assert_eq(expected, new_df)
+
+
 def test_valuecount(tmpdir):
     df = dispatch._make_df(
         {