
我在生物信息这个圈子里泡了快十年各种项目复现见过不少但像Wellcome Sanger研究所Sarah Teichmann团队这篇160万肠道单细胞分析这样的工作还是值得专门写一篇完整复现笔记的。原因很简单这篇文章不只是数据量大而是把单细胞分析从“看聚类”拉高到了“看器官级细胞图谱”的层面从数据质控到细胞注释再到组织微环境解析每一步都有值得拆解的细节。这篇文章我会从复现角度出发把全流程拆开揉碎讲清楚包括环境搭建、数据获取、核心分析代码、常见报错以及一些文档里不会写的实操心得。1. 整体设计与思路拆解1.1 这篇Nature到底做了什么Sarah Teichmann团队发表在Nature的这项工作核心是对人类胎儿肠道组织进行大规模单细胞RNA测序最终构建了包含约160万个细胞的高分辨率图谱。先说清楚一个概念普通单细胞研究做到几万细胞已经能讲故事了但要做到160万意味着样本覆盖度、稀有细胞类型捕获概率、发育轨迹推断精度都上了几个台阶。这篇文章的标题里有几个关键词值得注意“超全代码”说明分析流程完全公开“160万”说明规模非比寻常“多组学整合”的应用场景也非常典型。整个项目的数据量差不多是几十个标准10x Genomics文库的规模测序数据在TB级别中间分析产物在百GB级别。从技术路线看核心思路是先把所有样本的细胞经过严格质控再用整合算法去除批次效应然后做聚类、注释、轨迹推断和细胞间通讯分析。这里有一个关键设计他们不只是把数据合并起来跑一遍而是分成了“胎儿早期vs晚期”“肠道不同区域”等多个维度交叉验证这样得出的细胞类型和发育轨迹才更有说服力。1.2 为什么值得花时间复现很多朋友问我论文复现到底图什么文章都发了数据也在照着代码跑一遍有意义吗我的回答是真正完整跑完一遍这篇160万单细胞分析你对单细胞分析的理解会完全不同。首先超大数据的预处理逻辑和常规项目不一样。平时处理1万个细胞normalization和聚类参数随便调调都差不多但160万细胞对内存管理、矩阵稀疏化、降维算法的并行效率都提出了很高要求。你会在实操里真切感受到为什么需要高效的稀疏矩阵存储为什么PCA计算要考虑内存上限。其次细胞注释环节非常有参考价值。160万细胞里包含了几十种细胞类型包括上皮细胞祖细胞、间充质细胞、内皮细胞、免疫细胞还有非常稀有的肠道微环境细胞。作者团队的注释策略是分层次进行的先粗注释大类再细注释亚群这种两级注释策略在常规项目里同样适用能大幅减少误注释的概率。再者这个项目的代码组织方式本身就是一套很好的分析范式。从原始数据下载到最终可视化的每一步都有独立脚本输入输出接口清晰参数配置集中在配置文件里。这种工程化思路对做大型单细胞项目非常有参考价值哪怕你的数据规模只有几万细胞也建议按这套逻辑组织代码。2. 复现前的准备工作2.1 硬件与软件环境评估先说硬件。160万细胞的单细胞分析对计算资源的要求远高于常规项目。我在一台配备64GB内存、16核CPU的服务器上跑过完整流程坦白说比较吃力尤其在聚类和UMAP可视化阶段内存接近极限。我的实际建议是内存至少128GB起步CPU核心数32核以上如果能用上256GB内存会从容很多。如果本地资源实在不够也别硬扛。可以考虑把部分步骤放到云计算平台上跑比如把最耗内存的harmony整合步骤放到高内存实例上。一个通用经验法则是单细胞数据的峰值内存需求大约等于细胞数乘以150~200字节矩阵存储和中间计算的综合开销160万细胞对应大致需要240GB到320GB内存实测在稀疏矩阵处理得当的情况下128GB也能跑下来但绝对不能开太多并行任务。软件环境方面我强烈建议用Conda管理所有依赖。直接pip install看似方便但Scanpy、Harmony、Faiss这些库对版本很敏感尤其是NumPy和SciPy的版本配不好容易遇到ABI兼容问题。conda create -n scrna160 python3.10 conda activate scrna160 pip install scanpy leidenalg harmonypy scvi-tools scrna0.4.0 pip install numpy1.24.0 scipy1.10.0 pandas1.5.0Scanpy用1.9.x版本比较稳定Harmony用harmonypy而不是R版本的harmony因为Python接口在处理AnnData对象时更顺滑。2.2 数据获取的完整流程数据获取是这个项目复现中第一个容易卡住的环节。160万细胞的原始数据来自Array Express数据集编号是E-MTAB-XXXX文章补充材料里会给出具体编号。下载方式推荐用Array Express的FTP接口或者EBI的BioStudies数据库速度稳定。wget -r -np -nH --cut-dirs3 ftp://ftp.ebi.ac.uk/pub/databases/microarray/data/experiment/MTAB/E-MTAB-XXXX/这里要特别注意下载的原始数据是FASTQ格式每个样本一个文件夹10x格式的barcodes.tsv.gz、features.tsv.gz和matrix.mtx.gz这三件套通常需要自己从Cell Ranger的count结果里提取。文章提供了处理好的表达矩阵文件但为了完整复现质控环节建议从FASTQ走一遍Cell Ranger流程。另一个常见问题是元数据匹配。160万细胞不是一次性测的而是几十个样本合并的结果每个样本有自己的供体信息、组织区域、发育阶段。这些元数据在文章的Supplementary Table里以Excel表形式提供复现时务必提前做数据清洗确认样本ID和矩阵列名能严格对应。这个工作看似简单实操里非常容易出错——我见过很多人在这个环节把样本标签搞乱后面所有分析全都对不上。3. 核心分析流程逐段拆解3.1 数据读取与Barcode匹配拿到Cell Ranger输出的三件套之后第一步是用Scanpy把表达矩阵读进来。这里有一个容易被忽略的坑160万细胞的文件非常大直接用read_10x_h5读取会大量占用内存建议先做Barcode过滤把空液滴和低质量细胞剔除后再做后续处理。import scanpy as sc import anndata as ad import pandas as pd import numpy as np adata sc.read_10x_h5(filtered_feature_bc_matrix.h5) adata.var_names_make_unique() adata.obs_names_make_unique()读取之后首先要处理的是Barcode匹配。很多时候不同样本的Barcode存在冲突或者元数据表里的样本命名和矩阵列名格式不一致。我的习惯是读进来之后立刻把样本信息写进obs里做一个统一命名规范。adata.obs[sample_id] sample_01逐个样本读取后合并时用concat函数并设置index_unique参数避免合并后obs索引冲突。adata_all ad.concat(adatas, index_unique-)这里要提醒一句concat之前务必确认每个AnnData的var完全一致基因名有缺失或者顺序不一致都会导致合并失败。如果用的是同一个参考基因组版本通常问题不大但不同批次跑Cell Ranger时如果Ensembl版本有更新可能会出现基因注释差异。3.2 质控指标计算与阈值确定这是整个流程中最反直觉、最需要手动经验的环节。常规单细胞项目大家习惯用“检测到的基因数200~6000、线粒体基因比例低于20%”这类通用阈值但160万细胞跨多发育阶段、多组织区域的大项目全局统一阈值反而会引入系统性偏差。我的做法是分样本计算质控指标再按组织区域分别设定阈值。具体来说先算出每个样本的UMI总数、基因数、线粒体占比三个指标画出分布图后人工确定阈值。adata_all.var[mt] adata_all.var_names.str.startswith(MT-) sc.pp.calculate_qc_metrics(adata_all, qc_vars[mt], percent_topNone, log1pFalse, inplaceTrue)质控阈值建议写成配置文件方便之后复现和调整。例如qc: min_genes: 500 max_genes: 8000 max_mt_percent: 20 min_counts: 1000特别要说一下线粒体比例这个指标。在肠道组织中上皮细胞因为代谢活跃线粒体基因比例天然偏高尤其肠上皮的肠细胞线粒体占比15%~25%并不罕见。如果统一用10%的严格阈值会杀掉大量真实的肠上皮细胞导致后续分析的细胞类型构成严重失真。所以我的做法是先看每个细胞类型在质控前后的变化再决定是否需要调整阈值。质控过滤代码执行后一定要打印过滤掉的细胞数量和百分比。正常情况下去除20%~30%的细胞是合理的如果去除率超过50%说明原始数据质量或阈值设置有严重问题。3.3 归一化与高变基因选择这部分在常规项目中很无脑但大规模数据下要特别注意计算效率。正常的normalize_total和log1p操作对160万细胞来说耗时并不算太长但如果你用了Pearson残差方法替代log归一化内存占用会显著上升。sc.pp.normalize_total(adata_all, target_sum1e4) sc.pp.log1p(adata_all)高变基因选择上要理解一个关键差异选择高变基因的数量直接影响后续降维的精度和计算开销。小项目选2000个高变基因160万细胞的项目我建议至少选5000个。原因在于细胞数量越多越多的稀有细胞类型需要靠低频差异表达基因来区分高变基因太少会导致这些稀有群体在聚类时被掩盖。sc.pp.highly_variable_genes(adata_all, n_top_genes5000, batch_keysample_id)使用batch_key参数是为了避免高变基因完全被某个优势样本主导。160万细胞的样本组成并不均衡如果某个样本细胞数特别多它的基因表达波动会在全局方差中占比过大结果高变基因全被这个样本刷屏了。按样本分组的variance stabilization可以有效缓解这个问题。这里还有一个经验高变基因选择建议用Seurat的VST方法对应的Scanpy实现而不是简单的dispersion方法。实测VST在跨样本数据上更稳健基因选择结果更可解释。3.4 PCA降维与Harmony批次整合批次整合是整个流程的重头戏。160万细胞来自几十个样本供体差异、文库批次差异、测序深度差异都会造成明显的批次效应。如果不做整合聚类结果通常按样本分开而不是按细胞类型分这样的图谱就没有生物学意义。PCA降维本身很简单sc.tl.pca(adata_all, n_comps50, svd_solverarpack)但50个主成分对160万细胞来说往往不够。实际操作中我发现大细胞数数据集的有效信号维度比小数据集更高所以建议保留100个主成分。计算时间会增加一些但后续整合和聚类的信息损失会小很多。批次整合我用的harmonypyimport harmonypy as hm data_mat adata_all.obsm[X_pca] meta_data adata_all.obs[[sample_id]].reset_index(dropTrue) vars_use [sample_id] ho hm.run_harmony(data_mat, meta_data, vars_use, max_iter_harmony20) adata_all.obsm[X_pca_harmony] ho.Z_corr.THarmony的核心参数是theta默认2。但160万细胞跨几十个样本的场景theta调小一点效果更好。theta1.5可以让Harmony做适度校正而不是把所有样本差异全部抹平。尤其当你关心发育过程中的样本异质性时过度的批次校正反而会掩盖真实的生物学差异。整合完成后必须做一次可视化诊断用UMAP查看按样本和按细胞类型的分布。如果样本充分混合且细胞类型分开说明整合质量不错如果不同样本形成明显分团需要重新调整theta或检查元数据映射是否有误。3.5 聚类参数选择与分辨率调优聚类这一环节很多教程都是用默认分辨率直接跑但160万细胞的项目不能这么随意。Leiden聚类的分辨率参数直接决定细胞亚群的颗粒度分辨率太低稀有细胞群被合并分辨率太高同一类型被过度拆分。sc.pp.neighbors(adata_all, n_neighbors15, n_pcs30, use_repX_pca_harmony) sc.tl.leiden(adata_all, resolution1.0, key_addedleiden_1_0)针对160万规模我建议的做法是跑一组分辨率扫描——分别用0.5、0.8、1.0、1.5、2.0跑聚类然后评估簇的数量和每簇的Marker基因特异性。簇数量按经验160万细胞最终注释出30~40个簇比较合理分辨率1.0左右通常落在这个范围附近。另一个关键参数是n_neighbors。默认15在小数据集上没问题但超大细胞量下邻域图会非常稠密计算时间也大幅增长。如果资源有限可以调小到10对聚类稳定性影响不大但内存消耗会显著下降。聚类完成后建议立刻做cluster fingerprint分析也就是每个簇的特异性高表达基因这不仅是注释的基础也是判断聚类是否合理的依据。3.6 细胞类型注释的两级策略细胞注释是这篇Nature分析中我认为最有学习价值的部分。作者团队没有直接拿全部160万细胞做自动注释而是采用了分层次策略第一轮注释出粗粒度大类上皮、基质、内皮、免疫等第二轮再对每个大类内部做细分。我用一种半自动方式实现# 第一轮注释大类粗分类 refined_markers { Epithelial: [EPCAM, KRT8, KRT18], Stromal: [COL1A1, COL3A1, DCN], Endothelial: [PECAM1, VWF, CDH5], Immune: [PTPRC, CD3D, CD79A, LYZ], } sc.tl.score_genes(adata_all, gene_listrefined_markers[Epithelial], score_nameEpithelial_score)用基因集评分的方式先给每个细胞打分然后根据最高分分配粗标签。这一步的效果比直接跑singleR好尤其在数据规模大、亚群重叠多的情况下人工定义的Marker组合更可控。粗注释完成后再把每个大类对应的细胞子集抽出来分别做第二轮聚类和细分注释。第二轮的Marker选择就要细致很多。比如上皮类内部肠干细胞用LGR5、OLFM4潘氏细胞用LYZ、DEFA5杯状细胞用MUC2。间充质内部成纤维细胞用COL1A1、PDGFRA肌成纤维细胞用TAGLN、ACTA2平滑肌用MYH11。每个亚群的Marker至少选3个如果只有1个Marker表达有重叠注释结果很容易翻车。这个过程中有个判断技巧当某个亚群的Marker基因表达并不显著但UMAP上位置很独立时不要急着注释成某类细胞建议先标记为“Unknown”用差异表达分析找出Top marker再做二次判断。3.7 差异表达分析与可视化细胞类型注释完成后差异表达分析是验证注释质量的重要手段。常规的rank_genes_groups会用Wilcoxon秩和检验但160万细胞下这个方法的p值会非常显著哪怕表达差异很小也会被标记为差异基因。所以我在大规模数据里更多参考effect size也就是log2 fold-change而不是单纯看p值。sc.tl.rank_genes_groups(adata_all, groupbycell_type, methodwilcoxon, n_genes50)实际筛选差异基因时我的标准是平均log2FC绝对值大于0.5且在目标簇中的表达比例高于对照簇20个百分点以上。这样筛出来的Marker基因特异性更高注释报告也更经得起推敲。可视化方面dotplot是展示各类细胞Marker表达的最佳工具sc.pl.dotplot(adata_all, var_namesmarker_dict, groupbycell_type, standard_scalevar)另外160万细胞直接画UMAP会导致严重overplotting一整片密密麻麻的黑点什么都看不出来。我的做法是用Scanpy的sample函数先抽10万细胞做可视化或者用gaussian_density图展示细胞密度分布。adata_sub sc.pp.sample(adata_all, n_obs100000, random_state42) sc.pl.umap(adata_sub, color[cell_type], save_100k.png)这样做有两个好处一是出图速度快二是图上能清楚看到细胞类型的分布层次而不是一团糊。4. 实操中的坑与排查经验4.1 内存爆炸问题这是复现160万单细胞时最常遇到的问题。具体表现是聚类阶段内存撑不住程序被系统kill日志里出现OOMout of memory字样。我实测的最有效解决方案是分步释放中间变量。在完成PCA和Harmony整合之后把原始的log表达矩阵释放掉只保留降维后的数据。因为Leiden聚类实际只用到邻域图基于PCA坐标构建不需要原始表达矩阵。del adata_all.X import gc gc.collect()把adata_all.X置为None之后AnnData对象依然保留obsm、obs、var等元数据后续几乎所有分析都能正常进行。只有在做差异表达时需要重新加载表达矩阵那时候再读进来也不迟。4.2 Harmony运行时进程卡死harmonypy在大矩阵上运行时会卡住或者很慢。160万细胞经过PCA降维后的矩阵是160万×50这个规模Harmony处理起来确实吃力。实测优化方案是把max_iter_harmony从默认的20逐步调低先跑10轮迭代看效果如果收敛情况良好就没必要跑满。另一个有效优化是并行计算。Harmony本身不支持多进程但你可以把多个样本分组并行跑整合。这个操作对实际效果会有一定影响不太建议初学阶段尝试。更简单的做法是直接用高核数、大内存实例一次性跑完。4.3 基因名与注释版本不匹配这个问题很隐蔽但破坏力极大。文章提供的一些中间分析文件用的是GENCODE v32的基因名而你自己用Cell Ranger定量时如果参考转录组用的是GENCODE v38或Ensembl新版本基因ID可能对不上。我的建议是所有样本用同一版本参考基因组定量并且复现前先检查var_names里是否包含核心Marker基因比如检查“EPCAM”“LGR5”“PTPRC”这些基因是否存在。如果缺失大概率是基因注释版本不同需要用gconvert这类工具做ID转换。4.4 与官方发布的数据做对比复现过程中很容易陷入“结果看起来不太一样”的焦虑。我的解决方法是把官方发布在细胞图谱门户的注释结果下载下来和自己注释的细胞类型分布做一致性对比。具体操作是把两组注释结果做cross-tabulation计算匹配率。如果总体匹配率超过85%说明你的注释基本到位如果某个细胞亚群匹配率低比如LGR5阳性干细胞的注释比例和官方偏差较大重点检查这一亚群的Marker阈值以及聚类分辨率是否合适。还有一个简单的对比技巧直接比较关键Marker基因在不同细胞类型中的表达模式。比如官方注释的T细胞亚群在CD3D表达上应该明显高于上皮细胞如果你自己的聚类结果在这点上不明显说明聚类分辨率设置有问题返回上一步调参。5. 常用脚本片段汇总5.1 配置驱动的分析框架这160万项目的代码量非常大如果全部用命令行交互式跑基本不可维护。我的做法是把所有参数集中到一个YAML配置文件里用Python脚本读取后执行对应分析步骤。data: raw_dir: /data/raw processed_dir: /data/processed metadata: /data/meta/sample_info.csv params: min_genes: 500 max_genes: 8000 max_mt_percent: 20 n_top_genes: 5000 n_pcs: 100 n_neighbors: 15 leiden_resolution: 1.0这种配置驱动的写法最大的好处是后续调整参数时不需要改代码只改配置再重跑对应脚本即可也方便把每次实验的参数记录归档复现结果时能精确定位到具体参数组合。5.2 分阶段运行的脚本骨架我习惯把流程拆成独立的阶段脚本每个脚本只做一件事python 01_load_and_qc.py --config config.yaml python 02_normalize_and_hvg.py --config config.yaml python 03_pca_and_harmony.py --config config.yaml python 04_clustering.py --config config.yaml --resolution 1.0 python 05_annotation.py --config config.yaml --ref markers.csv python 06_deg_and_viz.py --config config.yaml这样即使中间某一步出问题也只需要重跑失败的步骤不需要整体推倒重来。在160万细胞规模下每个步骤耗时动辄半小时以上这种分阶段执行的工程化设计省下的时间非常可观。6. 给复现者的建议整个项目复现下来我的体会是难度不在单个技术点而在于对计算资源的统筹、对数据质量的判断以及最核心的——细胞注释时的生物学直觉。代码跑通只是基础真正有价值的部分是你在跑的过程中建立起来的“什么结果才是合理的”这种感觉。如果你准备开始复现我有几个具体建议第一不要一开始就想全量跑160万细胞。先用随机抽样的10万细胞跑通整个流程验证代码、参数和marker列表都没问题再全量跑。这样能省下大量排查错误的时间。第二每个环节都要记录中间结果。生成的AnnData对象、聚类结果、Marker基因列表都建议带版本号保存。这样不会出现“昨天跑的结果找不到了”或者“同样代码跑出的结果对不上”的混乱。第三代码组织上不要追求写出多复杂的封装逻辑。对于分析类项目线性脚本加配置文件是最可靠、最易审查的方式复杂抽象反而会增加调试难度。单细胞分析这个方向跑通一个大规模项目后积累的经验是可以复用到几乎所有后续项目里的。这篇Nature的复现流程从质控到注释从整合到可视化基本覆盖了单细胞分析的全链路知识体系。希望这篇笔记能帮你少走一些弯路把精力真正花在理解数据背后的生物学意义上。