Making sure 0 gets white in a RdBu colorbar - matplotlib

I create a heatmap with the following snippet:
import numpy as np
import matplotlib.pyplot as plt
d = np.random.normal(.4,2,(10,10))
plt.imshow(d,cmap=plt.cm.RdBu)
plt.colorbar()
plt.show()
The result is plot below:
Now, since the middle point of the data is not 0, the cells in which the colormap has value 0 are not white, but rather a little reddish.
How do I force the colormap so that max=blue, min=red and 0=white?

Use a DivergingNorm.
Note: From matplotlib 3.2 onwards DivergingNorm is renamed to TwoSlopeNorm.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
d = np.random.normal(.4,2,(10,10))
norm = mcolors.DivergingNorm(vmin=d.min(), vmax = d.max(), vcenter=0)
plt.imshow(d, cmap=plt.cm.RdBu, norm=norm)
plt.colorbar()
plt.show()

A previous SO post (Change colorbar gradient in matplotlib) wanted a solution for a more complicated situation, but one of the answers talked about the MidpointNormalize subclass in the matplotlib documentation. With that, the solution becomes:
import matplotlib as mpl
import numpy as np
import matplotlib.pyplot as plt
class MidpointNormalize(mpl.colors.Normalize):
## class from the mpl docs:
# https://matplotlib.org/users/colormapnorms.html
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
self.midpoint = midpoint
super().__init__(vmin, vmax, clip)
def __call__(self, value, clip=None):
# I'm ignoring masked values and all kinds of edge cases to make a
# simple example...
x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1]
return np.ma.masked_array(np.interp(value, x, y))
d = np.random.normal(.4,2,(10,10))
plt.imshow(d,cmap=plt.cm.RdBu,norm=MidpointNormalize(midpoint=0))
plt.colorbar()
plt.show()
Kudos to Joe Kington for writing the subclass, and to Rutger Kassies for pointing out the answer.

Related

Matplotlib TwoSlopeNorm use base2 in colorbar

is there a way to get TwoSlopeNorm in combination with base 2 ticks on the colorbar?
An example is something like this where you have normal linear scaling:
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
x = np.arange(-50,100,1)
y = x.copy()
c = x.copy()
scatter_plot = plt.scatter(x, y, c=c, cmap='bwr', norm=matplotlib.colors.TwoSlopeNorm(vmin=-50, vcenter=0, vmax=100))
cbar = plt.colorbar(scatter_plot)
plt.show()
I know based on a previous question of mine that SymLogNorm supports base2, but it looks like this is not the case for TwoSlopeNorm. Does anyone have a suggestion on how to do it?

How can Matplotlib axes be scaled hyperbolically?

I have a plot a bit like this:
The differences between the two lines (red and blue) are most important in my actual data (a ROC curve) at say the grid cell 0.2<x<0.4, 0.8<y<1. Now, I could crop for that grid cell, but let's say I'd rather scale both the x and y axes hyperbolically -- where the y-axis hyperbolic curve has its peak at about 0.9 and the x-axis has its peak at about 0.3 -- such that the 2D space gets stretched out for the grid cell of interest and gets compacted elsewhere (and preserving the meaning of the axes tick numbers). How would one accomplish this? The beginnings of my attempt are below. How would my code be modified to implement the axis scaling I described?
from matplotlib import gridspec
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
from matplotlib.ticker import FormatStrFormatter
from matplotlib.ticker import NullFormatter, NullLocator, MultipleLocator
import math
import matplotlib
import matplotlib.patches as mpatches
import matplotlib.pylab as plt
import matplotlib.pyplot as plt
import matplotlib.ticker
import numpy as np
import seaborn as sns
sns.set_palette('husl')
sns.set()
plt.rcParams["figure.figsize"] = [5, 5]
x = np.arange(0, 1, step=0.01)
y1 = 1-1/np.exp(10*x)
y2 = 1-1.1/np.exp(10*x)
plt.scatter(x, y1, s=1, facecolor='red')
plt.scatter(x, y2, s=1, facecolor='blue')
plt.show();
class CustomScale(mscale.ScaleBase):
name = 'custom'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.thresh = None #thresh
self.name = 'custom'
def get_transform(self):
return self.CustomTransform(self.thresh)
def set_default_locators_and_formatters(self, axis):
pass
class CustomTransform(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, thresh):
mtransforms.Transform.__init__(self)
self.thresh = thresh
def transform_non_affine(self, a):
#return np.log(1+a)
return np.exp(a)-1
#return 1+(1/2)*a
mscale.register_scale(CustomScale)
plt.scatter(x, y1, s=1, facecolor='red')
plt.scatter(x, y2, s=1, facecolor='blue')
plt.xscale('custom')
plt.show();
You may be able to achieve this using FuncScale (registered as 'function').
f = lambda a: np.exp(a) - 1
g = lambda b: np.log(b + 1)
plt.xscale('function', functions=(f, g))
For hyperbolic scaling, you could use lambda x: 1 / x for both functions.
See the example in the scales documentation: https://matplotlib.org/3.3.4/gallery/scales/scales.html

Understanding plt.show() in Matplotlib

import numpy as np
import os.path
from skimage.io import imread
from skimage import data_dir
img = imread(os.path.join(data_dir, 'checker_bilevel.png'))
import matplotlib.pyplot as plt
#plt.imshow(img, cmap='Blues')
#plt.show()
imgT = img.T
plt.figure(1)
plt.imshow(imgT,cmap='Greys')
#plt.show()
imgR = img.reshape(20,5)
plt.figure(2)
plt.imshow(imgR,cmap='Blues')
plt.show(1)
I read that plt.figure() will create or assign the image a new ID if not explicitly given one. So here, I have given the two figures, ID 1 & 2 respectively. Now I wish to see only one one of the image.
I tried plt.show(1) epecting ONLY the first image will be displayed but both of them are.
What should I write to get only one?
plt.clf() will clear the figure
import matplotlib.pyplot as plt
plt.plot(range(10), 'r')
plt.clf()
plt.plot(range(12), 'g--')
plt.show()
plt.show will show all the figures created. The argument you forces the figure to be shown in a non-blocking way. If you only want to show a particular figure you can write a wrapper function.
import matplotlib.pyplot as plt
figures = [plt.subplots() for i in range(5)]
def show(figNum, figures):
if plt.fignum_exists(figNum):
fig = [f[0] for f in figures if f[0].number == figNum][0]
fig.show()
else:
print('figure not found')

LogFormatter tickmarks scientific format limits

I'm trying to plot over a wide range with a log-scaled axis, but I want to show 10^{-1}, 10^0, 10^1 as just 0.1, 1, 10. ScalarFormatter will change everything to integers instead of scientific notation, but I'd like most of the tickmark labels to be scientific; I'm only wanting to change a few of the labels. So the MWE is
import numpy as np
import matplotlib as plt
fig = plt.figure(figsize=[7,7])
ax1 = fig.add_subplot(111)
ax1.set_yscale('log')
ax1.set_xscale('log')
ax1.plot(np.logspace(-4,4), np.logspace(-4,4))
plt.show()
and I want the middle labels on each axis to read 0.1, 1, 10 instead of 10^{-1}, 10^0, 10^1
Thanks for any help!
When setting set_xscale('log'), you're using a LogFormatterSciNotation (not a ScalarFormatter). You may subclass LogFormatterSciNotation to return the desired values 0.1,1,10 if they happen to be marked as ticks.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import LogFormatterSciNotation
class CustomTicker(LogFormatterSciNotation):
def __call__(self, x, pos=None):
if x not in [0.1,1,10]:
return LogFormatterSciNotation.__call__(self,x, pos=None)
else:
return "{x:g}".format(x=x)
fig = plt.figure(figsize=[7,7])
ax = fig.add_subplot(111)
ax.set_yscale('log')
ax.set_xscale('log')
ax.plot(np.logspace(-4,4), np.logspace(-4,4))
ax.xaxis.set_major_formatter(CustomTicker())
plt.show()
Update: With matplotlib 2.1 there is now a new option
Specify minimum value to format as scalar for LogFormatterMathtext
LogFormatterMathtext now includes the option to specify a minimum value exponent to format as a scalar (i.e., 0.001 instead of 10-3).
This can be done as follows, by using the rcParams (plt.rcParams['axes.formatter.min_exponent'] = 2):
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['axes.formatter.min_exponent'] = 2
fig = plt.figure(figsize=[7,7])
ax = fig.add_subplot(111)
ax.set_yscale('log')
ax.set_xscale('log')
ax.plot(np.logspace(-4,4), np.logspace(-4,4))
plt.show()
This results in the same plot as above.
Note however that this limit is symmetric, it would not allow to set only 1 and 10, but not 0.1. Hence the initial solution is more generic.

My pandas-generated subplots are layouted incorrectly

I ran the following code to get two plots next to each other (it is a minimal working example that you can copy):
import pandas as pd
import numpy as np
from matplotlib.pylab import plt
comp1 = np.random.normal(0,1,size=200)
values = pd.Series(comp1)
plt.close("all")
f = plt.figure()
plt.show()
sp1 = f.add_subplot(2,2,1)
values.hist(bins=100, alpha=0.5, color="r", normed=True)
sp2 = f.add_subplot(2,2,2)
values.plot(kind="kde")
Unfortunately, I then get the following image:
This is also an interesting layout, but I wanted the figures to be next to each other. What did I do wrong? How can I correct it?
For clarity, I could also use this:
import pandas as pd
import numpy as np
from matplotlib.pylab import plt
comp1 = np.random.normal(0,1,size=200)
values = pd.Series(comp1)
plt.close("all")
fig, axes = plt.subplots(2,2)
plt.show()
axes[0,0].hist(values, bins=100, alpha=0.5, color="r", normed=True) # Until here, it works. You get a half-finished correct image of what I was going for (though it is 2x2 here)
axes[0,1].plot(values, kind="kde") # This does not work
Unfortunately, in this approach axes[0,1] refers to the subplot that has a plot method but does not know kind="kde". Please take into consideration that the in the first version plot is executed on the pandas object, whereas in the second version plot is executed on the subplot, which does not work with the kind="kde" parameter.
use ax= argument to set which subplot object to plot:
import pandas as pd
import numpy as np
from matplotlib.pylab import plt
comp1 = np.random.normal(0,1,size=200)
values = pd.Series(comp1)
plt.close("all")
f = plt.figure()
sp1 = f.add_subplot(2,2,1)
values.hist(bins=100, alpha=0.5, color="r", normed=True, ax=sp1)
sp2 = f.add_subplot(2,2,2)
values.plot(kind="kde", ax=sp2)