From 971fb40ffa2145c080f00f66f638012d9ae9dd93 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 10:42:40 +0100 Subject: [PATCH 01/14] bug: formatter overwrites na_rep --- pandas/io/formats/style.py | 202 ++++++++++---------- pandas/tests/io/formats/style/test_style.py | 25 +-- 2 files changed, 109 insertions(+), 118 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 854f41d6b4dc3..d6afde7ac85df 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -7,7 +7,6 @@ from contextlib import contextmanager import copy from functools import partial -from itertools import product from typing import ( Any, Callable, @@ -36,14 +35,10 @@ from pandas.compat._optional import import_optional_dependency from pandas.util._decorators import doc -from pandas.core.dtypes.common import is_float from pandas.core.dtypes.generic import ABCSeries import pandas as pd -from pandas.api.types import ( - is_dict_like, - is_list_like, -) +from pandas.api.types import is_list_like from pandas.core import generic import pandas.core.common as com from pandas.core.frame import DataFrame @@ -222,13 +217,105 @@ def _init_tooltips(self): self.tooltips = _Tooltips() def _default_display_func(self, x): - if self.na_rep is not None and pd.isna(x): - return self.na_rep - elif is_float(x): - display_format = f"{x:.{self.precision}f}" - return display_format + return self._maybe_wrap_formatter(formatter=None)(x) + + def _default_formatter(self, x): + if isinstance(x, (float, complex)): + return f"{x:.{self.precision}f}" + return x + + def _maybe_wrap_formatter( + self, + formatter: Optional[Union[Callable, str]] = None, + na_rep: Optional[str] = None, + ) -> Callable: + """ + Allows formatters to be expressed as str, callable or None, where None returns + a default formatting function. wraps with na_rep where it is available. + """ + if formatter is None: + func = self._default_formatter + elif isinstance(formatter, str): + func = lambda x: formatter.format(x) + elif callable(formatter): + func = formatter else: - return x + raise TypeError( + f"'formatter' expected str or callable, got {type(formatter)}" + ) + + if all((na_rep is None, self.na_rep is None)): + return func + elif na_rep is not None: + return lambda x: na_rep if pd.isna(x) else func(x) + elif self.na_rep is not None: + return lambda x: self.na_rep if pd.isna(x) else func(x) + + def format( + self, + formatter: Optional[ + Union[Dict[Any, Union[str, Callable]], str, Callable] + ] = None, + subset=None, + na_rep: Optional[str] = None, + ) -> Styler: + """ + Format the text display value of cells. + + Parameters + ---------- + formatter : str, callable, dict or None + If ``formatter`` is None, the default formatter is used. + subset : IndexSlice + An argument to ``DataFrame.loc`` that restricts which elements + ``formatter`` is applied to. + na_rep : str, optional + Representation for missing values. + If ``na_rep`` is None, no special formatting is applied. + + .. versionadded:: 1.0.0 + + Returns + ------- + self : Styler + + Notes + ----- + ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where + ``a`` is one of + + - str: this will be wrapped in: ``a.format(x)`` + - callable: called with the value of an individual cell + + The default display value for numeric values is the "general" (``g``) + format with ``pd.options.display.precision`` precision. + + Examples + -------- + >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b']) + >>> df.style.format("{:.2%}") + >>> df['c'] = ['a', 'b', 'c', 'd'] + >>> df.style.format({'c': str.upper}) + """ + subset = slice(None) if subset is None else subset + subset = _non_reducing_slice(subset) + data = self.data.loc[subset] + + if not isinstance(formatter, dict): + formatter = {col: formatter for col in data.columns} + + for col in data.columns: + try: + format_func = formatter[col] + except KeyError: + format_func = None + format_func = self._maybe_wrap_formatter(format_func, na_rep=na_rep) + + for row, value in data[[col]].itertuples(): + i, j = self.index.get_loc(row), self.columns.get_loc(col) + self._display_funcs[(i, j)] = format_func + + return self def set_tooltips(self, ttips: DataFrame) -> Styler: """ @@ -575,77 +662,6 @@ def _translate(self): return d - def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> Styler: - """ - Format the text display value of cells. - - Parameters - ---------- - formatter : str, callable, dict or None - If ``formatter`` is None, the default formatter is used. - subset : IndexSlice - An argument to ``DataFrame.loc`` that restricts which elements - ``formatter`` is applied to. - na_rep : str, optional - Representation for missing values. - If ``na_rep`` is None, no special formatting is applied. - - .. versionadded:: 1.0.0 - - Returns - ------- - self : Styler - - Notes - ----- - ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where - ``a`` is one of - - - str: this will be wrapped in: ``a.format(x)`` - - callable: called with the value of an individual cell - - The default display value for numeric values is the "general" (``g``) - format with ``pd.options.display.precision`` precision. - - Examples - -------- - >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b']) - >>> df.style.format("{:.2%}") - >>> df['c'] = ['a', 'b', 'c', 'd'] - >>> df.style.format({'c': str.upper}) - """ - if formatter is None: - assert self._display_funcs.default_factory is not None - formatter = self._display_funcs.default_factory() - - if subset is None: - row_locs = range(len(self.data)) - col_locs = range(len(self.data.columns)) - else: - subset = _non_reducing_slice(subset) - if len(subset) == 1: - subset = subset, self.data.columns - - sub_df = self.data.loc[subset] - row_locs = self.data.index.get_indexer_for(sub_df.index) - col_locs = self.data.columns.get_indexer_for(sub_df.columns) - - if is_dict_like(formatter): - for col, col_formatter in formatter.items(): - # formatter must be callable, so '{}' are converted to lambdas - col_formatter = _maybe_wrap_formatter(col_formatter, na_rep) - col_num = self.data.columns.get_indexer_for([col])[0] - - for row_num in row_locs: - self._display_funcs[(row_num, col_num)] = col_formatter - else: - # single scalar to format all cells with - formatter = _maybe_wrap_formatter(formatter, na_rep) - locs = product(*(row_locs, col_locs)) - for i, j in locs: - self._display_funcs[(i, j)] = formatter - return self - def set_td_classes(self, classes: DataFrame) -> Styler: """ Add string based CSS class names to data cells that will appear within the @@ -2035,26 +2051,6 @@ def _get_level_lengths(index, hidden_elements=None): return non_zero_lengths -def _maybe_wrap_formatter( - formatter: Union[Callable, str], na_rep: Optional[str] -) -> Callable: - if isinstance(formatter, str): - formatter_func = lambda x: formatter.format(x) - elif callable(formatter): - formatter_func = formatter - else: - msg = f"Expected a template string or callable, got {formatter} instead" - raise TypeError(msg) - - if na_rep is None: - return formatter_func - elif isinstance(na_rep, str): - return lambda x: na_rep if pd.isna(x) else formatter_func(x) - else: - msg = f"Expected a string, got {na_rep} instead" - raise TypeError(msg) - - def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: """ Convert css-string to sequence of tuples format if needed. diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 01ed234f6e248..5404b679d5aa7 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -565,12 +565,12 @@ def test_format_non_numeric_na(self): assert ctx["body"][1][1]["display_value"] == "-" assert ctx["body"][1][2]["display_value"] == "-" - def test_format_with_bad_na_rep(self): - # GH 21527 28358 - df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"]) - msg = "Expected a string, got -1 instead" - with pytest.raises(TypeError, match=msg): - df.style.format(None, na_rep=-1) + # def test_format_with_bad_na_rep(self): + # # GH 21527 28358 + # df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"]) + # msg = "Expected a string, got -1 instead" + # with pytest.raises(TypeError, match=msg): + # df.style.format(None, na_rep=-1) def test_nonunique_raises(self): df = DataFrame([[1, 2]], columns=["A", "A"]) @@ -697,15 +697,10 @@ def test_display_format(self): ) assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 - def test_display_format_raises(self): - df = DataFrame(np.random.randn(2, 2)) - msg = "Expected a template string or callable, got 5 instead" - with pytest.raises(TypeError, match=msg): - df.style.format(5) - - msg = "Expected a template string or callable, got True instead" - with pytest.raises(TypeError, match=msg): - df.style.format(True) + @pytest.mark.parametrize("formatter", [5, True, [2.0]]) + def test_display_format_raises(self, formatter): + with pytest.raises(TypeError, match="expected str or callable"): + self.df.style.format(formatter) def test_display_set_precision(self): # Issue #13257 From 6f573eebb1f51e2fa44e5bbfd10b88df511db1b5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 11:50:05 +0100 Subject: [PATCH 02/14] bug: formatter overwrites na_rep --- pandas/io/formats/style.py | 52 ++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d6afde7ac85df..f7df8e640fe67 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -265,13 +265,15 @@ def format( Parameters ---------- formatter : str, callable, dict or None - If ``formatter`` is None, the default formatter is used. + Format specification to use for displaying values. If ``None``, the default + formatter is used. If ``dict``, keys should corresponcd to column names, + and values should be string or callable. subset : IndexSlice An argument to ``DataFrame.loc`` that restricts which elements ``formatter`` is applied to. na_rep : str, optional - Representation for missing values. - If ``na_rep`` is None, no special formatting is applied. + Representation for missing values. If ``None``, will revert to using + ``Styler.na_rep`` .. versionadded:: 1.0.0 @@ -279,23 +281,45 @@ def format( ------- self : Styler + See Also + -------- + Styler.set_na_rep : Set the missing data representation on a Styler. + Styler.set_precision :Set the precision used to display values. + Notes ----- - ``formatter`` is either an ``a`` or a dict ``{column name: a}`` where - ``a`` is one of + This method assigns a formatting function to each cell in the DataFrame. Where + arguments are given as string this is wrapped to a callable as ``str.format(x)`` - - str: this will be wrapped in: ``a.format(x)`` - - callable: called with the value of an individual cell + The ``subset`` argument is all encompassing. If a dict key for ``formatter`` is + not included within the ``subset`` columns it will be ignored. Any cells + included within the subset that do not correspond to dict key columns will + have default formatter applied. - The default display value for numeric values is the "general" (``g``) - format with ``pd.options.display.precision`` precision. + The default formatter currently expresses floats and complex numbers with the + precision defined by ``Styler.precision``, leaving all other types unformatted. Examples -------- - >>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b']) - >>> df.style.format("{:.2%}") - >>> df['c'] = ['a', 'b', 'c', 'd'] - >>> df.style.format({'c': str.upper}) + >>> df = pd.DataFrame([[1.0, 2.0],[3.0, 4.0]], columns=['a', 'b']) + >>> df.style.format({'a': '{:.0f}'}) + a b + 0 1 2.000000 + 1 3 4.000000 + + >>> df = pd.DataFrame(np.nan, + ... columns=['a', 'b', 'c', 'd'], + ... index=['x', 'y', 'z']) + >>> df.iloc[0, :] = 1.9 + >>> df.style.set_precision(3) + ... .format({'b': '{:.0f}', 'c': '{:.1f}'.format}, + ... na_rep='HARD', + ... subset=pd.IndexSlice[['y','x'], ['a', 'b', 'c']]) + ... .set_na_rep('SOFT') + a b c d + x 1.900 2 1.9 1.900 + y HARD HARD HARD SOFT + z SOFT SOFT SOFT SOFT """ subset = slice(None) if subset is None else subset subset = _non_reducing_slice(subset) @@ -1047,7 +1071,7 @@ def where( def set_precision(self, precision: int) -> Styler: """ - Set the precision used to render. + Set the precision used to display values. Parameters ---------- From f50f89408a21f0196fbbd684e06272e57a9fc597 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 19:14:58 +0100 Subject: [PATCH 03/14] bug: formatter overwrites na_rep --- pandas/io/formats/style.py | 18 +++++++++----- pandas/tests/io/formats/style/test_style.py | 26 +++++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index f7df8e640fe67..8fdcdc0a71492 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -223,6 +223,10 @@ def _default_formatter(self, x): if isinstance(x, (float, complex)): return f"{x:.{self.precision}f}" return x + # if self.na_rep is None: + # return x + # else: + # return self.na_rep if pd.isna(x) else x def _maybe_wrap_formatter( self, @@ -244,12 +248,14 @@ def _maybe_wrap_formatter( f"'formatter' expected str or callable, got {type(formatter)}" ) - if all((na_rep is None, self.na_rep is None)): - return func - elif na_rep is not None: + if na_rep is not None: return lambda x: na_rep if pd.isna(x) else func(x) - elif self.na_rep is not None: - return lambda x: self.na_rep if pd.isna(x) else func(x) + else: + return ( + lambda x: self.na_rep + if all((self.na_rep is not None, pd.isna(x))) + else func(x) + ) def format( self, @@ -284,7 +290,7 @@ def format( See Also -------- Styler.set_na_rep : Set the missing data representation on a Styler. - Styler.set_precision :Set the precision used to display values. + Styler.set_precision : Set the precision used to display values. Notes ----- diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 5404b679d5aa7..605217c552379 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -565,13 +565,6 @@ def test_format_non_numeric_na(self): assert ctx["body"][1][1]["display_value"] == "-" assert ctx["body"][1][2]["display_value"] == "-" - # def test_format_with_bad_na_rep(self): - # # GH 21527 28358 - # df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"]) - # msg = "Expected a string, got -1 instead" - # with pytest.raises(TypeError, match=msg): - # df.style.format(None, na_rep=-1) - def test_nonunique_raises(self): df = DataFrame([[1, 2]], columns=["A", "A"]) msg = "style is not supported for non-unique indices." @@ -697,6 +690,25 @@ def test_display_format(self): ) assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 + @pytest.mark.parametrize( + "kwargs, prec", + [ + ({"formatter": "{:.1f}", "subset": pd.IndexSlice["x", :]}, 2), + ({"formatter": "{:.2f}", "subset": pd.IndexSlice[:, "a"]}, 1), + ], + ) + def test_display_format_subset_interaction(self, kwargs, prec): + # test subset and formatter interaction in conjunction with other methods + df = DataFrame([[np.nan, 1], [2, np.nan]], columns=["a", "b"], index=["x", "y"]) + ctx = df.style.format(**kwargs).set_na_rep("-").set_precision(prec)._translate() + assert ctx["body"][0][1]["display_value"] == "-" + assert ctx["body"][0][2]["display_value"] == "1.0" + assert ctx["body"][1][1]["display_value"] == "2.00" + assert ctx["body"][1][2]["display_value"] == "-" + ctx = df.style.format(**kwargs)._translate() + assert ctx["body"][0][1]["display_value"] == "nan" + assert ctx["body"][1][2]["display_value"] == "nan" + @pytest.mark.parametrize("formatter", [5, True, [2.0]]) def test_display_format_raises(self, formatter): with pytest.raises(TypeError, match="expected str or callable"): From 47c4021d642133afca45de991f9d811b7ed8c0cf Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 19:16:33 +0100 Subject: [PATCH 04/14] bug: formatter overwrites na_rep --- pandas/io/formats/style.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 8fdcdc0a71492..717ee2727b5f4 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -223,10 +223,6 @@ def _default_formatter(self, x): if isinstance(x, (float, complex)): return f"{x:.{self.precision}f}" return x - # if self.na_rep is None: - # return x - # else: - # return self.na_rep if pd.isna(x) else x def _maybe_wrap_formatter( self, From 7371e44a454c491a12cc58a7599482c8927af03e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 19:22:17 +0100 Subject: [PATCH 05/14] bug: formatter overwrites na_rep --- pandas/io/formats/style.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 717ee2727b5f4..de5cd7af3975a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -299,7 +299,9 @@ def format( have default formatter applied. The default formatter currently expresses floats and complex numbers with the - precision defined by ``Styler.precision``, leaving all other types unformatted. + precision defined by ``Styler.precision``, leaving all other types unformatted, + and replacing missing values with the string defined in ``Styler.na_rep``, if + set. Examples -------- From 82474b11924726a0fb8756d0a546b99e8af49303 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 20:07:20 +0100 Subject: [PATCH 06/14] group tests --- pandas/tests/io/formats/style/test_style.py | 217 +++++++++++--------- 1 file changed, 115 insertions(+), 102 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 605217c552379..7703ff2aa44db 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -565,6 +565,121 @@ def test_format_non_numeric_na(self): assert ctx["body"][1][1]["display_value"] == "-" assert ctx["body"][1][2]["display_value"] == "-" + def test_display_format(self): + df = DataFrame(np.random.random(size=(2, 2))) + ctx = df.style.format("{:0.1f}")._translate() + + assert all(["display_value" in c for c in row] for row in ctx["body"]) + assert all( + [len(c["display_value"]) <= 3 for c in row[1:]] for row in ctx["body"] + ) + assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 + + def test_display_format_subset_interaction(self): + # GH40032 + # test subset and formatter interaction in conjunction with other methods + df = DataFrame([[np.nan, 1], [2, np.nan]], columns=["a", "b"], index=["x", "y"]) + + ctx = df.style.format({"a": "{:.1f}"}).set_na_rep("X")._translate() + assert ctx["body"][0][1]["display_value"] == "X" + assert ctx["body"][1][2]["display_value"] == "X" + ctx = df.style.format({"a": "{:.1f}"}, na_rep="Y").set_na_rep("X")._translate() + assert ctx["body"][0][1]["display_value"] == "Y" + assert ctx["body"][1][2]["display_value"] == "Y" + ctx = ( + df.style.format("{:.1f}", na_rep="Y", subset=["a"]) + .set_na_rep("X") + ._translate() + ) + assert ctx["body"][0][1]["display_value"] == "Y" + assert ctx["body"][1][2]["display_value"] == "X" + + ctx = df.style.format({"a": "{:.1f}"}).set_precision(2)._translate() + assert ctx["body"][0][2]["display_value"] == "1.00" + assert ctx["body"][1][1]["display_value"] == "2.0" + ctx = df.style.format("{:.1f}").set_precision(2)._translate() + assert ctx["body"][0][2]["display_value"] == "1.0" + assert ctx["body"][1][1]["display_value"] == "2.0" + ctx = df.style.format("{:.1f}", subset=["a"]).set_precision(2)._translate() + assert ctx["body"][0][2]["display_value"] == "1.00" + assert ctx["body"][1][1]["display_value"] == "2.0" + ctx = df.style.format(None, subset=["a"]).set_precision(2)._translate() + assert ctx["body"][0][2]["display_value"] == "1.00" + assert ctx["body"][1][1]["display_value"] == "2.00" + + @pytest.mark.parametrize("formatter", [5, True, [2.0]]) + def test_display_format_raises(self, formatter): + with pytest.raises(TypeError, match="expected str or callable"): + self.df.style.format(formatter) + + def test_display_set_precision(self): + # Issue #13257 + df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"]) + s = Styler(df) + + ctx = s.set_precision(1)._translate() + + assert s.precision == 1 + assert ctx["body"][0][1]["display_value"] == "1.0" + assert ctx["body"][0][2]["display_value"] == "2.0" + assert ctx["body"][1][1]["display_value"] == "3.2" + assert ctx["body"][1][2]["display_value"] == "4.6" + + ctx = s.set_precision(2)._translate() + assert s.precision == 2 + assert ctx["body"][0][1]["display_value"] == "1.00" + assert ctx["body"][0][2]["display_value"] == "2.01" + assert ctx["body"][1][1]["display_value"] == "3.21" + assert ctx["body"][1][2]["display_value"] == "4.57" + + ctx = s.set_precision(3)._translate() + assert s.precision == 3 + assert ctx["body"][0][1]["display_value"] == "1.000" + assert ctx["body"][0][2]["display_value"] == "2.009" + assert ctx["body"][1][1]["display_value"] == "3.212" + assert ctx["body"][1][2]["display_value"] == "4.566" + + def test_format_subset(self): + df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) + ctx = df.style.format( + {"a": "{:0.1f}", "b": "{0:.2%}"}, subset=pd.IndexSlice[0, :] + )._translate() + expected = "0.1" + raw_11 = "1.123400" + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == raw_11 + assert ctx["body"][0][2]["display_value"] == "12.34%" + + ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, :])._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == raw_11 + + ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice["a"])._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][0][2]["display_value"] == "0.123400" + + ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, "a"])._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == raw_11 + + ctx = df.style.format( + "{:0.1f}", subset=pd.IndexSlice[[0, 1], ["a"]] + )._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == "1.1" + assert ctx["body"][0][2]["display_value"] == "0.123400" + assert ctx["body"][1][2]["display_value"] == raw_11 + + def test_display_dict(self): + df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) + ctx = df.style.format({"a": "{:0.1f}", "b": "{0:.2%}"})._translate() + assert ctx["body"][0][1]["display_value"] == "0.1" + assert ctx["body"][0][2]["display_value"] == "12.34%" + df["c"] = ["aaa", "bbb"] + ctx = df.style.format({"a": "{:0.1f}", "c": str.upper})._translate() + assert ctx["body"][0][1]["display_value"] == "0.1" + assert ctx["body"][0][3]["display_value"] == "AAA" + def test_nonunique_raises(self): df = DataFrame([[1, 2]], columns=["A", "A"]) msg = "style is not supported for non-unique indices." @@ -680,108 +795,6 @@ def test_export(self): assert style1._todo == style2._todo style2.render() - def test_display_format(self): - df = DataFrame(np.random.random(size=(2, 2))) - ctx = df.style.format("{:0.1f}")._translate() - - assert all(["display_value" in c for c in row] for row in ctx["body"]) - assert all( - [len(c["display_value"]) <= 3 for c in row[1:]] for row in ctx["body"] - ) - assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 - - @pytest.mark.parametrize( - "kwargs, prec", - [ - ({"formatter": "{:.1f}", "subset": pd.IndexSlice["x", :]}, 2), - ({"formatter": "{:.2f}", "subset": pd.IndexSlice[:, "a"]}, 1), - ], - ) - def test_display_format_subset_interaction(self, kwargs, prec): - # test subset and formatter interaction in conjunction with other methods - df = DataFrame([[np.nan, 1], [2, np.nan]], columns=["a", "b"], index=["x", "y"]) - ctx = df.style.format(**kwargs).set_na_rep("-").set_precision(prec)._translate() - assert ctx["body"][0][1]["display_value"] == "-" - assert ctx["body"][0][2]["display_value"] == "1.0" - assert ctx["body"][1][1]["display_value"] == "2.00" - assert ctx["body"][1][2]["display_value"] == "-" - ctx = df.style.format(**kwargs)._translate() - assert ctx["body"][0][1]["display_value"] == "nan" - assert ctx["body"][1][2]["display_value"] == "nan" - - @pytest.mark.parametrize("formatter", [5, True, [2.0]]) - def test_display_format_raises(self, formatter): - with pytest.raises(TypeError, match="expected str or callable"): - self.df.style.format(formatter) - - def test_display_set_precision(self): - # Issue #13257 - df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"]) - s = Styler(df) - - ctx = s.set_precision(1)._translate() - - assert s.precision == 1 - assert ctx["body"][0][1]["display_value"] == "1.0" - assert ctx["body"][0][2]["display_value"] == "2.0" - assert ctx["body"][1][1]["display_value"] == "3.2" - assert ctx["body"][1][2]["display_value"] == "4.6" - - ctx = s.set_precision(2)._translate() - assert s.precision == 2 - assert ctx["body"][0][1]["display_value"] == "1.00" - assert ctx["body"][0][2]["display_value"] == "2.01" - assert ctx["body"][1][1]["display_value"] == "3.21" - assert ctx["body"][1][2]["display_value"] == "4.57" - - ctx = s.set_precision(3)._translate() - assert s.precision == 3 - assert ctx["body"][0][1]["display_value"] == "1.000" - assert ctx["body"][0][2]["display_value"] == "2.009" - assert ctx["body"][1][1]["display_value"] == "3.212" - assert ctx["body"][1][2]["display_value"] == "4.566" - - def test_display_subset(self): - df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) - ctx = df.style.format( - {"a": "{:0.1f}", "b": "{0:.2%}"}, subset=pd.IndexSlice[0, :] - )._translate() - expected = "0.1" - raw_11 = "1.123400" - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == raw_11 - assert ctx["body"][0][2]["display_value"] == "12.34%" - - ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, :])._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == raw_11 - - ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice["a"])._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][0][2]["display_value"] == "0.123400" - - ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, "a"])._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == raw_11 - - ctx = df.style.format( - "{:0.1f}", subset=pd.IndexSlice[[0, 1], ["a"]] - )._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == "1.1" - assert ctx["body"][0][2]["display_value"] == "0.123400" - assert ctx["body"][1][2]["display_value"] == raw_11 - - def test_display_dict(self): - df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) - ctx = df.style.format({"a": "{:0.1f}", "b": "{0:.2%}"})._translate() - assert ctx["body"][0][1]["display_value"] == "0.1" - assert ctx["body"][0][2]["display_value"] == "12.34%" - df["c"] = ["aaa", "bbb"] - ctx = df.style.format({"a": "{:0.1f}", "c": str.upper})._translate() - assert ctx["body"][0][1]["display_value"] == "0.1" - assert ctx["body"][0][3]["display_value"] == "AAA" - def test_bad_apply_shape(self): df = DataFrame([[1, 2], [3, 4]]) msg = "returned the wrong shape" From 9a9a39b53118026ce55ae1e80cea189796d20f7e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 20:20:07 +0100 Subject: [PATCH 07/14] revert small chg --- pandas/io/formats/style.py | 7 +++++-- pandas/tests/io/formats/style/test_style.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index de5cd7af3975a..b6358a22fb14c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -330,9 +330,12 @@ def format( data = self.data.loc[subset] if not isinstance(formatter, dict): - formatter = {col: formatter for col in data.columns} + columns = data.columns + formatter = {col: formatter for col in columns} + else: + columns = formatter.keys() - for col in data.columns: + for col in columns: try: format_func = formatter[col] except KeyError: diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 7703ff2aa44db..0fb0b89210c18 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -585,7 +585,7 @@ def test_display_format_subset_interaction(self): assert ctx["body"][1][2]["display_value"] == "X" ctx = df.style.format({"a": "{:.1f}"}, na_rep="Y").set_na_rep("X")._translate() assert ctx["body"][0][1]["display_value"] == "Y" - assert ctx["body"][1][2]["display_value"] == "Y" + assert ctx["body"][1][2]["display_value"] == "X" ctx = ( df.style.format("{:.1f}", na_rep="Y", subset=["a"]) .set_na_rep("X") From f72b335348ba840db90380b609d68c5946870490 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 20:30:16 +0100 Subject: [PATCH 08/14] docs --- pandas/io/formats/style.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b6358a22fb14c..743bb18ab6aa7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -293,10 +293,10 @@ def format( This method assigns a formatting function to each cell in the DataFrame. Where arguments are given as string this is wrapped to a callable as ``str.format(x)`` - The ``subset`` argument is all encompassing. If a dict key for ``formatter`` is - not included within the ``subset`` columns it will be ignored. Any cells - included within the subset that do not correspond to dict key columns will - have default formatter applied. + If the ``subset`` argument is given as well as the ``formatter`` argument in + dict form then the intersection of the ``subset`` and the columns as keys + of the dict are used to define the formatting region. Keys in the dict that + do not exist in the ``subset`` will raise a ``KeyError``. The default formatter currently expresses floats and complex numbers with the precision defined by ``Styler.precision``, leaving all other types unformatted, @@ -322,8 +322,8 @@ def format( ... .set_na_rep('SOFT') a b c d x 1.900 2 1.9 1.900 - y HARD HARD HARD SOFT - z SOFT SOFT SOFT SOFT + y SOFT HARD HARD SOFT + z SOFT SOFT SOFT SOFT """ subset = slice(None) if subset is None else subset subset = _non_reducing_slice(subset) From 0792393346de548ba0b95386aa08e36a9fbe7460 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 20:45:41 +0100 Subject: [PATCH 09/14] test --- pandas/tests/io/formats/style/test_style.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 0fb0b89210c18..9a7bce37cefa1 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -607,6 +607,9 @@ def test_display_format_subset_interaction(self): assert ctx["body"][0][2]["display_value"] == "1.00" assert ctx["body"][1][1]["display_value"] == "2.00" + with pytest.raises(KeyError, match="are in the [columns]"): + df.style.format({"a": "{:.0f}"}, subset=["b"]) + @pytest.mark.parametrize("formatter", [5, True, [2.0]]) def test_display_format_raises(self, formatter): with pytest.raises(TypeError, match="expected str or callable"): From 7bb11710f1670e9761903e2d9f35d139a2e13de9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 22:58:07 +0100 Subject: [PATCH 10/14] mypy --- 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 743bb18ab6aa7..e6b975c480aae 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -236,7 +236,7 @@ def _maybe_wrap_formatter( if formatter is None: func = self._default_formatter elif isinstance(formatter, str): - func = lambda x: formatter.format(x) + func = lambda x: formatter.format(x) # type: ignore # mypy issue 10136 elif callable(formatter): func = formatter else: @@ -256,7 +256,7 @@ def _maybe_wrap_formatter( def format( self, formatter: Optional[ - Union[Dict[Any, Union[str, Callable]], str, Callable] + Union[Dict[Any, Optional[Union[str, Callable]]], str, Callable] ] = None, subset=None, na_rep: Optional[str] = None, From 0d675624a208bddcede783f2d9b2b9c4bf79852e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 23:01:50 +0100 Subject: [PATCH 11/14] mypy --- pandas/io/formats/style.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e6b975c480aae..4bb9e7b164105 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -233,12 +233,12 @@ def _maybe_wrap_formatter( Allows formatters to be expressed as str, callable or None, where None returns a default formatting function. wraps with na_rep where it is available. """ - if formatter is None: - func = self._default_formatter - elif isinstance(formatter, str): - func = lambda x: formatter.format(x) # type: ignore # mypy issue 10136 + if isinstance(formatter, str): + func = lambda x: formatter.format(x) elif callable(formatter): func = formatter + elif formatter is None: + func = self._default_formatter else: raise TypeError( f"'formatter' expected str or callable, got {type(formatter)}" From 384916d381224f7f950c483bbffa84944350d561 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 23:09:31 +0100 Subject: [PATCH 12/14] easy compare --- pandas/io/formats/style.py | 190 ++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 4bb9e7b164105..dd4e3a1ef955a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -253,101 +253,6 @@ def _maybe_wrap_formatter( else func(x) ) - def format( - self, - formatter: Optional[ - Union[Dict[Any, Optional[Union[str, Callable]]], str, Callable] - ] = None, - subset=None, - na_rep: Optional[str] = None, - ) -> Styler: - """ - Format the text display value of cells. - - Parameters - ---------- - formatter : str, callable, dict or None - Format specification to use for displaying values. If ``None``, the default - formatter is used. If ``dict``, keys should corresponcd to column names, - and values should be string or callable. - subset : IndexSlice - An argument to ``DataFrame.loc`` that restricts which elements - ``formatter`` is applied to. - na_rep : str, optional - Representation for missing values. If ``None``, will revert to using - ``Styler.na_rep`` - - .. versionadded:: 1.0.0 - - Returns - ------- - self : Styler - - See Also - -------- - Styler.set_na_rep : Set the missing data representation on a Styler. - Styler.set_precision : Set the precision used to display values. - - Notes - ----- - This method assigns a formatting function to each cell in the DataFrame. Where - arguments are given as string this is wrapped to a callable as ``str.format(x)`` - - If the ``subset`` argument is given as well as the ``formatter`` argument in - dict form then the intersection of the ``subset`` and the columns as keys - of the dict are used to define the formatting region. Keys in the dict that - do not exist in the ``subset`` will raise a ``KeyError``. - - The default formatter currently expresses floats and complex numbers with the - precision defined by ``Styler.precision``, leaving all other types unformatted, - and replacing missing values with the string defined in ``Styler.na_rep``, if - set. - - Examples - -------- - >>> df = pd.DataFrame([[1.0, 2.0],[3.0, 4.0]], columns=['a', 'b']) - >>> df.style.format({'a': '{:.0f}'}) - a b - 0 1 2.000000 - 1 3 4.000000 - - >>> df = pd.DataFrame(np.nan, - ... columns=['a', 'b', 'c', 'd'], - ... index=['x', 'y', 'z']) - >>> df.iloc[0, :] = 1.9 - >>> df.style.set_precision(3) - ... .format({'b': '{:.0f}', 'c': '{:.1f}'.format}, - ... na_rep='HARD', - ... subset=pd.IndexSlice[['y','x'], ['a', 'b', 'c']]) - ... .set_na_rep('SOFT') - a b c d - x 1.900 2 1.9 1.900 - y SOFT HARD HARD SOFT - z SOFT SOFT SOFT SOFT - """ - subset = slice(None) if subset is None else subset - subset = _non_reducing_slice(subset) - data = self.data.loc[subset] - - if not isinstance(formatter, dict): - columns = data.columns - formatter = {col: formatter for col in columns} - else: - columns = formatter.keys() - - for col in columns: - try: - format_func = formatter[col] - except KeyError: - format_func = None - format_func = self._maybe_wrap_formatter(format_func, na_rep=na_rep) - - for row, value in data[[col]].itertuples(): - i, j = self.index.get_loc(row), self.columns.get_loc(col) - self._display_funcs[(i, j)] = format_func - - return self - def set_tooltips(self, ttips: DataFrame) -> Styler: """ Add string based tooltips that will appear in the `Styler` HTML result. These @@ -693,6 +598,101 @@ def _translate(self): return d + def format( + self, + formatter: Optional[ + Union[Dict[Any, Optional[Union[str, Callable]]], str, Callable] + ] = None, + subset=None, + na_rep: Optional[str] = None, + ) -> Styler: + """ + Format the text display value of cells. + + Parameters + ---------- + formatter : str, callable, dict or None + Format specification to use for displaying values. If ``None``, the default + formatter is used. If ``dict``, keys should corresponcd to column names, + and values should be string or callable. + subset : IndexSlice + An argument to ``DataFrame.loc`` that restricts which elements + ``formatter`` is applied to. + na_rep : str, optional + Representation for missing values. If ``None``, will revert to using + ``Styler.na_rep`` + + .. versionadded:: 1.0.0 + + Returns + ------- + self : Styler + + See Also + -------- + Styler.set_na_rep : Set the missing data representation on a Styler. + Styler.set_precision : Set the precision used to display values. + + Notes + ----- + This method assigns a formatting function to each cell in the DataFrame. Where + arguments are given as string this is wrapped to a callable as ``str.format(x)`` + + If the ``subset`` argument is given as well as the ``formatter`` argument in + dict form then the intersection of the ``subset`` and the columns as keys + of the dict are used to define the formatting region. Keys in the dict that + do not exist in the ``subset`` will raise a ``KeyError``. + + The default formatter currently expresses floats and complex numbers with the + precision defined by ``Styler.precision``, leaving all other types unformatted, + and replacing missing values with the string defined in ``Styler.na_rep``, if + set. + + Examples + -------- + >>> df = pd.DataFrame([[1.0, 2.0],[3.0, 4.0]], columns=['a', 'b']) + >>> df.style.format({'a': '{:.0f}'}) + a b + 0 1 2.000000 + 1 3 4.000000 + + >>> df = pd.DataFrame(np.nan, + ... columns=['a', 'b', 'c', 'd'], + ... index=['x', 'y', 'z']) + >>> df.iloc[0, :] = 1.9 + >>> df.style.set_precision(3) + ... .format({'b': '{:.0f}', 'c': '{:.1f}'.format}, + ... na_rep='HARD', + ... subset=pd.IndexSlice[['y','x'], ['a', 'b', 'c']]) + ... .set_na_rep('SOFT') + a b c d + x 1.900 2 1.9 1.900 + y SOFT HARD HARD SOFT + z SOFT SOFT SOFT SOFT + """ + subset = slice(None) if subset is None else subset + subset = _non_reducing_slice(subset) + data = self.data.loc[subset] + + if not isinstance(formatter, dict): + columns = data.columns + formatter = {col: formatter for col in columns} + else: + columns = formatter.keys() + + for col in columns: + try: + format_func = formatter[col] + except KeyError: + format_func = None + format_func = self._maybe_wrap_formatter(format_func, na_rep=na_rep) + + for row, value in data[[col]].itertuples(): + i, j = self.index.get_loc(row), self.columns.get_loc(col) + self._display_funcs[(i, j)] = format_func + + return self + def set_td_classes(self, classes: DataFrame) -> Styler: """ Add string based CSS class names to data cells that will appear within the From 76a44ae19fff91cb38c371a6856258b4271433bf Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 23:12:31 +0100 Subject: [PATCH 13/14] easy compare --- pandas/io/formats/style.py | 68 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index dd4e3a1ef955a..82d9ef87dd25c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -219,40 +219,6 @@ def _init_tooltips(self): def _default_display_func(self, x): return self._maybe_wrap_formatter(formatter=None)(x) - def _default_formatter(self, x): - if isinstance(x, (float, complex)): - return f"{x:.{self.precision}f}" - return x - - def _maybe_wrap_formatter( - self, - formatter: Optional[Union[Callable, str]] = None, - na_rep: Optional[str] = None, - ) -> Callable: - """ - Allows formatters to be expressed as str, callable or None, where None returns - a default formatting function. wraps with na_rep where it is available. - """ - if isinstance(formatter, str): - func = lambda x: formatter.format(x) - elif callable(formatter): - func = formatter - elif formatter is None: - func = self._default_formatter - else: - raise TypeError( - f"'formatter' expected str or callable, got {type(formatter)}" - ) - - if na_rep is not None: - return lambda x: na_rep if pd.isna(x) else func(x) - else: - return ( - lambda x: self.na_rep - if all((self.na_rep is not None, pd.isna(x))) - else func(x) - ) - def set_tooltips(self, ttips: DataFrame) -> Styler: """ Add string based tooltips that will appear in the `Styler` HTML result. These @@ -1341,6 +1307,40 @@ def hide_columns(self, subset) -> Styler: self.hidden_columns = self.columns.get_indexer_for(hidden_df.columns) return self + def _default_formatter(self, x): + if isinstance(x, (float, complex)): + return f"{x:.{self.precision}f}" + return x + + def _maybe_wrap_formatter( + self, + formatter: Optional[Union[Callable, str]] = None, + na_rep: Optional[str] = None, + ) -> Callable: + """ + Allows formatters to be expressed as str, callable or None, where None returns + a default formatting function. wraps with na_rep where it is available. + """ + if isinstance(formatter, str): + func = lambda x: formatter.format(x) + elif callable(formatter): + func = formatter + elif formatter is None: + func = self._default_formatter + else: + raise TypeError( + f"'formatter' expected str or callable, got {type(formatter)}" + ) + + if na_rep is not None: + return lambda x: na_rep if pd.isna(x) else func(x) + else: + return ( + lambda x: self.na_rep + if all((self.na_rep is not None, pd.isna(x))) + else func(x) + ) + # ----------------------------------------------------------------------- # A collection of "builtin" styles # ----------------------------------------------------------------------- From e3d4daf41cb2082a1c863436115a94fe37e5612d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 25 Feb 2021 23:16:36 +0100 Subject: [PATCH 14/14] easy compare --- pandas/tests/io/formats/style/test_style.py | 166 ++++++++++---------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 9a7bce37cefa1..f534ecddb9cfc 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -565,16 +565,6 @@ def test_format_non_numeric_na(self): assert ctx["body"][1][1]["display_value"] == "-" assert ctx["body"][1][2]["display_value"] == "-" - def test_display_format(self): - df = DataFrame(np.random.random(size=(2, 2))) - ctx = df.style.format("{:0.1f}")._translate() - - assert all(["display_value" in c for c in row] for row in ctx["body"]) - assert all( - [len(c["display_value"]) <= 3 for c in row[1:]] for row in ctx["body"] - ) - assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 - def test_display_format_subset_interaction(self): # GH40032 # test subset and formatter interaction in conjunction with other methods @@ -610,79 +600,6 @@ def test_display_format_subset_interaction(self): with pytest.raises(KeyError, match="are in the [columns]"): df.style.format({"a": "{:.0f}"}, subset=["b"]) - @pytest.mark.parametrize("formatter", [5, True, [2.0]]) - def test_display_format_raises(self, formatter): - with pytest.raises(TypeError, match="expected str or callable"): - self.df.style.format(formatter) - - def test_display_set_precision(self): - # Issue #13257 - df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"]) - s = Styler(df) - - ctx = s.set_precision(1)._translate() - - assert s.precision == 1 - assert ctx["body"][0][1]["display_value"] == "1.0" - assert ctx["body"][0][2]["display_value"] == "2.0" - assert ctx["body"][1][1]["display_value"] == "3.2" - assert ctx["body"][1][2]["display_value"] == "4.6" - - ctx = s.set_precision(2)._translate() - assert s.precision == 2 - assert ctx["body"][0][1]["display_value"] == "1.00" - assert ctx["body"][0][2]["display_value"] == "2.01" - assert ctx["body"][1][1]["display_value"] == "3.21" - assert ctx["body"][1][2]["display_value"] == "4.57" - - ctx = s.set_precision(3)._translate() - assert s.precision == 3 - assert ctx["body"][0][1]["display_value"] == "1.000" - assert ctx["body"][0][2]["display_value"] == "2.009" - assert ctx["body"][1][1]["display_value"] == "3.212" - assert ctx["body"][1][2]["display_value"] == "4.566" - - def test_format_subset(self): - df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) - ctx = df.style.format( - {"a": "{:0.1f}", "b": "{0:.2%}"}, subset=pd.IndexSlice[0, :] - )._translate() - expected = "0.1" - raw_11 = "1.123400" - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == raw_11 - assert ctx["body"][0][2]["display_value"] == "12.34%" - - ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, :])._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == raw_11 - - ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice["a"])._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][0][2]["display_value"] == "0.123400" - - ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, "a"])._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == raw_11 - - ctx = df.style.format( - "{:0.1f}", subset=pd.IndexSlice[[0, 1], ["a"]] - )._translate() - assert ctx["body"][0][1]["display_value"] == expected - assert ctx["body"][1][1]["display_value"] == "1.1" - assert ctx["body"][0][2]["display_value"] == "0.123400" - assert ctx["body"][1][2]["display_value"] == raw_11 - - def test_display_dict(self): - df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) - ctx = df.style.format({"a": "{:0.1f}", "b": "{0:.2%}"})._translate() - assert ctx["body"][0][1]["display_value"] == "0.1" - assert ctx["body"][0][2]["display_value"] == "12.34%" - df["c"] = ["aaa", "bbb"] - ctx = df.style.format({"a": "{:0.1f}", "c": str.upper})._translate() - assert ctx["body"][0][1]["display_value"] == "0.1" - assert ctx["body"][0][3]["display_value"] == "AAA" - def test_nonunique_raises(self): df = DataFrame([[1, 2]], columns=["A", "A"]) msg = "style is not supported for non-unique indices." @@ -798,6 +715,89 @@ def test_export(self): assert style1._todo == style2._todo style2.render() + def test_display_format(self): + df = DataFrame(np.random.random(size=(2, 2))) + ctx = df.style.format("{:0.1f}")._translate() + + assert all(["display_value" in c for c in row] for row in ctx["body"]) + assert all( + [len(c["display_value"]) <= 3 for c in row[1:]] for row in ctx["body"] + ) + assert len(ctx["body"][0][1]["display_value"].lstrip("-")) <= 3 + + @pytest.mark.parametrize("formatter", [5, True, [2.0]]) + def test_display_format_raises(self, formatter): + with pytest.raises(TypeError, match="expected str or callable"): + self.df.style.format(formatter) + + def test_display_set_precision(self): + # Issue #13257 + df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"]) + s = Styler(df) + + ctx = s.set_precision(1)._translate() + + assert s.precision == 1 + assert ctx["body"][0][1]["display_value"] == "1.0" + assert ctx["body"][0][2]["display_value"] == "2.0" + assert ctx["body"][1][1]["display_value"] == "3.2" + assert ctx["body"][1][2]["display_value"] == "4.6" + + ctx = s.set_precision(2)._translate() + assert s.precision == 2 + assert ctx["body"][0][1]["display_value"] == "1.00" + assert ctx["body"][0][2]["display_value"] == "2.01" + assert ctx["body"][1][1]["display_value"] == "3.21" + assert ctx["body"][1][2]["display_value"] == "4.57" + + ctx = s.set_precision(3)._translate() + assert s.precision == 3 + assert ctx["body"][0][1]["display_value"] == "1.000" + assert ctx["body"][0][2]["display_value"] == "2.009" + assert ctx["body"][1][1]["display_value"] == "3.212" + assert ctx["body"][1][2]["display_value"] == "4.566" + + def test_format_subset(self): + df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) + ctx = df.style.format( + {"a": "{:0.1f}", "b": "{0:.2%}"}, subset=pd.IndexSlice[0, :] + )._translate() + expected = "0.1" + raw_11 = "1.123400" + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == raw_11 + assert ctx["body"][0][2]["display_value"] == "12.34%" + + ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, :])._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == raw_11 + + ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice["a"])._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][0][2]["display_value"] == "0.123400" + + ctx = df.style.format("{:0.1f}", subset=pd.IndexSlice[0, "a"])._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == raw_11 + + ctx = df.style.format( + "{:0.1f}", subset=pd.IndexSlice[[0, 1], ["a"]] + )._translate() + assert ctx["body"][0][1]["display_value"] == expected + assert ctx["body"][1][1]["display_value"] == "1.1" + assert ctx["body"][0][2]["display_value"] == "0.123400" + assert ctx["body"][1][2]["display_value"] == raw_11 + + def test_display_dict(self): + df = DataFrame([[0.1234, 0.1234], [1.1234, 1.1234]], columns=["a", "b"]) + ctx = df.style.format({"a": "{:0.1f}", "b": "{0:.2%}"})._translate() + assert ctx["body"][0][1]["display_value"] == "0.1" + assert ctx["body"][0][2]["display_value"] == "12.34%" + df["c"] = ["aaa", "bbb"] + ctx = df.style.format({"a": "{:0.1f}", "c": str.upper})._translate() + assert ctx["body"][0][1]["display_value"] == "0.1" + assert ctx["body"][0][3]["display_value"] == "AAA" + def test_bad_apply_shape(self): df = DataFrame([[1, 2], [3, 4]]) msg = "returned the wrong shape"