In 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 separater Einheiten und die Verbindungen zwischen ihnen. Das Vokabular kann ein wenig technisch und sogar uneinheitlich zwischen verschiedenen Disziplinen, Paketen und Software sein. 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, ich bespreche Pakete, 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 konvertiert 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 betrachtet.

Kantenlisten enthalten alle Informationen, die zur Erstellung von Netzwerkobjekten notwendig 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 folgenden Beispiel ändere ich die Befehle, die ich im vorigen Beitrag verwendet habe, geringfügig ab, damit der Name der Spalten mit den Städtenamen für die Datenrahmen sources und destinations gleich ist, um die Funktion full_join() 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. Dies können wir erreichen, indem wir dem nodes-Datenrahmen eine „id“-Spalte hinzufügen, die Zahlen von eins bis zur 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, so dass es möglich ist, full_join() und die „id“-Spalte in einem einzigen Befehl hinzuzufügen. 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 dadurch erschwert, dass zwei ID-Spalten anstelle von einer benötigt werden. Außerdem soll eine Gewichtsspalte erstellt werden, in der die Anzahl der zwischen den einzelnen Knoten gesendeten Buchstaben vermerkt wird. Hierfür verwende ich den gleichen 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. Dadurch wird es einfacher, den resultierenden per_route-Datenrahmen ungehindert zu manipulieren.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 statt 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. In der Tat ist es notwendig, zwei Joins durchzuführen, einen für die Spalte „Quelle“ und einen für „Ziel“. In diesem Fall verwende ich einen left_join() mit per_route als linken Datenrahmen, da wir die Anzahl der Zeilen in per_route beibehalten wollen. Beim Erstellen von left_join 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 Joins 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 so sehr, 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 Paket network die Nomenklatur von Scheitelpunkten anstelle von Knoten verwendet. Das Gleiche gilt für igraph. Wir müssen dann 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() einsetzen.

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 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 nach seinem Umzug nach Bremen im September.

plot(routes_network, vertex.cex = 3)

Die Funktion plot() mit einem Objekt network 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 Paket igraph besprechen. Zunächst müssen wir die Umgebung in R aufräumen, indem wir das Paket network entfernen, damit es nicht mit den igraph-Befehlen interferiert. Wir können auch routes_network entfernen, da wir es nicht mehr verwenden werden. Das Paket network 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. Diese besagt, 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 geben die Anzahl der Knoten bzw. Kanten des Graphen an. Die nächste Zeile name (v/c), label (v/c), weight (e/n) enthält 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 Voreinstellung eines igraph-Plots nicht besonders ästhetisch, aber alle Aspekte der 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 Netzwerkanalyselandschaft, 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 Tidyverse-Workflow 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 ermöglicht die Darstellung von Netzwerkdiagrammen unter Verwendung der Konventionen und Möglichkeiten von ggplot2. Mit anderen Worten: tidygraph und ggraph ermöglichen den Umgang mit Netzwerkobjekten auf eine Art und Weise, die mit den Befehlen für die Arbeit mit Tibbles und Datenrahmen besser vereinbar 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.

Wir müssen wie immer 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 leicht geändert.

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

Die zweite Möglichkeit, ein tbl_graph-Objekt zu erstellen, ist die Umwandlung 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 von „Node Data“ und die ersten drei Zeilen von „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 die aktive Tibble mit der Funktion activate() ändern. 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, grundlegende ggplot-Kenntnisse 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-Diagrammen auf hilfreiche Weise. ggraph fügt dem Basissatz der ggplot-Geoms spezielle Geoms hinzu, die speziell für Netzwerke entwickelt wurden. So gibt es einen Satz von geom_node und geom_edge Geoms. Die grundlegende Plot-Funktion 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 Netzwerkgraphen bietet als die normalen ggplot-Vorgaben. Das Thema ggraph kann für eine Reihe von Diagrammen mit dem Befehl set_graph_style() eingestellt werden, bevor die Diagramme gezeichnet werden, oder indem theme_graph() in den einzelnen Diagrammen verwendet wird. Hier werde ich die letztere Methode verwenden.

Lassen Sie uns sehen, wie eine grundlegende 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 einen range. Ich wähle eine relativ kleine Breite für das Minimum, weil es einen signifikanten Unterschied zwischen der maximalen und minimalen Anzahl von Buchstaben 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 ähnlich wie das ggrepel-Paket sicherstellt, dass sich die Beschriftungen nicht mit den Knoten überschneiden. 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 die Verwendung von R zur Erstellung interaktiver JavaScript-Visualisierungen. Hier zeige ich, wie man Diagramme mit den Paketen visNetwork und networkD3 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, was eine große Vielfalt an möglichen Layouts bietet. 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 des Randes 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. Hier erstelle ich eine neue Spalte in edges und skaliere die Gewichtswerte, 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 von numerischen 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 nodes und edges Datenrahmen 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 zu viele Knoten in den Daten, so dass sich der Fluss der Briefe leichter visualisieren lässt. 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 erscheinen, wenn der Mauszeiger über ein Diagrammelement bewegt wird.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 zu geben, wobei die Pakete network, igraph, tidygraph und ggraph für statische Darstellungen und visNetwork und networkD3 für interaktive Darstellungen verwendet wurden. 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 Uneingeweihte gefunden habe, ist Katya Ognyanovas Network Visualization with R. Dies ist 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 hauptsächlich 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.

  1. Ein Beispiel für das Interesse an der Netzwerkanalyse innerhalb der digitalen Geisteswissenschaften ist das neu gegründete Journal of Historical Network Research. ︎

  2. Eine gute Beschreibung der Objektklasse network, einschließlich einer Diskussion ihrer Beziehung zur Objektklasse igraph, findet sich in Carter Butts, „network: A Package for Managing Relational Data in R“, Journal of Statistical Software, 24 (2008): 1-36 ︎

  3. Dies ist die spezifische Struktur, die von visNetwork erwartet wird, während sie auch den allgemeinen Erwartungen der anderen Pakete entspricht. ︎

  4. Dies ist die erwartete Reihenfolge der Spalten für einige der Netzwerkpakete, die ich im Folgenden verwenden werde. ︎

  5. 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. ︎

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

  7. Die Funktion rm() ist nützlich, wenn Ihre Arbeitsumgebung in R unübersichtlich wird, Sie aber nicht die gesamte Umgebung löschen und neu beginnen wollen. ︎

  8. Die Beziehung zwischen tbl_graph und igraph Objekten ist ähnlich wie die zwischen tibble und data.frame Objekten. ︎

  9. Es ist möglich, ggraph Pfeile zeichnen zu lassen, aber das habe ich hier nicht gezeigt. ︎

  10. Es kann ein bisschen dauern, bis der Tooltip erscheint. ︎

r

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.