比对 β 与 δ 珠蛋白基因
实现仿射缺口全局与局部比对,跟随回溯路径,准确理解一致性、覆盖率及评分。
目标与运行方法
本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。
学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。
环境与参数
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
RECIPE_ID='02-affine-sequence-alignment'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
"axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6 Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads Core data mode: embedded snapshot; no network requests
读取内嵌快照
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
"""Read embedded data from this notebook, including a renamed copy."""
for path in sorted(Path.cwd().glob("*.ipynb")):
try:
notebook = json.loads(path.read_text(encoding="utf-8"))
metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
blob = metadata.get("snapshots", {}).get(snapshot_key)
if metadata.get("recipe_id") != recipe_id or not blob:
continue
if blob.get("sha256") != expected_sha256:
continue
raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
if hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
return json.loads(raw)
except (OSError, json.JSONDecodeError):
continue
raise FileNotFoundError(
"Save/open the complete .ipynb in the active working folder. "
"The embedded snapshot could not be located for " + recipe_id)
SNAPSHOT_HASHES={'sequences': 'a8ae02a43c841e2932efafaf82a0bf78199baa8921a821fcbd774c8d7649e8b7'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['sequences']
方法函数
def affine_align(a, b, local=False, match=2, mismatch=-1, gap_open=-3, gap_extend=-1):
"""Three-state affine alignment. X consumes a; Y consumes b. Ties favor M,X,Y."""
if not a or not b or len(a) > 2000 or len(b) > 2000:
raise ValueError("Use two nonempty sequences, each at most 2,000 bases")
if gap_open > 0 or gap_extend > 0:
raise ValueError("Gap penalties must be nonpositive")
n, m = len(a), len(b)
scores = np.full((3,n+1,m+1), -np.inf)
trace = np.full((3,n+1,m+1), 255, dtype=np.uint8)
scores[0,0,0] = 0
if local:
scores[:] = 0
else:
for i in range(1,n+1):
scores[1,i,0] = gap_open + (i-1)*gap_extend
trace[1,i,0] = 0 if i == 1 else 1
for j in range(1,m+1):
scores[2,0,j] = gap_open + (j-1)*gap_extend
trace[2,0,j] = 0 if j == 1 else 2
best = (0.0,0,0,0)
for i in range(1,n+1):
for j in range(1,m+1):
values = scores[:,i-1,j-1]
parent = int(np.argmax(values))
scores[0,i,j] = values[parent] + (match if a[i-1] == b[j-1] else mismatch)
trace[0,i,j] = parent
values = (scores[0,i-1,j]+gap_open, scores[1,i-1,j]+gap_extend)
parent = int(np.argmax(values))
scores[1,i,j] = values[parent]; trace[1,i,j] = (0,1)[parent]
values = (scores[0,i,j-1]+gap_open, scores[2,i,j-1]+gap_extend)
parent = int(np.argmax(values))
scores[2,i,j] = values[parent]; trace[2,i,j] = (0,2)[parent]
if local:
for state in range(3):
if scores[state,i,j] <= 0:
scores[state,i,j] = 0; trace[state,i,j] = 255
state = int(np.argmax(scores[:,i,j]))
if scores[state,i,j] > best[0]:
best = (float(scores[state,i,j]),state,i,j)
if local:
score, state, i, j = best
else:
i, j = n, m; state = int(np.argmax(scores[:,i,j])); score = float(scores[state,i,j])
end = (i,j); aa, bb, path = [], [], [(i,j)]
while i or j:
if local and scores[state,i,j] == 0:
break
parent = int(trace[state,i,j])
if parent == 255:
raise ValueError("Invalid traceback")
if state == 0:
aa.append(a[i-1]); bb.append(b[j-1]); i-=1; j-=1
elif state == 1:
aa.append(a[i-1]); bb.append("-"); i-=1
else:
aa.append("-"); bb.append(b[j-1]); j-=1
state = parent; path.append((i,j))
return dict(a="".join(reversed(aa)),b="".join(reversed(bb)),score=score,
start=(i,j),end=end,path=list(reversed(path)),matrix=np.max(scores,axis=0))
print("Method ready: affine_align")
Method ready: affine_align
def alignment_summary(result, length_a, length_b):
a, b = result["a"], result["b"]
columns = len(a)
matches = sum(x == y and x != "-" for x,y in zip(a,b))
nongap = sum(x != "-" and y != "-" for x,y in zip(a,b))
return dict(score=result["score"],columns=columns,matches=matches,
identity_all_columns=matches/columns if columns else 0,
identity_nongap_columns=matches/nongap if nongap else 0,
coverage_a=(len(a)-a.count("-"))/length_a,
coverage_b=(len(b)-b.count("-"))/length_b,
gap_characters=a.count("-")+b.count("-"))
print("Method ready: alignment_summary")
Method ready: alignment_summary
分步分析
1. 选择序列与评分
先比对注释中的编码区,避免将 UTR 与 CDS 混合解释。全局比对处理每一个碱基,局部比对寻找高分片段。match +2、mismatch −1、gap-open −3、gap-extension −1 是透明的教学参数,不能视作推断出的进化模型。长度 k 的缺口得分为 open + (k−1)×extension;本课全局模型也惩罚两端缺口。
records = {r["gene"]:r for r in snapshot("sequences")["records"]}
sequences = {g:r["sequence"][r["cds_start"]-1:r["cds_end"]] for g,r in records.items()}
a,b = sequences["HBB"],sequences["HBD"]
SCORING = dict(match=2,mismatch=-1,gap_open=-3,gap_extend=-1)
print("HBB / HBD CDS lengths:",len(a),len(b)); print("Scoring:",SCORING)
HBB / HBD CDS lengths: 444 444
Scoring: {'match': 2, 'mismatch': -1, 'gap_open': -3, 'gap_extend': -1}
2. 运行三状态动态规划
上方函数保留三个状态:M 以成对碱基结束,X 表示 b 序列中的缺口,Y 表示 a 序列中的缺口。延伸缺口保持在同一个缺口状态,打开缺口则从 M 转入。回溯沿着记录的父状态恢复比对。局部比对允许从零重新开始,其坐标仍指向原 CDS,不是截短后输出的坐标。使用 AI 修改前先查看函数。
global_result = affine_align(a,b,**SCORING)
local_result = affine_align(a,b,local=True,**SCORING)
summary = pd.DataFrame([alignment_summary(r,len(a),len(b)) for r in (global_result,local_result)],
index=["global","local"])
display(summary)
print("Local 0-based intervals:",local_result["start"],local_result["end"])
| score | columns | matches | identity_all_columns | identity_nongap_columns | coverage_a | coverage_b | gap_characters | |
|---|---|---|---|---|---|---|---|---|
| global | 793.0 | 445 | 414 | 0.930337 | 0.934537 | 1.0 | 1.0 | 2 |
| local | 793.0 | 445 | 414 | 0.930337 | 0.934537 | 1.0 | 1.0 | 2 |
Local 0-based intervals: (0, 0) (444, 444)
3. 阅读着色比对
绿色列表示一致,橙色列表示替换,灰色表示缺口。每块显示 60 列,避免输出一条难以阅读的长序列。比对看起来相似不等同于基因注释,也不是统计上的同源性检验。包含所有比对列的一致性与只包含成对非缺口列的一致性采用不同分母。
import html
blocks=[]
for start in range(0,len(global_result["a"]),60):
x,y=global_result["a"][start:start+60],global_result["b"][start:start+60]
colors=["#daf0e6" if p==q else "#eee" if "-" in (p,q) else "#ffe3cb" for p,q in zip(x,y)]
rows=["".join(f'<span style="background:{c};padding:1px">{html.escape(base)}</span>'
for base,c in zip(seq,colors)) for seq in (x,y)]
blocks.append(f'<p>Columns {start+1}–{start+len(x)}</p><div>HBB {rows[0]}</div><div>HBD {rows[1]}</div>')
display(HTML('<div style="font:12px/1.8 monospace;overflow-x:auto">'+"".join(blocks)+'</div>'))
Columns 1–60
Columns 61–120
Columns 121–180
Columns 181–240
Columns 241–300
Columns 301–360
Columns 361–420
Columns 421–445
4. 绘制序列点阵图
每个点表示一段完全一致的 9-mer,坐标为从 0 开始的 CDS 片段起点。重复片段可能产生非对角线上的点。这幅图是对序列相似性的独立可视化,没有复用动态规划的回溯结果。
KMER=9
locations={}
for j in range(len(b)-KMER+1): locations.setdefault(b[j:j+KMER],[]).append(j)
dots=[(i,j) for i in range(len(a)-KMER+1) for j in locations.get(a[i:i+KMER],[])]
fig,ax=plt.subplots(figsize=(5,5))
if dots:
xy=np.array(dots); ax.scatter(xy[:,0],xy[:,1],s=8,color="#18785f",alpha=.6)
ax.set(xlabel="HBB CDS start (0-based)",ylabel="HBD CDS start (0-based)",title="Exact 9-mer matches")
plt.tight_layout();plt.show();print("Matching k-mer pairs:",len(dots))
Matching k-mer pairs: 268
5. 跟随矩阵中的回溯路径
热图展示每个位置的 M/X/Y 最大得分,蓝色路径是从全局终点恢复出的完整回溯路径。该路径不一定是唯一最优解;同分情况遵循函数中规定的确定性优先顺序。将它与点阵图比较,再一次只修改一个评分参数。
fig,ax=plt.subplots(figsize=(6,5))
matrix=global_result["matrix"].copy();matrix[~np.isfinite(matrix)]=np.nan
im=ax.imshow(matrix,origin="lower",aspect="auto",cmap="YlGn")
path=np.array(global_result["path"])
ax.plot(path[:,1],path[:,0],color="#3565af",lw=1.3)
ax.set(xlabel="HBD bases consumed",ylabel="HBB bases consumed",title="Global affine alignment and traceback")
fig.colorbar(im,ax=ax,label="Best state score");plt.tight_layout();plt.show()
6. 比较评分敏感性
保持输入序列及缺口参数不变,只修改替换罚分,比较得分、缺口数及全列一致性。这是在研究参数敏感性,不是在进行同源性假设检验。不同评分体系的原始得分不能当成同一尺度上的证据直接比较。
sensitivity=pd.DataFrame([dict(mismatch=penalty,**alignment_summary(affine_align(a,b,match=2,mismatch=penalty,gap_open=-3,gap_extend=-1),len(a),len(b))) for penalty in [-1,-2,-3]])
fig,axes=plt.subplots(1,2,figsize=(8,3))
axes[0].bar(sensitivity.mismatch.astype(str),sensitivity.identity_all_columns,color="#18785f")
axes[0].set(xlabel="Mismatch penalty",ylabel="Identity / all columns",ylim=(0,1),title="Identity under alternative costs")
axes[1].bar(sensitivity.mismatch.astype(str),sensitivity.gap_characters,color="#416aa6")
axes[1].set(xlabel="Mismatch penalty",ylabel="Gap characters",title="Gap sensitivity")
plt.tight_layout();plt.show();display(sensitivity[["mismatch","score","gap_characters"]])
| mismatch | score | gap_characters | |
|---|---|---|---|
| 0 | -1 | 793.0 | 2 |
| 1 | -2 | 764.0 | 2 |
| 2 | -3 | 738.0 | 6 |
7. 检查覆盖率并导出差异
对全局比对去除缺口后,应恢复原始序列。差异表采用比对列坐标,不是染色体坐标;存在缺口时,这些坐标不再一致。输出中同时保留评分参数及 accession,才能可靠地复现结果。
assert global_result["a"].replace("-","")==a
assert global_result["b"].replace("-","")==b
assert summary.loc["global","coverage_a"]==1 and summary.loc["global","coverage_b"]==1
differences=pd.DataFrame([dict(alignment_column=i+1,HBB=x,HBD=y)
for i,(x,y) in enumerate(zip(global_result["a"],global_result["b"])) if x!=y])
display(differences.head(12))
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
summary.to_csv(OUTPUT_DIR/"alignment-summary.csv")
differences.to_csv(OUTPUT_DIR/"alignment-differences.csv",index=False)
(OUTPUT_DIR/"alignment.txt").write_text(str(SCORING)+"\nHBB " +global_result["a"]+"\nHBD "+global_result["b"]+"\n")
print("Checks passed; differing columns:",len(differences))
| alignment_column | HBB | HBD | |
|---|---|---|---|
| 0 | 28 | T | A |
| 1 | 33 | C | T |
| 2 | 36 | T | C |
| 3 | 38 | C | A |
| 4 | 54 | G | A |
| 5 | 68 | A | C |
| 6 | 93 | G | A |
| 7 | 94 | C | T |
| 8 | 96 | G | A |
| 9 | 151 | A | T |
| 10 | 201 | A | G |
| 11 | 207 | C | A |
Checks passed; differing columns: 31
自主练习与参考答案
- 用默认评分计算长度 1 和长度 4 的缺口罚分。
- 仅把 gap-open 改为 −5,重新运行,比较得分及缺口字符数。
参考解释。 罚分分别为 −3 和 −6。修改评分可能改变最优路径,但不会改变输入序列。全局比对去除缺口后仍应恢复完整 CDS。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
添加独立函数,逐列统计一致、替换及连续缺口段,重新计算已有比对得分,并与动态规划结果比较。
提示词 2
对三个 mismatch 参数绘制全局一致性及覆盖率,使用已有仿射实现,并区分全部列与非缺口列的分母。
人工验收。 仅在测试中使用短合成序列,覆盖完全一致、长度 4 的缺失与局部共享片段;验证得分、原序列恢复及局部坐标。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore&id=NM_000518.5&rettype=fasta&retmode=text',timeout=20)
response.raise_for_status()
print("Source:",response.url)
print(response.text[:700])
except requests.RequestException as error:
print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged
来源、快照与下一步
- RefSeq HBB/HBD — NCBI public sequence data; retrieved 2026-10-09. SHA-256
ea64e38e8e5ffad06f6d8770eaee066ef556e1488c4b9a6b340cbd445abe04ad. GenBank XML CDS uses 1-based inclusive coordinates
下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。