Folioevanzhou.org
← 返回 Tessera
R2 张图2026-08-12

venn

Two to three sets drawn as overlapping circles, with every region labelled by the number of members it holds.

示例数据
pbmc5k_donor4_cluster_de
配色
walter_white2
语言
R
成图预览另有 1 张在配方里
venn

Introduction

韦恩图回答的是成员资格的重叠:哪些元素只在 A,哪些同时在 A 和 B,哪些三个集合都有。每个元素只有"在/不在"两种状态,不带权重。

它最适合 2–3 个集合。集合一多,区域数按 (2^n-1) 增长:4 个集合已经有 15 个可能区域,5 个有 31 个。圆还能画出来,不等于人还能读出来。四个以上集合通常换 UpSet 图:交集组合排在共同基线上的柱子上,既能排序,也能精确比较。

读的时候按这个顺序:

  1. 先看集合总数。 它决定每个圆代表多大的候选池,也能暴露某个列表是不是因为阈值更宽而格外大。
  2. 再看最中间。 三圆共同区域是最严格的共识,但"更可信"要由研究设计决定,不能只凭它重叠次数最多。
  3. 沿着一个圆向外读。 Cluster 0 的圆里依次有它独有、与 Cluster 1 共有、与 Cluster 2 共有、三者共有;这四块相加才是 Cluster 0 的总数。
  4. 把区域数字当答案,把面积只当位置。 数字是集合运算算出来的;圆的大小和交叠面积通常只是布局。
  5. 最后核对分母。 百分比最容易误读,因为不同包可能用并集、集合总数或其他口径。没有明确分母时,优先标计数。

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
venn — classic
venn-classic
# 图上的区域互斥:Cluster 0 与 Cluster 1 相交那块写的是 487,
# 而 intersect() 给出的 629 还含着三簇共有的 142 个 —— 要减掉第三个集合
both_01 <- intersect(gene_sets[["Cluster 0"]], gene_sets[["Cluster 1"]])

length(both_01)
#> 629
length(setdiff(both_01, gene_sets[["Cluster 2"]]))
#> 487

2 · ggVennDiagram

换成 ggVennDiagram 之后,颜色的含义整个变了:它不再表示"哪个簇",而是表示"这一块里有多少基因",越深越多。簇的身份改由圆的轮廓和圆外面的名字来承担。

limits = c(0, 1800) 是手写死的,1800 刚好高过最大的那一块(Cluster 1 独有的 1711)。如果不写、让它自动取每张图各自的最大值,那么把两张韦恩图并排放的时候,同样深浅的青色在两张图里会代表完全不同的数量。

#| fig: intersection_count
#| fig-width: 8
#| fig-height: 6
library(ggVennDiagram)

p_count <- ggVennDiagram(
  x              = gene_sets,
  category.names = names(gene_sets),
  label          = "count",     # 同样只给计数
  label_alpha    = 0,           # 去掉标签的白色底衬,不在填充色上开洞
  label_geom     = "label",
  label_color    = "#262622",
  label_size     = 3.8,
  set_color      = "#353531",   # 填充已被计数占用,簇的身份改由轮廓和圆外的名字承担
  set_size       = 4.6,
  edge_size      = 0.7
) +
  scale_fill_gradientn(
    # 从 walter_white2 的天青向白色插值,不另加 recipe 外的类别色
    colours = colorRampPalette(c("white", set_colors[1]))(3),
    # 上界写死,不让它自动取每张图各自的最大值:否则两张韦恩图并排时,
    # 同样深浅的青色代表的数量不一样。1800 刚好高过最大区域(Cluster 1 独有的 1711)
    limits  = c(0, 1800),
    breaks  = c(0, 450, 900, 1350, 1800),
    name    = "Genes"
  ) +
  coord_fixed(clip = "off") +
  theme_void() +
  theme(
    plot.margin       = margin(18, 24, 18, 24),
    legend.position   = "bottom",
    legend.key.width  = grid::unit(34, "mm"),
    legend.key.height = grid::unit(3, "mm")
  )

p_count
venn — intersection_count
venn-intersection_count

Constraints

  • 圆的面积不编码集合大小。 Cluster 1(2710)和 Cluster 2(1302)画出来差不多大:经典韦恩图优先保证七个区域都放得下标签。要面积近似成比例,改用 Euler diagram。
  • 只有成员资格,没有量级。 log2 fold change、表达量、权重都进不了圆。

两个 recipe 怎么比

  1. 说清三个簇各有哪些 marker、又共享了哪些 → recipe 1。一簇一色,从头到尾不变,也最容易换一套定性色板。
  2. 区域太多,想先找出最大的那几块 → recipe 2。深浅直接把大区域推到眼前,代价是三个簇失去各自的颜色身份。
  3. 四个以上集合,或者需要给交集排序 → 换 UpSet 图。
  4. 需要圆的面积近似反映集合大小 → 用 area-proportional 的 Euler diagram,并在图注里说明面积只是近似。
运行环境6 个包 · 2026-09-02T13:29:15.671+0800

R version 4.5.1 (2025-06-13 ucrt) · x86_64-w64-mingw32

  • biopalette 0.2.2
  • dplyr 1.2.1
  • ggVennDiagram 1.5.7
  • ggplot2 4.0.3
  • ggvenn 0.1.19
  • grid 4.5.1