|
| 1 | +""" |
| 2 | +.. _tutorials-clique-percolation: |
| 3 | +
|
| 4 | +========================== |
| 5 | +Clique Percolation Method |
| 6 | +========================== |
| 7 | +
|
| 8 | +This example shows how to detect **overlapping communities** using the |
| 9 | +Clique Percolation Method (CPM). Unlike partitioning algorithms, CPM allows |
| 10 | +a node to belong to more than one community simultaneously. |
| 11 | +""" |
| 12 | + |
| 13 | +import itertools |
| 14 | +from collections import Counter |
| 15 | + |
| 16 | +import igraph as ig |
| 17 | +import matplotlib.pyplot as plt |
| 18 | + |
| 19 | +# %% |
| 20 | +# We construct a graph with three dense cliques that share individual nodes, |
| 21 | +# creating natural *overlapping* community boundaries: |
| 22 | +g = ig.Graph(9) |
| 23 | +g.add_edges(list(itertools.combinations([0, 1, 2, 3], 2))) # clique A |
| 24 | +g.add_edges(list(itertools.combinations([3, 4, 5, 6], 2))) # clique B, shares node 3 with A |
| 25 | +g.add_edges(list(itertools.combinations([6, 7, 8], 2))) # clique C, shares node 6 with B |
| 26 | + |
| 27 | +# %% |
| 28 | +# The CPM algorithm works in three steps: |
| 29 | +# |
| 30 | +# 1. Find all k-cliques (complete subgraphs of exactly k nodes). |
| 31 | +# 2. Build a clique graph : two k-cliques are adjacent when they share k-1 nodes. |
| 32 | +# 3. Each connected component of the clique graph is a community — the union |
| 33 | +# of all its k-cliques. A node shared between cliques in *different* |
| 34 | +# components belongs to multiple communities simultaneously. |
| 35 | +k = 3 |
| 36 | +cliques = [set(c) for c in g.cliques(k, k)] |
| 37 | + |
| 38 | +clique_graph = ig.Graph(len(cliques)) |
| 39 | +clique_graph.add_edges([ |
| 40 | + (i, j) |
| 41 | + for i, j in itertools.combinations(range(len(cliques)), 2) |
| 42 | + if len(cliques[i] & cliques[j]) >= k - 1 |
| 43 | +]) |
| 44 | + |
| 45 | +communities = [] |
| 46 | +for component in clique_graph.connected_components(): |
| 47 | + members = set() |
| 48 | + for idx in component: |
| 49 | + members |= cliques[idx] |
| 50 | + communities.append(sorted(members)) |
| 51 | + |
| 52 | +# %% |
| 53 | +# Nodes that appear in more than one community are *overlapping nodes*: |
| 54 | +overlap = [ |
| 55 | + v for v, count in Counter(v for comm in communities for v in comm).items() |
| 56 | + if count > 1 |
| 57 | +] |
| 58 | +print(f"Communities (k={k}): {communities}") |
| 59 | +print(f"Overlapping nodes: {overlap}") |
| 60 | + |
| 61 | +# %% |
| 62 | +# We visualize the result using :class:`igraph.VertexCover`, which draws a |
| 63 | +# shaded hull around each community and handles overlapping nodes naturally: |
| 64 | +cover = ig.VertexCover(g, communities) |
| 65 | +palette = ig.RainbowPalette(n=len(communities)) |
| 66 | + |
| 67 | +fig, ax = plt.subplots(figsize=(6, 5)) |
| 68 | +ig.plot( |
| 69 | + cover, |
| 70 | + mark_groups=True, |
| 71 | + palette=palette, |
| 72 | + vertex_size=25, |
| 73 | + vertex_label=list(range(g.vcount())), |
| 74 | + vertex_label_size=10, |
| 75 | + edge_width=1.5, |
| 76 | + target=ax, |
| 77 | +) |
| 78 | +ax.set_title( |
| 79 | + f"Clique Percolation Method (k={k})\n" |
| 80 | + f"{len(communities)} communities — overlapping nodes: {overlap}" |
| 81 | +) |
| 82 | +plt.show() |
| 83 | + |
| 84 | +# %% |
| 85 | +# Advanced: effect of k |
| 86 | +# ---------------------- |
| 87 | +# Raising k to 4 requires larger cliques. The 3-clique {6, 7, 8} no longer |
| 88 | +# qualifies, so community C disappears and node 6 is no longer in any community: |
| 89 | +k = 4 |
| 90 | +cliques_4 = [set(c) for c in g.cliques(k, k)] |
| 91 | + |
| 92 | +clique_graph_4 = ig.Graph(len(cliques_4)) |
| 93 | +clique_graph_4.add_edges([ |
| 94 | + (i, j) |
| 95 | + for i, j in itertools.combinations(range(len(cliques_4)), 2) |
| 96 | + if len(cliques_4[i] & cliques_4[j]) >= k - 1 |
| 97 | +]) |
| 98 | + |
| 99 | +communities_4 = [] |
| 100 | +for component in clique_graph_4.connected_components(): |
| 101 | + members = set() |
| 102 | + for idx in component: |
| 103 | + members |= cliques_4[idx] |
| 104 | + communities_4.append(sorted(members)) |
| 105 | + |
| 106 | +print(f"Communities (k=4): {communities_4}") |
| 107 | + |
| 108 | +cover_4 = ig.VertexCover(g, communities_4) |
| 109 | +palette_4 = ig.RainbowPalette(n=max(len(communities_4), 1)) |
| 110 | + |
| 111 | +fig, ax = plt.subplots(figsize=(6, 5)) |
| 112 | +ig.plot( |
| 113 | + cover_4, |
| 114 | + mark_groups=True, |
| 115 | + palette=palette_4, |
| 116 | + vertex_size=25, |
| 117 | + vertex_label=list(range(g.vcount())), |
| 118 | + vertex_label_size=10, |
| 119 | + edge_width=1.5, |
| 120 | + target=ax, |
| 121 | +) |
| 122 | +ax.set_title( |
| 123 | + f"Clique Percolation Method (k=4)\n" |
| 124 | + f"{len(communities_4)} communities — nodes 6, 7, 8 no longer qualify" |
| 125 | +) |
| 126 | +plt.show() |
0 commit comments