Kapitel 11 Datenstrukturen
Wer tiefer in R eintauchen möchte, sollte neben den bereits verwendeten
tibbles
ebenfalls die anderen Datenstrukturen kennenlernen. In R gibt es außerdem Vektoren, Matrizen, data.frames und Listen. Ein Vektor enthält einen oder mehrere Werte in einer Reihe desselben Datentyps. Eine Matrix erweitert den Vektor um eine zweite Dimension, kann aber ebenfalls nur denselben Datentyp beinhalten. Data.frames sind wie Matrizen zweidimensional, allerdings kann jede Spalte einen beliebigen Datentyp enthalten, der sich von den anderen Spalten unterscheidet. Eine Liste kann in jedem Element entweder Vektoren, Matrizen oder data.frames enthalten, welche auch miteinander kombiniert werden können.
11.1 Vektor
Jede Spalte innerhalb eines tibbles
ist für sich genommen ein Vektor, der aus einem einzigen Datentyp besteht. Erstellen kann man einen Vektor auf unterschiedliche Art und Weise. Bereits kennengelernt haben wir die c()
Funktion (combine, engl. für kombinieren).
[1] 11 8 24 53
Man kann so neben numerischen Werten auch jeden anderen Datentyp zu einem Vektor kombinieren (z.B. Character). Für eine einfach Sequenz können wir den Doppelpunkt verwenden. Man könnte so bspw. das Erstellen des Vektors c(1, 2, 3, 4)
etwas abkürzen.
[1] 1 2 3 4
Der Doppelpunkt ist dabei ein Shortcut für seq()
(sequence, engl. für Sequenz).
[1] 1 2 3 4
Zusätzlich können hier auch die Abstände zwischen den Zahlen der Frequenz kleiner oder größer gewählt werden (z.B. by = 0.2
oder by = 2
).
Auf einzelne Werte innerhalb eines Vektors kann mithilfe eckiger Klammern zugegriffen werden. Exemplarisch wird hier das dritte Element des Vektors c(1, 3, 2, 4)
, welcher als vec
gespeichert ist, ausgewählt.
[1] 2
Die eckigen Klammern können auch mit c()
oder dem Doppelpunkt kombiniert werden, um mehrere Elemente ausgeben zu lassen.
[1] 1 4
[1] 1 3
Vektoren können immer nur einen Datentyp enthalten. Wenn eine Zahl mit einem Wort kombiniert wird, werden alle im Vektor enthaltenen Werte zum Typ Character
umgewandelt.
11.2 Matrix
Wenn man mehrere Vektoren eines Datentyps aneinander bindet, erhält man eine Matrix. Für zeilenweises Binden der Vektoren wird rbind()
verwendet (row bind, engl. für Zeilen verbinden). Dabei müssen alle Vektoren dieselbe Länge haben.
[,1] [,2] [,3] [,4]
[1,] 1 3 2 4
[2,] 1 2 3 4
Das Äquivalent zum Verbinden von Spalten ist cbind()
(column bind, engl. für Spalten verbinden).
[,1] [,2]
[1,] 1 1
[2,] 3 2
[3,] 2 3
[4,] 4 4
Seltener in der Datenanalyse benutzt, aber trotzdem manchmal nützlich ist die matrix()
Funktion. Als Argumente müssen der Vektor, die Anzahl der Zeilen oder Spalten sowie die Information übergeben werden, ob die Werte zeilenweise (byrow
) eingefügt werden sollen.
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
[3,] 7 8 9
Da nun zwei Dimensionen involviert sind, müssen zum Zugreifen auf Elemente innerhalb der Matrix auch zwei Parameter berücksichtigt werden: die Spalten- und Zeilenposition. Dabei werden innerhalb der eckigen Klammern getrennt von einem Komma zuerst die Zeilen und dann die Spalten angegeben. Möchte man den Wert aus Zeile 2 und Spalte 3 erhalten, würde man [2, 3]
an den Variablennamen hängen.
[1] 6
Wenn eine ganze Zeile oder Spalte zurückgeben werden soll, lässt man schlichtweg das auszulassende Argument weg. Für die erste Zeile schreibt man folglich:
[1] 1 2 3
Das Leerzeichen nach dem Komma ist zwar nicht zwingend notwendig, allerdings macht es deutlich, dass dort ein zweiter Wert fehlt.
Genau wie Vektoren können auch in Matrizen nur Werte von einem Datentyp gespeichert werden. Bei Vermischung der Datentypen wird automatisch die Umwandlungsregel Character > Integer > Logical
angewandt. Außerdem müssen die Vektoren innerhalb der Matrix dieselbe Länge haben.
Aufgrund der Limitation, nur einen Datentyp enthalten zu können, findet die Matrix als Datenstruktur in der gewöhnlichen Datenanalyse in der Regel keine Anwendung.
11.3 Data.frame und tibble
Ein data.frame
ist ein zweidimensionales Datenformat, welches direkt in R integriert ist. Hingegen müssen tibbles
aus dem gleichnamigen tibble
Package des tidyverse
zur Verwendung nach jedem Start von R neu geladen werden. Beide haben eine Anordnung wie die Matrix mit Zeilen und Spalten. Zwischen den Spalten darf der Datentyp variieren, wobei innerhalb einer Spalte noch immer derselbe Datentyp vorhanden sein muss.
Einer der größten Vorteile der tibbles
ist die übersichtlichere Ausgabe. Es werden nur 10 Zeilen ausgegeben. Auf einen Blick sieht man dabei die Datentypen der Spalten und die Dimensionen des Datensatzes. Außerdem sind die Zahlen zur besseren Übersichtlichkeit entsprechend eingerückt und negative Werte rot hervorgehoben. Das automatische Runden von tibbles
bei der Anzeige ist hingegen nicht immer ein Vorteil. Während es beim explorativen Anschauen der Daten praktisch ist, muss beim deskriptiven oder inferenzstatistischen Betrachten der Daten eine bestimmte Anzahl von Kommastellen sichtbar sein, um sie in einer wissenschaftlichen Arbeit zu berichten.
Grundsätzlich sind Funktionen, die für data.frames
verwendet werden können, bis auf wenige Ausnahmen auch auf tibbles
anwendbar. Wie man einen tibble
erstellt, wurde bereits in Kapitel 5.3 eingeführt. Bei der Erstellung eines data.frames
ändert sich nur die Funktion.
a b
1 1 3
2 2 4
Beim Zugriff auf einzelne Spalten wurde im Verlauf des Buches entweder select()
aus dem tidyverse
oder der Dollar-Operator verwendet. Die Funktion select()
gibt dabei die einzelne Spalte als tibble
zurück.
# A tibble: 3 × 1
Alter
<dbl>
1 44
2 16
3 52
Eine alternative Schreibweise dafür sind einfache eckige Klammern. Da hier alle Zeilen der Spalte Alter zurückgegeben werden sollen, wird keine Zahl vor dem Komma angegeben.
# A tibble: 3 × 1
Alter
<dbl>
1 44
2 16
3 52
Hier gibt es einen weiteren Unterschied zwischen data.frames
und tibbles
. Während in data.frames
bei Auswahl nur einer Spalte ein Vektor zurückgegeben wird, gibt ein tibble
immer einen tibble
zurück.
Einzelne Spalten als Vektor können entweder mit dem Dollar-Zeichen oder mit doppelten eckigen Klammern mit dem Spaltennamen in Anführungszeichen aus einem data.frame
oder tibble
extrahiert werden.
[1] 44 16 52
Einzelne Werte können mit einfachen eckigen Klammern äquivalent zu Matrizen ausgewählt werden.
# A tibble: 1 × 1
Geschlecht
<chr>
1 f
11.4 Liste
Listen sind die allgemeinste Datenstruktur. Ein Listenelement kann jede Datenstruktur enthalten – sogar ganze tibbles
. Beim Erstellen ändert sich der Befehl zu list()
.
$Vektor
[1] 1 3 2 4
$Matrix
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
[3,] 7 8 9
$Tibble
# A tibble: 3 × 2
Geschlecht Alter
<chr> <dbl>
1 m 44
2 f 16
3 d 52
Die Zeichen vor dem Gleichheitszeichen sind dabei die Namen der Listenelemente, die man zum Abrufen verwenden kann. Mit Listen haben wir bis zu drei Dimensionen. Die verschiedenen Elemente innerhalb der Liste, die wiederum zweidimensionale tibbles
enthalten können. Es ist sogar möglich, Listen innerhalb von Listen zu speichern. Im Rahmen des Einnistens greifen wir diesen Gedanken in Kapitel 12.4 wieder auf.
Das Prinzip beim Zugreifen ändert sich im Vergleich zu tibbles
nicht. Allerdings gibt es eine Dimensionen mehr. Würde man also auf den tibble
der eben erstellen Liste ls
zugreifen wollen, könnte man ls$Tibble
oder ls[[3]]
verwenden. Möchte man direkt auf Elemente innerhalb des tibbles
zugreifen, erreicht man dies wie gewohnt mit einfach eckigen Klammern.
# A tibble: 1 × 1
Alter
<dbl>
1 44
Eine leere Liste einer bestimmten Länge kann mit vector()
erstellt werden (hier z.B. eine Liste der Länge 3). Dies ist vor allem bei for-Schleifen wichtig, da so die Dauer der Berechnungen reduziert werden kann (siehe Kapitel 12.3).
[[1]]
NULL
[[2]]
NULL
[[3]]
NULL
Eine besondere Art der Liste ist der tibble
. Daher können wir grundsätzlich in eine Zelle nicht nur Zahlen oder Buchstaben hineinschreiben, sondern sogar ganze andere Datensätze darin abspeichern.
df <- tibble(
a = c(1, 2, 3),
b = list(
tibble(a = c(1, 2, 3, 4), b = c("m", "f", "f", "m")),
tibble(x = 4:5, y = 6:7),
Number = 1
)
)
df
# A tibble: 3 × 2
a b
<dbl> <named list>
1 1 <tibble [4 × 2]>
2 2 <tibble [2 × 2]>
3 3 <dbl [1]>
Wie du siehst, sind in der Spalte b
nun in den ersten zwei Zeilen tibbles
enthalten. Wenn wir mit df$b
oder df[[2]]
nur diese Spalte anschauen, sehen wir eine Liste als Ausgabe.
[[1]]
# A tibble: 4 × 2
a b
<dbl> <chr>
1 1 m
2 2 f
3 3 f
4 4 m
[[2]]
# A tibble: 2 × 2
x y
<int> <int>
1 4 6
2 5 7
$Number
[1] 1
11.5 Umwandlungen
Sofern die Voraussetzungen der Dimensionen und Datentypen erfüllt sind, können Datenstrukturen ineinander überführt werden. Dabei haben die Funktionen immer das Präfix as.
mit der Ausnahme bei tibbles
mit dem Präfix as_
.
Besonders nützlich ist hierbei as.data.frame()
, um einen tibble
in einen data.frame
umzuwandeln, falls die angezeigten Rundungen zu ungenau sind oder Funktionen nur mit data.frames
fehlerfrei funktionieren. Für as_tibble()
muss zuvor das tibble
Package oder das tidyverse
geladen sein.
11.6 Objekte und Objektorientierung
Für dieses Kapitel muss das sloop
Package installiert und geladen werden.
In R ist grundsätzlich alles ein Objekt. Dazu gehören auch die bereits kennengelernten Vektoren, Matrizen, data.frames, tibbles und Listen. Das ist insofern eine irreführende Aussage, als dass R eine primär funktionelle Programmiersprache ist und keine klassische Objektorientierung bietet, wie es z.B. in Java oder C/C++ umgesetzt ist. Schließlich lösen wir anspruchsvolle Probleme in R durch das Erstellen neuer Funktionen und nicht neuer Objekttypen mit spezifischen Eigenschaften.
Die vier wichtigsten Systeme zur Objektorientierung in R sind S3, S4, R6 und S7. Wir haben im Verlauf dieses Buches nur das S3 System kennengelernt. Du hast dich vielleicht bereits gefragt, weshalb ein und dieselbe Funktion mit dem Namen summary()
in Abhängigkeit des Kontextes unterschiedliche Ausgaben liefern kann. In Kapitel 7.1 erhalten wir Lage- und Streuungsmaße für Numerics und Häufigkeiten für Faktoren, während wir in Kapitel 9 je nach statistischem Modell ebenfalls ein unterschiedliche Ausgabe ausgegeben bekommen.
Die Antwort liegt in den Klassen der S3 Objekte, wodurch sich generische Funktionen wie summary()
je nach Klasse anders verhalten. Mit der Funktion otype()
aus dem sloop
Package kann der Objekttyp herausgefunden werden. Einfache Vektoren gehören keinem der Systeme zur Objektorientierung an.
[1] "base"
Hingegen ist jeder verwendete Datensatz dieses Buches ein S3 Objekt.
[1] "S3"
Dasselbe gilt auch für die Ergebnisse der Statistikfunktionen. Beim Vergleich deren jeweiliger Klasse mithilfe der Funktion class()
kriegen wir einen anderes Ergebnis für einen t Test als für ein lineares Regressionsmodell.
[1] "htest"
[1] "lm"
Die Funktion s3_methods_generic()
des sloop
Packages gibt Auskunft über alle von einer Funktion unterstützten Klassen. An dieser Stelle möchten wir summary()
untersuchen.
# A tibble: 134 × 4
generic class visible source
<chr> <chr> <lgl> <chr>
1 summary aareg FALSE registered S3method
2 summary afex_aov FALSE registered S3method
3 summary allFit FALSE registered S3method
4 summary Anova.mlm FALSE registered S3method
# ℹ 130 more rows
137 verschiedene Klassen werden nach Laden aller in diesem Buch verwendeten Packages unterstützt. Bereits in Zeile zwei sehen wir die Klasse der Funktion zur Varianzanalyse mit Messwiederholung aus Kapitel 9.3.2. Welche Klassen eine Implementierung für summary()
erhalten, hängt maßgeblich von den PackageentwicklerInnen ab, die das entsprechende Verhalten von summary()
für die neue Klasse programmatisch festlegen müssen.
Ein weiteres Beispiel für eine generische Funktion, die sich je nach Klasse anders verhält, ist autoplot()
aus dem ggplot2
Package. Wir haben in Kapitel 8.13.2 durch das Package ggfortify
das Verhalten für die Klasse lm
eines Regressionsmodells kennengelernt. Tatsächlich können mit der Funktion autoplot()
aber diverse Visualisierungen von statistischen Modellen erstellt werden.
R6 und S4 Systeme sind deutlich ähnlicher der klassischen Objektorientierung aus Java oder C/C++. Die S4 Objektorientierung ist eine Voraussetzung zum Hochladen eines Packages auf Bioconductor, welches eine Alternative mit biologischen Fokus zu CRAN darstellt. Als Konsequenz können die meisten der in diesem Buch kennengelernten Funktionen nicht mit Packages von Bioconductor kombiniert werden. Jedes S4 Objekt erhält eigene Methoden, um auf Informationen der Objekte zuzugreifen. Ein großer Unterschied in der Syntax ist auch die Verwendung eines at-Zeichens (@
) anstelle des uns bekannten Dollar-Operators, um einzelne Spalten zu extrahieren. Hätten wir in einem Objekt namens ls
eine Information namens ergebnis
gespeichert, würden wir im S4 System über ls@ergebnis
und nicht ls$ergebnis
Zugriff auf das Ergebnis bekommen.
Langfristig wird das neue S7 System, welches planmäßig direkt in R integriert werden soll, voraussichtlich das S4 und R6 System ersetzen. Für ausführlichere Erklärungen zum funktionellen Programmieren und der Objektorientierung sei auf Advanced R von Wickham verwiesen.