From 354c0bdc12934869856f3aaa4ef3a24534f3455d Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Sun, 27 Feb 2022 21:05:06 +0100 Subject: [PATCH] ENH: allow concat of Styler objects (#46105) --- doc/source/_static/style/footer_extended.png | Bin 0 -> 12326 bytes doc/source/_static/style/footer_simple.png | Bin 0 -> 8717 bytes doc/source/reference/style.rst | 1 + doc/source/whatsnew/v1.5.0.rst | 1 + pandas/io/formats/style.py | 87 +++++++++++++++++- pandas/io/formats/style_render.py | 61 ++++++++---- .../tests/io/formats/style/test_exceptions.py | 33 +++++++ pandas/tests/io/formats/style/test_html.py | 21 +++++ pandas/tests/io/formats/style/test_style.py | 1 + .../tests/io/formats/style/test_to_latex.py | 16 ++++ .../tests/io/formats/style/test_to_string.py | 13 +++ 11 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 doc/source/_static/style/footer_extended.png create mode 100644 doc/source/_static/style/footer_simple.png create mode 100644 pandas/tests/io/formats/style/test_exceptions.py diff --git a/doc/source/_static/style/footer_extended.png b/doc/source/_static/style/footer_extended.png new file mode 100644 index 0000000000000000000000000000000000000000..3699d61ad4346e84748334a8fd5e5eb556364154 GIT binary patch literal 12326 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 literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/footer_simple.png b/doc/source/_static/style/footer_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..56dc3c09cc700209a5d2922304a3dc48d48581d0 GIT binary patch literal 8717 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 JDkMGy{4d{@&d2}& literal 0 HcmV?d00001 diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index dd7e2fe7434cd..77e1b0abae0c4 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -42,6 +42,7 @@ Style application Styler.format Styler.format_index Styler.hide + Styler.concat Styler.set_td_classes Styler.set_table_styles Styler.set_table_attributes diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index b472067e28328..527c4215d22ca 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -21,6 +21,7 @@ Styler - New method :meth:`.Styler.to_string` for alternative customisable output methods (:issue:`44502`) - Added the ability to render ``border`` and ``border-{side}`` CSS properties in Excel (:issue:`42276`) + - Added a new method :meth:`.Styler.concat` which allows adding customised footer rows to visualise additional calculations on the data, e.g. totals and counts etc. (:issue:`43875`) .. _whatsnew_150.enhancements.enhancement2: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9d0b213e44671..27f9801ea35e3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -271,6 +271,87 @@ def __init__( thousands=thousands, ) + def concat(self, other: Styler) -> Styler: + """ + Append another Styler to combine the output into a single table. + + .. versionadded:: 1.5.0 + + Parameters + ---------- + other : Styler + The other Styler object which has already been styled and formatted. The + data for this Styler must have the same columns as the original. + + Returns + ------- + self : Styler + + Notes + ----- + The purpose of this method is to extend existing styled dataframes with other + metrics that may be useful but may not conform to the original's structure. + For example adding a sub total row, or displaying metrics such as means, + variance or counts. + + Styles that are applied using the ``apply``, ``applymap``, ``apply_index`` + and ``applymap_index``, and formatting applied with ``format`` and + ``format_index`` will be preserved. + + .. warning:: + Only the output methods ``to_html`` and ``to_string`` currently work with + concatenated Stylers. + + The output methods ``to_latex`` and ``to_excel`` **do not** work with + concatenated Stylers. + + The following should be noted: + + - ``table_styles``, ``table_attributes``, ``caption`` and ``uuid`` are all + inherited from the original Styler and not ``other``. + - hidden columns and hidden index levels will be inherited from the + original Styler + + A common use case is to concatenate user defined functions with + ``DataFrame.agg`` or with described statistics via ``DataFrame.describe``. + See examples. + + Examples + -------- + A common use case is adding totals rows, or otherwise, via methods calculated + in ``DataFrame.agg``. + + >>> df = DataFrame([[4, 6], [1, 9], [3, 4], [5, 5], [9,6]], + ... columns=["Mike", "Jim"], + ... index=["Mon", "Tue", "Wed", "Thurs", "Fri"]) + >>> styler = df.style.concat(df.agg(["sum"]).style) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_simple.png + + Since the concatenated object is a Styler the existing functionality can be + used to conditionally format it as well as the original. + + >>> descriptors = df.agg(["sum", "mean", lambda s: s.dtype]) + >>> descriptors.index = ["Total", "Average", "dtype"] + >>> other = (descriptors.style + ... .highlight_max(axis=1, subset=(["Total", "Average"], slice(None))) + ... .format(subset=("Average", slice(None)), precision=2, decimal=",") + ... .applymap(lambda v: "font-weight: bold;")) + >>> styler = (df.style + ... .highlight_max(color="salmon") + ... .set_table_styles([{"selector": ".foot_row0", + ... "props": "border-top: 1px solid black;"}])) + >>> styler.concat(other) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_extended.png + """ + if not isinstance(other, Styler): + raise TypeError("`other` must be of type `Styler`") + if not self.data.columns.equals(other.data.columns): + raise ValueError("`other.data` must have same columns as `Styler.data`") + self.concatenated = other + return self + def _repr_html_(self) -> str | None: """ Hooks into Jupyter notebook rich display system, which calls _repr_html_ by @@ -1405,6 +1486,7 @@ def _copy(self, deepcopy: bool = False) -> Styler: - cell_context (cell css classes) - ctx (cell css styles) - caption + - concatenated stylers Non-data dependent attributes [copied and exported]: - css @@ -1435,6 +1517,7 @@ def _copy(self, deepcopy: bool = False) -> Styler: ] deep = [ # nested lists or dicts "css", + "concatenated", "_display_funcs", "_display_funcs_index", "_display_funcs_columns", @@ -2348,11 +2431,13 @@ def set_table_styles( "col_heading": "col_heading", "index_name": "index_name", "col": "col", + "row": "row", "col_trim": "col_trim", "row_trim": "row_trim", "level": "level", "data": "data", - "blank": "blank} + "blank": "blank", + "foot": "foot"} Examples -------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8a0b7f21ce023..475e49cb848b5 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -120,8 +120,9 @@ def __init__( "level": "level", "data": "data", "blank": "blank", + "foot": "foot", } - + self.concatenated: StylerRenderer | None = None # add rendering variables self.hide_index_names: bool = False self.hide_column_names: bool = False @@ -148,6 +149,35 @@ def __init__( tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) + def _render( + self, + sparse_index: bool, + sparse_columns: bool, + max_rows: int | None = None, + max_cols: int | None = None, + blank: str = "", + ): + """ + Computes and applies styles and then generates the general render dicts + """ + self._compute() + dx = None + if self.concatenated is not None: + self.concatenated.hide_index_ = self.hide_index_ + self.concatenated.hidden_columns = self.hidden_columns + self.concatenated.css = { + **self.css, + "data": f"{self.css['foot']}_{self.css['data']}", + "row_heading": f"{self.css['foot']}_{self.css['row_heading']}", + "row": f"{self.css['foot']}_{self.css['row']}", + "foot": self.css["foot"], + } + dx, _ = self.concatenated._render( + sparse_index, sparse_columns, max_rows, max_cols, blank + ) + d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank, dx) + return d, dx + def _render_html( self, sparse_index: bool, @@ -160,9 +190,7 @@ def _render_html( 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(sparse_index, sparse_columns, max_rows, max_cols) + d, _ = self._render(sparse_index, sparse_columns, max_rows, max_cols, " ") d.update(kwargs) return self.template_html.render( **d, @@ -176,16 +204,12 @@ def _render_latex( """ Render a Styler in latex format """ - self._compute() - - d = self._translate(sparse_index, sparse_columns, blank="") + d, _ = self._render(sparse_index, sparse_columns, None, None) self._translate_latex(d, clines=clines) - self.template_latex.globals["parse_wrap"] = _parse_latex_table_wrapping self.template_latex.globals["parse_table"] = _parse_latex_table_styles self.template_latex.globals["parse_cell"] = _parse_latex_cell_styles self.template_latex.globals["parse_header"] = _parse_latex_header_span - d.update(kwargs) return self.template_latex.render(**d) @@ -200,10 +224,7 @@ def _render_string( """ Render a Styler in string format """ - self._compute() - - d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank="") - + d, _ = self._render(sparse_index, sparse_columns, max_rows, max_cols) d.update(kwargs) return self.template_string.render(**d) @@ -231,6 +252,7 @@ def _translate( max_rows: int | None = None, max_cols: int | None = None, blank: str = " ", + dx: dict | None = None, ): """ Process Styler data and settings into a dict for template rendering. @@ -246,10 +268,12 @@ def _translate( sparse_cols : bool Whether to sparsify the columns or print all hierarchical column elements. Upstream defaults are typically to `pandas.options.styler.sparse.columns`. - blank : str - Entry to top-left blank cells. max_rows, max_cols : int, optional Specific max rows and cols. max_elements always take precedence in render. + blank : str + Entry to top-left blank cells. + dx : dict + The render dict of the concatenated Styler. Returns ------- @@ -295,7 +319,7 @@ def _translate( self.cellstyle_map_index: DefaultDict[ tuple[CSSPair, ...], list[str] ] = defaultdict(list) - body = self._translate_body(idx_lengths, max_rows, max_cols) + body: list = self._translate_body(idx_lengths, max_rows, max_cols) d.update({"body": body}) ctx_maps = { @@ -310,6 +334,11 @@ def _translate( ] d.update({k: map}) + if dx is not None: # self.concatenated is not None + d["body"].extend(dx["body"]) # type: ignore[union-attr] + d["cellstyle"].extend(dx["cellstyle"]) # type: ignore[union-attr] + d["cellstyle_index"].extend(dx["cellstyle"]) # type: ignore[union-attr] + table_attr = self.table_attributes if not get_option("styler.html.mathjax"): table_attr = table_attr or "" diff --git a/pandas/tests/io/formats/style/test_exceptions.py b/pandas/tests/io/formats/style/test_exceptions.py new file mode 100644 index 0000000000000..b9f6662ed92cc --- /dev/null +++ b/pandas/tests/io/formats/style/test_exceptions.py @@ -0,0 +1,33 @@ +import pytest + +jinja2 = pytest.importorskip("jinja2") + +from pandas import DataFrame + +from pandas.io.formats.style import Styler + + +@pytest.fixture +def df(): + return DataFrame( + data=[[0, -0.609], [1, -1.228]], + columns=["A", "B"], + index=["x", "y"], + ) + + +@pytest.fixture +def styler(df): + return Styler(df, uuid_len=0) + + +def test_concat_bad_columns(styler): + msg = "`other.data` must have same columns as `Styler.data" + with pytest.raises(ValueError, match=msg): + styler.concat(DataFrame([[1, 2]]).style) + + +def test_concat_bad_type(styler): + msg = "`other` must be of type `Styler`" + with pytest.raises(TypeError, match=msg): + styler.concat(DataFrame([[1, 2]])) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 2010d06c9d22d..2abc963525977 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -804,3 +804,24 @@ def test_multiple_rendered_links(): for link in links: assert href.format(link) in result assert href.format("text") not in result + + +def test_concat(styler): + other = styler.data.agg(["mean"]).style + styler.concat(other).set_uuid("X") + result = styler.to_html() + expected = dedent( + """\ + + b + 2.690000 + + + mean + 2.650000 + + + + """ + ) + assert expected in result diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 157d046590535..e8187f7e8871c 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -56,6 +56,7 @@ def mi_styler_comp(mi_styler): mi_styler.hide(axis="index") mi_styler.hide([("i0", "i1_a")], axis="index", names=True) mi_styler.set_table_attributes('class="box"') + mi_styler.concat(mi_styler.data.agg(["mean"]).style) mi_styler.format(na_rep="MISSING", precision=3) mi_styler.format_index(precision=2, axis=0) mi_styler.format_index(precision=4, axis=1) diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 2cffcb1843fcf..387cd714c69d1 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -997,3 +997,19 @@ def test_col_format_len(styler): result = styler.to_latex(environment="longtable", column_format="lrr{10cm}") expected = r"\multicolumn{4}{r}{Continued on next page} \\" assert expected in result + + +@pytest.mark.xfail # concat not yet implemented for to_latex +def test_concat(styler): + result = styler.concat(styler.data.agg(["sum"]).style).to_latex() + expected = dedent( + """\ + \\begin{tabular}{lrrl} + & A & B & C \\\\ + 0 & 0 & -0.61 & ab \\\\ + 1 & 1 & -1.22 & cd \\\\ + sum & 1 & -1.830000 & abcd \\\\ + \\end{tabular} + """ + ) + assert result == expected diff --git a/pandas/tests/io/formats/style/test_to_string.py b/pandas/tests/io/formats/style/test_to_string.py index 5b3e0079bd95c..fcac304b8c3bb 100644 --- a/pandas/tests/io/formats/style/test_to_string.py +++ b/pandas/tests/io/formats/style/test_to_string.py @@ -40,3 +40,16 @@ def test_string_delimiter(styler): """ ) assert result == expected + + +def test_concat(styler): + result = styler.concat(styler.data.agg(["sum"]).style).to_string() + expected = dedent( + """\ + A B C + 0 0 -0.61 ab + 1 1 -1.22 cd + sum 1 -1.830000 abcd + """ + ) + assert result == expected