18  Kontrollstrukturen

Perse wird imperativer Programmiercode streng sequentiell Befehl für Befehl ausgeführt. Manchmal möchte man von dieser rein sequentiellen Befehlsreihenfolge allerdings abweichen und die Ausführungsreihenfolge von Befehlen flexibler kontrollieren. Prinzipielle Werkzeuge zu diesem Zweck sind konditionale Kontrollstrukturen und iterative Kontrollstrukturen. Konditionale Kontrollstrukturen steuern die Programmausführung, indem sie festlegen, dass bestimmte Befehle nur dann ausgeführt werden, wenn definierte Bedingungen erfüllt sind. R bietet zu diesem Zweck if() und switch() Statement. Oft möchte man bestimmte Codeabschnitte auch in Schleifen häufig wiederholen (iterieren). Zu diesem Zweck bietet R for(), while(), und repeat() Schleifen. In diesem Abschnitt stellen wir die grundlegenden konditionalen und iterativen Kontrollstrukturen in R vor und folgen dabei der Darstellung in Cotton (2013), Kapitel 8 und Kapitel 9, sowie Wickham (2019), Kapitel 5.

18.1 Konditionale Kontrollstrukturen

Konditionale Kontrollstrutkuren erlauben die bedingungsabhängige Ausführung von Befehlen.

if-statements

Die grundlegende konditionale Kontrollstruktur ist das if-statement. Die allgemeine Syntax für ein if-statement in R hat folgende Form:

if (Bedingung){
  TrueActions
}

Dabei gilt, dass wenn die Variable Bedingung den Wert TRUE hat, also wahr ist, die mit TrueActions bezeichneten Befehle ausgeführt werden. Ist der Wert der Variable Bedingung dagegen FALSE, so werden die Befehle TrueActions nicht ausgeführt und die Programmausführung wird in der nächsten Zeile nach dem if-statement fortgesetzt. Beispielsweise wird in folgendem Code der Befehl print("x ist größer als 0") ausgeführt, da x ja nach vorheriger Zuordnung den Wert 1 hat.

x = 1
if(x > 0){
  print("x ist größer als 0")
}
[1] "x ist größer als 0"

Das if-statement kann durch eine else Bedingung erweitert werden, um zu spezifieren, welche Befehle in dem Fall ausgeführt werden sollen, dass die Variable Bedingung den Wert FALSE hat.

if (Bedingung){
  TrueActions
} else {
  FalseActions
}

Hier werden die Befehle TrueActions ausgeführt, wenn die Variable Bedingung den Wert TRUE hat und es werden die Befehle FalseActions ausgeführt, wenn die Variable Bedingung den Wert FALSE hat. Beispielsweise wird in folgendem Code der Befehl print("y ist nicht größer als 0") ausgeführt, da die Bedingung y > 0 nicht zutrifft.

y = -1
if(y > 0){
  print("y ist größer als 0")
} else{
  print("y ist nicht größer als 0")
}
[1] "y ist nicht größer als 0"

R umfasst die in Tabelle 18.1 aufgeführten Relationsoperatoren zur Prüfung binärer logischer Bedingungen.

Tabelle 18.1: Relationsoperatoren in R
Relationsoperator Bedeutung
== Gleich
!= Ungleich
<, > Kleiner, Größer
<=, >= Kleiner gleich, Größer gleich
& UND
| ODER

Dabei werden die Relationsoperatoren <, <=, >, >= zumeist nur auf numerische Werte angewendet. Die Gleichheitsoperatoren ==,!= können auf beliebige Datenstrukturen angewendet werden, um deren Gleichheit zu überprüfen. Die logischen Verknüpfungen & und | werden zumeist auf logische Werte angewendet. Die Funktion xor() schließlich implementiert das exklusive ODER. Es können beispielsweise folgende Tests auf Gleicheit und Relation durchgeführt werden

# Variablendefinition
x = 1
y = 2

# Test auf Gleichheit
if(x == y){
  print("x ist gleich y")
} else {
  print("x ist ungleich y")
}
[1] "x ist ungleich y"
# Test auf Ungleichheit
if(x < y){
  print("x ist kleiner als y")
} else {
  print("x ist gfößer oder gleich y")
}
[1] "x ist kleiner als y"

Die if-statement Tests können durch logische Bedingungen erweitert werden, wie folgendes Beispiel demonstrieren soll:

# Variablendefinition
x = 2
y = 2

# logisches UND/ODER
if(x == y | x < y){
  print("x ist kleiner oder gleich y")
} else {
  print("x ist größer als y")
}
[1] "x ist kleiner oder gleich y"
# logisches UND
if(x > y & x != y){
  print("x ist größer als y")
} else {
  print("x ist kleiner oder gleich y")
}
[1] "x ist kleiner oder gleich y"

Bei if-Statements ist es entscheidend, dass die Bedingung einem einzigen logischen Wert entspricht. Folgende Bedingungen sind beispielsweise fehlerhaft:

if("x"){ 
  print(1)
}
Error in if ("x") {: argument is not interpretable as logical
if(NA){
  print(1)
}
Error in if (NA) {: missing value where TRUE/FALSE needed
if(c(T,F)){
  print(1)
}
Error in if (c(T, F)) {: the condition has length > 1

switch-statements

Eine weitere konditionale Kontrollstruktur ist das switch-statement. Switch-statements sind dadurch motiviert, dass kombinierte if-else-statements werden leicht unübersichtlich werden können, wie folgendes Beispiel zeigt:

x = 2
if (x == 1){
  print("Aktion 1")
} else if(x == 2){
  print("Aktion 2")
} else if(x == 3){
  print("Aktion 3")
} else if(x == 4){
  print("Aktion 4")
}
[1] "Aktion 2"

Hat man also einen Fall vorliegen, in dem in Abhängigkeit des Wertes einer Variable eine von vielen Aktionen ausgeführt werden soll, so kann sich ein switch-statement zur Verbesserung der Codelesbarkeit anbieten. R implementiert switch-statements in der switch() Funktion, die in Abhängigkeit vom Datentyp ihrer Variable leicht unterschiedlich funktioniert. Ist die switch Variable vom Typ Integer, so wird die \(i\)te Aktion ausgeführt, wie folgendes Beispiel demonstriert:

x = 2                                                 # switch Variable
switch(x,                                             # x = 2
print("Aktion 1"),                                    # 1. Aktion
print("Aktion 2"),                                    # 2. Aktion
print("Aktion 3"),                                    # 3. Aktion
print("Aktion 4"))                                    # 4. Aktion
[1] "Aktion 2"

Ist die switch-Variable dagegen vom Typ character, so wird die Aktion mit dem entsprechenden Namen ausgeführt:

x = "a"                                         # switch Variable
switch(x,                                       # x = 2
a = print("Aktion a"),                          # a Aktion
b = print("Aktion b"),                          # b Aktion
c = print("Aktion c"),                          # c Aktion
d = print("Aktion d"))                          # d Aktion
[1] "Aktion a"

Generell lässt sich zu if- und switch-statements festhalten, dass sich jedes if-statemen säquivalent als switch-statement formulieren lässt und andersherum. If-statements sind dabei in der Anwendung sicherlich häufiger anzutreffen als switch-statements. Generell macht es für das menschliche Codeverständnis Sinn, if-else-statements nicht zu komplex zu formulieren. Ist dies allerdings nötig, so können komplexe if-else-statements durch switch-statements vereinfacht werden.

18.2 Iterative Kontrollstrukturen

Iterative Kontrollstrukturen dienen dazu, bestimmte Befehle wiederholt auszuführen. Die grundlegende iterative Kontrollstruktur in R ist die for-Schleife. Diese hat die generelle Form

for (item in vector){ 
  perform_action
}

Dabei wird der Befehl perform_action wird für jedes item in vector einmal ausgeführt und der Wert von item jeweils aktualisiert. Folgendes Beispiel soll dies verdeutlichen:

for (i in 1:3){
  print(i)
}
[1] 1
[1] 2
[1] 3

Der Vektor besteht hier aus 1:3 = [1,2,3], der ausgeführte Befehl ist print(i) und die items sind die Einträge des Vektors. Allerdings muss der Befehl innerhalb der for-Schleife nicht zwangsläufig auf die items bezug nehmen, wie folgendes Beispiel zeigt:

for (i in 1:3){
  print('a')
}
[1] "a"
[1] "a"
[1] "a"

Hier wird dreimal der konstante Wert a ausgegeben. Typischerweise wird innerhalb einer for-Schleife etwas erzeugt und gespeichert. Dabei sollte generell die entsprechende Speicherstruktur vorallokiert werden, da dann der entsprechende Platz im Arbeitsspeicher schon bereitgestellt ist und nicht während der Ausführung der for-Schleife bereitgestellt werden muss. Der Effizienzgewinn dieser Strategie mag in den meisten Fällen vernachlässigbar sein, allerdings erlaubt dieses Vorgehen auch insbesondere, Fehler leichter zu detektieren, wenn es beispielsweise Dimensionserwatungsunterschiede oder fehlende Einträge gibt, die dann schon als NaN alloziert sind. Folgendes Beispiel demonstriert das iterative Auffüllen eines Vektors mit Mittelwerten.

ns    = 3                           # Anzahl an Simulationen
X_bar = rep(NaN, ns)                # Speicherstruktur

# Simulationsiterationen
for(i in 1:ns){                     # Iterationsindices sind typischerweise i,j,k
  X         = rnorm(12)             # Realisierung von 12 Z-Variablen
  X_bar[i]  = mean(X)               # Mittelwert der i-ten Realisierung
  print(X_bar)                      # Anzeige zur Demonstration
}
[1] -0.04737814         NaN         NaN
[1] -0.04737814  0.06236309         NaN
[1] -0.04737814  0.06236309  0.08232465

Um linear entlang eines Vektors mit variablen Einträgen zu iterieren, bietet sich die Funktion seq_along() an. Diese erzeugt die linearen Indizes 1,2, 3, … bis zur Länge des betreffenden Vektors, wie folgendes Beispiel demonstriert:

mu    = c(0,5,50)                   # Drei Erwartungswertparameter
X_bar = rep(NaN, ns)                # Speicherstruktur

# Simulationsiterationen
for(i in seq_along(mu)){            # Iterationsindices sind typischerweise i,j,k
  X         = rnorm(12, mu[i], 1)   # Realisierung von 12 N(\mu,1) Varialben
  X_bar[i]  = mean(X)               # Mittelwert der i-ten Realisierung
  print(X_bar)                      # Anzeige zur Demonstration
}
[1] -0.3160148        NaN        NaN
[1] -0.3160148  5.0948158        NaN
[1] -0.3160148  5.0948158 50.2098457

for-Schleifen können auch geschachtelt werden, also innerhalb von for-Schleifen weitere for-Schleifen ausgeführt werden. Dies mag etwas gewöhnungsbedürftig sein. Entscheidend ist dabei, dass man sich klarmacht, dass für jede Iteration der äußeren Schleife alle Iterationen der inneren Schleife durchlaufen werden. Es ist oft hilfreich, sich die generelle Logik einer geschachtelten Schleifenstruktur zunächst mithilfe von print()-statements klarzumachen, wie folgendes Beispiel demonstriert:

n_i = 2                             # Anzahl der Iterationen äußere Schleife
n_j = 3                             # Anzahl der Iterationen innere Schleife
for(i in 1:n_i){                    # Äußere Schleife
  for (j in 1:n_j){                 # Innere Schleife
    print(c(i,j))                   # Anzeigen der Iternationsindices [i,j]                     
    }
}
[1] 1 1
[1] 1 2
[1] 1 3
[1] 2 1
[1] 2 2
[1] 2 3

Folgendes Beispiel etwa erzeugt eine 4 x 5 Matrix, der Elemente den jeweiligen Spaltenindizes entsprechen. Dabei bedient die innere for-Schleife mit der Iteratorvariable j die Spaltenindizes und die äußere for-Schleife mit der Iteratorvariable i bedient die Zeilenindizes:

     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    1    2    3    4    5
[3,]    1    2    3    4    5
[4,]    1    2    3    4    5

Analog erzeugt folgendes Beispiel eine 4 x 5 Matrix, der Elemente den jeweiligen Spaltenindizes entsprechen. Dabei bedient die innere for-Schleife mit der Iteratorvariable j wiederrum die Spaltenindizes und die äußere for-Schleife mit der Iteratorvariable i bedient die Zeilenindizes:

     [,1] [,2] [,3] [,4] [,5]
[1,]    1    1    1    1    1
[2,]    2    2    2    2    2
[3,]    3    3    3    3    3
[4,]    4    4    4    4    4

In folgendem Beispiel schließlich wird mithilfe zweier geschachtelter for-Schleifen und eines if-statements eine 4 x 5 Matrix, deren Elemente gleich den Spaltenindizes sind, wenn der jeweilige Spaltenindex größer oder gleich dem Zeilenindex ist, und deren Elemente ansonsten Null sind erzeugt:

     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    0    2    3    4    5
[3,]    0    0    3    4    5
[4,]    0    0    0    4    5
Cotton, R. (2013). Learning R (First Edition). O’Reilly.
Wickham, H. (2019). Advanced R, Second Edition. CRC Press.