Time series, error bars and bands

Matplotlib · Practical · 12 min

Most real data is a time series, and a value without its uncertainty can mislead.

Matplotlib understands datetime, numpy.datetime64 and pandas timestamps on either axis: pass them to plot directly. It converts them to floating-point days internally and picks date-aware ticks.

To control the ticks, use the date locators and formatters from matplotlib.dates:

  • mdates.MonthLocator(), WeekdayLocator(), DayLocator(interval=7) choose positions;
  • mdates.DateFormatter("%b %Y") formats with strftime codes;
  • mdates.ConciseDateFormatter(locator) produces compact, non-repetitive labels (year shown only when it changes) and is usually the best default.

fig.autofmt_xdate() rotates and right-aligns crowded date labels.

For uncertainty:

  • ax.errorbar(x, y, yerr=err, fmt="o", capsize=3) draws points with error bars; yerr can be a scalar, one value per point, or a 2xN array for asymmetric errors (below, above).
  • ax.fill_between(x, low, high, alpha=0.25) draws a shaded band, which reads better than error bars for dense series such as forecasts or rolling statistics.

Say in the caption or legend what the bars mean: one standard deviation, standard error and a 95% confidence interval differ by large factors.

With pandas, df.plot() already uses these mechanisms; any formatter you set afterwards on ax.xaxis still applies.

Example

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np

days = np.arange("2026-01-01", "2026-04-01", dtype="datetime64[D]")
rng = np.random.default_rng(3)
y = np.cumsum(rng.normal(size=days.size))
err = 1.0 + 0.02 * np.arange(days.size)       # uncertainty grows over time

fig, ax = plt.subplots(layout="constrained")
ax.plot(days, y, color="C0")
ax.fill_between(days, y - err, y + err, color="C0", alpha=0.25)
loc = mdates.MonthLocator()
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(loc))

fig.canvas.draw()
print(days.size)                               # 90
print([t.get_text() for t in ax.get_xticklabels()])  # ['2026', 'Feb', 'Mar', 'Apr']
fig.savefig("ts.png")

Remember

  • Pass datetimes straight to plot; Matplotlib picks date ticks
  • ConciseDateFormatter gives compact, readable date labels
  • Use errorbar for sparse points and fill_between bands for dense series

Watch out for

  • Showing error bars without stating whether they are standard deviation, standard error or a confidence interval.

Check yourself

  1. You have a daily forecast for a year with a lower and upper bound per day. What is the clearest way to show the bounds?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: matplotlib.org/stable/api/dates_api.html