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:

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:

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.

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:

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:

[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:

  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:

[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.


  1. Wer nach dem Durcharbeiten des Kapitels noch nicht genug von Schleifen hat, kann sich mithilfe einer Google-Suche mit der while-Schleife vertraut machen.

  2. Tatsächlich ist der Vektor nicht wirklich leer. Schaut ihn euch einmal nach der Erstellung an (d.h., gebt ihn in der Konsole aus).

  3. 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 Funktion list(), die eine Liste erstellt, oder die Funktion matrix(), die eine Matrix erstellt. Diese Funktionen sind oft nützlich, um leere Datencontainer zu erstellen, die im Verlaufe einer for-Schleife gefüllt werden.