Skip to content

Commit

Permalink
ENH: display_format for style
Browse files Browse the repository at this point in the history
Closes #11692
Closes #12134
Closes #12125

This adds a `.format` method to Styler for formatting the display value
(the actual text) of each scalar value.

In the processes of cleaning up the template, I close #12134 (spurious 0)
and #12125 (KeyError from using iloc improperly)

cherry pick test from #12126

only allow str formatting for now

fix tests for new spec

formatter callable

update notebook
  • Loading branch information
TomAugspurger committed Feb 12, 2016
1 parent c805c3b commit a3c38fe
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 75 deletions.
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
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)
raise TypeError(msg)
Loading

0 comments on commit a3c38fe

Please sign in to comment.