Skip to content

Commit 0ba305b

Browse files
ENH: level keyword in rename (GH4160) (pandas-dev#13766)
1 parent f53d38b commit 0ba305b

File tree

4 files changed

+81
-11
lines changed

4 files changed

+81
-11
lines changed

doc/source/whatsnew/v0.20.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ Other Enhancements
490490
- ``DataFrame.plot`` now prints a title above each subplot if ``suplots=True`` and ``title`` is a list of strings (:issue:`14753`)
491491
- ``DataFrame.plot`` can pass the matplotlib 2.0 default color cycle as a single string as color parameter, see `here <http://matplotlib.org/2.0.0/users/colors.html#cn-color-selection>`__. (:issue:`15516`)
492492
- ``Series.interpolate()`` now supports timedelta as an index type with ``method='time'`` (:issue:`6424`)
493+
- Addition of a ``level`` keyword to ``DataFrame/Series.rename`` to rename
494+
labels in the specified level of a MultiIndex (:issue:`4160`).
493495
- ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs <timedeltas.isoformat>` (:issue:`15136`)
494496
- ``.select_dtypes()`` now allows the string ``datetimetz`` to generically select datetimes with tz (:issue:`14910`)
495497
- The ``.to_latex()`` method will now accept ``multicolumn`` and ``multirow`` arguments to use the accompanying LaTeX enhancements

pandas/core/generic.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,9 @@ def swaplevel(self, i=-2, j=-1, axis=0):
645645
inplace : boolean, default False
646646
Whether to return a new %(klass)s. If True then value of copy is
647647
ignored.
648+
level : int or level name, default None
649+
In case of a MultiIndex, only rename labels in the specified
650+
level.
648651
649652
Returns
650653
-------
@@ -701,6 +704,7 @@ def rename(self, *args, **kwargs):
701704
axes, kwargs = self._construct_axes_from_arguments(args, kwargs)
702705
copy = kwargs.pop('copy', True)
703706
inplace = kwargs.pop('inplace', False)
707+
level = kwargs.pop('level', None)
704708

705709
if kwargs:
706710
raise TypeError('rename() got an unexpected keyword '
@@ -734,7 +738,10 @@ def f(x):
734738
f = _get_rename_function(v)
735739

736740
baxis = self._get_block_manager_axis(axis)
737-
result._data = result._data.rename_axis(f, axis=baxis, copy=copy)
741+
if level is not None:
742+
level = self.axes[axis]._get_level_number(level)
743+
result._data = result._data.rename_axis(f, axis=baxis, copy=copy,
744+
level=level)
738745
result._clear_item_cache()
739746

740747
if inplace:

pandas/core/internals.py

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

28382838
self.axes[axis] = new_labels
28392839

2840-
def rename_axis(self, mapper, axis, copy=True):
2840+
def rename_axis(self, mapper, axis, copy=True, level=None):
28412841
"""
28422842
Rename one of axes.
28432843
@@ -2846,10 +2846,11 @@ def rename_axis(self, mapper, axis, copy=True):
28462846
mapper : unary callable
28472847
axis : int
28482848
copy : boolean, default True
2849+
level : int, default None
28492850
28502851
"""
28512852
obj = self.copy(deep=copy)
2852-
obj.set_axis(axis, _transform_index(self.axes[axis], mapper))
2853+
obj.set_axis(axis, _transform_index(self.axes[axis], mapper, level))
28532854
return obj
28542855

28552856
def add_prefix(self, prefix):
@@ -4735,15 +4736,20 @@ def _safe_reshape(arr, new_shape):
47354736
return arr
47364737

47374738

4738-
def _transform_index(index, func):
4739+
def _transform_index(index, func, level=None):
47394740
"""
47404741
Apply function to all values found in index.
47414742
47424743
This includes transforming multiindex entries separately.
4744+
Only apply function to one level of the MultiIndex if level is specified.
47434745
47444746
"""
47454747
if isinstance(index, MultiIndex):
4746-
items = [tuple(func(y) for y in x) for x in index]
4748+
if level is not None:
4749+
items = [tuple(func(y) if i == level else y
4750+
for i, y in enumerate(x)) for x in index]
4751+
else:
4752+
items = [tuple(func(y) for y in x) for x in index]
47474753
return MultiIndex.from_tuples(items, names=index.names)
47484754
else:
47494755
items = [func(x) for x in index]

pandas/tests/frame/test_alter_axes.py

+61-6
Original file line numberDiff line numberDiff line change
@@ -415,15 +415,20 @@ def test_rename(self):
415415
pd.Index(['bar', 'foo'], name='name'))
416416
self.assertEqual(renamed.index.name, renamer.index.name)
417417

418-
# MultiIndex
418+
def test_rename_multiindex(self):
419+
419420
tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]
420421
tuples_columns = [('fizz1', 'buzz1'), ('fizz2', 'buzz2')]
421422
index = MultiIndex.from_tuples(tuples_index, names=['foo', 'bar'])
422423
columns = MultiIndex.from_tuples(
423424
tuples_columns, names=['fizz', 'buzz'])
424-
renamer = DataFrame([(0, 0), (1, 1)], index=index, columns=columns)
425-
renamed = renamer.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
426-
columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'})
425+
df = DataFrame([(0, 0), (1, 1)], index=index, columns=columns)
426+
427+
#
428+
# without specifying level -> accross all levels
429+
430+
renamed = df.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
431+
columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'})
427432
new_index = MultiIndex.from_tuples([('foo3', 'bar1'),
428433
('foo2', 'bar3')],
429434
names=['foo', 'bar'])
@@ -432,8 +437,58 @@ def test_rename(self):
432437
names=['fizz', 'buzz'])
433438
self.assert_index_equal(renamed.index, new_index)
434439
self.assert_index_equal(renamed.columns, new_columns)
435-
self.assertEqual(renamed.index.names, renamer.index.names)
436-
self.assertEqual(renamed.columns.names, renamer.columns.names)
440+
self.assertEqual(renamed.index.names, df.index.names)
441+
self.assertEqual(renamed.columns.names, df.columns.names)
442+
443+
#
444+
# with specifying a level (GH13766)
445+
446+
# dict
447+
new_columns = MultiIndex.from_tuples([('fizz3', 'buzz1'),
448+
('fizz2', 'buzz2')],
449+
names=['fizz', 'buzz'])
450+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
451+
level=0)
452+
self.assert_index_equal(renamed.columns, new_columns)
453+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
454+
level='fizz')
455+
self.assert_index_equal(renamed.columns, new_columns)
456+
457+
new_columns = MultiIndex.from_tuples([('fizz1', 'buzz1'),
458+
('fizz2', 'buzz3')],
459+
names=['fizz', 'buzz'])
460+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
461+
level=1)
462+
self.assert_index_equal(renamed.columns, new_columns)
463+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
464+
level='buzz')
465+
self.assert_index_equal(renamed.columns, new_columns)
466+
467+
# function
468+
func = str.upper
469+
new_columns = MultiIndex.from_tuples([('FIZZ1', 'buzz1'),
470+
('FIZZ2', 'buzz2')],
471+
names=['fizz', 'buzz'])
472+
renamed = df.rename(columns=func, level=0)
473+
self.assert_index_equal(renamed.columns, new_columns)
474+
renamed = df.rename(columns=func, level='fizz')
475+
self.assert_index_equal(renamed.columns, new_columns)
476+
477+
new_columns = MultiIndex.from_tuples([('fizz1', 'BUZZ1'),
478+
('fizz2', 'BUZZ2')],
479+
names=['fizz', 'buzz'])
480+
renamed = df.rename(columns=func, level=1)
481+
self.assert_index_equal(renamed.columns, new_columns)
482+
renamed = df.rename(columns=func, level='buzz')
483+
self.assert_index_equal(renamed.columns, new_columns)
484+
485+
# index
486+
new_index = MultiIndex.from_tuples([('foo3', 'bar1'),
487+
('foo2', 'bar2')],
488+
names=['foo', 'bar'])
489+
renamed = df.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
490+
level=0)
491+
self.assert_index_equal(renamed.index, new_index)
437492

438493
def test_rename_nocopy(self):
439494
renamed = self.frame.rename(columns={'C': 'foo'}, copy=False)

0 commit comments

Comments
 (0)