Coverage Report

Created: 2026-10-22 06:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/igraph/src/community/modularity.c
Line
Count
Source
1
/*
2
   igraph library.
3
   Copyright (C) 2007-2020 The igraph development team
4
5
   This program is free software; you can redistribute it and/or modify
6
   it under the terms of the GNU General Public License as published by
7
   the Free Software Foundation; either version 2 of the License, or
8
   (at your option) any later version.
9
10
   This program is distributed in the hope that it will be useful,
11
   but WITHOUT ANY WARRANTY; without even the implied warranty of
12
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
   GNU General Public License for more details.
14
15
   You should have received a copy of the GNU General Public License
16
   along with this program; if not, write to the Free Software
17
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
   02110-1301 USA
19
20
*/
21
22
#include "igraph_community.h"
23
24
#include "igraph_interface.h"
25
#include "igraph_structural.h"
26
27
#include "community/community_internal.h"
28
29
/**
30
 * \function igraph_modularity
31
 * \brief Calculates the modularity of a graph with respect to some clusters or vertex types.
32
 *
33
 * The modularity of a graph with respect to some clustering of the vertices
34
 * (or assignment of vertex types)
35
 * measures how strongly separated the different clusters are from each
36
 * other compared to a random null model. It is defined as
37
 *
38
 * </para><para>
39
 * <code>Q = 1/(2m) sum_ij (A_ij - γ k_i k_j / (2m)) δ(c_i,c_j)</code>,
40
 *
41
 * </para><para>
42
 * where \c m is the number of edges, <code>A_ij</code> is the adjacency matrix,
43
 * \c k_i is the degree of vertex \c i, \c c_i is the cluster that vertex \c i belongs to
44
 * (or its vertex type), <code>δ(i,j)=1</code> if <code>i=j</code> and 0 otherwise,
45
 * and the sum goes over all \c i, \c j pairs of vertices. Note that in this formula,
46
 * the diagonal of the adjacency matrix contains twice the number of self-loops.
47
 *
48
 * </para><para>
49
 * The resolution parameter \c γ allows weighting the random null model, which
50
 * might be useful when finding partitions with a high modularity. Maximizing modularity
51
 * with higher values of the resolution parameter typically results in more, smaller clusters
52
 * when finding partitions with a high modularity. Lower values typically result in
53
 * fewer, larger clusters. The original definition of modularity is retrieved
54
 * when setting <code>γ = 1</code>.
55
 *
56
 * </para><para>
57
 * Modularity can also be calculated on directed graphs. This only requires a relatively
58
 * modest change,
59
 *
60
 * </para><para>
61
 * <code>Q = 1/m sum_ij (A_ij - γ k^out_i k^in_j / m) δ(c_i,c_j)</code>,
62
 *
63
 * </para><para>
64
 * where \c k^out_i is the out-degree of node \c i and \c k^in_j is the in-degree of node \c j.
65
 *
66
 * </para><para>
67
 * Modularity on weighted graphs is also meaningful. When taking
68
 * edge weights into account, \c A_ij equals the weight of the corresponding edge
69
 * (or 0 if there is no edge), \c k_i is the strength (i.e. the weighted degree) of
70
 * vertex \c i, with similar counterparts for a directed graph, and \c m is the total
71
 * weight of all edges.
72
 *
73
 * </para><para>
74
 * Note that the modularity is not well-defined for graphs with no edges.
75
 * igraph returns \c NaN for graphs with no edges; see
76
 * https://github.com/igraph/igraph/issues/1539 for
77
 * a detailed discussion.
78
 *
79
 * </para><para>
80
 * For the original definition of modularity, see Newman, M. E. J., and Girvan, M.
81
 * (2004). Finding and evaluating community structure in networks.
82
 * Physical Review E 69, 026113. https://doi.org/10.1103/PhysRevE.69.026113
83
 *
84
 * </para><para>
85
 * For the directed definition of modularity, see Leicht, E. A., and Newman, M. E.
86
 * J. (2008). Community Structure in Directed Networks. Physical Review Letters 100,
87
 * 118703. https://doi.org/10.1103/PhysRevLett.100.118703
88
 *
89
 * </para><para>
90
 * For the introduction of the resolution parameter \c γ, see Reichardt, J., and
91
 * Bornholdt, S. (2006). Statistical mechanics of community detection. Physical
92
 * Review E 74, 016110. https://doi.org/10.1103/PhysRevE.74.016110
93
 *
94
 * \param graph      The input graph.
95
 * \param membership Numeric vector of integer values which gives the type of each
96
 *                   vertex, i.e. the cluster to which it belongs.
97
 *                   It does not have to be consecutive, i.e. empty communities
98
 *                   are allowed. For better performance, ensure that community
99
 *                   indices are nonnegative and smaller than the vertex count.
100
 *                   This can be ensured using \ref igraph_reindex_membership().
101
 * \param weights    Weight vector or \c NULL if no weights are specified.
102
 * \param resolution The resolution parameter \c γ. Must not be negative.
103
 *                   Set it to 1 to use the classical definition of modularity.
104
 * \param directed   Whether to use the directed or undirected version of modularity.
105
 *                   Ignored for undirected graphs.
106
 * \param modularity Pointer to a real number, the result will be
107
 *                   stored here.
108
 * \return Error code.
109
 *
110
 * \sa \ref igraph_modularity_matrix().
111
 *
112
 * Time complexity: O(|V|+|E|), the number of vertices plus the number
113
 * of edges, assuming that community indices are nonnegative and smaller
114
 * than the vertex count. Otherwise, O(|V| log |V| + |E|).
115
 */
116
igraph_error_t igraph_modularity(const igraph_t *graph,
117
                      const igraph_vector_int_t *membership,
118
                      const igraph_vector_t *weights,
119
                      const igraph_real_t resolution,
120
                      const igraph_bool_t directed,
121
52.6k
                      igraph_real_t *modularity) {
122
123
52.6k
    const igraph_int_t vcount = igraph_vcount(graph);
124
52.6k
    const igraph_int_t ecount = igraph_ecount(graph);
125
52.6k
    const igraph_vector_int_t *p_membership;
126
52.6k
    igraph_vector_int_t i_membership;
127
52.6k
    igraph_bool_t using_i_membership = false;
128
52.6k
    igraph_vector_t k_out, k_in;
129
52.6k
    igraph_int_t min_cluster_id, max_cluster_id, no_of_partitions;
130
52.6k
    igraph_real_t e; /* count/fraction of edges/weights within partitions */
131
52.6k
    igraph_real_t m; /* edge count / weight sum */
132
52.6k
    igraph_int_t c1, c2;
133
    /* Only consider the graph as directed if it actually is directed */
134
52.6k
    igraph_bool_t use_directed = directed && igraph_is_directed(graph);
135
52.6k
    igraph_real_t directed_multiplier = (use_directed ? 1 : 2);
136
137
52.6k
    if (igraph_vector_int_size(membership) != vcount) {
138
0
        IGRAPH_ERROR("Membership vector size differs from number of vertices.",
139
0
                     IGRAPH_EINVAL);
140
0
    }
141
52.6k
    if (resolution < 0.0) {
142
0
      IGRAPH_ERROR("The resolution parameter must not be negative.", IGRAPH_EINVAL);
143
0
    }
144
145
52.6k
    if (ecount == 0) {
146
        /* Special case: the modularity of graphs with no edges is not
147
         * well-defined */
148
205
        *modularity = IGRAPH_NAN;
149
205
        return IGRAPH_SUCCESS;
150
205
    }
151
152
    /* At this point, the 'membership' vector does not have length zero,
153
       thus it is safe to call igraph_vector_int_minmax(). */
154
155
    /* If community indices are outside of the standard range, automatically
156
     * reindex them. */
157
52.4k
    igraph_vector_int_minmax(membership, &min_cluster_id, &max_cluster_id);
158
52.4k
    if (min_cluster_id < 0 || max_cluster_id >= vcount) {
159
41.8k
        IGRAPH_CHECK(igraph_vector_int_init_copy(&i_membership, membership));
160
41.8k
        IGRAPH_FINALLY(igraph_vector_int_destroy, &i_membership);
161
41.8k
        IGRAPH_CHECK(igraph_i_reindex_membership_large(&i_membership, NULL, &no_of_partitions));
162
41.8k
        p_membership = &i_membership;
163
41.8k
        using_i_membership = true;
164
41.8k
    } else {
165
10.5k
        no_of_partitions = max_cluster_id + 1;
166
10.5k
        p_membership = membership;
167
10.5k
    }
168
169
52.4k
    IGRAPH_VECTOR_INIT_FINALLY(&k_out, no_of_partitions);
170
52.4k
    IGRAPH_VECTOR_INIT_FINALLY(&k_in, no_of_partitions);
171
172
52.4k
    e = 0.0;
173
52.4k
    if (weights) {
174
52.4k
        if (igraph_vector_size(weights) != ecount)
175
0
            IGRAPH_ERROR("Weight vector size differs from number of edges.",
176
52.4k
                         IGRAPH_EINVAL);
177
52.4k
        m = 0.0;
178
2.57M
        for (igraph_int_t i = 0; i < ecount; i++) {
179
2.52M
            igraph_real_t w = VECTOR(*weights)[i];
180
2.52M
            if (w < 0) {
181
0
                IGRAPH_ERROR("Negative weight in weight vector.", IGRAPH_EINVAL);
182
0
            }
183
2.52M
            c1 = VECTOR(*p_membership)[ IGRAPH_FROM(graph, i) ];
184
2.52M
            c2 = VECTOR(*p_membership)[ IGRAPH_TO(graph, i) ];
185
2.52M
            if (c1 == c2) {
186
1.30M
                e += directed_multiplier * w;
187
1.30M
            }
188
2.52M
            VECTOR(k_out)[c1] += w;
189
2.52M
            VECTOR(k_in)[c2]  += w;
190
2.52M
            m += w;
191
2.52M
        }
192
52.4k
    } else {
193
0
        m = ecount;
194
0
        for (igraph_int_t i = 0; i < ecount; i++) {
195
0
            c1 = VECTOR(*p_membership)[ IGRAPH_FROM(graph, i) ];
196
0
            c2 = VECTOR(*p_membership)[ IGRAPH_TO(graph, i) ];
197
0
            if (c1 == c2) {
198
0
                e += directed_multiplier;
199
0
            }
200
0
            VECTOR(k_out)[c1] += 1;
201
0
            VECTOR(k_in)[c2]  += 1;
202
0
        }
203
0
    }
204
205
52.4k
    if (!use_directed) {
206
        /* Graph is undirected, simply add vectors.
207
         * Vectors are the same size, calls below can't fail. */
208
3.54k
        igraph_vector_add(&k_out, &k_in);
209
3.54k
        igraph_vector_update(&k_in, &k_out);
210
3.54k
    }
211
212
    /* Divide all vectors by total weight. */
213
52.4k
    igraph_vector_scale(&k_out, 1.0/( directed_multiplier * m ) );
214
52.4k
    igraph_vector_scale(&k_in, 1.0/( directed_multiplier * m ) );
215
52.4k
    e /= directed_multiplier * m;
216
217
52.4k
    if (m > 0) {
218
52.4k
        *modularity = e;
219
2.24M
        for (igraph_int_t i = 0; i < no_of_partitions; i++) {
220
2.19M
            *modularity -= resolution * VECTOR(k_out)[i] * VECTOR(k_in)[i];
221
2.19M
        }
222
52.4k
    } else {
223
0
        *modularity = IGRAPH_NAN;
224
0
    }
225
226
52.4k
    igraph_vector_destroy(&k_out);
227
52.4k
    igraph_vector_destroy(&k_in);
228
52.4k
    IGRAPH_FINALLY_CLEAN(2);
229
230
52.4k
    if (using_i_membership) {
231
41.8k
        igraph_vector_int_destroy(&i_membership);
232
41.8k
        IGRAPH_FINALLY_CLEAN(1);
233
41.8k
    }
234
235
52.4k
    return IGRAPH_SUCCESS;
236
52.4k
}
237
238
static igraph_error_t igraph_i_modularity_matrix_get_adjacency(
239
           const igraph_t *graph, igraph_matrix_t *res,
240
3.59k
           const igraph_vector_t *weights, igraph_bool_t directed) {
241
242
    /* Specifically used to handle weights and/or ignore direction */
243
3.59k
    igraph_eit_t edgeit;
244
3.59k
    igraph_int_t no_of_nodes = igraph_vcount(graph);
245
3.59k
    igraph_int_t from, to;
246
247
3.59k
    IGRAPH_CHECK(igraph_matrix_resize(res, no_of_nodes, no_of_nodes));
248
3.59k
    igraph_matrix_null(res);
249
3.59k
    IGRAPH_CHECK(igraph_eit_create(graph, igraph_ess_all(IGRAPH_EDGEORDER_ID), &edgeit));
250
3.59k
    IGRAPH_FINALLY(igraph_eit_destroy, &edgeit);
251
252
3.59k
    if (weights) {
253
98.1k
        for (; !IGRAPH_EIT_END(edgeit); IGRAPH_EIT_NEXT(edgeit)) {
254
94.5k
            igraph_int_t edge = IGRAPH_EIT_GET(edgeit);
255
94.5k
            from = IGRAPH_FROM(graph, edge);
256
94.5k
            to = IGRAPH_TO(graph, edge);
257
94.5k
            MATRIX(*res, from, to) += VECTOR(*weights)[edge];
258
94.5k
            if (!directed) {
259
0
                MATRIX(*res, to, from) += VECTOR(*weights)[edge];
260
0
            }
261
94.5k
        }
262
3.59k
    } else {
263
0
        for (; !IGRAPH_EIT_END(edgeit); IGRAPH_EIT_NEXT(edgeit)) {
264
0
            igraph_int_t edge = IGRAPH_EIT_GET(edgeit);
265
0
            from = IGRAPH_FROM(graph, edge);
266
0
            to = IGRAPH_TO(graph, edge);
267
0
            MATRIX(*res, from, to) += 1;
268
0
            if (!directed) {
269
0
                MATRIX(*res, to, from) += 1;
270
0
            }
271
0
        }
272
0
    }
273
274
3.59k
    igraph_eit_destroy(&edgeit);
275
3.59k
    IGRAPH_FINALLY_CLEAN(1);
276
3.59k
    return IGRAPH_SUCCESS;
277
3.59k
}
278
279
/**
280
 * \function igraph_modularity_matrix
281
 * \brief Calculates the modularity matrix.
282
 *
283
 * This function returns the modularity matrix, which is defined as
284
 *
285
 * </para><para>
286
 * <code>B_ij = A_ij - γ k_i k_j / (2m)</code>
287
 *
288
 * </para><para>
289
 * for undirected graphs, where \c A_ij is the adjacency matrix, \c γ is the
290
 * resolution parameter, \c k_i is the degree of vertex \c i, and \c m is the
291
 * number of edges in the graph. When there are no edges, or the weights add up
292
 * to zero, the result is undefined.
293
 *
294
 * </para><para>
295
 * For directed graphs the modularity matrix is changed to
296
 *
297
 * </para><para>
298
 * <code>B_ij = A_ij - γ k^out_i k^in_j / m</code>
299
 *
300
 * </para><para>
301
 * where <code>k^out_i</code> is the out-degree of node \c i and <code>k^in_j</code> is the
302
 * in-degree of node \c j.
303
 *
304
 * </para><para>
305
 * Note that self-loops in undirected graphs are multiplied by 2 in this
306
 * implementation. If weights are specified, the weighted counterparts of the adjacency
307
 * matrix and degrees are used.
308
 *
309
 * \param graph      The input graph.
310
 * \param weights    Edge weights, pointer to a vector. If this is a null pointer
311
 *                   then every edge is assumed to have a weight of 1.
312
 * \param resolution The resolution parameter \c γ. Must not be negative.
313
 *                   Default is 1. Lower values favor fewer, larger communities;
314
 *                   higher values favor more, smaller communities.
315
 * \param modmat     Pointer to an initialized matrix in which the modularity
316
 *                   matrix is stored.
317
 * \param directed   For directed graphs: if the edges should be treated as
318
 *                   undirected. For undirected graphs this is ignored.
319
 * \return Error code.
320
 *
321
 * \sa \ref igraph_modularity().
322
 */
323
igraph_error_t igraph_modularity_matrix(const igraph_t *graph,
324
                             const igraph_vector_t *weights,
325
                             const igraph_real_t resolution,
326
                             igraph_matrix_t *modmat,
327
3.59k
                             igraph_bool_t directed) {
328
329
3.59k
    const igraph_int_t no_of_nodes = igraph_vcount(graph);
330
3.59k
    const igraph_int_t no_of_edges = igraph_ecount(graph);
331
3.59k
    const igraph_real_t sw = weights ? igraph_vector_sum(weights) : no_of_edges;
332
3.59k
    igraph_vector_t deg, in_deg, out_deg;
333
3.59k
    igraph_real_t scaling_factor;
334
335
3.59k
    if (weights && igraph_vector_size(weights) != no_of_edges) {
336
0
        IGRAPH_ERROR("Invalid weight vector length.", IGRAPH_EINVAL);
337
0
    }
338
339
3.59k
    if (resolution < 0.0) {
340
0
        IGRAPH_ERROR("The resolution parameter must not be negative.", IGRAPH_EINVAL);
341
0
    }
342
343
3.59k
    if (!igraph_is_directed(graph)) {
344
0
        directed = false;
345
0
    }
346
3.59k
    IGRAPH_CHECK(igraph_i_modularity_matrix_get_adjacency(graph, modmat, weights, directed));
347
348
    /* Performance notes:
349
     *  - Iterating in column-major order makes a large difference.
350
     *  - Applying the scaling_factor to in_deg (or out_deg) first to reduce the
351
     *    number of multiplications does not make an appreciable performance
352
     *    difference. However, doing this in the undirected case causes the result
353
     *    matrix to sometimes not be strictly symmetric due to the non-associativity
354
     *    of floating point multiplication.
355
     */
356
357
3.59k
    if (directed) {
358
3.59k
        IGRAPH_VECTOR_INIT_FINALLY(&in_deg, no_of_nodes);
359
3.59k
        IGRAPH_VECTOR_INIT_FINALLY(&out_deg, no_of_nodes);
360
361
3.59k
        IGRAPH_CHECK(igraph_strength(graph, &in_deg, igraph_vss_all(), IGRAPH_IN,
362
3.59k
                                     IGRAPH_LOOPS, weights));
363
3.59k
        IGRAPH_CHECK(igraph_strength(graph, &out_deg, igraph_vss_all(), IGRAPH_OUT,
364
3.59k
                                     IGRAPH_LOOPS, weights));
365
366
3.59k
        scaling_factor = resolution / sw;
367
368
162k
        for (igraph_int_t j = 0; j < no_of_nodes; j++) {
369
8.99M
            for (igraph_int_t i = 0; i < no_of_nodes; i++) {
370
8.84M
                MATRIX(*modmat, i, j) -= VECTOR(out_deg)[i] * VECTOR(in_deg)[j] * scaling_factor;
371
8.84M
            }
372
159k
        }
373
3.59k
        igraph_vector_destroy(&in_deg);
374
3.59k
        igraph_vector_destroy(&out_deg);
375
3.59k
        IGRAPH_FINALLY_CLEAN(2);
376
3.59k
    } else {
377
0
        IGRAPH_VECTOR_INIT_FINALLY(&deg, no_of_nodes);
378
379
0
        IGRAPH_CHECK(igraph_strength(graph, &deg, igraph_vss_all(), IGRAPH_ALL,
380
0
                                     IGRAPH_LOOPS, weights));
381
382
0
        scaling_factor = resolution / 2.0 / sw;
383
384
0
        for (igraph_int_t j = 0; j < no_of_nodes; j++) {
385
0
            for (igraph_int_t i = 0; i < no_of_nodes; i++) {
386
0
                MATRIX(*modmat, i, j) -= VECTOR(deg)[i] * VECTOR(deg)[j] * scaling_factor;
387
0
            }
388
0
        }
389
0
        igraph_vector_destroy(&deg);
390
0
        IGRAPH_FINALLY_CLEAN(1);
391
0
    }
392
393
3.59k
    return IGRAPH_SUCCESS;
394
3.59k
}