pythonmatplotlibseabornheatmapgrayscale

How to set heatmap to grayscale and annotate with a mask


I have a matrix A:

A = np.array([[ 0.        ,  0.00066748, -0.00097412, -0.00748846,  0.00305338],
       [-0.00157652,  0.        ,  0.0048117 ,  0.01069083, -0.0137888 ],
       [-0.00713212, -0.00170574,  0.        ,  0.00096385,  0.00212367],
       [-0.00186541,  0.00351104, -0.00590608,  0.        , -0.00448311],
       [-0.00929146,  0.00157808,  0.01300444, -0.00078593,  0.        ]])

With the following code I create a heatmap that assigns blank (white) to zero values, and green for positive values and red for negative ones:

import matplotlib.pyplot as plt
import seaborn as sns

rdgn = sns.diverging_palette(h_neg=10, h_pos=130, s=99, l=55, sep=3, as_cmap=True)
fig = plt.figure()
sns.heatmap(A, mask=(A == 0), cmap=rdgn, center=0)
plt.xticks(np.arange(0, 5, 1) + 0.5, [i + 1 for i in range(5)])
plt.yticks(np.arange(0, 5, 1) + 0.5, [i + 1 for i in range(5)], rotation=0)
plt.tick_params(axis='both', which='major', labelsize=10, labelbottom=False, bottom=False, top=False, labeltop=True)
plt.show()

I obtain this:

enter image description here

Now my goal is to convert this picture to greyscale only. One way would be to assign darker colors to reds and lighter to greens, but I would like to highlight the transition from negative to positive making the zero values very different from others, ideally keeping them blank. If I try with

fig = plt.figure()
sns.heatmap(A, mask=(A == 0), cmap = 'gray', center = 0)
plt.xticks(np.arange(0, 5, 1) + 0.5, [i + 1 for i in range(5)])
plt.yticks(np.arange(0, 5, 1) + 0.5, [i + 1 for i in range(5)], rotation=0)
plt.tick_params(axis='both', which='major', labelsize=10, labelbottom=False, bottom=False, top=False, labeltop=True)
plt.show()

enter image description here I do not get what I want. With mask==0 I can keep zeros to blank, but this is not reflected in the colorbar on the right. What I would like to do is basically "split" the colors into 2: from pitch black to "half" grey for negatives (from the farthest away to the closest to zero), white for zeros and from white to "half" grey again for positive (from the closest to farthest away from zero). Is there a way to achieve this? I am open to any suggestion on how to tackle the problem


Solution

  • The idea of having a gradient of color is to be able to compare the values directly. It might be difficult to compare the absolute values if different shades of grey are used for negative/positive.

    Instead, why not plot the absolute value and add an additional information to tell negative and positive values apart?

    sns.heatmap(abs(A), annot=np.where(A<0, '−', ''), fmt='', cmap = 'gray_r', vmin=0)
    

    enter image description here

    Or:

    sns.heatmap(abs(A), annot=np.select([A<0, A>0], ['−', '+'], ''),
                fmt='', cmap = 'gray_r', vmin=0)
    

    enter image description here

    using symbols / hatches

    You can use unicode symbols for easier visualization:

    sns.heatmap(abs(A), annot=np.where(A<0, '▽', ''), fmt='',
                cmap = 'gray_r', vmin=0, annot_kws={'size': 30})
    

    Output:

    enter image description here

    You can also add hatches if you want as shown here:

    sns.heatmap(abs(A), cmap = 'gray_r', vmin=0)
    zm = np.ma.masked_greater_equal(A, 0)
    plt.pcolor(np.arange(A.shape[0])+0.5,
               np.arange(A.shape[1])+0.5,
               zm, hatch='//', alpha=0, zorder=3)
    

    Output:

    enter image description here