Skip to content

ENH: Allow rename_axis to specify index and columns arguments #20046

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 19 commits into from
Oct 29, 2018
Merged
Show file tree
Hide file tree
Changes from 4 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
19 changes: 17 additions & 2 deletions doc/source/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1465,10 +1465,25 @@ for altering the ``Series.name`` attribute.

s.rename("scalar-name")

The Panel class has a related :meth:`~Panel.rename` class which can rename
Copy link
Contributor

Choose a reason for hiding this comment

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

this is not needed, we are not advertising Panel any longer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I deleted that, but Panel appears a lot in that document!

Copy link
Contributor

Choose a reason for hiding this comment

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

i know, but no need to add at this point

any of its three axes.

.. _basics.rename_axis:

The Panel class has a related :meth:`~Panel.rename_axis` class which can rename
any of its three axes.
.. versionadded:: 0.23.0

The methods :meth:`~DataFrame.rename_axis` and :meth:`~Series.rename_axis`
allow specific names of a `MultiIndex` to be changed (as opposed to the
labels).

.. ipython:: python

df = pd.DataFrame({'x': [1,2,3,4,5,6], 'y': [10,20,30,40,50,60]},
Copy link
Contributor

Choose a reason for hiding this comment

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

can you make this pep-y, iow spaces after commas (you can also line up the x and y columns on separate lines)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

index=pd.MultiIndex.from_product([['a','b','c'],[1,2]],
names=['let','num']))
df
df.rename_axis(index={'let': 'abc'})
df.rename_axis(index=str.upper)

.. _basics.iteration:

Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ Other Enhancements
- Added option ``display.html.use_mathjax`` so `MathJax <https://www.mathjax.org/>`_ can be disabled when rendering tables in ``Jupyter`` notebooks (:issue:`19856`, :issue:`19824`)
- :meth:`Timestamp.month_name`, :meth:`DatetimeIndex.month_name`, and :meth:`Series.dt.month_name` are now available (:issue:`12805`)
- :meth:`Timestamp.day_name` and :meth:`DatetimeIndex.day_name` are now available to return day names with a specified locale (:issue:`12806`)
- :func:`DataFrame.rename_axis` supports ``index`` and ``columns`` arguments and :func:`Series.rename_axis` supports ``index`` argument (:issue:`19978`)

.. _whatsnew_0230.api_breaking:

Expand Down
155 changes: 126 additions & 29 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,20 @@ def swaplevel(self, i=-2, j=-1, axis=0):
# ----------------------------------------------------------------------
# Rename

# renamer function if passed a dict
Copy link
Member

Choose a reason for hiding this comment

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

I know you just moved the function, but let's take this opportunity to write a brief docstring.

def _get_rename_function(self, mapper):
if isinstance(mapper, (dict, ABCSeries)):

def f(x):
if x in mapper:
return mapper[x]
else:
return x
else:
f = mapper

return f

# TODO: define separate funcs for DataFrame, Series and Panel so you can
# get completion on keyword arguments.
_shared_docs['rename'] = """
Expand Down Expand Up @@ -874,20 +888,6 @@ def rename(self, *args, **kwargs):
if com._count_not_none(*axes.values()) == 0:
raise TypeError('must pass an index to rename')

# renamer function if passed a dict
def _get_rename_function(mapper):
if isinstance(mapper, (dict, ABCSeries)):

def f(x):
if x in mapper:
return mapper[x]
else:
return x
else:
f = mapper

return f

self._consolidate_inplace()
result = self if inplace else self.copy(deep=copy)

Expand All @@ -896,7 +896,7 @@ def f(x):
v = axes.get(self._AXIS_NAMES[axis])
if v is None:
continue
f = _get_rename_function(v)
f = self._get_rename_function(v)

baxis = self._get_block_manager_axis(axis)
if level is not None:
Expand All @@ -912,13 +912,21 @@ def f(x):

rename.__doc__ = _shared_docs['rename']

def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
"""Alter the name of the index or columns.
def rename_axis(self, mapper=None, **kwargs):
"""Alter the name of the index or name of index backing the
columns.
Copy link
Member

Choose a reason for hiding this comment

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

Let's keep this to be one line.


Parameters
----------
mapper : scalar, list-like, optional
Value to set the axis name attribute.
index, columns : scalar, list-like, dict-like or function, optional
dict-like or functions transformations to apply to
that axis' values.

Use either ``mapper`` and ``axis`` to
specify the axis to target with ``mapper``, or ``index``
and/or ``columns``.
Copy link
Contributor

Choose a reason for hiding this comment

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

add a versionchanged

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

axis : int or string, default 0
copy : boolean, default True
Also copy underlying data
Expand All @@ -935,6 +943,23 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
deprecated and will be removed in a future version. Use ``rename``
instead.

``DataFrame.rename_axis`` supports two calling conventions

* ``(index=index_mapper, columns=columns_mapper, ...)``
* ``(mapper, axis={'index', 'columns'}, ...)``

The first calling convention will only modify the names of
the index and/or the names of the index backing the columns.
Copy link
Contributor

Choose a reason for hiding this comment

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

same: index -> Index, or just remove.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed to "names of the Index object that is the columns."

In this case, the parameter ``copy`` is ignored.

The second calling convention will modify the names of the
the corresponding index if mapper is a list or a scalar.
However, if mapper is dict-like or a function, it will use the
deprecated behavior of modifying the axis *labels*.

We *highly* recommend using keyword arguments to clarify your
intent.

See Also
--------
pandas.Series.rename, pandas.DataFrame.rename
Expand All @@ -957,20 +982,92 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
1 2 5
2 3 6

"""
>>> mi = pd.MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
names=['let','num'])
>>> df = pd.DataFrame({'x': [i for i in range(len(mi))],
'y' : [i*10 for i in range(len(mi))]},
index=mi)
>>> df.rename_axis(index={'num' : 'n'})
x y
let n
a 1 0 0
2 1 10
b 1 2 20
2 3 30
c 1 4 40
2 5 50

>>> cdf = df.rename_axis(columns='col')
>>> cdf
col x y
let num
a 1 0 0
2 1 10
b 1 2 20
2 3 30
c 1 4 40
2 5 50

>>> cdf.rename_axis(columns=str.upper)
COL x y
let num
a 1 0 0
2 1 10
b 1 2 20
2 3 30
c 1 4 40
2 5 50

"""
axes, kwargs = self._construct_axes_from_arguments((), kwargs)
copy = kwargs.pop('copy', True)
inplace = kwargs.pop('inplace', False)
axis = kwargs.pop('axis', 0)
if axis is not None:
axis = self._get_axis_number(axis)

if kwargs:
raise TypeError('rename_axis() got an unexpected keyword '
'argument "{0}"'.format(list(kwargs.keys())[0]))
inplace = validate_bool_kwarg(inplace, 'inplace')
non_mapper = is_scalar(mapper) or (is_list_like(mapper) and not
is_dict_like(mapper))
if non_mapper:
return self._set_axis_name(mapper, axis=axis, inplace=inplace)

if (mapper is not None):
# Use v0.23 behavior if a scalar or list
non_mapper = is_scalar(mapper) or (is_list_like(mapper) and not
is_dict_like(mapper))
if non_mapper:
return self._set_axis_name(mapper, axis=axis, inplace=inplace)
else:
# Deprecated (v0.21) behavior is if mapper is specified,
# and not a list or scalar, then call rename
msg = ("Using 'rename_axis' to alter labels is deprecated. "
"Use '.rename' instead")
warnings.warn(msg, FutureWarning, stacklevel=2)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if you increase stack level by 1, you can revert your changes to the tests below. The stack level increased by 1 since you're using the decorator to fix the function signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the tip. It worked.

axis = self._get_axis_name(axis)
d = {'copy': copy, 'inplace': inplace}
d[axis] = mapper
return self.rename(**d)
else:
msg = ("Using 'rename_axis' to alter labels is deprecated. "
"Use '.rename' instead")
warnings.warn(msg, FutureWarning, stacklevel=2)
axis = self._get_axis_name(axis)
d = {'copy': copy, 'inplace': inplace}
d[axis] = mapper
return self.rename(**d)
# Use new behavior. Means that index and/or columns
# is specified
result = self if inplace else self.copy(deep=copy)

for axis in lrange(self._AXIS_LEN):
v = axes.get(self._AXIS_NAMES[axis])
if v is None:
continue
non_mapper = is_scalar(v) or (is_list_like(v) and not
is_dict_like(v))
if non_mapper:
newnames = v
else:
f = self._get_rename_function(v)
curnames = self._get_axis(axis).names
newnames = [f(name) for name in curnames]
result._set_axis_name(newnames, axis=axis,
inplace=True)
if not inplace:
return result

def _set_axis_name(self, name, axis=0, inplace=False):
"""
Expand Down
38 changes: 38 additions & 0 deletions pandas/tests/frame/test_alter_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,44 @@ def test_rename_axis_warns(self):
df['A'].rename_axis(id)
assert 'rename' in str(w[0].message)

def test_rename_axis_mapper(self):
# GH 19978
mi = MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
names=['ll', 'nn'])
df = DataFrame({'x': [i for i in range(len(mi))],
'y': [i * 10 for i in range(len(mi))]},
Copy link
Contributor

Choose a reason for hiding this comment

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

can you sprinkle some comments to delineate the cases you are testing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

index=mi)
result = df.rename_axis('cols', axis=1)
tm.assert_index_equal(result.columns,
Index(['x', 'y'], name='cols'))

result = result.rename_axis(columns={'cols': 'new'}, axis=1)
tm.assert_index_equal(result.columns,
Index(['x', 'y'], name='new'))

result = df.rename_axis(index={'ll': 'foo'})
assert result.index.names == ['foo', 'nn']

result = df.rename_axis(index=str.upper, axis=0)
assert result.index.names == ['LL', 'NN']

result = df.rename_axis(index=['foo', 'goo'])
assert result.index.names == ['foo', 'goo']

sdf = df.reset_index().set_index('nn').drop(columns=['ll', 'y'])
result = sdf.rename_axis(index='foo', columns='meh')
assert result.index.name == 'foo'
assert result.columns.name == 'meh'

with tm.assert_raises_regex(TypeError, 'Must pass'):
df.rename_axis(index='wrong')

with tm.assert_raises_regex(ValueError, 'Length of names'):
df.rename_axis(index=['wrong'])

with tm.assert_raises_regex(TypeError, 'bogus'):
df.rename_axis(bogus=None)

def test_rename_multiindex(self):

tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]
Expand Down
4 changes: 2 additions & 2 deletions pandas/tests/io/test_pytables.py
Original file line number Diff line number Diff line change
Expand Up @@ -1288,8 +1288,8 @@ def test_append_with_strings(self):
with ensure_clean_store(self.path) as store:
with catch_warnings(record=True):
wp = tm.makePanel()
wp2 = wp.rename_axis(
{x: "%s_extra" % x for x in wp.minor_axis}, axis=2)
wp2 = wp.rename(
minor_axis={x: "%s_extra" % x for x in wp.minor_axis})

def check_col(key, name, size):
assert getattr(store.get_storer(key)
Expand Down
9 changes: 4 additions & 5 deletions pandas/tests/reshape/test_concat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1401,12 +1401,11 @@ def df():
panel1 = make_panel()
panel2 = make_panel()

panel2 = panel2.rename_axis(dict((x, "%s_1" % x)
for x in panel2.major_axis),
axis=1)
panel2 = panel2.rename(major_axis=dict((x, "%s_1" % x)
for x in panel2.major_axis))

panel3 = panel2.rename_axis(lambda x: '%s_1' % x, axis=1)
panel3 = panel3.rename_axis(lambda x: '%s_1' % x, axis=2)
panel3 = panel2.rename(major_axis=lambda x: '%s_1' % x)
panel3 = panel3.rename(minor_axis=lambda x: '%s_1' % x)

# it works!
concat([panel1, panel3], axis=1, verify_integrity=True)
Expand Down
18 changes: 18 additions & 0 deletions pandas/tests/series/test_alter_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,24 @@ def test_reorder_levels(self):
expected = Series(np.arange(6), index=e_idx)
assert_series_equal(result, expected)

def test_rename_axis_mapper(self):
# GH 19978
mi = MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
names=['ll', 'nn'])
s = Series([i for i in range(len(mi))], index=mi)

result = s.rename_axis(index={'ll': 'foo'})
assert result.index.names == ['foo', 'nn']

result = s.rename_axis(index=str.upper, axis=0)
assert result.index.names == ['LL', 'NN']

result = s.rename_axis(index=['foo', 'goo'])
assert result.index.names == ['foo', 'goo']

with tm.assert_raises_regex(TypeError, 'unexpected'):
s.rename_axis(columns='wrong')

def test_rename_axis_inplace(self):
# GH 15704
series = self.ts.copy()
Expand Down
6 changes: 3 additions & 3 deletions pandas/tests/test_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2176,16 +2176,16 @@ def test_rename(self):
with catch_warnings(record=True):
mapper = {'ItemA': 'foo', 'ItemB': 'bar', 'ItemC': 'baz'}

renamed = self.panel.rename_axis(mapper, axis=0)
renamed = self.panel.rename(items=mapper)
exp = Index(['foo', 'bar', 'baz'])
tm.assert_index_equal(renamed.items, exp)

renamed = self.panel.rename_axis(str.lower, axis=2)
renamed = self.panel.rename(minor_axis=str.lower)
exp = Index(['a', 'b', 'c', 'd'])
tm.assert_index_equal(renamed.minor_axis, exp)

# don't copy
renamed_nocopy = self.panel.rename_axis(mapper, axis=0, copy=False)
renamed_nocopy = self.panel.rename(items=mapper, copy=False)
renamed_nocopy['foo'] = 3.
assert (self.panel['ItemA'].values == 3).all()

Expand Down