Skip to content

Commit c63d51a

Browse files
ENH: level keyword in rename (GH4160)
1 parent 5a3b071 commit c63d51a

File tree

4 files changed

+79
-5
lines changed

4 files changed

+79
-5
lines changed

doc/source/whatsnew/v0.19.0.txt

+3
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ Other enhancements
322322
index=['row1', 'row2'])
323323
df.sort_values(by='row2', axis=1)
324324

325+
- Addition of a ``level`` keyword to ``DataFrame/Series.rename`` to rename
326+
labels in the specified level of a MultiIndex (:issue:`4160`).
327+
325328

326329
.. _whatsnew_0190.api:
327330

pandas/core/generic.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,9 @@ def swaplevel(self, i=-2, j=-1, axis=0):
588588
inplace : boolean, default False
589589
Whether to return a new %(klass)s. If True then value of copy is
590590
ignored.
591+
level : int or level name, default None
592+
In case of a MultiIndex, only rename labels in the specified
593+
level.
591594
592595
Returns
593596
-------
@@ -643,6 +646,7 @@ def rename(self, *args, **kwargs):
643646
axes, kwargs = self._construct_axes_from_arguments(args, kwargs)
644647
copy = kwargs.pop('copy', True)
645648
inplace = kwargs.pop('inplace', False)
649+
level = kwargs.pop('level', None)
646650

647651
if kwargs:
648652
raise TypeError('rename() got an unexpected keyword '
@@ -676,7 +680,10 @@ def f(x):
676680
f = _get_rename_function(v)
677681

678682
baxis = self._get_block_manager_axis(axis)
679-
result._data = result._data.rename_axis(f, axis=baxis, copy=copy)
683+
if level is not None:
684+
level = self.axes[axis]._get_level_number(level)
685+
result._data = result._data.rename_axis(f, axis=baxis, copy=copy,
686+
level=level)
680687
result._clear_item_cache()
681688

682689
if inplace:

pandas/core/internals.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -2770,7 +2770,7 @@ def set_axis(self, axis, new_labels):
27702770

27712771
self.axes[axis] = new_labels
27722772

2773-
def rename_axis(self, mapper, axis, copy=True):
2773+
def rename_axis(self, mapper, axis, copy=True, level=None):
27742774
"""
27752775
Rename one of axes.
27762776
@@ -2779,10 +2779,11 @@ def rename_axis(self, mapper, axis, copy=True):
27792779
mapper : unary callable
27802780
axis : int
27812781
copy : boolean, default True
2782+
level : int, default None
27822783
27832784
"""
27842785
obj = self.copy(deep=copy)
2785-
obj.set_axis(axis, _transform_index(self.axes[axis], mapper))
2786+
obj.set_axis(axis, _transform_index(self.axes[axis], mapper, level))
27862787
return obj
27872788

27882789
def add_prefix(self, prefix):
@@ -4708,15 +4709,20 @@ def _safe_reshape(arr, new_shape):
47084709
return arr
47094710

47104711

4711-
def _transform_index(index, func):
4712+
def _transform_index(index, func, level=None):
47124713
"""
47134714
Apply function to all values found in index.
47144715
47154716
This includes transforming multiindex entries separately.
4717+
Only apply function to one level of the MultiIndex if level is specified.
47164718
47174719
"""
47184720
if isinstance(index, MultiIndex):
4719-
items = [tuple(func(y) for y in x) for x in index]
4721+
if level is not None:
4722+
items = [tuple(func(y) if i == level else y
4723+
for i, y in enumerate(x)) for x in index]
4724+
else:
4725+
items = [tuple(func(y) for y in x) for x in index]
47204726
return MultiIndex.from_tuples(items, names=index.names)
47214727
else:
47224728
items = [func(x) for x in index]

pandas/tests/frame/test_alter_axes.py

+58
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,64 @@ def test_rename(self):
376376
self.assertEqual(renamed.index.names, renamer.index.names)
377377
self.assertEqual(renamed.columns.names, renamer.columns.names)
378378

379+
def test_rename_multiindex_level(self):
380+
381+
tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]
382+
tuples_columns = [('fizz1', 'buzz1'), ('fizz2', 'buzz2')]
383+
index = MultiIndex.from_tuples(tuples_index, names=['foo', 'bar'])
384+
columns = MultiIndex.from_tuples(
385+
tuples_columns, names=['fizz', 'buzz'])
386+
renamer = DataFrame([(0, 0), (1, 1)], index=index, columns=columns)
387+
renamed = renamer.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
388+
columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'})
389+
390+
# dict
391+
new_columns = MultiIndex.from_tuples([('fizz3', 'buzz1'),
392+
('fizz2', 'buzz2')],
393+
names=['fizz', 'buzz'])
394+
renamed = renamer.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
395+
level=0)
396+
self.assert_index_equal(renamed.columns, new_columns)
397+
renamed = renamer.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
398+
level='fizz')
399+
self.assert_index_equal(renamed.columns, new_columns)
400+
401+
new_columns = MultiIndex.from_tuples([('fizz1', 'buzz1'),
402+
('fizz2', 'buzz3')],
403+
names=['fizz', 'buzz'])
404+
renamed = renamer.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
405+
level=1)
406+
self.assert_index_equal(renamed.columns, new_columns)
407+
renamed = renamer.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
408+
level='buzz')
409+
self.assert_index_equal(renamed.columns, new_columns)
410+
411+
# function
412+
func = str.upper
413+
new_columns = MultiIndex.from_tuples([('FIZZ1', 'buzz1'),
414+
('FIZZ2', 'buzz2')],
415+
names=['fizz', 'buzz'])
416+
renamed = renamer.rename(columns=func, level=0)
417+
self.assert_index_equal(renamed.columns, new_columns)
418+
renamed = renamer.rename(columns=func, level='fizz')
419+
self.assert_index_equal(renamed.columns, new_columns)
420+
421+
new_columns = MultiIndex.from_tuples([('fizz1', 'BUZZ1'),
422+
('fizz2', 'BUZZ2')],
423+
names=['fizz', 'buzz'])
424+
renamed = renamer.rename(columns=func, level=1)
425+
self.assert_index_equal(renamed.columns, new_columns)
426+
renamed = renamer.rename(columns=func, level='buzz')
427+
self.assert_index_equal(renamed.columns, new_columns)
428+
429+
# index
430+
new_index = MultiIndex.from_tuples([('foo3', 'bar1'),
431+
('foo2', 'bar2')],
432+
names=['foo', 'bar'])
433+
renamed = renamer.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
434+
level=0)
435+
self.assert_index_equal(renamed.index, new_index)
436+
379437
def test_rename_nocopy(self):
380438
renamed = self.frame.rename(columns={'C': 'foo'}, copy=False)
381439
renamed['foo'] = 1.

0 commit comments

Comments
 (0)