Repository: NVIDIA-Merlin/NVTabular

Issue:
[BUG] proc.ds_to_tensors does not support empy category/numeric columns
**Describe the bug**
proc.ds_to_tensors expect that categorical/numerical columns are defined.
I think there are datasets, 
* which does not have a categorical column - e.g. all columns are numeric.
* which does not have a numeric column - e.g. all columns are categorical

**Steps/Code to reproduce bug**
```
dataset = nvt.Dataset('./train.csv')
proc = nvt.Workflow(
        cat_names=['ROLE_ROLLUP_1'],
        cont_names=[],
        label_name=['ACTION']
)
proc.finalize()
proc.ds_to_tensors(dataset.to_iter())
```

Throws following error:
```
/opt/conda/envs/rapids/lib/python3.6/site-packages/nvtabular/torch_dataloader.py in _get_final_cols(preproc)
    154         preproc.create_final_cols()
    155     cat_names = _get_embedding_order(preproc.columns_ctx["final"]["cols"]["categorical"])
--> 156     cont_names = sorted(preproc.columns_ctx["final"]["cols"]["continuous"])
    157     label_name = sorted(preproc.columns_ctx["final"]["cols"]["label"])
    158     return cat_names, cont_names, label_name

TypeError: 'NoneType' object is not iterable
```

If I run, then it works.
```
dataset = nvt.Dataset('./train.csv')
proc = nvt.Workflow(
        cat_names=['ROLE_ROLLUP_1'],
        cont_names=['ROLE_ROLLUP_1'],
        label_name=['ACTION']
)
proc.finalize()
proc.ds_to_tensors(dataset.to_iter())
```

I know, that the error is not related to dataset.to_iter() - this works - it is related to proc.ds_to_tensors

**Expected behavior**
I think the expected behavior should be that it supports empty lists for categorical and numerical features




Discussion:

Resolving patch:
diff --git a/nvtabular/torch_dataloader.py b/nvtabular/torch_dataloader.py
--- a/nvtabular/torch_dataloader.py
+++ b/nvtabular/torch_dataloader.py
@@ -29,7 +29,6 @@
 class TensorItr:
     """
         Tensor dataset, for data already in tensor format.
-        (see preproc::ds_to_tensor)
 
         Parameters
         -----------
@@ -44,7 +43,7 @@ def __init__(self, tensors, batch_size=1, pin_memory=False, shuffle=False):
         self.tensors = tensors
         self.batch_size = batch_size
 
-        self.num_samples = self.tensors[0].size(0)
+        self.num_samples = self.tensors[2].size(0)
         if shuffle:
             self.shuffle()
 
@@ -60,9 +59,10 @@ def __len__(self):
 
     def __iter__(self):
         for idx in range(0, self.num_samples, self.batch_size):
-            tens = [tensor[idx : idx + self.batch_size] for tensor in self.tensors]
-            yield tens[0], tens[1], tens[2]
-            del tens
+            yield [
+                tensor[idx : idx + self.batch_size] if tensor is not None else None
+                for tensor in self.tensors
+            ]
 
     def shuffle(self):
         idx = torch.randperm(self.num_samples, dtype=torch.int64)
@@ -349,9 +349,11 @@ def gather_indices(self):
             return self.indices[start : start + per_worker]
 
     def _to_tensor(self, gdf, dtype=None):
+        if gdf.empty:
+            return
         dl_pack = self.to_dlpack(gdf)
         tens = from_dlpack(dl_pack).type(dtype)
-        return tens, gdf.columns, dtype
+        return tens
 
     def create_tensors(self, gdf, cat_names=None, cont_names=None, label_names=None):
         gdf_cats, gdf_conts, gdf_label = (
@@ -360,14 +362,11 @@ def create_tensors(self, gdf, cat_names=None, cont_names=None, label_names=None)
             gdf[label_names],
         )
         del gdf
-        if len(gdf_cats) > 0:
-            cats = self._to_tensor(gdf_cats, torch.long)
-        if len(gdf_conts) > 0:
-            conts = self._to_tensor(gdf_conts, torch.float32)
-        if len(gdf_label) > 0:
-            label = self._to_tensor(gdf_label, torch.float32)
+        cats = self._to_tensor(gdf_cats, torch.long)
+        conts = self._to_tensor(gdf_conts, torch.float32)
+        label = self._to_tensor(gdf_label, torch.float32)
         del gdf_cats, gdf_conts, gdf_label
-        return [cats[0], conts[0], label[0]]
+        return [cats, conts, label]
 
 
 class DLDataLoader(torch.utils.data.DataLoader):
diff --git a/nvtabular/workflow.py b/nvtabular/workflow.py
--- a/nvtabular/workflow.py
+++ b/nvtabular/workflow.py
@@ -527,11 +527,6 @@ def _set_stats(self, stats_dict):
     def clear_stats(self):
         self.stats = {}
 
-    def ds_to_tensors(self, itr, apply_ops=True):
-        from nvtabular.torch_dataloader import create_tensors
-
-        return create_tensors(self, itr=itr, apply_ops=apply_ops)
-
 
 def get_new_config():
     """


Test patch:
diff --git a/tests/unit/test_torch_dataloader.py b/tests/unit/test_torch_dataloader.py
--- a/tests/unit/test_torch_dataloader.py
+++ b/tests/unit/test_torch_dataloader.py
@@ -41,6 +41,23 @@ def test_gpu_file_iterator_ds(df, dataset, batch, engine):
     assert_eq(df_itr.reset_index(drop=True), df.reset_index(drop=True))
 
 
+@pytest.mark.parametrize("engine", ["parquet"])
+def test_empty_cols(tmpdir, df, dataset, engine):
+    # test out https://github.com/NVIDIA/NVTabular/issues/149 making sure we can iterate over
+    # empty cats/conts
+    # first with no continuous columns
+    no_conts = torch_dataloader.AsyncTensorBatchDatasetItr(
+        dataset, cats=["id"], conts=[], labels=["label"], batch_size=1
+    )
+    assert all(conts is None for _, conts, _ in no_conts)
+
+    # and with no categorical columns
+    no_cats = torch_dataloader.AsyncTensorBatchDatasetItr(
+        dataset, cats=[], conts=["x"], labels=["label"]
+    )
+    assert all(cats is None for cats, _, _ in no_cats)
+
+
 @pytest.mark.parametrize("part_mem_fraction", [0.000001, 0.1])
 @pytest.mark.parametrize("batch_size", [1, 10, 100])
 @pytest.mark.parametrize("engine", ["parquet"])