16_centrality_analytics

command
v0.17.0 Latest Latest
Warning

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

Go to latest
Published: Oct 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 network, one directed relationship per edge.
  • graph/csr.BuildFromAdjList / CSR.BuildSymmetric — freeze the builder into an immutable CSR snapshot and project it to the symmetric (undirected) snapshot both analytics read.
  • 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 analysed as UNDIRECTED: each edge is stored once, as one directed relationship, and both analytics read the symmetric projection built by csr.CSR.BuildSymmetric. 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