Skip to content

Commit dd301b3

Browse files
attack68feefladder
authored andcommitted
ENH: add kw levels to Styler.hide_index to hide only specific levels (pandas-dev#42636)
1 parent 6deb238 commit dd301b3

File tree

4 files changed

+98
-22
lines changed

4 files changed

+98
-22
lines changed

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Other enhancements
3434
- :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`)
3535
- Additional options added to :meth:`.Styler.bar` to control alignment and display, with keyword only arguments (:issue:`26070`, :issue:`36419`)
3636
- :meth:`Styler.bar` now validates the input argument ``width`` and ``height`` (:issue:`42511`)
37+
- Add keyword ``levels`` to :meth:`.Styler.hide_index` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`)
3738
- :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`)
3839
- Added ``sparse_index`` and ``sparse_columns`` keyword arguments to :meth:`.Styler.to_html` (:issue:`41946`)
3940
- Added keyword argument ``environment`` to :meth:`.Styler.to_latex` also allowing a specific "longtable" entry with a separate jinja2 template (:issue:`41866`)

pandas/io/formats/style.py

+51-5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
FilePathOrBuffer,
2525
FrameOrSeries,
2626
IndexLabel,
27+
Level,
2728
Scalar,
2829
)
2930
from pandas.compat._optional import import_optional_dependency
@@ -748,7 +749,9 @@ def to_latex(
748749
self.data.columns = RangeIndex(stop=len(self.data.columns))
749750
numeric_cols = self.data._get_numeric_data().columns.to_list()
750751
self.data.columns = _original_columns
751-
column_format = "" if self.hide_index_ else "l" * self.data.index.nlevels
752+
column_format = ""
753+
for level in range(self.index.nlevels):
754+
column_format += "" if self.hide_index_[level] else "l"
752755
for ci, _ in enumerate(self.data.columns):
753756
if ci not in self.hidden_columns:
754757
column_format += (
@@ -1746,14 +1749,18 @@ def set_na_rep(self, na_rep: str) -> StylerRenderer:
17461749
self.na_rep = na_rep
17471750
return self.format(na_rep=na_rep, precision=self.precision)
17481751

1749-
def hide_index(self, subset: Subset | None = None) -> Styler:
1752+
def hide_index(
1753+
self,
1754+
subset: Subset | None = None,
1755+
levels: Level | list[Level] | None = None,
1756+
) -> Styler:
17501757
"""
17511758
Hide the entire index, or specific keys in the index from rendering.
17521759
17531760
This method has dual functionality:
17541761
1755-
- if ``subset`` is ``None`` then the entire index will be hidden whilst
1756-
displaying all data-rows.
1762+
- if ``subset`` is ``None`` then the entire index, or specified levels, will
1763+
be hidden whilst displaying all data-rows.
17571764
- if a ``subset`` is given then those specific rows will be hidden whilst the
17581765
index itself remains visible.
17591766
@@ -1765,6 +1772,11 @@ def hide_index(self, subset: Subset | None = None) -> Styler:
17651772
A valid 1d input or single key along the index axis within
17661773
`DataFrame.loc[<subset>, :]`, to limit ``data`` to *before* applying
17671774
the function.
1775+
levels : int, str, list
1776+
The level(s) to hide in a MultiIndex if hiding the entire index. Cannot be
1777+
used simultaneously with ``subset``.
1778+
1779+
.. versionadded:: 1.4.0
17681780
17691781
Returns
17701782
-------
@@ -1814,9 +1826,43 @@ def hide_index(self, subset: Subset | None = None) -> Styler:
18141826
a b c a b c
18151827
0.7 1.0 1.3 1.5 -0.0 -0.2
18161828
-0.6 1.2 1.8 1.9 0.3 0.3
1829+
1830+
Hide a specific level:
1831+
1832+
>>> df.style.format("{:,.1f").hide_index(levels=1) # doctest: +SKIP
1833+
x y
1834+
a b c a b c
1835+
x 0.1 0.0 0.4 1.3 0.6 -1.4
1836+
0.7 1.0 1.3 1.5 -0.0 -0.2
1837+
1.4 -0.8 1.6 -0.2 -0.4 -0.3
1838+
y 0.4 1.0 -0.2 -0.8 -1.2 1.1
1839+
-0.6 1.2 1.8 1.9 0.3 0.3
1840+
0.8 0.5 -0.3 1.2 2.2 -0.8
18171841
"""
1842+
if levels is not None and subset is not None:
1843+
raise ValueError("`subset` and `levels` cannot be passed simultaneously")
1844+
18181845
if subset is None:
1819-
self.hide_index_ = True
1846+
if levels is None:
1847+
levels_: list[Level] = list(range(self.index.nlevels))
1848+
elif isinstance(levels, int):
1849+
levels_ = [levels]
1850+
elif isinstance(levels, str):
1851+
levels_ = [self.index._get_level_number(levels)]
1852+
elif isinstance(levels, list):
1853+
levels_ = [
1854+
self.index._get_level_number(lev)
1855+
if not isinstance(lev, int)
1856+
else lev
1857+
for lev in levels
1858+
]
1859+
else:
1860+
raise ValueError(
1861+
"`levels` must be of type `int`, `str` or list of such"
1862+
)
1863+
self.hide_index_ = [
1864+
True if lev in levels_ else False for lev in range(self.index.nlevels)
1865+
]
18201866
else:
18211867
subset_ = IndexSlice[subset, :] # new var so mypy reads not Optional
18221868
subset = non_reducing_slice(subset_)

pandas/io/formats/style_render.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def __init__(
9797
self.cell_ids = cell_ids
9898

9999
# add rendering variables
100-
self.hide_index_: bool = False # bools for hiding col/row headers
100+
self.hide_index_: list = [False] * self.index.nlevels
101101
self.hide_columns_: bool = False
102102
self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
103103
self.hidden_columns: Sequence[int] = []
@@ -305,17 +305,18 @@ def _translate_header(
305305
# 1) column headers
306306
if not self.hide_columns_:
307307
for r in range(self.data.columns.nlevels):
308-
index_blanks = [
309-
_element("th", blank_class, blank_value, not self.hide_index_)
310-
] * (self.data.index.nlevels - 1)
308+
# number of index blanks is governed by number of hidden index levels
309+
index_blanks = [_element("th", blank_class, blank_value, True)] * (
310+
self.index.nlevels - sum(self.hide_index_) - 1
311+
)
311312

312313
name = self.data.columns.names[r]
313314
column_name = [
314315
_element(
315316
"th",
316317
f"{blank_class if name is None else index_name_class} level{r}",
317318
name if name is not None else blank_value,
318-
not self.hide_index_,
319+
not all(self.hide_index_),
319320
)
320321
]
321322

@@ -352,15 +353,15 @@ def _translate_header(
352353
if (
353354
self.data.index.names
354355
and com.any_not_none(*self.data.index.names)
355-
and not self.hide_index_
356+
and not all(self.hide_index_)
356357
and not self.hide_columns_
357358
):
358359
index_names = [
359360
_element(
360361
"th",
361362
f"{index_name_class} level{c}",
362363
blank_value if name is None else name,
363-
True,
364+
not self.hide_index_[c],
364365
)
365366
for c, name in enumerate(self.data.index.names)
366367
]
@@ -435,7 +436,7 @@ def _translate_body(
435436
"th",
436437
f"{row_heading_class} level{c} {trimmed_row_class}",
437438
"...",
438-
not self.hide_index_,
439+
not self.hide_index_[c],
439440
attributes="",
440441
)
441442
for c in range(self.data.index.nlevels)
@@ -472,7 +473,7 @@ def _translate_body(
472473
"th",
473474
f"{row_heading_class} level{c} row{r}",
474475
value,
475-
(_is_visible(r, c, idx_lengths) and not self.hide_index_),
476+
(_is_visible(r, c, idx_lengths) and not self.hide_index_[c]),
476477
id=f"level{c}_row{r}",
477478
attributes=(
478479
f'rowspan="{idx_lengths.get((c, r), 0)}"'
@@ -537,7 +538,7 @@ def _translate_latex(self, d: dict) -> None:
537538
d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]]
538539
body = []
539540
for r, row in enumerate(d["body"]):
540-
if self.hide_index_:
541+
if all(self.hide_index_):
541542
row_body_headers = []
542543
else:
543544
row_body_headers = [

pandas/tests/io/formats/style/test_style.py

+35-7
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,32 @@ def test_clear(mi_styler_comp):
260260
assert all(res) if hasattr(res, "__iter__") else res
261261

262262

263+
def test_hide_raises(mi_styler):
264+
msg = "`subset` and `levels` cannot be passed simultaneously"
265+
with pytest.raises(ValueError, match=msg):
266+
mi_styler.hide_index(subset="something", levels="something else")
267+
268+
msg = "`levels` must be of type `int`, `str` or list of such"
269+
with pytest.raises(ValueError, match=msg):
270+
mi_styler.hide_index(levels={"bad": 1, "type": 2})
271+
272+
273+
@pytest.mark.parametrize("levels", [1, "one", [1], ["one"]])
274+
def test_hide_level(mi_styler, levels):
275+
mi_styler.index.names, mi_styler.columns.names = ["zero", "one"], ["zero", "one"]
276+
ctx = mi_styler.hide_index(levels=levels)._translate(False, True)
277+
assert len(ctx["head"][0]) == 3
278+
assert len(ctx["head"][1]) == 3
279+
assert len(ctx["head"][2]) == 4
280+
assert ctx["head"][2][0]["is_visible"]
281+
assert not ctx["head"][2][1]["is_visible"]
282+
283+
assert ctx["body"][0][0]["is_visible"]
284+
assert not ctx["body"][0][1]["is_visible"]
285+
assert ctx["body"][1][0]["is_visible"]
286+
assert not ctx["body"][1][1]["is_visible"]
287+
288+
263289
class TestStyler:
264290
def setup_method(self, method):
265291
np.random.seed(24)
@@ -1157,7 +1183,7 @@ def test_hide_single_index(self):
11571183
def test_hide_multiindex(self):
11581184
# GH 14194
11591185
df = DataFrame(
1160-
{"A": [1, 2]},
1186+
{"A": [1, 2], "B": [1, 2]},
11611187
index=MultiIndex.from_arrays(
11621188
[["a", "a"], [0, 1]], names=["idx_level_0", "idx_level_1"]
11631189
),
@@ -1167,16 +1193,17 @@ def test_hide_multiindex(self):
11671193
assert ctx1["body"][0][0]["is_visible"]
11681194
assert ctx1["body"][0][1]["is_visible"]
11691195
# check for blank header rows
1170-
assert ctx1["head"][0][0]["is_visible"]
1171-
assert ctx1["head"][0][1]["is_visible"]
1196+
print(ctx1["head"][0])
1197+
assert len(ctx1["head"][0]) == 4 # two visible indexes and two data columns
11721198

11731199
ctx2 = df.style.hide_index()._translate(True, True)
11741200
# tests for 'a' and '0'
11751201
assert not ctx2["body"][0][0]["is_visible"]
11761202
assert not ctx2["body"][0][1]["is_visible"]
11771203
# check for blank header rows
1204+
print(ctx2["head"][0])
1205+
assert len(ctx2["head"][0]) == 3 # one hidden (col name) and two data columns
11781206
assert not ctx2["head"][0][0]["is_visible"]
1179-
assert not ctx2["head"][0][1]["is_visible"]
11801207

11811208
def test_hide_columns_single_level(self):
11821209
# GH 14194
@@ -1243,9 +1270,10 @@ def test_hide_columns_index_mult_levels(self):
12431270
# hide second column and index
12441271
ctx = df.style.hide_columns([("b", 1)]).hide_index()._translate(True, True)
12451272
assert not ctx["body"][0][0]["is_visible"] # index
1246-
assert ctx["head"][0][2]["is_visible"] # b
1247-
assert ctx["head"][1][2]["is_visible"] # 0
1248-
assert not ctx["head"][1][3]["is_visible"] # 1
1273+
assert len(ctx["head"][0]) == 3
1274+
assert ctx["head"][0][1]["is_visible"] # b
1275+
assert ctx["head"][1][1]["is_visible"] # 0
1276+
assert not ctx["head"][1][2]["is_visible"] # 1
12491277
assert not ctx["body"][1][3]["is_visible"] # 4
12501278
assert ctx["body"][1][2]["is_visible"]
12511279
assert ctx["body"][1][2]["display_value"] == 3

0 commit comments

Comments
 (0)