4. 序列ID(seqid)命名规范与陷阱
序列ID,也就是GFF3文件里第一列的seqid,是每个注释行与基因组序列之间最直接的桥梁。很多人觉得它不过是个名字,写对了就行——但我在项目中见过无数次,因为seqid的拼写、大小写、甚至一个多余的空格,导致整个分析流程直接崩掉。
今天我们就来把seqid的坑一个个填平。
4.1 GFF3标准对seqid的要求
翻一下GFF3的官方规范(Sequence Ontology Project),对seqid其实只说了两句话:
- 必须与FASTA文件夹中的序列标识符完全一致。
- 不能包含空白字符(空格、制表符、换行符)。
但实际项目里,你遇到的基因组文件可能来自不同来源。有的用chr1,有的用1,有的带scaffold_前缀……每个名称都有它的脾气。
| 来源 | 常见seqid示例 | 问题指数 |
|---|---|---|
| Ensembl | 1、MT、X |
低(有惯例) |
| UCSC | chr1、chrX、chrM |
中(chr前缀需确认) |
| NCBI RefSeq | NC_000001.11、NT_123456 |
高(版本号和点号) |
| 自组装基因组 | Contig1、scaffold_01、chr1_v2 |
高(无统一标准) |
4.2 大小写问题:别以为“chr1”和“CHR1”是一个东西
我之前接手过一个项目,GFF3文件里写的是chr1,FASTA的序列头却是1。下游工具比如bedtools、gffread直接报错说“找不到序列”。花了半天排查,最后发现只是多了个"chr"前缀。嗯,这种错误最烦人——你根本不会怀疑是名字问题。
更隐蔽的是大小写。Linux默认的文件系统是区分大小写的,但很多数据库(比如MySQL的默认排序规则)不区分。所以如果你用内存数据库加载GFF3时,Chr1和chr1可能被当作同一个东西,但写回文件时又分开了。
我的一个习惯:在项目开始前,先确认一个“字典文件”,把所有seqid列出来,做成一个对照表,存成纯文本。然后用脚本检查GFF3里的seqid是否都在这个表里。这一步虽然笨,但能挡住99%的命名错误。
4.3 特殊字符与转义
GFF3文档明确说了seqid不能包含以下字符:
空格 制表符 换行符 | 回车符 %
但实际数据里,我见过以下奇奇怪怪的字符:
- 连字符和点号:
scaffold_1.v2——点号在GFF3中不是保留字符,但有些解析器会把点号当作URL编码的一部分,容易出bug。 - 冒号:
chr1:1000-2000——这其实是区域表示法,不该出现在seqid里,但我真见过有人这么写。 - 分号:
chr1;version2——分号是GFF3的注释起始符,写在seqid里会导致整行被当作注释。
另外,%字符是不允许的,但有人会用%20代替空格——这也不行。GFF3规范要求seqid必须能用正则表达式[a-zA-Z0-9_:.*^$#@!-]描述,但实际更严格:最好只包含字母、数字、下划线、连字符和点号。
4.4 版本号与后缀
NCBI的RefSeq序列ID通常带版本号,比如NC_000001.11。那个“.11”是版本号,是seqid的一部分。很多人在做对照时只写NC_000001,然后匹配不上。
还有更坑的:同一个基因组的不同版本,比如GRCh37和GRCh38,它们的seqid可能一模一样(比如都叫1),但序列内容不同。如果你不把版本信息嵌入seqid或附加元数据,后续分析完全不知道用的是哪个版本。
4.5 多序列ID与别名
有些基因组有多个名称,比如线粒体可能叫chrM、M、MT、mitochondria。如果你手里的GFF3文件用了chrM,但FASTA用的是M,那就要做别名映射。
我曾经在一个植物基因组项目里,遇到三种不同的命名系统:
GFF3里用: scaffold_1 scaffold_2 ...
FASTA用: SCAF00001 SCAF00002 ...
第三方注释用: chr1A chr1B ...
我当时写了一个Python脚本,先读.fai索引文件获取所有序列名,再读GFF3文件,检查每个seqid是否存在于索引中。不存在就报错并输出可能的候选(比如去掉前缀、加上后缀等)。那套脚本后来成了我们团队的质检标准。
4.6 工具对seqid的处理差异
不同的工具对seqid的处理方式不一样,这是最容易踩坑的地方。我整理了一个对比表:
| 工具 | 对seqid大小写是否敏感 | 是否自动修剪 | 是否允许点号 |
|---|---|---|---|
| bedtools | 敏感 | 否 | 是 |
| gffread | 敏感 | 是(会去掉chr前缀) | 是 |
| htseq-count | 敏感 | 否 | 是 |
| R中的rtracklayer | 敏感 | 否 | 是(但会强制转为字符型) |
| GffUtils (Python) | 敏感 | 否 | 是 |
| IIT (二代测序) | 不敏感(系统内部转换) | 是 | 可能报错 |
看到没?gffread会自作主张把chr1变成1——如果你的分析链路混合使用了多个工具,这种“自动修剪”就可能让上下游衔接不上。我个人建议,在管道的第一个步骤就统一使用一个“标准化函数”,将seqid转为一致格式,后面所有工具都基于这个标准格式运行。
4.7 避坑指南:我的一些血泪总结
- 永远不要靠人眼看:检查seqid一致性,用脚本。哪怕只有几兆大小的小文件,也要跑一遍。
- 正则表达式检查:先用一个简单脚本,提取所有seqid,去重,然后跟FASTA索引对比。
- 注意隐藏字符:Windows下编辑的GFF3文件可能在seqid末尾带
\r。用cat -A看一眼,或者用sed 's/\r//'清理。 - 别名映射表:当基因组文件来自不同来源时,建一个
map.tsv文件,每一行表示“别名 -> 标准名”。这比硬编码更可靠。
最后,送你一个我常用的检查命令:
# 提取GFF3所有seqid并排序去重
cut -f1 sample.gff3 | sort -u > seqid_in_gff.txt
# 提取FASTA索引中的序列名
cut -f1 genome.fasta.fai | sort -u > seqid_in_fasta.txt
# 对比差异
diff seqid_in_gff.txt seqid_in_fasta.txt
如果有输出,那就是seqid不匹配。赶紧去修吧。
gffutils,可以在创建数据库时指定disable_infer_transcripts=True,这样它会完全按照你给的seqid创建表,不会做任何自动修正。推荐这么做,方便出问题时追查。