將多個 boxplot 畫在同一張
(group boxplot) put several boxplots sharing the same x in the same figure
使用 matplotlib 的 boxplot ,將不同種但有相同分析指標的畫在一起,方便對比同一指標下不同種的差別。直接使用 boxplot 是沒辦法將不同資料組合在一起,但可以藉由指定位置跟寬度,來達成所需,最後可以畫出類似於下圖或封面圖的效果。除此之外,也會順帶介紹一些 boxplot 相關參數。

boxplot 參數介紹

上圖是將我們後面用到的相關參數表現出來:
- 左側是調整 boxplot 的各個部分 (flier, cap, whisker, box, media) 的影響範圍,並由 *props 所設置(如 flierprops, capprops 等)
- 下方 xtick 預設會是從 1 至 num of cols,每一個 column 產生一個 box
- width 預設是 0.5 且是整個 box 的寬度
- position 是中心點且預設為 xtick 上。
將多個 boxplot 畫在一起
雖然 matplotlib 並不直接讓我們能夠結合多個 boxplot,但藉由設定 width, position 可以將各個 box 排好,再藉由顏色來區分。

假如要將 n 個類別 (num_kind) 放在同一指標裡頭,用預設的 base position (1, 2, 3, ..., n)來畫的話,各個的間距是 1,避免兩邊衝突到,我們所畫的範圍 total width 這裡限縮為 0.9 。
那每一類別分配到的空間是 \( \frac{total\ width}{n} \),為避免各類別靠太近,就只用 \( \frac{total\ width}{n+1} \) 來畫 box (綠色),那他們之間的空間加總起來就也會有 \( \frac{total\ width}{n+1} \);但下一個的中心點距離上一個中心點還是 \( \frac{total\ width}{n} \) (藍色)。
初始位置的計算可以用 base position (紫色) 先扣除 total width 的一半,移到最左邊 (黃色),再加上\( \frac{total\ width}{n} \) 的一半,來移到第一個中心點 (紅色)。配合前面所講的距離,就可以依序算出第 k 類別 (k = 0, 1, ..., num_kind - 1)的中心點位置
\[ position_k = (base\_position - \frac{total\ width}{2} + \frac{total\ width}{2n}) + k \times \frac{total\ width}{n} \]
那當然這是我的一種算法,大家可以根據自己的喜好去調整位置以及寬度。
實作參考
引用相關函式庫
import numpy as np
import matplotlib.pyplot as plt
numpy 拿來產生資料,而 matplotlib 拿來畫 boxplot
生成資料
def generate_data(num_data, num_label, num_kind):
# we only use Cn as color, so we limit the num_kind
assert(num_kind < 9)
x_list = [np.random.rand(num_data, num_label) for i in range(num_kind)]
color_list = ['C' + str(i) for i in range(num_kind)]
label_list = ['label' + str(i) for i in range(num_label)]
return x_list, color_list, label_list
因為我們使用 Cn 作為顏色,所以只能用 C0 ~ C8,這裡加上檢查確保不會超出範圍,每組就生成出 \( num\_data \times num\_label \) 的資料。
畫圖
def multiple_boxplot(x_list, color_list, legend_list, label_list):
num_kind = len(x_list)
assert(num_kind == len(color_list))
num_label = len(label_list)
plt.rcParams.update({'font.size': 18})
fig, ax = plt.subplots(1, 1, figsize=(10, 10))
# the total width of one label (width <= diff of base_position)
total_width = 0.9
# the tick position i.e. the position for one boxplot
base_position = np.arange(num_label) + 1
# compute the beginning position for each num_label from 1 to num_label
position = base_position - total_width/2 + total_width/2/num_kind
# box_list stores the box color information for legend
box_list=[]
for i in np.arange(num_kind):
color = color_list[i]
bp = ax.boxplot(x_list[i],
# use n+1 not n for space between kind
widths=total_width/(num_kind+1),
positions=position + i * total_width/num_kind,
# patch_artist=True is necessary for the color
patch_artist=True,
boxprops=dict(facecolor=color, color=color, alpha=0.5),
capprops=dict(color=color),
whiskerprops=dict(color=color),
flierprops=dict(color=color, markeredgecolor=color))
# change the median color by adding medianprops=dict(color=c)
# add the boxes color to the list
box_list.append(bp["boxes"][0])
# set the legend
ax.legend(box_list, legend_list, framealpha=0.3)
ax.set_xticks(base_position)
ax.set_xticklabels(label_list)
# use white background
fig.patch.set_facecolor('w')
plt.show()
如果要著色的話,記得要將 patch_artist 設為 True,才能改變顏色。前面沒提到的變數有 num_label 用來記錄有幾種指標要畫 。 我個人是習慣將 median 保留原始顏色,但如果要改也是把 medianprops 加回去,並選擇自己要的顏色即可。使用 boxplot 回傳中的 ["boxes"][0]
當作圖例使用,含有邊跟面的顏色。最後記得要指定 xticks 跟相對應的 label,如果有改位置的話這裡可能需要更動。
展示
這裡偷懶,直接把顏色當圖例的名字使用
- 三個指標(num_label)、四種類別(num_kind)
x_list, color_list, label_list = generate_data(100, 3, 4)
multiple_boxplot(x_list, color_list, color_list, label_list)

- 四種指標、七種類別
x_list, color_list, label_list = generate_data(100, 4, 7)
multiple_boxplot(x_list, color_list, color_list, label_list)

參考資料
- https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.boxplot.html
- https://stackoverflow.com/questions/41997493/python-matplotlib-boxplot-color
- https://stackoverflow.com/questions/20365122/how-to-make-a-grouped-boxplot-graph-in-matplotlib
- https://stackoverflow.com/questions/16592222/matplotlib-group-boxplots