Pandas Style: Draw borders over whole row including the multiindex

Viewed 3190

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!

3 Answers
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

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

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
Related