V širokém spektru oborů se síťová analýza stává stále oblíbenějším nástrojem vědců, kteří se zabývají složitostí vzájemných vztahů mezi aktéry všeho druhu. Příslibem síťové analýzy je přikládání významu vztahům mezi aktéry, spíše než vnímání aktérů jako izolovaných entit. Důraz na komplexnost spolu s vytvořením řady algoritmů pro měření různých aspektů sítí činí ze síťové analýzy ústřední nástroj digitálních humanitních věd.1 Tento příspěvek poskytne úvod do práce se sítěmi v jazyce R na příkladu sítě měst v korespondenci Daniela van der Meulena z roku 1585.

Existuje řada aplikací určených pro síťovou analýzu a vytváření síťových grafů, například gephi a cytoscape. Ačkoli k tomu R nebyl speciálně navržen, vyvinul se v mocný nástroj pro analýzu sítí. Síla R ve srovnání se samostatným softwarem pro analýzu sítí je trojnásobná. V první řadě R umožňuje reprodukovatelný výzkum, který není možný u aplikací s grafickým uživatelským rozhraním. Za druhé, síla analýzy dat v R poskytuje robustní nástroje pro manipulaci s daty a jejich přípravu pro analýzu sítí. V neposlední řadě existuje stále rostoucí počet balíčků určených k tomu, aby se R stal kompletním nástrojem pro analýzu sítí. Mezi významné balíčky pro analýzu sítí v R patří sada balíčků statnet a igraph. Thomas Lin Pedersen navíc nedávno vydal balíčky tidygraph a ggraph, které využívají sílu balíčku igraph způsobem konzistentním s pracovním postupem tidyverse. R lze také použít k vytváření interaktivních síťových grafů pomocí frameworku htmlwidgets, který překládá kód R do jazyka JavaScript.

Tento příspěvek začíná krátkým úvodem do základní slovní zásoby síťové analýzy, po kterém následuje diskuse o postupu, jak dostat data do správné struktury pro síťovou analýzu. Všechny balíčky síťové analýzy mají implementovány vlastní třídy objektů. V tomto příspěvku ukážu, jak vytvořit specifické objektové třídy pro sadu balíčků statnet s balíčkem network a také pro igraph a tidygraph, který je založen na implementaci igraph. Nakonec se zaměřím na tvorbu interaktivních grafů pomocí balíků vizNetwork a networkD3.

Síťová analýza: Uzly a hrany

Dvěma základními aspekty sítí jsou množství samostatných entit a spojení mezi nimi. Slovník může být poněkud technický a dokonce nejednotný mezi různými obory, balíčky a softwarem. Entity se označují jako uzly nebo vrcholy grafu, zatímco spojení jsou hrany nebo spoje. V tomto příspěvku budu používat hlavně názvosloví uzlů a hran s výjimkou případů, kdy budu diskutovat o balíčcích, které používají jiný slovník.

Balíčky pro síťovou analýzu potřebují, aby data byla v určité podobě, aby bylo možné vytvořit speciální typ objektu používaný jednotlivými balíčky. Třídy objektů pro network, igraph a tidygraph jsou všechny založeny na maticích adjacencí, známých také jako sociomatrice.2 Matice adjacencí je čtvercová matice, v níž názvy sloupců a řádků představují uzly sítě. V matici značí 1, že mezi uzly existuje spojení, a 0 značí, že žádné spojení neexistuje. Matice adjacencí implementují zcela jinou strukturu dat než datové rámce a nehodí se do pracovního postupu tidyverse, který jsem použil ve svých předchozích příspěvcích. Nápomocné je, že specializované síťové objekty lze vytvořit také z datového rámce se seznamem hran, který do pracovního postupu tidyverse zapadá. V tomto příspěvku se budu držet technik analýzy dat tidyverse a vytvořím seznamy hran, které pak převedu na specifické třídy objektů pro network, igraph a tidygraph.

Seznam hran je datový rámec, který obsahuje minimálně dva sloupce, jeden sloupec uzlů, které jsou zdrojem spojení, a druhý sloupec uzlů, které jsou cílem spojení. Uzly v datech jsou identifikovány jedinečnými ID. Pokud je rozlišení mezi zdrojem a cílem smysluplné, je síť směrovaná. Pokud rozlišení není smysluplné, je síť neusměrněná. U příkladu dopisů posílaných mezi městy je rozlišení mezi zdrojem a cílem jednoznačně smysluplné, a síť je tedy směrovaná. V níže uvedených příkladech budu sloupec zdroje označovat jako „od“ a sloupec cíle jako „do“. Jako ID uzlů budu používat celá čísla začínající jedničkou.3 Seznam hran může obsahovat i další sloupce, které popisují atributy hran, například aspekt velikosti hrany. Pokud mají hrany atribut magnituda, je graf považován za vážený.

Seznamy hran obsahují všechny informace potřebné k vytvoření síťových objektů, ale někdy je vhodnější vytvořit také samostatný seznam uzlů. V nejjednodušším případě je seznam uzlů datový rámec s jediným sloupcem – který označím jako „id“ – který obsahuje seznam ID uzlů nalezených v seznamu hran. Výhodou vytvoření samostatného seznamu uzlů je možnost přidat do datového rámce sloupce atributů, jako jsou názvy uzlů nebo jakékoliv seskupení. Níže uvádím příklad minimálních seznamů hran a uzlů vytvořených pomocí funkce 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

Srovnejte to s maticí přiléhavosti se stejnými daty.

#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0

Vytvoření seznamů hran a uzlů

Pro vytvoření síťových objektů z databáze dopisů přijatých Danielem van der Meulenem v roce 1585 vytvořím seznam hran i seznam uzlů. To si vyžádá použití balíčku dplyr pro manipulaci s datovým rámcem dopisů zaslaných Danielovi a jeho rozdělení na dva datové rámce neboli tibbles se strukturou seznamů hran a uzlů. V tomto případě budou uzly města, ze kterých Danielovi korespondenti posílali dopisy, a města, ve kterých je přijímal. Seznam uzlů bude obsahovat sloupec „štítek“, který bude obsahovat názvy měst. Seznam hran bude obsahovat také sloupec „atribut“, ve kterém bude uvedeno množství dopisů odeslaných mezi jednotlivými dvojicemi měst. Pracovní postup pro vytvoření těchto objektů bude podobný tomu, který jsem použil ve svém stručném úvodu do R a v geokódování pomocí R. Pokud byste se jím chtěli řídit, data použitá v tomto příspěvku a použitý skript R najdete na GitHubu.

Prvním krokem je načtení knihovny tidyverse pro import a manipulaci s daty. Při vypisování datového rámce letters zjistíte, že obsahuje čtyři sloupce: „writer“, „source“, „destination“ a „date“. V tomto příkladu se budeme zabývat pouze sloupci „zdroj“ a „cíl“.

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

Seznam uzlů

Pracovní postup pro vytvoření seznamu uzlů je podobný tomu, který jsem použil pro získání seznamu měst za účelem geokódování dat v předchozím příspěvku. Chceme získat různá města ze sloupců „zdroj“ i „cíl“ a poté spojit informace z těchto sloupců dohromady. V příkladu níže mírně měním příkazy oproti těm, které jsem použil v předchozím příspěvku, aby název sloupců s názvy měst byl stejný pro oba datové rámce sources a destinations, aby se zjednodušila funkce full_join(). Sloupec s názvy měst přejmenuji na „label“, abych převzal slovník používaný balíky pro síťovou analýzu.

sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)

Pro vytvoření jediného datového rámce se sloupcem s unikátními místy musíme použít úplné spojení, protože chceme zahrnout všechna unikátní místa jak ze zdrojů dopisů, tak z cílů.

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

Výsledkem je datový rámec s jednou proměnnou. Proměnná obsažená v datovém rámci však ve skutečnosti není tím, co hledáme. Sloupec „label“ obsahuje názvy uzlů, ale my chceme mít pro každé město také jedinečné ID. Toho můžeme dosáhnout přidáním sloupce „id“ do datového rámce nodes, který obsahuje čísla od jedné do libovolného celkového počtu řádků v datovém rámci. Užitečnou funkcí pro tento pracovní postup je rowid_to_column(), která přidá sloupec s hodnotami z id řádků a umístí sloupec na začátek datového rámce.4 Všimněte si, že rowid_to_column() je rourový příkaz, a tak je možné provést full_join() a přidat sloupec „id“ v jediném příkazu. Výsledkem je seznam uzlů se sloupcem ID a atributem label.

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

Seznam hran

Vytvoření seznamu hran je podobné výše uvedenému, ale je komplikováno nutností pracovat se dvěma sloupci ID místo jednoho. Chceme také vytvořit sloupec váhy, který bude zaznamenávat množství písmen posílaných mezi jednotlivými sadami uzlů. K tomu použiji stejný pracovní postup group_by() a summarise(), který jsem probíral v předchozích příspěvcích. Rozdíl je v tom, že zde chceme seskupit datový rámec podle dvou sloupců – „zdroj“ a „cíl“ – namísto jednoho. Dříve jsem sloupec, který počítá počet pozorování ve skupině, pojmenoval „count“, zde však přejímám názvosloví síťové analýzy a nazývám jej „weight“. Poslední příkaz v pipeline odstraní seskupení pro datový rámec zavedené funkcí group_by(). To usnadňuje nerušenou manipulaci s výsledným datovým rámcem per_route. 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

Stejně jako seznam uzlů má nyní per_route základní tvar, který chceme, ale opět se potýkáme s problémem, že sloupce „zdroj“ a „cíl“ obsahují spíše štítky než ID. Potřebujeme tedy propojit ID, která byla přiřazena v nodes, s jednotlivými místy ve sloupcích „zdroj“ i „cíl“. Toho lze dosáhnout pomocí další funkce join. Ve skutečnosti je nutné provést dvě spojení, jedno pro sloupec „zdroj“ a druhé pro sloupec „cíl“. V tomto případě použiji left_join() s per_route jako levým datovým rámcem, protože chceme zachovat počet řádků v per_route. Při provádění left_join chceme také přejmenovat dva sloupce „id“, které jsou přeneseny z nodes. Pro spojení pomocí sloupce „source“ přejmenuji sloupec na „from“. Sloupec přenesený z „cílového“ spojení přejmenuji na „do“. Bylo by možné provést obě spojení v jednom příkazu s použitím roury. Pro přehlednost však provedu spojení ve dvou samostatných příkazech. Protože se spojení provádí ve dvou příkazech, všimněte si, že se datový rámec na začátku potrubí změní z per_route na edges, který je vytvořen prvním příkazem.

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)

Nyní, když má edges sloupce „od“ a „do“ s ID uzlů, musíme změnit pořadí sloupců, aby se „od“ a „do“ dostaly do levé části datového rámce. V současné době datový rámec edges stále obsahuje sloupce „zdroj“ a „cíl“ s názvy měst, které odpovídají ID. Tyto údaje jsou však nadbytečné, protože jsou již obsaženy v nodes. Proto do funkce select() zahrnu pouze sloupce „od“, „do“ a „váha“.

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

Datový rámec edges nevypadá příliš působivě; jsou to tři sloupce celých čísel. Nicméně edges v kombinaci s nodes nám poskytuje všechny informace potřebné k vytvoření síťových objektů pomocí balíčků network, igraph a tidygraph.

Vytváření síťových objektů

Třídy síťových objektů pro network, igraph a tidygraph spolu úzce souvisejí. Je možné převádět mezi objektem network a objektem igraph. Nejlepší je však tyto dva balíky a jejich objekty oddělit. Ve skutečnosti se schopnosti balíčků network a igraph překrývají do té míry, že je nejlepší mít v daném okamžiku načtený pouze jeden z nich. Nejprve projdu balíček network a poté přejdu k balíčkům igraph a tidygraph.

síť

library(network)

Funkce používaná k vytvoření objektu network je network(). Příkaz není nijak zvlášť přímočarý, ale kdybyste byli zmateni, vždy můžete do konzoly zadat ?network(). Prvním argumentem je – jak je uvedeno v dokumentaci – „matice udávající strukturu sítě ve formě adjacencí, incidence nebo edgelistů“. Jazyk demonstruje význam matic v síťové analýze, ale místo matice máme k dispozici seznam hran, který plní stejnou úlohu. Druhým argumentem je seznam atributů vrcholů, který odpovídá seznamu uzlů. Všimněte si, že balíček network používá názvosloví vrcholů místo uzlů. Totéž platí i pro igraph. Poté musíme určit typ dat, která byla zadána do prvních dvou argumentů, a to tak, že uvedeme, že matrix.type je "edgelist". Nakonec nastavíme ignore.eval na FALSE, aby naše síť mohla být vážená a zohledňovala počet písmen na každé trase.

routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)

Typ objektu vytvořeného funkcí network() zjistíte vložením routes_network do funkce class().

class(routes_network)#> "network"

Vypsání routes_network do konzoly ukazuje, že struktura objektu je zcela odlišná od objektů ve stylu datového rámce, jako jsou edges a nodes. Příkaz pro tisk odhalí informace, které jsou specificky definovány pro síťovou analýzu. Ukazuje, že v routes_network je 13 vrcholů neboli uzlů a 15 hran. Tato čísla odpovídají počtu řádků v nodes a edges. Vidíme také, že vrcholy i hrany obsahují atributy, jako je značka a váha. Ještě více informací, včetně sociomatrix dat, získáte zadáním 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

Nyní je možné získat základní, i když ne příliš estetický graf naší sítě písmen. Oba balíky network a igraph používají základní vykreslovací systém R. Konvence pro základní grafy se výrazně liší od konvencí ggplot2 – o kterém jsem hovořil v předchozích příspěvcích – a proto zůstanu u poměrně jednoduchých grafů, místo abych se zabýval podrobnostmi vytváření složitých grafů pomocí základního R. V tomto případě jedinou změnou, kterou provedu ve výchozí funkci plot() pro balík network, je zvětšení velikosti uzlů pomocí argumentu vertex.cex, aby byly uzly lépe viditelné. I z tohoto velmi jednoduchého grafu se již můžeme něco dozvědět o datech. Z grafu je zřejmé, že existují dvě hlavní seskupení či shluky dat, které odpovídají době, kterou Daniel strávil v Holandsku v prvních třech čtvrtinách roku 1585, a po jeho přesunu do Brém v září.

plot(routes_network, vertex.cex = 3)

Funkce plot() s objektem network používá k rozhodnutí o umístění uzlů Fruchtermanův a Reingoldův algoritmus6. Algoritmus rozmístění můžete změnit pomocí argumentu mode. Níže uvádím rozložení uzlů do kruhu. Toto uspořádání není pro tuto síť nijak zvlášť užitečné, ale dává představu o některých dostupných možnostech.

plot(routes_network, vertex.cex = 3, mode = "circle")

igraph

Přejděme nyní k diskusi o balíčku igraph. Nejprve musíme vyčistit prostředí v R odstraněním balíčku network, aby nezasahoval do příkazů igraph. Rovněž bychom mohli odstranit balík routes_network, protože ho již nebudeme používat. Balík network odstraníme pomocí funkce detach() a routes_network odstraníme pomocí funkce rm().7 Poté můžeme bezpečně načíst igraph.

detach(package:network)rm(routes_network)library(igraph)

Pro vytvoření objektu igraph z datového rámce edge-list můžeme použít funkci graph_from_data_frame(), která je o něco přímočařejší než network(). Funkce graph_from_data_frame() má tři argumenty: d, vrcholy a directed. Zde d odkazuje na seznam hran, vrcholy na seznam uzlů a directed může být buď TRUE nebo FALSE podle toho, zda jsou data směrovaná nebo neusměrněná.

routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)

Vytištění objektu igraph vytvořeného pomocí graph_from_data_frame() na konzolu odhalí podobné informace jako u objektu network, i když struktura je záhadnější.

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

Hlavní informace o objektu jsou obsaženy v DNW- 13 15 --. Ta říká, že routes_igraph je směrovaná síť (D), která má atribut jméno (N) a je vážená (W). Pomlčka za W nám říká, že graf není bipartitní. Čísla, která následují, popisují počet uzlů, respektive hran v grafu. Dále name (v/c), label (v/c), weight (e/n) uvádí informace o atributech grafu. Jsou zde dva atributy vrcholů (v/c) jména – což jsou ID – a značky a atribut hran (e/n) váhy. Nakonec je zde výpis všech hran.

Stejně jako u balíčku network můžeme pomocí funkce plot() vytvořit graf s objektem igraph. Jedinou změnou, kterou zde provedu oproti výchozímu nastavení, je zmenšení velikosti šipek. Ve výchozím nastavení igraph označuje uzly sloupcem štítků, pokud je k dispozici, nebo pomocí ID.

plot(routes_igraph, edge.arrow.size = 0.2)

Stejně jako u předchozího grafu network není výchozí nastavení grafu igraph nijak zvlášť estetické, ale se všemi aspekty grafu lze manipulovat. Zde chci pouze změnit rozložení uzlů tak, aby používaly algoritmus graphopt, který vytvořil Michael Schmuhl. Tento algoritmus usnadňuje zobrazení vztahu mezi Haarlemem, Antverpami a Delftem, což jsou tři nejvýznamnější místa v korespondenční síti, tím, že je dále rozprostře.

plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)

tidygraph a ggraph

Balíčky tidygraph a ggraph jsou na poli síťové analýzy nováčky, ale společně tyto dva balíčky poskytují skutečné výhody oproti balíčkům network a igraph. Balíky tidygraph a ggraph představují pokus o zavedení síťové analýzy do pracovního postupu tidyverse. Balík tidygraph poskytuje způsob, jak vytvořit síťový objekt, který se více podobá tibble nebo datovému rámci. To umožňuje používat mnoho funkcí dplyr pro manipulaci se síťovými daty. ggraph poskytuje způsob vykreslování síťových grafů s využitím konvencí a možností ggplot2. Jinými slovy, tidygraph a ggraph umožňují pracovat se síťovými objekty způsobem, který je více konzistentní s příkazy používanými pro práci s tibbles a datovými rámci. Skutečným příslibem tidygraph a ggraph je však to, že využívají sílu igraph. To znamená, že použitím tidygraph a ggraph obětujete jen málo z možností síťové analýzy igraph.

Na začátek musíme jako vždy načíst potřebné balíčky.

library(tidygraph)library(ggraph)

Nejprve vytvoříme síťový objekt pomocí tidygraph, který se nazývá tbl_graph. A tbl_graph se skládá ze dvou tibble: tibble hran a tibble uzlů. Je příhodné, že třída objektů tbl_graph je obalem kolem objektu igraph, což znamená, že ve svém základu je objekt tbl_graph v podstatě objektem igraph.8 Úzká vazba mezi objekty tbl_graph a igraph vede ke dvěma hlavním způsobům vytvoření objektu tbl_graph. Prvním je použití seznamu hran a seznamu uzlů pomocí tbl_graph(). Argumenty funkce jsou téměř totožné s argumenty funkce graph_from_data_frame(), pouze s malou změnou názvů argumentů.

routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)

Druhým způsobem vytvoření objektu tbl_graph je konverze objektu igraph nebo network pomocí as_tbl_graph(). Mohli bychom tedy převést routes_igraph na objekt tbl_graph.

routes_igraph_tidy <- as_tbl_graph(routes_igraph)

Teď, když jsme vytvořili dva objekty tbl_graph, zkontrolujme je pomocí funkce class(). Z toho vyplývá, že routes_tidy a routes_igraph_tidy jsou objekty třídy "tbl_graph" "igraph", zatímco routes_igraph je objekt třídy "igraph".

class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"

Výpis objektu tbl_graph na konzolu vede k drasticky odlišnému výstupu než u objektu igraph. Je to výstup podobný výstupu normálního tibblu.

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

Vytisknutí routes_tidy ukazuje, že jde o objekt tbl_graph s 13 uzly a 15 hranami. Příkaz také vytiskne prvních šest řádků „Údaje o uzlech“ a první tři řádky „Údaje o hranách“. Všimněte si také, že uvádí, že „Data uzlu“ jsou aktivní. Pojem aktivní uzel v objektu tbl_graph umožňuje manipulovat s daty v jednom uzlu najednou. Ve výchozím nastavení je aktivován tibble uzlu, ale pomocí funkce activate() můžete změnit, který tibble je aktivní. Pokud bych tedy chtěl změnit uspořádání řádků v tibblu hran tak, aby byly nejprve uvedeny ty s nejvyšší „váhou“, mohl bych použít activate() a poté arrange(). Zde výsledek místo ukládání jednoduše vypíšu.

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

Protože s routes_tidy nemusíme dále manipulovat, můžeme graf vykreslit pomocí ggraph. Stejně jako ggmap je ggraph rozšířením ggplot2, což usnadňuje přenesení základních dovedností ggplot do tvorby síťových grafů. Stejně jako u všech síťových grafů existují tři hlavní aspekty grafu ggraph: uzly, hrany a rozložení. Vignety pro balík ggraph užitečně pokrývají základní aspekty ggraph grafů. ggraph přidává k základní sadě ggplot geomů speciální geomy, které jsou určeny speciálně pro sítě. Existuje tedy sada geom_node a geom_edge geomů. Základní vykreslovací funkcí je ggraph(), která přebírá data, která mají být použita pro graf, a požadovaný typ rozložení. Oba argumenty pro ggraph() jsou postaveny kolem igraph. Funkce ggraph() tedy může používat buď objekt igraph, nebo objekt tbl_graph. Kromě toho jsou dostupné algoritmy rozvržení primárně odvozeny od igraph. A konečně, ggraph zavádí speciální téma ggplot, které poskytuje lepší výchozí nastavení pro síťové grafy než běžné výchozí nastavení ggplot. Téma ggraph lze nastavit pro sérii grafů pomocí příkazu set_graph_style() spuštěného před vykreslením grafů nebo pomocí theme_graph() v jednotlivých grafech. Zde použiji druhý způsob.

Podívejme se, jak vypadá základní ggraph graf. Graf začíná ggraph() a daty. Poté přidám základní geomy hran a uzlů. V rámci hranových a uzlových geomů nejsou nutné žádné argumenty, protože přebírají informace z dat uvedených v ggraph().

ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()

Jak vidíte, struktura příkazu je podobná jako u ggplot s tím, že samostatné vrstvy jsou přidány pomocí znaku +. Základní graf ggraph vypadá podobně jako grafy network a igraph, ne-li ještě jednodušeji, ale pomocí podobných příkazů jako ggplot můžeme vytvořit informativnější graf. Pomocí funkce width ve funkci geom_edge_link() můžeme zobrazit „váhu“ hran – neboli množství písmen odeslaných po jednotlivých trasách. Aby se šířka čáry měnila v závislosti na proměnné váha, umístíme argument do funkce aes(). Abych mohl řídit maximální a minimální šířku okrajů, použiji scale_edge_width() a nastavím range. Pro minimální šířku volím poměrně malou šířku, protože mezi maximálním a minimálním počtem písmen posílaných po trasách je značný rozdíl. Uzly můžeme také označit názvy míst, protože uzlů je relativně málo. Je výhodné, že geom_node_text() obsahuje argument repel, který zajišťuje, aby se popisky nepřekrývaly s uzly podobně jako v balíčku ggrepel. Pomocí argumentu alpha přidávám hranám trochu průhlednosti. Používám také labs() k přeznačení legendy „Písmena“.

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()

Kromě možností rozložení, které poskytuje igraph, implementuje ggraph také vlastní rozložení. Například při vytváření obloukových diagramů můžete použít koncept kružnic ggraph's. Zde jsem rozložil uzly do vodorovné linie a hrany nechal vykreslit jako oblouky. Na rozdíl od předchozího grafu je v tomto grafu vyznačena směrovost hran.9 Hrany nad vodorovnou čarou se pohybují zleva doprava, zatímco hrany pod čarou se pohybují zprava doleva. Místo přidávání bodů pro uzly uvádím pouze názvy štítků. Pro označení rozdílu ve váze jednotlivých hran používám stejnou estetickou šířku. Všimněte si, že v tomto grafu používám jako data pro graf objekt igraph, což prakticky nic nemění.

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()

Interaktivní síťové grafy s visNetwork a networkD3

Sada balíčků htmlwidgets umožňuje používat R k vytváření interaktivních vizualizací v jazyce JavaScript. Zde ukážu, jak vytvořit grafy pomocí balíčků visNetwork a networkD3. Tyto dva balíčky používají k vytváření grafů různé knihovny jazyka JavaScript. Balík visNetwork používá vis.js, zatímco balík networkD3 používá k tvorbě grafů populární vizualizační knihovnu d3. Jedním z problémů při práci s balíky visNetwork i networkD3 je, že očekávají, že seznamy hran a seznamy uzlů budou používat specifické názvosloví. Výše uvedená manipulace s daty je v souladu se základní strukturou pro visNetwork, ale pro networkD3 bude třeba udělat nějakou práci. I přes tuto nepříjemnost disponují oba balíky širokou škálou grafických možností a oba umí pracovat s objekty a rozvržením igraph.

library(visNetwork)library(networkD3)

visNetwork

Funkce visNetwork() používá seznam uzlů a seznam hran k vytvoření interaktivního grafu. Seznam uzlů musí obsahovat sloupec „id“ a seznam hran musí mít sloupce „od“ a „do“. Funkce také vykreslí popisky uzlů pomocí názvů měst ze sloupce „label“ v seznamu uzlů. S výsledným grafem je zábavné si hrát. Uzly můžete přesouvat a graf použije algoritmus, který udržuje správné rozestupy uzlů. Graf můžete také zvětšovat a zmenšovat a přesouvat jej, abyste jej znovu vycentrovali.

visNetwork(nodes, edges)

visNetwork může používat igraph rozložení, což poskytuje velké množství možných rozložení. Kromě toho můžete pomocí visIgraph() vykreslit objekt igraph přímo. Zde se budu držet pracovních postupů nodes a edges a pro přizpůsobení grafu použiji rozložení igraph. Přidám také proměnnou pro změnu šířky hrany, stejně jako jsme to udělali u ggraph. visNetwork() používá názvy sloupců ze seznamů hran a uzlů pro vykreslení atributů sítě místo argumentů v rámci volání funkce. To znamená, že je nutné provést určitou manipulaci s daty, abychom získali sloupec „šířka“ v seznamu hran. Atribut šířky pro visNetwork() neškáluje hodnoty, takže to musíme udělat ručně. Obě tyto činnosti lze provést pomocí funkce mutate() a několika jednoduchých aritmetických úkonů. Zde vytvořím nový sloupec v edges a hodnoty vah škáluji dělením 5. Přičtením 1 k výsledku získáme způsob, jak vytvořit minimální šířku.

edges <- mutate(edges, width = weight/5 + 1)

Po provedení této operace můžeme vytvořit graf s proměnlivou šířkou hran. Zvolím také algoritmus rozvržení z igraph a přidám k hranám šipky, které umístím do středu hrany.

visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")

networkD3

Na přípravu dat pro vytvoření grafu networkD3 je potřeba ještě trochu práce. Vytvoření grafu networkD3 se seznamem hran a uzlů vyžaduje, aby ID byla řada celých čísel, která začínají číslem 0. V současné době začínají ID uzlů našich dat číslem 1, a proto musíme provést trochu manipulace s daty. Uzly je možné přečíslovat odečtením 1 od sloupců ID v datových rámcích nodes a edges. To lze opět provést pomocí funkce mutate(). Cílem je znovu vytvořit aktuální sloupce, přičemž se od každého ID odečte 1. Funkce mutate() pracuje tak, že vytvoří nový sloupec, ale můžeme ji nechat nahradit sloupec tak, že novému sloupci dáme stejné jméno jako starému sloupci. Zde pojmenovávám nové datové rámce příponou d3, abych je odlišil od předchozích datových rámců nodes a edges.

nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)

Nyní je možné vykreslit graf networkD3. Na rozdíl od funkce visNetwork() používá funkce forceNetwork() řadu argumentů pro úpravu grafu a vykreslení atributů sítě. Argumenty „Links“ a „Nodes“ poskytují data pro vykreslení v podobě seznamů hran a uzlů. Funkce rovněž vyžaduje argumenty „NodeID“ a „Group“. Data, která jsou zde použita, nemají žádné seskupení, a tak mám pouze každý uzel svou vlastní skupinu, což v praxi znamená, že všechny uzly budou mít různé barvy. Níže je navíc funkci sděleno, že síť má pole „Source“ a „Target“, a je tedy směrovaná. Do tohoto grafu zahrnuji „Hodnotu“, která škáluje šířku hran podle sloupce „Váha“ v seznamu hran. Nakonec přidávám několik estetických úprav, aby uzly byly neprůhledné, a zvětšuji velikost písma popisků pro zlepšení čitelnosti. Výsledek je velmi podobný prvnímu grafu visNetwork(), který jsem vytvořil, ale s jinou estetickou stylizací.

forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)

Jednou z hlavních výhod networkD3 je, že implementuje Sankeyho diagram ve stylu d3. Sankeyho diagram se dobře hodí pro dopisy zaslané Danielovi v roce 1585. V datech není příliš mnoho uzlů, což usnadňuje vizualizaci toku dopisů. Při vytváření Sankeyova diagramu se používá funkce sankeyNetwork(), která přijímá mnoho stejných argumentů jako funkce forceNetwork(). Tento graf nevyžaduje argument skupiny a jedinou další změnou je přidání „jednotky“. Ta poskytuje označení hodnot, které se objeví v tipu nástroje, když kurzor najede na prvek diagramu.10

sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")

Další čtení o síťové analýze

Tento příspěvek se pokusil poskytnout obecný úvod do vytváření a vykreslování objektů síťového typu v R pomocí balíčků network, igraph, tidygraph a ggraph pro statické grafy a visNetwork a networkD3 pro interaktivní grafy. Tyto informace jsem prezentoval z pozice nespecialisty na teorii sítí. Pokryl jsem jen velmi malé procento možností síťové analýzy v R. Zejména jsem se nezabýval statistickou analýzou sítí. Naštěstí existuje nepřeberné množství zdrojů o analýze sítí obecně a zejména v R.

Nejlepším úvodem do sítí, který jsem pro nezasvěcené našel, je kniha Katyi Ognyanové Network Visualization with R. Ta představuje jak užitečný úvod do vizuálních aspektů sítí, tak hlubší výuku vytváření síťových grafů v R. Ognyanová používá především igraph, ale představuje také interaktivní sítě.

Vydavatelství Springer vydalo poměrně nedávno dvě knihy o síťové analýze v jazyce R. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) je velmi užitečným úvodem do síťové analýzy s R. Luke pokrývá jak oblek balíčků statnet, tak igragh. Obsah je po celou dobu na velmi přístupné úrovni. Pokročilejší je kniha Eric D. Kolaczyk a Gábor Csárdi, Statistical Analysis of Network Data with R (2014). Kniha Kolaczyka a Csárdiho využívá především igraph, protože Csárdi je hlavním správcem balíčku igraph pro R. Tato kniha se dále dostává k pokročilým tématům statistické analýzy sítí. Navzdory použití velmi odborného jazyka jsou první čtyři kapitoly obecně přístupné i z pohledu neodborníka.

Seznam, jehož kurátorem je François Briatte, je dobrým přehledem zdrojů o analýze sítí obecně. Za prostudování stojí také série příspěvků Networks Demystified Scotta Weingarta.

  1. Jedním z příkladů zájmu o síťovou analýzu v rámci digitálních humanitních věd je nově založený časopis Journal of Historical Network Research. ︎

  2. Dobrý popis třídy objektů network, včetně diskuse o jejím vztahu ke třídě objektů igraph, viz Carter Butts, „network: A Package for Managing Relational Data in R“, Journal of Statistical Software, 24 (2008): 1-36 ︎

  3. Tato specifická struktura se očekává od visNetwork a zároveň odpovídá obecným očekáváním ostatních balíků. ︎

  4. Toto je očekávané pořadí sloupců pro některé síťové balíčky, které budu používat níže. ︎

  5. ungroup() není v tomto případě nezbytně nutné. Pokud však datový rámec nezeskupíte, není možné vypustit sloupce „zdroj“ a „cíl“, jak to udělám později ve skriptu. ︎

  6. Thomas M. J. Fruchterman a Edward M. Reingold, „Graph Drawing by Force-Directed Placement“, Software: Practice and Experience, 21 (1991): 1129-1164. ︎

  7. Funkce rm() je užitečná, pokud se vám pracovní prostředí v R rozsype, ale nechcete celé prostředí vymazat a začít znovu. ︎

  8. Vztah mezi objekty tbl_graph a igraph je podobný vztahu mezi objekty tibble a data.frame. ︎

  9. Je možné, aby ggraph kreslil šipky, ale to jsem zde neukazoval. ︎

  10. Může chvíli trvat, než se zobrazí tip nástroje. ︎

r

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.