Discrete Heatmap, change the cell opacity depending on variable (seaborn?)

47 Views Asked by At

I'd like to make an heatmap featuring both the continuous variable and the categorical data the sample came with. The goal is to have the hue coming from the category and the opacity (or transparency, saturation) from the continuous values.

A toy dataset would be like:

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

names = ['Berthe', 'Paul', 'Francis', 'Jane']

time_spent_together = [
    [ 1.0, 0.2, 0.7, 0.5 ],
    [ 0.2, 1.0, 0.8, 0.5 ],
    [ 0.7, 0.8, 1.0, 0.1 ],
    [ 0.5, 0.5, 0.1, 1.0 ],
]

type_of_relationship = [
    [ 'id', 'friends', 'coworkers', 'nemesis' ],
    [ 'friends', 'id', 'family', 'family' ],
    [ 'coworkers', 'family', 'id', 'friends' ],
    [ 'nemesis', 'family', 'friends', 'id' ],
]

df_times = pd.DataFrame(data=time_spent_together, index=names, columns=names)
df_relationships = pd.DataFrame(data=type_of_relationship, index=names, columns=names)

And the result would 'alter' the discrete plot:

plt.figure(figsize=(3,3))
value_to_int = {j:i for i,j in enumerate(pd.unique(df_relationships.values.ravel()))}
n = len(value_to_int)
cmap = sns.color_palette("tab10", n) 
sns.heatmap(df_relationships.replace(value_to_int), cmap=cmap)

enter image description here

With the continuous one

plt.figure(figsize=(3,3))
sns.heatmap(df_times, cmap='Greys',annot=True, 
            annot_kws={"size": 7}, vmin=0.25, vmax=1)

enter image description here

As you can see, I used seaborn and pyplot. I struggle to override the basic behavior. Being able to directly set a cell's color can be the right path ?

Thanks in advance for your answers, Cheers!

2

There are 2 best solutions below

1
JohanC On BEST ANSWER

Here is an approach using multiple mono-color colormaps, together with a legend:

import matplotlib.pyplot as plt
from matplotlib.cm import ScalarMappable
import seaborn as sns
import pandas as pd
import numpy as np

fig, ax = plt.subplots(figsize=(6, 6))

cmaps = ['Blues', 'Oranges', 'Greens', 'Reds', 'Purples']
norm = plt.Normalize(vmin=0.25, vmax=1)
handles = []
for rel, cmap in zip(np.unique(df_relationships.values), cmaps):
    sns.heatmap(df_times, mask=df_relationships.values != rel, cmap=cmap, norm=norm, annot=True, cbar=False)
    handles.append(plt.Rectangle((0, 0), 0, 0, color=plt.get_cmap(cmap)(0.55), lw=0, label=rel))

plt.colorbar(ScalarMappable(cmap='Greys', norm=norm), ax=ax)
ax.legend(handles=handles, ncol=len(handles), bbox_to_anchor=(0, 1.01), loc='lower left', handlelength=0.7)
plt.tight_layout()
plt.show()

sns.heatmap with multiple color ranges

1
mozway On

You could plot the two graphs on top of each other, one with the colors and one with a cmap that goes from transparent to white:

import matplotlib as mpl
import matplotlib.colors as mcolors

plt.figure(figsize=(4, 3))

# categories
value_to_int = {j:i for i,j in enumerate(pd.unique(df_relationships.values.ravel()))}
n = len(value_to_int)
cmap = sns.color_palette("tab10", n) 
ax = sns.heatmap(df_relationships.replace(value_to_int), cmap=cmap)

# values
c_alpha = mcolors.colorConverter.to_rgba('white', alpha=0)
c_white = mcolors.colorConverter.to_rgba('white', alpha=1)
alpha_cmap = mcolors.LinearSegmentedColormap.from_list('alpha_cmap', [c_alpha,c_white], 512)
ax2 = sns.heatmap(df_times.rsub(1), cmap=alpha_cmap, annot=df_times, 
            annot_kws={"size": 7}, vmin=0.25, vmax=1, ax=ax, cbar=None)
cbar = ax.figure.colorbar(mpl.cm.ScalarMappable(mcolors.Normalize(vmin=0, vmax=1), cmap='Greys'), ax=ax2)
cbar.ax.set_ylim(bottom=0.25)

Note that the perception of the lightness will depend on each color so it might be difficult to map an exact absolute value when reading the graph.

Output:

enter image description here