Build, analyze, and visualize social networks with NetworkX: centrality, community detection, small-world metrics, bipartite networks, and Gephi export from edge lists or adjacency matrices.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Build, analyze, and visualize social networks with NetworkX: centrality, community detection, small-world metrics, bipartite networks, and Gephi export from edge lists or adjacency matrices.
Social network analysis (SNA) treats actors (people, organizations, papers) as nodes and
their relationships as edges. This skill covers the full pipeline from raw edge-list data to
publication-ready network visualizations, covering:
Building networks from edge lists and adjacency matrices
"""
Build a NetworkX graph from a pandas edge-list DataFrame.
Parameters
----------
df : pd.DataFrame
Edge list with at least source and target columns.
source_col, target_col : str
Column names for edge endpoints.
weight_col : str or None
Column with edge weights. If None, all weights default to 1.
directed : bool
If True, return a DiGraph; otherwise an undirected Graph.
node_attr_df : pd.DataFrame, optional
Node attribute table with ``node_id_col`` as key.
node_id_col : str
Column in ``node_attr_df`` that matches node identifiers.
Returns
-------
nx.Graph or nx.DiGraph
"""
"""
Build a NetworkX graph from an adjacency (or weighted) matrix.
Parameters
----------
matrix : array-like or pd.DataFrame
Square adjacency matrix. Use a DataFrame for automatic node labels.
node_labels : list of str, optional
Labels for rows/columns if ``matrix`` is a numpy array.
directed : bool
Build a DiGraph if True.
threshold : float
Only include edges where matrix value exceeds this threshold.
Returns
-------
nx.Graph or nx.DiGraph
"""
"""
Compute degree, betweenness, closeness, eigenvector, and PageRank centralities.
Parameters
----------
G : nx.Graph or nx.DiGraph
weight : str
Edge attribute to use as weight (set to None for unweighted).
k_betweenness : int or None
Number of pivot nodes for approximate betweenness (faster for large graphs).
Returns
-------
pd.DataFrame
One row per node, columns for each centrality metric, sorted by degree.
"""
"""
Detect communities using the Girvan-Newman edge-betweenness algorithm.
Parameters
----------
G : nx.Graph
Input graph.
n_communities : int
Target number of communities to extract.
Returns
-------
list of frozenset
Each frozenset is a community of node identifiers.
"""
"""
Visualize a NetworkX graph with attribute-driven node colors and sizes.
Parameters
----------
G : nx.Graph
Graph to draw.
node_color_attr : str or None
Node attribute used for color mapping. Use None for uniform color.
node_size_attr : str or None
Node attribute used for size scaling. Use None for uniform size.
layout : str
Layout algorithm: ``"spring"``, ``"circular"``, ``"kamada_kawai"``, ``"random"``.
title : str
Plot title.
figsize : tuple
Figure size.
save_path : str, optional
Save figure path.
seed : int
Random seed for layout reproducibility.
Returns
-------
matplotlib.figure.Figure
"""
"""
Export graph to Gephi-compatible formats (.gexf or .graphml).
Parameters
----------
G : nx.Graph
Graph with optional node attributes.
output_prefix : str
Base filename (without extension).
format : str
``"gexf"`` or ``"graphml"``.
"""
f"{output_prefix}.{format}"
if
format
"gexf"
elif
format
"graphml"
else
raise
f"Unknown format: {format}. Use 'gexf' or 'graphml'."
"""
Compute clustering coefficient and average path length, compare to random graphs.
Parameters
----------
G : nx.Graph
Input graph (should be connected for path length; uses largest component).
n_random : int
Number of random Erdos-Renyi graphs to average over.
seed : int
Random seed.
Returns
-------
dict with keys: ``clustering``, ``avg_path_length``, ``sigma`` (small-world coefficient),
``omega``, ``random_clustering``, ``random_path_length``.
"""
# Use largest connected component
max
len
1
2
"weight"
for
in
0
10_000
int
max
len
# sigma > 1 indicates small-world
return
"n_nodes"
"n_edges"
"clustering"
"avg_path_length"
"random_clustering"
"random_path_length"
"sigma"
"is_small_world"
1.0
Example A: Co-Authorship Network from OpenAlex Data
This example builds a co-authorship network from OpenAlex API results, detects communities, and
exports a Gephi-ready file.
# ── Example A ─────────────────────────────────────────────────────────────import requests
import time
deffetch_openalex_works(topic: str, n_results: int = 200) -> list[dict]:
"""Fetch works from OpenAlex API for a given topic."""
url = "https://api.openalex.org/works"
works = []
cursor = "*"
per_page = min(n_results, 200)
whilelen(works) < n_results:
params = {
"search": topic,
"per-page": per_page,
"cursor": cursor,
"select": "id,title,authorships",
"filter": "is_oa:true",
}
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
results = data.get("results", [])
ifnot results:
break
works.extend(results)
cursor = data.get("meta", {}).get("next_cursor", None)
ifnot cursor:
break
time.sleep(0.1) # Respect rate limitsreturn works[:n_results]
defworks_to_coauthorship_edgelist(works: list[dict]) -> pd.DataFrame:
"""Convert OpenAlex works list to co-authorship edge list."""
edges = defaultdict(float)
for work in works:
authors = [
a["author"]["display_name"]
for a in work.get("authorships", [])
if a.get("author") and a["author"].get("display_name")
]
for a1, a2 in itertools.combinations(authors, 2):
key = tuple(sorted([a1, a2]))
edges[key] += 1.0
records = [{"source": s, "target": t, "weight": w} for (s, t), w in edges.items()]
return pd.DataFrame(records)
# --- Fetch co-authorship data ------------------------------------------------
TOPIC = "social network analysis"print(f"Fetching OpenAlex works on: {TOPIC}")
works = fetch_openalex_works(TOPIC, n_results=300)
print(f"Retrieved {len(works)} works.")
edge_df = works_to_coauthorship_edgelist(works)
print(f"Edge list: {len(edge_df)} co-authorship pairs")
# --- Build network -----------------------------------------------------------
G_coauth = build_network_from_edgelist(
edge_df,
source_col="source",
target_col="target",
weight_col="weight",
directed=False,
)
# Remove isolated nodes (authors who only appear in multi-author papers once)
G_coauth.remove_nodes_from(list(nx.isolates(G_coauth)))
print(f"After removing isolates: {G_coauth.number_of_nodes()} nodes")
# --- Centrality analysis -----------------------------------------------------
centrality_df = compute_all_centralities(G_coauth, k_betweenness=200)
print("\nTop 10 authors by betweenness centrality:")
print(centrality_df.nlargest(10, "betweenness")[["node", "degree", "betweenness", "pagerank"]])
# Set node attributes for visualizationfor _, row in centrality_df.iterrows():
if row["node"] in G_coauth.nodes:
G_coauth.nodes[row["node"]]["degree"] = row["degree"]
# --- Community detection -----------------------------------------------------
partition = detect_communities_louvain(G_coauth, resolution=1.0)
nx.set_node_attributes(G_coauth, partition, "community")
# Community size distributionfrom collections import Counter
comm_sizes = Counter(partition.values())
print(f"\nCommunity sizes (top 5): {comm_sizes.most_common(5)}")
# --- Visualize ---------------------------------------------------------------
fig = visualize_network(
G_coauth,
node_color_attr="community",
node_size_attr="degree",
layout="spring",
title=f"Co-Authorship Network: '{TOPIC}' (OpenAlex)",
save_path="coauthorship_network.png",
)
plt.show()
# --- Export to Gephi ---------------------------------------------------------
export_to_gephi(G_coauth, output_prefix="coauthorship_network", format="gexf")
# --- Small-world test --------------------------------------------------------
metrics = compute_small_world_metrics(G_coauth)
print(f"\nSmall-world metrics:")
for k, v in metrics.items():
print(f" {k}: {v}")
Example B: Twitter/X Follower Ego Network Analysis
This example builds an ego network from a manually prepared follower list CSV and computes
structural properties: triadic closure, clustering, and centrality of the ego node.
# ── Example B ─────────────────────────────────────────────────────────────# Input: CSV with columns user_id, follower_id (followers of your ego node)# Plus a second CSV: follower_follower_edges.csv — edges BETWEEN followersimport os
# --- Load data (replace paths with actual file locations) -------------------
EGO_ID = "ego_user_123"
FOLLOWERS_CSV = os.environ.get("FOLLOWERS_CSV", "followers.csv")
FF_EDGES_CSV = os.environ.get("FOLLOWER_FOLLOWER_CSV", "follower_follower_edges.csv")
followers_df = pd.read_csv(FOLLOWERS_CSV) # columns: user_id, follower_id
ff_edges_df = pd.read_csv(FF_EDGES_CSV) # columns: source, target# Build ego network: add ego→follower edges + follower↔follower edges
ego_edges = pd.DataFrame({
"source": EGO_ID,
"target": followers_df["follower_id"],
"weight": 1.0,
})
all_edges = pd.concat([ego_edges, ff_edges_df.assign(weight=1.0)], ignore_index=True)
G_ego = build_network_from_edgelist(
all_edges,
source_col="source",
target_col="target",
weight_col="weight",
directed=True,
)
# --- Ego-specific metrics ----------------------------------------------------# Alters = direct neighbors of ego
alters = list(G_ego.successors(EGO_ID)) + list(G_ego.predecessors(EGO_ID))
alters = list(set(alters))
G_alter = G_ego.subgraph(alters).copy() # subgraph of alters onlyprint(f"Ego: {EGO_ID}")
print(f"Alters (direct neighbors): {len(alters)}")
print(f"Edges among alters: {G_alter.number_of_edges()}")
# Density of alter subgraph
n_alters = len(alters)
max_possible = n_alters * (n_alters - 1)
alter_density = G_alter.number_of_edges() / max_possible if max_possible > 0else0print(f"Alter subgraph density: {alter_density:.4f}")
# Effective size (structural holes measure)
redundancy = sum(
G_alter.degree(j) / n_alters
for j in alters
if G_alter.degree(j) > 0
)
effective_size = n_alters - redundancy
print(f"Effective network size (Burt): {effective_size:.2f}")
# --- Centrality of ego in its full network -----------------------------------
centrality_df = compute_all_centralities(G_ego, k_betweenness=300)
ego_row = centrality_df[centrality_df["node"] == EGO_ID]
print(f"\nEgo centrality profile:\n{ego_row.to_string(index=False)}")
# --- Community structure among alters ----------------------------------------
G_alter_undirected = G_alter.to_undirected()
if G_alter_undirected.number_of_edges() > 0:
partition_alter = detect_communities_louvain(G_alter_undirected)
nx.set_node_attributes(G_alter_undirected, partition_alter, "community")
fig = visualize_network(
G_alter_undirected,
node_color_attr="community",
node_size_attr=None,
layout="spring",
title=f"Ego Network Alters: {EGO_ID}",
save_path="ego_network_alters.png",
)
plt.show()
# --- Triadic closure: fraction of open triads that are closed ----------------
transitivity = nx.transitivity(G_alter_undirected)
avg_clustering = nx.average_clustering(G_alter_undirected)
print(f"\nTriadic closure (transitivity): {transitivity:.4f}")
print(f"Average clustering coefficient: {avg_clustering:.4f}")
# --- Export ------------------------------------------------------------------
export_to_gephi(G_ego, output_prefix=f"ego_network_{EGO_ID}", format="graphml")
Bipartite Networks
Bipartite networks connect two disjoint node sets (e.g., users and movies, authors and papers).
Use NetworkX's bipartite module to project onto one mode.
from networkx.algorithms import bipartite
defbuild_bipartite_from_membership(
membership_df: pd.DataFrame,
actor_col: str = "actor",
group_col: str = "group",
) -> tuple[nx.Graph, set, set]:
"""
Build a bipartite graph from actor–group membership data.
Returns the bipartite graph plus node sets for each layer.
"""
B = nx.Graph()
actors = set(membership_df[actor_col])
groups = set(membership_df[group_col])
B.add_nodes_from(actors, bipartite=0)
B.add_nodes_from(groups, bipartite=1)
B.add_edges_from(zip(membership_df[actor_col], membership_df[group_col]))
return B, actors, groups
defproject_bipartite(
B: nx.Graph,
nodes: set,
weighted: bool = True,
) -> nx.Graph:
"""
Project a bipartite graph onto one node set.
Parameters
----------
B : nx.Graph
Bipartite graph.
nodes : set
The node set to project onto.
weighted : bool
If True, edge weight = number of shared neighbors in the other layer.
Returns
-------
nx.Graph
Projected unipartite graph.
"""if weighted:
return bipartite.weighted_projected_graph(B, nodes)
return bipartite.projected_graph(B, nodes)
# Usage example:# membership_df = pd.DataFrame({"actor": ["A","A","B","C"], "group": ["G1","G2","G1","G2"]})# B, actors, groups = build_bipartite_from_membership(membership_df)# G_actors = project_bipartite(B, actors)# print(nx.info(G_actors))
Notes and Best Practices
Performance on Large Graphs
Graph Size
Recommended Approach
< 10 K nodes
All exact centralities safe
10 K – 100 K nodes
Use k_betweenness=500 approximation
> 100 K nodes
Use nx.pagerank only; consider graph-tool or igraph
Community Detection Comparison
Louvain: Fast, scales to millions of nodes, non-deterministic (use random_state).
Girvan-Newman: Slow (O(m² n)), but hierarchical; good for small networks (<1 K nodes).
For directed networks, use partition = community_louvain.best_partition(G.to_undirected()).
References
Newman, M. E. J. (2010). Networks: An Introduction. Oxford University Press.
Blondel, V. D. et al. (2008). Fast unfolding of communities in large networks.
Journal of Statistical Mechanics, P10008.
Burt, R. S. (2004). Structural holes and good ideas. American Journal of Sociology, 110(2).