
单细胞 RNA-seq 质量控制:工作原理

深入理解 QC 方法的理论依据
本章讲解单细胞 QC 的理论基础、统计方法和工具原理,帮助你从根本上理解为什么这些方法有效,以及如何正确应用它们。
注意:本章需要一定的统计学基础。如果你主要关注应用而非理论,可以跳过本章直接进入 FAQ 部分。
5.1 单细胞数据的质量问题来源
5.1.1 技术噪声 vs 生物学信号
理解单细胞数据质量问题的第一步是区分技术噪声和真实生物学变异。
技术噪声来源:
| 来源 | 机制 | QC 指标 |
|---|---|---|
| 细胞破碎 | 细胞膜破损,RNA 泄漏 | 高 mt%、低 n_genes、低 n_counts |
| 测序失败 | 捕获或扩增效率低 | 低 n_counts |
| 空液滴 | 无细胞的液滴捕获环境 RNA | 极低 n_counts |
| 双细胞 | 两个细胞被封装在一起 | 异常高的 n_counts 和 n_genes |
| 批次效应 | 不同实验条件 | 需要整合分析 |
| 细胞周期效应 | 增殖状态差异 | 细胞周期评分 |
生物学信号:
- 稀有细胞群(频率 < 5%)
- 低转录量细胞(如静息细胞)
- 组织特异性表达谱(如心肌细胞高线粒体)
QC 的核心挑战:区分这两者,保留真实信号,去除技术噪声。
5.1.2 单细胞捕获的统计特性
泊松分布与 UMI 计数
理想情况下,UMI 计数遵循泊松分布:
P(X = k) = (λ^k * e^(-λ)) / k!其中 λ 是期望的 UMI 数。
实际数据偏离泊松分布的原因:
-
过度离散(Over-dispersion):方差 > 均值
- 原因:基因表达异质性、捕获效率差异
- 结果:负二项分布更合适
-
零膨胀(Zero-inflation):零值过多
- 原因:dropout 事件(基因未检测到)
- 结果:零膨胀负二项分布(ZINB)
对 QC 的启示:
- 不能假设"正常"细胞服从简单分布
- 需要鲁棒的统计量(如 MAD)
- 固定阈值方法(如 3σ)不适用
5.2 MAD 方法的统计学原理
5.2.1 为什么不用标准差?
标准差(Standard Deviation)的问题:
import numpy as np
# 示例数据(包含异常值)
data = np.array([1000, 1200, 1100, 1300, 1050, 50000]) # 50000 是异常值
# 标准差
mean = np.mean(data)
std = np.std(data)
upper_threshold_std = mean + 3 * std
print(f"标准差方法:")
print(f" 均值:{mean:.0f}")
print(f" 标准差:{std:.0f}")
print(f" 上限:{upper_threshold_std:.0f}")
print(f" 被标记为异常:{np.sum(data > upper_threshold_std)} 个")
# MAD
median = np.median(data)
mad = np.median(np.abs(data - median))
upper_threshold_mad = median + 3 * mad
print(f"\nMAD 方法:")
print(f" 中位数:{median:.0f}")
print(f" MAD: {mad:.0f}")
print(f" 上限:{upper_threshold_mad:.0f}")
print(f" 被标记为异常:{np.sum(data > upper_threshold_mad)} 个")输出对比:
标准差方法:
均值: 10775 ← 被异常值拉高
标准差: 19517
上限: 69326
被标记为异常: 0 个 ← 未能检测到异常值!
MAD 方法:
中位数: 1150 ← 不受异常值影响
MAD: 75
上限: 1375
被标记为异常: 1 个 ← 成功检测!结论:MAD 对异常值具有鲁棒性,更适合单细胞 QC。
5.2.2 MAD 的数学性质
定义:
MAD = median(|xi - median(x)|)与标准差的关系(正态分布下):
σ ≈ 1.4826 × MAD因此,MAD 的 "3σ" 等价于标准差的 3 × 1.4826 ≈ 4.45σ,更加保守。
优势:
- 鲁棒性:不受极端值影响
- 自适应性:自动适应数据分布
- 保守性:天然倾向于宽松过滤
5.2.3 MAD 倍数的选择
理论依据(正态分布假设):
| MAD 倍数 | 等价 σ | 预期异常值比例 | 适用场景 |
|---|---|---|---|
| 2 | 2.97 | ~0.3% | 严格过滤(高置信度数据) |
| 3 | 4.45 | ~0.01% | 标准过滤(推荐) |
| 5 | 7.41 | ~0.0001% | 极宽松(稀有细胞群) |
实际应用建议:
- 标准场景:n_mads = 3
- 高异质性数据(肿瘤、胚胎):n_mads = 5
- 高质量数据(重复样本):n_mads = 2
验证方法:
# 检查过滤后的分布
adata_filtered = adata[filter_mask]
# 计算偏度和峰度
from scipy.stats import skew, kurtosis
skewness = skew(adata_filtered.obs['n_genes_by_counts'])
kurt = kurtosis(adata_filtered.obs['n_genes_by_counts'])
print(f"偏度:{skewness:.2f}") # 期望接近 0
print(f"峰度:{kurt:.2f}") # 期望接近 0(正态分布)5.3 QC 指标的生物学解释
5.3.1 线粒体基因比例
为什么线粒体比例反映细胞质量?
生物学机制:
-
细胞凋亡/坏死:
- 细胞膜破裂,细胞质 RNA 泄漏
- 线粒体位于内膜,相对保留
- 结果:mt% 升高
-
低温应激:
- 组织解离过程中温度下降
- 线粒体转录受抑制程度低于核基因
- 结果:mt% 相对升高
-
细胞类型特异性:
- 高能量需求细胞(心肌、肾小管)天生 mt% 高
- 不是所有高 mt% 都是低质量!
组织基线参考(来自文献):
| 组织 | 正常 mt% | 病理阈值 |
|---|---|---|
| PBMC | 3-8% | > 12% |
| 心肌 | 15-25% | > 30% |
| 肝脏 | 8-15% | > 20% |
| 肾脏 | 5-12% | > 18% |
| 肿瘤 | 5-20% | 视具体类型 |
| 神经元 | 5-10% | > 15% |
验证策略:
# 检查高 mt% 细胞是否表达细胞类型标志物
high_mt = adata.obs['pct_counts_mt'] > 20
# 心肌细胞标志物
if 'MYH6' in adata.var_names:
cardiomyocyte = adata[:, 'MYH6'].X > 0
overlap = (high_mt & cardiomyocyte).sum()
print(f"高 mt% 且表达 MYH6 的细胞:{overlap}")
# 如果重叠高,说明这些是正常心肌细胞,不应过滤5.3.2 基因数与 UMI 数的关系
理论期望:
- 强正相关(r > 0.85)
- 线性关系(在某些范围内)
偏离原因:
-
双细胞:
预期:n_counts = 2×正常,n_genes ≈ 1.5×正常 原因:两个细胞的部分基因重叠 -
低质量细胞:
预期:n_counts 低,n_genes 低 原因:RNA 损失 -
高转录活性细胞:
预期:n_counts 高,n_genes 高 原因:增殖或激活状态
检测异常:
# 计算残差
from scipy.stats import linregress
slope, intercept, r_value, p_value, std_err = linregress(
adata.obs['n_counts'],
adata.obs['n_genes_by_counts']
)
# 预测值
predicted_genes = intercept + slope * adata.obs['n_counts']
residuals = adata.obs['n_genes_by_counts'] - predicted_genes
# 标记异常(残差 > 3 MAD)
mad_residuals = np.median(np.abs(residuals - np.median(residuals)))
outliers = np.abs(residuals) > 3 * mad_residuals
print(f"异常细胞数:{outliers.sum()}")5.3.3 核糖体基因比例
生物学意义:
- 反映蛋白质合成活性
- 与细胞增殖相关
细胞周期关联:
# 核糖体比例 vs 细胞周期
sc.tl.score_genes_cell_cycle(adata, ...)
# 可视化
import seaborn as sns
sns.boxplot(
data=adata.obs,
x='phase',
y='pct_counts_ribo'
)
# 期望:S 期和 G2M 期核糖体比例高是否需要过滤?
- 通常不需要:核糖体高表达是生物学信号,非技术噪声
- 例外:如果核糖体比例异常高(> 50%),可能存在核糖体 RNA 污染
5.4 双细胞检测原理
5.4.1 双细胞的来源
10X Genomics 中的双细胞率:
理论双细胞率 ≈ (加载细胞数 / 回收细胞数) × 1000
例如:
- 目标回收: 5000 细胞
- 实际加载: 6000 细胞
- 双细胞率: (6000 / 5000) × 0.8% ≈ 1.0%5.4.2 Scrublet 算法原理
核心思想:
- 创建"人工双细胞":随机合并两个细胞的表达谱
- 训练分类器区分真实细胞和人工双细胞
- 对每个细胞计算"双细胞得分"
关键步骤:
import scrublet as scr
# 1. 创建 Scrublet 对象
scrub = scr.Scrublet(counts_matrix)
# 2. 检测双细胞
doublet_scores, predicted_doublets = scrub.scrub_doublets()
# 3. 双细胞得分分布
# - 低得分:单细胞
# - 高得分:可能的双细胞
# - 双峰分布:单细胞和双细胞分离良好
# 4. 自动阈值选择
# Scrublet 自动在双峰之间的谷处设定阈值阈值调整:
# 查看双细胞得分分布
scrub.plot_doublet_score_histogram()
# 如果阈值不合理,手动调整
threshold = 0.25 # 根据直方图选择
predicted_doublets = doublet_scores > threshold局限性:
- 对同类双细胞(同种细胞类型)敏感性低
- 对罕见细胞群可能误判为双细胞
5.4.3 其他检测方法
DoubletFinder(R):
- 基于最近邻算法
- 在 Seurat 流程中集成
scds(混合方法):
- 结合多种算法(Scrublet、DoubletDetector 等)
- 提供集成预测
5.5 QC 参数的选择策略
5.5.1 数据驱动的参数选择
Step 1:探索性分析
# 检查数据分布
for metric in ['n_genes_by_counts', 'n_counts', 'pct_counts_mt']:
median = np.median(adata.obs[metric])
mad = np.median(np.abs(adata.obs[metric] - median))
print(f"\n{metric}:")
print(f" 中位数:{median:.1f}")
print(f" MAD: {mad:.1f}")
print(f" 3 MAD 阈值:[{median-3*mad:.1f}, {median+3*mad:.1f}]")Step 2:迭代优化
# 尝试不同 n_mads
for n_mads in [2, 3, 5]:
filter_mask = (
mad_filter(adata.obs['n_genes_by_counts'], n_mads, 'lower') &
mad_filter(adata.obs['pct_counts_mt'], n_mads, 'upper')
)
retention = filter_mask.sum() / len(filter_mask) * 100
print(f"n_mads={n_mads}: 保留率 {retention:.1f}%")Step 3:生物学验证
# 检查已知细胞群是否保留
marker_genes = {
'T_cell': 'CD3D',
'B_cell': 'MS4A1',
'Monocyte': 'LYZ'
}
for cell_type, marker in marker_genes.items():
if marker in adata.var_names:
before = (adata[:, marker].X > 0).sum()
after = (adata_qc[:, marker].X > 0).sum()
retention = after / before * 100
print(f"{cell_type} 保留率:{retention:.1f}%")5.5.2 组织特异性调整
决策树:
组织类型?
├─ 免疫细胞(PBMC)
│ └─ 标准 MAD (3)
│ └─ mt% 阈值: 中位数 + 3MAD (≈ 10-12%)
│
├─ 高能量组织(心脏、肌肉、肾)
│ └─ 提高 mt% 阈值 (5 MAD)
│ └─ 验证: 检查组织特异标志物
│
├─ 肿瘤
│ └─ 极宽松 (5 MAD)
│ └─ 不过滤低 UMI(可能是 CTC)
│
├─ 胚胎/干细胞
│ └─ 极宽松 (5 MAD)
│ └─ 允许低 n_genes
│
└─ 非模式生物
└─ 验证线粒体基因前缀5.6 QC 与下游分析的关系
5.6.1 过滤对聚类的影响
过度过滤的后果:
# 模拟:严格过滤 vs 宽松过滤
strict_filter = mad_filter(adata.obs['pct_counts_mt'], 2, 'upper') # 严格
permissive_filter = mad_filter(adata.obs['pct_counts_mt'], 5, 'upper') # 宽松
# 分别聚类
adata_strict = adata[strict_filter].copy()
adata_permissive = adata[permissive_filter].copy()
sc.pp.pca(adata_strict)
sc.pp.neighbors(adata_strict)
sc.tl.leiden(adata_strict, resolution=0.5)
sc.pp.pca(adata_permissive)
sc.pp.neighbors(adata_permissive)
sc.tl.leiden(adata_permissive, resolution=0.5)
print(f"严格过滤聚类数:{adata_strict.obs['leiden'].nunique()}")
print(f"宽松过滤聚类数:{adata_permissive.obs['leiden'].nunique()}")
# 预期:宽松过滤保留更多稀有簇5.6.2 过滤对差异表达的影响
低质量细胞的污染:
# 比较:过滤前 vs 过滤后的 DE 结果
# (假设 sc.tl.rank_genes_groups 已运行)
# 高 mt% 细胞的伪基因
high_mt_genes = adata[adata.obs['pct_counts_mt'] > 20].var_names
print(f"高 mt% 细胞富集的基因:{high_mt_genes[:10]}")
# 这些"差异表达"可能是技术伪影5.6.3 QC 的迭代优化
推荐流程:
1. 初始 QC(宽松)
↓
2. 初步聚类和细胞类型注释
↓
3. 检查可疑簇
- 高 mt%?
- 低 n_genes?
- 表达应激反应基因?
↓
4. 调整 QC 参数(如果需要)
↓
5. 重新分析5.7 自动化 QC 工具
5.7.1 Scanpy 的 QC 流程
优势:
- 集成度高
- 与下游分析无缝衔接
- 社区支持好
局限:
- 需要手动选择阈值
- 缺少高级检测(如双细胞)
5.7.2 专业 QC 工具
scran(R):
- 基于 deconvolution 的计算大小因子
- 更好的归一化
scater(R):
- 丰富的 QC 可视化
- 自动化异常检测
scds(Python/R):
- 专注于双细胞检测
- 集成多种算法
cellxgene(可视化):
- 交互式 QC
- 实时探索
下一章
通过本章的学习,你已经深入理解了单细胞 QC 的理论基础和工具原理。在最后一章中,我们汇总了常见问题和解决方案,助你快速排查实际问题。
→ 继续阅读:第六章 - 常见问题 FAQ