Skip to content

Commit 08ac29a

Browse files
authored
ENH: styler.format options (#43256)
1 parent 344c691 commit 08ac29a

File tree

7 files changed

+171
-24
lines changed

7 files changed

+171
-24
lines changed

doc/source/user_guide/options.rst

+12-3
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ More information can be found in the `IPython documentation
138138
import pandas as pd
139139
140140
pd.set_option("display.max_rows", 999)
141-
pd.set_option("precision", 5)
141+
pd.set_option("display.precision", 5)
142142
143143
.. _options.frequently_used:
144144

@@ -253,9 +253,9 @@ This is only a suggestion.
253253
.. ipython:: python
254254
255255
df = pd.DataFrame(np.random.randn(5, 5))
256-
pd.set_option("precision", 7)
256+
pd.set_option("display.precision", 7)
257257
df
258-
pd.set_option("precision", 4)
258+
pd.set_option("display.precision", 4)
259259
df
260260
261261
``display.chop_threshold`` sets at what level pandas rounds to zero when
@@ -489,6 +489,15 @@ styler.sparse.columns True "Sparsify" MultiIndex displ
489489
in Styler output.
490490
styler.render.max_elements 262144 Maximum number of datapoints that Styler will render
491491
trimming either rows, columns or both to fit.
492+
styler.format.formatter None Object to specify formatting functions to ``Styler.format``.
493+
styler.format.na_rep None String representation for missing data.
494+
styler.format.precision 6 Precision to display floating point and complex numbers.
495+
styler.format.decimal . String representation for decimal point separator for floating
496+
point and complex numbers.
497+
styler.format.thousands None String representation for thousands separator for
498+
integers, and floating point and complex numbers.
499+
styler.format.escape None Whether to escape "html" or "latex" special
500+
characters in the display representation.
492501
======================================= ============ ==================================
493502

494503

doc/source/whatsnew/v1.4.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Styler
7575
- :meth:`.Styler.to_latex` introduces keyword argument ``environment``, which also allows a specific "longtable" entry through a separate jinja2 template (:issue:`41866`).
7676
- :meth:`.Styler.to_html` introduces keyword arguments ``sparse_index`` and ``sparse_columns`` (:issue:`41946`)
7777
- 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`)
78+
- Global options have been extended to configure default ``Styler`` properties including formatting options (:issue:`41395`)
7879

7980
There are also bug fixes and deprecations listed below.
8081

pandas/core/config_init.py

+65
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
is_int,
2121
is_nonnegative_int,
2222
is_one_of_factory,
23+
is_str,
2324
is_text,
2425
)
2526

@@ -762,6 +763,36 @@ def register_converter_cb(key):
762763
trimming will occur over columns, rows or both if needed.
763764
"""
764765

766+
styler_precision = """
767+
: int
768+
The precision for floats and complex numbers.
769+
"""
770+
771+
styler_decimal = """
772+
: str
773+
The character representation for the decimal separator for floats and complex.
774+
"""
775+
776+
styler_thousands = """
777+
: str, optional
778+
The character representation for thousands separator for floats, int and complex.
779+
"""
780+
781+
styler_na_rep = """
782+
: str, optional
783+
The string representation for values identified as missing.
784+
"""
785+
786+
styler_escape = """
787+
: str, optional
788+
Whether to escape certain characters according to the given context; html or latex.
789+
"""
790+
791+
styler_formatter = """
792+
: str, callable, dict, optional
793+
A formatter object to be used as default within ``Styler.format``.
794+
"""
795+
765796
with cf.config_prefix("styler"):
766797
cf.register_option("sparse.index", True, styler_sparse_index_doc, validator=bool)
767798

@@ -775,3 +806,37 @@ def register_converter_cb(key):
775806
styler_max_elements,
776807
validator=is_nonnegative_int,
777808
)
809+
810+
cf.register_option("format.decimal", ".", styler_decimal, validator=is_str)
811+
812+
cf.register_option(
813+
"format.precision", 6, styler_precision, validator=is_nonnegative_int
814+
)
815+
816+
cf.register_option(
817+
"format.thousands",
818+
None,
819+
styler_thousands,
820+
validator=is_instance_factory([type(None), str]),
821+
)
822+
823+
cf.register_option(
824+
"format.na_rep",
825+
None,
826+
styler_na_rep,
827+
validator=is_instance_factory([type(None), str]),
828+
)
829+
830+
cf.register_option(
831+
"format.escape",
832+
None,
833+
styler_escape,
834+
validator=is_one_of_factory([None, "html", "latex"]),
835+
)
836+
837+
cf.register_option(
838+
"format.formatter",
839+
None,
840+
styler_formatter,
841+
validator=is_instance_factory([type(None), dict, callable, str]),
842+
)

pandas/io/formats/style.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from pandas.io.formats.style_render import (
5353
CSSProperties,
5454
CSSStyles,
55+
ExtFormatter,
5556
StylerRenderer,
5657
Subset,
5758
Tooltips,
@@ -85,8 +86,11 @@ class Styler(StylerRenderer):
8586
----------
8687
data : Series or DataFrame
8788
Data to be styled - either a Series or DataFrame.
88-
precision : int
89-
Precision to round floats to, defaults to pd.options.display.precision.
89+
precision : int, optional
90+
Precision to round floats to. If not given defaults to
91+
``pandas.options.styler.format.precision``.
92+
93+
.. versionchanged:: 1.4.0
9094
table_styles : list-like, default None
9195
List of {selector: (attr, value)} dicts; see Notes.
9296
uuid : str, default None
@@ -103,7 +107,8 @@ class Styler(StylerRenderer):
103107
number and ``<num_col>`` is the column number.
104108
na_rep : str, optional
105109
Representation for missing values.
106-
If ``na_rep`` is None, no special formatting is applied.
110+
If ``na_rep`` is None, no special formatting is applied, and falls back to
111+
``pandas.options.styler.format.na_rep``.
107112
108113
.. versionadded:: 1.0.0
109114
@@ -113,13 +118,15 @@ class Styler(StylerRenderer):
113118
114119
.. versionadded:: 1.2.0
115120
116-
decimal : str, default "."
117-
Character used as decimal separator for floats, complex and integers
121+
decimal : str, optional
122+
Character used as decimal separator for floats, complex and integers. If not
123+
given uses ``pandas.options.styler.format.decimal``.
118124
119125
.. versionadded:: 1.3.0
120126
121127
thousands : str, optional, default None
122-
Character used as thousands separator for floats, complex and integers
128+
Character used as thousands separator for floats, complex and integers. If not
129+
given uses ``pandas.options.styler.format.thousands``.
123130
124131
.. versionadded:: 1.3.0
125132
@@ -128,9 +135,14 @@ class Styler(StylerRenderer):
128135
in cell display string with HTML-safe sequences.
129136
Use 'latex' to replace the characters ``&``, ``%``, ``$``, ``#``, ``_``,
130137
``{``, ``}``, ``~``, ``^``, and ``\`` in the cell display string with
131-
LaTeX-safe sequences.
138+
LaTeX-safe sequences. If not given uses ``pandas.options.styler.format.escape``
132139
133140
.. versionadded:: 1.3.0
141+
formatter : str, callable, dict, optional
142+
Object to define how values are displayed. See ``Styler.format``. If not given
143+
uses ``pandas.options.styler.format.formatter``.
144+
145+
.. versionadded:: 1.4.0
134146
135147
Attributes
136148
----------
@@ -184,9 +196,10 @@ def __init__(
184196
cell_ids: bool = True,
185197
na_rep: str | None = None,
186198
uuid_len: int = 5,
187-
decimal: str = ".",
199+
decimal: str | None = None,
188200
thousands: str | None = None,
189201
escape: str | None = None,
202+
formatter: ExtFormatter | None = None,
190203
):
191204
super().__init__(
192205
data=data,
@@ -196,13 +209,21 @@ def __init__(
196209
table_attributes=table_attributes,
197210
caption=caption,
198211
cell_ids=cell_ids,
212+
precision=precision,
199213
)
200214

201215
# validate ordered args
216+
thousands = thousands or get_option("styler.format.thousands")
217+
decimal = decimal or get_option("styler.format.decimal")
218+
na_rep = na_rep or get_option("styler.format.na_rep")
219+
escape = escape or get_option("styler.format.escape")
220+
formatter = formatter or get_option("styler.format.formatter")
221+
# precision is handled by superclass as default for performance
222+
202223
self.precision = precision # can be removed on set_precision depr cycle
203224
self.na_rep = na_rep # can be removed on set_na_rep depr cycle
204225
self.format(
205-
formatter=None,
226+
formatter=formatter,
206227
precision=precision,
207228
na_rep=na_rep,
208229
escape=escape,

pandas/io/formats/style_render.py

+17-8
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(
7777
table_attributes: str | None = None,
7878
caption: str | tuple | None = None,
7979
cell_ids: bool = True,
80+
precision: int | None = None,
8081
):
8182

8283
# validate ordered args
@@ -107,10 +108,10 @@ def __init__(
107108
self.cell_context: DefaultDict[tuple[int, int], str] = defaultdict(str)
108109
self._todo: list[tuple[Callable, tuple, dict]] = []
109110
self.tooltips: Tooltips | None = None
110-
def_precision = get_option("display.precision")
111+
precision = precision or get_option("styler.format.precision")
111112
self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function
112113
tuple[int, int], Callable[[Any], str]
113-
] = defaultdict(lambda: partial(_default_formatter, precision=def_precision))
114+
] = defaultdict(lambda: partial(_default_formatter, precision=precision))
114115

115116
def _render_html(self, sparse_index: bool, sparse_columns: bool, **kwargs) -> str:
116117
"""
@@ -688,6 +689,16 @@ def format(
688689
When using a ``formatter`` string the dtypes must be compatible, otherwise a
689690
`ValueError` will be raised.
690691
692+
When instantiating a Styler, default formatting can be applied be setting the
693+
``pandas.options``:
694+
695+
- ``styler.format.formatter``: default None.
696+
- ``styler.format.na_rep``: default None.
697+
- ``styler.format.precision``: default 6.
698+
- ``styler.format.decimal``: default ".".
699+
- ``styler.format.thousands``: default None.
700+
- ``styler.format.escape``: default None.
701+
691702
Examples
692703
--------
693704
Using ``na_rep`` and ``precision`` with the default ``formatter``
@@ -956,11 +967,9 @@ def _default_formatter(x: Any, precision: int, thousands: bool = False) -> Any:
956967
Matches input type, or string if input is float or complex or int with sep.
957968
"""
958969
if isinstance(x, (float, complex)):
959-
if thousands:
960-
return f"{x:,.{precision}f}"
961-
return f"{x:.{precision}f}"
962-
elif isinstance(x, int) and thousands:
963-
return f"{x:,.0f}"
970+
return f"{x:,.{precision}f}" if thousands else f"{x:.{precision}f}"
971+
elif isinstance(x, int):
972+
return f"{x:,.0f}" if thousands else f"{x:.0f}"
964973
return x
965974

966975

@@ -1024,7 +1033,7 @@ def _maybe_wrap_formatter(
10241033
elif callable(formatter):
10251034
func_0 = formatter
10261035
elif formatter is None:
1027-
precision = get_option("display.precision") if precision is None else precision
1036+
precision = precision or get_option("styler.format.precision")
10281037
func_0 = partial(
10291038
_default_formatter, precision=precision, thousands=(thousands is not None)
10301039
)

pandas/tests/io/formats/style/test_format.py

+42
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
IndexSlice,
77
NaT,
88
Timestamp,
9+
option_context,
910
)
1011

1112
pytest.importorskip("jinja2")
@@ -256,3 +257,44 @@ def test_str_escape_error():
256257
_str_escape("text", [])
257258

258259
_str_escape(2.00, "bad_escape") # OK since dtype is float
260+
261+
262+
def test_format_options():
263+
df = DataFrame({"int": [2000, 1], "float": [1.009, None], "str": ["&<", "&~"]})
264+
ctx = df.style._translate(True, True)
265+
266+
# test option: na_rep
267+
assert ctx["body"][1][2]["display_value"] == "nan"
268+
with option_context("styler.format.na_rep", "MISSING"):
269+
ctx_with_op = df.style._translate(True, True)
270+
assert ctx_with_op["body"][1][2]["display_value"] == "MISSING"
271+
272+
# test option: decimal and precision
273+
assert ctx["body"][0][2]["display_value"] == "1.009000"
274+
with option_context("styler.format.decimal", "_"):
275+
ctx_with_op = df.style._translate(True, True)
276+
assert ctx_with_op["body"][0][2]["display_value"] == "1_009000"
277+
with option_context("styler.format.precision", 2):
278+
ctx_with_op = df.style._translate(True, True)
279+
assert ctx_with_op["body"][0][2]["display_value"] == "1.01"
280+
281+
# test option: thousands
282+
assert ctx["body"][0][1]["display_value"] == "2000"
283+
with option_context("styler.format.thousands", "_"):
284+
ctx_with_op = df.style._translate(True, True)
285+
assert ctx_with_op["body"][0][1]["display_value"] == "2_000"
286+
287+
# test option: escape
288+
assert ctx["body"][0][3]["display_value"] == "&<"
289+
assert ctx["body"][1][3]["display_value"] == "&~"
290+
with option_context("styler.format.escape", "html"):
291+
ctx_with_op = df.style._translate(True, True)
292+
assert ctx_with_op["body"][0][3]["display_value"] == "&amp;&lt;"
293+
with option_context("styler.format.escape", "latex"):
294+
ctx_with_op = df.style._translate(True, True)
295+
assert ctx_with_op["body"][1][3]["display_value"] == "\\&\\textasciitilde "
296+
297+
# test option: formatter
298+
with option_context("styler.format.formatter", {"int": "{:,.2f}"}):
299+
ctx_with_op = df.style._translate(True, True)
300+
assert ctx_with_op["body"][0][1]["display_value"] == "2,000.00"

pandas/tests/io/formats/style/test_style.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1146,9 +1146,9 @@ def test_hide_columns_index_mult_levels(self):
11461146
assert ctx["body"][0][0]["is_visible"]
11471147
# data
11481148
assert ctx["body"][1][2]["is_visible"]
1149-
assert ctx["body"][1][2]["display_value"] == 3
1149+
assert ctx["body"][1][2]["display_value"] == "3"
11501150
assert ctx["body"][1][3]["is_visible"]
1151-
assert ctx["body"][1][3]["display_value"] == 4
1151+
assert ctx["body"][1][3]["display_value"] == "4"
11521152

11531153
# hide top column level, which hides both columns
11541154
ctx = df.style.hide_columns("b")._translate(True, True)
@@ -1164,7 +1164,7 @@ def test_hide_columns_index_mult_levels(self):
11641164
assert not ctx["head"][1][2]["is_visible"] # 0
11651165
assert not ctx["body"][1][2]["is_visible"] # 3
11661166
assert ctx["body"][1][3]["is_visible"]
1167-
assert ctx["body"][1][3]["display_value"] == 4
1167+
assert ctx["body"][1][3]["display_value"] == "4"
11681168

11691169
# hide second column and index
11701170
ctx = df.style.hide_columns([("b", 1)]).hide_index()._translate(True, True)
@@ -1175,7 +1175,7 @@ def test_hide_columns_index_mult_levels(self):
11751175
assert not ctx["head"][1][2]["is_visible"] # 1
11761176
assert not ctx["body"][1][3]["is_visible"] # 4
11771177
assert ctx["body"][1][2]["is_visible"]
1178-
assert ctx["body"][1][2]["display_value"] == 3
1178+
assert ctx["body"][1][2]["display_value"] == "3"
11791179

11801180
# hide top row level, which hides both rows
11811181
ctx = df.style.hide_index("a")._translate(True, True)

0 commit comments

Comments
 (0)