diff --git a/doc/source/user_guide/options.rst b/doc/source/user_guide/options.rst index 62a347acdaa34..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 @@ -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. ======================================= ============ ================================== diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index be647e344f270..2b11d9f01d88d 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. diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py index 27b898782fbef..89f3bc76d2905 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_instance_factory([type(None), str]), + ) + + cf.register_option( + "format.na_rep", + None, + styler_na_rep, + validator=is_instance_factory([type(None), 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 31e10534d853a..7c3d7fe57b7b1 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, @@ -85,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 @@ -103,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 @@ -113,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 @@ -128,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 ---------- @@ -184,9 +196,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, @@ -196,13 +209,21 @@ def __init__( table_attributes=table_attributes, caption=caption, cell_ids=cell_ids, + precision=precision, ) # 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..69ab134a38e29 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("display.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: """ @@ -686,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`` @@ -954,11 +965,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 +1031,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)