| name | network-graph-analysis |
| description | Graph and network analysis — graph construction, centrality measures, community detection, graph ML basics, and visualization. Use when data has relational/network structure. |
| allowed_agents | ["data","experiment"] |
Network Graph Analysis
Overview
Graph and network analysis extracts structural information from relational data. Use this skill when your data describes connections between entities — citations, protein interactions, social ties, trade flows, co-occurrence matrices, or any adjacency structure. The key insight: if relationships carry information that tabular rows cannot, model the structure explicitly.
When to Use This Skill
Use this skill when:
- Your data is fundamentally relational (entities linked by edges)
- You need to identify influential nodes, bridging nodes, or tightly-knit communities
- You are building or evaluating a graph neural network (GNN)
- You need to visualize network structure for publication or exploration
- Your dataset is a co-occurrence matrix, adjacency matrix, or edge list
Do not reach for graph methods if rows in your tabular dataset are independent (no meaningful pairwise relationships). First run EDA (EDA skill) to understand data shape, then apply this skill.
Graph Construction from Data
From a NumPy/SciPy Adjacency Matrix
import numpy as np
import networkx as nx
import scipy.sparse as sp
A = np.array([[0, 1, 0],
[1, 0, 1],
[0, 1, 0]])
G = nx.from_numpy_array(A)
G_directed = nx.from_numpy_array(A, create_using=nx.DiGraph())
A_sparse = sp.csr_matrix(A)
G_sparse = nx.from_scipy_sparse_array(A_sparse)
From an Edge List CSV
import pandas as pd
edges_df = pd.read_csv('edges.csv')
G = nx.from_pandas_edgelist(edges_df, source='source', target='target')
G_weighted = nx.from_pandas_edgelist(
edges_df,
source='source',
target='target',
edge_attr='weight',
create_using=nx.DiGraph()
)
From Bipartite Data (e.g., users x items)
from networkx.algorithms import bipartite
B = nx.Graph()
B.add_nodes_from(['u1', 'u2', 'u3'], bipartite=0)
B.add_nodes_from(['i1', 'i2'], bipartite=1)
B.add_edges_from([('u1','i1'), ('u2','i1'), ('u2','i2'), ('u3','i2')])
user_nodes = {n for n, d in B.nodes(data=True) if d['bipartite'] == 0}
G_projected = bipartite.weighted_projected_graph(B, user_nodes)
Weighted vs Unweighted / Directed vs Undirected
| Choice | When to use |
|---|
| Undirected | Symmetric relationships (co-authorship, friendship) |
| Directed | Asymmetric relationships (citations, follows, flows) |
| Unweighted | Only presence/absence of edge matters |
| Weighted | Edge strength matters (call frequency, correlation strength) |
Use nx.DiGraph() for directed, nx.Graph() for undirected. Add weight edge attributes for weighted graphs — most centrality functions respect the weight keyword.
Basic Graph Statistics
G = nx.karate_club_graph()
print(f"Nodes: {G.number_of_nodes()}")
print(f"Edges: {G.number_of_edges()}")
print(f"Density: {nx.density(G):.4f}")
degrees = [d for _, d in G.degree()]
print(f"Average degree: {sum(degrees)/len(degrees):.2f}")
print(f"Max degree: {max(degrees)}")
components = list(nx.connected_components(G))
print(f"Connected components: {len(components)}")
print(f"Largest component: {max(len(c) for c in components)} nodes")
if nx.is_connected(G):
print(f"Diameter: {nx.diameter(G)}")
else:
largest = G.subgraph(max(components, key=len))
print(f"Diameter (LCC): {nx.diameter(largest)}")
print(f"Global clustering: {nx.transitivity(G):.4f}")
print(f"Avg local clustering:{nx.average_clustering(G):.4f}")
Centrality Measures
Degree Centrality — Who Has the Most Connections?
The simplest measure. Node importance = fraction of other nodes it connects to. Use as a baseline; fast to compute.
degree_cent = nx.degree_centrality(G)
strength = dict(G.degree(weight='weight'))
top_degree = sorted(degree_cent.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top nodes by degree centrality:", top_degree)
Betweenness Centrality — Bridges Between Communities
Fraction of shortest paths passing through a node. High betweenness = bottleneck / broker. Expensive: O(VE) for unweighted, slower for weighted. Sample for large graphs.
between_cent = nx.betweenness_centrality(G, normalized=True, weight='weight')
between_approx = nx.betweenness_centrality(G, k=200, normalized=True)
top_between = sorted(between_cent.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top nodes by betweenness:", top_between)
Closeness Centrality — Reaches Others Quickly
Average shortest path distance to all other nodes (inverted). High closeness = spreads information fast. Meaningful only in connected graphs (or apply per component).
closeness_cent = nx.closeness_centrality(G)
top_close = sorted(closeness_cent.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top nodes by closeness:", top_close)
Eigenvector Centrality — Connected to Important Nodes
Score proportional to scores of neighbors. Being connected to high-scoring nodes raises your own score. PageRank is a damped variant. Use for undirected or directed networks where prestige propagates.
eigen_cent = nx.eigenvector_centrality(G, max_iter=1000, weight='weight')
pagerank = nx.pagerank(G, alpha=0.85, weight='weight')
top_eigen = sorted(eigen_cent.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top nodes by eigenvector centrality:", top_eigen)
When to Use Each
| Measure | Best for |
|---|
| Degree | Quick scan, finding hubs, baseline |
| Betweenness | Finding brokers, bridges, critical infrastructure nodes |
| Closeness | Information diffusion, finding broadcast centers |
| Eigenvector/PageRank | Influence, authority, prestige propagation |
Compute all four and compare — they often disagree and the disagreement is informative.
Community Detection
Louvain — Fast, Large Graphs (Recommended Default)
Maximizes modularity greedily. Non-deterministic; run multiple times and pick highest modularity. Available via python-louvain (community package) or igraph.
import community as community_louvain
partition = community_louvain.best_partition(G, weight='weight', random_state=42)
modularity = community_louvain.modularity(partition, G)
num_communities = len(set(partition.values()))
print(f"Communities: {num_communities}, Modularity: {modularity:.4f}")
nx.set_node_attributes(G, partition, 'community')
import igraph as ig
G_ig = ig.Graph.from_networkx(G)
louvain_result = G_ig.community_multilevel(weights='weight')
print(f"Modularity (igraph): {louvain_result.modularity:.4f}")
Girvan-Newman — Hierarchical, Small Graphs
Removes edges with highest betweenness iteratively, revealing community hierarchy. Slow (O(E²V)); use only when n < 1000 and you need a dendrogram.
from networkx.algorithms.community import girvan_newman
comp = girvan_newman(G)
for communities in itertools.islice(comp, 3):
print(f"k={len(communities)}: {[len(c) for c in communities]}")
Spectral Clustering — Exactly k Communities
Use when you know k in advance. Builds graph Laplacian, finds k eigenvectors, runs k-means in spectral space.
from sklearn.cluster import SpectralClustering
import numpy as np
A = nx.to_numpy_array(G)
sc = SpectralClustering(
n_clusters=5,
affinity='precomputed',
assign_labels='kmeans',
random_state=42
)
labels = sc.fit_predict(A)
Label Propagation — Very Fast, Non-Deterministic
Each node inherits the most common label of its neighbors. Converges in O(E). Results vary between runs; use when speed matters more than stability.
from networkx.algorithms.community import label_propagation_communities
communities = list(label_propagation_communities(G))
print(f"Found {len(communities)} communities")
Graph ML Basics
When Does Graph Structure Help?
Graph structure adds value beyond tabular features when:
- Nodes are connected in ways that predict the target (social influence, protein proximity)
- Labels propagate through edges (homophily: connected nodes share labels)
- Global topology matters (scale-free vs random network properties)
If nodes have no meaningful connections or edges are arbitrary, stick with tabular models.
GNN Overview: Message Passing
At each layer, each node aggregates feature vectors from its neighbors, then updates its own representation. After k layers, a node sees its k-hop neighborhood.
Key architectures:
- GCN (Graph Convolutional Network): normalized sum aggregation — simple, effective
- GraphSAGE: sample-and-aggregate — scales to large graphs via mini-batching
- GAT (Graph Attention Network): learns attention weights over neighbors — best when neighbor importance varies
Task Types
| Task | Input | Output | Example |
|---|
| Node classification | Node features + graph | Label per node | Classify paper topics in citation network |
| Link prediction | Node features + partial graph | Edge probability | Recommend friends, predict protein interactions |
| Graph classification | Full graph | Label per graph | Classify molecules, detect fraud subgraphs |
Libraries
from torch_geometric.data import Data
from torch_geometric.nn import GCNConv, GATConv, SAGEConv
import torch
data = Data(
x=node_features,
edge_index=edge_index,
y=labels
)
class GCN(torch.nn.Module):
def __init__(self, in_channels, hidden, out_channels):
super().__init__()
self.conv1 = GCNConv(in_channels, hidden)
self.conv2 = GCNConv(hidden, out_channels)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index).relu()
x = torch.nn.functional.dropout(x, p=0.5, training=self.training)
x = self.conv2(x, edge_index)
return x
import dgl
g = dgl.from_networkx(G)
Visualization
NetworkX + Matplotlib — Small Graphs (<500 nodes)
import matplotlib.pyplot as plt
import networkx as nx
fig, ax = plt.subplots(figsize=(12, 8))
pos = nx.spring_layout(G, seed=42)
communities = community_louvain.best_partition(G)
colors = [communities[n] for n in G.nodes()]
sizes = [300 * nx.degree_centrality(G)[n] + 50 for n in G.nodes()]
nx.draw_networkx(
G, pos,
node_color=colors,
node_size=sizes,
cmap=plt.cm.tab20,
with_labels=True,
font_size=8,
edge_color='gray',
alpha=0.8,
ax=ax
)
ax.set_title("Network with community coloring")
plt.tight_layout()
plt.savefig('network.png', dpi=150, bbox_inches='tight')
Pyvis — Interactive HTML Visualization
from pyvis.network import Network
net = Network(height='750px', width='100%', bgcolor='#222222', font_color='white')
net.from_nx(G)
for node in net.nodes:
node['size'] = degree_cent[node['id']] * 50 + 5
node['title'] = f"Node {node['id']}, degree={G.degree(node['id'])}"
net.set_options("""
{
"physics": {
"forceAtlas2Based": {
"gravitationalConstant": -50,
"centralGravity": 0.01,
"springLength": 100
},
"solver": "forceAtlas2Based"
}
}
""")
net.save_graph('network.html')
Gephi — Large Graphs (>5000 nodes)
Export from networkx and open in Gephi for GPU-accelerated layout and interactive exploration:
nx.write_gexf(G, 'network.gexf')
nx.write_graphml(G, 'network.graphml')
In Gephi: use ForceAtlas2 layout, color by modularity class (run it under Statistics), size by degree. Export as SVG for publication.
Evaluation
Community Detection
from sklearn.metrics import normalized_mutual_info_score, adjusted_rand_score
true_labels = [ground_truth[n] for n in G.nodes()]
pred_labels = [partition[n] for n in G.nodes()]
nmi = normalized_mutual_info_score(true_labels, pred_labels)
ari = adjusted_rand_score(true_labels, pred_labels)
print(f"NMI: {nmi:.4f}, ARI: {ari:.4f}")
modularity = community_louvain.modularity(partition, G)
print(f"Modularity Q: {modularity:.4f}")
Link Prediction
from sklearn.metrics import roc_auc_score
import random
edges = list(G.edges())
test_edges = random.sample(edges, int(0.2 * len(edges)))
G_train = G.copy()
G_train.remove_edges_from(test_edges)
non_edges = list(nx.non_edges(G))
test_non_edges = random.sample(non_edges, len(test_edges))
def score_pairs(G, pairs):
return [len(list(nx.common_neighbors(G, u, v))) for u, v in pairs]
scores_pos = score_pairs(G_train, test_edges)
scores_neg = score_pairs(G_train, test_non_edges)
y_true = [1]*len(test_edges) + [0]*len(test_non_edges)
y_score = scores_pos + scores_neg
auc = roc_auc_score(y_true, y_score)
print(f"Link prediction AUC (common neighbors): {auc:.4f}")
Node Classification
Use standard classification metrics (accuracy, F1, AUC) from sklearn. Evaluate on a held-out set of nodes — do not leak graph structure from test nodes into training via multi-hop neighborhoods unless you explicitly account for it.
Best Practices
- Inspect the degree distribution first — scale-free (power-law) vs Poisson shapes imply different generative processes
- Always check for isolated nodes and giant component fraction — disconnected graphs require careful handling
- Self-loops and multi-edges — decide explicitly:
nx.Graph() removes multi-edges, use nx.MultiGraph() to keep them
- Large graphs (>100k nodes) — switch to igraph or graph-tool; networkx is Python-native and slow at scale
- Reproducibility — set
random_state/seed for any non-deterministic algorithm (Louvain, spring layout)
- Null models — compare your graph properties against an Erdos-Renyi or configuration model null to validate findings
Common Pitfalls
- Treating directed graphs as undirected: PageRank on undirected graphs is meaningless — check
nx.is_directed(G)
- Diameter on disconnected graphs: Always check
nx.is_connected(G) first
- Betweenness on large graphs without sampling: It will run for hours — use
k parameter to approximate
- Comparing modularity across algorithms: Modularity is maximized differently by each algorithm; comparisons are not apples-to-apples
- Forgetting to normalize centrality: Raw degree vs degree centrality (0–1) depend on graph size — always normalize for cross-graph comparison