From 12036b73d9c9e9bd9fed476419781ac48498bda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 18 Jun 2025 22:07:32 +0000 Subject: [PATCH 1/2] refactor: community_edge_betweenness() now always computes modularity as weights represent connection strengths --- src/_igraph/graphobject.c | 28 +++++++++++++--------------- src/igraph/community.py | 9 ++++++++- tests/test_decomposition.py | 6 +----- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8c82bf79f..761cd63b0 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13076,7 +13076,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject /* edge_betweenness = */ 0, /* merges = */ &merges, /* bridges = */ 0, - /* modularity = */ weights ? 0 : &q, + /* modularity = */ &q, /* membership = */ 0, PyObject_IsTrue(directed), weights, @@ -13094,19 +13094,11 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject igraph_vector_destroy(weights); free(weights); } - if (weights == 0) { - /* Calculate modularity vector only in the unweighted case as we don't - * calculate modularities for the weighted case */ - qs=igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); - igraph_vector_destroy(&q); - if (!qs) { - igraph_matrix_int_destroy(&merges); - return NULL; - } - } else { - qs = Py_None; - Py_INCREF(qs); - igraph_vector_destroy(&q); + qs = igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); + igraph_vector_destroy(&q); + if (!qs) { + igraph_matrix_int_destroy(&merges); + return NULL; } ms=igraphmodule_matrix_int_t_to_PyList(&merges); @@ -18531,12 +18523,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "is typically high. So we gradually remove the edge with the highest\n" "betweenness from the network and recalculate edge betweenness after every\n" "removal, as long as all edges are removed.\n\n" + "When edge weights are given, the ratio of betweenness and weight values\n" + "is used to choose which edges to remove first, as described in\n" + "M. E. J. Newman: Analysis of Weighted Networks (2004), Section C.\n" + "Thus, edges with large weights are treated as strong connections,\n" + "and will be removed later than weak connections having similar betweenness.\n" + "Weights are also used for calculating modularity.\n\n" "Attention: this function is wrapped in a more convenient syntax in the\n" "derived class L{Graph}. It is advised to use that instead of this version.\n\n" "@param directed: whether to take into account the directedness of the edges\n" " when we calculate the betweenness values.\n" "@param weights: name of an edge attribute or a list containing\n" - " edge weights.\n\n" + " edge weights. Higher weights indicate stronger connections.\n\n" "@return: a tuple with the merge matrix that describes the dendrogram\n" " and the modularity scores before each merge. The modularity scores\n" " use the weights if the original graph was weighted.\n" diff --git a/src/igraph/community.py b/src/igraph/community.py index 0fdcdf154..dfdcb3954 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -235,6 +235,13 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non separate components. The result of the clustering will be represented by a dendrogram. + When edge weights are given, the ratio of betweenness and weight values + is used to choose which edges to remove first, as described in + M. E. J. Newman: Analysis of Weighted Networks (2004), Section C. + Thus, edges with large weights are treated as strong connections, + and will be removed later than weak connections having similar betweenness. + Weights are also used for calculating modularity. + @param clusters: the number of clusters we would like to see. This practically defines the "level" where we "cut" the dendrogram to get the membership vector of the vertices. If C{None}, the dendrogram @@ -245,7 +252,7 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non @param directed: whether the directionality of the edges should be taken into account or not. @param weights: name of an edge attribute or a list containing - edge weights. + edge weights. Higher weights indicate stronger connections. @return: a L{VertexDendrogram} object, initally cut at the maximum modularity or at the desired number of clusters. """ diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 0bfdd1b6a..336db7ded 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -263,11 +263,7 @@ def testEdgeBetweenness(self): g.es["weight"] = 1 g[0, 1] = g[1, 2] = g[2, 0] = g[3, 4] = 10 - # We need to specify the desired cluster count explicitly; this is - # because edge betweenness-based detection does not play well with - # modularity-based cluster count selection (the edge weights have - # different semantics) so we need to give igraph a hint - cl = g.community_edge_betweenness(weights="weight").as_clustering(n=2) + cl = g.community_edge_betweenness(weights="weight").as_clustering() self.assertMembershipsEqual(cl, [0, 0, 0, 1, 1]) self.assertAlmostEqual(cl.q, 0.2750, places=3) From 699837c24af5b992f7433107d9483d9538ec5e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Wed, 18 Jun 2025 22:10:08 +0000 Subject: [PATCH 2/2] refactor: prettier code in igraphmodule_Graph_community_edge_betweenness --- src/_igraph/graphobject.c | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 761cd63b0..d3056f941 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13048,16 +13048,18 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject PyObject *res, *qs, *ms; igraph_matrix_int_t merges; igraph_vector_t q; - igraph_vector_t *weights = 0; + igraph_vector_t *weights = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) { return NULL; + } - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } if (igraph_matrix_int_init(&merges, 0, 0)) { - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); @@ -13065,32 +13067,33 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject if (igraph_vector_init(&q, 0)) { igraph_matrix_int_destroy(&merges); - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); } if (igraph_community_edge_betweenness(&self->g, - /* removed_edges = */ 0, - /* edge_betweenness = */ 0, + /* removed_edges = */ NULL, + /* edge_betweenness = */ NULL, /* merges = */ &merges, - /* bridges = */ 0, + /* bridges = */ NULL, /* modularity = */ &q, - /* membership = */ 0, + /* membership = */ NULL, PyObject_IsTrue(directed), weights, - /* lengths = */ 0)) { - igraphmodule_handle_igraph_error(); - if (weights != 0) { + /* lengths = */ NULL)) { + + igraph_vector_destroy(&q); + igraph_matrix_int_destroy(&merges); + if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_matrix_int_destroy(&merges); - igraph_vector_destroy(&q); - return NULL; + + return igraphmodule_handle_igraph_error();; } - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -13101,7 +13104,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject return NULL; } - ms=igraphmodule_matrix_int_t_to_PyList(&merges); + ms = igraphmodule_matrix_int_t_to_PyList(&merges); igraph_matrix_int_destroy(&merges); if (ms == NULL) {