# SD vs SEM error bars: which to show, and what to write in the legend

> Show the data points, then pick the bar by purpose: SD for spread, a 95% CI for the precision of a mean. Why SEM bars mislead at small n, overlap rules, legend templates and Python/R code.

Canonical: https://www.licklider.ai/guides/sd-vs-sem-error-bars/
Section: Guides › Writing and reporting
Author: Licklider · Published 2026-11-10 · Last reviewed 2026-11-10 (UTC)

> **Quick answer.** Show every data point (one per animal or independent experiment), then choose the bar for its purpose. The default is **points plus mean ± 95% confidence interval (CI)**, which shows how precisely each mean is known; use **SD** when the figure should show how much individual animals vary. With only 3 animals per group, show the points and the mean without a bar. For roughly normal data with 3–10 animals per group, a mean ± SEM bar contains the true mean only 58–66% of the time (about 67% at 20–30 animals), so keep SEM bars only together with the points and the stated n. In every legend, state n (animals, not wells or cells) and which bar is drawn, and report the difference between groups with its 95% CI in the text. If an AI assistant wrote the plotting code, run the [three checks below](#check-what-an-ai-assistant-drew).

> **Terms used below.** *SD* (standard deviation): the typical distance of individual values from their mean. *SEM* (standard error of the mean): SD ÷ √n, how much the mean would vary between repeated experiments. *95% CI*: mean ± t × SEM, where t comes from the t distribution with n − 1 degrees of freedom (4.30 for n = 3, 2.78 for n = 5); in repeated experiments it contains the true mean 95% of the time. *n*: the number of independent experimental units (animals, independent cultures), not technical replicates. *Degrees of freedom*: the number that picks which t distribution to use; for one group's CI it is n − 1. Welch's test computes its degrees of freedom from both groups' SDs and n, so they are usually not a whole number (7.56 for 5 + 5 mice below). *Coverage*: the share of repeated experiments in which an interval contains the true mean. *Welch's t-test*: the two-group t-test that does not assume equal SDs (R's default). *Student's t-test*: the two-group t-test that assumes equal SDs. *Two-sided*: the test counts a difference in either direction as evidence. *Bootstrap*: an interval built by resampling the observed values many times. *Arm*: one side of an error bar, from the mean to the bar's end. *Geometric mean*: the mean of the log values, transformed back; it suits skewed positive data. Note: some papers write *SE* for the SEM.

## The same ten mice, shown four ways

<img src="/guides/sd-vs-sem-error-bars/sd-vs-sem.svg" alt="Four panels show the same body-weight gains for 5 control and 5 treated mice as grey points. Panel 1: points with a line at each mean. Panel 2: mean ± SD bars, roughly spanning the points. Panel 3: mean ± SEM bars, less than half as long. Panel 4: mean ± 95% CI bars, the longest, about 2.8 times the SEM bars." width="1000" height="340" loading="lazy">

The data are constructed: body-weight gain in 5 control and 5 treated mice, one value per mouse.

| Group | n | Mean (g) | SD | SEM | 95% CI of the mean |
| --- | --- | --- | --- | --- | --- |
| Control | 5 | 3.84 | 1.12 | 0.50 | 2.44 to 5.24 |
| Treated | 5 | 6.60 | 1.44 | 0.64 | 4.82 to 8.38 |

The data are identical in all four panels; only the bars differ. The SEM bar is the shortest because it is SD ÷ √5, and the 95% CI is 2.78 SEMs on each side because n = 5. Welch's t-test gives a difference of 2.76 g (95% CI 0.86 to 4.66), t(7.56) = 3.38, p = 0.010: that interval for the **difference** is the number a reader needs to judge the comparison, and no bar on the group means shows it directly.

**What to do:** show the points, add the bar that matches your purpose from the table below, and report the difference with its CI in the text.

## Which bar for which purpose

| You want the figure to show | Draw | Why |
| --- | --- | --- |
| How much animals differ from one another | Mean ± SD (with points) | For roughly normal data about two thirds of individual values lie within one SD of the mean; the SD does not shrink as n grows |
| How precisely each group mean is known | Mean ± 95% CI (with points) | For roughly normal data its coverage is 95% at every n, so readers can interpret it without knowing n |
| Only 3 animals per group | The points and the mean, no bar (state n = 3 in the legend) | Cumming et al. (2007, Rule 4): "if n is very small (for example n = 3), rather than showing error bars and statistics, it is better to simply plot the individual data points" |
| Skewed positive data (concentrations, fold changes) | Points on a log axis; geometric mean with its 95% CI | A CI computed on the log scale and back-transformed is asymmetric, as the data are |
| A change within the same animals (before/after, paired) | The paired differences, or lines joining each animal's values | Bars on the group means say nothing about within-animal change (Cumming et al. 2007, Rule 8) |

**What to do:** use points plus mean ± 95% CI by default; choose SD when the figure's purpose is to show variability between animals; with 3 animals per group, show only the points and the mean.

## What an SEM bar actually covers

SEM bars are the most common choice, including in figures that are meant to show spread, which is the job of the SD. In a review of 703 physiology articles, most papers with bar graphs showed mean ± SE (77.6%) rather than mean ± SD (15.3%) (Weissgerber et al. 2015); in four anaesthesia journals in 2001, 23% of articles (198 of 860) used the SEM to describe the variability of the sample (Nagele 2003).

A mean ± 1 SEM bar is a confidence interval with a level that depends on n. For normal data its coverage is P(|t| ≤ 1) with n − 1 degrees of freedom:

| Animals per group (n) | Coverage of mean ± 1 SEM | Multiplier for a 95% CI (t) |
| --- | --- | --- |
| 3 | 57.7% | 4.30 |
| 4 | 60.9% | 3.18 |
| 5 | 62.6% | 2.78 |
| 6 | 63.7% | 2.57 |
| 8 | 64.9% | 2.36 |
| 10 | 65.7% | 2.26 |
| 20 | 67.0% | 2.09 |
| 30 | 67.4% | 2.05 |

This matches Cumming et al. (2007, Rule 5): "SE bars can be doubled in width to get the approximate 95% CI, provided n is 10 or more. If n = 3, SE bars must be multiplied by 4 to get the approximate 95% CI."

A simulation confirms the table and adds the interval that common plotting software draws by default (normal data, 20,000 simulated experiments per row, with the seed fixed before the run):

| n | Mean ± 1 SEM | Mean ± 2 SEM | t-based 95% CI | Bootstrap "95%" CI (1,000 resamples) |
| --- | --- | --- | --- | --- |
| 3 | 57.9% | 81.8% | 95.0% | 75.0% |
| 5 | 62.1% | 88.3% | 94.9% | 83.6% |
| 10 | 66.3% | 92.5% | 95.3% | 90.3% |

With 3–10 animals, a percentile-bootstrap interval labelled 95% contained the true mean only 75–90% of the time. Use the t-based CI for small groups. The simulation code and its output are published: [`example.py`](/guides/sd-vs-sem-error-bars/example.py), [`example_output.json`](/guides/sd-vs-sem-error-bars/example_output.json).

**What to do:** show the points and state n whenever you keep SEM bars, so readers can rescale them; choose the 95% CI when you want an interval with the same meaning at every n.

## Reading overlap: approximate rules

Bars on two group means are not a test. For two **independent** groups with **equal n and similar SDs**, the p-value of Student's t-test for given bar positions is shown below. The table uses Student's t-test because with equal n and similar SDs the bar positions convert directly into a t value; Welch's test gives nearly the same p-values in that case, and it remains the test to run on real data.

| n per group | SEM bars just touch | Gap of one SEM | 95% CI bars just touch | 95% CIs overlap by half an arm | 95% CIs overlap by one arm |
| --- | --- | --- | --- | --- | --- |
| 3 | 0.23 | 0.10 | 0.004 | 0.010 | 0.038 |
| 5 | 0.20 | 0.067 | 0.004 | 0.019 | 0.085 |
| 10 | 0.17 | 0.048 | 0.005 | 0.027 | 0.13 |

Two readings follow. For two independent groups with equal n of 3–10 and similar SDs, SEM bars that just touch correspond to p of 0.17–0.23, not 0.05; and under the same conditions, 95% CIs that overlap can still give p < 0.05. Cumming et al. (2007) give the same rules of thumb: for n of 10 or more, "a gap of SE indicates P ≈ 0.05" (Rule 6), and with 95% CIs and n = 3 "overlap of one full arm indicates P ≈ 0.05, and overlap of half an arm indicates P ≈ 0.01" (Rule 7). The rules apply only under the conditions above; with unequal n or unequal spreads, or for paired data, run the test and report the difference with its CI.

Readers misjudge these figures. In a web experiment with 473 authors of journal articles, Belia et al. (2005) concluded that many "have severe misconceptions about how error bars relate to statistical significance, do not adequately distinguish CIs and SE bars".

**What to do:** report the test and the CI of the difference in the text or on the figure, so readers judge the comparison from numbers rather than from bars.

## Before you choose a bar: is n right?

Error bars computed over technical replicates describe pipetting, not biology. Cumming et al. (2007, Rule 3): "error bars and statistics should only be shown for independently repeated experiments, and never for replicates. If a 'representative' experiment is shown, it should not have error bars or P values, because in such an experiment, n = 1." Average the technical replicates within each animal or culture first, and compute the bar from those means ([what counts as n](/guides/what-counts-as-n/); for cells within experiments, a [SuperPlot](/guides/superplots/)).

For data normalised to a control mean set to 1 or 100%, keep the control group's variability: normalise each value to the control mean of the same experiment, and show the control points too ([fold change and normalised data](/guides/statistics-on-fold-change/)).

**What to do:** use the number of independent experimental units as n, both for the bar and in the legend.

## Compute it

```python
import numpy as np
from scipy import stats

control = np.array([3.1, 4.6, 2.4, 5.2, 3.9])
treated = np.array([5.8, 7.4, 4.9, 8.6, 6.3])
for name, x in [("control", control), ("treated", treated)]:
    n = len(x)
    sd = x.std(ddof=1)                      # ddof=1 gives the sample SD; NumPy's default is ddof=0
    sem = sd / np.sqrt(n)
    half = stats.t.ppf(0.975, n - 1) * sem  # half-width of the 95% CI
    print(f"{name}: n = {n}, mean = {x.mean():.2f}, SD = {sd:.2f}, SEM = {sem:.2f}, "
          f"95% CI {x.mean() - half:.2f} to {x.mean() + half:.2f}")

res = stats.ttest_ind(treated, control, equal_var=False)  # Welch's t-test, two-sided
ci = res.confidence_interval(0.95)                        # needs SciPy 1.11 or later
print(f"difference {treated.mean() - control.mean():.2f} g, 95% CI {ci.low:.2f} to {ci.high:.2f}; "
      f"Welch t({res.df:.2f}) = {res.statistic:.2f}, p = {res.pvalue:.3f}")
```

```text
control: n = 5, mean = 3.84, SD = 1.12, SEM = 0.50, 95% CI 2.44 to 5.24
treated: n = 5, mean = 6.60, SD = 1.44, SEM = 0.64, 95% CI 4.82 to 8.38
difference 2.76 g, 95% CI 0.86 to 4.66; Welch t(7.56) = 3.38, p = 0.010
```

**The same in R:**

```r
control <- c(3.1, 4.6, 2.4, 5.2, 3.9)
treated <- c(5.8, 7.4, 4.9, 8.6, 6.3)
for (name in c("control", "treated")) {
  x <- get(name)
  n <- length(x)
  sem <- sd(x) / sqrt(n)                  # sd() is the sample SD (n - 1)
  half <- qt(0.975, n - 1) * sem          # half-width of the 95% CI
  cat(sprintf("%s: n = %d, mean = %.2f, SD = %.2f, SEM = %.2f, 95%% CI %.2f to %.2f\n",
              name, n, mean(x), sd(x), sem, mean(x) - half, mean(x) + half))
}
w <- t.test(treated, control)             # Welch's t-test is R's default (var.equal = FALSE)
cat(sprintf("difference %.2f g, 95%% CI %.2f to %.2f; Welch t(%.2f) = %.2f, p = %.3f\n",
            mean(treated) - mean(control), w$conf.int[1], w$conf.int[2],
            w$parameter, w$statistic, w$p.value))
```

```text
control: n = 5, mean = 3.84, SD = 1.12, SEM = 0.50, 95% CI 2.44 to 5.24
treated: n = 5, mean = 6.60, SD = 1.44, SEM = 0.64, 95% CI 4.82 to 8.38
difference 2.76 g, 95% CI 0.86 to 4.66; Welch t(7.56) = 3.38, p = 0.010
```

**Excel**, with one group's values in B2:B6: SD `=STDEV.S(B2:B6)` (STDEV.S uses the n − 1 method; STDEV.P is for a whole population), SEM `=STDEV.S(B2:B6)/SQRT(COUNT(B2:B6))`, and the 95% CI half-width `=T.INV.2T(0.05,COUNT(B2:B6)-1)*STDEV.S(B2:B6)/SQRT(COUNT(B2:B6))`; with that half-width in C2, the limits are `=AVERAGE(B2:B6)-C2` and `=AVERAGE(B2:B6)+C2`. For the difference between two groups with its CI, use the Python or R code above.

**Plot points with a t-based 95% CI in Python (seaborn):**

```python
import numpy as np
import pandas as pd
import seaborn as sns
from scipy import stats

d = pd.DataFrame({"group": ["Control"] * 5 + ["Treated"] * 5,
                  "gain": [3.1, 4.6, 2.4, 5.2, 3.9, 5.8, 7.4, 4.9, 8.6, 6.3]})


def t_ci(x):                                # 95% CI from the t distribution, not seaborn's bootstrap
    x = np.asarray(x)
    half = stats.t.ppf(0.975, len(x) - 1) * x.std(ddof=1) / np.sqrt(len(x))
    return x.mean() - half, x.mean() + half


ax = sns.stripplot(data=d, x="group", y="gain", color="0.4", jitter=0.08)
sns.pointplot(data=d, x="group", y="gain", errorbar=t_ci, linestyle="none",
              capsize=0.2, color="black", ax=ax)
ax.set_ylabel("Body-weight gain (g)")
ax.figure.savefig("gain.png", dpi=150)
print("Saved gain.png: points plus mean and 95% CI (t-based)")
```

**In R (ggplot2):**

```r
library(ggplot2)
d <- data.frame(group = rep(c("Control", "Treated"), each = 5),
                gain = c(3.1, 4.6, 2.4, 5.2, 3.9, 5.8, 7.4, 4.9, 8.6, 6.3))
t_ci <- function(x) {                     # 95% CI from the t distribution
  half <- qt(0.975, length(x) - 1) * sd(x) / sqrt(length(x))
  data.frame(y = mean(x), ymin = mean(x) - half, ymax = mean(x) + half)
}
p <- ggplot(d, aes(group, gain)) +
  geom_jitter(width = 0.08, height = 0, colour = "grey40") +
  stat_summary(fun.data = t_ci, geom = "errorbar", width = 0.2) +
  stat_summary(fun = mean, geom = "point", size = 3) +
  labs(x = NULL, y = "Body-weight gain (g)")
ggsave("gain.png", p, width = 4, height = 3.5, dpi = 150)
print(layer_data(p, 2)[, c("x", "y", "ymin", "ymax")])
```

The R plot prints the same limits as the Python output (2.44 to 5.24 and 4.82 to 8.38).

**What to do:** check that the printed limits match the figure, and compute every bar from the one-value-per-animal data.

## Write the legend

Pick the sentence that matches the bar you drew, and fill in every bracket.

> Points show individual mice; horizontal lines show group means; error bars show [95% confidence intervals of the mean / ± 1 SD]. n = [5] mice per group[; each value is the mean of 3 technical replicates]. [Welch's t-test, two-sided]: difference [2.76 g (95% CI 0.86 to 4.66)], p = [0.010].

> Points show individual mice; bars show mean ± SEM (n = [5] mice per group; SEM = SD ÷ √n, computed from one value per mouse[, the mean of its technical replicates]).

> Points show individual experiments (n = [4] independent experiments, each the mean of [50] cells); bars show 95% confidence intervals of the mean of experiment means.

**Recovering SD from a published SEM:** SD = SEM × √n, where n is the number of independent units the SEM was computed from (animals, not wells or cells). A paper reporting SEM 0.52 with n = 5 mice implies SD = 1.16. If the paper computed its SEM over technical replicates or cells, this SD describes those, not animals. Use this when you need the SD for a sample-size calculation ([sample size calculator](/tools/sample-size-calculator/)).

**If a reviewer asks you to switch bars**, keep the points and answer with the purpose: "We show [SD / 95% CIs] because the figure is intended to show [variability between animals / precision of the group means]; individual animals are plotted and n is stated in the legend. The group difference and its 95% CI are reported in the text." Keep the points and n even when the journal requires SEM bars.

## Check what an AI assistant drew

Plotting code inherits library defaults, so a call that names no interval is not evidence of which interval was drawn. An unmodified call produces these:

- **seaborn 0.13.2** `barplot` and `pointplot`: `errorbar=("ci", 95)` by default, a percentile bootstrap with 1,000 resamples and no fixed seed. With 5 animals that interval covered the true mean 84% of the time in the simulation above, and it changes slightly on every run. Ask for `errorbar=t_ci` (above) or `errorbar="sd"`, and name the choice in the legend.
- **ggplot2 4.0.3** `stat_summary()` with no summary function draws **mean ± 1 SE** and prints only "No summary function supplied, defaulting to `mean_se()`".
- **NumPy** `np.std(x)` uses ddof = 0, a population SD that is smaller than the sample SD; pass `ddof=1`.

Before you accept the figure, check three things in the code:

1. Which interval the bar is (SD, SEM, t-based CI or bootstrap).
2. Which n it used: animals, not rows of a long table.
3. That the legend names the same bar and n.

## References

- Belia S, Fidler F, Williams J, Cumming G (2005). Researchers misunderstand confidence intervals and standard error bars. *Psychological Methods* 10(4):389–396. [doi:10.1037/1082-989X.10.4.389](https://doi.org/10.1037/1082-989X.10.4.389)
- Cumming G, Fidler F, Vaux DL (2007). Error bars in experimental biology. *Journal of Cell Biology* 177(1):7–11. [doi:10.1083/jcb.200611141](https://doi.org/10.1083/jcb.200611141)
- Nagele P (2003). Misuse of standard error of the mean (SEM) when reporting variability of a sample. A critical evaluation of four anaesthesia journals. *British Journal of Anaesthesia* 90(4):514–516. [doi:10.1093/bja/aeg087](https://doi.org/10.1093/bja/aeg087)
- Weissgerber TL, Milic NM, Winham SJ, Garovic VD (2015). Beyond bar and line graphs: time for a new data presentation paradigm. *PLoS Biology* 13(4):e1002128. [doi:10.1371/journal.pbio.1002128](https://doi.org/10.1371/journal.pbio.1002128)
- Microsoft Support. STDEV.S function; T.INV.2T function. [support.microsoft.com](https://support.microsoft.com/en-us/office/stdev-s-function-7d69cf97-0c1f-4acf-be27-f3e83904cc23) (accessed 10 November 2026)

## Related

- [SuperPlots: cells and biological replicates in one figure](https://www.licklider.ai/guides/superplots/)
- [What counts as n: biological vs technical replicates](https://www.licklider.ai/guides/what-counts-as-n/)
- [How to report a t-test](https://www.licklider.ai/guides/how-to-report-a-t-test/)
- [Statistics on fold change and normalised data](https://www.licklider.ai/guides/statistics-on-fold-change/)

## How this page was made

- Written and published by Licklider. The recommendations follow Cumming, Fidler and Vaux (2007) and the other peer-reviewed papers listed under References; quoted passages were checked against the source text.
- The example, figure, tables and simulation use constructed data and stored code (Python 3.13 with SciPy 1.18.1, matplotlib 3.11.2 and seaborn 0.13.2; R 4.3.3 with ggplot2 4.0.3); Python and R give the same numbers.
- Separate review passes checked the statistics, the citations and readability before publication.
