Skip to content

Note

Click here to download the full example code

Graphs with string node labels

pymocd reads node ids as integers. Relabel a graph whose nodes are strings, run pymocd.rimpso on it, and map the communities back to the original names. The example uses the Les Misérables co-appearance network that ships with NetworkX, whose nodes are character names.

String labels are rejected

Passing the graph as it is raises a RuntimeError, because every node id must be an integer. The Input graphs page lists the accepted inputs.

from collections import Counter

import matplotlib.pyplot as plt
import networkx as nx

import pymocd

L = nx.les_miserables_graph()
print(f"{L.number_of_nodes()} nodes, {L.number_of_edges()} edges, e.g. {list(L)[:3]}")

try:
    pymocd.rimpso(L)
except RuntimeError as err:
    print(f"RuntimeError: {err}")

Out:

77 nodes, 254 edges, e.g. ['Napoleon', 'Myriel', 'MlleBaptistine']
RuntimeError: Failed getting node id's. Verify if all Graph.nodes are positive integers; <str> as node_id isn't supported

Relabel, detect, map back

nx.convert_node_labels_to_integers numbers the nodes 0, 1, 2, ... and, with label_attribute="name", keeps each original label as a node attribute. The partition is keyed by the integer ids; the attribute turns it back into names.

G = nx.convert_node_labels_to_integers(L, label_attribute="name")
partition = pymocd.rimpso(G)
names = nx.get_node_attributes(G, "name")

members = {}
for v, c in partition.items():
    members.setdefault(c, []).append(names[v])

print(f"{len(members)} communities")
for c, group in sorted(members.items(), key=lambda item: -len(item[1])):
    shown = ", ".join(sorted(group)[:6]) + (", ..." if len(group) > 6 else "")
    print(f"community {c:>2} ({len(group):>2}): {shown}")

Out:

13 communities
community  1 (19): Anzelma, Babet, Brujon, Claquesous, Cosette, Eponine, ...
community  9 (13): Bahorel, Bossuet, Combeferre, Courfeyrac, Enjolras, Feuilly, ...
community  2 (11): Blacheville, Dahlia, Fameuil, Fantine, Favourite, Listolier, ...
community  0 (10): Champtercier, Count, CountessDeLo, Cravatte, Geborand, MlleBaptistine, ...
community  5 ( 6): Bamatabois, Brevet, Champmathieu, Chenildieu, Cochepaille, Judge
community 10 ( 6): BaronessT, Gillenormand, LtGillenormand, Magnon, MlleGillenormand, MlleVaubois
community  4 ( 3): Fauchelevent, Gribier, MotherInnocent
community  6 ( 2): MmePontmercy, Pontmercy
community  8 ( 2): Jondrette, MmeBurgon
community 12 ( 2): Child1, Child2
community  3 ( 1): MmeDeR
community  7 ( 1): Boulatruelle
community 11 ( 1): MotherPlutarch

Draw it with names

The communities with at least three characters are colored, up to nine of them (the tab10 palette without its gray); the rest are gray. Each colored community is named in the legend after its highest-degree character, and the six highest-degree characters are labeled on the graph.

sizes = Counter(partition.values())
colored = [c for c, size in sizes.most_common() if size >= 3][:9]
palette = [color for i, color in enumerate(plt.colormaps["tab10"].colors) if i != 7]
color_of = {c: palette[i] for i, c in enumerate(colored)}

degree = dict(G.degree())
hubs = sorted(G, key=degree.get, reverse=True)[:6]

pos = nx.spring_layout(G, k=0.25, seed=2, weight=None)

fig, ax = plt.subplots(figsize=(8, 7))
nx.draw_networkx_edges(G, pos, ax=ax, width=0.6, alpha=0.25)
for c in colored:
    nodes = [v for v in G if partition[v] == c]
    leader = max(nodes, key=degree.get)
    nx.draw_networkx_nodes(
        G,
        pos,
        nodelist=nodes,
        node_color=[color_of[c]],
        node_size=60,
        edgecolors="white",
        linewidths=0.6,
        ax=ax,
        label=f"{names[leader]} ({len(nodes)})",
    )
rest = [v for v in G if partition[v] not in color_of]
if rest:
    nx.draw_networkx_nodes(
        G,
        pos,
        nodelist=rest,
        node_color="0.7",
        node_size=60,
        edgecolors="white",
        linewidths=0.6,
        ax=ax,
        label=f"smaller communities ({len(rest)})",
    )
for v in hubs:
    ax.annotate(
        names[v],
        pos[v],
        xytext=(0, 6),
        textcoords="offset points",
        ha="center",
        va="bottom",
        fontsize=9,
        bbox={"boxstyle": "round,pad=0.15", "facecolor": "white", "edgecolor": "none", "alpha": 0.8},
    )
ax.set_title("Les Misérables co-appearances: rimpso communities", fontsize=12)
ax.legend(loc="lower left", fontsize=8, frameon=False, title="community (size)", title_fontsize=8)
ax.set_axis_off()
fig.tight_layout()
plt.show()

Les Misérables co-appearances: rimpso communities

Edge weights are ignored

Every edge of this graph carries a weight attribute, but pymocd reads only the two endpoints of each edge (get_edges in src/core/graph/adj.rs), so every detector sees the unweighted graph. Because rimpso is deterministic, this is easy to confirm: dropping the weights gives the same partition. The only place weights can enter is a custom objective passed to pymocd.HpMocd, which receives the original NetworkX graph (see Custom objectives with HpMocd).

unweighted = nx.Graph()
unweighted.add_nodes_from(G)
unweighted.add_edges_from(G.edges())

print(f"weighted input: {nx.is_weighted(G)}, unweighted copy: {nx.is_weighted(unweighted)}")
print(f"same partition: {pymocd.rimpso(unweighted) == partition}")

Out:

weighted input: True, unweighted copy: False
same partition: True

Total running time of the script: ( 0 minutes 0.176 seconds)

Download Python source code: plot_les_miserables.py

Download Jupyter notebook: plot_les_miserables.ipynb

Gallery generated by mkdocs-gallery