生成蛋白质向量之前:用 Python 检查 FASTA 文件
运行蛋白质编码器之前,先检查 FASTA 文件里到底有什么。两个不同编号可能对应同一条序列,同一个编号也可能重复出现,却带着不同序列。这两种情况都容易导致向量与测量值匹配错误,或让重复输入同时进入训练集和测试集。
本文用 Python 标准库编写一个小工具,只生成 CSV 检查报告,不修改输入文件。可以把它放在氨基酸组成基线或 ESM-2 向量提取流程之前运行。
先明确输入规则
FASTA 记录以 > 开头的描述行起始,后面是序列行。NCBI 格式说明介绍了这一结构及 BLAST 接受的字符。模型流程可以采用更严格的规则。
本例仅接受 20 种标准氨基酸字母,比较时把小写转为大写,并拼接换行分隔的序列。解析器会跳过空白行,接受 UTF-8 字节顺序标记。这些是本文明确选择的处理方式,不代表所有 FASTA 软件都如此。
| 检查项 | 处理方式 |
|---|---|
| 记录编号 | 取 > 后按空白分隔的第一个字段,要求在文件内唯一。 |
| 空序列 | 标记,等待检查。 |
| 20 种字母以外的字符 | 标记,不删除或替换残基。 |
| 序列行中的空格或制表符 | 标记,包括行首和行尾空白。 |
| 超出指定长度 | 标记,不截断。 |
| 完全相同的标准字母序列 | 后续记录指向首次出现的记录序号。 |
X、B、Z、U、O、缺口和终止符都不符合这套刻意收窄的规则,但不代表含这些字符的序列在生物学上一定无效。应根据数据来源和所选模型的分词器决定如何处理。仅含 ACGT 的字符串也能通过字母检查;程序无法据此判定输入是蛋白质还是核酸。
长度上限必须由用户提供,因为它取决于具体流程。分词器的 token 预算可能包含特殊标记,不同编码器也可能采用不同分词方式,不能直接把模型标称的 token 上限当作残基上限。
运行一个方便核对的例子
下载 audit_fasta.py 和 demo.fasta,放在同一目录。不需要安装第三方依赖,脚本已在 Python 3.14.7 上测试。
示例包含用于检查程序行为的人工字符串:
>sample_a synthetic example
ACDEFGHIKL
MNPQRSTVWY
>sample_b same sequence in lowercase
acdefghiklmnpqrstvwy
>sample_c requires a residue policy
ACDXEFG
>sample_a reused identifier
MKT
>sample_empty
运行:
python3 audit_fasta.py demo.fasta --max-residues 20 --out audit.csv
这里用 20 作为长度上限,只是为了演示,并非推荐的模型设置。输出:
records=5 flagged=4 exact_repeats=1
report=audit.csv
两条 sample_a 都会因编号冲突被标记;sample_c 包含 X;sample_empty 没有残基。sample_b 转成大写后与记录 1 相同,重复关系写在单独一列。
退出状态 0 表示没有问题记录或完全重复序列,1 表示报告已完成但需要检查,2 表示解析、参数或文件错误。因此,本例虽然成功生成报告,仍会返回状态 1。程序不会覆盖已有报告,再次运行时请使用新的 --out 路径。
完整检查脚本
"""Audit protein FASTA under an explicit 20-residue policy; never rewrite input."""
import argparse
import csv
import hashlib
import sys
from collections import Counter
from pathlib import Path
AA = set("ACDEFGHIKLMNPQRSTVWY")
def records(path):
header, chunks = None, []
with path.open(encoding="utf-8-sig") as handle:
for line_no, raw in enumerate(handle, 1):
line = raw.rstrip("\r\n")
if not line.strip():
continue
if line.startswith(">"):
if header is not None:
yield header, "".join(chunks)
header, chunks = line[1:].strip(), []
if not header:
raise ValueError(f"line {line_no}: empty FASTA header")
else:
if header is None:
raise ValueError(f"line {line_no}: sequence before first header")
chunks.append(line)
if header is not None:
yield header, "".join(chunks)
def audit(path, max_residues):
rows, first_sequence = [], {}
for number, (header, raw) in enumerate(records(path), 1):
ident = header.split()[0]
sequence = raw.upper()
problems = []
if not sequence:
problems.append("empty_sequence")
if any(c.isspace() for c in raw):
problems.append("whitespace_in_sequence")
invalid = sorted(set(raw) - AA - set("acdefghiklmnpqrstvwy"))
if invalid:
problems.append("unsupported=" + repr("".join(invalid)))
if len(sequence) > max_residues:
problems.append("over_limit")
# Hash and duplicate comparison only for canonical, nonempty sequences.
canonical = bool(sequence) and not invalid
digest = hashlib.sha256(sequence.encode("ascii")).hexdigest() if canonical else ""
duplicate = ""
if canonical:
duplicate = first_sequence.get(sequence, "")
first_sequence.setdefault(sequence, number)
rows.append(dict(record=number, id=ident, length=len(sequence),
lowercase=raw != sequence, sha256=digest,
duplicate_of_record=duplicate, issues=";".join(problems)))
if not rows:
raise ValueError("no FASTA records")
counts = Counter(row["id"] for row in rows)
for row in rows:
if counts[row["id"]] > 1:
row["issues"] = ";".join(filter(None, [row["issues"], "duplicate_id"]))
return rows
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("fasta", type=Path)
parser.add_argument("--max-residues", type=int, required=True)
parser.add_argument("--out", type=Path, default=Path("audit.csv"))
args = parser.parse_args()
if args.max_residues < 1:
parser.error("--max-residues must be positive")
try:
rows = audit(args.fasta, args.max_residues)
with args.out.open("x", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
except (OSError, ValueError) as error:
parser.exit(2, f"error: {error}\n")
flagged = sum(bool(row["issues"]) for row in rows)
repeats = sum(row["duplicate_of_record"] != "" for row in rows)
print(f"records={len(rows)} flagged={flagged} exact_repeats={repeats}")
print(f"report={args.out}")
return 1 if flagged or repeats else 0
if __name__ == "__main__":
sys.exit(main())
程序使用 csv.DictWriter 写报告,打开文件时指定 newline=""。序列指纹由 hashlib.sha256 计算,对应拼接并转成大写后的标准字母序列,不包含原始文件字节、标题或测量值。
重复检查直接比较规范化后的序列字符串。含不支持字符的序列和空序列不生成指纹,也不参与重复比较。若标准字母序列只是超长,仍会获得指纹,并参与重复检查。
工具逐行读取输入,但会把报告行和不同的标准字母序列保留在内存中,适合内存能够容纳的数据集。处理大型语料时,可将编号和序列索引移入磁盘数据库,同时保持一致的检查规则。
根据报告整理可复现的数据集
合并标签或向量前,先解决编号冲突。程序保留标题的完整第一个字段,包括其中的竖线,不会自动识别数据库 accession。若标签表使用另一套编号,需要明确编写并检查映射关系。
重复序列需要结合来源判断。它们可能来自重复下载、重复测量,也可能是同一种蛋白质在不同条件下的实验。应保留测量来源。若要评估对未见序列的泛化能力,完全相同的序列应放在同一侧,并进一步处理相关序列。完全匹配无法发现同源关系、共享抗体谱系或抗原重叠;抗体模型评价指南讨论了这些更广泛的分组问题。
处理完报告后,保存原始 FASTA、规范化规则、最终编号映射和数据划分。修改序列后,重新生成对应指纹和向量。合并时核对编号与序列指纹,不要只依赖数组中的行顺序。
通过检查并不代表标签正确,也不能证明数据完全没有泄漏。标准化、特征选择等需要从数据学习参数的步骤,仍应只在训练集上拟合,参见 scikit-learn 的数据泄漏说明。
常见问题
| 提示或现象 | 检查方法 |
|---|---|
sequence before first header |
序列数据前必须有 FASTA 标题,标题的 > 应位于行首。 |
empty FASTA header |
在 > 后填写有意义的记录编号。 |
duplicate_id |
检查两条记录,不要直接保留最后一条。 |
unsupported= |
查看原始字符,并记录适用于当前数据集的处理规则。 |
over_limit |
核对编码器可用的残基预算,明确长序列处理方法。 |
| 退出状态 1 | 打开已生成的 CSV,检查问题记录和重复序列。 |
报告中的问题解决后,再运行组成基线并保存划分,为后续向量实验留下可追溯的输入数据。
资料核对日期:2026 年 9 月 23 日。封面为 AI 生成的概念插图。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/protein-fasta-audit-python.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。