Skip to content

Commit 4f0e2e9

Browse files
attack68jreback
andauthored
ENH: add kw level to Styler.hide_columns (#42914)
* add levels to hide_columns * add levels to hide_columns * add tests * change levels to level (jreback) * Update doc/source/whatsnew/v1.4.0.rst Co-authored-by: JHM Darbyshire (iMac) <[email protected]> Co-authored-by: Jeff Reback <[email protected]>
1 parent 60126ff commit 4f0e2e9

File tree

4 files changed

+95
-38
lines changed

4 files changed

+95
-38
lines changed

doc/source/whatsnew/v1.4.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +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`)
37+
- Add keyword ``level`` to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`)
3838
- :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`)
3939
- Added ``sparse_index`` and ``sparse_columns`` keyword arguments to :meth:`.Styler.to_html` (:issue:`41946`)
4040
- 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

+71-26
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import pandas as pd
3434
from pandas import (
35+
Index,
3536
IndexSlice,
3637
RangeIndex,
3738
)
@@ -1785,7 +1786,7 @@ def set_na_rep(self, na_rep: str) -> StylerRenderer:
17851786
def hide_index(
17861787
self,
17871788
subset: Subset | None = None,
1788-
levels: Level | list[Level] | None = None,
1789+
level: Level | list[Level] | None = None,
17891790
) -> Styler:
17901791
"""
17911792
Hide the entire index, or specific keys in the index from rendering.
@@ -1805,7 +1806,7 @@ def hide_index(
18051806
A valid 1d input or single key along the index axis within
18061807
`DataFrame.loc[<subset>, :]`, to limit ``data`` to *before* applying
18071808
the function.
1808-
levels : int, str, list
1809+
level : int, str, list
18091810
The level(s) to hide in a MultiIndex if hiding the entire index. Cannot be
18101811
used simultaneously with ``subset``.
18111812
@@ -1862,7 +1863,7 @@ def hide_index(
18621863
18631864
Hide a specific level:
18641865
1865-
>>> df.style.format("{:,.1f").hide_index(levels=1) # doctest: +SKIP
1866+
>>> df.style.format("{:,.1f").hide_index(level=1) # doctest: +SKIP
18661867
x y
18671868
a b c a b c
18681869
x 0.1 0.0 0.4 1.3 0.6 -1.4
@@ -1872,27 +1873,11 @@ def hide_index(
18721873
-0.6 1.2 1.8 1.9 0.3 0.3
18731874
0.8 0.5 -0.3 1.2 2.2 -0.8
18741875
"""
1875-
if levels is not None and subset is not None:
1876-
raise ValueError("`subset` and `levels` cannot be passed simultaneously")
1876+
if level is not None and subset is not None:
1877+
raise ValueError("`subset` and `level` cannot be passed simultaneously")
18771878

18781879
if subset is None:
1879-
if levels is None:
1880-
levels_: list[Level] = list(range(self.index.nlevels))
1881-
elif isinstance(levels, int):
1882-
levels_ = [levels]
1883-
elif isinstance(levels, str):
1884-
levels_ = [self.index._get_level_number(levels)]
1885-
elif isinstance(levels, list):
1886-
levels_ = [
1887-
self.index._get_level_number(lev)
1888-
if not isinstance(lev, int)
1889-
else lev
1890-
for lev in levels
1891-
]
1892-
else:
1893-
raise ValueError(
1894-
"`levels` must be of type `int`, `str` or list of such"
1895-
)
1880+
levels_ = _refactor_levels(level, self.index)
18961881
self.hide_index_ = [
18971882
True if lev in levels_ else False for lev in range(self.index.nlevels)
18981883
]
@@ -1906,14 +1891,18 @@ def hide_index(
19061891
self.hidden_rows = hrows # type: ignore[assignment]
19071892
return self
19081893

1909-
def hide_columns(self, subset: Subset | None = None) -> Styler:
1894+
def hide_columns(
1895+
self,
1896+
subset: Subset | None = None,
1897+
level: Level | list[Level] | None = None,
1898+
) -> Styler:
19101899
"""
19111900
Hide the column headers or specific keys in the columns from rendering.
19121901
19131902
This method has dual functionality:
19141903
1915-
- if ``subset`` is ``None`` then the entire column headers row will be hidden
1916-
whilst the data-values remain visible.
1904+
- if ``subset`` is ``None`` then the entire column headers row, or
1905+
specific levels, will be hidden whilst the data-values remain visible.
19171906
- if a ``subset`` is given then those specific columns, including the
19181907
data-values will be hidden, whilst the column headers row remains visible.
19191908
@@ -1925,6 +1914,11 @@ def hide_columns(self, subset: Subset | None = None) -> Styler:
19251914
A valid 1d input or single key along the columns axis within
19261915
`DataFrame.loc[:, <subset>]`, to limit ``data`` to *before* applying
19271916
the function.
1917+
level : int, str, list
1918+
The level(s) to hide in a MultiIndex if hiding the entire column headers
1919+
row. Cannot be used simultaneously with ``subset``.
1920+
1921+
.. versionadded:: 1.4.0
19281922
19291923
Returns
19301924
-------
@@ -1979,9 +1973,26 @@ def hide_columns(self, subset: Subset | None = None) -> Styler:
19791973
y a 1.0 -1.2
19801974
b 1.2 0.3
19811975
c 0.5 2.2
1976+
1977+
Hide a specific level:
1978+
1979+
>>> df.style.format("{:.1f}").hide_columns(level=1) # doctest: +SKIP
1980+
x y
1981+
x a 0.1 0.0 0.4 1.3 0.6 -1.4
1982+
b 0.7 1.0 1.3 1.5 -0.0 -0.2
1983+
c 1.4 -0.8 1.6 -0.2 -0.4 -0.3
1984+
y a 0.4 1.0 -0.2 -0.8 -1.2 1.1
1985+
b -0.6 1.2 1.8 1.9 0.3 0.3
1986+
c 0.8 0.5 -0.3 1.2 2.2 -0.8
19821987
"""
1988+
if level is not None and subset is not None:
1989+
raise ValueError("`subset` and `level` cannot be passed simultaneously")
1990+
19831991
if subset is None:
1984-
self.hide_columns_ = True
1992+
levels_ = _refactor_levels(level, self.columns)
1993+
self.hide_columns_ = [
1994+
True if lev in levels_ else False for lev in range(self.columns.nlevels)
1995+
]
19851996
else:
19861997
subset_ = IndexSlice[:, subset] # new var so mypy reads not Optional
19871998
subset = non_reducing_slice(subset_)
@@ -3172,3 +3183,37 @@ def css_calc(x, left: float, right: float, align: str):
31723183
index=data.index,
31733184
columns=data.columns,
31743185
)
3186+
3187+
3188+
def _refactor_levels(
3189+
level: Level | list[Level] | None,
3190+
obj: Index,
3191+
) -> list[Level]:
3192+
"""
3193+
Returns a consistent levels arg for use in ``hide_index`` or ``hide_columns``.
3194+
3195+
Parameters
3196+
----------
3197+
level : int, str, list
3198+
Original ``level`` arg supplied to above methods.
3199+
obj:
3200+
Either ``self.index`` or ``self.columns``
3201+
3202+
Returns
3203+
-------
3204+
list : refactored arg with a list of levels to hide
3205+
"""
3206+
if level is None:
3207+
levels_: list[Level] = list(range(obj.nlevels))
3208+
elif isinstance(level, int):
3209+
levels_ = [level]
3210+
elif isinstance(level, str):
3211+
levels_ = [obj._get_level_number(level)]
3212+
elif isinstance(level, list):
3213+
levels_ = [
3214+
obj._get_level_number(lev) if not isinstance(lev, int) else lev
3215+
for lev in level
3216+
]
3217+
else:
3218+
raise ValueError("`level` must be of type `int`, `str` or list of such")
3219+
return levels_

pandas/io/formats/style_render.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def __init__(
9898

9999
# add rendering variables
100100
self.hide_index_: list = [False] * self.index.nlevels
101-
self.hide_columns_: bool = False
101+
self.hide_columns_: list = [False] * self.columns.nlevels
102102
self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
103103
self.hidden_columns: Sequence[int] = []
104104
self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
@@ -303,8 +303,10 @@ def _translate_header(
303303

304304
head = []
305305
# 1) column headers
306-
if not self.hide_columns_:
307-
for r in range(self.data.columns.nlevels):
306+
for r, hide in enumerate(self.hide_columns_):
307+
if hide:
308+
continue
309+
else:
308310
# number of index blanks is governed by number of hidden index levels
309311
index_blanks = [_element("th", blank_class, blank_value, True)] * (
310312
self.index.nlevels - sum(self.hide_index_) - 1
@@ -354,7 +356,7 @@ def _translate_header(
354356
self.data.index.names
355357
and com.any_not_none(*self.data.index.names)
356358
and not all(self.hide_index_)
357-
and not self.hide_columns_
359+
and not all(self.hide_columns_)
358360
):
359361
index_names = [
360362
_element(

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

+17-7
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,19 @@ def test_clear(mi_styler_comp):
261261

262262

263263
def test_hide_raises(mi_styler):
264-
msg = "`subset` and `levels` cannot be passed simultaneously"
264+
msg = "`subset` and `level` cannot be passed simultaneously"
265265
with pytest.raises(ValueError, match=msg):
266-
mi_styler.hide_index(subset="something", levels="something else")
266+
mi_styler.hide_index(subset="something", level="something else")
267267

268-
msg = "`levels` must be of type `int`, `str` or list of such"
268+
msg = "`level` must be of type `int`, `str` or list of such"
269269
with pytest.raises(ValueError, match=msg):
270-
mi_styler.hide_index(levels={"bad": 1, "type": 2})
270+
mi_styler.hide_index(level={"bad": 1, "type": 2})
271271

272272

273-
@pytest.mark.parametrize("levels", [1, "one", [1], ["one"]])
274-
def test_hide_level(mi_styler, levels):
273+
@pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
274+
def test_hide_index_level(mi_styler, level):
275275
mi_styler.index.names, mi_styler.columns.names = ["zero", "one"], ["zero", "one"]
276-
ctx = mi_styler.hide_index(levels=levels)._translate(False, True)
276+
ctx = mi_styler.hide_index(level=level)._translate(False, True)
277277
assert len(ctx["head"][0]) == 3
278278
assert len(ctx["head"][1]) == 3
279279
assert len(ctx["head"][2]) == 4
@@ -286,6 +286,16 @@ def test_hide_level(mi_styler, levels):
286286
assert not ctx["body"][1][1]["is_visible"]
287287

288288

289+
@pytest.mark.parametrize("level", [1, "one", [1], ["one"]])
290+
@pytest.mark.parametrize("names", [True, False])
291+
def test_hide_columns_level(mi_styler, level, names):
292+
mi_styler.columns.names = ["zero", "one"]
293+
if names:
294+
mi_styler.index.names = ["zero", "one"]
295+
ctx = mi_styler.hide_columns(level=level)._translate(True, False)
296+
assert len(ctx["head"]) == (2 if names else 1)
297+
298+
289299
class TestStyler:
290300
def setup_method(self, method):
291301
np.random.seed(24)

0 commit comments

Comments
 (0)