Repository: modin-project/modin

Issue:
using .iloc to assign new value
#### System information
- **Linux Ubuntu 19.10**
- **Modin 0.7.3**
- **Python 3.7.7**

#### Problem:
When trying to use .iloc to assign a new value there are no changes taking place.

I already left a comment on the issue #147 but it's already closed and I wasn't sure that you can see it.

#### Code to reproduce:
```
import modin.pandas as pd #dask
test = pd.DataFrame({'first': range(10)})
test['first'].iloc[0] = 500
#or test['first'].iloc[[0, 1, 2]] = 500
print(test) # there is no changes
```


Discussion:
Thanks @ndy-cd, we prefer the new issue since the old one was resolved. This issue is slightly different because of the assignment.

So what is happening is that the `test['first'].iloc` assignment is happening inplace in pandas. Modin is doing the assignment, but not propagating the `test['first']` back to the original dataframe. It will be straightforward to fix, the two types of indexing case in the same statement (`first`, and `iloc`) was not handled.

Assignment with `loc`/`iloc` will work if you don't access the column first. A simple temporary workaround for the double indexing is to turn it into a single `loc` call:

```python
import modin.pandas as pd #dask
test = pd.DataFrame({'first': range(10)})
test.loc[0, 'first'] = 500  # 0 is the Index value of the 0th row
#or test.iloc[[0, 1, 2], 'first'] = 500
print(test)
```

If you prefer to use `iloc` you can also do this:

```python
import modin.pandas as pd #dask
test = pd.DataFrame({'first': range(10)})
test.iloc[0, 0] = 500  # 0 is the column and row number
#or test.iloc[[0, 1, 2], 0] = 500
print(test)
```

We have the machinery in place to support this, but it looks like something got lost with the double indexing. Thanks for the detailed report! We will get this fixed.
Thank you very much for detailed answer!

It seems to be a same problem with using .replace()
`test['first'].replace({500: 40}, inplace=True)`
Thanks @ndy-cd, that is expected behavior.

Pandas has many ways to do the same thing as you probably know, we have one internal way to do things so that we can better optimize these fundamental operations.

Resolving patch:
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -132,6 +132,15 @@ def _validate_dtypes_min_max(self, axis, numeric_only):
     def _validate_dtypes(self, numeric_only=False):
         pass
 
+    def _update_inplace(self, new_query_compiler):
+        super(Series, self)._update_inplace(new_query_compiler=new_query_compiler)
+        # Propagate changes back to parent so that column in dataframe had the same contents
+        if self._parent is not None:
+            if self._parent_axis == 0:
+                self._parent.loc[self.name] = self
+            else:
+                self._parent[self.name] = self
+
     def _create_or_update_from_compiler(self, new_query_compiler, inplace=False):
         """Returns or updates a DataFrame given new query_compiler"""
         assert (
@@ -329,12 +338,6 @@ def __setitem__(self, key, value):
         self._create_or_update_from_compiler(
             self._query_compiler.setitem(1, key, value), inplace=True
         )
-        # Propagate changes back to parent so that column in dataframe had the same contents
-        if self._parent is not None:
-            if self._parent_axis == 0:
-                self._parent.loc[self.name] = self
-            else:
-                self._parent[self.name] = self
 
     def __sub__(self, right):
         return self.sub(right)


Test patch:
diff --git a/modin/pandas/test/test_dataframe.py b/modin/pandas/test/test_dataframe.py
--- a/modin/pandas/test/test_dataframe.py
+++ b/modin/pandas/test/test_dataframe.py
@@ -4581,6 +4581,21 @@ def test_loc_assignment(self):
         pandas_df.loc["row3"]["col2"] = 32
         df_equals(modin_df, pandas_df)
 
+    @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
+    def test_loc_nested_assignment(self, data):
+        modin_df = pd.DataFrame(data)
+        pandas_df = pandas.DataFrame(data)
+        key1 = modin_df.columns[0]
+        key2 = modin_df.columns[1]
+
+        modin_df[key1].loc[0] = 500
+        pandas_df[key1].loc[0] = 500
+        df_equals(modin_df, pandas_df)
+
+        modin_df[key2].loc[0] = None
+        pandas_df[key2].loc[0] = None
+        df_equals(modin_df, pandas_df)
+
     def test_iloc_assignment(self):
         modin_df = pd.DataFrame(
             index=["row1", "row2", "row3"], columns=["col1", "col2"]
@@ -4602,6 +4617,21 @@ def test_iloc_assignment(self):
         pandas_df.iloc[2]["col2"] = 32
         df_equals(modin_df, pandas_df)
 
+    @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
+    def test_iloc_nested_assignment(self, data):
+        modin_df = pd.DataFrame(data)
+        pandas_df = pandas.DataFrame(data)
+        key1 = modin_df.columns[0]
+        key2 = modin_df.columns[1]
+
+        modin_df[key1].iloc[0] = 500
+        pandas_df[key1].iloc[0] = 500
+        df_equals(modin_df, pandas_df)
+
+        modin_df[key2].iloc[0] = None
+        pandas_df[key2].iloc[0] = None
+        df_equals(modin_df, pandas_df)
+
     @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
     def test_pop(self, request, data):
         modin_df = pd.DataFrame(data)