Repository: modin-project/modin

Issue:
loc: ValueError: Must have equal len keys and value when setting with an iterable
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: `Windows 10`
- **Modin version** (`modin.__version__`): 270fd697a82d051f0ab2c54b70b74296699bdcf2
- **Python version**: `3.8.13`
- **Code we can use to reproduce**:

```python
import modin.pandas as pd
import pandas
import numpy as np

data = np.random.uniform(-10, 10, size=(1_000, 64))
modin_df = pd.DataFrame(data, columns=[f"col{x}" for x in range(64)])
pandas_df = pandas.DataFrame(data)

value = [[1, 2]] * pandas_df.shape[0]
df_value = pandas.DataFrame(value, columns=["value_col1", "value_col2"])

pandas_df.loc[:,["col0", "col1"]] = df_value
print(pandas_df)

modin_df.loc[:,["col0", "col1"]] = df_value
print(modin_df)
```

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->

Doesn't match pandas behavior.

### Source code / logs
<!-- Include any logs or source code that would be helpful to diagnose the problem. If including tracebacks, please include the full traceback. Large logs and files should be attached. Try to provide a reproducible test case that is the bare minimum necessary to generate the problem. -->

```python
UserWarning: Distributing <class 'numpy.ndarray'> object. This may take some time.
            0         1         2         3         4  ...        61        62        63  col0  col1
0    7.434462 -6.642546 -5.053403 -3.082483 -5.166161  ... -2.321723  2.344384 -5.086450   NaN   NaN
1   -5.588129  3.408298  6.107310 -9.978693  7.971698  ... -8.958338 -6.877036  8.365029   NaN   NaN
2    0.930564  6.818884  8.976215 -8.916126 -0.109316  ... -5.584792  9.976801  4.072142   NaN   NaN
3    4.225055  0.676622 -7.934408 -6.333212  7.102772  ...  0.274859  4.476704 -2.380820   NaN   NaN
4    3.219583  9.577160 -0.208430 -0.401685 -1.422718  ... -6.373741 -7.312425 -1.023087   NaN   NaN
..        ...       ...       ...       ...       ...  ...       ...       ...       ...   ...   ...
995 -1.971414 -1.158964 -6.605528 -0.458600 -9.196682  ... -3.741060 -1.780379 -5.259047   NaN   NaN
996 -8.957609 -1.981501 -7.567003 -8.744357  8.112483  ... -5.843098 -6.853522 -1.260222   NaN   NaN
997 -0.181999 -0.573202 -0.244353  8.773964  5.622878  ... -8.762187  3.118430 -4.345774   NaN   NaN
998 -6.319434 -4.489640  2.354008 -0.376875 -5.858796  ... -9.203798 -5.107277 -3.389242   NaN   NaN
999  7.352918  2.249689  2.908928 -8.399926 -8.573160  ...  6.329783 -6.007046  0.691303   NaN   NaN

[1000 rows x 66 columns]
Traceback (most recent call last):
  File "test_4455.py", line 15, in <module>
    modin_df.loc[:,["col0", "col1"]] = df_value
  File "...\modin\pandas\indexing.py", line 
719, in __setitem__
    super(_LocIndexer, self).__setitem__(
  File "...\modin\pandas\indexing.py", line 
389, in __setitem__
    item = self._broadcast_item(row_lookup, col_lookup, item, to_shape)
  File "...\modin\pandas\indexing.py", line 
437, in _broadcast_item
    raise ValueError(
ValueError: Must have equal len keys and value when setting with an iterable
```


Discussion:

Resolving patch:
diff --git a/modin/pandas/indexing.py b/modin/pandas/indexing.py
--- a/modin/pandas/indexing.py
+++ b/modin/pandas/indexing.py
@@ -424,23 +424,17 @@ def _broadcast_item(self, row_lookup, col_lookup, item, to_shape):
         # the target the user is trying to overwrite. This
         if isinstance(item, (pandas.Series, pandas.DataFrame, Series, DataFrame)):
             # convert indices in lookups to names, as Pandas reindex expects them to be so
+            axes_to_reindex = {}
             index_values = self.qc.index[row_lookup]
-            if not all(idx in item.index for idx in index_values):
-                raise ValueError(
-                    "Must have equal len keys and value when setting with "
-                    + "an iterable"
-                )
+            if not index_values.equals(item.index):
+                axes_to_reindex["index"] = index_values
             if hasattr(item, "columns"):
                 column_values = self.qc.columns[col_lookup]
-                if not all(col in item.columns for col in column_values):
-                    # TODO: think if it is needed to handle cases when columns have duplicate names
-                    raise ValueError(
-                        "Must have equal len keys and value when setting "
-                        + "with an iterable"
-                    )
-                item = item.reindex(index=index_values, columns=column_values)
-            else:
-                item = item.reindex(index=index_values)
+                if not column_values.equals(item.columns):
+                    axes_to_reindex["columns"] = column_values
+            # New value for columns/index make that reindex add NaN values
+            if axes_to_reindex:
+                item = item.reindex(**axes_to_reindex)
         try:
             item = np.array(item)
             if np.prod(to_shape) == np.prod(item.shape):


Test patch:
diff --git a/modin/pandas/test/dataframe/test_indexing.py b/modin/pandas/test/dataframe/test_indexing.py
--- a/modin/pandas/test/dataframe/test_indexing.py
+++ b/modin/pandas/test/dataframe/test_indexing.py
@@ -42,6 +42,7 @@
 from modin.config import NPartitions
 from modin.utils import get_current_execution
 from modin.test.test_utils import warns_that_defaulting_to_pandas
+from modin.pandas.indexing import is_range_like
 
 NPartitions.put(4)
 
@@ -67,6 +68,24 @@ def eval_setitem(md_df, pd_df, value, col=None, loc=None):
     )
 
 
+def eval_loc(md_df, pd_df, value, key):
+    if isinstance(value, tuple):
+        assert len(value) == 2
+        # case when value for pandas different
+        md_value, pd_value = value
+    else:
+        md_value, pd_value = value, value
+
+    eval_general(
+        md_df,
+        pd_df,
+        lambda df: df.loc.__setitem__(
+            key, pd_value if isinstance(df, pandas.DataFrame) else md_value
+        ),
+        __inplace__=True,
+    )
+
+
 @pytest.mark.parametrize(
     "dates",
     [
@@ -384,6 +403,75 @@ def test_loc(data):
         modin_df.loc["NO_EXIST"]
 
 
+@pytest.mark.parametrize(
+    "key_getter, value_getter",
+    [
+        pytest.param(
+            lambda df, axis: (
+                (slice(None), df.axes[axis][:2])
+                if axis
+                else (df.axes[axis][:2], slice(None))
+            ),
+            lambda df, axis: df.iloc[:, :1] if axis else df.iloc[:1, :],
+            id="len(key)_>_len(value)",
+        ),
+        pytest.param(
+            lambda df, axis: (
+                (slice(None), df.axes[axis][:2])
+                if axis
+                else (df.axes[axis][:2], slice(None))
+            ),
+            lambda df, axis: df.iloc[:, :3] if axis else df.iloc[:3, :],
+            id="len(key)_<_len(value)",
+        ),
+        pytest.param(
+            lambda df, axis: (
+                (slice(None), df.axes[axis][:2])
+                if axis
+                else (df.axes[axis][:2], slice(None))
+            ),
+            lambda df, axis: df.iloc[:, :2] if axis else df.iloc[:2, :],
+            id="len(key)_==_len(value)",
+        ),
+    ],
+)
+@pytest.mark.parametrize("key_axis", [0, 1])
+@pytest.mark.parametrize("reverse_value_index", [True, False])
+@pytest.mark.parametrize("reverse_value_columns", [True, False])
+def test_loc_4456(
+    key_getter, value_getter, key_axis, reverse_value_index, reverse_value_columns
+):
+    data = test_data["float_nan_data"]
+    modin_df, pandas_df = pd.DataFrame(data), pandas.DataFrame(data)
+
+    key = key_getter(pandas_df, key_axis)
+
+    # `df.loc` doesn't work right for range-like indexers. Converting them to a list.
+    # https://github.com/modin-project/modin/issues/4497
+    if is_range_like(key[0]):
+        key = (list(key[0]), key[1])
+    if is_range_like(key[1]):
+        key = (key[0], list(key[1]))
+
+    value = pandas.DataFrame(
+        np.random.randint(0, 100, size=pandas_df.shape),
+        index=pandas_df.index,
+        columns=pandas_df.columns,
+    )
+    pdf_value = value_getter(value, key_axis)
+    mdf_value = value_getter(pd.DataFrame(value), key_axis)
+
+    if reverse_value_index:
+        pdf_value = pdf_value.reindex(index=pdf_value.index[::-1])
+        mdf_value = mdf_value.reindex(index=mdf_value.index[::-1])
+    if reverse_value_columns:
+        pdf_value = pdf_value.reindex(columns=pdf_value.columns[::-1])
+        mdf_value = mdf_value.reindex(columns=mdf_value.columns[::-1])
+
+    eval_loc(modin_df, pandas_df, pdf_value, key)
+    eval_loc(modin_df, pandas_df, (mdf_value, pdf_value), key)
+
+
 # This tests the bug from https://github.com/modin-project/modin/issues/3736
 def test_loc_setting_single_categorical_column():
     modin_df = pd.DataFrame({"status": ["a", "b", "c"]}, dtype="category")