Skip to content

Commit a3c38fe

Browse files
committed
ENH: display_format for style
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
1 parent c805c3b commit a3c38fe

File tree

4 files changed

+240
-75
lines changed

4 files changed

+240
-75
lines changed

doc/source/api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,7 @@ Style Application
18201820

18211821
Styler.apply
18221822
Styler.applymap
1823+
Styler.format
18231824
Styler.set_precision
18241825
Styler.set_table_styles
18251826
Styler.set_caption

doc/source/whatsnew/v0.18.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ Other enhancements
392392
values it contains (:issue:`11597`)
393393
- ``Series`` gained an ``is_unique`` attribute (:issue:`11946`)
394394
- ``DataFrame.quantile`` and ``Series.quantile`` now accept ``interpolation`` keyword (:issue:`10174`).
395+
- Added ``DataFrame.style.format`` for more flexible formatting of cell values (:issue:`11692`)
395396
- ``DataFrame.select_dtypes`` now allows the ``np.float16`` typecode (:issue:`11990`)
396397
- ``pivot_table()`` now accepts most iterables for the ``values`` parameter (:issue:`12017`)
397398
- Added Google ``BigQuery`` service account authentication support, which enables authentication on remote servers. (:issue:`11881`). For further details see :ref:`here <io.bigquery_authentication>`

pandas/core/style.py

+120-28
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
DataFrames and Series.
44
"""
55
from functools import partial
6+
from itertools import product
67
from contextlib import contextmanager
78
from uuid import uuid1
89
import copy
9-
from collections import defaultdict
10+
from collections import defaultdict, MutableMapping
1011

1112
try:
1213
from jinja2 import Template
@@ -18,7 +19,8 @@
1819

1920
import numpy as np
2021
import pandas as pd
21-
from pandas.compat import lzip
22+
from pandas.compat import lzip, range
23+
import pandas.core.common as com
2224
from pandas.core.indexing import _maybe_numeric_slice, _non_reducing_slice
2325
try:
2426
import matplotlib.pyplot as plt
@@ -117,11 +119,7 @@ class Styler(object):
117119
<tr>
118120
{% for c in r %}
119121
<{{c.type}} id="T_{{uuid}}{{c.id}}" class="{{c.class}}">
120-
{% if c.value is number %}
121-
{{c.value|round(precision)}}
122-
{% else %}
123-
{{c.value}}
124-
{% endif %}
122+
{{ c.display_value }}
125123
{% endfor %}
126124
</tr>
127125
{% endfor %}
@@ -152,6 +150,15 @@ def __init__(self, data, precision=None, table_styles=None, uuid=None,
152150
precision = pd.options.display.precision
153151
self.precision = precision
154152
self.table_attributes = table_attributes
153+
# display_funcs maps (row, col) -> formatting function
154+
155+
def default_display_func(x):
156+
if com.is_float(x):
157+
return '{:>.{precision}g}'.format(x, precision=self.precision)
158+
else:
159+
return x
160+
161+
self._display_funcs = defaultdict(lambda: default_display_func)
155162

156163
def _repr_html_(self):
157164
"""Hooks into Jupyter notebook rich display system."""
@@ -199,10 +206,12 @@ def _translate(self):
199206
"class": " ".join([BLANK_CLASS])}] * n_rlvls
200207
for c in range(len(clabels[0])):
201208
cs = [COL_HEADING_CLASS, "level%s" % r, "col%s" % c]
202-
cs.extend(
203-
cell_context.get("col_headings", {}).get(r, {}).get(c, []))
209+
cs.extend(cell_context.get(
210+
"col_headings", {}).get(r, {}).get(c, []))
211+
value = clabels[r][c]
204212
row_es.append({"type": "th",
205-
"value": clabels[r][c],
213+
"value": value,
214+
"display_value": value,
206215
"class": " ".join(cs)})
207216
head.append(row_es)
208217

@@ -231,15 +240,22 @@ def _translate(self):
231240
cell_context.get("row_headings", {}).get(r, {}).get(c, []))
232241
row_es = [{"type": "th",
233242
"value": rlabels[r][c],
234-
"class": " ".join(cs)} for c in range(len(rlabels[r]))]
243+
"class": " ".join(cs),
244+
"display_value": rlabels[r][c]}
245+
for c in range(len(rlabels[r]))]
235246

236247
for c, col in enumerate(self.data.columns):
237248
cs = [DATA_CLASS, "row%s" % r, "col%s" % c]
238249
cs.extend(cell_context.get("data", {}).get(r, {}).get(c, []))
239-
row_es.append({"type": "td",
240-
"value": self.data.iloc[r][c],
241-
"class": " ".join(cs),
242-
"id": "_".join(cs[1:])})
250+
formatter = self._display_funcs[(r, c)]
251+
value = self.data.iloc[r, c]
252+
row_es.append({
253+
"type": "td",
254+
"value": value,
255+
"class": " ".join(cs),
256+
"id": "_".join(cs[1:]),
257+
"display_value": formatter(value)
258+
})
243259
props = []
244260
for x in ctx[r, c]:
245261
# have to handle empty styles like ['']
@@ -255,6 +271,71 @@ def _translate(self):
255271
precision=precision, table_styles=table_styles,
256272
caption=caption, table_attributes=self.table_attributes)
257273

274+
def format(self, formatter, subset=None):
275+
"""
276+
Format the text display value of cells.
277+
278+
.. versionadded:: 0.18.0
279+
280+
Parameters
281+
----------
282+
formatter: str, callable, or dict
283+
subset: IndexSlice
284+
A argument to DataFrame.loc that restricts which elements
285+
``formatter`` is applied to.
286+
287+
Returns
288+
-------
289+
self : Styler
290+
291+
Notes
292+
-----
293+
294+
``formatter`` is either an ``a`` or a dict ``{column name: a}`` where
295+
``a`` is one of
296+
297+
- str: this will be wrapped in: ``a.format(x)``
298+
- callable: called with the value of an individual cell
299+
300+
The default display value for numeric values is the "general" (``g``)
301+
format with ``pd.options.display.precision`` precision.
302+
303+
Examples
304+
--------
305+
306+
>>> df = pd.DataFrame(np.random.randn(4, 2), columns=['a', 'b'])
307+
>>> df.style.format("{:.2%}")
308+
>>> df['c'] = ['a', 'b', 'c', 'd']
309+
>>> df.style.format({'C': str.upper})
310+
"""
311+
if subset is None:
312+
row_locs = range(len(self.data))
313+
col_locs = range(len(self.data.columns))
314+
else:
315+
subset = _non_reducing_slice(subset)
316+
if len(subset) == 1:
317+
subset = subset, self.data.columns
318+
319+
sub_df = self.data.loc[subset]
320+
row_locs = self.data.index.get_indexer_for(sub_df.index)
321+
col_locs = self.data.columns.get_indexer_for(sub_df.columns)
322+
323+
if isinstance(formatter, MutableMapping):
324+
for col, col_formatter in formatter.items():
325+
# formatter must be callable, so '{}' are converted to lambdas
326+
col_formatter = _maybe_wrap_formatter(col_formatter)
327+
col_num = self.data.columns.get_indexer_for([col])[0]
328+
329+
for row_num in row_locs:
330+
self._display_funcs[(row_num, col_num)] = col_formatter
331+
else:
332+
# single scalar to format all cells with
333+
locs = product(*(row_locs, col_locs))
334+
for i, j in locs:
335+
formatter = _maybe_wrap_formatter(formatter)
336+
self._display_funcs[(i, j)] = formatter
337+
return self
338+
258339
def render(self):
259340
"""
260341
Render the built up styles to HTML
@@ -376,7 +457,7 @@ def apply(self, func, axis=0, subset=None, **kwargs):
376457
377458
Returns
378459
-------
379-
self
460+
self : Styler
380461
381462
Notes
382463
-----
@@ -415,7 +496,7 @@ def applymap(self, func, subset=None, **kwargs):
415496
416497
Returns
417498
-------
418-
self
499+
self : Styler
419500
420501
"""
421502
self._todo.append((lambda instance: getattr(instance, '_applymap'),
@@ -434,7 +515,7 @@ def set_precision(self, precision):
434515
435516
Returns
436517
-------
437-
self
518+
self : Styler
438519
"""
439520
self.precision = precision
440521
return self
@@ -453,7 +534,7 @@ def set_table_attributes(self, attributes):
453534
454535
Returns
455536
-------
456-
self
537+
self : Styler
457538
"""
458539
self.table_attributes = attributes
459540
return self
@@ -489,7 +570,7 @@ def use(self, styles):
489570
490571
Returns
491572
-------
492-
self
573+
self : Styler
493574
494575
See Also
495576
--------
@@ -510,7 +591,7 @@ def set_uuid(self, uuid):
510591
511592
Returns
512593
-------
513-
self
594+
self : Styler
514595
"""
515596
self.uuid = uuid
516597
return self
@@ -527,7 +608,7 @@ def set_caption(self, caption):
527608
528609
Returns
529610
-------
530-
self
611+
self : Styler
531612
"""
532613
self.caption = caption
533614
return self
@@ -550,7 +631,7 @@ def set_table_styles(self, table_styles):
550631
551632
Returns
552633
-------
553-
self
634+
self : Styler
554635
555636
Examples
556637
--------
@@ -583,7 +664,7 @@ def highlight_null(self, null_color='red'):
583664
584665
Returns
585666
-------
586-
self
667+
self : Styler
587668
"""
588669
self.applymap(self._highlight_null, null_color=null_color)
589670
return self
@@ -610,7 +691,7 @@ def background_gradient(self, cmap='PuBu', low=0, high=0, axis=0,
610691
611692
Returns
612693
-------
613-
self
694+
self : Styler
614695
615696
Notes
616697
-----
@@ -695,7 +776,7 @@ def bar(self, subset=None, axis=0, color='#d65f5f', width=100):
695776
696777
Returns
697778
-------
698-
self
779+
self : Styler
699780
"""
700781
subset = _maybe_numeric_slice(self.data, subset)
701782
subset = _non_reducing_slice(subset)
@@ -720,7 +801,7 @@ def highlight_max(self, subset=None, color='yellow', axis=0):
720801
721802
Returns
722803
-------
723-
self
804+
self : Styler
724805
"""
725806
return self._highlight_handler(subset=subset, color=color, axis=axis,
726807
max_=True)
@@ -742,7 +823,7 @@ def highlight_min(self, subset=None, color='yellow', axis=0):
742823
743824
Returns
744825
-------
745-
self
826+
self : Styler
746827
"""
747828
return self._highlight_handler(subset=subset, color=color, axis=axis,
748829
max_=False)
@@ -771,3 +852,14 @@ def _highlight_extrema(data, color='yellow', max_=True):
771852
extrema = data == data.min().min()
772853
return pd.DataFrame(np.where(extrema, attr, ''),
773854
index=data.index, columns=data.columns)
855+
856+
857+
def _maybe_wrap_formatter(formatter):
858+
if com.is_string_like(formatter):
859+
return lambda x: formatter.format(x)
860+
elif callable(formatter):
861+
return formatter
862+
else:
863+
msg = "Expected a template string or callable, got {} instead".format(
864+
formatter)
865+
raise TypeError(msg)

0 commit comments

Comments
 (0)