Repository: mars-project/mars

Issue:
[BUG] Histogram met ValueError when some chunk is empty
When executing `mt.histogram` and some chunk in a tensor is empty, histogram produces an ValueError

Code example: 

```python
import mars.tensor as mt
val = mt.arange(100, chunk_size=10)
vt = val[val < 20]
mt.histogram(vt).execute()
```

Error stack:
```
Traceback (most recent call last):
  File "miniconda3/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3417, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-26-c79b5d2b7d19>", line 1, in <module>
    histogram(vt).execute()
  File "Code/mars/mars/core.py", line 764, in execute
    return super().execute(session=session, **kw)
  File "Code/mars/mars/core.py", line 379, in execute
    return run()
  File "Code/mars/mars/core.py", line 374, in run
    session.run(self, **kw)
  File "Code/mars/mars/session.py", line 506, in run
    result = self._sess.run(*tileables, **kw)
  File "Code/mars/mars/session.py", line 111, in run
    res = self._executor.execute_tileables(tileables, **kw)
  File "Code/mars/mars/utils.py", line 459, in _inner
    return func(*args, **kwargs)
  File "Code/mars/mars/executor.py", line 883, in execute_tileables
    self.execute_graph(chunk_graph, list(temp_result_keys),
  File "Code/mars/mars/executor.py", line 698, in execute_graph
    res = graph_execution.execute(retval)
  File "Code/mars/mars/executor.py", line 579, in execute
    future.result()
  File "miniconda3/lib/python3.8/concurrent/futures/_base.py", line 432, in result
    return self.__get_result()
  File "miniconda3/lib/python3.8/concurrent/futures/_base.py", line 388, in __get_result
    raise self._exception
  File "miniconda3/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "Code/mars/mars/utils.py", line 459, in _inner
    return func(*args, **kwargs)
  File "Code/mars/mars/executor.py", line 446, in _execute_operand
    self.handle_op(first_op, results, self._mock)
  File "Code/mars/mars/executor.py", line 378, in handle_op
    return Executor.handle(*args, **kw)
  File "Code/mars/mars/executor.py", line 649, in handle
    return runner(results, op)
  File "Code/mars/mars/tensor/reduction/core.py", line 291, in execute
    return cls.execute_agg(ctx, op)
  File "Code/mars/mars/tensor/reduction/core.py", line 272, in execute_agg
    ret = reduce_func(input_chunk, axis=axis,
  File "<__array_function__ internals>", line 5, in amin
  File "miniconda3/lib/python3.8/site-packages/numpy/core/fromnumeric.py", line 2830, in amin
    return _wrapreduction(a, np.minimum, 'min', axis, None, out,
  File "miniconda3/lib/python3.8/site-packages/numpy/core/fromnumeric.py", line 87, in _wrapreduction
    return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
ValueError: zero-size array to reduction operation minimum which has no identity
```


Discussion:

Resolving patch:
diff --git a/mars/lib/sparse/matrix.py b/mars/lib/sparse/matrix.py
--- a/mars/lib/sparse/matrix.py
+++ b/mars/lib/sparse/matrix.py
@@ -124,6 +124,10 @@ def __init__(self, spmatrix, shape=()):
     def shape(self):
         return self.spmatrix.shape
 
+    @property
+    def size(self):
+        return int(np.prod(self.shape))
+
     def transpose(self, axes=None):
         assert axes is None or tuple(axes) == (1, 0)
         return SparseMatrix(self.spmatrix.transpose())
diff --git a/mars/tensor/reduction/core.py b/mars/tensor/reduction/core.py
--- a/mars/tensor/reduction/core.py
+++ b/mars/tensor/reduction/core.py
@@ -264,7 +264,10 @@ def execute_agg(cls, ctx, op):
         reduce_func = getattr(xp, func_name)
         out = op.outputs[0]
         with device(device_id):
-            if "dtype" in inspect.getfullargspec(reduce_func).args:
+            if input_chunk.size == 0 and op.keepdims:
+                # input chunk is empty, when keepdims is True, return itself
+                ret = input_chunk
+            elif "dtype" in inspect.getfullargspec(reduce_func).args:
                 ret = reduce_func(input_chunk, axis=axis,
                                   dtype=op.dtype,
                                   keepdims=bool(op.keepdims))


Test patch:
diff --git a/mars/tensor/reduction/tests/test_reduction_execute.py b/mars/tensor/reduction/tests/test_reduction_execute.py
--- a/mars/tensor/reduction/tests/test_reduction_execute.py
+++ b/mars/tensor/reduction/tests/test_reduction_execute.py
@@ -106,6 +106,13 @@ def testMaxMinExecution(self):
         a = tensor(list('abcdefghi'), dtype=object, chunk_size=2)
         self.assertEqual(self.executor.execute_tensor(a.max(), concat=True)[0], 'i')
 
+        # test empty chunks
+        raw = np.arange(3, 10)
+        arr = tensor(np.arange(0, 10), chunk_size=3)
+        arr = arr[arr >= 3]
+        self.assertEqual([raw.max()], self.executor.execute_tensor(arr.max()))
+        self.assertEqual([raw.min()], self.executor.execute_tensor(arr.min()))
+
     def testAllAnyExecution(self):
         raw1 = np.zeros((10, 15))
         raw2 = np.ones((10, 15))
diff --git a/mars/tensor/statistics/tests/test_statistics_execute.py b/mars/tensor/statistics/tests/test_statistics_execute.py
--- a/mars/tensor/statistics/tests/test_statistics_execute.py
+++ b/mars/tensor/statistics/tests/test_statistics_execute.py
@@ -260,6 +260,14 @@ def testHistogramExecution(self):
             expected = np.histogram(raw4[raw4 < 0.9])[0]
             np.testing.assert_array_equal(result, expected)
 
+            raw5 = np.arange(3, 10)
+            e = arange(10, chunk_size=3)
+            e = e[e >= 3]
+            hist = histogram(e)
+            result = executor.execute_tensors(hist)[0]
+            expected = np.histogram(raw5)[0]
+            np.testing.assert_array_equal(result, expected)
+
     def testQuantileExecution(self):
         # test 1 chunk, 1-d
         raw = np.random.rand(20)