Coverage Report

Created: 2026-07-22 06:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/igraph/src/misc/mixing.c
Line
Count
Source
1
/*
2
   igraph library.
3
   Copyright (C) 2009-2025  The igraph development team <igraph@igraph.org>
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, see <https://www.gnu.org/licenses/>.
17
*/
18
19
#include "igraph_mixing.h"
20
21
#include "igraph_interface.h"
22
#include "igraph_structural.h"
23
24
/**
25
 * \function igraph_assortativity_nominal
26
 * \brief Assortativity of a graph based on vertex categories.
27
 *
28
 * Assuming the vertices of the input graph belong to different
29
 * categories, this function calculates the assortativity coefficient of
30
 * the graph. The assortativity coefficient is between minus one and one
31
 * and it is one if all connections stay within categories, it is
32
 * minus one, if the network is perfectly disassortative. For a
33
 * randomly connected network it is (asymptotically) zero.
34
 *
35
 * </para><para>
36
 * The unnormalized version, computed when \p normalized is set to false,
37
 * is identical to the modularity, and is defined as follows for
38
 * directed networks:
39
 * </para><para>
40
 * <code>1/m sum_ij (A_ij - k^out_i k^in_j / m) d(i,j)</code>,
41
 * </para><para>
42
 * where \c m denotes the number of edges, \c A_ij is the adjacency matrix,
43
 * <code>k^out</code> and <code>k^in</code> are the out- and in-degrees,
44
 * and <code>d(i,j)</code> is one if vertices \c i and \c j are in the same
45
 * category and zero otherwise.
46
 *
47
 * </para><para>
48
 * The normalized assortativity coefficient is obtained by dividing the
49
 * previous expression by
50
 * </para><para>
51
 * <code>1/m sum_ij (m - k^out_i k^in_j d(i,j) / m)</code>.
52
 * </para><para>
53
 * It can take any value within the interval [-1, 1].
54
 *
55
 * </para><para>
56
 * Undirected graphs are effectively treated as directed ones with all-reciprocal
57
 * edges. Thus, self-loops are taken into account twice in undirected graphs.
58
 *
59
 * </para><para>
60
 * References:
61
 *
62
 * </para><para>
63
 * M. E. J. Newman: Mixing patterns in networks,
64
 * Phys. Rev. E 67, 026126 (2003)
65
 * https://doi.org/10.1103/PhysRevE.67.026126.
66
 * See section II and equation (2) for the definition of the concept.
67
 *
68
 * </para><para>
69
 * For an educational overview of assortativity, see
70
 * M. E. J. Newman,
71
 * Networks: An Introduction, Oxford University Press (2010).
72
 * https://doi.org/10.1093/acprof%3Aoso/9780199206650.001.0001.
73
 *
74
 * \param graph The input graph, it can be directed or undirected.
75
 * \param weights Weighted nominal assortativity is not currently implemented.
76
 *    Pass \c NULL to ignore.
77
 * \param types Integer vector giving the vertex categories. The types
78
 *    are represented by integers starting at zero.
79
 * \param res Pointer to a real variable, the result is stored here.
80
 * \param directed Boolean, it gives whether to consider edge
81
 *    directions in a directed graph. It is ignored for undirected
82
 *    graphs.
83
 * \param normalized Boolean, whether to compute the usual normalized
84
 *    assortativity. The unnormalized version is identical to
85
 *    modularity. Supply true here to compute the standard assortativity.
86
 * \return Error code.
87
 *
88
 * Time complexity: O(|E|+t), |E| is the number of edges, t is the
89
 * number of vertex types.
90
 *
91
 * \sa \ref igraph_assortativity() for computing the assortativity
92
 * based on continuous vertex values instead of discrete categories.
93
 * \ref igraph_modularity() to compute generalized modularity.
94
 * \ref igraph_joint_type_distribution() to obtain the mixing matrix.
95
 *
96
 * \example examples/simple/igraph_assortativity_nominal.c
97
 */
98
99
igraph_error_t igraph_assortativity_nominal(
100
        const igraph_t *graph, const igraph_vector_t *weights,
101
        const igraph_vector_int_t *types,
102
        igraph_real_t *res,
103
3.43k
        igraph_bool_t directed, igraph_bool_t normalized) {
104
105
3.43k
    igraph_int_t no_of_nodes = igraph_vcount(graph);
106
3.43k
    igraph_int_t no_of_edges = igraph_ecount(graph);
107
3.43k
    igraph_real_t no_of_edges_real = (igraph_real_t) no_of_edges;   /* for divisions */
108
3.43k
    igraph_int_t no_of_types;
109
3.43k
    igraph_vector_int_t ai, bi, eii;
110
3.43k
    igraph_real_t sumaibi = 0.0, sumeii = 0.0;
111
112
3.43k
    if (weights) {
113
0
        IGRAPH_ERROR("Weighted nominal assortativity is not yet implemented.",
114
0
                     IGRAPH_UNIMPLEMENTED);
115
0
    }
116
117
3.43k
    if (igraph_vector_int_size(types) != no_of_nodes) {
118
0
        IGRAPH_ERROR("Invalid types vector length.", IGRAPH_EINVAL);
119
0
    }
120
121
3.43k
    if (no_of_nodes == 0) {
122
1
        *res = IGRAPH_NAN;
123
1
        return IGRAPH_SUCCESS;
124
1
    }
125
126
    /* 'types' length > 0 here, safe to call vector_min() */
127
3.42k
    if (igraph_vector_int_min(types) < 0) {
128
0
        IGRAPH_ERROR("Vertex types must not be negative.", IGRAPH_EINVAL);
129
0
    }
130
131
3.42k
    directed = directed && igraph_is_directed(graph);
132
133
3.42k
    no_of_types = igraph_vector_int_max(types) + 1;
134
3.42k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&ai, no_of_types);
135
3.42k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&bi, no_of_types);
136
3.42k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&eii, no_of_types);
137
138
109k
    for (igraph_int_t e = 0; e < no_of_edges; e++) {
139
105k
        igraph_int_t from = IGRAPH_FROM(graph, e);
140
105k
        igraph_int_t to = IGRAPH_TO(graph, e);
141
105k
        igraph_int_t from_type = VECTOR(*types)[from];
142
105k
        igraph_int_t to_type = VECTOR(*types)[to];
143
144
105k
        VECTOR(ai)[from_type] += 1;
145
105k
        VECTOR(bi)[to_type] += 1;
146
105k
        if (from_type == to_type) {
147
38.2k
            VECTOR(eii)[from_type] += 1;
148
38.2k
        }
149
105k
        if (!directed) {
150
0
            if (from_type == to_type) {
151
0
                VECTOR(eii)[from_type] += 1;
152
0
            }
153
0
            VECTOR(ai)[to_type] += 1;
154
0
            VECTOR(bi)[from_type] += 1;
155
0
        }
156
105k
    }
157
158
164k
    for (igraph_int_t i = 0; i < no_of_types; i++) {
159
161k
        sumaibi += (VECTOR(ai)[i] / no_of_edges_real) * (VECTOR(bi)[i] / no_of_edges_real);
160
161k
        sumeii  += (VECTOR(eii)[i] / no_of_edges_real);
161
161k
    }
162
163
3.42k
    if (!directed) {
164
0
        sumaibi /= 4.0;
165
0
        sumeii  /= 2.0;
166
0
    }
167
168
3.42k
    if (normalized) {
169
3.42k
        *res = (sumeii - sumaibi) / (1.0 - sumaibi);
170
3.42k
    } else {
171
0
        *res = (sumeii - sumaibi);
172
0
    }
173
174
3.42k
    igraph_vector_int_destroy(&eii);
175
3.42k
    igraph_vector_int_destroy(&bi);
176
3.42k
    igraph_vector_int_destroy(&ai);
177
3.42k
    IGRAPH_FINALLY_CLEAN(3);
178
179
3.42k
    return IGRAPH_SUCCESS;
180
3.42k
}
181
182
/**
183
 * \function igraph_assortativity
184
 * \brief Assortativity based on numeric properties of vertices.
185
 *
186
 * This function calculates the assortativity coefficient of a
187
 * graph based on given values \c x_i for each vertex \c i. This type of
188
 * assortativity coefficient equals the Pearson correlation of the values
189
 * at the two ends of the edges.
190
 *
191
 * </para><para>
192
 * The unnormalized covariance of values, computed when \p normalized is
193
 * set to false, is defined as follows in a directed graph:
194
 * </para><para>
195
 * <code>cov(x_out, x_in) = 1/m sum_ij (A_ij - k^out_i k^in_j / m) x_i x_j</code>,
196
 * </para><para>
197
 * where \c m denotes the number of edges, \c A_ij is the adjacency matrix, and
198
 * <code>k^out</code> and <code>k^in</code> are the out- and in-degrees.
199
 * \c x_out and \c x_in refer to the sets of vertex values at the start and end of
200
 * the directed edges.
201
 *
202
 * </para><para>
203
 * The normalized covariance, i.e. Pearson correlation, is obtained by dividing
204
 * the previous expression by
205
 * <code>sqrt(var(x_out)) sqrt(var(x_in))</code>, where
206
 * </para><para>
207
 * <code>var(x_out) = 1/m sum_i k^out_i x_i^2 - (1/m sum_i k^out_i x_i^2)^2</code>
208
 * </para><para>
209
 * <code>var(x_in)  = 1/m sum_j k^in_j x_j^2 - (1/m sum_j k^in_j x_j^2)^2</code>
210
 *
211
 * </para><para>
212
 * Undirected graphs are effectively treated as directed graphs where all edges
213
 * are reciprocal. Therefore, self-loops are effectively considered twice in
214
 * undirected graphs.
215
 *
216
 * </para><para>
217
 * When edge weights are given, they are effectively treated as edge multiplicities.
218
 * The above formulas are valid for weighted graph as well when \c m is interpreted
219
 * as the total edge weight (instead of the edge count) and \c k as vertex strengths
220
 * (instead of degrees).
221
 *
222
 * </para><para>
223
 * References:
224
 *
225
 * </para><para>
226
 * M. E. J. Newman: Mixing patterns
227
 * in networks, Phys. Rev. E 67, 026126 (2003)
228
 * https://doi.org/10.1103/PhysRevE.67.026126.
229
 * See section III and equation (21) for the definition, and equation (26) for
230
 * performing the calculation in directed graphs with the degrees as values.
231
 *
232
 * </para><para>
233
 * M. E. J. Newman: Assortative mixing in networks,
234
 * Phys. Rev. Lett. 89, 208701 (2002)
235
 * https://doi.org/10.1103/PhysRevLett.89.208701.
236
 * See equation (4) for performing the calculation in undirected
237
 * graphs with the degrees as values.
238
 *
239
 * </para><para>
240
 * For an educational overview of the concept of assortativity, see
241
 * M. E. J. Newman,
242
 * Networks: An Introduction, Oxford University Press (2010).
243
 * https://doi.org/10.1093/acprof%3Aoso/9780199206650.001.0001.
244
 *
245
 * \param graph The input graph, it can be directed or undirected.
246
 * \param weights The edge weights. Pass \c NULL to compute unweighed
247
 *     assortativity, which in effect assumes all weights to be 1.
248
 * \param values The vertex values, these can be arbitrary numeric
249
 *     values.
250
 * \param values_in A second value vector to be used for the incoming
251
 *     edges when calculating assortativity for a directed graph.
252
 *     Supply \c NULL here if you want to use the same values
253
 *     for outgoing and incoming edges. This argument is ignored
254
 *     (with a warning) if it is not a null pointer and the undirected
255
 *     assortativity coefficient is being calculated.
256
 * \param res Pointer to a real variable, the result is stored here.
257
 * \param directed Boolean, whether to consider edge directions for
258
 *     directed graphs. It is ignored for undirected graphs.
259
 * \param normalized Boolean, whether to compute the normalized
260
 *     covariance, i.e. Pearson correlation. Supply true here to
261
 *     compute the standard assortativity.
262
 * \return Error code.
263
 *
264
 * Time complexity: O(|E|), linear in the number of edges of the
265
 * graph.
266
 *
267
 * \sa \ref igraph_assortativity_nominal() if you have discrete vertex
268
 * categories instead of numeric labels, and \ref
269
 * igraph_assortativity_degree() for the special case of assortativity
270
 * based on vertex degrees.
271
 */
272
273
igraph_error_t igraph_assortativity(
274
        const igraph_t *graph, const igraph_vector_t *weights,
275
        const igraph_vector_t *values, const igraph_vector_t *values_in,
276
        igraph_real_t *res,
277
0
        igraph_bool_t directed, igraph_bool_t normalized) {
278
279
0
    const igraph_int_t no_of_nodes = igraph_vcount(graph);
280
0
    const igraph_int_t no_of_edges = igraph_ecount(graph);
281
0
    igraph_real_t total_weight;
282
283
0
    directed = directed && igraph_is_directed(graph);
284
285
0
    if (weights && igraph_vector_size(weights) != no_of_edges) {
286
0
        IGRAPH_ERRORF("Weight vector length (%" IGRAPH_PRId ") does not match number of edges (%" IGRAPH_PRId ").",
287
0
                      IGRAPH_EINVAL,
288
0
                      igraph_vector_size(weights), no_of_edges);
289
0
    }
290
291
0
    if (!directed && values_in) {
292
0
        IGRAPH_WARNING(
293
0
            "Incoming vertex values are ignored when calculating undirected assortativity.");
294
0
    }
295
296
0
    if (igraph_vector_size(values) != no_of_nodes) {
297
0
        IGRAPH_ERROR("Invalid vertex values vector length.", IGRAPH_EINVAL);
298
0
    }
299
300
0
    if (values_in && igraph_vector_size(values_in) != no_of_nodes) {
301
0
        IGRAPH_ERROR("Invalid incoming vertex values vector length.", IGRAPH_EINVAL);
302
0
    }
303
304
0
    total_weight = weights ? igraph_vector_sum(weights) : no_of_edges;
305
306
0
    if (!directed) {
307
0
        igraph_real_t num1 = 0.0, num2 = 0.0, den1 = 0.0;
308
309
0
        if (weights) {
310
0
            for (igraph_int_t e = 0; e < no_of_edges; e++) {
311
0
                igraph_int_t from = IGRAPH_FROM(graph, e);
312
0
                igraph_int_t to = IGRAPH_TO(graph, e);
313
0
                igraph_real_t from_value = VECTOR(*values)[from];
314
0
                igraph_real_t to_value = VECTOR(*values)[to];
315
0
                igraph_real_t w = VECTOR(*weights)[e];
316
317
0
                num1 += w * from_value * to_value;
318
0
                num2 += w * (from_value + to_value);
319
0
                if (normalized) {
320
0
                    den1 += w * (from_value * from_value + to_value * to_value);
321
0
                }
322
0
            }
323
0
        } else {
324
0
            for (igraph_int_t e = 0; e < no_of_edges; e++) {
325
0
                igraph_int_t from = IGRAPH_FROM(graph, e);
326
0
                igraph_int_t to = IGRAPH_TO(graph, e);
327
0
                igraph_real_t from_value = VECTOR(*values)[from];
328
0
                igraph_real_t to_value = VECTOR(*values)[to];
329
330
0
                num1 += from_value * to_value;
331
0
                num2 += from_value + to_value;
332
0
                if (normalized) {
333
0
                    den1 += from_value * from_value + to_value * to_value;
334
0
                }
335
0
            }
336
0
        }
337
338
0
        num1 /= total_weight;
339
0
        if (normalized) {
340
0
            den1 /= total_weight * 2.0;
341
0
        }
342
0
        num2 /= total_weight * 2.0;
343
0
        num2 = num2 * num2;
344
345
0
        if (normalized) {
346
0
            *res = (num1 - num2) / (den1 - num2);
347
0
        } else {
348
0
            *res = (num1 - num2);
349
0
        }
350
351
0
    } else {
352
0
        igraph_real_t num1 = 0.0, num2 = 0.0, num3 = 0.0,
353
0
                      den1 = 0.0, den2 = 0.0;
354
0
        igraph_real_t num, den;
355
356
0
        if (!values_in) {
357
0
            values_in = values;
358
0
        }
359
360
0
        if (weights) {
361
0
            for (igraph_int_t e = 0; e < no_of_edges; e++) {
362
0
                igraph_int_t from = IGRAPH_FROM(graph, e);
363
0
                igraph_int_t to = IGRAPH_TO(graph, e);
364
0
                igraph_real_t from_value = VECTOR(*values)[from];
365
0
                igraph_real_t to_value = VECTOR(*values_in)[to];
366
0
                igraph_real_t w = VECTOR(*weights)[e];
367
368
0
                num1 += w * from_value * to_value;
369
0
                num2 += w * from_value;
370
0
                num3 += w * to_value;
371
0
                if (normalized) {
372
0
                    den1 += w * (from_value * from_value);
373
0
                    den2 += w * (to_value * to_value);
374
0
                }
375
0
            }
376
0
        } else {
377
0
            for (igraph_int_t e = 0; e < no_of_edges; e++) {
378
0
                igraph_int_t from = IGRAPH_FROM(graph, e);
379
0
                igraph_int_t to = IGRAPH_TO(graph, e);
380
0
                igraph_real_t from_value = VECTOR(*values)[from];
381
0
                igraph_real_t to_value = VECTOR(*values_in)[to];
382
383
0
                num1 += from_value * to_value;
384
0
                num2 += from_value;
385
0
                num3 += to_value;
386
0
                if (normalized) {
387
0
                    den1 += from_value * from_value;
388
0
                    den2 += to_value * to_value;
389
0
                }
390
0
            }
391
0
        }
392
393
0
        num = num1 - num2 * num3 / total_weight;
394
0
        if (normalized) {
395
0
            den = sqrt(den1 - num2 * num2 / total_weight) *
396
0
                    sqrt(den2 - num3 * num3 / total_weight);
397
398
0
            *res = num / den;
399
0
        } else {
400
0
            *res = num / total_weight;
401
0
        }
402
0
    }
403
404
0
    return IGRAPH_SUCCESS;
405
0
}
406
407
/**
408
 * \function igraph_assortativity_degree
409
 * \brief Assortativity of a graph based on vertex degree.
410
 *
411
 * Assortativity based on vertex degree, please see the discussion at
412
 * the documentation of \ref igraph_assortativity() for details.
413
 * This function simply calls \ref igraph_assortativity() with
414
 * the degrees as the vertex values and normalization enabled.
415
 * In the directed case, it uses out-degrees as out-values and
416
 * in-degrees as in-values.
417
 *
418
 * </para><para>
419
 * For regular graphs, i.e. graphs in which all vertices have the
420
 * same degree, computing degree correlations is not meaningful,
421
 * and this function returns NaN.
422
 *
423
 * \param graph The input graph, it can be directed or undirected.
424
 * \param res Pointer to a real variable, the result is stored here.
425
 * \param directed Boolean, whether to consider edge directions for
426
 *     directed graphs. This argument is ignored for undirected
427
 *     graphs. Supply true here to do the natural thing, i.e. use
428
 *     directed version of the measure for directed graphs and the
429
 *     undirected version for undirected graphs.
430
 * \return Error code.
431
 *
432
 * Time complexity: O(|E|+|V|), |E| is the number of edges, |V| is
433
 * the number of vertices.
434
 *
435
 * \sa \ref igraph_assortativity() for the general function
436
 * calculating assortativity for any kind of numeric vertex values,
437
 * and \ref igraph_joint_degree_distribution() to get the complete
438
 * joint degree distribution.
439
 *
440
 * \example examples/simple/igraph_assortativity_degree.c
441
 */
442
443
igraph_error_t igraph_assortativity_degree(const igraph_t *graph,
444
                                igraph_real_t *res,
445
0
                                igraph_bool_t directed) {
446
447
0
    directed = directed && igraph_is_directed(graph);
448
0
    igraph_int_t no_of_nodes = igraph_vcount(graph);
449
450
    /* This function uses igraph_strength() instead of igraph_degree() in order to obtain
451
     * a vector of reals instead of a vector of integers. */
452
0
    if (directed) {
453
0
        igraph_vector_t indegree, outdegree;
454
0
        IGRAPH_VECTOR_INIT_FINALLY(&indegree, no_of_nodes);
455
0
        IGRAPH_VECTOR_INIT_FINALLY(&outdegree, no_of_nodes);
456
0
        IGRAPH_CHECK(igraph_strength(graph, &indegree, igraph_vss_all(), IGRAPH_IN, IGRAPH_LOOPS, NULL));
457
0
        IGRAPH_CHECK(igraph_strength(graph, &outdegree, igraph_vss_all(), IGRAPH_OUT, IGRAPH_LOOPS, NULL));
458
0
        IGRAPH_CHECK(igraph_assortativity(graph, NULL, &outdegree, &indegree, res, /* directed */ true, /* normalized */ true));
459
0
        igraph_vector_destroy(&indegree);
460
0
        igraph_vector_destroy(&outdegree);
461
0
        IGRAPH_FINALLY_CLEAN(2);
462
0
    } else {
463
0
        igraph_vector_t degree;
464
0
        IGRAPH_VECTOR_INIT_FINALLY(&degree, no_of_nodes);
465
0
        IGRAPH_CHECK(igraph_strength(graph, &degree, igraph_vss_all(), IGRAPH_ALL, IGRAPH_LOOPS, NULL));
466
0
        IGRAPH_CHECK(igraph_assortativity(graph, NULL, &degree, 0, res, /* directed */ false, /* normalized */ true));
467
0
        igraph_vector_destroy(&degree);
468
0
        IGRAPH_FINALLY_CLEAN(1);
469
0
    }
470
471
0
    return IGRAPH_SUCCESS;
472
0
}
473
474
/**
475
 * \function igraph_joint_degree_matrix
476
 * \brief The joint degree matrix of a graph.
477
 *
478
 * In graph theory, the joint degree matrix \c J_ij of a graph gives the number
479
 * of edges, or sum of edge weights, between vertices of degree \c i and degree
480
 * \c j. This function stores \c J_ij into <code>jdm[i-1, j-1]</code>.
481
 * Each edge, including self-loops, is counted precisely once, both in undirected
482
 * and directed graphs.
483
 *
484
 * </para><para>
485
 * <code>sum_(i,j) J_ij</code> is the total number of edges (or total edge weight)
486
 * \c m in the graph, where <code>(i,j)</code> refers to ordered or unordered
487
 * pairs in directed and undirected graphs, respectively. Thus <code>J_ij / m</code>
488
 * is the probability that an edge chosen at random (with probability proportional
489
 * to its weight) connects vertices with degrees \c i and \c j.
490
 *
491
 * </para><para>
492
 * Note that \c J_ij is similar, but not identical to the joint degree
493
 * \em distribution, computed by \ref igraph_joint_degree_distribution(),
494
 * which is defined for \em ordered <code>(i, j)</code> degree
495
 * pairs even in the undirected case. When considering undirected graphs, the
496
 * diagonal of the joint degree distribution is twice that of the joint
497
 * degree matrix.
498
 *
499
 * </para><para>
500
 * References:
501
 *
502
 * </para><para>
503
 * Isabelle Stanton and Ali Pinar:
504
 * Constructing and sampling graphs with a prescribed joint degree distribution.
505
 * ACM J. Exp. Algorithmics 17, Article 3.5 (2012).
506
 * https://doi.org/10.1145/2133803.2330086
507
 *
508
 * \param graph A pointer to an initialized graph object.
509
 * \param weights A vector containing the weights of the edges. If passing a
510
 *        \c NULL pointer, edges will be assumed to have unit weights, i.e.
511
 *        the matrix entries will be connection counts.
512
 * \param jdm A pointer to an initialized matrix that will be resized. The values
513
 *        will be written here.
514
 * \param max_out_degree Number of rows in the result, i.e. the largest (out-)degree
515
 *        to consider. If negative or \ref IGRAPH_UNLIMITED, the largest (out-)degree
516
 *        of the graph will be used.
517
 * \param max_in_degree Number of columns in the result, i.e. the largest (in-)degree
518
 *        to consider. If negative or \ref IGRAPH_UNLIMITED, the largest (in-)degree
519
 *        of the graph will be used.
520
 * \return Error code.
521
 *
522
 * \sa \ref igraph_joint_degree_distribution() to count ordered vertex pairs instead of
523
 * edges, or to obtain a normalized matrix.
524
 *
525
 * Time complexity: O(E), where E is the number of edges in input graph.
526
 */
527
528
igraph_error_t igraph_joint_degree_matrix(
529
        const igraph_t *graph, const igraph_vector_t *weights,
530
        igraph_matrix_t *jdm,
531
0
        igraph_int_t max_out_degree, igraph_int_t max_in_degree) {
532
533
0
    igraph_int_t no_of_nodes = igraph_vcount(graph);
534
0
    igraph_int_t no_of_edges = igraph_ecount(graph);
535
0
    igraph_eit_t eit;
536
0
    igraph_int_t eid;
537
0
    igraph_int_t v1id;
538
0
    igraph_int_t v2id;
539
0
    igraph_int_t v1deg;
540
0
    igraph_int_t v2deg;
541
542
0
    if (weights && igraph_vector_size(weights) != no_of_edges) {
543
0
        IGRAPH_ERRORF("Weight vector length (%" IGRAPH_PRId ") does not match number of edges (%" IGRAPH_PRId ").",
544
0
                      IGRAPH_EINVAL,
545
0
                      igraph_vector_size(weights), no_of_edges);
546
0
    }
547
548
0
    if (igraph_is_directed(graph)) {
549
0
        igraph_vector_int_t out_degrees;
550
0
        igraph_vector_int_t in_degrees;
551
552
        // Compute max degrees
553
0
        IGRAPH_VECTOR_INT_INIT_FINALLY(&out_degrees, no_of_nodes);
554
0
        IGRAPH_VECTOR_INT_INIT_FINALLY(&in_degrees, no_of_nodes);
555
0
        IGRAPH_CHECK(igraph_degree(graph, &out_degrees, igraph_vss_all(), IGRAPH_OUT, IGRAPH_LOOPS));
556
0
        IGRAPH_CHECK(igraph_degree(graph, &in_degrees, igraph_vss_all(), IGRAPH_IN, IGRAPH_LOOPS));
557
558
0
        if (max_out_degree < 0) {
559
0
            max_out_degree = no_of_nodes > 0 ? igraph_vector_int_max(&out_degrees) : 0;
560
0
        }
561
562
0
        if (max_in_degree < 0) {
563
0
            max_in_degree = no_of_nodes > 0 ? igraph_vector_int_max(&in_degrees) : 0;
564
0
        }
565
566
0
        IGRAPH_CHECK(igraph_matrix_resize(jdm, max_out_degree, max_in_degree));
567
0
        igraph_matrix_null(jdm);
568
569
0
        IGRAPH_CHECK(igraph_eit_create(graph, igraph_ess_all(IGRAPH_EDGEORDER_ID), &eit));
570
0
        IGRAPH_FINALLY(igraph_eit_destroy, &eit);
571
0
        for (; !IGRAPH_EIT_END(eit); IGRAPH_EIT_NEXT(eit)) {
572
0
            eid = IGRAPH_EIT_GET(eit);
573
0
            v1id = IGRAPH_FROM(graph, eid);
574
0
            v2id = IGRAPH_TO(graph, eid);
575
0
            v1deg = VECTOR(out_degrees)[v1id];
576
0
            v2deg = VECTOR(in_degrees)[v2id];
577
0
            if (v1deg <= max_out_degree && v2deg <= max_in_degree) {
578
0
                MATRIX(*jdm, v1deg-1, v2deg-1) += weights ? VECTOR(*weights)[eid] : 1;
579
0
            }
580
0
        }
581
582
0
        igraph_eit_destroy(&eit);
583
0
        igraph_vector_int_destroy(&in_degrees);
584
0
        igraph_vector_int_destroy(&out_degrees);
585
0
        IGRAPH_FINALLY_CLEAN(3);
586
587
0
    } else {
588
0
        igraph_vector_int_t degrees;
589
0
        igraph_int_t maxdeg;
590
591
0
        IGRAPH_VECTOR_INT_INIT_FINALLY(&degrees, no_of_nodes);
592
0
        IGRAPH_CHECK(igraph_degree(graph, &degrees, igraph_vss_all(), IGRAPH_ALL, IGRAPH_LOOPS));
593
594
        // Compute max degree of the graph only if needed
595
0
        if (max_out_degree < 0 || max_in_degree < 0) {
596
0
            maxdeg = no_of_nodes > 0 ? igraph_vector_int_max(&degrees) : 0;
597
0
        }
598
599
0
        if (max_out_degree < 0) {
600
0
            max_out_degree = maxdeg;
601
0
        }
602
0
        if (max_in_degree < 0) {
603
0
            max_in_degree = maxdeg;
604
0
        }
605
606
0
        IGRAPH_CHECK(igraph_matrix_resize(jdm, max_out_degree, max_in_degree));
607
0
        igraph_matrix_null(jdm);
608
609
0
        IGRAPH_CHECK(igraph_eit_create(graph, igraph_ess_all(IGRAPH_EDGEORDER_ID), &eit));
610
0
        IGRAPH_FINALLY(igraph_eit_destroy, &eit);
611
0
        while (!IGRAPH_EIT_END(eit)) {
612
0
            eid = IGRAPH_EIT_GET(eit);
613
0
            v1id = IGRAPH_FROM(graph, eid);
614
0
            v2id = IGRAPH_TO(graph, eid);
615
0
            v1deg = VECTOR(degrees)[v1id];
616
0
            v2deg = VECTOR(degrees)[v2id];
617
618
            // Undirected JDMs are symmetrical, needs to be accounted for this when indexing.
619
0
            if (v1deg <= max_out_degree && v2deg <= max_in_degree) {
620
0
                MATRIX(*jdm, v1deg-1, v2deg-1) += weights ? VECTOR(*weights)[eid] : 1;
621
0
            }
622
            // Do not double-count connections between same-degree vertices.
623
0
            if (v1deg != v2deg && v2deg <= max_out_degree && v1deg <= max_in_degree) {
624
0
                MATRIX(*jdm, v2deg-1, v1deg-1) += weights ? VECTOR(*weights)[eid] : 1;
625
0
            }
626
627
0
            IGRAPH_EIT_NEXT(eit);
628
0
        }
629
630
0
        igraph_eit_destroy(&eit);
631
0
        igraph_vector_int_destroy(&degrees);
632
0
        IGRAPH_FINALLY_CLEAN(2);
633
0
    }
634
635
0
    return IGRAPH_SUCCESS;
636
0
}
637
638
/**
639
 * Common implementation for igraph_joint_type_distribution() and igraph_joint_degree_distribution()
640
 *
641
 * For the max_from/to_type parameters, negative values mean "automatic". These are used
642
 * only with igraph_joint_degree_distribution().
643
 *
644
 * check_types controls whether types should be validated to be non-negative. Validation
645
 * is only necessary with igraph_joint_type_distribution() but not with igraph_joint_degree_distribution().
646
 *
647
 * directed_neighbors must NOT be true when the graph is undirected.
648
 */
649
static igraph_error_t mixing_matrix(
650
        const igraph_t *graph, const igraph_vector_t *weights,
651
        igraph_matrix_t *p,
652
        const igraph_vector_int_t *from_types, const igraph_vector_int_t *to_types,
653
        igraph_bool_t directed_neighbors, igraph_bool_t normalized,
654
        igraph_int_t max_from_type, igraph_int_t max_to_type,
655
0
        igraph_bool_t check_types) {
656
657
0
    igraph_int_t no_of_nodes = igraph_vcount(graph);
658
0
    igraph_int_t no_of_edges = igraph_ecount(graph);
659
0
    igraph_int_t nrow, ncol;
660
0
    igraph_real_t sum;
661
0
    igraph_bool_t negative_weight;
662
663
0
    if (igraph_vector_int_size(from_types) != no_of_nodes) {
664
0
        IGRAPH_ERROR("Length of 'from' type vector must agree with vertex count.", IGRAPH_EINVAL);
665
0
    }
666
667
0
    if (igraph_vector_int_size(to_types) != no_of_nodes) {
668
0
        IGRAPH_ERROR("Length of 'to' type vector must agree with vertex count.", IGRAPH_EINVAL);
669
0
    }
670
671
0
    if (weights && igraph_vector_size(weights) != no_of_edges) {
672
0
        IGRAPH_ERRORF("Weight vector length (%" IGRAPH_PRId ") does not match number of edges (%" IGRAPH_PRId ").",
673
0
                      IGRAPH_EINVAL,
674
0
                      igraph_vector_size(weights), no_of_edges);
675
0
    }
676
677
0
    if (max_from_type < 0) {
678
0
        if (no_of_nodes == 0) {
679
0
            nrow = 0;
680
0
        } else {
681
0
            nrow = igraph_vector_int_max(from_types) + 1;
682
0
        }
683
0
    } else {
684
0
        nrow = max_from_type + 1;
685
0
    }
686
687
0
    if (max_to_type < 0) {
688
0
        if (no_of_nodes == 0) {
689
0
            ncol = 0;
690
0
        } else if (to_types == from_types) {
691
            /* Avoid computing the maximum again if target vertex types
692
             * are the same as source vertex types. */
693
0
            ncol = nrow;
694
0
        } else {
695
0
            ncol = igraph_vector_int_max(to_types) + 1;
696
0
        }
697
0
    } else {
698
0
        ncol = max_to_type + 1;
699
0
    }
700
701
0
    if (check_types && no_of_nodes > 0) {
702
0
        igraph_int_t min;
703
704
0
        min = igraph_vector_int_min(from_types);
705
0
        if (min < 0) {
706
0
            IGRAPH_ERROR("Invalid source vertex type.", IGRAPH_EINVAL);
707
0
        }
708
709
0
        if (to_types != from_types) {
710
0
            min = igraph_vector_int_min(from_types);
711
0
            if (min < 0) {
712
0
                IGRAPH_ERROR("Invalid target vertex type.", IGRAPH_EINVAL);
713
0
            }
714
0
        }
715
0
    }
716
717
0
    IGRAPH_CHECK(igraph_matrix_resize(p, nrow, ncol));
718
0
    igraph_matrix_null(p);
719
720
0
    sum = 0;
721
0
    negative_weight = false;
722
0
    for (igraph_int_t eid=0; eid < no_of_edges; eid++) {
723
0
        igraph_int_t from = IGRAPH_FROM(graph, eid);
724
0
        igraph_int_t to   = IGRAPH_TO(graph, eid);
725
0
        igraph_int_t from_type = VECTOR(*from_types)[from];
726
0
        igraph_int_t to_type   = VECTOR(*to_types)[to];
727
0
        igraph_real_t w = weights ? VECTOR(*weights)[eid] : 1;
728
729
0
        if (from_type >= nrow || to_type >= ncol) {
730
0
            continue;
731
0
        }
732
733
0
        MATRIX(*p, from_type, to_type) += w;
734
0
        sum += w;
735
736
0
        if (! directed_neighbors) {
737
0
            MATRIX(*p, to_type, from_type) += w;
738
0
            sum += w;
739
0
        }
740
741
0
        if (w < 0) {
742
0
            negative_weight = true;
743
0
        }
744
0
    }
745
746
0
    if (normalized) {
747
0
        if (negative_weight) {
748
            /* When some edge weights are negative, they cannot be interpreted as sampling weights,
749
             * and the sum of weights may be zero, potentially leading to Inf/NaN results. */
750
0
            IGRAPH_WARNING("Negative edge weights are present. Normalization may not be meaningful.");
751
0
        }
752
0
        if (no_of_edges > 0) {
753
            /* Scale only when there are some edges, thus 'sum' can be non-zero. */
754
0
            igraph_matrix_scale(p, 1.0 / sum);
755
0
        }
756
0
    }
757
758
0
    return IGRAPH_SUCCESS;
759
0
}
760
761
762
/**
763
 * \function igraph_joint_degree_distribution
764
 * \brief The joint degree distribution of a graph.
765
 *
766
 * Computes the joint degree distribution \c P_ij of a graph, used in the
767
 * study of degree correlations. \c P_ij is the probability that a randomly
768
 * chosen ordered pair of \em connected vertices have degrees \c i and \c j.
769
 *
770
 * </para><para>
771
 * In directed graphs, directionally connected <code>u -> v</code> pairs
772
 * are considered. The joint degree distribution of an undirected graph is the
773
 * same as that of the corresponding directed graph in which all connection are
774
 * bidirectional, assuming that \p from_mode is \c IGRAPH_OUT, \p to_mode is
775
 * \c IGRAPH_IN and \p directed_neighbors is true.
776
 *
777
 * </para><para>
778
 * When \p normalized is false, <code>sum_ij P_ij</code> gives the total
779
 * number of connections in a directed graph, or twice that value in an
780
 * undirected graph. The sum is taken over ordered <code>(i,j)</code> degree
781
 * pairs.
782
 *
783
 * </para><para>
784
 * The joint degree distribution relates to other concepts used in the study of
785
 * degree correlations. If \c P_ij is normalized then the degree correlation
786
 * function <code>k_nn(k)</code> is obtained as
787
 *
788
 * </para><para>
789
 * <code>k_nn(k) = (sum_j j P_kj) / (sum_j P_kj)</code>.
790
 *
791
 * </para><para>
792
 * The non-normalized degree assortativity is obtained as
793
 *
794
 * </para><para>
795
 * <code>a = sum_ij i j (P_ij - q_i r_j)</code>,
796
 *
797
 * </para><para>
798
 * where <code>q_i = sum_k P_ik</code> and <code>r_j = sum_k P_kj</code>.
799
 *
800
 * </para><para>
801
 * Note that the joint degree distribution \c P_ij is similar, but not identical
802
 * to the joint degree matrix \c J_ij computed by \ref igraph_joint_degree_matrix().
803
 * If the graph is undirected, then the diagonal entries of an unnormalized \c P_ij
804
 * are double that of \c J_ij, as any undirected connection between same-degree vertices
805
 * is counted in both directions. In contrast to \ref igraph_joint_degree_matrix(),
806
 * this function returns matrices which include the row and column corresponding
807
 * to zero degrees. In directed graphs, this row and column is not necessarily
808
 * zero when \p from_mode is different from \c IGRAPH_OUT or \p to_mode is different
809
 * from \c IGRAPH_IN.
810
 *
811
 * </para><para>
812
 * References:
813
 *
814
 * </para><para>
815
 * M. E. J. Newman: Mixing patterns in networks,
816
 * Phys. Rev. E 67, 026126 (2003)
817
 * https://doi.org/10.1103/PhysRevE.67.026126.
818
 *
819
 * \param graph A pointer to an initialized graph object.
820
 * \param weights A vector containing the weights of the edges. If passing a
821
 *    \c NULL pointer, edges will be assumed to have unit weights.
822
 * \param p A pointer to an initialized matrix that will be resized. The \c P_ij
823
 *    value will be written into <code>p[i,j]</code>.
824
 * \param from_mode How to compute the degree of sources? Can be \c IGRAPH_OUT
825
 *    for out-degree, \c IGRAPH_IN for in-degree, or \c IGRAPH_ALL for total degree.
826
 *    Ignored in undirected graphs.
827
 * \param to_mode How to compute the degree of targets? Can be \c IGRAPH_OUT
828
 *    for out-degree, \c IGRAPH_IN for in-degree, or \c IGRAPH_ALL for total degree.
829
 *    Ignored in undirected graphs.
830
 * \param directed_neighbors Whether to consider <code>u -> v</code> connections
831
 *    to be directed. Undirected connections are treated as reciprocal directed ones,
832
 *    i.e. both <code>u -> v</code> and <code>v -> u</code> will be considered.
833
 *    Ignored in undirected graphs.
834
 * \param normalized Whether to normalize the matrix so that entries sum to 1.0.
835
 *    If false, matrix entries will be connection counts. Normalization is not
836
 *    meaningful if some edge weights are negative.
837
 * \param max_from_degree The largest source vertex degree to consider. If
838
 *    negative or \ref IGRAPH_UNLIMITED, the largest source degree will be used.
839
 *    The row count of the result matrix is one larger than this value.
840
 * \param max_to_degree The largest target vertex degree to consider. If
841
 *    negative or \ref IGRAPH_UNLIMITED, the largest target degree will be used.
842
 *    The column count of the result matrix is one larger than this value.
843
 * \return Error code.
844
 *
845
 * \sa \ref igraph_joint_degree_matrix() for computing the joint degree matrix;
846
 * \ref igraph_assortativity_degree() and \ref igraph_assortativity() for
847
 * degree correlations coefficients, and \ref igraph_degree_correlation_vector()
848
 * for the degree correlation function.
849
 *
850
 * Time complexity: O(E), where E is the number of edges in the input graph.
851
 */
852
igraph_error_t igraph_joint_degree_distribution(
853
        const igraph_t *graph, const igraph_vector_t *weights,
854
        igraph_matrix_t *p,
855
        igraph_neimode_t from_mode, igraph_neimode_t to_mode,
856
        igraph_bool_t directed_neighbors,
857
        igraph_bool_t normalized,
858
0
        igraph_int_t max_from_degree, igraph_int_t max_to_degree) {
859
860
0
    igraph_int_t no_of_nodes = igraph_vcount(graph);
861
0
    igraph_vector_int_t *deg_from, *deg_to, deg_out, deg_in, deg_all;
862
863
    /* Make sure directionality parameters are consistent for undirected graphs. */
864
0
    if (! igraph_is_directed(graph)) {
865
0
        from_mode = to_mode = IGRAPH_ALL;
866
0
        directed_neighbors = false;
867
0
    }
868
869
0
    igraph_bool_t have_out = from_mode == IGRAPH_OUT || to_mode == IGRAPH_OUT;
870
0
    igraph_bool_t have_in  = from_mode == IGRAPH_IN  || to_mode == IGRAPH_IN;
871
0
    igraph_bool_t have_all = from_mode == IGRAPH_ALL || to_mode == IGRAPH_ALL;
872
873
0
    if (have_out) {
874
0
        IGRAPH_VECTOR_INT_INIT_FINALLY(&deg_out, no_of_nodes);
875
0
        IGRAPH_CHECK(igraph_degree(graph, &deg_out, igraph_vss_all(), IGRAPH_OUT, IGRAPH_LOOPS));
876
0
    }
877
878
0
    if (have_in) {
879
0
        IGRAPH_VECTOR_INT_INIT_FINALLY(&deg_in, no_of_nodes);
880
0
        IGRAPH_CHECK(igraph_degree(graph, &deg_in, igraph_vss_all(), IGRAPH_IN, IGRAPH_LOOPS));
881
0
    }
882
883
0
    if (have_all) {
884
0
        IGRAPH_VECTOR_INT_INIT_FINALLY(&deg_all, no_of_nodes);
885
0
        IGRAPH_CHECK(igraph_degree(graph, &deg_all, igraph_vss_all(), IGRAPH_ALL, IGRAPH_LOOPS));
886
0
    }
887
888
0
    switch (from_mode) {
889
0
    case IGRAPH_OUT: deg_from = &deg_out; break;
890
0
    case IGRAPH_IN:  deg_from = &deg_in;  break;
891
0
    case IGRAPH_ALL: deg_from = &deg_all; break;
892
0
    default:
893
0
        IGRAPH_ERROR("Invalid 'from' degree mode.", IGRAPH_EINVMODE);
894
0
    }
895
896
0
    switch (to_mode) {
897
0
    case IGRAPH_OUT: deg_to = &deg_out; break;
898
0
    case IGRAPH_IN:  deg_to = &deg_in;  break;
899
0
    case IGRAPH_ALL: deg_to = &deg_all; break;
900
0
    default:
901
0
        IGRAPH_ERROR("Invalid 'to' degree mode.", IGRAPH_EINVMODE);
902
0
    }
903
904
0
    IGRAPH_CHECK(mixing_matrix(graph,
905
0
                               weights, p,
906
0
                               deg_from, deg_to,
907
0
                               directed_neighbors, normalized,
908
0
                               max_from_degree, max_to_degree,
909
0
                               /*check_types=*/ false));
910
911
0
    if (have_all) {
912
0
        igraph_vector_int_destroy(&deg_all);
913
0
        IGRAPH_FINALLY_CLEAN(1);
914
0
    }
915
916
0
    if (have_in) {
917
0
        igraph_vector_int_destroy(&deg_in);
918
0
        IGRAPH_FINALLY_CLEAN(1);
919
0
    }
920
921
0
    if (have_out) {
922
0
        igraph_vector_int_destroy(&deg_out);
923
0
        IGRAPH_FINALLY_CLEAN(1);
924
0
    }
925
926
0
    return IGRAPH_SUCCESS;
927
0
}
928
929
930
/**
931
 * \function igraph_joint_type_distribution
932
 * \brief Mixing matrix for vertex categories.
933
 *
934
 * Computes the mixing matrix M_ij, i.e. the joint distribution of vertex types
935
 * at the endpoints directed of edges. Categories are represented by non-negative integer
936
 * indices, passed in \p from_types and \p to_types. The row and column counts of \p m
937
 * will be one larger than the largest source and target type, respectively. Re-index type
938
 * vectors using \ref igraph_reindex_membership() if they are not contiguous integers,
939
 * to avoid producing a very large matrix.
940
 *
941
 * </para><para>
942
 * M_ij is proportional to the probability that a randomly chosen ordered pair of vertices
943
 * have types \c i and \c j.
944
 *
945
 * </para><para>
946
 * When there is a single categorization of vertices, i.e. \p from_types and \p to_types
947
 * are the same, M_ij is related to the modularity (\ref igraph_modularity()) and nominal
948
 * assortativity (\ref igraph_assortativity_nominal()). Let <code>a_i = sum_j M_ij</code> and
949
 * <code>b_j = sum_i M_ij</code>. If M_ij is normalized, i.e. <code>sum_ij M_ij = 1</code>,
950
 * and the types represent membership in vertex partitions, then the modularity of the
951
 * partitioning can be computed as
952
 *
953
 * </para><para>
954
 * <code>Q = sum_ii M_ii - sum_i a_i b_i</code>
955
 *
956
 * </para><para>
957
 * The normalized nominal assortativity is
958
 *
959
 * </para><para>
960
 * <code>Q / (1 - sum_i a_i b_i)</code>
961
 *
962
 * </para><para>
963
 * \ref igraph_joint_degree_distribution() is a special case of this function, with
964
 * categories consisting vertices of the same degree.
965
 *
966
 * </para><para>
967
 * References:
968
 *
969
 * </para><para>
970
 * M. E. J. Newman: Mixing patterns in networks,
971
 * Phys. Rev. E 67, 026126 (2003)
972
 * https://doi.org/10.1103/PhysRevE.67.026126.
973
 *
974
 * \param graph The input graph.
975
 * \param weights A vector containing the weights of the edges. If passing a
976
 *    \c NULL pointer, edges will be assumed to have unit weights.
977
 * \param p The mixing matrix \c M_ij will be stored here.
978
 * \param from_types Vertex types for source vertices. These must be non-negative integers.
979
 * \param to_types Vertex types for target vertices. These must be non-negative integers.
980
 *    If \c NULL, it is assumed to be the same as \p from_types.
981
 * \param directed Whether to treat edges are directed. Ignored for undirected graphs.
982
 * \param normalized Whether to normalize the matrix so that entries sum to 1.0.
983
 *    If false, matrix entries will be connection counts. Normalization is not
984
 *    meaningful if some edge weights are negative.
985
 * \return Error code.
986
 *
987
 * \sa \ref igraph_joint_degree_distribution() to compute the joint distribution
988
 * of vertex degrees; \ref igraph_modularity() to compute the modularity of
989
 * a vertex partitioning; \ref igraph_assortativity_nominal() to compute
990
 * assortativity based on vertex categories.
991
 *
992
 * Time complexity: O(E), where E is the number of edges in the input graph.
993
 */
994
igraph_error_t igraph_joint_type_distribution(
995
        const igraph_t *graph, const igraph_vector_t *weights,
996
        igraph_matrix_t *p,
997
        const igraph_vector_int_t *from_types, const igraph_vector_int_t *to_types,
998
0
        igraph_bool_t directed, igraph_bool_t normalized) {
999
1000
0
    IGRAPH_ASSERT(from_types != NULL);
1001
0
    if (to_types == NULL) {
1002
0
        to_types = from_types;
1003
0
    }
1004
0
    if (! igraph_is_directed(graph)) {
1005
0
        directed = false;
1006
0
    }
1007
    return mixing_matrix(graph, weights, p, from_types, to_types, directed, normalized, -1, -1, true);
1008
0
}