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.
mdf <- data.frame(
Fallnummer = 1:5,
Item1 = c(1, 0, 0, 1, 1),
Item2 = c(1, 1, 0, 0, 1),
Alter = c(13, 14, 13, 12, 15),
Geschlecht = c("w", "m", "m", "w", "m")
)
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 mdf
16 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
## Wähle per logischem Vektor alle Personen aus, die beide Aufgaben
## richtig gelöst haben:
mdf[mdf$Testscore == 2, ]
Fallnummer Item1 Item2 Alter Geschlecht Augenfarbe Testscore
1 1 1 1 13 w braun 2
5 5 1 1 15 m gruen 2
## Wähle Fallnummer, Alter und Testscore per Spaltenname aus:
mdf[, c("Fallnummer", "Alter", "Testscore")]
Fallnummer Alter Testscore
1 1 13 2
2 2 14 1
3 3 13 0
4 4 12 1
5 5 15 2
## Wähle Fallnummer, Alter und Testscore aus für alle Personen, die
## älter als 13 sind
mdf[mdf$Alter > 13, c("Fallnummer", "Alter", "Testscore")]
Fallnummer Alter Testscore
2 2 14 1
5 5 15 2
## Wähle Fallnummer, Alter und Testscore aus für die ersten drei Fälle
mdf[1:3, c("Fallnummer", "Alter", "Testscore")]
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:
- Die Auswahl von Zeilen mithilfe logischer Bedingungen
- 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.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:
data.frame(
wert = minidf$wert,
kleiner4 = minidf$wert < 4,
nichtNA = !is.na(minidf$wert),
kleiner4_und_nichtNA = minidf$wert < 4 & !is.na(minidf$wert)
)
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 einendata.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.
## Benannte Vektoren erstellen funktioniert wie einen data.frame zu
## erstellen:
vec <- c(foo = 1, bar = 2)
vec
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 Paketskimr
(Waring et al., 2020) - Die Funktion
describe()
aus dem Paketpsych
(Revelle, 2019) - Die Funktion
describe()
aus dem PaketHmisc
(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:
# Man kann mit einem Befehl mehrere Pakete installieren:
install.packages(c("skimr", "psych", "Hmisc"))
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 vondata.frames
kennengelernt - Mit der
[·,·]
-Notation und der Funktionsubset()
können wir Zeilen und Spalten ausdata.frames
auslesen - Zur Anforderung von deskriptiven Statistiken können wir die Funktionen
table()
undtapply()
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
- Worin unterscheiden sich die folgenden Aufrufe? Welche Aufrufe sind zueinander äquivalent?
- 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?
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 inR
.↩Technisch korrekt müsste ich sagen: Der
data.frame
, der in der Variablenmdf
abgespeichert ist, hat nun eine zusätzliche Spalte.↩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 inR
auf die Spalte in einemdata.frame
verweisen, da Spalten Messvariablen beinhalten. Diese Doppeldeutigkeit ist deswegen unglücklich, da eine Spalte in einemdata.frame
keineR
-Variable ist. Stattdessen ist der gesamtedata.frame
in einer Variablen abgespeichert.↩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.↩
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.↩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 Funktionsubset()
funktioniert.↩Unter welchen Umständen würde
R
keinen Fehler ausgeben, wenn wirc(Augenfarbe, Alter)
in die Konsole eingeben? Es ist nützlich, diesen Punkt zu verstehen.↩Beachtet, dass sowohl Messwerte als auch Gruppierungsvariable als Vektoren übergeben werden. Ich behandle die Funktion
tapply()
jedoch im Kapitel zudata.frames
, da es zumeist so sein wird, dass wir beide Vektoren aus einemdata.frame
mit der$
-Notation auslesen werden.↩Wie viele Datenpunkte in die Berechnung jedes Mittelwerts eingehen, können wir in diesem Fall prüfen mit
table(mdf$Geschlecht, mdf$Augenfarbe)
.↩