Pandas Style: Draw borders over whole row including the multiindex
Asked Answered
K

3

5

I'm using pandas style in a jupyter notebook to emphasize the borders between subgroups in this dataframe:

(technically speaking: to draw borders at every changed multiindex but disregarding the lowest level)

# some sample df with multiindex
res = np.repeat(["response1","response2","response3","response4"], 4)
mod = ["model1", "model2","model3","model4"]*len(res)
data = np.random.randint(0,50,size=len(mod))
df = pd.DataFrame(zip(res,mod,data), columns=["res","mod","data"])
df.set_index(["res","mod"], inplace=True)

# set borders at individual frequency
indices_with_borders = range(0,len(df), len(np.unique(mod)))
df.style.set_properties(subset=(df.index[indices_with_borders], df.columns), **{
                      'border-width': '1px', "border-top-style":"solid"}) 

Result:

enter image description here

Now it looks a bit silly, that the borders are only drawn across the columns but not continue all the way through the multiindex. This would be a more pleasing style:

enter image description here

Does anybody know how / if it can be achieved? Thanks in advance!

Kiss answered 28/1, 2021 at 14:40 Comment(1)
reset_index can be a workaround - I thought of that. But just asking if it's also possible while keeping the multiindexKiss
P
5
s = df.style
for l0 in ['response1', 'response2', 'response3', 'response4']:
    s.set_table_styles({(l0, 'model4'): [{'selector': '', 'props': 'border-bottom: 3px solid red;'}],
                        (l0, 'model1'): [{'selector': '.level0', 'props': 'border-bottom: 3px solid green'}]},
                      overwrite=False, axis=1)
s

Because a multiindex sparsifies and spans rows you need to control the row classes with a little care. This is a bit painful but it does what you need...

enter image description here

Pilsen answered 22/2, 2021 at 19:23 Comment(4)
Why is the green line border-bottom for (response1, model1)? It looks like a border-bottom for (response1, model4) as well to me.Surefooted
Because the CSS hierarchy of specifying '.level0' (a css class) is higher than that for the red border (which would otherwise be applied instead of the green.Pilsen
Can we set to default style so that we do not need style it time to timeEngorge
You should define your own factory function which applies styles by default and then call that function: my_styler(df) instead of df.style.Pilsen
S
4
s = df.style
for idx, group_df in df.groupby('res'):
    s.set_table_styles({group_df.index[0]: [{'selector': '', 'props': 'border-top: 3px solid green;'}]}, 
                       overwrite=False, axis=1)
s

I took Attack68's answer and thought I would show how to make it more generic which can be useful if you have more levels in the multiindex. Allows you to groupby any level in the multiindex and adds a border at the top of that level. So if we wanted to do the same for the level mod we could also do:

df = df.sort_index(level=['mod'])
s = df.style
for idx, group_df in df.groupby('mod'):
    s.set_table_styles({group_df.index[0]: [{'selector': '', 'props': 'border-top: 3px solid green;'}]},
                       overwrite=False, axis=1)
s
Seer answered 14/9, 2021 at 12:51 Comment(0)
W
2

I found this to be the easiest solution to automatically add all lines for an arbitrarily deep multi-index:

df.sort_index(inplace=True)
s = df.style

for i, _ in df.iterrows():
    s.set_table_styles({i: [{'selector': '', 'props': 'border-top: 3px solid black;'}]}, overwrite=False, axis=1)

s
Westfall answered 3/7, 2022 at 14:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.