Skip to content

Commit b06e726

Browse files
committed
API: Added axis argument to rename
xref: pandas-dev#12392
1 parent d12a7a0 commit b06e726

File tree

6 files changed

+193
-6
lines changed

6 files changed

+193
-6
lines changed

doc/source/whatsnew/v0.21.0.txt

+15
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,21 @@ For example:
111111
# the following is now equivalent
112112
df.drop(columns=['B', 'C'])
113113

114+
``rename`` now also accepts axis keyword
115+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
116+
117+
The :meth:`~DataFrame.rename` method has gained the ``axis`` keyword as an
118+
alternative to specify the ``axis`` to target (:issue:`12392`).
119+
120+
.. ipython::
121+
122+
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
123+
df.rename(str.lower, axis='columns')
124+
df.rename(id, axis='index')
125+
126+
The ``.rename(index=id, columns=str.lower)`` style continues to work as before.
127+
We *highly* encourage using named arguments to avoid confusion.
128+
114129
.. _whatsnew_0210.enhancements.categorical_dtype:
115130

116131
``CategoricalDtype`` for specifying categoricals

pandas/core/frame.py

+52-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@
111111
optional_by="""
112112
by : str or list of str
113113
Name or list of names which refer to the axis items.""",
114-
versionadded_to_excel='')
114+
versionadded_to_excel='',
115+
optional_mapper="""mapper : dict-like or function
116+
Applied to the axis specified by `axis`""",
117+
optional_axis="""axis : int or str, optional
118+
Axis to target. Can be either the axis name ('rows', 'columns')
119+
or number (0, 1).""",
120+
)
115121

116122
_numeric_only_doc = """numeric_only : boolean, default None
117123
Include only float, int, boolean data. If None, will attempt to use
@@ -2776,6 +2782,46 @@ def reindexer(value):
27762782

27772783
return np.atleast_2d(np.asarray(value))
27782784

2785+
def _validate_axis_style_args(self, arg, arg_name, index, columns,
2786+
axis, method_name):
2787+
if axis is not None:
2788+
# Using "axis" style, along with a positional arg
2789+
# Both index and columns should be None then
2790+
axis = self._get_axis_name(axis)
2791+
if index is not None or columns is not None:
2792+
msg = (
2793+
"Can't specify both 'axis' and 'index' or 'columns'. "
2794+
"Specify either\n"
2795+
"\t.{method_name}.rename({arg_name}, axis=axis), or\n"
2796+
"\t.{method_name}.rename(index=index, columns=columns)"
2797+
).format(arg_name=arg_name, method_name=method_name)
2798+
raise TypeError(msg)
2799+
if axis == 'index':
2800+
index = arg
2801+
elif axis == 'columns':
2802+
columns = arg
2803+
2804+
elif all(x is not None for x in (arg, index, columns)):
2805+
msg = (
2806+
"Cannot specify all of '{arg_name}', 'index', and 'columns'. "
2807+
"Specify either {arg_name} and 'axis', or 'index' and "
2808+
"'columns'."
2809+
).format(arg_name=arg_name)
2810+
raise TypeError(msg)
2811+
2812+
elif axis is None and (arg is not None and index is not None):
2813+
# This is the "ambiguous" case, so emit a warning
2814+
msg = (
2815+
"Interpreting call to '.{method_name}(a, b)' as "
2816+
"'.{method_name}(index=a, columns=b)'. "
2817+
"Use keyword arguments to remove any ambiguity."
2818+
).format(method_name=method_name)
2819+
warnings.warn(msg)
2820+
index, columns = arg, index
2821+
elif index is None and columns is None:
2822+
index = arg
2823+
return index, columns
2824+
27792825
@property
27802826
def _series(self):
27812827
result = {}
@@ -2915,7 +2961,11 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True,
29152961
limit=limit, fill_value=fill_value)
29162962

29172963
@Appender(_shared_docs['rename'] % _shared_doc_kwargs)
2918-
def rename(self, index=None, columns=None, **kwargs):
2964+
def rename(self, mapper=None, index=None, columns=None, axis=None,
2965+
**kwargs):
2966+
index, columns = self._validate_axis_style_args(mapper, 'mapper',
2967+
index, columns,
2968+
axis, 'rename')
29192969
return super(DataFrame, self).rename(index=index, columns=columns,
29202970
**kwargs)
29212971

pandas/core/generic.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -742,11 +742,13 @@ def swaplevel(self, i=-2, j=-1, axis=0):
742742
743743
Parameters
744744
----------
745+
%(optional_mapper)s
745746
%(axes)s : scalar, list-like, dict-like or function, optional
746747
Scalar or list-like will alter the ``Series.name`` attribute,
747748
and raise on DataFrame or Panel.
748749
dict-like or functions are transformations to apply to
749750
that axis' values
751+
%(optional_axis)s
750752
copy : boolean, default True
751753
Also copy underlying data
752754
inplace : boolean, default False
@@ -787,6 +789,7 @@ def swaplevel(self, i=-2, j=-1, axis=0):
787789
3 2
788790
5 3
789791
dtype: int64
792+
790793
>>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
791794
>>> df.rename(2)
792795
Traceback (most recent call last):
@@ -802,12 +805,27 @@ def swaplevel(self, i=-2, j=-1, axis=0):
802805
0 1 4
803806
1 2 5
804807
2 3 6
808+
809+
Using axis-style parameters
810+
811+
>>> df.rename(str.lower, axis='columns')
812+
a b
813+
0 1 4
814+
1 2 5
815+
2 3 6
816+
817+
>>> df.rename({1: 2, 2: 4}, axis='index')
818+
A B
819+
0 1 4
820+
2 2 5
821+
4 3 6
805822
"""
806823

807824
@Appender(_shared_docs['rename'] % dict(axes='axes keywords for this'
808-
' object', klass='NDFrame'))
825+
' object', klass='NDFrame',
826+
optional_mapper='',
827+
optional_axis=''))
809828
def rename(self, *args, **kwargs):
810-
811829
axes, kwargs = self._construct_axes_from_arguments(args, kwargs)
812830
copy = kwargs.pop('copy', True)
813831
inplace = kwargs.pop('inplace', False)

pandas/core/panel.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
_shared_doc_kwargs = dict(
4040
axes='items, major_axis, minor_axis',
4141
klass="Panel",
42-
axes_single_arg="{0, 1, 2, 'items', 'major_axis', 'minor_axis'}")
42+
axes_single_arg="{0, 1, 2, 'items', 'major_axis', 'minor_axis'}",
43+
optional_mapper='', optional_axis='')
4344
_shared_doc_kwargs['args_transpose'] = ("three positional arguments: each one"
4445
"of\n%s" %
4546
_shared_doc_kwargs['axes_single_arg'])

pandas/core/series.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
inplace="""inplace : boolean, default False
8686
If True, performs operation inplace and returns None.""",
8787
unique='np.ndarray', duplicated='Series',
88-
optional_by='',
88+
optional_by='', optional_mapper='', optional_axis='',
8989
versionadded_to_excel='\n .. versionadded:: 0.20.0\n')
9090

9191

pandas/tests/frame/test_alter_axes.py

+103
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,101 @@ def test_rename_objects(self):
837837
assert 'FOO' in renamed
838838
assert 'foo' not in renamed
839839

840+
def test_rename_columns(self):
841+
df = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['X', 'Y'])
842+
expected = pd.DataFrame({"a": [1, 2], "b": [1, 2]}, index=['X', 'Y'])
843+
844+
result = df.rename(str.lower, axis=1)
845+
assert_frame_equal(result, expected)
846+
847+
result = df.rename(str.lower, axis='columns')
848+
assert_frame_equal(result, expected)
849+
850+
result = df.rename({"A": 'a', 'B': 'b'}, axis=1)
851+
assert_frame_equal(result, expected)
852+
853+
result = df.rename({"A": 'a', 'B': 'b'}, axis='columns')
854+
assert_frame_equal(result, expected)
855+
856+
# Index
857+
expected = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['x', 'y'])
858+
result = df.rename(str.lower, axis=0)
859+
assert_frame_equal(result, expected)
860+
861+
result = df.rename(str.lower, axis='index')
862+
assert_frame_equal(result, expected)
863+
864+
result = df.rename({'X': 'x', 'Y': 'y'}, axis=0)
865+
assert_frame_equal(result, expected)
866+
867+
result = df.rename({'X': 'x', 'Y': 'y'}, axis='index')
868+
assert_frame_equal(result, expected)
869+
870+
def test_rename_mapper_multi(self):
871+
df = pd.DataFrame({"A": ['a', 'b'], "B": ['c', 'd'],
872+
'C': [1, 2]}).set_index(["A", "B"])
873+
result = df.rename(str.upper)
874+
expected = df.rename(index=str.upper)
875+
assert_frame_equal(result, expected)
876+
877+
def test_rename_raises(self):
878+
df = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['0', '1'])
879+
880+
# Named target and axis
881+
with tm.assert_raises_regex(TypeError, None):
882+
df.rename(index=str.lower, axis=1)
883+
884+
with tm.assert_raises_regex(TypeError, None):
885+
df.rename(index=str.lower, axis='columns')
886+
887+
with tm.assert_raises_regex(TypeError, None):
888+
df.rename(index=str.lower, axis=0)
889+
890+
with tm.assert_raises_regex(TypeError, None):
891+
df.rename(index=str.lower, axis='columns')
892+
893+
with tm.assert_raises_regex(TypeError, None):
894+
df.rename(columns=str.lower, axis='columns')
895+
896+
# Multiple targets and axis
897+
with tm.assert_raises_regex(TypeError, None):
898+
df.rename(str.lower, str.lower, axis='columns')
899+
900+
# Too many targets
901+
with tm.assert_raises_regex(TypeError, None):
902+
df.rename(str.lower, str.lower, str.lower)
903+
904+
def test_drop_api_equivalence(self):
905+
# equivalence of the labels/axis and index/columns API's
906+
df = DataFrame([[1, 2, 3], [3, 4, 5], [5, 6, 7]],
907+
index=['a', 'b', 'c'],
908+
columns=['d', 'e', 'f'])
909+
910+
res1 = df.drop('a')
911+
res2 = df.drop(index='a')
912+
tm.assert_frame_equal(res1, res2)
913+
914+
res1 = df.drop('d', 1)
915+
res2 = df.drop(columns='d')
916+
tm.assert_frame_equal(res1, res2)
917+
918+
res1 = df.drop(labels='e', axis=1)
919+
res2 = df.drop(columns='e')
920+
tm.assert_frame_equal(res1, res2)
921+
922+
res1 = df.drop(['a'], axis=0)
923+
res2 = df.drop(index=['a'])
924+
tm.assert_frame_equal(res1, res2)
925+
926+
res1 = df.drop(['a'], axis=0).drop(['d'], axis=1)
927+
res2 = df.drop(index=['a'], columns=['d'])
928+
929+
with pytest.raises(ValueError):
930+
df.drop(labels='a', index='b')
931+
932+
with pytest.raises(ValueError):
933+
df.drop(axis=1)
934+
840935
def test_assign_columns(self):
841936
self.frame['hi'] = 'there'
842937

@@ -860,6 +955,14 @@ def test_set_index_preserve_categorical_dtype(self):
860955
result = result.reindex(columns=df.columns)
861956
tm.assert_frame_equal(result, df)
862957

958+
def test_ambiguous_warns(self):
959+
df = pd.DataFrame({"A": [1, 2]})
960+
with tm.assert_produces_warning(UserWarning):
961+
df.rename(id, id)
962+
963+
with tm.assert_produces_warning(UserWarning):
964+
df.rename({0: 10}, {"A": "B"})
965+
863966

864967
class TestIntervalIndex(object):
865968

0 commit comments

Comments
 (0)