Skip to content

Commit d1ff9b0

Browse files
committed
Improve MultiIndex label rename checks, docs and tests
1 parent 0e11d6d commit d1ff9b0

File tree

4 files changed

+60
-12
lines changed

4 files changed

+60
-12
lines changed

doc/source/whatsnew/v2.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ Styler
213213
Other
214214
^^^^^
215215
- 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`)
216+
- Fixed bug in :meth:`DataFrame.rename` where checks on argument errors="raise" are not consistent with the actual transformation applied (:issue:`55169`). Logic change is accompanied with improvement to docs, a new test and a more descriptive ``KeyError`` message when a tuple label rename is attempted across :class:`MultiIndex` levels
216217

217218

218219
.. ***DO NOT USE THIS SECTION***

pandas/core/frame.py

+5
Original file line numberDiff line numberDiff line change
@@ -5650,6 +5650,11 @@ def rename(
56505650
level : int or level name, default None
56515651
In case of a MultiIndex, only rename labels in the specified
56525652
level.
5653+
5654+
.. note::
5655+
Labels are renamed individually, and not via tuples across
5656+
MultiIndex levels
5657+
56535658
errors : {'ignore', 'raise'}, default 'ignore'
56545659
If 'raise', raise a `KeyError` when a dict-like `mapper`, `index`,
56555660
or `columns` contains labels that are not present in the Index

pandas/core/generic.py

+30-12
Original file line numberDiff line numberDiff line change
@@ -1118,18 +1118,36 @@ def _rename(
11181118

11191119
# GH 13473
11201120
if not callable(replacements):
1121-
if ax._is_multi and level is not None:
1122-
indexer = ax.get_level_values(level).get_indexer_for(replacements)
1123-
else:
1124-
indexer = ax.get_indexer_for(replacements)
1125-
1126-
if errors == "raise" and len(indexer[indexer == -1]):
1127-
missing_labels = [
1128-
label
1129-
for index, label in enumerate(replacements)
1130-
if indexer[index] == -1
1131-
]
1132-
raise KeyError(f"{missing_labels} not found in axis")
1121+
if errors == "raise":
1122+
missing_labels = []
1123+
for replacement in replacements:
1124+
if ax._is_multi:
1125+
indexers = [
1126+
ax.get_level_values(i).get_indexer_for([replacement])
1127+
for i in range(ax.nlevels)
1128+
if i == level or level is None
1129+
]
1130+
else:
1131+
indexers = [ax.get_indexer_for([replacement])]
1132+
1133+
found_anywhere = any(any(indexer != -1) for indexer in indexers)
1134+
if not found_anywhere:
1135+
missing_labels.append(replacement)
1136+
1137+
if len(missing_labels) > 0:
1138+
error = f"{missing_labels} not found in axis"
1139+
if ax._is_multi:
1140+
tuple_rename_tried = any(
1141+
type(label) is tuple and label in ax
1142+
for label in missing_labels
1143+
)
1144+
if tuple_rename_tried:
1145+
error += (
1146+
". Please provide individual labels for "
1147+
"replacement, and not tuples across "
1148+
"MultiIndex levels"
1149+
)
1150+
raise KeyError(error)
11331151

11341152
new_index = ax._transform_index(f, level=level)
11351153
result._set_axis_nocheck(new_index, axis=axis_no, inplace=True, copy=False)

pandas/tests/frame/methods/test_rename.py

+24
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,16 @@ def test_rename_multiindex(self):
164164
renamed = df.rename(index={"foo1": "foo3", "bar2": "bar3"}, level=0)
165165
tm.assert_index_equal(renamed.index, new_index)
166166

167+
def test_rename_multiindex_with_checks(self):
168+
df = DataFrame({("a", "count"): [1, 2], ("a", "sum"): [3, 4]})
169+
renamed = df.rename(
170+
columns={"a": "b", "count": "number_of", "sum": "total"}, errors="raise"
171+
)
172+
173+
new_columns = MultiIndex.from_tuples([("b", "number_of"), ("b", "total")])
174+
175+
tm.assert_index_equal(renamed.columns, new_columns)
176+
167177
def test_rename_nocopy(self, float_frame, using_copy_on_write, warn_copy_on_write):
168178
renamed = float_frame.rename(columns={"C": "foo"}, copy=False)
169179

@@ -225,6 +235,20 @@ def test_rename_errors_raises(self):
225235
with pytest.raises(KeyError, match="'E'] not found in axis"):
226236
df.rename(columns={"A": "a", "E": "e"}, errors="raise")
227237

238+
def test_rename_error_raised_for_label_across_multiindex_levels(self):
239+
df = DataFrame([{"a": 1, "b": 2}, {"a": 3, "b": 4}])
240+
df = df.groupby("a").agg({"b": ("count", "sum")})
241+
with pytest.raises(
242+
KeyError,
243+
match=(
244+
"\\[\\('b', 'count'\\)\\] not found "
245+
"in axis\\. Please provide individual "
246+
"labels for replacement, and not "
247+
"tuples across MultiIndex levels"
248+
),
249+
):
250+
df.rename(columns={("b", "count"): "new"}, errors="raise")
251+
228252
@pytest.mark.parametrize(
229253
"mapper, errors, expected_columns",
230254
[

0 commit comments

Comments
 (0)