⇦ Back

For this page we will use data from the finals of various Olympic events, available on Wikipedia.

This page functions as a follow-on from Boxplots with One Group of Data.

1 Too Simple

If we try to plot multiple datasets (eg the results from more than one Olympic Games) with two groups within each dataset (men and women), things start to become confusing:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# Results of the long jump finals at two Olympic Games
data = pd.DataFrame({
    'London 2012 (Men)': [8.31, 8.16, 8.12, 8.11, 8.10, 8.07, 8.01, 7.93],
    'Rio 2016 (Men)': [8.38, 8.37, 8.29, 8.25, 8.17, 8.10, 8.06, 8.05],
    'London 2012 (Women)': [7.12, 7.07, 6.89, 6.88, 6.77, 6.76, 6.72, 6.67],
    'Rio 2016 (Women)': [7.17, 7.15, 7.08, 6.95, 6.81, 6.79, 6.74, 6.69]
})

# Plot
bp = plt.boxplot(
    # A data frame needs to be converted to an array before it can be plotted this way
    np.array(data),
    # You can use the column headings from the data frame as labels
    labels=list(data)
)
# Axis details
plt.title('Long Jump Finals')
plt.ylabel('Distance [m]')
plt.xlabel('Olympics')
plt.minorticks_on()
plt.tick_params(axis='x', which='minor', bottom=False)
plt.tick_params(axis='x', which='major', labelsize='small')

plt.show()

2 A Better Approach

Try breaking the plots up into groups, clearly showing showing which boxes belong together in order to aid interpretation. This will also allow you to fit more data in:

import numpy as np
import matplotlib.ticker as ticker

# Results of the long jump finals at four Olympic games
athens = pd.DataFrame({
    'Men': [8.59, 8.47, 8.32, 8.31, 8.25, 8.24, 8.23, 8.21],
    'Women': [7.07, 7.05, 7.05, 6.96, 6.85, 6.83, 6.80, 6.73]
})
beijing = pd.DataFrame({
    'Men': [8.34, 8.24, 8.20, 8.19, 8.19, 8.16, 8.07, 8.00],
    'Women': [7.04, 7.03, 6.91, 6.79, 6.76, 6.70, 6.64, 6.58]
})
london = pd.DataFrame({
    'Men': [8.31, 8.16, 8.12, 8.11, 8.10, 8.07, 8.01, 7.93],
    'Women': [7.12, 7.07, 6.89, 6.88, 6.77, 6.76, 6.72, 6.67]
})
rio = pd.DataFrame({
    'Men': [8.38, 8.37, 8.29, 8.25, 8.17, 8.10, 8.06, 8.05],
    'Women': [7.17, 7.15, 7.08, 6.95, 6.81, 6.79, 6.74, 6.69]
})
datasets = [athens, beijing, london, rio]

# Set x-positions for boxes
x_pos_range = np.arange(len(datasets)) / (len(datasets) - 1)
x_pos = (x_pos_range * 0.5) + 0.75
# Plot
for i, data in enumerate(datasets):
    bp = plt.boxplot(
        np.array(data), sym='', whis=[0, 100], widths=0.6 / len(datasets),
        labels=list(datasets[0]),
        positions=[x_pos[i] + j * 1 for j in range(len(data.T))]
    )
# Titles
plt.title('Long Jump Finals at the Last Four Olympic Games')
plt.ylabel('Distance [m]')
# Axis ticks and labels
plt.xticks(np.arange(len(list(datasets[0]))) + 1)
plt.gca().xaxis.set_minor_locator(ticker.FixedLocator(
    np.array(range(len(list(datasets[0])) + 1)) + 0.5)
)
plt.gca().tick_params(axis='x', which='minor', length=4)
plt.gca().tick_params(axis='x', which='major', length=0)
# Change the limits of the x-axis
plt.xlim([0.5, len(list(datasets[0])) + 0.5])

plt.show()

Note that we could also have used lists-of-lists as the datasets instead of data frames (with some minor changes, eg data would not have been transposed and the labels would have needed to be manually coded).

3 Colour and Format

A big step towards better communication will be colour-coding the boxes. At the same time we’ll improve the format and size of the plots (see Image Sizes and Latex in Labels for more info):

# Make figures A6 in size
A = 6
plt.rc('figure', figsize=[46.82 * .5**(.5 * A), 33.11 * .5**(.5 * A)])
# Use Latex
plt.rc('text', usetex=True)
plt.rc('font', family='serif')

# Define which colours you want to use
colours = ['blue', 'red']

# Set x-positions for boxes
x_pos_range = np.arange(len(datasets)) / (len(datasets) - 1)
x_pos = (x_pos_range * 0.5) + 0.75
# Plot
for i, data in enumerate(datasets):
    bp = plt.boxplot(
        np.array(data), sym='', whis=[0, 100], widths=0.6 / len(datasets),
        labels=list(datasets[0]), patch_artist=True,
        positions=[x_pos[i] + j * 1 for j in range(len(data.T))]
    )
    # Fill the boxes with colours (requires patch_artist=True)
    k = i % len(colours)
    for box in bp['boxes']:
        box.set(facecolor=colours[k])
    for element in ['boxes', 'fliers', 'means', 'medians']:
        plt.setp(bp[element], color=colours[k])
    for element in ['whiskers', 'caps']:
        plt.setp(bp[element], color=colours[k])
        plt.setp(bp[element], color=colours[k])
# Titles
plt.title('Long Jump Finals at the Last Four Olympic Games')
plt.ylabel('Distance [m]')
# Axis ticks and labels
plt.xticks(np.arange(len(list(datasets[0]))) + 1)
plt.gca().xaxis.set_minor_locator(ticker.FixedLocator(
    np.array(range(len(list(datasets[0])) + 1)) + 0.5)
)
plt.gca().tick_params(axis='x', which='minor', length=4)
plt.gca().tick_params(axis='x', which='major', length=0)
# Change the limits of the x-axis
plt.xlim([0.5, len(list(datasets[0])) + 0.5])

plt.show()

The one glaring omission is that we still need to show which Olympics each set of data comes from…

4 Legend

Adding a legend will clarify what data belongs to what group.

from matplotlib.patches import Patch

# Define which colours you want to use
colours = ['blue', 'red']
# Define the groups
groups = ['Athens 2004', 'Beijing 2008', 'London 2012', 'Rio 2016']

# Legend
legend_elements = []
for i in range(len(datasets)):
    j = i % len(groups)
    k = i % len(colours)
    legend_elements.append(Patch(facecolor=colours[k], label=groups[j]))
plt.legend(handles=legend_elements, fontsize=8)

A trick to get circles instead of rectangles in the legend is to create dummy scatter plots and refer to those when creating it:

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import pandas as pd

# Define which colours you want to use
colours = ['blue', 'red']
# Define the groups
groups = ['Athens 2004', 'Beijing 2008', 'London 2012', 'Rio 2016']

plt.title('Long Jump Finals at the Last Four Olympic Games')
for i, data in enumerate(datasets):
    bp = plt.boxplot(
        np.array(data), sym='', whis=[0, 100], widths=0.6 / len(datasets),
        labels=list(datasets[0]), patch_artist=True,
        positions=[x_pos[i] + j * 1 for j in range(len(data.T))]
    )
    # Fill the boxes with colours (requires patch_artist=True)
    k = i % len(colours)
    for box in bp['boxes']:
        box.set(facecolor=colours[k])
    for element in ['boxes', 'fliers', 'means', 'medians']:
        plt.setp(bp[element], color=colours[k])
    for element in ['whiskers', 'caps']:
        plt.setp(bp[element], color=colours[k])
        plt.setp(bp[element], color=colours[k])
# Axes
plt.ylabel(r'Distance $[m]$')
plt.xlim([0.5, 2.5])
plt.xticks([1, 2])
plt.gca().xaxis.set_minor_locator(ticker.FixedLocator([0.5, 1.5, 2.5]))
plt.gca().tick_params(axis='x', which='minor', length=4)
plt.gca().tick_params(axis='x', which='major', length=0)
# Legend
g1 = plt.gca().scatter(0, 7, color='b')
g2 = plt.gca().scatter(0, 7, color='r')
plt.gca().legend([g1, g2, g1, g2], groups, fontsize='small')

plt.show()

5 Annotate the Median Values

The median value of each boxplot can be shown directly on the plot using annotations. Note that if there are too many groups when doing this the annotations might start to overlap, so we’ll drop down to just two for this example.

datasets = [london, rio]

# Define which colours you want to use
colours = ['blue', 'red']
# Define the groups
groups = ['Athens 2004', 'Beijing 2008', 'London 2012', 'Rio 2016']

# Get the max of the dataset
all_maximums = [d.max(axis=1).values for d in datasets]
dataset_maximums = [max(m) for m in all_maximums]
y_max = max(dataset_maximums)
# Get the min of the dataset
all_minimums = [d.min(axis=1).values for d in datasets]
dataset_minimums = [min(m) for m in all_minimums]
y_min = min(dataset_minimums)
# Calculate the y-axis range
y_range = y_max - y_min

# Set x-positions for boxes
x_pos_range = np.arange(len(datasets)) / (len(datasets) - 1)
x_pos = (x_pos_range * 0.5) + 0.75
# Plot
for i, data in enumerate(datasets):
    positions = [x_pos[i] + j * 1 for j in range(len(data.T))]
    bp = plt.boxplot(
        np.array(data), sym='', whis=[0, 100], widths=0.6 / len(datasets),
        labels=list(datasets[0]), patch_artist=True,
        positions=positions
    )
    # Fill the boxes with colours (requires patch_artist=True)
    k = i % len(colours)
    for box in bp['boxes']:
        box.set(facecolor=colours[k])
    # Make the median lines more visible
    plt.setp(bp['medians'], color='black')

    # Get the samples' medians
    medians = [bp['medians'][j].get_ydata()[0] for j in range(len(data.T))]
    medians = [str(round(s, 2)) for s in medians]
    # Increase the height of the plot by 5% to fit the labels
    plt.ylim([y_min - 0.1 * y_range, y_max + 0.05 * y_range])
    # Set the y-positions for the labels
    y_pos = y_min - 0.075 * y_range
    for tick, label in zip(range(len(data.T)), plt.xticks()):
        k = tick % 2
        plt.text(
            positions[tick], y_pos, r'$\tilde{x} =' + fr' {medians[tick]}$m',
            horizontalalignment='center', size='medium'
        )

# Titles
plt.title('Long Jump Finals at the Last Four Olympic Games')
plt.ylabel('Distance [m]')
# Axis ticks and labels
plt.xticks(np.arange(len(list(datasets[0]))) + 1)
plt.gca().xaxis.set_minor_locator(ticker.FixedLocator(
    np.array(range(len(list(datasets[0])) + 1)) + 0.5)
)
plt.gca().tick_params(axis='x', which='minor', length=4)
plt.gca().tick_params(axis='x', which='major', length=0)
# Change the limits of the x-axis
plt.xlim([0.5, len(list(datasets[0])) + 0.5])
# Legend
legend_elements = []
for i in range(len(datasets)):
    j = i % len(groups)
    k = i % len(colours)
    legend_elements.append(Patch(facecolor=colours[k], label=groups[j]))
plt.legend(handles=legend_elements, fontsize=8)

plt.show()

6 Adjusting the Plot Area

When things start getting busier you’ll want to have the legend positioned outside the plot itself. This will require you to edit the amount of white space between the edge of the graph and the edge of the image, and this can be done in the subplots_adjust() function:

  • The function subplots_adjust() can be fed keyword arguments that set the positions of the left, right, top and bottom edges of the plot area. The arguments must be numbers that represent the fractions of the figure’s width/height at which the relevant edges should be located. In other words, right=0.8 will position the right edge of the plot so it is 80% of the way along the width of the image.
    • The default values are 0.125, 0.9, 0.88 and 0.11, meaning that the white space to the left of the plot is equal to 12.5% of the image’s width, the white space at the bottom of the image is 11% of its height, etc

With regards to the legend, its location cannot be finely adjusted when you use legend() so it cannot be moved outside the plot as things stand. However, we can use the more basic function gca().legend() (where gca stands for ‘get current axis’) to isolate just the legend object in order to edit its hidden attributes. Specifically, we can use the bbox_to_anchor=() keyword argument to define its position, so by setting this to \((1, 0.5)\) the legend will be placed at \(x = 1\) (ie outside the plot, to the right of it) and at \(y = 0.5\) (ie halfway up the plot).

# Legend
legend_elements = []
for i in range(len(datasets)):
    j = i % len(groups)
    k = i % len(colours)
    legend_elements.append(Patch(facecolor=colours[k], label=groups[j]))
plt.subplots_adjust(right=0.75)
plt.gca().legend(
    legend_elements, groups,
    fontsize=8, loc='center left', bbox_to_anchor=(1, 0.5)
)

plt.show()

7 Add Gridlines

Gridlines can aid in interpreting a graph.

  • Show the major axis gridlines using grid()
  • Plot horizontal lines to aid in interpretation using axhline()
# Straight lines
plt.grid(color='grey')
plt.axhline(7, color='black', alpha=0.4)
plt.axhline(8, color='black', alpha=0.4)

8 Swapping Groups and Categories

The method of plotting we have been using is robust to the number of datasets and groups that are used, so if we wanted to add in more data and swap things around it shouldn’t be too much of a problem:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.patches import Patch

london = pd.DataFrame({
    'Long Jump': [8.31, 8.16, 8.12, 8.11, 8.10, 8.07, 8.01, 7.93],
    'Shot Put': [21.89, 21.86, 21.23, 21.19, 20.93, 20.84, 20.71, 20.69],
    'Discus': [68.27, 68.18, 68.03, 67.38, 67.19, 65.85, 65.56, 64.79],
    'Hammer Throw': [80.59, 79.36, 78.71, 78.25, 77.86, 77.17, 77.10, 76.07],
    'Javelin': [84.58, 84.51, 84.12, 83.34, 82.80, 82.63, 81.91, 81.21]
})
rio = pd.DataFrame({
    'Long Jump': [8.38, 8.37, 8.29, 8.25, 8.17, 8.10, 8.06, 8.05],
    'Shot Put': [22.52, 21.78, 21.36, 21.20, 21.02, 20.72, 20.64, 20.64],
    'Discus': [68.37, 67.55, 67.05, 66.58, 65.10, 64.95, 64.50, 63.72],
    'Hammer Throw': [78.68, 77.79, 77.73, 76.05, 75.97, 75.46, 75.28, 74.61],
    'Javelin': [90.30, 88.24, 85.38, 85.32, 83.95, 83.05, 82.51, 82.42]
})
datasets = [london, rio]
# Define which colours you want to use
colours = ['blue', 'red']
# Define the groups
groups = ['Athens 2004', 'Beijing 2008', 'London 2012', 'Rio 2016']

# Get the max of the dataset
all_maximums = [d.max(axis=1).values for d in datasets]
dataset_maximums = [max(m) for m in all_maximums]
y_max = max(dataset_maximums)
# Get the min of the dataset
all_minimums = [d.min(axis=1).values for d in datasets]
dataset_minimums = [min(m) for m in all_minimums]
y_min = min(dataset_minimums)
# Calculate the y-axis range
y_range = y_max - y_min

# Set x-positions for boxes
x_pos_range = np.arange(len(datasets)) / (len(datasets) - 1)
x_pos = (x_pos_range * 0.5) + 0.75
# Plot
for i, data in enumerate(datasets):
    positions = [x_pos[i] + j * 1 for j in range(len(data.T))]
    bp = plt.boxplot(
        np.array(data), sym='', whis=[0, 100], widths=0.6 / len(datasets),
        labels=list(datasets[0]), patch_artist=True,
        positions=positions
    )
    # Fill the boxes with colours (requires patch_artist=True)
    k = i % len(colours)
    for box in bp['boxes']:
        box.set(facecolor=colours[k])
    # Make the median lines more visible
    plt.setp(bp['medians'], color='black')

    # Get the samples' medians
    medians = [bp['medians'][j].get_ydata()[0] for j in range(len(data.T))]
    medians = [str(int(s)) for s in medians]
    # Increase the height of the plot by 5% to fit the labels
    plt.ylim([y_min - 0.1 * y_range, y_max + 0.05 * y_range])
    # Set the y-positions for the labels
    y_pos = y_min - 0.075 * y_range
    for j in range(len(data.T)):
        plt.text(
            positions[j], y_pos, r'$\tilde{x} =' + fr' {medians[j]}$m',
            horizontalalignment='center', size='xx-small'
        )

# Titles
plt.title("Men's Finals at the Last Two Olympic Games")
plt.ylabel('Distance [m]')
plt.xlabel('Event')
# Axis ticks and labels
plt.xticks(np.arange(len(list(datasets[0]))) + 1)
plt.minorticks_on()
plt.tick_params(axis='x', which='minor', bottom=False)
plt.subplots_adjust(right=0.98)
# Change the limits of the x-axis
plt.xlim([0.5, len(list(datasets[0])) + 0.5])
# Legend
legend_elements = []
for i in range(len(datasets)):
    j = i % len(groups)
    k = i % len(colours)
    legend_elements.append(Patch(facecolor=colours[k], label=groups[j]))
plt.legend(handles=legend_elements, fontsize=8)
# Straight lines
plt.grid(color='grey')

plt.show()

9 Subplots

The easiest way to plots multiple graphs that all have the same format is to create a function and call that repeatedly on each dataset. Some changes do need to be made, however:

  • Increase the size of the image so that all plots can fit onto it. Generally speaking, six plots can fit onto an A4 page.
  • Subplots need to be created using subplot(rcn) where r is the total number of rows of plots you intend to make, c is the number of columns of plots you intend to make and n is the number that this plot will be within the grid of plots. For example, subplot(321) will divide your figure into a grid with 3 rows and 2 columns (ie space for 6 plots) and then create an empty sub-plot for the first (top-left) of these plots. The plots are numbered using ‘reading order’ (left-to-right, top-to-bottom), ie plot 2 will be top-right, plot 3 will be middle-left and so on.
  • Change the spacing between graphs and the edges of the image with subplots_adjust()
  • Certain elements that change from plot-to-plot now become arguments of the function:
    • The datasets themselves
    • The axes themselves
    • The plot titles

First, create the data:

# Long jump
london = pd.DataFrame({
    'Men': [8.31, 8.16, 8.12, 8.11, 8.10, 8.07, 8.01, 7.93],
    'Women': [7.12, 7.07, 6.89, 6.88, 6.77, 6.76, 6.72, 6.67]
})
rio = pd.DataFrame({
    'Men': [8.38, 8.37, 8.29, 8.25, 8.17, 8.10, 8.06, 8.05],
    'Women': [7.17, 7.15, 7.08, 6.95, 6.81, 6.79, 6.74, 6.69]
})
data1 = [london, rio]
title1 = 'Long Jump'

# Shot put
london = pd.DataFrame({
    'Men': [21.89, 21.86, 21.23, 21.19, 20.93, 20.84, 20.71, 20.69],
    'Women': [21.36, 20.70, 20.48, 20.22, 19.63, 19.42, 19.18, 19.02]
})
rio = pd.DataFrame({
    'Men': [22.52, 21.78, 21.36, 21.20, 21.02, 20.72, 20.64, 20.64],
    'Women': [20.63, 20.42, 19.87, 19.39, 19.35, 19.03, 18.37, 18.23]
})
data2 = [london, rio]
title2 = 'Shot Put'

# Javelin
london = pd.DataFrame({
    'Men': [84.58, 84.51, 84.12, 83.34, 82.80, 82.63, 81.91, 81.21],
    'Women': [69.55, 65.16, 64.91, 64.53, 63.70, 62.89, 61.62, 60.73]
})
rio = pd.DataFrame({
    'Men': [90.30, 88.24, 85.38, 85.32, 83.95, 83.05, 82.51, 82.42],
    'Women': [66.18, 64.92, 64.80, 64.78, 64.60, 64.36, 64.04, 62.92]
})
data3 = [london, rio]
title3 = 'Javelin'

# Discus
london = pd.DataFrame({
    'Men': [68.27, 68.18, 68.03, 67.38, 67.19, 65.85, 65.56, 64.79],
    'Women': [69.11, 67.22, 66.38, 65.94, 63.98, 63.62, 63.01, 61.68]
})
rio = pd.DataFrame({
    'Men': [68.37, 67.55, 67.05, 66.58, 65.10, 64.95, 64.50, 63.72],
    'Women': [69.21, 66.73, 65.34, 64.90, 64.37, 63.13, 63.11, 63.06]
})
data4 = [london, rio]
title4 = 'Discus'

# Hammer throw
london = pd.DataFrame({
    'Men': [80.59, 79.36, 78.71, 78.25, 77.86, 77.17, 77.10, 76.07],
    'Women': [78.18, 77.60, 77.13, 76.34, 76.05, 74.60, 74.40, 74.06]
})
rio = pd.DataFrame({
    'Men': [78.68, 77.79, 77.73, 76.05, 75.97, 75.46, 75.28, 74.61],
    'Women': [82.29, 76.75, 74.54, 73.71, 73.50, 72.74, 71.90, 70.95]
})
data5 = [london, rio]
title5 = 'Hammer Throw'

# Triple jump
london = pd.DataFrame({
    'Men': [17.81, 17.62, 17.48, 17.34, 17.19, 17.08, 16.95, 16.92],
    'Women': [14.98, 14.80, 14.79, 14.56, 14.48, 14.48, 14.35, 14.24]
})
rio = pd.DataFrame({
    'Men': [17.86, 17.76, 17.58, 17.13, 17.09, 17.03, 16.90, 16.68],
    'Women': [15.17, 14.98, 14.74, 14.71, 14.68, 14.65, 14.53, 14.26]
})
data6 = [london, rio]
title6 = 'Triple Jump'

datasets = [data1, data4, data2, data5, data3, data6]
title = [title1, title4, title2, title5, title3, title6]
groups = [['London 2012', 'Rio 2016']] * 6
colours = ['blue', 'red']

Now plot it:

def plot_boxplots(datasets, colours, groups, title):
    # Get the max of the dataset
    all_maximums = [d.max(axis=1).values for d in datasets]
    dataset_maximums = [max(m) for m in all_maximums]
    y_max = max(dataset_maximums)
    # Get the min of the dataset
    all_minimums = [d.min(axis=1).values for d in datasets]
    dataset_minimums = [min(m) for m in all_minimums]
    y_min = min(dataset_minimums)
    # Calculate the y-axis range
    y_range = y_max - y_min

    # Set x-positions for boxes
    x_pos_range = np.arange(len(datasets)) / (len(datasets) - 1)
    x_pos = (x_pos_range * 0.5) + 0.75
    # Create the plot
    for i, data in enumerate(datasets):
        positions = [x_pos[i] + j * 1 for j in range(len(data.T))]
        bp = plt.boxplot(
            np.array(data), sym='', whis=[0, 100], widths=0.6 / len(datasets),
            labels=list(datasets[0]), patch_artist=True,
            positions=positions
        )
        # Fill the boxes with colours (requires patch_artist=True)
        k = i % len(colours)
        for box in bp['boxes']:
            box.set(facecolor=colours[k])
        # Make the median lines more visible
        plt.setp(bp['medians'], color='black')

        # Get the samples' medians
        medians = [bp['medians'][j].get_ydata()[0] for j in range(len(data.T))]
        medians = [str(int(s)) for s in medians]
        # Increase the height of the plot by 5% to fit the labels
        plt.ylim([y_min - 0.1 * y_range, y_max + 0.05 * y_range])
        # Set the y-positions for the labels
        y_pos = y_min - 0.075 * y_range
        for j in range(len(data.T)):
            plt.text(
                positions[j], y_pos, r'$\tilde{x} =' + fr' {medians[j]}$m',
                horizontalalignment='center', size='xx-small'
            )

    # Axis details
    plt.title(title)
    plt.ylabel('Distance [m]')
    plt.xticks(np.arange(len(list(datasets[0]))) + 1)
    plt.gca().xaxis.set_minor_locator(ticker.FixedLocator(
        np.array(range(len(list(datasets[0])) + 1)) + 0.5)
    )
    plt.gca().tick_params(axis='x', which='minor', length=4)
    plt.gca().tick_params(axis='x', which='major', length=0)
    plt.xlim([0.5, len(list(datasets[0])) + 0.5])

    # Legend
    legend_elements = []
    for i in range(len(datasets)):
        j = i % len(groups)
        k = i % len(colours)
        legend_elements.append(Patch(facecolor=colours[k], label=groups[j]))
    plt.legend(handles=legend_elements, fontsize=8)


# Make the figure A4
A = 4
plt.rc('figure', figsize=[33.11 * .5**(.5 * A), 46.82 * .5**(.5 * A)])
# Use Latex
plt.rc('text', usetex=True)
plt.rc('font', family='serif')

# Call function
for i, v in enumerate(range(321, 327)):
    plt.subplot(v)
    plot_boxplots(datasets[i], colours, groups[i], title[i])
plt.subplots_adjust(
    left=0.1, right=0.98, top=0.97, bottom=0.06, wspace=0.3, hspace=0.25
)   

plt.show()

⇦ Back