Skip to content

Commit 9012100

Browse files
Dr-IrvPingviinituutti
authored andcommitted
ENH: Allow rename_axis to specify index and columns arguments (pandas-dev#20046)
1 parent 14d2f44 commit 9012100

File tree

10 files changed

+293
-45
lines changed

10 files changed

+293
-45
lines changed

doc/source/advanced.rst

+41
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,47 @@ method, allowing you to permute the hierarchical index levels in one step:
503503
504504
df[:5].reorder_levels([1,0], axis=0)
505505
506+
.. _advanced.index_names:
507+
508+
Renaming names of an ``Index`` or ``MultiIndex``
509+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
510+
511+
The :meth:`~DataFrame.rename` method is used to rename the labels of a
512+
``MultiIndex``, and is typically used to rename the columns of a ``DataFrame``.
513+
The ``columns`` argument of ``rename`` allows a dictionary to be specified
514+
that includes only the columns you wish to rename.
515+
516+
.. ipython:: python
517+
518+
df.rename(columns={0: "col0", 1: "col1"})
519+
520+
This method can also be used to rename specific labels of the main index
521+
of the ``DataFrame``.
522+
523+
.. ipython:: python
524+
525+
df.rename(index={"one" : "two", "y" : "z"})
526+
527+
The :meth:`~DataFrame.rename_axis` method is used to rename the name of a
528+
``Index`` or ``MultiIndex``. In particular, the names of the levels of a
529+
``MultiIndex`` can be specified, which is useful if ``reset_index()`` is later
530+
used to move the values from the ``MultiIndex`` to a column.
531+
532+
.. ipython:: python
533+
534+
df.rename_axis(index=['abc', 'def'])
535+
536+
Note that the columns of a ``DataFrame`` are an index, so that using
537+
``rename_axis`` with the ``columns`` argument will change the name of that
538+
index.
539+
540+
.. ipython:: python
541+
542+
df.rename_axis(columns="Cols").columns
543+
544+
Both ``rename`` and ``rename_axis`` support specifying a dictionary,
545+
``Series`` or a mapping function to map labels/names to new values.
546+
506547
Sorting a ``MultiIndex``
507548
------------------------
508549

doc/source/basics.rst

+15-2
Original file line numberDiff line numberDiff line change
@@ -1466,8 +1466,21 @@ for altering the ``Series.name`` attribute.
14661466
14671467
.. _basics.rename_axis:
14681468

1469-
The Panel class has a related :meth:`~Panel.rename_axis` class which can rename
1470-
any of its three axes.
1469+
.. versionadded:: 0.24.0
1470+
1471+
The methods :meth:`~DataFrame.rename_axis` and :meth:`~Series.rename_axis`
1472+
allow specific names of a `MultiIndex` to be changed (as opposed to the
1473+
labels).
1474+
1475+
.. ipython:: python
1476+
1477+
df = pd.DataFrame({'x': [1, 2, 3, 4, 5, 6],
1478+
'y': [10, 20, 30, 40, 50, 60]},
1479+
index=pd.MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
1480+
names=['let', 'num']))
1481+
df
1482+
df.rename_axis(index={'let': 'abc'})
1483+
df.rename_axis(index=str.upper)
14711484
14721485
.. _basics.iteration:
14731486

doc/source/whatsnew/v0.24.0.txt

+23
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,29 @@ array, but rather an ``ExtensionArray``:
180180
This is the same behavior as ``Series.values`` for categorical data. See
181181
:ref:`whatsnew_0240.api_breaking.interval_values` for more.
182182

183+
.. _whatsnew_0240.enhancements.rename_axis:
184+
185+
Renaming names in a MultiIndex
186+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187+
188+
:func:`DataFrame.rename_axis` now supports ``index`` and ``columns`` arguments
189+
and :func:`Series.rename_axis` supports ``index`` argument (:issue:`19978`)
190+
191+
This change allows a dictionary to be passed so that some of the names
192+
of a ``MultiIndex`` can be changed.
193+
194+
Example:
195+
196+
.. ipython:: python
197+
198+
mi = pd.MultiIndex.from_product([list('AB'), list('CD'), list('EF')],
199+
names=['AB', 'CD', 'EF'])
200+
df = pd.DataFrame([i for i in range(len(mi))], index=mi, columns=['N'])
201+
df
202+
df.rename_axis(index={'CD': 'New'})
203+
204+
See the :ref:`advanced docs on renaming<advanced.index_names>` for more details.
205+
183206
.. _whatsnew_0240.enhancements.other:
184207

185208
Other Enhancements

pandas/core/common.py

+18
Original file line numberDiff line numberDiff line change
@@ -454,3 +454,21 @@ def _pipe(obj, func, *args, **kwargs):
454454
return func(*args, **kwargs)
455455
else:
456456
return func(obj, *args, **kwargs)
457+
458+
459+
def _get_rename_function(mapper):
460+
"""
461+
Returns a function that will map names/labels, dependent if mapper
462+
is a dict, Series or just a function.
463+
"""
464+
if isinstance(mapper, (compat.Mapping, ABCSeries)):
465+
466+
def f(x):
467+
if x in mapper:
468+
return mapper[x]
469+
else:
470+
return x
471+
else:
472+
f = mapper
473+
474+
return f

pandas/core/generic.py

+123-33
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
isidentifier, set_function_name, cPickle as pkl)
5454
from pandas.core.ops import _align_method_FRAME
5555
import pandas.core.nanops as nanops
56-
from pandas.util._decorators import Appender, Substitution
56+
from pandas.util._decorators import (Appender, Substitution,
57+
rewrite_axis_style_signature)
5758
from pandas.util._validators import validate_bool_kwarg, validate_fillna_kwargs
5859
from pandas.core import config
5960

@@ -1079,20 +1080,6 @@ def rename(self, *args, **kwargs):
10791080
if com.count_not_none(*axes.values()) == 0:
10801081
raise TypeError('must pass an index to rename')
10811082

1082-
# renamer function if passed a dict
1083-
def _get_rename_function(mapper):
1084-
if isinstance(mapper, (dict, ABCSeries)):
1085-
1086-
def f(x):
1087-
if x in mapper:
1088-
return mapper[x]
1089-
else:
1090-
return x
1091-
else:
1092-
f = mapper
1093-
1094-
return f
1095-
10961083
self._consolidate_inplace()
10971084
result = self if inplace else self.copy(deep=copy)
10981085

@@ -1101,7 +1088,7 @@ def f(x):
11011088
v = axes.get(self._AXIS_NAMES[axis])
11021089
if v is None:
11031090
continue
1104-
f = _get_rename_function(v)
1091+
f = com._get_rename_function(v)
11051092

11061093
baxis = self._get_block_manager_axis(axis)
11071094
if level is not None:
@@ -1115,16 +1102,28 @@ def f(x):
11151102
else:
11161103
return result.__finalize__(self)
11171104

1118-
def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
1105+
@rewrite_axis_style_signature('mapper', [('copy', True),
1106+
('inplace', False)])
1107+
def rename_axis(self, mapper=None, **kwargs):
11191108
"""
1120-
Alter the name of the index or columns.
1109+
Alter the name of the index or name of Index object that is the
1110+
columns.
11211111
11221112
Parameters
11231113
----------
11241114
mapper : scalar, list-like, optional
1125-
Value to set as the axis name attribute.
1126-
axis : {0 or 'index', 1 or 'columns'}, default 0
1127-
The index or the name of the axis.
1115+
Value to set the axis name attribute.
1116+
index, columns : scalar, list-like, dict-like or function, optional
1117+
dict-like or functions transformations to apply to
1118+
that axis' values.
1119+
1120+
Use either ``mapper`` and ``axis`` to
1121+
specify the axis to target with ``mapper``, or ``index``
1122+
and/or ``columns``.
1123+
1124+
.. versionchanged:: 0.24.0
1125+
1126+
axis : int or string, default 0
11281127
copy : boolean, default True
11291128
Also copy underlying data.
11301129
inplace : boolean, default False
@@ -1143,6 +1142,23 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
11431142
deprecated and will be removed in a future version. Use ``rename``
11441143
instead.
11451144
1145+
``DataFrame.rename_axis`` supports two calling conventions
1146+
1147+
* ``(index=index_mapper, columns=columns_mapper, ...)``
1148+
* ``(mapper, axis={'index', 'columns'}, ...)``
1149+
1150+
The first calling convention will only modify the names of
1151+
the index and/or the names of the Index object that is the columns.
1152+
In this case, the parameter ``copy`` is ignored.
1153+
1154+
The second calling convention will modify the names of the
1155+
the corresponding index if mapper is a list or a scalar.
1156+
However, if mapper is dict-like or a function, it will use the
1157+
deprecated behavior of modifying the axis *labels*.
1158+
1159+
We *highly* recommend using keyword arguments to clarify your
1160+
intent.
1161+
11461162
See Also
11471163
--------
11481164
pandas.Series.rename : Alter Series index labels or name
@@ -1176,20 +1192,94 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
11761192
0 1 4
11771193
1 2 5
11781194
2 3 6
1179-
"""
1195+
1196+
>>> mi = pd.MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
1197+
... names=['let','num'])
1198+
>>> df = pd.DataFrame({'x': [i for i in range(len(mi))],
1199+
... 'y' : [i*10 for i in range(len(mi))]},
1200+
... index=mi)
1201+
>>> df.rename_axis(index={'num' : 'n'})
1202+
x y
1203+
let n
1204+
a 1 0 0
1205+
2 1 10
1206+
b 1 2 20
1207+
2 3 30
1208+
c 1 4 40
1209+
2 5 50
1210+
1211+
>>> cdf = df.rename_axis(columns='col')
1212+
>>> cdf
1213+
col x y
1214+
let num
1215+
a 1 0 0
1216+
2 1 10
1217+
b 1 2 20
1218+
2 3 30
1219+
c 1 4 40
1220+
2 5 50
1221+
1222+
>>> cdf.rename_axis(columns=str.upper)
1223+
COL x y
1224+
let num
1225+
a 1 0 0
1226+
2 1 10
1227+
b 1 2 20
1228+
2 3 30
1229+
c 1 4 40
1230+
2 5 50
1231+
1232+
"""
1233+
axes, kwargs = self._construct_axes_from_arguments((), kwargs)
1234+
copy = kwargs.pop('copy', True)
1235+
inplace = kwargs.pop('inplace', False)
1236+
axis = kwargs.pop('axis', 0)
1237+
if axis is not None:
1238+
axis = self._get_axis_number(axis)
1239+
1240+
if kwargs:
1241+
raise TypeError('rename_axis() got an unexpected keyword '
1242+
'argument "{0}"'.format(list(kwargs.keys())[0]))
1243+
11801244
inplace = validate_bool_kwarg(inplace, 'inplace')
1181-
non_mapper = is_scalar(mapper) or (is_list_like(mapper) and not
1182-
is_dict_like(mapper))
1183-
if non_mapper:
1184-
return self._set_axis_name(mapper, axis=axis, inplace=inplace)
1245+
1246+
if (mapper is not None):
1247+
# Use v0.23 behavior if a scalar or list
1248+
non_mapper = is_scalar(mapper) or (is_list_like(mapper) and not
1249+
is_dict_like(mapper))
1250+
if non_mapper:
1251+
return self._set_axis_name(mapper, axis=axis, inplace=inplace)
1252+
else:
1253+
# Deprecated (v0.21) behavior is if mapper is specified,
1254+
# and not a list or scalar, then call rename
1255+
msg = ("Using 'rename_axis' to alter labels is deprecated. "
1256+
"Use '.rename' instead")
1257+
warnings.warn(msg, FutureWarning, stacklevel=3)
1258+
axis = self._get_axis_name(axis)
1259+
d = {'copy': copy, 'inplace': inplace}
1260+
d[axis] = mapper
1261+
return self.rename(**d)
11851262
else:
1186-
msg = ("Using 'rename_axis' to alter labels is deprecated. "
1187-
"Use '.rename' instead")
1188-
warnings.warn(msg, FutureWarning, stacklevel=2)
1189-
axis = self._get_axis_name(axis)
1190-
d = {'copy': copy, 'inplace': inplace}
1191-
d[axis] = mapper
1192-
return self.rename(**d)
1263+
# Use new behavior. Means that index and/or columns
1264+
# is specified
1265+
result = self if inplace else self.copy(deep=copy)
1266+
1267+
for axis in lrange(self._AXIS_LEN):
1268+
v = axes.get(self._AXIS_NAMES[axis])
1269+
if v is None:
1270+
continue
1271+
non_mapper = is_scalar(v) or (is_list_like(v) and not
1272+
is_dict_like(v))
1273+
if non_mapper:
1274+
newnames = v
1275+
else:
1276+
f = com._get_rename_function(v)
1277+
curnames = self._get_axis(axis).names
1278+
newnames = [f(name) for name in curnames]
1279+
result._set_axis_name(newnames, axis=axis,
1280+
inplace=True)
1281+
if not inplace:
1282+
return result
11931283

11941284
def _set_axis_name(self, name, axis=0, inplace=False):
11951285
"""

pandas/tests/frame/test_alter_axes.py

+46
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,52 @@ def test_rename_axis_warns(self):
538538
df['A'].rename_axis(id)
539539
assert 'rename' in str(w[0].message)
540540

541+
def test_rename_axis_mapper(self):
542+
# GH 19978
543+
mi = MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
544+
names=['ll', 'nn'])
545+
df = DataFrame({'x': [i for i in range(len(mi))],
546+
'y': [i * 10 for i in range(len(mi))]},
547+
index=mi)
548+
549+
# Test for rename of the Index object of columns
550+
result = df.rename_axis('cols', axis=1)
551+
tm.assert_index_equal(result.columns,
552+
Index(['x', 'y'], name='cols'))
553+
554+
# Test for rename of the Index object of columns using dict
555+
result = result.rename_axis(columns={'cols': 'new'}, axis=1)
556+
tm.assert_index_equal(result.columns,
557+
Index(['x', 'y'], name='new'))
558+
559+
# Test for renaming index using dict
560+
result = df.rename_axis(index={'ll': 'foo'})
561+
assert result.index.names == ['foo', 'nn']
562+
563+
# Test for renaming index using a function
564+
result = df.rename_axis(index=str.upper, axis=0)
565+
assert result.index.names == ['LL', 'NN']
566+
567+
# Test for renaming index providing complete list
568+
result = df.rename_axis(index=['foo', 'goo'])
569+
assert result.index.names == ['foo', 'goo']
570+
571+
# Test for changing index and columns at same time
572+
sdf = df.reset_index().set_index('nn').drop(columns=['ll', 'y'])
573+
result = sdf.rename_axis(index='foo', columns='meh')
574+
assert result.index.name == 'foo'
575+
assert result.columns.name == 'meh'
576+
577+
# Test different error cases
578+
with tm.assert_raises_regex(TypeError, 'Must pass'):
579+
df.rename_axis(index='wrong')
580+
581+
with tm.assert_raises_regex(ValueError, 'Length of names'):
582+
df.rename_axis(index=['wrong'])
583+
584+
with tm.assert_raises_regex(TypeError, 'bogus'):
585+
df.rename_axis(bogus=None)
586+
541587
def test_rename_multiindex(self):
542588

543589
tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]

pandas/tests/io/test_pytables.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1345,8 +1345,8 @@ def test_append_with_strings(self):
13451345
with catch_warnings(record=True):
13461346
simplefilter("ignore", FutureWarning)
13471347
wp = tm.makePanel()
1348-
wp2 = wp.rename_axis(
1349-
{x: "%s_extra" % x for x in wp.minor_axis}, axis=2)
1348+
wp2 = wp.rename(
1349+
minor_axis={x: "%s_extra" % x for x in wp.minor_axis})
13501350

13511351
def check_col(key, name, size):
13521352
assert getattr(store.get_storer(key)

0 commit comments

Comments
 (0)