做单细胞转录组的人几乎都遇到过这个场景聚类跑完UMAP也画出来了clusters都标好了然后项目组里抛过来一个问题——“我们感兴趣的那个基因到底在哪群细胞里能不能在UMAP图上一眼标出来”我第一次被问的时候第一反应是画个FeaturePlot然后指着颜色深的地方说“在这”。事后发现这个回答既不够严谨也不够专业。因为UMAP上颜色深的位置可能只是少数极端高表达的细胞而真正特异性表达这个基因的类群可能是cluster 3和cluster 7合并以后才对应某类细胞也可能在这两个cluster里只有一半细胞是阳性。单纯画一张表达量图和把“基因对应的类群”明确地标识出来其实是两件事。这篇文章就围绕这个具体需求展开。我会从UMAP图上的“类群”到底是什么讲起然后给出从FeaturePlot到真正“标注出目标类群”的完整流程包括手动圈选、基因集打分、细胞类型注释、批量输出以及最后出图时那些影响观感的小细节。内容以Seurat为主也会对照一下用Python做单细胞时scanpy的对应做法。适合正在做单细胞数据分析、需要把目标基因和特定细胞类群对应关系讲清楚的读者参考。1. 标注基因类群前先想清楚UMAP图上“类群”到底指什么很多新手拿到单细胞数据就直接画UMAP看到上面一团一团的点就自然把它们当成“细胞类型”。这个直觉大方向没错但它忽略了一件事UMAP本身不负责定义类群它只是把每个细胞的高维表达向量压缩到二维坐标上供人观察。你看到的“一团”是由你事先做好的聚类结果cluster或者进一步注释出的细胞类型cell type决定的。1.1 每个点是一颗细胞不是基因单细胞转录组表达矩阵的基本结构是行是基因列是细胞。UMAP上的每一个点代表一个细胞这个细胞在数万个基因维度上的表达谱被降维算法压缩成了二维平面上的一个坐标。所以当你问“这个基因在UMAP的哪个位置”时严格来说是“这个基因在哪些细胞中高表达而这些细胞在UMAP的哪个区域”。这个区别不是文字游戏它直接影响画图方式。基因本身没有位置只有细胞有位置。我们通常把基因表达量映射到细胞的颜色上让高表达的点呈现深色然后肉眼寻找那些深色点聚集的区域。但一张图里同时有数万个点点与点之间还会重叠光靠肉眼常年会误判。1.2 “基因所在类群”的两种常见理解在单细胞项目里刚才那句“我们感兴趣的基因在哪群细胞里”其实可能指两种不同的需求需求类型典型问法核心操作A. 基因高表达于哪个类群“CD8A是不是T细胞特异表达的”观察某个cluster或细胞类型中该基因阳性细胞比例、平均表达量B. 把表达该基因的细胞标成一个群体“能不能把表达IFNG的这些细胞在UMAP上圈出来”按表达阈值取细胞子集或手动圈选然后高亮展示需求A是常规marker验证FeaturePlot加一个按类群统计的表达量表基本就能回答。需求B更接近“标注类群”本身——你需要把满足某种条件的细胞集合在UMAP上用一种醒目的方式标识出来让读者一眼看到这个群体在哪里、边界大概如何。建议在动手之前先明确是哪种需求。我见过不少人在需求B的场景下只画了FeaturePlot结果别人盯着图问“那到底哪些细胞算你目标的群体”答不上来。反过来如果是需求A画太多花哨的高亮图反而显得刻意。1.3 类群粒度先确认你标的是cluster还是cell type单细胞聚类中一个基本参数是分辨率resolution。同一份数据resolution设0.2可能得到5个cluster设0.8可能得到12个cluster设1.5可能得到20个以上。这意味着“类群”不是数据里的天然标签而是你通过参数选择构造出来的分组。这带来一个实际操作中很关键的问题如果直接拿未注释的cluster编号作为类群最后得到结论往往是“目标基因在cluster 0、2、5中高表达”。这种表述没有生物学意义因为cluster 0、2、5可能只是同一个细胞类型被分辨率切成了三块也可能cluster 0里混了两种不同的细胞类型。我的习惯是在回答“基因对应哪个类群”之前先完成一轮细胞类型注释至少要把cluster归并成有生物学意义的类型比如T细胞、B细胞、单核细胞、NK细胞这样的水平。否则后面所有标注操作都建立在不可靠的“类群”定义上图画得再漂亮也站不住脚。这一步可以用已知marker手工注释也可以配合SingleR之类的自动注释工具具体做法后面章节会展开。2. FeaturePlot显示表达量但距离“标出基因所在类群”还差一步FeaturePlot是单细胞可视化里最常用的函数没有之一。它做的事情很简单把某个基因的表达量映射到UMAP点的颜色上。但它展示的是“表达量的连续分布”而不是“类群的边界”。很多人用错它是因为默认FeaturePlot就是“标识基因类群”的最终答案。2.1 基础参数颜色、点大小、绘制顺序先给一个我日常用的FeaturePlot版本FeaturePlot(seu, features CD8A, pt.size 1.2, cols c(grey90, firebrick3), order TRUE)这个配置有几个值得解释的地方。cols控制颜色范围默认配色是浅灰到蓝色我习惯改成灰色到红色因为人眼对暖色更敏感高表达点更突出。order TRUE很关键它的作用是让表达量高的点后画也就是画在最上层避免被周围大量低表达或零表达的点盖住。如果不设这个参数在细胞密度大的区域高表达点可能被灰色点遮挡看起来好像那个类群没有信号。pt.size控制点的大小一般不用太大1到1.5在横版图中比较合适。如果细胞总数很多比如超过5万个点可以适当减小到0.8左右不然图会糊成一片。2.2 判断“基因在哪个类群表达”不能只靠肉眼FeaturePlot画完很多人会凭颜色深浅下结论。这在探索阶段没问题但如果要写进报告或者论文一定要有一个量化过程。因为单细胞表达数据极度稀疏许多基因在某个细胞类群中可能只有少数细胞高表达平均表达量被大部分零表达细胞拉低肉眼看到的“某处有一小撮红点”不一定代表这个类群整体富集目标基因。我常用的做法是拉出表达矩阵按cluster或cell type统计阳性细胞比例library(Seurat) # 拉出目标基因表达量并附上cluster信息 expr_df - FetchData(seu, vars CD8A) expr_df$cluster - Idents(seu) # 按cluster统计平均表达和阳性比例 summary_by_cluster - do.call(rbind, lapply(levels(Idents(seu)), function(cl) { sub - expr_df[expr_df$cluster cl, ] data.frame( cluster cl, mean_expr round(mean(sub$CD8A), 4), pct_positive round(mean(sub$CD8A 0), 4) ) })) print(summary_by_cluster)这里用mean(sub$CD8A 0)统计阳性比例比单纯看平均表达量稳得多。原因是单细胞数据里大量dropout事件——一个基因在某个细胞中确实有转录但因为测序深度不够没有被捕获到表现为0。所以阳性比例更接近生物学意义的“这个类群有多少比例细胞在表达该基因”。如果你把Idents(seu)换成已经注释好的细胞类型这一步可以直接输出“目标基因在各细胞类型中的阳性比例表”。做完这个统计你才会真正知道目标基因是特异性表达在某个类群还是仅仅在那里高了一点甚至只是少数异常细胞的信号。2.3 从“表达图”到“类群标签图”怎么过渡假设你已经确定了目标基因的特异性类群下一步是把结论变成图上可读的标签。至少有两种做法一种是直接把Idents改成细胞类型名称然后画DimPlot。这时候UMAP上每个类群边上有文字标签你可以在图注里写明“表达CD8A的细胞主要对应图中T细胞类群”。这是学术发表里最常见的呈现方式。另一种是保留cluster编号但只高亮目标类群比如把目标cluster的颜色设为红色、其他全部置灰。这对内部讨论很直观但对不了解你数据的人来说cluster编号没有语义需要搭配注释信息。关键提醒FeaturePlot展示的是连续的表达量而“类群标注”是离散的分类信息。从连续到离散中间必须经过一个判断步骤——要么人为定义阳性/阴性阈值要么基于统计结果指定目标类群。跳过这个步骤直接画图就很容易出现“图很漂亮但结论模糊”的情况。3. 把目标类群真正圈出来手动圈选、打分筛选、半自动注释如果FeaturePlot只是“看”那接下来的操作才是真正的“标”。根据目标群体的特征不同我一般用三种方式手动圈选、基因集打分、结合细胞类型注释。它们不是互斥的项目里经常组合使用。3.1 手动圈选CellSelector适合跨cluster的目标群体手动圈选适合一类特殊场景目标细胞并不是一个完整cluster而是散落在两三个cluster里但你根据基因表达或其他特征能看出它们是一个连续的群体。这种情况用自动聚类重新分组反而费劲直接在UMAP上圈更高效。Seurat提供了一个交互式圈选函数CellSelector# 先画一张基础UMAP p - DimPlot(seu, label TRUE) # 弹出一个可以自由圈选的窗口 seu - CellSelector(plot p, object seu)运行后会打开一个交互式图形窗口你可以按住鼠标在UMAP上画任意形状把目标细胞圈进来。圈选结束后被选中的细胞在Idents(seu)里会被赋予一个名为“selected”的新身份你可以把它重命名为目标群体名称比如levels(Idents(seu)) # 其中新出现了一个selected身份 Idents(seu)[Idents(seu) selected] - CD8A_positive手动圈选最大的坑是主观性太强。不同人圈的边界可能差很多。所以我不建议单独依靠圈选得出可靠结论。圈完之后一定要做验证至少要看这些细胞的目标基因表达分布和已知marker表达情况确认你圈出来的细胞确实是目标群体。我一般会用VlnPlot比较圈选群体和非圈选群体的目标基因表达量差异不显著就说明圈错了。3.2 基因集打分单基因有噪声用一组基因更稳另一个常见情况是“感兴趣的基因”其实只是一类基因中的代表。比如你想标出“细胞毒性T细胞”类群只拿CD8A画图有时候不够因为CD8A可能是其他免疫细胞的低水平marker。更稳的做法是选一组已知的细胞毒性相关基因比如CD8A、GZMB、PRF1、IFNG做基因集打分然后基于分数来圈定目标细胞。Seurat里最方便的是AddModuleScorecytotoxic_features - list(c(CD8A, GZMB, PRF1, IFNG)) seu - AddModuleScore(seu, features cytotoxic_features, name CytotoxicScore) FeaturePlot(seu, features CytotoxicScore1, pt.size 1.0, cols c(grey90, firebrick3), order TRUE)打分完成后每个细胞会得到一个综合分数代表这组基因在该细胞中的整体表达水平。你可以在UMAP上观察分数分布并选择阈值来定义“阳性细胞群”。阈值怎么定我通常先看分数的直方图或分位数quantile(seu$CytotoxicScore1, probs c(0.5, 0.75, 0.9, 0.99))如果目标类群是在背景中明显分离的分数分布会呈现双峰选峰谷即可如果没有明显双峰可以选0.9分位数作为高表达阈值。阈值定了之后用WhichCells筛选细胞子集再高亮到UMAP上这就是“标出目标类群”了。除了AddModuleScore工具链里还有两个常用的打分方法UCell和AUCell。简单来说AddModuleScore是取基因表达量的平均值计算快但容易受个别高表达基因影响UCell基于秩次统计对基因数多寡和饱和度更鲁棒AUCell基于AUC值判断基因集在细胞中的富集情况适合大型基因集。日常标记一个小基因集AddModuleScore够用基因集有几十个基因时我更推荐UCell# UCell包先安装 # BiocManager::install(UCell) library(UCell) seu - AddModuleScore_UCell(seu, features cytotoxic_features, name CytotoxicUCell)打分结果和AddModuleScore类似但得到的分数解释性更稳尤其适合在不同样本间比较时使用。3.3 半自动注释细胞类型让UMAP上的标签有生物学含义如果你想标出的“类群”其实是某种细胞类型那最应该做的是先完成细胞类型注释再回到基因可视化顺序别搞反。手工注释的流程一般是找一组每个细胞类型对应的已知marker用DotPlot或VlnPlot检查它们的表达模式然后把每个cluster归因到对应的细胞类型。举个例子假设你怀疑数据里有T细胞、B细胞、NK细胞、单核细胞可以构造一个marker集合markers_to_check - list( Tcell c(CD3D, CD3E), Bcell c(MS4A1, CD79A), NK c(NKG7, KLRD1), Monocyte c(LYZ, CD14) ) DotPlot(seu, features markers_to_check, group.by seurat_clusters) RotatedAxis()看每个cluster在哪些marker上特异性高表达然后将cluster号重命名为细胞类型名new_ids - c(T细胞, B细胞, NK细胞, 单核细胞, T细胞) names(new_ids) - levels(seu) seu - RenameIdents(seu, new_ids)修改Idents之后再画DimPlot(seu, label TRUE)UMAP类群的标签就从“0、1、2、3、4”变成了可读的“T细胞、B细胞、NK细胞、单核细胞”。此时再回到最初的问题“目标基因在哪群细胞里”答案可以写成“该基因主要富集在T细胞类群”配一张按细胞类型着色的UMAP图配合FeaturePlot信息就完整了。3.4 高亮目标群体灰底加红点比ColorScale更清晰很多时候我们不需要在整张UMAP上展示所有细胞类型只希望“目标群体”一眼可见。这种情况下最推荐的做法是把背景细胞设成灰色目标群体设成醒目的颜色target_cells - WhichCells(seu, expression CD8A 0) DimPlot(seu, cells.highlight target_cells, cols.highlight firebrick3, cols grey85, pt.size 0.8, sizes.highlight 1.2)这里WhichCells用Seurat的表达式语法直接从对象里筛出满足CD8A 0的细胞。cells.highlight接受一个细胞名向量cols.highlight控制高亮颜色cols控制背景颜色。这样画出来的图信息层次非常清楚灰色是全部细胞红色是目标基因阳性细胞边界在哪里一目了然。如果需要同时高亮多个条件可以使用逻辑表达式比如double_positive - WhichCells(seu, expression CD8A 0 MS4A1 0)这个手法在我的项目里用的频率最高也最容易被听众理解。4. 多基因、多类群的批量标注整理成一组能放进论文的图真实项目里你很少只关心一个基因。常见情况是一批候选基因——有的负责定义目标类群有的用来排除其他类群有的代表下游功能。这时候一张一张图画不是不行但效率太低而且不利于统一展示。批量处理的目标是把多个基因对应的表达分布或类群高亮整理成一张大图或一组对比图同时输出一张量化表格作为支撑。4.1 多基因FeaturePlot的循环生产与拼接Seurat里批量画FeaturePlot最简单的方式是lapply遍历基因列表再用patchwork拼接。patchwork是R里做图组合最顺手的包library(patchwork) features_of_interest - c(CD8A, MS4A1, LYZ, CD14) plot_list - lapply(features_of_interest, function(gene) { FeaturePlot(seu, features gene, pt.size 1.0, cols c(grey90, firebrick3), order TRUE) }) combined_plot - wrap_plots(plot_list, ncol 2) ggsave(umap_marker_panel.pdf, combined_plot, width 12, height 10, units in)这里有个细节基因列表我用的是features_of_interest但最终图中每个小图会由FeaturePlot自动显示基因名作为标题不需要额外加标签。如果担心部分基因表达量太低、图上几乎全是灰色可以在循环里对表达量做一个判断自动跳过那些阳性比例低于某个阈值的基因避免生成一堆“啥也看不见”的图。拼接多基因FeaturePlot适合展示“这些marker在UMAP上的空间分布”但注意不要期待它能直接回答“目标类群在哪”。它是一个探索性视图真正用于结论输出时还需要配合后面的高亮图或量化表格。4.2 基因与类群对应关系表比图更能说服人在做标注类群的结论时我习惯搭配一张基因-类群对应表。表格可以直接通过计算每个细胞类型的平均表达量和阳性比例获得# 按细胞类型计算目标基因的平均表达量 avg_expr - AverageExpression(seu, features features_of_interest, group.by celltype, assays RNA)$RNA write.csv(round(avg_expr, 3), target_genes_by_celltype.csv)如果你的Idents已经是细胞类型名直接把group.by改成Idents(seu)即可。这张表的价值在于它把UMAP图上的相对位置转换成了绝对数字比如“CD8A在T细胞中平均表达量是3.2在其他类群中不到0.1”。审稿人和同事都可以快速验证结论不需要反复盯着图猜。还可以结合FindAllMarkers输出每个类群的marker基因列表从中挑出你感兴趣的基因查看它具体在哪个类群的排序最靠前all_markers - FindAllMarkers(seu, only.pos TRUE, min.pct 0.25)然后筛选gene %in% features_of_interest的行查看某个目标基因在哪些类群中被识别为差异表达marker这比AverageExpression多一层统计检验作为定量支撑更扎实。4.3 组合验证UMAP、点图、小提琴图放在一起画图很容易自嗨但“基因富集在某个类群”这个结论必须交叉验证。我的最低标准是至少同时出示UMAP、DotPlot、VlnPlot三种视图。p_umap - FeaturePlot(seu, features CD8A, pt.size 1.0, cols c(grey90, firebrick3), order TRUE) p_dot - DotPlot(seu, features CD8A, group.by celltype) RotatedAxis() p_vln - VlnPlot(seu, features CD8A, group.by celltype)p_umap展示空间位置p_dot直观展示目标基因在哪些细胞类型中高表达以及阳性比例p_vln展示表达量分布和离散程度。三个图拼接后放在一张展示页里结论的说服力要比单独一张FeaturePlot强很多。图注方面我一般会写“UMAP上CD8A的表达分布以及它在各细胞类型中的富集情况”避免直接替读者下结论让图自己说话。5. 图和“像样”之间只差这些细节展示规范与常见伪象到了这一步功能上的“标出类群”已经实现了但要把图放进报告、PPT或论文里还有几个平时很容易踩的细节问题。这些问题不解决你的UMAP图可能被审稿人质疑或者团队内部讨论时被误解。5.1 点大小、坐标轴比例和透明度的调节UMAP图最常见的视觉问题是点太大太密导致高亮群体被背景盖住。pt.size这个参数要根据细胞总数动态调整细胞数在1万以内pt.size 1.5没问题超过3万我会降到0.8以下超过10万可能需要0.3到0.5。如果只截图不调整图中的目标类群会糊成一坨完全看不出边界。另一个经常被忽略的点是UMAP的坐标轴比例。默认的DimPlot可能让X轴和Y轴的单位长度不一样图形看起来被拉伸了。在出正式图时我习惯加上coord_fixed()让图形保持等比例DimPlot(seu) coord_fixed()这会让UMAP的结构看起来更紧凑真实不会因为画面比例导致远近距离失真。出图保存时优先输出矢量图。PDF和SVG是首选因为论文印刷时不会产生马赛克感。ggsave(umap_final.pdf, width 8, height 7, units in)保存的PDF可以放到任何排版软件里无损缩放。5.2 背景与目标颜色的对比策略画高亮图时很多人直接用默认配色结果一整张图五种颜色目标群体和无关群体都花里胡哨反而看不出重点。我的建议是目标群体永远是暖色高饱和色其余全部置灰或浅色。一种简单实现是前面提到的cells.highlight方案。另一种情况是你想高亮的是已注释的某个细胞类型而不是按阈值筛出的细胞那就直接把该类型设为红色其他类型设为灰色celltype_colors - rep(grey85, length(levels(Idents(seu)))) names(celltype_colors) - levels(Idents(seu)) celltype_colors[T细胞] - firebrick3 DimPlot(seu, cols celltype_colors, label TRUE)这样出来的图读者第一眼就看到T细胞的位置其他细胞自动退到背景。这个“背景压暗、前景高亮”的逻辑适用于所有需要强调单一类群的场景比试图在五种颜色里找出红色点要省力得多。5.3 批次效应和聚类分辨率可能制造“假类群”这个问题必须重点提醒。UMAP上看起来聚成一团的“类群”未必是你真正关心的生物学群体。最常见的情况之一来自批次效应如果你的数据来自多个样本或多种条件且没有做好整合UMAP上经常先按样本分堆再按细胞类型分堆。这时候某个基因的阳性细胞在图上“聚成一团”很可能只是因为它们来自同一个样本而不是因为该基因定义了这类细胞。排查方法很简单画图时按样本拆开看DimPlot(seu, group.by sample, split.by sample, ncol 3, label TRUE)如果每个样本的UMAP结构差异很大说明批次效应还没有处理好此时在合并图上高亮目标群体要非常谨慎。另一个坑是聚类分辨率。如果某位同事调高了resolution原本一个合理细胞类型被切成七八个碎块然后你拿基因表达去对应“cluster 1、cluster 4、cluster 9”结论就会非常碎且难以解释。反过来resolution太低也会把不同细胞类型合并成一个cluster目标基因富集情况被“稀释”。我的建议是在确定“目标类群”之前先用一组核心marker检查分辨率是否合适——比如T细胞marker、B细胞marker是否都集中在各自单独的大块里。如果它们混在一起或者被彻底分开就需要调整resolution重新聚类再继续后续可视化。这个步骤属于基础质量控制却是在“标注基因类群”时最常出问题的源头。5.4 如果项目用的是Pythonscanpy的快速对照现在不少单细胞流程已经完全跑在Python里了。scanpy做UMAP和基因可视化思路与Seurat几乎一一对应import scanpy as sc import matplotlib.pyplot as plt # 基础UMAP按细胞类型着色 sc.pl.umap(adata, colorcelltype) # 按基因表达量着色 sc.pl.umap(adata, colorCD8A, cmapReds, orderTrue)高亮某个群体的逻辑也类似只需要把目标细胞的颜色映射写进一个自定义列里或者用adata.obs里的布尔列# 先创建一个布尔列 adata.obs[target_group] (adata.obs[CD8A_raw] 0).astype(str) # 画UMAP目标群体单独着色 sc.pl.umap(adata, colortarget_group, palette[grey85, firebrick3])本质是一样的先计算每个细胞是否属于目标群体再把群体归属映射到点颜色上。无论R还是Python标注的核心逻辑都是“表达信息 - 群体定义 - 颜色映射”。6. 我习惯保留的流程清单和几个容易踩的小细节写到这里我把整个工作流在脑子里过了一遍觉得有几个小经验值得单独列出来它们不见得写在任何手册里但实操中非常影响效率和结果。第一在做任何UMAP可视化之前先把Idents整理成有序的、有注释的状态。如果Idents还是原始的seurat_clusters数字编号后面每个可视化步骤都要额外分组一次非常累。我用的是“先注释细胞类型再可视化”的顺序这样DimPlot、FeaturePlot、DotPlot全部可以基于同一种分组逻辑不会因为换一种图就换一种分组方式。第二FindAllMarkers这类差异表达分析在大型数据上跑起来很慢但如果只是为了确定目标基因在哪些类群富集直接跑AverageExpression加阳性比例统计往往更快结果也足够用。差异表达分析用在正式marker筛选阶段不用每次可视化都全量跑一遍。第三有关“目标基因阳性细胞”的定义体现在代码里就是阈值选择。用expression CD8A 0是最宽松的标准只要是深度够、被捕获到表达的都算阳性。如果某个基因背景表达较高用 0会把一大批背景细胞也选进来这时可以上调阈值比如按高表达细胞的表达量分位数来切expr_cd8a - FetchData(seu, vars CD8A)[[1]] thr - quantile(expr_cd8a[expr_cd8a 0], probs 0.5) # 取阳性细胞的中位数表达 target_cells - WhichCells(seu, expression CD8A thr)这个阈值不是固定的取决于你的生物学问题。想粗看分布就取0想标出真正高表达群体就往上调。关键是阈值一旦定了写论文时需要注明保持可重复性。最后我几乎每一张“基因对应类群”的结论图都会配套至少三样东西FeaturePlot展示表达连续性高亮图展示目标群体边界DotPlot或VlnPlot展示类群特异性。这三样东西放在一起无论给谁看都不容易产生歧义。这个方法还有一个好处它逼我自己先确认目标类群是不是真的存在而不是看着颜色深浅自己脑补一个结论。单细胞可视化的工具更新很快但“基因表达信息 - 细胞群体定义 - UMAP上明确标注”这条主线一直没有变。理解了这条主线无论是换包、换语言还是换降维算法你都能快速迁移。