4

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!

Lutz
  • 86
  • 1
  • 8
  • reset_index can be a workaround - I thought of that. But just asking if it's also possible while keeping the multiindex – Lutz Jan 28 '21 at 14:45

3 Answers3

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

Attack68
  • 4,437
  • 1
  • 20
  • 40
  • Why is the green line `border-bottom` for (response1, model1)? It looks like a `border-bottom` for (response1, model4) as well to me. – data-monkey Sep 06 '22 at 15:12
  • 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. – Attack68 Sep 06 '22 at 16:19
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
Arthur
  • 115
  • 9
1

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
Kenneth
  • 11
  • 1