I have python code like so:
import dataframe_image as dfi
def color_negative_red(val):
color = f'rgba(255, 50, 50, {min(1, val / 350)})'
return f'background-color: {color}'
styled_df = df.style.applymap(color_negative_red, subset=pd.IndexSlice[:, pd.IndexSlice['Active (bps)', :]])
dfi.export(styled_df, 'test.png', table_conversion='matplotlib')
but it throws this error:
ValueError: Invalid RGBA argument: 'rgba(255, 50, 50, 0.002857)'
df has MultiIndex columns and I want to color only "Active (bps)" columns:
import pandas as pd
data = {
('Act', 'bps'): [-14, 341, -14],
('Dur', 'bps'): [49, 379, 50],
('Active (bps)', '3M'): [1, -7, 3],
('Active (bps)', '1Y'): [3, 3, 4],
('Active (bps)', '2Y'): [14, 10, -36],
('Active (bps)', '3Y'): [118, 105, -59],
('Active (bps)', '5Y'): [-295, 205, 68],
('Active (bps)', '7Y'): [101, 25, 5]
}
df = pd.DataFrame(data)
df.index = ['NO2', 'BSB', 'GOB']
df.columns = pd.MultiIndex.from_tuples(df.columns)
print(df)
Act Dur Active (bps)
(bps) (bps) 3M 1Y 2Y 3Y 5Y 7Y
NO2 -14 49 1 3 14 118 -295 101
BSB 341 379 -7 3 10 105 205 25
GOB -14 50 3 4 -36 -59 68 5
Option 1: use default table_conversion='chrome'
As specified in the doc string for dfi.export
, under table_conversion
:
Use chrome unless you cannot get it to work. matplotlib provides a decent alternative.
If you run into issues using the default, the fix might be this. The latest version of dfi
, released yesterday (!), actually includes it (see here), so upgrading is surely worth a try.
The point being that your code, with minor corrections, should work fine with 'chrome'
:
import dataframe_image as dfi
def color_negative_red(val):
# minimal adjustment here: you need `max` to clip the lower bound at 0!
color = f'rgba(255, 50, 50, {max(0, min(1, val / 350))})'
return f'background-color: {color}'
# using `map` instead of `applymap`, which is deprecated
styled_df = df.style.map(color_negative_red,
subset=pd.IndexSlice[:, pd.IndexSlice['Active (bps)', :]])
dfi.export(styled_df, 'test.png')
Result:
Option 2: use default table_conversion='matplotlib'
One can get this option to work, but the result might still not be as desired, since matplotlib
has a different way of handling the column labels (and then the column width). See the result below.
At any rate, the main problem with our "decent alternative" is that in the conversion to mpl
, you will enter this block of code, which expects the use of hex
colors instead of an rgba
string. This explains the somewhat cryptic error:
ValueError: Invalid RGBA argument: 'rgba(255, 50, 50, 0.002857)'
What is meant, is that the expected value (for variable c
, to be used for a regex
match) is something like '#fffefe'
. To accommodate this, we need to convert your rgba
colors into hex
. This is a little bit tricky: to get from rgba
to hex
, we need to blend the rgb
value with the rgb
value of some background. Now, the background for each of your rows will be slightly different:
rgb(245, 245, 245)
: light grey for odd rowsrgb(255, 255, 255)
: white for even rows(assuming use of default css setttings for Jupyter Notebook.)
Let's define a helper function then, to get from rgba
to hex
:
def rgba_to_hex_alpha(rgba, background=(255, 255, 255)):
# unpack rgb + alpha
*rgb, a = rgba
# blended rgb
blended_rgb = [int((1 - a) * i2 + a * i1) for i1, i2 in zip(rgb, background)]
return '#{:02x}{:02x}{:02x}'.format(*blended_rgb)
We will be calling this function inside a new version of color_negative_red
, which we use inside df.style.apply
. Note the use of helper=range(len(df))
below, which we pass alongside each column (axis=0
) of the subset, to help decide whether we are looking at a value for an odd row or an even one.
def color_negative_red(row, helper):
vals = []
# loop over `helper` and `row` simultaneously
for idx, val in zip(helper, row):
# `True` for row 0, 2, etc. (odd row!)
if idx % 2 == 0:
background = (245, 245, 245)
else:
background = (255, 255, 255)
alpha = max(0, min(1, val / 350))
color = rgba_to_hex_alpha(rgba=(255,50,50, alpha), background=background)
bg = f'background-color: {color}'
vals.append(bg)
return vals
styled_df = df.style.apply(color_negative_red,
helper=range(len(df)),
axis=0,
subset=pd.IndexSlice[:, pd.IndexSlice['Active (bps)', :]]
)
dfi.export(styled_df, 'test_matplotlib.png', table_conversion='matplotlib')
Result:
(Incidentally, using dfi.export
with default 'chrome'
on this method will lead to the exact same result as shown at the end of option 1.)