Skip to content

Commit 0065973

Browse files
ENH: level keyword in rename (GH4160)
1 parent 1751628 commit 0065973

File tree

4 files changed

+78
-11
lines changed

4 files changed

+78
-11
lines changed

doc/source/whatsnew/v0.20.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ Other Enhancements
345345
- The ``skiprows`` argument in ``pd.read_csv()`` now accepts a callable function as a value (:issue:`10882`)
346346
- The ``nrows`` and ``chunksize`` arguments in ``pd.read_csv()`` are supported if both are passed (:issue:`6774`, :issue:`15755`)
347347
- ``pd.DataFrame.plot`` now prints a title above each subplot if ``suplots=True`` and ``title`` is a list of strings (:issue:`14753`)
348+
- Addition of a ``level`` keyword to ``DataFrame/Series.rename`` to rename
349+
labels in the specified level of a MultiIndex (:issue:`4160`).
348350
- ``pd.Series.interpolate`` now supports timedelta as an index type with ``method='time'`` (:issue:`6424`)
349351
- ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs <timedeltas.isoformat>` (:issue:`15136`)
350352
- ``.select_dtypes()`` now allows the string 'datetimetz' to generically select datetimes with tz (:issue:`14910`)

pandas/core/generic.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,9 @@ def swaplevel(self, i=-2, j=-1, axis=0):
634634
inplace : boolean, default False
635635
Whether to return a new %(klass)s. If True then value of copy is
636636
ignored.
637+
level : int or level name, default None
638+
In case of a MultiIndex, only rename labels in the specified
639+
level.
637640
638641
Returns
639642
-------
@@ -690,6 +693,7 @@ def rename(self, *args, **kwargs):
690693
axes, kwargs = self._construct_axes_from_arguments(args, kwargs)
691694
copy = kwargs.pop('copy', True)
692695
inplace = kwargs.pop('inplace', False)
696+
level = kwargs.pop('level', None)
693697

694698
if kwargs:
695699
raise TypeError('rename() got an unexpected keyword '
@@ -723,7 +727,10 @@ def f(x):
723727
f = _get_rename_function(v)
724728

725729
baxis = self._get_block_manager_axis(axis)
726-
result._data = result._data.rename_axis(f, axis=baxis, copy=copy)
730+
if level is not None:
731+
level = self.axes[axis]._get_level_number(level)
732+
result._data = result._data.rename_axis(f, axis=baxis, copy=copy,
733+
level=level)
727734
result._clear_item_cache()
728735

729736
if inplace:

pandas/core/internals.py

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

28342834
self.axes[axis] = new_labels
28352835

2836-
def rename_axis(self, mapper, axis, copy=True):
2836+
def rename_axis(self, mapper, axis, copy=True, level=None):
28372837
"""
28382838
Rename one of axes.
28392839
@@ -2842,10 +2842,11 @@ def rename_axis(self, mapper, axis, copy=True):
28422842
mapper : unary callable
28432843
axis : int
28442844
copy : boolean, default True
2845+
level : int, default None
28452846
28462847
"""
28472848
obj = self.copy(deep=copy)
2848-
obj.set_axis(axis, _transform_index(self.axes[axis], mapper))
2849+
obj.set_axis(axis, _transform_index(self.axes[axis], mapper, level))
28492850
return obj
28502851

28512852
def add_prefix(self, prefix):
@@ -4731,15 +4732,20 @@ def _safe_reshape(arr, new_shape):
47314732
return arr
47324733

47334734

4734-
def _transform_index(index, func):
4735+
def _transform_index(index, func, level=None):
47354736
"""
47364737
Apply function to all values found in index.
47374738
47384739
This includes transforming multiindex entries separately.
4740+
Only apply function to one level of the MultiIndex if level is specified.
47394741
47404742
"""
47414743
if isinstance(index, MultiIndex):
4742-
items = [tuple(func(y) for y in x) for x in index]
4744+
if level is not None:
4745+
items = [tuple(func(y) if i == level else y
4746+
for i, y in enumerate(x)) for x in index]
4747+
else:
4748+
items = [tuple(func(y) for y in x) for x in index]
47434749
return MultiIndex.from_tuples(items, names=index.names)
47444750
else:
47454751
items = [func(x) for x in index]

pandas/tests/frame/test_alter_axes.py

+58-6
Original file line numberDiff line numberDiff line change
@@ -400,15 +400,18 @@ def test_rename(self):
400400
pd.Index(['bar', 'foo'], name='name'))
401401
self.assertEqual(renamed.index.name, renamer.index.name)
402402

403-
# MultiIndex
403+
def test_rename_multiindex(self):
404+
404405
tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]
405406
tuples_columns = [('fizz1', 'buzz1'), ('fizz2', 'buzz2')]
406407
index = MultiIndex.from_tuples(tuples_index, names=['foo', 'bar'])
407408
columns = MultiIndex.from_tuples(
408409
tuples_columns, names=['fizz', 'buzz'])
409-
renamer = DataFrame([(0, 0), (1, 1)], index=index, columns=columns)
410-
renamed = renamer.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
411-
columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'})
410+
df = DataFrame([(0, 0), (1, 1)], index=index, columns=columns)
411+
412+
## without specifying level -> accross all levels
413+
renamed = df.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
414+
columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'})
412415
new_index = MultiIndex.from_tuples([('foo3', 'bar1'),
413416
('foo2', 'bar3')],
414417
names=['foo', 'bar'])
@@ -417,8 +420,57 @@ def test_rename(self):
417420
names=['fizz', 'buzz'])
418421
self.assert_index_equal(renamed.index, new_index)
419422
self.assert_index_equal(renamed.columns, new_columns)
420-
self.assertEqual(renamed.index.names, renamer.index.names)
421-
self.assertEqual(renamed.columns.names, renamer.columns.names)
423+
self.assertEqual(renamed.index.names, df.index.names)
424+
self.assertEqual(renamed.columns.names, df.columns.names)
425+
426+
## with specifying a level
427+
428+
# dict
429+
new_columns = MultiIndex.from_tuples([('fizz3', 'buzz1'),
430+
('fizz2', 'buzz2')],
431+
names=['fizz', 'buzz'])
432+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
433+
level=0)
434+
self.assert_index_equal(renamed.columns, new_columns)
435+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
436+
level='fizz')
437+
self.assert_index_equal(renamed.columns, new_columns)
438+
439+
new_columns = MultiIndex.from_tuples([('fizz1', 'buzz1'),
440+
('fizz2', 'buzz3')],
441+
names=['fizz', 'buzz'])
442+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
443+
level=1)
444+
self.assert_index_equal(renamed.columns, new_columns)
445+
renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'},
446+
level='buzz')
447+
self.assert_index_equal(renamed.columns, new_columns)
448+
449+
# function
450+
func = str.upper
451+
new_columns = MultiIndex.from_tuples([('FIZZ1', 'buzz1'),
452+
('FIZZ2', 'buzz2')],
453+
names=['fizz', 'buzz'])
454+
renamed = df.rename(columns=func, level=0)
455+
self.assert_index_equal(renamed.columns, new_columns)
456+
renamed = df.rename(columns=func, level='fizz')
457+
self.assert_index_equal(renamed.columns, new_columns)
458+
459+
new_columns = MultiIndex.from_tuples([('fizz1', 'BUZZ1'),
460+
('fizz2', 'BUZZ2')],
461+
names=['fizz', 'buzz'])
462+
renamed = df.rename(columns=func, level=1)
463+
self.assert_index_equal(renamed.columns, new_columns)
464+
renamed = df.rename(columns=func, level='buzz')
465+
self.assert_index_equal(renamed.columns, new_columns)
466+
467+
# index
468+
new_index = MultiIndex.from_tuples([('foo3', 'bar1'),
469+
('foo2', 'bar2')],
470+
names=['foo', 'bar'])
471+
renamed = df.rename(index={'foo1': 'foo3', 'bar2': 'bar3'},
472+
level=0)
473+
self.assert_index_equal(renamed.index, new_index)
422474

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

0 commit comments

Comments
 (0)