Skip to main content
Back to module
Scientific Python · Plotting Scientific Data

Publication-quality plots

Reading1h 10mLesson 1 of 3 in this module

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 × SEM for large n; for small n multiply 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"

  1. Axis labels with units in a consistent style, such as Load (N) or Temperature / K. Never leave the default blank axes.
  2. Markers for data, lines for models. Measured points are discrete; connecting them with a line suggests you measured in between.
  3. 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.
  4. Vector formats (.pdf, .svg) for line art; .png at 300 dpi or more for raster.
  5. Colour that survives. Use colour-blind-safe palettes (matplotlib's default tab10 and the viridis colormap are good) and vary marker shape as well as colour, so it still works in greyscale.
  6. Honest axes. Start bar charts at zero. If you zoom a line plot, make it obvious.
  7. A caption that says what the error bars are and what n is.

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() without ddof=1 for 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) and SEM = SD / sqrt(n).
  • fig, ax = plt.subplots(), then ax.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.

Also in this lesson: