Skip to content

ENH/API: Scalar values for Series.rename and NDFrame.rename_axis #11980

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

Closed
Closed
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
9 changes: 9 additions & 0 deletions doc/source/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,15 @@ The :meth:`~DataFrame.rename` method also provides an ``inplace`` named
parameter that is by default ``False`` and copies the underlying data. Pass
``inplace=True`` to rename the data in place.

.. versionadded:: 0.18.0

Finally, :meth:`~Series.rename` also accepts a scalar or list-like
for altering the ``Series.name`` attribute.

.. ipython:: python

s.rename("scalar-name")

.. _basics.rename_axis:

The Panel class has a related :meth:`~Panel.rename_axis` class which can rename
Expand Down
11 changes: 11 additions & 0 deletions doc/source/dsintro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,17 @@ Series can also have a ``name`` attribute:
The Series ``name`` will be assigned automatically in many cases, in particular
when taking 1D slices of DataFrame as you will see below.

.. versionadded:: 0.18.0

You can rename a Series with the :meth:`pandas.Series.rename` method.

.. ipython:: python

s2 = s.rename("different")
s2.name

Note that ``s`` and ``s2`` refer to different objects.

.. _basics.dataframe:

DataFrame
Expand Down
21 changes: 21 additions & 0 deletions doc/source/whatsnew/v0.18.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ And multiple aggregations
r.agg({'A' : ['mean','std'],
'B' : ['mean','std']})

.. _whatsnew_0180.enhancements.rename:

``Series.rename`` and ``NDFrame.rename_axis`` can now take a scalar or list-like
argument for altering the Series or axis *name*, in addition to their old behaviors of altering labels. (:issue:`9494`, :issue:`11965`)

.. ipython: python

s = pd.Series(np.random.randn(10))
s.rename('newname')

.. ipython: python

df = pd.DataFrame(np.random.randn(10, 2))
(df.rename_axis("indexname")
.rename_axis("columns_name", axis="columns"))

The new functionality works well in method chains.
Previously these methods only accepted functions or dicts mapping a *label* to a new label.
This continues to work as before for function or dict-like values.


.. _whatsnew_0180.enhancements.rangeindex:

Range Index
Expand Down
4 changes: 4 additions & 0 deletions pandas/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2456,6 +2456,10 @@ def is_list_like(arg):
not isinstance(arg, compat.string_and_binary_types))


def is_dict_like(arg):
return hasattr(arg, '__getitem__') and hasattr(arg, 'keys')


def is_named_tuple(arg):
return isinstance(arg, tuple) and hasattr(arg, '_fields')

Expand Down
138 changes: 129 additions & 9 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,13 +546,16 @@ def swaplevel(self, i, j, axis=0):
_shared_docs['rename'] = """
Alter axes input function or functions. Function / dict values must be
unique (1-to-1). Labels not contained in a dict / Series will be left
as-is.
as-is. Alternatively, change ``Series.name`` with a scalar
Copy link
Contributor

Choose a reason for hiding this comment

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

these are shared between Series & DataFrame yes? if so, then need to do something to only show the .rename(scalar) for Series

value (Series only).

Parameters
----------
%(axes)s : dict-like or function, optional
Transformation to apply to that axis values

%(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
copy : boolean, default True
Also copy underlying data
inplace : boolean, default False
Expand All @@ -562,6 +565,43 @@ def swaplevel(self, i, j, axis=0):
Returns
-------
renamed : %(klass)s (new object)

See Also
--------
pandas.NDFrame.rename_axis

Examples
--------
>>> s = pd.Series([1, 2, 3])
>>> s
0 1
1 2
2 3
dtype: int64
>>> s.rename("my_name") # scalar, changes Series.name
0 1
1 2
2 3
Name: my_name, dtype: int64
>>> s.rename(lambda x: x ** 2) # function, changes labels
0 1
1 2
4 3
dtype: int64
>>> s.rename({1: 3, 2: 5}) # mapping, changes labels
0 1
3 2
5 3
dtype: int64
>>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
>>> df.rename(2)
...
TypeError: 'int' object is not callable
>>> df.rename(index=str, columns={"A": "a", "B": "c"})
a c
0 1 4
1 2 5
2 3 6
"""

@Appender(_shared_docs['rename'] % dict(axes='axes keywords for this'
Expand Down Expand Up @@ -617,12 +657,15 @@ def f(x):
def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
"""
Alter index and / or columns using input function or functions.
A scaler or list-like for ``mapper`` will alter the ``Index.name``
or ``MultiIndex.names`` attribute.
A function or dict for ``mapper`` will alter the labels.
Function / dict values must be unique (1-to-1). Labels not contained in
a dict / Series will be left as-is.

Parameters
----------
mapper : dict-like or function, optional
mapper : scalar, list-like, dict-like or function, optional
axis : int or string, default 0
copy : boolean, default True
Also copy underlying data
Expand All @@ -631,11 +674,88 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
Returns
-------
renamed : type of caller

See Also
--------
pandas.NDFrame.rename
pandas.Index.rename

Examples
--------
>>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
>>> df.rename_axis("foo") # scalar, alters df.index.name
A B
foo
0 1 4
1 2 5
2 3 6
>>> df.rename_axis(lambda x: 2 * x) # function: alters labels
A B
0 1 4
2 2 5
4 3 6
>>> df.rename_axis({"A": "ehh", "C": "see"}, axis="columns") # mapping
ehh B
0 1 4
1 2 5
2 3 6
"""
is_scalar_or_list = (
(not com.is_sequence(mapper) and not callable(mapper)) or
(com.is_list_like(mapper) and not com.is_dict_like(mapper))
)

if is_scalar_or_list:
return self._set_axis_name(mapper, axis=axis)
else:
axis = self._get_axis_name(axis)
d = {'copy': copy, 'inplace': inplace}
d[axis] = mapper
return self.rename(**d)

def _set_axis_name(self, name, axis=0):
"""
axis = self._get_axis_name(axis)
d = {'copy': copy, 'inplace': inplace}
d[axis] = mapper
return self.rename(**d)
Alter the name or names of the axis, returning self.

Parameters
----------
name : str or list of str
Name for the Index, or list of names for the MultiIndex
axis : int or str
0 or 'index' for the index; 1 or 'columns' for the columns

Returns
-------
renamed : type of caller

See Also
--------
pandas.DataFrame.rename
Copy link
Contributor

Choose a reason for hiding this comment

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

pandas.Series.rename as well

Copy link
Contributor

Choose a reason for hiding this comment

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

did you add this?

pandas.Series.rename
pandas.Index.rename

Examples
--------
>>> df._set_axis_name("foo")
A
foo
0 1
1 2
2 3
>>> df.index = pd.MultiIndex.from_product([['A'], ['a', 'b', 'c']])
>>> df._set_axis_name(["bar", "baz"])
A
bar baz
A a 1
b 2
c 3
"""
axis = self._get_axis_number(axis)
idx = self._get_axis(axis).set_names(name)

renamed = self.copy(deep=True)
renamed.set_axis(axis, idx)
return renamed

# ----------------------------------------------------------------------
# Comparisons
Expand Down
21 changes: 21 additions & 0 deletions pandas/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import types
import warnings
from collections import MutableMapping

from numpy import nan, ndarray
import numpy as np
Expand Down Expand Up @@ -1109,6 +1110,20 @@ def to_sparse(self, kind='block', fill_value=None):
return SparseSeries(self, kind=kind,
fill_value=fill_value).__finalize__(self)

def _set_name(self, name, inplace=False):
'''
Set the Series name.

Parameters
----------
name : str
inplace : bool
whether to modify `self` directly or return a copy
'''
ser = self if inplace else self.copy()
ser.name = name
return ser

# ----------------------------------------------------------------------
# Statistics, overridden ndarray methods

Expand Down Expand Up @@ -2313,6 +2328,12 @@ def align(self, other, join='outer', axis=None, level=None, copy=True,

@Appender(generic._shared_docs['rename'] % _shared_doc_kwargs)
def rename(self, index=None, **kwargs):
is_scalar_or_list = (
(not com.is_sequence(index) and not callable(index)) or
(com.is_list_like(index) and not isinstance(index, MutableMapping))
)
if is_scalar_or_list:
return self._set_name(index, inplace=kwargs.get('inplace'))
return super(Series, self).rename(index=index, **kwargs)

@Appender(generic._shared_docs['reindex'] % _shared_doc_kwargs)
Expand Down
23 changes: 23 additions & 0 deletions pandas/tests/series/test_alter_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@ def test_rename(self):
renamed = renamer.rename({})
self.assertEqual(renamed.index.name, renamer.index.name)

def test_rename_set_name(self):
s = Series(range(4), index=list('abcd'))
for name in ['foo', ['foo'], ('foo',)]:
result = s.rename(name)
self.assertEqual(result.name, name)
self.assert_numpy_array_equal(result.index.values, s.index.values)
self.assertTrue(s.name is None)

def test_rename_set_name_inplace(self):
s = Series(range(3), index=list('abc'))
for name in ['foo', ['foo'], ('foo',)]:
s.rename(name, inplace=True)
self.assertEqual(s.name, name)
self.assert_numpy_array_equal(s.index.values,
np.array(['a', 'b', 'c']))

def test_set_name(self):
s = Series([1, 2, 3])
s2 = s._set_name('foo')
self.assertEqual(s2.name, 'foo')
self.assertTrue(s.name is None)
self.assertTrue(s is not s2)

def test_rename_inplace(self):
renamer = lambda x: x.strftime('%Y%m%d')
expected = renamer(self.ts.index[0])
Expand Down
11 changes: 11 additions & 0 deletions pandas/tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,17 @@ def test_is_list_like():
assert not com.is_list_like(f)


def test_is_dict_like():
passes = [{}, {'A': 1}, pd.Series([1])]
fails = ['1', 1, [1, 2], (1, 2), range(2), pd.Index([1])]

for p in passes:
assert com.is_dict_like(p)
Copy link
Contributor

Choose a reason for hiding this comment

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

e.g. need to pass a dictview here


for f in fails:
assert not com.is_dict_like(f)


def test_is_named_tuple():
passes = (collections.namedtuple('Test', list('abc'))(1, 2, 3), )
fails = ((1, 2, 3), 'a', Series({'pi': 3.14}))
Expand Down
Loading