Turn replicate measurements into means and standard errors and plot them with matplotlib, error bars, labelled axes and units.
Why this matters
A plot is often the only part of your work a reader looks at closely. A good scientific plot answers three questions at a glance: what was measured (axis labels with units), what the values are (points, not just a smooth line), and how sure you are (error bars). Reviewers, supervisors and clients will ask about all three. matplotlib is the standard Python plotting library, and its object-oriented interface gives you full control.
From replicates to error bars
Most plotted points are a summary of several replicate measurements. Suppose you measure the extension of a spring at four loads, three times each:
import numpy as np
load_N = np.array([1.0, 2.0, 3.0, 4.0])
extension_mm = np.array([
[2.1, 2.0, 2.2],
[4.0, 4.3, 4.1],
[6.2, 5.9, 6.1],
[8.3, 8.0, 8.2],
]) # rows = loads, columns = replicates
mean = extension_mm.mean(axis=1) # one mean per load
sd = extension_mm.std(axis=1, ddof=1) # sample SD per load
n = extension_mm.shape[1]
sem = sd / np.sqrt(n) # standard error of the mean
Decide, and say in the caption, what your error bars show:
- SD: the spread of individual measurements.
- SEM (standard error of the mean): how well you know the mean. It shrinks with more replicates.
- 95% confidence interval: roughly
2 × SEMfor largen; for smallnmultiply SEM by the t-value (4.30 for n = 3).
Unlabelled error bars are a common reason papers get sent back.
The matplotlib object interface
Use fig, ax = plt.subplots() and call methods on ax. This scales to multi-panel figures and avoids the confusing global state of plt.plot.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(4.5, 3.2), dpi=150)
ax.errorbar(load_N, mean, yerr=sem, fmt="o", capsize=3,
color="black", label="measured (mean ± SEM, n = 3)")
# Fitted line for comparison
k, c = np.polyfit(load_N, mean, 1)
x_fit = np.linspace(0, 4.5, 100)
ax.plot(x_fit, k * x_fit + c, "-", color="tab:blue",
label=f"fit: {k:.2f} mm/N")
ax.set_xlabel("Load (N)")
ax.set_ylabel("Extension (mm)")
ax.set_xlim(0, 4.5)
ax.set_ylim(bottom=0)
ax.legend(frameon=False)
fig.tight_layout()
fig.savefig("spring.png") # or .pdf / .svg for vector output
Matplotlib is not available in this course's in-browser exercises, so run plotting code in Jupyter, Google Colab or a local Python install. The calculations that feed the plot (means, SEMs, fits) are plain NumPy and work anywhere.
What makes a plot "publication quality"
- Axis labels with units in a consistent style, such as
Load (N)orTemperature / K. Never leave the default blank axes. - Markers for data, lines for models. Measured points are discrete; connecting them with a line suggests you measured in between.
- Readable sizes. Make the figure at its final printed size (a journal column is about 8.5 cm, or 3.3 inches, wide) so fonts are not shrunk to illegibility. Font size 8 to 10 pt at final size is typical.
- Vector formats (
.pdf,.svg) for line art;.pngat 300 dpi or more for raster. - Colour that survives. Use colour-blind-safe palettes (matplotlib's default
tab10and theviridiscolormap are good) and vary marker shape as well as colour, so it still works in greyscale. - Honest axes. Start bar charts at zero. If you zoom a line plot, make it obvious.
- A caption that says what the error bars are and what
nis.
Several panels
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 3), sharey=False)
ax1.errorbar(load_N, mean, yerr=sem, fmt="o", capsize=3)
residuals = mean - (k * load_N + c)
ax2.axhline(0, color="grey", lw=0.8)
ax2.plot(load_N, residuals, "s")
ax2.set_xlabel("Load (N)")
ax2.set_ylabel("Residual (mm)")
A residual plot next to a fit is one of the most useful panels you can add: if the residuals show a pattern (a curve, a funnel), the model is missing something.
Common mistakes
- Using
std()withoutddof=1for three replicates: it underestimates the spread by about 18%. - Mixing up SD and SEM or not saying which you plotted.
- Plotting means with no indication of
n. Three replicates and three hundred look identical without it. - Default tiny figures saved at screen resolution, then stretched in a report.
- Rainbow (
jet) colormaps, which create false boundaries and fail for colour-blind readers. - Units in the title instead of on the axes.
Quick recap
- Summarise replicates with
mean(axis=...),std(ddof=1)andSEM = SD / sqrt(n). fig, ax = plt.subplots(), thenax.errorbar(x, y, yerr=..., fmt="o", capsize=3).- Label every axis with units, use markers for data and lines for models, and state what the error bars mean.
- Save at final size in a vector format.
Try it
In the Do tab you will compute the means and standard errors behind an error-bar plot and write a helper that formats axis labels with units.