diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4fc13fd54df02..9e0769f22e102 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1114,6 +1114,8 @@ def _copy(self, deepcopy: bool = False) -> Styler: shallow = [ # simple string or boolean immutables "hide_index_", "hide_columns_", + "hide_column_names", + "hide_index_names", "table_attributes", "cell_ids", "caption", @@ -2024,6 +2026,7 @@ def hide_index( self, subset: Subset | None = None, level: Level | list[Level] | None = None, + names: bool = False, ) -> Styler: """ Hide the entire index, or specific keys in the index from rendering. @@ -2047,6 +2050,11 @@ def hide_index( The level(s) to hide in a MultiIndex if hiding the entire index. Cannot be used simultaneously with ``subset``. + .. versionadded:: 1.4.0 + names : bool + Whether to hide the index name(s), in the case the index or part of it + remains visible. + .. versionadded:: 1.4.0 Returns @@ -2100,7 +2108,7 @@ def hide_index( Hide a specific level: - >>> df.style.format("{:,.1f").hide_index(level=1) # doctest: +SKIP + >>> df.style.format("{:,.1f}").hide_index(level=1) # doctest: +SKIP x y a b c a b c x 0.1 0.0 0.4 1.3 0.6 -1.4 @@ -2109,11 +2117,29 @@ def hide_index( y 0.4 1.0 -0.2 -0.8 -1.2 1.1 -0.6 1.2 1.8 1.9 0.3 0.3 0.8 0.5 -0.3 1.2 2.2 -0.8 + + Hiding just the index level names: + + >>> df.index.names = ["lev0", "lev1"] + >>> df.style.format("{:,.1f}").hide_index(names=True) # doctest: +SKIP + x y + a b c a b c + x a 0.1 0.0 0.4 1.3 0.6 -1.4 + b 0.7 1.0 1.3 1.5 -0.0 -0.2 + c 1.4 -0.8 1.6 -0.2 -0.4 -0.3 + y a 0.4 1.0 -0.2 -0.8 -1.2 1.1 + b -0.6 1.2 1.8 1.9 0.3 0.3 + c 0.8 0.5 -0.3 1.2 2.2 -0.8 """ if level is not None and subset is not None: raise ValueError("`subset` and `level` cannot be passed simultaneously") if subset is None: + if level is None and names: + # this combination implies user shows the index and hides just names + self.hide_index_names = True + return self + levels_ = _refactor_levels(level, self.index) self.hide_index_ = [ True if lev in levels_ else False for lev in range(self.index.nlevels) @@ -2126,12 +2152,16 @@ def hide_index( # error: Incompatible types in assignment (expression has type # "ndarray", variable has type "Sequence[int]") self.hidden_rows = hrows # type: ignore[assignment] + + if names: + self.hide_index_names = True return self def hide_columns( self, subset: Subset | None = None, level: Level | list[Level] | None = None, + names: bool = False, ) -> Styler: """ Hide the column headers or specific keys in the columns from rendering. @@ -2155,6 +2185,11 @@ def hide_columns( The level(s) to hide in a MultiIndex if hiding the entire column headers row. Cannot be used simultaneously with ``subset``. + .. versionadded:: 1.4.0 + names : bool + Whether to hide the column index name(s), in the case all column headers, + or some levels, are visible. + .. versionadded:: 1.4.0 Returns @@ -2221,11 +2256,29 @@ def hide_columns( y a 0.4 1.0 -0.2 -0.8 -1.2 1.1 b -0.6 1.2 1.8 1.9 0.3 0.3 c 0.8 0.5 -0.3 1.2 2.2 -0.8 + + Hiding just the column level names: + + >>> df.columns.names = ["lev0", "lev1"] + >>> df.style.format("{:.1f").hide_columns(names=True) # doctest: +SKIP + x y + a b c a b c + x a 0.1 0.0 0.4 1.3 0.6 -1.4 + b 0.7 1.0 1.3 1.5 -0.0 -0.2 + c 1.4 -0.8 1.6 -0.2 -0.4 -0.3 + y a 0.4 1.0 -0.2 -0.8 -1.2 1.1 + b -0.6 1.2 1.8 1.9 0.3 0.3 + c 0.8 0.5 -0.3 1.2 2.2 -0.8 """ if level is not None and subset is not None: raise ValueError("`subset` and `level` cannot be passed simultaneously") if subset is None: + if level is None and names: + # this combination implies user shows the column headers but hides names + self.hide_column_names = True + return self + levels_ = _refactor_levels(level, self.columns) self.hide_columns_ = [ True if lev in levels_ else False for lev in range(self.columns.nlevels) @@ -2238,6 +2291,9 @@ def hide_columns( # error: Incompatible types in assignment (expression has type # "ndarray", variable has type "Sequence[int]") self.hidden_columns = hcols # type: ignore[assignment] + + if names: + self.hide_column_names = True return self # ----------------------------------------------------------------------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index f51a1f5d9809d..e94dc63530163 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -98,6 +98,8 @@ def __init__( self.cell_ids = cell_ids # add rendering variables + self.hide_index_names: bool = False + self.hide_column_names: bool = False self.hide_index_: list = [False] * self.index.nlevels self.hide_columns_: list = [False] * self.columns.nlevels self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols @@ -334,7 +336,9 @@ def _translate_header( _element( "th", f"{blank_class if name is None else index_name_class} level{r}", - name if name is not None else blank_value, + name + if (name is not None and not self.hide_column_names) + else blank_value, not all(self.hide_index_), ) ] @@ -383,6 +387,7 @@ def _translate_header( and com.any_not_none(*self.data.index.names) and not all(self.hide_index_) and not all(self.hide_columns_) + and not self.hide_index_names ): index_names = [ _element( diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index dc8be68532f0e..4fb8c90bbd77e 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -48,8 +48,10 @@ def mi_styler_comp(mi_styler): mi_styler.set_table_styles([{"selector": "a", "props": "a:v;"}]) mi_styler.hide_columns() mi_styler.hide_columns([("c0", "c1_a")]) + mi_styler.hide_columns(names=True) mi_styler.hide_index() mi_styler.hide_index([("i0", "i1_a")]) + mi_styler.hide_index(names=True) mi_styler.set_table_attributes('class="box"') mi_styler.format(na_rep="MISSING", precision=3) mi_styler.highlight_max(axis=None) @@ -239,6 +241,8 @@ def test_copy(comprehensive, render, deepcopy, mi_styler, mi_styler_comp): "cell_ids", "hide_index_", "hide_columns_", + "hide_index_names", + "hide_column_names", "table_attributes", ] for attr in shallow: @@ -1398,3 +1402,40 @@ def test_non_reducing_multi_slice_on_multiindex(self, slice_): with tm.assert_produces_warning(warn, match=msg): result = df.loc[non_reducing_slice(slice_)] tm.assert_frame_equal(result, expected) + + +def test_hidden_index_names(mi_df): + mi_df.index.names = ["Lev0", "Lev1"] + mi_styler = mi_df.style + ctx = mi_styler._translate(True, True) + assert len(ctx["head"]) == 3 # 2 column index levels + 1 index names row + + mi_styler.hide_index(names=True) + ctx = mi_styler._translate(True, True) + assert len(ctx["head"]) == 2 # index names row is unparsed + for i in range(4): + assert ctx["body"][0][i]["is_visible"] # 2 index levels + 2 data values visible + + mi_styler.hide_index(level=1) + ctx = mi_styler._translate(True, True) + assert len(ctx["head"]) == 2 # index names row is still hidden + assert ctx["body"][0][0]["is_visible"] is True + assert ctx["body"][0][1]["is_visible"] is False + + +def test_hidden_column_names(mi_df): + mi_df.columns.names = ["Lev0", "Lev1"] + mi_styler = mi_df.style + ctx = mi_styler._translate(True, True) + assert ctx["head"][0][1]["display_value"] == "Lev0" + assert ctx["head"][1][1]["display_value"] == "Lev1" + + mi_styler.hide_columns(names=True) + ctx = mi_styler._translate(True, True) + assert ctx["head"][0][1]["display_value"] == " " + assert ctx["head"][1][1]["display_value"] == " " + + mi_styler.hide_columns(level=0) + ctx = mi_styler._translate(True, True) + assert len(ctx["head"]) == 1 # no index names and only one visible column headers + assert ctx["head"][0][1]["display_value"] == " "