箱线图叠加抖动散点(分组比较)
GAL-0002
用箱线图概括各组的中位数与四分位范围,再叠加抖动的原始数据点,让读者同时看到摘要统计和样本量、离散与离群情况。适合两因素分组比较。
图与代码

code/R/main.R
suppressPackageStartupMessages(library(ggplot2))
d <- read.csv("data/toothgrowth.csv", encoding = "UTF-8")
d$dose <- factor(d$dose, levels = c(0.5, 1, 2))
d$supp <- factor(d$supp, levels = c("OJ", "VC"))
p <- ggplot(d, aes(x = dose, y = len, fill = supp, colour = supp)) +
geom_boxplot(aes(colour = NULL), alpha = 0.55, outlier.shape = NA,
width = 0.7, linewidth = 0.5, colour = "grey20",
position = position_dodge(width = 0.8)) +
geom_point(position = position_jitterdodge(jitter.width = 0.15, jitter.height = 0,
dodge.width = 0.8, seed = 42),
size = 1.6, alpha = 0.9, shape = 21, colour = "grey20", stroke = 0.3) +
scale_fill_manual(values = c(OJ = "#66C2A5", VC = "#FC8D62"),
labels = c("Orange juice", "Ascorbic acid")) +
labs(x = "Dose (mg/day)", y = "Tooth length", fill = "Supplement") +
theme_classic(base_size = 12) +
theme(legend.position = "top")
ggsave("outputs/figure_r.png", p, width = 6, height = 4.5, dpi = 300, bg = "white")
code/py/main.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scienceplots # noqa: F401
from matplotlib.patches import Patch
plt.style.use(["science", "no-latex"])
plt.rcParams.update({"font.family": "sans-serif", "font.size": 11}) # 与 R 版(theme_classic 无衬线)统一
d = pd.read_csv("data/toothgrowth.csv")
doses = [0.5, 1.0, 2.0]
supps = ["OJ", "VC"]
labels = {"OJ": "Orange juice", "VC": "Ascorbic acid"}
colors = {"OJ": "#66C2A5", "VC": "#FC8D62"}
dodge, width = 0.8, 0.7
rng = np.random.default_rng(42)
fig, ax = plt.subplots(figsize=(6, 4.5))
for i, dose in enumerate(doses):
for j, s in enumerate(supps):
y = d.loc[(d["dose"] == dose) & (d["supp"] == s), "len"].to_numpy()
x0 = i + (j - 0.5) * dodge / 2
bp = ax.boxplot(y, positions=[x0], widths=width / 2, patch_artist=True, showfliers=False,
medianprops=dict(color="grey", lw=1.2), boxprops=dict(edgecolor="#333333", lw=0.8),
whiskerprops=dict(color="#333333", lw=0.8), capprops=dict(visible=False))
bp["boxes"][0].set_facecolor(colors[s])
bp["boxes"][0].set_alpha(0.55)
bp["medians"][0].set_color("#333333")
ax.scatter(x0 + rng.uniform(-0.15, 0.15, len(y)) * 0.25, y, s=16, color=colors[s],
edgecolor="#333333", linewidth=0.4, zorder=3)
ax.set_xticks(range(3))
ax.set_xticklabels(["0.5", "1", "2"])
ax.set_xlabel("Dose (mg/day)")
ax.set_ylabel("Tooth length")
ax.legend(handles=[Patch(facecolor=colors[s], edgecolor="#333333", label=labels[s]) for s in supps],
title="Supplement", loc="lower center", bbox_to_anchor=(0.5, 1.0), ncol=2, frameon=False) # 与 R 版一样放在图上方,不压数据
ax.tick_params(axis="x", which="minor", bottom=False, top=False)
fig.savefig("outputs/figure_py.png", dpi=300, facecolor="white")什么时候用
- 每组样本量不大(几十个以内),希望把原始数据点一并展示
- 比较多个组别的中位数和离散程度
- 需要在同一图中展示两个分组因素(如剂量 × 处理方式)
常见误用
- 抖动只是为了避免点重叠,横向位置没有数值含义;固定随机种子才能复现点的位置
- 箱线图本身不展示显著性;图上若加星号,需另行说明所用检验和多重比较校正方法
- 样本量很小(如每组 10 个)时,四分位数和须的位置不稳定,不宜过度解读箱体形状
- 须线通常为 1.5×IQR 以内的最远点,不同软件的分位数定义可能略有差异
数据与下载
数据:R 内置数据集 ToothGrowth(datasets 包)(R 内置数据集(随 R 分发,GPL-2 | GPL-3))。60 只豚鼠的成牙本质细胞长度(len,R 文档:负责牙齿生长的成牙本质细胞的长度),分为 3 个维生素 C 剂量(0.5、1、2 mg/天)和 2 种给药方式(橙汁 OJ、抗坏血酸 VC),每个组合 10 只。
下载包 sha256:bfc00971007d8dad4b8d9a4ca029d7425ac82340c6f8484cb1b5dc822a73582d