Repository: modin-project/modin

Issue:
Behavior of `insert` function is different from pandas in case of numpy array with several columns
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: `Windows 10`
- **Modin version** (`modin.__version__`): f83e98bde759a74330a20c89437ce347e8a703ed
- **Python version**: `3.8.12`
- **Code we can use to reproduce**:
```python
import modin.pandas as pd
import pandas

df = pd.DataFrame({"col1": [1,2,3], "col2": [2,3,4]})
df2 = pd.DataFrame({"col3": [1,2,0], "col4": [2,3,0]}).to_numpy()
df.insert(0, "col3", df2)
print(df)

df = pandas.DataFrame({"col1": [1,2,3], "col2": [2,3,4]})
df2 = pandas.DataFrame({"col3": [1,2,0], "col4": [2,3,0]}).to_numpy()
df.insert(0, "col3", df2)
print(df)
```
<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
<!-- Describe the problem clearly here. -->

Behavior different from pandas.

### 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 'dict'> object. This may take some time.
     col3  col1  col2
0  [1, 2]     1     2
1  [2, 3]     2     3
2  [0, 0]     3     4
Traceback (most recent call last):
  File "test-4326.py", line 11, in <module>
    df.insert(0, "col3", df2)
  File "C:\Users\amyachev\Miniconda3\envs\modin\lib\site-packages\pandas\core\frame.py", line 4445, in insert 
    self._mgr.insert(loc, column, value)
  File "C:\Users\amyachev\Miniconda3\envs\modin\lib\site-packages\pandas\core\internals\managers.py", line 1235, in insert
    raise ValueError(
ValueError: Expected a 1D array, got an array with shape (3, 2)
```


Discussion:

Resolving patch:
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -1126,13 +1126,16 @@ def insert(self, loc, column, value, allow_duplicates=False):  # noqa: PR01, D20
         """
         Insert column into ``DataFrame`` at specified location.
         """
-        if isinstance(value, (DataFrame, pandas.DataFrame)):
-            if len(value.columns) != 1:
+        if (
+            isinstance(value, (DataFrame, pandas.DataFrame))
+            or isinstance(value, np.ndarray)
+            and len(value.shape) > 1
+        ):
+            if value.shape[1] != 1:
                 raise ValueError(
-                    f"Wrong number of items passed {len(value.columns)}, placement implies 1"
+                    f"Expected a 1D array, got an array with shape {value.shape}"
                 )
             value = value.squeeze(axis=1)
-
         if not self._query_compiler.lazy_execution and len(self.index) == 0:
             if not hasattr(value, "index"):
                 try:
@@ -1161,15 +1164,11 @@ def insert(self, loc, column, value, allow_duplicates=False):  # noqa: PR01, D20
                 raise ValueError("Length of values does not match length of index")
             if not allow_duplicates and column in self.columns:
                 raise ValueError(f"cannot insert {column}, already exists")
-            if loc > len(self.columns):
+            if not -len(self.columns) <= loc <= len(self.columns):
                 raise IndexError(
                     f"index {loc} is out of bounds for axis 0 with size {len(self.columns)}"
                 )
-            if loc < 0:
-                if loc < -len(self.columns):
-                    raise IndexError(
-                        f"index {loc} is out of bounds for axis 0 with size {len(self.columns)}"
-                    )
+            elif loc < 0:
                 raise ValueError("unbounded slice")
             if isinstance(value, Series):
                 value = value._query_compiler
@@ -2521,30 +2520,13 @@ def __setitem__(self, key, value):
 
         if hashable(key) and key not in self.columns:
             if isinstance(value, Series) and len(self.columns) == 0:
+                # Note: column information is lost when assigning a query compiler
+                prev_index = self.columns
                 self._query_compiler = value._query_compiler.copy()
                 # Now that the data is appended, we need to update the column name for
-                # that column to `key`, otherwise the name could be incorrect. Drop the
-                # last column name from the list (the appended value's name and append
-                # the new name.
-                self.columns = self.columns[:-1].append(pandas.Index([key]))
+                # that column to `key`, otherwise the name could be incorrect.
+                self.columns = prev_index.insert(0, key)
                 return
-            elif (
-                isinstance(value, (pandas.DataFrame, DataFrame)) and value.shape[1] != 1
-            ):
-                raise ValueError(
-                    "Wrong number of items passed %i, placement implies 1"
-                    % value.shape[1]
-                )
-            elif isinstance(value, np.ndarray) and len(value.shape) > 1:
-                if value.shape[1] == 1:
-                    # Transform into columnar table and take first column
-                    value = value.copy().T[0]
-                else:
-                    raise ValueError(
-                        "Wrong number of items passed %i, placement implies 1"
-                        % value.shape[1]
-                    )
-
             # Do new column assignment after error checks and possible value modifications
             self.insert(loc=len(self.columns), column=key, value=value)
             return


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
@@ -1755,6 +1755,20 @@ def applyier(df):
     eval_general(modin_df, pandas_df, applyier)
 
 
+def test_setitem_on_empty_df_4407():
+    data = {}
+    index = pd.date_range(end="1/1/2018", periods=0, freq="D")
+    column = pd.date_range(end="1/1/2018", periods=1, freq="h")[0]
+    modin_df = pd.DataFrame(data, columns=index)
+    pandas_df = pandas.DataFrame(data, columns=index)
+
+    modin_df[column] = pd.Series([1])
+    pandas_df[column] = pandas.Series([1])
+
+    df_equals(modin_df, pandas_df)
+    assert modin_df.columns.freq == pandas_df.columns.freq
+
+
 def test___setitem__unhashable_list():
     # from #3258 and #3291
     cols = ["a", "b"]
diff --git a/modin/pandas/test/dataframe/test_map_metadata.py b/modin/pandas/test/dataframe/test_map_metadata.py
--- a/modin/pandas/test/dataframe/test_map_metadata.py
+++ b/modin/pandas/test/dataframe/test_map_metadata.py
@@ -14,7 +14,7 @@
 import pytest
 import numpy as np
 import pandas
-from pandas.testing import assert_index_equal
+from pandas.testing import assert_index_equal, assert_series_equal
 import matplotlib
 import modin.pandas as pd
 from modin.utils import get_current_execution
@@ -1032,6 +1032,28 @@ def test_insert(data):
     )
 
 
+def test_insert_4407():
+    data = {"col1": [1, 2, 3], "col2": [2, 3, 4]}
+    modin_df, pandas_df = pd.DataFrame(data), pandas.DataFrame(data)
+
+    def comparator(df1, df2):
+        assert_series_equal(df1.dtypes, df2.dtypes, check_index=False)
+        return df_equals(df1, df2)
+
+    for idx, value in enumerate(
+        (pandas_df.to_numpy(), np.array([[1]] * 3), np.array([[1, 2, 3], [4, 5, 6]]))
+    ):
+        eval_insert(
+            modin_df,
+            pandas_df,
+            loc=0,
+            col=f"test_col{idx}",
+            value=value,
+            __inplace__=True,
+            comparator=lambda df1, df2: comparator(df1, df2),
+        )
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_ndim(data):
     modin_df = pd.DataFrame(data)