Listen

Listen sind uns aus der R-Übung schon im Rahmen der Gruppenweisen Aggregation mit across und als Output von Inferenzstatistischen Funktionen untergekommen. Aber was genau Listen architektonisch sind, haben wir bisher übergangen.

Listen sind in R ziemlich ähnlich zu Vektoren 3. Sie bilden Aneinander-Kettungen von Objekten ab, bei denen wir die einzelnen Elemente benennen können. Ein Unterschied zu Vektoren ist aber, dass die Objekte nicht von einem einzelnen Typ sein müssen. Ein anderer Unterschied wird deutlich, wenn wir uns den Output von benannten Vektoren und Listen genauer anschauen.

c('a'= 1,
  'b'= 2)
## a b 
## 1 2
list('a'= 1,
     'b'= 2)
## $a
## [1] 1
## 
## $b
## [1] 2

Die Ausgabe der Liste ähnelt unter der jeweiligen Überschrift (z.B.: $a) dem Output, den wir bei einem unbenannten Vektor sehen:

c(1)
## [1] 1

Das liegt daran, dass in einer Liste unter dem Namen auch der ganze Vektor “abgespeichert” ist. Abgespeichert ist hier in Anführungszeichen, da in der Liste eigentlich nur ein Verweis auf einen Vektor liegt. Das kann man sich verdeutlichen, wenn man sich die Größen von Vektoren und Listen im Arbeitsspeicher anguckt.

Dazu erstellen wir einen Vektoren mit den Zahlen von 1:1000 und eine Liste, der wir dreimal diesen Vektor übergeben.

a <- c(1:1000)

b <- list(a = a, b = a, c = a, d = a)

Wenn wir uns jetzt die Größen der beiden Objekte angucken, sehen wir dass die Liste kleiner ist, als vielleicht zuerst erwartet:

lobstr::obj_size(a)
## 4,048 B
lobstr::obj_size(b)
## 4,544 B

Wenn wir die Liste mit einem vergleichbaren Vektor gegenüberstellen sehen wir, dass dieser die Werte offensichtlich direkt ablegt, wohingegen die Liste die Werte nur einmal beinhaltet (plus ein bisschen Speicher für die wiederholten Verweise und Namen):

d <- c(a, a, a, a)

lobstr::obj_size(a)
## 4,048 B
lobstr::obj_size(b)
## 4,544 B
lobstr::obj_size(d)
## 16,048 B

Was passiert nun, wenn wir einen Teil eines der vier Einträge in der Liste ändern?

Dazu können wir die schon von data.frames bekannte Index-Variante mit dem $-Operator nutzen um einen der Vektoren in der Liste zu modifizieren:

b$a[1] <- 5
lobstr::obj_size(b)
## 12,592 B

Die Liste wird größer. Dass sie nicht in Inkrementen von 4000 B größer wird, liegt daran, dass die Zahlen in a als Sequenz von R effizienter gespeichert werden können, als als einfache Zahlen. Wenn wir uns a alleine angucken und die Größe vor und nach Änderung der ersten Stelle betrachten, wird das deutlich:

lobstr::obj_size(a)
## 4,048 B
a[1] <- 5

lobstr::obj_size(a)
## 8,048 B

Für mehr Details zu diesem Speicher-Verhalten und dem zugrunde liegenden Prinzip ist das kostenlos hier zugängliche Buch “Advanced R” Wickham (2019) sehr gut, vor allem die Kapitel 2.2 und folgende und das Kapitel über Vektoren.

Aufgabe

Überlege dir, wie sich der Speicherbedarf der Liste b ändert, wenn Du den zweiten Platz der Liste mit dem ersten Platz der Liste überschreibst. Probiere dann aus, ob sich deine Vorhersage bewahrheitet. Überprüfe dann, was passiert, wenn du die erste Stelle des dritten Platzes der Liste durch fünf ersetzt. Was könnte hier passiert sein?

Antwort
a <- c(1:1000)
b <- list(a = a,b = a,c = a,d = a)
b$a[1] <- 5
lobstr::obj_size(b)
## 12,592 B
b$b <- b$a
lobstr::obj_size(b)
## 12,592 B
b$c[1] <- 5
lobstr::obj_size(b)
## 20,640 B

Listen und Datensätze

Das Arbeiten mit Listen ist ziemlich ähnlich zu der mit Datensätzen. Das liegt ganz einfach daran, dass Datensätze auch Listen sind, der einzige wirklich wichtige Unterschied ist, dass Datensätze im Vergleich zu Listen einheitliche Längen von den eingefügten Vektoren erwarten.

Zu sehen ist dieses Verhältnis ganz einfach, wenn man sich den mode eines Datensatzes anguckt:

a <- data.frame(1:10,
                1:10)

class(a)
## [1] "data.frame"
mode(a)
## [1] "list"

Der Unterschied ist in den attributes des Datensatzes festgelegt.

attributes(a)
## $names
## [1] "X1.10"   "X1.10.1"
## 
## $class
## [1] "data.frame"
## 
## $row.names
##  [1]  1  2  3  4  5  6  7  8  9 10

Nur zum Spaß können wir so auch versuchen, umständlich einen Datensatz zu erstellen:

a <- list(1:10,1:10)
attributes(a)
## NULL
attributes(a) <- list(names = letters[1:2],
                      row.names = 1:10,
                      class = "data.frame")
a
##     a  b
## 1   1  1
## 2   2  2
## 3   3  3
## 4   4  4
## 5   5  5
## 6   6  6
## 7   7  7
## 8   8  8
## 9   9  9
## 10 10 10

Aufgabe

Überlege Dir, was wohl passiert, wenn du auf die gerade demonstrierte Art und Weise einen Datensatz erstellst, bei dem die eingefügten Vektoren von unterschiedlicher Länge sind. Überprüfe dann, ob die Erwartungen stimmen.

Antwort
a <- list(1:15,21:30,41:45)
attributes(a)
## NULL
attributes(a) <- list(names = letters[1:3],
                      row.names = 1:10,
                      class = "data.frame")
a
## Warning in format.data.frame(if (omit) x[seq_len(n0), , drop = FALSE] else x, :
## corrupt data frame: columns will be truncated or padded with NAs
##     a  b    c
## 1   1 21   41
## 2   2 22   42
## 3   3 23   43
## 4   4 24   44
## 5   5 25   45
## 6   6 26 <NA>
## 7   7 27 <NA>
## 8   8 28 <NA>
## 9   9 29 <NA>
## 10 10 30 <NA>

Arbeiten mit Listen

Da Datensätze eigentlich nur Listen sind, gibt es Listen-Operationen die wir schon von Datensätzen kennen, bei denen wir einfach noch nicht wussten, dass sie eigentlich aus dem Listen-Kontext stammen.

Insbesondere sind die Operationen, die wir auch schon hier im Skript genutzt haben, Listen-Operationen, die wir aus dem data.frame-Kontext kennen. Da wären das Anlegen von Spalten/Listen-Einträgen mit Namen, wie wir es eben gesehen haben:

a <- list(a = 1:10)
b <- data.frame(a = 1:10)

Und das Indizieren mit dem $-Operator:

a$a
##  [1]  1  2  3  4  5  6  7  8  9 10
b$a
##  [1]  1  2  3  4  5  6  7  8  9 10

Als kleinen Zusatz können wir uns noch die numerische Indizierung angucken, die bei Listen und damit auch Datensätzen mit doppelten eckigen Klammern funktioniert ([[]]). Dieser Index-Operator ist hilfreich, wenn nicht jedes mal für jeden Eintrag ein Name angelegt werden soll. Das kann zum Beispiel sinnvoll bei functional-Iteratoren sein, bei denen man nur eine schnelle Stapelverarbeitung plant, dazu aber später mehr.

Bei Listen und Datensätzen sieht der [[]]-Einsatz dann so aus:

a[[1]]
##  [1]  1  2  3  4  5  6  7  8  9 10
b[[1]]
##  [1]  1  2  3  4  5  6  7  8  9 10

Aufgabe

Baue ein Skript, das eine Liste erstellt, die drei Einträge enthält. Jeder dieser Einträge soll auch wieder eine Liste sein. Das Skript soll nun mit Hilfe einer Schleife die Zahlen von 1 bis 100 durchgehen und alle durch 2 teilbaren Zahlen in den zweiten Eintrag, alle durch 3 teilbaren Zahlen in den dritten Eintrag und alle restlichen Zahlen in den ersten Eintrag einfügen. Nutze hierfür den “Modulo”-Operator %%, der den ‘Rest’ einer Ganzzahldivision ausgibt. Überlege dir, wie du mit Zahlen wie der 6 umgehst, die sowohl durch 3 als auch durch 2 teilbar sind. Kleiner Tipp: length kann hier sehr hilfreich sein.

Zusatzaufgabe: Überlege dir, wie man dieses Skript so umbauen könnte, dass es für einen Vektor mit beliebigen Teilern funktioniert.

Antwort
divisions <- list(list(), list(), list())

for(i in 1:100){
  if(i%%2 == 0){
    divisions[[2]][[length(divisions[[2]]) + 1]] <- i
  }
  if(i%%3 == 0){
    divisions[[3]][[length(divisions[[3]]) + 1]] <- i
  }
  if(i%%2 != 0 & i%%3 != 0){
    divisions[[1]][[length(divisions[[1]]) + 1]] <- i
  }
}
summary(divisions)
##      Length Class  Mode
## [1,] 33     -none- list
## [2,] 50     -none- list
## [3,] 33     -none- list
## Zusatz:
divisions <- list(list()) # mit einer leeren Liste für die nicht-teilbaren initiieren
divider <- c(2,3,5,7,9)
for(i in divider){
  divisions[[i]] <- list()
}
for(i in 1:100){
  divided <-  F
  for(j in divider){
    if(i %% j == 0){
      divisions[[j]][[length(divisions[[j]]) + 1]] <- i
      divided <- T
    }
  }
  if(!divided){
    divisions[[1]][[length(divisions[[1]]) + 1]] <- i
  }
}

summary(divisions)
##       Length Class  Mode
##  [1,] 22     -none- list
##  [2,] 50     -none- list
##  [3,] 33     -none- list
##  [4,]  0     -none- NULL
##  [5,] 20     -none- list
##  [6,]  0     -none- NULL
##  [7,] 14     -none- list
##  [8,]  0     -none- NULL
##  [9,] 11     -none- list
unlist(divisions[[1]]) # Mit Teilern und ohne 1 haben wir hier die Primzahlen bis 100 gesammelt.
##  [1]  1 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

Literatur

Wickham, Hadley. 2019. Advanced r. CRC press.

  1. Wobei der Ausdruck “Vektoren” hier irreführend ist, in R wird eigentlich die Eltern-Klasse der Objekte Vector genannt, zu der Atomic(unsere “Vektoren”) und List gehören. Da wir aber in den Grundlagenfächern wegen der mathematischen Analogie “Vektor” zu den Atomic-Objekten gesagt haben, behalten wir das hier bei.↩︎