Repository: NVIDIA-Merlin/NVTabular

Issue:
[BUG] Categorify generates Null counts for a df without Nulls
**Describe the bug**
I get an unexpected behaviour from Categorify() op. The unique value stats are not correctly calculated. It includes `nulls` and corresponding size although dataset does not have any nulls. Please see the repro example below for the details.

**Steps/Code to reproduce bug**
Run the code below to repro the issue:

```
import nvtabular as nvt
from nvtabular import Workflow, ops

gdf = nvt.dispatch._make_df(
    {
        "user_id": [1, 2, 3, 4, 6, 8, 5, 3] *10,
        "item_id": [2, 4, 4, 7, 5, 2, 5, 2] *10,
    }
)

cats = ["user_id", "item_id"]
cat_features = cats >> ops.Categorify()

workflow = Workflow(cat_features)
workflow.fit_transform(nvt.Dataset(gdf)).to_ddf().compute()
```
THEN
```
import pandas as pd
pd.read_parquet('./categories/unique.user_id.parquet')

	user_id	user_id_size
0	<NA>	10
1	3	20
2	1	10
3	2	10
4	4	10
5	5	10
6	6	10
7	8	10
```

**Expected behavior**
There should not be null in the `unique.user_id.parquet`  file.

**Environment details (please complete the following information):**
 - Environment location: [Bare-metal, Docker, Cloud(specify cloud provider)]
 - Method of NVTabular install: [conda, Docker, or from source]: Docker, `merlin-tensorflow-training:21.12` with the latest main branch pulled.
   - If method of install is [Docker], provide `docker pull` & `docker run` commands used
 

**Additional context**
Add any other context about the problem here.



Discussion:
@rjzamora The (known) impact of this is relatively low, but it does look weird and seems like it could cause problems. After talking to @rnyak about it, we're not 100% sure what the correct behavior is. Should the counts include NA with size 0, or omit it entirely when there are no NAs in the dataset?
>The (known) impact of this is relatively low

Seems pretty critical to me :) - I'll see if I can figure out what is going on here. Thank you for the nice/simple reproducer @rnyak !

Resolving patch:
diff --git a/nvtabular/ops/categorify.py b/nvtabular/ops/categorify.py
--- a/nvtabular/ops/categorify.py
+++ b/nvtabular/ops/categorify.py
@@ -1052,7 +1052,9 @@ def _write_uniques(dfs, base_path, col_selector: ColumnSelector, options: FitOpt
         for col in col_selector.names:
             name_size = col + "_size"
             null_size = 0
-            if name_size in df:
+            # Set null size if first element in `col` is
+            # null, and the `size` aggregation is known
+            if name_size in df and df[col].iloc[:1].isnull().any():
                 null_size = df[name_size].iloc[0]
             if options.max_size:
                 max_emb_size = options.max_size


Test patch:
diff --git a/tests/unit/ops/test_categorify.py b/tests/unit/ops/test_categorify.py
--- a/tests/unit/ops/test_categorify.py
+++ b/tests/unit/ops/test_categorify.py
@@ -559,3 +559,19 @@ def test_categorify_embedding_sizes(dataset, engine):
     workflow.fit_transform(dataset)
 
     assert get_embedding_sizes(workflow) == {"name-cat": (27, 16), "name-string_test": (27, 16)}
+
+
+def test_categorify_no_nulls():
+    # See https://github.com/NVIDIA-Merlin/NVTabular/issues/1325
+    df = nvt.dispatch._make_df(
+        {
+            "user_id": [1, 2, 3, 4, 6, 8, 5, 3] * 10,
+            "item_id": [2, 4, 4, 7, 5, 2, 5, 2] * 10,
+        },
+    )
+    workflow = nvt.Workflow(["user_id", "item_id"] >> ops.Categorify())
+    workflow.fit(nvt.Dataset(df))
+
+    df = pd.read_parquet("./categories/unique.user_id.parquet")
+    assert df["user_id"].iloc[:1].isnull().any()
+    assert df["user_id_size"][0] == 0