Read a protein interaction network
Map 12 DNA-damage response proteins to STRING, retain isolates, compare confidence thresholds, and inspect evidence channels.
Goal and how to run
This lesson assumes basic Python knowledge. Read each method and caption before changing parameters. AI exercises are optional; no AI account is needed for the core workflow. Data lives in notebook metadata, so there is no companion data download. Keep the notebook saved in the active working folder.
Your goal is to understand the biological question, execute transparent calculations, check the results, and state the limits of the conclusion.
Setup and parameters
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
Load the embedded snapshot
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']
Read the method functions
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
Steps
1. Read the query, mapping and network type
The query contains twelve human DNA-damage response proteins. STRING identifiers refer to proteins; a gene symbol is resolved before graph construction. The API was queried with network_type=physical, species 9606 and no added neighbors. The frozen edge set starts at API score 400, enabling comparisons at 0.4 or above. STRING's physical network can include predicted evidence; it does not mean every edge was directly measured in one experiment.
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. Build a graph that retains all queried proteins
Create nodes from the mapping first, then add qualifying edges. A protein with no edge at this threshold stays in the graph. Such an isolate may reflect limited evidence or the chosen query; it is not proof that the protein has no partners. Self-loops are excluded and repeated undirected edges retain their largest combined score.
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. Inspect evidence channels
Combined score is a confidence measure, not binding affinity or interaction strength. escore describes experimental evidence, dscore curated database evidence, and tscore text-mining evidence. These channels are not independent probabilities to add together. Inspect them separately to see whether a high combined score rests on the evidence appropriate to your question.
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. Compare thresholds using one frozen edge set
Change only the threshold. All twelve nodes remain, so the isolate count is comparable. A denser network is not necessarily better evidence. Scores below 0.4 were not retrieved: lowering the local threshold further cannot recover edges absent from the snapshot. Refresh the data explicitly for that new question.
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. Compute topology and describe its limits
Degree counts neighbors within the selected query. Unweighted betweenness counts shortest-path mediation without pretending confidence is a physical distance. Communities use confidence as a graph weight, with a fixed seed. Neither centrality nor a community label proves essentiality, causality or a biological module; the query itself already focuses on a pathway.
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. Check and export a portable network
The GraphML file preserves node identifiers and selected confidence channels. The source snapshot inside the notebook preserves the full returned evidence fields, including those not displayed here. Export the threshold audit too, so a reader can reproduce the graph rather than merely view its picture.
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
Exercises and reference explanation
- Why must an isolated query protein remain visible?
- Change THRESHOLD from 0.7 to 0.95 and compare the isolates with the sensitivity table.
Reference explanation. Dropping isolates changes the node universe and hides lack of evidence within the query. Increasing the threshold can remove edges but cannot add them when the snapshot and nodes are fixed.
Keep a copy before changing parameters. Compare old and new figures and record which inputs, calculations and conclusions changed.
Optional AI coding exercises
Prompt 1
Create a side-by-side network view for confidence thresholds 0.7 and 0.95 with one shared seeded layout. Keep all 12 nodes and show their STRING identifiers in a table.
Prompt 2
Add a separate evidence audit listing edges with experimental score ≥0.7. Explain that this is an additional evidence filter within the frozen physical network.
Human acceptance. Check 12 nodes at every threshold, no self-loops, no added neighbors, correct human species, and a monotonically nonincreasing edge count as the threshold rises.
Review the edited source, then manually run affected cells and subsequent checks. Ask the assistant to explain the purpose, packages and data provenance of its changes, and reconcile that explanation with actual output.
Optional online extension
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
Sources, snapshots and next steps
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.
Next steps. Transfer these methods to your own public or authorized data while retaining input checks, recorded parameters and result validation. Document the new experimental design and method limits; this example does not validate a new experiment. Full data licenses and generation instructions accompany the cookbook.