Chapter 24 · Graph Algorithms: Centrality, Community Detection, Similarity, Paths, and Topology Analytics
Louvain/Leiden/Label Propagation and Community Detection: Resolution, Stability, and Validation
Run Louvain, Leiden, and Label Propagation on a bounded AtlasMart product graph, test resolution/randomness/stability, and treat community IDs as analytical partition labels rather than business truth.
Learning outcomes
Distinguish Louvain, Leiden, and Label Propagation by objective/mechanism rather than by name recognition.
Explain modularity, resolution, hierarchy, randomness, seed properties, convergence, and community identifiers.
Run bounded community algorithms with reproducible parameters and inspect distributions instead of only one partition.
Measure partition stability across algorithm/parameter choices without treating community IDs as semantic labels.
Document when a community result is exploratory structure versus evidence safe enough for downstream decisions.
1. AtlasMart problem: merchandising segments appear in the graph, but are they stable?
AtlasMart wants product “communities” for catalog navigation. A dense group of products may be useful, but community detection does not discover an objective business taxonomy. Louvain and Leiden optimize modularity-style structure; Label Propagation spreads neighbor labels until a stable labeling or iteration limit. Their outputs depend on graph scope, orientation, weights, parameters, and sometimes randomness.
The safe question is not “what are the true communities?” It is “does this projection contain stable, interpretable structural groups that improve a measured decision?”
| Dimension | Chapter 24 reproducible assumption |
|---|---|
| Neo4j |
2026.07.1 Community, disposable local/container
deployment; database neo4j.
|
| Cypher | Cypher 25 for course examples; Cypher 5 differences are not needed for the GDS procedures used here. |
| Java | Java 21/25 supported by the 2026 line; container image supplies the runtime. |
| Auth/TLS |
User neo4j, password
atlasmart-course-2026; loopback Bolt without
TLS only for this isolated lab.
|
| Driver | No application driver is required for mandatory GDS procedure labs; cypher-shell/Browser is sufficient. |
| GDS | GDS Community 2026.07.0. Community includes all algorithms; maximum GDS concurrency is four CPU cores. |
| APOC | Not required. |
| Graph scope |
Only identifiers prefixed CH24- plus named
in-memory projections beginning
atlas-ch24- are created.
|
| Runtime evidence | This artifact does not execute Neo4j/GDS. Exact fixture counts and hand calculations are deterministic; timings, memory estimates, and algorithm scores must be measured locally. |
// Run in the disposable atlasmart-gds lab from Chapter 23.
RETURN gds.version() AS gdsVersion;
CALL dbms.components() YIELD name, versions, edition
RETURN name, versions, edition;
// Defensive cleanup of Chapter 24 projections if rerunning.
CALL gds.graph.list()
YIELD graphName
WITH graphName WHERE graphName STARTS WITH 'atlas-ch24-'
CALL gds.graph.drop(graphName) YIELD graphName AS dropped
RETURN dropped;
2. Community identifiers are opaque partition labels
| Concept | Precise meaning |
|---|---|
| community ID | An algorithm-produced identifier grouping nodes in one run; do not attach permanent business semantics to its numeric value. |
| modularity | A quality measure comparing within-community density against a random-network expectation. Higher is not automatically “better business segmentation.” |
| resolution |
Controls the granularity of detected groups. In Leiden,
gamma is the explicit resolution parameter;
higher values tend toward more communities.
|
| randomness |
Leiden uses randomized refinement and exposes
randomSeed; Louvain and LPA have different
control surfaces, so do not invent one universal seed
parameter.
|
| seedProperty | Optional existing node property used as initial communities/labels in supported algorithms; seeding changes the optimization start state. |
| stability | Similarity of partitions under reruns, parameter changes, refreshed data, or algorithm family changes—not merely “did the procedure finish.” |
3. Create two dense product groups with one weak bridge
// Remove only this lesson's synthetic nodes if rerunning after Lesson 1 cleanup.
MATCH (n:Product) WHERE n.productId STARTS WITH 'CH24-COM-' DETACH DELETE n;
UNWIND range(1,8) AS i
CREATE (:Product:CH24CommunityProduct {
productId:'CH24-COM-' + toString(i),
name:'Community Product ' + toString(i),
chapter24:true
});
MATCH (p1:Product {productId:'CH24-COM-1'}), (p2:Product {productId:'CH24-COM-2'}),
(p3:Product {productId:'CH24-COM-3'}), (p4:Product {productId:'CH24-COM-4'}),
(p5:Product {productId:'CH24-COM-5'}), (p6:Product {productId:'CH24-COM-6'}),
(p7:Product {productId:'CH24-COM-7'}), (p8:Product {productId:'CH24-COM-8'})
CREATE (p1)-[:RELATED_TO {strength:5.0}]->(p2),
(p2)-[:RELATED_TO {strength:5.0}]->(p3),
(p3)-[:RELATED_TO {strength:5.0}]->(p4),
(p4)-[:RELATED_TO {strength:5.0}]->(p1),
(p1)-[:RELATED_TO {strength:4.0}]->(p3),
(p5)-[:RELATED_TO {strength:5.0}]->(p6),
(p6)-[:RELATED_TO {strength:5.0}]->(p7),
(p7)-[:RELATED_TO {strength:5.0}]->(p8),
(p8)-[:RELATED_TO {strength:5.0}]->(p5),
(p5)-[:RELATED_TO {strength:4.0}]->(p7),
(p4)-[:RELATED_TO {strength:0.5}]->(p5);
CALL gds.graph.project(
'atlas-ch24-community',
'CH24CommunityProduct',
{RELATED_TO:{orientation:'UNDIRECTED', properties:['strength']}}
)
YIELD graphName, nodeCount, relationshipCount
RETURN graphName, nodeCount, relationshipCount;
If Lesson 1 Product nodes still exist, the label-only projection will include them too. Either run the Chapter cleanup before this lesson, or replace the simple native projection with a filtered Cypher projection. A bounded analytical question requires a bounded projection.
4. Run Louvain, Leiden, and Label Propagation as competing hypotheses
CALL gds.louvain.stats('atlas-ch24-community', {
relationshipWeightProperty:'strength',
maxLevels:10,
maxIterations:10,
tolerance:0.0001,
concurrency:2
}) YIELD communityCount, modularity, ranLevels, communityDistribution
RETURN *;
CALL gds.louvain.stream('atlas-ch24-community', {
relationshipWeightProperty:'strength', concurrency:2
}) YIELD nodeId, communityId
RETURN gds.util.asNode(nodeId).productId AS productId, communityId
ORDER BY communityId, productId;
CALL gds.leiden.stream('atlas-ch24-community', {
relationshipWeightProperty:'strength',
gamma:1.0,
theta:0.01,
randomSeed:24,
concurrency:2
}) YIELD nodeId, communityId
RETURN gds.util.asNode(nodeId).productId AS productId, communityId
ORDER BY communityId, productId;
CALL gds.labelPropagation.stream('atlas-ch24-community', {
relationshipWeightProperty:'strength',
maxIterations:10,
concurrency:2
}) YIELD nodeId, communityId
RETURN gds.util.asNode(nodeId).productId AS productId, communityId
ORDER BY communityId, productId;
The fixture is designed with two dense four-node blocks and one weak bridge, so “two groups” is a reasonable structural hypothesis to check. Do not hard-code numeric community IDs in an acceptance test; IDs are labels, not stable domain keys.
5. Resolution and randomness need explicit experiments
UNWIND [0.5, 1.0, 2.0] AS gamma
CALL gds.leiden.stats('atlas-ch24-community', {
relationshipWeightProperty:'strength',
gamma:gamma,
theta:0.01,
randomSeed:24,
concurrency:2
})
YIELD communityCount, modularity
RETURN gamma, communityCount, modularity
ORDER BY gamma;
UNWIND [11,24,37] AS seed
CALL gds.leiden.stats('atlas-ch24-community', {
relationshipWeightProperty:'strength',
gamma:1.0,
randomSeed:seed,
concurrency:2
})
YIELD communityCount, modularity
RETURN seed, communityCount, modularity
ORDER BY seed;
Do not assume higher modularity or more communities is automatically a better product taxonomy. Record whether the partition is stable enough for the intended downstream use and compare it with human judgments or a known fixture.
6. Compare partitions without comparing raw IDs
A simple hand-checkable stability measure is pair agreement: for every pair of products, ask whether each run puts the pair in the same community. Compare that Boolean relation across runs. This avoids pretending that community ID 17 from one run must equal community ID 17 from another.
from itertools import combinations
def pair_signature(assignments):
# assignments: {product_id: opaque_community_id}
return {
tuple(sorted((a, b))): assignments[a] == assignments[b]
for a, b in combinations(assignments, 2)
}
def agreement(a, b):
sa, sb = pair_signature(a), pair_signature(b)
keys = sa.keys() & sb.keys()
return sum(sa[k] == sb[k] for k in keys) / len(keys)
# Feed this with actual streamed assignments from two runs.
# Do not paste invented community IDs into the course evidence.
7. Deliberately wrong: rename community IDs as “customer segments”
A common failure is to persist communityId, display
“Segment 42,” and let business workflows depend on it. A
refreshed graph may split or merge groups; a parameter change
may relabel IDs; a different algorithm may optimize another
objective.
Repair by treating the partition as a versioned analytical artifact: store algorithm, projection fingerprint, parameter set, source-data cutoff, evaluation metrics, and a separate governed business-segment key only after validation. If the product needs stable segmentation, stability itself is a requirement to measure.
Production judgment and bridge
| Question | Required evidence |
|---|---|
| Does the graph have meaningful community structure? | Compare against random/baseline partitions, known fixtures, modularity/distribution, and domain judgments. |
| Is the partition stable? | Rerun/refit under data refreshes and parameter/seed changes; compare pair membership, not numeric IDs. |
| Do weights mean what the algorithm assumes? | Document source and scale of non-negative relationship weights; run unweighted baseline. |
| Can the system afford refresh? | Projection + estimate + measured compute time/memory/concurrency at expected cadence. |
| Can a community drive a decision? | Only after external validation; communities are structural association, not causal or protected-class truth. |
Next, AtlasMart shifts from grouping a whole graph to generating pairwise candidates: “which customers or products are similar?” That exposes the difference between structural Node Similarity and property-based KNN.
Check your understanding
- Why should community IDs not be compared directly across runs?
-
Which of these three algorithms exposes an explicit
randomSeedin current GDS? - What does Leiden
gammacontrol? - Why run an unweighted baseline?
- What business claim follows automatically from two dense graph groups?
Review the answers
1. They are opaque partition labels; compare node co-membership or other partition metrics instead.
2. Leiden. Do not invent a shared randomness parameter model for Louvain and Label Propagation.
3. Resolution/granularity of modularity optimization; changing it can change how many communities appear.
4. To show whether the chosen relationship weights materially drive the partition and to catch bad weight semantics.
5. None. They are structural groups that require separate interpretation and validation.
// Drop all Chapter 24 in-memory projections first.
CALL gds.graph.list()
YIELD graphName
WITH graphName WHERE graphName STARTS WITH 'atlas-ch24-'
CALL gds.graph.drop(graphName) YIELD graphName AS dropped
RETURN dropped;
// Then delete only synthetic Chapter 24 persisted fixtures.
MATCH (n)
WHERE n.chapter24 = true
DETACH DELETE n;
MATCH (n) WHERE n.chapter24 IS NOT NULL REMOVE n.chapter24;
RETURN 'Chapter 24 reset complete' AS status;
Summary and next step
Louvain/Leiden/Label Propagation and Community Detection: Resolution, Stability, and Validation is useful only when its assumptions and observed evidence stay attached to the decision. The examples above establish a reproducible mechanism and boundary; they do not turn one lab result into a universal production rule.
Next, continue to Node Similarity, KNN, Jaccard/Overlap/Cosine Concepts, and Candidate Generation. Carry forward the verified assumptions, fixture state, version/edition boundaries, and measurements from this lesson instead of treating the next topic as an isolated recipe.
Authoritative references
- GDS Manual v2026.07 — Current Graph Data Science manual and release baseline.
- GDS supported Neo4j versions — Compatibility matrix mapping Neo4j 2026.07 to GDS 2026.07.
- GDS editions — Community includes all algorithms but caps concurrency at four CPU cores and the model catalog at three models; Enterprise adds operational capabilities.
- Algorithm syntax and execution modes — stream/stats/mutate/write/estimate semantics and common algorithm configuration.
- Memory estimation — Estimate memory before allocating algorithm working state.
- Centrality algorithms — Current centrality family and quality tiers.
- Degree Centrality — Degree semantics and weighted/directed behavior.
- PageRank — PageRank iteration, damping, weights, and execution modes.
- Betweenness Centrality — Exact/sampled betweenness, samplingSize/samplingSeed, memory and concurrency tradeoffs.
- Eigenvector Centrality — Transitive influence and power-iteration semantics.
- Louvain — Modularity, levels, tolerance, seeding and execution modes.
- Leiden — Community detection with gamma, theta, randomSeed and connectivity refinement.
- Label Propagation — Fast topology-driven community detection, optional weights and seeds.
- Node Similarity — Structural neighborhood similarity with Jaccard, Overlap, or Cosine.
- K-Nearest Neighbors — Property-based approximate KNN and supported metrics.
- Path finding algorithms — Current shortest path, traversal, spanning, and flow families.
- Dijkstra Source-Target — Positive-cost shortest path semantics; sourceNode/targetNodes current syntax.
- Breadth First Search — Reachability traversal, targets, maxDepth, and stream/mutate modes.
- Graph algorithm operations reference — Current procedure/function names including spanningTree and similarity functions.
- Neo4j current versions — Current Neo4j database release and 5.26 LTS line.