From 0fd6ca7b8e5ae1ad09b0f7e76a535c2fab64ac9e Mon Sep 17 00:00:00 2001 From: Ijtaba Hussain Date: Thu, 3 Jul 2025 19:56:28 +0100 Subject: [PATCH] Improve Index._transform_index level handling --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/core/indexes/base.py | 15 +++++++++------ pandas/tests/frame/methods/test_rename.py | 8 ++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 94e375615d122..05573ee9ea344 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -909,6 +909,7 @@ Other - Bug in :meth:`DataFrame.sort_values` where sorting by a column explicitly named ``None`` raised a ``KeyError`` instead of sorting by the column as expected. (:issue:`61512`) - Bug in :meth:`DataFrame.transform` that was returning the wrong order unless the index was monotonically increasing. (:issue:`57069`) - Bug in :meth:`DataFrame.where` where using a non-bool type array in the function would return a ``ValueError`` instead of a ``TypeError`` (:issue:`56330`) +- Bug in :meth:`Index._transform_index` where transformations are applied across all levels one by one even when a level is not specified (:issue:`55169`) - Bug in :meth:`Index.sort_values` when passing a key function that turns values into tuples, e.g. ``key=natsort.natsort_key``, would raise ``TypeError`` (:issue:`56081`) - Bug in :meth:`MultiIndex.fillna` error message was referring to ``isna`` instead of ``fillna`` (:issue:`60974`) - Bug in :meth:`Series.describe` where median percentile was always included when the ``percentiles`` argument was passed (:issue:`60550`). diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 2deaaae85e56b..cbb77369f20a8 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -6429,12 +6429,15 @@ def _transform_index(self, func, *, level=None) -> Index: Only apply function to one level of the MultiIndex if level is specified. """ if isinstance(self, ABCMultiIndex): - values = [ - self.get_level_values(i).map(func) - if i == level or level is None - else self.get_level_values(i) - for i in range(self.nlevels) - ] + if level is None: + return self.map(func) + else: + values = [ + self.get_level_values(i).map(func) + if i == level + else self.get_level_values(i) + for i in range(self.nlevels) + ] return type(self).from_arrays(values) else: items = [func(x) for x in self] diff --git a/pandas/tests/frame/methods/test_rename.py b/pandas/tests/frame/methods/test_rename.py index 6153a168476d4..5a2dd796d7231 100644 --- a/pandas/tests/frame/methods/test_rename.py +++ b/pandas/tests/frame/methods/test_rename.py @@ -164,6 +164,14 @@ def test_rename_multiindex(self): renamed = df.rename(index={"foo1": "foo3", "bar2": "bar3"}, level=0) tm.assert_index_equal(renamed.index, new_index) + def test_rename_multiindex_tuples_with_checks(self): + df = DataFrame({("a", "count"): [1, 2], ("a", "sum"): [3, 4]}) + renamed = df.rename( + columns={("a", "count"): ("b", "number_of"), ("a", "sum"): ("b", "total")}, errors="raise" + ) + new_columns = MultiIndex.from_tuples([("b", "number_of"), ("b", "total")]) + tm.assert_index_equal(renamed.columns, new_columns) + def test_rename_nocopy(self, float_frame): renamed = float_frame.rename(columns={"C": "foo"})