From 540471b5ceca6f54cd2fd072a01a6ec69bb219ed Mon Sep 17 00:00:00 2001 From: Fabio Leimgruber Date: Sat, 24 Feb 2018 14:33:14 +0100 Subject: [PATCH 1/4] Include MultiIndex slice in non-reducing slices Changes behaviour of user-passed IndexSlice to return DataFrame instead of reducing to Series. MultiIndex slices are tuples so this explicitly checks type and guards with parentheses. Fixes #19861 --- pandas/core/indexing.py | 3 +++ pandas/tests/indexing/test_indexing.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index ab4ad693a462e..9e6129edec2ea 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2733,6 +2733,9 @@ def _non_reducing_slice(slice_): def pred(part): # true when slice does *not* reduce + # False when part is a tuple, i.e. MultiIndex slice + if isinstance(part, tuple): + return False return isinstance(part, slice) or is_list_like(part) if not is_list_like(slice_): diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 03f1975c50d2a..090947a473900 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -812,6 +812,21 @@ def test_non_reducing_slice(self): tslice_ = _non_reducing_slice(slice_) assert isinstance(df.loc[tslice_], DataFrame) + def test_non_reducing_slice_on_multiindex(self): + dic = { + ('a', 'd'): [1, 4], + ('a', 'c'): [2, 3], + ('b', 'c'): [3, 2], + ('b', 'd'): [4, 1] + } + + df = pd.DataFrame(dic, index=[0, 1]) + idx = pd.IndexSlice + + slice_ = idx[:, idx['b', 'd']] + tslice_ = _non_reducing_slice(slice_) + assert isinstance(df.loc[tslice_], DataFrame) + def test_list_slice(self): # like dataframe getitem slices = [['A'], Series(['A']), np.array(['A'])] From f00fa73f7cd8768acc34392a1d26cea808145360 Mon Sep 17 00:00:00 2001 From: Fabio Leimgruber Date: Sat, 24 Feb 2018 18:46:30 +0100 Subject: [PATCH 2/4] Use tm.assert_frame_equal and smoke test from GH issue --- pandas/core/indexing.py | 4 ++-- pandas/tests/indexing/test_indexing.py | 7 ++++--- pandas/tests/io/formats/test_style.py | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 9e6129edec2ea..724bcc0e9c637 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2732,8 +2732,8 @@ def _non_reducing_slice(slice_): slice_ = IndexSlice[:, slice_] def pred(part): - # true when slice does *not* reduce - # False when part is a tuple, i.e. MultiIndex slice + # true when slice does *not* reduce False when part is a tuple, + # i.e. MultiIndex slice if isinstance(part, tuple): return False return isinstance(part, slice) or is_list_like(part) diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 090947a473900..3dc768943b34f 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -819,13 +819,14 @@ def test_non_reducing_slice_on_multiindex(self): ('b', 'c'): [3, 2], ('b', 'd'): [4, 1] } - df = pd.DataFrame(dic, index=[0, 1]) idx = pd.IndexSlice - slice_ = idx[:, idx['b', 'd']] tslice_ = _non_reducing_slice(slice_) - assert isinstance(df.loc[tslice_], DataFrame) + + result = df.loc[tslice_] + expected = pd.DataFrame({('b', 'd'): [4, 1]}) + tm.assert_frame_equal(result, expected) def test_list_slice(self): # like dataframe getitem diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index fa8bd91dce939..77a69eef8b140 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -274,6 +274,31 @@ def f(x): col in self.df.loc[slice_].columns} assert result == expected + def test_applymap_subset_multiindex(self): + # https://github.com/pandas-dev/pandas/issues/19861 + def color_negative_red(val): + """ + Takes a scalar and returns a string with + the css property `'color: red'` for negative + strings, black otherwise. + """ + color = 'red' if val < 0 else 'black' + return 'color: %s' % color + + dic = { + ('a', 'd'): [-1.12, 2.11], + ('a', 'c'): [2.78, -2.88], + ('b', 'c'): [-3.99, 3.77], + ('b', 'd'): [4.21, -1.22], + } + + idx = pd.IndexSlice + df = pd.DataFrame(dic, index=[0, 1]) + + (df.style + .applymap(color_negative_red, subset=idx[:, idx['b', 'd']]) + .render()) + def test_where_with_one_style(self): # GH 17474 def f(x): From 0885c79277273a676684c18daa2d42b94cdb16fb Mon Sep 17 00:00:00 2001 From: Fabio Leimgruber Date: Sat, 24 Feb 2018 19:44:24 +0100 Subject: [PATCH 3/4] whatsnew and changes requested --- doc/source/whatsnew/v0.24.0.rst | 1 + pandas/core/indexing.py | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.rst b/doc/source/whatsnew/v0.24.0.rst index 1fe5e4e6e7087..da1ea62e6f49a 100644 --- a/doc/source/whatsnew/v0.24.0.rst +++ b/doc/source/whatsnew/v0.24.0.rst @@ -1538,6 +1538,7 @@ Missing MultiIndex ^^^^^^^^^^ +- Bug in :func:`io.formats.style.Styler.applymap` where ``subset=`` with :class:`MultiIndex` slice would reduce to :class:`Series` (:issue:`19861`) - Removed compatibility for :class:`MultiIndex` pickles prior to version 0.8.0; compatibility with :class:`MultiIndex` pickles from version 0.13 forward is maintained (:issue:`21654`) - :meth:`MultiIndex.get_loc_level` (and as a consequence, ``.loc`` on a ``Series`` or ``DataFrame`` with a :class:`MultiIndex` index) will now raise a ``KeyError``, rather than returning an empty ``slice``, if asked a label which is present in the ``levels`` but is unused (:issue:`22221`) - :class:`MultiIndex` has gained the :meth:`MultiIndex.from_frame`, it allows constructing a :class:`MultiIndex` object from a :class:`DataFrame` (:issue:`22420`) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 724bcc0e9c637..77db21bcab73c 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2732,11 +2732,10 @@ def _non_reducing_slice(slice_): slice_ = IndexSlice[:, slice_] def pred(part): - # true when slice does *not* reduce False when part is a tuple, + # true when slice does *not* reduce, False when part is a tuple, # i.e. MultiIndex slice - if isinstance(part, tuple): - return False - return isinstance(part, slice) or is_list_like(part) + return (isinstance(part, slice) or is_list_like(part))\ + and not isinstance(part, tuple) if not is_list_like(slice_): if not isinstance(slice_, slice): From a00dd9e37339a0741a5d9ebc60420d7fb6a04542 Mon Sep 17 00:00:00 2001 From: Fabio Leimgruber Date: Tue, 1 Jan 2019 23:19:44 +0100 Subject: [PATCH 4/4] Update comments and parenthesis for if condition --- pandas/core/indexing.py | 4 ++-- pandas/tests/indexing/test_indexing.py | 1 + pandas/tests/io/formats/test_style.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 77db21bcab73c..3504c6e12b896 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2734,8 +2734,8 @@ def _non_reducing_slice(slice_): def pred(part): # true when slice does *not* reduce, False when part is a tuple, # i.e. MultiIndex slice - return (isinstance(part, slice) or is_list_like(part))\ - and not isinstance(part, tuple) + return ((isinstance(part, slice) or is_list_like(part)) + and not isinstance(part, tuple)) if not is_list_like(slice_): if not isinstance(slice_, slice): diff --git a/pandas/tests/indexing/test_indexing.py b/pandas/tests/indexing/test_indexing.py index 3dc768943b34f..2224c3ab9935a 100644 --- a/pandas/tests/indexing/test_indexing.py +++ b/pandas/tests/indexing/test_indexing.py @@ -813,6 +813,7 @@ def test_non_reducing_slice(self): assert isinstance(df.loc[tslice_], DataFrame) def test_non_reducing_slice_on_multiindex(self): + # GH 19861 dic = { ('a', 'd'): [1, 4], ('a', 'c'): [2, 3], diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 77a69eef8b140..3432d686a9fd6 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -275,7 +275,8 @@ def f(x): assert result == expected def test_applymap_subset_multiindex(self): - # https://github.com/pandas-dev/pandas/issues/19861 + # GH 19861 + # Smoke test for applymap def color_negative_red(val): """ Takes a scalar and returns a string with