Wprowadzenie do analizy sieciowej z R
On 4 listopada, 2021 by adminW szerokim zakresie dziedzin analiza sieci stała się coraz bardziej popularnym narzędziem dla naukowców do czynienia ze złożonością wzajemnych powiązań między aktorami wszelkiego rodzaju. Obietnica analizy sieciowej jest umieszczenie znaczenia na relacje między podmiotami, a nie widząc podmioty jako odizolowane jednostki. Nacisk na złożoność, wraz z powstaniem wielu algorytmów do pomiaru różnych aspektów sieci, czyni z analizy sieci centralne narzędzie humanistyki cyfrowej.1 Ten post będzie stanowił wprowadzenie do pracy z sieciami w R, na przykładzie sieci miast w korespondencji Daniela van der Meulena z 1585 roku.
Istnieje wiele aplikacji przeznaczonych do analizy sieci i tworzenia wykresów sieciowych, takich jak gephi i cytoscape. Chociaż nie został specjalnie zaprojektowany do tego celu, R rozwinął się w potężne narzędzie do analizy sieci. Siła R w porównaniu z samodzielnym oprogramowaniem do analizy sieci jest potrójna. Po pierwsze, R umożliwia powtarzalne badania, co nie jest możliwe w przypadku aplikacji z graficznym interfejsem użytkownika. Po drugie, możliwości analizy danych w R zapewniają solidne narzędzia do manipulacji danymi w celu przygotowania ich do analizy sieciowej. Wreszcie, istnieje stale rosnąca liczba pakietów zaprojektowanych w celu uczynienia z R kompletnego narzędzia do analizy sieci. Znaczące pakiety do analizy sieciowej dla R zawierają pakiet pakietów statnet i igraph
. Dodatkowo, Thomas Lin Pedersen wydał ostatnio pakiety tidygraph
i ggraph
, które wykorzystują możliwości igraph
w sposób zgodny z przepływem pracy tidyverse. R może być również używany do tworzenia interaktywnych wykresów sieciowych za pomocą frameworka htmlwidgets, który tłumaczy kod R na JavaScript.
Ten post zaczyna się od krótkiego wprowadzenia do podstawowego słownictwa analizy sieciowej, po którym następuje dyskusja na temat procesu uzyskiwania danych w odpowiedniej strukturze do analizy sieciowej. Wszystkie pakiety analizy sieciowej mają zaimplementowane swoje własne klasy obiektów. W tym poście pokażę, jak utworzyć konkretne klasy obiektów dla pakietu statnet z pakietem network
, jak również dla igraph
i tidygraph
, który jest oparty na implementacji igraph
. Na koniec przejdę do tworzenia interaktywnych grafów za pomocą pakietów vizNetwork
i networkD3
.
Analiza sieci: Nodes and Edges
Dwa podstawowe aspekty sieci to mnogość oddzielnych jednostek i połączenia między nimi. Słownictwo może być trochę techniczny, a nawet niespójne między różnymi dyscyplinami, pakietów i oprogramowania. Podmioty są określane jako węzły lub wierzchołki grafu, podczas gdy połączenia są krawędziami lub linkami. W tym poście będę głównie używał nomenklatury węzłów i krawędzi, z wyjątkiem omawiania pakietów, które używają innego słownictwa.
Pakiety analizy sieciowej potrzebują danych w określonej formie, aby utworzyć specjalny typ obiektu używany przez każdy pakiet. Klasy obiektów dla network
, igraph
i tidygraph
są oparte na macierzach adjacency, znanych również jako socjomatryce.2 Macierz adjacency jest kwadratową macierzą, w której nazwy kolumn i wierszy są węzłami sieci. W macierzy 1 oznacza, że istnieje połączenie między węzłami, a 0 oznacza brak połączenia. Macierze adjacency implementują zupełnie inną strukturę danych niż ramki danych i nie pasują do przepływu pracy tidyverse, którego używałem w moich poprzednich postach. Na szczęście, wyspecjalizowane obiekty sieciowe mogą być również tworzone z ramek danych typu edge-list, które pasują do przepływu pracy tidyverse. W tym poście będę trzymał się technik analizy danych tidyverse, aby utworzyć listy krawędzi, które następnie zostaną przekonwertowane na konkretne klasy obiektów dla network
, igraph
i tidygraph
.
Lista krawędzi jest ramką danych, która zawiera minimum dwie kolumny, jedną kolumnę węzłów, które są źródłem połączenia i drugą kolumnę węzłów, które są celem połączenia. Węzły w danych są identyfikowane za pomocą unikalnych identyfikatorów. Jeśli rozróżnienie między źródłem a celem jest znaczące, sieć jest skierowana. Jeśli rozróżnienie nie jest znaczące, sieć jest nieukierunkowana. W przykładzie listów wysyłanych między miastami, rozróżnienie między źródłem a celem jest wyraźnie znaczące, a więc sieć jest skierowana. W poniższych przykładach kolumnę źródłową będę nazywał „od”, a kolumnę docelową „do”. Jako identyfikatorów węzłów będę używał liczb całkowitych zaczynających się od jeden.3 Lista krawędzi może również zawierać dodatkowe kolumny, które opisują atrybuty krawędzi, takie jak aspekt wielkości dla krawędzi. Jeśli krawędzie mają atrybut magnitude, graf jest uważany za ważony.
Listy krawędzi zawierają wszystkie informacje niezbędne do tworzenia obiektów sieciowych, ale czasami lepiej jest również utworzyć osobną listę węzłów. W najprostszym przypadku lista węzłów jest ramką danych z pojedynczą kolumną, którą oznaczymy jako „id”, zawierającą identyfikatory węzłów znalezionych na liście krawędzi. Zaletą stworzenia osobnej listy węzłów jest możliwość dodania do ramki danych kolumn atrybutów, takich jak nazwy węzłów lub dowolne grupowanie. Poniżej podaję przykład minimalnych list krawędziowych i węzłowych utworzonych za pomocą funkcji 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
Porównaj to z macierzą adjacencji o tych samych danych.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Tworzenie list krawędziowych i węzłowych
Aby utworzyć obiekty sieciowe z bazy listów otrzymanych przez Daniela van der Meulena w 1585 roku, utworzę zarówno listę krawędziową, jak i węzłową. Będzie to wymagało użycia pakietu dplyr do manipulacji ramką danych listów wysłanych do Daniela i podzielenia jej na dwie ramki danych lub tibble o strukturze list krawędziowych i węzłowych. W tym przypadku węzłami będą miasta, z których korespondenci Daniela wysyłali do niego listy oraz miasta, w których on je otrzymywał. Lista węzłów będzie zawierała kolumnę „label”, zawierającą nazwy miast. Lista krawędzi będzie miała również kolumnę atrybutów, która będzie pokazywać ilość listów wysłanych pomiędzy każdą parą miast. Przepływ pracy do tworzenia tych obiektów będzie podobny do tego, którego użyłem w moim krótkim wprowadzeniu do R i w geokodowaniu z R. Jeśli chcesz śledzić, możesz znaleźć dane używane w tym poście i skrypt R używany na GitHub.
Pierwszym krokiem jest załadowanie biblioteki tidyverse
w celu zaimportowania i manipulowania danymi. Wydruk ramki danych letters
pokazuje, że zawiera ona cztery kolumny: „pisarz”, „źródło”, „miejsce docelowe” oraz „data”. W tym przykładzie zajmiemy się tylko kolumnami „źródło” i „miejsce docelowe”.
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
Lista węzłów
Procedura tworzenia listy węzłów jest podobna do tej, której użyłem do uzyskania listy miast w celu geokodowania danych w poprzednim poście. Chcemy uzyskać różne miasta z obu kolumn „źródło” i „miejsce docelowe”, a następnie połączyć informacje z tych kolumn razem. W poniższym przykładzie nieznacznie zmieniam polecenia z tych, których użyłem w poprzednim poście, aby nazwa kolumn z nazwami miast była taka sama dla obu ramek danych sources
i destinations
, aby uprościć funkcję full_join()
. Zmieniam nazwę kolumny z nazwami miast na „label”, aby przyjąć słownictwo używane przez pakiety do analizy sieci.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
Aby utworzyć pojedynczą ramkę danych z kolumną z unikalnymi miejscami, musimy użyć pełnego złączenia, ponieważ chcemy uwzględnić wszystkie unikalne miejsca zarówno ze źródeł listów, jak i miejsc docelowych.
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
W wyniku tego otrzymujemy ramkę danych z jedną zmienną. Jednak zmienna zawarta w ramce danych nie jest tak naprawdę tym, czego szukamy. Kolumna „label” zawiera nazwy węzłów, ale chcemy również mieć unikalne identyfikatory dla każdego miasta. Możemy to zrobić, dodając do ramki danych nodes
kolumnę „id”, która zawiera liczby od jednego do liczby równej całkowitej liczbie wierszy w ramce danych. Pomocną funkcją dla tego przepływu pracy jest rowid_to_column()
, która dodaje kolumnę z wartościami z identyfikatorów wierszy i umieszcza ją na początku ramki danych.4 Zauważ, że rowid_to_column()
jest poleceniem potokowym, a więc możliwe jest wykonanie full_join()
i dodanie kolumny „id” w jednym poleceniu. Wynikiem jest lista węzłów z kolumną ID i atrybutem 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
Lista krawędzi
Tworzenie listy krawędzi jest podobne do powyższego, ale jest skomplikowane przez konieczność radzenia sobie z dwiema kolumnami ID zamiast jednej. Chcemy również utworzyć kolumnę wagi, która będzie notować ilość list wysyłanych pomiędzy każdym zestawem węzłów. Aby to osiągnąć, użyję tego samego przepływu pracy group_by()
i summarise()
, który omówiłem w poprzednich postach. Różnica polega na tym, że chcemy pogrupować ramkę danych według dwóch kolumn – „źródło” i „miejsce docelowe” – zamiast tylko jednej. Poprzednio kolumnę zliczającą liczbę obserwacji w grupie nazwałem „count”, ale tutaj przyjmuję nomenklaturę analizy sieciowej i nazywam ją „weight”. Ostatnie polecenie w potoku usuwa grupowanie dla ramki danych wprowadzone przez funkcję group_by()
. Ułatwia to nieskrępowane manipulowanie wynikową ramką danych 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
Podobnie jak lista węzłów, per_route
ma teraz podstawową formę, jakiej oczekujemy, ale znów mamy problem z tym, że kolumny „źródło” i „miejsce docelowe” zawierają raczej etykiety niż identyfikatory. To, co musimy zrobić, to połączyć identyfikatory, które zostały przypisane w nodes
do każdej lokalizacji zarówno w kolumnie „źródłowej”, jak i „docelowej”. Można to osiągnąć za pomocą innej funkcji join. W rzeczywistości konieczne jest wykonanie dwóch złączeń, jednego dla kolumny „źródłowej” i jednego dla „docelowej”. W tym przypadku użyję left_join()
z per_route
jako lewą ramką danych, ponieważ chcemy zachować liczbę wierszy w per_route
. Podczas wykonywania left_join
, chcemy również zmienić nazwy dwóch kolumn „id”, które zostały przeniesione z nodes
. Dla złączenia używającego kolumny „source” zmienię nazwę kolumny na „from”. Kolumnie przeniesionej z złączenia „destination” zmienię nazwę na „to”. Możliwe byłoby wykonanie obu złączeń w jednym poleceniu z użyciem potoku. Jednakże, dla jasności, wykonam złączenia w dwóch oddzielnych poleceniach. Ponieważ złączenie jest wykonywane w dwóch poleceniach, zauważ, że ramka danych na początku potoku zmienia się z per_route
na edges
, która jest tworzona przez pierwsze polecenie.
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)
Teraz, gdy edges
ma kolumny „od” i „do” z identyfikatorami węzłów, musimy zmienić kolejność kolumn, aby „od” i „do” znalazły się po lewej stronie ramki danych. Obecnie ramka danych edges
nadal zawiera kolumny „źródło” i „cel” z nazwami miast odpowiadającymi identyfikatorom. Jednak te dane są zbędne, ponieważ są już obecne w nodes
. Dlatego w funkcji select()
uwzględnię tylko kolumny „od”, „do” i „waga”.
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
Ramka danych edges
nie wygląda zbyt imponująco; są to trzy kolumny liczb całkowitych. Jednak edges
w połączeniu z nodes
dostarcza nam wszystkich informacji niezbędnych do tworzenia obiektów sieciowych za pomocą pakietów network
, igraph
i tidygraph
.
Tworzenie obiektów sieciowych
Klasy obiektów sieciowych dla network
, igraph
i tidygraph
są ze sobą ściśle powiązane. Możliwe jest tłumaczenie pomiędzy obiektem network
i obiektem igraph
. Jednak najlepiej jest trzymać te dwa pakiety i ich obiekty oddzielnie. W rzeczywistości, możliwości network
i igraph
nakładają się na siebie do tego stopnia, że najlepszą praktyką jest mieć załadowany tylko jeden z tych pakietów naraz. Zacznę od omówienia pakietu network
, a następnie przejdę do pakietów igraph
i tidygraph
.
network
library(network)
Funkcją używaną do tworzenia obiektu network
jest network()
. Polecenie nie jest szczególnie proste, ale zawsze możesz wpisać ?network()
do konsoli, jeśli się pogubisz. Pierwszym argumentem jest – jak podaje dokumentacja – „macierz podająca strukturę sieci w postaci adjacency, incidence lub edgelist”. Język demonstruje znaczenie macierzy w analizie sieciowej, ale zamiast macierzy mamy listę krawędzi, która spełnia tę samą rolę. Drugim argumentem jest lista atrybutów wierzchołków, która odpowiada liście węzłów. Zauważ, że pakiet network
używa nomenklatury wierzchołków zamiast węzłów. Podobnie jest w przypadku igraph
. Następnie musimy określić typ danych, które zostały wprowadzone do dwóch pierwszych argumentów, określając, że matrix.type
jest pakietem "edgelist"
. Na koniec ustawiamy ignore.eval
na FALSE
, aby nasza sieć mogła być ważona i uwzględniać liczbę liter wzdłuż każdej trasy.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
Typ obiektu tworzonego przez funkcję network()
można zobaczyć, umieszczając routes_network
w funkcji class()
.
class(routes_network)#> "network"
Wypisanie routes_network
na konsolę pokazuje, że struktura obiektu jest zupełnie inna niż obiektów w stylu ramki danych, takich jak edges
i nodes
. Polecenie print ujawnia informacje, które są specyficznie zdefiniowane dla analizy sieci. Pokazuje ono, że w routes_network
jest 13 wierzchołków lub węzłów i 15 krawędzi. Liczby te odpowiadają liczbie wierszy odpowiednio w nodes
i edges
. Możemy również zobaczyć, że zarówno wierzchołki jak i krawędzie zawierają atrybuty takie jak etykieta i waga. Jeszcze więcej informacji, w tym socjomatrycę danych, można uzyskać, wpisując 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
Można teraz uzyskać podstawowy, choć niezbyt estetyczny, graf naszej sieci liter. Oba pakiety network
i igraph
używają bazowego systemu wykresów w R. Konwencje bazowych wykresów różnią się znacznie od konwencji ggplot2 – które omówiłem w poprzednich postach – i dlatego będę się trzymał raczej prostych wykresów, zamiast zagłębiać się w szczegóły tworzenia złożonych wykresów za pomocą bazowego R. W tym przypadku jedyną zmianą, jaką wprowadzam do domyślnej funkcji plot()
pakietu network
, jest zwiększenie rozmiaru węzłów za pomocą argumentu vertex.cex
, aby węzły były bardziej widoczne. Nawet z tego bardzo prostego wykresu możemy już dowiedzieć się czegoś o danych. Wykres wyraźnie pokazuje, że istnieją dwa główne zgrupowania lub skupiska danych, które odpowiadają czasowi, jaki Daniel spędził w Holandii w pierwszych trzech czwartych 1585 roku oraz po przeprowadzce do Bremy we wrześniu.
plot(routes_network, vertex.cex = 3)
Funkcja plot()
z obiektem network
wykorzystuje algorytm Fruchtermana i Reingolda do decydowania o rozmieszczeniu węzłów.6 Algorytm rozmieszczania można zmienić za pomocą argumentu mode
. Poniżej rozmieściłem węzły w okręgu. Nie jest to szczególnie użyteczny układ dla tej sieci, ale daje wyobrażenie o niektórych dostępnych opcjach.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Przejdźmy teraz do omówienia pakietu igraph
. Po pierwsze, musimy oczyścić środowisko w R, usuwając pakiet network
, aby nie kolidował z poleceniami igraph
. Równie dobrze możemy usunąć routes_network
, ponieważ nie będziemy go już używać. Pakiet network
można usunąć za pomocą funkcji detach()
, a routes_network
usuwamy za pomocą rm()
.7 Po tych czynnościach możemy bezpiecznie załadować igraph
.
detach(package:network)rm(routes_network)library(igraph)
Aby utworzyć obiekt igraph
z ramki danych listy krawędziowej, możemy użyć funkcji graph_from_data_frame()
, która jest nieco prostsza niż network()
. W funkcji graph_from_data_frame()
są trzy argumenty: d, vertices i directed. Tutaj d odnosi się do listy krawędzi, wierzchołki do listy węzłów, a directed może być albo TRUE
albo FALSE
w zależności od tego, czy dane są skierowane czy nieskierowane.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Wypisanie na konsolę obiektu igraph
utworzonego przez graph_from_data_frame()
ujawnia informacje podobne do tych z obiektu network
, choć struktura jest bardziej kryptyczna.
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
Główna informacja o obiekcie jest zawarta w DNW- 13 15 --
. Mówi ona, że routes_igraph
jest siecią skierowaną (D), która ma atrybut nazwy (N) i jest ważona (W). Myślnik po W mówi nam, że graf nie jest dwudzielny. Kolejne liczby opisują odpowiednio liczbę węzłów i krawędzi w grafie. Następnie name (v/c), label (v/c), weight (e/n)
podaje informacje o atrybutach grafu. Są to dwa atrybuty wierzchołków (v/c) o nazwach – czyli identyfikatorach – i etykietach oraz atrybut krawędzi (e/n) o wadze. Na końcu znajduje się wydruk wszystkich krawędzi.
Podobnie jak w przypadku pakietu network
, możemy utworzyć wykres z obiektem igraph
za pomocą funkcji plot()
. Jedyną zmianą, jaką wprowadzam tutaj w stosunku do domyślnej, jest zmniejszenie rozmiaru strzałek. Domyślnie igraph
etykietuje węzły za pomocą kolumny etykiety, jeśli taka istnieje, lub za pomocą identyfikatorów.
plot(routes_igraph, edge.arrow.size = 0.2)
Podobnie jak w przypadku wcześniejszego wykresu network
, domyślny wykres igraph
nie jest szczególnie estetyczny, ale wszystkimi aspektami wykresów można manipulować. Tutaj chcę tylko zmienić układ węzłów, aby użyć algorytmu graphopt stworzonego przez Michaela Schmuhla. Algorytm ten ułatwia dostrzeżenie relacji między Haarlemem, Antwerpią i Delft, które są trzema z najbardziej znaczących miejsc w sieci korespondencji, poprzez ich dalsze rozłożenie.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph i ggraph
Pakiety tidygraph
i ggraph
są nowicjuszami w krajobrazie analizy sieciowej, ale razem te dwa pakiety zapewniają rzeczywiste korzyści w porównaniu z pakietami network
i igraph
. tidygraph
i ggraph
reprezentują próbę wprowadzenia analizy sieciowej do przepływu pracy tidyverse. tidygraph
dostarcza sposobu na stworzenie obiektu sieciowego, który bardziej przypomina tibble lub ramkę danych. Dzięki temu możliwe jest użycie wielu funkcji dplyr
do manipulowania danymi sieciowymi. ggraph
daje sposób na wykreślanie wykresów sieciowych przy użyciu konwencji i mocy ggplot2
. Innymi słowy, tidygraph
i ggraph
pozwalają radzić sobie z obiektami sieciowymi w sposób, który jest bardziej spójny z poleceniami używanymi do pracy z tibble i ramkami danych. Jednak prawdziwą obietnicą tidygraph
i ggraph
jest to, że wykorzystują one moc igraph
. Oznacza to, że poświęcasz niewiele z możliwości analizy sieciowej igraph
, używając tidygraph
i ggraph
.
Musimy zacząć jak zawsze od załadowania niezbędnych pakietów.
library(tidygraph)library(ggraph)
Po pierwsze, utwórzmy obiekt sieciowy za pomocą tidygraph
, który nazywa się tbl_graph
. A tbl_graph
składa się z dwóch tibble: tibble edges i tibble nodes. Wygodnie, klasa obiektów tbl_graph
jest opakowaniem wokół obiektu igraph
, co oznacza, że u swoich podstaw obiekt tbl_graph
jest zasadniczo obiektem igraph
.8 Ścisły związek między obiektami tbl_graph
i igraph
powoduje, że istnieją dwa główne sposoby tworzenia obiektu tbl_graph
. Pierwszy z nich polega na wykorzystaniu listy krawędzi i listy węzłów, przy użyciu funkcji tbl_graph()
. Argumenty funkcji są niemal identyczne jak w przypadku graph_from_data_frame()
, z niewielką tylko zmianą nazw argumentów.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
Drugim sposobem utworzenia obiektu tbl_graph
jest konwersja obiektu igraph
lub network
przy użyciu as_tbl_graph()
. W ten sposób moglibyśmy przekonwertować routes_igraph
na obiekt tbl_graph
.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Teraz, gdy utworzyliśmy dwa obiekty tbl_graph
, sprawdźmy je za pomocą funkcji class()
. Wynika z tego, że routes_tidy
i routes_igraph_tidy
są obiektami klasy "tbl_graph" "igraph"
, natomiast routes_igraph
jest obiektem klasy "igraph"
.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Wypisanie obiektu tbl_graph
na konsolę daje wynik drastycznie różny od wyniku obiektu igraph
. Jest to wyjście podobne do zwykłego 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
Wypisanie routes_tidy
pokazuje, że jest to obiekt tbl_graph
z 13 węzłami i 15 krawędziami. Polecenie drukuje również sześć pierwszych wierszy „Danych o węzłach” i trzy pierwsze wiersze „Danych o krawędziach”. Zauważ też, że stwierdza ono, że dane węzła są aktywne. Pojęcie aktywnego pęku w obiekcie tbl_graph
umożliwia manipulowanie danymi w jednym pęku na raz. Domyślnie aktywna jest tibble węzłów, ale można zmienić, która tibble jest aktywna za pomocą funkcji activate()
. Tak więc, jeśli chciałbym zmienić kolejność wierszy w tablicy krawędzi, aby najpierw wypisać te o największej „wadze”, mógłbym użyć activate()
, a następnie arrange()
. Tutaj po prostu wypisuję wynik, zamiast go zapisywać.
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
Ponieważ nie musimy dalej manipulować routes_tidy
, możemy wykreślić wykres za pomocą ggraph
. Podobnie jak ggmap, ggraph
jest rozszerzeniem ggplot2
, dzięki czemu łatwiej jest przenieść podstawowe umiejętności ggplot
do tworzenia wykresów sieciowych. Podobnie jak w przypadku wszystkich wykresów sieciowych, istnieją trzy główne aspekty wykresu ggraph
: węzły, krawędzie i układy. Winiety dla pakietu ggraph pomagają w omówieniu podstawowych aspektów działek ggraph
. ggraph
dodaje specjalne geomy do podstawowego zestawu ggplot
geomów, które są zaprojektowane specjalnie dla sieci. Tak więc istnieje zestaw geom_node
i geom_edge
geomów. Podstawową funkcją wykreślania jest ggraph()
, która pobiera dane, które mają być użyte do wykresu, oraz typ żądanego układu. Oba argumenty dla ggraph()
są zbudowane wokół igraph
. Dlatego ggraph()
może korzystać albo z obiektu igraph
, albo z obiektu tbl_graph
. Ponadto, dostępne algorytmy układów wywodzą się przede wszystkim z igraph
. Wreszcie, ggraph
wprowadza specjalny temat ggplot
, który zapewnia lepsze ustawienia domyślne dla wykresów sieciowych niż normalne ustawienia domyślne ggplot
. Motyw ggraph
można ustawić dla serii wykresów za pomocą polecenia set_graph_style()
uruchamianego przed wykreśleniem wykresów lub za pomocą theme_graph()
w poszczególnych wykresach. Tutaj użyję tej drugiej metody.
Zobaczmy, jak wygląda podstawowy wykres ggraph
. Działka zaczyna się od ggraph()
i danych. Następnie dodaję podstawowe geomy krawędzi i węzłów. W ramach geomów krawędzi i węzłów nie są potrzebne żadne argumenty, ponieważ pobierają one informacje z danych podanych w ggraph()
.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Jak widać, struktura polecenia jest podobna do tej z ggplot
z oddzielnymi warstwami dodanymi za pomocą znaku +
. Podstawowy wykres ggraph
wygląda podobnie do tych z network
i igraph
, jeśli nie jeszcze bardziej prosto, ale możemy użyć podobnych poleceń do ggplot
, aby stworzyć bardziej informacyjny wykres. Możemy pokazać „wagę” krawędzi – lub ilość listów wysłanych każdą trasą – używając width w funkcji geom_edge_link()
. Aby szerokość linii zmieniała się w zależności od zmiennej weight, umieszczamy ten argument w funkcji aes()
. Aby kontrolować maksymalną i minimalną szerokość krawędzi, używam scale_edge_width()
i ustawiam range
. Wybieram stosunkowo małą szerokość dla minimum, ponieważ istnieje znaczna różnica pomiędzy maksymalną i minimalną liczbą listów wysyłanych trasami. Możemy również oznaczyć węzły nazwami miejscowości, ponieważ jest ich stosunkowo niewiele. Wygodnie, geom_node_text()
posiada argument repel, który zapewnia, że etykiety nie będą się nakładać na węzły w sposób podobny do pakietu ggrepel. Dodaję trochę przezroczystości do krawędzi za pomocą argumentu alpha. Używam również labs()
do zmiany etykiety legendy na „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()
Oprócz możliwości wyboru układu dostarczanych przez igraph
, ggraph
implementuje również swoje własne układy. Na przykład, można użyć ggraph's
koncepcji okrągłości do tworzenia diagramów łukowych. Tutaj rozmieściłem węzły w linii poziomej, a krawędzie narysowałem jako łuki. W przeciwieństwie do poprzedniego wykresu, ten wykres wskazuje kierunkowość krawędzi.9 Krawędzie powyżej linii poziomej przesuwają się z lewej strony na prawą, podczas gdy krawędzie poniżej linii przesuwają się z prawej strony na lewą. Zamiast dodawać punkty dla węzłów, zamieszczam tylko nazwy etykiet. Używam estetyki o tej samej szerokości, aby zaznaczyć różnicę w wadze każdej krawędzi. Zauważ, że w tym wątku używam obiektu igraph
jako danych dla wykresu, co nie robi praktycznej różnicy.
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()
Interaktywne wykresy sieciowe z visNetwork i networkD3
Zestaw pakietów htmlwidgets umożliwia wykorzystanie R do tworzenia interaktywnych wizualizacji w JavaScript. Tutaj pokażę, jak tworzyć wykresy za pomocą pakietów visNetwork
i networkD3
. Te dwa pakiety używają różnych bibliotek JavaScript do tworzenia swoich wykresów. visNetwork
używa vis.js, podczas gdy networkD3
używa popularnej biblioteki wizualizacji d3 do tworzenia swoich wykresów. Jedną z trudności w pracy zarówno z visNetwork
jak i networkD3
jest to, że oczekują one, że listy krawędzi i węzłów będą używać specyficznej nomenklatury. Powyższa manipulacja danymi jest zgodna z podstawową strukturą dla visNetwork
, ale dla networkD3
trzeba będzie wykonać trochę pracy. Mimo tej niedogodności oba pakiety mają szeroki zakres możliwości tworzenia wykresów i oba mogą pracować z obiektami i układami igraph
.
library(visNetwork)library(networkD3)
visNetwork
Funkcja visNetwork()
używa listy węzłów i listy krawędzi do utworzenia interaktywnego wykresu. Lista węzłów musi zawierać kolumnę „id”, a lista krawędzi musi mieć kolumny „od” i „do”. Funkcja tworzy również etykiety dla węzłów, używając nazw miast z kolumny „label” na liście węzłów. Wykresem wynikowym można się bawić. Możesz przesuwać węzły, a wykres użyje algorytmu, aby utrzymać je w odpowiedniej odległości od siebie. Można również powiększać i pomniejszać działkę oraz przesuwać ją w celu ponownego wyśrodkowania.
visNetwork(nodes, edges)
visNetwork
może używać układów igraph
, zapewniając dużą różnorodność możliwych układów. Dodatkowo, można użyć visIgraph()
do bezpośredniego wykreślenia obiektu igraph
. Tutaj będę trzymał się przepływu pracy nodes
i edges
i użyję układu igraph
, aby dostosować wykres do własnych potrzeb. Dodam również zmienną, aby zmienić szerokość krawędzi, tak jak zrobiliśmy to w przypadku ggraph
. visNetwork()
używa nazw kolumn z list krawędzi i węzłów do wykreślania atrybutów sieci zamiast argumentów w wywołaniu funkcji. Oznacza to, że konieczne jest wykonanie pewnych manipulacji danymi, aby uzyskać kolumnę „width” na liście krawędzi. Atrybut width dla visNetwork()
nie skaluje wartości, więc musimy to zrobić ręcznie. Obie te czynności można wykonać za pomocą funkcji mutate()
i kilku prostych działań arytmetycznych. Tutaj tworzę nową kolumnę w edges
i skaluję wartości wag dzieląc je przez 5. Dodanie 1 do wyniku daje sposób na utworzenie minimalnej szerokości.
edges <- mutate(edges, width = weight/5 + 1)
Po wykonaniu tych czynności możemy utworzyć graf o zmiennej szerokości krawędzi. Wybieram również algorytm układu z igraph
i dodaję strzałki do krawędzi, umieszczając je w środku krawędzi.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
networkD3
Nieco więcej pracy wymaga przygotowanie danych do utworzenia grafu networkD3
. Utworzenie grafu networkD3
z listą krawędzi i węzłów wymaga, aby ich identyfikatory były serią liczb całkowitych zaczynających się od 0. Obecnie identyfikatory węzłów w naszych danych zaczynają się od 1, więc musimy trochę pomanipulować danymi. Możliwe jest przenumerowanie węzłów przez odjęcie 1 od kolumn ID w ramkach danych nodes
i edges
. Po raz kolejny można to zrobić za pomocą funkcji mutate()
. Celem jest odtworzenie bieżących kolumn z jednoczesnym odjęciem 1 od każdego identyfikatora. Funkcja mutate()
działa poprzez tworzenie nowej kolumny, ale możemy ją zastąpić nadając nowej kolumnie taką samą nazwę jak starej kolumnie. W tym przypadku nazwałem nowe ramki danych przyrostkiem d3, aby odróżnić je od poprzednich nodes
i edges
ramek danych.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Teraz możliwe jest wykreślenie wykresu networkD3
. W przeciwieństwie do funkcji visNetwork()
, funkcja forceNetwork()
używa serii argumentów do dostosowania wykresu i atrybutów sieci wydruku. Argumenty „Links” i „Nodes” dostarczają danych do wykresu w postaci list krawędzi i węzłów. Funkcja wymaga również argumentów „NodeID” i „Group”. Dane używane tutaj nie mają żadnych grup, więc po prostu każdy węzeł jest swoją własną grupą, co w praktyce oznacza, że wszystkie węzły będą miały różne kolory. Dodatkowo, poniższy argument mówi funkcji, że sieć ma pola „Źródło” i „Cel”, a więc jest skierowana. Do tego wykresu dołączam „Value”, który skaluje szerokość krawędzi zgodnie z kolumną „weight” na liście krawędzi. Na koniec dodaję kilka estetycznych poprawek, aby uczynić węzły nieprzezroczystymi i zwiększyć rozmiar czcionki etykiet, aby poprawić czytelność. Wynik jest bardzo podobny do pierwszego visNetwork()
wykresu, który stworzyłem, ale z inną estetyką.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
Jedną z głównych zalet networkD3
jest to, że implementuje on diagram Sankeya w stylu d3. Diagram Sankeya dobrze pasuje do listów wysłanych do Daniela w 1585 roku. W danych nie ma zbyt wielu węzłów, dzięki czemu łatwiej jest zwizualizować przepływ listów. Tworzenie wykresu Sankeya wykorzystuje funkcję sankeyNetwork()
, która przyjmuje wiele z tych samych argumentów, co funkcja forceNetwork()
. Ten wykres nie wymaga argumentu grupy, a jedyną inną zmianą jest dodanie „jednostki”. Zapewnia to etykietę dla wartości, które wyskakują w końcówce narzędzia, gdy kursor najeżdża na element wykresu.10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Dalsza lektura na temat analizy sieci
W tym poście starałem się przedstawić ogólne wprowadzenie do tworzenia i wykreślania obiektów typu sieciowego w R przy użyciu pakietów network
, igraph
, tidygraph
i ggraph
dla działek statycznych oraz visNetwork
i networkD3
dla działek interaktywnych. Przedstawiłem te informacje z pozycji niespecjalisty w dziedzinie teorii sieci. W szczególności, nie omówiłem statystycznej analizy sieci. Na szczęście istnieje wiele zasobów dotyczących analizy sieci w ogóle, a w R w szczególności.
Najlepszym wprowadzeniem do sieci, jakie znalazłem dla niewtajemniczonych, jest Network Visualization with R Katii Ognyanovej. Przedstawia ono zarówno pomocne wprowadzenie do wizualnych aspektów sieci, jak i bardziej dogłębny samouczek na temat tworzenia wykresów sieciowych w R. Ognyanova używa przede wszystkim igraph
, ale wprowadza również sieci interaktywne.
Dwie stosunkowo niedawne książki opublikowane przez Springera na temat analizy sieciowej za pomocą R. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) to bardzo przydatne wprowadzenie do analizy sieciowej za pomocą R. Luke obejmuje zarówno garnitur pakietów statnet, jak i igragh
. Zawartość jest na bardzo przystępnym poziomie przez cały czas. Bardziej zaawansowana jest Eric D. Kolaczyk i Gábor Csárdi’s, Statistical Analysis of Network Data with R (2014). Książka Kolaczyka i Csárdi’ego używa głównie igraph
, ponieważ Csárdi jest głównym opiekunem pakietu igraph
dla R. Książka ta wchodzi dalej w zaawansowane tematy dotyczące statystycznej analizy sieci. Pomimo użycia bardzo technicznego języka, pierwsze cztery rozdziały są ogólnie przystępne z punktu widzenia niespecjalisty.
Lista prowadzona przez François Briatte jest dobrym przeglądem zasobów dotyczących analizy sieci w ogóle. Warto również zapoznać się z serią postów Scotta Weingarta Networks Demystified.
-
Jednym z przykładów zainteresowania analizą sieciową w ramach humanistyki cyfrowej jest nowo powstałe czasopismo Journal of Historical Network Research. ︎
-
Dobry opis klasy obiektów
network
, w tym omówienie jej relacji do klasy obiektówigraph
, zob. Carter Butts, „network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Jest to specyficzna struktura oczekiwana przez
visNetwork
, a jednocześnie zgodna z ogólnymi oczekiwaniami innych pakietów. ︎ -
To jest oczekiwana kolejność kolumn dla niektórych pakietów sieciowych, których będę używał poniżej. ︎
-
ungroup()
nie jest bezwzględnie konieczne w tym przypadku. Jeśli jednak nie zgrupujesz ramki danych, nie jest możliwe usunięcie kolumn „źródło” i „miejsce docelowe”, jak to robię w dalszej części skryptu. ︎ -
Thomas M. J. Fruchterman i Edward M. Reingold, „Graph Drawing by Force-Directed Placement,” Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
Funkcja
rm()
jest przydatna, jeśli środowisko pracy w R ulegnie dezorganizacji, ale nie chcesz czyścić całego środowiska i zaczynać od nowa. ︎ -
Zależność między obiektami
tbl_graph
iigraph
jest podobna do zależności między obiektamitibble
idata.frame
. ︎ -
Możliwe jest, aby
ggraph
rysował strzałki, ale nie pokazałem tego tutaj. ︎ -
Wyświetlanie końcówki narzędzia może trochę potrwać. ︎
Dodaj komentarz