拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Seurat小提琴图腰围调整全攻略:从width到adjust的底层原理与实战

Seurat小提琴图腰围调整全攻略:从width到adjust的底层原理与实战

在单细胞转录组分析里,小提琴图(VlnPlot)大概是出镜率最高的可视化之一。很多人在群里问"怎么让Seurat3.0的小提琴图腰细一点""怎么控制小提琴图的宽度""这个腰围参数在哪调",其实问题本质都是对VlnPlot底层绘图逻辑不太熟。今天就把这块一次性讲透,读完你不仅能自由控制小提琴图的"腰围",还能理解每个参数背后到底在改什么。

1. 小提琴图的"腰围"到底指什么,先搞清楚原理

1.1 小提琴图是怎么画出来的

很多人用小提琴图用了大半年,但没想过它背后的绘制机制。小提琴图本质上是"箱线图 + 核密度图"的合体:先把某个基因在某一组细胞里的表达量分布做成核密度估计曲线,然后沿着纵轴方向把这条曲线左右对称地"糊"出来,中间再叠一个箱线图或中位数标记。

也就是说,你看到的那个像小提琴一样的轮廓,不是凭空生成的,而是每个表达量取值对应的细胞密度。密度越高,那个位置的"琴肚"就越鼓;密度低的地方,琴体就收窄,形成"腰"。

这就能解释很多调参现象了。你改的"腰围",实际上是在改两条东西:一是琴体整体占多宽,二是核密度估计本身的平滑程度。前者是几何层面的缩放,后者是统计层面的形状改变,很多人只盯着前者,忽略了后者。

1.2 控制"腰围"的底层参数:width和adjust

在Seurat里,VlnPlot最终调用的是ggplot2的geom_violin()图层。这个图层有两个跟"腰围"最直接相关的参数:

  • width:直接控制小提琴图在x轴方向上的占位宽度。默认值是0.8左右,意思是一组数据最大能占x轴单位宽度的80%。你把它改成0.4,琴体就整体瘦一圈;改成1.2,琴体就明显变胖。这就是最直观的"腰围"参数。

  • adjust:控制核密度估计的带宽调整系数。默认值是1,调大(比如改成1.5)会让密度曲线更平滑,琴体边缘更圆润,"腰身"过渡更柔和;调小(比如0.5)会让曲线保留更多细节,琴体可能呈现出更多锯齿状的小起伏,显得更"瘦削"。

注意区分这两个参数的作用:width是"等比缩放",改了之后整个琴体包括腰、肚、颈都按比例变宽或变窄;adjust是"重新塑造形状",它改变的是核密度曲线的平滑程度,间接影响到腰的位置和弧度。实际使用中经常需要组合调整,单改一个往往达不到理想效果。

1.3 为什么有人调width参数"没反应"

这是最常见的问题,后面排查部分会细讲,但这里先提一句。Seurat的VlnPlot函数有一个参数传递机制:它允许通过...把额外的参数传递给底层的geom_violin()。但如果你用的是Seurat 3.0系列的VlnPlot(),直接写width = 0.4是有可能被Seurat内部的某些逻辑吞掉的。

因为VlnPlot在绘图时,针对是否使用split.by、是否有stack等不同情况,会分别调用不同的绘图分支,有的分支会把额外的参数原样传给geom_violin,有的分支则不会完整传递。这就导致同一个写法的参数,在不同模式下表现不一样。后面我会把每种场景对应的传参方式列清楚。

实际工作中建议多使用geom_violin的方式手动追加参数,或者用VlnPlot()套一层p$layers修改已有的图层参数,这样比单纯传参数要可靠得多。

2. VlnPlot中能够控制腰围的几个入口

2.1 最基本的VlnPlot参数:pt.size、y.max、cols

先看Seurat 3.0最常见的基础用法:

VlnPlot(object = pbmc, features = "CD3D", pt.size = 0.01)

这里面的核心参数有:

  • pt.size:控制小提琴图内部散点的大小,默认是1。如果细胞数量非常多,建议设成0.01甚至0.001,否则密密麻麻的点和琴体糊在一起,你根本看不出腰围的形状。如果不想显示散点,直接pt.size = 0。

  • y.max:控制y轴的显示上限。这个参数看起来跟腰围无关,但实际作用非常大——它决定了琴体在纵向上显示到哪个位置。如果某个细胞亚群有极端高表达,默认的y轴范围会被拉得很大,琴体就会显得又矮又扁,视觉上"腰围"被压缩。手动调低y.max可以让琴体比例恢复正常,但注意这样会截断超出部分的数据显示,属于视觉取舍。

  • cols:控制颜色。严格说跟腰围无关,但影响观感。深色配色比浅色更显"胖",因为高对比度让轮廓更突出。

这3个参数是入门级,但很多人忽略了pt.size对腰围视觉的间接影响。数据点越多、点越大,琴体的边缘就被遮得越多,看起来越粗糙,腰线越不明显。所以想要一张"腰身明显"的图,第一步就是把点调到不抢镜为止。

2.2 用split.by画分组小提琴图时的腰围陷阱

split.by是在Seurat里做对照组比较时常用的方式。比如你想看CD3D基因在"处理组vs对照组"两种条件下的表达差异,可以这样:

VlnPlot(object = pbmc, features = "CD3D", split.by = "group", pt.size = 0.01)

这个模式下,每个细胞亚群会被拆成两个并排的小提琴,对应的x轴空间被一分为二。这时候"腰围"问题就变得非常突出:

一是每一半的琴体会被压缩。因为原本一个细胞类型占据一个x轴单位宽度,现在要挤两个小提琴进去,如果width参数不跟着调整,就会出现两半琴体挤在一起、胖瘦失衡的情况。

二是分组间样本量不平衡时,琴体粗细会误导判断。比如处理组只有50个细胞,对照组有500个细胞,默认情况下核密度估计会按照各自的实际分布来塑形,但琴体的整体宽度是固定的,所以视觉上两组琴体一样宽,容易让人误以为细胞数量相当。这时我通常手动调整width,比如width = 0.4,让每组的小提琴宽度明显小于1/n,之间留出间隔,提示读者这是两个独立的分布。

三是split.by和adjust配合使用时,容易出现腰身中断。当某一分组内细胞数太少或者表达量分布有多个峰时,核密度曲线会产生多处凹陷,琴体看起来像是被腰斩了几段。这种情况下适当调大adjust参数(比如adjust = 1.5或更高)可以平滑掉一些噪声,让腰身恢复连贯。

2.3 通过...向底层geom_violin传参

Seurat的VlnPlot支持通过...传参给geom_violin和geom_boxplot。这是控制腰围最关键的一条通道。实际代码可以这样写:

# 直接传width和adjust VlnPlot(pbmc, features = "CD3D", pt.size = 0, cols = c("#E64B35", "#4DBBD5"), width = 0.6, adjust = 1.2)

不过这里要注意版本兼容性问题。在Seurat 3.0里,...传递机制在部分场景下不够稳定。我自己实测过,当加了split.by参数时,width不一定能穿透到geom_violin里。保险的做法是用ggplot2的图层修改方法:

p <- VlnPlot(pbmc, features = "CD3D", pt.size = 0) p$layers[[1]]$aes_params$width <- 0.5 p$layers[[1]]$aes_params$adjust <- 1.5 p

这段代码的意思是:先把VlnPlot画成一个ggplot对象,然后直接修改第一个图层(也就是geom_violin)的美学参数。这样绕过了Seurat的参数传递机制,改的是ggplot2图层本身的属性,效果更可控。

如果是用VlnPlot之后再叠加自定义几何对象,还可以直接把p当作一个普通的ggplot对象继续操作,比如p + geom_violin(width = 0.6, adjust = 1.5),但注意这样会重复叠加琴体,不推荐,除非你先把原来的图层删掉。

2.4 补充参数:alpha、trim、scale对腰围的间接影响

除了width和adjust,还有几个参数虽然不是直接控制腰围,但会间接改变琴体的视觉效果:

  • alpha:琴体的透明度。默认是1(完全不透明),如果设成0.6~0.8,琴体边缘会比较透明,看起来轻盈一些,腰线更通透。这个参数尤其适合在需要叠加分组、对比多个基因时使用。

  • trim:是否裁剪掉核密度估计的末端尾巴。默认是FALSE,意思是琴体的上下两端会延伸到数据范围之外(延伸到核密度估计的支撑范围)。如果设为TRUE,琴体就会在数据的最小值和最大值处截断,看起来上下收得更紧,"腰"相对会更明显。

  • scale:控制琴体宽度的缩放方式。"area"表示每个琴体的面积与其观测数量成比例;"count"表示琴体宽度与样本量成正比;"width"表示所有琴体等宽。默认在geom_violin里是"area"。这个参数很容易被忽略,但它对"腰围"的影响非常大——如果你希望每个组的小提琴体现出样本量差异,用"count";如果只是想看分布形态差异,用"width"让所有琴体一样宽,比较公平。

3. 实操:从单细胞数据到一张腰围合适的图

3.1 准备演示数据

空讲没什么感觉,直接上一个完整的实操流程。这里用Seurat自带的PBMC示例数据来演示。先跑通基本流程:

library(Seurat) # 加载示例数据 pbmc <- readRDS(url("https://cf.10xgenomics.com/samples/cell/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz", method = "libcurl")) # 这部分通常需要先解压,也可以直接用SeuratData包 # 如果网络不方便,可以直接用Shiny或本地rds文件替代

用SeuratData包会更稳妥:

library(SeuratData) InstallData("pbmc3k") pbmc <- LoadData("pbmc3k") pbmc <- NormalizeData(pbmc) pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000) pbmc <- ScaleData(pbmc) pbmc <- RunPCA(pbmc) pbmc <- FindNeighbors(pbmc, dims = 1:10) pbmc <- FindClusters(pbmc, resolution = 0.5)

这里我额外建议,在画图之前先明确你想要的"腰围"效果。是要每个cluster内部独立比较?还是要突出不同cluster之间的差异?还是关注某个基因在特定条件下的表达模式?目标不同,参数组合差别挺大。

3.2 场景一:只想把琴体调瘦一点

如果细胞数特别多(比如10万+),默认的琴体会显得非常臃肿,每个cluster之间的琴体几乎贴在一起。这种情况我是这样调的:

p <- VlnPlot(pbmc, features = "CD3D", pt.size = 0, cols = colorRampPalette(brewer.pal(9, "Set1"))(length(levels(pbmc$seurat_clusters)))) p$layers[[1]]$aes_params$width <- 0.4 p$layers[[1]]$aes_params$adjust <- 1.1 print(p)

width = 0.4就是最直接的"腰围"控制。从默认的0.8降到0.4,琴体瘦了一半,cluster之间空出了清晰的间隔。同时adjust = 1.1保持核密度相对紧凑,不会因为过度平滑丢掉细节。

实测下来,当细胞数超过5万个时,建议把width控制在0.3~0.5之间,这样既不会糊成一团,也不至于细得不像小提琴图。

3.3 场景二:分组对比时两边宽度保持一致

如果是用split.by做分组比较,我倾向于先画一版默认的,再手动调:

pbmc$group <- sample(c("ctrl", "treat"), ncol(pbmc), replace = TRUE) p <- VlnPlot(pbmc, features = "CD3D", split.by = "group", pt.size = 0, cols = c("#E64B35", "#4DBBD5")) # 查看图层结构 summary(p$layers[[1]])

在split.by模式下,Seurat会自动用position = position_dodge(width = ...)并排两个琴体,这个dodge的宽度决定了两个琴体之间的距离。实测下来,直接改$aes_params$width往往只能改变琴体本身宽度,但两个琴体的相对位置不会自动适应。

更靠谱的做法是直接重新定义图层的position和width:

library(ggplot2) p$layers[[1]]$position <- position_dodge(width = 0.2) print(p)

这里width = 0.2的意思是:两个琴体在x轴方向上错开的幅度是0.2个x轴单位。如果这个值太小,两半琴体几乎重叠;太大,琴体之间间隔过宽。我一般从0.2开始试,再根据图形调整。

3.4 场景三:想让琴体上下"收腰"

如果基因表达分布集中在某个区间,而上下的尾巴拖得很长,琴体看起来会像个细长的花瓶,"腰"很不明显。这时有两个处理办法:

一个是trim = TRUE,直接裁剪掉超出数据范围的密度曲线部分:

p <- VlnPlot(pbmc, features = "CD3D", pt.size = 0) p$layers[[1]]$aes_params$trim <- TRUE print(p)

另一个是调大adjust,让核密度估计的带宽变宽:

p$layers[[1]]$aes_params$adjust <- 2 print(p)

这两个方法的效果不太一样。trim = TRUE是让琴体的上下端点收在数据的最值处,视觉上更干净;adjust = 2则会让琴体整体更圆润,腰线更平滑。我习惯两个同时用,出来的图既干净又有形态。

但注意,trim = TRUE在某些极端情况下会让琴体顶端出现一条平线,尤其是当数据里有较多重复相同最大值时,看起来不太自然。如果遇到这种情况,保留trim = FALSE只调adjust反而更合适。

3.5 把多张图拼版时,注意腰围视觉统一

在实际发文章的过程中,很少只画一张小提琴图。经常是一组基因排在一起,比如CD3D、CD8A、CD79A这些marker基因,用CombinePlots或patchwork拼成一张大图。

p1 <- VlnPlot(pbmc, features = "CD3D", pt.size = 0) p2 <- VlnPlot(pbmc, features = "CD8A", pt.size = 0) p3 <- VlnPlot(pbmc, features = "CD79A", pt.size = 0) p1$layers[[1]]$aes_params$width <- 0.5 p2$layers[[1]]$aes_params$width <- 0.5 p3$layers[[1]]$aes_params$width <- 0.5 wrap_plots(p1, p2, p3, ncol = 1)

这里最容易踩的坑是:每张图的y轴范围可能不同,导致琴体在纵向上的"胖瘦"观感不一致。一个基因表达量范围是0~4,另一个是0~10,如果各自用默认的y轴,第一张琴体会被拉得很瘦长,第二张则显得矮胖。即使width设置一样,视觉上"腰围"还是不统一。

解决办法是设置y.max统一y轴上限,或者把same.y.lims = TRUE传入(seurat3.0没有这个参数,可以用scale_y_continuous(limits = c(0, max))实现),这样多个基因的琴体就有可比性了。

另一个实测技巧:拼图时调整adjust参数让分布更平滑,可以减少不同图之间的视觉噪音。我一般对要做拼图的基因统一用一套参数,比如width = 0.5、adjust = 1.2、pt.size = 0、trim = TRUE,这样出来的图风格高度统一。

4. 高频问题与排查技巧实录

4.1 为什么改了width参数琴体没有任何变化

这是群里问得最多的一个问题。通常有三个原因:

  • 版本兼容问题:如前面提到的,Seurat 3.0某些分支下width传不到geom_violin层。检测方法是先画一个图,然后用p$layers[[1]]$aes_params看看里面有没有width这个字段。没有的话,说明你的参数被吞了。解决办法是用p$layers[[1]]$aes_params$width <- 0.5直接修改。

  • split.by模式下width被覆盖:用了split.by时,Seurat内部的position设置会覆盖width。你需要同时改position = position_dodge(),参考上面3.3的写法。

  • 忘记重新渲染:改完aes_params之后必须重新print(p)或执行这个对象,而不是看之前的图片。我见过有人改完参数忘了重跑,还在问为什么没变化。

4.2 split.by之后琴体挤压成一团

这个问题在分组数多的时候特别严重。比如有10个cluster,每个cluster再拆成2个处理组,x轴上就要挤20个琴体。默认状态下,琴体之间几乎没有间隔,看起来就是一整块色块。

我的解决方案是:

p <- VlnPlot(pbmc, features = "CD3D", split.by = "group", pt.size = 0) p$layers[[1]]$aes_params$width <- 0.3 p$layers[[1]]$position <- position_dodge(width = 0.5) print(p)

关键在于position_dodge的width要比琴体width大,这样每组之间的两个琴体才有明显的间隔。我通常设定width = 0.3、dodge = 0.6,也就是琴体宽度占组内空间的一半,留出一半的间隙,视觉上最清爽。

4.3 细胞数太少导致琴体形状异常

当你画的某个亚群只有30个细胞时,核密度估计的结果非常不稳定,琴体可能呈现出尖刺状,或者多处凹陷,甚至直接是一条细线,完全看不出"小提琴"的样子。

处理办法:

  • 调大adjust,比如从1调到2或2.5,让密度曲线更平滑。
  • 使用trim = TRUE避免上下拖尾。
  • 如果还是不行,考虑用geom_boxplot替代,或者加上箱线图叠加(Seurat默认叠加箱线图,Version3.0默认加,但我经常去掉)。
  • 另外可以检查一下该细胞群是否真的需要单独展示。细胞数太少时,即使画出来了,统计分析意义也不大,不如合并或者注明样本量。

4.4 导出PDF或PNG时腰围出现锯齿

这个跟腰围本身无关,但导出时经常暴露问题。小提琴图的边缘本来就是核密度曲线的拟合结果,在低分辨率导出时容易出现锯齿感。

建议直接导出PDF矢量图:

ggsave("vlnplot.pdf", plot = p, width = 8, height = 6, dpi = 300)

如果必须要PNG,dpi至少要300,同时可以把adjust适当调大,让曲线更平滑,锯齿感会减轻不少。

另外,PDF导出后如果遇到文字乱码,一般是中文字体问题。建议图片内不要用中文标签,全部换成英文,导出后再在AI或PPT里加注释。

4.5 Seurat小提琴图参数速查表

整理一份我常用的参数速查表,方便直接对照:

参数作用常用值备注
pt.size内部散点大小0 / 0.01细胞多时必须调小
width琴体宽度,最核心的腰围参数0.3~0.6直接改aes_params最稳
adjust核密度平滑程度1~2影响腰线平滑度
trim是否裁剪尾部TRUE/FALSETRUE更干净
cols颜色向量自定义影响视觉层次
y.maxy轴上限自定义可统一各组基准线
alpha琴体透明度0.6~0.9叠加时常用
scale宽度缩放方式"width" / "count"影响组间宽度关系
dodge分组时琴体错开距离0.2~0.6配合width使用

5. 关于参数的一些容易混淆的细节

5.1 width和scale的区别

这两个是Visual效果最像但语义完全不同的参数。width是绝对的几何宽度,单位是x轴坐标单位;scale是不同琴体之间的相对宽度策略。如果设scale = "width",所有琴体最终会等宽,此时width设置多大,所有琴体就是多宽;如果设scale = "count",细胞数多的组琴体会更宽,细胞数少的组会更窄,此时width变成了基准值,实际宽度会乘以样本量相关的系数。

这在小提琴图用来展示不同亚群细胞数量差异时非常有用,但如果你只想比较表达分布形态,建议设为"width",不然不同组样本量的差异会干扰视觉判断。

5.2 split.by和scale的交互

当使用split.by时,scale的默认行为会让同一cluster内不同split组按照各自的样本量比例来决定宽度。这看起来挺合理,但有时候会造成两半琴体宽度差异巨大,读者会误以为表达丰度有差异。

如果想避免这种误导,建议显式设置scale = "width",强制两半琴体等宽,只保留分布形态的差异。

5.3 不同Seurat版本的VlnPlot差异

Seurat 3.0和4.0在VlnPlot上的实现有细微区别。3.0使用的是SingleDimPlot内部逻辑,4.0之后做了重构。如果你是从4.0或5.0的教程转到3.0,建议小心参数传递的差异。最稳妥的做法是不依赖参数穿透,直接对返回的ggplot对象做图层修改,这在任何版本都通用。

另外,在Seurat 3.0里,VlnPlot默认会叠加一个geom_boxplot在琴体内部(width很小,只有一条线),如果你觉得它挡住了腰线,可以通过p$layers[[2]]找到箱线图图层并删掉:

p$layers[2] <- NULL print(p)

6. 我个人的调参心得

最后说点实际体验。我最初也被这个"腰围"问题折腾过一阵。一开始以为调调width就完了,后来发现不同的细胞类型、不同的基因分布、不同的分组结构,最佳参数差异非常大。

我现在的基本工作流是:先用默认参数快速跑一张总览图,看看哪些琴体太胖、哪些太瘦、哪些糊在一起;然后针对具体问题分别处理——全局调整width控制整体形态,局部调整adjust处理因为细胞数过少导致的形状异常,最后统一用scale = "width"保证组间可比性。

还有一个小技巧是,如果画出来的图还是觉得不理想,不要一直纠结参数,先检查一下数据质量:是不是某个细胞亚群混入了异常值?是不是NormalizeData的scale factor设置有问题?有时候"琴体腰围"异常,其实是数据本身有异常。先把数据清洗干净,再来调图,往往事半功倍。

我在实际工作中还经常遇到一种情况:有人希望小提琴图"越细越好",觉得细的图显得精致。但我一般不建议把width调得太小,尤其是论文里要展示的图。琴体太细的时候,内部的箱线图或者中位数标记会被挤压得看不清,反而丢失了信息量。找到一个平衡点最重要,我通常控制在默认值的50%~75%之间,既能清晰展示分布形态,又不显得臃肿。

另一个容易被忽视的细节是,如果你后续要叠加上统计检验结果(比如某种显著性标记),琴体的宽度会影响标记的位置。太窄的时候,显著性星号或者括号会和小提琴边缘重叠,这时候要把geom_signif之类的标记位置适当调整,或者把width放宽一点。这些都是在实际出图过程中才会遇到的坑,提前知道能少走不少弯路。

Seurat3.0的小提琴图调参,说穿了就是理解ggplot2图层逻辑再加上一点实操经验。搞清楚"腰围"对应的底层参数后,剩下的就是多画几版对比着看,慢慢就会找到自己习惯的那套参数组合。

返回列表