From ac19894fec264b80cf040205b25c87b68bbe371f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 25 Apr 2021 13:26:03 +0200 Subject: [PATCH 1/6] initial hide_values method --- pandas/io/formats/style.py | 36 +++++++++++++++++++++++++++++++ pandas/io/formats/style_render.py | 5 +++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ff25bb1411189..6f16219e690ec 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1011,6 +1011,42 @@ def hide_columns(self, subset) -> Styler: self.hidden_columns = hcols # type: ignore[assignment] return self + def hide_values(self, subset, show: bool = False) -> Styler: + """ + Hide (or explicitly show) rows and/or columns from rendering. + + Parameters + ---------- + subset : IndexSlice + An argument to ``DataFrame.loc`` that identifies which rows and/or columns + are hidden. + show : bool + Indicates whether the supplied subset should be hidden, or explicitly shown. + + Returns + ------- + self : Styler + """ + subset = non_reducing_slice(subset) + data = self.data.loc[subset] + if show: + # QUESTION FOR REVIEWER: is there a better way to invert an indexer??? + # then invert the hidden elements + hidden_df = self.data.loc[ + ~self.data.index.isin(data.index.to_list()), + ~self.data.columns.isin(data.columns.to_list()), + ] + else: + hidden_df = data + + hcols = self.columns.get_indexer_for(hidden_df.columns) + hrows = self.index.get_indexer_for(hidden_df.index) + # error: Incompatible types in assignment (expression has type + # "ndarray", variable has type "Sequence[int]") + self.hidden_columns = hcols # type: ignore[assignment] + self.hidden_rows = hrows # type: ignore[assignment] + return self + # ----------------------------------------------------------------------- # A collection of "builtin" styles # ----------------------------------------------------------------------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 15557c993eab4..071fef9180909 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -93,6 +93,7 @@ def __init__( # add rendering variables self.hidden_index: bool = False self.hidden_columns: Sequence[int] = [] + self.hidden_rows: Sequence[int] = [] self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str) self._todo: list[tuple[Callable, tuple, dict]] = [] @@ -307,7 +308,7 @@ def _translate_body(self, data_class, row_heading_class): block """ # for sparsifying a MultiIndex - idx_lengths = _get_level_lengths(self.index) + idx_lengths = _get_level_lengths(self.index, self.hidden_rows) rlabels = self.data.index.tolist() if self.data.index.nlevels == 1: @@ -342,7 +343,7 @@ def _translate_body(self, data_class, row_heading_class): "td", f"{data_class} row{r} col{c}{cls}", value, - (c not in self.hidden_columns), + (c not in self.hidden_columns and r not in self.hidden_rows), attributes="", display_value=self._display_funcs[(r, c)](value), ) From 7af2849b92c81bd455322bc21649b3e1328f8706 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 25 Apr 2021 17:26:54 +0200 Subject: [PATCH 2/6] initial hide_values method --- pandas/io/formats/style.py | 53 ++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6f16219e690ec..da14ada067def 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -28,6 +28,7 @@ from pandas.util._decorators import doc import pandas as pd +from pandas import IndexSlice from pandas.api.types import is_list_like from pandas.core import generic import pandas.core.common as com @@ -602,7 +603,7 @@ def apply( def _applymap(self, func: Callable, subset=None, **kwargs) -> Styler: func = partial(func, **kwargs) # applymap doesn't take kwargs? if subset is None: - subset = pd.IndexSlice[:] + subset = IndexSlice[:] subset = non_reducing_slice(subset) result = self.data.loc[subset].applymap(func) self._update_ctx(result) @@ -1011,15 +1012,17 @@ def hide_columns(self, subset) -> Styler: self.hidden_columns = hcols # type: ignore[assignment] return self - def hide_values(self, subset, show: bool = False) -> Styler: + def hide_values(self, subset, axis: Axis = "columns", show: bool = False) -> Styler: """ - Hide (or explicitly show) rows and/or columns from rendering. + Hide (or explicitly show) columns or rows upon rendering. Parameters ---------- subset : IndexSlice - An argument to ``DataFrame.loc`` that identifies which rows and/or columns - are hidden. + An valid input to a specific ``axis`` in ``DataFrame.loc`` that identifies + which columns or rows are hidden/shown. + axis : {0 or 'index', 1 or 'columns'} + Axis along which the ``subset`` is applied. show : bool Indicates whether the supplied subset should be hidden, or explicitly shown. @@ -1027,24 +1030,30 @@ def hide_values(self, subset, show: bool = False) -> Styler: ------- self : Styler """ - subset = non_reducing_slice(subset) - data = self.data.loc[subset] - if show: - # QUESTION FOR REVIEWER: is there a better way to invert an indexer??? - # then invert the hidden elements - hidden_df = self.data.loc[ - ~self.data.index.isin(data.index.to_list()), - ~self.data.columns.isin(data.columns.to_list()), - ] + if axis in [0, "index"]: + subset = IndexSlice[subset, :] + subset = non_reducing_slice(subset) + hide = self.data.loc[subset] + if show: # invert the display + hide = self.data.loc[~self.data.index.isin(hide.index.to_list()), :] + hrows = self.index.get_indexer_for(hide.index) + # error: Incompatible types in assignment (expression has type + # "ndarray", variable has type "Sequence[int]") + self.hidden_rows = hrows # type: ignore[assignment] + elif axis in [1, "columns"]: + subset = IndexSlice[:, subset] + subset = non_reducing_slice(subset) + hide = self.data.loc[subset] + if show: # invert the display + hide = self.data.loc[:, ~self.data.index.isin(hide.columns.to_list())] + hcols = self.columns.get_indexer_for(hide.columns) + # error: Incompatible types in assignment (expression has type + # "ndarray", variable has type "Sequence[int]") + self.hidden_columns = hcols # type: ignore[assignment] else: - hidden_df = data - - hcols = self.columns.get_indexer_for(hidden_df.columns) - hrows = self.index.get_indexer_for(hidden_df.index) - # error: Incompatible types in assignment (expression has type - # "ndarray", variable has type "Sequence[int]") - self.hidden_columns = hcols # type: ignore[assignment] - self.hidden_rows = hrows # type: ignore[assignment] + raise ValueError( + f"`axis` must be one of [0, 1] or 'index' or 'columns', got: {axis}" + ) return self # ----------------------------------------------------------------------- From 3ec679f4c1c94ba55a9dd5440f68f88c7daaa2c1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 25 Apr 2021 19:56:12 +0200 Subject: [PATCH 3/6] add tests --- pandas/io/formats/style.py | 8 +-- pandas/tests/io/formats/style/test_style.py | 76 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index da14ada067def..800d90b173595 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1004,13 +1004,7 @@ def hide_columns(self, subset) -> Styler: ------- self : Styler """ - subset = non_reducing_slice(subset) - hidden_df = self.data.loc[subset] - hcols = self.columns.get_indexer_for(hidden_df.columns) - # error: Incompatible types in assignment (expression has type - # "ndarray", variable has type "Sequence[int]") - self.hidden_columns = hcols # type: ignore[assignment] - return self + return self.hide_values(subset) def hide_values(self, subset, axis: Axis = "columns", show: bool = False) -> Styler: """ diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index eadb90839c74d..8f863c7af6ef1 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1419,6 +1419,82 @@ def test_non_reducing_multi_slice_on_multiindex(self, slice_): result = df.loc[non_reducing_slice(slice_)] tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize( + "subset", + [ + pd.Series(["i1", "i2"]), + np.array(["i1", "i2"]), + pd.Index(["i1", "i2"]), + ["i1", "i2"], + pd.IndexSlice["i1":"i2"], + ], + ) + def test_hide_values_index(self, subset): + df = DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=["i1", "i2", "i3"], + columns=["c1", "c2", "c3"], + ) + styler = Styler(df, uuid_len=0, cell_ids=False) + styler.hide_values(subset=subset, axis="index") + result = styler.render() + + assert ( + 'i1' + not in result + ) + assert ( + 'i2' + not in result + ) + assert ( + 'i3' in result + ) + + assert '1' not in result + assert '2' not in result + assert '3' not in result + assert '4' not in result + assert '5' not in result + assert '6' not in result + assert '7' in result + assert '8' in result + assert '9' in result + + @pytest.mark.parametrize( + "subset", + [ + pd.Series(["c1", "c2"]), + np.array(["c1", "c2"]), + pd.Index(["c1", "c2"]), + ["c1", "c2"], + pd.IndexSlice["c1":"c2"], + ], + ) + def test_hide_values_columns(self, subset): + df = DataFrame( + [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=["i1", "i2", "i3"], + columns=["c1", "c2", "c3"], + ) + styler = Styler(df, uuid_len=0, cell_ids=False) + styler.hide_values(subset=subset, axis="columns") + result = styler.render() + + assert 'c1' not in result + assert 'c2' not in result + assert 'c3' in result + + assert '1' not in result + assert '2' not in result + assert '3' in result + assert '4' not in result + assert '5' not in result + assert '6' in result + assert '7' not in result + assert '8' not in result + assert '9' in result + def test_block_names(): # catch accidental removal of a block From 8bdb867e8e8603d3049495be5830e8a81f0b2318 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 26 Apr 2021 07:59:11 +0200 Subject: [PATCH 4/6] add tests --- pandas/io/formats/style.py | 2 +- pandas/tests/io/formats/style/test_style.py | 41 +++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 800d90b173595..f2b641b0d0327 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1039,7 +1039,7 @@ def hide_values(self, subset, axis: Axis = "columns", show: bool = False) -> Sty subset = non_reducing_slice(subset) hide = self.data.loc[subset] if show: # invert the display - hide = self.data.loc[:, ~self.data.index.isin(hide.columns.to_list())] + hide = self.data.loc[:, ~self.data.columns.isin(hide.columns.to_list())] hcols = self.columns.get_indexer_for(hide.columns) # error: Incompatible types in assignment (expression has type # "ndarray", variable has type "Sequence[int]") diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 8f863c7af6ef1..27dc3fd071a81 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1457,6 +1457,7 @@ def test_hide_values_index(self, subset): assert '4' not in result assert '5' not in result assert '6' not in result + assert '7' in result assert '8' in result assert '9' in result @@ -1485,15 +1486,49 @@ def test_hide_values_columns(self, subset): assert 'c2' not in result assert 'c3' in result + assert '3' in result + assert '6' in result + assert '9' in result + assert '1' not in result assert '2' not in result - assert '3' in result assert '4' not in result assert '5' not in result - assert '6' in result assert '7' not in result assert '8' not in result - assert '9' in result + + def test_hide_values_multiindex(self): + idx = pd.MultiIndex.from_product([["i1", "i2"], ["j1", "j2"]]) + col = pd.MultiIndex.from_product([["c1", "c2"], ["d1", "d2"]]) + df = DataFrame(np.arange(16).reshape((4, 4)), columns=col, index=idx) + + # test hide + styler = ( + Styler(df, uuid_len=0, cell_ids=False) + .hide_values(subset=(slice(None), "j1"), axis="index") + .hide_values(subset="c1", axis="columns") + ) + result = styler.render() + for header in [">c1<", ">j1<"]: + assert header not in result + for data in [0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13]: + assert f">{data}<" not in result + for data in [6, 7, 14, 15]: + assert f">{data}<" in result + + # test show + styler = ( + Styler(df, uuid_len=0, cell_ids=False) + .hide_values(subset=(slice(None), "j1"), axis="index", show=True) + .hide_values(subset="c1", axis="columns", show=True) + ) + result = styler.render() + for header in [">c2<", ">j2<"]: + assert header not in result + for data in [2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15]: + assert f">{data}<" not in result + for data in [0, 1, 8, 9]: + assert f">{data}<" in result def test_block_names(): From e9b011c85ac056cecf617555f9133c878fc6e702 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 26 Apr 2021 08:19:28 +0200 Subject: [PATCH 5/6] add doc examples --- pandas/io/formats/style.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f2b641b0d0327..ca4b6c6fefb32 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1008,7 +1008,7 @@ def hide_columns(self, subset) -> Styler: def hide_values(self, subset, axis: Axis = "columns", show: bool = False) -> Styler: """ - Hide (or explicitly show) columns or rows upon rendering. + Hide (or exclusively show) columns or rows upon rendering. Parameters ---------- @@ -1018,11 +1018,37 @@ def hide_values(self, subset, axis: Axis = "columns", show: bool = False) -> Sty axis : {0 or 'index', 1 or 'columns'} Axis along which the ``subset`` is applied. show : bool - Indicates whether the supplied subset should be hidden, or explicitly shown. + Indicates whether the supplied subset should be hidden, or exclusively + shown. Returns ------- self : Styler + + Examples + -------- + >>> df = DataFrame([[1, 2], [3, 4]], columns=["c1", "c2"], index=["i1", "i2"]) + >>> df.style.hide_values("c1") + c2 + i1 2 + i2 4 + + >>> df.style.hide_values("i1", axis="index") + c1 c2 + i2 3 4 + + >>> df.style.hide_values("i1", axis="index", show=True) + c1 c2 + i1 1 2 + + >>> mcols = MultiIndex.from_product([["c1", "c2"], ["d1", "d2", "d3"]]) + >>> data = np.arange(12).reshape((2,6)) + >>> df = DataFrame(data, columns=mcols, index=["i1", "i2"]) + >>> df.style.hide_values(subset=(slice(None), "d2":"d3")) + c1 c2 + d1 d1 + i1 0 6 + i2 3 9 """ if axis in [0, "index"]: subset = IndexSlice[subset, :] From 654015ec0b4edb0b8a529de95d86d9dea26a928a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 26 Apr 2021 13:42:25 +0200 Subject: [PATCH 6/6] mypy error: Slice index must be an integer or None --- pandas/tests/io/formats/style/test_style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 27dc3fd071a81..84d31d913cc9d 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1426,7 +1426,7 @@ def test_non_reducing_multi_slice_on_multiindex(self, slice_): np.array(["i1", "i2"]), pd.Index(["i1", "i2"]), ["i1", "i2"], - pd.IndexSlice["i1":"i2"], + pd.IndexSlice["i1":"i2"], # type: ignore[misc] ], ) def test_hide_values_index(self, subset): @@ -1469,7 +1469,7 @@ def test_hide_values_index(self, subset): np.array(["c1", "c2"]), pd.Index(["c1", "c2"]), ["c1", "c2"], - pd.IndexSlice["c1":"c2"], + pd.IndexSlice["c1":"c2"], # type: ignore[misc] ], ) def test_hide_values_columns(self, subset):