Search code examples
pythonpandasjupyter-notebookmulti-indexpandas-styles

How to apply condition styling to a multi-level dataframe?


I have the following dataframe:

dic = {'US':{'Quality':{'points':"-2 n", 'difference':'equal', 'stat': 'same'}, 'Prices':{'points':"-7 n", 'difference':'negative', 'stat': 'below'}, 'Satisfaction':{'points':"3 n", 'difference':'positive', 'stat': 'below'}},
      'UK': {'Quality':{'points':"3 n", 'difference':'equal', 'stat': 'above'}, 'Prices':{'points':"-13 n", 'difference':'negative', 'stat': 'below'}, 'Satisfaction':{'points':"2 n", 'difference':'negative', 'stat': 'same'}}}
d1 = defaultdict(dict)
for k, v in dic.items():
    for k1, v1 in v.items():
        for k2, v2 in v1.items():
            d1[(k, k2)].update({k1: v2})

df = pd.DataFrame(d1)
print(df)

I have to format the style of the cells as per the logic below:

#1. 
# If the difference is negative, then the font color of the points cell is red, 
# else if the difference is equal, then the font color of the points cell is grey

#2. 
#If stat is below or above, then the font color of the stat cell is red and the background color is blue

The output should look similar to this:enter image description here


Solution

  • You can apply conditional styling on your multi-level dataframe by first defining the following helper functions:

    # 1. If the difference is negative, then the font color of the points cell is red,
    # else if the difference is equal, then the font color of the points cell is grey
    
    
    def red_if_negative(s, **kwargs):
        return ["color: red" if v == "negative" else "" for v in df[(col, "difference")]]
    
    
    def grey_if_equal(s, **kwargs):
        return ["color: lightgrey" if v == "equal" else "" for v in df[(col, "difference")]]
    
    
    # 2. If stat is below or above, then the font color of the stat cell is red
    # and the background color is blue.
    
    
    def red_on_blue_if_below_or_above(s, **kwargs):
        is_above = (s == "below") | (s == "above")
        return [
            "background-color: lightsteelblue; color: red" if v else "" for v in is_above
        ]
    

    Then:

    styler = df.style
    for (func, subset, col) in [
        (red_on_blue_if_below_or_above, [("US", "stat"), ("UK", "stat")], None),
        (red_if_negative, [("US", "points")], "US"),
        (red_if_negative, [("UK", "points")], "UK"),
        (grey_if_equal, [("US", "points")], "US"),
        (grey_if_equal, [("UK", "points")], "UK"),
    ]:
        syler = styler.apply(func, subset=subset, col=col)
    
    styler
    

    Outputs in a Jupyter notebook:

    enter image description here