ARTICLE DETAIL

资讯详情

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

R语言ggplot2绘制带连线的堆叠柱状图:单细胞比例趋势可视化

R语言ggplot2绘制带连线的堆叠柱状图:单细胞比例趋势可视化 前阵子帮实验室处理单细胞转录组数据做到细胞类型比例可视化的时候隔壁师弟问了一句“这个堆叠柱状图能不能把CD8T细胞的变化趋势用线连起来”当时我想这不就是bar的基础上再叠一层line的事嘛结果真动手做才发现坑不少。ggplot2里直接往堆叠图上画线线会老老实实从柱顶穿过根本不会停在你想要的那个细胞亚群对应的堆叠段上。今天就把这套带连线的堆叠柱状图完整方案捋清楚从数据准备、堆叠位置计算到最终出图代码全给出来。R语言新手能直接抄作业已经在做单细胞分析的朋友也能少踩几个我踩过的坑。这种图解决的核心问题其实很现实普通堆叠柱状图只能让你看到每个样本里各类细胞占比多少但看不到同一个细胞亚群在不同样本之间到底是升高还是降低。尤其当样本有明确的分组关系比如用药前后、不同时间点、疾病组与健康组你关心的往往是某个T细胞亚群的比例有没有随条件变化。这时候在堆叠柱状图上加一条连线就能把视线一下子引导到对应亚群的变化趋势上。文章里用到的数据来自Seurat的标准分析流程但方法本身完全不挑数据来源你只要有一张“样本、细胞类型、细胞数量”的统计表就够了。1. 需求拆解为什么普通堆叠图不够用1.1 三个典型场景让连线变得必要先说说我在实际项目中遇到过的三种情况基本覆盖了这个图的主要用途。第一种是时间序列分析。我自己做过一批体外分化实验在第0天、第3天、第7天分别取样做单细胞测序。常规堆叠柱状图能看出整体结构变化但你想单独追踪某个细胞亚群比如“杀伤性T细胞从第0天到第7天占比是上升还是下降”就得靠眼睛在柱子上来回比划非常不直观。连线的意义就在这里它把同一个亚群在不同时间点的比例点串起来趋势一眼就出来了。第二种是配对样本对比就是同一个个体或者同一批样本在处理前后的对比。这种情况和对照组实验不太一样样本之间存在天然的配对关系用连线把同一个亚群的前后变化连起来比单纯看两根柱子要准确得多。比如我处理过一批肿瘤样本的用药前后数据CD8T细胞比例从8%升到15%在堆叠图上你其实不太容易注意到但连线会非常清楚地画出这条上扬的折线。第三种是跨样本的组成结构比较比如不同组织部位、不同分组的细胞组成差异。这种场景不强制要求配对但当你关注少数几个关键亚群时连线依然能起到“视觉锚点”的作用。我之前做BCR单细胞分析的时候也想展示不同样本之间某个克隆型的占比变化趋势本质上就是同一套逻辑。甚至你在做微生物组α多样性分析时想比较不同组别之间某个菌门相对丰度的变化趋势这套代码稍微改改列名就能直接用。1.2 为什么选择ggplot2手动画线而不是现成的包其实R语言生态里有好几个现成的方案可以画这种带连线的堆叠图。比如ggalluvial包画冲击图alluvial plot、ggforce里的geom_flow、还有ggstream做流图。这些包做得确实漂亮但我在实际项目中还是更喜欢用ggplot2手动叠加连线层。原因有三点。第一可控性强。ggalluvial的输出样式比较固定想把连线做成实线、把点放在堆叠段的中心、让颜色和填充完全对应它管不了那么细。自己用geom_line和geom_point画每一步都清清楚楚。第二图例管理方便。单细胞分析里的细胞类型少则十几个多则几十个冲积图在这种规模下特别容易糊成一团而堆叠柱状图加上细线的组合在视觉上更干净。第三是性能。几十个样本、几十个细胞类型时手工方案跑起来非常快不会像某些包那样在布局计算上卡半天。当然如果你的数据特别复杂比如细胞类型非常多而且你更关注多条轨迹的“流动感”而不是具体数值对比那就直接上ggalluvial没必要死磕我这种方式。工具选择永远看场景这里给的是大多数单细胞比例分析中最通用的一套做法。2. 数据准备与完整代码实现2.1 从Seurat对象中提取比例矩阵先说数据从哪里来。大多数做单细胞分析的人手上都有一个跑完标准流程的Seurat对象细胞类型注释已经做好了。这时候你想看的通常是不同样本之间细胞类型的比例变化所以第一步是整理出一张统计表每一行是一个样本和一种细胞类型的组合包含细胞数量、占比信息。我用一个简化示例来演示假设你的Seurat对象叫scRNA样本名存在orig.ident里细胞类型注释存在Idents(scRNA)里。library(Seurat) library(ggplot2) library(dplyr) library(tidyr) # 方法1直接从meta.data提取 meta - scRNAmeta.data meta$sample - meta$orig.ident meta$celltype - as.character(Idents(scRNA)) # 统计每个样本、每种细胞类型的细胞数量 prop_df - meta %% dplyr::count(sample, celltype, name Count) %% group_by(sample) %% mutate(Proportion Count / sum(Count) * 100) %% ungroup() head(prop_df)这一步有两点要注意。第一dplyr::count()后面的name Count是给统计出来的列命名避免默认叫n后面调用的时候看着更清楚。第二ungroup()一定要记得加否则后面所有基于样本组的操作都会保留分组状态容易在排顺序或者汇总时踩坑。如果你不是用Seurat而是从其他软件导出数据比如Cell Ranger的filtered_feature_bc_matrix或者从10x官网下载的注释结果也没关系。你只需要在R里整理出这样一个数据框列名包含样本标识和细胞类型标识然后用同样的count()统计就可以。这张统计表是整个可视化流程的输入。2.2 提前确认细胞类型因子顺序防止堆叠和连线错位接下来的这一步是整个方案的灵魂也是我第一次画这个图时踩得最痛的一个坑。ggplot2画堆叠柱状图时堆叠顺序取决于fill对应列因子水平的顺序。而画连线的时候线会按照你给定的group分组连接数据点。如果连线的分组顺序和堆叠顺序不一致就会出现错位线连的点和柱子上对应的色块完全对不上。解决办法是在画图前先把细胞类型的因子水平确定下来然后按照这个水平对数据进行排序让堆叠顺序和数据框里的排列顺序完全一致。我一般习惯先看一下数据里有哪些细胞类型然后指定一个自己想要的顺序。# 查看当前有哪些细胞类型 unique(prop_df$celltype) # 指定你要展示的顺序通常是把关注的重点亚群放到前面 level_celltype - c(CD4_T, CD8_T, NK, B_cell, Mono, DC, Other) prop_df$celltype - factor(prop_df$celltype, levels level_celltype)因子顺序定好之后还有一个细节factor()不会自动帮你排数据行。你需要显式地对数据框排序保证cumsum()计算结果的顺序和ggplot2画堆叠条时的顺序一致。这一步放在计算堆叠位置之前。2.3 手动计算堆叠段的中心位置这里解释一下为什么要手动算“中心位置”。ggplot2画堆叠柱状图时每个细胞类型对应的色块不是从0开始的而是从它下面所有色块的累计高度开始的。比如一个样本里CD4_T占40%它上面叠了NK占20%那NK色块的视觉起点是40%终点是60%。如果你直接在aes(x sample, y Proportion, group celltype)里加geom_line()ggplot2并不知道y应该取色块中间的位置它会直接把Proportion当成y坐标也就是把所有线画到柱顶甚至柱外。所以正确的做法是自己算出色块的下边界、上边界和中心位置然后拿中心位置去画线和点。# 按样本分组细胞类型按因子顺序排列后计算累计值 prop_df - prop_df %% arrange(sample, celltype) %% # 关键是这一步 group_by(sample) %% mutate( CumSum cumsum(Proportion), ymin CumSum - Proportion, ymax CumSum, ycenter (ymin ymax) / 2 ) %% ungroup() head(prop_df)arrange(sample, celltype)这一行的作用就是让同一个样本内部的细胞类型顺序和因子水平顺序一致这样cumsum()算出来的累计高度就是ggplot2画图时色块的累计高度线上线下才能对齐。这一步做完画图的准备工作就齐了。接下来就是纯粹的代码组装。2.4 完整绘图代码从堆叠柱到连线层一次成型正式画图时我分了三层来叠加最底层是geom_bar()画堆叠柱第二层是geom_line()画同一个细胞类型跨样本的连线第三层是geom_point()把每个连线的节点点出来让读者能清楚地看到线条对应的位置。p - ggplot(prop_df, aes(x sample, y Proportion, fill celltype)) # 第一层堆叠柱状图 geom_bar(stat identity, width 0.65, color white, linewidth 0.3) # 第二层连线 geom_line(aes(x sample, y ycenter, group celltype, color celltype), linewidth 0.8) # 第三层节点 geom_point(aes(x sample, y ycenter, group celltype, color celltype), size 2.5) # 坐标轴和主题设置 labs(x Sample, y Cell type proportion (%), fill Cell type, color Cell type) theme_classic(base_size 14) theme( axis.text.x element_text(angle 45, hjust 1), legend.key.size unit(0.6, cm) ) print(p)这段代码有两个地方需要单独说明。第一个是group celltype这个映射它告诉ggplot2线要按细胞类型分组否则ggplot2会以为所有点是一组把不同细胞类型的点连成一个乱七八糟的圈。第二个是color和fill的映射fill控制柱状色块的颜色color控制线和点的颜色理论上它们的内容是一致的所以后面配色的时候也要让两组颜色对齐。跑完这段代码你已经能得到一张带连线的堆叠柱状图了。接下来的工作主要是视觉层面的打磨。3. 配色方案与导出设置图要能直接进论文3.1 单细胞类型多配色千万别用默认色板ggplot2默认的hue_pal()会在细胞类型多的时候给你看图猜谜的机会。超过10个类型的时候邻近颜色几乎分不清尤其是发表在论文里的时候审稿人最烦这种图。所以我在做单细胞比例图时一定会自定义一组色板。一个简单靠谱的方案是直接使用scales包里调好的离散色板或者自己手写一组颜色。如果是做免疫细胞注释通常有约定俗成的颜色习惯比如T细胞偏红系、B细胞偏蓝系、髓系偏绿系。我常用的一个数色板是这样的my_colors - c( CD4_T #E64B35, CD8_T #F39B7F, NK #4DBBD5, B_cell #3C5488, Mono #00A087, DC #7E6148, Other #D3D3D3 )然后用scale_fill_manual()和scale_color_manual()把颜色加进去。注意两个scale要一起加否则柱子的颜色变了连线的颜色还是默认的两张图的图例就对不上了。p - p scale_fill_manual(values my_colors) scale_color_manual(values my_colors)这里有一个小细节细胞类型越多颜色越不能追求“惊艳”反而要选色相差异大、亮度区分明显的颜色。深色背景配色、彩虹色这种看着炫酷的方案只适合PPT不适合数据分析报告。3.2 样本多时调整排序和坐标轴别让图变成“意大利面”样本数量上来之后横坐标的标签很容易挤成一团。我的做法通常有三种第一种是调整坐标轴文本角度。当样本是分组标签比如“Control_1”“Treat_1”这种时45度旋转是最折中的方案既比90度好读又不占太多纵向空间。第二种是调整柱宽。样本多的时候柱宽可以适当调窄比如width 0.5这样柱子之间的间距变大线和点不至于挤在一起。样本少的时候柱宽可以调到0.7甚至0.75视觉上更饱满。第三种是排序。如果你的样本有天然的顺序时间点、剂量梯度直接按照这个顺序对sample列做factor()处理。如果样本是分组数据我习惯把对照组放前面处理组放后面方便读者从左到右看变化。# 设置样本显示顺序 level_sample - c(Control_1, Control_2, Treat_1, Treat_2) prop_df$sample - factor(prop_df$sample, levels level_sample)注意这一步要在前面计算ycenter之前做因为arrange(sample, celltype)会按照因子顺序排列。如果你在画图前才设置样本因子水平数据行的顺序不会自动跟着变一样会有错位风险。3.3 导出图片分辨率、宽度和字号的平衡图做完最后一步是导出。我见过太多人直接用RStudio的Export按钮另存为PNG结果出来的图字小得像蚂蚁。正确做法是用ggsave()指定参数保存。ggsave(celltype_proportion_line.png, plot p, width 8, height 6, dpi 300)如果是投期刊我一般保存PDF格式作为矢量图然后由排版系统自行转换。PDF格式的好处是无论放大多少倍都不糊。PNG格式则适合放在PPT或者Word里预览。宽度和高度的设置要看你横坐标有多少个样本样本少比如只有4个宽高比可以4:3样本多到十几个甚至几十个建议把宽度拉长比如width 12, height 5避免柱子被压成细条。dpi现在基本无脑300起步如果评审要求更高600也行但文件体积会明显变大。字体大小我推荐在theme_classic(base_size 14)的基础上调整正文用12到14号字坐标轴标签用13到15号字图例用12号字左右。这个范围在大多数场景下都是安全值。4. 常见问题与避坑实录4.1 柱子的堆叠顺序和连线的位置对不上这个问题几乎每个照着代码实验的人都会遇到我自己第一次画的时候也没幸免。具体表现是柱状图里红色的色块堆在底部但红色连线的起点却飘在柱状图顶部线与色块完全错位。原因就是我前面强调过的因子顺序和数据行顺序不一致。排查思路很简单画出图之后在数据框里随便挑一个样本看一眼celltype这一列的顺序是不是和图上柱子的堆叠顺序一致。如果不一致回到arrange(sample, celltype)和factor(levels ...)那两步确认两者匹配。这里有个更隐蔽的变体如果你在某个环节用了group_by(sample)之后忘了ungroup()后面再排序的时候排序可能只在你指定的分组内部进行整体顺序还是乱的。我在实际调试中就遇到过这种问题排查了很久才发现是分组状态没去掉。所以我每次会习惯性地在mutate()和arrange()之后加一个ungroup()防患于未然。4.2 细胞类型太多图例密成“天书”单细胞注释结果经常有20到30种细胞类型全画上去图例比图本身还高。我的处理策略有两种按需选择。第一种是合并低频亚群。把所有占比低于某个阈值比如小于1%的细胞类型合并成“Other”。这个操作在统计层面会损失一点信息但视觉上图片的可读性提升巨大。不过要注意合并之后再计算比例时要按样本重新归一化不能让所有样本加起来超过100%。prop_df - prop_df %% mutate(celltype ifelse(Proportion 1, Other, as.character(celltype))) %% group_by(sample, celltype) %% summarise(Count sum(Count), .groups drop) %% group_by(sample) %% mutate(Proportion Count / sum(Count) * 100) %% ungroup()第二种是只展示你关心的几个亚群。比如你研究的就是CD8T细胞在不同状态下的比例变化那就只保留CD8T相关的亚群其他全部折叠成Other。这种做法的好处是图会非常聚焦读者的注意力不会被无关的10几个颜色块分散。另外提醒一点合并操作请在计算ycenter之前完成因为合并改变了比例累计位置必须重新算。4.3 其他高频问题速查我把平时被问得多的问题整理成一个表格方便大家快速对照症状可能原因解决办法所有线都从第一根柱子底部连到最后一根柱子顶部形成一条斜线aes()里缺少group celltype在geom_line()和geom_point()都加上group celltype柱子颜色和图例颜色对不上scale_fill_manual()和scale_color_manual()颜色向量不一致建立一个命名颜色向量同一个向量传给两个scale某个样本的比例加起来不是100%合并“Other”时没有按样本重新归一化在summarise()之后重新group_by(sample)计算比例连线穿过柱顶或飘在柱子外面没有手动计算ycenter直接用Proportion作为y按文中步骤计算ymin、ymax、ycenter横坐标标签重叠严重样本名太长旋转45度、缩短样本标签或者增大图的宽度图线条粗细不合适单一样本只有一个点时线显示不出趋势样本少于3个时不建议加连线只保留点即可还有一个很多人会忽略的点如果在geom_bar()里加了color white作为柱子描边那color这个属性就被占用了。这时候再在aes()里映射color celltype会冲突吗不会因为color white是固定值aes(color celltype)是映射两者处理逻辑不同ggplot2是允许同时存在的。这个组合在实际效果上就是彩色柱状图带白色描边同时连线也按细胞类型着色视觉层次很清楚。4.4 关于“线”的数量与样本数的取舍我最后想专门聊一下连线的适用条件。如果你的样本只有两个也就是只有两个堆叠柱那加连线其实是画一条从左边点连到右边点的直线信息量有限但也不算多余。如果样本超过10个比如做时间序列连线就非常有价值它能把趋势直接“画”出来。但如果样本是三个、四个这种中间状态同时细胞类型又有20个以上整张图的连线会变成一团毛线。这种时候我通常会把“Other”合并得狠一点只保留8到10个主要亚群再去画连线。这是一个需要看图说话的经验判断没有绝对的阈值标准。我在实操中的习惯是先画一版全量的图再画一版过滤后的图两版都看一遍。如果全量版的连线已经乱到影响判断趋势果断用过滤版如果全量版还能接受就用全量的毕竟信息完整度还是越高越好。另外如果是要发文章或做正式汇报我强烈建议在图上额外标注样本的分组信息用分面facet_wrap或者色块背景都行。否则读者看到两根柱子之间的连线可能会误以为这是一种统计检验的标记实际并不是。连线只是视觉辅助工具不代表显著性也不代表线性关系提前在图注里说明清楚能省掉不少解释的麻烦。最后再分享一个小技巧如果你用的是RStudio画完图之后先用p变量直接在Viewer里看一眼大小比例再决定要不要调柱宽和图片宽度。我的经验是很多时候你觉得图“挤”不是配色问题而是图太窄了。把这个顺序调过来能省下不少反复导出的时间。
返回列表