diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index e1a1c975b5ed8..5dd6ce168a0de 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -25,7 +25,7 @@ Other Enhancements - ``Series.str`` has gained :meth:`Series.str.casefold` method to removes all case distinctions present in a string (:issue:`25405`) - :meth:`DataFrame.set_index` now works for instances of ``abc.Iterator``, provided their output is of the same length as the calling frame (:issue:`22484`, :issue:`24984`) - :meth:`DatetimeIndex.union` now supports the ``sort`` argument. The behaviour of the sort parameter matches that of :meth:`Index.union` (:issue:`24994`) -- +- :meth:`DataFrame.rename` now supports the ``errors`` argument to raise errors when attempting to rename nonexistent keys (:issue:`13473`) .. _whatsnew_0250.api_breaking: diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 6b4d95055d06d..eadffb779734f 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -3911,7 +3911,8 @@ def drop(self, labels=None, axis=0, index=None, columns=None, @rewrite_axis_style_signature('mapper', [('copy', True), ('inplace', False), - ('level', None)]) + ('level', None), + ('errors', 'ignore')]) def rename(self, *args, **kwargs): """ Alter axes labels. @@ -3924,30 +3925,49 @@ def rename(self, *args, **kwargs): Parameters ---------- - mapper, index, columns : dict-like or function, optional - dict-like or functions transformations to apply to + mapper : dict-like or function + 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 + index : dict-like or function + Alternative to specifying axis (``mapper, axis=0`` + is equivalent to ``index=mapper``). + columns : dict-like or function + Alternative to specifying axis (``mapper, axis=1`` + is equivalent to ``columns=mapper``). + axis : int or str 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 + copy : bool, default True + Also copy underlying data. + inplace : bool, default False Whether to return a new DataFrame. 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. + errors : {'ignore', 'raise'}, default 'ignore' + If 'raise', raise a `KeyError` when a dict-like `mapper`, `index`, + or `columns` contains labels that are not present in the Index + being transformed. + If 'ignore', existing keys will be renamed and extra keys will be + ignored. Returns ------- DataFrame + DataFrame with the renamed axis labels. + + Raises + ------ + KeyError + If any of the labels is not found in the selected axis and + "errors='raise'". See Also -------- - DataFrame.rename_axis + DataFrame.rename_axis : Set the name of the axis. Examples -------- @@ -3973,6 +3993,10 @@ def rename(self, *args, **kwargs): 1 2 5 2 3 6 + >>> df.rename(index=str, columns={"A": "a", "C": "c"}, errors="raise") + Traceback (most recent call last): + KeyError: ['C'] not found in axis + Using axis-style parameters >>> df.rename(str.lower, axis='columns') diff --git a/pandas/core/generic.py b/pandas/core/generic.py index ee8f9cba951b3..7915d98662c9e 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -981,11 +981,23 @@ def rename(self, *args, **kwargs): level : int or level name, default None In case of a MultiIndex, only rename labels in the specified level. + errors : {'ignore', 'raise'}, default 'ignore' + If 'raise', raise a `KeyError` when a dict-like `mapper`, `index`, + or `columns` contains labels that are not present in the Index + being transformed. + If 'ignore', existing keys will be renamed and extra keys will be + ignored. Returns ------- renamed : %(klass)s (new object) + Raises + ------ + KeyError + If any of the labels is not found in the selected axis and + "errors='raise'". + See Also -------- NDFrame.rename_axis @@ -1065,6 +1077,7 @@ def rename(self, *args, **kwargs): inplace = kwargs.pop('inplace', False) level = kwargs.pop('level', None) axis = kwargs.pop('axis', None) + errors = kwargs.pop('errors', 'ignore') if axis is not None: # Validate the axis self._get_axis_number(axis) @@ -1085,10 +1098,19 @@ def rename(self, *args, **kwargs): if v is None: continue f = com._get_rename_function(v) - baxis = self._get_block_manager_axis(axis) if level is not None: level = self.axes[axis]._get_level_number(level) + + # GH 13473 + if not callable(v): + indexer = self.axes[axis].get_indexer_for(v) + if errors == 'raise' and len(indexer[indexer == -1]): + missing_labels = [label for index, label in enumerate(v) + if indexer[index] == -1] + raise KeyError('{} not found in axis' + .format(missing_labels)) + result._data = result._data.rename_axis(f, axis=baxis, copy=copy, level=level) result._clear_item_cache() diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index f4a2a5f8032a0..bc5cf30d096fd 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -872,6 +872,23 @@ def test_rename_bug2(self): columns=["a"]) tm.assert_frame_equal(df, expected) + def test_rename_errors_raises(self): + df = DataFrame(columns=['A', 'B', 'C', 'D']) + with pytest.raises(KeyError, match='\'E\'] not found in axis'): + df.rename(columns={'A': 'a', 'E': 'e'}, errors='raise') + + @pytest.mark.parametrize('mapper, errors, expected_columns', [ + ({'A': 'a', 'E': 'e'}, 'ignore', ['a', 'B', 'C', 'D']), + ({'A': 'a'}, 'raise', ['a', 'B', 'C', 'D']), + (str.lower, 'raise', ['a', 'b', 'c', 'd'])]) + def test_rename_errors(self, mapper, errors, expected_columns): + # GH 13473 + # rename now works with errors parameter + df = DataFrame(columns=['A', 'B', 'C', 'D']) + result = df.rename(columns=mapper, errors=errors) + expected = DataFrame(columns=expected_columns) + tm.assert_frame_equal(result, expected) + def test_reorder_levels(self): index = MultiIndex(levels=[['bar'], ['one', 'two', 'three'], [0, 1]], codes=[[0, 0, 0, 0, 0, 0], @@ -1329,7 +1346,7 @@ def test_rename_signature(self): sig = inspect.signature(DataFrame.rename) parameters = set(sig.parameters) assert parameters == {"self", "mapper", "index", "columns", "axis", - "inplace", "copy", "level"} + "inplace", "copy", "level", "errors"} @pytest.mark.skipif(PY2, reason="inspect.signature") def test_reindex_signature(self):