Bedingung und Alternative in R: if else und ifelse

Die Bedingungsprüfung mit if und die Alternative mit if else sind die wohl am häufigsten eingesetzten Kontrollstrukturen, durch die sich der Ablauf eines Programmes steuern lässt – sie sorgen dafür, dass gewisse Programm-Teile nur ausgeführt werden, wenn eine bestimmte Bedingung erfüllt ist. In R gibt es zusätzlich eine vektorisierte Variante der Alternative mit ifelse().

Einordnung des Artikels

Vorausgesetzt werden hier insbesondere Kenntnisse über logische Werte und Operationen (siehe Einführung in R: Logische Werte) und der Zusammenhang zwischen logischen Werten und Vektoren (siehe Vektoren in R: Anwendungen).

Einführung

Ohne Kontrollstrukturen könnte man Algorithmen (oder Programme) nur in der Form schreiben, die in Abbildung 1 links zu sehen ist: Ein Programm ist ein Abfolge von Anweisungen, die der Reihe nach abgearbeitet werden. Für einfache Programme mag dies ausreichen, um komplexe Probleme zu lösen, benötigt man Kontrollstrukturen, die den Ablauf eines Programmes steuern und eine andere als die sequentielle Befehlsabarbeitung ermöglichen. Von diesen Kontrollstrukturen werden hier die Bedingungsprüfung und die Alternative besprochen (siehe Abbildung 1 mitte und rechts).

Abbildung 1: Links: Abfolge (oder Sequenz) von Befehlen, die nacheinander ausgeführt werden. Mitte: Die Bedingungsprüfung sorgt dafür, dass Anweisung 2 nur dann ausgeführt wird, wenn die Bedingung erfüllt ist (Ja-Zweig). Rechts: Bei der Alternative wird nach der Bedingungsprüfung einer der beiden Zweige durchlaufen. Ist die Bedingung erfüllt, wird Anweisung 2 ausgeführt (Ja-Zweig); ist die Bedingung nicht erfüllt, wird Anweisung 3 ausgeführt (Nein-Zweig).Abbildung 1: Links: Abfolge (oder Sequenz) von Befehlen, die nacheinander ausgeführt werden. Mitte: Die Bedingungsprüfung sorgt dafür, dass Anweisung 2 nur dann ausgeführt wird, wenn die Bedingung erfüllt ist (Ja-Zweig). Rechts: Bei der Alternative wird nach der Bedingungsprüfung einer der beiden Zweige durchlaufen. Ist die Bedingung erfüllt, wird Anweisung 2 ausgeführt (Ja-Zweig); ist die Bedingung nicht erfüllt, wird Anweisung 3 ausgeführt (Nein-Zweig).

Beispiele:

1. Bedingungsprüfung: Ein Nutzer soll eine Zahl x eingeben, mit der später weitergerechnet werden soll, was aber bei negativen x-Werten zu einem Fehler führen soll. Daher wird die Nutzer-Eingabe zunächst überprüft.

Falls x < 0, wird x = -x gesetzt.

Andernfalls werden die beabsichtigten Berechnungen sofort durchgeführt.

Im Vergleich mit Abbildung 1 mitte ist dann:

Anweisung 1: Eingabe x

Bedingungsprüfung: x < 0?
    Falls JA:
    Anweisung 2: x = -x

Anweisung 3: weitere Berechnungen

Man erkennt (sowohl in der Abbildung als auch im Pseudocode):

2. Alternative: Wieder soll der Nutzer eine Zahl x eingeben. Es soll geprüft werden, ob x gerade oder ungerade ist.

Anschließend sollen weitere Berechnungen folgen.

Vergleicht man dies mit Abbildung 1 rechts, so ist

Anweisung 1: Eingabe x

Bedingungsprüfung: x gerade?

    Falls JA:
    Anweisung 2: Ausgabe "x ist gerade"
    
    Falls NEIN:
    Anweisung 3: Ausgabe "x ist ungerade"

Anweisung 4: weitere Berechnungen

Man erkennt (sowohl in der Abbildung als auch im Pseudocode):

♦ ♦ ♦ ♦ ♦

Es sollte klar sein, dass dort, wo in den Abbildungen einzelne Anweisungen stehen, auch ganze Blöcke von Anweisungen stehen könnten. Und dass diese Blöcke wiederum Bedingungsprüfungen und Alternativen enthalten können, so dass sie beliebig ineinander verschachtelt werden können.

In den folgenden Abschnitten wird gezeigt, wie Bedingungsprüfung und Alternative mit R realisiert wird. Die Schlüsselworte dazu sind if und else.

Da es in R eigentlich keine elementaren Datentypen sondern nur Vektoren gibt, muss man bei der Bedingungsprüfung unterscheiden, ob eine Bedingung geprüft wird oder mehrere Bedingungen. Ersteres heißt, dass ein logischer Ausdruck gleich TRUE ist (genauer: ein logischer Vektor der Länge 1 ist gleich TRUE); Letzteres soll heißen, dass ein logischer Vektor komponentenweise überprüft wird, und für jede Komponente entschieden wird, welche Anweisung ausgeführt wird. Für Ersteres wird in R if eingesetzt, für Letzteres ifelse.

Die Bedingungsprüfung mit if()

Die Syntax der Bedingungsprüfung

Nach den Erklärungen in der Einführung zu Bedingungsprüfung muss man folgende Anweisungen unterscheiden (den ersten Punkt kann man auch weglassen):

  1. Die Anweisungen vor der Bedingungsprüfung.
  2. Die eigentliche Bedingungsprüfung.
  3. Die Anweisungen, die nur ausgeführt werden, wenn die Bedingung erfüllt ist.
  4. Die Anweisungen nach der Bedingungsprüfung (die immer ausgeführt werden).

Das Beispiel aus der Einführung zur Bedingungsprüfung wird jetzt in der Syntax von R gezeigt, wobei der erste und vierte Punkt der Liste nur im Pseudocode dargestellt werden – sie sind für das Verständnis der Syntax einer Bedingungsprüfung unerheblich:

# Eingabe x

if(x < 0){
    x <- -x
}

# weitere Berechnungen

Zeile 3: Die Bedingungsprüfung wird mit dem Schlüsselwort if eingeleitet. Die Bedingungsprüfung findet in runden Klammern statt. Hier muss ein logischer Ausdruck stehen, der also zu TRUE oder FALSE ausgewertet werden kann.

Zeile 3 bis 5: In den geschweiften Klammern stehen alle Anweisungen, die ausgeführt werden, wenn die Bedingungsprüfung TRUE ergeben hat. Da es hier nur eine Anweisung gibt, könnte man die geschweiften Klammern auch weglassen; dies wird unten sofort erklärt.

Zeile 7: Die Anweisungen nach dem Schließen der geschweiften Klammern werden immer ausgeführt.

Es sollte sofort klar sein, wie das Skript abzuändern ist, wenn mehrere Anweisungen im if-Zweig ausgeführt werden sollen. Der if-Zweig ist durch die geschweiften Klammern vom Rest des Programmes abgegrenzt, daher werden zusätzliche Anweisungen in die geschweiften Klammern mit aufgenommen.

Nicht ganz klar ist: Benötigt man für eine Anweisung tatsächlich die geschweiften Klammern?

Nein sie sind nicht nötig und können weggelassen werden. Es wird aber empfohlen, die Klammern trotzdem wie im Beispiel oben zu setzen: Der Quelltext ist leichter lesbar, da man auf einen Blick den if-Zweig erkennt und ihn klar von den Anweisungen nach dem if-Zweig unterscheiden kann. Ein weiterer Grund ist, dass man oftmals ein Programm erweitert und womöglich neue Anweisungen zum if-Zweig hinzufügt – in diesem Fall sind die Klammern schon vorhanden, wenn man sich angewöhnt, sie immer zu setzen.

Die folgenden 2 Skripte sind gleichwertig zum Skript oben und verzichten auf die geschweiften Klammern:

# Eingabe x

if(x < 0)   x <- -x

# weitere Berechnungen
# Eingabe x

if(x < 0)
    x <- -x

# weitere Berechnungen

Es sollte klar sein: Wird die Leerzeile nach dem if-Zweig nicht gesetzt, kann man leicht das Ende des if-Zweiges übersehen und als Leser den Quelltext falsch interpretieren. (Wenn Sie also auf die geschweiften Klammern verzichten, setzen Sie zumindest die Leerzeile.) Syntaktisch korrekt sind alle drei Versionen.

Die Bedingung in if()

Oben wurde schon gesagt, dass in den runden Klammern ein logischer Ausdruck stehen muss, der zu TRUE oder FALSE ausgewertet werden kann. Das heißt aber, dass hier auch beliebige logische Verknüpfungen eingesetzt werden können. Man sollte dabei bedenken, dass verschachtelte logische Ausdrücke schwer nachvollziehbar sind und sich dort leicht Fehler einschleichen. Fehler nicht im Sinne der Syntax sondern semantische Fehler: Der logische Ausdruck liefert ein anderes als das beabsichtigte Ergebnis.

Als Faustregel kann man verwenden, dass logische Ausdrücke mit mehr als zwei Verknüpfungen schwer nachvollziehbar sind und geeignet vorbereitet werden sollten. Oder wer kann in 10 Sekunden sagen, welche Bedingung hier beschrieben wird und ob sie überhaupt erfüllt werden kann:

if(x > y && x > 5 && y > 2 && x < 2*y){
    # doSomething(x, y)
}

Wenn sich derartige Ausdrücke nicht vermeiden lassen, sollte man sie zumindest ausreichend kommentieren.

Muss man Bedingungen über Vektoren formulieren, sind oft Funktionen wie all() oder any() hilfreich, die in Vektoren in R: Anwendungen im Abschnitt Die Funktionen all() und any() beschrieben wurden.

Derartige Funktionen – oder allgemein die Verarbeitung von Vektoren – werfen sofort ein Problem auf: Was passiert, wenn in der Bedingung nicht ein logischer Ausdruck, sondern ein logischer Vektor steht?

Das folgende Skript untersucht zwei Vektoren auf Gleichheit:

v <- c(1, 1, 1)
w <- c(1, 1, 0)

if(v == w){
  cat(v, w)
}
# 1 1 1 1 1 0
# Warning message:
#   In if (v == w) { :
#       the condition has length > 1 and only the first element will be used

Zeile 1 und 2: Es werden zwei Vektoren der Länge 3 definiert.

Zeile 4: Die beiden Vektoren werden mit dem Vergleichs-Operator == verglichen. Dieser Vergleich liefert einen logischen Vektor der Länge 3, da jede der 3 Komponenten auf Gleichheit geprüft wird. Mehr zum Vergleichs-Operator findet man in Vektoren in R: Anwendungen im Abschnitt Der Vergleichsoperator und die Funktion identical().

Zeile 8 bis 10: An der Warnung erkennt man, wie R reagiert:

Da man derartige Warnungen bei längeren Skripten leicht übersehen kann, ist es ratsam, niemals den Vergleichs-Operator == in der Bedingungsprüfung einzusetzen (oder nur, wenn kein unerwünschtes Verhalten auftreten kann). Durch den recycling-Mechanismus werden in R oft Vektoren angelegt, wo man eigentlich nur Zahlen erwartet. Zudem kann beim Vergleich mit == unerwünschtes Verhalten eintreten, wenn ein Wert gleich NA ist.

Möchte man tatsächlich mehrere Komponenten miteinander vergleichen und mit einem logischen Vektor in der Bedingungsprüfung arbeiten, gibt es ifelse, was später besprochen wird.

Die Alternative mit if() und else

Bei der Alternative findet wieder zuerst eine Bedingungsprüfung statt. Liefert sie TRUE, wird der if-Zweig durchlaufen, bei FALSE der else-Zweig. Es kann nicht vorkommen, dass beide Zweige durchlaufen werden.

Das folgende Skript nimmt das Beispiel aus der Einführung auf (Prüfung einer Zahl, ob sie gerade oder ungerade ist) und zeigt, wie man eine Alternative syntaktisch korrekt formuliert:

# Eingabe x

if(x %% 2 == 0){
    cat("x ist gerade\n")
} else {
    cat("x ist ungerade\n")
    }

# weitere Berechnungen

Man erkennt:

Zeile 3: Die Bedingungsprüfung sieht aus wie bei der reinen Bedingungsprüfung. Auch hier gilt, dass in den runden Klammern nur ein logischer Wert stehen darf.

Zeile 3 bis 5: Der if-Zweig befindet sich wie bei der Bedingungsprüfung in den geschweiften Klammern.

Zeile 5: Neu ist das Schlüsselwort else, das den Alternativ-Zweig oder kurz else-Zweig einleitet. Man beachte die Schreibweise:

Zeile 5 bis 7: Der else-Zweig ist in geschweifte Klammern eingeschlossen.

Zeile 5 sieht unscheinbar aus, es ist aber wichtig, diese Konvention einzuhalten. Das folgende Skript scheint identisch zum vorhergehenden Skript zu sein, liefert aber einen Syntax-Fehler – wer das Skript zum ersten Mal sieht, wird hier keinen Fehler vermuten:

# Eingabe x

if(x %% 2 == 0){
    cat("x ist gerade\n")
}
else {              # SYNTAX-FEHLER!
    cat("x ist ungerade\n")
}

# weitere Berechnungen

Im Vergleich zu oben wurde nur Zeile 5 in zwei Zeilen zerlegt; else steht jetzt in einer eigenen Zeile, wodurch der Quelltext sogar übersichtlicher wirkt. Aber er ist syntaktisch falsch: In der Entwicklungsumgebung sollte in Zeile 6 angezeigt werden:

unexpected token 'else'

Der Grund für den Fehler ist nicht so schwer aufzudecken: Vergleicht man das Skript mit einer echten Bedingungsprüfung, so erkennt man, dass Zeile 3 bis 5 eine vollständige Bedingungsprüfung bilden. Und diese ist mit der geschweiften Klammer in Zeile 5 zu Ende.

Zeile 6 wird jetzt so interpretiert, dass ein neuer Befehl mit else startet – aber das ist syntaktisch nicht korrekt. Das Schlüsselwort else kann nur innerhalb einer Alternative stehen und muss dann wie im Skript oben in der selben Zeile stehen wie die geschlossene geschweifte Klammer des if-Zweiges.

Tip:

Schreiben Sie die Alternative immer so wie im Skript oben; setzen Sie dazu insbesondere das Schlüsselwort else wie in Zeile 5. Verwenden
Sie zudem für beide Zweige die geschweiften Klammern, auch wenn sie eigentlich überflüssig sind.

Wie bei der Bedingungsprüfung kann man auch auf die geschweiften Klammern verzichten, wenn ein Zweig nur einen Befehl enthält; syntaktisch korrekt ist somit auch:

# Eingabe x

if(x %% 2 == 0) cat("x ist gerade\n")   else    cat("x ist ungerade\n")

# weitere Berechnungen

Zu empfehlen ist diese Schreibweise nur in Fällen, bei denen der Inhalt völlig klar ist, bei schwierigen Befehlen ist der Quelltext schwer zu lesen.

Aufgabe:

Testen Sie das letzte Skript, wenn statt Eingabe x erfolgt: x <- NA . Können Sie voraussagen, welcher Zweig durchlaufen wird?

Testen Sie das Skript (mit x <- NA ) wenn Sie if(x %% 2 == 0) ersetzen durch: if(identical(x %% 2, 0)) . Können Sie voraussagen, welcher Zweig jetzt durchlaufen wird?

Bedingungsprüfung für einen logischen Vektor mit ifelse()

Oben wurde sowohl bei der Bedingungsprüfung als auch bei der Alternative betont, dass ein logischer Wert als Bedingung abgefragt wird. Mit ifelse kann man mehrere logische Werte abfragen, aber man sollte ifelse weniger als Kontrollstruktur verstehen, sondern als eine Funktion mit drei Eingabewerten:

ifelse(test, yes, no)

Dabei ist:

Damit der Rückgabewert gesetzt werden kann, sollten die drei Vektoren test, yes und no gleiche Länge haben, andernfalls wird der recycling-Mechanismus angewendet. Der Rückgabewert von ifelse() ist ein Vektor mit derselben Länge wie test.

Ein Beispiel:

v <- as.integer( runif(n = 20, min = 1, max = 20) )
v
# [1]  5  4 10 18 19 13 12  1 19  5  2 16 12  4 12 11  5  2  9  5

w <- ifelse(test = v > 14, yes = NA, no =  v)
w
# [1]  5  4 10 NA NA 13 12  1 NA  5  2 NA 12  4 12 11  5  2  9  5

v[v > 14] <- NA
v
# [1]  5  4 10 NA NA 13 12  1 NA  5  2 NA 12  4 12 11  5  2  9  5

Zeile 1 bis 3: Es wird ein Vektor v mit Zufallszahlen (ganze Zahlen, zwischen 1 und 20) erzeugt und ausgegeben.

Zeile 5: Der Aufruf von ifelse() mit ifelse(test = v > 14, yes = NA, no = v) sorgt dafür, dass:

Zeile 6 und 7; Das Ergebnis wird im Vektor w abgespeichert und ausgegeben.

Zeile 9 bis 11: Man erkennt, dass der Vektor w auch ohne ifelse() erzeugt werden kann – in diesem Fall sogar deutlich einfacher. Aber es sollte klar sein, dass man mit Hilfe von ifelse() Anweisungen schreiben kann, die sich sonst nicht als Einzeiler formulieren lassen.

Das folgende Beispiel zegt einen komplizierteren Aufruf von ifelse():

u <- as.integer( runif(n = 20, min = 1, max = 20) )
u
# [1]  3  4  4 11 14 17 18  9  5 19 15 17 15 12  5  5  2  5  5  6

v <- as.integer( runif(n = 20, min = 1, max = 20) )
v
# [1]  3  5  3  8  7 16 18  8 18  7  1  6  8  4  6  3  3  5  9 18

pmin(u, v)
# 1]  3  4  3  8  7 16 18  8  5  7  1  6  8  4  5  3  2  5  5  6

pmax(u, v)
# [1]  3  5  4 11 14 17 18  9 18 19 15 17 15 12  6  5  3  5  9 18

w <- ifelse( test = u > 10, yes = pmin(u, v), no =  pmax(u, v) )
w
# [1]  3  5  4  8  7 16 18  9 18  7  1  6  8  4  6  5  3  5  9 18

Oben wurde schon gesagt, dass man die Funktion ifelse() eigentlich nicht zu den Kontrollstrukturen zählen sollte, da sie nicht den Ablauf eines Programmes steuert. Man sollte sie eher als Funktion verstehen, die den häufigsten Gebrauch der Alternative verallgemeinert. Damit ist folgendes gemeint: Die Alternative wird oft so eingesetzt, dass der Wert einer Variable in Abhängigkeit von einer Bedingung gesetzt wird, etwa:

# x ist eine Zahl, die früher berechnet wurde, 
# deren Wert aber im Programmablauf nicht bekannt ist

if(x < 0){
    y <- -1
} else {
    y <- 1
}

Dieses Beispiel einer Alternative ist so einfach, dass man es in eine Zeile schreiben kann:

if(x < 0)   y <- -1    else    y <- 1

Und an dieser Darstellung wird sofort klar, dass man die Alternative hier auch als Funktions-Aufruf verstehen kann:

Die Variable y wird durch eine Funktion gesetzt, die drei Eingabewerte besitzt, nämlich

Verallgemeinert man diese Funktion im folgenden Sinn:

so hat man genau die Funktion ifelse() konstruiert.

Zusammenfassung

Informationen über die Bedingungsprüfung mit if und die Alternative mit if else findet man in der Dokumentation unter ?Control im Paket base.

Die Bedingungsprüfung

Im allgemeinen Fall sieht eine Bedingungsprüfung folgendermaßen aus:

if(cond){
    # Anweisungen im if-Zweig
}

Dabei ist cond ein logischer Ausdruck der Länge 1 und in den geschweiften Klammern stehen beliebig viele Anweisungen. Sie werden nur ausgeführt, wenn cond gleich TRUE ist.

Die Alternative

Im allgemeinen Fall sieht eine Alternative folgendermaßen aus:

if(cond){
    # Anweisungen im if-Zweig
} else {
    # Anweisungen im else-Zweig
}

Auch hier ist cond ein logischer Ausdruck der Länge 1. Ist cond gleich TRUE, werden die Anweisungen im if-Zweig ausgeführt, ist cond gleich FALSE, werden die Anweisungen im else-Zweig ausgeführt.

Die Funktion ifelse()

Die Funktion ifelse() besitzt drei Eingabewerte, die Vektoren identischer Länge sein sollten:

ifelse(test, yes, no)

Rückgabewert ist ein Vektor mit der Länge des logischen Vektors test, wobei der Rückgabewert gleich yes[i] ist, falls test[i] gleich TRUE ist und no[i] , falls test[i] gleich FALSE ist. Falls yes und no nicht die geeigneten Längen haben, wird der recycling-Mechanismus angewendet.