From a7b634c66a0d219f5899b0224ba68368c0528bde Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 10 Oct 2017 10:17:56 -0500 Subject: [PATCH] API: Added axis argument to rename, reindex (#17800) * API: Added axis argument to rename xref: https://github.com/pandas-dev/pandas/issues/12392 * API: Accept 'axis' keyword argument for reindex --- doc/source/basics.rst | 24 +++- doc/source/whatsnew/v0.21.0.txt | 34 +++++ pandas/core/frame.py | 136 +++++++++++++++++- pandas/core/generic.py | 78 +++++++++- pandas/core/panel.py | 3 +- pandas/core/series.py | 63 +++++++- pandas/core/sparse/series.py | 3 +- pandas/tests/frame/test_alter_axes.py | 108 ++++++++++++++ .../tests/frame/test_axis_select_reindex.py | 98 ++++++++++++- 9 files changed, 529 insertions(+), 18 deletions(-) diff --git a/doc/source/basics.rst b/doc/source/basics.rst index 0990d2bd15ee6f..be9d1a5d83b85a 100644 --- a/doc/source/basics.rst +++ b/doc/source/basics.rst @@ -1217,6 +1217,15 @@ following can be done: This means that the reindexed Series's index is the same Python object as the DataFrame's index. +.. versionadded:: 0.21.0 + +:meth:`DataFrame.reindex` also supports an "axis-style" calling convention, +where you specify a single ``labels`` argument and the ``axis`` it applies to. + +.. ipython:: python + + df.reindex(['c', 'f', 'b'], axis='index') + df.reindex(['three', 'two', 'one'], axis='columns') .. seealso:: @@ -1413,12 +1422,23 @@ Series can also be used: .. ipython:: python - df.rename(columns={'one' : 'foo', 'two' : 'bar'}, - index={'a' : 'apple', 'b' : 'banana', 'd' : 'durian'}) + df.rename(columns={'one': 'foo', 'two': 'bar'}, + index={'a': 'apple', 'b': 'banana', 'd': 'durian'}) If the mapping doesn't include a column/index label, it isn't renamed. Also extra labels in the mapping don't throw an error. +.. versionadded:: 0.21.0 + +:meth:`DataFrame.rename` also supports an "axis-style" calling convention, where +you specify a single ``mapper`` and the ``axis`` to apply that mapping to. + +.. ipython:: python + + df.rename({'one': 'foo', 'two': 'bar'}, axis='columns'}) + df.rename({'a': 'apple', 'b': 'banana', 'd': 'durian'}, axis='columns'}) + + 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. diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 1c4af579d16dcb..f04410ef635318 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -111,6 +111,40 @@ For example: # the following is now equivalent df.drop(columns=['B', 'C']) +.. _whatsnew_0210.enhancements.rename_reindex_axis: + +``rename``, ``reindex`` now also accept axis keyword +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :meth:`DataFrame.rename` and :meth:`DataFrame.reindex` methods have gained +the ``axis`` keyword to specify the axis to target with the operation +(:issue:`12392`). + +Here's ``rename``: + +.. ipython:: python + + df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + df.rename(str.lower, axis='columns') + df.rename(id, axis='index') + +And ``reindex``: + +.. ipython:: python + + df.reindex(['A', 'B', 'C'], axis='columns') + df.reindex([0, 1, 3], axis='index') + +The "index, columns" style continues to work as before. + +.. ipython:: python + + df.rename(index=id, columns=str.lower) + df.reindex(index=[0, 1, 3], columns=['A', 'B', 'C']) + +We *highly* encourage using named arguments to avoid confusion when using either +style. + .. _whatsnew_0210.enhancements.categorical_dtype: ``CategoricalDtype`` for specifying categoricals diff --git a/pandas/core/frame.py b/pandas/core/frame.py index c536cc9f2b82c5..94ff70f287fbe7 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -65,6 +65,7 @@ _values_from_object, _maybe_box_datetimelike, _dict_compat, + _all_not_none, standardize_mapping) from pandas.core.generic import NDFrame, _shared_docs from pandas.core.index import (Index, MultiIndex, _ensure_index, @@ -111,7 +112,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_labels="""labels : array-like, optional + New labels / index to conform the axis specified by 'axis' to.""", + optional_axis="""axis : int or str, optional + Axis to target. Can be either the axis name ('index', '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 @@ -2776,6 +2783,47 @@ 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_not_none(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 _all_not_none(arg, index): + # 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, stacklevel=3) + index, columns = arg, index + elif index is None: + # This is for the default axis, like reindex([0, 1]) + index = arg + return index, columns + @property def _series(self): result = {} @@ -2902,7 +2950,11 @@ def align(self, other, join='outer', axis=None, level=None, copy=True, broadcast_axis=broadcast_axis) @Appender(_shared_docs['reindex'] % _shared_doc_kwargs) - def reindex(self, index=None, columns=None, **kwargs): + def reindex(self, labels=None, index=None, columns=None, axis=None, + **kwargs): + index, columns = self._validate_axis_style_args(labels, 'labels', + index, columns, + axis, 'reindex') return super(DataFrame, self).reindex(index=index, columns=columns, **kwargs) @@ -2914,8 +2966,84 @@ def reindex_axis(self, labels, axis=0, method=None, level=None, copy=True, method=method, level=level, copy=copy, 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): + """Alter axes labels. + + Function / dict values must be unique (1-to-1). Labels not contained in + a dict / Series will be left as-is. Extra labels listed don't throw an + error. + + See the :ref:`user guide ` for more. + + Parameters + ---------- + mapper, index, columns : dict-like or function, optional + dict-like or functions transformations to apply to + that axis' values. Use either ``mapper`` and ``axis`` to + specify the axis to target with ``mapper``, or ``index`` and + ``columns``. + axis : int or str, optional + Axis to target with ``mapper``. Can be either the axis name + ('index', 'columns') or number (0, 1). The default is 'index'. + copy : boolean, default True + Also copy underlying data + inplace : boolean, default False + Whether to return a new %(klass)s. If True then value of copy is + ignored. + level : int or level name, default None + In case of a MultiIndex, only rename labels in the specified + level. + + Returns + ------- + renamed : DataFrame + + See Also + -------- + pandas.DataFrame.rename_axis + + Examples + -------- + + ``DataFrame.rename`` supports two calling conventions + + * ``(index=index_mapper, columns=columns_mapper, ...) + * ``(mapper, axis={'index', 'columns'}, ...) + + We *highly* recommend using keyword arguments to clarify your + intent. + + >>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + >>> df.rename(index=str, columns={"A": "a", "B": "c"}) + a c + 0 1 4 + 1 2 5 + 2 3 6 + + >>> df.rename(index=str, columns={"A": "a", "C": "c"}) + a B + 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 + """ + index, columns = self._validate_axis_style_args(mapper, 'mapper', + index, columns, + axis, 'rename') return super(DataFrame, self).rename(index=index, columns=columns, **kwargs) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index bc0f10a3f79abb..9d9d8334fcaf40 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -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 @@ -766,6 +768,7 @@ def swaplevel(self, i=-2, j=-1, axis=0): Examples -------- + >>> s = pd.Series([1, 2, 3]) >>> s 0 1 @@ -787,27 +790,58 @@ def swaplevel(self, i=-2, j=-1, axis=0): 3 2 5 3 dtype: int64 + + Since ``DataFrame`` doesn't have a ``.name`` attribute, + only mapping-type arguments are allowed. + >>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) >>> df.rename(2) Traceback (most recent call last): ... TypeError: 'int' object is not callable + + ``DataFrame.rename`` supports two calling conventions + + * ``(index=index_mapper, columns=columns_mapper, ...) + * ``(mapper, axis={'index', 'columns'}, ...) + + We *highly* recommend using keyword arguments to clarify your + intent. + >>> df.rename(index=str, columns={"A": "a", "B": "c"}) a c 0 1 4 1 2 5 2 3 6 + >>> df.rename(index=str, columns={"A": "a", "C": "c"}) a B 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 + + See the :ref:`user guide ` for more. """ @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) @@ -886,6 +920,7 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False): Examples -------- + >>> df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) >>> df.rename_axis("foo") # scalar, alters df.index.name A B @@ -2746,10 +2781,11 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, Parameters ---------- - %(axes)s : array-like, optional (can be specified in order, or as - keywords) + %(optional_labels)s + %(axes)s : array-like, optional (should be specified using keywords) New labels / index to conform to. Preferably an Index object to avoid duplicating data + %(optional_axis)s method : {None, 'backfill'/'bfill', 'pad'/'ffill', 'nearest'}, optional method to use for filling holes in reindexed DataFrame. Please note: this is only applicable to DataFrames/Series with a @@ -2781,6 +2817,14 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, Examples -------- + ``DataFrame.reindex`` supports two calling conventions + + * ``(index=index_labels, columns=column_labels, ...) + * ``(labels, axis={'index', 'columns'}, ...) + + We *highly* recommend using keyword arguments to clarify your + intent. + Create a dataframe with some fictional data. >>> index = ['Firefox', 'Chrome', 'Safari', 'IE10', 'Konqueror'] @@ -2831,6 +2875,26 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, IE10 404 0.08 Chrome 200 0.02 + We can also reindex the columns. + + >>> df.reindex(columns=['http_status', 'user_agent']) + http_status user_agent + Firefox 200 NaN + Chrome 200 NaN + Safari 404 NaN + IE10 404 NaN + Konqueror 301 NaN + + Or we can use "axis-style" keyword arguments + + >>> df.reindex(['http_status', 'user_agent'], axis="columns") + http_status user_agent + Firefox 200 NaN + Chrome 200 NaN + Safari 404 NaN + IE10 404 NaN + Konqueror 301 NaN + To further illustrate the filling functionality in ``reindex``, we will create a dataframe with a monotonically increasing index (for example, a sequence @@ -2893,6 +2957,8 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, desired indexes. If you do want to fill in the ``NaN`` values present in the original dataframe, use the ``fillna()`` method. + See the :ref:`user guide ` for more. + Returns ------- reindexed : %(klass)s @@ -2901,7 +2967,9 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, # TODO: Decide if we care about having different examples for different # kinds - @Appender(_shared_docs['reindex'] % dict(axes="axes", klass="NDFrame")) + @Appender(_shared_docs['reindex'] % dict(axes="axes", klass="NDFrame", + optional_labels="", + optional_axis="")) def reindex(self, *args, **kwargs): # construct the args diff --git a/pandas/core/panel.py b/pandas/core/panel.py index 14fba9560cae25..b2f50eaf733d87 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -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='', optional_labels='') _shared_doc_kwargs['args_transpose'] = ("three positional arguments: each one" "of\n%s" % _shared_doc_kwargs['axes_single_arg']) diff --git a/pandas/core/series.py b/pandas/core/series.py index be4066f0c39b93..93afdc5151b35e 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -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_labels='', optional_axis='', versionadded_to_excel='\n .. versionadded:: 0.20.0\n') @@ -2525,8 +2525,67 @@ def align(self, other, join='outer', axis=None, level=None, copy=True, limit=limit, fill_axis=fill_axis, broadcast_axis=broadcast_axis) - @Appender(generic._shared_docs['rename'] % _shared_doc_kwargs) def rename(self, index=None, **kwargs): + """Alter Series index labels or name + + Function / dict values must be unique (1-to-1). Labels not contained in + a dict / Series will be left as-is. Extra labels listed don't throw an + error. + + Alternatively, change ``Series.name`` with a scalar value. + + See the :ref:`user guide ` for more. + + Parameters + ---------- + index : scalar, hashable sequence, dict-like or function, optional + dict-like or functions are transformations to apply to + the index. + Scalar or hashable sequence-like will alter the ``Series.name`` + attribute. + copy : boolean, default True + Also copy underlying data + inplace : boolean, default False + Whether to return a new %(klass)s. If True then value of copy is + ignored. + level : int or level name, default None + In case of a MultiIndex, only rename labels in the specified + level. + + Returns + ------- + renamed : Series (new object) + + See Also + -------- + pandas.Series.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 + + """ kwargs['inplace'] = validate_bool_kwarg(kwargs.get('inplace', False), 'inplace') diff --git a/pandas/core/sparse/series.py b/pandas/core/sparse/series.py index 3255bd6bd17e8c..5c76cca08f6094 100644 --- a/pandas/core/sparse/series.py +++ b/pandas/core/sparse/series.py @@ -35,7 +35,8 @@ _shared_doc_kwargs = dict(axes='index', klass='SparseSeries', - axes_single_arg="{0, 'index'}") + axes_single_arg="{0, 'index'}", + optional_labels='', optional_axis='') # ----------------------------------------------------------------------------- # Wrapper function for Series arithmetic methods diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 27906838abb2de..feb32324ff1b10 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -837,6 +837,106 @@ def test_rename_objects(self): assert 'FOO' in renamed assert 'foo' not in renamed + def test_rename_axis_style(self): + # https://github.com/pandas-dev/pandas/issues/12392 + 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_positional_named(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = pd.DataFrame({"a": [1, 2], "b": [1, 2]}, index=['X', 'Y']) + result = df.rename(str.lower, columns=str.upper) + expected = pd.DataFrame({"A": [1, 2], "B": [1, 2]}, index=['x', 'y']) + assert_frame_equal(result, expected) + + def test_rename_axis_style_raises(self): + # https://github.com/pandas-dev/pandas/issues/12392 + 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='columns') + + with tm.assert_raises_regex(TypeError, None): + df.rename(columns=str.lower, axis='columns') + + with tm.assert_raises_regex(TypeError, None): + df.rename(index=str.lower, axis=0) + + # 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_reindex_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.reindex(['b', 'a']) + res2 = df.reindex(index=['b', 'a']) + res3 = df.reindex(labels=['b', 'a']) + res4 = df.reindex(labels=['b', 'a'], axis=0) + res5 = df.reindex(['b', 'a'], axis=0) + for res in [res2, res3, res4, res5]: + tm.assert_frame_equal(res1, res) + + res1 = df.reindex(columns=['e', 'd']) + res2 = df.reindex(['e', 'd'], axis=1) + res3 = df.reindex(labels=['e', 'd'], axis=1) + for res in [res2, res3]: + tm.assert_frame_equal(res1, res) + + res1 = df.reindex(index=['b', 'a'], columns=['e', 'd']) + res2 = df.reindex(columns=['e', 'd'], index=['b', 'a']) + res3 = df.reindex(labels=['b', 'a'], axis=0).reindex(labels=['e', 'd'], + axis=1) + for res in [res2, res3]: + tm.assert_frame_equal(res1, res) + def test_assign_columns(self): self.frame['hi'] = 'there' @@ -860,6 +960,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): diff --git a/pandas/tests/frame/test_axis_select_reindex.py b/pandas/tests/frame/test_axis_select_reindex.py index f9a4275d14f55d..38ed8ee20bc501 100644 --- a/pandas/tests/frame/test_axis_select_reindex.py +++ b/pandas/tests/frame/test_axis_select_reindex.py @@ -447,6 +447,98 @@ def test_reindex_dups(self): # reindex fails pytest.raises(ValueError, df.reindex, index=list(range(len(df)))) + def test_reindex_axis_style(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + expected = pd.DataFrame({"A": [1, 2, np.nan], "B": [4, 5, np.nan]}, + index=[0, 1, 3]) + result = df.reindex([0, 1, 3]) + assert_frame_equal(result, expected) + + result = df.reindex([0, 1, 3], axis=0) + assert_frame_equal(result, expected) + + result = df.reindex([0, 1, 3], axis='index') + assert_frame_equal(result, expected) + + def test_reindex_positional_warns(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + expected = pd.DataFrame({"A": [1., 2], 'B': [4., 5], + "C": [np.nan, np.nan]}) + with tm.assert_produces_warning(UserWarning): + result = df.reindex([0, 1], ['A', 'B', 'C']) + + assert_frame_equal(result, expected) + + def test_reindex_axis_style_raises(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = pd.DataFrame({"A": [1, 2, 3], 'B': [4, 5, 6]}) + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex([0, 1], ['A'], axis=1) + + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex([0, 1], ['A'], axis='index') + + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex(index=[0, 1], axis='index') + + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex(index=[0, 1], axis='columns') + + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex(columns=[0, 1], axis='columns') + + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex(index=[0, 1], columns=[0, 1], axis='columns') + + with tm.assert_raises_regex(TypeError, 'Cannot specify all'): + df.reindex([0, 1], [0], ['A']) + + # Mixing styles + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex(index=[0, 1], axis='index') + + with tm.assert_raises_regex(TypeError, 'reindex'): + df.reindex(index=[0, 1], axis='columns') + + def test_reindex_single_named_indexer(self): + # https://github.com/pandas-dev/pandas/issues/12392 + df = pd.DataFrame({"A": [1, 2, 3], "B": [1, 2, 3]}) + result = df.reindex([0, 1], columns=['A']) + expected = pd.DataFrame({"A": [1, 2]}) + assert_frame_equal(result, expected) + + def test_reindex_api_equivalence(self): + # https://github.com/pandas-dev/pandas/issues/12392 + # 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.reindex(['b', 'a']) + res2 = df.reindex(index=['b', 'a']) + res3 = df.reindex(labels=['b', 'a']) + res4 = df.reindex(labels=['b', 'a'], axis=0) + res5 = df.reindex(['b', 'a'], axis=0) + for res in [res2, res3, res4, res5]: + tm.assert_frame_equal(res1, res) + + res1 = df.reindex(columns=['e', 'd']) + res2 = df.reindex(['e', 'd'], axis=1) + res3 = df.reindex(labels=['e', 'd'], axis=1) + for res in [res2, res3]: + tm.assert_frame_equal(res1, res) + + with tm.assert_produces_warning(UserWarning) as m: + res1 = df.reindex(['b', 'a'], ['e', 'd']) + assert 'reindex' in str(m[0].message) + res2 = df.reindex(columns=['e', 'd'], index=['b', 'a']) + res3 = df.reindex(labels=['b', 'a'], axis=0).reindex(labels=['e', 'd'], + axis=1) + for res in [res2, res3]: + tm.assert_frame_equal(res1, res) + def test_align(self): af, bf = self.frame.align(self.frame) assert af._data is not self.frame._data @@ -974,21 +1066,21 @@ def test_reindex_with_nans(self): def test_reindex_multi(self): df = DataFrame(np.random.randn(3, 3)) - result = df.reindex(lrange(4), lrange(4)) + result = df.reindex(index=lrange(4), columns=lrange(4)) expected = df.reindex(lrange(4)).reindex(columns=lrange(4)) assert_frame_equal(result, expected) df = DataFrame(np.random.randint(0, 10, (3, 3))) - result = df.reindex(lrange(4), lrange(4)) + result = df.reindex(index=lrange(4), columns=lrange(4)) expected = df.reindex(lrange(4)).reindex(columns=lrange(4)) assert_frame_equal(result, expected) df = DataFrame(np.random.randint(0, 10, (3, 3))) - result = df.reindex(lrange(2), lrange(2)) + result = df.reindex(index=lrange(2), columns=lrange(2)) expected = df.reindex(lrange(2)).reindex(columns=lrange(2)) assert_frame_equal(result, expected)