570 Scanpy 单细胞分析从零入门¶
适用人群:有宏基因组经验但没有单细胞经验的生信新手 Scanpy 版本:1.12.1(2026-04-10 发布) Python 版本要求:≥3.10
一、单细胞 RNA-seq 是什么?¶
1.1 白话解释:从"一锅粥"到"一粒一粒看"¶
做过宏基因组的同学都知道,Bulk RNA-seq(常规转录组)是把一坨组织直接磨碎提 RNA,测出来的是所有细胞的平均表达量。这就好比你问一个班 50 人的平均身高 ——"1米70",但你不知道哪些是篮球队的(1米90),哪些是刚入学的小个子(1米55)。
单细胞 RNA-seq(scRNA-seq) 就是把组织里的细胞一颗一颗拆开,每颗细胞单独测它的基因表达。这样你就能看到: - 这颗细胞是 T 细胞,表达高水平的 CD3 - 那颗细胞是巨噬细胞,表达高水平的 CD68 - 还有一颗是上皮细胞,表达 EPCAM
1.2 Bulk RNA-seq vs scRNA-seq 对比¶
| 对比项 | Bulk RNA-seq | scRNA-seq |
|---|---|---|
| 分辨率 | 组织/细胞群水平 | 单个细胞水平 |
| 类比 | 全班平均身高 | 每个人的身高 |
| 能否发现稀有细胞 | 不行,被平均掉了 | 可以 |
| 数据量 | 几 GB | 几十 GB 到上百 GB |
| 成本 | 相对便宜 | 较贵(每个样本几千到几万元) |
| 典型细胞数 | N/A | 几千到几十万个细胞 |
1.3 典型应用场景¶
- 肿瘤异质性:同一个肿瘤里有哪些不同的癌细胞亚群?哪些对药物耐药?
- 发育轨迹:干细胞是怎么一步步分化成成熟细胞的?(伪时间分析)
- 免疫细胞分类:患者体内有哪些免疫细胞?比例怎么样?
二、单细胞数据从哪来?¶
2.1 10x Genomics 平台简介¶
目前最主流的单细胞平台是 10x Genomics Chromium。简单说,它的原理是: 1. 把细胞一颗颗包进油滴(droplet)里 2. 每个油滴里有一个带唯一条码(barcode)的凝胶珠 3. 细胞在油滴里裂解,mRNA 被捕获并带上 barcode 4. 最后统一测序,根据 barcode 就知道每条 reads 来自哪个细胞
2.2 Cell Ranger 输出文件¶
10x 的官方分析软件叫 Cell Ranger,它处理完原始 fastq 文件后会输出三个关键文件:
filtered_feature_bc_matrix/
├── barcodes.tsv.gz # 每个细胞的条码(barcode),一行一个细胞
├── features.tsv.gz # 基因列表(以前叫 genes.tsv),一行一个基因
└── matrix.mtx.gz # 稀疏矩阵,记录每个细胞中每个基因的 UMI 计数
白话解释: - barcodes.tsv = 学生名单(哪些细胞) - features.tsv = 考试科目(哪些基因) - matrix.mtx = 成绩表(每个学生每科多少分)
2.3 h5ad 格式(AnnData 对象)¶
Scanpy 使用的核心数据结构叫 AnnData(Annotated Data),保存为 .h5ad 文件。你可以把它理解成一个"打包好的数据盒子",里面既有表达矩阵,又有细胞信息、基因信息、降维结果等。
三、Scanpy 安装¶
3.1 conda 安装(推荐)¶
# 创建一个专门的单细胞分析环境
conda create -n scrnaseq python=3.12 -y # 创建名为 scrnaseq 的环境,Python 3.12
# 激活环境
conda activate scrnaseq # 切换到新环境
# 安装 scanpy(当前最新版 1.12.1)
conda install -c conda-forge scanpy=1.12.1 -y # 从 conda-forge 频道安装
# 安装可视化和辅助依赖
conda install -c conda-forge leidenalg python-igraph -y # Leiden 聚类算法依赖
pip install scrublet # 双细胞检测工具(可选)
3.2 验证安装¶
四、完整分析流程实战:PBMC 3k¶
PBMC = 外周血单个核细胞(Peripheral Blood Mononuclear Cells) 这是单细胞领域最经典的入门数据集,来自 10x Genomics,包含约 2700 个细胞。
Step 0: 导入和设置¶
import scanpy as sc # 导入 scanpy,习惯简写为 sc
import numpy as np # 导入 numpy,用于数值计算
import pandas as pd # 导入 pandas,用于表格操作
sc.settings.verbosity = 3 # 设置日志级别:3 = 输出提示信息
sc.settings.set_figure_params(dpi=80, facecolor='white') # 设置画图参数:分辨率和背景色
sc.logging.print_header() # 打印 scanpy 版本和依赖信息
Step 1: 读取数据¶
# 方法一:直接用 scanpy 内置数据集(推荐新手用这个,自动下载)
adata = sc.datasets.pbmc3k() # 下载并加载 PBMC 3k 数据
# adata 就是一个 AnnData 对象,包含了所有信息
# 方法二:从 Cell Ranger 输出目录读取
# adata = sc.read_10x_mtx(
# 'data/filtered_feature_bc_matrix/', # Cell Ranger 输出目录的路径
# var_names='gene_symbols', # 用基因名(而不是 Ensembl ID)作为变量名
# cache=True # 缓存为 h5ad 格式,下次读取更快
# )
# 方法三:读取已保存的 h5ad 文件
# adata = sc.read_h5ad('pbmc3k.h5ad')
print(adata) # 查看数据基本信息
# 输出类似:AnnData object with n_obs × n_vars = 2700 × 32738
# n_obs = 细胞数(2700),n_vars = 基因数(32738)
Step 2: 质控(QC)¶
质控的目的是过滤掉低质量细胞和双细胞(doublets)。
# 确保基因名是唯一的(有些基因有重名)
adata.var_names_make_unique() # 给重名基因加后缀,确保唯一
# 标记线粒体基因(线粒体基因名字以 "MT-" 开头)
adata.var['mt'] = adata.var_names.str.startswith('MT-') # 布尔标记
# 计算 QC 指标
sc.pp.calculate_qc_metrics(
adata, # 数据对象
qc_vars=['mt'], # 计算线粒体相关指标
percent_top=None, # 不计算 top N 基因的占比
log1p=False, # 不取对数
inplace=True # 直接写入 adata
)
# 画 QC 小提琴图,直观看数据质量
sc.pl.violin(
adata,
['n_genes_by_counts', # 每个细胞检测到的基因数
'total_counts', # 每个细胞的总 UMI 数
'pct_counts_mt'], # 线粒体基因占比(%)
jitter=0.4, # 加点抖动,看分布
multi_panel=True # 每个指标画一个面板
)
# 过滤低质量细胞
sc.pp.filter_cells(adata, min_genes=200) # 去掉基因数 < 200 的细胞(可能是空液滴)
sc.pp.filter_cells(adata, max_genes=2500) # 去掉基因数 > 2500 的细胞(可能是双细胞)
adata = adata[adata.obs.pct_counts_mt < 5, :] # 去掉线粒体占比 > 5% 的细胞(可能是死细胞)
# 过滤低表达基因
sc.pp.filter_genes(adata, min_cells=3) # 去掉在少于 3 个细胞中表达的基因
print(f"质控后:{adata.n_obs} 个细胞, {adata.n_vars} 个基因")
# 典型输出:质控后:2638 个细胞, 13714 个基因
QC 三板斧的含义: - 基因数太少(<200)→ 空液滴或碎片,不是真细胞 - 基因数太多(>2500)→ 可能是两个细胞粘一起了(doublet) - 线粒体比例太高(>5%)→ 细胞正在死亡,细胞质 RNA 已经降解,只剩线粒体 RNA
Step 3: 归一化¶
# 归一化:让不同细胞之间的 UMI 总数可比
sc.pp.normalize_total(
adata,
target_sum=1e4 # 把每个细胞的总 UMI 数缩放到 10000
)
# 对数化:压缩数据范围,让高表达基因不会主导分析
sc.pp.log1p(adata) # log1p = log(x + 1),加 1 是为了避免 log(0)
# 保存归一化后的数据(后面差异表达要用原始数据)
adata.raw = adata # .raw 保存一份完整数据的副本
Step 4: 高变基因选择¶
# 找出高变基因(Highly Variable Genes, HVGs)
# 这些基因在不同细胞间表达差异大,最有信息量
sc.pp.highly_variable_genes(
adata,
min_mean=0.0125, # 平均表达量下限
max_mean=3, # 平均表达量上限
min_disp=0.5 # 离散度下限(变异程度)
)
# 画高变基因图:红色点是被选中的高变基因
sc.pl.highly_variable_genes(adata)
# 只保留高变基因用于后续分析
adata = adata[:, adata.var.highly_variable] # 筛选列(基因维度)
print(f"高变基因数:{adata.n_vars}")
# 典型输出:高变基因数:约 1800 个
Step 5: 降维 —— PCA → UMAP¶
# 缩放数据(让每个基因的均值为 0,方差为 1)
sc.pp.scale(
adata,
max_value=10 # 把极端值截断到 10,避免异常值干扰
)
# PCA 降维:从几千个基因压缩到 50 个主成分
sc.tl.pca(
adata,
svd_solver='arpack', # 使用 arpack 求解器,适合大规模稀疏矩阵
n_comps=50 # 计算 50 个主成分
)
# 画碎石图(Elbow Plot):帮你决定用多少个 PC
sc.pl.pca_variance_ratio(adata, log=True, n_pcs=50)
# 看图找"拐点",一般在第 10-15 个 PC 附近曲线变平
# 计算邻域图(KNN图):基于 PCA 结果找每个细胞的邻居
sc.pp.neighbors(
adata,
n_neighbors=10, # 每个细胞找 10 个最近邻
n_pcs=10 # 使用前 10 个主成分
)
# UMAP 降维:把高维数据压到 2D 用于可视化
sc.tl.umap(adata)
Step 6: 聚类 —— Leiden 算法¶
# Leiden 聚类:自动把细胞分成若干群
sc.tl.leiden(
adata,
resolution=0.5, # 分辨率参数:越大分的群越多,0.5 是常用起点
flavor='igraph', # 使用 igraph 后端
n_iterations=2 # 迭代次数
)
# 可视化聚类结果
sc.pl.umap(
adata,
color=['leiden'], # 用聚类标签着色
title='Leiden Clustering' # 图标题
)
Step 7: 细胞类型注释(marker 基因法)¶
这是最关键也最需要生物学知识的一步。通过已知的 marker 基因 来判断每一群是什么细胞。
# PBMC 中常见细胞类型的 marker 基因
marker_genes = {
'T cells CD4': ['IL7R', 'CD4'], # CD4+ T 细胞
'T cells CD8': ['CD8A', 'CD8B'], # CD8+ T 细胞
'NK cells': ['GNLY', 'NKG7'], # 自然杀伤细胞
'B cells': ['MS4A1', 'CD79A'], # B 细胞(MS4A1 = CD20)
'Monocytes CD14+': ['CD14', 'LYZ'], # CD14+ 单核细胞
'Monocytes FCGR3A+': ['FCGR3A', 'MS4A7'], # CD16+ 单核细胞
'Dendritic cells': ['FCER1A', 'CST3'], # 树突状细胞
'Platelets': ['PPBP'], # 血小板
}
# 用 dotplot 展示每一群中 marker 基因的表达情况
sc.pl.dotplot(
adata,
marker_genes, # marker 基因字典
groupby='leiden', # 按 leiden 聚类分组
dendrogram=True # 加上树状图
)
# 根据 dotplot 结果手动注释
# 假设 cluster 0 高表达 IL7R → 是 CD4+ T 细胞
new_cluster_names = [
'CD4 T', # cluster 0
'CD14 Monocytes', # cluster 1
'B cells', # cluster 2
'CD8 T', # cluster 3
'NK cells', # cluster 4
'FCGR3A Mono', # cluster 5
'Dendritic', # cluster 6
'Platelets', # cluster 7
]
# 把聚类编号替换成细胞类型名字
adata.rename_categories('leiden', new_cluster_names) # 重命名聚类
# 再画一次 UMAP,现在标签是细胞类型
sc.pl.umap(
adata,
color='leiden',
title='Cell Types (PBMC 3k)',
frameon=False # 去掉边框,更好看
)
Step 8: 差异表达分析¶
# 找每一群的 marker 基因(差异表达基因)
sc.tl.rank_genes_groups(
adata,
groupby='leiden', # 按聚类/细胞类型分组
method='wilcoxon', # 使用 Wilcoxon 秩和检验(非参数检验,最常用)
use_raw=True # 使用归一化后的原始数据(.raw 里存的)
)
# 查看每群 top 5 marker 基因
sc.pl.rank_genes_groups(adata, n_genes=5, sharey=False)
# 以表格形式查看结果
result = adata.uns['rank_genes_groups'] # 结果存在 .uns 里
groups = result['names'].dtype.names # 获取所有组名
df = pd.DataFrame({
group + '_' + key: result[key][group]
for group in groups
for key in ['names', 'pvals_adj', 'logfoldchanges']
}).head(10) # 查看前 10 个基因
print(df)
Step 9: 常用可视化¶
# 1. UMAP 图:展示特定基因的表达分布
sc.pl.umap(
adata,
color=['CD4', 'CD8A', 'MS4A1', 'CD14'], # 想看的基因
ncols=2, # 每行 2 列
cmap='Reds' # 颜色方案:红色梯度
)
# 2. 小提琴图(Violin Plot):看某个基因在不同群中的表达分布
sc.pl.violin(
adata,
['CD4', 'CD8A', 'MS4A1'], # 基因列表
groupby='leiden', # 按细胞类型分组
rotation=45 # 横轴标签旋转 45 度,防止重叠
)
# 3. 点图(Dot Plot):同时展示表达量和表达比例
sc.pl.dotplot(
adata,
marker_genes, # 前面定义的 marker 基因字典
groupby='leiden', # 按细胞类型分组
standard_scale='var' # 按基因标准化颜色
)
# 4. 热图(Heatmap):展示 top marker 基因的表达模式
sc.pl.rank_genes_groups_heatmap(
adata,
n_genes=5, # 每群展示 top 5 基因
groupby='leiden',
show_gene_labels=True # 显示基因名
)
# 5. 保存结果
adata.write('pbmc3k_analyzed.h5ad') # 保存为 h5ad 文件,下次直接读取
五、AnnData 对象结构详解¶
AnnData 是 Scanpy 的核心数据结构,可以类比成一个 Excel 工作簿:
AnnData 对象结构:
┌─────────────────────────────────────────────┐
│ .X 表达矩阵(细胞 × 基因) │ ← 就像 Excel 里的成绩表
│ shape: (n_cells, n_genes) │
├─────────────────────────────────────────────┤
│ .obs 细胞元数据(DataFrame) │ ← 学生信息表(姓名、班级、成绩)
│ 每行一个细胞 │
│ 例如:聚类标签、QC指标 │
├─────────────────────────────────────────────┤
│ .var 基因元数据(DataFrame) │ ← 科目信息表(科目名、是否必修)
│ 每行一个基因 │
│ 例如:是否高变基因、线粒体标记 │
├─────────────────────────────────────────────┤
│ .obsm 细胞的多维嵌入(dict) │ ← 降维结果
│ 'X_pca': PCA 坐标 │
│ 'X_umap': UMAP 坐标 │
├─────────────────────────────────────────────┤
│ .uns 非结构化数据(dict) │ ← 杂物柜
│ 聚类颜色、差异表达结果等 │
├─────────────────────────────────────────────┤
│ .obsp 细胞间的关系矩阵 │ ← 邻域图
│ 'connectivities': KNN 连接 │
│ 'distances': 距离矩阵 │
├─────────────────────────────────────────────┤
│ .raw 归一化后的原始数据备份 │ ← 用于差异表达
└─────────────────────────────────────────────┘
常用查看方法¶
adata.shape # 查看维度:(细胞数, 基因数)
adata.obs.head() # 查看前 5 个细胞的元数据
adata.var.head() # 查看前 5 个基因的元数据
adata.obs_names[:5] # 查看前 5 个细胞的 barcode
adata.var_names[:5] # 查看前 5 个基因名
adata.obsm.keys() # 查看有哪些降维结果
adata.uns.keys() # 查看有哪些非结构化数据
六、常见报错与解决¶
报错 1:内存不足(MemoryError / Killed)¶
原因:单细胞数据很大,几万个细胞 × 几万个基因的矩阵非常吃内存。
解决办法:
# 方法一:读取时使用稀疏矩阵(默认就是,确认一下)
import scipy.sparse as sp # 导入 scipy 稀疏矩阵模块
print(type(adata.X)) # 应该是 csr_matrix 或 csc_matrix
# 方法二:尽早过滤基因,减少数据量
sc.pp.filter_genes(adata, min_cells=10) # 把阈值提高
# 方法三:使用 backed 模式(不把全部数据加载到内存)
adata = sc.read_h5ad('large_data.h5ad', backed='r') # 只读模式
报错 2:基因名不匹配¶
原因:你用的基因名格式和数据里的不一样。数据可能用 Ensembl ID(ENSG00000...),而你输入的是基因 symbol。
解决办法:
# 查看数据里基因名长什么样
print(adata.var_names[:5]) # 看看是 gene symbol 还是 Ensembl ID
# 如果是 Ensembl ID,读取时指定使用 gene_symbols
adata = sc.read_10x_mtx(
'filtered_feature_bc_matrix/',
var_names='gene_symbols' # 关键参数:用基因名而不是 ID
)
报错 3:Leiden 聚类报错(没装 leidenalg)¶
解决办法:
报错 4:画图报错(后端问题)¶
原因:在没有图形界面的服务器上运行时,matplotlib 找不到显示器。
解决办法:
# 在脚本最开头(import 之前)加上这两行
import matplotlib # 导入 matplotlib
matplotlib.use('Agg') # 使用非交互式后端,不需要显示器
import matplotlib.pyplot as plt # 然后再导入 pyplot
# 或者保存图片而不是显示
sc.settings.set_figure_params(dpi=150) # 设置分辨率
sc.pl.umap(adata, color='leiden', save='_clusters.png') # save 参数保存图片
# 图片会保存到 ./figures/ 目录下
报错 5:数据格式不兼容¶
解决办法:
import scipy.sparse as sp # 导入稀疏矩阵模块
# 如果 .X 是 numpy array,转成稀疏矩阵节省内存
adata.X = sp.csr_matrix(adata.X)
# 如果 .X 是其他格式,先转成 numpy array
import numpy as np # 导入 numpy
adata.X = np.array(adata.X)
七、速查表:Scanpy 常用函数¶
数据读写¶
| 函数 | 功能 |
|---|---|
sc.read_10x_mtx() | 读取 Cell Ranger 输出的 mtx 文件 |
sc.read_h5ad() | 读取 h5ad 格式文件 |
sc.read_10x_h5() | 读取 Cell Ranger 的 h5 文件 |
adata.write() | 保存为 h5ad 文件 |
sc.datasets.pbmc3k() | 加载 PBMC 3k 示例数据 |
预处理(sc.pp)¶
| 函数 | 功能 |
|---|---|
sc.pp.filter_cells() | 按基因数/UMI数过滤细胞 |
sc.pp.filter_genes() | 按细胞数过滤基因 |
sc.pp.calculate_qc_metrics() | 计算质控指标 |
sc.pp.normalize_total() | 归一化(缩放总 UMI 数) |
sc.pp.log1p() | 对数化 log(x+1) |
sc.pp.highly_variable_genes() | 找高变基因 |
sc.pp.scale() | 标准化(均值 0,方差 1) |
sc.pp.neighbors() | 计算 KNN 邻域图 |
分析工具(sc.tl)¶
| 函数 | 功能 |
|---|---|
sc.tl.pca() | PCA 降维 |
sc.tl.umap() | UMAP 降维 |
sc.tl.tsne() | t-SNE 降维 |
sc.tl.leiden() | Leiden 聚类 |
sc.tl.louvain() | Louvain 聚类 |
sc.tl.rank_genes_groups() | 差异表达分析 |
sc.tl.dendrogram() | 聚类树状图 |
可视化(sc.pl)¶
| 函数 | 功能 |
|---|---|
sc.pl.umap() | 画 UMAP 图 |
sc.pl.pca() | 画 PCA 图 |
sc.pl.violin() | 小提琴图 |
sc.pl.dotplot() | 点图(大小=比例,颜色=表达量) |
sc.pl.heatmap() | 热图 |
sc.pl.stacked_violin() | 堆叠小提琴图 |
sc.pl.rank_genes_groups() | 差异表达结果图 |
sc.pl.pca_variance_ratio() | PCA 碎石图 |
sc.pl.highly_variable_genes() | 高变基因图 |
参考资源¶
- Scanpy 官方文档:https://scanpy.readthedocs.io/
- Scanpy PBMC 3k 教程:https://scanpy-tutorials.readthedocs.io/en/latest/pbmc3k.html
- 10x Genomics 数据下载:https://www.10xgenomics.com/datasets
- AnnData 文档:https://anndata.readthedocs.io/