UNPKG

graphology-communities-louvain

Version:

Louvain community detection for graphology.

785 lines (626 loc) 22.8 kB
/** * Graphology Louvain Algorithm * ============================= * * JavaScript implementation of the famous Louvain community detection * algorithm for graphology. * * [Articles] * M. E. J. Newman, « Modularity and community structure in networks », * Proc. Natl. Acad. Sci. USA, vol. 103, no 23, 2006, p. 8577–8582 * https://dx.doi.org/10.1073%2Fpnas.0601602103 * * Newman, M. E. J. « Community detection in networks: Modularity optimization * and maximum likelihood are equivalent ». Physical Review E, vol. 94, no 5, * novembre 2016, p. 052315. arXiv.org, doi:10.1103/PhysRevE.94.052315. * https://arxiv.org/pdf/1606.02319.pdf * * Blondel, Vincent D., et al. « Fast unfolding of communities in large * networks ». Journal of Statistical Mechanics: Theory and Experiment, * vol. 2008, no 10, octobre 2008, p. P10008. DOI.org (Crossref), * doi:10.1088/1742-5468/2008/10/P10008. * https://arxiv.org/pdf/0803.0476.pdf * * Nicolas Dugué, Anthony Perez. Directed Louvain: maximizing modularity in * directed networks. [Research Report] Université d’Orléans. 2015. hal-01231784 * https://hal.archives-ouvertes.fr/hal-01231784 * * R. Lambiotte, J.-C. Delvenne and M. Barahona. Laplacian Dynamics and * Multiscale Modular Structure in Networks, * doi:10.1109/TNSE.2015.2391998. * https://arxiv.org/abs/0812.1770 * * Traag, V. A., et al. « From Louvain to Leiden: Guaranteeing Well-Connected * Communities ». Scientific Reports, vol. 9, no 1, décembre 2019, p. 5233. * DOI.org (Crossref), doi:10.1038/s41598-019-41695-z. * https://arxiv.org/abs/1810.08473 */ var resolveDefaults = require('graphology-utils/defaults'); var isGraph = require('graphology-utils/is-graph'); var inferType = require('graphology-utils/infer-type'); var SparseMap = require('mnemonist/sparse-map'); var SparseQueueSet = require('mnemonist/sparse-queue-set'); var createRandomIndex = require('pandemonium/random-index').createRandomIndex; var indices = require('graphology-indices/louvain'); var UndirectedLouvainIndex = indices.UndirectedLouvainIndex; var DirectedLouvainIndex = indices.DirectedLouvainIndex; var DEFAULTS = { nodeCommunityAttribute: 'community', getEdgeWeight: 'weight', fastLocalMoves: true, randomWalk: true, resolution: 1, rng: Math.random }; function addWeightToCommunity(map, community, weight) { var currentWeight = map.get(community); if (typeof currentWeight === 'undefined') currentWeight = 0; currentWeight += weight; map.set(community, currentWeight); } var EPSILON = 1e-10; function tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ) { if (Math.abs(delta - bestDelta) < EPSILON) { if (bestCommunity === currentCommunity) { return false; } else { return targetCommunity > bestCommunity; } } else if (delta > bestDelta) { return true; } return false; } function undirectedLouvain(detailed, graph, options) { var index = new UndirectedLouvainIndex(graph, { getEdgeWeight: options.getEdgeWeight, keepDendrogram: detailed, resolution: options.resolution }); var randomIndex = createRandomIndex(options.rng); // State variables var moveWasMade = true, localMoveWasMade = true; // Communities var currentCommunity, targetCommunity; var communities = new SparseMap(Float64Array, index.C); // Traversal var queue, start, end, weight, ci, ri, s, i, j, l; // Metrics var degree, targetCommunityDegree; // Moves var bestCommunity, bestDelta, deltaIsBetter, delta; // Details var deltaComputations = 0, nodesVisited = 0, moves = [], localMoves, currentMoves; if (options.fastLocalMoves) queue = new SparseQueueSet(index.C); while (moveWasMade) { l = index.C; moveWasMade = false; localMoveWasMade = true; if (options.fastLocalMoves) { currentMoves = 0; // Traversal of the graph ri = options.randomWalk ? randomIndex(l) : 0; for (s = 0; s < l; s++, ri++) { i = ri % l; queue.enqueue(i); } while (queue.size !== 0) { i = queue.dequeue(); nodesVisited++; degree = 0; communities.clear(); currentCommunity = index.belongings[i]; start = index.starts[i]; end = index.starts[i + 1]; // Traversing neighbors for (; start < end; start++) { j = index.neighborhood[start]; weight = index.weights[start]; targetCommunity = index.belongings[j]; // Incrementing metrics degree += weight; addWeightToCommunity(communities, targetCommunity, weight); } // Finding best community to move to bestDelta = index.fastDeltaWithOwnCommunity( i, degree, communities.get(currentCommunity) || 0, currentCommunity ); bestCommunity = currentCommunity; for (ci = 0; ci < communities.size; ci++) { targetCommunity = communities.dense[ci]; if (targetCommunity === currentCommunity) continue; targetCommunityDegree = communities.vals[ci]; deltaComputations++; delta = index.fastDelta( i, degree, targetCommunityDegree, targetCommunity ); deltaIsBetter = tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ); if (deltaIsBetter) { bestDelta = delta; bestCommunity = targetCommunity; } } // Should we move the node? if (bestDelta < 0) { // NOTE: this is to allow nodes to move back to their own singleton // This code however only deals with modularity (e.g. the condition // about bestDelta < 0, which is the delta for moving back to // singleton wrt. modularity). Indeed, rarely, the Louvain // algorithm can produce such cases when a node would be better in // a singleton that in its own community when considering self loops // or a resolution != 1. In this case, delta with your own community // is indeed less than 0. To handle different metrics, one should // consider computing the delta for going back to singleton because // it might not be 0. bestCommunity = index.isolate(i, degree); // If the node was already in a singleton community, we don't consider // a move was made if (bestCommunity === currentCommunity) continue; } else { // If no move was made, we continue to next node if (bestCommunity === currentCommunity) { continue; } else { // Actually moving the node to a new community index.move(i, degree, bestCommunity); } } moveWasMade = true; currentMoves++; // Adding neighbors from other communities to the queue start = index.starts[i]; end = index.starts[i + 1]; for (; start < end; start++) { j = index.neighborhood[start]; targetCommunity = index.belongings[j]; if (targetCommunity !== bestCommunity) queue.enqueue(j); } } moves.push(currentMoves); } else { localMoves = []; moves.push(localMoves); // Traditional Louvain iterative traversal of the graph while (localMoveWasMade) { localMoveWasMade = false; currentMoves = 0; ri = options.randomWalk ? randomIndex(l) : 0; for (s = 0; s < l; s++, ri++) { i = ri % l; nodesVisited++; degree = 0; communities.clear(); currentCommunity = index.belongings[i]; start = index.starts[i]; end = index.starts[i + 1]; // Traversing neighbors for (; start < end; start++) { j = index.neighborhood[start]; weight = index.weights[start]; targetCommunity = index.belongings[j]; // Incrementing metrics degree += weight; addWeightToCommunity(communities, targetCommunity, weight); } // Finding best community to move to bestDelta = index.fastDeltaWithOwnCommunity( i, degree, communities.get(currentCommunity) || 0, currentCommunity ); bestCommunity = currentCommunity; for (ci = 0; ci < communities.size; ci++) { targetCommunity = communities.dense[ci]; if (targetCommunity === currentCommunity) continue; targetCommunityDegree = communities.vals[ci]; deltaComputations++; delta = index.fastDelta( i, degree, targetCommunityDegree, targetCommunity ); deltaIsBetter = tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ); if (deltaIsBetter) { bestDelta = delta; bestCommunity = targetCommunity; } } // Should we move the node? if (bestDelta < 0) { // NOTE: this is to allow nodes to move back to their own singleton // This code however only deals with modularity (e.g. the condition // about bestDelta < 0, which is the delta for moving back to // singleton wrt. modularity). Indeed, rarely, the Louvain // algorithm can produce such cases when a node would be better in // a singleton that in its own community when considering self loops // or a resolution != 1. In this case, delta with your own community // is indeed less than 0. To handle different metrics, one should // consider computing the delta for going back to singleton because // it might not be 0. bestCommunity = index.isolate(i, degree); // If the node was already in a singleton community, we don't consider // a move was made if (bestCommunity === currentCommunity) continue; } else { // If no move was made, we continue to next node if (bestCommunity === currentCommunity) { continue; } else { // Actually moving the node to a new community index.move(i, degree, bestCommunity); } } localMoveWasMade = true; currentMoves++; } localMoves.push(currentMoves); moveWasMade = localMoveWasMade || moveWasMade; } } // We continue working on the induced graph if (moveWasMade) index.zoomOut(); } var results = { index: index, deltaComputations: deltaComputations, nodesVisited: nodesVisited, moves: moves }; return results; } function directedLouvain(detailed, graph, options) { var index = new DirectedLouvainIndex(graph, { getEdgeWeight: options.getEdgeWeight, keepDendrogram: detailed, resolution: options.resolution }); var randomIndex = createRandomIndex(options.rng); // State variables var moveWasMade = true, localMoveWasMade = true; // Communities var currentCommunity, targetCommunity; var communities = new SparseMap(Float64Array, index.C); // Traversal var queue, start, end, offset, out, weight, ci, ri, s, i, j, l; // Metrics var inDegree, outDegree, targetCommunityDegree; // Moves var bestCommunity, bestDelta, deltaIsBetter, delta; // Details var deltaComputations = 0, nodesVisited = 0, moves = [], localMoves, currentMoves; if (options.fastLocalMoves) queue = new SparseQueueSet(index.C); while (moveWasMade) { l = index.C; moveWasMade = false; localMoveWasMade = true; if (options.fastLocalMoves) { currentMoves = 0; // Traversal of the graph ri = options.randomWalk ? randomIndex(l) : 0; for (s = 0; s < l; s++, ri++) { i = ri % l; queue.enqueue(i); } while (queue.size !== 0) { i = queue.dequeue(); nodesVisited++; inDegree = 0; outDegree = 0; communities.clear(); currentCommunity = index.belongings[i]; start = index.starts[i]; end = index.starts[i + 1]; offset = index.offsets[i]; // Traversing neighbors for (; start < end; start++) { out = start < offset; j = index.neighborhood[start]; weight = index.weights[start]; targetCommunity = index.belongings[j]; // Incrementing metrics if (out) outDegree += weight; else inDegree += weight; addWeightToCommunity(communities, targetCommunity, weight); } // Finding best community to move to bestDelta = index.deltaWithOwnCommunity( i, inDegree, outDegree, communities.get(currentCommunity) || 0, currentCommunity ); bestCommunity = currentCommunity; for (ci = 0; ci < communities.size; ci++) { targetCommunity = communities.dense[ci]; if (targetCommunity === currentCommunity) continue; targetCommunityDegree = communities.vals[ci]; deltaComputations++; delta = index.delta( i, inDegree, outDegree, targetCommunityDegree, targetCommunity ); deltaIsBetter = tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ); if (deltaIsBetter) { bestDelta = delta; bestCommunity = targetCommunity; } } // Should we move the node? if (bestDelta < 0) { // NOTE: this is to allow nodes to move back to their own singleton // This code however only deals with modularity (e.g. the condition // about bestDelta < 0, which is the delta for moving back to // singleton wrt. modularity). Indeed, rarely, the Louvain // algorithm can produce such cases when a node would be better in // a singleton that in its own community when considering self loops // or a resolution != 1. In this case, delta with your own community // is indeed less than 0. To handle different metrics, one should // consider computing the delta for going back to singleton because // it might not be 0. bestCommunity = index.isolate(i, inDegree, outDegree); // If the node was already in a singleton community, we don't consider // a move was made if (bestCommunity === currentCommunity) continue; } else { // If no move was made, we continue to next node if (bestCommunity === currentCommunity) { continue; } else { // Actually moving the node to a new community index.move(i, inDegree, outDegree, bestCommunity); } } moveWasMade = true; currentMoves++; // Adding neighbors from other communities to the queue start = index.starts[i]; end = index.starts[i + 1]; for (; start < end; start++) { j = index.neighborhood[start]; targetCommunity = index.belongings[j]; if (targetCommunity !== bestCommunity) queue.enqueue(j); } } moves.push(currentMoves); } else { localMoves = []; moves.push(localMoves); // Traditional Louvain iterative traversal of the graph while (localMoveWasMade) { localMoveWasMade = false; currentMoves = 0; ri = options.randomWalk ? randomIndex(l) : 0; for (s = 0; s < l; s++, ri++) { i = ri % l; nodesVisited++; inDegree = 0; outDegree = 0; communities.clear(); currentCommunity = index.belongings[i]; start = index.starts[i]; end = index.starts[i + 1]; offset = index.offsets[i]; // Traversing neighbors for (; start < end; start++) { out = start < offset; j = index.neighborhood[start]; weight = index.weights[start]; targetCommunity = index.belongings[j]; // Incrementing metrics if (out) outDegree += weight; else inDegree += weight; addWeightToCommunity(communities, targetCommunity, weight); } // Finding best community to move to bestDelta = index.deltaWithOwnCommunity( i, inDegree, outDegree, communities.get(currentCommunity) || 0, currentCommunity ); bestCommunity = currentCommunity; for (ci = 0; ci < communities.size; ci++) { targetCommunity = communities.dense[ci]; if (targetCommunity === currentCommunity) continue; targetCommunityDegree = communities.vals[ci]; deltaComputations++; delta = index.delta( i, inDegree, outDegree, targetCommunityDegree, targetCommunity ); deltaIsBetter = tieBreaker( bestCommunity, currentCommunity, targetCommunity, delta, bestDelta ); if (deltaIsBetter) { bestDelta = delta; bestCommunity = targetCommunity; } } // Should we move the node? if (bestDelta < 0) { // NOTE: this is to allow nodes to move back to their own singleton // This code however only deals with modularity (e.g. the condition // about bestDelta < 0, which is the delta for moving back to // singleton wrt. modularity). Indeed, rarely, the Louvain // algorithm can produce such cases when a node would be better in // a singleton that in its own community when considering self loops // or a resolution != 1. In this case, delta with your own community // is indeed less than 0. To handle different metrics, one should // consider computing the delta for going back to singleton because // it might not be 0. bestCommunity = index.isolate(i, inDegree, outDegree); // If the node was already in a singleton community, we don't consider // a move was made if (bestCommunity === currentCommunity) continue; } else { // If no move was made, we continue to next node if (bestCommunity === currentCommunity) { continue; } else { // Actually moving the node to a new community index.move(i, inDegree, outDegree, bestCommunity); } } localMoveWasMade = true; currentMoves++; } localMoves.push(currentMoves); moveWasMade = localMoveWasMade || moveWasMade; } } // We continue working on the induced graph if (moveWasMade) index.zoomOut(); } var results = { index: index, deltaComputations: deltaComputations, nodesVisited: nodesVisited, moves: moves }; return results; } /** * Function returning the communities mapping of the graph. * * @param {boolean} assign - Assign communities to nodes attributes? * @param {boolean} detailed - Whether to return detailed information. * @param {Graph} graph - Target graph. * @param {object} options - Options: * @param {string} nodeCommunityAttribute - Community node attribute name. * @param {string} getEdgeWeight - Weight edge attribute name or getter function. * @param {string} deltaComputation - Method to use to compute delta computations. * @param {boolean} fastLocalMoves - Whether to use the fast local move optimization. * @param {boolean} randomWalk - Whether to traverse the graph in random order. * @param {number} resolution - Resolution parameter. * @param {function} rng - RNG function to use. * @return {object} */ function louvain(assign, detailed, graph, options) { if (!isGraph(graph)) throw new Error( 'graphology-communities-louvain: the given graph is not a valid graphology instance.' ); var type = inferType(graph); if (type === 'mixed') throw new Error( 'graphology-communities-louvain: cannot run the algorithm on a true mixed graph.' ); // Attributes name options = resolveDefaults(options, DEFAULTS); // Empty graph case var c = 0; if (graph.size === 0) { if (assign) { graph.forEachNode(function (node) { graph.setNodeAttribute(node, options.nodeCommunityAttribute, c++); }); return; } var communities = {}; graph.forEachNode(function (node) { communities[node] = c++; }); if (!detailed) return communities; return { communities: communities, count: graph.order, deltaComputations: 0, dendrogram: null, level: 0, modularity: NaN, moves: null, nodesVisited: 0, resolution: options.resolution }; } var fn = type === 'undirected' ? undirectedLouvain : directedLouvain; var results = fn(detailed, graph, options); var index = results.index; // Standard output if (!detailed) { if (assign) { index.assign(options.nodeCommunityAttribute); return; } return index.collect(); } // Detailed output var output = { count: index.C, deltaComputations: results.deltaComputations, dendrogram: index.dendrogram, level: index.level, modularity: index.modularity(), moves: results.moves, nodesVisited: results.nodesVisited, resolution: options.resolution }; if (assign) { index.assign(options.nodeCommunityAttribute); return output; } output.communities = index.collect(); return output; } /** * Exporting. */ var fn = louvain.bind(null, false, false); fn.assign = louvain.bind(null, true, false); fn.detailed = louvain.bind(null, false, true); fn.defaults = DEFAULTS; module.exports = fn;