Show code
from __future__ import annotations
import pandas as pd
from IPython.display import Markdown, display
from eu_trade_network import config, data_loader, db, graph, metrics, vizRQ1 — who is central, and where does Austria rank? Computes degree, strength, betweenness, eigenvector centrality and PageRank on the directed weighted graph, ranks the hubs through SQL, and extracts the disparity-filter backbone.
Why weighted betweenness uses distance = 1 / value_kusd, and why unweighted betweenness is identically zero here, is explained on the Data & method page.
Rebuild the edge list / digraph for config.YEAR, compute strengths and centralities, then refresh the DuckDB nodes table.
edgelist = data_loader.build_edgelist()
G = graph.build_graph(edgelist)
summary = graph.graph_summary(G)
cent = metrics.compute_centralities(G)
node_meta = (
pd.DataFrame([{"iso3": n, **attrs} for n, attrs in G.nodes(data=True)])
.sort_values("iso3")
.reset_index(drop=True)
)
nodes_db = node_meta.merge(cent, on="iso3", how="inner")
con = db.connect()
db.init_schema(con)
db.write_table(
con,
"nodes",
nodes_db[
[
"iso3",
"name",
"grp",
"out_strength",
"in_strength",
"degree",
"betweenness",
"betweenness_u",
"eigenvector",
"pagerank",
]
],
)
# Keep edges in sync if NB01 has not been run in this environment.
edges_db = edgelist.copy()
edges_db["year"] = config.YEAR
db.write_table(con, "edges", edges_db[["exporter_iso3", "importer_iso3", "value_kusd", "year"]])
print(summary)
print(f"Updated {len(nodes_db)} nodes → {config.DB_PATH}")
cent.sort_values("betweenness", ascending=False).head(10)Hub ranking by weighted betweenness, then Austria’s bilateral partners.
aut = hubs.loc[hubs["iso3"] == "AUT"].iloc[0]
aut_pr_rank = int(
hubs.sort_values("pagerank", ascending=False)
.reset_index(drop=True)
.assign(r=lambda d: d.index + 1)
.loc[lambda d: d["iso3"] == "AUT", "r"]
.iloc[0]
)
aut_str_rank = int(
cent.sort_values("out_strength", ascending=False)
.reset_index(drop=True)
.assign(r=lambda d: d.index + 1)
.loc[lambda d: d["iso3"] == "AUT", "r"]
.iloc[0]
)
n_zero_b = int((hubs["betweenness"] == 0).sum())
n_pos_b = int((hubs["betweenness"] > 0).sum())
top_hub = hubs.iloc[0]
display(
Markdown(
f"**{top_hub['name']} leads, and position is not volume.** "
f"{top_hub['name']} ({top_hub['iso3']}) scores "
f"{float(top_hub['betweenness']):.3f} on weighted betweenness. Only {n_pos_b} of "
f"{len(hubs)} economies score above zero at all, because the digraph is complete "
f"(density = {summary['density']:.2f}): for most pairs the direct link is already "
f"the cheapest path, so nobody sits between them.\n\n"
f"**Austria is not a bridge.** It is one of the {n_zero_b} economies with "
f"betweenness = 0 — the SQL rank of {int(aut['rank'])} is an arbitrary tie-break "
f"among those zeros, not a ranking. Its partners already trade with each other "
f"directly. By export strength Austria is {aut_str_rank}th of {len(hubs)} and by "
f"PageRank {aut_pr_rank}th: a mid-tier EU economy whose largest partner is Germany "
f"on both sides of the trade balance."
)
)The network is dense (near-complete bilateral coverage). We plot the strength ranking and do not fit a power law.
Statistically significant edges at config.DISPARITY_ALPHA (Serrano, Boguñá & Vespignani 2009). Also compute the rich-club coefficient on the undirected projection.
backbone = metrics.disparity_filter(G, alpha=config.DISPARITY_ALPHA)
bb_edges = pd.DataFrame(
[
{"exporter_iso3": u, "importer_iso3": v, "value_kusd": float(d["weight"])}
for u, v, d in backbone.edges(data=True)
]
)
rc = metrics.rich_club(G)
print(
f"Backbone keeps {backbone.number_of_edges()} / {G.number_of_edges()} edges "
f"at alpha={config.DISPARITY_ALPHA}"
)
rc.tail(10)# Two-panel backbone map: Europe detail + global context (kaleido-safe).
fig_backbone = viz.plot_backbone_map(
bb_edges,
nodes_db,
title=(
f"Disparity-filter backbone (α={config.DISPARITY_ALPHA}): "
f"{backbone.number_of_edges()} significant edges"
),
)
out = viz.save_fig(fig_backbone, "02_backbone_map.png", headline=True)
print(f"Saved {out}")
fig_backbone.show()
con.close()