Advanced

The basic interface explained in the Introduction should provide you enough to start detecting communities. However, perhaps you want to improve the partitions further or want to do some more advanced analysis. In this section, we will explain this in more detail.

Optimiser

Although the package provides simple access to the function find_partition(), there is actually an underlying Optimiser class that is doing the actual work. We can also explicitly construct an Optimiser object:

>>> optimiser = louvain.Optimiser()

The function find_partition() then does nothing else then calling optimise_partition() on the provided partition.

>>> diff = optimiser.optimise_partition(partition)

But optimise_partition() simply tries to improve any provided partition. We can thus try to repeatedly call optimise_partition() to keep on improving the current partition:

>>> G = ig.Graph.Erdos_Renyi(100, p=5./100)
>>> partition = louvain.ModularityVertexPartition(G)
>>> improv = 1
>>> while improv > 0:
...   improv = optimiser.optimise_partition(partition)

Even if a call to optimise_partition() did not improve the current partition, it is still possible that a next call will improve the partition. Of course, if the current partition is already optimal, this will never happen, but it is not possible to decide whether a partition is optimal.

The optimise_partition() itself is built on a basic algorithm: move_nodes(). You can also call this function yourself. For example:

>>> diff = optimiser.move_nodes(partition)

The usual strategy in the Louvain algorithm is then to aggregate the partition and repeat the move_nodes() on the aggregated partition. We can easily repeat that:

>>> partition = louvain.ModularityVertexPartition(G)
>>> while optimiser.move_nodes(partition) > 0:
...   partition = partition.aggregate_partition()

This summarises the whole Louvain algorithm in just three lines of code. Although this finds the final aggregate partition, this leaves it unclear the actual partition on the level of the individual nodes. In order to do that, we need to update the membership based on the aggregate partition, for which we use the function from_coarse_partition().

>>> partition = louvain.ModularityVertexPartition(G)
>>> partition_agg = partition.aggregate_partition()
>>> while optimiser.move_nodes(partition_agg):
...   partition.from_coarse_partition(partition_agg)
...   partition_agg = partition_agg.aggregate_partition()

Now partition_agg contains the aggregate partition and partition contains the actual partition of the original graph G. Of course, partition_agg.quality() == partition.quality() (save some rounding).

The function move_nodes() in turn relies on two key functions of the partition: diff_move() and move_node(). The first calculates the difference when moving a node, and the latter actually moves the node, and updates all necessary internal administration. The move_nodes() then does something as follows

>>> for v in G.vs:
...   best_comm = max(range(len(partition)),
...                   key=lambda c: partition.diff_move(v.index, c))
...   partition.move_node(v.index, best_comm)

The actual implementation is more complicated, but this gives the general idea.

Resolution profile

Some methods accept so-called resolution parameters, such as CPMVertexPartition or RBConfigurationVertexPartition. Although some method may seem to have some ‘natural’ resolution, in reality this is often quite arbitrary. However, the methods implemented here (which depend in a linear way on resolution parameters) allow for an effective scanning of a full range for the resolution parameter. In particular, these methods somehow can be formulated as \(Q = E - \gamma N\) where \(E\) and \(N\) are some other quantities. In the case for CPMVertexPartition for example, \(E = \sum_c m_c\) is the number of internal edges and \(N = \sum_c \binom{n_c}{2}\) is the sum of the internal possible edges. The essential insight for these formulations 1 is that if there is an optimal partition for both \(\gamma_1\) and \(\gamma_2\) then the partition is also optimal for all \(\gamma_1 \leq \gamma \leq \gamma_2\).

Such a resolution profile can be constructed using the Optimiser object.

>>> G = ig.Graph.Famous('Zachary')
>>> optimiser = louvain.Optimiser()
>>> profile = optimiser.resolution_profile(G, louvain.CPMVertexPartition,
...                                        resolution_range=(0,1))

Plotting the resolution parameter versus the total number of internal edges we thus obtain something as follows:

_images/resolution_profile.png

Now profile contains a list of partitions of the specified type (CPMVertexPartition in this case) for resolution parameters at which there was a change. In particular, profile[i] should be better until profile[i+1], or stated otherwise for any resolution parameter between profile[i].resolution_parameter and profile[i+1].resolution_parameter the partition at position i should be better. Of course, there will be some variations because optimise_partition() will find partitions of varying quality. The change points can then also vary for different runs.

This function repeatedly calls optimise_partition() and can therefore require a lot of time. Especially for resolution parameters right around a change point there may be many possible partitions, thus requiring a lot of runs.

References

1

Traag, V. A., Krings, G., & Van Dooren, P. (2013). Significant scales in community structure. Scientific Reports, 3, 2930. 10.1038/srep02930