From 35c9d47380d6247d849f4f63c599b8b2b198a72f Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:34:30 -0700 Subject: [PATCH 1/4] PERF: RangeIndex.round returns RangeIndex when possible --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/indexes/base.py | 1 - pandas/core/indexes/range.py | 35 +++++++++++++++++++++++ pandas/tests/indexes/ranges/test_range.py | 31 ++++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index e43f6fdf9c173..70a5e4636e60c 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -277,6 +277,7 @@ Performance improvements - Performance improvement in :meth:`RangeIndex.join` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57651`) - Performance improvement in :meth:`RangeIndex.reindex` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57647`) - Performance improvement in :meth:`RangeIndex.take` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57445`) +- Performance improvement in :meth:`RangeIndex.round` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`?`) - Performance improvement in ``DataFrameGroupBy.__len__`` and ``SeriesGroupBy.__len__`` (:issue:`57595`) - Performance improvement in indexing operations for string dtypes (:issue:`56997`) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 052ecbafa686a..578f519f6bbd9 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -6738,7 +6738,6 @@ def diff(self, periods: int = 1) -> Index: """ return Index(self.to_series().diff(periods)) - @final def round(self, decimals: int = 0) -> Self: """ Round each value in the Index to the given number of decimals. diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index 24f53f16e1985..68d4f1fc2aba5 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -1140,6 +1140,41 @@ def any(self, *args, **kwargs) -> bool: # -------------------------------------------------------------------- + def round(self, decimals: int = 0) -> Self | Index: + """ + Round each value in the Index to the given number of decimals. + + Parameters + ---------- + decimals : int, optional + Number of decimal places to round to. If decimals is negative, + it specifies the number of positions to the left of the decimal point. + + Returns + ------- + Index or RangeIndex + A new Index with the rounded values. + + Examples + -------- + >>> import pandas as pd + >>> idx = pd.RangeIndex(10, 30, 10) + >>> idx.round(decimals=-1) + RangeIndex(start=10, stop=30, step=10) + >>> idx = pd.RangeIndex(10, 15, 1) + >>> idx.round(decimals=-1) + Index([10, 10, 10, 10, 10], dtype='int64') + """ + if decimals >= 0: + return self.copy() + elif all( + getattr(self, attr) % 10**-decimals == 0 for attr in ("start", "step") + ): + # e.g. Range(10, 30, 10).round(-1) doesn't need rounding + return self.copy() + else: + return super().round(decimals=decimals) + def _cmp_method(self, other, op): if isinstance(other, RangeIndex) and self._range == other._range: # Both are immutable so if ._range attr. are equal, shortcut is possible diff --git a/pandas/tests/indexes/ranges/test_range.py b/pandas/tests/indexes/ranges/test_range.py index 8c24ce5d699d5..77a1942029182 100644 --- a/pandas/tests/indexes/ranges/test_range.py +++ b/pandas/tests/indexes/ranges/test_range.py @@ -608,6 +608,37 @@ def test_range_index_rsub_by_const(self): tm.assert_index_equal(result, expected) +@pytest.mark.parametrize( + "rng, decimals", + [ + [range(5), 0], + [range(5), 2], + [range(10, 30, 10), -1], + [range(30, 10, -10), -1], + ], +) +def test_range_round_returns_rangeindex(rng, decimals): + ri = RangeIndex(rng) + expected = ri.copy() + result = ri.round(decimals=decimals) + tm.assert_index_equal(result, expected, exact=True) + + +@pytest.mark.parametrize( + "rng, decimals", + [ + [range(10, 30, 1), -1], + [range(30, 10, -1), -1], + [range(11, 14), -10], + ], +) +def test_range_round_returns_index(rng, decimals): + ri = RangeIndex(rng) + expected = Index(list(rng)).round(decimals=decimals) + result = ri.round(decimals=decimals) + tm.assert_index_equal(result, expected, exact=True) + + def test_append_non_rangeindex_return_rangeindex(): ri = RangeIndex(1) result = ri.append(Index([1])) From f8a3798225b0504f31336cd86fc63723eb08d13c Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:35:30 -0700 Subject: [PATCH 2/4] Add whatsnew --- doc/source/whatsnew/v3.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 70a5e4636e60c..4a9d95e380a05 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -276,8 +276,8 @@ Performance improvements - Performance improvement in :meth:`RangeIndex.append` when appending the same index (:issue:`57252`) - Performance improvement in :meth:`RangeIndex.join` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57651`) - Performance improvement in :meth:`RangeIndex.reindex` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57647`) +- Performance improvement in :meth:`RangeIndex.round` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57824`) - Performance improvement in :meth:`RangeIndex.take` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`57445`) -- Performance improvement in :meth:`RangeIndex.round` returning a :class:`RangeIndex` instead of a :class:`Index` when possible. (:issue:`?`) - Performance improvement in ``DataFrameGroupBy.__len__`` and ``SeriesGroupBy.__len__`` (:issue:`57595`) - Performance improvement in indexing operations for string dtypes (:issue:`56997`) From 203b330dd65821dd31ba07eeffbd9182d469a196 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 12 Mar 2024 16:58:22 -0700 Subject: [PATCH 3/4] Add typing --- pandas/core/indexes/range.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index f35d7099b7f11..bd5ad46c91aca 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -1165,7 +1165,9 @@ def any(self, *args, **kwargs) -> bool: # -------------------------------------------------------------------- - def round(self, decimals: int = 0) -> Self | Index: + # error: Return type "RangeIndex | Index" of "round" incompatible with + # return type "RangeIndex" in supertype "Index" + def round(self, decimals: int = 0) -> Self | Index: # type: ignore[override] """ Round each value in the Index to the given number of decimals. From 8fa8683c7cb6b699ac2b287028ff68671afadfbe Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:56:13 -0700 Subject: [PATCH 4/4] Address feedback --- pandas/core/indexes/range.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index bd5ad46c91aca..8199ba8ed3a71 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -1175,7 +1175,8 @@ def round(self, decimals: int = 0) -> Self | Index: # type: ignore[override] ---------- decimals : int, optional Number of decimal places to round to. If decimals is negative, - it specifies the number of positions to the left of the decimal point. + it specifies the number of positions to the left of the decimal point + e.g. ``round(11.0, -1) == 10.0``. Returns ------- @@ -1194,10 +1195,8 @@ def round(self, decimals: int = 0) -> Self | Index: # type: ignore[override] """ if decimals >= 0: return self.copy() - elif all( - getattr(self, attr) % 10**-decimals == 0 for attr in ("start", "step") - ): - # e.g. Range(10, 30, 10).round(-1) doesn't need rounding + elif self.start % 10**-decimals == 0 and self.step % 10**-decimals == 0: + # e.g. RangeIndex(10, 30, 10).round(-1) doesn't need rounding return self.copy() else: return super().round(decimals=decimals)