3 data.frames

Wir haben gelernt, dass R Daten in Vektoren abspeichert. Im Normalfall haben wir in der psychometrischen Datenauswertung aber eine große Datenmenge vorliegen, die wir nicht sinnvoll als einzelnen Vektor darstellen können. Etwa: 150 Studierende bearbeiten in einer Diagnostikklausur 42 Multiple-Choice-Klausuritems. Wir stellen solche Daten in Tabellen dar, wie man sie auch aus Excel oder SPSS kennt. In diesen Tabellen repräsentieren Spalten Messvariablen, etwa die Punktzahlen in einer Klausuraufgabe. Zeilen stellen Fälle dar, etwa Personen, die an der Klausur teilgenommen haben. Andere Datenformate wären auch denkbar, etwa eines bei dem jede Zeile einer Antwort entspricht. Bei uns wird aber gelten: Jede Zeile entspricht genau einer Person.

In R speichern wir Datentabellen in data.frames ab. Ein data.frame ist, vereinfacht gesagt, eine Sammlung von Vektoren gleicher Länge. Jede Spalte – also jede Messvariable – ist ein Vektor. Mit dieser Datenstruktur werden wir uns im vorliegenden Kapitel auseinandersetzen.

3.1 Die Funktion data.frame()

Mit der Funktion data.frame() können wir “händisch” einen data.frame erstellen.15 Die folgende unscheinbare Tabelle wird uns durch einen Großteil des Kapitels begleiten, um Grundlagen von data.frame-Operationen zu betrachten.

Der erstellte data.frame ist nun in der Variablen mdf – was beispielsweise für “mein data.frame” stehen könnte – abgespeichert. Durch Eingabe des Variablennamens in der R-Konsole können wir die ganze Tabelle ausgeben lassen:

  Fallnummer Item1 Item2 Alter Geschlecht
1          1     1     1    13          w
2          2     0     1    14          m
3          3     0     0    13          m
4          4     1     0    12          w
5          5     1     1    15          m

Bei der Erstellung des data.frames mit der Funktion data.frame() wurde jede Spalte mit der Funktion c() oder dem Doppelpunktoperator mit genau einem Vektor befüllt. Alle Spalten wurden bei der Erstellung benannt. Dieser Punkt ist sehr wichtig, da wir Spalten anhand ihrer Namen gezielt auswählen können. Wenn ich die Spaltennamen eines data.frames nicht mehr weiß, kann ich sie mit der Funktion names() abrufen:

[1] "Fallnummer" "Item1"      "Item2"      "Alter"      "Geschlecht"

3.2 Zugriff auf eine einzelne Spalte: die $-Notation

Der $-Zugriff ist die grundlegendste Operation auf data.frames. Wir nutzen ihn, um auf einzelne Spalten zuzugreifen und diese als Vektor auszulesen:

[1] 1 0 0 1 1

Ich kann den $-Zugriff nicht nur verwenden, um eine Spalte aus einem data.frame auszulesen, sondern kann damit auch neue Spalten hinzufügen. Das funktioniert, indem ich der neu zu erstellenden Spalte per “<-” einen Vektor zuweise:

Die Tabelle mdf16 hat nun eine zusätzliche Spalte:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe
1          1     1     1    13          w      braun
2          2     0     1    14          m       blau
3          3     0     0    13          m       blau
4          4     1     0    12          w      braun
5          5     1     1    15          m      gruen

Beim Anhängen von Spalten an data.frames mit der $-Notation kann ich jegliche Berechnungsvorschriften für Vektoren verwenden. So kann ich etwa einen Testscore über zwei Items berechnen und direkt an den data.frame anhängen:

[1] 2 1 0 1 2

In diesem Beispiel kommt die $-Notation recht häufig zum Einsatz, was etwas gewöhnungsbedürftig aussieht. Aber es ist wichtig darauf zu achten. Die Variablen,17 die wir verwenden, um den Testscore zu berechnen, “wohnen” in mdf und können nicht ohne Verweis darauf adressiert werden. Das hier geht schief:

Hier sucht R nach einer Variablen Item1, die aber nicht existiert; Item1 ist nur eine Spalte von mdf. Noch schlimmer wäre es, wenn in meiner Arbeitsumgebung tatsächlich Variablen mit den Namen Item1 und Item2 existieren sollten. In dem Fall würden wir gegebenenfalls falsche Daten abspeichern und nicht einmal eine Fehlermeldung erhalten.

Mit der $-Notation werden wir häufig auf Daten zugreifen, um Berechnungen durchzuführen. Wir können beispielsweise Mittelwerte von Messvariablen berechnen oder uns Häufigkeiten von kategorialen Daten angeben lassen:

[1] 13.4

m w 
3 2 

Die Funktion mean() kennen wir bereits. Die Funktion table() gibt aus, wie häufig jeder Wert in einem Vektor vorkommen. Wir nutzen table() vor allem zur Beschreibung kategorialer Messvariablen. Auch zur Überprüfung der Plausibilität von Daten ist table() nützlich: Ist jeder Wert ein “legaler” Wert, der auch vorkommen sollte? Wir können die Funktion table() auch verwenden, um die Häufigkeit der Kombination von mehreren Variablen zu erfragen:

   
    blau braun gruen
  m    2     0     1
  w    0     2     0

3.3 Zugriff auf Spalten und Zeilen: die [·,·]-Notation

Einzelne Spalten können wir mit dem $-Zugriff aus data.frames auslesen. Wir lernen nun den [·,·]-Zugriff kennen, mit dem wir nicht nur einzelne Spalten, sondern beliebige Spalten und Zeilen aus data.frames auslesen können. Wie wir sehen werden, ist der [·,·]-Zugriff dem [·]-Zugriff ähnlich, den wir zur Auswahl von Daten aus Vektoren kennengelernt haben.

Der [·,·]-Zugriff erlaubt es uns, eine Teilmenge aller Fälle aus mdf auszuwählen, etwa nur die Personen mit blauen Augen, oder alle Personen, die den maximalen Testwert erreicht haben. Für solche Auswahlen hilft uns unser Wissen über logische Vergleiche aus dem letzten Kapitel. Betrachten wir zunächst ein Beispiel:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0

Beachtet, dass durch diesen Aufruf der data.frame, der in der Variablen mdf abgespeichert ist, nicht verändert wird. Der [·,·]-Zugriff gibt stattdessen einen neuen data.frame zurück, der nur die Fälle enthält, bei denen mdf$Augenfarbe == "blau" TRUE ergibt. Wir müssten das Ergebnis der Funktion in einer Variablen speichern, wenn wir damit weiter arbeiten wollen (Erinnerung: Kapitel 2).

Wie das folgende Beispiel zeigt, können wir mit der [·,·]-Notation auch gezielt Spalten aus data.frames auswählen:

  Augenfarbe Alter
1      braun    13
2       blau    14
3       blau    13
4      braun    12
5      gruen    15

Die zwei Beispiele zeigen, dass das Komma in der [·,·]-Notation dafür entscheidend ist, ob eine Auswahl nach Zeilen oder Spalten stattfindet. Vor dem Komma werden Zeilen adressiert, nach dem Komma Spalten. Es ist auch eine gleichzeitige Auswahl nach Zeilen und Spalten möglich. Allgemein ist die Syntax zum Ansprechen von data.frames mit dem [·,·]-Zugriff die folgende:

data.frame[Reihenvektor, Spaltenvektor]

Dabei ist Reihenvektor/Spaltenvektor entweder ein (a) numerischer Vektor, der die Indexe der Reihen/Spalten enthält, die ausgewählt werden sollen, (b) ein logischer Vektor, der für jede Reihe/Spalte kodiert, ob diese in der Ausgabe enthalten sein soll (vgl. Kapitel 2), oder (c) Vektor vom Typ character, der die Namen der Zeilen/Spalten enthält, die ausgegeben werden sollen.18

Spalten werden am häufigsten per Namen – also durch Angabe eines einen Vektors vom Typ character – adressiert; Zeilen werden am häufigsten durch einen logischen Ausdruck – also durch Angabe eines einen Vektors vom Typ logical – adressiert (“Gib mir alle Fälle aus, die eine bestimmte Eigenschaft aufweisen.”).

3.3.1 Spaltenauswahl von Items mit der Funktion paste0()

Die Funktion paste0() kann genutzt werden, um effizient Spaltennamen zu erstellen, wenn die Spaltennamen einem bestimmten Schema folgen, wie es bei der Benennung von Fragebogen-Items oftmals der Fall ist. Sie erstellt character-Vektoren beliebiger Länge. Etwa wie folgt lassen sich bequem 10 durchnummerierte Itemnamen generieren:

 [1] "item_1"  "item_2"  "item_3"  "item_4"  "item_5"  "item_6"  "item_7" 
 [8] "item_8"  "item_9"  "item_10"

Hierbei wird der Text “item_” mit den Zahlen von 1 bis 10 gepaart. Das Ergebnis des Befehls ist ein 10-elementiger Vektor, der zur Spaltenauswahl mithilfe der [·,·]-Notation genutzt werden könnte. Diese Operation ist nützlich, da Spaltennamen in echten Datentabellen oft aus einem fixen Stamm – hier item – und einem variablen Teil bestehen – hier den Zahlen von 1 bis 10. Die Funktion paste0() ermöglicht uns dann, den fixen Teil nicht mehrfach aufschreiben zu müssen. Außerdem besteht der variable Teil oftmals aus einer aufsteigenden Zahlenfolge, die wir unkompliziert mit dem Doppelpunktoperator erstellen können.19

In unserer kleinen Datentabelle mdf können wir demnach wie folgt die zwei Items auswählen:

  Item1 Item2
1     1     1
2     0     1
3     0     0
4     1     0
5     1     1

Natürlich ist diese Operation nützlicher, wenn die Tabelle mehr Items enthält.

Die Funktion paste0() ist nicht auf die Kombination von zwei Vektoren beschränkt, sondern verknüpft im Allgemeinen beliebig viele Vektoren. Sollten die Spaltennamen beispielsweise noch ein festes Suffix nach der Nummerierung enthalten, kann dieses einfach durch ein zusätzliches Argument angefügt werden:

[1] "item_1R" "item_2R" "item_3R"

Eine Verallgemeinerung von paste0() stellt die Funktion paste() dar, die das Trennzeichen zwischen den zusammengefügten Vektoren explizit definieren kann. Dazu verwendet paste() das benannte Argument sep:

 [1] "item-1"  "item-2"  "item-3"  "item-4"  "item-5"  "item-6"  "item-7" 
 [8] "item-8"  "item-9"  "item-10"

Die Funktionen paste() und paste0() arbeiten vektorisiert und im Allgemeinen auch komponentenweise, verknüfen also gleich lange Eingabevektoren paarweise anhand der Position ihrer Elemente.

3.3.2 Spaltenauswahl von Items mit der Funktion grepl()

Die Funktion grepl() kann ebenfalls genutzt werden, um gezielt Spalten auszuwählen, deren Namen einem gewissen Format entsprechen. Sie nimmt (als zweites Argument) einen Vektor vom Typ character entgegen und vergleicht darin alle Elemente mit einer Vorlage (die als erstes Argument übergeben wird):

[1] "Fallnummer" "Item1"      "Item2"      "Alter"      "Geschlecht"
[6] "Augenfarbe" "Testscore" 
[1] FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE
  Item1 Item2
1     1     1
2     0     1
3     0     0
4     1     0
5     1     1

Ähnlich funktioniert die Funktion grep(), die jedoch numerische Indices statt eines logischen Vektors zurückgibt und demnach auch zur Spaltenauswahl genutzt werden kann.

[1] 2 3

Bei der Nutzung von grepl() oder grep() zur Spaltenauswahl ist Vorsicht geboten, dass wirklich nur die gewünschten Spalten ausgewählt werden und nicht andere, die zufällig denselben Stamm enthalten, aber nicht in der Ausgabemenge erwünscht sind.

3.3.3 Komplexere logische Operationen zur Zeilenauswahl

Durch die UND- bzw. ODER-Operationen können wir auch komplexere logische Bedingungen zur Auswahl von Fällen formulieren:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0
4          4     1     0    12          w      braun         1
  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
4          4     1     0    12          w      braun         1

Beachtet, dass wir hier ohne Klammerung der ODER-Operation eine andere Ausgabe erhalten (Erinnerung: Diesen Fall kennen wir auch aus Kapitel 2):

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0
4          4     1     0    12          w      braun         1

Wie wir merken, wird die [·,·]-Notation recht schnell unübersichtlich, wenn sie komplexere logische Bedingungen enthält. Die Verknüpfung mehrerer ODER-Bedingungen lässt sich durch den %in%-Operator verkürzen:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0
4          4     1     0    12          w      braun         1

Allgemein können beliebige logische Operationen bzw. Vektoren zur Zeilenauswahl angegeben werden. Im nächsten Abschnitt lernen wir mit der Funktion subset() eine Möglichkeit kennen, komplexere logische Anfragen zur Zeilenauswahl noch etwas prägnanter zu formulieren.

3.3.4 Weitere Beispiele zur Verwendung der [·,·]-Notation

Zum Abschluss dieses Abschnitts betrachten wir noch einige weitere Beispiele für die verschiedenen Auswahlmöglichkeiten per [·,·]:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0
  Item1 Alter
1     1    13
2     0    14
3     0    13
4     1    12
5     1    15
  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
5          5     1     1    15          m      gruen         2
  Fallnummer Alter Testscore
1          1    13         2
2          2    14         1
3          3    13         0
4          4    12         1
5          5    15         2
  Fallnummer Alter Testscore
2          2    14         1
5          5    15         2
  Fallnummer Alter Testscore
1          1    13         2
2          2    14         1
3          3    13         0
  Item1 Item2
1     1     1
2     0     1
3     0     0
4     1     0
5     1     1

Merke: Mit dem [·,·]-Zugriff wird vor dem Komma die Zeile und nach dem Komma die Spalte adressiert. Man kann die Auswahl nach numerischem Index, mit einem logischen Vektor oder mit einem character-Vektor durchführen.

3.4 Die Funktion subset()

In diesem Abschnitt lernen wir die Funktion subset() kennen, die wir ebenfalls verwenden können, um Zeilen und Spalten aus data.frames auszulesen. Ganz ähnlich zur [·,·]-Notation funktioniert etwa der folgende Aufruf:

  Item1 Augenfarbe
2     0       blau
3     0       blau

Als erstes Argument nimmt die Funktion subset() den data.frame an, aus dem wir Daten auslesen wollen. Danach folgen Argumente zur Auswahl von Zeilen und zur Auswahl von Spalten.

Wie hier angewendet, ist durch die Funktion subset() im Vergleich zur [·,·]-Notation noch nicht viel gewonnen. Was die Funktion subset() so nützlich macht, ist dass sie zwei wichtige Zugriffe auf data.frames vereinfacht:

  1. Die Auswahl von Zeilen mithilfe logischer Bedingungen
  2. Die Auswahl von Spalten durch Angabe von Spaltennamen

Diese Vereinfachungen besprechen wir im Folgenden. Des Weiteren bietet dieser Abschnitt einen ersten theoretischen Überblick über die Arbeitsweise von Funktionen in R, der in Kapitel 6 vertieft wird.

3.4.1 Vereinfachte Zeilenauswahl

Die Funktion subset() erlaubt uns logische Bedingungen für die Zeilenauswahl zu formulieren, ohne die $-Notation zu verwenden:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0

Folgendermaßen könnte man durch Verwendung von $ einen äquivalenten Aufruf durchführen, der uns eher an die [·,·]-Notation erinnert:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0

Außerhalb der Funktion subset() würde der Ausdruck Augenfarbe == "blau" einen Fehler ausgeben; schließlich ist Augenfarbe selbst keine R-Variable, sondern nur eine Spalte von mdf.20 Innerhalb der Funktion subset() kann die logische Bedingung in dieser Form jedoch verarbeitet werden.

Der äquivalente Befehl mit der [·,·]-Notation sähe folgendermaßen aus:

Gerade bei der Verknüpfung mehrerer logischer Bedingungen ist es praktisch, nicht mehrfach die $-Notation verwenden zu müssen:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
2          2     0     1    14          m       blau         1

3.4.2 Funktionsargumente

Die Funktion subset() nimmt optional ein drittes Argument an, das auszulesende Spalten adressiert:

  Item1 Augenfarbe
2     0       blau
3     0       blau

Durch die Kombination der Auswahl von Zeilen und Spalten gibt dieser Befehl einen data.frame aus, der nur die Spalten Item1 und Augenfarbe enthält, und diese nur für Personen mit blauen Augen.

Was machen wir aber, wenn wir nur eine Auswahl nach Spalten durchführen wollen? Probieren wir erst einmal Folgendes:

Hier habe ich einfach das Argument für die Zeilenauswahl weggelassen und als zweites Argument einen character-Vektor zur Auswahl zweier Spalten angegeben – was aber nicht funktioniert hat. R gibt uns eine kryptische Fehlermeldung aus:

Warum gibt uns R hier einen Fehler aus? An dieser Stellen machen wir uns eine wichtige Eigenschaft von Funktionen bewusst: Funktionen identifizieren Argumente anhand der Reihenfolge, in der sie übergeben werden. Bei der Funktion subset() ist das erste Argument der data.frame, von dem wir Daten anfordern. Das zweite Argument wählt mit einem logischen Ausdruck Zeilen aus. Das dritte Argument adressiert Spalten.

Wir erhalten den obigen Fehler also, weil die Funktion subset() an zweiter Stelle einen logischer Ausdruck zur Zeilenauswahl erwartet; die Auswahl der Spalten muss mit dem dritten Argument geschehen. Um eine Auswahl trotzdem nur nach Spalten auszuführen, können wir eine praktische Eigenschaft von R ausnutzen: Funktionsargumente haben Namen. Anstatt Argumente anhand ihrer Position zu identifizieren, können wir sie auch benennen. Bislang haben wir das ignoriert bzw. es ist uns nur am Rande begegnet – erinnern wir uns an das Argument na.rm der Funktion mean().

Die Funktion subset() hat drei benannte Argumente:

  • x: der Datensatz, aus dem ausgewählt wird
  • subset: wählt Zeilen aus
  • select: wählt Spalten aus

Um eine Übersicht über die Argumente einer Funktion zu erhalten, können wir mit dem ?-Operator die R-Hilfe anfordern:

Leider ist die R-Hilfe oftmals kryptisch – und das nicht nur für Anfänger. Sie ist die offizielle Dokumentation von Funktionen und legt deswegen zwar großen Wert auf technische Genauigkeit, ist aber nicht immer sonderlich ausführlich oder gar verständlich. Wir werden in Kapitel 6 bei einer ausführlicheren Besprechung von Funktionen noch einmal darauf zurückkommen, wie wir mit der R-Hilfe umgehen können.

Wenn wir die Namen der Argumente kennen, können wir die Funktion subset() auch wie folgt aufrufen:

  Item1 Augenfarbe
2     0       blau
3     0       blau

Beachtet, dass ich beim Funktionsaufruf Zeilenumbrüche zwischen den Argumenten nutze, was nicht hätte sein müssen – das mache ich nur, damit mein Code schön übersichtlich ist.

Wenn ich Funktionsargumente mit Namen adressiere, kann ich deren Reihenfolge beliebig vertauschen. Deswegen funktioniert auch der folgende Aufruf:

  Item1 Augenfarbe
2     0       blau
3     0       blau

Eine Auswahl nur anhand von Spalten können wir umsetzen, indem wir benannt nur das Argument select angeben, aber nicht das Argument subset:

  Item1 Augenfarbe
1     1      braun
2     0       blau
3     0       blau
4     1      braun
5     1      gruen

Dieser Aufruf zeigt, dass wir im selben Aufruf manche Argumente anhand ihrer Position identifizieren können und manche anhand ihres Namens. Für das erste Argument mdf habe ich den Namen nicht extra angegeben – daher wurde das Argument anhand seiner Position identifiziert. Für die Auswahl der Spalten habe ich jedoch den Argumentnamen angegeben. Das war auch nötig, da subset() als zweites Argument ansonsten die Auswahl der Zeilen erwartet hätte.

Merke: In R können Funktionsargumente per Position und per Namen identifiziert werden. Die Identifikation per Name schlägt dabei die Identifikation per Position. Argumente explizit mit ihrem Namen zu benennen ist oft sicherer als auf die richtige Reihenfolge der Argumente zu vertrauen.

3.4.3 Sonderregeln zur Auswahl von Spalten

Die Funktion subset() bietet einige Sonderregeln zur Auswahl von Spalten, die über die Angabe der Spaltennamen per character-Vektor hinausgehen, wie wir sie von der [·,·]-Notation kennen. Zunächst einmal können wir Spaltennamen ohne Anführungszeichen angeben:

  Augenfarbe Alter
1      braun    13
2       blau    14
3       blau    13
4      braun    12
5      gruen    15

Genau wie die Zeilenauswahl, die ohne die $-Notation auskommt, ist es eine Besonderheit der Funktion subset(), dass wir Spaltennamen ohne Anführungszeichen adressieren können. Einfach in die Konsole eingegeben würde der Ausdruck c(Augenfarbe, Alter) höchstwahrscheinlich21 einen Fehler verursachen, da Augenfarbe und Alter nicht unbedingt als Variablen definiert sind; sie sind bloß Spalten von mdf.

Die Auswahl von Spalten ohne Anführungszeichen ist noch keine allzu große Arbeitserleichterung im Vergleich zur [·,·]-Notation. Die Funktion subset() lässt aber noch einen weiteren Sonderfall bei der Spaltenauswahl zu, der einiges an Schreibarbeit ersparen kann: Wir können den Doppelpunktoperator verwenden, um mehrere Spalten auszuwählen.

  Item1 Item2 Alter Geschlecht
1     1     1    13          w
2     0     1    14          m
3     0     0    13          m
4     1     0    12          w
5     1     1    15          m

Von „links nach rechts“ wählt der Doppelpunktperator alle Spalten zwischen einschließlich Item1 und Geschlecht aus. Auch hier verzichten wir auf die Angabe von Anführungszeichen.

Es gibt noch eine weitere Vereinfachung, die uns die Funktion subset() bietet. Wir können angeben, welche Spalten wir nicht ausgeben wollen:

  Fallnummer Item1 Item2 Augenfarbe Testscore
1          1     1     1      braun         2
2          2     0     1       blau         1
3          3     0     0       blau         0
4          4     1     0      braun         1
5          5     1     1      gruen         2

3.4.4 Non-Standard-Evaluation

Wie schafft es die Funktion subset(), dass wir etwa wie folgt Spalten in einem data.frame – ohne Anführungszeichen und ohne die $-Notation – ansteuern können:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
2          2     0     1    14          m       blau         1
4          4     1     0    12          w      braun         1
  Item1 Item2 Alter
1     1     1    13
2     0     1    14
3     0     0    13
4     1     0    12
5     1     1    15

Dass das funktioniert, ist gewiss nicht selbstverständlich. Normalerweise würden wir ja erwarten, dass ein Name ohne Anführungszeichen eine eigenständige Variable sein muss, nicht bloß eine Spalte in einem data.frame. Auch der Doppelpunktoperator kann außerhalb von subset() nicht zur Generierung von Spaltennamen, sondern nur zur Erstellung ganzer Zahlen genutzt werden. Dass solche Sonderregeln bei subset() trotzdem funktionieren, ist einer mächtigen Eigenart von R zu verdanken, die unter dem Namen „Non-Standard-Evaluation“ (NSE) bekannt ist. NSE ist ein komplexes und sogar kontroverses Thema. Ohne in die Tiefe zu gehen, können wir uns NSE wie folgt klar machen: Wann immer eine Funktion ein Argument annimmt, das für sich gesehen kein legales R-Objekt ist, muss sie intern so damit umgehen, dass daraus korrekter R-Code wird. Dieses Verhalten nennt man “non-standard”; die Funktion wendet NSE an.

Dass subset() NSE anwendet, können wir daran sehen, dass die oben übergebenen Argumente für sich gesehen keine R-Objekte, sondern bloß Fehlermeldungen ausgeben:

Error in eval(expr, envir, enclos): Objekt 'Testscore' nicht gefunden
Error in eval(expr, envir, enclos): Objekt 'Item1' nicht gefunden

Weitere Ausführungen zu NSE spare ich mir an dieser Stelle; für Interessierte sei das Kapitel zu NSE in Hadley Wickhams Buch „Advanced R“ (2014) als Standardreferenz genannt; eine schnelle Internetrecherche wird ebenfalls einen Überblick zu dem Thema liefern.

Merke: Wann immer eine Funktion ein Argument annimmt, das für sich selbst genommen kein legales R-Objekt ist, wendet sie Non-Standard-Evaluation an.

3.4.5 Umgang mit NA: subset() bevorzugt zur Zeilenauswahl

Die Funktion subset() bietet uns nicht nur syntaktische Vereinfachungen zur Auswahl von Zeilen und Spalten. Darüber hinaus ist auch das Verhalten von subset() manchmal gegenüber dem [·,·]-Zugriff zu bevorzugen, zumindest was die Auswahl von Zeilen angeht. Betrachten wir im Folgenden, wie subset() und der [·,·]-Zugriff jeweils auf die Anwesenheit von fehlenden Werten (NA) reagieren:

   nummer wert
1       1    1
2       2    2
NA     NA   NA
5       5    1

Die Ausgabe des [·,·]-Zugriffs sieht etwas unschön aus; für die Zeile, in der minidf$wert den Wert NA hat, wird hier eine Zeile ausgegeben, die nur aus fehlenden Werten besteht. (Selbst die Zeilennummer wurde auf NA gesetzt!) Es ist schwer vorstellbar, dass es sich hierbei um die gewünschte Ausgabe handelt. Höchstens erinnert sie an die Anwesenheit fehlender Werte – was vielleicht manchmal nützlich sein mag.

Die Funktion subset() hingegen schließt die Zeile aus, in der minidf$wert fehlt:

  nummer wert
1      1    1
2      2    2
5      5    1

Diese Ausgabe entspricht vermutlich häufiger dem gewünschten Verhalten. Um diese Ausgabe auch mit der [·,·]-Notation zu erhalten, kann ein etwas komplizierterer, expliziter Ausschluss der NA-Werte genutzt werden:

  nummer wert
1      1    1
2      2    2
5      5    1

Es lohnt sich ein wenig über die logische Operation nachzudenken, die diesem Befehl zugrunde liegt. Man kann sie mithilfe der folgenden Wahrheitstabelle verdeutlichen:

  wert kleiner4 nichtNA kleiner4_und_nichtNA
1    1     TRUE    TRUE                 TRUE
2    2     TRUE    TRUE                 TRUE
3    4    FALSE    TRUE                FALSE
4   NA       NA   FALSE                FALSE
5    1     TRUE    TRUE                 TRUE

Die gewünschte Ausgabe wird erreicht, da die logische Operation NA & FALSE immer FALSE ergibt. Hier erinnere ich auch noch einmal an den Abschnitt zu logischen Operationen mit NA aus dem letzten Kapitel.

Es gibt jedoch eine weitere, einfachere und zu bevorzugende Variante, um mithilfe des [·,·]-Zugriffs fehlende Werte bei einer logischen Auswahl nicht zu berücksichtigen: durch Verwendung der Funktion which(). Die Funktion which() gibt für einen logischen Vektor alle Positionen aus, an denen dieser auf TRUE steht. Etwa:

[1]  TRUE  TRUE FALSE    NA  TRUE
[1] 1 2 5

Die Länge der Ausgabe von which(x) entspricht sum(x, na.rm = TRUE) für einen beliebigen logischen Vektor x.

Wir können which() also zur Datenauswahl nutzen; die Auswahl findet dann im Endeffekt nicht mit einem logischen, sondern einem numerischen Vektor statt. Die zugrundeliegende logische Abfrage wird durch which() in einen numerischen Vektor konvertiert.

  nummer wert
1      1    1
2      2    2
5      5    1

Durch Verwendung der Funktion which() werden also die Zeilen ausgelassen, in denen minidf$wert den Wert NA hat. Stattdessen beinhaltet die Ausgabe nur die Positionen, an denen die logische Abfrage minidf$wert < 4 den Wert TRUE ergibt. Die Funktion which() ist also bevorzugt zu nutzen, wenn fehlende Werte vorliegen.

3.5 Weitere Zugriffe auf data.frames

Dieser Abschnitt behandelt zwei weitere Möglichkeiten, mit eckigen Klammern auf Spalten in data.frames zuzugreifen. Da wir diese Zugriffe danach erst einmal nicht weiter verwenden, kann der folgende Inhalt jedoch zunächst problemlos übersprungen werden. Datenzugriffe mit eckigen Klammern sind jedoch ein zentraler Bestandteil von R; daher lohnt es sich, diesen Abschnitt später zu konsultieren oder zum Nachschlagen zu nutzen.

3.5.1 Der [[·]]-Zugriff

Den [[·]]-Zugriff nutzen wir genau wie den $-Zugriff zum Auslesen einzelner Spalten aus data.frames:

[1] 1 0 0 1 1

Hierbei wird der Spaltenname als ein-elementiger Vektor vom Typ character angegeben – also in Anführungszeichen. Die Anführungszeichen sind hier notwendig, bei der $-Notation verwenden wir sie hingegen nicht. Das hat zur Folge, dass wir statt der expliziten Angabe des Texts auch eine Variable übergeben können, die einen ein-elementigen character-Vektor abgespeichert hat; dies ist mit der $-Notation nicht möglich.

[1] "braun" "blau"  "blau"  "braun" "gruen"

Ebenso ist es möglich, der [[·]]-Klammerung eine Funktion zu übergeben, die einen ein-elementigen Vektor vom Typ character ausgibt – etwa die Funktion paste0():

[1] 1 0 0 1 1

Der [[·]]-Zugriff wird in Zusammenspiel mit der Funktion paste0() noch einmal interessant werden, wenn wir in Kapitel 7 mit Schleifen nacheinander auf beliebig viele Spalten von data.frames zugreifen. In einer Schleife können wir dann Spaltennamen automatisiert nacheinander austauschen (etwa: Item_1, Item_2, …).

3.5.2 Der [·]-Zugriff

Nicht äquivalent zu den Zugriffen mit $ und [[·]] ist folgender [·]-Zugriff:

  Item1
1     1
2     0
3     0
4     1
5     1

Auch hier sind Anführungszeichen zur Identifikation der auszuwählenden Spalte nötig. Der Unterschied von [·] zu [[·]] und $:

  • [[·]] und $ ergeben einen Vektor
  • [·] ergibt einen data.frame mit einer Spalte

Außerdem können wir mit dem [·]-Zugriff gleichzeitig mehrere Spalten auswählen, indem wir einen mehr-elementigen Vektor vom Typ character übergeben. Das ist mit den Zugriffen per [[·]] und $ nicht möglich, die immer nur eine Spalte ausgeben.

  Item1 Augenfarbe
1     1      braun
2     0       blau
3     0       blau
4     1      braun
5     1      gruen

Dieser Aufruf sollte uns an die [·,·]-Notation zur Auswahl von Spalten erinnern; in der Tat ist der folgende Ausdruck äquivalent:

  Item1 Augenfarbe
1     1      braun
2     0       blau
3     0       blau
4     1      braun
5     1      gruen

Zwischen dem [·]-Zugriff und der [·,·]-Auswahl für Spalten gibt es jedoch einen Unterschied, der zutage kommt, wenn wir nur eine Spalte auswählen:

  Item1
1     1
2     0
3     0
4     1
5     1
[1] 1 0 0 1 1

Wenn wir nur eine Spalte auslesen, gibt die [·,·]-Auswahl einen Vektor aus, die [·]-Auswahl jedoch einen data.frame mit einer Spalte. Dieses Verhalten offenbart einen Sonderfall der [·,·]-Auswahl: Im Normalfall gibt [·,·] ebenfalls einen ganzen data.frame aus. Wenn wir aber nur eine einzige Spalte anfordern, „reduziert“ sich die Ausgabe zu einem Vektor.

Ich persönlich bevorzuge die [·,·]-Notation gegenüber der [·]-Notation zur Auswahl von Spalten, auch wenn ich hier ein zusätzliches Komma verwenden muss (ansonsten sind die beiden Notationen ja fast äquivalent zur Auswahl von Spalten). Wenn ich Code mit der [·,·]-Notation lese, weiß ich, dass Spalten ausgewählt werden – selbst wenn ich gar nicht weiß, was in dem Objekt steckt, auf dem die Auswahl stattfindet. Die [·]-Notation ist uneindeutiger: Sie könnte auch auf einem Vektor operieren, der gar keine Spalten enthält. Wir merken uns: Code ist in erster Linie für Menschen gemacht; verständlicher Code ist gegenüber kürzerem Code zu bevorzugen.

3.5.3 Zugriff nach Name und Index

Wir haben nun alle wichtigen Möglichkeiten kennengelernt, Zugriffe auf data.frames durchzuführen. An dieser Stelle lohnt es sich deswegen, ein grundsätzliches Prinzip von Datenzugriffen in R festzuhalten: Datenzugriffe können nach nach Index oder nach Name stattfinden. Dies gilt für Vektoren, data.frames und auch für andere Datenstrukturen, die wir noch gar nicht behandelt haben.

Wir haben bereits Beispiele für beide Arten des Datenzugriffs kennengelernt: In Vektoren haben wir Zugriffe mithilfe von Indexen durchgeführt, indem wir (a) die Position von auszuwählenden Elementen mit einem numerischen Vektor angegeben haben, oder (b) indem wir einen logischen Vektor übergeben haben, der die Indexe auswählt, deren Elemente ausgegeben werden sollen. Der Vollständigkeit halber sei hier mitgeteilt, dass man sogar bei Vektoren Zugriffe nach Namen durchführen kann, wenn die Elemente des Vektors benannt sind. Das ist gar nicht so ungewöhnlich; wie folgt könnte man einen Vektor mit benannten Elementen erstellen und mit der bekannten [·]-Notation darauf zugreifen.

foo bar 
  1   2 
foo 
  1 
bar 
  2 
bar foo 
  2   1 

In data.frames haben wir Spalten zumeist nach Namen ausgewählt:

  • Mit der $-Notation
  • Mit der [·,·]-Notation
  • Mit der Funktion subset()
  • Mit der [[·]]-Notation
  • Mit der [·]-Notation

Wie wir gesehen haben, können wir mit der [·,·]-Notation in data.frames zusätzlich auch Zugriffe nach numerischem oder logischem Index durchführen. Dabei kann die Auswahl sowohl nach Spalten als auch nach Zeilen – oder beidem – geschehen.

3.6 Nützliche Funktionen zum Arbeiten mit data.frames

3.6.1 Gruppierte Statistiken: tapply()

Die Funktion tapply() kann ich verwenden, um mir deskriptive Statistiken anhand von Gruppierungsvariablen ausgeben zu lassen, hier etwa die mittlere Punktzahl oder das mittlere Alter nach Geschlecht der Schüler/innen:

  m   w 
1.0 1.5 
   m    w 
14.0 12.5 

Die Funktion tapply() erhält als erstes Argument den Messwertvektor, für den Statistiken angefordert werden. Das zweite Argument ist die Gruppierungsvariable.22 Interessanterweise ist das dritte Argument eine Funktion, in diesem Fall die Funktion mean(). So können wir die mittlere Punktzahl nach Geschlecht anfordern. Entsprechend könnten wir hier andere Funktionen übergeben, um etwa die Standardabweichung des Alters zu erfragen:

        m         w 
1.0000000 0.7071068 

Wie table() kann auch tapply() deskriptive Statistiken anhand mehrerer Gruppierungsvariablen anfordern. Um mehrere Gruppierungsvariablen zu nutzen, lesen wir diese als data.frame aus:

          Augenfarbe
Geschlecht blau braun gruen
         m 13.5    NA    15
         w   NA  12.5    NA

Mit nur fünf Datenpunkten macht diese Anfrage hier nur wenig Sinn, da jeder ausgegebene Mittelwert nur anhand eines einzelnen Wertes gebildet wurde,23 was die Idee des Mittelwerts eher ad absurdum führt. Manche Kombinationen von Geschlecht und Augenfarbe kommen in unseren Daten sogar gar nicht vor; in diesen Fällen wird NA ausgegeben. Die Funktion tapply() zeigt ihre Stärke vor allem, wenn man viele – und nicht nur fünf – Datenpunkte hat. Das gilt gerade dann, wenn wir mehrere Gruppierungsvariablen angeben.

3.6.2 Datenstruktur: nrow() und ncol()

Wie viele Zeilen ein data.frame hat – d.h. oftmals die Zahl der Fälle – lässt sich mit der Funktion nrow() bestimmen:

[1] 5

Analog gibt ncol() die Zahl der Spalten aus:

[1] 7

3.6.3 Wie sieht die Tabelle aus: head() und tail()

Um sich einen Überblick über einen data.frame zu verschaffen, sind die Funktionen head() und tail() sehr nützlich. Die Funktion head() gibt die ersten Zeilen eines data.frames aus; tail() gibt entsprechend die letzten Zeilen aus. Beide Funktionen haben ein zweites Argument n, mit dem wir spezifizieren können, wie viele Zeilen ausgegeben werden sollen. Wenn wir n nicht angeben, werden sechs Zeilen ausgegeben (in R-Jargon: 6 ist der Standardwert des optionalen Arguments n). Wir können die Funktionen wie folgt nutzen:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
2          2     0     1    14          m       blau         1
  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
3          3     0     0    13          m       blau         0
4          4     1     0    12          w      braun         1
5          5     1     1    15          m      gruen         2

3.6.4 Zusammenfassung aller Spalten: Hilfe aus Zusatzpaketen

Es gibt zahlreiche Funktionen, aus verschiedenen R-Zusatzpaketen und auch der Basisversion von R („Base-R“) selbst, die alle dafür gedacht sind, möglichst kompakt einen Überblick über einen ganzen data.frame zu bieten. Solche Funktionen geben normalerweise pro Spalte deskriptive Statistiken aus – etwa Mittelwert, Standardabweichung, Perzentile, Minimum, Maximum, oder eine Häufigkeitsverteilung bei kategorialen Faktoren – und wie viele Werte darin fehlen, also NA sind. Bei der Arbeit mit einem neuen Datensatz ist es nützlich auf diese Weise erst einmal einen groben Überblick zu erhalten.

Zu nennen sind etwa folgende Funktionen:

  • Die Funktion skim() aus dem Paket skimr (Waring et al., 2020)
  • Die Funktion describe() aus dem Paket psych (Revelle, 2019)
  • Die Funktion describe() aus dem Paket Hmisc (Harrell, 2020)
  • Die Funktion summary(), schon ohne Zusatzpaket in Base-R enthalten

Zusatzpakete stellen zusätzliche Funktionen zur Verfügung, die in der Basisversion von R nicht enthalten sind. Um die Funktionen auszuprobieren, können die nötigen Zusatzpakete mit der Funktion install.packages() installiert und der Funktion library() geladen werden:

Beachtet an dieser Stelle, dass Zusatzfunktionen aus Paketen erst dann genutzt werden können, wenn diese per library() geladen wurden; es reicht nicht aus, wenn ein Paket nur per install.packages() installiert wurde. Insbesondere heißt das, dass benötigte R-Zusatzpakete in jeder neuen R-Sitzung mit library() neu geladen werden müssen. Eine (fast) äquivalente Alternative zu library() stellt die Funktion require() dar.

Interessanterweise bemerken wir an dieser Stelle ein mögliches Problem, das sich bei der Arbeit mit R-Paketen ergeben kann: Sowohl im Paket psych[^psychwichtig] als auch im Paket Hmisc gibt es eine Funktion mit dem Namen describe(). Wenn ich per library() beide Pakete geladen habe – welche Funktion wird dann genutzt, wenn ich describe() aufrufe? Das ist von vornherein leider nicht zu sagen und hängt von der Reihenfolge ab, in der die Pakete geladen wurden. Wenn ich gezielt genau eine der beiden Funktionen ansteuern will, kann ich den doppelten Doppelpunktoperator nutzen, um das zugehörige Paket anzugeben:

Die Variante mit doppeltem Doppelpunkt ist in jedem Fall sicher und sollte verwendet werden, wenn aus verschiedenen Paketen Funktionen mit demselben Namen in Konflikt stehen. Wer ganz sicher gehen will, kann bei der Verwendung von Funktionen aus Zusatzpaketen immer den doppelten Doppelpunktoperator nutzen, auch wenn das vermutlich ein bisschen übertrieben ist.

3.6.5 Sortieren: Die Funktion arrange() aus dem Paket dplyr

Oftmals wollen wir Datentabellen nach einer oder mehreren Variablen sortieren. Dies funktioniert am bequemsten, wenn wir das Paket dplyr laden (Wickham, François, Henry, & Müller, 2018):

Voraussetzung dafür, dass ich das Paket dplyr nutzen kann ist, dass ich das Paket auf meinem Rechner installiert habe. Falls das Paket noch nicht installiert ist – in dem Fall ergibt der Befehl library('dyplr') einen Fehler – können wir es es mit dem folgenden Befehl installieren:

Die Funktion arrange() aus dem Paket dplyr ermöglicht es uns, einen data.frame zu sortieren:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          3     0     0    13          m       blau         0
2          2     0     1    14          m       blau         1
3          4     1     0    12          w      braun         1
4          1     1     1    13          w      braun         2
5          5     1     1    15          m      gruen         2

In der Funktion arrange() geben wir als erstes Argument den zu sortierenden data.frame an. Darauf folgen – mit Komma separiert – alle Spalten nach denen wir sortieren wollen (hier erst mal nur der Testscore). Standardmäßig sortiert arrange() aufsteigend; wenn wir eine absteigende Sortierung wünschen, müssen wir ein Minus vor die Sortierspalte setzen:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          1     1     1    13          w      braun         2
2          5     1     1    15          m      gruen         2
3          2     0     1    14          m       blau         1
4          4     1     0    12          w      braun         1
5          3     0     0    13          m       blau         0

Es ist auch möglich, nach mehreren Spalten zu sortieren. In dem Fall wird bei gleichen Werten im ersten Sortierkriterium anhand des nächsten Kriteriums die Reihenfolge entschieden. Wir könnten etwa unsere Daten nach Geschlecht sortieren, und innerhalb der Personen gleichen Geschlechts nach Punktzahl:

  Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1          5     1     1    15          m      gruen         2
2          2     0     1    14          m       blau         1
3          3     0     0    13          m       blau         0
4          1     1     1    13          w      braun         2
5          4     1     0    12          w      braun         1

Beachtet, dass die Funktion arrange() eine Ähnlichkeit zur Funktion subset() aufweist und ebenfalls auf Non-Standard-Evaluation vertraut: Die Spaltennamen können adressiert werden, ohne dass sie explizit aus dem zugehörigen data.frame ausgelesen werden. Das Paket dplyr gehört zu einer umfangreichen Sammlung von Paketen, dem Tidyverse (Wickham et al., 2019), das stark von Non-Standard-Evaluation Gebrauch macht. Die Pakete aus dem Tidyverse folgen einem zugrundeliegenden Entwurfsprinzip und sollen viele Aufgaben der statistischen Datenanalyse vereinfachen.

3.7 Zusammenfassung

  • Wir haben den data.frame als Datenstruktur zur Speicherung von psychometrischen Daten kennengelernt
  • Wir haben die $-Notation für den Zugriff auf einzelne Spalten von data.frames kennengelernt
  • Mit der [·,·]-Notation und der Funktion subset() können wir Zeilen und Spalten aus data.frames auslesen
  • Zur Anforderung von deskriptiven Statistiken können wir die Funktionen table() und tapply() verwenden
  • Wir haben weitere Funktionen kennengelernt, die uns einen Überblick über data.frames verschaffen:
    • names()
    • nrow()/ncol()
    • head()/tail()
    • dplyr::arrange()

3.8 Fragen zum vertiefenden Verständnis

  1. Worin unterscheiden sich die folgenden Aufrufe? Welche Aufrufe sind zueinander äquivalent?
  1. Vergleicht die folgenden Aufrufe der Funktion subset. Warum funktionieren der erste und der zweite Aufruf, aber nicht der dritte und vierte? Wie kann es überhaupt sein, dass die ersten beiden Funktionsaufrufe funktionieren, obwohl Argumente unbenannt an der “falschen” Position stehen?

  1. In der Praxis werden wir selten händisch einen ganzen data.frame aufschreiben, sondern stattdessen Daten aus einer externen Datei einlesen. Beispielsweise können die Daten in einem Spreadsheet-Editor wie Excel eingegeben worden sein und wir importieren diese dann in R.

  2. Technisch korrekt müsste ich sagen: Der data.frame, der in der Variablen mdf abgespeichert ist, hat nun eine zusätzliche Spalte.

  3. Es ist etwas unglücklich, dass der Begriff “Variable” doppeldeutig ist: (1) In R sind Variablen die Speicherorte von Objekten; ich erstelle sie mit der “<-”-Zuweisung. (2) Andererseits werden auch Messwerte – etwa die Punktzahlen in einem Testitem – als Variablen bezeichnet. In diesem Sinne würde der Begriff Variable in R auf die Spalte in einem data.frame verweisen, da Spalten Messvariablen beinhalten. Diese Doppeldeutigkeit ist deswegen unglücklich, da eine Spalte in einem data.frame keine R-Variable ist. Stattdessen ist der gesamte data.frame in einer Variablen abgespeichert.

  4. Auch Zeilen können benannt sein. Den Fall hatten wir bislang aber nicht und es kommt auch nicht oft vor, dass Zeilen explizit benannt sind. Häufiger ist der Fall, in dem wir Spalten nach Namen auswählen.

  5. Bei der Auswahl von Items aus Subskalen ist dieses Vorgehen oftmals etwas schwieriger, da hier nur Teilmengen des gesamten Itempools ausgewählt werden sollen. Aber auch dann kann die Funktion paste0() helfen.

  6. Es macht an dieser Stelle Sinn, einen Moment inne zu halten und zu überlegen, warum es eigentlich außergewöhnlich ist, dass der Befehl Augenfarbe == "blau" innerhalb der Funktion subset() funktioniert.

  7. Unter welchen Umständen würde R keinen Fehler ausgeben, wenn wir c(Augenfarbe, Alter) in die Konsole eingeben? Es ist nützlich, diesen Punkt zu verstehen.

  8. Beachtet, dass sowohl Messwerte als auch Gruppierungsvariable als Vektoren übergeben werden. Ich behandle die Funktion tapply() jedoch im Kapitel zu data.frames, da es zumeist so sein wird, dass wir beide Vektoren aus einem data.frame mit der $-Notation auslesen werden.

  9. Wie viele Datenpunkte in die Berechnung jedes Mittelwerts eingehen, können wir in diesem Fall prüfen mit table(mdf$Geschlecht, mdf$Augenfarbe).