Listen sind geordnete Folgen von R Objekten. Listen werden als rekursiver Datentyp bezeichnet, da sie Objektve verschiedener Datentypen, insbesondere auch wiederum Listen bündeln können. Obwohl sich die intuitive Sichtweise, dass Listen Objekte “enthalten” anbietet, “enthalten” Listen defacto keine Objekte, sondern lediglich die Referenzen zu den entsprechenden Objekten im Arbeitsspeicher (Abbildung 16.1). Listen sind ein wesentlicher Baustein der zentralen dataframe Datenstruktur in R.
Abbildung 16.1: Listenrepräsentation. Eine Liste in R ist eine indizierbare Folge von Referenzen zu R Objekten im Arbeitsspeicher.
Erzeugung
Listen können durch die direkte Konkatenation der Listenelemente mithilfe der Funktion list() erzeugt werden.
# Erzeugung einer Liste mit einem Vektor-, Matrix-, und FunktionselementL =list(c(1,4,5), matrix(1:8, nrow =2), exp) print(L)
Insbesondere können auch Listen selbst Elemente von Listen sein, was ihre Bezeichnung als rekursiver Datentyp motiviert.
L =list(list(1)) # Liste mit Element 1 in einer Listeprint(L) # Ausgabe
Die von den R Vektoren vertraute Funktion c() kann auch zum Konkatenieren von mehreren Listen genutzt werden.
L =c(list(pi), list("a")) # Konkatenation zweier Listenprint(L) # Ausgabe
Charakterisierung
Der Datentyp von Listen ist list:
L =list(1:2, "a", log) # Erzeugung einer Listetypeof(L) # Typenbestimmung
[1] "list"
length() gibt die Anzahl der Listenelemente auf dem obersten Listenlevel aus, die Elementinhalte werden also ingoriert.
L =list(1:2, list("a", pi), exp) # Liste mit drei Toplevelelementenlength(L) # length() ignoriert Elementinhalte, length() von L ist also 3
[1] 3
Die Dimension, Zeilen- und Spaltenanzahl von Listen ist NULL:
L =list(1:2, "a", sin) # eine Listedim(L) # Die Dimension von Listen ist NULL
NULL
nrow(L) # Die Zeilenanzahl von Listen ist NULL
NULL
ncol(L) # Die Spaltenanzahl von Listen ist NULL
NULL
Indizierung
Bei der Indizierung von Listen ist zu beachten, dass Listen auf zwei unterschiedliche Arten indiziert werden können. Einfache eckige Klammern [ ] indizieren Listenelemente als Listen:
L =list(1:3, "a", exp) # eine Listel1 = L[1] # Indizierung eines Listenelementsprint(l1) # Ausgabe
[[1]]
[1] 1 2 3
typeof(l1) # Typbestimmung von l1
[1] "list"
Doppelte eckige Klammern [[ ]] indizieren den Inhalt von Listenelementen:
L =list(1:3, "a", exp) # eine Listei2 = L[[2]] # Indizierung des Listenelementinhaltsprint(i2) # Ausgabe des Inhalts von Listenelement L[2]typeof(i2) # Typbestimmung von i2
Auch beim Ersetzen von Listeninhalten sind diese Konventionen zu beachten. So ist es zum Beispiel nicht möglich durch den Ausdruck L[1] = c(1,2,3) das erste Listenelement zu ändern, da der Vektor c(1,2,3) keine Liste ist:
L =list(1:3, "a", exp) # eine ListeL[1] =4:6# keine Typkonversion, Fehlermeldung
Warning in L[1] = 4:6: number of items to replace is not a multiple of
replacement length
L[1] =list(4:6) # Ersetzung des 1. ListenelementesL[[3]] ="c"# Ersetzung des 3. Listenelementinhaltes
Indizierung
Die Prinzipien der Listenindizierung sind analog zu denen der Vektorindizierung. Vektoren positiver Zahlen adressieren die entsprechenden Elemente:
L =list(1:3, "a", pi) # eine Listel = L[c(1,3)] # 1. und 3. Listenelementl
[[1]]
[1] 1 2 3
[[2]]
[1] 3.141593
Vektoren negativer Zahlen adressieren die zu ihren Werten komplementären Elemente:_
L =list(1:3, "a", pi) # eine Listel = L[-c(1,3)] # 2. Listenelementl
[[1]]
[1] "a"
Logische Vektoren adressieren Elemente mit Index TRUE:
L =list(1:3, "a", pi) # eine Listel = L[c(T,T,F)] # 1. und 2. Listenelementl
[[1]]
[1] 1 2 3
[[2]]
[1] "a"
Zur nicht-numerischen Addressierung können auch Listenelementen können Namen gegeben werden. Dies ist nach der Erzeugung einer Liste mithilfe der names() Funktion möglich:
K =list(1:2, TRUE) # eine unbenannte Listenames(K) =c("Frodo", "Sam") # Namensgebung mit names()K # Ausgabe
$Frodo
[1] 1 2
$Sam
[1] TRUE
Ebenso ist es möglich, die Namen der Listenelemente direkt bei Erzeugung der Liste durch Zuweisung zu vergeben:
L =list(greta =1:3, luisa ="a", carla ="b") # Listenerzeugung mit Namenvergabe
Sowohl Listenelemente als auch Listenelementinhalte können dann mit ihren Namen addressiert werden:
L =list(greta =1:3, luisa ="a", carla ="b") # Listenerzeugung mit NamenvergabeL["greta"] # Listenelementausgabe
$greta
[1] 1 2 3
typeof(L["greta"]) # Listenelementtyp
[1] "list"
L[["greta"]] # Listenelementinhaltausgabe
[1] 1 2 3
typeof(L[["luisa"]]) # Listenelementinhaltstyp
[1] "character"
Insbesondere können Listenelementinhalte auch mit dem $ Operator und ihrem Namen indiziert werden. Dies stellt im Rahmen der Dataframes die gebräuchlichste Art der Listenindizierung dar:
L =list(greta =1:3, luisa ="a", carla ="b") # Listenerzeugung mit NamenvergabeL$greta # Listenelementinhaltsausgabe
[1] 1 2 3
typeof(L$greta) # Listenelementinhaltstyp
[1] "integer"
L$luisa # Listenelementinhaltsausgabe
[1] "a"
typeof(L$luisa) # Listenelementinhaltstyp
[1] "character"
L$carla # Listenelementinhaltsausgabe
[1] "b"
typeof(L$carla) # Listenelementinhaltstyp
[1] "character"
Arithmetik
Eine allgemeine Listenarithmetik ist nicht definiert, da Listenelementinhalte ja unterschiedlichen Datentyps sein können, für die unitäre oder binäre Operationen nicht definiert sind:
L1 =list(1:3, "a" ) # eine ListeL2 =list(T, exp ) # eine ListeL1+L2 # Versuch der Listenaddition
Error in L1 + L2: non-numeric argument to binary operator
Sind Inhalte von Listenelementen allerdings von einem gemeinsamen Datentyp, für den bestimmte binäre Operationen definiert sind, so können sie auch z.B. arithmetisch verknüpft werden:
L1 =list(1:3, pi ) # eine ListeL2 =list(4:6, exp ) # eine ListeL1[[1]] + L2[[1]] # Addition der 1. Listenelementinhalte, [1+4, 2+5,3+6]
[1] 5 7 9
L2[[2]](1) # Anwendung des 2. Listenelementinhalts, exp(1)
[1] 2.718282
Copy-on-modify
Wie bei Vektoren gilt bei Listen das Copy-on-Modify Prinzip. Insbesondere gilt bei Listen, dass sogenannte Shallow copies bei Modifikation einer Liste erzeugt werdem. Dabei wird nur das Listenobjekt selbst kopiert und erhält eine neue Addresse im Arbeitsspeicher, nicht aber die durch die Liste nicht-modifizierent gebundenen Objekte, die an den gleichen Addressen im Arbeitsspeicher verbleiben. lobstr::ref() erlaubt es, dieses Verhalten zu studieren.
L1 =list(1,2,3) # Erzeugen einer Liste als Objekt lobstr::ref(L1) # Ausgabe der Referenzen
Offenbar ändert die Zuweisung L2 = L1 zunächst nichts an den Arbeitsspeicheraddressen auf die L1 und L2 sowie ihre Elemente verweisen. Die Modifikation des Inhalts des dritten Listenelements von L2 durch L2[[3]] = 4 allerdings ändert dann die Addresse des Listenobjekts im Arbeitsspeicher, da eine Kopie des Listenobjekts erzeugt wird. Gleichzeitig ändern sich gemäß dem shallow copy-Prinzips die Addressen der ersten beiden Inhalte von L2 aber nicht. Abbildung 16.2 visualisiert diese Prinzipien an einem Beispiel.
Abbildung 16.2: Effekte der Zuweisung L2 = L1 für eine Liste L1 sowie der Modifikation eines Elements von L2.
16.2 Dataframes
Dataframes sind die zentrale Datenstruktur in R. Dataframes stellt man sich am besten als Tabelle vor, deren Spalten Namen haben. Technisch ist ein Dataframe eine R Liste, deren Elemente R Vektoren gleicher Länge sind. Die Listenelemente entsprechen dabei den Spalten einer Tabelle und die Vektorelemente an gleicher Position entsprechen den Zeilen einer Tabelle.
Abbildung 16.3: Dataframerepräsentation. Ein R Dataframe ist einer R Liste deren Elemente R Vektoren gleicher Länge sind.
Erzeugung
Dataframes werden mit der Funktion data.frame() erzeugt. Dabei werden als Argument der Funktion die Spaltennamen und ihre vektorwertigen Inhalte festegelegt:
D =data.frame(x = letters[1:4], # 1. Spalte mit Name xy =1:4, # 2. Spalte mit Name yz =c(T,T,F,T)) # 3. Spalte mit Name zprint(D) # Ausgabe des Dataframes als Tabelle
x y z
1 a 1 TRUE
2 b 2 TRUE
3 c 3 FALSE
4 d 4 TRUE
Die Spalten des Dataframes müssen die gleiche Länge haben, ansonsten ist ein Dataframe nicht definiert:
D =data.frame(x = letters[1:4], # 1. Spalte mit Name xy =1:4, # 2. Spalte mit Name yz =c(T,T,F)) # 3. Spalte mit Name z
Error in data.frame(x = letters[1:4], y = 1:4, z = c(T, T, F)): arguments imply differing number of rows: 4, 3
Die Spalten eines Dataframes können offenbar unterschiedlichen Datentyps sein, dies ist natürlich mit dem Charakter von R Listen kongruent.
Charakterisierung
Mit den Funktionen names() und colnames() können die Namen der Spalten eines Dataframes ausgegeben werden, mit der Funktion rownames() seine Zeilennamen. Per Default sind die Zeilennamen eines Dataframes die ganzen Zahlen 1,2,…:
D =data.frame(age =c(30,35,40,45), # 1. Spalteheight =c(178,189,165,171), # 2. Spalteweight =c(67, 76, 81, 92)) # 3. Spaltenames(D) # names gibt die Spaltennamen aus
[1] "age" "height" "weight"
colnames(D) # colnames entspricht names
[1] "age" "height" "weight"
rownames(D) # default rownames sind 1,2,...
[1] "1" "2" "3" "4"
Des Weiteren ist ein Dataframe durch die Anzahl seiner Zeilen und Spalten charkterisiert. Diese können mit den Funktionen nrow() bzw. ncol() ausgegeben werden. Die Funktion length() gibt für Dataframes wie für Listen die Anzahl der Listeneinträge, bei Dataframes also entsprechend die Spaltenanzahl, aus.
nrow(D) # Zeilenanzahl
[1] 4
ncol(D) # Spaltenanzahl
[1] 3
length(D) # Länge ist die Spaltenanzahl
[1] 3
Eine hilfreiche Funktion zur Charaktersierung eines Dataframes ist die Funktion str(), die in kompakter Form wesentliche Aspekte eines Dataframes anzeigt. str() steht dabei für structure und bezeichnet die Spalten des Dataframes als variables und Zeileneinträge als observations:
str(D)
'data.frame': 4 obs. of 3 variables:
$ age : num 30 35 40 45
$ height: num 178 189 165 171
$ weight: num 67 76 81 92
Attribute
Technisch sind Dataframes Listen mit Attributen für (column) names und row.names. Dataframes sind von derclass “data.frame”:
Dataframes können sowohl wie Matrizen als auch wie Listen indiziert werden. Die Prinzipien der Indizierung entsprechen dabei denen von Vektoren. Folgendes Beispiel zeigt zunächst, wie ein Dataframe im Sinne einer Liste mit einfachen Klammern indiziert wird. Das entsprechende Listenelement ist bei Dataframes dann selbst ein Dataframe:
D =data.frame(x = letters[1:4], # 1. Spalte mit Name xy =1:4, # 2. Spalte mit Name yz =c(T,T,F,T)) # 3. Spalte mit Name zclass(D) # D ist ein Dataframe
[1] "data.frame"
v = D[1] # 1. Listenelement als Dataframev
x
1 a
2 b
3 c
4 d
class(v) # v ist ein Dataframe
[1] "data.frame"
Des Weiteren können Dataframes wie Listen mit doppelten Klammern indiziert werden, es werden dadurch entsprechend die Inhalte eines Listenelements addressiert:
w = D[[1]] # Inhalt des 1. Listenelements w
[1] "a" "b" "c" "d"
class(w) # w ist ein character vector
[1] "character"
Die typische Indizierung eines Dataframes erfolgt mit der von den Listen bekannten $ Indizierung, die den Inhalt der entsprechenden Dataframe Spalte addressiert:
y = D$y # $ zur Indizierung der y Spaltey
[1] 1 2 3 4
class(y) # v ist ein Vektor vom Typ "integer" (!)
[1] "integer"
Ist man an speziellen Inhalten des Dataframes in spezifischen Zeilen und Spalten interessiert, so bieten sich die Prinzipien der Matrixindizierung an, wie folgendes Beispiel zeigt:
D[2:3,-2] # 1. Index fuer Zeilen, 2. Index fuer Spalten
x z
2 b TRUE
3 c FALSE
D[c(T,F,T,F),] # 1. Index fuer Zeilen, 2. Index fuer Spalten
x y z
1 a 1 TRUE
3 c 3 FALSE
D[,c("x", "z")] # 1. Index fuer Zeilen, 2. Index fuer Spalten
x z
1 a TRUE
2 b TRUE
3 c FALSE
4 d TRUE
Copy-on-modify
Zur optimalen Nutzung des Arbeitsspeichers gelten die Copy-on-Modify Prinzipien für Listen auch für Dataframes. Weiterhin gelten dabei, dass die Modifikation einer Spalte zur Kopie der entsprechenden Spalte führt, wohingegen die Modifikation einer Zeile in der Kopie des gesamten Dataframes resultiert.
Abbildung 16.4: Effekte von Spalten und Zeilen Modifikationen bei Dataframes. Die Modifikation einer Spalte führt zur Kopie der entsprechenden Spalte, die Modifikation einer Zeile zur Kopie des gesamten Dataframes.
16.3 Tibbles
In Ergänzung zu den R Dataframes haben in den letzten zehn Jahren die Tibbles (diminutiv für tables) als Teil des tidyverse Pakets Verbreitung gefunden. Tibbles sind programmiertechnisch optimierte Dataframes und stellen ein Beispiel für die datenwissenschaftliche Verallgemeinerung der R Anwendung über den Bereich der linearen statistischen Modellbildung hinaus dar. Zur Nutzung von Tibbles muss das tidyverse Paket installiert und der Tibble Code geladen sein:
install.packages("tidyverse") # Einmaliger Paket Download und Installationlibrary(tibble) # Sessionspezifisches Laden des tibble Codes
Im Großen und Ganzen enstpren Tibbles den Dataframes. Wie bei Dataframes handelt es sich bei Tibbles um Listen von Vektorreferenzen, wobei auch hier die Vektoren die gleiche Länge haben und die benannten Spalten einer Tabelle darstellen. Tibbles können mit der Funktion tibble() erzeugt werden. Die Anzeige im R Terminal ist im Vergleich zu Dataframes optimiert, wie folgendes Beispiel zeigt:
library(tibble) # PaketT =tibble(x =c("a", "c", "d"), y =c(1,2,3), z =c(TRUE,FALSE,FALSE)) # Erzeugungprint(T) # Ausgabe
# A tibble: 3 × 3
x y z
<chr> <dbl> <lgl>
1 a 1 TRUE
2 c 2 FALSE
3 d 3 FALSE
Tibbles wurden 2016 im Rahmen des tidyverse Pakets eingeführt, um wahrgenommene Schwächen der R Dataframes auszugleichen. Dazu gehörte unter anderem, dass die Funktion data.frame() character Vektoren automatisiert in den R Datentyp factor umwandelte. Dies ist seit einer R Revision von 2019 allerdings nicht mehr der Fall, wie folgendes Beispiel zeigt:
D =data.frame(x =1:3, y =c("a", "b", "c")) # character vector y str(D) # ... bleibt character vector
'data.frame': 3 obs. of 2 variables:
$ x: int 1 2 3
$ y: chr "a" "b" "c"
Per Default wandelt data.frame() weiterhin nicht legale Spaltennamen wie eine Zahl (1) in legale Spaltennamen (X1) um. Möchte man dies nicht, so kann man dieses Verhalten allerdings durch Setzen des Arguments check.names abstellen
names(data.frame(`1`=1)) # Per Default Umwandlung von nicht legalen Variablennamen
[1] "X1"
names(data.frame(`1`=1, check.names =FALSE)) # Dieser Default kann jedoch abgestellt werden.
[1] "1"
Wie in Kapitel 14 gesehen, hat R die Tendenz, bei von ihrer Länge unpassenden Datentypen selbst Daten durch Recycling zu erzeugen, was leicht zu Fehlern führen kann. So recyclet die Funktion data.frame() bei der Erzeugung eines Dataframes zum Beispiel Vektoren, wenn einer davon ein ganzzahliges Vielfaches des anderen ist:
D =data.frame(x =1:2, y =3:6) # length(y) = 2 * length(x)print(D) # Ausgabe
x y
1 1 3
2 2 4
3 1 5
4 2 6
tibble() dagegen ist hier genauer und gibt im obigen Fall einen Fehler aus:
T =tibble(x =1:2, y =3:6) # tibble gibt hier eine Fehler aus
Error in `tibble()`:
! Tibble columns must have compatible sizes.
• Size 2: Existing data.
• Size 4: Column `y`.
ℹ Only values of size one are recycled.
Vektoren der Länge 1 werden allerdings auch von tibble() recyclet:
T =tibble(x =1:3, y =3) # Vektoren mit length 1print(T) # Ausgabe
# A tibble: 3 × 2
x y
<int> <dbl>
1 1 3
2 2 3
3 3 3
Dataframes sind manchmal inkonsistent, was die von ihnen ausgegeben Datentypen angeht. So ergibt, wie unteres Beispiel zeigt, die Indizierung einer Dataframespalte mit ihrem Namen etwa einen Dataframe, die Indizierung der gleichen Spalte mit ihrem Namen in Matrixindizierung allerdings einen Vektor:
D =data.frame(x =1:3, y =4:6) # Dataframestr(D["x"]) # Indizierung mit Namen gibt einen Dataframe aus
'data.frame': 3 obs. of 1 variable:
$ x: int 1 2 3
str(D[,"x"]) # Matrixindizierung mit Namen gibt einen Vektor aus
int [1:3] 1 2 3
Tibbles sind in dieser Hinsicht konsistenter:
T =tibble(x =1:3, y =4:6) # Tibblestr(T["x"]) # Indizierung mit Namen ergibt tibble
Ein zusätzliches Feature von Tibbles gegenüber Dataframes ist, dass die Funktion tibble() es erlaubt, Spaltennamen schon bei Erzeugung eines Tibble zu referenzieren, was bei Dataframes nicht möglich ist. Zusätzliches Feature von Tibbles
T =tibble(x =1, y =2*x) # Die Spalte x wird definiert und und benutztD =data.frame(x =1, y =2*x) # data.frame erlaubt dies nicht
Error in eval(expr, envir, enclos): object 'x' not found
Zusammenfassend lässt sich festhalten, dass Tibbles in mancher Hinsicht weniger flexibel und damit präziser als Dataframes funktionieren. Generell verhalten sich Tibbles in tidyverse Kontexten konsistenter als Dataframes. Allerdings sind die Unterschiede zwischen Dataframes und Tibbles wie oben gesehen eher subtil, sodass ein effizienter Umgang mit Dataframes auch einen effizienten Umgang mit Tibbles ermöglicht. Da nicht alle R Projekte die Pakete des tidyverse nutzen, werden wohl auch in Zukunft Dataframes die Standarddatenstruktur für tabellarische Daten in R bleiben.