16  Listen, Dataframes und Tibbles

16.1 Listen

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 Funktionselement
L = list(c(1,4,5), matrix(1:8, nrow = 2), exp)                                  
print(L)
[[1]]
[1] 1 4 5

[[2]]
     [,1] [,2] [,3] [,4]
[1,]    1    3    5    7
[2,]    2    4    6    8

[[3]]
function (x)  .Primitive("exp")

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 Liste
print(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 Listen
print(L)                                # Ausgabe

Charakterisierung

Der Datentyp von Listen ist list:

L = list(1:2, "a", log)                 # Erzeugung einer Liste
typeof(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 Toplevelelementen
length(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 Liste
dim(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 Liste
l1 = L[1]                               # Indizierung eines Listenelements
print(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 Liste
i2 = L[[2]]                             # Indizierung des Listenelementinhalts
print(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 Liste
L[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. Listenelementes
L[[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 Liste
l = L[c(1,3)]                           # 1. und 3. Listenelement
l
[[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 Liste
l = L[-c(1,3)]                          # 2. Listenelement
l
[[1]]
[1] "a"

Logische Vektoren adressieren Elemente mit Index TRUE:

L = list(1:3, "a", pi)                  # eine Liste
l = L[c(T,T,F)]                         # 1. und 2. Listenelement
l
[[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 Liste
names(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 Namenvergabe
L["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 Namenvergabe
L$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 Liste
L2 = list(T, exp )          # eine Liste
L1+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 Liste
L2 = list(4:6, exp )        # eine Liste
L1[[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
█ [1:0x235d68744f8] <list> 
├─[2:0x235d3495d58] <dbl> 
├─[3:0x235d3495d20] <dbl> 
└─[4:0x235d3495cb0] <dbl> 
L2 = L1                     # L1 und L2 referenzieren dasselbe Objekt  
lobstr::ref(L2)             # Ausgabe der Referenzen
█ [1:0x235d68744f8] <list> 
├─[2:0x235d3495d58] <dbl> 
├─[3:0x235d3495d20] <dbl> 
└─[4:0x235d3495cb0] <dbl> 
L2[[3]] = 4                 # Copy-on-Modify mit shallow Objekt Kopie
lobstr::ref(L2)             # Ausgabe der Referenzen
█ [1:0x235d681d278] <list> 
├─[2:0x235d3495d58] <dbl> 
├─[3:0x235d3495d20] <dbl> 
└─[4:0x235d3495b28] <dbl> 

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 x
               y = 1:4,             # 2. Spalte mit Name y
               z = c(T,T,F,T))      # 3. Spalte mit Name z
print(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 x
               y = 1:4,             # 2. Spalte mit Name y
               z = 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. Spalte
               height = c(178,189,165,171), # 2. Spalte
               weight = c(67, 76, 81, 92))  # 3. Spalte
names(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”:

typeof(D)
[1] "list"
attributes(D)
$names
[1] "age"    "height" "weight"

$class
[1] "data.frame"

$row.names
[1] 1 2 3 4

Indizierung

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 x
               y = 1:4,             # 2. Spalte mit Name y
               z = c(T,T,F,T))      # 3. Spalte mit Name z
class(D)                            # D ist ein Dataframe
[1] "data.frame"
v = D[1]                            # 1. Listenelement als Dataframe
v
  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 Spalte
y
[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 Installation
library(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)                                                             # Paket
T = tibble(x = c("a", "c", "d"),  y = c(1,2,3), z = c(TRUE,FALSE,FALSE))    # Erzeugung
print(T)                                                                    # Ausgabe
# A tibble: 3 × 3
  x         y z    
  <chr> <dbl> <lgl>
1 a         1 TRUE 
2 c         2 FALSE
3 d         3 FALSE
attributes(T)                                                               # Attribute
$class
[1] "tbl_df"     "tbl"        "data.frame"

$row.names
[1] 1 2 3

$names
[1] "x" "y" "z"

16.3.1 Tibbles vs. Dataframes

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 1
print(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)    # Dataframe
str(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)    # Tibble
str(T["x"])                     # Indizierung mit Namen ergibt tibble
tibble [3 × 1] (S3: tbl_df/tbl/data.frame)
 $ x: int [1:3] 1 2 3
str(T[,"x"])                    # Matrixindizierung mit Namen ergibt tibble
tibble [3 × 1] (S3: tbl_df/tbl/data.frame)
 $ x: int [1:3] 1 2 3

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 benutzt
D = 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.