Einführung in die Netzwerkanalyse mit R
On November 4, 2021 by adminIn vielen Bereichen ist die Netzwerkanalyse zu einem immer beliebteren Instrument für Wissenschaftler geworden, um die Komplexität der Beziehungen zwischen Akteuren aller Art zu erfassen. Das Versprechen der Netzwerkanalyse besteht darin, die Beziehungen zwischen den Akteuren in den Vordergrund zu stellen, anstatt die Akteure als isolierte Einheiten zu betrachten. Die Betonung der Komplexität und die Entwicklung einer Vielzahl von Algorithmen zur Messung verschiedener Aspekte von Netzwerken machen die Netzwerkanalyse zu einem zentralen Werkzeug für die digitalen Geisteswissenschaften.1 Dieser Beitrag bietet eine Einführung in die Arbeit mit Netzwerken in R am Beispiel des Städtenetzwerks in der Korrespondenz von Daniel van der Meulen aus dem Jahr 1585.
Es gibt eine Reihe von Anwendungen, die für die Netzwerkanalyse und die Erstellung von Netzwerkgraphen entwickelt wurden, wie zum Beispiel gephi und cytoscape. Obwohl nicht speziell dafür entwickelt, hat sich R zu einem leistungsfähigen Werkzeug für die Netzwerkanalyse entwickelt. Die Stärke von R im Vergleich zu eigenständiger Netzwerkanalysesoftware ist dreifach. Erstens ermöglicht R eine reproduzierbare Forschung, die mit GUI-Anwendungen nicht möglich ist. Zweitens bietet die Datenanalyseleistung von R robuste Werkzeuge zur Manipulation von Daten, um sie für die Netzwerkanalyse vorzubereiten. Und schließlich gibt es eine ständig wachsende Anzahl von Paketen, die R zu einem vollständigen Netzwerkanalysewerkzeug machen. Zu den wichtigsten Netzwerkanalysepaketen für R gehören die statnet-Pakete und igraph
. Darüber hinaus hat Thomas Lin Pedersen kürzlich die Pakete tidygraph
und ggraph
veröffentlicht, die die Leistungsfähigkeit von igraph
in einer Weise nutzen, die mit dem tidyverse-Arbeitsablauf vereinbar ist. R kann auch verwendet werden, um interaktive Netzwerkgraphen mit dem htmlwidgets-Framework zu erstellen, das R-Code in JavaScript übersetzt.
Dieser Beitrag beginnt mit einer kurzen Einführung in das grundlegende Vokabular der Netzwerkanalyse, gefolgt von einer Diskussion des Prozesses, wie man Daten in die richtige Struktur für die Netzwerkanalyse bringt. Die Netzwerkanalysepakete haben alle ihre eigenen Objektklassen implementiert. In diesem Beitrag zeige ich, wie man die spezifischen Objektklassen für die statnet-Paketsuite mit dem Paket network
sowie für igraph
und tidygraph
, das auf der Implementierung von igraph
basiert, erstellt. Schließlich wende ich mich der Erstellung von interaktiven Graphen mit den Paketen vizNetwork
und networkD3
zu.
Netzwerkanalyse: Nodes and Edges
Die beiden Hauptaspekte von Netzwerken sind eine Vielzahl von separaten Einheiten und die Verbindungen zwischen ihnen. Das Vokabular kann ein wenig technisch sein und sogar zwischen verschiedenen Disziplinen, Paketen und Software uneinheitlich. Die Entitäten werden als Knoten oder Scheitelpunkte eines Graphen bezeichnet, während die Verbindungen als Kanten oder Links bezeichnet werden. In diesem Beitrag werde ich hauptsächlich die Nomenklatur von Knoten und Kanten verwenden, es sei denn, es werden Pakete besprochen, die ein anderes Vokabular verwenden.
Die Pakete für die Netzwerkanalyse benötigen Daten in einer bestimmten Form, um die spezielle Art von Objekt zu erstellen, die von jedem Paket verwendet wird. Die Objektklassen für network
, igraph
und tidygraph
basieren alle auf Adjazenzmatrizen, auch bekannt als Soziomatrizen.2 Eine Adjazenzmatrix ist eine quadratische Matrix, in der die Spalten- und Zeilennamen die Knoten des Netzwerks darstellen. Innerhalb der Matrix zeigt eine 1 an, dass es eine Verbindung zwischen den Knoten gibt, und eine 0 bedeutet, dass keine Verbindung besteht. Adjazenzmatrizen implementieren eine ganz andere Datenstruktur als Datenrahmen und passen nicht in den tidyverse-Workflow, den ich in meinen früheren Beiträgen verwendet habe. Hilfreich ist, dass die spezialisierten Netzwerkobjekte auch aus einem Edge-List-Datenrahmen erstellt werden können, die in den Tidyverse-Workflow passen. In diesem Beitrag werde ich mich an die Datenanalysetechniken von tidyverse halten, um Kantenlisten zu erstellen, die dann in die spezifischen Objektklassen für network
, igraph
und tidygraph
umgewandelt werden.
Eine Kantenliste ist ein Datenrahmen, der mindestens zwei Spalten enthält, eine Spalte mit Knoten, die die Quelle einer Verbindung sind, und eine weitere Spalte mit Knoten, die das Ziel der Verbindung sind. Die Knoten in den Daten werden durch eindeutige IDs identifiziert. Wenn die Unterscheidung zwischen Quelle und Ziel sinnvoll ist, ist das Netz gerichtet. Ist die Unterscheidung nicht sinnvoll, handelt es sich um ein ungerichtetes Netz. Bei dem Beispiel der Briefe, die zwischen Städten verschickt werden, ist die Unterscheidung zwischen Quelle und Ziel eindeutig sinnvoll, so dass das Netzwerk gerichtet ist. In den folgenden Beispielen bezeichne ich die Quellspalte als „von“ und die Zielspalte als „nach“. Als Knoten-IDs verwende ich ganze Zahlen, die mit 1 beginnen.3 Eine Kantenliste kann auch zusätzliche Spalten enthalten, die Attribute der Kanten beschreiben, wie z. B. einen Größenaspekt für eine Kante. Wenn die Kanten ein Größenattribut haben, wird der Graph als gewichtet angesehen.
Kantenlisten enthalten alle Informationen, die zur Erstellung von Netzwerkobjekten erforderlich sind, aber manchmal ist es besser, auch eine separate Knotenliste zu erstellen. Im einfachsten Fall ist eine Knotenliste ein Datenrahmen mit einer einzigen Spalte – die ich als „id“ bezeichne -, in der die in der Kantenliste gefundenen Knoten-IDs aufgeführt sind. Der Vorteil der Erstellung einer separaten Knotenliste ist die Möglichkeit, dem Datenrahmen Attributspalten hinzuzufügen, wie z. B. die Namen der Knoten oder jegliche Art von Gruppierungen. Im Folgenden gebe ich ein Beispiel für minimale Kanten- und Knotenlisten, die mit der Funktion tibble()
erstellt wurden.
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
Vergleichen Sie dies mit einer Adjazenzmatrix mit denselben Daten.
#> 1 2 3 4#> 1 0 1 0 0#> 2 0 0 1 1#> 3 0 1 0 0#> 4 1 0 0 0
Erstellen von Kanten- und Knotenlisten
Um Netzwerkobjekte aus der Datenbank der Briefe zu erstellen, die Daniel van der Meulen im Jahr 1585 erhielt, werde ich sowohl eine Kanten- als auch eine Knotenliste erstellen. Dazu muss das Paket dplyr verwendet werden, um den Datenrahmen der an Daniel gesendeten Briefe zu bearbeiten und in zwei Datenrahmen oder Tibbles mit der Struktur von Kanten- und Knotenlisten aufzuteilen. In diesem Fall sind die Knoten die Städte, aus denen Daniels Korrespondenten ihm Briefe geschickt haben, und die Städte, in denen er sie erhalten hat. Die Knotenliste enthält eine „label“-Spalte, die die Namen der Städte enthält. Die Kantenliste enthält außerdem eine Attributspalte, die die Anzahl der Briefe anzeigt, die zwischen jedem Städtepaar verschickt wurden. Der Arbeitsablauf zur Erstellung dieser Objekte ähnelt dem, den ich in meiner kurzen Einführung in R und bei der Geokodierung mit R verwendet habe. Wenn Sie dem folgen möchten, finden Sie die in diesem Beitrag verwendeten Daten und das verwendete R-Skript auf GitHub.
Der erste Schritt besteht darin, die tidyverse
-Bibliothek zu laden, um die Daten zu importieren und zu manipulieren. Das Ausdrucken des letters
-Datenrahmens zeigt, dass er vier Spalten enthält: „Verfasser“, „Quelle“, „Ziel“ und „Datum“. In diesem Beispiel werden wir uns nur mit den Spalten „Quelle“ und „Ziel“ befassen.
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
Knotenliste
Der Arbeitsablauf zur Erstellung einer Knotenliste ähnelt dem, den ich in einem früheren Beitrag verwendet habe, um die Liste der Städte für die Geokodierung der Daten zu erhalten. Wir wollen die einzelnen Städte aus den Spalten „Quelle“ und „Ziel“ abrufen und dann die Informationen aus diesen Spalten miteinander verbinden. Im nachstehenden Beispiel ändere ich die Befehle, die ich im vorherigen Beitrag verwendet habe, geringfügig ab, damit der Name für die Spalten mit den Städtenamen sowohl für die sources
– als auch für die destinations
-Datenrahmen gleich ist, um die full_join()
-Funktion zu vereinfachen. Ich benenne die Spalte mit den Städtenamen in „label“ um, um das von Netzwerkanalysepaketen verwendete Vokabular zu übernehmen.
sources <- letters %>% distinct(source) %>% rename(label = source)destinations <- letters %>% distinct(destination) %>% rename(label = destination)
Um einen einzigen Datenrahmen mit einer Spalte mit den eindeutigen Orten zu erstellen, müssen wir eine vollständige Verknüpfung verwenden, da wir alle eindeutigen Orte sowohl von den Quellen der Briefe als auch von den Zielen einschließen wollen.
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
Das Ergebnis ist ein Datenrahmen mit einer Variablen. Die im Datenrahmen enthaltene Variable ist jedoch nicht wirklich das, wonach wir suchen. Die Spalte „label“ enthält die Namen der Knoten, aber wir wollen auch eindeutige IDs für jede Stadt haben. Dazu fügen wir dem Datenrahmen nodes
eine „id“-Spalte hinzu, die Zahlen von eins bis zu der Gesamtzahl der Zeilen im Datenrahmen enthält. Eine hilfreiche Funktion für diesen Arbeitsablauf ist rowid_to_column()
, die eine Spalte mit den Werten der Zeilen-IDs hinzufügt und die Spalte am Anfang des Datenrahmens platziert.4 Beachten Sie, dass rowid_to_column()
ein Pipe-Befehl ist und es daher möglich ist, full_join()
und das Hinzufügen der „id“-Spalte mit einem einzigen Befehl auszuführen. Das Ergebnis ist eine Knotenliste mit einer ID-Spalte und einem Label-Attribut.
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
Kantenliste
Die Erstellung einer Kantenliste ähnelt der obigen, wird aber durch die Notwendigkeit, mit zwei ID-Spalten statt einer zu arbeiten, erschwert. Außerdem soll eine Gewichtsspalte erstellt werden, in der die Anzahl der zwischen den einzelnen Knoten gesendeten Buchstaben vermerkt wird. Dazu verwende ich denselben group_by()
– und summarise()
-Workflow, den ich in früheren Beiträgen besprochen habe. Der Unterschied besteht darin, dass wir den Datenrahmen nach zwei Spalten – „Quelle“ und „Ziel“ – gruppieren wollen, anstatt nur nach einer. Bisher habe ich die Spalte, die die Anzahl der Beobachtungen pro Gruppe zählt, „Anzahl“ genannt, aber hier übernehme ich die Nomenklatur der Netzwerkanalyse und nenne sie „Gewicht“. Mit dem letzten Befehl in der Pipeline wird die durch die Funktion group_by()
eingeführte Gruppierung des Datenrahmens aufgehoben. Dies erleichtert die ungehinderte Manipulation des resultierenden per_route
-Datenrahmens.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
Wie die Knotenliste hat per_route
nun die gewünschte Grundform, aber wir haben wieder das Problem, dass die Spalten „Quelle“ und „Ziel“ Bezeichnungen und keine IDs enthalten. Wir müssen die IDs, die in nodes
zugewiesen wurden, mit jedem Ort in den Spalten „Quelle“ und „Ziel“ verknüpfen. Dies kann mit einer anderen Verknüpfungsfunktion erreicht werden. Tatsächlich ist es notwendig, zwei Verknüpfungen durchzuführen, eine für die Spalte „Quelle“ und eine für „Ziel“. In diesem Fall verwende ich einen left_join()
mit per_route
als linken Datenrahmen, weil wir die Anzahl der Zeilen in per_route
beibehalten wollen. Während wir left_join
erstellen, wollen wir auch die beiden „id“-Spalten umbenennen, die von nodes
übernommen werden. Für die Verknüpfung mit der „Quell“-Spalte werde ich die Spalte in „von“ umbenennen. Die Spalte, die aus dem „Ziel“-Join übernommen wird, wird in „to“ umbenannt. Es wäre möglich, beide Verknüpfungen in einem einzigen Befehl mit Hilfe der Pipe durchzuführen. Der Übersichtlichkeit halber werde ich die Joins jedoch in zwei separaten Befehlen durchführen. Da die Verknüpfung über zwei Befehle erfolgt, ändert sich der Datenrahmen am Anfang der Pipeline von per_route
zu edges
, der durch den ersten Befehl erstellt wird.
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)
Nun, da edges
„von“- und „bis“-Spalten mit Knoten-IDs hat, müssen wir die Spalten neu anordnen, um „von“ und „bis“ auf die linke Seite des Datenrahmens zu bringen. Derzeit enthält der Datenrahmen edges
noch die Spalten „Quelle“ und „Ziel“ mit den Namen der Städte, die den IDs entsprechen. Diese Daten sind jedoch überflüssig, da sie bereits in nodes
vorhanden sind. Daher werde ich nur die Spalten „von“, „bis“ und „Gewicht“ in die Funktion select()
aufnehmen.
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
Der Datenrahmen edges
sieht nicht sehr beeindruckend aus; es sind drei Spalten mit ganzen Zahlen. Aber edges
kombiniert mit nodes
liefert uns alle Informationen, die wir brauchen, um mit den Paketen network
, igraph
und tidygraph
Netzwerkobjekte zu erstellen.
Erstellen von Netzwerkobjekten
Die Netzwerkobjektklassen für network
, igraph
und tidygraph
sind alle eng miteinander verbunden. Es ist möglich, zwischen einem network
-Objekt und einem igraph
-Objekt zu übersetzen. Es ist jedoch am besten, die beiden Pakete und ihre Objekte getrennt zu halten. Tatsächlich überschneiden sich die Fähigkeiten von network
und igraph
in einem solchen Ausmaß, dass es am besten ist, immer nur eines der Pakete zu laden. Ich beginne mit dem Paket network
und gehe dann zu den Paketen igraph
und tidygraph
über.
Netzwerk
library(network)
Die Funktion zum Erstellen eines network
-Objekts ist network()
. Der Befehl ist nicht besonders einfach, aber Sie können jederzeit ?network()
in die Konsole eingeben, wenn Sie verwirrt sind. Das erste Argument ist – wie in der Dokumentation angegeben – „eine Matrix, die die Netzwerkstruktur in Form von Adjazenz, Inzidenz oder Kantenliste angibt.“ Die Sprache demonstriert die Bedeutung von Matrizen in der Netzwerkanalyse, aber statt einer Matrix haben wir eine Kantenliste, die dieselbe Rolle erfüllt. Das zweite Argument ist eine Liste von Vertex-Attributen, die der Knotenliste entspricht. Beachten Sie, dass das network
-Paket die Nomenklatur von Scheitelpunkten anstelle von Knoten verwendet. Das gleiche gilt für igraph
. Dann müssen wir den Typ der Daten angeben, die in die ersten beiden Argumente eingegeben wurden, indem wir angeben, dass matrix.type
ein "edgelist"
ist. Schließlich setzen wir ignore.eval
auf FALSE
, damit unser Netzwerk gewichtet werden kann und die Anzahl der Buchstaben entlang jeder Route berücksichtigt wird.
routes_network <- network(edges, vertex.attr = nodes, matrix.type = "edgelist", ignore.eval = FALSE)
Den Typ des von der Funktion network()
erzeugten Objekts können Sie sehen, indem Sie routes_network
in die Funktion class()
einfügen.
class(routes_network)#> "network"
Der Ausdruck von routes_network
auf der Konsole zeigt, dass sich die Struktur des Objekts deutlich von Datenrahmen-Objekten wie edges
und nodes
unterscheidet. Der Druckbefehl offenbart Informationen, die speziell für die Netzwerkanalyse definiert sind. Er zeigt, dass es in routes_network
13 Scheitelpunkte oder Knoten und 15 Kanten gibt. Diese Zahlen entsprechen der Anzahl der Zeilen in nodes
bzw. edges
. Wir können auch sehen, dass die Knoten und Kanten Attribute wie Bezeichnung und Gewicht enthalten. Sie können noch mehr Informationen, einschließlich einer Soziomatrix der Daten, erhalten, indem Sie 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
Es ist nun möglich, einen rudimentären, wenn auch nicht allzu ästhetischen Graphen unseres Buchstabennetzes zu erhalten. Sowohl das network
– als auch das igraph
-Paket verwenden das Base-Plot-System von R. Die Konventionen für Base-Plots unterscheiden sich erheblich von denen von ggplot2 – die ich in früheren Beiträgen besprochen habe -, und daher werde ich mich auf eher einfache Plots beschränken, anstatt auf die Einzelheiten der Erstellung komplexer Plots mit Base R einzugehen. In diesem Fall besteht die einzige Änderung, die ich an der Standardfunktion plot()
für das network
-Paket vornehme, darin, die Größe der Knoten mit dem Argument vertex.cex
zu erhöhen, um die Knoten besser sichtbar zu machen. Selbst mit diesem sehr einfachen Diagramm können wir bereits etwas über die Daten lernen. Das Diagramm macht deutlich, dass es zwei Hauptgruppierungen oder Cluster der Daten gibt, die der Zeit entsprechen, die Daniel in den ersten drei Vierteln des Jahres 1585 in Holland verbrachte, und der Zeit nach seinem Umzug nach Bremen im September.
plot(routes_network, vertex.cex = 3)
Die Funktion plot()
mit einem network
-Objekt verwendet den Algorithmus von Fruchterman und Reingold, um über die Platzierung der Knoten zu entscheiden.6 Sie können den Layout-Algorithmus mit dem Argument mode
ändern. Unten habe ich die Knoten in einem Kreis angeordnet. Dies ist keine besonders nützliche Anordnung für dieses Netzwerk, aber es gibt eine Vorstellung von einigen der verfügbaren Optionen.
plot(routes_network, vertex.cex = 3, mode = "circle")
igraph
Lassen Sie uns nun das igraph
-Paket diskutieren. Zunächst müssen wir die Umgebung in R aufräumen, indem wir das Paket network
entfernen, damit es die igraph
-Befehle nicht behindert. Wir können auch routes_network
entfernen, da wir es nicht mehr verwenden werden. Das network
-Paket kann mit der Funktion detach()
entfernt werden, und routes_network
wird mit rm()
entfernt.7 Danach können wir igraph
sicher laden.
detach(package:network)rm(routes_network)library(igraph)
Um ein igraph
-Objekt aus einem Edge-List-Datenrahmen zu erstellen, können wir die Funktion graph_from_data_frame()
verwenden, die etwas einfacher ist als network()
. Die Funktion graph_from_data_frame()
hat drei Argumente: d, vertices und directed. Dabei bezieht sich d auf die Kantenliste, vertices auf die Knotenliste, und directed kann entweder TRUE
oder FALSE
sein, je nachdem, ob die Daten gerichtet oder ungerichtet sind.
routes_igraph <- graph_from_data_frame(d = edges, vertices = nodes, directed = TRUE)
Wenn man das durch graph_from_data_frame()
erzeugte igraph
-Objekt auf der Konsole ausgibt, erhält man ähnliche Informationen wie bei einem network
-Objekt, obwohl die Struktur kryptischer ist.
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
Die wichtigsten Informationen über das Objekt sind in DNW- 13 15 --
enthalten. Daraus geht hervor, dass routes_igraph
ein gerichtetes Netz (D) ist, das ein Namensattribut (N) hat und gewichtet ist (W). Der Bindestrich nach W besagt, dass der Graph nicht zweistufig ist. Die folgenden Zahlen bezeichnen die Anzahl der Knoten bzw. Kanten des Graphen. Als nächstes gibt name (v/c), label (v/c), weight (e/n)
Informationen über die Attribute des Graphen. Es gibt zwei Vertex-Attribute (v/c) mit Namen – das sind die IDs – und Bezeichnungen und ein Kantenattribut (e/n) mit Gewicht. Schließlich gibt es noch einen Ausdruck aller Kanten.
Genauso wie mit dem Paket network
können wir mit der Funktion plot()
ein Diagramm mit einem igraph
-Objekt erstellen. Die einzige Änderung, die ich hier an der Vorgabe vornehme, besteht darin, die Größe der Pfeile zu verringern. Standardmäßig beschriftet igraph
die Knoten mit der Beschriftungsspalte, wenn es eine gibt, oder mit den IDs.
plot(routes_igraph, edge.arrow.size = 0.2)
Wie das network
-Diagramm zuvor ist die Standardeinstellung eines igraph
-Plots nicht besonders ästhetisch, aber alle Aspekte des Plots können manipuliert werden. Hier möchte ich lediglich das Layout der Knoten ändern, um den von Michael Schmuhl entwickelten graphopt-Algorithmus zu verwenden. Dieser Algorithmus macht es einfacher, die Beziehung zwischen Haarlem, Antwerpen und Delft, den drei wichtigsten Orten im Korrespondenznetz, zu erkennen, indem er sie weiter ausbreitet.
plot(routes_igraph, layout = layout_with_graphopt, edge.arrow.size = 0.2)
tidygraph und ggraph
Die Pakete tidygraph
und ggraph
sind Neulinge in der Landschaft der Netzwerkanalyse, aber zusammen bieten die beiden Pakete echte Vorteile gegenüber den Paketen network
und igraph
. tidygraph
und ggraph
stellen einen Versuch dar, die Netzwerkanalyse in den Arbeitsablauf von Tidyverse zu integrieren. tidygraph
bietet eine Möglichkeit, ein Netzwerkobjekt zu erstellen, das eher einem Tibble oder Datenrahmen ähnelt. Dadurch ist es möglich, viele der dplyr
-Funktionen zur Bearbeitung von Netzwerkdaten zu verwenden. ggraph
bietet eine Möglichkeit, Netzwerkgraphen unter Verwendung der Konventionen und Möglichkeiten von ggplot2
zu zeichnen. Mit anderen Worten: tidygraph
und ggraph
ermöglichen den Umgang mit Netzwerkobjekten in einer Weise, die mit den Befehlen für die Arbeit mit Tibbles und Datenrahmen konsistent ist. Das wahre Versprechen von tidygraph
und ggraph
ist jedoch, dass sie die Leistungsfähigkeit von igraph
nutzen. Das bedeutet, dass Sie durch die Verwendung von tidygraph
und ggraph
nur wenige der Netzwerkanalysefähigkeiten von igraph
opfern.
Wie immer müssen wir damit beginnen, die erforderlichen Pakete zu laden.
library(tidygraph)library(ggraph)
Zunächst erstellen wir mit tidygraph
ein Netzwerkobjekt, das tbl_graph
genannt wird. Ein tbl_graph
besteht aus zwei Tibbles: einem Edge-Tibble und einem Nodes-Tibble. Praktischerweise ist die tbl_graph
-Objektklasse ein Wrapper um ein igraph
-Objekt, was bedeutet, dass ein tbl_graph
-Objekt im Grunde ein igraph
-Objekt ist.8 Die enge Verbindung zwischen tbl_graph
– und igraph
-Objekten führt zu zwei Hauptmöglichkeiten, ein tbl_graph
-Objekt zu erstellen. Die erste ist die Verwendung einer Kantenliste und einer Knotenliste mit tbl_graph()
. Die Argumente für die Funktion sind fast identisch mit denen von graph_from_data_frame()
, nur die Namen der Argumente wurden geringfügig geändert.
routes_tidy <- tbl_graph(nodes = nodes, edges = edges, directed = TRUE)
Die zweite Möglichkeit, ein tbl_graph
-Objekt zu erstellen, ist die Konvertierung eines igraph
– oder network
-Objekts mit as_tbl_graph()
. Wir könnten also routes_igraph
in ein tbl_graph
-Objekt umwandeln.
routes_igraph_tidy <- as_tbl_graph(routes_igraph)
Nachdem wir nun zwei tbl_graph
-Objekte erstellt haben, wollen wir sie mit der Funktion class()
untersuchen. Dies zeigt, dass routes_tidy
und routes_igraph_tidy
Objekte der Klasse "tbl_graph" "igraph"
sind, während routes_igraph
zur Objektklasse "igraph"
gehört.
class(routes_tidy)#> "tbl_graph" "igraph"class(routes_igraph_tidy)#> "tbl_graph" "igraph"class(routes_igraph)#> "igraph"
Der Ausdruck eines tbl_graph
-Objekts auf der Konsole ergibt eine völlig andere Ausgabe als die eines igraph
-Objekts. Es ist eine Ausgabe, die der eines normalen Tibble ähnelt.
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
Der Ausdruck von routes_tidy
zeigt, dass es sich um ein tbl_graph
-Objekt mit 13 Knoten und 15 Kanten handelt. Der Befehl druckt auch die ersten sechs Zeilen der „Node Data“ und die ersten drei Zeilen der „Edge Data“. Es wird auch angegeben, dass die Knotendaten aktiv sind. Das Konzept eines aktiven Tibble innerhalb eines tbl_graph
-Objekts ermöglicht es, die Daten in einem Tibble auf einmal zu manipulieren. Die Knoten-Tibble ist standardmäßig aktiviert, aber Sie können mit der Funktion activate()
ändern, welche Tibble aktiv ist. Wenn ich also die Zeilen in der Kanten-Tibble neu anordnen wollte, um die Zeilen mit dem höchsten „Gewicht“ zuerst aufzulisten, könnte ich activate()
und dann arrange()
verwenden. Hier drucke ich das Ergebnis einfach aus, anstatt es zu speichern.
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
Da wir routes_tidy
nicht weiter bearbeiten müssen, können wir das Diagramm mit ggraph
zeichnen. Wie ggmap ist ggraph
eine Erweiterung von ggplot2
, was es einfacher macht, die Grundkenntnisse von ggplot
auf die Erstellung von Netzwerkdiagrammen zu übertragen. Wie bei allen Netzwerkgraphen gibt es drei Hauptaspekte in einem ggraph
Plot: Knoten, Kanten und Layouts. Die Vignetten für das ggraph-Paket behandeln die grundlegenden Aspekte von ggraph
-Plots auf hilfreiche Weise. ggraph
fügt dem Basissatz der ggplot
-Geoms spezielle Geoms hinzu, die speziell für Netzwerke entwickelt wurden. Es gibt also einen Satz von geom_node
und geom_edge
Geoms. Die grundlegende Plotfunktion ist ggraph()
, die die Daten, die für das Diagramm verwendet werden sollen, und die Art des gewünschten Layouts entgegennimmt. Beide Argumente für ggraph()
sind um igraph
herum aufgebaut. Daher kann ggraph()
entweder ein igraph
-Objekt oder ein tbl_graph
-Objekt verwenden. Darüber hinaus leiten sich die verfügbaren Layout-Algorithmen hauptsächlich von igraph
ab. Schließlich wird mit ggraph
ein spezielles ggplot
-Thema eingeführt, das bessere Vorgaben für Netzgraphen bietet als die normalen ggplot
-Vorgaben. Das Thema ggraph
kann für eine Reihe von Diagrammen mit dem Befehl set_graph_style()
eingestellt werden, der vor dem Plotten der Diagramme ausgeführt wird, oder indem theme_graph()
in den einzelnen Diagrammen verwendet wird. Hier werde ich die letztere Methode verwenden.
Sehen wir uns einmal an, wie eine einfache ggraph
-Darstellung aussieht. Die Darstellung beginnt mit ggraph()
und den Daten. Dann füge ich grundlegende Kanten- und Knoten-Geoms hinzu. Innerhalb der Kanten- und Knoten-Geoms sind keine Argumente erforderlich, da sie die Informationen aus den in ggraph()
angegebenen Daten übernehmen.
ggraph(routes_tidy) + geom_edge_link() + geom_node_point() + theme_graph()
Wie Sie sehen, ähnelt die Struktur des Befehls der von ggplot
, wobei die einzelnen Ebenen mit dem Zeichen +
hinzugefügt werden. Die grundlegende ggraph
-Darstellung sieht ähnlich aus wie die von network
und igraph
, wenn nicht sogar noch schlichter, aber wir können ähnliche Befehle wie ggplot
verwenden, um eine informativere Grafik zu erstellen. Wir können das „Gewicht“ der Kanten – oder die Anzahl der Briefe, die auf jeder Route verschickt werden – anzeigen, indem wir die Breite in der Funktion geom_edge_link()
verwenden. Damit sich die Breite der Linie entsprechend der Gewichtsvariablen ändert, platzieren wir das Argument innerhalb einer aes()
-Funktion. Um die maximale und minimale Breite der Kanten zu steuern, verwende ich scale_edge_width()
und setze ein range
. Ich wähle eine relativ kleine Breite für das Minimum, weil es einen signifikanten Unterschied zwischen der maximalen und minimalen Anzahl von Briefen gibt, die entlang der Routen gesendet werden. Wir können die Knoten auch mit den Namen der Orte beschriften, da es relativ wenige Knoten gibt. Praktischerweise verfügt geom_node_text()
über ein repel-Argument, das dafür sorgt, dass sich die Beschriftungen nicht mit den Knoten überschneiden, ähnlich wie das ggrepel-Paket. Mit dem Argument alpha füge ich den Kanten ein wenig Transparenz hinzu. Außerdem verwende ich labs()
, um die Legende „Letters“ neu zu beschriften.
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()
Zusätzlich zu den von igraph
bereitgestellten Layout-Optionen implementiert ggraph
auch eigene Layouts. Sie können zum Beispiel das ggraph's
-Konzept der Kreisförmigkeit verwenden, um Bogen-Diagramme zu erstellen. Hier habe ich die Knoten in einer horizontalen Linie angeordnet und die Kanten als Bögen gezeichnet. Im Gegensatz zum vorherigen Diagramm zeigt dieses Diagramm die Richtung der Kanten an.9 Die Kanten oberhalb der horizontalen Linie bewegen sich von links nach rechts, während die Kanten unterhalb der Linie sich von rechts nach links bewegen. Anstatt Punkte für die Knoten hinzuzufügen, füge ich nur die Namen der Labels ein. Ich verwende die gleiche ästhetische Breite, um den Unterschied in der Gewichtung der einzelnen Kanten zu kennzeichnen. Beachten Sie, dass ich in dieser Darstellung ein igraph
-Objekt als Daten für den Graphen verwende, was in der Praxis keinen Unterschied macht.
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()
Interaktive Netzwerkgraphen mit visNetwork und networkD3
Die htmlwidgets-Pakete ermöglichen es, mit R interaktive JavaScript-Visualisierungen zu erstellen. Hier werde ich zeigen, wie man mit den Paketen visNetwork
und networkD3
Graphen erstellt. Diese beiden Pakete verwenden unterschiedliche JavaScript-Bibliotheken, um ihre Diagramme zu erstellen. visNetwork
verwendet vis.js, während networkD3
die beliebte d3-Visualisierungsbibliothek zur Erstellung von Diagrammen verwendet. Eine Schwierigkeit bei der Arbeit mit visNetwork
und networkD3
besteht darin, dass sie erwarten, dass Kantenlisten und Knotenlisten eine bestimmte Nomenklatur verwenden. Die oben beschriebene Datenmanipulation entspricht der Grundstruktur von visNetwork
, aber für networkD3
muss noch etwas Arbeit geleistet werden. Trotz dieser Unannehmlichkeiten verfügen beide Pakete über eine breite Palette von Diagrammfunktionen und können mit igraph
Objekten und Layouts arbeiten.
library(visNetwork)library(networkD3)
visNetwork
Die Funktion visNetwork()
verwendet eine Knotenliste und eine Kantenliste, um einen interaktiven Graphen zu erstellen. Die Knotenliste muss eine „id“-Spalte enthalten, und die Kantenliste muss die Spalten „von“ und „bis“ haben. Die Funktion zeichnet auch die Beschriftungen für die Knoten, indem sie die Namen der Städte aus der Spalte „label“ in der Knotenliste verwendet. Es macht Spaß, mit dem resultierenden Diagramm herumzuspielen. Sie können die Knoten verschieben und das Diagramm verwendet einen Algorithmus, um die Knoten im richtigen Abstand zu halten. Sie können auch in das Diagramm hinein- und herauszoomen und es verschieben, um es neu zu zentrieren.
visNetwork(nodes, edges)
visNetwork
kann igraph
Layouts verwenden und bietet so eine große Vielfalt an möglichen Layouts. Darüber hinaus können Sie visIgraph()
verwenden, um ein igraph
-Objekt direkt zu zeichnen. Hier werde ich mich an den nodes
– und edges
-Workflow halten und ein igraph
-Layout verwenden, um das Diagramm anzupassen. Ich werde auch eine Variable hinzufügen, um die Breite der Kante zu ändern, wie wir es mit ggraph
getan haben. visNetwork()
verwendet Spaltennamen aus den Kanten- und Knotenlisten zur Darstellung von Netzwerkattributen anstelle von Argumenten innerhalb des Funktionsaufrufs. Das bedeutet, dass eine gewisse Datenmanipulation erforderlich ist, um eine Spalte „Breite“ in der Kantenliste zu erhalten. Das Attribut width für visNetwork()
skaliert die Werte nicht, so dass wir dies manuell tun müssen. Beide Aktionen können mit der Funktion mutate()
und einigen einfachen arithmetischen Operationen durchgeführt werden. In diesem Fall erstelle ich eine neue Spalte in edges
und skaliere die Gewichtungswerte, indem ich durch 5 teile. Durch Hinzufügen von 1 zum Ergebnis lässt sich eine Mindestbreite erstellen.
edges <- mutate(edges, width = weight/5 + 1)
Nachdem dies erledigt ist, können wir einen Graphen mit variablen Kantenbreiten erstellen. Ich wähle auch einen Layout-Algorithmus aus igraph
und füge Pfeile zu den Kanten hinzu, indem ich sie in der Mitte der Kante platziere.
visNetwork(nodes, edges) %>% visIgraphLayout(layout = "layout_with_fr") %>% visEdges(arrows = "middle")
networkD3
Ein wenig mehr Arbeit ist nötig, um die Daten für die Erstellung eines networkD3
-Graphen vorzubereiten. Um einen networkD3
-Graphen mit einer Kanten- und Knotenliste zu erstellen, müssen die IDs eine Reihe numerischer Ganzzahlen sein, die mit 0 beginnen. Derzeit beginnen die Knoten-IDs für unsere Daten mit 1, so dass wir die Daten ein wenig manipulieren müssen. Es ist möglich, die Knoten neu zu nummerieren, indem man 1 von den ID-Spalten in den Datenrahmen nodes
und edges
subtrahiert. Auch dies kann mit der Funktion mutate()
durchgeführt werden. Ziel ist es, die aktuellen Spalten neu zu erstellen, wobei von jeder ID 1 abgezogen wird. Die Funktion mutate()
erstellt eine neue Spalte, kann aber auch eine Spalte ersetzen, indem sie der neuen Spalte denselben Namen wie der alten Spalte gibt. Hier benenne ich die neuen Datenrahmen mit dem Suffix d3, um sie von den vorherigen Datenrahmen nodes
und edges
zu unterscheiden.
nodes_d3 <- mutate(nodes, id = id - 1)edges_d3 <- mutate(edges, from = from - 1, to = to - 1)
Es ist nun möglich, ein networkD3
Diagramm zu erstellen. Im Gegensatz zu visNetwork()
verwendet die Funktion forceNetwork()
eine Reihe von Argumenten, um das Diagramm anzupassen und Netzwerkattribute darzustellen. Die Argumente „Links“ und „Nodes“ liefern die Daten für die Darstellung in Form von Kanten- und Knotenlisten. Die Funktion benötigt außerdem die Argumente „NodeID“ und „Group“. Die hier verwendeten Daten sind nicht gruppiert, so dass jeder Knoten seine eigene Gruppe ist, was in der Praxis bedeutet, dass die Knoten alle unterschiedliche Farben haben werden. Darüber hinaus wird der Funktion im Folgenden mitgeteilt, dass das Netzwerk über „Source“- und „Target“-Felder verfügt und somit gerichtet ist. Ich füge in diesen Graphen einen „Wert“ ein, der die Breite der Kanten entsprechend der Spalte „Gewicht“ in der Kantenliste skaliert. Schließlich füge ich einige ästhetische Verbesserungen hinzu, um die Knoten undurchsichtig zu machen und die Schriftgröße der Beschriftungen zu erhöhen, um die Lesbarkeit zu verbessern. Das Ergebnis ist dem ersten visNetwork()
-Diagramm, das ich erstellt habe, sehr ähnlich, allerdings mit einer anderen ästhetischen Gestaltung.
forceNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Group = "id", Value = "weight", opacity = 1, fontSize = 16, zoom = TRUE)
Einer der Hauptvorteile von networkD3
ist, dass es ein Sankey-Diagramm im Stil von d3 implementiert. Ein Sankey-Diagramm passt gut zu den Briefen an Daniel aus dem Jahr 1585. Es gibt nicht allzu viele Knoten in den Daten, wodurch der Fluss der Briefe leichter zu visualisieren ist. Zum Erstellen eines Sankey-Diagramms wird die Funktion sankeyNetwork()
verwendet, die viele der gleichen Argumente wie forceNetwork()
übernimmt. Für dieses Diagramm ist kein Gruppenargument erforderlich, und die einzige weitere Änderung ist die Hinzufügung einer „Einheit“. Dies stellt eine Beschriftung für die Werte bereit, die in einem Tooltip auftauchen, wenn der Mauszeiger über einem Diagrammelement schwebt.10
sankeyNetwork(Links = edges_d3, Nodes = nodes_d3, Source = "from", Target = "to", NodeID = "label", Value = "weight", fontSize = 16, unit = "Letter(s)")
Weitere Lektüre zur Netzwerkanalyse
Dieser Beitrag hat versucht, eine allgemeine Einführung in die Erstellung und Darstellung von netzwerkartigen Objekten in R mit den Paketen network
, igraph
, tidygraph
und ggraph
für statische Darstellungen und visNetwork
und networkD3
für interaktive Darstellungen zu geben. Ich habe diese Informationen aus der Sicht eines Nicht-Spezialisten für Netzwerktheorie dargestellt. Ich habe nur einen sehr kleinen Prozentsatz der Netzwerkanalysefähigkeiten von R abgedeckt. Insbesondere habe ich nicht die statistische Analyse von Netzwerken diskutiert. Glücklicherweise gibt es eine Fülle von Ressourcen zur Netzwerkanalyse im Allgemeinen und in R im Besonderen.
Die beste Einführung in Netzwerke, die ich für den Uneingeweihten gefunden habe, ist Katya Ognyanovas Network Visualization with R. Es bietet sowohl eine hilfreiche Einführung in die visuellen Aspekte von Netzwerken als auch ein ausführlicheres Tutorial zur Erstellung von Netzwerkdiagrammen in R. Ognyanova verwendet in erster Linie igraph
, aber sie stellt auch interaktive Netzwerke vor.
Es gibt zwei relativ neue Bücher zur Netzwerkanalyse mit R, die von Springer veröffentlicht wurden. Douglas A. Luke, A User’s Guide to Network Analysis in R (2015) ist eine sehr nützliche Einführung in die Netzwerkanalyse mit R. Luke behandelt sowohl die statnet-Pakete als auch igragh
. Die Inhalte sind durchweg auf einem sehr angenehmen Niveau. Etwas fortgeschrittener ist Eric D. Kolaczyk und Gábor Csárdi’s, Statistical Analysis of Network Data with R (2014). Das Buch von Kolaczyk und Csárdi verwendet hauptsächlich igraph
, da Csárdi der primäre Betreuer des igraph
-Pakets für R ist. Dieses Buch geht weiter in fortgeschrittene Themen der statistischen Analyse von Netzwerken ein. Trotz der Verwendung einer sehr technischen Sprache sind die ersten vier Kapitel auch für Nicht-Fachleute zugänglich.
Die von François Briatte kuratierte Liste bietet einen guten Überblick über Ressourcen zur Netzwerkanalyse im Allgemeinen. Die Reihe Networks Demystified von Scott Weingart ist ebenfalls einen Blick wert.
-
Ein Beispiel für das Interesse an der Netzwerkanalyse innerhalb der digitalen Geisteswissenschaften ist das neu gegründete Journal of Historical Network Research. ︎
-
Eine gute Beschreibung der Objektklasse
network
, einschließlich einer Diskussion ihrer Beziehung zur Objektklasseigraph
, findet sich in Carter Butts, „network: A Package for Managing Relational Data in R“, Journal of Statistical Software, 24 (2008): 1-36 ︎ -
Dies ist die spezifische Struktur, die von
visNetwork
erwartet wird, während sie auch den allgemeinen Erwartungen der anderen Pakete entspricht. ︎ -
Dies ist die erwartete Reihenfolge der Spalten für einige der Netzwerkpakete, die ich im Folgenden verwenden werde. ︎
-
ungroup()
ist in diesem Fall nicht unbedingt erforderlich. Wenn Sie jedoch die Gruppierung des Datenrahmens nicht aufheben, ist es nicht möglich, die Spalten „Quelle“ und „Ziel“ zu löschen, wie ich es später im Skript tue. ︎ -
Thomas M. J. Fruchterman und Edward M. Reingold, „Graph Drawing by Force-Directed Placement,“ Software: Practice and Experience, 21 (1991): 1129-1164. ︎
-
Die Funktion
rm()
ist nützlich, wenn Ihre Arbeitsumgebung in R durcheinander gerät, Sie aber nicht die gesamte Umgebung löschen und neu beginnen wollen. ︎ -
Die Beziehung zwischen
tbl_graph
undigraph
Objekten ist ähnlich wie die zwischentibble
unddata.frame
Objekten. ︎ -
Es ist möglich,
ggraph
Pfeile zeichnen zu lassen, aber das habe ich hier nicht gezeigt. ︎ -
Es kann ein bisschen dauern, bis der Tooltip erscheint. ︎
Schreibe einen Kommentar