Skip to content

Commit a633a57

Browse files
Preserve nullable index/column dtypes in rename path
1 parent 2b9c54e commit a633a57

4 files changed

Lines changed: 83 additions & 1 deletion

File tree

doc/source/whatsnew/v3.1.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ Indexing
275275
- Bug in :meth:`DataFrame.loc` raising ``ValueError`` when setting a row with a list-like value on a single-column :class:`DataFrame` with :class:`ExtensionArray` dtype (:issue:`44103`)
276276
- Bug in :meth:`DataFrame.loc` with a :class:`MultiIndex` returning wrong results instead of raising ``KeyError`` when passing string keys for numeric index levels (:issue:`60104`)
277277
- Bug in :meth:`DataFrame.mask` with ``inplace=True`` where incorrect values were produced when ``other`` was a :class:`Series` with :class:`ExtensionArray` values (:issue:`64635`)
278+
- Bug in :meth:`DataFrame.rename` and :meth:`Series.rename` not preserving nullable extension dtype (e.g. ``Int64``, ``Float64``) when relabeling index or column labels (:issue:`65315`)
278279
- Bug in :meth:`DataFrame.where` and :meth:`DataFrame.mask` raising ``TypeError`` when ``cond`` is a :class:`Series` and ``axis=1`` (:issue:`58190`)
279280
- Bug in :meth:`DataFrame.xs` where ``drop_level=False`` was ignored for fully specified :class:`MultiIndex` keys when ``level`` was not explicitly provided (:issue:`6507`)
280281
- Bug in :meth:`Index.get_level_values` mishandling boolean, NA-like (``np.nan``, ``pd.NA``, ``pd.NaT``) and integer index names (:issue:`62169`)

pandas/core/indexes/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6889,7 +6889,18 @@ def _transform_index(self, func: Callable, *, level: int | None = None) -> Index
68896889
return type(self).from_arrays(values)
68906890
else:
68916891
items = [func(x) for x in self]
6892-
return Index(items, name=self.name, tupleize_cols=False)
6892+
if not items:
6893+
return Index(
6894+
items, dtype=self.dtype, name=self.name, tupleize_cols=False
6895+
)
6896+
new_values = self.array._cast_pointwise_result(items)
6897+
return Index(
6898+
new_values,
6899+
dtype=new_values.dtype,
6900+
copy=False,
6901+
name=self.name,
6902+
tupleize_cols=False,
6903+
)
68936904

68946905
def isin(
68956906
self, values: Axes | set, level: str_t | int | None = None

pandas/tests/frame/methods/test_rename.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import pytest
66

77
from pandas import (
8+
NA,
89
DataFrame,
910
Index,
1011
MultiIndex,
1112
Series,
13+
array,
1214
merge,
1315
)
1416
import pandas._testing as tm
@@ -411,6 +413,64 @@ def test_rename_boolean_index(self):
411413
)
412414
tm.assert_frame_equal(res, exp)
413415

416+
def test_rename_preserves_nullable_int_index(self):
417+
# GH#65315
418+
df = DataFrame(
419+
{"val": [1, 2, 3]},
420+
index=Index(array([1, 2, 3], dtype="Int64"), name="id"),
421+
)
422+
result = df.rename({1: 9})
423+
expected = Index(array([9, 2, 3], dtype="Int64"), name="id")
424+
tm.assert_index_equal(result.index, expected)
425+
426+
def test_rename_preserves_nullable_float_columns(self):
427+
# GH#65315
428+
df = DataFrame(
429+
[[1, 2, 3]],
430+
columns=Index(array([1.0, 2.0, 3.0], dtype="Float64")),
431+
)
432+
result = df.rename(columns={1.0: 9.0})
433+
expected = Index(array([9.0, 2.0, 3.0], dtype="Float64"))
434+
tm.assert_index_equal(result.columns, expected)
435+
436+
def test_rename_nullable_index_to_na(self):
437+
# GH#65315
438+
df = DataFrame(
439+
{"val": [1, 2, 3]},
440+
index=Index(array([1, 2, 3], dtype="Int64")),
441+
)
442+
result = df.rename({1: NA})
443+
expected = Index(array([NA, 2, 3], dtype="Int64"))
444+
tm.assert_index_equal(result.index, expected)
445+
446+
def test_rename_empty_nullable_index(self):
447+
# GH#65315
448+
df = DataFrame({"val": []}, index=Index(array([], dtype="Int64")))
449+
result = df.rename(index={})
450+
tm.assert_index_equal(result.index, df.index)
451+
452+
def test_rename_nullable_index_type_change_widens(self):
453+
# GH#65315
454+
df = DataFrame(
455+
{"val": [1, 2, 3]},
456+
index=Index(array([1, 2, 3], dtype="Int64")),
457+
)
458+
result = df.rename(lambda x: f"label_{x}")
459+
assert list(result.index) == ["label_1", "label_2", "label_3"]
460+
assert result.index.dtype != "Int64"
461+
462+
def test_rename_tuple_columns_not_multiindex(self):
463+
# GH#65315
464+
df = DataFrame(
465+
[[1, 2]],
466+
columns=Index(array([1, 2], dtype="Int64")),
467+
)
468+
result = df.rename(columns=lambda x: (x, x))
469+
assert not isinstance(result.columns, MultiIndex)
470+
tm.assert_index_equal(
471+
result.columns, Index([(1, 1), (2, 2)], tupleize_cols=False)
472+
)
473+
414474
def test_rename_non_unique_index_series(self):
415475
# GH#58621
416476
df = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})

pandas/tests/series/methods/test_rename.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ def test_rename_series_with_multiindex_keeps_ea_dtypes(self):
162162

163163
tm.assert_series_equal(result, expected)
164164

165+
def test_rename_preserves_nullable_index_dtype(self):
166+
# GH#65315
167+
ser = Series(
168+
[1, 2, 3],
169+
index=Index(array([1, 2, 3], dtype="Int64"), name="id"),
170+
)
171+
result = ser.rename({1: 9})
172+
expected = Index(array([9, 2, 3], dtype="Int64"), name="id")
173+
tm.assert_index_equal(result.index, expected)
174+
165175
def test_rename_error_arg(self):
166176
# GH 46889
167177
ser = Series(["foo", "bar"])

0 commit comments

Comments
 (0)