← 返回 Tessera
构成2 张图模拟三组差异表达基因(96 个唯一基因 · 7 个交集区域)2026-08-12

venn

两到三个集合各自有哪些成员,又共享了多少?

需要的输入
2–3 个向量,或一个以集合名命名的向量列表
示例数据
模拟三组差异表达基因(96 个唯一基因 · 7 个交集区域)
依赖
ggplot2 · ggvenn · ggVennDiagram
配色
walter_white2#5AB5BF #808C56 #F29E6D
成图预览另有 1 张在配方里
venn

什么时候用它

韦恩图回答的是成员资格的重叠:哪些元素只在 A,哪些同时在 A 和 B,哪些三个集合都有。典型输入是三份基因、蛋白、通路或病例 ID 列表;每个元素只有“在/不在”两种状态,不带权重。

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

别把它拿来表达下面几件事:

  • 集合之间的流动。 “筛选前 → 筛选后”是流向,应该画桑基图或流程图。
  • 每个元素的强弱。 log2 fold change、表达量、权重进不了圆的面积;韦恩图只知道成员在不在。
  • 精确比较集合大小。 常见实现里的圆不是按数量成比例画的。要比较 58 和 50 谁大多少,看数字或另画条形图。

怎么读

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

常见陷阱

重复值会把“行数”冒充“成员数”

集合的元素必须唯一。同一个基因在一张差异分析表里出现两行,集合意义上仍然只算一个成员。绘图前显式 unique(),不要指望每个包都以同一种方式替你去重。

NA 不是一个真实成员

三个列表里各有一个 NA,集合函数可能把它们当成共同值,也可能在内部丢掉;两种结果都不是你要表达的生物学重叠。先去掉缺失和空字符串,再画图。

标识符体系不一致,会制造“没有交集”

一份是 Ensembl ID,另一份是 gene symbol,即使指向同一批基因,字符串交集也会接近零。大小写、版本后缀(如 .12)、同义名和一对多映射都有同样问题。统一标识符是数据准备,不是绘图参数。

圆面积通常不按数量成比例

下面 RNA-seq 有 58 个成员,Proteomics 有 42 个,但三个圆的面积看起来接近。这是有意的:经典韦恩图优先保证七个区域放得下标签。若面积比例本身必须有意义,需要的是 area-proportional Euler diagram;即便如此,也要说明布局可能只是近似解。

半透明混色会生成色板之外的新颜色

经典画法的交集区由半透明填充叠出来。中间看到的橄榄、棕或灰不是新的类别色,而是图形设备混出的结果。因此图例只解释三个集合的原色,不要再给混色区域编一套颜色含义。后续把色板换进 Palette Lab 时,也要评价叠色后的可分辨性,而不只看三个原始色块。

百分比没有写分母,就没有可比性

“交集 20%”可能是占 A、占 B、占并集,甚至是包自己选的统计口径。图中直接放区域计数;确实需要比例时,在正文或图注里写出算式,例如 (|A \cap B| / |A \cup B|)。

配色

经典版使用 walter_white2 中的天青、橄榄绿和沙漠橙。它们负责标识三个无序集合,所以用定性色板;圆填充设为 0.46 的透明度,让边界重叠后仍能看见下层,但不把混出的颜色误当第四种编码。

轮廓统一用深灰而不是各圆同色描边。原因很实际:填充已经承担集合身份,深色轮廓只负责把区域边界切清楚;两套视觉信号不会互相竞争。

区域计数版的颜色含义完全不同:它不再给集合上色,而是从纸白到天青按交集成员数连续变化。此时集合身份由轮廓和名称承担。两种颜色语义不要混在同一张图里——否则读者无法判断一块颜色是在说“属于哪个集合”,还是“这里有多少成员”。

配方

方法绘图系统什么时候选它
Aggvenn()ggplot2 扩展集合身份是主角——每个圆固定一种颜色,适合两到三个集合的常规展示
BggVennDiagram()ggplot2 扩展交集大小是主角——区域按计数连续着色,集合只用轮廓和名称区分

数据准备

这里不用随机抽样,而是直接构造七个互斥区域。好处是每次运行都得到同一组数字,也能从生成代码一眼核对每块区域本来应该有多少成员。

library(ggplot2)

# 七个互斥区域:三个独有、三个两两共有、一个三者共有
only_rna     <- sprintf("RNA_%02d", 1:28)
only_atac    <- sprintf("ATAC_%02d", 1:22)
only_protein <- sprintf("PROT_%02d", 1:18)
rna_atac     <- sprintf("RA_%02d", 1:12)
rna_protein  <- sprintf("RP_%02d", 1:8)
atac_protein <- sprintf("AP_%02d", 1:6)
all_three    <- sprintf("ALL_%02d", 1:10)

gene_sets <- list(
  `RNA-seq` = c(only_rna, rna_atac, rna_protein, all_three),
  `ATAC-seq` = c(only_atac, rna_atac, atac_protein, all_three),
  Proteomics = c(only_protein, rna_protein, atac_protein, all_three)
)

# 真数据也先走同一遍清洗:去缺失、去空值、去重复
gene_sets <- lapply(gene_sets, function(x) unique(x[!is.na(x) & nzchar(x)]))

vapply(gene_sets, length, integer(1))
#>    RNA-seq    ATAC-seq  Proteomics
#>         58          50          42

length(unique(unlist(gene_sets)))
#> 96

方法 A · 集合身份着色

ggvenn 直接接受命名列表。这里只显示计数,不显示百分比:七个区域已经很拥挤,再叠一行百分比会让读者先解读版式,而不是解读集合。

#| fig: classic
#| fig-width: 8
#| fig-height: 6
library(ggvenn)

set_colors <- c("#5AB5BF", "#808C56", "#F29E6D")
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   = 5.2,
  text_color      = "#262622",
  text_size       = 4.5,
  padding         = 0.08
) +
  coord_fixed(clip = "off") +
  theme_void() +
  theme(
    plot.margin      = margin(18, 24, 18, 24),
    plot.background  = element_rect(fill = "#F4F3EE", colour = NA),
    panel.background = element_rect(fill = "#F4F3EE", colour = NA)
  )

p_classic
venn — classic
venn-classic

这张图里颜色只回答“这是哪个集合”。交集区的颜色是透明叠加的结果,区域里的数字才回答“这里有多少成员”。集合总数由 lengths(gene_sets) 算出并显式拼进名称,读者不用再手动把四块区域相加,也不会把总数维护成一份可能过期的手写文本。

方法 B · 交集大小着色

如果问题从“每个集合是谁”变成“哪个交集最大”,改用 ggVennDiagram。它把每个互斥区域的计数映射到填充色,三条集合边界则保持同一种深灰。

#| 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     = 4.2,
  set_color      = "#353531",
  set_size       = 5.2,
  edge_size      = 0.7
) +
  scale_fill_gradientn(
    colours = c("#F4F3EE", "#B9D9DC", "#5AB5BF"),
    limits  = c(0, 28),
    breaks  = c(0, 7, 14, 21, 28),
    name    = "Genes"
  ) +
  coord_fixed(clip = "off") +
  theme_void() +
  theme(
    legend.position  = "bottom",
    legend.key.width = grid::unit(34, "mm"),
    legend.key.height = grid::unit(3, "mm"),
    plot.margin      = margin(18, 24, 18, 24),
    plot.background  = element_rect(fill = "#F4F3EE", colour = NA),
    panel.background = element_rect(fill = "#F4F3EE", colour = NA)
  )

p_count
venn — intersection_count
venn-intersection_count

limits = c(0, 28) 在这里是同一张图内部的尺度:28 是最大的独有区域。若要把多张韦恩图并排比较,必须给它们统一 limits;让每张图各自自动取最大值,会导致同一种深青在不同图里代表完全不同的计数。

用集合运算核对数字

图不是校验器。定稿前至少把关键交集从原始列表直接算一遍;这也能输出后续富集分析真正要用的成员,而不是只留下图片上的数字。

rna_atac_only <- setdiff(
  intersect(gene_sets[["RNA-seq"]], gene_sets[["ATAC-seq"]]),
  gene_sets[["Proteomics"]]
)

shared_all <- Reduce(intersect, gene_sets)

length(rna_atac_only)
#> 12
length(shared_all)
#> 10
shared_all
#>  "ALL_01" "ALL_02" "ALL_03" "ALL_04" "ALL_05"
#>  "ALL_06" "ALL_07" "ALL_08" "ALL_09" "ALL_10"

两两 intersect(A, B) 包含三者共有的成员,所以这里得到的是 22,不是图上 A∩B 独有区域的 12;必须再 setdiff(..., C)。这是从图抄数字回分析代码时最常犯的集合语义错误。

导出

两种方法最终都是 ggplot 对象,直接交给同一套设备。色块、曲线和小字边缘都不适合 JPEG;网页用 PNG,投稿优先 PDF 或 TIFF。

#| eval: false
library(ragg)

ggsave(
  "venn_classic.png", p_classic,
  width = 8, height = 6, dpi = 300,
  device = ragg::agg_png, bg = "#F4F3EE"
)

ggsave(
  "venn_classic.pdf", p_classic,
  width = 8, height = 6,
  device = cairo_pdf, bg = "#F4F3EE"
)

两种方法怎么选

  1. 报告三组实验结果有哪些共同与独有成员 → 方法 A。集合身份沿整张图保持同一颜色,最符合常规阅读方式,也最适合替换不同定性色板。
  2. 交集区域很多,想先把最大的区域找出来 → 方法 B。连续深浅直接突出计数,但它牺牲了集合各自的颜色身份。
  3. 四个以上集合,或需要给交集排序 → 两种都不选,换 UpSet 图。
  4. 需要圆面积近似集合大小 → 使用明确声明 area-proportional 的 Euler diagram,并在图注说明面积是近似布局;不要把普通韦恩图改个圆大小就当成比例图。

evanverse::plot_venn() 把这两条路线封装成 classicgradient,并处理 2–4 个向量、标签选择、去重和返回集合列表。这里没有调用它:recipe 要把数据清洗、颜色语义和底层绘图参数完整摊开,方便逐项修改,也避免把一个便利封装变成复现所必需的依赖。

运行环境5 个包 · 2026-08-12T11:52:15.934+0800

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

  • ggplot2 4.0.3
  • ggvenn 0.1.19
  • ggVennDiagram 1.5.7
  • grid 4.5.1
  • ragg 1.5.0