Repository: modin-project/modin

Issue:
Sum not following min_count argument
### Describe the problem
`sum` often returns `NaN`s if min_count is used

### Source code / logs
```
data = {
    "col3": [np.NaN, 2, np.NaN, 0],
    "col4": [3, 4, np.NaN, 1],
    "col1": [np.NaN, np.NaN, np.NaN, 5],
    "col2": [6, np.NaN, np.NaN, 10],
}

modin_df = pd.DataFrame(data)
modin_df.sum(axis = 1, min_count = 3)
```
returns 
```
0   NaN
1   NaN
2   NaN
3   NaN
dtype: float64
```
instead of
```
0     9.0
1     6.0
2     NaN
3    16.0
dtype: float64
```



Discussion:

Resolving patch:
diff --git a/modin/data_management/data_manager.py b/modin/data_management/data_manager.py
--- a/modin/data_management/data_manager.py
+++ b/modin/data_management/data_manager.py
@@ -935,7 +935,6 @@ def mean(self, **kwargs):
         """
         # Pandas default is 0 (though not mentioned in docs)
         axis = kwargs.get("axis", 0)
-        kwargs["numeric_only"] = True
         return self.sum(**kwargs).divide(self.count(axis=axis, numeric_only=True))
 
     def min(self, **kwargs):
@@ -950,16 +949,65 @@ def min(self, **kwargs):
         func = self._prepare_method(pandas.DataFrame.min, **kwargs)
         return self.full_reduce(axis, func, numeric_only=numeric_only)
 
+    def _process_sum_prod(self, func, ignore_axis=False, **kwargs):
+        """Calculates the sum or product of the DataFrame.
+
+        Args:
+            func: Pandas func to apply to DataFrame.
+            ignore_axis: Whether to ignore axis when raising TypeError
+        Return:
+            Pandas Series with sum or prod of DataFrame.
+        """
+        axis = kwargs.get("axis", 0)
+        numeric_only = kwargs.get("numeric_only", None)
+        min_count = kwargs.get("min_count", 0)
+
+        numeric_only = True if axis else kwargs.get("numeric_only", False)
+
+        reduce_index = self.columns if axis else self.index
+        if numeric_only:
+            result, data_manager = self.numeric_function_clean_dataframe(axis)
+        else:
+            data_manager = self
+        new_index = data_manager.index if axis else data_manager.columns
+
+        def sum_prod_builder(df, **kwargs):
+            if not df.empty:
+                return func(df, **kwargs)
+
+        map_func = self._prepare_method(sum_prod_builder, **kwargs)
+
+        if all(
+            dtype == np.dtype("datetime64[ns]") or dtype == np.dtype("timedelta64[ns]")
+            for dtype in self.dtypes
+        ):
+            if numeric_only is None:
+                new_index = [
+                    col
+                    for col, dtype in zip(self.columns, self.dtypes)
+                    if dtype == np.dtype("timedelta64[ns]")
+                ]
+                return self.full_axis_reduce(map_func, axis, new_index)
+            else:
+                return self.full_axis_reduce(map_func, axis)
+        elif min_count == 0:
+            if numeric_only is None:
+                numeric_only = True
+            return self.full_reduce(axis, map_func, numeric_only=numeric_only)
+        elif min_count > len(reduce_index):
+            return pandas.Series(
+                [np.nan] * len(new_index), index=new_index, dtype=np.dtype("object")
+            )
+        else:
+            return self.full_axis_reduce(map_func, axis, new_index)
+
     def prod(self, **kwargs):
         """Returns the product of each numerical column or row.
 
         Return:
             Pandas series with the product of each numerical column or row.
         """
-        # Pandas default is 0 (though not mentioned in docs)
-        axis = kwargs.get("axis", 0)
-        func = self._prepare_method(pandas.DataFrame.prod, **kwargs)
-        return self.full_reduce(axis, func, numeric_only=True)
+        return self._process_sum_prod(pandas.DataFrame.prod, ignore_axis=True, **kwargs)
 
     def sum(self, **kwargs):
         """Returns the sum of each numerical column or row.
@@ -967,11 +1015,7 @@ def sum(self, **kwargs):
         Return:
             Pandas series with the sum of each numerical column or row.
         """
-        # Pandas default is 0 (though not mentioned in docs)
-        axis = kwargs.get("axis", 0)
-        numeric_only = True if axis else kwargs.get("numeric_only", False)
-        func = self._prepare_method(pandas.DataFrame.sum, **kwargs)
-        return self.full_reduce(axis, func, numeric_only=numeric_only)
+        return self._process_sum_prod(pandas.DataFrame.sum, ignore_axis=False, **kwargs)
 
     # END Full Reduce operations
 
@@ -1094,11 +1138,14 @@ def astype(df, internal_indices=[]):
     # Currently, this means a Pandas Series will be returned, but in the future
     # we will implement a Distributed Series, and this will be returned
     # instead.
-    def full_axis_reduce(self, func, axis):
+    def full_axis_reduce(self, func, axis, alternate_index=None):
         """Applies map that reduce Manager to series but require knowledge of full axis.
 
         Args:
             func: Function to reduce the Manager by. This function takes in a Manager.
+            axis: axis to apply the function to.
+            alternate_index: If the resulting series should have an index
+                different from the current data_manager's index or columns.
 
         Return:
             Pandas series containing the reduced data.
@@ -1111,10 +1158,16 @@ def full_axis_reduce(self, func, axis):
         result = self.data.map_across_full_axis(axis, func).to_pandas(
             self._is_transposed ^ axis
         )
+        if result.empty:
+            return result
         if not axis:
-            result.index = self.columns
+            result.index = (
+                alternate_index if alternate_index is not None else self.columns
+            )
         else:
-            result.index = self.index
+            result.index = (
+                alternate_index if alternate_index is not None else self.index
+            )
         return result
 
     def all(self, **kwargs):
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -397,6 +397,7 @@ def sum(
             The sum of the DataFrame.
         """
         axis = pandas.DataFrame()._get_axis_number(axis) if axis is not None else 0
+        self._validate_dtypes_sum_prod_mean(axis, numeric_only, ignore_axis=False)
 
         return self._data_manager.sum(
             axis=axis,
@@ -2197,8 +2198,8 @@ def mean(self, axis=None, skipna=None, level=None, numeric_only=None, **kwargs):
             The mean of the DataFrame. (Pandas series)
         """
         axis = pandas.DataFrame()._get_axis_number(axis) if axis is not None else 0
-        if numeric_only is not None and not numeric_only:
-            self._validate_dtypes(numeric_only=True)
+        self._validate_dtypes_sum_prod_mean(axis, numeric_only, ignore_axis=False)
+
         return self._data_manager.mean(
             axis=axis, skipna=skipna, level=level, numeric_only=numeric_only, **kwargs
         )
@@ -2628,6 +2629,7 @@ def prod(
             prod : Series or DataFrame (if level specified)
         """
         axis = pandas.DataFrame()._get_axis_number(axis) if axis is not None else 0
+        self._validate_dtypes_sum_prod_mean(axis, numeric_only, ignore_axis=True)
         return self._data_manager.prod(
             axis=axis,
             skipna=skipna,
@@ -4686,6 +4688,38 @@ def _validate_dtypes(self, numeric_only=False):
                     "Cannot compare type '{0}' with type '{1}'".format(t, dtype)
                 )
 
+    def _validate_dtypes_sum_prod_mean(self, axis, numeric_only, ignore_axis=False):
+        """Raises TypeErrors for sum, prod, and mean where necessary"""
+        # We cannot add datetime types, so if we are summing a column with
+        # dtype datetime64 and cannot ignore non-numeric types, we must throw a
+        # TypeError.
+        if (
+            not axis
+            and numeric_only is False
+            and any(dtype == np.dtype("datetime64[ns]") for dtype in self.dtypes)
+        ):
+            raise TypeError("Cannot add Timestamp Types")
+
+        # If our DataFrame has both numeric and non-numeric dtypes then
+        # operations between these types do not make sense and we must raise a
+        # TypeError. The exception to this rule is when there are datetime and
+        # timedelta objects, in which case we proceed with the comparison
+        # without ignoring any non-numeric types. We must check explicitly if
+        # numeric_only is False because if it is None, it will default to True
+        # if the operation fails with mixed dtypes.
+        if (
+            (axis or ignore_axis)
+            and numeric_only is False
+            and np.unique([is_numeric_dtype(dtype) for dtype in self.dtypes]).size == 2
+        ):
+            # check if there are columns with dtypes datetime or timedelta
+            if all(
+                dtype != np.dtype("datetime64[ns]")
+                and dtype != np.dtype("timedelta64[ns]")
+                for dtype in self.dtypes
+            ):
+                raise TypeError("Cannot operate on Numeric and Non-Numeric Types")
+
     def _default_to_pandas_func(self, op, *args, **kwargs):
         """Helper method to use default pandas function"""
         warnings.warn("Defaulting to Pandas implementation", UserWarning)


Test patch: