I'm implementing a Naive Bayes classifier.
I have the following figure showing me my classification boundaries:
I want to make the axes equally scaled for the figure, because I think it would help me better understand what is going on. However, I haven't found any way to do this. The plot is generated by a function not written by me:
%matplotlib inline
plotBoundary(BayesClassifier(), dataset='iris',split=0.7)
# ## Plotting the decision boundary
#
# This is some code that you can use for plotting the decision boundary
# boundary in the last part of the lab.
def plotBoundary(classifier, dataset='iris', split=0.7):
X,y,pcadim = fetchDataset(dataset)
xTr,yTr,xTe,yTe,trIdx,teIdx = trteSplitEven(X,y,split,1)
classes = np.unique(y)
pca = decomposition.PCA(n_components=2)
pca.fit(xTr)
xTr = pca.transform(xTr)
xTe = pca.transform(xTe)
pX = np.vstack((xTr, xTe))
py = np.hstack((yTr, yTe))
# Train
trained_classifier = classifier.trainClassifier(xTr, yTr)
xRange = np.arange(np.min(pX[:,0]),np.max(pX[:,0]),np.abs(np.max(pX[:,0])-np.min(pX[:,0]))/100.0)
yRange = np.arange(np.min(pX[:,1]),np.max(pX[:,1]),np.abs(np.max(pX[:,1])-np.min(pX[:,1]))/100.0)
grid = np.zeros((yRange.size, xRange.size))
for (xi, xx) in enumerate(xRange):
for (yi, yy) in enumerate(yRange):
# Predict
grid[yi,xi] = trained_classifier.classify(np.array([[xx, yy]]))
ys = [i+xx+(i*xx)**2 for i in range(len(classes))]
colormap = cm.rainbow(np.linspace(0, 1, len(ys)))
fig = plt.figure()
# plt.hold(True)
conv = ColorConverter()
for (color, c) in zip(colormap, classes):
try:
CS = plt.contour(xRange,yRange,(grid==c).astype(float),15,linewidths=0.25,colors=conv.to_rgba_array(color))
except ValueError:
pass
trClIdx = np.where(y[trIdx] == c)[0]
teClIdx = np.where(y[teIdx] == c)[0]
plt.scatter(xTr[trClIdx,0],xTr[trClIdx,1],marker='o',c=color,s=40,alpha=0.5, label="Class "+str(c)+" Train")
plt.scatter(xTe[teClIdx,0],xTe[teClIdx,1],marker='*',c=color,s=50,alpha=0.8, label="Class "+str(c)+" Test")
plt.legend(bbox_to_anchor=(1., 1), loc=2, borderaxespad=0.)
fig.subplots_adjust(right=0.7)
plt.axis("equal") # <------- TRIED TO INJECT axis("equal") here
plt.show()
I've tried injecting plt.axis("equal") into this function (1 line from the bottom of the code) but it doesn't make my axes equal. How can I achieve this?
EDIT: I also tried injecting plt.gca().set_aspect('equal', adjustable='box'). It didn't change anything.
the equal keyword scales x and y to be on the same scale. However if you meant that you want square axis you can try plt.axis('box')
You can set the limits manually:
xmin, xmax = plt.xlim()
ymin, ymax = plt.ylim()
fmin = min(xmin, ymin)
fmax = max(xmax, ymax)
plt.xlim(fmin, fmax)
plt.ylim(fmin, fmax)
Then make sure you have a 1:1 aspect ratio
Related
I am trying to plot a line that overlays several axes using matplotlib and transforms, but I cannot seem to get the transforms right. Here is the code:
# Define figure
fig = plt.figure(figsize=(7,3))
# Define colors
Lcolor="grey"
Pcolor=[0.8, 0.47058823529411764, 0.7372549019607844, 1.0]
MAPcolor = [0.00784313725490196, 0.6196078431372549, 0.45098039215686275, 1.0]
# Add gridspec for legend
gs0 = fig.add_gridspec(nrows=1, ncols=1, left=0, right=0.15, bottom=0.01,top=.85)
ax0 = fig.add_subplot(gs0[0])
# Add grid spec for data
gs1 = fig.add_gridspec(nrows=1, ncols=1, left=0.25, right=.8,bottom=0.01,top=gs0.top)
ax1 = fig.add_subplot(gs1[0])
ax1.set_ylim(-.5,1.5)
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
ax1.set_xlabel("Position",fontweight="bold")
ax1.set_ylabel("Position",fontweight="bold")
# Add gridspec for legend plot
gs2 = fig.add_gridspec(nrows=1, ncols=1, left=gs1.left, right=gs1.right, bottom=gs1.top+0.05,top=1)#,hspace=.25)
ax2 = fig.add_subplot(gs2[0])
# Define prior
xx = np.linspace(-.5, 1.5, 100)
prior = norm.pdf(xx, 0.5, .35)
ax0.plot(prior,xx,color="dodgerblue")
ax0.set_ylim(-.75,1.75)
ax0.xaxis.set_visible(False)
ax0.spines['bottom'].set_visible(False)
ax0.spines['top'].set_visible(False)
ax0.spines['right'].set_visible(False)
ax0.set_title("Prior belief",fontstyle="italic",color="dodgerblue",fontsize=10)
#### AXIS 2
plt.sca(ax1)
ax1.set_xlim(0,1.2)
nn=6
gs1_1 = fig.add_gridspec(nrows=1, ncols=nn, left=gs1.left, right=gs1.right, bottom=gs1.bottom,top=gs1.top)#,hspace=.25)
ax1_1 = []
xvals=np.linspace(0,1,nn)
for cnt,xval in enumerate(xvals):
# Store axis
ax1_1.append(fig.add_subplot(gs1_1[cnt],facecolor=None))
# Generate likelihood and posterior
_L = norm.pdf(xx, xval, 0.1)
_P = np.multiply(_L,prior)
# Plot distributions
ax1_1[cnt].plot(prior,xx,color="dodgerblue",zorder=3)
ax1_1[cnt].plot(_L,xx,color=Lcolor,zorder=3)
ax1_1[cnt].plot(_P,xx,color=Pcolor,zorder=4)
ax1_1[cnt].axvline(x=0,color="darkgrey",linestyle="--")
ax1_1[cnt].set_ylim(-.5,1.5)
ax1_1[cnt].set_axis_off()
# Plot MAP and adjust axis
ax1_1[cnt].scatter(_P.max(),xx[_P.argmax()],color=MAPcolor,s=30,clip_on=False)
xlims = ax1_1[cnt].get_xlim()
ax1_1[cnt].set_xlim(0,np.maximum(_P.max(),_L.max())+.25)
# Add axis on right side
ax1_r=ax1.twinx()
ax1_r.set_ylim(-.5,1.5)
ax1_r.set_ylabel("Error Predicted",color=MAPcolor,fontweight="bold",rotation=270)
ax1_r.tick_params(axis='y', colors=MAPcolor)
# Generate custom legend
colors=["dodgerblue",Lcolor,Pcolor,MAPcolor]
labels=["Prior","Likelihood","Posterior","Prediction (MAP)"]
for clr,lab in zip(colors,labels):
ax2.plot([], [], color=clr,alpha=1, label=lab)
leg = ax2.legend(loc="upper center",frameon=False,ncol=4,fontsize=10)
ax2.set_frame_on(False)
ax2.yaxis.set_visible(False)
ax2.xaxis.set_visible(False)
for (text,clr) in zip(leg.get_texts(),colors):
text.set_color(clr)
plt.show()
Here is the image: https://i.stack.imgur.com/dWGEP.png (I am unable to post inline)
Here is what I have tried so far:
Get the coordinates of the peak of the distribution and convert to display coordinates.
Define an inverse transform for the right axis
Apply transform to data and plot
So in the for loop:
for cnt,xval in enumerate(xvals):
...
temp=ax1_1[cnt].transData.transform((_P.max(),xx[_P.argmax()]))
invax = ax1_r.transData.inverted()
out = invax.transform(temp)
ax1_r.scatter(out[0],out[1],s=100)
...
In theory, I can then append the values to a list and then just fit a line to the points. I can't quite figure out the proper order of transformations to get from the subplots in ax1_1 to the bigger axis ax1_r
Thanks for the help.
I am learning to make color bars, and thus learning to make good use of plt.Normalize , I succeeded to make it work with scipy.stats.norm, but when tryin to use plt.norm, I found out that I have to do two things to make it work well :
defining vmin and vmax to -1.96 and 1.96 respectively,I guess that it's because they are the z value for 95% confidence interval, but I still don't precisely know why they have we have to set vmin and vmax to those values
dividing the standard deviation by sqrt( number of elements )
I don't understand why are those two points important for using the Norm. Any help is welcome ! thank you in advance
# Use the following data for this assignment:
%matplotlib notebook
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as st
df = pd.DataFrame([np.random.normal(33500,150000,3650),
np.random.normal(41000,90000,3650),
np.random.normal(41000,120000,3650),
np.random.normal(48000,55000,3650)],
index=[1992,1993,1994,1995])
new_df = pd.DataFrame()
new_df['mean'] = df.mean(axis =1)
new_df['std'] = df.std(axis =1)
new_df['se'] = df.sem(axis= 1)
new_df['C_low'] = new_df['mean'] - 1.96 * new_df['se']
new_df['C_high'] = new_df['mean'] + 1.96 * new_df['se']
from scipy.stats import norm
import numpy as np
# First, Define a figure
fig = plt.figure()
# next define its the axis and create a plot
ax = fig.add_subplot(1,1,1)
# change the ticks
xticks = np.array(new_df.index,dtype= 'str')
# remove the top and right borders
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# draw the bars in the axis
bars = ax.bar(xticks,new_df['mean'].values,
yerr = (1.96*new_df['se'],1.96*new_df['se']),
capsize= 10)
# define labels
plt.xlabel('YEARS',size = 14)
plt.ylabel('FREQUENCY',size = 14)
# Define color map
cmap = plt.cm.get_cmap('coolwarm')
# define scalar mappable
sm = plt.cm.ScalarMappable(cmap = cmap)
# draw the color bar
cbar = plt.colorbar(cmap = cmap, mappable =sm)
# define norm (will be used later to turn y to a value from 0 to 1 )
# define the events
class Cursor(object):
def __init__(self,ax):
self.ax = ax
self.lx = ax.axhline(color = 'c')
self.txt = ax.text(1,50000,'')
def mouse_movemnt(self,event):
#behaviour outside of the plot
if not event.inaxes:
return
#behavior inside the plot
y = event.ydata
self.lx.set_ydata(y)
for idx,bar in zip(new_df.index, bars):
norm = plt.Normalize(vmin =-1.96,vmax = 1.96)
mean = new_df.loc[idx,'mean']
err = new_df.loc[idx, 'se']
std = new_df.loc[idx,'std']/ np.sqrt(df.shape[1]) # not sure why we re dividing by np.sqrt(df.shape[1])
self.txt.set_text(f'Y = {round(y,2)} \n')
color_prob = norm( (mean - y)/std)
#color_prob = norm.cdf(y,loc = mean, scale = err) # you can also use this
bar.set_color( cmap(color_prob))
# connect the events to the plot
cursor = Cursor(ax)
plt.connect('motion_notify_event', cursor.mouse_movemnt)
None
After few hours of thinking, an explanation barged into my head and I was able to answer all of my inquiries,
first before answering the first point, I will answer the second one, the standard deviation was divided by the sqrt(nbr of element) because the resulting value is the standard error.
I will now move on to answering the first part:
(I can't embed images for now and I can't use latex either so I have to put links of the image instead). But here is the conclusion in advance, for all values within that confidence interval, the function (y-mean)/se will spit out a value within the range [−1.96,1.96]
answer of first part
Please, if I left something out or you have a better answer, share it with me.
I currently work with an instrument that provides data in Wavenumber, but most of my community works in wavelength. Because of this I would like to create plots that display Wavenumber in cm^-1 along the bottom x-axis and wavelength in µm along the top. However the spacing doesn't quite match up between the two units of measurement to display a single spectrum. How do I create a different spacing for wavelength?
Here is an example of how a portion of one spectrum looks when plotted as a function of wavenumber against when it's plotted as a function of wavelength. Below is the code I'm currently implementing.
wn = wn_tot[425:3175] #range of 250 to 3000 cm-1
wl = 10000/wn #wavelength in microns
fig = plt.figure(1)
ax1 = plt.subplot(1,1,1)
ax2 = ax1.twiny()
ax1.plot(wn, spc[45], 'c', label='Wavenumber')
ax2.plot(wl, spc[45], 'm', label='Wavelength')
ax1.set_xlabel('Wavenumber (cm$^{-1}$)')
ax2.set_xlabel('Wavelength ($\mu$m)')
ax1.set_ylabel('Relative Intensity')
ax2.invert_xaxis()
fig.legend(loc=2, bbox_to_anchor=(0,1), bbox_transform=ax1.transAxes)
As said in the comment on the OP, both scales cannot be simultaneously linear, since one cannot be obtained from the other via a linear transformation. You must hence accept that one (or both) have ticks at non-regular intervals.
The correct way to do it
Apply a transformation to the scale, which causes matplotlib to have a non-homogeneous scale.
The doc for Axes.set_yscale leads to that example which demonstrate the syntax ax1.set_xscale('function', functions=(forward, inverse)). Here in that case, the transformation functions are simply
def forward(wn):
# cm^{-1} to μm
return 1.0e4 / wn
def reverse(lam):
# μm to cm^{-1}
return 1.0e4 / lam
However, my matplotlib is stuck on version 2.2.2 which does not have that feature, so I cannot give a working example.
The hacky way that works with older versions
Give tick positions and labels by hand, performing the calculations yourself.
# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
def lambda_to_wave(lam):
# μm to cm^{-1}
return 1.0e4 / lam
x_wave = np.linspace(2000.0, 3000.0)
y_arb = np.linspace(0.0, 1.0e6)
ticks_wavelength_values = np.linspace(3.5, 5.5, num=5)
ticks_labels = [str(lam) for lam in ticks_wavelength_values]
ticks_wavenumber_positions = lambda_to_wave(ticks_wavelength_values)
print ticks_wavelength_values
print ticks_wavenumber_positions
fig = plt.figure(1)
ax1 = plt.subplot(1,1,1) # wavenumber
ax2 = ax1.twiny() # wavelength
ax2.get_shared_x_axes().join(ax1, ax2) # https://stackoverflow.com/questions/42973223/how-share-x-axis-of-two-subplots-after-they-are-created
ax1.plot(x_wave, y_arb, 'c', label='Data')
ax1.set_xlabel('Wavenumber (cm$^{-1}$)')
ax1.set_ylabel('Relative Intensity')
ax2.set_xticks(ticks_wavenumber_positions)
ax2.set_xticklabels(ticks_labels)
ax2.set_xlabel('Wavelength ($\mu$m)')
ax1.set_xlim(left=1800.0, right=3000.0)
fig.legend(loc=2, bbox_to_anchor=(0,1), bbox_transform=ax1.transAxes)
plt.show()
You can do without the second call to plot if you prefer: https://matplotlib.org/gallery/subplots_axes_and_figures/secondary_axis.html#sphx-glr-gallery-subplots-axes-and-figures-secondary-axis-py
wn = wn_tot[425:3175] #range of 250 to 3000 cm-1
fig = plt.figure(1)
ax1 = plt.subplot(1,1,1)
ax1.plot(wn, spc[45], 'c', label='Wavenumber')
def forward(x):
return 10000 / x
def inverse(x):
return 10000 / x
secax = ax.secondary_xaxis('top', functions=(forward, inverse))
ax1.set_xlabel('Wavenumber (cm$^{-1}$)')
secax.set_xlabel('Wavelength ($\mu$m)')
ax1.set_ylabel('Relative Intensity')
I would like to plot a vector field with curved arrows in python, as can be done in vfplot (see below) or IDL.
You can get close in matplotlib, but using quiver() limits you to straight vectors (see below left) whereas streamplot() doesn't seem to permit meaningful control over arrow length or arrowhead position (see below right), even when changing integration_direction, density, and maxlength.
So, is there a python library that can do this? Or is there a way of getting matplotlib to do it?
If you look at the streamplot.py that is included in matplotlib, on lines 196 - 202 (ish, idk if this has changed between versions - I'm on matplotlib 2.1.2) we see the following:
... (to line 195)
# Add arrows half way along each trajectory.
s = np.cumsum(np.sqrt(np.diff(tx) ** 2 + np.diff(ty) ** 2))
n = np.searchsorted(s, s[-1] / 2.)
arrow_tail = (tx[n], ty[n])
arrow_head = (np.mean(tx[n:n + 2]), np.mean(ty[n:n + 2]))
... (after line 196)
changing that part to this will do the trick (changing assignment of n):
... (to line 195)
# Add arrows half way along each trajectory.
s = np.cumsum(np.sqrt(np.diff(tx) ** 2 + np.diff(ty) ** 2))
n = np.searchsorted(s, s[-1]) ### THIS IS THE EDITED LINE! ###
arrow_tail = (tx[n], ty[n])
arrow_head = (np.mean(tx[n:n + 2]), np.mean(ty[n:n + 2]))
... (after line 196)
If you modify this to put the arrow at the end, then you could generate the arrows more to your liking.
Additionally, from the docs at the top of the function, we see the following:
*linewidth* : numeric or 2d array
vary linewidth when given a 2d array with the same shape as velocities.
The linewidth can be a numpy.ndarray, and if you can pre-calculate the desired width of your arrows, you'll be able to modify the pencil width while drawing the arrows. It looks like this part has already been done for you.
So, in combination with shortening the arrows maxlength, increasing the density, and adding start_points, as well as tweaking the function to put the arrow at the end instead of the middle, you could get your desired graph.
With these modifications, and the following code, I was able to get a result much closer to what you wanted:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.patches as pat
w = 3
Y, X = np.mgrid[-w:w:100j, -w:w:100j]
U = -1 - X**2 + Y
V = 1 + X - Y**2
speed = np.sqrt(U*U + V*V)
fig = plt.figure(figsize=(14, 18))
gs = gridspec.GridSpec(nrows=3, ncols=2, height_ratios=[1, 1, 2])
grains = 10
tmp = tuple([x]*grains for x in np.linspace(-2, 2, grains))
xs = []
for x in tmp:
xs += x
ys = tuple(np.linspace(-2, 2, grains))*grains
seed_points = np.array([list(xs), list(ys)])
# Varying color along a streamline
ax1 = fig.add_subplot(gs[0, 1])
strm = ax1.streamplot(X, Y, U, V, color=U, linewidth=np.array(5*np.random.random_sample((100, 100))**2 + 1), cmap='winter', density=10,
minlength=0.001, maxlength = 0.07, arrowstyle='fancy',
integration_direction='forward', start_points = seed_points.T)
fig.colorbar(strm.lines)
ax1.set_title('Varying Color')
plt.tight_layout()
plt.show()
tl;dr: go copy the source code, and change it to put the arrows at the end of each path, instead of in the middle. Then use your streamplot instead of the matplotlib streamplot.
Edit: I got the linewidths to vary
Starting with David Culbreth's modification, I rewrote chunks of the streamplot function to achieve the desired behaviour. Slightly too numerous to specify them all here, but it includes a length-normalising method and disables the trajectory-overlap checking. I've appended two comparisons of the new curved quiver function with the original streamplot and quiver.
Here's a way to obtain the desired output in vanilla pyplot (i.e., without modifying the streamplot function or anything that fancy). For reminder, the goal is to visualize a vector field with curved arrows whose length is proportional to the norm of the vector.
The trick is to:
make streamplot with no arrows that is traced backward from a given point (see)
plot a quiver from that point. Make the quiver small enough so that only the arrow is visible
repeat 1. and 2. in a loop for every seed and scale the length of the streamplot to be proportional to the norm of the vector.
import matplotlib.pyplot as plt
import numpy as np
w = 3
Y, X = np.mgrid[-w:w:8j, -w:w:8j]
U = -Y
V = X
norm = np.sqrt(U**2 + V**2)
norm_flat = norm.flatten()
start_points = np.array([X.flatten(),Y.flatten()]).T
plt.clf()
scale = .2/np.max(norm)
plt.subplot(121)
plt.title('scaling only the length')
for i in range(start_points.shape[0]):
plt.streamplot(X,Y,U,V, color='k', start_points=np.array([start_points[i,:]]),minlength=.95*norm_flat[i]*scale, maxlength=1.0*norm_flat[i]*scale,
integration_direction='backward', density=10, arrowsize=0.0)
plt.quiver(X,Y,U/norm, V/norm,scale=30)
plt.axis('square')
plt.subplot(122)
plt.title('scaling length, arrowhead and linewidth')
for i in range(start_points.shape[0]):
plt.streamplot(X,Y,U,V, color='k', start_points=np.array([start_points[i,:]]),minlength=.95*norm_flat[i]*scale, maxlength=1.0*norm_flat[i]*scale,
integration_direction='backward', density=10, arrowsize=0.0, linewidth=.5*norm_flat[i])
plt.quiver(X,Y,U/np.max(norm), V/np.max(norm),scale=30)
plt.axis('square')
Here's the result:
Just looking at the documentation on streamplot(), found here -- what if you used something like streamplot( ... ,minlength = n/2, maxlength = n) where n is the desired length -- you will need to play with those numbers a bit to get your desired graph
you can control for the points using start_points, as shown in the example provided by #JohnKoch
Here's an example of how I controlled the length with streamplot() -- it's pretty much a straight copy/paste/crop from the example from above.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.patches as pat
w = 3
Y, X = np.mgrid[-w:w:100j, -w:w:100j]
U = -1 - X**2 + Y
V = 1 + X - Y**2
speed = np.sqrt(U*U + V*V)
fig = plt.figure(figsize=(14, 18))
gs = gridspec.GridSpec(nrows=3, ncols=2, height_ratios=[1, 1, 2])
grains = 10
tmp = tuple([x]*grains for x in np.linspace(-2, 2, grains))
xs = []
for x in tmp:
xs += x
ys = tuple(np.linspace(-2, 2, grains))*grains
seed_points = np.array([list(xs), list(ys)])
arrowStyle = pat.ArrowStyle.Fancy()
# Varying color along a streamline
ax1 = fig.add_subplot(gs[0, 1])
strm = ax1.streamplot(X, Y, U, V, color=U, linewidth=1.5, cmap='winter', density=10,
minlength=0.001, maxlength = 0.1, arrowstyle='->',
integration_direction='forward', start_points = seed_points.T)
fig.colorbar(strm.lines)
ax1.set_title('Varying Color')
plt.tight_layout()
plt.show()
Edit: made it prettier, though still not quite what we were looking for.
Is it possible to clip an image generated by imshow() to the area under a line/multiple lines? I think Clip an image using several patches in matplotlib may have the solution, but I'm not sure how to apply it here.
I just want the coloring (from imshow()) under the lines in this plot:
Here is my plotting code:
from __future__ import division
from matplotlib.pyplot import *
from numpy import *
# wavelength array
lambd = logspace(-3.8, -7.2, 1000)
# temperatures
T_earth = 300
T_sun = 6000
# planck's law constants
h = 6.626069e-34
c = 2.997925e8
k = 1.380648e-23
# compute power using planck's law
power_earth = 2*h*c**2/lambd**5 * 1/(exp(h*c/(lambd*k*T_earth)) - 1)
power_sun = 2*h*c**2/lambd**5 * 1/(exp(h*c/(lambd*k*T_sun)) - 1)
# set up color array based on "spectrum" colormap
colors = zeros((1000,1000))
colors[:,:1000-764] = 0.03
for x,i in enumerate(range(701,765)):
colors[:,1000-i] = 1-x/(765-701)
colors[:,1000-701:] = 0.98
figure(1,(4,3),dpi=100)
# plot normalized planck's law graphs
semilogx(lambd, power_earth/max(power_earth), 'b-', lw=4, zorder=5); hold(True)
semilogx(lambd, power_sun/max(power_sun), 'r-', lw=4, zorder=5); hold(True)
# remove ticks (for now)
yticks([]); xticks([])
# set axis to contain lines nicely
axis([min(lambd), max(lambd), 0, 1.1])
# plot colors, shift extent to match graph
imshow(colors, cmap="spectral", extent=[min(lambd), max(lambd), 0, 1.1])
# reverse x-axis (longer wavelengths to the left)
ax = gca(); ax.set_xlim(ax.get_xlim()[::-1])
tight_layout()
show()
What you can do in this case is using the area under the curve as a Patch to apply set_clip_path. All you have to do is call fill_between and extract the corresponding path, like this:
semilogx(lambd, power_earth/max(power_earth), 'b-', lw=4, zorder=5)
# Area under the curve
fillb_earth = fill_between(lambd, power_earth/max(power_earth), color='none', lw=0)
# Get the path
path_earth, = fillb_earth.get_paths()
# Create a Patch
mask_earth = PathPatch(path_earth, fc='none')
# Add it to the current axes
gca().add_patch(mask_earth)
# Add the image
im_earth = imshow(colors, cmap="spectral", extent=[min(lambd), max(lambd), 0, 1.1])
# Clip the image with the Patch
im_earth.set_clip_path(mask_earth)
And then repeat the same lines for the Sun. Here is the result.