Skip to content

Style display format #12162

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,7 @@ Style Application

Styler.apply
Styler.applymap
Styler.format
Styler.set_precision
Styler.set_table_styles
Styler.set_caption
Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.18.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ Other enhancements
values it contains (:issue:`11597`)
- ``Series`` gained an ``is_unique`` attribute (:issue:`11946`)
- ``DataFrame.quantile`` and ``Series.quantile`` now accept ``interpolation`` keyword (:issue:`10174`).
- Added ``DataFrame.style.format`` for more flexible formatting of cell values (:issue:`11692`)
- ``DataFrame.select_dtypes`` now allows the ``np.float16`` typecode (:issue:`11990`)
- ``pivot_table()`` now accepts most iterables for the ``values`` parameter (:issue:`12017`)
- Added Google ``BigQuery`` service account authentication support, which enables authentication on remote servers. (:issue:`11881`). For further details see :ref:`here <io.bigquery_authentication>`
Expand Down
148 changes: 120 additions & 28 deletions pandas/core/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
DataFrames and Series.
"""
from functools import partial
from itertools import product
from contextlib import contextmanager
from uuid import uuid1
import copy
from collections import defaultdict
from collections import defaultdict, MutableMapping

try:
from jinja2 import Template
Expand All @@ -18,7 +19,8 @@

import numpy as np
import pandas as pd
from pandas.compat import lzip
from pandas.compat import lzip, range
import pandas.core.common as com
from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
try:
import matplotlib.pyplot as plt
Expand Down Expand Up @@ -117,11 +119,7 @@ class Styler(object):
<tr>
{% for c in r %}
<{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}">
{% if c.value is number %}
{{c.value|round(precision)}}
{% else %}
{{c.value}}
{% endif %}
{{ c.display_value }}
{% endfor %}
</tr>
{% endfor %}
Expand Down Expand Up @@ -152,6 +150,15 @@ def __init__(self, data, precision=None, table_styles=None, uuid=None,
precision = pd.options.display.precision
self.precision = precision
self.table_attributes = table_attributes
# display_funcs maps (row, col) -> formatting function

def default_display_func(x):
if com.is_float(x):
return '{:>.{precision}g}'.format(x, precision=self.precision)
else:
return x

self._display_funcs = defaultdict(lambda: default_display_func)

def _repr_html_(self):
"""Hooks into Jupyter notebook rich display system."""
Expand Down Expand Up @@ -199,10 +206,12 @@ def _translate(self):
"class": " ".join([BLANK_CLASS])}] * n_rlvls
for c in range(len(clabels[0])):
cs = [COL_HEADING_CLASS, "level%s" % r, "col%s" % c]
cs.extend(
cell_context.get("col_headings", {}).get(r, {}).get(c, []))
cs.extend(cell_context.get(
"col_headings", {}).get(r, {}).get(c, []))
value = clabels[r][c]
row_es.append({"type": "th",
"value": clabels[r][c],
"value": value,
"display_value": value,
"class": " ".join(cs)})
head.append(row_es)

Expand Down Expand Up @@ -231,15 +240,22 @@ def _translate(self):
cell_context.get("row_headings", {}).get(r, {}).get(c, []))
row_es = [{"type": "th",
"value": rlabels[r][c],
"class": " ".join(cs)} for c in range(len(rlabels[r]))]
"class": " ".join(cs),
"display_value": rlabels[r][c]}
for c in range(len(rlabels[r]))]

for c, col in enumerate(self.data.columns):
cs = [DATA_CLASS, "row%s" % r, "col%s" % c]
cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
row_es.append({"type": "td",
"value": self.data.iloc[r][c],
"class": " ".join(cs),
"id": "_".join(cs[1:])})
formatter = self._display_funcs[(r, c)]
value = self.data.iloc[r, c]
row_es.append({
"type": "td",
"value": value,
"class": " ".join(cs),
"id": "_".join(cs[1:]),
"display_value": formatter(value)
})
props = []
for x in ctx[r, c]:
# have to handle empty styles like ['']
Expand All @@ -255,6 +271,71 @@ def _translate(self):
precision=precision, table_styles=table_styles,
caption=caption, table_attributes=self.table_attributes)

def format(self, formatter, subset=None):
"""
Format the text display value of cells.

.. versionadded:: 0.18.0

Parameters
----------
formatter: str, callable, or dict
subset: IndexSlice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide some explanation for this one?

A argument to DataFrame.loc that restricts which elements
``formatter`` is applied to.

Returns
-------
self : Styler

Notes
-----

``formatter`` is either an ``a`` or a dict ``{column name: a}`` where
``a`` is one of

- str: this will be wrapped in: ``a.format(x)``
- callable: called with the value of an individual cell

The default display value for numeric values is the "general" (``g``)
format with ``pd.options.display.precision`` precision.

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})
"""
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 isinstance(formatter, MutableMapping):
for col, col_formatter in formatter.items():
# formatter must be callable, so '{}' are converted to lambdas
col_formatter = _maybe_wrap_formatter(col_formatter)
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
locs = product(*(row_locs, col_locs))
for i, j in locs:
formatter = _maybe_wrap_formatter(formatter)
self._display_funcs[(i, j)] = formatter
return self

def render(self):
"""
Render the built up styles to HTML
Expand Down Expand Up @@ -376,7 +457,7 @@ def apply(self, func, axis=0, subset=None, **kwargs):

Returns
-------
self
self : Styler

Notes
-----
Expand Down Expand Up @@ -415,7 +496,7 @@ def applymap(self, func, subset=None, **kwargs):

Returns
-------
self
self : Styler

"""
self._todo.append((lambda instance: getattr(instance, '_applymap'),
Expand All @@ -434,7 +515,7 @@ def set_precision(self, precision):

Returns
-------
self
self : Styler
"""
self.precision = precision
return self
Expand All @@ -453,7 +534,7 @@ def set_table_attributes(self, attributes):

Returns
-------
self
self : Styler
"""
self.table_attributes = attributes
return self
Expand Down Expand Up @@ -489,7 +570,7 @@ def use(self, styles):

Returns
-------
self
self : Styler

See Also
--------
Expand All @@ -510,7 +591,7 @@ def set_uuid(self, uuid):

Returns
-------
self
self : Styler
"""
self.uuid = uuid
return self
Expand All @@ -527,7 +608,7 @@ def set_caption(self, caption):

Returns
-------
self
self : Styler
"""
self.caption = caption
return self
Expand All @@ -550,7 +631,7 @@ def set_table_styles(self, table_styles):

Returns
-------
self
self : Styler

Examples
--------
Expand Down Expand Up @@ -583,7 +664,7 @@ def highlight_null(self, null_color='red'):

Returns
-------
self
self : Styler
"""
self.applymap(self._highlight_null, null_color=null_color)
return self
Expand All @@ -610,7 +691,7 @@ def background_gradient(self, cmap='PuBu', low=0, high=0, axis=0,

Returns
-------
self
self : Styler

Notes
-----
Expand Down Expand Up @@ -695,7 +776,7 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100):

Returns
-------
self
self : Styler
"""
subset = _maybe_numeric_slice(self.data, subset)
subset = _non_reducing_slice(subset)
Expand All @@ -720,7 +801,7 @@ def highlight_max(self, subset=None, color='yellow', axis=0):

Returns
-------
self
self : Styler
"""
return self._highlight_handler(subset=subset, color=color, axis=axis,
max_=True)
Expand All @@ -742,7 +823,7 @@ def highlight_min(self, subset=None, color='yellow', axis=0):

Returns
-------
self
self : Styler
"""
return self._highlight_handler(subset=subset, color=color, axis=axis,
max_=False)
Expand Down Expand Up @@ -771,3 +852,14 @@ def _highlight_extrema(data, color='yellow', max_=True):
extrema = data == data.min().min()
return pd.DataFrame(np.where(extrema, attr, ''),
index=data.index, columns=data.columns)


def _maybe_wrap_formatter(formatter):
if com.is_string_like(formatter):
return lambda x: formatter.format(x)
elif callable(formatter):
return formatter
else:
msg = "Expected a template string or callable, got {} instead".format(
formatter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, though I would have simply done an if/elsif/raise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want to fix, then go ahead and merge (use the scripts/merge-pr.py tool. note that I will occasionaly edit the generated commit message if it includes too much stuff (e.g. it by default includes everything at the top of the PR). you can do it before it pushes upstream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix and merge tonight.

raise TypeError(msg)
Loading