单细胞 RNA-seq 质量控制:常见问题

单细胞 RNA-seq 质量控制:常见问题

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

快速解决你遇到的问题

本章汇总了单细胞 QC 过程中最常见的问题和解决方案,涵盖数据处理、过滤策略、工具使用和结果解释的各个环节。

6.1 数据加载与准备

Q1: 如何加载不同格式的数据?

问题:我有多种格式的数据文件,不知道如何加载。

解决方案

import scanpy as sc
import anndata as ad

# 1. .h5ad 文件(AnnData 格式,推荐)
adata = sc.read_h5ad('data.h5ad')

# 2. .h5 文件(10X Genomics 输出)
adata = sc.read_10x_h5('filtered_feature_bc_matrix.h5')

# 3. 10X 目录(包含 barcodes.tsv, genes.tsv, matrix.mtx)
adata = sc.read_10x_mtx('path/to/10X/output/')

# 4. .csv 文件(基因 × 细胞矩阵)
import pandas as pd
counts = pd.read_csv('counts.csv', index_col=0)
adata = ad.AnnData(X=counts.T)  # 转置为细胞 × 基因

# 5. .txt 文件(类似 csv)
counts = pd.read_csv('counts.txt', sep='\t', index_col=0)
adata = ad.AnnData(X=counts.T)

# 验证加载结果
print(f"数据形状:{adata.shape}")
print(f"细胞数:{adata.n_obs}")
print(f"基因数:{adata.n_vars}")

Q2: 如何合并多个数据集?

问题:我有多个批次或样本的数据,需要合并后一起 QC。

解决方案

import scanpy as sc

# 1. 加载多个数据集
adata1 = sc.read_h5ad('sample1.h5ad')
adata2 = sc.read_h5ad('sample2.h5ad')
adata3 = sc.read_h5ad('sample3.h5ad')

# 2. 添加批次标识
adata1.obs['batch'] = 'sample1'
adata2.obs['batch'] = 'sample2'
adata3.obs['batch'] = 'sample3'

# 3. 合并(推荐方法)
adata_combined = adata1.concatenate(adata2, adata3, index_unique=None)

print(f"合并后:{adata_combined.n_obs} 细胞")
print(f"批次分布:\n{adata_combined.obs['batch'].value_counts()}")

# 4. 执行 QC(一起过滤所有批次)
# ... QC 代码 ...

# 5. (可选)分回各批次
adata1_qc = adata_combined[adata_combined.obs['batch'] == 'sample1']
adata2_qc = adata_combined[adata_combined.obs['batch'] == 'sample2']

注意事项

  • 确保不同批次的基因名一致(基因顺序可以不同)
  • 如果基因名不同,需要先取交集
# 取基因交集
common_genes = adata1.var_names.intersection(adata2.var_names)
adata1 = adata1[:, common_genes]
adata2 = adata2[:, common_genes]

Q3: 数据已归一化,如何获取原始计数?

问题:我的数据已经是 log-normalized,但 QC 需要原始计数。

解决方案

# 方案 1:检查是否保存了原始层
print("可用的层:")
print(list(adata.layers.keys()))

# 如果有 'counts' 或 'raw' 层
if 'counts' in adata.layers:
    adata.X = adata.layers['counts'].copy()

# 方案 2:从 log1p 转换回去(近似,不推荐)
# 如果数据是 log1p(counts + 1) 转换的
adata.X = np.expm1(adata.X) - 1  # 反向变换

# 方案 3:重新加载原始数据(最佳)
# 如果你有原始 .h5 文件
adata_raw = sc.read_10x_h5('raw_feature_bc_matrix.h5')

6.2 QC 指标计算

Q4: 找不到线粒体基因

问题adata.var['mt'].sum() 返回 0,没有检测到线粒体基因。

原因与解决

# 1. 检查基因名格式
print("前 20 个基因名:")
print(adata.var_names[:20])

# 2. 可能的情况:
# 情况 A: 基因名是 Ensembl ID(如 ENSG00000244734)
# 解决:需要转换基因 ID 为基因符号
# 使用 mygene 包或参考文件

# 情况 B: 基因名大小写不同
# 小鼠(小写 mt-)
adata.var['mt'] = adata.var_names.str.lower().str.startswith('mt-')

# 人类(大写 MT-)
adata.var['mt'] = adata.var_names.str.upper().str.startswith('MT-')

# 情况 C: 特殊格式(如 "mt-Co1")
# 使用更灵活的匹配
adata.var['mt'] = adata.var_names.str.contains('mt-', case=False, regex=True)

# 3. 验证
mt_genes = adata.var_names[adata.var['mt']]
print(f"检测到 {len(mt_genes)} 个线粒体基因:")
print(mt_genes[:10])

Q5: QC 指标计算报错

问题sc.pp.calculate_qc_metrics 报错。

常见错误与解决

# 错误 1: KeyError: 'mt'
# 原因:未先定义 mt 列
# 解决:
adata.var['mt'] = adata.var_names.str.startswith('MT-')
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True)

# 错误 2: 内存不足
# 原因:数据太大
# 解决:分批计算或使用稀疏矩阵
from scipy import sparse
if not sparse.issparse(adata.X):
    adata.X = sparse.csr_matrix(adata.X)

# 错误 3: 数据类型不对
# 原因:X 不是计数矩阵
# 解决:检查数据
print(f"数据类型:{adata.X.dtype}")
print(f"最小值:{adata.X.min()}")
print(f"最大值:{adata.X.max()}")
# 如果是负数或小数,可能已归一化

Q6: 如何添加自定义 QC 指标?

问题:我想计算某些特定基因的占比(如血红蛋白、细胞周期基因)。

解决方案

# 1. 定义基因列表
hemoglobin_genes = ['HBA1', 'HBA2', 'HBB', 'HBD', 'HBE1', 'HBG1', 'HBG2']
cell_cycle_genes = ['MKI67', 'PCNA', 'MCM5', 'STMN1', 'TOP2A']

# 2. 计算自定义指标
def calculate_gene_percentage(adata, gene_list, var_name='custom'):
    """计算特定基因列表的 UMI 百分比"""
    # 找到存在的基因
    genes_present = [g for g in gene_list if g in adata.var_names]

    if not genes_present:
        print(f"警告:{var_name} 基因未找到")
        adata.obs[f'pct_counts_{var_name}'] = 0
        return

    # 计算这些基因的总 UMI
    gene_mask = adata.var_names.isin(genes_present)
    custom_counts = adata[:, gene_mask].X.sum(axis=1).A1
    total_counts = adata.X.sum(axis=1).A1

    # 计算百分比
    percentage = (custom_counts / total_counts) * 100
    adata.obs[f'pct_counts_{var_name}'] = percentage

    print(f"{var_name} 基因数:{len(genes_present)}")
    print(f"平均占比:{percentage.mean():.2f}%")

# 3. 使用
calculate_gene_percentage(adata, hemoglobin_genes, 'hb')
calculate_gene_percentage(adata, cell_cycle_genes, 'cellcycle')

# 4. 查看
print(adata.obs[['pct_counts_hb', 'pct_counts_cellcycle']].describe())

6.3 过滤策略

Q7: 过滤后保留率过低(< 50%)

问题:应用 MAD 过滤后,超过一半的细胞被过滤掉。

可能原因

  1. 数据质量确实差
  2. MAD 倍数太严格
  3. 组织类型未考虑
  4. 存在技术批次

解决方案

# 1. 检查数据分布
print("=== QC 指标统计 ===")
print(adata.obs[['n_genes_by_counts', 'n_counts', 'pct_counts_mt']].describe())

# 2. 使用更宽松的阈值(5 MAD)
N_MADS = 5  # 增加到 5

# 3. 分别检查各指标的过滤率
def mad_filter(data, n_mads, direction='both'):
    median = np.median(data)
    mad = np.median(np.abs(data - median))
    if direction == 'lower':
        return data > (median - n_mads * mad)
    elif direction == 'upper':
        return data < (median + n_mads * mad)
    else:
        lower = median - n_mads * mad
        upper = median + n_mads * mad
        return (data > lower) & (data < upper)

for metric in ['n_genes_by_counts', 'n_counts', 'pct_counts_mt']:
    if metric == 'pct_counts_mt':
        filter_mask = mad_filter(adata.obs[metric], 3, 'upper')
    else:
        filter_mask = mad_filter(adata.obs[metric], 3, 'both')

    retention = filter_mask.sum() / len(filter_mask) * 100
    print(f"{metric}: {retention:.1f}% 保留")

# 4. 检查不同批次的保留率
if 'batch' in adata.obs.columns:
    for batch in adata.obs['batch'].unique():
        batch_cells = adata.obs['batch'] == batch
        retention = (filter_mask[batch_cells].sum() / batch_cells.sum() * 100)
        print(f"  {batch}: {retention:.1f}%")

# 5. 如果某个批次保留率特别低,考虑单独 QC 或丢弃

Q8: 如何处理双峰分布的数据?

问题:QC 指标呈现双峰分布,MAD 方法可能不适用。

示例

# 可视化
import matplotlib.pyplot as plt
plt.hist(adata.obs['n_genes_by_counts'], bins=100)
plt.show()

# 如果出现双峰,可能原因:
# 1. 混入不同细胞类型
# 2. 技术批次效应
# 3. 部分样本质量差

解决方案

# 方案 1: 分组过滤
if 'batch' in adata.obs.columns:
    # 对每个批次分别过滤
    adata_list = []
    for batch in adata.obs['batch'].unique():
        batch_adata = adata[adata.obs['batch'] == batch]
        # 对该批次应用 MAD 过滤
        # ...
        adata_list.append(batch_adata_filtered)

    # 合并
    adata_filtered = adata_list[0].concatenate(adata_list[1:], index_unique=None)

# 方案 2: 使用百分位数而非 MAD
lower = np.percentile(adata.obs['n_genes_by_counts'], 5)
upper = np.percentile(adata.obs['n_genes_by_counts'], 95)
filter_mask = (adata.obs['n_genes_by_counts'] > lower) & (adata.obs['n_genes_by_counts'] < upper)

# 方案 3: 基于建模的方法(如 GMM)
from sklearn.mixture import GaussianMixture

# 选择"高质量"峰
gmm = GaussianMixture(n_components=2, random_state=42)
gmm.fit(adata.obs[['n_genes_by_counts']])
labels = gmm.predict(adata.obs[['n_genes_by_counts']])

# 选择基因数较高的簇
high_quality_cluster = np.argmax(gmm.means_)
filter_mask = labels == high_quality_cluster

Q9: 过滤后某个细胞类型完全消失

问题:过滤后,预期的某个细胞群不见了。

原因:该细胞群的 QC 特征与"低质量"细胞相似。

解决方案

# 1. 检查过滤前该细胞群的 QC 特征
# 假设有细胞类型注释
if 'cell_type' in adata.obs.columns:
    for ct in adata.obs['cell_type'].unique():
        ct_cells = adata.obs['cell_type'] == ct
        print(f"\n{ct}:")
        print(f"  n_genes: {adata.obs[ct_cells]['n_genes_by_counts'].median():.0f}")
        print(f"  n_counts: {adata.obs[ct_cells]['n_counts'].median():.0f}")
        print(f"  mt%: {adata.obs[ct_cells]['pct_counts_mt'].median():.1f}%")

# 2. 调整过滤策略
# 如果某个细胞类型天生基因数低,不要使用固定的 n_genes 下限
# 如果某个细胞类型线粒体含量高,使用更高的 mt% 阈值

# 3. 基于细胞类型的分群过滤(如果有参考)
# 对不同细胞类型使用不同阈值
filter_mask = np.ones(adata.n_obs, dtype=bool)

for ct in adata.obs['cell_type'].unique():
    ct_cells = adata.obs['cell_type'] == ct
    ct_adata = adata[ct_cells]

    # 对该细胞类型计算 MAD
    median = np.median(ct_adata.obs['n_genes_by_counts'])
    mad = np.median(np.abs(ct_adata.obs['n_genes_by_counts'] - median))
    lower = median - 5 * mad  # 使用 5 MAD(宽松)

    ct_filter = ct_adata.obs['n_genes_by_counts'] > lower
    filter_mask[ct_cells] = ct_filter

6.4 双细胞检测

Q10: Scrublet 报错或结果不合理

问题:Scrublet 运行报错或双细胞率异常高(> 20%)。

解决方案

import scrublet as scr

# 错误 1: 数据类型问题
counts_matrix = adata.X.toarray() if hasattr(adata.X, 'toarray') else adata.X
scrub = scr.Scrublet(counts_matrix)

# 错误 2: 双细胞率异常高
# 原因:可能是阈值设置不当
scrub = scr.Scrublet(counts_matrix)
doublet_scores, predicted_doublets = scrub.scrub_doublets()

# 可视化分布
scrub.plot_doublet_score_histogram()

# 手动调整阈值
# 在双峰之间的谷处选择阈值
threshold = 0.25  # 根据直方图调整
predicted_doublets_manual = doublet_scores > threshold

print(f"自动阈值双细胞率:{predicted_doublets.mean():.1%}")
print(f"手动阈值双细胞率:{predicted_doublets_manual.mean():.1%}")

# 错误 3: 期望双细胞率设置不当
expected_doublet_rate = 0.06  # 根据实际加载调整
scrub = scr.Scrublet(counts_matrix, expected_doublet_rate=expected_doublet_rate)

Q11: 如何验证双细胞检测结果?

问题:如何确定检测到的双细胞是真实的?

验证方法

# 方法 1: 检查双细胞的基因表达特征
doublets = adata.obs['predicted_doublet']
adata_doublets = adata[doublets]

# 检查是否表达两个不同细胞类型的标志基因
# 例如:T 细胞(CD3D)+ B 细胞(MS4A1)
if 'CD3D' in adata.var_names and 'MS4A1' in adata.var_names:
    t_cell = adata_doublets[:, 'CD3D'].X > 0
    b_cell = adata_doublets[:, 'MS4A1'].X > 0
    both = (t_cell & b_cell).sum()
    print(f"同时表达 CD3D 和 MS4A1 的细胞:{both}")

# 方法 2: 检查 QC 指标
print("双细胞 QC 特征:")
print(adata_doublets.obs[['n_genes_by_counts', 'n_counts']].describe())
print("\n单细胞 QC 特征:")
print(adata[~doublets].obs[['n_genes_by_counts', 'n_counts']].describe())
# 双细胞应该有更高的 n_genes 和 n_counts

6.5 结果解释

Q12: 如何判断 QC 是否成功?

问题:过滤后如何验证质量改善?

验证清单

# 1. 基本统计
print("=== 过滤前后对比 ===")
print(f"细胞数:{adata.n_obs}{adata_filtered.n_obs}")
print(f"保留率:{adata_filtered.n_obs / adata.n_obs * 100:.1f}%")
print(f"基因数:{adata.n_vars}{adata_filtered.n_vars}")

# 2. QC 指标改善
print("\n=== MT% 改善 ===")
print(f"过滤前中位数:{adata.obs['pct_counts_mt'].median():.1f}%")
print(f"过滤后中位数:{adata_filtered.obs['pct_counts_mt'].median():.1f}%")

# 3. 可视化对比
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# 过滤前
axes[0, 0].violinplot([adata.obs['n_genes_by_counts']])
axes[0, 0].set_title('n_genes (过滤前)')
axes[0, 1].violinplot([adata.obs['n_counts']])
axes[0, 1].set_title('n_counts (过滤前)')
axes[0, 2].violinplot([adata.obs['pct_counts_mt']])
axes[0, 2].set_title('MT% (过滤前)')

# 过滤后
axes[1, 0].violinplot([adata_filtered.obs['n_genes_by_counts']])
axes[1, 0].set_title('n_genes (过滤后)')
axes[1, 1].violinplot([adata_filtered.obs['n_counts']])
axes[1, 1].set_title('n_counts (过滤后)')
axes[1, 2].violinplot([adata_filtered.obs['pct_counts_mt']])
axes[1, 2].set_title('MT% (过滤后)')

plt.tight_layout()
plt.show()

# 4. 快速聚类检查(可选)
sc.pp.pca(adata_filtered)
sc.pp.neighbors(adata_filtered)
sc.tl.leiden(adata_filtered, resolution=0.5)
sc.tl.umap(adata_filtered)
sc.pl.umap(adata_filtered, color='leiden')

# 期望:聚类清晰,无明显技术伪影

Q13: 保留率多少合适?

问题:过滤后保留多少细胞是合理的?

经验值

数据类型预期保留率说明
高质量 PBMC85-95%样本质量好
肿瘤组织75-90%异质性高,坏死多
胚胎组织85-95%细胞脆弱但整体均匀
低温处理样本60-80%应激反应明显
固定组织50-70%固定导致 RNA 损失

判断标准

  • 保留率 > 90%:可能过滤不足
  • 保留率 70-90%:合理范围
  • 保留率 50-70%:可接受,但需要验证
  • 保留率 < 50%:需要重新审视策略或数据质量
# 如果保留率过低,检查
retention = adata_filtered.n_obs / adata.n_obs * 100
if retention < 70:
    print("警告:保留率过低!")
    print("建议:")
    print("1. 增加 MAD 倍数到 5")
    print("2. 检查是否有技术批次")
    print("3. 考虑组织特异性调整")
    print("4. 验证数据质量")

6.6 工具与环境

Q14: Scanpy 版本不兼容

问题:代码报错或行为与教程不一致。

解决方案

# 1. 检查版本
python -c "import scanpy as sc; print(sc.__version__)"

# 2. 更新到最新稳定版
pip install --upgrade scanpy

# 3. 或安装特定版本(如果需要)
pip install scanpy==1.10.0

# 4. 完整的环境重建(推荐)
conda create -n scrna-qc python=3.10 -y
conda activate scrna-qc
pip install scanpy==1.10.0 anndata==0.10.0

Q15: 内存不足错误

问题:处理大数据集时内存溢出。

解决方案

# 1. 使用稀疏矩阵
from scipy import sparse
if not sparse.issparse(adata.X):
    adata.X = sparse.csr_matrix(adata.X)

# 2. 分批处理
def batch_qc(adata, batch_size=10000):
    """分批计算 QC 指标"""
    results = []
    for i in range(0, adata.n_obs, batch_size):
        batch = adata[i:i+batch_size]
        sc.pp.calculate_qc_metrics(batch, qc_vars=['mt'], inplace=True)
        results.append(batch.obs.copy())

    import pandas as pd
    adata.obs = pd.concat(results)

# 3. 删除中间变量
import gc
del adata_large  # 删除大对象
gc.collect()  # 强制垃圾回收

# 4. 使用 Dask(适合超大数据)
# import dask.array as da
# adata.X = da.from_array(adata.X, chunks=(10000, 1000))

6.7 进阶问题

Q16: 如何处理多模态数据(如 CITE-seq)?

问题:我有 RNA + 蛋白质数据,如何 QC?

解决方案

# CITE-seq 数据有两个矩阵
# RNA: adata.X
# Protein: adata.obsm['protein_counts']

# 1. RNA QC(标准流程)
adata.var['mt'] = adata.var_names.str.startswith('MT-')
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True)

# 2. 蛋白质 QC
protein_counts = adata.obsm['protein_counts']
adata.obs['n_proteins'] = (protein_counts > 0).sum(axis=1)
adata.obs['total_protein_counts'] = protein_counts.sum(axis=1)

# 3. 联合过滤
# 基于 RNA 和蛋白质指标
filter_mask = (
    mad_filter(adata.obs['n_genes_by_counts'], 3, 'lower') &
    mad_filter(adata.obs['n_proteins'], 3, 'lower')  # 蛋白质指标
)

adata_filtered = adata[filter_mask].copy()

# 4. 可视化
import matplotlib.pyplot as plt
plt.scatter(
    adata.obs['n_genes_by_counts'],
    adata.obs['n_proteins'],
    alpha=0.5
)
plt.xlabel('n_genes')
plt.ylabel('n_proteins')
plt.show()

Q17: 如何自动化整个 QC 流程?

问题:需要处理多个数据集,希望自动化。

完整脚本

#!/usr/bin/env python3
"""自动化单细胞 QC 脚本"""

import scanpy as sc
import numpy as np
import sys

def mad_filter(data, n_mads=3, direction='both'):
    """MAD 过滤"""
    median = np.median(data)
    mad = np.median(np.abs(data - median))
    if direction == 'lower':
        return data > (median - n_mads * mad)
    elif direction == 'upper':
        return data < (median + n_mads * mad)
    else:
        lower = median - n_mads * mad
        upper = median + n_mads * mad
        return (data > lower) & (data < upper)

def auto_qc(input_file, output_file, species='human', n_mads=3):
    """自动化 QC"""
    print(f"=== QC: {input_file} ===")

    # 1. 加载
    adata = sc.read_h5ad(input_file)
    print(f"加载:{adata.n_obs} 细胞 × {adata.n_vars} 基因")

    # 2. 识别线粒体基因
    if species == 'human':
        adata.var['mt'] = adata.var_names.str.startswith('MT-')
    elif species == 'mouse':
        adata.var['mt'] = adata.var_names.str.startswith('mt-')
    else:
        adata.var['mt'] = adata.var_names.str.contains('mt-', case=False)

    # 3. 计算 QC 指标
    sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True)

    # 4. 过滤
    filter_mask = (
        mad_filter(adata.obs['n_genes_by_counts'], n_mads, 'lower') &
        mad_filter(adata.obs['n_genes_by_counts'], n_mads, 'upper') &
        mad_filter(adata.obs['n_counts'], n_mads, 'lower') &
        mad_filter(adata.obs['n_counts'], n_mads, 'upper') &
        mad_filter(adata.obs['pct_counts_mt'], n_mads, 'upper')
    )

    # 5. 应用过滤
    adata_filtered = adata[filter_mask].copy()
    sc.pp.filter_genes(adata_filtered, min_cells=10)

    # 6. 保存
    adata_filtered.write(output_file)

    retention = filter_mask.sum() / len(filter_mask) * 100
    print(f"保存:{adata_filtered.n_obs} 细胞 ({retention:.1f}%)")
    print(f"输出:{output_file}\n")

if __name__ == '__main__':
    # 命令行使用
    if len(sys.argv) < 3:
        print("用法:python auto_qc.py input.h5ad output.h5ad [species] [n_mads]")
        sys.exit(1)

    input_file = sys.argv[1]
    output_file = sys.argv[2]
    species = sys.argv[3] if len(sys.argv) > 3 else 'human'
    n_mads = int(sys.argv[4]) if len(sys.argv) > 4 else 3

    auto_qc(input_file, output_file, species, n_mads)

# 使用:
# python auto_qc.py data_raw.h5ad data_qc.h5ad human 3

教程总结

恭喜你完成了单细胞 RNA-seq 质量控制完整教程!现在你应该能够:

  • ✅ 理解 QC 的重要性和核心指标
  • ✅ 独立完成单细胞数据的 QC 分析
  • ✅ 应用 MAD 方法进行自适应过滤
  • ✅ 处理不同物种和组织类型的 QC 需求
  • ✅ 排查常见问题和优化 QC 流程

下一步学习建议

  1. 实践项目:用自己的数据集完成 QC 分析
  2. 深入特定领域:学习你研究组织的特异性 QC 策略
  3. 学习下游分析:聚类、细胞类型注释、轨迹分析
  4. 探索高级工具:双细胞检测、批次校正、数据整合

推荐资源

  • Scanpy 官方文档
  • scverse QC Guidelines
  • Single-Cell Best Practices

祝你在单细胞分析的旅程中收获满满!