单细胞分析入门(Scanpy)¶
彭文强 | 2026届 | 生信分析工程师求职
1. 一句话说明¶
Scanpy 是 Python 生态中最主流的单细胞 RNA-seq 分析框架,从数据加载、质控、降维、聚类到细胞类型注释一站式搞定——你之前做 bulk RNA-seq 是把一堆细胞搅碎后看"平均值",而单细胞分析是给每个细胞单独拍一张"基因表达快照",Scanpy 就是处理这些快照的瑞士军刀。
2. 为什么要学¶
2.1 单细胞的革命性意义¶
传统 bulk RNA-seq 的结果是"一群细胞的平均表达量",就像把一个班 50 个学生的成绩平均后说"这个班数学平均 75 分"。问题在于:这个班可能有 10 个学霸考 95 分、10 个差生考 40 分,平均值把关键信息抹平了。
单细胞测序让你看到每个学生的成绩单:
- 发现稀有细胞类型:肿瘤里的少量干细胞、免疫逃逸细胞
- 追踪发育轨迹:一个干细胞怎么一步步分化成不同细胞
- 解析异质性:同一个肿瘤里不同细胞的基因表达完全不同
- 单细胞技术是当前生命科学最前沿方向之一,Nature Methods 多次将其评为"年度技术"
2.2 就业热门方向¶
| 岗位方向 | 用单细胞做什么 | 需求热度 |
|---|---|---|
| 肿瘤免疫 | 分析免疫微环境、找免疫治疗靶点 | 极高 |
| 发育生物学 | 追踪细胞命运决定 | 高 |
| 药物研发 | 评估药物对不同细胞亚群的影响 | 高 |
| 临床诊断 | 单细胞水平的疾病分型 | 上升中 |
| 空间转录组 | 单细胞 + 空间位置信息 | 最新热点 |
白话总结:你做宏基因组已经很好了,如果再会单细胞分析,面试时能覆盖微生物 + 转录组 + 单细胞三条线,竞争力直接翻倍。很多生信岗位 JD 里会写"有单细胞分析经验者优先"。
3. 核心概念白话版¶
3.1 单细胞 vs 传统 bulk RNA-seq¶
Bulk RNA-seq(你之前做的):
把 100 万个细胞搅碎 → 提取总 RNA → 测序
结果:每个基因一个"平均表达量"
白话:把一锅大杂烩汤测一下,知道汤里有什么成分
单细胞 RNA-seq(scRNA-seq):
把细胞一个一个分开 → 每个细胞单独测序
结果:每个细胞各有一套基因表达谱
白话:把每一碗汤单独尝一下,知道每碗汤的味道
| 对比维度 | Bulk RNA-seq | scRNA-seq |
|---|---|---|
| 分辨率 | 组织/群体水平 | 单个细胞水平 |
| 数据量 | 几十个样本 × 2万基因 | 几千~几万细胞 × 2万基因 |
| 结果 | 基因表达矩阵(样本×基因) | 基因表达矩阵(细胞×基因) |
| 发现的东西 | 两组之间哪些基因差异表达 | 有哪些细胞类型、各类型特征基因 |
| 白话 | 班级平均分 | 每个学生的成绩单 |
3.2 细胞 Barcode 和 UMI¶
单细胞测序最核心的问题:怎么知道一条 read 来自哪个细胞?
一条 10x Genomics 的 read 结构:
|-- 细胞 Barcode (16bp) --|-- UMI (12bp) --|-- cDNA 序列 --|
标记"哪个细胞" 标记"哪个分子" 实际的基因片段
- 细胞 Barcode(细胞条形码):每个细胞被包在一个油滴(droplet)里,油滴里有一种特定的 DNA 序列标签。同一个细胞产出的所有 read 共享同一个 barcode。白话:就像超市里每件商品有自己的条形码,扫一下就知道是哪个商品——barcode 扫一下就知道是哪个细胞。
- UMI(Unique Molecular Identifier,唯一分子标识符):PCR 扩增会把一个 mRNA 分子复制成很多条 read,UMI 帮你去重——同一个 UMI 不管被扩增了多少次,只算 1 个分子。白话:给每个原始 mRNA 贴一个"身份证号",扩增出来的"克隆体"都带同一个号,数人头时只数身份证号不重复数。
3.3 10x Genomics 平台¶
目前最主流的单细胞测序平台,市场占有率超过 80%:
10x Genomics Chromium 工作流程:
细胞悬液 → Chromium 仪器 → 形成 GEM(油包水液滴)
↓
每个液滴里有:1个细胞 + 1颗凝胶珠(gel bead)
凝胶珠上连着:Barcode + UMI + 引物
↓
液滴内:mRNA 被捕获 → 逆转录 → 带上 Barcode 和 UMI
↓
打破液滴 → 扩增 → 建库 → Illumina 测序
↓
Cell Ranger 软件处理 → 输出表达矩阵
Cell Ranger 输出的关键文件:
- barcodes.tsv.gz:所有有效细胞的 barcode 列表
- features.tsv.gz:所有基因的列表
- matrix.mtx.gz:稀疏矩阵(细胞 × 基因的表达量)
3.4 AnnData 数据结构¶
Scanpy 的核心数据容器是 AnnData(Annotated Data),你可以把它想象成一个"超级 DataFrame":
AnnData 结构图:
var (基因注释, 列信息)
gene1 gene2 gene3 ...
┌──────────────────────────┐
obs │ 3 0 12 ... │ ← 细胞1(barcode: AACG...)
(细胞 │ 0 5 0 ... │ ← 细胞2(barcode: TTCG...)
注释, │ 8 2 7 ... │ ← 细胞3(barcode: GGAC...)
行信息)│ ... │
└──────────────────────────┘
X (表达矩阵)
adata.X → 表达矩阵(细胞×基因),通常是稀疏矩阵(因为大部分值是0)
adata.obs → 细胞的注释信息(如:细胞类型、样本来源、QC指标)
adata.var → 基因的注释信息(如:基因名、是否高变基因)
adata.obsm → 细胞的降维坐标(如:PCA、UMAP 坐标)
adata.uns → 非结构化信息(如:颜色方案、分析参数)
adata.layers → 存放不同版本的表达矩阵(如:raw counts、normalized)
白话:AnnData 就像一个 Excel 工作簿。主表 X 存数据,obs 是行标签(每行一个细胞的信息),var 是列标签(每列一个基因的信息),还有几个"附属工作表"存降维坐标等额外信息。
3.5 降维(PCA / UMAP / t-SNE)¶
单细胞数据的维度极高:每个细胞有 2 万多个基因(2 万多个维度)。人眼只能看 2D/3D,所以需要"降维":
| 方法 | 全称 | 做什么 | 白话 |
|---|---|---|---|
| PCA | Principal Component Analysis 主成分分析 | 找出数据中变化最大的几个方向,把 2 万维压到 30-50 维 | 2 万门课太多了,先找出最能区分学生的 30 门核心课 |
| UMAP | Uniform Manifold Approximation and Projection | 把 PCA 的 30-50 维进一步压到 2 维,保留"邻居关系" | 把 30 门课的成绩压成一张 2D 散点图,成绩相近的同学挨在一起 |
| t-SNE | t-distributed Stochastic Neighbor Embedding | 功能类似 UMAP,但更老、更慢 | 和 UMAP 目的一样,但现在用得少了 |
分析中的使用顺序:原始数据 → PCA(降到 30-50 维)→ UMAP(降到 2 维可视化)。PCA 是中间步骤,UMAP/t-SNE 是最终可视化。
3.6 聚类(Clustering)¶
降维后,表达谱相似的细胞会聚在一起。聚类算法自动把它们分成不同的"群":
- Leiden 算法(Scanpy 默认):基于图论的社区发现算法。先用 KNN 建一个"邻居图"(每个细胞连接它最像的 k 个邻居),然后在图上找"关系紧密的社区"。白话:先给每个学生找到和他成绩最像的 15 个同学连线,然后看哪些学生互相连得特别紧密,把他们分成一个"小团体"。
- Louvain 算法:Leiden 的前身,现在逐渐被 Leiden 取代。
- 分辨率参数(resolution):控制分多少个群。值越大群越多(分得越细),值越小群越少。
3.7 Marker 基因¶
每个细胞群的"身份标签"——在这个群里高表达、在其他群里低表达的基因:
例子:
Cluster 0 高表达 CD3D, CD3E → 这是 T 细胞!
Cluster 1 高表达 CD19, MS4A1 → 这是 B 细胞!
Cluster 2 高表达 CD14, LYZ → 这是单核细胞!
找 Marker 基因就是做"一对多"的差异表达分析:这个群 vs 其余所有群,找显著上调的基因。然后对照文献/数据库(如 CellMarker、PanglaoDB)确定细胞类型。
4. Scanpy 完整分析流程¶
环境准备¶
# 创建 conda 环境
conda create -n scanpy python=3.10 -y
conda activate scanpy
# 安装核心包
pip install scanpy leidenalg
# scanpy 是主框架
# leidenalg 是 Leiden 聚类算法的底层库,需要单独装
完整流程代码¶
# ============================================================
# 单细胞 RNA-seq 标准分析流程(Scanpy)
# 数据:PBMC 3k(外周血单核细胞,10x Genomics 官方示例数据)
# ============================================================
import scanpy as sc # 导入 scanpy,约定俗成缩写 sc
import numpy as np # 数值计算库
import pandas as pd # 数据处理库
# ---------- 全局设置 ----------
sc.settings.verbosity = 3 # 输出详细程度:0=error, 1=warning, 2=info, 3=hint
sc.settings.set_figure_params(dpi=80, facecolor='white')
# dpi=80 控制图片分辨率,facecolor='white' 白色背景
# ============================================================
# Step 1: 数据加载
# ============================================================
# 方法1:从 10x Cell Ranger 输出目录读取
# Cell Ranger 处理完原始测序数据后,会产出 filtered_feature_bc_matrix/ 文件夹
# 里面有 barcodes.tsv.gz(细胞列表)、features.tsv.gz(基因列表)、matrix.mtx.gz(表达矩阵)
adata = sc.read_10x_mtx(
'data/filtered_gene_bc_matrices/hg19/', # Cell Ranger 输出目录路径
var_names='gene_symbols', # 用基因名(而非 Ensembl ID)作为列名
cache=True # 缓存为 .h5ad 文件,下次加载更快
)
# 白话:从 10x 的标准输出文件夹里把数据读进来,就像打开一个 Excel 表格
# 方法2:也可以从 .h5ad 文件直接读取(如果已经保存过)
# adata = sc.read_h5ad('pbmc3k.h5ad')
# 方法3:使用 Scanpy 自带的示例数据(推荐练习用)
# adata = sc.datasets.pbmc3k()
# 查看数据基本信息
print(adata)
# 输出类似:AnnData object with n_obs × n_vars = 2700 × 32738
# n_obs = 2700 个细胞,n_vars = 32738 个基因
# 确保基因名唯一(有些基因有重复名字,加后缀区分)
adata.var_names_make_unique()
# 白话:就像班里有两个"张三",给第二个改名叫"张三-1"
# ============================================================
# Step 2: 质控过滤(QC Filtering)
# ============================================================
# 首先标记哪些是线粒体基因(以 'MT-' 开头的基因)
adata.var['mt'] = adata.var_names.str.startswith('MT-')
# 白话:线粒体基因名字都以 MT- 开头,比如 MT-CO1, MT-ND1
# 线粒体基因比例高 = 细胞可能已经破损/死亡(细胞质漏出,线粒体 mRNA 残留)
# 计算 QC 指标(必须在标记 mt 基因之后调用)
sc.pp.calculate_qc_metrics(
adata,
qc_vars=['mt'], # 计算线粒体基因(mitochondrial)的比例
percent_top=None, # 不计算 top N 基因占比(加快速度)
log1p=False, # 不做 log 转换
inplace=True # 直接修改 adata,不返回新对象
)
# 计算完后 adata.obs 会多出几列:
# n_genes_by_counts:每个细胞检测到多少个基因
# total_counts:每个细胞的总 UMI 数(测到多少个 mRNA 分子)
# pct_counts_mt:线粒体基因占总 UMI 的百分比
# 可视化 QC 指标(画小提琴图)
sc.pl.violin(
adata,
['n_genes_by_counts', 'total_counts', 'pct_counts_mt'],
jitter=0.4, # 散点抖动幅度,防止点重叠
multi_panel=True # 每个指标画一个子图
)
# 白话:画三张图看看细胞质量分布——检测到的基因数、总分子数、线粒体占比
# 过滤低质量细胞
adata = adata[adata.obs.n_genes_by_counts < 2500, :]
# 过滤检测到基因数 > 2500 的细胞(可能是双细胞 doublet:两个细胞挤进同一个液滴)
adata = adata[adata.obs.n_genes_by_counts > 200, :]
# 过滤检测到基因数 < 200 的细胞(可能是空液滴或碎片)
adata = adata[adata.obs.pct_counts_mt < 5, :]
# 过滤线粒体比例 > 5% 的细胞(可能是死细胞或破损细胞)
# 白话:三条过滤线 =
# 1. 太多基因 → 可能两个细胞粘一起了,去掉
# 2. 太少基因 → 可能是空液滴,去掉
# 3. 线粒体太高 → 可能是死细胞,去掉
# 过滤在极少细胞中表达的基因
sc.pp.filter_genes(adata, min_cells=3)
# 一个基因至少要在 3 个细胞中表达才保留,否则太稀有没有统计意义
print(f"过滤后:{adata.n_obs} 个细胞,{adata.n_vars} 个基因")
# ============================================================
# Step 3: 标准化(Normalization)
# ============================================================
# 保存原始 counts(后面找 Marker 基因要用)
adata.raw = adata.copy()
# 白话:先备份一份原始数据,后面做差异分析要用原始值
# 文库大小标准化:把每个细胞的总 counts 统一到 10000
sc.pp.normalize_total(adata, target_sum=1e4)
# 白话:每个细胞测到的总分子数不同(有的细胞测到 5000 个,有的 20000 个)
# 这不是因为基因表达不同,而是测序深度不同
# 统一到 10000 后才能公平比较
# 对数转换
sc.pp.log1p(adata)
# log1p = log(x + 1),+1 是为了避免 log(0) = -∞
# 白话:基因表达量差异非常大(有的基因表达 10000,有的只有 1)
# 取 log 后把差距"压缩",让下游分析更稳定
# 就像地震的里氏震级用对数——震级差 1 级其实能量差 32 倍
# ============================================================
# Step 4: 高变基因筛选(Highly Variable Genes, HVG)
# ============================================================
sc.pp.highly_variable_genes(
adata,
min_mean=0.0125, # 平均表达量下限(太低的基因不要)
max_mean=3, # 平均表达量上限(管家基因表达太稳定,不要)
min_disp=0.5 # 离散度下限(变化太小的基因没有区分度)
)
# 白话:从 2 万个基因中挑出 ~2000 个"变化最大的"基因
# 这些基因才是区分不同细胞类型的关键
# 就像考试中选"区分度最高"的题目——所有人都会或都不会的题没用
# 可视化高变基因
sc.pl.highly_variable_genes(adata)
# 红色点 = 高变基因(被选中的),灰色 = 不高变的基因
# 只保留高变基因(减少计算量)
adata = adata[:, adata.var.highly_variable]
# 白话:把不重要的基因暂时"遮住",只看最有区分度的基因做后续分析
print(f"高变基因数:{adata.n_vars}")
# ============================================================
# Step 5: 回归和缩放(Regression & Scaling)
# ============================================================
# 回归掉总 counts 数和线粒体比例的影响
sc.pp.regress_out(adata, ['total_counts', 'pct_counts_mt'])
# 白话:消除"测序深度"和"细胞死活"对基因表达量的干扰
# 就像统计各省高考分数时要去掉"出题难度"的影响
# 缩放到单位方差(每个基因的表达量标准化到均值=0,方差=1)
sc.pp.scale(adata, max_value=10)
# max_value=10:超过 10 个标准差的极端值截断到 10,防止个别异常值干扰
# 白话:让每个基因"站在同一起跑线上"——不管原来表达量是 10 还是 10000
# 标准化后都在同一个范围内,这样 PCA 才不会被高表达基因"带偏"
# ============================================================
# Step 6: PCA 降维
# ============================================================
sc.tl.pca(adata, svd_solver='arpack')
# svd_solver='arpack':使用 ARPACK 求解器,适合大型稀疏矩阵
# 默认取前 50 个主成分(PC)
# 查看每个 PC 解释的方差比例(用来决定保留多少个 PC)
sc.pl.pca_variance_ratio(adata, log=True)
# 白话:画一个"碎石图"(scree plot),看前多少个 PC 就够了
# 通常曲线出现"拐点"的地方就是合适的 PC 数
# 比如前 10 个 PC 解释了大部分变异,后面的 PC 基本是噪声
# 可视化 PCA(前两个 PC)
sc.pl.pca(adata, color='n_genes_by_counts')
# 用检测到的基因数给点上色,看是否存在批次效应
# ============================================================
# Step 7: 计算邻域图(Neighborhood Graph)
# ============================================================
sc.pp.neighbors(
adata,
n_neighbors=10, # 每个细胞找 10 个最近邻
n_pcs=40 # 使用前 40 个 PC 计算距离
)
# 白话:在 PCA 空间中,给每个细胞找到和它"最像"的 10 个邻居
# 并把它们用"线"连起来,形成一个社交网络(KNN 图)
# 这个图是后面聚类和 UMAP 的基础
# ============================================================
# Step 8: 聚类(Leiden Clustering)
# ============================================================
sc.tl.leiden(
adata,
resolution=0.5, # 分辨率参数:越大分出的群越多
flavor='igraph', # 使用 igraph 实现(更快)
n_iterations=2 # 迭代次数
)
# 白话:在邻域图上跑 Leiden 算法,把"关系紧密"的细胞分成一组
# resolution=0.5 是比较温和的值,通常分出 5-15 个群
# 如果想分得更细可以调到 1.0 或更高
print(f"聚类得到 {adata.obs['leiden'].nunique()} 个细胞群")
# ============================================================
# Step 9: UMAP 可视化
# ============================================================
sc.tl.umap(adata)
# 基于邻域图计算 UMAP 坐标(2维),用于可视化
# 画 UMAP 散点图,按聚类结果上色
sc.pl.umap(adata, color=['leiden'], legend_loc='on data')
# color='leiden':用聚类标签上色
# legend_loc='on data':图例直接标在图上(而不是放在旁边)
# ============================================================
# Step 10: 找 Marker 基因
# ============================================================
sc.tl.rank_genes_groups(
adata,
groupby='leiden', # 按 leiden 聚类结果分组
method='wilcoxon' # 使用 Wilcoxon 秩和检验(非参数检验,推荐)
)
# 白话:对每个 cluster,做"这个群 vs 其他所有群"的差异分析
# 找出每个群特异性高表达的基因
# 可视化每个 cluster 的 top 5 Marker 基因
sc.pl.rank_genes_groups(adata, n_genes=5, sharey=False)
# n_genes=5:每个群展示前 5 个 Marker 基因
# sharey=False:每个子图 y 轴独立(因为不同群的分数范围不同)
# 查看具体结果(以 DataFrame 形式)
result = adata.uns['rank_genes_groups']
marker_df = sc.get.rank_genes_groups_df(adata, group='0')
# 获取 cluster 0 的所有 Marker 基因及其统计量
print(marker_df.head(10))
# 列包括:names(基因名), scores, logfoldchanges, pvals, pvals_adj
# ============================================================
# Step 11: 细胞类型注释
# ============================================================
# 手动注释(基于已知 Marker 基因)
# 这是最常见的方式:看每个 cluster 的 top Marker 基因,查文献确定细胞类型
# PBMC 常见细胞类型的经典 Marker:
marker_genes = {
'T cells (CD4+)': ['CD3D', 'CD3E', 'IL7R'], # CD4+ T 细胞
'T cells (CD8+)': ['CD3D', 'CD3E', 'CD8A', 'CD8B'], # CD8+ T 细胞
'B cells': ['CD19', 'MS4A1', 'CD79A'], # B 细胞
'NK cells': ['GNLY', 'NKG7', 'KLRD1'], # 自然杀伤细胞
'Monocytes (CD14+)':['CD14', 'LYZ', 'S100A8'], # CD14+ 单核细胞
'Monocytes (CD16+)':['FCGR3A', 'MS4A7'], # CD16+ 单核细胞
'Dendritic cells': ['FCER1A', 'CST3'], # 树突状细胞
'Platelets': ['PPBP', 'PF4'], # 血小板
}
# 用 dotplot 展示各 cluster 中 Marker 基因的表达
sc.pl.dotplot(adata, marker_genes, groupby='leiden', dendrogram=True)
# 白话:看每个 cluster(行)在每个已知 Marker 基因(列)上的表达
# 如果 cluster 0 在 CD3D/CD3E 上高表达 → 那它就是 T 细胞
# 根据观察结果给每个 cluster 赋予细胞类型名
new_cluster_names = [
'CD4 T', # cluster 0
'CD14 Monocytes', # cluster 1
'B cells', # cluster 2
'CD8 T', # cluster 3
'NK cells', # cluster 4
'FCGR3A Monocytes',# cluster 5
'Dendritic cells', # cluster 6
'Platelets', # cluster 7
]
adata.rename_categories('leiden', new_cluster_names)
# 白话:把 "0, 1, 2, 3..." 这些数字标签换成有生物学意义的名字
# 画最终的注释后 UMAP
sc.pl.umap(adata, color='leiden', legend_loc='on data', title='PBMC 3k - Cell Types')
# ============================================================
# 保存结果
# ============================================================
adata.write('pbmc3k_analyzed.h5ad')
# 保存为 .h5ad 文件(HDF5 格式),下次直接 sc.read_h5ad() 加载
# 包含所有分析结果(降维坐标、聚类标签、Marker 基因等)
流程总结¶
数据加载 → QC过滤 → 标准化 → 高变基因 → PCA → 邻域图 → 聚类 → UMAP → Marker基因 → 细胞注释
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
read_10x filter normalize hvg pca neighbors leiden umap rank_genes rename
cells + log1p select _groups categories
5. 常用可视化代码¶
5.1 小提琴图(Violin Plot)¶
# 看特定基因在不同 cluster 中的表达分布
sc.pl.violin(adata, ['CD3D', 'CD14', 'MS4A1'], groupby='leiden', rotation=45)
# groupby='leiden':按聚类结果分组
# rotation=45:x 轴标签旋转 45 度防止重叠
# 白话:每个 cluster 画一把"小提琴",胖的地方表示很多细胞在这个表达量
5.2 特征散点图(Feature Plot / UMAP 上色)¶
# 在 UMAP 上用基因表达量上色
sc.pl.umap(adata, color=['CD3D', 'CD14', 'MS4A1', 'NKG7'], ncols=2)
# color 传入基因名列表,每个基因画一张 UMAP
# ncols=2:每行 2 张图
# 白话:在散点图上把表达高的细胞涂成深色,一眼看出哪些细胞群表达了这个基因
5.3 热图(Heatmap)¶
# 画 Marker 基因热图
sc.pl.rank_genes_groups_heatmap(
adata,
n_genes=5, # 每个 cluster 取 top 5 基因
groupby='leiden',
show_gene_labels=True, # 显示基因名
cmap='viridis' # 颜色方案
)
# 白话:每行是一个基因,每列是一个细胞(按 cluster 排列)
# 颜色深浅表示表达量高低,可以一眼看出每个 cluster 的特征基因
# 自定义热图(指定基因列表)
sc.pl.heatmap(
adata,
var_names=['CD3D', 'CD14', 'MS4A1', 'NKG7', 'PPBP'],
groupby='leiden',
swap_axes=True # 交换行列(基因在列,cluster 在行)
)
5.4 点图(Dot Plot)¶
# 点图:同时展示基因表达比例和表达量
sc.pl.dotplot(
adata,
var_names=['CD3D', 'CD14', 'MS4A1', 'NKG7', 'PPBP', 'FCER1A'],
groupby='leiden'
)
# 点的大小 = 这个 cluster 中有多少比例的细胞表达了该基因
# 点的颜色 = 平均表达量
# 白话:大且深的点 = 很多细胞都高表达这个基因 → 强 Marker
5.5 堆叠小提琴图(Stacked Violin)¶
sc.pl.stacked_violin(
adata,
var_names=['CD3D', 'CD14', 'MS4A1', 'NKG7'],
groupby='leiden',
rotation=90
)
# 多个基因的小提琴图上下堆叠,节省空间
6. Scanpy vs Seurat 对比¶
| 对比维度 | Scanpy (Python) | Seurat (R) |
|---|---|---|
| 语言 | Python | R |
| 数据结构 | AnnData (.h5ad) | SeuratObject (.rds) |
| 安装 | pip install scanpy |
install.packages('Seurat') |
| 学习曲线 | 如果你会 Python 就很容易 | 如果你会 R 就很容易 |
| 性能 | 大数据集(>10万细胞)更快 | 中小数据集够用,大数据偏慢 |
| 生态 | scvi-tools, CellTypist, squidpy | Signac, Monocle, harmony |
| 可视化 | matplotlib 风格,需要自定义 | ggplot2 风格,默认好看 |
| 深度学习集成 | 天然优势(PyTorch / scvi) | 需要通过 reticulate 调 Python |
| 论文使用 | 两者都被广泛引用,Scanpy 增长更快 | 引用总量目前更多 |
| 互操作性 | 可通过 anndata2ri 转换 | 可通过 SeuratDisk 转换 |
选哪个?
- 你 Python 基础更好 → 选 Scanpy
- 你要做大规模数据集(atlas-level,几十万细胞)→ 选 Scanpy
- 你要做深度学习整合(scVI, scANVI)→ 选 Scanpy
- 你的合作者或实验室用 R 生态 → 选 Seurat
- 面试建议:两个都了解原理,至少一个能上手跑代码
7. 常见报错与解决¶
7.1 ImportError: No module named 'leidenalg'¶
报错原因:Leiden 聚类依赖 leidenalg 包,但没安装
解决方法:
pip install leidenalg
# 如果 pip 装不上(编译失败),用 conda:
conda install -c conda-forge leidenalg
7.2 ValueError: could not convert string to float¶
报错原因:数据矩阵中混入了非数值内容,通常是基因名被当成了数据
解决方法:
# 检查 adata.X 的类型
print(type(adata.X))
print(adata.X[:5, :5])
# 确认 var_names 设置正确
# 读 10x 数据时确保:var_names='gene_symbols' 或 'gene_ids'
7.3 adata.X is not in the expected format (sparse matrix vs dense)¶
报错原因:某些函数期望稀疏矩阵,但 adata.X 已经变成了密集矩阵(或反过来)
解决方法:
import scipy.sparse as sp
# 转成稀疏矩阵
adata.X = sp.csr_matrix(adata.X)
# 或转成密集矩阵
adata.X = adata.X.toarray()
7.4 KeyError: 'leiden' 或 KeyError: 'umap'¶
报错原因:你试图画 UMAP 或用 leiden 标签,但还没运行对应的分析步骤
解决方法:确认流程顺序完整——
sc.pp.neighbors(adata) # 先算邻域图
sc.tl.leiden(adata) # 再聚类
sc.tl.umap(adata) # 再算 UMAP
sc.pl.umap(adata, color='leiden') # 最后画图
7.5 MemoryError 或内核崩溃¶
报错原因:数据集太大(几十万细胞),内存不够
解决方法:
# 1. 使用稀疏矩阵(默认就是,别转成 dense)
# 2. regress_out 特别吃内存,可以跳过或用 scanpy 的 backed 模式:
adata = sc.read_h5ad('large_data.h5ad', backed='r')
# 3. 分批处理,或用 scvi-tools 做标准化(GPU 加速)
# 4. 加大服务器内存,或用高内存节点
7.6 FutureWarning: X.dtype is not float32/float64¶
报错原因:Scanpy 1.9+ 要求 adata.X 的数据类型是 float32 或 float64
解决方法:
adata.X = adata.X.astype('float32')
# 如果是稀疏矩阵:
import scipy.sparse as sp
adata.X = adata.X.astype(np.float32)
7.7 UMAP/t-SNE 图上聚类重叠严重¶
这不是"报错",而是分析问题:
原因1:分辨率(resolution)设太低 → 调高 resolution 参数
原因2:数据质量差,batch effect 没去除 → 用 Harmony 或 BBKNN 整合批次
原因3:高变基因选得不好 → 调整 min_mean, max_mean, min_disp 参数
原因4:PCA 保留的 PC 数不合适 → 看碎石图调整 n_pcs
8. 速查表¶
核心函数速查¶
| 步骤 | 函数 | 作用 |
|---|---|---|
| 读数据 | sc.read_10x_mtx() |
读 10x Cell Ranger 输出 |
| 读数据 | sc.read_h5ad() |
读 .h5ad 文件 |
| QC 计算 | sc.pp.calculate_qc_metrics() |
计算质控指标 |
| 过滤细胞 | adata = adata[条件, :] |
布尔索引过滤细胞 |
| 过滤基因 | sc.pp.filter_genes(min_cells=) |
过滤低表达基因 |
| 标准化 | sc.pp.normalize_total() |
文库大小标准化 |
| 对数化 | sc.pp.log1p() |
log(x+1) 转换 |
| 高变基因 | sc.pp.highly_variable_genes() |
筛选高变基因 |
| 回归 | sc.pp.regress_out() |
回归掉混杂变量 |
| 缩放 | sc.pp.scale() |
标准化到单位方差 |
| PCA | sc.tl.pca() |
主成分分析 |
| 邻域图 | sc.pp.neighbors() |
计算 KNN 邻居 |
| 聚类 | sc.tl.leiden() |
Leiden 聚类 |
| UMAP | sc.tl.umap() |
计算 UMAP 坐标 |
| Marker | sc.tl.rank_genes_groups() |
差异基因分析 |
| 保存 | adata.write() |
保存 .h5ad |
Scanpy 命名空间规律¶
sc.pp.xxx() → 预处理(preprocessing):过滤、标准化、缩放
sc.tl.xxx() → 分析工具(tools):PCA、聚类、差异分析
sc.pl.xxx() → 画图(plotting):UMAP、violin、heatmap
sc.get.xxx() → 获取数据(get):提取结果为 DataFrame
sc.datasets.xxx() → 内置数据集
常用 QC 阈值参考¶
| 指标 | 常用阈值 | 说明 |
|---|---|---|
| n_genes_by_counts 下限 | 200-500 | 太少 = 空液滴 |
| n_genes_by_counts 上限 | 2500-5000 | 太多 = doublet |
| pct_counts_mt 上限 | 5-20% | 看组织类型,心肌细胞可到 20% |
| min_cells (基因过滤) | 3-10 | 基因至少在 N 个细胞中表达 |
AnnData 常用属性速查¶
| 属性 | 类型 | 存什么 |
|---|---|---|
adata.X |
矩阵 | 当前表达矩阵(可能经过标准化) |
adata.raw.X |
矩阵 | 原始 counts(标准化前保存的) |
adata.obs |
DataFrame | 细胞注释(cluster、QC 指标等) |
adata.var |
DataFrame | 基因注释(highly_variable 等) |
adata.obsm['X_pca'] |
ndarray | PCA 坐标 |
adata.obsm['X_umap'] |
ndarray | UMAP 坐标 |
adata.obs['leiden'] |
Series | 聚类标签 |
adata.uns['rank_genes_groups'] |
dict | Marker 基因结果 |
9. 延伸学习资源¶
官方资源¶
- Scanpy 官方教程:最权威的入门教程,PBMC 3k 案例
- Scanpy API 文档:所有函数的参数说明
- AnnData 文档:数据结构详解
学习路径建议¶
第 1 步:跑通 PBMC 3k 教程(本文的代码就够) ← 需 1-2 天
第 2 步:学批次整合(Harmony / scVI) ← 多样本必学
第 3 步:学轨迹分析(scVelo / PAGA) ← 发育方向必学
第 4 步:学细胞通讯(CellChat / NicheNet) ← 免疫/肿瘤方向加分
第 5 步:学空间转录组(Squidpy / SpatialData) ← 最新方向
推荐数据库¶
| 数据库 | 用途 | 网址 |
|---|---|---|
| CellMarker 2.0 | 查细胞类型 Marker 基因 | http://bio-bigdata.hrbmu.edu.cn/CellMarker/ |
| PanglaoDB | 查细胞类型 Marker | https://panglaodb.se/ |
| Human Cell Atlas | 人类细胞图谱数据 | https://data.humancellatlas.org/ |
| CellxGene | 在线单细胞数据浏览器 | https://cellxgene.cziscience.com/ |
进阶工具生态¶
| 工具 | 功能 | 与 Scanpy 关系 |
|---|---|---|
| scvi-tools | 深度学习驱动的单细胞分析 | Scanpy 生态的深度学习扩展 |
| CellTypist | 自动细胞类型注释 | 输入 AnnData,输出注释 |
| Harmony (harmonypy) | 批次效应校正 | 作为 Scanpy 流程的一步 |
| scVelo | RNA velocity(RNA 速率) | 读 AnnData,分析细胞分化方向 |
| Squidpy | 空间转录组分析 | Scanpy 官方空间分析扩展 |
| BBKNN | 批次平衡的 KNN 图 | 替代 sc.pp.neighbors 做批次整合 |
面试高频问题¶
- "单细胞和 bulk RNA-seq 的区别?" → 见 3.1 节
- "Scanpy 分析流程说一下?" → 见第 4 节流程总结
- "什么是高变基因?为什么要筛选?" → 见 Step 4 注释
- "Leiden 和 Louvain 的区别?" → Leiden 是 Louvain 的改进版,解决了 Louvain 可能产生"断裂社区"的问题
- "怎么确定 resolution 参数?" → 没有标准答案,根据生物学先验知识和不同 resolution 的结果综合判断
- "doublet 怎么处理?" → 可用 Scrublet 或 DoubletFinder 检测并移除
最后更新:2026-05-03 | 配合面试速通计划使用