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).

c(11, 8, 24, 53)
[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:4
[1] 1 2 3 4

Der Doppelpunkt ist dabei ein Shortcut für seq() (sequence, engl. für Sequenz).

seq(from = 1, to = 4, by = 1)
[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.

vec <- c(1, 3, 2, 4)
vec[3]
[1] 2

Die eckigen Klammern können auch mit c() oder dem Doppelpunkt kombiniert werden, um mehrere Elemente ausgeben zu lassen.

vec[c(1, 4)]
[1] 1 4
vec[1:2]
[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.

rbind(
  c(1, 3, 2, 4),
  1:4
)
     [,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).

cbind(
  c(1, 3, 2, 4),
  1:4
)
     [,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.

mat <- matrix(
  1:9,
  ncol = 3,
  byrow = TRUE
)
mat
     [,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.

mat[2, 3]
[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:

mat[1, ]
[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.

data.frame(
  a = 1:2,
  b = 3:4
)
  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.

tb <- tibble(
  Geschlecht = c("m", "f", "d"),
  Alter = c(44, 16, 52),
)
tb |> 
  select(Alter)
# 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.

tb[ ,"Alter"]
# 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.

tb$Alter
tb[["Alter"]]
[1] 44 16 52

Einzelne Werte können mit einfachen eckigen Klammern äquivalent zu Matrizen ausgewählt werden.

tb[2, 1]
# 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().

ls <- list(
  Vektor = vec,
  Matrix = mat,
  Tibble = tb
)
ls
$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.

ls[[3]][1, 2]
# 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).

vector("list", 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.

df[[2]]
[[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_.

as.vector()
as.matrix()
as.data.frame()
as_tibble()

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.

library(sloop)

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.

otype(c(1, 3, 2, 4))
[1] "base"

Hingegen ist jeder verwendete Datensatz dieses Buches ein S3 Objekt.

otype(big5)
[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.

class(t.test(Leukos_t6 ~ Geschlecht, data = chemo))
[1] "htest"
class(lm(Leukos_t6 ~ Geschlecht, data = chemo))
[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.

s3_methods_generic("summary")
# 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.