New in version 0.17.1
*Provisional: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your [feedback](https://github.com/pydata/pandas/issues).*
You can apply conditional formatting, the visual styling of a DataFrame
depending on the data within, by using the DataFrame.style
property.
This is a property that returns a pandas.Styler
object, which has
useful methods for formatting and displaying DataFrames.
The styling is accomplished using CSS.
You write functions that take scalars, DataFrame
s or Series
, and return like-indexed DataFrames or Series with CSS "attribute: value"
pairs for the values.
These functions can be incrementally passed to the Styler
which collects the styles before rendering.
Pass your style functions into one of the following methods:
Styler.applymap
: elementwiseStyler.apply
: column-/row-/table-wiseBoth of those methods take a function (and some other keyword arguments) and applies your function to the DataFrame in a certain way.
Styler.applymap
works through the DataFrame elementwise.
Styler.apply
passes each column or row into your DataFrame one-at-a-time or the entire table at once, depending on the axis
keyword argument.
For columnwise use axis=0
, rowwise use axis=1
, and for the entire table at once use axis=None
.
The result of the function application, a CSS attribute-value pair, is stored in an internal dictionary on your Styler
object.
Let's see some examples.
import pandas as pd
import numpy as np
np.random.seed(24)
df = pd.DataFrame({'A': np.linspace(1, 10, 10)})
df = pd.concat([df, pd.DataFrame(np.random.randn(10, 4), columns=list('BCDE'))],
axis=1)
df.iloc[0, 2] = np.nan
Here's a boring example of rendering a DataFrame, without any (visible) styles:
df.style
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Note: The DataFrame.style
attribute is a propetry that returns a Styler
object. Styler
has a _repr_html_
method defined on it so they are rendered automatically. If you want the actual HTML back for further processing or for writing to file call the .render()
method which returns a string.
The above output looks very similar to the standard DataFrame HTML representation. But we've done some work behind the scenes to attach CSS classes to each cell. We can view these by calling the .render
method.
df.style.render(trim=False).split('\n')[:10] # ignore trim for now
['', ' <style type="text/css" >', ' ', ' ', ' #T_3bc80d1a_8757_11e5_a7fa_a45e60bd97fbrow0_col0 {', ' ', ' }', ' ', ' #T_3bc80d1a_8757_11e5_a7fa_a45e60bd97fbrow0_col1 {', ' ']
The row0_col0
is the identifier for that particular cell. We've also prepended each row/column identifier with a UUID unique to each DataFrame so that the style from one doesn't collied with the styling from another within the same notebook or page (you can set the uuid
if you'd like to tie together the styling of two DataFrames).
Let's write a simple style function that will color negative numbers red and positive numbers black.
def color_negative_red(val):
"""
Takes a scalar and returns a string with
the css property `'color: red'` for negative
strings, black otherwise.
"""
color = 'red' if val < 0 else 'black'
return 'color: %s' % color
In this case, the cell's style depends only on it's own value.
That means we should use the Styler.applymap
method which works elementwise.
df.style.applymap(color_negative_red)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Notice the similarity with the standard df.applymap
, which operates on DataFrames elementwise. We want you to be able to resuse your existing knowledge of how to interact with DataFrames.
Notice also that our function returned a string containing the CSS attribute and value, separated by a colon just like in a <style>
tag. This will be a common theme.
Now suppose you wanted to highlight the maximum value in each column.
We can't use .applymap
anymore since that operated elementwise.
Instead, we'll turn to .apply
which operates columnwise (or rowwise using the axis
keyword). Later on we'll see that something like highlight_max
is already defined on Styler
so you wouldn't need to write this yourself.
def highlight_max(s):
'''
highlight the maximum in a Series yellow.
'''
is_max = s == s.max()
return ['background-color: yellow' if v else '' for v in is_max]
df.style.apply(highlight_max)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
We encourage you to use method chains to build up a style piecewise, before finally rending at the end of the chain.
df.style.\
applymap(color_negative_red).\
apply(highlight_max)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Above we used Styler.apply
to pass in each column one at a time.
*Debugging Tip*: If you're having trouble writing your style function, try just passing it into df.apply
. Styler.apply
uses that internally, so the result (should) be the same.
What if you wanted to highlight just the maximum value in the entire table?
Use .apply(function, axis=None)
to indicate that your function wants the entire table, not one column or row at a time. Let's try that next.
We'll rewrite our highlight-max
to handle either Series (from .apply(axis=0 or 1)
) or DataFrames (from .apply(axis=None)
). We'll also allow the color to be adjustable, to demonstrate that .apply
, and .applymap
pass along keyword arguments.
def highlight_max(data, color='yellow'):
'''
highlight the maximum in a Series or DataFrame
'''
attr = 'background-color: {}'.format(color)
if data.ndim == 1: # Series from .apply(axis=0) or axis=1
is_max = data == data.max()
return [attr if v else '' for v in is_max]
else: # from .apply(axis=None)
is_max = data == data.max().max()
return pd.DataFrame(np.where(is_max, attr, ''),
index=data.index, columns=data.columns)
df.style.apply(highlight_max, color='darkorange', axis=None)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
df.style.highlight_max(subset=['B', 'C'], axis=0)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Both Styler.apply
, and Styler.applymap
accept a subset
keyword.
This allows you to apply styles to specific rows or columns, without having to code that logic into your style
function.
The value passed to subset
behaves simlar to slicing a DataFrame.
(row_indexer, column_indexer)
The last option let's you slice rows and columns simultanteously.
Consider using pd.IndexSlice
to construct the tuple for the last one.
df.style.apply(highlight_max, subset=['B', 'C', 'D'])
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
For row and column slicing, any valid indexer to .loc
will work.
df.style.applymap(color_negative_red,
subset=pd.IndexSlice[2:5, ['B', 'D']])
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Consider using pd.IndexSlice
to make writing the slices easier.
N.B. Only label-based slicing is supported right now, not positional.
N.B. If your style function uses a subset
keyword argument, consider wrapping your function in a functools.partial
, partialing out that keyword.
my_func2 = functools.partial(my_func, subset=42)
Finally, we expect certain styling functions to be common enough that we've included a few "built-in" to the Styler
, so you don't have to write them yourself.
df.style.highlight_null(null_color='red')
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Gradients for the background color. These require matplotlib, and we'll use Seaborn to get a nice colormap.
import seaborn as sns
cm = sns.light_palette("green", as_cmap=True)
s = df.style.background_gradient(cmap=cm)
s
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Styler.background_gradient
takes the keyword arguments low
and high
. Roughly speaking these extend the range of your data by low
and high
percent so that when we convert the colors, the colormap's entire range isn't used. This is useful so that you can actually read the text still.
# Uses the full color range
df.loc[:4].style.background_gradient(cmap='viridis')
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
# Compreess the color range
(df.loc[:4]
.style
.background_gradient(cmap='viridis', low=.5, high=0)
.highlight_null('red'))
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
You got your barchart in my DataFrame!
df.style.bar(subset=['A', 'B'], color='#7F7FFF')
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Use Styler.set_properties
when the style doesn't actually depend on the values.
df.style.set_properties(**{'background-color': 'black',
'color': 'lawngreen',
'border-color': 'white'})
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
You've seen a few methods for data-driven styling.
Styler
also provides a few other options for styling that don't depend on the data.
Each of these can be specified in three ways:
pandas.core.Styler
.render
.set_
methods, e.g. .set_caption
The best method to use depends on the context. Use the Styler
constructor when building many styled DataFrames that should all share the same properties. For interactive use, .render
or .set_
are more convinient.
You can control the precision of floats using pandas' regular display.precision
option.
with pd.option_context('display.precision', 2):
html = (df.style
.applymap(color_negative_red)
.apply(highlight_max))
html
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.33 | nan | -0.32 | -0.99 |
1 | 2.0 | -1.07 | -1.44 | 0.56 | 0.3 |
2 | 3.0 | -1.63 | 0.22 | 0.68 | 1.89 |
3 | 4.0 | 0.96 | 0.1 | -0.48 | 0.85 |
4 | 5.0 | 1.45 | 1.06 | 0.17 | 0.52 |
5 | 6.0 | -1.34 | 0.56 | 1.39 | -0.06 |
6 | 7.0 | 0.12 | 1.21 | -0.0 | 1.63 |
7 | 8.0 | 0.35 | 1.04 | -0.39 | 0.52 |
8 | 9.0 | 1.69 | -1.33 | 1.43 | -2.09 |
9 | 10.0 | -0.13 | 0.63 | -0.59 | 0.29 |
Or through a set_precision
method.
df.style\
.applymap(color_negative_red)\
.apply(highlight_max)\
.set_precision(2)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.33 | nan | -0.32 | -0.99 |
1 | 2.0 | -1.07 | -1.44 | 0.56 | 0.3 |
2 | 3.0 | -1.63 | 0.22 | 0.68 | 1.89 |
3 | 4.0 | 0.96 | 0.1 | -0.48 | 0.85 |
4 | 5.0 | 1.45 | 1.06 | 0.17 | 0.52 |
5 | 6.0 | -1.34 | 0.56 | 1.39 | -0.06 |
6 | 7.0 | 0.12 | 1.21 | -0.0 | 1.63 |
7 | 8.0 | 0.35 | 1.04 | -0.39 | 0.52 |
8 | 9.0 | 1.69 | -1.33 | 1.43 | -2.09 |
9 | 10.0 | -0.13 | 0.63 | -0.59 | 0.29 |
Setting the precision only affects the printed number; the full-precision values are always passed to your style functions. You can always use df.round(2).style
if you'd prefer to round from the start.
from pandas.core.style import Styler
s = Styler(df, caption='Colormaps, with a caption.')
s.background_gradient(cmap=cm)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Alternativly, pass the caption in when rendering.
from IPython.display import HTML
HTML(
df.style.background_gradient(cmap=cm).render(caption="Caption, from render.")
)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
The next option you have are "table styles".
These are styles that apply to the table as a whole, and don't look at the data.
Certain sytlings, including pseudo-selectors like :hover
can only be used this way.
def hover(hover_color="#ffff99"):
return dict(selector="tr:hover",
props=[("background-color", "%s" % hover_color)])
styles = [
hover(),
dict(selector="th", props=[("font-size", "150%"),
("text-align", "center")]),
dict(selector="caption", props=[("caption-side", "bottom")])
]
html = df.style.render(table_styles=styles, caption="Hover to highlight.")
HTML(html)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
table_styles
should be a list of dictionaries.
Each dictionary should have the selector
and props
keys.
The value for selector
should be a valid CSS selector.
Recall that all the styles already attached to an id
, unique to
each Styler
. This selector is in addition to that id
.
The value for props
should be a list of tuples of ('attribute', 'value')
.
table_styles
are extremely flexible, but not as fun to type out by hand.
We hope to collect some useful ones either in pandas, or preferable in a new package that builds on top the tools here.
(use Series.to_frame().style)
Some of these will be addressed in the future.
The core of pandas is, and will remain, its "high-performance, easy-to-use data structures".
With that in mind, we hope that DataFrame.style
accomplishes two goals
If you build a great library on top of this, let us know and we'll link to it.
This section contains a bit of information about the implementation of Styler
.
Since the feature is so new all of this is subject to change, even more so than the end-use API.
As users apply styles (via .apply
, .applymap
), we modify an internal defaultdict(list)
called self.ctx
.
This maps (row_position, col_position)
to a list of (for CSS) 'attribute: value'
strings.
Every call to .apply
or .applymap
or one of the builtin stylers should update self.ctx
and return self
so that the chain can continue.
Rendering uses Jinja templates.
The .translate
method takes self.ctx
and builds another dictionary ready to be passed into Styler.template.render
, the Jinja template.
We've used Jinja templates to build up the HTML.
The template is stored as a class variable Styler.template.
. Subclasses can override that.
class CustomStyle(Styler):
template = Template("""...""")
# https://developer.mozilla.org/en-US/docs/Web/CSS/animation#Cylon_Eye
# no animation yet :(
def cylon(s):
tpl = """
background-color: red;
background-image: -webkit-linear-gradient( left, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
background-image: -moz-linear-gradient( left, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
background-image: -o-linear-gradient( left, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
background-image: linear-gradient(to right, rgba( 0,0,0,0.9 ) 25%, rgba( 0,0,0,0.1 ) 50%, rgba( 0,0,0,0.9 ) 75%);
color: white;
height: 100%;
width: 20"""
return pd.Series([tpl for i in s], index=s.index, name=s.name)
df.style.apply(cylon)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
Interacts pretty well with widgets. If you're viewing this on NBViewer you're missing out on some interactivity.
from IPython.html import widgets
@widgets.interact
def f(h_neg=(0, 359, 1), h_pos=(0, 359), s=(0., 99.9), l=(0., 99.9)):
return df.style.background_gradient(
cmap=sns.palettes.diverging_palette(h_neg=h_neg, h_pos=h_pos, s=s, l=l,
as_cmap=True)
)
A | B | C | D | E | |
---|---|---|---|---|---|
0 | 1.0 | 1.329212 | nan | -0.31628 | -0.99081 |
1 | 2.0 | -1.070816 | -1.438713 | 0.564417 | 0.295722 |
2 | 3.0 | -1.626404 | 0.219565 | 0.678805 | 1.889273 |
3 | 4.0 | 0.961538 | 0.104011 | -0.481165 | 0.850229 |
4 | 5.0 | 1.453425 | 1.057737 | 0.165562 | 0.515018 |
5 | 6.0 | -1.336936 | 0.562861 | 1.392855 | -0.063328 |
6 | 7.0 | 0.121668 | 1.207603 | -0.00204 | 1.627796 |
7 | 8.0 | 0.354493 | 1.037528 | -0.385684 | 0.519818 |
8 | 9.0 | 1.686583 | -1.325963 | 1.428984 | -2.089354 |
9 | 10.0 | -0.12982 | 0.631523 | -0.586538 | 0.29072 |
# Don't do this
def panda(df):
spots = [
# left ear
(0, 0), (1, 0), (0, 1),
# right ear
(0, 10), (1, 10), (0, 9),
# left eye
(5, 2), (4, 3), (5, 3),
# right eye
(5, 7), (4, 7), (5, 8),
# nose
(10, 4), (10, 5), (10, 6), (11, 5),
(15, 5), (16, 5), (17, 5), (17, 4), (17, 6), (17, 3), (17, 7), (16, 2), (16, 8)
]
rows = []
for r in df.index:
cols = []
for c in df.columns:
if (r, c) in spots:
cols.append("background-color: black; color: white")
else:
cols.append("background-color: white; color: black")
rows.append(cols)
return pd.DataFrame(rows)
df = pd.DataFrame(np.random.randn(20, 11))
s = df.round(2).style
s._update_ctx(panda(df))
s
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1.26 | 0.29 | -1.97 | 0.8 | 1.03 | 0.12 | -0.02 | 0.05 | -1.63 | -0.39 | 1.7 |
1 | 1.06 | 0.7 | -0.44 | -0.33 | 0.6 | 0.11 | 0.04 | -0.54 | 0.5 | -0.71 | -0.24 |
2 | 0.86 | -1.88 | 0.42 | -1.07 | -2.58 | -1.22 | -1.16 | 0.93 | 0.98 | 2.23 | -0.42 |
3 | -0.33 | -0.15 | 1.56 | 0.68 | 0.03 | -0.85 | 1.98 | -1.63 | -0.24 | -0.16 | 0.66 |
4 | -1.31 | 1.35 | -0.13 | -0.97 | -0.69 | -0.05 | -0.58 | 0.87 | -0.97 | 0.42 | 1.97 |
5 | 0.31 | 1.0 | 0.87 | -1.87 | 1.23 | -0.13 | 0.84 | 1.1 | 0.47 | 0.21 | -1.28 |
6 | 0.81 | 0.09 | 0.48 | 0.99 | -1.12 | 0.99 | 1.39 | -0.38 | -1.76 | 2.11 | 0.67 |
7 | 0.49 | 0.01 | -0.62 | -0.66 | 0.13 | 0.07 | -0.35 | -0.9 | 0.15 | -1.16 | -1.64 |
8 | -1.44 | -1.67 | 0.57 | -0.79 | 0.73 | 0.34 | 1.7 | 0.69 | 0.74 | -0.59 | -1.82 |
9 | 0.6 | 1.13 | -0.44 | -0.55 | -0.43 | -0.74 | 1.15 | -0.17 | 0.73 | 0.31 | -0.91 |
10 | 1.88 | -0.23 | -0.81 | 1.41 | 0.65 | -0.36 | -1.02 | -1.49 | -1.0 | 0.41 | -0.91 |
11 | 1.0 | -0.45 | 0.83 | -0.86 | 0.13 | 0.08 | -0.16 | -0.02 | -0.5 | -0.17 | 1.84 |
12 | 0.23 | 1.39 | 1.42 | 0.56 | 1.23 | 0.48 | 0.03 | -0.25 | -0.67 | -0.85 | 1.94 |
13 | 0.01 | 0.06 | 0.95 | -2.87 | 0.35 | 0.27 | 0.11 | -0.58 | -0.41 | 1.09 | 1.43 |
14 | 0.69 | 0.19 | 0.11 | -0.21 | -1.13 | 0.31 | -0.8 | -1.78 | -0.14 | -0.47 | -0.31 |
15 | -0.31 | -1.3 | -0.59 | -0.66 | -1.67 | 0.32 | 0.81 | -0.24 | -0.26 | -0.13 | 0.17 |
16 | -1.21 | -0.11 | -0.53 | -1.11 | -0.26 | -0.99 | -0.46 | 0.73 | 1.65 | -0.56 | -1.3 |
17 | 1.12 | -1.09 | 0.28 | 0.02 | 0.15 | -0.33 | -1.35 | 0.66 | -0.26 | -1.01 | 0.13 |
18 | -0.8 | 2.21 | 0.51 | -0.94 | -0.18 | -0.7 | -0.95 | -0.93 | -0.34 | 0.25 | -0.07 |
19 | -0.39 | 0.32 | 0.98 | -0.55 | 1.33 | 1.45 | -0.98 | 0.09 | -0.22 | 0.5 | 1.56 |