How To Fetch The Exact Values From A Boxplot (Python) | by Elena Kosourova | Towards Data Science

A boxplot is a type of visualization used for displaying the five-number set of descriptive statistics for a dataset: the minimum and maximum (excluding the outliers), the median, the first (Q1) and third (Q3) quartiles. In Python, boxplots can be created in various data visualization libraries including the most basic one — matplotlib.
While the main scope of a boxplot is to visualize the statistical information about a dataset, what if we also need to extract and print out the exact numbers of such statistics? In this article, we’ll discuss the easiest way of doing so in the matplotlib library.
To start with, let’s create 3 dummy datasets and display the boxplots for them in matplotlib. To be able to further extract the necessary values, though, we have to assign the result of the plt.boxplot() method to a variable (bp):
import matplotlib.pyplot as pltimport numpy as npnp.random.seed(1)data_1 = np.random.normal(50, 30, 300)data_2 = np.random.normal(100, 40, 300)data_3 = np.random.normal(70, 10, 300)data = [data_1, data_2, data_3]bp = plt.boxplot(data)plt.show()
The resulting variable bp is a Python dictionary:
type(bp)Output:dict
with the following keys representing the main elements of a boxplot:
bp.keys()Output:dict_keys(['whiskers', 'caps', 'boxes', 'medians', 'fliers', 'means'])
Here is the dictionary itself:
bpOutput:{'whiskers': [<matplotlib.lines.Line2D at 0x1eaf6131b50>,  <matplotlib.lines.Line2D at 0x1eaf6131eb0>,  <matplotlib.lines.Line2D at 0x1eaf61533a0>,  <matplotlib.lines.Line2D at 0x1eaf6153700>,  <matplotlib.lines.Line2D at 0x1eaf6162b80>,  <matplotlib.lines.Line2D at 0x1eaf6162ee0>], 'caps': [<matplotlib.lines.Line2D at 0x1eaf614a250>,  <matplotlib.lines.Line2D at 0x1eaf614a5b0>,  <matplotlib.lines.Line2D at 0x1eaf6153a60>,  <matplotlib.lines.Line2D at 0x1eaf6153dc0>,  <matplotlib.lines.Line2D at 0x1eaf616d280>,  <matplotlib.lines.Line2D at 0x1eaf616d5e0>], 'boxes': [<matplotlib.lines.Line2D at 0x1eaf61317f0>,  <matplotlib.lines.Line2D at 0x1eaf6153040>,  <matplotlib.lines.Line2D at 0x1eaf6162820>], 'medians': [<matplotlib.lines.Line2D at 0x1eaf614a910>,  <matplotlib.lines.Line2D at 0x1eaf6162160>,  <matplotlib.lines.Line2D at 0x1eaf616d940>], 'fliers': [<matplotlib.lines.Line2D at 0x1eaf614ac70>,  <matplotlib.lines.Line2D at 0x1eaf61624c0>,  <matplotlib.lines.Line2D at 0x1eaf616dca0>], 'means': []}
We see that the values of the dictionary are actually lists of matplotlib objects (in particular, matplotlib.lines.Line2D). All in all, we have 6 whiskers (2 for each boxplot), 6 caps (2 for each boxplot), then 3 boxes, 3 medians, and 3 sets of outliers (fliers), one for each boxplot. A curious thing is that means has an empty list as its value. It's happened because, by default, a boxplot doesn't display the sample mean. To show the mean value on a boxplot, we have to specify it through an optional parameter showmeans:
bp = plt.boxplot(data, showmeans=True)bp['means']Output:[<matplotlib.lines.Line2D at 0x1eaf6262790>, <matplotlib.lines.Line2D at 0x1eaf627b340>, <matplotlib.lines.Line2D at 0x1eaf6286e80>][<matplotlib.lines.Line2D at 0x1eaf6262790>, <matplotlib.lines.Line2D at 0x1eaf627b340>, <matplotlib.lines.Line2D at 0x1eaf6286e80>]
Now, instead of an empty list, the means key has a list of 3 matplotlib objects as its value. In addition, on each of the 3 boxplots, we see a new marker representing the mean value for each dataset.
Before extracting the exact numbers, let’s take a look at the main elements of a boxplot with the mean value (i.e. the keys of our dictionary), as well as the descriptive statistics which we’re going to obtain:
Now, let’s extract preliminary statistical values from each item of the dictionary using the get_ydata() method and try to understand their structure keeping in mind the picture above:
for key in bp:    print(f'{key}: {[item.get_ydata() for item in bp[key]]}\n')Output:whiskers: [array([ 32.27380667, -23.04513292]), array([ 70.59264691, 125.8497712 ]), array([75.68154245, -0.65215444]), array([131.88978143, 185.5131227 ]), array([63.74451462, 46.95092062]), array([76.33158616, 95.05980285])]caps: [array([-23.04513292, -23.04513292]), array([125.8497712, 125.8497712]), array([-0.65215444, -0.65215444]), array([185.5131227, 185.5131227]), array([46.95092062, 46.95092062]), array([95.05980285, 95.05980285])]boxes: [array([32.27380667, 32.27380667, 70.59264691, 70.59264691, 32.27380667]), array([ 75.68154245,  75.68154245, 131.88978143, 131.88978143,        75.68154245]), array([63.74451462, 63.74451462, 76.33158616, 76.33158616, 63.74451462])]medians: [array([52.64528282, 52.64528282]), array([100.43803244, 100.43803244]), array([70.10978367, 70.10978367])]fliers: [array([-33.79255]), array([-11.30137871, 221.23428449, 258.34410816]), array([ 42.09003593,  43.01638258,  39.4623562 , 103.21078756])]means: [array([52.23199524]), array([102.26099759]), array([69.8400676])]
We obtained lists of ndarrays for each key of the dictionary. What we actually want to fetch includes the following information for each dataset:
the median,
the mean,
the minimum without outliers,
the maximum without outliers,
the first quartile Q1,
the third quartile Q3,
the lower outliers,
the upper outliers.
Looking at the picture above, it should be noted that we can extract the minimum and maximum values either from caps or whiskers (we'll use the first approach), and analogically for Q1 and Q3: from boxes or whiskers (again, we'll use the first approach).
From the extracted raw values above, we see that for medians, we have 3 ndarrays of 2 identical items, while for means — 3 one-item ndarrays. For medians, each ndarray describes the line for the median value in the corresponding box (from left to right), for means — the point for the mean. To obtain the exact values of the median and mean for each boxplot, we have to select the first item of the corresponding ndarray:
medians = [item.get_ydata()[0] for item in bp['medians']]means = [item.get_ydata()[0] for item in bp['means']]print(f'Medians: {medians}\n'      f'Means:   {means}')Output:Medians: [52.64528282246805, 100.43803243566403, 70.10978366813102] Means:   [52.23199524458482, 102.26099759095463, 69.84006759933192]
Most probably, we may not want such precision of the results. So, here and elsewhere, let’s consider rounding the numbers to the first decimal point:
medians = [round(item.get_ydata()[0], 1) for item in bp['medians']]means = [round(item.get_ydata()[0], 1) for item in bp['means']]print(f'Medians: {medians}\n'      f'Means:   {means}')Output:Medians: [52.6, 100.4, 70.1]Means:   [52.2, 102.3, 69.8]
For the minimum and maximum values, we can use the ndarrays extracted from caps. Let's take a closer look at them:
[item.get_ydata() for item in bp['caps']]Output:[array([-23.04513292, -23.04513292]), array([125.8497712, 125.8497712]), array([-0.65215444, -0.65215444]), array([185.5131227, 185.5131227]), array([46.95092062, 46.95092062]), array([95.05980285, 95.05980285])]
The ndarrays of 2 identical items each go in this order: minimum for the 1st (the leftmost) boxplot, maximum for the 1st boxplot, minimum for the 2nd boxplot, etc. Hence, to obtain the minimum values for all the boxplots, we have to use list slicing to get the odd ndarrays (and selecting the first item from each), to obtain the maximum values — the even ones. Also in this case, for convenience, we’ll apply rounding to the first decimal:
minimums = [round(item.get_ydata()[0], 1) for item in bp['caps']][::2]maximums = [round(item.get_ydata()[0], 1) for item in bp['caps']][1::2]print(f'Minimums: {minimums}\n'      f'Maximums: {maximums}')Output:Minimums: [-23.0, -0.7, 47.0]Maximums: [125.8, 185.5, 95.1]
Let’s extract the first and third quartiles from boxes. To remind the raw data of boxes:
[item.get_ydata() for item in bp['boxes']]Output:[array([32.27380667, 32.27380667, 70.59264691, 70.59264691, 32.27380667]), array([ 75.68154245,  75.68154245, 131.88978143, 131.88978143,         75.68154245]), array([63.74451462, 63.74451462, 76.33158616, 76.33158616, 63.74451462])]
Looking at the pattern of each ndarray (representing each box), we can notice that the boxplot polygons (rectangles) were drawn starting from the minimum on the left going to the right and, in the end, were closed on the minimum again. Practically, what we need here is to extract the minimum value from each box to obtain Q1 and the maximum value to obtain Q3:
q1 = [round(min(item.get_ydata()), 1) for item in bp['boxes']]q3 = [round(max(item.get_ydata()), 1) for item in bp['boxes']]print(f'Q1: {q1}\n'      f'Q3: {q3}')Output:Q1: [32.3, 75.7, 63.7]Q3: [70.6, 131.9, 76.3]
We can find the outlier values in the dictionary key fliers:
[item.get_ydata() for item in bp['fliers']]Output:[array([-33.79255]), array([-11.30137871, 221.23428449, 258.34410816]), array([ 42.09003593,  43.01638258,  39.4623562 , 103.21078756])]
For now, they are grouped by a boxplot. If we want to have 2 separate lists for lower and upper outliers, we can apply the following code:
fliers = [item.get_ydata() for item in bp['fliers']]lower_outliers = []upper_outliers = []for i in range(len(fliers)):    lower_outliers_by_box = []    upper_outliers_by_box = []    for outlier in fliers[i]:        if outlier < q1[i]:            lower_outliers_by_box.append(round(outlier, 1))        else:            upper_outliers_by_box.append(round(outlier, 1))    lower_outliers.append(lower_outliers_by_box)    upper_outliers.append(upper_outliers_by_box)print(f'Lower outliers: {lower_outliers}\n'      f'Upper outliers: {upper_outliers}')Output:Lower outliers: [[-33.8], [-11.3], [42.1, 43.0, 39.5]] Upper outliers: [[], [221.2, 258.3], [103.2]]
Since the 1st boxplot doesn’t have any upper outliers, we obtained an empty list for it.
Now, let’s summarize in an elegant way all the descriptive statistics that we extracted so from for each dataset. Even though the code below looks a bit scary, the only thing we’ll have to update every time we use a new set of categories (datasets) is the two lines of code marked with a lateral comment # to be updated (and, of course, we'll have to remove the case-specific section for creating datasets). Optionally, we can consider updating rounding the results (the round() method instances) that is currently set to the first decimal point:
# Gather all the previous codeimport matplotlib.pyplot as pltimport numpy as np#--------------------------------------# Creating datasetsnp.random.seed(1)data_1 = np.random.normal(50, 30, 300)data_2 = np.random.normal(100, 40, 300)data_3 = np.random.normal(70, 10, 300)#--------------------------------------data = [data_1, data_2, data_3]  # to be updatedbp = plt.boxplot(data, showmeans=True)medians = [round(item.get_ydata()[0], 1) for item in bp['medians']]means = [round(item.get_ydata()[0], 1) for item in bp['means']]minimums = [round(item.get_ydata()[0], 1) for item in bp['caps']][::2]maximums = [round(item.get_ydata()[0], 1) for item in bp['caps']][1::2]q1 = [round(min(item.get_ydata()), 1) for item in bp['boxes']]q3 = [round(max(item.get_ydata()), 1) for item in bp['boxes']]fliers = [item.get_ydata() for item in bp['fliers']]lower_outliers = []upper_outliers = []for i in range(len(fliers)):    lower_outliers_by_box = []    upper_outliers_by_box = []    for outlier in fliers[i]:        if outlier < q1[i]:            lower_outliers_by_box.append(round(outlier, 1))        else:            upper_outliers_by_box.append(round(outlier, 1))    lower_outliers.append(lower_outliers_by_box)    upper_outliers.append(upper_outliers_by_box)        # New codestats = [medians, means, minimums, maximums, q1, q3, lower_outliers, upper_outliers]stats_names = ['Median', 'Mean', 'Minimum', 'Maximum', 'Q1', 'Q3', 'Lower outliers', 'Upper outliers']categories = ['DATASET 1', 'DATASET 2', 'DATASET 3'] # to be updatedfor i in range(len(categories)):    print(f'\033[1m{categories[i]}\033[0m')    for j in range(len(stats)):        print(f'{stats_names[j]}: {stats[j][i]}')    print('\n')Output:DATASET 1Median: 52.6Mean: 52.2Minimum: -23.0Maximum: 125.8Q1: 32.3Q3: 70.6Lower outliers: [-33.8]Upper outliers: []DATASET 2Median: 100.4Mean: 102.3Minimum: -0.7Maximum: 185.5Q1: 75.7Q3: 131.9Lower outliers: [-11.3]Upper outliers: [221.2, 258.3]DATASET 3Median: 70.1Mean: 69.8Minimum: 47.0Maximum: 95.1Q1: 63.7Q3: 76.3Lower outliers: [42.1, 43.0, 39.5]Upper outliers: [103.2]
In this article, we’ve explored how to extract and print out the exact values of the descriptive statistics of a dataset from a boxplot created in the matplotlib library. Such statistical information can include the median, the mean, the minimum and maximum values without outliers, the first and third quartile, the lower and upper outliers. In some cases, this precise data can be a valuable supplement to the visual information from a boxplot itself.
Thanks for reading!
You can find interesting also these articles: