From c9e5e81760a796bee0181e90df89a9ffbd8c2c7e Mon Sep 17 00:00:00 2001 From: phofl Date: Thu, 12 Nov 2020 00:41:35 +0100 Subject: [PATCH 1/6] [BUG]: DataFrame.xs ignored droplevel for columns --- doc/source/whatsnew/v1.2.0.rst | 1 + pandas/core/generic.py | 4 +++- pandas/tests/frame/indexing/test_xs.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index f751a91cecf19..ef7869aa62a2c 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -483,6 +483,7 @@ MultiIndex - Bug in :meth:`DataFrame.xs` when used with :class:`IndexSlice` raises ``TypeError`` with message ``"Expected label or tuple of labels"`` (:issue:`35301`) - Bug in :meth:`DataFrame.reset_index` with ``NaT`` values in index raises ``ValueError`` with message ``"cannot convert float NaN to integer"`` (:issue:`36541`) - Bug in :meth:`DataFrame.combine_first` when used with :class:`MultiIndex` containing string and ``NaN`` values raises ``TypeError`` (:issue:`36562`) +- Bug in :meth:`DataFrame.xs` ignored ``droplevel=False`` for columnsm (:issue:`19056`) I/O ^^^ diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 24c1ae971686e..d41fc12ceb62e 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3708,7 +3708,9 @@ class animal locomotion return result if axis == 1: - return self[key] + if drop_level: + return self[key] + return self[[key]] index = self.index if isinstance(index, MultiIndex): diff --git a/pandas/tests/frame/indexing/test_xs.py b/pandas/tests/frame/indexing/test_xs.py index 11e076f313540..410b183d80062 100644 --- a/pandas/tests/frame/indexing/test_xs.py +++ b/pandas/tests/frame/indexing/test_xs.py @@ -297,3 +297,18 @@ def test_xs_levels_raises(self, klass): msg = "Index must be a MultiIndex" with pytest.raises(TypeError, match=msg): obj.xs(0, level="as") + + def test_xs_droplevel_false(self): + # GH: 19056 + mi = MultiIndex.from_tuples( + [("a", "x"), ("a", "y"), ("b", "x")], names=["level1", "level2"] + ) + df = DataFrame([[1, 2, 3]], columns=mi) + result = df.xs("a", axis=1, drop_level=False) + expected = DataFrame( + [[1, 2]], + columns=MultiIndex.from_tuples( + [("a", "x"), ("a", "y")], names=["level1", "level2"] + ), + ) + tm.assert_frame_equal(result, expected) From 3205e8de862001f0df1e2b2f6fcd9f951f2e618a Mon Sep 17 00:00:00 2001 From: phofl Date: Thu, 12 Nov 2020 00:42:50 +0100 Subject: [PATCH 2/6] Move whatsnew --- doc/source/whatsnew/v1.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index ef7869aa62a2c..b7378524c77bc 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -470,6 +470,7 @@ Indexing - Bug in :meth:`Index.where` incorrectly casting numeric values to strings (:issue:`37591`) - Bug in :meth:`Series.loc` and :meth:`DataFrame.loc` raises when numeric label was given for object :class:`Index` although label was in :class:`Index` (:issue:`26491`) - Bug in :meth:`DataFrame.loc` returned requested key plus missing values when ``loc`` was applied to single level from :class:`MultiIndex` (:issue:`27104`) +- Bug in :meth:`DataFrame.xs` ignored ``droplevel=False`` for columnsm (:issue:`19056`) Missing ^^^^^^^ @@ -483,7 +484,6 @@ MultiIndex - Bug in :meth:`DataFrame.xs` when used with :class:`IndexSlice` raises ``TypeError`` with message ``"Expected label or tuple of labels"`` (:issue:`35301`) - Bug in :meth:`DataFrame.reset_index` with ``NaT`` values in index raises ``ValueError`` with message ``"cannot convert float NaN to integer"`` (:issue:`36541`) - Bug in :meth:`DataFrame.combine_first` when used with :class:`MultiIndex` containing string and ``NaN`` values raises ``TypeError`` (:issue:`36562`) -- Bug in :meth:`DataFrame.xs` ignored ``droplevel=False`` for columnsm (:issue:`19056`) I/O ^^^ From 6614f5e827c52b686a67ecdfda8a82006ce230eb Mon Sep 17 00:00:00 2001 From: phofl Date: Thu, 12 Nov 2020 01:02:07 +0100 Subject: [PATCH 3/6] Fix typo --- doc/source/whatsnew/v1.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.2.0.rst b/doc/source/whatsnew/v1.2.0.rst index b7378524c77bc..5284569d8f642 100644 --- a/doc/source/whatsnew/v1.2.0.rst +++ b/doc/source/whatsnew/v1.2.0.rst @@ -470,7 +470,7 @@ Indexing - Bug in :meth:`Index.where` incorrectly casting numeric values to strings (:issue:`37591`) - Bug in :meth:`Series.loc` and :meth:`DataFrame.loc` raises when numeric label was given for object :class:`Index` although label was in :class:`Index` (:issue:`26491`) - Bug in :meth:`DataFrame.loc` returned requested key plus missing values when ``loc`` was applied to single level from :class:`MultiIndex` (:issue:`27104`) -- Bug in :meth:`DataFrame.xs` ignored ``droplevel=False`` for columnsm (:issue:`19056`) +- Bug in :meth:`DataFrame.xs` ignored ``droplevel=False`` for columns (:issue:`19056`) Missing ^^^^^^^ From f3d54f78e68640111aca7af329313ee047d7ff08 Mon Sep 17 00:00:00 2001 From: phofl Date: Thu, 12 Nov 2020 01:22:48 +0100 Subject: [PATCH 4/6] Add test --- pandas/tests/series/indexing/test_xs.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pandas/tests/series/indexing/test_xs.py b/pandas/tests/series/indexing/test_xs.py index 1a23b09bde816..ca7ed50ab8875 100644 --- a/pandas/tests/series/indexing/test_xs.py +++ b/pandas/tests/series/indexing/test_xs.py @@ -50,3 +50,18 @@ def test_series_getitem_multiindex_xs(xs): result = ser.xs("20130903", level=1) tm.assert_series_equal(result, expected) + + def test_series_xs_droplevel_false(self): + # GH: 19056 + mi = MultiIndex.from_tuples( + [("a", "x"), ("a", "y"), ("b", "x")], names=["level1", "level2"] + ) + df = Series([1, 1, 1], index=mi) + result = df.xs("a", axis=0, drop_level=False) + expected = Series( + [1, 1], + index=MultiIndex.from_tuples( + [("a", "x"), ("a", "y")], names=["level1", "level2"] + ), + ) + tm.assert_series_equal(result, expected) From 5c5eee1b24cac27eb8a5ce433787d734199d845b Mon Sep 17 00:00:00 2001 From: phofl Date: Thu, 12 Nov 2020 20:55:05 +0100 Subject: [PATCH 5/6] Refactor code --- pandas/core/generic.py | 22 ++++++++++++++-------- pandas/tests/frame/indexing/test_xs.py | 11 +++++++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index d41fc12ceb62e..4745cfce50322 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3710,18 +3710,19 @@ class animal locomotion if axis == 1: if drop_level: return self[key] - return self[[key]] + index = self.columns + else: + index = self.index - index = self.index if isinstance(index, MultiIndex): try: - loc, new_index = self.index._get_loc_level( + loc, new_index = index._get_loc_level( key, level=0, drop_level=drop_level ) except TypeError as e: raise TypeError(f"Expected label or tuple of labels, got {key}") from e else: - loc = self.index.get_loc(key) + loc = index.get_loc(key) if isinstance(loc, np.ndarray): if loc.dtype == np.bool_: @@ -3731,9 +3732,9 @@ class animal locomotion return self._take_with_is_copy(loc, axis=axis) if not is_scalar(loc): - new_index = self.index[loc] + new_index = index[loc] - if is_scalar(loc): + if is_scalar(loc) and axis == 0: # In this case loc should be an integer if self.ndim == 1: # if we encounter an array-like and we only have 1 dim @@ -3751,8 +3752,13 @@ class animal locomotion ) else: - result = self.iloc[loc] - result.index = new_index + if axis == 0: + result = self.iloc[loc] + result.index = new_index + else: + result = ( + self.iloc[:, loc] if not is_scalar(loc) else self.iloc[:, [loc]] + ) # this could be a view # but only in a single-dtyped view sliceable case diff --git a/pandas/tests/frame/indexing/test_xs.py b/pandas/tests/frame/indexing/test_xs.py index 410b183d80062..a90141e9fad60 100644 --- a/pandas/tests/frame/indexing/test_xs.py +++ b/pandas/tests/frame/indexing/test_xs.py @@ -298,8 +298,8 @@ def test_xs_levels_raises(self, klass): with pytest.raises(TypeError, match=msg): obj.xs(0, level="as") - def test_xs_droplevel_false(self): - # GH: 19056 + def test_xs_multiindex_droplevel_false(self): + # GH#19056 mi = MultiIndex.from_tuples( [("a", "x"), ("a", "y"), ("b", "x")], names=["level1", "level2"] ) @@ -312,3 +312,10 @@ def test_xs_droplevel_false(self): ), ) tm.assert_frame_equal(result, expected) + + def test_xs_droplevel_false(self): + # GH#19056 + df = DataFrame([[1, 2, 3]], columns=Index(["a", "b", "c"])) + result = df.xs("a", axis=1, drop_level=False) + expected = DataFrame({"a": [1]}) + tm.assert_frame_equal(result, expected) From 0f0bb7754c0f6a59942072be32e9154d31dfcd0e Mon Sep 17 00:00:00 2001 From: phofl Date: Fri, 13 Nov 2020 11:45:14 +0100 Subject: [PATCH 6/6] Modify if else condition --- pandas/core/generic.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 4745cfce50322..72978dd842918 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3750,15 +3750,13 @@ class animal locomotion name=self.index[loc], dtype=new_values.dtype, ) - + elif is_scalar(loc): + result = self.iloc[:, [loc]] + elif axis == 1: + result = self.iloc[:, loc] else: - if axis == 0: - result = self.iloc[loc] - result.index = new_index - else: - result = ( - self.iloc[:, loc] if not is_scalar(loc) else self.iloc[:, [loc]] - ) + result = self.iloc[loc] + result.index = new_index # this could be a view # but only in a single-dtyped view sliceable case