Repository: networkx/networkx

Issue:
greedy modularity communities fails
I am trying to find communities of a directed graph (Number of Nodes: 53663 and Number of Edges: 953380) with `greedy_modularity_communities`. 

This is the following error I receive:

```
 line 127, in greedy_modularity_communities
    if dq_heap[j].h[0] == (-dq, j, i):
IndexError: list index out of range
```

However, running greedy_modularity_communities on the same graph with undirected edges is successful.
greedy modularity communities fails
I am trying to find communities of a directed graph (Number of Nodes: 53663 and Number of Edges: 953380) with `greedy_modularity_communities`. 

This is the following error I receive:

```
 line 127, in greedy_modularity_communities
    if dq_heap[j].h[0] == (-dq, j, i):
IndexError: list index out of range
```

However, running greedy_modularity_communities on the same graph with undirected edges is successful.


Discussion:
@elplatt is this designed to work with directed graphs? 
If not, we should put the ```@not_implemented_for('directed')``` decorator on the function. Also helpful to know if it is supposed to work for multigraphs.   
As written, I believe it only works with undirected graphs. It only needs a
little extra code to work with weighted and directed graphs though, maybe
I'll see if I can work on that over the next couple weeks.

As for multigraphs, is there a well-defined concept of "weight" between two
nodes? Or a standard networkx way of defining a weight function? If so, the
above changes would probably allow the code to work for multigraphs as well.

On Sun, Aug 5, 2018 at 11:21 AM, Dan Schult <notifications@github.com>
wrote:

> @elplatt <https://github.com/elplatt> is this designed to work with
> directed graphs?
> If not, we should put the @not_implemented_for('directed') decorator on
> the function. Also helpful to know if it is supposed to work for
> multigraphs.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/networkx/networkx/issues/3112#issuecomment-410527238>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAS0WIMTY2hjkrKkEpCSui3kvvpDHgQzks5uNw1tgaJpZM4VvVfU>
> .
>



-- 
Edward L. Platt
PhD Candidate, University of Michigan School of Information
he/him | https://elplatt.com | @elplatt | @elplatt@social.coop

Tips for stopping email overload:
https://hbr.org/2012/02/stop-email-overload-1

Take a look at the weighted graph code for dijkstra (in ```algorithms/shortest_paths/weighted.py```) for a standard treatment of inputing a weight function or weight attribute name. You can probably use the same function to process your input. 

While you are in there, take a look at the doc_string formatting. For sphinx to make it look nice it is supposed to have a one-line short description followed by a blank line and then a short paragraph (I think this function has a short paragraph).  Then sections for paramaters, returns, Examples, References and/or Notes and See Also...  none of these are mandatory, but they might make the documentation look better after sphinx processes it.

Thanks very much for looking into expanding it for directed and multigraph! :)
Thank you for working on this for directed and weighted graphs. I was really curious is there an update available for it? Eagerly waiting for that. 

Thanks again.



Wanted to give an update that I'm working on this. Have derived the equations for weighted undirected with self-loops and working on directed. If anyone is interested in helping write test cases, that would really help!
Here are the equations for both directed and undirected networks, allowing self-loops:
https://github.com/elplatt/Paper-CNM-Modularity/blob/master/paper.pdf

I will be implementing these soon, and it would be really helpful to have some test data for possible configurations (directed w/ self-loops, directed wo/ self-loops, etc.). Ideally this would come from hand-calculating the algorithm for a small network (probably not too hard in a spreadsheet but a bit tedious). Anyone able to help?
@elplatt Sir, I met the same problem as @lhassan1 , is greedy_modularity_communities support directed graph now? 
If not, Does networkx support any methods for reliable community detection and modularity calculation on directed graphs?
I believe it still supports only undirected graphs. It's still something I plan to do, unless someone else gets to it first, but I'm focused on phd work right now and won't likely have a chance to look at it for several months. The necessary changes are fairly simple and detailed in the pdf I link above. Changing the modularity calculation always comes with the potential to introduce calculation errors, so the bulk of the work is in creating the appropriate test cases.
@elplatt Thanks. Good luck to your PDH work.
@elplatt is this designed to work with directed graphs? 
If not, we should put the ```@not_implemented_for('directed')``` decorator on the function. Also helpful to know if it is supposed to work for multigraphs.   
As written, I believe it only works with undirected graphs. It only needs a
little extra code to work with weighted and directed graphs though, maybe
I'll see if I can work on that over the next couple weeks.

As for multigraphs, is there a well-defined concept of "weight" between two
nodes? Or a standard networkx way of defining a weight function? If so, the
above changes would probably allow the code to work for multigraphs as well.

On Sun, Aug 5, 2018 at 11:21 AM, Dan Schult <notifications@github.com>
wrote:

> @elplatt <https://github.com/elplatt> is this designed to work with
> directed graphs?
> If not, we should put the @not_implemented_for('directed') decorator on
> the function. Also helpful to know if it is supposed to work for
> multigraphs.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/networkx/networkx/issues/3112#issuecomment-410527238>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAS0WIMTY2hjkrKkEpCSui3kvvpDHgQzks5uNw1tgaJpZM4VvVfU>
> .
>



-- 
Edward L. Platt
PhD Candidate, University of Michigan School of Information
he/him | https://elplatt.com | @elplatt | @elplatt@social.coop

Tips for stopping email overload:
https://hbr.org/2012/02/stop-email-overload-1

Take a look at the weighted graph code for dijkstra (in ```algorithms/shortest_paths/weighted.py```) for a standard treatment of inputing a weight function or weight attribute name. You can probably use the same function to process your input. 

While you are in there, take a look at the doc_string formatting. For sphinx to make it look nice it is supposed to have a one-line short description followed by a blank line and then a short paragraph (I think this function has a short paragraph).  Then sections for paramaters, returns, Examples, References and/or Notes and See Also...  none of these are mandatory, but they might make the documentation look better after sphinx processes it.

Thanks very much for looking into expanding it for directed and multigraph! :)
Thank you for working on this for directed and weighted graphs. I was really curious is there an update available for it? Eagerly waiting for that. 

Thanks again.



Wanted to give an update that I'm working on this. Have derived the equations for weighted undirected with self-loops and working on directed. If anyone is interested in helping write test cases, that would really help!
Here are the equations for both directed and undirected networks, allowing self-loops:
https://github.com/elplatt/Paper-CNM-Modularity/blob/master/paper.pdf

I will be implementing these soon, and it would be really helpful to have some test data for possible configurations (directed w/ self-loops, directed wo/ self-loops, etc.). Ideally this would come from hand-calculating the algorithm for a small network (probably not too hard in a spreadsheet but a bit tedious). Anyone able to help?
@elplatt Sir, I met the same problem as @lhassan1 , is greedy_modularity_communities support directed graph now? 
If not, Does networkx support any methods for reliable community detection and modularity calculation on directed graphs?
I believe it still supports only undirected graphs. It's still something I plan to do, unless someone else gets to it first, but I'm focused on phd work right now and won't likely have a chance to look at it for several months. The necessary changes are fairly simple and detailed in the pdf I link above. Changing the modularity calculation always comes with the potential to introduce calculation errors, so the bulk of the work is in creating the appropriate test cases.
@elplatt Thanks. Good luck to your PDH work.

Resolving patch:
diff --git a/networkx/algorithms/community/modularity_max.py b/networkx/algorithms/community/modularity_max.py
--- a/networkx/algorithms/community/modularity_max.py
+++ b/networkx/algorithms/community/modularity_max.py
@@ -1,8 +1,11 @@
 """Functions for detecting communities based on modularity."""
 
-from networkx.algorithms.community.quality import modularity
+from collections import Counter
 
+import networkx as nx
+from networkx.algorithms.community.quality import modularity
 from networkx.utils.mapped_queue import MappedQueue
+from networkx.utils import not_implemented_for
 
 __all__ = [
     "greedy_modularity_communities",
@@ -11,11 +14,114 @@
 ]
 
 
+def _greedy_modularity_communities_init(G, weight=None, resolution=1):
+    r"""Initializes the data structures for greedy_modularity_communities().
+
+    Clauset-Newman-Moore Eq 8-9. Eq 8 was missing a factor of 2 (from A_ij + A_ji).
+    See [2]_ at :func:`greedy_modularity_communities`.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    weight : string or None, optional (default=None)
+        The name of an edge attribute that holds the numerical value used
+        as a weight.  If None, then each edge has weight 1.
+        The degree is the sum of the edge weights adjacent to the node.
+
+    resolution : float (default=1)
+        If resolution is less than 1, modularity favors larger communities.
+        Greater than 1 favors smaller communities.
+
+    Returns
+    -------
+    dq_dict : dict of dict's
+        dq_dict[i][j]: dQ for merging community i, j
+
+    dq_heap : dict of MappedQueue's
+        dq_heap[i][n] : (-dq, i, j) for communitiy i nth largest dQ
+
+    H : MappedQueue
+        (-dq, i, j) for community with nth largest max_j(dQ_ij)
+
+    a, b : dict
+        undirected:
+            a[i]: fraction of (total weight of) edges within community i
+            b : None
+        directed:
+            a[i]: fraction of (total weight of) edges with tails within community i
+            b[i]: fraction of (total weight of) edges with heads within community i
+
+    See Also
+    --------
+    :func:`greedy_modularity_communities`
+    :func:`~networkx.algorithms.community.quality.modularity`
+    """
+    # Count nodes and edges (or the sum of edge-weights for weighted graphs)
+    N = G.number_of_nodes()
+    m = G.size(weight)
+
+    # Calculate degrees
+    if G.is_directed():
+        k_in = dict(G.in_degree(weight=weight))
+        k_out = dict(G.out_degree(weight=weight))
+        q0 = 1.0 / m
+    else:
+        k_in = k_out = dict(G.degree(weight=weight))
+        q0 = 1.0 / (2.0 * m)
+
+    a = {node: kout * q0 for node, kout in k_out.items()}
+    if G.is_directed():
+        b = {node: kin * q0 for node, kin in k_in.items()}
+    else:
+        b = None
+
+    dq_dict = {
+        i: {
+            j: q0
+            * (
+                G.get_edge_data(i, j, default={weight: 0}).get(weight, 1.0)
+                + G.get_edge_data(j, i, default={weight: 0}).get(weight, 1.0)
+                - resolution * q0 * (k_out[i] * k_in[j] + k_in[i] * k_out[j])
+            )
+            for j in nx.all_neighbors(G, i)
+            if j != i
+        }
+        for i in G.nodes()
+    }
+
+    # dq correction for multi-edges
+    # In case of multi-edges, get_edge_data(i, j) returns the key: data dict of the i, j
+    # edges, which does not have a 'weight' key. Therefore, when calculating dq for i, j
+    # Aij is always 1.0 and a correction is required.
+    if G.is_multigraph():
+        edges_count = dict(Counter(G.edges()))
+        multi_edges = [edge for edge, count in edges_count.items() if count > 1]
+        for edge in multi_edges:
+            total_wt = sum(d.get(weight, 1) for d in G.get_edge_data(*edge).values())
+            if G.is_directed():
+                # The correction applies only to the direction of the edge. The edge at
+                # the other direction is either not a multiedge (where the weight is
+                # added correctly), non-existent or it is also a multiedge, in which
+                # case it will be handled singly when its turn in the loop comes.
+                q00 = q0
+            else:
+                q00 = 2 * q0
+            dq_dict[edge[0]][edge[1]] += q00 * (total_wt - 1)
+            dq_dict[edge[1]][edge[0]] += q00 * (total_wt - 1)
+
+    dq_heap = {
+        i: MappedQueue([(-dq, i, j) for j, dq in dq_dict[i].items()]) for i in G.nodes()
+    }
+    H = MappedQueue([dq_heap[i].h[0] for i in G.nodes() if len(dq_heap[i]) > 0])
+
+    return dq_dict, dq_heap, H, a, b
+
+
 def greedy_modularity_communities(G, weight=None, resolution=1, n_communities=1):
     r"""Find communities in G using greedy modularity maximization.
 
     This function uses Clauset-Newman-Moore greedy modularity maximization [2]_.
-    This method currently supports the Graph class.
 
     Greedy modularity maximization begins with each node in its own community
     and joins the pair of communities that most increases modularity until no
@@ -48,8 +154,8 @@ def greedy_modularity_communities(G, weight=None, resolution=1, n_communities=1)
 
     Returns
     -------
-    list
-        A list of sets of nodes, one for each community.
+    partition: list
+        A list of frozensets of nodes, one for each community.
         Sorted by length with largest communities first.
 
     Examples
@@ -66,63 +172,30 @@ def greedy_modularity_communities(G, weight=None, resolution=1, n_communities=1)
 
     References
     ----------
-    .. [1] M. E. J Newman "Networks: An Introduction", page 224
+    .. [1] Newman, M. E. J. "Networks: An Introduction", page 224
        Oxford University Press 2011.
     .. [2] Clauset, A., Newman, M. E., & Moore, C.
        "Finding community structure in very large networks."
        Physical Review E 70(6), 2004.
     .. [3] Reichardt and Bornholdt "Statistical Mechanics of Community
        Detection" Phys. Rev. E74, 2006.
+    .. [4] Newman, M. E. J."Analysis of weighted networks"
+       Physical Review E 70(5 Pt 2):056131, 2004.
     """
-
-    # Count nodes and edges
-    N = len(G.nodes())
-    m = sum([d.get(weight, 1) for u, v, d in G.edges(data=True)])
-    q0 = 1.0 / (2.0 * m)
-
+    N = G.number_of_nodes()
     if (n_communities < 1) or (n_communities > N):
         raise ValueError(
-            f"n_communities must be between 1 and {len(G.nodes())}. Got {n_communities}"
+            f"n_communities must be between 1 and {N}. Got {n_communities}"
         )
 
-    # Map node labels to contiguous integers
-    label_for_node = {i: v for i, v in enumerate(G.nodes())}
-    node_for_label = {label_for_node[i]: i for i in range(N)}
-
-    # Calculate degrees
-    k_for_label = G.degree(G.nodes(), weight=weight)
-    k = [k_for_label[label_for_node[i]] for i in range(N)]
-
-    # Initialize community and merge lists
-    communities = {i: frozenset([i]) for i in range(N)}
-    merges = []
-
-    # Initial modularity
-    partition = [[label_for_node[x] for x in c] for c in communities.values()]
-    q_cnm = modularity(G, partition, resolution=resolution)
-
     # Initialize data structures
-    # CNM Eq 8-9 (Eq 8 was missing a factor of 2 (from A_ij + A_ji)
-    # a[i]: fraction of edges within community i
-    # dq_dict[i][j]: dQ for merging community i, j
-    # dq_heap[i][n] : (-dq, i, j) for communitiy i nth largest dQ
-    # H[n]: (-dq, i, j) for community with nth largest max_j(dQ_ij)
-    a = [k[i] * q0 for i in range(N)]
-    dq_dict = {
-        i: {
-            j: 2
-            * q0
-            * G.get_edge_data(label_for_node[i], label_for_node[j]).get(weight, 1.0)
-            - 2 * resolution * k[i] * k[j] * q0 * q0
-            for j in [node_for_label[u] for u in G.neighbors(label_for_node[i])]
-            if j != i
-        }
-        for i in range(N)
-    }
-    dq_heap = [
-        MappedQueue([(-dq, i, j) for j, dq in dq_dict[i].items()]) for i in range(N)
-    ]
-    H = MappedQueue([dq_heap[i].h[0] for i in range(N) if len(dq_heap[i]) > 0])
+    dq_dict, dq_heap, H, a, b = _greedy_modularity_communities_init(
+        G, weight, resolution
+    )
+    # Initialize single-node communities
+    communities = {i: frozenset([i]) for i in G.nodes()}
+    # Initial modularity
+    q_cnm = modularity(G, communities.values(), resolution=resolution)
 
     # Merge communities until we can't improve modularity or until desired number of
     # communities (n_communities) is reached.
@@ -159,7 +232,6 @@ def greedy_modularity_communities(G, weight=None, resolution=1, n_communities=1)
         # Perform merge
         communities[j] = frozenset(communities[i] | communities[j])
         del communities[i]
-        merges.append((i, j, dq))
         # New modularity
         q_cnm += dq
         # Get list of communities connected to merged communities
@@ -173,10 +245,16 @@ def greedy_modularity_communities(G, weight=None, resolution=1, n_communities=1)
             if k in both_set:
                 dq_jk = dq_dict[j][k] + dq_dict[i][k]
             elif k in j_set:
-                dq_jk = dq_dict[j][k] - 2.0 * resolution * a[i] * a[k]
+                if G.is_directed():
+                    dq_jk = dq_dict[j][k] - resolution * (a[i] * b[k] + a[k] * b[i])
+                else:
+                    dq_jk = dq_dict[j][k] - 2.0 * resolution * a[i] * a[k]
             else:
                 # k in i_set
-                dq_jk = dq_dict[i][k] - 2.0 * resolution * a[j] * a[k]
+                if G.is_directed():
+                    dq_jk = dq_dict[i][k] - resolution * (a[j] * b[k] + a[k] * b[j])
+                else:
+                    dq_jk = dq_dict[i][k] - 2.0 * resolution * a[j] * a[k]
             # Update rows j and k
             for row, col in [(j, k), (k, j)]:
                 # Save old value for finding heap index
@@ -237,13 +315,16 @@ def greedy_modularity_communities(G, weight=None, resolution=1, n_communities=1)
         # Merge i into j and update a
         a[j] += a[i]
         a[i] = 0
+        if G.is_directed():
+            b[j] += b[i]
+            b[i] = 0
 
-    communities = [
-        frozenset([label_for_node[i] for i in c]) for c in communities.values()
-    ]
-    return sorted(communities, key=len, reverse=True)
+    partition = sorted(communities.values(), key=len, reverse=True)
+    return partition
 
 
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
 def naive_greedy_modularity_communities(G, resolution=1):
     r"""Find communities in G using greedy modularity maximization.
 
diff --git a/networkx/algorithms/community/quality.py b/networkx/algorithms/community/quality.py
--- a/networkx/algorithms/community/quality.py
+++ b/networkx/algorithms/community/quality.py
@@ -283,9 +283,9 @@ def modularity(G, communities, weight="weight", resolution=1):
         These node sets must represent a partition of G's nodes.
 
     weight : string or None, optional (default="weight")
-            The edge attribute that holds the numerical value used
-            as a weight. If None or an edge does not have that attribute,
-            then that edge has weight 1.
+        The edge attribute that holds the numerical value used
+        as a weight. If None or an edge does not have that attribute,
+        then that edge has weight 1.
 
     resolution : float (default=1)
         If resolution is less than 1, modularity favors larger communities.


Test patch:
diff --git a/networkx/algorithms/community/tests/test_modularity_max.py b/networkx/algorithms/community/tests/test_modularity_max.py
--- a/networkx/algorithms/community/tests/test_modularity_max.py
+++ b/networkx/algorithms/community/tests/test_modularity_max.py
@@ -53,6 +53,33 @@ def test_greedy_modularity_communities_relabeled():
     assert greedy_modularity_communities(G) == expected
 
 
+def test_greedy_modularity_communities_directed():
+    G = nx.DiGraph(
+        [
+            ("a", "b"),
+            ("a", "c"),
+            ("b", "c"),
+            ("b", "d"),  # inter-community edge
+            ("d", "e"),
+            ("d", "f"),
+            ("d", "g"),
+            ("f", "g"),
+            ("d", "e"),
+            ("f", "e"),
+        ]
+    )
+    expected = [frozenset({"f", "g", "e", "d"}), frozenset({"a", "b", "c"})]
+    assert greedy_modularity_communities(G) == expected
+
+    # with loops
+    G = nx.DiGraph()
+    G.add_edges_from(
+        [(1, 1), (1, 2), (1, 3), (2, 3), (1, 4), (4, 4), (5, 5), (4, 5), (4, 6), (5, 6)]
+    )
+    expected = [frozenset({1, 2, 3}), frozenset({4, 5, 6})]
+    assert greedy_modularity_communities(G) == expected
+
+
 def test_modularity_communities_weighted():
     G = nx.balanced_tree(2, 3)
     for (a, b) in G.edges:
@@ -66,6 +93,161 @@ def test_modularity_communities_weighted():
     assert greedy_modularity_communities(G, weight="weight") == expected
 
 
+def test_modularity_communities_directed_weighted():
+    G = nx.DiGraph()
+    G.add_weighted_edges_from(
+        [
+            (1, 2, 5),
+            (1, 3, 3),
+            (2, 3, 6),
+            (2, 6, 1),
+            (1, 4, 1),
+            (4, 5, 3),
+            (4, 6, 7),
+            (5, 6, 2),
+            (5, 7, 5),
+            (5, 8, 4),
+            (6, 8, 3),
+        ]
+    )
+    expected = [frozenset({4, 5, 6, 7, 8}), frozenset({1, 2, 3})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+    # A large weight of the edge (2, 6) causes 6 to change group, even if it shares
+    # only one connection with the new group and 3 with the old one.
+    G[2][6]["weight"] = 20
+    expected = [frozenset({1, 2, 3, 6}), frozenset({4, 5, 7, 8})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_greedy_modularity_communities_multigraph():
+    G = nx.MultiGraph()
+    G.add_edges_from(
+        [
+            (1, 2),
+            (1, 2),
+            (1, 3),
+            (2, 3),
+            (1, 4),
+            (2, 4),
+            (4, 5),
+            (5, 6),
+            (5, 7),
+            (5, 7),
+            (6, 7),
+            (7, 8),
+            (5, 8),
+        ]
+    )
+    expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+    assert greedy_modularity_communities(G) == expected
+
+    # Converting (4, 5) into a multi-edge causes node 4 to change group.
+    G.add_edge(4, 5)
+    expected = [frozenset({4, 5, 6, 7, 8}), frozenset({1, 2, 3})]
+    assert greedy_modularity_communities(G) == expected
+
+
+def test_greedy_modularity_communities_multigraph_weighted():
+    G = nx.MultiGraph()
+    G.add_weighted_edges_from(
+        [
+            (1, 2, 5),
+            (1, 2, 3),
+            (1, 3, 6),
+            (1, 3, 6),
+            (2, 3, 4),
+            (1, 4, 1),
+            (1, 4, 1),
+            (2, 4, 3),
+            (2, 4, 3),
+            (4, 5, 1),
+            (5, 6, 3),
+            (5, 6, 7),
+            (5, 6, 4),
+            (5, 7, 9),
+            (5, 7, 9),
+            (6, 7, 8),
+            (7, 8, 2),
+            (7, 8, 2),
+            (5, 8, 6),
+            (5, 8, 6),
+        ]
+    )
+    expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+    # Adding multi-edge (4, 5, 16) causes node 4 to change group.
+    G.add_edge(4, 5, weight=16)
+    expected = [frozenset({4, 5, 6, 7, 8}), frozenset({1, 2, 3})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+    # Increasing the weight of edge (1, 4) causes node 4 to return to the former group.
+    G[1][4][1]["weight"] = 3
+    expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_greed_modularity_communities_multidigraph():
+    G = nx.MultiDiGraph()
+    G.add_edges_from(
+        [
+            (1, 2),
+            (1, 2),
+            (3, 1),
+            (2, 3),
+            (2, 3),
+            (3, 2),
+            (1, 4),
+            (2, 4),
+            (4, 2),
+            (4, 5),
+            (5, 6),
+            (5, 6),
+            (6, 5),
+            (5, 7),
+            (6, 7),
+            (7, 8),
+            (5, 8),
+            (8, 4),
+        ]
+    )
+    expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_greed_modularity_communities_multidigraph_weighted():
+    G = nx.MultiDiGraph()
+    G.add_weighted_edges_from(
+        [
+            (1, 2, 5),
+            (1, 2, 3),
+            (3, 1, 6),
+            (1, 3, 6),
+            (3, 2, 4),
+            (1, 4, 2),
+            (1, 4, 5),
+            (2, 4, 3),
+            (3, 2, 8),
+            (4, 2, 3),
+            (4, 3, 5),
+            (4, 5, 2),
+            (5, 6, 3),
+            (5, 6, 7),
+            (6, 5, 4),
+            (5, 7, 9),
+            (5, 7, 9),
+            (7, 6, 8),
+            (7, 8, 2),
+            (8, 7, 2),
+            (5, 8, 6),
+            (5, 8, 6),
+        ]
+    )
+    expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+    assert greedy_modularity_communities(G, weight="weight") == expected
+
+
 def test_resolution_parameter_impact():
     G = nx.barbell_graph(5, 3)