Repository: pandas-dev/pandas
Pull request: ENH: Allow rename_axis to specify index and columns arguments 
- [x] closes #19978
- [x] tests added / passed
   - `test_rename_axis_mapper` for `DataFrame` and `Series`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
API: Allow dictionary argument in rename_axis to change some names of MultiIndex
#### Code Sample, a copy-pastable example if possible

```python
mi = pd.MultiIndex.from_product([['a','b','c'],[1,2]], names=['ll','nn'])
df = pd.DataFrame({'x': [i for i in range(len(mi))], 'y' : [i*10 for i in range(len(mi))]}, index=mi)
df.rename(columns={'x':'z'})  # This works to change name of one column
df.rename_axis({'x' : 'z'}, axis='columns')  # This also works to change name of one column (but you get a deprecation warning)
df.rename(index={'nn':'zz'}) # This does not work to change name of one level of a MultiIndex
df.rename_axis({'nn' : 'zz'}, axis='index') # This does not work to change name of one level of a MultiIndex
```
#### Problem description

Ideally, if you call `df.rename_axis(dict, axis='index')`, only the names specified in the dictionary would be changed in the corresponding `MultiIndex`


#### Expected Output
For the expression
```
df.rename_axis({'nn' : 'zz'}, axis='index')
```
the output should be:
```
       x   y
zz ll       
1  a   0   0
   b   1  10
   c   2  20
2  a   3  30
   b   4  40
   c   5  50
```

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.4.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 60 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.22.0
pytest: 3.3.2
pip: 9.0.1
setuptools: 38.4.0
Cython: 0.27.3
numpy: 1.14.0
scipy: 1.0.0
pyarrow: None
xarray: None
IPython: 6.2.1
sphinx: 1.6.6
patsy: 0.5.0
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: 1.2.1
tables: 3.4.2
numexpr: 2.6.4
feather: None
matplotlib: 2.1.2
openpyxl: 2.4.10
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.2
lxml: 4.1.1
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.2.1
pymysql: 0.7.11.None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
In [ ]:

​
</details>

Hints from issue discussion:
duplicate of #4160 a pull-request is welcome.

MI rename is entirely broken.
This is another issue. https://github.com/pandas-dev/pandas/issues/4160 is about renaming *labels* in a MultiIndex (with `rename`), this issue is about renaming MultiIndex level *names* (with `rename_index`)
@jorisvandenbossche Yes, this is about changing the level names, but using `rename_axis` (not `rename_index` - that doesn't exist!)
So in order to allow this functionality (using a mapper to change some names of a `MultiIndex`) now without removing the deprecated behavior, I propose to add a parameter `rename_names=False` that keeps the deprecated behavior, and then we flip it to `rename_names=True` when the deprecated behavior (passing a `dict` or function to change some names, which changes the labels) is removed.

Otherwise, I can't see how to add this functionality until some future version where we can officially remove the deprecated behavior.  Comments from @jorisvandenbossche and @jreback welcome.
```
In [6]: mi = pd.MultiIndex.from_product([['a','b','c'],[1,2]], names=['ll','nn'])
   ...: df = pd.DataFrame({'x': [i for i in range(len(mi))], 'y' : [i*10 for i in range(len(mi))]}, index=mi)
   ...: 

In [7]: df.index = df.index.set_names('baz', level=1)

In [8]: df
Out[8]: 
        x   y
ll baz       
a  1    0   0
   2    1  10
b  1    2  20
   2    3  30
c  1    4  40
   2    5  50
```

@Dr-Irv you can just do this. this is quite idiomatic and should be used more anyhow.
@jreback Yes, but then I can't do the renaming inline, which is the capability that I'd like to have.

With the current `rename_axis`, I can rename *all* the names inline, but not individual ones. In other words, in your example:
```
df.rename_axis(['ll','baz'],axis='index')
```
produces the desired result, but then you have to specify *all* of the names, rather than just the ones you want to change.

Just like `rename` allows changing only some of the labels, I'd like to have `rename_axis` change some of the names.

sure you can, use ``inplace=True``. its completely non-idiomatic but you can use it
I am -1 on changing this and why I closed it.
@jreback I'm looking at this from having some cleanliness and symmetry in the API's.  Let's say that I have a `DataFrame` with lots of columns that has come from doing lots of computation, and it is indexed on a `MultiIndex` with multiple levels.  In a chained computation, I can write `df.rename(columns=dict(...))` to rename _some_ of the columns.  From my point of view, the names of the `MultiIndex` are just like columns. They just happen to be the index, i.e., like primary keys in a database. So I'd like to be able to do `df.rename_axis(dict(...), axis='index')` to change _some_ of the names in the index, and use that in a chained computation.
@jreback Here's an example that illustrates the need for rename_axis with an argument dictionary.  In this example, there are 2 DataFrames, (df3 and df2) and the contents of the indexes are exactly the same, except that one of the names of the index in df2 is different.  So if try to add the 2 Series, I get an error because the names of the indexes are different.  But if I change that single name, I can then do the add.
```

In [2]: mi3 = pd.MultiIndex.from_product([list('AB'),list('CD'),list('EF')], name
   ...: s=['AB', 'CD', 'EF'])
   ...: df3 = pd.DataFrame([i for i in range(len(mi3))], index=mi3, columns=['N']
   ...: )
   ...: df3
   ...:
Out[2]:
          N
AB CD EF
A  C  E   0
      F   1
   D  E   2
      F   3
B  C  E   4
      F   5
   D  E   6
      F   7

In [3]: df2 = df3.reorder_levels(['EF','CD','AB']).sort_index().reorder_levels(['
   ...: AB','CD','EF'])
   ...: df2.index.names = ['AB','CD','EF1']
   ...: df2
   ...:
Out[3]:
           N
AB CD EF1
A  C  E    0
B  C  E    4
A  D  E    2
B  D  E    6
A  C  F    1
B  C  F    5
A  D  F    3
B  D  F    7

In [4]: df3 + df2
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-4-b209b0c95b4e> in <module>()
----> 1 df3 + df2

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\ops.py in f(self, other, axis, level, fill_value)
   1493
   1494         if isinstance(other, ABCDataFrame):  # Another DataFrame
-> 1495             return self._combine_frame(other, na_op, fill_value, level)
   1496         elif isinstance(other, ABCSeries):
   1497             return _combine_series_frame(self, other, na_op,

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\frame.py in _combine_frame(self, other, func, fill_value, level)
   3967
   3968     def _combine_frame(self, other, func, fill_value=None, level=None):
-> 3969         this, other = self.align(other, join='outer', level=level, copy=False)
   3970         new_index, new_columns = this.index, this.columns
   3971

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\frame.py in align(self, other, join, axis, level, copy, fill_value, method, limit, fill_axis, broadcast_axis)
   3010                                             method=method, limit=limit,
   3011                                             fill_axis=fill_axis,
-> 3012                                             broadcast_axis=broadcast_axis)

   3013
   3014     @Appender(_shared_docs['reindex'] % _shared_doc_kwargs)

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\generic.py in align(self, other, join, axis, level, copy, fill_value, method, limit, fill_axis, broadcast_axis)
   6324                                      copy=copy, fill_value=fill_value,
   6325                                      method=method, limit=limit,
-> 6326                                      fill_axis=fill_axis)
   6327         elif isinstance(other, Series):
   6328             return self._align_series(other, join=join, axis=axis, level=level,

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\generic.py in _align_frame(self, other, join, axis, level, copy, fill_value, method, limit, fill_axis)
   6346             if not self.index.equals(other.index):
   6347                 join_index, ilidx, iridx = self.index.join(
-> 6348                     other.index, how=join, level=level, return_indexers=True)
   6349
   6350         if axis is None or axis == 1:

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\indexes\base.py in join(self, other, how, level, return_indexers, sort)
   3229             else:
   3230                 return self._join_multi(other, how=how,
-> 3231                                         return_indexers=return_indexers)
   3232
   3233         # join on the level

C:\EclipseWorkspaces\LiClipseWorkspace\pandas-dev\pandas36\pandas\core\indexes\base.py in _join_multi(self, other, how, return_indexers)
   3327                              "overlapping names")
   3328         if len(overlap) > 1:
-> 3329             raise NotImplementedError("merging with more than one level "
   3330                                       "overlap on a multi-index is not "
   3331                                       "implemented")

NotImplementedError: merging with more than one level overlap on a multi-index is not implemented
```
So now I change the name of the mismatched column:
```
In [5]: df3.index.names = ['AB','CD','EF1']
   ...: df3 + df2
   ...:
Out[5]:
            N
AB CD EF1
A  C  E     0
      F     2
   D  E     4
      F     6
B  C  E     8
      F    10
   D  E    12
      F    14
```
The example is convoluted, but what is happening in my application is that I have two Series.  In my application, the first Series `s1` has a 3-level MultiIndex, names are `['a','b','k1']`, and the other Series `s2` has a 3-level MultiIndex, names are `['a','b','k']`.  I now need to add them together.  That fails (as in the example above) because the level names are different.  But if I could do in my example:
```
df3.rename_axis({'EF' : 'EF1'}) + df2
```
or
```
df3 + df2.rename_axis({'EF1':'EF'}) 
```
I wouldn't have to permanently change the index of `df3`, which is needed elsewhere with the original index names.

Commit message: Fix signature of rename_axis, add docs

Patch to review:
diff --git a/doc/source/advanced.rst b/doc/source/advanced.rst
index 835c4cc9d4ab3..608e2c8e72ded 100644
--- a/doc/source/advanced.rst
+++ b/doc/source/advanced.rst
@@ -503,6 +503,47 @@ method, allowing you to permute the hierarchical index levels in one step:
 
    df[:5].reorder_levels([1,0], axis=0)
 
+.. _advanced.index_names:
+
+Renaming names of an ``Index`` or ``MultiIndex``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :meth:`~DataFrame.rename` method is used to rename the labels of a
+``MultiIndex``, and is typically used to rename the columns of a ``DataFrame``.
+The ``columns`` argument of ``rename`` allows a dictionary to be specified
+that includes only the columns you wish to rename.
+
+.. ipython:: python
+
+   df.rename(columns={0: "col0", 1: "col1"})
+
+This method can also be used to rename specific labels of the main index
+of the ``DataFrame``.
+
+.. ipython:: python
+
+   df.rename(index={"one" : "two", "y" : "z"})
+
+The :meth:`~DataFrame.rename_axis` method is used to rename the name of a
+``Index`` or ``MultiIndex``. In particular, the names of the levels of a
+``MultiIndex`` can be specified, which is useful if ``reset_index()`` is later
+used to move the values from the ``MultiIndex`` to a column.
+
+.. ipython:: python
+
+   df.rename_axis(index=['abc', 'def'])
+   
+Note that the columns of a ``DataFrame`` are an index, so that using
+``rename_axis`` with the ``columns`` argument will change the name of that
+index.
+
+.. ipython:: python
+
+   df.rename_axis(columns="Cols").columns
+
+Both ``rename`` and ``rename_axis`` support specifying a dictionary,
+``Series`` or a mapping function to map labels/names to new values.
+
 Sorting a ``MultiIndex``
 ------------------------
 
diff --git a/doc/source/basics.rst b/doc/source/basics.rst
index 6eeb97349100a..b6264c4071fc4 100644
--- a/doc/source/basics.rst
+++ b/doc/source/basics.rst
@@ -1466,8 +1466,20 @@ for altering the ``Series.name`` attribute.
 
 .. _basics.rename_axis:
 
-The Panel class has a related :meth:`~Panel.rename_axis` class which can rename
-any of its three axes.
+.. versionadded:: 0.24.0
+
+The methods :meth:`~DataFrame.rename_axis` and :meth:`~Series.rename_axis`
+allow specific names of a `MultiIndex` to be changed (as opposed to the
+labels).
+
+.. ipython:: python
+
+   df = pd.DataFrame({'x': [1,2,3,4,5,6], 'y': [10,20,30,40,50,60]},
+                     index=pd.MultiIndex.from_product([['a','b','c'],[1,2]],
+                     names=['let','num']))
+   df
+   df.rename_axis(index={'let': 'abc'})
+   df.rename_axis(index=str.upper)
 
 .. _basics.iteration:
 
diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt
index 7dd7a92b0d3e5..c409b3cb4021b 100644
--- a/doc/source/whatsnew/v0.24.0.txt
+++ b/doc/source/whatsnew/v0.24.0.txt
@@ -171,6 +171,26 @@ array, but rather an ``ExtensionArray``:
 This is the same behavior as ``Series.values`` for categorical data. See
 :ref:`whatsnew_0240.api_breaking.interval_values` for more.
 
+.. _whatsnew_0240.enhancements.rename_axis:
+
+Renaming names in a MultiIndex
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:func:`DataFrame.rename_axis` now supports ``index`` and ``columns`` arguments
+and :func:`Series.rename_axis` supports ``index`` argument (:issue:`19978`)
+
+This change allows a dictionary to be passed so that some of the names 
+of a ``MultiIndex`` can be changed.
+
+Example:
+
+.. ipython:: python
+
+    mi = pd.MultiIndex.from_product([list('AB'), list('CD'), list('EF')],
+                                    names=['AB', 'CD', 'EF'])
+    df = pd.DataFrame([i for i in range(len(mi))], index=mi, columns=['N'])
+    df
+    df.rename_axis(index={'CD': 'New'})
 
 .. _whatsnew_0240.enhancements.other:
 
diff --git a/pandas/core/common.py b/pandas/core/common.py
index f6e40faa79740..4bb860c2dbe7c 100644
--- a/pandas/core/common.py
+++ b/pandas/core/common.py
@@ -454,3 +454,21 @@ def _pipe(obj, func, *args, **kwargs):
         return func(*args, **kwargs)
     else:
         return func(obj, *args, **kwargs)
+
+
+def get_rename_function(mapper):
+    """
+    Returns a function that will map names/labels, dependent if mapper
+    is a dict, Series or just a function.
+    """
+    if isinstance(mapper, (compat.Mapping, ABCSeries)):
+
+        def f(x):
+            if x in mapper:
+                return mapper[x]
+            else:
+                return x
+    else:
+        f = mapper
+
+    return f
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 31b700abcfdb3..349a8e35d9b69 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -54,6 +54,7 @@
 from pandas.core.ops import _align_method_FRAME
 import pandas.core.nanops as nanops
 from pandas.util._decorators import (Appender, Substitution,
+                                     rewrite_axis_style_signature,
                                      deprecate_kwarg)
 from pandas.util._validators import validate_bool_kwarg, validate_fillna_kwargs
 from pandas.core import config
@@ -1080,20 +1081,6 @@ def rename(self, *args, **kwargs):
         if com.count_not_none(*axes.values()) == 0:
             raise TypeError('must pass an index to rename')
 
-        # renamer function if passed a dict
-        def _get_rename_function(mapper):
-            if isinstance(mapper, (dict, ABCSeries)):
-
-                def f(x):
-                    if x in mapper:
-                        return mapper[x]
-                    else:
-                        return x
-            else:
-                f = mapper
-
-            return f
-
         self._consolidate_inplace()
         result = self if inplace else self.copy(deep=copy)
 
@@ -1102,7 +1089,7 @@ def f(x):
             v = axes.get(self._AXIS_NAMES[axis])
             if v is None:
                 continue
-            f = _get_rename_function(v)
+            f = com.get_rename_function(v)
 
             baxis = self._get_block_manager_axis(axis)
             if level is not None:
@@ -1116,16 +1103,28 @@ def f(x):
         else:
             return result.__finalize__(self)
 
-    def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
+    @rewrite_axis_style_signature('mapper', [('copy', True),
+                                             ('inplace', False)])
+    def rename_axis(self, mapper=None, **kwargs):
         """
-        Alter the name of the index or columns.
+        Alter the name of the index or name of Index object that is the
+        columns.
 
         Parameters
         ----------
         mapper : scalar, list-like, optional
-            Value to set as the axis name attribute.
-        axis : {0 or 'index', 1 or 'columns'}, default 0
-            The index or the name of the axis.
+            Value to set the axis name attribute.
+        index, columns : scalar, list-like, 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/or ``columns``.
+
+            .. versionchanged:: 0.24.0
+
+        axis : int or string, default 0
         copy : boolean, default True
             Also copy underlying data.
         inplace : boolean, default False
@@ -1144,6 +1143,23 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
         deprecated and will be removed in a future version. Use ``rename``
         instead.
 
+        ``DataFrame.rename_axis`` supports two calling conventions
+
+        * ``(index=index_mapper, columns=columns_mapper, ...)``
+        * ``(mapper, axis={'index', 'columns'}, ...)``
+
+        The first calling convention will only modify the names of
+        the index and/or the names of the Index object that is the columns.
+        In this case, the parameter ``copy`` is ignored.
+
+        The second calling convention will modify the names of the
+        the corresponding index if mapper is a list or a scalar.
+        However, if mapper is dict-like or a function, it will use the
+        deprecated behavior of modifying the axis *labels*.
+
+        We *highly* recommend using keyword arguments to clarify your
+        intent.
+
         See Also
         --------
         pandas.Series.rename : Alter Series index labels or name
@@ -1177,20 +1193,94 @@ def rename_axis(self, mapper, axis=0, copy=True, inplace=False):
         0    1  4
         1    2  5
         2    3  6
-        """
+
+        >>> mi = pd.MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
+        ...                                 names=['let','num'])
+        >>> df = pd.DataFrame({'x': [i for i in range(len(mi))],
+        ...                    'y' : [i*10 for i in range(len(mi))]},
+        ...                    index=mi)
+        >>> df.rename_axis(index={'num' : 'n'})
+               x   y
+        let n
+        a   1  0   0
+            2  1  10
+        b   1  2  20
+            2  3  30
+        c   1  4  40
+            2  5  50
+
+        >>> cdf = df.rename_axis(columns='col')
+        >>> cdf
+        col      x   y
+        let num
+        a   1    0   0
+            2    1  10
+        b   1    2  20
+            2    3  30
+        c   1    4  40
+            2    5  50
+
+        >>> cdf.rename_axis(columns=str.upper)
+        COL      x   y
+        let num
+        a   1    0   0
+            2    1  10
+        b   1    2  20
+            2    3  30
+        c   1    4  40
+            2    5  50
+
+        """
+        axes, kwargs = self._construct_axes_from_arguments((), kwargs)
+        copy = kwargs.pop('copy', True)
+        inplace = kwargs.pop('inplace', False)
+        axis = kwargs.pop('axis', 0)
+        if axis is not None:
+            axis = self._get_axis_number(axis)
+
+        if kwargs:
+            raise TypeError('rename_axis() got an unexpected keyword '
+                            'argument "{0}"'.format(list(kwargs.keys())[0]))
+
         inplace = validate_bool_kwarg(inplace, 'inplace')
-        non_mapper = is_scalar(mapper) or (is_list_like(mapper) and not
-                                           is_dict_like(mapper))
-        if non_mapper:
-            return self._set_axis_name(mapper, axis=axis, inplace=inplace)
+
+        if (mapper is not None):
+            # Use v0.23 behavior if a scalar or list
+            non_mapper = is_scalar(mapper) or (is_list_like(mapper) and not
+                                               is_dict_like(mapper))
+            if non_mapper:
+                return self._set_axis_name(mapper, axis=axis, inplace=inplace)
+            else:
+                # Deprecated (v0.21) behavior is if mapper is specified,
+                # and not a list or scalar, then call rename
+                msg = ("Using 'rename_axis' to alter labels is deprecated. "
+                       "Use '.rename' instead")
+                warnings.warn(msg, FutureWarning, stacklevel=2)
+                axis = self._get_axis_name(axis)
+                d = {'copy': copy, 'inplace': inplace}
+                d[axis] = mapper
+                return self.rename(**d)
         else:
-            msg = ("Using 'rename_axis' to alter labels is deprecated. "
-                   "Use '.rename' instead")
-            warnings.warn(msg, FutureWarning, stacklevel=2)
-            axis = self._get_axis_name(axis)
-            d = {'copy': copy, 'inplace': inplace}
-            d[axis] = mapper
-            return self.rename(**d)
+            # Use new behavior.  Means that index and/or columns
+            # is specified
+            result = self if inplace else self.copy(deep=copy)
+
+            for axis in lrange(self._AXIS_LEN):
+                v = axes.get(self._AXIS_NAMES[axis])
+                if v is None:
+                    continue
+                non_mapper = is_scalar(v) or (is_list_like(v) and not
+                                              is_dict_like(v))
+                if non_mapper:
+                    newnames = v
+                else:
+                    f = com.get_rename_function(v)
+                    curnames = self._get_axis(axis).names
+                    newnames = [f(name) for name in curnames]
+                result._set_axis_name(newnames, axis=axis,
+                                      inplace=True)
+            if not inplace:
+                return result
 
     def _set_axis_name(self, name, axis=0, inplace=False):
         """
diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py
index 0bf2d537f3698..43deacb8fa247 100644
--- a/pandas/tests/frame/test_alter_axes.py
+++ b/pandas/tests/frame/test_alter_axes.py
@@ -522,22 +522,64 @@ def test_rename_axis_inplace(self, float_frame):
     def test_rename_axis_warns(self):
         # https://github.com/pandas-dev/pandas/issues/17833
         df = DataFrame({"A": [1, 2], "B": [1, 2]})
-        with tm.assert_produces_warning(FutureWarning) as w:
+        with tm.assert_produces_warning(FutureWarning,
+                                        check_stacklevel=False) as w:
             df.rename_axis(id, axis=0)
             assert 'rename' in str(w[0].message)
 
-        with tm.assert_produces_warning(FutureWarning) as w:
+        with tm.assert_produces_warning(FutureWarning,
+                                        check_stacklevel=False) as w:
             df.rename_axis({0: 10, 1: 20}, axis=0)
             assert 'rename' in str(w[0].message)
 
-        with tm.assert_produces_warning(FutureWarning) as w:
+        with tm.assert_produces_warning(FutureWarning,
+                                        check_stacklevel=False) as w:
             df.rename_axis(id, axis=1)
             assert 'rename' in str(w[0].message)
 
-        with tm.assert_produces_warning(FutureWarning) as w:
+        with tm.assert_produces_warning(FutureWarning,
+                                        check_stacklevel=False) as w:
             df['A'].rename_axis(id)
             assert 'rename' in str(w[0].message)
 
+    def test_rename_axis_mapper(self):
+        # GH 19978
+        mi = MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
+                                     names=['ll', 'nn'])
+        df = DataFrame({'x': [i for i in range(len(mi))],
+                        'y': [i * 10 for i in range(len(mi))]},
+                       index=mi)
+        result = df.rename_axis('cols', axis=1)
+        tm.assert_index_equal(result.columns,
+                              Index(['x', 'y'], name='cols'))
+
+        result = result.rename_axis(columns={'cols': 'new'}, axis=1)
+        tm.assert_index_equal(result.columns,
+                              Index(['x', 'y'], name='new'))
+
+        result = df.rename_axis(index={'ll': 'foo'})
+        assert result.index.names == ['foo', 'nn']
+
+        result = df.rename_axis(index=str.upper, axis=0)
+        assert result.index.names == ['LL', 'NN']
+
+        result = df.rename_axis(index=['foo', 'goo'])
+        assert result.index.names == ['foo', 'goo']
+
+        sdf = df.reset_index().set_index('nn').drop(columns=['ll', 'y'])
+        result = sdf.rename_axis(index='foo', columns='meh')
+        assert result.index.name == 'foo'
+        assert result.columns.name == 'meh'
+
+        with tm.assert_raises_regex(TypeError, 'Must pass'):
+            df.rename_axis(index='wrong')
+
+        with tm.assert_raises_regex(ValueError, 'Length of names'):
+            df.rename_axis(index=['wrong'])
+
+        with tm.assert_raises_regex(TypeError, 'bogus'):
+            df.rename_axis(bogus=None)
+
     def test_rename_multiindex(self):
 
         tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]
diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py
index 4e9da92edcf5e..99f3b1cfd0db7 100644
--- a/pandas/tests/io/test_pytables.py
+++ b/pandas/tests/io/test_pytables.py
@@ -1371,8 +1371,8 @@ def test_append_with_strings(self):
             with catch_warnings(record=True):
                 simplefilter("ignore", FutureWarning)
                 wp = tm.makePanel()
-                wp2 = wp.rename_axis(
-                    {x: "%s_extra" % x for x in wp.minor_axis}, axis=2)
+                wp2 = wp.rename(
+                    minor_axis={x: "%s_extra" % x for x in wp.minor_axis})
 
                 def check_col(key, name, size):
                     assert getattr(store.get_storer(key)
diff --git a/pandas/tests/reshape/test_concat.py b/pandas/tests/reshape/test_concat.py
index d39c9fafe5749..a2ab10a4955a0 100644
--- a/pandas/tests/reshape/test_concat.py
+++ b/pandas/tests/reshape/test_concat.py
@@ -1553,12 +1553,11 @@ def df():
         panel1 = make_panel()
         panel2 = make_panel()
 
-        panel2 = panel2.rename_axis({x: "%s_1" % x
-                                     for x in panel2.major_axis},
-                                    axis=1)
+        panel2 = panel2.rename(major_axis={x: "%s_1" % x
+                                           for x in panel2.major_axis})
 
-        panel3 = panel2.rename_axis(lambda x: '%s_1' % x, axis=1)
-        panel3 = panel3.rename_axis(lambda x: '%s_1' % x, axis=2)
+        panel3 = panel2.rename(major_axis=lambda x: '%s_1' % x)
+        panel3 = panel3.rename(minor_axis=lambda x: '%s_1' % x)
 
         # it works!
         concat([panel1, panel3], axis=1, verify_integrity=True, sort=sort)
diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py
index 7d4aa2d4df6fc..82b9768a9ce9c 100644
--- a/pandas/tests/series/test_alter_axes.py
+++ b/pandas/tests/series/test_alter_axes.py
@@ -227,6 +227,24 @@ def test_reorder_levels(self):
         expected = Series(np.arange(6), index=e_idx)
         tm.assert_series_equal(result, expected)
 
+    def test_rename_axis_mapper(self):
+        # GH 19978
+        mi = MultiIndex.from_product([['a', 'b', 'c'], [1, 2]],
+                                     names=['ll', 'nn'])
+        s = Series([i for i in range(len(mi))], index=mi)
+
+        result = s.rename_axis(index={'ll': 'foo'})
+        assert result.index.names == ['foo', 'nn']
+
+        result = s.rename_axis(index=str.upper, axis=0)
+        assert result.index.names == ['LL', 'NN']
+
+        result = s.rename_axis(index=['foo', 'goo'])
+        assert result.index.names == ['foo', 'goo']
+
+        with tm.assert_raises_regex(TypeError, 'unexpected'):
+            s.rename_axis(columns='wrong')
+
     def test_rename_axis_inplace(self, datetime_series):
         # GH 15704
         expected = datetime_series.rename_axis('foo')
diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py
index 51c779c6a97a3..775fcc2684f42 100644
--- a/pandas/tests/test_panel.py
+++ b/pandas/tests/test_panel.py
@@ -2110,16 +2110,16 @@ def test_repr_empty(self):
     def test_rename(self):
         mapper = {'ItemA': 'foo', 'ItemB': 'bar', 'ItemC': 'baz'}
 
-        renamed = self.panel.rename_axis(mapper, axis=0)
+        renamed = self.panel.rename(items=mapper)
         exp = Index(['foo', 'bar', 'baz'])
         tm.assert_index_equal(renamed.items, exp)
 
-        renamed = self.panel.rename_axis(str.lower, axis=2)
+        renamed = self.panel.rename(minor_axis=str.lower)
         exp = Index(['a', 'b', 'c', 'd'])
         tm.assert_index_equal(renamed.minor_axis, exp)
 
         # don't copy
-        renamed_nocopy = self.panel.rename_axis(mapper, axis=0, copy=False)
+        renamed_nocopy = self.panel.rename(items=mapper, copy=False)
         renamed_nocopy['foo'] = 3.
         assert (self.panel['ItemA'].values == 3).all()
 


Code review comments:
diff_hunk: @@ -454,3 +454,21 @@ def _pipe(obj, func, *args, **kwargs):
         return func(*args, **kwargs)
     else:
         return func(obj, *args, **kwargs)
+
+
+def get_rename_function(mapper):
path: pandas/core/common.py
text: @user1:
can you make this private (e.g. _get_rename_function)

@author:
done