Kapitel 4 Erste Schritte

Im letzten Abschnitt habt ihr R als glorifizierten Taschenrechner gesehen.
Als nächstes schauen wir uns an, was wir sonst so damit anstellen können.

Zuerst ein bisschen Terminologie:

“To understand computations in R, two slogans are helpful:

  • Everything that exists is an object.
  • Everything that happens is a function call."

– John Chambers

Oder in der Sprach-Analogie: Alles was in R existiert (Variablen, Tabellen, etc.) ist ein Nomen (Objekt) und alles, was etwas tut, ist ein Verb (Funktion).

4.1 Grundfunktionen

Die einfachsten Funktionen haben wir in Form der Rechenzeichen + - / * schon kennengelernt, aber es gibt natürlich noch mehr.
Eine Funktion in R hat sieht immer ungefähr so aus: sqrt(8). Der Name der Funktion, hier sqrt, (immer ohne Leerzeichen) gefolgt von Klammern, in denen ein oder mehrere Argumente stehen. Ein Argument ist das, womit die Funktion arbeiten soll. Eine gängige Veranschaulichung für Funktionen sind Verben einer Sprache, denn sie tun etwas.

Eine der wichtigsten Grundfunktionen ist c(), für combine. Mit c verbindet ihr mehrere Zahlen zu einem Vektor (ja, wie in der linearen Algebra. Mathe und so. Wisstschon.). Wenn ihr mehrere Zahlen zu einem Vektor kombiniert habt, könnt ihr damit so spaßige Dinge machen wie Mittelwerte ausrechen, sie aufsummieren oder zwei Vektoren gleicher Länge addieren.
Probiert mal ein paar Beispiele aus:

# Ein paar Zahlen
c(1, 1, 2, 3, 5, 8, 13, 21)
[1]  1  1  2  3  5  8 13 21
# Was ist der Mittelwert der Zahlen?
mean(c(1, 1, 2, 3, 5, 8, 13, 21))
[1] 6.75
# Und die Summe?
sum(c(1, 1, 2, 3, 5, 8, 13, 21))
[1] 54
# Und wenn wir quadrieren?
c(1, 1, 2, 3, 5, 8, 13, 21)^2
[1]   1   1   4   9  25  64 169 441
# Oder die Wurzel ziehen?
sqrt(c(1, 1, 2, 3, 5, 8, 13, 21))
[1] 1.000000 1.000000 1.414214 1.732051 2.236068 2.828427 3.605551 4.582576

Was wir hier sehen ist der Unterschied zwischen Funktionen, die aus mehreren Zahlen eine machen (mean, sum), und Funktionen, die auf jeder Zahl einzeln operieren (sqrt, ^).
Was wir außerdem sehen: Jedes mal die Liste von Zahlen c(1, 1, 2, 3, 5, 8, 13, 21) kopieren und in eine Funktion einsetzen ist ziemlich unpraktisch. Stellt euch vor, ihr habt eine Reihe von Testergebnissen von hunderten ProbandInnen und müsst da alles einzeln, also, nein, das wäre ja albern.
Für sowas gibt es dann Abstraktionen wie Variablen und Datensätze, die entweder eine Liste von Werten oder auch eine Liste einer Liste von Werten handlich machen — das sehen wir dann in den nächsten beiden Abschnitten.

Eine weitere praktische Funktion ist length(): Sie sagt uns, wie lang das Argument ist.
Wenn wir uns also angucken, wie der Mittelwert funktioniert…

\[\bar{x} = \frac{1}{n} \sum^n_{i=1} x_i\]

…und wir das übersetzen in “Die Summe aller Werte geteilt durch die Anzahl der Werte”, dann können wir statt mean also auch folgendes schreiben:

# in lang
sum(c(1, 1, 2, 3, 5, 8, 13, 21))/length(c(1, 1, 2, 3, 5, 8, 13, 21))
[1] 6.75
# in kurz
mean(c(1, 1, 2, 3, 5, 8, 13, 21))
[1] 6.75

Ihr seht vielleicht so langsam, wieso wir das mit den Klammern und den Leerzeichen für die Lesbarkeit erwähnt haben.
Aber gut, so langsam wird’s unübersichtlich, es wird Zeit ein paar Variablen anzulegen.

4.1.1 Funktionsbeispiele

Wenn ihr R lernt, werdet ihr erfahrungsgemäß die meiste Zeit damit verbringen herauszufinden wie bestimmte Funktionen funktionieren und welche Funktion für euer Vorhaben die richtige ist.

Funktionen sind zwar vom Schema immer gleich — ihr steckt irgendwelche Argumente rein, und es kommt irgendein Ergebnis raus — aber wie die Argumente aussehen unterscheidet sich von Funktion zu Funktion.
sd() zum Beispiel hat zwei Argumente:

  • x: Ein Vektor aus Zahlen, aus denen die Standardabweichung berechnet werden soll
  • na.rm: Für NA remove, entweder TRUE oder FALSE. Wenn x fehlende Werte (NA) enthält, dann werden diese automatisch ignoriert, wenn na.rm = TRUE
zahlen <- c(3, 6, 8, 3, 1, 2, 5, 6, 4, 3, NA, 4, 5, 7, NA, 1, 4)

# Ergibt NA :(
sd(zahlen)
[1] NA
# Ergibt ein Ergebnis :)
sd(zahlen, na.rm = TRUE)
[1] 2.065591

Der default für na.rm ist bei den meisten Funktionen (z.B auch mean) FALSE, das heißt fehlende Werte werden nicht automatisch ignoriert. Wenn euer input aber NA enthält, dann lässt sich daraus nicht sauber ein Mittelwert oder eine Standardabweichung berechnen, weil wir nichts über NA wissen (wir widmen uns NA im Kapitel zu Datentypen).
Nicht jede Funktion hat ein Argument namens na.rm, aber ihr werdet im Laufe der Zeit lernen, bei welchen Funktionen ihr darauf achten müsst, wie mit fehlenden Werten umgegangen wird.

Einige andere Funktionen, die insbesondere zum Lernen und Ausprobieren praktisch sind, werden zur Erstellung von Sequenzen benutzt — also Reihen von Zahlen in einem bestimmten Muster:

# Die Zahlen von 1 bis 100
1:100
  [1]   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17
 [18]  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34
 [35]  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51
 [52]  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68
 [69]  69  70  71  72  73  74  75  76  77  78  79  80  81  82  83  84  85
 [86]  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100
# 10 bis 15 in 0.5er-Schritten
seq(10, 15, 0.5)
 [1] 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0
# Äquivalent, mit explizit benannten Argumenten:
seq(from = 10, to = 15, by = 0.5)
 [1] 10.0 10.5 11.0 11.5 12.0 12.5 13.0 13.5 14.0 14.5 15.0
# Von 5 bis -5 in ganzen Schritten
seq(5, -5)
 [1]  5  4  3  2  1  0 -1 -2 -3 -4 -5
# Sequenz von 1 mit Länge 5, dasselbe wie 1:5
seq_len(5)
[1] 1 2 3 4 5
seq_len(length.out = 5)
[1] 1 2 3 4 5

Das : ist eine einfache Funktion und kann gelesen werden wie “Von … bis … in ganzen Schritten”. Zusötzlich gibt es die Funktion seq(), die etwas flexibler ist. Außerdem gibt es diverse Funktionen für zufallsgenerierte Zahlen:

# Normalverteilte Zufallszahlen (10 Stück, Mittelwert 5, Standardabweichung 2) 
rnorm(n = 10, mean = 5, sd = 2)
 [1] 0.1050496 5.6924596 5.4626791 7.1427846 6.7878764 3.7799881 7.5609843
 [8] 2.0065842 3.2722171 8.1344442
# Gleichverteilte Zufallszahlen (sprich "r unif", nicht "run if")
runif(n = 10)
 [1] 0.2129337 0.8390889 0.4725982 0.9289802 0.7350119 0.1165735 0.5998891
 [8] 0.4962630 0.9410604 0.3718608
# Münzwurf (Binomialverteilte Ergebnisse), 10 Stück
rbinom(n = 10, size = 1, prob = 0.5)
 [1] 1 1 0 1 1 1 1 1 1 1
# Würfel (W6) (10 Stück)
sample(x = 1:6, size = 10, replace = TRUE)
 [1] 3 4 5 5 1 1 1 4 3 4

4.2 Variablen

Wenn Funktionen wie Verben sind, dann sind Variablen wie Nomen. Sie haben einen Namen, und mit ihnen kann man Dinge tun. Oder sogar Sachen machen.
Variablen werden durch eine Zuweisung (Assignment) erstellt, was in R traditionell via <- passiert.
In den meisten anderen Programmiersprachen benutzt man dafür =, aber nun ja, R ist historisch gewachsen6, also nehmt für den Anfang einfach mal hin, dass das nunmal so ist.
Wir speichern also mal ein paar Dinge:

# Speichern in "fib"
fib <- c(1, 1, 2, 3, 5, 8, 13, 21) 

# Ausgeben lassen
fib
[1]  1  1  2  3  5  8 13 21
# Mittelwert…
mean(fib)
[1] 6.75
# …funktioniert immer noch. Angenehm.
# Und wenn wir…
fib + fib
[1]  2  2  4  6 10 16 26 42
# Abgefahrener Kram.

Hier haben wir die Zahlen 1, 1, 2, 3, 5, 8, 13, 21 in die Variable fib gespeichert, weil es die ersten paar Fibonacci-Zahlen sind, und wir unsere Variablen immer so benennen sollten, dass wir später noch wissen wofür sie da sind. Viele Tutorials beginnen damit, Variablen wie x, y und z anzulegen, aber da blickt ja irgendwann kein Mensch mehr durch.
Wenn ihr jetzt mit euren Zahlen arbeiten wollt, könnt ihr einfach in jeder Funktion fib statt der Liste mit c(…) einsetzen, und alles funktioniert wie vorher. Das liegt daran, dass R bei jedem Befehl erstmal nachschaut, ob ihr eine Variable benutzt (alles was Text ohne Anführungszeichen ist), und ob es die Variable findet. Wenn es die Variable gefunden hat, guckt es nach, was da drinsteht, in diesem Fall also c(1, 1, 2, 3, 5, 8, 13, 21), dann benutzt R den Inhalt der Variablen.

An dieser Stelle bietet es sich an, einen neuen Typ einzuführen: Den String (oder auch character). Als String bezeichnet man im Kontext von, naja, Computerkram generell eigentlich, alles was als Text durchgeht. Sobald wir etwas nicht mehr nur durch Zahlen darstellen können, ist es ein String.
Strings stehen in R immer in Anführungszeichen, entweder in "doppelten" oder in 'einfachen'.

Wichtig dabei ist, dass sich Anführungszeichen ähnlich verhalten wie Klammern. Wenn wir einen String mit beginnen, müssen wir ihn auch wieder mit schließen, ansonsten wartet R brav darauf, dass endlich das zweite kommt und verläuft sich.
Ein Beispiel:

# Vollkommen okay
namen <- c("Tobi", "Lukas", "Nadja", "Christoph")

# Auch okay, aber inkonsistent und daher eher unschön
namen <- c('Tobi', "Lukas", 'Nadja', "Christoph")

# Tod und Verderben (=> funktioniert nicht)
namen <- c("Tobi", 'Lukas", 'Nadja, Christop")

Wir können auch Zahlen in "" setzen — das ist kein Problem, aber dann sind es nunmal keine Zahlen in diesem Sinne mehr, es sind Strings, und mit Strings können wir nicht rechnen.
Probiert folgendes aus:

# Okay
5 + 5

# Hä?
5 + "5"

Ihr sehr jetzt vermutlich die Meldung Error in 5 + "5" : non-numeric argument to binary operator.
Das non-numeric argument hier ist die "5". Merken: 5 ist numerisch, aber "5" ist ein character (=> String).
Der binary operator an dieser Stelle ist übrigens das +. Ein Operator, weil es, äh… operiert? Naja, es tut Dinge, und wenn etwas in R Dinge tut, ist es meistens ein Operator in irgendeinem Sinne. Das binary heißt, dass es zwei (bi, binär, binary, zwei halt) Argumente nimmt.
Wie schon gesagt, Argumente sind die Dinge, die wir an Funktionen übergeben, und wenn wir an eine Funktion wie + oder auch mean() ein Argument übergeben, mit denen sie nichts anfangen können, dann beschwert sich R weil es nicht weiß was zum Geier ihr da vorhabt.

namen <- c("Tobi", "Lukas", "Nadja", "Christoph")

fib <- c(1, 1, 2, 3, 5, 8, 13, 21) 

# Alles knorke
mean(fib)
[1] 6.75
# Alles CHAOS UND UNHEIL
mean(namen)
[1] NA

Was es mit NA auf sich hat, und was es noch so für Datentypen gibt, sehen wir dann im Abschnitt zu Datentypen.

Eine letzte Sache noch: Strings sind “dominanter” als Zahlen, das heißt, wir können zwar Zahlen verbinden zu c(1, 2, 3), und Strings zu c("A", "B", "C"), aber wenn wir c("A", 2, "C", 4) schreiben, dann behandelt R einfach alle Elemente des Vektors (=> Das, was in c(…)) steht, als wären es character-Werte.

Merke: Ein Vektor in R muss immer Elemente des gleichen Typs haben, Zahlen und Buchstaben zusammen werden zu Strings konvertiert!

4.3 Tabellen

Jetzt haben wir schonmal das Vokuabular an der Hand um Zahlen und beliebige Strings in R zu verarbeiten, aber noch ist das alles etwas unahndlich um damit richtig zu arbeiten.

Stellt euch vor, wir wollen einen kleinen Datensatz erstellen über die Statistiktutorien in QM mit Variablen wie Namen, Alter, und vielleicht sowas wie Beliebtheit auf einer Skala von 1-10.
Wir könnten sowas machen:

namen <- c("Tobi", "Christoph", "Nadja", "Lukas")
alter <- c(20, 35, 30, 12) # (Nicht alle diese Werte sind korrekt)

mean(alter)
[1] 24.25

Schön und gut, aber das ist ja unhandlich. Was, wenn wir die Namen aller TutorInnen haben wollen, die jünger als 30 sind? Alles was wir mit alter machen, passiert unabhängig von namen.

Um mehrere Variablen in Kontext zu setzen, gibt es tabellarischen Datenstrukturen, namentlich nennt sich sowas in R dann data.frame. Letztendlich ist das nichts anderes als eine Tabelle, aber für R ist eine Tabelle praktische eine Liste von Vektoren mit gleicher länge:

leute <- data.frame(name = c("Tobi", "Christoph", "Nadja", "Lukas"),
                    alter = c(20, 35, 30, 12),
                    beliebtheit = c(9, 10, 8, 3))

# Anzeigen lassen
leute
       name alter beliebtheit
1      Tobi    20           9
2 Christoph    35          10
3     Nadja    30           8
4     Lukas    12           3

Was haben wir da gemacht?

  • Wir haben einen data.frame mit der gleichnamigen Funktion erstellt
  • Die Argumente der Funktion haben die Form Spaltenname = Werte der Spalte
  • Mehrere Argumente werden mit , getrennt und optional mit einem Zeilenumbruch übersichtlich gehalten

Das Ergebnis ist eine Variable leute, die drei Spalten mit je vier Werten hat.
Jede Spalte ist eine Variable, und jede Zeile der Tabelle kann als eine Beobachtung betrachtet werden.
Eine Beobachtung (Observation) sind alle Werte, die wir zu einem Untersuchungsobjekt haben, also in diesem Beispiel eine Person. Wenn wir uns nur die erste Zeile anschauen, sehen wir nur die Werte, die zu Tobi gehören, in der zweiten Zeile sehen wir die Werte zu Christoph etc.

Tabellen, und damit data.frames, sind für uns die wichtigsten Objekte in R, weil wir fast ausschließlich mit Datensätzen in dieser Form arbeiten werden um unsere Statistik da draufzuwerfen.
Wie können wir jetzt mit einzelnen Variablen arbeiten?

# Die Variable "name" ausgeben lassen
leute$name
[1] Tobi      Christoph Nadja     Lukas    
Levels: Christoph Lukas Nadja Tobi
# Den Mittelwert von "alter" bestimmen
mean(leute$alter)
[1] 24.25
# Die Standardabweichung von "beliebtheit"
sd(leute$beliebtheit)
[1] 3.109126
# Was auch funktioniert:
leute[["name"]]
[1] Tobi      Christoph Nadja     Lukas    
Levels: Christoph Lukas Nadja Tobi
leute[["alter"]]
[1] 20 35 30 12

Das mit den “Levels” wird im Abschnitt zu Datentypen erklärt

Was wir hier benutzen nennt sich Subsetting, also im Grunde nur einen Teil von etwas rausholen. Hier also einen Teil der Tabelle on Form einer einzelnen Spalte.
Spalten können wir mit $ oder [[ ]] direkt aus einem data.frame ansteuern, was unser Leben gleich viel einfacher macht. Strenggenommen sidn $ und [[ auch eigene Funktionen, aber dazu vielleicht später mehr, im Moment ist für uns nur wichtig, dass wir einzelne Spalten (Variablen) einer Tabelle (data.frame) einfach adressieren und genauso behandeln können wie die einzelnen Variablen name und alter, die wir weiter oben erstellt haben.

4.4 Umgang mit Tabellen

Da wir noch nicht an dem Punkt sind, wo wir beliebige Daten einlesen können, und wir natürlich zu faul sind uns eine größere Tabelle selber zu schreiben, greifen wir zu Übungszwecken mal auf einen Datensatz zurück, der bei R von Haus aus mitgeliefert wird: sleep.

Dieser Datensatz beinhaltet die Daten aus einer Medikamentenstudie, bei der es um Schlafgewinn bzw. -verlust ging. Die Tabelle hat drei Spalten (Variablen) zu 10 Personen:

  • extra: Schlafzuwachs in Stunden, positiv oder negativ für mehr bzw. weniger Schlaf als vorher
  • group: Die Versuchsgruppe, sprich welches Medikament die Person bekam, 1 oder 2
  • ID: Die Identifikationsnummer der Person. Es ist gängig, ProbandInnen pseudonymisiert durchzunummerieren, der Zuordnung unt des Datenschutzes wegen als Zahlen.
# Mit head() lassen wir uns die ersten paar Zeilen (den "Kopf") der Tabelle anzeigen
head(sleep)
  extra group ID
1   0.7     1  1
2  -1.6     1  2
3  -0.2     1  3
4  -1.2     1  4
5  -0.1     1  5
6   3.4     1  6

Wie viele Zeilen hat die Tabelle?

nrow(sleep)
[1] 20

Die number of rows bekommen wir mit nrow() — ihr dürft jetzt raten, wie wir uns die Anzahl der Spalten (columns) anzeigen lassen können.

ncol(sleep)
[1] 3

Surprise!

Okay, aber was interessiert uns an diesem Datensatz jetzt? Wie wäre es mit dem durchschnittlichen Schlafzuwachs:

mean(sleep$extra)
[1] 1.54

Schön und gut, aber wir wollen ja vermutlich die beiden Gruppen (Medikamente) vergleichen, also was tun?
Subsetting to the rescue /o/

gruppe1 <- sleep[sleep$group == 1, ]
gruppe2 <- sleep[sleep$group == 2, ]

# Mittelwert der ersten Gruppe
mean(gruppe1$extra)
[1] 0.75
# Mittelwert der zweiten Gruppe
mean(gruppe2$extra)
[1] 2.33

Okay, Schritt für Schritt.
Hier haben wir unseren ersten logischen vergleich benutzt, um eine Teilmenge der Tabelle zu extrahieren.
Das klingt fancy, ist aber ziemlich simpel.
In Worten heißt die Zeile gruppe1 <- sleep[sleep$group == 1] lediglich:
Nimm die Tabelle sleep und filtere daraus alle Zeilen, die zu der Gruppe 1 gehören, und speichere sie in die Variable gruppe1” Das Resultat sind zwei Variablen, die einen Teil der Tabelle sleep enthalten, und zwar jeweils zu einer der beiden Gruppen.

Wieso dann eigentlich noch diese ,-Sache am Ende der eckigen Klammern?
Das gehört zur Art, wie R Tabellen indiziert, sprich wie man einzelne Bereiche der Tabelle ansteuert:

# Die erste Spalte
sleep[1]
   extra
1    0.7
2   -1.6
3   -0.2
4   -1.2
5   -0.1
6    3.4
7    3.7
8    0.8
9    0.0
10   2.0
11   1.9
12   0.8
13   1.1
14   0.1
15  -0.1
16   4.4
17   5.5
18   1.6
19   4.6
20   3.4
# Die erste Zeile
sleep[1, ]
  extra group ID
1   0.7     1  1
# Die erste Zeile und die dritte Spalte
sleep[1, 3]
[1] 1
Levels: 1 2 3 4 5 6 7 8 9 10

Die allgemeine Form ist tabelle[Zeilennummer, Spaltennumer], und jetzt fragt ihr euch vermutlich, wieso wir vorhin [[ ]] benutzt haben, und jetzt [ ] — die kurze Antwort ist: Das ist halt was anderes. Die Details sind erstmal nicht so wichtig, was ihr euch vorerst merken solltet ist folgendes:

  • sleep[1] ergibt einen data.frame mit nur einer Spalte
  • sleep[1, ] ergibt einen data.frame mit nur einer Zeile
  • sleep[[1]] und sleep$extra sind dasselbe (weil extra die erste Spalte ist) und ergeben die erste Spalte als Vektor
  • sleep$extra[[2]] und sleep$extra[2] sind dasselbe: Das zweite Element im Vektor sleep$extra

Bei einer Tabelle ist es nützlich mit Zeilen und Spalten zu arbeiten, um die gewünschten Werte rauszuholen, aber bei einem Vektor gibt es in diesem Sinne nur eine Dimension.
Sinn der Sache ist, dass wir Funktionen wie mean oder sd nur auf Vektoren anwenden können, was auch intuitiv irgendwie sinnvoll scheint, denn der Mittelwert einer ganzen Tabelle mit mehreren Variablen ist ja konzeptionell etwas… schwierig.

# Okay
mean(sleep$extra)
[1] 1.54
# Das selbe Ergebnis
mean(sleep[[1]])
[1] 1.54
# Auch okay!
mean(sleep[["extra"]])
[1] 1.54
# Das hier nicht so
mean(sleep[1])
[1] NA

sleep[1] gibt euch zwar auch die Spalte extra, aber wie schon gesagt, in data.frame-Form, und nicht als Vektor.

Vermutlich verwirrt euch das ganze Geklammere jetzt mehr oder weniger stark, aber glaubt mir, wenn wir erstmal ein Gefühl dafür habt ist es sehr viel Wert diese Grundlagen auf dem Schirm zu haben (oder sie zumindest nachlesen zu können), denn in der ersten Zeit eurer R-Nutzung werdet ihr massenhaft kleinere und größere Fehler in dieser Art machen, wo ihr zwar das richtige meint, aber R nicht das richtige sagt.
Die andere Sache ist, dass ihr das mit den eckigen Klammern gar nicht so häufig brauchen werdet, wenn ihr euch erstmal an das tidyverse und dplyr gewöhnt habt, aber dazu später mehr.

Wir schneiden das Ganze Thema Subsetting hier auch erstmal nur an, aber wenn ihr’s jetzt schon ganz genau wissen wollt, könnt ihr die Details hier nachlesen

4.5 Logische Vergleiche

Logik! Eine Welt des Spaßes, der internen Konsistenz[^Naja, fast. Aber Gödel lassen wir mal aus.] und der unendlichen Anwendbarkeit in allen Bereichen.
Was ihr intuitiv als Logik kennt ist alleridngs etwas anderes als formale Logik, also das, was Computer verstehen.
Wir brauchen zum Glück nicht all zu viel davon, nur den Standardkram und nichtmal das volle Spekrum Bool’scher Algebra.

Wir brauchen Logik in R in erster Linie zum indizieren von Objekten. Das heißt, wenn wir alle Zeilen einer Tabelle haben wollen, für die eine bestimmte Variable einen bestimmten Wert hat oder eine Bedingung erfüllt, dann drücken wir das durch Logik aus. Dasselbe funktioniert natürlich auch bei Vektoren (und strenggenommen funktioniert Tabellenindizierung sowieso über Vektorindizierung). Man nehme folgendes Beispiel:

sleep[sleep$extra > 3, ]
   extra group ID
6    3.4     1  6
7    3.7     1  7
16   4.4     2  6
17   5.5     2  7
19   4.6     2  9
20   3.4     2 10

Das heißt: “Nimm die Tabelle sleep und gib mir alle Zeilen (das mit den eckigen Klammen), für die die Variable sleep$extra größer als 3 ist”.

Das Ergebnis eines logischen Vergleichs ist immer entweder TRUE oder FALSE für wahr oder falsch.
Wenn wir in R indizieren wollen, können wir dafür auch direkt TRUE und FALSE statt eines Vergleichs benutzen:

fib <- c(1, 1, 2, 3, 5, 8, 13, 21) 

fib[c(TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE)]
[1] 1 1

Hier haben wir uns effektiv nur die ersten beiden Werte des Vektors fib ausgeben lassen, weil R alles ausgibt, was mit TRUE indiziert ist und alles weglässt, was mit FALSE indiziert ist.
c(TRUE, TRUE, FALSE, FALSE, FALSE, FALSE, FALSE, FALSE) ist hier ein logischer Vektor, also ein Vektor mit, naja, logischen Werten, der praktisch der Reihe nach jedes Element im Vektor fib entweder an oder aus schaltet, wie Lichtschalter. Wir müssen dafür nicht unbedingt einen logischen Vektor der gleichen Länge (Anzahl der Elemente) wie unser Zielvektor (der, den wir indizieren/filtern wollen) benutzen, aber es bietet sich für den Einstieg an so genau wie möglich zu sein.
Wir könnten aber auch sowas machen:

# Immer abwechselnd TRUE und FALSE, also jedes zweite Element
fib[c(TRUE, FALSE)]
[1]  1  2  5 13
# Schema TRUE TRUE FALSE FALSE ginge auch
fib[c(TRUE, TRUE, FALSE, FALSE)]
[1] 1 1 5 8

Wichtig hierbei ist, dass der logische Vektor ein ganzer Faktor des Zielvektors ist, das heißt, dass die Anzahl der Element im Zielvektor ganz durch die Anzahl der Elemente im logischen Vektor teilbar sein muss (also ohne Rest), ansonsten bekommen wir potenziell schwer vorhersagbare Ergebnisse.
Was R hier macht nennt sich Recycling: In der ersten Zeile im letzten Beispiel wird der Vektor c(TRUE, FALSE) solange recyclet, also wiederverwendet, bis der ganze Zeilvektor “abgedeckt” ist. Wenn der logische Vektor nicht sauber in den Zielvektor passt (in Bezug auf die Anzahl der Elemente), dann bleibt entweder was übrig oder es reicht nicht. Das wäre schade.

4.5.1 Operatoren

Es gibt eine Reihe logischer Operatoren wie hier > für “ist größer als”, die wichtigsten in Übersicht:

  • == (doppeltes Gleichheitszeichen ohne Leerzeichen dazwischen)
    • “Ist gleich”
    • 1 == 2 –> FALSE
    • 3 == 3 –> TRUE
    • 3 == "Hallo" –> FALSE
  • !=
    • “ist ungleich”
    • Die Negation von ==, also immer da wo == euch TRUE zeigt, gibt != euch FALSE und andersherum.
    • pi != 3 –> TRUE
    • "Psychologiestudium" != "Voll gute Idee"
  • ! (ja, ein einfaches Ausrufezeichen)
    • “Negation”
    • Dreht ein FALSE zu einem TRUE um und andersherum. Wichtig: Klammern!
    • !(2 == 3)
    • TRUE == !FALSE
  • > und < (spitze Klammern)
    • “ist größer/kleiner als”
    • Da wo die Klamer spitz ist, soll das kleinere sein
    • 5 > 4 –> TRUE
    • 2^10 < 1000 –> FALSE
    • 2 < 5 < 4 –> Funktioniert nicht!
  • >=, <=
    • “Größer gleich bzw. kleiner gleich”
    • Ist Entweder a > b oder a == b?
    • 5 >= 4
    • 16 <= 2^4

Diese Ausdrücke können wir auch auf bestimmte Arten verknüpfen:

  • & (oder auch &&)
    • “Und”
    • Ist TRUE, wenn beide Seiten TRUE sind
    • (1 < 3) & (5 < 10) –> TRUE
    • (5 < 2) & (2 < 10) –> FALSE
    • 1 und 0 werden zu TRUE bzw. FALSE übersetzt:
      • 1 & (2 == 2) –> TRUE
      • !(0 & FALSE) –> TRUE
  • | (oder auch ||)
    • “Oder”
    • Ist entweder A oder B oder beides TRUE?
    • Da | auch wahr ist, wenn nur eine Seite wahr ist, ist es auch Grundlage etlicher Mathe-/Logikwitze
    • (1 < 3) | (5 < 10) –> TRUE
    • (5 < 2) | (2 < 10) –> TRUE
  • xor()
    • “Entweder … oder …” (ausschließend!)
    • Wenn euch | zu unspezifisch ist
    • Ist nur war, wenn eins von beidem wahr ist, aber nicht, wenn beides wahr ist
    • Kein binärer Operator wie die anderen, sondern eine R-Funktion mit Klammern und so
    • xor(TRUE, FALSE) –> TRUE
    • xor(TRUE, TRUE) –> FALSE
    • xor((1 < 3), (5 < 10)) –> FALSE
    • xor((5 < 2), (2 < 10)) –> TRUE

4.5.2 Spezielle Tests

Die obigen Operatoren können wir für getrost für Vektorvergleiche benutzen, aber es gibt noch ein paar Sonderfälle. Was zum Beispiel, wenn wir nur generell wissen wollen, ob ein Element wie eine Zahl oder ein String in einem Vektor enthalten ist? Oder was, wenn wir wir auf spezielle Typen oder Klassen testen wollen? Was das im Detail heißt sehen wir in den entsprechenden Abschnitten zu Datentypen noch einmal, aber hier schonmal eine Kurzreferenz:

  • %in% (auch hier, ohne Leerzeichen dazwischen!)
    • “Ist in”
    • Mengentheoretisch ist das \(x \in X\)
    • Ist Element a in Menge b?
    • 5 %in% c(1, 4, 5, 3) –> TRUE
    • "B" %in% c("a", "b", "c") –> FALSE
    • "B" %in% c("a", "B", "c") –> TRUE
    • c(1, 2) %in% 1:5 –> TRUE
  • is.na(): Testet auf fehlende Werte (missing values, NA)
  • is.null(): Testet auf leere Werte (NULL)
  • is.nan(): Testet auf NaN (Not a Number)

4.5.3 Indexing: Beispiele

Das war jetzt relativ viel Information, und ihr müsst euch das auch nicht alles sofort merken, sondern nur wissen, wo ihr’s bei Bedarf nachschlagen könnt.
Die Motivation hinter dem Logikram ist wie erwähnt primär das Filtern von Tabellen und Vektoren, was wir nunmal relativ häufig brauchen um zum Beispiel bestimmte Untergruppen in unseren Datensätze zu analysieren, zum Beispiel Personen älter als 35 (z.B. age > 35) oder Menschen, die sowohl weiblich sind als auch Medikament B bekommen haben (z.B. geschlecht == "weiblich" & drug == "B").

4.5.3.1 Vektoren

Vektoren werden immer elementweise verglichen, das heißt, dass das Ergebnis von c(1, 2) == 1 nicht FALSE oder TRUE ist, sondern der logische Vektor TRUE FALSE. Dadurch entsteht durch den logischen Vergleich eines Vektors ein Vektor aus TRUE und FALSE, den wir zum indizieren benutzen können, wie wir weiter oben schon gesehen haben.

# Irgendwelche Zahlen
x <- c(4, 7, 2, 1, 7, 9, 6, 5, 4, 3, 3, 2, 2, 5, 8, 9, 31)

# Alle Zahlen größer 4: "Gib mir x, wo x > 4"
x[x > 4]
[1]  7  7  9  6  5  5  8  9 31
# Die Logik dahinter
x > 4
 [1] FALSE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE
[12] FALSE FALSE  TRUE  TRUE  TRUE  TRUE
# Alle Zahlen größer 5 und kleiner 20
x[x > 5 & x < 20]
[1] 7 7 9 6 8 9

Das Ganze lässt sich natürlich beliebig komplex aussehen lassen, weshalb zu viele Bedingungen in Kombination etwas verwirrend aussehen können.
Weiterhin können wir einen Vektor natürlich auch durch einen Vergleich eines anderen Vektors indizieren.

x <- c(4, 7, 2, 1, 7, 9, 6, 5, 4, 3)
y <- c(6, 7, 10, 1, 9, 3, 6, 5, 6, 3)

# x, wo x größer gleich 4 ist *und* y kleiner 6
x[x >= 4 & (y < 6)]
[1] 9 5
# x, wo x und y identisch sind
x[x == y]
[1] 7 1 6 5 3
# x, wo x kleiner y ist
x[x < y]
[1] 4 2 7 4

4.5.3.2 Tabellen data.frame

Tabellenindexing ist nichts anderes als Vektorindexing mit einer anderen Struktur. Alle Regeln zum Vektorindexing, die wir bisher gesehen haben, gelten auch so für data.frames, nur dass wir hier jetzt auf einmal in Spalten und Zeilen denken müssen, anstatt in Vektoren.


  1. Money quote: “[…] the reason we use <- for assignment is it made sense in a programming language written before the incorporation of Apple Computers, because it made sense in a programming language written before the moon landings.”