Repository: modin-project/modin
Pull request: PERF-#5691: Set item via `.loc` without converting a Series to np.array
<!--
Thank you for your contribution!
Please review the contributing docs: https://modin.readthedocs.io/en/latest/development/contributing.html
if you have questions about contributing.
-->

## What do these changes do?

<!-- Please give a short brief about these changes. -->

- [x] first commit message and PR title follow format outlined [here](https://modin.readthedocs.io/en/latest/development/contributing.html#commit-message-formatting)
  > **_NOTE:_**  If you edit the PR title to match this format, you need to add another commit (even if it's empty) or amend your last commit for the CI job that checks the PR title to pick up the new PR title.
- [x] passes `flake8 modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] passes `black --check modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] signed commit with `git commit -s` <!-- you can amend your commit with a signature via `git commit -amend -s` -->
- [x] Resolves #5691  <!-- issue must be created for each patch -->
- [x] tests passing
- [x] module layout described at `docs/development/architecture.rst` is up-to-date <!-- if you have added, renamed or removed files or directories please update the documentation accordingly -->


Problem statement:
Do not convert a Series to numpy in case setting a value via `loc` when the row locator is a Series or booleans
```python
import modin.pandas as pd

df = pd.DataFrame({"a": [1,2,3], "b": [4,5,6], "c": [7,8,9]})
df.loc[df["a"] == 1, "b"] = 555
```

`df["a"] == 1` is converting to a numpy array, which can be too expensive when the length of a Series is large.

Hints from issue discussion:

Commit message: Add the method to base qc

Signed-off-by: Igoshev, Iaroslav <iaroslav.igoshev@intel.com>

Patch to review:
diff --git a/modin/core/storage_formats/base/query_compiler.py b/modin/core/storage_formats/base/query_compiler.py
index e0aa7093e9b..c203ffac469 100644
--- a/modin/core/storage_formats/base/query_compiler.py
+++ b/modin/core/storage_formats/base/query_compiler.py
@@ -2384,6 +2384,40 @@ def inserter(df, loc, column, value):
 
     # END Abstract insert
 
+    # __setitem__ methods
+    def setitem_bool(self, row_loc, col_loc, item):
+        """
+        Set an item to the given location based on `row_loc` and `col_loc`.
+
+        Parameters
+        ----------
+        row_loc : BaseQueryCompiler
+            Query Compiler holding a Series of booleans.
+        col_loc : label
+            Column label in `self`.
+        item : scalar
+            An item to be set.
+
+        Returns
+        -------
+        BaseQueryCompiler
+            New QueryCompiler with the inserted item.
+
+        Notes
+        -----
+        Currently, this method is only used to set a scalar to the given location.
+        """
+
+        def _set_item(df, row_loc, col_loc, item):
+            df.loc[row_loc.squeeze(axis=1), col_loc] = item
+            return df
+
+        return DataFrameDefault.register(_set_item)(
+            self, row_loc=row_loc, col_loc=col_loc, item=item
+        )
+
+    # END __setitem__ methods
+
     # Abstract drop
     def drop(self, index=None, columns=None, errors: str = "raise"):
         """
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
index f76bfbecc6e..988c4d5ddd4 100644
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -2251,6 +2251,25 @@ def applyier(df, internal_indices, other=[], internal_other_indices=[]):
         labels="drop",
     )
 
+    # __setitem__ methods
+    def setitem_bool(self, row_loc, col_loc, item):
+        def _set_item(df, row_loc):
+            df = df.copy()
+            df.loc[row_loc.squeeze(axis=1), col_loc] = item
+            return df
+
+        new_modin_frame = self._modin_frame.broadcast_apply_full_axis(
+            axis=1,
+            func=_set_item,
+            other=row_loc._modin_frame,
+            new_index=self._modin_frame._index_cache,
+            new_columns=self._modin_frame._columns_cache,
+            keep_partitioning=False,
+        )
+        return self.__constructor__(new_modin_frame, shape_hint="column")
+
+    # END __setitem__ methods
+
     def __validate_bool_indexer(self, indexer):
         if len(indexer) != len(self.index):
             raise ValueError(
diff --git a/modin/pandas/indexing.py b/modin/pandas/indexing.py
index 1ce87589ab9..6b5e71eeb95 100644
--- a/modin/pandas/indexing.py
+++ b/modin/pandas/indexing.py
@@ -813,6 +813,17 @@ def _set_item_existing_loc(self, row_loc, col_loc, item):
         item : modin.pandas.DataFrame, modin.pandas.Series or scalar
             Value that should be assigned to located dataset.
         """
+        if (
+            isinstance(row_loc, Series)
+            and is_boolean_array(row_loc)
+            and is_scalar(item)
+        ):
+            new_qc = self.df._query_compiler.setitem_bool(
+                row_loc._query_compiler, col_loc, item
+            )
+            self.df._update_inplace(new_qc)
+            return
+
         row_lookup, col_lookup = self.qc.get_positions_from_labels(row_loc, col_loc)
         self._setitem_positional(
             row_lookup,


Code review comments:
diff_hunk: @@ -2251,6 +2251,25 @@ def applyier(df, internal_indices, other=[], internal_other_indices=[]):
         labels="drop",
     )
 
+    # __setitem__ methods
+    def setitem_bool(self, row_loc, col_loc, item):
+        def _set_item(df, row_loc):
+            df = df.copy()
+            df.loc[row_loc.squeeze(axis=1), col_loc] = item
+            return df
+
+        new_modin_frame = self._modin_frame.broadcast_apply_full_axis(
+            axis=1,
+            func=_set_item,
+            other=row_loc._modin_frame,
+            new_index=self._modin_frame._index_cache,
+            new_columns=self._modin_frame._columns_cache,
+            keep_partitioning=False,
+        )
+        return self.__constructor__(new_modin_frame, shape_hint="column")
path: modin/core/storage_formats/pandas/query_compiler.py
text: @author:
```suggestion
        return self.__constructor__(new_modin_frame)
```
No need for this.