diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 0b95bf98b401d..0995df8101d06 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -490,6 +490,8 @@ Other Enhancements - ``DataFrame.plot`` now prints a title above each subplot if ``suplots=True`` and ``title`` is a list of strings (:issue:`14753`) - ``DataFrame.plot`` can pass the matplotlib 2.0 default color cycle as a single string as color parameter, see `here `__. (:issue:`15516`) - ``Series.interpolate()`` now supports timedelta as an index type with ``method='time'`` (:issue:`6424`) +- Addition of a ``level`` keyword to ``DataFrame/Series.rename`` to rename + labels in the specified level of a MultiIndex (:issue:`4160`). - ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs ` (:issue:`15136`) - ``.select_dtypes()`` now allows the string ``datetimetz`` to generically select datetimes with tz (:issue:`14910`) - The ``.to_latex()`` method will now accept ``multicolumn`` and ``multirow`` arguments to use the accompanying LaTeX enhancements diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 5f0c65ddfb9c3..841df3727e5a6 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -645,6 +645,9 @@ def swaplevel(self, i=-2, j=-1, axis=0): inplace : boolean, default False Whether to return a new %(klass)s. If True then value of copy is ignored. + level : int or level name, default None + In case of a MultiIndex, only rename labels in the specified + level. Returns ------- @@ -701,6 +704,7 @@ def rename(self, *args, **kwargs): axes, kwargs = self._construct_axes_from_arguments(args, kwargs) copy = kwargs.pop('copy', True) inplace = kwargs.pop('inplace', False) + level = kwargs.pop('level', None) if kwargs: raise TypeError('rename() got an unexpected keyword ' @@ -734,7 +738,10 @@ def f(x): f = _get_rename_function(v) baxis = self._get_block_manager_axis(axis) - result._data = result._data.rename_axis(f, axis=baxis, copy=copy) + if level is not None: + level = self.axes[axis]._get_level_number(level) + result._data = result._data.rename_axis(f, axis=baxis, copy=copy, + level=level) result._clear_item_cache() if inplace: diff --git a/pandas/core/internals.py b/pandas/core/internals.py index c698bcb9fa5ee..5a87574455a63 100644 --- a/pandas/core/internals.py +++ b/pandas/core/internals.py @@ -2837,7 +2837,7 @@ def set_axis(self, axis, new_labels): self.axes[axis] = new_labels - def rename_axis(self, mapper, axis, copy=True): + def rename_axis(self, mapper, axis, copy=True, level=None): """ Rename one of axes. @@ -2846,10 +2846,11 @@ def rename_axis(self, mapper, axis, copy=True): mapper : unary callable axis : int copy : boolean, default True + level : int, default None """ obj = self.copy(deep=copy) - obj.set_axis(axis, _transform_index(self.axes[axis], mapper)) + obj.set_axis(axis, _transform_index(self.axes[axis], mapper, level)) return obj def add_prefix(self, prefix): @@ -4735,15 +4736,20 @@ def _safe_reshape(arr, new_shape): return arr -def _transform_index(index, func): +def _transform_index(index, func, level=None): """ Apply function to all values found in index. This includes transforming multiindex entries separately. + Only apply function to one level of the MultiIndex if level is specified. """ if isinstance(index, MultiIndex): - items = [tuple(func(y) for y in x) for x in index] + if level is not None: + items = [tuple(func(y) if i == level else y + for i, y in enumerate(x)) for x in index] + else: + items = [tuple(func(y) for y in x) for x in index] return MultiIndex.from_tuples(items, names=index.names) else: items = [func(x) for x in index] diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 9add944d2293e..ce4dd6d38eeeb 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -415,15 +415,20 @@ def test_rename(self): pd.Index(['bar', 'foo'], name='name')) self.assertEqual(renamed.index.name, renamer.index.name) - # MultiIndex + def test_rename_multiindex(self): + tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')] tuples_columns = [('fizz1', 'buzz1'), ('fizz2', 'buzz2')] index = MultiIndex.from_tuples(tuples_index, names=['foo', 'bar']) columns = MultiIndex.from_tuples( tuples_columns, names=['fizz', 'buzz']) - renamer = DataFrame([(0, 0), (1, 1)], index=index, columns=columns) - renamed = renamer.rename(index={'foo1': 'foo3', 'bar2': 'bar3'}, - columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'}) + df = DataFrame([(0, 0), (1, 1)], index=index, columns=columns) + + # + # without specifying level -> accross all levels + + renamed = df.rename(index={'foo1': 'foo3', 'bar2': 'bar3'}, + columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'}) new_index = MultiIndex.from_tuples([('foo3', 'bar1'), ('foo2', 'bar3')], names=['foo', 'bar']) @@ -432,8 +437,58 @@ def test_rename(self): names=['fizz', 'buzz']) self.assert_index_equal(renamed.index, new_index) self.assert_index_equal(renamed.columns, new_columns) - self.assertEqual(renamed.index.names, renamer.index.names) - self.assertEqual(renamed.columns.names, renamer.columns.names) + self.assertEqual(renamed.index.names, df.index.names) + self.assertEqual(renamed.columns.names, df.columns.names) + + # + # with specifying a level (GH13766) + + # dict + new_columns = MultiIndex.from_tuples([('fizz3', 'buzz1'), + ('fizz2', 'buzz2')], + names=['fizz', 'buzz']) + renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'}, + level=0) + self.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'}, + level='fizz') + self.assert_index_equal(renamed.columns, new_columns) + + new_columns = MultiIndex.from_tuples([('fizz1', 'buzz1'), + ('fizz2', 'buzz3')], + names=['fizz', 'buzz']) + renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'}, + level=1) + self.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns={'fizz1': 'fizz3', 'buzz2': 'buzz3'}, + level='buzz') + self.assert_index_equal(renamed.columns, new_columns) + + # function + func = str.upper + new_columns = MultiIndex.from_tuples([('FIZZ1', 'buzz1'), + ('FIZZ2', 'buzz2')], + names=['fizz', 'buzz']) + renamed = df.rename(columns=func, level=0) + self.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns=func, level='fizz') + self.assert_index_equal(renamed.columns, new_columns) + + new_columns = MultiIndex.from_tuples([('fizz1', 'BUZZ1'), + ('fizz2', 'BUZZ2')], + names=['fizz', 'buzz']) + renamed = df.rename(columns=func, level=1) + self.assert_index_equal(renamed.columns, new_columns) + renamed = df.rename(columns=func, level='buzz') + self.assert_index_equal(renamed.columns, new_columns) + + # index + new_index = MultiIndex.from_tuples([('foo3', 'bar1'), + ('foo2', 'bar2')], + names=['foo', 'bar']) + renamed = df.rename(index={'foo1': 'foo3', 'bar2': 'bar3'}, + level=0) + self.assert_index_equal(renamed.index, new_index) def test_rename_nocopy(self): renamed = self.frame.rename(columns={'C': 'foo'}, copy=False)