Skip to content

Commit

Permalink
API: Added axis argument to rename
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAugspurger committed Oct 10, 2017
1 parent d12a7a0 commit b06e726
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 6 deletions.
15 changes: 15 additions & 0 deletions doc/source/whatsnew/v0.21.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ For example:
# the following is now equivalent
df.drop(columns=['B', 'C'])

``rename`` now also accepts axis keyword
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The :meth:`~DataFrame.rename` method has gained the ``axis`` keyword as an
alternative to specify the ``axis`` to target (:issue:`12392`).

.. ipython::

df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
df.rename(str.lower, axis='columns')
df.rename(id, axis='index')

The ``.rename(index=id, columns=str.lower)`` style continues to work as before.
We *highly* encourage using named arguments to avoid confusion.

.. _whatsnew_0210.enhancements.categorical_dtype:

``CategoricalDtype`` for specifying categoricals
Expand Down
54 changes: 52 additions & 2 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@
optional_by="""
by : str or list of str
Name or list of names which refer to the axis items.""",
versionadded_to_excel='')
versionadded_to_excel='',
optional_mapper="""mapper : dict-like or function
Applied to the axis specified by `axis`""",
optional_axis="""axis : int or str, optional
Axis to target. Can be either the axis name ('rows', 'columns')
or number (0, 1).""",
)

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

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

def _validate_axis_style_args(self, arg, arg_name, index, columns,
axis, method_name):
if axis is not None:
# Using "axis" style, along with a positional arg
# Both index and columns should be None then
axis = self._get_axis_name(axis)
if index is not None or columns is not None:
msg = (
"Can't specify both 'axis' and 'index' or 'columns'. "
"Specify either\n"
"\t.{method_name}.rename({arg_name}, axis=axis), or\n"
"\t.{method_name}.rename(index=index, columns=columns)"
).format(arg_name=arg_name, method_name=method_name)
raise TypeError(msg)
if axis == 'index':
index = arg
elif axis == 'columns':
columns = arg

elif all(x is not None for x in (arg, index, columns)):
msg = (
"Cannot specify all of '{arg_name}', 'index', and 'columns'. "
"Specify either {arg_name} and 'axis', or 'index' and "
"'columns'."
).format(arg_name=arg_name)
raise TypeError(msg)

elif axis is None and (arg is not None and index is not None):
# This is the "ambiguous" case, so emit a warning
msg = (
"Interpreting call to '.{method_name}(a, b)' as "
"'.{method_name}(index=a, columns=b)'. "
"Use keyword arguments to remove any ambiguity."
).format(method_name=method_name)
warnings.warn(msg)
index, columns = arg, index
elif index is None and columns is None:
index = arg
return index, columns

@property
def _series(self):
result = {}
Expand Down Expand Up @@ -2915,7 +2961,11 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True,
limit=limit, fill_value=fill_value)

@Appender(_shared_docs['rename'] % _shared_doc_kwargs)
def rename(self, index=None, columns=None, **kwargs):
def rename(self, mapper=None, index=None, columns=None, axis=None,
**kwargs):
index, columns = self._validate_axis_style_args(mapper, 'mapper',
index, columns,
axis, 'rename')
return super(DataFrame, self).rename(index=index, columns=columns,
**kwargs)

Expand Down
22 changes: 20 additions & 2 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,11 +742,13 @@ def swaplevel(self, i=-2, j=-1, axis=0):
Parameters
----------
%(optional_mapper)s
%(axes)s : scalar, list-like, dict-like or function, optional
Scalar or list-like will alter the ``Series.name`` attribute,
and raise on DataFrame or Panel.
dict-like or functions are transformations to apply to
that axis' values
%(optional_axis)s
copy : boolean, default True
Also copy underlying data
inplace : boolean, default False
Expand Down Expand Up @@ -787,6 +789,7 @@ def swaplevel(self, i=-2, j=-1, axis=0):
3 2
5 3
dtype: int64
>>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
>>> df.rename(2)
Traceback (most recent call last):
Expand All @@ -802,12 +805,27 @@ def swaplevel(self, i=-2, j=-1, axis=0):
0 1 4
1 2 5
2 3 6
Using axis-style parameters
>>> df.rename(str.lower, axis='columns')
a b
0 1 4
1 2 5
2 3 6
>>> df.rename({1: 2, 2: 4}, axis='index')
A B
0 1 4
2 2 5
4 3 6
"""

@Appender(_shared_docs['rename'] % dict(axes='axes keywords for this'
' object', klass='NDFrame'))
' object', klass='NDFrame',
optional_mapper='',
optional_axis=''))
def rename(self, *args, **kwargs):

axes, kwargs = self._construct_axes_from_arguments(args, kwargs)
copy = kwargs.pop('copy', True)
inplace = kwargs.pop('inplace', False)
Expand Down
3 changes: 2 additions & 1 deletion pandas/core/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
_shared_doc_kwargs = dict(
axes='items, major_axis, minor_axis',
klass="Panel",
axes_single_arg="{0, 1, 2, 'items', 'major_axis', 'minor_axis'}")
axes_single_arg="{0, 1, 2, 'items', 'major_axis', 'minor_axis'}",
optional_mapper='', optional_axis='')
_shared_doc_kwargs['args_transpose'] = ("three positional arguments: each one"
"of\n%s" %
_shared_doc_kwargs['axes_single_arg'])
Expand Down
2 changes: 1 addition & 1 deletion pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
inplace="""inplace : boolean, default False
If True, performs operation inplace and returns None.""",
unique='np.ndarray', duplicated='Series',
optional_by='',
optional_by='', optional_mapper='', optional_axis='',
versionadded_to_excel='\n .. versionadded:: 0.20.0\n')


Expand Down
103 changes: 103 additions & 0 deletions pandas/tests/frame/test_alter_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,101 @@ def test_rename_objects(self):
assert 'FOO' in renamed
assert 'foo' not in renamed

def test_rename_columns(self):
df = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['X', 'Y'])
expected = pd.DataFrame({"a": [1, 2], "b": [1, 2]}, index=['X', 'Y'])

result = df.rename(str.lower, axis=1)
assert_frame_equal(result, expected)

result = df.rename(str.lower, axis='columns')
assert_frame_equal(result, expected)

result = df.rename({"A": 'a', 'B': 'b'}, axis=1)
assert_frame_equal(result, expected)

result = df.rename({"A": 'a', 'B': 'b'}, axis='columns')
assert_frame_equal(result, expected)

# Index
expected = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['x', 'y'])
result = df.rename(str.lower, axis=0)
assert_frame_equal(result, expected)

result = df.rename(str.lower, axis='index')
assert_frame_equal(result, expected)

result = df.rename({'X': 'x', 'Y': 'y'}, axis=0)
assert_frame_equal(result, expected)

result = df.rename({'X': 'x', 'Y': 'y'}, axis='index')
assert_frame_equal(result, expected)

def test_rename_mapper_multi(self):
df = pd.DataFrame({"A": ['a', 'b'], "B": ['c', 'd'],
'C': [1, 2]}).set_index(["A", "B"])
result = df.rename(str.upper)
expected = df.rename(index=str.upper)
assert_frame_equal(result, expected)

def test_rename_raises(self):
df = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['0', '1'])

# Named target and axis
with tm.assert_raises_regex(TypeError, None):
df.rename(index=str.lower, axis=1)

with tm.assert_raises_regex(TypeError, None):
df.rename(index=str.lower, axis='columns')

with tm.assert_raises_regex(TypeError, None):
df.rename(index=str.lower, axis=0)

with tm.assert_raises_regex(TypeError, None):
df.rename(index=str.lower, axis='columns')

with tm.assert_raises_regex(TypeError, None):
df.rename(columns=str.lower, axis='columns')

# Multiple targets and axis
with tm.assert_raises_regex(TypeError, None):
df.rename(str.lower, str.lower, axis='columns')

# Too many targets
with tm.assert_raises_regex(TypeError, None):
df.rename(str.lower, str.lower, str.lower)

def test_drop_api_equivalence(self):
# equivalence of the labels/axis and index/columns API's
df = DataFrame([[1, 2, 3], [3, 4, 5], [5, 6, 7]],
index=['a', 'b', 'c'],
columns=['d', 'e', 'f'])

res1 = df.drop('a')
res2 = df.drop(index='a')
tm.assert_frame_equal(res1, res2)

res1 = df.drop('d', 1)
res2 = df.drop(columns='d')
tm.assert_frame_equal(res1, res2)

res1 = df.drop(labels='e', axis=1)
res2 = df.drop(columns='e')
tm.assert_frame_equal(res1, res2)

res1 = df.drop(['a'], axis=0)
res2 = df.drop(index=['a'])
tm.assert_frame_equal(res1, res2)

res1 = df.drop(['a'], axis=0).drop(['d'], axis=1)
res2 = df.drop(index=['a'], columns=['d'])

with pytest.raises(ValueError):
df.drop(labels='a', index='b')

with pytest.raises(ValueError):
df.drop(axis=1)

def test_assign_columns(self):
self.frame['hi'] = 'there'

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

def test_ambiguous_warns(self):
df = pd.DataFrame({"A": [1, 2]})
with tm.assert_produces_warning(UserWarning):
df.rename(id, id)

with tm.assert_produces_warning(UserWarning):
df.rename({0: 10}, {"A": "B"})


class TestIntervalIndex(object):

Expand Down

0 comments on commit b06e726

Please sign in to comment.