Skip to content

Commit e5f1f9c

Browse files
authored
ENH: add styler option context for sparsification of columns and index separately (#41512)
1 parent 3980696 commit e5f1f9c

File tree

9 files changed

+226
-167
lines changed

9 files changed

+226
-167
lines changed

asv_bench/benchmarks/io/style.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ def setup(self, cols, rows):
2020

2121
def time_apply_render(self, cols, rows):
2222
self._style_apply()
23-
self.st._render_html()
23+
self.st._render_html(True, True)
2424

2525
def peakmem_apply_render(self, cols, rows):
2626
self._style_apply()
27-
self.st._render_html()
27+
self.st._render_html(True, True)
2828

2929
def time_classes_render(self, cols, rows):
3030
self._style_classes()
31-
self.st._render_html()
31+
self.st._render_html(True, True)
3232

3333
def peakmem_classes_render(self, cols, rows):
3434
self._style_classes()
35-
self.st._render_html()
35+
self.st._render_html(True, True)
3636

3737
def time_format_render(self, cols, rows):
3838
self._style_format()

doc/source/user_guide/options.rst

+5
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,11 @@ plotting.backend matplotlib Change the plotting backend
482482
like Bokeh, Altair, etc.
483483
plotting.matplotlib.register_converters True Register custom converters with
484484
matplotlib. Set to False to de-register.
485+
styler.sparse.index True "Sparsify" MultiIndex display for rows
486+
in Styler output (don't display repeated
487+
elements in outer levels within groups).
488+
styler.sparse.columns True "Sparsify" MultiIndex display for columns
489+
in Styler output.
485490
======================================= ============ ==================================
486491

487492

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ precision, and perform HTML escaping (:issue:`40437` :issue:`40134`). There have
139139
properly format HTML and eliminate some inconsistencies (:issue:`39942` :issue:`40356` :issue:`39807` :issue:`39889` :issue:`39627`)
140140

141141
:class:`.Styler` has also been compatible with non-unique index or columns, at least for as many features as are fully compatible, others made only partially compatible (:issue:`41269`).
142+
One also has greater control of the display through separate sparsification of the index or columns, using the new 'styler' options context (:issue:`41142`).
142143

143144
Documentation has also seen major revisions in light of new features (:issue:`39720` :issue:`39317` :issue:`40493`)
144145

pandas/core/config_init.py

+23
Original file line numberDiff line numberDiff line change
@@ -726,3 +726,26 @@ def register_converter_cb(key):
726726
validator=is_one_of_factory(["auto", True, False]),
727727
cb=register_converter_cb,
728728
)
729+
730+
# ------
731+
# Styler
732+
# ------
733+
734+
styler_sparse_index_doc = """
735+
: bool
736+
Whether to sparsify the display of a hierarchical index. Setting to False will
737+
display each explicit level element in a hierarchical key for each row.
738+
"""
739+
740+
styler_sparse_columns_doc = """
741+
: bool
742+
Whether to sparsify the display of hierarchical columns. Setting to False will
743+
display each explicit level element in a hierarchical key for each column.
744+
"""
745+
746+
with cf.config_prefix("styler"):
747+
cf.register_option("sparse.index", True, styler_sparse_index_doc, validator=bool)
748+
749+
cf.register_option(
750+
"sparse.columns", True, styler_sparse_columns_doc, validator=bool
751+
)

pandas/io/formats/style.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import numpy as np
1919

20+
from pandas._config import get_option
21+
2022
from pandas._typing import (
2123
Axis,
2224
FrameOrSeries,
@@ -201,14 +203,27 @@ def _repr_html_(self) -> str:
201203
"""
202204
Hooks into Jupyter notebook rich display system.
203205
"""
204-
return self._render_html()
206+
return self.render()
205207

206-
def render(self, **kwargs) -> str:
208+
def render(
209+
self,
210+
sparse_index: bool | None = None,
211+
sparse_columns: bool | None = None,
212+
**kwargs,
213+
) -> str:
207214
"""
208215
Render the ``Styler`` including all applied styles to HTML.
209216
210217
Parameters
211218
----------
219+
sparse_index : bool, optional
220+
Whether to sparsify the display of a hierarchical index. Setting to False
221+
will display each explicit level element in a hierarchical key for each row.
222+
Defaults to ``pandas.options.styler.sparse.index`` value.
223+
sparse_columns : bool, optional
224+
Whether to sparsify the display of a hierarchical index. Setting to False
225+
will display each explicit level element in a hierarchical key for each row.
226+
Defaults to ``pandas.options.styler.sparse.columns`` value.
212227
**kwargs
213228
Any additional keyword arguments are passed
214229
through to ``self.template.render``.
@@ -240,7 +255,11 @@ def render(self, **kwargs) -> str:
240255
* caption
241256
* table_attributes
242257
"""
243-
return self._render_html(**kwargs)
258+
if sparse_index is None:
259+
sparse_index = get_option("styler.sparse.index")
260+
if sparse_columns is None:
261+
sparse_columns = get_option("styler.sparse.columns")
262+
return self._render_html(sparse_index, sparse_columns, **kwargs)
244263

245264
def set_tooltips(
246265
self,

pandas/io/formats/style_render.py

+11-16
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,14 @@ def __init__(
107107
tuple[int, int], Callable[[Any], str]
108108
] = defaultdict(lambda: partial(_default_formatter, precision=def_precision))
109109

110-
def _render_html(self, **kwargs) -> str:
110+
def _render_html(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str:
111111
"""
112112
Renders the ``Styler`` including all applied styles to HTML.
113113
Generates a dict with necessary kwargs passed to jinja2 template.
114114
"""
115115
self._compute()
116116
# TODO: namespace all the pandas keys
117-
d = self._translate()
117+
d = self._translate(sparse_index, sparse_columns)
118118
d.update(kwargs)
119119
return self.template_html.render(**d)
120120

@@ -133,9 +133,7 @@ def _compute(self):
133133
r = func(self)(*args, **kwargs)
134134
return r
135135

136-
def _translate(
137-
self, sparsify_index: bool | None = None, sparsify_cols: bool | None = None
138-
):
136+
def _translate(self, sparse_index: bool, sparse_cols: bool):
139137
"""
140138
Process Styler data and settings into a dict for template rendering.
141139
@@ -144,22 +142,19 @@ def _translate(
144142
145143
Parameters
146144
----------
147-
sparsify_index : bool, optional
148-
Whether to sparsify the index or print all hierarchical index elements
149-
sparsify_cols : bool, optional
150-
Whether to sparsify the columns or print all hierarchical column elements
145+
sparse_index : bool
146+
Whether to sparsify the index or print all hierarchical index elements.
147+
Upstream defaults are typically to `pandas.options.styler.sparse.index`.
148+
sparse_cols : bool
149+
Whether to sparsify the columns or print all hierarchical column elements.
150+
Upstream defaults are typically to `pandas.options.styler.sparse.columns`.
151151
152152
Returns
153153
-------
154154
d : dict
155155
The following structure: {uuid, table_styles, caption, head, body,
156156
cellstyle, table_attributes}
157157
"""
158-
if sparsify_index is None:
159-
sparsify_index = get_option("display.multi_sparse")
160-
if sparsify_cols is None:
161-
sparsify_cols = get_option("display.multi_sparse")
162-
163158
ROW_HEADING_CLASS = "row_heading"
164159
COL_HEADING_CLASS = "col_heading"
165160
INDEX_NAME_CLASS = "index_name"
@@ -176,14 +171,14 @@ def _translate(
176171
}
177172

178173
head = self._translate_header(
179-
BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS, sparsify_cols
174+
BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS, sparse_cols
180175
)
181176
d.update({"head": head})
182177

183178
self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(
184179
list
185180
)
186-
body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS, sparsify_index)
181+
body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS, sparse_index)
187182
d.update({"body": body})
188183

189184
cellstyle: list[dict[str, CSSList | list[str]]] = [

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

+24-22
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,28 @@ def styler(df):
2828

2929

3030
def test_display_format(styler):
31-
ctx = styler.format("{:0.1f}")._translate()
31+
ctx = styler.format("{:0.1f}")._translate(True, True)
3232
assert all(["display_value" in c for c in row] for row in ctx["body"])
3333
assert all([len(c["display_value"]) <= 3 for c in row[1:]] for row in ctx["body"])
3434
assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3
3535

3636

3737
def test_format_dict(styler):
38-
ctx = styler.format({"A": "{:0.1f}", "B": "{0:.2%}"})._translate()
38+
ctx = styler.format({"A": "{:0.1f}", "B": "{0:.2%}"})._translate(True, True)
3939
assert ctx["body"][0][1]["display_value"] == "0.0"
4040
assert ctx["body"][0][2]["display_value"] == "-60.90%"
4141

4242

4343
def test_format_string(styler):
44-
ctx = styler.format("{:.2f}")._translate()
44+
ctx = styler.format("{:.2f}")._translate(True, True)
4545
assert ctx["body"][0][1]["display_value"] == "0.00"
4646
assert ctx["body"][0][2]["display_value"] == "-0.61"
4747
assert ctx["body"][1][1]["display_value"] == "1.00"
4848
assert ctx["body"][1][2]["display_value"] == "-1.23"
4949

5050

5151
def test_format_callable(styler):
52-
ctx = styler.format(lambda v: "neg" if v < 0 else "pos")._translate()
52+
ctx = styler.format(lambda v: "neg" if v < 0 else "pos")._translate(True, True)
5353
assert ctx["body"][0][1]["display_value"] == "pos"
5454
assert ctx["body"][0][2]["display_value"] == "neg"
5555
assert ctx["body"][1][1]["display_value"] == "pos"
@@ -60,17 +60,17 @@ def test_format_with_na_rep():
6060
# GH 21527 28358
6161
df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"])
6262

63-
ctx = df.style.format(None, na_rep="-")._translate()
63+
ctx = df.style.format(None, na_rep="-")._translate(True, True)
6464
assert ctx["body"][0][1]["display_value"] == "-"
6565
assert ctx["body"][0][2]["display_value"] == "-"
6666

67-
ctx = df.style.format("{:.2%}", na_rep="-")._translate()
67+
ctx = df.style.format("{:.2%}", na_rep="-")._translate(True, True)
6868
assert ctx["body"][0][1]["display_value"] == "-"
6969
assert ctx["body"][0][2]["display_value"] == "-"
7070
assert ctx["body"][1][1]["display_value"] == "110.00%"
7171
assert ctx["body"][1][2]["display_value"] == "120.00%"
7272

73-
ctx = df.style.format("{:.2%}", na_rep="-", subset=["B"])._translate()
73+
ctx = df.style.format("{:.2%}", na_rep="-", subset=["B"])._translate(True, True)
7474
assert ctx["body"][0][2]["display_value"] == "-"
7575
assert ctx["body"][1][2]["display_value"] == "120.00%"
7676

@@ -85,13 +85,13 @@ def test_format_non_numeric_na():
8585
)
8686

8787
with tm.assert_produces_warning(FutureWarning):
88-
ctx = df.style.set_na_rep("NA")._translate()
88+
ctx = df.style.set_na_rep("NA")._translate(True, True)
8989
assert ctx["body"][0][1]["display_value"] == "NA"
9090
assert ctx["body"][0][2]["display_value"] == "NA"
9191
assert ctx["body"][1][1]["display_value"] == "NA"
9292
assert ctx["body"][1][2]["display_value"] == "NA"
9393

94-
ctx = df.style.format(None, na_rep="-")._translate()
94+
ctx = df.style.format(None, na_rep="-")._translate(True, True)
9595
assert ctx["body"][0][1]["display_value"] == "-"
9696
assert ctx["body"][0][2]["display_value"] == "-"
9797
assert ctx["body"][1][1]["display_value"] == "-"
@@ -150,19 +150,19 @@ def test_format_with_precision():
150150
df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"])
151151
s = Styler(df)
152152

153-
ctx = s.format(precision=1)._translate()
153+
ctx = s.format(precision=1)._translate(True, True)
154154
assert ctx["body"][0][1]["display_value"] == "1.0"
155155
assert ctx["body"][0][2]["display_value"] == "2.0"
156156
assert ctx["body"][1][1]["display_value"] == "3.2"
157157
assert ctx["body"][1][2]["display_value"] == "4.6"
158158

159-
ctx = s.format(precision=2)._translate()
159+
ctx = s.format(precision=2)._translate(True, True)
160160
assert ctx["body"][0][1]["display_value"] == "1.00"
161161
assert ctx["body"][0][2]["display_value"] == "2.01"
162162
assert ctx["body"][1][1]["display_value"] == "3.21"
163163
assert ctx["body"][1][2]["display_value"] == "4.57"
164164

165-
ctx = s.format(precision=3)._translate()
165+
ctx = s.format(precision=3)._translate(True, True)
166166
assert ctx["body"][0][1]["display_value"] == "1.000"
167167
assert ctx["body"][0][2]["display_value"] == "2.009"
168168
assert ctx["body"][1][1]["display_value"] == "3.212"
@@ -173,26 +173,28 @@ def test_format_subset():
173173
df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"])
174174
ctx = df.style.format(
175175
{"a": "{:0.1f}", "b": "{0:.2%}"}, subset=IndexSlice[0, :]
176-
)._translate()
176+
)._translate(True, True)
177177
expected = "0.1"
178178
raw_11 = "1.123400"
179179
assert ctx["body"][0][1]["display_value"] == expected
180180
assert ctx["body"][1][1]["display_value"] == raw_11
181181
assert ctx["body"][0][2]["display_value"] == "12.34%"
182182

183-
ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, :])._translate()
183+
ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, :])._translate(True, True)
184184
assert ctx["body"][0][1]["display_value"] == expected
185185
assert ctx["body"][1][1]["display_value"] == raw_11
186186

187-
ctx = df.style.format("{:0.1f}", subset=IndexSlice["a"])._translate()
187+
ctx = df.style.format("{:0.1f}", subset=IndexSlice["a"])._translate(True, True)
188188
assert ctx["body"][0][1]["display_value"] == expected
189189
assert ctx["body"][0][2]["display_value"] == "0.123400"
190190

191-
ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, "a"])._translate()
191+
ctx = df.style.format("{:0.1f}", subset=IndexSlice[0, "a"])._translate(True, True)
192192
assert ctx["body"][0][1]["display_value"] == expected
193193
assert ctx["body"][1][1]["display_value"] == raw_11
194194

195-
ctx = df.style.format("{:0.1f}", subset=IndexSlice[[0, 1], ["a"]])._translate()
195+
ctx = df.style.format("{:0.1f}", subset=IndexSlice[[0, 1], ["a"]])._translate(
196+
True, True
197+
)
196198
assert ctx["body"][0][1]["display_value"] == expected
197199
assert ctx["body"][1][1]["display_value"] == "1.1"
198200
assert ctx["body"][0][2]["display_value"] == "0.123400"
@@ -206,19 +208,19 @@ def test_format_thousands(formatter, decimal, precision):
206208
s = DataFrame([[1000000.123456789]]).style # test float
207209
result = s.format(
208210
thousands="_", formatter=formatter, decimal=decimal, precision=precision
209-
)._translate()
211+
)._translate(True, True)
210212
assert "1_000_000" in result["body"][0][1]["display_value"]
211213

212214
s = DataFrame([[1000000]]).style # test int
213215
result = s.format(
214216
thousands="_", formatter=formatter, decimal=decimal, precision=precision
215-
)._translate()
217+
)._translate(True, True)
216218
assert "1_000_000" in result["body"][0][1]["display_value"]
217219

218220
s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
219221
result = s.format(
220222
thousands="_", formatter=formatter, decimal=decimal, precision=precision
221-
)._translate()
223+
)._translate(True, True)
222224
assert "1_000_000" in result["body"][0][1]["display_value"]
223225

224226

@@ -229,11 +231,11 @@ def test_format_decimal(formatter, thousands, precision):
229231
s = DataFrame([[1000000.123456789]]).style # test float
230232
result = s.format(
231233
decimal="_", formatter=formatter, thousands=thousands, precision=precision
232-
)._translate()
234+
)._translate(True, True)
233235
assert "000_123" in result["body"][0][1]["display_value"]
234236

235237
s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
236238
result = s.format(
237239
decimal="_", formatter=formatter, thousands=thousands, precision=precision
238-
)._translate()
240+
)._translate(True, True)
239241
assert "000_123" in result["body"][0][1]["display_value"]

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def test_set_td_classes_non_unique_raises(styler):
108108

109109

110110
def test_hide_columns_non_unique(styler):
111-
ctx = styler.hide_columns(["d"])._translate()
111+
ctx = styler.hide_columns(["d"])._translate(True, True)
112112

113113
assert ctx["head"][0][1]["display_value"] == "c"
114114
assert ctx["head"][0][1]["is_visible"] is True

0 commit comments

Comments
 (0)