单细胞 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。这三个文件拼起来,就是一张完整的表达矩阵。
下面这张图把这些关系梳理了一遍:
实际加载时,R 中的 Read10X 和 Python 中的 scanpy.read_10x_mtx 会自动完成这三者的拼接,返回一个稀疏矩阵。你不需要手动去拼,但理解背后的对应关系能帮你定位许多数据问题。
- 表达矩阵是基因(行)× 细胞(列)的稀疏计数表。
- 细胞条形码是区分单个细胞的唯一标签,白名单过滤很重要。
- 基因信息包含 ID、symbol、类型,注意去重和版本一致性。
- 三件套(mtx + barcodes + features)必须同时存在、行列数对齐。
分析之前,我强烈建议你打印一下这三个文件的头几行,亲自验证一下对应关系。我曾经碰到过 barcodes.tsv 里多了一个换行符、或者 features.tsv 里混入了线粒体基因重复——这些细节如果不检查,后面整个质控报告都是错的。