Structural Backbone Methods#

This tutorial demonstrates the structural methods, which use topological properties of the network to identify important edges.

Setup#

import networkx as nx
import networkx_backbone as nb

G = nx.les_miserables_graph()

print(f"Original: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges")

Simple filters#

Global threshold: Keep edges whose weight meets a minimum:

scored = nb.global_threshold_filter(G, threshold=1.0)
backbone = nb.boolean_filter(scored, "global_threshold_keep")
print(f"Global threshold: {backbone.number_of_edges()} edges")

Strongest N ties: Keep each node’s N strongest edges:

scored = nb.strongest_n_ties(G, n=2)
backbone = nb.boolean_filter(scored, "strongest_n_ties_keep")
print(f"Strongest 2 ties: {backbone.number_of_edges()} edges")

Global sparsification (Satuluri et al., 2011): Keep the globally strongest fraction of edges:

scored = nb.global_sparsification(G, s=0.4)
backbone = nb.boolean_filter(scored, "global_sparsification_keep")
print(f"Global sparsification (40%): {backbone.number_of_edges()} edges")

Linkage and centrality filters#

Primary linkage analysis (Nystuen & Dacey, 1961): Keep each node’s strongest outgoing edge:

scored = nb.primary_linkage_analysis(G)
backbone = nb.boolean_filter(scored, "primary_linkage_keep")
print(f"Primary linkage: {backbone.number_of_edges()} edges")

Edge betweenness filter (Girvan & Newman, 2002): Keep edges with the highest edge-betweenness centrality:

scored = nb.edge_betweenness_filter(G, s=0.3)
backbone = nb.boolean_filter(scored, "edge_betweenness_keep")
print(f"Edge betweenness (30%): {backbone.number_of_edges()} edges")

Node degree filter: Keep only nodes whose degree meets a threshold:

scored = nb.node_degree_filter(G, min_degree=2)
backbone = nb.boolean_filter(scored, "node_degree_keep")
print(f"Node degree (>=2): {backbone.number_of_nodes()} nodes")

Shortest-path methods#

High salience skeleton (Grady et al., 2012): Each edge receives a salience score – the fraction of shortest-path trees that include it. Edges with high salience are consistently important for communication:

H = nb.high_salience_skeleton(G)

# Filter to keep edges with salience above 0.5
backbone = nb.threshold_filter(H, "salience", 0.5, mode="above")
print(f"High salience (>0.5): {backbone.number_of_edges()} edges")

Metric backbone (Simas et al., 2021): Keeps only edges that lie on shortest paths (using the inverse of weight as distance). Always preserves connectivity:

scored = nb.metric_backbone(G)
backbone = nb.boolean_filter(scored, "metric_keep")
print(f"Metric backbone: {backbone.number_of_edges()} edges")
print(f"Connected: {nx.is_connected(backbone)}")

Ultrametric backbone (Simas et al., 2021): Similar to metric backbone but uses minimax paths instead of shortest paths:

scored = nb.ultrametric_backbone(G)
backbone = nb.boolean_filter(scored, "ultrametric_keep")
print(f"Ultrametric backbone: {backbone.number_of_edges()} edges")

Normalization#

Doubly stochastic filter (Slater, 2009): Normalizes the weight matrix so that each row and column sums to 1 using Sinkhorn-Knopp iteration. Edges with high normalized weight are important:

H = nb.doubly_stochastic_filter(G)

# Filter: keep edges with high doubly-stochastic weight
backbone = nb.threshold_filter(H, "ds_weight", 0.1, mode="above")
print(f"Doubly stochastic (>0.1): {backbone.number_of_edges()} edges")

Index-based#

H-backbone (Zhang et al., 2018): Uses an h-index inspired criterion to identify important edges:

scored = nb.h_backbone(G)
backbone = nb.boolean_filter(scored, "h_backbone_keep")
print(f"H-backbone: {backbone.number_of_edges()} edges")

Community-based#

Modularity backbone (Rajeh et al., 2022): Computes a vitality score for each node based on how much removing it changes the network’s modularity. Use the boolean keep flag to extract retained edges:

scored = nb.modularity_backbone(G)
backbone = nb.boolean_filter(scored, "modularity_keep")
print(f"Modularity backbone: {backbone.number_of_nodes()} nodes")

Constrained methods#

Planar maximally filtered graph (Tumminello et al., 2005): Greedily adds edges from heaviest to lightest while maintaining planarity:

scored = nb.planar_maximally_filtered_graph(G)
backbone = nb.boolean_filter(scored, "pmfg_keep")
print(f"PMFG: {backbone.number_of_edges()} edges")

Maximum spanning tree: Wrapper around NetworkX’s MST algorithm. Always produces a connected tree with exactly N-1 edges:

scored = nb.maximum_spanning_tree_backbone(G)
backbone = nb.boolean_filter(scored, "mst_keep")
print(f"MST: {backbone.number_of_edges()} edges")
print(f"Connected: {nx.is_connected(backbone)}")

Comparing structural methods#

backbones = {
    "metric": nb.boolean_filter(nb.metric_backbone(G), "metric_keep"),
    "ultrametric": nb.boolean_filter(nb.ultrametric_backbone(G), "ultrametric_keep"),
    "h_backbone": nb.boolean_filter(nb.h_backbone(G), "h_backbone_keep"),
    "global_sparsification": nb.boolean_filter(
        nb.global_sparsification(G, s=0.4), "global_sparsification_keep"
    ),
    "edge_betweenness": nb.boolean_filter(
        nb.edge_betweenness_filter(G, s=0.3), "edge_betweenness_keep"
    ),
    "pmfg": nb.boolean_filter(nb.planar_maximally_filtered_graph(G), "pmfg_keep"),
    "mst": nb.boolean_filter(nb.maximum_spanning_tree_backbone(G), "mst_keep"),
}

results = nb.compare_backbones(G, backbones)
for name, metrics in results.items():
    ef = metrics["edge_fraction"]
    nf = metrics["node_fraction"]
    print(f"{name:15s}: edges={ef:.1%}, nodes={nf:.1%}")