Repository: modin-project/modin

Issue:
DataFrame operations affect other DataFrames
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: MacOS 12.0.1
- **Modin version** (`modin.__version__`): 0.13.1
- **Python version**: 3.8.8
- **Code we can use to reproduce**:
```
# import pandas as pd
import modin.pandas as pd

df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]})

a = pd.DataFrame(df.sum().reset_index())
b = pd.DataFrame(df.mean().reset_index())
b.columns = ["b_col1", "b_col2"]
b["b_col2"] = b["b_col2"] / 2 # This is the operation that causes the corruption.

print(a.columns) 
# Modin returns Index(['b_col1', 'b_col2'], dtype='object')
# pandas returns Index(['index', 0], dtype='object')
```

### Describe the problem
When two dataframes are created from the same DataFrame, their column names can become corrupted by an operation on the other. I haven't had time to dig into a simpler reproducible case that would help find the source of the bug, except that without the line `b["b_col2"] = b["b_col2"] / 2` this mistake doesn't happen.

### 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. -->



Discussion:
@jeffreykennethli thanks for finding this! I believe that the cause is that `a` and `b` in your example share a query compiler and Modin thus makes them [sibling objects](https://github.com/modin-project/modin/blob/32cff0d820f0149189d434cc5880f6fa91f5fd70/modin/pandas/base.py#L112). Assigning to `b["b_col2"]` calls [`_update_inplace`](https://github.com/modin-project/modin/blob/32cff0d820f0149189d434cc5880f6fa91f5fd70/modin/pandas/base.py#L216), which resets the query compilers of all siblings to the query compiler of the current dataframe.

I'm working on a fix.
Because all dataframes share the same `_siblings` object,  once any dataframe adds a sibling [here](https://github.com/modin-project/modin/blob/32cff0d820f0149189d434cc5880f6fa91f5fd70/modin/pandas/dataframe.py#L131), every dataframe gets that sibling. Here's a simpler reproducible example:

```python
import modin.pandas as pd
df1 = pd.DataFrame([[1]])
df1 = pd.DataFrame(df1)
df2 = pd.DataFrame([[2]])
# This will change the value in both df1 and df2.
# In pandas, it will only change the value in df2.
df2.iloc[0, 0] = 3
```

Resolving patch:
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -109,10 +109,6 @@ class BasePandasDataset(object):
     # but lives in "pandas" namespace.
     _pandas_class = pandas.core.generic.NDFrame
 
-    # Siblings are other objects that share the same query compiler. We use this list
-    # to update inplace when there is a shallow copy.
-    _siblings = []
-
     def _add_sibling(self, sibling):
         """
         Add a DataFrame or Series object to the list of siblings.
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -107,6 +107,9 @@ def __init__(
         copy=None,
         query_compiler=None,
     ):
+        # Siblings are other dataframes that share the same query compiler. We
+        # use this list to update inplace when there is a shallow copy.
+        self._siblings = []
         Engine.subscribe(_update_engine)
         if isinstance(data, (DataFrame, Series)):
             self._query_compiler = data._query_compiler.copy()
@@ -2448,9 +2451,18 @@ def __setattr__(self, key, value):
         value : Any
             Value to set.
         """
-        # We have to check for this first because we have to be able to set
-        # _query_compiler before we check if the key is in self
-        if key in ["_query_compiler"] or key in self.__dict__:
+        # While we let users assign to a column labeled "x" with "df.x" , there
+        # are some attributes that we should assume are NOT column names and
+        # therefore should follow the default Python object assignment
+        # behavior. These are:
+        # - anything in self.__dict__. This includes any attributes that the
+        #   user has added to the dataframe with,  e.g., `df.c = 3`, and
+        #   any attribute that Modin has added to the frame, e.g.
+        #   `_query_compiler` and `_siblings`
+        # - `_query_compiler`, which Modin initializes before it appears in
+        #   __dict__
+        # - `_siblings`, which Modin initializes before it appears in __dict__
+        if key in ["_query_compiler", "_siblings"] or key in self.__dict__:
             pass
         elif key in self and key not in dir(self):
             self.__setitem__(key, value)
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -125,6 +125,9 @@ def __init__(
         fastpath=False,
         query_compiler=None,
     ):
+        # Siblings are other dataframes that share the same query compiler. We
+        # use this list to update inplace when there is a shallow copy.
+        self._siblings = []
         Engine.subscribe(_update_engine)
         if isinstance(data, type(self)):
             query_compiler = data._query_compiler.copy()


Test patch: