Python - matlibplot - control figure size - python

I have some problems on controlling the figure size when using matlibplot. I want the figure to be 9cm * 14.5 cm in size exactly. I use "figsize" command to control the figure size; however, the figure exceeds the specified size. Please advise how to achieve this.
The python codes are attached as follows:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize = (9/2.54, 14.5/2.54), nrows = 2, ncols = 1)
ax[0] = plt.subplot(2, 1, 1)
x1 = [30, 40, 50, 60, 70, 80, 90, 100]
y1 = [6, 7, 8, 9, 10, 11, 12, 13]
ax[0].plot(x1, y1, 'r-s', label = 'XXXXXXXX')
ax[0].set_xlabel('Percentage load (%)')
ax[0].set_ylabel('Output parameter (%)')
ax[0].legend(loc = (0.05, 0.8))
ax[0].text(0.05, 0.7, '(a)', transform = ax[0].transAxes)
ax[0].set_xlim(x1[0], x1[-1])
x2 = [30, 40, 50, 60, 70, 80, 90, 100]
y2 = [6, 7, 8, 9, 10, 11, 12, 13]
ax[1] = plt.subplot(2, 1, 2)
ax[1].plot(x2, y2, 'k-o', label = 'YYYYYYYYY')
ax[1].set_xlabel('Percentage load (%)')
ax[1].set_ylabel('Output parameter (%)')
ax[1].legend(loc = (0.05, 0.8))
ax[1].text(0.05, 0.7, '(b)', transform = ax[1].transAxes)
ax[1].set_xlim(x2[0], x2[-1])
plt.subplots_adjust(top=1, bottom=0.0, left=0.0, right=1, hspace=0.2, wspace=0.0)
plt.savefig(fname = '2figax.png', dpi = 600, quality = 95, bbox_inches = 'tight', pad_inches = None)

Related

Merge dataframes with mirrored values

I have a dataframe which stores measurement points of an circular area. So each point has a Radius_mm and Angle_deg value.
As a visual representation of the data, I would now like to create a section through the surface. I.e. I choose one angle and the corresponding angle that lies at 180° to it, including the center.
The x-axis should display the Radius_mm and the y-axis the Value.
I could nearly archive this, as shown below. By plotting each data set separately, the result is unfortunatly not connected. I am sure there is a more elegant way, but couldn't get to it, can some help?
Thanks!
import pandas as pd
import matplotlib.pyplot as plt
df = pd.DataFrame(
{
"Point": (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15),
"Radius_mm": (0, 0.5, 0.5, 0.75, 0.75, 1.25, 1.25, 1.75, 1.75, 2, 2, 2.25, 2.25, 2.5, 2.5),
"Angle_deg": (0, 45, 225, 45, 225, 45, 225, 45, 225, 45, 225, 45, 225, 45, 225),
"Value": (70, 68, 66.75, 68.08, 66.72, 68.44, 67.31, 68.82, 68.02, 68.93, 68.41, 69.39, 68.3, 68.54, 68.55),
}
)
# first angle
filtered_df = df.loc[df["Angle_deg"] == 45]
# second angle = first angle + 180°
filtered_df2 = df.loc[df["Angle_deg"] == 225]
# x values ("mirrored")
xvalues_for_mirrored_angle = [x*-1 for x in filtered_df2["Radius_mm"].tolist()]
# center value
filtered_df3 = df.loc[df["Angle_deg"] == 0]
xvalue_center = [x for x in filtered_df3["Radius_mm"].tolist()]
# combining x axis values
xtick_values = xvalues_for_mirrored_angle + xvalue_center + filtered_df["Radius_mm"].tolist()
fig, ax = plt.subplots()
ax.plot("Radius_mm", "Value", data=filtered_df, marker="+", label=(f"Angle = 45"))
ax.plot(xvalues_for_mirrored_angle, "Value", data=filtered_df2, marker="+", label=(f"Angle = 225"))
ax.plot("Radius_mm", "Value", data=filtered_df3, marker="+", label=(f"Angle = Center"))
ax.grid(True)
ax.set_xticks(xtick_values)
ax.set_xticklabels(xtick_values, rotation = 45)
ax.set_xlabel("Radius")
ax.legend(fontsize=10)
fig.tight_layout()
plt.show()
You don't need the filtered_dfs, you can do most of this in a couple of lines of Pandas, and keep the line connected:
df = pd.DataFrame(... # as above
df.loc[df.Angle_deg==225, 'Radius_mm'] *= -1 # flip the reverse angle
df = df.sort_values(by='Radius_mm')
then the plot:
df.plot(x='Radius_mm', y='Value', marker='+', legend=False, grid=True, xlabel='radius', xticks=df['Radius_mm'], rot=45);
If you want to keep the colours separate, you can replace the last line with this:
f, ax = plt.subplots()
ax.plot("Radius_mm", "Value", data=df, marker="+", label=(f"Angle = 0"))
ax.plot("Radius_mm", "Value", data=df[df["Angle_deg"] == 45], marker="+", label=(f"Angle = 45"))
ax.plot("Radius_mm", "Value", data=df[df["Angle_deg"] == 225], marker="+", label=(f"Angle = 225"))
ax.grid(True)
ax.set_xticks(df["Radius_mm"])
ax.set_xticklabels(df["Radius_mm"], rotation = 45)
ax.set_xlabel("Radius")
ax.legend(fontsize=10)
f.tight_layout()
plt.show()

Output Values from Regression Line inside Matplotlib window

import matplotlib.pyplot as plt
import numpy as np
x = np.array([6, 15, 24, 33, 41, 52, 59, 66, 73, 81])
y = np.array([5, 10, 15, 20, 25, 30, 35, 40, 45, 50])
coef = np.polyfit(x, y, 1)
poly1d_fn = np.poly1d(coef) # to create a linear function with coefficients
plt.plot(x, y, 'ro', x, poly1d_fn(x), '-b')
plt.errorbar(x, poly1d_fn(x), yerr=poly1d_fn(x) - y, fmt='.k')
plt.show()
I have a working code which produces based upon my input a graph with error bars and the regression line. That's all fine. Now what I wanted to do is add a text box below and once a user inputs a number, e.g. 12 it outputs the according value (re regression line).
left, bottom, width, height = 0.15, 0.02, 0.7, 0.10
plt.subplots_adjust(left=left, bottom=0.25) # Make space for the slider
input_field = plt.axes([left, bottom, width, height])
box = TextBox(input_field, 'value')
I tried it with this approach. Though being unsuccessful: I can't get it to take a value and output it on the GUI interface matplotlib provides. The field would need to be checked for every input. Matplotlib offers on_text_change(self, func)or on_submit(self, func), so that might be working - but how to output?
Does anyone have an idea?
I would use a simple Text artist to display the result. But being fancy, I would also display lines on the graph showing the input and output values.
import matplotlib.pyplot as plt
import numpy as np
x = np.array([6, 15, 24, 33, 41, 52, 59, 66, 73, 81])
y = np.array([5, 10, 15, 20, 25, 30, 35, 40, 45, 50])
coef = np.polyfit(x, y, 1)
poly1d_fn = np.poly1d(coef) # to create a linear function with coefficients
def submit(val):
try:
x = float(val)
y = poly1d_fn(x)
ax.annotate('', xy=(x,0), xycoords=('data','axes fraction'),
xytext=(x,y), textcoords='data',
arrowprops=dict(arrowstyle='-', ls='--'))
ax.annotate(f'{x:.2f}', xy=(x,0), xycoords=('data','axes fraction'))
ax.annotate('', xy=(0,y), xycoords=('axes fraction','data'),
xytext=(x,y), textcoords='data',
arrowprops=dict(arrowstyle='-', ls='--'))
ax.annotate(f'{y:.2f}', xy=(0,y), xycoords=('axes fraction','data'))
output_box.set_text(f'Result = {y:.2f}')
plt.draw()
except ValueError:
pass
fig, ax = plt.subplots()
ax.plot(x, y, 'ro', x, poly1d_fn(x), '-b')
ax.errorbar(x, poly1d_fn(x), yerr=poly1d_fn(x) - y, fmt='.k')
left, bottom, width, height, pad = 0.15, 0.02, 0.3, 0.10, 0.1
fig.subplots_adjust(left=left, bottom=0.25) # Make space for the slider
input_field = fig.add_axes([left, bottom, width, height])
text_box = matplotlib.widgets.TextBox(input_field, 'value')
text_box.on_submit(submit)
output_box = fig.text(left+width+pad, bottom+height/2, s='Result = ', va='center')

Ylabel rescale range and end at 0%

import numpy as np
import matplotlib.pyplot as plt
n = 1000
x = np.arange(0, n)
y1 = np.random.normal(50, 4, n)
y2 = np.random.normal(25, 2.5, n)
y3 = np.random.normal(10, 1.1, n)
fig, (ax1, ax2, ax3) = plt.subplots(nrows = 3, ncols = 1)
ax1.plot(x, y1, 'royalblue')
ax1.set(xticks = [], title = 'Title')
ax2.plot(x, y2, 'darkorange')
ax2.set(xticks = [])
ax3.plot(x, y3, 'forestgreen')
ax3.set(xlabel = 'Random sample')
fig.legend(['First', 'Second', 'Third'])
plt.show()
I would like the ylabels to be shown in percentage, start at 0% and decrease. For example the blue one should go from [30, 40, 50, 60, 70] to [-57.1%, -42.9%, -28.6%, -14.3%, 0%]. The yellow one should go from [10, 20, 30, 40] to [-75%, -50%, -25%, 0%] and the green one should go from [5, 7.5, 10, 12.5, 15] to [-66.6%, -50%, -33.3%, -16.7%, 0%].
The rest of the graphs should look exactly the same, only the ylabels should change.
Just convert your current yticks to floats and change to the range you want them to be at before displaying:
import numpy as np
ticks = [float(x) for x in yvals]
ticks = np.array(ticks) - max(ticks)
yticklabels = ['{0:.1%}'.format(x) for x in ticks]
Do this for each plot separately.

Removing the bottom error caps only on matplotlib

I wanted to display only half error bars, as they are symetric ; as I had no clue how to do this with a "clean way", I chose to use asymetric errors with 0 on the bottom side ; but then, when I displayed caps, I realised this was not the best way to do this.
Here's the code :
import numpy as np
import matplotlib.pyplot as plt
N = 5
men_means = (20, 35, 30, 35, 27)
men_std = (2, 3, 4, 1, 2)
ind = np.arange(N)
width = 0.35
fig, ax = plt.subplots()
rects1 = ax.bar(ind, men_means, width, color='r',yerr=[np.zeros(len(men_std)),men_std],capsize = 5)
women_means = (25, 32, 34, 20, 25)
women_std = (3, 5, 2, 3, 3)
rects2 = ax.bar(ind + width, women_means, width, color='y',yerr=[np.zeros(len(women_std)),women_std],capsize = 5)
plt.show()
And this is the plot I get :. As you can see, my way of plotting half error bars is probably not what should be done.
So is there any way to hide the bottom cap line or a better way to plot half error bars ?
ax.errorbar has the option to set uplims=True or lolims=True to signify that the means repesent the upper or lower limits, respectively. Unfortunately, it doesn't seem like you can use these options directly with ax.bar, so we have to plot the errorbar and the bar plot separately.
The documentation for the uplims/lolims options in ax.errorbar:
lolims / uplims / xlolims / xuplims : bool, optional, default:None
These arguments can be used to indicate that a value gives only upper/lower limits. In that case a caret symbol is used to indicate this. lims-arguments may be of the same type as xerr and yerr. To use limits with inverted axes, set_xlim() or set_ylim() must be called before errorbar().
Note that using this option changes your caps to arrows. See below for an example of how to change them back to caps, if you need flat caps instead of arrows.
You can see these options in action in this example on the matplotlib website.
Now, here's your example, modified:
import numpy as np
import matplotlib.pyplot as plt
N = 5
men_means = (20, 35, 30, 35, 27)
men_std = (2, 3, 4, 1, 2)
ind = np.arange(N)
width = 0.35
fig, ax = plt.subplots()
rects1 = ax.bar(ind, men_means, width, color='r')
err1 = ax.errorbar(ind, men_means, yerr=men_std, lolims=True, capsize = 0, ls='None', color='k')
women_means = (25, 32, 34, 20, 25)
women_std = (3, 5, 2, 3, 3)
rects2 = ax.bar(ind + width, women_means, width, color='y')
err2 = ax.errorbar(ind + width, women_means, yerr=women_std, lolims=True, capsize = 0, ls='None', color='k')
plt.show()
If you don't like the arrows, you change them to flat caps, by changing the marker of the caplines that are returned (as the second item) from ax.errorbar. We can change them from the arrows to the marker style _, and then control their size with .set_markersize:
import numpy as np
import matplotlib.pyplot as plt
N = 5
men_means = (20, 35, 30, 35, 27)
men_std = (2, 3, 4, 1, 2)
ind = np.arange(N)
width = 0.35
fig, ax = plt.subplots()
rects1 = ax.bar(ind, men_means, width, color='r')
plotline1, caplines1, barlinecols1 = ax.errorbar(
ind, men_means, yerr=men_std, lolims=True,
capsize = 0, ls='None', color='k')
caplines1[0].set_marker('_')
caplines1[0].set_markersize(20)
women_means = (25, 32, 34, 20, 25)
women_std = (3, 5, 2, 3, 3)
rects2 = ax.bar(ind + width, women_means, width, color='y')
plotline2, caplines2, barlinecols2 = ax.errorbar(
ind + width, women_means, yerr=women_std,
lolims=True, capsize = 0, ls='None', color='k')
caplines2[0].set_marker('_')
caplines2[0].set_markersize(10)
plt.show()
A simpler solution is to use zorder. The grid has zorder=0. Setting the errorbar to zorder=1 and the bar to zorder=2and lowering the bottom error a bit will hide the lower error bar with little effort. This also allows to use bar_label. The only downside is if alpha is used for the bars.
I also changed to use np.zeros_like(std) instead of np.zeros(len(std)) and use error_kw to style the errorbar.
import numpy as np
import matplotlib.pyplot as plt
N = 5
ind = np.arange(N)
width = 0.8
fig, ax = plt.subplots()
women_means = (25, 32, 34, 20, 25)
women_std = (3, 5, 2, 3, 3)
rects2 = ax.bar(ind + width, women_means, width, color='y',yerr=[np.zeros_like(women_std)+0.5,women_std],zorder=2,error_kw=dict(capsize = 10, capthick=1,zorder=1))
ax.bar_label(rects2,labels=[f'{v:.2f} ± {e:.2f}' for v,e in zip(women_means, women_std)], padding=10, fontsize=14, label_type='edge')
fig.tight_layout()
plt.show()
Many thanks,
based on your answer, method below is my solution:
def process_error_bar(ax, x, y, y_err, marker_size):
"""
hide half error_bar
:param ax: plt.subplots()
:param x: x position
:param y: y position
:param y_err: y errors
:param marker_size: size
"""
lolims = []
uplims = []
for y_value in y:
if y_value < 0:
lolims.append(False)
uplims.append(True)
else:
lolims.append(True)
uplims.append(False)
plotline, caplines, barlinecols = ax.errorbar(
x, y, yerr=y_err, lolims=lolims, uplims=uplims,
capsize=0, ls='None', color='k')
# [arrow] -> [-]
for capline in caplines:
capline.set_marker('_')
capline.set_markersize(marker_size)

Multiple Broken Axis On A Histogram in Matplotlib

So I've got some data which I wish to plot via a frequency density (unequal class width) histogram, and via some searching online, I've created this to allow me to do this.
import numpy as np
import matplotlib.pyplot as plt
plt.xkcd()
freqs = np.array([3221, 1890, 866, 529, 434, 494, 382, 92, 32, 7, 7])
bins = np.array([0, 5, 10, 15, 20, 30, 50, 100, 200, 500, 1000, 1500])
widths = bins[1:] - bins[:-1]
heights = freqs.astype(np.float)/widths
plt.xlabel('Cost in Pounds')
plt.ylabel('Frequency Density')
plt.fill_between(bins.repeat(2)[1:-1], heights.repeat(2), facecolor='steelblue')
plt.show()
As you may see however, this data stretches into the thousands on the x axis and on the y axis (density) goes from tiny data (<1) to vast data (>100). To solve this I will need to break both axis. The closest to help I've found so far is this, which I've found hard to use. Would you be able to help?
Thanks, Aj.
You could just use a bar plot. Setting the xtick labels to represent the bin values.
With logarithmic y scale
import numpy as np
import matplotlib.pyplot as plt
plt.xkcd()
fig, ax = plt.subplots()
freqs = np.array([3221, 1890, 866, 529, 434, 494, 382, 92, 32, 7, 7])
freqs = np.log10(freqs)
bins = np.array([0, 5, 10, 15, 20, 30, 50, 100, 200, 500, 1000, 1500])
width = 0.35
ind = np.arange(len(freqs))
rects1 = ax.bar(ind, freqs, width)
plt.xlabel('Cost in Pounds')
plt.ylabel('Frequency Density')
tick_labels = [ '{0} - {1}'.format(*bin) for bin in zip(bins[:-1], bins[1:])]
ax.set_xticks(ind+width)
ax.set_xticklabels(tick_labels)
fig.autofmt_xdate()
plt.show()

Categories

Resources