Matplotlib - sequentially creating figures with the same size - python-3.x

I need to create a sequence of .pdf files where each .pdf contains a figure with five plots.
As I am going to include them in a LaTeX article, I wanted them all to be the same width and height so that each figure's corners are vertically aligned on both left and right sides.
I thought this would be enough, but apparently not:
common_figsize=(6,5)
fig, ax = plt.subplots(figsize = common_figsize)
# five plots in a loop for the first figure.
# my_code()...
plt.savefig("Figure-1.pdf", transparent=True)
plt.close(fig)
fig, ax = plt.subplots(figsize = common_figsize)
# five plots in a loop for the new figure.
# my_code()...
plt.savefig("Figure-2.pdf", transparent=True)
plt.close(fig)
If I understand correctly, this does not do exactly what I want because of different scales originating from different yticks resolutions.
For both figures, pyplot is fed the same list for xticks.
In this case, it is a list of 50 values, from 1 to 50.
CHUNK_COUNT = 50
x_step = CHUNK_COUNT / 10
new_xticks = list(range(x_step, CHUNK_COUNT + x_step, x_step)) + [1]
plt.xticks(new_xticks)
ax.set_xlim(left=1, right=CHUNK_COUNT)
This creates both figures with an X-axis that goes from 1 to 50.
So far so good.
However, I haven't figured out how to deal with the problem of yticks resolution.
One of the figures had less yticks than the other, so I overrode it to have as many ticks as the other:
# Add yticks to Figure 1.
y_divisor = 6
y_step = (100 - min_y_tick) / y_divisor
new_yticks = [min_y_tick + y_step * i for i in range(0, y_divisor + 1)]
plt.yticks(new_yticks)
This resulted in the following images:
(click on each to open in new tab to see that in fact the bounding square of each figure is different)
Figure 1:
Figure 2:
In summary, I believe matplotlib is accepting the figsize parameter, but then rearranges plot elements to accommodate for different tick values and text lengths.
Is it possible for it to operate in reverse? To change label and text rotations automagically so that the squares are absolutely the same length and height?
Apologies if this is a duplicate and thanks for the help.
EDIT:
Finally able to provide a minimal, complete and verifiable example.
Among the tests, I removed the custom yticks code and the problem still persists:
from matplotlib.lines import Line2D
import matplotlib.ticker as mtick
import matplotlib.pyplot as plt
from matplotlib import rc
# activate latex text rendering
rc('text', usetex=True)
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})
CHUNK_COUNT = 50
common_figsize=(6,5)
plot_counter = 5
x_step = int(int(CHUNK_COUNT) / 10)
new_xticks = list(range(x_step, int(CHUNK_COUNT) + x_step, x_step)) + [1]
##### Plot Figure 1
fig, ax = plt.subplots(figsize = common_figsize)
plt.ylabel("Summary of a simple YY axis")
plt.yticks(rotation=45)
ax.yaxis.set_major_formatter(mtick.PercentFormatter(is_latex=False))
for i in range(0, plot_counter):
xvals = range(1, CHUNK_COUNT + 1)
yvals = []
for j in xvals:
yvals.append(j + i)
plt.plot(xvals, yvals)
plt.xticks(new_xticks)
ax.set_xlim(left=1, right=int(CHUNK_COUNT))
plt.savefig("Figure_1.png", transparent=True)
plt.close(fig)
##### Plot Figure 2
fig, ax = plt.subplots(figsize = common_figsize)
plt.ylabel("Summary of another YY axis")
plt.yticks(rotation=45)
ax.yaxis.set_major_formatter(mtick.PercentFormatter(is_latex=False))
for i in range(0, plot_counter):
xvals = range(1, CHUNK_COUNT + 1)
yvals = []
for j in xvals:
yvals.append((j + i) / 100)
plt.plot(xvals, yvals)
plt.xticks(new_xticks)
ax.set_xlim(left=1, right=int(CHUNK_COUNT))
plt.savefig("Figure_2.png", transparent=True)
plt.close(fig)

It turns out this was due to a mistake on my part.
I carried over code from another context where
autolayout
was active:
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})
After setting it to False, the figure squares all had the same dimensions:
from matplotlib import rcParams
rcParams.update({'figure.autolayout': False})
Despite the length differences in ytick elements, it is now respecting the dimensions specified in my original question.
These results were generated with the MWE example I added at the end of my question:

Related

How to get / set the correct (formatted) yticks of a colorbar in matplotlib without whitespace in the colorbar?

How to get the correct yticks of a colorbar in matplotlib without whitespace in the colorbar?
This is my code, note that the colors of the colorbar are misaligned if I apply .set_ticks() using the (formatted) values I got through get.ticks(), these values (as printed in the output) seem incorrect as the minimum shown is 15 but my minimum input value is 17.15116279.
import geopandas as gpd # version 0.11.0
import matplotlib.pyplot as plt # version 3.5.2
import matplotlib.colors as clr
from matplotlib import colorbar
from matplotlib.colors import Normalize # tbv colorbar
from matplotlib import cm
import matplotlib.ticker as mtick
cmap = clr.LinearSegmentedColormap.from_list('custom blue', ["#fce19c", "#c4ddee"], N=400)
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
world = world[(world.pop_est>0) & (world.name!="Antarctica")]
vals = [22.36958444, 29.21348315, 30.74534161, 37.42331288, 20.,
19.31407942, 26.08695652, 26.36165577, 25.0, 17.79279279,
17.15116279, 19.60784314]
world = world[:len(vals)]
world['gdp_per_cap'] = vals
fig, ax = plt.subplots(1, 1)
ax = world.plot(column='gdp_per_cap', ax=ax, legend=False, cmap=cmapgeelblauw)
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1)
vmin = world['gdp_per_cap'].min()
vmax = world['gdp_per_cap'].max()
norm = Normalize(vmin=vmin, vmax=vmax)
n_cmap = cm.ScalarMappable(norm=norm, cmap=cmap)
n_cmap.set_array([])
cbar = fig.colorbar(n_cmap, cax=cax)
print(cax==cbar.ax) # True
vals = cbar.ax.get_yticks()
print(vals)
cbar.ax.yaxis.set_ticks(vals)
cbar.ax.set_yticklabels(['{:,.0%}'.format(x/100) for x in vals])
plt.show()
Note that the colorbar remains correct is if
cbar.ax.yaxis.set_ticks(vals)
is not applied. But in that case I get the warning "UserWarning: FixedFormatter should only be used together with FixedLocator".
Also note: to avoid the issue I could apply a format this way:
cax_format = mtick.PercentFormatter(decimals=2)
cbar = fig.colorbar(n_cmap, cax=cax, format=cax_format)
And if I add the line
fig.draw_without_rendering()
# followed by vals = cbar.ax.get_yticks()
as suggested by Stef in the comments then the values are different (but still incorrect from my point of view) and the colorbar gets a 2nd white area due to this:
This is what is looks like if I do not set the ticks: This is what I am after but the warning made me set the ticks and realise that something may be wrong.
Based on the 2nd comment by Stef: "note that not necessarily all ticks are within the view limits, i.e. this first and last one may not actually be displayed. Manually setting ticks, on the other hand, expands the view limits to the ticks range given. If these are outside vmin / vmax it will cause the white gap you see."
Indeed, if I manually adjust the values as follows:
fig.draw_without_rendering()
vals = cbar.ax.get_yticks()
print(vals)
vals = [vmin] + vals[1:-1].tolist() + [vmax]
print(vals)
cbar.ax.yaxis.set_ticks(vals)
vals = ['{:,.0%}'.format(x/100) for x in vals]
vals = [''] + vals[1:-1] + ['']
print(vals)
cbar.ax.set_yticklabels(vals)
plt.show()
Then you get:
By manually setting the ticks and tick labels, you create a fixed locator and a corresponding function formatter. Using a fixed locator is seldom the optimal solution due to the possible pitfalls outlined in the comments.
If you just want to add a % sign and/or change the number of decimals, you can use a string formatter which is implicitely created when you pass a formatting string to set_major_formatter:
cax.yaxis.set_major_formatter('{x:g} %')

Control marker properties in seaborn pairwise boxplot

I'm trying to plot a boxplot for two different datasets on the same plot. The x axis are the hours in a day, while the y axis goes from 0 to 1 (let's call it Efficiency). I would like to have different markers for the means of each dataset' boxes. I use the 'meanprops' for seaborn but that changes the marker style for both datasets at the same time. I've added 2000 lines of data in the excel that can be downloaded here. The values might not coincide with the ones in the picture but should be enough.
Basically I want the red squares to be blue on the orange boxplot, and red on the blue boxplot. Here is what I managed to do so far:
I tried changing the meanprops by using a dictionary with the labels as keys , but it seems to be entering a loop (in PyCharm is says Evaluating...)
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
#make sure you have your path sorted out
group1 = pd.read_excel('group1.xls')
ax,fig = plt.subplots(figsize = (20,10))
#does not work
#ax = sns.boxplot(data=group1, x='hour', y='M1_eff', hue='labels',showfliers=False, showmeans=True,\
# meanprops={"marker":{'7':"s",'8':'s'},"markerfacecolor":{'7':"white",'8':'white'},
#"markeredgecolor":{'7':"blue",'8':'red'})
#works but produces similar markers
ax = sns.boxplot(data=group1, x='hour', y='M1_eff', hue='labels',showfliers=False, showmeans=True,\
meanprops={"marker":"s","markerfacecolor":"white", "markeredgecolor":"blue"})
plt.legend(title='Groups', loc=2, bbox_to_anchor=(1, 1),borderaxespad=0.5)
# Add transparency to colors
for patch in ax.artists:
r, g, b, a = patch.get_facecolor()
patch.set_facecolor((r, g, b, .4))
ax.set_xlabel("Hours",fontsize=14)
ax.set_ylabel("M1 Efficiency",fontsize=14)
ax.tick_params(labelsize=10)
plt.show()
I also tried the FacetGrid but to no avail (Stops at 'Evaluating...'):
g = sns.FacetGrid(group1, col="M1_eff", hue="labels",hue_kws=dict(marker=["^", "v"]))
g = (g.map(plt.boxplot, "hour", "M1_eff")
.add_legend())
g.show()
Any help is appreciated!
I don't think you can do this using sns.boxplot() directly. I think you'll have to draw the means "by hand"
N=100
df = pd.DataFrame({'hour':np.random.randint(0,3,size=(N,)),
'M1_eff': np.random.random(size=(N,)),
'labels':np.random.choice([7,8],size=(N,))})
x_col = 'hour'
y_col = 'M1_eff'
hue_col = 'labels'
width = 0.8
hue_order=[7,8]
marker_colors = ['red','blue']
# get the offsets used by boxplot when hue-nesting is used
# https://github.com/mwaskom/seaborn/blob/c73055b2a9d9830c6fbbace07127c370389d04dd/seaborn/categorical.py#L367
n_levels = len(hue_order)
each_width = width / n_levels
offsets = np.linspace(0, width - each_width, n_levels)
offsets -= offsets.mean()
fig, ax = plt.subplots()
ax = sns.boxplot(data=df, x=x_col, y=y_col, hue=hue_col, hue_order=hue_order, showfliers=False, showmeans=False)
means = df.groupby([hue_col,x_col])[y_col].mean()
for (gr,temp),o,c in zip(means.groupby(level=0),offsets,marker_colors):
ax.plot(np.arange(temp.values.size)+o, temp.values, 's', c=c)

making multiple plot at the same time in python3

I have a list and a python array like these 2 examples:
example:
Neg = [37.972200755611425, 32.14963079785344]
Pos = array([[15.24373185, 13.66099865, 11.86959384, 9.72792045, 7.12928302, 6.04439412],[14.5235007 , 13. , 11.1792871 , 9.14974712, 6.4429435 , 5.04439412]
both Neg and Pos have 2 elements (in this example) therefore I would like to make 2 separate plots (pdf file) for every element.
in every plot there would be 2 lines:
1- comes from Pos and is a line plot basically which is made of all the elements in the sub-list.
2- comes from Neg and is a horizontal line on the y-axis.
I am trying to do that in a for loop for all elements at the same time. to do so, I made the following code in python but it does not return what I would like to get. do you know how to fix it ?
for i in range(len(Neg)):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(concentration, Pos[i], label='gg')
plt.axhline(y=Neg[i], color='b', linestyle='-')
ax.legend()
ax.set_xlabel("log2 concentration")
ax.set_ylabel("log2 raw counts")
ax.set_ylim(0, 40)
plt.savefig(f'{i}.pdf')
Not quite sure exactly what you want but this code creates two subplots of the data in the way I think you're describing it:
import numpy as np
from matplotlib import pyplot as plt
Neg = [37.972200755611425, 32.14963079785344]
Pos = np.array([[15.24373185, 13.66099865, 11.86959384, 9.72792045, 7.12928302, 6.04439412],[14.5235007 , 13. , 11.1792871 , 9.14974712, 6.4429435 , 5.04439412]])
fig = plt.figure()
for i in range(len(Neg)):
ax = fig.add_subplot(2,1,i+1)
ax.plot(Pos[i], label='gg')
plt.axhline(y=Neg[i], color='b', linestyle='-')
ax.legend()
ax.set_xlabel("log2 concentration")
ax.set_ylabel("log2 raw counts")
ax.set_ylim(0, 40)
plt.subplots_adjust(hspace=1.0)
extent = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(f'{i}.pdf', bbox_inches=extent.expanded(1.2, 1.9))
Edited the code to save each subplot individually to file by grabbing a specific part of the plot for saving, as used in this question: Save a subplot in matplotlib.
Also included some additional spacing between each subplot by calling subplots_adjust(), so that each subplot can be saved to individual files without any detail from the other subplots being included. This might not be the best way of doing what you want, but I think it will do what you want now.
Alternatively, if you're not set on using subplots, you could always just use a plot per element:
fig = plt.figure()
for i in range(len(Neg)):
plt.plot(Pos[i], label='gg')
plt.axhline(y=Neg[i], color='b', linestyle='-')
plt.legend()
plt.xlabel("log2 concentration")
plt.ylabel("log2 raw counts")
plt.ylim(0, 40)
fig = plt.gcf()
fig.savefig(f'{i}.pdf')
plt.show()

Matplotlib sliding window not plotting correctly

I have a code that runs a rolling window (30) average over a range (i.e. 300)
So I have 10 averages but they plot against ticks 1-10 rather than spaced over every window of 30.
The only way I can get it to look right is to plot it over (len(windowlength)) but the x-axis isnt right.
Is there any way to manually space the results?
windows30 = (sliding_window(sequence, 30))
Overall_Mean = mean(sequence)
fig, (ax) = plt.subplots()
plt.subplots_adjust(left=0.07, bottom=0.08, right=0.96, top=0.92, wspace=0.20, hspace=0.23)
ax.set_ylabel('mean (%)')
ax.set_xlabel(' Length') # axis titles
ax.yaxis.grid(True, linestyle='-', which='major', color='lightgrey', alpha=0.5)
ax.plot(windows30, color='r', marker='o', markersize=3)
ax.plot([0, len(sequence)], [Overall_Mean, Overall_Mean], lw=0.75)
plt.show()
From what I have understood you have a list of length 300 but only holds 10 values inside. If that is the case, you can remove the other values that are None from your windows30 list using the following solution.
Code Demonstration:
import numpy as np
import random
import matplotlib.pyplot as plt
# Generating the list of Nones and numbers
listofzeroes = [None] * 290
numbers = random.sample(range(50), 10)
numbers.extend(listofzeroes)
# Removing Nones from the list
numbers = [value for value in numbers if value is not None]
step = len(numbers)
x_values = np.linspace(0,300,step) # Generate x-values
plt.plot(x_values,numbers, color='red', marker='o')
This is a working example, the relevant code for you is after the second comment.
Output:
The above code will work independently of where the Nones are located in your list. I hope this solves your problem.

How to combine gridspec with plt.subplots() to eliminate space between rows of subplots

I am trying to plot multiple images in subplots and either eliminate the space between subplots (horizontal and vertical) or control it. I tried to use the suggestion in How to Use GridSpec.... I also tried here but they are not using subplots(): space between subplots
I am able to eliminate the horizontal space but not the vertical space with what I am doing in the code below. Please do not mark as duplicate as I have tried the other posts and they do not do what I want. My code is shown below. Maybe there is another keyword argument that I need in the gridspec_kw dictionary?
I want to use plt.subplots() not plt.subplot() for this. In case it matters, the images are not square they are rectangular. I also tried adding f.tight_layout(h_pad=0,w_pad=0) before plt.show() but it did not change anything.
def plot_image_array_with_angles(img_array,correct_angles,predict_angles,
fontsize=10,figsize=(8,8)):
'''
Imports:
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib.pyplot as plt
'''
num_images = len(img_array)
grid = int(np.sqrt(num_images)) # will only show all images if square
#f, axarr = plt.subplots(grid,grid,figsize=figsize)
f, axarr = plt.subplots(grid,grid,figsize=figsize,
gridspec_kw={'wspace':0,'hspace':0})
im = 0
for row in range(grid):
for col in range(grid):
axarr[row,col].imshow(img_array[im])
title = 'cor = ' + str(correct_angles[im]) + ' pred = ' + str(predict_angles[im])
axarr[row,col].set_title(title,fontsize=fontsize)
axarr[row,col].axis('off') # turns off all ticks
#axarr[row,col].set_aspect('equal')
im += 1
plt.show()
return
The aspect ratio of an imshow plot is automatically set such that pixels in the image are squared. This setting is stronger than any of the subplots_adjust or gridspec settings for spacing. Or in other words you cannot directly control the spacing between subplots if those subplots have their aspect set to "equal".
First obvious solution is to set the image aspect to automatic ax.set_aspect("auto"). This solves the problem of subplot spacing, but distorts the images.
The other option is to adjust the figure margins and the figure size such that the spacing between the subplots is as desired.
Let's say figh and figw are the figure height and width in inch, and s the subplot width in inch. The margins are bottom, top, left and right (relative to figure size) and the spacings hspace in vertical and wspace in horizontal direction (relative to subplot size). The number of rows is denoted n and the number of columns m. The aspect is the ratio between subplot (image) height and width (aspect = image height / image width).
Then the dimensions can be set via
fig, axes = plt.subplots(nrows=n, ncols=m, figsize=(figwidth, figheight))
plt.subplots_adjust(top=top, bottom=bottom, left=left, right=right,
wspace=wspace, hspace=hspace)
The respective values can be calculated according to:
Or, if the margins are the same:
An example:
import matplotlib.pyplot as plt
image = plt.imread("https://i.stack.imgur.com/9qe6z.png")
aspect = image.shape[0]/float(image.shape[1])
print aspect
n = 2 # number of rows
m = 4 # numberof columns
bottom = 0.1; left=0.05
top=1.-bottom; right = 1.-left
fisasp = (1-bottom-(1-top))/float( 1-left-(1-right) )
#widthspace, relative to subplot size
wspace=0.15 # set to zero for no spacing
hspace=wspace/float(aspect)
#fix the figure height
figheight= 3 # inch
figwidth = (m + (m-1)*wspace)/float((n+(n-1)*hspace)*aspect)*figheight*fisasp
fig, axes = plt.subplots(nrows=n, ncols=m, figsize=(figwidth, figheight))
plt.subplots_adjust(top=top, bottom=bottom, left=left, right=right,
wspace=wspace, hspace=hspace)
for ax in axes.flatten():
ax.imshow(image)
ax.set_title("title",fontsize=10)
ax.axis('off')
plt.show()

Resources