16_centrality_analytics

command
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 18 Imported by: 0

README

Example 16 — Centrality analytics

What it demonstrates

A suite of analytics over one shared, immutable CSR snapshot: exact betweenness centrality via Brandes' algorithm (search/centrality.BetweennessCtx); four complementary whole-graph centralities — closeness and harmonic (distance-based), eigenvector and Katz (spectral / walk-based); and label-propagation community detection (search/community.LabelPropagationCtx). It shows how the measures rank different nodes as "central" (a betweenness bottleneck is not the same as a node with the highest closeness or eigenvector score), how to make output deterministic in the face of structural ties (betweenness breaks score ties by node id), and reports per-analysis evidence (wall-clock, transient allocations, live heap). A final pass rebuilds the graph with one bridge removed and runs the distance-based centralities on the resulting disconnected graph, demonstrating they stay finite (no NaN/Inf) and that the Wasserman-Faust closeness normalisation rewards reaching more of the whole graph rather than being trapped in a small component.

A practical lesson surfaces here: eigenvector centrality converges slowly on a modular graph. Power iteration converges at a rate set by the gap between the top two adjacency eigenvalues, and on several dense clusters of near-equal size those eigenvalues are close, so the NetworkX-compatible default of 100 iterations is not enough — this chain of clusters needs about 800. That is a property of the graph's spectrum, not a defect; the example raises the iteration budget so the measure converges and reports the realised iteration count as telemetry.

Domain / scenario

A seeded synthetic network shaped as a chain of dense clusters joined by single bridge edges:

C0 == C1 == C2 == … == C(K-1)

Each cluster is a dense Erdős–Rényi subgraph laid down on top of a random spanning tree (the tree guarantees the cluster is one connected component; the extra edges at -intra-density make it dense). Consecutive clusters are joined by exactly one bridge edge, between the right gateway of cluster c and the left gateway of cluster c+1.

The topology serves both analytics on purpose:

  • Betweenness concentrates on the gateways. Each bridge is the unique edge across its cut, so every shortest path between the two sides must traverse both gateway endpoints — the gateways carry Θ(n_c²) pair-dependencies while interior nodes carry only O(n_c). The gateways are therefore the betweenness winners for every seed: this is a theorem of the cut structure, not a heuristic. A chain (not a ring) is used so the inter-cluster shortest path is unique, keeping the winners unambiguous.
  • Label propagation keeps each dense cluster as one label (the single bridge cannot out-vote a node's many intra-cluster neighbours) and the near-zero inter-cluster density is the stable regime that stops a label flooding across. It does, however, intermittently merge two whole adjacent clusters across a bridge — the documented "monster community" coarsening of synchronous label propagation (Raghavan–Albert–Kumara 2007; Leung et al. 2009), which the example reports honestly rather than tuning away.

How to run

go run ./examples/16_centrality_analytics                                       # small deterministic default
go run ./examples/16_centrality_analytics -communities 20 -nodes 200 -seed 7    # observable-scale run

Scale and flags

Flag Meaning Default Large
-communities number of clusters in the chain 6 20
-nodes nodes per cluster (≥ 2: two are gateways) 50 200
-intra-density Erdős–Rényi p_in for extra intra-cluster edges, in [0,1] 0.30 0.20
-top-k how many top-betweenness node ids to report 10 40
-seed RNG seed (fixes the data shape exactly) 1 any

The default builds 300 nodes and ~1.5k edges. Exact Brandes is O(V·E), so the default is deliberately small (it runs in milliseconds); the large run pushes the betweenness pass into seconds where its cost is observable.

Expected output

At the default config the deterministic fact lines are:

config.communities=6
config.nodes_per_community=50
config.intra_density=0.3
config.top_k=10
config.seed=1
nodes.total=300
edges.total=4836
nodes.gateways=10
betweenness.top1=101
betweenness.top2=150
betweenness.top3=151
betweenness.top4=100
betweenness.top5=200
betweenness.top6=51
betweenness.top7=201
betweenness.top8=50
betweenness.top9=250
betweenness.top10=1
communities.count=5
communities.sizes=[50 50 50 50 100]
centrality.closeness.top=101
centrality.harmonic.top=101
centrality.eigenvector.top=26
centrality.katz.top=161
centrality.eigenvector_converged=true
centrality.katz_converged=true
disconnected.components=2
disconnected.closeness_finite=true
disconnected.harmonic_finite=true
disconnected.closeness_top_in_large_component=true

The distance-based measures (closeness, harmonic) crown a node near the chain's centre, while eigenvector and Katz favour a node inside a dense cluster — the measures genuinely disagree on "most central". The regression test pins the boolean invariants (convergence, finiteness, the two-component split, the Wasserman-Faust reward) and range-checks the top-node ids rather than pinning their exact values, which are seed-dependent.

The ten betweenness.top* ids are exactly the ten bridge gateways {1, 50, 51, 100, 101, 150, 151, 200, 201, 250}; their order varies with the seed (gateways tie on score), but the set does not. communities.count is 5 here because label propagation merged two adjacent clusters into one community of 100; at other seeds it is 6 with sizes [50 50 50 50 50 50].

Interleaved with the facts are volatile telemetry lines, prefixed with # , for example:

# betweenness.elapsed=8.979ms
# betweenness.mallocs=12
# communities.elapsed=544µs
# mem.heap_alloc=314.78 KiB

Telemetry varies per run and per machine; the regression test pins the fact lines and ignores every # line.

Evidence it collects

For the centrality/community subject (per docs/examples-standard.md): the per-analysis wall-clock (# betweenness.elapsed, # communities.elapsed), transient allocations (# *.mallocs, the runtime.MemStats.Mallocs delta around each pass), and live heap (# mem.heap_alloc, # mem.heap_growth). Scale it up with -communities / -nodes and watch the betweenness elapsed grow as O(V·E) while the allocation counts stay flat (the algorithms reuse buffers), and watch communities.count fluctuate in [K/2, K] as label propagation coarsens the chain.

Key APIs

  • graph/adjlist.New / AdjList.AddEdge — build the mutable undirected network.
  • graph/csr.BuildFromAdjList — freeze the builder into an immutable CSR snapshot for analytics.
  • graph/adjlist.AdjList.Mapper / graph.Mapper.Resolve — translate compact NodeIDs back to user-facing node ids.
  • search/centrality.BetweennessCtx — exact Brandes betweenness centrality, returned as a NodeID-indexed []float64.
  • search/centrality.ClosenessCtx / HarmonicCtx — distance-based centralities; Closeness uses the Wasserman-Faust normalisation (finite on disconnected graphs), Harmonic sums 1/d over reachable nodes.
  • search/centrality.EigenvectorCtx / EigenvectorOptions — eigenvector centrality by power iteration; returns the score slice, the iteration count, and ErrMaxStepsExceeded when the budget is exhausted before convergence.
  • search/centrality.KatzCtx / DefaultKatzOptions — Katz centrality with an auto-selected attenuation factor.
  • search.WCC — weakly-connected components, used to confirm the disconnected variant splits into exactly two components.
  • search/community.LabelPropagationCtx / DefaultLabelPropagationOptions — community detection; Partition.Community is a NodeID-indexed slice of community IDs and Partition.NumCommunities counts the live communities.

Further reading

Documentation

Overview

Example 16_centrality_analytics — runs a suite of analytics over one shared, immutable CSR snapshot: exact Brandes betweenness centrality, four complementary whole-graph centralities (closeness and harmonic, distance-based; eigenvector and Katz, spectral/walk-based), and label-propagation community detection — with deterministic tie-breaking, and reports per-analysis evidence. A final pass rebuilds the graph with one bridge removed and runs the distance-based centralities on the resulting DISCONNECTED graph, showing they stay finite (no NaN/Inf) and that the Wasserman-Faust closeness normalisation rewards reaching more of the graph.

The example builds a seeded, scale-parametrised synthetic graph with the mutable adjlist builder, freezes it into an immutable CSR snapshot, then runs both analytics against that one snapshot. It reports the deterministic results (the top-k betweenness node ids, the number of communities found, and that partition's size distribution) as bare fact lines, and the volatile cost of each analysis (wall-clock, transient allocations, live heap) as "# "-prefixed telemetry.

Topology — why this generator

The generator was chosen on the advice of the graph-theory-expert sub-agent, so that BOTH analytics produce a meaningful, dramatically non-uniform result from one graph: betweenness must concentrate on a few obvious cut vertices, and label propagation must recover a sensible partition without collapsing to one giant label or fragmenting.

The graph is a CHAIN of dense clusters joined by single bridge edges:

	C0 == C1 == C2 == … == C(K-1)

  - Each cluster is a dense Erdős–Rényi-style subgraph laid down on top of
    a random spanning tree. The spanning tree GUARANTEES the cluster is one
    connected component (no reliance on luck), and the extra edges at
    intra-density p-in make it internally dense — dense enough that label
    propagation keeps each cluster as a single distinct label rather than
    fragmenting it.
  - Consecutive clusters are joined by exactly ONE bridge edge, between the
    right gateway of cluster c and the left gateway of cluster c+1. A single
    edge across the cut keeps the effective inter-cluster density near zero,
    which is the most stable regime for label propagation (no dense
    inter-coupling for a label to flood across), so the clusters survive as
    ~K separate communities.

The gateways being the betweenness winners is a theorem here, not a heuristic: each bridge is the UNIQUE edge across its cut, so every shortest path between the two sides must traverse both gateway endpoints. Each gateway therefore carries Θ(n_c^2) pair-dependencies while every interior node carries only O(n_c), so the gateways dominate the betweenness ranking for every seed — the argument depends only on the fixed cut structure, not on the random intra-cluster edges. A CHAIN (rather than a ring) is used on purpose: a ring gives two equal-length arcs between far clusters, which splits the pair-dependency between them and blurs the signal; the chain gives a unique inter-cluster path, so the betweenness winners are maximally unambiguous and test-assertable.

The graph is built UNDIRECTED: Brandes betweenness is classically read on undirected graphs, and label propagation is defined on undirected neighbourhoods. Both analytics here are unweighted (Brandes counts shortest-path hops; label propagation counts neighbour labels), so the edges carry no weight — the snapshot is purely structural.

Scale

Run with no flags, the example builds a small deterministic default (6 clusters of 50 nodes at intra-density 0.30 — 300 nodes, roughly 1.5k edges) that the regression test pins and that runs in milliseconds. Exact Brandes is O(V*E), so the default is deliberately small. Every dimension is a flag, so the same binary scales up to a size where the per-analysis cost is actually observable:

go run ./examples/16_centrality_analytics -communities 20 -nodes 200 -intra-density 0.2 -seed 7

That is ~4 000 nodes and tens of thousands of edges; because exact Brandes is O(V*E), pushing the cluster size into the hundreds moves the betweenness pass into seconds. The deterministic facts are reproducible for a fixed -seed; only the telemetry (lines prefixed with "# ") varies between runs and machines.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL