Repository: mars-project/mars

Issue:
[BUG] Error raised when call `iloc` on empty DataFrame
<!--
Thank you for your contribution!

Please review https://github.com/mars-project/mars/blob/master/CONTRIBUTING.rst before opening an issue.
-->

**Describe the bug**
Error raised when call `iloc` on empty DataFrame.

**To Reproduce**
``` Python
In [1]: import pandas as pd                                                                         

In [2]: a = pd.DataFrame(columns=list('ab'))                                                        

In [3]: import mars.dataframe as md                                                                 

In [4]: md.DataFrame(a).iloc[:2].execute()                                                          
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-4-426db4e594a4> in <module>
----> 1 md.DataFrame(a).iloc[:2].execute()

~/Documents/mars_dev/mars/mars/core.py in execute(self, session, **kw)
    576 
    577     def execute(self, session=None, **kw):
--> 578         self._data.execute(session, **kw)
    579         return self
    580 

~/Documents/mars_dev/mars/mars/core.py in execute(self, session, **kw)
    364 
    365         # no more fetch, thus just fire run
--> 366         session.run(self, **kw)
    367         # return Tileable or ExecutableTuple itself
    368         return self

~/Documents/mars_dev/mars/mars/session.py in run(self, *tileables, **kw)
    478         tileables = tuple(mt.tensor(t) if not isinstance(t, (Entity, Base)) else t
    479                           for t in tileables)
--> 480         result = self._sess.run(*tileables, **kw)
    481 
    482         for t in tileables:

~/Documents/mars_dev/mars/mars/session.py in run(self, *tileables, **kw)
    105             # set number of running cores
    106             self.context.set_ncores(kw['n_parallel'])
--> 107             res = self._executor.execute_tileables(tileables, **kw)
    108             return res
    109 

~/Documents/mars_dev/mars/mars/utils.py in _wrapped(*args, **kwargs)
    406                 _kernel_mode.eager = False
    407             _kernel_mode.eager_count = enter_eager_count + 1
--> 408             return func(*args, **kwargs)
    409         finally:
    410             _kernel_mode.eager_count -= 1

~/Documents/mars_dev/mars/mars/utils.py in inner(*args, **kwargs)
    500     def inner(*args, **kwargs):
    501         with build_mode():
--> 502             return func(*args, **kwargs)
    503     return inner
    504 

~/Documents/mars_dev/mars/mars/executor.py in execute_tileables(self, tileables, fetch, n_parallel, n_thread, print_progress, mock, compose, name)
    862                 # build chunk graph, tile will be done during building
    863                 chunk_graph = chunk_graph_builder.build(
--> 864                     tileables, tileable_graph=tileable_graph)
    865                 tileable_graph = chunk_graph_builder.prev_tileable_graph
    866                 temp_result_keys = set(result_keys)

~/Documents/mars_dev/mars/mars/utils.py in _wrapped(*args, **kwargs)
    406                 _kernel_mode.eager = False
    407             _kernel_mode.eager_count = enter_eager_count + 1
--> 408             return func(*args, **kwargs)
    409         finally:
    410             _kernel_mode.eager_count -= 1

~/Documents/mars_dev/mars/mars/utils.py in inner(*args, **kwargs)
    500     def inner(*args, **kwargs):
    501         with build_mode():
--> 502             return func(*args, **kwargs)
    503     return inner
    504 

~/Documents/mars_dev/mars/mars/tiles.py in build(self, tileables, tileable_graph)
    348 
    349         chunk_graph = super().build(
--> 350             tileables, tileable_graph=tileable_graph)
    351         self._iterative_chunk_graphs.append(chunk_graph)
    352         if len(self._interrupted_ops) == 0:

~/Documents/mars_dev/mars/mars/utils.py in _wrapped(*args, **kwargs)
    406                 _kernel_mode.eager = False
    407             _kernel_mode.eager_count = enter_eager_count + 1
--> 408             return func(*args, **kwargs)
    409         finally:
    410             _kernel_mode.eager_count -= 1

~/Documents/mars_dev/mars/mars/utils.py in inner(*args, **kwargs)
    500     def inner(*args, **kwargs):
    501         with build_mode():
--> 502             return func(*args, **kwargs)
    503     return inner
    504 

~/Documents/mars_dev/mars/mars/tiles.py in build(self, tileables, tileable_graph)
    261                     # for further execution
    262                     partial_tiled_chunks = \
--> 263                         self._on_tile_failure(tileable_data.op, exc_info)
    264                     if partial_tiled_chunks is not None and \
    265                             len(partial_tiled_chunks) > 0:

~/Documents/mars_dev/mars/mars/tiles.py in inner(op, exc_info)
    300                     on_tile_failure(op, exc_info)
    301                 else:
--> 302                     raise exc_info[1].with_traceback(exc_info[2]) from None
    303         return inner
    304 

~/Documents/mars_dev/mars/mars/tiles.py in build(self, tileables, tileable_graph)
    241                 continue
    242             try:
--> 243                 tiled = self._tile(tileable_data, tileable_graph)
    244                 tiled_op.add(tileable_data.op)
    245                 for t, td in zip(tileable_data.op.outputs, tiled):

~/Documents/mars_dev/mars/mars/tiles.py in _tile(self, tileable_data, tileable_graph)
    336         if any(inp.op in self._interrupted_ops for inp in tileable_data.inputs):
    337             raise TilesError('Tile fail due to failure of inputs')
--> 338         return super()._tile(tileable_data, tileable_graph)
    339 
    340     @kernel_mode

~/Documents/mars_dev/mars/mars/tiles.py in _tile(self, tileable_data, tileable_graph)
    199                 t._nsplits = o.nsplits
    200         elif on_tile is None:
--> 201             tds[0]._inplace_tile()
    202         else:
    203             tds = on_tile(tileable_data.op.outputs, tds)

~/Documents/mars_dev/mars/mars/core.py in _inplace_tile(self)
    161 
    162     def _inplace_tile(self):
--> 163         return handler.inplace_tile(self)
    164 
    165     def __getattr__(self, attr):

~/Documents/mars_dev/mars/mars/tiles.py in inplace_tile(self, to_tile)
    134         if not to_tile.is_coarse():
    135             return to_tile
--> 136         dispatched = self.dispatch(to_tile.op)
    137         self._assign_to([d.data for d in dispatched], to_tile.op.outputs)
    138         return to_tile

~/Documents/mars_dev/mars/mars/utils.py in _wrapped(*args, **kwargs)
    406                 _kernel_mode.eager = False
    407             _kernel_mode.eager_count = enter_eager_count + 1
--> 408             return func(*args, **kwargs)
    409         finally:
    410             _kernel_mode.eager_count -= 1

~/Documents/mars_dev/mars/mars/tiles.py in dispatch(self, op)
    117         else:
    118             try:
--> 119                 tiled = op_cls.tile(op)
    120             except NotImplementedError as ex:
    121                 cause = ex

~/Documents/mars_dev/mars/mars/dataframe/indexing/iloc.py in tile(cls, op)
    339 
    340         handler = DataFrameIlocIndexesHandler()
--> 341         return [handler.handle(op)]
    342 
    343     @classmethod

~/Documents/mars_dev/mars/mars/tensor/indexing/index_lib.py in handle(self, op, return_context)
    896 
    897         self._preprocess(context, index_infos)
--> 898         self._process(context, index_infos)
    899         self._postprocess(context, index_infos)
    900 

~/Documents/mars_dev/mars/mars/tensor/indexing/index_lib.py in _process(cls, context, index_infos)
    923         index_to_shape = OrderedDict(sorted([(c.index, c.shape) for c in out_chunks],
    924                                             key=itemgetter(0)))
--> 925         context.out_nsplits = calc_nsplits(index_to_shape)
    926 
    927     @classmethod

~/Documents/mars_dev/mars/mars/utils.py in calc_nsplits(chunk_idx_to_shape)
    664     :return: nsplits
    665     """
--> 666     ndim = len(next(iter(chunk_idx_to_shape)))
    667     tileable_nsplits = []
    668     # for each dimension, record chunk shape whose index is zero on other dimensions

StopIteration: 

```


Discussion:

Resolving patch:
diff --git a/mars/dataframe/utils.py b/mars/dataframe/utils.py
--- a/mars/dataframe/utils.py
+++ b/mars/dataframe/utils.py
@@ -137,7 +137,7 @@ def decide_dataframe_chunk_sizes(shape, chunk_size, memory_usage):
         row_left_size = shape[0]
     else:
         row_chunk_size = normalize_chunk_sizes((shape[0],), (chunk_size[0],))[0]
-        row_left_size = 0
+        row_left_size = -1
     # for the column side, along axis 1
     if 1 not in chunk_size:
         col_chunk_size = []
@@ -148,11 +148,18 @@ def decide_dataframe_chunk_sizes(shape, chunk_size, memory_usage):
         acc = [0] + np.cumsum(col_chunk_size).tolist()
         col_chunk_store = [average_memory_usage[acc[i]: acc[i + 1]].sum()
                            for i in range(len(col_chunk_size))]
-        col_left_size = 0
+        col_left_size = -1
 
     while True:
         nbytes_occupied = np.prod([max(it) for it in (row_chunk_size, col_chunk_store) if it])
         dim_size = np.maximum(int(np.power(max_chunk_size / nbytes_occupied, 1 / float(nleft))), 1)
+
+        if col_left_size == 0:
+            col_chunk_size.append(0)
+
+        if row_left_size == 0:
+            row_chunk_size.append(0)
+
         # check col first
         if col_left_size > 0:
             cs = min(col_left_size, dim_size)
@@ -166,7 +173,7 @@ def decide_dataframe_chunk_sizes(shape, chunk_size, memory_usage):
             row_chunk_size.append(cs)
             row_left_size -= cs
 
-        if col_left_size == 0 and row_left_size == 0:
+        if col_left_size <= 0 and row_left_size <= 0:
             break
 
     return tuple(row_chunk_size), tuple(col_chunk_size)
@@ -176,7 +183,7 @@ def decide_series_chunk_size(shape, chunk_size, memory_usage):
     from ..config import options
 
     chunk_size = dictify_chunk_size(shape, chunk_size)
-    average_memory_usage = memory_usage / shape[0]
+    average_memory_usage = memory_usage / shape[0] if shape[0] != 0 else memory_usage
 
     if len(chunk_size) == len(shape):
         return normalize_chunk_sizes(shape, chunk_size[0])
diff --git a/mars/tensor/utils.py b/mars/tensor/utils.py
--- a/mars/tensor/utils.py
+++ b/mars/tensor/utils.py
@@ -69,8 +69,11 @@ def normalize_chunk_sizes(shape, chunk_size):
         else:
             assert isinstance(chunk, int)
 
-            sizes = tuple(chunk for _ in range(int(size / chunk))) + \
-                (tuple() if size % chunk == 0 else (size % chunk,))
+            if size == 0:
+                sizes = (0,)
+            else:
+                sizes = tuple(chunk for _ in range(int(size / chunk))) + \
+                    (tuple() if size % chunk == 0 else (size % chunk,))
             chunk_sizes.append(sizes)
 
     return tuple(chunk_sizes)


Test patch:
diff --git a/mars/dataframe/datasource/tests/test_datasource_execution.py b/mars/dataframe/datasource/tests/test_datasource_execution.py
--- a/mars/dataframe/datasource/tests/test_datasource_execution.py
+++ b/mars/dataframe/datasource/tests/test_datasource_execution.py
@@ -41,6 +41,19 @@ def setUp(self):
         self.ctx, self.executor = self._create_test_context()
 
     def testFromPandasDataFrameExecution(self):
+        # test empty DataFrame
+        pdf = pd.DataFrame()
+        df = from_pandas_df(pdf)
+
+        result = self.executor.execute_dataframe(df, concat=True)[0]
+        pd.testing.assert_frame_equal(pdf, result)
+
+        pdf = pd.DataFrame(columns=list('ab'))
+        df = from_pandas_df(pdf)
+
+        result = self.executor.execute_dataframe(df, concat=True)[0]
+        pd.testing.assert_frame_equal(pdf, result)
+
         pdf = pd.DataFrame(np.random.rand(20, 30), index=[np.arange(20), np.arange(20, 0, -1)])
         df = from_pandas_df(pdf, chunk_size=(13, 21))
 
@@ -48,6 +61,13 @@ def testFromPandasDataFrameExecution(self):
         pd.testing.assert_frame_equal(pdf, result)
 
     def testFromPandasSeriesExecution(self):
+        # test empty Series
+        ps = pd.Series(name='a')
+        series = from_pandas_series(ps, chunk_size=13)
+
+        result = self.executor.execute_dataframe(series, concat=True)[0]
+        pd.testing.assert_series_equal(ps, result)
+
         ps = pd.Series(np.random.rand(20), index=[np.arange(20), np.arange(20, 0, -1)], name='a')
         series = from_pandas_series(ps, chunk_size=13)