Over een groot aantal gebieden is netwerkanalyse een steeds populairder instrument geworden voor wetenschappers om de complexiteit van de onderlinge relaties tussen actoren van allerlei aard aan te pakken. De belofte van netwerkanalyse is de betekenis die wordt toegekend aan de relaties tussen actoren, in plaats van actoren te zien als geïsoleerde entiteiten. De nadruk op complexiteit, samen met de creatie van een verscheidenheid aan algoritmen om verschillende aspecten van netwerken te meten, maakt netwerkanalyse tot een centraal instrument voor de digitale geesteswetenschappen.1 Deze post zal een inleiding geven tot het werken met netwerken in R, aan de hand van het voorbeeld van het netwerk van steden in de briefwisseling van Daniel van der Meulen in 1585.
Er zijn een aantal toepassingen ontworpen voor netwerkanalyse en het maken van netwerkgrafieken, zoals gephi en cytoscape. Hoewel R er niet specifiek voor is ontworpen, heeft het zich ontwikkeld tot een krachtig hulpmiddel voor netwerkanalyse. De kracht van R in vergelijking met stand-alone netwerkanalysesoftware is drieledig. In de eerste plaats maakt R reproduceerbaar onderzoek mogelijk dat niet mogelijk is met GUI-toepassingen. Ten tweede biedt de data-analyse kracht van R robuuste tools voor het manipuleren van gegevens om ze voor te bereiden op netwerkanalyse. Tenslotte is er een steeds groeiende reeks pakketten ontworpen om van R een complete netwerkanalyse tool te maken. Belangrijke netwerkanalyse pakketten voor R zijn onder andere de statnet suite van pakketten en igraph
. Bovendien heeft Thomas Lin Pedersen onlangs de pakketten tidygraph
en ggraph
uitgebracht die de kracht van igraph
benutten op een manier die consistent is met de tidyverse workflow. R kan ook worden gebruikt om interactieve netwerkgrafieken te maken met het htmlwidgets framework dat R code vertaalt naar JavaScript.
Deze post begint met een korte introductie tot het basisvocabulaire van netwerkanalyse, gevolgd door een bespreking van het proces om gegevens in de juiste structuur voor netwerkanalyse te krijgen. De netwerkanalyse pakketten hebben allemaal hun eigen object klassen geïmplementeerd. In dit artikel zal ik laten zien hoe de specifieke object klassen voor de statnet suite van pakketten met het network
pakket, evenals voor igraph
en tidygraph
, die is gebaseerd op de igraph
implementatie, worden gemaakt. Tenslotte zal ik ingaan op het maken van interactieve grafieken met de vizNetwork
en networkD3
pakketten.
Netwerk Analyse: Nodes and Edges
De twee belangrijkste aspecten van netwerken zijn een veelheid van afzonderlijke entiteiten en de verbindingen tussen hen. Het vocabulaire kan een beetje technisch zijn en zelfs inconsistent tussen verschillende disciplines, pakketten, en software. De entiteiten worden knooppunten of vertices van een grafiek genoemd, terwijl de verbindingen edges of links zijn. In dit bericht zal ik hoofdzakelijk de nomenclatuur van knooppunten en randen gebruiken, behalve bij de bespreking van pakketten die een ander vocabulaire gebruiken.
De netwerkanalysepakketten hebben gegevens nodig in een bepaalde vorm om het speciale type object te maken dat door elk pakket wordt gebruikt. De objectklassen voor network
, igraph
, en tidygraph
zijn allemaal gebaseerd op adjacency matrices, ook bekend als sociomatrices.2 Een adjacency matrix is een vierkante matrix waarin de kolom- en rijnamen de knooppunten van het netwerk zijn. Binnen de matrix geeft een 1 aan dat er een verband bestaat tussen de knooppunten, en een 0 dat er geen verband bestaat. Adjacency matrices implementeren een heel andere datastructuur dan data frames en passen niet binnen de tidyverse workflow die ik in mijn vorige posts heb gebruikt. Gelukkig kunnen de gespecialiseerde netwerk objecten ook gemaakt worden van een edge-list data frame, die wel passen in de tidyverse workflow. In deze post zal ik me houden aan de data-analysetechnieken van de tidyverse om edge-lijsten te maken, die vervolgens zullen worden geconverteerd naar de specifieke objectklassen voor network
, igraph
, en tidygraph
.
Een edge-lijst is een dataframe dat minimaal twee kolommen bevat, een kolom met knooppunten die de bron zijn van een verbinding en een andere kolom met knooppunten die het doel zijn van de verbinding. De knooppunten in de gegevens worden geïdentificeerd door unieke ID’s. Als het onderscheid tussen bron en doel zinvol is, is het netwerk gericht. Als het onderscheid niet zinvol is, is het netwerk ongericht. In het voorbeeld van de brieven die tussen steden worden verstuurd, is het onderscheid tussen bron en doel duidelijk zinvol, en dus is het netwerk gericht. In de onderstaande voorbeelden noem ik de bronkolom “van” en de doelkolom “naar”. Ik zal gehele getallen beginnend met één gebruiken als knooppunt-ID’s.3 Een lijst met randen kan ook extra kolommen bevatten die attributen van de randen beschrijven, zoals een magnitude-aspect voor een rand. Als de randen een magnitude-attribuut hebben, wordt de grafiek als gewogen beschouwd.
Edge-lijsten bevatten alle informatie die nodig is om netwerk-objecten te maken, maar soms is het beter om ook een aparte node-lijst te maken. Op zijn eenvoudigst is een knooppuntenlijst een dataframe met een enkele kolom – die ik zal labelen als “id” – waarin de knooppunt-ID’s staan die in de kantenlijst zijn gevonden. Het voordeel van het maken van een aparte knooppuntenlijst is de mogelijkheid om attribuutkolommen toe te voegen aan het dataframe, zoals de namen van de knooppunten of elke vorm van groepering. Hieronder geef ik een voorbeeld van minimale rand- en knooppuntenlijsten die zijn gemaakt met de functie 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
Vergelijk dit met een adjacency matrix met dezelfde gegevens.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Het maken van rand- en knooppuntenlijsten
Om netwerk-objecten te maken van de database met brieven die Daniel van der Meulen in 1585 heeft ontvangen, zal ik zowel een randlijst als een knooppuntenlijst maken. Dit vereist het gebruik van het pakket dplyr om het dataframe van de brieven aan Daniel te manipuleren en het op te splitsen in twee dataframes of tibbles met de structuur van kanten- en knopenlijsten. In dit geval zijn de knooppunten de steden van waaruit Daniël’s correspondenten hem brieven stuurden en de steden waar hij ze ontving. De knooppuntenlijst zal een kolom “label” bevatten, met de namen van de steden. De randlijst zal ook een attribuutkolom hebben die het aantal brieven toont dat tussen elk stedenpaar werd verzonden. De workflow om deze objecten te maken zal vergelijkbaar zijn met die ik heb gebruikt in mijn korte introductie tot R en in geocoderen met R. Als je wilt meelopen, kun je de data die in deze post is gebruikt en het gebruikte R script vinden op GitHub.
De eerste stap is het laden van de tidyverse
bibliotheek om de data te importeren en te manipuleren. Het afdrukken van het letters
-dataframe laat zien dat het vier kolommen bevat: “schrijver”, “bron”, “bestemming”, en “datum”. In dit voorbeeld zullen we alleen de kolommen “bron” en “bestemming” behandelen.
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
Knooppuntenlijst
De workflow om een knooppuntenlijst te maken is vergelijkbaar met de workflow die ik heb gebruikt om de lijst met steden te krijgen om de gegevens te geocoderen in een eerdere post. We willen de verschillende steden uit zowel de “bron” als de “bestemming” kolommen halen en dan de informatie uit deze kolommen samenvoegen. In het onderstaande voorbeeld verander ik de commando’s enigszins ten opzichte van die ik in de vorige post gebruikte om de naam voor de kolommen met de plaatsnamen dezelfde te laten zijn voor zowel de sources
als de destinations
data frames om de full_join()
functie te vereenvoudigen. Ik hernoem de kolom met de plaatsnamen als “label” om de woordenschat over te nemen die door netwerkanalysepakketten wordt gebruikt.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
Om een enkel dataframe met een kolom met de unieke plaatsen te maken, moeten we een volledige join gebruiken, omdat we alle unieke plaatsen van zowel de bronnen van de brieven als de bestemmingen willen opnemen.
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
Dit resulteert in een dataframe met één variabele. De variabele in het dataframe is echter niet echt wat we zoeken. De kolom “label” bevat de namen van de knooppunten, maar we willen ook unieke ID’s hebben voor elke stad. We kunnen dit doen door een “id” kolom toe te voegen aan het nodes
data frame dat getallen bevat van één tot wat ook het totaal aantal rijen in het data frame is. Een handige functie voor deze workflow is rowid_to_column()
, die een kolom toevoegt met de waarden van de rij-id’s en de kolom aan het begin van het data frame plaatst.4 Merk op dat rowid_to_column()
een pipeable commando is, en het dus mogelijk is om de full_join()
te doen en de “id” kolom in een enkel commando toe te voegen. Het resultaat is een knooppuntenlijst met een ID-kolom en een labelattribuut.
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
Kantenlijst
Het maken van een knooppuntenlijst is vergelijkbaar met het bovenstaande, maar het wordt gecompliceerd doordat we te maken hebben met twee ID-kolommen in plaats van één. We willen ook een gewichtskolom maken die het aantal brieven noteert tussen elke set knooppunten. Om dit te bereiken zal ik dezelfde group_by()
en summarise()
workflow gebruiken die ik in vorige posts heb besproken. Het verschil hier is dat we het dataframe willen groeperen volgens twee kolommen – “bron” en “bestemming” – in plaats van slechts één. Eerder heb ik de kolom die het aantal waarnemingen per groep telt “count” genoemd, maar hier neem ik de nomenclatuur van de netwerkanalyse over en noem hem “weight”. Het laatste commando in de pijplijn verwijdert de groepering voor het dataframe, ingesteld door de functie group_by()
. Dit maakt het gemakkelijker om het resulterende per_route
-dataframe ongehinderd te manipuleren.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
Net als de knooppuntenlijst heeft per_route
nu de basisvorm die we willen, maar we hebben opnieuw het probleem dat de kolommen “bron” en “bestemming” labels bevatten in plaats van ID’s. Wat we moeten doen is de ID’s die zijn toegewezen in nodes
koppelen aan elke locatie in zowel de “bron” als de “bestemming” kolommen. Dit kan worden bereikt met een andere join functie. In feite is het nodig om twee joins uit te voeren, één voor de kolom “bron” en één voor “bestemming”. In dit geval zal ik een left_join()
gebruiken met per_route
als het linker dataframe, omdat we het aantal rijen in per_route
willen behouden. Terwijl we de left_join
doen, willen we ook de twee “id” kolommen hernoemen die zijn overgebracht van nodes
. Voor de join met behulp van de “bron” kolom zal ik de kolom hernoemen als “van”. De kolom die wordt gebruikt voor de “destination” join krijgt de naam “to”. Het zou mogelijk zijn om beide joins in een enkel commando uit te voeren met behulp van de pipe. Maar voor de duidelijkheid zal ik de joins in twee afzonderlijke commando’s uitvoeren. Omdat de join in twee commando’s wordt uitgevoerd, is te zien dat het dataframe aan het begin van de pijplijn verandert van per_route
in edges
, dat is gemaakt door het eerste commando.
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)
Nu edges
“from” en “to” kolommen heeft met knooppunt-ID’s, moeten we de kolommen opnieuw rangschikken om “from” en “to” naar de linkerkant van het dataframe te brengen. Momenteel bevat het edges
-dataframe nog de kolommen “bron” en “bestemming” met de namen van de steden die overeenkomen met de ID’s. Deze gegevens zijn echter overbodig. Deze gegevens zijn echter overbodig, omdat zij reeds in nodes
aanwezig zijn. Daarom zal ik alleen de kolommen “van”, “naar”, en “gewicht” in de select()
functie opnemen.
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
Het edges
data frame ziet er niet erg indrukwekkend uit; het zijn drie kolommen met gehele getallen. Echter, edges
gecombineerd met nodes
verschaft ons alle informatie die nodig is om netwerk objecten te maken met de network
, igraph
, en tidygraph
packages.
Creëren van netwerk objecten
De netwerk object klassen voor network
, igraph
, en tidygraph
zijn allemaal nauw verwant. Het is mogelijk om te vertalen tussen een network
object en een igraph
object. Het is echter het beste om de twee pakketten en hun objecten gescheiden te houden. In feite overlappen de mogelijkheden van network
en igraph
elkaar in die mate dat het de beste praktijk is om slechts een van de pakketten tegelijk geladen te hebben. Ik zal beginnen met het network
pakket en dan verder gaan met de igraph
en tidygraph
pakketten.
network
library(network)
De functie die gebruikt wordt om een network
object te maken is network()
. Het commando is niet bijzonder recht-toe-recht-aan, maar u kunt altijd ?network()
in de console invoeren als u in de war raakt. Het eerste argument is – zoals in de documentatie staat – “een matrix die de netwerkstructuur geeft in de vorm van adjacency, incidence, of edgelist.” De taal demonstreert het belang van matrices in netwerkanalyse, maar in plaats van een matrix hebben we een lijst van kanten, die dezelfde rol vervult. Het tweede argument is een lijst van vertex attributen, die overeenkomt met de nodes lijst. Merk op dat het network
pakket de nomenclatuur van vertices gebruikt in plaats van nodes. Hetzelfde geldt voor igraph
. Vervolgens moeten we het type gegevens opgeven dat in de eerste twee argumenten is ingevoerd door te specificeren dat de matrix.type
een "edgelist"
is. Tenslotte stellen we ignore.eval
in op FALSE
, zodat ons netwerk kan worden gewogen en rekening kan houden met het aantal letters langs elke route.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
U kunt het type object zien dat door de functie network()
wordt gecreëerd door routes_network
in de functie class()
te plaatsen.
class(routes_network)#> "network"
Afdrukken van routes_network
naar de console laat zien dat de structuur van het object heel anders is dan dataframe-achtige objecten zoals edges
en nodes
. Het print commando onthult informatie die specifiek is gedefinieerd voor netwerk analyse. Het laat zien dat er 13 hoekpunten of knooppunten en 15 ribben zijn in routes_network
. Deze aantallen komen overeen met het aantal rijen in respectievelijk nodes
en edges
. We kunnen ook zien dat de hoekpunten en ribben beide attributen bevatten, zoals label en gewicht. U kunt nog meer informatie krijgen, waaronder een sociomatrix van de gegevens, door 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
Het is nu mogelijk om een rudimentaire, zij het niet al te esthetisch aantrekkelijke, grafiek te krijgen van ons netwerk van letters. Zowel de network
als igraph
pakketten gebruiken het basis plotten systeem van R. De conventies voor basis plots zijn significant verschillend van die van ggplot2 – die ik in vorige posts heb besproken – en daarom zal ik het houden bij vrij eenvoudige plots in plaats van in te gaan op de details van het maken van complexe plots met basis R. In dit geval is de enige verandering die ik aanbreng in de standaard plot()
functie voor het network
pakket het vergroten van de knooppunten met het vertex.cex
argument om de knooppunten meer zichtbaar te maken. Zelfs met deze zeer eenvoudige grafiek kunnen we al iets leren over de gegevens. De grafiek maakt duidelijk dat er twee hoofdgroepen of clusters van de gegevens zijn, die overeenkomen met de tijd die Daniël in Holland doorbracht in het eerste driekwart van 1585 en na zijn verhuizing naar Bremen in september.
plot(routes_network, vertex.cex = 3)
De plot()
functie met een network
object gebruikt het Fruchterman en Reingold algoritme om te beslissen over de plaatsing van de knooppunten.6 U kunt het opmaak-algoritme wijzigen met het mode
-argument. Hieronder worden de knooppunten in een cirkel geplaatst. Dit is geen bijzonder nuttige indeling voor dit netwerk, maar het geeft een idee van enkele van de beschikbare opties.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Laten we nu verder gaan met het bespreken van het igraph
pakket. Eerst moeten we de omgeving in R opschonen door het pakket network
te verwijderen, zodat het niet interfereert met de igraph
commando’s. We kunnen net zo goed ook routes_network
verwijderen, omdat we het niet langer zullen gebruiken. Het network
pakket kan worden verwijderd met de detach()
functie, en routes_network
wordt verwijderd met rm()
.7 Hierna kunnen we veilig igraph
laden.
detach(package:network)rm(routes_network)library(igraph)
Om een igraph
object te maken van een edge-list data frame kunnen we de graph_from_data_frame()
functie gebruiken, die een beetje meer recht-toe-recht-aan is dan network()
. Er zijn drie argumenten in de graph_from_data_frame()
functie: d, vertices, en directed. Hier verwijst d naar de lijst met randen, vertices naar de lijst met knooppunten, en directed kan TRUE
of FALSE
zijn, afhankelijk van of de gegevens gericht of niet-gericht zijn.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Afdrukken van het igraph
object gemaakt door graph_from_data_frame()
naar de console onthult soortgelijke informatie als die van een network
object, hoewel de structuur meer cryptisch is.
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
De belangrijkste informatie over het object staat in DNW- 13 15 --
. Deze vertelt ons dat routes_igraph
een gericht netwerk (D) is dat een naam-attribuut (N) heeft en gewogen (W) is. Het streepje na W vertelt ons dat de grafiek niet tweeledig is. De getallen die volgen beschrijven respectievelijk het aantal knooppunten en het aantal ribben in de grafiek. Vervolgens geeft name (v/c), label (v/c), weight (e/n)
informatie over de attributen van de grafiek. Er zijn twee vertex-attributen (v/c) van naam – dat zijn de ID’s – en labels en een rand-attribuut (e/n) van gewicht. Tenslotte is er een uitdraai van alle randen.
Net als met het network
pakket, kunnen we een plot maken met een igraph
object door middel van de plot()
functie. De enige wijziging die ik hier aanbreng is dat de pijlen kleiner worden. Standaard labelt igraph
de knooppunten met de label kolom als die er is of met de IDs.
plot(routes_igraph, edge.arrow.size = 0.2)
Zoals de network
grafiek hiervoor, is de standaard van een igraph
plot niet bijzonder esthetisch, maar alle aspecten van de plots kunnen worden gemanipuleerd. Hier wil ik alleen de layout van de knooppunten veranderen om het graphopt algoritme te gebruiken dat is gemaakt door Michael Schmuhl. Dit algoritme maakt het gemakkelijker om de relatie tussen Haarlem, Antwerpen en Delft te zien, drie van de meest significante locaties in het correspondentienetwerk, door ze verder uit te spreiden.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph en ggraph
De pakketten tidygraph
en ggraph
zijn nieuwkomers in het netwerkanalyse-landschap, maar samen bieden de twee pakketten echte voordelen boven de pakketten network
en igraph
. tidygraph
en ggraph
vertegenwoordigen een poging om netwerkanalyse in de tidyverse workflow te brengen. tidygraph
biedt een manier om een netwerk object te maken dat meer lijkt op een tibble of data frame. Dit maakt het mogelijk om veel van de dplyr
functies te gebruiken om netwerk gegevens te manipuleren. ggraph
biedt een manier om netwerkgrafieken te plotten met gebruikmaking van de conventies en mogelijkheden van ggplot2
. Met andere woorden, tidygraph
en ggraph
stellen u in staat om met netwerk objecten om te gaan op een manier die meer consistent is met de commando’s die gebruikt worden voor het werken met tibbles en data frames. De echte belofte van tidygraph
en ggraph
is echter dat ze gebruik maken van de kracht van igraph
. Dit betekent dat je weinig van de netwerk analyse mogelijkheden van igraph
opoffert door tidygraph
en ggraph
te gebruiken.
We moeten zoals altijd beginnen met het laden van de benodigde pakketten.
library(tidygraph)library(ggraph)
Laten we eerst een netwerk object maken met tidygraph
, dat een tbl_graph
wordt genoemd. Een tbl_graph
bestaat uit twee tibbles: een edges tibble en een nodes tibble. Gemakshalve is de tbl_graph
object klasse een omhulsel rond een igraph
object, wat betekent dat in zijn basis een tbl_graph
object in wezen een igraph
object is.8 De nauwe band tussen tbl_graph
en igraph
objecten resulteert in twee hoofdmanieren om een tbl_graph
object te maken. De eerste is om een edge list en node list te gebruiken, met behulp van tbl_graph()
. De argumenten voor de functie zijn bijna identiek aan die van graph_from_data_frame()
met slechts een kleine wijziging in de namen van de argumenten.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
De tweede manier om een tbl_graph
object te maken is door een igraph
of network
object te converteren met behulp van as_tbl_graph()
. Zo zouden we routes_igraph
kunnen converteren naar een tbl_graph
object.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Nu we twee tbl_graph
objecten hebben gecreëerd, laten we ze inspecteren met de class()
functie. Hieruit blijkt dat routes_tidy
en routes_igraph_tidy
objecten zijn van klasse "tbl_graph" "igraph"
, terwijl routes_igraph
objectklasse "igraph"
is.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Uitprinten van een tbl_graph
-object naar de console resulteert in een drastisch andere uitvoer dan die van een igraph
-object. Het is een uitvoer die lijkt op die van een 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
Afdrukken van routes_tidy
laat zien dat het een tbl_graph
-object is met 13 knooppunten en 15 randen. Het commando drukt ook de eerste zes rijen van “Knooppuntgegevens” en de eerste drie van “Randgegevens” af. Merk ook op dat er staat dat de knooppuntgegevens actief zijn. De notie van een actieve tibble binnen een tbl_graph
-object maakt het mogelijk om de gegevens in een tibble tegelijk te manipuleren. De knooppunten-tibble is standaard geactiveerd, maar u kunt veranderen welke tibble actief is met de activate()
-functie. Dus, als ik de rijen in de randen-tibble wil herschikken om de rijen met het hoogste “gewicht” eerst te tonen, kan ik activate()
gebruiken en dan arrange()
. Hier druk ik het resultaat gewoon af in plaats van het op te slaan.
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
Omdat we routes_tidy
niet verder hoeven te manipuleren, kunnen we de grafiek plotten met ggraph
. Net als ggmap is ggraph
een uitbreiding van ggplot2
, waardoor het gemakkelijker is om de basisvaardigheden van ggplot
over te brengen naar het maken van netwerkgrafieken. Zoals in alle netwerk-grafieken, zijn er drie hoofdaspecten aan een ggraph
plot: nodes, edges, en layouts. De vignetten voor het ggraph pakket behandelen de fundamentele aspecten van ggraph
-plots. ggraph
voegt speciale geoms toe aan de basis set van ggplot
geoms die speciaal zijn ontworpen voor netwerken. Zo is er een set van geom_node
en geom_edge
geoms. De basisplotfunctie is ggraph()
, die de gegevens neemt die voor de grafiek moeten worden gebruikt en het gewenste opmaaktype. Beide argumenten voor ggraph()
zijn opgebouwd rond igraph
. Daarom kan ggraph()
zowel een igraph
object als een tbl_graph
object gebruiken. Bovendien zijn de beschikbare lay-out algoritmen in de eerste plaats afgeleid van igraph
. Tenslotte introduceert ggraph
een speciaal ggplot
-thema dat betere standaards biedt voor netwerk-grafieken dan de normale ggplot
-standaards. Het ggraph
-thema kan worden ingesteld voor een serie plots met het set_graph_style()
commando dat wordt uitgevoerd voordat de grafieken worden geplot of door theme_graph()
te gebruiken in de individuele plots. Hier zal ik de laatste methode gebruiken.
Laten we eens kijken hoe een basis ggraph
-plot eruit ziet. De plot begint met ggraph()
en de gegevens. Daarna voeg ik basis rand- en knooppuntgeooms toe. Er zijn geen argumenten nodig binnen de rand- en knooppuntgeooms, omdat ze de informatie overnemen uit de gegevens die in ggraph()
zijn verstrekt.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Zoals u kunt zien, is de structuur van het commando gelijk aan die van ggplot
met de afzonderlijke lagen toegevoegd met het +
teken. De basisplot van ggraph
lijkt op die van network
en igraph
, zo niet nog eenvoudiger, maar we kunnen soortgelijke commando’s als ggplot
gebruiken om een meer informatieve grafiek te maken. We kunnen het “gewicht” van de randen laten zien – of de hoeveelheid brieven die langs elke route zijn verzonden – door width te gebruiken in de geom_edge_link()
functie. Om de breedte van de lijn te laten veranderen volgens de gewichtsvariabele, plaatsen we het argument in een aes()
-functie. Om de maximale en minimale breedte van de randen te regelen, gebruik ik scale_edge_width()
en stel een range
in. Ik kies een relatief kleine breedte voor het minimum, omdat er een aanzienlijk verschil is tussen het maximum en het minimum aantal letters dat langs de routes wordt gestuurd. We kunnen de knooppunten ook labelen met de namen van de locaties, omdat er relatief weinig knooppunten zijn. Handig is dat geom_node_text()
wordt geleverd met een repel argument dat ervoor zorgt dat de labels niet overlappen met de knooppunten op een manier die vergelijkbaar is met het ggrepel pakket. Ik voeg een beetje transparantie toe aan de randen met het alpha argument. Ik gebruik ook labs()
om de legenda een nieuw label te geven: “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()
Naast de lay-out keuzes die igraph
biedt, implementeert ggraph
ook zijn eigen lay-outs. U kunt bijvoorbeeld ggraph's
concept van circulariteit gebruiken om boogdiagrammen te maken. Hier heb ik de knooppunten in een horizontale lijn gelegd en de randen als bogen getekend. In tegenstelling tot de vorige plot, geeft deze grafiek de richting van de randen aan.9 De randen boven de horizontale lijn bewegen van links naar rechts, terwijl de randen onder de lijn van rechts naar links bewegen. In plaats van punten voor de knooppunten toe te voegen, vermeld ik alleen de labelnamen. Ik gebruik dezelfde breedte esthetisch om het verschil in gewicht van elke rand aan te geven. Merk op dat ik in deze plot een igraph
object gebruik als de data voor de grafiek, wat in de praktijk geen verschil maakt.
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()
Interactieve netwerk grafieken met visNetwork en networkD3
De htmlwidgets set van pakketten maakt het mogelijk om R te gebruiken om interactieve JavaScript visualisaties te maken. Hier zal ik laten zien hoe je grafieken kunt maken met de visNetwork
en networkD3
pakketten. Deze twee pakketten gebruiken verschillende JavaScript bibliotheken om hun grafieken te maken. visNetwork
gebruikt vis.js, terwijl networkD3
de populaire d3 visualisatie bibliotheek gebruikt om zijn grafieken te maken. Een moeilijkheid bij het werken met zowel visNetwork
als networkD3
is dat zij verwachten dat lijsten met randen en lijsten met knooppunten een specifieke nomenclatuur gebruiken. De bovenstaande datamanipulatie voldoet aan de basisstructuur voor visNetwork
, maar er zal wat werk moeten worden verricht voor networkD3
. Ondanks dit ongemak beschikken beide pakketten over een breed scala aan grafische mogelijkheden en kunnen beide werken met igraph
-objecten en lay-outs.
library(visNetwork)library(networkD3)
visNetwork
De visNetwork()
-functie gebruikt een knooppuntenlijst en een randenlijst om een interactieve grafiek te maken. De knooppuntenlijst moet een kolom “id” bevatten, en de lijst met randen moet kolommen “van” en “naar” hebben. De functie plot ook de labels voor de knooppunten, gebruikmakend van de namen van de steden uit de kolom “label” in de knooppuntenlijst. De resulterende grafiek is leuk om mee te spelen. U kunt de knooppunten verplaatsen en de grafiek zal een algoritme gebruiken om de knooppunten op de juiste afstand van elkaar te houden. U kunt ook in- en uitzoomen op de plot en deze verplaatsen om hem opnieuw te centreren.
visNetwork(nodes, edges)
visNetwork
kan igraph
lay-outs gebruiken, waardoor een grote verscheidenheid aan mogelijke lay-outs ontstaat. Bovendien kunt u visIgraph()
gebruiken om een igraph
-object direct te plotten. Hier zal ik me houden aan de nodes
en edges
workflow en een igraph
lay-out gebruiken om de grafiek aan te passen. Ik zal ook een variabele toevoegen om de breedte van de rand te veranderen, zoals we deden met ggraph
. visNetwork()
gebruikt kolomnamen uit de rand- en knooppuntlijsten om netwerkattributen te plotten in plaats van argumenten binnen de functie-aanroep. Dit betekent dat er enige datamanipulatie nodig is om een “width” kolom in de edge lijst te krijgen. Het width attribuut voor visNetwork()
schaalt de waarden niet, dus moeten we dit handmatig doen. Beide acties kunnen worden gedaan met de mutate()
functie en wat eenvoudige rekenkundige bewerkingen. Hier maak ik een nieuwe kolom in edges
en schaal de gewichtswaarden door te delen door 5. Door 1 toe te voegen aan het resultaat kunnen we een minimale breedte creëren.
edges <- mutate(edges, width = weight/5 + 1)
Als dit eenmaal is gedaan, kunnen we een grafiek maken met variabele randbreedtes. Ik kies ook een opmaakalgoritme uit igraph
en voeg pijlen toe aan de randen, door ze in het midden van de rand te plaatsen.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
networkD3
Een beetje meer werk is nodig om de gegevens voor te bereiden om een networkD3
-grafiek te maken. Om een networkD3
-grafiek te maken met een lijst met zijden en knooppunten, moeten de ID’s een reeks numerieke gehele getallen zijn die beginnen met 0. Momenteel beginnen de ID’s van de knooppunten in onze gegevens met 1, en dus moeten we de gegevens een beetje manipuleren. Het is mogelijk om de knooppunten te hernummeren door 1 af te trekken van de ID-kolommen in de nodes
en edges
dataframes. Ook dit kan worden gedaan met de mutate()
functie. Het doel is om de huidige kolommen opnieuw te maken, terwijl er 1 wordt afgetrokken van elke ID. De functie mutate()
creëert een nieuwe kolom, maar we kunnen de functie ook een kolom laten vervangen door de nieuwe kolom dezelfde naam te geven als de oude kolom. Hier noem ik de nieuwe dataframes met een d3 suffix om ze te onderscheiden van de vorige nodes
en edges
dataframes.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Het is nu mogelijk om een networkD3
grafiek te plotten. In tegenstelling tot visNetwork()
gebruikt de forceNetwork()
-functie een reeks argumenten om de attributen van de grafiek aan te passen en het netwerk te plotten. De argumenten “Links” en “Nodes” leveren de gegevens voor de plot in de vorm van lijsten met randen en knooppunten. De functie heeft ook de argumenten “NodeID” en “Group” nodig. De gegevens die hier gebruikt worden hebben geen groeperingen, en dus laat ik elk knooppunt zijn eigen groep zijn, wat in de praktijk betekent dat de knooppunten allemaal verschillende kleuren zullen hebben. Bovendien vertelt het onderstaande de functie dat het netwerk “Bron”- en “Doel”-velden heeft, en dus gericht is. Ik voeg in deze grafiek een “Waarde” toe, die de breedte van de randen schaalt volgens de kolom “gewicht” in de lijst met randen. Tenslotte voeg ik enkele esthetische aanpassingen toe om de knooppunten ondoorzichtig te maken en de lettergrootte van de labels te vergroten om de leesbaarheid te verbeteren. Het resultaat lijkt erg op de eerste visNetwork()
plot die ik heb gemaakt, maar met verschillende esthetische stijlen.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
Eén van de belangrijkste voordelen van networkD3
is dat het een d3-achtig Sankey diagram implementeert. Een Sankey-diagram past goed bij de brieven die in 1585 naar Daniel zijn gestuurd. Er zijn niet al te veel knooppunten in de gegevens, waardoor het gemakkelijker is om de stroom van brieven te visualiseren. Het maken van een Sankey-diagram maakt gebruik van de functie sankeyNetwork()
, die veel van dezelfde argumenten heeft als forceNetwork()
. Deze grafiek heeft geen groep-argument nodig, en de enige andere verandering is de toevoeging van een “eenheid”. Dit biedt een label voor de waarden die in een tooltip verschijnen wanneer uw cursor over een diagramelement zweeft.10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Verder lezen over netwerkanalyse
Deze post heeft geprobeerd een algemene inleiding te geven in het maken en plotten van netwerktype objecten in R met behulp van de network
, igraph
, tidygraph
, en ggraph
pakketten voor statische plots en visNetwork
en networkD3
voor interactieve plots. Ik heb deze informatie gepresenteerd vanuit de positie van een niet-specialist in netwerktheorie. Ik heb slechts een zeer klein percentage van de netwerkanalyse mogelijkheden van R behandeld. In het bijzonder heb ik de statistische analyse van netwerken niet besproken. Gelukkig is er een overvloed aan informatie over netwerkanalyse in het algemeen en in R in het bijzonder.
De beste inleiding tot netwerken die ik heb gevonden voor niet-ingewijden is Katya Ognyanova’s Network Visualization with R. Dit is zowel een nuttige inleiding tot de visuele aspecten van netwerken als een meer diepgaande tutorial over het maken van netwerkplots in R. Ognyanova gebruikt vooral igraph
, maar ze introduceert ook interactieve netwerken.
Er zijn twee relatief recente boeken verschenen over netwerkanalyse met R bij Springer. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) is een zeer nuttige inleiding tot netwerkanalyse met R. Luke behandelt zowel het statnet-pakket van pakketten als igragh
. De inhoud is overal op een zeer laagdrempelig niveau. Meer gevorderd is Eric D. Kolaczyk en Gábor Csárdi’s, Statistical Analysis of Network Data with R (2014). Kolaczyk en Csárdi’s boek maakt voornamelijk gebruik van igraph
, aangezien Csárdi de primaire onderhouder is van het igraph
pakket voor R. Dit boek gaat verder in op geavanceerde onderwerpen over de statistische analyse van netwerken. Ondanks het gebruik van zeer technische taal, zijn de eerste vier hoofdstukken over het algemeen benaderbaar vanuit een niet-specialistisch standpunt.
De door François Briatte samengestelde lijst is een goed overzicht van bronnen over netwerkanalyse in het algemeen. De serie Networks Demystified van Scott Weingart is ook zeer de moeite waard.
-
Een voorbeeld van de belangstelling voor netwerkanalyse binnen de digitale geesteswetenschappen is het onlangs gelanceerde Journal of Historical Network Research. ︎
-
Voor een goede beschrijving van de objectklasse
network
, inclusief een bespreking van de relatie met de objectklasseigraph
, zie Carter Butts, “network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Dit is de specifieke structuur die door
visNetwork
wordt verwacht, terwijl het ook voldoet aan de algemene verwachtingen van de andere pakketten. ︎ -
Dit is de verwachte volgorde voor de kolommen voor enkele van de netwerkpakketten die ik hieronder zal gebruiken. ︎
-
ungroup()
is in dit geval niet strikt noodzakelijk. Als u het dataframe echter niet ontgroepeert, is het niet mogelijk om de kolommen “bron” en “bestemming” te laten vallen, zoals ik later in het script doe. ︎ -
Thomas M. J. Fruchterman and Edward M. Reingold, “Graph Drawing by Force-Directed Placement,” Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
De functie
rm()
is handig als uw werkomgeving in R rommelig wordt, maar u niet de hele omgeving wilt leegmaken en opnieuw beginnen. ︎ -
De relatie tussen
tbl_graph
enigraph
objecten is vergelijkbaar met die tussentibble
endata.frame
objecten. ︎ -
Het is mogelijk om
ggraph
pijlen te laten tekenen, maar dat heb ik hier niet laten zien. ︎ -
Het kan even duren voordat de tool tip verschijnt. ︎