diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 495669d316e95..24ba8a1272eec 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -642,6 +642,7 @@ Other - Bug in :meth:`CustomBusinessMonthBegin.__add__` (:meth:`CustomBusinessMonthEnd.__add__`) not applying the extra ``offset`` parameter when beginning (end) of the target month is already a business day (:issue:`41356`) - Bug in :meth:`RangeIndex.union` with another ``RangeIndex`` with matching (even) ``step`` and starts differing by strictly less than ``step / 2`` (:issue:`44019`) - Bug in :meth:`RangeIndex.difference` with ``sort=None`` and ``step<0`` failing to sort (:issue:`44085`) +- Bug in :meth:`Series.to_frame` and :meth:`Index.to_frame` ignoring the ``name`` argument when ``name=None`` is explicitly passed (:issue:`44212`) .. ***DO NOT USE THIS SECTION*** diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index e82bd61938f15..d44a25c2677d1 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1510,7 +1510,9 @@ def to_series(self, index=None, name: Hashable = None) -> Series: return Series(self._values.copy(), index=index, name=name) - def to_frame(self, index: bool = True, name: Hashable = None) -> DataFrame: + def to_frame( + self, index: bool = True, name: Hashable = lib.no_default + ) -> DataFrame: """ Create a DataFrame with a column containing the Index. @@ -1561,7 +1563,7 @@ def to_frame(self, index: bool = True, name: Hashable = None) -> DataFrame: """ from pandas import DataFrame - if name is None: + if name is lib.no_default: name = self.name or 0 result = DataFrame({name: self._values.copy()}) diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index e2f1a2d6a1e23..156488ca08102 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -1684,7 +1684,7 @@ def unique(self, level=None): level = self._get_level_number(level) return self._get_level_values(level=level, unique=True) - def to_frame(self, index: bool = True, name=None) -> DataFrame: + def to_frame(self, index: bool = True, name=lib.no_default) -> DataFrame: """ Create a DataFrame with the levels of the MultiIndex as columns. @@ -1736,7 +1736,7 @@ def to_frame(self, index: bool = True, name=None) -> DataFrame: """ from pandas import DataFrame - if name is not None: + if name is not lib.no_default: if not is_list_like(name): raise TypeError("'name' must be a list / sequence of column names.") diff --git a/pandas/core/series.py b/pandas/core/series.py index 15715af05f904..2c6d4ed445394 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -1317,7 +1317,7 @@ def repeat(self, repeats, axis=None) -> Series: ) @deprecate_nonkeyword_arguments(version=None, allowed_args=["self", "level"]) - def reset_index(self, level=None, drop=False, name=None, inplace=False): + def reset_index(self, level=None, drop=False, name=lib.no_default, inplace=False): """ Generate a new DataFrame or Series with the index reset. @@ -1427,6 +1427,9 @@ def reset_index(self, level=None, drop=False, name=None, inplace=False): """ inplace = validate_bool_kwarg(inplace, "inplace") if drop: + if name is lib.no_default: + name = self.name + new_index = default_index(len(self)) if level is not None: if not isinstance(level, (tuple, list)): @@ -1448,6 +1451,14 @@ def reset_index(self, level=None, drop=False, name=None, inplace=False): "Cannot reset_index inplace on a Series to create a DataFrame" ) else: + if name is lib.no_default: + # For backwards compatibility, keep columns as [0] instead of + # [None] when self.name is None + if self.name is None: + name = 0 + else: + name = self.name + df = self.to_frame(name) return df.reset_index(level=level, drop=drop) @@ -1697,7 +1708,7 @@ def to_dict(self, into=dict): into_c = com.standardize_mapping(into) return into_c((k, maybe_box_native(v)) for k, v in self.items()) - def to_frame(self, name=None) -> DataFrame: + def to_frame(self, name: Hashable = lib.no_default) -> DataFrame: """ Convert Series to DataFrame. @@ -1723,7 +1734,7 @@ def to_frame(self, name=None) -> DataFrame: 2 c """ columns: Index - if name is None: + if name is lib.no_default: name = self.name if name is None: # default to [0], same as we would get with DataFrame(self) diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 061e36e457443..ba47391513ed2 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -460,7 +460,11 @@ def _compute_plot_data(self): label = self.label if label is None and data.name is None: label = "None" - data = data.to_frame(name=label) + if label is None: + # We'll end up with columns of [0] instead of [None] + data = data.to_frame() + else: + data = data.to_frame(name=label) elif self._kind in ("hist", "box"): cols = self.columns if self.by is None else self.columns + self.by data = data.loc[:, cols] diff --git a/pandas/tests/indexes/datetimes/methods/test_to_frame.py b/pandas/tests/indexes/datetimes/methods/test_to_frame.py index ec6254f52f4d5..80e8284abe031 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_frame.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_frame.py @@ -1,5 +1,6 @@ from pandas import ( DataFrame, + Index, date_range, ) import pandas._testing as tm @@ -12,3 +13,14 @@ def test_to_frame_datetime_tz(self): result = idx.to_frame() expected = DataFrame(idx, index=idx) tm.assert_frame_equal(result, expected) + + def test_to_frame_respects_none_name(self): + # GH#44212 if we explicitly pass name=None, then that should be respected, + # not changed to 0 + idx = date_range(start="2019-01-01", end="2019-01-30", freq="D", tz="UTC") + result = idx.to_frame(name=None) + exp_idx = Index([None], dtype=object) + tm.assert_index_equal(exp_idx, result.columns) + + result = idx.rename("foo").to_frame(name=None) + tm.assert_index_equal(exp_idx, result.columns) diff --git a/pandas/tests/series/methods/test_to_frame.py b/pandas/tests/series/methods/test_to_frame.py index 66e44f1a0caf0..55d49b8fbee70 100644 --- a/pandas/tests/series/methods/test_to_frame.py +++ b/pandas/tests/series/methods/test_to_frame.py @@ -1,11 +1,24 @@ from pandas import ( DataFrame, + Index, Series, ) import pandas._testing as tm class TestToFrame: + def test_to_frame_respects_name_none(self): + # GH#44212 if we explicitly pass name=None, then that should be respected, + # not changed to 0 + ser = Series(range(3)) + result = ser.to_frame(None) + + exp_index = Index([None], dtype=object) + tm.assert_index_equal(result.columns, exp_index) + + result = ser.rename("foo").to_frame(None) + tm.assert_index_equal(result.columns, exp_index) + def test_to_frame(self, datetime_series): datetime_series.name = None rs = datetime_series.to_frame()