In una vasta gamma di campi la network analysis è diventata uno strumento sempre più popolare tra gli studiosi per affrontare la complessità delle interrelazioni tra attori di ogni tipo. La promessa della network analysis è l’attribuzione di significato alle relazioni tra gli attori, piuttosto che vedere gli attori come entità isolate. L’enfasi sulla complessità, insieme alla creazione di una varietà di algoritmi per misurare vari aspetti delle reti, rende la network analysis uno strumento centrale per le digital humanities.1 Questo post fornirà un’introduzione al lavoro con le reti in R, usando l’esempio della rete di città nella corrispondenza di Daniel van der Meulen nel 1585.

Ci sono un certo numero di applicazioni progettate per la network analysis e la creazione di grafici di rete come gephi e cytoscape. Anche se non specificamente progettato per questo, R si è sviluppato in un potente strumento per l’analisi delle reti. La forza di R rispetto ai software di analisi di rete stand-alone è triplice. In primo luogo, R permette una ricerca riproducibile che non è possibile con le applicazioni GUI. In secondo luogo, la potenza di analisi dei dati di R fornisce strumenti robusti per manipolare i dati per prepararli all’analisi di rete. Infine, c’è una gamma sempre crescente di pacchetti progettati per rendere R uno strumento completo di analisi di rete. Significativi pacchetti di analisi di rete per R includono la suite di pacchetti statnet e igraph. Inoltre, Thomas Lin Pedersen ha recentemente rilasciato i pacchetti tidygraph e ggraph che sfruttano la potenza di igraph in modo coerente con il flusso di lavoro di tidyverse. R può anche essere usato per fare grafici di rete interattivi con il framework htmlwidgets che traduce il codice R in JavaScript.

Questo post inizia con una breve introduzione al vocabolario di base dell’analisi di rete, seguita da una discussione sul processo per ottenere dati nella struttura appropriata per l’analisi di rete. I pacchetti di analisi di rete hanno tutti implementato le proprie classi di oggetti. In questo post, mostrerò come creare le classi oggetto specifiche per la suite di pacchetti statnet con il pacchetto network, così come per igraph e tidygraph, che è basato sull’implementazione igraph. Infine, passerò alla creazione di grafici interattivi con i pacchetti vizNetwork e networkD3.

Analisi della rete: Nodi e Bordi

I due aspetti principali delle reti sono una moltitudine di entità separate e le connessioni tra loro. Il vocabolario può essere un po’ tecnico e anche incoerente tra diverse discipline, pacchetti e software. Le entità sono indicate come nodi o vertici di un grafico, mentre le connessioni sono bordi o collegamenti. In questo post userò principalmente la nomenclatura dei nodi e dei bordi, tranne quando si parla di pacchetti che usano un vocabolario diverso.

I pacchetti di analisi di rete hanno bisogno che i dati siano in una forma particolare per creare lo speciale tipo di oggetto usato da ogni pacchetto. Le classi di oggetti per network, igraph e tidygraph sono tutte basate su matrici di adiacenza, note anche come sociomatrici.2 Una matrice di adiacenza è una matrice quadrata in cui i nomi delle colonne e delle righe sono i nodi della rete. All’interno della matrice un 1 indica che c’è una connessione tra i nodi, e uno 0 indica nessuna connessione. Le matrici di adiacenza implementano una struttura dati molto diversa dai data frame e non si adattano al flusso di lavoro tidyverse che ho usato nei miei post precedenti. Fortunatamente, gli oggetti di rete specializzati possono anche essere creati da un frame di dati edge-list, che si adattano al flusso di lavoro di tidyverse. In questo post mi atterrò alle tecniche di analisi dei dati del tidyverse per creare elenchi di edge, che saranno poi convertiti nelle classi di oggetti specifici per network, igraph e tidygraph.

Un elenco di edge è un data frame che contiene un minimo di due colonne, una colonna di nodi che sono la fonte di una connessione e un’altra colonna di nodi che sono la destinazione della connessione. I nodi nei dati sono identificati da ID unici. Se la distinzione tra sorgente e destinazione è significativa, la rete è diretta. Se la distinzione non è significativa, la rete è indiretta. Nell’esempio delle lettere spedite tra città, la distinzione tra fonte e destinazione è chiaramente significativa, e quindi la rete è diretta. Per gli esempi seguenti, chiamerò la colonna della fonte come “da” e la colonna della destinazione come “a”. Userò numeri interi che iniziano con uno come ID dei nodi.3 Una lista di bordi può anche contenere colonne aggiuntive che descrivono gli attributi dei bordi, come un aspetto di magnitudine per un bordo. Se i bordi hanno un attributo di grandezza il grafico è considerato ponderato.

Le liste di bordi contengono tutte le informazioni necessarie per creare oggetti di rete, ma a volte è preferibile creare anche una lista di nodi separata. Nella sua forma più semplice, una lista di nodi è un frame di dati con una singola colonna – che etichetterò come “id” – che elenca gli ID dei nodi trovati nella lista degli edge. Il vantaggio di creare una lista di nodi separata è la possibilità di aggiungere colonne di attributi alla struttura dei dati, come i nomi dei nodi o qualsiasi tipo di raggruppamento. Di seguito do un esempio di liste minime di bordi e nodi create con la funzione tibble().

library(tidyverse)edge_list <- tibble(from = c(1, 2, 2, 3, 4), to = c(2, 3, 4, 2, 1))node_list <- tibble(id = 1:4)edge_list#> # A tibble: 5 x 2#> from to#> <dbl> <dbl>#> 1 1 2#> 2 2 3#> 3 2 4#> 4 3 2#> 5 4 1node_list#> # A tibble: 4 x 1#> id#> <int>#> 1 1#> 2 2#> 3 3#> 4 4

Confronta questo con una matrice di adiacenza con gli stessi dati.

#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0

Creazione di liste di bordi e nodi

Per creare oggetti di rete dal database delle lettere ricevute da Daniel van der Meulen nel 1585 farò sia una lista di bordi che una di nodi. Questo richiederà l’uso del pacchetto dplyr per manipolare il frame di dati delle lettere inviate a Daniel e dividerlo in due frame di dati o tibbie con la struttura di liste di bordi e nodi. In questo caso, i nodi saranno le città da cui i corrispondenti di Daniel gli hanno inviato le lettere e le città in cui le ha ricevute. La lista dei nodi conterrà una colonna “etichetta”, contenente i nomi delle città. La lista dei bordi avrà anche una colonna attributo che mostrerà la quantità di lettere inviate tra ogni coppia di città. Il flusso di lavoro per creare questi oggetti sarà simile a quello che ho usato nella mia breve introduzione a R e nella geocodifica con R. Se volete seguire, potete trovare i dati usati in questo post e lo script R usato su GitHub.

Il primo passo è caricare la libreria tidyverse per importare e manipolare i dati. La stampa del data frame letters mostra che contiene quattro colonne: “scrittore”, “fonte”, “destinazione” e “data”. In questo esempio, ci occuperemo solo delle colonne “sorgente” e “destinazione”.

library(tidyverse)letters <- read_csv("data/correspondence-data-1585.csv")letters#> # A tibble: 114 x 4#> writer source destination date#> <chr> <chr> <chr> <date>#> 1 Meulen, Andries van der Antwerp Delft 1585-01-03#> 2 Meulen, Andries van der Antwerp Haarlem 1585-01-09#> 3 Meulen, Andries van der Antwerp Haarlem 1585-01-11#> 4 Meulen, Andries van der Antwerp Delft 1585-01-12#> 5 Meulen, Andries van der Antwerp Haarlem 1585-01-12#> 6 Meulen, Andries van der Antwerp Delft 1585-01-17#> 7 Meulen, Andries van der Antwerp Delft 1585-01-22#> 8 Meulen, Andries van der Antwerp Delft 1585-01-23#> 9 Della Faille, Marten Antwerp Haarlem 1585-01-24#> 10 Meulen, Andries van der Antwerp Delft 1585-01-28#> # ... with 104 more rows

Lista di nodi

Il flusso di lavoro per creare una lista di nodi è simile a quello che ho usato per ottenere la lista di città per geocodificare i dati in un post precedente. Vogliamo ottenere le città distinte da entrambe le colonne “sorgente” e “destinazione” e poi unire le informazioni da queste colonne insieme. Nell’esempio qui sotto, cambio leggermente i comandi da quelli che ho usato nel post precedente per fare in modo che il nome delle colonne con i nomi delle città sia lo stesso per entrambi i data frame sources e destinations per semplificare la funzione full_join(). Rinomino la colonna con i nomi delle città come “label” per adottare il vocabolario usato dai pacchetti di analisi di rete.

sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)

Per creare un unico dataframe con una colonna con le località uniche dobbiamo usare un full join, perché vogliamo includere tutte le località uniche sia dalle fonti delle lettere che dalle destinazioni.

nodes <- full_join(sources, destinations, by = "label")nodes#> # A tibble: 13 x 1#> label#> <chr>#> 1 Antwerp#> 2 Haarlem#> 3 Dordrecht#> 4 Venice#> 5 Lisse#> 6 Het Vlie#> 7 Hamburg#> 8 Emden#> 9 Amsterdam#> 10 Delft#> 11 The Hague#> 12 Middelburg#> 13 Bremen

Questo risulta in un data frame con una variabile. Tuttavia, la variabile contenuta nel frame di dati non è proprio quella che stiamo cercando. La colonna “label” contiene i nomi dei nodi, ma vogliamo anche avere degli ID unici per ogni città. Possiamo farlo aggiungendo una colonna “id” al data frame nodes che contiene numeri da uno a qualunque sia il numero totale di righe nel data frame. Una funzione utile per questo flusso di lavoro è rowid_to_column(), che aggiunge una colonna con i valori degli id delle righe e pone la colonna all’inizio del frame di dati.4 Notate che rowid_to_column() è un comando pipeable, e quindi è possibile fare full_join() e aggiungere la colonna “id” in un unico comando. Il risultato è una lista di nodi con una colonna ID e un attributo label.

nodes <- nodes %>% rowid_to_column("id")nodes#> # A tibble: 13 x 2#> id label#> <int> <chr>#> 1 1 Antwerp#> 2 2 Haarlem#> 3 3 Dordrecht#> 4 4 Venice#> 5 5 Lisse#> 6 6 Het Vlie#> 7 7 Hamburg#> 8 8 Emden#> 9 9 Amsterdam#> 10 10 Delft#> 11 11 The Hague#> 12 12 Middelburg#> 13 13 Bremen

Lista di bordi

Creare una lista di bordi è simile a quanto detto sopra, ma è complicato dalla necessità di avere a che fare con due colonne ID invece di una. Vogliamo anche creare una colonna peso che annoterà la quantità di lettere inviate tra ogni set di nodi. Per fare questo userò lo stesso flusso di lavoro group_by() e summarise() che ho discusso nei post precedenti. La differenza qui è che vogliamo raggruppare il frame di dati per due colonne – “fonte” e “destinazione” – invece di una sola. In precedenza, ho chiamato la colonna che conta il numero di osservazioni per gruppo “count”, ma qui adotto la nomenclatura della network analysis e la chiamo “weight”. Il comando finale nella pipeline rimuove il raggruppamento per il data frame istituito dalla funzione group_by(). Questo rende più facile manipolare il risultante per_route data frame senza ostacoli.5

per_route <- letters %>% group_by(source, destination) %>% summarise(weight = n()) %>% ungroup()per_route#> # A tibble: 15 x 3#> source destination weight#> <chr> <chr> <int>#> 1 Amsterdam Bremen 1#> 2 Antwerp Delft 68#> 3 Antwerp Haarlem 5#> 4 Antwerp Middelburg 1#> 5 Antwerp The Hague 2#> 6 Dordrecht Haarlem 1#> 7 Emden Bremen 1#> 8 Haarlem Bremen 2#> 9 Haarlem Delft 26#> 10 Haarlem Middelburg 1#> 11 Haarlem The Hague 1#> 12 Hamburg Bremen 1#> 13 Het Vlie Bremen 1#> 14 Lisse Delft 1#> 15 Venice Haarlem 2

Come la lista dei nodi, per_route ha ora la forma base che vogliamo, ma abbiamo di nuovo il problema che le colonne “sorgente” e “destinazione” contengono etichette piuttosto che ID. Quello che dobbiamo fare è collegare gli ID che sono stati assegnati in nodes ad ogni posizione in entrambe le colonne “sorgente” e “destinazione”. Questo può essere realizzato con un’altra funzione di unione. Infatti, è necessario eseguire due join, uno per la colonna “sorgente” e uno per “destinazione”. In questo caso, userò un left_join() con per_route come data frame di sinistra, perché vogliamo mantenere il numero di righe in per_route. Mentre facciamo il left_join, vogliamo anche rinominare le due colonne “id” che vengono portate da nodes. Per il join usando la colonna “sorgente” rinominerò la colonna come “da”. La colonna portata dal join di “destinazione” viene rinominata “to”. Sarebbe possibile fare entrambi i join in un unico comando con l’uso della pipe. Tuttavia, per chiarezza, eseguirò i join in due comandi separati. Poiché l’unione è fatta in due comandi, notate che il frame di dati all’inizio della pipeline cambia da per_route a edges, che è creato dal primo comando.

edges <- per_route %>% left_join(nodes, by = c("source" = "label")) %>% rename(from = id)edges <- edges %>% left_join(nodes, by = c("destination" = "label")) %>% rename(to = id)

Ora che edges ha colonne “from” e “to” con ID di nodo, dobbiamo riordinare le colonne per portare “from” e “to” a sinistra del frame di dati. Attualmente, il data frame edges contiene ancora le colonne “origine” e “destinazione” con i nomi delle città che corrispondono agli ID. Tuttavia, questi dati sono superflui, poiché sono già presenti in nodes. Pertanto, includerò solo le colonne “da”, “a” e “peso” nella funzione select().

edges <- select(edges, from, to, weight)edges#> # A tibble: 15 x 3#> from to weight#> <int> <int> <int>#> 1 9 13 1#> 2 1 10 68#> 3 1 2 5#> 4 1 12 1#> 5 1 11 2#> 6 3 2 1#> 7 8 13 1#> 8 2 13 2#> 9 2 10 26#> 10 2 12 1#> 11 2 11 1#> 12 7 13 1#> 13 6 13 1#> 14 5 10 1#> 15 4 2 2

Il data frame edges non sembra molto impressionante; sono tre colonne di numeri interi. Tuttavia, edges combinato con nodes ci fornisce tutte le informazioni necessarie per creare oggetti di rete con i pacchetti network, igraph e tidygraph.

Creazione di oggetti di rete

Le classi di oggetti di rete per network, igraph e tidygraph sono tutte strettamente correlate. È possibile tradurre tra un oggetto network e un oggetto igraph. Tuttavia, è meglio tenere separati i due pacchetti e i loro oggetti. Infatti, le capacità di network e igraph si sovrappongono a tal punto che è meglio avere solo uno dei due pacchetti caricato alla volta. Inizierò esaminando il pacchetto network per poi passare ai pacchetti igraph e tidygraph.

network

library(network)

La funzione usata per creare un oggetto network è network(). Il comando non è particolarmente semplice, ma potete sempre inserire ?network() nella console se vi confondete. Il primo argomento è – come dichiarato nella documentazione – “una matrice che dà la struttura della rete in forma di adiacenza, incidenza o edgelist”. Il linguaggio dimostra l’importanza delle matrici nell’analisi della rete, ma invece di una matrice, abbiamo una lista di edge, che riempie lo stesso ruolo. Il secondo argomento è una lista di attributi dei vertici, che corrisponde alla lista dei nodi. Notate che il pacchetto network usa la nomenclatura dei vertici invece dei nodi. Lo stesso vale per igraph. Dobbiamo poi specificare il tipo di dati che è stato inserito nei primi due argomenti, specificando che il matrix.type è un "edgelist". Infine, impostiamo ignore.eval su FALSE in modo che la nostra rete possa essere ponderata e tenere conto del numero di lettere lungo ogni percorso.

routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)

Si può vedere il tipo di oggetto creato dalla funzione network() mettendo routes_network nella funzione class().

class(routes_network)#> "network"

Stampare routes_network sulla console mostra che la struttura dell’oggetto è abbastanza diversa dagli oggetti stile data-frame come edges e nodes. Il comando di stampa rivela informazioni che sono specificamente definite per l’analisi della rete. Mostra che ci sono 13 vertici o nodi e 15 bordi in routes_network. Questi numeri corrispondono al numero di righe in nodes e edges rispettivamente. Possiamo anche vedere che i vertici e i bordi contengono entrambi attributi come etichetta e peso. È possibile ottenere ancora più informazioni, inclusa una sociomatrice dei dati, inserendo summary(routes_network).

routes_network#> Network attributes:#> vertices = 13 #> directed = TRUE #> hyper = FALSE #> loops = FALSE #> multiple = FALSE #> bipartite = FALSE #> total edges= 15 #> missing edges= 0 #> non-missing edges= 15 #> #> Vertex attribute names: #> id label vertex.names #> #> Edge attribute names: #> weight

È ora possibile ottenere un grafico rudimentale, anche se non troppo esteticamente piacevole, della nostra rete di lettere. Entrambi i pacchetti network e igraph usano il sistema di plottaggio di base di R. Le convenzioni per i plot di base sono significativamente diverse da quelle di ggplot2 – di cui ho parlato nei post precedenti – e quindi mi atterrò a plot piuttosto semplici invece di entrare nei dettagli della creazione di plot complessi con R di base. In questo caso, l’unica modifica che faccio alla funzione predefinita plot() per il pacchetto network è di aumentare la dimensione dei nodi con l’argomento vertex.cex per renderli più visibili. Anche con questo grafico molto semplice, possiamo già imparare qualcosa sui dati. Il grafico rende chiaro che ci sono due raggruppamenti principali o cluster dei dati, che corrispondono al tempo che Daniel ha passato in Olanda nei primi tre quarti del 1585 e dopo il suo trasferimento a Brema in settembre.

plot(routes_network, vertex.cex = 3)

La funzione plot() con un oggetto network utilizza l’algoritmo di Fruchterman e Reingold per decidere il posizionamento dei nodi.6 Puoi cambiare l’algoritmo di disposizione con l’argomento mode. Sotto, ho disposto i nodi in un cerchio. Non è una disposizione particolarmente utile per questa rete, ma dà un’idea di alcune delle opzioni disponibili.

plot(routes_network, vertex.cex = 3, mode = "circle")

igraph

Passiamo ora a discutere il pacchetto igraph. Per prima cosa, dobbiamo pulire l’ambiente in R rimuovendo il pacchetto network in modo che non interferisca con i comandi igraph. Potremmo anche rimuovere routes_network dato che non lo useremo più. Il pacchetto network può essere rimosso con la funzione detach(), e routes_network viene rimosso con rm().7 Dopo questo, possiamo tranquillamente caricare igraph.

detach(package:network)rm(routes_network)library(igraph)

Per creare un oggetto igraph da un data frame edge-list possiamo usare la funzione graph_from_data_frame(), che è un po’ più diretta di network(). Ci sono tre argomenti nella funzione graph_from_data_frame(): d, verticali e diretti. Qui, d si riferisce alla lista dei bordi, vertices alla lista dei nodi, e directed può essere TRUE o FALSE a seconda che i dati siano diretti o indiretti.

routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)

Stampare l’oggetto igraph creato da graph_from_data_frame() sulla console rivela informazioni simili a quelle di un oggetto network, sebbene la struttura sia più criptica.

routes_igraph#> IGRAPH f84c784 DNW- 13 15 -- #> + attr: name (v/c), label (v/c), weight (e/n)#> + edges from f84c784 (vertex names):#> 9->13 1->10 1->2 1->12 1->11 3->2 8->13 2->13 2->10 2->12 2->11#> 7->13 6->13 5->10 4->2

Le informazioni principali sull’oggetto sono contenute in DNW- 13 15 --. Questo dice che routes_igraph è una rete diretta (D) che ha un attributo nome (N) ed è pesata (W). Il trattino dopo W ci dice che il grafico non è bipartito. I numeri che seguono descrivono rispettivamente il numero di nodi e di bordi nel grafico. Poi, name (v/c), label (v/c), weight (e/n) dà informazioni sugli attributi del grafico. Ci sono due attributi di vertice (v/c) di nome – che sono gli ID – e le etichette e un attributo di bordo (e/n) di peso. Infine, c’è una stampa di tutti i bordi.

Come per il pacchetto network, possiamo creare un grafico con un oggetto igraph attraverso la funzione plot(). L’unica modifica che faccio qui al default è di diminuire la dimensione delle frecce. Di default igraph etichetta i nodi con la colonna delle etichette se ce n’è una o con gli ID.

plot(routes_igraph, edge.arrow.size = 0.2)

Come il grafico network di prima, il default di un plot igraph non è particolarmente piacevole esteticamente, ma tutti gli aspetti dei plot possono essere manipolati. Qui, voglio solo cambiare la disposizione dei nodi per usare l’algoritmo graphopt creato da Michael Schmuhl. Questo algoritmo rende più facile vedere la relazione tra Haarlem, Anversa e Delft, che sono tre delle località più significative nella rete di corrispondenza, distribuendole ulteriormente.

plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)

tidygraph e ggraph

I pacchetti tidygraph e ggraph sono nuovi nel panorama dell’analisi delle reti, ma insieme i due pacchetti forniscono reali vantaggi rispetto ai pacchetti network e igraph. tidygraph e ggraph rappresentano un tentativo di portare l’analisi di rete nel flusso di lavoro di tidyverse. tidygraph fornisce un modo per creare un oggetto rete che assomiglia più da vicino a un tibble o a un data frame. Questo rende possibile utilizzare molte delle funzioni dplyr per manipolare i dati di rete. ggraph fornisce un modo per tracciare grafici di rete usando le convenzioni e la potenza di ggplot2. In altre parole, tidygraph e ggraph vi permettono di trattare gli oggetti di rete in un modo che è più coerente con i comandi usati per lavorare con tibbie e data frame. Tuttavia, la vera promessa di tidygraph e ggraph è che sfruttano la potenza di igraph. Questo significa che si sacrificano poche delle capacità di analisi di rete di igraph usando tidygraph e ggraph.

Dobbiamo iniziare come sempre caricando i pacchetti necessari.

library(tidygraph)library(ggraph)

Primo, creiamo un oggetto rete usando tidygraph, che è chiamato tbl_graph. Un tbl_graph consiste di due tibbie: una tibba di bordi e una tibba di nodi. Convenientemente, la classe dell’oggetto tbl_graph è un involucro attorno ad un oggetto igraph, il che significa che alla sua base un oggetto tbl_graph è essenzialmente un oggetto igraph.8 Lo stretto legame tra gli oggetti tbl_graph e igraph risulta in due modi principali per creare un oggetto tbl_graph. Il primo è quello di usare una lista di bordi e una lista di nodi, usando tbl_graph(). Gli argomenti della funzione sono quasi identici a quelli di graph_from_data_frame() con solo un leggero cambiamento nei nomi degli argomenti.

routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)

Il secondo modo di creare un oggetto tbl_graph è convertire un oggetto igraph o network usando as_tbl_graph(). Così, potremmo convertire routes_igraph in un oggetto tbl_graph.

routes_igraph_tidy <- as_tbl_graph(routes_igraph)

Ora che abbiamo creato due oggetti tbl_graph, esaminiamoli con la funzione class(). Questo mostra che routes_tidy e routes_igraph_tidy sono oggetti di classe "tbl_graph" "igraph", mentre routes_igraph è oggetto di classe "igraph".

class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"

Stampando un oggetto tbl_graph sulla console si ottiene un output drasticamente diverso da quello di un oggetto igraph. È un output simile a quello di un normale tibble.

routes_tidy#> # A tbl_graph: 13 nodes and 15 edges#> ##> # A directed acyclic simple graph with 1 component#> ##> # Node Data: 13 x 2 (active)#> id label#> <int> <chr>#> 1 1 Antwerp#> 2 2 Haarlem#> 3 3 Dordrecht#> 4 4 Venice#> 5 5 Lisse#> 6 6 Het Vlie#> # ... with 7 more rows#> ##> # Edge Data: 15 x 3#> from to weight#> <int> <int> <int>#> 1 9 13 1#> 2 1 10 68#> 3 1 2 5#> # ... with 12 more rows

Stampare routes_tidy mostra che è un oggetto tbl_graph con 13 nodi e 15 bordi. Il comando stampa anche le prime sei righe di “Node Data” e le prime tre di “Edge Data”. Notate anche che afferma che il Node Data è attivo. La nozione di un tibble attivo all’interno di un oggetto tbl_graph rende possibile manipolare i dati in un tibble alla volta. La tibla dei nodi è attivata di default, ma è possibile cambiare quale tibla è attiva con la funzione activate(). Così, se volessi riorganizzare le righe nella tibla dei bordi per elencare prima quelle con il “peso” più alto, potrei usare activate() e poi arrange(). Qui stampo semplicemente il risultato piuttosto che salvarlo.

routes_tidy %>% activate(edges) %>% arrange(desc(weight))#> # A tbl_graph: 13 nodes and 15 edges#> ##> # A directed acyclic simple graph with 1 component#> ##> # Edge Data: 15 x 3 (active)#> from to weight#> <int> <int> <int>#> 1 1 10 68#> 2 2 10 26#> 3 1 2 5#> 4 1 11 2#> 5 2 13 2#> 6 4 2 2#> # ... with 9 more rows#> ##> # Node Data: 13 x 2#> id label#> <int> <chr>#> 1 1 Antwerp#> 2 2 Haarlem#> 3 3 Dordrecht#> # ... with 10 more rows

Siccome non abbiamo bisogno di manipolare ulteriormente routes_tidy, possiamo tracciare il grafico con ggraph. Come ggmap, ggraph è un’estensione di ggplot2, rendendo più facile trasferire le abilità di base di ggplot alla creazione di grafici di rete. Come in tutti i grafici di rete, ci sono tre aspetti principali in una trama ggraph: nodi, bordi e layout. Le vignette per il pacchetto ggraph coprono utilmente gli aspetti fondamentali dei grafici ggraph. ggraph aggiunge geom speciali al set di base di ggplot geom che sono specificamente progettati per le reti. Così, c’è un insieme di geom_node e geom_edge geom. La funzione di plotting di base è ggraph(), che prende i dati da usare per il grafico e il tipo di layout desiderato. Entrambi gli argomenti per ggraph() sono costruiti intorno a igraph. Pertanto, ggraph() può utilizzare sia un oggetto igraph che un oggetto tbl_graph. Inoltre, gli algoritmi di layout disponibili derivano principalmente da igraph. Infine, ggraph introduce un tema speciale ggplot che fornisce migliori impostazioni predefinite per i grafici di rete rispetto a quelle normali ggplot. Il tema ggraph può essere impostato per una serie di grafici con il comando set_graph_style() eseguito prima che i grafici siano tracciati o usando theme_graph() nei singoli grafici. Qui userò quest’ultimo metodo.

Vediamo come appare un grafico ggraph di base. La trama inizia con ggraph() e i dati. Poi aggiungo i geom. di base dei bordi e dei nodi. Non sono necessari argomenti all’interno dei geom di bordo e di nodo, perché prendono le informazioni dai dati forniti in ggraph().

ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()

Come potete vedere, la struttura del comando è simile a quella di ggplot con i livelli separati aggiunti con il segno +. Il grafico di base di ggraph è simile a quelli di network e igraph, se non addirittura più semplice, ma possiamo usare comandi simili a ggplot per creare un grafico più informativo. Possiamo mostrare il “peso” dei bordi – o la quantità di lettere inviate lungo ogni percorso – usando la larghezza nella funzione geom_edge_link(). Per far sì che la larghezza della linea cambi in base alla variabile peso, mettiamo l’argomento all’interno di una funzione aes(). Per controllare la larghezza massima e minima dei bordi, uso scale_edge_width() e imposto un range. Scelgo una larghezza relativamente piccola per il minimo, perché c’è una differenza significativa tra il numero massimo e minimo di lettere inviate lungo i percorsi. Possiamo anche etichettare i nodi con i nomi delle località, dato che ci sono relativamente pochi nodi. Convenientemente, geom_node_text() viene fornito con un argomento repel che assicura che le etichette non si sovrappongano ai nodi in modo simile al pacchetto ggrepel. Aggiungo un po’ di trasparenza ai bordi con l’argomento alpha. Uso anche labs() per rietichettare la legenda “Letters”.

ggraph(routes_tidy, layout = "graphopt") + geom_node_point() + geom_edge_link(aes(width = weight), alpha = 0.8) + scale_edge_width(range = c(0.2, 2)) + geom_node_text(aes(label = label), repel = TRUE) + labs(edge_width = "Letters") + theme_graph()

In aggiunta alle scelte di layout fornite da igraph, ggraph implementa anche i propri layout. Per esempio, puoi usare il concetto di circolarità di ggraph's per creare diagrammi ad arco. Qui, imposto i nodi in una linea orizzontale e faccio disegnare i bordi come archi. A differenza del grafico precedente, questo grafico indica la direzionalità dei bordi.9 I bordi sopra la linea orizzontale si muovono da sinistra a destra, mentre i bordi sotto la linea si muovono da destra a sinistra. Invece di aggiungere punti per i nodi, includo solo i nomi delle etichette. Uso la stessa larghezza estetica per denotare la differenza di peso di ogni bordo. Notate che in questo grafico uso un oggetto igraph come dati per il grafico, il che non fa alcuna differenza pratica.

ggraph(routes_igraph, layout = "linear") + geom_edge_arc(aes(width = weight), alpha = 0.8) + scale_edge_width(range = c(0.2, 2)) + geom_node_text(aes(label = label)) + labs(edge_width = "Letters") + theme_graph()

Grafi di rete interattivi con visNetwork e networkD3

La serie di pacchetti htmlwidgets rende possibile usare R per creare visualizzazioni interattive in JavaScript. Qui mostrerò come creare grafici con i pacchetti visNetwork e networkD3. Questi due pacchetti usano diverse librerie JavaScript per creare i loro grafici. visNetwork usa vis.js, mentre networkD3 usa la popolare libreria di visualizzazione d3 per creare i suoi grafici. Una difficoltà nel lavorare sia con visNetwork che con networkD3 è che si aspettano che le liste di bordi e di nodi usino una nomenclatura specifica. La manipolazione dei dati di cui sopra è conforme alla struttura di base per visNetwork, ma sarà necessario fare del lavoro per networkD3. Nonostante questo inconveniente, entrambi i pacchetti possiedono una vasta gamma di capacità grafiche ed entrambi possono lavorare con oggetti e layout igraph.

library(visNetwork)library(networkD3)

visNetwork

La funzione visNetwork() usa una lista di nodi e una di bordi per creare un grafico interattivo. La lista dei nodi deve includere una colonna “id”, e la lista dei bordi deve avere colonne “from” e “to”. La funzione traccia anche le etichette per i nodi, usando i nomi delle città dalla colonna “label” nella lista dei nodi. Il grafico risultante è divertente da giocare. Potete spostare i nodi e il grafico userà un algoritmo per mantenere i nodi correttamente distanziati. Puoi anche ingrandire e ridurre il grafico e spostarlo per ricentrarlo.

visNetwork(nodes, edges)

visNetwork può usare igraph layout, fornendo una grande varietà di possibili layout. Inoltre, puoi usare visIgraph() per tracciare direttamente un oggetto igraph. Qui, mi atterrò al flusso di lavoro nodes e edges e userò un layout igraph per personalizzare il grafico. Aggiungerò anche una variabile per cambiare la larghezza del bordo come abbiamo fatto con ggraph. visNetwork() usa i nomi delle colonne dagli elenchi dei bordi e dei nodi per tracciare gli attributi della rete invece degli argomenti all’interno della chiamata della funzione. Questo significa che è necessario fare qualche manipolazione dei dati per ottenere una colonna “larghezza” nella lista dei bordi. L’attributo di larghezza per visNetwork() non scala i valori, quindi dobbiamo farlo manualmente. Entrambe queste azioni possono essere fatte con la funzione mutate() e qualche semplice aritmetica. Qui, creo una nuova colonna in edges e scala i valori del peso dividendo per 5. Aggiungendo 1 al risultato si ottiene un modo per creare una larghezza minima.

edges <- mutate(edges, width = weight/5 + 1)

Una volta fatto questo, possiamo creare un grafico con larghezze di bordo variabili. Scelgo anche un algoritmo di layout da igraph e aggiungo frecce ai bordi, mettendole al centro del bordo.

visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")

networkD3

È necessario un po’ più di lavoro per preparare i dati per creare un grafico networkD3. Creare un grafico networkD3 con una lista di nodi e bordi richiede che gli ID siano una serie di numeri interi che iniziano con 0. Attualmente, gli ID dei nodi per i nostri dati iniziano con 1, e quindi dobbiamo fare un po’ di manipolazione dei dati. È possibile rinumerare i nodi sottraendo 1 dalle colonne ID nei frame di dati nodes e edges. Ancora una volta, questo può essere fatto con la funzione mutate(). L’obiettivo è quello di ricreare le colonne attuali, sottraendo 1 da ogni ID. La funzione mutate() funziona creando una nuova colonna, ma possiamo farle sostituire una colonna dando alla nuova colonna lo stesso nome della vecchia colonna. Qui, chiamo i nuovi data frame con un suffisso d3 per distinguerli dai precedenti nodes e edges data frame.

nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)

È ora possibile tracciare un grafico networkD3. A differenza di visNetwork(), la funzione forceNetwork() usa una serie di argomenti per regolare il grafico e tracciare gli attributi della rete. Gli argomenti “Links” e “Nodes” forniscono i dati per il grafico sotto forma di liste di bordi e nodi. La funzione richiede anche gli argomenti “NodeID” e “Group”. I dati usati qui non hanno alcun raggruppamento, e quindi ho solo fatto in modo che ogni nodo sia il proprio gruppo, il che in pratica significa che i nodi saranno tutti di colori diversi. Inoltre, il sotto dice alla funzione che la rete ha campi “Source” e “Target”, e quindi è diretta. Includo in questo grafico un “Valore”, che scala la larghezza dei bordi secondo la colonna “peso” nella lista dei bordi. Infine, aggiungo alcune modifiche estetiche per rendere i nodi opachi e aumento la dimensione dei caratteri delle etichette per migliorare la leggibilità. Il risultato è molto simile alla prima trama visNetwork() che ho creato, ma con stili estetici diversi.

forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)

Uno dei principali vantaggi di networkD3 è che implementa un diagramma di Sankey in stile d3. Un diagramma di Sankey si adatta bene alle lettere inviate a Daniel nel 1585. Non ci sono troppi nodi nei dati, rendendo più facile la visualizzazione del flusso di lettere. La creazione di un diagramma di Sankey usa la funzione sankeyNetwork(), che prende molti degli stessi argomenti di forceNetwork(). Questo grafico non richiede un argomento di gruppo, e l’unico altro cambiamento è l’aggiunta di una “unità”. Questo fornisce un’etichetta per i valori che appaiono in un tool tip quando il cursore passa sopra un elemento del diagramma.10

sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")

Altra lettura di Network Analysis

Questo post ha cercato di dare un’introduzione generale alla creazione e al tracciamento di oggetti di tipo network in R usando i pacchetti network, igraph, tidygraph, e ggraph per grafici statici e visNetwork e networkD3 per grafici interattivi. Ho presentato queste informazioni dalla posizione di un non specialista della teoria delle reti. Ho coperto solo una piccola percentuale delle capacità di analisi delle reti di R. In particolare, non ho discusso l’analisi statistica delle reti. Fortunatamente, c’è una pletora di risorse sull’analisi delle reti in generale e in R in particolare.

La migliore introduzione alle reti che ho trovato per i non iniziati è Network Visualization with R di Katya Ognyanova, che presenta sia un’utile introduzione agli aspetti visivi delle reti che un tutorial più approfondito sulla creazione di grafici di rete in R. Ognyanova usa principalmente igraph, ma introduce anche le reti interattive.

Ci sono due libri relativamente recenti pubblicati da Springer sull’analisi di rete con R. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) è un’introduzione molto utile all’analisi delle reti con R. Luke copre sia l’insieme di pacchetti statnet che igragh. I contenuti sono ad un livello molto accessibile. Più avanzato è Eric D. Kolaczyk e Gábor Csárdi, Statistical Analysis of Network Data with R (2014). Il libro di Kolaczyk e Csárdi usa principalmente igraph, poiché Csárdi è il principale manutentore del pacchetto igraph per R. Questo libro si addentra ulteriormente in argomenti avanzati sull’analisi statistica delle reti. Nonostante l’uso di un linguaggio molto tecnico, i primi quattro capitoli sono generalmente accessibili da un punto di vista non specialistico.

La lista curata da François Briatte è una buona panoramica di risorse sull’analisi delle reti in generale. Anche la serie di post Networks Demystified di Scott Weingart merita di essere consultata.

  1. Un esempio dell’interesse per la network analysis all’interno delle digital humanities è il Journal of Historical Network Research, lanciato di recente. ︎

  2. Per una buona descrizione della classe oggetto network, inclusa una discussione della sua relazione con la classe oggetto igraph, vedi Carter Butts, “network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎

  3. Questa è la struttura specifica attesa da visNetwork, mentre è anche conforme alle aspettative generali degli altri pacchetti. ︎

  4. Questo è l’ordine previsto per le colonne per alcuni dei pacchetti di rete che userò di seguito. ︎

  5. ungroup() non è strettamente necessario in questo caso. Tuttavia, se non si scompone il data frame, non è possibile eliminare le colonne “sorgente” e “destinazione”, come faccio più avanti nello script. ︎

  6. Thomas M. J. Fruchterman and Edward M. Reingold, “Graph Drawing by Force-Directed Placement,” Software: Practice and Experience, 21 (1991): 1129-1164. ︎

  7. La funzione rm() è utile se il tuo ambiente di lavoro in R diventa disorganizzato, ma non vuoi cancellare l’intero ambiente e ricominciare da capo. ︎

  8. La relazione tra gli oggetti tbl_graph e igraph è simile a quella tra gli oggetti tibble e data.frame. ︎

  9. È possibile far disegnare frecce a ggraph, ma non l’ho mostrato qui. ︎

  10. Può volerci un po’ di tempo perché la punta dello strumento appaia. ︎

r

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.