Error Bars in R with ggplot2: SD, SE, and Confidence Intervals
Error bars in ggplot2 are added with geom_errorbar(), which takes ymin and ymax — typically computed as mean ± SD, mean ± SE, or a 95% confidence interval from your summary data.
Introduction
A mean without context is often misleading. If you say "Group A averages 42" and "Group B averages 45," the critical question is: how much variability is there within each group? Are those means reliably different, or could they easily swap on the next sample?
Error bars answer that question visually. But there are three common choices — standard deviation (SD), standard error (SE), and 95% confidence intervals (CI) — and picking the wrong one changes what your chart communicates:
- SD shows the spread of individual observations. Wide SD = high natural variability in the data.
- SE shows the precision of the mean estimate. Smaller SE = more confident the mean is a good estimate.
- 95% CI shows the range you're 95% confident contains the true population mean. Narrowest of the three.
This post shows you how to compute each, add them to ggplot2 charts, and which situations call for which.
How do you compute summary statistics for error bars?
Before adding error bars, you need a summary data frame with columns for the mean, and the upper/lower bounds of your error interval. dplyr makes this straightforward.
qt(0.975, df = n - 1) uses the t-distribution critical value (not 1.96) because n = 50 per group — for small samples, the t-distribution gives wider, more honest intervals than the normal approximation.
Try it: Change qt(0.975, df = n - 1) to 1.96 (the z-score for 95% CI). For n=50, the difference is small. Now try n = 5 observations — you'll see the t-distribution gives notably wider intervals.
How do you add error bars to a point plot?
geom_errorbar() draws vertical lines at ymin and ymax. Add it on top of geom_point() to show mean + uncertainty.
The width argument controls the horizontal caps at the top and bottom of each error bar. Set width = 0 to remove caps entirely (whiskers only).
Try it: Replace ymin = se_lo, ymax = se_hi with ymin = ci_lo, ymax = ci_hi to show 95% CI instead of SE. Notice how the intervals widen. Then try ymin = sd_lo, ymax = sd_hi for SD — they're much wider because SD describes individual spread, not mean precision.
What is geom_pointrange() and when should you use it?
geom_pointrange() combines the dot and the interval into a single geom — cleaner code and a cleaner look, since it ensures the dot and bar are perfectly aligned.
fatten controls the ratio of dot size to line width. fatten = 4 (default) makes the dot 4× the line width. Reduce it for a smaller dot, increase for a more prominent point.
Related geoms for comparison:
geom_linerange()— line only, no dotgeom_crossbar()— box with a middle line (like a boxplot whisker)geom_errorbarh()— horizontal error bars (for when the x-axis is continuous)
Try it: Replace geom_pointrange() with geom_linerange(aes(ymin = ci_lo, ymax = ci_hi)) — the dot disappears. Then try geom_crossbar(aes(ymin = ci_lo, ymax = ci_hi), width = 0.3) — you get a box instead.
How do you add error bars to a bar chart?
Bar charts with error bars are common in scientific papers. The key: add geom_errorbar() after geom_col() so it renders on top.
Note: many visualization experts argue against bar charts with error bars, because the bar fills from zero — implying the total area matters — while error bars relate only to the mean at the top. A point range or dot plot is more honest. But bar + error bar is so common in scientific literature that knowing how to make it is essential.
Try it: Change the error bars to show SD instead of SE: ymin = sd_lo, ymax = sd_hi. How does the perception of group differences change?
How do you make horizontal error bars?
When your continuous variable is on the x-axis (e.g., effect sizes, regression coefficients), use geom_errorbarh().
This "forest plot" style is standard for coefficient plots, meta-analyses, and effect size summaries. The vertical dashed line at x=0 is the null hypothesis reference — coefficients whose CI crosses zero are not statistically significant at α=0.05.
Try it: Change color = sig to color = estimate > 0 to color by direction (positive/negative) instead of significance. How does this change the message?
Complete Example: Multi-group Error Bar Plot
Common Mistakes and How to Fix Them
Mistake 1: Not labeling which error measure you used
❌ Adding error bars without saying whether they're SD, SE, or CI.
✅ Always add a caption or subtitle specifying the error measure:
Mistake 2: Using SD when you mean to show precision
SD and SE answer different questions. If you're making a claim about how precisely you've estimated the mean, use SE or CI. If you're describing natural variability in the population, use SD.
Mistake 3: Confusing width and linewidth
width— horizontal extent of the caps (set to 0 for no caps)linewidth— thickness of the vertical line
Mistake 4: Forgetting position_dodge() for grouped plots
When you have multiple groups per x-axis position, error bars stack on top of each other without position_dodge().
Mistake 5: Computing SE or CI on already-summarized data
If you pass a summary table to ggplot, stat_summary() won't recompute SE/CI for you. Compute them explicitly in your summarise() step first.
Practice Exercises
Exercise 1: Point plot with CI
Using ToothGrowth, compute mean tooth length (len) by supplement (supp) and dose (dose). Create a point + error bar plot showing 95% CI. Use position_dodge() to separate the two supplement groups.
Show solution
Exercise 2: Bar chart with error bars
Using the same tg_summary from Exercise 1, create a bar chart with error bars for each supplement-dose combination. Use facet_wrap(~ supp) to separate supplements into panels.
Show solution
Summary
| Geom | Use case |
|---|---|
geom_errorbar() |
Vertical error bars (with caps) on any plot |
geom_pointrange() |
Dot + interval in one geom |
geom_linerange() |
Interval line only, no dot |
geom_crossbar() |
Box-style interval with median line |
geom_errorbarh() |
Horizontal error bars (for x-axis intervals) |
| Error measure | Formula | Shows |
|---|---|---|
| SD | sd(x) |
Spread of individual observations |
| SE | sd(x) / sqrt(n) |
Precision of the mean estimate |
| 95% CI | qt(0.975, df=n-1) * se |
Range likely containing the true mean |
Always label your error bars — unlabeled error bars are ambiguous and a common criticism in peer review.
FAQ
Should I use SD or SE for error bars? Use SD to describe natural variability in your data (e.g., showing how spread out individual measurements are). Use SE or 95% CI to show how precisely you've estimated the mean — these are for inferential claims.
Why does 95% CI use qt() instead of 1.96? 1.96 is the z-score for a 95% CI under the normal distribution (infinite sample size). For finite samples, the t-distribution with df = n - 1 is more accurate. With n ≥ 30, the difference is negligible; for smaller samples, the t-distribution gives appropriately wider intervals.
How do I add error bars to a ggplot2 line chart? Add geom_ribbon(aes(ymin = lo, ymax = hi), alpha = 0.2) for shaded confidence bands, or geom_errorbar() at each time point. Ribbons look cleaner for dense time series.
Can I draw error bars horizontally? Yes — geom_errorbarh(aes(xmin = lo, xmax = hi)) draws horizontal bars. Use when the continuous variable is on the x-axis (regression coefficients, effect sizes).
What is width vs. linewidth in geom_errorbar()? width sets the horizontal extent of the end caps. linewidth sets the thickness of the vertical line. Set width = 0 to remove caps entirely.
References
- Wickham H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer.
- STHDA — ggplot2 error bars: sthda.com/english/wiki/ggplot2-error-bars
- Cumming G. et al. (2007). Error bars in experimental biology. Journal of Cell Biology.
- Wilke C. (2019). Fundamentals of Data Visualization — Chapter 16: Visualizing uncertainty
What's Next?
- ggplot2 Scatter Plots — the foundation for point-based visualizations
- geom_smooth() in ggplot2 — add regression lines and confidence ribbons
- R Correlation Matrix Plot — visualize pairwise correlations with uncertainty