UMI计数流程:从原始数据到计数矩阵

好,咱们正式进入UMI数据分析的核心。这一章我要把整个流程拆开,讲清楚数据是怎么从原始FASTQ文件,一步步变成最终那个计数矩阵的。

你想想看,如果手里拿着一堆测序数据,里面混着UMI序列、细胞条形码、还有实际的转录本序列,怎么把它们理清楚?这就是我们接下来要解决的问题。

原始数据长什么样?

先看看手里有什么。典型的单细胞UMI数据,一条read的结构大概是这样:

@read_id
[细胞条形码(16bp)] [UMI(10bp)] [polyT] [转录本序列...]
+
[质量分数...]

嗯,这里要注意,不同平台和试剂盒的UMI长度不一样。10x Genomics常用10bp,BD Rhapsody用的是8bp,还有些定制方案会用12bp。我习惯在拿到数据的第一时间,先看一眼README或者试剂说明,确认UMI到底占多少碱基。

第一步:拆分与预处理

流程的第一步,就是把这些混在一起的信息拆开。说白了,就是把reads按细胞条形码分组,同时把UMI序列单独拎出来。

具体步骤:

  1. 提取细胞条形码:从read头部固定位置截取
  2. 提取UMI序列:紧跟着条形码的固定长度
  3. 过滤低质量reads:条形码或UMI中如果有太多N碱基,直接扔掉
  4. 去除接头和polyT:避免污染转录本比对

我在项目中遇到过一批数据,UMI序列里莫名其妙出现了连续三个N。最开始没在意,后来发现这些reads的计数异常偏高。排查了半天才发现是测序仪在UMI区域出了错。从那以后,我每次都会加一个UMI质量过滤的步骤:

# 过滤UMI中包含超过1个N的reads
if umi_sequence.count('N') > 1:
    discard_read()
注意:质量过滤不是越严越好。UMI本身有纠错能力,太激进的过滤会丢失真实数据。我建议先跑一个质量分布图,根据实际情况设置阈值。

第二步:比对与基因分配

拆出转录本序列后,下一步就是比对到参考基因组。这一步其实和普通RNA-seq没有本质区别,但有个坑——UMI数据往往包含大量PCR重复,比对软件如果处理不当,会浪费很多计算资源。

我常用的是STAR或HISAT2,不过针对UMI数据,我更推荐使用专门优化的比对器:

STAR --genomeDir /ref/hg38 \
     --readFilesIn sample_R1.fastq.gz \
     --outSAMtype BAM SortedByCoordinate \
     --outFilterMultimapNmax 1 \
     --outSAMattributes NH HI nM AS

比对完成后,需要把每个read关联到具体的基因。这一步通常基于参考基因组的GTF注释文件。如果比对位置落在多个基因的重叠区域,我个人的处理原则是:只保留唯一匹配。虽然会损失一些数据,但能大幅降低假阳性。

第三步:UMI校正

终于到了UMI分析最核心的部分。为什么需要校正?我当年刚开始做这个的时候也纳闷:UMI不就是为了区分真实分子和PCR重复吗?怎么还需要校正?

原因其实很简单:测序会出错。UMI序列本身也会被测错,导致同一个原始分子被识别成不同的UMI。反过来,不同分子也可能因为测序错误变成同一个UMI序列。

常见的校正方法有下面几种:

方法 原理 适用场景
完全相同 只有序列完全一致的UMI才合并 测序质量高,UMI长度短(<8bp)
汉明距离 最多允许1-2个碱基差异 最常用,平衡灵敏度与特异性
编辑距离 允许插入/删除,不仅限于替换 测序质量一般,有indel风险
邻近图聚类 基于序列相似性构建网络,自动聚类 高深度测序,适合自动优化

说说我的习惯。对于10x Genomics的10bp UMI,我一般先用汉明距离=1。如果发现UMI多样性异常低(也就是校正后分子数远小于UMI数),我会降到距离=0,看看是不是过度合并了。

一个典型的校正代码片段:

def correct_umis(umi_list, max_distance=1):
    """基于汉明距离合并相似的UMI"""
    # 按丰度排序,丰度高的优先保留
    sorted_umis = Counter(umi_list).most_common()
    corrected = {}
    
    for umi, count in sorted_umis:
        # 如果这个UMI已经被合并,跳过
        found = False
        for ref_umi in corrected:
            if hamming_distance(umi, ref_umi) <= max_distance:
                corrected[ref_umi] += count
                found = True
                break
        if not found:
            corrected[umi] = count
    
    return corrected
关键点:校正的顺序很重要。我通常先按丰度排序,从出现次数最多的UMI开始。因为高丰度的UMI更可能是真实分子,这样能避免把真实的UMI合并到错误的簇里。

第四步:生成计数矩阵

校正完之后,就可以统计每个细胞每个基因的UMI计数了。这就是最后要的东西——一个细胞×基因的矩阵。

格式大概长这样:

gene     cell_1  cell_2  cell_3  cell_4
GAPDH    125     89      203     45
ACTB     78      156     34      212
TP53     12      3       8       0
...       ...     ...     ...     ...

生成这个矩阵的工具很多。我最熟悉的是用Python的pandas来处理:

import pandas as pd

# 假设我们已经有了一个字典:{(cell_barcode, gene): corrected_umi_count}
count_data = []
for (cell, gene), count in corrected_counts.items():
    count_data.append({'cell': cell, 'gene': gene, 'count': count})

umi_matrix = pd.DataFrame(count_data).pivot_table(
    index='gene', columns='cell', values='count', fill_value=0
)

但这里有一个隐藏问题。并不是所有细胞条形码都对应真正的细胞。有些是空的液滴,有些是死细胞。这就涉及到下一章要讲的内容——细胞过滤。嗯,你先记住这个节点就好。

常见错误类型

做了这么多年UMI分析,我踩过的坑真不少。整理一下最常见的几种:

  • UMI长度识别错误:不同试剂盒的UMI位置和长度不同,搞错了直接导致全盘崩溃。我建议建立自己的备忘清单,每次开工前先核对。
  • 校正阈值过松:我曾经把汉明距离设成2,结果把大量不同真实分子的UMI合到一起,导致基因表达量被严重低估。后来改成距离=1才正常。
  • 忽略PCR偏差:如果文库扩增轮次太多,UMI多样性会被耗尽,这时候校正效果会大打折扣。需要结合扩增深度来调整策略。
  • 细胞条形码污染:相邻液滴的细胞条形码有可能互换。我见过一个项目,5%的UMI分配到了错误的细胞里,直接影响了后续的聚类分析。
避坑指南:我曾经遇到一个案例,所有细胞都高表达线粒体基因。一开始以为是生物学现象,后来发现是细胞膜破裂,RNA泄露导致背景噪声。那个项目的UMI校正完全失效了。从那以后,我每次都会检查线粒体基因的比例,超过20%的基本可以放弃。