Skip to content

Commit df75ea6

Browse files
WillAydTomAugspurger
authored andcommitted
Convert DataFrame.rename to keyword only; simplify axis validation (#29140)
1 parent 3ddd495 commit df75ea6

File tree

7 files changed

+212
-70
lines changed

7 files changed

+212
-70
lines changed

doc/source/whatsnew/v1.0.0.rst

+65-1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,71 @@ New repr for :class:`~pandas.arrays.IntervalArray`
292292
293293
pd.arrays.IntervalArray.from_tuples([(0, 1), (2, 3)])
294294
295+
``DataFrame.rename`` now only accepts one positional argument
296+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
297+
298+
- :meth:`DataFrame.rename` would previously accept positional arguments that would lead
299+
to ambiguous or undefined behavior. From pandas 1.0, only the very first argument, which
300+
maps labels to their new names along the default axis, is allowed to be passed by position
301+
(:issue:`29136`).
302+
303+
*pandas 0.25.x*
304+
305+
.. code-block:: ipython
306+
307+
In [1]: df = pd.DataFrame([[1]])
308+
In [2]: df.rename({0: 1}, {0: 2})
309+
FutureWarning: ...Use named arguments to resolve ambiguity...
310+
Out[2]:
311+
2
312+
1 1
313+
314+
*pandas 1.0.0*
315+
316+
.. ipython:: python
317+
:okexcept:
318+
319+
df.rename({0: 1}, {0: 2})
320+
321+
Note that errors will now be raised when conflicting or potentially ambiguous arguments are provided.
322+
323+
*pandas 0.25.x*
324+
325+
.. code-block:: ipython
326+
327+
In [1]: df.rename({0: 1}, index={0: 2})
328+
Out[1]:
329+
0
330+
1 1
331+
332+
In [2]: df.rename(mapper={0: 1}, index={0: 2})
333+
Out[2]:
334+
0
335+
2 1
336+
337+
*pandas 1.0.0*
338+
339+
.. ipython:: python
340+
:okexcept:
341+
342+
df.rename({0: 1}, index={0: 2})
343+
df.rename(mapper={0: 1}, index={0: 2})
344+
345+
You can still change the axis along which the first positional argument is applied by
346+
supplying the ``axis`` keyword argument.
347+
348+
.. ipython:: python
349+
350+
df.rename({0: 1})
351+
df.rename({0: 1}, axis=1)
352+
353+
If you would like to update both the index and column labels, be sure to use the respective
354+
keywords.
355+
356+
.. ipython:: python
357+
358+
df.rename(index={0: 1}, columns={0: 2})
359+
295360
Extended verbose info output for :class:`~pandas.DataFrame`
296361
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
297362

@@ -573,7 +638,6 @@ Optional libraries below the lowest tested version may still work, but are not c
573638

574639
See :ref:`install.dependencies` and :ref:`install.optional_dependencies` for more.
575640

576-
577641
.. _whatsnew_100.api.other:
578642

579643
Other API changes

pandas/_typing.py

+9
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
from typing import (
33
IO,
44
TYPE_CHECKING,
5+
Any,
56
AnyStr,
7+
Callable,
68
Collection,
79
Dict,
10+
Hashable,
811
List,
12+
Mapping,
913
Optional,
1014
TypeVar,
1115
Union,
@@ -56,9 +60,14 @@
5660
FrameOrSeries = TypeVar("FrameOrSeries", bound="NDFrame")
5761

5862
Axis = Union[str, int]
63+
Label = Optional[Hashable]
64+
Level = Union[Label, int]
5965
Ordered = Optional[bool]
6066
JSONSerializable = Union[PythonScalar, List, Dict]
6167
Axes = Collection
6268

69+
# For functions like rename that convert one label to another
70+
Renamer = Union[Mapping[Label, Any], Callable[[Label], Label]]
71+
6372
# to maintain type information across generic functions and parametrization
6473
T = TypeVar("T")

pandas/core/frame.py

+24-8
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from pandas._config import get_option
3939

4040
from pandas._libs import algos as libalgos, lib
41-
from pandas._typing import Axes, Dtype, FilePathOrBuffer
41+
from pandas._typing import Axes, Axis, Dtype, FilePathOrBuffer, Level, Renamer
4242
from pandas.compat import PY37
4343
from pandas.compat._optional import import_optional_dependency
4444
from pandas.compat.numpy import function as nv
@@ -3986,7 +3986,19 @@ def drop(
39863986
"mapper",
39873987
[("copy", True), ("inplace", False), ("level", None), ("errors", "ignore")],
39883988
)
3989-
def rename(self, *args, **kwargs):
3989+
def rename(
3990+
self,
3991+
mapper: Optional[Renamer] = None,
3992+
*,
3993+
index: Optional[Renamer] = None,
3994+
columns: Optional[Renamer] = None,
3995+
axis: Optional[Axis] = None,
3996+
copy: bool = True,
3997+
inplace: bool = False,
3998+
level: Optional[Level] = None,
3999+
errors: str = "ignore",
4000+
) -> Optional["DataFrame"]:
4001+
39904002
"""
39914003
Alter axes labels.
39924004
@@ -4095,12 +4107,16 @@ def rename(self, *args, **kwargs):
40954107
2 2 5
40964108
4 3 6
40974109
"""
4098-
axes = validate_axis_style_args(self, args, kwargs, "mapper", "rename")
4099-
kwargs.update(axes)
4100-
# Pop these, since the values are in `kwargs` under different names
4101-
kwargs.pop("axis", None)
4102-
kwargs.pop("mapper", None)
4103-
return super().rename(**kwargs)
4110+
return super().rename(
4111+
mapper=mapper,
4112+
index=index,
4113+
columns=columns,
4114+
axis=axis,
4115+
copy=copy,
4116+
inplace=inplace,
4117+
level=level,
4118+
errors=errors,
4119+
)
41044120

41054121
@Substitution(**_shared_doc_kwargs)
41064122
@Appender(NDFrame.fillna.__doc__)

pandas/core/generic.py

+54-32
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@
3030
from pandas._config import config
3131

3232
from pandas._libs import Timestamp, iNaT, lib, properties
33-
from pandas._typing import Dtype, FilePathOrBuffer, FrameOrSeries, JSONSerializable
33+
from pandas._typing import (
34+
Axis,
35+
Dtype,
36+
FilePathOrBuffer,
37+
FrameOrSeries,
38+
JSONSerializable,
39+
Level,
40+
Renamer,
41+
)
3442
from pandas.compat import set_function_name
3543
from pandas.compat._optional import import_optional_dependency
3644
from pandas.compat.numpy import function as nv
@@ -921,7 +929,18 @@ def swaplevel(self: FrameOrSeries, i=-2, j=-1, axis=0) -> FrameOrSeries:
921929
# ----------------------------------------------------------------------
922930
# Rename
923931

924-
def rename(self, *args, **kwargs):
932+
def rename(
933+
self: FrameOrSeries,
934+
mapper: Optional[Renamer] = None,
935+
*,
936+
index: Optional[Renamer] = None,
937+
columns: Optional[Renamer] = None,
938+
axis: Optional[Axis] = None,
939+
copy: bool = True,
940+
inplace: bool = False,
941+
level: Optional[Level] = None,
942+
errors: str = "ignore",
943+
) -> Optional[FrameOrSeries]:
925944
"""
926945
Alter axes input function or functions. Function / dict values must be
927946
unique (1-to-1). Labels not contained in a dict / Series will be left
@@ -1034,44 +1053,46 @@ def rename(self, *args, **kwargs):
10341053
10351054
See the :ref:`user guide <basics.rename>` for more.
10361055
"""
1037-
axes, kwargs = self._construct_axes_from_arguments(args, kwargs)
1038-
copy = kwargs.pop("copy", True)
1039-
inplace = kwargs.pop("inplace", False)
1040-
level = kwargs.pop("level", None)
1041-
axis = kwargs.pop("axis", None)
1042-
errors = kwargs.pop("errors", "ignore")
1043-
if axis is not None:
1044-
# Validate the axis
1045-
self._get_axis_number(axis)
1046-
1047-
if kwargs:
1048-
raise TypeError(
1049-
"rename() got an unexpected keyword "
1050-
f'argument "{list(kwargs.keys())[0]}"'
1051-
)
1052-
1053-
if com.count_not_none(*axes.values()) == 0:
1056+
if mapper is None and index is None and columns is None:
10541057
raise TypeError("must pass an index to rename")
10551058

1056-
self._consolidate_inplace()
1059+
if index is not None or columns is not None:
1060+
if axis is not None:
1061+
raise TypeError(
1062+
"Cannot specify both 'axis' and any of 'index' or 'columns'"
1063+
)
1064+
elif mapper is not None:
1065+
raise TypeError(
1066+
"Cannot specify both 'mapper' and any of 'index' or 'columns'"
1067+
)
1068+
else:
1069+
# use the mapper argument
1070+
if axis and self._get_axis_number(axis) == 1:
1071+
columns = mapper
1072+
else:
1073+
index = mapper
1074+
10571075
result = self if inplace else self.copy(deep=copy)
10581076

1059-
# start in the axis order to eliminate too many copies
1060-
for axis in range(self._AXIS_LEN):
1061-
v = axes.get(self._AXIS_NAMES[axis])
1062-
if v is None:
1077+
for axis_no, replacements in enumerate((index, columns)):
1078+
if replacements is None:
10631079
continue
1064-
f = com.get_rename_function(v)
1065-
baxis = self._get_block_manager_axis(axis)
1080+
1081+
ax = self._get_axis(axis_no)
1082+
baxis = self._get_block_manager_axis(axis_no)
1083+
f = com.get_rename_function(replacements)
1084+
10661085
if level is not None:
1067-
level = self.axes[axis]._get_level_number(level)
1086+
level = ax._get_level_number(level)
10681087

10691088
# GH 13473
1070-
if not callable(v):
1071-
indexer = self.axes[axis].get_indexer_for(v)
1089+
if not callable(replacements):
1090+
indexer = ax.get_indexer_for(replacements)
10721091
if errors == "raise" and len(indexer[indexer == -1]):
10731092
missing_labels = [
1074-
label for index, label in enumerate(v) if indexer[index] == -1
1093+
label
1094+
for index, label in enumerate(replacements)
1095+
if indexer[index] == -1
10751096
]
10761097
raise KeyError(f"{missing_labels} not found in axis")
10771098

@@ -1082,6 +1103,7 @@ def rename(self, *args, **kwargs):
10821103

10831104
if inplace:
10841105
self._update_inplace(result._data)
1106+
return None
10851107
else:
10861108
return result.__finalize__(self)
10871109

@@ -4036,7 +4058,7 @@ def add_prefix(self: FrameOrSeries, prefix: str) -> FrameOrSeries:
40364058
f = functools.partial("{prefix}{}".format, prefix=prefix)
40374059

40384060
mapper = {self._info_axis_name: f}
4039-
return self.rename(**mapper)
4061+
return self.rename(**mapper) # type: ignore
40404062

40414063
def add_suffix(self: FrameOrSeries, suffix: str) -> FrameOrSeries:
40424064
"""
@@ -4095,7 +4117,7 @@ def add_suffix(self: FrameOrSeries, suffix: str) -> FrameOrSeries:
40954117
f = functools.partial("{}{suffix}".format, suffix=suffix)
40964118

40974119
mapper = {self._info_axis_name: f}
4098-
return self.rename(**mapper)
4120+
return self.rename(**mapper) # type: ignore
40994121

41004122
def sort_values(
41014123
self,

pandas/core/series.py

+18-6
Original file line numberDiff line numberDiff line change
@@ -3124,7 +3124,7 @@ def argsort(self, axis=0, kind="quicksort", order=None):
31243124
31253125
Parameters
31263126
----------
3127-
axis : int
3127+
axis : {0 or "index"}
31283128
Has no effect but is accepted for compatibility with numpy.
31293129
kind : {'mergesort', 'quicksort', 'heapsort'}, default 'quicksort'
31303130
Choice of sorting algorithm. See np.sort for more
@@ -3893,7 +3893,16 @@ def align(
38933893
broadcast_axis=broadcast_axis,
38943894
)
38953895

3896-
def rename(self, index=None, **kwargs):
3896+
def rename(
3897+
self,
3898+
index=None,
3899+
*,
3900+
axis=None,
3901+
copy=True,
3902+
inplace=False,
3903+
level=None,
3904+
errors="ignore",
3905+
):
38973906
"""
38983907
Alter Series index labels or name.
38993908
@@ -3907,6 +3916,8 @@ def rename(self, index=None, **kwargs):
39073916
39083917
Parameters
39093918
----------
3919+
axis : {0 or "index"}
3920+
Unused. Accepted for compatability with DataFrame method only.
39103921
index : scalar, hashable sequence, dict-like or function, optional
39113922
Functions or dict-like are transformations to apply to
39123923
the index.
@@ -3924,6 +3935,7 @@ def rename(self, index=None, **kwargs):
39243935
39253936
See Also
39263937
--------
3938+
DataFrame.rename : Corresponding DataFrame method.
39273939
Series.rename_axis : Set the name of the axis.
39283940
39293941
Examples
@@ -3950,12 +3962,12 @@ def rename(self, index=None, **kwargs):
39503962
5 3
39513963
dtype: int64
39523964
"""
3953-
kwargs["inplace"] = validate_bool_kwarg(kwargs.get("inplace", False), "inplace")
3954-
39553965
if callable(index) or is_dict_like(index):
3956-
return super().rename(index=index, **kwargs)
3966+
return super().rename(
3967+
index, copy=copy, inplace=inplace, level=level, errors=errors
3968+
)
39573969
else:
3958-
return self._set_name(index, inplace=kwargs.get("inplace"))
3970+
return self._set_name(index, inplace=inplace)
39593971

39603972
@Substitution(**_shared_doc_kwargs)
39613973
@Appender(generic.NDFrame.reindex.__doc__)

0 commit comments

Comments
 (0)