Introduction
韦恩图回答的是成员资格的重叠:哪些元素只在 A,哪些同时在 A 和 B,哪些三个集合都有。每个元素只有"在/不在"两种状态,不带权重。
它最适合 2–3 个集合。集合一多,区域数按 (2^n-1) 增长:4 个集合已经有 15 个可能区域,5 个有 31 个。圆还能画出来,不等于人还能读出来。四个以上集合通常换 UpSet 图:交集组合排在共同基线上的柱子上,既能排序,也能精确比较。
读的时候按这个顺序:
- 先看集合总数。 它决定每个圆代表多大的候选池,也能暴露某个列表是不是因为阈值更宽而格外大。
- 再看最中间。 三圆共同区域是最严格的共识,但"更可信"要由研究设计决定,不能只凭它重叠次数最多。
- 沿着一个圆向外读。 Cluster 0 的圆里依次有它独有、与 Cluster 1 共有、与 Cluster 2 共有、三者共有;这四块相加才是 Cluster 0 的总数。
- 把区域数字当答案,把面积只当位置。 数字是集合运算算出来的;圆的大小和交叠面积通常只是布局。
- 最后核对分母。 百分比最容易误读,因为不同包可能用并集、集合总数或其他口径。没有明确分母时,优先标计数。
Example Data
韦恩图要的输入很窄:2–3 个成员列表,元素唯一、只有"在/不在"。任何带权重的列(log2 fold change、表达量、P 值)都进不了圆,只能先拿来筛选。
pbmc5k_donor4_cluster_de 是一张差异表达结果表,一行是一个基因在一次比较里的结果,三次比较共享同一个参考组 Cluster 3。它离韦恩图的输入还差一步:先按阈值把它筛成"上调 marker",再按比较拆成三个基因列表。
library(ggplot2)
library(dplyr)
library(biopalette)
# 公开地址,和数据集页上「下载 CSV」给的是同一个 —— 不写仓库相对路径:
# 那个目录不进仓库,读者 clone 下来也没有这个文件,这段代码就跑不了
de <- read.csv("https://assets.evanzhou.org/tessera/csv/pbmc5k_donor4_cluster_de.csv")
# 三个比较共享参考组,所以方向可比:avg_log2fc > 0 一律表示相对 Cluster 3 高表达
# 阈值决定每个圆有多大,换一组数字整张图就变了 —— 它属于分析,不属于绘图
up <- de |>
filter(
p_adj < 0.05, # 多重检验校正后显著
avg_log2fc >= 0.25, # 只留上调的一侧
pct_target >= 0.10 # 目标簇里至少 10% 的细胞表达,滤掉零星阳性的基因
)
gene_sets <- split(up$gene, up$comparison)
# cluster_0_vs_3 -> Cluster 0;参考组 Cluster 3 三个比较都一样,名字里不再重复
names(gene_sets) <- sub("_vs_3", "", sub("cluster_", "Cluster ", names(gene_sets)))
# 去重并清掉 NA / 空串:重复行不会变成两个成员,而缺失值在有的包里
# 被算成"三个集合共有",在有的包里被静默丢掉
gene_sets <- lapply(gene_sets, function(x) unique(x[!is.na(x) & nzchar(x)]))
vapply(gene_sets, length, integer(1))
#> Cluster 0 Cluster 1 Cluster 2
#> 1430 2710 1302
length(unique(unlist(gene_sets)))
#> 4209
Palettes
三个圆用 walter_white2 里的天青、橄榄绿和沙漠橙,一个圆一个色——它们标的是 comparison 那一列的三个取值,也就是 Cluster 0、Cluster 1、Cluster 2 三个目标簇。图例里就是这三个名字。
交集区的颜色是三个填充半透明叠出来的,中间看到的橄榄、棕或灰不是第四种类别色,只是图形设备混色的结果。所以图例只解释三个原色。
轮廓统一深灰,不跟着各圆同色:填充已经承担集合身份,轮廓只负责把七个区域的边界切清楚。
# 取天青、橄榄绿、沙漠橙;跳过第 3 色房车米(#D1BE9E)——它太浅,
# 压到 46% 透明度后和纸白背景分不开,更不用说再叠一层
set_colors <- get_palette("walter_white2", type = "qualitative")[c(1, 2, 4)]
Recipe
| No. | Method | Input Data | Palettes |
|---|---|---|---|
| 1 | ggvenn |
gene_sets |
set_colors |
| 2 | ggVennDiagram |
gene_sets |
set_colors[1] |
1 · ggvenn
ggvenn 要的输入就是上面那个 gene_sets:一个带名字的列表,名字会变成圆旁边的标签,每一项是那个簇的基因名。
这里只在区域里写数量,不写百分比。七个区域本来就挤,再塞一行百分比,读者得先看懂版式才能看到数字。
图上颜色只说明"这是哪个簇"。某一块里到底有多少基因,看那块上的数字,不要试图从颜色深浅去读。
#| fig: classic
#| fig-width: 8
#| fig-height: 6
library(ggvenn)
# 集合总数写进圆外的标签:图上只有互斥区域,读者否则要自己把四块加起来
plot_sets <- gene_sets
names(plot_sets) <- sprintf("%s (n = %d)", names(gene_sets), lengths(gene_sets))
p_classic <- ggvenn(
data = plot_sets,
columns = names(plot_sets),
show_counts = TRUE,
show_percentage = FALSE, # 七个区域本来就挤,再叠一行百分比会先挤掉数字
fill_color = set_colors,
fill_alpha = 0.46, # 三层叠加后最中间那块仍要能看出是混色,不是纯黑
stroke_color = "#353531", # 统一深灰,不跟随各圆同色
stroke_size = 0.8,
set_name_color = "#262622",
set_name_size = 4.6,
text_color = "#262622",
text_size = 4.2,
padding = 0.08 # 给圆外的集合名留白,带 (n = ...) 的长标签才不会被裁
) +
# 圆必须是正圆:纵横比一变,本来就只是布局的面积会更容易被误读成数量
coord_fixed(clip = "off") +
theme_void() +
theme(plot.margin = margin(18, 24, 18, 24))
p_classic
