Skip to content

Commit 3863063

Browse files
TomAugspurgerjorisvandenbossche
authored andcommitted
BUG/REF: Handle ambiguous rename case (#17966)
* Signature rewriting
1 parent 7aeccd3 commit 3863063

File tree

9 files changed

+255
-87
lines changed

9 files changed

+255
-87
lines changed

pandas/core/frame.py

+24-12
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@
8282
from pandas import compat
8383
from pandas.compat import PY36
8484
from pandas.compat.numpy import function as nv
85-
from pandas.util._decorators import Appender, Substitution
86-
from pandas.util._validators import validate_bool_kwarg
85+
from pandas.util._decorators import (Appender, Substitution,
86+
rewrite_axis_style_signature)
87+
from pandas.util._validators import (validate_bool_kwarg,
88+
validate_axis_style_args)
8789

8890
from pandas.core.indexes.period import PeriodIndex
8991
from pandas.core.indexes.datetimes import DatetimeIndex
@@ -2917,12 +2919,19 @@ def align(self, other, join='outer', axis=None, level=None, copy=True,
29172919
broadcast_axis=broadcast_axis)
29182920

29192921
@Appender(_shared_docs['reindex'] % _shared_doc_kwargs)
2920-
def reindex(self, labels=None, index=None, columns=None, axis=None,
2921-
**kwargs):
2922-
axes = self._validate_axis_style_args(labels, 'labels',
2923-
axes=[index, columns],
2924-
axis=axis, method_name='reindex')
2922+
@rewrite_axis_style_signature('labels', [('method', None),
2923+
('copy', True),
2924+
('level', None),
2925+
('fill_value', np.nan),
2926+
('limit', None),
2927+
('tolerance', None)])
2928+
def reindex(self, *args, **kwargs):
2929+
axes = validate_axis_style_args(self, args, kwargs, 'labels',
2930+
'reindex')
29252931
kwargs.update(axes)
2932+
# Pop these, since the values are in `kwargs` under different names
2933+
kwargs.pop('axis', None)
2934+
kwargs.pop('labels', None)
29262935
return super(DataFrame, self).reindex(**kwargs)
29272936

29282937
@Appender(_shared_docs['reindex_axis'] % _shared_doc_kwargs)
@@ -2933,8 +2942,10 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True,
29332942
method=method, level=level, copy=copy,
29342943
limit=limit, fill_value=fill_value)
29352944

2936-
def rename(self, mapper=None, index=None, columns=None, axis=None,
2937-
**kwargs):
2945+
@rewrite_axis_style_signature('mapper', [('copy', True),
2946+
('inplace', False),
2947+
('level', None)])
2948+
def rename(self, *args, **kwargs):
29382949
"""Alter axes labels.
29392950
29402951
Function / dict values must be unique (1-to-1). Labels not contained in
@@ -3008,10 +3019,11 @@ def rename(self, mapper=None, index=None, columns=None, axis=None,
30083019
2 2 5
30093020
4 3 6
30103021
"""
3011-
axes = self._validate_axis_style_args(mapper, 'mapper',
3012-
axes=[index, columns],
3013-
axis=axis, method_name='rename')
3022+
axes = validate_axis_style_args(self, args, kwargs, 'mapper', 'rename')
30143023
kwargs.update(axes)
3024+
# Pop these, since the values are in `kwargs` under different names
3025+
kwargs.pop('axis', None)
3026+
kwargs.pop('mapper', None)
30153027
return super(DataFrame, self).rename(**kwargs)
30163028

30173029
@Appender(_shared_docs['fillna'] % _shared_doc_kwargs)

pandas/core/generic.py

+1-46
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from pandas.core.dtypes.cast import maybe_promote, maybe_upcast_putmask
2929
from pandas.core.dtypes.missing import isna, notna
3030
from pandas.core.dtypes.generic import ABCSeries, ABCPanel, ABCDataFrame
31-
from pandas.core.common import (_all_not_none, _count_not_none,
31+
from pandas.core.common import (_count_not_none,
3232
_maybe_box_datetimelike, _values_from_object,
3333
AbstractMethodError, SettingWithCopyError,
3434
SettingWithCopyWarning)
@@ -728,51 +728,6 @@ def swaplevel(self, i=-2, j=-1, axis=0):
728728
result._data.set_axis(axis, labels.swaplevel(i, j))
729729
return result
730730

731-
def _validate_axis_style_args(self, arg, arg_name, axes,
732-
axis, method_name):
733-
out = {}
734-
for i, value in enumerate(axes):
735-
if value is not None:
736-
out[self._AXIS_NAMES[i]] = value
737-
738-
aliases = ', '.join(self._AXIS_NAMES.values())
739-
if axis is not None:
740-
# Using "axis" style, along with a positional arg
741-
# Both index and columns should be None then
742-
axis = self._get_axis_name(axis)
743-
if any(x is not None for x in axes):
744-
msg = (
745-
"Can't specify both 'axis' and {aliases}. "
746-
"Specify either\n"
747-
"\t.{method_name}({arg_name}, axis=axis), or\n"
748-
"\t.{method_name}(index=index, columns=columns)"
749-
).format(arg_name=arg_name, method_name=method_name,
750-
aliases=aliases)
751-
raise TypeError(msg)
752-
out[axis] = arg
753-
754-
elif _all_not_none(arg, *axes):
755-
msg = (
756-
"Cannot specify all of '{arg_name}', {aliases}. "
757-
"Specify either {arg_name} and 'axis', or {aliases}."
758-
).format(arg_name=arg_name, aliases=aliases)
759-
raise TypeError(msg)
760-
761-
elif _all_not_none(arg, axes[0]):
762-
# This is the "ambiguous" case, so emit a warning
763-
msg = (
764-
"Interpreting call to '.{method_name}(a, b)' as "
765-
"'.{method_name}(index=a, columns=b)'. " # TODO
766-
"Use keyword arguments to remove any ambiguity."
767-
).format(method_name=method_name)
768-
warnings.warn(msg, stacklevel=3)
769-
out[self._AXIS_ORDERS[0]] = arg
770-
out[self._AXIS_ORDERS[1]] = axes[0]
771-
elif axes[0] is None:
772-
# This is for the default axis, like reindex([0, 1])
773-
out[self._AXIS_ORDERS[0]] = arg
774-
return out
775-
776731
# ----------------------------------------------------------------------
777732
# Rename
778733

pandas/core/panel.py

+18-10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from pandas.core.series import Series
3636
from pandas.core.reshape.util import cartesian_product
3737
from pandas.util._decorators import (deprecate, Appender)
38+
from pandas.util._validators import validate_axis_style_args
3839

3940
_shared_doc_kwargs = dict(
4041
axes='items, major_axis, minor_axis',
@@ -1199,20 +1200,27 @@ def _wrap_result(self, result, axis):
11991200
return self._construct_return_type(result, axes)
12001201

12011202
@Appender(_shared_docs['reindex'] % _shared_doc_kwargs)
1202-
def reindex(self, labels=None,
1203-
items=None, major_axis=None, minor_axis=None,
1204-
axis=None, **kwargs):
1205-
major_axis = (major_axis if major_axis is not None else
1206-
kwargs.pop('major', None))
1207-
minor_axis = (minor_axis if minor_axis is not None else
1208-
kwargs.pop('minor', None))
1209-
axes = self._validate_axis_style_args(
1210-
labels, 'labels', axes=[items, major_axis, minor_axis],
1211-
axis=axis, method_name='reindex')
1203+
def reindex(self, *args, **kwargs):
1204+
major = kwargs.pop("major", None)
1205+
minor = kwargs.pop('minor', None)
1206+
1207+
if major is not None:
1208+
if kwargs.get("major_axis"):
1209+
raise TypeError("Cannot specify both 'major' and 'major_axis'")
1210+
kwargs['major_axis'] = major
1211+
if minor is not None:
1212+
if kwargs.get("minor_axis"):
1213+
raise TypeError("Cannot specify both 'minor' and 'minor_axis'")
1214+
1215+
kwargs['minor_axis'] = minor
1216+
axes = validate_axis_style_args(self, args, kwargs, 'labels',
1217+
'reindex')
12121218
if self.ndim >= 4:
12131219
# Hack for PanelND
12141220
axes = {}
12151221
kwargs.update(axes)
1222+
kwargs.pop('axis', None)
1223+
kwargs.pop('labels', None)
12161224
return super(Panel, self).reindex(**kwargs)
12171225

12181226
@Appender(_shared_docs['rename'] % _shared_doc_kwargs)

pandas/core/panel4d.py

+30-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
""" Panel4D: a 4-d dict like collection of panels """
22

33
import warnings
4+
from pandas.core.generic import NDFrame
45
from pandas.core.panelnd import create_nd_panel_factory
56
from pandas.core.panel import Panel
7+
from pandas.util._validators import validate_axis_style_args
8+
69

710
Panel4D = create_nd_panel_factory(klass_name='Panel4D',
811
orders=['labels', 'items', 'major_axis',
@@ -62,12 +65,34 @@ def panel4d_reindex(self, labs=None, labels=None, items=None, major_axis=None,
6265
# Hack for reindex_axis deprecation
6366
# Ha, we used labels for two different things
6467
# I think this will work still.
65-
axes = self._validate_axis_style_args(
66-
labs, 'labels',
67-
axes=[labels, items, major_axis, minor_axis],
68-
axis=axis, method_name='reindex')
68+
if labs is None:
69+
args = ()
70+
else:
71+
args = (labs,)
72+
kwargs_ = dict(labels=labels,
73+
items=items,
74+
major_axis=major_axis,
75+
minor_axis=minor_axis,
76+
axis=axis)
77+
kwargs_ = {k: v for k, v in kwargs_.items() if v is not None}
78+
# major = kwargs.pop("major", None)
79+
# minor = kwargs.pop('minor', None)
80+
81+
# if major is not None:
82+
# if kwargs.get("major_axis"):
83+
# raise TypeError("Cannot specify both 'major' and 'major_axis'")
84+
# kwargs_['major_axis'] = major
85+
# if minor is not None:
86+
# if kwargs.get("minor_axis"):
87+
# raise TypeError("Cannot specify both 'minor' and 'minor_axis'")
88+
# kwargs_['minor_axis'] = minor
89+
90+
if axis is not None:
91+
kwargs_['axis'] = axis
92+
93+
axes = validate_axis_style_args(self, args, kwargs_, 'labs', 'reindex')
6994
kwargs.update(axes)
70-
return super(Panel, self).reindex(**kwargs)
95+
return NDFrame.reindex(self, **kwargs)
7196

7297

7398
Panel4D.__init__ = panel4d_init

pandas/tests/frame/test_alter_axes.py

+37-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
from __future__ import print_function
44

5+
import inspect
56
import pytest
67

78
from datetime import datetime, timedelta
89

910
import numpy as np
1011

11-
from pandas.compat import lrange
12+
from pandas.compat import lrange, PY2
1213
from pandas import (DataFrame, Series, Index, MultiIndex,
1314
RangeIndex, date_range, IntervalIndex,
1415
to_datetime)
@@ -887,6 +888,9 @@ def test_rename_axis_style(self):
887888
result = df.rename({'X': 'x', 'Y': 'y'}, axis='index')
888889
assert_frame_equal(result, expected)
889890

891+
result = df.rename(mapper=str.lower, axis='index')
892+
assert_frame_equal(result, expected)
893+
890894
def test_rename_mapper_multi(self):
891895
df = pd.DataFrame({"A": ['a', 'b'], "B": ['c', 'd'],
892896
'C': [1, 2]}).set_index(["A", "B"])
@@ -929,6 +933,10 @@ def test_rename_axis_style_raises(self):
929933
with tm.assert_raises_regex(TypeError, None):
930934
df.rename(str.lower, str.lower, str.lower)
931935

936+
# Duplicates
937+
with tm.assert_raises_regex(TypeError, "multiple values"):
938+
df.rename(id, mapper=id)
939+
932940
def test_reindex_api_equivalence(self):
933941
# equivalence of the labels/axis and index/columns API's
934942
df = DataFrame([[1, 2, 3], [3, 4, 5], [5, 6, 7]],
@@ -956,6 +964,17 @@ def test_reindex_api_equivalence(self):
956964
for res in [res2, res3]:
957965
tm.assert_frame_equal(res1, res)
958966

967+
def test_rename_positional(self):
968+
df = pd.DataFrame(columns=['A', 'B'])
969+
with tm.assert_produces_warning(FutureWarning) as rec:
970+
result = df.rename(None, str.lower)
971+
expected = pd.DataFrame(columns=['a', 'b'])
972+
assert_frame_equal(result, expected)
973+
assert len(rec) == 1
974+
message = str(rec[0].message)
975+
assert 'rename' in message
976+
assert 'Use named arguments' in message
977+
959978
def test_assign_columns(self):
960979
self.frame['hi'] = 'there'
961980

@@ -981,12 +1000,27 @@ def test_set_index_preserve_categorical_dtype(self):
9811000

9821001
def test_ambiguous_warns(self):
9831002
df = pd.DataFrame({"A": [1, 2]})
984-
with tm.assert_produces_warning(UserWarning):
1003+
with tm.assert_produces_warning(FutureWarning):
9851004
df.rename(id, id)
9861005

987-
with tm.assert_produces_warning(UserWarning):
1006+
with tm.assert_produces_warning(FutureWarning):
9881007
df.rename({0: 10}, {"A": "B"})
9891008

1009+
@pytest.mark.skipif(PY2, reason="inspect.signature")
1010+
def test_rename_signature(self):
1011+
sig = inspect.signature(pd.DataFrame.rename)
1012+
parameters = set(sig.parameters)
1013+
assert parameters == {"self", "mapper", "index", "columns", "axis",
1014+
"inplace", "copy", "level"}
1015+
1016+
@pytest.mark.skipif(PY2, reason="inspect.signature")
1017+
def test_reindex_signature(self):
1018+
sig = inspect.signature(pd.DataFrame.reindex)
1019+
parameters = set(sig.parameters)
1020+
assert parameters == {"self", "labels", "index", "columns", "axis",
1021+
"limit", "copy", "level", "method",
1022+
"fill_value", "tolerance"}
1023+
9901024

9911025
class TestIntervalIndex(object):
9921026

pandas/tests/frame/test_axis_select_reindex.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -468,42 +468,46 @@ def test_reindex_positional_warns(self):
468468
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
469469
expected = pd.DataFrame({"A": [1., 2], 'B': [4., 5],
470470
"C": [np.nan, np.nan]})
471-
with tm.assert_produces_warning(UserWarning):
471+
with tm.assert_produces_warning(FutureWarning):
472472
result = df.reindex([0, 1], ['A', 'B', 'C'])
473473

474474
assert_frame_equal(result, expected)
475475

476476
def test_reindex_axis_style_raises(self):
477477
# https://github.com/pandas-dev/pandas/issues/12392
478478
df = pd.DataFrame({"A": [1, 2, 3], 'B': [4, 5, 6]})
479-
with tm.assert_raises_regex(TypeError, 'reindex'):
479+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
480480
df.reindex([0, 1], ['A'], axis=1)
481481

482-
with tm.assert_raises_regex(TypeError, 'reindex'):
482+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
483483
df.reindex([0, 1], ['A'], axis='index')
484484

485-
with tm.assert_raises_regex(TypeError, 'reindex'):
485+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
486486
df.reindex(index=[0, 1], axis='index')
487487

488-
with tm.assert_raises_regex(TypeError, 'reindex'):
488+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
489489
df.reindex(index=[0, 1], axis='columns')
490490

491-
with tm.assert_raises_regex(TypeError, 'reindex'):
491+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
492492
df.reindex(columns=[0, 1], axis='columns')
493493

494-
with tm.assert_raises_regex(TypeError, 'reindex'):
494+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
495495
df.reindex(index=[0, 1], columns=[0, 1], axis='columns')
496496

497497
with tm.assert_raises_regex(TypeError, 'Cannot specify all'):
498498
df.reindex([0, 1], [0], ['A'])
499499

500500
# Mixing styles
501-
with tm.assert_raises_regex(TypeError, 'reindex'):
501+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
502502
df.reindex(index=[0, 1], axis='index')
503503

504-
with tm.assert_raises_regex(TypeError, 'reindex'):
504+
with tm.assert_raises_regex(TypeError, "Cannot specify both 'axis'"):
505505
df.reindex(index=[0, 1], axis='columns')
506506

507+
# Duplicates
508+
with tm.assert_raises_regex(TypeError, "multiple values"):
509+
df.reindex([0, 1], labels=[0, 1])
510+
507511
def test_reindex_single_named_indexer(self):
508512
# https://github.com/pandas-dev/pandas/issues/12392
509513
df = pd.DataFrame({"A": [1, 2, 3], "B": [1, 2, 3]})
@@ -532,7 +536,7 @@ def test_reindex_api_equivalence(self):
532536
for res in [res2, res3]:
533537
tm.assert_frame_equal(res1, res)
534538

535-
with tm.assert_produces_warning(UserWarning) as m:
539+
with tm.assert_produces_warning(FutureWarning) as m:
536540
res1 = df.reindex(['b', 'a'], ['e', 'd'])
537541
assert 'reindex' in str(m[0].message)
538542
res2 = df.reindex(columns=['e', 'd'], index=['b', 'a'])

pandas/tests/test_panel.py

+8
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,11 @@ def test_reindex(self):
14241424
result = self.panel.reindex(minor=new_minor)
14251425
assert_frame_equal(result['ItemB'], ref.reindex(columns=new_minor))
14261426

1427+
# raise exception put both major and major_axis
1428+
pytest.raises(Exception, self.panel.reindex,
1429+
minor_axis=new_minor,
1430+
minor=new_minor)
1431+
14271432
# this ok
14281433
result = self.panel.reindex()
14291434
assert_panel_equal(result, self.panel)
@@ -1460,6 +1465,9 @@ def test_reindex_axis_style(self):
14601465
result = panel.reindex([0, 1], axis=2)
14611466
assert_panel_equal(result, expected2)
14621467

1468+
result = panel.reindex([0, 1], axis=2)
1469+
assert_panel_equal(result, expected2)
1470+
14631471
def test_reindex_multi(self):
14641472
with catch_warnings(record=True):
14651473

0 commit comments

Comments
 (0)