Skip to content

Commit

Permalink
ENH: add styler option context for sparsification of columns and in…
Browse files Browse the repository at this point in the history
…dex separately (pandas-dev#41512)
  • Loading branch information
attack68 authored and JulianWgs committed Jul 3, 2021
1 parent 1b7bbb6 commit 2b05f6e
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 167 deletions.
8 changes: 4 additions & 4 deletions asv_bench/benchmarks/io/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ def setup(self, cols, rows):

def time_apply_render(self, cols, rows):
self._style_apply()
self.st._render_html()
self.st._render_html(True, True)

def peakmem_apply_render(self, cols, rows):
self._style_apply()
self.st._render_html()
self.st._render_html(True, True)

def time_classes_render(self, cols, rows):
self._style_classes()
self.st._render_html()
self.st._render_html(True, True)

def peakmem_classes_render(self, cols, rows):
self._style_classes()
self.st._render_html()
self.st._render_html(True, True)

def time_format_render(self, cols, rows):
self._style_format()
Expand Down
5 changes: 5 additions & 0 deletions doc/source/user_guide/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@ plotting.backend matplotlib Change the plotting backend
like Bokeh, Altair, etc.
plotting.matplotlib.register_converters True Register custom converters with
matplotlib. Set to False to de-register.
styler.sparse.index True "Sparsify" MultiIndex display for rows
in Styler output (don't display repeated
elements in outer levels within groups).
styler.sparse.columns True "Sparsify" MultiIndex display for columns
in Styler output.
======================================= ============ ==================================


Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ precision, and perform HTML escaping (:issue:`40437` :issue:`40134`). There have
properly format HTML and eliminate some inconsistencies (:issue:`39942` :issue:`40356` :issue:`39807` :issue:`39889` :issue:`39627`)

: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`).
One also has greater control of the display through separate sparsification of the index or columns, using the new 'styler' options context (:issue:`41142`).

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

Expand Down
23 changes: 23 additions & 0 deletions pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,3 +735,26 @@ def register_converter_cb(key):
validator=is_one_of_factory(["auto", True, False]),
cb=register_converter_cb,
)

# ------
# Styler
# ------

styler_sparse_index_doc = """
: bool
Whether to sparsify the display of a hierarchical index. Setting to False will
display each explicit level element in a hierarchical key for each row.
"""

styler_sparse_columns_doc = """
: bool
Whether to sparsify the display of hierarchical columns. Setting to False will
display each explicit level element in a hierarchical key for each column.
"""

with cf.config_prefix("styler"):
cf.register_option("sparse.index", True, styler_sparse_index_doc, validator=bool)

cf.register_option(
"sparse.columns", True, styler_sparse_columns_doc, validator=bool
)
25 changes: 22 additions & 3 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import numpy as np

from pandas._config import get_option

from pandas._typing import (
Axis,
FrameOrSeries,
Expand Down Expand Up @@ -201,14 +203,27 @@ def _repr_html_(self) -> str:
"""
Hooks into Jupyter notebook rich display system.
"""
return self._render_html()
return self.render()

def render(self, **kwargs) -> str:
def render(
self,
sparse_index: bool | None = None,
sparse_columns: bool | None = None,
**kwargs,
) -> str:
"""
Render the ``Styler`` including all applied styles to HTML.
Parameters
----------
sparse_index : bool, optional
Whether to sparsify the display of a hierarchical index. Setting to False
will display each explicit level element in a hierarchical key for each row.
Defaults to ``pandas.options.styler.sparse.index`` value.
sparse_columns : bool, optional
Whether to sparsify the display of a hierarchical index. Setting to False
will display each explicit level element in a hierarchical key for each row.
Defaults to ``pandas.options.styler.sparse.columns`` value.
**kwargs
Any additional keyword arguments are passed
through to ``self.template.render``.
Expand Down Expand Up @@ -240,7 +255,11 @@ def render(self, **kwargs) -> str:
* caption
* table_attributes
"""
return self._render_html(**kwargs)
if sparse_index is None:
sparse_index = get_option("styler.sparse.index")
if sparse_columns is None:
sparse_columns = get_option("styler.sparse.columns")
return self._render_html(sparse_index, sparse_columns, **kwargs)

def set_tooltips(
self,
Expand Down
27 changes: 11 additions & 16 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ def __init__(
tuple[int, int], Callable[[Any], str]
] = defaultdict(lambda: partial(_default_formatter, precision=def_precision))

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

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

def _translate(
self, sparsify_index: bool | None = None, sparsify_cols: bool | None = None
):
def _translate(self, sparse_index: bool, sparse_cols: bool):
"""
Process Styler data and settings into a dict for template rendering.
Expand All @@ -144,22 +142,19 @@ def _translate(
Parameters
----------
sparsify_index : bool, optional
Whether to sparsify the index or print all hierarchical index elements
sparsify_cols : bool, optional
Whether to sparsify the columns or print all hierarchical column elements
sparse_index : bool
Whether to sparsify the index or print all hierarchical index elements.
Upstream defaults are typically to `pandas.options.styler.sparse.index`.
sparse_cols : bool
Whether to sparsify the columns or print all hierarchical column elements.
Upstream defaults are typically to `pandas.options.styler.sparse.columns`.
Returns
-------
d : dict
The following structure: {uuid, table_styles, caption, head, body,
cellstyle, table_attributes}
"""
if sparsify_index is None:
sparsify_index = get_option("display.multi_sparse")
if sparsify_cols is None:
sparsify_cols = get_option("display.multi_sparse")

ROW_HEADING_CLASS = "row_heading"
COL_HEADING_CLASS = "col_heading"
INDEX_NAME_CLASS = "index_name"
Expand All @@ -176,14 +171,14 @@ def _translate(
}

head = self._translate_header(
BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS, sparsify_cols
BLANK_CLASS, BLANK_VALUE, INDEX_NAME_CLASS, COL_HEADING_CLASS, sparse_cols
)
d.update({"head": head})

self.cellstyle_map: DefaultDict[tuple[CSSPair, ...], list[str]] = defaultdict(
list
)
body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS, sparsify_index)
body = self._translate_body(DATA_CLASS, ROW_HEADING_CLASS, sparse_index)
d.update({"body": body})

cellstyle: list[dict[str, CSSList | list[str]]] = [
Expand Down
46 changes: 24 additions & 22 deletions pandas/tests/io/formats/style/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,28 @@ def styler(df):


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


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


def test_format_string(styler):
ctx = styler.format("{:.2f}")._translate()
ctx = styler.format("{:.2f}")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "0.00"
assert ctx["body"][0][2]["display_value"] == "-0.61"
assert ctx["body"][1][1]["display_value"] == "1.00"
assert ctx["body"][1][2]["display_value"] == "-1.23"


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

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

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

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

Expand All @@ -85,13 +85,13 @@ def test_format_non_numeric_na():
)

with tm.assert_produces_warning(FutureWarning):
ctx = df.style.set_na_rep("NA")._translate()
ctx = df.style.set_na_rep("NA")._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "NA"
assert ctx["body"][0][2]["display_value"] == "NA"
assert ctx["body"][1][1]["display_value"] == "NA"
assert ctx["body"][1][2]["display_value"] == "NA"

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

ctx = s.format(precision=1)._translate()
ctx = s.format(precision=1)._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "1.0"
assert ctx["body"][0][2]["display_value"] == "2.0"
assert ctx["body"][1][1]["display_value"] == "3.2"
assert ctx["body"][1][2]["display_value"] == "4.6"

ctx = s.format(precision=2)._translate()
ctx = s.format(precision=2)._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "1.00"
assert ctx["body"][0][2]["display_value"] == "2.01"
assert ctx["body"][1][1]["display_value"] == "3.21"
assert ctx["body"][1][2]["display_value"] == "4.57"

ctx = s.format(precision=3)._translate()
ctx = s.format(precision=3)._translate(True, True)
assert ctx["body"][0][1]["display_value"] == "1.000"
assert ctx["body"][0][2]["display_value"] == "2.009"
assert ctx["body"][1][1]["display_value"] == "3.212"
Expand All @@ -173,26 +173,28 @@ def test_format_subset():
df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"])
ctx = df.style.format(
{"a": "{:0.1f}", "b": "{0:.2%}"}, subset=IndexSlice[0, :]
)._translate()
)._translate(True, True)
expected = "0.1"
raw_11 = "1.123400"
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][1][1]["display_value"] == raw_11
assert ctx["body"][0][2]["display_value"] == "12.34%"

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

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

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

ctx = df.style.format("{:0.1f}", subset=IndexSlice[[0, 1], ["a"]])._translate()
ctx = df.style.format("{:0.1f}", subset=IndexSlice[[0, 1], ["a"]])._translate(
True, True
)
assert ctx["body"][0][1]["display_value"] == expected
assert ctx["body"][1][1]["display_value"] == "1.1"
assert ctx["body"][0][2]["display_value"] == "0.123400"
Expand All @@ -206,19 +208,19 @@ def test_format_thousands(formatter, decimal, precision):
s = DataFrame([[1000000.123456789]]).style # test float
result = s.format(
thousands="_", formatter=formatter, decimal=decimal, precision=precision
)._translate()
)._translate(True, True)
assert "1_000_000" in result["body"][0][1]["display_value"]

s = DataFrame([[1000000]]).style # test int
result = s.format(
thousands="_", formatter=formatter, decimal=decimal, precision=precision
)._translate()
)._translate(True, True)
assert "1_000_000" in result["body"][0][1]["display_value"]

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


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

s = DataFrame([[1 + 1000000.123456789j]]).style # test complex
result = s.format(
decimal="_", formatter=formatter, thousands=thousands, precision=precision
)._translate()
)._translate(True, True)
assert "000_123" in result["body"][0][1]["display_value"]
2 changes: 1 addition & 1 deletion pandas/tests/io/formats/style/test_non_unique.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def test_set_td_classes_non_unique_raises(styler):


def test_hide_columns_non_unique(styler):
ctx = styler.hide_columns(["d"])._translate()
ctx = styler.hide_columns(["d"])._translate(True, True)

assert ctx["head"][0][1]["display_value"] == "c"
assert ctx["head"][0][1]["is_visible"] is True
Expand Down
Loading

0 comments on commit 2b05f6e

Please sign in to comment.