Skip to content
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

DEPR: Styler.set_na_rep and .set_precision in favour of .format(na_rep='x', precision=3) #40134

Merged
merged 43 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
971fb40
bug: formatter overwrites na_rep
attack68 Feb 25, 2021
6f573ee
bug: formatter overwrites na_rep
attack68 Feb 25, 2021
f50f894
bug: formatter overwrites na_rep
attack68 Feb 25, 2021
47c4021
bug: formatter overwrites na_rep
attack68 Feb 25, 2021
7371e44
bug: formatter overwrites na_rep
attack68 Feb 25, 2021
82474b1
group tests
attack68 Feb 25, 2021
9a9a39b
revert small chg
attack68 Feb 25, 2021
f72b335
docs
attack68 Feb 25, 2021
0792393
test
attack68 Feb 25, 2021
7bb1171
mypy
attack68 Feb 25, 2021
0d67562
mypy
attack68 Feb 25, 2021
384916d
easy compare
attack68 Feb 25, 2021
76a44ae
easy compare
attack68 Feb 25, 2021
e3d4daf
easy compare
attack68 Feb 25, 2021
63532af
Merge remote-tracking branch 'upstream/master' into fix_formatter_na_rep
attack68 Feb 27, 2021
7bc66eb
deprecate set_na_rep() and set_precision() in favour of format()
attack68 Feb 28, 2021
518405c
edit
attack68 Feb 28, 2021
f3b38ae
edit
attack68 Feb 28, 2021
ae88c55
edit
attack68 Feb 28, 2021
859125f
edit
attack68 Feb 28, 2021
c34083e
docs
attack68 Feb 28, 2021
60a25ff
docs
attack68 Feb 28, 2021
7a64724
same var names
attack68 Mar 1, 2021
4c68cc9
same var names
attack68 Mar 1, 2021
c385a62
same var names
attack68 Mar 1, 2021
61e367a
doc examples
attack68 Mar 1, 2021
c664118
Merge remote-tracking branch 'upstream/master' into depr_format_na_rep
attack68 Mar 1, 2021
a880754
doc examples
attack68 Mar 1, 2021
ed5b13a
typing
attack68 Mar 1, 2021
27fb8d5
typing
attack68 Mar 1, 2021
88fc84d
Merge remote-tracking branch 'upstream/master' into depr_format_na_rep
attack68 Mar 2, 2021
bf7f9fc
req changes
attack68 Mar 3, 2021
892df77
req changes
attack68 Mar 3, 2021
b63220b
tests
attack68 Mar 3, 2021
fbe8bb2
whats new
attack68 Mar 3, 2021
302924c
req changes
attack68 Mar 4, 2021
43b8fa7
test clear
attack68 Mar 4, 2021
672f42c
Merge remote-tracking branch 'upstream/master' into depr_format_na_rep
attack68 Mar 5, 2021
482506c
Merge branch 'master' into depr_format_na_rep
jreback Mar 5, 2021
2c7d647
Update doc/source/whatsnew/v1.3.0.rst
attack68 Mar 5, 2021
aa51fe0
Update pandas/io/formats/style.py
attack68 Mar 5, 2021
d26d761
Update pandas/io/formats/style.py
attack68 Mar 5, 2021
94370d8
format docstring
attack68 Mar 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions doc/source/reference/style.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,7 +43,6 @@ Style application
Styler.set_caption
Styler.set_properties
Styler.set_uuid
Styler.set_na_rep
Styler.clear
Styler.pipe

Expand Down
210 changes: 134 additions & 76 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from contextlib import contextmanager
import copy
from functools import partial
from itertools import product
from typing import (
Any,
Callable,
Expand All @@ -22,6 +21,7 @@
cast,
)
from uuid import uuid4
import warnings

import numpy as np

Expand All @@ -37,14 +37,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
Expand All @@ -53,6 +49,8 @@

jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")

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]
Expand Down Expand Up @@ -182,9 +180,6 @@ 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].")
Expand All @@ -193,7 +188,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
Expand All @@ -204,7 +198,11 @@ 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))
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)

def _repr_html_(self) -> str:
"""
Expand All @@ -225,15 +223,6 @@ def _init_tooltips(self):
if self.tooltips is None:
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
else:
return x

def set_tooltips(self, ttips: DataFrame) -> Styler:
"""
Add string based tooltips that will appear in the `Styler` HTML result. These
Expand Down Expand Up @@ -389,7 +378,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
Expand Down Expand Up @@ -569,7 +557,6 @@ def _translate(self):
"cellstyle": cellstyle,
"body": body,
"uuid": uuid,
"precision": precision,
"table_styles": _format_table_styles(table_styles),
"caption": caption,
"table_attributes": table_attr,
Expand All @@ -579,14 +566,22 @@ def _translate(self):

return d

def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> Styler:
def format(
self,
formatter: Optional[ExtFormatter] = None,
subset: Optional[Union[slice, Sequence[Any]]] = None,
na_rep: Optional[str] = None,
precision: Optional[int] = 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.
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.
subset : IndexSlice
An argument to ``DataFrame.loc`` that restricts which elements
``formatter`` is applied to.
Expand All @@ -596,58 +591,97 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> Styler

.. versionadded:: 1.0.0

precision : int, optional
Floating point precision to use for display purposes, if not determined by
the specified ``formatter``.

.. versionadded:: 1.3.0

Returns
-------
self : Styler

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)``

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``.

- str: this will be wrapped in: ``a.format(x)``
- callable: called with the value of an individual cell
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 default display value for numeric values is the "general" (``g``)
format with ``pd.options.display.precision`` precision.
When using a ``formatter`` string the dtypes must be compatible, otherwise a
`ValueError` will be raised.

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})
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
"""
if formatter is None:
assert self._display_funcs.default_factory is not None
formatter = self._display_funcs.default_factory()
subset = slice(None) if subset is None else subset
subset = _non_reducing_slice(subset)
data = self.data.loc[subset]

columns = data.columns
if not isinstance(formatter, dict):
formatter = {col: formatter for col in columns}

for col in columns:
try:
format_func = formatter[col]
except KeyError:
format_func = None
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)
self._display_funcs[(i, j)] = format_func

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:
Expand Down Expand Up @@ -748,7 +782,6 @@ def render(self, **kwargs) -> str:
* cellstyle
* body
* uuid
* precision
* table_styles
* caption
* table_attributes
Expand Down Expand Up @@ -783,11 +816,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)
Expand Down Expand Up @@ -1035,7 +1066,7 @@ def where(

def set_precision(self, precision: int) -> Styler:
"""
Set the precision used to render.
Set the precision used to display values.

Parameters
----------
Expand All @@ -1044,9 +1075,16 @@ def set_precision(self, precision: int) -> Styler:
Returns
-------
self : Styler

Notes
-----
This method is deprecated see `Styler.format`.
"""
warnings.warn(
"this method is deprecated in favour of `Styler.format`", DeprecationWarning
)
self.precision = precision
return self
return self.format(precision=precision, na_rep=self.na_rep)

def set_table_attributes(self, attributes: str) -> Styler:
"""
Expand Down Expand Up @@ -1264,9 +1302,16 @@ def set_na_rep(self, na_rep: str) -> Styler:
Returns
-------
self : Styler

Notes
-----
This method is deprecated. See `Styler.format()`
"""
warnings.warn(
"This method is deprecated in favour of `Styler.format`", DeprecationWarning
)
self.na_rep = na_rep
return self
return self.format(na_rep=na_rep, precision=self.precision)

def hide_index(self) -> Styler:
"""
Expand Down Expand Up @@ -2028,24 +2073,37 @@ 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: Union[Callable, str], na_rep: Optional[str]
formatter: Optional[BaseFormatter] = 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):
formatter_func = lambda x: formatter.format(x)
elif callable(formatter):
formatter_func = formatter
elif formatter is None:
formatter_func = partial(_default_formatter, precision=precision)
else:
msg = f"Expected a template string or callable, got {formatter} instead"
raise TypeError(msg)
raise TypeError(f"'formatter' expected str or callable, got {type(formatter)}")

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)
return lambda x: na_rep if pd.isna(x) else formatter_func(x)


def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList:
Expand Down
Loading