Repository: pandas-dev/pandas
Pull request: BUG: Preserve name in Index.astype
- [x] closes #32013
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry


Problem statement:
pandas.index.copy remove index name information when specifying new dtype
#### Code Sample, a copy-pastable example if possible

```python
>>> import pandas as pd
>>> named_idx = pd.Index([17.3, 69.221, 33.1, 15.5, 19.3, 74.8, 10, 5.5]).set_names('old_name')
>>> named_idx
Float64Index([17.3, 69.221, 33.1, 15.5, 19.3, 74.8, 10.0, 5.5], dtype='float64', name='old_name')
>>> named_idx.copy(name='new_name', dtype='int64')
Int64Index([17, 69, 33, 15, 19, 74, 10, 5], dtype='int64')
>>> named_idx.copy(dtype='int64')
Int64Index([17, 69, 33, 15, 19, 74, 10, 5], dtype='int64')

```
#### Problem description
When set new dtype parameter in function pandas.index.copy somehow it remove name information on new object

#### Expected Output
>>> import pandas as pd
>>> named_idx = pd.Index([17.3, 69.221, 33.1, 15.5, 19.3, 74.8, 10, 5.5]).set_names('old_name')
>>> named_idx
Float64Index([17.3, 69.221, 33.1, 15.5, 19.3, 74.8, 10.0, 5.5], dtype='float64', name='old_name')
>>> named_idx.copy(name='new_name', dtype='int64')
Int64Index([17, 69, 33, 15, 19, 74, 10, 5], dtype='int64', name='new_name')
>>> named_idx.copy(dtype='int64')
Int64Index([17, 69, 33, 15, 19, 74, 10, 5], dtype='int64', name='old_name')

#### Output of ``pd.show_versions()``

<details>
>>> pd.show_versions()

INSTALLED VERSIONS
------------------
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}

{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
{k:<17}: {stat}
</details>

Hints from issue discussion:

Commit message: Change name

Patch to review:
diff --git a/doc/source/whatsnew/v1.0.2.rst b/doc/source/whatsnew/v1.0.2.rst
index 0216007ea5ba8..50d0d109bec1e 100644
--- a/doc/source/whatsnew/v1.0.2.rst
+++ b/doc/source/whatsnew/v1.0.2.rst
@@ -37,7 +37,9 @@ Bug fixes
 - Using ``pd.NA`` with :meth:`DataFrame.to_json` now correctly outputs a null value instead of an empty object (:issue:`31615`)
 - Fixed bug in parquet roundtrip with nullable unsigned integer dtypes (:issue:`31896`).
 
+**Index**
 
+- Fixed bug where :meth:`Index.astype` would lose the name attribute when converting from ``Float64Index`` to ``Int64Index``. (:issue:`32013`)
 
 **Experimental dtypes**
 
diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py
index 877b3d1d2ba30..24764d9e59ed9 100644
--- a/pandas/core/indexes/numeric.py
+++ b/pandas/core/indexes/numeric.py
@@ -376,7 +376,7 @@ def astype(self, dtype, copy=True):
             # TODO(jreback); this can change once we have an EA Index type
             # GH 13149
             arr = astype_nansafe(self.values, dtype=dtype)
-            return Int64Index(arr)
+            return Int64Index(arr, name=self.name)
         return super().astype(dtype, copy=copy)
 
     # ----------------------------------------------------------------
diff --git a/pandas/tests/indexes/base_class/test_astype.py b/pandas/tests/indexes/base_class/test_astype.py
new file mode 100644
index 0000000000000..11ccba7cc20fd
--- /dev/null
+++ b/pandas/tests/indexes/base_class/test_astype.py
@@ -0,0 +1,21 @@
+import pytest
+
+from pandas import Index
+
+
+@pytest.mark.parametrize(
+    "from_type, to_type", [("float64", "int64"), ("int64", "float64")]
+)
+def test_astype_preserves_name(from_type, to_type):
+    idx = Index([1, 2], name="abc", dtype=from_type).astype(to_type)
+
+    assert idx.name == "abc"
+
+
+@pytest.mark.parametrize(
+    "from_type, to_type", [("float64", "int64"), ("int64", "float64")]
+)
+def test_copy_with_astype_preserves_name(from_type, to_type):
+    idx = Index([1, 2], name="abc", dtype=from_type).copy(dtype=to_type)
+
+    assert idx.name == "abc"


Code review comments:
diff_hunk: @@ -0,0 +1,21 @@
+import pytest
+
+from pandas import Index
+
+
+@pytest.mark.parametrize(
path: pandas/tests/indexes/base_class/test_astype.py
text: @user1:
this is not very comprehensive. just hook into the current astype tests and check for this.

@user1:
there are llikely *many* more paths that are not exactly correct.

@user1:
@author this is the much more important point

@author:
I'm not sure what's meant by the current astype tests since there are lots of them. Would extending the parameterization work for being more comprehensive?

@user1:
no but i would rather simply add a check i. the current astype tests 

there are many many cases and this doesn’t begin to cover

@user3:
just spitballing, one option would be something like:

```
@user2.mark.parametrize("to_dtype", [lots, of, stuff])
def test_astype_retains_name(indices, to_dtype):
    indices.name = "foo"  # <-- maybe randomize, include some null-like names
    try:
        result = indices.astype(to_dtype)
    except TypeErrorOrValueErrorOrSomething:
        return
    assert result.name == indices.name
```

@author:
Not a bad idea, was struggling with how to test this exhaustively when certain conversions won't even be possible

@user3:
@author did you decide to stick with whats currently here, or have another approach in mind?

@author:
@user3 I think I'll try your idea for these tests

@author:
With this approach I'm assuming we'd skip the MultiIndex cases in the indices fixture, since the name attribute isn't so interesting (we care about the _names_)? Those seem to be all the test failures.

@user3:
that or check that the names are retained.  If this isnt for just the base Index class, then it probably belongs in tests.indexes.test_common