Repository: pandas-dev/pandas
Pull request: Bug fix - rename dataframe reset index
- [x] closes #6878
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


Problem statement:
ENH: name(s) argument for reset_index?
An idea: to be able to specify the a name when resetting the index, which would override the index name or the default value when index name is None.

Eg something like this:

```
In [10]: df = pd.DataFrame(np.random.rand(4,3), columns=list('ABC'), index=pd.date_range('2013-01-01', periods=4))
In [11]: df
Out[11]: 
                   A         B         C
2013-01-01  0.313664  0.606445  0.048081
2013-01-02  0.907785  0.004429  0.374456
2013-01-03  0.916584  0.067639  0.467712
2013-01-04  0.712824  0.687895  0.397960

In [12]: df.reset_index()
Out[12]: 
       index         A         B         C
0 2013-01-01  0.313664  0.606445  0.048081
1 2013-01-02  0.907785  0.004429  0.374456
2 2013-01-03  0.916584  0.067639  0.467712
3 2013-01-04  0.712824  0.687895  0.397960

In [12]: df.reset_index(name='datetime')
Out[12]: 
    datetime         A         B         C
0 2013-01-01  0.313664  0.606445  0.048081
1 2013-01-02  0.907785  0.004429  0.374456
2 2013-01-03  0.916584  0.067639  0.467712
3 2013-01-04  0.712824  0.687895  0.397960
```

For multi-index, you should provide a list.

Hints from issue discussion:
you can do

df = df.copy()
df.index = df.index.set_names([...])
df.reset_index()

so this is a convience yes?

yes, as a convenience I think (and like this it would do a copy less, only the copy inside reset_index?)

Series already has this do just a extending to df yes?

sounds good

Ah, I didn't know Series.reset_index already had something like this. Would certainly be a reason to get that aligned.

Only problems is that it is different there, as it specifies the name for the Series values, not for the index ..

```
In [19]: s = df['A']
In [20]: s.reset_index()
Out[20]: 
       index         A
0 2013-01-01  0.313664
1 2013-01-02  0.907785
2 2013-01-03  0.916584
3 2013-01-04  0.712824

In [21]: s.reset_index(name='other name')
Out[21]: 
       index  other name
0 2013-01-01    0.313664
1 2013-01-02    0.907785
2 2013-01-03    0.916584
3 2013-01-04    0.712824
```

index name kwarg?

This also partially solves #6181

Commit message: Docstring and typing validation

Patch to review:
diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst
index 86444d0b24002..388bc509a3512 100644
--- a/doc/source/whatsnew/v1.5.0.rst
+++ b/doc/source/whatsnew/v1.5.0.rst
@@ -41,7 +41,7 @@ Other enhancements
 - Implemented a ``bool``-dtype :class:`Index`, passing a bool-dtype array-like to ``pd.Index`` will now retain ``bool`` dtype instead of casting to ``object`` (:issue:`45061`)
 - Implemented a complex-dtype :class:`Index`, passing a complex-dtype array-like to ``pd.Index`` will now retain complex dtype instead of casting to ``object`` (:issue:`45845`)
 - :class:`Series` and :class:`DataFrame` with ``IntegerDtype`` now supports bitwise operations (:issue:`34463`)
--
+- :meth:`DataFrame.reset_index` now accepts a ``names`` argument which renames the index names (:issue:`6878`)
 
 .. ---------------------------------------------------------------------------
 .. _whatsnew_150.notable_bug_fixes:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index cf1988808bbb0..4c65a1ac3a411 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -5625,6 +5625,7 @@ def reset_index(
         col_level: Hashable = ...,
         col_fill: Hashable = ...,
         allow_duplicates: bool | lib.NoDefault = ...,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> DataFrame:
         ...
 
@@ -5637,6 +5638,7 @@ def reset_index(
         col_level: Hashable = ...,
         col_fill: Hashable = ...,
         allow_duplicates: bool | lib.NoDefault = ...,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> None:
         ...
 
@@ -5649,6 +5651,7 @@ def reset_index(
         col_level: Hashable = ...,
         col_fill: Hashable = ...,
         allow_duplicates: bool | lib.NoDefault = ...,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> None:
         ...
 
@@ -5661,6 +5664,7 @@ def reset_index(
         col_level: Hashable = ...,
         col_fill: Hashable = ...,
         allow_duplicates: bool | lib.NoDefault = ...,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> None:
         ...
 
@@ -5672,6 +5676,7 @@ def reset_index(
         col_level: Hashable = ...,
         col_fill: Hashable = ...,
         allow_duplicates: bool | lib.NoDefault = ...,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> None:
         ...
 
@@ -5684,6 +5689,7 @@ def reset_index(
         col_level: Hashable = ...,
         col_fill: Hashable = ...,
         allow_duplicates: bool | lib.NoDefault = ...,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> DataFrame | None:
         ...
 
@@ -5696,6 +5702,7 @@ def reset_index(
         col_level: Hashable = 0,
         col_fill: Hashable = "",
         allow_duplicates: bool | lib.NoDefault = lib.no_default,
+        names: Hashable | Sequence[Hashable] = None,
     ) -> DataFrame | None:
         """
         Reset the index, or a level of it.
@@ -5723,6 +5730,10 @@ def reset_index(
             levels are named. If None then the index name is repeated.
         allow_duplicates : bool, optional, default lib.no_default
             Allow duplicate column labels to be created.
+        names : str, tuple or list, default None
+            Using the given string, rename the DataFrame column which contains the
+            index data. If the DataFrame has a MultiIndex, this has to be a list or
+            tuple with length equal to the number of levels.
 
             .. versionadded:: 1.5.0
 
@@ -5841,6 +5852,15 @@ class    max    type
         parrot           bird   24.0     fly
         lion           mammal   80.5     run
         monkey         mammal    NaN    jump
+
+        Using the `names` parameter, choose a name for the index column:
+
+        >>> df.reset_index(names='name')
+            name   class  max_speed
+        0  falcon    bird      389.0
+        1  parrot    bird       24.0
+        2    lion  mammal       80.5
+        3  monkey  mammal        NaN
         """
         inplace = validate_bool_kwarg(inplace, "inplace")
         self._check_inplace_and_allows_duplicate_labels(inplace)
@@ -5861,12 +5881,12 @@ class    max    type
 
         if not drop:
             to_insert: Iterable[tuple[Any, Any | None]]
+
+            default = "index" if "index" not in self else "level_0"
+            names = self.index.get_default_index_names(names, default)
             if isinstance(self.index, MultiIndex):
-                names = com.fill_missing_names(self.index.names)
                 to_insert = zip(self.index.levels, self.index.codes)
             else:
-                default = "index" if "index" not in self else "level_0"
-                names = [default] if self.index.name is None else [self.index.name]
                 to_insert = ((self.index, None),)
 
             multi_col = isinstance(self.columns, MultiIndex)
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
index 25112ead2f0b7..349c15ee6bc76 100644
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -1765,6 +1765,29 @@ def _validate_names(
 
         return new_names
 
+    def get_default_index_names(self, names=None, default=None) -> Sequence[str]:
+        from pandas.core.indexes.multi import MultiIndex
+
+        # if names is not None and not all(isinstance(name, str) for name in names):
+        #    raise ValueError("Names must be a string")
+        if names is not None:
+            if isinstance(names, str):
+                names = [names]
+            elif isinstance(names, list) and not all(
+                isinstance(name, str) for name in names
+            ):
+                raise ValueError("Names must be a string")
+            elif not (isinstance(names, list) and not isinstance(names, str)):
+                raise ValueError("Names must be a string or list")
+
+        if not names:
+            if isinstance(self, MultiIndex):
+                names = com.fill_missing_names(self.names)
+            else:
+                names = [default] if self.name is None else [self.name]
+
+        return names
+
     def _get_names(self) -> FrozenList:
         return FrozenList((self.name,))
 
diff --git a/pandas/tests/frame/methods/test_reset_index.py b/pandas/tests/frame/methods/test_reset_index.py
index 840d0c6e6bdf4..0817970204c47 100644
--- a/pandas/tests/frame/methods/test_reset_index.py
+++ b/pandas/tests/frame/methods/test_reset_index.py
@@ -754,3 +754,30 @@ def test_reset_index_interval_columns_object_cast():
         columns=Index(["Year", Interval(0, 1), Interval(1, 2)]),
     )
     tm.assert_frame_equal(result, expected)
+
+
+def test_reset_index_rename(float_frame):
+    # GH 6878
+    result = float_frame.reset_index(names="new_name")
+    expected = Series(float_frame.index.values, name="new_name")
+    tm.assert_series_equal(result["new_name"], expected)
+
+    with pytest.raises(ValueError, match="Names must be a string"):
+        float_frame.reset_index(names=1)
+
+
+def test_reset_index_rename_multiindex(float_frame):
+    # GH 6878
+    stacked_df = float_frame.stack()[::2]
+    stacked_df = DataFrame({"foo": stacked_df, "bar": stacked_df})
+
+    names = ["first", "second"]
+    stacked_df.index.names = names
+
+    result = stacked_df.reset_index()
+    expected = stacked_df.reset_index(names=["new_first", "new_second"])
+    tm.assert_series_equal(result["first"], expected["new_first"], check_names=False)
+    tm.assert_series_equal(result["second"], expected["new_second"], check_names=False)
+
+    with pytest.raises(ValueError, match="Names must be a string or list"):
+        stacked_df.reset_index(names={"first": "new_first", "second": "new_second"})


Code review comments:
diff_hunk: @@ -1765,6 +1765,29 @@ def _validate_names(
 
         return new_names
 
+    def get_default_index_names(self, names=None, default=None) -> Sequence[str]:
+        from pandas.core.indexes.multi import MultiIndex
+
+        # if names is not None and not all(isinstance(name, str) for name in names):
+        #    raise ValueError("Names must be a string")
+        if names is not None:
+            if isinstance(names, str):
+                names = [names]
+            elif isinstance(names, list) and not all(
path: pandas/core/indexes/base.py
text: @user1:
you can use is_list_like

@user1:
why does this have to be a string? 

@author:
My mistake. Condition of index names should only in type either in int, str or 1-dimensional list, default None
```
names : int, str or 1-dimensional list, default None
``` 
Have make code changes.