假设你是一位刚接手“细胞杂货市场”的仓库管理员。你面前有一本砖头厚的账本,记录着几万个摊位(细胞)里几十种商品(基因)的备货数量(表达量)。这本账本厚到你看一眼就头皮发麻——怎么从里面找出规律?哪些摊位卖的是同类货品?市场里到底有几个大的商户群体? 这时你身边有两位得力助手。阿 Py(Scanpy)是 Python 出身的技术派,习惯写脚本批量处理,一条指令命令下去,一万个摊位瞬间处理完,所有操作自动留档;小 R(Seurat)是 R 语言出身的老牌分析师,习惯用图形界面交互操作,鼠标点选、框选、拖拽就能查看任意群体的细节,适合精细探查。两个人都能帮你完成“从账本到市场地图”的全流程:清理账本、统一标尺、寻找相似摊位、划分商圈、画市场地图。 这篇文章将带你从头认识这两套单细胞分析工具链:它们的数据结构有何不同、分析流程如何对照、可视化各自擅长什么、又该如何互相衔接。读完你就能根据自己团队的实际情况,做出务实的工具选型。

岗位关联

对于 AI+生命科学从业者,理解 Scanpy 与 Seurat 的异同是进入单细胞分析领域的必修课,能让你在组学数据处理的技术选型、团队协作与流程设计中少走大量弯路。

目录
  1. 可视化演示
  2. 工具缘起与定位
  3. 数据结构对比
  4. 分析流程概览
  5. 预处理与质控
  6. 降维与聚类
  7. 可视化对比
  8. 互操作与衔接
  9. 选型与实战建议
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Scanpy(Python)与Seurat(R)单细胞分析工具链的完整流程对比,从质控归一化到聚类降维可视化,以及细胞群体在UMAP空间中的分布。切换模式查看不同维度,拖动滑块调节细胞数量。

本节配有交互动效(进入视口后自动加载,约 3KB)。

单细胞分析流程对比 Scanpy使用AnnData结构,Python生态集成度高 Seurat使用Seurat对象,R统计可视化生态丰富 QC → Normalize → HVG → PCA → Cluster → UMAP 两者流程相似但API与底层实现不同

工具缘起与定位

想象你的杂货市场第一天开业,你同时雇了阿 Py 和小 R 两位助理。阿 Py 是个年轻的程序员,他的工具架上挂满了 Python 脚本,他说:“给我一份数据清单,我能批量处理任何规模的市场,而且每一步都留日志,出错了随时回放。”小 R 则是一位稳重的统计学家,他的办公桌上摆着 RStudio 交互面板,他说:“我能让你用鼠标直接点选任意摊位,图表实时刷新,适合你这种喜欢亲眼确认的老板。”

这两位助理的“出身”决定了他们的性格。Scanpy 诞生于 Python 生态,由德国亥姆霍兹慕尼黑研究中心(Theislab 实验室)开发,核心数据结构是 AnnData,天然与深度学习框架(PyTorch、scVI)、云端集群和自动化流程亲和。Seurat 诞生于 R 语言生态,由纽约基因组中心(Satija Lab)开发,核心数据结构是 SeuratObject,背靠 R 语言在生物统计领域几十年的积累,配套 Shiny 可以做交互式网页应用。

尽管成长背景不同,两人处理的却是同一本账本——基因表达矩阵。这本矩阵行是细胞、列是基因,每个格子记录一个分子计数。无论你选哪位助理,第一步都是把这本账本读进他们的系统里。

所以,选助理的底层逻辑不是“谁更强”,而是“你的团队更习惯哪种工作方式”。Python 团队与机器学习流程自然拥抱 Scanpy,而 R 背景深厚的生物信息实验室往往更顺手 Seurat。

Xn×m=[x11⋯x1m⋮⋱⋮xn1⋯xnm]\mathbf{X}_{n \times m} = \begin{bmatrix} x_{11} & \cdots & x_{1m} \\ \vdots & \ddots & \vdots \\ x_{n1} & \cdots & x_{nm} \end{bmatrix}
这就是“细胞杂货市场大账本”的数学形态:n 个摊位(细胞)对 m 种商品(基因)的备货清单。
  1. 行标 i 与细胞矩阵每一行是一个细胞摊位,行数 n 表示市场里摊位总数。
    i=1,2,…,ni = 1, 2, \dots, n
  2. 列标 j 与基因矩阵每一列是一种基因商品,列数 m 表示商品种类总数。
    j=1,2,…,mj = 1, 2, \dots, m
  3. 元素 x_ij 表达量第 i 行第 j 列的元素记录该细胞里该基因的分子计数,是后续一切分析的原料。
    xij=表达量(细胞i,基因j)x_{ij} = \text{表达量}(\text{细胞}_i, \text{基因}_j)

数据结构对比

两位助理记账的方式完全不同。阿 Py 习惯用一个多标签页的工作簿(AnnData),主表格放在最前面,后面跟着一堆附属页签:细胞属性、基因属性、降维坐标、还有一张便签页记录杂七杂八的元信息。小 R 则习惯用一个分层档案柜(SeuratObject),主抽屉放表达数据,侧边抽屉放细胞元数据,底下还有专门的格子放降维结果和邻居图。

AnnData 的核心设计是“一个主矩阵配多个附属表”。主矩阵 X 保存表达量;obs 是按行(细胞)索引的元数据表——每个摊位来自哪个样品批次、线粒体比例多少;var 是按列(基因)索引的元数据表——基因 ID、基因名、是否高变基因;obsm 保存 PCA、UMAP 等降维坐标;uns 是自由格式的杂项记录。这种设计非常 Pythonic:一切围绕 NumPy 数组,切分、索引、广播都很自然。

SeuratObject 的核心设计是“以对象为容器,多模态并存”。assays 存放多个测定层(RNA、蛋白、ATAC 等),meta.data 存放细胞元数据,reductions 存放降维坐标,graphs 存放细胞间的邻居图结构。多模态数据(同一个细胞同时测了 RNA 和蛋白)可以直接放在同一对象里联合分析,这是 Seurat 的招牌能力。

理解了数据结构,你就明白为什么两个工具的分析习惯不同:Scanpy 让你像操作 DataFrame 一样自由地筛选细胞和基因;Seurat 则鼓励你把多组学数据装进一个对象里统一管理。没有谁绝对更好,而是“怎么组织,就怎么思考”。

A=(X,O,V,R)\mathcal{A} = (X, O, V, R)
这是 AnnData 档案夹的四层收纳逻辑:主矩阵、细胞表、基因表、降维结果,各归其位。
  1. 档案夹对象AnnData 是一个整体容器,对应阿 Py 的多标签页工作簿。
    A\mathcal{A}
  2. 主矩阵 X账本正文,n 行 m 列的表达矩阵,是所有分析的出发点。
    X∈Rn×mX \in \mathbb{R}^{n \times m}
  3. 细胞表 Oobs 存放每个摊位的属性,如样品名、批次、质控指标。
    O={o1,o2,…,on}O = \{o_1, o_2, \dots, o_n\}
  4. 基因表 Vvar 存放每种商品的说明书,如基因名、染色体位置、高变标记。
    V={v1,v2,…,vm}V = \{v_1, v_2, \dots, v_m\}
  5. 降维结果 Robsm 存放 PCA/UMAP 坐标,相当于市场地图上每个摊位的平面坐标。
    R={PCA坐标,UMAP坐标}R = \{\text{PCA坐标}, \text{UMAP坐标}\}

分析流程概览

不管是阿 Py 还是小 R,完成一次合格的单细胞分析都要走一套固定的“六步工序”,就像从大账本到市场地图的生产线。第一步,质量评估:把账本里明显破损的记录挑出来;第二步,归一化:统一每个摊位的备货标尺,让不同摊位可以公平比较;第三步,高变基因筛选:从几万种商品里挑出最能区分摊位的“招牌商品”;第四步,线性降维(PCA):先看整体大趋势;第五步,非线性降维与聚类:画出精细市场地图并划分商圈;第六步,标记基因识别与注释:给每个商圈挂上名字——“这是水果区,那是水产区”。

这套流程在 Scanpy 中由 sc.pp、sc.tl、sc.pl 三个模块接力完成;在 Seurat 中则由 NormalizeData、ScaleData、RunPCA、FindClusters、FindMarkers 等函数接力完成。函数名不同,节奏一致。

这套流程是两条工具链共同的地基。无论你最终选谁,最该花时间理解的不是某个 API 的用法,而是每一步“为什么要做”。流程理解到位了,切换工具只是换一套函数名而已。

labels=f注释(f聚类(f降维(f归一化(Xraw))))\text{labels} = f_{\text{注释}}(f_{\text{聚类}}(f_{\text{降维}}(f_{\text{归一化}}(X_{\text{raw}}))))
整个分析就是一条流水线:原始账本依次经过清洗、标定、压缩、分组、命名,最终得出每个细胞的类型标签。
  1. 原始账本X_raw 是刚拿到手的计数矩阵,还没经过任何处理。
    XrawX_{\text{raw}}
  2. 归一化步骤f_归一化 把每个细胞的总计数统一到同一标尺。
    f归一化f_{\text{归一化}}
  3. 降维步骤f_降维 把上万个基因的高维空间压缩到几十个主成分。
    f降维f_{\text{降维}}
  4. 聚类步骤f_聚类 根据细胞间的相似性划分出自然群体。
    f聚类f_{\text{聚类}}
  5. 注释步骤f_注释 结合已知标记基因给每个群体赋予生物学身份。
    f注释f_{\text{注释}}
  6. 最终标签labels 是每个细胞对应的细胞类型,相当于市场分区图上各商圈的招牌。
    labels\text{labels}

预处理与质控

市场开业前,你得先做一次大扫除。有些摊位已经倒闭了,货架上空空如也(总表达量极低);有些摊位其实是两个人挤在一起摆的摊(双细胞 doublet),账目混在一起没法用;还有些摊位货物破损严重,细胞内线粒体基因的比例异常高——这在单细胞数据里是“细胞膜破损”的信号,相当于冷冻鱼化了冻,已经不能卖了。

质控(QC) 就是把这些坏摊位清出账本。常用指标有三个:每个细胞检测到的基因数(n_genes)、总分子计数(n_counts)、线粒体基因比例(pct_mt)。Scanpy 用 sc.pp.filter_cells 配合阈值过滤,Seurat 用 subset 函数,逻辑完全一样。

接着是归一化。因为测序深度不同,有的摊位记的账特别细(测序深),有的特别粗(测序浅),直接比较不公平。解决办法是把每个细胞的备货总量统一成“每万件商品中占比”——这就是 CP10K 归一化,再取对数压缩数值跨度。

质控与归一化是所有下游分析的“干净地基”。很多新手着急跑聚类,结果发现聚类结果全是批次(不同样品)而不是细胞类型——回头一看,正是质控阶段偷了懒。大扫除虽然枯燥,但决定整个市场地图的真实性。

xij′=log⁡2 ⁣(xij∑k=1mxik×104+1)x'_{ij} = \log_2\!\left(\frac{x_{ij}}{\sum_{k=1}^{m} x_{ik}} \times 10^4 + 1\right)
归一化把每个细胞的总量都换算成“一万件货里某某商品占几件”,再用对数压缩,让小差异也能被后续分析看见。
  1. 原始分子数x_ij 是细胞 i 中基因 j 的原始计数,粗细不均不可直接比。
    xijx_{ij}
  2. 细胞总量缩放分母为该细胞所有基因计数之和,让每个细胞的总量归一到 1。
    xij∑k=1mxik\frac{x_{ij}}{\sum_{k=1}^{m} x_{ik}}
  3. 放大到万件乘以 10⁴,得到“每万件商品中该基因占几件”的直观数值。
    ×104\times 10^4
  4. 对数压缩保护log₂(……+1) 压缩数值跨度,同时避免取对数为无穷。
    log⁡2(⋅+1)\log_2(\cdot + 1)

降维与聚类

账本擦干净后,你要开始画市场地图了。可是商品有几万种,每个摊位是一个几万维空间里的点,人脑无法想象。于是先让阿 Py / 小 R 做降维:先用 PCA 找到数据变异最大的几个方向,把几万维压缩到几十个主成分——这就像你先从直升机上看城市,只判断“哪片是住宅区、哪片是工业区”。然后是 UMAP 或 t-SNE 这类非线性降维,把几十维进一步压到二维平面,生成一张人类能直接看懂的“商圈地图”。

地图画好后,聚类负责在图上划分边界。最常用的 Louvain / Leiden 算法先在细胞之间构建“邻居图”——每个细胞连向最像它的 k 个邻居,然后不断尝试切分社区,目标是让社区内部的连接比随机情况更紧密。这个目标用一个叫做模块度 Q 的分数来量化。

降维和聚类是一对配合:降维让大规模计算可行、可视化直观;聚类给出客观的群体划分。但要记住,聚类得到的只是“编号”,不是生物学答案。地图上画出的商圈还要靠下一步“注释”来命名验证。

Q=12m∑i≠j(Aij−kikj2m)δ(ci,cj)Q = \frac{1}{2m}\sum_{i \neq j}\left(A_{ij} - \frac{k_i k_j}{2m}\right)\delta(c_i, c_j)
模块度衡量“商圈划分得好不好”:如果划分后社区内部连接显著多于随机期望,Q 就高,说明商圈边界合理。
  1. 邻居图连边A_ij 记录细胞 i 与细胞 j 在邻居图中是否有连接(1 有 / 0 无)。
    Aij∈{0,1}A_{ij} \in \{0, 1\}
  2. 随机期望连边k_i k_j / 2m 是两个细胞在随机连接时可能出现的期望边数。
    kikj2m\frac{k_i k_j}{2m}
  3. 总边数归一1/2m 让 Q 的取值范围收敛到 [-1, 1],便于跨数据集比较。
    12m\frac{1}{2m}
  4. 同社区判断δ(c_i, c_j) 在两细胞同属一个社区时取 1,否则为 0,只累加社区内部的超额连接。
    δ(ci,cj)\delta(c_i, c_j)

可视化对比

两位助理画地图的风格差异,是很多团队选人的决定性因素。阿 Py(Scanpy)的地图风格是“简洁、静态、可批量产出”:sc.pl.umap 一张散点图,sc.pl.dotplot 一张气泡图,sc.pl.heatmap 一张热图,三张图拼成一张出版级图版就搞定。小 R(Seurat)的地图风格是“丰富、交互、可反复探索”:DimPlot、FeaturePlot、VlnPlot、DotPlot 一应俱全,配合 Shiny 还能生成让 PI 自己在网页上点选的交互面板。

更深一层,可视化算法的数学目标也值得理解。t-SNE 画地图时,希望高维空间里相似的细胞在二维图上依然靠近,这种“地图失真度”用 KL 散度 来度量:高维相似度分布 P 与低维相似度分布 Q 的差异越小,地图越忠实。

可视化是分析的“门面”,也是检查问题的第一道岗。聚类分群是否合理、批次效应是否残留、标记基因是否特异——一张 FeaturePlot 往往比一千行统计输出更直观。所以选工具时,一定要把“谁的地图更符合你团队的汇报习惯”算进成本里。

KL(P∥Q)=∑i≠jpijlog⁡pijqij\text{KL}(P \| Q) = \sum_{i \neq j} p_{ij} \log \frac{p_{ij}}{q_{ij}}
KL 散度像“地图质检员”:把真实距离和图上距离的偏差逐对累加,偏差越大,分数越高。
  1. 高维相似概率p_ij 表示高维空间中细胞 i 与 j 互为邻居的概率。
    pijp_{ij}
  2. 低维相似概率q_ij 表示二维地图上这两个细胞互为邻居的概率。
    qijq_{ij}
  3. 偏差比对数log(p/q) 量化“真实关系”与“图上关系”的偏离方向与程度。
    log⁡pijqij\log \frac{p_{ij}}{q_{ij}}
  4. 全体求和对所有不重复细胞对求和,得到整张地图的总失真量,优化目标就是让它最小。
    ∑i≠j\sum_{i \neq j}

互操作与衔接

现实中的项目很少只用一个工具。很多实验室的现状是:生信老手用 Seurat 做前期的质控与可视化探索,把数据打磨干净;AI 工程师接手后要用 Scanpy 把数据送进深度学习模型(如 scVI、CellTypist)。这时候“账本翻译”就成了刚需——两位助理的档案格式不同,但账本内容是同一本。

翻译的桥梁是标准文件格式。Scanpy 的 AnnData 可以保存为 .h5ad;Seurat 对象可以保存为 .rds 或转成 .h5seurat。通过 SeuratDisk 包可以双向转换:Convert("data.h5ad", "h5seurat", overwrite=TRUE),反过来用 ReadH5AD 也能读。此外,.loom 和 .h5 是两家都承认的中间格式。

不过翻译有“注意事项”:基因 ID 到底是用的是 Symbol(如 CD3D)还是 Ensembl ID(如 ENSG00000167286),两边必须一致,否则合并后基因对不上;表达矩阵是原始 counts 还是归一化后的值,也要在转换前确认好;细胞 barcode 不能有重复。批次效应(不同样品或不同实验批次的技术差异)通常在整合时统一处理,Scanpy 支持 BBKNN、Harmony,Seurat 支持 Harmony、RPCA 等,两边结果可以互相认。

XSeurat↔ConvertXScanpy,dim⁡(X)=n×mX_{\text{Seurat}} \xleftrightarrow{\text{Convert}} X_{\text{Scanpy}}, \qquad \dim(X) = n \times m
互操作的本质是恒等变换:账本的行数和列数一字不变,只是换了装订方式,让两位助理都能读懂。
  1. R 侧矩阵X_Seurat 是 Seurat 对象中保存的表达矩阵。
    XSeuratX_{\text{Seurat}}
  2. Python 侧矩阵X_Scanpy 是 AnnData 中保存的表达矩阵。
    XScanpyX_{\text{Scanpy}}
  3. 双向转换通过 h5ad / h5seurat / loom 等格式,两套工具可以互相读取对方的数据。
  4. 维度不变转换只改格式不改数据,行数 n(细胞数)与列数 m(基因数)保持不变。
    dim⁡(X)=n×m\dim(X) = n \times m

选型与实战建议

最后回到那个最实际的问题:我的项目到底该用谁?答案不是“看哪个教程多”,而是看你的团队构成和数据规模。

如果团队以 Python / 机器学习背景为主,未来要做大量深度嵌入、模型训练、云端流程调度,那么 Scanpy 是自然选择——h5ad 支持内存映射(mmap),百万级细胞也能在普通服务器上流畅处理,配合 scvi-tools、squidpy 等扩展生态非常顺滑。如果团队以 R / 生信传统背景为主,日常产出以探索性图表和交互式报告为主,那么 Seurat 更顺手——它的图形语法、Shiny 集成和丰富的在线教程让新手更容易上手。

如果数据规模特别大(例如人类细胞图谱项目超过百万细胞),Scanpy 的内存效率优势会明显放大;而如果项目强调多组学联合分析(RNA + 蛋白 + ATAC 同时测),Seurat 的多模态对象设计更省心。当然,更常见的现实是“混合工作流”:Seurat 做前期打磨,Scanpy 做批量整合和模型嵌入,最后再回 Seurat 画图汇报。选型不是站队,而是排兵布阵。

没有哪套工具是银弹。真正专业的工作流,往往是“让合适的人用合适的工具干合适的活”——理解了这一点,你就从“工具的使用者”进阶为“分析方案的架构师”。

S=0.4×E+0.3×P+0.3×CS = 0.4 \times E + 0.3 \times P + 0.3 \times C
选型可以理解为三个维度的加权打分:生态契合度、规模性能、可视化交互,权重由项目实际情况决定。
  1. 生态契合度E 代表团队成员既有的语言熟练度与代码资产匹配程度。
    EE
  2. 规模性能P 代表工具在海量细胞数据处理中的内存与速度表现。
    PP
  3. 交互可视化C 代表交互式探索、图表定制与汇报呈现的便利程度。
    CC
  4. 加权总分把三项按 0.4 / 0.3 / 0.3 的权重相加,就是可比较的选型分。
    S=0.4E+0.3P+0.3CS = 0.4E + 0.3P + 0.3C

AI 应用场景

肿瘤免疫微环境解析

阶段:数据分析与细胞注释

解决的问题:传统流式细胞术一次只能看十几个标记物,无法在数万个细胞中无偏发现稀有亚群。单细胞测序配合 Seurat 的完整分析流程,能在全转录组尺度上自动划分细胞群体并注释身份。

具体案例:某肿瘤样本用 Seurat 跑完质控 → 归一化 → 聚类 → marker 注释后,发现一群此前被忽略的 CD8+ 耗竭 T 细胞。研究团队再结合轨迹分析定位其分化路径,为免疫治疗靶点筛选提供了直接线索。整个过程从原始数据到细胞图谱,完全在 Seurat 的图形化工作流中完成。

跨语言团队的混合分析流水线

阶段:数据工程与模型训练衔接

解决的问题:实验室传统上使用 Seurat 做可视化探索,而深度学习团队需要用 Python 框架(如 scVI、CellTypist)训练细胞嵌入模型。两套工具格式不通,导致协作断裂、重复造轮子。

具体案例:某项目组先用 Seurat 完成质控与归一化,通过 SeuratDisk::Convert 转成 h5ad;Scanpy 读入后直接对接 scVI 训练潜空间表示,再用 scanpy.read_h5ad 把结果导出;最后转回 Seurat 对象,在原有流程里完成论文级可视化。整个混合流水线两天跑通,两组成员各自发挥所长。

大规模细胞图谱构建

阶段:规模化数据集成

解决的问题:构建器官级或物种级细胞图谱需要处理数十万到上百万细胞,R 对象在内存中容易爆掉,流程需要支持集群调度和断点续跑。

具体案例:某人类细胞图谱子项目包含 120 万细胞、三个捐赠者、八个批次。团队选择 Scanpy + h5ad 内存映射做全量整合,用 Harmony 做批次校正,Leiden 聚类完成后,将各亚群子集导出到 Seurat 做精细可视化与标记基因审查。Scanpy 负责“扛重活”,Seurat 负责“精打磨”,各取所长。

Python 演示

python86 行
"""从零实现单细胞分析核心流程:质控 -> 归一化 -> 高变基因 -> PCA -> 聚类"""import numpy as np  def generate_demo_data(n_cells=600, n_genes=2000, seed=42):    """生成模拟单细胞表达矩阵:4 种细胞类型,每种有专属高表达基因,含10%破损细胞"""    rng = np.random.RandomState(seed)    X = rng.poisson(0.5, size=(n_cells, n_genes)).astype(float)    types = []    n_each = n_cells // 4    for ci in range(4):        for i in range(ci * n_each, (ci + 1) * n_each):            if rng.rand() < 0.1:                X[i] = 0                X[i, :10] = rng.poisson(1, size=10)            else:                X[i, ci * 50:(ci + 1) * 50] += rng.poisson(5, size=50)            types.append(ci)    return X, np.array(types)  def quality_control_mask(X, min_counts=30, max_counts=2000):    """质控:保留总表达量适中的细胞(模拟摊位整洁度检查)"""    counts = X.sum(axis=1)    return (counts >= min_counts) & (counts <= max_counts)  def normalize_cp10k(X, target=1e4):    """CP10K 归一化 + log1p 对数变换"""    X_norm = X / X.sum(axis=1, keepdims=True) * target    return np.log1p(X_norm)  def select_hvg(X, top=50):    """高变基因筛选:按表达方差取 top K"""    variances = X.var(axis=0)    return np.argsort(variances)[-top:]  def run_pca(X, n_components=20):    """PCA 主成分分析:特征分解协方差矩阵求主方向"""    Xc = X - X.mean(axis=0)    cov = np.cov(Xc.T)    _, eigvecs = np.linalg.eigh(cov)    return Xc @ eigvecs[:, -n_components:]  def kmeans(X, k=4, iters=20):    """KMeans 聚类:迭代更新簇中心"""    rng = np.random.RandomState(0)    centers = X[rng.choice(len(X), k, replace=False)]    for _ in range(iters):        dists = np.linalg.norm(X[:, None, :] - centers[None, :, :], axis=2)        labels = dists.argmin(axis=1)        for c in range(k):            if np.any(labels == c):                centers[c] = X[labels == c].mean(axis=0)            else:                centers[c] = X[rng.choice(len(X))]    return labels  def match_accuracy(pred, truth):    """把聚类簇号映射到真实类型,计算匹配准确率"""    mapping = {}    for p in np.unique(pred):        majority = np.bincount(truth[pred == p]).argmax()        mapping[p] = majority    mapped = np.array([mapping[p] for p in pred])    return (mapped == truth).mean()  def main():    X, truth = generate_demo_data()    mask = quality_control_mask(X)    X, truth = X[mask], truth[mask]    print(f"质控保留 {len(truth)}/600 个细胞")    X = normalize_cp10k(X)    genes = select_hvg(X, top=50)    pc = run_pca(X[:, genes], n_components=20)    pred = kmeans(pc, k=4)    print(f"聚类准确率: {match_accuracy(pred, truth):.1%}")  if __name__ == "__main__":    main()
目的:用纯 NumPy 模拟 Scanpy 单细胞分析的骨干流程——从一个模拟表达矩阵出发,依次完成质控、归一化、高变基因筛选、PCA 降维和 KMeans 聚类,并评估聚类准确率,帮初学者建立“每一步到底在做什么”的直觉。
  1. X[i, ci * 50:(ci + 1) * 50] += rng.poisson(5, size=50)给每种细胞类型注入 50 个专属高表达基因,模拟“细胞身份指纹”。
  2. mask = quality_control_mask(X)用总表达量阈值筛掉低质量细胞,相当于清退空置或破损摊位。
  3. X / X.sum(axis=1, keepdims=True) * targetCP10K 归一化,把每个细胞总量统一成一万份再算占比。
  4. return np.log1p(X_norm)log1p 对数压缩,避免高表达基因在数值上碾压低表达基因。
  5. variances = X.var(axis=0)逐基因计算方差,方差大的基因更可能是区分细胞类型的“招牌商品”。
  6. np.cov(Xc.T)中心化后计算基因协方差矩阵,这是 PCA 求主方向的输入。
  7. Xc @ eigvecs[:, -n_components:]把数据投影到方差最大的 20 个主成分方向,完成线性降维。
  8. np.linalg.norm(X[:, None, :] - centers[None, :, :], axis=2)广播计算每个细胞到所有簇中心的欧氏距离。
  9. centers[c] = X[labels == c].mean(axis=0)把簇中心更新为组内细胞的均值,若空簇则随机初始化。
  10. maj = np.bincount(truth[pred == p]).argmax()用多数投票将聚类编号映射到真实细胞类型,用于评估准确率。

常见误区

误区很多新手以为这两个工具是竞争对手,项目一开始就要“站队”,选了 Seurat 就不能碰 Scanpy,反过来也一样。看到同事两个都在用,就觉得不专业。

正解两者通过 h5ad、h5seurat、loom 等格式可以双向转换,实际工业界最常见的做法恰恰是混合使用——Seurat 做交互式探索与质控,Scanpy 做大规模整合和深度学习嵌入,最后再转回 Seurat 出图。工具是手段,分析目标才是目的。

为何常见:市面上的教程大多只讲其中一个工具,社区讨论也常常“各自为营”,导致初学者误以为两者是互斥选项。

误区看到 `log1p` 归一化后的矩阵,以为这些数字仍然代表基因的绝对分子数,直接拿两个细胞、两个基因之间的归一化值比较大小,甚至算“倍数变化”。

正解归一化后的数值只是“相对丰度”,相当于把每个摊位总量统一成一万件后算出的占比。它能用来比较“基因 A 在细胞 X 中是否比在细胞 Y 中相对更高”,不能用来回答“基因 A 在细胞 X 里到底有多少个分子”。后一个问题必须回到原始 counts 才能回答。

为何常见:归一化后的数字看起来还是“计数”,加上教程常说的“标准化”一词过于笼统,很容易让人忘记中间发生过缩放变换。

误区Leiden 聚类输出 0、1、2、3 编号,初学者就把 0 号群当成“T 细胞”、1 号群当成“B 细胞”,以为聚类结果就是生物学答案。

正解聚类只找到“统计上的群体结构”,它不知道这些群体是什么细胞。你必须用标记基因(如 CD3D 对应 T 细胞)逐一验证每个簇的表达特征,再结合生物学知识命名;同一群可能含有稀有亚群,也可能被过分割。商圈边界只是统计结果,“这里是水果市场”需要实地走访才能定论。

为何常见:软件输出编号太像“结论”,加上新手还不熟悉标记基因注释这一步,容易把计算中间结果当最终答案。

误区对着 UMAP 散点图说:“这两个点挨在一起,它们肯定是一类细胞;这两个点隔得远,差异一定很大。” 甚至用图上距离量化细胞差异。

正解UMAP 的首要目标是保持“局部结构”,它会刻意压缩或拉伸全局距离。图上相邻的细胞确实大概率相似,但图上很远的两个点,真实差异可能很大也可能很小。想严谨比较差异,应该回到 PCA 空间或直接看标记基因表达量。地图上两个摊位挨得近,不代表卖的一样。

为何常见:人脑天然把二维距离理解为差异程度,而 UMAP 这种非线性降维图恰恰打破了这种直觉,初学者很容易被“看起来像地图”欺骗。

面试题

Q1. Scanpy 是哪种编程语言生态中的单细胞分析工具?

单选题 · easy

  • A.Python 生态分析工具← 正确答案
  • B.R 语言生态分析工具
  • C.Java 生态分析工具
  • D.C++ 生态分析工具

答案:A。Scanpy 基于 Python 生态,核心数据结构是 AnnData,常用在深度学习与流程自动化场景;Seurat 才是 R 生态的工具。记住“Scanpy 配 Python,Seurat 配 R”这对搭档即可。

Q2. Seurat 中典型的单细胞数据结构是什么?

单选题 · easy

  • A.AnnData 对象
  • B.SeuratObject 对象← 正确答案
  • C.DataFrame 数据表
  • D.NumPy 多维数组

答案:B。Seurat 以 SeuratObject 为容器,内部包含 assays 表达数据、meta.data 细胞元数据和 reductions 降维坐标。AnnData 是 Scanpy 的对象,DataFrame 和 NumPy 数组只是底层存储的一环。

Q3. 单细胞质控中,线粒体基因比例过高通常反映什么?

单选题 · easy

  • A.细胞代谢非常旺盛
  • B.细胞膜破损或已死亡← 正确答案
  • C.基因测序出现错误
  • D.数据格式不正确

答案:B。线粒体基因比例过高是单细胞数据的经典“破损信号”——细胞破裂后胞质 mRNA 流失而线粒体 RNA 相对保留,类似冷冻鱼化冻后不再新鲜,这类细胞应在质控阶段过滤掉。

Q4. Scanpy 中常见的细胞水平归一化处理是?

单选题 · mid

  • A.按每个细胞总量缩放后取对数← 正确答案
  • B.按每个基因中位数中心化
  • C.对所有数值整体做最大最小值归一化
  • D.直接对原始计数做 Z-score 标准化

答案:A。单细胞归一化的标准做法是 CP10K/CPM——先按细胞总量缩放(如每万分子占比),再取 log1p 压缩动态范围。按基因中心化或整体 min-max 缩放无法校正不同细胞间测序深度的差异。

Q5. PCA 线性降维选择的坐标方向满足什么性质?

单选题 · mid

  • A.数据投影后方差最大← 正确答案
  • B.数据投影后方差最小
  • C.让类别之间的间距最小
  • D.让每个类别样本数相等

答案:A。PCA 的目标是找到数据变异最大的正交方向,投影后方差越大代表信息保留越多,相当于从直升机上找到最能区分地面的“大趋势”。方差最小、间距最小等说法都与 PCA 的设计初衷相反。

Q6. 关于 UMAP 与 t-SNE,下列说法正确的是?

多选题 · mid

  • A.UMAP 更注重保持局部结构← 正确答案
  • B.t-SNE 能很好地保留全局距离关系
  • C.UMAP 计算速度通常更快← 正确答案
  • D.两者都能保留全局距离关系

答案:AC。UMAP 和 t-SNE 都优先保局部结构,但二者都会扭曲全局距离,B 和 D 都不成立。UMAP 由于使用更高效的图算法,在大数据集上通常明显快于 t-SNE,因此成为当前单细胞可视化的默认选择。

Q7. Louvain/Leiden 聚类优化的模块度 Q 表示什么?

单选题 · mid

  • A.社区内部连接比随机期望更紧密的程度← 正确答案
  • B.每个细胞总表达量的大小
  • C.基因变异程度的强弱
  • D.批次效应强度的指标

答案:A。模块度把“实际社区内部连边数”与“随机连接假设下的期望连边数”做差并归一化。Q 越高说明划分出的社区越“货真价实”,类似商圈内部商户往来越频繁,越说明这是一个真正的商圈。

Q8. Scanpy 与 Seurat 互相转换数据时,需要注意哪些事项?

多选题 · hard

  • A.基因 ID 使用的是 Symbol 还是 Ensembl ID← 正确答案
  • B.表达量是原始计数还是归一化后的值← 正确答案
  • C.细胞 barcode 是否存在重复← 正确答案
  • D.转换前必须先完成批次校正

答案:ABC。转换的本质是换装订不改账本,但基因 ID 类型不一致会导致合并错位、表达量类型混乱会误导下游分析、barcode 重复会造成索引冲突。批次校正是分析流程中的一个环节,不是格式转换的前提,转换后再做完全可行。

Q9. 要处理百万级细胞数据且追求内存效率与批处理自动化,更推荐?

单选题 · hard

  • A.Seurat 因为可视化交互体验更好
  • B.Scanpy 因为 h5ad 支持内存映射← 正确答案
  • C.用 Excel 逐批人工检查最稳妥
  • D.两者的内存表现完全一样

答案:B。Scanpy 基于 h5ad 格式支持内存映射(mmap),百万级矩阵无需全部加载进内存即可切片计算,配合集群调度天然适合大规模自动化。Seurat 的交互体验虽好,但全对象常驻内存在大数据上会吃紧。Excel 处理单细胞数据则完全不现实。

Q10. 关于批次整合与互操作,下列说法错误的是?

多选题 · extreme

  • A.BBKNN 可以用于批次校正
  • B.整合后的降维坐标无法导出← 正确答案
  • C.批次整合应在聚类前完成
  • D.合并样本前无需任何批次处理← 正确答案

答案:BD。BBKNN 是 Scanpy 常用的批次校正方法,A 正确;整合后的降维坐标保存在 reductions 里,可随 h5ad/h5seurat 正常导出,B 错误;批次校正应在聚类前完成,否则聚类会按批次而非生物学差异分群,C 正确;不同样本/批次合并通常需要批次处理,D 错误。题目问“错误的”,故选 B、D。