Repository: modin-project/modin

Issue:
2D assignments in `loc/iloc` are broken
Code to reproduce:
```python
import os
os.environ["MODIN_ENGINE"] = "ray" 
import numpy as np
import modin.pandas as pd
data = pd.DataFrame({'A':[np.nan,'c','b','a'],'B':['d','b','a','d'],'C':[1,2,3,4],'D':[3,4,1,3],'E':[1,1,1,1]})
data.loc[:,['D','C']] =  data.loc[:,['D','C']].astype('float')
```

Result:
```
Traceback (most recent call last):
  File "/home/yz/IdeaProjects/modin_test/test.py", line 10, in <module>
    data.loc[:,['D','C']] =  data.loc[:,['D','C']].astype('float')
  File "/home/yz/anaconda3/lib/python3.7/site-packages/modin/pandas/indexing.py", line 275, in __setitem__
    super(_LocIndexer, self).__setitem__(row_lookup, col_lookup, item)
  File "/home/yz/anaconda3/lib/python3.7/site-packages/modin/pandas/indexing.py", line 166, in __setitem__
    item = self._broadcast_item(row_lookup, col_lookup, item, to_shape)
  File "/home/yz/anaconda3/lib/python3.7/site-packages/modin/pandas/indexing.py", line 186, in _broadcast_item
    "Must have equal len keys and value when setting "
ValueError: Must have equal len keys and value when setting with an iterable
```


Discussion:
Hi @YZ576023775, try this:

```python
import os
os.environ["MODIN_ENGINE"] = "ray"
import numpy as np
import modin.pandas as pd
data = pd.DataFrame({'A':[np.nan,'c','b','a'],'B':['d','b','a','d'],'C':[1,2,3,4],'D':[3,4,1,3],'E':[1,1,1,1]})
# data.loc[:,['D','C']] = data.loc[:,['D','C']].astype('float')
data["C"] = data["C"].astype("float")
data["D"] = data["D"].astype("float")
```

Something is happening with the `loc` accessor because it is not in the same order as the data, I will investigate.
> Hi @YZ576023775, try this:
> 
> ```python
> import os
> os.environ["MODIN_ENGINE"] = "ray"
> import numpy as np
> import modin.pandas as pd
> data = pd.DataFrame({'A':[np.nan,'c','b','a'],'B':['d','b','a','d'],'C':[1,2,3,4],'D':[3,4,1,3],'E':[1,1,1,1]})
> # data.loc[:,['D','C']] = data.loc[:,['D','C']].astype('float')
> data["C"] = data["C"].astype("float")
> data["D"] = data["D"].astype("float")
> ```
> 
> Something is happening with the `loc` accessor because it is not in the same order as the data, I will investigate.

thanks, but the scenario is that the project is written, and I hope to use modin directly without changing the code
I understand @YZ576023775, we will fix the bug. It is just a temporary workaround until the issue gets resolved.
> I understand @YZ576023775, we will fix the bug. It is just a temporary workaround until the issue gets resolved.

ok, thanks
This bug is still reproducible on the current master, the problem is in how `loc/iloc` assignments work:
- Going by `loc` arguments we're computing two variables to determine the view of the frame: `row_lookup` and `col_lookup` it's **numeric indices** for index and column of the frame
- When 2D assignments occurs, assignment will be done by [`_broadcast_item`](https://github.com/modin-project/modin/blob/fc3485273ff89b10c8559b08626ce2eb2198b345/modin/pandas/indexing.py#L363) method, that has these two checks at the beginning of it:
https://github.com/modin-project/modin/blob/fc3485273ff89b10c8559b08626ce2eb2198b345/modin/pandas/indexing.py#L388-L398
It checks that every value from row and column lookups exists in frame index and columns labels, however, these checks will be always false since row and column lookups are **numeric indices**

Resolving patch:
diff --git a/modin/pandas/indexing.py b/modin/pandas/indexing.py
--- a/modin/pandas/indexing.py
+++ b/modin/pandas/indexing.py
@@ -385,20 +385,24 @@ def _broadcast_item(self, row_lookup, col_lookup, item, to_shape):
         # It is valid to pass a DataFrame or Series to __setitem__ that is larger than
         # the target the user is trying to overwrite. This
         if isinstance(item, (pandas.Series, pandas.DataFrame, Series, DataFrame)):
-            if not all(idx in item.index for idx in row_lookup):
+            # convert indices in lookups to names, as Pandas reindex expects them to be so
+            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 hasattr(item, "columns"):
-                if not all(idx in item.columns for idx in col_lookup):
+                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=row_lookup, columns=col_lookup)
+                item = item.reindex(index=index_values, columns=column_values)
             else:
-                item = item.reindex(index=row_lookup)
+                item = item.reindex(index=index_values)
         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
@@ -411,6 +411,48 @@ def test_loc_assignment(index, columns):
     df_equals(md_df, pd_df)
 
 
+@pytest.fixture
+def loc_iter_dfs():
+    columns = ["col1", "col2", "col3"]
+    index = ["row1", "row2", "row3"]
+    return create_test_dfs(
+        {col: ([idx] * len(index)) for idx, col in enumerate(columns)},
+        columns=columns,
+        index=index,
+    )
+
+
+@pytest.mark.parametrize("reverse_order", [False, True])
+@pytest.mark.parametrize("axis", [0, 1])
+def test_loc_iter_assignment(loc_iter_dfs, reverse_order, axis):
+    if reverse_order and axis:
+        pytest.xfail(
+            "Due to internal sorting of lookup values assignment order is lost, see GH-#2552"
+        )
+
+    md_df, pd_df = loc_iter_dfs
+
+    select = [slice(None), slice(None)]
+    select[axis] = sorted(pd_df.axes[axis][:-1], reverse=reverse_order)
+    select = tuple(select)
+
+    pd_df.loc[select] = pd_df.loc[select] + pd_df.loc[select]
+    md_df.loc[select] = md_df.loc[select] + md_df.loc[select]
+    df_equals(md_df, pd_df)
+
+
+@pytest.mark.parametrize("reverse_order", [False, True])
+@pytest.mark.parametrize("axis", [0, 1])
+def test_loc_order(loc_iter_dfs, reverse_order, axis):
+    md_df, pd_df = loc_iter_dfs
+
+    select = [slice(None), slice(None)]
+    select[axis] = sorted(pd_df.axes[axis][:-1], reverse=reverse_order)
+    select = tuple(select)
+
+    df_equals(pd_df.loc[select], md_df.loc[select])
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_loc_nested_assignment(data):
     modin_df = pd.DataFrame(data)