From db54221be3511191241b6f575342f1927740d28b Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 27 Aug 2021 19:18:49 +0200 Subject: [PATCH 1/8] add options --- pandas/core/config_init.py | 65 +++++++++++++++++++++++++++++++ pandas/io/formats/style.py | 13 ++++++- pandas/io/formats/style_render.py | 2 +- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 27b898782fbef..f127b0644ceb2 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -20,6 +20,7 @@ is_int, is_nonnegative_int, is_one_of_factory, + is_str, is_text, ) @@ -762,6 +763,36 @@ def register_converter_cb(key): trimming will occur over columns, rows or both if needed. """ +styler_precision = """ +: int + The precision for floats and complex numbers. +""" + +styler_decimal = """ +: str + The character representation for the decimal separator for floats and complex. +""" + +styler_thousands = """ +: str, optional + The character representation for thousands separator for floats, int and complex. +""" + +styler_na_rep = """ +: str, optional + The string representation for values identified as missing. +""" + +styler_escape = """ +: str, optional + Whether to escape certain characters according to the given context; html or latex. +""" + +styler_formatter = """ +: str, callable, dict, optional + A formatter object to be used as default within ``Styler.format``. +""" + with cf.config_prefix("styler"): cf.register_option("sparse.index", True, styler_sparse_index_doc, validator=bool) @@ -775,3 +806,37 @@ def register_converter_cb(key): styler_max_elements, validator=is_nonnegative_int, ) + + cf.register_option("format.decimal", ".", styler_decimal, validator=is_str) + + cf.register_option( + "format.precision", 6, styler_precision, validator=is_nonnegative_int + ) + + cf.register_option( + "format.thousands", + None, + styler_thousands, + validator=is_one_of_factory([None, is_str]), + ) + + cf.register_option( + "format.na_rep", + None, + styler_na_rep, + validator=is_one_of_factory([None, is_str]), + ) + + cf.register_option( + "format.escape", + None, + styler_escape, + validator=is_one_of_factory([None, "html", "latex"]), + ) + + cf.register_option( + "format.formatter", + None, + styler_formatter, + validator=is_instance_factory([type(None), dict, callable, str]), + ) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 81bd14629cfd3..34cfc84cfbd0a 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -52,6 +52,7 @@ from pandas.io.formats.style_render import ( CSSProperties, CSSStyles, + ExtFormatter, StylerRenderer, Subset, Tooltips, @@ -184,9 +185,10 @@ def __init__( cell_ids: bool = True, na_rep: str | None = None, uuid_len: int = 5, - decimal: str = ".", + decimal: str | None = None, thousands: str | None = None, escape: str | None = None, + formatter: ExtFormatter | None = None, ): super().__init__( data=data, @@ -199,10 +201,17 @@ def __init__( ) # validate ordered args + thousands = thousands or get_option("styler.format.thousands") + decimal = decimal or get_option("styler.format.decimal") + na_rep = na_rep or get_option("styler.format.na_rep") + escape = escape or get_option("styler.format.escape") + formatter = formatter or get_option("styler.format.formatter") + # precision is handled by superclass as default for performance + 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 self.format( - formatter=None, + formatter=formatter, precision=precision, na_rep=na_rep, escape=escape, diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 4f1e98225aafe..4f92468221c82 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -107,7 +107,7 @@ def __init__( self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str) self._todo: list[tuple[Callable, tuple, dict]] = [] self.tooltips: Tooltips | None = None - def_precision = get_option("display.precision") + def_precision = get_option("styler.format.precision") self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function tuple[int, int], Callable[[Any], str] ] = defaultdict(lambda: partial(_default_formatter, precision=def_precision)) From d2dc5f228aa6adb8ea2b7010bcb2953c4bb55868 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 27 Aug 2021 19:27:31 +0200 Subject: [PATCH 2/8] add docs --- pandas/io/formats/style.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 34cfc84cfbd0a..62ce1a8543421 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -86,8 +86,11 @@ class Styler(StylerRenderer): ---------- data : Series or DataFrame Data to be styled - either a Series or DataFrame. - precision : int - Precision to round floats to, defaults to pd.options.display.precision. + precision : int, optional + Precision to round floats to. If not given defaults to + ``pandas.options.styler.format.precision``. + + .. versionchanged:: 1.4.0 table_styles : list-like, default None List of {selector: (attr, value)} dicts; see Notes. uuid : str, default None @@ -104,7 +107,8 @@ class Styler(StylerRenderer): number and ```` is the column number. na_rep : str, optional Representation for missing values. - If ``na_rep`` is None, no special formatting is applied. + If ``na_rep`` is None, no special formatting is applied, and falls back to + ``pandas.options.styler.format.na_rep``. .. versionadded:: 1.0.0 @@ -114,13 +118,15 @@ class Styler(StylerRenderer): .. versionadded:: 1.2.0 - decimal : str, default "." - Character used as decimal separator for floats, complex and integers + decimal : str, optional + Character used as decimal separator for floats, complex and integers. If not + given uses ``pandas.options.styler.format.decimal``. .. versionadded:: 1.3.0 thousands : str, optional, default None - Character used as thousands separator for floats, complex and integers + Character used as thousands separator for floats, complex and integers. If not + given uses ``pandas.options.styler.format.thousands``. .. versionadded:: 1.3.0 @@ -129,9 +135,14 @@ class Styler(StylerRenderer): in cell display string with HTML-safe sequences. Use 'latex' to replace the characters ``&``, ``%``, ``$``, ``#``, ``_``, ``{``, ``}``, ``~``, ``^``, and ``\`` in the cell display string with - LaTeX-safe sequences. + LaTeX-safe sequences. If not given uses ``pandas.options.styler.format.escape`` .. versionadded:: 1.3.0 + formatter : str, callable, dict, optional + Object to define how values are displayed. See ``Styler.format``. If not given + uses ``pandas.options.styler.format.formatter``. + + .. versionadded:: 1.4.0 Attributes ---------- From f093cea8756af191e0dc4d4df690c19ae41db2f3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 28 Aug 2021 09:35:28 +0200 Subject: [PATCH 3/8] add tests --- pandas/core/config_init.py | 4 +- pandas/io/formats/style_render.py | 10 ++--- pandas/tests/io/formats/style/test_format.py | 42 ++++++++++++++++++++ pandas/tests/io/formats/style/test_style.py | 8 ++-- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index f127b0644ceb2..89f3bc76d2905 100644 --- a/pandas/core/config_init.py +++ b/pandas/core/config_init.py @@ -817,14 +817,14 @@ def register_converter_cb(key): "format.thousands", None, styler_thousands, - validator=is_one_of_factory([None, is_str]), + validator=is_instance_factory([type(None), str]), ) cf.register_option( "format.na_rep", None, styler_na_rep, - validator=is_one_of_factory([None, is_str]), + validator=is_instance_factory([type(None), str]), ) cf.register_option( diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 4f92468221c82..591801bfb3c52 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -954,11 +954,9 @@ def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any: Matches input type, or string if input is float or complex or int with sep. """ if isinstance(x, (float, complex)): - if thousands: - return f"{x:,.{precision}f}" - return f"{x:.{precision}f}" - elif isinstance(x, int) and thousands: - return f"{x:,.0f}" + return f"{x:,.{precision}f}" if thousands else f"{x:.{precision}f}" + elif isinstance(x, int): + return f"{x:,.0f}" if thousands else f"{x:.0f}" return x @@ -1022,7 +1020,7 @@ def _maybe_wrap_formatter( elif callable(formatter): func_0 = formatter elif formatter is None: - precision = get_option("display.precision") if precision is None else precision + precision = precision or get_option("styler.format.precision") func_0 = partial( _default_formatter, precision=precision, thousands=(thousands is not None) ) diff --git a/pandas/tests/io/formats/style/test_format.py b/pandas/tests/io/formats/style/test_format.py index 299643028c141..58f18a6959efa 100644 --- a/pandas/tests/io/formats/style/test_format.py +++ b/pandas/tests/io/formats/style/test_format.py @@ -6,6 +6,7 @@ IndexSlice, NaT, Timestamp, + option_context, ) pytest.importorskip("jinja2") @@ -256,3 +257,44 @@ def test_str_escape_error(): _str_escape("text", []) _str_escape(2.00, "bad_escape") # OK since dtype is float + + +def test_format_options(): + df = DataFrame({"int": [2000, 1], "float": [1.009, None], "str": ["&<", "&~"]}) + ctx = df.style._translate(True, True) + + # test option: na_rep + assert ctx["body"][1][2]["display_value"] == "nan" + with option_context("styler.format.na_rep", "MISSING"): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][1][2]["display_value"] == "MISSING" + + # test option: decimal and precision + assert ctx["body"][0][2]["display_value"] == "1.009000" + with option_context("styler.format.decimal", "_"): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][0][2]["display_value"] == "1_009000" + with option_context("styler.format.precision", 2): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][0][2]["display_value"] == "1.01" + + # test option: thousands + assert ctx["body"][0][1]["display_value"] == "2000" + with option_context("styler.format.thousands", "_"): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][0][1]["display_value"] == "2_000" + + # test option: escape + assert ctx["body"][0][3]["display_value"] == "&<" + assert ctx["body"][1][3]["display_value"] == "&~" + with option_context("styler.format.escape", "html"): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][0][3]["display_value"] == "&<" + with option_context("styler.format.escape", "latex"): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][1][3]["display_value"] == "\\&\\textasciitilde " + + # test option: formatter + with option_context("styler.format.formatter", {"int": "{:,.2f}"}): + ctx_with_op = df.style._translate(True, True) + assert ctx_with_op["body"][0][1]["display_value"] == "2,000.00" diff --git a/pandas/tests/io/formats/style/test_style.py b/pandas/tests/io/formats/style/test_style.py index 5022a1eaa2c6e..afa50cf1cf963 100644 --- a/pandas/tests/io/formats/style/test_style.py +++ b/pandas/tests/io/formats/style/test_style.py @@ -1128,9 +1128,9 @@ def test_hide_columns_index_mult_levels(self): assert ctx["body"][0][0]["is_visible"] # data assert ctx["body"][1][2]["is_visible"] - assert ctx["body"][1][2]["display_value"] == 3 + assert ctx["body"][1][2]["display_value"] == "3" assert ctx["body"][1][3]["is_visible"] - assert ctx["body"][1][3]["display_value"] == 4 + assert ctx["body"][1][3]["display_value"] == "4" # hide top column level, which hides both columns ctx = df.style.hide_columns("b")._translate(True, True) @@ -1146,7 +1146,7 @@ def test_hide_columns_index_mult_levels(self): assert not ctx["head"][1][2]["is_visible"] # 0 assert not ctx["body"][1][2]["is_visible"] # 3 assert ctx["body"][1][3]["is_visible"] - assert ctx["body"][1][3]["display_value"] == 4 + assert ctx["body"][1][3]["display_value"] == "4" # hide second column and index ctx = df.style.hide_columns([("b", 1)]).hide_index()._translate(True, True) @@ -1157,7 +1157,7 @@ def test_hide_columns_index_mult_levels(self): assert not ctx["head"][1][2]["is_visible"] # 1 assert not ctx["body"][1][3]["is_visible"] # 4 assert ctx["body"][1][2]["is_visible"] - assert ctx["body"][1][2]["display_value"] == 3 + assert ctx["body"][1][2]["display_value"] == "3" # hide top row level, which hides both rows ctx = df.style.hide_index("a")._translate(True, True) From c3d386e35cfed7780b2f0ccfb6dcd25f25eadf84 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 28 Aug 2021 12:32:15 +0200 Subject: [PATCH 4/8] better control precision --- pandas/io/formats/style.py | 1 + pandas/io/formats/style_render.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 62ce1a8543421..2e5d5e3d931f9 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -209,6 +209,7 @@ def __init__( table_attributes=table_attributes, caption=caption, cell_ids=cell_ids, + precision=precision, ) # validate ordered args diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index 591801bfb3c52..a7fc2d0c6b5b9 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -77,6 +77,7 @@ def __init__( table_attributes: str | None = None, caption: str | tuple | None = None, cell_ids: bool = True, + precision: int | None = None, ): # validate ordered args @@ -107,10 +108,10 @@ def __init__( self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str) self._todo: list[tuple[Callable, tuple, dict]] = [] self.tooltips: Tooltips | None = None - def_precision = get_option("styler.format.precision") + precision = precision or get_option("styler.format.precision") self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function tuple[int, int], Callable[[Any], str] - ] = defaultdict(lambda: partial(_default_formatter, precision=def_precision)) + ] = defaultdict(lambda: partial(_default_formatter, precision=precision)) def _render_html(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str: """ From 0f7703794cf28b2eac68614d3705cf841a513ecd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 28 Aug 2021 12:47:00 +0200 Subject: [PATCH 5/8] add instruction to styler.format docstring --- pandas/io/formats/style_render.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pandas/io/formats/style_render.py b/pandas/io/formats/style_render.py index a7fc2d0c6b5b9..69ab134a38e29 100644 --- a/pandas/io/formats/style_render.py +++ b/pandas/io/formats/style_render.py @@ -687,6 +687,16 @@ def format( When using a ``formatter`` string the dtypes must be compatible, otherwise a `ValueError` will be raised. + When instantiating a Styler, default formatting can be applied be setting the + ``pandas.options``: + + - ``styler.format.formatter``: default None. + - ``styler.format.na_rep``: default None. + - ``styler.format.precision``: default 6. + - ``styler.format.decimal``: default ".". + - ``styler.format.thousands``: default None. + - ``styler.format.escape``: default None. + Examples -------- Using ``na_rep`` and ``precision`` with the default ``formatter`` From 0ce057b50abdff4c6b16dde2872f9ef5497dcec9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 28 Aug 2021 12:56:41 +0200 Subject: [PATCH 6/8] docstring options.rst --- doc/source/user_guide/options.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/source/user_guide/options.rst b/doc/source/user_guide/options.rst index 62a347acdaa34..1911b3fdf23da 100644 --- a/doc/source/user_guide/options.rst +++ b/doc/source/user_guide/options.rst @@ -489,6 +489,15 @@ styler.sparse.columns True "Sparsify" MultiIndex displ in Styler output. styler.render.max_elements 262144 Maximum number of datapoints that Styler will render trimming either rows, columns or both to fit. +styler.format.formatter None Object to specify formatting functions to ``Styler.format``. +styler.format.na_rep None String representation for missing data. +styler.format.precision 6 Precision to display floating point and complex numbers. +styler.format.decimal . String representation for decimal point separator for floating + point and complex numbers. +styler.format.thousands None String representation for thousands separator for + integers, and floating point and complex numbers. +styler.format.escape None Whether to escape "html" or "latex" special + characters in the display representation. ======================================= ============ ================================== From e9a36e573276af2ef92c892a0446d80b3eb96ac3 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 28 Aug 2021 13:03:51 +0200 Subject: [PATCH 7/8] whats new --- doc/source/whatsnew/v1.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index fc488504f1fdf..cc1b9f4efae4c 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -75,6 +75,7 @@ Styler - :meth:`.Styler.to_latex` introduces keyword argument ``environment``, which also allows a specific "longtable" entry through a separate jinja2 template (:issue:`41866`). - :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index`` and ``sparse_columns`` (:issue:`41946`) - Keyword argument ``level`` is added to :meth:`.Styler.hide_index` and :meth:`.Styler.hide_columns` for optionally controlling hidden levels in a MultiIndex (:issue:`25475`) + - Global options have been extended to configure default ``Styler`` properties including formatting options (:issue:`41395`) There are also bug fixes and deprecations listed below. From 1ffae69f3ff09f6b4ec13a76e0f5400663423dfc Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 28 Aug 2021 19:37:55 +0200 Subject: [PATCH 8/8] fix options doc for "display.precision" --- doc/source/user_guide/options.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/user_guide/options.rst b/doc/source/user_guide/options.rst index 1911b3fdf23da..41e0b754cfa81 100644 --- a/doc/source/user_guide/options.rst +++ b/doc/source/user_guide/options.rst @@ -138,7 +138,7 @@ More information can be found in the `IPython documentation import pandas as pd pd.set_option("display.max_rows", 999) - pd.set_option("precision", 5) + pd.set_option("display.precision", 5) .. _options.frequently_used: @@ -253,9 +253,9 @@ This is only a suggestion. .. ipython:: python df = pd.DataFrame(np.random.randn(5, 5)) - pd.set_option("precision", 7) + pd.set_option("display.precision", 7) df - pd.set_option("precision", 4) + pd.set_option("display.precision", 4) df ``display.chop_threshold`` sets at what level pandas rounds to zero when