Skip to content

ENH: add kw level to Styler.hide_columns #42914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``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 <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`)
Expand Down
97 changes: 71 additions & 26 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

import pandas as pd
from pandas import (
Index,
IndexSlice,
RangeIndex,
)
Expand Down Expand Up @@ -1785,7 +1786,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.
Expand All @@ -1805,7 +1806,7 @@ def hide_index(
A valid 1d input or single key along the index axis within
`DataFrame.loc[<subset>, :]`, 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``.

Expand Down Expand Up @@ -1862,7 +1863,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
Expand All @@ -1872,27 +1873,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:
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(level, self.index)
self.hide_index_ = [
True if lev in levels_ else False for lev in range(self.index.nlevels)
]
Expand All @@ -1906,14 +1891,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,
level: 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.

Expand All @@ -1925,6 +1914,11 @@ def hide_columns(self, subset: Subset | None = None) -> Styler:
A valid 1d input or single key along the columns axis within
`DataFrame.loc[:, <subset>]`, to limit ``data`` to *before* applying
the function.
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``.

.. versionadded:: 1.4.0

Returns
-------
Expand Down Expand Up @@ -1979,9 +1973,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(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
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:
self.hide_columns_ = True
levels_ = _refactor_levels(level, 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_)
Expand Down Expand Up @@ -3172,3 +3183,37 @@ def css_calc(x, left: float, right: float, align: str):
index=data.index,
columns=data.columns,
)


def _refactor_levels(
level: Level | list[Level] | None,
obj: Index,
) -> list[Level]:
"""
Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``.

Parameters
----------
level : int, str, list
Original ``level`` arg supplied to above methods.
obj:
Either ``self.index`` or ``self.columns``

Returns
-------
list : refactored arg with a list of levels to hide
"""
if level is None:
levels_: list[Level] = list(range(obj.nlevels))
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 level
]
else:
raise ValueError("`level` must be of type `int`, `str` or list of such")
return levels_
10 changes: 6 additions & 4 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
24 changes: 17 additions & 7 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_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
Expand All @@ -286,6 +286,16 @@ def test_hide_level(mi_styler, levels):
assert not ctx["body"][1][1]["is_visible"]


@pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
@pytest.mark.parametrize("names", [True, False])
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(level=level)._translate(True, False)
assert len(ctx["head"]) == (2 if names else 1)


class TestStyler:
def setup_method(self, method):
np.random.seed(24)
Expand Down