From 4cfb671d84ac48e3ca6e8e63664f5ea0b889aa7d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 08:51:35 +0100 Subject: [PATCH 01/44] skeleton --- doc/source/reference/style.rst | 1 + doc/source/whatsnew/v1.5.0.rst | 1 + pandas/io/formats/style.py | 56 +++++++++++++++++++++++++++++-- pandas/io/formats/style_render.py | 16 +++++++-- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index dd7e2fe7434cd..abb17c89b0b78 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.set_footer 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 c8b2617ffc535..1515e790e14f7 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.set_footer` which allows adding customised footer rows to explore and make 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..0d60907a4e01f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -54,6 +54,7 @@ from pandas.io.formats.style_render import ( CSSProperties, CSSStyles, + Descriptor, ExtFormatter, StylerRenderer, Subset, @@ -1435,6 +1436,7 @@ def _copy(self, deepcopy: bool = False) -> Styler: ] deep = [ # nested lists or dicts "css", + "descriptors", "_display_funcs", "_display_funcs_index", "_display_funcs_columns", @@ -1977,6 +1979,9 @@ def export(self) -> dict[str, Any]: Can be applied to a second Styler with ``Styler.use``. + .. versionchanged:: 1.5.0 + Adds ``descriptors`` to the exported items for use with ``set_footer``. + Returns ------- styles : dict @@ -1995,9 +2000,10 @@ def export(self) -> dict[str, Any]: The following items are exported since they are not generally data dependent: - Styling functions added by the ``apply`` and ``applymap`` - - Whether axes and names are hidden from the display, if unambiguous. + - Whether axes and names are hidden from the display, if unambiguous - Table attributes - Table styles + - Descriptors, i.e. from ``set_footer`` The following attributes are considered data dependent and therefore not exported: @@ -2027,6 +2033,7 @@ def export(self) -> dict[str, Any]: "hide_index_names": self.hide_index_names, "hide_column_names": self.hide_column_names, "css": copy.copy(self.css), + "descriptors": copy.copy(self.descriptors), } def use(self, styles: dict[str, Any]) -> Styler: @@ -2035,6 +2042,9 @@ def use(self, styles: dict[str, Any]) -> Styler: Possibly uses styles from ``Styler.export``. + .. versionchanged:: 1.5.0 + Adds ``descriptors`` to the used items for use with ``set_footer``. + Parameters ---------- styles : dict(str, Any) @@ -2052,6 +2062,7 @@ def use(self, styles: dict[str, Any]) -> Styler: - "hide_index_names": whether index names are hidden. - "hide_column_names": whether column header names are hidden. - "css": the css class names used. + - "descriptors": list of descriptors, typically added with ``set_footer``. Returns ------- @@ -2094,6 +2105,8 @@ def use(self, styles: dict[str, Any]) -> Styler: self.hide_column_names = styles.get("hide_column_names", False) if styles.get("css"): self.css = styles.get("css") # type: ignore[assignment] + if styles.get("descriptors"): + self.set_footer(styles.get("descriptors")) return self def set_uuid(self, uuid: str) -> Styler: @@ -2352,7 +2365,10 @@ def set_table_styles( "row_trim": "row_trim", "level": "level", "data": "data", - "blank": "blank} + "blank": "blank", + "descriptor": "descriptor", + "descriptor_name": "descriptor_name", + "descriptor_value": "descriptor_value"} Examples -------- @@ -2423,6 +2439,42 @@ def set_table_styles( self.table_styles = table_styles return self + def set_descriptors( + self, descriptors: list[Descriptor | tuple[str, Descriptor]] | None = None + ) -> Styler: + """ + Add header-level calculations to the output which describes the data. + .. versionadded:: 1.5.0 + Parameters + ---------- + descriptors : list of str, callables or 2-tuples of str and callable + If a string is given must be a valid Series method, e.g. "mean" invokes + Series.mean(). + If a callable is given must accept a Series and return a scalar. + If a 2-tuple, must be a string used as the name of the row and a + callable or string as above. + Returns + ------- + self : Styler + Examples + -------- + >>> df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) + >>> def udf_func(s): + ... return s.mean() + >>> styler = df.style.set_descriptors([ + ... "mean", + ... Series.mean, + ... ("my-text", "mean"), + ... ("my-text2", Series.mean), + ... ("my-func", lambda s: s.sum()/2), + ... lambda s: s.sum()/2, + ... udf_func, + ... ]) # doctest: +SKIP + .. figure:: ../../_static/style/des_mean.png + """ + self.descriptors = descriptors if descriptors is not None else [] + return self + def set_na_rep(self, na_rep: str) -> StylerRenderer: """ Set the missing data representation on a ``Styler``. diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 2e90074a6bbd3..20d541add8818 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -25,6 +25,11 @@ from pandas._typing import Level from pandas.compat._optional import import_optional_dependency +from pandas.core.dtypes.common import ( + is_complex, + is_float, + is_integer, +) from pandas.core.dtypes.generic import ABCSeries from pandas import ( @@ -46,6 +51,7 @@ CSSPair = Tuple[str, Union[str, int, float]] CSSList = List[CSSPair] CSSProperties = Union[str, CSSList] +Descriptor = Union[str, Callable[[Series], Any]] class CSSDict(TypedDict): @@ -115,6 +121,9 @@ def __init__( "level": "level", "data": "data", "blank": "blank", + "descriptor": "descriptor", + "descriptor_value": "descriptor_value", + "descriptor_name": "descriptor_name", } # add rendering variables @@ -124,6 +133,7 @@ def __init__( self.hide_columns_: list = [False] * self.columns.nlevels self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols self.hidden_columns: Sequence[int] = [] + self.descriptors: list[Descriptor | tuple[str, Descriptor]] = [] self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) @@ -1442,9 +1452,9 @@ def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any: value : Any Matches input type, or string if input is float or complex or int with sep. """ - if isinstance(x, (float, complex)): + if is_float(x) or is_complex(x): return f"{x:,.{precision}f}" if thousands else f"{x:.{precision}f}" - elif isinstance(x, int): + elif is_integer(x): return f"{x:,.0f}" if thousands else f"{x:.0f}" return x @@ -1459,7 +1469,7 @@ def _wrap_decimal_thousands( """ def wrapper(x): - if isinstance(x, (float, complex, int)): + if is_float(x) or is_integer(x) or is_complex(x): if decimal != "." and thousands is not None and thousands != ",": return ( formatter(x) From ba3d4218b9f62a4a400a375fccc1cbcb126a2479 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 13:49:44 +0100 Subject: [PATCH 02/44] html template --- pandas/io/formats/style.py | 57 ++++-- pandas/io/formats/style_render.py | 195 ++++++++++++++++++++- pandas/io/formats/templates/html_table.tpl | 19 ++ 3 files changed, 258 insertions(+), 13 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 0d60907a4e01f..dd36f784ae31e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -54,7 +54,6 @@ from pandas.io.formats.style_render import ( CSSProperties, CSSStyles, - Descriptor, ExtFormatter, StylerRenderer, Subset, @@ -2105,8 +2104,13 @@ def use(self, styles: dict[str, Any]) -> Styler: self.hide_column_names = styles.get("hide_column_names", False) if styles.get("css"): self.css = styles.get("css") # type: ignore[assignment] - if styles.get("descriptors"): - self.set_footer(styles.get("descriptors")) + if styles.get("descriptors") and styles["descriptors"]["methods"]: + self.set_footer( + func=styles["descriptors"]["methods"], + alias=styles["descriptors"]["names"], + format_kwargs=styles["descriptors"]["format"], + errors=styles["descriptors"]["errors"], + ) return self def set_uuid(self, uuid: str) -> Styler: @@ -2439,29 +2443,43 @@ def set_table_styles( self.table_styles = table_styles return self - def set_descriptors( - self, descriptors: list[Descriptor | tuple[str, Descriptor]] | None = None + def set_footer( + self, + func: Sequence[str | Callable] | None = None, + alias: Sequence[str] | None = None, + errors: str = "ignore", + format_kwargs: dict[str, Any] = {}, ) -> Styler: """ - Add header-level calculations to the output which describes the data. + Add footer-level calculations to the output which describes the data. + .. versionadded:: 1.5.0 + Parameters ---------- - descriptors : list of str, callables or 2-tuples of str and callable + func : list-like of str or callable If a string is given must be a valid Series method, e.g. "mean" invokes Series.mean(). If a callable is given must accept a Series and return a scalar. - If a 2-tuple, must be a string used as the name of the row and a - callable or string as above. + alias : list-like of str, optional + Aliases to use for the function names. Must have length equal to ``func``. + errors : {"ignore", "warn", "raise"} + If errors, will be ignored or warned returning ``NA``, or raise. + format_kwargs : dict + Keyword args to pass to the formatting function. See ``Styler.format``. + The ``formatter`` can be given as str, callable or list, where a list + must have the same length as ``func``. + Returns ------- self : Styler + Examples -------- >>> df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) >>> def udf_func(s): ... return s.mean() - >>> styler = df.style.set_descriptors([ + >>> styler = df.style.set_footer([ ... "mean", ... Series.mean, ... ("my-text", "mean"), @@ -2472,7 +2490,24 @@ def set_descriptors( ... ]) # doctest: +SKIP .. figure:: ../../_static/style/des_mean.png """ - self.descriptors = descriptors if descriptors is not None else [] + if func is not None: + if alias is not None and len(alias) != len(func): + raise ValueError("``alias`` must have same length as ``func``") + + if isinstance(format_kwargs.get("formatter", None), list) and len( + format_kwargs["formatter"] + ) != len(func): + raise ValueError( + "``formatter`` key of ``format_kwargs`` as list must have " + "same length as ``func``" + ) + + self.descriptors = { + "methods": func, + "names": alias, + "errors": errors, + "format": format_kwargs, + } return self def set_na_rep(self, na_rep: str) -> StylerRenderer: diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 20d541add8818..4088670ab9569 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -16,6 +16,7 @@ Union, ) from uuid import uuid4 +import warnings import numpy as np @@ -33,6 +34,7 @@ from pandas.core.dtypes.generic import ABCSeries from pandas import ( + NA, DataFrame, Index, IndexSlice, @@ -51,7 +53,13 @@ CSSPair = Tuple[str, Union[str, int, float]] CSSList = List[CSSPair] CSSProperties = Union[str, CSSList] -Descriptor = Union[str, Callable[[Series], Any]] + + +class Descriptors(TypedDict): + methods: Sequence[str | Callable] | None + names: Sequence[str] | None + format: dict[str, Any] + errors: str class CSSDict(TypedDict): @@ -133,7 +141,12 @@ def __init__( self.hide_columns_: list = [False] * self.columns.nlevels self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols self.hidden_columns: Sequence[int] = [] - self.descriptors: list[Descriptor | tuple[str, Descriptor]] = [] + self.descriptors: Descriptors = { + "methods": None, + "names": None, + "format": {}, + "errors": "ignore", + } self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) @@ -288,6 +301,9 @@ def _translate( head = self._translate_header(sparse_cols, max_cols) d.update({"head": head}) + foot = self._translate_footer(max_cols) + d.update({"foot": foot}) + # for sparsifying a MultiIndex and for use with latex clines idx_lengths = _get_level_lengths( self.index, sparse_index, max_rows, self.hidden_rows @@ -389,6 +405,88 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int): return head + def _translate_footer(self, max_cols: int): + """ + Build each within table as a list + + Using the structure: + +----------------------------+---------------+---------------------------+ + | index_blanks ... | name_0 | column_values | + 1) | .. | .. | .. | + | index_blanks ... | name_n | column_values | + +----------------------------+---------------+---------------------------+ + + Parameters + ---------- + max_cols : int + Maximum number of columns to render. If exceeded will contain `...` filler. + + Returns + ------- + foot : list + The associated HTML elements needed for template rendering. + """ + if self.descriptors["methods"] is None: + foot = None + else: + foot = [] + descriptors, names = self._calc_wrapped_descriptor_methods( + self.descriptors["methods"], self.descriptors["names"] + ) + for r, row in enumerate(descriptors.itertuples()): + descriptor_row = self._generate_footer_row((r, row, names[r]), max_cols) + foot.append(descriptor_row) + return foot + + def _calc_wrapped_descriptor_methods( + self, methods: Sequence[str | Callable], names: Sequence[str] | None + ) -> tuple[DataFrame, list[str | None]]: + """ + Use DataFrame.agg to calculate the UDF methods displayed in footer + + Wraps UDFs so they do not raise errors on, for example, non-conforming dtypes. + + Returns + ------- + DataFrame, list + """ + from functools import update_wrapper + + def _err_wrap(s: Series, method: Callable): + if not isinstance(s, Series): # called to trigger `agg` to use `df.apply` + raise TypeError("`agg` requires Series to reduce") # gh 45800 + try: + ret = method(s) + except Exception as e: + if self.descriptors["errors"] == "ignore": + return NA + elif self.descriptors["errors"] == "warn": + warnings.warn( + "``set_footer`` raised Exception when calculating a column", + Warning, + ) + return NA + else: + raise e + else: + return ret + + methods_: list[Callable] = [ + getattr(Series, method) if isinstance(method, str) else method + for method in methods + ] + for i, method in enumerate(methods_): + wrapper = partial(_err_wrap, method=method) + update_wrapper(wrapper, method) + methods_[i] = wrapper + + if names is not None: + names_: list[str | None] = list(names) + else: + names_ = [method.__name__ for method in methods_] + + return self.data.agg(methods_), names_ + def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict): """ Generate the row containing column headers: @@ -796,6 +894,99 @@ def _generate_body_row( return index_headers + data + def _generate_footer_row(self, iter: tuple, max_cols: int): + """ + Generate the row containing calculated descriptor values for columns: + + +----------------------------+---------------+---------------------------+ + | index_blanks ... | descriptor_i | value_i by col | + +----------------------------+---------------+---------------------------+ + + Parameters + ---------- + iter : tuple + Looping variables from outer scope. + max_cols : int + Permissible number of columns. + + Returns + ------- + list of elements + """ + + r, row, name = iter + + # number of index blanks is governed by number of hidden index levels + index_blanks = [ + _element("th", self.css["blank"], self.css["blank_value"], True) + ] * (self.index.nlevels - sum(self.hide_index_) - 1) + + # name cell + base_css = f"{self.css['descriptor_name']} {self.css['descriptor']}{r}" + if name is not None and not self.hide_column_names: + name_css = base_css + name_val = name + else: + name_css = f"{self.css['blank']} {base_css}" + name_val = self.css["blank_value"] + descriptor_name = _element("th", name_css, name_val, not all(self.hide_index_)) + + # descriptor values + format_ = { + "formatter": None, + "decimal": get_option("styler.format.decimal"), + "thousands": get_option("styler.format.thousands"), + "precision": get_option("styler.format.precision"), + "na_rep": get_option("styler.format.na_rep"), + "escape": get_option("styler.format.escape"), + **self.descriptors["format"], + } # set defaults from Styler options + if isinstance(self.descriptors["format"].get("formatter", None), list): + format_["formatter"] = self.descriptors["format"]["formatter"][r] + display_func: Callable = _maybe_wrap_formatter( + formatter=format_["formatter"], + decimal=format_["decimal"], # type: ignore[arg-type] + thousands=format_["thousands"], + precision=format_["precision"], + na_rep=format_["na_rep"], + escape=format_["escape"], + ) + descriptor_values: list[Any] = [] + visible_col_count = 0 + for c, col in enumerate(self.columns): + if c not in self.hidden_columns: + header_element_visible = True + visible_col_count += 1 + header_element_value = row[c + 1] + else: + header_element_visible = False + header_element_value = None + + if self._check_trim( + visible_col_count, + max_cols, + descriptor_values, + "td", + f"{self.css['descriptor_value']} {self.css['descriptor']}{r} " + f"{self.css['col_trim']}", + ): + break + + body_element = _element( + "th", + ( + f"{self.css['descriptor_value']} {self.css['descriptor']}{r} " + f"{self.css['col']}{c}" + ), + header_element_value, + header_element_visible, + display_value=display_func(header_element_value), + attributes="", + ) + descriptor_values.append(body_element) + + return index_blanks + [descriptor_name] + descriptor_values + def _translate_latex(self, d: dict, clines: str | None) -> None: r""" Post-process the default render dict for the LaTeX template format. diff --git a/pandas/io/formats/templates/html_table.tpl b/pandas/io/formats/templates/html_table.tpl index 17118d2bb21cc..2294ef4f935e3 100644 --- a/pandas/io/formats/templates/html_table.tpl +++ b/pandas/io/formats/templates/html_table.tpl @@ -58,6 +58,25 @@ {% block after_rows %}{% endblock after_rows %} {% endblock tbody %} +{% block tfoot %} +{% if foot is not none %} + +{% for r in foot %} + +{% if exclude_styles %} +{% for c in r %}{% if c.is_visible != False %} + <{{c.type}} {{c.attributes}}>{{c.display_value}} +{% endif %}{% endfor %} +{% else %} +{% for c in r %}{% if c.is_visible != False %} + <{{c.type}} {%- if c.id is defined %} id="T_{{uuid}}_{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} +{% endif %}{% endfor %} +{% endif %} + +{% endfor %} + +{% endif %} +{% endblock tfoot %} {% endblock table %} {% block after_table %}{% endblock after_table %} From 2f36ceff7310e5386ce07ae8ea710ea1a72d5932 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 13:55:21 +0100 Subject: [PATCH 03/44] test internals --- pandas/tests/io/formats/style/test_html.py | 1 + pandas/tests/io/formats/style/test_style.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 2010d06c9d22d..91acd254f8c47 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -234,6 +234,7 @@ def test_block_names(tpl_style, tpl_table): "after_head_rows", "before_rows", "tr", + "tfoot", "after_rows", } result1 = set(tpl_style.blocks) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 157d046590535..1a2f365562940 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.set_footer(["mean"]) mi_styler.format(na_rep="MISSING", precision=3) mi_styler.format_index(precision=2, axis=0) mi_styler.format_index(precision=4, axis=1) @@ -346,6 +347,7 @@ def test_export(mi_styler_comp, mi_styler): "table_attributes", "table_styles", "css", + "descriptors", ] for attr in exp_attrs: check = getattr(mi_styler, attr) == getattr(mi_styler_comp, attr) From 3927adc3b72df5a5301aeda2e8a9b6526809aca3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 13:57:39 +0100 Subject: [PATCH 04/44] additional tests --- pandas/tests/io/formats/style/test_html.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 91acd254f8c47..470e1e91191c4 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -615,6 +615,7 @@ def test_hiding_index_columns_multiindex_alignment(): styler.hide(level=1, axis=0).hide(level=0, axis=1) styler.hide([("j0", "i1", "j2")], axis=0) styler.hide([("c0", "d1", "d2")], axis=1) + styler.set_footer(["mean"]) result = styler.to_html() expected = dedent( """\ @@ -665,6 +666,15 @@ def test_hiding_index_columns_multiindex_alignment(): 10 + + +   + mean + 6.000000 + 7.000000 + 8.000000 + + """ ) From 8d6dabcd0580eed26f7f7e88ba302000d3b33379 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 14:02:26 +0100 Subject: [PATCH 05/44] additional tests --- pandas/io/formats/style_render.py | 2 +- pandas/tests/io/formats/style/test_html.py | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 4088670ab9569..f5c5927d6ead7 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -966,7 +966,7 @@ def _generate_footer_row(self, iter: tuple, max_cols: int): visible_col_count, max_cols, descriptor_values, - "td", + "th", f"{self.css['descriptor_value']} {self.css['descriptor']}{r} " f"{self.css['col_trim']}", ): diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 470e1e91191c4..f0614d2df01cd 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -689,7 +689,15 @@ def test_hiding_index_columns_multiindex_trimming(): df.index.names, df.columns.names = ["a", "b"], ["c", "d"] styler = Styler(df, cell_ids=False, uuid_len=0) styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0) - with option_context("styler.render.max_rows", 4, "styler.render.max_columns", 4): + styler.set_footer(["mean"]) + with option_context( + "styler.render.max_rows", + 4, + "styler.render.max_columns", + 4, + "styler.format.precision", + 0, + ): result = styler.to_html() expected = dedent( @@ -770,10 +778,20 @@ def test_hiding_index_columns_multiindex_trimming(): ... + + +   + mean + 31 + 32 + 33 + 34 + ... + + """ ) - assert result == expected From 56262b86aa43cf22d7d1c4b2584661b0f8a0e48e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 14:09:40 +0100 Subject: [PATCH 06/44] additional tests --- pandas/tests/io/formats/style/test_style.py | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 1a2f365562940..3d9bfa314ebea 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1560,3 +1560,33 @@ def test_no_empty_apply(mi_styler): # 45313 mi_styler.apply(lambda s: ["a:v;"] * 2, subset=[False, False]) mi_styler._compute() + + +@pytest.mark.parametrize("alias", [None, ["mean", "average", "lambda", "udf"]]) +def test_set_footer(mi_styler, alias): + def udf_func(s): + return s.mean() + + mi_styler.set_footer( + [ + "mean", + Series.mean, + lambda s: s.sum() / len(s), + udf_func, + ], + alias=alias, + ) + ctx = mi_styler._translate(True, True) + assert len(ctx["foot"]) == 4 # 4 descriptors + + exp_label = alias if alias is not None else ["mean", "mean", "", "udf_func"] + for r, row in enumerate(ctx["foot"]): + for c, col in enumerate(row[2:]): # iterate after row headers + result = {k: col[k] for k in ["type", "is_visible", "value"]} + assert ( + result.items() <= ctx["foot"][0][c + 2].items() + ) # test rows 3,4,5 are equivalent to row 2 in value, type and visibility + assert col["class"] == f"descriptor_value descriptor{r} col{c}" # test css + + assert row[1]["value"] == exp_label[r] # test label is printed + assert f"descriptor_name descriptor{r}" in row[1]["class"] # test css From aeeca3bdf56e301ab1807135f1fa6379d3a93aac Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 14:13:10 +0100 Subject: [PATCH 07/44] additional tests --- pandas/tests/io/formats/style/test_format.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 5207be992d606..ed7576d5458b7 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -434,3 +434,21 @@ def test_1level_multiindex(): assert ctx["body"][0][0]["is_visible"] is True assert ctx["body"][1][0]["display_value"] == "2" assert ctx["body"][1][0]["is_visible"] is True + + +def test_format_footer(styler): + with option_context( + "styler.format.precision", + 5, + "styler.format.decimal", + "*", + "styler.format.thousands", + "_", + ): + styler.set_footer([lambda s: s.sum() + 1000]) + ctx = styler._translate(True, True) + + exp_col_1 = {"value": 1001, "display_value": "1_001"} + assert exp_col_1.items() <= ctx["foot"][0][1].items() + exp_col_2 = {"value": 998.163, "display_value": "998*16300"} + assert exp_col_2.items() <= ctx["foot"][0][2].items() From f8cf8afa62841d03d94361475c751e1236c4994c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 14:20:29 +0100 Subject: [PATCH 08/44] additional tests --- pandas/tests/io/formats/style/test_format.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index ed7576d5458b7..ef69b766b0ac8 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -436,7 +436,14 @@ def test_1level_multiindex(): assert ctx["body"][1][0]["is_visible"] is True -def test_format_footer(styler): +@pytest.mark.parametrize( + "format_kwargs, exp", + [ + ({}, ["1_001", "998*16300"]), + ({"precision": 3, "decimal": "+", "thousands": ">"}, ["1>001", "998+163"]), + ], +) +def test_format_footer_option_context(styler, format_kwargs, exp): with option_context( "styler.format.precision", 5, @@ -445,10 +452,10 @@ def test_format_footer(styler): "styler.format.thousands", "_", ): - styler.set_footer([lambda s: s.sum() + 1000]) + styler.set_footer([lambda s: s.sum() + 1000], format_kwargs=format_kwargs) ctx = styler._translate(True, True) - exp_col_1 = {"value": 1001, "display_value": "1_001"} + exp_col_1 = {"value": 1001, "display_value": exp[0]} assert exp_col_1.items() <= ctx["foot"][0][1].items() - exp_col_2 = {"value": 998.163, "display_value": "998*16300"} + exp_col_2 = {"value": 998.163, "display_value": exp[1]} assert exp_col_2.items() <= ctx["foot"][0][2].items() From 77033433b34a814effc7b76422e5b32083766aa0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 14:21:08 +0100 Subject: [PATCH 09/44] additional tests --- pandas/tests/io/formats/style/test_format.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index ef69b766b0ac8..2cf3d9ee11c99 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -443,7 +443,8 @@ def test_1level_multiindex(): ({"precision": 3, "decimal": "+", "thousands": ">"}, ["1>001", "998+163"]), ], ) -def test_format_footer_option_context(styler, format_kwargs, exp): +def test_format_footer(styler, format_kwargs, exp): + # test explicit input and option context values with option_context( "styler.format.precision", 5, From 5d7029367fef015adb52bc281d86e63d24909718 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 14:40:20 +0100 Subject: [PATCH 10/44] errors testing --- pandas/io/formats/style_render.py | 11 +++++----- pandas/tests/io/formats/style/test_style.py | 24 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index f5c5927d6ead7..eb2080930d962 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -458,16 +458,17 @@ def _err_wrap(s: Series, method: Callable): try: ret = method(s) except Exception as e: + msg = ( + "`Styler.set_footer` raised Exception when calculating method " + f"`{method.__name__}` on column `{s.name}`" + ) if self.descriptors["errors"] == "ignore": return NA elif self.descriptors["errors"] == "warn": - warnings.warn( - "``set_footer`` raised Exception when calculating a column", - Warning, - ) + warnings.warn(msg, Warning) return NA else: - raise e + raise Exception(msg) from e else: return ret diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 3d9bfa314ebea..2ba6152fd8fc8 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1563,7 +1563,7 @@ def test_no_empty_apply(mi_styler): @pytest.mark.parametrize("alias", [None, ["mean", "average", "lambda", "udf"]]) -def test_set_footer(mi_styler, alias): +def test_set_footer_methods_names(mi_styler, alias): def udf_func(s): return s.mean() @@ -1590,3 +1590,25 @@ def udf_func(s): assert row[1]["value"] == exp_label[r] # test label is printed assert f"descriptor_name descriptor{r}" in row[1]["class"] # test css + + +def test_set_footer_warn(): + df = DataFrame([["a"]]) + styler = df.style.set_footer(["mean"], errors="warn") + msg = ( + "`Styler.set_footer` raised Exception when calculating method `mean` on " + "column `0`" + ) + with tm.assert_produces_warning(Warning, match=msg): + styler._translate(True, True) + + +def test_set_footer_raise(): + df = DataFrame([["a"]]) + styler = df.style.set_footer(["mean"], errors="raise") + msg = ( + "`Styler.set_footer` raised Exception when calculating method `mean` on " + "column `0`" + ) + with pytest.raises(Exception, match=msg): + styler._translate(True, True) From 6811f4b46c1d57e789c1b1ed1174d2da49c534d9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 19 Feb 2022 16:02:37 +0100 Subject: [PATCH 11/44] errors testing --- .../_static/style/footer_hypothesis.png | Bin 0 -> 32678 bytes doc/source/_static/style/footer_simple.png | Bin 0 -> 8494 bytes doc/source/_static/style/footer_stats.png | Bin 0 -> 19342 bytes pandas/io/formats/style.py | 60 ++++++++++++++---- 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 doc/source/_static/style/footer_hypothesis.png create mode 100644 doc/source/_static/style/footer_simple.png create mode 100644 doc/source/_static/style/footer_stats.png diff --git a/doc/source/_static/style/footer_hypothesis.png b/doc/source/_static/style/footer_hypothesis.png new file mode 100644 index 0000000000000000000000000000000000000000..18d32097131bfa1125f029d61af43fd16d724b33 GIT binary patch literal 32678 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

6Pg+pdNCsKrYIRzJU&bx9>cLFnd|@N zX=!VlqORvD24sU%EFoF9ZOeIiobhKI6Vc2ia{Pf}Mq>Cywb6vJV;^-cl)e(%$Q2<+ zA{O8BfKC&=NIZsxF5u0+;~@2F)i_Rb^Zn&WAgCpfLY&CH_w}wAK|}8O@AtZ~KgCIM zsJhHo!2C@-y-#s&jaruFP*&0(>?g@{On9{Ya%=J%6x0VZv+cj16+y!AD1o7Sisk(q zKZqotW7mDFhQs4h{_?!_o~l*TG>V49f4iu>Jd9`8PC~h!amjZ*$!Z(@TR`nOoXF2& z<_3b;`%go7OWtEb?R!a@ULufh!z5S?*7u?x>MG9vWw6$L&63KR^EJ#q83pMW&oo0> zBj%WU=vso%Q&a_XA{dFEn)8jG!AG8l$1F3>7)%z}EYlyNn2?_gPC?OP+N#i|WSRpX0JT z4(=qxEeq2MC0LE&n>1!cG4mBZ`6N4(PpPBUb={aZ5GaMNcfbW&*5v9*z|h2XHyLW7 zI`p=%1+*@&RY$VwxXg*U`A#yAfUgB?#E-ncZoU6xKP%0AZ1htmot-5VELg3zP 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 OB`f(^qFUS_@c#oKYE)_f 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..6bf0d9f7aa3f13ae1b341f03de6cd1f89c15d979 GIT binary patch literal 8494 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 literal 0 HcmV?d00001 diff --git a/doc/source/_static/style/footer_stats.png b/doc/source/_static/style/footer_stats.png new file mode 100644 index 0000000000000000000000000000000000000000..db70efab7405fd8b1c0c578af2110d91cdd698b9 GIT binary patch literal 19342 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 AH~;_u literal 0 HcmV?d00001 diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index dd36f784ae31e..d5377b77aa3b2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2474,21 +2474,55 @@ def set_footer( ------- self : Styler + Notes + ----- + These metrics are calculated at render time and can therefore be exported + and used on similar dataframes. This method will add a ``descriptors`` attribute + to the Styler instance, which can be inspected. + Examples -------- - >>> df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) - >>> def udf_func(s): - ... return s.mean() - >>> styler = df.style.set_footer([ - ... "mean", - ... Series.mean, - ... ("my-text", "mean"), - ... ("my-text2", Series.mean), - ... ("my-func", lambda s: s.sum()/2), - ... lambda s: s.sum()/2, - ... udf_func, - ... ]) # doctest: +SKIP - .. figure:: ../../_static/style/des_mean.png + A common use case is adding totals rows for descriptive tables. + + >>> df = DataFrame([[4, 6], [1, 9], [3, 4], [5, 5], [9,6]], + ... columns=["Mike", "Jim"], + ... index=["Mon", "Tue", "Wed", "Thurs", "Fri"]) + >>> styler = df.style.set_footer(["sum"]) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_simple.png + + It is possible to use the ``alias`` and ``format_kwargs`` arguments to have + greater control over the look of the table. + + >>> df = DataFrame({ + ... "Normal": np.random.randn(1000000), + ... "Uniform": np.random.rand(1000000), + ... "Poisson": np.random.poisson(size=1000000), + ... }) + >>> with pd.option_context("styler.render.max_rows", 5): + ... df.style.set_footer(["mean", "var", "skew", "kurtosis"], + ... alias=["1st Moment", "2nd", "3rd", "4th"], + ... format_kwargs={"precision": 3} + ... ) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_stats.png + + User defined functions can also be used, which is useful for displaying + metrics such as dtypes, missing value counts, or unique value counts etc. + + >>> def reject_h0(s): + ... count = (s > 0.8).sum() + ... return "Reject" if count > 3 else "Accept" + >>> df = DataFrame({ + ... "Machine 1": np.random.rand(10), + ... "Machine 2": np.random.rand(10), + ... "Machine 3": np.random.rand(10), + ... }) + >>> df.style.highlight_between(left=0.8, props="color: red;") + ... .set_footer([reject_h0], + ... alias=["Hypothesis Test:"]) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_hypothesis.png """ if func is not None: if alias is not None and len(alias) != len(func): From 409fad23e557d0aec8c0848c5cf28f5040c6b19b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 20 Feb 2022 08:33:34 +0100 Subject: [PATCH 12/44] doc edits --- pandas/io/formats/style.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d5377b77aa3b2..13f529187772a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2482,7 +2482,7 @@ def set_footer( Examples -------- - A common use case is adding totals rows for descriptive tables. + A common use case is adding totals rows for descriptive tables, with ``func``. >>> df = DataFrame([[4, 6], [1, 9], [3, 4], [5, 5], [9,6]], ... columns=["Mike", "Jim"], @@ -2500,7 +2500,7 @@ def set_footer( ... "Poisson": np.random.poisson(size=1000000), ... }) >>> with pd.option_context("styler.render.max_rows", 5): - ... df.style.set_footer(["mean", "var", "skew", "kurtosis"], + ... df.style.set_footer(func=["mean", "var", "skew", "kurtosis"], ... alias=["1st Moment", "2nd", "3rd", "4th"], ... format_kwargs={"precision": 3} ... ) # doctest: +SKIP @@ -2519,7 +2519,7 @@ def set_footer( ... "Machine 3": np.random.rand(10), ... }) >>> df.style.highlight_between(left=0.8, props="color: red;") - ... .set_footer([reject_h0], + ... .set_footer(func=[reject_h0], ... alias=["Hypothesis Test:"]) # doctest: +SKIP .. figure:: ../../_static/style/footer_hypothesis.png From d516b2fcc6708222ec964454cbf9d2a9d706b76b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 20 Feb 2022 08:57:57 +0100 Subject: [PATCH 13/44] LaTeX footer --- pandas/io/formats/style_render.py | 2 ++ pandas/io/formats/templates/latex_table.tpl | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index eb2080930d962..ca3c2096a1ce3 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -1040,6 +1040,8 @@ def _translate_latex(self, d: dict, clines: str | None) -> None: body.append(row_body_headers + row_body_cells) d["body"] = body + d["foot"] = [[{**col, "cellstyle": []} for col in row] for row in d["foot"]] + # clines are determined from info on index_lengths and hidden_rows and input # to a dict defining which row clines should be added in the template. if clines not in [ diff --git a/pandas/io/formats/templates/latex_table.tpl b/pandas/io/formats/templates/latex_table.tpl index 7858cb4c94553..b921dd2f8afe3 100644 --- a/pandas/io/formats/templates/latex_table.tpl +++ b/pandas/io/formats/templates/latex_table.tpl @@ -46,6 +46,16 @@ {% endif %} {% endfor %} +{% if foot is not none %} +{% if midrule is not none %} +\{{midrule}} +{% endif %} +{% for row in foot %} +{% for c in row %}{% if not loop.first %} & {% endif %} +{{parse_header(c, multirow_align, multicol_align, False, convert_css)}} +{%- endfor %} \\ +{% endfor %} +{% endif %} {% set bottomrule = parse_table(table_styles, 'bottomrule') %} {% if bottomrule is not none %} \{{bottomrule}} From 6890e5646ad53f2a129b153f880440d4f93d3f0a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 20 Feb 2022 15:29:17 +0100 Subject: [PATCH 14/44] to_string mods, and tests --- pandas/io/formats/templates/string.tpl | 8 ++++++++ pandas/tests/io/formats/style/test_to_latex.py | 9 +++++++++ pandas/tests/io/formats/style/test_to_string.py | 8 ++++++++ 3 files changed, 25 insertions(+) diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 06aeb2b4e413c..017c65b2bcd2d 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -10,3 +10,11 @@ {% endif %}{% endfor %} {% endfor %} +{% if foot is not none %} +{% for r in foot %} +{% for c in r %}{% if c["is_visible"] %} +{{ c["display_value"] }}{% if not loop.last %}{{ delimiter }}{% endif %} +{% endif %}{% endfor %} + +{% endfor %} +{% endif %} diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 145cd832ab270..2b8352d51c3cb 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -990,3 +990,12 @@ def test_clines_multiindex(clines, expected, env): styler.hide(level=1) result = styler.to_latex(clines=clines, environment=env) assert expected in result + + +@pytest.mark.parametrize("hrules", [False, True]) +def test_set_footer(styler, hrules): + styler.set_footer(["sum"]) + result = styler.to_latex(hrules=hrules) + midrule = "\\midrule\n" if hrules else "" + expected = f"{midrule}sum & 1 & -1.830000 & abcd \\\\" + assert expected in result diff --git a/pandas/tests/io/formats/style/test_to_string.py b/pandas/tests/io/formats/style/test_to_string.py index 5b3e0079bd95c..e1c260d3bc68a 100644 --- a/pandas/tests/io/formats/style/test_to_string.py +++ b/pandas/tests/io/formats/style/test_to_string.py @@ -40,3 +40,11 @@ def test_string_delimiter(styler): """ ) assert result == expected + + +@pytest.mark.parametrize("delimiter", [" ", ";"]) +def test_set_footer(styler, delimiter): + styler.set_footer(["sum"]) + expected = f"sum{delimiter}1{delimiter}-1.830000{delimiter}abcd" + result = styler.to_string(delimiter=delimiter) + assert expected in result From 7281a9ca1e247b53008d4de81a1785609894c17c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 20 Feb 2022 20:39:42 +0100 Subject: [PATCH 15/44] doc edits --- pandas/io/formats/style.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 13f529187772a..701444cb8e728 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2477,8 +2477,10 @@ def set_footer( Notes ----- These metrics are calculated at render time and can therefore be exported - and used on similar dataframes. This method will add a ``descriptors`` attribute - to the Styler instance, which can be inspected. + and used on other general Styler objects. + + Footers are applied to all Styler output formats including ``to_html``, + ``to_latex`` and ``to_string``. Examples -------- From e6fbf53efcf494cfed713385889a24f8629cf064 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 21 Feb 2022 20:39:00 +0100 Subject: [PATCH 16/44] valueerror test --- pandas/io/formats/style.py | 4 +-- .../tests/io/formats/style/test_exceptions.py | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 pandas/tests/io/formats/style/test_exceptions.py diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 701444cb8e728..21038dd753c6d 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2479,8 +2479,8 @@ def set_footer( These metrics are calculated at render time and can therefore be exported and used on other general Styler objects. - Footers are applied to all Styler output formats including ``to_html``, - ``to_latex`` and ``to_string``. + Footers are applied to Styler output formats including ``to_html``, + ``to_latex`` and ``to_string``, but not, currently, ``to_excel``. Examples -------- 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..8e365a0c4bfbd --- /dev/null +++ b/pandas/tests/io/formats/style/test_exceptions.py @@ -0,0 +1,34 @@ +import pytest + +jinja2 = pytest.importorskip("jinja2") + +from pandas import ( + DataFrame, + 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) + + +@pytest.mark.parametrize( + "kwarg, expected", + [ + ({"alias": [1, 2, 3]}, "``alias``"), + ], +) +def test_footer_bad_length(styler, kwarg, expected): + msg = f"{expected} must have same length as ``func``" + with pytest.raises(ValueError, match=msg): + styler.set_footer(func=["mean"], **kwarg) From 88db9474811f0064b1f881a5ab8fd8c321588c62 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 21 Feb 2022 20:51:39 +0100 Subject: [PATCH 17/44] REMOVE CUSTOM FORMATTING --- doc/source/_static/style/footer_stats.png | Bin 19342 -> 0 bytes pandas/io/formats/style.py | 36 ++---------------- pandas/io/formats/style_render.py | 8 +--- .../tests/io/formats/style/test_exceptions.py | 7 ++-- pandas/tests/io/formats/style/test_format.py | 17 +++------ 5 files changed, 13 insertions(+), 55 deletions(-) delete mode 100644 doc/source/_static/style/footer_stats.png diff --git a/doc/source/_static/style/footer_stats.png b/doc/source/_static/style/footer_stats.png deleted file mode 100644 index db70efab7405fd8b1c0c578af2110d91cdd698b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19342 zcmc$_WmFwOw=KGX4Q$-q-Q8V-TW|>u0fM^)cPBUm_uw7^1oz+;Tml4l2u@z}**WjN zG2Xqu?#UQ?P^s#!s_v>@Yt6Z4MX9OCq976?f3KTT9B?k*iJb5ctl*lB0m6$5RALUKSF*-p1 z=p05&I!gDpW%7qX{*FXS^|TiP2>aR%#C4@A02?rp_hoc)3!*7J{j(J#_zkB#tJTF{ zZ|=VzPg1W%qihR47XB~XATp=ayT(uRs%{6k$i8Hlfo{~`{jR+2G7uD4k5&A zR*&c4@QmmcszjEp_P2;a*H1blD#it(#oBM~yDZmJjwp>=f_jg{&(4J2QAv zHPQ+z;)c*rGo$4-2NV&ZwS47W%(X6_R@qaHwef594FZiOzBJcYaE<+z?pNVN53=rt zdKW%O4W-s?teI~|Z5?D$uSt!L%dWe>GH6vd@J`k@JSI5?2{h{I`Ae{)B%v!A{dqKn zt&0Ry%Q?r8ap^n;;=jNKEw?k|EfHNa+JiSW(R5f@vd>LZ$aJp{Y(Vz;@Uejeb#SQN zuwEcCbdaPtI1&nh9A`|*7AIJk92QA}Ll;3oLSPZn68xc#=ol8h+tdlWDKL4HxfN0q zOzMOr3_@fF83mf2qsB_2hept&qs)ZFQXmbZx&&n;l3_!?k#JUHOac1jppJ+xm{$|f zgwm4o&G*@t-3Pnjwjtk0-sX#1LwsQTf-y3XZTrIOXoO+j_cHTiss!%!Li3Zc1}kpf zIN`iQA`AY!)p)LThg%hB+)V`$pF9)}CMp!;+DI=as_eiO ziW^aqaquIf^mX%_d}QCskVeK!PS6Y2i}*4~+|6x1VeVk^z}}ZptBbe*vW>VIY^Z0@ zh4P?i!8V9mOAv|w9{4?sq`R|k%gHSpX95c^UU9&3J8g4e%i~v;(@Cq*J23}v!qCyy z$$94;zA(ITAlrcB5Ls_tkMR)NrWpDhWDtxeNly_+PFqE(iHVA-f>nc+iy;%0C5=?b z#}PhE#Y*cyeoEDXQHuc|Swzi}gfo(!B0fr6O|A8bP##YXP2O>SZcb^wWA1%r)SPy? z))$hu?Zpdae?BSEtR#gDA6N)@i+qb;Rq2%VS1eFtW`IiJR)De;Damz-zs$Il$SFN7 zpU~d>!lqOE)=|5w=0zc-+_a!)CZp_a@!K!XO50h3ipqthCGQhyWnDAh$Nfea7s{g2 z=Vr00#;+ExcGrh*X0=68ztB|t07;c)DL5_3E&4Nyo<+bCIapF%dQxPVk7B*uRNb_+ zytKT#9MRm~9N}Ir=pZOB7~}TAP33@oAF`G-QMxz7Hil|7g>nAhT=zuj~+ zyMCnd6J=`mDD8mvV14GfcxVb~`p5K2@z26rIkFQCkneDYa8%(k;ilo3eJA02$i{@zanP|1aV_{cyo8*k1djwUv0w4? zIQ%E3OzZH!#WD_h57tEm$+t$WS|rGS9V!2kYEyW?I7>S#`<|MsR4;7_w-L3`)YbgT z`pWDI?&=X&8mTmF%Ix!-vH@R3rXLnRc+yzXLbTho?J6lN;VL5*ux$g5J|5{GedgsS zDpigjSNTk8u(+_MZEaxgJC53NRfn~7(sa~3wCuYSb7FgP#-B){iyDTCg6bFVHbg(9 z70(i1`#xh*?sL73%wBA*%BjjXmB(ySevKTeY#o8G0$tAa?ptkht*dS&t>JB~p7!p3 zC-3(LJy&);e&+lpJ?R|$-uE06Q5G>*G;KaSyUNw)8nH)G%FC7ZE#7+s=?G`PdG)CH zr>rB|8T47vdefsH;SXiAV`L8JDpdCs4}{%4*AsMm5AG$uz&|o5q;_Ttqg} zkU^6>j|pS8$nn{nbU=UL_t1+l5ucEU$M|uX^Mvz&bIwK6-Tejp_67}@)=thV(K?4- z^;m93PC>Oo)hCZQ``xE1xy(-u+`Att{4402c~J7wQd-39EmVfHt6kw{)@BuF9VeT! z+O1D_y&tYz_HwdnIr6yv>ca6-32d7TTB#tHrEw*VOsswq7(@Bxu-&-{d){&$e%JYg z_|nL{Q59V!5mal^Xd<`IR)yV(4I5SQrna%K(bKAHGHT?@4}u@c6ZWH2JC-{+=o$k8 zzpjvCahdg}nCg>L6tM5t?+vaZNOQMHjPvYknf~HZCwWRo;pSlSfKm znbD^d0VwkZU>BFlI^eV>~ULnM@a>>80 zVrlTQC>U_GtMGMnZAETnX9Zj9tycBQ_*ci(mv8#IewDO!r8W^g@y$Z(XFDeyolGV= z+YWteyX|6rY!5G|xpgAr&1uc4zRtc#x1ww5O|@6>ztL_UZ*J4i{;Jc}Xb?vVId!f# zcE05I^q`;zit34Ft_Vq*<5Rnkxa!-FAO$emAq?!yEySw-^JzPXGlrcmY&~Rrn z*@V?yJ`N}D#?#*=8^0F`GFKOA&sw)?HBmp%-HZBdnQzpKkl*yMVswCtv2y)G^ z+5~uOJGFF~`wxYLBY30LE7|Yruv+h{k0afBr!U=e{4dtC z)6MUzzO`p~>tud)zbO8gE>VrkrY4|}{(SjxWt-e9 zK4lujNsyD8L{9 zA8AWha~B&YHycL>^4ES%%^cm`L?|g=2l~&yf6mj=%jUmEa&Z0EV*wAy_S(Y6!OG6| zpT2=xgh?R2GW?19$;FG3|25)2@6>X&bdhqj2X1r|{jc}? zubcmS>I?1G`B4W@d=U6V0O)J{=^17K}w43_DB$_VkSCGeo;Ze+Lt^Nyty$ zy0+zEJ=J$m>pw92I4+dwuk+rsvC-g0_;JEcRq>{)tg_uUIUsK9O?DK83B-*V+T6o# zvU)o_kRl(Ffl-IogTC(s9)q~uA(^c40fj{&;3|IT_h7*%*t%09pm0pW4i+Sa;(}&5 zhtBr?o$0U@hUbxP?C&lAxhP&xVb=d{D?(kL%i-brVEfOy@7X)$9Jd+yibo>Hj<*$7 zwcr2DG}U&!w56nVdLLDP<(}mE`K~Cz;rosD&##Wgp4Zc|WsSTykJ|}iH9j0NAJYsR z1U%1s;M>nSA8QOmP=(LMzh2KMdVVg$A*`tP6Dj*3i7D0t9;Q8jK^%|C{aJ_-=N%LA&?~IKIrgG@Nc&Qvoad0Z2yC&XMyqc;jq#( zy@Vz=6T>>n^iBgo8&`X*=?R#@Y2M$bGV|2mG{U>l(m2CW+Xua>uw$o`wnKp50bWBUhX-X-Z;&ket?Wc*I~>p!T^IiqfJn?!C` zHO|n4j@kd#20SaCF)&q@bX^P*JyRzu71~@%-hbQ6H8}Hb*^U=F{EkoX_q6@&@Ozc| zOX+8a%S*|}0bBzmiTo#@qv~{|#QdUTMui(-4F#8fTjlxv?h`qwZC=pNVwHvb9c8NB z|E`401}Ayu{Q~R~pCz3ATdD-%2F>@2b*3*(vnsQ0?D?HSXB`?{vk6jYB2Cv-0e@c} zvOk)MEBI)+P$e54E!lrvd%BtwI|H_?&vR!#rsCaRuCL0O&O7Hf0mt=2j4wY}+pF;U zPWV>rxcvSA`y=gg)u|69%W7Ur=O#h)p}h15>Qs#Lv~T9l+5$meO_G2AW$Pl0q9ZAE z52WMi@`wK*fz{mPAdzDg8>~#Ayzt|3V>-6J|NSnHDm!$*-^UHbNw0fvicjZe7Y!Hc zoKP7`9Vx0JHz{3*6_vtlB$n)8Ij%U6U$U0*&9e7ij-rgXwlkY7rt;q*v6n5&2+h%u zc$cxMGyDM)Fl+lUCu!;-x>&G3+Tb?u$zI^ou!GV+P~$_5hNJNZ*|Kw!`CWg~CWys* zpo;_DGdzCndLGa7J^f}Buk>0FAANk*QfrB_3lN9*7*MMLFahlbuyH7}FyvmK}3;1W*3I>#V>-$+dP z#>U4h<)7`Ij-HnzbV8ZL4i5)qKd*hBGe2IAG*{GiK2&IyR9*UrZG~bT;uzBAxT*6E zU~BAq#tU;G80@H>fw7eG^^)TS^itcg%N(dpy4_cuhfb2pmf6h8v+-4LfzL}|X}Uaw z{Z2&Ew?K>{hj)-H;NuMgvFEC@5AvdAy5TCYO^&DZJOor?TS-|gzW(Y5wW{%ITk$Mv z&B$^=r{>S~3un*n>Q0rsF~Zloxgj3Qylq)=n7#Tq%)Ov%9<~x)(*SKFy3{GCmq{Go z;iGUK=_=Z}8H}|0XT`CWt&3QGj$CU6ch6B^xs|E5W2WFy&nl-xtE#RSv81A6AYZdC z`g5jLuHNJ`5_4Ukie2>Rg0XFPemp*hq#xhD(9d_SsUQ5$zTd7&BC!;UCKO0T9PR|% z%C4=ez8n6deETr-P! z$3XvxqWe#J9&%p`e?qv|aOPc=zIlR(M{Y$M%j0gkv0yyi%?FrCAu*bU0SB5b-GQzk z&}4>F6$P5CP^>iiOuuT8WwKC~!XdKz&bKU#;gH-^9`G^A{n;%^kt;2k74KPM;N@nFyn9#=FpTq&=(y; z2U=+-dVJ5m|3q;9qTM{NZSWf8OZ)CJJ@qXlI*8(fFJy4HIoA< zHeIhoUBSss^HukshN;o9XOKl>udT3XkvaRRMm)WWp0*_aEDX7Vens#eksUv}7by@E zWSQsr{gL0TdSYra?C=sgt_yK_koX2GgqlnP=`T1xFbA8UF1!3-SySQ~FMfBw4->JL z?K<%V3%*dEmiObc>ljA_Tu)#dVoti=djk6-z$kl)_*|=naNNvk*yaHqOOQug#P`qK z`-1P1p}*eUBW;~-0G@}ZI!mk{v3pTXtXJft?>TfStb<4_N{}}CgLD^$ZZdo8$5%6+ z;&zp!$m;wWm#8~QbR(KFSY#i#W%xLB-oKCgE-oBSUiZgDv)gK%=cikuH>SC1%hG!! zJw*3vxFZFj_cG*R*jT|hDOy;pV$i1IX_-Tij`LomWyBtuJF7)Wj+z_5nu3!hxoXHb z8yl`>8eu4|aj-~(@UMXn|80W;Eg=()()X&Gj&oJvi%>XvgdQPX96bRTJq9887l8w} zS!JDH-9;p{RN-H60pIAWSzFgz$P}7oL)CqVHp{9DCn|Lr`VqXa`0;9dJ`y3U)diCi z4&9OSq5zjF177&!fg64@!XKy*#~Dc7k8TKh3akQ8uul^qj~-8kPP99Z`|D!CaxrtB zbzB{~Prt!)?=RdgLAjoP$d4AoPXc1RGHJIF#%sV4dwdDIGSVX zlV34xztx-B?-xITCDPGVSc;v))-v{m(8DEHaOB-%@V#x8l^XS%v*%v8019Q;BM)ww34o9Co%E!9l|?` zX%7Vz7FwuEN-S{Cr$KGPke<+OrST7JpJLpQ;!vm3B0$912{f20>S0-D1-NCZ68o}Q z7z81G9=TqT#C}d63W+p#R<{$nr(jKqCQpOy#mGFki>LRhuB-6l=V0THDJ9QwLZk_?Q8C;O;2 z`O>kIy-~0~xa@kmw;E3sNdns*?DSg(uj8RAmKX^=IBAOY1|226kz){iCKMj_ka2e> zw3wNw(&+=~;l5C2&%Ae1B^<*02=fjBeUuHCUmu}R!9?p?Vk(YGeMV2#f)sf}RqVOg zZPg?qge#5o2p&pdF?0HC6Dz~9^WJR9KvX2$opit+m~z4ub0w~ki#DnFP-C{NZk*Hb z);B912_osDH|*H_{kjzQ2Z^qt?HI_QN-8U3ZN2Oe3Edl~_fC zd<;(tfplWW<x^_qv=CT-1lkWYy){7ysS}eGvSjS2@}|NJ$ttS9zubs+FwGO^j5fd7 z;2#aW3!w;L77mvY=v6`E(G=_qm@byOODbq0$02W||h?C`(Zb+h(_if?ndak*X*!4W6 zZxoC3Cnw>ry%qObcN-EldyHepi}j|awo`Un7&C)A?EOroi>6ixR%3(ppsI^ z&uPVD5z%@yR7G%-QlSLq5Ns3@4II9e%xQCqXsgn+g|)@Fgz=&fA5mTwW?GPw9}5`` z3v{sGyce!!m8boT2aUqELt!MR_?`XbPLb_N`PU9~yAPxk zcsG)vL2u3_hrihD9%8eC?ST!ZZ~{qAz#|o7! z(9l9DhLKP{a_9VMJp3a4bxH=KMslO{EO@Hr$ZJ8$st8Z$-S^&UyyAAj2xNl7;@p0Lz%#V1eUKL3wLbVf zu#O9iG%`xI{AK51T9%B#Qh!)+t(XQ@@@;6fFbC`=UZ@tqFC`AS2jo+!QwqS)E@A+4w^?uHtM?*d84vr9`;M9WvHWcY7wM3{YcHAg=cxN`j+ z9(tW0f~rpXrq|VS7j2jE+vo>PPN$u+06kH(+@7xxdB)YhPDsD#g4|L^Mt9H#F-k4K zRN)+<$rwc#)H$S;WrS;zYdIb~REZqF8th3elipiY@<+|brwT!^aDJ~gp+8BroD;f* zwnD7|gAdC(gL==rCn*KuHXf}*-gqU%NHxz^`F~H)A8=Z z9@;r?Z_W`Q8=VU@L1G;p{YJv;Mv|Wx?D#t^V6XY>_#1!hMD|9xa@>K<+@tF+F#E*C z__3YY)%oIOy?YctpQ9!N2uPFWbxr{ z>o(0qkznhNSN;oz(mUn2OK=5;+(I;vEG z6)w3yIZPI8uUM);mZ++_5LvSQ6UwTCG&GjH=>Ob_jC`J#E;#?r&jhaoW+Ch z6BnH5EiS&I%X9ZkC{m>)u6)5K^dBKt)_$*?%=JBeJJqL2YGY@(2rGLe!5IWU8VnO0 z^vid65+{T}F<~BSwz~ZVdrMCwF7R2$%zL&W^n~&0?^6wHdr%d#R+)qVYMi%Eqcc`dODS>?C6&og z5M@H{fdga?3b4BFpy>*Ba5Rt&|39yN#UW}t6U~LE=er~xi>9cE)y0sW2&|S>izLht zM3_%5Fp`{zcs+1LcALBa_c2a)+g)1z(7#nE~9c4c6Zxpgym z^eBRL?NfGXY6Ls>Huhr7H!=>hP#41Rjs%fE87qFbYpFK)BnMbe5xtN(dp%_A@Z<)F zB?mkD_io+Qu7Ci(k9STqA4U|0ywIn14=Zc6aji@_T-Z9!CGp^X<(p&Fa1fi;)o6`D ziWAM74IRGia;GveF|r9@@h5x)Sk{MM5(GIkO6PTjJ=n3NiSg?|{EsT=A}8?)csKL% z;@CO+RmZ^hpC-eY<wJOVI?Q%{mNyVUW)>sIqHvur|v<9r*dJ-pPb0SY=b zu#8VIT7oCwXB*%BsRF}T(5GNv^&@5u(U$fBpI9uzs-1?=0gzt&TZ^9T^c1}W$cOZu(=lTwpECN8=tLHnqIw)B^ItdNSp)J_5whpdouCFUy{?)iGm70+fzEjnq0iO zquY?=tytOD6?vLf-ybri`u(Q24ubF{t@avC({3akUE6dfNs=hY?=n@A?l7W})IF^Y|cy=91OWb1YO+Vg7l^TVuUlD*yms^6;Yps zz=y6>V`Vx8$$ZFm9-Pi+ydzQJ-vXlqVlpumEH)Y6p(a7gQosd~pb6pcj|-d)&GbNp zNd3jvh-c`@r#@mC^+O&KepqoP4_gOd^UfXnC+8-FH!Zo_8cS`@qk6el0=#67q`<=#S@m3Hl@`n(AfVb_&MNQs-{lpzZ9*ek+>2a&f`?bK^P~ibF^K zA`P7?((FLYvixEgl}2FZyxhjq%{sM4gba}ggAx1U#m>a0Iz2cM^aUgQ`miwKyY&tD zG=TXh*6Z?rcEc&_y2E6jx)!*J(9)9TvVw^krkabx5`W`tt6dt z-|tCONg3XZHkd|VWpQVq`Z+G1xp2^frluCN~_$Esl(HvCgF>cMa@#M z8M~E{ht6GXSq$kF1>#{(s09v+ljoT7PPr=GMYds!kJxT=w2&R|rLmOfXqdM9z6s{6 z(`fustRf{&I+nh?=ICHbeG&T;s^6x5^?jycU(tBx-u&R0IB~I->!EJ^rMN!IeKJ-Y znLv>bj2JIoBGdg58kWpjV*`CS2Jj{w5Pff{{LNPnb_dFO2p2a3LjRpNQ=>%Cu(AOE z)dN2@#|ORyumm|1R_nBSb$2zTQwH~u>0Ce z!4wyPxky=T8F2u2#z#XDb%0)3rs)jJr&eL?v1FNrRfw${I0G$4X)losm&uqN0C6)8 z62c&TV?Fmt*WZsm07&Fu?&^XRsZ0BSAhp_}I!pdVoX(i##DIPU@h83!&k=1XHZ21< z*XNLdEtcO%*S6z)#g}45Znylj65Y_tbl{?wd?)O16iGP)k9TWKA@oTykF9A+^5GqH zq*oNt=5E++FUEeD)udt~nqSL=;L)AyguoEeNsE7|lP5>E4Ix4krrG<I;2=g6Ee*N4@Vw`w*s!gO-uyA{ln#Kp@SZgzjcrikPC zbOBrxt&3(r%-aIdg0`7nN!r7(KK4dRz4?xQqVpce`7hS4r^}KKNs8Z=mOsxrkb775 z!fuAI8mRD@X(RbgwkP4vlCq-Y7R&thf?sM~kuq;57DoILE$`{8E5+LEmsF%kfaytvI!X5MwG_%=9IsW7BeP zO+edwc@6%EX6-oc7!F0GlFq0q1-u%fBmJ;9oGHUQA26%l$BO~QPbuuI93*Bpp?{dZ zjWWJslF4Wx^ur*+k0UhcZm$d@Clx%YAAU4PTUoBphY^q(ZBCvSOdVB3YiPlG1}grW z!|<@oK>E!vQ-+F^zREOzfUgQBRB7+dnM72~sL_XY@Y60JSGwaq;i-o zp4yNNO1xQ=SR3>G*bnecL+^pCWW*OKG%Gx^iQ_xQB+77kl1MZ5IkIkncjW^&#|fQt zQJYfT7kIN2+NurJF1MIU(te~S{qTR`ZSLrLwSoi66JX1-iEXcy&=$-#JCw#S*9)q3 z{WI@qvd?oiukvd-Ncg2fQ2Zfvy$mrhjpdkqOsWB692t^mlS0Xmh~#XN`NlD7WR>rl zf1bw6j_*p{jFv=K4t7j_E*kw z7GKT4tNNv(D#h!H$mg`Rm!U>26x#>qWdGC>6Hu@r#L}UJI;dqSKvOdEukYSer!96J zU?04syGcAZrx`L)g3k_m8im0%i7OZrN|Pta@+0eGN+2DiBX=}W-?5w-?vn13fcVjb z7hspMWqt)DNJlR3RUK0+tLZYua6LgUe#^gwS_ef2?W1&ydXXaKH%kZtj&! zSB#U*1||OPf4_NWK7#|G55o(3zvR4v>yD8n5w9CXw39(}YpPcpK5e3}gB+xfUY7^4 zl^7qzhqey|Og}H7(or;p8WKQOefa;a1rQ1^vA?H%k*ERCO}u!1dqC|caJL7vJooWB z`o1_YgxhAmK{`qhHPBa*_2PGmsUW~%>~~9iWhH`^2imEp>wKm1VP4%#P>6#eFE|Nk zmn$^A4Ht)}RQt1m;6_ec!rDg#%E2Lix%u@ag6eNwAFAuDs+ixGoNHxfw9qoA6Dbn7 zTJm~G66X)*##R)*Rdz1JjMx;mPn=UuJtt_YWZWNmPb;61-4P@rFG1mFsqcVfp3$x$ zc`bT4eSuX~c4ClqM_@M=={i%_^hy0`coh)(vV?W4c2KX@uvX89 zD4MjXYv9d4mB1l+U>#Xiud!31OT6j?Mt29zgLQUzpuvNIo0nO(HO`Nx?T+s&*LE=~ zSm~P930p&Y(UY%FR=#SY^(bsU#FR+ULZfY=3LWPweLtV;`8E`4nPh_P^XK@>jj2)v z2LB`Pq6tiOgv)9IK1VM?@Ba72*Rqm2PqXsC|Tl4ayTXWz9wNI*qKm$77h49R?FS9jjpaVw&gs9 z`kGSX-#{cdAiY@TBRv~;Ic?@^|wBMII7iAuy1`zPExVz4L#w#m$U=%;}kH` ziNmj)tbg~dM5ETz`R0pk#m+j?mb^@Anw=N)a{#qkqYm3i`@Mw3Ht=>!rYog0E3WLVqylHxpZBQGTBZ=1G%lt zTIJNr+D`p#^g1C5QYfomB7YX8=vbp68ue5lYyFvns>oxwV3T+J>f#;p-KljkJR$ymaC8!x!wPvp%+=o%2mKj9; z>KkQ%Qmi<{>jcx9v8QA|%9{_QZ?b&&D2YwZ2&WUTkBKDSSK27FnU3&;4E5l z#MG+jW{}Gmh%Cvq%aAMD-@leUB);haRdC{ZYI;O=1HC;+KueUssSxEK7=5M`h$lf zqzMB$l@RI*p=mn*)ne-dNdPsZ58D5$JNVz$j=o4Q%J?P2R(4UAEOkb$B_pqYHQI2{E5Mv`S#9zH-U z09-;2gRRI37JSWba4v_|>jOMZ%7c){h65X{+xj(5)EZv%B>M#_0jKmfN&jig+*e() zq=Rn{RNS*9Y?v2=NRdX8E{}_T?Lpk*+_PR$>cwl(ueSg>tjaaODkVJJbrUMap`2?aOZ=)+;aY~P*hnM&o~z2c!4e0?5@VQm?1L^fcI5g7ZTtHFu_A=+++QC;{n{mLi`@ubcRH_d? z+cn5DR4?(db>0;oJ_Fh-zV-ua-+eG`!1Iyu8UkJRCO`&$eS0~0srgw{VL(}~70`~Y z5!zN2Y5Jbqn^drgd_vK#Iq>%8ps2@e+>OACMMS9043wik`1hm+r%w=f=qb zplSG7rb_mzWP9HUlZi5bX=_OhOw8)Zq5Y&3nN^n9S^Dez@Xo?VfRMVSzenZY1o_cE z+ye}k0^nWl_ez>xGx8~%;|h?rSM}idhlUl8Zs#O<+6%4;{vtf7e0-9CrwhZ;eqg%m zB{Q^s-hhK|Qhk+;?ez&=+-)a#+B`Q-iq2k_r0I`3*7d+Wd;6-dU_bIS5JTN z18`Q(@SvYQ;Pl(x!@Q2M;Zb@{+(JU9M6Kt#x9kyqPC!lP!-x63f(TM|o-{x&$^((l zG4rlaR$W-CdOT@aQF#Qs@`ksliK@Q(zR36S!667K!J0A0e(iG?aZF^%K<+-pvMw;+xs2RUd(*`i&wwwu;F*>uE_eW*j$JW zYc|JyLC+x!M|(<%Yf`w;z`7vprAz#m3TEg|mxyVoDcpV{v) z#xdkZ&9n5dVS`K)I*qII!40fM>iq&fbTt z5*zgU2cv;}PQj^*c3~RJD$t+vi#M|?Dq0^~*4zqqpwP&vP@2Caik&vud z5Du5T@Adw4NgVL?sCK!&lwpOyp|~_q3Qagg9;04Yl^LrC0BTPrHPJmro9c=>;`doQTWWUunpt9E7kE6zY$jepCAzwhl2wl3=< zdKZ?hJVN`Kd086xRs>zk9XC2p0XdQLYg(7J0*EoKn_d)1=j^}@j8-$k z_dCh)T91H+$mRL*)-#onevdXMqiPCL>6~~@sU&*8lf~Tw#@bKCBOfkKR;l<3N^l|p z1cW!aV#oN2pQvQp0HNY2$CuYUi1N1TqPr5$yk-`y?B|)rX?LzceDi&26VAFvOyj_c z;35QXz>r`!0(MNT>xwST3Q!Ww3LiAoUt8pqj1qn10#F3MfQNuU=|RLJJ8OXrEp~h# zpzGAh8L4_vD;pXeoRRV?dL#svHs0BUkNkyyZ5)+MRQMKJONYv7&Hd*$K*BX%4dgOj zvoUHPH~b&%hknGKABivaT1);yOXfYs$M%LNQXl2N^j#Mz2N`*=N(O-o&+DPQ;AtMJM$2qRw9q=!T1iNNp|?W<+0 zLE*Wdtf;IxWIvWA^)CMN$yea)kmFM;y4S!GgG&__S^VkhBF5BLU0@xP+ZRd%%qT-& z`mbY3Tsz*cE@6xb>}5y85AD568Y&>Kj25J|m}(--_T)d2rg$FSxoO~E+t5koKe;VotJCun9^JMYX`R98NxQ-zZ8+GVU?<@_;_02HbRp~=`H$@Q31989V# z2jiz~+uwH64ALIxWTi>A0fVhH>x91z8N47KbpC#`aMj^i4=mgJKg#j76=#5~x^|rQ z5erxsQhyuB^B$vr%eyrjxmBopfzfBc%KMO-5fOz}BU??^S)v`xzjMCZ;wJ<=LnVXu zXp9kb-O*{b_9!zSG18O}u|(3O%ukVrI)zd{PIHIL@XHI$7+-5&Sa|_q9%efJq6)fLD*FT}0$;rTQ z)I3`(=#oN2Oq+xKk9(2WLgfu&df4O%Ct2}MN8T%b-$w0!Fm~;7d7ISaWlCY8mh0d8 zP(X;6m~vlE*j%78`D0aG*?0FcE#_9u{$r5ti;EBUFYj1eu_bNh$yXumHq!PSU?XacosS1Z3)gNbSnwF2u)5(!lJQ4 zHj-|Y^#pQ{z}`?i7!f3L--v95sejo!kTM{H$9?A{L30oqsa{AZ&?_#Xi^!v+T?JMC z6+r9kbQp%j*%v#$SL1xP>z4EFCd}(n4LKeOLg?Gu5le)U`qr&9^f3*rNX7*-Q++bX z-{`<4YH1jag#p)z zBQ*~8uaYE4T`!&TQmAhNg%_lc#@R6S@Xbrje?$O>oN>@{!f|w_(mMKmH!A9;8#4eG8 zc*CPGkL=C}k!oWqLB_(}FfB%u$`}}iVo-x!xOh9|)Svhs4-)>PbP6># z0LIKA$yW}c)6)Zp2vN|fwB`SPt>7R;yvQAh$eC{VEr!Qd?5a zV9Hbx;Pgp~t{GsWH44$8ejrh)F0nRY{=08C5PB)efb<={NFJ67e*rUV+_2jJyKf88 zsMP#38l^==mgbaoB>2KwUJ*U3=39GZfm|?*2Z52(PB@W+THR~XG5|Pk;9WlO`T!*a zj+Ubr3j00g-ew14FLy;|&H$=-08XW}98?L7a8!h<2&Dl_W6>VhmT-__r%FFBjI2F1 zf`kwo59^He8VBp0_!+n&(|(mn5WbA`$6@sWBIi8@$4x++e4rw9!o7u9w+(77%Bmx$_<_=hPI815x~16vYA>v*a*vT%mAV zPZ@Z#X|_Z7L7<7&?fA+R13)DlpG;0op@;AWIJu<4H><>zNzMKHv#JKN>SbKpLK7ry|7GNSPzn?*d^6f6x+Ky_Ds5Mh(R9!7o zhipRXkh($SN`=}b#0k?;s{C7_6+!`K-kaO9kueb+K!Ru4G1d&wldK{#Rc*dprgycb zYU|7AMQF))aqpvzO)9+;?U#+5wxj!;u>0k8sf$j{1!-2O$Vcl;`&NM{syxWW3(r)h z=Cy79c0QLZI?I?gBD|;#IAQgyZD3zwRSQH1V@NpP2APnbmo1W&uLdykXP!6 zl{Lw-KLh85!q$;;S_1$V!81Yn5~rK1+XgY*QJSu&va* zjTNudh|dFHPKB6Xp!Oqx9Iw6UhYIQj6T$Cxo{o2wvi5Q+zA?AS?bju-;@i&jTJ(~? zMsG%qNm|&k2Pwx1(#kW=$ajju(Dnt(*Nk(7G`2(aC|DKIEI)yhHsq~CC8-QU=1p58 z>;0GgGXk8>(pdn$Po9rWd&D32P|wuO{V?F}rjRui*KI@ref0SIE&p=Q<^B4~t zrhzuU2bda(d@ACQ9L|gdRjGX3S927cmC6BLh5!VLJ$J?e&i`a>P&_xZfjk>u~ee@x$Fl6rUITK zd*F7o>^F@GEF1?I*USESzp3dN)6Tfo^C9l(8_pv`ydeKvf0^miuk@SC$;nhG+_RZBQm z9sR`eug>C!;3K|(GGG}QIxRX$&mr}wSPgLSAo*@GE;Zydk0)B5_n+-i_@l+--Ysg&POnw1&$?L4s7E*ICH0{ONVwS z?~9!i3tE@kdFc4-ygLA#$9S${G`Si$pmc|cTW7(In98S97w&3{>$GMNwV4l`#dl}U z`_SZXb~EK-ndI_)wZF6K)R-C*4PF`c)i-ULvfB7o#5wkNF(RJ<*r;@Kcxn`ec^;rjaM*HxUF|#~Z$h!35;o-Yig8gFy&op_x z$}gK2k<%1mWgTL<%gS=!&g^wN-Fg+c!{lDh;BfiuLJJyy!sTlku6+w zlFi?T)0>y9WAW}R{L^l?<;e04KXvG=Z8i8Lq6=0i4I{waYX?T4foX)->%&LM(=neJr8# z!(t|I;ml!BAI%_RZ4z+25Y$IIW-)UbsiHsZyZ&r={%pYs&^cQSp00i_>zopr04qOV AH~;_u diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 21038dd753c6d..75d05d6af1cdf 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2108,7 +2108,6 @@ def use(self, styles: dict[str, Any]) -> Styler: self.set_footer( func=styles["descriptors"]["methods"], alias=styles["descriptors"]["names"], - format_kwargs=styles["descriptors"]["format"], errors=styles["descriptors"]["errors"], ) return self @@ -2448,7 +2447,6 @@ def set_footer( func: Sequence[str | Callable] | None = None, alias: Sequence[str] | None = None, errors: str = "ignore", - format_kwargs: dict[str, Any] = {}, ) -> Styler: """ Add footer-level calculations to the output which describes the data. @@ -2465,10 +2463,6 @@ def set_footer( Aliases to use for the function names. Must have length equal to ``func``. errors : {"ignore", "warn", "raise"} If errors, will be ignored or warned returning ``NA``, or raise. - format_kwargs : dict - Keyword args to pass to the formatting function. See ``Styler.format``. - The ``formatter`` can be given as str, callable or list, where a list - must have the same length as ``func``. Returns ------- @@ -2493,24 +2487,9 @@ def set_footer( .. figure:: ../../_static/style/footer_simple.png - It is possible to use the ``alias`` and ``format_kwargs`` arguments to have - greater control over the look of the table. - - >>> df = DataFrame({ - ... "Normal": np.random.randn(1000000), - ... "Uniform": np.random.rand(1000000), - ... "Poisson": np.random.poisson(size=1000000), - ... }) - >>> with pd.option_context("styler.render.max_rows", 5): - ... df.style.set_footer(func=["mean", "var", "skew", "kurtosis"], - ... alias=["1st Moment", "2nd", "3rd", "4th"], - ... format_kwargs={"precision": 3} - ... ) # doctest: +SKIP - - .. figure:: ../../_static/style/footer_stats.png - - User defined functions can also be used, which is useful for displaying - metrics such as dtypes, missing value counts, or unique value counts etc. + User defined functions and an ``alias`` can also be used, which is useful for + displaying metrics such as dtypes, missing value counts, or unique value + counts etc. >>> def reject_h0(s): ... count = (s > 0.8).sum() @@ -2530,19 +2509,10 @@ def set_footer( if alias is not None and len(alias) != len(func): raise ValueError("``alias`` must have same length as ``func``") - if isinstance(format_kwargs.get("formatter", None), list) and len( - format_kwargs["formatter"] - ) != len(func): - raise ValueError( - "``formatter`` key of ``format_kwargs`` as list must have " - "same length as ``func``" - ) - self.descriptors = { "methods": func, "names": alias, "errors": errors, - "format": format_kwargs, } return self diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index ca3c2096a1ce3..7a9a9f556321a 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -58,7 +58,6 @@ class Descriptors(TypedDict): methods: Sequence[str | Callable] | None names: Sequence[str] | None - format: dict[str, Any] errors: str @@ -144,7 +143,6 @@ def __init__( self.descriptors: Descriptors = { "methods": None, "names": None, - "format": {}, "errors": "ignore", } self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) @@ -940,10 +938,7 @@ def _generate_footer_row(self, iter: tuple, max_cols: int): "precision": get_option("styler.format.precision"), "na_rep": get_option("styler.format.na_rep"), "escape": get_option("styler.format.escape"), - **self.descriptors["format"], } # set defaults from Styler options - if isinstance(self.descriptors["format"].get("formatter", None), list): - format_["formatter"] = self.descriptors["format"]["formatter"][r] display_func: Callable = _maybe_wrap_formatter( formatter=format_["formatter"], decimal=format_["decimal"], # type: ignore[arg-type] @@ -1040,7 +1035,8 @@ def _translate_latex(self, d: dict, clines: str | None) -> None: body.append(row_body_headers + row_body_cells) d["body"] = body - d["foot"] = [[{**col, "cellstyle": []} for col in row] for row in d["foot"]] + if d["foot"] is not None: + d["foot"] = [[{**col, "cellstyle": []} for col in row] for row in d["foot"]] # clines are determined from info on index_lengths and hidden_rows and input # to a dict defining which row clines should be added in the template. diff --git a/pandas/tests/io/formats/style/test_exceptions.py b/pandas/tests/io/formats/style/test_exceptions.py index 8e365a0c4bfbd..49c0782f18c25 100644 --- a/pandas/tests/io/formats/style/test_exceptions.py +++ b/pandas/tests/io/formats/style/test_exceptions.py @@ -2,10 +2,9 @@ jinja2 = pytest.importorskip("jinja2") -from pandas import ( - DataFrame, - Styler, -) +from pandas import DataFrame + +from pandas.io.formats.style import Styler @pytest.fixture diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 2cf3d9ee11c99..e2818581dc4e5 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -436,15 +436,8 @@ def test_1level_multiindex(): assert ctx["body"][1][0]["is_visible"] is True -@pytest.mark.parametrize( - "format_kwargs, exp", - [ - ({}, ["1_001", "998*16300"]), - ({"precision": 3, "decimal": "+", "thousands": ">"}, ["1>001", "998+163"]), - ], -) -def test_format_footer(styler, format_kwargs, exp): - # test explicit input and option context values +def test_format_footer(styler): + # test option context values with option_context( "styler.format.precision", 5, @@ -453,10 +446,10 @@ def test_format_footer(styler, format_kwargs, exp): "styler.format.thousands", "_", ): - styler.set_footer([lambda s: s.sum() + 1000], format_kwargs=format_kwargs) + styler.set_footer([lambda s: s.sum() + 1000]) ctx = styler._translate(True, True) - exp_col_1 = {"value": 1001, "display_value": exp[0]} + exp_col_1 = {"value": 1001, "display_value": "1_001"} assert exp_col_1.items() <= ctx["foot"][0][1].items() - exp_col_2 = {"value": 998.163, "display_value": exp[1]} + exp_col_2 = {"value": 998.163, "display_value": "998*16300"} assert exp_col_2.items() <= ctx["foot"][0][2].items() From 8c8ae963afce6015cc3fc3b1c52d64c125172d0a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 21 Feb 2022 21:00:18 +0100 Subject: [PATCH 18/44] doc edits --- pandas/io/formats/style_render.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 7a9a9f556321a..738592df92877 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -444,6 +444,13 @@ def _calc_wrapped_descriptor_methods( Wraps UDFs so they do not raise errors on, for example, non-conforming dtypes. + Parameters + ---------- + methods : sequence of str or callable + A list of methods that will called on each column of data. + names : sequence of str + A list of aliases that will overwrite the system method names in display. + Returns ------- DataFrame, list From 5511e80afdfad423cf6dc91ad048cc288a06fe2c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 21 Feb 2022 21:01:49 +0100 Subject: [PATCH 19/44] doc edits --- pandas/io/formats/style_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 738592df92877..3933246d3b504 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -491,6 +491,7 @@ def _err_wrap(s: Series, method: Callable): else: names_ = [method.__name__ for method in methods_] + # performance: should probably subsample based on hidden columns so as no calc return self.data.agg(methods_), names_ def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict): From 54006141a57c14de388d3c21265a6b4cb3755b8e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 21 Feb 2022 21:47:55 +0100 Subject: [PATCH 20/44] test moved --- .../tests/io/formats/style/test_exceptions.py | 23 +++++++++++++++++++ pandas/tests/io/formats/style/test_style.py | 22 ------------------ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/pandas/tests/io/formats/style/test_exceptions.py b/pandas/tests/io/formats/style/test_exceptions.py index 49c0782f18c25..897d779cc3647 100644 --- a/pandas/tests/io/formats/style/test_exceptions.py +++ b/pandas/tests/io/formats/style/test_exceptions.py @@ -3,6 +3,7 @@ jinja2 = pytest.importorskip("jinja2") from pandas import DataFrame +import pandas._testing as tm from pandas.io.formats.style import Styler @@ -31,3 +32,25 @@ def test_footer_bad_length(styler, kwarg, expected): msg = f"{expected} must have same length as ``func``" with pytest.raises(ValueError, match=msg): styler.set_footer(func=["mean"], **kwarg) + + +def test_set_footer_warn(): + df = DataFrame([["a"]]) + styler = df.style.set_footer(["mean"], errors="warn") + msg = ( + "`Styler.set_footer` raised Exception when calculating method `mean` on " + "column `0`" + ) + with tm.assert_produces_warning(Warning, match=msg): + styler._translate(True, True) + + +def test_set_footer_raise(): + df = DataFrame([["a"]]) + styler = df.style.set_footer(["mean"], errors="raise") + msg = ( + "`Styler.set_footer` raised Exception when calculating method `mean` on " + "column `0`" + ) + with pytest.raises(Exception, match=msg): + styler._translate(True, True) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 2ba6152fd8fc8..cf646bf544ae6 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1590,25 +1590,3 @@ def udf_func(s): assert row[1]["value"] == exp_label[r] # test label is printed assert f"descriptor_name descriptor{r}" in row[1]["class"] # test css - - -def test_set_footer_warn(): - df = DataFrame([["a"]]) - styler = df.style.set_footer(["mean"], errors="warn") - msg = ( - "`Styler.set_footer` raised Exception when calculating method `mean` on " - "column `0`" - ) - with tm.assert_produces_warning(Warning, match=msg): - styler._translate(True, True) - - -def test_set_footer_raise(): - df = DataFrame([["a"]]) - styler = df.style.set_footer(["mean"], errors="raise") - msg = ( - "`Styler.set_footer` raised Exception when calculating method `mean` on " - "column `0`" - ) - with pytest.raises(Exception, match=msg): - styler._translate(True, True) From f158eebb6f9098214638eabc895af462b1754321 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 20:48:09 +0100 Subject: [PATCH 21/44] concatenate instead --- pandas/io/formats/style.py | 22 +++++++++++++++++++--- pandas/io/formats/style_render.py | 12 ++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 75d05d6af1cdf..8d13ec5babcfb 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -251,6 +251,7 @@ def __init__( cell_ids=cell_ids, precision=precision, ) + self.concatenated: Styler | None = None # validate ordered args thousands = thousands or get_option("styler.format.thousands") @@ -271,6 +272,19 @@ def __init__( thousands=thousands, ) + def concat(self, other: Styler) -> Styler: + if not self.data.columns.equals(other.data.columns): + raise ValueError("`other.data` must have same columns as `Styler.data`") + other.set_table_styles( + css_class_names={ + "data": self.css["foot"], + "row_heading": self.css["foot_heading"], + "row": self.css["foot"], + } + ) + 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 +1419,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 @@ -1436,6 +1451,7 @@ def _copy(self, deepcopy: bool = False) -> Styler: deep = [ # nested lists or dicts "css", "descriptors", + "concatenated", "_display_funcs", "_display_funcs_index", "_display_funcs_columns", @@ -2364,14 +2380,14 @@ 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", - "descriptor": "descriptor", - "descriptor_name": "descriptor_name", - "descriptor_value": "descriptor_value"} + "foot": "foot", + "foot_heading": "foot_heading"} Examples -------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 3933246d3b504..0571e2eef7b26 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -131,6 +131,8 @@ def __init__( "descriptor": "descriptor", "descriptor_value": "descriptor_value", "descriptor_name": "descriptor_name", + "foot": "foot", + "foot_heading": "foot_heading", } # add rendering variables @@ -232,6 +234,8 @@ def _compute(self): (application method, *args, **kwargs) """ + if self.concatenated is not None: + self.concatenated._compute() self.ctx.clear() self.ctx_index.clear() self.ctx_columns.clear() @@ -329,6 +333,14 @@ def _translate( ] d.update({k: map}) + if self.concatenated is not None: + dx = self.concatenated._translate( + sparse_index, sparse_cols, max_rows, max_cols, blank + ) + d["body"].extend(dx["body"]) + d["cellstyle"].extend(dx["cellstyle"]) + d["cellstyle_index"].extend(dx["cellstyle"]) + table_attr = self.table_attributes if not get_option("styler.html.mathjax"): table_attr = table_attr or "" From bf06ce1322c31abb264dcef35f05232b508d91c3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 20:52:08 +0100 Subject: [PATCH 22/44] removing redundant code --- doc/source/reference/style.rst | 2 +- doc/source/whatsnew/v1.5.0.rst | 2 +- pandas/io/formats/style.py | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index abb17c89b0b78..77e1b0abae0c4 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -42,7 +42,7 @@ Style application Styler.format Styler.format_index Styler.hide - Styler.set_footer + 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 fb87de57fe80f..3acd0b2d3be9d 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -21,7 +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.set_footer` which allows adding customised footer rows to explore and make calculations on the data, e.g. totals and counts etc. (:issue:`43875`) + - 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 8d13ec5babcfb..3d31aa22483f2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1450,7 +1450,6 @@ def _copy(self, deepcopy: bool = False) -> Styler: ] deep = [ # nested lists or dicts "css", - "descriptors", "concatenated", "_display_funcs", "_display_funcs_index", @@ -1994,9 +1993,6 @@ def export(self) -> dict[str, Any]: Can be applied to a second Styler with ``Styler.use``. - .. versionchanged:: 1.5.0 - Adds ``descriptors`` to the exported items for use with ``set_footer``. - Returns ------- styles : dict @@ -2018,7 +2014,6 @@ def export(self) -> dict[str, Any]: - Whether axes and names are hidden from the display, if unambiguous - Table attributes - Table styles - - Descriptors, i.e. from ``set_footer`` The following attributes are considered data dependent and therefore not exported: @@ -2048,7 +2043,6 @@ def export(self) -> dict[str, Any]: "hide_index_names": self.hide_index_names, "hide_column_names": self.hide_column_names, "css": copy.copy(self.css), - "descriptors": copy.copy(self.descriptors), } def use(self, styles: dict[str, Any]) -> Styler: From af287e23e875a6ada241f74de66050621076483e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 20:54:50 +0100 Subject: [PATCH 23/44] removing redundant code --- pandas/io/formats/style.py | 120 ++++++++++++------------------------- 1 file changed, 39 insertions(+), 81 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3d31aa22483f2..53cbf91a8ab53 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -273,6 +273,45 @@ def __init__( ) def concat(self, other: Styler) -> Styler: + """ + + Notes + ----- + These metrics are calculated at render time and can therefore be exported + and used on other general Styler objects. + + Footers are applied to Styler output formats including ``to_html``, + ``to_latex`` and ``to_string``, but not, currently, ``to_excel``. + + Examples + -------- + A common use case is adding totals rows for descriptive tables, with ``func``. + + >>> df = DataFrame([[4, 6], [1, 9], [3, 4], [5, 5], [9,6]], + ... columns=["Mike", "Jim"], + ... index=["Mon", "Tue", "Wed", "Thurs", "Fri"]) + >>> styler = df.style.set_footer(["sum"]) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_simple.png + + User defined functions and an ``alias`` can also be used, which is useful for + displaying metrics such as dtypes, missing value counts, or unique value + counts etc. + + >>> def reject_h0(s): + ... count = (s > 0.8).sum() + ... return "Reject" if count > 3 else "Accept" + >>> df = DataFrame({ + ... "Machine 1": np.random.rand(10), + ... "Machine 2": np.random.rand(10), + ... "Machine 3": np.random.rand(10), + ... }) + >>> df.style.highlight_between(left=0.8, props="color: red;") + ... .set_footer(func=[reject_h0], + ... alias=["Hypothesis Test:"]) # doctest: +SKIP + + .. figure:: ../../_static/style/footer_hypothesis.png + """ if not self.data.columns.equals(other.data.columns): raise ValueError("`other.data` must have same columns as `Styler.data`") other.set_table_styles( @@ -2071,7 +2110,6 @@ def use(self, styles: dict[str, Any]) -> Styler: - "hide_index_names": whether index names are hidden. - "hide_column_names": whether column header names are hidden. - "css": the css class names used. - - "descriptors": list of descriptors, typically added with ``set_footer``. Returns ------- @@ -2114,12 +2152,6 @@ def use(self, styles: dict[str, Any]) -> Styler: self.hide_column_names = styles.get("hide_column_names", False) if styles.get("css"): self.css = styles.get("css") # type: ignore[assignment] - if styles.get("descriptors") and styles["descriptors"]["methods"]: - self.set_footer( - func=styles["descriptors"]["methods"], - alias=styles["descriptors"]["names"], - errors=styles["descriptors"]["errors"], - ) return self def set_uuid(self, uuid: str) -> Styler: @@ -2452,80 +2484,6 @@ def set_table_styles( self.table_styles = table_styles return self - def set_footer( - self, - func: Sequence[str | Callable] | None = None, - alias: Sequence[str] | None = None, - errors: str = "ignore", - ) -> Styler: - """ - Add footer-level calculations to the output which describes the data. - - .. versionadded:: 1.5.0 - - Parameters - ---------- - func : list-like of str or callable - If a string is given must be a valid Series method, e.g. "mean" invokes - Series.mean(). - If a callable is given must accept a Series and return a scalar. - alias : list-like of str, optional - Aliases to use for the function names. Must have length equal to ``func``. - errors : {"ignore", "warn", "raise"} - If errors, will be ignored or warned returning ``NA``, or raise. - - Returns - ------- - self : Styler - - Notes - ----- - These metrics are calculated at render time and can therefore be exported - and used on other general Styler objects. - - Footers are applied to Styler output formats including ``to_html``, - ``to_latex`` and ``to_string``, but not, currently, ``to_excel``. - - Examples - -------- - A common use case is adding totals rows for descriptive tables, with ``func``. - - >>> df = DataFrame([[4, 6], [1, 9], [3, 4], [5, 5], [9,6]], - ... columns=["Mike", "Jim"], - ... index=["Mon", "Tue", "Wed", "Thurs", "Fri"]) - >>> styler = df.style.set_footer(["sum"]) # doctest: +SKIP - - .. figure:: ../../_static/style/footer_simple.png - - User defined functions and an ``alias`` can also be used, which is useful for - displaying metrics such as dtypes, missing value counts, or unique value - counts etc. - - >>> def reject_h0(s): - ... count = (s > 0.8).sum() - ... return "Reject" if count > 3 else "Accept" - >>> df = DataFrame({ - ... "Machine 1": np.random.rand(10), - ... "Machine 2": np.random.rand(10), - ... "Machine 3": np.random.rand(10), - ... }) - >>> df.style.highlight_between(left=0.8, props="color: red;") - ... .set_footer(func=[reject_h0], - ... alias=["Hypothesis Test:"]) # doctest: +SKIP - - .. figure:: ../../_static/style/footer_hypothesis.png - """ - if func is not None: - if alias is not None and len(alias) != len(func): - raise ValueError("``alias`` must have same length as ``func``") - - self.descriptors = { - "methods": func, - "names": alias, - "errors": errors, - } - return self - def set_na_rep(self, na_rep: str) -> StylerRenderer: """ Set the missing data representation on a ``Styler``. From aa02e865469969723e398a68bdc3dc031462380c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 20:57:26 +0100 Subject: [PATCH 24/44] removing redundant code --- pandas/io/formats/style.py | 3 - pandas/io/formats/style_render.py | 200 ------------------------------ 2 files changed, 203 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 53cbf91a8ab53..80bbd9a3b06d5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2090,9 +2090,6 @@ def use(self, styles: dict[str, Any]) -> Styler: Possibly uses styles from ``Styler.export``. - .. versionchanged:: 1.5.0 - Adds ``descriptors`` to the used items for use with ``set_footer``. - Parameters ---------- styles : dict(str, Any) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 0571e2eef7b26..725d34c13f66c 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -16,7 +16,6 @@ Union, ) from uuid import uuid4 -import warnings import numpy as np @@ -34,7 +33,6 @@ from pandas.core.dtypes.generic import ABCSeries from pandas import ( - NA, DataFrame, Index, IndexSlice, @@ -55,12 +53,6 @@ CSSProperties = Union[str, CSSList] -class Descriptors(TypedDict): - methods: Sequence[str | Callable] | None - names: Sequence[str] | None - errors: str - - class CSSDict(TypedDict): selector: str props: CSSProperties @@ -128,9 +120,6 @@ def __init__( "level": "level", "data": "data", "blank": "blank", - "descriptor": "descriptor", - "descriptor_value": "descriptor_value", - "descriptor_name": "descriptor_name", "foot": "foot", "foot_heading": "foot_heading", } @@ -142,11 +131,6 @@ def __init__( self.hide_columns_: list = [False] * self.columns.nlevels self.hidden_rows: Sequence[int] = [] # sequence for specific hidden rows/cols self.hidden_columns: Sequence[int] = [] - self.descriptors: Descriptors = { - "methods": None, - "names": None, - "errors": "ignore", - } self.ctx: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_index: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) self.ctx_columns: DefaultDict[tuple[int, int], CSSList] = defaultdict(list) @@ -303,9 +287,6 @@ def _translate( head = self._translate_header(sparse_cols, max_cols) d.update({"head": head}) - foot = self._translate_footer(max_cols) - d.update({"foot": foot}) - # for sparsifying a MultiIndex and for use with latex clines idx_lengths = _get_level_lengths( self.index, sparse_index, max_rows, self.hidden_rows @@ -415,97 +396,6 @@ def _translate_header(self, sparsify_cols: bool, max_cols: int): return head - def _translate_footer(self, max_cols: int): - """ - Build each within table as a list - - Using the structure: - +----------------------------+---------------+---------------------------+ - | index_blanks ... | name_0 | column_values | - 1) | .. | .. | .. | - | index_blanks ... | name_n | column_values | - +----------------------------+---------------+---------------------------+ - - Parameters - ---------- - max_cols : int - Maximum number of columns to render. If exceeded will contain `...` filler. - - Returns - ------- - foot : list - The associated HTML elements needed for template rendering. - """ - if self.descriptors["methods"] is None: - foot = None - else: - foot = [] - descriptors, names = self._calc_wrapped_descriptor_methods( - self.descriptors["methods"], self.descriptors["names"] - ) - for r, row in enumerate(descriptors.itertuples()): - descriptor_row = self._generate_footer_row((r, row, names[r]), max_cols) - foot.append(descriptor_row) - return foot - - def _calc_wrapped_descriptor_methods( - self, methods: Sequence[str | Callable], names: Sequence[str] | None - ) -> tuple[DataFrame, list[str | None]]: - """ - Use DataFrame.agg to calculate the UDF methods displayed in footer - - Wraps UDFs so they do not raise errors on, for example, non-conforming dtypes. - - Parameters - ---------- - methods : sequence of str or callable - A list of methods that will called on each column of data. - names : sequence of str - A list of aliases that will overwrite the system method names in display. - - Returns - ------- - DataFrame, list - """ - from functools import update_wrapper - - def _err_wrap(s: Series, method: Callable): - if not isinstance(s, Series): # called to trigger `agg` to use `df.apply` - raise TypeError("`agg` requires Series to reduce") # gh 45800 - try: - ret = method(s) - except Exception as e: - msg = ( - "`Styler.set_footer` raised Exception when calculating method " - f"`{method.__name__}` on column `{s.name}`" - ) - if self.descriptors["errors"] == "ignore": - return NA - elif self.descriptors["errors"] == "warn": - warnings.warn(msg, Warning) - return NA - else: - raise Exception(msg) from e - else: - return ret - - methods_: list[Callable] = [ - getattr(Series, method) if isinstance(method, str) else method - for method in methods - ] - for i, method in enumerate(methods_): - wrapper = partial(_err_wrap, method=method) - update_wrapper(wrapper, method) - methods_[i] = wrapper - - if names is not None: - names_: list[str | None] = list(names) - else: - names_ = [method.__name__ for method in methods_] - - # performance: should probably subsample based on hidden columns so as no calc - return self.data.agg(methods_), names_ - def _generate_col_header_row(self, iter: tuple, max_cols: int, col_lengths: dict): """ Generate the row containing column headers: @@ -913,96 +803,6 @@ def _generate_body_row( return index_headers + data - def _generate_footer_row(self, iter: tuple, max_cols: int): - """ - Generate the row containing calculated descriptor values for columns: - - +----------------------------+---------------+---------------------------+ - | index_blanks ... | descriptor_i | value_i by col | - +----------------------------+---------------+---------------------------+ - - Parameters - ---------- - iter : tuple - Looping variables from outer scope. - max_cols : int - Permissible number of columns. - - Returns - ------- - list of elements - """ - - r, row, name = iter - - # number of index blanks is governed by number of hidden index levels - index_blanks = [ - _element("th", self.css["blank"], self.css["blank_value"], True) - ] * (self.index.nlevels - sum(self.hide_index_) - 1) - - # name cell - base_css = f"{self.css['descriptor_name']} {self.css['descriptor']}{r}" - if name is not None and not self.hide_column_names: - name_css = base_css - name_val = name - else: - name_css = f"{self.css['blank']} {base_css}" - name_val = self.css["blank_value"] - descriptor_name = _element("th", name_css, name_val, not all(self.hide_index_)) - - # descriptor values - format_ = { - "formatter": None, - "decimal": get_option("styler.format.decimal"), - "thousands": get_option("styler.format.thousands"), - "precision": get_option("styler.format.precision"), - "na_rep": get_option("styler.format.na_rep"), - "escape": get_option("styler.format.escape"), - } # set defaults from Styler options - display_func: Callable = _maybe_wrap_formatter( - formatter=format_["formatter"], - decimal=format_["decimal"], # type: ignore[arg-type] - thousands=format_["thousands"], - precision=format_["precision"], - na_rep=format_["na_rep"], - escape=format_["escape"], - ) - descriptor_values: list[Any] = [] - visible_col_count = 0 - for c, col in enumerate(self.columns): - if c not in self.hidden_columns: - header_element_visible = True - visible_col_count += 1 - header_element_value = row[c + 1] - else: - header_element_visible = False - header_element_value = None - - if self._check_trim( - visible_col_count, - max_cols, - descriptor_values, - "th", - f"{self.css['descriptor_value']} {self.css['descriptor']}{r} " - f"{self.css['col_trim']}", - ): - break - - body_element = _element( - "th", - ( - f"{self.css['descriptor_value']} {self.css['descriptor']}{r} " - f"{self.css['col']}{c}" - ), - header_element_value, - header_element_visible, - display_value=display_func(header_element_value), - attributes="", - ) - descriptor_values.append(body_element) - - return index_blanks + [descriptor_name] + descriptor_values - def _translate_latex(self, d: dict, clines: str | None) -> None: r""" Post-process the default render dict for the LaTeX template format. From 0ae6b53a6a9b5916a120bf5c386c8632cb1c60ed Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 20:58:09 +0100 Subject: [PATCH 25/44] removing redundant code --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 80bbd9a3b06d5..24e8b8b738a0f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2050,7 +2050,7 @@ def export(self) -> dict[str, Any]: The following items are exported since they are not generally data dependent: - Styling functions added by the ``apply`` and ``applymap`` - - Whether axes and names are hidden from the display, if unambiguous + - Whether axes and names are hidden from the display, if unambiguous. - Table attributes - Table styles From 7ebddd0fed99e7551a1e968b58bc70bc0894d14a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 21:00:29 +0100 Subject: [PATCH 26/44] removing redundant code --- pandas/io/formats/style_render.py | 3 --- pandas/io/formats/templates/html_table.tpl | 19 ------------------- pandas/io/formats/templates/latex_table.tpl | 10 ---------- pandas/io/formats/templates/string.tpl | 8 -------- 4 files changed, 40 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 725d34c13f66c..eb8c3021b464a 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -855,9 +855,6 @@ def _translate_latex(self, d: dict, clines: str | None) -> None: body.append(row_body_headers + row_body_cells) d["body"] = body - if d["foot"] is not None: - d["foot"] = [[{**col, "cellstyle": []} for col in row] for row in d["foot"]] - # clines are determined from info on index_lengths and hidden_rows and input # to a dict defining which row clines should be added in the template. if clines not in [ diff --git a/pandas/io/formats/templates/html_table.tpl b/pandas/io/formats/templates/html_table.tpl index 2294ef4f935e3..17118d2bb21cc 100644 --- a/pandas/io/formats/templates/html_table.tpl +++ b/pandas/io/formats/templates/html_table.tpl @@ -58,25 +58,6 @@ {% block after_rows %}{% endblock after_rows %} {% endblock tbody %} -{% block tfoot %} -{% if foot is not none %} - -{% for r in foot %} - -{% if exclude_styles %} -{% for c in r %}{% if c.is_visible != False %} - <{{c.type}} {{c.attributes}}>{{c.display_value}} -{% endif %}{% endfor %} -{% else %} -{% for c in r %}{% if c.is_visible != False %} - <{{c.type}} {%- if c.id is defined %} id="T_{{uuid}}_{{c.id}}" {%- endif %} class="{{c.class}}" {{c.attributes}}>{{c.display_value}} -{% endif %}{% endfor %} -{% endif %} - -{% endfor %} - -{% endif %} -{% endblock tfoot %} {% endblock table %} {% block after_table %}{% endblock after_table %} diff --git a/pandas/io/formats/templates/latex_table.tpl b/pandas/io/formats/templates/latex_table.tpl index b921dd2f8afe3..7858cb4c94553 100644 --- a/pandas/io/formats/templates/latex_table.tpl +++ b/pandas/io/formats/templates/latex_table.tpl @@ -46,16 +46,6 @@ {% endif %} {% endfor %} -{% if foot is not none %} -{% if midrule is not none %} -\{{midrule}} -{% endif %} -{% for row in foot %} -{% for c in row %}{% if not loop.first %} & {% endif %} -{{parse_header(c, multirow_align, multicol_align, False, convert_css)}} -{%- endfor %} \\ -{% endfor %} -{% endif %} {% set bottomrule = parse_table(table_styles, 'bottomrule') %} {% if bottomrule is not none %} \{{bottomrule}} diff --git a/pandas/io/formats/templates/string.tpl b/pandas/io/formats/templates/string.tpl index 017c65b2bcd2d..06aeb2b4e413c 100644 --- a/pandas/io/formats/templates/string.tpl +++ b/pandas/io/formats/templates/string.tpl @@ -10,11 +10,3 @@ {% endif %}{% endfor %} {% endfor %} -{% if foot is not none %} -{% for r in foot %} -{% for c in r %}{% if c["is_visible"] %} -{{ c["display_value"] }}{% if not loop.last %}{{ delimiter }}{% endif %} -{% endif %}{% endfor %} - -{% endfor %} -{% endif %} From 5a7ccc7db08a5172f57552bdc54d5f3ff6e97be9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 21:53:01 +0100 Subject: [PATCH 27/44] doc edits --- doc/source/_static/style/footer_simple.png | Bin 8494 -> 8717 bytes pandas/io/formats/style.py | 37 ++++++++++++++++++--- pandas/io/formats/style_render.py | 2 ++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/doc/source/_static/style/footer_simple.png b/doc/source/_static/style/footer_simple.png index 6bf0d9f7aa3f13ae1b341f03de6cd1f89c15d979..56dc3c09cc700209a5d2922304a3dc48d48581d0 100644 GIT binary patch delta 5450 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 RbSD&`qM#vv!)c$zmh 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 z zdeHj6OZ9DdWJAO(0)D@U?G&Or-S?u&T<5>cl|aA|mb!E=^dr`Al9(DArvg|5Xa6T5tjT^TRF6nDTptSj0zMe8TPq zLb2iClqV!wyy6A$2kTI6B)O=-v*CR)5c-F8R!V@oX0S3q4;B!hGha>-v5znmO6}Za z)R)BA52SKzWVJ`j9ECIKcLN~4LGI7v)KE7Op98CDRW;+f_{aV&oJ*aQUpceMTuTkh zNfY_ad77_Icet)k9EUr@5=Rnl8f;B3(S(M_h*i>+UJ;}0f>CwXtD14jbj;(#J-3xE zf>Z(Ujv4V>10oI)TU`Guh_)zxZ>=xM6aot%ii0kz;Fx%X;*Y_mfr@u`u^cZk-o2Uu z(LNG*b0?Hn0jGNQ9zw6Zk49R6J58iWBkGhs@{HBSer`10kT&TM*LnA<42?F(zWS`N zK3cBokY#JirI#3T6=>1^he@lB`r#fg7)-?gSGitEj1K!lG!g%EYaSK_V_43Nxiw$u z(}Y9u`v!2z5T2nx%Da5i9Ib@Mgi54C7>;%6lP?3KWs^m(mCouf0{@bb#b`j;>#*Uu zEIxVr{O$I1b>a40W1Zj)qE0}PUJMq`TVv4lLy!)E-Dr3X^x1FS0dRlcQALEmxU$m<(PS4(nw4ycopH-x}B;%-7 zEPorqwxCqWmWD`j=zX;k^2p-W&Zq$j-IH($dkwtOiiI~xV;LjPk wP0m{ji2Ge3GN>hZ-n%*cv?qcckW0`@tq4;Jm?g;^_NEX=Lq!LB$7mJ&e~O+>pa1{> diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 24e8b8b738a0f..a1ef506b08fb6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -274,14 +274,41 @@ def __init__( def concat(self, other: Styler) -> Styler: """ + Append another Styler to combine the output into a single table. + + 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 ----- - These metrics are calculated at render time and can therefore be exported - and used on other general Styler objects. + 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. + + The following are the current limitations: + + - ``table_styles``, ``table_attributes``, ``caption`` and ``uuid`` are all + inherited from the original Styler and not ``other``. + - the concatenated object will not currently export via ``to_excel``, although + support for `to_html``, ``to_latex`` and ``to_string`` is available. + - hidden columns and hidden index levels will be inherited from the + original Styler - Footers are applied to Styler output formats including ``to_html``, - ``to_latex`` and ``to_string``, but not, currently, ``to_excel``. + A common use case is to concatenate user defined functions with + ``DataFrame.agg`` or with described statistics via ``DataFrame.describe``. + See examples. Examples -------- @@ -290,7 +317,7 @@ def concat(self, other: Styler) -> Styler: >>> df = DataFrame([[4, 6], [1, 9], [3, 4], [5, 5], [9,6]], ... columns=["Mike", "Jim"], ... index=["Mon", "Tue", "Wed", "Thurs", "Fri"]) - >>> styler = df.style.set_footer(["sum"]) # doctest: +SKIP + >>> styler = df.style.concat(df.agg(["sum"]).style) # doctest: +SKIP .. figure:: ../../_static/style/footer_simple.png diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index eb8c3021b464a..8761023e09d96 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -219,6 +219,8 @@ def _compute(self): (application method, *args, **kwargs) """ if self.concatenated is not None: + self.concatenated.hide_index_ = self.hide_index_ + self.concatenated.hidden_columns = self.hidden_columns self.concatenated._compute() self.ctx.clear() self.ctx_index.clear() From bba607743e854fbdc8c58a99f48e4b9e9e417702 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 22:10:42 +0100 Subject: [PATCH 28/44] doc edits --- doc/source/_static/style/footer_extended.png | Bin 0 -> 12302 bytes .../_static/style/footer_hypothesis.png | Bin 32678 -> 0 bytes pandas/io/formats/style.py | 31 ++++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) create mode 100644 doc/source/_static/style/footer_extended.png delete mode 100644 doc/source/_static/style/footer_hypothesis.png diff --git a/doc/source/_static/style/footer_extended.png b/doc/source/_static/style/footer_extended.png new file mode 100644 index 0000000000000000000000000000000000000000..95a26fdd3ab90ecf55f31600fcffbf028601efba GIT binary patch literal 12302 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

6Pg+pdNCsKrYIRzJU&bx9>cLFnd|@N zX=!VlqORvD24sU%EFoF9ZOeIiobhKI6Vc2ia{Pf}Mq>Cywb6vJV;^-cl)e(%$Q2<+ zA{O8BfKC&=NIZsxF5u0+;~@2F)i_Rb^Zn&WAgCpfLY&CH_w}wAK|}8O@AtZ~KgCIM zsJhHo!2C@-y-#s&jaruFP*&0(>?g@{On9{Ya%=J%6x0VZv+cj16+y!AD1o7Sisk(q zKZqotW7mDFhQs4h{_?!_o~l*TG>V49f4iu>Jd9`8PC~h!amjZ*$!Z(@TR`nOoXF2& z<_3b;`%go7OWtEb?R!a@ULufh!z5S?*7u?x>MG9vWw6$L&63KR^EJ#q83pMW&oo0> zBj%WU=vso%Q&a_XA{dFEn)8jG!AG8l$1F3>7)%z}EYlyNn2?_gPC?OP+N#i|WSRpX0JT z4(=qxEeq2MC0LE&n>1!cG4mBZ`6N4(PpPBUb={aZ5GaMNcfbW&*5v9*z|h2XHyLW7 zI`p=%1+*@&RY$VwxXg*U`A#yAfUgB?#E-ncZoU6xKP%0AZ1htmot-5VELg3zP z&KAuU>x2`7!#X_g|9Yw)K8VOWS*x0%_Gco7bJN0DNcX9+I=yUJv~AKwrPO3T!g%U^ zLjR3agQUw>TT3gE(a3YqorW^<+2dLNd7n%=ckgWcPjh- z_IkkC`xp~VB%tTgprxx3oM4vc>((FFd|LZ`ysEB;R^R`%MKkyzyB_v#%iq7Lww~W* zNBcW!Y^xU^*>N$R9l7i_VoWGP1q@c5tLo7cmLQQK(3XKMdrHVd<)v<+!RTilvN} z71wXFadJ*fvd-9C8AV?atDAXBI-`n8(Frq7+!~ao_YDp~OLg z3k~vldpzREXqR|osP6;2O3Hf)(I)##f-a1ia%6Z4*bSc*x9{@r zh6PHDR|dQokMr(`Yr^jd?wPAb42`3ttq021l6UL&y*GQAwThe6)GZ-m49*#*shWvb zRyh$`ai#4?x!!g%_EuKXtv#&MTty{{VQ<&)MjOP*2r50QAeI~`W_2)1;gOg~&p-}DQRs62W{#>2Dz)$bt^>3z;lBt_=)otD?24`lAF+SJhS zeH%(ZvQgmj2bx=rHcgVy4b~L%2)>!)uE3A*FxQ|ssHhqgE$1or8ZJz#DC~D^Saw=I z!p7~(UqaCnO*8z~sW`@}NMdwxjZZs;A!09u5h)#i8Yk9RhY71-dCt48Z6*`&-s+mg z>~XAKuAA~+C#5HO&$~F@Iw{VJM~?+~A$p8<%MKurN9u-8cud_*m!-&+#f5#=onr+C z4IGOtcS5l+TUj(HyF!=BjN^XZZFiaBHQp|3uNNx4r);mvUdvuf>zf~Bm}Oc{!WzH@ zJBdcrn;zT2VLXtQVs=nt;$tPbMumqZ?8$xEdty%K2 zCvsBsP@ya4hRhp0EAby+R~O;xX%sa>&^06;1R9z1W|;Faoq{71Lms+)-&~|Nn{Vf8KxcF zDgn?yTEz#Rx?qff{B)q|X1;C~U50@65@Y~Wm0DCYUmc*JX3YUHRXu|k!#N44ta351 zt=BCXtdenoW4DAQq7EP|W8en4!UWSurIK098w*ccE;reS)}n|f0o^h)gC0)$NI)Y4 zRy$c*eR;L~aV9uGf>aW0c{kuZO6dV*m(@5tp?wO7I}<>!X@(i_{rr%0jUSS39GA(y z=3n8|&2WM)O+K{Yc%qEsak?3h!I+5Xjn07`M(mvDq zT$ZrqITxT)c2#<&&hkA=rC)x>)(Q20)z-Qb&{<=|r4%X}!Q_7u%}g=hnQEh`ZY*pj zs81VZ6Nn!edH=J0y}YiZ;}_73v?0NEWvrx5#s6zD3*BZr2)YgUH)unp^4M5aayI+QUj7U=f3EJGJ}zh|Hk?rhKY)#xdU|r`<9d6s2Ogb`*hy8u><=of6 z_IF#EIG*oWO!0};TB=Fyb)o01fu+~IuO;uH0xu_#yEK(tYH4K2nR~d*0-n9pbwpXp z{*LFuCu6ZS^iLo|bu93>4OqFMBYBC3O*3sT3{d!thMkrrXBVGJH1$EIj`WhJB=TI; z?WJ~j?7m$@7Lqvq1yrmp^Ib$~7Uhrtdpln;_T|GE(yX%@Pd_?LOAqk1rfp&*W`-J2 z2x>y&MUN61;Q^P&762zmOWh7ln=>TvtJSMcH)Dye19BYH2-FDqU9(>Z{_fk+D#&_b zp{)A`*_j0XqM?uh4K;?IM+dq5PElNm?-@IJb$d2?+b?0*JX+Y9A8Jj9Yq9()L0UM} zg9A`RKbuO`^^)%LYV9!f9792e@xnCxQ?8=Kxrb2 z+-`Q-$<7EvlS@mH1QK@-@w9VyDe=lfZ>6r;R*H-S||G{`aPz*kqbPw0+R*i%;vfOgp_P+9H?@8K9z$d4Eic8)t z#nTedW_nBdBJ9})kH=V3RGU`1e*Sl~_QC^MTNxccawfUAR2JV?CcLrt1=q6YubE|c z^`KGJsKmAT65KnVkSW`P)X!}fmvZ?irIf)|C5)}UfArc2gHcrSaYn!#YzWn4*6lOhT}T=H8g4i=wJn`&zdj4Y_6@x%G=Q<$Py8G#Iap09xgCcCqT ztep3nNSWuklgr?;Yi7%H>D083 zqk0XSqg3In(iF7htB$&eX;6_EAe3DSE9&jSf?|RzP1{M!95N*WW#rRdo5rU1nGCRZ>~1MB?HwHAXDh#5dpjin zmuYLCmM?jSwRYF=#&~FcSM>G7II$IYB@ab?uk~%nxy^1!t)C_h99Kou0{b0V*fTM~ z)Jtze($Jn??INgu5g|q|i;ng7dA3AB3*>QH;Qgf#`Z5d~A{xJ^37`P|dW#yaf+UTy zH7fQt)@0^wleh_bIi~#>`v}88Iq4JvQzKu_E+wj^uCvhg`~nRl*Ix?uNtKDdsTh5c zX6t6QNNu!L=A^!rFVB~;aAshVRq7f>1e{1|ox6-GG8*jFMP@h^h4w{60J0n@A`VXT zk-<1rK-0D=zNM$wvMk}Vfu$v`=l7JO$!?3zG2sK08(dnx>eq0+OoOuBtBKJc&-O{J zTZCp%kqt3S1y(h8TNZdt8j2dS?psVl`fp%O5?ZnRdayHm<_B;y{3{I%MD;3S{9>)A z(g(N0cdLVt?*R#q18_{xJ*{jay!=NIaK8d*IxGpy^*kHaxezzI=#WEoh$$qu^0W*g2 z?30z{zbwrbW)NGF|Hb{OI;p3kuXbJgEd4&DmKx+|Vu9xs3ErZVdG0Bg2}L(>4;LrB zemNr9yM@2Fga7`7XSrpMqb?sdEVY+(78M5<8rUSa!?@st#s zK|$x=98VhOF>R&olRMIJdxKW8^%qL=y23mY)%tY;Vz^b)KXM?olOi z&=l{|p;cEElqr(Ldq4%spK}fIZR`{1aweqVR2^jth=bSOr@^u$p;V}^Q*S>#6APd2 z77BcJe$l;jG+j<{@p{1V1MlIwymN5^Bp#1U zKBj4Ep6z-ELGFAEcZLhx(r82?{*75@kguRl@YR8AD8Z4)!wJweU+F!*K^F&9Bj;M= z`Q9a9sA=>UrmF2qjoOn&q2BunLCj#YS;SAWujnMy@Bv(m#qE#A!IJulM(l5ZqV9%n5p^nE*u5(K7_q7B@AwPyf_!;FhXUIOxD zz~iup>iJxXMYy66Gb{1DyWn0^vSFMvNNwrEg!`OYJG>A+5j;U>vFl{y? zXjO=ydr3p1G7}Tl>6-Z!Re?M!KZ?`>{&qtKCand`3S2#6Uq*BFOVyhU9cGl}UU4E(;4rq{{UuOi>j> z2Dn;#Vqx+^3#LkhCXcb?d%29G!WoY)agM$|i;$3L;@-s|5)xf)yV0y*#AMyi9lg#PA`m#ceH#`hS*2GSpPSrS6kVxf(Gi@_Q)grdD|JMBR&y#u4~EPFL65AhS|1x_?(l8pAA3Qo)Xe z{*C+(Fi=WOOuez0T!=}I|8XBV1dt3}Z*}?_HV-Bo%@I*I)s`e97G(BF*w!t6`0U z%U`L=LZzm6Glu0}UYuj%>cBN11F&qkK{I5P)!tlk#cO}^3+F=!!F>gbia0JFI=$lS zB=)q?mY5#>1At=`VTE8A7)E-0BT%oO*F7&u`_@a1@nt-maP19@NzxFiB37+htu~3 zH1?1(%Z8M$ra7^?^Q1CWw?=yqCZFLzWxDLe^UHnh9{w{7d_$NqpHZK{MM8a4Qzo@N=o=isP6m>+}_MLhy&l|?^A zHGYriB-S^KVZ|y!tr~QTa2%@lkgkq82&*0D=_*R}XY&Tg&Rk7H;Co^?R$(WW$pKJ^ zc3sh3=?f7G1=GI8D51ma*>Q-PA-h!+ z{+M~yH{~`Clfl=cI0K~7i>ie2othiTgPw@n@Z&EZ6pHFgtZOwpKiCJXFHNq$Q3wIR zS$B>QV`cLDnsprpI`p*r)s+yjePNy@U3Tj<&uKNq()Q#C*k0&xzXZ~JI@x~Q4=C_q z0x(0EZT(-ZBO@$EKcEMvqnD}MF*&z>XJpT+Mr6-mMgY2AP4k{(AClN^0os-xX+^_~nhYIfU9QLCGSy7@iUQR%ZAkf* zDl9riiHvDo+m&aqm2@eymo$kHwM?~`#maN+ADK~}ua4s&!eN$#2Urw`(tB2~fj``khv zXTN;{hfl`u>sO%QzssK!xaC>z(%D1pcXXBWn3gkIvU7aT@=vxeIDIaR?45oi!(1J#es zQzOmcJR~zdO}a~o$8s>DPjRCEL7&?-=4$A&t6ZTsZ#9)SL`BzK)icDRd!SgdX=zP3 zk&<_*ykjjIYw54**|XVoR0Ls+-4>B@QX03dYgADRBLi27c@pE5KL(Tadwepi@4X$6 z9Lk;G8|EA(artdqfI#@au0-Zn`kP1ga%a$b{mF6oAYa%pD$l%}Jo~9X(OIiE6+V;* z-G>s9=6U;oZRtiIN+c48*1r5hiKKie5efqT!w*Ts{80lPf=}UG1Umkf2H;v}VywUi z*8`#gYRM>U9Pb3^=t2=#>1V!-@yTB;54^7g+&bPvDa*Ds!w@!g1feSKx>yO)!rE!su0qb21)|lvc)H(l9Q>UN)?Y^b?#m^y;^TmYH^1XX_ zVC6xBAx)1W)G-VxiM>WnYL$^2=xLsV1lO3BpzgHN86ajE)vW!E{IizXD~*pZ5q+E3@UW?_?7~pf3>9zH@g{!$( zJ0*}d$YLlNZMcL~t3K$8DzUurP1QflE+Ts;zd&YT(|2!Lt{DG2QAh9pEIlFPSaf>^ zJ;6jL>)$P7@^oCB*@_bk`NoEXOD}ug!(=;xZ*_FICwKf~YF?$F<@K+FrPm&VCI2Pc z#KO&map4?2?~~kn3lXj(%Q2kqJazK?&C5h(cUFn%d9awVI9=Xg@o~yZ@(l8v$I^Tu z3sNMUARcU-0L7Gfp8bn9qe9%WSI5q|M%4%F+DyM)5q*V5`bj`c2n z{T9qa9~Ah!IyeB=`Jklo@Y~o@-x~pjj*)7}wk>|34EBY&VF}aZSzet}rnY?mWKW2> zx~sJaDW(1eL^&}OiT{MUXr`VYATElL+P0~S0b*hIrkH)qVx)=W{GMHby*^yA%@G+hM27IFN*#O-SG*#2Fw{7Bys;XHuoTHUa zxD|U2?ov3ZNvm?QvP~+L$erzsO|1XJV&(fPo=NDh^$+c{s%Rl{G?2hkG33y9PU@!5 z4AZBzHHTO0s3o?g!Y4ahPZPc@S=X3hcYG)yi{%nVkJt2jUf;qM7syk|WbZ#>=@C`F?k(xf9YBZz^I!rhCmLcmk(g8$$V%mZ^n5(_IN2Rj3*_f~NY z!-5J>aHR5Awr08W)rnZ~ciRX->7Uv^EnHj}EkIz$_D1 z_=ejSZ>929Qrwm>^ttKYTm`?8+BDVvoAeR?i}c}Tc)Xet%%lh{=-x}Eo0`F}zW=CB z^b04Vs}Ye;IE+Sg;p{TXGR?_5K7 zvfOaL+x48)BFfmIo}g-L?uQian<_s1apvXB`=X<8l5G9FKt#Xorz8}Ng}bHS*B5O# zi>2crq_`1^S_`roP~1GpGLh~YVj2n&ZUiPqgNlh`nXoDw&ji+%UyMGq8Q>(C9T}1s z0uo|45orpkfe3y_ZNT$VrL=F1g!>;*_?sJ^@v~|E^5+5%?>lOC8cZW^s9ck3DAOS~ z*r=ePHJ3qX8VmaS{~so35ITm$F4uKsF&Db(^k;0|>manVZP9kQ1FM3fDK?}e?OSy( zuQ3GzxAIyn{)4X8qiSetih0E`zn$+=CQIZI%S83#2_89pL%EjR1W@KK_gx_44ntI z+@=;1Z&D3cgDf)RqN+i*gK|l!zKCc0U=OjLXX+^qtU{L3Xgyl!Fb}E34xlvQNN`_6 z%E8CY0^;(O*Xm#4Qr+27e|GG{(wA*Pd)mxUAGVsuw)m!5l zMg+G$Svr!-vT1mLk$9)J&`R!1fJI`9F~wG7Kp-Sk^G!0;|9vER{)c*AE7GSS2F{eX zs9Nf?U?@%ffj>=J%ezmfk`w(SF4TCcFH>apSXY&rD4J3`8Aruib@xeycFfu{UqotV z5ihV?6M^cI_N$Gy_8l+2O)FxL_G{r@btBWueXmu=D{I3I>aeR`Kvsk+CX6Y>iAN@g z-cBg23jUAOgtI+-=SjnHhQotB5RZK1RQ!5Z5D|9?8{>wHRWql6Jt^FH9~;igk-#}v z`TSE5J-g%$7HgFt*W}*qAh@pyh6T*qUy(zps)Ci>67E#h(bqSR@j5PEWRzy>yfi+A z-Y=$I1N?EeRp7}KRUdVoXwKa5FrzrnuDiPW@L$OBJUkA1iV9w_cMfviZTgrX5}wiO z_NxkA>DhyE@mX7VhJX31x^@(OL;8}Uf_bN};B1Xuadqp^5kj8pDJCR6gKVV-@$^Ph z39Du~-$1b~$r-DcrSOR6`cMCNQ@OdZlr`B!+!E4n-8=FMgbCk z86B~QmA(;h)Cd9QDFo-`z4rmjxIFkFuu4>qG~y6YFa$1EePTtH550Z46Ujg);a>dN zkHusZIifwg8KP~x-f+-HVgSzP`&lb%fMsaqX{@g5%rBNI>93Dwy9=D)l79-qPcTIy zYAE$%uL=vF+B4mMsXsBbHg-R*FXmw^N?Z0Efm#y!gnjkdQB)Ru^>bDTcYZmwFZS*I zLQc;qs1=?~{CWdNAi#-y%i4mVKJr@4Ds#=yrg`mPAYtWer!#h!m=_rGc$AW#inI7* zhHzPr)vV1~ZC#p$-D9D0ebDba5%E-6uBCLf>Z_hY(;IqdjpO=H02^yg2)1;ZDMYEJ zpd$L3EV?aM%-iqTirpMhDV&Cyuj4?dDx9*uqG97LdUP}sxQE>E z{&6SGJBbq3hpbVr;7id8&#{B8f24m6xX_1~>3+yQPc|?CR?bzY^8QarkEWNFJNG=m zx_9b6v=?*2$~Tl_{}aeHq7uBU^UE~~$O;rDZis!953)Xwy2cJK+&U`j+vhqVpFID! zM}`rAE*UOs;9kYsG720kbj`stwos@;upeDcTA^9#kK4-G`{*XoM@ic@@oiJetr2Px zV*X9j|9M{fpeZb`z;0_@zG?N8zrJ6bH`gX!VpW5#GACNHA<4TsfAzh|sqwAp_MGJL z3N7aS1e3!KZQ}GuFVKIAg)R;cbH&Hgi-J(Mq@Y^C;I>oIs$wfeQ=M#Q=A*JbMjNP= zobfzKd8*)f@BtC^J}TE~ekUHA?Q0{6lDz^*KV{5{78-R3ofKed>krft<|mjf*;#&r ze&|A|&k%~DGhBP{a6i)Qko)hTuOx~UcH5KeXY++vR&<;-*_7ua)TDJkEQ~_wB@<|z zejjcr;((is?LcBB#ayqQ94b)jyAOuBJB8+nQJjC(t{37&iF-2@I)Qrena*ELS5*DW{f2XrT_D~Vmw5>lj&pnZXTzWvvW8cfID~3#ZaYd( zfC7-j?*a&h0;nq_ej~)p5+c_K;z32p7;XAN$-5sfV$!mtcFOXu>;8)(P2aDh0d%{V zuaLgIVNz%<6^_6wneih(hzJR4i(gRK3T zJz;%hp+MUj#OaN+h0fDjJe9GGXl$q<&u040`e0fDx+=JhTF>|tfhtCb9rljOL?ryk zZzeBI^Jj*L{@CWjS&rr}8Bl;5RTJiDM&Ny8SV|}$cwpiuc9l{a$8dxCkf~?;k(Tjo zIAB$Q6w_{eiA0Le(7yz?nIvNx7P!j`vq;wTu61yxIJ4S^WWxmbB}i1*SgE35@PNn9 z{KoR@V*Qt0ERN&t2&00Y5hDMtVpy?6jv>*)u!~lh%;|t2Lr@6{aHN<|_L_^Gjc#-| z=``dJSuHLQ(Jd}9uXgv5c^1-0p{S4fT!bFmfvEgex2wK{{%ob)EkdBI(F9S+L!w^f zn0!oVE%5YGLg|CG-~U>zP;0d){K2KjPmSq@rad^XWJD*8ddzX(r14BJDH&;H_cF`I zsd^ArOup>%kxbJ1Y6zIw+Q^5MYKAWUasI8gi<>sB=y^jVJCHxnWGZ*HusU-cz5~1z zn;K0D)CbHG**O@Cgefsy;Lv*i&v-S;F}sG^&goQP|J9Cc3Kb1kXm<+P%!gB~wSL$4 zX_A~wKusf41Dt+ea(|BD+pm-5ALt>%DWSk(a2*pV35!m!%_22iEXYVuu`!^CMI=IE z$Z0{A^XIz2=13i{A(Ht;+EMoy^Q4=34|vxDeA%yVkS?=}s7r6wb-UIx_pYtOZ z(rG7B{x5F=hzilfzr1i} zp{Rd>`2FVKp;622t?E?4!L>l>;cR(9s48Pg<^0@?5mi8f8kqE$hN(mp62Z7x1@=2$=QDlJ_?1iFIP(YDqklZ z08m_xA8G+@9NOu85j8r^h1(NZHy`M?LOAC_h{`oFOkR{^;7_itfmanO>keGU<6+jP zb%~s4p`6iWH&qMfs9#%-BLsK&6NmzM_MR_R4|^9a4lymmuA9XM_-bvtthmKe|{X!(wmG4{ZSx+%_Umd43#jqjBiJrD0k!NoEd zs#tyXpk#)<1#H31eDqqsANJyqo>D48XwVWF$R8a~ieB1>{Ri9vJ2jfA8TnD`yQ85z zZ=%fmE~D*f?THMfL~g?%f{EaFC%=Tya}~}<`iTxPw75y=6W=3`WBqK1=T1AKk}v5j zM2(iSAI==gN^Tiaw-^@=*vm z2>FDahx3-YdOjN7B%GLnW%-*BjCaSgGVGPYz>*3NuIayZ4M)R@h zWWXs9$xjq987?`+4$1jx&|`t~MB*n4Q9ZPjNWlf_?ty48_e~jA#LQQJIV1jN7i9&b zhowMNtYbkOTR-t^@K5|((C?ZJxM z-%p)=5S~?1M#fcu7aUFzODaMhJa=*Ey!j&U7Gge>%nAm!7lqEmK>36eqXnPmU~VWk zS@t=v@ydKj%+IF~pv}q=l8y5+fbI)^NBePg)wy8f>aC5!$(lpui;zpXr+ua~2ItTE zE3DdD-Aps_(&eKN8krd2Kb2h=Hi7q#$4&~66{#kCzX0u!Uic4V#AURv{!l6ufNc36 zgMf!5tlGO8KIBI#AWTf9h8O-Z^GSoLzSdK}shLEcWN2gzI*+>7~ul9efdc(I~5c>EAY{0tvvLobgm$B)1*fOt`>Ddx-jV4P(iWJxXt^4YhOJ1;EVu4lG5~ol#2+Aah0Nvj;EltJw3fsd4!C=je&ai1si_sUP}2N9Df(sbs9jn$>&FlcF$ExxkPrK>U1w6X4_ z1k5dHSYtlnKJX6UmqaMc^_|dML|9BR%{}O04p1MG=x`wW;Mq1O+(NW>ITdydFEGic zHcj)$0DfUy8To4xA>$Rzb++1c3m-1fVWz{gALY{_1J?GE*N# zhh#aL`DEQU>89E??N-jd|0N!BT}JodjOk(8=@)Z+&69Gxj$wUFHmThkW@zFGNmWNN zGNVyS3Rp?!40J5-2e&OK$mRfQcjP_()z(X@>M2tk>yxo5YFcBTQ6iMX5=9aMK|#)d z2Rz)SrPY5kN9D=;B@;b>{J0;N_6NRGYizeIQt6bdsKW)QnPMufuMTvi46QB%mGwsR zR&S^UB!j7O?I@)+i(0jCFJ}v@xzO^S-K zYK4>#iWv`p&$JH!dYEO$P*Di5+j*zHg!!Ilx%r+mwR!ut*OH52?Mg><0fUDUr46l= zB@XR16m+Vli>2P;t>y2M8cvRuTb{g@+`lboJ{I_dzDioL+(HUQXWBMax0stg+GKi+ z2ID9Yni~eXs&>YrTA9EkvDHrFwN0L&>ktlsbj<#)1OG?dvAG*rJ6Mqs~P?LvY15}|W%e~(761^o;e@GD7_SXPG+ud79&{~*hr5*8w)t~mm%9tu$GWl286@vX)*PE97VPpLK+3~QC|hA zC}9yIsW)tB*qa+@%4+9@Y}NoT5B)rSD&+VlKh0y}Drbc{uriGt&$kE^p9WNt)=1>z zd_y4(72G>*bhtkB?tzmGuzj3$X~Y=W8Cxph%K$Ytz`KyjM<-wRBfPv98N~s|yS1|! z^k7YuZ96ftvxCOxB_c!&lS52=dH3a7dV%*mC7HIiD=5GG>GhB};r(SMSzW_W)6&bJ z8a>xv@#$4KWm#g}yt}3NcA!a2&r=o_x25&n3NQWtsp~BRqIdvqZyM?Dlw3l(rKLkk zN~F6R>FyNi?(XjHMx;TyJ9X)I_WwNZ{dBJ%b(z6ccFvshJ7**{)}7QW$=uo-DcEP5 z%h*^u?_+VZJ@oc4HrqIB|E9ZGuRbn!k=>hLF<5@f;w=x&^3;I|PBF`C=u%9 zmbk#*kor2-PQgB0B`rJu;GAl@5CDtqm&c)*s6ra!qfoEj~!nxxzxfz z&IcOWlO6!1@m*Qa#SUX#OMrCUV)+kJP2@v=jz%qCV6BN@Umssi}(N~)`m?_WhUFoQ+duor*i zCr1TlEJvC83_U?gIs1ufpO5FnRe?soS%$5gFm5j=lh9&{u<^sqb&y+Kh-PgZ%c(+2 z)rsdLMOuA?hvV|Zq)0@In_c@Z`4eciT9~_3yQ;carJ$&F$qsw|C#R}lSS|%dx}M)} zVW_RA$hB!V*H;3%qY>&qPCJ~|s!HgrW{YXJg8Y8F!2>8?C6qgOGX-lQ zZH?d|?@R>XP#p5xntCNCO}ilp&xhoMpA+HBXn3l6VCX7Tpj$udf(XMo{}ocBpjUEt ze};6vk`W~py*k!T{y5JFAp}~7sQT5KEoc`ogHstQ0BU(`P^X4L#iX%^?Ui3GI>Kqbky&`p{{OcRM>Oxw zTQ~ zT~oBnnJ!u_aC=>+4<5(nV!11Q4IXA)0uCE572Yf zmLc}tNxojBpchR&O%tOE+?EB_bP{_5b?#$C|JMpzizUPRq#~b^^TN#l=@V&x$2q?U z^@M{)UEgjWOwjYLE7HV=ghFSPZ-*tS5lXVf0`}$N<~Nw4u5vArM#zCy%BeF?~}{D$9wsrlOjewX3mH0#-ih<4fU~-WK7ZO zNh>nAnM;W9N{6#jF}vVAZ8E%$gvhPzL_m9GCsyA=+7_OQKve9hIIkBEky{B6<0xUy zbps;AY9O+(CSsKdg6w&!LSCoZKD`DAu_NyPJS#ox7|lK446Se{iT*lfuE^6-kp71c zq}uVg=zvoz0yb0w-eo<+A^kn4dx7ci(>6gZH(zn^T2+mY~UG4(*#szCjt=3 zQz_MkJl|nuBiM@RN#K(6JDI>-)L(W#eYLO`j}8JS(NV&SCGV^N9ZsuXUVXekLH7f* z4!+;&@Oq&wz<)N_07jN&^S!<`)W?!BN@&S{NCH2`^0p`-W|ih_c&V82Gor4tVYbHs z67rjdYFKDAIx+^qMbDcrgs{_K+ssw54C-$P3BmsgYHVr>0e}?5tX{DJU&NS1E7P%> zyrv))J*EP!M4BW_+h0V)b{*$D@?mi>ZE!KG#JFQ)tHt9jg$f0{#CIGPh1wNCod5P4 zFWOHL)DNKfj{=dSktEs~MG}G-H*+0a(vBOS7Io|uV}VMmASl;CImFhR@!EVn9_OWG zLyn^X`Sf{!k8>mXqw$zvD^M1bYyD+6B=HVt58HFR64UZofSpOC=eQs1V%9`nuj6u& zMUHnN#GS`c3!};Iz%y-@qXzT*k0E_nudmwhrRb0OP4$m+AWNMt-a(J=C#Fc=fC0yI z6lq5GUscf&_*XhyMUFY8zztq|%8+LES|IkaqGzw$ESfjPP}~JZ=fM*%p_QkTLPx98hBEq%tNlySca49Wzvu z=b3Pv2uRPJQMIkkxXwYzlcq`TLkuU-BEwk8m&VH7T0m}cNo=IDT#3q6Z8uH73lq)- zt6XS5k3~{xS`W+Rnan$-55CXA*^TBfex?lhg)$k&#gLxJpxcBU6xcx%hda8W$Hp$^ zh#VMtAKT(~0AH9p?l!fqY=hKcR(*vn5ny(0u6GVN9LYI#nt})!e#x_26q;se$hFx! z$RHz>2u_WFP^IcH#T3)zuT~5hU)X=o_`N!NH4J-AE68k2@0r#y{ro9p=ESj`nkp!* zQWW8x*ml*A@_S6IrHFPsdThL3@;Z+aFtgaDhWgiXJa>GM1_bPP3wAD{pg8y-TL4;_ zcVR>Djt9bB7P3lA4u25nqoU2*;gOMNt>vIm7*#a{X7G*~jx;`)P3^@ke9H0QcNiuf zPpC~Lyp#bE3^OT4!*ldm8Ap3yvFiahDT+pY1wY)2*ZK=V8U(f)nC9}Ijtj2b8Pw>L zKYG`LDSYmJB@+}Wy+pp6vW^!9BCtHe`pY%8FbqE}oku^Ih;qe&#Do@-#h28xQz`G+qP`L2TbbxZ8(Ta`4t=VMI(C1;#d9s+J2 zL6sUzBrYy)9|*+pQ`=17$HB+41l=JcoBesi48eibcHraXrx%O+MDdG7XICBe@@GSK zNuwwyfE-3}T}8}_W?yOk5*e4aUh<*A0YBDL%Q$0-dy#m_Lv4MhEND~9 zS!&o(p6Jy0V`x$UH(pDUmL3?1x=g&KK9i|P`l0T=pF>`a_H{I_`3Mntg81^clj3dZ zVY*|ij>tGC-~w6pH({=c3Umv5r)jB!(H+rh;T3Qe32l|0T4RO&YB{S9dV`7i>%q6t zZ4|OeMcSp;-SG|foFtH*grj${j|m71|9f_!{???@&|lkU@HG#T_B_}*U-gsa_JF@~ zrNS2REl_Q0!T;0&`9tdpmu|duK&@z zS9;dPJOt@K_66MZ*KkSs2wO(#e8I&kT#RJs1SClE(2P8zD!yMLyi+J^u+w4o-T$~+ z;N2P%;o2_q4aZpHN%^pIo;Ahhl9X>w5f4}SKR>rboU^YDaBVx~US1Vq*l^MCSrD>e!SuKvZDqH~euFYq*^X z8kF=11>*5{J7|+MyF|y25r5{cy9T+yj66j}BO?3b%*D=9{5#SS_$0Q0(tyHtdHe1o3!u5Ey(+*B&*bE)AGL< zb{0gd>`fvvJI>WsDo<-!*P&^d-38TD)O|}tlS5U_RJnC5$_Q5#4^TnSzMh-QDKxn$ zK|c`i;J;ePdv{9-kaz{NYr(8?WH~VP{DYS^cTs9PVWWV-wSl~;h;LW?nd&zF`eTWc zjjS*I&}HekBj-aFZqEp#(oDl2kE@e%`#;wsBZYE*{1bjVjmcfc>OVN^&PS~OeyzIk zj#;nRH|}wT#o>x_@zUX~wo0dWhBvA%VuU%I!0WZfg)iT$_)F7vNT*QTohEnuA zlG*j8OLG@HwH)GNl@ouyjF#VeVuF6N9RBzHtm}{^pbUb>6_;<{O}D~z_W*L|?+cmN zWt+S~IV7$tv*PRGWC^&e0hEc4-^hm71>{i5>VllFN;6}C@&SZOGn~}}S-Or{mym$o ze;ySzL^M$wxfVl2lRYphqHQm~ehP89JS8BaNuqN4KPb-@qHt^@ynG<9i-SaYap4PT zkX)||Vt{phaIBeuh$}2WTzzaHFI!3bTM8Fmr&Q??!^lJoH?z}hjXC^NfcvJxqPP1e zm(jAdvN-?S^X&(gR~Qs#gVto{X@2vcaeOw!r7F0XLvy(Kl}5fX%~s&lEz4J`69CTk=aBW&sw)A;;dA}Q#h_Oo!Xz5Vweqtly@-O^jMJfNE z+$UyMmwc|b4!Jm`VU>KUuPb7ap}r(ehH8Z=tOD*|lF^F;rJ9;v0}qk##s3$K3qtJ0 z09^DI#dmaMeUCSVz_Nf87e{%(8%%8nV9tMsq0n|+P&aLu;%P7j23qPPM)K9aP{C&M z0rMyy94JJqW&yIvS`hkHk*Tlp86=OJ7HZLa17!3WS&($>v!AjaG1WKR+qr3#>*5t_ zElb#4p|jcniBF0p6f4}OTD@>4!S(Ko{b68Nm4i$_fTkzumCg=eK^h-*=e&pvH8CzG zL|+U8hf(xfn;1ZDNl{z*i(#)&*|_AQ&98oqHX?ZEzf(YXg@Hm{I_A$cTtc(Z8Ym-S z0#kb77E!qnVX|L8L16>5w)~RC@Nm!1Gh;6+{tigB+G%`ZPs=>Z6?NMJsJjC#(1_gi z8!Eb(KA{3$~j2EL%F0D4$r=~k~7h3RlpD{PXgjR zeQt^&Wbe-}hEUdjE19yJuHKh3Bg4!oFY-ts=Vn;X>kSjlUCrY^<4Cv-b>mi+$wc#) zDriX}GY}+9{Y1dAlEz~DPaEWf= zhe9)PVMTF81=2og`FFDIpm18skP!eRJ?7T}$?Db}WaEIbX%baOMAHqAL)%1K*@u=5 zPkF6T%ta^%L*RNJ|0m^5%&`tnG*1kqK_@UC-NG;^v=&U8tksX36$t(+;8%ZRJ?TDC zz=zn6Z!-Ghy;}~%zXuPvVH#PZ)P)4gL-%!faQJ|VEgJ^Y~S*I|NcyeAu?ko}d6ohqPba7GEC7E!LJ zqj<43D4bznI2?e~6{~p`By2Gw^skXqN8^BBV>v&H#VQ$e6QW$&xm*jr{cp*lPKsOs zlMV+Ty6$&h-k14I;$199$m}tb|&D zYOn>1&ez+V4s1erd9NR5CcOb82(`RCTOUaIcA#;j(r)Qkl}5r+5G82`1EEVpID_F= z$e@R#7m$9SbPI_FKD^7dkk)AYnyu|!J5EH>tO+gzst)ZJ>0fT0pB;b_fH2!k`TOS} z@Fby^UTriHoDx`KX8>jy51HG1P5RjaWF~1{k0H}pTDu3pPu}3|uwe`+U~rYPS8Eci za|pDEMw{~*{GcpkyzlX1b>uQj!PCnKd!|++_yK&=f3d9LD3JBw2-)bX${Ck(UiH~wANiK9?)hX*|z4j?Ry*T+(Dsp zj`iqExjxFVZrrs?<7Vs=7-#BlNxM$H(h_8L+B&$Q*{F_`1wkws?Er99ttv3o^x1h> z>NdMZc|QTkI{Lhq$UM_f(dc91hEZY*C(|2X&={}DHQOjyNTRp2xN3OvZ%SP}h*xg4 zG>$@Cs_b@QJ5%+>aQBD(kfK+ZhQ z4`;q=ZLMEQ>QP%BunmOmswS4c+7Bz~b&3pc4`Ldg>E_f+AT(iv+Nz3Xj6VxYID#D& z4I78v+F$(td^=R1hhG>b`qak$&z+$VN%-aI-z69QoJVVQGx<$B$0y{32ykJ{&>W7O z4TtMkg)7qDe+-a4r!$_9?IhRND zE0RJNpfh;xW2^p6(q45z)XL?8gO|B3j2c9(D0Ug23OOTr*8sxj%!_+{Lp!B+9x~#g zcDd{cpi7&OBEgd$TFduETDE&RpzLTusB?vfWS1h`+X{FeX$TJQWJLYV6jIspaJ+VO zu^Qt9w}_<@voFSBD5MR>WOVoGjTju?B3R@`Z9o+pqpWe9j+5a;h%zXL?_^XXcxOhX z&#JhRN`hG6V6Z#@pV~x5eUJX=eIJ`F;(E|g`6ygt)lHDOgsx%o8aBru zzUKl_hV1{6rKlgbIy!X#KK$WVD;=+K^t2z!GCGO7xImK`_nO9&VU?aZq5R)Kgz~eJ zRzACvKKR$s?8=kDKbiNBkq5C{*b!qcDY!TslufaYW>n=r;Njv9pQz&I=kn!2^D}~E zoGi|sa({S<;d#u4H;IX<1?G#axwSRd9a-!>&hBomidjq;qnG^e+EtznUtw6r@&j~6pk8~}v`HoA@wjsk>EwXTGlBacJRrVu41yXB~Hw-PA zIq(}{l!5`av>R-c+ePfzCXr^kFTGvIR+=*;WHw2t!>oOu%vBvgf{W^-;*Yl5pTC=Z z=QM{o4fsTBgoR1k9_><6+~k?>C22Wm#mFo-Iti7xEX9jEJKc#yN8u$)Fu!N%u^l_z z>v2wlIQP&Qlw)Ho(uz%4NMJEg-Zr2t)p6e9yp6I_WLr(*@h^f4d;Uo zqy&8S=5+RrTX0Hz z;_9WHKUqJgeH4!_3^q6=Sw!f>n_VJ5{yTB;TcfuBFZ_^5xMbC5lfd;KDEMEqgY!)~ zso9g~ljv}jZ3@~6D#_Yo6zV}z5)m9~e0{l4g2jo)HB{Cs&w2iVsE9*f*i7+~h>}YK zF-b)B;xg+-J1TfBMac-rEj6UgoUkY>{YCue=6~ZlqW`|f{HaCXc;q(t%m~$7jMZbj zA!oYVAa|IWL-;o6iFkxp%I+_A#RP4vso0g|u9<*fMVC~V5{8ehmeRT5zK?H=2XXP6*|gco+pTduwkw=_-X)|hms&W77SMx z7J{U%#&dJdC=!Kt$l;Hl8wFbthC>9eSGK`5BUMc;OxKzxx+eLoEUHcw5L@_4H827f zf=oIJpj|7&1*(rg2h|!GHSv9uFzPL?D7wr`K)*EBiCEq_5E&r(qgrfl1xa};2>be5 z1=~;>thhXxQAnOZXR*_n`W=~n^_XknYi4H}N2@h^TAE=5zBZcoJ0M;Pw}bavV#akV5eao}=bEL8T{9ID>S1MYn7JymWuV2bhCm z8tDWpr#g5Oog(#zHHEYswZ!&^03X$gX@^C=?CnU~$SM8+9O*0if9>u*jEeKPJq z&?kaGKG$B!do#0JN!h4D|B`P9v;qSa`jVH#vhNW6!GgsNloO_SFRJZ-xb(4I^Op0C zNhPI;GWRWw#1c|!qDetZoe|fu=S7Oq@n7YVLu-<2>aIOVy}E6r7&XoA;3)`iwzjA- z$JrJ6zy&j?w4wSjo%00-eaTH7p1+^1y8gPGF&>wtfxGa!vpt*Hnv$o+YcyhP4*{3K z>+3~3;iNe%$WE^{O}8m+(=5q;zv1T8h}XoS`A$gE#xmX~+jZ--@O(kMcbl^cfr1l* zs1>q0F+Bj>gQuQhU%f%4o_*RvgTxy*klg^m!XrUQ95KcMTvZc%-gQDluIuE$MH%pP zvciBsgAEv|0{CBy0A9!d>F6imDu44M{}yrtx(>MV$M1B$OgsM*m;j0K!fSSnTbt?P zTEXr;BsLME0*)(0rC|=K=>aYS!2LE4b_qjb6KUY0UMW3zq5x438<1UujpWv)|EQX9 z$m_zjvdSRw%^JjsSi}DJ2>CwA;~|q+r}61MhYH*>`9q=f)2RrR2>?>dxtXd%eA)ef zY5B>$01W#6PLIrX9X;)&NKGS!r++ZNnfwiIHYw{=oDi)o;$=qRP*n0Rg8b8BhOCw= zttF9f#Hh9XptPIara1+W1 zsj}@LJzvFvmPyEr#x9MDS^L=+&4w@mUU2^5FlV#hC_(1gOWdgk*lUs{nX4PB2h*UE zv{IThl*7uaBW5c9`&zb%ZWkuogd*CJ?3I+z~Lr zNT*dT*}CTYh&tkZhRhg#y|}c^q*~?3BId`$X*}_oKkKGZ{v$xBhf*>9jW9{FKE?a~ zl%0FG>9TS_A}x1oswuyg_Y}q4=zsULK%RS9+#oYi=9k#7HY2lUkCBv1iTCoC*kT3+ z*t;@)I`>efrVL zHSv~`1rx-tVu`FG{ysoC9JQCe82TV+XDVp@f7VyRN4Fs!NL68|$~0aUV)6g(U(pyQ z;T75L;}yQ`Ysznjq|jDgkvT~I&iW^FiZ?*Z+!5W#+66SD8WM3gJf(33WRc$&#A4fhDQ?D>{ z$&ZibsMqWjP6G2+l;UDGFO+k;`8(v{T=)c*z1s{|;k7aib1I)B|4>FcTduLWi`vZa z`G1ke372TaP)>!Q@$ZI`hv;>i<4=JG_o%_HQRx*t!8c{DVn4~`POY>kki$C@`c=fd z;2$VDl8@Nir?_A(TpTfo!QpXMH^O+3<2X#D7(40sBY*jXQOY0K%|!frQ;U3uflp=m ze^AX0iQdkt=2a_v`K+OWQjz|`Q+6xH@W$_3vcm$A?eMnzS0ILq))+1!c40d6Y@O*Y znaI;BYQtk%fx|v&-aQ4&m)qJsZH?Ejr;3Cc9(`27J3CQ_Ty}IbD;(Q3nZ~jF!M4jl z9%CZhEO!hzEScJrfMu4fZI*Z$%hGYip&~)uFuDA5n|_Ejj8+XL=i-FH@AI2+j>oX8 z$EI;w-lwqi4C=?%+H}Z#?HYN(AV96Ur-0{{KLZoR$Mr|~QRzwQ3?i%+CXwEK@3&|J zX3)NcmYQ@}>)04K0qoO)9OH;LR8M}ZL#8-iIKb9>>wNQ2S4#yN$=G1+Cky z$5}&7>8V*q&T*xv=xW@0uKS z^BB)EYkQ=hr{TD+gxPEMLzN1_gLQdJA=bhls4bZ7n*QxcqDYiv>t{Il{A|u zCGz1<@s)6*CaY3jQ~}2kcqxbvi2e{ZHxBq5pTNw9&KRQFjaq{_SsP1O)ztmf9PucZ z+<(g|D~6|}WhUF#_TP$?`ULP;fLoyEc}+l4#&H79*Pf4q$CKT>z`5*ET#Xfx5+Nj| zgOC`n6;+bSM^_!VC0};yAuVG zx>0w_K$h72;v9D5*nZMgdG0Q1gOAah3hb;?k@;0Ncq*PYBG>Vfl2Q%zHw~`)L5Lbu z$wc?{ABx`m^$TS9Tj@SvI888PFf5M3j)=r62?u>bHG3vhrmmPg8+!4hp6a+@dLhu! zMt*y#X)McOn&c|r+PwAW!VvyM*L_v`)wn0w_lb<-%^q%;6o->+-sC zj)l~^xVF_9s=Lf&%POCn7Ipd?$4PvX{q3=bg0KfD5DcMjvHWvPP$`7JE$Vrl!|9Ud zpeKJSa={!!L?!$&kFJF8iXTkTt?X?Aan!ZVL>qO1>D=KSd|}xT?r#P$jG{HRmMYEQ zB5)eAM*fcO>I4q_aLN##MyiRd($371qREgn7Bi?h4@~~BTDJ8oREi=)957dJL$XEt}%>qApKi8Bv%LCJ+(gof6XC7oI1D(ypx^fQZgQwh*(;*SqWcr?SxhKoHE`z7wePv+;?c?M6g>S}bgFJ;VS__G` zTo^i*@F(Z_#WNx|d z1&b!|;{^1O*K8R=7Vtu&+qZ7FT4USMf3^FkJ#8+j3LbzjBd4BmECn7~&+aarc)TT- z$#(8>xRVS*;EnyR!0AW^|AGja$ z5HGb1#KoTd!E*-0EaC>!GQs|dzv5_+sDWaGc(we=U06E0}~pF57L?2hhjb50)hCyA~ub^o?#Th3q+*p4u?P zyNpLP7(h47cNQv8OpM#fUhI1ORD2EDI&8;SDRd|8Fr=AQ1Kn_E@Tcc$oUm}?n^l}L z>6?v($m{%U_bdM2PH>oEp3km+=+5t%&>c&2*F;-;rE%iR<3seJkC(@zenU-pK2?h1 z*JIatFY}zA*2E`5a($SEZ~x7{HcW|M_c@Y@y4OEK%1X=vo*8W4`s~Npsvkt!G=oV|f+|hC_IosKoNj76zY4 zj61-y;O_|`$y_MN#uzPVMg1D25FlUql~&V7r^)t8|_4kqDy1zED>V&oAS0{sle zQUI{ddpXnz+u0;%QG7evjeT0$iEdL!5Jw8fQ^c|Pw-F$9uW3hKW$M4*LEaTNHW(=WPujgnD;qIH%~GCEuAJyOox@L}R(I%(`F%4Utws*@49Dtg8X zZo(I>yMf|Yw3eM(c(E*Phk=KY=a!b*58P*7$`!wFa+i-Wkhz$80^8x>73-cyB|mu~ zqK(-8i+Kgs=StyDK{Z?GJV(qdF+p}w(pX+6TZy`bwn_Jsj8XinE$a-p_7r6H=QbV} zsU~>7H%e;aLymDFSNgi63HZK_d*@`y`2PeRCXKutAxIKUVgM zWLrP4#eJl0oN9B=CC4?rRw8gq)0=BWu;W^97)u0b7^$>)y$IaV!a)vW1u9GMp~v6A9#Ah|D{O}9XHTM+6Q zRs@vt7N!2Y(t6ZZTQo>To^!kbZ8Ph|lE8U2Uk(YL zK${x_it*P5o$fb;Qs`0ykT9lylaL%DiDQKt*BYx-Ch`eLQC6rMJ+ZwVwL1;Gh z5H+IgZ?qJqXpe^AhUt|4&{J8%UR7E}w1Ufq+I4tIPS)qM64g$r-E$@_NyDmzqT|0L zg%1&kg~w%xMSK>uqfZoO#VKsITcOGe%BX5&GoxL^{TNcuUPj9mRJDBI#0j}WqR~%K zB~Y4(Lyqt=A=U8 z5||++FlfS{2q@G25&E(op4_ z)eg<}HSki{+g7jG9mqDTM{A^T*y$S&D3jhQmoP@+=%EeiT)q=)iEEznzHh>OeJFeA zt?h2*Q9TVkJlDCj@#T3oI-vL3yGHL>2D-Hy2lO3>0hlfmX@7sG(T4A*BxO5ToKe{! zEg0HI^HJ}%x-uW?*`?Jf4lwqO_uqskCQUmxn`oKEyZri^g-nH=a7JDc(F|g%CHn?> z&Ow_UH=aAq%A-n~z+DhkBK`Rm%qfySi zV-Aamu5eRx7f4ZKTk+cRG&e|N8cr9MA>;=yCs6s#L+~|7c@FVVm zc@AQ8unI6B;^hRYrybOuK@5VnUUq%jK+M?f=@gRuqc=R@?KHrOv>n9?yJW*)Yh6B$ zA!$y5=kTiU6I3T=nO^^VwTr&5`M#^OTk68GzvAAQna>#`hC*Zmy(8_8zG_lg51cVv z$_i4{K4!;|3rP8ruAyz30qO~45yu!1Paq5T4DgEYyG}_O3gn3pePo}6cne> z^Kx!)`|&$~>8(BdlxJb5C2mPSZcojyfd#NweU%}F_x0)$0d-R*dgl9w#soTg&1ZYk z@k~Fzw3oL-2Ae;$KcaBdx!+zVAH+<%Ug&czAclsR2s1sar|6lYNwqC6NnLDsEM-0< zOZ(^yg)La6n*`*!8wr?a`OuQtiCm2inrnC;*PTw*Jz%yHfOmYDR}gY-Pwur1^kSdd#A zfyMo_YA%%~Q+RmJ8T2oNKri(URpho!o>OckTqw!>`eZf3~a$0Q<$ z1dqV_*dK-<>}IkLJhqM0F?gT0=XH;oHz#d`^S8>8Z9fHUC}lT7_$Jr&$5Qe<)*kG% zi!fdBsqIbGFy&ok=lP;qqd5_H>k7K6LfJ|5fBUNO$~e?uUWmbF zp#bP8;)I+O2!s6ipNSP8aAgLOL@^LX*!bpl4srwv$SA!q2j&JUL^`npI?7tXS8o*} zs{X@nq!IcvwgJD4iw-z%%-D=PM11)`u$#wGQFl&=4>kiFZA`tdD zO?*g!;+xF@uD-)_kj>t29nwGA0Lh`pg<5+p1VEdnL1;V{t>`h1cIREd)OGtl@WcUo zZN0~DzO$EBWu2@G$*0HO9cT6OvON&`p9mQ7gsyQ>ZwWQQA7A{0JY}{EdDy~&nqUeC z)E!4QK%wZcJX;yU5Wc%5{*f>y36(PY*12TE{i0F{qtZELRj-j$VrMFqD1!hSf{E1l ze(okalbBNaQi7g4xTeWw()YEUW^BsGypRmt6~)VJ3hbzKsuPR z1z9iUZwxr}{W%nK7PZaERIX5>W(9!V3Vt9I=&&L3;6HgE6Jozhtgm{1WzmQ_x_b9i zUQvyGpl&}0vD#naRK9vO*qw++NX`F)^n0nw)$N-;4U8O;81j=Hb|jyM3&T>1(Of-z z!3>>t0-h2SM)rOEmne!m`dowfAfhfMf-iC_$RC5UNy0{WdT@k3t91u>Qqf>E;uu2r zpeiDJn3qwqv!b{2k38r|e?8CRqTC;s!?0hhR%O?(-*t-muEIdUf6JP>HjVk@IiJ_NlM=NE4+p?Si*ZAW8`+-@y8x+^#vuQN7zyvQV~K;Vu6^* z9MILD4FN8)xedkP9UKR?GpdX8Bg$tQaN^{7IGOCaw|Zf65ARzvDSv(8n}4+gOd3)r z+yT5lXp2Zx`&ipJr)dK+ou@M}>bIo(J!XN&<<(Adag>gclXxIkViw^dAOQw9;1nn+ zh?EwYEh#*?^d27VNFX#Q z*g88%PLVuO$A)0`UUVWs*k-vl_n=w#4S04DI@Hb2>>`7mJ!zAsMePlOAB|=5j(FCj z0p1PLro(WW%#vzSjVLa8mBMo{vy}`JC_&fIg@H(@Fc>$8>*7@ur@Q1{XIOk&B=iq& zE#Ln##>0t`vU1H1h+e_ssVKm~VO}G7BXLrQ-XVEIPf4&5DBwj)pvIsAf0)yI zR_#0V66ScM{l|G-$gRU5EA1rhqB;(EXdQl}mt2f`Q~;N}ZaZ%@#%_V}7n7Z^ZFn|; zMlw>;U}47@($gzOg#kL2tU@lbq)0HnuPCRWVdmP}8Yo+yr2Xmd-{kB)45OylMn@G- zbb|u(*wLSnm+PGNi{8je`{e>F*3l=PJ*};v$aMEVlTxM6&Fn!2Zic>R2JaA!4~B$x z$!Fw~jFb_u5MiN+*cMI(KEfD)eEJNCK9{#codnr)p>KMWDtiYx8s+^8JCxiR|Mu_i zU*8TdikFm_w8U6@Wf?0B)nX=iQ{%;pRQ0d!!>8Bz1Z8;wp$Y6k06NNA{y6_DJU)Tl z%hNOR5}@IXjemSA6Ah8>H$&X&%go6ckqY1))^cb414XpHw6tUd+tJpB1UuRJ#grTa zJ*)4d>~`6^X{y7p4V9*he^;tSnuM&P84RJHGulqP+noP;Rdb`u#UuCf$+q<~T zC#R$ocq)~@ect_67d_6ur@F3Fn~;$3wL7^2jo}hjWUGgyX6t2gQb8~-E)Ee;+}c`N zwRLrMpA-HXc0N6M-73;pWMpM=Z#oo^`6MY6=^U**KX;Z#9zEM?G zg=b7 zQc%=r^Vao1FHT5UIQ^2VxvPteK37vy^E%cG+i`(nQxYx6&E1`F%-||pP0jhd=?}Xp zlmyCnV|TY*N=izTu}gcv06NBtvDVDK7Fo>K)Xvt{R*Q?Vp}@_Rl?X5J#fIEfcD?6C zK!`U#gL;}!cNg%5&$HpaI?$d7?u52Sa3ZL+VS;twSxI#G^@2z>T@x-24s3z1L&mAI ztkEJFxrbBzGKpx-UKV*B*&m}FH8nJlyFyu4QS<9y{j~fDiIiaB&nKb%DQu4X36#+J z9luUl)_5(;KF`gmR-HXiHf@B3h1ohdu%YYnE!e=lE)1!)e)$e1+H-k%iDmsHF^~jL z-=b!SmKV*$qo}g4{ho9a)2JOjj%OHJ8Z1u165hMp9k}`DPmFoF=Ivx$x5Fipw0bG7 zpz%+4*08j79OPr2pL4-l47vI-N?sq0Z+z46^}n#wwUyby+=+MiV`i9(OHfKuyN9H$ zCy0i&cq977#>I(A$FFg``6NU+Bq)c?M*lnRQKRgV*w}l7qbW*5*-o7O)Cbv>+xEnw1*Y1wgZ-qG-+fn?RhRxso5n3)nb-V^M;9psJdL}HJ(;*AcX zc@RC8!gw&?E9lK- zTagCc{$+<_97~}^*?@C-m<>FKStiiIU5{TqCV705Hn1yeQVt6XEk6^{xjg!kroXv8 z5T~BA|2Fenx+chA(Yg$?J~1{n=02}(+s+tQY_1y?8tSrkBd15VfLPbP^YR5T*uNX4 zBwcu@3G$shxNM?I1a&0f z>H(7mn+EGM5MVR;C^erpI~lNXAj$&(%bNl<(eKa@>)B69RLsRW5j`_t{h)q=?5k&}0fYhg OB`f(^qFUS_@c#oKYE)_f diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index a1ef506b08fb6..ace9b481e4c80 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -312,7 +312,8 @@ def concat(self, other: Styler) -> Styler: Examples -------- - A common use case is adding totals rows for descriptive tables, with ``func``. + 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"], @@ -321,23 +322,19 @@ def concat(self, other: Styler) -> Styler: .. figure:: ../../_static/style/footer_simple.png - User defined functions and an ``alias`` can also be used, which is useful for - displaying metrics such as dtypes, missing value counts, or unique value - counts etc. - - >>> def reject_h0(s): - ... count = (s > 0.8).sum() - ... return "Reject" if count > 3 else "Accept" - >>> df = DataFrame({ - ... "Machine 1": np.random.rand(10), - ... "Machine 2": np.random.rand(10), - ... "Machine 3": np.random.rand(10), - ... }) - >>> df.style.highlight_between(left=0.8, props="color: red;") - ... .set_footer(func=[reject_h0], - ... alias=["Hypothesis Test:"]) # doctest: +SKIP + 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") + >>> styler.concat(other) # doctest: +SKIP - .. figure:: ../../_static/style/footer_hypothesis.png + .. figure:: ../../_static/style/footer_extended.png """ if not self.data.columns.equals(other.data.columns): raise ValueError("`other.data` must have same columns as `Styler.data`") From 10323362ee25f512a40fafa4f6fe8715d4b0e4ce Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 22:47:44 +0100 Subject: [PATCH 29/44] doc edits --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ace9b481e4c80..4ac8002614528 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -302,7 +302,7 @@ def concat(self, other: Styler) -> Styler: - ``table_styles``, ``table_attributes``, ``caption`` and ``uuid`` are all inherited from the original Styler and not ``other``. - the concatenated object will not currently export via ``to_excel``, although - support for `to_html``, ``to_latex`` and ``to_string`` is available. + support for ``to_html``, ``to_latex`` and ``to_string`` is available. - hidden columns and hidden index levels will be inherited from the original Styler From 394b9e0c94f07f2ad86dc25b200a56dd301a8ab7 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 23:32:48 +0100 Subject: [PATCH 30/44] edit tests --- .../tests/io/formats/style/test_exceptions.py | 35 ++----------------- pandas/tests/io/formats/style/test_format.py | 19 ---------- pandas/tests/io/formats/style/test_html.py | 23 ------------ 3 files changed, 3 insertions(+), 74 deletions(-) diff --git a/pandas/tests/io/formats/style/test_exceptions.py b/pandas/tests/io/formats/style/test_exceptions.py index 897d779cc3647..09e70e09ad6bd 100644 --- a/pandas/tests/io/formats/style/test_exceptions.py +++ b/pandas/tests/io/formats/style/test_exceptions.py @@ -3,7 +3,6 @@ jinja2 = pytest.importorskip("jinja2") from pandas import DataFrame -import pandas._testing as tm from pandas.io.formats.style import Styler @@ -22,35 +21,7 @@ def styler(df): return Styler(df, uuid_len=0) -@pytest.mark.parametrize( - "kwarg, expected", - [ - ({"alias": [1, 2, 3]}, "``alias``"), - ], -) -def test_footer_bad_length(styler, kwarg, expected): - msg = f"{expected} must have same length as ``func``" +def test_concat_bad_columns(styler): + msg = "`other.data` must have same columns as `Styler.data" with pytest.raises(ValueError, match=msg): - styler.set_footer(func=["mean"], **kwarg) - - -def test_set_footer_warn(): - df = DataFrame([["a"]]) - styler = df.style.set_footer(["mean"], errors="warn") - msg = ( - "`Styler.set_footer` raised Exception when calculating method `mean` on " - "column `0`" - ) - with tm.assert_produces_warning(Warning, match=msg): - styler._translate(True, True) - - -def test_set_footer_raise(): - df = DataFrame([["a"]]) - styler = df.style.set_footer(["mean"], errors="raise") - msg = ( - "`Styler.set_footer` raised Exception when calculating method `mean` on " - "column `0`" - ) - with pytest.raises(Exception, match=msg): - styler._translate(True, True) + styler.concat(DataFrame([[1, 2]]).style) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index e2818581dc4e5..5207be992d606 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -434,22 +434,3 @@ def test_1level_multiindex(): assert ctx["body"][0][0]["is_visible"] is True assert ctx["body"][1][0]["display_value"] == "2" assert ctx["body"][1][0]["is_visible"] is True - - -def test_format_footer(styler): - # test option context values - with option_context( - "styler.format.precision", - 5, - "styler.format.decimal", - "*", - "styler.format.thousands", - "_", - ): - styler.set_footer([lambda s: s.sum() + 1000]) - ctx = styler._translate(True, True) - - exp_col_1 = {"value": 1001, "display_value": "1_001"} - assert exp_col_1.items() <= ctx["foot"][0][1].items() - exp_col_2 = {"value": 998.163, "display_value": "998*16300"} - assert exp_col_2.items() <= ctx["foot"][0][2].items() diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index f0614d2df01cd..49c9cc794f026 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -234,7 +234,6 @@ def test_block_names(tpl_style, tpl_table): "after_head_rows", "before_rows", "tr", - "tfoot", "after_rows", } result1 = set(tpl_style.blocks) @@ -615,7 +614,6 @@ def test_hiding_index_columns_multiindex_alignment(): styler.hide(level=1, axis=0).hide(level=0, axis=1) styler.hide([("j0", "i1", "j2")], axis=0) styler.hide([("c0", "d1", "d2")], axis=1) - styler.set_footer(["mean"]) result = styler.to_html() expected = dedent( """\ @@ -666,15 +664,6 @@ def test_hiding_index_columns_multiindex_alignment(): 10 - - -   - mean - 6.000000 - 7.000000 - 8.000000 - - """ ) @@ -689,7 +678,6 @@ def test_hiding_index_columns_multiindex_trimming(): df.index.names, df.columns.names = ["a", "b"], ["c", "d"] styler = Styler(df, cell_ids=False, uuid_len=0) styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0) - styler.set_footer(["mean"]) with option_context( "styler.render.max_rows", 4, @@ -778,17 +766,6 @@ def test_hiding_index_columns_multiindex_trimming(): ... - - -   - mean - 31 - 32 - 33 - 34 - ... - - """ ) From cf42933c2b246e49d8988a4ee8fccc290a9f7547 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 22 Feb 2022 23:56:49 +0100 Subject: [PATCH 31/44] edit tests --- pandas/io/formats/style.py | 2 ++ .../tests/io/formats/style/test_exceptions.py | 6 ++++ pandas/tests/io/formats/style/test_html.py | 9 +---- pandas/tests/io/formats/style/test_style.py | 35 ++----------------- .../tests/io/formats/style/test_to_latex.py | 20 +++++++---- .../tests/io/formats/style/test_to_string.py | 17 +++++---- 6 files changed, 35 insertions(+), 54 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4ac8002614528..3c16dfff8950c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -336,6 +336,8 @@ def concat(self, other: Styler) -> Styler: .. 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`") other.set_table_styles( diff --git a/pandas/tests/io/formats/style/test_exceptions.py b/pandas/tests/io/formats/style/test_exceptions.py index 09e70e09ad6bd..b9f6662ed92cc 100644 --- a/pandas/tests/io/formats/style/test_exceptions.py +++ b/pandas/tests/io/formats/style/test_exceptions.py @@ -25,3 +25,9 @@ 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 49c9cc794f026..eda7c43379d1c 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -678,14 +678,7 @@ def test_hiding_index_columns_multiindex_trimming(): df.index.names, df.columns.names = ["a", "b"], ["c", "d"] styler = Styler(df, cell_ids=False, uuid_len=0) styler.hide([(0, 0), (0, 1), (1, 0)], axis=1).hide([(0, 0), (0, 1), (1, 0)], axis=0) - with option_context( - "styler.render.max_rows", - 4, - "styler.render.max_columns", - 4, - "styler.format.precision", - 0, - ): + with option_context("styler.render.max_rows", 4, "styler.render.max_columns", 4): result = styler.to_html() expected = dedent( diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index cf646bf544ae6..9607c82041217 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -43,7 +43,7 @@ def mi_styler(mi_df): @pytest.fixture -def mi_styler_comp(mi_styler): +def mi_styler_comp(mi_styler, mi_df): # comprehensively add features to mi_styler mi_styler = mi_styler._copy(deepcopy=True) mi_styler.css = {**mi_styler.css, **{"row": "ROW", "col": "COL"}} @@ -56,7 +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.set_footer(["mean"]) + 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) @@ -347,7 +347,6 @@ def test_export(mi_styler_comp, mi_styler): "table_attributes", "table_styles", "css", - "descriptors", ] for attr in exp_attrs: check = getattr(mi_styler, attr) == getattr(mi_styler_comp, attr) @@ -1560,33 +1559,3 @@ def test_no_empty_apply(mi_styler): # 45313 mi_styler.apply(lambda s: ["a:v;"] * 2, subset=[False, False]) mi_styler._compute() - - -@pytest.mark.parametrize("alias", [None, ["mean", "average", "lambda", "udf"]]) -def test_set_footer_methods_names(mi_styler, alias): - def udf_func(s): - return s.mean() - - mi_styler.set_footer( - [ - "mean", - Series.mean, - lambda s: s.sum() / len(s), - udf_func, - ], - alias=alias, - ) - ctx = mi_styler._translate(True, True) - assert len(ctx["foot"]) == 4 # 4 descriptors - - exp_label = alias if alias is not None else ["mean", "mean", "", "udf_func"] - for r, row in enumerate(ctx["foot"]): - for c, col in enumerate(row[2:]): # iterate after row headers - result = {k: col[k] for k in ["type", "is_visible", "value"]} - assert ( - result.items() <= ctx["foot"][0][c + 2].items() - ) # test rows 3,4,5 are equivalent to row 2 in value, type and visibility - assert col["class"] == f"descriptor_value descriptor{r} col{c}" # test css - - assert row[1]["value"] == exp_label[r] # test label is printed - assert f"descriptor_name descriptor{r}" in row[1]["class"] # test css diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index 2b8352d51c3cb..e8757c6681096 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -992,10 +992,16 @@ def test_clines_multiindex(clines, expected, env): assert expected in result -@pytest.mark.parametrize("hrules", [False, True]) -def test_set_footer(styler, hrules): - styler.set_footer(["sum"]) - result = styler.to_latex(hrules=hrules) - midrule = "\\midrule\n" if hrules else "" - expected = f"{midrule}sum & 1 & -1.830000 & abcd \\\\" - assert expected in result +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 e1c260d3bc68a..fcac304b8c3bb 100644 --- a/pandas/tests/io/formats/style/test_to_string.py +++ b/pandas/tests/io/formats/style/test_to_string.py @@ -42,9 +42,14 @@ def test_string_delimiter(styler): assert result == expected -@pytest.mark.parametrize("delimiter", [" ", ";"]) -def test_set_footer(styler, delimiter): - styler.set_footer(["sum"]) - expected = f"sum{delimiter}1{delimiter}-1.830000{delimiter}abcd" - result = styler.to_string(delimiter=delimiter) - assert expected in result +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 From dfcdf6b3aff69b65af0ecd185453f28854e5dfa7 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 10:08:11 +0100 Subject: [PATCH 32/44] general render method --- pandas/io/formats/style_render.py | 45 ++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 8761023e09d96..bad34b34c5ebe 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -150,6 +150,24 @@ 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 = "", + ): + self._compute() + + dx = None + if self.concatenated is not None: + dx = self.concatenated._translate( + 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, @@ -162,9 +180,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, @@ -178,16 +194,12 @@ def _render_latex( """ Render a Styler in latex format """ - self._compute() - - d = self._translate(sparse_index, sparse_columns, blank="") + d, dx = 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) @@ -202,10 +214,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) @@ -237,6 +246,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. @@ -252,10 +262,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 ------- @@ -316,10 +328,7 @@ def _translate( ] d.update({k: map}) - if self.concatenated is not None: - dx = self.concatenated._translate( - sparse_index, sparse_cols, max_rows, max_cols, blank - ) + if dx is not None: # self.concatenated is not None d["body"].extend(dx["body"]) d["cellstyle"].extend(dx["cellstyle"]) d["cellstyle_index"].extend(dx["cellstyle"]) From 64cf7bcb835bb43be83e5fd51e15d9a0560a5d4d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 20:32:08 +0100 Subject: [PATCH 33/44] doc warnings --- pandas/io/formats/style.py | 11 ++++++++--- pandas/io/formats/style_render.py | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3c16dfff8950c..2bfa9bc792d8e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -297,12 +297,17 @@ def concat(self, other: Styler) -> Styler: and ``applymap_index``, and formatting applied with ``format`` and ``format_index`` will be preserved. - The following are the current limitations: + .. 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``. - - the concatenated object will not currently export via ``to_excel``, although - support for ``to_html``, ``to_latex`` and ``to_string`` is available. - hidden columns and hidden index levels will be inherited from the original Styler diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index bad34b34c5ebe..bfc53905ed811 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -158,13 +158,17 @@ def _render( 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: - dx = self.concatenated._translate( + dx = ( + self.concatenated._translate( sparse_index, sparse_columns, max_rows, max_cols, blank ) + if self.concatenated is not None + else None + ) d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank, dx) return d, dx @@ -194,7 +198,7 @@ def _render_latex( """ Render a Styler in latex format """ - d, dx = self._render(sparse_index, sparse_columns, None, None) + 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 From ad2374811217baa6056572e690a766c2440f2f19 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 20:35:38 +0100 Subject: [PATCH 34/44] doc warnings --- pandas/tests/io/formats/style/test_html.py | 1 + pandas/tests/io/formats/style/test_style.py | 2 +- pandas/tests/io/formats/style/test_to_latex.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index eda7c43379d1c..2010d06c9d22d 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -762,6 +762,7 @@ def test_hiding_index_columns_multiindex_trimming(): """ ) + assert result == expected diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 9607c82041217..e8187f7e8871c 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -43,7 +43,7 @@ def mi_styler(mi_df): @pytest.fixture -def mi_styler_comp(mi_styler, mi_df): +def mi_styler_comp(mi_styler): # comprehensively add features to mi_styler mi_styler = mi_styler._copy(deepcopy=True) mi_styler.css = {**mi_styler.css, **{"row": "ROW", "col": "COL"}} diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py index e8757c6681096..6cc86be07eea3 100644 --- a/pandas/tests/io/formats/style/test_to_latex.py +++ b/pandas/tests/io/formats/style/test_to_latex.py @@ -992,6 +992,7 @@ def test_clines_multiindex(clines, expected, env): 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( From f92c54c081f9614b2b701abe84d3031dee39064f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 20:54:27 +0100 Subject: [PATCH 35/44] revert merge --- pandas/io/formats/style_render.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index bfc53905ed811..c0c87dd48c196 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -25,11 +25,6 @@ from pandas._typing import Level from pandas.compat._optional import import_optional_dependency -from pandas.core.dtypes.common import ( - is_complex, - is_float, - is_integer, -) from pandas.core.dtypes.generic import ABCSeries from pandas import ( @@ -1474,9 +1469,9 @@ def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any: value : Any Matches input type, or string if input is float or complex or int with sep. """ - if is_float(x) or is_complex(x): + if isinstance(x, (float, complex)): return f"{x:,.{precision}f}" if thousands else f"{x:.{precision}f}" - elif is_integer(x): + elif isinstance(x, int): return f"{x:,.0f}" if thousands else f"{x:.0f}" return x @@ -1491,7 +1486,7 @@ def _wrap_decimal_thousands( """ def wrapper(x): - if is_float(x) or is_integer(x) or is_complex(x): + if isinstance(x, (float, complex, int)): if decimal != "." and thousands is not None and thousands != ",": return ( formatter(x) From 5aeb8b8e7a2b4946033b1a9c0f2d568f75fd0c9c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 21:08:10 +0100 Subject: [PATCH 36/44] html test --- pandas/tests/io/formats/style/test_html.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 2010d06c9d22d..31dbf10a43a84 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -804,3 +804,29 @@ 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( + """\ + + + a + 2.610000 + + + b + 2.690000 + + + mean + 2.650000 + + + + """ + ) + assert expected in result From 70184fa69f67d151c78c1d02f6e6b81b6bfbe431 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 23:07:27 +0100 Subject: [PATCH 37/44] doc test fix --- pandas/io/formats/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2bfa9bc792d8e..d1bd169f05159 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -332,10 +332,10 @@ def concat(self, other: Styler) -> Styler: >>> descriptors = df.agg(["sum", "mean", lambda s: s.dtype]) >>> descriptors.index = ["Total", "Average", "dtype"] - >>> other = descriptors.style + >>> 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;") + ... .applymap(lambda v: "font-weight: bold;")) >>> styler = df.style.highlight_max(color="salmon") >>> styler.concat(other) # doctest: +SKIP From 0c107d2bcc5de61a00e4c1753f42fa4e582bf773 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 23 Feb 2022 23:18:08 +0100 Subject: [PATCH 38/44] edit docs --- doc/source/_static/style/footer_extended.png | Bin 12302 -> 12326 bytes pandas/io/formats/style.py | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/source/_static/style/footer_extended.png b/doc/source/_static/style/footer_extended.png index 95a26fdd3ab90ecf55f31600fcffbf028601efba..3699d61ad4346e84748334a8fd5e5eb556364154 100644 GIT binary patch delta 9025 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 NlwPUJRmxa|{4WT#CISEe diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d1bd169f05159..2373bfa261bda 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -336,7 +336,11 @@ def concat(self, other: Styler) -> Styler: ... .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") + >>> styler = (df.style + ... .highlight_max(color="salmon") + ... .set_table_styles([ + ... {"selector": ".foot0", "props": "border-top: 1px solid black;"} + ... ])) >>> styler.concat(other) # doctest: +SKIP .. figure:: ../../_static/style/footer_extended.png From 5b44e9200d9f75f485b07df3e7d26fe5bdf5c868 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 25 Feb 2022 18:54:39 +0100 Subject: [PATCH 39/44] fix typing --- pandas/io/formats/style.py | 1 - pandas/io/formats/style_render.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2373bfa261bda..00dd63e7ae73e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -251,7 +251,6 @@ def __init__( cell_ids=cell_ids, precision=precision, ) - self.concatenated: Styler | None = None # validate ordered args thousands = thousands or get_option("styler.format.thousands") diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index c0c87dd48c196..a8d7a89461c45 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -118,7 +118,7 @@ def __init__( "foot": "foot", "foot_heading": "foot_heading", } - + self.concatenated: StylerRenderer | None = None # add rendering variables self.hide_index_names: bool = False self.hide_column_names: bool = False @@ -312,7 +312,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 = { @@ -328,9 +328,9 @@ def _translate( d.update({k: map}) if dx is not None: # self.concatenated is not None - d["body"].extend(dx["body"]) - d["cellstyle"].extend(dx["cellstyle"]) - d["cellstyle_index"].extend(dx["cellstyle"]) + 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"): From a21beefe5f3dcb2b1ce4b761b76d89128f1b3e17 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 25 Feb 2022 23:04:49 +0100 Subject: [PATCH 40/44] reorder calculation for recursion --- pandas/io/formats/style_render.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index a8d7a89461c45..59244352c6814 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -157,13 +157,13 @@ def _render( Computes and applies styles and then generates the general render dicts """ self._compute() - dx = ( - self.concatenated._translate( + dx = None + if self.concatenated is not None: + self.concatenated.hide_index_ = self.hide_index_ + self.concatenated.hidden_columns = self.hidden_columns + dx, _ = self.concatenated._render( sparse_index, sparse_columns, max_rows, max_cols, blank ) - if self.concatenated is not None - else None - ) d = self._translate(sparse_index, sparse_columns, max_rows, max_cols, blank, dx) return d, dx @@ -226,10 +226,6 @@ def _compute(self): (application method, *args, **kwargs) """ - if self.concatenated is not None: - self.concatenated.hide_index_ = self.hide_index_ - self.concatenated.hidden_columns = self.hidden_columns - self.concatenated._compute() self.ctx.clear() self.ctx_index.clear() self.ctx_columns.clear() From 5debdf68f6f4bbddffb63c94cbf12a196d35e99d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 25 Feb 2022 23:21:33 +0100 Subject: [PATCH 41/44] allow working chained multiple concatenation --- pandas/io/formats/style.py | 15 +++------------ pandas/io/formats/style_render.py | 9 ++++++++- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 00dd63e7ae73e..cf1aa8f0e6675 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -337,9 +337,8 @@ def concat(self, other: Styler) -> Styler: ... .applymap(lambda v: "font-weight: bold;")) >>> styler = (df.style ... .highlight_max(color="salmon") - ... .set_table_styles([ - ... {"selector": ".foot0", "props": "border-top: 1px solid black;"} - ... ])) + ... .set_table_styles([{"selector": ".foot_row0", + ... "props": "border-top: 1px solid black;"}])) >>> styler.concat(other) # doctest: +SKIP .. figure:: ../../_static/style/footer_extended.png @@ -348,13 +347,6 @@ def concat(self, other: Styler) -> 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`") - other.set_table_styles( - css_class_names={ - "data": self.css["foot"], - "row_heading": self.css["foot_heading"], - "row": self.css["foot"], - } - ) self.concatenated = other return self @@ -2443,8 +2435,7 @@ def set_table_styles( "level": "level", "data": "data", "blank": "blank", - "foot": "foot", - "foot_heading": "foot_heading"} + "foot": "foot"} Examples -------- diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 59244352c6814..eaffec4c2ef6d 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -116,7 +116,6 @@ def __init__( "data": "data", "blank": "blank", "foot": "foot", - "foot_heading": "foot_heading", } self.concatenated: StylerRenderer | None = None # add rendering variables @@ -161,6 +160,14 @@ def _render( if self.concatenated is not None: self.concatenated.hide_index_ = self.hide_index_ self.concatenated.hidden_columns = self.hidden_columns + self.concatenated.set_table_styles( + css_class_names={ + "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 ) From eccf6db4033366845fee1ea4dad0e094605ab37c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 25 Feb 2022 23:27:43 +0100 Subject: [PATCH 42/44] fix test with chaining --- pandas/tests/io/formats/style/test_html.py | 27 +++++++++------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/pandas/tests/io/formats/style/test_html.py b/pandas/tests/io/formats/style/test_html.py index 31dbf10a43a84..2abc963525977 100644 --- a/pandas/tests/io/formats/style/test_html.py +++ b/pandas/tests/io/formats/style/test_html.py @@ -812,21 +812,16 @@ def test_concat(styler): result = styler.to_html() expected = dedent( """\ - - - a - 2.610000 - - - b - 2.690000 - - - mean - 2.650000 - - - - """ + + b + 2.690000 + + + mean + 2.650000 + + + + """ ) assert expected in result From f2701a245e958681c844a2880825c443b977d249 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sat, 26 Feb 2022 21:09:54 +0100 Subject: [PATCH 43/44] mypy fix --- pandas/io/formats/style_render.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 6ce7d33b14234..475e49cb848b5 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -165,14 +165,13 @@ def _render( if self.concatenated is not None: self.concatenated.hide_index_ = self.hide_index_ self.concatenated.hidden_columns = self.hidden_columns - self.concatenated.set_table_styles( - css_class_names={ - "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"], - } - ) + 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 ) From 534d5c04d796bab3e5bbf947d9c413486e58ebdd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 27 Feb 2022 12:32:26 +0100 Subject: [PATCH 44/44] mypy fix --- pandas/io/formats/style.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index cf1aa8f0e6675..27f9801ea35e3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -275,6 +275,8 @@ def concat(self, other: Styler) -> Styler: """ Append another Styler to combine the output into a single table. + .. versionadded:: 1.5.0 + Parameters ---------- other : Styler