Indledning til netværksanalyse med R
On november 4, 2021 by adminPå en lang række områder er netværksanalyse blevet et stadig mere populært redskab for forskere til at håndtere kompleksiteten i de indbyrdes relationer mellem aktører af enhver art. Det lovende ved netværksanalyse er, at der lægges vægt på forbindelserne mellem aktørerne i stedet for at se aktørerne som isolerede enheder. Betoningen på kompleksitet sammen med skabelsen af en række algoritmer til at måle forskellige aspekter af netværk gør netværksanalyse til et centralt værktøj for digitale humaniora.1 Dette indlæg vil give en introduktion til at arbejde med netværk i R ved hjælp af eksemplet med netværket af byer i Daniel van der Meulens korrespondance fra 1585.
Der findes en række programmer, der er designet til netværksanalyse og oprettelse af netværksgrafer, såsom gephi og cytoscape. Selv om R ikke er specielt udviklet til dette formål, har det udviklet sig til et effektivt værktøj til netværksanalyse. Styrken ved R i forhold til stand-alone software til netværksanalyse er tredobbelt. For det første giver R mulighed for reproducerbar forskning, hvilket ikke er muligt med GUI-programmer. For det andet giver R’s dataanalysekraft robuste værktøjer til at manipulere data for at forberede dem til netværksanalyse. Endelig er der et stadigt voksende udvalg af pakker, der er designet til at gøre R til et komplet værktøj til netværksanalyse. Betydningsfulde netværksanalysepakker til R omfatter statnet-pakken og igraph
. Desuden har Thomas Lin Pedersen for nylig udgivet pakkerne tidygraph
og ggraph
, der udnytter styrken i igraph
på en måde, der er i overensstemmelse med tidyverse-arbejdsgangen. R kan også bruges til at lave interaktive netværksgrafer med htmlwidgets-rammen, der oversætter R-kode til JavaScript.
Dette indlæg begynder med en kort introduktion til det grundlæggende ordforråd for netværksanalyse, efterfulgt af en diskussion af processen for at få data ind i den rette struktur til netværksanalyse. Netværksanalysepakkerne har alle implementeret deres egne objektklasser. I dette indlæg vil jeg vise, hvordan man opretter de specifikke objektklasser for statnet-pakkesættet med pakken network
samt for igraph
og tidygraph
, som er baseret på igraph
-implementeringen. Endelig vil jeg vende mig mod oprettelsen af interaktive grafer med pakkerne vizNetwork
og networkD3
.
Netværksanalyse: Nodes and Edges
De to primære aspekter af netværk er en mangfoldighed af separate enheder og forbindelserne mellem dem. Ordforrådet kan være en smule teknisk og endda inkonsekvent mellem forskellige discipliner, pakker og software. Enhederne omtales som knuder eller hjørner i en graf, mens forbindelserne er kanter eller links. I dette indlæg vil jeg hovedsageligt bruge nomenklaturen for knuder og kanter, undtagen når jeg diskuterer pakker, der bruger et andet ordforråd.
Netværksanalysepakkerne har brug for, at data skal være i en bestemt form for at skabe den særlige type objekt, der bruges af hver pakke. Objektklasserne for network
, igraph
og tidygraph
er alle baseret på adjacensmatricer, også kendt som sociomatricer.2 En adjacensmatrix er en kvadratisk matrix, hvor kolonne- og rækkebetegnelserne er knuderne i netværket. I matricen angiver en 1, at der er en forbindelse mellem knuderne, og en 0 angiver, at der ikke er nogen forbindelse. Adjacensmatricer implementerer en meget anderledes datastruktur end datarammer og passer ikke ind i den tidyverse-arbejdsgang, som jeg har brugt i mine tidligere indlæg. Heldigvis kan de specialiserede netværksobjekter også oprettes fra en edge-list-dataramme, som passer ind i tidyverse-arbejdsgangen. I dette indlæg vil jeg holde mig til dataanalyseteknikkerne i tidyverse for at oprette kantlister, som derefter konverteres til de specifikke objektklasser for network
, igraph
og tidygraph
.
En kantliste er en dataramme, der indeholder mindst to kolonner, en kolonne med knuder, der er kilden til en forbindelse, og en anden kolonne med knuder, der er målet for forbindelsen. Knuderne i dataene identificeres ved hjælp af unikke ID’er. Hvis sondringen mellem kilde og mål er meningsfuld, er netværket rettet. Hvis sondringen ikke er meningsfuld, er netværket udirigeret. I eksemplet med breve, der sendes mellem byer, er sondringen mellem kilde og mål klart meningsfuld, og netværket er derfor rettet. I eksemplerne nedenfor benævner jeg kildekolonnen “fra” og målkolonnen “til”. Jeg vil bruge hele tal, der begynder med et, som node-id’er.3 En kantliste kan også indeholde yderligere kolonner, der beskriver egenskaber ved kanterne, f.eks. et størrelsesaspekt for en kant. Hvis kanterne har en størrelsesattribut, anses grafen for at være vægtet.
Kantlister indeholder alle de oplysninger, der er nødvendige for at oprette netværksobjekter, men nogle gange er det at foretrække også at oprette en separat knodeliste. I sin simpleste form er en node-liste en dataramme med en enkelt kolonne – som jeg vil betegne som “id” – der opregner node-id’erne, der findes i kantlisten. Fordelen ved at oprette en separat nodeliste er muligheden for at tilføje attributkolonner til datarammen, f.eks. navnene på noderne eller enhver form for gruppering. Nedenfor giver jeg et eksempel på minimale kant- og nodelister oprettet med funktionen 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
Sammenlign dette med en adjacensmatrix med de samme data.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Skabelse af kant- og nodelister
For at skabe netværksobjekter fra databasen over breve modtaget af Daniel van der Meulen i 1585 vil jeg lave både en kantliste og en nodeliste. Dette vil gøre det nødvendigt at anvende pakken dplyr til at manipulere datarammen af breve sendt til Daniel og opdele den i to datarammer eller tibbles med strukturen af kant- og nodelister. I dette tilfælde vil knuderne være de byer, hvorfra Daniels korrespondenter har sendt ham breve, og de byer, hvor han har modtaget dem. Node-listen vil indeholde en “label”-kolonne, som indeholder byernes navne. Kantlisten vil også have en attributkolonne, som vil vise mængden af breve, der er sendt mellem hvert bypar. Arbejdsgangen til at oprette disse objekter vil svare til den, jeg har brugt i min korte introduktion til R og i geokodning med R. Hvis du vil følge med, kan du finde de data, der er brugt i dette indlæg, og det anvendte R-script på GitHub.
Det første skridt er at indlæse tidyverse
-biblioteket for at importere og manipulere dataene. Udskrivning af letters
-datarammen viser, at den indeholder fire kolonner: “writer”, “source”, “source”, “destination” og “date”. I dette eksempel vil vi kun behandle kolonnerne “source” og “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
Nodeliste
Arbejdsgangen til at oprette en nodeliste svarer til den, jeg brugte til at få listen over byer med henblik på at geokode dataene i et tidligere indlæg. Vi ønsker at få de forskellige byer fra både kolonnerne “source” og “destination” og derefter at sammenføje oplysningerne fra disse kolonner. I eksemplet nedenfor ændrer jeg lidt på kommandoerne i forhold til dem, jeg brugte i det tidligere indlæg, så navnet for kolonnerne med bynavnene er det samme for både sources
og destinations
datarammen for at forenkle full_join()
-funktionen. Jeg omdøber kolonnen med bynavnene til “label” for at overtage det ordforråd, der anvendes af netværksanalysepakker.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
For at oprette en enkelt dataramme med en kolonne med de unikke steder skal vi bruge et fuldt join, fordi vi ønsker at medtage alle unikke steder fra både kilderne til brevene og destinationerne.
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
Dette resulterer i en dataramme med én variabel. Den variabel, der er indeholdt i datarammen, er dog ikke rigtig det, vi leder efter. Kolonnen “label” indeholder navnene på knudepunkterne, men vi ønsker også at have unikke ID’er for hver by. Det kan vi gøre ved at tilføje en “id”-kolonne til datarammen nodes
, som indeholder tal fra et til det samlede antal rækker i datarammen. En nyttig funktion til denne arbejdsgang er rowid_to_column()
, som tilføjer en kolonne med værdierne fra række-id’erne og placerer kolonnen i starten af datarammen.4 Bemærk, at rowid_to_column()
er en pipe-kommando, og det er derfor muligt at udføre full_join()
og tilføje “id”-kolonnen i en enkelt kommando. Resultatet er en noderliste med en ID-kolonne og en label-attribut.
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
Kantliste
Skabelse af en kantliste svarer til ovenstående, men det kompliceres af behovet for at håndtere to ID-kolonner i stedet for én. Vi ønsker også at oprette en vægtkolonne, der noterer mængden af breve, der sendes mellem hvert sæt knuder. For at opnå dette vil jeg bruge den samme group_by()
– og summarise()
-arbejdsgang, som jeg har diskuteret i tidligere indlæg. Forskellen her er, at vi ønsker at gruppere datarammen efter to kolonner – “kilde” og “destination” – i stedet for kun én kolonne. Tidligere har jeg kaldt den kolonne, der tæller antallet af observationer pr. gruppe, for “count”, men her overtager jeg nomenklaturen for netværksanalyse og kalder den for “weight”. Den sidste kommando i pipelinen fjerner den gruppering for datarammen, der er indført af group_by()
-funktionen. Dette gør det lettere at manipulere den resulterende per_route
dataramme uhindret. 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
Lige node-listen har per_route
nu den grundlæggende form, som vi ønsker, men vi har igen det problem, at kolonnerne “kilde” og “destination” indeholder etiketter i stedet for ID’er. Det, vi skal gøre, er at knytte de ID’er, der er blevet tildelt i nodes
, til hvert sted i både “kilde”- og “bestemmelses”-kolonnerne. Dette kan gøres med en anden join-funktion. Faktisk er det nødvendigt at udføre to sammenføjninger, en for “kilde”-kolonnen og en for “destination”. I dette tilfælde vil jeg bruge en left_join()
med per_route
som venstre dataramme, fordi vi ønsker at bevare antallet af rækker i per_route
. Mens vi laver left_join
, vil vi også omdøbe de to “id”-kolonner, der er bragt over fra nodes
. For joinet ved hjælp af “source”-kolonnen vil jeg omdøbe kolonnen til “from”. Den kolonne, der overføres fra “destination”-forbindelsen, omdøbes til “to”. Det ville være muligt at foretage begge sammenføjninger i en enkelt kommando ved hjælp af pipe. For overskuelighedens skyld vil jeg dog udføre sammenføjningerne i to separate kommandoer. Da sammenføjningen udføres på tværs af to kommandoer, skal du bemærke, at datarammen i begyndelsen af pipelinen ændres fra per_route
til edges
, som er oprettet af den første kommando.
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, hvor edges
har kolonner “fra” og “til” med node-id’er, skal vi omarrangere kolonnerne for at bringe “fra” og “til” til venstre i datarammen. I øjeblikket indeholder datarammen edges
stadig kolonnerne “kilde” og “destination” med navnene på de byer, der svarer til ID’erne. Disse data er imidlertid overflødige, da de allerede findes i nodes
. Derfor vil jeg kun medtage kolonnerne “fra”, “til” og “vægt” i select()
-funktionen.
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
Datarammen edges
ser ikke særlig imponerende ud; den består af tre kolonner med hele tal. Men edges
kombineret med nodes
giver os alle de oplysninger, der er nødvendige for at oprette netværksobjekter med pakkerne network
, igraph
og tidygraph
.
Skabelse af netværksobjekter
Netværksobjektklasserne for network
, igraph
og tidygraph
er alle nært beslægtede. Det er muligt at oversætte mellem et network
-objekt og et igraph
-objekt. Det er dog bedst at holde de to pakker og deres objekter adskilt. Faktisk overlapper mulighederne i network
og igraph
hinanden i en sådan grad, at det er den bedste praksis kun at have en af pakkerne indlæst ad gangen. Jeg vil begynde med at gennemgå network
-pakken og derefter gå over til igraph
– og tidygraph
-pakkerne.
network
library(network)
Den funktion, der bruges til at oprette et network
-objekt, er network()
. Kommandoen er ikke særlig ligetil, men du kan altid indtaste ?network()
i konsollen, hvis du bliver forvirret. Det første argument er – som det fremgår af dokumentationen – “en matrix, der angiver netværksstrukturen i adjacens-, incidens- eller kantlisteform”. Sproget demonstrerer betydningen af matricer i netværksanalyser, men i stedet for en matrix har vi en kantliste, som udfylder den samme rolle. Det andet argument er en liste over toppunktsattributter, som svarer til knudepunktslisten. Bemærk, at network
-pakken anvender nomenklaturen for vertices i stedet for nodes. Det samme gør sig gældende for igraph
. Vi skal derefter angive den type data, der er blevet indtastet i de to første argumenter, ved at angive, at matrix.type
er en "edgelist"
. Endelig sætter vi ignore.eval
til FALSE
, så vores netværk kan vægtes og tage hensyn til antallet af bogstaver langs hver rute.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
Du kan se den type objekt, der er oprettet af network()
-funktionen, ved at placere routes_network
i class()
-funktionen.
class(routes_network)#> "network"
Det fremgår af udskrift af routes_network
til konsollen, at objektets struktur er helt anderledes end objekter i dataramme-stil som edges
og nodes
. Udskrivningskommandoen afslører oplysninger, der er specifikt defineret til netværksanalyse. Den viser, at der er 13 vertices eller knuder og 15 kanter i routes_network
. Disse tal svarer til antallet af rækker i henholdsvis nodes
og edges
. Vi kan også se, at både toppene og kanterne indeholder attributter som f.eks. label og vægt. Du kan få endnu flere oplysninger, herunder en sociomatrix af dataene, ved at indtaste 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
Det er nu muligt at få en rudimentær, om end ikke overdrevent æstetisk tiltalende, graf af vores netværk af bogstaver. Både network
– og igraph
-pakkerne bruger baseplotsystemet i R. Konventionerne for baseplots er væsentligt anderledes end konventionerne for ggplot2 – som jeg har omtalt i tidligere indlæg – og derfor vil jeg holde mig til ret simple plots i stedet for at gå i detaljer med at skabe komplekse plots med base R. I dette tilfælde er den eneste ændring, jeg foretager i standardfunktionen plot()
for network
-pakken, at jeg øger størrelsen af knuderne med vertex.cex
-argumentet for at gøre knuderne mere synlige. Selv med denne meget enkle graf kan vi allerede lære noget om dataene. Grafen gør det klart, at der er to hovedgrupperinger eller klynger af dataene, som svarer til den tid, Daniel tilbragte i Holland i de første tre fjerdedele af 1585 og efter hans flytning til Bremen i september.
plot(routes_network, vertex.cex = 3)
Funktionen plot()
med et network
objekt bruger Fruchterman og Reingold algoritmen til at bestemme placeringen af knuderne.6 Du kan ændre placeringsalgoritmen med mode
-argumentet. Nedenfor har jeg layoutet knuderne i en cirkel. Dette er ikke en særlig brugbar opstilling for dette netværk, men det giver en idé om nogle af de tilgængelige muligheder.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Lad os nu gå over til at diskutere igraph
-pakken. Først skal vi rydde op i miljøet i R ved at fjerne network
-pakken, så den ikke forstyrrer igraph
-kommandoerne. Vi kan lige så godt også fjerne routes_network
, da vi ikke længere vil bruge den. network
-pakken kan fjernes med detach()
-funktionen, og routes_network
fjernes med rm()
.7 Herefter kan vi trygt indlæse igraph
.
detach(package:network)rm(routes_network)library(igraph)
For at oprette et igraph
-objekt fra en edge-list-datteramme kan vi bruge graph_from_data_frame()
-funktionen, som er lidt mere ligetil end network()
. Der er tre argumenter i graph_from_data_frame()
-funktionen: d, vertices og directed. Her henviser d til kantlisten, vertices til node-listen, og directed kan være enten TRUE
eller FALSE
afhængigt af, om dataene er dirigerede eller udirigerede.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Udskrivningen af igraph
-objektet oprettet af graph_from_data_frame()
til konsollen afslører lignende oplysninger som fra et network
-objekt, selv om strukturen er mere kryptisk.
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 vigtigste oplysninger om objektet er indeholdt i DNW- 13 15 --
. Dette fortæller, at routes_igraph
er et rettet netværk (D), der har en navneattribut (N) og er vægtet (W). Stregen efter W fortæller os, at grafen ikke er bipartit. De efterfølgende tal beskriver henholdsvis antallet af knuder og kanter i grafen. Dernæst giver name (v/c), label (v/c), weight (e/n)
oplysninger om grafens attributter. Der er to vertex-attributter (v/c) med navn – som er ID’er – og etiketter og en kantattribut (e/n) med vægt. Endelig er der en udskrift af alle kanter.
Som med network
-pakken kan vi oprette et plot med et igraph
-objekt via plot()
-funktionen. Den eneste ændring, som jeg foretager i forhold til standarden her, er at mindske størrelsen på pilene. Som standard mærker igraph
noderne med label-kolonnen, hvis der er en, eller med ID’erne.
plot(routes_igraph, edge.arrow.size = 0.2)
Lige network
-grafen før er standardværdien af et igraph
-plot ikke særlig æstetisk tiltalende, men alle aspekter af plottene kan manipuleres. Her ønsker jeg blot at ændre knudernes layout til at anvende graphopt-algoritmen, der er skabt af Michael Schmuhl. Denne algoritme gør det lettere at se forholdet mellem Haarlem, Antwerpen og Delft, som er tre af de mest betydningsfulde steder i korrespondancenettet, ved at sprede dem yderligere ud.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph og ggraph
Pakkerne tidygraph
og ggraph
er nytilkomne i netværksanalyselandskabet, men tilsammen giver de to pakker reelle fordele i forhold til pakkerne network
og igraph
. tidygraph
og ggraph
repræsenterer et forsøg på at bringe netværksanalyse ind i tidyverse-arbejdsgangen. tidygraph
giver en måde at oprette et netværksobjekt, der mere ligner en tibble eller en dataramme. Dette gør det muligt at bruge mange af dplyr
-funktionerne til at manipulere netværksdata. ggraph
giver en måde at plotte netværksgrafer på ved hjælp af konventionerne og styrken i ggplot2
. Med andre ord giver tidygraph
og ggraph
dig mulighed for at håndtere netværksobjekter på en måde, der er mere i overensstemmelse med de kommandoer, der anvendes til at arbejde med tibbles og datarammer. Det virkeligt lovende ved tidygraph
og ggraph
er imidlertid, at de udnytter styrken i igraph
. Det betyder, at du ofrer få af netværksanalysefunktionerne i igraph
ved at bruge tidygraph
og ggraph
.
Vi skal som altid starte med at indlæse de nødvendige pakker.
library(tidygraph)library(ggraph)
Først skal vi oprette et netværksobjekt ved hjælp af tidygraph
, som kaldes en tbl_graph
. En tbl_graph
består af to tibbles: en edges-bibble og en nodes-bibble. Det er praktisk nok, at tbl_graph
-objektklassen er en indpakning omkring et igraph
-objekt, hvilket betyder, at et tbl_graph
-objekt i sit udgangspunkt i alt væsentligt er et igraph
-objekt.8 Den tætte forbindelse mellem tbl_graph
– og igraph
-objekter resulterer i to hovedmetoder til at oprette et tbl_graph
-objekt. Den første er at anvende en kantliste og en node-liste ved hjælp af tbl_graph()
. Argumenterne for funktionen er næsten identiske med dem i graph_from_data_frame()
med kun en lille ændring af argumenternes navne.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
Den anden måde at oprette et tbl_graph
-objekt på er ved at konvertere et igraph
– eller network
-objekt ved hjælp af as_tbl_graph()
. Vi kan således konvertere routes_igraph
til et tbl_graph
-objekt.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Nu da vi har oprettet to tbl_graph
-objekter, kan vi inspicere dem med class()
-funktionen. Dette viser, at routes_tidy
og routes_igraph_tidy
er objekter af klassen "tbl_graph" "igraph"
, mens routes_igraph
er objektklasse "igraph"
.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Udskrivning af et tbl_graph
-objekt til konsollen resulterer i et drastisk anderledes output end et igraph
-objekt. Det er et output, der svarer til et normalt 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
Udskrivning af routes_tidy
viser, at det er et tbl_graph
-objekt med 13 knuder og 15 kanter. Kommandoen udskriver også de første seks rækker af “Node Data” og de første tre rækker af “Edge Data”. Bemærk også, at den angiver, at “Node Data” er aktiv. Begrebet en aktiv tibble i et tbl_graph
-objekt gør det muligt at manipulere dataene i en tibble ad gangen. Nodes tibble er som standard aktiveret, men du kan ændre, hvilken tibble der er aktiv, med activate()
-funktionen. Hvis jeg således ønsker at omarrangere rækkerne i edges tibble, så de rækker, der har den højeste “vægt”, vises først, kan jeg bruge activate()
og derefter arrange()
. Her udskriver jeg blot resultatet i stedet for at gemme det.
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
Da vi ikke har brug for at manipulere routes_tidy
yderligere, kan vi plotte grafen med ggraph
. Ligesom ggmap er ggraph
en udvidelse af ggplot2
, hvilket gør det lettere at overføre grundlæggende ggplot
-færdigheder til oprettelse af netværksgrafer. Som i alle netværksgrafer er der tre hovedaspekter i et ggraph
-plot: knuder, kanter og layouts. Vignetterne for ggraph-pakken dækker på en nyttig måde de grundlæggende aspekter af ggraph
-plots. ggraph
tilføjer særlige geoms til det grundlæggende sæt af ggplot
-geoms, som er specielt designet til netværk. Der er således et sæt af geom_node
og geom_edge
geoms. Den grundlæggende plotfunktion er ggraph()
, som tager de data, der skal bruges til grafen, og den ønskede type layout. Begge argumenter for ggraph()
er bygget op omkring igraph
. Derfor kan ggraph()
bruge enten et igraph
-objekt eller et tbl_graph
-objekt. Desuden stammer de tilgængelige layouts-algoritmer primært fra igraph
. Endelig introducerer ggraph
et særligt ggplot
-tema, der giver bedre standardindstillinger for netværksgrafer end de normale ggplot
-standardindstillinger. ggraph
-temaet kan indstilles for en række plot med kommandoen set_graph_style()
, der køres før graferne plottes, eller ved at bruge theme_graph()
i de enkelte plot. Her vil jeg bruge sidstnævnte metode.
Lad os se, hvordan et grundlæggende ggraph
-plot ser ud. Plottet begynder med ggraph()
og dataene. Derefter tilføjer jeg grundlæggende kant- og knudegeomer. Der er ingen argumenter nødvendige inden for kant- og knudegeometrierne, fordi de tager oplysningerne fra de data, der er angivet i ggraph()
.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Som du kan se, ligner kommandoens struktur den samme som i ggplot
med de separate lag tilføjet med +
-tegnet. Det grundlæggende ggraph
-plot ligner dem i network
og igraph
, hvis ikke endnu mere almindeligt, men vi kan bruge lignende kommandoer som ggplot
til at skabe en mere informativ graf. Vi kan vise “vægten” af kanterne – eller mængden af breve, der er sendt langs hver rute – ved at bruge width i geom_edge_link()
-funktionen. For at få linjens bredde til at ændre sig i overensstemmelse med vægtvariablen, placerer vi argumentet i en aes()
-funktion. For at styre den maksimale og minimale bredde af kanterne bruger jeg scale_edge_width()
og sætter en range
. Jeg vælger en forholdsvis lille bredde for minimum, fordi der er en betydelig forskel mellem det maksimale og minimale antal bogstaver, der sendes langs ruterne. Vi kan også mærke knuderne med stednavne, da der er relativt få knuder. Det er praktisk, at geom_node_text()
leveres med et repel-argument, der sikrer, at etiketterne ikke overlapper med knuderne på samme måde som ggrepel-pakken. Jeg tilføjer en smule gennemsigtighed til kanterne med alpha-argumentet. Jeg bruger også labs()
til at ommærke legenden “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()
Ud over de layoutvalg, der leveres af igraph
, implementerer ggraph
også sine egne layouts. Du kan f.eks. bruge ggraph's
s begreb om cirkularitet til at oprette buediagrammer. Her har jeg layoutet knuderne i en vandret linje og ladet kanterne tegnes som buer. I modsætning til det foregående plot angiver dette diagram kanternes retningsbestemthed.9 Kanterne over den vandrette linje bevæger sig fra venstre mod højre, mens kanterne under linjen bevæger sig fra højre mod venstre. I stedet for at tilføje punkter til knudepunkterne, medtager jeg blot navnene på etiketterne. Jeg bruger den samme bredde æstetisk til at angive forskellen i vægten af hver kant. Bemærk, at jeg i dette plot bruger et igraph
-objekt som data til grafen, hvilket ikke gør nogen praktisk forskel.
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()
Interaktive netværksgrafer med visNetwork og networkD3
Sættet af htmlwidgets-pakker gør det muligt at bruge R til at lave interaktive JavaScript-visualiseringer. Her vil jeg vise, hvordan man laver grafer med visNetwork
– og networkD3
-pakkerne. Disse to pakker bruger forskellige JavaScript-biblioteker til at oprette deres grafer. visNetwork
bruger vis.js, mens networkD3
bruger det populære d3-visualiseringsbibliotek til at lave sine grafer. En vanskelighed ved at arbejde med både visNetwork
og networkD3
er, at de forventer, at kantlister og node-lister bruger en bestemt nomenklatur. Ovenstående datamanipulation er i overensstemmelse med den grundlæggende struktur for visNetwork
, men der skal gøres noget arbejde for networkD3
. På trods af denne ulempe besidder begge pakker en bred vifte af grafiske muligheder, og begge kan arbejde med igraph
-objekter og layouts.
library(visNetwork)library(networkD3)
visNetwork
Funktionen visNetwork()
bruger en node- og kantliste til at oprette en interaktiv graf. Noderlisten skal indeholde en “id”-kolonne, og kantlisten skal have “from”- og “to”-kolonner. Funktionen tegner også nodernes etiketter ved hjælp af bynavne fra kolonnen “label” i nodelisten. Den resulterende graf er sjov at lege med. Du kan flytte knuderne, og grafen vil bruge en algoritme til at holde knuderne korrekt adskilt. Du kan også zoome ind og ud på grafen og flytte den rundt for at centrere den igen.
visNetwork(nodes, edges)
visNetwork
kan bruge igraph
layouts, hvilket giver et stort udvalg af mulige layouts. Desuden kan du bruge visIgraph()
til at plotte et igraph
-objekt direkte. Her vil jeg holde mig til nodes
og edges
-arbejdsgangen og bruge et igraph
-layout til at tilpasse grafen. Jeg vil også tilføje en variabel til at ændre bredden af kanten, som vi gjorde med ggraph
. visNetwork()
bruger kolonnenavne fra kant- og knudelisterne til at plotte netværksattributter i stedet for argumenter i funktionskaldet. Det betyder, at det er nødvendigt at foretage en vis datamanipulation for at få en “bredde”-kolonne i kantlisten. Breddeattributten for visNetwork()
skalerer ikke værdierne, så vi er nødt til at gøre dette manuelt. Begge disse handlinger kan udføres med funktionen mutate()
og noget simpel aritmetik. Her opretter jeg en ny kolonne i edges
og skalerer vægtværdierne ved at dividere med 5. Ved at lægge 1 til resultatet får vi en måde at skabe en minimumsbredde på.
edges <- mutate(edges, width = weight/5 + 1)
Når dette er gjort, kan vi oprette en graf med variable kantbredder. Jeg vælger også en layoutalgoritme fra igraph
og tilføjer pile til kanterne og placerer dem i midten af kanten.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
networkD3
Det er nødvendigt med lidt mere arbejde for at forberede dataene til at skabe en networkD3
graf. For at lave en networkD3
-graf med en liste over kanter og knuder kræves det, at ID’erne er en række numeriske hele tal, der begynder med 0. I øjeblikket begynder knude-ID’erne for vores data med 1, så vi er nødt til at foretage en smule datamanipulation. Det er muligt at omnummerere knuderne ved at trække 1 fra ID-kolonnerne i nodes
– og edges
-datarammene. Igen kan dette gøres med funktionen mutate()
. Målet er at genskabe de aktuelle kolonner, samtidig med at der trækkes 1 fra hvert ID. Funktionen mutate()
fungerer ved at oprette en ny kolonne, men vi kan få den til at erstatte en kolonne ved at give den nye kolonne det samme navn som den gamle kolonne. Her navngiver jeg de nye datarammer med et d3-suffiks for at skelne dem fra de tidligere nodes
og edges
datarammer.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Det er nu muligt at plotte en networkD3
graf. I modsætning til visNetwork()
bruger forceNetwork()
-funktionen en række argumenter til at justere grafen og plotte netværksattributter. Argumenterne “Links” og “Nodes” leverer dataene til plotningen i form af lister over kanter og knuder. Funktionen kræver også argumenterne “NodeID” og “Group”. De data, der anvendes her, har ingen grupperinger, og derfor lader jeg bare hver knude være sin egen gruppe, hvilket i praksis betyder, at knuderne alle vil have forskellige farver. Desuden fortæller nedenstående funktionen, at netværket har “Source”- og “Target”-felter, og dermed er rettet. Jeg inkluderer i denne graf en “Value”, som skalerer bredden af kanterne i forhold til kolonnen “weight” i kantlisten. Endelig tilføjer jeg nogle æstetiske justeringer for at gøre knuderne uigennemsigtige og øge skriftstørrelsen på etiketterne for at forbedre læsbarheden. Resultatet ligner meget det første visNetwork()
-plot, som jeg oprettede, men med andre æstetiske stilarter.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
En af de største fordele ved networkD3
er, at det implementerer et Sankey-diagram i d3-stil. Et Sankey-diagram passer godt til de breve, der blev sendt til Daniel i 1585. Der er ikke for mange knuder i dataene, hvilket gør det lettere at visualisere strømmen af breve. Ved oprettelse af et Sankey-diagram anvendes funktionen sankeyNetwork()
, som tager mange af de samme argumenter som forceNetwork()
. Dette diagram kræver ikke et gruppeargument, og den eneste anden ændring er tilføjelsen af en “enhed”. Dette giver en etiket til de værdier, der dukker op i et værktøjstip, når markøren svæver over et diagramelement. 10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Videre læsning om netværksanalyse
Dette indlæg har forsøgt at give en generel introduktion til oprettelse og plotte objekter af netværkstypen i R ved hjælp af pakkerne network
, igraph
, tidygraph
og ggraph
til statiske diagrammer og visNetwork
og networkD3
til interaktive diagrammer. Jeg har præsenteret disse oplysninger ud fra en ikke-specialist inden for netværksteori. Jeg har kun dækket en meget lille procentdel af R’s muligheder for netværksanalyse. Navnlig har jeg ikke drøftet den statistiske analyse af netværk. Heldigvis findes der et væld af ressourcer om netværksanalyse i almindelighed og i R i særdeleshed.
Den bedste introduktion til netværk, som jeg har fundet for den uindviede, er Katya Ognyanovas Network Visualization with R. Denne præsenterer både en nyttig introduktion til de visuelle aspekter af netværk og en mere dybtgående vejledning i at skabe netværksplots i R. Ognyanova bruger primært igraph
, men hun introducerer også interaktive netværk.
Der er udgivet to relativt nye bøger om netværksanalyse med R af Springer. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) er en meget nyttig introduktion til netværksanalyse med R. Luke dækker både statnet-pakkerne og igragh
. Indholdet er hele vejen igennem på et meget lettilgængeligt niveau. Mere avanceret er Eric D. Kolaczyk og Gábor Csárdi’s, Statistical Analysis of Network Data with R (2014). Kolaczyk og Csárdis bog anvender hovedsageligt igraph
, da Csárdi er den primære vedligeholder af igraph
-pakken til R. Denne bog kommer længere ind på avancerede emner om statistisk analyse af netværk. Trods brugen af et meget teknisk sprog er de første fire kapitler generelt tilgængelige fra et ikke-specialistisk synspunkt.
Listen kurateret af François Briatte er en god oversigt over ressourcer om netværksanalyse i almindelighed. Networks Demystified-serien af indlæg af Scott Weingart er også værd at læse.
-
Et eksempel på interessen for netværksanalyse inden for digital humaniora er det nyligt lancerede Journal of Historical Network Research. ︎
-
For en god beskrivelse af
network
objektklassen, herunder en diskussion af dens forhold tiligraph
objektklassen, se Carter Butts, “network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Dette er den specifikke struktur, der forventes af
visNetwork
, samtidig med at den er i overensstemmelse med de generelle forventninger fra de andre pakker. ︎ -
Dette er den forventede rækkefølge for kolonnerne for nogle af de netværkspakker, som jeg vil bruge nedenfor. ︎
-
ungroup()
er ikke strengt nødvendigt i dette tilfælde. Men hvis du ikke ophæver grupperingen af datarammen, er det ikke muligt at droppe kolonnerne “kilde” og “destination”, som jeg gør senere i scriptet. ︎ -
Thomas M. J. Fruchterman og Edward M. Reingold, “Graph Drawing by Force-Directed Placement,” Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
Funktionen
rm()
er nyttig, hvis dit arbejdsmiljø i R bliver uorganiseret, men du ikke ønsker at rydde hele miljøet og starte forfra igen. ︎ -
Forholdet mellem
tbl_graph
– ogigraph
-objekter svarer til forholdet mellemtibble
– ogdata.frame
-objekter. ︎ -
Det er muligt at lade
ggraph
tegne pile, men det har jeg ikke vist her. ︎ -
Det kan tage lidt tid, før værktøjstip vises. ︎
Skriv et svar