-
Notifications
You must be signed in to change notification settings - Fork 229
Add Louvain community detection algorithm #453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
374db7b
6ef3267
76efd88
de9b6a8
385e8c8
78d9225
422d376
6b278b8
28721e1
c5c9ac4
24002db
a02bc0f
0034d3f
e8760cf
7180cd6
fb61051
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,248 @@ | ||
| <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> | ||
| <!-- | ||
| Copyright (c) 2026 Arnaud Becheler | ||
|
|
||
| Distributed under the Boost Software License, Version 1.0. | ||
| (See accompanying file LICENSE_1_0.txt or copy at | ||
| http://www.boost.org/LICENSE_1_0.txt) | ||
| --> | ||
| <HTML> | ||
| <Head> | ||
| <Title>Boost Graph Library: Louvain Clustering</Title> | ||
| </Head> | ||
| <BODY BGCOLOR="#ffffff" LINK="#0000ee" TEXT="#000000" VLINK="#551a8b" | ||
| ALINK="#ff0000"> | ||
| <IMG SRC="../../../boost.png" | ||
| ALT="C++ Boost" width="277" height="86"> | ||
|
|
||
| <BR Clear> | ||
|
|
||
| <H1><A NAME="sec:louvain-clustering"></A> | ||
| <TT>louvain_clustering</TT> | ||
| </H1> | ||
|
|
||
| <PRE> | ||
| template <typename QualityFunction = newman_and_girvan, | ||
| typename Graph, typename ComponentMap, | ||
| typename WeightMap, typename URBG> | ||
| typename property_traits<WeightMap>::value_type | ||
| louvain_clustering(const Graph& g, | ||
| ComponentMap components, | ||
| const WeightMap& w, | ||
| URBG&& gen, | ||
| typename property_traits<WeightMap>::value_type min_improvement_inner = 0, | ||
| typename property_traits<WeightMap>::value_type min_improvement_outer = 0); | ||
| </PRE> | ||
|
|
||
| <P> | ||
| This algorithm implements the Louvain method for community detection | ||
| [<a href="#references">1</a>]. It finds a partition of the vertices into communities | ||
| that approximately maximizes a quality function (by default, | ||
| <a href="louvain_quality_functions.html#newman_and_girvan">Newman–Girvan | ||
| modularity</a>). | ||
|
|
||
| <P>The algorithm alternates two phases: | ||
| <OL> | ||
| <LI><B>Local optimization.</B> Each vertex is moved to the neighboring | ||
| community that yields the largest improvement in the quality function. | ||
| Vertices are visited in random order and the process repeats until no | ||
| single-vertex move improves the quality by more than | ||
| <TT>min_improvement_inner</TT>. | ||
|
|
||
| <LI><B>Aggregation.</B> The graph is contracted by collapsing each | ||
| community into a single super-vertex. Edge weights between | ||
| super-vertices are the sums of the original inter-community edge | ||
| weights and self-loops carry the total intra-community weight. | ||
| </OL> | ||
|
|
||
| <P> These two phases are applied repeatedly on the coarsened graph, | ||
| discovering communities of communities, until | ||
| the quality improvement between successive levels falls below | ||
| <TT>min_improvement_outer</TT>, or the graph can no longer be | ||
| coarsened. | ||
|
|
||
| <P> Once every level has converged, the algorithm iterates | ||
| from the coarsest aggregated graph down to the original graph to | ||
| trace assignment of vertices to communities to produce the final | ||
| community label written into <TT>components</TT>. | ||
|
|
||
| <P> The speed of the local optimization phase depends on the quality | ||
| function's interface. A quality function that only models | ||
| <a href="louvain_quality_functions.html#base_concept"> | ||
| <TT>GraphPartitionQualityFunctionConcept</TT></a> requires a full | ||
| O(V+E) recomputation of the quality for every candidate vertex move. | ||
| A quality function that also models | ||
| <a href="louvain_quality_functions.html#incremental_concept"> | ||
| <TT>GraphPartitionQualityFunctionIncrementalConcept</TT></a> | ||
| evaluates each candidate move in O(1) using incremental | ||
| bookkeeping, making the total cost per vertex O(degree). | ||
| The algorithm detects which interface is available at | ||
| compile time and selects the appropriate code path automatically. | ||
|
|
||
| <H3>Where Defined</H3> | ||
|
|
||
| <P> | ||
| <a href="../../../boost/graph/louvain_clustering.hpp"><TT>boost/graph/louvain_clustering.hpp</TT></a> | ||
|
|
||
| <H3>Parameters</H3> | ||
|
|
||
| IN: <tt>const Graph& g</tt> | ||
| <blockquote> | ||
| An undirected graph. Must model | ||
| <a href="VertexListGraph.html">Vertex List Graph</a> and | ||
| <a href="IncidenceGraph.html">Incidence Graph</a>. | ||
| The graph is not modified by the algorithm. | ||
| Passing a directed graph produces a compile-time error. | ||
| </blockquote> | ||
|
|
||
| OUT: <tt>ComponentMap components</tt> | ||
| <blockquote> | ||
| Records the community each vertex belongs to. After the call, | ||
| <tt>get(components, v)</tt> returns an identifier (a vertex | ||
| descriptor of the original graph) for the community of vertex | ||
| <tt>v</tt>. Two vertices with the same identifier are in the | ||
| same community.<br> | ||
| Must model | ||
| <a href="../../property_map/doc/ReadWritePropertyMap.html">Read/Write | ||
| Property Map</a> with the graph's vertex descriptor as both key | ||
| type and value type. | ||
| </blockquote> | ||
|
|
||
| IN: <tt>const WeightMap& w</tt> | ||
| <blockquote> | ||
| Edge weights. Must model | ||
| <a href="../../property_map/doc/ReadablePropertyMap.html">Readable | ||
| Property Map</a> with the graph's edge descriptor as key type. | ||
| Weights must be non-negative. | ||
| </blockquote> | ||
|
|
||
| IN: <tt>URBG&& gen</tt> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to provide a default arg for this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what it would be. And also it would differ from what i've seen in |
||
| <blockquote> | ||
| A random number generator used to shuffle the vertex processing | ||
| order at each pass. Any type meeting the C++ | ||
| <i>UniformRandomBitGenerator</i> requirements works | ||
| (e.g. <tt>std::mt19937</tt>). | ||
| </blockquote> | ||
|
|
||
| IN: <tt>weight_type min_improvement_inner</tt> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is Louvain guaranteed to converge (i.e. to eventually stop)? If this is not clear, does it make sense to provide additional hard limits on the number of inner/outer iterations?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand well it's guaranteed to terminate in theory:
That being said there is the case of large graphs and the trouble on floating point precision.
Am i making sense ? |
||
| <blockquote> | ||
| The inner loop (local optimization) stops when a full pass over | ||
| all vertices improves quality by less than this value.<br> | ||
| <b>Default:</b> <tt>0</tt> | ||
| </blockquote> | ||
|
|
||
| IN: <tt>weight_type min_improvement_outer</tt> | ||
| <blockquote> | ||
| The outer loop (aggregation) stops when quality improves by less | ||
| than this value between successive levels.<br> | ||
| <b>Default:</b> <tt>0</tt> | ||
| </blockquote> | ||
|
|
||
| <H3>Template Parameters</H3> | ||
|
|
||
| <tt>QualityFunction</tt> | ||
| <blockquote> | ||
| The partition quality metric to maximize. Must model | ||
| <a href="louvain_quality_functions.html#base_concept"> | ||
| <tt>GraphPartitionQualityFunctionConcept</tt></a>. If it also models | ||
| <a href="louvain_quality_functions.html#incremental_concept"> | ||
| <tt>GraphPartitionQualityFunctionIncrementalConcept</tt></a>, the | ||
| faster incremental code path is selected automatically.<br> | ||
| <b>Default:</b> | ||
| <tt><a href="louvain_quality_functions.html#newman_and_girvan">newman_and_girvan</a></tt> | ||
| </blockquote> | ||
|
|
||
| <H3>Return Value</H3> | ||
| <P>The quality (e.g. modularity) of the best partition found. | ||
| For Newman–Girvan modularity this is a value in | ||
| [−0.5, 1). | ||
|
|
||
| <H3>Complexity</H3> | ||
| <P>With the incremental quality function (the default), each local | ||
| optimization pass costs O(E) since every vertex is visited once and | ||
| each visit scans its neighbors. With a non-incremental quality function, | ||
| each candidate move requires a full O(V+E) traversal, making each pass | ||
| O(E · (V+E)). The number of passes per level and the | ||
| number of aggregation levels are both small in practice, so the | ||
| incremental path typically runs in O(E log V) overall on | ||
| sparse graphs. | ||
|
|
||
| <H3>Preconditions</H3> | ||
| <UL> | ||
| <LI>The graph must be undirected (enforced at compile time). | ||
| <LI>Edge weights must be non-negative. | ||
| <LI>The graph must have a <TT>vertex_index</TT> property mapping | ||
| vertices to contiguous integers in | ||
| [0, <TT>num_vertices(g)</TT>). | ||
| </UL> | ||
|
|
||
| <H3>Example</H3> | ||
| <PRE> | ||
| #include <boost/graph/adjacency_list.hpp> | ||
| #include <boost/graph/louvain_clustering.hpp> | ||
| #include <random> | ||
| #include <iostream> | ||
|
|
||
| int main() | ||
| { | ||
| using Graph = boost::adjacency_list< | ||
| boost::vecS, boost::vecS, boost::undirectedS, | ||
| boost::no_property, | ||
| boost::property<boost::edge_weight_t, double>>; | ||
|
|
||
| // Two triangles connected by a weak bridge | ||
| Graph g(6); | ||
| boost::add_edge(0, 1, 1.0, g); | ||
| boost::add_edge(1, 2, 1.0, g); | ||
| boost::add_edge(0, 2, 1.0, g); | ||
| boost::add_edge(3, 4, 1.0, g); | ||
| boost::add_edge(4, 5, 1.0, g); | ||
| boost::add_edge(3, 5, 1.0, g); | ||
| boost::add_edge(2, 3, 0.1, g); | ||
|
|
||
| using vertex_t = boost::graph_traits<Graph>::vertex_descriptor; | ||
| std::vector<vertex_t> communities(boost::num_vertices(g)); | ||
| auto cmap = boost::make_iterator_property_map( | ||
| communities.begin(), boost::get(boost::vertex_index, g)); | ||
|
|
||
| std::mt19937 rng(42); | ||
| double Q = boost::louvain_clustering( | ||
| g, cmap, boost::get(boost::edge_weight, g), rng); | ||
|
|
||
| std::cout << "Modularity: " << Q << "\n"; | ||
| for (auto v : boost::make_iterator_range(boost::vertices(g))) | ||
| std::cout << " vertex " << v | ||
| << " -> community " << boost::get(cmap, v) << "\n"; | ||
| } | ||
| </PRE> | ||
|
|
||
| <H3>See Also</H3> | ||
| <P> | ||
| <a href="louvain_quality_functions.html">Louvain Quality Function Concepts</a>, | ||
| <a href="bc_clustering.html"><TT>betweenness_centrality_clustering</TT></a> | ||
|
|
||
| <H3>References</H3> | ||
| <a name="references"></a> | ||
| <P>[1] V. D. Blondel, J.‑L. Guillaume, | ||
| R. Lambiotte, and E. Lefebvre, | ||
| “Fast unfolding of communities in large networks,” | ||
| <i>Journal of Statistical Mechanics: Theory and Experiment</i>, | ||
| vol. 2008, no. 10, P10008, 2008. | ||
| <a href="https://doi.org/10.1088/1742-5468/2008/10/P10008">doi:10.1088/1742-5468/2008/10/P10008</a> | ||
|
|
||
| <P>[2] V. A. Traag, L. Waltman, and | ||
| N. J. van Eck, | ||
| “From Louvain to Leiden: guaranteeing well-connected communities,” | ||
| <i>Scientific Reports</i>, vol. 9, 5233, 2019. | ||
| <a href="https://doi.org/10.1038/s41598-019-41695-z">doi:10.1038/s41598-019-41695-z</a> | ||
|
|
||
| <BR> | ||
| <HR> | ||
| <TABLE> | ||
| <TR valign=top> | ||
| <TD nowrap>Copyright © 2026</TD><TD> | ||
| Arnaud Becheler | ||
| </TD></TR></TABLE> | ||
|
|
||
| </BODY> | ||
| </HTML> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The algorithm has the additional requirement that vertices are copyable, hashable etc., as they're internally stored in unordered_sets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I have been changing the vertices handling in this aspect because it was not friendly with some types of graphs. The interface now takes a VertexIndexMap but I still have to commit those changes, sorry 😓
I will update the documentation in that sense once I merged the new stuff