File Coverage

blib/lib/SNA/Network/Algorithm/Louvain.pm
Criterion Covered Total %
statement 139 139 100.0
branch 24 24 100.0
condition 2 2 100.0
subroutine 14 14 100.0
pod 1 1 100.0
total 180 180 100.0


line stmt bran cond sub pod time code
1             package SNA::Network::Algorithm::Louvain;
2              
3 14     14   58 use strict;
  14         21  
  14         475  
4 14     14   64 use warnings;
  14         23  
  14         514  
5              
6             require Exporter;
7 14     14   55 use base qw(Exporter);
  14         20  
  14         1052  
8             our @EXPORT = qw(identify_communities_with_louvain);
9              
10 14     14   4757 use SNA::Network::Community;
  14         33  
  14         121  
11 14     14   5090 use SNA::Network::CommunityStructure;
  14         23  
  14         397  
12              
13 14     14   66 use List::Util qw(sum);
  14         19  
  14         800  
14 14     14   66 use List::MoreUtils qw(uniq notall);
  14         19  
  14         19210  
15              
16              
17             =head1 NAME
18              
19             SNA::Network::Algorithm::Louvain - identifies communities with the Louvain-method developed by Blondel and Guillaume and Lamboitte and Lefebvre
20              
21              
22             =head1 SYNOPSIS
23              
24             use SNA::Network;
25              
26             my $net = SNA::Network->new();
27             $net->load_from_pajek_net($filename);
28             ...
29             my $num_communities = $net->identify_communities_with_louvain;
30              
31              
32             =head1 METHODS
33              
34             The following methods are added to L.
35              
36             =head2 identify_communities_with_louvain
37              
38             Performs the community identification algorithm and returns the number of communities it identified. The corresponding L objects can be accessed with the C method of L.
39              
40             =cut
41              
42             sub identify_communities_with_louvain {
43 3     3 1 11 my ($self, $level) = @_;
44 3   100     12 $level ||= 0;
45 3         5 $self->{louvain_levels} = $level;
46            
47 3         11 $self->{total_weight} = $self->total_weight;
48            
49             # initialize communities
50 3         99 foreach ($self->nodes) {
51 190         290 $_->{community} = $_->index;
52 190         297 $_->{_k_i} = $_->weighted_summed_degree;
53             }
54 190 100       721 my @communities = map {
    100          
55 3         15 SNA::Network::Community->new(
56             network => $self,
57             level => $level,
58             index => $_->index,
59             members_ref => [$_],
60             w_in => $level > 0 ? $_->loop->weight : 0,
61             w_tot => $_->{_k_i} - ($level > 0 ? $_->loop->weight : 0),
62             )
63             } $self->nodes;
64              
65 3         15 $self->{communities_ref} = \@communities;
66            
67 3         4 my $has_changed = 0;
68 3         4 my $has_improved;
69            
70             PHASE_ONE_ITERATIONS:
71 3         4 do {
72 9         9 $has_improved = 0;
73            
74 9         41 foreach my $node_i ($self->nodes) {
75 1096         948 my $max_gain = 0;
76 1096         726 my $best_community_id;
77              
78 7738         11947 $node_i->{_k_i_in} = sum(
79             $level > 0 ? - $node_i->loop->weight : 0,
80             map {
81 17528         34617 $_->weight
82             } grep {
83 1096 100       2850 $_->source->community == $_->target->community
84             } $node_i->edges
85             );
86            
87 1096         2577 my $current_community = @communities[ $node_i->community ];
88            
89             # gain in current community on removal
90 1096         2125 my $current_community_module = _module_value(
91             $current_community->w_in,
92             $current_community->w_tot,
93             $self->{total_weight},
94             );
95            
96 1096         2835 my $new_current_community_module = _module_value(
97             $current_community->w_in - $node_i->{_k_i_in},
98             $current_community->w_tot - $node_i->{_k_i} + $node_i->{_k_i_in},
99             $self->{total_weight},
100             );
101            
102 1096         1495 my $gain_on_removal = $new_current_community_module - $current_community_module;
103            
104             # pre-calculate k_i_new values for all neighbour communities
105 1096         1838 foreach ($node_i->outgoing_edges) {
106 8764         14492 $node_i->{_k_i_new}->{ $_->target->community } += $_->weight
107             }
108              
109 1096         2346 foreach ($node_i->incoming_edges) {
110 8764         14112 $node_i->{_k_i_new}->{ $_->source->community } += $_->weight
111             }
112              
113 1096 100       2113 if ($level > 0) {
114 16         24 my $loop_weight = $node_i->loop->weight;
115              
116 16         28 foreach ( values %{ $node_i->{_k_i_new} } ) {
  16         32  
117 65         61 $_ += $loop_weight;
118             }
119             }
120              
121 17528         25363 my @neighbour_community_ids = uniq grep {
122 17528         21741 $_ != $node_i->community
123             } map {
124 1096         1893 $_->community
125             } $node_i->related_nodes;
126            
127 1096         3764 foreach my $neighbour_community_id (@neighbour_community_ids) {
128             # calculate modularity changes
129 4966         5430 my $gain_on_addition = _modularity_gain($self, $node_i, $neighbour_community_id);
130 4966         4602 my $gain = $gain_on_removal + $gain_on_addition;
131            
132 4966 100       9053 if ( $gain > $max_gain ) {
133 500         411 $max_gain = $gain;
134 500         599 $best_community_id = $neighbour_community_id;
135             }
136             }
137            
138 1096 100       1825 if ( $max_gain > 0.0000001 ) {
139             # merge node to best community
140            
141 275         323 _switch_community($self, $node_i, $best_community_id);
142 275         215 $has_improved = 1;
143 275         233 $has_changed = 1;
144             }
145            
146 1096         1260 undef $node_i->{_k_i_in};
147 1096         2542 undef $node_i->{_k_i_new};
148             }
149             } while ($has_improved);
150              
151 3         7 _consolidate_community_structure($self);
152              
153 3 100       6 return map { $_->subcommunities } @communities unless $has_changed;
  4         6  
154              
155 2         5 $self->{louvain_levels} += 1;
156            
157            
158 2         4 PHASE_TWO:
159              
160             # merge communities to new nodes in a new network
161             my $next_level_network = _create_next_level_network($self);
162            
163             # apply the algorithm recursively to the new network
164 2         22 my @new_community_structure = $next_level_network->identify_communities_with_louvain($level + 1);
165            
166 2         4 $self->{louvain_levels} = $next_level_network->{louvain_levels};
167 2 100       6 return @new_community_structure if $level > 0;
168              
169             # build the hierarchical community structure
170 1         8 my @community_levels = _build_hierarchy(@new_community_structure);
171              
172 1         4 $self->{community_levels} = \@community_levels;
173 1         2 $self->{communities_ref} = \@new_community_structure;
174 1         3 return int $self->communities;
175             }
176              
177              
178             sub _modularity_gain {
179 4966     4966   5172 my ($net, $node_i, $new_community_id) = @_;
180 4966         5721 my $new_community = $net->{communities_ref}->[$new_community_id];
181              
182 4966         5382 my $k_i_new = $node_i->{_k_i_new}->{ $new_community_id };
183              
184 4966         7994 my $neighbour_community_module = _module_value(
185             $new_community->w_in,
186             $new_community->w_tot,
187             $net->{total_weight},
188             );
189            
190 4966         9824 my $new_neighbour_community_module = _module_value(
191             $new_community->w_in + $k_i_new,
192             $new_community->w_tot + $node_i->{_k_i} - $k_i_new,
193             $net->{total_weight},
194             );
195              
196 4966         7176 return $new_neighbour_community_module - $neighbour_community_module;
197             }
198              
199              
200             sub _module_value {
201 12124     12124   11070 my ($w_in, $w_tot, $w_net) = @_;
202 12124         19178 return $w_in / $w_net - ( ( $w_in + $w_tot ) / ( 2 * $w_net ) ) ** 2;
203             }
204              
205              
206             sub _switch_community {
207 275     275   300 my ($net, $node, $new_community_id) = @_;
208 275         405 my $current_community = $net->{communities_ref}->[ $node->community ];
209 275         279 my $new_community = $net->{communities_ref}->[$new_community_id];
210            
211 275         468 @{ $current_community->members_ref } = grep {
  686         1054  
212 275         606 $_ != $node
213             } $current_community->members;
214              
215 275         270 push @{ $new_community->{members_ref} }, $node;
  275         425  
216              
217 275         335 $node->{community} = $new_community_id;
218            
219 275         307 $current_community->{w_in} -= $node->{_k_i_in};
220 275         298 $current_community->{w_tot} -= $node->{_k_i} - $node->{_k_i_in};
221            
222 275         321 $new_community->{w_in} += $node->{_k_i_new}->{ $new_community_id };
223 275         411 $new_community->{w_tot} += $node->{_k_i} - $node->{_k_i_new}->{ $new_community_id };
224             }
225              
226              
227             sub _consolidate_community_structure {
228 3     3   5 my ($self) = @_;
229              
230 3         12 @{$self->{communities_ref}} = grep {
  190         299  
231 3         14 int $_->members > 0
232             } $self->communities;
233              
234              
235 3         79 my $index = 0;
236 3         7 foreach my $community ($self->communities) {
237 14         13 $community->{index} = $index;
238              
239 14         29 foreach my $member ($self->{communities_ref}->[$index]->members) {
240 190         181 $member->{community} = $index;
241             }
242              
243 14         16 $index += 1;
244              
245 14 100       31 next if $self->{louvain_levels} == 0;
246              
247 10         15 my @subcommunities = map {
248 8         13 $_->{subcommunity}
249             } $community->members;
250            
251 8         13 $community->{subcommunities} = \@subcommunities;
252 8         13 undef $community->{members_ref};
253             }
254            
255             # for my $index (0 .. int($self->communities) - 1) {
256             # $self->{communities_ref}->[$index]->{index} = $index;
257             # foreach my $member ($self->{communities_ref}->[$index]->members) {
258             # $member->{community} = $index;
259             # }
260             # }
261             }
262              
263              
264             sub _create_next_level_network {
265 2     2   3 my ($net) = @_;
266            
267 2         10 my $next_level_network = SNA::Network->new;
268 2         5 foreach my $community ($net->communities) {
269 10         25 my $new_node = $next_level_network->create_node(
270             community => $community->index,
271             subcommunity => $community,
272             );
273 10         30 $next_level_network->create_edge(
274             source_index => $new_node->index,
275             target_index => $new_node->index,
276             weight => $community->w_in,
277             );
278             }
279            
280 2         6 my $nc = int $net->communities;
281 2         7 my @edge_weights = map { {} } 1 .. $nc;
  10         11  
282 2         7 foreach my $edge ($net->edges) {
283 1478         2409 $edge_weights[ $edge->source->community ]->{ $edge->target->community } += $edge->weight;
284             }
285            
286 2         39 foreach my $meta_node ($next_level_network->nodes) {
287             PEERS:
288 10         17 foreach my $peer_node ($next_level_network->nodes) {
289 52 100       88 next PEERS if $meta_node == $peer_node;
290 42         69 my $weight = $edge_weights[ $meta_node->index ]->{ $peer_node->index };
291            
292 42 100       57 if ($weight) {
293 36         71 $next_level_network->create_edge(
294             source_index => $meta_node->index,
295             target_index => $peer_node->index,
296             weight => $weight,
297             );
298             }
299             }
300             }
301            
302 2         9 return $next_level_network;
303             }
304              
305              
306             sub _build_hierarchy {
307 2     2   3 my (@communities) = @_;
308              
309 2 100       15 return SNA::Network::CommunityStructure->new(@communities) if $communities[0]->level == 0;
310              
311 4         9 my @subcommunities = map {
312 1         2 $_->subcommunities
313             } @communities;
314              
315 1         7 return _build_hierarchy(@subcommunities), SNA::Network::CommunityStructure->new(@communities);
316             }
317              
318              
319             =head1 AUTHOR
320              
321             Darko Obradovic, C<< >>
322              
323             =head1 BUGS
324              
325             Please report any bugs or feature requests to C, or through
326             the web interface at L. I will be notified, and then you'll
327             automatically be notified of progress on your bug as I make changes.
328              
329              
330              
331              
332             =head1 SUPPORT
333              
334             You can find documentation for this module with the perldoc command.
335              
336             perldoc SNA::Network
337              
338              
339             You can also look for information at:
340              
341             =over 4
342              
343             =item * RT: CPAN's request tracker
344              
345             L
346              
347             =item * AnnoCPAN: Annotated CPAN documentation
348              
349             L
350              
351             =item * CPAN Ratings
352              
353             L
354              
355             =item * Search CPAN
356              
357             L
358              
359             =back
360              
361              
362             =head1 ACKNOWLEDGEMENTS
363              
364              
365             =head1 COPYRIGHT & LICENSE
366              
367             Copyright 2012 Darko Obradovic, all rights reserved.
368              
369             This program is free software; you can redistribute it and/or modify it
370             under the same terms as Perl itself.
371              
372              
373             =cut
374              
375             1;
376