|
/* |
|
Author: Corneliu S. (github.com/upphiminn) |
|
|
|
This is a javascript implementation of the Louvain |
|
community detection algorithm (http://arxiv.org/abs/0803.0476) |
|
Based on https://bitbucket.org/taynaud/python-louvain/overview |
|
|
|
*/ |
|
(function(){ |
|
jLouvain = function(){ |
|
//Constants |
|
var __PASS_MAX = -1 |
|
var __MIN = 0.0000001 |
|
|
|
//Local vars |
|
var original_graph_nodes; |
|
var original_graph_edges; |
|
var original_graph = {}; |
|
var partition_init; |
|
|
|
//Helpers |
|
function make_set(array){ |
|
var set = {}; |
|
array.forEach(function(d,i){ |
|
set[d] = true; |
|
}); |
|
return Object.keys(set); |
|
}; |
|
|
|
function obj_values(obj){ |
|
var vals = []; |
|
for( var key in obj ) { |
|
if ( obj.hasOwnProperty(key) ) { |
|
vals.push(obj[key]); |
|
} |
|
} |
|
return vals; |
|
}; |
|
|
|
function get_degree_for_node(graph, node){ |
|
var neighbours = graph._assoc_mat[node] ? Object.keys(graph._assoc_mat[node]) : []; |
|
var weight = 0; |
|
neighbours.forEach(function(neighbour,i){ |
|
var value = graph._assoc_mat[node][neighbour] || 1; |
|
if(node == neighbour) |
|
value *= 2; |
|
weight += value; |
|
}); |
|
return weight; |
|
}; |
|
|
|
function get_neighbours_of_node(graph, node){ |
|
if(typeof graph._assoc_mat[node] == 'undefined') |
|
return []; |
|
|
|
var neighbours = Object.keys(graph._assoc_mat[node]); |
|
return neighbours; |
|
} |
|
|
|
function get_edge_weight(graph, node1, node2){ |
|
return graph._assoc_mat[node1] ? graph._assoc_mat[node1][node2] : undefined; |
|
} |
|
|
|
function get_graph_size(graph){ |
|
var size = 0; |
|
graph.edges.forEach(function(edge){ |
|
size += edge.weight; |
|
}); |
|
return size; |
|
} |
|
|
|
function add_edge_to_graph(graph, edge){ |
|
update_assoc_mat(graph, edge); |
|
|
|
var edge_index = graph.edges.map(function(d){ |
|
return d.source+'_'+d.target; |
|
}).indexOf(edge.source+'_'+edge.target); |
|
|
|
if(edge_index != -1) |
|
graph.edges[edge_index].weight = edge.weight; |
|
else |
|
graph.edges.push(edge); |
|
} |
|
|
|
function make_assoc_mat(edge_list){ |
|
var mat = {}; |
|
edge_list.forEach(function(edge, i){ |
|
mat[edge.source] = mat[edge.source] || {}; |
|
mat[edge.source][edge.target] = edge.weight; |
|
mat[edge.target] = mat[edge.target] || {}; |
|
mat[edge.target][edge.source] = edge.weight; |
|
}); |
|
|
|
return mat; |
|
} |
|
|
|
function update_assoc_mat(graph, edge){ |
|
graph._assoc_mat[edge.source] = graph._assoc_mat[edge.source] || {}; |
|
graph._assoc_mat[edge.source][edge.target] = edge.weight; |
|
graph._assoc_mat[edge.target] = graph._assoc_mat[edge.target] || {}; |
|
graph._assoc_mat[edge.target][edge.source] = edge.weight; |
|
} |
|
|
|
function clone(obj){ |
|
if(obj == null || typeof(obj) != 'object') |
|
return obj; |
|
|
|
var temp = obj.constructor(); |
|
|
|
for(var key in obj) |
|
temp[key] = clone(obj[key]); |
|
return temp; |
|
} |
|
|
|
//Core-Algorithm Related |
|
function init_status(graph, status, part){ |
|
status['nodes_to_com'] = {}; |
|
status['total_weight'] = 0; |
|
status['internals'] = {}; |
|
status['degrees'] = {}; |
|
status['gdegrees'] = {}; |
|
status['loops'] = {}; |
|
status['total_weight'] = get_graph_size(graph); |
|
|
|
if(typeof part == 'undefined'){ |
|
graph.nodes.forEach(function(node,i){ |
|
status.nodes_to_com[node] = i; |
|
var deg = get_degree_for_node(graph, node); |
|
if (deg < 0) |
|
throw 'Bad graph type, use positive weights!'; |
|
status.degrees[i] = deg; |
|
status.gdegrees[node] = deg; |
|
status.loops[node] = get_edge_weight(graph, node, node) || 0; |
|
status.internals[i] = status.loops[node]; |
|
}); |
|
}else{ |
|
graph.nodes.forEach(function(node,i){ |
|
var com = part[node]; |
|
status.nodes_to_com[node] = com; |
|
var deg = get_degree_for_node(graph, node); |
|
status.degrees[com] = (status.degrees[com] || 0) + deg; |
|
status.gdegrees[node] = deg; |
|
var inc = 0.0; |
|
|
|
var neighbours = get_neighbours_of_node(graph, node); |
|
neighbours.forEach(function(neighbour, i){ |
|
var weight = graph._assoc_mat[node][neighbour]; |
|
if (weight <= 0){ |
|
throw "Bad graph type, use positive weights"; |
|
} |
|
|
|
if(part[neighbour] == com){ |
|
if (neighbour == node){ |
|
inc += weight; |
|
}else{ |
|
inc += weight/2.0; |
|
} |
|
} |
|
}); |
|
status.internals[com] = (status.internals[com] || 0) + inc; |
|
}); |
|
} |
|
} |
|
|
|
function __modularity(status){ |
|
var links = status.total_weight; |
|
var result = 0.0; |
|
var communities = make_set(obj_values(status.nodes_to_com)); |
|
|
|
communities.forEach(function(com,i){ |
|
var in_degree = status.internals[com] || 0 ; |
|
var degree = status.degrees[com] || 0 ; |
|
if(links > 0){ |
|
result = result + in_degree / links - Math.pow((degree / (2.0*links)), 2); |
|
} |
|
}); |
|
return result; |
|
} |
|
|
|
function __neighcom(node, graph, status){ |
|
// compute the communities in the neighb. of the node, with the graph given by |
|
// node_to_com |
|
|
|
var weights = {}; |
|
var neighboorhood = get_neighbours_of_node(graph, node);//make iterable; |
|
|
|
neighboorhood.forEach(function(neighbour, i){ |
|
if(neighbour != node){ |
|
var weight = graph._assoc_mat[node][neighbour] || 1; |
|
var neighbourcom = status.nodes_to_com[neighbour]; |
|
weights[neighbourcom] = (weights[neighbourcom] || 0) + weight; |
|
} |
|
}); |
|
|
|
return weights; |
|
} |
|
|
|
function __insert(node, com, weight, status){ |
|
//insert node into com and modify status |
|
status.nodes_to_com[node] = +com; |
|
status.degrees[com] = (status.degrees[com] || 0) + (status.gdegrees[node]||0); |
|
status.internals[com] = (status.internals[com] || 0) + weight + (status.loops[node]||0); |
|
} |
|
|
|
function __remove(node, com, weight, status){ |
|
//remove node from com and modify status |
|
status.degrees[com] = ((status.degrees[com] || 0) - (status.gdegrees[node] || 0)); |
|
status.internals[com] = ((status.internals[com] || 0) - weight -(status.loops[node] ||0)); |
|
status.nodes_to_com[node] = -1; |
|
} |
|
|
|
function __renumber(dict){ |
|
var count = 0; |
|
var ret = clone(dict); //deep copy :) |
|
var new_values = {}; |
|
var dict_keys = Object.keys(dict); |
|
dict_keys.forEach(function(key){ |
|
var value = dict[key]; |
|
var new_value = typeof new_values[value] =='undefined' ? -1 : new_values[value]; |
|
if(new_value == -1){ |
|
new_values[value] = count; |
|
new_value = count; |
|
count = count + 1; |
|
} |
|
ret[key] = new_value; |
|
}); |
|
return ret; |
|
} |
|
|
|
function __one_level(graph, status){ |
|
//Compute one level of the Communities Dendogram. |
|
var modif = true, |
|
nb_pass_done = 0, |
|
cur_mod = __modularity(status), |
|
new_mod = cur_mod; |
|
|
|
while (modif && nb_pass_done != __PASS_MAX){ |
|
cur_mod = new_mod; |
|
modif = false; |
|
nb_pass_done += 1 |
|
|
|
graph.nodes.forEach(function(node,i){ |
|
var com_node = status.nodes_to_com[node]; |
|
var degc_totw = (status.gdegrees[node] || 0) / (status.total_weight * 2.0); |
|
var neigh_communities = __neighcom(node, graph, status); |
|
__remove(node, com_node, (neigh_communities[com_node] || 0.0), status); |
|
var best_com = com_node; |
|
var best_increase = 0; |
|
var neigh_communities_entries = Object.keys(neigh_communities);//make iterable; |
|
|
|
neigh_communities_entries.forEach(function(com,i){ |
|
var incr = neigh_communities[com] - (status.degrees[com] || 0.0) * degc_totw; |
|
if (incr > best_increase){ |
|
best_increase = incr; |
|
best_com = com; |
|
} |
|
}); |
|
|
|
__insert(node, best_com, neigh_communities[best_com] || 0, status); |
|
|
|
if(best_com != com_node) |
|
modif = true; |
|
}); |
|
new_mod = __modularity(status); |
|
if(new_mod - cur_mod < __MIN) |
|
break; |
|
} |
|
} |
|
|
|
function induced_graph(partition, graph){ |
|
var ret = {nodes:[], edges:[], _assoc_mat: {}}; |
|
var w_prec, weight; |
|
//add nodes from partition values |
|
var partition_values = obj_values(partition); |
|
ret.nodes = ret.nodes.concat(make_set(partition_values)); //make set |
|
graph.edges.forEach(function(edge,i){ |
|
weight = edge.weight || 1; |
|
var com1 = partition[edge.source]; |
|
var com2 = partition[edge.target]; |
|
w_prec = (get_edge_weight(ret, com1, com2) || 0); |
|
var new_weight = (w_prec + weight); |
|
add_edge_to_graph(ret, {'source': com1, 'target': com2, 'weight': new_weight}); |
|
}); |
|
return ret; |
|
} |
|
|
|
function partition_at_level(dendogram, level){ |
|
var partition = clone(dendogram[0]); |
|
for(var i = 1; i < level + 1; i++ ) |
|
Object.keys(partition).forEach(function(key,j){ |
|
var node = key; |
|
var com = partition[key]; |
|
partition[node] = dendogram[i][com]; |
|
}); |
|
return partition; |
|
} |
|
|
|
|
|
function generate_dendogram(graph, part_init){ |
|
|
|
if(graph.edges.length == 0){ |
|
var part = {}; |
|
graph.nodes.forEach(function(node,i){ |
|
part[node] = node; |
|
}); |
|
return part; |
|
} |
|
var status = {}; |
|
|
|
init_status(original_graph, status, part_init); |
|
var mod = __modularity(status); |
|
var status_list = []; |
|
__one_level(original_graph, status); |
|
var new_mod = __modularity(status); |
|
var partition = __renumber(status.nodes_to_com); |
|
status_list.push(partition); |
|
mod = new_mod; |
|
var current_graph = induced_graph(partition, original_graph); |
|
init_status(current_graph, status); |
|
|
|
while (true){ |
|
__one_level(current_graph, status); |
|
new_mod = __modularity(status); |
|
if(new_mod - mod < __MIN) |
|
break; |
|
|
|
partition = __renumber(status.nodes_to_com); |
|
status_list.push(partition); |
|
|
|
mod = new_mod; |
|
current_graph = induced_graph(partition, current_graph); |
|
init_status(current_graph, status); |
|
} |
|
|
|
return status_list; |
|
} |
|
|
|
var core = function(){ |
|
var status = {}; |
|
var dendogram = generate_dendogram(original_graph, partition_init); |
|
return partition_at_level(dendogram, dendogram.length - 1); |
|
}; |
|
|
|
core.nodes = function(nds){ |
|
if(arguments.length > 0){ |
|
original_graph_nodes = nds; |
|
} |
|
return core; |
|
}; |
|
|
|
core.edges = function(edgs){ |
|
if(typeof original_graph_nodes == 'undefined') |
|
throw 'Please provide the graph nodes first!'; |
|
|
|
if(arguments.length > 0){ |
|
original_graph_edges = edgs; |
|
var assoc_mat = make_assoc_mat(edgs); |
|
original_graph = { 'nodes': original_graph_nodes, |
|
'edges': original_graph_edges, |
|
'_assoc_mat': assoc_mat }; |
|
} |
|
return core; |
|
|
|
}; |
|
|
|
core.partition_init = function(prttn){ |
|
if(arguments.length > 0){ |
|
partition_init = prttn; |
|
} |
|
return core; |
|
}; |
|
|
|
return core; |
|
} |
|
})(); |