Skip to content

Commit 7488d8e

Browse files
Backport PR #41266: API: make hide_columns and hide_index have a consistent signature and function in Styler (#42041)
Co-authored-by: attack68 <[email protected]>
1 parent 05cfa0d commit 7488d8e

File tree

3 files changed

+235
-68
lines changed

3 files changed

+235
-68
lines changed

pandas/io/formats/style.py

+154-19
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
from pandas.util._decorators import doc
3232

3333
import pandas as pd
34-
from pandas import RangeIndex
34+
from pandas import (
35+
IndexSlice,
36+
RangeIndex,
37+
)
3538
from pandas.api.types import is_list_like
3639
from pandas.core import generic
3740
import pandas.core.common as com
@@ -726,7 +729,7 @@ def to_latex(
726729
self.data.columns = RangeIndex(stop=len(self.data.columns))
727730
numeric_cols = self.data._get_numeric_data().columns.to_list()
728731
self.data.columns = _original_columns
729-
column_format = "" if self.hidden_index else "l" * self.data.index.nlevels
732+
column_format = "" if self.hide_index_ else "l" * self.data.index.nlevels
730733
for ci, _ in enumerate(self.data.columns):
731734
if ci not in self.hidden_columns:
732735
column_format += (
@@ -971,7 +974,7 @@ def _copy(self, deepcopy: bool = False) -> Styler:
971974
)
972975

973976
styler.uuid = self.uuid
974-
styler.hidden_index = self.hidden_index
977+
styler.hide_index_ = self.hide_index_
975978

976979
if deepcopy:
977980
styler.ctx = copy.deepcopy(self.ctx)
@@ -1010,7 +1013,7 @@ def clear(self) -> None:
10101013
self.cell_context.clear()
10111014
self._todo.clear()
10121015

1013-
self.hidden_index = False
1016+
self.hide_index_ = False
10141017
self.hidden_columns = []
10151018
# self.format and self.table_styles may be dependent on user
10161019
# input in self.__init__()
@@ -1141,7 +1144,7 @@ def _applymap(
11411144
) -> Styler:
11421145
func = partial(func, **kwargs) # applymap doesn't take kwargs?
11431146
if subset is None:
1144-
subset = pd.IndexSlice[:]
1147+
subset = IndexSlice[:]
11451148
subset = non_reducing_slice(subset)
11461149
result = self.data.loc[subset].applymap(func)
11471150
self._update_ctx(result)
@@ -1556,37 +1559,169 @@ def set_na_rep(self, na_rep: str) -> StylerRenderer:
15561559
self.na_rep = na_rep
15571560
return self.format(na_rep=na_rep, precision=self.precision)
15581561

1559-
def hide_index(self) -> Styler:
1562+
def hide_index(self, subset: Subset | None = None) -> Styler:
15601563
"""
1561-
Hide any indices from rendering.
1564+
Hide the entire index, or specific keys in the index from rendering.
1565+
1566+
This method has dual functionality:
1567+
1568+
- if ``subset`` is ``None`` then the entire index will be hidden whilst
1569+
displaying all data-rows.
1570+
- if a ``subset`` is given then those specific rows will be hidden whilst the
1571+
index itself remains visible.
1572+
1573+
.. versionchanged:: 1.3.0
1574+
1575+
Parameters
1576+
----------
1577+
subset : label, array-like, IndexSlice, optional
1578+
A valid 1d input or single key along the index axis within
1579+
`DataFrame.loc[<subset>, :]`, to limit ``data`` to *before* applying
1580+
the function.
15621581
15631582
Returns
15641583
-------
15651584
self : Styler
1585+
1586+
See Also
1587+
--------
1588+
Styler.hide_columns: Hide the entire column headers row, or specific columns.
1589+
1590+
Examples
1591+
--------
1592+
Simple application hiding specific rows:
1593+
1594+
>>> df = pd.DataFrame([[1,2], [3,4], [5,6]], index=["a", "b", "c"])
1595+
>>> df.style.hide_index(["a", "b"])
1596+
0 1
1597+
c 5 6
1598+
1599+
Hide the index and retain the data values:
1600+
1601+
>>> midx = pd.MultiIndex.from_product([["x", "y"], ["a", "b", "c"]])
1602+
>>> df = pd.DataFrame(np.random.randn(6,6), index=midx, columns=midx)
1603+
>>> df.style.format("{:.1f}").hide_index()
1604+
x y
1605+
a b c a b c
1606+
0.1 0.0 0.4 1.3 0.6 -1.4
1607+
0.7 1.0 1.3 1.5 -0.0 -0.2
1608+
1.4 -0.8 1.6 -0.2 -0.4 -0.3
1609+
0.4 1.0 -0.2 -0.8 -1.2 1.1
1610+
-0.6 1.2 1.8 1.9 0.3 0.3
1611+
0.8 0.5 -0.3 1.2 2.2 -0.8
1612+
1613+
Hide specific rows but retain the index:
1614+
1615+
>>> df.style.format("{:.1f}").hide_index(subset=(slice(None), ["a", "c"]))
1616+
x y
1617+
a b c a b c
1618+
x b 0.7 1.0 1.3 1.5 -0.0 -0.2
1619+
y b -0.6 1.2 1.8 1.9 0.3 0.3
1620+
1621+
Hide specific rows and the index:
1622+
1623+
>>> df.style.format("{:.1f}").hide_index(subset=(slice(None), ["a", "c"]))
1624+
... .hide_index()
1625+
x y
1626+
a b c a b c
1627+
0.7 1.0 1.3 1.5 -0.0 -0.2
1628+
-0.6 1.2 1.8 1.9 0.3 0.3
15661629
"""
1567-
self.hidden_index = True
1630+
if subset is None:
1631+
self.hide_index_ = True
1632+
else:
1633+
subset_ = IndexSlice[subset, :] # new var so mypy reads not Optional
1634+
subset = non_reducing_slice(subset_)
1635+
hide = self.data.loc[subset]
1636+
hrows = self.index.get_indexer_for(hide.index)
1637+
# error: Incompatible types in assignment (expression has type
1638+
# "ndarray", variable has type "Sequence[int]")
1639+
self.hidden_rows = hrows # type: ignore[assignment]
15681640
return self
15691641

1570-
def hide_columns(self, subset: Subset) -> Styler:
1642+
def hide_columns(self, subset: Subset | None = None) -> Styler:
15711643
"""
1572-
Hide columns from rendering.
1644+
Hide the column headers or specific keys in the columns from rendering.
1645+
1646+
This method has dual functionality:
1647+
1648+
- if ``subset`` is ``None`` then the entire column headers row will be hidden
1649+
whilst the data-values remain visible.
1650+
- if a ``subset`` is given then those specific columns, including the
1651+
data-values will be hidden, whilst the column headers row remains visible.
1652+
1653+
.. versionchanged:: 1.3.0
15731654
15741655
Parameters
15751656
----------
1576-
subset : label, array-like, IndexSlice
1577-
A valid 1d input or single key along the appropriate axis within
1578-
`DataFrame.loc[]`, to limit ``data`` to *before* applying the function.
1657+
subset : label, array-like, IndexSlice, optional
1658+
A valid 1d input or single key along the columns axis within
1659+
`DataFrame.loc[:, <subset>]`, to limit ``data`` to *before* applying
1660+
the function.
15791661
15801662
Returns
15811663
-------
15821664
self : Styler
1665+
1666+
See Also
1667+
--------
1668+
Styler.hide_index: Hide the entire index, or specific keys in the index.
1669+
1670+
Examples
1671+
--------
1672+
Simple application hiding specific columns:
1673+
1674+
>>> df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=["a", "b", "c"])
1675+
>>> df.style.hide_columns(["a", "b"])
1676+
c
1677+
0 3
1678+
1 6
1679+
1680+
Hide column headers and retain the data values:
1681+
1682+
>>> midx = pd.MultiIndex.from_product([["x", "y"], ["a", "b", "c"]])
1683+
>>> df = pd.DataFrame(np.random.randn(6,6), index=midx, columns=midx)
1684+
>>> df.style.format("{:.1f}").hide_columns()
1685+
x d 0.1 0.0 0.4 1.3 0.6 -1.4
1686+
e 0.7 1.0 1.3 1.5 -0.0 -0.2
1687+
f 1.4 -0.8 1.6 -0.2 -0.4 -0.3
1688+
y d 0.4 1.0 -0.2 -0.8 -1.2 1.1
1689+
e -0.6 1.2 1.8 1.9 0.3 0.3
1690+
f 0.8 0.5 -0.3 1.2 2.2 -0.8
1691+
1692+
Hide specific columns but retain the column headers:
1693+
1694+
>>> df.style.format("{:.1f}").hide_columns(subset=(slice(None), ["a", "c"]))
1695+
x y
1696+
b b
1697+
x a 0.0 0.6
1698+
b 1.0 -0.0
1699+
c -0.8 -0.4
1700+
y a 1.0 -1.2
1701+
b 1.2 0.3
1702+
c 0.5 2.2
1703+
1704+
Hide specific columns and the column headers:
1705+
1706+
>>> df.style.format("{:.1f}").hide_columns(subset=(slice(None), ["a", "c"]))
1707+
... .hide_columns()
1708+
x a 0.0 0.6
1709+
b 1.0 -0.0
1710+
c -0.8 -0.4
1711+
y a 1.0 -1.2
1712+
b 1.2 0.3
1713+
c 0.5 2.2
15831714
"""
1584-
subset = non_reducing_slice(subset)
1585-
hidden_df = self.data.loc[subset]
1586-
hcols = self.columns.get_indexer_for(hidden_df.columns)
1587-
# error: Incompatible types in assignment (expression has type
1588-
# "ndarray", variable has type "Sequence[int]")
1589-
self.hidden_columns = hcols # type: ignore[assignment]
1715+
if subset is None:
1716+
self.hide_columns_ = True
1717+
else:
1718+
subset_ = IndexSlice[:, subset] # new var so mypy reads not Optional
1719+
subset = non_reducing_slice(subset_)
1720+
hide = self.data.loc[subset]
1721+
hcols = self.columns.get_indexer_for(hide.columns)
1722+
# error: Incompatible types in assignment (expression has type
1723+
# "ndarray", variable has type "Sequence[int]")
1724+
self.hidden_columns = hcols # type: ignore[assignment]
15901725
return self
15911726

15921727
# -----------------------------------------------------------------------

pandas/io/formats/style_render.py

+54-43
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ def __init__(
9898
self.cell_ids = cell_ids
9999

100100
# add rendering variables
101-
self.hidden_index: bool = False
101+
self.hide_index_: bool = False # bools for hiding col/row headers
102+
self.hide_columns_: bool = False
103+
self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols
102104
self.hidden_columns: Sequence[int] = []
103105
self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list)
104106
self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str)
@@ -298,55 +300,56 @@ def _translate_header(
298300

299301
head = []
300302
# 1) column headers
301-
for r in range(self.data.columns.nlevels):
302-
index_blanks = [
303-
_element("th", blank_class, blank_value, not self.hidden_index)
304-
] * (self.data.index.nlevels - 1)
305-
306-
name = self.data.columns.names[r]
307-
column_name = [
308-
_element(
309-
"th",
310-
f"{blank_class if name is None else index_name_class} level{r}",
311-
name if name is not None else blank_value,
312-
not self.hidden_index,
313-
)
314-
]
315-
316-
if clabels:
317-
column_headers = [
303+
if not self.hide_columns_:
304+
for r in range(self.data.columns.nlevels):
305+
index_blanks = [
306+
_element("th", blank_class, blank_value, not self.hide_index_)
307+
] * (self.data.index.nlevels - 1)
308+
309+
name = self.data.columns.names[r]
310+
column_name = [
318311
_element(
319312
"th",
320-
f"{col_heading_class} level{r} col{c}",
321-
value,
322-
_is_visible(c, r, col_lengths),
323-
attributes=(
324-
f'colspan="{col_lengths.get((r, c), 0)}"'
325-
if col_lengths.get((r, c), 0) > 1
326-
else ""
327-
),
313+
f"{blank_class if name is None else index_name_class} level{r}",
314+
name if name is not None else blank_value,
315+
not self.hide_index_,
328316
)
329-
for c, value in enumerate(clabels[r])
330317
]
331318

332-
if len(self.data.columns) > max_cols:
333-
# add an extra column with `...` value to indicate trimming
334-
column_headers.append(
319+
if clabels:
320+
column_headers = [
335321
_element(
336322
"th",
337-
f"{col_heading_class} level{r} {trimmed_col_class}",
338-
"...",
339-
True,
340-
attributes="",
323+
f"{col_heading_class} level{r} col{c}",
324+
value,
325+
_is_visible(c, r, col_lengths),
326+
attributes=(
327+
f'colspan="{col_lengths.get((r, c), 0)}"'
328+
if col_lengths.get((r, c), 0) > 1
329+
else ""
330+
),
341331
)
342-
)
343-
head.append(index_blanks + column_name + column_headers)
332+
for c, value in enumerate(clabels[r])
333+
]
334+
335+
if len(self.data.columns) > max_cols:
336+
# add an extra column with `...` value to indicate trimming
337+
column_headers.append(
338+
_element(
339+
"th",
340+
f"{col_heading_class} level{r} {trimmed_col_class}",
341+
"...",
342+
True,
343+
attributes="",
344+
)
345+
)
346+
head.append(index_blanks + column_name + column_headers)
344347

345348
# 2) index names
346349
if (
347350
self.data.index.names
348351
and com.any_not_none(*self.data.index.names)
349-
and not self.hidden_index
352+
and not self.hide_index_
350353
):
351354
index_names = [
352355
_element(
@@ -412,7 +415,9 @@ def _translate_body(
412415
The associated HTML elements needed for template rendering.
413416
"""
414417
# for sparsifying a MultiIndex
415-
idx_lengths = _get_level_lengths(self.index, sparsify_index, max_rows)
418+
idx_lengths = _get_level_lengths(
419+
self.index, sparsify_index, max_rows, self.hidden_rows
420+
)
416421

417422
rlabels = self.data.index.tolist()[:max_rows] # slice to allow trimming
418423
if self.data.index.nlevels == 1:
@@ -426,7 +431,7 @@ def _translate_body(
426431
"th",
427432
f"{row_heading_class} level{c} {trimmed_row_class}",
428433
"...",
429-
not self.hidden_index,
434+
not self.hide_index_,
430435
attributes="",
431436
)
432437
for c in range(self.data.index.nlevels)
@@ -463,7 +468,7 @@ def _translate_body(
463468
"th",
464469
f"{row_heading_class} level{c} row{r}",
465470
value,
466-
(_is_visible(r, c, idx_lengths) and not self.hidden_index),
471+
(_is_visible(r, c, idx_lengths) and not self.hide_index_),
467472
id=f"level{c}_row{r}",
468473
attributes=(
469474
f'rowspan="{idx_lengths.get((c, r), 0)}"'
@@ -497,7 +502,7 @@ def _translate_body(
497502
"td",
498503
f"{data_class} row{r} col{c}{cls}",
499504
value,
500-
(c not in self.hidden_columns),
505+
(c not in self.hidden_columns and r not in self.hidden_rows),
501506
attributes="",
502507
display_value=self._display_funcs[(r, c)](value),
503508
)
@@ -528,7 +533,7 @@ def _translate_latex(self, d: dict) -> None:
528533
d["head"] = [[col for col in row if col["is_visible"]] for row in d["head"]]
529534
body = []
530535
for r, row in enumerate(d["body"]):
531-
if self.hidden_index:
536+
if self.hide_index_:
532537
row_body_headers = []
533538
else:
534539
row_body_headers = [
@@ -843,7 +848,13 @@ def _get_level_lengths(
843848
last_label = j
844849
lengths[(i, last_label)] = 0
845850
elif j not in hidden_elements:
846-
lengths[(i, last_label)] += 1
851+
if lengths[(i, last_label)] == 0:
852+
# if the previous iteration was first-of-kind but hidden then offset
853+
last_label = j
854+
lengths[(i, last_label)] = 1
855+
else:
856+
# else add to previous iteration
857+
lengths[(i, last_label)] += 1
847858

848859
non_zero_lengths = {
849860
element: length for element, length in lengths.items() if length >= 1

0 commit comments

Comments
 (0)