做单细胞测序数据分析的同行应该都有这种体验:聚类、注释忙活一周,最后画marker基因气泡图的时候,反而被Seurat自带的DotPlot卡住了。不是不能画,是画出来总觉得差点意思——基因排列要么按字母序要么按平均表达量,你想按照自己的逻辑排,得先改因子水平;细胞类型默认按cluster编号排,注释完想按生物学顺序排,又是一通操作;配色就更不用说了,默认那套红蓝配色投到组会上,被老板吐槽过不止一次。
后来我在GitHub上翻到一个专门解决这个问题的R包,叫scDotPlot,作者主要针对单细胞marker基因气泡图做了全套优化。用下来最大的感受是:本来要折腾半小时的细节调整,现在一行代码就能搞定,出图质量也能直接放到论文里。今天这篇就把我的使用流程、参数心得和踩过的坑完整整理出来,给正在被气泡图折磨的同学一个能直接抄作业的参考。
1. 先把痛点说透:Seurat自带的DotPlot到底哪里不给力
1.1 默认绘图的三宗罪
气泡图(dot plot)在单细胞测序数据分析里几乎是标配图。它用点的大小表示某个基因在该细胞类型中表达的比例(percent expressed),用点的颜色深浅表示平均表达水平(average expression)。一张图同时承载两个维度的信息,既能快速扫一眼marker基因的特异性,又是单细胞手动注释前必做的检查项,还是论文里展示细胞亚群纯度的常用图。
Seurat自带的DotPlot功能本身没问题,但实际用起来有三个地方非常难受。
第一是排序问题。默认情况下,基因按字母序或聚类后的顺序排列,细胞群按cluster编号顺序排列。你做完细胞类型注释之后,组里通常约定俗成按"T细胞→B细胞→NK细胞→单核细胞→树突状细胞"这种符合生物学逻辑的顺序展示,但Seurat不会帮你按这个排,你得自己把cluster列转成factor、手动指定levels。调一次能行,但基因顺序也想自定义的时候,又要再一次改因子。这些操作不是不会,而是每次都要重复,项目一多,光是理因子水平就能理到崩溃。
第二是分组信息很难直观展示。如果你有对照组和处理组,想把两组分别画出来对比,Seurat的DotPlot要么用split.by参数分成两个panel,要么得你提前把数据拆开、分别画图再拼。split.by在细胞类型多的时候排版特别拥挤,你自己拼图又要处理图例对齐问题,最后出来的图总有拼接感。
第三是配色和细节。Seurat默认的色带是蓝到红,表达量跨度大的数据集里,低表达群的颜色几乎糊成一片,区分度不高。另外,想在图里加细胞类型注释条、调整点的大小范围、控制图例文字大小,这些在Seurat里都不是一个参数能解决的,往往要深入ggplot2的theme层面自己改。
所以不是Seurat画不了气泡图,而是它在"能画"和"画得好、画得快"之间,隔着一堆重复劳动。
1.2 scDotPlot的设计思路:把气泡图做成一个成品组件
scDotPlot这个包的核心思路其实很朴素:把气泡图绘制的完整流程封装成一个函数,把排序、配色、图例、尺寸、分组、注释这些高频需求全部做成参数暴露给用户。你传入一个Seurat对象(或者直接的表达矩阵)、指定聚类标签所在的列、再给一个基因列表,它就帮你把图组织好。作者在设计时明显考虑过真实的生信分析习惯,所以把最常见的"反人类操作"都提前做了处理。
我特别喜欢的几个设计点:cluster.order和gene.order参数可以直接用字符向量指定顺序,不用手动改factor;对分组(group)的支持比较友好,可以在同一个坐标系里按照组的维度展开,两组或多组对比的排版很规整;内置的几套配色方案里,默认的viridis系列在表达量渐变上表现很稳定,比红蓝两色在低表达区间更细腻,打印出来也依然有层次。
当然,这个包不是什么银弹,它是基于ggplot2的封装,底层能力还是ggplot2那一套。但正因为封装做得好,你平时80%的微调需求都不需要碰底层代码了。后面我会把每个参数怎么用、怎么组合出效果讲清楚,读完你就能根据自己的数据灵活组合,而不是死记几个模板。
2. 装好环境配好数据:scDotPlot使用前的必修课
2.1 安装三步走:依赖、官方源、验证
安装scDotPlot之前,先检查三样东西:R版本、devtools/remotes包、常用依赖包。它的依赖主要是Seurat(至少要有SeuratObject)、ggplot2、dplyr、magrittr这些。如果你的R版本低于4.0,建议先升到4.1以上,否则个别依赖装不上,报错会让人误以为是这个包本身有问题。
我在Windows上踩过一个坑:直接用install_github装这个包,不带Rtools的话会一直报错,因为源码包编译这关过不去。Rtools装好之后,用下面这行命令安装:
install.packages("devtools") library(devtools) devtools::install_github("Jiaxin-Fan/scDotPlot")如果服务器上已经装了remotes,用remotes::install_github("Jiaxin-Fan/scDotPlot")效果一样,二选一就行。编译过程一般两三分钟,主要耗时在依赖包编译。装完以后加载一下,再输出版本号:
library(scDotPlot) packageVersion("scDotPlot")能正常输出版本号就说明安装成功。如果报缺某个依赖,缺什么装什么,逐个补齐就行,这种依赖缺失问题在R里很好解决。这里我多说一句:不同小版本的参数名可能有细微差别,装好后先跑一下?scDotPlot看看帮助文档,确认参数名和当前版本一致。这不是废话,我吃过版本差异的亏,照着网上旧教程写参数名,结果全部不生效,浪费了一下午。
2.2 输入数据长什么样:先备好一个干净的Seurat对象
用scDotPlot之前,你要有一个完成了基础分析的Seurat对象。所谓基础分析,至少包括:归一化(NormalizeData)、高变基因识别(FindVariableFeatures)、标准化(ScaleData)、降维(PCA/UMAP)、聚类(FindClusters)。这些是单细胞测序数据分析的标准流程,如果还没走到这一步,先把流程跑完再回来画图,否则输入数据本身就不干净,画什么都白搭。
关键的一点是,Seurat对象的meta.data里必须有一列指定细胞类型或聚类群。这一列可以是原始的seurat_clusters,也可以是你手动注释后的cell_type列。比如meta.data里有一列叫cell_type,内容是t cell、b cell这种,那cluster参数就传"cell_type",它会自动读取每个细胞对应的分组。如果你既没注释也没聚类,就别急着画图,先把分群做出来。
另外两个数据层面的要求容易被忽略,但实际经常出问题。第一,基因名要和表达矩阵一致。如果你的Seurat对象里基因符号是小写的,或者用的是Ensembl ID,而你传入的marker列表是标准大写基因名,匹配不上,画出来就是一堆空行。第二,meta.data列最好是factor类型,字符型在某些版本下排序不顺,后面我会专门讲。如果只是偶尔用一次,字符型问题不大;但如果要频繁比较不同样本,我建议把注释列统一转成factor并设好levels,一劳永逸。
如果没有Seurat对象,只有一张表达矩阵(行为基因、列为细胞)加一个细胞分组向量,理论上也可以画,只是要把矩阵和分组整理成包要求的标准格式。对绝大多数人来说,直接传Seurat对象是最省事、最少出错的路线,也是我唯一推荐的路线。
3. 核心参数逐个拆解:气泡图的每个细节都能控制
3.1 先搞懂气泡图的底层逻辑:一个点两条信息
在动手调参数之前,花一分钟把气泡图的信息编码逻辑理顺。气泡图本质上是热图的变体,但它不用颜色块承载所有信息,而是把"表达比例"和"平均表达量"拆成两个视觉通道:点的大小和点的颜色。
点的大小代表:该细胞类型中,有多少比例的细胞检测到了这个基因的表达。比如某个细胞类型有1000个细胞,其中800个表达CD3D,那这个点的大小就对应80%。点的颜色深浅代表:表达了该基因的细胞,平均表达量有多高。平均表达量的算法一般有两种,一种是直接取归一化后的表达值平均,一种是先对基因做z-score标准化再取平均。scDotPlot内部默认的处理逻辑和Seurat保持一致,保证两个维度互不干扰。
理解这个逻辑之后,你就明白了为什么气泡图特别适合看marker基因:你不仅希望基因在目标细胞群里平均表达水平高,还希望它表达的细胞比例高,两者同时满足才说明这是个靠谱的marker。只看平均表达量高的基因,很可能是少数细胞高表达拉高了均值,特异性并不好;气泡图把占比同时展示出来,正好可以避免这种误判。这也是为什么组会和文章里,气泡图永远比热图更受青睐。
3.2 参数速查表:照着这张表调就够了
下面这些是我日常使用最高频的参数,整理成表。注意装好包之后先用?scDotPlot核对一遍,少数小版本参数名可能有差异,核对这个动作花不了三十秒,能省大半天。
| 参数 | 作用 | 我的推荐用法 |
|---|---|---|
| data | Seurat对象或表达矩阵 | 直接传Seurat对象 |
| cluster | meta.data中指定细胞类型的列名 | 传"cell_type" |
| gene | 要展示的marker基因向量 | 按生物学逻辑排序 |
| group | 分组列名,用于样本/条件对比 | 需要对比时传入 |
| cluster.order | 指定细胞类型的展示顺序 | 传字符向量 |
| gene.order | 指定基因的展示顺序 | 直接传gene向量 |
| point.size | 点的大小范围 | 按细胞数微调 |
| color / palette | 表达量渐变的配色 | 推荐viridis系列 |
| legend.position | 图例位置 | 默认即可 |
值得单独说的是cluster.order。细胞类型多的时候,比如有15种注释结果,你不想让doublet或者unknown这种群出现在图的最前面,就把想展示的顺序完整写在cluster.order里,剩下的类型会自动排在末尾。这个参数的体验比手动改factor水平舒服太多,是我换到这个包之后最明显的感觉,也是我向周围人安利它时最常用的理由。它背后的实现其实很简单,但就是把"每次都要手动做"变成了"一个参数解决",这就是好的工具设计。
3.3 group参数:分组对比是隐藏的高频需求
group参数我单独拿出来讲,因为这是scDotPlot相比Seurat原生DotPlot最有优势的场景之一。假设你有对照组和给药组两组样本,想看同一组marker基因、同一批细胞类型在两组间的表达差异,用Seurat的split.by会变成并列的两个panel,图拉得很宽,细胞类型一多就没法看。
用group参数,你可以把分组作为一个维度合并到图里,包会按分组分别统计每个细胞类型的表达比例和平均表达量,再用分面或分组标记的方式呈现。实际出图布局会根据数据自动调整,比手动拼图省心。我在药物处理前后的免疫细胞状态比较、以及不同组织来源对比的场景里,都用过这个功能,汇报时一张图就能把差异讲清楚,也不用在slides里贴两张图费力对齐。
需要注意的是,group参数传的列名对应的meta.data列,最好是样本名称或者条件标签,不要是那种只分一两个类的简单因子,否则分组维度没有信息量,画出来的图反而啰嗦。另外,如果三个以上分组,建议先想好配色,让每个组的图例颜色固定,避免汇报时被追问"这组是哪个处理"。
4. 保姆级实操:5分钟从Seurat对象到成品气泡图
4.1 第一步:把marker基因列表整理好
动笔写代码之前,先决定画哪些marker基因。来源一般有三个:参考文献里发表的细胞类型标记物、自己用FindAllMarkers算出的top差异基因、两者结合——用文献marker做骨架,用自己的差异基因做补充。
我的习惯是建一个命名list,每个细胞类型对应几个marker,最后按展示顺序展开成一个向量。这样基因的顺序天然跟着细胞类型走,看图的人一眼就能对应上,不用在figure legend里反复解释"哪个基因是哪个群的"。示例代码:
marker_list <- list( `T cell` = c("CD3D", "CD3E", "CD8A", "GZMB"), `B cell` = c("MS4A1", "CD79A"), `NK cell` = c("NKG7", "GNLY"), Monocyte = c("CD14", "LYZ", "FCGR3A"), DC = c("FCER1A", "CLEC10A") ) marker_genes <- unlist(marker_list, use.names = FALSE)这里有个被坑过很多次的问题:基因名的格式。小鼠基因首字母大写,人类基因全大写,如果对象里是Ensembl ID或者带版本号,记得先转成symbol。最稳的办法是,在整理marker列表的时候就直接对着rownames(seu)去写,别自己凭记忆敲,一旦有个基因名大小写不对,那一行就是空的,排查起来很痛苦。如果你是从文献里复制的marker列表,务必先跑一遍交集检查,把不匹配的基因挑出来单独处理。
4.2 第二步:一行核心代码直接出图
假设Seurat对象已经完成注释,meta.data里有一列cell_type,那最基本的调用就是:
library(Seurat) library(scDotPlot) seu <- readRDS("your_seurat_annotated.rds") p <- scDotPlot(data = seu, cluster = "cell_type", gene = marker_genes) print(p)运行完如果数据没问题,绘图窗口就会出现一张完整的气泡图。这一步其实就是整个流程的核心,后面全是锦上添花。标题里说的"5分钟搞定",指的就是数据备好之后,从写代码到出图控制在几分钟内,大部分时间还是在整理marker列表上,真正画图的时间是秒级的。第一次用的时候,看到一行代码出来的图居然连图例、配色、点的大小梯度都处理好了,确实有一种"这才是工具该有的样子"的感觉。
4.3 第三步:排序、配色、点大小一次调到位
图片出来后,先检查细胞类型的顺序。不对就用cluster.order参数指定:
p <- scDotPlot(data = seu, cluster = "cell_type", gene = marker_genes, cluster.order = c("T cell", "B cell", "NK cell", "Monocyte", "DC", "Doublet"), gene.order = marker_genes)gene.order直接传原始marker_genes向量,基因顺序就和定义的一致,不用重复写列表。如果你的基因数量特别多,也可以让基因按表达量聚类排列,具体看包的帮助文档,不过我建议按生物学逻辑排,这样读者更好理解,审稿人也更友好。
配色方面,默认不满意就自己传颜色向量:
p <- scDotPlot(data = seu, cluster = "cell_type", gene = marker_genes, color = c("#440154", "#3B528B", "#21918C", "#5DC863", "#FDE725"))这个向量是给表达量渐变用的,方向从低到高,至少给两个,多给几个渐变更平滑。点的大小范围要根据细胞数量微调。每个群只有几百个细胞的小数据集,点默认可能偏小,把范围调大;几十万细胞的大数据集,点要调小,不然占满格子。这个没有万能参数,我是按"图里最密的地方不重叠、最稀的地方还能看清"的标准来调的,每次新数据集都要试一下,但整个过程也就一两分钟。
4.4 第四步:高分辨率导出,别在最后一步翻车
画完图要用于汇报或投稿,就得导出高分辨率版本。scDotPlot返回的是ggplot对象,所以照常用ggsave保存,这一点很方便,意味着所有ggplot2的theme调整技能都能继续用:
ggsave("marker_bubbleplot.pdf", p, width = 8, height = 6, dpi = 300)PNG用于PPT或者组会,dpi设300起步。尺寸有个经验公式:宽度按基因数量×0.3英寸估算,高度按细胞类型数量×0.3英寸估算,再留出边距。基因多或者细胞类型多的时候,图会变得特别扁或特别瘦,提前估一下尺寸能少走很多弯路。输出前先在RStudio里放大看一遍,确认没有文字重叠、没有空白行,再落盘,别等图存好了才发现要重画。
5. 进阶玩法:把气泡图用进注释流程和BCR/TCR分析
5.1 单细胞手动注释的核心验证工具
单细胞手动注释是单细胞测序数据分析里最关键的一步,流程大概是这样:先无监督聚类,再看着UMAP图初步判断每个cluster可能是哪种细胞,然后用marker基因验证,最后给每个cluster打上细胞类型标签。这个过程非常依赖marker基因的可视化,而气泡图是效率最高的呈现方式。
气泡图在这个流程里是刚需。你不可能盯着一堆矩阵看十几个cluster的marker表达,气泡图可以在一张图里承载全部关键信息。我实际用的一个技巧是:把样本维度传给group参数,对比不同样本里同一个cluster的marker表达是否一致。如果A样本里高表达T cell marker,但B样本里表达量又低、比例又小,就要警惕这个cluster是否真的在两个样本里都是T cell,可能要单独亚聚类重新确认。
所以我的工作流里,气泡图不是一个"最后画一张"的动作,而是"注释前验证、注释后再验证"的反复动作。每次调整注释结果,都重新出一张气泡图确认,这套循环虽然朴素,但能把错注释漏注释的概率压到最低。单细胞手动注释这事,没有什么捷径可走,靠的就是反复验证,一个可视化工具用顺了,效率自然就上来。
5.2 汇报和投稿:让气泡图变成文章的加分项
注释完成之后,气泡图又承担起"对外展示"的任务。导师汇报、写文章、投期刊,气泡图都是展示注释质量的标配图。这时候我会做两件事:一是把细胞类型顺序按文献常见顺序排好,让读者第一眼就能对号入座;二是把配色调成和UMAP图一致的主色调,整篇文章视觉风格统一。
有一个细节值得分享:先记录好UMAP图里每个细胞类型的颜色,整理成一个命名向量,然后让气泡图使用同一套颜色逻辑。审稿人看到的是一套颜色体系,阅读门槛会降不少。这个做法我用了很久,效果好,成本却很低,就是复制粘贴几行代码的事。如果你的UMAP图是用Seurat默认配色画的,那也可以接受,但最好在投稿前把整套图的配色统一,这种细节虽然不影响科学性,但很影响第一印象。
5.3 衔接BCR/TCR单细胞分析:亚群marker先验证再往下走
现在很多单细胞项目不只是做表达谱,还会做BCR单细胞分析(VDJ)和TCR分析,研究B细胞受体和T细胞受体的克隆型、多样性。这类分析通常是先做表达谱聚类注释,把B细胞亚群和T细胞亚群找出来,再单独做VDJ数据拆分和克隆型分析,所以表达谱注释的准确性直接决定了后面VDJ结论的可靠性。
气泡图在这个流程里的位置很明确:你在把B细胞/T细胞进一步分成naive、memory、regulatory等亚群之后,得先用marker验证亚群划分是否合理。比如naive B和memory B的marker表达应该有明显差异,CD4 T和CD8 T也不能混在一起。用scDotPlot把亚群marker画出来,既能确认分群对错,也能在论文里作为"我确实细分出了这个亚群"的证据。
我建议在拆分VDJ数据之前,把这个亚群marker验证做扎实。不要急着往下跑BCR/TCR分析,分群错了,后面所有克隆型结论都建立在错误的基础上,返工成本极高。宁可多花两天在注释和验证上,也别省这一步,这是我做过几个V(D)J项目之后最深的体会。
6. 高频报错与排查技巧:踩过的坑一次说清
6.1 基因找不到或者整行空白
遇到最多的一个问题,症状是某些基因根本没画出来,或者对应行全是空点。原因不外乎两个:基因名格式不匹配,或者这些基因在数据集中真的没表达。
排查先看基因名。你传的基因必须和表达矩阵里的基因名完全一致,包括大小写。用下面这行代码看交集:
intersect(marker_genes, rownames(seu))如果交集很少,就是格式问题,比如你写了人的大写基因名,数据里是小写或者Ensembl ID。如果交集正常但图还是空的,再看看这些基因是否在质量过滤阶段被筛掉了,或者是不是在所有细胞里的表达本来就很低。还有一个小概率情况:你用的是小鼠大脑数据,却塞了一个B细胞的marker列表,那自然画不出东西。总之,marker列表一定要对着rownames(seu)抄,这是最笨也最稳的办法。
6.2 细胞类型顺序不对或图例乱
顺序不对,基本就是cluster.order没生效。要么是参数名写错了,要么是传进去的类型名和meta.data里的实际值不一致,最常见的是多了个空格、大小写不同。先用table(seu$cell_type)把实际值打出来,然后复制粘贴进cluster.order,不要手敲,这是经验之谈。
还有个隐藏问题:meta.data这一列如果是字符型,某些版本下的排序不按预期走。建议提前转成factor并指定levels,scDotPlot会优先尊重levels的顺序。如果转完factor还是不对,就把levels的顺序检查一遍,别把"T cell"写成了"T cells",这种一个字母的差异很难发现,但table一眼就能看出来。
6.3 颜色糊成一片或者点小到看不见
颜色层次分不出来,多半是数据里有极端高表达基因,把色阶整个拉开了。解决办法是自定color参数的范围,把低表达和高表达的颜色都控制在一个视觉友好的区间里。点太小就调point.size,这个参数是按实际展示效果调的,多试几次就有手感。
我建议画图前先统计一下表达矩阵的表达值范围,心里有个底。某个基因表达值特别高的话,考虑是否保留它,或者用ScaleData处理后再传。scDotPlot底层会做scale,但极端值存在的时候结果依然受影响,提前处理数据比事后调色阶更省事。另外,如果你发现某个细胞类型的点整体都偏小,可以先确认一下这个群是不是真的表达很低,也许它本身注释就有问题,别急着调参数。
6.4 Seurat版本兼容问题
scDotPlot基于Seurat对象开发,不同Seurat版本内部结构有差异。如果你用的是比较新的Seurat(比如5.x),某些旧版本封装的函数读取对象时可能报错。报错信息一般会指向object@assays或者某个方法名,看到这类报错别慌。
稳妥做法是:调用前先用UpdateSeuratObject()把对象过一遍,让它转成当前版本兼容的结构。如果还报错,就去包的GitHub页面看有没有更新版本,或者提issue的时候把完整报错信息贴上去。我个人的习惯是,生产环境里固定好R和Seurat的版本,至少在单个项目周期内不随意升级,这样能最大程度避免这种"昨天还能跑今天报错"的玄学问题。
6.5 导出图片模糊或者文字重叠
模糊基本是dpi不够或尺寸太小,重叠往往是基因名太长或细胞类型太多。基因名长就旋转x轴文字:
p + theme(axis.text.x = element_text(angle = 45, hjust = 1))细胞类型多就按经验公式增加高度。最后再强调一遍:导出前先放大预览,确认没问题再保存,别等存完了发现重画。我有一段时间总在最后一步翻车,导出到一半发现某个基因名被截断了,然后改参数重新导,反复几次之后才养成放大预览的习惯。这个习惯看起来很小,但真的能省下大量返工时间。
高频问题速查表
| 现象 | 常见原因 | 处理办法 |
|---|---|---|
| 基因没画出来 | 基因名格式不匹配 | intersect检查,对照rownames重写 |
| 顺序不对 | cluster.order未生效或类型名不一致 | table核对实际值再传参 |
| 颜色层次差 | 极端值拉宽色阶 | 自定color范围,预处理表达值 |
| 版本报错 | Seurat新旧版本结构差异 | UpdateSeuratObject,更新包 |
| 图片模糊 | dpi或尺寸不足 | 300dpi,按公式估算画幅 |
我自己用下来的体会是,scDotPlot最大的价值不在于它能画出什么惊天动地的图,而在于它把气泡图这个高频组件做得足够顺手。排序、配色、分组、导出这些琐碎细节,本来每次都值得花时间去调,因为它们直接决定了数据呈现质量;有了这个包之后,你可以把时间省下来,放到真正重要的生物学问题、注释准确性和下游分析上。
最后分享一个小技巧:把常用的marker列表和参数组合整理成一个R脚本,放在你的单细胞分析流程目录里。下次拿到新数据集,改一下对象路径和细胞类型列名就能直接跑,几分钟内就能得到一张能去开组会的气泡图。对经常处理多个单细胞项目的人来说,这个脚本就是你的效率工具,也是我用了这么久最想推荐给同行的一招。