Introduktion till nätverksanalys med R
On november 4, 2021 by adminNätverksanalys har blivit ett alltmer populärt verktyg för forskare för att hantera komplexiteten i de ömsesidiga relationerna mellan aktörer av alla slag. Nätverksanalysens löfte är att man lägger vikt vid relationerna mellan aktörerna, snarare än att se aktörerna som isolerade enheter. Betoningen på komplexitet, tillsammans med skapandet av en mängd olika algoritmer för att mäta olika aspekter av nätverk, gör nätverksanalys till ett centralt verktyg för digital humaniora.1 Det här inlägget kommer att ge en introduktion till hur man arbetar med nätverk i R, med hjälp av exemplet med nätverket av städer i Daniel van der Meulens korrespondens från 1585.
Det finns ett antal program som är utformade för nätverksanalys och skapandet av nätverksgrafer, t.ex. gephi och cytoscape. Även om R inte är särskilt utformat för detta har det utvecklats till ett kraftfullt verktyg för nätverksanalys. R:s styrka i jämförelse med fristående programvaror för nätverksanalys är trefaldig. För det första möjliggör R reproducerbar forskning som inte är möjlig med GUI-program. För det andra ger R:s kraft för dataanalys robusta verktyg för att manipulera data för att förbereda dem för nätverksanalys. Slutligen finns det ett ständigt växande utbud av paket som är utformade för att göra R till ett komplett verktyg för nätverksanalys. Viktiga paket för nätverksanalys för R inkluderar paketpaketet statnet och igraph
. Dessutom har Thomas Lin Pedersen nyligen släppt paketen tidygraph
och ggraph
som utnyttjar kraften i igraph
på ett sätt som överensstämmer med tidyverse-arbetsflödet. R kan också användas för att göra interaktiva nätverksgrafer med ramverket htmlwidgets som översätter R-kod till JavaScript.
Detta inlägg inleds med en kort introduktion till den grundläggande vokabulären för nätverksanalys, följt av en diskussion om processen för att få in data i rätt struktur för nätverksanalys. Nätverksanalyspaketen har alla implementerat sina egna objektklasser. I det här inlägget kommer jag att visa hur man skapar de specifika objektklasserna för statnetpaketet med paketet network
, samt för igraph
och tidygraph
, som bygger på igraph
s implementering. Slutligen kommer jag att övergå till skapandet av interaktiva grafer med paketen vizNetwork
och networkD3
.
Nätverksanalys: Nodes and Edges
De två primära aspekterna av nätverk är en mängd separata enheter och förbindelserna mellan dem. Ordförrådet kan vara lite tekniskt och till och med inkonsekvent mellan olika discipliner, paket och programvaror. Enheterna kallas noder eller hörn i ett diagram, medan förbindelserna är kanter eller länkar. I det här inlägget kommer jag huvudsakligen att använda nomenklaturen för noder och kanter utom när jag diskuterar paket som använder en annan vokabulär.
Nätverksanalyspaketen behöver data i en viss form för att skapa den speciella typ av objekt som används av varje paket. Objektklasserna för network
, igraph
och tidygraph
är alla baserade på adjacensmatriser, även kallade sociomatriser.2 En adjacensmatris är en kvadratisk matris där kolumn- och radnamnen är nätets noder. I matrisen anger en 1 att det finns ett samband mellan noderna och en 0 att det inte finns något samband. Adjacensmatriser implementerar en mycket annorlunda datastruktur än dataramar och passar inte in i det tidyverse-arbetsflöde som jag har använt i mina tidigare inlägg. Som en hjälp kan de specialiserade nätverksobjekten också skapas från en dataruta med kantlistor, som passar in i tidyverse-arbetsflödet. I det här inlägget kommer jag att hålla mig till dataanalysmetoderna i tidyverse för att skapa kantlistor, som sedan kommer att konverteras till de specifika objektklasserna för network
, igraph
och tidygraph
.
En kantlista är en dataram som innehåller minst två kolumner, en kolumn med noder som är källan till en anslutning och en annan kolumn med noder som är målet för anslutningen. Noderna i uppgifterna identifieras med unika ID. Om distinktionen mellan källa och mål är meningsfull är nätverket riktat. Om distinktionen inte är meningsfull är nätverket ostyrt. I exemplet med brev som skickas mellan städer är distinktionen mellan källa och mål tydligt meningsfull, och därför är nätverket riktat. I exemplen nedan kommer jag att kalla källkolumnen för ”från” och målkolumnen för ”till”. Jag kommer att använda heltal som börjar med ett som nod-ID.3 En kantlista kan också innehålla ytterligare kolumner som beskriver egenskaper hos kanterna, t.ex. en storleksaspekt för en kant. Om kanterna har ett magnitudattribut anses grafen vara viktad.
Kantlistor innehåller all information som behövs för att skapa nätverksobjekt, men ibland är det att föredra att även skapa en separat nodlista. I sin enklaste form är en nodlista en dataruta med en enda kolumn – som jag kommer att beteckna som ”id” – som listar de nod-ID:n som finns i kantlistan. Fördelen med att skapa en separat nodlista är möjligheten att lägga till attributkolumner till dataramen, t.ex. nodernas namn eller någon form av gruppering. Nedan ger jag ett exempel på minimala kant- och nodlistor som skapats 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
Växla detta med en adjacensmatris med samma 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
Skapande av kant- och nodlistor
För att skapa nätverksobjekt från databasen med brev som Daniel van der Meulen mottog 1585 kommer jag att skapa både en kantlista och en nodlista. Detta kommer att kräva att paketet dplyr används för att manipulera dataramen av brev som skickats till Daniel och dela upp den i två dataramar eller tibbles med strukturen av kant- och nodlistor. I det här fallet kommer noderna att vara de städer från vilka Daniels korrespondenter skickade brev till honom och de städer där han tog emot dem. Node-listan kommer att innehålla en kolumn ”etikett” med städernas namn. Kantlistan kommer också att ha en attributkolumn som visar hur många brev som skickats mellan varje stadspar. Arbetsflödet för att skapa dessa objekt kommer att likna det som jag har använt i min korta introduktion till R och i geokodning med R. Om du vill följa med kan du hitta de data som används i det här inlägget och det R-skript som används på GitHub.
Det första steget är att ladda tidyverse
-biblioteket för att importera och manipulera data. Att skriva ut letters
-dataramen visar att den innehåller fyra kolumner: ”writer”, ”source”, ”destination” och ”date”. I det här exemplet kommer vi bara att hantera kolumnerna ”source” och ”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
Nodlista
Arbetsflödet för att skapa en nodlista liknar det som jag använde för att få fram listan över städer för att geokoda data i ett tidigare inlägg. Vi vill få fram de olika städerna från både kolumnerna ”source” och ”destination” och sedan sammanföra informationen från dessa kolumner. I exemplet nedan ändrar jag kommandona något jämfört med de kommandon jag använde i det tidigare inlägget så att namnet på kolumnerna med ortsnamnen är detsamma för både sources
och destinations
dataramar för att förenkla full_join()
-funktionen. Jag döper om kolumnen med ortsnamnen till ”label” för att anta den vokabulär som används av paket för nätverksanalys.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
För att skapa en enda dataram med en kolumn med de unika platserna måste vi använda en full join, eftersom vi vill inkludera alla unika platser från både källorna till breven och destinationerna.
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
Det här resulterar i en dataram med en variabel. Den variabel som ingår i dataramen är dock inte riktigt vad vi letar efter. Kolumnen ”label” innehåller namnen på noderna, men vi vill också ha unika ID för varje stad. Vi kan göra detta genom att lägga till en kolumn ”id” i dataramen nodes
som innehåller siffror från ett till det totala antalet rader i dataramen. En användbar funktion för detta arbetsflöde är rowid_to_column()
, som lägger till en kolumn med värdena från rad-id:erna och placerar kolumnen i början av dataramen.4 Observera att rowid_to_column()
är ett pipeable-kommando, så det är möjligt att göra full_join()
och lägga till ”id”-kolumnen i ett enda kommando. Resultatet är en nodlista med en ID-kolumn och ett etikettattribut.
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
Skapandet av en edge list liknar ovanstående, men kompliceras av att man måste hantera två ID-kolumner i stället för en. Vi vill också skapa en viktkolumn som noterar hur många brev som skickas mellan varje uppsättning noder. För att åstadkomma detta kommer jag att använda samma group_by()
och summarise()
arbetsflöde som jag har diskuterat i tidigare inlägg. Skillnaden här är att vi vill gruppera dataramen efter två kolumner – ”källa” och ”destination” – i stället för bara en. Tidigare har jag kallat kolumnen som räknar antalet observationer per grupp för ”count”, men här antar jag nätverksanalysens nomenklatur och kallar den för ”weight”. Det sista kommandot i pipeline tar bort den gruppering för dataramen som instiftats av funktionen group_by()
. Detta gör det lättare att obehindrat manipulera den resulterande per_route
dataramen.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
Likt nodlistan har per_route
nu den grundläggande form som vi vill ha, men vi har återigen problemet att kolumnerna ”source” och ”destination” innehåller etiketter i stället för ID. Vad vi behöver göra är att koppla de ID som har tilldelats i nodes
till varje plats i både kolumnerna ”source” och ”destination”. Detta kan åstadkommas med en annan sammanfogningsfunktion. Det är faktiskt nödvändigt att utföra två sammanfogningar, en för kolumnen ”källa” och en för kolumnen ”destination”. I det här fallet kommer jag att använda en left_join()
med per_route
som vänster dataruta, eftersom vi vill behålla antalet rader i per_route
. När vi gör left_join
vill vi också byta namn på de två ”id”-kolumnerna som förs över från nodes
. För den sammanfogning som använder kolumnen ”source” kommer jag att byta namn på kolumnen till ”from”. Den kolumn som tas över från ”destination”-fogningen döps om till ”to”. Det skulle vara möjligt att göra båda sammanfogningarna i ett enda kommando med hjälp av pipe. För tydlighetens skull kommer jag dock att utföra sammanfogningarna i två separata kommandon. Eftersom sammanfogningen görs i två kommandon, observera att dataramen i början av pipeline ändras från per_route
till edges
, som skapas av det första kommandot.
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 när edges
har kolumnerna ”from” och ”to” med nod-ID:n måste vi ordna om kolumnerna så att kolumnerna ”from” och ”to” hamnar till vänster i dataramen. För närvarande innehåller dataramen edges
fortfarande kolumnerna ”source” och ”destination” med namnen på de städer som motsvarar ID:erna. Dessa uppgifter är dock överflödiga eftersom de redan finns i nodes
. Därför kommer jag bara att inkludera kolumnerna ”från”, ”till” och ”vikt” 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
Databasen edges
ser inte särskilt imponerande ut; den består av tre kolumner med heltal. Men edges
i kombination med nodes
ger oss all information som behövs för att skapa nätverksobjekt med paketen network
, igraph
och tidygraph
.
Skapa nätverksobjekt
Nätverksobjektklasserna för network
, igraph
och tidygraph
är alla nära relaterade. Det är möjligt att översätta mellan ett network
-objekt och ett igraph
-objekt. Det är dock bäst att hålla de två paketen och deras objekt åtskilda. Faktum är att funktionerna i network
och igraph
överlappar varandra i sådan utsträckning att det är bäst att bara ha ett av paketen inläst åt gången. Jag kommer att börja med att gå igenom paketet network
och sedan gå över till paketen igraph
och tidygraph
.
nätverk
library(network)
Funktionen som används för att skapa ett network
-objekt är network()
. Kommandot är inte särskilt enkelt, men du kan alltid skriva ?network()
i konsolen om du blir förvirrad. Det första argumentet är – enligt dokumentationen – ”en matris som ger nätverksstrukturen i form av adjacens, incidens eller edgelist”. Språket visar betydelsen av matriser i nätverksanalys, men i stället för en matris har vi en kantlista, som fyller samma roll. Det andra argumentet är en lista med vertexattribut, som motsvarar nodlistan. Observera att network
-paketet använder nomenklaturen för vertices i stället för nodes. Samma sak gäller för igraph
. Vi måste sedan ange vilken typ av data som har angetts i de två första argumenten genom att ange att matrix.type
är en "edgelist"
. Slutligen sätter vi ignore.eval
till FALSE
så att vårt nätverk kan viktas och ta hänsyn till antalet bokstäver längs varje väg.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
Du kan se vilken typ av objekt som skapas av network()
-funktionen genom att placera routes_network
i class()
-funktionen.
class(routes_network)#> "network"
Att skriva ut routes_network
till konsolen visar att objektets struktur är helt annorlunda än objekt i stil med dataramar som edges
och nodes
. Utskriftskommandot avslöjar information som är specifikt definierad för nätverksanalys. Det visar att det finns 13 hörn eller noder och 15 kanter i routes_network
. Dessa siffror motsvarar antalet rader i nodes
respektive edges
. Vi kan också se att noderna och kanterna båda innehåller attribut som etikett och vikt. Du kan få ännu mer information, inklusive en sociomatrix av uppgifterna, genom att skriva in 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 är nu möjligt att få fram en rudimentär, om än inte överdrivet estetiskt tilltalande, graf över vårt nätverk av bokstäver. Både paketen network
och igraph
använder R:s system för basplottar. Konventionerna för basplottar skiljer sig avsevärt från konventionerna för ggplot2 – som jag har diskuterat i tidigare inlägg – och därför kommer jag att hålla mig till ganska enkla plottar i stället för att gå in på detaljerna kring hur man skapar komplexa plottar med bas-R. I det här fallet är den enda ändringen som jag gör till standardfunktionen plot()
för paketet network
att jag ökar storleken på noderna med argumentet vertex.cex
för att göra noderna mer synliga. Även med denna mycket enkla graf kan vi redan lära oss något om data. Grafen klargör att det finns två huvudsakliga grupperingar eller kluster av data, som motsvarar den tid som Daniel tillbringade i Holland under de första tre fjärdedelarna av 1585 och efter hans flytt till Bremen i september.
plot(routes_network, vertex.cex = 3)
Funktionen plot()
med objektet network
använder Fruchterman- och Reingold-algoritmen för att besluta om nodernas placering.6 Du kan ändra placeringsalgoritmen med argumentet mode
. Nedan lägger jag ut noderna i en cirkel. Detta är inte ett särskilt användbart arrangemang för det här nätverket, men det ger en uppfattning om några av de tillgängliga alternativen.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Låts oss nu gå vidare till att diskutera igraph
-paketet. Först måste vi städa upp miljön i R genom att ta bort network
-paketet så att det inte stör igraph
-kommandona. Vi kan lika gärna ta bort routes_network
också eftersom vi inte längre kommer att använda det. Paketet network
kan tas bort med funktionen detach()
och routes_network
tas bort med rm()
.7 Efter detta kan vi säkert ladda igraph
.
detach(package:network)rm(routes_network)library(igraph)
För att skapa ett igraph
-objekt från en dataruta för kantlistor kan vi använda funktionen graph_from_data_frame()
, som är lite enklare än network()
. Det finns tre argument i funktionen graph_from_data_frame()
: d, vertices och directed. Här hänvisar d till kantlistan, vertices till nodlistan och directed kan vara antingen TRUE
eller FALSE
beroende på om uppgifterna är riktade eller oriktade.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Om man skriver ut igraph
-objektet som skapats av graph_from_data_frame()
till konsolen får man liknande information som från ett network
-objekt, även om strukturen är mer 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
Den viktigaste informationen om objektet finns i DNW- 13 15 --
. Här står det att routes_igraph
är ett riktat nätverk (D) som har ett namnattribut (N) och är viktat (W). Strecket efter W talar om att grafen inte är tvådelad. Siffrorna som följer beskriver antalet noder respektive kanter i grafen. Därefter ger name (v/c), label (v/c), weight (e/n)
information om grafens attribut. Det finns två vertexattribut (v/c) med namn – som är ID – och etiketter och ett kantattribut (e/n) med vikt. Slutligen finns det en utskrift av alla kanter.
Samma som med network
-paketet kan vi skapa en graf med ett igraph
-objekt genom plot()
-funktionen. Den enda ändring som jag gör av standardvärdet här är att minska storleken på pilarna. Som standard märker igraph
noderna med etikettkolumnen om det finns en sådan eller med ID:n.
plot(routes_igraph, edge.arrow.size = 0.2)
Likt network
-grafen tidigare är standardvärdet för en igraph
-plott inte särskilt estetiskt tilltalande, men alla aspekter av plotten kan manipuleras. Här vill jag bara ändra nodernas layout för att använda algoritmen graphopt som skapats av Michael Schmuhl. Denna algoritm gör det lättare att se förhållandet mellan Haarlem, Antwerpen och Delft, som är tre av de mest betydelsefulla platserna i korrespondensnätverket, genom att sprida ut dem ytterligare.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph och ggraph
Paketen tidygraph
och ggraph
är nykomlingar i landskapet för nätverksanalys, men tillsammans ger de två paketen verkliga fördelar jämfört med paketen network
och igraph
. tidygraph
och ggraph
utgör ett försök att föra in nätverksanalys i tidyverse-arbetsflödet. tidygraph
erbjuder ett sätt att skapa ett nätverksobjekt som mer liknar en tibble eller dataram. Detta gör det möjligt att använda många av dplyr
-funktionerna för att manipulera nätverksdata. ggraph
ger ett sätt att plotta nätverksgrafer med hjälp av konventionerna och kraften i ggplot2
. Med andra ord gör tidygraph
och ggraph
det möjligt att hantera nätverksobjekt på ett sätt som är mer konsekvent med de kommandon som används för att arbeta med tibbles och dataramar. Det verkliga löftet med tidygraph
och ggraph
är dock att de utnyttjar kraften i igraph
. Detta innebär att du offrar få av nätverksanalysfunktionerna i igraph
genom att använda tidygraph
och ggraph
.
Vi måste som vanligt börja med att ladda in de nödvändiga paketen.
library(tidygraph)library(ggraph)
Först ska vi skapa ett nätverksobjekt med hjälp av tidygraph
, vilket kallas för ett tbl_graph
. En tbl_graph
består av två tibbles: en edges tibble och en nodes tibble. Det är praktiskt att tbl_graph
-objektklassen är en omslagsklass runt ett igraph
-objekt, vilket innebär att ett tbl_graph
-objekt i grunden är ett igraph
-objekt.8 Den nära kopplingen mellan tbl_graph
– och igraph
-objekten resulterar i att det finns två huvudsakliga sätt att skapa ett tbl_graph
-objekt. Det första är att använda en kantlista och en nodlista med hjälp av tbl_graph()
. Argumenten för funktionen är nästan identiska med dem i graph_from_data_frame()
med endast en liten ändring av argumentens namn.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
Det andra sättet att skapa ett tbl_graph
-objekt är att konvertera ett igraph
– eller network
-objekt med hjälp av as_tbl_graph()
. Vi kan alltså konvertera routes_igraph
till ett tbl_graph
-objekt.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Nu när vi har skapat två tbl_graph
-objekt kan vi inspektera dem med funktionen class()
. Detta visar att routes_tidy
och routes_igraph_tidy
är objekt av klassen "tbl_graph" "igraph"
, medan routes_igraph
är objekt av klassen "igraph"
.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Att skriva ut ett tbl_graph
-objekt till konsolen resulterar i en drastiskt annorlunda utskrift än för ett igraph
-objekt. Det är en utdata som liknar den för en normal 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
Utskrift av routes_tidy
visar att det är ett tbl_graph
-objekt med 13 noder och 15 kanter. Kommandot skriver också ut de sex första raderna av ”Node Data” och de tre första raderna av ”Edge Data”. Lägg också märke till att det anges att Node Data är aktiv. Begreppet en aktiv tibble i ett tbl_graph
-objekt gör det möjligt att manipulera data i en tibble i taget. Nodes tibble är aktiverad som standard, men du kan ändra vilken tibble som är aktiv med funktionen activate()
. Om jag alltså vill ordna om raderna i edges tibble så att de med högst ”vikt” listas först, kan jag använda activate()
och sedan arrange()
. Här skriver jag helt enkelt ut resultatet i stället för att spara 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
Då vi inte behöver manipulera routes_tidy
ytterligare kan vi plotta grafen med ggraph
. Liksom ggmap är ggraph
en förlängning av ggplot2
, vilket gör det lättare att överföra grundläggande ggplot
-färdigheter till skapandet av nätverksdiagram. Liksom i alla nätverksgrafer finns det tre huvudaspekter i en ggraph
-plott: noder, kanter och layouter. Vignetterna för paketet ggraph täcker på ett bra sätt de grundläggande aspekterna av ggraph
-plottar. ggraph
lägger till speciella geoms till den grundläggande uppsättningen ggplot
geoms som är särskilt utformade för nätverk. Det finns alltså en uppsättning geom_node
och geom_edge
geoms. Den grundläggande plottfunktionen är ggraph()
, som tar emot de data som ska användas för grafen och den typ av layout som önskas. Båda argumenten för ggraph()
är uppbyggda kring igraph
. Därför kan ggraph()
använda antingen ett igraph
-objekt eller ett tbl_graph
-objekt. Dessutom härrör de tillgängliga layoutalgoritmerna i första hand från igraph
. Slutligen introducerar ggraph
ett särskilt ggplot
-tema som ger bättre standardvärden för nätverksgrafer än de normala ggplot
-standardvärdena. ggraph
-temat kan ställas in för en serie grafer med kommandot set_graph_style()
som körs innan graferna plottas eller genom att använda theme_graph()
i de enskilda graferna. Här kommer jag att använda den senare metoden.
Låt oss se hur en grundläggande ggraph
-plott ser ut. Plotten börjar med ggraph()
och data. Därefter lägger jag till grundläggande kant- och nodgeoms. Inga argument behövs inom kant- och nodgeometrarna, eftersom de tar informationen från de data som tillhandahålls i ggraph()
.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Som du kan se liknar strukturen på kommandot ggplot
med de separata lagren som läggs till med +
-tecknet. Den grundläggande ggraph
-grafen ser likadan ut som i network
och igraph
, om inte ännu slätstrukenare, men vi kan använda liknande kommandon som ggplot
för att skapa en mer informativ graf. Vi kan visa kanternas ”vikt” – eller hur många brev som skickats längs varje rutt – genom att använda width i geom_edge_link()
-funktionen. För att få linjens bredd att förändras i enlighet med variabeln vikt placerar vi argumentet i en aes()
-funktion. För att styra den maximala och minimala bredden på kanterna använder jag scale_edge_width()
och ställer in en range
. Jag väljer en relativt liten bredd för minimum, eftersom det finns en betydande skillnad mellan det maximala och minimala antalet brev som skickas längs linjerna. Vi kan också märka noderna med platsernas namn eftersom det finns relativt få noder. Bekvämt nog har geom_node_text()
ett repel-argument som ser till att etiketterna inte överlappar noderna på ett sätt som liknar paketet ggrepel. Jag lägger till lite transparens till kanterna med argumentet alpha. Jag använder också labs()
för att byta etikett till 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()
Inom de layoutalternativ som tillhandahålls av igraph
implementerar ggraph
också sina egna layouter. Du kan till exempel använda ggraph's
s begrepp cirkularitet för att skapa bågdiagram. Här lägger jag noderna i en horisontell linje och låter kanterna ritas som bågar. Till skillnad från den föregående plottningen anger detta diagram kanternas riktning.9 Kanterna ovanför den horisontella linjen rör sig från vänster till höger, medan kanterna under linjen rör sig från höger till vänster. I stället för att lägga till punkter för noderna tar jag bara med etikettnamnen. Jag använder samma bredd estetiskt för att beteckna skillnaden i vikt för varje kant. Observera att jag i den här grafen använder ett igraph
-objekt som data för grafen, vilket inte gör någon praktisk skillnad.
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()
Interaktiva nätverksgrafer med visNetwork och networkD3
Med paketuppsättningen htmlwidgets kan man använda R för att skapa interaktiva JavaScript-visualiseringar. Här kommer jag att visa hur man gör grafer med paketen visNetwork
och networkD3
. Dessa två paket använder olika JavaScript-bibliotek för att skapa sina grafer. visNetwork
använder vis.js, medan networkD3
använder det populära visualiseringsbiblioteket d3 för att göra sina grafer. En svårighet med att arbeta med både visNetwork
och networkD3
är att de förväntar sig att kantlistor och nodlistor ska använda en specifik nomenklatur. Ovanstående datamanipulering överensstämmer med den grundläggande strukturen för visNetwork
, men en del arbete måste göras för networkD3
. Trots denna olägenhet har båda paketen ett brett utbud av grafiska möjligheter och båda kan arbeta med igraph
-objekt och layouter.
library(visNetwork)library(networkD3)
visNetwork
Funktionen visNetwork()
använder en lista med noder och en lista med kanter för att skapa en interaktiv graf. Nodes-listan måste innehålla en ”id”-kolumn och edge-listan måste ha kolumnerna ”from” och ”to”. Funktionen visar också nodernas etiketter med hjälp av namnen på städerna i kolumnen ”label” i nodenlistan. Den resulterande grafen är rolig att leka med. Du kan flytta noderna och grafen kommer att använda en algoritm för att hålla noderna på rätt avstånd. Du kan också zooma in och ut på grafen och flytta runt den för att åter centrera den.
visNetwork(nodes, edges)
visNetwork
kan använda igraph
layouter, vilket ger en stor variation av möjliga layouter. Dessutom kan du använda visIgraph()
för att plotta ett igraph
-objekt direkt. Här kommer jag att hålla mig till nodes
och edges
-arbetsflödet och använda en igraph
-layout för att anpassa grafen. Jag kommer också att lägga till en variabel för att ändra bredden på kanten som vi gjorde med ggraph
. visNetwork()
använder kolumnnamn från listorna över kanter och noder för att plotta nätverksattribut i stället för argument inom funktionsanropet. Detta innebär att det är nödvändigt att göra en viss datamanipulering för att få fram en kolumn ”bredd” i kantlistan. Width-attributet för visNetwork()
skalar inte värdena, så vi måste göra detta manuellt. Båda dessa åtgärder kan göras med funktionen mutate()
och lite enkel aritmetik. Här skapar jag en ny kolumn i edges
och skalar viktvärdena genom att dividera med 5. Genom att lägga till 1 till resultatet får vi ett sätt att skapa en minsta bredd.
edges <- mutate(edges, width = weight/5 + 1)
När detta är gjort kan vi skapa en graf med variabla kantbredder. Jag väljer också en layoutalgoritm från igraph
och lägger till pilar till kanterna och placerar dem i mitten av kanten.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
nätverkD3
Det krävs lite mer arbete för att förbereda data för att skapa en networkD3
graf. För att skapa en networkD3
-graf med en kant- och nodlista krävs att ID:erna är en serie numeriska heltal som börjar med 0. För närvarande börjar nod-ID:erna för våra data med 1, så vi måste göra lite datamanipulation. Det är möjligt att numrera om noderna genom att subtrahera 1 från ID-kolumnerna i nodes
och edges
dataramar. Återigen kan detta göras med funktionen mutate()
. Målet är att återskapa de aktuella kolumnerna, samtidigt som man subtraherar 1 från varje ID. Funktionen mutate()
fungerar genom att skapa en ny kolumn, men vi kan låta den ersätta en kolumn genom att ge den nya kolumnen samma namn som den gamla kolumnen. Här namnger jag de nya dataramerna med ett d3-suffix för att skilja dem från de tidigare nodes
och edges
dataramerna.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Det är nu möjligt att rita en networkD3
graf. Till skillnad från visNetwork()
använder forceNetwork()
-funktionen en rad argument för att justera grafen och plotta nätverksattribut. Argumenten ”Links” och ”Nodes” tillhandahåller data för plotten i form av listor över kanter och noder. Funktionen kräver också argumenten ”NodeID” och ”Group”. De data som används här har inga grupperingar och därför låter jag bara varje nod vara en egen grupp, vilket i praktiken innebär att noderna alla kommer att ha olika färger. Dessutom talar nedanstående om för funktionen att nätverket har fälten ”Source” och ”Target” och därmed är riktat. Jag inkluderar i den här grafen ett ”Value”, som skalar bredden på kanterna i enlighet med kolumnen ”weight” i kantlistan. Slutligen lägger jag till några estetiska justeringar för att göra noderna ogenomskinliga och öka teckenstorleken på etiketterna för att förbättra läsbarheten. Resultatet är mycket likt den första visNetwork()
-plotten som jag skapade men med olika estetiska stilar.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
En av de största fördelarna med networkD3
är att den implementerar ett d3-stiliserat Sankey-diagram. Ett Sankey-diagram passar bra för de brev som skickades till Daniel år 1585. Det finns inte alltför många noder i data, vilket gör det lättare att visualisera flödet av brev. För att skapa ett Sankey-diagram används funktionen sankeyNetwork()
, som tar många av samma argument som forceNetwork()
. Det här diagrammet kräver inget gruppargument, och den enda andra förändringen är tillägget av en ”enhet”. Detta ger en etikett för de värden som dyker upp i ett verktygstips när markören svävar över ett diagramelement. 10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Fortsatt läsning om nätverksanalys
Detta inlägg har försökt ge en allmän introduktion till hur man skapar och plottar objekt av nätverkstyp i R med hjälp av paketen network
, igraph
, tidygraph
och ggraph
för statiska plottar och visNetwork
och networkD3
för interaktiva plottar. Jag har presenterat denna information från en icke-specialist i nätverksteori. Jag har endast täckt en mycket liten del av R:s möjligheter till nätverksanalys. I synnerhet har jag inte diskuterat den statistiska analysen av nätverk. Lyckligtvis finns det en uppsjö av resurser om nätverksanalys i allmänhet och i R i synnerhet.
Den bästa introduktionen till nätverk som jag har hittat för den oinvigde är Katya Ognyanovas Network Visualization with R. Denna presenterar både en användbar introduktion till de visuella aspekterna av nätverk och en mer djupgående handledning om hur man skapar nätverksdiagram i R. Ognyanova använder i första hand igraph
, men hon introducerar också interaktiva nätverk.
Det finns två relativt nya böcker som publicerats om nätverksanalys med R av Springer. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) är en mycket användbar introduktion till nätverksanalys med R. Luke täcker både statnet-paketet och igragh
. Innehållet är genomgående på en mycket lättillgänglig nivå. Mer avancerad är Eric D. Kolaczyk och Gábor Csárdis, Statistical Analysis of Network Data with R (2014). Kolaczyk och Csárdis bok använder huvudsakligen igraph
, eftersom Csárdi är den primära upprätthållaren av igraph
-paketet för R. Den här boken går djupare in på avancerade ämnen om statistisk analys av nätverk. Trots användningen av ett mycket tekniskt språk är de fyra första kapitlen i allmänhet lätt att närma sig från en icke-specialist.
Listan som upprättats av François Briatte är en bra översikt över resurser om nätverksanalys i allmänhet. Den serie inlägg i Networks Demystified av Scott Weingart är också väl värd att läsa.
-
Ett exempel på intresset för nätverksanalys inom digital humaniora är det nystartade Journal of Historical Network Research. ︎
-
För en bra beskrivning av objektklassen
network
, inklusive en diskussion om dess förhållande till objektklassenigraph
, se Carter Butts, ”network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Detta är den specifika struktur som
visNetwork
förväntar sig, samtidigt som den överensstämmer med de andra paketens allmänna förväntningar. ︎ -
Detta är den förväntade ordningen för kolumnerna för några av de nätverkspaket som jag kommer att använda nedan. ︎
-
ungroup()
är inte strikt nödvändigt i det här fallet. Om du inte avgrupperar dataramen är det dock inte möjligt att ta bort kolumnerna ”source” och ”destination”, vilket jag gör senare i skriptet. ︎ -
Thomas M. J. Fruchterman och Edward M. Reingold, ”Graph Drawing by Force-Directed Placement”, Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
Funktionen
rm()
är användbar om din arbetsmiljö i R blir oorganiserad, men du vill inte rensa hela miljön och börja om från början. ︎ -
Sambandet mellan
tbl_graph
ochigraph
-objekten liknar det mellantibble
ochdata.frame
-objekten. ︎ -
Det är möjligt att låta
ggraph
rita pilar, men jag har inte visat det här. ︎ -
Det kan ta lite tid innan verktygstipsen visas. ︎
Lämna ett svar