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%}")