Repository: modin-project/modin

Issue:
inter_df_operations not working as expected
### Describe the problem
`inter_df_operations` such as `add` and `lt` are not working as they should

### Source code / logs
```
import modin.pandas as pd
import numpy as np

data = {
    "col3": [0, 1, 2, 3],
    "col4": [4, 5, 6, 7],
    "col5": [8, 9, 10, 11],
    "col1": [12, 13, 14, 15],
    "col2": [0, 0, 0, 0],
}

modin_df = pd.DataFrame(data)

modin_df.add(np.random.randint(-100, 100, size=(modin_df.shape[1])))
```
Throws a `RayGetError`


Discussion:

Resolving patch:
diff --git a/modin/data_management/query_compiler/pandas_query_compiler.py b/modin/data_management/query_compiler/pandas_query_compiler.py
--- a/modin/data_management/query_compiler/pandas_query_compiler.py
+++ b/modin/data_management/query_compiler/pandas_query_compiler.py
@@ -449,7 +449,7 @@ def _inter_df_op_handler(self, func, other, **kwargs):
         Returns:
             New DataManager with new data and index.
         """
-        axis = kwargs.get("axis", 0)
+        axis = pandas.DataFrame()._get_axis_number(kwargs.get("axis", 0))
 
         if isinstance(other, type(self)):
             return self.inter_manager_operations(
@@ -469,7 +469,6 @@ def add(self, other, **kwargs):
         Returns:
             New DataManager with added data and new index.
         """
-        # TODO: need to write a prepare_function for inter_df operations
         func = pandas.DataFrame.add
         return self._inter_df_op_handler(func, other, **kwargs)
 
@@ -650,21 +649,7 @@ def sub(self, other, **kwargs):
         Returns:
             New DataManager with subtracted data and new index.
         """
-
-        def sub_builder(df, other, **kwargs):
-            axis = kwargs.get("axis", 0)
-            index = kwargs.pop("index")
-            if axis == 0:
-                old_index = df.index
-                df.index = index
-            df = df.sub(other, **kwargs)
-            if axis == 0:
-                df.index = old_index
-            return df
-
-        func = sub_builder
-        kwargs["axis"] = pandas.DataFrame()._get_axis_number(kwargs.get("axis", 0))
-        kwargs["index"] = self.index
+        func = pandas.DataFrame.sub
         return self._inter_df_op_handler(func, other, **kwargs)
 
     def truediv(self, other, **kwargs):
@@ -786,7 +771,7 @@ def scalar_operations(self, axis, scalar, func):
         Returns:
             New DataManager with updated data and new index.
         """
-        if isinstance(scalar, (list, pandas.Series)):
+        if isinstance(scalar, (list, np.ndarray, pandas.Series)):
             new_data = self.map_across_full_axis(axis, func)
             return self.__constructor__(new_data, self.index, self.columns)
         else:
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -13,6 +13,8 @@
     is_numeric_dtype,
     is_datetime_or_timedelta_dtype,
     is_dtype_equal,
+    is_object_dtype,
+    is_integer_dtype,
 )
 from pandas.core.index import _ensure_index_from_sequences
 from pandas.core.indexing import check_bool_indexer, convert_to_index_sliceable
@@ -531,7 +533,6 @@ def dropna(self, axis=0, how="any", thresh=None, subset=None, inplace=False):
         new_manager = self._query_compiler.dropna(
             axis=axis, how=how, thresh=thresh, subset=subset
         )
-
         if not inplace:
             return DataFrame(query_compiler=new_manager)
         else:
@@ -550,6 +551,7 @@ def add(self, other, axis="columns", level=None, fill_value=None):
         Returns:
             A new DataFrame with the applied addition.
         """
+        axis = pandas.DataFrame()._get_axis_number(axis)
         if level is not None:
             if isinstance(other, DataFrame):
                 other = other._query_compiler.to_pandas()
@@ -560,8 +562,7 @@ def add(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_or_object_only=True)
         new_manager = self._query_compiler.add(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -1172,7 +1173,7 @@ def div(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
         new_manager = self._query_compiler.div(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -1610,7 +1611,7 @@ def floordiv(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
         new_manager = self._query_compiler.floordiv(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -1692,7 +1693,7 @@ def ge(self, other, axis="columns", level=None):
             return self._default_to_pandas_func(
                 pandas.DataFrame.ge, other, axis=axis, level=level
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, comparison_dtypes_only=True)
         new_manager = self._query_compiler.ge(other=other, axis=axis, level=level)
         return self._create_dataframe_from_manager(new_manager)
 
@@ -1758,7 +1759,7 @@ def gt(self, other, axis="columns", level=None):
             return self._default_to_pandas_func(
                 pandas.DataFrame.gt, other, axis=axis, level=level
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, comparison_dtypes_only=True)
         new_manager = self._query_compiler.gt(other=other, axis=axis, level=level)
         return self._create_dataframe_from_manager(new_manager)
 
@@ -2220,7 +2221,7 @@ def le(self, other, axis="columns", level=None):
             return self._default_to_pandas_func(
                 pandas.DataFrame.le, other, axis=axis, level=level
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, comparison_dtypes_only=True)
         new_manager = self._query_compiler.le(other=other, axis=axis, level=level)
         return self._create_dataframe_from_manager(new_manager)
 
@@ -2246,7 +2247,7 @@ def lt(self, other, axis="columns", level=None):
             return self._default_to_pandas_func(
                 pandas.DataFrame.lt, other, axis=axis, level=level
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, comparison_dtypes_only=True)
         new_manager = self._query_compiler.lt(other=other, axis=axis, level=level)
         return self._create_dataframe_from_manager(new_manager)
 
@@ -2476,7 +2477,7 @@ def mod(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
         new_manager = self._query_compiler.mod(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -2521,7 +2522,7 @@ def mul(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
         new_manager = self._query_compiler.mul(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -2727,8 +2728,33 @@ def pow(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
+        # Check to make sure integers are not raised to negative integer powers
+        if isinstance(other, type(self._query_compiler)):
+            other_dtypes = other.dtypes
+        elif is_list_like(other):
+            other_dtypes = [type(x) for x in other]
+        else:
+            other_dtypes = [
+                type(other)
+                for _ in range(len(self.index) if axis else len(self.columns))
+            ]
+        for i in range(len(other_dtypes)):
+            if is_integer_dtype(other_dtypes[i]) and is_integer_dtype(self.dtypes[i]):
+                if isinstance(other, type(self._query_compiler)):
+                    continue
+                    # TODO: Come back to this when we have an efficient way to preprocess data
+                    # if old_other.iloc[:, i].lt(0).any():
+                    #     raise ValueError("Integers to negative integer powers are not allowed.")
+                elif is_list_like(other):
+                    if any(x < 0 for x in other):
+                        raise ValueError(
+                            "Integers to negative integer powers are not allowed."
+                        )
+                elif other < 0:
+                    raise ValueError(
+                        "Integers to negative integer powers are not allowed."
+                    )
         new_manager = self._query_compiler.pow(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -2950,7 +2976,7 @@ def rdiv(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
         new_manager = self._query_compiler.rdiv(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -3296,7 +3322,14 @@ def rpow(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
+        # Check to make sure integers are not raised to negative integer powers
+        if (
+            is_integer_dtype(type(other))
+            and other < 0
+            and all(is_integer_dtype(t) for t in self.dtypes)
+        ):
+            raise ValueError("Integers to negative integer powers are not allowed.")
         new_manager = self._query_compiler.rpow(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -3308,13 +3341,14 @@ def rsub(self, other, axis="columns", level=None, fill_value=None):
 
         Args:
             other: The object to use to apply the subtraction to this.
-            axis: THe axis to apply the subtraction over.
+            axis: The axis to apply the subtraction over.
             level: Mutlilevel index level to subtract over.
             fill_value: The value to fill NaNs with.
 
         Returns:
              A new DataFrame with the subtraciont applied.
         """
+        axis = pandas.DataFrame()._get_axis_number(axis)
         if level is not None:
             if isinstance(other, DataFrame):
                 other = other._query_compiler.to_pandas()
@@ -3325,7 +3359,7 @@ def rsub(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_or_time_only=True)
         new_manager = self._query_compiler.rsub(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -3848,6 +3882,7 @@ def sub(self, other, axis="columns", level=None, fill_value=None):
         Returns:
              A new DataFrame with the subtraciont applied.
         """
+        axis = pandas.DataFrame()._get_axis_number(axis)
         if level is not None:
             if isinstance(other, DataFrame):
                 other = other._query_compiler.to_pandas()
@@ -3858,7 +3893,7 @@ def sub(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_or_time_only=True)
         new_manager = self._query_compiler.sub(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -4315,7 +4350,7 @@ def truediv(self, other, axis="columns", level=None, fill_value=None):
                 level=level,
                 fill_value=fill_value,
             )
-        other = self._validate_other(other, axis)
+        other = self._validate_other(other, axis, numeric_only=True)
         new_manager = self._query_compiler.truediv(
             other=other, axis=axis, level=level, fill_value=fill_value
         )
@@ -4883,12 +4918,23 @@ def _create_dataframe_from_manager(self, new_manager, inplace=False):
         else:
             self._update_inplace(new_manager=new_manager)
 
-    def _validate_other(self, other, axis):
+    def _validate_other(
+        self,
+        other,
+        axis,
+        numeric_only=False,
+        numeric_or_time_only=False,
+        numeric_or_object_only=False,
+        comparison_dtypes_only=False,
+    ):
         """Helper method to check validity of other in inter-df operations"""
         axis = pandas.DataFrame()._get_axis_number(axis)
+        result = other
         if isinstance(other, DataFrame):
+            other_dtypes = other.dtypes
             return other._query_compiler
         elif is_list_like(other):
+            other_dtypes = [type(x) for x in other]
             if axis == 0:
                 if len(other) != len(self.index):
                     raise ValueError(
@@ -4901,7 +4947,53 @@ def _validate_other(self, other, axis):
                         "Unable to coerce to Series, length must be {0}: "
                         "given {1}".format(len(self.columns), len(other))
                     )
-        return other
+        else:
+            other_dtypes = [
+                type(other)
+                for _ in range(len(self.index) if axis else len(self.columns))
+            ]
+
+        # Do dtype checking
+        if numeric_only:
+            if not all(
+                is_numeric_dtype(self_dtype) and is_numeric_dtype(other_dtype)
+                for self_dtype, other_dtype in zip(self.dtypes, other_dtypes)
+            ):
+                raise TypeError("Cannot do operation on non-numeric dtypes")
+        elif numeric_or_object_only:
+            if not all(
+                (is_numeric_dtype(self_dtype) and is_numeric_dtype(other_dtype))
+                or (is_object_dtype(self_dtype) and is_object_dtype(other_dtype))
+                for self_dtype, other_dtype in zip(self.dtypes, other_dtypes)
+            ):
+                raise TypeError("Cannot do operation non-numeric dtypes")
+        elif comparison_dtypes_only:
+            if not all(
+                (is_numeric_dtype(self_dtype) and is_numeric_dtype(other_dtype))
+                or (
+                    is_datetime_or_timedelta_dtype(self_dtype)
+                    and is_datetime_or_timedelta_dtype(other_dtype)
+                )
+                or is_dtype_equal(self_dtype, other_dtype)
+                for self_dtype, other_dtype in zip(self.dtypes, other_dtypes)
+            ):
+                raise TypeError(
+                    "Cannot do operation non-numeric objects with numeric objects"
+                )
+        elif numeric_or_time_only:
+            if not all(
+                (is_numeric_dtype(self_dtype) and is_numeric_dtype(other_dtype))
+                or (
+                    is_datetime_or_timedelta_dtype(self_dtype)
+                    and is_datetime_or_timedelta_dtype(other_dtype)
+                )
+                for self_dtype, other_dtype in zip(self.dtypes, other_dtypes)
+            ):
+                raise TypeError(
+                    "Cannot do operation non-numeric objects with numeric objects"
+                )
+
+        return result
 
     def _validate_dtypes(self, numeric_only=False):
         """Helper method to check that all the dtypes are the same"""


Test patch: