Skip to content

ENH: level keyword in rename (GH4160) #13766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/source/whatsnew/v0.20.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://matplotlib.org/2.0.0/users/colors.html#cn-color-selection>`__. (: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 <timedeltas.isoformat>` (: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
Expand Down
9 changes: 8 additions & 1 deletion pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 10 additions & 4 deletions pandas/core/internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shoukd be in a rename method in Index
MI should also just iterate in the levels and call rename on that Index

should not be in internals

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's how rename is implemented at the moment ..
And AFAIK there is not a rename method on Index itself? (there is, but that is to change the name attribute, so something else)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I mean move it there
much cleaner to do it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I mean this comment. Do you mean it would be cleaner to have a method on Index/MultiIndex itself that does this renaming?
I can agree with that, but the problem is that there already is a 'rename' method which renames the names of a multi index (it could of course also be a private method)

But my preference is to leave the existing implementation intact in this PR (I just expanded the existing method a bit).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I mean move this to MultiIndex, and create a corresponding (private is fine) one for Index. ideally it would be rename, but I guess has to be a private method.

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]
Expand Down
67 changes: 61 additions & 6 deletions pandas/tests/frame/test_alter_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand All @@ -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)
Expand Down