scanpy单样本分析全流程:降维、聚类与细胞注释实战指南 📅 发布时间:2026/9/17 10:54:49 👁 浏览次数: 拿到一个过滤完低质量细胞、做过归一化处理的h5ad文件之后单样本数据分析就进入最核心也最好玩的阶段降维、聚类、细胞注释。很多第一次用scanpy跑单细胞转录组的同学会在这里卡住——要么不停报错要么跑出来的图总觉得不对劲要么注释出来一堆Unknown。这篇教程我会用一套完整的PBMC数据流程把降维、聚类、细胞注释这三步的原理、参数、代码和避坑经验一次讲清楚适合已经完成QC和标准化、想系统走完单样本分析闭环的朋友。在往下读之前先确认你的环境Python 3.8以上scanpy 1.9以上并且已经安装了python-igraphLeiden聚类依赖它。如果没有装pip install scanpy python-igraph一步到位。数据层面我假设你已经有了一个adata对象里面是归一化、取log之后、并且已经筛过特征基因的表达矩阵这是进入降维前的标准姿势。1. 降维为什么不能直接拿两万个基因去画图有些人会问怎么不直接拿原始基因表达矩阵去聚类原因很现实单细胞表达矩阵通常有两万个左右的基因但绝大多数基因在单个细胞里的表达是稀疏的、噪声很大直接扔给聚类算法计算量爆炸不说聚类结果还会被大量无用基因干扰。所以业内标准流程是先PCA再邻居图最后UMAP/t-SNE可视化每一步都在逐步压缩信息、去噪让下游分析更稳。1.1 PCA基因空间的压缩与去噪PCA主成分分析做的事情是把两万个基因的表达量重新组合成几个互不相关的主成分每个主成分本质上是基因表达的一个线性组合前几个主成分往往就捕获了数据中最大的方差来源。在单细胞分析里我们不是拿PCA做生物学解释而是把它当作一个数据压缩和去噪的前处理步骤——把维度从两万压到几十后续的邻居计算、聚类都在这个低维空间里进行。在scanpy里跑PCA非常直接import scanpy as sc # 假设 adata 已经做过 normalize_total log1p sc.tl.pca(adata, n_comps50, svd_solverarpack) sc.pl.pca_variance_ratio(adata, n_pcs50, logTrue)这里n_comps50的意思是保留前50个主成分svd_solverarpack表示用ARPACK迭代方法计算部分主成分速度比直接算全部SVD快得多。跑完之后一定要看pca_variance_ratio图也就是每个主成分解释的方差占比。正常情况下前几个主成分方差占比很高后面会出现一个明显的肘部——拐点之后的主成分解释的方差很少基本可以认为是噪声。主成分个数怎么定不同教程给的答案不一样但我个人习惯的做法是先看肘部图如果肘部在10左右那说明10到20个主成分就够如果数据复杂比如包含多种细胞类型、有部分批次效应残留会适当提高到30到50。不建议一上来就选100个主成分因为后面主成分里大多是技术噪声聚类时反而会把本来连续的细胞群切碎。有个细节容易被忽略PCA的标准做法是只使用高变基因highly variable genes所以前面做sc.pp.highly_variable_genes时不要省。如果你发现PCA结果第一主成分和第二主成分分别把某个技术因素比如测序深度、细胞周期打分分开了这说明技术噪声太强最好回到上游做一次回归或者用后面的sc.regress_out处理而不是硬着头皮往下走。1.2 邻居图与UMAP真正驱动聚类的数据结构PCA完成之后下一步是构建邻居图和可视化。很多新手会误以为UMAP就是聚类其实不是。真正驱动聚类的是sc.pp.neighbors计算出来的细胞间邻居关系图UMAP只是把这个图投影到二维平面方便人眼观察。所以邻居图的质量直接决定聚类的好坏而UMAP的参数只影响看起来好不好看。sc.pp.neighbors(adata, n_neighbors15, n_pcs30) sc.tl.umap(adata, min_dist0.5, spread1.0)n_neighbors是邻居图的参数默认15表示每个细胞跟周围的15个细胞建立连接。这个值越小图越精细聚类时越容易分出小群值越大图越粗糙小群容易被吞并。建议先跑一遍15如果后面发现分群过碎、或者大群内部被强行切开再把n_neighbors调到20或25重新跑。n_pcs30表示邻居计算时使用前30个主成分这个值要和上一步选的n_comps配套可以留在PCA选定的范围内。有些人会问我在PCA里保留了50个主成分邻居图也用50个行不行理论上可以但实际经验是越靠后的主成分噪声越大一般用20到30个就够不必把50个全喂进去。UMAP的参数里真正值得调的是min_dist。它控制点在二维平面上的最小间距默认0.5偏保守群与群之间分得比较开想要更明显的离散分群可以降到0.1到0.3如果只是想看整体结构用0.5到1.0都没问题。需要注意UMAP的投影形态不等于真实的细胞发育关系两个群在UMAP上离得近只表示它们的转录组相似度较高不代表一定存在分化关系。我见过不少人拿着UMAP图硬说某个群是另一个群的前体这种推断需要额外的轨迹分析证据支持不能直接靠肉眼判断。2. 聚类算法与分辨率的选择降维只是给数据换了种表达方式真正把细胞划分成不同的类靠的是聚类算法。在scanpy生态里现在的主流方案已经从最早的Louvain迁移到了Leiden2020年之后发的高分文章基本都在用后者。原因很简单Louvain在部分随机种子下会识别出断连的假社区Leiden修复了这个问题并且能保证社区内部充分连通结果更稳定。2.1 Leiden/Louvain怎么选scanpy怎么跑直接从代码层面看差异# 老方法Louvain sc.tl.louvain(adata, resolution0.5, key_addedlouvain) # 推荐方法Leiden sc.tl.leiden(adata, resolution0.5, key_addedleiden)resolution分辨率是最重要的参数它控制聚类的颗粒度。分辨率越小分出来的群越少越粗分辨率越大群越多越细。举个生活化的例子把一堆混杂的豆子倒进筛子筛孔越大捞出来的类越少筛孔越小豆子按颜色、大小分得越细。分辨率不是一个有绝对标准答案的参数它取决于你的生物学问题。比如PBMC样本常见的预期细胞类型是T细胞、B细胞、NK细胞、单核/巨噬细胞、树突状细胞这么几大类那分辨率0.5左右通常够用如果你想进一步把T细胞分成CD4和CD8或者把单核细胞分成经典和非经典亚群就需要把分辨率调到0.8甚至1.2让大群分裂成小亚群再在每个亚群内部验证marker基因。实操上不推荐只跑一个分辨率就下结论。我习惯的做法是一次跑好几个分辨率比如0.1、0.3、0.5、0.8、1.2然后把这个参数写成leiden_res0.5、leiden_res1.2这样的列存进adata.obs里再结合marker基因去判断哪个层级的划分在生物学上解释得通。scanpy允许你在同一份数据上反复调用sc.tl.leiden只要每次给key_added一个不同的名字就行不会互相覆盖。2.2 分辨率怎么调先看稳定性再看marker判断一个聚类结果好不好靠的不是UMAP图上看起来爽不爽而是标记基因的表达分布。基本流程是先对每个cluster跑sc.tl.rank_genes_groups找出差异基因再看差异基因列表里的已知细胞类型marker判断这个群是不是有身份的群。sc.tl.rank_genes_groups(adata, groupbyleiden, methodwilcoxon, use_rawTrue) sc.pl.rank_genes_groups(adata, n_genes10, shareyFalse)method我默认用wilcoxon也就是Wilcoxon秩和检验它对表达分布的形状要求较低适合单细胞这种非正态、零膨胀的数据。use_rawTrue表示用原始counts数据做差异检验避免log后数据在低表达区间的伪差异干扰结果。跑完之后你会得到一个每个cluster对应的差异基因列表这时候我建议马上做两件事第一把每个cluster的高表达基因里属于免疫细胞典型marker的挑出来看能不能对应上第二把明显冗余的cluster合并或重新聚类。举一个实际例子某次我跑PBMC数据分辨率0.5时0号群高表达CD3D、CD3E、TRAC是T细胞4号群高表达MS4A1、CD79A是B细胞。两个群分得开、marker也清晰这个分辨率就是可用的。但如果某个群同时高表达CD3D和CD14单核细胞marker那多半是双细胞或者聚类边界没切好要么提高分辨率再看要么考虑用双细胞预测工具兜底。这里有个常被忽略的操作细节如果UMAP图上的某个cluster边界模糊、和其他cluster犬牙交错不要急着去调分辨率先回去看neighbors的n_neighbors参数。我在B细胞和浆细胞样本里踩过坑n_neighbors15时浆细胞被硬塞进B细胞群里怎么调分辨率都分不开把n_neighbors降到8之后浆细胞立刻自己成了一团表达出MZB1、XBP1、JCHAIN这些非常清晰的浆细胞marker。所以算法参数和分辨率参数要配合着调整不要只在一个维度上猛拧。3. 细胞注释从marker基因表到可靠的细胞类型标签聚类结束后每个cluster只是一堆数字编号细胞注释的任务是给这些编号赋予生物学身份。这一步是整个单细胞分析里最依赖专业判断的部分也最容易翻车。我的经验是先建立一张可靠的marker基因表用它做人工注释再借助自动注释工具交叉验证最后统一命名并检查是否存在明显的错误注释。3.1 marker基因表来源与常用清单marker基因表的来源主要有三块CellMarker数据库、PanglaoDB数据库、以及文献里反复验证过的经典marker组合。数据库的好处是全面坏处是比较杂有些条目来自质量不高的旧文章所以我平时会维护一张自己常用的精简清单覆盖大多数组织样本里最常见的细胞大类细胞类型常用marker基因T细胞CD3D, CD3E, CD2, TRACCD4 T细胞CD4, IL7R, LEF1CD8 T细胞CD8A, CD8B, GZMANK细胞NKG7, KLRD1, GNLY, KLRC1B细胞MS4A1, CD79A, CD79B, BANK1浆细胞MZB1, XBP1, JCHAIN, SDC1单核/巨噬细胞CD14, LYZ, FCGR3A, CSF1R树突状细胞FCER1A, LILRA4, CLEC9A, ITGAX血小板PPBP, PF4, GNG11注意这张表只是起点不要机械照搬。不同组织、不同疾病状态的样本marker的表达强度差异很大同一个基因在不同细胞类型里也可能有低水平表达比如FCGR3A在NK细胞和单核细胞里都出现所以单看一个基因很容易误判至少要有两个以上的marker共同指向同一类细胞才能下结论。3.2 人工注释与自动注释的配合dotplot、小提琴图与评分法拿到聚类编号后我第一个看的图是sc.pl.dotplot它在每个cluster里展示每个marker基因的平均表达量和阳性细胞比例信息量很大cell_type_markers { T cell: [CD3D, CD3E, CD2], B cell: [MS4A1, CD79A, CD79B], NK cell: [NKG7, KLRD1, GNLY], Monocyte: [CD14, LYZ, FCGR3A], } sc.pl.dotplot(adata, var_namescell_type_markers, groupbyleiden)如果某个cluster的dotplot里T细胞marker的点又大又红B细胞marker的点又小又灰那这个cluster可以初步判定为T细胞。如果两个marker在同一个cluster里同时高表达比如CD3D和MS4A1都呈现高表达那就要怀疑这个cluster是双细胞或者聚类分辨率太低把两个群混在了一起。人工看dotplot直观是直观但样本里有几十个cluster的时候一个个看容易眼瞎。我通常会用sc.tl.score_genes做一个快速的自动打分注释把每个细胞对每一类细胞类型marker的打分算出来取最高分对应的类型作为初始注释for ct, genes in cell_type_markers.items(): genes [g for g in genes if g in adata.var_names] sc.tl.score_genes(adata, gene_listgenes, score_namef{ct}_score) score_cols [f{ct}_score for ct in cell_type_markers.keys()] adata.obs[predicted_celltype] adata.obs[score_cols].idxmax(axis1).str.replace(_score, )这段代码的原理很简单对每个细胞计算它在一组基因上的平均表达量减去随机基因集的基线得分最高的细胞类型就是它的候选身份。这个方法胜在快速几分钟就能为整个数据集生成一版注释但它不是万能的——如果一个cluster的marker列表不完整或者几种细胞类型的marker高度重叠就容易给出错误标签。所以自动打分结果只能作为初筛一定要回到dotplot和小提琴图上复核。如果嫌自己的marker列表不够权威也可以借助SingleR、scType、CellTypist这些工具做参考注释。SingleR的思路是拿已有注释的参考数据集和新数据做基因表达相关性比较scType的思路是自动化地匹配组织特异性markerCellTypist是机器学习训练出来的分类器。它们的优点是不需要人工逐群判断缺点是依赖参考数据或训练模型参考数据和你自己样本的组织来源不同时结果可能更离谱。我的习惯是自动注释至少跑两个工具把结果一致的部分作为高置信注释直接采用结果不一致的cluster再人工复查这里面的仲裁工作恰恰是单细胞分析里最体现经验的地方。3.3 注释后的命名与验证注释完成后不要直接在adata.obs里随便起个名就完事。我推荐遵循Cell Ontology的命名约定比如CD4-positive, alpha-beta T cell这类规范名或者至少用通俗但无歧义的简写比如CD4 T、CD8 T、NK、Mono CD14、DC pDC尽量别用cluster0这种编号当最终标签。命名统一之后后续做差异分析、富集分析组别比较代码会省很多事。验证环节有一个容易被忽略的点把注释结果以adata.obs[celltype]的形式保存下来然后重新跑一次sc.pl.umap(adata, colorcelltype)按注释上色看整体分布是否合理。比如T细胞和NK细胞虽然在转录组上比较接近但通常不会完全重叠单核细胞和树突状细胞也不应该糊成一片。如果发现某两类细胞在UMAP上彻底重叠而它们表达着截然不同的marker那大概率是聚类参数有问题而不是注释错了。还有个小技巧每次注释完都adata.write(sample_annotated.h5ad)存一个带注释的完整对象别只导出一个CSV结果表不然回头要改分辨率、重新注释的时候又要从头跑一遍邻居图。4. 常见问题与排查技巧实录单样本分析走到这里该踩的坑基本都踩过了。我把自己在降维聚类和注释阶段遇到过的高频问题集中整理一下算是速查手册碰到类似情况可以直接对照排查。4.1 聚出太多小群或者大群怎么都分不开先把现象说清楚小群过多通常表现为UMAP上有好几个三五成群的碎片每个碎片里只有几十个细胞差异基因也不强这种情况多半是resolution设得过高或者n_neighbors设得过小。处理方法是降低分辨率到0.1到0.3同时把n_neighbors提到20左右让邻居关系更平滑。反过来大群分不开最常见的是CD4T细胞和CD8T细胞糊在一起或者B细胞和浆细胞糊在一起。这种情况不要急着调参数先在dotplot里确认这两个群的marker是否都有表达——如果确实都表达了说明数据里这个分辨率下本来就是连续过渡的可以适当提高分辨率强行切如果marker表达本来就很弱那说明上游标准化或特征基因筛选有问题切了也是硬切注释出来也站不住脚。4.2 注释结果诡异marker表达跟预期完全对不上遇到这种情况第一件事不是怀疑算法而是检查基因名。人类基因符号是全大写字母如CD3D小鼠基因符号是首字母大写其余小写如Cd3d。如果你用人的marker列表去注释小鼠数据或者反过来所有marker评分都会是一坨乱码。我用过一次Ensembl ID导入的矩阵基因名全是ENSG开头直接拿CD3D去var_names里找是找不到的必须先把基因ID转换成标准symbol这一步在scanpy里可以用sc.queries.biomart_annotations等工具做但前提是你要能联网并正确配置参考物种。另外有些marker在不同数据版本里的注释符号有别名比如老的MZB1曾被叫做PLAC1如果在你的矩阵里找不到MZB1可以试试用它的别名搜索。还有一个非常容易踩的坑你用use_rawTrue做差异分析时adata.raw里的基因名和adata.var_names必须一致否则sc.pl.dotplot会提示找不到基因或者画出来全是灰点。4.3 细胞周期和混杂细胞干扰聚类细胞周期效应是单细胞聚类里绕不开的幽灵。同一个细胞类型处在G1期和S期的细胞转录组差异可能比不同细胞类型之间的差异还大。结果就是UMAP图上会出现一个增殖群里面混着各种类型的细胞注释的时候特别容易误判成某种独立的细胞类型。处理方案有两个方向一是用sc.tl.score_genes_cell_cycle计算每个细胞的细胞周期打分然后用sc.regress_out把周期信号回归掉再回到PCA那一步重新跑邻居图和聚类二是在解释结果时把周期群单独标记为Cycling不强行赋予细胞类型身份。我个人倾向于先评分、再看实际影响如果周期群的marker确实很干净比如全是MKI67、TOP2A这类增殖基因那直接标注成增殖细胞反而是更诚实的做法不做回归也能让其他细胞类型的注释更清晰。还有一类混杂细胞也容易捣乱红细胞。红细胞高表达HBB、HBA1、HBA2这些血红蛋白基因如果红细胞残留较多它们会聚成一个非常耀眼的大群把所有稀有细胞群都挤扁。我处理这类问题的方法是在QC阶段就利用红细胞marker把明显的红细胞团先过滤掉而不是等到聚类后再清理。如果在聚类后才发现也可以手动把高表达HBB的cluster从下游分析里剔除但这样做之前一定要确认阳性细胞比例别误伤了正常的低表达群。4.4 性能与运行环境问题速查scanpy处理十万细胞级别的单样本对内存的要求不低但也没到离谱的程度。我遇到过几个典型的性能问题列在下面供参考问题可能原因解决建议sc.pp.neighbors跑得很慢细胞数量大默认CPU多线程没吃满设置n_jobs参数或手动调sc.settings.n_jobssc.tl.leiden报错找不到igraph缺少Leiden依赖pip install python-igraphUMAP反复卡死sc.tl.umap默认使用umap-learn数据量大时耗时长考虑切到scanpy支持的rapids或先对数据下采样测试参数代码在Windows上经常报并行相关的错Windows对多进程支持不如Linux建议在WSL或Linux服务器上跑正式分析一个容易被忽略的技巧在跑大规模数据前先对数据做一个随机子采样比如抽2万个细胞用子集把PCA、邻居图、Leiden参数全部调顺确认marker注释逻辑没问题再用全量数据正式跑一遍。这样调试迭代的周期会短很多参数也不是靠猜而是有据可依。最后再分享一个小技巧在把注释结果写进论文或报告之前记得导出一张按细胞类型着色的UMAP图、一张核心marker的dotplot、一张每个集群细胞数量的条形图这三张图基本就是单样本数据分析的标准配置。我个人的体会是单样本分析最考验人的不是代码跑不跑得通而是面对一个模糊的聚类结果时你敢不敢根据marker证据下结论。多跑几个分辨率、多画几个图、多对照几次marker表注释的置信度就会上去一大截。如果你在实操里遇到其他奇怪的报错把你的adata.obs信息、聚类参数和报错截图整理好多数问题都能从这几个环节里找到答案。