@graphty/algorithms
Version:
Graph algorithms library for browser environments implemented in TypeScript
307 lines • 10.5 kB
JavaScript
/**
* Label Propagation Algorithm for Community Detection
*
* A fast, near-linear time algorithm that detects communities by
* propagating labels through the network. Each node adopts the label
* that most of its neighbors have.
*
* Reference: Raghavan et al. (2007) "Near linear time algorithm to
* detect community structures in large-scale networks"
*/
/**
* Label Propagation Algorithm
* Each node adopts the most frequent label among its neighbors
*
* @param graph - Undirected graph (can be weighted)
* @param options - Algorithm options
* @returns Community assignments
*
* Time Complexity: O(m) per iteration, typically O(km) for k iterations
* Space Complexity: O(n)
*/
export function labelPropagation(graph, options = {}) {
const { maxIterations = 100, randomSeed = 42, } = options;
if (graph.size === 0) {
return {
communities: new Map(),
iterations: 0,
converged: true,
};
}
// Initialize random number generator
let seed = randomSeed;
const random = () => {
seed = ((seed * 1664525) + 1013904223) % 2147483647;
return seed / 2147483647;
};
// Initialize labels - each node gets its own label
const labels = new Map();
const nodes = Array.from(graph.keys());
nodes.forEach((node, i) => labels.set(node, i));
let iterations = 0;
let converged = false;
// Main loop
while (iterations < maxIterations && !converged) {
iterations++;
converged = true;
// Create random order for node updates
const nodeOrder = [...nodes];
shuffle(nodeOrder, random);
// Update each node's label
for (const node of nodeOrder) {
const neighbors = graph.get(node);
if (!neighbors || neighbors.size === 0) {
continue;
}
// Count label frequencies among neighbors
const labelCounts = new Map();
let maxCount = 0;
const candidateLabels = [];
for (const [neighbor, weight] of neighbors) {
const neighborLabel = labels.get(neighbor);
if (neighborLabel === undefined) {
continue;
}
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
labelCounts.set(neighborLabel, count);
if (count > maxCount) {
maxCount = count;
candidateLabels.length = 0;
candidateLabels.push(neighborLabel);
}
else if (count === maxCount) {
candidateLabels.push(neighborLabel);
}
}
// Choose label (break ties randomly)
const currentLabel = labels.get(node);
if (currentLabel === undefined) {
continue;
}
let newLabel = currentLabel;
if (candidateLabels.length > 0) {
// Include current label if it has max count
if (labelCounts.get(currentLabel) === maxCount) {
candidateLabels.push(currentLabel);
}
// Random selection from candidates
const index = Math.floor(random() * candidateLabels.length);
const selectedLabel = candidateLabels[index];
if (selectedLabel !== undefined) {
newLabel = selectedLabel;
}
}
// Update label if changed
if (newLabel !== currentLabel) {
labels.set(node, newLabel);
converged = false;
}
}
}
// Renumber communities consecutively
const uniqueLabels = new Set(labels.values());
const labelMap = new Map();
let communityId = 0;
for (const label of uniqueLabels) {
labelMap.set(label, communityId++);
}
const communities = new Map();
for (const [node, label] of labels) {
const mappedLabel = labelMap.get(label);
if (mappedLabel !== undefined) {
communities.set(node, mappedLabel);
}
}
return {
communities,
iterations,
converged,
};
}
/**
* Asynchronous Label Propagation
* Updates all nodes simultaneously (can lead to oscillations)
*/
export function labelPropagationAsync(graph, options = {}) {
const { maxIterations = 100, } = options;
if (graph.size === 0) {
return {
communities: new Map(),
iterations: 0,
converged: true,
};
}
// Initialize labels
const labels = new Map();
const nodes = Array.from(graph.keys());
nodes.forEach((node, i) => labels.set(node, i));
let iterations = 0;
let converged = false;
// Main loop
while (iterations < maxIterations && !converged) {
iterations++;
converged = true;
// Store new labels
const newLabels = new Map();
// Update all nodes simultaneously
for (const node of nodes) {
const neighbors = graph.get(node);
if (!neighbors || neighbors.size === 0) {
const nodeLabel = labels.get(node);
if (nodeLabel !== undefined) {
newLabels.set(node, nodeLabel);
}
continue;
}
// Count label frequencies
const labelCounts = new Map();
let maxCount = 0;
const nodeLabel = labels.get(node);
if (nodeLabel === undefined) {
continue;
}
let maxLabel = nodeLabel;
for (const [neighbor, weight] of neighbors) {
const neighborLabel = labels.get(neighbor);
if (neighborLabel === undefined) {
continue;
}
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
labelCounts.set(neighborLabel, count);
if (count > maxCount || (count === maxCount && neighborLabel < maxLabel)) {
maxCount = count;
maxLabel = neighborLabel;
}
}
newLabels.set(node, maxLabel);
if (maxLabel !== labels.get(node)) {
converged = false;
}
}
// Apply new labels
for (const [node, label] of newLabels) {
labels.set(node, label);
}
}
// Renumber communities
const uniqueLabels = new Set(labels.values());
const labelMap = new Map();
let communityId = 0;
for (const label of uniqueLabels) {
labelMap.set(label, communityId++);
}
const communities = new Map();
for (const [node, label] of labels) {
const mappedLabel = labelMap.get(label);
if (mappedLabel !== undefined) {
communities.set(node, mappedLabel);
}
}
return {
communities,
iterations,
converged,
};
}
/**
* Semi-supervised Label Propagation
* Some nodes have fixed labels that don't change
*/
export function labelPropagationSemiSupervised(graph, seedLabels, options = {}) {
const { maxIterations = 100, randomSeed = 42, } = options;
// Initialize random number generator
let seed = randomSeed;
const random = () => {
seed = ((seed * 1664525) + 1013904223) % 2147483647;
return seed / 2147483647;
};
// Initialize labels
const labels = new Map();
const nodes = Array.from(graph.keys());
let labelCounter = Math.max(...Array.from(seedLabels.values())) + 1;
for (const node of nodes) {
if (seedLabels.has(node)) {
const seedLabel = seedLabels.get(node);
if (seedLabel !== undefined) {
labels.set(node, seedLabel);
}
}
else {
labels.set(node, labelCounter++);
}
}
let iterations = 0;
let converged = false;
// Main loop
while (iterations < maxIterations && !converged) {
iterations++;
converged = true;
// Create random order
const nodeOrder = nodes.filter((n) => !seedLabels.has(n));
shuffle(nodeOrder, random);
// Update non-seed nodes
for (const node of nodeOrder) {
const neighbors = graph.get(node);
if (!neighbors || neighbors.size === 0) {
continue;
}
// Count label frequencies
const labelCounts = new Map();
let maxCount = 0;
const candidateLabels = [];
for (const [neighbor, weight] of neighbors) {
const neighborLabel = labels.get(neighbor);
if (neighborLabel === undefined) {
continue;
}
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
labelCounts.set(neighborLabel, count);
if (count > maxCount) {
maxCount = count;
candidateLabels.length = 0;
candidateLabels.push(neighborLabel);
}
else if (count === maxCount) {
candidateLabels.push(neighborLabel);
}
}
// Choose label
const currentLabel = labels.get(node);
if (currentLabel === undefined) {
continue;
}
let newLabel = currentLabel;
if (candidateLabels.length > 0) {
const index = Math.floor(random() * candidateLabels.length);
const selectedLabel = candidateLabels[index];
if (selectedLabel !== undefined) {
newLabel = selectedLabel;
}
}
if (newLabel !== currentLabel) {
labels.set(node, newLabel);
converged = false;
}
}
}
return {
communities: labels,
iterations,
converged,
};
}
/**
* Fisher-Yates shuffle
*/
function shuffle(array, random) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
const temp = array[i];
const swapItem = array[j];
if (temp !== undefined && swapItem !== undefined) {
array[i] = swapItem;
array[j] = temp;
}
}
}
//# sourceMappingURL=label-propagation.js.map