louvian_ruby 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/louvian.rb +195 -0
- data/lib/louvian/community.rb +42 -0
- metadata +46 -0
data/lib/louvian.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
require 'set'
|
2
|
+
module Louvian
|
3
|
+
require 'louvian/community'
|
4
|
+
|
5
|
+
# Node => community
|
6
|
+
@@n2c = {}
|
7
|
+
|
8
|
+
# Adjacency list
|
9
|
+
@@adj={}
|
10
|
+
|
11
|
+
# List of all communities (meta_nodes)
|
12
|
+
@@communities = []
|
13
|
+
|
14
|
+
# List of all nodes
|
15
|
+
@@nodes = []
|
16
|
+
|
17
|
+
# Sum of all links half edges (double the number of edges)
|
18
|
+
@@total_weight = 0.0
|
19
|
+
|
20
|
+
def self.communities
|
21
|
+
@@communities
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.get_adj node_id
|
25
|
+
return @@adj[node_id]
|
26
|
+
end
|
27
|
+
|
28
|
+
# This method outputs information about communities
|
29
|
+
def self.display_communities
|
30
|
+
@@communities.each do |m|
|
31
|
+
puts "#{m.id} => #{m.nodes_ids} in=#{m.in} tot=#{m.tot}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# This method sets up the whole environemnt for calculations
|
36
|
+
#
|
37
|
+
# @param string [String] in the form of src dest (one edge per line)
|
38
|
+
def self.init_env string
|
39
|
+
make_adj string
|
40
|
+
@@nodes = @@adj.keys.sort
|
41
|
+
@@nodes.each do |k,v|
|
42
|
+
@@communities << Community.new(k)
|
43
|
+
@@n2c[k] = @@communities.last.id
|
44
|
+
end
|
45
|
+
@@total_weight = @@adj.inject(0) {|r,(k,v)| r+v.count}
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
# This method reset the enviroment and all counters
|
50
|
+
def self.reset
|
51
|
+
@@n2c = {}
|
52
|
+
@@adj={}
|
53
|
+
@@communities = []
|
54
|
+
@@nodes = []
|
55
|
+
@@total_weight = 0.0
|
56
|
+
Community.reset
|
57
|
+
end
|
58
|
+
|
59
|
+
# This method calcualtes the current modularity of the communities
|
60
|
+
# @returns q [Float]
|
61
|
+
def self.modularity
|
62
|
+
q = 0.0
|
63
|
+
m2 = @@total_weight
|
64
|
+
|
65
|
+
@@communities.each do |m_node|
|
66
|
+
q += m_node.in.to_f/m2 - (m_node.tot.to_f/m2 * m_node.tot.to_f/m2)
|
67
|
+
end
|
68
|
+
return q
|
69
|
+
end
|
70
|
+
|
71
|
+
# This method iterates over the graph to optimze the modularity. Iterations
|
72
|
+
# stops when there are no possible moves anymore.
|
73
|
+
def self.iterate
|
74
|
+
cur_mod = self.modularity
|
75
|
+
new_mod = cur_mod
|
76
|
+
improvement = 0.0
|
77
|
+
min_improvement = 0.01
|
78
|
+
nb_passes = 0
|
79
|
+
max_passes = 10
|
80
|
+
begin
|
81
|
+
#puts "Iterating"
|
82
|
+
#puts "modularity is #{self.modularity}"
|
83
|
+
cur_mod = new_mod
|
84
|
+
nb_moves = 0
|
85
|
+
nb_passes += 1
|
86
|
+
@@nodes.shuffle.each do |node|
|
87
|
+
#puts "\tconsidering node #{node}"
|
88
|
+
orig_community_id = @@n2c[node]
|
89
|
+
orig_community = @@communities.find {|i| i.id == orig_community_id}
|
90
|
+
node_to_comms_links = self.get_node_to_comms_links node
|
91
|
+
neighbour_communities = @@communities.find_all {|i| node_to_comms_links.include? i.id}
|
92
|
+
|
93
|
+
orig_community.remove node, node_to_comms_links[orig_community_id]
|
94
|
+
if orig_community.nodes_ids.empty?
|
95
|
+
@@communities.delete orig_community
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
best_community_id = orig_community_id
|
100
|
+
max_gain = 0.0
|
101
|
+
|
102
|
+
neighbour_communities.each do |m_node|
|
103
|
+
mod_gain = self.modularity_gain(node, m_node, node_to_comms_links[m_node.id])
|
104
|
+
#puts "\t\tfor comm #{m_node.id} mod increase is #{mod_gain}"
|
105
|
+
if mod_gain > max_gain
|
106
|
+
max_gain = mod_gain
|
107
|
+
best_community_id = m_node.id
|
108
|
+
end
|
109
|
+
end
|
110
|
+
if best_community_id != orig_community_id
|
111
|
+
nb_moves += 1
|
112
|
+
end
|
113
|
+
|
114
|
+
best_community = @@communities.find{|m| m.id == best_community_id}
|
115
|
+
#puts "\t\tbest comm #{best_community.id}"
|
116
|
+
|
117
|
+
best_community.insert node, node_to_comms_links[best_community.id]
|
118
|
+
@@n2c[node] = best_community_id
|
119
|
+
end
|
120
|
+
display_communities
|
121
|
+
puts "modularity is #{self.modularity}"
|
122
|
+
end while nb_moves > 0
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
# This method builds the adjacency list from +string+
|
128
|
+
# @param string [String] in the form of src dest (edge per line)
|
129
|
+
def self.make_adj string
|
130
|
+
@@adj = {}
|
131
|
+
lines = string.split("\n")
|
132
|
+
lines.each do |l|
|
133
|
+
@@adj[l.split[0].to_i] ||= Set.new
|
134
|
+
@@adj[l.split[0].to_i] << l.split[1].to_i
|
135
|
+
@@adj[l.split[1].to_i] ||= Set.new
|
136
|
+
@@adj[l.split[1].to_i] << l.split[0].to_i
|
137
|
+
end
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
# This method calcualtes the modularity gain for moving +node+ to +community+
|
143
|
+
# @param +node+ this is the node to be moved
|
144
|
+
# @param +community+ this is the destination community
|
145
|
+
# @param +nb_links_to_comm+ is the number of links from +node+ to +community+
|
146
|
+
# @returns delta_q (the gain of modularity)
|
147
|
+
def self.modularity_gain node, community, nb_links_to_comm
|
148
|
+
tot = community.tot
|
149
|
+
deg = get_adj(node).count
|
150
|
+
m2 = @@total_weight
|
151
|
+
|
152
|
+
#puts "\t\t\tcomm #{community.id} #{[tot, deg, m2, nb_links_to_comm]}"
|
153
|
+
# what makes sense
|
154
|
+
return (nb_links_to_comm.to_f/m2) - (tot * deg.to_f/m2**2/2)
|
155
|
+
|
156
|
+
# copied from the cpp code
|
157
|
+
#return nb_links_to_comm.to_f - tot*deg.to_f/m2
|
158
|
+
end
|
159
|
+
|
160
|
+
# This method gets all neighbour communities and the number of links from +node+
|
161
|
+
# to all neighbou communities
|
162
|
+
# @param +node+ to be considered
|
163
|
+
# @return +node_to_comms_links+ [Hash] {community => number_of_links from +node+
|
164
|
+
# to +community+}
|
165
|
+
def self.get_node_to_comms_links node
|
166
|
+
neighbour_nodes = self.get_adj node
|
167
|
+
node_to_comms_links = {}
|
168
|
+
neighbour_nodes.each do |node|
|
169
|
+
node_to_comms_links[@@n2c[node]] = (node_to_comms_links[@@n2c[node]] || 0) + 1
|
170
|
+
end
|
171
|
+
node_to_comms_links[@@n2c[node]] ||= 0
|
172
|
+
return node_to_comms_links
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
def self.example
|
177
|
+
s='0 1
|
178
|
+
0 8
|
179
|
+
1 3
|
180
|
+
1 4
|
181
|
+
1 8
|
182
|
+
2 3
|
183
|
+
2 5
|
184
|
+
2 7
|
185
|
+
3 8
|
186
|
+
5 6
|
187
|
+
5 7'
|
188
|
+
|
189
|
+
Louvian.init_env s
|
190
|
+
Louvian.iterate
|
191
|
+
Louvian.display_communities
|
192
|
+
Louvian.reset
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Louvian::Community
|
2
|
+
attr_accessor :in, :tot, :nodes_ids, :id
|
3
|
+
@@count = 0
|
4
|
+
def initialize node_id
|
5
|
+
@id = @@count
|
6
|
+
@@count+=1
|
7
|
+
@nodes_ids = [node_id]
|
8
|
+
@in = 0 # sum of links weights inside the community
|
9
|
+
@tot = Louvian.get_adj(node_id).count # sum of links weights incident to the community
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.reset
|
13
|
+
@@count = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def insert node, links_to_comm
|
17
|
+
#puts "\t\tinsert node #{node} to comm #{@id}"
|
18
|
+
@nodes_ids << node
|
19
|
+
|
20
|
+
# what makes sense
|
21
|
+
#@in += links_to_comm
|
22
|
+
#@tot += (Louvian.get_adj(node).count - links_to_comm)
|
23
|
+
|
24
|
+
# Copied from the cpp code
|
25
|
+
@in += 2*links_to_comm
|
26
|
+
@tot += (Louvian.get_adj(node).count)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove node, links_to_comm
|
30
|
+
#puts "\t\tremove node #{node} to comm #{@id}"
|
31
|
+
@nodes_ids.delete node
|
32
|
+
|
33
|
+
# what makes sense
|
34
|
+
#@in -= links_to_comm
|
35
|
+
#@tot -= (Louvian.get_adj(node).count - links_to_comm)
|
36
|
+
|
37
|
+
# Copied from the cpp code
|
38
|
+
@in -= 2*links_to_comm
|
39
|
+
@tot -= (Louvian.get_adj(node).count)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: louvian_ruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Yahia El Gamal
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-03 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Ruby implementation of Louvian community detection algorithim
|
15
|
+
email: yahiaelgamal@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/louvian.rb
|
21
|
+
- lib/louvian/community.rb
|
22
|
+
homepage: http://rubygems.org/gems/louvian_ruby
|
23
|
+
licenses: []
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 1.8.21
|
43
|
+
signing_key:
|
44
|
+
specification_version: 3
|
45
|
+
summary: Louvian wrapper
|
46
|
+
test_files: []
|