I'm creating a GUI where there is live 'point in time' data for several records ("things") and fields. records are comparable based on field, but fields are not necessarily related (at least not on same scale). For my eventual GUI, I want to have the main page be a heatmap (really a bunch of 1-D heatmaps based on columns (fields), then if you click on one it will give a time series history and some other charts.
Anyway, what I'm after here, is trying to get the initial heatmap to show the way I want. As of now I can get the pcolormesh from Matplotlib to essentially show individual 1-D heatmaps based on field by hacking it and heatmapping based on percentile of column, and then adding text of the actual value on top.
But, as I said, fields are not necessarily related, and I would like to be able to have individual colormaps for each field. For example: say Fields 3 and 4 are qualitatively related to one another, but not to Fields 0-3 - so it would be nice to have those mapped to say the 'greens' colormesh rather than 'coolwarm'.
Here is my code so far and resulting pcolormesh/heatmap:
import pandas as pd
import matplotlib.pyplot as plt
def DFPercentiles(df,bycols=True):
p=pd.DataFrame(index=df.index,columns=df.columns)
if bycols!=True:
for j in df.index:
for i in df.columns:
p.loc[j,i]=(df.loc[j,i]-min(df.loc[j,:]))/(max(df.loc[j,:])-min(df.loc[j,:]))
else:
for i in df.index:
for j in df.columns:
p.loc[i,j]=(df.loc[i,j]-min(df.loc[:,j]))/(max(df.loc[:,j])-min(df.loc[:,j]))
return p
def Heatmap(df,figsize='auto'):
if figsize=='auto':
figsize=[shape(df)[1],shape(df)[0]/2]
fig=figure(figsize=figsize)
pdf=array(DFPercentiles(df,bycols=True)).astype(float)[::-1]
plt.pcolormesh(pdf,cmap=cm.coolwarm,alpha=0.8)
plt.yticks(arange(0.5,len(df)),df.index[::-1])
plt.xticks(arange(0.5,len(df.columns)),df.columns)
for y in range(df.shape[0]):
for x in range(df.shape[1]):
plt.text(x + 0.5, y + 0.5, '%.3f' % df[::-1].iloc[y, x],
horizontalalignment='center',
verticalalignment='center',
)
return plt
hmap=Heatmap(mydf)
hmap.show()
And the result:
I've had no luck trying to get multiple colormaps for separate fields.
Each colormesh plot has one colormap associated to it. In order to use several colormaps in one diagram, I therefore see the following options:
- Individual rectangles:
Don't use pcolormesh but draw individual rectangles in the color of your liking.
Create your custom colormap which incorporates different colormaps within different ranges. E.g. values from 0 to 0.4 are mapped to colors from one colormap and values from 0.4 to 1 to colors from another colormap. This may then look like:
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
x,y = np.meshgrid(range(4), range(4))
z = np.array([[0.2,.3,.95],[.5,.76,0.4],[.3,.1,.6]]).astype(float)
mask= np.array([[1,0,0],[1,0,0],[1,1,1]]).astype(float)
Z = z + mask
c2 = plt.cm.Greens(np.linspace(0,1,128))
c1 = plt.cm.coolwarm(np.linspace(0,1,128))
cols = np.vstack((c1, c2))
cmap=matplotlib.colors.LinearSegmentedColormap.from_list("q", cols)
fig, ax=plt.subplots()
ax.pcolormesh(x,y,Z, vmin=0, vmax=2, cmap=cmap)
plt.show()
Mask the arrays and plot several pcolormesh plots. The following example shows how this might then look like:
import matplotlib.pyplot as plt
import numpy as np
import numpy.ma as ma
x,y = np.meshgrid(range(4), range(4))
z = np.array([[1,1.3,3],[2.2,2.8,1.8],[3,1,3]]).astype(float)
mask= np.array([[1,0,0],[1,0,0],[1,1,1]]).astype(bool)
z1 = np.copy(z)
z1[mask] = np.nan
z2 = np.copy(z)
z2[~mask] = np.nan
fig, ax=plt.subplots()
ax.pcolormesh(x,y,ma.masked_invalid(z1), vmin=1, vmax=3, cmap="coolwarm")
ax.pcolormesh(x,y,ma.masked_invalid(z2), vmin=1, vmax=3, cmap="Greens")
plt.show()
Implementation of @ImportanceOfBeingErnest solution:
Used the stacking of colormaps to create a custom colormap of maps. Basically my function now takes tuples of ([list of columns],multiplier,colormap to apply) and stacks the unique colormaps and groups the percentile data to match the individual colormaps (endpoints were tricky to avoid overlap of coloring). I probably didn't implement it super efficiently, but it works well:
def DFPercentiles_hmapshift(df,bycols=True):
p=pd.DataFrame(index=df.index,columns=df.columns)
if bycols!=True:
for j in df.index:
for i in df.columns:
pct=(df.loc[j,i]-min(df.loc[j,:]))/((max(df.loc[j,:])-min(df.loc[j,:]))*1.)
pct=pct-(pct-0.5)*1./40 #have to rescale it to account for endpoints of cmaps
p.loc[j,i]=pct
#print '('+str(max(p.loc[j,:]))+', '+str(min(p.loc[j,:]))+')'
else:
for i in df.index:
for j in df.columns:
pct=(df.loc[i,j]-min(df.loc[:,j]))/((max(df.loc[:,j])-min(df.loc[:,j]))*1.)
pct=pct-(pct-0.5)*1./40 #have to rescale it to account for endpoints of cmaps
p.loc[i,j]=pct
#print '('+str(max(p.loc[:,j]))+', '+str(min(p.loc[:,j]))+')'
return p
def Heatmap(df,figsize='auto',ccmaps=[(['Default'],0,'coolwarm')]):
if figsize=='auto':
figsize=[shape(df)[1],shape(df)[0]/2]
fig=figure(figsize=figsize)
#pdf=array(DFPercentiles(df,bycols=True)).astype(float)[::-1]
pdf=DFPercentiles_hmapshift(df,bycols=True)
if len(ccmaps)==1:
cmap=ccmaps[0][2]
else:
cmapl=[]
for x in ccmaps:
if x[1]!=0:
for y in x[0]:
pdf[y]=pdf[y]+x[1]
cmapl.append(getattr(plt.cm,x[2])(np.linspace(0,1,256,endpoint=False)+0.5/256.))
pdf=np.divide(pdf,len(ccmaps))
cs=np.vstack(cmapl)
cmap=matplotlib.colors.LinearSegmentedColormap.from_list("custom",cs)
pdf=array(pdf).astype(float)[::-1]
plt.pcolormesh(pdf,cmap=cmap,alpha=0.8)
plt.yticks(arange(0.5,len(df)),df.index[::-1])
plt.xticks(arange(0.5,len(df.columns)),df.columns)
for y in range(df.shape[0]):
for x in range(df.shape[1]):
plt.text(x + 0.5, y + 0.5, '%.3f' % df[::-1].iloc[y, x],
horizontalalignment='center',
verticalalignment='center',
)
return plt
hmap=Heatmap(mydf,ccmaps=[(['Default'],0,'RdBu_r'),(['Field3','Field4'],1,'Greens'),
(['Field0'],2,'Greys')])
hmap.show()
for the beautiful (ok, it's just an example!) result: