I have the following dataframe, df
:
data = {'person': {0: 'a',
1: 'a',
2: 'a',
3: 'a',
4: 'a',
5: 'a',
6: 'b',
7: 'b',
8: 'b',
9: 'b',
10: 'b',
11: 'b',
12: 'c',
13: 'c',
14: 'c',
15: 'c',
16: 'c',
17: 'c'},
'x': {0: 1,
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1,
7: 1,
8: 1,
9: 1,
10: 1,
11: 1,
12: 1,
13: 1,
14: 1,
15: 1,
16: 1,
17: 1},
'y': {0: 2,
1: 2,
2: 2,
3: 2,
4: 2,
5: 2,
6: 2,
7: 2,
8: 2,
9: 2,
10: 2,
11: 2,
12: 2,
13: 2,
14: 2,
15: 2,
16: 2,
17: 2},
'z': {0: 'foo',
1: 'foo',
2: 'foo',
3: 'bar',
4: 'bar',
5: 'bar',
6: 'foo',
7: 'foo',
8: 'foo',
9: 'bar',
10: 'bar',
11: 'bar',
12: 'foo',
13: 'foo',
14: 'foo',
15: 'bar',
16: 'bar',
17: 'bar'}}
df = pd.DataFrame.from_dict(data, orient='columns')
And I want to style groups of rows differently (in different sets of alternating colors) based on the value of z
FOR EACH VALUE OF person
.
Initially, I figured I could use a nested loop to break out each z
for each person
. I tried testing this out initially for just one person
, like so:
COLORS = {
'foo':['red','green'],
'bar':['blue','yellow']
}
test = df.loc[df.person=='a'].copy()
sub_person = pd.DataFrame()
for val in test.z.unique():
i_test = test.loc[test.z==val].copy()
c1 = COLORS[val][0]
c2 = COLORS[val][-1]
css_alt_rows = f'background-color: {c1}; color: {c2};'
i_test = (i_test.style.apply(lambda col: np.where(col.index % 2, css_alt_rows,None)))
sub_person = pd.concat([sub_person,i_test])
I thought this was a clever solution to handle the different stylings individually, but I was met with the error:
TypeError: cannot concatenate object of type '<class 'pandas.io.formats.style.Styler'>'; only Series and DataFrame objs are valid
So, turns out this code cannot work as you cannot concat Styler objects.
Next, I tried a similar strategy by nesting the lambda function with another np.where()
conditional:
COLORS = {
'foo':['red','green'],
'bar':['blue','yellow']
}
test = df.loc[df.person=='a'].copy()
for val in test.z.unique():
c1 = COLORS[val][0]
c2 = COLORS[val][-1]
css_alt_rows = f'background-color: {c1}; color: {c2};'
test = (test.style.apply(lambda col: np.where(np.where(col.index % 2, css_alt_rows,None),None)))
But I get the following error:
AttributeError: 'Styler' object has no attribute 'style'
Which makes sense as after the first iteration of the loop, test
is a styler object, which results in test.style
raising an error in the next iteration.
So, how would I go about applying these stylings for each z
for each person
?
Also, how could I add a bottom border in the last row of each person
without being able to style groupings individually and concatenating them?
Note: Yes, the colors in colors
won't match the image, which is fine.
Another possibility is to do a background_gradient
with a custom ListedColormap
:
from operator import itemgetter
from matplotlib.colors import ListedColormap
colors = {"y": ["#fff2cc", "#ffe59a"], "b": ["#d0e1e2", "#a2c4c9"]}
cmap = ListedColormap(colors["y"] + colors["b"])
ng = df.groupby(["person", "z"], sort=False).ngroup()
gmap = ng.groupby(ng).cumcount() % 2 + ng % 2 * 2 # in a mozway
border_css = {"selector": "td", "props": "border-bottom: 3px solid black;"}
st = (
df.style.background_gradient(gmap=gmap, cmap=cmap)
.set_table_styles(
{
idx: [border_css]
for idx in map(
itemgetter(-1),
df.groupby("person").indices.values(),
)
}, axis=1,
).set_properties(**{"text-align": "right", "width": "110px"})
.hide(axis=0)
)