Skip to content

ENH: add kw levels to Styler.hide_index to hide only specific levels #42636

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 17 commits into from
Aug 5, 2021
Merged
Show file tree
Hide file tree
Changes from 16 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
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +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`)
- :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
56 changes: 51 additions & 5 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
FilePathOrBuffer,
FrameOrSeries,
IndexLabel,
Level,
Scalar,
)
from pandas.compat._optional import import_optional_dependency
Expand Down Expand Up @@ -748,7 +749,9 @@ def to_latex(
self.data.columns = RangeIndex(stop=len(self.data.columns))
numeric_cols = self.data._get_numeric_data().columns.to_list()
self.data.columns = _original_columns
column_format = "" if self.hide_index_ else "l" * self.data.index.nlevels
column_format = ""
for level in range(self.index.nlevels):
column_format += "" if self.hide_index_[level] else "l"
for ci, _ in enumerate(self.data.columns):
if ci not in self.hidden_columns:
column_format += (
Expand Down Expand Up @@ -1746,14 +1749,18 @@ def set_na_rep(self, na_rep: str) -> StylerRenderer:
self.na_rep = na_rep
return self.format(na_rep=na_rep, precision=self.precision)

def hide_index(self, subset: Subset | None = None) -> Styler:
def hide_index(
self,
subset: Subset | None = None,
levels: Level | list[Level] | None = None,
) -> Styler:
"""
Hide the entire index, or specific keys in the index from rendering.

This method has dual functionality:

- if ``subset`` is ``None`` then the entire index will be hidden whilst
displaying all data-rows.
- if ``subset`` is ``None`` then the entire index, or specified levels, will
be hidden whilst displaying all data-rows.
- if a ``subset`` is given then those specific rows will be hidden whilst the
index itself remains visible.

Expand All @@ -1765,6 +1772,11 @@ def hide_index(self, subset: Subset | None = None) -> Styler:
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
The level(s) to hide in a MultiIndex if hiding the entire index. Cannot be
used simultaneously with ``subset``.

.. versionadded:: 1.4.0

Returns
-------
Expand Down Expand Up @@ -1814,9 +1826,43 @@ def hide_index(self, subset: Subset | None = None) -> Styler:
a b c a b c
0.7 1.0 1.3 1.5 -0.0 -0.2
-0.6 1.2 1.8 1.9 0.3 0.3

Hide a specific level:

>>> df.style.format("{:,.1f").hide_index(levels=1) # doctest: +SKIP
x y
a b c a b c
x 0.1 0.0 0.4 1.3 0.6 -1.4
0.7 1.0 1.3 1.5 -0.0 -0.2
1.4 -0.8 1.6 -0.2 -0.4 -0.3
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
"""
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_index_ = True
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"
)
self.hide_index_ = [
True if lev in levels_ else False for lev in range(self.index.nlevels)
]
else:
subset_ = IndexSlice[subset, :] # new var so mypy reads not Optional
subset = non_reducing_slice(subset_)
Expand Down
21 changes: 11 additions & 10 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def __init__(
self.cell_ids = cell_ids

# add rendering variables
self.hide_index_: bool = False # bools for hiding col/row headers
self.hide_index_: list = [False] * self.index.nlevels
self.hide_columns_: bool = False
self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
self.hidden_columns: Sequence[int] = []
Expand Down Expand Up @@ -305,17 +305,18 @@ def _translate_header(
# 1) column headers
if not self.hide_columns_:
for r in range(self.data.columns.nlevels):
index_blanks = [
_element("th", blank_class, blank_value, not self.hide_index_)
] * (self.data.index.nlevels - 1)
# 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
)

name = self.data.columns.names[r]
column_name = [
_element(
"th",
f"{blank_class if name is None else index_name_class} level{r}",
name if name is not None else blank_value,
not self.hide_index_,
not all(self.hide_index_),
)
]

Expand Down Expand Up @@ -352,14 +353,14 @@ def _translate_header(
if (
self.data.index.names
and com.any_not_none(*self.data.index.names)
and not self.hide_index_
and not all(self.hide_index_)
):
index_names = [
_element(
"th",
f"{index_name_class} level{c}",
blank_value if name is None else name,
True,
not self.hide_index_[c],
)
for c, name in enumerate(self.data.index.names)
]
Expand Down Expand Up @@ -434,7 +435,7 @@ def _translate_body(
"th",
f"{row_heading_class} level{c} {trimmed_row_class}",
"...",
not self.hide_index_,
not self.hide_index_[c],
attributes="",
)
for c in range(self.data.index.nlevels)
Expand Down Expand Up @@ -471,7 +472,7 @@ def _translate_body(
"th",
f"{row_heading_class} level{c} row{r}",
value,
(_is_visible(r, c, idx_lengths) and not self.hide_index_),
(_is_visible(r, c, idx_lengths) and not self.hide_index_[c]),
id=f"level{c}_row{r}",
attributes=(
f'rowspan="{idx_lengths.get((c, r), 0)}"'
Expand Down Expand Up @@ -536,7 +537,7 @@ def _translate_latex(self, d: dict) -> None:
d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]]
body = []
for r, row in enumerate(d["body"]):
if self.hide_index_:
if all(self.hide_index_):
row_body_headers = []
else:
row_body_headers = [
Expand Down
42 changes: 35 additions & 7 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,32 @@ def test_clear(mi_styler_comp):
assert all(res) if hasattr(res, "__iter__") else res


def test_hide_raises(mi_styler):
msg = "`subset` and `levels` cannot be passed simultaneously"
with pytest.raises(ValueError, match=msg):
mi_styler.hide_index(subset="something", levels="something else")

msg = "`levels` 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})


@pytest.mark.parametrize("levels", [1, "one", [1], ["one"]])
def test_hide_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
assert len(ctx["head"][1]) == 3
assert len(ctx["head"][2]) == 4
assert ctx["head"][2][0]["is_visible"]
assert not ctx["head"][2][1]["is_visible"]

assert ctx["body"][0][0]["is_visible"]
assert not ctx["body"][0][1]["is_visible"]
assert ctx["body"][1][0]["is_visible"]
assert not ctx["body"][1][1]["is_visible"]


class TestStyler:
def setup_method(self, method):
np.random.seed(24)
Expand Down Expand Up @@ -1157,7 +1183,7 @@ def test_hide_single_index(self):
def test_hide_multiindex(self):
# GH 14194
df = DataFrame(
{"A": [1, 2]},
{"A": [1, 2], "B": [1, 2]},
index=MultiIndex.from_arrays(
[["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
),
Expand All @@ -1167,16 +1193,17 @@ def test_hide_multiindex(self):
assert ctx1["body"][0][0]["is_visible"]
assert ctx1["body"][0][1]["is_visible"]
# check for blank header rows
assert ctx1["head"][0][0]["is_visible"]
assert ctx1["head"][0][1]["is_visible"]
print(ctx1["head"][0])
assert len(ctx1["head"][0]) == 4 # two visible indexes and two data columns

ctx2 = df.style.hide_index()._translate(True, True)
# tests for 'a' and '0'
assert not ctx2["body"][0][0]["is_visible"]
assert not ctx2["body"][0][1]["is_visible"]
# check for blank header rows
print(ctx2["head"][0])
assert len(ctx2["head"][0]) == 3 # one hidden (col name) and two data columns
assert not ctx2["head"][0][0]["is_visible"]
assert not ctx2["head"][0][1]["is_visible"]

def test_hide_columns_single_level(self):
# GH 14194
Expand Down Expand Up @@ -1243,9 +1270,10 @@ def test_hide_columns_index_mult_levels(self):
# hide second column and index
ctx = df.style.hide_columns([("b", 1)]).hide_index()._translate(True, True)
assert not ctx["body"][0][0]["is_visible"] # index
assert ctx["head"][0][2]["is_visible"] # b
assert ctx["head"][1][2]["is_visible"] # 0
assert not ctx["head"][1][3]["is_visible"] # 1
assert len(ctx["head"][0]) == 3
assert ctx["head"][0][1]["is_visible"] # b
assert ctx["head"][1][1]["is_visible"] # 0
assert not ctx["head"][1][2]["is_visible"] # 1
assert not ctx["body"][1][3]["is_visible"] # 4
assert ctx["body"][1][2]["is_visible"]
assert ctx["body"][1][2]["display_value"] == 3
Expand Down