From 0fd4ffad9b5a6f5e4779fd9127f90ddfec150efb Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 27 Oct 2021 16:22:20 -0700 Subject: [PATCH 1/4] BUG: Index/Series.to_frame not respecting explicit name=None --- doc/source/whatsnew/v1.4.0.rst | 1 + pandas/core/indexes/base.py | 6 ++++-- pandas/core/indexes/multi.py | 4 ++-- pandas/core/series.py | 4 ++-- .../indexes/datetimes/methods/test_to_frame.py | 12 ++++++++++++ pandas/tests/series/methods/test_to_frame.py | 13 +++++++++++++ 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index fc3eaec47431f..5e48a39e64cee 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:`??`) .. ***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 9795e1f7141ee..7a27d98736d7a 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -1720,7 +1720,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. @@ -1746,7 +1746,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/tests/indexes/datetimes/methods/test_to_frame.py b/pandas/tests/indexes/datetimes/methods/test_to_frame.py index ec6254f52f4d5..0561d6ce4c6cb 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): + # 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..830852f40bda5 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): + # 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() From 93805bae230822e971d8221d8c2d5c8e7aa7afc7 Mon Sep 17 00:00:00 2001 From: Brock Date: Wed, 27 Oct 2021 16:23:47 -0700 Subject: [PATCH 2/4] GH ref --- doc/source/whatsnew/v1.4.0.rst | 2 +- pandas/tests/indexes/datetimes/methods/test_to_frame.py | 2 +- pandas/tests/series/methods/test_to_frame.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 5e48a39e64cee..91519de6adefd 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -642,7 +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:`??`) +- 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/tests/indexes/datetimes/methods/test_to_frame.py b/pandas/tests/indexes/datetimes/methods/test_to_frame.py index 0561d6ce4c6cb..80e8284abe031 100644 --- a/pandas/tests/indexes/datetimes/methods/test_to_frame.py +++ b/pandas/tests/indexes/datetimes/methods/test_to_frame.py @@ -15,7 +15,7 @@ def test_to_frame_datetime_tz(self): tm.assert_frame_equal(result, expected) def test_to_frame_respects_none_name(self): - # if we explicitly pass name=None, then that should be respected, + # 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) diff --git a/pandas/tests/series/methods/test_to_frame.py b/pandas/tests/series/methods/test_to_frame.py index 830852f40bda5..55d49b8fbee70 100644 --- a/pandas/tests/series/methods/test_to_frame.py +++ b/pandas/tests/series/methods/test_to_frame.py @@ -8,7 +8,7 @@ class TestToFrame: def test_to_frame_respects_name_none(self): - # if we explicitly pass name=None, then that should be respected, + # 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) From 0f21b2d935c8db9efbac49583c7e6bcb71995052 Mon Sep 17 00:00:00 2001 From: Brock Date: Thu, 28 Oct 2021 10:41:30 -0700 Subject: [PATCH 3/4] fix tests --- pandas/core/series.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pandas/core/series.py b/pandas/core/series.py index 2b9c99fcf383b..aeebb554a5417 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) From a4e692a173a9988943391d0b0c7ef111b79481b1 Mon Sep 17 00:00:00 2001 From: Brock Date: Fri, 29 Oct 2021 20:06:38 -0700 Subject: [PATCH 4/4] fix plotting tests --- pandas/plotting/_matplotlib/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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]