资讯动态

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

发布时间:2026/9/23 23:59:56 来源:尧图企业网站定制
简介一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码针对计算机相关专业正在做毕设或需要项目实战的学习者可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过可直接运行覆盖数据预处理、特征筛选、模型构建、训练测试与结果预测等核心流程并附有大量功能测试脚本及README说明便于从零掌握单细胞数据分析与深度学习结合的实现思路。压缩包共90个文件以61个py源码文件为主辅以7个xml配置文件、3个csv数据标签、6个pyc缓存文件及md/txt说明文档整体仅235KB轻量易部署。目前已有85人学习浏览适合希望快速复现算法流程、参考毕业设计架构的学习者。1. 单细胞注释不是分类题是决策题拿到一份 scRNA-seq 数据绝大多数人的第一反应是“跑个聚类看有几个群”。但聚类只是形状注释才是把形状翻译成生物学结论的步骤。一个 cluster 是 T 细胞还是 NK 细胞单靠聚类算法永远答不出来。细胞类型注释的本质是在表达矩阵上执行一次有约束的映射既要保留数据驱动的分群结构又要参考已知的 marker 基因或参考注释集。这也是为什么“注释算法研究”在很多毕业论文里既做方法对比又做流程整合。本文围绕 Python 生态主要是 Scanpy 分类器 参考映射拆解一套可复现的注释方案从数据质检到 marker 打分从有监督训练到批次处理最后给出参数调优与排错思路。适合正在做毕业设计、或者刚接手单细胞流程但不想只调包的工程师。2. 先看懂注释的三种技术路线再写代码2.1 基于 marker 基因的特征打分法最早也是最直观的注释方法人为指定每个细胞类型的标志基因然后看每个 cluster 中这些基因的表达是否显著富集。常见做法是先用scanpy.tl.score_genes给每个聚类打分再结合rank_genes_groups的结果做人工核对。import scanpy as sc import numpy as np adata sc.read_h5ad(filtered_clusters.h5ad) # 准备 marker 列表CD3D/CD3E 标记 T 细胞MS4A1 标记 B 细胞 markers { T_cell: [CD3D, CD3E], B_cell: [MS4A1, CD79A], Monocyte: [LYZ, CD14] } # 为每个 cluster 计算 marker 打分 for cell_type, gene_list in markers.items(): # 过滤掉数据集中不存在的基因避免 KeyError valid_genes [g for g in gene_list if g in adata.var_names] if valid_genes: sc.tl.score_genes(adata, gene_listvalid_genes, score_namefscore_{cell_type}) # 查看每个 cluster 的平均打分 df adata.obs.groupby(leiden)[[fscore_{t} for t in markers]].mean() print(df.round(3))这段代码的关键是score_genes会计算每个细胞中指定基因相对随机基因集的表达富集程度输出为 z-score 形式的得分。判断某个 cluster 是什么类型不是看绝对分而是横向比较不同 cluster 在同一 marker 组上的得分差异。比如 T_cell 得分在 cluster 0 是 1.8、在 cluster 3 是 -0.2那 cluster 0 倾向归为 T 细胞。这个方法的局限也很明确marker 列表基本靠人工维护不同文献给出的 marker 往往不一致并且低质量细胞或 doublet 会造成假阳性打分。它适合做初步筛选不适合直接当最终结论。2.2 基于参考数据集的标签迁移方法如果手头有标注好的公共数据集比如 Human Cell Atlas 或某个组织的参考图谱更稳定做法是把“未知标签的查询集”映射到“已知标签的参考集”然后取近邻标签作为预测结果。import scanpy as sc import numpy as np from sklearn.neighbors import KNeighborsClassifier # 假设 ref_adata 已注释好label 存在 obs[cell_type] ref_adata sc.read_h5ad(reference_atlas.h5ad) query_adata sc.read_h5ad(query_raw.h5ad) # 用参考集的高变基因作为特征空间 sc.pp.normalize_total(ref_adata, target_sum1e4) sc.pp.log1p(ref_adata) sc.pp.highly_variable_genes(ref_adata, n_top_genes2000, flavorseurat) hv_genes ref_adata.var[highly_variable].values # 查询集也做同样归一化并只保留参考集的特征基因 sc.pp.normalize_total(query_adata, target_sum1e4) sc.pp.log1p(query_adata) query_adata query_adata[:, ref_adata.var_names].copy()基因对齐做完后用 PCA 嵌入向量训练一个 KNN 分类器。这里用 KNN 而不是直接拼表达值的原因在于表达谱噪声大直接算欧氏距离会被高表达基因主导而 PCA 层已经做了去相关和降维距离度量更稳定。# 参考集 PCA 嵌入 sc.pp.pca(ref_adata, n_comps50) X_ref ref_adata.obsm[X_pca] y_ref ref_adata.obs[cell_type].values # 查询集投影到参考的 PCA 空间 sc.pp.pca(query_adata, n_comps50) X_query query_adata.obsm[X_pca] # KNN 分类k 取 10~30具体看参考集规模 knn KNeighborsClassifier(n_neighbors15, weightsdistance) knn.fit(X_ref, y_ref) pred knn.predict(X_query) query_adata.obs[pred_celltype] pred参数上最值得注意的其实不是n_neighbors而是 PCA 的n_comps。单细胞数据往往只有几千个高变基因但有效信号可能只有 20~30 个主成分设成 50 可能引入噪声设成 10 又会丢掉弱信号。一般建议用sc.tl.pca的方差解释曲线看看拐点按拐点选主成分个数这也是我处理参考映射时固定顺序先对齐基因再选主成分再跑 KNN。2.3 基于有监督分类器的注释方法参考映射的升级版是把问题当作标准的监督分类任务特征工程的方式可以更灵活。除了 PCA还可以直接输入高变基因的表达值、或者用 marker 基因集打分拼成的“特征面板”。分类器上常用随机森林、XGBoost 或带 Dropout 的全连接网络。import xgboost as xgb from sklearn.model_selection import cross_val_score # 用高变基因表达矩阵作为输入 X_train ref_adata[:, ref_adata.var[highly_variable]].X.toarray() y_train ref_adata.obs[cell_type].values model xgb.XGBClassifier( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.6, eval_metricmlogloss, tree_methodhist, n_jobs8 ) # 交叉验证估计泛化能力 scores cross_val_score(model, X_train, y_train, cv5) print(CV accuracy:, scores.mean())树模型对单细胞表达矩阵有一个天然优势数值经过 log 归一化后并非线性可分而树模型可以通过分裂点自动处理非线性边界。相比 KNNXGBoost 对参考集中的稀有细胞类型更友好因为 KNN 的决策边界会被多数类样本拉扯而树的每次分裂只考虑当前节点的纯度和信息增益不会受全局类别分布主导。需要额外说明的是XGBoost 训练阶段如果直接把.X矩阵传入数据量在五万细胞以上时内存会飙升。建议用 PCA 层或挑选 top 500~1000 个高变基因做特征压缩而不是全量喂入这一步能省下 60% 的训练时间和内存。2.4 三条路线的选型判断实际项目里我会按一个简单的规则选型数据量小于 2 万细胞、且参考集与目标组织同源时用 PCAKNN 标签迁移最稳妥因为参数少、可解释性强且不容易过拟合。当参考集规模偏大且来源批次参差时优先考虑带类别权重调整的树模型。而如果样本来自全新组织、没有可靠参考集就只能退回 marker 打分加人工经验。千万不要在拿不到可靠参考集的情况下强行训练分类器。“用模型把未知分出来”听上去很智能但监督学习的目标分布是参考集定义的如果参考集里没有某个细胞类模型永远不可能把它分对。3. 搭建一套可复现的注释分析流程3.1 原始数据处理到聚类分群的完整链路注释的前提是把细胞分群做好。经验不足的分析者会在未过滤低质量细胞时直接跑聚类然后发现 cluster 数量异常偏多、marker 表达混乱。我会把上游到注释的流程固定为下面这个顺序而不是想到哪步做哪步。import scanpy as sc import scrublet as scr adata sc.read_10x_h5(filtered_feature_bc_matrix.h5) # 基础质控过滤低质量细胞和低表达基因 sc.pp.filter_cells(adata, min_genes200) sc.pp.filter_genes(adata, min_cells3) # 线粒体基因比例高比例代表细胞状态差 adata.var[mt] adata.var_names.str.startswith(MT-) sc.pp.calculate_qc_metrics(adata, qc_vars[mt], percent_topNone, log1pFalse) adata adata[adata.obs[pct_counts_mt] 20, :] adata adata[adata.obs[n_genes_by_counts] 6000, :]n_genes_by_counts上限的确定需要看测序深度。表观上设置 6000 是偏保守的做法如果文库很深会在 8000~10000 之间才出现拐点。一个比较实用的办法是画sc.pl.violin(adata, keys[n_genes_by_counts])看分布而不是硬套阈值。# doublet 检测用 Scrublet 识别多细胞捕获 scrub scr.Scrublet(adata.X) doublet_scores, predicted_doublets scrub.scrub_doublets() adata.obs[doublet_score] doublet_scores adata adata[~predicted_doublets, :] # 归一化、高变基因、PCA、邻居图、聚类 sc.pp.normalize_total(adata, target_sum1e4) sc.pp.log1p(adata) sc.pp.highly_variable_genes(adata, n_top_genes2000, flavorseurat) adata adata[:, adata.var[highly_variable]].copy() sc.pp.pca(adata, n_comps30) sc.pp.neighbors(adata, n_neighbors15, n_pcs20) sc.tl.leiden(adata, resolution0.5)关键参数在sc.pp.neighbors的n_neighbors。注释任务不同于纯聚类探索我们既要维持正确分群又要避免把同一类型的细胞碎成多个群。n_neighbors设大20~30会让局部结构模糊小类群容易被吞并设小5~10则分群过多后面对照 marker 会非常痛苦。我一般先跑 15看 cluster 数量在 8~15 之间就继续如果是 25 个以上就提高到 20。3.2 用 marker 打分与 rank_genes 半自动注释聚类完成后下一步是找出每个 cluster 的差异基因与公共 marker 列表重合并给出候选类型。这里可以做一个“题名对账”式的匹配而不是纯人工看图。import pandas as pd # 对每个 cluster 跑差异表达分析 sc.tl.rank_genes_groups(adata, groupbyleiden, methodwilcoxon) result pd.DataFrame(adata.uns[rank_genes_groups][names]).head(50) # 定义 marker 字典 marker_dict { CD4 T: [CD3D, CD3E, IL7R], CD8 T: [CD3D, CD8A, CD8B], NK: [NKG7, GNLY, KLRD1], B: [MS4A1, CD79A], Monocyte: [LYZ, CD14], Dendritic: [FCER1A, CST3], Platelet: [PPBP] } for cluster in result.columns: cluster_genes set(result[cluster]) overlap {ct: len(set(genes) cluster_genes) for ct, genes in marker_dict.items()} best max(overlap, keyoverlap.get) print(fcluster {cluster}: predicted {best}, overlap {overlap})rank_genes_groups的methodwilcoxon是 Wilcoxon 秩和检验适合非正态分布的高通量数据比t-test更稳健。输出中每个 cluster 的 top 基因若和某种 marker 组有 2 个以上重合就可以给这个 cluster 打上初始标签。重合数小于 2 时建议标为 “Unknown”不要硬猜。这段逻辑写好后后续替换 marker 列表、调整 cluster 数量都只要重跑一遍即可。这也是毕业设计里保留“源代码可复现性”最有力的部分评审最吃这一套。4. 参考映射构建自己的注释模型并完成标签迁移4.1 构造参考数据集的三个硬性要求参考映射的效果上限由参考集决定而不是由模型决定。构造参考集时必须满足三条注释标签经过人工审核、参考集与查询集经过相同的归一化流程、参考集的基因命名规范统一。如果参考集是从多个公共数据集合并的还需要先做批次校正。Harmony 是当前最通用的方案在 Scanpy 中可以直接调用。import scanpy.external as sce # 合并多批次数据后执行 Harmony sc.pp.normalize_total(adata_merged, target_sum1e4) sc.pp.log1p(adata_merged) sc.pp.highly_variable_genes(adata_merged, n_top_genes2000) sc.pp.pca(adata_merged, n_comps30) sce.pp.harmony_integrate(adata_merged, keybatch) # 在 Harmony 校正后的嵌入上重新建图、聚类 sc.pp.neighbors(adata_merged, use_repX_pca_harmony) sc.tl.leiden(adata_merged, resolution0.5)harmony_integrate的key参数是数据中表示批次的列名多个批次通过迭代聚类的方式消除技术差异。值得留意的是Harmony 校正后的X_pca_harmony不能直接当作普通 PCA 使用它的坐标不保留原始方差结构如果需要做差异表达分析仍然要用校正前的表达矩阵。这也常是毕业设计里被答辩老师追问的地方。4.2 用参考集对查询集做标签预测参考集构造完之后用 KNN 映射即可给查询集打上标签。关键环节是查询集必须经过与参考集一致的基因过滤和归一化否则特征空间不对齐迁移后准确率直线下降。import scanpy as sc from sklearn.neighbors import KNeighborsClassifier # 参考集已完成注释 ref sc.read_h5ad(ref_annotated.h5ad) query sc.read_h5ad(query_raw.h5ad) # 统一基因集 common_genes list(set(ref.var_names) set(query.var_names)) common_genes.sort() ref ref[:, common_genes].copy() query query[:, common_genes].copy() # 同样归一化 sc.pp.normalize_total(ref, target_sum1e4) sc.pp.log1p(ref) sc.pp.normalize_total(query, target_sum1e4) sc.pp.log1p(query) sc.pp.pca(ref, n_comps30) sc.pp.pca(query, n_comps30) knn KNeighborsClassifier(n_neighbors15, weightsdistance) knn.fit(ref.obsm[X_pca], ref.obs[cell_type]) query.obs[pred_celltype] knn.predict(query.obsm[X_pca])KNN 的weightsdistance比默认的uniform更适合单细胞数据原因是近邻细胞与目标细胞的距离差异本身携带强度信息距离加权可以让近处细胞影响更大。n_neighbors推荐在 10~30 之间搜索但过大会引入跨类型噪声。批量预测时每跑一组参数就输出一次混淆矩阵或可信度分布不要只盯着准确率。4.3 对预测结果做可信度过滤标签迁移后的结果质量问题集中在两类参考集里不存在的细胞类型、以及位于两种类型边界上的过渡态细胞。处理方式是计算预测概率设定阈值过滤低置信度细胞。# 用 predict_proba 获取每个细胞在每个类别上的概率 prob knn.predict_proba(query.obsm[X_pca]) max_prob prob.max(axis1) # 阈值设 0.6低于该值的标记为 Unknown query.obs[pred_prob] max_prob query.obs[cell_type_final] query.obs[pred_celltype] query.obs.loc[query.obs[pred_prob] 0.6, cell_type_final] Unknown # 统计各类别细胞数 print(query.obs.groupby(cell_type_final).size())阈值的选择与参考集的注释粒度强相关。如果注释类型是“T cell”这种粗粒度概率分布通常集中在 0.8 以上阈值设 0.7 也不为过。如果注释粒度细到“Naive CD4 T cell”边界概率自然会低设太高会丢掉大量真实细胞。0.6 是一个起步值我的习惯是画一个分位数直方图如果 0.6~0.7 这个区间出现明显尖峰说明参考集和查询集之间有明显批次差异应该先做数据整合而非调低阈值。5. 收敛到一张验证清单调优、评估与常见坑位注释模型的调优空间不在神经网络的层数而在数据链路的一致性。先盘点一下最常出问题的三个地方参考集基因名不一致导致特征大量丢失归一化流程顺序不同造成分布偏移阈值设定不合理使好细胞被误标为 Unknown。把这三个排查完已经能解决 80% 的注释质量投诉。验证阶段我会同时看聚类与注释的一致性。用 ARIAdjusted Rand Index对比聚类标签和预测标签能够量化评价“同一类型的细胞是否被拆到了多个 cluster 中”。from sklearn.metrics import adjusted_rand_score, confusion_matrix # 将未知细胞过滤后再计算 mask query.obs[cell_type_final] ! Unknown ari adjusted_rand_score(query.obs.loc[mask, leiden], query.obs.loc[mask, cell_type_final]) print(ARI between clustering and annotation:, round(ari, 3)) # 混淆矩阵方便查看哪些类型被混在一起 cm confusion_matrix( query.obs.loc[mask, leiden], query.obs.loc[mask, cell_type_final], )ARI 的值超过 0.6 表示聚类与注释之间的一致性已经够好低于 0.4 则说明聚类分辨率不匹配注释粒度。分辨率不匹配时的补救方案是改变leiden的resolution参数后重新映射一次而不是修改分类器权重。最后一层验证是视觉核对。sc.tl.draw_graph或 UMAP 图上按标签着色观察同一类型的细胞是否存在离群分布的小岛。如果存在优先怀疑 doublet 未清理干净或参考集本身有误注释。这两类问题靠调参数解决不了只能回到质控环节重新过滤。把这条验证链路固定下来一套注释流程从原始数据出图到质控整体耗时能控制在半天以内。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价