Repository: pandas-dev/pandas

Issue:
rename_axis(None) now (0.24) does nothing. Previously it removed the axis name
#### Code Sample, a copy-pastable example if possible

In 0.23 `rename_axis` was able to remove the axis name like this:
```python
>>> s = pd.Series([1, 2, 3])
>>> s = s.rename_axis('asdf')
>>> s
asdf
0    1
1    2
2    3
dtype: int64

>>> s.rename_axis(None)
0    1
1    2
2    3
dtype: int64
```

In 0.24, the `mapper` parameter is defaulted to `None` so this does not work. The only way to remove an axis label is to use `del s.index.name` or `s.index.name = None`
#### Problem description

Seems like a regression. I have some code that relies on axis name removal with `s.rename_axis(None)`. Seems like there is no clean way to remove an axis name now.

#### Expected Output

`s.rename_axis(None)` should work as it did in 0.23.



Discussion:
`Series.rename(None)` would be my recommended way of removing the name from
a Series.

We could use `object()` as the default mapper if we want to support
`Series.rename_axis(None)`.

On Wed, Jan 30, 2019 at 11:22 AM Ted Petrou <notifications@github.com>
wrote:

> Code Sample, a copy-pastable example if possible
>
> In 0.23 rename_axis was able to remove the axis name like this:
>
> >>> s = pd.Series([1, 2, 3])>>> s = s.rename_axis('asdf')>>> s
> asdf0    11    22    3
> dtype: int64
> >>> s.rename_axis(None)0    11    22    3
> dtype: int64
>
> In 0.24, the mapper parameter is defaulted to None so this does not work.
> The only way to remove an axis label is to use del s.index.name or
> s.index.name = None
> Problem description
>
> Seems like a regression. I have some code that relies on axis name removal
> with s.rename_axis(None). Seems like there is no clean way to remove an
> axis name now.
> Expected Output
>
> s.rename_axis(None) should work as it did in 0.23.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/25034>, or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIke1TnqYETrTFTQkV_LufIcuE2Pwks5vIdTEgaJpZM4aarl4>
> .
>

@TomAugspurger That renames the Series `name` attribute. My example involves renaming the axis name. This would go for both the Series and the DataFrame.
Ah, sorry misread.

On Wed, Jan 30, 2019 at 11:37 AM Ted Petrou <notifications@github.com>
wrote:

> @TomAugspurger <https://github.com/TomAugspurger> That renames the Series
> name attribute. My example involves renaming the axis name. This would go
> for both the Series and the DataFrame.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/25034#issuecomment-459036872>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHItdbUXetlr8P_gUL4rpTDwZuCtoUks5vIdhPgaJpZM4aarl4>
> .
>

> The only way to remove an axis label is to use `del s.index.name` or `s.index.name = None`

You can also pass `lambda x: None` for the `index` argument of `rename_axis`.

Still should fix the regression of course, but at least this provides method chaining compatible workaround in the meantime:

```python
In [2]: s = pd.Series([1, 2, 3], index=pd.Index(list('abc'), name='foo'))

In [3]: s
Out[3]:
foo
a    1
b    2
c    3
dtype: int64

In [4]: s.rename_axis(index=lambda x: None)
Out[4]:
a    1
b    2
c    3
dtype: int64
```
@jschendel Thank you for pointing out an alternative. Seems like one way to correct this is to have all the axis level names set equal to None when calling `s.rename_axis()` or `df.rename_axis()`. I'm not even sure what semantics would work here.

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -61,6 +61,10 @@
         by : str or list of str
             Name or list of names to sort by""")
 
+# sentinel value to use as kwarg in place of None when None has special meaning
+# and needs to be distinguished from a user explicitly passing None.
+sentinel = object()
+
 
 def _single_replace(self, to_replace, method, inplace, limit):
     """
@@ -290,11 +294,16 @@ def _construct_axes_dict_for_slice(self, axes=None, **kwargs):
         d.update(kwargs)
         return d
 
-    def _construct_axes_from_arguments(self, args, kwargs, require_all=False):
+    def _construct_axes_from_arguments(
+            self, args, kwargs, require_all=False, sentinel=None):
         """Construct and returns axes if supplied in args/kwargs.
 
         If require_all, raise if all axis arguments are not supplied
         return a tuple of (axes, kwargs).
+
+        sentinel specifies the default parameter when an axis is not
+        supplied; useful to distinguish when a user explicitly passes None
+        in scenarios where None has special meaning.
         """
 
         # construct the args
@@ -322,7 +331,7 @@ def _construct_axes_from_arguments(self, args, kwargs, require_all=False):
                         raise TypeError("not enough/duplicate arguments "
                                         "specified!")
 
-        axes = {a: kwargs.pop(a, None) for a in self._AXIS_ORDERS}
+        axes = {a: kwargs.pop(a, sentinel) for a in self._AXIS_ORDERS}
         return axes, kwargs
 
     @classmethod
@@ -1089,7 +1098,7 @@ def rename(self, *args, **kwargs):
 
     @rewrite_axis_style_signature('mapper', [('copy', True),
                                              ('inplace', False)])
-    def rename_axis(self, mapper=None, **kwargs):
+    def rename_axis(self, mapper=sentinel, **kwargs):
         """
         Set the name of the axis for the index or columns.
 
@@ -1218,7 +1227,8 @@ class  name
                cat            4         0
                monkey         2         2
         """
-        axes, kwargs = self._construct_axes_from_arguments((), kwargs)
+        axes, kwargs = self._construct_axes_from_arguments(
+            (), kwargs, sentinel=sentinel)
         copy = kwargs.pop('copy', True)
         inplace = kwargs.pop('inplace', False)
         axis = kwargs.pop('axis', 0)
@@ -1231,7 +1241,7 @@ class  name
 
         inplace = validate_bool_kwarg(inplace, 'inplace')
 
-        if (mapper is not None):
+        if (mapper is not sentinel):
             # 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))
@@ -1254,7 +1264,7 @@ class  name
 
             for axis in lrange(self._AXIS_LEN):
                 v = axes.get(self._AXIS_NAMES[axis])
-                if v is None:
+                if v is sentinel:
                     continue
                 non_mapper = is_scalar(v) or (is_list_like(v) and not
                                               is_dict_like(v))


Test patch:
diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py
--- a/pandas/tests/frame/test_alter_axes.py
+++ b/pandas/tests/frame/test_alter_axes.py
@@ -600,6 +600,26 @@ def test_rename_axis_mapper(self):
         with pytest.raises(TypeError, match='bogus'):
             df.rename_axis(bogus=None)
 
+    @pytest.mark.parametrize('kwargs, rename_index, rename_columns', [
+        ({'mapper': None, 'axis': 0}, True, False),
+        ({'mapper': None, 'axis': 1}, False, True),
+        ({'index': None}, True, False),
+        ({'columns': None}, False, True),
+        ({'index': None, 'columns': None}, True, True),
+        ({}, False, False)])
+    def test_rename_axis_none(self, kwargs, rename_index, rename_columns):
+        # GH 25034
+        index = Index(list('abc'), name='foo')
+        columns = Index(['col1', 'col2'], name='bar')
+        data = np.arange(6).reshape(3, 2)
+        df = DataFrame(data, index, columns)
+
+        result = df.rename_axis(**kwargs)
+        expected_index = index.rename(None) if rename_index else index
+        expected_columns = columns.rename(None) if rename_columns else columns
+        expected = DataFrame(data, expected_index, expected_columns)
+        tm.assert_frame_equal(result, expected)
+
     def test_rename_multiindex(self):
 
         tuples_index = [('foo1', 'bar1'), ('foo2', 'bar2')]
diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py
--- a/pandas/tests/series/test_alter_axes.py
+++ b/pandas/tests/series/test_alter_axes.py
@@ -258,6 +258,17 @@ def test_rename_axis_inplace(self, datetime_series):
         assert no_return is None
         tm.assert_series_equal(result, expected)
 
+    @pytest.mark.parametrize('kwargs', [{'mapper': None}, {'index': None}, {}])
+    def test_rename_axis_none(self, kwargs):
+        # GH 25034
+        index = Index(list('abc'), name='foo')
+        df = Series([1, 2, 3], index=index)
+
+        result = df.rename_axis(**kwargs)
+        expected_index = index.rename(None) if kwargs else index
+        expected = Series([1, 2, 3], index=expected_index)
+        tm.assert_series_equal(result, expected)
+
     def test_set_axis_inplace_axes(self, axis_series):
         # GH14636
         ser = Series(np.arange(4), index=[1, 3, 5, 7], dtype='int64')