Introducción al análisis de redes con R
On noviembre 4, 2021 by adminEn una amplia gama de campos el análisis de redes se ha convertido en una herramienta cada vez más popular para que los estudiosos traten la complejidad de las interrelaciones entre actores de todo tipo. La promesa del análisis de redes consiste en dar importancia a las relaciones entre los actores, en lugar de considerarlos como entidades aisladas. El énfasis en la complejidad, junto con la creación de una variedad de algoritmos para medir diversos aspectos de las redes, hace que el análisis de redes sea una herramienta central para las humanidades digitales.1 Este post proporcionará una introducción al trabajo con redes en R, utilizando el ejemplo de la red de ciudades en la correspondencia de Daniel van der Meulen en 1585.
Hay una serie de aplicaciones diseñadas para el análisis de redes y la creación de gráficos de redes, como gephi y cytoscape. Aunque no está diseñado específicamente para ello, R se ha convertido en una potente herramienta para el análisis de redes. La fuerza de R en comparación con el software de análisis de redes independiente es triple. En primer lugar, R permite una investigación reproducible que no es posible con las aplicaciones GUI. En segundo lugar, la potencia de análisis de datos de R proporciona herramientas sólidas para manipular los datos y prepararlos para el análisis de redes. Por último, existe una gama cada vez mayor de paquetes diseñados para hacer de R una herramienta completa de análisis de redes. Entre los paquetes de análisis de redes más importantes para R se encuentran el conjunto de paquetes statnet y igraph
. Además, Thomas Lin Pedersen ha publicado recientemente los paquetes tidygraph
y ggraph
que aprovechan la potencia de igraph
de forma coherente con el flujo de trabajo de tidyverse. R también se puede utilizar para hacer gráficos de red interactivos con el marco htmlwidgets que traduce el código de R a JavaScript.
Este post comienza con una breve introducción al vocabulario básico del análisis de redes, seguido de una discusión del proceso para obtener datos en la estructura adecuada para el análisis de redes. Todos los paquetes de análisis de redes han implementado sus propias clases de objetos. En este post, mostraré cómo crear las clases de objeto específicas para el conjunto de paquetes statnet con el paquete network
, así como para igraph
y tidygraph
, que se basa en la implementación de igraph
. Por último, pasaré a la creación de gráficos interactivos con los paquetes vizNetwork
y networkD3
.
Análisis de redes: Nodos y Aristas
Los dos aspectos principales de las redes son una multitud de entidades separadas y las conexiones entre ellas. El vocabulario puede ser un poco técnico e incluso inconsistente entre diferentes disciplinas, paquetes y software. Las entidades se denominan nodos o vértices de un grafo, mientras que las conexiones son aristas o enlaces. En este post utilizaré principalmente la nomenclatura de nodos y aristas, excepto cuando se hable de paquetes que utilizan un vocabulario diferente.
Los paquetes de análisis de redes necesitan que los datos tengan una forma particular para crear el tipo de objeto especial que utiliza cada paquete. Las clases de objetos para network
, igraph
y tidygraph
se basan en matrices de adyacencia, también conocidas como sociomatrices.2 Una matriz de adyacencia es una matriz cuadrada en la que los nombres de las columnas y filas son los nodos de la red. Dentro de la matriz, un 1 indica que hay una conexión entre los nodos, y un 0 indica que no hay conexión. Las matrices de adyacencia implementan una estructura de datos muy diferente a la de los marcos de datos y no encajan dentro del flujo de trabajo tidyverse que he utilizado en mis posts anteriores. Afortunadamente, los objetos de red especializados también pueden crearse a partir de un marco de datos de lista de aristas, que sí encajan en el flujo de trabajo del tidyverse. En este post me ceñiré a las técnicas de análisis de datos del tidyverse para crear listas de aristas, que luego se convertirán en las clases de objeto específicas para network
, igraph
y tidygraph
.
Una lista de aristas es un marco de datos que contiene un mínimo de dos columnas, una columna de nodos que son el origen de una conexión y otra columna de nodos que son el destino de la conexión. Los nodos en los datos son identificados por IDs únicos. Si la distinción entre origen y destino es significativa, la red es dirigida. Si la distinción no tiene sentido, el grafo es no dirigido. En el ejemplo de las cartas enviadas entre ciudades, la distinción entre origen y destino es claramente significativa, por lo que el grafo es dirigido. En los ejemplos siguientes, llamaré a la columna de origen «desde» y a la de destino «hacia». Utilizaré números enteros que empiecen por uno como ID de los nodos.3 Una lista de aristas también puede contener columnas adicionales que describan los atributos de las aristas, como el aspecto de la magnitud de una arista. Si las aristas tienen un atributo de magnitud el grafo se considera ponderado.
Las listas de aristas contienen toda la información necesaria para crear objetos de grafo, pero a veces es preferible crear también una lista de nodos separada. En su forma más simple, una lista de nodos es un marco de datos con una sola columna -que etiquetaré como «id»- que enumera los ID de los nodos encontrados en la lista de aristas. La ventaja de crear una lista de nodos separada es la posibilidad de añadir columnas de atributos al marco de datos, como los nombres de los nodos o cualquier tipo de agrupación. A continuación doy un ejemplo de listas mínimas de aristas y nodos creadas con la función 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
Comparar esto con una matriz de adyacencia con los mismos datos.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Crear listas de aristas y nodos
Para crear objetos de red a partir de la base de datos de cartas recibidas por Daniel van der Meulen en 1585 haré tanto una lista de aristas como una lista de nodos. Para ello será necesario utilizar el paquete dplyr para manipular el marco de datos de las cartas enviadas a Daniel y dividirlo en dos marcos de datos o tibbles con la estructura de listas de aristas y nodos. En este caso, los nodos serán las ciudades desde las que los corresponsales de Daniel le enviaron cartas y las ciudades en las que las recibió. La lista de nodos contendrá una columna «etiqueta» con los nombres de las ciudades. La lista de bordes también tendrá una columna de atributos que mostrará la cantidad de cartas enviadas entre cada par de ciudades. El flujo de trabajo para crear estos objetos será similar al que he utilizado en mi breve introducción a R y en la geocodificación con R. Si quieres seguirlo, puedes encontrar los datos utilizados en este post y el script de R utilizado en GitHub.
El primer paso es cargar la librería tidyverse
para importar y manipular los datos. Al imprimir el marco de datos letters
se observa que contiene cuatro columnas: «escritor», «fuente», «destino» y «fecha». En este ejemplo, sólo trataremos las columnas «origen» y «destino».
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 de nodos
El flujo de trabajo para crear una lista de nodos es similar al que utilicé para obtener la lista de ciudades con el fin de geocodificar los datos en un post anterior. Queremos obtener las ciudades distintas de las columnas «origen» y «destino» y luego unir la información de estas columnas. En el ejemplo siguiente, cambio ligeramente los comandos de los que utilicé en el post anterior para que el nombre de las columnas con los nombres de las ciudades sea el mismo para los marcos de datos sources
y destinations
para simplificar la función full_join()
. Renombro la columna con los nombres de las ciudades como «label» para adoptar el vocabulario utilizado por los paquetes de análisis de redes.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
Para crear un único marco de datos con una columna con los lugares únicos necesitamos utilizar un full join, porque queremos incluir todos los lugares únicos tanto de las fuentes de las cartas como de los destinos.
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
Esto da como resultado un marco de datos con una variable. Sin embargo, la variable contenida en el marco de datos no es realmente lo que estamos buscando. La columna «label» contiene los nombres de los nodos, pero también queremos tener identificadores únicos para cada ciudad. Podemos hacerlo añadiendo una columna «id» al marco de datos nodes
que contenga números del uno al número total de filas del marco de datos. Una función útil para este flujo de trabajo es rowid_to_column()
, que añade una columna con los valores de los ID de las filas y coloca la columna al principio del marco de datos.4 Tenga en cuenta que rowid_to_column()
es un comando canalizable, por lo que es posible hacer el full_join()
y añadir la columna «id» en un solo comando. El resultado es una lista de nodos con una columna «id» y un atributo «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 de aristas
Crear una lista de aristas es similar a lo anterior, pero se complica por la necesidad de tratar con dos columnas «id» en lugar de una. También queremos crear una columna de peso que anotará la cantidad de letras enviadas entre cada conjunto de nodos. Para lograr esto voy a utilizar el mismo flujo de trabajo group_by()
y summarise()
que he discutido en posts anteriores. La diferencia aquí es que queremos agrupar el marco de datos por dos columnas – «origen» y «destino»- en lugar de una sola. Anteriormente, he llamado a la columna que cuenta el número de observaciones por grupo «recuento», pero aquí adopto la nomenclatura del análisis de redes y la llamo «peso». El último comando de la tubería elimina la agrupación del marco de datos instituida por la función group_by()
. Esto facilita la manipulación del marco de datos resultante per_route
sin obstáculos.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
Al igual que la lista de nodos, per_route
tiene ahora la forma básica que deseamos, pero volvemos a tener el problema de que las columnas «origen» y «destino» contienen etiquetas en lugar de identificadores. Lo que tenemos que hacer es vincular los IDs que se han asignado en nodes
a cada ubicación en las columnas «origen» y «destino». Esto se puede lograr con otra función join. De hecho, es necesario realizar dos joins, uno para la columna «origen» y otro para «destino». En este caso, utilizaré un left_join()
con per_route
como marco de datos izquierdo, porque queremos mantener el número de filas en per_route
. Al hacer el left_join
, también queremos renombrar las dos columnas «id» que se traen de nodes
. Para la unión utilizando la columna «origen», cambiaré el nombre de la columna como «desde». La columna traída desde la unión «destino» se renombra como «a». Sería posible hacer ambas uniones en un solo comando con el uso de la tubería. Sin embargo, para mayor claridad, realizaré las uniones en dos comandos separados. Debido a que la unión se realiza a través de dos comandos, observe que el marco de datos al principio de la tubería cambia de per_route
a edges
, que es creado por el primer 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)
Ahora que edges
tiene columnas «desde» y «hacia» con IDs de nodos, necesitamos reordenar las columnas para llevar «desde» y «hacia» a la izquierda del marco de datos. Actualmente, el marco de datos edges
sigue conteniendo las columnas «origen» y «destino» con los nombres de las ciudades que se corresponden con los ID. Sin embargo, estos datos son superfluos, puesto que ya están presentes en nodes
. Por lo tanto, sólo incluiré las columnas «origen», «destino» y «peso» en la función 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
El marco de datos edges
no parece muy impresionante; son tres columnas de números enteros. Sin embargo, edges
combinado con nodes
nos proporciona toda la información necesaria para crear objetos de red con los paquetes network
, igraph
y tidygraph
.
Creación de objetos de red
Las clases de objetos de red para network
, igraph
y tidygraph
están muy relacionadas. Es posible traducir entre un objeto network
y un objeto igraph
. Sin embargo, es mejor mantener los dos paquetes y sus objetos separados. De hecho, las capacidades de network
y igraph
se solapan hasta tal punto que es mejor tener cargado sólo uno de los paquetes a la vez. Comenzaré revisando el paquete network
y luego pasaré a los paquetes igraph
y tidygraph
.
red
library(network)
La función utilizada para crear un objeto network
es network()
. El comando no es especialmente sencillo, pero siempre puedes introducir ?network()
en la consola si te confundes. El primer argumento es -como se indica en la documentación- «una matriz que da la estructura de la red en forma de adyacencia, incidencia o lista de bordes». El lenguaje demuestra la importancia de las matrices en el análisis de redes, pero en lugar de una matriz, tenemos una lista de aristas, que cumple el mismo papel. El segundo argumento es una lista de atributos de vértices, que corresponde a la lista de nodos. Observe que el paquete network
utiliza la nomenclatura de vértices en lugar de la de nodos. Lo mismo ocurre con igraph
. A continuación, tenemos que especificar el tipo de datos que se ha introducido en los dos primeros argumentos especificando que el matrix.type
es un "edgelist"
. Por último, establecemos ignore.eval
en FALSE
para que nuestra red pueda ser ponderada y tener en cuenta el número de letras a lo largo de cada ruta.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
Podemos ver el tipo de objeto creado por la función network()
colocando routes_network
en la función class()
.
class(routes_network)#> "network"
La impresión de routes_network
en la consola muestra que la estructura del objeto es bastante diferente de los objetos de estilo data-frame como edges
y nodes
. El comando de impresión revela información definida específicamente para el análisis de redes. Muestra que hay 13 vértices o nodos y 15 aristas en routes_network
. Estos números corresponden al número de filas en nodes
y edges
respectivamente. También podemos ver que tanto los vértices como las aristas contienen atributos como la etiqueta y el peso. Se puede obtener aún más información, incluyendo una sociomatriz de los datos, introduciendo 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
Ahora es posible obtener un gráfico rudimentario, aunque no demasiado agradable estéticamente, de nuestra red de letras. Tanto el paquete network
como el igraph
utilizan el sistema de trazado de base de R. Las convenciones para los trazados de base son significativamente diferentes de las de ggplot2 -de las que he hablado en entradas anteriores- y por ello me ceñiré a trazados más bien sencillos en lugar de entrar en los detalles de la creación de trazados complejos con R de base. En este caso, el único cambio que hago a la función plot()
por defecto del paquete network
es aumentar el tamaño de los nodos con el argumento vertex.cex
para hacerlos más visibles. Incluso con este gráfico tan sencillo, ya podemos aprender algo sobre los datos. El gráfico deja claro que hay dos agrupaciones o clusters principales de los datos, que corresponden al tiempo que Daniel pasó en Holanda en los tres primeros cuartos de 1585 y después de su traslado a Bremen en septiembre.
plot(routes_network, vertex.cex = 3)
La función plot()
con un objeto network
utiliza el algoritmo de Fruchterman y Reingold para decidir la colocación de los nodos.6 Puedes cambiar el algoritmo de disposición con el argumento mode
. A continuación, he colocado los nodos en un círculo. No es una disposición especialmente útil para este grafo, pero da una idea de algunas de las opciones disponibles.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Pasemos ahora a discutir el paquete igraph
. En primer lugar, tenemos que limpiar el entorno en R eliminando el paquete network
para que no interfiera con los comandos igraph
. También podríamos eliminar routes_network
ya que no lo vamos a utilizar más. El paquete network
se puede eliminar con la función detach()
, y routes_network
se elimina con rm()
.7 Después de esto, podemos cargar con seguridad igraph
.
detach(package:network)rm(routes_network)library(igraph)
Para crear un objeto igraph
a partir de un marco de datos edge-list podemos utilizar la función graph_from_data_frame()
, que es un poco más directa que network()
. Hay tres argumentos en la función graph_from_data_frame()
: d, vértices y dirigido. Aquí, d se refiere a la lista de aristas, vértices a la lista de nodos, y dirigido puede ser TRUE
o FALSE
dependiendo de si los datos son dirigidos o no dirigidos.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Imprimir el objeto igraph
creado por graph_from_data_frame()
en la consola revela información similar a la de un objeto network
, aunque la estructura es más críptica.
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
La información principal sobre el objeto está contenida en DNW- 13 15 --
. Esto nos dice que routes_igraph
es un grafo dirigido (D) que tiene un atributo nombre (N) y está ponderado (W). El guión después de W nos dice que el grafo no es bipartito. Los números que siguen describen el número de nodos y aristas del grafo, respectivamente. A continuación, name (v/c), label (v/c), weight (e/n)
da información sobre los atributos del grafo. Hay dos atributos de vértice (v/c) de nombre -que son los ID- y etiquetas y un atributo de arista (e/n) de peso. Por último, hay una impresión de todas las aristas.
Al igual que con el paquete network
, podemos crear un gráfico con un objeto igraph
a través de la función plot()
. El único cambio que hago por defecto aquí es disminuir el tamaño de las flechas. Por defecto igraph
etiqueta los nodos con la columna label si la hay o con los IDs.
plot(routes_igraph, edge.arrow.size = 0.2)
Al igual que el gráfico network
anterior, el valor por defecto de un gráfico igraph
no es particularmente agradable estéticamente, pero todos los aspectos de los gráficos pueden ser manipulados. Aquí, sólo quiero cambiar la disposición de los nodos para utilizar el algoritmo graphopt creado por Michael Schmuhl. Este algoritmo hace que sea más fácil ver la relación entre Haarlem, Amberes y Delft, que son tres de los lugares más significativos de la red de correspondencia, extendiéndolos más.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph y ggraph
Los paquetes tidygraph
y ggraph
son recién llegados al panorama del análisis de redes, pero juntos los dos paquetes proporcionan ventajas reales sobre los paquetes network
y igraph
. tidygraph
y ggraph
representan un intento de introducir el análisis de redes en el flujo de trabajo del tidyverse. tidygraph
proporciona una forma de crear un objeto de red que se asemeja más a un tibble o marco de datos. Esto hace posible el uso de muchas de las funciones de dplyr
para manipular los datos de la red. ggraph
ofrece una manera de trazar gráficos de red utilizando las convenciones y la potencia de ggplot2
. En otras palabras, tidygraph
y ggraph
le permiten tratar con objetos de red de una manera que es más consistente con los comandos utilizados para trabajar con tibbles y marcos de datos. Sin embargo, la verdadera promesa de tidygraph
y ggraph
es que aprovechan la potencia de igraph
. Esto significa que se sacrifican pocas de las capacidades de análisis de redes de igraph
al utilizar tidygraph
y ggraph
.
Necesitamos empezar como siempre cargando los paquetes necesarios.
library(tidygraph)library(ggraph)
Primero, vamos a crear un objeto de red utilizando tidygraph
, que se llama un tbl_graph
. Un tbl_graph
consiste en dos tibbles: un tibble de aristas y un tibble de nodos. Convenientemente, la clase de objeto tbl_graph
es una envoltura de un objeto igraph
, lo que significa que en su base un objeto tbl_graph
es esencialmente un objeto igraph
.8 El estrecho vínculo entre los objetos tbl_graph
y igraph
da lugar a dos formas principales de crear un objeto tbl_graph
. La primera es utilizar una lista de aristas y una lista de nodos, utilizando tbl_graph()
. Los argumentos de la función son casi idénticos a los de graph_from_data_frame()
con sólo un ligero cambio en los nombres de los argumentos.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
La segunda forma de crear un objeto tbl_graph
es convertir un objeto igraph
o network
utilizando as_tbl_graph()
. Así, podríamos convertir routes_igraph
en un objeto tbl_graph
.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Ahora que hemos creado dos objetos tbl_graph
, vamos a inspeccionarlos con la función class()
. Esto muestra que routes_tidy
y routes_igraph_tidy
son objetos de la clase "tbl_graph" "igraph"
, mientras que routes_igraph
es objeto de la clase "igraph"
.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Al imprimir un objeto tbl_graph
en la consola se obtiene una salida drásticamente diferente a la de un objeto igraph
. Es una salida similar a la de un 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
La impresión de routes_tidy
muestra que es un objeto tbl_graph
con 13 nodos y 15 aristas. El comando también imprime las seis primeras filas de «Datos de nodos» y las tres primeras de «Datos de aristas». Observe también que indica que los Datos del Nodo están activos. La noción de un tibble activo dentro de un objeto tbl_graph
permite manipular los datos en un tibble a la vez. El tibble de los nodos está activado por defecto, pero se puede cambiar qué tibble está activo con la función activate()
. Así, si quisiera reordenar las filas del tríbulo de aristas para listar primero las de mayor «peso», podría usar activate()
y luego arrange()
. Aquí simplemente imprimo el resultado en lugar de guardarlo.
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
Como no necesitamos manipular más routes_tidy
, podemos trazar el gráfico con ggraph
. Al igual que ggmap, ggraph
es una extensión de ggplot2
, por lo que es más fácil de llevar a cabo las habilidades básicas de ggplot
para la creación de gráficos de red. Como en todos los gráficos de red, hay tres aspectos principales en un gráfico de ggraph
: nodos, aristas y diseños. Las viñetas del paquete ggraph cubren de forma útil los aspectos fundamentales de los gráficos ggraph
. ggraph
añade geoms especiales al conjunto básico de geoms ggplot
que están específicamente diseñados para redes. Así, hay un conjunto de geom_node
y geom_edge
geoms. La función básica de trazado es ggraph()
, que toma los datos que se van a utilizar para el gráfico y el tipo de diseño deseado. Ambos argumentos para ggraph()
se construyen alrededor de igraph
. Por lo tanto, ggraph()
puede utilizar tanto un objeto igraph
como un objeto tbl_graph
. Además, los algoritmos de diseño disponibles se derivan principalmente de igraph
. Por último, ggraph
introduce un tema especial ggplot
que proporciona mejores valores predeterminados para los gráficos de red que los valores predeterminados normales de ggplot
. El tema ggraph
puede establecerse para una serie de gráficos con el comando set_graph_style()
que se ejecuta antes de trazar los gráficos o utilizando theme_graph()
en los gráficos individuales. En este caso, utilizaré este último método.
Veamos el aspecto de un gráfico básico ggraph
. El gráfico comienza con ggraph()
y los datos. A continuación, añado geoms básicos de aristas y nodos. No se necesitan argumentos dentro de los geoms de borde y nodo, porque toman la información de los datos proporcionados en ggraph()
.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Como puede ver, la estructura del comando es similar a la de ggplot
con las capas separadas añadidas con el signo +
. El gráfico básico de ggraph
es similar a los de network
y igraph
, si no más sencillo, pero podemos utilizar comandos similares a los de ggplot
para crear un gráfico más informativo. Podemos mostrar el «peso» de las aristas -o la cantidad de cartas enviadas a lo largo de cada ruta- utilizando el ancho en la función geom_edge_link()
. Para conseguir que el ancho de la línea cambie según la variable peso, colocamos el argumento dentro de una función aes()
. Para controlar el ancho máximo y mínimo de los bordes, utilizo scale_edge_width()
y coloco un range
. Elijo una anchura relativamente pequeña para el mínimo, porque hay una diferencia significativa entre el número máximo y mínimo de letras enviadas a lo largo de las rutas. También podemos etiquetar los nodos con los nombres de las localidades ya que hay relativamente pocos nodos. Convenientemente, geom_node_text()
viene con un argumento de repelencia que asegura que las etiquetas no se solapen con los nodos de forma similar al paquete ggrepel. Agrego un poco de transparencia a los bordes con el argumento alpha. También uso labs()
para reetiquetar la leyenda «Letras».
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()
Además de las opciones de diseño proporcionadas por igraph
, ggraph
también implementa sus propios diseños. Por ejemplo, puede utilizar el concepto de circularidad de ggraph's
para crear diagramas de arco. En este caso, he dispuesto los nodos en una línea horizontal y he dibujado los bordes como arcos. A diferencia del diagrama anterior, este gráfico indica la direccionalidad de las aristas.9 Las aristas por encima de la línea horizontal se mueven de izquierda a derecha, mientras que las aristas por debajo de la línea se mueven de derecha a izquierda. En lugar de añadir puntos para los nodos, sólo incluyo los nombres de las etiquetas. Utilizo la misma anchura estética para denotar la diferencia en el peso de cada arista. Tenga en cuenta que en este gráfico utilizo un objeto igraph
como los datos para el gráfico, que no hace ninguna diferencia práctica.
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()
Gráficos de red interactivos con visNetwork y networkD3
El conjunto de paquetes htmlwidgets hace posible el uso de R para crear visualizaciones interactivas de JavaScript. Aquí mostraré cómo hacer gráficos con los paquetes visNetwork
y networkD3
. Estos dos paquetes utilizan diferentes bibliotecas de JavaScript para crear sus gráficos. visNetwork
utiliza vis.js, mientras que networkD3
utiliza la popular biblioteca de visualización d3 para hacer sus gráficos. Una de las dificultades de trabajar tanto con visNetwork
como con networkD3
es que esperan que las listas de aristas y las listas de nodos utilicen una nomenclatura específica. La manipulación de datos anterior se ajusta a la estructura básica para visNetwork
, pero habrá que trabajar un poco para networkD3
. A pesar de este inconveniente, ambos paquetes poseen una amplia gama de capacidades de graficación y ambos pueden trabajar con objetos y diseños de igraph
.
library(visNetwork)library(networkD3)
visNetwork
La función visNetwork()
utiliza una lista de nodos y una lista de aristas para crear un gráfico interactivo. La lista de nodos debe incluir una columna «id», y la lista de aristas debe tener columnas «desde» y «hasta». La función también traza las etiquetas de los nodos, utilizando los nombres de las ciudades de la columna «label» de la lista de nodos. Es divertido jugar con el gráfico resultante. Puedes mover los nodos y el gráfico utilizará un algoritmo para mantener los nodos correctamente espaciados. También puede acercar y alejar el gráfico y moverlo para volver a centrarlo.
visNetwork(nodes, edges)
visNetwork
puede utilizar igraph
diseños, proporcionando una gran variedad de diseños posibles. Además, puede utilizar visIgraph()
para trazar un objeto igraph
directamente. Aquí, me quedaré con el flujo de trabajo nodes
y edges
y utilizaré un diseño igraph
para personalizar el gráfico. También añadiré una variable para cambiar el ancho del borde como hicimos con ggraph
. visNetwork()
utiliza los nombres de las columnas de las listas de aristas y nodos para trazar los atributos del grafo en lugar de los argumentos dentro de la llamada a la función. Esto significa que es necesario hacer alguna manipulación de datos para obtener una columna de «anchura» en la lista de aristas. El atributo de anchura para visNetwork()
no escala los valores, así que tenemos que hacerlo manualmente. Ambas acciones se pueden realizar con la función mutate()
y algo de aritmética simple. En este caso, creo una nueva columna en edges
y escalo los valores de peso dividiéndolos por 5. Sumando 1 al resultado se obtiene una forma de crear un ancho mínimo.
edges <- mutate(edges, width = weight/5 + 1)
Una vez hecho esto, podemos crear un gráfico con anchos de arista variables. También elijo un algoritmo de diseño de igraph
y añado flechas a las aristas, colocándolas en el centro de la arista.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
redD3
Un poco más de trabajo es necesario para preparar los datos para crear un gráfico networkD3
. Hacer un gráfico networkD3
con una lista de aristas y nodos requiere que los IDs sean una serie de enteros numéricos que empiecen por 0. Actualmente, los IDs de los nodos de nuestros datos empiezan por 1, por lo que tenemos que hacer un poco de manipulación de los datos. Es posible renumerar los nodos restando 1 a las columnas de ID en los marcos de datos nodes
y edges
. Una vez más, esto se puede hacer con la función mutate()
. El objetivo es recrear las columnas actuales, mientras se resta 1 de cada ID. La función mutate()
funciona creando una nueva columna, pero podemos hacer que reemplace una columna dándole a la nueva columna el mismo nombre que la antigua. Aquí, nombro los nuevos marcos de datos con un sufijo d3 para distinguirlos de los marcos de datos anteriores nodes
y edges
.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Ahora es posible trazar un gráfico networkD3
. A diferencia de visNetwork()
, la función forceNetwork()
utiliza una serie de argumentos para ajustar el gráfico y trazar los atributos de la red. Los argumentos «Links» y «Nodes» proporcionan los datos para el trazado en forma de listas de aristas y nodos. La función también requiere los argumentos «NodeID» y «Group». Los datos que se utilizan aquí no tienen ninguna agrupación, por lo que cada nodo es su propio grupo, lo que en la práctica significa que todos los nodos serán de diferentes colores. Además, lo que sigue indica a la función que el grafo tiene campos «Origen» y «Destino», y por tanto es dirigido. Incluyo en este gráfico un «Valor», que escala el ancho de las aristas según la columna «peso» de la lista de aristas. Por último, añado algunos retoques estéticos para que los nodos sean opacos y aumento el tamaño de la letra de las etiquetas para mejorar la legibilidad. El resultado es muy similar al primer diagrama visNetwork()
que creé pero con diferentes estilos estéticos.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
Una de las principales ventajas de networkD3
es que implementa un diagrama Sankey de estilo d3. Un diagrama de Sankey se ajusta bien a las cartas enviadas a Daniel en 1585. No hay demasiados nodos en los datos, lo que facilita la visualización del flujo de cartas. La creación de un diagrama de Sankey utiliza la función sankeyNetwork()
, que toma muchos de los mismos argumentos que forceNetwork()
. Este gráfico no requiere un argumento de grupo, y el único otro cambio es la adición de una «unidad». Esto proporciona una etiqueta para los valores que aparecen en una punta de la herramienta cuando el cursor pasa por encima de un elemento del diagrama.10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Lectura adicional sobre el análisis de redes
Este post ha tratado de dar una introducción general a la creación y el trazado de objetos de tipo de red en R utilizando los paquetes network
, igraph
, tidygraph
, y ggraph
para los gráficos estáticos y visNetwork
y networkD3
para los gráficos interactivos. He presentado esta información desde la posición de un no especialista en teoría de redes. Sólo he cubierto un porcentaje muy pequeño de las capacidades de análisis de redes de R. En particular, no he discutido el análisis estadístico de redes. Afortunadamente, hay una plétora de recursos sobre el análisis de redes en general y en R en particular.
La mejor introducción a las redes que he encontrado para los no iniciados es Network Visualization with R de Katya Ognyanova. Esto presenta tanto una introducción útil a los aspectos visuales de las redes como un tutorial más profundo sobre la creación de gráficos de redes en R. Ognyanova utiliza principalmente igraph
, pero también introduce las redes interactivas.
Hay dos libros relativamente recientes publicados sobre el análisis de redes con R por Springer. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) es una introducción muy útil al análisis de redes con R. Luke cubre tanto el conjunto de paquetes statnet como igragh
. El contenido está a un nivel muy accesible en todo momento. Más avanzado es el libro de Eric D. Kolaczyk y Gábor Csárdi, Statistical Analysis of Network Data with R (2014). El libro de Kolaczyk y Csárdi utiliza principalmente igraph
, ya que Csárdi es el principal mantenedor del paquete igraph
para R. Este libro se adentra más en temas avanzados sobre el análisis estadístico de redes. A pesar del uso de un lenguaje muy técnico, los cuatro primeros capítulos son generalmente accesibles desde un punto de vista no especializado.
La lista comisariada por François Briatte es un buen resumen de recursos sobre el análisis de redes en general. La serie de posts Networks Demystified de Scott Weingart también merece la pena.
-
Un ejemplo del interés por el análisis de redes dentro de las humanidades digitales es el recién lanzado Journal of Historical Network Research. ︎
-
Para una buena descripción de la clase de objeto
network
, incluyendo una discusión de su relación con la clase de objetoigraph
, véase Carter Butts, «network: A Package for Managing Relational Data in R», Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Esta es la estructura específica esperada por
visNetwork
, al tiempo que se ajusta a las expectativas generales de los otros paquetes. ︎ -
Este es el orden esperado para las columnas de algunos de los paquetes de red que utilizaré a continuación. ︎
-
ungroup()
no es estrictamente necesario en este caso. Sin embargo, si no se desagrupa el marco de datos, no es posible eliminar las columnas «origen» y «destino», como hago más adelante en el script. ︎ -
Thomas M. J. Fruchterman y Edward M. Reingold, «Graph Drawing by Force-Directed Placement», Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
La función
rm()
es útil si su entorno de trabajo en R se desorganiza, pero no quiere borrar todo el entorno y empezar de nuevo. ︎ -
La relación entre los objetos
tbl_graph
yigraph
es similar a la que existe entre los objetostibble
ydata.frame
. ︎ -
Es posible hacer que
ggraph
dibuje flechas, pero no lo he mostrado aquí. ︎ -
La punta de la herramienta puede tardar un poco en aparecer. ︎
Deja una respuesta