Reputation: 1936
Let's say I have 25 lines like this:
x = np.linspace(0, 30, 60)
y = np.sin(x/6*np.pi)
error = np.random.normal(0.1, 0.02, size=y.shape)
y1 = y+ np.random.normal(0, 0.1, size=y.shape)
y2= y+ np.random.normal(0, 0.1, size=y.shape)
plt.plot(x, y, 'k-')
plt.plot(x, y1, 'k-')
plt.plot(x, y2,'k-')
.
.
.
Now, I'd like to make a plot like this: . How do I automatically make these error bars and make the shading given just a bunch of lines, all carrying the same overall shape but with slight variations.
Upvotes: 2
Views: 5188
Reputation: 477
I had the same question as you and these other answers really just missed the point about x-axis errors. You can't display those with fill_between
, but with fill_betweenx
you can: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.fill_betweenx.html
If you want to display both y-axis and x-axis error bands, the only I way I could come up with is to draw both, but with a light color instead of some color with alpha, because obviously the both bands will overlap.
EDIT:
Since I did it anyway, here's a code example for if you need both bands and the output (not perfect but you should be able to iterate on it):
import matplotlib.pyplot as plt
import numpy as np
x = np.array([1, 2, 3, 4, 5])
y = np.array([2, 3, 5, 4, 6])
x_errors = np.array([0.5, 0.2, 0.3, 0.4, 0.25])
y_errors = np.array([0.2, 0.3, 0.25, 0.35, 0.1])
x_lower = x - x_errors
x_upper = x + x_errors
y_lower = y - y_errors
y_upper = y + y_errors
x_top_left_corner = x_lower + y_errors
x_top_right_corner = x_upper + y_errors
x_bottom_left_corner = x_lower - y_errors
x_bottom_right_corner = x_upper - y_errors
repeated_x = np.repeat(x, 2)
repeated_x_lower = np.repeat(x_lower, 2)
repeated_x_upper = np.repeat(x_upper, 2)
stacked_y_1 = np.stack((y_lower, y))
transposed_y_1 = stacked_y_1.T
interweaved_y_1 = transposed_y_1.ravel()
stacked_y_2 = np.stack((y, y_upper))
transposed_y_2 = stacked_y_2.T
interweaved_y_2 = transposed_y_2.ravel()
repeated_y = np.repeat(y, 2)
repeated_y_lower = np.repeat(y_lower, 2)
repeated_y_upper = np.repeat(y_upper, 2)
stacked_x_1 = np.stack((x_lower, x))
transposed_x_1 = stacked_x_1.T
interweaved_x_1 = transposed_x_1.ravel()
stacked_x_2 = np.stack((x, x_upper))
transposed_x_2 = stacked_x_2.T
interweaved_x_2 = transposed_x_2.ravel()
plt.figure(figsize=(10, 6))
plt.errorbar(x, y, xerr=x_errors, yerr=y_errors, fmt='o', linestyle='None', color='blue', label='points')
plt.fill_between(x, y_lower, y_upper, color='#ddd', alpha=1)
plt.fill_betweenx(y, x_lower, x_upper, color='#ddd', alpha=1)
plt.fill_between(repeated_x_lower, interweaved_y_1, interweaved_y_2, color='#ddd', alpha=1)
plt.fill_between(repeated_x, interweaved_y_1, interweaved_y_2, color='#ddd', alpha=1)
plt.fill_between(repeated_x_upper, interweaved_y_1, interweaved_y_2, color='#ddd', alpha=1)
plt.fill_betweenx(repeated_y_lower, interweaved_x_1, interweaved_x_2, color='#ddd', alpha=1)
plt.fill_betweenx(repeated_y, interweaved_x_1, interweaved_x_2, color='#ddd', alpha=1)
plt.fill_betweenx(repeated_y_upper, interweaved_x_1, interweaved_x_2, color='#ddd', alpha=1, label='error')
plt.plot(x, y, 'darkgray', label='line')
plt.xlabel('x-axis')
plt.ylabel('y-axis')
plt.legend()
plt.show()
Upvotes: 0
Reputation: 5199
You can do it only with matplot lib as follows:
def plot_with_error_bands(x: np.ndarray, y: np.ndarray, yerr: np.ndarray,
xlabel: str, ylabel: str,
title: str,
curve_label: Optional[str] = None,
error_band_label: Optional[str] = None,
color: Optional[str] = None, ecolor: Optional[str] = None,
linewidth: float = 1.0,
style: Optional[str] = 'default',
capsize: float = 3.0,
alpha: float = 0.2,
show: bool = False
):
"""
note:
- example values for color and ecolor:
color='tab:blue', ecolor='tab:blue'
- capsize is the length of the horizontal line for the error bar. Larger number makes it longer horizontally.
- alpha value create than 0.2 make the error bands color for filling it too dark. Really consider not changing.
- sample values for curves and error_band labels:
curve_label: str = 'mean with error bars',
error_band_label: str = 'error band',
refs:
- for making the seaborn and matplot lib look the same see: https://stackoverflow.com/questions/54522709/my-seaborn-and-matplotlib-plots-look-the-same
"""
if style == 'default':
# use the standard matplotlib
plt.style.use("default")
elif style == 'seaborn' or style == 'sns':
# looks idential to seaborn
import seaborn as sns
sns.set()
elif style == 'seaborn-darkgrid':
# uses the default colours of matplot but with blue background of seaborn
plt.style.use("seaborn-darkgrid")
elif style == 'ggplot':
# other alternative to something that looks like seaborn
plt.style.use('ggplot')
# ax = plt.gca()
# fig = plt.gcf(
# fig, axs = plt.subplots(nrows=1, ncols=1, sharex=True, tight_layout=True)
plt.errorbar(x=x, y=y, yerr=yerr, color=color, ecolor=ecolor,
capsize=capsize, linewidth=linewidth, label=curve_label)
plt.fill_between(x=x, y1=y - yerr, y2=y + yerr, alpha=alpha, label=error_band_label)
plt.grid(True)
if curve_label or error_band_label:
plt.legend()
plt.title(title)
plt.xlabel(xlabel)
plt.ylabel(ylabel)
if show:
plt.show()
e.g.
def plot_with_error_bands_test():
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
# the number of x values to consider in a given range e.g. [0,1] will sample 10 raw features x sampled at in [0,1] interval
num_x: int = 30
# the repetitions for each x feature value e.g. multiple measurements for sample x=0.0 up to x=1.0 at the end
rep_per_x: int = 5
total_size_data_set: int = num_x * rep_per_x
print(f'{total_size_data_set=}')
# - create fake data set
# only consider 10 features from 0 to 1
x = np.linspace(start=0.0, stop=2*np.pi, num=num_x)
# to introduce fake variation add uniform noise to each feature and pretend each one is a new observation for that feature
noise_uniform: np.ndarray = np.random.rand(rep_per_x, num_x)
# same as above but have the noise be the same for each x (thats what the 1 means)
noise_normal: np.ndarray = np.random.randn(rep_per_x, 1)
# signal function
sin_signal: np.ndarray = np.sin(x)
cos_signal: np.ndarray = np.cos(x)
# [rep_per_x, num_x]
y1: np.ndarray = sin_signal + noise_uniform + noise_normal
y2: np.ndarray = cos_signal + noise_uniform + noise_normal
y1mean = y1.mean(axis=0)
y1err = y1.std(axis=0)
y2mean = y2.mean(axis=0)
y2err = y2.std(axis=0)
plot_with_error_bands(x=x, y=y1mean, yerr=y1err, xlabel='x', ylabel='y', title='Custom Seaborn')
plot_with_error_bands(x=x, y=y2mean, yerr=y2err, xlabel='x', ylabel='y', title='Custom Seaborn')
plt.show()
if you want to use seaborn check this question out: How to show error bands for pure matrices [Samples, X_Range] with Seaborn error bands?
Upvotes: 1
Reputation: 3660
It is not very clear to me how the error variable in your code sample relates to the variations of the y variables. So here I give an example of how to compute and draw an error band based on the random variations of 25 y variables, and I use these same variations to create y error bars on top of the band. The same logic would apply to variations/errors on the x-axis.
Let's first create some random data and see what a line plot of 25 similar lines looks like:
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
rng = np.random.default_rng(seed=1)
x = np.linspace(0, 5*np.pi, 50)
y = np.sin(x)
# error = np.random.normal(0.1, 0.02, size=x.shape) # I leave this out
nb_yfuncs = 25
ynoise = rng.normal(1, 0.1, size=(nb_yfuncs, y.size))
yfuncs = nb_yfuncs*[y] + ynoise
fig, ax = plt.subplots(figsize=(10,4))
for yfunc in yfuncs:
plt.plot(x, yfunc, 'k-')
plt.show()
I use the mean of yfuncs
as the baseline variable. I extract the minimum and maximum of yfuncs
for each x to compute the error band. I compute error bars that cover the same extent as the error band. Therefore, the errors are asymmetrical relative to the mean which is why they are entered as a 2-D array in the plotting function. The error band is drawn with fill_between
and the error bars with errorbar
. Here is what the code looks like:
ymean = yfuncs.mean(axis=0)
ymin = yfuncs.min(axis=0)
ymax = yfuncs.max(axis=0)
yerror = np.stack((ymean-ymin, ymax-ymean))
fig, ax = plt.subplots(figsize=(10,4))
plt.fill_between(x, ymin, ymax, alpha=0.2, label='error band')
plt.errorbar(x, ymean, yerror, color='tab:blue', ecolor='tab:blue',
capsize=3, linewidth=1, label='mean with error bars')
plt.legend()
plt.show()
Upvotes: 4