Repository: pandas-dev/pandas

Issue:
BUG: DataFrame.astype(dict) can cause column metadata to be lost
#### Code Sample, a copy-pastable example if possible

Setup:
```python
In [2]: df = pd.DataFrame(data=np.random.randint(5, size=(3, 4)),
   ...:                   columns=pd.UInt64Index(range(500, 504), name='foo'))
   ...:

In [3]: df
Out[3]:
foo  500  501  502  503
0      4    4    2    1
1      0    2    3    1
2      1    4    0    0

In [4]: df.columns
Out[4]: UInt64Index([500, 501, 502, 503], dtype='uint64', name='foo')
```

Using `.astype(dict)` loses column metadata:
```python
In [5]: df = df.astype({500: 'float64', 501: 'uint64'})

In [6]: df.columns
Out[6]: Int64Index([500, 501, 502, 503], dtype='int64')
```

#### Problem description

Column metadata is lost: the `name` has been removed and it has been cast from `UInt64Index` to `Int64Index`.

#### Expected Output

I'd expect the columns post-`astype` to be the same as pre-`astype`.



Discussion:
xref https://github.com/pandas-dev/pandas/pull/18099#discussion_r170541955

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -4436,17 +4436,21 @@ def astype(self, dtype, copy=True, errors='raise', **kwargs):
                     results.append(col.astype(dtype[col_name], copy=copy))
                 else:
                     results.append(results.append(col.copy() if copy else col))
-            return pd.concat(results, axis=1, copy=False)
 
         elif is_categorical_dtype(dtype) and self.ndim > 1:
             # GH 18099: columnwise conversion to categorical
             results = (self[col].astype(dtype, copy=copy) for col in self)
-            return pd.concat(results, axis=1, copy=False)
 
-        # else, only a single dtype is given
-        new_data = self._data.astype(dtype=dtype, copy=copy, errors=errors,
-                                     **kwargs)
-        return self._constructor(new_data).__finalize__(self)
+        else:
+            # else, only a single dtype is given
+            new_data = self._data.astype(dtype=dtype, copy=copy, errors=errors,
+                                         **kwargs)
+            return self._constructor(new_data).__finalize__(self)
+
+        # GH 19920: retain column metadata after concat
+        result = pd.concat(results, axis=1, copy=False)
+        result.columns = self.columns
+        return result
 
     def copy(self, deep=True):
         """


Test patch:
diff --git a/pandas/tests/frame/test_dtypes.py b/pandas/tests/frame/test_dtypes.py
--- a/pandas/tests/frame/test_dtypes.py
+++ b/pandas/tests/frame/test_dtypes.py
@@ -649,6 +649,15 @@ def test_astype_categoricaldtype_class_raises(self, cls):
         with tm.assert_raises_regex(TypeError, xpr):
             df['A'].astype(cls)
 
+    @pytest.mark.parametrize('dtype', [
+        {100: 'float64', 200: 'uint64'}, 'category', 'float64'])
+    def test_astype_column_metadata(self, dtype):
+        # GH 19920
+        columns = pd.UInt64Index([100, 200, 300], name='foo')
+        df = DataFrame(np.arange(15).reshape(5, 3), columns=columns)
+        df = df.astype(dtype)
+        tm.assert_index_equal(df.columns, columns)
+
     @pytest.mark.parametrize("dtype", ["M8", "m8"])
     @pytest.mark.parametrize("unit", ['ns', 'us', 'ms', 's', 'h', 'm', 'D'])
     def test_astype_from_datetimelike_to_objectt(self, dtype, unit):