Fundamentos

Detección de comunidades en grafos

Muchas redes se dividen en grupos muy unidos por dentro y poco ligados entre sí: amigos, equipos, complejos de proteínas, temas. Esta guía encuentra esos grupos en una pequeña red con la modularidad y tres algoritmos clásicos, Girvan-Newman, Louvain y propagación de etiquetas, muestra dónde falla cada uno, desde resultados aleatorios hasta el límite de resolución, y los pone a prueba con el club de kárate de Zachary y con un banco de pruebas de respuestas conocidas, con código Python probado en todo momento.

22 Min de lectura Actualizado: Noviembre 2026 Nivel Principiante
Mohammed Islam Hadjoudj
Mohammed Islam Hadjoudj
Especialista en investigación operativa

1. La respuesta corta

La detección de comunidades divide los vértices de una red en grupos con muchas aristas dentro y pocas aristas entre ellos. Es el agrupamiento (clustering) aplicado a grafos: círculos de amigos en una red social, equipos en un registro de correos, complejos de proteínas en una célula, temas en una red de citas. No hay una única respuesta correcta, así que todo método descansa en una definición de lo que es un buen grupo, y la mayoría de los métodos populares usan la misma, llamada modularidad.

MétodoIdeaCoste¿Mismo resultado en cada ejecución?Cuidado con
Girvan-NewmanBorrar las aristas por las que pasan más caminos más cortosAproximadamente O(m²n)SíDemasiado lento por encima de unos pocos miles de vértices
LouvainMover vértices para subir la modularidad, después fusionar grupos y repetirCasi lineal en la prácticaSí, para un orden fijo de los vérticesEl límite de resolución; grupos mal conectados
Propagación de etiquetasCada vértice adopta la etiqueta más común entre sus vecinosAproximadamente O(m) por barridoNoRespuestas distintas en ejecuciones distintas
Maximización exacta de la modularidadBuscar entre todas las particiones posiblesExponencialSíSolo para redes diminutas

En una frase: usa Louvain, o su sucesor Leiden, como opción por defecto, usa la propagación de etiquetas cuando la velocidad importe más que la estabilidad, reserva Girvan-Newman para redes pequeñas en las que quieras ver toda la jerarquía de particiones, y mira siempre los grupos que obtienes en lugar de fiarte de una sola puntuación. Aquí, n es el número de vértices y m el número de aristas.

2. Quince amigos, tres grupos

El ejemplo que recorre la guía es un grupo de quince amigos, de A a O, con una arista entre dos personas que se envían mensajes cada semana. Hay 26 aristas. La red se construyó para contener tres círculos de cinco amigos, de A a E, F a J y K a O, unidos por cinco aristas, y después se comprobó por ordenador para que los métodos de abajo se comporten de forma instructiva.

Una red de quince amigos dibujada en tres grupos de colores: de A a E a la izquierda en azul, de F a J en el centro en violeta y de K a O a la derecha en verde azulado. 21 aristas van dentro de los grupos y cinco aristas discontinuas van entre ellos: C con H, D con J, E con M, H con K y H con M. H está resaltado porque tres de sus seis aristas salen de su grupo.
Tres grupos, 21 aristas dentro de ellos y 5 entre ellos. H es el caso dudoso: la mitad de sus seis aristas salen de su grupo.

La mayoría de la gente dibujaría a ojo los mismos tres círculos. Hay una persona menos clara: H tiene seis contactos, tres en su propio grupo y tres fuera, incluidos dos en el grupo de K a O. Cualquier método tiene que decidir dónde colocar a H, y el resto de esta guía usa esa decisión como prueba.

3. Modularidad: puntuar una partición

Para comparar particiones hace falta una puntuación. Contar las aristas dentro de los grupos no basta, porque poner a todo el mundo en un solo grupo deja todas las aristas dentro. La modularidad, introducida por Mark Newman y Michelle Girvan en 2004, resta lo que daría el azar por sí solo:

Q = suma sobre los grupos de [ (aristas dentro del grupo) / m  -  ( (grado total del grupo) / 2m )² ]

El segundo término es la fracción de aristas que cabría esperar dentro del grupo si cada vértice conservara su grado pero las aristas se conectaran al azar. Un grupo solo suma puntos por las aristas que tiene por encima de esa expectativa. El programa de abajo calcula Q para cinco formas de dividir a los amigos.

# Quince amigos; una arista une a dos personas que se envían mensajes cada semana.
FRIENDS = 'AB AC BD BE CE CH DE DJ EM FH FI FJ GH GI HJ HK HM IJ KL KM KN LN LO MN MO NO'.split()

def graph(edges):
    """Las aristas son 'AB' para letras sueltas o '12-34' para números."""
    adj = {}
    for e in edges:
        a, b = e.split('-') if '-' in e else e
        adj.setdefault(a, set()).add(b)
        adj.setdefault(b, set()).add(a)
    return adj

def modularity(adj, groups):
    """Q = suma sobre los grupos de (fracción de aristas dentro) - (fracción del grado) al cuadrado."""
    m = sum(len(nbrs) for nbrs in adj.values()) / 2
    q = 0.0
    for g in groups:
        inside = sum(1 for v in g for u in adj[v] if u in g) / 2
        degree = sum(len(adj[v]) for v in g)
        q += inside / m - (degree / (2 * m)) ** 2
    return q

def show(groups):
    return ' '.join(sorted(''.join(sorted(g)) for g in groups))

if __name__ == '__main__':
    adj = graph(FRIENDS)
    for name, groups in [('three groups', [set('ABCDE'), set('FGHIJ'), set('KLMNO')]),
                         ('H moved to K', [set('ABCDE'), set('FGIJ'), set('HKLMNO')]),
                         ('two groups', [set('ABCDE'), set('FGHIJKLMNO')]),
                         ('everyone together', [set(adj)]),
                         ('everyone alone', [{v} for v in adj])]:
        print(f'{name:18} Q = {modularity(adj, groups):7.4f}')
# three groups       Q =  0.4712
# H moved to K       Q =  0.4016
# two groups         Q =  0.2951
# everyone together  Q =  0.0000
# everyone alone     Q = -0.0725

Los tres círculos obtienen 0,4712. En números: 21 de las 26 aristas, el 81 %, quedan dentro de un grupo, mientras que un cableado aleatorio con los mismos grados pondría dentro el 33,65 %, y 0,8077 - 0,3365 = 0,4712. Poner a todos en un solo grupo da exactamente 0, porque no es mejor que el azar, y dejar a cada uno solo da menos de cero. Mover a H al grupo de la derecha baja la puntuación a 0,4016, así que, según esta medida, H pertenece al grupo de F a J.

¿Es 0,4712 lo mejor posible? Una red de 15 vértices se puede dividir de 1.382.958.545 formas distintas, demasiadas para enumerarlas una a una, pero un algoritmo de programación dinámica sobre los 32.768 subconjuntos de vértices encuentra el máximo de forma exacta. La mejor partición son los tres círculos, con Q = 0,4712. En redes grandes no es posible una comprobación así: Ulrik Brandes y sus colegas demostraron en 2008 que encontrar la partición de máxima modularidad es NP-difícil, y por eso todo método práctico es heurístico.

4. Girvan-Newman: cortar los puentes entre grupos

El método de Michelle Girvan y Mark Newman de 2002 trabaja de arriba abajo. Una arista que conecta dos grupos lleva muchos caminos más cortos, porque toda ruta de un grupo al otro tiene que usarla a ella o a una de las pocas que se le parecen. Eso es lo que mide la intermediación de aristas, que se calcula con la misma búsqueda en anchura que la centralidad de intermediación. El método borra la arista con la mayor intermediación, recalcula y repite. Cada vez que la red se rompe en más partes, registra la partición y su modularidad.

from collections import deque
from modularity import FRIENDS, graph, modularity, show

def edge_betweenness(adj):
    """Algoritmo de Brandes, que reparte el crédito entre aristas en lugar de vértices."""
    score = {}
    for s in adj:
        order, preds, sigma, dist = [], {v: [] for v in adj}, dict.fromkeys(adj, 0), {s: 0}
        sigma[s] = 1
        queue = deque([s])
        while queue:
            v = queue.popleft()
            order.append(v)
            for w in adj[v]:
                if w not in dist:
                    dist[w] = dist[v] + 1
                    queue.append(w)
                if dist[w] == dist[v] + 1:
                    sigma[w] += sigma[v]
                    preds[w].append(v)
        delta = dict.fromkeys(adj, 0.0)
        for w in reversed(order):
            for v in preds[w]:
                credit = sigma[v] / sigma[w] * (1 + delta[w])
                edge = tuple(sorted((v, w)))
                score[edge] = score.get(edge, 0) + credit / 2   # cada par se ve desde ambos extremos
                delta[v] += credit
    return score

def components(adj):
    seen, parts = set(), []
    for s in sorted(adj):
        if s not in seen:
            part, stack = set(), [s]
            while stack:
                v = stack.pop()
                if v not in part:
                    part.add(v)
                    stack.extend(adj[v])
            seen |= part
            parts.append(part)
    return parts

def girvan_newman(adj, verbose=False):
    """Borra la arista de mayor intermediación, recalcula, repite; guarda cada partición."""
    work = {v: set(nbrs) for v, nbrs in adj.items()}
    splits = {1: [set(adj)]}
    while any(work.values()):
        score = edge_betweenness(work)
        a, b = max(sorted(score), key=score.get)
        work[a].discard(b)
        work[b].discard(a)
        parts = components(work)
        if len(parts) not in splits:
            splits[len(parts)] = parts
            if verbose:
                print(f'cut {a}{b}: {len(parts):2} groups  Q = {modularity(adj, parts):.4f}')
    best = max(splits.values(), key=lambda p: modularity(adj, p))
    return best, splits

if __name__ == '__main__':
    adj = graph(FRIENDS)
    best, splits = girvan_newman(adj, verbose=True)
    print('best:', show(best), round(modularity(adj, best), 4))
# cut DJ:  2 groups  Q = 0.2951
# cut HM:  3 groups  Q = 0.4712
# cut CE:  4 groups  Q = 0.4312
# cut GI:  5 groups  Q = 0.3780
# cut KM:  6 groups  Q = 0.3070
# cut HJ:  7 groups  Q = 0.2744
# cut LO:  8 groups  Q = 0.2241
# cut AC:  9 groups  Q = 0.1901
# cut BE: 10 groups  Q = 0.1287
# cut DE: 11 groups  Q = 0.0991
# cut FJ: 12 groups  Q = 0.0377
# cut IJ: 13 groups  Q = 0.0081
# cut MO: 14 groups  Q = -0.0429
# cut NO: 15 groups  Q = -0.0725
# best: ABCDE FGHIJ KLMNO 0.4712

La primera arista en caer es D-J, que ya separa el grupo de la izquierda: dos partes, Q = 0,2951. Después cae H-M y el grupo de la derecha queda suelto: tres partes, Q = 0,4712. A partir de ahí los cortes empiezan a romper grupos reales y la modularidad solo baja, hasta -0,0725 cuando cada vértice queda solo. Quedarse con la partición de mayor puntuación vuelve a dar los tres círculos.

Un gráfico de líneas de la modularidad frente al número de grupos a medida que Girvan-Newman borra aristas. Sube de 0 con un grupo a 0,2951 con dos grupos tras cortar D con J, alcanza el máximo de 0,4712 con tres grupos tras cortar H con M y luego baja sin parar hasta -0,0725 con quince grupos.
Cada borrado da una partición; la modularidad elige una. El máximo está en tres grupos.

El punto débil es el coste. Cada ronda recalcula la intermediación de todas las aristas, lo que lleva O(mn) de tiempo, y hay hasta m rondas, así que el método completo tarda aproximadamente O(m²n). Eso va bien para unos cientos de aristas y es inviable para unos millones.

5. Louvain: hacer crecer los grupos de abajo arriba

El método de Louvain, publicado en 2008 por Vincent Blondel, Jean-Loup Guillaume, Renaud Lambiotte y Etienne Lefebvre en la Université catholique de Louvain, maximiza la modularidad de forma voraz en dos fases que se repiten:

  1. Movimientos locales. Se empieza con cada vértice en su propio grupo. Se visitan los vértices uno a uno y cada uno se mueve al grupo vecino que más aumente la modularidad, si algún movimiento la aumenta. Se repite hasta que ningún movimiento individual ayude.
  2. Agregación. Cada grupo se fusiona en un único vértice. Las aristas dentro del grupo se convierten en un bucle sobre el nuevo vértice, y las aristas entre grupos se suman.

Después se vuelve a ejecutar la fase 1 sobre la red más pequeña. La ganancia de un movimiento se puede calcular a partir de unas pocas sumas, sin recalcular Q desde cero, y eso es lo que hace rápido el método.

from modularity import FRIENDS, graph, modularity, show

def louvain(adj, verbose=False):
    """Louvain: mueve vértices sueltos mientras Q sube y luego fusiona cada grupo en un vértice."""
    w = {v: {u: 1 for u in adj[v]} for v in adj}       # w[v][v] guarda las aristas dentro de un vértice fusionado
    members = {v: {v} for v in adj}
    m2 = sum(len(nbrs) for nbrs in adj.values())        # el doble del número de aristas
    level = 0
    while True:
        k = {v: sum(w[v].values()) for v in w}           # grado de cada vértice (fusionado)
        group, total = {v: v for v in w}, dict(k)        # total = suma de los grados de un grupo
        moved = True
        while moved:
            moved = False
            for v in sorted(w):
                old = group[v]
                total[old] -= k[v]
                links = {}
                for u, x in w[v].items():
                    if u != v:
                        links[group[u]] = links.get(group[u], 0) + x
                gain = lambda g: links.get(g, 0) - total[g] * k[v] / m2
                best = old
                for g in sorted(links):
                    if gain(g) > gain(best) + 1e-12:
                        best = g
                group[v] = best
                total[best] += k[v]
                moved |= best != old
        if len(set(group.values())) == len(w):           # no se fusionó nada: terminado
            break
        level += 1
        merged = {}
        for v, g in group.items():
            merged.setdefault(g, {})
            for u, x in w[v].items():
                merged[g][group[u]] = merged[g].get(group[u], 0) + x
        members = {g: set().union(*(members[v] for v in w if group[v] == g)) for g in merged}
        w = merged
        if verbose:
            print(f'pass {level}: {len(w)} groups  Q = {modularity(adj, members.values()):.4f}')
    return list(members.values())

if __name__ == '__main__':
    adj = graph(FRIENDS)
    groups = louvain(adj, verbose=True)
    print('result:', show(groups))
# pass 1: 4 groups  Q = 0.4312
# pass 2: 3 groups  Q = 0.4712
# result: ABCDE FGHIJ KLMNO

La primera pasada termina con cuatro grupos: los círculos del centro y de la derecha ya están completos, pero el círculo de la izquierda se ha dividido en A, C y B, D, E, con Q = 0,4312. Los movimientos de vértices por sí solos no pueden arreglarlo, porque mover a cualquier persona sola entre esas dos mitades baja la puntuación. La segunda pasada mueve grupos enteros, fusiona las dos mitades y alcanza el óptimo de 0,4712.

Louvain tiene dos defectos conocidos. Hereda todos los límites de la modularidad, descritos en la sección 7. Y sus grupos pueden estar mal conectados, o incluso romperse: un vértice que mantenía unido un grupo puede irse en una pasada posterior y dejar desconectado al resto. El algoritmo de Leiden de Vincent Traag, Ludo Waltman y Nees Jan van Eck, de 2019, añade un paso de refinamiento que garantiza grupos conexos, y a menudo se recomienda en lugar de Louvain.

6. Propagación de etiquetas: rápida y distinta cada vez

La propagación de etiquetas, propuesta por Usha Nandini Raghavan, Réka Albert y Soundar Kumara en 2007, no calcula la modularidad en absoluto. Cada vértice empieza con su propia etiqueta. En orden aleatorio, cada vértice adopta la etiqueta más común entre sus vecinos y deshace los empates al azar. Los grupos densos se asientan pronto en una sola etiqueta, y el proceso se detiene cuando cada vértice ya tiene una de las etiquetas más comunes a su alrededor. Cada barrido lleva O(m) de tiempo, y normalmente hacen falta pocos barridos.

import random
from collections import Counter
from modularity import FRIENDS, graph, show

def label_propagation(adj, seed):
    """Cada uno empieza con su propia etiqueta y va adoptando la más común a su alrededor."""
    rnd = random.Random(seed)
    label = {v: v for v in adj}
    for _ in range(1000):
        order = sorted(adj)
        rnd.shuffle(order)
        changed = False
        for v in order:
            if not adj[v]:
                continue
            count = Counter(label[u] for u in adj[v])
            top = max(count.values())
            best = sorted(l for l, c in count.items() if c == top)
            if label[v] not in best:                  # conserva tu etiqueta si está entre las mejores
                label[v] = rnd.choice(best)
                changed = True
        if not changed:
            break
    groups = {}
    for v, l in label.items():
        groups.setdefault(l, set()).add(v)
    return list(groups.values())

if __name__ == '__main__':
    adj = graph(FRIENDS)
    results = Counter(show(label_propagation(adj, seed)) for seed in range(1000))
    print('different results in 1000 runs:', len(results))
    for groups, n in results.most_common(4):
        print(f'{n:4}  {groups}')
# different results in 1000 runs: 49
#  387  ABCDE FGHIJ KLMNO
#  275  AC BDE FGHIJ KLMNO
#   46  ABCDE FIJ GHKLMNO
#   35  AB CE DFGHIJ KLMNO

La aleatoriedad importa. En 1.000 ejecuciones sobre los quince amigos, la propagación de etiquetas produjo 49 respuestas distintas. Encontró los tres círculos 387 veces. La segunda respuesta más común, hallada 275 veces, divide el círculo de la izquierda en A, C y B, D, E, el mismo estado intermedio al que llegó la primera pasada de Louvain. Otras ejecuciones llevaron a G y H al grupo de la derecha, o fusionaron dos círculos.

Cuatro copias pequeñas de la red de amigos coloreadas según los cuatro resultados más comunes de la propagación de etiquetas en 1.000 ejecuciones: los tres círculos, 387 veces; el círculo de la izquierda dividido en A y C frente a B, D y E, 275 veces; G y H unidos al grupo de la derecha, 46 veces; y A con B, C con E, y D fusionado con el grupo del centro, 35 veces.
Los cuatro más comunes de 49 resultados distintos. La respuesta correcta es la más común, pero solo en 387 de 1.000 ejecuciones.

Nada de esto es un fallo del programa: cada uno de esos resultados es un estado de parada válido. Si usas la propagación de etiquetas, ejecútala varias veces y quédate con el resultado de mejor modularidad, o compara con qué frecuencia acaba junto cada par de vértices.

7. El límite de resolución

La modularidad tiene un punto ciego que ningún algoritmo puede arreglar, porque está en la propia puntuación. Santo Fortunato y Marc Barthélemy demostraron en 2007 que, en una red grande, maximizar la modularidad puede fusionar grupos pequeños que están claramente separados. La causa es el término de aristas esperadas: se encoge a medida que crece la red, así que fusionar dos grupos pequeños no cuesta casi nada, mientras que la única arista entre ellos cuenta como ganancia.

El ejemplo clásico es un anillo de cliques: grupos de personas que se conocen todas entre sí, cada uno unido al siguiente por una sola arista. El siguiente programa construye 30 grupos de cinco y puntúa dos particiones.

from modularity import modularity
from louvain import louvain

def ring_of_cliques(n, size):
    """n grupos completos de `size` personas, cada uno unido al siguiente por una sola arista."""
    adj = {v: set() for v in range(n * size)}
    for c in range(n):
        people = range(c * size, (c + 1) * size)
        for a in people:
            adj[a] |= set(people) - {a}
        a, b = c * size, ((c + 1) % n) * size + 1
        adj[a].add(b)
        adj[b].add(a)
    return adj

if __name__ == '__main__':
    n, size = 30, 5
    adj = ring_of_cliques(n, size)
    cliques = [set(range(c * size, (c + 1) * size)) for c in range(n)]
    pairs = [cliques[i] | cliques[i + 1] for i in range(0, n, 2)]
    print('one group per clique:   Q =', round(modularity(adj, cliques), 4))
    print('cliques merged in pairs: Q =', round(modularity(adj, pairs), 4))
    found = louvain(adj)
    whole = all(any(g == set().union(*[c for c in cliques if c <= g]) for _ in [0]) for g in found)
    print('Louvain:', len(found), 'groups, Q =', round(modularity(adj, found), 4),
          ', every group a union of whole cliques:', whole)
# one group per clique:   Q = 0.8758
# cliques merged in pairs: Q = 0.8879
# Louvain: 15 groups, Q = 0.8879 , every group a union of whole cliques: True
Treinta pequeños grupos completos de cinco vértices dispuestos en anillo, cada uno unido al siguiente por una arista. Los grupos vecinos están sombreados por parejas, mostrando la partición que maximiza la modularidad: 15 parejas con Q = 0,8879, más que 0,8758 para los 30 grupos naturales.
Cada grupo de cinco es completo y toca a sus vecinos con una arista, y aun así la modularidad prefiere fusionarlos por parejas.

Mantener separados los 30 grupos da 0,8758. Fusionarlos por parejas da 0,8879, así que ni siquiera un maximizador perfecto de la modularidad devolvería nunca los 30 grupos naturales, y Louvain los fusiona por parejas: devuelve 15 grupos. El parámetro de resolución γ de Jörg Reichardt y Stefan Bornholdt multiplica el término de aristas esperadas; los valores mayores que 1 favorecen grupos más pequeños y los menores que 1, grupos más grandes. Desplaza el límite en lugar de eliminarlo, así que conviene comparar el tamaño de los grupos que encuentras con lo que sabes de la red.

8. Una prueba real: el club de kárate de Zachary

En los años setenta, el antropólogo Wayne Zachary registró las amistades entre 34 miembros de un club universitario de kárate mientras una disputa entre el instructor y el administrador del club lo partía en dos. Su artículo de 1977 enumeraba 78 amistades, y como se conoce la partición real, la red se convirtió en la primera prueba estándar de la detección de comunidades. El siguiente programa ejecuta los tres métodos y los compara con el bando al que se unió cada miembro, usando las etiquetas que se distribuyen con la biblioteca NetworkX. Estas registran el club al que se unió cada miembro tras la escisión. Los datos de facciones de Zachary sitúan al miembro 9 en el bando del administrador, aunque se unió al club del instructor, así que los estudios que puntúan frente a las facciones, como hicieron Girvan y Newman, cuentan solo al miembro 3 como mal colocado.

from collections import Counter
from modularity import graph, modularity
from girvan_newman import girvan_newman
from louvain import louvain
from lpa import label_propagation

# Club de kárate de Zachary (1977): 34 miembros, 78 amistades y el bando que tomó cada miembro
# cuando el club se partió (H = el club del instructor, O = el del administrador), miembros 1 a 34.
CLUB = ('1-2 1-3 1-4 1-5 1-6 1-7 1-8 1-9 1-11 1-12 1-13 1-14 1-18 1-20 1-22 1-32 2-3 2-4 2-8 '
        '2-14 2-18 2-20 2-22 2-31 3-4 3-8 3-9 3-10 3-14 3-28 3-29 3-33 4-8 4-13 4-14 5-7 5-11 '
        '6-7 6-11 6-17 7-17 9-31 9-33 9-34 10-34 14-34 15-33 15-34 16-33 16-34 19-33 19-34 '
        '20-34 21-33 21-34 23-33 23-34 24-26 24-28 24-30 24-33 24-34 25-26 25-28 25-32 26-32 '
        '27-30 27-34 28-34 29-32 29-34 30-33 30-34 31-33 31-34 32-33 32-34 33-34').split()
SIDE = dict(zip([str(i) for i in range(1, 35)], 'HHHHHHHHHOHHHHOOHHOHOHOOOOOOOOOOOO'))

def wrong(groups):
    """Miembros colocados en un grupo cuya mayoría tomó el otro bando."""
    bad = []
    for g in groups:
        majority = Counter(SIDE[v] for v in g).most_common(1)[0][0]
        bad += sorted((v for v in g if SIDE[v] != majority), key=int)
    return bad

if __name__ == '__main__':
    adj = graph(CLUB)
    best, splits = girvan_newman(adj)
    print('Girvan-Newman, first split:', [len(g) for g in splits[2]], 'misplaced:', wrong(splits[2]))
    print('Girvan-Newman, best Q:', len(best), 'groups, Q =', round(modularity(adj, best), 4))
    groups = louvain(adj)
    print('Louvain:', len(groups), 'groups, Q =', round(modularity(adj, groups), 4),
          'sizes', sorted(len(g) for g in groups))
    runs = Counter(len(label_propagation(adj, s)) for s in range(1000))
    print('label propagation, groups found in 1000 runs:', dict(sorted(runs.items())))
# Girvan-Newman, first split: [15, 19] misplaced: ['3', '9']
# Girvan-Newman, best Q: 5 groups, Q = 0.4013
# Louvain: 4 groups, Q = 0.4188 sizes [5, 6, 11, 12]
# label propagation, groups found in 1000 runs: {1: 6, 2: 218, 3: 369, 4: 295, 5: 104, 6: 8}

La primera partición que hace Girvan-Newman divide el club en 15 y 19 miembros y coloca mal a dos personas, los miembros 3 y 9; ambos tienen amigos en los dos bandos. Su mejor modularidad llega más tarde, con cinco grupos y Q = 0,4013. Louvain encuentra cuatro grupos con Q = 0,4188, muy cerca de 0,4198, el máximo demostrado para esta red, y cada uno de sus cuatro grupos queda en un solo lado de la partición real salvo los miembros 9 y 10. La propagación de etiquetas vuelve a variar: en 1.000 ejecuciones encontró entre uno y seis grupos, tres con mayor frecuencia.

La lección es que la respuesta "verdadera" de una red real depende de la pregunta. El club se partió en dos, pero la modularidad prefiere cuatro grupos, porque cada una de las dos facciones contiene círculos más estrechos. Ninguna respuesta está mal; responden a preguntas distintas.

9. Cuando los grupos se difuminan: un banco de pruebas medido

Para medir la precisión hacen falta redes con grupos conocidos. El banco de pruebas de Girvan y Newman de 2002 tiene 128 vértices en cuatro grupos de 32. Cada vértice tiene unas 16 aristas, de las que en promedio z_out van a otros grupos. Cuanto mayor es z_out, más difícil es ver los grupos. La precisión se mide con la información mutua normalizada (NMI), una puntuación de 0 a 1 que vale 1 solo cuando los grupos encontrados coinciden exactamente con los plantados, tal como propusieron para este fin Leon Danon y sus colegas en 2005.

import math, random
from louvain import louvain
from lpa import label_propagation

def planted(rnd, z_out, groups=4, size=32, degree=16):
    """Prueba de Girvan y Newman: 4 grupos de 32, cada vértice con unas z_out aristas que salen de su grupo."""
    n = groups * size
    adj = {v: set() for v in range(n)}
    p_in, p_out = (degree - z_out) / (size - 1), z_out / (n - size)
    for a in range(n):
        for b in range(a + 1, n):
            if rnd.random() < (p_in if a // size == b // size else p_out):
                adj[a].add(b)
                adj[b].add(a)
    return adj

def nmi(found, size=32):
    """Información mutua normalizada entre los grupos encontrados y los plantados (1 = idénticos)."""
    n = sum(len(g) for g in found)
    planted = {}
    for g in found:
        for v in g:
            planted.setdefault(v // size, []).append(v)
    h = lambda parts: -sum(len(p) / n * math.log(len(p) / n) for p in parts)
    mutual = 0.0
    for g in found:
        for p in planted.values():
            k = len(g & set(p))
            if k:
                mutual += k / n * math.log(k * n / (len(g) * len(p)))
    hf, hp = h(found), h(list(planted.values()))
    return 1.0 if hf + hp == 0 else 2 * mutual / (hf + hp)

if __name__ == '__main__':
    rnd = random.Random(7)
    print('edges out   Louvain   label propagation   (average NMI over 10 networks)')
    for z_out in range(1, 10):
        a = b = 0.0
        for _ in range(10):
            adj = planted(rnd, z_out)
            a += nmi(louvain(adj)) / 10
            b += nmi(label_propagation(adj, rnd.randrange(10**6))) / 10
        print(f'{z_out:9} {a:9.2f} {b:12.2f}')
# edges out   Louvain   label propagation   (average NMI over 10 networks)
#         1      1.00         1.00
#         2      1.00         1.00
#         3      1.00         1.00
#         4      1.00         0.90
#         5      0.99         0.85
#         6      0.97         0.35
#         7      0.80         0.10
#         8      0.50         0.02
#         9      0.20         0.00
Un gráfico de líneas de la precisión, medida como información mutua normalizada, frente al número de aristas por vértice que salen de su grupo, de 1 a 9. Louvain se mantiene en 1,00 hasta 4 y en 0,97 o más hasta 6, y luego cae a 0,80, 0,50 y 0,20. La propagación de etiquetas se mantiene en 1,00 hasta 3 y luego cae a 0,90, 0,85, 0,35, 0,10, 0,02 y 0,00.
Promedio de 10 redes por cada punto. Louvain sigue encontrando los grupos mucho después de que la propagación de etiquetas los pierda.

Ambos métodos son perfectos mientras los grupos están claros. A partir de 4 aristas salientes de 16, la propagación de etiquetas empieza a fallar, y en 6 su puntuación se ha desplomado a 0,35 mientras Louvain aún obtiene 0,97. En 8, la mitad de las aristas de cada vértice salen de su grupo, aunque el interior de un grupo sigue siendo 3,1 veces más denso que el espacio entre grupos, e incluso Louvain baja a 0,50. Con z_out = 12 los grupos no serían más densos por dentro que por fuera. De hecho, se vuelven imposibles de recuperar antes: Aurelien Decelle y sus colegas demostraron en 2011 que, en redes grandes construidas así, ningún método puede hacerlo mejor que el azar en cuanto z_out supera aproximadamente 9.

10. Dónde se usa la detección de comunidades

11. Una breve historia

Dividir grafos en partes es algo antiguo: la heurística de Brian Kernighan y Shen Lin de 1970 dividía un grafo en partes de tamaños dados cortando el menor número posible de aristas, un problema que surgió al colocar circuitos electrónicos en placas de circuito. La detección de comunidades en el sentido actual, en la que el número y el tamaño de los grupos no se dan de antemano, despegó con el artículo de Girvan y Newman de 2002, que además popularizó el club de kárate de Zachary como prueba. Newman y Girvan definieron la modularidad en 2004, y ese mismo año Aaron Clauset, Newman y Cristopher Moore publicaron un método voraz rápido para maximizarla. La propagación de etiquetas llegó en 2007, el mismo año que el límite de resolución, y el método de Louvain en 2008. La revisión de Santo Fortunato de 2010 es una visión general de referencia, y el algoritmo de Leiden de 2019 corrigió los grupos desconectados de Louvain.

12. Errores comunes

ErrorPor qué está malSolución
Fiarse de una sola ejecución de la propagación de etiquetasEn el ejemplo, 1.000 ejecuciones dieron 49 respuestas distintasEjecútala varias veces, o usa Louvain o Leiden
Tomar el Q más alto por la verdadEl límite de resolución fusiona grupos pequeñosComprueba el tamaño de los grupos; prueba un parámetro de resolución
Comparar Q entre redes distintasSus valores típicos dependen del tamaño y la densidadCompara particiones de la misma red
Considerar un fallo que todos queden en un solo grupoPuede que una red no tenga ninguna estructura de comunidadesCompara con una red aleatoria con los mismos grados
Ejecutar Girvan-Newman sobre un grafo grandeTarda aproximadamente O(m²n) de tiempoUsa Louvain o Leiden
Ignorar los grupos desconectados de LouvainUn grupo puede romperse en pasadas posterioresUsa Leiden, o divide los grupos en partes conexas
Esperar que cada vértice pertenezca a un solo grupoLas personas pertenecen a la vez a círculos de familia, trabajo y deporteUsa un método con solapamiento cuando eso importe

13. Glosario

TérminoSignificado
ComunidadUn grupo de vértices con muchas aristas dentro y pocas hacia el resto
ParticiónUna división de los vértices en grupos, con cada vértice en exactamente uno
ModularidadLa fracción de aristas dentro de los grupos menos la fracción esperada por azar
Intermediación de aristasCuántos caminos más cortos entre pares de vértices usan una arista
Girvan-NewmanBorrar repetidamente la arista con la mayor intermediación
Método de LouvainMovimientos locales que suben la modularidad y luego fusión de grupos, repetidos
Algoritmo de LeidenUn refinamiento de Louvain que garantiza grupos conexos
Propagación de etiquetasCada vértice adopta repetidamente la etiqueta más común entre sus vecinos
Límite de resoluciónLa tendencia de la modularidad a fusionar grupos pequeños en redes grandes
Parámetro de resoluciónUn peso γ sobre el término de aristas esperadas que desplaza el tamaño de grupo preferido
CliqueUn grupo en el que cada dos vértices están unidos
NMIInformación mutua normalizada, 1 cuando dos particiones son idénticas

14. Preguntas frecuentes

¿Qué es la detección de comunidades en grafos?

La detección de comunidades consiste en dividir los vértices de una red en grupos con muchas aristas dentro y pocas aristas entre ellos. Es la versión para grafos del agrupamiento (clustering). Usos típicos son encontrar círculos de amigos en redes sociales, equipos en datos de comunicación, complejos en redes de proteínas y temas relacionados en grafos de citas o de la web.

¿Qué es la modularidad?

La modularidad, que suele escribirse Q, puntúa una partición de una red en grupos. Es la fracción de aristas que caen dentro de los grupos menos la fracción que cabría esperar si los mismos vértices conservaran sus grados pero las aristas se colocaran al azar. Los valores cercanos a 0 significan que no es mejor que el azar; los grupos bien separados suelen obtener entre 0,3 y 0,7 aproximadamente.

¿Qué es el algoritmo de Louvain?

El método de Louvain, publicado por Blondel y sus colegas en 2008, maximiza la modularidad en dos fases que se repiten. Primero, cada vértice se mueve al grupo vecino que más aumenta la modularidad, hasta que ningún movimiento ayuda. Después, cada grupo se fusiona en un único vértice y el proceso se repite sobre la red más pequeña. Es lo bastante rápido para redes con millones de aristas.

¿Cuál es la diferencia entre Louvain y Girvan-Newman?

Girvan-Newman trabaja de arriba abajo: borra repetidamente la arista con la mayor intermediación, la que con más probabilidad va entre grupos, y se queda con la partición de mejor modularidad. Louvain trabaja de abajo arriba, fusionando vértices en grupos mientras la modularidad sube. Girvan-Newman tarda un tiempo de aproximadamente O(m al cuadrado por n), así que se limita a redes pequeñas; Louvain escala a redes muy grandes.

¿Es fiable la propagación de etiquetas?

La propagación de etiquetas es muy rápida, pero su resultado depende de elecciones aleatorias. En el ejemplo de 15 personas de esta guía, 1.000 ejecuciones produjeron 49 respuestas distintas y solo encontraron los tres grupos reales 387 veces. Ejecútala varias veces y compara, o usa un método de modularidad cuando importe que los resultados sean estables.

¿Qué es el límite de resolución de la modularidad?

Fortunato y Barthélemy demostraron en 2007 que maximizar la modularidad puede fusionar grupos pequeños y claramente separados cuando la red es grande. En un anillo de 30 grupos de cinco personas, fusionar los grupos vecinos por parejas puntúa más que mantenerlos separados. Un parámetro de resolución, o los métodos que no dependen de la modularidad, pueden resolverlo.

15. Referencias

Los algoritmos, resultados y atribuciones anteriores proceden de estas fuentes, listadas en orden cronológico.

  1. Kernighan, B. W. y Lin, S. (1970). "An Efficient Heuristic Procedure for Partitioning Graphs." Bell System Technical Journal 49(2), 291 a 307.
  2. Zachary, W. W. (1977). "An Information Flow Model for Conflict and Fission in Small Groups." Journal of Anthropological Research 33(4), 452 a 473.
  3. Girvan, M. y Newman, M. E. J. (2002). "Community Structure in Social and Biological Networks." Proceedings of the National Academy of Sciences 99(12), 7821 a 7826.
  4. Newman, M. E. J. y Girvan, M. (2004). "Finding and Evaluating Community Structure in Networks." Physical Review E 69, 026113.
  5. Clauset, A., Newman, M. E. J. y Moore, C. (2004). "Finding Community Structure in Very Large Networks." Physical Review E 70, 066111.
  6. Danon, L., Díaz-Guilera, A., Duch, J. y Arenas, A. (2005). "Comparing Community Structure Identification." Journal of Statistical Mechanics: Theory and Experiment, P09008.
  7. Reichardt, J. y Bornholdt, S. (2006). "Statistical Mechanics of Community Detection." Physical Review E 74, 016110.
  8. Fortunato, S. y Barthélemy, M. (2007). "Resolution Limit in Community Detection." Proceedings of the National Academy of Sciences 104(1), 36 a 41.
  9. Raghavan, U. N., Albert, R. y Kumara, S. (2007). "Near Linear Time Algorithm to Detect Community Structures in Large-Scale Networks." Physical Review E 76, 036106.
  10. Brandes, U., Delling, D., Gaertler, M., Görke, R., Hoefer, M., Nikoloski, Z. y Wagner, D. (2008). "On Modularity Clustering." IEEE Transactions on Knowledge and Data Engineering 20(2), 172 a 188.
  11. Agarwal, G. y Kempe, D. (2008). "Modularity-Maximizing Graph Communities via Mathematical Programming." European Physical Journal B 66(3), 409 a 418.
  12. Blondel, V. D., Guillaume, J.-L., Lambiotte, R. y Lefebvre, E. (2008). "Fast Unfolding of Communities in Large Networks." Journal of Statistical Mechanics: Theory and Experiment, P10008.
  13. Fortunato, S. (2010). "Community Detection in Graphs." Physics Reports 486(3 a 5), 75 a 174.
  14. Decelle, A., Krzakala, F., Moore, C. y Zdeborová, L. (2011). "Asymptotic Analysis of the Stochastic Block Model for Modular Networks and Its Algorithmic Applications." Physical Review E 84(6), 066106.
  15. Traag, V. A., Waltman, L. y van Eck, N. J. (2019). "From Louvain to Leiden: Guaranteeing Well-Connected Communities." Scientific Reports 9, 5233.

Mira la búsqueda que hay detrás de la intermediación de aristas

Girvan-Newman clasifica las aristas con una búsqueda en anchura desde cada vértice. Dibuja en el visualizador dos grupos unidos por una sola arista y lanza la búsqueda desde ambos lados para ver por qué esa arista lleva todas las rutas entre ellos.

Abrir el visualizador

Mira la búsqueda que hay detrás de la intermediación de aristas

Girvan-Newman clasifica las aristas con una búsqueda en anchura desde cada vértice. Dibuja dos grupos unidos por una sola arista y lanza la búsqueda desde ambos lados para ver por qué esa arista lleva todas las rutas entre ellos.

Abrir el visualizador de BFS