Showing uncertainty - and saying which kind you are showing.
Overview
errorbar
Every measurement has uncertainty. A model prediction has a confidence interval. An average has a standard error. A forecast has a range. Showing that uncertainty on a chart is what error bars and bands are for.
A chart without uncertainty is a chart that overstates what is known. A point plotted at 42.3 implies a precision that the data may not support. An error bar from 38 to 47 tells the reader how much to trust that number. A band around a trend line shows where the model is confident and where it is guessing.
matplotlib provides two main tools: ax.errorbar for discrete points with intervals, and ax.fill_between for continuous bands around a line. Both accept symmetric and asymmetric ranges. Both can be combined with other chart elements.
This page covers errorbar, fill_between, asymmetric errors, horizontal errors, cap styles, error bars on bar charts, confidence bands, prediction intervals and the visual choices that make uncertainty readable without overwhelming the data.
The most common mistake is passing absolute bounds instead of offsets. errorbar expects the distance from the point to the bar end, not the bar end itself. Passing the raw upper and lower bounds produces bars that are far too large. The page shows this mistake and the fix.
Bands are often better than bars for time series. A line with a shaded region around it reads more naturally than a line with vertical bars at each point. fill_between takes the same x array as the line and two y arrays for the upper and lower edges. Setting alpha to a low value keeps the band from hiding the line.
ax.errorbar(x, y, yerr=err) plots points with vertical intervals.
A single array or scalar gives even bars. The same distance above and below. A (2, n) array or a list of two arrays gives skewed ones, with the first row the distance below and the second the distance above.
Both are given as plus offsets from the point, not as abs spots. Passing abs upper and lower bounds is a common mistake and makes huge bars.
xerr does the same flatly, and both can be used at once.
capsize adds the end caps. Without it the bars are bare lines. This Is harder to read against a busy background. fmt="o" sets the marker. fmt="none" draws the bars with no marker at all.
Worth knowing
errorbar(x, y, yerr=e) is symmetric; yerr=[lo, hi] is asymmetric, given as positive offsets.
For a curve, use fill_between — hundreds of error bars become a smear.
Standard deviation, standard error and a confidence interval differ by large factors. An unlabelled interval is uninterpretable.
Bars take yerr directly, and error_kw styles the whiskers.
With few observations, showing the points with jitter beats any summary of them.
A prediction interval and a confidence interval for the fitted line are different claims and are constantly confused.
Error Bars and Bands
Showing uncertainty, and saying which kind you're showing.
errorbar draws the interval
Symmetric with one array, asymmetric with two rows.
example_01.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(1, 6)
y = np.array([10, 14, 12, 17, 15], dtype=float)
err = np.array([1.0, 2.0, 1.5, 0.8, 2.5])
fig, (a, b) = plt.subplots(1, 2, figsize=(9, 3))
a.errorbar(x, y, yerr=err, marker="o", capsize=4)
a.set_title("symmetric")
lo = np.array([0.5, 1.0, 0.8, 0.4, 1.2])
hi = np.array([2.0, 3.0, 1.6, 1.5, 3.0])
b.errorbar(x, y, yerr=[lo, hi], marker="o", capsize=4, color="crimson")
b.set_title("asymmetric: yerr=[lower, upper]")
print("yerr as one array -> the same distance above and below")
print("yerr as [lo, hi] -> different distances, given as POSITIVE")
print(" offsets from the point")
print()
print("capsize adds the end caps; without it the bars are bare lines.")
Output
Bands for continuous data
fill_between, because a bar per point is unreadable on a curve.
example_02.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 200)
y = np.sin(x)
sd = 0.1 + 0.15 * np.abs(np.cos(x))
fig, (a, b) = plt.subplots(1, 2, figsize=(9, 3))
a.errorbar(x, y, yerr=sd, alpha=0.4)
a.set_title("200 error bars: a smear")
b.plot(x, y, color="crimson")
b.fill_between(x, y - sd, y + sd, alpha=0.25, color="crimson")
b.set_title("a band")
print("Error bars suit a handful of discrete measurements.")
print("For a curve, fill_between shows the same information and stays")
print("readable at any number of points.")
print()
print("alpha around 0.2-0.3 keeps the central line clearly on top.")
Output
Say which interval it is
Standard deviation, standard error and a confidence interval differ by a lot.
example_03.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
n = 40
sample = rng.normal(100, 15, n)
sd = sample.std(ddof=1)
se = sd / np.sqrt(n)
ci = 1.96 * se
fig, ax = plt.subplots(figsize=(6, 3))
for i, (label, half) in enumerate([("sd", sd), ("se", se), ("95% ci", ci)]):
ax.errorbar([i], [sample.mean()], yerr=[[half], [half]],
marker="o", capsize=6)
ax.text(i, sample.mean() + half + 1, "%s: +/-%.1f" % (label, half),
ha="center", fontsize=9)
ax.set_xticks(range(3))
ax.set_xticklabels(["sd", "se", "95% ci"])
ax.set_title("same data, three intervals")
print("sd = %.2f spread of the DATA" % sd)
print("se = %.2f uncertainty of the MEAN" % se)
print("95%% ci = %.2f about 1.96 standard errors" % ci)
print()
print("They differ by a factor of six here. An unlabelled error bar is")
print("uninterpretable, which is why the caption has to say which it is.")
Output
This is the part that matters and is most often skipped.
Standard deviation shows the spread of the data. It doesn't shrink as you collect more.
Standard error shows the uncertainty of the mean. It's the standard deviation cut by the square root of n, so it does shrink.
A trust interval is roughly 1.96 standard errors for a 95% interval on a mean, under assumptions.
A guess interval is where a new finding should fall. Is wider than a trust interval for the fitted line.
On a sample of forty with a standard deviation of fifteen, these differ by a factor of six. An error bar without a caption saying which one it's can't be read. Readers well assume whichever supports their prior.
Put it in the axis label or the legend: "mean (95% CI)" costs nothing.
Bars with error bars
The same yerr argument, and the same zero-baseline rule.
example_04.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
groups = ["a", "b", "c", "d"]
means = np.array([23, 31, 27, 35], dtype=float)
errs = np.array([3, 5, 2, 4], dtype=float)
fig, ax = plt.subplots(figsize=(6, 3))
ax.bar(groups, means, yerr=errs, capsize=5,
color="steelblue", edgecolor="black", linewidth=0.5)
ax.set_ylabel("mean (95% ci)")
print("bar takes yerr directly.")
print()
print("error_kw passes styling through to the error bars:")
print(" error_kw=dict(ecolor='0.3', lw=1, capthick=1)")
print()
print("The overlap rule people remember - 'if the bars overlap it is not")
print("significant' - is wrong for confidence intervals of two means.")
print("Non-overlapping implies significance; overlapping does not imply")
print("its absence.")
Output
ax.bar(x, heights, yerr=errs, capsize=5) works right, and error_kw passes styling through to the whiskers.
Two cautions exact to bars.
The zero baseline rule still applies. Error bars make it more tempting to break. A truncated axis makes the intervals look more split.
And the share heuristic — "the error bars share. The gap isn't large" — is wrong for trust intervals of two means. Non-shared intervals do imply a large gap. shared ones do not imply the lack of one. The relevant quantity is the interval around the *gap*. This Is narrower than either single interval suggests. Two 95% intervals can share by a fair margin while the gap is still large.
Showing the data instead
With few points, the observations beat any summary of them.
example_05.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(1)
groups = {"a": rng.normal(20, 3, 12),
"b": rng.normal(24, 3, 12),
"c": rng.normal(22, 9, 12)}
fig, (p, q) = plt.subplots(1, 2, figsize=(9, 3.2), sharey=True)
names = list(groups)
means = [groups[k].mean() for k in names]
sds = [groups[k].std(ddof=1) for k in names]
p.bar(names, means, yerr=sds, capsize=5, color="0.7")
p.set_title("bar + error bar")
for i, k in enumerate(names):
vals = groups[k]
p_x = np.full(len(vals), i) + rng.uniform(-0.12, 0.12, len(vals))
q.scatter(p_x, vals, alpha=0.7, s=25)
q.hlines(vals.mean(), i - 0.25, i + 0.25, color="crimson", linewidth=2)
q.set_xticks(range(3)); q.set_xticklabels(names)
q.set_title("the points themselves")
print("Group c has three times the spread of the others, and one clear")
print("outlier. The bar chart shows a taller error bar; the strip plot")
print("shows what is actually going on.")
print()
print("Jitter - a small random x offset - stops points overlapping.")
Output
Bands from a model
The prediction and its interval, drawn together.
example_06.pymatplotlib
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(2)
x = np.sort(rng.uniform(0, 10, 40))
y = 2.0 * x + 5 + rng.normal(0, 3, 40)
coef = np.polyfit(x, y, 1)
xs = np.linspace(0, 10, 100)
fit = np.polyval(coef, xs)
resid = y - np.polyval(coef, x)
s = resid.std(ddof=2)
fig, ax = plt.subplots(figsize=(6.5, 3.5))
ax.scatter(x, y, alpha=0.7, s=30, label="observations")
ax.plot(xs, fit, color="crimson", linewidth=2, label="fit")
ax.fill_between(xs, fit - 1.96 * s, fit + 1.96 * s,
color="crimson", alpha=0.15, label="+/-1.96 s")
ax.legend()
print("fit: y = %.2fx + %.2f" % (coef[0], coef[1]))
print("residual sd: %.2f" % s)
print()
print("This band is a rough PREDICTION interval - where new points")
print("should fall. A confidence interval for the LINE is narrower and")
print("bow-shaped, widest at the ends. They are different claims and")
print("get confused constantly.")
Output
Bands
Error bars work for a handful of discrete readings. On a curve with two hundred points they share into a grey smear that conveys nothing.
fill_between(x, low, high) is the smooth version:
ax.plot(x, y)
ax.fill_between(x, y - sd, y + sd, alpha=0.25)
alpha around 0.2 to 0.3 keeps the central line clearly on top. Using the same colour for line and band ties them together. Using a other one implies they're other series.
fill_between also takes where= to shade only part of the range. step="pre" for step-like data where a smooth fill would be wrong.
Show the data when you can
With a dozen findings per group, a bar and an error bar throw away almost all: the shape, the outliers, the sample size.
A strip plot — the single points with a small random x offset, or "jitter", to stop them shared — shows all of it in the same space. Adding a flat line for the mean keeps the summary.
The fifth editor shows a case where two groups have the same mean and very other spreads, one with a clear outlier. The bar chart reports a taller error bar. The points report what happened.
The rule of thumb: under about fifty points per group, show the points. Above that, a box plot or a violin summarises without hiding as much as a bar does.
Model intervals
Drawing a fit with a band is the standard way to show a model and its uncertainty. It needs being clear about which band.
A trust band for the fitted line is narrow in the middle and bow-shaped, widening at the ends where the fit is less constrained.
A guess band for new findings is much wider and roughly parallel to the line. It includes the residual scatter and the uncertainty in the line.
The sixth editor draws the second, about. For anything that will be used to make a choice, statsmodels or SciPy find these properly. The math is worth doing rather than approximating.
Styling error bars
errorbar takes split styling for the line, the markers and the bars:
ax.errorbar(x, y, yerr=e,
fmt="o", markersize=5,
ecolor="0.4", elinewidth=1, capsize=4, capthick=1)
fmt="o" draws markers with no connecting line. This Is right when the x values are discrete groups rather than a sequence. fmt="none" draws bars only.
ecolor lighter than the marker keeps the point as the subject and the interval as context. This Is often the correct emphasis.
errorevery=5 draws bars on each fifth point, for a dense series where each bar would be a smear but some indication of uncertainty is wanted.
Asymmetric intervals from quantiles
Real uncertainty is often skewed, and quantiles give it right:
lo = np.percentile(samples, 5, axis=0)
hi = np.percentile(samples, 95, axis=0)
ax.fill_between(x, lo, hi, alpha=0.2)
For errorbar the same quantiles must be converted to offsets from the central value:
yerr = np.vstack([median - lo, hi - median])
Losing that conversion — passing the quantiles themselves — is the most common error here. Makes intervals that are wrong by the magnitude of the data rather than subtly off.
Several series with intervals
Two series each with a band share into a thing unreadable if both bands are the same weight.
Three things help: low alpha on the bands (0.15 rather than 0.3), matching each band to its line's colour. Drawing all the bands before all the lines so no line is buried.
for name, (y, lo, hi) in series.items():
ax.fill_between(x, lo, hi, alpha=0.15)
for name, (y, lo, hi) in series.items():
ax.plot(x, y, label=name)
Beyond three series with bands, the bands stop being separable and small multiples are the answer.
Error bars on log axes
An interval that is even on a linear scale is skewed on a log one, and vice versa.
Passing even yerr to a log-scaled axis makes a lower bar that is visually much longer than the upper one. This Is correct math and often not what was intended. Scaling uncertainty is most times what you've on a log scale.
The fix is to find the interval in the space you're plotting. Bounds as scaling factors, converted to offsets at each point.
A bar reaching below zero on a log axis simply doesn't draw. This Is the visible symptom.
What the interval is for
An error bar is a claim about repeatability, and it's worth being clear which claim.
"If I did this again, the mean would land in here" is a trust interval.
"A new finding would land in here" is a guess interval, and is much wider.
"The data spread this much" is a standard deviation, and doesn't shrink with more data.
Charts routinely show one and are read as another. The caption is the only thing that resolves it. This Is why each editor here labels the interval rather than leaving it to the reader.
Uncertainty that is not statistical
Not each band is a trust interval. Saying which kind it's matters as much as the math.
A reading handling — the instrument's stated score, fixed and known.
A range across cases — best and worst case. This Isn't a chance statement at all.
A forecast interval — widening with horizon, and gated on the model.
Seen min and max — the actual extremes. This Say nothing about what a new finding would do.
Each is a other claim, and drawn the same way. The caption is the only thing distinguishing them. This Is why an untagged band is the most common way a chart overstates what is known.
Choosing what to show
The choice is what the reader should conclude.
If the question is "is this gap real?", show a trust interval on the gap, not two intervals on the means. The eye can't combine them right.
If the question is "how var is this?", show the spread: a standard deviation, a percentile band, or the points.
If the question is "what will happen next?", show a guess interval. This Is wider than either.
If the sample is small, showing the findings answers all three better than any interval.
The failure to avoid is showing a standard error because it's narrowest and letting it be read as spread. This Understates spread by a factor of the square root of n.
Intervals in a report
An interval on a chart is a claim. The nearby text is part of it.
Three things belong in the caption or the axis label, and are almost always omitted:
Which interval it's — standard deviation, standard error, a trust interval and at what level, or a guess interval.
What it assumes — normality, freedom, a model.
The sample size, because an interval from eight findings and one from eight hundred are other kinds of claim even when they're the same width.
"Mean ± 95% CI, n = 42" in the y label costs nine words and makes the chart readable. Without it the reader either assumes the most favourable reading or discounts the interval fully. Both are worse than being told.
Error Bars and Bands, in summary
errorbar for discrete readings, fill_between for a curve. Hundreds of bars become a smear.
yerr=[lo, hi] takes plus offsets, not abs bounds. Getting that wrong makes intervals wrong by the scale of the data.
Standard deviation, standard error and a trust interval differ by large factors on the same data. An untagged bar can't be read.
shared trust intervals don't imply the lack of a large gap, though non-shared ones do imply its presence.
On a log axis, even offsets are skewed on screen. The interval should be found in the space being plotted.
And with few findings, showing the points beats any summary of them.
Bands on a forecast
A forecast chart has a exact rule worth following.
The historical series is drawn solid. The forecast is drawn dashed or in a other shade. The boundary between seen and predicted is visible without reading the caption.
A vertical line at the forecast origin makes it unmissable.
The band widens with horizon. Uncertainty grows. A constant-width band on a forecast is almost always wrong. It understates the far end.
Two bands at other levels. 50% and 90%, with the inner one darker — send the shape of the uncertainty better than one. Are standard in published forecasts.
And the caption states the model and the interval. A forecast band is fully gated on a model the chart doesn't show.
Without the vertical line and the style change, a reader takes the whole line as data. This Is the most consequential misreading on hand on this kind of chart.
Error Bars and Bands: the short version
An interval is a claim, and the caption is part of the claim.
Standard deviation, standard error, trust and guess intervals differ by large factors on the same data. Drawing them the same way without saying which is the most common way a chart overstates what is known.
Questions people ask about Error Bars and Bands
What does an error bar represent? Whatever you say it does: standard deviation, standard error, or a confidence interval. State which one in the caption or label.
How do I draw error bars? Use ax.errorbar(x, y, yerr=err). Pass a 2 by N array for different lower and upper sizes.
When should I use a band instead? For a continuous series such as a forecast, use ax.fill_between with some transparency. Bands read more clearly than many bars.
Should I show standard deviation or standard error? Standard deviation describes spread in the data. Standard error describes uncertainty in the mean. Pick the one that matches the claim.
Do error bars hide the data? They can. When samples are small, plot the individual points as well.
Why do my error bars look asymmetric on a log axis? Equal absolute sizes span different visual distances on a log scale. Compute bounds in log space or use asymmetric intervals.
Reading the code back
Drawing an interval is one arg. Deciding which interval, finding it right as offsets. Saying in the chart which one it's are three split pieces of work. Only the first is about matplotlib. A band drawn without the other two is a decoration that readers will read as a claim.
Check yourself
0 of 4
Answer without scrolling back up.
How is `yerr=[lo, hi]` interpreted?
Passing absolute bounds is a common mistake and produces enormous bars.
Why use `fill_between` rather than error bars on a curve?
alpha around 0.2-0.3 keeps the central line clearly on top, and the same colour ties the band to the line.
Two 95% confidence intervals for means overlap slightly. What follows?
Non-overlapping does imply significance, but the relevant interval is the one around the difference, which is narrower than either alone.
You have 12 observations per group. What shows the most?
A bar and error bar throw away the shape, the outliers and the sample size. Under about fifty points per group, show the points.
Cheat sheet
Error Bars and Bands
Add error bars and shaded confidence bands to a matplotlib plot. Label whether you are showing standard deviation, standard error or a CI.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.