跳转至

185_单细胞数据质控标准

一句话概述

单细胞RNA-seq数据质控通过过滤低质量细胞(低基因数/高线粒体比例/doublet)和低质量基因(低表达),确保下游分析基于可靠的数据,是单细胞分析流程中最关键且最容易出错的步骤。

核心知识点表格

知识点说明
细胞级QCnFeature(基因数)、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,导致虚假聚类和错误的差异基因。

易错点

  1. 不同物种使用相同的线粒体基因模式:人类用^MT-,小鼠用^mt-,需确认
  2. 对所有样本使用相同固定阈值:不同样本质量差异大,应分别评估
  3. 先合并再QC:应先对每个样本单独QC,然后再合并
  4. 忽略doublet检测:QC三指标无法完全检测doublet,需专门工具
  5. 过滤后不重新检查:过滤后应重新画图确认分布合理

补充知识

不同组织的参考QC阈值

组织类型nFeaturepercent.mt特殊注意
PBMC200-4000<10%注意血小板/红细胞污染
肿瘤200-8000<20%高异质性,阈值放宽
脑组织200-6000<5%线粒体阈值较严
肝脏200-5000<30%肝细胞线粒体含量高
心脏200-6000<30%心肌细胞线粒体多
肾脏200-5000<50%近端小管细胞MT极高

QC决策流程

  1. 加载raw和filtered矩阵
  2. Ambient RNA校正(SoupX/CellBender)
  3. 计算QC指标(基因数、UMI、线粒体%)
  4. 可视化分布,确定阈值
  5. 过滤低质量细胞
  6. Doublet检测与过滤(scDblFinder)
  7. 验证过滤结果