Repository: pandas-dev/pandas
Pull request #58018 (2024-03-26 19:29:43)

PERF: Allow Index.to_frame to return RangeIndex columns
Discovered in https://github.com/pandas-dev/pandas/pull/57441


Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 26dd6f83ad..e798f19bc1 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -299,6 +299,7 @@ Performance improvements
 - Performance improvement in :meth:`DataFrameGroupBy.ffill`, :meth:`DataFrameGroupBy.bfill`, :meth:`SeriesGroupBy.ffill`, and :meth:`SeriesGroupBy.bfill` (:issue:`56902`)
 - Performance improvement in :meth:`Index.join` by propagating cached attributes in cases where the result matches one of the inputs (:issue:`57023`)
 - Performance improvement in :meth:`Index.take` when ``indices`` is a full range indexer from zero to length of index (:issue:`56806`)
+- Performance improvement in :meth:`Index.to_frame` returning a :class:`RangeIndex` columns of a :class:`Index` when possible. (:issue:`58018`)
 - Performance improvement in :meth:`MultiIndex.equals` for equal length indexes (:issue:`56990`)
 - Performance improvement in :meth:`RangeIndex.__getitem__` with a boolean mask or integers returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57588`)
 - Performance improvement in :meth:`RangeIndex.append` when appending the same index (:issue:`57252`)
@@ -320,6 +321,7 @@ Bug fixes
 ~~~~~~~~~
 - Fixed bug in :class:`SparseDtype` for equal comparison with na fill value. (:issue:`54770`)
 - Fixed bug in :meth:`.DataFrameGroupBy.median` where nat values gave an incorrect result. (:issue:`57926`)
+- Fixed bug in :meth:`DataFrame.cumsum` which was raising ``IndexError`` if dtype is ``timedelta64[ns]`` (:issue:`57956`)
 - Fixed bug in :meth:`DataFrame.join` inconsistently setting result index name (:issue:`55815`)
 - Fixed bug in :meth:`DataFrame.to_string` that raised ``StopIteration`` with nested DataFrames. (:issue:`16098`)
 - Fixed bug in :meth:`DataFrame.update` bool dtype being converted to object (:issue:`55509`)
diff --git a/pandas/core/array_algos/datetimelike_accumulations.py b/pandas/core/array_algos/datetimelike_accumulations.py
index 55942f2c93..c3a7c2e4fe 100644
--- a/pandas/core/array_algos/datetimelike_accumulations.py
+++ b/pandas/core/array_algos/datetimelike_accumulations.py
@@ -49,7 +49,8 @@ def _cum_func(
     if not skipna:
         mask = np.maximum.accumulate(mask)
 
-    result = func(y)
+    # GH 57956
+    result = func(y, axis=0)
     result[mask] = iNaT
 
     if values.dtype.kind in "mM":
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index 76dd19a942..e510d487ac 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -1374,16 +1374,19 @@ class Index(IndexOpsMixin, PandasObject):
         return attrs
 
     @final
-    def _get_level_names(self) -> Hashable | Sequence[Hashable]:
+    def _get_level_names(self) -> range | Sequence[Hashable]:
         """
         Return a name or list of names with None replaced by the level number.
         """
         if self._is_multi:
-            return [
-                level if name is None else name for level, name in enumerate(self.names)
-            ]
+            return maybe_sequence_to_range(
+                [
+                    level if name is None else name
+                    for level, name in enumerate(self.names)
+                ]
+            )
         else:
-            return 0 if self.name is None else self.name
+            return range(1) if self.name is None else [self.name]
 
     @final
     def _mpl_repr(self) -> np.ndarray:
@@ -1630,8 +1633,11 @@ class Index(IndexOpsMixin, PandasObject):
         from pandas import DataFrame
 
         if name is lib.no_default:
-            name = self._get_level_names()
-        result = DataFrame({name: self}, copy=False)
+            result_name = self._get_level_names()
+        else:
+            result_name = Index([name])  # type: ignore[assignment]
+        result = DataFrame(self, copy=False)
+        result.columns = result_name
 
         if index:
             result.index = self


Test patch:
diff --git a/pandas/tests/indexes/multi/test_conversion.py b/pandas/tests/indexes/multi/test_conversion.py
index 3c2ca045d6..f6b10c9893 100644
--- a/pandas/tests/indexes/multi/test_conversion.py
+++ b/pandas/tests/indexes/multi/test_conversion.py
@@ -5,6 +5,7 @@ import pandas as pd
 from pandas import (
     DataFrame,
     MultiIndex,
+    RangeIndex,
 )
 import pandas._testing as tm
 
@@ -148,6 +149,13 @@ def test_to_frame_duplicate_labels():
     tm.assert_frame_equal(result, expected)
 
 
+def test_to_frame_column_rangeindex():
+    mi = MultiIndex.from_arrays([[1, 2], ["a", "b"]])
+    result = mi.to_frame().columns
+    expected = RangeIndex(2)
+    tm.assert_index_equal(result, expected, exact=True)
+
+
 def test_to_flat_index(idx):
     expected = pd.Index(
         (
diff --git a/pandas/tests/indexes/test_common.py b/pandas/tests/indexes/test_common.py
index eb0010066a..a2dee61295 100644
--- a/pandas/tests/indexes/test_common.py
+++ b/pandas/tests/indexes/test_common.py
@@ -508,3 +508,17 @@ def test_compare_read_only_array():
     idx = pd.Index(arr)
     result = idx > 69
     assert result.dtype == bool
+
+
+def test_to_frame_column_rangeindex():
+    idx = pd.Index([1])
+    result = idx.to_frame().columns
+    expected = RangeIndex(1)
+    tm.assert_index_equal(result, expected, exact=True)
+
+
+def test_to_frame_name_tuple_multiindex():
+    idx = pd.Index([1])
+    result = idx.to_frame(name=(1, 2))
+    expected = pd.DataFrame([1], columns=MultiIndex.from_arrays([[1], [2]]), index=idx)
+    tm.assert_frame_equal(result, expected)
diff --git a/pandas/tests/series/test_cumulative.py b/pandas/tests/series/test_cumulative.py
index 68d7fd8b90..9b7b08127a 100644
--- a/pandas/tests/series/test_cumulative.py
+++ b/pandas/tests/series/test_cumulative.py
@@ -91,6 +91,25 @@ class TestSeriesCumulativeOps:
         result = getattr(ser, method)(skipna=skipna)
         tm.assert_series_equal(expected, result)
 
+    def test_cumsum_datetimelike(self):
+        # GH#57956
+        df = pd.DataFrame(
+            [
+                [pd.Timedelta(0), pd.Timedelta(days=1)],
+                [pd.Timedelta(days=2), pd.NaT],
+                [pd.Timedelta(hours=-6), pd.Timedelta(hours=12)],
+            ]
+        )
+        result = df.cumsum()
+        expected = pd.DataFrame(
+            [
+                [pd.Timedelta(0), pd.Timedelta(days=1)],
+                [pd.Timedelta(days=2), pd.NaT],
+                [pd.Timedelta(days=1, hours=18), pd.Timedelta(days=1, hours=12)],
+            ]
+        )
+        tm.assert_frame_equal(result, expected)
+
     @pytest.mark.parametrize(
         "func, exp",
         [