做数据可视化这几年,我越来越发现一个现象:一旦集合数量超过四个,维恩图基本就变成了“一团乱麻”。五个集合的维恩图还能勉强看,六个以上简直是在考验人类的视觉极限。直到我接触到Upset plot,这个问题才算真正解决。它用条形图加点阵的组合,把多个集合的交集关系表达得清清楚楚,既保留了细节,又避免了维恩图那种互相重叠的视觉灾难。
R语言里做Upset plot主要有两条路:一是专门的UpSetR包,二是ComplexHeatmap包里的UpSet()函数。这两条路我都走过,各有优劣。这篇博文就围绕Upset plot的R实现,把从数据准备、图形绘制、参数调优到问题排查的完整过程都梳理一遍,希望能给正在跟集合数据较劲的朋友一些参考。
1. 为什么不用维恩图:Upset plot的核心逻辑
先聊一个基础问题:为什么集合交集可视化偏偏要选Upset plot?
1.1 维恩图的三个硬伤
维恩图适合展示的集合数量非常有限。三个集合的维恩图有七个区域,人眼还能分辨;四个集合就已经有十五个区域,很多读者已经开始犯迷糊;五个集合三十一个区域,基本就变成了“数格子”游戏。
更麻烦的是,维恩图里那些闭合曲线的形状和相对位置会直接影响区域面积的含义,但大多数绘图工具生成的维恩图,区域面积并不与集合元素数量成正比,很容易让读者产生“这个交集很大”的错觉。此外,当某个交集为空集时,维恩图仍然会画出对应的区域,这种“空白格子”会干扰对实际有意义交集的关注。
1.2 Upset plot的替代思路
Upset plot的设计思路非常巧妙:把集合关系拆成两个维度来展示。左侧(或上方)的条形图展示每个集合的大小,中间的点阵图展示哪些集合参与了某个交集,顶部(或右侧)的条形图展示各个交集的大小。
举个例子,假设你有“看过电影A”“看过电影B”“看过电影C”三个集合,Upset plot会用点阵告诉你“同时看过A和C的人有23个”“只看过B的人有45个”,所有信息一目了然,完全不需要那些交织在一起的曲线。
这种设计有三个明显优势:第一,集合数量不受限制,理论上几十个集合都能画;第二,交集大小用条形图高度表示,视觉上非常直观,不存在面积误导;第三,可以方便地标注出哪些交集是空的,哪些是重点关注的。
1.3 什么场景下应该选Upset plot
根据我的实际经验,当你有以下需求时,Upset plot基本就是最优解:
- 超过4个集合的交集分析,维恩图已经无法清晰表达的场景
- 需要精确比较多个交集之间的大小关系,而不是只看大致分布
- 需要在同一张图里同时展示集合的并集、交集信息,且后续要做分组对比
- 做RNA-seq、单细胞测序等组学数据分析时,多个差异基因列表之间的重叠关系展示
Upset plot虽然不能百分之百取代维恩图,但在处理多集合数据时确实比维恩图靠谱得多。接下来直接进入正题,看看在R里具体怎么实现。
2. R实现方案选型:UpSetR包与ComplexHeatmap包
R语言里能画Upset plot的包有不少,但主流的就两个:UpSetR和ComplexHeatmap。这两个方案我都仔细用过,先做一个整体评述,方便你根据自己的需求选型。
2.1 UpSetR包:简单直接,上手最快
UpSetR包是专门做Upset plot的,函数调用非常简单,基本一行代码就能出图。它支持从list、data.frame等格式直接输入,不需要手动转换数据格式,对新手特别友好。
它的核心函数就一个:upset()。传入数据、设定集合列、指定排序方式,图就出来了。如果你只是想快速看一眼集合之间的交集关系,不需要太多定制,UpSetR是最省心的选择。
2.2 ComplexHeatmap包的UpSet()函数:功能强大,扩展性好
ComplexHeatmap包本身是画热图的,它内置的UpSet()函数是用来配合热图展示“关注度矩阵”的。与UpSetR相比,ComplexHeatmap方案的定制能力更强,支持把upset plot和热图组合在同一张图上,而且对OncoPrint那种带有临床信息的热图场景做了很好的衔接。
但代价是代码复杂度更高。你需要自己构建组合矩阵,手动设置多个参数,不太适合“随手画个图”的场景。如果你需要把交集信息跟热图关联起来展示,或者需要做高度定制化出版级图形,ComplexHeatmap是更好的选择。
2.3 两个方案的核心对比
我用一个表格把两家的差异梳理清楚:
| 对比维度 | UpSetR | ComplexHeatmap |
|---|---|---|
| 上手难度 | 低,一行函数出图 | 中高,需理解组合矩阵逻辑 |
| 输入格式 | list、data.frame均可 | 需要矩阵或列表结构 |
| 定制能力 | 中等,参数较多但边界清晰 | 强,可与热图联合作图 |
| 对分组/样本信息的支持 | 有限,需额外处理 | 天然支持,可关联临床信息 |
| 组合其他图表 | 需要grid布局或拼图 | 原生支持,组合灵活 |
| 出版级图形 | 需微调样式 | 更容易达到 |
我的建议是:常规分析用UpSetR,快速出图看结果;如果需要把upset plot跟热图放一起,或者需要精细控制每个元素,选ComplexHeatmap。下面分别把两条路的完整实现过程都走一遍。
3. 用UpSetR画Upset plot:从数据准备到参数详解
UpSetR包的使用虽然简单,但很多细节如果不注意,画出来的图要么信息缺失,要么丑得没法看。这一节我按实际操作顺序,把每一步的关键点讲清楚。
3.1 安装与数据格式准备
安装UpSetR没有什么特别之处,直接从CRAN拉取即可:
install.packages("UpSetR") library(UpSetR)重点在于数据格式。UpSetR支持三种输入格式:
第一种是list格式。每个元素是一个向量,向量里存放的是该集合的元素标识。比如做基因集分析时,每个集合就是一个基因名向量:
gene_sets <- list( SetA = c("gene1", "gene2", "gene3", "gene4"), SetB = c("gene2", "gene3", "gene5", "gene6"), SetC = c("gene1", "gene3", "gene5", "gene7") )第二种是data.frame格式。每一列是一个集合,每一行是一个元素,表中用0和1表示该元素是否属于该集合。这种格式也叫二进制矩阵,是UpSetR最常用的输入形式:
df <- data.frame( SetA = c(1, 1, 1, 1, 0, 0, 0), SetB = c(0, 1, 1, 0, 1, 1, 0), SetC = c(1, 0, 1, 0, 1, 0, 1) ) rownames(df) <- paste0("item", 1:7)第三种是vector格式。把一个元素的集合归属信息用压缩的字符串表示,比如每个元素用二进制字符串“101”表示属于SetA和SetC。这种方式适合数据量特别大的场景,可以节省内存空间。
我实际用下来,绝大多数场景选择list格式或data.frame格式就够了。如果你手头数据已经在Excel里整理成“样本x基因”的表达矩阵,那么把它转成0/1的二进制矩阵是很快的。
3.2 一行代码出基础图
数据准备好之后,最基本的Upset plot用一行代码就能画出来:
upset(df, sets = c("SetA", "SetB", "SetC"))这里df就是那个二进制矩阵,sets参数指定哪些列作为集合列。如果你不在sets里指定,UpSetR会默认取所有列作为集合。
画出来的图包含三部分:左侧的集合大小条形图、中间的交集点阵、上方的交集大小条形图。默认情况下,点阵中一个点代表一个集合参与了该交集,多个点用连线连起来,表示这些集合共同构成了一个特定的交集组合。
用list格式数据时,调用方式几乎一样:
upset(fromList(gene_sets))fromList()函数负责把list格式转换成UpSetR需要的内部结构。注意:如果你直接用list格式传入upset()函数,需要先做这步转换,否则可能会报错。
3.3 常用参数逐个拆解
基本的图能出来以后,你就会发现默认样式有很多地方不够用。这里把最常用的参数一个个讲清楚:
sets参数:指定需要展示哪些集合。默认是全选。如果集合太多,建议只保留最关心的几组,避免点阵图变得过于密集。
order.by参数:控制条形图的排序方式。可以选择按交集大小排序(order.by = "freq"),按集合大小排序(order.by = "size"),或者保持输入顺序(order.by = "degree")。实际分析时,我更习惯按交集大小排序,这样一眼就能看出重点交集。
nintersects参数:控制展示多少个交集。默认展示所有非空交集,但集合多的时候交集组合爆炸式增长,全部展示会导致图形拥挤。我通常设置为30左右。
upset(df, sets = c("SetA", "SetB", "SetC"), order.by = "freq", nintersects = 20)sets.bar.color:控制左侧集合大小条形图的颜色。默认是深蓝色,当你需要区分不同分组时,可以通过传递一个颜色向量来实现。
main.bar.color:控制上方交集条形图的颜色。通常保持默认即可。
point.size和line.size:控制点阵图中点的大小和连线的粗细。当数据点密集时,适当调大point.size让点更醒目;如果要减少视觉噪音,则调小。
empty.intersections:是否显示空交集。默认是关闭的,如果你想看到所有可能的组合情况,可以开启这个参数。
upset(df, sets = c("SetA", "SetB", "SetC"), empty.intersections = TRUE)有一个参数我要专门强调:queries参数。它允许你给特定的交集添加额外标注或特殊颜色,比如标出某个基因集合是否带有特定注释信息。这个功能非常实用,尤其是在做富集分析时,需要突出某个通路相关的交集。
upset(df, sets = c("SetA", "SetB", "SetC"), queries = list( list(query = elements, params = list("SetA", "gene3"), color = "red", active = TRUE) ))3.4 调节布局与美化
UpSetR默认的布局是集合条形图在左侧,交集条形图在上方。但有时你希望把集合条形图放在顶部,点阵图放在中间。这可以通过参数来调整:
upset(df, sets = c("SetA", "SetB", "SetC"), mb.ratio = c(0.35, 0.65))mb.ratio控制上方条形图和点阵图之间的高度比例。默认值根据不同版本略有差异。我一般会调到0.3:0.7,让点阵区域更宽敞一些。
另外,UpSetR基于grid绘图系统,你可以直接用grid.text()在图上追加文字标签,用grid.lines()追加辅助线。比如在某个关键交集上方加一条注释线,标注统计检验的p值,这种操作完全可行。
upset(df, sets = c("SetA", "SetB", "SetC")) grid.text("关键交集", x = 0.75, y = 0.65, gp = gpar(fontsize = 14, col = "red"))3.5 一个完整的实际案例
前面讲的都是离散的知识点,我在这里放一个相对完整的例子,大家可以直接复制运行。
假设你手头有一个表达矩阵,记录了10个样本中某些基因是否发生了突变(1表示突变,0表示未突变),你想看看哪些基因组合经常同时发生突变。
library(UpSetR) # 模拟突变矩阵 set.seed(42) mut_matrix <- as.data.frame(matrix( rbinom(100, size = 1, prob = 0.3), nrow = 10, ncol = 10 )) colnames(mut_matrix) <- paste0("Gene", 1:10) rownames(mut_matrix) <- paste0("Sample", 1:10) # 绘制Upset plot upset(mut_matrix, sets = colnames(mut_matrix), order.by = "freq", nintersects = 20, main.bar.color = "#2c3e50", sets.bar.color = "#e74c3c", point.size = 3.5, line.size = 1.2, mb.ratio = c(0.3, 0.7))运行这段代码,你会看到一张清晰的Upset plot:左侧是每个基因的突变样本数,上方是各个基因组合同时突变的样本数,点阵图清晰标出每个交集由哪些基因组成。如果发现某个基因组合频繁共突变,下游分析就可以重点关注这个组合的功能关联。
4. 用ComplexHeatmap实现Upset plot:进阶定制方案
如果你已经用UpSetR画过图,但发现需要和热图组合展示,或者要控制更复杂的样式,这时候就该轮到ComplexHeatmap出场了。
4.1 为什么需要ComplexHeatmap方案
ComplexHeatmap包的UpSet()函数本质上是建立在组合矩阵(combination matrix)基础上的。它把upset plot拆解成两个核心组件:交集组合点阵图和交集大小条形图,然后将它们与热图等其他组件拼接在一起。
这种设计的最大好处是:所有组件都遵循ComplexHeatmap的图形语法,你可以用Heatmap()函数做热图,然后用UpSet()函数做交集图,最后用draw()把它们拼接到同一个画布上,形成“热图+upset plot”的组合图。
4.2 基本用法
用ComplexHeatmap画upset plot,核心函数是UpSet()。它接收一个list格式的数据,每个元素是一个集合的成员名称:
library(ComplexHeatmap) set.seed(123) m1 <- sample(letters, 15, replace = TRUE) m2 <- sample(letters, 20, replace = TRUE) m3 <- sample(letters, 12, replace = TRUE) m4 <- sample(letters, 18, replace = TRUE) lt <- list(set1 = m1, set2 = m2, set3 = m3, set4 = m4) UpSet(lt)直接运行就会输出一个基本的Upset plot。但这个函数默认输出的布局可能跟你预期的不太一样,它默认把集合条形图放在顶部,交集条形图放在右侧,点阵图在中间偏右下。这意味着如果你习惯了UpSetR的左侧布局,需要适应一下。
ComplexHeatmap版的UpSet()函数支持的参数非常多,常用的有:
- set_size参数:控制集合条形图的展示方式,可以显示每个集合的总大小
- comb_order参数:控制交集组合的排序方式,可以按大小、按集合顺序等
- top_annotation参数:设置顶部或侧边的注释对象,可以自定义颜色、文字等
4.3 跟热图组合出图
这是ComplexHeatmap方案的最大卖点。假设你有一个表达矩阵,想同时展示样本分组的表达模式和不同基因集合的交集情况,可以用如下方式组合:
library(ComplexHeatmap) library(circlize) # 模拟一个表达矩阵 expr_data <- matrix(rnorm(200), nrow = 20, ncol = 10) rownames(expr_data) <- paste0("Gene", 1:20) colnames(expr_data) <- paste0("Sample", 1:10) # 构建基因集合 gene_sets <- list( PathwayA = paste0("Gene", 1:6), PathwayB = paste0("Gene", 5:12), PathwayC = paste0("Gene", 8:15) ) # 画热图 ht <- Heatmap(expr_data, name = "Expression") # 画Upset plot并拼接 upset_plot <- UpSet(gene_sets, set_size = "both", comb_order = "size") # 组合绘图 draw(upset_plot + ht)这里的upset_plot + ht操作,是ComplexHeatmap的add_heatmap功能,它会把upset plot和热图拼接到同一画布。这种组合对发表论文、做组学综合分析非常有用,因为一张图就能同时展示基因表达模式和集合交集关系。
4.4 自定义颜色与注释
ComplexHeatmap方案的颜色控制也比较灵活。你可以通过参数指定集合条形图颜色、交集条形图颜色、点阵颜色等。例如:
UpSet(gene_sets, set_size = "both", top_annotation = upset_set_size( gp = gpar(fill = "tomato") ), right_annotation = upset_annotation( gp = gpar(fill = "steelblue") ))如果想要区分不同分组的样本,还可以在点阵图上用不同颜色标记。这个功能在做多组别比较时非常实用,能够一眼看出各个交集的构成差异。
5. 从数据清洗到结果解读:完整实操流程
前面的内容已经覆盖了两套方案的基础用法,这一节我把自己实际跑过的一个完整流程分享出来,包含数据清洗、交集计算、绘图和结果解读。这个流程的每一步都是干货,可以直接套用到自己的数据上。
5.1 数据准备与清洗
我遇到的最典型场景是这样的:手头有一份CSV文件,记录了不同样本中某些基因的突变或表达变化情况。列名是样本名,行名是基因名,单元格是数值或“是/否”标记。这种数据想画upset plot,第一步必须转成二进制矩阵。
下面是一段清洗代码:
library(dplyr) # 读取原始数据 raw_data <- read.csv("mutation_data.csv", row.names = 1) # 把非二进制值转成0/1 bin_data <- as.data.frame( ifelse(raw_data > 0 & !is.na(raw_data), 1, 0) ) # 删除全为0的基因(在所有样本中都没有突变) keep_genes <- rowSums(bin_data) > 0 bin_data <- bin_data[keep_genes, ] # 删除全为0的样本(没有发生任何突变的样本) keep_samples <- colSums(bin_data) > 0 bin_data <- bin_data[, keep_samples]这段代码做了三件事:把连续值转成二进制,过滤掉无信息基因,过滤掉无信息样本。前两步看着简单,但能极大提升后续图形的可读性。假如你保留了大量全为0的基因,左侧条形图上会多出一堆高度为零的“空条形”,白白占用空间。
另外要注意CSV文件里可能存在的缺失值。如果你的数据中存在NA,直接用ifelse(raw_data > 0)会得到NA,最终导致0/1矩阵里出现NA,影响绘图。所以在转换前先处理NA:
raw_data[is.na(raw_data)] <- 05.2 从二进制矩阵推导交集数量
画upset plot之前,你可能会想先确认哪些集合之间确实存在交集,以及交集大小是否值得关注。这一步用R基础函数就能实现:
# 计算各集合的大小 colSums(bin_data) # 计算两两交集大小 intersect_counts <- crossprod(as.matrix(bin_data)) View(intersect_counts)crossprod()函数计算的是矩阵的内积,因为矩阵元素只有0和1,所以内积结果正好是两列中同时为1的行数,也就是两两交集大小。这个技巧非常方便,不用写循环就能快速看到所有两两交集的数值。
如果交集数量极少,或者最大的交集也只包含一两个样本,那你画upset plot前要有心理准备:图上可能会有大量高度为1或2的小条形,显得很稀疏。这种情况下,我建议先对集合做过滤,只保留足够大的集合,再画图。
5.3 绘制并保存出版级图片
画好图后,保存图片时一定注意分辨率问题。用默认的PDF输出往往在期刊排版时会显得模糊,推荐用tiff格式,并设置足够高的dpi:
tiff("upset_plot.tiff", width = 8, height = 6, units = "in", res = 300) upset(bin_data, sets = colnames(bin_data)[1:8], order.by = "freq", nintersects = 30) dev.off()这里width和height的单位是英寸,res=300表示每英寸300个像素点,这是期刊投稿的基本要求。如果你是做PPT汇报,PNG格式、150dpi就够了,文件体积也小得多。
5.4 结果解读的三种思路
画完图,怎么读图才是关键。我总结三种最常见的解读思路:
第一种是看整体分布。先看上方交集条形图,最高的几个条形代表最频繁共现的集合组合。如果某个组合远远高于其他组合,说明这些集合之间存在强关联,值得深挖背后的生物学意义或业务逻辑。
第二种是看特定集合的贡献。如果你想判断某个集合是否“偏独立”,就去看点阵图中该集合单独出现的交集条形高度,以及它与其它集合组合时出现的高度。对比两者,就能判断该集合是经常单独出现,还是常常与其他集合捆绑出现。
第三种是看空交集与稀有交集。如果某些集合组合理论上应该存在,但在图中完全没有对应的条形,说明这些集合几乎没有共同元素,它们在功能上可能是互斥的。这种“负向信息”在探索性分析中很有价值,往往能提示分组结构。
6. 高频踩坑与排查实录
用了这么久Upset plot,我积累了不少坑的经验,有些坑真的是不踩一遍根本想不到。
6.1 数据格式报错:从list到data.frame的转换问题
一个非常常见的报错是:Error in (function (x, sets) : object not found。这个错误十有八九是因为你把list格式的数据直接传给了upset()函数,但UpSetR内部需要的是data.frame或已转换的list。解决办法是提前用fromList()转换:
upset(fromList(gene_list))如果你用的是UpSetR 2.0以上版本,部分新版本已经自动支持list输入了,但为了保险起见,还是建议显式转换。
6.2 中文标签乱码
R在Windows环境下画图,中文标签经常显示为方块。这跟R的默认字体设置有关。解决办法是设置绘图设备的字体:
# Windows下可用 windowsFonts(CN = windowsFont("SimSun")) # 或 par(family = "sans")更好的做法是在出图前统一把列名改成英文,图内文字全部用英文,避免字体兼容性问题。毕竟Upset plot的受众往往也习惯看英文标签。
6.3 集合列太多导致图形混乱
我刚上手时,直接把所有聚类结果或所有基因通路都塞进upset()函数,结果点阵图密密麻麻,连哪个点对应哪个集合都看不清。这个问题可以通过两个思路解决:
第一个思路是减少集合数量。只保留最核心的前5-8个集合,不重要的集合信息可以通过其他方式补充。第二个思路是利用nintersects参数限制显示的交集数量。先设置nintersects=15看主要交集,再决定是否需要展示更多。
6.4 条形图排序不理想
默认排序未必是你最想看的排序。当你发现最重要的交集不在最前面时,需要手动调整order.by参数或者传入自定义排序向量。
upset(bin_data, sets = c("GroupA", "GroupB", "GroupC"), order.by = c("freq", "degree"))这里的order.by接受一个向量,可以组合排序条件。比如先按交集大小排序,再按集合数量排序。这种两级排序方式可以让图显得更有逻辑。
6.5 图例与标题缺失
很多人在作图时忽略了图例和标题,导致图放进文档后读者根本不知道每个条形代表什么。UpSetR本身有自带图例,但位置比较尴尬,我通常会自己加一个简单的标题和注释:
grid.text("不同突变基因组合的交集情况", y = 0.98, gp = gpar(fontsize = 16))6.6 常见问题速查表
为了方便大家排查,我把最常见的几类问题和对应解决办法整理成了表格:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 函数报object not found | 数据格式未正确转换 | 用fromList()提前转换list |
| 图内全是空交集 | empty.intersections被开启 | 关闭参数或减少集合数量 |
| 条形高度全部为1 | 数据过滤不充分 | 删除低频集合,适当合并类别 |
| 标签显示为方块 | 中文字体不支持 | 改用英文标签或设置字体 |
| 点阵图过于密集 | 集合数量太多 | 只保留重点集合,或用nintersects限制 |
| 图上没有标题图例 | 需要手动添加 | 用grid.text()补标题 |
| 保存后图形模糊 | 分辨率不够 | 用tiff格式,设置dpi=300 |
7. 我个人实际使用中的一些体会
用Upset plot这几年,我最大的感受是:它的门槛不高,但想把图画得好,需要认真打磨数据的预处理和细节调整。
先说一下工具选型的最终建议。如果你只是做探索性分析,想快速看结果,UpSetR足够用了。但如果你要做出版级图表,尤其需要跟热图组合展示,ComplexHeatmap方案是更优选择。我自己现在的工作流是:先用UpSetR快速检查数据质量,确认交集结构合理;然后再用ComplexHeatmap做正式出图。两条技术路线互相补充,节省了不少时间。
另一个我觉得值得分享的经验是:Upset plot并不是只能画“基因集合”或“突变组合”。它本质上是通用集合交集可视化工具,可以用来展示客户重叠、功能模块复用、文档标签关联等任何集合型数据。不要被生物信息学的应用场景限制住思维,换个领域,这依然是一个利器。
最后聊一下调试技巧。画Upset plot时,如果图看起来不对劲,先不要急着调参数,回到数据本身检查三分:数据格式对不对?集合数量多不多?有没有全空的行列?大多数图形问题都源于数据预处理阶段留下了脏数据,而不是绘图函数本身出了问题。
上面这些内容,算是把自己在Upset plot的R实现上积累的经验做了一个比较系统的梳理。从选型、数据准备、基础绘图到进阶定制,每条路径都给出了完整的操作步骤和避坑方案。如果按照这个流程走下来,相信你也能在自己的数据集上画出干净、清晰、有说服力的Upset plot。