Matplotlib step function: How to extend the first and last steps - python

I am using a step and fill_between functions in Matplotlib and want the steps to be centred on the x points.
Code
import matplotlib.pyplot as plt
import numpy as np
xpoints=np.array([1,2,3,4])
ypoints=np.array([4,6,5,2])
ypoints_std=np.array([0.5,0.3,0.4,0.2])
plt.step(xpoints,ypoints,where='mid')
plt.fill_between(xpoints,ypoints+ypoints_std,ypoints-ypoints_std,step='mid',alpha=0.2)
plt.show()
Current plot:
At the moment, the step centred on 1 is only 0.5 wide, whereas the step centred on 2 is 1 wide.
Wanted
I actually want the step-width of 1 for all steps and also for the fill. This should include first and last step, so that they are extended compared to the current plot.
Of course I can pad the data, but that is getting messy in my actual code.
Questions
Is there a way to make the first and last steps the same size as the middle ones?
Or is there a way to produce a similar graph using histogram ? i.e. showing an error the size of the full width of the bar, centred on the y position of the graph?

Using a bar plot at a height
The error bands could be shown via a bar plot with a bottom at ypoints - ypoints_std and a height of 2*ypoints_std.
import matplotlib.pyplot as plt
import numpy as np
xpoints = np.array([1, 2, 3, 4])
ypoints = np.array([4, 6, 5, 2])
ypoints_std = np.array([0.5, 0.3, 0.4, 0.2])
plt.bar(xpoints, ypoints, width=1, facecolor='none', edgecolor='dodgerblue')
plt.bar(xpoints, height=2 * ypoints_std, bottom=ypoints - ypoints_std, width=1, color='dodgerblue', alpha=0.2)
plt.xticks(xpoints)
plt.show()
Using zero-height bars
To only have horizontal lines, you could replace the first bar plot with zero-height bars. Adding the original plt.step with the same color will create the connecting lines
plt.gca().use_sticky_edges = False # prevent bars from "sticking" to the bottom
plt.step(xpoints, ypoints, where='mid', color='dodgerblue')
plt.bar(xpoints, height=0, bottom=ypoints, width=1, facecolor='none', edgecolor='dodgerblue')
plt.bar(xpoints, height=2 * ypoints_std, bottom=ypoints - ypoints_std, width=1, color='dodgerblue', alpha=0.2)
Extending the points
You could add dummy values to repeat the first and last point. And then use plt.xlim(...) to limit the plot between 0.5 and 4.5.
import matplotlib.pyplot as plt
import numpy as np
xpoints = np.array([1, 2, 3, 4])
ypoints = np.array([4, 6, 5, 2])
ypoints_std = np.array([0.5, 0.3, 0.4, 0.2])
xpoints = np.concatenate([[xpoints[0] - 1], xpoints, [xpoints[-1] + 1]])
ypoints = np.pad(ypoints, 1, mode='edge')
ypoints_std = np.pad(ypoints_std, 1, mode='edge')
plt.step(xpoints, ypoints, where='mid')
plt.fill_between(xpoints, ypoints + ypoints_std, ypoints - ypoints_std, step='mid', alpha=0.2)
plt.xlim(xpoints[0] + 0.5, xpoints[-1] - 0.5)
plt.show()

You could use pyplot.margins(0) to at least let your graph touch the axis on all 4 sides (left/right and bottom/top).
Either use two positional arguments for x and y, or use one to be applied for both:
import matplotlib.pyplot as plt
import numpy as np
xpoints=np.array([1,2,3,4])
ypoints=np.array([4,6,5,2])
ypoints_std=np.array([0.5,0.3,0.4,0.2])
fig, ax = plt.subplots()
ax.step(xpoints,ypoints,where='mid')
ax.fill_between(xpoints,ypoints+ypoints_std,ypoints-ypoints_std,step='mid',alpha=0.2)
ax.margins(0) # default margins are 0.5 for x-axis and y-axis
plt.show()
Output:

Related

How to achieve a straight regression line in a log-log sns.regplot

I am trying to recreate this plot created with R in Python:
This is where I got:
This is the code I used:
from matplotlib.ticker import ScalarFormatter
fig, ax = plt.subplots(figsize=(10,8))
sns.regplot(x='Platform2',y='Platform1',data=duplicates[['Platform2','Platform1']].dropna(thresh=2), scatter_kws={'s':80, 'alpha':0.5})
plt.ylabel('Platform1', labelpad=15, fontsize=15)
plt.xlabel('Platform2', labelpad=15, fontsize=15)
plt.title('Sales of the same game in different platforms', pad=30, size=20)
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xticks([1,2,5,10,20])
ax.set_yticks([1,2,5,10,20])
ax.get_xaxis().set_major_formatter(ScalarFormatter())
ax.get_yaxis().set_major_formatter(ScalarFormatter())
ax.set_xlim([0.005, 25.])
ax.set_ylim([0.005, 25.])
plt.show()
I think I am missing some conceptual knowledge behind the logarithmic values I plotted here. Since I did not change the values themselves but the scale of the graph I think I am doing something wrong. When I tried changing the values themselves I was not successful.
What I wanted was to show the regression line like the one in the R plot and also show the 0s in the x and y axes. The logarithmic nature of the plot does not allow me to add the 0 limits in the x and y axes. I found this StackOverflow entry: LINK but I was not able to make it work. Maybe if someone can rephrase it or if someone has any suggestions on how to move forward it would be great!
Thanks!
Seaborn's regplot creates either a line in linear space (y ~ x), or (with logx=True) a linear regression of the form y ~ log(x). Your question asks for a linear regression of the form log(y) ~ log(x).
This can be accomplished by calling regplot with the log of the input data.
However, this will change the data axes showing the log of the data instead of the data themselves. With a special tick formatter (taking the power of the value), these tick values can be converted again to the original data format.
Note that both the calls to set_xticks() and set_xlim() will need their values converted to log space for this to work. The calls to set_xscale('log') need to be removed.
The code below also changes most plt. calls to ax. calls, and adds the ax as argument to sns.regplot(..., ax=ax).
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
p1 = 10 ** np.random.uniform(-2, 1, 1000)
p2 = 10 ** np.random.uniform(-2, 1, 1000)
duplicates = pd.DataFrame({'Platform1': 0.6 * p1 + 0.4 * p2, 'Platform2': 0.1 * p1 + 0.9 * p2})
fig, ax = plt.subplots(figsize=(10, 8))
data = duplicates[['Platform2', 'Platform1']].dropna(thresh=2)
sns.regplot(x=np.log10(data['Platform2']), y=np.log10(data['Platform1']),
scatter_kws={'s': 80, 'alpha': 0.5}, ax=ax)
ax.set_ylabel('Platform1', labelpad=15, fontsize=15)
ax.set_xlabel('Platform2', labelpad=15, fontsize=15)
ax.set_title('Sales of the same game in different platforms', pad=30, size=20)
ticks = np.log10(np.array([1, 2, 5, 10, 20]))
ax.set_xticks(ticks)
ax.set_yticks(ticks)
formatter = lambda x, pos: f'{10 ** x:g}'
ax.get_xaxis().set_major_formatter(formatter)
ax.get_yaxis().set_major_formatter(formatter)
lims = np.log10(np.array([0.005, 25.]))
ax.set_xlim(lims)
ax.set_ylim(lims)
plt.show()
To create a jointplot similar to the example in R (to set the figure size, use sns.jointplot(...., height=...), the figure will always be square):
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
sns.set()
p1 = 10 ** np.random.uniform(-2.1, 1.3, 1000)
p2 = 10 ** np.random.uniform(-2.1, 1.3, 1000)
duplicates = pd.DataFrame({'Platform1': 0.6 * p1 + 0.4 * p2, 'Platform2': 0.1 * p1 + 0.9 * p2})
data = duplicates[['Platform2', 'Platform1']].dropna(thresh=2)
g = sns.jointplot(x=np.log10(data['Platform2']), y=np.log10(data['Platform1']),
scatter_kws={'s': 80, 'alpha': 0.5}, kind='reg', height=10)
ax = g.ax_joint
ax.set_ylabel('Platform1', labelpad=15, fontsize=15)
ax.set_xlabel('Platform2', labelpad=15, fontsize=15)
g.fig.suptitle('Sales of the same game in different platforms', size=20)
ticks = np.log10(np.array([.01, .1, 1, 2, 5, 10, 20]))
ax.set_xticks(ticks)
ax.set_yticks(ticks)
formatter = lambda x, pos: f'{10 ** x:g}'
ax.get_xaxis().set_major_formatter(formatter)
ax.get_yaxis().set_major_formatter(formatter)
lims = np.log10(np.array([0.005, 25.]))
ax.set_xlim(lims)
ax.set_ylim(lims)
plt.tight_layout()
plt.show()

How to create a bar plot with long horizontal error bars?

I'd like to emulate the blueish background lines from the given image. Can someone tell me how to do so using Matplotlib?
Basically, it's a less complicated version of this.
Here is an approach using a stretched 1D image to draw the bands. The image gets an extent to fill the complete width and the desired height. The vmax for the color mapping is set a bit higher to avoid too light colors, which would be too similar to the white background.
import matplotlib.pyplot as plt
import numpy as np
# create some test data
N = 40
x = np.linspace(-3, 3, N)
values = np.random.normal(0, 2, N)
error_bands = np.array([2, 1, 0, 0, 1, 2])
plt.imshow(error_bands.reshape(-1, 1), extent=[-10, 10, -3, 3], origin='lower',
cmap='Blues_r', vmin=error_bands.min(), vmax=error_bands.max() * 1.4, alpha=0.3)
plt.axhline(0, color='blueviolet', lw=3) # horizontal line at x=0
plt.bar(x, values, width=(x[1] - x[0]) * 0.8, bottom=0, color='blueviolet')
plt.gca().set_aspect('auto') # removed the fixed aspect ratio forced by imshow
plt.xlim(x[0] - 0.3, x[-1] + 0.3) # explicitly set the xlims, shorter than the extent of imshow
plt.show()
PS: A simpler approach, replacing imshow with some calls to axhspan drawing horizontal bars over eachother, using a small alpha:
for i in range(1, 4):
plt.axhspan(-i, i, color='b', alpha=0.1)

How to have patterns for the color bar in Biokit/corrplot

I wonder if it is possible with corrplot (from the biokit package) to have a colobar with patterns.
In this example below, for the colobar, I would like to have 5 bubbles with differents sizes associated to the matrix's values ([-1, -0.5, 0, +0.5, +1]).
Ideas ?
thanks a lot
import numpy as np
import pandas as pd
from biokit.viz import corrplot
from matplotlib import pyplot as plt
import string
letters = string.ascii_uppercase[0:15]
df = pd.DataFrame(dict(( (k, np.random.random(10)+ord(k)-65) for k in letters)))
df = df.corr()
c = corrplot.Corrplot(df)
c.plot(colorbar=True, upper='circle', rotation=60, cmap='Oranges', fontsize=12)
plt.tight_layout()
plt.show()
Adding filled shapes to a colorbar seems not to be part of the standard interface. However, a legend could serve your goals. Some shapes are supported directly by the legend, others need a special handler, as described in this post.
It seems first some circles need to be created, for which color etc. can be set.
To tell the handler which shape exactly is meant, I'm misusing the label parameter.
From the source of biokit's corrplot, we learn that the ellipse is rotated either + or -45°, and that it is scaled by the absolute value of the correlation.
The following code puts everything together. The colorbar is drawn as a reference, but can be left out once everything is checked. The legend is positioned outside the main plot via bbox_to_anchor=(x, y). These coordinates are in axes coordinates. The ideal location depends on the size of the other elements, so some experimentation could be useful. I didn't draw the corrplot itself, as I don't have it installed, but you can replace the dummy scatter plot with it.
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
import matplotlib as mpl
class HandlerEllipse(mpl.legend_handler.HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
d = float(orig_handle.get_label())
center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
radius = (height + ydescent) * 1.8
p = Ellipse(xy=center, width=radius, height=radius * (1 - abs(d)), angle=45 if d > 0 else -45)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
#values = [1, 0.5, 0, -0.5, -1]
values = [1, 0.75, 0.5, 0.25, 0, -0.25, -0.5, -0.75, -1]
cmap = plt.cm.Oranges # or plt.cm.PiYG
norm = mpl.colors.Normalize(vmin=-1, vmax=1)
fig, ax = plt.subplots()
plt.scatter([0,1], [0,1], c=[-1,1], cmap=cmap, norm=norm) # a dummy plot as a stand-in for the corrplot
char = plt.colorbar(ticks=values)
shapes = [Circle((0.5, 0.5), 1, facecolor=cmap(norm(d)), edgecolor='k', alpha=1, zorder=2, linewidth=1, label=d)
for d in values]
plt.legend(handles=shapes, labels=values, title='Correlation', framealpha=1,
bbox_to_anchor=(1.25, 1), loc='upper left',
handler_map={Circle: HandlerEllipse()})
plt.tight_layout() # make sure legend and colorbar fit nicely in the plot
plt.show()

Add horizontal line with conditional coloring

I make a contourf plot using matplotlib.pyplot. Now I want to have a horizontal line (or something like ax.vspan would work too) with conditional coloring at y = 0. I will show you what I have and what I would like to get. I want to do this with an array, let's say landsurface that represents either land, ocean or ice. This array is filled with 1 (land), 2 (ocean) or 3 (ice) and has the len(locs) (so the x-axis).
This is the plot code:
plt.figure()
ax=plt.axes()
clev=np.arange(0.,50.,.5)
plt.contourf(locs,height-surfaceheight,var,clev,extend='max')
plt.xlabel('Location')
plt.ylabel('Height above ground level [m]')
cbar = plt.colorbar()
cbar.ax.set_ylabel('o3 mixing ratio [ppb]')
plt.show()
This is what I have so far:
This is what I want:
Many thanks in advance!
Intro
I'm going to use a line collection .
Because I have not your original data, I faked some data using a simple sine curve and plotting on the baseline the color codes corresponding to small, middle and high values of the curve
Code
Usual boilerplate, we need to explicitly import LineCollection
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
Just to plot something, a sine curve (x r
x = np.linspace(0, 50, 101)
y = np.sin(0.3*x)
The color coding from the curve values (corresponding to your surface types) to the LineCollection colors, note that LineCollection requires that the colors are specified as RGBA tuples but I have seen examples using color strings, bah!
# 1 when near min, 2 when near 0, 3 when near max
z = np.where(y<-0.5, 1, np.where(y<+0.5, 2, 3))
col_d = {1:(0.4, 0.4, 1.0, 1), # blue, near min
2:(0.4, 1.0, 0.4, 1), # green, near zero
3:(1.0, 0.4, 0.4, 1)} # red, near max
# prepare the list of colors
colors = [col_d[n] for n in z]
In a line collection we need a sequence of segments, here I have decided to place my coded line at y=0 but you can just add a constant to s to move it up and down.
I admit that forming the sequence of segments is a bit tricky...
# build the sequence of segments
s = np.zeros(101)
segments=np.array(list(zip(zip(x,x[1:]),zip(s,s[1:])))).transpose((0,2,1))
# and fill the LineCollection
lc = LineCollection(segments, colors=colors, linewidths=5,
antialiaseds=0, # to prevent artifacts between lines
zorder=3 # to force drawing over the curve) lc = LineCollection(segments, colors=colors, linewidths=5) # possibly add zorder=...
Finally, we put everything on the canvas
# plot the function and the line collection
fig, ax = plt.subplots()
ax.plot(x,y)
ax.add_collection(lc)
I would suggest adding an imshow() with proper extent, e.g.:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colorbar as colorbar
import matplotlib.colors as colors
### generate some data
np.random.seed(19680801)
npts = 50
x = np.random.uniform(0, 1, npts)
y = np.random.uniform(0, 1, npts)
X,Y=np.meshgrid(x,y)
z = x * np.exp(-X**2 - Y**2)*100
### create a colormap of three distinct colors for each landmass
landmass_cmap=colors.ListedColormap(["b","r","g"])
x_land=np.linspace(0,1,len(x)) ## this should be scaled to your "location"
## generate some fake landmass types (either 0, 1, or 2) with probabilites
y_land=np.random.choice(3, len(x), p=[0.1, 0.6, 0.3])
print(y_land)
fig=plt.figure()
ax=plt.axes()
clev=np.arange(0.,50.,.5)
## adjust the "height" of the landmass
x0,x1=0,1
y0,y1=0,0.05 ## y1 is the "height" of the landmass
## make sure that you're passing sensible zorder here and in your .contourf()
im = ax.imshow(y_land.reshape((-1,len(x))),cmap=landmass_cmap,zorder=2,extent=(x0,x1,y0,y1))
plt.contourf(x,y,z,clev,extend='max',zorder=1)
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax.plot()
ax.set_xlabel('Location')
ax.set_ylabel('Height above ground level [m]')
cbar = plt.colorbar()
cbar.ax.set_ylabel('o3 mixing ratio [ppb]')
## add a colorbar for your listed colormap
cax = fig.add_axes([0.2, 0.95, 0.5, 0.02]) # x-position, y-position, x-width, y-height
bounds = [0,1,2,3]
norm = colors.BoundaryNorm(bounds, landmass_cmap.N)
cb2 = colorbar.ColorbarBase(cax, cmap=landmass_cmap,
norm=norm,
boundaries=bounds,
ticks=[0.5,1.5,2.5],
spacing='proportional',
orientation='horizontal')
cb2.ax.set_xticklabels(['sea','land','ice'])
plt.show()
yields:

Phase plot using matplotlib tricontourf

I want to plot an image of the results of a finite element simulation with a personalized colormap.
I have been trying to use tricontourf to plot it as follow :
#Z = self.phi.compute_vertex_values(self.mesh)
Z = np.mod(self.phi.compute_vertex_values(self.mesh),2*np.pi)
triang = tri.Triangulation(*self.mesh.coordinates().reshape((-1, 2)).T,
triangles=self.mesh.cells())
zMax = np.max(Z)
print(zMax)
#Colormap creation
nColors = np.max(Z)*200/(2*np.pi)
phiRange = np.linspace(0,zMax,nColors)
intensity = np.sin(phiRange)**2
intensityArray = np.array([intensity, intensity, intensity])
colors = tuple(map(tuple, intensityArray.T))
self.cm = LinearSegmentedColormap.from_list("BAM", colors, N=nColors)
#Figure creation
fig, ax = plt.subplots()
levels2 = np.linspace(0., zMax,nColors)
cax = ax.tricontourf(triang, Z,levels=levels2, cmap = self.cm) #plot of the solution
fig.colorbar(cax)
ax.triplot(triang, lw=0.5, color='yellow') #plot of the mesh
plt.savefig("yolo.png")
plt.close(fig)
And it gives the result :
As you can see there are some trouble where the phase goes from 2pi to 0 that comes from tricontourf when there is a modulo...
My first idea for work around was to work directly on my phase Z. The problem is that if I do this I need to create a much larger colormap. Ultimately, the phase will be very large and so will be the colormap if I want a correct color resolution... Furthemore I would like to have only one period in the colormap on the right (just like in the first figure).
Any idea how I could obtain a figure just like the second one, with a colormap just like the one from the first figure and without creating a very large and expensive colormap ?
EDIT : I have written a small code that is runnable out of the box : It reproduces the problem I have and I have also tried to apply Thomas Kuhn answer to my preoblem. However, it seems that there are some problem with the colorbar... Any idea how I could fix this ?
import matplotlib.pyplot as plt
import matplotlib.tri as mtri
import numpy as np
import matplotlib.colors as colors
class PeriodicNormalize(colors.Normalize):
def __init__(self, vmin=None, vmax=None, clip=False):
colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
x, y = [self.vmin, self.vmax], [0, 1]
return np.ma.masked_array(np.interp(
np.mod(value-self.vmin, self.vmax-self.vmin),x,y
))
# Create triangulation.
x = np.asarray([0, 1, 2, 3, 0.5, 1.5, 2.5, 1, 2, 1.5])
y = np.asarray([0, 0, 0, 0, 1.0, 1.0, 1.0, 2, 2, 3.0])
triangles = [[0, 1, 4], [1, 2, 5], [2, 3, 6], [1, 5, 4], [2, 6, 5], [4, 5, 7],
[5, 6, 8], [5, 8, 7], [7, 8, 9]]
triang = mtri.Triangulation(x, y, triangles)
cm = colors.LinearSegmentedColormap.from_list('test', ['k','w','k'], N=1000)
#Figure 1 : modulo is applied on the data :
#Results : problem with the interpolation, but the colorbar is fine
z = np.mod(10*x,2*np.pi)
zMax = np.max(z)
levels = np.linspace(0., zMax,100)
fig1, ax1 = plt.subplots()
cax1=ax1.tricontourf(triang, z,cmap = cm,levels= levels)
fig1.colorbar(cax1)
plt.show()
#Figure 2 : We use the norm parameter with a custom norm that does the modulo
#Results : the graph is the way it should be but the colormap is messed up
z = 10*x
zMax = np.max(z)
levels = np.linspace(0., zMax,100)
fig2, ax2 = plt.subplots()
cax2=ax2.tricontourf(triang, z,levels= levels,norm = PeriodicNormalize(0, 2*np.pi),cmap = cm)
fig2.colorbar(cax2)
plt.show()
Last solution would be to do as I did above : to create a much larger colormap that goes up to zmax and is periodic every 2 pi. However the colorbar would not be nice...
here are the results :
I'm guessing that your problem arises from using modulo on your data before you call tricontourf (which, I guess, does some interpolation on your data and then maps that interpolated data to a colormap). Instead, you can pass a norm to your tricontourf function. Writing a small class following this tutorial, you can make the norm take care of the modulo of your data. As your code is not runnable as such, I came up with an a bit simpler example. Hopefully this is applicable to your problem:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as colors
class PeriodicNormalize(colors.Normalize):
def __init__(self, vmin=None, vmax=None, clip=False):
colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
x, y = [self.vmin, self.vmax], [0, 1]
return np.ma.masked_array(np.interp(
np.mod(value-self.vmin, self.vmax-self.vmin),x,y
))
fig,ax = plt.subplots()
x,y = np.meshgrid(
np.linspace(0, 1, 1000),
np.linspace(0, 1, 1000),
)
z = x*10*np.pi
cm = colors.LinearSegmentedColormap.from_list('test', ['k','w','k'], N=1000)
ax.pcolormesh(x,y,z,norm = PeriodicNormalize(0, 2*np.pi), cmap = cm)
plt.show()
The result looks like this:
EDIT:
As the ContourSet you get back from tricontourf spans the full phase, not just the first [0,2pi], the colorbar is created for that full range, which is why you see the colormap repeat itself many times. I'm not quite sure if I understand how the ticks are created, but I'm guessing that it would be quite some work to get that automated to work right. Instead, I suggest to generate a colorbar "by hand", as is done in this tutorial. This, however, requires that you create the axes (cax) where the colorbar is put yourself. Luckily there is a function called matplotlib.colorbar.make_axes() that does this for you (all thanks goes to this answer). So, instead of your original colorbar command, use these two lines:
cax,kw = mcbar.make_axes([ax2], location = 'right')
cb1 = mcbar.ColorbarBase(cax, cmap = cm, norm = norm, orientation='vertical')
To get this picture:

Categories

Resources