W 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 analizę sieci centralnym narzędziem 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.

Network Analysis: Nodes and Edges

Dwa podstawowe aspekty sieci to mnogość oddzielnych jednostek i połączenia między nimi. Słownictwo może być nieco 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 pakietów 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 magnitudy, 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ć oddzielną 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 zawierającą te same dane.

#> 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 przy tworzeniu 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żyte w tym poście i skrypt R 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” i „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 analizy sieciowej.

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 kolumnę „id” do ramki danych nodes, która zawiera liczby od jednego do liczby całkowitej liczby 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 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”. Kolumna przeniesiona z złączenia „destination” zostanie przemianowana 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, które odpowiadają 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 utrzymywać 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 najlepiej 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żmy, ż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 "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)

Możesz zobaczyć typ obiektu utworzonego przez funkcję network(), 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 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 jasno pokazuje, że istnieją dwa główne zgrupowania danych, które odpowiadają czasowi, jaki Daniel spędził w Holandii w pierwszych trzech kwartałach 1585 roku oraz po jego 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 on, ż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.

Tak jak w przypadku pakietu network, możemy utworzyć wykres z obiektem igraph za pomocą funkcji plot(). Jedyną zmianą, jaką wprowadzam do domyślnych ustawień 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ślna działka igraph nie jest szczególnie estetyczna, ale wszystkimi jej aspektami można manipulować. W tym przypadku chcę tylko zmienić układ węzłów tak, aby korzystał z algorytmu graphopt stworzonego przez Michaela Schmuhla. Algorytm ten ułatwia dostrzeżenie relacji między Haarlemem, Antwerpią i Delft, które są trzema najbardziej znaczącymi miejscami 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ą prawdziwe 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 zapewnia sposób na stworzenie obiektu sieciowego, który bardziej przypomina tibble lub ramkę danych. Umożliwia to 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 prawdziwa obietnica tidygraph i ggraph polega na tym, ż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. Obiekt 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 możemy 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 niej, ż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 drastycznie inny wynik niż w przypadku 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 aktywnej wstęgi w obiekcie tbl_graph umożliwia manipulowanie danymi w jednej wstędze na raz. Domyślnie aktywna jest belka węzłów, ale można zmienić, która belka 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(). W tym przypadku 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 ggraph działek. 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 używać albo obiektu igraph, albo obiektu tbl_graph. Ponadto, dostępne algorytmy układów przede wszystkim wywodzą się 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 po trasach. Możemy również oznaczyć węzły nazwami miejscowości, ponieważ węzłów jest 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 „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 wykresie używam obiektu igraph jako danych dla wykresu, co nie robi żadnej 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 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ć wykres oraz przesuwać go w celu ponownego wyśrodkowania.

visNetwork(nodes, edges)

visNetwork może używać igraph układów, 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. 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 ze zmiennymi szerokościami 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 ID. 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 utworzył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 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 osoby nie będącej specjalistą w teorii sieci. Omówiłem tylko bardzo niewielki procent możliwości R w zakresie analizy sieci. W szczególności nie omówiłem statystycznej analizy sieci. Na szczęście istnieje wiele zasobów na temat 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.

Istnieją dwie stosunkowo niedawne książki opublikowane na temat analizy sieciowej z R przez Springera. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) jest bardzo użytecznym wprowadzeniem 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. Ta książka 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.

  1. Jednym z przykładów zainteresowania analizą sieciową w ramach humanistyki cyfrowej jest nowo uruchomiony Journal of Historical Network Research. ︎

  2. Dobry opis klasy obiektów network, w tym omówienie jej relacji do klasy obiektów igraph, zob. Carter Butts, „network: A Package for Managing Relational Data in R”, Journal of Statistical Software, 24 (2008): 1-36 ︎

  3. Jest to specyficzna struktura oczekiwana przez visNetwork, a jednocześnie zgodna z ogólnymi oczekiwaniami innych pakietów. ︎

  4. To jest oczekiwana kolejność kolumn dla niektórych pakietów sieciowych, których będę używał poniżej. ︎

  5. ungroup() nie jest w tym przypadku konieczne. Jeśli jednak nie rozgrupujesz ramki danych, nie będzie możliwe usunięcie kolumn „źródło” i „miejsce docelowe”, co zrobię w dalszej części skryptu. ︎

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

  7. Funkcja rm() jest przydatna, gdy środowisko pracy w R ulegnie dezorganizacji, ale nie chcemy czyścić całego środowiska i zaczynać od nowa. ︎

  8. Zależność między obiektami tbl_graph i igraph jest podobna do zależności między obiektami tibble i data.frame. ︎

  9. Możliwe jest, aby ggraph rysował strzałki, ale nie pokazałem tego tutaj. ︎

  10. Wyświetlanie końcówki narzędzia może trochę potrwać. ︎

r

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.