Repository: modin-project/modin (python)

Issue:
PERF: add_suffix and add_prefix shouldn't map partitions
### Describe the problem
`add_suffix` and `add_prefix` call the modin frame's `rename`, which [maps all blocks](https://github.com/modin-project/modin/blob/49f47b1ed66583d4b90d3c06faf3e1d457cf4df8/modin/core/dataframe/pandas/dataframe/dataframe.py#L1719). Instead, we can just change the names of the indices entire pandas object, and defer the index renaming in the partitions till later.

Pull request description:
FIX-#4355: Fix rename algebraic operator to avoid copying
Resolves #4355

Signed-off-by: Devin Petersohn <devin.petersohn@gmail.com>

<!--
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] commit message follows format outlined [here](https://modin.readthedocs.io/en/latest/development/contributing.html#commit-message-formatting)
- [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 #4355 <!-- issue must be created for each patch -->
- [x] tests added and 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 -->


Patch:
diff --git a/modin/core/dataframe/base/dataframe/dataframe.py b/modin/core/dataframe/base/dataframe/dataframe.py
index 4567f23c..7fbf776f 100644
--- a/modin/core/dataframe/base/dataframe/dataframe.py
+++ b/modin/core/dataframe/base/dataframe/dataframe.py
@@ -478,7 +478,6 @@ class ModinDataframe(ABC):
         self,
         new_row_labels: Optional[Union[Dict[Hashable, Hashable], Callable]] = None,
         new_col_labels: Optional[Union[Dict[Hashable, Hashable], Callable]] = None,
-        level: Optional[Union[int, List[int]]] = None,
     ) -> "ModinDataframe":
         """
         Replace the row and column labels with the specified new labels.
@@ -489,17 +488,11 @@ class ModinDataframe(ABC):
             Mapping or callable that relates old row labels to new labels.
         new_col_labels : dictionary or callable, optional
             Mapping or callable that relates old col labels to new labels.
-        level : int or list of ints, optional
-            Level(s) whose row labels to replace.
 
         Returns
         -------
         ModinDataframe
             A new ModinDataframe with the new row and column labels.
-
-        Notes
-        -----
-        If level is not specified, the default behavior is to replace row labels in all levels.
         """
         pass
 
diff --git a/modin/core/dataframe/pandas/dataframe/dataframe.py b/modin/core/dataframe/pandas/dataframe/dataframe.py
index 4be07569..7676e5ea 100644
--- a/modin/core/dataframe/pandas/dataframe/dataframe.py
+++ b/modin/core/dataframe/pandas/dataframe/dataframe.py
@@ -1610,57 +1610,6 @@ class PandasDataframe(ClassLogger):
             new_dtypes,
         )
 
-    # Metadata modification methods
-    def add_prefix(self, prefix, axis):
-        """
-        Add a prefix to the current row or column labels.
-
-        Parameters
-        ----------
-        prefix : str
-            The prefix to add.
-        axis : int
-            The axis to update.
-
-        Returns
-        -------
-        PandasDataframe
-            A new dataframe with the updated labels.
-        """
-
-        def new_labels_mapper(x, prefix=str(prefix)):
-            return prefix + str(x)
-
-        if axis == 0:
-            return self.rename(new_row_labels=new_labels_mapper)
-        return self.rename(new_col_labels=new_labels_mapper)
-
-    def add_suffix(self, suffix, axis):
-        """
-        Add a suffix to the current row or column labels.
-
-        Parameters
-        ----------
-        suffix : str
-            The suffix to add.
-        axis : int
-            The axis to update.
-
-        Returns
-        -------
-        PandasDataframe
-            A new dataframe with the updated labels.
-        """
-
-        def new_labels_mapper(x, suffix=str(suffix)):
-            return str(x) + suffix
-
-        if axis == 0:
-            return self.rename(new_row_labels=new_labels_mapper)
-        return self.rename(new_col_labels=new_labels_mapper)
-
-    # END Metadata modification methods
-
     def numeric_columns(self, include_bool=True):
         """
         Return the names of numeric columns in the frame.
@@ -2323,7 +2272,6 @@ class PandasDataframe(ClassLogger):
         self,
         new_row_labels: Optional[Union[Dict[Hashable, Hashable], Callable]] = None,
         new_col_labels: Optional[Union[Dict[Hashable, Hashable], Callable]] = None,
-        level: Optional[Union[int, List[int]]] = None,
     ) -> "PandasDataframe":
         """
         Replace the row and column labels with the specified new labels.
@@ -2334,60 +2282,22 @@ class PandasDataframe(ClassLogger):
             Mapping or callable that relates old row labels to new labels.
         new_col_labels : dictionary or callable, optional
             Mapping or callable that relates old col labels to new labels.
-        level : int, optional
-            Level whose row labels to replace.
 
         Returns
         -------
         PandasDataframe
             A new PandasDataframe with the new row and column labels.
-
-        Notes
-        -----
-        If level is not specified, the default behavior is to replace row labels in all levels.
         """
-        new_index = self.index.copy()
-
-        def make_label_swapper(label_dict):
-            if isinstance(label_dict, dict):
-                return lambda label: label_dict.get(label, label)
-            return label_dict
-
-        def swap_labels_levels(index_tuple):
-            if isinstance(new_row_labels, dict):
-                return tuple(new_row_labels.get(label, label) for label in index_tuple)
-            return tuple(new_row_labels(label) for label in index_tuple)
-
-        if new_row_labels:
-            swap_row_labels = make_label_swapper(new_row_labels)
-            if isinstance(self.index, pandas.MultiIndex):
-                if level is not None:
-                    new_index.set_levels(
-                        new_index.levels[level].map(swap_row_labels), level
-                    )
-                else:
-                    new_index = new_index.map(swap_labels_levels)
-            else:
-                new_index = new_index.map(swap_row_labels)
-        new_cols = self.columns.copy()
-        if new_col_labels:
-            new_cols = new_cols.map(make_label_swapper(new_col_labels))
-
-        def map_fn(df):
-            return df.rename(index=new_row_labels, columns=new_col_labels, level=level)
-
-        new_parts = self._partition_mgr_cls.map_partitions(self._partitions, map_fn)
-        new_dtypes = None
-        if self.has_materialized_dtypes:
-            new_dtypes = self.dtypes.set_axis(new_cols)
-        return self.__constructor__(
-            new_parts,
-            new_index,
-            new_cols,
-            self._row_lengths_cache,
-            self._column_widths_cache,
-            new_dtypes,
-        )
+        result = self.copy()
+        if new_row_labels is not None:
+            if callable(new_row_labels):
+                new_row_labels = result.index.map(new_row_labels)
+            result.index = new_row_labels
+        if new_col_labels is not None:
+            if callable(new_col_labels):
+                new_col_labels = result.columns.map(new_col_labels)
+            result.columns = new_col_labels
+        return result
 
     def combine_and_apply(
         self, func, new_index=None, new_columns=None, new_dtypes=None
diff --git a/modin/core/dataframe/pandas/partitioning/axis_partition.py b/modin/core/dataframe/pandas/partitioning/axis_partition.py
index 30ed90c4..bfc94605 100644
--- a/modin/core/dataframe/pandas/partitioning/axis_partition.py
+++ b/modin/core/dataframe/pandas/partitioning/axis_partition.py
@@ -422,7 +422,13 @@ class PandasDataframeAxisPartition(BaseDataframeAxisPartition):
         dataframe = pandas.concat(list(partitions), axis=axis, copy=False)
         with warnings.catch_warnings():
             warnings.filterwarnings("ignore", category=FutureWarning)
-            result = func(dataframe, *f_args, **f_kwargs)
+            try:
+                result = func(dataframe, *f_args, **f_kwargs)
+            except ValueError as err:
+                if "assignment destination is read-only" in str(err):
+                    result = func(dataframe.copy(), *f_args, **f_kwargs)
+                else:
+                    raise err
 
         if num_splits == 1:
             # If we're not going to split the result, we don't need to specify
diff --git a/modin/core/dataframe/pandas/partitioning/partition.py b/modin/core/dataframe/pandas/partitioning/partition.py
index 38cc41ca..224c40a1 100644
--- a/modin/core/dataframe/pandas/partitioning/partition.py
+++ b/modin/core/dataframe/pandas/partitioning/partition.py
@@ -209,7 +209,7 @@ class PandasDataframePartition(ABC):  # pragma: no cover
         If the underlying object is a pandas DataFrame, this will return
         a 2D NumPy array.
         """
-        return self.apply(lambda df, **kwargs: df.to_numpy(**kwargs)).get()
+        return self.apply(lambda df: df.to_numpy(**kwargs)).get()
 
     @staticmethod
     def _iloc(df, row_labels, col_labels):  # noqa: RT01, PR01
diff --git a/modin/core/execution/ray/generic/partitioning/partition_manager.py b/modin/core/execution/ray/generic/partitioning/partition_manager.py
index f5795d07..d4b61b25 100644
--- a/modin/core/execution/ray/generic/partitioning/partition_manager.py
+++ b/modin/core/execution/ray/generic/partitioning/partition_manager.py
@@ -42,7 +42,7 @@ class GenericRayDataframePartitionManager(PandasDataframePartitionManager):
         """
         if partitions.shape[1] == 1:
             parts = cls.get_objects_from_partitions(partitions.flatten())
-            parts = [part.to_numpy() for part in parts]
+            parts = [part.to_numpy(**kwargs) for part in parts]
         else:
             parts = RayWrapper.materialize(
                 [
diff --git a/modin/core/execution/unidist/generic/partitioning/partition_manager.py b/modin/core/execution/unidist/generic/partitioning/partition_manager.py
index 666152a0..8052c5cc 100644
--- a/modin/core/execution/unidist/generic/partitioning/partition_manager.py
+++ b/modin/core/execution/unidist/generic/partitioning/partition_manager.py
@@ -42,7 +42,7 @@ class GenericUnidistDataframePartitionManager(PandasDataframePartitionManager):
         """
         if partitions.shape[1] == 1:
             parts = cls.get_objects_from_partitions(partitions.flatten())
-            parts = [part.to_numpy() for part in parts]
+            parts = [part.to_numpy(**kwargs) for part in parts]
         else:
             parts = UnidistWrapper.materialize(
                 [
diff --git a/modin/core/storage_formats/pandas/query_compiler.py b/modin/core/storage_formats/pandas/query_compiler.py
index 373d0d87..80027ec1 100644
--- a/modin/core/storage_formats/pandas/query_compiler.py
+++ b/modin/core/storage_formats/pandas/query_compiler.py
@@ -326,10 +326,24 @@ class PandasQueryCompiler(BaseQueryCompiler):
 
     # Metadata modification methods
     def add_prefix(self, prefix, axis=1):
-        return self.__constructor__(self._modin_frame.add_prefix(prefix, axis))
+        if axis == 1:
+            return self.__constructor__(
+                self._modin_frame.rename(new_col_labels=lambda x: f"{prefix}{x}")
+            )
+        else:
+            return self.__constructor__(
+                self._modin_frame.rename(new_row_labels=lambda x: f"{prefix}{x}")
+            )
 
     def add_suffix(self, suffix, axis=1):
-        return self.__constructor__(self._modin_frame.add_suffix(suffix, axis))
+        if axis == 1:
+            return self.__constructor__(
+                self._modin_frame.rename(new_col_labels=lambda x: f"{x}{suffix}")
+            )
+        else:
+            return self.__constructor__(
+                self._modin_frame.rename(new_row_labels=lambda x: f"{x}{suffix}")
+            )
 
     # END Metadata modification methods
 


Test patch:
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
index 1b2c5eb0..e64e1371 100644
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -1474,6 +1474,10 @@ def test_cumsum(data, skipna):
         df_equals(modin_series.cumsum(skipna=skipna), pandas_result)
 
 
+def test_cumsum_6771():
+    _ = to_pandas(pd.Series([1, 2, 3], dtype="Int64").cumsum())
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_describe(data):
     modin_series, pandas_series = create_test_series(data)
@@ -3475,6 +3479,15 @@ def test_to_numpy(data):
     assert_array_equal(modin_series.to_numpy(), pandas_series.to_numpy())
 
 
+def test_to_numpy_dtype():
+    modin_series, pandas_series = create_test_series(test_data["float_nan_data"])
+    assert_array_equal(
+        modin_series.to_numpy(dtype="int64"),
+        pandas_series.to_numpy(dtype="int64"),
+        strict=True,
+    )
+
+
 @pytest.mark.parametrize(
     "data",
     test_data_values + test_data_large_categorical_series_values,