解读蛋白质物理互作网络
将 12 个 DNA 损伤响应蛋白映射到 STRING,保留孤立节点,比较阈值并查看证据来源。
目标与运行方法
本课适合具备 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
import networkx as nx
RECIPE_ID='05-string-physical-network'
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={'network': '19b6b87b2a66cd2e11f0c8c82ee712659dc7e8a9eaded9e36238604a5223d136'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['network']
方法函数
def physical_graph(snapshot, threshold=0.7):
if not 0 <= threshold <= 1:
raise ValueError("Network threshold must be between 0 and 1")
graph = nx.Graph()
for mapping in snapshot["mapping"]:
graph.add_node(mapping["preferredName"],string_id=mapping["stringId"])
for row in snapshot["edges"]:
a, b = row["preferredName_A"], row["preferredName_B"]
if a == b or row["score"] < threshold:
continue
if row["ncbiTaxonId"] != 9606 or a not in graph or b not in graph:
raise ValueError("Unexpected network species or neighbor")
attrs = {key:float(row.get(key,0)) for key in ("score","escore","dscore","tscore")}
if not graph.has_edge(a,b) or graph[a][b]["score"] < attrs["score"]:
graph.add_edge(a,b,**attrs)
return graph
print("Method ready: physical_graph")
Method ready: physical_graph
分步分析
1. 阅读查询、映射及网络类型
查询包含 12 个人类 DNA 损伤响应相关蛋白。STRING 标识符指向蛋白,构图前需要解析基因符号。本快照采用 network_type=physical、物种 9606,不额外添加邻居;冻结边集从 API score 400 开始,因此可比较 ≥0.4 的阈值。STRING 物理网络可以包含预测证据,并不意味着每条边都在某个实验中被直接测量。
network=snapshot("network")
mapping=pd.DataFrame(network["mapping"])
display(mapping[[c for c in ["queryItem","queryIndex","stringId","preferredName","ncbiTaxonId"] if c in mapping]])
print("Query:",network["query"])
print("Snapshot API threshold:",network["request"]["required_score"],"; added nodes:",network["request"]["add_nodes"])
print("Version:",network["version"])
| queryItem | queryIndex | stringId | preferredName | ncbiTaxonId | |
|---|---|---|---|---|---|
| 0 | TP53 | 0 | 9606.ENSP00000269305 | TP53 | 9606 |
| 1 | MDM2 | 1 | 9606.ENSP00000258149 | MDM2 | 9606 |
| 2 | MDM4 | 2 | 9606.ENSP00000356150 | MDM4 | 9606 |
| 3 | ATM | 3 | 9606.ENSP00000278616 | ATM | 9606 |
| 4 | ATR | 4 | 9606.ENSP00000343741 | ATR | 9606 |
| 5 | CHEK1 | 5 | 9606.ENSP00000391090 | CHEK1 | 9606 |
| 6 | CHEK2 | 6 | 9606.ENSP00000372023 | CHEK2 | 9606 |
| 7 | BRCA1 | 7 | 9606.ENSP00000418960 | BRCA1 | 9606 |
| 8 | BRCA2 | 8 | 9606.ENSP00000369497 | BRCA2 | 9606 |
| 9 | CDKN1A | 9 | 9606.ENSP00000384849 | CDKN1A | 9606 |
| 10 | BAX | 10 | 9606.ENSP00000293288 | BAX | 9606 |
| 11 | BCL2 | 11 | 9606.ENSP00000381185 | BCL2 | 9606 |
Query: ['TP53', 'MDM2', 'MDM4', 'ATM', 'ATR', 'CHEK1', 'CHEK2', 'BRCA1', 'BRCA2', 'CDKN1A', 'BAX', 'BCL2']
Snapshot API threshold: 400 ; added nodes: 0
Version: [{'stable_address': 'https://version-12-5.string-db.org', 'string_version': '12.5'}]
2. 构图并保留所有查询蛋白
先根据映射创建节点,再添加通过阈值的边。没有边的查询蛋白仍保留在图中。孤立节点可能反映证据不足或查询范围限制,不能证明该蛋白没有互作伙伴。排除自环,对重复无向边保留最大的综合得分。
THRESHOLD=.7
graph=physical_graph(network,THRESHOLD)
print("Nodes / edges / isolates:",graph.number_of_nodes(),graph.number_of_edges(),list(nx.isolates(graph)))
positions=nx.spring_layout(graph,seed=42,weight="score")
fig,ax=plt.subplots(figsize=(8,6))
nx.draw_networkx_edges(graph,positions,ax=ax,alpha=.5,width=[1+2*graph[a][b]["score"] for a,b in graph.edges],edge_color="#829b8f")
nx.draw_networkx_nodes(graph,positions,ax=ax,node_size=[450+80*graph.degree(n) for n in graph],
node_color=["#18785f" if n=="TP53" else "#c4ded3" for n in graph],edgecolors="white",linewidths=1.5)
nx.draw_networkx_labels(graph,positions,ax=ax,font_size=10)
ax.set_title("STRING physical network: human query, confidence ≥0.7");ax.axis("off")
plt.tight_layout();plt.show()
Nodes / edges / isolates: 12 32 []
3. 查看证据来源
综合得分表示置信度,不是结合亲和力或互作强度。escore 表示实验证据,dscore 表示数据库证据,tscore 表示文本挖掘证据。不能将这些字段当成独立概率直接相加;分别查看它们,才能判断高综合得分是否具有与你的问题相符的证据。
edge_table=pd.DataFrame([dict(protein_a=a,protein_b=b,**attrs) for a,b,attrs in graph.edges(data=True)],
columns=["protein_a","protein_b","score","escore","dscore","tscore"])
evidence=edge_table.sort_values("score",ascending=False).head(15)
fig,ax=plt.subplots(figsize=(7,5))
if len(evidence):
im=ax.imshow(evidence[["score","escore","dscore","tscore"]],vmin=0,vmax=1,cmap="YlGnBu",aspect="auto")
ax.set(xticks=range(4),xticklabels=["Combined","Experiments","Database","Text mining"],
yticks=range(len(evidence)),yticklabels=evidence.protein_a+" — "+evidence.protein_b)
fig.colorbar(im,ax=ax,label="Channel score")
else:ax.text(.5,.5,"No edges at this threshold",ha="center",transform=ax.transAxes)
ax.set_title("Evidence channels for the highest-score edges");plt.tight_layout();plt.show()
display(edge_table.head(10))
| protein_a | protein_b | score | escore | dscore | tscore | |
|---|---|---|---|---|---|---|
| 0 | TP53 | MDM2 | 0.999 | 0.999 | 0.7 | 0.927 |
| 1 | TP53 | BAX | 0.997 | 0.995 | 0.0 | 0.560 |
| 2 | TP53 | ATM | 0.999 | 0.999 | 0.0 | 0.651 |
| 3 | TP53 | ATR | 0.999 | 0.999 | 0.0 | 0.113 |
| 4 | TP53 | MDM4 | 0.999 | 0.999 | 0.7 | 0.760 |
| 5 | TP53 | BRCA2 | 0.999 | 0.999 | 0.0 | 0.190 |
| 6 | TP53 | CHEK2 | 0.999 | 0.999 | 0.0 | 0.518 |
| 7 | TP53 | BCL2 | 0.999 | 0.999 | 0.9 | 0.680 |
| 8 | TP53 | CDKN1A | 0.999 | 0.999 | 0.0 | 0.343 |
| 9 | TP53 | CHEK1 | 0.999 | 0.999 | 0.0 | 0.223 |
4. 用同一个冻结边集比较阈值
比较时只修改阈值,始终保留 12 个节点,因此孤立节点数可以直接比较。网络更稠密并不一定意味着证据更好。快照未获取 <0.4 的边,在本地继续降低阈值无法恢复不存在于快照中的边;若要研究该范围,需要明确重新获取数据。
thresholds=[.4,.7,.9,.95,.99]
sensitivity=pd.DataFrame([dict(threshold=t,edges=g.number_of_edges(),isolates=len(list(nx.isolates(g))),
components=nx.number_connected_components(g)) for t in thresholds for g in [physical_graph(network,t)]])
fig,ax=plt.subplots(figsize=(7,3))
ax.plot(sensitivity.threshold,sensitivity.edges,"o-",label="Edges")
ax.plot(sensitivity.threshold,sensitivity.isolates,"s-",label="Isolates")
ax.set(xlabel="Minimum combined confidence",ylabel="Count",title="Network sensitivity to the confidence threshold")
ax.legend();plt.tight_layout();plt.show();display(sensitivity)
| threshold | edges | isolates | components | |
|---|---|---|---|---|
| 0 | 0.40 | 38 | 0 | 1 |
| 1 | 0.70 | 32 | 0 | 1 |
| 2 | 0.90 | 27 | 0 | 1 |
| 3 | 0.95 | 23 | 0 | 1 |
| 4 | 0.99 | 20 | 0 | 1 |
5. 计算拓扑并解释局限
度数统计查询范围内的邻居。无权介数统计最短路径中介作用,避免把置信度伪装成物理距离。社区检测使用置信度权重及固定随机种子。中心性或社区标签不能证明必需性、因果作用或功能模块;查询本身已经偏向特定通路。
centrality=nx.betweenness_centrality(graph,weight=None)
communities=nx.community.louvain_communities(graph,weight="score",seed=42) if graph.number_of_edges() else [{n} for n in graph]
membership={n:i+1 for i,nodes in enumerate(communities) for n in nodes}
node_table=pd.DataFrame([dict(protein=n,degree=graph.degree(n),betweenness=centrality[n],community=membership[n],
string_id=graph.nodes[n]["string_id"]) for n in graph]).sort_values(["degree","protein"],ascending=[False,True])
display(node_table)
| protein | degree | betweenness | community | string_id | |
|---|---|---|---|---|---|
| 0 | TP53 | 11 | 0.434848 | 1 | 9606.ENSP00000269305 |
| 3 | ATM | 7 | 0.031818 | 2 | 9606.ENSP00000278616 |
| 6 | CHEK2 | 7 | 0.031818 | 2 | 9606.ENSP00000372023 |
| 7 | BRCA1 | 6 | 0.009091 | 2 | 9606.ENSP00000418960 |
| 5 | CHEK1 | 6 | 0.030303 | 2 | 9606.ENSP00000391090 |
| 4 | ATR | 5 | 0.000000 | 2 | 9606.ENSP00000343741 |
| 1 | MDM2 | 5 | 0.030303 | 1 | 9606.ENSP00000258149 |
| 2 | MDM4 | 5 | 0.040909 | 1 | 9606.ENSP00000356150 |
| 8 | BRCA2 | 4 | 0.000000 | 2 | 9606.ENSP00000369497 |
| 11 | BCL2 | 3 | 0.009091 | 1 | 9606.ENSP00000381185 |
| 9 | CDKN1A | 3 | 0.000000 | 1 | 9606.ENSP00000384849 |
| 10 | BAX | 2 | 0.000000 | 1 | 9606.ENSP00000293288 |
6. 校验并导出可移植网络
GraphML 保留节点标识符与本课展示的置信度字段。notebook 内部原始快照保留完整返回字段,包括未展示的证据。阈值审计表也应导出,让读者能够复现网络,而不只是查看一张图片。
assert graph.number_of_nodes()==12
assert nx.number_of_selfloops(graph)==0
assert all(attrs["score"]>=THRESHOLD for _,_,attrs in graph.edges(data=True))
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
edge_table.to_csv(OUTPUT_DIR/"physical-edges.csv",index=False)
node_table.to_csv(OUTPUT_DIR/"proteins.csv",index=False)
sensitivity.to_csv(OUTPUT_DIR/"threshold-audit.csv",index=False)
nx.write_graphml(graph,OUTPUT_DIR/"physical-network.graphml")
print("Checks passed; GraphML, nodes, edges and threshold audit exported")
Checks passed; GraphML, nodes, edges and threshold audit exported
自主练习与参考答案
- 为什么孤立查询蛋白仍应显示在图中?
- 将 THRESHOLD 从 0.7 改为 0.95,与敏感性表中的孤立节点数比较。
参考解释。 删除孤立节点会改变节点背景,掩盖查询范围内的证据缺口。快照及节点固定时,升高阈值可以删除边,但不能增加边。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
用一个共享的固定随机布局,并排展示 0.7 与 0.95 的网络,保留全部 12 个节点,表格中显示 STRING 标识符。
提示词 2
添加实验证据得分 ≥0.7 的边列表,明确这是冻结物理网络内部的额外证据过滤。
人工验收。 检查各阈值均有 12 个节点、没有自环或额外邻居、物种为人类,且随阈值升高边数不增加。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://string-db.org/api/json/version',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
来源、快照与下一步
STRING physical network — CC BY 4.0 (STRING data); retrieved 2026-10-09. SHA-256
bfae5cea28cd3e90ff8bc12f768bfcfd66a6037c25f7c47fe2a81baaa7ea222f. Original response preserved in the embedded snapshot.STRING identifier mapping — CC BY 4.0 (STRING data); retrieved 2026-10-09. SHA-256
77fc78421492b54895dc862e7a8f4de4a84c61db73fdfbf0696ccee24fe1c371. Original response preserved in the embedded snapshot.
下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。