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

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

Written By
技能练习生
技能练习生

深入理解 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 数。

实际数据偏离泊松分布的原因

  1. 过度离散(Over-dispersion):方差 > 均值

    • 原因:基因表达异质性、捕获效率差异
    • 结果:负二项分布更合适
  2. 零膨胀(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σ,更加保守。

优势

  1. 鲁棒性:不受极端值影响
  2. 自适应性:自动适应数据分布
  3. 保守性:天然倾向于宽松过滤

5.2.3 MAD 倍数的选择

理论依据(正态分布假设):

MAD 倍数等价 σ预期异常值比例适用场景
22.97~0.3%严格过滤(高置信度数据)
34.45~0.01%标准过滤(推荐)
57.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 线粒体基因比例

为什么线粒体比例反映细胞质量?

生物学机制

  1. 细胞凋亡/坏死

    • 细胞膜破裂,细胞质 RNA 泄漏
    • 线粒体位于内膜,相对保留
    • 结果:mt% 升高
  2. 低温应激

    • 组织解离过程中温度下降
    • 线粒体转录受抑制程度低于核基因
    • 结果:mt% 相对升高
  3. 细胞类型特异性

    • 高能量需求细胞(心肌、肾小管)天生 mt% 高
    • 不是所有高 mt% 都是低质量!

组织基线参考(来自文献):

组织正常 mt%病理阈值
PBMC3-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)
  • 线性关系(在某些范围内)

偏离原因

  1. 双细胞

    预期:n_counts = 2×正常,n_genes ≈ 1.5×正常
    原因:两个细胞的部分基因重叠
  2. 低质量细胞

    预期:n_counts 低,n_genes 低
    原因:RNA 损失
  3. 高转录活性细胞

    预期: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 算法原理

核心思想

  1. 创建"人工双细胞":随机合并两个细胞的表达谱
  2. 训练分类器区分真实细胞和人工双细胞
  3. 对每个细胞计算"双细胞得分"

关键步骤

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