'Showing different size circles in heatmap with legend using Matplotlib

I am asking a question stemming from this original post Heatmap with circles indicating size of population

I am trying to replicate this using my dataframe, however, my circles are non aligning to the plot. Secondary, I want to also create a legend which indicates the value relative to the size of circle.

   x= {'ID': {0: 'GO:0002474',
      1: 'GO:0052548',
      2: 'GO:0002483',
      3: 'GO:0043062',
      4: 'GO:0060333'},
     'TERM': {0: 'antigen processing and presentation of peptide antigen via MHC class I',
      1: 'regulation of endopeptidase activity',
      2: 'antigen processing and presentation of endogenous peptide antigen',
      3: 'extracellular structure organization',
      4: 'interferon-gamma-mediated signaling pathway'},
     'Count': {0: 11, 1: 17, 2: 5, 3: 15, 4: 6},
     'Ratio': {0: 18.64, 1: 14.53, 2: 8.47, 3: 12.82, 4: 10.17},
     'pvalue': {0: -15.83, 1: -11.39, 2: -9.67, 3: -9.05, 4: -7.41},
     'qvalue': {0: -11.63, 1: -7.49, 2: -6.52, 3: -5.63, 4: -4.55},
     'Label': {0: 'NODAL', 1: 'NODAL', 2: 'NODAL', 3: 'SHARED', 4: 'NODAL'}}

A2780_GOBP= pd.DataFrame(x)

Attempted Code:

ylabels = A2780_GOBP["TERM"]
xlabels = ["GFP","SHARED","NODAL"]
x, y = np.meshgrid(np.arange(len(xlabels)), np.arange(len(ylabels)))
s = A2780_GOBP["Count"].values
c = A2780_GOBP["pvalue"].values

fig, ax = plt.subplots()

R = s/s.max()/2
circles = [plt.Circle((j,i), radius=r) for r, j, i in zip(R.flat, x.flat, y.flat)]
col = PatchCollection(circles, array=c.flatten(), cmap=cmap)
ax.add_collection(col)

ax.set(xticks=np.arange(3), yticks=np.arange(10),
       xticklabels=xlabels, yticklabels=ylabels)
ax.set_xticks(np.arange(3+1)-0.5, minor=True)
ax.set_yticks(np.arange(10+1)-0.5, minor=True)
ax.grid(which='minor')


fig.colorbar(col)
plt.show()

Output

Any help would be greatly appreciated!



Solution 1:[1]

The problem is here that the copied code fills all fields, whereas your code not necessarily has an entry in each box. We have to look up, where each circle has to be plotted:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
import pandas as pd

x= {'ID': {0: 'GO:0002474',
      1: 'GO:0052548',
      2: 'GO:0002483',
      3: 'GO:0043062',
      4: 'GO:0060333'},
     'TERM': {0: 'antigen processing and presentation of peptide antigen via MHC class I',
      1: 'regulation of endopeptidase activity',
      2: 'antigen processing and presentation of endogenous peptide antigen',
      3: 'extracellular structure organization',
      4: 'interferon-gamma-mediated signaling pathway'},
     'Count': {0: 11, 1: 17, 2: 5, 3: 15, 4: 6},
     'Ratio': {0: 18.64, 1: 14.53, 2: 8.47, 3: 12.82, 4: 10.17},
     'pvalue': {0: -15.83, 1: -11.39, 2: -9.67, 3: -9.05, 4: -7.41},
     'qvalue': {0: -11.63, 1: -7.49, 2: -6.52, 3: -5.63, 4: -4.55},
     'Label': {0: 'NODAL', 1: 'GFP', 2: 'NODAL', 3: 'SHARED', 4: 'NODAL'}}

A2780_GOBP= pd.DataFrame(x)
cmap = "plasma"
 
#retrieve unique labels
ylabels = A2780_GOBP["TERM"].unique().tolist()
xlabels = A2780_GOBP["Label"].unique().tolist()
xn = len(xlabels)
yn = len(ylabels)
#retrieve size and color information    
s = A2780_GOBP["Count"].values
c = A2780_GOBP["pvalue"].values


#preparation of the figure with its grid
fig, ax = plt.subplots(figsize=(10, 5))
ax.set_xlim(-0.5, xn-0.5)
ax.set_ylim(-0.5, yn-0.5)
ax.set(xticks=np.arange(xn), yticks=np.arange(yn),
       xticklabels=xlabels, yticklabels=ylabels)
ax.set_xticks(np.arange(xn)-0.5, minor=True)
ax.set_yticks(np.arange(yn)-0.5, minor=True)
ax.grid(which='minor')
#ensure circles are displayed as circles
ax.set_aspect("equal", "box")

#create circles patches and colorbar
R = s/s.max()/2
circles = [plt.Circle((xlabels.index(A2780_GOBP.loc[i, "Label"]), ylabels.index(A2780_GOBP.loc[i, "TERM"])), radius=r) for i, r in enumerate(R)]
col = PatchCollection(circles, array=c, cmap=cmap)
ax.add_collection(col)
fig.colorbar(col)

plt.show()

Sample output:

enter image description here

The code does not check the integrity of your original database, i.e., that each Label-Term pair indeed only occurs once.

Solution 2:[2]

Adapted answer for @Mr. T to include legend generator

from matplotlib.legend_handler import HandlerPatch
import matplotlib.patches as mpatches

ylabels = A2780_GOBP["TERM"].unique().tolist()
xlabels = A2780_GOBP["Label"].unique().tolist()
xn = len(xlabels)
yn = len(ylabels)    
s = A2780_GOBP["Count"].values
c = A2780_GOBP["pvalue"].values

fig, ax = plt.subplots(figsize=(20,10))
ax.set_xlim(-0.5, xn-0.5)
ax.set_ylim(-0.5, yn-0.5)
ax.set(xticks=np.arange(xn), yticks=np.arange(yn), yticklabels=ylabels)
ax.set_xticklabels(xlabels, rotation='vertical')
ax.set_xticks(np.arange(xn)-0.5, minor=True)
ax.set_yticks(np.arange(yn)-0.5, minor=True)
ax.grid(which='minor')
ax.set_aspect("equal", "box")

R = s/s.max()/2
circles = [plt.Circle((xlabels.index(A2780_GOBP.loc[i, "Label"]), ylabels.index(A2780_GOBP.loc[i, "TERM"])), radius=r) for i, r in enumerate(R)]
col = PatchCollection(circles, array=c, cmap=cmap)
sc=ax.add_collection(col)
cbar=fig.colorbar(col).set_label('$-log_{10}(p-value)$', rotation=270, size=16,labelpad=15)

smax=s.max()
smin=s.min()
smid=(smax+smin)/2
texts = ["3","10","17"]


class HandlerEllipse(HandlerPatch):
    def create_artists(self, legend, orig_handle,
                       xdescent, ydescent, width, height, fontsize, trans):
        center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
        p = mpatches.Ellipse(xy=center, width=orig_handle.width,
                                        height=orig_handle.height)
        self.update_prop(p, orig_handle, legend)
        p.set_transform(trans)
        return [p]
    
c = [mpatches.Ellipse((), width=smin, height=smin, color="grey"),
     mpatches.Ellipse((), width=smid, height=smid, color="grey"),
     mpatches.Ellipse((), width=smax, height=smax, color="grey"),
    ]

legend = ax.legend(c,texts, handler_map={mpatches.Ellipse: HandlerEllipse()},title="Number of Proteins",bbox_to_anchor=(3.50, 0.82, 1.0, .102),fontsize="large")
plt.setp(legend.get_title(),fontsize='large')
plt.show()

Output: output

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2