第4章 基因数的含义:检测到的基因数量如何反映细胞质量
好,我们继续往前走。前面聊了UMI是怎么捕获和计数RNA分子的,这节我重点讲另一个关键指标——基因数。说白了,检测到的基因数量就是在一个细胞中实际测到了多少种不同的基因。
你想想看,一个正常哺乳动物细胞大约能表达2万多种基因中的4000到6000种。这个数我记了很多年——因为一旦你的数据里出现了明显偏离这个范围的值,那大概率是样本里有猫腻。
我刚开始做单细胞分析时吃过一个亏。那时候我拿到一批数据,基因数分布看起来特别漂亮,中间高两头低,感觉没有任何问题。结果老板让我手工检查几个极端高值的barcode,一查发现全是双细胞融合的伪迹。嗯,从那以后我再也不只看分布形状了,开始自己手写过滤逻辑。
4000-6000到底怎么来的?
这里需要铺垫一下基础知识。我们先看正常细胞的特征。
一个典型的哺乳动物细胞:
- 表达的基因种类通常在4000-6000之间(10x Genomics平台常见的结果)
- 如果用Smart-seq2这类全长测序,这个数会更高,因为覆盖更全
- 细胞类型越复杂,基因数范围越宽——比如T细胞和神经元就不一样
| 细胞类型 | 典型基因数范围(10x) | 备注 |
|---|---|---|
| PBMC淋巴细胞 | 3000-5000 | T细胞、NK细胞普遍偏低 |
| PBMC单核细胞 | 4000-7000 | 比淋巴细胞高,活性也强 |
| 肝细胞 | 5000-8000 | 代谢活跃,基因表达谱更广 |
| 神经元 | 6000-10000 | 复杂细胞类型,基因数自然高 |
为什么单核细胞基因数能到7000?因为它的功能更多样,吞噬、抗原呈递、信号转导都需要大量基因参与。你想想看,如果一个单核细胞样本里基因数只有1500,那八成是有问题。
双细胞:基因数为什么虚高?
双细胞是最常见的污染来源。一个液滴里包裹了两个细胞,很自然的,检测到的基因总数就会接近两个独立细胞的总和。
但这里有个细节你可能没注意过:双细胞的基因数并不是简单的1+1=2。为什么?
因为两个细胞如果来自同一种系,它们的表达谱可能相似度很高,部分共表达的基因只会被计一次。所以双细胞的基因数通常会落在单个细胞的1.3到1.8倍之间,而不是整两倍。
# 举例:假设一个T细胞基因数是3000
# 那么:
# T细胞 + T细胞(同种)→ 约4000-5000
# T细胞 + 单核细胞(异种)→ 约6000-9000
# 后者更容易被识别出来,因为基因数跳得高
我遇到过一个项目,样品始终有一只细胞异常高表达。排查了很久,最后发现是那个单细胞悬液里有细胞团块没完全分散开。细胞团块造成的多细胞效果很隐蔽,基因数分布只略高一点点,但后续聚类完全乱套。
空液滴:基因数极低的常见原因
空液滴的问题也很有意思。空液滴本质上是一个barcode结合了一些游离的RNA片段(来自细胞裂解释放的背景RNA),但没有包裹完整的细胞。
空液滴在基因数上的表现:
- 基因数通常低于500
- UMI总数也极低,一般不超过1000
- 另一个特征是——表达谱往往是随机的,不成系统
为什么会随机?因为空液滴里的RNA来自环境中大量细胞的碎片,拼凑出来的基因组合没有任何生物学逻辑。你想想看,一个活细胞哪会同时表达线粒体基因和核糖体基因却几乎不表达其他关键通路基因?但空液滴就能做到。
我记得有一次合作者给我传来数据,说细胞数量明显偏多,而且全是些奇怪的细胞类型。我一查空液滴比例,发现竟然有20%的barcode特征和空液滴一模一样。后来重新设定了阈值,把那些基因数低于800的barcode全部过滤掉,聚类结果才恢复正常。
死亡细胞:基因数也下降但另有特征
死亡细胞和空液滴的区别在于:死亡细胞确实曾是一个完整的细胞,只是它的RNA已经降解得差不多了。
死亡细胞在基因数上的特征:
- 基因数显著下降——可能从5000掉到1500甚至更低
- 但是与此同时,线粒体基因占比升高
- UMI总数下降,但不像空液滴那样少得离谱
这里有个陷阱:神经元本身的基因数就高(6000-10000)。如果某个神经元因为操作过程中的损伤导致基因数掉到3000,这个数值在PBMC的分析流程里可能被误判为正常值。实际上它已经是个需要剔除的死亡细胞了。
我以前建议在过滤时采用多模态阈值:不只看基因数绝对值,还要结合线粒体比例和UMI总数。
# 我常用的过滤条件(参考值)
filter_cells = (
(adata.obs['n_genes_by_counts'] > 500) &
(adata.obs['n_genes_by_counts'] < 7000) &
(adata.obs['pct_counts_mt'] < 20) &
(adata.obs['total_counts'] > 1000)
)
看到了吗?我把基因数的上下限都设了。上限7000是为了排除双细胞和多细胞,下限500是为了排除空液滴和严重受损的细胞。另外线粒体比例和UMI总数是加固防线。
实操建议:阈值怎么设?
我见过很多人把基因数阈值设成一个固定值(比如低于500就删除)。这种做法太死板了。
好的做法是基于数据分布动态设定:
# Python示例:基于3倍MAD(中位数绝对偏差)设定
import numpy as np
gene_counts = adata.obs['n_genes_by_counts']
median = np.median(gene_counts)
mad = np.median(np.abs(gene_counts - median))
lower = median - 3 * mad
upper = median + 3 * mad
print(f"自动阈值范围:{lower:.0f} - {upper:.0f}")
核心思路是:让数据自己决定阈值,而不是拍脑袋定个数。我觉得这样更稳妥。