File Coverage

blib/lib/SNA/Network/Algorithm/Louvain.pm
Criterion Covered Total %
statement 130 130 100.0
branch 24 24 100.0
condition 2 2 100.0
subroutine 13 13 100.0
pod 1 1 100.0
total 170 170 100.0


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