| name | bio-applied-ppi-networks |
| description | Build and analyze protein-protein interaction (PPI) networks from STRING DB with NetworkX: compute degree/betweenness/closeness/eigenvector centrality, classify hub and bottleneck genes, test scale-free topology, and detect network communities/modules (Louvain, greedy modularity). Use when asked to find hub genes, identify drug targets from a network, query the STRING API, build a gene interaction graph, or cluster a PPI network into functional modules. |
| tool_type | python |
| primary_tool | networkx |
PPI Network Construction and Analysis
When to Use
- Querying STRING DB for a gene/protein list and building an interaction network from the results.
- Ranking genes by centrality to nominate hub genes, essential proteins, or bottleneck drug targets.
- Testing whether a network follows the scale-free (power-law) topology expected of biological PPI networks.
- Detecting functional modules/communities in a PPI or co-expression network (Louvain, greedy modularity, WGCNA-style).
- Visualizing a gene network colored by function, module, or centrality.
Version Compatibility
- Python ≥ 3.10,
networkx ≥ 3.2, pandas ≥ 2.0, requests ≥ 2.31
- Optional:
python-louvain (community package) for true Louvain partitioning — networkx.community.greedy_modularity_communities works without it.
- STRING DB REST API v12 (
https://string-db.org/api)
Prerequisites
pip install networkx pandas requests matplotlib python-louvain
- Familiarity with graph concepts (nodes/edges, connected components) and basic pandas.
Goal: Fetch confidence-scored protein interactions for a gene list from STRING DB.
Approach: POST/GET gene identifiers to the STRING network endpoint, filter by combined_score, and load into a pandas.DataFrame.
import requests
import pandas as pd
def query_string_network(genes: list[str], species: int = 9606,
required_score: int = 400,
caller_identity: str = "my_app") -> pd.DataFrame:
"""Query STRING DB for interactions among a list of gene symbols.
Args:
genes: gene symbols, e.g. ["TP53", "MDM2", "BRCA1", "EGFR"]
species: NCBI taxonomy ID (9606 = human)
required_score: STRING combined_score threshold (0-999)
caller_identity: required by STRING API to identify the calling app
Returns:
DataFrame with columns preferredName_A, preferredName_B, combined_score, ...
"""
url = "https://string-db.org/api/json/network"
params = {
"identifiers": "%0d".join(genes),
"species": species,
"required_score": required_score,
"caller_identity": caller_identity,
}
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
return pd.DataFrame(response.json())
STRING score channels: neighborhood, gene fusion, co-occurrence, coexpression, experimental, database, text mining.
combined_score = 1 - prod(1 - individual_scores).
| Score range | Confidence |
|---|
| 0–150 | Low (noise) |
| 400–700 | Medium (common threshold) |
| 700–900 | High |
| 900–999 | Highest (multiple independent evidence sources) |
Goal: Build a NetworkX graph from a STRING edge list and compute centrality metrics to find hubs and bottlenecks.
Approach: Filter by confidence, restrict to the largest connected component, then compute degree/betweenness/closeness/eigenvector centrality and classify each gene's role.
import networkx as nx
import pandas as pd
def build_ppi_graph(df_edges: pd.DataFrame, score_col: str = "combined_score",
threshold: int = 400) -> nx.Graph:
"""Build an undirected PPI graph from a STRING-style edge DataFrame.
Args:
df_edges: columns preferredName_A/protein1, preferredName_B/protein2, score_col
threshold: minimum combined_score to keep an edge (400 = medium confidence)
Returns:
Graph restricted to the largest connected component.
"""
src, dst = df_edges.columns[0], df_edges.columns[1]
df_filtered = df_edges[df_edges[score_col] >= threshold].copy()
G = nx.from_pandas_edgelist(df_filtered, source=src, target=dst, edge_attr=score_col)
largest_cc = max(nx.connected_components(G), key=len)
return G.subgraph(largest_cc).copy()
def centrality_table(G: nx.Graph) -> pd.DataFrame:
"""Compute centrality metrics and classify each node as hub/bottleneck/peripheral."""
degree = dict(G.degree())
betweenness = nx.betweenness_centrality(G, normalized=True)
closeness = nx.closeness_centrality(G)
eigenvector = nx.eigenvector_centrality(G, max_iter=500, tol=1e-6)
df = pd.DataFrame({
"Gene": list(G.nodes()),
"Degree": [degree[g] for g in G.nodes()],
"Betweenness": [betweenness[g] for g in G.nodes()],
"Closeness": [closeness[g] for g in G.nodes()],
"Eigenvector": [eigenvector[g] g G.nodes()],
}).sort_values(, ascending=)
deg_thresh = df[].median()
bet_thresh = df[].median()
():
is_hub = row.Degree > deg_thresh
is_bottleneck = row.Betweenness > bet_thresh
is_hub is_bottleneck:
is_hub:
is_bottleneck:
df[] = df.apply(classify, axis=)
df
Hub/bottleneck classification
High betweenness Low betweenness
High degree Hub + bottleneck Hub only
Low degree Bottleneck only Peripheral
| Metric | Definition | Biological use |
|---|
| Degree | Direct neighbor count | Hub genes; essential proteins |
| Betweenness | Fraction of shortest paths through node | Bottleneck drug targets |
| Closeness | Inverse avg shortest path to all nodes | Signal propagation speed |
| Eigenvector | Importance weighted by neighbor importance | Quality over quantity of connections |
| Clustering coeff | Fraction of neighbors that are connected | Module membership |
PPI networks are expected to be scale-free, with degree distribution P(k) ~ k^-γ: most nodes have few connections while a few hubs (e.g. TP53, EGFR, MYC) have many. Verify by plotting the degree distribution on log-log axes — it should be roughly linear.
Goal: Partition a PPI network into functional modules/communities.
Approach: Use Louvain (python-louvain) if available, else fall back to NetworkX's greedy modularity maximization; report the modularity score Q.
import networkx as nx
def detect_communities(G: nx.Graph) -> tuple[dict, float]:
"""Partition graph nodes into communities and compute modularity Q.
Returns:
(partition, modularity) where partition maps node -> community id.
Q > 0.3 indicates meaningful community structure; Q > 0.5 is strong.
"""
try:
import community as community_louvain
partition = community_louvain.best_partition(G, random_state=42)
except ImportError:
communities = nx.community.greedy_modularity_communities(G)
partition = {node: i for i, comm in enumerate(communities) for node in comm}
n_communities = len(set(partition.values()))
comm_sets = [{n for n, c in partition.items() if c == i} for i in range(n_communities)]
modularity = nx.community.modularity(G, comm_sets)
return partition, modularity
Pitfalls
- Always filter by confidence score before building the graph — unfiltered STRING data includes many low-confidence edges that inflate connectivity.
- Use the largest connected component for centrality metrics; isolated nodes and small components distort global statistics and can make
eigenvector_centrality fail to converge.
- Directed vs undirected: use
nx.DiGraph for regulatory networks (TF → target), nx.Graph for PPI/co-expression networks.
- Betweenness is O(V·E) — expensive on large graphs; use
nx.betweenness_centrality(G, k=500) (sampling) for approximation on graphs with thousands of nodes.
- Modularity Q depends on resolution — communities found by Louvain are non-deterministic without
random_state; always fix a seed for reproducibility.
See Also
bio-applied-network-modules — deeper community detection (Louvain/Leiden, WGCNA eigengenes)
bio-applied-gene-regulatory-networks — directed TF-target inference (GENIE3, ARACNE)
network-biology — broader network biology overview including Cytoscape export and GO enrichment per module