Skip to content

Commit d700c92

Browse files
committed
Improve MultiIndex label rename checks
1 parent 391107a commit d700c92

File tree

4 files changed

+60
-12
lines changed

4 files changed

+60
-12
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,7 @@ Other
928928
- Bug in printing a :class:`Series` with a :class:`DataFrame` stored in :attr:`Series.attrs` raised a ``ValueError`` (:issue:`60568`)
929929
- Fixed bug where the :class:`DataFrame` constructor misclassified array-like objects with a ``.name`` attribute as :class:`Series` or :class:`Index` (:issue:`61443`)
930930
- Fixed regression in :meth:`DataFrame.from_records` not initializing subclasses properly (:issue:`57008`)
931+
- Bug in :meth:`DataFrame.rename` where checks on argument errors="raise" are not consistent with the actual transformation applied (:issue:`55169`)
931932

932933
.. ***DO NOT USE THIS SECTION***
933934

pandas/core/frame.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5677,6 +5677,11 @@ def rename(
56775677
level : int or level name, default None
56785678
In case of a MultiIndex, only rename labels in the specified
56795679
level.
5680+
5681+
.. note::
5682+
Labels are renamed individually, and not via tuples across
5683+
MultiIndex levels
5684+
56805685
errors : {'ignore', 'raise'}, default 'ignore'
56815686
If 'raise', raise a `KeyError` when a dict-like `mapper`, `index`,
56825687
or `columns` contains labels that are not present in the Index

pandas/core/generic.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,18 +1047,36 @@ def _rename(
10471047

10481048
# GH 13473
10491049
if not callable(replacements):
1050-
if ax._is_multi and level is not None:
1051-
indexer = ax.get_level_values(level).get_indexer_for(replacements)
1052-
else:
1053-
indexer = ax.get_indexer_for(replacements)
1054-
1055-
if errors == "raise" and len(indexer[indexer == -1]):
1056-
missing_labels = [
1057-
label
1058-
for index, label in enumerate(replacements)
1059-
if indexer[index] == -1
1060-
]
1061-
raise KeyError(f"{missing_labels} not found in axis")
1050+
if errors == "raise":
1051+
missing_labels = []
1052+
for replacement in replacements:
1053+
if ax._is_multi:
1054+
indexers = [
1055+
ax.get_level_values(i).get_indexer_for([replacement])
1056+
for i in range(ax.nlevels)
1057+
if i == level or level is None
1058+
]
1059+
else:
1060+
indexers = [ax.get_indexer_for([replacement])]
1061+
1062+
found_anywhere = any(any(indexer != -1) for indexer in indexers)
1063+
if not found_anywhere:
1064+
missing_labels.append(replacement)
1065+
1066+
if len(missing_labels) > 0:
1067+
error = f"{missing_labels} not found in axis"
1068+
if ax._is_multi:
1069+
tuple_rename_tried = any(
1070+
type(label) is tuple and label in ax
1071+
for label in missing_labels
1072+
)
1073+
if tuple_rename_tried:
1074+
error += (
1075+
". Please provide individual labels for "
1076+
"replacement, and not tuples across "
1077+
"MultiIndex levels"
1078+
)
1079+
raise KeyError(error)
10621080

10631081
new_index = ax._transform_index(f, level=level)
10641082
result._set_axis_nocheck(new_index, axis=axis_no, inplace=True)

pandas/tests/frame/methods/test_rename.py

Lines changed: 24 additions & 0 deletions
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):
168178
renamed = float_frame.rename(columns={"C": "foo"})
169179

@@ -221,6 +231,20 @@ def test_rename_errors_raises(self):
221231
with pytest.raises(KeyError, match="'E'] not found in axis"):
222232
df.rename(columns={"A": "a", "E": "e"}, errors="raise")
223233

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

0 commit comments

Comments
 (0)