Repository: modin-project/modin

Issue:
BUG: binary operations fail when `other` is a dictionary
### Modin version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the latest released version of Modin.

- [X] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-master-branch).)


### Reproducible Example

```python
import modin.pandas as pd
import numpy as np

df = pd.DataFrame(np.ones([2, 4]), columns=["a", "b", "c", "d"])
dictionary = {"a": 1, "b": 2, "c": 3, "d": 4}
print(df * dictionary)
```


### Issue Description

The related snippet fails with `TypeError: Cannot do operation with improper dtypes` from here:
https://github.com/modin-project/modin/blob/c30ab4c132295b1e168fbb06a050c76554759e11/modin/pandas/base.py#L296

It seems that the [`._validate_other`](https://github.com/modin-project/modin/blob/c30ab4c132295b1e168fbb06a050c76554759e11/modin/pandas/base.py#L212) function doesn't work as expected when `other` is a dictionary.

### Expected Behavior

As the pandas support binary operations between dataframe and dictionary modin should as well.

### Error Logs

<details>

```python-traceback

Replace this line with the error backtrace (if applicable).

```

</details>


### Installed Versions

<details>

Replace this line with the output of pd.show_versions()

</details>



Discussion:

Resolving patch:
diff --git a/modin/core/dataframe/algebra/binary.py b/modin/core/dataframe/algebra/binary.py
--- a/modin/core/dataframe/algebra/binary.py
+++ b/modin/core/dataframe/algebra/binary.py
@@ -102,7 +102,9 @@ def caller(
                         )
                     )
             else:
-                if isinstance(other, (list, np.ndarray, pandas.Series)):
+                # TODO: it's possible to chunk the `other` and broadcast them to partitions
+                # accordingly, in that way we will be able to use more efficient `._modin_frame.map()`
+                if isinstance(other, (dict, list, np.ndarray, pandas.Series)):
                     new_modin_frame = query_compiler._modin_frame.apply_full_axis(
                         axis,
                         lambda df: func(df, other, *args, **kwargs),
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -267,6 +267,14 @@ def _validate_other(
                     )
             if hasattr(other, "dtype"):
                 other_dtypes = [other.dtype] * len(other)
+            elif is_dict_like(other):
+                other_dtypes = [
+                    type(other[label])
+                    for label in self._query_compiler.get_axis(axis)
+                    # The binary operation is applied for intersection of axis labels
+                    # and dictionary keys. So filtering out extra keys.
+                    if label in other
+                ]
             else:
                 other_dtypes = [type(x) for x in other]
         else:
@@ -283,6 +291,19 @@ def _validate_other(
                 raise TypeError("Cannot perform operation with non-equal index")
         # Do dtype checking.
         if dtype_check:
+            self_dtypes = self._get_dtypes()
+            if is_dict_like(other):
+                # The binary operation is applied for the intersection of axis labels
+                # and dictionary keys. So filtering `self_dtypes` to match the `other`
+                # dictionary.
+                self_dtypes = [
+                    dtype
+                    for label, dtype in zip(
+                        self._query_compiler.get_axis(axis), self._get_dtypes()
+                    )
+                    if label in other
+                ]
+
             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))
@@ -291,7 +312,7 @@ def _validate_other(
                     and is_datetime_or_timedelta_dtype(other_dtype)
                 )
                 or is_dtype_equal(self_dtype, other_dtype)
-                for self_dtype, other_dtype in zip(self._get_dtypes(), other_dtypes)
+                for self_dtype, other_dtype in zip(self_dtypes, other_dtypes)
             ):
                 raise TypeError("Cannot do operation with improper dtypes")
         return result


Test patch:
diff --git a/modin/pandas/test/dataframe/test_binary.py b/modin/pandas/test/dataframe/test_binary.py
--- a/modin/pandas/test/dataframe/test_binary.py
+++ b/modin/pandas/test/dataframe/test_binary.py
@@ -47,8 +47,21 @@
     [
         lambda df: 4,
         lambda df, axis: df.iloc[0] if axis == "columns" else list(df[df.columns[0]]),
+        lambda df, axis: {
+            label: idx + 1
+            for idx, label in enumerate(df.axes[0 if axis == "rows" else 1])
+        },
+        lambda df, axis: {
+            label if idx % 2 else f"random_key{idx}": idx + 1
+            for idx, label in enumerate(df.axes[0 if axis == "rows" else 1][::-1])
+        },
+    ],
+    ids=[
+        "scalar",
+        "series_or_list",
+        "dictionary_keys_equal_columns",
+        "dictionary_keys_unequal_columns",
     ],
-    ids=["scalar", "series_or_list"],
 )
 @pytest.mark.parametrize("axis", ["rows", "columns"])
 @pytest.mark.parametrize(