將多個 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)

參考資料