Repository: dask/dask

Issue:
Possible bug when checking an empty dataframe
I am aware of [this question][1]. But check the code(minimal-working example) below:
```
import dask.dataframe as dd
import pandas as pd

# intialise data of lists.
data = {'Name': ['Tom', 'nick', 'krish', 'jack'], 'Age': [20, 21, 19, 18]}

# Create DataFrame
df = pd.DataFrame(data)
dask_df = dd.from_pandas(df, npartitions=1)

categoric_df = dask_df.select_dtypes(include="category")
```
When I try to print the `categoric_df` I get the following error:
```
ValueError: No objects to concatenate
```
And when I check the `categoric_df` from PyCharm debugger:

    Unable to get repr for <class 'dask.dataframe.core.DataFrame'>
With these errors, I can build a try/except block to check if the dataframe is empty or not. But I don't want to use this approach since it is not guaranteed to work all the time and try/except slows down the code.
And when I try to print computed `categoric_df` it looks like this:
```
>>>print(categoric_df.compute())
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3]
```
In summary: Here if I select the non-existing dypes and create a dask.DataFrame from it, I get a `dask.DataFrame` which at first glance doesn't seem empty if I use `len()` function.
```
>>>print(len(categoric_df))
4
>>>print(len(categoric_df.compute())
4
>>>print(categoric_df.compute().empty)
True
```
 **Is this a bug?**  Is there a way to check if the `categoric_df` is empty or not without computing it? (I want it to stay lazily evaluated.)

> **UPDATE**: `print(len(categoric_df.columns))` is returning `0`. This can be used for figuring out if the dataframe is empty or not. But is this viable? I am not sure.

  [1]: https://stackoverflow.com/questions/50206730/how-to-check-if-dask-dataframe-is-empty





Discussion:
I think the bug is just in `DataFrame.__repr__`, it's not related to categorical / select_dtypes. This shouldn't raise

```pytb
In [21]: empty = dd.from_pandas(pd.DataFrame(), 2)

In [22]: empty
Out[22]: ---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/Envs/pandas-dev/lib/python3.7/site-packages/IPython/core/formatters.py in __call__(self, obj)
    700                 type_pprinters=self.type_printers,
    701                 deferred_pprinters=self.deferred_printers)
--> 702             printer.pretty(obj)
    703             printer.flush()
    704             return stream.getvalue()

~/Envs/pandas-dev/lib/python3.7/site-packages/IPython/lib/pretty.py in pretty(self, obj)
    400                         if cls is not object \
    401                                 and callable(cls.__dict__.get('__repr__')):
--> 402                             return _repr_pprint(obj, self, cycle)
    403
    404             return _default_pprint(obj, self, cycle)

~/Envs/pandas-dev/lib/python3.7/site-packages/IPython/lib/pretty.py in _repr_pprint(obj, p, cycle)
    695     """A pprint that just redirects to the normal repr function."""
    696     # Find newlines and replace them with p.break_()
--> 697     output = repr(obj)
    698     for idx,output_line in enumerate(output.splitlines()):
    699         if idx:

~/Envs/pandas-dev/lib/python3.7/site-packages/dask/dataframe/core.py in __repr__(self)
    413
    414     def __repr__(self):
--> 415         data = self._repr_data().to_string(max_rows=5, show_dimensions=False)
    416         return """Dask {klass} Structure:
    417 {data}

~/Envs/pandas-dev/lib/python3.7/site-packages/dask/dataframe/core.py in _repr_data(self)
   4175         index = self._repr_divisions
   4176         series_list = [_repr_data_series(s, index=index) for _, s in meta.iteritems()]
-> 4177         return pd.concat(series_list, axis=1)
   4178
   4179     _HTML_FMT = """<div><strong>Dask DataFrame Structure:</strong></div>

~/sandbox/pandas/pandas/core/reshape/concat.py in concat(objs, axis, join, ignore_index, keys, levels, names, verify_integrity, sort, copy)
    241         verify_integrity=verify_integrity,
    242         copy=copy,
--> 243         sort=sort,
    244     )
    245

~/sandbox/pandas/pandas/core/reshape/concat.py in __init__(self, objs, axis, join, keys, levels, names, ignore_index, verify_integrity, copy, sort)
    289
    290         if len(objs) == 0:
--> 291             raise ValueError("No objects to concatenate")
    292
    293         if keys is None:

ValueError: No objects to concatenate

```

> Is there a way to check if the categoric_df is empty or not without computing it? (I want it to stay lazily evaluated.)

You can check if the number of columns is 0

```python
if len(df.columns):
    # nonempty...
```

You can't lazily check if there are no rows.
To fix the bug, we can check at https://github.com/dask/dask/blob/c69d10f97cadd06f147817f0084bddeadb8195e6/dask/dataframe/core.py#L4286-L4289 if `len(meta.columns)` is 0 and return a special repr for the empty dataframe. We should probably still have the information about divisions / partitioning.
> > Is there a way to check if the categoric_df is empty or not without computing it? (I want it to stay lazily evaluated.)
> 
> You can check if the number of columns is 0
> 
> ```python
> if len(df.columns):
>     # nonempty...
> ```
> 
> You can't lazily check if there are no rows.

Ah... I forgot about this. Thank you, kind sir.

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -413,9 +413,13 @@ def _repr_divisions(self):
 
     def __repr__(self):
         data = self._repr_data().to_string(max_rows=5, show_dimensions=False)
-        return """Dask {klass} Structure:
+        _str_fmt = """Dask {klass} Structure:
 {data}
-Dask Name: {name}, {task} tasks""".format(
+Dask Name: {name}, {task} tasks"""
+        if len(self.columns) == 0:
+            data = data.partition("\n")[-1].replace("Index", "Divisions")
+            _str_fmt = "Empty {}".format(_str_fmt)
+        return _str_fmt.format(
             klass=self.__class__.__name__,
             data=data,
             name=key_split(self._name),
@@ -4297,8 +4301,14 @@ def to_html(self, max_rows=5):
     def _repr_data(self):
         meta = self._meta
         index = self._repr_divisions
-        series_list = [_repr_data_series(s, index=index) for _, s in meta.iteritems()]
-        return pd.concat(series_list, axis=1)
+        cols = meta.columns
+        if len(cols) == 0:
+            series_df = pd.DataFrame([[]] * len(index), columns=cols, index=index)
+        else:
+            series_df = pd.concat(
+                [_repr_data_series(s, index=index) for _, s in meta.iteritems()], axis=1
+            )
+        return series_df
 
     _HTML_FMT = """<div><strong>Dask DataFrame Structure:</strong></div>
 {data}


Test patch:
diff --git a/dask/dataframe/tests/test_format.py b/dask/dataframe/tests/test_format.py
--- a/dask/dataframe/tests/test_format.py
+++ b/dask/dataframe/tests/test_format.py
@@ -527,3 +527,41 @@ def test_duplicate_columns_repr():
     arr = da.from_array(np.arange(10).reshape(5, 2), chunks=(5, 2))
     frame = dd.from_dask_array(arr, columns=["a", "a"])
     repr(frame)
+
+
+def test_empty_repr():
+    df = pd.DataFrame()
+    ddf = dd.from_pandas(df, npartitions=1)
+    exp = (
+        "Empty Dask DataFrame Structure:\n"
+        "Columns: []\n"
+        "Divisions: [, ]\n"
+        "Dask Name: from_pandas, 1 tasks"
+    )
+    assert repr(ddf) == exp
+    exp_table = """<table border="1" class="dataframe">
+  <thead>
+    <tr style="text-align: right;">
+      <th></th>
+    </tr>
+    <tr>
+      <th>npartitions=1</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th></th>
+    </tr>
+    <tr>
+      <th></th>
+    </tr>
+  </tbody>
+</table>"""
+    exp = """<div><strong>Dask DataFrame Structure:</strong></div>
+<div>
+{style}{exp_table}
+</div>
+<div>Dask Name: from_pandas, 1 tasks</div>""".format(
+        style=style, exp_table=exp_table
+    )
+    assert ddf._repr_html_() == exp