Chapter 3 Erste Schritte in R
Nach den (wichtigen) Vorbereitungsschritten im vorangehenden Kapitel wollen wir an dieser Stelle mit dem eigentlichen Programmieren anfangen. Zu diesem Zweck müssen wir uns mit der Syntax von R vertraut machen, also mit den Regeln, denen wir folgen müssen, wenn wir Code schreiben, damit der Computer versteht, was wir ihm eigentlich in R sagen wollen.
Das Kapitel ist dabei folgendermaßen aufgebaut: zunächst lernen wir in Abschnitt 3.1 wie wir mit Hilfe von R und R-Studio mit unserem Computer ‘kommunizieren’ können. Danach lernen wir in Abschnitt 3.2 nicht nur die zentralen Elemente der Programmiersprache R kennen, nämlich Objekte und Funktionen, sondern lernen auch wie wir solche Objekte erstellen und ihnen Namen zuweisen können. Ein großer Teil des Kapitels (Abschnitt 3.3) ist dann den unterschiedlichen Arten von Objekten in R gewidmet. Wir lernen zum Beispiel wie sich Vektoren von Listen unterscheiden, und welche Pendants in R es zu ‘Zahlen’ und ‘Wörtern’ in unserer Alltagssprache gibt. Abgeschlossen wir das Kapitel mit einer kurzen Einführung von ‘Paketen’ in Abschnitt 3.4. Pakete sind ein wichtiger Bestandteil der Open-Source Sprache R: hier handelt es sich um Code, den andere Menschen geschrieben und der Allgemeinheit frei zugänglich gemacht haben. Dadurch ist sichergestellt, dass R immer auf dem neuesten Stand der Forschung und Praxis ist.
3.1 Befehle in R an den Computer übermitteln
Grundsätzlich können wir über R Studio auf zwei Arten mit dem Computer “kommunizieren”: über die Konsole direkt, oder indem wir im Skriptbereich ein Skript schreiben und dies dann ausführen.
Als Beispiel für die erste Möglichkeit wollen wir mit Hilfe von R die Zahlen
2
und 5
miteinander addieren.
Zu diesem Zweck können wir einfach 2 + 5
in die Konsole eingeben, und den
Befehl mit ‘Enter’ an den Computer senden.
Da es sich beim Ausdruck 2 + 5
um korrekten R Code handelt, ‘versteht’ der
Computer was wir von ihm wollen und gibt uns das entsprechende Ergebnis aus:
2 + 5
#> [1] 7
Die Zeichenkombination #>
am Beginn der Zeile zeigt an, dass es sich bei
dieser Zeile um den Output eines R-Befehlt handelt. Das kann bei Ihrem
Computer durchaus anders aussehen.
Das Ergebnis von 2+5
ist eine Zahl (genauer: ein ‘Skalar’).
In R werden Skalare immer als Vektor der Länge 1 dargestellt.
Die [1]
gibt also an, dass hier ein Vektor der Länge 1 angezeigt wird.
Wäre das Ergebnis unserer Berechnung ein Vektor der Länge 2 würde die
Outputzeile dementsprechend mit #> [2]
eingeleitet werden.
Auf diese Art und Weise können wir R als einfachen Taschenrechner verwenden,
denn für alle einfachen mathematischen Operationen können wir bestimmte Symbole
als Operatoren verwenden.
An dieser Stelle sei noch darauf hingewiesen, dass das Symbol #
in R einen
Kommentar einleitet, das heißt alles was in einer Zeile nach #
steht wird
vom Computer ignoriert und man kann sich an dieser Stelle Notizen im Code machen.
2 + 5 # Addition
#> [1] 7
2/2 # Division
#> [1] 1
4*2 # Multiplikation
#> [1] 8
3**2 # Potenzierung
#> [1] 9
Alternativ können wir die Befehle in einem Skript aufschreiben, und dieses Skript
dann ausführen. Während die Interaktion über die Konsole sinnvoll ist um die
Effekte bestimmter Befehle auszuprobieren, bietet sich die Verwendung von
Skripten an, wenn wir mit den Befehlen später weiter arbeiten wollen, oder sie
anderen Menschen zugänglich zu machen. Denn das Skript können wir als Datei auf
unserem Computer speichern, vorzugsweise im Unterordner R
unseres R-Projekts
(siehe Abschnitt Relevante Unterordner erstellen), und dann
später weiterverwenden.
Die Berechnungen, die wir bislang durchgeführt haben sind zugegebenermaßen nicht sonderlich spannend. Um fortgeschrittene Operationen in R durchführen und verstehen zu können müssen wir uns zunächst mit den Konzepten von Objekten, Funktionen und Zuweisungen beschäftigen.
3.2 Objekte, Funktionen und Zuweisungen
To understand computations in R, two slogans are helpful: Everything that exists is an object. Everything that happens is a function call.
Mit der Aussage ‘Alles in R ist ein Objekt’ ist gemeint, dass jede Zahl, jede Funktion, oder jeder Buchstabe in R ein Objekt ist, das irgendwo auf dem Speicher Ihres Rechners abgespeichert ist.
In der Berechnung 2 + 3
ist die Zahl 2
genauso ein Objekt wie die Zahl 3
und die Additionsfunktion, die durch den Operator +
aufgerufen wird.
Mit der Aussage ‘Alles was in R passiert ist ein Funktionsaufruf’ ist gemeint, dass wir R eine Berechnung durchführen lassen indem wir eine Funktion aufrufen.
Funktionen sind Algorithmen, die bestimmte Routinen auf einen Input
anwenden und dabei einen Output produzieren.
Die Additionsfunktion, die wir in der Berechnung 2 + 3
aufgerufen haben hat
als Input die beiden Zahlen 2
und 3
aufgenommen, hat auf sie die Routine
der Addition angewandt und als Output die Zahl 5
ausgegeben.
Der Output 5
ist dabei in R genauso ein Objekt wie die Inputs 2
und 3
,
sowie die Funktion +
.
Ein ‘Problem’ ist, dass R im vorliegenden Fall den Output der Berechnung zwar ausgibt, wir danach aber keinen Zugriff darauf mehr haben:
2 + 3
#> [1] 5
Falls wir den Output weiterverwenden wollen, macht es Sinn, dem Output Objekt
einen Namen zu geben, damit wir später wieder darauf zugreifen können.
Der Prozess einem Objekt einen Namen zu geben wird Zuweisung oder
Assignment genannt und durch die Funktion assign
vorgenommen:
assign("zwischenergebnis", 2 + 3)
Wir können nun das Ergebnis der Berechnung 2 + 3
aufrufen, indem wir in R
den Namen des Output Objekts eingeben:
zwischenergebnis
#> [1] 5
Da Zuweisungen so eine große Rolle spielen und sehr häufig vorkommen gibt es
auch für die Funktion assign
eine Kurzschreibweise, nämlich <-
.
Entsprechend sind die folgenden beiden Befehle äquivalent:
assign("zwischenergebnis", 2 + 3)
<- 2 + 3 zwischenergebnis
Daher werden wir Zuweisungen immer mit dem <-
Operator durchführen.4
Exkurs: warum
<-
? Die Verwendung der Zeichenkette<-
als Zuweisungsoperator ist auf den ersten Blick unintuitiv und in der Welt der Programmiersprachen ziemlich einzigartig. Viel weiter verbreitet ist die Verwendung von=
. Woher kommt das? Neben praktischen Gründen -<-
macht im Gegensatz zu=
z.B. die Unidirektionalität einer Zuweisung besser deutlich - hat das vor allem historische Ursachen:R
entstand aus der ProgrammierspracheS
. Diese wiederum hat das<-
von der Sprache APL übernommen. Und die wiederum wurde auf einem Tastaturlayout entwickelt, wo es eine Taste für<-
gab. Zudem war damals der Operator==
noch nicht verbreitet. Dementsprechend war=
bereits als Test auf Gleichheit vergeben. Und so hatte man sich für die Zuweisung also für<-
entschieden. In R kann man eine Zuweisung seit 2001 auch mit=
vorgenommen werden, aber aufgrund der besseren Lesbarkeit und einiger technischer Spitzfindigkeiten sollte<-
weiterhin der Standard im Bereich der Zuweisung bleiben.
Wir können in R nicht beliebig Namen vergeben. Gültige (also: syntaktisch korrekte) Namen
- enthalten nur Buchstaben, Zahlen und die Symbole
.
und_
- fangen nicht mit
.
oder einer Zahl an!
Zudem gibt es einige Wörter, die schlicht nicht als Name verwendet werden
dürfen, z.B. function
, TRUE
, oder if
. Die gesamte Liste verbotener Worte
kann mit dem Befehl ?Reserved
ausgegeben werden.
Wenn man einen Namen vergeben möchte, der nicht mit den gerade formulierten Regeln kompatibel ist, gibt R eine Fehlermeldung aus:
TRUE <- 5
#> Error in TRUE <- 5: invalid (do_set) left-hand side to assignment
Zudem sollte man Folgendes beachten:
- Namen sollten kurz und informativ sein; entsprechend ist
sample_mean
ein guter Name,vector_2
dagegen eher weniger - Man sollte nie Umlaute in Namen verwenden
- R ist case sensitive, d.h.
mean_value
ist ein anderer Name alsMean_Value
- Auch wenn möglich, sollte man nie von R bereit gestellte Funktionen
überschreiben. Eine Zuweisung wie
assign <- 2
ist zwar möglich, führt in der Regel aber zu großem Unglück, weil man nicht mehr ganz einfach auf die zugrundeliegende Funktion zurückgreifen kann.
Hinweis: Alle aktuellen Namenszuweisungen sind im Bereich
Environment
in R Studio (Nr. 4 in Abbildung 2.4 oben) aufgelistet und können durch die Funktionls()
angezeigt werden.
Hinweis: Ein Objekt kann mehrere Namen haben, aber kein Name kann zu mehreren Objekten zeigen, da im Zweifel eine neue Zuweisung die alte Zuweisung überschreibt:
<- 2
x <- 2 # Das Objekt 2 hat nun zwei Namen
y print(x)
#> [1] 2
print(y)
#> [1] 2
<- 4 # Der Name 'x' zeigt nun zum Objekt '4', nicht mehr zu '2'
x print(x)
#> [1] 4
Hinweis: Wie Sie vielleicht bereits bemerkt haben wird nach einer Zuweisung kein Wert sichtbar ausgegeben:
2 + 2 # Keine Zuweisung, R gibt das Ergebnis in der Konsole aus
#> [1] 4
<- 2 + 2 # Zuweisung, R gibt das Ergebnis in der Konsole nicht aus x
An dieser Stelle wollen wir das bisher Gelernte kurz zusammenfassen:
- Wir können Befehle in R Studio an den Computer übermitteln indem wir (a) den R Code in die Konsole schreiben und Enter drücken oder (b) den Code in ein Skript schreiben und dann ausführen
- Alles was in R existiert ist ein Objekt, alles was in R passiert ist ein Funktionsaufruf
- Wir können einem Objekt mit Hilfe von
<-
einen Namen geben und dann später wieder aufrufen. Den Prozess der Namensgebung nennen wir Assignment und wir können uns alle aktuell von uns vergebenen Namen mit der Funktionls()
anzeigen lassen - Eine Funktion ist ein Objekt, das auf einen Input eine bestimmte Routine anwendet und einen Output produziert
An dieser Stelle sei noch auf die Hilfefunktion help()
hingewiesen.
Über diese können Sie weitere Informationen über ein Objekt bekommen. Wenn Sie
z.B. genauere Informationen über die Verwendung der Funktion assign
erhalten
wollen, können Sie Folgendes eingeben:
help(assign)
3.3 Grundlegende Objeke in R
Wir haben bereits gelernt, dass alles was in R existiert ein Objekt ist.
Wir haben aber auch schon gelernt, dass es unterschiedliche Typen von Objekten
gibt: Zahlen, wie 2
oder 3
und Funktionen wie assign
.5
Tatsächlich gibt es noch viel mehr Arten von Objekten.
Ein gutes Verständnis der Objektarten ist Grundvoraussetzung um später
anspruchsvolle Programmieraufgaben zu lösen.
Daher wollen wir uns im Folgenden mit den wichtigsten Objektarten in R
auseinandersetzen.
3.3.1 Funktionen
Wie oben bereits kurz erwähnt handelt es sich bei Funktionen um Algorithmen, die bestimmte Routinen auf einen Input anwenden und dabei einen Output produzieren.
Die Funktion log()
zum Beispiel nimmt als Input eine Zahl und gibt als
Output den Logarithmus dieser Zahl aus:
log(2)
#> [1] 0.6931472
Eine Funktion aufrufen
In R gibt es prinzipiell vier verschiedene Arten Funktionen aufzurufen. Nur zwei davon sind allerdings aktuell für uns relevant.
Die bei weitem wichtigste Variante ist die so genannte Prefix-Form.
Dies ist die Form, die wir bei der überwältigenden Anzahl von Funktionen
verwenden werden.
Wir schreiben hier zunächst den Namen der Funktion (im Folgenden Beispiel
assign
), dann in Klammern und mit Kommata getrennt die Argumente der Funktion
(hier der Name test
und die Zahl 2
):
assign("test", 2)
Eine hin und wieder auftretende Form ist die sogenannte Infix-Form.
Hier wird der Funktionsname zwischen die Argumente geschrieben.
Dies ist, wie wir oben bereits bemerkt haben, bei vielen mathematischen
Funktionen wie +
, -
oder /
der Fall.
Streng genommen ist die Infix-Form aber nur eine Abkürzung, denn jeder
Funktionsaufruf in Infix-Form kann auch in Prefix-Form geschrieben werden, wie
folgendes Beispiel zeigt:
2 + 3
#> [1] 5
`+`(2,3)
#> [1] 5
Die Argumente einer Funktion
Die Argumente einer Funktion stellen zum einen den Input für die in der Funktion implementierte Routine dar.
Die Funktion sum
zum Beispiel nimmt als Argumente eine beliebige Anzahl an
Zahlen (ihr ‘Input’) und berechnet die Summe dieser Zahlen:
sum(1,2,3,4)
#> [1] 10
Darüber hinaus akzeptiert sum()
noch ein optionales Argument, na.rm
,
welches entweder den Wert TRUE
oder FALSE
annehmen kann. Die Buchstaben na
stehen hier für “not available”, und bezeichnen fehlende Werte. Wenn wir das
Argument nicht explizit spezifizieren nimmt es automatisch FALSE
als den Standardwert an.
Dieses optionale Argument ist kein klassischer Input, sondern kontrolliert das
genaue Verhalten der Funktion. Wenn na.rm
den Wert TRUE
hat, dann werden im
Falle von sum()
die fehlende Werte, also die NA
, ignoriert bevor die Summe
der Inputs gebildet wird:
sum(1,2,3,4,NA)
#> [1] NA
sum(1,2,3,4,NA, na.rm = TRUE)
#> [1] 10
Wenn wir wissen wollen, welche Argumente eine Funktion akzeptiert, ist es immer
eine gute Idee über die Funktion help()
einen Blick in die Dokumentation zu
werfen!
Im Falle von sum()
sehen wir hier sofort, dass die Funktion neben den zu
addierenden Zahlen ein optionales Argument na.rm
akzeptiert, welches den
Standardwert FALSE
annimmt.
Eigene Funktionen definieren
Sehr häufig möchten wir selbst Funktionen definieren.
Das können wir mit dem reservierten Keyword function
machen.
Als Beispiel wollen wir eine Funktion pythagoras
definieren, die als Argumente
die Seitenlängen der Katheten eines rechtwinkligen Dreiecks annimmt und über den
Satz des Pythagoras
die Länge der Hypothenuse bestimmt:
<- function(kathete_1, kathete_2){
pythagoras <- kathete_1**2 + kathete_2**2
hypo_quadrat <- sqrt(hypo_quadrat) # sqrt() zieht die Quadratwurzel
hypothenuse return(hypothenuse)
}
Wir definieren eine Funktion durch die Funktion function()
.
In der Regel beginnen wir die Definition indem wir die zu erstellende Funktion
mit einem Namen assoziieren (hier: ‘pythagoras’) damit wir sie später auch
verwenden können.
Die Argumente für function
sind dann die Argumente, welche die zu definierende
Funktion annehmen soll, in diesem Fall kathete_1
und kathete_2
.
Danach beginnen wir den ‘function body’, also den Code für die Routine, welche
die Funktion ausführen soll, mit einer geschweiften Klammer.
Innerhalb des function bodies wird dann die entsprechende Routine implementiert.
Im vorliegenden Beispiel definieren wir zunächst die Summe der Werte von
kathete_1
und kathete_2
als ein Zwischenergebnis, welches hier hypo_quadrat
genannt wird.
Dies ist der häufig unter \(c^2=a^2 + b^2\) bekannte Teil des Satz von Pythagoras.
Da wir an der ‘normalen’ Länge der Hypothenuse interesssiert sind, ziehen wir
mit der Funktion sqrt()
noch die Wurzel von hypo_quadrat
, und geben dem
resultierenden Objekt den Namen hypothenuse
, welches in der letzten Zeile
mit Hilfe des Keywords return
als der Wert definiert wird, den die Funktion
als Output ausgibt.6
Am Ende der Routine kann man mit dem Keyword return
explizit machen welchen
Wert die Funktion als Output ausgeben soll.
Wenn wir die Funktion nun aufrufen wird die oben definierte Routine ausgeführt:
pythagoras(2, 4)
#> [1] 4.472136
Beachten Sie, dass alle Objektnamen, die innerhalb des function bodies verwendet
werden, nach dem Funktionsaufruf verloren gehen, weil Funktionen
ihr eigenes environment haben.
Deswegen kommt es im vorliegenden Falle zu einem Fehler, da hypo_quadrat
nur
innerhalb des function bodies existiert:
<- function(kathete_1, kathete_2){
pythagoras <- kathete_1**2 + kathete_2**2
hypo_quadrat <- sqrt(hypo_quadrat) # sqrt() zieht die Quadratwurzel
hypothenuse return(hypothenuse)
}<- pythagoras(2, 4)
x hypo_quadrat
#> Error in eval(expr, envir, enclos): object 'hypo_quadrat' not found
Es ist immer eine gute Idee, die selbst definierten Funktionen zu dokumentieren - nicht nur wenn wir sie auch anderen zur Verfügung stellen wollen, sondern auch damit wir selbst nach einer möglichen Pause unseren Code noch gut verstehen können. Nichts ist frustrierender als nach einer mehrwöchigen Pause viele Stunden investieren zu müssen, den eigens programmierten Code zu entschlüsseln!
Die Dokumentation von Funktionen kann mit Hilfe von einfachen Kommentaren
erfolgen, ich empfehle jedoch sofort sich die
hier beschriebenen Konventionen
anzugewöhnen. In diesem Falle würde eine Dokumentation unserer Funktion pythagoras
folgendermaßen aussehen:
#' Berechne die Länge der Hypothenuse in einem rechtwinkligen Dreieck
#'
#' Diese Funktion nimmt als Argumente die Längen der beiden Katheten eines
#' rechtwinkligen Dreiecks und berechnet daraus die Länge der Hypothenuse.
#' @param kathete_1 Die Länge der ersten Kathete
#' @param kathete_2 Die Länge der zweiten Kathete
#' @return Die Länge der Hypothenuse des durch a und b definierten
#' rechtwinkligen Dreieckst
<- function(kathete_1, kathete_2){
pythagoras <- kathete_1**2 + kathete_2**2
hypo_quadrat <- sqrt(hypo_quadrat) # sqrt() zieht die Quadratwurzel
hypothenuse return(hypothenuse)
}
Die Dokumentation wird also direkt vor die Definition der Funktion gesetzt. In der ersten Zeile gibt man der Funktion einen maximal einzeiligen Titel, der nicht länger als 80 Zeichen sein sollte und die Funktion prägnant beschreibt.
Dann, nach einer Leerzeile wird genauer beschrieben was die Funktion macht.
Danach werden die Argumente der Funktion beschrieben. Für jedes Argument
beginnen wir die Reihe mit @param
, gefolgt von dem Namen des Arguments und
dann einer kurzen Beschreibung.
Nach den Argumenten beschreiben wir noch kurz was der Output der Funktion ist.
Diese Zeile wird mit @return
begonnen.
Die Dokumentation einer Funktion sollte also zumindest die Parameter und die Art des Outputs erklären.
Gründe für die Verwendung eigener Funktionen
Eigene Funktionen zu definieren ist in der Praxis extrem hilfreich und es ist empfehlenswert Routinen, die mehrere Male verwendet werden grundsätzlich als Funktionen zu schreiben. Dafür gibt es mehrere Gründe:
- Der Code wird kürzer und transparenter. Zwar ist kurzer Code nicht notwendigerweise leichter zu verstehen als langer, aber Funktionen können besonders gut dokumentiert werden (am besten indem man den hier beschriebenen Konventionen folgt).
- Funktionen bieten Struktur. Funktionen fassen in der Regel Ihre Vorstellung davon zusammen, wie ein bestimmtes Problem zu lösen ist. Da man sich diese Gedanken nicht ständig neu machen möchte ist es sinnvoll sie einmalig in einer Funktion zusammenzufassen.
- Funktionen erleichtern Korrekturen. Wenn Sie merken, dass Sie in der Implementierung einer Routine einen Fehler gemacht haben müssen Sie im besten Falle nur einmal die Definition der Funktion korrigieren - im schlimmsten Falle müssen Sie in Ihrem Code nach der Routine suchen und sie in jedem einzelnen Anwendungsfall erneut korrigieren.
Es gibt noch viele weitere Gründe dafür, Funktionen häufig zu verwenden. Viele hängen mit dem Entwicklerprinzip DRY (“Don’t Repeat Yourself”) zusammen.
3.3.2 Vektoren
Vektoren sind einer der wichtigsten Objektypen in R. Quasi alle Daten mit denen wir in R arbeiten werden als Vektoren behandelt.
Was Vektoren angeht gibt es wiederum die wichtige Unterscheidung von atomaren Vektoren und Listen. Beide bestehen ihrerseits aus Objekten und sie unterscheiden sich dadurch, dass atomare Vektoren nur aus Objekten des gleichen Typs bestehen können, Listen dagegen auch Objekte unterschiedlichen Typs beinhalten können.
Entsprechend kann jeder atomare Vektor einem Typ zugeordnet werden, je nachdem welchen Typ seine Bestandteile haben. Hier sind insbesondere vier Typen relevant:
logical
(logische Werte): es gibt zwei logische Werte,TRUE
undFALSE
, welche auch mitT
oderF
abgekürzt werden könneninteger
(ganze Zahlen): das sollte im Prinzip selbsterklärend sein, allerding muss den ganzen Zahlen in R immer der BuchstabeL
folgen, damit die Zahl tatsächlich als ganze Zahl interpretiert wird.7 Beispiele sind1L
,400L
oder10L
.
double
(Dezimalzahlen): auch das sollte selbsterklärend sein; Beispiele wären1.5
,0.0
, oder-500.32
.- Ganze Zahlen und Dezimalzahlen werden häufig unter der Kategorie
numeric
zusammengefasst. Dies ist in der Praxis aber quasi nie hilfreich und man sollte diese Kategorie möglichst nie verwenden. character
(Wörter): sie sind dadurch gekennzeichnet, dass sie auch Buchstaben enthalten können und am Anfang und Ende ein"
haben. Beispiele hier wären"Hallo"
,"500"
oder"1_2_Drei"
.- Es gibt noch zwei weitere besondere ‘Typen’, die strikt gesehen keine
atomaren Vektoren darstellen, allerdings in diesem Kontext schon häufig
auftauchen:
NULL
, was strikt genommen ein eigener Datentyp ist und immer die Länge 0 hat, sowieNA
, das einen fehlenden Wert darstellt.
Hieraus ergibt sich die in Abbildung 3.1 aufgezeigte Aufteilung von Vektoren.
Wir werden nun die einzelnen Typen genauer betrachten.
Vorher wollen wir jedoch noch die Funktion typeof
einführen.
Sie hilft uns in der Praxis den Typ eines Objekts herauszufinden.
Dafür rufen wir einfach die Funktion typeof
mit dem zu untersuchenden Objekt
oder dessen Namen auf:
typeof(2L)
#> [1] "integer"
<- 22.0
x typeof(x)
#> [1] "double"
Wir können auch explizit testen ob ein Objekt tatsächlich ein Objekt eines bestimmten Typs ist.
Die generelle Syntax hierfür ist: is.*()
, also z.B.:
<- 1.0
x is.integer(x)
#> [1] FALSE
is.double(x)
#> [1] TRUE
Diese Funktion gibt als Output also immer einen logischen Wert aus, je nachdem ob die Inputs des entsprechenden Typs sind oder nicht.
Bestimmte Objekte können auch in einen anderen Typ transformiert werden.
Hier spricht man von coercion
und die generelle Syntax hierfür ist:
as.*()
, also z.B.:
<- "2"
x print(
typeof(x)
)
#> [1] "character"
<- as.double(x)
x print(
typeof(x)
)
#> [1] "double"
Allerdings ist eine Transformation nicht immer möglich:
as.double("Hallo")
#> [1] NA
Da R nicht weiß wie man aus dem Wort ‘Hallo’ eine Dezimalzahl machen soll,
transformiert R das Wort in einen ‘Fehlenden Wert’, der in R als NA
bekannt ist und unten noch genauer diskutiert wird.
Für die Grundtypen ergibt sich folgende logische Hierachie an trivialen
Transformationen: logical
→ integer
→ double
→ character
,
d.h. man kann eine Dezimalzahl ohne Probleme in ein Wort transformieren,
aber nicht umgekehrt:
Exkurs: Warum überhaupt transformieren? Für eine Programmiersprache sind Datentypen extrem wichtig, weil sonst unklar bliebe wie mathematische Operationen auf unterschiedliche Objekte wie Zahlen oder Wörter anzuwenden wären. Selbst transformieren werden Sie Objekte vor allem wenn Sie eine bestimmte, nur für eine bestimmte Objektart definierte Operation verwenden wollen und das Objekt bislang als ein anderer Typ gespeichert ist. Das kann zum Beispiel passieren wenn Sie Daten einlesen oder Wörter selbst in Zahlenwerte übersetzen. Wenn in Ihrem Code unerwartete Fehler mit kryptischen Fehlermeldungen auftauchen ist es immer eine gute Idee, erst einmal die Typen der verwendeten Objekte zu checken und die Objekte ggf. zu transformieren.
<- 2
x <- as.character(x)
y print(y)
#> [1] "2"
<- as.double(y) # Das funktioniert
z print(z)
#> [1] 2
<- as.double("Hallo") # Das nicht
k print(k)
#> [1] NA
Bei der Transformation logischer Werte wird TRUE
übrigens zu 1
und FALSE
zu 0
, eine Tatsache, die wir uns später noch zunutze machen werden:
<- TRUE
x as.integer(x)
#> [1] 1
<- FALSE
y as.integer(y)
#> [1] 0
Da nicht immer ganz klar ist wann R bei Transformationen entgegen der gerade eingeführten Hierachie eine Warnung ausgibt und wann nicht sollte man hier immer besondere Vorsicht walten lassen!
Zudem ist bei jeder Transformation Vorsicht geboten, da sie häufig Eigenschaften der Objekte implizit verändert. So führt eine Transformation von einer Dezimalzahl hin zu einer ganzen Zahl teils zu unerwartetem Rundungsverhalten:
<- 1.99
x as.integer(x)
#> [1] 1
Auch führen Transformationen, die der eben genannten Hierachie zuwiderlaufen, nicht zwangsweise zu Fehlern, sondern ‘lediglich’ zu unerwarteten Änderungen, die in jedem Fall vermieden werden sollten:
<- as.logical(99)
z print(z)
#> [1] TRUE
Häufig transformieren Funktionen ihre Argumente automatisch, was meistens hilfreich ist, manchmal aber auch gefährlich sein kann:
<- 1L # Integer
x <- 2.0 # Double
y <- x + y
z typeof(z)
#> [1] "double"
Bei einer Addition werden logische Werte ebenfalls automatisch transformiert:
<- TRUE
x <- FALSE
y <- x + y # TRUE wird zu 1, FALSE zu 0
z print(z)
#> [1] 1
Daher sollte man immer den Überblick behalten, mit welchen Objekttypen man gerade arbeitet.
Einen Überblick zu den Test- und Transformationsbefehlen finden Sie in Tabelle 3.1.
Typ | Test | Transformation |
---|---|---|
logical | is.logical |
as.logical |
double | is.double |
as.double |
integer | is.integer |
as.integer |
character | is.character |
as.character |
function | is.function |
as.function |
NA | is.na |
NA |
NULL | is.null |
as.null |
Ein letzter Hinweis zu Skalaren.
Unter Skalaren verstehen wir in der Regel ‘einzelne Zahlen’, z.B. 2
.
Dieses Konzept gibt es in R nicht.
2
ist ein Vektor der Länge 1.
Wir unterscheiden also vom Typ her nicht zwischen einem Vektor, der nur ein oder
mehrere Elemente hat.
Hinweis: Um längere Vektoren zu erstellen, verwenden wir die Funktion c()
:
<- c(1, 2, 3)
x x
#> [1] 1 2 3
Dabei können Vektoren auch miteinander verbunden werden:
<- 1:3 # Shortcut für: x <- c(1, 2, 3)
x <- 4:6
y <- c(x, y)
z z
#> [1] 1 2 3 4 5 6
Da atomare Vektoren immer nur Objekte des gleichen Typs enthalten, könnte man erwarten, dass es zu einem Fehler kommt, wenn wir Objete unterschiedlichen Type kombinieren wollen:
<- c(1, "Hallo") x
Tatsächlich transformiert R die Objekte allerdings nach der oben beschriebenen
Hierachie logical
→ integer
→ double
→ character
.
Da hier keine Warnung oder kein Fehler ausgegeben wird, sind
derlei Transformationen eine gefährliche Fehlerquelle!
Hinweis: Die Länge eines Vektors kann mit der Funktion length
bestimmt
werden:
= c(1, 2, 3)
x <- length(x)
len_x len_x
#> [1] 3
Exkurs: Wie groß kann eine ganze Zahl sein? In R werden
integer
als 32-Bit Daten gespeichert. Das bedeutet, dass für einen einzelneninteger
32 Bit Speicherplatz zur Verfügung steht. Das bedeutet, dass sehr große ganze Zahlen nicht alsinteger
gespeichert werden können, einfach weil die 32 Bit nicht ausreichen.
<- 2147483647L
x typeof(x)
#> [1] "integer"
<- 2147483648L
y typeof(y)
#> [1] "double"
Mit 32-Bit Integern kann man also maximal die Zahl 2147483647 speichern. Größere Zahlen können nur als
double
gespeichert werden. Das geht allerdings möglicherweise mit einem Verlust an Präzision einher. Wenn man letzteres vermeiden möchte kann man auch 64-Bit Integer verwenden. Diese wurden nachträglich in R eingeführt um auch sehr große Zahlen als Integer speichern zu können. Das geht über das Paketbit64
(mehr zu Paketen unten in Abschnit 3.4):
<- bit64::as.integer64(2147483648)
z ::is.integer64(z) bit64
#> [1] TRUE
Da aber dieser Datentyp später hinzugefügt wurde funktionieren einige Funktionen damit nicht, wenn das Paket
bit64
nicht installiert ist und einige Standard- Funktionen geben irreführende Ergebnisse, z.B.:
typeof(z)
#> [1] "double"
Deswegen, und weil
bit64
nicht Teil der Standard-Installation von R ist, sollten wir sehr große ganze Zahlen und die Verwendung vom Datentypinteger64
dringend vermeiden, es sei denn wir haben gute Gründe dazu. Sehr große ganze Zahlen sollten also entweder als Dezimalzahlen gespeichert werden wenn kleine Verluste an Präzision keine Rolle spielen, oder aber sie sollten angemessen skaliert werden.
3.3.3 Logische Werte (logical)
Die logischen Werte TRUE
und FALSE
sind häufig das Ergebnis von logischen
Abfragen, z.B. ‘Ist 2 größer als 1?’.
Solche Abfragen kommen in der Forschungspraxis häufig vor und es macht Sinn,
sich mit den häufigsten logischen Operatoren vertraut zu machen.
Einen Überblick finden Sie in Tabelle 3.2.
Operator | Funktion in R | Beispiel |
---|---|---|
größer | > |
2>1 |
kleiner | < |
2<4 |
gleich | == |
4==3 |
größer gleich | >= |
8>=8 |
kleiner gleich | <= |
5<=9 |
nicht gleich | != |
4!=5 |
und | & |
x<90 & x>55 |
oder | | |
x<90 | x>55 |
entweder oder | xor() |
xor(2<1, 2>1) |
nicht | ! |
!(x==2) |
ist wahr | isTRUE() |
isTRUE(1>2) |
Das Ergebnis eines solches Tests ist immer ein logischer Wert:
<- 4
x <- x == 8
y typeof(y)
#> [1] "logical"
Es können auch längere Vektoren getestet werden:
<- 1:3
x <2 x
#> [1] TRUE FALSE FALSE
Tests können beliebig miteinander verknüpft werden:
<- 1L
x >2 | x<2 & (is.double(x) & x!=0) x
#> [1] FALSE
Da für viele mathematischen Operationen TRUE
als die Zahl 1
interpretiert
wird, ist es einfach zu testen wie häufig eine bestimmte Bedingung erfüllt ist:
<- 1:50
x <- x<20
smaller_20 print(
sum(smaller_20) # Wie viele Elemente sind kleiner als 20?
)
#> [1] 19
print(
sum(smaller_20/length(x)) # Wie hoch ist der Anteil von diesen Elementen?
)
#> [1] 0.38
3.3.4 Wörter (character)
Wörter werden in R dadurch gebildet, dass an ihrem Anfang und Ende das Symbol
'
oder "
steht:
<- "Hallo"
x typeof(x)
#> [1] "character"
<- 'Auf Wiedersehen'
y typeof(y)
#> [1] "character"
Wie andere Vektoren können sie mit der Funktion c()
verbunden werden:
<- c(x, "und", y)
z z
#> [1] "Hallo" "und" "Auf Wiedersehen"
Nützlich ist in diesem Zusammenhang die Funktion paste()
, die Elemente von
mehreren Vektoren in Wörter transformiert und verbindet:
<- 1:10
x <- paste("Versuch Nr.", x)
y y
#> [1] "Versuch Nr. 1" "Versuch Nr. 2" "Versuch Nr. 3" "Versuch Nr. 4"
#> [5] "Versuch Nr. 5" "Versuch Nr. 6" "Versuch Nr. 7" "Versuch Nr. 8"
#> [9] "Versuch Nr. 9" "Versuch Nr. 10"
Die Funktion paste()
akzeptiert ein optionales Argument sep
, mit dem wir
den Wert angeben können, der zwischen die zu verbindenden Elemente gesetzt wird
(der Default ist sep=" "
):
<- 1:10
tag_nr <- paste("Tag", tag_nr, sep = ": ")
x_axis x_axis
#> [1] "Tag: 1" "Tag: 2" "Tag: 3" "Tag: 4" "Tag: 5" "Tag: 6" "Tag: 7"
#> [8] "Tag: 8" "Tag: 9" "Tag: 10"
Hinweis: Hier haben wir ein Beispiel für das sogenannte ‘Recycling’ gesehen: da der Vektor
c("Tag")
kürzer war als der Vektortag_nr
wirdc("Tag")
einfach kopiert damit die Operation mitpaste()
Sinn ergibt. Recycling ist oft praktisch, aber manchmal auch schädlich, nämlich dann, wenn man eigentlich davon ausgeht eine Operation mit zwei gleich langen Vektoren durchzuführen, dies aber tatsächlich nicht tut. In einem solchen Fall führt Recycling dazu, dass keine Fehlermeldung ausgegeben wird. Ein Beispiel dafür gibt folgender Code, in dem die Intention klar die Verbindung aller Wochentage zu Zahlen ist und einfach ein Wochentag vergessen wurde:
<- paste("Tag ", 1:7, ":", sep="")
tage <- c("Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag")
tag_namen paste(tage, tag_namen) # default ist sep=" "
#> [1] "Tag 1: Montag" "Tag 2: Dienstag" "Tag 3: Mittwoch"
#> [4] "Tag 4: Donnerstag" "Tag 5: Freitag" "Tag 6: Samstag"
#> [7] "Tag 7: Montag"
3.3.5 Fehlende Werte und NULL
Fehlende Werte werden in R als NA
kodiert.
NA
erfüllt gerade in statistischen Anwendungen eine wichtige Rolle, da ein
bestimmter Platz in einem Vektor aktuell fehlend sein müsste, aber als Platz
dennoch existieren muss.
Beispiel: Der Vektor
x
enthält einen logischen Wert, der zeigt ob eine Person die Fragen auf einem Fragebogen richtig beantwortet hat. Wenn die Person die dritte Frage auf dem Fragebogen nicht beantwortet hat, sollte dies durchNA
kenntlich gemacht werden. Einfach den Wert komplett wegzulassen macht es im Nachhinein unmöglich festzustellen welche Frage die Person nicht beantwortet hat.
Die meisten Operationen die NA
als einen Input bekommen geben auch als
Output NA
aus, weil unklar ist wie die Operation mit unterschiedlichen Werten
für den fehlenden Wert ausgehen würde:
5 + NA
#> [1] NA
Einzige Ausnahmen sind Operationen, die unabhängig vom fehlenden Wert einen bestimmten Wert annehmen:
NA | TRUE # Gibt immer TRUE, unabhängig vom Wert für NA
#> [1] TRUE
Um zu testen ob ein Vektor x
fehlende Werte enthält sollte die Funktion is.na
verwendet werden, und nicht etwa der Ausdruck x==NA
:
<- c(NA, 5, NA, 10)
x print(x == NA) # Unklar, da man nicht weiß, ob alle NA für den gleichen Wert stehen
#> [1] NA NA NA NA
print(
is.na(x)
)
#> [1] TRUE FALSE TRUE FALSE
Wenn eine Operation einen nicht zu definierenden Wert ausgibt, ist das Ergebnis
nicht NA
sondern NaN
(not a number):
0 / 0
#> [1] NaN
Eine weitere Besonderheit ist NULL
, welches in der Regel als Vektor der
Länge 0 gilt, aber häufig zu besonderen Zwecken verwendet wird:
<- NULL
x length(x)
#> [1] 0
NULL
wird häufig verwendet um zu signalisieren, dass etwas nicht existiert.
So ist ein leerer Vektor NULL
:
<- c()
x x
#> NULL
length(x)
#> [1] 0
Damit unterscheidet er sich von einem Vektor mit einem (oder mehreren) fehlenden Werten:
<- NA
y length(y)
#> [1] 1
Auch im Programmieren von Funktionen wird NULL
häufig für optionale Argumente
verwendet.
Solche fortgeschrittene Konzepte werden aber erst an späterer Stelle behandelt.
Für jetzt reicht die Idee, NULL
als einen Vektor der Länge 0 zu verstehen.
3.3.6 Indizierung und Ersetzung
Einzelne Elemente von atomaren Vektoren können mit eckigen Klammern extrahiert werden:
<- c(2,4,6)
x 1] x[
#> [1] 2
Auf diese Weise können auch bestimmte Elemente modifiziert werden:
<- c(2,4,6)
x 2] <- 99
x[ x
#> [1] 2 99 6
Es kann auch mehr als ein Element extrahiert werden:
1:2] x[
#> [1] 2 99
Negative Indizes sind auch möglich, diese eliminieren die entsprechenden Elemente:
-1] x[
#> [1] 99 6
Um das letzte Element eines Vektors zu bekommen verwendet man einen Umweg über
die Funktion length()
:
length(x)] x[
#> [1] 6
3.3.7 Nützliche Funktionen für atomare Vektoren
Hier sollen nur einige Funktionen erwähnt werden, die im Kontext von atomaren Vektoren besonders praktisch sind,8 inbesondere wenn es darum geht solche Vektoren herzustellen, bzw. Rechenoperationen mit ihnen durchzuführen.
Herstellung von atomaren Vektoren:
Eine Sequenz ganzer Zahlen wird in der Regel sehr häufig gebraucht.
Entsprechend gibt es den hilfreichen Shortcut:
, den wir bei der Besprechung
von Vektoren bereits kennengelernt haben:
<- 1:10
x x
#> [1] 1 2 3 4 5 6 7 8 9 10
<- 10:1
y y
#> [1] 10 9 8 7 6 5 4 3 2 1
Häufig möchten wir jedoch eine kompliziertere Sequenz bauen.
In dem Fall hilft uns die allgemeinere Funktion seq()
:
<- seq(1, 10)
x print(x)
#> [1] 1 2 3 4 5 6 7 8 9 10
In diesem Fall ist seq()
äquivalent zu :
.
Die Funktion seq
erlaubt aber mehrere optionale Argumente: so können wir
mit by
die Schrittlänge zwischen den einzelnen Zahlen definieren.
<- seq(1, 10, by = 0.5)
y print(y)
#> [1] 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0 7.5 8.0
#> [16] 8.5 9.0 9.5 10.0
Wenn wir die Länge des resultierenden Vektors festlegen wollen und die
Schrittlänge von R automatisch festgelegt werden soll, können wir dies mit
dem Argument length.out
machen:
<- seq(2, 8, length.out = 4)
z print(z)
#> [1] 2 4 6 8
Und wenn wir einen Vektor in der Länge eines anderen Vektors erstellen wollen,
bietet sich das Argument along.with
an. Dies wird häufig für das Erstellen
von Indexvektoren verwendet.9
In einem solchen Fall müssen wir die Indexzahlen
nicht direkt angeben:
<- seq(along.with = z)
z_index print(z_index)
#> [1] 1 2 3 4
Auch häufig möchten wir einen bestimmten Wert wiederholen.
Das geht mit der Funktion rep
:
<- rep(NA, 5)
x print(x)
#> [1] NA NA NA NA NA
Rechenoperationen
Es gibt eine Reihe von Operationen, die wir sehr häufig gemeinsam mit Vektoren
anwenden.
Häufig interessiert und die Länge eines Vektors.
Dafür können wir die Funktion length()
verwenden:
<- c(1,2,3,4)
x length(x)
#> [1] 4
Wenn wir den größten oder kleinsten Wert eines Vektors erfahren möchten
geht das mit den Funktionen min()
und max()
:
min(x)
#> [1] 1
max(x)
#> [1] 4
Beide Funktionen besitzen ein optionales Argument na.rm
, das entweder TRUE
oder FALSE
sein kann. Im Falle von TRUE
werden alle NA
Werte für die
Rechenoperation entfernt:
<- c(1,2,3,4,NA)
y min(y)
#> [1] NA
min(y, na.rm = TRUE)
#> [1] 1
Den Mittelwert bzw die Varianz/Standardabweichung der Elemente bekommen
wir mit mean()
, var()
, bzw. sd()
, wobei alle Funktionen auch das optionale
Argument na.rm
akzeptieren:
mean(x)
#> [1] 2.5
var(y)
#> [1] NA
var(y, na.rm = T)
#> [1] 1.666667
Ebenfalls häufig sind wir an der Summe, bzw, dem Produkt aller Elemente
des Vektors interessiert. Die Funktionen sum()
und prod()
helfen weiter und
auch sie kennen das optionale Argument na.rm
:
sum(x)
#> [1] 10
prod(y, na.rm = T)
#> [1] 24
3.3.8 Listen
Im Gegensatz zu atomaren Vektoren können Listen Objekte verschiedenen Typs
enthalten.
Sie werden mit der Funktion list()
erstellt:
<- list(
l_1 "a",
c(1,2,3),
FALSE
)typeof(l_1)
#> [1] "list"
l_1
#> [[1]]
#> [1] "a"
#>
#> [[2]]
#> [1] 1 2 3
#>
#> [[3]]
#> [1] FALSE
Wir können Listen mit der Funktion str()
(kurz für “structure”) inspizieren.
In diesem Fall erhalten wir unmittelbar Informationen über die Art der Elemente:
str(l_1)
#> List of 3
#> $ : chr "a"
#> $ : num [1:3] 1 2 3
#> $ : logi FALSE
Die einzelnen Elemente einer Liste können auch benannt werden:
<- list(
l_2 "erstes_element" = "a",
"zweites_element" = c(1,2,3),
"drittes_element" = FALSE
)
Die Namen aller Elemente in der Liste erhalten wir mit der Funktion names()
:
names(l_2)
#> [1] "erstes_element" "zweites_element" "drittes_element"
Um einzelne Elemente einer Liste auszulesen müssen wir [[
anstatt [
verwenden.
Wir können dann Elemente entweder nach ihrer Position oder nach ihren Namen auswählen:
1]] l_2[[
#> [1] "a"
"erstes_element"]] l_2[[
#> [1] "a"
Im Folgenden wollen wir uns noch mit drei speziellen Typen beschäftigen, die weniger fundamental als die bislang diskutierten Typen sind, jedoch häufig in der alltäglichen Arbeit vorkommen: Faktoren, Matrizen und Data Frames.
3.3.9 Faktoren
Faktoren werden verwendet um ordinale oder kategoriale Daten darzustellen.
Ein Faktor kann nur einen von mehreren vorher definierten Werten annehmen, so
genannten Levels.
Faktoren werden über die Funktion factor()
erstellt.
Sie nimmt als erstes Argument die Werte für den Faktor:
<- c("Frau", "Mann", "Frau")
x <- factor(c("Frau", "Mann", "Frau"))
x x
#> [1] Frau Mann Frau
#> Levels: Frau Mann
Wenn wir Levels definieren wollen, die aber aktuell noch keine Ausprägung
haben können wir dies mit dem Argument levels
bewerkstelligen:
<- c("Frau", "Mann", "Frau")
x <- factor(c("Frau", "Mann", "Frau"),
x levels=c("Divers","Frau", "Mann"))
x
#> [1] Frau Mann Frau
#> Levels: Divers Frau Mann
Wenn wir das Argument levels
verwenden werden dort nicht genannte
Ausprägungen den Wert NA
erhalten:
<- c("Frau", "Mann", "Frau")
x <- factor(c("Frau", "Mann", "Frau", "Divers"),
x levels=c("Frau", "Mann"))
x
#> [1] Frau Mann Frau <NA>
#> Levels: Frau Mann
Die Reihenfolge der einzelnen Levels spielt meist keine Rolle.
Bei ordinalen Daten möchten wir aber eine sinnvolle Wertigkeit der
Ausprägungen sicherstellen.
Das geht mit der Funktion factor()
und dem Argument ordered
:
<- c("Hoch", "Hoch", "Gering", "Hoch")
x <- factor(x,
x levels = c("Gering", "Mittel", "Hoch"),
ordered = TRUE)
x
#> [1] Hoch Hoch Gering Hoch
#> Levels: Gering < Mittel < Hoch
Häufig handelt es sich bei den Ausprägungen von Faktoren um Wörter, also
Objekte vom Type character
.
Technisch gesehen werden Faktoren aber als integer
gespeichert: um
Speicherplatz zu sparen wird jedem Level auf dem Computer eine ganze Zahl
zugewiesen, die dann auf den eigentlichen Wert gemapt wird. Gerade wenn die
Ausprägungen als solche große Zahlen oder lange Wörter sind spart das Speicher,
weil diese Ausprägungen nur einmal gespeichert werden müssen, und jedes Element
des Fakors nur noch eine einfache Zahl ist.
Daher gibt typeof()
für Faktoren auch integer
aus:
<- factor(c("Frau", "Mann", "Frau"),
x levels=c("Mann", "Frau", "Divers"))
typeof(x)
#> [1] "integer"
Um zu überprüfen ob es sich bei einem Objekt um einen Faktor handelt verwenden
wir die Funktion is.factor()
:
is.factor(x)
#> [1] TRUE
Manche Operationen, die für integer
definiert sind, funktionieren bei Faktoren
aber nicht, z.B. Addition:
1] + x[2] x[
#> [1] NA
Dafür können wir andere nützliche Dinge mit Faktoren anstellen, z.B. die
absoluten Häufigkeiten über die Funktion table()
anzeigen:
table(x)
#> x
#> Mann Frau Divers
#> 1 2 0
Faktoren werden vor allem in der Arbeit mit ordinalen und kategorialen Daten verwendet (siehe Kapitel 4).
3.3.10 Matrizen
Bei Matrizen handelt es sich um zweidimensionale Objekte mit Zeilen und Spalten, bei denen es sich jeweils um atomare Vektoren handelt.
Erstellen von Matrizen
Matrizen werden mit der Funktion matrix()
erstellt.
Diese Funktion nimmt als erstes Argument die Elemente der Matrix und dann
die Spezifikation der Anzahl von Zeilen (nrow
) und/oder der Anzahl von
Spalten (ncol
):
<- matrix(11:20, nrow = 5)
m_1 m_1
#> [,1] [,2]
#> [1,] 11 16
#> [2,] 12 17
#> [3,] 13 18
#> [4,] 14 19
#> [5,] 15 20
Wir können die Zeilen und Spalten sowie einzelne Werte folgendermaßen extrahieren und gegebenenfalls Ersetzungen vornehmen:
1] # Erste Spalte m_1[,
#> [1] 11 12 13 14 15
1,] # Erste Zeile m_1[
#> [1] 11 16
2,2] # Element [2,2] m_1[
#> [1] 17
Optionaler Hinweis: Matrizen sind weniger ‘fundamental’ als atomare Vektoren. Entsprechend gibt uns
typeof()
für eine Matrix auch den Typ der enthaltenen atomaren Vektoren an:
typeof(m_1)
#> [1] "integer"
Um zu testen ob es sich bei einem Objekt um eine Matrix handelt verwenden wir entsprechend
is.matrix()
:
is.matrix(m_1)
#> [1] TRUE
is.matrix(2.0)
#> [1] FALSE
Die Grundlagen der Matrizenalgebra und ihre Implementierung in R wird später in Kapitel 6 erläutert. Zudem gibt es im Internet zahlreiche gute Überblicksartikel zum Thema Matrizenalgebra in R, z.B. hier oder in größerem Umfang hier.
3.3.11 Data Frames
Der data.frame
ist eine besondere Art von Liste und ist ein in der
Datenanalyse regelmäßig auftretender Datentyp.
Im Gegensatz zu einer normalen Liste müssen bei einem data.frame
alle Elemente
die gleiche Länge aufweisen.
Das heißt man kann sich einen data.frame
als eine rechteckig angeordnete Liste
vorstellen.
Wegen der engen Verwandschaft können wir einen data.frame
direkt aus einer Liste
erstellen indem wir die Funktion as.data.frame()
verwenden:
<- list(
l_3 "a" = 1:3,
"b" = 4:6,
"c" = 7:9
)<- as.data.frame(l_3) df_3
Wenn wir R nach dem Typ von df_3
fragen, sehen wir, dass es sich weiterhin um
eine Liste handelt:
typeof(df_3)
#> [1] "list"
Allerdings können wir testen ob df_3
ein data.frame
ist indem wir
is.data.frame
benutzen:
is.data.frame(df_3)
#> [1] TRUE
is.data.frame(l_3)
#> [1] FALSE
Wenn wir df_3
ausgeben sehen wir unmittelbar den Unterschied zur klassischen
Liste:
l_3
#> $a
#> [1] 1 2 3
#>
#> $b
#> [1] 4 5 6
#>
#> $c
#> [1] 7 8 9
df_3
#> a b c
#> 1 1 4 7
#> 2 2 5 8
#> 3 3 6 9
Die andere Möglichkeit einen data.frame
zu erstellen ist direkt über die
Funktion data.frame()
, wobei es hier in der Regel ratsam ist das optionale
Argument stringsAsFactors
auf FALSE
zu setzen, da sonst Wörter in so
genannte Faktoren umgewandelt werden:10
<- data.frame(
df_4 "gender" = c(rep("male", 3), rep("female", 2)),
"height" = c(189, 175, 180, 166, 150),
stringsAsFactors = FALSE
) df_4
#> gender height
#> 1 male 189
#> 2 male 175
#> 3 male 180
#> 4 female 166
#> 5 female 150
Data Frames sind das klassische Objekt um eingelesene Daten zu repräsentieren.
Wenn Sie sich z.B. Daten zum BIP in Deutschland aus dem Internet runterladen und
diese Daten dann in R einlesen, werden diese Daten zunächst einmal als data.frame
repräsentiert.11
Diese Repräsentation erlaubt dann eine einfache Analyse und Manipulation der Daten.
Zwar gibt es ein eigenes Kapitel zur Bearbeitung von Daten (siehe Kapitel 4), wir wollen aber schon hier einige zentrale Befehle im Zusammenhang von Data Frames einführen.
An dieser Stelle sei schon angemerkt, dass um Zeilen, Spalten oder einzelne Elemente auszuwählen die gleichen Befehle wie bei Matrizen verwendet werden können:
1] # Werte der ersten Spalte df_4[,
#> [1] "male" "male" "male" "female" "female"
2] # Werte der zweiten Spalte df_4[,
#> [1] 189 175 180 166 150
Die Abfrage funktioniert nicht nur mit Indices, sondern auch mit Spaltennamen:12
"gender"]] df_4[[
#> [1] "male" "male" "male" "female" "female"
Wenn wir [
anstatt von [[
verwenden erhalten wir als Output einen (reduzierten)
Data Frame:
"gender"] df_4[
#> gender
#> 1 male
#> 2 male
#> 3 male
#> 4 female
#> 5 female
Es können auch mehrere Zeilen ausgewählt werden:
1:2, ] # Die ersten beiden Zeilen df_4[
#> gender height
#> 1 male 189
#> 2 male 175
Oder einzelne Werte:
2, 2] # Zweiter Wert der zweiten Spalte df_4[
#> [1] 175
Dies können wir uns zu Nutze machen um den Typ der einzelnen Spalten herauszufinden:
typeof(df_4[["gender"]])
#> [1] "character"
Gerade bei sehr großen Data Frames möchte man oft nur die ersten paar Zeilen
inspizieren.
Das ist mit der Funktion head()
möglich.
Das erste Argument ist immer der Name des Data Frames.
Das zweite (optionale) Argument ist ein integer
, der die Anzahl der
anzuzeigenden Zeilen angibt (Standardwert: 5
):
head(df_4, 2) # gibt die ersten zwei Zeilen aus
#> gender height
#> 1 male 189
#> 2 male 175
3.4 Pakete
Bei Paketen handelt es sich um eine Kombination aus R Code, Daten, Dokumentationen und Tests. Sie sind der beste Weg, reproduzierbaren Code zu erstellen und frei zugänglich zu machen. Zwar werden Pakete häufig der Öffentlichkeit zugänglich gemacht, z.B. über GitHub oder CRAN, es ist aber genauso hilfreich, Pakete für den privaten Gebrauch zu schreiben, z.B. um für bestimmte Routinen Funktionen zu programmieren, zu dokumentieren und in verschiedenen Projekten verfügbar zu machen.13
Die Tatsache, dass viele Menschen statistische Probleme lösen indem sie bestimmte Routinen entwickeln, diese dann generalisieren und über Pakete der ganzen R Community frei verfügbar machen, ist einer der Hauptgründe für den Erfolg und die breite Anwendbarkeit von R.
Wenn man R startet haben wir Zugriff auf eine gewisse Anzahl von Funktionen,
vordefinierten Variablen und Datensätzen. Die Gesamtheit dieser Objekte wird
in der Regel base R
genannt, weil wir alle Funktionalitäten ohne Weiteres
nutzen können.
Die Funktion assign
, zum Beispiel, ist Teil von base R
: wir starten R und
können sie ohne Weiteres verwenden.
Im Prinzip kann so gut wie jedwede statistische Prozedur in base R
implementiert
werden. Dies ist aber häufig zeitaufwendig und fehleranfällig: wie wir am
Beispiel von Funktionen gelernt haben, sollten häufig verwendete Routinen im
Rahmen von einer Funktion implementiert werden, die dann immer wieder angewendet
werden kann. Das reduziert nicht nur Fehler, sondern macht den Code besser
verständlich.
Pakete folgen dem gleichen Prinzip, nur tragen sie die Idee noch weiter: hier wollen wir die Funktionen auch über ein einzelnes R Projekt hinaus nutzbar machen, sodass sie nicht in jedem Projekt neu definiert werden müssen, sondern zentral nutzbar gemacht und dokumentiert werden.
Um ein Paket in R zu nutzen, muss es zunächst installiert werden.
Für Pakete, die auf der zentralen R Pakete Plattform CRAN verfügbar sind,
geht dies mit der Funktion install.packages
.
Wenn wir z.B. das Paket data.table
installieren wollen geht das mit dem
folgenden Befehl:
install.packages("data.table")
Das Paket data.table
enthält viele Objekte, welche die Arbeit mit großen
Datensätzen enorm erleichtern. Beispielsweise ist darunter eine verbesserte Version des
data.frame
, der data.table
. Wir können einen data.frame
mit Hilfe der
Funktion as.data.table()
in einen data.table
umwandeln.
Allerdings haben wir selbst nach erfolgreicher Installation von data.table
nicht direkt Zugriff auf diese Funktion:
<- data.frame(
x a=1:5,
b=21:25
)as.data.table(x)
#> Error in as.data.table(x): could not find function "as.data.table"
Wir haben zwei Möglichkeiten auf die Objekte im Paket data.table
zuzugreifen:
zum einen können wir mit dem Operator ::
arbeiten:
<- data.table::as.data.table(x)
y y
#> a b
#> 1: 1 21
#> 2: 2 22
#> 3: 3 23
#> 4: 4 24
#> 5: 5 25
Wir schreiben also den Namen des Pakets, direkt gefolgt von ::
und dann den
Namen des Objekts aus dem Paket, das wir verwenden wollen.
Zwar ist das der transparenteste und sauberste Weg auf Objekte aus anderen
Paketen zuzugreifen, allerdings kann es auch nervig sein wenn man häufig oder
sehr viele Objekte aus dem gleichen Paket verwendet.
Wir können alle Objekte eines Paketes direkt zugänglich machen indem wir die
Funktion library()
verwenden.
library(data.table)
<- as.data.table(x) y
Der Übersicht halber sollte das für alle in einem Skript verwendeten Pakete ganz am Anfang des Skripts gemacht werden. So sieht man auch unmittelbar welche Pakete für das Skript installiert sein müssen.
Grundsätzlich sollte man in jedem Skript nur die Pakete mit library()
einlesen,
die auch tatsächlich verwendet werden. Ansonsten lädt man unnötigerweise viele
Objekte und verliert den Überblick woher eine bestimmte Funktion eigentlich
kommt. Außerdem ist es schwieriger für andere das Skript zu verwenden, weil
unter Umständen viele Pakete unnötigerweise installiert werden müssen.
Da Pakete dezentral von verschiedensten Menschen hergestellt werden, besteht die Gefahr, dass Objekte in unterschiedlichen Paketen den gleichen Namen bekommen. Da in R ein Name nur zu einem Objekt gehören kann, werden beim Einladen mehrerer Pakete eventuell Namen überschrieben, oder ‘maskiert’. Dies wird am Anfang beim Einlesen der Pakete mitgeteilt, gerät aber leicht in Vergessenheit und kann zu sehr kryptischen Fehlermeldungen führen.
Wir wollen das kurz anhand der beiden Pakete dplyr
und plm
illustrieren:
library(dplyr)
library(plm)
In beiden Paketen gibt es Objekte mit den Namen between
, lag
und lead
.
Bei der Verwendung von library
maskiert das später eingelesene Paket die
Objekte des früheren.
Wir können das illustrieren indem wir den Namen des Objekts eingeben:
lead
#> function (x, k = 1, ...)
#> {
#> UseMethod("lead")
#> }
#> <bytecode: 0x7fc748025300>
#> <environment: namespace:plm>
Aus der letzten Zeile wird ersichtlich, dass lead
hier aus dem Paket plm
kommt.
Wenn wir die Funktion aus dplyr
verwenden wollen, müssen wir ::
verwenden:
::lead dplyr
#> function (x, n = 1L, default = NA, order_by = NULL, ...)
#> {
#> if (!is.null(order_by)) {
#> return(with_order(order_by, lead, x, n = n, default = default))
#> }
#> if (length(n) != 1 || !is.numeric(n) || n < 0) {
#> bad_args("n", "must be a nonnegative integer scalar, ",
#> "not {friendly_type_of(n)} of length {length(n)}.")
#> }
#> if (n == 0)
#> return(x)
#> xlen <- vec_size(x)
#> n <- pmin(n, xlen)
#> inputs <- vec_cast_common(default = default, x = x)
#> vec_c(vec_slice(inputs$x, -seq_len(n)), vec_rep(inputs$default,
#> n))
#> }
#> <bytecode: 0x7fc7485548d0>
#> <environment: namespace:dplyr>
Wenn es zu Maskierungen kommt ist es also der Transparenz wegen besser in beiden
Fällen ::
zu verwenden, also plm::lead
und dplyr::lead
.
Hinweis: Alle von Konflikten betroffenen Objekte können mit der Funktion
conflicts()
angezeigt werden.
Optionale Info: Um zu überprüfen in welcher Reihenfolge R nach Objekten sucht, kann die Funktion
search()
verwendet werden. Wenn ein Objekt aufgerufen wird schaut R zuerst im ersten Element des Vektors nach, der globalen Umgebung. Wenn das Objekt dort nicht gefunden wird, schaut es im zweiten, etc. Wie man hier auch erkennen kann, werden einige Pakete standardmäßig eingelesen. Wenn ein Objekt nirgends gefunden wird gibt R einen Fehler aus. Im vorliegenden Fall zeigt uns die Funktion, dass R erst im Paketplm
nach der Funktionlead()
sucht, und nicht im Paketdplyr
:
search()
#> [1] ".GlobalEnv" "package:plm" "package:dplyr"
#> [4] "package:data.table" "package:bit64" "package:bit"
#> [7] "package:tufte" "package:stats" "package:graphics"
#> [10] "package:grDevices" "package:datasets" "renv:shims"
#> [13] "package:utils" "package:methods" "Autoloads"
#> [16] "package:base"
Weiterführender Hinweis: Um das Maskieren besser zu verstehen sollte man sich mit dem Konzept von namespaces und environments auseinandersetzen. Eine gute Erklärung bietet Wickham und Bryan (2019).
Weiterführender Hinweis: Das Paket
conflicted
führt dazu, dass R immer einen Fehler ausgibt wenn nicht eindeutige Objektnamen verwendet werden.
Der besseren Transparenz wegen wird in diesem Buch ab jetzt immer die Notation
mit ::
verwendet, auch wenn dies nicht unbedingt nötig wäre.
So sehen Sie bei jedem Code-Beispiel unmittelbar aus welchem Paket die
verwendeten Funktionen stammen.
Lediglich bei den Basispaketen werden wir auf ::
verzichten.
Theoretisch kann
<-
auch andersherum verwendet werden:2 + 3 -> zwischenergebnis
. Das mag zwar auf den ersten Blick intuitiver erscheinen, da das aus2 + 3
resultierende Objekt den Namenzwischenergebnis
bekommt, also immer erst das Objekt erstellt wird und dann der Name zugewiesen wird. Es führt jedoch in der Regel zu deutlich weniger gut lesbarem Code und sollte daher nie verwendet werden.↩︎Wie wir unten lernen werden sind
2
und3
in erster Linie keine Zahlen, sondern Vektoren der Länge 1, und gelten erst in nächster Instanz als ‘Zahl’ (genauer: ‘double’).↩︎Das ist strikt genommen nicht notwendig, aber der Übersichtlichkeit werden wir immer
return
verwenden. Eine interessante Debatte darüber ob manreturn
verwenden sollte oder nicht findet sich hier.↩︎Diese auf den ersten Blick merkwürdige Syntax hat historische Gründe: als der integer Typ in die R Programmiersprache eingeführt wurde war er sehr stark an den Typ
long integer
in der Programmiersprache ‘C’ angelehnt. In C wurde ein solcher ‘long integer’ mit dem Suffix ‘l’ oder ‘L’ definiert, diese Regel wurde aus Kompatibilitätsgründen auch für R übernommen, jedoch nur mit ‘L’, da man Angst hatte, dass ‘l’ mit ‘i’ verwechselt wird, was in R für die imaginäre Komponente komplexer Zahlen verwendet wird.↩︎Für viele typische Aufgaben gibt es in R bereits eine vordefinierte Funktion. Am einfachsten findet man diese durch googlen.↩︎
Ein Indexvektor
x
zu einem beliebigen Vektory
mitN
Elementen enthält die ganzen Zahlen von 1 bisN
. Der n-te Wert von x korrespontiert also zum Index des n-ten Wert vony
.↩︎Zur Geschichte dieses wirklich ärgerlichen Verhaltens siehe diesen Blog. Zwar wurde das Standardverhalten mit R 4.0 umgestellt, allerdings empfiehlt sich die explizite Setzung von
stringsAsFactors=F
trotzdem, damit der Code auch mit älteren Versionen gut funktioniert.↩︎Das ist nicht ganz korrekt, weil es mittlerweilse Erweiterungen gibt, welche den
data.frame
mit effizienteren Objekten ersetzen, z.B. demtibble
oder demdata.table
. Der Umgang mit diesen Objekten ist jedoch sehr ähnlich zumdata.frame
.↩︎Anstelle von
[[
kann auch der Shortcut$
verwendet werden. Das werden wir aufgrund der größeren Transparenz von[[
hier jedoch nicht verwenden.↩︎Wickham und Bryan (2019) bietet eine exzellente Einführung in das Programmieren von R Paketen.↩︎