Guida totale a igraph per Python

Quando si parla di igraph Python, si entra nel cuore dell’analisi dei network complessi. Se il tuo obiettivo è studiare relazioni, flussi, strutture e dinamiche in sistemi complessi, igraph è una libreria Python di riferimento che ti offre strumenti potenti, efficienti e altamente performanti. Non lasciarti ingannare dal prefisso i: sotto il cofano, igraph è scritto in C, garantendo velocità notevoli, essenziali quando si lavora con grafi di grandi dimensioni.

Questo articolo esplorerà come iniziare a utilizzare igraph in Python, dalla creazione dei grafi all’analisi delle loro proprietà e alla loro visualizzazione, con esempi pratici e gradualmente più complessi.

1. Installazione: Il Primo Passo

Per iniziare a lavorare con igraph, devi prima installarlo. Il modo più semplice è tramite pip:

pip install python-igraph

Potrebbe essere necessario installare anche matplotlib se intendi visualizzare i grafi direttamente all’interno di Python, dato che igraph si integra bene con esso per il plotting.

pip install matplotlib

Se usate Python 3, si può scrivere equivalentemente:

pip3 install python-igraph

pip3 install matplotlib

2. Concetti Fondamentali di igraph

Prima di immergerci nel codice, è utile ripassare alcuni concetti chiave che igraph modella:

  • Grafo (Graph): La struttura dati principale, composta da vertici (nodi) e archi (collegamenti). A sua volta il grafo può essere:
    • Diretto (Directed): I archi hanno una direzione (A -> B). Utile per relazioni di precedenza, dipendenza.
    • Non Diretto (Undirected): I archi non hanno una direzione (A – B). Utile per amicizie, connessioni fisiche.
    • Misto (con archi sia orientati che non orientati)
  • Nodo / Vertice (Vertex): Gli elementi individuali nel grafo. In igraph, sono indicizzati a partire da 0.
  • Collegamento / Arco (Edge): Le connessioni tra i vertici.
  • Attributi (Attributes): Proprietà personalizzate che puoi assegnare a vertici e lati (es. nome di una persona per un vertice, peso per un lato, colore, etc.).

3. Creazione di Grafi

igraph offre diversi modi per costruire un grafo, a seconda dei dati che hai a disposizione.

a) Grafo Vuoto

Puoi iniziare con un grafo vuoto e aggiungere vertici e lati in seguito.

b) Aggiunta di Vertici e Lati

Dopo aver creato un grafo, potrai aggiungere elementi. Questo esempio crea il seguente grafo in formato JPG e PDF.

import igraph as ig
import matplotlib.pyplot as plt

# Costruisci un grafo con 5 vertici (persone)
n_vertices = 5
# Definiamo i lati (le relazioni tra le persone)
# Es: (0, 1) significa che la persona 0 è connessa alla persona 1
edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)]
g = ig.Graph(n_vertices, edges)

# Imposta attributi per il grafo, i nodi (persone) e i lati (relazioni)
g["titolo"] = "Piccola Rete Sociale Italiana" # Titolo del grafo
g.vs["nome"] = ["Mario Rossi", "Anna Ferrari", "Luca Bianchi", "Giulia Verdi", "Paolo Russo"] # Nomi delle persone
g.vs["genere"] = ["M", "F", "M", "F", "M"] # Genere M (Maschio) o F (Femmina)
g.es["sposati"] = [False, False, False, False, False, False, False, True] # Se la relazione è di matrimonio (tra 3 e 4)

# Imposta attributi individuali per specifiche persone o relazioni
g.vs[1]["nome"] = "Anna Rossi" # Anna Ferrari ha cambiato cognome, è ora Anna Rossi
g.es[0]["sposati"] = True # La relazione tra Mario (0) e Anna (1) è un matrimonio

# Plot in matplotlib
# Nota: gli attributi possono essere impostati globalmente (es. vertex_size),
# o impostati individualmente usando array (es. vertex_color)
fig, ax = plt.subplots(figsize=(7,7)) # Aumentiamo leggermente la dimensione per una migliore leggibilità

ig.plot(
g,
target=ax,
layout="circle", # Dispone i nodi in un layout circolare
vertex_size=30, # Dimensione dei nodi
# Colora i nodi: azzurro ("steelblue") per maschi, salmone ("salmon") per femmine
vertex_color=["steelblue" if genere == "M" else "salmon" for genere in g.vs["genere"]],
vertex_frame_width=4.0, # Larghezza del bordo del nodo
vertex_frame_color="white", # Colore del bordo del nodo
vertex_label=g.vs["nome"], # Etichette dei nodi (i nomi delle persone)
vertex_label_size=8.0, # Dimensione del font delle etichette
# Larghezza dei lati: più spessa (2) se 'sposati' è True, altrimenti normale (1)
edge_width=[3 if sposati else 1 for sposati in g.es["sposati"]], # Rendo la linea del matrimonio più evidente
# Colore dei lati: viola scuro per relazioni di matrimonio, grigio per le altre
edge_color=["#8A2BE2" if sposati else "#AAA" for sposati in g.es["sposati"]], # Viola per il matrimonio
edge_curved=0.2 # Rendi i lati leggermente curvi per maggiore chiarezza
)

plt.title(g["titolo"], fontsize=12) # Aggiungi il titolo del grafo
plt.show()

# Salva il grafo come file immagine
fig.savefig('rete_sociale_italiana.png')
fig.savefig('rete_sociale_italiana.jpg')
fig.savefig('rete_sociale_italiana.pdf')

print("\nGrafo della rete sociale italiana generato e salvato.")

# Esporta e importa un grafo come file GML (Graph Modeling Language)
# GML è un formato testuale comune per la descrizione dei grafi
g.save("rete_sociale_italiana.gml")
g_caricato = ig.load("rete_sociale_italiana.gml")

print(f"\nGrafo salvato come 'rete_sociale_italiana.gml' e ricaricato. Vertici: {g_caricato.vcount()}, Lati: {g_caricato.ecount()}")

c) Da Lista di Lati (Graph.TupleList())

Questo è un modo molto comune per creare grafi quando hai una lista di connessioni. Puoi anche includere attributi per i lati.

import igraph as ig
import matplotlib.pyplot as plt
# Definiamo le relazioni direttamente con i nomi delle persone,
# e aggiungiamo un attributo ‘tipo_relazione’ per ogni lato.
# Ogni tupla è (sorgente, destinazione, tipo_relazione)
relazioni_con_nomi = [
(“Mario Rossi”, “Anna Rossi”, “coniugi”),
(“Mario Rossi”, “Luca Bianchi”, “amici”),
(“Mario Rossi”, “Giulia Verdi”, “colleghi”),
(“Mario Rossi”, “Paolo Russo”, “vicini”),
(“Anna Rossi”, “Luca Bianchi”, “amici”),
(“Anna Rossi”, “Giulia Verdi”, “colleghi”),
(“Anna Rossi”, “Paolo Russo”, “conoscenti”),
(“Giulia Verdi”, “Paolo Russo”, “sposati”) # L’ultimo matrimonio è tra Giulia e Paolo
]
# Creiamo il grafo utilizzando Graph.TupleList()
# “vertex_name_attr=’nome’” indica che i nomi dei vertici saranno memorizzati nell’attributo ‘nome’
# “edge_attrs=[‘tipo_relazione’]” indica che l’ultimo elemento di ogni tupla sarà l’attributo ‘tipo_relazione’ del lato
g = ig.Graph.TupleList(relazioni_con_nomi, directed=False,
vertex_name_attr=’nome’, edge_attrs=[‘tipo_relazione’])
# Impostiamo il titolo del grafo
g[“titolo”] = “Piccola Rete Sociale Italiana (da TupleList)”
# Aggiungiamo attributi ai vertici creati automaticamente
# igraph ha già assegnato ID numerici e memorizzato i nomi
# Recuperiamo i nomi per assegnare il genere
nomi_vertici = g.vs[“nome”]
generi = []
for nome in nomi_vertici:
if”Mario”innomeor”Luca”innomeor”Paolo”innome:
generi.append(“M”)
else:
generi.append(“F”)
g.vs[“genere”] = generi
# Aggiorniamo l’attributo ‘sposati’ per i lati
# Utilizziamo l’attributo ‘tipo_relazione’ appena creato
# Rimuoviamo il vecchio attributo ‘sposati’ per ricrearlo in base al nuovo criterio
if “sposati” in g.es.attributes():
delg.es[“sposati”]
g.es[“sposati”] = [True if tipo == “sposati” or tipo == “coniugi” else False for tipo in g.es[“tipo_relazione”]]
# Plot in matplotlib
fig, ax = plt.subplots(figsize=(7,7))
ig.plot(
g,
target=ax,
layout=”circle”, # Dispone i nodi in un layout circolare
vertex_size=30, # Dimensione dei nodi
# Colora i nodi: azzurro (“steelblue”) per maschi, salmone (“salmon”) per femmine
vertex_color=[“steelblue”ifgenere==”M”else”salmon”forgenereing.vs[“genere”]],
vertex_frame_width=4.0, # Larghezza del bordo del nodo
vertex_frame_color=”white”, # Colore del bordo del nodo
vertex_label=g.vs[“nome”], # Etichette dei nodi (i nomi delle persone)
vertex_label_size=8.0, # Dimensione del font delle etichette
# Larghezza dei lati: più spessa (3) se ‘sposati’ è True, altrimenti normale (1)
edge_width=[3ifsposatielse1forsposatiing.es[“sposati”]],
# Colore dei lati: viola scuro per relazioni di matrimonio, grigio per le altre
edge_color=[“#8A2BE2″ifsposatielse”#AAA”forsposatiing.es[“sposati”]],
edge_curved=0.2# Rendi i lati leggermente curvi per maggiore chiarezza
)
plt.title(g[“titolo”], fontsize=12) # Aggiungi il titolo del grafo
plt.show()
# Salva il grafo come file immagine
fig.savefig(‘rete_sociale_italiana_tuplelist.png’)
fig.savefig(‘rete_sociale_italiana_tuplelist.jpg’)
fig.savefig(‘rete_sociale_italiana_tuplelist.pdf’)
print(“\nGrafo della rete sociale italiana (creato con TupleList) generato e salvato.”)
# Esporta e importa un grafo come file GML (Graph Modeling Language)
g.save(“rete_sociale_italiana_tuplelist.gml”)
g_caricato = ig.load(“rete_sociale_italiana_tuplelist.gml”)
print(f”\nGrafo salvato come ‘rete_sociale_italiana_tuplelist.gml’ e ricaricato. Vertici: {g_caricato.vcount()}, Lati: {g_caricato.ecount()}”)

d) Da Matrice di Adiacenza (Graph.Adjacency())

Certamente! Procediamo con la spiegazione del metodo Graph.Adjacency() di igraph, iniziando con una breve introduzione su cosa sia una matrice di adiacenza.

Cos’è una Matrice di Adiacenza?

Una matrice di adiacenza è una rappresentazione matematica di un grafo. È una matrice quadrata (ovvero, ha lo stesso numero di righe e colonne) dove sia le righe che le colonne corrispondono ai vertici (nodi) del grafo.

  • Se il grafo ha vertici, la matrice di adiacenza sarà una matrice .
  • L’elemento (situato all’intersezione della riga e della colonna ) della matrice indica se esiste un lato (o arco) che connette il vertice al vertice .

Per i grafi non diretti:

  • se esiste un lato tra il vertice e il vertice .
  • se non esiste un lato tra il vertice e il vertice .
  • La matrice è simmetrica (), poiché se c’è un lato , c’è anche un lato .

Per i grafi diretti:

  • se esiste un lato che va dal vertice al vertice .
  • se non esiste un lato in quella direzione.
  • La matrice non è necessariamente simmetrica.

Matrici Pesate:

  • Per i grafi pesati, può contenere il peso del lato anziché semplicemente o . Un valore di indica assenza di lato.

Creazione di un Grafo da Matrice di Adiacenza con Graph.Adjacency()

Il metodo Graph.Adjacency() di igraph ti permette di costruire un grafo a partire da una matrice di adiacenza fornita come una lista di liste (o un array NumPy).

Sintassi:

Python

Graph.Adjacency(adj_matrix, mode="undirected" | "directed" | "max" | "min" | "plus" | "lower" | "upper", attr=None, mapping=True)

Parametri chiave:

  • adj_matrix: La matrice di adiacenza, tipicamente una lista di liste di numeri interi (0 o 1 per grafi non pesati, pesi per grafi pesati).
  • mode: Questo parametro è fondamentale per specificare come igraph deve interpretare la matrice, specialmente per i grafi non diretti o quando la matrice non è perfettamente simmetrica:
    • "undirected" (default): Crea un grafo non diretto. Se la matrice non è simmetrica, igraph aggiungerà un lato se o è diverso da zero.
    • "directed": Crea un grafo diretto. Un lato esiste se è diverso da zero.
    • "max", "min", "plus": Utilizzati per grafi non diretti pesati. Determinano come combinare i pesi e se la matrice non è simmetrica (es. max prende il massimo tra i due).
    • "lower", "upper": Utilizzati per grafi non diretti. Usano solo la parte inferiore o superiore della matrice (utile se la matrice è già stata pensata come simmetrica e si vuole risparmiare sul tempo di elaborazione).
  • attr (opzionale): Se la matrice contiene pesi, questo parametro specifica il nome dell’attributo del lato in cui verranno memorizzati i pesi (es. attr="weight").
  • mapping: Se True (default), igraph crea vertici con ID da a . Se False, usa direttamente i valori non zero come ID, il che è raro e va usato con cautela.

Esempio: Creazione di un Grafo Sociale da Matrice di Adiacenza

Per collegarci al tuo esempio precedente, useremo una matrice di adiacenza per rappresentare le stesse connessioni, ma senza nomi diretti nella matrice stessa (i nomi verranno aggiunti come attributi in seguito).

Matrice di Adiacenza per il tuo Grafo (5 vertici):

Consideriamo i tuoi vertici come:

  • 0: Mario Rossi
  • 1: Anna Rossi
  • 2: Luca Bianchi
  • 3: Giulia Verdi
  • 4: Paolo Russo

Le tue connessioni originali erano: [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)]

Quindi, la matrice di adiacenza (non diretta, simmetrica) sarà:

    0 1 2 3 4
0: [0,1,1,1,1]  (0 è connesso con 1,2,3,4)
1: [1,0,1,1,1]  (1 è connesso con 0,2,3,4)
2: [1,1,0,0,0]  (2 è connesso con 0,1)
3: [1,1,0,0,1]  (3 è connesso con 0,1,4)
4: [1,1,0,1,0]  (4 è connesso con 0,1,3)

Nota: ho basato la matrice sugli edges forniti nel tuo primo esempio.

[(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)]

Convertiamo questa logica in una matrice:

  • (0,1): 0-1, 1-0
  • (0,2): 0-2, 2-0
  • (0,3): 0-3, 3-0
  • (0,4): 0-4, 4-0
  • (1,2): 1-2, 2-1
  • (1,3): 1-3, 3-1
  • (1,4): 1-4, 4-1
  • (3,4): 3-4, 4-3

Matrice corretta per i tuoi edges:

Indices: 0 1 2 3 4

—————-

0: [0, 1, 1, 1, 1]

1: [1, 0, 1, 1, 1]

2: [1, 1, 0, 0, 0]

3: [1, 1, 0, 0, 1]

4: [1, 1, 0, 1, 0]

Python

import igraph as ig
import matplotlib.pyplot as plt

# Definizione della matrice di adiacenza
# I valori '1' indicano una connessione, '0' l'assenza
adj_matrix = [
    [0, 1, 1, 1, 1], # Connessioni di Mario (0)
    [1, 0, 1, 1, 1], # Connessioni di Anna (1)
    [1, 1, 0, 0, 0], # Connessioni di Luca (2)
    [1, 1, 0, 0, 1], # Connessioni di Giulia (3)
    [1, 1, 0, 1, 0]  # Connessioni di Paolo (4)
]

# Creiamo il grafo dalla matrice di adiacenza
# Usiamo mode="undirected" perché le relazioni sociali sono tipicamente bidirezionali
g = ig.Graph.Adjacency(adj_matrix, mode="undirected")

# Impostiamo gli attributi dei vertici (persone)
g["titolo"] = "Piccola Rete Sociale Italiana (da Matrice di Adiacenza)"
g.vs["nome"] = ["Mario Rossi", "Anna Ferrari", "Luca Bianchi", "Giulia Verdi", "Paolo Russo"]
g.vs["genere"] = ["M", "F", "M", "F", "M"]

# Impostiamo gli attributi dei lati (relazioni)
# Dobbiamo sapere quali lati sono "sposati" in base alla loro posizione
# Ricordiamo che igraph assegna ID agli edge nell'ordine in cui vengono "trovati"
# per prima il lato (0,1), poi (0,2), (0,3), (0,4), (1,2), (1,3), (1,4), (3,4)
# Nel tuo esempio originale, il matrimonio era (3,4) e (0,1).
# Qui le dobbiamo gestire manualmente, dato che la matrice non ha questo attributo
married_edges = []
for edge in g.es:
    # Controlliamo se la coppia di nomi corrisponde a quelle sposate
    source_name = g.vs[edge.source]["nome"]
    target_name = g.vs[edge.target]["nome"]

    # Anna e Mario (0,1)
    if (source_name == "Mario Rossi" and target_name == "Anna Ferrari") or \
       (source_name == "Anna Ferrari" and target_name == "Mario Rossi"):
        married_edges.append(True)
    # Giulia e Paolo (3,4)
    elif (source_name == "Giulia Verdi" and target_name == "Paolo Russo") or \
         (source_name == "Paolo Russo" and target_name == "Giulia Verdi"):
        married_edges.append(True)
    else:
        married_edges.append(False)

g.es["sposati"] = married_edges


# Plot in matplotlib
fig, ax = plt.subplots(figsize=(7,7))

ig.plot(
    g,
    target=ax,
    layout="circle",
    vertex_size=30,
    vertex_color=["steelblue" if genere == "M" else "salmon" for genere in g.vs["genere"]],
    vertex_frame_width=4.0,
    vertex_frame_color="white",
    vertex_label=g.vs["nome"],
    vertex_label_size=8.0,
    edge_width=[3 if sposati else 1 for sposati in g.es["sposati"]],
    edge_color=["#8A2BE2" if sposati else "#AAA" for sposati in g.es["sposati"]],
    edge_curved=0.2
)

plt.title(g["titolo"], fontsize=12)
plt.show()

# Salva il grafo come file immagine
fig.savefig('rete_sociale_italiana_adj_matrix.png')
print("\nGrafo della rete sociale italiana (da Matrice di Adiacenza) generato e salvato.")

# Esporta e importa un grafo come file GML
g.save("rete_sociale_italiana_adj_matrix.gml")
g_caricato = ig.load("rete_sociale_italiana_adj_matrix.gml")

print(f"\nGrafo salvato come 'rete_sociale_italiana_adj_matrix.gml' e ricaricato. Vertici: {g_caricato.vcount()}, Lati: {g_caricato.ecount()}")

Spiegazione delle Modifiche e Considerazioni

  1. Definizione della Matrice di Adiacenza:

    • Abbiamo creato una lista di liste adj_matrix che rappresenta esplicitamente le connessioni tra i 5 vertici. L’indice di riga/colonna corrisponde all’ID del vertice (da 0 a 4).
    • I valori 1 indicano la presenza di un lato, 0 l’assenza. Data la natura non diretta del grafo, la matrice è simmetrica.
  2. g = ig.Graph.Adjacency(adj_matrix, mode="undirected"):

    • Questo è il cuore della creazione. igraph legge la matrice e crea i vertici (ID da 0 a 4) e i lati corrispondenti.
    • Il mode="undirected" è cruciale per indicare che stiamo costruendo un grafo non diretto.
  3. Attribuzione degli Attributi:

    • A differenza di TupleList, la matrice di adiacenza di base non veicola direttamente nomi o attributi dei lati. Quindi, dobbiamo assegnare g.vs["nome"] e g.vs["genere"] separatamente, una volta che il grafo e i suoi vertici numerici sono stati creati.
    • Per l’attributo g.es["sposati"], è necessario iterare sui lati esistenti del grafo (che igraph ha creato dalla matrice) e determinare quali di essi corrispondono alle relazioni di matrimonio, basandosi sui nomi dei vertici coinvolti. Questo è un po’ più laborioso qui perché la matrice di adiacenza “pura” non ha un concetto di “tipo di relazione” o “peso” a meno che non si usino valori diversi da 0 e 1 e l’argomento attr.

Vantaggi di Graph.Adjacency():

  • Input diretto: Ideale se hai già i tuoi dati in formato matrice di adiacenza (es. da calcoli scientifici o da altri sistemi).
  • Controllo esplicito: Puoi vedere esattamente quali connessioni stai definendo a livello di riga/colonna.

Svantaggi (rispetto a TupleList per alcuni casi):

  • Meno intuitivo per grafi sparsi: Per grafi con pochi lati rispetto al numero di vertici (grafi sparsi), la maggior parte della matrice sarà piena di zeri, rendendola inefficiente in termini di spazio e meno immediata da leggere per gli esseri umani.
  • Gestione attributi: I nomi dei vertici e gli attributi dei lati (come “sposati” o “tipo_relazione”) non sono inclusi direttamente nella matrice (a meno che non sia una matrice pesata e tu usi l’argomento attr per un singolo attributo numerico). Richiedono un passaggio successivo per l’assegnazione.

Questo esempio mostra chiaramente come puoi costruire un grafo igraph partendo dalla sua rappresentazione matriciale, e poi arricchirlo con gli attributi necessari per l’analisi e la visualizzazione.