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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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" From 7bc66eb9b624b0594c3f8799252cb7c353762f3a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 20:48:15 +0100 Subject: [PATCH 15/38] deprecate set_na_rep() and set_precision() in favour of format() --- pandas/io/formats/style.py | 110 +++++++++++--------- pandas/tests/io/formats/style/test_style.py | 56 +--------- 2 files changed, 61 insertions(+), 105 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 82d9ef87dd25c..95986c0da116e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -20,6 +20,7 @@ Union, ) from uuid import uuid4 +import warnings import numpy as np @@ -173,9 +174,7 @@ def __init__( self.data: DataFrame = data self.index: pd.Index = data.index self.columns: pd.Index = data.columns - if precision is None: - precision = get_option("display.precision") - self.precision = precision + self.table_styles = table_styles if not isinstance(uuid_len, int) or not uuid_len >= 0: raise TypeError("``uuid_len`` must be an integer in range [0, 32].") @@ -184,7 +183,6 @@ def __init__( self.caption = caption self.table_attributes = table_attributes self.cell_ids = cell_ids - self.na_rep = na_rep # assign additional default vars self.hidden_index: bool = False @@ -195,7 +193,9 @@ def __init__( self.tooltips: Optional[_Tooltips] = None self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function Tuple[int, int], Callable[[Any], str] - ] = defaultdict(lambda: self._default_display_func) + ] = defaultdict(lambda: partial(_default_formatter, precision=None)) + if any((precision is not None, na_rep is not None)): + self.format(precision=precision, na_rep=na_rep) def _repr_html_(self) -> str: """ @@ -216,9 +216,6 @@ def _init_tooltips(self): if self.tooltips is None: self.tooltips = _Tooltips() - def _default_display_func(self, x): - return self._maybe_wrap_formatter(formatter=None)(x) - def set_tooltips(self, ttips: DataFrame) -> Styler: """ Add string based tooltips that will appear in the `Styler` HTML result. These @@ -374,7 +371,6 @@ def _translate(self): table_styles = self.table_styles or [] caption = self.caption ctx = self.ctx - precision = self.precision hidden_index = self.hidden_index hidden_columns = self.hidden_columns uuid = self.uuid @@ -554,7 +550,6 @@ def _translate(self): "cellstyle": cellstyle, "body": body, "uuid": uuid, - "precision": precision, "table_styles": table_styles, "caption": caption, "table_attributes": table_attr, @@ -571,6 +566,7 @@ def format( ] = None, subset=None, na_rep: Optional[str] = None, + precision: Optional[int] = None, ) -> Styler: """ Format the text display value of cells. @@ -651,7 +647,9 @@ def format( format_func = formatter[col] except KeyError: format_func = None - format_func = self._maybe_wrap_formatter(format_func, na_rep=na_rep) + format_func = _maybe_wrap_formatter( + format_func, na_rep=na_rep, precision=precision + ) for row, value in data[[col]].itertuples(): i, j = self.index.get_loc(row), self.columns.get_loc(col) @@ -757,7 +755,6 @@ def render(self, **kwargs) -> str: * cellstyle * body * uuid - * precision * table_styles * caption * table_attributes @@ -792,11 +789,9 @@ def _update_ctx(self, attrs: DataFrame) -> None: def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( self.data, - precision=self.precision, caption=self.caption, uuid=self.uuid, table_styles=self.table_styles, - na_rep=self.na_rep, ) if deepcopy: styler.ctx = copy.deepcopy(self.ctx) @@ -1053,9 +1048,15 @@ def set_precision(self, precision: int) -> Styler: Returns ------- self : Styler + + Notes + ----- + This method is deprecated see `Styler.format`. """ - self.precision = precision - return self + warnings.warn( + "this method is deprecated in favour of `Styler.format`", DeprecationWarning + ) + return self.format(precision=precision) def set_table_attributes(self, attributes: str) -> Styler: """ @@ -1273,9 +1274,15 @@ def set_na_rep(self, na_rep: str) -> Styler: Returns ------- self : Styler + + Notes + ----- + This method is deprecated. See `Styler.format()` """ - self.na_rep = na_rep - return self + warnings.warn( + "This method is deprecated in favour of `Styler.format`", DeprecationWarning + ) + return self.format(na_rep=na_rep) def hide_index(self) -> Styler: """ @@ -1307,40 +1314,6 @@ 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 # ----------------------------------------------------------------------- @@ -2143,3 +2116,36 @@ def pred(part) -> bool: else: slice_ = [part if pred(part) else [part] for part in slice_] return tuple(slice_) + + +def _default_formatter(x: Any, precision: Optional[int] = None): + if precision is None: + precision = get_option("display.precision") + if isinstance(x, (float, complex)): + return f"{x:.{precision}f}" + return x + + +def _maybe_wrap_formatter( + formatter: Optional[Union[Callable, str]] = None, + na_rep: Optional[str] = None, + precision: Optional[int] = None, +) -> Callable: + """ + Allows formatters to be expressed as str, callable or None, where None returns + a default formatting function. wraps with na_rep, and precision where they are + available. + """ + if isinstance(formatter, str): + func = lambda x: formatter.format(x) + elif callable(formatter): + func = formatter + elif formatter is None: + func = partial(_default_formatter, precision=precision) + 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 func diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index f534ecddb9cfc..d4de0308c775d 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -565,41 +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_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"] == "X" - 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" - - with pytest.raises(KeyError, match="are in the [columns]"): - df.style.format({"a": "{:.0f}"}, subset=["b"]) - def test_nonunique_raises(self): df = DataFrame([[1, 2]], columns=["A", "A"]) msg = "style is not supported for non-unique indices." @@ -675,17 +640,6 @@ def test_table_attributes(self): result = self.df.style.set_table_attributes(attributes).render() assert 'class="foo" data-bar' in result - def test_precision(self): - with pd.option_context("display.precision", 10): - s = Styler(self.df) - assert s.precision == 10 - s = Styler(self.df, precision=2) - assert s.precision == 2 - - s2 = s.set_precision(4) - assert s is s2 - assert s.precision == 4 - def test_apply_none(self): def f(x): return DataFrame( @@ -735,23 +689,19 @@ def test_display_set_precision(self): 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 + ctx = s.format(precision=1)._translate() 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 + ctx = s.format(precision=2)._translate() 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 + ctx = s.format(precision=3)._translate() 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" From 518405c2d29c75d5e7d078533c5ca3f2fe06346f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 21:24:47 +0100 Subject: [PATCH 16/38] edit --- pandas/tests/io/formats/style/test_style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index d4de0308c775d..5c748c234f0c3 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -707,7 +707,7 @@ def test_display_set_precision(self): assert ctx["body"][1][1]["display_value"] == "3.212" assert ctx["body"][1][2]["display_value"] == "4.566" - def test_format_subset(self): + 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, :] From f3b38ae7a124e36d7708f25d23dbb877df2cf991 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 21:27:02 +0100 Subject: [PATCH 17/38] edit --- pandas/io/formats/style.py | 66 +++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 95986c0da116e..5df388421f528 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2055,6 +2055,39 @@ def _get_level_lengths(index, hidden_elements=None): return non_zero_lengths +def _default_formatter(x: Any, precision: Optional[int] = None): + if precision is None: + precision = get_option("display.precision") + if isinstance(x, (float, complex)): + return f"{x:.{precision}f}" + return x + + +def _maybe_wrap_formatter( + formatter: Optional[Union[Callable, str]] = None, + na_rep: Optional[str] = None, + precision: Optional[int] = None, +) -> Callable: + """ + Allows formatters to be expressed as str, callable or None, where None returns + a default formatting function. wraps with na_rep, and precision where they are + available. + """ + if isinstance(formatter, str): + func = lambda x: formatter.format(x) + elif callable(formatter): + func = formatter + elif formatter is None: + func = partial(_default_formatter, precision=precision) + 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 func + + def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: """ Convert css-string to sequence of tuples format if needed. @@ -2116,36 +2149,3 @@ def pred(part) -> bool: else: slice_ = [part if pred(part) else [part] for part in slice_] return tuple(slice_) - - -def _default_formatter(x: Any, precision: Optional[int] = None): - if precision is None: - precision = get_option("display.precision") - if isinstance(x, (float, complex)): - return f"{x:.{precision}f}" - return x - - -def _maybe_wrap_formatter( - formatter: Optional[Union[Callable, str]] = None, - na_rep: Optional[str] = None, - precision: Optional[int] = None, -) -> Callable: - """ - Allows formatters to be expressed as str, callable or None, where None returns - a default formatting function. wraps with na_rep, and precision where they are - available. - """ - if isinstance(formatter, str): - func = lambda x: formatter.format(x) - elif callable(formatter): - func = formatter - elif formatter is None: - func = partial(_default_formatter, precision=precision) - 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 func From ae88c558be202646eff0d8ec31efbc912fe7270a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 22:49:12 +0100 Subject: [PATCH 18/38] edit --- pandas/io/formats/style.py | 41 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 5df388421f528..46f2830ce4870 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -582,19 +582,20 @@ def format( ``formatter`` is applied to. na_rep : str, optional Representation for missing values. If ``None``, will revert to using - ``Styler.na_rep`` + If ``na_rep`` is None, no special formatting is applied. .. versionadded:: 1.0.0 + precision : int, optional + Floating point precision to use for display purposes, if not specifying a + ``formatter``. + + .. versionadded:: 1.3.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 @@ -606,31 +607,9 @@ def format( 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 + pandas display precision unless using the ``precision`` argument here. The + default formatter does not adjust the representation of missing values unless + the ``na_rep`` argument is used. """ subset = slice(None) if subset is None else subset subset = _non_reducing_slice(subset) From 859125f7ef82c21277d1aaebe0e56bf95600f224 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 22:54:14 +0100 Subject: [PATCH 19/38] edit --- 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 46f2830ce4870..d28300e8c9686 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -581,7 +581,7 @@ def format( 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 + Representation for missing values. If ``na_rep`` is None, no special formatting is applied. .. versionadded:: 1.0.0 From c34083e72d89441452edb0e280fa02cd21f1e22c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 23:03:03 +0100 Subject: [PATCH 20/38] docs --- pandas/io/formats/style.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d28300e8c9686..da58af634ac37 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -601,10 +601,11 @@ 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)`` - 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 ``subset`` argument defines which region to apply the formatting function + to. If the ``formatter`` argument is given in dict form but does not include + all columns within the subset then these columns will have the default formatter + applied. Any columns in the ``formatter`` dict excluded from the ``subset`` will + raise a ``KeyError``. The default formatter currently expresses floats and complex numbers with the pandas display precision unless using the ``precision`` argument here. The @@ -615,11 +616,9 @@ def format( subset = _non_reducing_slice(subset) data = self.data.loc[subset] + columns = data.columns if not isinstance(formatter, dict): - columns = data.columns formatter = {col: formatter for col in columns} - else: - columns = formatter.keys() for col in columns: try: From 60a25ffe93118e1cf824579cf0b56d2c71a3cf58 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 28 Feb 2021 23:04:28 +0100 Subject: [PATCH 21/38] docs --- 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 da58af634ac37..f5364c71f8438 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -575,7 +575,7 @@ def format( ---------- 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, + formatter is used. If ``dict``, keys should correspond to column names, and values should be string or callable. subset : IndexSlice An argument to ``DataFrame.loc`` that restricts which elements From 7a64724c4a2172783ee926d281d2312b8d23249d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 06:57:11 +0100 Subject: [PATCH 22/38] same var names --- 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 f5364c71f8438..66504ccf074d3 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2052,18 +2052,18 @@ def _maybe_wrap_formatter( available. """ if isinstance(formatter, str): - func = lambda x: formatter.format(x) + formatter_func = lambda x: formatter.format(x) elif callable(formatter): - func = formatter + formatter_func = formatter elif formatter is None: - func = partial(_default_formatter, precision=precision) + formatter_func = partial(_default_formatter, precision=precision) 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) + if na_rep is None: + return formatter_func else: - return func + return lambda x: na_rep if pd.isna(x) else formatter_func(x) def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: From 4c68cc9d78fd155aea56dbbbac1279d6080ec8b1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 06:59:13 +0100 Subject: [PATCH 23/38] same var names --- pandas/io/formats/style.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 66504ccf074d3..cba74a44e50fb 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -174,7 +174,6 @@ def __init__( self.data: DataFrame = data self.index: pd.Index = data.index self.columns: pd.Index = data.columns - self.table_styles = table_styles if not isinstance(uuid_len, int) or not uuid_len >= 0: raise TypeError("``uuid_len`` must be an integer in range [0, 32].") From c385a626b1683be1a32d6b8a30d507aa4c7c17fd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 07:02:38 +0100 Subject: [PATCH 24/38] same var names --- 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 cba74a44e50fb..2f02ac8838256 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -586,8 +586,8 @@ def format( .. versionadded:: 1.0.0 precision : int, optional - Floating point precision to use for display purposes, if not specifying a - ``formatter``. + Floating point precision to use for display purposes, if not determined by + the specified ``formatter``. .. versionadded:: 1.3.0 From 61e367a2febb29d9dc397f272aa7ec27ecc740eb Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 08:45:56 +0100 Subject: [PATCH 25/38] doc examples --- pandas/io/formats/style.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2f02ac8838256..2d0442a8e22a1 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -610,6 +610,48 @@ def format( pandas display precision unless using the ``precision`` argument here. The default formatter does not adjust the representation of missing values unless the ``na_rep`` argument is used. + + When using a ``formatter`` string the dtypes must be compatible, otherwise a + `ValueError` will be raised, + + Examples + -------- + Using ``na_rep`` and ``precision`` with the default ``formatter`` + + >>> df = pd.DataFrame([[np.nan, 1.0, 'A'], [2.0, np.nan, 3.0]]) + >>> df.style.format(na_rep='MISS', precision=3) + 0 1 2 + 0 MISS 1.000 A + 1 2.000 MISS 3.000 + + Using a format specification on consistent column dtypes + + >>> df.style.format('{:.2f}', na_rep='MISS', subset=[0,1]) + 0 1 2 + 0 MISS 1.00 A + 1 2.00 MISS 3.000000 + + Using the default ``formatter`` for unspecified columns + + >>> df.style.format({0: '{:.2f}', 1: '£ {:.1f}'}, na_rep='MISS', precision=1) + 0 1 2 + 0 MISS £ 1.0 A + 1 2.00 MISS 3.0 + + Multiple `na_rep` or `precision` specifications under the default ``formatter``. + + >>> df.style.format(na_rep='MISS', precision=1, subset=[0]) + ... .format(na_rep='PASS', precision=2, subset=[1, 2]) + 0 1 2 + 0 MISS 1.00 A + 1 2.0 PASS 3.00 + + Using a callable formatting function + >>> func = lambda s: 'STRING' if isinstance(s, str) else 'FLOAT' + >>> df.style.format({0: '{:.1f}', 2: func}, precision=4, na_rep='MISS') + 0 1 2 + 0 MISS 1.0000 STRING + 1 2.0 MISS FLOAT """ subset = slice(None) if subset is None else subset subset = _non_reducing_slice(subset) From a880754c84ad7555c7c2d661b4550fddd06a0955 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 10:19:59 +0100 Subject: [PATCH 26/38] doc examples --- doc/source/reference/style.rst | 2 -- pandas/io/formats/style.py | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/doc/source/reference/style.rst b/doc/source/reference/style.rst index 3a8d912fa6ffe..79a52b7cdb9d1 100644 --- a/doc/source/reference/style.rst +++ b/doc/source/reference/style.rst @@ -35,7 +35,6 @@ Style application Styler.applymap Styler.where Styler.format - Styler.set_precision Styler.set_td_classes Styler.set_table_styles Styler.set_table_attributes @@ -44,7 +43,6 @@ Style application Styler.set_caption Styler.set_properties Styler.set_uuid - Styler.set_na_rep Styler.clear Styler.pipe diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 334aa990303f5..d8469e4ede852 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -197,6 +197,8 @@ def __init__( self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function Tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=None)) + self.precision = precision # can be removed on set_precision depr cycle + self.na_rep = na_rep # can be removed on set_na_rep depr cycle if any((precision is not None, na_rep is not None)): self.format(precision=precision, na_rep=na_rep) @@ -616,7 +618,7 @@ def format( the ``na_rep`` argument is used. When using a ``formatter`` string the dtypes must be compatible, otherwise a - `ValueError` will be raised, + `ValueError` will be raised. Examples -------- @@ -642,18 +644,20 @@ def format( 0 MISS £ 1.0 A 1 2.00 MISS 3.0 - Multiple `na_rep` or `precision` specifications under the default ``formatter``. + Multiple ``na_rep`` or ``precision`` specifications under the default + ``formatter``. >>> df.style.format(na_rep='MISS', precision=1, subset=[0]) - ... .format(na_rep='PASS', precision=2, subset=[1, 2]) + ... .format(na_rep='PASS', precision=2, subset=[1, 2]) 0 1 2 0 MISS 1.00 A 1 2.0 PASS 3.00 Using a callable formatting function + >>> func = lambda s: 'STRING' if isinstance(s, str) else 'FLOAT' >>> df.style.format({0: '{:.1f}', 2: func}, precision=4, na_rep='MISS') - 0 1 2 + 0 1 2 0 MISS 1.0000 STRING 1 2.0 MISS FLOAT """ @@ -1079,7 +1083,8 @@ def set_precision(self, precision: int) -> Styler: warnings.warn( "this method is deprecated in favour of `Styler.format`", DeprecationWarning ) - return self.format(precision=precision) + self.precision = precision + return self.format(precision=precision, na_rep=self.na_rep) def set_table_attributes(self, attributes: str) -> Styler: """ @@ -1305,7 +1310,8 @@ def set_na_rep(self, na_rep: str) -> Styler: warnings.warn( "This method is deprecated in favour of `Styler.format`", DeprecationWarning ) - return self.format(na_rep=na_rep) + self.na_rep = na_rep + return self.format(na_rep=na_rep, precision=self.precision) def hide_index(self) -> Styler: """ From ed5b13ab47c037be9e326c00be27adffabafe922 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 16:18:37 +0100 Subject: [PATCH 27/38] typing --- pandas/io/formats/style.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index d8469e4ede852..9a9d08880d26e 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -49,6 +49,7 @@ jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") +Formatter = Union[str, Callable, Dict[Any, Optional[Union[str, Callable]]]] CSSPair = Tuple[str, Union[str, int, float]] CSSList = List[CSSPair] CSSProperties = Union[str, CSSList] @@ -566,10 +567,8 @@ def _translate(self): def format( self, - formatter: Optional[ - Union[Dict[Any, Optional[Union[str, Callable]]], str, Callable] - ] = None, - subset=None, + formatter: Optional[Formatter] = None, + subset: Optional[Union[slice, Sequence[Any]]] = None, na_rep: Optional[str] = None, precision: Optional[int] = None, ) -> Styler: From 27fb8d582cb8516d497078bbc75ec78c2ffa97f6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 1 Mar 2021 16:28:58 +0100 Subject: [PATCH 28/38] typing --- pandas/io/formats/style.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9a9d08880d26e..dd994634ddf74 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -49,7 +49,8 @@ jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") -Formatter = Union[str, Callable, Dict[Any, Optional[Union[str, Callable]]]] +BaseFormatter = Union[str, Callable] +ExtFormatter = Union[BaseFormatter, Dict[Any, Optional[BaseFormatter]]] CSSPair = Tuple[str, Union[str, int, float]] CSSList = List[CSSPair] CSSProperties = Union[str, CSSList] @@ -567,7 +568,7 @@ def _translate(self): def format( self, - formatter: Optional[Formatter] = None, + formatter: Optional[ExtFormatter] = None, subset: Optional[Union[slice, Sequence[Any]]] = None, na_rep: Optional[str] = None, precision: Optional[int] = None, @@ -2081,7 +2082,7 @@ def _default_formatter(x: Any, precision: Optional[int] = None): def _maybe_wrap_formatter( - formatter: Optional[Union[Callable, str]] = None, + formatter: Optional[BaseFormatter] = None, na_rep: Optional[str] = None, precision: Optional[int] = None, ) -> Callable: From bf7f9fc021fefda9fc4fdda5010f9df29385e902 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Wed, 3 Mar 2021 14:33:30 +0100 Subject: [PATCH 29/38] req changes --- pandas/io/formats/style.py | 21 ++++++++++++++++++++- pandas/tests/io/formats/style/test_style.py | 8 ++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index dd994634ddf74..076a1d2e6c487 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1068,6 +1068,8 @@ def set_precision(self, precision: int) -> Styler: """ Set the precision used to display values. + .. deprecated:: 1.3.0 + Parameters ---------- precision : int @@ -1295,6 +1297,8 @@ def set_na_rep(self, na_rep: str) -> Styler: .. versionadded:: 1.0.0 + .. deprecated:: 1.3.0 + Parameters ---------- na_rep : str @@ -2073,7 +2077,22 @@ def _get_level_lengths(index, hidden_elements=None): return non_zero_lengths -def _default_formatter(x: Any, precision: Optional[int] = None): +def _default_formatter(x: Any, precision: Optional[int] = None) -> Any: + """ + Format the display of a value + + Parameters + ---------- + x : Any + Input variable to be formatted + precision : Int, optional + Floating point precision used if ``x`` is float or complex. + + Returns + ------- + value : Any + Matches input type, or string if input is float or complex. + """ if precision is None: precision = get_option("display.precision") if isinstance(x, (float, complex)): diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 1fbb9cd0eb6bf..0da2c228def9c 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -653,6 +653,14 @@ def test_table_attributes(self): result = self.df.style.set_table_attributes(attributes).render() assert 'class="foo" data-bar' in result + def test_precision(self): + s = Styler(self.df, precision=2) + assert s.precision == 2 + + s2 = s.set_precision(4) + assert s is s2 + assert s.precision == 4 + def test_apply_none(self): def f(x): return DataFrame( From 892df77a93abfce900c69a00cb1411785070eb3f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Wed, 3 Mar 2021 14:38:33 +0100 Subject: [PATCH 30/38] req changes --- pandas/tests/io/formats/style/test_style.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 0da2c228def9c..cd5dc906a2e19 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -532,15 +532,17 @@ def test_set_na_rep(self): # GH 21527 28358 df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"]) - ctx = df.style.set_na_rep("NA")._translate() + with pytest.deprecated_call(): + ctx = df.style.set_na_rep("NA")._translate() assert ctx["body"][0][1]["display_value"] == "NA" assert ctx["body"][0][2]["display_value"] == "NA" - ctx = ( - df.style.set_na_rep("NA") - .format(None, na_rep="-", subset=["B"]) - ._translate() - ) + with pytest.deprecated_call(): + ctx = ( + df.style.set_na_rep("NA") + .format(None, na_rep="-", subset=["B"]) + ._translate() + ) assert ctx["body"][0][1]["display_value"] == "NA" assert ctx["body"][0][2]["display_value"] == "-" @@ -553,7 +555,8 @@ def test_format_non_numeric_na(self): } ) - ctx = df.style.set_na_rep("NA")._translate() + with pytest.deprecated_call(): + ctx = df.style.set_na_rep("NA")._translate() assert ctx["body"][0][1]["display_value"] == "NA" assert ctx["body"][0][2]["display_value"] == "NA" assert ctx["body"][1][1]["display_value"] == "NA" @@ -657,7 +660,8 @@ def test_precision(self): s = Styler(self.df, precision=2) assert s.precision == 2 - s2 = s.set_precision(4) + with pytest.deprecated_call(): + s2 = s.set_precision(4) assert s is s2 assert s.precision == 4 From b63220bd173f88f06de1073a496fc11b8efeecd2 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Wed, 3 Mar 2021 18:34:21 +0100 Subject: [PATCH 31/38] tests --- pandas/tests/io/formats/style/test_style.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index cd5dc906a2e19..b5217ef845f6c 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -705,11 +705,11 @@ def test_display_format(self): 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): + def test_format_raises(self, formatter): with pytest.raises(TypeError, match="expected str or callable"): self.df.style.format(formatter) - def test_display_set_precision(self): + def test_format_with_precision(self): # Issue #13257 df = DataFrame(data=[[1.0, 2.0090], [3.2121, 4.566]], columns=["a", "b"]) s = Styler(df) @@ -732,7 +732,7 @@ def test_display_set_precision(self): assert ctx["body"][1][1]["display_value"] == "3.212" assert ctx["body"][1][2]["display_value"] == "4.566" - def test_display_subset(self): + 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, :] @@ -763,7 +763,7 @@ def test_display_subset(self): assert ctx["body"][0][2]["display_value"] == "0.123400" assert ctx["body"][1][2]["display_value"] == raw_11 - def test_display_dict(self): + def test_format_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" From fbe8bb2fc16d779f07cad8a632f745f49296d6d1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Wed, 3 Mar 2021 18:42:34 +0100 Subject: [PATCH 32/38] whats new --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 41db72612a66b..bbee550b00473 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -321,6 +321,7 @@ Deprecations - Deprecated :meth:`core.window.ewm.ExponentialMovingWindow.vol` (:issue:`39220`) - Using ``.astype`` to convert between ``datetime64[ns]`` dtype and :class:`DatetimeTZDtype` is deprecated and will raise in a future version, use ``obj.tz_localize`` or ``obj.dt.tz_localize`` instead (:issue:`38622`) - Deprecated casting ``datetime.date`` objects to ``datetime64`` when used as ``fill_value`` in :meth:`DataFrame.unstack`, :meth:`DataFrame.shift`, :meth:`Series.shift`, and :meth:`DataFrame.reindex`, pass ``pd.Timestamp(dateobj)`` instead (:issue:`39767`) +- Deprecated :meth:`Styler.set_na_rep` and :meth:`Styler.set_precision` in favour of :meth:`Styler.format` with ``na_rep`` and ``precision`` as existing and new input arguments respectively (:issue:`40134`) .. --------------------------------------------------------------------------- From 302924ca69898629db6dbd78eec17a79e28600f5 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Thu, 4 Mar 2021 08:48:46 +0100 Subject: [PATCH 33/38] req changes --- pandas/io/formats/style.py | 15 +++++++++++---- pandas/tests/io/formats/style/test_style.py | 8 ++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 076a1d2e6c487..d73e397c840ca 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -201,8 +201,7 @@ def __init__( ] = defaultdict(lambda: partial(_default_formatter, precision=None)) self.precision = precision # can be removed on set_precision depr cycle self.na_rep = na_rep # can be removed on set_na_rep depr cycle - if any((precision is not None, na_rep is not None)): - self.format(precision=precision, na_rep=na_rep) + self.format(formatter=None, precision=precision, na_rep=na_rep) def _repr_html_(self) -> str: """ @@ -661,6 +660,10 @@ def format( 0 MISS 1.0000 STRING 1 2.0 MISS FLOAT """ + if all((formatter is None, subset is None, precision is None, na_rep is None)): + self._display_funcs.clear() + return self # clear the formatter / revert to default and avoid looping + subset = slice(None) if subset is None else subset subset = _non_reducing_slice(subset) data = self.data.loc[subset] @@ -1083,7 +1086,9 @@ def set_precision(self, precision: int) -> Styler: This method is deprecated see `Styler.format`. """ warnings.warn( - "this method is deprecated in favour of `Styler.format`", DeprecationWarning + "this method is deprecated in favour of `Styler.format`", + FutureWarning, + stacklevel=2, ) self.precision = precision return self.format(precision=precision, na_rep=self.na_rep) @@ -1312,7 +1317,9 @@ def set_na_rep(self, na_rep: str) -> Styler: This method is deprecated. See `Styler.format()` """ warnings.warn( - "This method is deprecated in favour of `Styler.format`", DeprecationWarning + "this method is deprecated in favour of `Styler.format`", + FutureWarning, + stacklevel=2, ) self.na_rep = na_rep return self.format(na_rep=na_rep, precision=self.precision) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index b5217ef845f6c..c3599e5bc12e7 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -532,12 +532,12 @@ def test_set_na_rep(self): # GH 21527 28358 df = DataFrame([[None, None], [1.1, 1.2]], columns=["A", "B"]) - with pytest.deprecated_call(): + with tm.assert_produces_warning(FutureWarning): ctx = df.style.set_na_rep("NA")._translate() assert ctx["body"][0][1]["display_value"] == "NA" assert ctx["body"][0][2]["display_value"] == "NA" - with pytest.deprecated_call(): + with tm.assert_produces_warning(FutureWarning): ctx = ( df.style.set_na_rep("NA") .format(None, na_rep="-", subset=["B"]) @@ -555,7 +555,7 @@ def test_format_non_numeric_na(self): } ) - with pytest.deprecated_call(): + with tm.assert_produces_warning(FutureWarning): ctx = df.style.set_na_rep("NA")._translate() assert ctx["body"][0][1]["display_value"] == "NA" assert ctx["body"][0][2]["display_value"] == "NA" @@ -660,7 +660,7 @@ def test_precision(self): s = Styler(self.df, precision=2) assert s.precision == 2 - with pytest.deprecated_call(): + with tm.assert_produces_warning(FutureWarning): s2 = s.set_precision(4) assert s is s2 assert s.precision == 4 From 43b8fa728ea974f4556386786817ec0773ce0930 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Thu, 4 Mar 2021 12:55:01 +0100 Subject: [PATCH 34/38] test clear --- pandas/tests/io/formats/style/test_style.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index c3599e5bc12e7..6c831bebef327 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -568,6 +568,13 @@ def test_format_non_numeric_na(self): assert ctx["body"][1][1]["display_value"] == "-" assert ctx["body"][1][2]["display_value"] == "-" + def test_format_clear(self): + assert (0, 0) not in self.styler._display_funcs # using default + self.styler.format("{:.2f") + assert (0, 0) in self.styler._display_funcs # formatter is specified + self.styler.format() + assert (0, 0) not in self.styler._display_funcs # formatter cleared to default + def test_nonunique_raises(self): df = DataFrame([[1, 2]], columns=["A", "A"]) msg = "style is not supported for non-unique indices." From 2c7d6475574458f254d87023b1aceb46ab9f0f30 Mon Sep 17 00:00:00 2001 From: attack68 <24256554+attack68@users.noreply.github.com> Date: Fri, 5 Mar 2021 17:50:52 +0100 Subject: [PATCH 35/38] Update doc/source/whatsnew/v1.3.0.rst Co-authored-by: Joris Van den Bossche --- doc/source/whatsnew/v1.3.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 12dcd08368c91..00c35a935e9cd 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -363,7 +363,7 @@ Deprecations - Deprecated :meth:`core.window.ewm.ExponentialMovingWindow.vol` (:issue:`39220`) - Using ``.astype`` to convert between ``datetime64[ns]`` dtype and :class:`DatetimeTZDtype` is deprecated and will raise in a future version, use ``obj.tz_localize`` or ``obj.dt.tz_localize`` instead (:issue:`38622`) - Deprecated casting ``datetime.date`` objects to ``datetime64`` when used as ``fill_value`` in :meth:`DataFrame.unstack`, :meth:`DataFrame.shift`, :meth:`Series.shift`, and :meth:`DataFrame.reindex`, pass ``pd.Timestamp(dateobj)`` instead (:issue:`39767`) -- Deprecated :meth:`Styler.set_na_rep` and :meth:`Styler.set_precision` in favour of :meth:`Styler.format` with ``na_rep`` and ``precision`` as existing and new input arguments respectively (:issue:`40134`) +- Deprecated :meth:`.Styler.set_na_rep` and :meth:`.Styler.set_precision` in favour of :meth:`.Styler.format` with ``na_rep`` and ``precision`` as existing and new input arguments respectively (:issue:`40134`) - Deprecated allowing partial failure in :meth:`Series.transform` and :meth:`DataFrame.transform` when ``func`` is list-like or dict-like; will raise if any function fails on a column in a future version (:issue:`40211`) .. --------------------------------------------------------------------------- From aa51fe0a62c3eeb54bb294c3df709ab79c55daca Mon Sep 17 00:00:00 2001 From: attack68 <24256554+attack68@users.noreply.github.com> Date: Fri, 5 Mar 2021 17:51:12 +0100 Subject: [PATCH 36/38] Update pandas/io/formats/style.py Co-authored-by: Joris Van den Bossche --- 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 dffb95144a38c..a09af6e445e14 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1332,7 +1332,7 @@ def set_na_rep(self, na_rep: str) -> Styler: This method is deprecated. See `Styler.format()` """ warnings.warn( - "this method is deprecated in favour of `Styler.format`", + "this method is deprecated in favour of `Styler.format(na_rep=..)`", FutureWarning, stacklevel=2, ) From d26d7616d32be3ea503de08e341407f88b3d8f2f Mon Sep 17 00:00:00 2001 From: attack68 <24256554+attack68@users.noreply.github.com> Date: Fri, 5 Mar 2021 17:51:21 +0100 Subject: [PATCH 37/38] Update pandas/io/formats/style.py Co-authored-by: Joris Van den Bossche --- 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 a09af6e445e14..bed4e9f95c028 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -1101,7 +1101,7 @@ def set_precision(self, precision: int) -> Styler: This method is deprecated see `Styler.format`. """ warnings.warn( - "this method is deprecated in favour of `Styler.format`", + "this method is deprecated in favour of `Styler.format(precision=..)`", FutureWarning, stacklevel=2, ) From 94370d809425f09d8bd87cb07efd4f19943e0dc1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Fri, 5 Mar 2021 18:08:04 +0100 Subject: [PATCH 38/38] format docstring --- pandas/io/formats/style.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index bed4e9f95c028..1ce50d3f905f6 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -578,9 +578,7 @@ def format( 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 correspond to column names, - and values should be string or callable. + Object to define how values are displayed. See notes. subset : IndexSlice An argument to ``DataFrame.loc`` that restricts which elements ``formatter`` is applied to. @@ -602,20 +600,26 @@ def format( 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)`` - - The ``subset`` argument defines which region to apply the formatting function - to. If the ``formatter`` argument is given in dict form but does not include - all columns within the subset then these columns will have the default formatter - applied. Any columns in the ``formatter`` dict excluded from the ``subset`` will - raise a ``KeyError``. + This method assigns a formatting function, ``formatter``, to each cell in the + DataFrame. If ``formatter`` is ``None``, then the default formatter is used. + If a callable then that function should take a data value as input and return + a displayable representation, such as a string. If ``formatter`` is + given as a string this is assumed to be a valid Python format specification + and is wrapped to a callable as ``string.format(x)``. If a ``dict`` is given, + keys should correspond to column names, and values should be string or + callable, as above. The default formatter currently expresses floats and complex numbers with the pandas display precision unless using the ``precision`` argument here. The default formatter does not adjust the representation of missing values unless the ``na_rep`` argument is used. + The ``subset`` argument defines which region to apply the formatting function + to. If the ``formatter`` argument is given in dict form but does not include + all columns within the subset then these columns will have the default formatter + applied. Any columns in the formatter dict excluded from the subset will + raise a ``KeyError``. + When using a ``formatter`` string the dtypes must be compatible, otherwise a `ValueError` will be raised.