Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

To make a boxplot of time series data in Matplotlib, group the observations into time periods, pass each period’s raw values to ax.boxplot() as a separate array, and label each box with its period. Each box then shows how the values are spread within one period. It does not show how the values change from one period to the next, so pair it with a line chart when the trend is the question.

What a time series boxplot shows

A boxplot summarises one distribution per box. In a time series setting, each box should be the distribution of the observations that fall inside one period, such as one month or one weekday. The chart compares those distributions side by side. It does not preserve the order of observations inside a period, and it does not draw a path connecting one period to the next.

That makes the boxplot a good tool for questions like “Did the spread of daily sales widen in the second half of the year?” and a poor tool for “Is the metric rising?” For the second question, plot the raw series or a rolling statistic with a line chart, and use the boxplot as a companion view.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 1: Prepare the timestamps and values

Your DataFrame needs a datetime-like column and a numeric column. The example below assumes columns named timestamp and value. Convert the timestamp column explicitly, drop rows that cannot be placed in time or have no measurement, and set the timestamp as the index so that time-based grouping works.

The pandas resample() method is documented as “a time-based groupby, followed by a reduction method on each of its groups.” A boxplot needs the raw values in each group, so you stop before any reduction and keep every observation. The method is documented in the pandas resample reference, and the broader concepts are covered in the pandas time-series user guide.

Step 2: Build one sample per period

The following script groups the values by calendar month, removes empty months, and draws one box per month. Replace df with your own DataFrame.

import matplotlib.pyplot as plt
import pandas as pd

work = df.assign(timestamp=pd.to_datetime(df["timestamp"]))
work = work.dropna(subset=["timestamp", "value"])
work = work.set_index("timestamp").sort_index()

# Keep raw observations in each month; do not aggregate to one value first.
groups = list(work["value"].resample("MS"))

samples = []
labels = []
for period, group in groups:
    values = group.dropna().to_numpy()
    if values.size:  # skip months with no observations
        samples.append(values)
        labels.append(period.strftime("%Y-%m"))

fig, ax = plt.subplots(figsize=(10, 5))
ax.boxplot(samples, tick_labels=labels, showfliers=True)
ax.set_xlabel("Month")
ax.set_ylabel("Value")
ax.set_title("Distribution of observations by month")
ax.tick_params(axis="x", labelrotation=45)
fig.tight_layout()
plt.show()

The Matplotlib boxplot API takes a sequence of one-dimensional arrays and draws one box per array. The tick_labels argument names the boxes. Older Matplotlib releases used labels for the same purpose, so use tick_labels in current code.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reading the chart correctly

Matplotlib’s documentation states: “The box extends from the first quartile (Q1) to the third quartile (Q3) of the data, with a line at the median.” Read each box with that in mind:

  • The box spans Q1 to Q3, so its height is the interquartile range (IQR) for that period.
  • The line inside the box is the median of that period’s values.
  • By default, whiskers extend to the most distant observations still within 1.5 times the IQR from the box. Points beyond that reach are drawn as fliers.
  • Whisker ends are therefore not the minimum and maximum. A flier is a value the default rule classifies as unusually far from the box, not necessarily an error.
  • A box built from very few observations is unstable. Two or three points can produce a box that looks precise but is not.

Choosing what each box represents

The grouping decision changes the meaning of every box, so make it on purpose.

Approach What each box shows When to use it Caveat
Raw observations per period (the script above) Spread of the individual measurements inside that period Comparing variability and typical values across months, weekdays, or seasons Periods with few measurements produce unstable boxes
One mean per period, then one box per period Nothing useful: each box has a single value and is drawn as a flat line Not recommended for period boxes Use a line chart of the means instead
Finer-grained means grouped into larger periods Spread of the finer means inside each larger period, such as daily means within each month Looking at variability of a smoothed daily measure across months The boxes describe the daily means, not the raw readings

The third row is the one that a single .resample("MS").mean() call cannot produce. It needs two steps: first resample to a finer frequency, then group the results by month.

daily = work["value"].resample("D").mean().dropna()
monthly = daily.groupby(daily.index.to_period("M"))

samples = [group.to_numpy() for _, group in monthly]
labels = [str(period) for period, _ in monthly]

Pass closed and label to resample() when the bin edge or label position matters, for example with weekly bins. Their defaults vary by frequency, so check the resample reference for the frequency you use.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Discrete period labels or a continuous date axis

Use text labels when the periods are discrete and evenly spaced, such as months, weekdays, or seasons. Use a continuous date axis when the actual elapsed time matters, or when the bins are unevenly spaced. In that case, place each box at the numeric date of its period start. Matplotlib converts dates to floating-point day counts, so the box width is also measured in days.

import matplotlib.dates as mdates

periods = []
samples = []
for period, group in groups:
    values = group.dropna().to_numpy()
    if values.size:
        periods.append(period)
        samples.append(values)

x = mdates.date2num(periods)

fig, ax = plt.subplots(figsize=(10, 5))
ax.boxplot(samples, positions=x, widths=20, manage_ticks=False)

locator = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))
fig.autofmt_xdate()
plt.show()

The widths=20 value gives each box a width of 20 days, which suits monthly bins. For weekly or daily bins, reduce it. Strings passed as positions are not treated as date labels; use numeric positions and let the date formatter draw the ticks. The date conversion behaviour is described in the Matplotlib dates API.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Keep comparisons fair

  • Use the same time bins for every series you compare, and the same y-axis limits where the values share a unit.
  • Show sample sizes, for instance by adding the count to each tick label, so readers can judge how much to trust each box.
  • Leave out empty periods rather than drawing a zero-valued box. A missing month is not a month with zero observations.

Version and precision notes

At the time of writing, the current stable documentation for Matplotlib is 3.11.2 and for pandas is 3.0.6. In the boxplot API, orientation was added in Matplotlib 3.10 and controls whether boxes are vertical or horizontal (orientation="horizontal"). The older vert argument is deprecated since Matplotlib 3.11. If you support older environments, check the signatures installed in your own version.

Matplotlib’s date handling uses days since the 1970-01-01 UTC epoch. The documentation notes that microsecond precision holds for dates about 70 years on either side of that epoch, with precision degrading farther out. For plots that need sub-microsecond precision, the documentation recommends floating-point seconds, and any change of epoch must happen before dates are converted. Ordinary daily or monthly charts are not affected.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting

  • ValueError when building the labels or samples: every period is empty. Check that value is numeric and that dropna() has not removed all rows. Inspect work.head() and work["value"].dtype before grouping.
  • Every period lands in one month or the date parse fails: the timestamp strings do not match pandas’ default parser. Pass an explicit format= to pd.to_datetime().
  • Boxes are flat lines: a period has one observation, so its quartiles collapse to the value itself. Aggregate to a coarser period or drop that period from the chart.
  • Boxes overlap or are too thin on a date axis: widths is measured in days. Set it relative to your bin size.
  • Date ticks show numbers instead of dates: the axis formatter was not set. Assign the ConciseDateFormatter as shown above.

pandas can also draw grouped boxplots directly through DataFrameGroupBy.boxplot. That is convenient for quick views, but building the sample list yourself, as shown here, gives you control over each box’s values, labels, and empty-period handling.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.