Repository: modin-project/modin

Issue:
AssertionError when calling head
### Describe the problem
When calling head on `df.loc[['col1', 'col3', 'col3']].head()`, an `AssertionError` gets thrown.

### Source code / logs
```
import modin.pandas as pd
from modin.pandas.test.utils import test_data
data = test_data['float_data']
modin_df = pd.DataFrame(data)
modin_df.loc[:,['col1', 'col3', 'col3']].head()
```
Traceback:
```
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-5-69e2ecd84881> in <module>
----> 1 modin_df.loc[:,['col1', 'col3', 'col3']].head()

~/Documents/modin/modin/pandas/dataframe.py in head(self, n)
   1793         if n >= len(self.index):
   1794             return self.copy()
-> 1795         return DataFrame(query_compiler=self._query_compiler.head(n))
   1796 
   1797     def hist(

~/Documents/modin/modin/data_management/query_compiler/pandas_query_compiler.py in head(self, n)
   2132         else:
   2133             result = self.__constructor__(
-> 2134                 self.data.take(0, n), self.index[:n], self.columns, self._dtype_cache
   2135             )
   2136         return result

~/Documents/modin/modin/data_management/query_compiler/pandas_query_compiler.py in __init__(self, block_partitions_object, index, columns, dtypes, index_map_series, columns_map_series)
   2816                 numeric index.
   2817         """
-> 2818         assert index_map_series is not None
   2819         assert columns_map_series is not None
   2820         assert index.equals(index_map_series.index)

AssertionError: 
```



Discussion:

Resolving patch:
diff --git a/modin/data_management/query_compiler/base_query_compiler.py b/modin/data_management/query_compiler/base_query_compiler.py
--- a/modin/data_management/query_compiler/base_query_compiler.py
+++ b/modin/data_management/query_compiler/base_query_compiler.py
@@ -1049,6 +1049,10 @@ def __init__(
         """
         raise NotImplementedError("Must be implemented in children classes")
 
+    @property
+    def __constructor__(self):
+        raise NotImplementedError("Must be implemented in children classes")
+
     _dtype_cache = None
 
     def _get_dtype(self):
@@ -1056,7 +1060,7 @@ def _get_dtype(self):
         raise NotImplementedError("Must be implemented in children classes")
 
     def _set_dtype(self, dtypes):
-        self._dtype_cache = dtypes
+        raise NotImplementedError("Must be implemented in children classes")
 
     dtypes = property(_get_dtype, _set_dtype)
 
@@ -1070,9 +1074,9 @@ def _get_data(self) -> BaseBlockPartitions:
 
     def _set_data(self, new_data):
         """Note this setter will be called by the
-            `super(PandasDataManagerView).__init__` function
+            `super(BaseQueryCompiler).__init__` function
         """
-        self.parent_data = new_data
+        raise NotImplementedError("Must be implemented in children classes")
 
     data = property(_get_data, _set_data)
 
@@ -1080,7 +1084,3 @@ def global_idx_to_numeric_idx(self, axis, indices):
         raise NotImplementedError("Must be implemented in children classes")
 
     # END Abstract functions for QueryCompilerView
-
-    @property
-    def __constructor__(self):
-        return type(self)
diff --git a/modin/data_management/query_compiler/pandas_query_compiler.py b/modin/data_management/query_compiler/pandas_query_compiler.py
--- a/modin/data_management/query_compiler/pandas_query_compiler.py
+++ b/modin/data_management/query_compiler/pandas_query_compiler.py
@@ -43,7 +43,20 @@ def __init__(
     _dtype_cache = None
 
     def _get_dtype(self):
+        calculate_dtype = False
         if self._dtype_cache is None:
+            calculate_dtype = True
+        else:
+            if len(self.columns) != len(self._dtype_cache):
+                if all(col in self._dtype_cache.index for col in self.columns):
+                    self._dtype_cache = pandas.Series(
+                        {col: self._dtype_cache[col] for col in self.columns}
+                    )
+                else:
+                    calculate_dtype = True
+            elif not self._dtype_cache.equals(self.columns):
+                self._dtype_cache.index = self.columns
+        if calculate_dtype:
             map_func = self._prepare_method(lambda df: df.dtypes)
 
             def dtype_builder(df):
@@ -51,8 +64,6 @@ def dtype_builder(df):
 
             self._dtype_cache = self._full_reduce(0, map_func, dtype_builder)
             self._dtype_cache.index = self.columns
-        elif not self._dtype_cache.index.equals(self.columns):
-            self._dtype_cache.index = self.columns
         return self._dtype_cache
 
     def _set_dtype(self, dtypes):
@@ -452,11 +463,14 @@ def to_pandas(self):
         """
         df = self.data.to_pandas(is_transposed=self._is_transposed)
         if df.empty:
-            dtype_dict = {
-                col_name: pandas.Series(dtype=self.dtypes[col_name])
-                for col_name in self.columns
-            }
-            df = pandas.DataFrame(dtype_dict, self.index)
+            if len(self.columns) != 0:
+                data = [
+                    pandas.Series(dtype=self.dtypes[col_name], name=col_name)
+                    for col_name in self.columns
+                ]
+                df = pandas.concat(data, axis=1)
+            else:
+                df = pandas.DataFrame(index=self.index)
         else:
             ErrorMessage.catch_bugs_and_request_email(
                 len(df.index) != len(self.index) or len(df.columns) != len(self.columns)
@@ -2790,7 +2804,7 @@ def enlarge_partitions(self, new_row_labels=None, new_col_labels=None):
 
 class PandasQueryCompilerView(PandasQueryCompiler):
     """
-    This class represent a view of the PandasDataManager
+    This class represent a view of the PandasQueryCompiler
 
     In particular, the following constraints are broken:
     - (len(self.index), len(self.columns)) != self.data.shape
@@ -2827,6 +2841,11 @@ def __init__(
             self, block_partitions_object, index, columns, dtypes
         )
 
+    @property
+    def __constructor__(self):
+        """Return parent object when getting the constructor."""
+        return PandasQueryCompiler
+
     _dtype_cache = None
 
     def _get_dtype(self):
@@ -2869,7 +2888,7 @@ def iloc(partition, row_internal_indices, col_internal_indices):
 
     def _set_data(self, new_data):
         """Note this setter will be called by the
-            `super(PandasDataManagerView).__init__` function
+            `super(PandasQueryCompiler).__init__` function
         """
         self.parent_data = new_data
 


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
@@ -2294,11 +2294,16 @@ def test_get_values():
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 @pytest.mark.parametrize("n", int_arg_values, ids=arg_keys("n", int_arg_keys))
 def test_head(data, n):
+    # Test normal dataframe head
     modin_df = pd.DataFrame(data)
     pandas_df = pandas.DataFrame(data)
-
     df_equals(modin_df.head(n), pandas_df.head(n))
 
+    # Test head when we call it from a QueryCompilerView
+    modin_result = modin_df.loc[:, ["col1", "col3", "col3"]].head(n)
+    pandas_result = pandas_df.loc[:, ["col1", "col3", "col3"]].head(n)
+    df_equals(modin_result, pandas_result)
+
 
 def test_hist():
     data = test_data_values[0]