用 Biopython 计算蛋白质序列一致性与覆盖度
蛋白质比对可以得到 100% 的一致性,同时只匹配较长序列的一小半。当短查询序列匹配到大蛋白内部的一段区域时,就可能出现这种情况。没有比对长度、分母定义和两条序列各自的覆盖度,这个百分比很容易被误读。
这篇教程用 Biopython 比对两条 FASTA 记录,报告完全相同的残基数、两种一致性定义,以及两条蛋白质各自的覆盖度。示例序列是构造数据,不需要 GPU 或序列数据库。它承接 FASTA 审计教程,也帮助明确蛋白质数据集按组划分所需的相似性检查规则。
1 明确比对问题
如果要寻找得分最高的匹配区域,使用局部比对;如果要让比对跨越两条完整输入序列,并允许插入缺口,使用全局比对。这两种模式都优化得分函数,不以之后报告的一致性百分比为优化目标。Biopython 比对教程介绍了两种模式和蛋白质替换矩阵。
这里两种模式都使用 BLOSUM62,缺口开启分为 -10,延伸分为 -0.5,同时应用于内部和末端缺口。按这种仿射缺口约定,长度为 k 的缺口得分是 -10 - 0.5 * (k - 1)。这些是明确的演示设置,不表示某一套打分规则适合所有蛋白质比较。
比较不同工具的结果前,先统一分母:
| 报告字段 | 计算方式 | 含义 |
|---|---|---|
identity_paired_percent |
100 × 完全匹配数 / 残基对残基列数 |
排除所有包含缺口的列。 |
identity_columns_percent |
100 × 完全匹配数 / 全部比对列数 |
分母包含缺口列。 |
query_paired_coverage_percent |
100 × 配对残基数 / 查询序列总长 |
查询序列中与目标残基相对的比例。 |
target_paired_coverage_percent |
100 × 配对残基数 / 目标序列总长 |
目标序列中与查询残基相对的比例。 |
这里两个覆盖度字段只统计配对残基,不是比对区间跨度覆盖度。位于比对区间内的插入残基,如果对面是缺口,就不计入配对覆盖度。脚本还报告 query_span 和 target_span,使用从零开始、左闭右开的坐标,便于检查这个区别。
2 安装 Biopython 并下载文件
将 compare_proteins.py、motif.fasta和 insertion.fasta下载到新的目录。需要 Python 3.10 或更新版本。示例在 Python 3.14.7、Biopython 1.88 下验证。
python3 -m venv .venv
source .venv/bin/activate
python -m pip install "biopython==1.88"
python compare_proteins.py --help
输入必须恰好包含两条尚未比对的 FASTA 记录,查询序列在前,目标序列在后。ID 必须非空且不同。本示例只接受 20 种标准氨基酸字母,将小写转为大写,拒绝模糊字符、终止符和输入缺口。文件可以带 UTF-8 BOM;普通 FASTA 序列空白由解析器处理。需要更完整的输入审计时,使用之前的 FASTA 教程。
3 运行局部匹配示例
第一份数据把八个残基的查询序列放入十八个残基的目标序列中:
>query synthetic short sequence
ACDEFGHI
>target synthetic sequence with unmatched flanks
LLLLLACDEFGHILLLLL
python compare_proteins.py motif.fasta --mode local --show > motif-local.json
python -m json.tool motif-local.json
脚本把 JSON 写入标准输出,指定 --show 时把可读比对写入标准错误输出。因此,上面的命令只把 JSON 存入 motif-local.json;需要保留旧报告时,换一个输出文件名。显示的比对为:
target 5 ACDEFGHI 13
0 |||||||| 8
query 0 ACDEFGHI 8
| 字段 | 局部比对结果 |
|---|---|
| 完全匹配数和配对残基数 | 8 和 8 |
| 比对列数 | 8 |
| 两种一致性百分比 | 100.00% |
| 查询序列配对覆盖度 | 100.00% |
| 目标序列配对覆盖度 | 44.44% |
| 查询区间 | [0, 8] |
| 目标区间 | [5, 13] |
目标序列有十个残基不在选中的局部比对内。这里的 100% 一致性只描述八个配对位置,不能代表整条目标序列,也不能据此确定功能相同或估计统计显著性。脚本报告原始比对得分,不是 BLAST bit score 或 E-value。
4 比较全局比对和插入序列
python compare_proteins.py motif.fasta --mode global --show > motif-global.json
python compare_proteins.py insertion.fasta --mode global --show > insertion-global.json
第二份数据在二十个残基的构造序列中插入四个丙氨酸:
>query synthetic reference
ACDEFGHIKLMNPQRSTVWY
>target synthetic sequence with a four-residue insertion
ACDEFGHIAAAAKLMNPQRSTVWY
| 指标 | 短区域局部比对 | 短区域全局比对 | 插入序列全局比对 |
|---|---|---|---|
| 完全匹配数 | 8 | 8 | 20 |
| 配对残基数 | 8 | 8 | 20 |
| 比对列数 | 8 | 18 | 24 |
| 配对残基一致性 | 100.00% | 100.00% | 100.00% |
| 全部比对列一致性 | 100.00% | 44.44% | 83.33% |
| 查询序列配对覆盖度 | 100.00% | 100.00% | 100.00% |
| 目标序列配对覆盖度 | 44.44% | 44.44% | 83.33% |
全局比对把短区域示例中的目标两端纳入缺口列,因此改变了按全部列计算的一致性,而配对残基一致性仍为 100%。插入示例的目标区间是 [0, 24],但只有二十个目标残基与查询残基相对:区间跨度覆盖整个目标,配对覆盖度却是 83.33%。
保守替换可能得到正的 BLOSUM62 分数,但在完全一致性统计中仍属于不匹配。得分用于选择比对,脚本随后通过逐个比较配对字母统计一致性。
5 使用完整比较脚本
下面的代码与下载文件一致:
"""Compare exactly two protein FASTA records with explicit identity denominators."""
import argparse
from itertools import islice
import json
from pathlib import Path
import sys
import Bio
from Bio import SeqIO
from Bio.Align import PairwiseAligner, substitution_matrices
ALPHABET = set("ACDEFGHIKLMNPQRSTVWY")
def read_pair(path, max_length):
if max_length < 1:
raise ValueError("max length must be positive")
with Path(path).open(encoding="utf-8-sig") as handle:
if handle.read(1) != ">":
raise ValueError("FASTA must begin with a header")
handle.seek(0)
records = list(islice(SeqIO.parse(handle, "fasta"), 3))
if len(records) != 2:
raise ValueError("supply exactly two FASTA records: query, then target")
if not all(record.id for record in records) or records[0].id == records[1].id:
raise ValueError("record IDs must be nonempty and distinct")
sequences = [str(record.seq).upper() for record in records]
for record, sequence in zip(records, sequences):
if not sequence or len(sequence) > max_length:
raise ValueError(f"{record.id}: sequence length must be 1..{max_length}")
invalid = set(sequence) - ALPHABET
if invalid:
raise ValueError(f"{record.id}: unsupported symbols {''.join(sorted(invalid))!r}")
return records, sequences
def compare(path, mode="local", max_length=5000):
records, (query, target) = read_pair(path, max_length)
aligner = PairwiseAligner(mode=mode)
aligner.substitution_matrix = substitution_matrices.load("BLOSUM62")
aligner.open_gap_score = -10.0
aligner.extend_gap_score = -0.5
alignment = next(iter(aligner.align(target, query)), None)
if alignment is None:
raise ValueError("no positive-scoring local alignment")
paired = matches = 0
for (t0, t1), (q0, q1) in zip(*alignment.aligned):
left, right = target[t0:t1], query[q0:q1]
paired += len(left)
matches += sum(a == b for a, b in zip(left, right))
if paired == 0:
raise ValueError("alignment has no residue-to-residue columns")
columns = int(alignment.length)
result = {
"query_id": records[0].id, "target_id": records[1].id,
"query_length": len(query), "target_length": len(target),
"mode": mode, "matrix": "BLOSUM62", "gap_open": -10.0, "gap_extend": -0.5,
"biopython": Bio.__version__, "score": float(alignment.score),
"matches": matches, "paired_residues": paired, "alignment_columns": columns,
"identity_paired_percent": round(100 * matches / paired, 2),
"identity_columns_percent": round(100 * matches / columns, 2),
"query_paired_coverage_percent": round(100 * paired / len(query), 2),
"target_paired_coverage_percent": round(100 * paired / len(target), 2),
"query_span": [int(x) for x in alignment.coordinates[1, [0, -1]]],
"target_span": [int(x) for x in alignment.coordinates[0, [0, -1]]],
}
return result, str(alignment)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("fasta", type=Path, help="two records: query, then target")
parser.add_argument("--mode", choices=["local", "global"], default="local")
parser.add_argument("--max-length", type=int, default=5000)
parser.add_argument("--show", action="store_true", help="print alignment to stderr")
args = parser.parse_args()
try:
result, alignment = compare(args.fasta, args.mode, args.max_length)
except (OSError, ValueError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
if args.show:
print(alignment, file=sys.stderr)
print(json.dumps(result, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
Bio.SeqIO.parse负责读取 FASTA 记录。脚本最多读取三条记录,用于拒绝超过两条记录的输入。Biopython 的 Alignment.aligned 属性给出配对序列块,alignment.length 统计打印出来的比对列数。报告中的匹配数和覆盖度遵循上表定义。
脚本选取返回的第一个最优比对,不枚举其他方案。相同得分的比对可能采用不同的缺口位置或区间坐标,因此应随报告保存 Biopython 版本及打分设置。百分比保留两位小数,整数计数同时保存,便于重新计算。
6 修复输入并解释结果
| 错误或结果 | 处理方式 |
|---|---|
supply exactly two FASTA records |
提取要比较的两条记录,查询在前、目标在后,保留来源记录副本。 |
record IDs must be nonempty and distinct |
为两条记录设置稳定且不同的标识符;重复表头 ID 会让报告含义不清。 |
unsupported symbols |
核查序列规则,解决模糊残基,或选择明确支持它们的工具和打分规则。不要悄悄删除字符。 |
no positive-scoring local alignment |
检查输入和打分规则。全局比对回答不同的问题,不要仅为强行得到匹配而切换模式。 |
sequence length must be 1..5000 |
检查是否存在空序列或意外的长输入。这个限制用于约束小型双序列示例,不是性能保证。 |
| 一致性高但覆盖度低 | 在对整条蛋白质作出判断前,检查区间、结构域和完整序列长度。 |
脚本用动态规划处理一对序列。长输入即使低于可配置限制,也可能消耗较多时间和内存。搜索数据库时,应采用面向该规模的检索流程。本示例不对数据集聚类,不检查全部训练测试序列对,也不提供判断生物功能的通用阈值。
如果用序列相似性设计机器学习数据划分,应在查看模型分数之前记录比对模式、矩阵、缺口规则、一致性分母,以及两条序列各自的最低覆盖度。对子集之间实际执行的比较使用同样定义。聚类标签不同本身不能证明序列一致性低于某个上限。
小结
一起报告完全匹配数、一致性分母和两条蛋白质的覆盖度。局部匹配与含缺口的全局比对可能具有相同的配对残基一致性,却涉及不同数量的序列位置。保留输入记录和打分设置,让其他读者能够复现比较。
示例序列和封面用于解释概念,不代表实验蛋白质结构或生物学基准结果。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/protein-sequence-identity-biopython.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。