Skip to content

ENH: styler.format options #43256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 31, 2021
Merged
15 changes: 12 additions & 3 deletions doc/source/user_guide/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
======================================= ============ ==================================


Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
65 changes: 65 additions & 0 deletions pandas/core/config_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
is_int,
is_nonnegative_int,
is_one_of_factory,
is_str,
is_text,
)

Expand Down Expand Up @@ -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)

Expand All @@ -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]),
)
39 changes: 30 additions & 9 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from pandas.io.formats.style_render import (
CSSProperties,
CSSStyles,
ExtFormatter,
StylerRenderer,
Subset,
Tooltips,
Expand Down Expand Up @@ -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
Expand All @@ -103,7 +107,8 @@ class Styler(StylerRenderer):
number and ``<num_col>`` 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

Expand All @@ -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

Expand All @@ -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
----------
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
25 changes: 17 additions & 8 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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``
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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)
)
Expand Down
42 changes: 42 additions & 0 deletions pandas/tests/io/formats/style/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
IndexSlice,
NaT,
Timestamp,
option_context,
)

pytest.importorskip("jinja2")
Expand Down Expand Up @@ -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"] == "&amp;&lt;"
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"
8 changes: 4 additions & 4 deletions pandas/tests/io/formats/style/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down