diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 02e1369a05b93..dab5c54b40cd7 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 @@ -640,7 +641,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) @@ -1060,12 +1061,76 @@ 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.hide_values(subset) + + def hide_values(self, subset, axis: Axis = "columns", show: bool = False) -> Styler: + """ + Hide (or exclusively show) columns or rows upon rendering. + + Parameters + ---------- + subset : IndexSlice + 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 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, :] + 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.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]") + self.hidden_columns = hcols # type: ignore[assignment] + else: + raise ValueError( + f"`axis` must be one of [0, 1] or 'index' or 'columns', got: {axis}" + ) return self # ----------------------------------------------------------------------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 0253852fbb39a..31682690a46fd 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -99,6 +99,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]] = [] @@ -281,7 +282,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: @@ -316,7 +317,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), ) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 3b614be770bc5..177ac23fa88b7 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1423,6 +1423,117 @@ 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"], # type: ignore[misc] + ], + ) + 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"], # type: ignore[misc] + ], + ) + 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 '3' in result + assert '6' in result + assert '9' in result + + assert '1' not in result + assert '2' not in result + assert '4' not in result + assert '5' not in result + assert '7' not in result + assert '8' not 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(): # catch accidental removal of a block