Bevezetés a hálózatelemzésbe az R segítségével
On november 4, 2021 by adminA hálózatelemzés a legkülönbözőbb területeken egyre népszerűbb eszközzé vált a tudósok számára, hogy a legkülönbözőbb szereplők közötti kapcsolatok komplexitásával foglalkozzanak. A hálózatelemzés ígérete az, hogy a szereplők közötti kapcsolatokra helyezi a jelentőséget, ahelyett, hogy a szereplőket elszigetelt egységeknek tekintené. A komplexitás hangsúlyozása, valamint a hálózatok különböző aspektusainak mérésére szolgáló különféle algoritmusok létrehozása a hálózatelemzést a digitális bölcsészettudományok központi eszközévé teszi.1 Ez a bejegyzés bevezetést nyújt a hálózatokkal való munkába az R-ben, Daniel van der Meulen 1585-ös levelezésében szereplő városok hálózatának példáján keresztül.
Számos olyan alkalmazás létezik, amelyet hálózatelemzésre és hálózati gráfok létrehozására terveztek, mint például a gephi és a cytoscape. Bár nem kifejezetten erre tervezték, az R a hálózatelemzés hatékony eszközévé fejlődött. Az R erőssége az önálló hálózatelemző szoftverekkel szemben háromszoros. Először is, az R lehetővé teszi a reprodukálható kutatást, ami a GUI-alkalmazásokkal nem lehetséges. Másodszor, az R adatelemző képessége robusztus eszközöket biztosít az adatok manipulálásához, hogy előkészítse azokat a hálózatelemzésre. Végül pedig egyre több olyan csomag létezik, amelyek célja, hogy az R-t teljes körű hálózatelemző eszközzé tegyék. Az R jelentős hálózatelemző csomagjai közé tartozik a statnet csomagcsomag és a igraph
. Emellett Thomas Lin Pedersen nemrégiben kiadta a tidygraph
és ggraph
csomagokat, amelyek a igraph
teljesítményét a tidyverse munkafolyamatnak megfelelő módon használják ki. Az R segítségével interaktív hálózati gráfok is készíthetők a htmlwidgets keretrendszerrel, amely az R kódot JavaScriptre fordítja.
Ez a bejegyzés a hálózatelemzés alapszókincsének rövid bemutatásával kezdődik, majd az adatoknak a hálózatelemzéshez szükséges megfelelő struktúrába hozásának folyamatát tárgyalja. A hálózatelemző csomagok mindegyike saját objektumosztályokat valósított meg. Ebben a bejegyzésben bemutatom, hogyan lehet létrehozni a statnet csomagcsomagok specifikus objektumosztályait a network
csomaggal, valamint a igraph
és a tidygraph
csomaghoz, amely a igraph
implementáción alapul. Végül rátérek az interaktív gráfok létrehozására a vizNetwork
és networkD3
csomagokkal.
Hálózatelemzés: Csomópontok és élek
A hálózatok két elsődleges szempontja a különálló entitások sokasága és a köztük lévő kapcsolatok. A szókincs kissé technikai, sőt, a különböző tudományágak, csomagok és szoftverek között ellentmondásos is lehet. Az entitásokat a gráf csomópontjainak vagy csúcsainak, míg a kapcsolatokat éleknek vagy linkeknek nevezzük. Ebben a bejegyzésben főként a csomópontok és élek nómenklatúráját fogom használni, kivéve, amikor olyan csomagokat tárgyalok, amelyek más szókincset használnak.
A hálózatelemző csomagoknak szükségük van arra, hogy az adatok egy bizonyos formában legyenek, hogy létrehozzák az egyes csomagok által használt speciális típusú objektumokat. A network
, igraph
és tidygraph
objektumosztályai mind szomszédsági mátrixokon, más néven szociomatrixokon alapulnak.2 Az szomszédsági mátrix egy négyzetmátrix, amelyben az oszlopok és sorok nevei a hálózat csomópontjai. A mátrixon belül az 1 azt jelzi, hogy a csomópontok között kapcsolat van, a 0 pedig azt, hogy nincs kapcsolat. A szomszédsági mátrixok az adatkeretektől nagyon eltérő adatszerkezetet valósítanak meg, és nem illeszkednek a korábbi bejegyzéseimben használt tidyverse munkafolyamatba. Segítségképpen a speciális hálózati objektumok létrehozhatók egy éllistás adatkeretből is, amelyek illeszkednek a tidyverse munkafolyamatba. Ebben a bejegyzésben ragaszkodom a tidyverse adatelemzési technikáihoz az éllisták létrehozásához, amelyeket aztán a network
, igraph
és tidygraph
speciális objektumosztályokká alakítok át.
Az éllista olyan adatkeret, amely legalább két oszlopot tartalmaz, egy oszlopban a kapcsolat forrását jelentő csomópontokat, egy másik oszlopban pedig a kapcsolat célját jelentő csomópontokat. Az adatokban szereplő csomópontokat egyedi azonosítókkal azonosítjuk. Ha a forrás és a cél közötti különbségtétel értelmes, akkor a hálózat irányított. Ha a megkülönböztetés nem értelmes, a hálózat irányítatlan. A városok között küldött levelek példájánál a forrás és a cél között egyértelműen értelmes a különbségtétel, így a hálózat irányított. Az alábbi példákban a forrás oszlopot “from”-nak, a cél oszlopot pedig “to”-nak fogom nevezni. A csomópontok azonosítójaként eggyel kezdődő egész számokat fogok használni.3 Az élek listája tartalmazhat további oszlopokat is, amelyek az élek tulajdonságait írják le, például egy él nagyságrendi szempontját. Ha az éleknek van nagyság attribútuma, a gráf súlyozottnak tekinthető.
Az éllisták tartalmazzák a hálózati objektumok létrehozásához szükséges összes információt, de néha előnyösebb egy külön csomópontlistát is létrehozni. A legegyszerűbb esetben a csomópontlista egy adatkeret egyetlen oszloppal – amelyet “id”-ként fogok megjelölni -, amely az éllistában található csomópontok azonosítóit sorolja fel. A külön csomópontlista létrehozásának előnye, hogy az adatkerethez attribútumoszlopokat is hozzáadhatunk, például a csomópontok nevét vagy bármilyen csoportosítást. Az alábbiakban egy példát adok a tibble()
függvénnyel létrehozott minimális él- és csomópontlistákra.
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
Ezt összehasonlítom egy szomszédsági mátrixszal, amely ugyanazokat az adatokat tartalmazza.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Élom- és csomópontlisták létrehozása
A Daniel van der Meulen által 1585-ben kapott levelek adatbázisából hálózati objektumok létrehozásához egy él- és egy csomópontlistát is készítek. Ehhez szükség lesz a dplyr csomag használatára, hogy a Danielnek küldött levelek adatkeretét manipuláljuk, és két adatkeretre vagy tibbletre osszuk fel az él- és csomópontlisták szerkezetével. Ebben az esetben a csomópontok azok a városok lesznek, ahonnan Dániel levelezőpartnerei leveleket küldtek neki, és azok a városok, ahonnan Dániel leveleket kapott. A csomópontlista tartalmazni fog egy “címke” oszlopot, amely a városok nevét tartalmazza. Az éllistának lesz egy attribútum oszlopa is, amely az egyes várospárok között küldött levelek mennyiségét mutatja. Az objektumok létrehozásának munkafolyamata hasonló lesz ahhoz, amit az R-be való rövid bevezetésemben és az R-rel való geokódolásban használtam. Ha szeretné követni, a GitHubon megtalálja az ebben a bejegyzésben használt adatokat és a felhasznált R-szkriptet.
Az első lépés az tidyverse
könyvtár betöltése az adatok importálásához és manipulálásához. A letters
adatkeret kinyomtatása azt mutatja, hogy négy oszlopot tartalmaz: “író”, “forrás”, “célállomás” és “dátum”. Ebben a példában csak a “forrás” és a “célállomás” oszlopokkal fogunk foglalkozni.
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
Központlista
A munkafolyamat a csomópontlista létrehozásához hasonló ahhoz, amit egy korábbi bejegyzésben a városok listájának megszerzéséhez használtam az adatok geokódolásához. Mind a “forrás”, mind a “cél” oszlopokból meg akarjuk szerezni a különböző városokat, majd az ezekből az oszlopokból származó információkat összekapcsoljuk. Az alábbi példában kissé megváltoztatom a parancsokat az előző bejegyzésben használtakhoz képest, hogy a városneveket tartalmazó oszlopok neve ugyanaz legyen mind a sources
, mind a destinations
adatkeret esetében, hogy egyszerűsítse a full_join()
függvényt. A városneveket tartalmazó oszlopot “label”-nek nevezem át, hogy átvegyem a hálózatelemző csomagok által használt szókincset.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
Az egyedi helyeket tartalmazó oszlopot tartalmazó egyetlen adatkeret létrehozásához teljes összekapcsolást kell használnunk, mivel mind a levelek forrásaiból, mind a célállomásokból az összes egyedi helyet fel akarjuk venni.
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
Ez egy egy változóval rendelkező adatkeretet eredményez. Az adatkeretben szereplő változó azonban nem igazán az, amit keresünk. A “címke” oszlop tartalmazza a csomópontok nevét, de szeretnénk, ha minden városhoz egyedi azonosítót is kapnánk. Ezt úgy érhetjük el, hogy a nodes
adatkerethez hozzáadunk egy “id” oszlopot, amely számokat tartalmaz egytől addig a számig, amennyi az adatkeret összes sorának száma. Ehhez a munkafolyamathoz hasznos függvény a rowid_to_column()
, amely egy oszlopot ad hozzá a sorok azonosítóinak értékeivel, és az oszlopot az adatkeret elejére helyezi.4 Vegye figyelembe, hogy a rowid_to_column()
egy pipázható parancs, így a full_join()
és az “id” oszlop hozzáadása egyetlen parancsban is elvégezhető. Az eredmény egy csomópontlista lesz egy azonosító oszloppal és egy címkeattribútummal.
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
Szegmenslista
A szegmenslista létrehozása hasonló a fentiekhez, de bonyolítja, hogy egy helyett két azonosító oszloppal kell foglalkoznunk. Szeretnénk létrehozni egy súlyoszlopot is, amely az egyes csomópontok között küldött levelek mennyiségét jegyzi. Ehhez ugyanazt a group_by()
és summarise()
munkafolyamatot fogom használni, amelyet a korábbi bejegyzésekben már tárgyaltam. A különbség itt az, hogy az adatkeretet nem egy, hanem két oszlop – “forrás” és “cél” – szerint akarjuk csoportosítani. Korábban a csoportonkénti megfigyelések számát számláló oszlopot “count”-nak neveztem el, itt azonban átveszem a hálózatelemzés nómenklatúráját, és “weight”-nek nevezem. A csővezeték utolsó parancsa eltávolítja a group_by()
függvény által létrehozott adatkeret csoportosítását. Ez megkönnyíti a kapott per_route
adatkeret akadálytalan manipulálását. 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
A csomópontlistához hasonlóan a per_route
most már a kívánt alapformával rendelkezik, de ismét az a probléma, hogy a “forrás” és a “cél” oszlopok azonosítók helyett címkéket tartalmaznak. Azt kell tennünk, hogy a nodes
-ben hozzárendelt azonosítókat összekapcsoljuk a “forrás” és a “cél” oszlopok egyes helyeihez. Ezt egy másik join függvénnyel érhetjük el. Valójában két egyesítést kell végrehajtani, egyet a “forrás” oszlophoz és egyet a “cél” oszlophoz. Ebben az esetben egy left_join()
-et fogok használni, amelynek bal oldali adatkerete per_route
lesz, mert meg akarjuk tartani a per_route
-ben lévő sorok számát. A left_join
elkészítése közben a nodes
-ből áthozott két “id” oszlopot is át akarjuk nevezni. A “forrás” oszlopot használó joinhoz átnevezem az oszlopot “from”-ra. A “cél” joinból áthozott oszlopot átnevezzük “to”-ra. Lehetséges lenne mindkét join egyetlen parancsban elvégezni a pipe használatával. Az egyértelműség kedvéért azonban két külön parancsban fogom végrehajtani az egyesítéseket. Mivel az egyesítés két parancsban történik, vegye észre, hogy a csővezeték elején lévő adatkeret per_route
-ről edges
-re változik, amelyet az első parancs hoz létre.
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)
Most, hogy a edges
rendelkezik “from” és “to” oszlopokkal a csomópontok azonosítóival, át kell rendeznünk az oszlopokat, hogy a “from” és “to” az adatkeret bal oldalán legyen. Jelenleg a edges
adatkeret még mindig tartalmazza a “forrás” és a “cél” oszlopokat az azonosítóknak megfelelő városok neveivel. Ezek az adatok azonban feleslegesek, mivel a nodes
-ben már szerepelnek. Ezért a select()
függvényben csak a “honnan”, “hová” és “súly” oszlopokat veszem fel.
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
A edges
adatkeret nem tűnik túl látványosnak; három oszlopnyi egész számot tartalmaz. A edges
azonban a nodes
-vel kombinálva minden szükséges információt megad ahhoz, hogy a network
, igraph
és tidygraph
csomagokkal hálózati objektumokat hozzunk létre.
Hálózati objektumok létrehozása
A network
, igraph
és tidygraph
csomagok hálózati objektumosztályai szorosan kapcsolódnak egymáshoz. Lehetőség van egy network
objektum és egy igraph
objektum közötti fordításra. A legjobb azonban, ha a két csomagot és objektumaikat külön tartjuk. Valójában a network
és a igraph
képességei olyan mértékben átfedik egymást, hogy a legjobb gyakorlat, ha egyszerre csak az egyik csomag van betöltve. Először a network
csomagot tekintem át, majd áttérek a igraph
és tidygraph
csomagokra.
network
library(network)
A network
objektum létrehozásához használt függvény a network()
. A parancs nem különösebben egyszerű, de bármikor beírhatod a konzolba a ?network()
, ha összezavarodnál. Az első argumentum – a dokumentáció szerint – “egy mátrix, amely a hálózat szerkezetét adja meg szomszédsági, incidencia vagy éllista formájában”. A nyelv demonstrálja a mátrixok jelentőségét a hálózatelemzésben, de mátrix helyett éllistát kapunk, ami ugyanazt a szerepet tölti be. A második argumentum a csúcsok attribútumainak listája, amely megfelel a csomópontok listájának. Vegyük észre, hogy a network
csomag a csomópontok helyett a csúcsok nomenklatúráját használja. Ugyanez igaz a igraph
csomagra is. Ezután meg kell adnunk az első két argumentumba beírt adatok típusát azzal, hogy megadjuk, hogy a matrix.type
egy "edgelist"
. Végül a ignore.eval
-öt FALSE
-re állítjuk, hogy a hálózatunk súlyozott legyen, és figyelembe vegye az egyes útvonalak mentén található betűk számát.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
A network()
függvény által létrehozott objektum típusát a routes_network
class()
függvénybe helyezésével láthatjuk.
class(routes_network)#> "network"
A routes_network
kiírása a konzolra megmutatja, hogy az objektum szerkezete egészen más, mint az olyan adatkeret stílusú objektumoké, mint a edges
és a nodes
. A nyomtatási parancs olyan információkat tár fel, amelyek kifejezetten a hálózatelemzéshez vannak meghatározva. Azt mutatja, hogy a routes_network
-ben 13 csúcs vagy csomópont és 15 él van. Ezek a számok megfelelnek a nodes
és a edges
sorainak. Azt is láthatjuk, hogy a csúcsok és élek egyaránt tartalmaznak olyan attribútumokat, mint a címke és a súly. Még több információt kaphatunk, beleértve az adatok szociomátrixát is, ha beírjuk a 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
A betűhálózatunk kezdetleges, ha nem is túlságosan esztétikus grafikonját. Mind a network
, mind a igraph
csomag az R alapplotrendszerét használja. Az alapplotok konvenciói jelentősen eltérnek a ggplot2 konvencióitól – amelyeket korábbi bejegyzésekben már tárgyaltam -, ezért maradok a meglehetősen egyszerű plotoknál, ahelyett, hogy belemennék az összetett plotok létrehozásának részleteibe az alap R-rel. Ebben az esetben az egyetlen változtatás, amit a network
csomag alapértelmezett plot()
függvényén végrehajtok, hogy a vertex.cex
argumentummal növelem a csomópontok méretét, hogy a csomópontok jobban láthatóak legyenek. Még ezzel a nagyon egyszerű grafikonnal is megtudhatunk már valamit az adatokról. A grafikon egyértelművé teszi, hogy az adatoknak két fő csoportosulása vagy klasztere van, amelyek megfelelnek annak az időnek, amelyet Daniel 1585 első háromnegyedévében Hollandiában töltött, illetve a szeptemberi Brémába költözése után.
plot(routes_network, vertex.cex = 3)
A plot()
függvény a network
objektummal a Fruchterman és Reingold algoritmus segítségével dönt a csomópontok elhelyezéséről.6 Az mode
argumentummal megváltoztathatja az elrendezési algoritmust. Az alábbiakban a csomópontokat kör alakban rendezem el. Ez nem egy különösebben hasznos elrendezés ehhez a hálózathoz, de képet ad néhány lehetőségről.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Kezdjük most a igraph
csomag tárgyalását. Először is meg kell tisztítanunk a környezetet az R-ben a network
csomag eltávolításával, hogy ne zavarja a igraph
parancsokat. Akár a routes_network
csomagot is eltávolíthatjuk, mivel a továbbiakban nem fogjuk használni. A network
csomagot a detach()
függvénnyel, a routes_network
csomagot pedig a rm()
függvénnyel távolíthatjuk el.7 Ezek után nyugodtan betölthetjük a igraph
csomagot.
detach(package:network)rm(routes_network)library(igraph)
Az éllista adatkeretből egy igraph
objektum létrehozásához használhatjuk a graph_from_data_frame()
függvényt, ami egy kicsit egyszerűbb, mint a network()
. A graph_from_data_frame()
függvénynek három argumentuma van: d, csúcsok és irányított. Itt a d az éllistára, a vertices a csomópontlistára utal, a directed pedig TRUE
vagy FALSE
lehet attól függően, hogy az adat irányított vagy irányítatlan.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
A graph_from_data_frame()
által létrehozott igraph
objektumot a konzolon kinyomtatva hasonló információkat kapunk, mint egy network
objektumról, bár a struktúra rejtélyesebb.
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
Az objektumra vonatkozó legfontosabb információkat a DNW- 13 15 --
tartalmazza. Ebből kiderül, hogy a routes_igraph
egy irányított hálózat (D), amely rendelkezik névattribútummal (N) és súlyozott (W). A W utáni kötőjel azt mondja, hogy a gráf nem kétrészes. Az ezt követő számok a gráf csomópontjainak, illetve éleinek számát írják le. Ezután a name (v/c), label (v/c), weight (e/n)
a gráf attribútumairól ad információt. Van két vertex attribútum (v/c) a név – azaz az azonosítók – és a címkék, valamint egy él attribútum (e/n) a súly. Végül az összes él kiírása következik.
Az network
csomaghoz hasonlóan a plot()
függvény segítségével egy igraph
objektummal is létrehozhatunk egy gráfot. Az egyetlen változtatás, amit itt az alapértelmezetthez képest végzek, az a nyilak méretének csökkentése. Alapértelmezés szerint a igraph
a csomópontokat a címkeoszloppal címkézi, ha van, vagy az azonosítókkal.
plot(routes_igraph, edge.arrow.size = 0.2)
Az előző network
grafikonhoz hasonlóan a igraph
plot alapértelmezése sem különösebben esztétikus, de a plotok minden aspektusa manipulálható. Itt csak a csomópontok elrendezését szeretném megváltoztatni, hogy a Michael Schmuhl által létrehozott graphopt algoritmust használjam. Ez az algoritmus megkönnyíti a Haarlem, Antwerpen és Delft – a levelezési hálózat három legjelentősebb helyszíne – közötti kapcsolat áttekintését azáltal, hogy jobban széthúzza őket.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph és ggraph
A tidygraph
és ggraph
csomagok újoncok a hálózatelemzésben, de a két csomag együtt valódi előnyöket nyújt a network
és igraph
csomagokkal szemben. A tidygraph
és a ggraph
egy kísérletet jelentenek arra, hogy a hálózatelemzés bekerüljön a tidyverse munkafolyamatba. A tidygraph
egy olyan hálózati objektum létrehozásának módját biztosítja, amely jobban hasonlít egy tibble-re vagy adatkeretre. Ez lehetővé teszi a dplyr
számos függvényének használatát a hálózati adatok manipulálására. A ggraph
módot ad a hálózati grafikonok ábrázolására a ggplot2
konvencióinak és teljesítményének felhasználásával. Más szóval, a tidygraph
és a ggraph
lehetővé teszi, hogy a hálózati objektumokat olyan módon kezelje, amely jobban összhangban van a tibbletekkel és adatkeretekkel való munkához használt parancsokkal. A tidygraph
és ggraph
igazi ígérete azonban az, hogy kihasználják a igraph
erejét. Ez azt jelenti, hogy a tidygraph
és ggraph
használatával keveset áldozunk fel a igraph
hálózatelemzési lehetőségeiből.
Azzal kell kezdenünk, mint mindig, hogy betöltjük a szükséges csomagokat.
library(tidygraph)library(ggraph)
Először hozzunk létre egy hálózati objektumot a tidygraph
segítségével, amelyet tbl_graph
-nek hívunk. Egy tbl_graph
két tibble-ből áll: egy élek tibble-ből és egy csomópontok tibble-ből. Kényelmes módon a tbl_graph
objektumosztály egy igraph
objektum körüli burkolat, ami azt jelenti, hogy az alapját tekintve egy tbl_graph
objektum lényegében egy igraph
objektum.8 A tbl_graph
és a igraph
objektumok közötti szoros kapcsolat két fő módot eredményez egy tbl_graph
objektum létrehozására. Az első az él- és csomópontlista használata, a tbl_graph()
használatával. A függvény argumentumai majdnem azonosak a graph_from_data_frame()
-éval, csak az argumentumok nevei változnak kissé.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
A tbl_graph
objektum létrehozásának második módja egy igraph
vagy network
objektum átalakítása a as_tbl_graph()
használatával. Így a routes_igraph
objektumot átalakíthatjuk tbl_graph
objektummá.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Most, hogy létrehoztunk két tbl_graph
objektumot, vizsgáljuk meg őket a class()
függvénnyel. Ebből kiderül, hogy routes_tidy
és routes_igraph_tidy
a "tbl_graph" "igraph"
osztályba tartozó objektumok, míg routes_igraph
a "igraph"
objektumosztályba tartozik.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
A tbl_graph
objektumot a konzolra kiírva drasztikusan más eredményt kapunk, mint egy igraph
objektum esetében. A kimenet egy normál tibble-hez hasonló.
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
A routes_tidy
kinyomtatása azt mutatja, hogy ez egy tbl_graph
objektum 13 csomóponttal és 15 éllel. A parancs kiírja a “Node Data” első hat sorát és az “Edge Data” első három sorát is. Vegyük észre azt is, hogy kijelzi, hogy a Node Data aktív. Az aktív tibble fogalma egy tbl_graph
objektumon belül lehetővé teszi, hogy az adatokat egyszerre egy tibble-ben manipuláljuk. A csomópont tibble alapértelmezés szerint aktív, de a activate()
függvénnyel megváltoztathatjuk, hogy melyik tibble legyen aktív. Így ha át akarom rendezni az élek tibble sorait, hogy először a legnagyobb “súllyal” rendelkező sorokat soroljam fel, akkor a activate()
, majd a arrange()
funkciót használhatom. Itt egyszerűen csak kiírom az eredményt ahelyett, hogy elmenteném.
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
Mivel nem kell tovább manipulálnunk a routes_tidy
-t, a ggraph
segítségével ábrázolhatjuk a grafikont. A ggmap-hoz hasonlóan a ggraph
is a ggplot2
kiterjesztése, ami megkönnyíti az alapvető ggplot
ismeretek átvitelét a hálózati ábrák készítésére. Mint minden hálózati gráf esetében, a ggraph
plotnak is három fő aspektusa van: csomópontok, élek és elrendezések. A ggraph csomag vignettái segítőkészen tárgyalják a ggraph
-plotok alapvető aspektusait. A ggraph
speciális geomokat ad a ggplot
geomok alapkészletéhez, amelyeket kifejezetten hálózatokhoz terveztek. Így létezik egy geom_node
és egy geom_edge
geomkészlet. Az alapvető ábrázoló funkció a ggraph()
, amely a grafikonhoz használandó adatokat és a kívánt elrendezés típusát veszi fel. A ggraph()
mindkét argumentuma a igraph
köré épül. Ezért a ggraph()
használhat akár egy igraph
objektumot, akár egy tbl_graph
objektumot. Ezenkívül a rendelkezésre álló elrendezési algoritmusok elsősorban a igraph
-ból származnak. Végül a ggraph
bevezet egy speciális ggplot
témát, amely jobb alapértelmezéseket biztosít a hálózati gráfok számára, mint a normál ggplot
alapértelmezések. A ggraph
téma beállítható egy plotsorozathoz a set_graph_style()
paranccsal, amelyet a grafikonok kirajzolása előtt futtatunk, vagy a theme_graph()
használatával az egyes plotokban. Itt az utóbbi módszert fogom használni.
Lássuk, hogyan néz ki egy alapvető ggraph
plot. A plot a ggraph()
és az adatokkal kezdődik. Ezután hozzáadom az alapvető él- és csomópontgeomákat. Az él- és csomópontgeomokon belül nincs szükség argumentumokra, mert azok a ggraph()
-ben megadott adatokból veszik az információt.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Amint látható, a parancs felépítése hasonló a ggplot
-hoz, a +
jellel hozzáadott külön rétegekkel. Az alapvető ggraph
grafikon hasonlóan néz ki, mint a network
és a igraph
, ha nem még egyszerűbben, de a ggplot
-hoz hasonló parancsokkal még informatívabb grafikont készíthetünk. Az élek “súlyát” – vagyis az egyes útvonalakon küldött levelek mennyiségét – a geom_edge_link()
függvényben a width használatával mutathatjuk meg. Ahhoz, hogy a vonal szélessége a súly változónak megfelelően változzon, az argumentumot egy aes()
függvényen belül helyezzük el. Az élek maximális és minimális szélességének szabályozásához a scale_edge_width()
funkciót használom, és egy range
-et állítok be. A minimumnak viszonylag kis szélességet választok, mert az útvonalak mentén küldött betűk maximális és minimális száma között jelentős különbség van. A csomópontokat a helyszínek nevével is felcímkézhetjük, mivel viszonylag kevés csomópont van. Kényelmes módon a geom_node_text()
-hez tartozik egy repel argumentum, amely a ggrepel csomaghoz hasonlóan biztosítja, hogy a címkék ne fedjék át a csomópontokat. Az élekhez egy kis átlátszóságot adok az alpha argumentummal. A labs()
-t arra is használom, hogy a “Levelek” legendát átcímkézzem.
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()
A igraph
által biztosított elrendezési lehetőségek mellett a ggraph
saját elrendezéseket is megvalósít. Például a ggraph's
körkörösség koncepcióját használhatja ívdiagramok létrehozásához. Itt a csomópontokat vízszintes vonalban elrendezem, és az éleket ívként rajzoltatom. Az előző ábrázolással ellentétben ez a grafikon jelzi az élek irányultságát.9 A vízszintes vonal feletti élek balról jobbra, míg a vonal alatti élek jobbról balra mozognak. Ahelyett, hogy pontokat adnék a csomópontokhoz, csak a címkék nevét adom meg. Az egyes élek súlykülönbségének jelölésére azonos szélességű esztétikumot használok. Vegyük észre, hogy ebben a grafikonban egy igraph
objektumot használok a grafikon adataként, ami gyakorlatilag nem jelent különbséget.
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()
Interaktív hálózati grafikonok a visNetwork és networkD3 segítségével
A htmlwidgets csomagkészlet lehetővé teszi, hogy az R segítségével interaktív JavaScript vizualizációkat hozzunk létre. Itt bemutatom, hogyan készíthetünk grafikonokat a visNetwork
és networkD3
csomagokkal. Ez a két csomag különböző JavaScript-könyvtárakat használ a grafikonok létrehozásához. A visNetwork
a vis.js-t használja, míg a networkD3
a népszerű d3 vizualizációs könyvtárat használja a grafikonok elkészítéséhez. A visNetwork
és a networkD3
csomagokkal való munka egyik nehézsége, hogy mindkettő elvárja, hogy az él- és csomópontlisták sajátos nómenklatúrát használjanak. A fenti adatmanipuláció a visNetwork
esetében megfelel az alapstruktúrának, de a networkD3
esetében némi munkát kell végezni. E kellemetlenség ellenére mindkét csomag széles körű grafikus képességekkel rendelkezik, és mindkettő képes igraph
objektumokkal és elrendezésekkel dolgozni.
library(visNetwork)library(networkD3)
visNetwork
A visNetwork()
függvény egy csomópontlistát és egy éllistát használ egy interaktív gráf létrehozásához. A csomópontlistának tartalmaznia kell egy “id” oszlopot, az éllistának pedig “from” és “to” oszlopokat. A függvény a csomópontok címkéit is kirajzolja, a csomópontlista “label” oszlopában szereplő városnevek felhasználásával. Az eredményül kapott gráffal jó szórakozást nyújt a játék. A csomópontokat mozgathatja, és a gráf egy algoritmust használ a csomópontok megfelelő távolságtartására. A grafikonra is rá- és ráközelíthet, és mozgathatja azt, hogy újra középre állítsa.
visNetwork(nodes, edges)
visNetwork
használhatja a igraph
elrendezéseket, ami a lehetséges elrendezések nagy választékát biztosítja. Ezenkívül a visIgraph()
segítségével közvetlenül egy igraph
objektumot is kirajzolhat. Itt maradok a nodes
és edges
munkafolyamatnál, és egy igraph
elrendezést használok a grafikon testreszabásához. Egy változót is hozzáadok az él szélességének megváltoztatásához, ahogyan azt a ggraph
esetében is tettük. A visNetwork()
az élek és csomópontok listáiból származó oszlopneveket használja a hálózati attribútumok ábrázolásához a függvényhíváson belüli argumentumok helyett. Ez azt jelenti, hogy némi adatmanipulációt kell végezni ahhoz, hogy az éllistában legyen egy “szélesség” oszlop. A visNetwork()
szélesség attribútuma nem skálázza az értékeket, ezért ezt manuálisan kell elvégeznünk. Mindkét művelet elvégezhető a mutate()
függvénnyel és némi egyszerű aritmetikával. Itt létrehozok egy új oszlopot a edges
-ben, és a súlyértékeket úgy skálázom, hogy elosztom 5-tel. Ha az eredményhez hozzáadok 1-et, akkor egy minimális szélességet hozhatunk létre.
edges <- mutate(edges, width = weight/5 + 1)
Mihelyt ez megtörtént, létrehozhatunk egy változó élszélességű gráfot. Én is választok egy elrendezési algoritmust a igraph
-ból, és nyilakat adok az élekhez, az él közepére helyezve őket.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
networkD3
Az adatok előkészítéséhez még egy kis munkára van szükség, hogy egy networkD3
gráfot hozzunk létre. Egy networkD3
gráf elkészítéséhez egy él- és csomópontlistával az szükséges, hogy az azonosítók numerikus egész számok sorozatai legyenek, amelyek 0-val kezdődnek. Jelenleg az adataink csomópont-azonosítói 1-gyel kezdődnek, ezért egy kis adatmanipulációt kell végeznünk. A csomópontok átszámozása úgy lehetséges, hogy a nodes
és edges
adatkeretek ID oszlopaiból kivonjuk az 1-et. Ezt ismét a mutate()
függvénnyel lehet elvégezni. A cél az aktuális oszlopok újrateremtése, miközben minden ID-ből kivonunk 1-et. A mutate()
függvény úgy működik, hogy új oszlopot hoz létre, de lecserélhetünk vele egy oszlopot, ha az új oszlopnak ugyanazt a nevet adjuk, mint a régi oszlopnak. Itt az új adatkereteket d3 utótaggal nevezem el, hogy megkülönböztessem őket a korábbi nodes
és edges
adatkeretektől.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Az networkD3
grafikon ábrázolása most már lehetséges. A visNetwork()
függvénytől eltérően a forceNetwork()
függvény egy sor argumentumot használ a grafikon beállításához és a hálózat attribútumainak ábrázolásához. A “Linkek” és a “Csomópontok” argumentumok élek és csomópontok listái formájában szolgáltatják az adatokat a grafikonhoz. A függvénynek szüksége van a “NodeID” és a “Group” argumentumokra is. Az itt használt adatok nem rendelkeznek csoportosítással, ezért minden csomópontnak csak saját csoportja van, ami a gyakorlatban azt jelenti, hogy a csomópontok mind különböző színűek lesznek. Ezenkívül az alábbiakban közöljük a függvénnyel, hogy a hálózatnak vannak “Source” és “Target” mezői, tehát irányított. Ebbe a gráfba beleveszek egy “Value” értéket, amely az élek szélességét az éllista “weight” oszlopának megfelelően skálázza. Végül néhány esztétikai finomítást adok hozzá, hogy a csomópontok átláthatatlanok legyenek, és növelem a címkék betűméretét az olvashatóság javítása érdekében. Az eredmény nagyon hasonlít az általam készített első visNetwork()
diagramhoz, de más esztétikai stílusban.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
A networkD3
egyik fő előnye, hogy egy d3-stílusú Sankey-diagramot valósít meg. A Sankey-diagram jól illeszkedik a Dánielnek 1585-ben küldött levelekhez. Az adatokban nincs túl sok csomópont, ami megkönnyíti a levelek áramlásának vizualizálását. A Sankey-diagram létrehozása a sankeyNetwork()
függvényt használja, amely sokszor ugyanazokat az argumentumokat veszi fel, mint a forceNetwork()
. Ez a diagram nem igényel csoport argumentumot, és az egyetlen további változás az “egység” hozzáadása. Ez egy címkét ad az értékeknek, amelyek felugranak az eszköztippben, amikor a kurzor egy diagramelem fölé kerül.10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
További olvasmányok a hálózatelemzésről
Ez a bejegyzés megkísérelt egy általános bevezetést adni a hálózati típusú objektumok létrehozásához és ábrázolásához az R-ben a network
, igraph
, tidygraph
és ggraph
csomagok segítségével statikus ábrákhoz, valamint a visNetwork
és networkD3
csomagok segítségével interaktív ábrákhoz. Ezt az információt a hálózatelméletben nem jártas szakember pozíciójából mutattam be. Az R hálózatelemzési képességeinek csak egy nagyon kis százalékát fedtem le. Különösen a hálózatok statisztikai elemzését nem tárgyaltam. Szerencsére rengeteg forrás áll rendelkezésre a hálózatelemzésről általában és különösen az R-ben.
A legjobb bevezetés a hálózatokba, amit a beavatatlanok számára találtam, Katya Ognyanova Network Visualization with R című könyve. Ez egyrészt hasznos bevezetést nyújt a hálózatok vizuális aspektusaiba, másrészt mélyebb útmutatást nyújt a hálózati ábrák létrehozásához az R-ben. Ognyanova elsősorban a igraph
-t használja, de bemutatja az interaktív hálózatokat is.
A Springer kiadó két viszonylag friss könyvet is megjelentetett a hálózatelemzésről az R-rel. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) egy nagyon hasznos bevezetés a hálózatelemzésbe az R-rel. Luke foglalkozik mind a statnet csomagokkal, mind a igragh
csomagokkal. A tartalom végig nagyon megközelíthető szinten van. Haladóbb Eric D. Kolaczyk és Csárdi Gábor: Statistical Analysis of Network Data with R (2014) című könyve. Kolaczyk és Csárdi könyve elsősorban a igraph
-t használja, mivel Csárdi az R számára a igraph
csomag elsődleges karbantartója. Ez a könyv mélyebben belemegy a hálózatok statisztikai elemzésének haladó témáiba. A nagyon technikai nyelvezet használata ellenére az első négy fejezet általában megközelíthető egy nem szakember szemszögéből is.
A François Briatte által gondozott lista jó áttekintést nyújt az általános hálózatelemzéssel kapcsolatos forrásokról. Scott Weingart Networks Demystified című bejegyzéssorozatát is érdemes áttanulmányozni.
-
A digitális bölcsészettudományokon belül a hálózatelemzés iránti érdeklődés egyik példája az újonnan indított Journal of Historical Network Research. ︎
-
A
network
objektumosztály jó leírását, beleértve aigraph
objektumosztályhoz való viszonyának tárgyalását, lásd Carter Butts, “network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Ez a
visNetwork
által elvárt speciális struktúra, miközben megfelel a többi csomag általános elvárásainak is. ︎ -
Ez az oszlopok elvárt sorrendje néhány hálózati csomag esetében, amelyeket az alábbiakban használni fogok. ︎
-
ungroup()
ebben az esetben nem feltétlenül szükséges. Ha azonban nem szünteti meg az adatkeret csoportosítását, akkor nem lehetséges a “forrás” és a “cél” oszlopok elhagyása, ahogyan azt a szkriptben később megteszem. ︎ -
Thomas M. J. Fruchterman és Edward M. Reingold, “Graph Drawing by Force-Directed Placement,” Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
A
rm()
funkció hasznos, ha az R-ben a munkakörnyezetünk rendezetlenné válik, de nem akarjuk az egész környezetet kitakarítani és újrakezdeni. ︎ -
A
tbl_graph
ésigraph
objektumok közötti kapcsolat hasonló atibble
ésdata.frame
objektumok közötti kapcsolathoz. ︎ -
Lehetséges, hogy a
ggraph
nyilakat rajzoljon, de ezt itt nem mutattam be. ︎ -
Egy kis időbe telhet, amíg az eszköztipp megjelenik. ︎
Vélemény, hozzászólás?