前六篇我们分别学习了 Python 基础、进阶语法、pandas 数据框、AnnData 数据结构、单样本 pipeline 与多样本整合,以及细胞类型注释。本篇把两条常用路线放在同一工作流中:CellTypist 判断细胞更像参考中的哪个标签,scVI 从原始计数学习低维表示。标签不是潜空间,潜空间也不会自动给出可靠标签。两条路线的输入不能混用,结果保存位置必须清楚,标签和潜空间都要经过样本与生物学审核。
一、两条路线的输入不能混用
方法输入矩阵其他输入输出
CellTypist全基因、总量归一化后的 log1p 表达参考模型、基因符号标签与置信信息
scVI原始 UMI 计数batch_key 和可选协变量模型与潜空间
先检查对象:
print(adata.shape)print(adata.layers.keys())print(adata.obs[[”sample_id”, ”batch”]].head())
输出类似:
(8000, 20000)KeysView(AnnData object with layer: 'counts', 'log1p', 'raw') sample_id batchAAAC-1 sample_01 batch1AAAC-2 sample_01 batch1AAAC-3 sample_02 batch2AAAC-4 sample_02 batch2AAAC-5 sample_03 batch3
参数含义
adata.shape返回 (n_cells, n_genes),确认全基因维度
adata.layers.keys()列出可用矩阵层,确认有 counts 和 log1p
adata.obs[...]查看样本和批次元数据是否已就位
这三行分别检查细胞×基因维度、可用矩阵层和批次元数据。如果没有明确的 counts 层,不能把当前 X 直接交给 scVI;如果对象只剩高变基因,也不适合直接交给 CellTypist。
二、CellTypist 参考注释
1. 准备注释副本
query = adata.copy()query.X = query.layers[”log1p”].copy()
注释函数可能读取或转换 X。使用副本可以避免影响主对象中的矩阵语义。运行后,query.X 是全基因 log1p 表达,主对象保持不变。
2. 加载参考模型并检查基因交集
import celltypistmodel = celltypist.models.Model.load(model=”reference_model.pkl”)model_genes = set(model.features)query_genes = set(query.var_names)overlap = model_genes & query_genesprint(”query genes:”, len(query_genes))print(”model genes:”, len(model_genes))print(”overlap:”, len(overlap))
输出类似:
query genes: 20000model genes: 18000overlap: 15500
参数含义
model.features参考模型训练时使用的基因符号列表
query.var_names查询对象的基因名
overlap两者交集基因数,应占模型基因大部分
交集不是越大就一定越好,但若只剩很少基因,应先检查:
- 使用的是 gene symbol 还是 Ensembl ID;
不要通过随意改名提高交集;映射必须有明确规则。
3. 运行注释
prediction = celltypist.annotate( query, model=model, majority_voting=True,)result = prediction.predicted_labels.copy()print(result.head())
输出类似:
predicted_labels majority_voting confidence_scorecellAAAC-1 T cells T cells 0.92AAAC-2 B cells B cells 0.88AAAC-3 T cells T cells 0.95AAAC-4 Monocytes Monocytes 0.79AAAC-5 B cells B cells 0.91
参数含义
query全基因 log1p 副本
model参考模型对象
majority_voting是否在局部邻域内平滑标签
model 指定参考模型;majority_voting=True 会结合局部邻域产生平滑候选标签;逐细胞预测与 majority voting 都应保留,不要用后者覆盖前者;标签表的索引应与 query.obs_names 一致。
4. 写回主对象
result = result.reindex(adata.obs_names)adata.obs[”celltypist_label”] = result[”predicted_labels”]if ”majority_voting” in result: adata.obs[”celltypist_majority”] = result[”majority_voting”]
只向 adata.obs 添加候选标签,不改变表达矩阵。写回前 reindex() 是关键:它保证结果按细胞名对齐,而不是按当前行序盲目赋值。
5. 检查标签质量
print(pd.crosstab(adata.obs[”sample_id”], adata.obs[”celltypist_label”]))sc.pl.umap( adata, color=[”celltypist_label”, ”sample_id”, ”leiden”],)
输出类似:
celltypist_label B cells Monocytes T cells NK cellssample_idsample_01 320 180 450 50sample_02 280 220 380 40sample_03 350 150 400 60
怎样看结果:
自动标签是候选,不是最终真值。需要结合标志基因、样本一致性和参考适用范围审核。
记忆: CellTypist 需要全基因 log1p 输入,scVI 需要原始计数。两条路线各自准备独立副本,不要共用同一个 X。标签写回主对象前必须 reindex() 对齐索引。
三、scVI 潜空间
1. 在登记前冻结对象结构
scvi_adata = adata.copy()scvi_adata.X = scvi_adata.layers[”counts”].copy()print(scvi_adata.shape)print(scvi_adata.obs[”batch”].value_counts())
输出类似:
(8000, 20000)batch1 1800batch2 2200batch3 1500batch4 2500Name: count, dtype: int64
scVI 登记后会记录计数层、批次和协变量。登记之后再删除细胞、重排基因或替换层,会使模型登记与对象不一致。因此,过滤、拼接和 metadata 整理应在这一步之前完成。
2. 登记 AnnData
import scviscvi.model.SCVI.setup_anndata( scvi_adata, layer=”counts”, batch_key=”batch”,)
参数含义
layer指定原始计数所在层,不能传 log1p
batch_keyadata.obs 中批次列名
categorical_covariate_keys可选额外协变量列名列表
setup_anndata() 不训练模型,只告诉 scVI:原始计数在哪一层、哪个字段代表批次。运行后对象中会增加 scVI 登记信息,但还没有潜空间。
3. 建模并训练
model = scvi.model.SCVI( scvi_adata, n_latent=20, n_layers=2,)model.train( max_epochs=200, early_stopping=True,)
参数含义
n_latent每个细胞潜空间维度
n_layers神经网络层数
max_epochs最大训练轮次
early_stopping监控指标不再改善时提前停止
训练结束不等于批次处理合理。需要查看训练历史,并检查批次和目标状态在潜空间中的表现。
4. 提取潜空间并建图
scvi_adata.obsm[”X_scVI”] = model.get_latent_representation()sc.pp.neighbors(scvi_adata, use_rep=”X_scVI”, n_neighbors=15)sc.tl.umap(scvi_adata, random_state=0)sc.tl.leiden(scvi_adata, resolution=0.8, key_added=”leiden_scVI”)
参数含义
use_rep使用哪个嵌入矩阵构建邻图
n_neighbors每个细胞的邻居数
resolutionLeiden 聚类粒度,越大簇越多
key_added聚类标签存入 obs 的列名
对象变化:
- obsp/uns:基于 scVI 潜空间的新邻图;
- obsm['X_umap']:由新邻图产生的 UMAP;
- obs['leiden_scVI']:潜空间邻图上的聚类。
5. 检查过度校正与混杂
sc.pl.umap( scvi_adata, color=[”batch”, ”sample_id”, ”leiden_scVI”],)
理想目标不是让所有批次完全混成一团,而是在相同状态内减弱技术分离,同时保留目标生物差异。如果批次与条件完全混淆,任何模型都无法从数据中可靠拆开二者。
记忆: setup_anndata() 只登记不训练。登记后不可再改对象结构。潜空间 UMAP 不追求批次完全混合,而要在去技术分离的同时保留生物差异。
四、两条路线怎样联用
推荐顺序:
counts对象→ scVI潜空间→ neighbors/UMAP/聚类→ 独立全基因log1p副本→ CellTypist候选标签→ 标志与样本一致性审核
不要把 X_scVI 交给 CellTypist,也不要把 CellTypist 标签当作 scVI 的训练真值。
完整输出保存
from pathlib import Pathoutput_dir = Path(”results”)output_dir.mkdir(parents=True, exist_ok=True)model.save(output_dir / ”scvi_model”, overwrite=True)scvi_adata.write_h5ad(output_dir / ”scvi_latent_with_labels.h5ad”)
参数含义
overwrite=True若目录已存在则覆盖旧模型文件
write_h5ad保存含潜空间、邻图、标签的完整对象
保存模型和 AnnData 缺一不可:模型用于以后映射或重建输出,AnnData 保存当前细胞索引、潜空间、邻图和标签。
五、完整流程代码
# ============================================================# 完整流程:CellTypist 自动注释 + scVI 潜空间# ============================================================import scanpy as scimport scviimport celltypistimport pandas as pdfrom pathlib import Path# ---------- 0. 前置检查 ----------print(adata.shape)print(adata.layers.keys())print(adata.obs[[”sample_id”, ”batch”]].head())# ---------- 1. scVI 潜空间 ----------scvi_adata = adata.copy()scvi_adata.X = scvi_adata.layers[”counts”].copy()scvi.model.SCVI.setup_anndata( scvi_adata, layer=”counts”, batch_key=”batch”,)model = scvi.model.SCVI( scvi_adata, n_latent=20, n_layers=2,)model.train(max_epochs=200, early_stopping=True)scvi_adata.obsm[”X_scVI”] = model.get_latent_representation()sc.pp.neighbors(scvi_adata, use_rep=”X_scVI”, n_neighbors=15)sc.tl.umap(scvi_adata, random_state=0)sc.tl.leiden(scvi_adata, resolution=0.8, key_added=”leiden_scVI”)# ---------- 2. CellTypist 注释 ----------query = adata.copy()query.X = query.layers[”log1p”].copy()ct_model = celltypist.models.Model.load(model=”reference_model.pkl”)prediction = celltypist.annotate( query, model=ct_model, majority_voting=True,)result = prediction.predicted_labels.copy()result = result.reindex(adata.obs_names)adata.obs[”celltypist_label”] = result[”predicted_labels”]if ”majority_voting” in result: adata.obs[”celltypist_majority”] = result[”majority_voting”]# ---------- 3. 质控与可视化 ----------print(pd.crosstab(adata.obs[”sample_id”], adata.obs[”celltypist_label”]))sc.pl.umap(scvi_adata, color=[”batch”, ”leiden_scVI”])# ---------- 4. 保存 ----------output_dir = Path(”results”)output_dir.mkdir(parents=True, exist_ok=True)model.save(output_dir / ”scvi_model”, overwrite=True)scvi_adata.write_h5ad(output_dir / ”scvi_latent_with_labels.h5ad”)adata.write_h5ad(output_dir / ”annotated_full.h5ad”)
记忆: 完整流程中 scVI 先行(需要原始计数),CellTypist 后行(需要全基因 log1p),两者使用各自的副本,最后统一保存。顺序不可颠倒:先建潜空间和聚类,再注释标签。
六、常见错误
CellTypist 标签几乎全相同参考域错配或基因交集不足回到模型、物种和全基因输入检查
注释结果行数对不上细胞索引顺序变化按 obs_names 重新索引
scVI 报非计数错误传入了 log1p 表达回到 layers['counts']
setup 后对象变更报错登记生命周期被破坏重新整理对象并再次 setup
UMAP 批次消失但状态也混合过度校正检查批次—条件关系并调整设计
本篇小结
CellTypist 提供参考标签候选,scVI 提供计数生成模型潜空间。两者最重要的共同原则是:输入矩阵值域明确、细胞索引对齐、结果保存位置清楚、标签和潜空间都要经过样本与生物学审核。
下一篇我们将学习差异表达分析——如何在单细胞数据中正确进行基因层面的差异检验,以及如何为下游通路富集准备有方向的统计量排序。