Repository: modin-project/modin

Issue:
Can't construct frame with MultiIndex without names
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Any
- **Modin version** (`modin.__version__`): 0.8.0
- **Python version**: 3.7.7
- **Code we can use to reproduce**:
```python
if __name__ == "__main__":
    import os

    os.environ["MODIN_EXPERIMENTAL"] = "True"
    os.environ["MODIN_ENGINE"] = "ray"
    os.environ["MODIN_BACKEND"] = "omnisci"

    import modin.pandas as pd
    import numpy as np

    data = {"a": np.arange(10), "b": np.arange(10)}

    mi_index = pd.MultiIndex.from_tuples([(i, j) for i in range(2) for j in range(5)])

    md_df = pd.DataFrame(data, index=mi_index)

```

<details><summary>Traceback</summary>


```
Traceback (most recent call last):
  File "../reproducer.py", line 15, in <module>
    md_df = pd.DataFrame(data, index=mi_index)
  File "/localdisk/dchigare/repos/modin_bp/modin/pandas/dataframe.py", line 142, in __init__
    self._query_compiler = from_pandas(pandas_df)._query_compiler
  File "/localdisk/dchigare/repos/modin_bp/modin/pandas/utils.py", line 37, in from_pandas
    return DataFrame(query_compiler=EngineDispatcher.from_pandas(df))
  File "/localdisk/dchigare/repos/modin_bp/modin/data_management/factories/dispatcher.py", line 97, in from_pandas
    return cls.__engine._from_pandas(df)
  File "/localdisk/dchigare/repos/modin_bp/modin/data_management/factories/factories.py", line 71, in _from_pandas
    return cls.io_cls.from_pandas(df)
  File "/localdisk/dchigare/repos/modin_bp/modin/engines/base/io/io.py", line 31, in from_pandas
    return cls.query_compiler_cls.from_pandas(df, cls.frame_cls)
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/backends/omnisci/query_compiler.py", line 42, in from_pandas
    return cls(data_cls.from_pandas(df))
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/engines/omnisci_on_ray/frame/data.py", line 1142, in from_pandas
    df = df.reset_index()
  File "/localdisk/dchigare/miniconda3/envs/modin_on_omnisci/lib/python3.7/site-packages/pandas/core/frame.py", line 4854, in reset_index
    new_obj.insert(0, name, level_values)
  File "/localdisk/dchigare/miniconda3/envs/modin_on_omnisci/lib/python3.7/site-packages/pandas/core/frame.py", line 3623, in insert
    self._mgr.insert(loc, column, value, allow_duplicates=allow_duplicates)
  File "/localdisk/dchigare/miniconda3/envs/modin_on_omnisci/lib/python3.7/site-packages/pandas/core/internals/managers.py", line 1177, in insert
    raise ValueError(f"cannot insert {item}, already exists")
ValueError: cannot insert __index__, already exists
```



</details>

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
The root cause seems to be here:
https://github.com/modin-project/modin/blob/f35074c1037b946ba663cabbd0ce9cad0a1567fe/modin/experimental/engines/omnisci_on_ray/frame/data.py#L1137-L1144

When we're processing multi-index we giving `__index__` name to all levels that unnamed, and after that doing `df.reset_index()` to insert all levels from the index as columns. But if we'll have more than one level that named like `__index__` then `reset_index` will throw an error that we're trying to insert columns with non-unique names.



Discussion:

Resolving patch:
diff --git a/modin/experimental/engines/omnisci_on_ray/frame/data.py b/modin/experimental/engines/omnisci_on_ray/frame/data.py
--- a/modin/experimental/engines/omnisci_on_ray/frame/data.py
+++ b/modin/experimental/engines/omnisci_on_ray/frame/data.py
@@ -45,6 +45,7 @@
 
 import numpy as np
 import pyarrow
+import re
 
 
 class OmnisciOnRayFrame(BasePandasFrame):
@@ -694,10 +695,7 @@ def sort_rows(self, columns, ascending, ignore_index, na_position):
 
         if not isinstance(columns, list):
             columns = [columns]
-
-        for col in columns:
-            if col not in self._table_cols:
-                raise ValueError(f"Unknown column '{col}'")
+        columns = [self._find_index_or_col(col) for col in columns]
 
         if isinstance(ascending, list):
             if len(ascending) != len(columns):
@@ -1055,11 +1053,24 @@ def reset_index(self, drop):
                 raise NotImplementedError(
                     "default index reset with no drop is not supported"
                 )
-            new_columns = Index.__new__(Index, data=self._table_cols, dtype="O")
+            # Need to demangle index names.
+            exprs = OrderedDict()
+            for i, c in enumerate(self._index_cols):
+                name = self._index_name(c)
+                if name is None:
+                    name = f"level_{i}"
+                if name in exprs:
+                    raise ValueError(f"cannot insert {name}, already exists")
+                exprs[name] = self.ref(c)
+            for c in self.columns:
+                if c in exprs:
+                    raise ValueError(f"cannot insert {c}, already exists")
+                exprs[c] = self.ref(c)
+            new_columns = Index.__new__(Index, data=exprs.keys(), dtype="O")
             return self.__constructor__(
                 columns=new_columns,
-                dtypes=self._dtypes_for_cols(None, new_columns),
-                op=self._op,
+                dtypes=self._dtypes_for_exprs(exprs),
+                op=TransformNode(self, exprs),
                 index_cols=None,
                 force_execution_mode=self._force_execution_mode,
             )
@@ -1118,10 +1129,30 @@ def _index_names(self, cols):
         return [self._index_name(n) for n in cols]
 
     def _index_name(self, col):
-        if col.startswith("__index__"):
+        if col == "__index__":
             return None
+
+        match = re.search("__index__\\d+_(.*)", col)
+        if match:
+            name = match.group(1)
+            if name == "__None__":
+                return None
+            return name
+
         return col
 
+    def _find_index_or_col(self, col):
+        """For given column or index name return a column name"""
+        if col in self.columns:
+            return col
+
+        if self._index_cols is not None:
+            for idx_col in self._index_cols:
+                if re.match(f"__index__\\d+_{col}", idx_col):
+                    return idx_col
+
+        raise ValueError(f"Unknown column '{col}'")
+
     @classmethod
     def from_pandas(cls, df):
         new_index = df.index
@@ -1137,7 +1168,10 @@ def from_pandas(cls, df):
             orig_index_names = df.index.names
             orig_df = df
 
-            index_cols = ["__index__" if n is None else n for n in df.index.names]
+            index_cols = [
+                f"__index__{i}_{'__None__' if n is None else n}"
+                for i, n in enumerate(df.index.names)
+            ]
             df.index.names = index_cols
             df = df.reset_index()
 


Test patch:
diff --git a/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py b/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
--- a/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
+++ b/modin/experimental/engines/omnisci_on_ray/test/test_dataframe.py
@@ -28,6 +28,7 @@
     test_data_values,
     test_data_keys,
     generate_multiindex,
+    eval_general,
 )
 
 
@@ -338,6 +339,43 @@ def filter(df, **kwargs):
         run_and_compare(filter, data=self.data)
 
 
+class TestMultiIndex:
+    data = {"a": np.arange(24), "b": np.arange(24)}
+
+    @pytest.mark.parametrize("names", [None, ["", ""], ["name", "name"]])
+    def test_dup_names(self, names):
+        index = pandas.MultiIndex.from_tuples(
+            [(i, j) for i in range(3) for j in range(8)], names=names
+        )
+
+        pandas_df = pandas.DataFrame(self.data, index=index) + 1
+        modin_df = pd.DataFrame(self.data, index=index) + 1
+
+        df_equals(pandas_df, modin_df)
+
+    @pytest.mark.parametrize(
+        "names",
+        [
+            None,
+            [None, "s", None],
+            ["i1", "i2", "i3"],
+            ["i1", "i1", "i3"],
+            ["i1", "i2", "a"],
+        ],
+    )
+    def test_reset_index(self, names):
+        index = pandas.MultiIndex.from_tuples(
+            [(i, j, k) for i in range(2) for j in range(3) for k in range(4)],
+            names=names,
+        )
+
+        def applier(lib):
+            df = lib.DataFrame(self.data, index=index) + 1
+            return df.reset_index()
+
+        eval_general(pd, pandas, applier)
+
+
 class TestFillna:
     data = {"a": [1, 1, None], "b": [None, None, 2], "c": [3, None, None]}
     values = [1, {"a": 1, "c": 3}, {"a": 1, "b": 2, "c": 3}]