Repository: modin-project/modin

Issue:
`QueryCompiler.columnarize` triggers unnecessary frame execution
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: Any
- **Modin version** (`modin.__version__`): f9c99c3817d35f7bf0d91df9aa9644545ea27517
- **Python version**: 3.7.5
- **Code we can use to reproduce**:
```python
import modin.pandas as pd
from modin.experimental.engines.omnisci_on_ray.test.test_dataframe import (
    set_execution_mode,
)

df = pd.DataFrame({"a": [1, 1, 1, 2, 3], "b": [1, 2, 3, 4, 5]})
set_execution_mode(df, "lazy")

res = df.groupby("a").sum()
res.squeeze(axis=1)  # RuntimeError: unexpected execution triggered on lazy frame
```

<details><summary>Traceback</summary>

```
Traceback (most recent call last):
  File "repr.py", line 14, in <module>
    res.squeeze(axis=1)
  File "/localdisk/dchigare/repos/modin_bp/modin/pandas/dataframe.py", line 1938, in squeeze
    return Series(query_compiler=self._query_compiler)
  File "/localdisk/dchigare/repos/modin_bp/modin/pandas/series.py", line 160, in __init__
    self._query_compiler = query_compiler.columnarize()
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/backends/omnisci/query_compiler.py", line 46, in method_wrapper
    return method(self, *args, **kwargs)
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/backends/omnisci/query_compiler.py", line 624, in columnarize
    len(self.index) == 1 and self.index[0] == "__reduced__"
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/backends/omnisci/query_compiler.py", line 336, in _get_index
    return self._modin_frame.index
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/engines/omnisci_on_ray/frame/data.py", line 1170, in _get_index
    self._execute()
  File "/localdisk/dchigare/repos/modin_bp/modin/experimental/engines/omnisci_on_ray/frame/data.py", line 1024, in _execute
    raise RuntimeError("unexpected execution triggered on lazy frame")
RuntimeError: unexpected execution triggered on lazy frame
```

</details>

<!--
You can obtain the Modin version with

python -c "import modin; print(modin.__version__)"
-->

### Describe the problem
`columnarize` transposes single-row query compilers in order to translate it to the proper Series representation (single-column qc). To determine the shape of the qc without triggering the actual computation, `columnarize` [uses `_shape_hint`](https://github.com/modin-project/modin/blob/f9c99c3817d35f7bf0d91df9aa9644545ea27517/modin/experimental/backends/omnisci/query_compiler.py#L611) field of the self, it appears that sometimes the `shape_hint` is missing (like in the single-column groupby case) and so unnecessary shape computations are triggered.



Discussion:

Resolving patch:
diff --git a/modin/experimental/backends/omnisci/query_compiler.py b/modin/experimental/backends/omnisci/query_compiler.py
--- a/modin/experimental/backends/omnisci/query_compiler.py
+++ b/modin/experimental/backends/omnisci/query_compiler.py
@@ -86,6 +86,8 @@ class DFAlgQueryCompiler(BaseQueryCompiler):
     def __init__(self, frame, shape_hint=None):
         assert frame is not None
         self._modin_frame = frame
+        if shape_hint is None and len(self._modin_frame.columns) == 1:
+            shape_hint = "column"
         self._shape_hint = shape_hint
 
     def finalize():
@@ -97,11 +99,23 @@ def to_pandas(self):
 
     @classmethod
     def from_pandas(cls, df, data_cls):
-        return cls(data_cls.from_pandas(df))
+        if len(df.columns) == 1:
+            shape_hint = "column"
+        elif len(df) == 1:
+            shape_hint = "row"
+        else:
+            shape_hint = None
+        return cls(data_cls.from_pandas(df), shape_hint=shape_hint)
 
     @classmethod
     def from_arrow(cls, at, data_cls):
-        return cls(data_cls.from_arrow(at))
+        if len(at.columns) == 1:
+            shape_hint = "column"
+        elif len(at) == 1:
+            shape_hint = "row"
+        else:
+            shape_hint = None
+        return cls(data_cls.from_arrow(at), shape_hint=shape_hint)
 
     default_to_pandas = PandasQueryCompiler.default_to_pandas
 


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
@@ -714,6 +714,17 @@ def groupby(df, *args, **kwargs):
 
         run_and_compare(groupby, data=self.data, constructor_kwargs={"index": index})
 
+    def test_groupby_lazy_squeeze(self):
+        def applier(df, **kwargs):
+            return df.groupby("a").sum().squeeze(axis=1)
+
+        run_and_compare(
+            applier,
+            data=self.data,
+            constructor_kwargs={"columns": ["a", "b"]},
+            force_lazy=True,
+        )
+
     taxi_data = {
         "a": [1, 1, 2, 2],
         "b": [11, 21, 12, 11],
@@ -1757,5 +1768,46 @@ def test_unsupported_columns(self, data, is_good):
             assert not obj and bad_cols == ["col"]
 
 
+class TestConstructor:
+    @pytest.mark.parametrize(
+        "index",
+        [
+            None,
+            pandas.Index([1, 2, 3]),
+            pandas.MultiIndex.from_tuples([(1, 1), (2, 2), (3, 3)]),
+        ],
+    )
+    def test_shape_hint_detection(self, index):
+        df = pd.DataFrame({"a": [1, 2, 3]}, index=index)
+        assert df._query_compiler._shape_hint == "column"
+
+        transposed_data = df._to_pandas().T.to_dict()
+        df = pd.DataFrame(transposed_data)
+        assert df._query_compiler._shape_hint == "row"
+
+        df = pd.DataFrame({"a": [1, 2, 3], "b": [1, 2, 3]}, index=index)
+        assert df._query_compiler._shape_hint is None
+
+        df = pd.DataFrame({"a": [1]}, index=None if index is None else index[:1])
+        assert df._query_compiler._shape_hint == "column"
+
+    def test_shape_hint_detection_from_arrow(self):
+        at = pyarrow.Table.from_pydict({"a": [1, 2, 3]})
+        df = pd.utils.from_arrow(at)
+        assert df._query_compiler._shape_hint == "column"
+
+        at = pyarrow.Table.from_pydict({"a": [1], "b": [2], "c": [3]})
+        df = pd.utils.from_arrow(at)
+        assert df._query_compiler._shape_hint == "row"
+
+        at = pyarrow.Table.from_pydict({"a": [1, 2, 3], "b": [1, 2, 3]})
+        df = pd.utils.from_arrow(at)
+        assert df._query_compiler._shape_hint is None
+
+        at = pyarrow.Table.from_pydict({"a": [1]})
+        df = pd.utils.from_arrow(at)
+        assert df._query_compiler._shape_hint == "column"
+
+
 if __name__ == "__main__":
     pytest.main(["-v", __file__])