Co-sponsorship Network Analysis
Graphs, centrality, community detection, and cross-party collaboration
Overview
Co-sponsorship networks reveal the hidden structure of legislative coalitions. When legislators co-sign bills together, they signal policy alignment - and these signals aggregate into a network where clusters, bridges, and brokers become visible.
What you will build:
- Co-sponsorship edge list from the Open Assembly API
- NetworkX graph with party-colored nodes
- Centrality measures (degree, betweenness, closeness)
- Community detection (Louvain algorithm)
- Cross-party collaboration index
Step 1: Build edge list
import httpx
import pandas as pd
import time
from itertools import combinations
API_KEY = "YOUR_KEY"
BASE = "https://open.assembly.go.kr/portal/openapi"
def api_get(endpoint, **params):
params = {"KEY": API_KEY, "Type": "json", "pSize": 100, **params}
r = httpx.get(f"{BASE}/{endpoint}", params=params,
headers={"User-Agent": "research-client"})
body = r.json()
rows = body.get(endpoint, [{}])
if isinstance(rows, dict):
return []
head = rows[0] if rows else {}
if "head" in head:
code = head["head"][1]["RESULT"]["CODE"]
if code != "INFO-000":
return []
return rows[1].get("row", [])
return []
# Collect co-sponsors for bills on a topic
bills = []
for page in range(1, 20):
rows = api_get("nzmimeepazxkubdpn", AGE="22", BILL_NAME="환경", pIndex=page)
if not rows:
break
bills.extend(rows)
time.sleep(0.3)
print(f"Bills found: {len(bills)}")
# Get co-sponsors per bill
edges = []
for i, bill in enumerate(bills):
bill_id = bill["BILL_ID"]
sponsors = api_get("BILLINFOPPSR", BILL_ID=bill_id)
names = [s["PPSR_NM"] for s in sponsors if s.get("PPSR_NM")]
# Create pairwise edges
for a, b in combinations(sorted(set(names)), 2):
edges.append({"source": a, "target": b, "bill_id": bill_id})
if (i + 1) % 20 == 0:
print(f" {i + 1}/{len(bills)} bills processed")
time.sleep(0.3)
edge_df = pd.DataFrame(edges)
print(f"Raw edges: {len(edge_df)}")Faster with kna: The master database includes pre-built cosponsorship data:
from kna.data import BillDB
db = BillDB()
edges = pd.read_parquet(db.data_dir / "cosponsorship_edges.parquet")
# 769K edges, 661 unique members, instantStep 2: Build weighted graph
import networkx as nx
# Aggregate: weight = number of shared bills
weighted = (
edge_df
.groupby(["source", "target"])
.size()
.reset_index(name="weight")
)
G = nx.from_pandas_edgelist(weighted, "source", "target", edge_attr="weight")
print(f"Nodes: {G.number_of_nodes()}, Edges: {G.number_of_edges()}")
print(f"Density: {nx.density(G):.4f}")Step 3: Add party attributes
# Get member info for party labels
members = api_get("nwvrqwxyaytdsfvhu", AGE="22", pSize=300)
party_map = {m["HG_NM"]: m["POLY_NM"] for m in members if m.get("HG_NM")}
for node in G.nodes():
G.nodes[node]["party"] = party_map.get(node, "unknown")
# Party distribution
from collections import Counter
party_counts = Counter(nx.get_node_attributes(G, "party").values())
print(pd.Series(party_counts).sort_values(ascending=False))Step 4: Centrality measures
# Degree centrality: how many co-sponsors
degree = nx.degree_centrality(G)
# Weighted degree (strength): total co-sponsorship ties
strength = {n: sum(d["weight"] for _, _, d in G.edges(n, data=True)) for n in G.nodes()}
# Betweenness: who bridges communities
betweenness = nx.betweenness_centrality(G, weight="weight")
# Compile into DataFrame
centrality = pd.DataFrame({
"name": list(G.nodes()),
"party": [G.nodes[n].get("party", "") for n in G.nodes()],
"degree": [degree[n] for n in G.nodes()],
"strength": [strength[n] for n in G.nodes()],
"betweenness": [betweenness[n] for n in G.nodes()],
}).sort_values("betweenness", ascending=False)
print("Top 10 brokers (betweenness centrality):")
print(centrality.head(10)[["name", "party", "degree", "betweenness"]].to_string(index=False))Interpreting centrality:
| Measure | High value means |
|---|---|
| Degree | Co-sponsors with many different legislators |
| Strength | Frequently co-sponsors (many shared bills) |
| Betweenness | Bridges between otherwise disconnected groups |
Step 5: Community detection
The Louvain algorithm finds densely connected subgroups:
try:
from community import community_louvain
partition = community_louvain.best_partition(G, weight="weight")
except ImportError:
# Fallback
communities = nx.community.greedy_modularity_communities(G, weight="weight")
partition = {}
for i, comm in enumerate(communities):
for node in comm:
partition[node] = i
n_communities = len(set(partition.values()))
modularity = nx.community.modularity(G,
[{n for n, c in partition.items() if c == i} for i in set(partition.values())],
weight="weight")
print(f"Communities detected: {n_communities}")
print(f"Modularity (Q): {modularity:.3f}")Interpreting modularity: Q > 0.3 suggests meaningful community structure. In Korean legislative networks, Q typically ranges 0.3-0.6, reflecting strong party-line clustering with some cross-party bridges.
# Compare detected communities vs actual parties
for node in G.nodes():
G.nodes[node]["community"] = partition[node]
community_party = pd.DataFrame([
{"name": n, "party": G.nodes[n]["party"], "community": G.nodes[n]["community"]}
for n in G.nodes()
])
cross_tab = pd.crosstab(community_party["community"], community_party["party"])
print(cross_tab)Step 6: Cross-party collaboration
What fraction of a legislator’s co-sponsorships are with members of other parties?
def cross_party_rate(node, graph):
"""Fraction of co-sponsors from different parties."""
my_party = graph.nodes[node].get("party", "")
neighbors = list(graph.neighbors(node))
if not neighbors:
return 0.0
cross = sum(1 for n in neighbors if graph.nodes[n].get("party", "") != my_party)
return cross / len(neighbors)
centrality["cross_party"] = [cross_party_rate(n, G) for n in centrality["name"]]
# Average cross-party rate by party
print("\nCross-party collaboration by party:")
print(centrality.groupby("party")["cross_party"].mean().sort_values(ascending=False).round(3))Visualization
import plotly.graph_objects as go
pos = nx.spring_layout(G, k=0.3, seed=42, weight="weight")
PARTY_COLORS = {
"더불어민주당": "#004EA2", "국민의힘": "#E61E2B",
"조국혁신당": "#003764", "개혁신당": "#FF6B00",
"정의당": "#FFCC00", "무소속": "#999999",
}
node_x, node_y, node_color, node_text = [], [], [], []
for node in G.nodes():
x, y = pos[node]
node_x.append(x)
node_y.append(y)
party = G.nodes[node].get("party", "")
node_color.append(PARTY_COLORS.get(party, "#999999"))
node_text.append(f"{node} ({party})<br>Degree: {degree[node]:.3f}")
fig = go.Figure()
fig.add_trace(go.Scatter(
x=node_x, y=node_y, mode="markers",
marker=dict(size=8, color=node_color, opacity=0.8),
text=node_text, hoverinfo="text",
))
fig.update_layout(
title="Co-sponsorship Network",
showlegend=False, template="plotly_white",
xaxis=dict(showgrid=False, visible=False),
yaxis=dict(showgrid=False, visible=False),
)
fig.show()Export
# Nodes with centrality
centrality.to_csv("network_nodes.csv", index=False, encoding="utf-8-sig")
# Weighted edges
weighted.to_csv("network_edges.csv", index=False, encoding="utf-8-sig")References
- Fowler, J. H. (2006). Connecting the Congress: A study of cosponsorship networks. Political Analysis, 14(4), 456-487.
- Kirkland, J. H., & Gross, J. H. (2014). Measurement and theory in legislative networks. Social Networks, 36, 97-109.
- Blondel, V. D., et al. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics, P10008.