质量分数编码体系:Phred质量分数、ASCII偏移量与编码辨别
做生物信息分析的人,每天跟FASTQ打交道。但很多人只盯着序列看,忽略了那一串质量字符里的门道。说实话,质量分数才是决定比对、变异检测可信度的关键。今天我就把这套编码体系掰开揉碎讲清楚。
Phred质量分数:从测序仪到置信度
Phred质量分数,最早是Phred软件用来评估碱基识别准确率的。它的核心公式很简单:
Q = -10 × log10(P)
其中P是碱基识别错误的概率。举个例子,Q=30代表错误概率0.001,也就是千分之一。Q=20,错误概率百分之一。你想想看,Q值越高,测序越靠谱。
我在一次人类全外显子组分析项目中,发现测序公司给的Q值普遍偏低。后来查记录,原来是测序仪跑到了后期,化学试剂消耗导致质量下降。当时我建议客户重新补测了部分文库,才避免了大量假阳性变异。这个经历让我养成了习惯:拿到FASTQ第一件事就是看质量分数分布。
| Q值 | 错误概率 | 碱基准确率 |
|---|---|---|
| 10 | 1/10 = 0.1 | 90% |
| 20 | 1/100 = 0.01 | 99% |
| 30 | 1/1000 = 0.001 | 99.9% |
| 40 | 1/10000 = 0.0001 | 99.99% |
ASCII偏移量:两个不同的阵营
问题来了:Q值是个整数,怎么存进FASTQ文件?答案是用ASCII字符。不同机构用了不同的偏移量。这里要记住两大阵营:
- Sanger / Illumina 1.8+:ASCII偏移量33,Q范围0–93,字符范围从'!'到'~'
- Solexa / early Illumina:ASCII偏移量64,Q范围3–62(实际常用),字符范围从'@'到'~'
为什么会偏移?早年Solexa自己定义了一套,后来Illumina收购后统一到Sanger标准。但很多老数据集流传至今,如果不搞清楚,直接把质量分数当同样标准用,分析结果会一塌糊涂。
如何辨别编码标准?三个方法
辨别方法不复杂,但需要经验。我总结出三个思路:
- 观察字符范围:查看质量字符串里有没有出现在32以下(即ASCII码小于'!')或42以上(即大头针符号'*'等)。如果出现'!'(ASCII 33),说明是Sanger编码。如果最低字符是'@'(ASCII 64),那就是Illumina 1.8+之前的编码。
- 计算Q值分布:将ASCII字符转成数字后减去偏移量,看看Q值是否落在合理区间(0–41常见)。如果减64后出现大量负值,则偏移量应为33。
- 查看头信息:有些FASTQ头里会写明平台版本。例如“@SRR001666.1 1 1101 length=36”这种格式不告诉你编码。但Illumina Casava 1.8以后的版本会在头中包含“instrument:run:flowcell:lane:...”并用空格分隔,且质量分数采用Sanger标准。
说实话,最稳妥的还是写个小脚本自动检测。下面是我平时用的检测函数:
def guess_quality_encoding(qual_str):
"""根据一条质量字符串推断偏移量"""
ascii_codes = [ord(ch) for ch in qual_str]
min_code = min(ascii_codes)
max_code = max(ascii_codes)
if min_code >= 33 and max_code <= 74:
# Sanger + Illumina 1.8+
return 33
elif min_code >= 59 and max_code <= 104:
# Solexa / Illumina 1.3-1.7
return 64
else:
# 无法确定,返回None
return None
使用的时候,从FASTQ前1000条序列中各取一条质量字符串,统计最小值做判断。记住,不能只看一条,因为测序末位质量低可能导致字符接近'!',容易误判。
实战:用Python批量检测FASTQ编码
我写过一个比较完整的检测脚本,从文件里随机抽样判断。这里给出核心部分:
import gzip
def detect_fastq_encoding(filepath):
# 判断是否gzip
open_func = gzip.open if filepath.endswith('.gz') else open
with open_func(filepath, 'rt') as f:
line_count = 0
min_codes = []
for line in f:
line_count += 1
# FASTQ结构:@head, seq, +, qual
if line_count % 4 == 0: # 质量行
qual = line.strip()
codes = [ord(c) for c in qual]
min_codes.append(min(codes))
if line_count >= 1000:
break
overall_min = min(min_codes)
if overall_min >= 33 and overall_min < 59:
return 33
elif overall_min >= 59:
return 64
else:
return None
这个脚本只统计前250条序列(1000行÷4 = 250条),效果足够。我曾在一个公开数据集上跑它,发现最小字符是'B'(ASCII 66),果断判断64偏移,后续分析一切正常。
两个编码标准的详细对比
| 特性 | Sanger / Illumina 1.8+ | Solexa / Illumina 1.3–1.7 |
|---|---|---|
| ASCII偏移量 | 33 | 64 |
| 最小字符 | '!' (33) | '@' (64) |
| 常用Q值范围 | 0–41(实际常用2–40) | 3–62 |
| 对应公测系列 | Phred+33 | Phred+64 |
| 是否兼容Illumina Hiseq 2000/2500 | 是(1.8+) | 否 |
| 数据处理工具默认支持 | 绝大多数现代工具 | 需指定参数 |
这里多说一句:很多新手直接用python的`ord()`减去33就以为得到真实Q值,如果文件实际是64偏移,算出来的Q值全都偏高30多,那后续QC就全错了。
上面这个流程总结了从碱基call到FASTQ质量字符串的转换。你可以看到,偏移量的选择决定了最终字符落在哪个区间。
避坑指南
最后分享几个我踩过的坑:
- 不要相信文件后缀或描述:我曾经遇到一个标记为“.fastq”的文件,实际是旧的Illumina 1.3格式,里面质量字符全是'@'和'A'。查了半天才发现。
- 混合数据最麻烦:如果一条序列的质量字符串里同时出现低质量字符和高质量字符,比如既有'!'又有'@',那肯定是混合编码(几乎不可能存在,但确实见过)。这时需要分离来源。
- 单端和双端文件的编码可能不同:有人把R1和R2跑在不同版本bcl2fastq上导出,结果两个文件编码不一致。建议同时检测。
掌握了Phred质量分数的原理和编码辨别方法,你就能在数据分析的第一步就卡住潜在错误。我个人的习惯是,写一个自动检测函数嵌入到预处理流程里,每次跑数据都自动输出编码类型。这样既省心,又避免人为失误。