单细胞 RNA-seq 数据基础:表达矩阵、细胞条形码、基因

做单细胞分析,第一件事就是搞懂数据长什么样。你拿到的不是一张图,也不是一段序列,而是一堆数字。这堆数字背后藏着三个核心要素:表达矩阵、细胞条形码和基因。搞明白这三样,后面所有分析才站得住脚。

我个人刚入行时,有个误区——以为单细胞数据跟 bulk RNA-seq 差不多,只是样本多了一点。结果第一次拿到 10x 数据,打开那个 mtx 文件,傻眼了。行和列全对不上,UMI 计数怎么跟基因名匹配?后来才意识到,这三者互为索引,少一个都无法还原生物学信息。

1. 表达矩阵:你以为就是个表格?

表达矩阵说白了,是基因 × 细胞的二维计数表。行是基因,列是细胞,每个格子里的数字代表该基因在这个细胞中捕获到的转录本数量(通常用 UMI 计数表示)。

但有个坑:这个矩阵非常稀疏。一个典型的 10x 实验,测 5000 个细胞,检测 20000 个基因,理论上矩阵大小是 5000×20000 = 1 亿个元素,但其中 90% 以上是 0。因为每个细胞只表达几千个基因,其他基因压根没检测到。

个人习惯:每次拿到数据,我第一件事先用 dim(counts) 看矩阵维度,再算一下空间占比——object.size(counts) 和 length(which(counts > 0)) / prod(dim(counts))。如果稀疏度低于 85%,我得怀疑是不是做了多余过滤。

表达矩阵常见的三种存储格式:

格式 说明 典型文件名
dense matrix 普通二维矩阵,所有元素全存储 matrix.tsv
sparse Matrix (Market Exchange Format) 只存储非零元素的行、列、值 matrix.mtx
HDF5 (h5) 分层数据格式,支持大型稀疏矩阵 raw_feature_bc_matrix.h5

实际工作中,10x 数据默认输出的是 mtx + barcodes + features 三件套。下面是我经常用来读入数据的代码片段:

# 用 Seurat 读 10x 标准输出
library(Seurat)
counts <- Read10X(data.dir = "./filtered_feature_bc_matrix/")
dim(counts)
# 输出: 基因数 × 细胞数,例如 33694 × 5438

# 查看前几个基因在第一个细胞的表达量
counts[1:5, 1:3]

我曾经遇到过一个情况:同事自己用 read.table 读 mtx 文件,结果把稀疏矩阵读成密集矩阵,内存直接爆了。嗯,读单细胞数据,永远记得用专门函数:Read10X、ReadMtx、dropletUtils::read10xCounts。

2. 细胞条形码:每个细胞的身份证

细胞条形码(Cell Barcode)是一段短核苷酸序列(通常是 16 bp),在文库构建时通过“标记-捕获”流程粘到每个细胞的 mRNA 上。你想想看,一个测序芯片上同时跑几万个细胞,怎么区分哪些 reads 来自哪个细胞?就靠这条条形码。

10x 的 barcode 设计有白名单机制——不是所有测到的序列都算有效细胞。软件会跟预定义的 737K 个已知 barcode 序列比对,只有完全匹配或单碱基容差的才被保留。这就是为什么 filtered_feature_bc_matrix 目录下的 barcodes.tsv 一般只有几千行,而 raw 目录下有几万行。

避坑指南:千万不要直接拿 raw 目录的数据做分析。那些是未过滤的空滴(empty droplets),大部分是背景 RNA 污染。我曾经有个学生图省事,直接用 raw 数据跑聚类,结果画出来的 UMAP 全是技术噪音,根本看不到细胞分群。

barcodes.tsv 文件长这样:

AAACCCAAGAAACTCA-1
AAACCCACAGTTAGCA-1
AAACCCAGTATCGATG-1
AAACCCATCGTACGGA-1
...

每行一个条形码序列,末尾的 -1 是样品编号(如果有多重测序的话会变成 -2、-3)。在分析时,这些条形码就是列名,跟表达矩阵的列一一对应。

3. 基因:不只是名字

基因层面,我们关心的是三样东西:基因 ID(Ensembl ID 或 Entrez ID)、基因符号(symbol)、以及基因类型(蛋白编码、lncRNA、假基因等)。

10x 的 features.tsv(或者 h5 里的 feature 数据集)包含三列:

列 示例 用途
Gene ID ENSG00000141510 唯一标识,用于跨数据库匹配
Gene Symbol TP53 人类可读名称,用于可视化
Feature Type Gene Expression 备选:Antibody Capture, CRISPR Guide 等

我遇到过最头疼的问题:基因 symbol 有重复。比如有些已废弃的 symbol 跟正式 symbol 重名,或者同一基因在不同版本的注释里有不同写法。所以我在做分析前,一定会先做一步去重:

# 检查是否有重复 symbol
table(duplicated(rownames(counts)))
# 如果有重复,我习惯用以下方法合并(按最大表达值保留)
library(Matrix)
counts <- counts[!duplicated(rownames(counts)), ]
# 或者用 aggregate 按 symbol 求和

4. 三者如何拼成一张分析表?

你想想看:测序仪吐出来的是 FASTQ 文件,里边包含 read 序列、barcode 序列、UMI 序列。经过 Cell Ranger 之类的流程后,它把 reads 拆分到每个 barcode,再比对到基因组,统计每个基因在每个细胞里有多少个唯一的 UMI。最后输出三个文件:barcodes.tsv、features.tsv、matrix.mtx。这三个文件拼起来,就是一张完整的表达矩阵。

下面这张图把这些关系梳理了一遍:

单细胞 RNA-seq 数据三大核心要素 FASTQ 原始数据 读取序列 + Barcode + UMI barcodes.tsv 细胞条形码列表 matrix.mtx 稀疏表达矩阵(行/列/值) features.tsv 基因 ID / Symbol / 类型 完整表达矩阵 基因 × 细胞,稀疏计数矩阵 三个文件通过行号/列名一一对应,缺一不可

实际加载时,R 中的 Read10X 和 Python 中的 scanpy.read_10x_mtx 会自动完成这三者的拼接,返回一个稀疏矩阵。你不需要手动去拼,但理解背后的对应关系能帮你定位许多数据问题。

关键总结:
  • 表达矩阵是基因(行)× 细胞(列)的稀疏计数表。
  • 细胞条形码是区分单个细胞的唯一标签,白名单过滤很重要。
  • 基因信息包含 ID、symbol、类型,注意去重和版本一致性。
  • 三件套(mtx + barcodes + features)必须同时存在、行列数对齐。

分析之前,我强烈建议你打印一下这三个文件的头几行,亲自验证一下对应关系。我曾经碰到过 barcodes.tsv 里多了一个换行符、或者 features.tsv 里混入了线粒体基因重复——这些细节如果不检查,后面整个质控报告都是错的。