185_单细胞数据质控标准¶
一句话概述¶
单细胞RNA-seq数据质控通过过滤低质量细胞(低基因数/高线粒体比例/doublet)和低质量基因(低表达),确保下游分析基于可靠的数据,是单细胞分析流程中最关键且最容易出错的步骤。
核心知识点表格¶
| 知识点 | 说明 |
|---|---|
| 细胞级QC | nFeature(基因数)、nCount(UMI数)、percent.mt(线粒体基因比例) |
| 基因级QC | 至少在N个细胞中表达的基因才保留 |
| 线粒体比例 | 高线粒体比例提示细胞破损/死亡 |
| 核糖体比例 | 辅助指标,异常高可能提示细胞状态异常 |
| Doublet | 两个细胞共享一个barcode,需用专门工具检测 |
| Ambient RNA | 背景游离RNA污染,使用SoupX/CellBender去除 |
| 自适应阈值 | 基于MAD(中位绝对偏差)自动确定阈值 |
| 组织特异性 | 不同组织/物种的QC标准不同 |
步骤详解¶
第一步:初始数据加载和QC指标计算¶
白话解释:单细胞数据中混有很多"不好"的东西:死细胞、空液滴、两个细胞粘在一起的doublet、和游离的RNA污染。质控就是把这些不好的东西找出来去掉。
技术细节:CellRanger等上游工具已经过滤了完全空的液滴(空barcode),但通过的细胞仍可能包含低质量细胞。需要从多个维度评估细胞质量。
library(Seurat)
library(ggplot2)
library(patchwork)
# 加载数据
data <- Read10X(data.dir = "filtered_feature_bc_matrix/")
seurat_obj <- CreateSeuratObject(counts = data, project = "sample1",
min.cells = 3, min.features = 200)
# 计算QC指标
# 线粒体基因比例
seurat_obj[["percent.mt"]] <- PercentageFeatureSet(seurat_obj, pattern = "^MT-")
# 核糖体基因比例
seurat_obj[["percent.ribo"]] <- PercentageFeatureSet(seurat_obj, pattern = "^RP[SL]")
# 血红蛋白基因比例(血液样本)
seurat_obj[["percent.hb"]] <- PercentageFeatureSet(seurat_obj, pattern = "^HB[^(P)]")
# 计算复杂度(log10GenesPerUMI)
seurat_obj[["log10GenesPerUMI"]] <- log10(seurat_obj$nFeature_RNA) / log10(seurat_obj$nCount_RNA)
# 查看QC指标分布
head(seurat_obj@meta.data[, c("nFeature_RNA", "nCount_RNA", "percent.mt",
"percent.ribo", "log10GenesPerUMI")])
第二步:QC指标可视化¶
白话解释:先画图看看数据长什么样,再决定过滤阈值。好的数据应该有明确的峰,坏的细胞通常是分布的"尾巴"。
# 小提琴图
p1 <- VlnPlot(seurat_obj, features = c("nFeature_RNA", "nCount_RNA", "percent.mt"),
ncol = 3, pt.size = 0)
print(p1)
# 散点图:UMI vs 基因数(应该呈正相关)
p2 <- FeatureScatter(seurat_obj, feature1 = "nCount_RNA", feature2 = "nFeature_RNA") +
geom_smooth(method = "lm", se = FALSE) +
NoLegend()
# 散点图:UMI vs 线粒体比例
p3 <- FeatureScatter(seurat_obj, feature1 = "nCount_RNA", feature2 = "percent.mt") +
NoLegend()
p2 | p3
# 密度图
library(ggplot2)
ggplot(seurat_obj@meta.data, aes(x = nFeature_RNA)) +
geom_density(fill = "steelblue", alpha = 0.5) +
geom_vline(xintercept = c(200, 5000), linetype = "dashed", color = "red") +
theme_minimal() +
labs(title = "Gene Count Distribution")
ggplot(seurat_obj@meta.data, aes(x = percent.mt)) +
geom_density(fill = "salmon", alpha = 0.5) +
geom_vline(xintercept = 20, linetype = "dashed", color = "red") +
theme_minimal() +
labs(title = "Mitochondrial % Distribution")
第三步:确定过滤阈值¶
白话解释:过滤阈值没有"放之四海而皆准"的标准。不同组织、物种、平台都不同。可以用MAD方法自动确定,也可以根据图形手动设定。
技术细节:MAD(Median Absolute Deviation)方法:计算各指标的中位数和MAD,将超过中位数±N×MAD的细胞标记为异常。通常N取3(约99.7%置信区间)。这比固定阈值更适应不同数据集。
# 方法1:基于MAD的自适应阈值
calc_mad_threshold <- function(values, nmads = 3, type = "both") {
med <- median(values)
mad_val <- mad(values)
lower <- med - nmads * mad_val
upper <- med + nmads * mad_val
if (type == "lower") return(lower)
if (type == "upper") return(upper)
return(c(lower = lower, upper = upper))
}
# 计算各指标阈值
nfeature_lower <- calc_mad_threshold(log10(seurat_obj$nFeature_RNA), 3, "lower")
nfeature_upper <- calc_mad_threshold(log10(seurat_obj$nFeature_RNA), 3, "upper")
mt_upper <- calc_mad_threshold(seurat_obj$percent.mt, 3, "upper")
cat("nFeature阈值:", 10^nfeature_lower, "-", 10^nfeature_upper, "\n")
cat("percent.mt上限:", mt_upper, "\n")
# 方法2:scater包的自动QC
library(scater)
sce <- as.SingleCellExperiment(seurat_obj)
sce <- addPerCellQCMetrics(sce, subsets = list(mito = grep("^MT-", rownames(sce))))
qc_results <- quickPerCellQC(colData(sce),
sub.fields = "subsets_mito_percent",
nmads = 3)
cat("自动过滤的细胞数:", sum(qc_results$discard), "\n")
# 方法3:常用的经验阈值(参考值)
# nFeature_RNA: 200-5000 (或 200-8000 for complex tissues)
# nCount_RNA: 500-50000
# percent.mt: <10% (default) 或 <20% (for certain tissues)
# log10GenesPerUMI: >0.8
第四步:执行过滤¶
# 记录过滤前信息
cat("过滤前细胞数:", ncol(seurat_obj), "\n")
# 过滤
seurat_obj <- subset(seurat_obj,
subset = nFeature_RNA > 200 &
nFeature_RNA < 5000 &
nCount_RNA > 500 &
nCount_RNA < 50000 &
percent.mt < 15 &
log10GenesPerUMI > 0.8)
cat("过滤后细胞数:", ncol(seurat_obj), "\n")
# 过滤后重新可视化确认
VlnPlot(seurat_obj, features = c("nFeature_RNA", "nCount_RNA", "percent.mt"),
ncol = 3, pt.size = 0)
第五步:Ambient RNA去除¶
白话解释:在液滴中,除了细胞自身的RNA,还有背景中游离的RNA(来自破碎的细胞)。这些"汤"中的RNA会污染每个液滴,使得不应该表达某个基因的细胞看起来也表达了。
# 使用SoupX去除ambient RNA
library(SoupX)
# 加载数据(需要raw和filtered矩阵)
toc <- Read10X("filtered_feature_bc_matrix/")
tod <- Read10X("raw_feature_bc_matrix/")
# 创建SoupChannel
sc <- SoupChannel(tod, toc)
# 需要先做聚类
seurat_temp <- CreateSeuratObject(counts = toc)
seurat_temp <- NormalizeData(seurat_temp) %>%
FindVariableFeatures() %>%
ScaleData() %>%
RunPCA() %>%
FindNeighbors(dims = 1:20) %>%
FindClusters(resolution = 0.5)
sc <- setClusters(sc, setNames(seurat_temp$seurat_clusters,
colnames(seurat_temp)))
# 估计污染比例
sc <- autoEstCont(sc)
cat("估计的污染比例:", sc$fit$rhoEst, "\n")
# 校正
corrected_counts <- adjustCounts(sc)
# 使用校正后的数据
seurat_obj <- CreateSeuratObject(counts = corrected_counts)
第六步:Python实现(Scanpy)¶
import scanpy as sc
import numpy as np
adata = sc.read_10x_h5("filtered_feature_bc_matrix.h5")
# 计算QC指标
adata.var['mt'] = adata.var_names.str.startswith('MT-')
adata.var['ribo'] = adata.var_names.str.startswith(('RPS', 'RPL'))
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt', 'ribo'], percent_top=None,
log1p=False, inplace=True)
# 可视化
sc.pl.violin(adata, ['n_genes_by_counts', 'total_counts', 'pct_counts_mt'],
jitter=0.4, multi_panel=True, save='_qc.png')
sc.pl.scatter(adata, x='total_counts', y='n_genes_by_counts',
color='pct_counts_mt', save='_qc_scatter.png')
# MAD过滤
def mad_filter(adata, metric, nmads=3):
med = np.median(adata.obs[metric])
mad = np.median(np.abs(adata.obs[metric] - med))
upper = med + nmads * 1.4826 * mad
lower = med - nmads * 1.4826 * mad
return lower, upper
nf_low, nf_high = mad_filter(adata, 'n_genes_by_counts')
mt_low, mt_high = mad_filter(adata, 'pct_counts_mt')
print(f"Gene filter: {nf_low:.0f} - {nf_high:.0f}")
print(f"MT filter: < {mt_high:.1f}%")
# 过滤
print(f"Before filtering: {adata.n_obs} cells")
sc.pp.filter_cells(adata, min_genes=200)
adata = adata[adata.obs.n_genes_by_counts < 5000, :]
adata = adata[adata.obs.pct_counts_mt < 15, :]
print(f"After filtering: {adata.n_obs} cells")
实战命令速查¶
# Seurat快速QC
obj[["percent.mt"]] <- PercentageFeatureSet(obj, pattern = "^MT-")
obj <- subset(obj, nFeature_RNA > 200 & nFeature_RNA < 5000 & percent.mt < 15)
# 自适应MAD阈值
library(scater); qc <- quickPerCellQC(colData(sce), sub.fields = "subsets_mito_percent")
# SoupX ambient RNA去除
sc <- SoupChannel(raw, filtered); sc <- autoEstCont(sc); out <- adjustCounts(sc)
面试常问点¶
Q1: 单细胞QC的三个核心指标是什么? A: (1) nFeature/nGene:检测到的基因数,太低可能是空液滴或破碎细胞,太高可能是doublet;(2) nCount/nUMI:UMI总数,反映测序深度和细胞RNA含量;(3) percent.mt:线粒体基因比例,高比例表示细胞膜破损,胞质RNA泄漏,线粒体RNA相对富集。
Q2: 为什么高线粒体比例代表低质量细胞? A: 当细胞破损时,细胞质中的mRNA会泄漏出去,但线粒体mRNA因为在线粒体内膜上,相对不容易丢失。因此破损细胞中线粒体RNA的比例会升高。但注意某些正常细胞类型(如心肌细胞、肾脏细胞)本身线粒体含量就很高。
Q3: QC阈值应该如何确定? A: 没有universal标准。推荐:(1)先可视化分布;(2)使用MAD方法(median±3*MAD)自适应确定;(3)参考同类型数据的文献;(4)对不同样本/组织分别设定阈值。避免使用过于严格或过于宽松的固定阈值。
Q4: Ambient RNA污染如何影响分析? A: Ambient RNA会使得所有细胞都看起来表达某些高表达基因(如血红蛋白基因),导致:(1)标记基因在非预期细胞类型中出现表达;(2)聚类不清晰;(3)差异表达分析出现假阳性。使用SoupX或CellBender可以校正。
Q5: 过滤太严格和太宽松分别有什么后果? A: 过于严格会丢失真实细胞(特别是小细胞、静息态细胞),影响细胞类型覆盖。过于宽松会保留低质量细胞和doublet,导致虚假聚类和错误的差异基因。
易错点¶
- 不同物种使用相同的线粒体基因模式:人类用
^MT-,小鼠用^mt-,需确认 - 对所有样本使用相同固定阈值:不同样本质量差异大,应分别评估
- 先合并再QC:应先对每个样本单独QC,然后再合并
- 忽略doublet检测:QC三指标无法完全检测doublet,需专门工具
- 过滤后不重新检查:过滤后应重新画图确认分布合理
补充知识¶
不同组织的参考QC阈值¶
| 组织类型 | nFeature | percent.mt | 特殊注意 |
|---|---|---|---|
| PBMC | 200-4000 | <10% | 注意血小板/红细胞污染 |
| 肿瘤 | 200-8000 | <20% | 高异质性,阈值放宽 |
| 脑组织 | 200-6000 | <5% | 线粒体阈值较严 |
| 肝脏 | 200-5000 | <30% | 肝细胞线粒体含量高 |
| 心脏 | 200-6000 | <30% | 心肌细胞线粒体多 |
| 肾脏 | 200-5000 | <50% | 近端小管细胞MT极高 |
QC决策流程¶
- 加载raw和filtered矩阵
- Ambient RNA校正(SoupX/CellBender)
- 计算QC指标(基因数、UMI、线粒体%)
- 可视化分布,确定阈值
- 过滤低质量细胞
- Doublet检测与过滤(scDblFinder)
- 验证过滤结果