1

I have a questionnaire where users answers with scores from 1 to 7 (Likert scale). The questionnaire is divided in two sections. The users belongs to two groups (X and Y), and each user may have one of two roles (A and B). I am using seaborn heatmap on a FacetGrid to show the results of the questionnaire.

Here is my code:

import pandas as pd
import seaborn as sns

df = pd.DataFrame(
    data={
        'Group': ['X', 'Y', 'Y', 'X', 'Y', 'X', 'Y', 'Y', 'X', 'X'],
        'Role':  ['A', 'B', 'A', 'A', 'B', 'B', 'A', 'A', 'A', 'B'],        
        'Question 1': [3,6,5,5,6,6,4,5,7,5],
        'Question 2': [7,7,5,6,4,4,4,4,7,5],
        'Question 3': [6,5,3,5,7,7,6,5,4,4],
        'Question 4': [6,3,4,5,5,7,6,5,4,4]
    }
)

def f(group):
    gg = group[group.columns[-4:]].T.apply(lambda row : row.value_counts(), axis=1)
    for score in range(1, 8):
        if score not in gg:
            gg[score] = 0.0
    return gg

df1 = df.groupby(['Group', 'Role']) \
        .apply(f) \
        .fillna(0) \
        .reset_index() \
        .rename(columns={'level_2':'Question'})

fg = sns.FacetGrid(
    data=df1,
    row='Group',
    col='Role'
)

def draw_heatmap(*args, **kwargs):
    data = kwargs.pop('data')
    d = data[['Question', 1, 2, 3, 4, 5, 6, 7]] \
            .melt(id_vars='Question', var_name="Likert Score", value_name="Count") \
            .pivot(index="Question", columns="Likert Score", values="Count")
    d = d.div(d.sum(axis=1), axis=0).round(2)
    sns.heatmap(d, **kwargs)
    
fg.map_dataframe(
    draw_heatmap, 
    cbar_ax=fg.fig.add_axes([1, 0.3, .02, .4]),
    cbar_kws={'label': 'Percentage of responses'},
    vmin=0,
    vmax=1,
    cmap="Blues",
    linewidths=.1
)

And this is the output:

Output of my code

I would like to show which section of the questionnaire the questions belong to, ideally something like the following:

Desired output

I have seen this question, but I have not been able to apply the suggested solution to my case.

Any help is highly appreciated. Thank you!

MarcoS
  • 13,386
  • 7
  • 42
  • 63

1 Answers1

1

I found a solution; I am not sure it is the best solution, but I am sharing in case someone else needs to do the same or something similar.

from typing import List            
                 
def draw_labels_groups(groups: List[List[int]], groups_labels: List[str]) -> None:    
    delta_x = 0.08
    delta_y = 0.08   
    for index, ax in enumerate(fg.axes.flat):
        if index % 2 == 0:  # only modify axis on the left column (assuming we have 2 columns)       
            yticklabels = ax.get_yticklabels()
            r = ax.figure.canvas.get_renderer()
            # get bounding boxes of y tick labels
            bounding_boxes = [t.get_window_extent(renderer=r).transformed(ax.transAxes.inverted()) \
                              for t in yticklabels]
            # compute left-most x coordinates of all bounding boxes of y tick labels
            # xmin will be the right-most x coordinate of the grouping line
            xmin = min([bb.get_points()[0][0] for bb in bounding_boxes])
            # we need to move the label for the yaxis to the left, and x_coordinate_for_yaxis_label
            # will be its coordinate
            x_coordinate_for_yaxis_label = 0
            # draw every group
            for group_index, group in enumerate(groups):
                first, last = group[0], group[1]
                (x0f, y0f), (x1f, y1f) = bounding_boxes[first].get_points()
                (x0l, y0l), (x1l, y1l) = bounding_boxes[last].get_points()
                a = (xmin, y1f + delta_y)
                b = (xmin - delta_x, y1f + delta_y)
                c = (xmin - delta_x, y0l - delta_y)
                d = (xmin, y0l - delta_y)
                # uncomment the following four lines if you want to see 
                # positions of points a, b, c, and d
#                 ax.text(a[0], a[1], "a", ha='center', va='center', transform=ax.transAxes) 
#                 ax.text(b[0], b[1], "b", ha='center', va='center', transform=ax.transAxes)                 
#                 ax.text(c[0], c[1], "c", ha='center', va='center', transform=ax.transAxes)                 
#                 ax.text(d[0], d[1], "d", ha='center', va='center', transform=ax.transAxes)                
                line = plt.Line2D([a[0], b[0], c[0], d[0]], [a[1], b[1], c[1], d[1]],
                                  color='black', 
                                  transform=ax.transAxes, 
                                  linewidth=0.5)
                line.set_clip_on(False)
                ax.add_line(line)
                t = ax.text(
                    b[0] - delta_x * 1.5,
                    c[1] + (b[1] - c[1]) / 2,
                    textwrap.fill(groups_labels[group_index], 10),
                    ha='center', 
                    va='center',
                    rotation=90,
                    wrap=True,
                    transform=ax.transAxes
                )
                x_coordinate_for_yaxis_label = min(
                    x_coordinate_for_yaxis_label, 
                    t.get_window_extent(renderer=r).transformed(ax.transAxes.inverted()).get_points()[0][0])
            # move the label of the y axis
            ax.yaxis.set_label_coords(x_coordinate_for_yaxis_label - delta_x, 0.5)
             

Calling for example draw_labels_groups([[0,2], [3,3]], ["Section 1", "Section 2 with long label"]) on the example in my question, yields the following result (Note: I have modified the values of yticklables in the original example adding a long one, to demonstrate how the code works)

enter image description here

MarcoS
  • 13,386
  • 7
  • 42
  • 63