Introdução à Análise de Redes com R
On Novembro 4, 2021 by adminAcima de uma ampla gama de campos, a análise de redes tornou-se uma ferramenta cada vez mais popular para os estudiosos lidarem com a complexidade das inter-relações entre atores de todos os tipos. A promessa da análise de rede é a colocação de significado nas relações entre os actores, em vez de ver os actores como entidades isoladas. A ênfase na complexidade, juntamente com a criação de uma variedade de algoritmos para medir vários aspectos das redes, faz da análise de rede uma ferramenta central para as humanidades digitais.1 Este post fornecerá uma introdução ao trabalho com redes em R, usando o exemplo da rede de cidades na correspondência de Daniel van der Meulen em 1585.
Existem várias aplicações projetadas para a análise de rede e a criação de gráficos de rede, como gephi e cytoscape. Apesar de não ter sido especificamente projetado para isso, R se tornou uma ferramenta poderosa para análise de rede. A força do R em comparação com softwares de análise de rede autônomos é três vezes maior. Em primeiro lugar, R permite uma pesquisa reprodutível que não é possível com aplicações GUI. Em segundo lugar, o poder de análise de dados de R fornece ferramentas robustas para manipulação de dados para prepará-los para análise de rede. Finalmente, há uma gama sempre crescente de pacotes desenhados para fazer do R uma ferramenta completa de análise de rede. Pacotes significativos de análise de rede para R incluem o conjunto de pacotes statnet e igraph
. Além disso, Thomas Lin Pedersen lançou recentemente os pacotes tidygraph
e ggraph
que aproveitam o poder de igraph
de uma forma consistente com o fluxo de trabalho arrumado. R também pode ser usado para fazer gráficos de rede interativos com o framework htmlwidgets que traduz o código R para JavaScript.
Este post começa com uma breve introdução ao vocabulário básico de análise de rede, seguida por uma discussão do processo de entrada de dados na estrutura adequada para análise de rede. Os pacotes de análise de rede têm todos implementados suas próprias classes de objetos. Neste post, vou mostrar como criar as classes objeto específicas para o conjunto de pacotes statnet com o pacote network
, assim como para igraph
e tidygraph
, que é baseado na implementação de igraph
. Finalmente, vou voltar à criação de gráficos interativos com os pacotes vizNetwork
e networkD3
: Nós e Edges
Os dois aspectos primários das redes são uma multiplicidade de entidades separadas e as conexões entre elas. O vocabulário pode ser um pouco técnico e até mesmo inconsistente entre diferentes disciplinas, pacotes e software. As entidades são referidas como nós ou vértices de um gráfico, enquanto as conexões são bordas ou links. Neste post vou usar principalmente a nomenclatura de nós e bordas, exceto ao discutir pacotes que usam vocabulário diferente.
Os pacotes de análise de rede precisam de dados em uma forma particular para criar o tipo especial de objeto usado por cada pacote. As classes de objetos para network
, igraph
, e tidygraph
são todas baseadas em matrizes adjacentes, também conhecidas como sociomatrices.2 Uma matriz adjacente é uma matriz quadrada na qual os nomes das colunas e linhas são os nós da rede. Dentro da matriz um 1 indica que há uma conexão entre os nós, e um 0 indica que não há conexão. As matrizes de adjacência implementam uma estrutura de dados muito diferente dos quadros de dados e não se encaixam no fluxo de trabalho arrumado que eu usei nos meus posts anteriores. De forma útil, os objetos de rede especializados também podem ser criados a partir de um quadro de dados de uma lista de borda, que se encaixam no fluxo de trabalho arrumado. Neste post vou me ater às técnicas de análise de dados do tidyverse para criar listas de borda, que serão então convertidas para as classes de objetos específicos para network
, igraph
, e tidygraph
.
Uma lista de borda é um quadro de dados que contém um mínimo de duas colunas, uma coluna de nós que são a fonte de uma conexão e outra coluna de nós que são o alvo da conexão. Os nós nos dados são identificados por identificações únicas. Se a distinção entre origem e destino for significativa, a rede é direcionada. Se a distinção não for significativa, a rede é não direcionada. Com o exemplo de cartas enviadas entre cidades, a distinção entre fonte e alvo é claramente significativa, e assim a rede é direcionada. Para os exemplos abaixo, vou nomear a coluna da fonte como “de” e a coluna do destino como “para”. Vou usar inteiros começando com um como IDs de nó.3 Uma lista de bordas também pode conter colunas adicionais que descrevem atributos das bordas, como um aspecto de magnitude para uma borda. Se as arestas tiverem um atributo de magnitude o gráfico é considerado ponderado.
Edge lists contém todas as informações necessárias para criar objetos de rede, mas às vezes é preferível criar também uma lista de nós separada. Na sua forma mais simples, uma lista de nós é um quadro de dados com uma única coluna – que eu vou rotular como “id” – que lista os IDs dos nós encontrados na lista de borda. A vantagem de criar uma lista de nós separada é a capacidade de adicionar colunas de atributos ao quadro de dados, como os nomes dos nós ou qualquer tipo de agrupamento. Abaixo eu dou um exemplo de listas mínimas de bordas e nós criados com a função 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 isto com uma matriz de adjacências com os mesmos dados.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Criar listas de bordas e nós
Criar objetos de rede a partir do banco de dados de cartas recebidas por Daniel van der Meulen em 1585 Eu farei tanto uma lista de bordas quanto uma lista de nós. Isto exigirá o uso do pacote dplyr para manipular o quadro de dados de cartas enviadas a Daniel e dividi-lo em dois quadros de dados ou tibbles com a estrutura de edge e node lists. Neste caso, os nós serão as cidades das quais os correspondentes de Daniel lhe enviaram cartas e as cidades nas quais ele as recebeu. A lista de nós conterá uma coluna “etiqueta”, contendo os nomes das cidades. A lista de bordas também terá uma coluna de atributos que mostrará a quantidade de cartas enviadas entre cada par de cidades. O workflow para criar estes objetos será similar ao que usei na minha breve introdução ao R e na geocodificação com R. Se você quiser acompanhar, você pode encontrar os dados usados neste post e o script R usado no GitHub.
O primeiro passo é carregar a biblioteca tidyverse
para importar e manipular os dados. Imprimir o quadro de dados letters
mostra que ele contém quatro colunas: “escritor”, “fonte”, “destino”, e “data”. Neste exemplo, vamos lidar apenas com as colunas “source” e “destination”.
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
Node list
O fluxo de trabalho para criar uma lista de nós é similar ao que eu usei para obter a lista de cidades para geocodificar os dados em um post anterior. Nós queremos obter as cidades distintas das colunas “origem” e “destino” e depois juntar as informações dessas colunas. No exemplo abaixo, altero ligeiramente os comandos daqueles que usei no post anterior para que o nome das colunas com os nomes das cidades seja o mesmo tanto para as colunas sources
como destinations
quadros de dados para simplificar a função full_join()
. Eu renomeio a coluna com os nomes das cidades como “label” para adotar o vocabulário usado pelos pacotes de análise de rede.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
Para criar um único dataframe com uma coluna com as localizações únicas que precisamos para usar um join completo, porque queremos incluir todos os lugares únicos tanto das fontes das letras como dos 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
Isso resulta em um quadro de dados com uma variável. Entretanto, a variável contida no quadro de dados não é realmente o que estamos procurando. A coluna “etiqueta” contém os nomes dos nós, mas nós também queremos ter IDs únicos para cada cidade. Podemos fazer isso adicionando uma coluna “id” ao quadro de dados nodes
que contém números de um a qualquer número total de linhas no quadro de dados. Uma função útil para este fluxo de trabalho é rowid_to_column()
, que adiciona uma coluna com os valores dos ids de linha e coloca a coluna no início do quadro de dados.4 Note que rowid_to_column()
é um comando “pipeable”, e assim é possível fazer o full_join()
e adicionar a coluna “id” em um único comando. O resultado é uma lista de nós com uma coluna ID e um atributo de etiqueta.
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
Edge list
Criar uma lista de borda é similar ao acima, mas é complicado pela necessidade de lidar com duas colunas ID em vez de uma. Também queremos criar uma coluna de peso que anotará a quantidade de cartas enviadas entre cada conjunto de nós. Para isso vou usar o mesmo fluxo de trabalho group_by()
e summarise()
que já discuti em posts anteriores. A diferença aqui é que queremos agrupar o quadro de dados por duas colunas – “origem” e “destino” – em vez de apenas uma. Anteriormente, chamei a coluna que conta o número de observações por grupo de “contagem”, mas aqui adoto a nomenclatura de análise de rede e a chamo de “peso”. O comando final no pipeline remove o agrupamento para o quadro de dados instituído pela função group_by()
. Isto facilita a manipulação do quadro de dados resultante per_route
sem 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
Like a lista de nós, per_route
agora tem a forma básica que queremos, mas temos novamente o problema de que as colunas “origem” e “destino” contêm etiquetas em vez de IDs. O que precisamos fazer é ligar os IDs que foram atribuídos em nodes
a cada local em ambas as colunas “origem” e “destino”. Isto pode ser feito com outra função de junção. Na verdade, é necessário realizar duas junções, uma para a coluna “origem” e outra para a coluna “destino”. Neste caso, vou usar um left_join()
com per_route
como quadro de dados à esquerda, pois queremos manter o número de linhas em per_route
. Enquanto fazemos o left_join
, também queremos renomear as duas colunas “id” que são trazidas de nodes
. Para o join usando a coluna “source” eu renomearei a coluna como “from”. A coluna trazida do join de “destino” é renomeada para “to”. Seria possível fazer ambos os join em um único comando com o uso do pipe. No entanto, para maior clareza, eu executarei as junções em dois comandos separados. Como a junção é feita através de dois comandos, note que o quadro de dados no início do pipe muda de per_route
para edges
, que é criado pelo primeiro 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)
Agora que edges
tem colunas “de” e “para” com IDs de nó, precisamos reordenar as colunas para trazer “de” e “para” para a esquerda do quadro de dados. Atualmente, o quadro de dados edges
ainda contém as colunas “origem” e “destino” com os nomes das cidades que correspondem com os IDs. No entanto, estes dados são supérfluos, já que já estão presentes em nodes
. Por isso, vou incluir apenas as colunas “de”, “para” e “peso” nas colunas select()
function.
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
O quadro de dados edges
não parece muito impressionante; são três colunas de números inteiros. Contudo, edges
combinado com nodes
fornece-nos toda a informação necessária para criar objectos de rede com os network
, igraph
, e tidygraph
pacotes.
Criar objectos de rede
As classes de objectos de rede para network
, igraph
, e tidygraph
estão todas intimamente relacionadas. É possível traduzir entre um objecto network
e um objecto igraph
. Entretanto, é melhor manter os dois pacotes e seus objetos separados. De facto, as capacidades de network
e igraph
sobrepõem-se a tal ponto que é melhor ter apenas um dos pacotes carregados de cada vez. Vou começar por rever o pacote network
e depois passar para os pacotes igraph
e tidygraph
.
network
library(network)
A função usada para criar um objecto network
é network()
. O comando não é particularmente direto, mas você sempre pode entrar ?network()
no console se você ficar confuso. O primeiro argumento é – como declarado na documentação – “uma matriz dando a estrutura de rede em forma de adjacência, incidência ou edgelist”. A linguagem demonstra o significado das matrizes na análise da rede, mas em vez de uma matriz, temos uma lista de bordas, que preenche o mesmo papel. O segundo argumento é uma lista de atributos de vértices, que corresponde à lista de nós. Note que o pacote network
utiliza a nomenclatura de vértices ao invés de nós. O mesmo é válido para igraph
. Precisamos então especificar o tipo de dados que foram inseridos nos dois primeiros argumentos especificando que o matrix.type
é um "edgelist"
. Finalmente, definimos ignore.eval
para FALSE
para que nossa rede possa ser ponderada e levar em conta o número de letras ao longo de cada rota.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
Você pode ver o tipo de objeto criado pela função network()
, colocando routes_network
na função class()
.
class(routes_network)#> "network"
Imprimir routes_network
na consola mostra que a estrutura do objecto é bastante diferente dos objectos do estilo data-frame como edges
e nodes
. O comando de impressão revela informações que são especificamente definidas para análise de rede. Ele mostra que existem 13 vértices ou nós e 15 bordas em routes_network
. Estes números correspondem ao número de linhas em nodes
e edges
, respectivamente. Também podemos ver que os vértices e bordas contêm atributos como etiqueta e peso. Você pode obter ainda mais informações, incluindo uma sociomatriz dos dados, digitando 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
Agora é possível obter um gráfico rudimentar, se não esteticamente muito agradável, da nossa rede de letras. Ambos os pacotes network
e igraph
usam o sistema de gráficos base do R. As convenções para gráficos base são significativamente diferentes daquelas do ggplot2 – que eu discuti em posts anteriores – e então eu vou ficar com gráficos bastante simples ao invés de entrar nos detalhes da criação de gráficos complexos com base R. Neste caso, a única mudança que eu faço para a função padrão plot()
para o pacote network
é aumentar o tamanho dos nós com o argumento vertex.cex
para tornar os nós mais visíveis. Mesmo com este gráfico muito simples, nós já podemos aprender algo sobre os dados. O gráfico deixa claro que existem dois agrupamentos ou clusters principais dos dados, que correspondem ao tempo que Daniel passou na Holanda nos primeiros três quartos de 1585 e após sua mudança para Bremen em setembro.
plot(routes_network, vertex.cex = 3)
A função plot()
com um objeto network
usa o algoritmo Fruchterman e Reingold para decidir sobre a colocação dos nós.6 Você pode alterar o algoritmo de layout com o argumento mode
. Abaixo, eu layout os nós em um círculo. Este não é um arranjo particularmente útil para esta rede, mas dá uma idéia de algumas das opções disponíveis.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Vamos agora discutir o pacote igraph
. Primeiro, precisamos de limpar o ambiente em R removendo o pacote network
para que não interfira com os comandos igraph
. Também podemos remover o routes_network
, uma vez que não o vamos utilizar mais. O pacote network
pode ser removido com a função detach()
, e routes_network
é removido com rm()
.7 Depois disto, podemos carregar com segurança igraph
.
detach(package:network)rm(routes_network)library(igraph)
Para criar um objecto igraph
a partir de uma moldura de dados de uma lista de arestas podemos usar a função graph_from_data_frame()
, que é um pouco mais directa do que network()
. Há três argumentos na função graph_from_data_frame()
: d, vértices, e dirigido. Aqui, d refere-se à lista de bordas, vértices à lista de nós, e direcionado pode ser TRUE
ou FALSE
, dependendo se os dados são direcionados ou não.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Imprimir o objeto igraph
criado por graph_from_data_frame()
para o console revela informações semelhantes às de um objeto network
, embora a estrutura seja mais 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
A informação principal sobre o objeto está contida em DNW- 13 15 --
. Isto diz que routes_igraph
é uma rede dirigida (D) que tem um atributo de nome (N) e é ponderada (W). O traço após W nos diz que o gráfico não é bipartido. Os números que se seguem descrevem o número de nós e bordas no gráfico, respectivamente. A seguir, name (v/c), label (v/c), weight (e/n)
dá informações sobre os atributos do gráfico. Há dois atributos de vértices (v/c) de nome – que são os IDs – e etiquetas e um atributo de borda (e/n) de peso. Finalmente, há uma impressão de todas as bordas.
Apenas como com o pacote network
, podemos criar um gráfico com um objeto igraph
através da função plot()
. A única alteração que eu faço ao padrão aqui é diminuir o tamanho das setas. Por padrão igraph
etiqueta os nós com a coluna de etiqueta se houver um ou com os IDs.
plot(routes_igraph, edge.arrow.size = 0.2)
Tal como o gráfico network
antes, o padrão de um gráfico igraph
não é particularmente agradável esteticamente, mas todos os aspectos dos gráficos podem ser manipulados. Aqui, eu só quero alterar o layout dos nós para usar o algoritmo graphopt criado por Michael Schmuhl. Este algoritmo torna mais fácil ver a relação entre Haarlem, Antuérpia e Delft, que são três dos locais mais significantes na rede de correspondência, espalhando-os ainda mais.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph e ggraph
Os pacotes tidygraph
e ggraph
são recém-chegados ao cenário de análise da rede, mas juntos os dois pacotes oferecem vantagens reais sobre os pacotes network
e igraph
. Os pacotes tidygraph
e ggraph
representam uma tentativa de trazer a análise de rede para o fluxo de trabalho arrumado. tidygraph
fornece uma maneira de criar um objeto de rede que mais se assemelha a um tibble ou frame de dados. Isto torna possível usar muitas das funções dplyr
para manipular os dados da rede. ggraph
dá uma maneira de traçar gráficos de rede usando as convenções e a potência de ggplot2
. Em outras palavras, tidygraph
e ggraph
permitem lidar com objetos de rede de uma maneira mais consistente com os comandos usados para trabalhar com tibbles e quadros de dados. No entanto, a verdadeira promessa de tidygraph
e ggraph
é que eles aproveitam a potência de igraph
. Isto significa que você sacrifica poucas das capacidades de análise de rede de igraph
usando tidygraph
e ggraph
.
Precisamos começar como sempre carregando os pacotes necessários.
library(tidygraph)library(ggraph)
Primeiro, vamos criar um objeto de rede usando tidygraph
, que é chamado de tbl_graph
. Um tbl_graph
consiste de dois tibbles: um tibble de bordas e um tibble de nós. Convenientemente, a classe de objetos tbl_graph
é um envoltório em torno de um objeto igraph
, significando que na sua base um objeto tbl_graph
é essencialmente um objeto igraph
.8 A estreita ligação entre objetos tbl_graph
e igraph
resulta em duas formas principais de criar um objeto tbl_graph
. A primeira é usar uma lista de bordas e lista de nós, usando tbl_graph()
. Os argumentos para a função são quase idênticos aos de graph_from_data_frame()
com apenas uma pequena alteração nos nomes dos argumentos.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
A segunda maneira de criar um objeto tbl_graph
é converter um objeto igraph
ou network
usando as_tbl_graph()
. Assim, poderíamos converter um routes_igraph
para um tbl_graph
objeto.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Agora que criamos dois tbl_graph
objetos, vamos inspecioná-los com a função class()
. Isto mostra que routes_tidy
e routes_igraph_tidy
são objectos da classe "tbl_graph" "igraph"
, enquanto routes_igraph
é a classe de objectos "igraph"
.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Imprimir um objecto tbl_graph
para a consola resulta numa saída drasticamente diferente da de um objecto igraph
. É uma saída semelhante à de um tibble normal.
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
Impressão routes_tidy
mostra que é um objeto tbl_graph
com 13 nós e 15 bordas. O comando também imprime as seis primeiras linhas de “Node Data” e as três primeiras de “Edge Data”. Observe também que ele afirma que o Node Data está ativo. A noção de um tibble ativo dentro de um objeto tbl_graph
torna possível a manipulação dos dados em um tibble de cada vez. O nó tibble é ativado por padrão, mas você pode alterar qual tibble está ativo com a função activate()
. Assim, se eu quisesse reorganizar as linhas na tibble de bordas para listar aqueles com o maior “peso” primeiro, eu poderia usar activate()
e depois arrange()
. Aqui eu simplesmente imprimo o resultado ao invés de salvá-lo.
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
Posto que não precisamos mais manipular routes_tidy
, podemos plotar o gráfico com ggraph
. Como o ggmap, ggraph
é uma extensão de ggplot2
, facilitando a transferência de habilidades básicas de ggplot
para a criação de gráficos de rede. Como em todos os gráficos de rede, há três aspectos principais para um gráfico com ggraph
: nós, bordas, e layouts. As vinhetas para o pacote de gráficos ajudam a cobrir os aspectos fundamentais de ggraph
plots. ggraph
adiciona geoms especiais ao conjunto básico de ggplot
geoms que são especificamente projetados para redes. Assim, existe um conjunto de geom_node
e geom_edge
geoms. A função básica de plotting é ggraph()
, que leva os dados a serem usados para o gráfico e o tipo de layout desejado. Ambos os argumentos para ggraph()
são construídos em torno de igraph
. Portanto, ggraph()
pode usar ou um objeto igraph
ou um objeto tbl_graph
. Além disso, os algoritmos de layouts disponíveis derivam principalmente de igraph
. Finalmente, ggraph
introduz um tema especial ggplot
que fornece melhores padrões para gráficos de rede do que os normais ggplot
padrões. O tema ggraph
pode ser definido para uma série de gráficos com o comando set_graph_style()
antes dos gráficos serem plotados ou usando theme_graph()
nos gráficos individuais. Aqui, vou usar o último método.
Vejamos como é um gráfico básico ggraph
. O gráfico começa com ggraph()
e os dados. Eu então adiciono os geoms de borda básica e de nó. Não são necessários argumentos dentro dos geoms de borda e nó, porque eles tiram a informação dos dados fornecidos em ggraph()
.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Como você pode ver, a estrutura do comando é similar à de ggplot
com as camadas separadas adicionadas com o sinal +
. O gráfico básico ggraph
parece semelhante aos de network
e igraph
, se não mesmo mais simples, mas podemos usar comandos semelhantes a ggplot
para criar um gráfico mais informativo. Podemos mostrar o “peso” das bordas – ou a quantidade de cartas enviadas ao longo de cada rota – usando a largura na função geom_edge_link()
. Para obter a largura da linha a ser alterada de acordo com a variável peso, colocamos o argumento dentro de uma função aes()
. Para controlar a largura máxima e mínima das bordas, eu uso scale_edge_width()
e defino uma função range
. Escolho uma largura relativamente pequena para o mínimo, pois existe uma diferença significativa entre o número máximo e mínimo de cartas enviadas ao longo dos percursos. Também podemos rotular os nós com os nomes das localizações, uma vez que há relativamente poucos nós. Convenientemente, geom_node_text()
vem com um argumento repelente que garante que as etiquetas não se sobrepõem aos nós de forma semelhante ao pacote ggrepel. Eu adiciono um pouco de transparência às bordas com o argumento alfa. Também uso labs()
para relabelar a 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()
Além das escolhas de layout fornecidas por igraph
, ggraph
também implementa os seus próprios layouts. Por exemplo, você pode usar ggraph's
conceito de circularidade para criar diagramas de arco. Aqui, eu layout os nós em uma linha horizontal e tenho as bordas desenhadas como arcos. Ao contrário do gráfico anterior, este gráfico indica direcionalidade das arestas.9 As arestas acima da linha horizontal movem-se da esquerda para a direita, enquanto as arestas abaixo da linha movem-se da direita para a esquerda. Ao adicionar pontos para os nós, eu apenas incluo os nomes das etiquetas. Utilizo a mesma largura estética para denotar a diferença no peso de cada borda. Note que neste gráfico eu uso um objeto igraph
como dado para o gráfico, o que não faz diferença prática.
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 rede interativos com visNetwork e networkD3
O conjunto de pacotes htmlwidgets torna possível usar o R para criar visualizações interativas em JavaScript. Aqui, vou mostrar como fazer gráficos com os pacotes visNetwork
e networkD3
. Estes dois pacotes usam bibliotecas JavaScript diferentes para criar os seus gráficos. visNetwork
usa vis.js, enquanto networkD3
usa a popular biblioteca de visualização d3 para fazer seus gráficos. Uma dificuldade em trabalhar com ambos visNetwork
e networkD3
é que eles esperam que as listas de bordas e listas de nós usem nomenclatura específica. A manipulação de dados acima está de acordo com a estrutura básica para visNetwork
, mas algum trabalho precisará ser feito para networkD3
. Apesar deste inconveniente, ambos os pacotes possuem uma grande variedade de recursos gráficos e ambos podem trabalhar com igraph
objetos e layouts.
library(visNetwork)library(networkD3)
visNetwork
A função visNetwork()
usa uma lista de nós e lista de bordas para criar um gráfico interativo. A lista de nós deve incluir uma coluna “id”, e a lista de bordas deve ter colunas “de” e “para”. A função também plota as etiquetas para os nós, usando os nomes das cidades da coluna “label” na lista de nós. O gráfico resultante é divertido de se brincar. Você pode mover os nós e o gráfico usará um algoritmo para manter os nós devidamente espaçados. Você também pode aumentar e diminuir o gráfico e movê-lo para re-centrá-lo.
visNetwork(nodes, edges)
visNetwork
pode usar igraph
layouts, fornecendo uma grande variedade de layouts possíveis. Além disso, você pode usar visIgraph()
para plotar um objeto igraph
diretamente. Aqui, vou ficar com o fluxo de trabalho nodes
e edges
e usar um layout igraph
para customizar o gráfico. Também vou adicionar uma variável para alterar a largura da borda, como fizemos com ggraph
. visNetwork()
usa nomes de colunas das listas de bordas e nós para plotar atributos de rede em vez de argumentos dentro da chamada de função. Isto significa que é necessário fazer alguma manipulação de dados para obter uma coluna de “largura” na lista de borda. O atributo width para visNetwork()
não escalona os valores, então temos que fazer isso manualmente. Ambas as ações podem ser feitas com a função mutate()
e alguma aritmética simples. Aqui, eu crio uma nova coluna em edges
e escalo os valores de peso dividindo por 5. Adicionar 1 ao resultado fornece uma forma de criar uma largura mínima.
edges <- mutate(edges, width = weight/5 + 1)
Após isto ser feito, podemos criar um gráfico com larguras de borda variáveis. Eu também escolho um algoritmo de layout de igraph
e adiciono setas nas bordas, colocando-as no meio da borda.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
redeD3
Um pouco mais de trabalho é necessário para preparar os dados para criar um gráfico de networkD3
. Para fazer um gráfico networkD3
com uma lista de bordas e nós requer que os IDs sejam uma série de inteiros numéricos que começam com 0. Atualmente, os IDs dos nós para os nossos dados começam com 1, e assim temos que fazer um pouco de manipulação de dados. É possível renumerar os nós subtraindo 1 das colunas de ID nos quadros de dados nodes
e edges
. Mais uma vez, isto pode ser feito com a função mutate()
. O objetivo é recriar as colunas atuais, enquanto subtrai 1 de cada ID. A função mutate()
funciona criando uma nova coluna, mas podemos fazer com que ela substitua uma coluna dando à nova coluna o mesmo nome que a coluna antiga. Aqui, nomeio os novos quadros de dados com um sufixo d3 para distingui-los dos anteriores nodes
e edges
quadros de dados.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Agora é possível plotar um gráfico networkD3
. Ao contrário de visNetwork()
, a função forceNetwork()
usa uma série de argumentos para ajustar o gráfico e plotar os atributos da rede. Os argumentos “Links” e “Nodes” fornecem os dados para o gráfico na forma de listas de bordas e nós. A função também requer os argumentos “NodeID” e “Group”. Os dados usados aqui não têm nenhum agrupamento, então eu só tenho cada nó como seu próprio grupo, o que na prática significa que os nós serão todos de cores diferentes. Além disso, o abaixo diz a função que a rede tem campos “Origem” e “Destino”, e assim é direcionada. Incluo neste gráfico um “Valor”, que escalona a largura das bordas de acordo com a coluna “peso” na lista de bordas. Finalmente, adiciono alguns ajustes estéticos para tornar os nós opacos e aumentar o tamanho da fonte dos rótulos para melhorar a legibilidade. O resultado é muito semelhante ao primeiro gráfico visNetwork()
que criei mas com 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)
Um dos principais benefícios de networkD3
é que ele implementa um diagrama Sankey com estilo d3. Um diagrama Sankey é um bom ajuste para as cartas enviadas a Daniel em 1585. Não há muitos nós nos dados, o que facilita a visualização do fluxo de letras. A criação de um diagrama Sankey utiliza a função sankeyNetwork()
, que leva muitos dos mesmos argumentos que forceNetwork()
. Este gráfico não requer um argumento de grupo, e a única outra alteração é a adição de uma “unidade”. Isto fornece uma etiqueta para os valores que aparecem na ponta de uma ferramenta quando o cursor paira sobre um elemento do diagrama.10
>
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Outras leituras em Network Analysis
Este post tentou dar uma introdução geral à criação e plotagem de objetos do tipo rede em R usando os network
, igraph
, tidygraph
, e ggraph
pacotes para gráficos estáticos e visNetwork
e networkD3
para gráficos interativos. Eu apresentei esta informação a partir da posição de um não-especialista em teoria de rede. Eu cobri apenas uma porcentagem muito pequena das capacidades de análise de redes do R. Em particular, eu não discuti a análise estatística de redes. Felizmente, existe uma infinidade de recursos sobre análise de redes em geral e em R em particular.
A melhor introdução às redes que encontrei para os não iniciados é a Visualização de Redes de Katya Ognyanova com R. Isto apresenta tanto uma introdução útil aos aspectos visuais das redes como um tutorial mais aprofundado sobre a criação de gráficos de rede em R. Ognyanova usa principalmente igraph
, mas ela também introduz redes interativas.
Existem dois livros relativamente recentes publicados sobre análise de redes com R por Springer. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) é uma introdução muito útil à análise de rede com R. Luke cobre tanto o fato statnet dos pacotes como igragh
. O conteúdo está a um nível muito acessível ao longo de todo o processo. Mais avançado é o de Eric D. Kolaczyk e Gábor Csárdi, Análise Estatística de Dados em Rede com R (2014). O livro de Kolaczyk e Csárdi usa principalmente igraph
, já que Csárdi é o principal mantenedor do pacote igraph
para o R. Este livro aborda tópicos mais avançados sobre a análise estatística de redes. Apesar do uso de linguagem muito técnica, os primeiros quatro capítulos são geralmente acessíveis de um ponto de vista não-especialista.
A lista curada por François Briatte é uma boa visão geral dos recursos sobre análise de redes em geral. A série de posts “Networks Demystified” de Scott Weingart também vale bem a pena ler.
-
Um exemplo do interesse em análise de redes dentro das humanidades digitais é o recém-lançado Journal of Historical Network Research. ︎
-
Para uma boa descrição da classe de objetos
network
, incluindo uma discussão de sua relação com a classe de objetosigraph
, veja Carter Butts, “network”: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Esta é a estrutura específica esperada por
visNetwork
, ao mesmo tempo em que está de acordo com as expectativas gerais dos outros pacotes. ︎ -
Esta é a ordem esperada para as colunas de alguns dos pacotes de rede que irei utilizar abaixo. ︎
-
ungroup()
não é estritamente necessário neste caso. No entanto, se você não desagrupar o quadro de dados, não é possível soltar as colunas “fonte” e “destino”, como eu faço mais tarde no script. ︎ -
Thomas M. J. Fruchterman e Edward M. Reingold, “Graph Drawing by Force-Directed Placement,” Software: Prática e experiência, 21 (1991): 1129-1164. ︎
-
A função
rm()
é útil se o seu ambiente de trabalho em R ficar desorganizado, mas você não quer limpar todo o ambiente e começar de novo. ︎ -
A relação entre
tbl_graph
eigraph
objectos é semelhante à relação entretibble
edata.frame
objectos. ︎ -
É possível ter
ggraph
setas de desenho, mas não mostrei isso aqui. ︎ -
Pode demorar um pouco para que a ponta da ferramenta apareça. ︎
Deixe uma resposta