
上周帮一位做湿实验的师妹整理数据她把富集分析的结果表发过来附了一句“师兄有没有能在线画气泡图的网站我不想学R”。结果我打开表格一看第一反应不是帮她找工具而是告诉她“这图画不了因为你给的表少了一列分组信息。”她一脸懵“这不就是基因列表、通路名、p值都有了吗”这件事让我意识到很多人卡在“在线绘图”这一步真正缺的往往不是工具而是对图表背后数据结构的理解。这篇内容我就围绕标题里的两个方向展开一类是富集分析的多组气泡图另一类是单细胞分析里的marker基因矩阵气泡图。两者都叫气泡图但输入数据、绘图逻辑、适用场景完全不同。我会把数据格式怎么整理、代码怎么写、在线环境怎么跑通、常见的坑有哪些一次性讲清楚。适合不想从零搭R环境、又希望拥有灵活定制能力的研究生和科研工作者参考也适合已经把图做出来但总是差点意思、想优化到自己满意的同学。1. 为什么都在找“在线”画法——两种气泡图的真实门槛在哪先说说为什么这类需求这么普遍。富集分析气泡图是GO/KEGG结果的标配展示方式单细胞marker基因气泡图是细胞注释环节的“标准动作”。但大多数人去找在线工具不是因为这图有多难画而是被环境配置劝退的R要装、包要装、依赖还容易冲突光是搞定一个clusterProfiler就能耗掉半天。对以实验为主的课题组来说这个成本确实高。但在我看来真正的门槛从来不是“画”这一步而是“整理数据”这一步。在线工具解决的是画图动作本身可它没法替你决定该用哪列做横轴、哪列映射点的大小、哪列控制颜色。如果你不理解这些映射关系哪怕把ggplot2代码原封不动贴到在线平台上出来的图照样是乱的。先给这两类图一个基本定位富集分析多组气泡图展示的是一个统计表本质上是“通路 × 比较组”的富集结果。横轴一般用GeneRatio或Count纵轴是通路/条目名称气泡大小代表富集到的基因数量颜色代表显著性p.adjust或qvalue。多组意味着你需要在同一张图里分面呈现若干个比较组组与组之间互相独立但又共享图例和视觉风格。单细胞marker基因矩阵气泡图展示的是一个表达矩阵的统计摘要。行是marker基因列是细胞亚群每个格子里其实是两个维度——气泡大小代表该基因在该亚群中表达的阳性细胞比例颜色深浅代表平均表达量。这个图不是用来做富集检验的而是用来证明“你这群细胞确实表达了XX基因、不表达YY基因”。一眼就能看出虽然都叫气泡图但语义完全不同。很多人拿富集分析的结果去套单细胞气泡图的模板或者反过来自然做不出来。后面我会分别把两条线的数据准备和画法讲透先从相对简单的富集分析多组气泡图入手。2. 富集分析多组气泡图的输入数据格式少一列都出不来2.1 表格里必须有哪几列少了会怎样画任何图之前第一件事是检查数据。富集分析气泡图的标准输入格式并不复杂但每一列都有不可替代的用途。我把它整理成一张表你直接对着检查列名含义映射到图的位置缺失后果group比较组标签例如“上调_up”、“下调_down”分面facet或填充无法区分多个比较组变成一坨点ID通路/条目ID如GO:0006915可用来排重缺失可用Description代替Description通路描述如“apoptotic process”纵轴标签没有它纵轴只能显示ID可读性差GeneRatio富集基因数占通路基因数的比例横轴位置缺失时可用Count替代但横轴含义会变Count富集到的基因数量气泡大小缺口会非常明显图里所有点一样大p.adjust校正后的p值气泡颜色颜色无法映射显著性图的价值打对折单组数据缺一两列还能补救多组数据一旦没有group列问题就大了。因为在线工具通常只负责渲染不具备帮你拆分比较组的智能。我见过很多次用户把三个比较组的结果直接堆在一个表里上传工具压根不知道哪些行属于一组只能把所有点叠在同一面板里横轴基因占比全都错位。2.2 GeneRatio和Count到底怎么选两者有什么区别GeneRatio是富集分析里一个容易被忽略的细节。以clusterProfiler为例它在输出结果里给的GeneRatio是字符串形式比如“25/114”代表这条通路里富集到的25个基因占你输入基因列表总量114个的比例。如果你直接用这个原始字符串做横轴R会把它当作离散因子轴的排布会按字母序走完全乱掉。正确做法是做一个长表的时候就把GeneRatio拆成数值列或者直接用Count列代替横轴。两种方案的差异在于用GeneRatio图能体现不同通路之间的富集强度差异适合做多组对比时消除输入基因总数的量纲影响。比如比较组A输进去2000个基因比较组B输进去500个基因如果横轴都用CountB组所有点都会挤在左边看不出趋势。此时打分比更公平。用Count更直观读者一看到点的大小就知道富集到什么程度但如果你把多个比较组放在一起且各组输入基因数悬殊误导性会比较强。我的建议是如果所有比较组来自同一次分析输入基因数量相近用Count没问题如果各组输入基因数差很多或者你希望图更“论文级”一点就用解析后的GeneRatio。具体到在线绘制场景你可以提前在Excel里加一列把“25/114”手动拆成数值0.219这比让工具去处理简单得多。2.3 一个可直接对照的长表示例为了让你直观理解什么叫“可以直接上传绘图工具”的数据我给一个精简示例groupIDDescriptionGeneRatioCountp.adjust组1_upGO:0006915apoptotic process0.219253.2e-08组1_upGO:0006954inflammatory response0.132151.5e-05组1_downGO:0008283cell population proliferation0.180204.6e-06组1_downGO:0007165signal transduction0.088108.1e-03组2_upGO:0006915apoptotic process0.110122.2e-03组2_upGO:0006954inflammatory response0.04653.0e-02组2_downGO:0008283cell population proliferation0.06571.8e-02这里需要注意两个细节。第一Description这一列在不同比较组之间会重复比如组1_up和组2_up都富集到apoptotic process这是正常现象。但你在上传之前要确认同一个组内Description不重复否则纵轴会出问题。第二如果你想按p.adjust大小对通路排序必须考虑分组。不同比较组的p值范围可能差几个数量级如果全图统一排序显著性低的组可能选不出代表性通路。一个稳妥办法是每组先按p.adjust排序取前10到15条通路再合并成长表。3. 从云端的RStudio到出图富集气泡图的完整落地链路3.1 为什么我推荐“云RStudio”而不是花哨的网页生成器市面上的在线绘图平台不少很多生物信息学网站也提供了富集气泡图入口。但它们通常有一个共性问题只能画单组、选色板有限、纵轴标签一旦过长就自动截断而且当你需要一个杂志要求的特定分辨率或者排版风格时完全无从下手。所以我个人最常用、也最推荐给身边人的方案是用云端RStudio例如Posit Cloud这类提供R环境的在线服务浏览器打开就能运行R不需要在本地安装任何东西配合ggplot2或者基于ggplot2的工具包完成绘制。它本质上就是一个跑在浏览器里的R环境免费配额足够应付这类分析。这样做有两个好处。第一所有代码可复现下一次换数据只需改路径和组名不用重新学习平台操作。第二对于“多组对比”这种需求ggplot2的facet语法天然就是为这个场景设计的比任何在线工具的封装都灵活。云端RStudio也会面临包安装的问题但通常内置了常用包源安装clusterProfiler这类包只是几分钟的事。3.2 从上传数据到输出图片的完整代码实现这里给一份可以直接跑的完整流程。你只需要把前面整理好的长表保存为CSV文件比如go_multi.csv然后按以下步骤操作。# 1. 读取数据 library(ggplot2) df - read.csv(go_multi.csv, header TRUE, stringsAsFactors FALSE) df$p.adjust - as.numeric(df$p.adjust) df$GeneRatio - as.numeric(df$GeneRatio) # 2. 按分组和p.adjust排序保证每条通路在组内的位置合理 df - df[order(df$group, df$p.adjust), ] # 3. 将Description转成因子并按组内顺序固定轴顺序 # 这里用ave和rank生成组内顺序索引再按索引排因子水平 df$desc_order - ave(seq_len(nrow(df)), df$group, FUN function(x) rank(df$p.adjust[x])) df$Description - factor(df$Description, levels unique(df$Description[order(df$group, df$desc_order)])) # 4. 核心绘图 p - ggplot(df, aes(x GeneRatio, y Description)) geom_point(aes(size Count, color p.adjust)) scale_color_gradient(low #D32F2F, high #1E88E5, trans log10) scale_size_continuous(range c(2, 8)) facet_wrap(~ group, scales free_y, ncol 2) theme_bw(base_size 12) theme( axis.text.y element_text(size 9), strip.text element_text(face bold), panel.grid.minor element_blank() ) labs(x GeneRatio, y NULL) # 5. 输出高分辨率图片论文常用300dpi ggsave(富集分析多组气泡图.pdf, p, width 10, height 7, dpi 300) ggsave(富集分析多组气泡图.png, p, width 10, height 7, dpi 300)这段代码里有几个关键点值得单独强调。scale_color_gradient里用了trans log10是因为p.adjust通常跨度非常广从1e-30到1e-2都有如果用线性色标所有点都会偏向同一颜色显著性差异根本显示不出来。取对数后颜色层次才拉得开。facet_wrap(~ group, scales free_y)是“多组”的核心。free_y意味着每个比较组的纵轴只展示该组富集到的通路不会因为其他组通路多而被压扁或留白。如果你希望所有组共享相同的通路轴比如只关注几个感兴趣的共同通路就改成scales free或直接去掉这行。因子顺序处理是ggplot2里最容易翻车的一步。很多人上来直接factor(Description)结果纵轴按字母序排列逻辑完全不对。富集图通常希望最显著的通路排在最上方所以我在第3步先计算了组内p.adjust排序再用这个顺序设置因子水平。3.3 报错排查在线环境里最常见的三种翻车现场把绘图的坑挑几个高频率的说一下希望你别再踩。第一个报错是Error: Insufficient values in manual scale。这通常是你手动设置了颜色或者大小范围但数据里存在NA值。解决办法很简单先跑sum(is.na(df$p.adjust))如果有NA用df - df[!is.na(df$p.adjust), ]过滤掉。出现NA的原因大多是Excel里把极小值显示成了科学计数法后复制粘贴时丢失了小数部分。第二个问题不是报错而是图出来以后纵轴乱序。根本原因就是因子水平顺序不是按组内显著性排的。我上面代码里已经把顺序逻辑写进去了但如果你希望“每组各取Top10”需要先在合并表之前就对每组单独排序筛选。这一步最好在Excel里完成或者用R的dplyr包分组建top_n比在ggplot里硬调聪明得多。第三个问题是中文乱码。如果你把Description写成了中文在云端RStudio里经常出现豆腐块。这不是代码问题而是字体缺失。最省事的方案是Description全部用英文中文含义放在论文正文里解释。如果导师要求图中显示中文你需要额外配置中文字体文件复杂度上升不少并不建议在在线环境里做。4. 单细胞Marker基因矩阵气泡图DotPlot的底层逻辑与数据提取4.1 Seurat的DotPlot到底在算什么单细胞marker基因气泡图最常见的来源是Seurat。很多人只记住了DotPlot(seu, features markers)这行函数并不清楚它内部生成了什么矩阵结果一旦离开Seurat对象比如想用一个在线工具画图就完全不会了。实际上DotPlot只是两步计算的包装第一步对每个基因、每个细胞亚群计算平均表达量avg.exp实际是expm1(mean(x))处理后的均值也就是先对表达值做log1p逆变换再求平均避免极少数高表达细胞拉高整体均值。第二步计算每个基因在每个细胞亚群中的表达阳性比例pct.exp阈值是表达值大于0的细胞比例Seurat默认pct.exp用的是log1p后的表达矩阵里大于0的比例。我们最终看到的图气泡大小映射的是pct.exp颜色映射的是avg.exp。也就是说一个气泡同时承载两个维度的信息这个基因在这个亚群里有多少细胞在表达阳性比例以及这些细胞的平均表达水平有多高颜色深浅。理解这一点对你的数据整理能力提升非常大。因为如果你想把单细胞图也拖到在线工具里去画根本不需要上传整个Seurat对象只需导出一个四列表——基因、细胞类型、平均表达量、阳性比例——就足够了。文件从几百MB缩小到几十KB任何在线平台都能轻松接收。4.2 如何从Seurat对象导出可直接上传的绘图矩阵下面给一段在本地R里提取矩阵的代码。你只需要在本地跑一次把结果存成CSV之后就再也不用碰R了后续所有微调都丢给在线绘图环境或者自己熟悉的工具。library(Seurat) # seu是已经完成聚类和细胞类型注释的Seurat对象 # markers是你选定的marker基因列表按你想展示的顺序排列 markers - c(CD3D, CD3E, CD79A, MS4A1, LYZ, FCGR3A, NKG7) # 用Seurat内置函数直接提取绘图数据 plot_data - DotPlot(seu, features markers, group.by celltype)$data # 此时的plot_data包含四列avg.exp, pct.exp, features.plot, id # 列名不太直观建议重命名后导出 colnames(plot_data) - c(avg_exp, pct_exp, gene, cluster) # pct.exp在旧版本Seurat中可能是0~1的小数建议转成0~100的百分比便于理解 plot_data$pct_exp - plot_data$pct_exp * 100 write.csv(plot_data, dotplot_matrix.csv, row.names FALSE)这段代码的价值在于它把Seurat的复杂对象缩减成一个四列矩阵后续一切操作都不再依赖Seurat。如果你在本地连Seurat都没装还可以手动从表达矩阵算。思路是对每个细胞亚群取平均算出平均值矩阵再对每个亚群计算表达大于0的细胞比例按同样的行列结构得到比例矩阵。两个矩阵分别堆叠成长表后合并起来就得到了同样的四列。我见过一些在线工具要求输入“基因×亚群”的宽表矩阵而不是四列长表。如果遇到这种情况你可以在Excel里用数据透视表或者R的pivot_wider转换一下。把gene放行、cluster放列、avg_exp或pct_exp作为值分别导出一份颜色矩阵和一份大小矩阵即可。4.3 用ggplot2复刻DotPlot在线环境也能画出满意的图拿到四列长表后剩余的事情就简单多了。即使你没有现成的在线R环境能跑R的云端或者本地环境都适用。下面是完整绘制代码。library(ggplot2) # 读取上一步导出的矩阵 df - read.csv(dotplot_matrix.csv, header TRUE, stringsAsFactors FALSE) # 固定基因顺序和细胞类型顺序 df$gene - factor(df$gene, levels rev(c(CD3D, CD3E, CD79A, MS4A1, LYZ, FCGR3A, NKG7))) df$cluster - factor(df$cluster, levels c(CD4 T, CD8 T, B cell, Monocyte, NK)) p - ggplot(df, aes(x cluster, y gene)) geom_point(aes(size pct_exp, color avg_exp)) scale_color_gradient(low #F5F5F5, high #C2185B) scale_size_continuous(range c(1, 10)) theme_bw(base_size 12) theme( axis.text.x element_text(angle 45, hjust 1), panel.grid.major element_blank(), legend.position right ) labs(x NULL, y NULL, size Percent Expressed, color Average Expression)这个复刻版有一个比Seurat原生DotPlot更友好的地方你可以自由调整点的大小映射范围、颜色渐变方向甚至可以把细胞类型放到纵轴、基因放横轴这在展示大量marker时会更方便排版。一个值得注意的细节是纵轴基因顺序。我用了rev()倒转基因列表顺序这样在图上第一个基因会出现在顶部与你选定marker时的阅读顺序一致。如果不做这步基因会从底部往上排很多人第一次画出来都会觉得“顺序怎么反了”。4.4 这张图最常见的三个改稿问题我审过不少人的单细胞marker气泡图问题集中在三个方面。第一个是颜色深浅区分度不够。Seurat默认色板偏浅打印出来以后低表达和高表达之间差别很模糊。我的建议是低端选浅灰白高端选酒红或深紫并且在Figure里加上“表达量低的亚群”那部分说明。如果数据里本身没有高表达的基因-亚群组合整张图会偏淡这时可以通过limits参数把色标下限抬高或者换离散色板效果会立刻改观。第二个是气泡大小与颜色信息重复造成误解。有的基因在一个亚群里avg.exp很高但pct.exp很低少数细胞极高表达这时图上会出现一个又大又深的点读者容易误以为该亚群普遍高表达。要规避这个你不要在图上标注绝对阈值而是在方法学里说明“气泡大小反映阳性比例”并在正文解读时特别注意这类基因-亚群组合。第三个是marker基因顺序的“逻辑”问题。推荐的排列方式不是随便列而是按细胞类型把marker分组排列比如T cell相关marker放一起、B cell marker放一起这样读者一眼就能看到整个矩阵的对角块。你可以在导出数据前手动整理markers向量代码里那种硬编码的方式就是为此设计的。5. 三条在线路径怎么选在线R、Shiny应用与公共平台对比最后聊一下工具选型。既然你已经知道了两种图的底层数据和代码逻辑那选哪条路径完全取决于你的动手意愿和分析频率。我按三类人群分别说明。方案典型工具适合人群优点短板云端RStudioPosit Cloud等愿意写代码、希望成图风格可控的人可复现性强、图表定制空间大、免费额度够用有学习曲线需要熟悉R基础现成Shiny应用一些实验室或工具站开放的绘图应用零R基础、想快速出图看效果的人上手快上传数据点两下就出图定制度有限格式未必符合期刊要求公共在线分析平台单细胞数据门户、富集分析在线服务整体分析场景不只是画一张图功能全面从分析到作图一体化数据需上传有时格式限制严格且不太适合自定义预览如果你是零基础只想快速看趋势选第二条路。但务必提前把数据整理成前面讲的四列或七列格式否则任何工具都帮不了你。如果你接下来还有多组比较、换色板、调分辨率等个性化需求选第一条路一次性投资学习成本之后能反复用。第三条路适合你本身就要做整套在线分析、不想在本机装任何生物信息学工具的情况。以我自己的经验来说混合搭配是最好的数据分析阶段用R把矩阵导成长表绘图阶段在云端RStudio里跑固定模板。这样既不用本地装环境又保留了随时改图的能力。很多线上工具适合“第一次出图”但论文返修时往往需要改配色、改字号、改分面方式那时没有代码在手只能从头再来。6. 我自己踩过几次坑之后的一点点体会说个真实经历。我最早画多组富集气泡图时直接在本地把三组数据合并成大表用ggplot2画完后看起来不错但投出去审稿人提了一个问题三个比较组的纵轴通路数量差异太大组1有30条通路组3只有5条导致组3的条带特别宽点被拉得很大视觉上很夸张。后来我才意识到问题不在绘图代码而在数据处理应该每组先按p.adjust取Top10再合并绘图。这个“TopN截取”的习惯后来我每次做多组气泡图都会保留它能让你的图清爽很多。单细胞marker气泡图这边我最大的教训是导出CSV时没有检查重复行。如果一个基因在两个亚群的avg.exp和pct.exp完全一样ggplot会重叠绘制两个同一个位置的点稀释颜色饱和度视觉上看起来像颜色变淡了。这种情况虽然少但一旦出现就很隐蔽。最简单的排查方式是在导出前跑一下nrow(unique(plot_data))看和原始行数是否相等。最后分享一个小技巧无论多组富集图还是单细胞DotPlot我都建议把最终出图的PDF和PNG同时保存。PNG方便日常查看和插入PPTPDF则用来投稿。很多在线工具只能输出PNG一旦期刊要求矢量图就非常被动。用R的话ggsave一句就能同时输出两种格式这也是我坚持走代码路线的重要原因。希望这篇内容能帮你少走一段弯路把时间花在解读结果上而不是耗在环境和报错里。