From be99a74ca5bd5e12eaac919475f37f9c1ac5274f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 6 Aug 2021 00:34:26 +0200 Subject: [PATCH 1/5] add levels to hide_columns --- pandas/io/formats/style.py | 87 +++++++++++++++++++++++-------- pandas/io/formats/style_render.py | 10 ++-- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 53ae2daa31235..4d39f84a582c0 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -23,6 +23,7 @@ Axis, FilePathOrBuffer, FrameOrSeries, + Index, IndexLabel, Level, Scalar, @@ -1843,23 +1844,7 @@ def hide_index( raise ValueError("`subset` and `levels` cannot be passed simultaneously") if subset is None: - if levels is None: - levels_: list[Level] = list(range(self.index.nlevels)) - elif isinstance(levels, int): - levels_ = [levels] - elif isinstance(levels, str): - levels_ = [self.index._get_level_number(levels)] - elif isinstance(levels, list): - levels_ = [ - self.index._get_level_number(lev) - if not isinstance(lev, int) - else lev - for lev in levels - ] - else: - raise ValueError( - "`levels` must be of type `int`, `str` or list of such" - ) + levels_ = _refactor_levels(levels, self.index) self.hide_index_ = [ True if lev in levels_ else False for lev in range(self.index.nlevels) ] @@ -1873,14 +1858,18 @@ def hide_index( self.hidden_rows = hrows # type: ignore[assignment] return self - def hide_columns(self, subset: Subset | None = None) -> Styler: + def hide_columns( + self, + subset: Subset | None = None, + levels: Level | list[Level] | None = None, + ) -> Styler: """ Hide the column headers or specific keys in the columns from rendering. This method has dual functionality: - - if ``subset`` is ``None`` then the entire column headers row will be hidden - whilst the data-values remain visible. + - if ``subset`` is ``None`` then the entire column headers row, or + specific levels, will be hidden whilst the data-values remain visible. - if a ``subset`` is given then those specific columns, including the data-values will be hidden, whilst the column headers row remains visible. @@ -1892,6 +1881,11 @@ def hide_columns(self, subset: Subset | None = None) -> Styler: A valid 1d input or single key along the columns axis within `DataFrame.loc[:, ]`, to limit ``data`` to *before* applying the function. + levels : int, str, list + 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 Returns ------- @@ -1946,9 +1940,26 @@ def hide_columns(self, subset: Subset | None = None) -> Styler: y a 1.0 -1.2 b 1.2 0.3 c 0.5 2.2 + + Hide a specific level: + + >>> df.style.format("{:.1f}").hide_columns(levels=1) # doctest: +SKIP + x y + 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 levels is not None and subset is not None: + raise ValueError("`subset` and `levels` cannot be passed simultaneously") + if subset is None: - self.hide_columns_ = True + levels_ = _refactor_levels(levels, self.columns) + self.hide_columns_ = [ + True if lev in levels_ else False for lev in range(self.columns.nlevels) + ] else: subset_ = IndexSlice[:, subset] # new var so mypy reads not Optional subset = non_reducing_slice(subset_) @@ -3139,3 +3150,37 @@ def css_calc(x, left: float, right: float, align: str): index=data.index, columns=data.columns, ) + + +def _refactor_levels( + levels: Level | list[Level] | None, + obj: Index, +) -> list[Level]: + """ + Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``. + + Parameters + ---------- + levels : int, str, list + Original ``levels`` arg supplied to above methods. + obj: + Either ``self.index`` or ``self.columns`` + + Returns + ------- + list : refactored arg with a list of levels to hide + """ + if levels is None: + levels_: list[Level] = list(range(obj.nlevels)) + elif isinstance(levels, int): + levels_ = [levels] + elif isinstance(levels, str): + levels_ = [obj._get_level_number(levels)] + elif isinstance(levels, list): + levels_ = [ + obj._get_level_number(lev) if not isinstance(lev, int) else lev + for lev in levels + ] + else: + raise ValueError("`levels` must be of type `int`, `str` or list of such") + return levels_ diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 176468315a487..e89d4519543c6 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -98,7 +98,7 @@ def __init__( # add rendering variables self.hide_index_: list = [False] * self.index.nlevels - self.hide_columns_: bool = False + self.hide_columns_: list = [False] * self.columns.nlevels self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols self.hidden_columns: Sequence[int] = [] self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) @@ -303,8 +303,10 @@ def _translate_header( head = [] # 1) column headers - if not self.hide_columns_: - for r in range(self.data.columns.nlevels): + for r, hide in enumerate(self.hide_columns_): + if hide: + continue + else: # number of index blanks is governed by number of hidden index levels index_blanks = [_element("th", blank_class, blank_value, True)] * ( self.index.nlevels - sum(self.hide_index_) - 1 @@ -354,7 +356,7 @@ def _translate_header( self.data.index.names and com.any_not_none(*self.data.index.names) and not all(self.hide_index_) - and not self.hide_columns_ + and not all(self.hide_columns_) ): index_names = [ _element( From e298a1e2f776a48298df8cd1fc31741559813fce Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 6 Aug 2021 00:54:27 +0200 Subject: [PATCH 2/5] add levels to hide_columns --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4d39f84a582c0..5504c67fbd26c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -23,7 +23,6 @@ Axis, FilePathOrBuffer, FrameOrSeries, - Index, IndexLabel, Level, Scalar, @@ -33,6 +32,7 @@ import pandas as pd from pandas import ( + Index, IndexSlice, RangeIndex, ) From 7e63d2c231bf6e6b9c492845f846a237de9e3655 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 6 Aug 2021 13:52:23 +0200 Subject: [PATCH 3/5] add tests --- doc/source/whatsnew/v1.4.0.rst | 2 +- pandas/tests/io/formats/style/test_style.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index fa9c424351b00..d5cf32654a324 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -34,7 +34,7 @@ Other enhancements - :meth:`Series.sample`, :meth:`DataFrame.sample`, and :meth:`.GroupBy.sample` now accept a ``np.random.Generator`` as input to ``random_state``. A generator will be more performant, especially with ``replace=False`` (:issue:`38100`) - Additional options added to :meth:`.Styler.bar` to control alignment and display, with keyword only arguments (:issue:`26070`, :issue:`36419`) - :meth:`Styler.bar` now validates the input argument ``width`` and ``height`` (:issue:`42511`) -- Add keyword ``levels`` to :meth:`.Styler.hide_index` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`) +- Add keyword ``levels`` to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`) - :meth:`Series.ewm`, :meth:`DataFrame.ewm`, now support a ``method`` argument with a ``'table'`` option that performs the windowing operation over an entire :class:`DataFrame`. See :ref:`Window Overview ` for performance and functional benefits (:issue:`42273`) - Added ``sparse_index`` and ``sparse_columns`` keyword arguments to :meth:`.Styler.to_html` (:issue:`41946`) - Added keyword argument ``environment`` to :meth:`.Styler.to_latex` also allowing a specific "longtable" entry with a separate jinja2 template (:issue:`41866`) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 6b084ecc2ca6c..abfc48a779a05 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -271,7 +271,7 @@ def test_hide_raises(mi_styler): @pytest.mark.parametrize("levels", [1, "one", [1], ["one"]]) -def test_hide_level(mi_styler, levels): +def test_hide_index_level(mi_styler, levels): mi_styler.index.names, mi_styler.columns.names = ["zero", "one"], ["zero", "one"] ctx = mi_styler.hide_index(levels=levels)._translate(False, True) assert len(ctx["head"][0]) == 3 @@ -286,6 +286,16 @@ def test_hide_level(mi_styler, levels): assert not ctx["body"][1][1]["is_visible"] +@pytest.mark.parametrize("levels", [1, "one", [1], ["one"]]) +@pytest.mark.parametrize("names", [True, False]) +def test_hide_columns_level(mi_styler, levels, names): + mi_styler.columns.names = ["zero", "one"] + if names: + mi_styler.index.names = ["zero", "one"] + ctx = mi_styler.hide_columns(levels=levels)._translate(True, False) + assert len(ctx["head"]) == (2 if names else 1) + + class TestStyler: def setup_method(self, method): np.random.seed(24) From 91d1c58010d15416a09a56dd06344785df09ae18 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 8 Aug 2021 14:25:06 +0200 Subject: [PATCH 4/5] change levels to level (jreback) --- pandas/io/formats/style.py | 46 ++++++++++----------- pandas/tests/io/formats/style/test_style.py | 20 ++++----- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 5504c67fbd26c..6812afc58c37e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1753,7 +1753,7 @@ def set_na_rep(self, na_rep: str) -> StylerRenderer: def hide_index( self, subset: Subset | None = None, - levels: Level | list[Level] | None = None, + level: Level | list[Level] | None = None, ) -> Styler: """ Hide the entire index, or specific keys in the index from rendering. @@ -1773,7 +1773,7 @@ def hide_index( A valid 1d input or single key along the index axis within `DataFrame.loc[, :]`, to limit ``data`` to *before* applying the function. - levels : int, str, list + level : int, str, list The level(s) to hide in a MultiIndex if hiding the entire index. Cannot be used simultaneously with ``subset``. @@ -1830,7 +1830,7 @@ def hide_index( Hide a specific level: - >>> df.style.format("{:,.1f").hide_index(levels=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 @@ -1840,11 +1840,11 @@ def hide_index( -0.6 1.2 1.8 1.9 0.3 0.3 0.8 0.5 -0.3 1.2 2.2 -0.8 """ - if levels is not None and subset is not None: - raise ValueError("`subset` and `levels` cannot be passed simultaneously") + if level is not None and subset is not None: + raise ValueError("`subset` and `level` cannot be passed simultaneously") if subset is None: - levels_ = _refactor_levels(levels, self.index) + levels_ = _refactor_levels(level, self.index) self.hide_index_ = [ True if lev in levels_ else False for lev in range(self.index.nlevels) ] @@ -1861,7 +1861,7 @@ def hide_index( def hide_columns( self, subset: Subset | None = None, - levels: Level | list[Level] | None = None, + level: Level | list[Level] | None = None, ) -> Styler: """ Hide the column headers or specific keys in the columns from rendering. @@ -1881,7 +1881,7 @@ def hide_columns( A valid 1d input or single key along the columns axis within `DataFrame.loc[:, ]`, to limit ``data`` to *before* applying the function. - levels : int, str, list + level : int, str, list The level(s) to hide in a MultiIndex if hiding the entire column headers row. Cannot be used simultaneously with ``subset``. @@ -1943,7 +1943,7 @@ def hide_columns( Hide a specific level: - >>> df.style.format("{:.1f}").hide_columns(levels=1) # doctest: +SKIP + >>> df.style.format("{:.1f}").hide_columns(level=1) # doctest: +SKIP x y 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 @@ -1952,11 +1952,11 @@ def hide_columns( 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 levels is not None and subset is not None: - raise ValueError("`subset` and `levels` cannot be passed simultaneously") + if level is not None and subset is not None: + raise ValueError("`subset` and `level` cannot be passed simultaneously") if subset is None: - levels_ = _refactor_levels(levels, self.columns) + levels_ = _refactor_levels(level, self.columns) self.hide_columns_ = [ True if lev in levels_ else False for lev in range(self.columns.nlevels) ] @@ -3153,7 +3153,7 @@ def css_calc(x, left: float, right: float, align: str): def _refactor_levels( - levels: Level | list[Level] | None, + level: Level | list[Level] | None, obj: Index, ) -> list[Level]: """ @@ -3161,8 +3161,8 @@ def _refactor_levels( Parameters ---------- - levels : int, str, list - Original ``levels`` arg supplied to above methods. + level : int, str, list + Original ``level`` arg supplied to above methods. obj: Either ``self.index`` or ``self.columns`` @@ -3170,17 +3170,17 @@ def _refactor_levels( ------- list : refactored arg with a list of levels to hide """ - if levels is None: + if level is None: levels_: list[Level] = list(range(obj.nlevels)) - elif isinstance(levels, int): - levels_ = [levels] - elif isinstance(levels, str): - levels_ = [obj._get_level_number(levels)] - elif isinstance(levels, list): + elif isinstance(level, int): + levels_ = [level] + elif isinstance(level, str): + levels_ = [obj._get_level_number(level)] + elif isinstance(level, list): levels_ = [ obj._get_level_number(lev) if not isinstance(lev, int) else lev - for lev in levels + for lev in level ] else: - raise ValueError("`levels` must be of type `int`, `str` or list of such") + raise ValueError("`level` must be of type `int`, `str` or list of such") return levels_ diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index abfc48a779a05..3c042e130981c 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -261,19 +261,19 @@ def test_clear(mi_styler_comp): def test_hide_raises(mi_styler): - msg = "`subset` and `levels` cannot be passed simultaneously" + msg = "`subset` and `level` cannot be passed simultaneously" with pytest.raises(ValueError, match=msg): - mi_styler.hide_index(subset="something", levels="something else") + mi_styler.hide_index(subset="something", level="something else") - msg = "`levels` must be of type `int`, `str` or list of such" + msg = "`level` must be of type `int`, `str` or list of such" with pytest.raises(ValueError, match=msg): - mi_styler.hide_index(levels={"bad": 1, "type": 2}) + mi_styler.hide_index(level={"bad": 1, "type": 2}) -@pytest.mark.parametrize("levels", [1, "one", [1], ["one"]]) -def test_hide_index_level(mi_styler, levels): +@pytest.mark.parametrize("level", [1, "one", [1], ["one"]]) +def test_hide_index_level(mi_styler, level): mi_styler.index.names, mi_styler.columns.names = ["zero", "one"], ["zero", "one"] - ctx = mi_styler.hide_index(levels=levels)._translate(False, True) + ctx = mi_styler.hide_index(level=level)._translate(False, True) assert len(ctx["head"][0]) == 3 assert len(ctx["head"][1]) == 3 assert len(ctx["head"][2]) == 4 @@ -286,13 +286,13 @@ def test_hide_index_level(mi_styler, levels): assert not ctx["body"][1][1]["is_visible"] -@pytest.mark.parametrize("levels", [1, "one", [1], ["one"]]) +@pytest.mark.parametrize("level", [1, "one", [1], ["one"]]) @pytest.mark.parametrize("names", [True, False]) -def test_hide_columns_level(mi_styler, levels, names): +def test_hide_columns_level(mi_styler, level, names): mi_styler.columns.names = ["zero", "one"] if names: mi_styler.index.names = ["zero", "one"] - ctx = mi_styler.hide_columns(levels=levels)._translate(True, False) + ctx = mi_styler.hide_columns(level=level)._translate(True, False) assert len(ctx["head"]) == (2 if names else 1) From b0639721aaeb0cc46c841e37606d2c9e859cf201 Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Sun, 8 Aug 2021 19:31:20 -0400 Subject: [PATCH 5/5] Update doc/source/whatsnew/v1.4.0.rst --- doc/source/whatsnew/v1.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index d5cf32654a324..d54c185355788 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -34,7 +34,7 @@ Other enhancements - :meth:`Series.sample`, :meth:`DataFrame.sample`, and :meth:`.GroupBy.sample` now accept a ``np.random.Generator`` as input to ``random_state``. A generator will be more performant, especially with ``replace=False`` (:issue:`38100`) - Additional options added to :meth:`.Styler.bar` to control alignment and display, with keyword only arguments (:issue:`26070`, :issue:`36419`) - :meth:`Styler.bar` now validates the input argument ``width`` and ``height`` (:issue:`42511`) -- Add keyword ``levels`` to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`) +- Add keyword ``level`` to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`) - :meth:`Series.ewm`, :meth:`DataFrame.ewm`, now support a ``method`` argument with a ``'table'`` option that performs the windowing operation over an entire :class:`DataFrame`. See :ref:`Window Overview ` for performance and functional benefits (:issue:`42273`) - Added ``sparse_index`` and ``sparse_columns`` keyword arguments to :meth:`.Styler.to_html` (:issue:`41946`) - Added keyword argument ``environment`` to :meth:`.Styler.to_latex` also allowing a specific "longtable" entry with a separate jinja2 template (:issue:`41866`)