7 Schleifen
Schleifen spielen in allen Programmiersprachen eine wichtige Rolle. Sie
erlauben uns, eine Aufgabe mehrfach durchzuführen, ohne dass wir den
Code für die Aufgabe mehrfach schreiben müssen. Beispielsweise müssen
wir Umkodierungen von Items (vgl. Kapitel 5) nicht für
jedes einzelne Item neu eingeben – d.h. fehleranfällig copy-pasten –,
sondern können sie mithilfe einer Schleife für alle Items auf einmal
durchführen. Wie auch eigene Funktionen helfen uns Schleifen bei der
Automatisierung unserer Arbeit. Sie helfen uns, R
als
Programmiersprache zu nutzen.
Im Allgemeinen und in R
im Speziellen gibt es mehrere schleifenartige
Gebilde; in diesem Kapitel lernen wir die wichtige for
-Schleife
kennen.45 Das logische Prinzip einer for
-Schleifen ist
recht simpel: Sie führt einen Code-Block mehrfach durch. In der Regel
wird in den verschiedenen Durchläufen der Schleife variiert, auf welche
Daten – also etwa auf welche Items eines Tests – zugegriffen wird,
damit nicht in jedem Durchgang einfach dasselbe passiert. Dies ist die
Syntax einer for
-Schleife:
Das sieht erst einmal etwas beunruhigend aus. Gehen wir die einzelnen
Bestandteile der Schleife einmal durch und schauen uns danach eine
for
-Schleife in Aktion an.
Den Anfang der Schleife definieren wir mit dem Schlagwort for
. Die
eigentliche Musik spielt in der darauf folgenden Klammer
(Schleifenvariable in vector)
. Dabei ist vector
ein beliebiger
R
-Vektor. Von der Länge dieses Vektors hängt ab, wie oft der Code im
Körper der Schleife – eingeschlossen in den geschwungenen Klammern
{·}
– ausgeführt wird. Wir könnten der for
-Schleife beispielsweise
einen der folgenden Vektoren übergeben:
c(83, 45, 12, -99) # Die Schleife würde 4x laufen
c("Cronbach", "Spearman", "Brown") # 3x
1:10 # 10x
paste0("item", 1:50) # 50x
Wäre vector
einer dieser vier Vektoren, würde die Schleife viermal,
dreimal, zehnmal, oder 50 Mal durchgeführt werden. Um zu verstehen,
warum es überhaupt Sinn macht, denselben Code-Block mehrfach
durchzuführen, betrachten wir zusätzlich den Ausdruck
Schleifenvariable
, der die Magie der for
-Schleife offenbart: Der
Schleifenvariable
wird in jedem Schleifendurchlauf schrittweise das
nächste Element von vector
zugeordnet. Auf die Schleifenvariable
können wir also im Körper der Schleife zugreifen und ihr Inhalt ändert
sich in jedem Durchlauf der Schleife. Betrachten wir folgendes
Spielzeug-Beispiel, das das Konzept der for
-Schleife verdeutlicht:
[1] "Cronbach"
[1] "Spearman"
[1] "Brown"
Die Funktion print()
ist die explizite Anweisung, ein R
-Objekt in der
Konsole auszugeben. Wir sehen, dass uns die Schleife in ihren drei
Durchläufen drei verschiedene Texte ausgibt, nämlich nacheinander den
Inhalt des Vektors c('Cronbach', 'Spearman', 'Brown')
. Da print()
auf
die Variable name
angewendet wurde, sehen wir, dass name
ihren
Inhalt in jedem Durchlauf der Schleife geändert hat.
Wir stellen fest, dass for
-Schleifen folgende Eigenschaften haben:
- Sie führen einen Code-Block genauso oft aus, wie ein übergebener Vektor lang ist.
- Eine Schleifenvariable nimmt in jedem Durchlauf den nächsten Wert des übergebenen Vektors an.
- Im Körper der Schleife können wir auf die Schleifenvariable zugreifen, um in jedem Durchlauf andere Berechnungen durchzuführen.
Das ist tatsächlich schon alles! Im Folgenden lernen wir zwei konkrete
Anwendungen von for
-Schleifen kennen.
7.1 Sequentielle Bepunktung von Testitems
Ein Hinweis zu Beginn: Dieses Kapitel nutzt den [[·]]
-Zugriff auf
Spalten in data.frames
. Wer damit noch nicht vertraut ist, sollte
sich vor dem Weiterlesen zunächst den kurzen Abschnitt zum
[[·]]
-Zugriff in Kapitel 3 ansehen.
In Kapitel 5 haben wir gelernt, wie wir mithilfe eines Schlüssels Testfragen aus einem psychologischen Test bepunkten können. Im NPI hatten wir den Fall, dass jedes Item aus einer narzisstischen und einer nicht-narzisstischen Aussage bestand; wir haben für ein Item genau dann einen Punkt vergeben, wenn die narzisstische Aussage gewählt wurde.
Wir wollen im Folgenden diese Bepunktung mithilfe einer for
-Schleife
automatisieren, das heißt auf einmal für alle 40 Items des NPI
durchführen. Dafür benötige ich für jedes Item des NPI den Schlüssel,
den ich dem Codebuch entnehmen kann. Wir übertragen die 40 Schlüssel
zunächst manuell in einen Vektor:
## Schlüssel aller 40 Items in einen Vektor
keys <- c(1, 1, 1, 2, 2, 1, 2, 1, 2, 2, 1, 1, 1, 1, 2, 1, 2, 2, 2, 2,
1, 2, 2, 1, 1, 2, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 2)
Als Nächstes führe ich mit einer for
-Schleife die Bepunktung aller
Items durch. In diesem Code nehme ich an, dass die NPI-Antwortdaten
schon eingelesen wurden und die Datenbereinigung aus Kapitel 5
durchgeführt wurde, mir also ein data.frame
mit Namen npi_clean
vorliegt, der alle Fälle ohne fehlende Antworten enthält.
## Die Variable `npi_clean` enthält die Antworten für das NPI, siehe
## Kapitel 5
# for-Schleife für die Kodierung:
for (i in 1:40) {
# 1. Wähle Spaltenname des i-ten Items aus:
colname <- paste0("Q", i)
# 2. Wähle aus Spalte die Antworten aus:
ith_item <- npi_clean[[colname]]
# 3. Führe Umkodierung durch:
narcissistic_response <- ifelse(ith_item == keys[i], 1, 0)
# 4. Erstelle Namen für neue Spalte:
new_colname <- paste0("coded", colname)
# 5. Hänge kodierte Werte an data.frame an:
npi_clean[[new_colname]] <- narcissistic_response
}
Der erste Befehl im Körper der Schleife generiert mit der Funktion
paste0()
den Namen der Spalte, der adressiert werden soll. Im ersten
Durchgang der for
-Schleife wird also die Spalte Q1
adressiert, da
die Schleifenvariable i
den ersten Wert des Vektors 1:40
angenommen
hat. Dann folgen Q2
, Q3
und so weiter. Als Namen von
Schleifenvariablen werden häufig kurze Namen wie i
oder j
verwendet,
insbesondere wenn es sich um Indexvariablen handelt, sie also eine
numerische Sequenz der Form 1:n
durchlaufen.
Der zweite Befehl wählt die zuvor definierte Spalte von npi_clean
als
Vektor aus. Ich benutze dabei die [[·]]
-Notation, da sie mir erlaubt,
eine Variable vom Typ character
in den Klammern zu übergeben. Der
folgende Aufruf mit der $
-Notation würde nicht funktionieren:
npi_clean$colname
– hierbei würde R
nach einer Spalte mit dem Namen
colname
suchen, aber wir wollen hier ja stattdessen eigentlich den in
der Variable colname
enthaltenen Spaltennamen (etwa Q23
) verwenden.
Der dritte Befehl führt mit einem Aufruf der Funktion ifelse()
die
eigentliche Umkodierung durch. Es wird kodiert, ob Probanden beim i-ten
Item die narzisstische Aussage ausgewählt haben. Eine 1 wird vergeben,
wenn das der Fall war, andernfalls eine Null. Ich speichere diesen
numerischen Vektor aus Einsen und Nullen in der Variablen
narcissistic_response
zwischen. Beachtet, dass diese Variable in jedem
Durchlauf der Schleife überschrieben wird (dasselbe gilt für die
Variablen colname
, ith_item
und new_colname
) .
Der vierte Befehl generiert mit einem Aufruf der Funktion paste0()
in
jedem Durchlauf einen neuen Spaltennamen. Die neuen Spalten haben Namen
der Form codedQ1
, codedQ2
und so weiter.
Der fünfte Befehl fügt mit der [[·]]
-Notation die umkodierten
Narzissmus-Werte als Spalte an npi_clean
hinzu. So stelle ich sicher,
dass ich auch später darauf zugreifen kann, etwa um Summenwerte über
alle Items oder Item-Trennschärfen zu bestimmen.
Beachtet, dass ich die for
-Schleife auch mit weniger
Zwischenschritten hätte umsetzen können; folgender Code würde in
weniger Zeilen dasselbe Ergebnis erzielen:
for (i in 1:40) {
colname <- paste0("Q", i)
npi_clean[[paste0("coded", colname)]] <-
ifelse(npi_clean[[colname]] == keys[i], 1, 0)
}
Im ersten Beispiel habe ich jedoch jeden Zwischenschritt in einer eigenen Variablen abgespeichert, um den Code besser verständlich zu machen und alle Schritte zu erklären. Aus meiner Sicht ist das Zwischenspeichern in Variablen gut geeignet, um zu kommunizieren, was Code macht – insbesondere, wenn die Variablen gut benannt sind.
7.2 Berechnung von part-whole korrigierten Trennschärfen
Nachdem ich mit der letzten for
-Schleife die rohen Antwortdaten in die
angemessene Form umkodiert habe, kann ich mit meiner Analyse starten.
Ein wichtiger Teil einer Item-Analyse ist die Berechnung von
Item-Trennschärfen. Dieser Abschnitt behandelt die
Frage, wie wir mithilfe einer for
-Schleife korrigierte
Item-Trennschärfen für alle 40 Items des NPI berechnen können. Wir
nutzen diesen Code:
## Wir nutzen die umkodierten NPI-Werte: speichere diese
## zunächst in einem separaten data.frame ab:
columns <- paste0("codedQ", 1:40)
items <- npi_clean[, columns]
## Berechne die Trennschärfen in Schleife
for (column in columns) {
# 1. Summenwert unter Ausschluss eines Items
scores <- rowSums(items[, column != colnames(items)])
# 2. Korreliere damit den Item-Score
part_whole <- cor(items[[column]], scores)
# 3. Gib die Trennschärfe aus
print(part_whole)
}
[1] 0.3558674
[1] 0.4201535
[1] 0.3321716
...
Mithilfe der Funktion print()
gebe ich nacheinander die 40
Trennschärfen aus, die ich in den Durchläufen der Schleife berechne; um
die Seite nicht mit einer ausufernden Liste an Trennschärfen zu fluten,
habe ich hier aber nur drei davon angezeigt. Um ein Objekt während des
Laufs einer Schleife in der Konsole auszugeben, muss man print()
explizit auf das auszugebende Objekt aufrufen; das Objekt ohne print()
anzusteuern würde in keiner Reaktion resultieren.
Nun aber zur Logik des Codes: Vor dem Durchlauf der Schleife wähle ich
genau die Spalten aus npi_clean
aus, die die zuvor erstellten
Item-Scores enthalten, und speichere sie in der Variable items
ab.
Danach startet die Schleife. In diesem Beispiel habe ich die
Schleifenvariable column
genannt. Das war recht willkürlich – ich
kann der Schleifenvariable jeden Namen geben, den ich möchte. Hier habe
ich mich anders als im vorherigen Beispiel nicht für den Namen i
entschieden, da die Schleifenvariable keine Indexvariable ist und keine
Sequenz von ganzen Zahlen der Form 1:n
durchläuft. Stattdessen
durchläuft sie einen Vektor, der die Spaltennamen enthält, auf die ich
zugreifen möchte. Deswegen erschien mir der Variablenname column
passend.
Der erste Befehl im Schleifenkörper berechnet einen Summenwert über 39
Items. Dabei wird jeweils das Item nicht berücksichtigt, das in der
Spalte name
des von items
abgespeichert ist. Betrachtet den Code
genau: Mithilfe der [·,·]
-Notation werden genau die 39 anderen Spalten
ausgewählt. Dafür wird hinter dem Komma der [·,·]
-Notation ein
logischer Vektor der Länge 40 übergeben, der nur an einer Stelle FALSE
enthält und sonst TRUE
. Dieser logische Vektor wurde mit dem Befehl
column != colnames(items)
erstellt.
Der zweite Befehl im Schleifenkörper berechnet die korrigierte
Trennschärfe. Hier wird der Summenscore über 39 Items mit dem
verbleibenden Item korreliert und der resultierende
Korrelationskoeffizient wird in der Variablen part_whole
abgespeichert. Der dritte Befehl gibt lediglich die Trennschärfe in der
Konsole aus.
7.3 Datenspeicherung in einer Schleife
Im vorherigen Beispiel habe ich Item-Trennschärfen berechnet und dann
mit dem print()
-Befehl in der Konsole ausgegeben. Oftmals möchte ich die
Ergebnisse von Berechnungen, die während einer Schleife anfallen, aber
nicht nur ausgeben, sondern auch abspeichern. Im ersten
Anwendungsbeispiel einer for
-Schleife – Umkodierung von Items –
hatten wir uns zunutze gemacht, dass man mit der [[·]]
-Notation neue
Spalten an an data.frames
anhängen kann. Es macht jedoch keinen Sinn,
die 40 Trennschärfen an den data.frame
mit 10418
anzuhängen. Stattdessen könnte ich einen Vektor mit 40 Elementen zu
erstellen, in dem ich die Trennschärfen speichere; ich berechne ja in
jedem Durchlauf der Schleife genau einen Wert. Im Folgenden sehen wir
uns an, wie wir das machen können. Dabei betrachten wir zwei Fälle:
Einmal adressieren wir die Elemente des Vektors, der die Trennschärfen
beinhaltet per Name und einmal per Index (siehe Kapitel
3.5).
7.3.1 Adressierung per Name
Zunächst erstelle ich wie folgt einen leeren46 Vektor der Länge 40:
Mithilfe der Funktion vector()
47 erstelle ich einen
Vektor; das Argument length
bestimmt dabei die Länge des Vektors.
Darin möchte ich im Verlauf der 40 Durchläufe der Schleife die
Trennschärfen der 40 Items abspeichern. Um eine Adressierung per Name zu
ermöglichen, gebe ich den Elementen des Vektors wie folgt Namen:
codedQ1 codedQ2 codedQ3
FALSE FALSE FALSE
So haben die Elemente meines leeren Vektors dieselben Namen wie die
Spalten des data.frames
items
, den ich zur Berechnung der
Trennschärfen verwendet habe. Dass ich ausgerechnet diese Namen vergeben
habe, hat zur Folge, dass ich recht einfach den obigen Code zur
Berechnung der Trennschärfen umwandeln kann, um die Trennschärfen auch
noch abzuspeichern. Dies ist der leicht angepasste Code:
## Wähle Items aus:
columns <- paste0("codedQ", 1:40)
items <- npi_clean[, columns]
## Erstelle leeren Vektor-Container und benenne ihn:
discriminations <- vector(length = 40)
names(discriminations) <- columns
for (column in columns) {
# 1. Summenwert unter Ausschluss eines Items
scores <- rowSums(items[, column != colnames(items)])
# 2. Korreliere damit den Item-Score
part_whole <- cor(items[[column]], scores)
# 3. Speichere Trennschärfe ab
discriminations[column] <- part_whole
}
## Voilá:
head(discriminations)
codedQ1 codedQ2 codedQ3 codedQ4 codedQ5 codedQ6
0.3561849 0.4202523 0.3322548 0.5014695 0.4411108 0.4795097
7.3.2 Vektorspeicherung – Adressierung per Index
Oftmals wird die Schleifenvariable als Indexvariable verwendet, d.h.,
sie durchläuft einen ganzzahligen numerischen Vektor, zumeist der Form
1:n
. So war es beispielsweise der Fall, als ich die 40 Items des NPI
umkodiert habe. Diese Verwendung der Schleifenvariable ist oft dann
nützlich, wenn ich in jedem Durchlauf der Schleife auf verschiedene
Datenstrukturen zugreifen möchte – etwa auf einen data.frame
, der
Antworten enthält, und einen Vektor, der Schlüssel enthält. Da dieser
Spezialfall wichtig ist, zeige ich auch für die Berechnung der
Item-Trennschärfen, wie man die for
-Schleife mit einer
Index-Schleifenvariablen umsetzen kann:
## Wähle Items aus:
columns <- paste0("codedQ", 1:40)
items <- npi_clean[, columns]
## Erstelle leeren Vektor-Container:
discriminations <- vector(length = 40)
## Erstelle Index-Vektor:
indices <- 1:40
## Berechne Trennschärfen in Schleife
for (i in indices) {
# 1. Summenwert unter Ausschluss eines Items
scores <- rowSums(items[, indices[-i]])
# 2. Korreliere damit den Item-Score
part_whole <- cor(items[[i]], scores)
# 3. Speichere Trennschärfe ab
discriminations[i] <- part_whole
}
## Voilá:
head(discriminations)
[1] 0.3561849 0.4202523 0.3322548 0.5014695 0.4411108 0.4795097
Hier wird die Index-Variable i
gleich mehrfach verwendet: (1) zur
Auswahl der Spalten, die den jeweiligen Testwert berechnen; (2) zur
Auswahl der Spalte des Items, für das die Trennschärfe bestimmt wird;
(3) zum Abspeichern der Trennschärfe im Vektor discriminations
.
Wer nach dem Durcharbeiten des Kapitels noch nicht genug von Schleifen hat, kann sich mithilfe einer Google-Suche mit der
while
-Schleife vertraut machen.↩Tatsächlich ist der Vektor nicht wirklich leer. Schaut ihn euch einmal nach der Erstellung an (d.h., gebt ihn in der Konsole aus).↩
Es ist allgemein so, dass Funktionen mit dem Namen einer Datenstruktur besagte Datenstruktur erstellen. Erinnern wir uns an die Funktion
data.frame()
. Ebenso gibt es die Funktionlist()
, die eine Liste erstellt, oder die Funktionmatrix()
, die eine Matrix erstellt. Diese Funktionen sind oft nützlich, um leere Datencontainer zu erstellen, die im Verlaufe einerfor
-Schleife gefüllt werden.↩