How to boxplot multiple dictionaries on the same plot

Viewed 85

For a change point detection task, I am testing my own algorithm against a baseline, and I would like to plot the results from the two algorithms on the same boxplot.

My results (F Score values) are stored in a dictionary where the keys are two parameters a and b (both with 4 distinct values):

resultsOwnAlgorithm = {'a1, b1': [0.8, 0.7, 0.6, ...], 'a1, b2': [...], ..., 'a2, b1': [...], ...}
resultsBaseline = {'a1, b1': [0.7, 0.6, ...], 'a1, b2': [...], ..., 'a2, b1': [...], ...}

For now, I have a function to plot them individually. I create 4 subplots where a is set and b is changing, see image (values are random, just to create an example image). The function looks like this:

def plotResults(results, keys, test):
    
    fig, axs = plt.subplots(2,2,figsize=(10,10))
    for ax in axs.flat:
        ax.set_ylim(0,1)
        ax.set_xticks(range(len(abrs)))
        ax.set_xticklabels(abrs)
    
    count = 0
    for i in (0,1):
        for j in (0,1):
            axs[i,j].set_title(str(test) + ', mean shift: ' + str(keys[count][0][0:2]).strip('x,') + ', iters=' + str(iterations), fontweight ="bold")
            l = keys[count]
            k = {k:results[k] for k in l if k in results}
            label, data = k.keys(), k.values()
            axs[i,j].boxplot(data,showfliers=False,patch_artist=True)
            axs[i,j].set_xticks(range(1, len(label) + 1))
            axs[i,j].set_xticklabels(label)
            count+=1

where results is either resultsOwnAlgorithm or resultsBaseline, keys is the dicitonary keys, so the different combinations of a and b, and test is just used to put which algorithm is being plotted in the title.

My question is: how do I plot them side by side on the same plot?

see image

2 Answers

There's a few errors in your plotting function, so I could get it to work without making great assumptions, like what abrs is and what iterations is. You should fix them before continuing with your work as this function is getting them likely from the global scope (assuming a jupyter notebook) and that will lead to bugs later on, as I've painfully experienced before.

Anyway, your problem can be tackled first by adapting your code to use seaborn. Check the example here, "Draw a boxplot with nested grouping by two categorical variables".

The method that can be more easily modified to fit your usecase is this: Generate a set of x values that will be associated with each boxplot group. Then, add a shift to the left or right depending on where you want to place this boxplot. Then you have to fix the ticks and so on, but you already know how to do that. Here's an example that maintains as much as possible of your structure.

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
resultsOwnAlgorithm = {'a1, b1': np.random.normal(scale=2, size=20), 'a2, b2': np.random.normal(scale=1.5, size=20)}
resultsBaseline = {'a1, b1': np.random.normal(scale=2, size=20), 'a2, b2': np.random.normal(scale=1.5, size=20)}
x_vals = np.arange(0, len(resultsOwnAlgorithm))
xs = {key:val for key, val in zip(resultsOwnAlgorithm.keys(), x_vals)}
shift = 0.1

fig, ax = plt.subplots()
for key in resultsOwnAlgorithm.keys():
    ax.boxplot(resultsOwnAlgorithm[key], positions=[xs[key] - shift], boxprops=dict(color='r'))
    ax.boxplot(resultsBaseline[key], positions=[xs[key] + shift], boxprops=dict(color='b'))

ax.set_xticks(x_vals)
ax.set_xticklabels(resultsOwnAlgorithm.keys())

This yields the following graph: enter image description here

  • The easiest solution is probably to combine all of the dictionaries into a single pands.DataFrame. This will make the data easy to analyze, and plot.
    1. Iterate through a list of dictionaries, zipped to a string that will be used to identify where the data came from.
    2. Create the dataframe.
    3. Add a new column to identify the data.
    4. Append the dataframe to a list.
    5. Combine the list of DataFrames with pd.concat, and reset the index.
    6. Reshape the DataFrame into a long form with pd.DataFrame.melt
  • Seaborn is a high-level api for matplotlib, and easily plots long form data and separates the groups by the hue parameter.
    • Use the figure-level plot sns.catplot with kind='box', or use the axes-level plot sns.boxplot.
    • Seaborn scales more easily than using multiple calls to matplotlib.axes.Axes.boxplot, where positions= must be specified for each extra group of boxplots.
  • Tested in python 3.10, pandas 1.4.2, matplotlib 3.5.1, seaborn 0.11.2
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt

# create sample dictionaries
np.random.seed(2022)
custom = {f'a{v}, b{v}': np.random.normal(scale=v, size=100) for v in range(1, 5)}
baseline = {f'a{i}, b{i}': np.random.normal(scale=v, size=100) for i, v in enumerate(np.arange(1.5, 5.5), 1)}

# create and shape dataframe
dfs = list()
for d, _id in zip([resultsBaseline, resultsOwnAlgorithm], ['baseline', 'custom']):
    df = pd.DataFrame(d)
    df['Algorithm'] = _id
    dfs.append(df)
dfs = pd.concat(dfs).reset_index(drop=True)
dfm = dfs.melt(id_vars='Algorithm', var_name='Parameters', value_name='Score')

# plot
g = sns.catplot(kind='box', data=dfm, x='Parameters', y='Score', hue='Algorithm', height=6, aspect=2)
plt.show

enter image description here

DataFrame Views

  • dfs.head()
     a1, b1    a2, b2    a3, b3    a4, b4 Algorithm
0  0.834463 -1.092923  4.875117 -4.946214  baseline
1  1.338891  0.225008 -0.305499  0.570333  baseline
2  0.261615  2.128844  2.194177  0.494803  baseline
3  0.273740 -2.395624 -3.495572  0.006312  baseline
4 -0.997368  0.984808 -3.956302  0.206667  baseline
  • dfm.head()
  Algorithm Parameters     Score
0  baseline     a1, b1  0.834463
1  baseline     a1, b1  1.338891
2  baseline     a1, b1  0.261615
3  baseline     a1, b1  0.273740
4  baseline     a1, b1 -0.997368
Related