4. 基因检测效率:每个 spot 检测到的基因数量与基因饱和度曲线
聊完了测序质量和比对,咱们来看看更实在的东西——基因检测效率。说白了,就是你花了那么多钱做实验,每个 spot 到底检测到了多少基因?数据饱和了没有?
我个人习惯把这两个指标放在一起看,因为它们就像一枚硬币的两面。一面是“量”——每个 spot 能抓到多少种基因;另一面是“质”——测序深度增加了,还能不能挖出新基因来。
4.1 每个 spot 检测到的基因数量
这是最直接的 QC 指标。通常我们用 n_genes_by_counts 或者 total_genes 来表示。这里的核心问题是:多少算正常?多少算异常?
我建议先看中位数,别被平均值骗了。曾经我在一个脑组织的 Visium 数据里,平均值看着有 2500,但一看分布,发现有一半的 spot 低于 1500。这就是典型的分布偏倚。
核心经验:对于 10x Visium 平台,组织切片的 spot 中位数基因数通常在 1000-3000 之间。低于 500 的 spot,我通常会标记为低质量。
具体怎么算?看代码:
import scanpy as sc
import numpy as np
# 加载数据
adata = sc.read_visium('path/to/spatial/')
# 计算每个 spot 的基因数
sc.pp.calculate_qc_metrics(adata, expr_type='counts')
# 查看分布
median_genes = np.median(adata.obs['n_genes_by_counts'])
print(f'Spot 中位基因数: {median_genes}')
# 标记低质量 spot
adata.obs['low_genes'] = adata.obs['n_genes_by_counts'] < 500
print(f'低质量 spot 数: {adata.obs["low_genes"].sum()}')
这里有个坑要注意。有些 spot 基因数特别高,比如超过 6000,不一定就是好事。为什么?因为可能是组织边缘或褶皱区域捕获了太多背景 RNA。嗯,我处理过一个淋巴瘤样本,边缘区域基因数暴增到 8000 多,但那些基因大部分是核糖体蛋白和线粒体基因——说白了,技术噪音。
避坑指南:基因数过高(超过整体中位数的 3 倍或更多)的 spot,建议先检查组织形态学图像。如果对应 H&E 图像上该区域有明显组织扭曲或边缘效应,直接过滤掉更安全。
4.2 基因饱和度曲线
基因饱和度曲线,听起来挺玄乎,其实没那么复杂。它回答的就是一个问题:再测更多 reads,我还能不能发现新基因?
曲线的横轴是测序深度(每个 spot 的总 UMI 数),纵轴是检测到的基因数。曲线陡峭上升,说明还没饱和;曲线开始变平,说明快饱和了。
| 曲线形态 | 含义 | 我的建议 |
|---|---|---|
| 持续上升,斜率 > 0.3 | 远未饱和,深度不足 | 考虑加测,或接受低检测率 |
| 中度弯曲,斜率 0.1-0.3 | 部分饱和,尚可接受 | 正常范围,继续分析 |
| 接近平坦,斜率 < 0.1 | 接近饱和,深度充足 | 完美,可以放心做差异分析 |
绘制饱和度曲线其实很简单,用 Python 就能搞定:
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
# 从 adata 中提取 UMI 数和基因数
umi_per_spot = adata.obs['total_counts']
genes_per_spot = adata.obs['n_genes_by_counts']
# 绘制散点图 + 拟合曲线
plt.figure(figsize=(8, 6))
sns.scatterplot(x=umi_per_spot, y=genes_per_spot, s=5, alpha=0.5)
# 分箱计算均值用于拟合曲线
bins = np.linspace(umi_per_spot.min(), umi_per_spot.max(), 50)
bin_means = []
for i in range(len(bins)-1):
mask = (umi_per_spot >= bins[i]) & (umi_per_spot < bins[i+1])
if mask.sum() > 0:
bin_means.append(genes_per_spot[mask].mean())
else:
bin_means.append(np.nan)
plt.plot(bins[:-1], bin_means, color='red', linewidth=2, label='饱和度曲线')
plt.xlabel('每个 spot 的总 UMI 数')
plt.ylabel('检测到的基因数')
plt.title('基因饱和度曲线')
plt.legend()
plt.show()
我曾经在一个新手的数据里看到,饱和度曲线在中低深度区域就变得很平——这其实是 library complexity 太差。为什么?因为建库时 PCR 扩增偏好性导致只有少数高表达基因被放大,其余的基因压根就检测不到。这就是个典型的实验质量问题,不是测序深度能补救的。
实用技巧:计算出每个 spot 的“每百万 UMI 新增基因数”(n_genes_per_10k_umi),也就是基因检测效率的归一化值。这个值在不同样本间可比性更好。公式很简单:n_genes_per_10k_umi = n_genes / total_counts * 10000。如果这个值低于 100,我建议提高警惕。
4.3 综合判断:这两个指标怎么看?
单独看一个指标容易误判。举个例子:一个 spot 基因数很高,但饱和度曲线也很陡,说明深度还远远不够,高基因数只是因为组织本身基因表达丰富。这时候盲目过滤反而会丢掉好数据。
反过来,基因数低 + 曲线平坦,这才真正值得警惕。说白了,就是既没检测到足够基因,深度增加也救不回来。这种 spot 我建议直接标记排除。
下面这张图可以帮你理清判断逻辑:
4.4 实际项目中的阈值设置
你可能会问,阈值到底设多少?说实话,没有放之四海而皆准的数值。不过我可以分享几个我在不同平台上的经验值。
对于 10x Visium v2 试剂盒:
- 理想:中位基因数 > 2000,饱和度曲线在 15000 UMI 后趋于平坦
- 可接受:中位基因数 1000-2000,曲线仍在缓慢上升
- 低质量:中位基因数 < 500,或曲线在低深度就饱和
对于 Slide-seq v2:
- 由于 bead 尺寸小,每个 spot 的中位基因数通常在 500-1500 之间
- 饱和度曲线通常更平坦,因为深度普遍偏低
这里要特别提醒:不要把不同平台的阈值搬来直接用。曾经有个团队用 Visium 的阈值去过滤 Slide-seq 数据,结果滤掉了 80% 的 spot,差点整个实验报废。
个人习惯:我会先用 R 包 DropletUtils 里的 brimq 方法做一次无偏估计,看数据本身的“自然阈值”在哪里。然后再结合生物学知识微调。这样比硬性规定一个数字要科学得多。
好了,基因检测效率这块的核心内容就这么多了。记住一句话:基因数看分布,饱和度看趋势,两者结合才能做出靠谱的过滤决策。