Beruflich Dokumente
Kultur Dokumente
Andreas Baierl
Statistisches
Programmieren mit R
Eine ausführliche, übersichtliche,
spannende und praxiserprobte Einführung
Statistisches Programmieren mit R
Lizenz zum Wissen.
Sichern Sie sich umfassendes Technikwissen mit Sofortzugriff auf
tausende Fachbücher und Fachzeitschriften aus den Bereichen:
Automobiltechnik, Maschinenbau, Energie + Umwelt, E-Technik,
Informatik + IT und Bauwesen.
www.ATZonline.de
Automobiltechnische Zeitschrift
03
März 2012 | 114. Jahrgang
03
FormoPtimierung in der
Fahrzeugentwicklung
/// BEGEGNUNGEN
Walter Reithmaier
TÜV Süd Automotive
/// INTERVIEW
Claudio Santoni
McLaren
Jetzt
PersPektive Leichtbau
Werkstoffe optimieren
Optimale Energiebilanz
im Lackierprozess
30 Tage
testen!
issn 0001-2785 10810
Statistisches
Programmieren mit R
Eine ausführliche, übersichtliche,
spannende und praxiserprobte Einführung
Daniel Obszelka Andreas Baierl
Institut für Statistik und Operations Research Institut für Statistik und Operations Research
Universität Wien Universität Wien
Wien, Österreich Wien, Österreich
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detail
lierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung, die nicht
ausdrücklich vom Urheberrechtsgesetz zugelassen ist, bedarf der vorherigen Zustimmung des Verlags.
Das gilt insbesondere für Vervielfältigungen, Bearbeitungen, Übersetzungen, Mikroverfilmungen und die
Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Wiedergabe von allgemein beschreibenden Bezeichnungen, Marken, Unternehmensnamen etc. in diesem
Werk bedeutet nicht, dass diese frei durch jedermann benutzt werden dürfen. Die Berechtigung zur Benutzung
unterliegt, auch ohne gesonderten Hinweis hierzu, den Regeln des Markenrechts. Die Rechte des jeweiligen
Zeicheninhabers sind zu beachten.
Der Verlag, die Autoren und die Herausgeber gehen davon aus, dass die Angaben und Informationen in
diesem Werk zum Zeitpunkt der Veröffentlichung vollständig und korrekt sind. Weder der Verlag, noch
die Autoren oder die Herausgeber übernehmen, ausdrücklich oder implizit, Gewähr für den Inhalt des
Werkes, etwaige Fehler oder Äußerungen. Der Verlag bleibt im Hinblick auf geografische Zuordnungen und
Gebietsbezeichnungen in veröffentlichten Karten und Institutionsadressen neutral.
Einladung an dich
Was können wir tun, um dein Lesevergnügen zu maximieren und dir die zahlreichen
Konzepte der statistischen Programmierung möglichst verständlich zu vermitteln?
Diese beiden Fragen haben wir uns nicht nur einmal gestellt.
Aber der Reihe nach: Wir freuen uns, dass du unser Buch gerade in Händen hältst
(oder am Computerbildschirm geöffnet hast) und in R durchstarten willst! Mit R
wählst du eine Programmiersprache, die sich hervorragend für statistische Analysen
und die Erstellung von Grafiken eignet.
Dieses Buch ist eine ausführliche Einführung in R und ist für dich besonders gut
geeignet, wenn
• du neben Standardproblemen auch die Fertigkeit erwerben willst, komplexere
Probleme lösen zu können.
• du großen Wert auf eine übersichtliche Aufbereitung und ausführliche Erklä-
rungen legst.
• du R und grundlegende statistische Programmierkonzepte wirklich verstehen
willst.
• du dich nicht mit oberflächlichen Erklärungen und unkommentierten Pro-
grammcodes zufrieden gibst.
• dir nachhaltiges Wissen wichtig ist.
Seit vielen Jahren haben wir die große Freude, Studierende in die spannende und
herausfordernde Welt der statistischen Programmierung mit R einzuführen! Bis heu-
te haben über 200 Studierende mit diversen Versionen dieses Buches (als Skriptum)
R gelernt und wir haben uns über die zahlreichen positiven und konstruktiven Rück-
meldungen sehr gefreut! Höhepunkt unserer gemeinsamen Lehre war der Gewinn des
Teaching Awards der Universität Wien im Jahr 2017.
Unsere Studierenden waren und sind vom Vorwissen her sehr heterogen. Manche
haben bisher kaum Erfahrungen mit Computern gesammelt, andere wiederum be-
reits in der Schule oder anderorts Programmieren gelernt. Wir waren stets mit der
Herausforderung konfrontiert erstere Gruppe nicht zu überfordern und gleichzeitig
zweitere Gruppe nicht zu unterfordern. Daher ist das Buch sowohl für Programmier-
anfänger gedacht, als auch für jene, die bereits Programmiererfahrung (in R oder
einer anderen Sprache) haben und ihre Kenntnisse vertiefen wollen.
Wir bedanken uns bei all jenen Studierenden, die uns mit ihren Feedbacks zu di-
versen Weiterentwicklungen inspiriert haben! Insbesondere bei Isabella Deutsch und
Cordula Eggerth: Ihr habt mit euren Hinweisen einen wertvollen Beitrag zur Quali-
tätssteigerung dieses Buches geleistet!
VI
Und wir bedanken uns bei jenen Studierenden, die sich bei manchen Themen schwer
getan haben und das auch offen angesprochen haben. Ihr habt uns dazu ermutigt,
entsprechende Kapitel zu überarbeiten. Nochmal in uns zu gehen und neue Mög-
lichkeiten zu finden, die schwierigen Konzepte verständlicher und anschaulicher zu
vermitteln. Auch ihr habt einen wertvollen Beitrag geleistet!
Und jetzt laden wir dich herzlich ein auf eine spannende Entdeckungsreise durch die
Welt von R! Wir spielen Karten- und Würfelspiele, begleiten Minigolfspieler, erkun-
den Lotterien, extrahieren Informationen aus Pizzakarten, erforschen Schwertlilien,
finden heraus, wann wir nächstes Mal an einem Samstag Geburtstag haben und
vieles mehr.
Dabei wünschen wir dir viel Spaß und Erfolg! Möge R stets mit dir sein!
Daniel Obszelka und Andreas Baierl (Wien im April 2020)
Andreas Baierl war mein Mentor, bei ihm und Prof. Marcus Hudec habe ich Pro-
grammieren mit R gelernt und durfte sie als Tutor einige Jahre durch diese Lehrver-
anstaltung begleiten.
Schon damals hat es mich gereizt, Inhalte der Vorlesung aus einer alternativen Sicht
und mit zusätzlichen Beispielen zu erklären, sodass die ersten Word-Dateien ent-
standen. Damals war das Ganze noch ein lichter Fleckerlteppich, der im Laufe der
Zeit immer dichter wurde.
Im Jahr 2014 habe ich dann die große Chance bekommen, als Lektor eine Paral-
lelgruppe dieser Lehrveranstaltung zu übernehmen. Schon als Tutor habe ich mir
gesagt: Wenn ich jemals an der Universität Lehre betreiben sollte, dann soll es Sta-
tistisches Programmieren sein. Nie hätte ich gedacht, dass sich mein Wunsch derart
schnell erfüllt! Ich kann mich noch ganz genau daran erinnern, als mich Birgit Ewald,
Sekretärin des Statistikinstitutes, im September (ca. drei Wochen vor Beginn des Se-
mesters) am Telefon fragte, ob ich Interesse hätte. Sofort habe ich zugesagt.
Ich hatte also rund drei Wochen Zeit, um eine damals dreistündige Lehrveranstal-
tung aus dem Boden zu stampfen. Die Gelegenheit wollte ich unbedingt dazu nützen,
um aus dem Fleckerlteppich des Tutoriums ein Skriptum zusammenzustellen. Aus
Zeitgründen musste ich an manchen Stellen improvisieren, dennoch (oder vielleicht
sogar auch deswegen) war mein Einstieg in die Lehre erfolgreich und ist mit sehr vie-
len schönen Erinnerungen verbunden. Unter anderem ist eine tiefe Freundschaft mit
Fritz Luther entstanden, für die ich sehr dankbar bin. Deine Bakkarbeit zum The-
ma Labyrinth generierende Algorithmen war tatsächlich nicht nur exzellent, sondern
auch sexy ;-)
VII
Prof. Marcus Hudec, Andreas Baierl und ich haben im Laufe des Semesters beschlos-
sen, alle existierenden Unterlagen den Studierenden beider Gruppen zur Verfügung
zu stellen. Damals waren unsere Gruppen noch unabhängig voneinander. Im Jahr
2016 haben Andreas Baierl und ich unsere beiden Gruppen zusammengelegt und
uns (inspiriert von allen Unterlagen) auf ein gemeinsames Skriptum geeinigt, das
die Basis dieses Buches liefert. Die ersten Teile dieses Buches basieren dabei groß-
teils auf meinen Unterlagen, während Andreas Baierl den Input für den Teil Data
Science und Statistik in der Praxis beigetragen hat.
Das Buch hätte bereits Ende 2018 fertig sein sollen. Mitten in der heißen Zeit des
Schreibens hat mich eine schwere Infektionskrankheit rund ein halbes Jahr komplett
außer Gefecht gesetzt.
Ich bedanke mich bei all den lieben Menschen, die mich durch diese schwierige Zeit
begleitet haben. Bei meinen Eltern, die mir stets Halt und Kraft gegeben haben. Bei
meinen Freunden, die mich mit Nutella, Keksen und weiteren Köstlichkeiten versorgt
haben und mich bei dem einen oder anderen Brettspiel haben gewinnen lassen, um
mich aufzuheitern :-) Die mir aufmunternde Alles-Gute-Fotos geschickt haben und
mir das Gefühl gegeben haben, nie alleine zu sein. Bei Prof. Günther Raidl, der
trotz meiner schweren Krankheit an mir festgehalten hat. Das Wort „Durchstarten“
werde ich ewig mit dir verbinden. Ich habe im Spital zwei Wochen um mein Leben
gekämpft und gewonnen. Und ich bin dankbar, dass ich die Krankheit gut bewältigt
habe und die Veröffentlichung dieses Buches miterleben darf!
Und ordentlich feiern werde! An dieser Stelle bedanke ich mich herzlich bei Victoria
Luther, die mich all die Monate hindurch mit ihren ganz speziellen Vorstellungen von
dieser Party erheitert hat (der rote Teppich für dich sollte zum Beispiel machbar sein
;-)) Ebenso herzlich bedanke ich mich bei dir, lieber Thomas Ledl! Du bist wie ein
großer Bruder für mich und ich bin dankbar, dass es Menschen wie dich gibt! Schon
jetzt wünsche ich dir viel Vergnügen mit dem ersten Obszelkerrrrr in deinem Regal
;-) Weiter bedanke ich mich bei der gesamten Studienvertretung Statistik, auch bei
den Vorgängern. Ich genieße jeden Stammtisch, jeden Actionday, jedes Pokerturnier,
jede Grillfeier und jede Weihnachtsfeier in vollen Zügen mit euch! Danke auch an
meine Studierenden für die vielen tollen und unvergesslichen Momente im Hörsaal!
Ich habe jede Minute mit euch genossen!
Und ganz herzlich bedanke ich mich bei dir, lieber Andreas! Später habe ich erfahren,
dass du es warst, der mich 2014 für die Parallelgruppe empfohlen hat. Dafür werde
ich dir ewig dankbar sein und es ist mir persönlich eine große Freude wie Ehre, mit
dir gemeinsam dieses Buch zu verlegen!
VIII
Meinen ersten Kontakt mit R, um genauer zu sein mit dem Vorläufer S, verdanke
ich Marcus Hudec. Als Visionär hast Du bereits damals zukünftige Trends antizi-
piert. Unsere gemeinsame Geschichte mit R verlief facettenreich mit gemeinsamen
Lehrveranstaltungen und ersten Ideen für ein R-Buch.
Dass es tatsächlich soweit gekommen ist, verdanke ich Dir, Daniel. Wir konnten
auf deinem umfangreichen Skriptum aufbauen, der Austausch mit Dir war für mich
immer sehr inspirierend.
Die Realisierung des Buches gelang nur mit der vollen familiären Unterstützung.
Danke Eva, dass Du neben Schwangerschaft und Geburt unserer Tochter und beruf-
lichem Wiedereinstieg mir die Arbeit an diesem Buch ermöglicht hast.
Vielen Dank an den Springer Verlag, insbesondere an Sybille Thelen, für Ihre un-
mittelbare Begeisterung für unser Buchprojekt und Ihr Durchhaltevermögen!
Nachhaltigkeit
Auch R entwickelt sich laufend weiter. Zu den Basispaketen (Base-R) haben sich
in den letzten Jahren viele Zusatzpakete gesellt, mit deren Hilfe manche (spezielle)
Aufgaben bequemer und einfacher lösbar sind. Würden wir uns aber (nur) auf diese
Zusatzpakete fokussieren, so liefen wir Gefahr, dass dieses Wissen schneller veraltet
und die Problemlösungskompetenz leiden könnte.
Wir verfolgen in diesem Buch daher die Philosophie, dir die Grundlagen von Base-R
gründlich und ausführlich zu erläutern und dich in die Lage zu versetzen, ein mög-
lichst breites Spektrum von Problemen damit gut lösen zu können. Dabei geben
wir dir auch das nötige Rüstzeug mit, dich selbstständig mit neueren Paketlösungen
auseinanderzusetzen. Im Anhang haben wir dir einen Mix aus diversen Zusatzpa-
keten zusammengestellt, die sich aus jetziger Sicht gut für eine Vertiefung bzw. als
Ergänzung eignen.
IX
Programmierbausteine zu lernen ist nicht schwierig. Es gehört nicht viel dazu, je-
mandem zu erklären, wie man den Mittelwert aus mehreren Zahlen berechnet. Die
Kunst der Programmierung ist es viel mehr, sich zu überlegen, wie man diese Bau-
steine geschickt zu funktionstüchtigen Programmen zusammensetzen kann, die ein
gegebenes Problem lösen. Dabei führen viele Wege nach Rom und wir entwickeln
anhand von Beispielen ein Gespür dafür, welche Wege tendenziell besser sind.
Wir wollen dir in diesem Buch also nicht Programmierrezepte, sondern Program-
mieren beibringen. Dir Konzepte vermitteln, die du bei Bedarf auch auf andere
Programmiersprachen umlegen kannst. Unter anderem stellen wir dir im Laufe des
Buches immer wieder Zwischenfragen, die dich zum Mit- und Nachdenken animieren
sollen.
Aufmerksamkeit
Programmieren bedeutet auch aufmerksam zu sein. Oft schreiben wir einen Pro-
grammcode, der gut aussieht und dennoch fehlerbehaftet ist, weil wir etwa eine An-
nahme treffen, die nicht erfüllt ist. Wir schenken diesem äußerst wichtigen Aspekt
viel Bedeutung. An manchen Stellen des Buches bauen wir daher bewusst Unge-
reimtheiten und Fehler ein und weisen dich darauf hin. Einige sind ziemlich subtil,
sodass sie zunächst gar nicht auffallen.
Wir fangen bei Null an und führen neue Konzepte sukzessive ein. Die Konzepte bau-
en dabei aufeinander auf; wenn du Neueinsteiger bist, empfehlen wir dir daher, ganz
von vorne anzufangen. Falls du bereits Vorerfahrungen in R gesammelt hast, kannst
du grundsätzlich beliebige Kapitel lesen. Wenn dir bestimmte Konzepte oder Funk-
tionen nicht geläufig sind, kannst du zu den entsprechenden Kapiteln zurückblättern.
Wichtige Konzepte früherer Kapitel sind dabei referenziert, für das Auffinden von
Funktionen bietet sich der Funktionsindex am Ende des Buches an.
Die Kapitelüberschriften enthalten neben dem eigentlichen Inhalt auch die Na-
men der dort eingeführten Funktionen, was von unseren Studierenden als sehr über-
sichtlich empfunden wurde. Das Stichwort- und Funktionsverzeichnis im An-
hang ermöglicht es dir, schnell bestimmte Stellen des Buches ausfindig zu machen.
Die Gliederung der einzelnen Kapitel ist weitgehend einheitlich. Zu Beginn eines Ka-
pitels erfährst du, was dich erwartet. Die allermeisten Kapitel umfassen ein Leitbei-
spiel mit Leitfragen, die wir im Laufe des Kapitels beantworten. Die Leitbeispiele
sollen dem Buch auch einen gewissen Romancharakter verleihen.
X
An geeigneten Stellen präsentieren wir in der Rubrik „Aus der guten Praxis“
Wir wollen dich von Beginn an dazu motivieren, saubere Programmcodes zu ent-
wickeln und dir möglichst früh die Gelegenheit geben, dich mit potenziellen Fehler-
quellen auseinanderzusetzen.
Die Kapitel schließen mit zusammenfassenden Kontrollfragen ab, mit denen du
dein Wissen testen kannst. Im Ausblick erfährst du, wo weitere Kapitel anknüpfen
und in den Übungen erhältst du die Chance zu zeigen, was du gelernt hast.
Neu gelernte Funktionen sind speziell markiert. Die Programmcodes werden kom-
mentiert und erklärt, wichtige Passagen werden dabei mit unterschiedlichen Farben
hervorgehoben, um den Überblick zu erhöhen.
Onlinematerial
Lerntipps
Im Laufe der Zeit haben unsere Studierenden viele kreative Möglichkeiten gefunden,
den Stoff besser und vor allem nachhaltiger zu lernen. Wir bedanken uns herzlich
bei all jenen Studierenden, die ihre Lerntipps mit uns geteilt haben, damit wir sie
jetzt mit dir teilen können!
Inhaltsverzeichnis
5.6 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.6.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 68
5.6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.6.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
6 Mengen, Sortieren und Kombinatorik . . . . . . . . . . . . . . . . . . . . . 70
6.1 Mengenfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
6.1.1 Matchings bestimmen – "%in%" . . . . . . . . . . . . . . . . . 71
6.1.2 Mehrfacheinträge streichen – unique() . . . . . . . . . . . . 73
6.1.3 Schnittmenge und Vereinigung mit dem %in%-Operator . . . 73
6.1.4 Schnittmenge und Vereinigung – intersect(), union() . . . 74
6.2 Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
6.2.1 Sortierung der Einträge – sort() . . . . . . . . . . . . . . . . 75
6.2.2 Generierung der sortierenden Entnahmereihenfolge – order() 75
6.2.3 Ränge bestimmen – rank() . . . . . . . . . . . . . . . . . . . 76
6.2.4 Vektoren umdrehen – rev() . . . . . . . . . . . . . . . . . . . 78
6.2.5 Mehrfachsortierung – order() . . . . . . . . . . . . . . . . . 78
6.2.6 Mehrfachsortierung und Ränge – rank() . . . . . . . . . . . 80
6.3 Kombinatorik – factorial(), choose(), prod() . . . . . . . . . . . 82
6.4 Wissenschaftliche Notation – options(scipen) . . . . . . . . . . . . 82
6.5 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.5.1 Fallbeispiel: Unterscheidbare Lottoziehungsergebnisse . . . . 83
6.5.2 Fallbeispiel: Unterscheidbare Playlists einer Musik-CD . . . . 84
6.6 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.6.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 85
6.6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
6.6.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
7 Deskriptive Statistik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
7.1 Funktionsübersicht und fehlende Werte ausschließen – na.rm . . . . 90
7.2 Minimum und Maximum – min(), max(), range() . . . . . . . . . . 90
7.3 Mittelwert, Varianz und Standardabweichung – mean(), var(), sd() 92
7.4 Median und Quantile – median(), quantile() . . . . . . . . . . . . 92
7.5 Summarys – summary() . . . . . . . . . . . . . . . . . . . . . . . . . 94
7.6 Aus der guten Statistikpraxis . . . . . . . . . . . . . . . . . . . . . . 94
7.6.1 Robustheit und Ausreißer . . . . . . . . . . . . . . . . . . . . 94
7.6.2 Standardisierung . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
7.7.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 96
7.7.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
7.7.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
8 Kumulieren und Parallelisieren . . . . . . . . . . . . . . . . . . . . . . . . 99
8.1 Kumulierende Funktionen . . . . . . . . . . . . . . . . . . . . . . . . 100
8.1.1 Kumulierte Summen – cumsum() . . . . . . . . . . . . . . . . 100
8.1.2 Kumulierte Produkte – cumprod() . . . . . . . . . . . . . . . 102
8.1.3 Kumulierte Minima und Maxima – cummin(), cummax() . . . 102
Inhaltsverzeichnis XV
20 Dataframes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
20.1 Allgemeines zu Dataframes . . . . . . . . . . . . . . . . . . . . . . . 238
20.1.1 Dataframes generieren – data.frame() . . . . . . . . . . . . 238
20.1.2 Dimension von Dataframes – nrow(), ncol(), length() . . . 239
20.1.3 Beschriftungen ändern – rownames(), colnames(), names() . 239
20.1.4 Überblick über das Dataframe – str(), head(), tail() . . . 240
20.2 Zusammenhang mit Matrizen – as.matrix() . . . . . . . . . . . . . 241
20.3 Zugriff auf Zeilen und Spalten: Subsetting . . . . . . . . . . . . . . . 241
20.3.1 Zugriff auf Spalten/Variablen . . . . . . . . . . . . . . . . . . 241
20.3.2 Zugriff auf Zeilen/Beobachtungen . . . . . . . . . . . . . . . . 242
20.3.3 Flexibler Zugriff – subset() . . . . . . . . . . . . . . . . . . 243
20.4 Manipulation von Dataframes . . . . . . . . . . . . . . . . . . . . . . 243
20.4.1 Variablen anfügen und löschen – cbind(), NULL, list(NULL) 243
20.4.2 Zeilen und Spalten umordnen . . . . . . . . . . . . . . . . . . 246
20.4.3 Einträgen oder Variablen ersetzen . . . . . . . . . . . . . . . 246
20.5 Zusammenhang mit Listen – is.list(), as.list(), is.data.frame(),
as.data.frame(), class(), unclass() . . . . . . . . . . . . . . . . 247
20.6 Funktionen revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
20.6.1 apply() und sapply() bei Dataframes . . . . . . . . . . . . 249
20.6.2 is.na() und Vergleichsoperatoren bei Dataframes . . . . . . 252
20.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
20.7.1 Objekte sichern . . . . . . . . . . . . . . . . . . . . . . . . . . 252
20.7.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 253
20.7.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
20.7.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
21 Dataframes verknüpfen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
21.1 Joins – merge() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
21.2 Zeilenweise Verknüpfung – rbind() . . . . . . . . . . . . . . . . . . . 258
21.3 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
21.3.1 Fallbeispiel: Verwaltung einfacher Datenbanken . . . . . . . . 259
21.4 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
21.4.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 261
21.4.2 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261
E Tools für Data Science und Statistik 263
22 Textmanipulation: Stringfunktionen . . . . . . . . . . . . . . . . . . . . . 264
22.1 Simple Stringfunktionen – nchar(), tolower(), toupper() . . . . . 265
22.2 Nach Mustern in Texten suchen . . . . . . . . . . . . . . . . . . . . . 266
22.2.1 Elemente des Vektors finden – grepl(), grep() . . . . . . . 267
22.2.2 Stellen in Texten finden – regexpr(), gregexpr() . . . . . . 269
22.2.3 Anzahl der Übereinstimmungen zählen . . . . . . . . . . . . . 271
22.3 Extraktion von Teilen aus Zeichenketten – substring() . . . . . . . 273
22.4 Sonderzeichen mit besonderen Fähigkeiten – [ ], "\\" . . . . . . . . 275
22.5 Ersetzungen in Zeichenketten – sub(), gsub() . . . . . . . . . . . . 276
22.6 Zerlegung von Zeichenketten – strsplit() . . . . . . . . . . . . . . 278
XX Inhaltsverzeichnis
1 R startklar machen
• R installieren
Bevor wir mit R durchstarten können, müssen wir das Programm zunächst instal-
lieren. Eine Anleitung dazu finden wir in (1.2). Davor machen wir uns in (1.1) noch
kurz mit der R-Homepage vertraut.
1.1 R-Homepage
Auf der R-Homepage finden wir einige interessante Inhalte und Themen zu R. Un-
ter anderem Informationen zu R-Konferenzen, eine Suchfunktion, einen Blog zu R,
Manuals und eine Liste von (weiteren) Büchern zu R etc.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_1
1 R startklar machen 3
• Wir klicken auf der Startseite oben bei Getting Started auf download R.
• Anschließend öffnet sich ein Fenster, in dem wir den Download-Mirror aus-
wählen, also jene Seite, von der wir R herunterladen wollen. Wir können zum
Beispiel den Mirror der Wirtschaftsuniversität Wien verwenden (Austria):
https://cran.wu.ac.at/
• Jetzt wählen wir oben bei Download and Install R das korrekte Betriebs-
system aus. Zur Verfügung stehen:
– Download R for Linux
– Download R for (Mac) OS X
– Download R for Windows
Im folgenden zeigen wir die weiteren Schritte für Windows. Bei Linux und Mac
folge einfach den weiteren Anweisungen der Homepage.
2 Los geht’s
Bevor wir voll durchstarten, machen wir uns in (2.1) zunächst mit R vertraut. Ab-
schließend besprechen wir in (2.4), wie wir R als Taschenrechner verwenden, sehen
uns in (2.5) nützliche Tastenkombinationen an und erfahren in (2.6) und (2.7), was
uns beim Beenden einer R-Sitzung erwartet und wie wir Pakete installieren und
laden.
Uns stehen die 32-bit und die 64-bit Version zur Verfügung. Unter Windows sieht
das zum Beispiel so aus (links die 32-bit, rechts die 64-bit Version):
Es wird empfohlen, die Version ans Betriebssystem anzugleichen. Ist ein 64-bit Be-
triebssystem installiert, so nehmen wir die 64-bit Version; bei einem 32-bit Betriebs-
system greifen wir zur 32-bit Version. Im Zweifel beginnen wir mit der 64-bit Version
und wechseln zur 32-bit Version, sollte es zu Fehlermeldungen kommen.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_2
2 Los geht’s 5
Führen wir eine der beiden Versionen aus, so öffnet sich RGui. Nun können wir direkt
in die R-Console Befehle eingeben, wie in Abb. 2.1 gezeigt.
R zeigt den Beginn von Kommandozeilen mit Prompts an. Der "> "-Prompt mar-
kiert eine neue Kommandozeile. Wir können nun Befehle eingeben, die unvollständig
oder vollständig sein können.
Die Befehle 2 + 3 und 4 * (2 + 3) sind vollständig. Nach dem Drücken der
Enter-Taste werden sie umgehend ausgeführt und die entsprechenden Ergebnisse
5 und 20 auf der Console ausgegeben.
Die Zahl in den eckigen Klammern (hier [1]) gibt uns den Index des ersten in der
Zeile folgenden Eintrags eines Vektors an, aber das beschäftigt uns im Moment nicht
weiter. In (3) schauen wir uns Vektoren und Indizes genauer an.
Der "+ "-Prompt sagt, dass der aktuelle Befehl unvollständig bzw. noch nicht
abgeschlossen ist. So fehlt etwa in 4 * (2 + 3 die schließende Klammer. Wir tragen
die fehlende Klammer in der nächsten Zeile nach und schließen damit den Befehl ab.
6 A Erste Schritte mit R
Für ganz kurze Rechnungen reicht die R-Console aus. Allerdings möchten wir den
Code meistens abspeichern, um ihn später bei Bedarf wieder verwenden zu können.
Eine einfache Möglichkeit für diese Zwecke sind Skripte.
Mit Datei/Neues Skript können wir ein neues Skript erstellen.
• Wir können ein Skript abspeichern. Hierzu klicken wir das Skriptfenster an,
sodass es zum aktiven Fenster wird, dann Datei/Speichern unter.
• Mit Datei/Öffne Skript können wir ein gespeichertes Skript laden.
• Wir können den Code bequem modifizieren und Fehler leicht korrigieren.
• Mit STRG-R kopieren wir die aktuelle Zeile bzw. den markierten Code direkt
in die R-Console, wo der Code sodann ausgeführt wird.
• Den gesamten Code führen wir aus, indem wir zunächst STRG-A (alles markie-
ren) und anschließend STRG-R drücken.
Wir betrachten unser erstes großes Beispiel, um uns mit R und dem Skriptfenster
vertraut zu machen. Für erfahrene Programmierer folgt sogleich der erste „Schock“:
Es hat nichts mit „Hello world“ oder „Hallo Welt“ zu tun.
Wir schreiben einen Code, der die Lösungen der quadratischen Gleichung
a · x2 + b · x + c = 0
• Alles, was in einer Zeile nach einem "#"-Zeichen folgt, wird als Kommentar
interpretiert. Kommentare werden nicht ausgeführt.
• a <- 2 ist eine Zuweisung: Der Variable bzw. dem Objekt a wird der Wert 2
zugewiesen. "<-" heißt Zuweisungsoperator.
• sqrt(x) berechnet die Quadratwurzel (square root) von x.
• Die Anweisungen x1 und x2 veranlassen R, den Inhalt der beiden Variablen
x1 und x2 (1.5 und 1) auf die R-Console zu drucken.
• In R werden Dezimalzahlen mit dem Dezimalpunkt dargestellt.
• Gibt es keine reellwertige Lösung, so wird eine Warnmeldung ausgegeben und
x1 und x2 wird NaN (Not a Number) zugewiesen. In (11.5) auf Seite 136
betrachten wir einen solchen Fall.
Anhand des Beispiels aus (2.2.1) zeigen wir diese farbliche Gliederung.
Der Code ist in vier logische Absätze untergliedert: Zunächst wird erklärt, was der
Code tut (Quadratische Gleichungen lösen). Anschließend werden die Koeffizienten
definiert. Nachdem die Lösungen berechnet werden, werden diese ausgegeben.
Während wir einen Code schreiben, ist uns völlig klar, was der Code tut. Stecken
wir aber unser Skript in die Schublade und kramen es erst fünf Jahre später wieder
heraus, so wird unsere Erinnerung bis dahin verblasst sein. Kommentare helfen dabei,
die Erinnerung wiederherzustellen!
Zeit für unsere erste Regel, die wir in Regel 1 niederschreiben!
Schreibe und kommentiere deinen R-Code immer so, dass dein Code übersicht-
lich ist und du auch in fünf Jahren schnell nachvollziehen kannst, was dein Code
tut, wie und wieso er funktioniert und wie du ihn richtig einsetzt.
2 Los geht’s 9
Bei Kommentaren geht es nicht darum, den nachfolgenden Code 1-zu-1 in Worte zu
übersetzen! Wenn wir das manchmal tun, dann nur, um den Einstieg zu erleichtern.
Bestimmte Codeteile, die wir im Fließtext näher erläutern, heben wir oft farblich
gesondert hervor. Damit wollen wir dir ein maximales Lesevergnügen bereiten!
Wir verwenden immer die Linkszuweisung! Die Rechtszuweisung erschwert die Les-
barkeit des Codes, daher vergessen wir sie am besten gleich wieder ;-)
Kommentare ("#") können übrigens auch neben dem Code stehen.
2.4 R als Taschenrechner – "+", "-", "*", "/", "ˆ", "( )", sqrt()
Das Rechnen funktioniert wie bei normalen Taschenrechnern. Es gilt Punkt- vor
Strichrechnung, wobei wir mit runden Klammern die Reihenfolge beeinflussen
können. In Tab. 2.1 sehen wir fünf wichtige Rechenoperatoren im Überblick.
Operator Bedeutung
+ Addition
- Subtraktion
* Multiplikation
/ Division
ˆ Potenz
In den vergangenen beiden Zeilen sehen wir, dass keine Klammern um -4 notwendig
sind. Das Minuszeichen bindet als Vorzeichen also stärker, als die Multiplikation.
Wenn wir uns nicht sicher sind, welche Rechenoperation Vorrang hat, so setzen wir
im Zweifel lieber mehr als weniger Klammern. Die Vermeidung von Klammern erhöht
jedoch oft die Lesbarkeit des Codes.
In Tab. 2.2 zeigen wir einige nützliche Tastenkombinationen für die R-Console.
Tastenkürzel Funktion
STRG+L Leert die R-Console.
STRG+TAB Wechselt zum nächsten Fenster (zum Beispiel von der Console
zum Skriptfenster).
STRG+A Markiert im Skriptfenster den gesamten Code.
STRG+R Der markierte Code oder die aktuelle Zeile des Skriptfensters
wird in der R-Console ausgeführt (STRG+ENTER in RStudio).
PFEILTASTEN Springe in der R-Console zur vorangehenden Codezeile (Pfeil
rauf) bzw. zur nächsten Codezeile (Pfeil runter).
ESC Bricht in der R-Console den aktuellen Befehl ab.
Beim Beenden von R werden wir gefragt, ob wir den Workspace sichern wollen
(Abb. 2.3 links). Wählen wir „Ja“ aus, dann werden unter anderem die existierenden
Objekte und eingegebenen Befehle gespeichert und sind beim nächsten Programm-
start von R wieder verfügbar. Es ist, als hätten wir R niemals beendet.
Es ist jedoch nicht notwendig, den Workspace zu sichern, daher wählen wir in der
Regel „Nein“ aus. Denn wenn wir Skripte verwenden, was praktisch immer der Fall
ist, dann haben wir ja die Befehle ohnehin in der Skriptdatei abgespeichert. Und in
(5) sehen wir uns an, wie wir auch die erstellten Objekte in einer Datei abspeichern
können. Dateien haben den großen Vorteil, dass sie sehr einfach portierbar sind, also
einfach auf andere PCs (zum Beispiel via USB-Stick) übertragbar sind.
Falls wir Skripte modifiziert, aber nicht abgespeichert haben, werden wir außerdem
gefragt, ob wir die Änderungen speichern wollen (Abb. 2.3 rechts).
Abbildung 2.3: Zwei Dialoge beim Beenden von R. Links: Workspace sichern?
Rechts: Änderungen der Skripte speichern?
12 A Erste Schritte mit R
Wir kommen über weite Strecken mit Basispaketen (wie zum Beispiel base) aus, die
bei der Installation von R automatisch mitinstalliert werden. Manchmal bieten sich
jedoch für bestimmte Aufgaben Zusatzpakete an. Wir kommen an geeigneter Stelle
darauf zurück. Sollte es soweit sein, blättere ggf. einfach auf diese Seite zurück.
Um in R ein Paket verfügbar zu machen, sind zwei Schritte nötig:
install.packages("Paketname ")
Es öffnet sich ein Fenster, in dem wir den CRAN Mirror auswählen, von dem
der Download des Pakets erfolgt soll. Wir können zum Beispiel den Mirror
Austria [https] nehmen. Danach wird das Paket installiert.
2. Paket laden (bei jeder R-Sitzung): library()
Um das Paket Paketname zu laden, geben wir folgendes ein:
library(Paketname )
2.8 Abschluss
• Was sind vollständige und unvollständige Befehle? Was bedeuten der "+ "-
Prompt bzw. "> "-Prompt? (2.1.1)
• Wie speichern und laden wir Skripte? Welche Dateiendung haben Skriptdatei-
en? Wie führen wir den (gesamten) Code eines Skriptes aus? (2.1.2), (2.5)
• Was ist ein Kommentar und wie wird er eingeläutet? Wie funktioniert eine
Variablenzuweisung? Wie berechnen wir die Wurzel einer Zahl? (2.2.1)
• Worauf sollten wir stets achten, wenn wir einen R-Code schreiben? (2.2.2)
• Welche Rechenoperatoren haben wir gelernt? Wie steuern wir die Reihenfolge
der Rechenausführung? Wie berechnen wir die n. Wurzel einer Zahl? (2.4)
• Wie installieren und laden wir Pakete? (2.7)
2 Los geht’s 13
Achte wirklich darauf, dass du deine Programme übersichtlich schreibst und kom-
mentierst! In fünf Jahren wirst du dir dafür dankbar sein ;-)
2.8.2 Ausblick
Nachdem wir unsere ersten Rechnungen durchgeführt haben, lernen wir in (3) Vek-
toren kennen. Mit ihrer Hilfe steigern wir Berechnungen auf das nächste Level.
Wir haben mit sqrt() bereits eine Funktion kennengelernt. Was Funktionen sind
und wie wir auf die R-Hilfe zu Funktionen, Operatoren (wie etwa "+") und zu
Paketen zugreifen, lernen wir in (4).
2.8.3 Übungen
1. Der BMI (Body Maß Index) ist ein Maß dafür, ob eine Person unter-, normal-
oder übergewichtig ist und errechnet sich gemäß kg/m2 (Gewicht in kg durch
Körpergröße in m zum Quadrat).
a) Erstelle mit den Daten dieser Person die beiden Variablen kg und m.
b) Berechne mit Hilfe von kg und m den BMI dieser Person und gebe das
Ergebnis auf der R-Console aus.
c) Kommentiere dein Skript und speichere es unter einem geeigneten Datei-
namen ab.
Schreibe die Formel in der allgemeinen Form in R auf und werte sie für n = 8,
n = 9 und n = 10 aus. Achte auf die korrekte Klammersetzung!
14 A Erste Schritte mit R
Wir betrachten in diesem Kapitel ein einfaches Kartenset bestehend aus den Kar-
tenwerten 2, 3, 4, 5, 6 und 7 (siehe Abb. 3.1).
2 2 3 3 4 4 5 5 6 6 7 7
2 3 2 4 3 5 4 6 5 7 6 7
Wir mischen die Karten und legen sie auf einen Stapel (siehe Abb. 3.2).
4 4 7 7 6 6 2 2 3 3 5 5
4 7 4 6 7 2 6 3 2 5 3 5
Abbildung 3.2: Gemischter Kartenstapel. Die erste Karte links entspricht der
obersten Karte des Stapels; die letzte Karte rechts der untersten.
Wir wollen einige typische Spielaktionen mit R umsetzen und fragen uns:
• Wie können wir uns Karten vom Stapel ansehen bzw. Karten austeilen? (3.2),
(3.3)
• Wie viele Karten einer bestimmten Wertemenge halten wir nach dem Austeilen
in unserer Hand? (3.4), (3.5.1)
• Die wievielte Karte im Stapel ist die 7? (3.5.2)
• Wie können wir eine Karte abwerfen und durch eine neue ersetzen? (3.6)
• Was sollten wir bei der Wahl der Variablennamen beachten? (3.7.1)
Ein Vektor besteht aus Indizes und Elementen. Folgende Tabelle zeigt unseren
gemischten Kartenstapel:
Index 1 2 3 4 5 6
Element 4 7 6 2 3 5
Mit der Funktion c() können wir beliebig viele Elemente zu einem Vektor ver-
knüpfen. Dabei steht c für „concatenate“ (verketten, verknüpfen).
Wir generieren diesen Vektor in R und weisen ihn der Variable stapel zu.
> # Erstelle den Vektor mit den Kartenwerten des gemischten Stapels
> stapel <- c(4, 7, 6, 2, 3, 5)
> stapel
[1] 4 7 6 2 3 5
Der erste Eintrag hat den Index 1, der letzte Eintrag den Index 6 (Anzahl der
Elemente des Vektors). Das ist gar nicht so trivial, wie es scheint, da in vielen
Programmiersprachen wie etwa Java der erste Eintrag den Index 0 trägt.
Zählen wir mal nach, ob tatsächlich 6 Karten im Stapel liegen! Dazu bestimmen wir
die Länge des Vektors stapel, was wir mit Hilfe von length() bewerkstelligen:
Einfache Sequenzen mit Schrittweite 1 erzeugen wir mit dem Doppelpunkt (":").
Da im rechten Code 4.5 - 1 nicht ganzzahlig ist, schneidet R die Sequenz bei 4 ab.
Mit diesem Wissen können wir einen Indexvektor für den Vektor stapel erzeugen.
Auf Elemente eines Vektors können wir mit "[ ]" zugreifen (Subsetting). Wir un-
terscheiden im Moment zwei Möglichkeiten für den Zugriff:1
Wir nützen dieses Wissen dazu, uns bestimmte Karten des Stapels anzusehen.
In dieser Variante schreiben wir in die eckigen Klammern die gewünschten Indizes,
um gewünschte Elemente zu selektieren. Auf Seite 18 sehen wir uns an, wie wir
Elemente ausschließen.
> stapel
[1] 4 7 6 2 3 5
stapel[2, 4] funktioniert nicht, da wir die Indizes als Vektor übergeben müssen:
> stapel[2, 4]
Fehler in stapel[2, 4] : falsche Anzahl von Dimensionen
Wir wissen zwar, dass der Stapel 6 Karten hat. Dennoch sollten wir length(stapel)
statt 6 schreiben. Dadurch wird der Code flexibler und sollten wir den Code auch für
größere Kartensets verwenden wollen, so bleibt er ohne Änderung funktionstüchtig.
Durch Subsetting wird ein neuer Vektor kreiert, der neu indiziert wird.
Index 1 2 3 4 5 6
stapel 4 7 6 2 3 5
temp 6 2 3
Bemerkung: temp steht für „temporär“ und bezeichnet oft eine Variable, die nur
für Zwischenresultate gebraucht wird.
Die 6, die beim Vektor stapel noch den Index 3 hatte, rutscht etwa beim Vektor
temp an die 1. Stelle. temp ist also ein neuer Vektor, der auch neu indiziert wird.
Nehmen wir an, wir wollen die 1. Karte von temp sehen.
Im linken Befehl wird zunächst der Teilbereich 3:5 aus stapel selektiert und dem
resultierenden Vektor das 1. Element entnommen. Da wir die selektierten Elemente
auf temp gespeichert haben, können wir stapel[3:5] durch temp ersetzen.
18 A Erste Schritte mit R
Wir können auch die Indizes zwischenspeichern und dann die Elemente selektieren.
Frage: Was passiert eigentlich, wenn wir uns eine Karte anschauen wollen, die gar
nicht existiert? Angenommen, wir wollten uns die 2. Karte, dann die 9. Karte und
schließlich nochmal die 2. Karte ansehen.
Die 9. Karte existiert nicht, was uns R mit dem NA (steht für Not Available) mitteilt.
Wir können auch bestimmte Einträge ausschließen und alle anderen selektieren.
Dies erreichen wir, indem wir den Indizes ein "-" voranstellen.
> stapel
[1] 4 7 6 2 3 5
> # Selektiere alle Elemente außer dem 3. bis 5. - beachte die Klammern!
> stapel[-(3:5)]
[1] 4 7 5
Im letzten Codeteil erkennen wir, dass wir mit der Funktion c() Vektoren beliebiger
Längen verketten können. Hier wird der Vektor 3:5 an den einelementigen Vektor 1
angehängt.
Beispiel: Wir verteilen die Karten des Stapels auf zwei Spieler. Spieler 1 bekommt
jede zweite Karte (beginnend bei der ersten) und Spieler 2 alle anderen Karten.
Spieler 2 bekommt all jene Karten, die Spieler 1 nicht bekommt, was wir im rechten
Code bequem mit stapel[-ind] anweisen.
Kritiker bemängeln, dass der Code suboptimal ist, da ind <- c(1, 3, 5) nicht
an die Stapelgröße gekoppelt ist und daher für Stapel anderer Größen nicht kor-
rekt funktioniert. Diese Personen haben völlig recht! In (3.3) reparieren wir diesen
Mangel. Davor brauchen wir aber noch eine kleine Vorleistung.
Ein logischer Vektor (logical) besteht aus den Wahrheitswerten TRUE und FALSE.
Mit Wahrheitswerten geben wir in den eckigen Klammern an, ob der Eintrag an der
entsprechenden Position selektiert werden soll (TRUE) oder nicht (FALSE).
Beispiel: Wir verteilen die Karten des Stapels wie im letzten Beispiel auf zwei
Spieler. Dieses Vorhaben können wir alternativ auch so realisieren:
> stapel
[1] 4 7 6 2 3 5
Bemerkung: Der Begriff bool bezieht sich auf boolean, eine in vielen Program-
miersprachen gebräuchliche Bezeichnung für logische Typen.
Damit bestimmen wir die Hand des 1. Spielers. Für den zweiten Spieler drehen wir
die Wahrheitswerte mit Hilfe des Negationsoperators "!" um.
> # Karten für den 2. Spieler: jene Karten, die Spieler 1 nicht bekommt
> !bool1
[1] FALSE TRUE FALSE TRUE FALSE TRUE
> hand2 <- stapel[!bool1]
> hand2
[1] 7 2 5
In diesem Beispiel ist es noch recht fingerschonend, jeden zweiten Eintrag zu selek-
tieren. Stellen wir uns vor, der Stapel hätte 52 (oder mehr) Karten... Betrachten wir
im nächsten Abschnitt eine Weiterentwicklung dieser Idee!
20 A Erste Schritte mit R
3.3 Recycling
Recycling ist in R ein wichtiges Konzept, das implizit sehr oft vorkommt. Wir kom-
men auch nach diesem Abschnitt noch einige Male darauf zurück.
> stapel
[1] 4 7 6 2 3 5
> # Selektiere jedes zweite Element aus stapel (beginnend beim ersten)
> stapel[c(TRUE, FALSE)]
[1] 4 6 3
Der Vektor c(TRUE, FALSE) wird so lange repliziert, bis er die Länge von stapel
erreicht. Mit anderen Worten: Der Vektor c(TRUE, FALSE) wird recycelt. Der Vektor
stapel hat 6 Elemente und der Vektor c(TRUE, FALSE) 2 Elemente. Da 2 ein Teiler
von 6 ist, kann der Vektor c(TRUE, FALSE) vollständig recycelt werden.
Intern entspricht obiger Befehl also dem folgenden:
Kann der logische Vektor nicht vollständig recycelt werden, so wird der replizierte
Vektor auf die benötigte Länge abgeschnitten. Das ist zum Beispiel der Fall, wenn
wir jede vierte Karte betrachten wollten.
> stapel
[1] 4 7 6 2 3 5
4 (Länge von c(TRUE, FALSE, FALSE, FALSE)) ist kein Teiler von 6 (Länge von
stapel). Um den logischen Vektor an die Länge von stapel anzugleichen, werden
die ersten beiden Einträge TRUE, FALSE hinten erneut angehängt. Äquivalent ist
daher folgender Aufruf:
> y <- 1:5 * c(-1, 1) # 2 ist kein Teiler von 5 => unvollständiges Recycling
Warnung in 1:5 * c(-1, 1)
Länge des längeren Objektes
ist kein Vielfaches der Länge des kürzeren Objektes
> y
[1] -1 2 -3 4 -5
Bei einer Warnung wird der Code normal ausgeführt. Die Warnung soll uns lediglich
auf einen eventuellen Fehler hinweisen.
Beispiel: Verteile die Karten des Stapels sauber an zwei Spieler.
> stapel
[1] 4 7 6 2 3 5
> # Spieler 1 bekommt jede zweite Karte (beginnend bei der ersten).
> # Spieler 2 erhält alle anderen Karten.
> bool1 <- c(TRUE, FALSE)
Das Verteilen der Karten ist uns schon mal super gelungen! Die meisten Spieler
interessieren sich jetzt dafür, wie viele Karten einer bestimmten Wertemenge sie in
ihren Händen halten und ob ihre Karten besser sind als jene des Mitspielers? Wir
lernen adäquate Techniken kennen um diese spannenden Fragen zu beantworten!
3.4.1 Elemente vergleichen – "==", "!", "!=", "<", "<=", ">", ">="
Mit logischen Vektoren können wir jene Elemente eines Vektors selektieren, die eine
oder mehrere Bedingungen erfüllen. In Tab. 3.1 listen wir logische Operatoren auf,
mit denen wir bereits interessante logische Bedingungen generieren können.
Operator Bedeutung
== gleich
! nicht (Wahrheitswerte umkehren)
!= ungleich
< bzw. <= kleiner bzw. kleiner gleich
> bzw. >= größer bzw. größer gleich
22 A Erste Schritte mit R
Beispiel: Erstelle logische Vektoren, die angeben, ob die Elemente von stapel
1. gleich 5 sind.
2. größer als 5 sind.
3. ungleich 5 sind.
> stapel
[1] 4 7 6 2 3 5
Die Anweisung stapel == 5 generiert einen logischen Vektor, der angibt, ob die
entsprechenden Einträge dem Wert 5 gleichen. Der einelementige Vektor 5 wird
dabei übrigens recycelt.
Völlig analog funktionieren die anderen beiden Abfragen:
Beispiel: Ziehe alle Karten aus stapel, die einen Wert von mindestens 5 haben.
Wir sehen, dass die selektierten Elemente ihre ursprüngliche Reihenfolge behalten.
Natürlich könnten wir alternativ auch all jene Kartenwerte selektieren, die nicht
kleiner als 5 sind.
Wir sehen im unteren Code, dass "<" Vorrang gegenüber "!" hat. Es wird also zuerst
stapel < 5 ausgeführt und anschließend der Ausdruck negiert. Wenn wir uns nicht
sicher sind, welcher Operator Vorrang hat, dann sind wir mit Klammern – wie im
vorletzten Codeblock verwendet – auf der sicheren Seite.
3 Vektoren und logische Abfragen 23
Erfüllt kein Eintrag die Bedingung, so wird ein leerer Vektor zurückgegeben.
> # Ein leerer Vektor hat Länge 0. > # Ist obiger Vektor leer?
> length(stapel[stapel < 0]) > length(stapel[stapel < 0]) == 0
[1] 0 [1] TRUE
Logische Bedingungen können wir auch miteinander verknüpfen. Hierzu eignen sich
die in Tab. 3.2 dargestellten Verknüpfungsoperatoren "&" bzw. "|".
Operator Bedeutung
& Und-Verknüpfung
| Oder-Verknüpfung
Die Und-Verknüpfung erzeugt genau dann TRUE, wenn beide beteiligten Wahrheits-
werte TRUE sind. Die Oder-Verknüpfung gibt uns TRUE zurück, wenn mindestens
einer der beiden Wahrheitswerte TRUE ist.
Beispiel: Ziehe aus stapel alle Karten größer 2 und kleiner oder gleich 5 heraus.
> stapel
[1] 4 7 6 2 3 5
Wir bemerken, dass die Verknüpfungsoperatoren "&" sowie "|" Nachrang gegenüber
den logischen Operatoren in Tab. 3.1 auf Seite 21 haben.
Ein beliebter Syntaxfehler ist folgender:
> stapel
[1] 4 7 6 2 3 5
In (3.3) haben wir auf Seite 21 die Karten auf zwei Spieler verteilt. Stürzen wir uns
gleich in ein Beispiel!
Beispiel: Wie viele Karten mit einem Wert von mindestens 5 hat jeder Spieler?
> # Anzahl der Karten >= 5 > # Anzahl der Karten >= 5
> sum(hand1 >= 5) > sum(hand2 >= 5)
[1] 1 [1] 2
Frage: Warum ist dies möglich? In R können wir mit Wahrheitswerten rechnen: R
codiert nämlich TRUE mit 1 und FALSE mit 0. Summieren wir mit Hilfe von sum()
über logische Vektoren, so werden die TRUE-Werte zusammengezählt. Mit anderen
Worten: Die Anzahl der TRUE wird bestimmt.
3 Vektoren und logische Abfragen 25
> # Bestimme den Anteil der Karten im Stapel mit einem Wert >= 5
> mean(stapel >= 5)
[1] 0.5
> # Anzahl Karten >= 5 von Spieler 1 > # Anzahl Karten >= 5 von Spieler 2
> sum(hand1 >= 5) > sum(hand2 >= 5)
[1] 1 [1] 2
Klarerweise könnten wir bereits jetzt die Frage mit Nein beantworten, da 1 <= 2 ist.
R kann das jedoch so nicht erkennen. Um diese Information sinnvoll weiterverwenden
zu können, müssen wir die Antwort in Form eines TRUE oder eines FALSE geben.
> # Hat Spieler 1 mehr Karten mit einem Wert >= 5 bekommen als Spieler 2?
> sum(hand1 >= 5) > sum(hand2 >= 5)
[1] FALSE
Oft interessieren wir uns für die Indizes jener Einträge, die eine oder mehrere Be-
dingungen erfüllen. Mit den bisher gelernten Techniken können wir dieses Vorhaben
bereits umsetzen.
Sei vektor ein beliebiger Vektor und bool ein (dazu passender) logischer Vektor,
dann sieht das Indexing schematisch so aus:
(1:length(vektor ))[bool ]
> stapel
[1] 4 7 6 2 3 5
> 1:length(stapel)
[1] 1 2 3 4 5 6
> stapel == 7
[1] FALSE TRUE FALSE FALSE FALSE FALSE
Die Karte mit dem Wert 7 befindet sich also an der 2. Stelle.
Die Funktion which() ermöglicht uns eine bequemere und kürzere Schreibweise. Wir
übergeben which() einen logischen Vektor und erhalten die Indizes der TRUE.
> # Ermittle den Index des Eintrags mit dem Wert 7 - mit which()
> which(stapel == 7)
[1] 2
Beispiel: Wir wollen die Positionen der Karten mit großer Wertigkeit bestimmen.
1. An welchen Stellen befinden sich all jene Karten mit einem Wert von 5 oder
größer?
2. An welcher Stelle befindet sich die erste Karte mit einem Wert von 5 oder
größer? Welchen Wert hat diese Karte?
> stapel
[1] 4 7 6 2 3 5
> # 1.)
> # Alle Indizes jener Elemente, die >= 5 sind
> which(stapel >= 5)
[1] 2 3 6
> # 2.)
> # Erster Index eines Elements, das >= 5 ist
> ind1 <- which(stapel >= 5)[1]
> ind1
[1] 2
Die erste Karte mit einem Wert von mindestens 5 steht also an 2. Stelle und hat
den Wert 7.
3 Vektoren und logische Abfragen 27
Frage: Was passiert wiederum, wenn wir die Indizes von nicht existierenden Ele-
menten erfragen? Finden wir es heraus!
Beispiel: An welchen Stellen befinden sich Karten mit einem negativen Wert? Wel-
che Karte ist die erste mit einem negativen Wert?
Im ersten Block entsteht bei which(stapel < 0) ein leerer Vektor, da kein Eintrag
kleiner als 0 ist. Ein leerer Vektor hat 0 Elemente, folglich existiert das 1. Element
nicht, was wir im zweiten Block durch ein NA (Not Available) mitgeteilt bekommen.
Wollen wir schließlich die Karte mit dem Index/Wahrheitswert NA aus stapel se-
lektieren, so weiß R nicht, was wir eigentlich wollen und antwortet mit NA.
Bemerkung: Wir verwenden which() mit Bedacht!
Sind wir ausschließlich daran interessiert, wie viele bzw. welche Elemente eines Vek-
tors eine Bedingung erfüllen, so verzichten wir aus Effizienzgründen auf which().
Bei der ineffizienten Variante machen wir einen Umweg: which() muss die Indizes
erst aus dem logischen Vektor heraus ermitteln, bevor die Selektion erfolgt.
Bei der effizienten Variante ersparen wir R diesen Zwischenschritt und befehlen ohne
Umwege die Selektion.
Wir erörtern die Frage, wie wir Elemente eines Vektors ersetzen bzw. tauschen. Dabei
begegnen uns tückische Fehlerquellen, vor denen wir uns geeignet wappnen!
28 A Erste Schritte mit R
Beispiel: Tausche im Kartenstapel stapel die erste und die letzte Karte aus.
> stapel
[1] 4 7 6 2 3 5
Frage: Erkennst du den Fehler? Nimm dir eine Minute Zeit, um den Fehler von
selbst zu finden, bevor du weiterliest!
Kleiner Tipp: Falsch gesetzte Klammern sind häufig die Ursache von Fehlern ;-)
Um obigen Code zu reparieren, setzen wir eine Klammer um.
> stapel
[1] 4 7 6 2 3 5
Wir selektieren zuerst die letzte Karte, dann die 2. bis vorletzte Karte und schließlich
die 1. Karte. Dazu haben wir noch keine neuen Techniken gebraucht.
Alternativ können wir die Ersetzfunktion benützen, die schematisch so aussieht:
Dabei werden die durch position markierten Elemente von vektor durch die Ele-
mente von ersetzung ersetzt. Gegebenenfalls wird ersetzung dabei recycelt.
Bevor wir mit unserem Kartenspiel weitermachen, betrachten wir kleinere Beispiele.
> # Beispielvektor
> x <- c(2, -5, 3, 4, -1)
> x
[1] 2 -5 3 4 -1
Da die Vektoren x[x > 0] und x[x > 0] * 2 gleich lang sind, ist bei der Ersetzung
kein Recycling notwendig. Na ja, ganz ohne Recycling kommen wir nicht aus: Die 2
wird beim Multiplizieren auf die Länge von x[x > 0] aufgeblasen ;-)
3 Vektoren und logische Abfragen 29
Wir sehen im R-Code auch, dass die Elemente von x überschrieben werden, die alten
Elemente sind also nicht mehr verfügbar.
> x
[1] 4 -5 6 8 -1
Hier erfolgt bei der Ersetzung ein Recycling: Der einelementige Vektor 0 wird auf
die Länge von x[x < 0] aufgeblasen.
Beispiel: Tausche im Kartenstapel die erste und die letzte Karte aus. Damit der
originale Stapel nicht überschrieben wird, kopiere davor stapel auf stapel.neu.
Diesmal setzen wir das Ganze mit der Ersetzfunktion um.
> stapel.neu
[1] 5 7 6 2 3 5
Frage: Erkennst du auch hier den Fehler? Nimm dir auch hier eine Minute Zeit um
den Fehler von selbst zu finden, bevor du weiterliest!
Zuerst überschreiben wir das 1. Element von stapel.neu. Das heißt, das alte Ele-
ment ist futsch! Bei der zweiten Ersetzung greifen wir also nicht auf das ursprüngli-
che, sondern auf das bereits überschriebene Element zu.
Zwei Lösungsansätze:
1. Wir greifen bei der Ersetzung auf die Elemente des Vektors stapel zu. Dazu
ersetzen wir auf der rechten Seite der Zuweisung stapel.neu durch stapel:
> # Vertausche 1. und letztes Element - fehlerfrei
> stapel.neu[1] <- stapel[length(stapel)]
> stapel.neu[length(stapel.neu)] <- stapel[1]
> stapel.neu
[1] 5 7 6 2 3 4
> stapel.neu
[1] 5 7 6 2 3 4
Frage: Was sollten wir bei der Benennung unserer Objekte beachten?
Sprechend heißt, dass anhand des Namens bereits klar ist, was sich hinter dem
Objekt verbirgt. Kurze Objektnamen erhöhen die Übersicht des R-Codes.
Gute Variablennamen zu finden ist manchmal eine Kunst, da beide genannten Ziele
(kurze sowie sprechende Namen) oft konfligieren. Die beiden Objektnamen stapel
und hand1 aus (3.1.1) und (3.2.1) erfüllen beide Anforderungen von Regel 2.
Weitere Beispiele für gute Variablennamen:
• kartenstapel: etwas lang, aber noch nicht zu lang und aussagekräftig.
• karten.gemischt oder stapel.gemischt: Gibt es keinen ungemischten Sta-
pel, kann der Zusatz .gemischt entfallen.
• blatt1 oder blatt.1: Blatt (= Karten) von Spieler 1
• hand.spieler1: Hand von Spieler 1. Der Punkt erhöht die Übersicht.
Beispiele für schlechte Variablennamen:
• fischteich: nichtssagend, absolut ungeeignet.
• gemischterkartenstapel: viel zu lang, unübersichtlich und fehleranfällig!
• diehandvonspieler1: zu lang und unübersichtlich!
3 Vektoren und logische Abfragen 31
Außerdem sind bei Objektnamen die meisten Sonderzeichen (wie etwa Rechenope-
ratoren) sowie Leerzeichen tabu. Erlaubt sind Buchstaben sowie Ziffern (sofern sie
nicht an erster Stelle stehen), der Punkt "." sowie der Unterstrich "_".
R ist case sensitive, unterscheidet also zwischen Groß- und Kleinschreibung, weshalb
zum Beispiel stapel und Stapel unabhängig voneinander definiert werden können.
Diese Möglichkeit setzen wir jedoch mit Bedacht ein, da Verwechslungen auftreten
können. Normalerweise wird die kleingeschriebene Variante bevorzugt.
Wenn du folgende Regel 4 beachtest, dann machst du deinen R-Code flexibler, mäch-
tiger und robuster!
Wir betrachten zwei Beispiele: Im ersten greifen wir jene Beobachtungen auf, die wir
in diesem Kapitel gesammelt haben. Im zweiten schauen wir uns das Beispiel zur
Lösung einer quadratischen Gleichung aus (2.2.1) ab Seite 6 nochmal an.
Beispiel: Wir vergleichen für die beiden Aktionen Ziehe die letzte Karte und Ziehe
jede zweite Karte eine ausbaufähige Variante mit einer allgemein und automatisiert
programmierten Variante und studieren die Unterschiede.
> stapel
[1] 4 7 6 2 3 5
Im linken Code haben wir die Karten per Hand abgezählt und die Anzahl der Karten
(6) und die Indizes (c(1, 3, 5)) manuell eingetragen. Das funktioniert hier zwar
noch, aber was passiert, wenn wir zwei weitere Karten unter den Stapel legen?
Schon funktioniert der linke Code nicht mehr korrekt! Wir müssten die 6 durch 8
und c(1, 3, 5) durch c(1, 3, 5, 7) ersetzen. Wir malen uns lieber nicht aus, was
passiert, wenn wir auch nur eine Änderung übersehen oder wenn der Stapel erneut
wächst und wir alle Änderungen nochmal vornehmen müssten. Die allgemein und
automatisiert programmierte Variante (rechter Code) erspart uns derlei Albträume,
denn sie funktioniert auch für größere Kartenstapel einwandfrei.
Wollten wir die Lösungen für eine andere Konstellation für a, b und c berechnen,
müssten wir den Code an sehr vielen Stellen (10 an der Zahl) modifizieren. Das ist
nicht nur mühsam, sondern auch extrem fehleranfällig! Denn die Wahrscheinlichkeit,
dass wir eine notwendige Änderung übersehen, ist groß.
In diesem Fall ist es ganz leicht, einen Code zu schreiben, der allgemein funktioniert.
Weil’s so schön war, bilden wir den flexiblen Code aus (2.2.1) noch einmal ab.
Beachte, dass wir Regel 4 folgend die Parameter a, b und c ganz oben definieren!
Wollen wir die Lösungen für eine andere Parameterkonstellation bestimmen, brau-
chen wir den Code nur an 3 Stellen zu Beginn des R-Codes zu ändern.
34 A Erste Schritte mit R
Tipp: Wenn dir das Konzept des allgemeinen Programmierens am Anfang noch
schwer fällt, dann fange am besten mit einem Spezialfall an und ersetze sukzessive
die Zahlen durch entsprechende Hilfsvariablen.
3.8 Abschluss
• Wie verketten wir Elemente zu einem Vektor? Wie ist ein Vektor indiziert und
wie bestimmen wir die Anzahl der Elemente eines Vektors? Wie erzeugen wir
einfache Sequenzen bzw. einen Indexvektor? (3.1)
• Wie selektieren wir Elemente oder Teilbereiche eines Vektors mit Hilfe von
Indizes? Wie schließen wir bei der Selektion bestimmte Indizes aus? Was gibt
R zurück, wenn wir nicht existierende Elemente selektieren? (3.2.1)
• Was ist ein logischer Vektor? Wie selektieren wir mit seiner Hilfe Elemente
eines Vektors? Wie negieren wir einen logischen Vektor? (3.2.2)
• Was bedeutet Recycling? Wie läuft vollständiges sowie unvollständiges Recy-
cling bei Rechenoperationen und beim Subsetting ab? (3.3)
• Welche Vergleichsoperatoren haben wir kennengelernt und wie generieren wir
mit ihnen logische Bedingungen? Was gibt uns R bei der Selektion zurück,
wenn kein Element eines Vektors eine Bedingung erfüllt? Wie verknüpfen wir
zwei logische Vektoren mit der Und- bzw. Oder-Verknüpfung und was bewirken
beide Verknüpfungen? (3.4)
• Was passiert mit den Wahrheitswerten FALSE bzw. TRUE eines logischen Vek-
tors, wenn wir Rechenoperationen auf den Vektor anwenden? Wie bestimmen
wir die Anzahl, den relativen Anteil und die Indizes jener Elemente, die eine
Bedingung erfüllen? (3.5)
• Wie ersetzen bzw. vertauschen wir Elemente eines Vektors korrekt? (3.6)
• Welche beiden Eigenschaften erfüllen gut gewählte Objektnamen? Warum soll-
ten wir im R-Code auf Umlaute und „ß“ möglichst verzichten? Welche Zeichen
sind bei Objektnamen erlaubt? (3.7.1)
Darüber hinaus beherzigen wir die Regel des allgemeinen und automatisierten Pro-
grammierens (Regel 4)! Wir achten darauf, dass unser Code robust ist gegenüber
Veränderungen der Problemgröße (zum Beispiel Anzahl der Elemente eines Vektors)
und des Problems selbst (zum Beispiel Elemente ändern sich). Wir definieren zu
Beginn Hilfsvariablen/Parameter, um unseren R-Code flexibler zu machen. (3.7.2)
3 Vektoren und logische Abfragen 35
3.8.2 Ausblick
Die Reise in die wunderbare Welt von R hat für uns gerade erst begonnen! Am Ende
des Buches kannst du folgende und weitere spannende Fragen beantworten.
• Wie kann ich den Stapel in R mischen?
• Wie kann ich bestimmen, wie viele Paare jemand abgehoben hat?
• Wie kann ich die Wahrscheinlichkeiten für bestimmte Kartenkonstellationen
(zum Beispiel beim Poker) simulieren?
3.8.3 Übungen
> stapel
[1] 4 7 6 2 3 5
Wir wollen zählen, wie viele Karten einen Wert von 4 oder mehr haben.
Welche der folgenden R-Codes geben uns die richtige Antwort? Erkläre dabei
die internen Abläufe!
2. Was wird folgend jeweils nach Eingabe von x ausgegeben? Erkläre dabei jeweils
die internen Abläufe.
x <- (1:6) * c(-1, 1)
x
x[x <= 0] <- -x[x <= 0]
x
x <- x * c(TRUE, FALSE)
x
x <- x[x != 0 & x < 5]
x
x <- x[c(0:3, length(x), 2, 1)]
x
36 A Erste Schritte mit R
> alter <- c(16, 18, 19, 24, 28, 32, 32, 45)
5. Wir betrachten die ausgeteilten Karten aus (3.3) auf Seite 21.
Beide Spieler geben jeweils ihre erste Karte an den Mitspieler ab. Folgender
Ansatz existiert bereits:
> # Die Zahl, von der wir die Wurzel bestimmen wollen
> n <- 9
> # Hilfsvariablen
> a <- 0
> b <- n
Du brauchst den Code natürlich noch nicht zu verstehen. Wir sind uns aber sicher
einig, dass folgender Code deutlich übersichtlicher ist:
> # Berechne die Wurzel einer Zahl und gib das Ergebnis aus
> sqrt(9)
[1] 3
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_4
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 39
Mit dem Hilfeoperator "?" rufen wir die R-Hilfe auf. Die Synax lautet:
?Funktionsname für Funktionen sowie
?"Operator " für Operatoren, den Operator dabei unter Anführungszeichen setzen
Nach der Anforderung der Hilfe erscheint eine Auflistung der in diesem Paket defi-
nierten Funktionen und Operatoren. Mit Hilfe der Links gelangen wir direkt auf die
Hilfeseiten der jeweiligen Funktionen und Operatoren.
Mit der Funktion seq() können wir ausgefeilte Sequenzen generieren. Mit ?seq
erhalten wir Informationen über diese Funktion.
## Default S3 method:
seq(from = 1, to = 1, by = ((to - from)/(length.out - 1)),
length.out = NULL, along.with = NULL, ...)
Parameter Bedeutung
from Startwert der Sequenz
to Endwert der Sequenz, der nicht überschritten werden darf.
by Schrittweite der Sequenz (kann auch negativ sein).
length.out Länge der Sequenz
Beachte: Bei seq() dürfen wir maximal drei Parameter spezifizieren. Der vierte
ergibt sich automatisch. Wir betrachten ein paar einfache Anwendungsbeispiele.
Im letzten Befehl sehen wir, dass der gewünschte Endwert to = 20 nicht erreicht
wird. Die Sequenz wird beim größten Wert kleiner oder gleich 20 abgeschnitten (18).
R verfolgt ein auf den ersten Blick eigenartiges Regelwerk, was den Funktionsaufruf
angeht. Auf den zweiten Blick erleichtert dieses Konzept die Bedienung aber enorm!
Innerhalb einer Funktion sind sogenannte formale Argumente definiert, die wir oft
auch als Parameter bezeichnen. So sind etwa from und to zwei formale Argumente
der Funktion seq(). Diesen formalen Argumenten können wir Objekte (zum Beispiel
Vektoren) zuweisen, wobei die Zuweisung auf drei Arten erfolgen kann:
• namentlich exakt,
• namentlich eindeutig abgekürzt oder
• unbenannt
Innerhalb einer Funktion werden dabei (bei Bedarf) Kopien der übergebenen Ob-
jekte verwendet. Objekte außerhalb einer Funktion bleiben also unverändert.
Beispiel: Wir wollen eine Sequenz der Länge 6 von 0 bis 100 erzeugen. Wir ver-
wenden die Funktion seq() und die Parameter from, to und length.out:
Hier sprechen wir alle formalen Argumente namentlich exakt an. Was passiert im
folgenden Befehl?
Hier haben wir die Objekte unbenannt übergeben. Somit erfolgt die Zuweisung
der Elemente auf die formalen Argumente in jener Reihenfolge, wie die formalen
Argumente in seq() definiert sind:
• Das 1. formale Argument ist from, also wird from der Wert 0 zugewiesen.
Das 4. formale Argument length.out wird nicht spezifiziert. Das liegt daran, dass
in der Funktionsdefinition das formale Argument by vor dem formalen Argument
length.out definiert ist und daher bei der unbenannten Übergabe Vorrang hat.
Wollen wir length.out spezifizieren ohne by anzugeben, müssen wir das R mitteilen:
Im linken Code sprechen wir length.out namentlich exakt an, während wir im
rechten Code das dritte Argument by leer lassen und damit überspringen. In beiden
Fällen wird die 6 dem 4. formalen Argument length.out zugeordnet.
Wir springen weiter. Was geht im folgenden Funktionsaufruf ab?
3. Jetzt werden die übrig gebliebenen unbenannt übergebenen Objekte der Reihe
nach auf alle noch nicht spezifizierten formalen Argumente zugewiesen. 100
bleibt übrig, das 1. formale Argument from ist bereits spezifiziert, das 2. for-
male Argument to noch nicht. Also wird to der Wert 100 zugewiesen.
seq(leng = 6, 100, from = 0)
Bei der Funktion seq() dürfen wir maximal drei Parameter spezifizieren. Wenn wir
mehr als drei Parameter spezifizieren, folgt eine Fehlermeldung.
Der Fehler tritt auch dann auf, wenn die übergebene Parameterkonstellation wider-
spruchsfrei ist, so wie im obigen Befehl.
Frage: Was passiert aber, wenn wir weniger als drei Parameter spezifizieren? Pro-
bieren wir es einfach aus!
Die Funktion seq() setzt in diesem Fall Defaultwerte (Standardwerte) ein. Defi-
nieren wir lediglich from und to, so wird by auf ±1 gesetzt, je nachdem, ob from
oder to größer ist. In diesem Fall handelt es sich also um einen dynamischen De-
faultwert. Das heißt, die Festlegung des nicht spezifizierten Parameters hängt von
anderen Parametern ab.
Betrachten wir zwei weitere Beispiele.
Hier wird jeweils from = 1 als Defaultwert herangezogen. Wird by nicht spezifiziert,
so wird by = 1 und to = length.out gesetzt.
Tipp: Wenn du dich bei Standardwerten zu Beginn noch nicht wohl fühlen solltest,
kannst du selbstverständlich im Zweifel lieber mehr als weniger Parameter spezifi-
zieren. Das Gefühl für die Defaultwerte kommt mit der regelmäßigen Anwendung.
Neben dynamischen Defaultwerten gibt es auch statische Defaultwerte, bei denen
die Festlegung der nicht spezifizierten Parameter unabhängig von anderen Parame-
tern erfolgt. Ein Beispiel dafür betrachten wir im folgenden Unterabschnitt. Dabei
schauen wir uns einen weiteren wichtigen Aspekt an.
Wir haben in (3.5.1) ab Seite 24 die Funktion sum() kennengelernt, mit der wir
Elemente eines Vektors aufsummieren können. Das klingt zunächst unspektakulär.
Prickelnd wird es aber, wenn fehlende Werte (NA – Not Available) ins Spiel kommen!
44 B Vektorfunktionen für Data Science und Statistik
Beispiel: Gegeben ist ein numerischer Vektor x, der fehlende Werte enthält. Wir
wollen die Summe aller Elemente berechnen und dabei NA-Werte ausschließen.
> sum(x)
[1] NA
Fehlende Werte werden nicht ignoriert. Wir schauen in der Hilfe nach (?sum) ...
... und erfahren, dass wir mit na.rm einstellen können, ob fehlende Werte vor der
Berechnung entfernt werden sollen. Spezifizieren wir na.rm nicht, so setzt R den
statischen Defaultwert FALSE ein. Statisch heißt, dass der Defaultwert nicht von
anderen Parametern abhängt. Wir wollen also na.rm auf TRUE setzen, wenn wir
dabei aber na.rm nicht exakt ansprechen, so werden wir bitter enttäuscht:
Der Haken an der Sache ist das sogenannte Dreipunkteargument ("..."), das in
sum() ganz zu Beginn steht. Es kann beliebig viele Objekte aufnehmen und schluckt
sowohl 1:4 als auch TRUE, sodass im Endeffekt die Summe aus 1:4 und TRUE gebildet
wird. TRUE wird dabei in 1 konvertiert (vgl. (3.5.1)), womit 11 herauskommt.
Frage: Was können/müssen wir tun, um von na.rm Gebrauch machen zu können?
Wir hangeln uns zur Lösung empor und setzen Regel 5 um.
> sum(x, na = TRUE) # na.rm ist NICHT exakt benannt: Es klappt nicht!
[1] NA
> sum(x, na.rm = TRUE) # na.rm ist exakt benannt: Es klappt!
[1] 10
> sum(na.rm = TRUE, x) # So klappt es übrigens auch.
[1] 10
Der Parameter na.rm (NA remove) kommt übrigens in sehr vielen Funktionen vor.
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 45
Objekte bzw. Elemente eines Objektes können wir mit Hilfe der Funktion rep()
replizieren. Die Hilfe zur Funktion rufen wir mit ?rep auf. Wir konzentrieren uns
auf die in Tab. 4.2 angeführten Parameter.1
Parameter Bedeutung
x das zu replizierende Objekt, zum Beispiel ein Vektor
times Bei einer Zahl wird das Objekt x als Ganzes times Mal repliziert.
Bei einem Vektor der Länge length(x) gibt times an, wie oft
jedes Element von x repliziert wird.
length.out Das ganze Objekt x wird so lange repliziert, bis length.out
Elemente erreicht sind.
each Jedes Element von x wird each Mal wiederholt.
> # 0:2 als Ganzes replizieren > # Jedes Element von 0:2 replizieren
> rep(0:2, times = 3) > rep(0:2, each = 3)
[1] 0 1 2 0 1 2 0 1 2 [1] 0 0 0 1 1 1 2 2 2
Im linken Code wird der Vektor 0:2 als Ganzes times = 3 Mal aneinandergehängt.
Im rechten Code hingegen wird 0:2 elementweise repliziert, und zwar each = 3 Mal.
Im linken Code wird 0:2 solange aneinandergehängt, bis die gewünschte Länge
length.out = 10 erreicht ist. Bei der 4. Wiederholung wird das Ergebnis abge-
schnitten. Den rechten Code sehen wir uns genauer an:
x 0 1 2
times 3 2 1
Der 1. Eintrag von x (0) wird 3 Mal wiederholt, der 2. Eintrag von x (1) wird 2 Mal
wiederholt und der 3. Eintrag von x (2) wird 1 Mal wiederholt.
1 Hier haben wir den Sonderfall, dass times, length.out und each innerhalb des Dreipunkteargu-
ments definiert sind: rep(x, ...). Intern wird innerhalb des Dreipunktearguments geprüft, ob
die Argumentnamen zu times, length.out oder each passen. Das funktioniert ähnlich wie beim
seq()-Beispiel.
46 B Vektorfunktionen für Data Science und Statistik
Für die ganzzahlige Division steht uns der Operator "%/%" zur Verfügung. Den
Rest der ganzzahligen Division ermitteln wir mit dem Modulooperator "%%".
Mit ?"%/%" bzw. ?"%%" rufen wir die R-Hilfe zu beiden Operatoren auf.
Beispiel: Ist 7 durch 3 teilbar?
7 ist durch 3 genau dann teilbar, wenn bei der ganzzahligen Division ein Rest von 0
herauskommt. Wir dividieren 7 durch 3 ganzzahlig; zunächst per Hand:
7 : 3 = 2
1 Rest
Die Zahl 7 ist also nicht durch 3 teilbar, da bei der ganzzahligen Division ein Rest
von 1 übrig bleibt. Es gilt: 2 · 3 + 1 = 7. Jetzt mir R:
In beiden Fällen wird die 3 recycelt, also auf die Länge von x aufgeblasen.
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 47
Tabellarisch aufgeschlüsselt:
x 1 2 3 4 5 6 7 8 9
ganz 0 0 1 1 1 2 2 2 3
ganz * 3 0 0 3 3 3 6 6 6 9
rest 1 2 0 1 2 0 1 2 0
ganz * 3 + rest 1 2 3 4 5 6 7 8 9
Das ist bei weitem nicht die einzige Möglichkeit, alle einstelligen Zahlen zu ermitteln,
die durch 3 teilbar sind! Wir kommen in Übung 9 auf Seite 60 darauf zurück.
Mit Hilfe von exp() bzw. log() können wir die Exponentialfunktion anwenden
bzw. Logarithmen berechnen. Die Anwendung ist ganz einfach.
Die Funktionen exp() und log() arbeiten vektorwertig. Das heißt, dass wir diesen
Funktionen Vektoren beliebiger Länge übergeben können und sie für jedes Element
des Vektors das entsprechende Ergebnis liefern.
Bei log() können wir mit dem Parameter base die Basis umstellen und jeden belie-
bigen Logarithmus bestimmen. Mit base = 10 zum Beispiel den 10er-Logarithmus:
Uns stehen unter anderem folgende 4 Funktionen zum Runden zur Verfügung:
Die mathematische Rundung mit round() entspricht fast der kaufmännischen Run-
dung. Einziger Unterschied: Folgt auf die letzte darzustellende Ziffer eine 5 sowie
lauter Nullen, so wird bei der mathematischen Rundung zur geraden Ziffer hin ge-
rundet. So wird etwa 1.5 auf 2 aufgerundet, während 2.5 auf 2 abgerundet wird.
In Übungsbeispiel 11 auf Seite 61 überlegen wir uns, wie wir kaufmännisch runden
können.
Frage: Wie können wir Zahlen auf eine gewisse Anzahl an Nachkommastellen run-
den? Die Funktion round() bietet uns hierzu den Parameter digits an.
> # Auf Ganze runden > # Auf Zehntel runden > # Auf Zehner runden
> round(x, digits = 0) > round(x, digits = 1) > round(x, digits = -1)
[1] 5 5 6 6 [1] 4.9 5.5 5.5 5.5 [1] 0 10 10 10
Standardgemäß wird auf Ganze gerundet (digits = 0). Wir können für digits
auch negative Zahlen einsetzen, um etwa auf Zehner zu runden (digits = -1).
Bevor du weiterliest: Versuche eine Möglichkeit zu finden, wie wir ohne den Para-
meter digits auf Zehner runden können!
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 49
Wenn wir eine Zahl z auf ganze k ∈ R runden wollen, so runden wir z/k auf
eine ganze Zahl und multiplizieren das Ergebnis mit k.
> k <- 10
Diesen Trick können wir auch bei den anderen Rundungsfunktionen anwenden! Für
k = 1 erhalten wir die ganzzahlige Rundung. Für k = 0.5 würden wir beispielsweise
auf ganze Halbe runden.
Es ist mit den bisher gelernten Techniken für uns schon möglich, den Absolutbetrag
sowie das Vorzeichen von Zahlen zu bestimmen. In Übung 10 auf Seite 61 schauen
wir uns an, wie das funktioniert und wiederholen dabei wichtige Konzepte.
An dieser Stelle beschränken wir uns auf bequem anwendbare Funktionen. Mit abs()
können wir den Absolutbetrag von Elementen bestimmen und mit sign() rufen
wir die Vorzeichenfunktion auf.
In (7.6.2) auf Seite 95 lernen wir im Zuge der Standardisierung eine wichtige An-
wendungsmöglichkeit für abs() kennen!
50 B Vektorfunktionen für Data Science und Statistik
Die Funktionen sin(), cos() und tan() sind selbsterklärend. Die Zahl π ist in R
unter pi abrufbar, solange wir pi nicht überschreiben (etwa mit pi <- 55).2
> pi
[1] 3.141593
π 3·π
Beispiel: Bestimme für 0, 2, π, 2 und 2 · π den Sinus und den Cosinus.
Bei der Auswertung von sin(x) und cos(x) entstehen Rundungsfehler und die
Ausgabe würde in der wissenschaftlichen Notation angezeigt werden. Daher runden
wir die Ergebnisse mittels round(), um die Ergebnisse übersichtlicher zu gestalten.
In (6.4) erfahren wir mehr über die wissenschaftliche Notation.
Für weitere trigonometrische Funktionen verweisen wir auf die R-Hilfe (?Trig).
Wir haben uns in (3.4) schon umfassend mit logischen Operatoren beschäftigt. Drei
typische Fragestellungen sind zum Beispiel:
• Erfüllen alle Elemente eines Vektors eine Bedingung?
• Erfüllt zumindest ein Element eine Bedingung?
• Erfüllen Elemente genau eine von zwei Bedingungen?
Wir schauen uns gleich anhand eines längeren Beispiels an, wie wir mit den bisher
gelernten Mitteln Fragen wie diese beantworten können. Gleichzeitig verwenden wir
die in Tab. 4.3 aufgelisteten Funktionen, die uns viel Tipparbeit ersparen.
Funktion Bedeutung
all(...) Sind alle Elemente von ... TRUE?
any(...) Ist zumindest ein Element von ... TRUE?
xor(x, y) elementweiser Vergleich: Ist entweder x oder y (im ausschließenden
Sinne) TRUE?
2 Überschreiben wir pi, so gewinnen wir π mit rm(pi) wieder zurück ((5.1.2) und (14.1.1)).
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 51
> # 1.) alle Einträge positiv? > # 2.) mind. 1 Zahl durch 5 teilbar?
> sum(x > 0) == length(x) > sum(x %% 5 == 0) > 0
[1] TRUE [1] FALSE
> # Kürzere Alternative > # Kürzere Alternative
> all(x > 0) > any(x %% 5 == 0)
[1] TRUE [1] FALSE
> x > x
[1] 3 4 9 2 [1] 3 4 9 2
Es sind also alle Zahlen positiv und es gibt keine Zahl, die durch 5 teilbar ist. Die
jeweils erstgenannten Möglichkeiten kommen mit den bisher gelernten Techniken
aus, die wir uns bei der Gelegenheit noch einmal in Erinnerung rufen.
Für die 3. und 4. Frage kreieren wir uns zunächst zwei Hilfsobjekte.
Beachte die kurzen und sprechenden Objektnamen bool.tb3 und bool.ge4. bool
deutet an, dass es sich um einen logischen Vektor handelt (boolean). Der Zusatz
.tb3 steht für „teilbar durch 3“ und .ge4 deutet „größer oder gleich 4“ („greater
or equal 4“) an. Damit haben wir Regel 2 auf Seite 30 gut eingehalten.
52 B Vektorfunktionen für Data Science und Statistik
> # 3.) durch 3 teilbar oder >= 4? > # 4.) entweder tb3 oder ge4?
> bool.tb3 > bool.tb3 + bool.ge4
[1] TRUE FALSE TRUE FALSE [1] 1 1 2 0
> bool.ge4 > bool.tb3 + bool.ge4 == 1
[1] FALSE TRUE TRUE FALSE [1] TRUE TRUE FALSE FALSE
> bool.tb3 | bool.ge4 > xor(bool.tb3, bool.ge4) # mit xor()
[1] TRUE TRUE TRUE FALSE [1] TRUE TRUE FALSE FALSE
> sum(bool.tb3 | bool.ge4) > sum(xor(bool.tb3, bool.ge4))
[1] 3 [1] 2
> x > x
[1] 3 4 9 2 [1] 3 4 9 2
Die Aufgabe 3.) sollte klar sein. Betrachten wir daher Aufgabe 4.)! 3 ist durch 3
teilbar, aber nicht größer oder gleich 4 und bei 4 ist es genau umgekehrt. Beide
Zahlen erfüllen also genau eine Bedingung. Die 9 ist sowohl durch 3 teilbar als
auch größer oder gleich 4. Sie erfüllt damit nicht genau eine Bedingung, sondern
beide und fällt somit bei xor() durch.
Bemerkung: Die Idee in Aufgabe 4.), für jedes Element die Anzahl der erfüllten
Bedingungen zu zählen, lässt sich ganz einfach auf folgende Fragestellung adaptieren:
Werden genau k von n (k ≤ n) Bedingungen erfüllt?
Beispiel: Wir wollen für den Vektor stapel aus (3) einen Indexvektor generieren.
Sehen wir in der Hilfe der Funktion seq() (?seq) nach, so stoßen wir auf die Funktion
seq_along(). Klingt sehr vielversprechend!
> stapel
[1] 4 7 6 2 3 5
Du fragst dich vielleicht, was der Unterschied zwischen den beiden Codezeilen ist.
Aus der Hilfe zum Sequenzoperator (?":") erfahren wir, dass 1:length(stapel)
intern haargenau seq(from = 1, to = length(stapel)) entspricht. Für den Spe-
zialfall, einen Indexvektor zu generieren, ist seq_along() etwas effizienter als die
seq()-Variante.
Eine weitere coole Sache ist die Rubrik See also in der R-Hilfe. Dort werden the-
matisch verwandte Funktionen aufgelistet.
Bei diesem Glücksspiel der österreichischen Lotterien wählen wir 6 Zahlen von
1 bis 45 aus. Wir vergleichen unsere 6 getippten Zahlen mit den 6 Zahlen
der offiziellen Ziehung. Je mehr Zahlen übereinstimmen, desto höher ist unser
Gewinnrang. Zusätzlich wird eine 7. Zahl gezogen – die Zusatzzahl, die uns
weitere Gewinnränge ermöglicht.
Wir versuchen unser Glück und füllen einen Lottoschein aus (Abb. 4.1).
1 2 3 4 5 6 1 2 3 4 5 6
7 8 9 10 11 12 7 8 9 10 11 12
13 14 15 16 17 18 13 14 15 16 17 18
19 20 21 22 23 24 19 20 21 22 23 24
25 26 27 28 29 30 25 26 27 28 29 30
31 32 33 34 35 36 31 32 33 34 35 36
37 38 39 40 41 42 37 38 39 40 41 42
43 44 45 43 44 45
Abbildung 4.1: Links: Ein blanker Lottoschein der Lotterie 6 aus 45. Rechts:
Unser ausgefüllter Lottoschein mit 6 angekreuzten Zahlen.
Frage: In welchen Zeilen und Spalten des Lottotippscheins befinden sich unsere
getippten Zahlen?
Wir betrachten eine Lösungsmöglichkeit. Dazu generieren wir uns mit rep() zu-
nächst die Zeilen- und Spaltenindizes aller Zahlen und selektieren zum Schluss die
Indizes unserer getippten Zahlen.
> spalte
[1] 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6
[37] 1 2 3 4 5 6 1 2 3
> zeile
[1] 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6
[37] 7 7 7 7 7 7 8 8 8
Nach einem Abgleich mit dem Lottoschein in Abb. 4.1 stellen wir fest: Es funktio-
niert! Jetzt brauchen wir nur noch die passenden Zeilen und Spalten zu selektieren.
Funktioniert für diese Lotterie einwandfrei. Allerdings ist der Code nicht wirklich
flexibel. Zur Motivation bilden wir in Abb. 4.2 die fiktive Lotterie 60 aus 450 ab,
für die es mit obigem Code düster aussähe. Wir müssten an 5 Stellen Änderungen
vornehmen. Wir erinnern uns an Regel 4 auf Seite 31: Programmiere möglichst
allgemein und automatisiert!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 2 3 4 5 6 21
41
22
42
23
43
24
44
25
45
26
46
27
47
28
48
29
49
30
50
31
51
32
52
33
53
34
54
35
55
36
56
37
57
38
58
39
59
40
60
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
7 8 9 10 11 12 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
13 14 15 16 17 18 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
19 20 21 22 23 24 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
25 26 27 28 29 30
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
31 32 33 34 35 36 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
37 38 39 40 41 42 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
43 44 45 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
441 442 443 444 445 446 447 448 449 450
Abbildung 4.2: Tippscheine von Lotto 6 aus 45 und Lotto 60 aus 450
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 55
Gehen wir es frohen Mutes an! Um unseren Code allgemein einsetzbar zu machen,
definieren wir zwei Hilfsvariablen:
• nzahlen speichert die Anzahl der Zahlen der Lotterie.
• ncol verwaltet die Anzahl der Spalten des Lottoscheins
Bemerkung: Mit dem Prefix n werden oft Anzahlen angedeutet.
Anschließend ersetzen wir jedes Vorkommen von 45 durch nzahlen und jedes Vor-
kommen von 6 durch ncol. Die korrekte Anzahl der Zeilen (8 bei Lotto 6 aus 45)
können wir aus den anderen beiden Variablen ausrechnen.
Hier müssen wir jedoch aufpassen...
> # Bestimme die Anzahl der Zeilen (rows) des Tippscheins - mit Fehler
> nrow <- nzahlen %/% ncol
> nrow
[1] 7
7 Zeilen ist eine Zeile zu wenig! Durch die ganzzahlige Division erhalten wir ledig-
lich die Anzahl der vollständigen Zeilen. Wir addieren die unvollständige Zeile im
folgenden Code noch hinzu.
> # Bestimme die korrekte Anzahl der Zeilen (rows) des Tippscheins
> nrow <- nzahlen %/% ncol + 1
> nrow
[1] 8
Es funktioniert wunderbar für 6 aus 45. Wenn du magst, kannst du dich gerne davon
überzeugen, dass der Code auch für 60 aus 450 fein arbeitet, wenn wir nur nzahlen
und ncol zu Beginn des Codes adaptieren.
Frage: Sind wir jetzt am Ziel? Funktioniert unser Code jetzt immer einwandfrei?
Bevor du weiterliest: Philosophiere zwei Minuten über diese Frage und versuche eine
Situation zu finden, in der unser Code nicht das korrekte Ergebnis liefert.
56 B Vektorfunktionen für Data Science und Statistik
Um diese Frage zu beantworten, werfen wir einen Blick auf Deutschland. Dort wird
Lotto 6 aus 49 gespielt und ein Tippschein sieht so aus wie in Abb. 4.3.
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31 32 33 34 35
36 37 38 39 40 41 42
43 44 45 46 47 48 49
Wir sehen: Die letzte Zeile ist voll. Führen wir unseren Code jetzt mit den deutschen
Lottodaten aus, so wird die Anzahl der Zeilen nicht korrekt bestimmt:
Es sollten aber 7 Zeilen und nicht 8 sein! Was können wir tun?
Um korrekt vorzugehen, überprüfen wir, ob die letzte Zeile voll ist und addieren nur
dann die 1 hinzu, wenn das nicht der Fall ist. Die Typumwandlung (Rechnen mit
Wahrheitswerten) aus (3.5.1) erweist sich hier als wertvolle Freundin!
Die letzte Zeile ist genau dann voll, wenn bei der ganzzahligen Division von nzahlen
durch ncol kein Rest bleibt, also wenn nzahlen %% ncol == 0 gilt.
45 dividiert durch 6 ergibt einen Rest von 3 6= 0, daher ist der erste Eintrag FALSE.
49 ist durch 7 teilbar; der Rest ergibt 0, womit das TRUE erklärt ist. Überall, wo ein
FALSE steht, soll jetzt eine 1 erzeugt werden, ansonsten eine 0. Diese Zahlen addieren
wir schließlich zu der Anzahl der vollständigen Zeilen hinzu.
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 57
Damit können wir für die Lotterien 6 aus 45 und 6 aus 49 die korrekte Anzahl von
Zeilen bestimmen: Wir bestimmen die Anzahl der vollständigen Zeilen und addieren
eine 1, falls es eine unvollstängige Zeile gibt:
> # Anzahl der Zeilen für die Lotterien 6 aus 45 und 6 aus 49
> c(45, 49) %/% c(6, 7) + (c(45, 49) %% c(6, 7) != 0)
[1] 8 7
Jetzt haben wir unser Ziel erreicht! Voller Stolz präsentieren wir unsere endgültige
Lösung :-)
Dieses Beispiel ist schon sehr anspruchsvoll. Die implizite Annahme, dass die
letzte Zeile des Lottotippscheins immer unvollständig ist, nur weil dies im Spezialfall
Lotto 6 aus 45 so ist, hat bei der deutschen Lotterie 6 aus 49 dazu geführt, dass die
Anzahl der Zeilen nicht korrekt bestimmt wurde.
Wenn du nicht gleich alles 100-prozentig begriffen hast, geht die Welt nicht unter!
Lass das Ganze einmal sacken und schaue dir dieses Beispiel später nochmal an.
Beherzige aber stets folgende Regel 6:
4.9 Abschluss
• Wie rufen wir die R-Hilfe zu Funktionen, Operatoren und Paketen auf? (4.1)
• Was können wir mit der Funktion seq() tun? Welche Parameter hat seq()
und was bewirken sie? Wie erfolgt der Funktionsaufruf? (4.2)
• Auf welche drei Arten können wir Objekte den formalen Argumenten einer
Funktion zuordnen und nach welchen Regeln erfolgt die Zuordnung? Werden
Objekte außerhalb einer Funktion von einer Funktion verändert? Was sind
Defaultwerte und welche zwei Arten von Defaultwerten gibt es? Was ist das
Dreipunkteargument? Wie übergeben wir Objekte an formale Argumente, die
nach dem Dreipunkteargument definiert sind? (4.3)
• Was können wir mit der Funktion rep() tun? Was bewirken die Parameter
times, length.out und each und wodurch unterscheiden sie sich? (4.4)
• Wie führen wir eine ganzzahlige Division durch und wie bestimmen wir den
Rest einer ganzzahligen Division? Wie ermitteln wir, ob eine Zahl durch eine
andere Zahl teilbar ist? (4.5)
• Was bedeutet komponentenweises Rechnen? Was passiert, wenn bei binären
Rechenoperationen beide Vektoren ungleich lange sind? (4.6)
• Wie werten wir die Exponentialfunktion aus? Wie berechnen wir Logarithmen
zu beliebigen Basen? Was ist eine vektorwertige Funktion? (4.7.1)
• Nach welchen Regeln runden die Funktionen round(), floor(), ceiling()
und trunc() jeweils? Wie funktioniert der Parameter digits in round()?
Mit welchem Trick können wir Zahlen auf beliebige k ∈ R runden? (4.7.2)
• Wie bestimmen wir den Absolutbetrag und das Vorzeichen eines numerischen
Vektors? (4.7.3)
• Wie berechnen wir den Sinus, Cosinus und Tangens eines numerischen Vektors?
Wie greifen wir auf die Zahl π zu? (4.7.4)
• Wie können wir bestimmen, ob alle Elemente eine Bedingung erfüllen bzw.
ob zumindest ein Element eine Bedingung erfüllt? Wie zählen wir die Anzahl
der Bedingungen, die jedes Element erfüllt? Wie erfragen wir, ob ein Element
genau eine von zwei Bedingungen erfüllt. (4.7.5)
Darüber hinaus sind wir uns der vielen Möglichkeiten bewusst, wie wir unser Wissen
selbstständig erweitern können (4.8.1) und wir hinterfragen kritisch unseren R-Code
bezüglich der (implizit) getroffenen Annahmen (4.8.2).
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 59
4.9.2 Ausblick
In (29) lernen wir eigene Funktionen zu schreiben und vertiefen dabei Konzepte
dieses Kapitels. Unter anderem besprechen wir die sinnvolle Parametrisierung von
Funktionen (inkl. Defaultwerte und dem Dreipunkteargument).
Falls du dich fragst, wozu das Dreipunkteargument überhaupt gut ist, dann hab
noch ein wenig Geduld! In (10) lernen wir mit paste() eine Funktion kennen, die
ohne Dreipunkteargument bei weitem nicht so bequem wäre! Fürs erste genügt es
für uns zu wissen, dass wir – dem Dreipunkteargument sei Dank – zum Beispiel statt
sum(c(1, 2, 3, 4)) auch sum(1, 2, 3, 4) schreiben können.
4.9.3 Übungen
• 1 1 1 1 2 2 2 2
• 1 1 2 2 1 1 2 2
• 1 2 1 2 1 2 1 2
2. Generiere die folgenden Vektoren ohne jede Zahl einzeln einzutippen.
• 2 1 4 3 6 5 8 7
• 1 2 3 5 6 7 9 10 11 13 14 15
3. Finde heraus, was bei der Funktion seq() passiert, wenn wir lediglich
5. Gegeben ist ein Vektor x <- c(1:4, NA). Studiere, was bei folgenden Abfra-
gen jeweils passiert. Was beobachtest du?
6. Gegeben ist ein beliebiger numerischer Vektor x ohne fehlende Werte. Wir wol-
len wissen, ob alle Einträge von x positiv sind. Welche der folgenden Codezeilen
liefert/liefern das gewünschte Resultat? Warum (nicht)?
7. In (3) hatten wir einen Kartenstapel, aus dem wir unter anderem jede zweite
Karte selektieren wollten. Wir betrachten folgenden Ansatz:
> stapel
[1] 4 7 6 2 3 5
> # Selektiere jedes zweite Element aus stapel (beginnend beim ersten)
> stapel[c(1, 3, 5)]
[1] 4 6 3
Wir haben bemängelt, dass dieser Ansatz nicht allgemein programmiert ist.
Modifiziere diesen Ansatz mit Hilfe von seq(), sodass er auch für größere
Kartenstapel funktioniert.
8. Ein Beispiel zu den von allen Computerfans heißgeliebten 2er-Potenzen.
a) Generiere einen Vektor, der alle 2er-Potenzen von 1 bis 31 enthält (also
2, 4, 8, 16, 32, ..., 2147483648).
b) Die wievielte 2er-Potenz überschreitet erstmals den Wert 10000?
c) Gibt es im Vektor aus 8a) eine 2er-Potenz, die durch 3 teilbar ist? Erstelle
eine Abfrage, die uns diese Frage entweder mit einem TRUE oder einem
FALSE beantwortet.
d) Wende den 2er-Logarithmus auf den Vektor aus 8a) an und kontrolliere,
ob tatsächlich 1:31 herauskommt. Erstelle auch hier eine Abfrage, welche
uns entweder ein TRUE oder ein FALSE liefert.
Übrigens: 231 − 1 ist eine Primzahl.
9. Bestimme alle durch 7 teilbaren Zahlen kleiner oder gleich 100. Finde mindes-
tens zwei konzeptionell verschiedene Möglichkeiten!
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 61
a) Was wird nach Ausführung dieser beiden Codezeilen auf die R-Console
gedruckt? Erkläre dabei auch die internen Abläufe.
b) Finde eine alternative und kürzere Schreibweise für obigen Code.
11. Wie können wir einen numerischen Vektor kaufmännisch runden? Verwende
zu Testzwecken zum Beispiel folgenden Vektor x:
12. Betrachte beispielhaft den Vektor x <- c(3, 4, 9, 2). Wie viele Elemente
von x sind ungerade Zahlen zwischen -5 und +5?
13. Was wird nach Ausführung der folgenden Codezeilen auf die Console gedruckt?
Erkläre dabei hinreichend genau die internen Abläufe! Gehe dabei insbesondere
auf die Zuordnung der Objekte auf die formalen Argumente ein.
In (2) haben wir gelernt, wie wir erstellten R-Code in Form von Skripten abspeichern
und laden können. In unseren R-Sitzungen erstellen wir aber auch Objekte, die wir
sichern wollen, um sie in einer neuen Sitzung wieder laden zu können.
Das ist unter anderem Thema dieses Kapitels und wir klären außerdem:
In (5.5) listen wir Funktionen zur Manipulation von Dateien und Ordnern auf.
5.1 Objekte
Mit der Funktion ls() fragen wir ab, welche Objekte uns derzeit in R zur Verfügung
stehen. Das sieht je nach Vorgeschichte unterschiedlich aus, zum Beispiel so:
Auf existierende Objekte können wir zugreifen, während beim Zugriff auf nicht exis-
tierende Objekte eine Fehlermeldung erscheint.
Wir können bestimmte Objekte löschen, indem wir die Funktion rm() (remove)
bemühen und die gewünschten Objekte der Reihe nach angeben und mit einem
Komma trennen.
> ls()
[1] "hand1" "hand2" "stapel" "x"
Die Objekte hand1 und hand2 existieren, wir können auf diese Objekte zugreifen.
Jetzt löschen wir diese beiden Objekte.
> ls()
[1] "stapel" "x"
> hand1
Fehler: Objekt ’hand1’ nicht gefunden
> hand2
Fehler: Objekt ’hand2’ nicht gefunden
> ls()
character(0)
Damit werden alle Objekte gelöscht. Wenn jetzt der R-Code ohne Fehlermel-
dung funktioniert, dann weißt du, dass alle verwendeten Objekte existieren.
64 B Vektorfunktionen für Data Science und Statistik
Das Arbeitsverzeichnis (working directory) ist jener Ordner, den uns R als erstes
anzeigt, wenn wir ein Skript (zum Beispiel das Skript zur Lösung quadratischer
Gleichungen aus (2.2.1)) laden wollen. Gleiches gilt, wenn wir ein Skript abspeichern
wollen. Es ist auch jener Ordner, in dem R nach abgespeicherten Objekten sucht,
sofern wir keinen anderen Ordner angeben.
Die Funktion getwd() (get working directory) zeigt uns den absoluten Dateipfad
des aktuellen Arbeitsverzeichnisses. Das kann etwa folgender Ordner sein:
Erscheint keine Fehlermeldung, so hat alles geklappt! Existiert der Ordner nicht, so
wird eine Fehlermeldung ausgegeben.
Neben absoluten Pfaden gibt es auch relative Dateipfade. R erkennt einen relativen
Pfad daran, dass keine Laufwerkinformation angegeben wird.
> getwd()
[1] "D:/Studium/Statistisches Programmieren"
R stellt uns die beiden Funktionen save() zum Sichern bzw. load() zum Laden
von Objekten zur Verfügung. Die Funktionen (vor allem save()) bieten uns mehr
Möglichkeiten, als wir im Moment brauchen. Wir sehen uns hier ein kurzes Bei-
spielprogramm an, in dem wir erläutern, wie wir die beiden Funktionen einsetzen
können.
Die Handhabung der Funktion save() ist relativ leicht. Zu Beginn der Funktion
steht das Dreipunkteargument ("..."), dem wir der Reihe nach – durch Kommata
getrennt – jene Objekte übergeben, die wir abspeichern wollen.
66 B Vektorfunktionen für Data Science und Statistik
Das Dreipunkteargument erleichtert uns in diesem Fall die Bedienung enorm! Denn
gäbe es das Dreipunkteargument nicht, dann könnten wir die Objekte immer nur
einzeln abspeichern, was unpraktikabel wäre.
Bei file schreiben wir den gewünschten Dateinamen, wobei die Dateiendung RData
sein sollte (Konvention). Im Ordner D:/Studium/Statistisches Programmieren
wird nach Durchlaufen des obigen Codes die Datei Karten.RData erzeugt, in der
sich die Objekte stapel, hand1 und hand2 befinden, so der Ordner existiert.
Beachte: Wir müssen den Parameter file exakt ansprechen, da dieser nach dem
Dreipunkteargument definiert ist. Siehe Regel 5 auf Seite 44.
Die erstellte Datei können wir ab sofort mit load() laden.
Die Funktion load() gibt uns die Namen der geladenen Objekte zurück. Dies ge-
schieht jedoch unsichtbar (invisible), das heißt wir können diese Information nur
dann einsehen, wenn wir sie zwischenspeichern. So wie oben im Objekt objekte.
Wenn wir wissen wollen, welche Ordner und Dateien sich im aktuellen Arbeitsver-
zeichnis befinden, so brauchen wir lediglich list.files() einzutippen. Das könnte
dann – wiederum je nach Vorgeschichte – zum Beispiel so aussehen:
Abschließend zeigen wir in Tab. 5.1 einige Funktionen, mit denen wir Dateien und
Ordner manipulieren können. Die Hilfe zu diesen Funktionen rufen wir mit "?files"
und "?files2" auf.
Tabelle 5.1: Funktionen zur Manipulation von Dateien und Ordnern. Funktionen,
die mit (*) markiert sind, haben mehr Parameter als angeführt.
Funktion Bedeutung
dir.exists(paths) Existieren die Ordner paths?
dir.create(path) (*) Ordner mit Pfad path erzeugen
file.create(...) (*) Datei(en) erzeugen
file.exists(...) Existieren die Ordner oder Dateien?
file.remove(...) Lösche Ordner oder Dateien
file.rename(from, to) Benenne die Ordner/Dateien from in to um
file.copy(from, to) (*) Kopiere die Ordner/Dateien from in to
Zunächst erzeugen wir die Datei "AlteDatei.txt", die wir schon gleich darauf
in "KopierteDatei.txt" kopieren. Anschließend benennen wir "AlteDatei.txt"
in "NeueDatei.txt" um. Die TRUE sagen uns, dass alle Manipulationen erfolg-
reich durchgeführt wurden. All dies geschieht dabei im aktuellen Arbeitsverzeichnis
"D:/Studium/Statistisches Programmieren".
Wenn wir Ordner oder Dateien manipulieren wollen, die sich nicht im aktuellen Ar-
beitsverzeichnis befinden, so geben wir den absoluten oder relativen Pfad (vgl. (5.2))
mit an. Zum Beispiel können wir statt file.create("AlteDatei.txt") äquivalent
auch folgendes schreiben:
file.create("D:/Studium/Statistisches Programmieren/AlteDatei.txt")
68 B Vektorfunktionen für Data Science und Statistik
Die Datei "AlteDatei.txt" existiert nicht (mehr), da wir sie ja umbenannt haben.
Wollen wir Dateien löschen, die nicht existieren, so werden wir von R gewarnt.
Das FALSE sagt uns, dass "AlteDatei.txt" nicht gelöscht werden konnte. Der hier
offensichtliche Grund wird in der Warnmeldung angezeigt. Jetzt existiert keine der
drei Dateien mehr.
Wir sehen uns in Übung 1 auf Seite 69 die Erstellung von Ordnern an.
5.6 Abschluss
• Wie erstellen wir neue Ordner bzw. Dateien? Mit welchen Funktionen können
wir Ordner und Dateien umbenennen, kopieren und löschen? Wie prüfen wir,
ob eine Datei oder ein Ordner existiert? (5.5)
5 Arbeitsverzeichnis, Objekte und Dateiordner 69
5.6.2 Ausblick
Die Anwendungen der Funktion list.files() sind vielseitiger, als wir an dieser
Stelle zeigen. Eine Anwendung ist etwa, Dateien mit bestimmten Dateiendungen zu
filtern. In (22) und (23) lernen wir jene Techniken kennen, die wir dazu brauchen.
Spannend ist auch die Aufgabe, einen Ordner nur dann zu erstellen, wenn er noch
nicht existiert. Die dazu notwendige if-Anweisung lernen wir in (28).
In (32) erfahren wir mehr über das Einlesen und Speichern von Daten und schauen
uns unterschiedliche Dateiformate an.
5.6.3 Übungen
In (4.8.2) ab Seite 53 haben wir einen Lottoschein von Lotto 6 aus 45 ausgefüllt
und in Infobox 1 auf Seite 53 diese Lotterie kurz vorgestellt. In Abb. 6.1 zeigen wir
unseren ausgefüllten Schein noch einmal.
1 2 3 4 5 6
7 8 9 10 11 12
13 14 15 16 17 18
19 20 21 22 23 24
25 26 27 28 29 30
31 32 33 34 35 36
37 38 39 40 41 42
43 44 45
In Abb. 6.2 sehen wir das Ergebnis der offiziellen Lottoziehung. Die Zahlen werden
in gezogener Reihenfolge dargestellt.
3 19 24 23 7 34 16
Abbildung 6.2: Gewinnzahlen der Lotterie 6 aus 45 in gezogener Reihenfolge. Die
Zusatzzahl 16 ist orange hinterlegt.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_6
6 Mengen, Sortieren und Kombinatorik 71
• Welche Zahlen und wie viele Zahlen haben wir richtig getippt? (6.1)
• Wie sortieren wir die Lottozahlen? (6.2.1)
• In welcher Reihenfolge müssten wir die Lottozahlen entnehmen, damit sie sor-
tiert sind? Und an die wievielte Stelle müssten wir jede Lottozahl setzen, damit
die Lottozahlen sortiert sind? (6.2.2), (6.2.3)
• Wie viele mögliche Ziehungsergebnisse gibt es beim Lotto? (6.5.1)
Darüber hinaus schauen wir uns in (6.2.5) an, wie wir Personen mit Hilfe der Mehr-
fachsortierung eindeutig nach ihrem Alter sortieren können und in (6.5.2), wie viele
unterscheidbare Playlists wir mit einer Tanz-CD erstellen können. Davor lernen wir
in (6.3), wie wir Fakultäten und Binomialkoeffizienten berechnen und in (6.4) bespre-
chen wir die wissenschaftliche Notation, die R bei der Darstellung von sehr großen
absoluten Zahlen und sehr kleinen absoluten Zahlen verwendet.
6.1 Mengenfunktionen
Wir wollen erfahren, welche Elemente eines Vektors in einem anderen Vektor enthal-
ten sind. Bevor wir eine unkomplizierte Lösungsmöglichkeit betrachten, versuchen
wir mit den bisher gelernten Techniken folgende Frage zu beantworten.
Beispiel: Wie viele Zahlen des Vektors tipp kommen im Vektor lotto vor?
> lotto
[1] 3 19 24 23 7 34 16
> tipp
[1] 4 7 15 19 20 38
In Übung 1 überlegen wir, warum dieser Ansatz scheitert. Versuchen wir es weiter.
> # Wie viele der getippten Zahlen sind auch in lotto enthalten?
> bool.richtig <- tipp[1] == lotto | tipp[2] == lotto | tipp[3] == lotto |
+ tipp[4] == lotto | tipp[5] == lotto | tipp[6] == lotto
> bool.richtig
[1] FALSE TRUE FALSE FALSE TRUE FALSE FALSE
72 B Vektorfunktionen für Data Science und Statistik
> sum(bool.richtig)
[1] 2
Sieht schon besser aus! Es kommen also 2 Zahlen von tipp auch in lotto vor.
Wir gehen also jede getippte Zahl einzeln durch und prüfen auf Gleichheit mit den
gezogenen Lottozahlen. Durch die Veroderung sammeln wir die dabei entstandenen
TRUE ein. Wir erfahren also, welche Lottozahlen auch in unserem Tipp vorkommen.
Engagierten R-Nachwuchshoffnungen wird aber schnell klar: Das kann nicht der
Weisheit letzter Schluss sein! Muskelkater in den Fingern wollen wir uns gerne er-
sparen! Außerdem ist obiger Lösungsansatz fehleranfällig (copy and paste error)!
Der %in%-Operator ermöglicht uns eine deutlich kürzere Umsetzung. Er überprüft
für jedes Element des linken Vektors, ob es im rechten Vektor eine Übereinstimmung
(ein Matching) gibt.
Das erste Element von tipp (4) hat keinen Partner im Vektor lotto. Für das zweite
Element von tipp (7) hingegen findet R eine Übereinstimmung mit einem Element
aus lotto, womit das erste TRUE erklärt ist.
Die Anzahl der Übereinstimmungen zu ermitteln ist für uns nur noch Formsache!
Wenn wir die Anzahl der Übereinstimmungen zählen wollen, so ist es übrigens egal,
welcher der beiden Vektoren links steht.
Die orange bzw. grün markierten TRUE gehören jeweils zu 7 bzw. 19.
Die Varianten lotto[tipp %in% lotto] und tipp[lotto %in% tipp] funktionie-
ren nicht. Den Grund dafür überlegen wir uns in Übung 2.
6 Mengen, Sortieren und Kombinatorik 73
Mit der Funktion unique() können wir aus einem Vektor mehrfach vorkommen-
de Elemente entfernen. Betrachten wir ein kleines Beispiel:
Mit dem %in%-Operator und der Funktion unique() können wir Mengenfunktionen
nachbauen. Wir betrachten gleich die Vereinigung und Schnittmenge. In Übung 3
auf Seite 87 sehen wir uns die Differenzmenge und symmetrische Differenz an.
Beispiel: Gegeben sind die Mengen A = {2, 3, 4, 5, 7} und B = {1, 2, 6, 7}. Bestim-
me die Vereinigung A ∪ B sowie die Schnittmenge A ∩ B.
> A > B
[1] 2 3 4 5 7 [1] 1 2 6 7
A 2 3 4 5 7
A %in% B TRUE FALSE FALSE FALSE TRUE
Jedes Element aus A, das auch in B vorkommt, wird mit einem TRUE markiert. Diese
Elemente werden sodann aus dem Vektor A selektiert.
74 B Vektorfunktionen für Data Science und Statistik
Bemerkung: Wenn du dich daran anstößt, dass die Elemente in der Vereinigungs-
menge nicht sortiert sind, so möge dich (6.2) (Sortieren) inspirieren!
Die Schnittmenge bzw. Vereinigung zweier Mengen können wir auch mit Hilfe der
Funktionen intersect() bzw. union() bilden. Wir finden beide Funktionen gele-
gentlich innerhalb anderer Funktionen.
Beispiel: Selbes Beispiel wie im vergangenen Abschnitt. Gegeben sind die Mengen
A = {2, 3, 4, 5, 7} und B = {1, 2, 6, 7}. Bestimme die Vereinigung A ∪ B sowie die
Schnittmenge A ∩ B.
> A > B
[1] 2 3 4 5 7 [1] 1 2 6 7
6.2 Sortieren
Beim Thema Sortieren denken wohl viele zunächst daran, eine Menge von Elementen
auf- oder absteigend zu sortieren, zum Beispiel Lottozahlen aufsteigend sortieren,
Namen alphabetisch sortieren usw.
Die in der Praxis sehr wichtige und relevantere Anwendung: einen Vektor gemäß
eines anderen sortieren. Zum Beispiel könnten wir Personen dem Alter nach ordnen.
Mit anderen Worten: Selektiere die Personen in jener Reihenfolge, sodass sie nach
dem Alter sortiert sind.
Eine dritte Anwendung: Wir können uns auch für den Rang eines Elements interes-
sieren, um Aussagen wie „Person x ist die i. älteste Person“ treffen zu können.
Es gibt also drei Arten, den Begriff Sortieren auszulegen:
1. Sortierung der Einträge selbst (sort()) (6.2.1)
2. Generierung jener Reihenfolge, in der wir die Elemente eines Vektors entneh-
men müssen, damit sie sortiert sind. (order()) (6.2.2)
3. Ermittlung der Ränge der Einträge (rank()) (6.2.3)
Beachte, dass wir mit order() auch jene Reihenfolge finden können, in der wir die
Elemente eines Vektors entnehmen müssen, damit sie gemäß eines anderen Vektors
sortiert sind. Ein cooles Beispiel schauen wir uns in (6.2.5) an.
6 Mengen, Sortieren und Kombinatorik 75
Anhand dieses kleinen Beispielvektors schauen wir uns die drei Funktionen an:
> # Beispielvektor
> x <- c(5, 7, 5, 3, 4)
Die Funktion sort() sortiert die Einträge standardmäßig aufsteigend. Mit der Ein-
stellung decreasing = TRUE können wir absteigend sortieren.
## Default S3 method:
sort(x, decreasing = FALSE, na.last = NA, ...)
Den Sinn des Parameters na.last schauen wir uns in (14.3.3) an.
Beispiel: Wir wollen die Lottozahlen aufsteigend sortieren. Die Zusatzzahl (letzte
gezogene Zahl) soll aber an der letzten Stelle stehen.
> lotto
[1] 3 19 24 23 7 34 16
Stopp! Die Zusatzzahl 16 soll nicht mitsortiert werden, sie soll nach hinten gereiht
werden.
Eine kleine Modifikation löst das Problem: Wir sortieren den Vektor lotto ohne das
letzte Element und hängen die Zusatzzahl anschließend dran.
In welcher Reihenfolge müssen wir die Einträge entnehmen um einen sortierten Vek-
tor zu erhalten? Diese Frage beantwortet uns die Funktion order().
76 B Vektorfunktionen für Data Science und Statistik
Bei method verweisen wir auf die R-Hilfe und na.last betrachten wir in (14.3.3).
Index 1 2 3 4 5
x 5 7 5 3 4
order(x) 4 5 1 3 2
sort(x) 3 4 5 5 7
Abbildung 6.3: Veranschaulichung der Funktion order()
Wie bei sort() können wir auch hier mit decreasing = TRUE absteigend sortie-
ren. Wir müssen dabei decreasing exakt ansprechen, da das erste Argument von
order() das Dreipunkteargument ist (vgl. Regel 5 auf Seite 44). Bei der Mehrfach-
sortierung in (6.2.5) wird uns der Sinn des Dreipunktearguments eröffnet.
Mit rank() können wir Ränge ermitteln, wobei kleine Elemente niedrige Ränge
und große Elemente hohe Ränge erhalten. Wir können also die Frage klären, an
welcher Stelle die Elemente eines Vektors eingeordnet werden müssen, damit sie
aufsteigend sortiert sind.
6 Mengen, Sortieren und Kombinatorik 77
Den Parameter na.last betrachten wir (wie du evtl. schon ahnst) in (14.3.3).
> x
[1] 5 7 5 3 4
Der 1. Eintrag (5) hat den Rang 3.5. Da es einen zweiten 5er im Vektor x gibt,
wird für beide defaultmäßig der Durchschnittsrang ermittelt ((3 + 4)/2 = 3.5).
Dieses Tie-Verhalten (tie: englisch für Bindung, Unentschieden) können wir mit dem
Parameter ties.method steuern.
> x
[1] 5 7 5 3 4
Um einen sortierten Vektor zu erhalten, müssen wir die 5 an die 3. Stelle platzieren,
die 7 an die 5. Stelle, die 5 an die 4. Stelle, die 3 an die 1. und die 4 an die 2. Stelle.
Wir veranschaulichen in Abb. 6.4 die Funktion rank().
5 7 5 3 4 x
3 5 4 1 2 rank(x, ties.method = "first")
1 2 3 4 5 Index
3 4 5 5 7 sort(x)
Abbildung 6.4: Veranschaulichung der Funktion rank()
In Tab. 6.1 auf der nächsten Seite sind vier Möglichkeiten zur Steuerung des Tie-
Verhaltens (ohne last und random) aufgelistet.
Bei average (Standardmethode) wird der Durchschnittsrang ermittelt. Bei min wird
jeder Beobachtung der niedrigst mögliche Rang zugewiesen, wie es zum Beispiel bei
Ex-aequo-Platzierungen in Schirennen der Fall ist, bei max der größtmögliche. Bei
first und last werden Ties nach der Reihenfolge des Auftretens aufgelöst und die
Einstellung random löst Ties zufällig auf.
78 B Vektorfunktionen für Data Science und Statistik
x 5 7 5 3 4
ties.method = "average" 3.5 5 3.5 1 2
ties.method = "min" 3 5 3 1 2
ties.method = "max" 4 5 4 1 2
ties.method = "first" 3 5 4 1 2
Oft reicht ein Sortierkriterium nicht aus, um einen Vektor eindeutig zu sortieren. In
dem Fall wollen wir weitere Kriterien angeben, um Ties (Unentschieden) aufzulösen.
Tab. 6.2 enthält die Namen und die Geburtsdaten von einigen bei der Fußball-EM
2017 eingesetzten österreichischen Fußballspielerinnen.3
Name Geburtsdatum
Manuela Zinsberger 19.10.1995
Carina Wenninger 06.02.1991
Viktoria Schnaderbeck 04.01.1991
Katharina Schiechtl 27.02.1993
Laura Feiersinger 05.04.1993
Sarah Zadrazil 19.02.1993
Wir stellen nach kurzer Überlegung fest: order() ist die Funktion unserer Wahl.
Denn wir wollen die Namen der Spielerinnen in jener Reihenfolge selektieren, sodass
die Namen nach dem Geburtsdatum sortiert sind. Und genau für Aufgaben wie diese
wurde order() erfunden!
Carina Wenninger hat das niedrigste Geburtsjahr und wäre somit die Älteste. Nun
sortieren wir die Namen der Spielerinnen in umgekehrter Reihenfolge.
Manuela Zinsberger hat das höchste Geburtsjahr und wäre somit die Jüngste.
Leider stimmt das nicht ganz. Ein Blick auf die Geburtsdaten der Spielerinnen verrät
uns, dass das Geburtsjahr nicht für eine korrekte Sortierung ausreicht:
• Carina Wenninger und Viktoria Schnaderbeck sind beide im Jahr 1991 auf die
Welt gekommen und Viktoria Schnaderbeck ist um ca. einen Monat älter.
• Katharina Schiechtl und Sarah Zadrazil sind nicht nur im selben Jahr geboren
worden, sie haben sogar denselben Geburtsmonat.
Wir haben also Ties, ein Fall für die Mehrfachsortierung! Wir können dem Drei-
punkteargument zu Beginn von order() beliebig viele Vektoren übergeben. Es wird
zunächst nach dem 1. Vektor sortiert. Etwaige Ties werden dann der Reihe nach mit
Hilfe der darauf folgenden Vektoren aufgelöst.
80 B Vektorfunktionen für Data Science und Statistik
Wie wäre es, wenn wir zunächst nach jahr, dann nach monat und schließlich nach
tag sortieren würden? Gut? Dann machen wir es so ;-)
Wir überzeugen uns in Tab. 6.3, dass unsere Idee wirklich gut war.
Name Geburtsdatum
Manuela Zinsberger 19.10.1995
Laura Feiersinger 05.04.1993
Katharina Schiechtl 27.02.1993
Sarah Zadrazil 19.02.1993
Carina Wenninger 06.02.1991
Viktoria Schnaderbeck 04.01.1991
Viktoria Schnaderbeck und nicht Carina Wenninger ist die Älteste! Zumindest unter
den 6 genannten Spielerinnen.
Im folgenden Beispiel packen wir zwei geniale Tricks aus der Trickkiste aus.
Beispiel: Wir setzen das Beispiel des vergangenen Abschnitts fort. Wir möchten
jeder Spielerin den Altersrang zuordnen. Die Jüngste soll dabei den Rang 1 erhalten.
Die Funktion rank() sieht vielversprechend aus. In der Hilfe (?rank) stellen wir
jedoch fest, dass es keine Möglichkeit gibt, Ties nach unseren Wünschen aufzulösen.
Das Jahr alleine reicht ja nicht für eine eindeutige Sortierung aus.
Frage: Was können wir tun? Bevor du weiterliest: Nimm dir eine Minute Zeit und
überlege dir eine mögliche Lösung für das Problem.
Schon eine Idee? Kommen wir zur Auflösung!
Trick 17a: Wir basteln uns eine Funktion N3 7→ N, welche das Jahr, den Monat
und den Tag auf eine Zahl abbildet und zwar so, dass die Ordnung korrekt ist.
Das erreichen wir zum Beispiel mit folgender Berechnung:
jahr · 10000 + monat · 100 + tag
6 Mengen, Sortieren und Kombinatorik 81
rank() ordnet der niedrigsten Beobachtung den Rang 1 zu. Wir wollen jedoch der
höchsten Beobachtung (hohe Jahreszahl = junge Spielerin) den Rang 1 zuordnen.
Was tun wir jetzt? Falls du an den Parameter decreasing denken solltest, dann
müssen wir dich leider enttäuschen. Einen solchen Parameter kennt rank() nicht.
Kommen wir also zum zweiten Trick, der trotz seiner Einfachheit in vielen Situatio-
nen super anwendbar ist.
Trick 17b: Die Vorzeichen umdrehen!
Wir fassen beide Tricks übersichtlich zusammen.
Wenn wir mit rank() eine Mehrfachsortierung durchführen wollen, dann bilden
wir die Sortierkriterien auf eine Zahl ab, sodass die Ordnung korrekt ist. Wenn
wir absteigend sortieren wollen, dann drehen wir die Vorzeichen um.
Mit der Funktion factorial() können wir Fakultäten à la n! berechnen und die
Funktion choose() ermöglicht uns die Bestimmung des Binomialkoeffizienten nk .
Der linke Code berechnet vektorwertig die Ausdrücke 3! = 6 und 4! = 24, während
der rechte Code die Ausdrücke 32 = 3 und 42 = 6 bestimmt.
Eine nützliche Funktion in der Kombinatorik ist prod(), die das Produkt eines
Vektors berechnet.
Gemeinsam mit den arithmetischen Rechenoperatoren lassen sich damit bereits viele
kombinatorische Aufgaben lösen. In (6.5.1) und (6.5.2) schauen wir uns zwei inter-
essante Fallbeispiele an. Und schulen dabei das wichtige Konzept der allgemeinen
und automatisierten Programmierung.
R druckt Zahlen ab einer gewissen absoluten Größe oder Kleine in der wissenschaftli-
chen Notation aus. Wir betrachten ein Beispiel: Wie hoch ist die Wahrscheinlichkeit
in Lotto 6 aus 45, mit nur einem Tipp einen Lottosechser zu erzielen?
> # Wahrscheinlichkeit eines Lottosechsers mit einem Tipp in Lotto 6 aus 45.
> 1 / choose(45, 6)
[1] 1.227738e-07
Das e-07 am Ende der Ausgabe teilt uns mit, dass wir die Zahl davor mit 10−7
multiplizieren müssen, also 1.227738 · 10−7 = 0.0000001227738.
Gleiches gilt in die andere Richtung. Wie viele Möglichkeiten gibt es, ein Kartenset
bestehend aus 52 Karten gleichmäßig auf zwei Spieler zu verteilen?
Wollen wir die wissenschaftliche Notation abschalten, so können wir den Para-
meter scipen (scientific penalty) in der Funktion options() auf einen hinreichend
großen Wert setzen. Für scipen = 0 (Standard) wird etwa 10−k und 10k+1 für k ≥ 4
in wissenschaftlicher Notation dargestellt. Der Wert für scipen kann auch negativ
sein, um die wissenschaftliche Notation zu forcieren.
Die Funktion options() steuert eine Vielzahl an R-Eigenschaften, wie etwa das An-
zeigeverhalten von Warnmeldungen und den Umgang mit fehlenden Werten. Wir
vertiefen diese Funktion nicht, behalten uns aber in Erinnerung, dass es diese Funk-
tion gibt und dass wir selbstverständlich die R-Hilfe dazu durchlesen können ;-)
Stellen wir die Optionen um, so gibt uns options() die derzeitigen Einstellungen
(unsichtbar) zurück. Es ist ratsam, diese Einstellungen zu sichern (hier auf opt), um
die alten Optionen später wiederherstellen zu können.
4 Wie in Österreich wird auch in Deutschland eine 7. Zahl gezogen, die dort als „Superzahl“
bezeichnet wird und die ersten 6 Zahlen werden in aufsteigender Reihenfolge präsentiert.
84 B Vektorfunktionen für Data Science und Statistik
Schon haben wir eine allgemeine Lösung gefunden. Implizit nehmen wir dabei an,
dass genau eine Zusatzzahl gezogen wird.
Wenn wir uns lediglich für die Anzahl der unterscheidbaren Lottosechser interessie-
ren, dann lassen wir die Zusatzzahl einfach weg.
Beispiel: Auf einer Tanz-CD für Standardtänze befinden sich folgende Songs:
• 5 langsame Walzer
• 3 Wiener Walzer
• 3 Tangos
• 4 Quicksteps
• 1 Slowfox
• 2 Boogies
Wir möchten gerne jeden Tanz genau einmal tanzen. Wie viele unterschiedliche
Playlists können wir mit dieser CD erstellen?
Wiederum eine Vorüberlegung für 2 Tänze, die wir leicht verallgemeinern können.
Jeder der 5 langsamen Walzer kann mit jedem der 3 Wiener Walzer kombiniert
werden, macht also 5 · 3 Kombinationen. Für jede dieser Kombinationen gibt es 2!
mögliche Reihenfolgen, macht also 5 · 3 · 2! Playlists. Die Verallgemeinerung dieser
Vorüberlegung führt zu unserer ersten Lösung.
Stimmt zwar, hat jedoch folgenden Nachteil: Wir missachten Regel 4 (allgemein
und automatisiert Programmieren). Wir könnten jetzt natürlich 6 Hilfsvariablen
definieren, das hat jedoch den Nachteil, dass dadurch der Code lange wird und wir
anfällig gegenüber Veränderungen der Problemstellung sind. So könnte etwa auf einer
anderen CD kein Boogie enthalten sein.
Wir präsentieren eine allgemeiner programmierte Lösung.
6 Mengen, Sortieren und Kombinatorik 85
Très chic! Wenn wir auf einer anderen CD lauter lateinamerikanische Lieder entde-
cken, so brauchen wir nur den Vektor nlieder zu adaptieren.
6.6 Abschluss
• Wie fragen wir ab, welche Elemente eines Vektors Matchings (Übereinstim-
mungen) mit Elementen eines anderen Vektors haben? Was müssen wir bei
der Selektion der gemeinsamen Elemente beachten? (6.1.1)
• Mit welcher Funktion streichen wir mehrfach vorkommende Elemente aus ei-
nem Vektor? (6.1.2)
• Wie bestimmen wir die Schnittmenge und Vereinigungsmenge zweier Mengen?
(6.1.3), (6.1.4)
• Wie sortieren wir einen Vektor auf- bzw. absteigend? (6.2.1)
• Wie bestimmen wir jene Reihenfolge, in der wir die Elemente eines Vektors
entnehmen müssten, damit sie sortiert sind (auf- bzw. absteigend)? In welcher
Anwendung ist es notwendig, diese Reihenfolge zu wissen? (6.2), (6.2.2)
• Wie bestimmen wir die Ränge von Elementen eines Vektors und welche Mög-
lichkeiten für die Auflösung von Ties (Unentschieden) stehen uns dabei zur
Verfügung? (6.2.3)
• Wie drehen wir einen Vektor um? (6.2.4)
• Wie sortieren wir einen Vektor nach mehreren Sortierkriterien auf- bzw. ab-
steigend? (6.2.5)
• Wie führen wir eine Mehrfachsortierung bei der Rangbildung durch? (6.2.6)
• Wie bestimmen wir die Ausdrücke n! bzw. nk ? Mit welcher Funktion berech-
6.6.2 Ausblick
Neben all den wichtigen Funktionen dieses Kapitels ist die Funktion order() beson-
ders wichtig! Freunde dich also lieber so früh wie möglich mit dieser coolen Funktion
an, denn sie wird uns in zahlreichen weiteren Kapiteln wertvolle Dienste erweisen.
Und zwar in so vielen, dass wir sie an dieser Stelle aus Platzgründen nicht auflisten.
Auch der %in%-Operator wird uns noch häufig begegnen. Wenn du den Unterschied
zwischen "%in%" und "==" verstehst, bist du gut gerüstet!
In (8) begegnen wir kumulierenden Funktionen und lösen mit ihrer Hilfe weitere
spannende Aufgaben aus der Welt der Wahrscheinlichkeitsrechnung, so zum Beispiel
das Geburtstagsproblem in (8.5.2).
6.6.3 Übungen
1. In (6.1.1) wollten wir zunächst mit folgender Anweisung erfragen, welche Ele-
mente des Vektors tipp auch im Vektor lotto vorkommen:
> tipp
[1] 4 7 15 19 20 38
> lotto
[1] 3 19 24 23 7 34 16
> # Selektiere alle Zahlen aus lotto, die auch in tipp vorkommen.
> lotto[lotto %in% tipp]
[1] 19 7
Ein Kollege behauptet, dass die folgenden beiden Anweisungen das identische
Resultat liefern wie obiger Code. Erkläre ihm möglichst verständlich, warum er
sich irrt! Welcher tückische Fehler wurde in der zweiten Anweisung gemacht?
4. Werfen wir erneut einen Blick auf die in (6.2.5) ab Seite 78 angeführten Fuß-
ballspielerinnen.
a) Welche der Fußballspielerinnen sind vor dem 20.02.1993 auf die Welt ge-
kommen?
Hinweis: Unterscheide mehrere Fälle (Jahr, Monat, Tag) und baue aus
ihnen mit geeigneten logischen Verknüpfungen die Lösung zusammen.
b) Sortiere die Namen der Spielerinnen chronologisch nach dem Auftreten
ihres Geburtstages in einem Kalenderjahr.
5. Gegeben sind die Seitenlängen a und b von vier Rechtecken sowie deren Flächen
area:
c) Sortiere die Seitenlänge a aufsteigend nach der Fläche und im Falle von
Ties weiter nach der Seitenlänge b. Welcher der folgenden Codes löst diese
Aufgabe korrekt?
Finde einen alternativen Code, der inhaltlich dasselbe tut und für x1
dasselbe Ergebnis liefert.
b) Für den Vektor x <- c(2, 4, 1, 2, 3) funktioniert obiger Code leider
nicht. Erkläre warum und modifiziere den Code unter Beibehaltung der
Funktion rank(), sodass er funktioniert.
7 Deskriptive Statistik
Der Vektor apfel.voll enthält eine Stichprobe mit 53 abgewogenen Äpfeln. Zwecks
Übersicht beschränken wir uns hier aber auf die kleinere Stichprobe in apfel.teil,
die lediglich aus 10 dieser 53 Äpfel besteht und zusätzlich einen Messfehler enthält:
> apfel.teil
[1] 134 165 155 19 199 142 119 143 150 198 149
Der Vektor apfel unterscheidet sich von apfel.teil nur durch ein winziges aber
entscheidendes Detail. Welches Detail das ist, lösen wir in (7.2) auf ;-)
Wir gehen in diesem Kapitel folgenden Fragen nach:
Zunächst verschaffen wir uns aber in (7.1) einen Überblick über die Funktionen.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_7
90 B Vektorfunktionen für Data Science und Statistik
Bevor wir loslegen, geben wir in Tab. 7.1 eine Übersicht über gängige Funktionen
der deskriptiven Statistik.
Funktion Bedeutung
min() Minimum
max() Maximum
range() Minimum und Maximum
mean() Mittelwert
var() Varianz
sd() Standardabweichung (standard deviation)
median() Median
quantile() Beliebige Quantile
Alle angeführten Funktionen bieten den Parameter na.rm an, mit dem wir mittels
na.rm = TRUE fehlende Werte ausschließen können. (vgl. (4.3.3)). Es empfiehlt
sich, na.rm exakt zu benennen; bei min(), max() und range() ist das sogar not-
wendig.
Kleines Detail am Rande: Die Funktionen var(), sd(), median(), quantile() sind
Funktionen des Pakets stats (help(package = stats)), das beim Programmstart
von R automatisch geladen wird.
Mit den Funktionen min() bzw. max() bestimmen wir das Minimum bzw. Maximum
aller Elemente. Die Funktion range() gibt uns einen Vektor mit dem Minimum und
Maximum zurück.
Beispiel: Wie schwer ist der leichteste bzw. der schwerste Apfel?
> apfel.teil
[1] 134 165 155 19 199 142 119 143 150 198 149
Das Minimum von 19 kommt uns – verglichen mit den anderen Werten – seltsam
niedrig und unplausibel vor. Das wäre gar ein bisschen leicht.
7 Deskriptive Statistik 91
> # Vektor kopieren und dem unplausiblen Wert einen fehlenden Wert zuweisen
> apfel <- apfel.teil
> apfel[apfel == min(apfel.teil)] <- NA
> apfel.teil
[1] 134 165 155 19 199 142 119 143 150 198 149
> apfel
[1] 134 165 155 NA 199 142 119 143 150 198 149
Damit ist geklärt, worin genau der kleine aber entscheidende Unterschied zwischen
den beiden Vektoren apfel.teil und apfel besteht. Wir kommen in (7.6.1) auf die
inhaltliche Bedeutung dieses Unterschieds zu sprechen.
Nachdem wir jetzt den unplausiblen Wert abgehakt haben, berechnen wir erneut
das Minimum und Maximum.
> range(apfel)
[1] NA NA
Hoppla! Wir erhalten NA. Wir möchten aber gerne fehlende Werte ausschließen und
erinnern uns an den Parameter na.rm, den wir in (4.3.3) erstmalig besprochen und
in (7.1) erneut erwähnt haben.
Jetzt sehen die Werte deutlich plausibler aus! Wir rechnen ab sofort mit dem Vektor
apfel weiter.
Beispiel: Wie viele Gramm liegen zwischen dem schwersten und dem leichtesten
Apfel? Mit anderen Worten: Berechne die Spannweite von apfel.
Die Spannweite ist die Differenz zwischen dem Maximum und dem Minimum.
Es liegen also 80 Gramm zwischen dem schwersten und dem leichtesten Apfel.
92 B Vektorfunktionen für Data Science und Statistik
Mit Hilfe der Funktionen mean(), var() und sd() berechnen wir den Mittelwert x̄,
die Varianz s2 und die Standardabweichung s unserer Apfelstichprobe.
In dieser Stichprobe wiegt also ein Apfel im Mittel 155.4 Gramm. Der Mittelwert
für die volle Stichprobe apfel.voll weicht mit 155.2 Gramm nur geringfügig ab.
Was hast du zu Kapitelbeginn geschätzt?
Beispiel: Selektiere alle überdurchschnittlich schweren Äpfel.
> apfel
[1] 134 165 155 NA 199 142 119 143 150 198 149
> # Überdurchschnittlich schwere Äpfel selektieren
> apfel[apfel > mean(apfel, na.rm = TRUE)]
[1] 165 NA 199 198
Wir sehen, dass fehlende Werte mitselektiert werden. Wie wir das verhindern können,
lernen wir in (14). Falls du zu den Ungeduldigen gehörst, dann darfst du dir jetzt
schon die Funktion is.na() ansehen! Oder/Und in Übung 7 auf Seite 98.
In (7.6.2) schauen wir uns die Standardisierung an, die wir mit mean() und sd()
einfach und bequem vornehmen können.
Kommen wir zu den allseits beliebten Quantilen, die wir in Infobox 3 auffrischen.
Das p-Quantil (p ∈ [0, 1]) einer Datenreihe x ist ein Wert x̃p , sodass der Anteil
p der Werte von x kleiner und der Anteil 1 − p größer als x̃p ist.
Der Median ist das 0.5-Quantil: 50 % der Werte sind größer und 50 % kleiner
als der Median.
Das 0.25-Quantil heißt auch unteres Quartil und das 0.75-Quantil heißt analog
oberes Quartil.
7 Deskriptive Statistik 93
> sort(apfel)
[1] 119 134 142 143 149 150 155 165 198 199
> median(apfel, na.rm = TRUE)
[1] 149.5
50 Prozent der Äpfel wiegen weniger und 50 Prozent wiegen mehr als 149.5 Gramm.
Der Median ergibt sich hier als Mittelwert zwischen 149 und 150.
Wollen wir allgemeine Quantile berechnen, so greifen wir zu quantile().
Mit dem Parameter probs geben wir an, welche Quantile wir bestimmen wollen.
Standardmäßig werden das 0 %-, 25 %-, 50 %-, 75 %- und 100 %- Quantil bestimmt.
Welche der 9 angebotenen Berechnungsmethoden zur Anwendung kommt, steuert
type. Wir gehen auf die zugrunde liegenden Ideen in diesem Buch nicht ein. Es lohnt
sich aber, diesbezüglich einen Blick in die R-Hilfe (?quantile) zu werfen!
> # Variante 1: Subseting mit Indizes > # Variante 2: Subsetting mit names
> temp[2] - temp[1] > temp["75%"] - temp["25%"]
75% 75%
20.25 20.25
An dieser Stelle sei bereits erwähnt, dass die 2. Variante sicherer ist. Lassen wir den
Codeteil probs = c(0.25, 0.75) weg, so funktioniert temp[2] - temp[1] nicht
mehr sauber, da sich die Indizierung ändert. temp["75%"] - temp["25%"] klappt
hingegen nach wie vor! In (12) befassen wir uns genau mit Beschriftungen.
94 B Vektorfunktionen für Data Science und Statistik
Mit summary() verschaffen wir uns einen schnellen Überblick über unsere Daten.
> summary(apfel)
Min. 1st Qu. Median Mean 3rd Qu. Max. NA’s
119.0 142.2 149.5 155.4 162.5 199.0 1
R liefert uns einen Vektor mit Minimum, unterem Quartil, Median, Mittelwert, obe-
rem Quartil und Maximum aller gültigen Beobachtungen zurück. Insbesondere er-
fahren wir auch, dass es 1 fehlenden Wert (NA’s) gibt.
Ausreißer sind Werte, die auffällig stark von den restlichen Werten abweichen. So
haben wir in (7.2) bemerkt, dass der Wert 19 auffällig stark abweicht.
> apfel.teil
[1] 134 165 155 19 199 142 119 143 150 198 149
Minimum und Maximum sind extrem anfällig gegenüber Ausreißern und eignen sich
daher besonders gut zum Aufspüren etwaiger Ausreißer. Mittelwert und Standardab-
weichung sind ebenfalls anfällig. Der Ausschluss des Ausreißers 19 sorgt dafür, dass
der Mittelwert um 155.4 - 143 = 12.4 Gramm steigt. Diesen Umstand müssen
wir etwa bei statistischen Tests berücksichtigen, die auf Mittelwerten beruhen. Der
Median ist hingegen robust gegenüber Ausreißern. Er verändert sich kaum.
7.6.2 Standardisierung
Ein sehr wichtiges Konzept in der Statistik ist die Standardisierung. Sei x ein Da-
tenvektor, x̄ der Mittelwert und s die Standardabweichung, dann ist der Vektor
x − x̄
y=
s
standardisiert. Das heißt, der Vektor y hat jetzt Mittelwert 0 und Standardabwei-
chung 1. Mit diesem Konzept lassen sich einige Aufgaben elegant lösen.
Beispiel: Das Gewicht von wie vielen Äpfeln des Vektors apfel.teil ist um mehr
als zwei Standardabweichung vom Mittelwert entfernt?
> apfel.teil
[1] 134 165 155 19 199 142 119 143 150 198 149
Ergebnis: Ein Apfel (der mit dem Gewicht 19). Auch so können wir auffällige Werte
ermitteln. Abb. 7.1 zeigt eine Visualisierung der Stichprobe.
Machen wir den R-Code mit Hilfe der Standardisierung kürzer! Formen wir die
Bedingung aus Variante 1 einmal um:
x − x̄ x − x̄ x − x̄
x < x̄ − 2 · s ∨ x > x̄ + 2 · s ⇔ − >2∨ >2⇔
>2
s s s
> # Variante 2: Vektor standardisieren
> apfel.teil.scale <- (apfel.teil - xquer) / s
> apfel.teil.scale
[1] -0.18803943 0.45965194 0.25071924 -2.59076546 1.17002311 -0.02089327
[7] -0.50143848 0.00000000 0.14625289 1.14912984 0.12535962
Wichtig ist, dass wir an den Absolutbetrag abs() denken und die Klammern richtig
setzen. Ein häufiger Fehler ist etwa sum(abs(apfel.teil.scale > 2)).
96 B Vektorfunktionen für Data Science und Statistik
Mit Ausreißer
Ohne Ausreißer
Der Mittelwert des standardisierten Vektors ist rundungsbedingt nicht ganz exakt
0. Über die wissenschaftliche Notation haben wir in (6.4) gesprochen.
7.7 Abschluss
• Mit welchem Parameter schließen wir fehlende Werte in sehr vielen Funktionen
aus? (7.1)
• Wie bestimmen wir das Minimum und Maximum eines Vektors? Wofür steht
NA und wie ersetzen wir Elemente eines Vektors durch NA? (7.2)
• Wie berechnen wir Mittelwert, Varianz und Standardabweichung? (7.3)
• Wie bestimmen wir den Median? Wie berechnen wir beliebige Quantile und
ganz im speziellen Quartile? (7.4)
• Was macht die Funktion summary()? (7.5)
• Mit welchen Maßzahlen können wir Ausreißer besonders gut aufspüren? Welche
Maßzahlen sind robust gegenüber Ausreißern? (7.6.1)
• Wie standardisieren wir einen numerischen Vektor? (7.6.2)
7 Deskriptive Statistik 97
7.7.2 Ausblick
Vielleicht ist dir aufgefallen, dass auf Seite 93 der fehlende Wert bei sort(apfel)
eliminiert wurde. Den Grund dafür erfährst du in (14), selbstverständlich darfst du
aber auch in Übung 1 auf dieser Seite nach dem Grund forschen ;-). In diesem Kapitel
lernen wir außerdem, wie wir beim Subsetting fehlende Werte ausschließen.
In (7.6.2) haben wir gesehen, dass bei der Standardisierung ein Rundungsfehler
aufgetreten ist. Wie wir damit adäquat umgehen, besprechen wir in (13).
Und zum Schluss gibt es eine kleine Belohnung für dich! Grafiken besprechen wir
erst relativ weit hinten im Buch, falls du aber zu den Ungeduldigen zählst, dann gib
einmal folgende Codezeilen ein:
hist(apfel.voll)
boxplot(apfel.voll)
7.7.3 Übungen
1. Sortiere den Vektor apfel dieses Kapitels aufsteigend. Fehlende Werte (NAs)
sollen dabei
a) hinten angehängt werden.
b) vorne angeführt werden.
Hinweis: Die R-Hilfe zeigt dir, wie du das einstellen kannst ;-)
2. Die Formel für die Stichprobenvarianz eines numerischen Vektors x ist ge-
geben durch:
n
1 X 2
s2 = · (xi − x̄)
n − 1 i=1
Berechne für den Vektor apfel dieses Kapitels die Varianz, ohne dabei var()
und sd() zu verwenden. Die Funktion is.na() hilft dir dabei, gültige Werte
eines Vektors zu selektieren.
3. Ein Kollege möchte das untere Quartil, den Median und das obere Quartil
eines numerischen Vektors x bestimmen. Folgenden Code hat er geschrieben:
quantile(x, 0.25)
quantile(x, 0.5)
quantile(x, 0.75)
Ein anderer Kollege schlägt folgende Variante vor:
quantile(x, c(0.25, 0.5, 0.75))
Welche Variante liefert wohl schneller (die Laufzeit betreffend) die Lösung?
Welchen Grund könnte das haben?
98 B Vektorfunktionen für Data Science und Statistik
4. Finde in der R-Hilfe zur Funktion mean() heraus, was der Parameter trim
bedeutet und wie bzw. wofür wir ihn einsetzen können.
5. Gegeben ist ein numerischer Vektor x. Folgendem R-Code begegnest du:
y <- sort(x)
y <- mean(y[((length(y) + 1:2) %/% 2)])
y
Was macht obiger Code? Finde einen kürzeren Code, der dasselbe berechnet.
Hinweis: Setze für x zum Beispiel die Vektoren 1:3 und 1:4 ein.
6. Von 10 Studierenden wurden die Körpergröße in cm und das Gewicht in kg
gemessen:
> groesse <- c(176, 181, 181, 183, 163, 157, 164, 166, 176, 184)
> gewicht <- c(65, 92, 65, 93, 49, 47, 55, 50, 62, 84)
kg
Der BMI berechnet sich gemäß m2 (Gewicht in kg durch Größe in m zum
Quadrat).
Leider haben einige ihre Größe nicht in cm, sondern in m angegeben. Andere
haben offensichtlich unplausible Werte angegeben.
Dein Code soll auch für andere ähnlich geartete Vektoren funktionieren!
8 Kumulieren und Parallelisieren 99
Wir begleiten in diesem Kapitel zwei Minigolfspieler auf ihrer Reise durch die Hin-
dernisse. In Infobox 4 finden wir eine Kurzbeschreibung von Minigolf.
Infobox 4: Minigolf
Beim Minigolf geht es darum, einen Ball mit Hilfe eines Schlägers vom Start-
punkt in das Zielloch zu befördern. Je weniger Schläge dafür gebraucht werden,
desto besser. Die zumeist 18 Bahnen sind mit Hindernissen gespickt, die es zu
überwinden gilt.
Nach 6 Bahnen machen sie eine Pause. Zeit für eine Zwischenabrechnung.
Die beiden Spieler (wie auch wir) fragen sich unter anderem:
• Wie groß ist bei jeder Bahn sein Vorsprung bzw. Rückstand? (8.1.1)
• Wie viele Schläge hat auf jeder Bahn der jeweils bessere Spieler benötigt?
(8.2.1)
• Welche Bahnen sind einfacher, welche schwieriger? (8.2.1)
• Wie oft haben die Spieler auf einer Bahn höchstens so viele Schläge gebraucht,
wie auf der Bahn davor? (8.3)
• Gibt es einen Zusammenhang zwischen den Schlagzahlen beider Spieler? (8.4)
Was könnte Spieler 1 wohl interessieren? Sicherlich, ob er in Führung liegt und wie
groß sein Vorsprung oder Rückstand ist. Die Frage nach der Gesamtführung ist mit
den bisher besprochenen Mitteln leicht zu beantworten.
Leider keine guten Nachrichten für Spieler 1. Sein Rückstand beträgt 2 Schläge. Er
fragt sich jetzt, wann er den Rückstand aufgerissen hat.
Von Hand würden wir wohl damit beginnen, die kumulierten Summen der Schlag-
zahlen beider Spieler zu bestimmen, also zu ermitteln, wie viele Schläge jeder Spieler
nach jeder Bahn bis dahin insgesamt gebraucht hat. In Abb. 8.1 sind die kumulierten
Schlagzahlen für beide Spieler abgebildet.
2 3 2 4 7 3 1 3 3 6 4 2
+ + + + + + + + + +
2 5 7 11 18 21 1 4 7 13 17 19
Abbildung 8.1: Kumulierte Summe der Schlagzahlen für Spieler 1 (links) und jene
für Spieler 2 (rechts).
Aus der Differenz der beiden kumulierten Summen können wir dann die Antwort
leicht ableiten. So hatte Spieler 1 nach der vierten Bahn noch einen Vorsprung von
13 − 11 = 2 Schlägen.
Im Prinzip können wir auch diese Aufgabe mit den bisher gelernten Techniken lösen.
Das hat aber mehrere Haken, wie wir gleich sehen werden!
8 Kumulieren und Parallelisieren 101
> # Bilde die kumulierte Summe für die Schlagzahlen von Spieler 1
> # Mühsame und fehleranfällige Variante
> c(sum(schlaege1[1:1]), sum(schlaege1[1:2]), sum(schlaege1[1:3]),
+ sum(schlaege1[1:4]), sum(schlaege1[1:2]), sum(schlaege1[1:6]))
[1] 2 5 7 11 5 21
Wir merken schnell: Das kann nicht die beste Variante sein! Dieser Ansatz ist aus
mehreren Gründen suboptimal:
• Der Tippaufwand ist sehr groß, denken wir etwa an Vektoren der Länge 100.
• Der Ansatz ist sehr fehleranfällig. So wurde oben die erste Zeile kopiert und in
der zweiten Zeile vergessen, 1:2 auf 1:5 zu korrigieren. Copy & Paste-Fehler
passieren häufig und sind tückisch, da wir den schwerwiegenden Fehler oft nicht
bemerken. Selbst wenn wir ihn bemerken, müssen wir ihn dann noch finden!
• Wir missachten die Regel 4 des allgemeinen Programmierens auf Seite 31. Der
Code funktioniert nur für 6 Bahnen korrekt und müsste mühsam umgeschrie-
ben werden, wenn sich die Anzahl der Bahnen ändert.
Mit Hilfe der Funktion cumsum() bilden wir die kumulierten Summen und bügeln
gleichzeitig die oben genannten Nachteile aus!
Besonders kritische Personen werden jetzt bemerken, dass auch dieser Ansatz Ge-
fahren beinhaltet. Was passiert, wenn 20 Mitspieler mitspielen? Schreiben wir dann
20 Codezeilen à la cumsum(schlaege1) bis cumsum(schlaege20)? Die Antwort lau-
tet natürlich Nein! Wir werden in (15) die Datenstruktur Matrix kennenlernen, die
uns in dieser Hinsicht deutlich entgegenkommt und welche die kritischen Stimmen
verstummen lassen wird.
Beispiel: Nach welchen Bahnen war Spieler 1 in Führung? Wie viele Runden lang
war Spieler 1 in Führung?
Spieler 1 führt nach einer Bahn, wenn er bis dahin weniger oder gleich viele Schläge
wie Spieler 2 benötigt hat.
Die Antworten können wir bequem ablesen: Spieler 1 hat nach den Bahnen 3 und 4
geführt. Das sind 2 Bahnen.
Beachte: Der Ausdruck cumsum(schlaege1) - cumsum(schlaege2) <= 0 wird im
obigen Code zwei Mal ausgeführt. R muss also denselben Ausdruck zwei Mal berech-
nen, das kostet bei großen Datensätzen und Simulationen viel Zeit! Eine Inspiration
für (8.5.3), wo wir aus dieser Beobachtung eine wichtige Regel ableiten.
Mit der Funktion cumprod() bilden wir kumulierte Produkte. Die Idee ist dieselbe
wie bei kumulierten Summen, nur werden die Zahlen miteinander multipliziert, statt
addiert. Wir betrachten ein Beispiel abseits des Minigolfbeispiels.
Erinnern wir uns an (6.3), wo wir Fakultäten berechnet haben. Mit der Funktion
cumprod() haben wir eine mächtige Alternative für die vektorwertige Berechnung
von Fakultäten.
t
Beispiel: Wir wollen einen Vektor (1!, 2!, . . . n!) für ein n ∈ N kreieren.
> n <- 5
> # Vektor mit 1!, 2!, ..., n! > # Alternative (weniger effizient)
> cumprod(1:n) > factorial(1:n)
[1] 1 2 6 24 120 [1] 1 2 6 24 120
In (8.5.2) schauen wir uns mit dem Geburtstagsproblem eine echt coole Anwen-
dungsmöglichkeit für cumprod() an!
Für kumulierte Minima und Maxima gibt es die Funktionen cummin() und cummax().
Beispiel: Wir wollen für beide Spieler herausfinden, was nach jeder Bahn ihre bis
dahin schlechteste Einzelleistung war.
Spieler 1 hat nach 5 Bahnen 7 Schläge als bis dato schlechteste Einzelleistung zu
Buche stehen und Spieler 2 zum selben Zeitpunkt 6 Schläge.
Wir betrachten in (8.5.1) eine weitere Lotterie, in der bei der Gewinnrangermittlung
cummin() eine große Rolle spielt.
8 Kumulieren und Parallelisieren 103
Parallele Funktionen ermöglichen es uns, eine Funktion parallel auf Elemente meh-
rerer Vektoren auszuführen. Was damit gemeint ist, schauen wir uns gleich an.
Wir möchten gerne für jede Bahn bestimmen, wie viele Schläge der bessere Spie-
ler gebraucht hat. Mit der Funktion min() kommen wir nicht weiter; sie bestimmt
lediglich das Minimum über alle Elemente der übergebenen Vektoren.
Hier genügt ein Buchstabe, um das Pflänzchen zum Erblühen zu bringen. Wir hängen
den Buchstaben „p“ an und kommen so zur Funktion pmin() (parallel minimum).
> schlaege1
[1] 2 3 2 4 7 3
> schlaege2
[1] 1 3 3 6 4 2
pmin() retourniert also einen Vektor, der an jeder Stelle das jeweils kleinste Element
der Vektoren schlaege1 und schlaege2 enthält.
Wir können pmin(), dem Dreipunkteargument sei Dank, auch mehr als zwei Vek-
toren übergeben, wenn wir etwa einen dritten oder vierten Mitspieler hätten. Mit
na.rm steuern wir, ob fehlende Werte ausgeschlossen werden sollen (vgl. (7.1)).
Analog dazu gibt es die Funktion pmax() für das parallele Maximum zweier oder
mehrerer Vektoren. Sind die übergebenen Vektoren unterschiedlich lang, greift wie-
derum das Recycling (vgl. (3.3)).
Beispiel: Sortiere die Bahnnummern aufsteigend nach ihrer Schwierigkeit. Das 1.
Sortierkriterium ist die Anzahl der Schläge, die beide Spieler zusammen gebraucht
haben, das 2. Kriterium die Anzahl der Schläge des auf dieser Bahn schlechteren
Spielers.
Bahnnummer 1 3 6 2 4 5
schlaege1 2 2 3 3 4 7
schlaege2 1 3 2 3 6 4
Schlagsumme 3 5 5 6 10 11
Schlechtere Schlagzahl 2 3 3 3 6 7
Bei der 3. und 6. Bahn tritt ein Tie auf, es ist also keine eindeutige Reihung möglich.
Gerne darfst du dir ein 3. Kriterium überlegen, um die Reihenfolge eindeutig zu
machen. Programmieren hat auch sehr viele kreative Seiten!
Stellen wir uns vor, wir wollen für zwei Vektoren x1 und x2 das parallele Minimum
berechnen (vgl. (8.2.1)). Dann könnten wir auch so vorgehen:
• test: Ist ein Element von x1 kleiner als das entsprechende Element von x2 ?
• yes: Falls ja, so entnehme das Element von x1 .
• no: Falls nicht, so entnehme das Element von x2 .
Mit der Funktion ifelse() können wir diese Idee vektorwertig umsetzen und derlei
binäre Fallunterscheidungen durchführen.
Für test übergeben wir einen logischen Vektor mit Wahrheitswerten. Die beiden
Vektoren yes bzw. no erhalten einen Vektor mit den Rückgabewerten für den Fall,
dass die Elemente von test TRUE bzw. FALSE sind. Die Funktion arbeitet vektorwer-
tig, das heißt wir können für test einen mehrelementigen Vektor und nicht bloß ein
TRUE oder FALSE übergeben. Gegebenenfalls werden die Vektoren yes und no einem
Recycling zugeführt.
Bevor wir uns ein umfangreiches Beispiel ansehen, wollen wir die Funktion ifelse()
anhand des parallelen Minimums nachvollziehen. Beachte jedoch, dass die Funktion
pmin() parallele Minima effizienter berechnet!
> schlaege1
[1] 2 3 2 4 7 3
> schlaege2
[1] 1 3 3 6 4 2
Überall dort, wo Spieler 1 weniger Schläge als Spieler 2 benötigt hat, selektiert
ifelse() die entsprechende Schlagzahl von Spieler 1 (yes). Ansonsten wird die
entsprechende Schlagzahl von Spieler 2 selektiert (no).
8 Kumulieren und Parallelisieren 105
Beispiel: Generiere einen Vektor mit den Einträgen 0, 1 und 2 wie folgt: Haben
beide Spieler gleich viele Schläge benötigt, soll eine 0 erzeugt werden. War Spieler 1
besser, soll eine 1 geschrieben werden, war Spieler 2 besser, eine 2.
Zuerst wird der innerste Ausdruck schlaege1 < schlaege2 ausgewertet. Dann wer-
den yes = 1 und no = 2 recycelt und auf dieselbe Länge wie test gebracht. Nun
selektiert ifelse() die passenden Werte aus beiden Vektoren. Das Ergebnis des
inneren ifelse()-Befehls wird für no im äußeren Funktionsaufruf eingesetzt. Dort
wird auch yes = 0 recycelt (siehe Abb. 8.2).
schlaege1 == schlaege2
TRUE FALSE
TRUE FALSE
1 2
Wie oft haben die beiden Spieler auf einer Bahn höchstens so viele Schläge benötigt,
wie auf der Bahn davor? Per Hand hätten wir unter anderem zwei Möglichkeiten:
1. Wir vergleichen jeweils zwei benachbarte Zahlen miteinander und zählen, wie
oft eine Zahl kleiner oder gleich ihrer vorhergehenden Zahl ist.
2. Wir bilden die paarweisen Differenzen zweier benachbarter Zahlen und zählen
ab, wie oft das Ergebnis ≤ 0 ist. Wir differenzieren also den Vektor.
2 3 2 4 7 3 2 3 2 4 7 3
- - - - -
3>2 2≤3 4>2 7>4 3≤7 1 -1 2 3 -4
Mit der Funktion diff() können wir einen Vektor differenzieren und somit
elegant die 2. Variante umsetzen.
> schlaege1
[1] 2 3 2 4 7 3
Das erste Element von schlaege1 können wir dabei nicht rekonstruieren.
Bemerkung: Die Funktion diff() hat unter anderem noch den Parameter lag.
Die R-Hilfe zeigt dir, was du mit diesem Parameter einstellen kannst.
Unsere Intuition hat uns nicht im Stich gelassen. Je mehr Schläge Spieler 1 benötigt
hat, desto mehr Schläge hat tendenziell auch Spieler 2 auf einer Bahn benötigt.
5 Die Funktionen stellen uns auch rangbasierte Methoden zur Verfügung – siehe R-Hilfe.
8 Kumulieren und Parallelisieren 107
Infobox 5: Jokerziehung
4 3 9 1 0 3 Ziehungsergebnis
4 3 9 1 0 4 Gewinnrang 0, da die 4 von 3 abweicht.
4 3 9 0 0 3 Gewinnrang 2, da die 0 von 1 abweicht.
2 1 7 1 0 3 Gewinnrang 3, da die 7 von 9 abweicht.
Bestimme den Gewinnrang für einen gegebenen Jokertipp und eine Jokerziehung.
Die letzten zwei Zahlen stimmen überein, bei der drittletzten gibt es leider eine
Diskrepanz. Der Gewinnrang ist also 2. Hauchdünn am Reichtum vorbei...
Wir hangeln uns nun schrittweise zur Lösung empor.
P (An ) = 1 − P (AC
n)
= 1 − P (alle n Leute haben an unterschiedlichen Tagen Geburtstag)
365 364 363 365 − (n − 1)
=1− · · ···
365 365 365 365
n−1
Y 365 − k n−1
Y
=1− =1− pk
365
k=0 k=0
Dabei haben wir die Unabhängigkeitsannahme verwendet. Schaltjahre und die in der
Realität ungleichmäßige Verteilung der Geburtstage haben wir hier vernachlässigt.
Wir könnten jetzt einen Taschenrechner oder ein Smartphone bemühen, und fleißig
tippen: 365/365·364/365·363/365·362/365... bis erstmals der Wert 0.5 unterschritten
wird und dann die Gegenwahrscheinlichkeit berechnen.
Viel schneller und bequemer geht es mit R:
> # Ersten Index finden, bei dem die Wahrscheinlichkeit 50% ueberschreitet.
> index <- which(res > 0.5)[1]
> index
[1] 23
Im Beispiel auf Seite 101 haben wir herausgefunden, nach welchen und nach wievie-
len Bahnen Spieler 1 in Führung lag. Wir präsentieren jetzt eine laufzeittechnisch
schnellere Abwandlung, in welcher wir die relativ zeitaufwändige Berechnung von
cumsum(schlaege1) - cumsum(schlaege2) <= 0 nur noch einmal ausführen und
das Ergebnis in der Hilfsvariable fuehrung1 abspeichern.
> # Hilfsvariable
> fuehrung1 <- cumsum(schlaege1) - cumsum(schlaege2) <= 0
> fuehrung1
[1] FALSE FALSE TRUE TRUE FALSE FALSE
Führe Berechnungen, deren Ergebnisse du häufig benötigst, ein Mal aus und
speichere das Resultat in einer Variablen ab.
8.6 Abschluss
• Was sind kumulierte Summen, Produkte, Minima und Maxima? Wie können
wir sie in R berechnen? (8.1)
• Wie bestimmen wir parallele Minima und Maxima? Was passiert beim Funk-
tionsaufruf, wenn die übergebenen Vektoren ungleich lang sind? (8.2.1)
• Was sind vektorwertige binäre Fallunterscheidungen und wie nehmen wir diese
in R vor? (8.2.2)
110 B Vektorfunktionen für Data Science und Statistik
• Wie differenzieren wir einen Vektor? Welches Konzept bzw. welche Funktion
bildet das Gegenstück zum Differenzieren? (8.3)
• Wie bestimmen wir die Kovarianz und den Korrelationskoeffizienten? (8.4)
• Was sollten wir tun, wenn wir auf das Ergebnis einer aufwändigen Berechnung
mehrmals zugreifen? (8.5.3)
In (8.5.2) und (8.5.1) haben wir zwei coole Anwendungen für kumulierende Funk-
tionen kennengelernt. Wenn der Funke für die Funktionen dieses Kapitels auf dich
übergesprungen ist, dann haben die Autoren eines ihrer Ziele erreicht!
8.6.3 Ausblick
Mit den Funktionen dieses Kapitels kannst du sehr viele Problemstellungen (unter
anderem jene aus (8.5.2) und (8.5.1)) effizient lösen. In (33) betrachten wir viele
weitere Beispiele zum Thema effizientes Programmieren. Wenn du bereits andere
Programmiersprachen gelernt hast oder schon erste Erfahrungen mit R gesammelt
hast, bist du vielleicht schon auf Schleifen getrimmt.
Schleifen besprechen wir erst relativ spät in (28). Das hat einen Grund: Schleifen sind
in R oft langsam. Viele Schleifen lassen sich just durch jene schnelleren Funktionen
ersetzen, die wir in diesem Kapitel gelernt haben.
8.6.4 Übungen
1. Schreibe einen R-Code, der die kumulierten Mittelwerte für einen numerischen
t
Vektor x = (x1 , x2 , x3 , . . . , xn ) berechnet. Gesucht ist also folgender Vektor:
n
!t
x1 x1 + x2 x1 + x2 + x3 1 X
, , ,..., · xi
1 2 3 n i=1
2. Sei x ein numerischer Vektor. Was tun folgende R-Codes? Finde für jeden Code
eine kürzere Schreibweise.
Platz 1 2 3 4 5 6 7 8 9 10
Punkte 100 80 60 50 45 40 36 32 29 26
Platz 11 12 13 14 15 16 17 18 19 20
Punkte 24 22 20 18 16 15 14 13 12 11
Platz 21 22 23 24 25 26 27 28 29 30
Punkte 10 9 8 7 6 5 4 3 2 1
4. Eine Supermarktkette möchte ihren treuen Kunden eine Prämie in Form eines
Gutscheines schenken. Wir betrachten den Umsatz von 4 Personen (in Euro):
300, 150, 700, 400
Die Geschäftsführung schlägt vier Modelle vor:
Berechne für jedes Modell die Prämie für alle Kunden. Schreibe deinen Code
derart, dass er auch für eine größere Anzahl an Kunden funktioniert.
112 B Vektorfunktionen für Data Science und Statistik
5. Wir betrachten das Minigolfbeispiel dieses Kapitels. Wir wollen einen Vektor
erstellen, der für jede Bahn eines der folgenden Elemente enthält:
6. Wir wollen für einen Vektor x die Indizes jener Elemente bestimmen, die größer
sind als das Element zuvor. Folgender Code ist fehlerbehaftet, korrigiere ihn:
7. (Fortsetzung von Übung 7 auf Seite 88) Sei X ∼ Binom(n, p) eine binomial-
verteilte Zufallsvariable mit den Parametern n ∈ N und p ∈ (0, 1). Die Wahr-
scheinlichkeitsfunktion lautet:
n
P (X = k) = · pk · (1 − p)n−k
k
Kontrolliere dein Ergebnis auf Richtigkeit! Wenn für die gegebene Bei-
spielkonstellation 8 herauskommt, bist du auf einem sehr guten Weg ;-)
Dein Code soll für beliebige n ∈ N, p ∈ (0, 1) und α ∈ (0, 1) funktionieren.
9 Verteilungen und Zufallszahlen 113
Stell dir vor, du möchtest die Wahrscheinlichkeiten bestimmter Ereignisse eines Zu-
fallsexperimentes bestimmen. Dann hast du unter anderem zwei Möglichkeiten:
• Du berechnest die Wahrscheinlichkeiten und findest eine auswertbare Formel.
• Du simulierst die Wahrscheinlichkeiten.
Für beide Möglichkeiten lernen wir in diesem Kapitel grundlegende Techniken. Wir
klären unter anderem folgende Fragen:
In (9.3) schauen wir uns eine Übersicht über viele gängige Verteilungen an und in
(9.4) erfahren wir, wie wir gleichverteilte Zufallszahlen generieren.
Erinnerst du dich noch an deine Schätzung für das mittlere Gewicht von Äpfeln aus
(7)? In (9.7.1) testen wir das Hypothesenpaar:
H0 : Das mittlere Gewicht von Äpfeln unterscheidet sich nicht von c g.
H1 : Das mittlere Gewicht von Äpfeln unterscheidet sich von c g.
Es gibt also viel Spannendes zu entdecken!
9.1 Namenskonventionen – d, p, q, r
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_9
114 B Vektorfunktionen für Data Science und Statistik
Die Inverse der Verteilungsfunktion F −1 (p) ist die Quantilsfunktion. Sie be-
stimmt für p ∈ [0, 1] den Wert x so, dass P (X ≤ x) = p gilt. x ist das
p-Quantil.
Die Funktionsnamen setzen sich aus den folgenden beiden Teilen zusammen:
• einem Buchstabenkürzel, mit dem wir bestimmen, ob wir die Dichtefunkti-
on, Verteilungsfunktion oder Quantilsfunktion auswerten wollen oder Zufalls-
zahlen ziehen wollen.
• dem Namen der Verteilung
Kürzel Bedeutung
d Dichtefunktion (density function)
p Verteilungsfunktion (probability distribution function)
q Quantilsfunktion (quantile function)
r Zufallszahlen ziehen (random number)
Schauen wir uns das Ganze gleich für die Normalverteilung an!
Die Normalverteilung trägt in R den Namen norm. Gemeinsam mit den in Tab. 9.1
angeführten Kürzeln stehen uns die folgenden vier Funktionen zur Verfügung:
Jede dieser Funktionen verfügt über die beiden Parameter mean (Erwartungswert)
und sd (Standardabweichung, nicht Varianz!). Betrachten wir ein paar Beispiele.
9 Verteilungen und Zufallszahlen 115
In Tab. 9.2 listen wir einige wichtige Verteilungen auf. Bei diskreten Verteilungen
bezeichnet dverteilung(x) die Wahrscheinlichkeitsfunktion, also P (X = x). Wir ver-
weisen dabei auf die R-Hilfe; ?dt ruft etwa die Hilfe zur t-Verteilung auf.
Zufallszahlen, die mit einem Computer generiert werden, sind keine Zufallszahlen im
eigentlichen Sinn. Vielmehr wird ausgehend von einem Startwert x0 (engl. Seed) die
Folge der Zufallszahlen nach einer vorgegebenen Rechenvorschrift (einem Algorith-
mus) erzeugt, der in aller Regel deterministisch ist. Kennen wir den Seed und den
zugrunde liegenden Algorithmus des Zufallszahlengenerators, so können wir die gan-
ze „Zufallszahlenfolge“ rekonstruieren. Wir sprechen daher von Pseudozufallszahlen.
Den Seed können wir mit der Funktion set.seed() einstellen, der wir eine Zahl
zwischen −(231 − 1) und 231 − 1 übergeben können.
Die Zahlen sind unterschiedlich und solange wir die Zahlen nicht irgendwo als Datei
abspeichern, haben wir keine Chance, sie in einer neuen R-Sitzung zu rekonstruieren.
Genau hier setzt set.seed() an. Probieren wir es aus!
Damit stellst du sicher, dass deine simulierten Ergebnisse immer dieselben sind
und machst sie damit reproduzierbar und nachvollziehbar. Damit dein Code
auch in neueren R-Versionen dieselben Zahlen liefert, setze vor set.seed() den
Befehl RNGversion(vstr) ein, wobei vstr die gewünschte R-Version angibt.
Die Funktion sample() ist eine extrem flexible und dennoch einfach zu handhabende
Funktion, um Stichproben zu ziehen und Permutationen zu erzeugen. Werfen
wir einen Blick auf die Parametrisierung:
Hierbei ist x ein Vektor, der die Elemente enthält, aus denen die Stichprobe gezo-
gen werden soll. Der Parameter size steuert die Größe der Stichprobe; wird nichts
übergeben, so werden ebenso viele Elemente gezogen, wie in x enthalten sind.
Mit replace können wir steuern, ob mit (TRUE) oder ohne (FALSE) Zurücklegen
gezogen werden soll. Standardgemäß wird jedes Element von x mit gleicher Wahr-
scheinlichkeit gezogen – dies können wir mit prob ändern.
Betrachten wir ein paar Codebeispiele.
Im letzten Beispiel sehen wir, dass wir für x auch Zeichenketten übergeben kön-
nen. Auf Zeichenketten gehen wir in (10) ein. Übrigens: Für prob können wir auch
Wahrscheinlichkeitsgewichte übergeben. Statt prob = c(2/3, 1/3) hätten wir
also zum Beispiel auch prob = c(2, 1) schreiben können.
9 Verteilungen und Zufallszahlen 119
In (7) haben wir bereits das mittlere Gewicht eines Apfels einer Stichprobe be-
rechnet. Jetzt wollen wir testen, ob das mittlere Gewicht eines Apfels der Grund-
gesamtheit (= alle Äpfel) µ einem Testwert c gleicht. Wir testen also:
H0 : Das mittlere Gewicht von Äpfeln unterscheidet sich nicht von c g. (µ = c)
H1 : Das mittlere Gewicht von Äpfeln unterscheidet sich von c g. (µ 6= c)
Die Teststatistik berechnet aus der Stichprobe eine Zahl, mit der wir eine
Entscheidung entweder für H0 oder H1 ableiten können.
Wenn wir H0 vs. H1 testen, so begehen wir einen Fehler 1. Art, wenn wir H0
verwerfen, obwohl H0 in Wahrheit zutrifft.
Der p-Wert gibt für die gegebene Stichprobe die Wahrscheinlichkeit für den
Fehler 1. Art an. Dazu berechnen wir die Wahrscheinlichkeit, dass die Teststa-
tistik unter H0 einen mindestens so extremen Wert annimmt.
Das Risiko, einen Fehler 1. Art zu begehen, wollen wir mit dem Signifikanzni-
veau α nach oben hin beschränken. Eine häufige Wahl für α ist 5% = 0.05.
Unter der Annahme, dass das Gewicht von Äpfeln normalverteilt ist oder die Stich-
probe hinreichend groß ist, eignet sich der t-Test für eine Stichprobe.
Die Formel für die Teststatistik lautet in diesem Fall:
x̄ − c √
T = · n (9.1)
s
Das (1 − α)-Konfidenzintervall gibt uns einen Bereich an, in dem sich das mittle-
re Gewicht eines Apfels der Grundgesamtheit mit einer Wahrscheinlichkeit von
1 − α befindet. Korrekt formuliert: Ziehen wir unendlich viele gleich große Stichpro-
ben aus derselben Grundgesamtheit, so umfasst der Anteil 1 − α aller berechneten
Konfidenzintervalle das mittlere Gewicht eines Apfels der Grundgesamtheit.
Die Formel für das (1 − α)-Konfidenzintervall lautet in diesem Fall:
(t)
α s
x̄ ± Qdf =n−1 1 − ·√ (9.2)
2 n
Wobei x̄, s und n der Mittelwert, die Standardabweichung und Größe der Stichprobe
(t)
sind. Qdf =n−1 1 − α2 ist das Quantil der t-Verteilung mit n − 1 Freiheitsgraden (df
3. p-Wert < α.
Beispiel: Teste mit der Apfelstichprobe apfel.voll der Datei Apfel.RData auf
dem Signifikanzniveau von α = 0.05 folgendes Hypothesenpaar:
H0 : Das mittlere Gewicht von Äpfeln unterscheidet sich nicht von 160 g.
H1 : Das mittlere Gewicht von Äpfeln unterscheidet sich von 160 g.
Sehr gerne darfst du statt 160 deine Schätzung aus (7) nehmen! Leite die Testent-
scheidung mit den drei oben besprochenen Entscheidungsregeln ab.
Bemerkung: Die Stichprobe enthält keine Messfehler. In der Praxis müssten wir
an dieser Stelle zunächst die Plausibilität der Werte überprüfen (vgl. (7)).
Nein, c liegt im Intervall, daher können wir H0 nicht verwerfen. Das ± in Formel (9.2)
können wir mit c(-1, 1) wunderbar umsetzen!
3. Entscheidungsregel: Laut Infobox 7 müssen wir die Wahrscheinlichkeit berech-
nen, dass die Teststatistik unter H0 einen mindestens so extremen Wert annimmt.
Da H1 zweiseitig ist (µ < c oder µ > c), berechnet sich der p-Wert wie folgt:
Nein, der p-Wert ist nicht kleiner als α = 0.05, daher können wir H0 nicht verwerfen.
Rechnen wir zur Kontrolle den Test mit der Funktion t.test() nach.
Schaut gut aus! Wir überzeugen uns leicht davon, dass wir richtig gerechnet haben,
indem wir den Output mit unseren Ergebnissen vergleichen.
Die Funktion t.test() kann mehr, als wir an dieser Stelle gezeigt haben! Wir gehen
auf diese Funktion in (37.3.2) genauer ein, gerne darfst du bei Interesse auch schon
jetzt einen Blick in die R-Hilfe zu dieser Funktion werfen ;-)
9.8 Abschluss
• Wie teilen wir R mit, ob wir die Dichte-, Verteilungs- oder Quantilsfunktion
auswerten oder Zufallszahlen ziehen wollen? (9.1)
• Wie ist die Normalverteilung R parametrisiert? Wie werten wir die Dichte-,
Verteilungs- und Quantilsfunktion der Normalverteilung aus und wie ziehen
wir normalverteilte Zufallszahlen? (9.2)
• Wie erzeugen wir gleichverteilte Zufallszahlen aus einem beliebigen Intervall?
Aus welchem Intervall werden die Zahlen standardmäßig gezogen? (9.4)
• Was ist bzw. kann Seeding und warum ist Seeding von Bedeutung? Wie stel-
len wir den Seed in R ein? Wie sorgen wir dafür, dass auch in künftigen R-
Versionen dieselben Zufallszahlen herauskommen? (9.5)
• Welche Parameter hat die Funktion sample() und was bewirken sie? Welche
Standardeinstellungen haben die Parameter? (9.6)
In (9.3) hast du eine Übersicht über viele gängige Verteilungen bekommen und
in (9.7.1) haben wir unseren ersten t-Test durchgeführt. Insbesondere haben wir
dort den korrekten Umgang mit den Buchstabenkürzeln geübt und uns mit der t-
Verteilung eine weitere Verteilung angesehen.
9.8.2 Ausblick
Simulation wird uns in weiterer Folge immer wieder begegnen, sei es in Form von
kleinen Übungsbeispielen oder in Form von größeren Aufgaben, wie wir sie zum
Beispiel in (33) betrachten werden.
In (37) und (38) schauen wir uns Hypothesentests und lineare Modelle genauer
an. Derzeit fehlen uns noch die Mittel, um automatisiert auf Teile des Outputs
zuzugreifen. Unter anderem brauchen wir dazu Listen, die wir in (17) einführen.
9 Verteilungen und Zufallszahlen 123
9.8.3 Übungen
In Übung 4 auf Seite 59 haben wir bereits zahlreiche coole Beispiele für sample()
betrachtet.
a) mit der passenden Funktion aus Tab. 9.2 auf Seite 116.
b) ohne die Funktion aus 1a).
3. Seeding ist für die Reproduzierbarkeit sehr wichtig. Ein Kollege schlägt folgen-
de Idee für die Bestimmung des Seeds vor:
Warum sind Ergebnisse, die auf dieser Idee beruhen, nicht reproduzierbar?
5. Überlege dir, wie wir mit Hilfe der Funktion runif() Würfelwürfe eines nor-
malen 6-seitigen Würfels erzeugen können. Finde nach Möglichkeit mehr als
eine Möglichkeit. Beachte dabei, dass runif() die Zufallszahlen aus dem halb-
offenen Intervall [min, max) zieht.
Hinweis: Lass dich von den bisher gelernten Funktionen und Operatoren
inspirieren!
124 B Vektorfunktionen für Data Science und Statistik
xi − min(x)
x̃i =
max(x) − min(x)
a) Schreibe einen Code, der für einen beliebigen numerischen Vektor x den
transformierten Vektor x̃ berechnet.
b) Seien a, b ∈ R und sei a < b. Eine Verallgemeinerung der Min-Max-
Transformation ist gegeben durch:
0
für xi < a
x̃i = x−a
b−a für a ≤ xi ≤ b
1 für xi > b
Mit a = min(x) und b = max(x) erhalten wir den Spezialfall der Min-
Max-Transformation.
Schreibe einen Code, der für einen beliebigen numerischen Vektor x den
transformierten Vektor berechnet.
Daniel Obszelka
126 C Wichtige Hilfsmittel
In (2.2.1) haben wir ein Programm zur Lösung der quadratischen Gleichung
a · x2 + b · x + c = 0
geschrieben. Für die Parameterkonstellation a = 2, b = −5 und c = 3 haben wir die
Lösungen x1 = 1 und x2 = 1.5 errechnet. Wie wäre es, wenn wir die Lösungen in
Form eines schönen Textes ausgeben? Zum Beispiel in folgender Form:
Und wenn wir schon dabei sind, möchten wir vielleicht auch die dazu gehörige Pa-
rameterkonstellation darstellen:
Wir lernen eine coole Funktion kennen, mit der wir Texte und Zahlen flexibel zu
neuen Texten zusammenbauen können und klären unter anderem folgende Fragen:
• Wie erstellen wir einen Text mit den Lösungen in obiger Bauart? (10.3)
• Müssen wir bei der Darstellung der Parameterkonstellation jeden Buchstaben
einzeln eintippen? Oder gibt es eine fingerschonendere Möglichkeit? (10.4)
Davor klären wir, woran R Zeichenketten überhaupt erkennt (10.1) und wie wir
Texte alphabetisch sortieren können (10.2).
Texte (auch Zeichenketten bzw. Strings genannt) sind in R vom Typ character. R
erkennt bei der Eingabe Strings an den Anführungszeichen.
Werden die Anführungszeichen nicht übergeben, so wird (fast immer) eine Fehler-
meldung ausgegeben. In folgendem Fall werden keine Anführungszeichen verwendet,
daher sucht R (hier vergebens) nach dem Objekt Walter.
Zahlen werden mit sort() nach ihrer Größe sortiert, wie wir schon in (6.2) bespro-
chen haben. Die Funktion sort() kann aber auch mit Texten umgehen: Sie werden
alphabetisch sortiert. Die Anwendung der Funktionen funktioniert dabei so, wie
wir es schon kennen.
Dasselbe Prinzip gilt auch für die Funktionen order(), rank() und einige weitere
Funktionen. Betrachten wir ein paar einfache Codebeispiele.
> namen
[1] "Walter" "Alex" "Julia"
Das klingt unspektakulär. Prickelnd wird das Ganze aber, wenn wir Zahlen nach der
Größe sortieren wollen, diese aber als Text gegeben sind.
Im rechten Code werden die Zahlen alphabetisch sortiert, da sie wegen der Anfüh-
rungszeichen als Text markiert sind.
Frage: Wann tritt dieses Problem in der Praxis auf?
Zum Beispiel dann, wenn wir einen Text einlesen und daraus Zahlen extrahieren. R
erkennt nicht ohne weiteres, dass es sich um Zahlen handelt und behandelt sie wie
Text. Wie wir den Text in Zahlen umwandeln können, schauen wir uns in (11) an.
Dem Dreipunkteargument können wir beliebig viele Vektoren übergeben. Die Para-
meter sep und collapse steuern, wie die einzelnen Bestandteile zusammengefügt
werden. Wir schauen uns das am besten anhand eines Beispiels an!
128 C Wichtige Hilfsmittel
Beispiel: Erzeuge mit Hilfe von paste() einen String der Form "x1 und x2".
Die beiden Objekte "x" und 1:2 werden in das Dreipunkteargument gesteckt. "x"
wird zunächst recycelt und auf die Länge 2 (Länge von 1:2) gebracht. Da wir den Pa-
rameter sep nicht spezifiziert haben, wird standardmäßig ein Leerzeichen eingesetzt.
Das erklärt die Lücke in der Ausgabe zwischen x und 1 bzw. 2.
Stopfen wir diese Lücke, indem wir sep = "", also auf einen Leerstring setzen!
Jetzt schmiegen sich die 1 und 2 eng an x an. sep steuert also, wie die Elemente
zwischen den Objekten separiert werden.
Der resultierende Vektor besteht aus zwei Elementen, nämlich "x1" und "x2". Wol-
len wir diese Elemente miteinander zu einem Element kollabieren lassen, so
bedienen wir uns des Parameters collapse.
Hier werden also die Elemente x1 und x2 via collapse = " und " miteinander zu
einem Element verschmolzen. Beachte das Leerzeichen vor und nach dem und!
Das Dreipunkteargument zu Beginn von paste() erhöht die Flexibilität enorm, da
wir der Funktion beliebig viele Objekte übergeben können. Insbesondere können wir
zum Beispiel auch nur numerische Vektoren einsetzen.
> paste(1:3, 4:6, 7:9, sep = " * ", collapse = " und ")
[1] "1 * 4 * 7 und 2 * 5 * 8 und 3 * 6 * 9"
Beispiel: Erzeuge aus den Lösungen der quadratischen Gleichung des Kapitelbe-
ginns auf Seite 126 einen String der folgenden Form:
Voilà! In Abb. 10.1 visualisieren wir die Funktionsweise von sep und collapse.
10 Texte, Zeichenketten und Strings 129
> paste(var, res, sep = " = ", collapse = " und ")
var res
"x1" 1.5
sep = " = " collapse = " und "
"x2" 1
Abbildung 10.1: Die Parameter sep und collapse der Funktion paste()
Beachte: Die Parameter sep und collapse müssen exakt benannt werden, da
beide Parameter nach dem Dreipunkteargument stehen (vgl. (4.3.3)).
Uns steht auch die Funktion paste0() zur Verfügung. Sie entspricht paste() mit
dem Unterschied, dass sep ein Leerstring ("") und nicht umstellbar ist.
Oftmals möchten wir gerne auf Sequenzen von Buchstaben zugreifen. So könn-
ten wir uns etwa für alle Buchstaben von a bis c interessieren. Anstatt explizit alle
Buchstaben via c("a", "b", "c") aufzulisten, was bei vielen Buchstaben anstren-
gend ist, können wir auf die vordefinierte Variable letters zugreifen:
> # Sequenz von "a" bis "c" > # Sequenz von "a" bis "c"
> letters[1:which(letters == "c")] > letters[letters <= "c"]
[1] "a" "b" "c" [1] "a" "b" "c"
Wir sehen im rechten Code, dass auch Vergleichsoperatoren mit Texten umgehen
können. Die Anweisung letters <= "c" gibt uns einen logischen Vektor zurück,
wobei ein TRUE bedeutet, dass der entsprechende Buchstabe auf der linken Seite im
Alphabet nicht nach dem "c" kommt.
130 C Wichtige Hilfsmittel
Für die Großbuchstaben steht völlig analog die Variable LETTERS zur Verfügung.
Das Ganze funktioniert nur dann, wenn wir die Objekte letters und LETTERS nicht
überschreiben! Dasselbe Prinzip, wie wir es von der Zahl pi aus (4.7.4) kennen.
Strings sind case sensitive, das heißt, dass zwischen Groß- und Kleinschreibung un-
terschieden wird.
10.5 Abschluss
10.5.2 Ausblick
In (31) erfahren wir, wie wir Zeichenketten ohne Anführungszeichen und hübsch
gestylt auf dem Bildschirm ausgeben können. Und nicht nur das: Wir können die
Ausgabe statt auf die R-Console auch auf Dateien umleiten! Zentrale Funktion dieses
Vorhabens wird cat() sein, sehr gerne darfst du dir schon jetzt diese Funktion in
der R-Hilfe anschauen, wenn du möchtest ;-)
Im Code paste("x", 1:2, sep = ) haben wir Strings ("x") mit Zahlen (1:2) ver-
mischt. Was intern dabei passiert, betrachten wir in (11), wenn wir über Datentypen
sprechen. Ebenda erfahren wir auch, wie wir Texte in Zahlen umwandeln.
10 Texte, Zeichenketten und Strings 131
10.5.3 Übungen
1. Erzeuge zwei Vektoren derselben Länge mit beliebigen (zufälligen) Zahlen zwi-
schen 0 und 9. Zum Beispiel:
[1] [1] "3 + 5 = 8" "8 + 9 = 17" "4 + 2 = 6" "1 + 5 = 6"
2. In (6) ab Seite 70 haben wir Lotto gespielt. Der abgegebene Tipp und das
Ziehungsergebnis haben gelautet:
Dein Code soll für beliebige Tipps und Ziehungsergebnisse funktionieren. Teste
deinen Code, indem du mit sample() einige Tipps und Ziehungen simulierst!
132 C Wichtige Hilfsmittel
Was meinst du: Ist 12 kleiner als "9"? Nein (FALSE) sagst du? Fragen wir einmal R:
In (10.2) haben wir schon anklingen lassen, warum "12" kleiner als "9" ist (beachte
die Anführungszeichen). Warum aber auch TRUE herauskommt, wenn wir eine Zahl
mit einem String vergleichen, erfahren wir in (11.3).
Oft extrahieren wir numerische Informationen aus Texten. Damit R diese korrekt als
Zahlen interpretiert und wir mit den Zahlen rechnen können, müssen wir die Strings
in numerische Vektoren umwandeln. Wie das funktioniert, schauen wir uns in (11.4)
an.
Davor studieren wir in (11.1) und (11.2), in welcher Beziehung die Datentypen
logical, numeric und character zueinander stehen und wie wir den Datentyp
erfragen. Und abschließend werfen wir in (11.5) einen Blick auf komplexe Zahlen.
Vektoren können nur einen einfachen Datentyp (oder Mode, wie es in R heißt) auf-
weisen1 . Folgende Modes kennen wir bereits:
Frage: Was passiert, wenn wir in der Funktion c() gemischte Modes verwenden?
Um diese Frage zu beantworten, schauen wir uns folgende Hierarchie an:
Jeder logische Wert (TRUE und FALSE) lässt sich als Zahl darstellen (1 und 0). Um-
gekehrt lassen sich aber nicht alle numerischen Werte als Wahrheitswert darstellen.
Jede Zahl kann als Zeichenkette dargestellt werden, umgekehrt geht das nicht.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_11
11 Einfache Datentypen, Modes und Typumwandlung 133
Die Funktion mode() gibt uns an, welchen Mode das übergebene Objekt hat. Wollen
wir explizit nach einem bestimmten Mode fragen, so eignen sich die Funktionen
is.logical(), is.numeric() und is.character().
Frage: Was passiert, wenn wir numerische und logische Werte mischen?
> # Mische numerische Werte und logische Werte: Ergebnis ist vom Mode numeric
> x <- c(5, TRUE, 0, FALSE)
> x
[1] 5 1 0 0
> # Mische Strings mit anderen Modes: Ergebnis ist vom Mode character
> x <- c(1, "a", TRUE, -5.5, 1/4)
> x
[1] "1" "a" "TRUE" "-5.5" "0.25"
Hier dominiert "a" das Geschehen: Alle anderen Einträge werden zu Strings um-
funktioniert. Interessant sind die beiden folgenden Umwandlungen:
1. Der Ausdruck 1/4 wird nicht in "1/4" umgewandelt, sondern zuerst zu 0.25
ausgewertet und erst dann in "0.25" umgewandelt. Ausdrücke werden also vor
der Konvertierung ausgewertet.
2. Der Eintrag TRUE wird nicht in "1", sondern direkt in "TRUE" umgewandelt, da
TRUE ein bereits ausgewerteter Ausdruck ist. Es wird also direkt konvertiert.
134 C Wichtige Hilfsmittel
Rechenoperationen wie etwa "+" machen nur für Zahlen Sinn. Daher werden bei
solchen Operationen TRUE und FALSE implizit in 1 und 0 konvertiert. Ganz analog
verhält es sich bei gängigen Rechenfunktionen wie etwa sum().
Von dieser impliziten Typumwandlung haben wir schon in (3.5.1) ab Seite 24 Ge-
brauch gemacht, wo wir mit Hilfe von sum() die Anzahl der TRUE gezählt haben.
Verknüpfungsoperatoren wie etwa "|" machen nur für logische Vektoren Sinn.
Ein numerischer Vektor x wird zu einem logischen konvertiert (mittels x != 0).
Wie verhält es sich bei Vergleichsoperatoren? Schauen wir uns dazu folgenden
interessanten Fall an:
Im rechten Code wird vor dem Vergleich die 9 in einen String konvertiert um Ty-
pengleichheit herzustellen. Strings werden aber im Gegensatz zu Zahlen zeichenweise
verglichen. Der rechte Code sagt uns daher, dass der Text "12" im Alphabet vor
dem Text "9" kommt, während uns der linke Code mitteilt, dass die Zahl 12 nicht
kleiner als die Zahl 9 ist.
Der Operator < passt also sein Verhalten an den Datentyp bzw. an die Eigenschaften
der beteiligten Objekte an. Dasselbe Prinzip gilt für andere Operatoren auch. Wir
vertiefen dieses Konzept unter anderem in (26).
11 Einfache Datentypen, Modes und Typumwandlung 135
Frage: Was passiert, wenn wir einen Vektor vom Mode numeric in einen Vektor
vom Mode logical umwandeln?
a · x2 + b · x + c = 0
Wenn wir unser Skript zur Lösung einer quadratischen Gleichung aus (2.2.1) auf Sei-
te 6 mit obiger Parameterkonstellation ausführen, dann kommt NaN (Not a Num-
ber) heraus. Es gibt also keine reellwertige Lösung.
Aber es existiert eine Lösung in den komplexen Zahlen. Und genau die schauen wir
uns jetzt an!
√ √
−b ± b2 − 4ac 3 ± −4 √
x1,2 = = = 3 ± 2 · −1 = 3 ± 2i
2a 1
√
Die Zahl i = −1 heißt imaginäre Einheit und es gilt i2 = −1. Die beiden Lösungen
bestehen aus dem Realteil 3 und dem Imaginärteil ±2.
Wir überprüfen, ob x1 = 3 − 2i und x2 = 3 + 2i die quadratische Gleichung lösen.
In Übung 4 auf Seite 138 darfst du diese Lösung (automatisiert) bestimmen ;-)
> # Kontrolle
> a * x^2 + b * x + c
[1] 0+0i 0+0i
√
0+0· −1 = 0, wir haben uns also nicht verrechnet!
Den Realteil bzw. Imaginärteil einer komplexen Zahl rufen wir mit den Funktionen
Re() bzw. Im() ab.
Mehr zum Thema komplexe Zahlen findest du unter anderem in der R-Hilfe unter
"?complex".
11 Einfache Datentypen, Modes und Typumwandlung 137
11.6 Abschluss
• Wie erfragen wir den Datentyp (Mode) eines Objekts? Was passiert, wenn wir
der Funktion c() Elemente unterschiedlichen Modes übergeben? (11.2)
• Was passiert mit logischen Vektoren bei Anwendung von Rechenoperationen
bzw. Rechenfunktionen? Was passiert, wenn wir Strings in Rechenoperationen,
logische Verknüpfungen und Vergleichsoperatoren stecken? Was passiert, wenn
wir Zahlen logisch verknüpfen? (11.3)
• Wie führen wir explizite Typumwandlungen durch? In welchen Fällen treten
NA-Werte dabei auf? (11.4)
• Wie definieren wir komplexe Zahlen in R? Wie fragen wir den Real- bzw.
Imaginärteil ab? (11.5)
11.6.2 Ausblick
In (17) besprechen wir die Datenstruktur list, die mehrere Modes verwalten kann.
Vektoren und Matrizen (siehe (15)) können nur einen Mode verwalten.
In (22) und (23) extrahieren wir unter anderem numerische Informationen aus Tex-
ten. Um mit ihnen rechnen zu können, ist die Umwandlung in einen numerischen
Datentyp notwendig, wie wir in (11.4) schon gesehen haben. Die explizite Typum-
wandlung wird auch in (12) notwendig, wenn wir zum Beispiel aus Häufigkeitstabel-
len gewichtete Mittelwerte berechnen.
11.6.3 Übungen
1. Was kommt bei folgenden Ausdrücken heraus: TRUE, FALSE oder eine Fehler-
meldung? Begründe und erkläre jeweils deine Wahl!
2. Wir wollen aus dem folgenden Vektor x all jene Elemente selektieren, die dem
Wert 2 oder 4 gleichen:
In diesem Fall also das Element 2. Ein Kollege schlägt folgende Lösung vor:
# Vorschlag Nummer 1
> x[x == 2 | 4]
Ein anderer Kollege kritisiert, dass der Vergleichsoperator "==" Vorrang ge-
genüber dem Vergleichsoperator "|" hat (womit er recht hat) und meint, es
müssen Klammern gesetzt werden:
# Vorschlag Nummer 2
> x[x == (2 | 4)]
a) Was wird nach Ausführung der beiden vorgeschlagenen Codes jeweils auf
die Console gedruckt? Erkläre dabei die internen Abläufe.
b) Schlage mindestens eine Lösung vor, die das Problem korrekt löst.
Wir haben in (3.2) gesagt, dass es neben Subsetting mit Indizes und Wahrheitswerten
noch eine weitere Möglichkeit des Subsettings gibt. Jetzt lüften wir das Geheimnis
und lernen die Magie von Names kennen.
Wir befragen 8 Studierende, wie viele Geschwister sie haben. Die Befragung ergibt:
0, 1, 0, 1, 6, 5, 2, 0
Unser Hauptziel in diesem Kapitel ist die Erstellung einer Häufigkeitstabelle mit der
Anzahl der Geschwister. In Tab. 12.1 sehen wir eine erste einfache Häufigkeitstabelle,
wie wir sie in (12.2.1) erstellen werden.
Anzahl Geschwister 0 1 2 5 6
Häufigkeit 3 2 1 1 1
Standardmäßig werden nur jene Kategorien abgebildet, die mindestens einmal ge-
nannt werden. Da niemand 3 oder 4 Geschwister hat, scheinen diese Kategorien in
der Häufigkeitstabelle also nicht auf.
Unter anderem dieser Umstand sorgt dafür, dass folgende Fragen nicht so trivial zu
beantworten sind, wenn wir ausschließlich auf die Häufigkeitstabelle zugreifen:
In (12.2.4) lernen wir schließlich eine Technik kennen, wie wir eine lückenlose Häu-
figkeitstabelle erstellen können, in der auch 3 und 4 Geschwister mit Häufigkeit 0
abgebildet werden.
Genau dafür brauchen wir Beschriftungen (names)! Daher führen wir gleich zu Be-
ginn in (12.1) zuerst die notwendigen Grundlagen zu Beschriftungen ein, bevor wir
uns auf das heiße Thema „Häufigkeitstabellen“ stürzen.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_12
140 C Wichtige Hilfsmittel
Der Vektor ngeschwister.quant ist also benannt. Wir vergleichen jetzt zwei Zu-
griffsmethoden und klären gleich, warum eine Methode sicherer ist.
Beispiel: Selektiere das 1. und das 3. Quartil aus dem Vektor ngeschwister.quant.
Im linken Code selektieren wir das 2. und 4. Element. Im rechten Code hingegen
selektieren wir die Elemente mit den Beschriftungen "25%" und "75%".
Beachte: Die Beschriftungen (hier 25% und 75%) werden bei der Selektion mitge-
nommen! Die Beschriftungen kleben also quasi an ihren Elementen.
Frage: Was passiert, wenn wir andere Quantile berechnen und dann beim Subsetting
darauf vergessen die Indizes anzupassen?
Wir müssten c(2, 4) in c(1, 3) ändern und genau hier steckt die tückische Fehler-
quelle. Wenn wir die Interquartilsdistanz (Differenz zwischen 75%- und 25%-Quantil)
berechnen, so schützt uns das NA noch davor, dass wir sie nicht versehentlich mit den
falschen Quantilen bestimmen. Im rechten Code würde hingegen alles glatt laufen.
Probieren wir andere Quantile aus!
Damit erhöhst du die Sicherheit, dass dein Code auch dann korrekte Ergebnisse
liefert, wenn sich die Vorgeschichte deines Codes im Nachhinein ändert! Achte
darauf, dass deine Beschriftungen eindeutig sind!
R sorgt also in diesem Fall dafür, dass die Beschriftungen eindeutig sind.
Wenn die Beschriftungen nicht eindeutig sind, dann wird immer nur das erste
Auftreten selektiert:
Der zweite Eintrag mit der Beschriftung "a" (3) wird nicht selektiert. Wir beherzigen
also immer Regel 11 und wählen eindeutige Beschriftungen.
Wenn wir nachträglich Beschriftungen hinzufügen wollen oder die bestehende Be-
schriftung ersetzen wollen, so können wir von folgender Syntax Gebrauch machen:
names(vektor ) <- gewünschteBeschriftung
Wir beschriften sogleich die Elemente von ngeschwister.range mit Min und Max.
> ngeschwister.range
[1] 0 6
Wunderbar! Wir können auch nur einen Teil der Beschriftung ändern.
12 Beschriftungen, Names und Häufigkeitstabellen 143
Mit Hilfe von Häufigkeitstabellen zeigen wir einige Tücken im Zusammenhang mit
Names auf. Wir wollen für die Anzahl der Geschwister der befragten Studierenden
> ngeschwister
[1] 0 1 0 1 6 5 2 0
Die Berechnung der mittleren Anzahl an Geschwistern mit Hilfe der Häufigkeitsta-
belle schaut einfach aus: Die Names von tab.geschwister beinhalten die Anzahl
der Geschwister und die Elemente von tab.geschwister die absoluten Häufigkeiten.
Nun können wir (einfach?) ein gewichtetes Mittel bestimmen à la:
1
x̄ = · (0 · 3 + 1 · 2 + 2 · 1 + 5 · 1 + 6 · 1) = 1.875
3+2+1+1+1
> # Gewichtetes Mittel berechnen - So geht es nicht!
> sum(names(tab.geschwister) * tab.geschwister / sum(tab.geschwister))
Fehler in names(tab.geschwister) * tab.geschwister :
nicht-numerisches Argument für binären Operator
> # Kontrolle
> mean(ngeschwister)
[1] 1.875
Übrigens: Wenn wir uns in der R-Hilfe bei der Funktion mean() die Rubrik see
also zu Gemüte führen, so stoßen wir auf die Funktion weighted.mean(), mit der
wir gewichtete Mittelwerte berechnen können.
> tab.geschwister
0 1 2 5 6
3 2 1 1 1
Dieser Ansatz führt nicht zum Ziel. Zur Erinnerung: names sind immer vom Mode
character! Wenn wir tab.geschwister[1] eingeben, wird das 1. Element selek-
tiert, und das ist jenes mit der Beschriftung 0 (Einzelkind).
Wir möchten aber jenen Eintrag mit der Beschriftung 1, also müssen wir einen String
übergeben, damit es sicher klappt:
Wir müssen also die gewünschten Zahlen in Zeichenketten konvertieren, ehe wir mit
sum() die Anzahlen addieren.
146 C Wichtige Hilfsmittel
Niemand hat also 3 oder 4 Geschwister und wir erkennen die Lücke in der Häufig-
keitstabelle, die Beschriftungen 3 und 4 fehlen.
> tab.geschwister
0 1 2 5 6
3 2 1 1 1
Die Zeit ist gekommen, diese Lücke elegant mit Nullen zu stopfen!
> # ... der mit den Werten von tab.geschwister befüllt werden soll.
> tab.geschwister
0 1 2 5 6
3 2 1 1 1
Du erkennst vielleicht schon, worauf diese Idee hinausläuft. Wenn wir die Nullen im
selektierten Teilbereich von tab.geschwister.voll durch die korrekten Werte aus
tab.geschwister ersetzen, dann sind wir am Ziel!
12 Beschriftungen, Names und Häufigkeitstabellen 147
12.3 Abschluss
• Wie greifen wir auf die Beschriftungen eines Vektors zu? (12.1.1)
• Wie funktioniert das Subsetting mit Names? Warum ist Subsetting mit Names
sicherer als Subsetting mit Indizes? (12.1.2)
• Wie können wir Beschriftungen hinzufügen, ersetzen oder löschen? Warum
sollen die Beschriftungen eindeutig sein? (12.1.3), (12.1.4)
148 C Wichtige Hilfsmittel
• Welchen Mode haben Names? Wie berechnen wir gewichtete Mittelwerte aus
einer Häufigkeitstabelle heraus? (12.2.2)
• Worauf müssen wir bei der Selektion von Elementen oder Teilbereichen einer
Häufigkeitstabelle mit numerischen Kategorien achten? (12.2.3)
12.3.2 Ausblick
Beschriftungen werden uns noch häufiger begegnen, unter anderem in (15), wenn
wir Matrizen einführen. Kreuztabellen als Erweiterung von Häufigkeitstabellen be-
sprechen wir in (25).
In (12.1.4) haben wir heimlich, still und leise NULL verwendet. In (14) erfahren wir
mehr über NULL und NA.
12.3.3 Übungen
> tab.geschwister
0 1 2 5 6
3 2 1 1 1
tab.geschwister[1:5]
tab.geschwister[as.character(1:5)]
i. Schaut eigentlich gut aus, aber warum funktioniert der Code nicht
mehr korrekt, wenn jemand zum Beispiel 12 Geschwister hat?
ii. Welche implizite Annahme steckt in diesem Code? Korrigiere den
Code, sodass er die Aufgabe immer korrekt erfüllt.
Falls du TRUE vermutest, so müssen wir dich leider enttäuschen! Warum FALSE her-
auskommt, klären wir in (13.1).
In (7.6.2) auf Seite 95 haben wir einen Vektor mit Gewichten einer Apfelstichprobe
standardisiert, also auf Mittelwert 0 und Standardabweichung 1 gebracht.
Der Mittelwert des standardisierten Vektors ist hier aber nicht exakt gleich 0, so-
dass die Abfrage auf Gleichheit mit 0 ein FALSE liefert. Wie wir mit derartigen
Rundungsfehlern umgehen, schauen wir uns in (13.2) an.
Beispiel: Prüfe nach, ob das Ergebnis der Rechnung 2.05 − 0.05 exakt 2 ergibt.
Es kommt hier aber nur scheinbar exakt 2 heraus. Die Prüfung auf Gleichheit be-
schert uns FALSE. Um den Rundungsfehler zu sehen, stellen wir mit options() (vgl.
auch (6.4)) die Anzahl der Nachkommastellen mittels digits um.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_13
13 Computerarithmetik und Rundungsfehler 151
Das hängt mit der Computernumerik zusammen, Dezimalzahlen werden mit 2er-
Potenzen angenähert, sofern sie nicht exakt darstellbar sind. So gibt es Zahlen,
die der Computer sehr gerne hat (zum Beispiel 0.25 = 2−2 , 0.15625 = 2−3 + 2−5 )
und Zahlen, die er nicht sonderlich mag (zum Beispiel 0.05).
Die Zahl 0.05 lässt sich nicht exakt mit endlich vielen 2er-Potenzen nachbauen. R
kann sie lediglich so gut es geht annähern.
> 2^-5
[1] 0.03125
> 2^-5 + 2^-6
[1] 0.046875
> 2^-5 + 2^-6 + 2^-9
[1] 0.04882812
> 2^-5 + 2^-6 + 2^-9 + 2^-10 + 2^-13 + 2^-14 + 2^-17 + 2^-18 + 2^-21
[1] 0.04999971
Die Funktion vergleicht zwei Objekte (target und current) auf Gleichheit unter
Berücksichtigung der Rechengenauigkeit die wir mit tolerance einstellen können.
Relevante Details zu dieser Funktion schauen wir uns gleich an. Zunächst zeigen wir
die Anwendung dieser Funktion anhand des Beispiels aus (13.2).
Beispiel: In (7) haben wir deskriptive Statistiken für das Gewicht einer Apfelstich-
probe berechnet und in (7.6.2) den Vektor apfel.teil standardisiert.
Überprüfe, ob der Mittelwert bzw. die Standardabweichung des standardisierten
Vektors annähernd gleich 0 bzw. 1 sind.
Also ja: Der Mittelwert ist annähernd 0 und die Standardabweichung annähernd 1.
13 Computerarithmetik und Rundungsfehler 153
Aufpassen müssen wir darauf, dass all.equal() standardmäßig die Größe, Attribute
und Beschriftungen der Objekte mitvergleicht.
Hier macht uns R darauf aufmerksam, dass lediglich current (soll) beschriftet ist.
> names(soll) <- c("Mean", "Sd") # soll anders beschriften als ist
> soll
Mean Sd
0 1
Hier passen die Beschriftungen beider Objekte nicht zusammen. Zur Erinnerung: R
ist case sensitive (vgl. (10.4)).
Wollen wir von all.equal(), dass Beschriftungen ignoriert werden, so setzen
wir check.attributes = FALSE.
13.3 Abschluss
• Wieso kommt bei der Abfrage 2.05 - 0.05 == 2 ein FALSE heraus? Welche
Dezimalzahlen lassen sich mit R exakt darstellen? Wie stellen wir die Anzahl
der angezeigten Nachkommastellen um? (13.1)
154 C Wichtige Hilfsmittel
• Wie funktioniert die Abfrage zweier Zahlen auf annähernde Gleichheit? Was
ist die Idee dahinter? (13.2.1)
• Wie können wir zwei Objekte auf annähernde Gleichheit überprüfen? Was
müssen wir beachten, wenn die Objekte Beschriftungen haben? (13.2.2)
13.3.2 Ausblick
Die hier gelernten Techniken finden unter anderem bei Iterationsverfahren Anwen-
dung. Bei solchen Verfahren wird eine Lösung für ein Problem solange schrittweise
verfeinert, bis sie das Problem hinreichend genau löst.
Ein Beispiel sind Nullstellenprobleme: Wir wollen eine Nullstelle für eine stetige
Funktion f (x) bestimmen, also ein x finden, sodass f (x) = 0 ist. Wir geben eine
Startlösung x0 vor und bestimmen eine Folge x1 , x2 , x3 , . . . von Lösungen, die gegen
eine Nullstelle konvergiert. Wir brechen ab, sobald ein Folgenglied xn nahe genug
an 0 ist, also |f (xn )| < ϵ gilt.
In (29.5.2) schauen wir uns als Beispiel den relativ einfachen Bisektionsalgorithmus
an, mit dem wir die Wurzel aus 2 berechnen. Davor brauchen wir aber noch Schleifen,
die wir in (28) besprechen.
13.3.3 Übungen
1. Was wird nach Ausführung der folgenden Codezeile auf die Console gedruckt
und warum?
14 Konstante
Das funktioniert leider nicht. Anders als in herkömmlichen Situationen, in denen wir
mit "==" auf Gleichheit prüfen können, funktioniert dies bei vielen Konstanten wie
NA nicht. Unter anderem darauf gehen wir genau ein!
Wir begleiten in diesem Kapitel Studierende, die eine Prüfung schreiben. Insgesamt
konnten sie 40 Punkte erzielen. Die Prüfungsergebnisse lauten:
> pkt <- c(11, 38, NA, 24, 24, 31, 19, 35, NA, 22)
> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22
Fehlende Werte (NAs) bedeuten, dass die Person nicht zur Prüfung angetreten ist.
Wir fragen uns unter anderem:
Zusätzlich schauen wir uns in (14.1) bekannte Konstante noch einmal genauer an,
lernen in (14.4) die Unendlichkeit kennen und erfahren in (14.5), was bei nicht defi-
nierten Rechenoperationen wie etwa 0/0 passiert.
In (14.3.3) rechnen wir die Punkte in die Note um und stoßen dabei auf eine span-
nende Codezeile.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_14
156 C Wichtige Hilfsmittel
Mit ?Constants verschaffen wir uns einen Überblick über gebräuchliche Konstan-
te, die in R eingebaut sind und uns das Programmierleben angenehmer machen.
Folgende Konstante kennen wir bereits aus (10.4) und (4.7.4):
In (4.7.4) und (10.4) haben wir erwähnt, dass wir diese Konstanten nicht überschrei-
ben sollten, da sie dann nicht mehr zur Verfügung stehen.
Das schlimmste Szenario: Wir verwenden im Code diese Konstanten und überschrei-
ben sie oberhalb der betreffenden Codezeile(n) zu einem späteren Zeitpunkt.
Im besten Fall bekommen wir eine Fehlermeldung, dann wissen wir wenigstens, dass
etwas nicht stimmt. Im schlechtesten Fall werden wir im Glauben gelassen, dass alles
in Ordnung ist, bekommen aber völlig falsche Ergebnisse. In (14.1.2) schauen wir
uns ein besonders tückisches Beispiel an.
Frage: Was können wir tun, wenn wir eingebaute Konstante zurückgewinnen
wollen, die wir versehentlich überschrieben haben?
Die Antwort lautet: Lösche die überschriebene Version der Konstante. Wir schauen
uns das Rezept anhand der Kreiszahl π (pi) an.
> # pi überschreiben
> pi <- "pi ist im Moment leider nicht erreichbar!"
> pi # Die Zahl pi, wie sie NICHT sein sollte.
[1] "pi ist im Moment leider nicht erreichbar!"
> # pi zurückgewinnen
> rm(pi)
> pi # Jetzt haben wir pi wieder!
[1] 3.141593
Mit rm(pi) löschen wir die überschriebene Version von pi, sodass wieder die ur-
sprüngliche Belegung (3.141593) zu Tage tritt.
14 Konstante 157
Wir brauchen uns übrigens keine Sorgen zu machen, dass wir die Kreiszahl pi ver-
sehentlich löschen können. Denn in dem Fall warnt uns R:
> rm(pi)
Warnung in rm(pi) Objekt ’pi’ nicht gefunden
> pi # pi ist immer noch da!
[1] 3.141593
Vielleicht hast du ja schon aufgeschnappt, dass du TRUE mit T und FALSE mit F
abkürzen kannst. Falls ja: Vergiss es wieder! Denn T und F sind nicht vor Über-
schreibungen abgesichert! Wir schauen uns ein tückisches Beispiel an.
Beispiel: Wir betrachten ein Kartenset mit den Kartenwerten 1, 2, 3, 4, 5, 6.
Mische die Karten und verteile sie auf 2 Spieler. Spieler 1 bekommt jede zweite
Karte (beginnend bei der ersten) und Spieler 2 erhält alle anderen Karten.
> # Überschreibe T
> T <- FALSE
> T # (!!!)
[1] FALSE
Spieler 1 wird keine große Freude haben, denn er bekommt keine Karte, während
Spieler 2 alle Karten bekommt. Der Grund für dieses Desaster liegt in der Zuweisung
von c(T, F) auf das Objekt bool.1. Da wir T via T <- FALSE überschrieben haben,
enthält c(T, F) nicht mehr das, was wir uns erwarten.
Zugegeben, das Beispiel ist ziemlich konstruiert. Aber in (9.7.1) haben wir etwa die
Teststatistik eines t-Tests berechnet und diese mit T bezeichnet.
158 C Wichtige Hilfsmittel
Regel 12: Verwende stets TRUE und FALSE anstelle von T und F!
TRUE und FALSE sind vor Überschreibungen geschützt! T und F sind es nicht.
Gelegentlich haben wir NULL schon gesehen. Grob gesagt ist NULL ein Objekt, welches
das Nichts verkörpert. Ein Objekt ohne Eigenschaften bzw. ohne Existenz und Wert.
Wir listen ein paar Anwendungsmöglichkeiten für NULL auf:
1. Dummy-Initialisierung eines Objekts.
2. Löschen von Elementen einer Liste.
3. Löschen von Beschriftungen bzw. Markierung des Umstands, dass keine Be-
schriftung existiert.
Die erste Einsatzmöglichkeit schauen wir uns in (30.3) im Zuge der Rekursion an.
Die zweite in (17.6), wenn wir über Listen sprechen. Die letzte hingegen betrachten
wir schon jetzt.
In (12.1.4) haben wir schon erwähnt, wie wir Beschriftungen löschen. Daher
stürzen wir uns gleich in ein Beispiel!
Beispiel: Füge dem Vektor pkt die Beschriftung "Nr1", "Nr2", "Nr3", . . . zu. Lösche
anschließend die Beschriftungen wieder.
Das Ergebnis ist NULL, das heißt, es existieren derzeit keine Beschriftungen. Fügen
wir jetzt Beschriftungen hinzu!
Wollen wir Beschriftungen löschen, so weisen wir den Names NULL zu.
Wenn wir wissen wollen, ob Beschriftungen existieren, so kommen wohl viele zu-
nächst auf die folgende naheliegende Idee:
Klappt nicht: Es wird ein leerer logischer Vektor zurückgegeben. Mit der Funktion
is.null() funktioniert es hingegen!
Wie viele Studierende sind der Prüfung fern geblieben? Mit anderen Worten: Wie
viele fehlende Werte (NA-Werte) enthält der Vektor pkt?
Zunächst möchten wir eine kreative – wenngleich sehr ineffiziente – Lösungsvariante
einer Studentin unserer Lehrveranstaltung präsentieren. Ihre Idee basiert darauf,
dass die Funktion sort() fehlende Werte defaultmäßig löscht.
> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22
> sort(pkt)
[1] 11 19 22 24 24 31 35 38
Irgendwie genial!
Im folgenden Unterabschnitt schauen wir uns die effizientere Variante an.
160 C Wichtige Hilfsmittel
> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22
> pkt == NA # schaut wild aus ;-)
[1] NA NA NA NA NA NA NA NA NA NA
Funktioniert aber genauso wenig, wie bei NULL. Besser klappt es mit der Funktion
is.na(). Mit ihr prüfen wir Elemente auf Gleichheit mit NA.
Wenn wir den Mittelwert und andere deskriptive Maßzahlen des Vektors pkt be-
stimmen wollen, wobei NAs nicht berücksichtigt werden sollen, so können wir zuerst
die fehlenden Werte entfernen und anschließend den Mittelwert bestimmen.
Mit is.na(pkt) fragen wir, ob die Elemente NA gleichen. Wir selektieren jene Ele-
mente, die das nicht tun. Der Zusatz .valid steht für valide bzw. gültig.
Jetzt können wir nach Herzenslust Maßzahlen berechnen, ohne den Parameter na.rm
bemühen zu müssen.
> # Mittelwert der gültigen Punkte > # Quantile der gültigen Punkte
> mean(pkt.valid) > quantile(pkt.valid)
[1] 25.5 0% 25% 50% 75% 100%
11.00 21.25 24.00 32.00 38.00
Viele Funktionen haben eigene Parameter zum Umgang mit fehlenden Werten. Den
Parameter na.rm kennen wir schon aus (7).
In sort() finden wir den Parameter na.last, mit dem wir steuern, ob und wo wir
fehlende Werte anordnen wollen.
Wir wollen eine Häufigkeitstabelle der Noten erstellen. Davor rechnen wir die Punkte
noch in eine Note um, und zwar nach folgendem Notenschlüssel:
1: [35, 40] 2: [30, 35) 3: [25, 30) 4: [20, 25) 5: [0, 20)
Es folgt eine echt coole Codezeile, die du dir gerne (evtl. mit Hilfe von (8.2.1) und
(4.7.2)) genauer überlegen darfst! Ein kleiner Magic Moment!
In table() entdecken wir aufregende Parameter. Diese müssen wir dabei exakt
benennen, den Grund dafür kannst du die vielleicht denken. Kleiner Tipp: Der
Grund hat drei Punkte ;-).
Die erste Entdeckung ist exclude. Mit exclude steuern wir, welche Ausprägungen
ausgeschlossen werden sollen. Setzen wir etwa exclude = NULL, so wird nichts aus-
geschlossen, sodass auch fehlende Werte vorkommen. Mit exclude = NA schließen
wir NA-Werte aus.
Eine weitere Entdeckung ist useNA. Mit useNA können wir einstellen, wann NA an-
gezeigt werden soll:
• useNA = "ifany": NA wird nur dann angezeigt, wenn es fehlende Werte gibt.
• useNA = "always": NA wird immer angezeigt. Gibt es keine fehlenden Werte,
so beträgt die Häufigkeit gleich 0.
Oftmals möchten wir bedingte Maßzahlen bestimmen. Ein Beispiel bringt Klar-
heit, was damit gemeint ist.
Beispiel: Bestimme für den Vektor pkt:
1. Wie viele Studierende haben den Test mit mindestens 20 Punkten bestanden?
2. Bestimme den Punktemittelwert aller abgegebenen Tests.
3. Bestimme den Punktedurchschnitt bedingt darauf, dass mindestens 20 Punkte
erzielt wurden.
> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22
Eine bequeme Alternative bietet uns die Funktion subset(), mit der wir (unter
anderem) Teilmengen aus Vektoren selektieren können.
Dem gleichnamigen Parameter subset können wir eine Bedingung übergeben, die
dann automatisch mit !is.na(pkt) mit der Und-Verknüpfung kombiniert wird.
Der Punkteschnitt aller abgegebenen Tests mit mindestens 20 Punkten beträgt also
29 Punkte.
Es stellt sich die Frage, warum der Code pkt[pkt >= 20 & !is.na(pkt)] des vor-
angehenden Abschnitts überhaupt funktioniert. Gehen wir diese Codezeile schritt-
weise durch und schauen uns ihre Einzelbestandteile an.
> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22
> pkt >= 20 # NA bleibt NA
[1] FALSE TRUE NA TRUE TRUE TRUE FALSE TRUE NA TRUE
> !is.na(pkt)
[1] TRUE TRUE FALSE TRUE TRUE TRUE TRUE TRUE FALSE TRUE
> pkt >= 20 & !is.na(pkt)
[1] FALSE TRUE FALSE TRUE TRUE TRUE FALSE TRUE FALSE TRUE
Mit der Und-Verknüpfung kombinieren wir beim 3. und 9. Element NA mit FALSE.
Frage: Was passiert allgemein, wenn wir NA und logische Werte verknüpfen? Fol-
gender Code beantwortet unsere Frage.
Die Ergebnisse machen absolut Sinn! Bei der Und-Verknüpfung (linke Spalte)
müssen beide Ausdrücke TRUE sein. Bei TRUE & NA können wir das wegen des feh-
lenden Wertes nicht beurteilen, NA ist die logische Konsequenz. Bei FALSE & NA ist
der Ausdruck definitiv FALSE.
Wir wollen in einem Land mit 8 Millionen Einwohnern eine Zufallsstichprobe der
Größe 100 ziehen. Wie viele mögliche Zufallsstichproben gibt es?
> # Parameter
> n <- 8 * 10^6 # Größe der Grundgesamtheit
> k <- 100 # Größe der Stichprobe
> choose(n = n, k = k) # Anzahl der Stichproben
[1] Inf
Das Ergebnis ist Inf (Inf inite – Unendlich). Laut R gibt es also unendlich viele
Zufallsstichproben. Gibt es unendlich viele Zufallsstichproben? Natürlich nicht. Al-
lerdings ist die Anzahl der Stichproben so groß, dass sie R nicht mehr exakt darstel-
len kann. Für die angewandte Statistik ist dies praktisch bedeutungslos; die größte
darstellbare ganze Zahl in R ist ca. 10308 , also eine 1 mit 308 Nullen (!).
Wir können mit Inf normal rechnen, solange die Rechnung wohldefiniert ist.
> Inf * 5 > Inf + 2 > -Inf - 55 > Inf - 2 > Inf / 2
[1] Inf [1] Inf [1] -Inf [1] Inf [1] Inf
Bei der Division durch 0 entsteht Inf oder -Inf, je nach Vorzeichen. Dividieren wir
durch Inf, so kommt 0 heraus. Beides gilt nur dann, wenn die Rechnung wohldefi-
niert ist, andernfalls kommt NaN (Not a Number, siehe (14.5)) heraus.
Anhand eines Codebeispiels schauen wir uns die Abfragemöglichkeiten auf Un-
endlichkeit an.
Wollen wir auf Gleichheit mit −∞ oder +∞ abfragen, so haben wir mehrere Op-
tionen. In der rechten Spalte lernen wir dabei die Funktionen is.infinite() und
is.finite() kennen.
Beachte: Die Funktion is.infinite() prüft explizit auf Gleichheit mit ±∞. Im
Gegensatz dazu prüft die Funktion is.finite() auf Ungleichheit mit ±∞ sowie NA
und NaN.
Also: is.finite(x) und !is.infinite(x) sind nicht ident! Wir betrachten ein
kleines Codebeispiel.
Die Operation 0 / 0 ist nicht definiert, ebenso wie ∞ − ∞. Die Wurzel einer nega-
tiven Zahl liefert ebenso NaN.
Mit der Funktion is.nan() fragen wir ab, ob Elemente NaN sind.
> is.nan(x)
[1] TRUE FALSE TRUE FALSE FALSE TRUE
Beachte: Die Funktion is.na() prüft auch auf Gleichheit mit NaN:
Möchten wir lediglich auf reine NA prüfen, böte sich folgende Möglichkeit an:
14.6 Abschluss
Die Konstanten NULL, NA, Inf und NaN wollen von uns besondere Zuwendung. Wir
fassen diese in Tab. 14.1 zusammen.
• Wie gewinnen wir in R eingebaute Konstante zurück, wenn wir diese verse-
hentlich überschreiben? (14.1.1)
• Warum sollten wir TRUE und FALSE anstatt T und F verwenden? (14.1.2)
• Wie löschen wir Beschriftungen eines Vektors? Wie fragen wir auf Gleichheit
mit NULL ab? (14.2)
• Was bedeutet NA? Wie fragen wir auf Gleichheit mit NA ab? Wie selektieren
wir aus einem Vektor Elemente ungleich NA? (14.3.1), (14.3.2)
• Was bewirkt der Parameter na.rm? Was bewirken die Parameter na.last in
sort() sowie exclude und useNA in table()? Wie können wir diese Parameter
einstellen? (14.3.3)
• Wie berechnen wir bedingte Maßzahlen? Wie wenden wir subset() korrekt
an und wie verhält sich subset() bei fehlenden Werten? (14.3.4)
• Was kommt heraus, wenn wir NA mit TRUE bzw. FALSE per Und- sowie Oder-
Verknüpfung kombinieren? (14.3.5)
• Welche Konstante steht für ∞? Wie fragen wir ab, ob Elemente ±∞ sind?
Wodurch unterscheiden sich is.finite() und !is.infinite()? (14.4)
• Was bedeutet die Konstante NaN und wann entsteht sie? Wie erfragen wir, ob
Elemente NaN sind? (14.5)
14 Konstante 167
14.6.3 Ausblick
Der Konstante NULL werden wir unter anderem bei Listen bzw. Dataframes in (17)
bzw. (20) wieder begegnen. Die Funktion is.na() ist nicht nur bei Vektoren nützlich,
sondern etwa auch bei Matrizen (15) und Dataframes (20).
Wenn du dich tiefer mit NA befassen möchtest, dann kannst du dir zum Beispiel die
Funktion na.omit() in der R-Hilfe ansehen. Dort findest du auch weitere interes-
sante Funktionen im Umgang mit fehlenden Werte.
14.6.4 Übungen
1. Von 8 Personen wurde das Geschlecht ("m" für männlich, "w" für weiblich),
die Größe in cm sowie das Gewicht in kg erhoben.
> geschlecht <- c("w", "w", "m", "m", "w", "w", "m", "m")
> groesse <- c(163, 171, 192, 183, 0, 172, 208, 182)
> gewicht <- c(NA, 60, Inf, 88, 0, -3, 878, 78)
Leider haben sich viele Befragte einen Scherz mit uns erlaubt und unplausible
(unrealistische) Angaben beim Gewicht gemacht.
a) Berechne mit den Rohdaten von oben den BMI jeder Person. Der BMI
errechnet sich gemäß kg/m2 (Gewicht in kg durch Körpergröße in m zum
Quadrat).
b) Wie viele plausible Werte des BMI gibt es insgesamt?
c) Wie viele plausible Werte des BMI gibt es bei Männern?
d) Ersetze im Vektor gewicht alle unplausiblen Werte durch NA und führe
die Berechnungen von 1a) bis 1c) erneut aus.
e) Selektiere alle gültigen (plausiblen) BMI-Werte, die von Männern stam-
men.
2. Wir betrachten das Codebeispiel auf Seite 164: Wie viele Zufallsstichproben
der Größe k gibt es in einem Land mit 8 Millionen Einwohnern? Finde heraus,
ab welchem Wert für k das Ergebnis Inf herauskommt.
D Datenstrukturen
Daniel Obszelka
170 D Datenstrukturen
15 Matrizen
Erinnern wir uns an die beiden Minigolfspieler von (8). Wir haben dort zwei Mini-
golfspieler auf den ersten 6 Bahnen begleitet und zwei Vektoren erstellt, welche die
Schlagzahlen der beiden Spieler enthalten:
Du wirst dich vielleicht gefragt haben, was gewesen wäre, wenn nicht 2, sondern 100
Spieler mitgespielt hätten. Hätten wir 100 Vektoren erstellt und 100 Mal dieselben
Berechnungen durchgeführt? Die Antwort lautet natürlich „Nein“! Deutlich besser
ist es, eine Matrix zu erstellen, die alle Schlagzahlen verwaltet, wobei zum Beispiel
gilt: Anzahl Zeilen = Anzahl bespielte Bahnen und Anzahl Spalten = Anzahl Spieler.
Alle in diesem Kapitel verwendeten Minigolf-Objekte können wie folgt geladen wer-
den. In (15.1.2) gesellt sich dann ein dritter Spieler dazu.
Darüber hinaus schauen wir uns viele interessante wie wichtige Aspekte im Zusam-
menhang mit Matrizen an. Und in (15.7.1) erfahren wir, wie wir unseren Code mit
Hilfe von Abstandsregeln lesbarer gestalten können.
Mit der Funktion matrix() können wir eine Matrix erstellen. Die Funktion sieht
(vereinfacht) so aus:
Die gewünschten Elemente der Matrix übergeben wir dem Parameter data. Mit nrow
bzw. ncol stellen wir die Anzahl der Zeilen (rows) bzw. Spalten (columns) ein. Mit
byrow steuern wir, wie die Elemente in die Matrix eingeordnet werden sollen; für
byrow = FALSE (Standard) werden sie spaltenweise eingefügt. Der Vektor data wird
beim Einordnen gegebenenfalls recycelt.
Es genügt, zwei der ersten drei Parameter zu spezifizieren, da sich der 3. automatisch
ergibt. Allerdings können wir (im Gegensatz zu seq()) auch alle drei spezifizieren,
sofern keine Widersprüche auftreten.
Beispiel: Generiere folgende Matrix:
" #
3 1 −1
X=
2 0 5
> # Möglichkeit 1: Elemente spaltenweise einfügen
> X <- matrix(data = c(3, 2, 1, 0, -1, 5), nrow = 2, ncol = 3)
> X
[,1] [,2] [,3]
[1,] 3 1 -1
[2,] 2 0 5
In beiden Versionen könnten wir nrow = 2 oder ncol = 3 weglassen. Die fehlende
Information würde R eigenständig ermitteln.
172 D Datenstrukturen
Matrizen können wir auch durch das „Aneinanderkleben“ von Vektoren generieren.
Hierzu machen wir Gebrauch von den beiden Funktionen rbind() (row bind) und
cbind() (column bind).
cbind(..., deparse.level = 1)
rbind(..., deparse.level = 1)
Auf diese Weise können wir beliebig viele Vektoren entlang der Zeile oder entlang
der Spalte zu einer Matrix zusammenkleben. Wir verdanken diesen angenehmen
Service jeweils dem Dreipunkteargument.
Betrachten wir dazu zunächst einfache Codebeispiele, bevor wir uns auf die Mini-
golfspieler stürzen.
> # Entlang der Spalte (column) > # Entlang der Zeile (row)
> cbind(x, y, z) > rbind(x, y, z)
x y z [,1] [,2] [,3] [,4]
[1,] 1 5 9 x 1 2 3 4
[2,] 2 6 10 y 5 6 7 8
[3,] 3 7 11 z 9 10 11 12
[4,] 4 8 12
Gegebenenfalls werden die Vektoren recycelt. Ist kein vollständiges Recycling mög-
lich, so gibt R eine Warnmeldung aus.
Der Vektor 1:3 (das 2. Argument) kann nicht vollständig recycelt werden.
Beispiel: Erzeuge eine Matrix, welche die Schlagzahlen der beiden Spieler enthält.
Die Zahlen sollen so angeordnet werden, dass die Ergebnisse der Spieler in den
Spalten stehen und die Bahnen in den Zeilen.
Die Vektoren schlaege1 und schlaege2 enthalten für Spieler 1 und 2 die Schlag-
zahlen. Diese sollen die Spalten der Matrix bilden, daher ist cbind() unsere Wahl.
Wir können auch Matrizen und Vektoren auf diese Art verknüpfen. Vektoren
werden gegebenenfalls einem Recycling zugeführt.
Beispiel: Ein dritter Spieler gesellt sich hinzu und sorgt für noch mehr Spannung!
Seine Schlagzahlen lauten: 1 2 3 7 4 1. Hänge die Schlagzahlen des 3. Spielers an
die Matrix Schlaege an.
Die Dimensionen einer Matrix können wir mit Hilfe der Funktionen nrow(),
ncol(), dim() und length() abfragen.
> Schlaege
schlaege1 schlaege2 schlaege3
[1,] 2 1 1
[2,] 3 3 2
[3,] 2 3 3
[4,] 4 6 7
[5,] 7 4 4
[6,] 3 2 1
Damit finden wir also heraus, dass 3 Spieler teilnehmen (Anzahl der Spalten) und
dass sie bis jetzt 6 Bahnen gespielt haben (Anzahl der Zeilen). Die Matrix Schlaege
hat insgesamt 6 · 3 = 18 Elemente.
Mit den Funktionen colnames() bzw. rownames() können wir die Spalten bzw.
Zeilen einer Matrix beschriften.
15 Matrizen 175
Beispiel: Wir wollen den Spalten und den Zeilen der Matrix Schlaege sprechende
Namen geben. Die Spalten sollen mit "Spieler1", "Spieler2", ... und die Zeilen
mit "Bahn1", "Bahn2", ... beschriftet werden.
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 7
Bahn5 7 4 4
Bahn6 3 2 1
Mit den Funktionen is.matrix() bzw. is.vector() fragen wir ab, ob ein Objekt
eine Matrix bzw. ein Vektor ist.
> schlaege1
[1] 2 3 2 4 7 3
> # schlaege1 ist ein Vektor. > # schlaege1 ist keine Matrix.
> is.vector(schlaege1) > is.matrix(schlaege1)
[1] TRUE [1] FALSE
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 7
Bahn5 7 4 4
Bahn6 3 2 1
> # Schlaege ist kein Vektor. > # Schlaege ist eine Matrix.
> is.vector(Schlaege) > is.matrix(Schlaege)
[1] FALSE [1] TRUE
Frage: Was passiert eigentlich mit einem Vektor, wenn wir ihn transponieren?
Beim Transponieren eines Vektors entsteht eine Matrix mit einer Zeile. Das ist unsere
Möglichkeit, Zeilenvektoren in R umzusetzen.
> # Transponiere einen Vektor > # Ein Zeilenvektor ist eine Matrix
> t(schlaege1) > is.matrix(t(schlaege1))
[,1] [,2] [,3] [,4] [,5] [,6] [1] TRUE
[1,] 2 3 2 4 7 3
Das Subsetting funktioniert (wie bei Vektoren) ebenfalls mit eckigen Klammern.
Wenn wir aus einer Matrix X bestimmte Zeilen und/oder Spalten selektieren
wollen, schreiben wir:
X[Zeilen, Spalten, drop = Wahrheitswert ]
Das funktioniert mit Indizes bzw. Names (15.2.1) oder mit logischen Bedingungen
(15.2.2). Mit dem Parameter drop steuern wir, ob die Matrixstruktur erhalten blei-
ben soll oder nicht. Das schauen wir uns in (15.2.1) an.
15 Matrizen 177
Wenn wir aus einer Matrix bestimmte Elemente selektieren wollen, so gehen wir
wie bei Vektoren vor:
X[Index ]
Diese Möglichkeit besprechen wir in (15.2.3). Zusätzlich können wir Elemente mit
Hilfe von Matrizen selektieren. Wie das geht, schauen wir uns in (15.2.4) an.
15.2.1 Selektion von Zeilen und Spalten mit Indizes und Names
Wenn wir erfahren wollen, wie viele Schläge die ersten beiden Spieler auf den Bahnen
1 bis 3 gebraucht haben, so schreiben wir:
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 7
Bahn5 7 4 4
Bahn6 3 2 1
Wollen wir ganze Zeilen (Spalten) selektieren, so lassen wir die Spaltenindizes
(Zeilenindizes) einfach leer. Wir betrachten einfache Codebeispiele.
Bis jetzt sind immer Matrizen herausgekommen, da wir jeweils mindestens zwei
Zeilen und mindestens zwei Spalten selektiert haben. Ist eine dieser beiden Gege-
benheiten nicht erfüllt, dann gibt uns R standardmäßig einen (ggf. beschrifteten)
Vektor zurück.
Es kommt also keine Matrix heraus. Der Umstand, dass manchmal ein Vektor her-
auskommt ist bemerkenswerter, als er aussieht! Wir sehen in (15.1.6), warum!
Frage: Wie verhindern wir, dass bei der Selektion die Matrixstruktur verloren geht?
Genau hier kommt der Parameter drop ins Spiel, den wir auf FALSE setzen.
> # Selektiere letzte Zeile - Bewahrung der Matrixstruktur mit drop = FALSE
> Schlaege[nrow(Schlaege), , drop = FALSE]
Spieler1 Spieler2 Spieler3
Bahn6 3 2 1
Gerne darfst du dich davon überzeugen, dass in beiden Codezeilen tatsächlich eine
Matrix herauskommt!
Wir erläutern, wie wir all jene Zeilen und/oder Spalten einer Matrix selektieren
können, die eine Bedingung erfüllen.
15 Matrizen 179
Beispiel: Selektiere alle Zeilen, in denen Spieler 3 mehr Schläge benötigt hat als
Spieler 2. Beantworte dann folgende Fragen:
1. Auf welchen Bahnen war dies der Fall?
2. Auf wie vielen Bahnen war dies der Fall?
Es werden nun bei der Selektion alle Zeilen selektiert, die in bool mit TRUE markiert
sind. Dank unserer coolen Beschriftungen erkennen wir schon auf den ersten Blick,
welche Bahnen betroffen sind bzw. welche Bahn betroffen ist. Explizite Abfrage:
> # 1.) Auf welchen Bahnen? (Name) > # 1.) Auf welchen Bahnen? (Index)
> rownames(Schlaege)[bool] > which(bool)
[1] "Bahn4" Bahn4
4
> # Alle Zeilen, in denen Spieler 1 mehr als 7 Schläge benötigt hat
> bool <- Schlaege[, "Spieler1"] > 7
> bool
Bahn1 Bahn2 Bahn3 Bahn4 Bahn5 Bahn6
FALSE FALSE FALSE FALSE FALSE FALSE
> dim(temp)
[1] 0 3
In diesem Fall entsteht also eine leere Matrix, in diesem Fall mit 0 Zeilen.
180 D Datenstrukturen
Für die Antwort auf die Frage haben wir zwei Möglichkeiten: Entweder wir bestim-
men die Anzahl der Zeilen oder zählen die Anzahl der TRUE.
Bei logischen Abfragen entsteht eine gleich große Matrix mit TRUE- und FALSE-
Einträgen. Wollen wir nun all jene Elemente selektieren, welche die Bedingung
erfüllen, so werden die entsprechenden Elemente spaltenweise entnommen und zu
einem Vektor zusammengekettet.
Beispiel: Selektiere alle Elemente von Schlaege, die größer oder gleich 5 sind.
Das Ergebnis der logischen Abfrage Schlaege >= 5 ist also eine Matrix, deren Di-
mension mit jener der Matrix Schlaege übereinstimmt. Bei der Selektion der Werte
entsteht aber ein Vektor:
Angenommen, wir wollen im Minigolfbeispiel wissen, wie viele Schläge Spieler 1 auf
Bahn 2 (2. Zeile, 1. Spalte), Spieler 2 auf Bahn 5 (5. Zeile, 2. Spalte) und Spieler 3
auf Bahn 1 (1. Zeile, 3. Spalte) benötigt hat. Klappt das mit obiger Methode?
Nein. Wir bekommen mehr, als wir haben wollen. Um zum gewünschten Ergebnis
zu kommen, benötigen wir eine andere Methode: Selektion mittels n × 2-Matrizen.
Die erste Spalte der Matrix Indizes enthält die gewünschten Zeilen und die zweite
Spalte die dazugehörigen Spalten. Das Ergebnis ist dasselbe wie in folgendem Code.
Alle Funktionen, die wir im Zuge der letzten Kapitel kennengelernt haben, lassen
sich grundsätzlich auch auf Matrizen anwenden. Das Ergebnis ist dann aber in den
meisten Fällen keine Matrix mehr.
Wenn wir beispielsweise die Summe bzw. den Mittelwert aller Elemente der Ma-
trix berechnen wollen, so können wir die bereits bekannten Funktionen sum() bzw.
mean() bemühen. Das Ergebnis ist ein einelementiger Vektor mit der Summe bzw.
dem Mittelwert.
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 7
Bahn5 7 4 4
Bahn6 3 2 1
Um einen Sieger nach 6 Bahnen küren zu können, müssen wir allerdings die Summe
der Schlagzahlen für jeden Spieler getrennt ermitteln.
Glücklicherweise gibt es vier echt coole Funktionen, welche in der Matrixwelt von
zentraler Bedeutung sind und mit deren Hilfe wir Aufgaben wie diese leicht bewälti-
gen! Sie sind in Tab. 15.1 aufgelistet. Alle vier Funktionen stellen uns übrigens den
Parameter na.rm (siehe (7.1)) zur Verfügung.
Tabelle 15.1: Funktionen für die Berechnung von Summen und Mittelwerten für
Zeilen und Spalten einer Matrix
Funktion Bedeutung
rowSums() berechnet Zeilensummen
colSums() berechnet Spaltensummen
rowMeans() berechnet Zeilenmittelwerte
colMeans() berechnet Spaltenmittelwerte
Beispiel: Bestimme, wie viele Schläge jeder Spieler in Summe gebraucht hat. Sor-
tiere die Schlagzahlen aufsteigend. Bestimme für jeden Spieler die Platzierung.
Mit colSums() keine große Sache! Bei der Beantwortung der anderen Fragen haben
wir die Chance, das Thema Sortieren (siehe (6.2)) zu wiederholen :-)
Spieler 3 hat gewonnen, gefolgt von Spieler 2 und Spieler 1. Spannendes Detail: Die
Funktion sort() sortiert die Beschriftungen mit.
Allerdings hat sort() einen Haken: Eine Mehrfachsortierung (vgl. (6.2.5)) ist nicht
möglich. Wenn wir weitere Sortierkriterien hinzufügen wollen, um etwaige Unent-
schieden bei gleichen Schlagsummen aufzulösen, müssen wir order() nehmen!
15 Matrizen 183
Wollen wir alle Informationen in einem Objekt vereinen, können wir so vorgehen:
Es ist verlockend, schlagsummen als letzte Zeile an die Matrix Schlaege anzuhängen.
Diese Idee ist im Allgemeinen jedoch nicht sinnvoll.
Frage: Warum ist es oft nicht sinnvoll, Zeilen- und Spaltensummen an eine Matrix
anzuhängen? Die Antwort folgt sogleich.
Wenn wir jetzt herausfinden wollen, auf welchen Bahnen Spieler 3 weniger Schläge
gebraucht hat als Spieler 1, so erleben wir eine ungute Überraschung:
> # Auf welchen Bahnen hat Spieler 3 weniger Schläge als Spieler 1 gebraucht?
> which(Temp[, "Spieler3"] < Temp[, "Spieler1"])
Bahn1 Bahn2 Bahn5 Bahn6 Summe
1 2 5 6 7
Unter anderem auch auf Bahn Nummer 7 (!?) mit dem Namen Summe (!?). Diese
Bahn gibt es jedoch nicht...
184 D Datenstrukturen
Die Summe wird mitgeschleppt und wenn wir lediglich die Bahnen betrachten woll-
ten, müssten wir jedes Mal ein kompliziertes und unleserliches Subsetting durchfüh-
ren um die Schlagsummen auszuschließen.
Hier ist es also tendenziell günstiger, die Schlagzahlen und die Schlagsummen ge-
trennt voneinander zu verwalten. Ausnahme: Wir wollen eine Ergebnistabelle für
Darstellungszwecke generieren.
Wir wiederholen ein wichtiges Konzept, das wir bereits von den Vektoren kennen.
Beispiel: Wie oft wurden bei jeder Bahn mehr als zwei Schläge benötigt?
> # Wie oft mehr als zwei Schläge bei jeder Bahn?
> rowSums(Schlaege > 2)
Bahn1 Bahn2 Bahn3 Bahn4 Bahn5 Bahn6
0 2 2 3 3 1
Auf Bahn 1 hat kein Spieler mehr als zwei Schläge benötigt, auf Bahn 2 zwei Spieler,
auf Bahn 3 zwei Spieler usw. Die Idee zusammengefasst in zwei Schritten:
1. Wir generieren mit Schlaege > 2 eine Matrix mit TRUE und FALSE Werten.
Ein TRUE gibt an, dass der entsprechende Spieler auf der entsprechenden Bahn
mehr als 2 Schläge benötigt hat.
2. Wir bilden die Zeilensummen dieser Matrix. Vor der Summenbildung werden
die Elemente konvertiert (vgl. (3.5)): TRUE in 1 und FALSE in 0.
15 Matrizen 185
Die Elemente einer Matrix X zeilenweise zu entnehmen ist also dasselbe, wie die
Elemente der transponierten Matrix t(X) spaltenweise zu entnehmen.
In (15.2.1) haben wir bereits erwähnt, dass wir bei der Selektion von Zeilen und
Spalten einer Matrix entweder eine Matrix oder einen Vektor erhalten.
Schauen wir uns anhand eines Beispiels an, warum es in der Praxis zu Problemen
kommen kann, wenn wir darauf nicht aufpassen.
Beispiel: Selektiere alle Zeilen, in denen der 1. Spieler 7 Schläge benötigt hat.
In obigem Code ist temp eine Matrix, daher können wir dim() problemlos anwenden.
Frage: Angenommen, wir hätten drop = FALSE weggelassen. Was wäre dann pas-
siert?
186 D Datenstrukturen
Wenn wir einen R-Code schreiben, so stecken wir oft Annahmen hinein, die nicht
immer erfüllt sind. So könnten wir etwa davon ausgehen, dass immer mindestens zwei
Zeilen selektiert werden und übersehen, dass es wie bei temp1 manchmal nur eine
Zeile ist. Wir erinnern uns an Regel 6 auf Seite 57: Achte auf implizite Annahmen!
Wenden wir in weiterer Folge Funktionen auf die vermeintliche Matrix an, so passen
sie unter Umständen nicht zum Vektor. Im besten Fall bekommen wir eine Fehler-
meldung. Im ungünstigsten Fall rechnet R weiter und produziert grausige inhaltliche
Fehler.
> # Gibt es eine Bahn, auf welcher der 1. Spieler 7 Schläge benötigt hat?
> nrow(temp) > 0 # klappt wunderbar!
[1] TRUE
> nrow(temp1) > 0 # leerer logischer Vektor, beantwortet Frage so nicht!
logical(0)
Die Fehlersuche gleicht oft der Suche nach der Nadel im Heuhaufen. drop = FALSE
kann dazu beitragen, den Heuhaufen deutlich kleiner zu machen ;-)
Begeisterte Minigolfspieler wissen: Nach 6 Fehlversuchen ist Schluss und es wird das
Ergebnis 7 eingetragen. Schauen wir mal, wie oft das der Fall war.
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 7
Bahn5 7 4 4
Bahn6 3 2 1
15 Matrizen 187
Nehmen wir einmal an, die Spieler hätten so lange weitergespielt, bis sie wirklich
den Ball eingelocht haben. Dann haben sie ggf. mehr als 7 Schläge benötigt.
Beispiel: Ersetze in Schlaege alle Vorkommen von 7 jeweils durch eine Zufallszahl
zwischen 8 und 11.
Der Parameter size ist an die benötigte Anzahl sum(bool7) gekoppelt, damit funk-
tioniert der Code allgemein, was immer einen sicheren und guten Eindruck vermit-
telt! Wir sehen auch, dass die Elemente spaltenweise ersetzt werden.
Eine interessante Möglichkeit, die ursprüngliche Matrix zurückzugewinnen, liefert
die Funktion pmin() (vgl. (8.2.1)), die auch für Matrizen funktioniert.
Die Matrix Schlaege wird dabei aber nicht überschrieben. Bevor wir die Matrix aber
auf den Ursprungszustand zurücksetzen, machen wir noch eine ähnliche Beobachtung
im Zusammenhang mit fehlenden Werten.
188 D Datenstrukturen
Da mehr als 7 Schläge nicht möglich (sprich ungültig) sind, könnten wir Werte größer
als 7 durch NA ersetzen.
Seit (14.3.1) wissen wir, dass die Abfrage auf Gleichheit mit NA mit is.na() funktio-
niert. Es ist extrem naheliegend und verlockend auszuprobieren, ob diese Funktion
auch für Matrizen funktioniert ;-).
Nachdem wir gesehen haben, dass is.na() auch für Matrizen funktioniert, stellen
wir den Ursprungszustand wieder her und ersetzen die fehlenden Werte durch 7.
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 7
Bahn5 7 4 4
Bahn6 3 2 1
Etwas komplexer wird es, wenn wir Ersetzungen lediglich in Teilbereichen ei-
ner Matrix durchführen wollen. Hierbei unterstützen uns die Funktionen col()
bzw. row(), die uns eine Matrix derselben Dimension liefern, mit Einträgen gleich
den jeweiligen Spalten- bzw. Zeilenindizes. Wir demonstrieren beide Funktionen an-
hand der Matrix X, die wir in (15.1.1) auf Seite 171 erstellt haben.
> X
[,1] [,2] [,3]
[1,] 3 1 -1
[2,] 2 0 5
15 Matrizen 189
Beispiel: Ersetze in der 2. und 3. Spalte der Matrix X alle positiven Einträge durch
-9. Alle anderen Zeilen sollen unangetastet bleiben.
Wir haben also zwei Bedingungen, die beide erfüllt sein müssen: Das Element ist
größer als 0 und befindet sich in der Spalte 2 oder 3.
Ein guter Zeitpunkt, um uns über das Spacing zu unterhalten. Darunter verste-
hen wir die Organisation von Leerzeichen im R-Code. Wir betrachten jetzt anhand
kleinerer Beispiele, wie Leerzeichen zu einer besseren Lesbarkeit beitragen.
Vor und nach binären Operatoren sollten Leerzeichen eingefügt werden. Ausnah-
me: Beim Sequenzoperator fügen wir keinen Leerraum ein.
Bei Funktionsaufrufen sollten die öffnenden und schließenden Klammern eng an-
liegen. Vor und nach dem "="-Zeichen sollten Leerzeichen stehen, ebenso wie nach
Beistrichen.
190 D Datenstrukturen
Im ersten Fall fehlen Leerzeichen vor und nach dem "=" sowie nach Beistrichen und
im zweiten Fall sollten die Leerzeichen vor bzw. nach den Klammern entfallen.
In allen vier nicht empfohlenen Varianten fehlen Leerzeichen nach den Beistrichen.
Wir fassen in Regel 13 die besprochenen Empfehlungen zusammen.
Setze Leerzeichen immer vor und nach binären Operatoren (Ausnahme ":"),
Zuweisungsoperatoren "<-", dem "="-Zeichen bei Parameterzuweisungen und
nach Beistrichen. Setze kein Leerzeichen vor Beistrichen.
15.8 Abschluss
• Wie generieren wir mit Hilfe der Funktionen matrix(), rbind() und cbind()
Matrizen? Wie bestimmen wir bei matrix(), ob die Elemente spaltenweise
oder zeilenweise einfügt werden? (15.1.1), (15.1.2)
• Wie bestimmen wir die Anzahl der Zeilen, Spalten und Elemente einer Matrix?
(15.1.3)
• Wie beschriften wir die Zeilen und Spalten einer Matrix? (15.1.4)
• Wie transponieren wir eine Matrix? (15.1.5)
• Wie erfragen wir, ob ein Objekt ein Vektor oder eine Matrix ist? (15.1.6)
• Wie selektieren wir bestimmte Zeilen oder Spalten aus einer Matrix? Welche
wichtige Rolle hat dabei die Option drop? Was passiert, wenn bei der Selektion
keine Zeile oder Spalte eine Bedingung erfüllt? (15.2.1), (15.2.2)
• Wie selektieren wir all jene Elemente einer Matrix, die eine Bedingung erfül-
len? Werden die Elemente dabei zeilen- oder spaltenweise entnommen? Wie
funktioniert Subsetting mit n × 2-Matrizen? (15.2.3), (15.2.4)
• Wie bestimmen wir Zeilen- und Spaltensummen bzw. Zeilen- und Spaltenmit-
telwerte einer Matrix? (15.3)
• Wie bestimmen wir für eine Matrix, wie viele Elemente jeder Zeile oder jeder
Spalte eine Bedingung erfüllen? (15.4)
• Wie können wir die Elemente einer Matrix spalten- oder zeilenweise entnehmen
und als Vektor anordnen? Warum ist bei der Selektion drop = FALSE oft so
bedeutsam? (15.5)
• Wie ersetzen wir all jene Elemente einer Matrix bzw. eines Teilbereichs einer
Matrix, welche eine Bedingung erfüllen, durch andere Elemente? Werden die
Elemente dabei zeilenweise oder spaltenweise ersetzt? (15.6)
Und wir beherzigen die gesammelten Spacingregeln aus (15.7.1), um unseren Code
lesbarer zu gestalten.
15.8.3 Ausblick
Hier stoßen wir also im Moment an unsere Grenzen! Aber keine Sorge: Wir werden
diese Grenzen überwinden, sobald wir in (19) über apply() sprechen. Generell gilt
aber: Probiere Dinge aus!
15.8.4 Übungen
1. Was wird bei den folgenden Codezeilen auf die Console gedruckt und warum?
> Pkt <- matrix(c(43, 45, 17, NA, 13, 32, NA, NA, 49, 15),
+ ncol = 2, byrow = TRUE)
> Pkt
[,1] [,2]
[1,] 43 45
[2,] 17 NA
[3,] 13 32
[4,] NA NA
[5,] 49 15
a) Beschrifte die Zeilen mit Stud1, Stud2, ... und die Spalten mit T1, T2, ...
b) Wie viele Studierende sind jeweils bei jedem Test nicht angetreten?
c) Welcher Test war der leichteste (höchster Punktedurchschnitt)? Schließe
dabei bei jedem Test getrennt die NA-Werte aus.
d) Wie 3c), aber nun schließe alle Studierenden aus, die mindestens einmal
gefehlt haben.
e) Ersetze alle fehlenden Werte in Pkt durch 0.
f) Gib die Namen all jener Studierenden (= Zeilenbeschriftungen von Pkt)
aus, die auf jeden Test mindestens 40 Punkte bekommen haben.
g) Sortiere die Zeilen absteigend nach der Zeilensumme.
h) Wie viel Prozent der Personen haben die Lehrveranstaltung bestanden?
4. Ein Kollege möchte mit R gerne ein Schachbrett als Matrix generieren:
> S
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8]
[1,] 1 2 1 2 1 2 1 2
[2,] 2 1 2 1 2 1 2 1
[3,] 1 2 1 2 1 2 1 2
[4,] 2 1 2 1 2 1 2 1
[5,] 1 2 1 2 1 2 1 2
[6,] 2 1 2 1 2 1 2 1
[7,] 1 2 1 2 1 2 1 2
[8,] 2 1 2 1 2 1 2 1
194 D Datenstrukturen
Das Ergebnis ist leider ernüchternd: eine Matrix mit lauter Einsern. Was
muss korrigiert werden, damit das Ergebnis passt?
b) Schlage eine weitere Möglichkeit vor, die obige Matrix zu erzeugen.
c) Beschrifte die Zeilen und Spalten des Schachbretts wie folgt: Die Spalten-
namen sollen von A bis H reichen und die Zeilennamen von 8 bis 1 (die
unterste/letzte Zeile hat den Namen 1).
> Tipps
[,1] [,2] [,3] [,4] [,5] [,6]
Tipp1 35 18 6 15 41 30
Tipp2 29 6 17 40 35 9
Tipp3 23 38 8 1 39 26
Hinweis: Falls du nicht weiterkommst, schau dir der Reihe nach folgende
Kapitel an: (6.2.5), (9.4), (4.4), (15.1.1)
Wenn du es jetzt noch mit den bisher gelernten Mitteln zusätzlich schaffst, die
Zahlen innerhalb eines jeden Tipps aufsteigend zu sortieren, dann ziehen wir
den Hut vor dir!
Vielleicht bist du bereits auf die in Statistikkreisen legendäre Formel des Kleinst-
quadrateschätzers gestoßen:
−1
β̂ = X t X X ty (16.1)
Dabei ist X eine n × k-Matrix und y ein Vektor mit n Elementen. Ebenso klassisch
ist die Formel zur Lösung von linearen Gleichungssystemen:
x = A−1 b (16.2)
Bevor wir uns in (16.3.1) und (16.3.2) der Auswertung von (16.1) und (16.2) widmen,
lernen wir davor in (16.2) die notwendigen Techniken dafür kennen. Und nochmals
davor tauchen wir in (16.1) in die Welt der Diagonalmatrizen ein.
Abschließend erfährst du im Ausblick (16.4.2), mit welchen Funktionen du Zufalls-
zahlen aus einer multivariaten Normalverteilung ziehen, QR-Zerlegungen und Sin-
gulärwertzerlegungen durchführen sowie Eigenwerte und Eigenvektoren berechnen
kannst.
Besonders spannend sind auch die Übungsaufgaben in (16.4.3). Wir schauen uns
dort unter anderem Dreiecksmatrizen und Rundungsfehler bei der Invertierung an.
Die Funktion diag() ist extrem vielseitig. Folgende Dinge können wir mit ihr an-
stellen:
Funktionsaufruf:
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_16
196 D Datenstrukturen
Beispiel: Generiere aus der Matrix D eine Diagonalmatrix bestehend aus den Dia-
gonalelementen.
Mit diag() keine große Sache!
Im inneren Aufruf von diag() werden die Diagonalelemente aus D selektiert und
daraufhin als Vektor in das äußere diag() eingesetzt.
1 Skalare gibt es in R nicht. Vielmehr handelt es sich um Vektoren mit einem Element.
16 Rechnen mit Matrizen und Lineare Algebra 197
Tabelle 16.1: Wichtige Aufgaben und Funktionen beim Rechnen mit Matrizen
Aufgabe Ausführung
Elementweise Multiplikation A * B
Matrixmultiplikation, A · B A %*% B
Elementweise Division A / B
Inverse einer Matrix, A−1 solve(A)
Transponieren, At t(A)
At · B t(A) %*% B; bzw. crossprod(A, B)
At · A t(A) %*% A; bzw. crossprod(A)
A · Bt A %*% t(B); bzw. tcrossprod(A, B)
A · At A %*% t(A); bzw. tcrossprod(A)
Determinante von A, det(A) det(A)
Zeilen- und Spaltensummen von A rowSums(A) und colSums(A)
Zeilen- und Spaltenmittelwerte von A rowMeans(A) und colMeans(A)
Wir schauen uns die Funktionen kurz mit folgenden beiden Beispielmatrizen an:
> A <- cbind(c(1, 1), c(3, 2)) > B <- cbind(c(-2, 2), c(2, -1))
> A > B
[,1] [,2] [,1] [,2]
[1,] 1 3 [1,] -2 2
[2,] 1 2 [2,] 2 -1
Die beiden Matrizen müssen von ihren Dimensionen her zusammenpassen, sonst gibt
R eine Fehlermeldung aus. Es erfolgt in diesem Fall kein Recycling. Verknüpfen wir
hingegen eine Matrix mit einem Vektor, so erfolgt ein Recycling. Im Code ganz rechts
wird der Vektor c(2, 3) auf dieselbe Länge wie A (4) gebracht. Der recycelte Vektor
wird dann spaltenweise mit den Elementen von A multipliziert, sodass im Endeffekt
beide Spalten von A mit c(2, 3) multipliziert werden.
198 D Datenstrukturen
Mit dem Operator %*% führen wir Matrizenmultiplikationen durch. Für die Ausdrü-
cke At · B bzw. A · B t stehen uns die Funktionen crossprod() bzw. tcrossprod()
zur Verfügung.
Dabei ist die Berechnung mittels crossprod(A, B) und tcrossprod(A, B) ein we-
nig effizienter als t(A) %*% B und A %*% t(B).
Die Multiplikation einer Matrix mit ihrer Inversen ergibt die Einheitsmatrix. Bei
der Invertierung können jedoch Rundungsfehler auftreten (vgl. (13)), sodass der
Ausdruck A · A−1 nicht immer exakt die Einheitsmatrix ergibt. Wir schauen uns
dieses Phänomen in Übung 2 auf Seite 203 an.
Mit solve() können wir auch lineare Gleichungssysteme lösen. Wie das geht, schau-
en wir uns in (16.3.2) an.
Mit der Funktion det() berechnen wir die Determinante einer Matrix.
Jetzt haben wir das nötige Rüstzeug, um uns zwei coole Beispiele anzusehen!
16 Rechnen mit Matrizen und Lineare Algebra 199
Wir möchten das Gewicht durch die Größe mit Hilfe eines einfachen linearen
Regressionsmodells prognostizieren. Die Größe entspricht der unabhängigen Va-
riablen x, das Gewicht der abhängigen Variablen y. Statistische Hintergründe und
mathematische Details blenden wir aus. Wir sind vor allem daran interessiert, die
Funktionen aus (16.2) anzuwenden und führen daher nur kurz ein paar Formeln ein.
Für den i. Datenpunkt lautet die Gleichung des einfachen linearen Modells wie folgt:
yi = a + b · xi + ui (16.3)
Das Interzept bzw. die Konstante wird durch a, die Steigung durch b geschätzt
und ui ist ein (per Annahme) normalverteilter Zufallsfehler mit Erwartungswert 0
und Varianz σ 2 . Wir übersetzen das Modell wie folgt in Matrixschreibweise:
y = Xβ + u (16.4)
Die Matrix X besteht aus zwei Spalten: Die erste Spalte enthält nur Einsen (für
das Interzept), die zweite die Größen der Damen (für die Steigung). Der Vektor
β = (a, b)t enthält die zu schätzenden Parameter des Modells.
Unser Ziel ist es, jenes β zu finden, sodass der folgende Ausdruck minimiert wird:
2 2
kuk = ky − Xβk (16.5)
Man kann zeigen, dass der OLS-Schätzer (Ordinary Least Squares)
−1 t
β̂ = X t X X y (16.6)
dieser Anforderung gerecht wird. Die Prognose des Gewichts erfolgt via
ŷ = X β̂ (16.7)
Klarerweise ist es fast nie möglich, den Vektor y exakt durch ŷ zu prognostizieren.
Es entsteht ein Prognosefehler (Residuum), den wir wie folgt berechnen:
û = y − ŷ (16.8)
> # Die Daten (verwenden hier eine Doppelzuweisung)
> x <- groesse <- c(184, 168, 160, 168, 170, 168, 163, 170)
> y <- gewicht <- c(65, 70, 48, 52, 53, 56, 56, 59)
Bei der Dateneingabe haben wir eine Doppelzuweisung verwendet. Generell ist sie
nicht empfehlenswert, da sie die Lesbarkeit des Codes erschwert. Aber in die-
sem Fall passt sie ganz gut um zu verdeutlichen, was die unabhängige und was die
abhängige Variable ist.
200 D Datenstrukturen
Die Grafiken in Abb. 16.1 zeigen, was wir soeben ausgerechnet haben. Im Ausblick
(16.4.2) findest du den zugrundeliegenden Code dazu.
70
65
65
Gewicht im kg
Gewicht im kg
60
60
55
55
Regressionsgerade
50
50
Residuen
160 165 170 175 180 160 165 170 175 180
Körpergröße in cm Körpergröße in cm
(eindeutig) nach x lösen. Wenn die Matrix A invertierbar ist, können wir die ein-
deutige Lösung analytisch bestimmen.
x = A−1 b (16.9)
Tipp: Wenn wir ?solve eingeben, stellen wir fest, dass wir so schneller ans Ziel
kommen:
16.4 Abschluss
16.4.2 Ausblick
Die QR-Zerlegung und Singulärwertzerlegung einer Matrix können wir mit den Funk-
tionen qr() und svd() durchführen. Auf Seite 214 schauen wir uns die QR-Zerlegung
in einer Übung an. Eigenwerte und Eigenvektoren berechnest du mit der Funktion
eigen(). Diese Funktion schauen wir uns in (17.1) an.
Alle gerade genannten Funktionen geben uns eine Liste zurück. Wie wir auf die
Bestandteile dieser Listen zugreifen, besprechen wir in (17).
Wenn du Zufallszahlen aus einer multivariaten Normalverteilung ziehen möchtest,
dann steht dir die Funktion mvrnorm() aus dem Paket MASS zur Verfügung. Wir
verweisen dabei auf die R-Hilfe.
In (38) besprechen wir lineare Modelle mit einer ausgefeilteren Methode (lm()). In
(34) und (35) lernen wir Grafiken zu erstellen. Falls du zu den Ungeduldigen zählst,
dann darfst du jetzt schon folgende Codezeilen ausführen:
# Streudiagramm erstellen
plot(x, y, main = "Regressionsbeispiel: Gewicht vs. Größe",
xlab = "Körpergröße in cm", ylab = "Gewicht im kg")
# Legende einzeichnen
legend(max(x), min(y), xjust = 1, yjust = 0,
legend = c("Regressionsgerade", "Residuen"), col = c(2,4), lwd = 1)
16.4.3 Übungen
a) Ist die Matrix D quadratisch? Erstelle eine Abfrage, die uns diese Frage
mit TRUE oder FALSE beantwortet.
b) Die Spur einer quadratischen Matrix ist die Summe ihrer Diagonalele-
mente. Berechne die Spur der Matrix D.
d) Bei der oberen Dreiecksmatrix sind alle Elemente unterhalb der Hauptdia-
gonalen gleich 0. Erstelle aus der der Matrix D eine obere Dreiecksmatrix.
17 Listen
Es gibt in R eine Funktion, welche uns die Eigenwerte und Eigenvektoren von Matri-
zen berechnet. Die Eigenwerte werden als Vektor und die Eigenvektoren als Matrix
verwaltet. Funktionen in R können aber immer nur ein Objekt zurückgeben.
Wie wäre es, wenn wir eine Möglichkeit hätten, die beiden Objekte in ein ande-
res Objekt zu stecken, sodass die Rückgabe beider Objekte dadurch möglich wird?
Genau hier kommen Listen ins Spiel!
Eine Liste ist eine Art Container, in den wir den Vektor und die Matrix hineinpacken
können. Es ist wie beim Einkaufen: 6 Packungen Joghurt, 10 Liter Saft und 20
Äpfel mit bloßen Händen zu tragen ist kaum möglich. Wenn wir sie jedoch in einen
Rucksack, eine Tüte oder ein Sackerl packen, dann geht es wunderbar! Wie heißt
Rucksack, Tüte bzw. Sackerl auf R? Die Antwort lautet: Liste!
Wir betrachten in diesem Kapitel die folgende Beispielmatrix X:
• Wie verschaffen wir uns einen Überblick über die erstellte Liste? (17.2)
• Wie greifen wir auf die Eigenwerte und Eigenvektoren zu? (17.3)
• Wie fügen wir der Liste die Dimension, Spur und Determinante von X hinzu?
Wie löschen wir aus der Liste nicht benötigte Elemente? (17.6)
Daneben schauen wir uns in (17.4), (17.5) und (17.7) weitere spannende Aspekte im
Zusammenhang mit Listen an! Packen wir es also sprichwörtlich an!
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_17
17 Listen 205
Die Funktion eigen() berechnet uns die Eigenwerte und Eigenvektoren einer
übergebenen Matrix. Was per Hand mühsam ist, geht mit R ganz schnell.
Beispiel: Berechne die Eigenwerte und Eigenvektoren der Matrix X.
Voilà! Eigenwerte und Eigenvektoren in einer Liste! Es können dabei unter Umstän-
den komplexe Zahlen herauskommen (siehe (11.5)).
Die Spalten der Matrix eigen$vectors sind normiert. Sei x eine beliebige Spalte
der Matrix eigen$vectors, dann gilt also:
v
u n
uX
kxk = t x2i = 1
i=1
Das gilt jedoch nur in der Theorie. In der Praxis kommt für kxk fast immer nicht
exakt 1 heraus, da bei der Normierung Rundungsfehler auftreten (vgl. (13)).
Tipp: In der Hilfe von eigen() finden sich einige spannende Funktionen zur Zerle-
gung von Matrizen (Singulärwertzerlegung, QR-Zerlegung und Choleskyzerlegung).
Wir führen mit der Liste X.eigen aus (17.1) einige Konzepte ein. Mit der Funktion
is.list() fragen wir ab, ob das Objekt eine Liste ist.
Ja, X.eigen ist eine Liste. Möchten wir mehr über die Eigenschaften der Listen-
elemente erfahren, so nehmen wir str() (structure).
206 D Datenstrukturen
Wir erfahren, dass die Liste zwei Elemente hat. Sie heißen values und vectors.
Beide sind numerisch und an den Klammern [1:3] bzw. [1:3, 1:3] erkennen wir,
dass es sich um einen dreielementigen Vektor bzw. um eine 3 × 3-Matrix handelt.
Die Anzahl der Elemente der Liste erfragen wir wie gehabt mit length().
> length(X.eigen)
[1] 2
17.3 Subsetting und Names – "$", "[ ]", "[[ ]]", names()
Mit names() können wir wie gehabt auf Beschriftungen von Listen zugreifen.
Wiederum greifen wir zwecks Demonstration auf die Liste X.eigen aus (17.1) zu.
Damit ist es uns auch möglich, Elemente mit Namen anzusprechen. Die Zuweisung
und das Löschen von Beschriftungen funktionieren wie bei Vektoren via
names(Liste ) <- Beschriftung und names(Liste ) <- NULL
Beim Subsetting müssen wir aufpassen! Es macht einen Unterschied, ob wir auf
einzelne Elemente einer Liste zugreifen wollen oder auf Teilbereiche.
Beispiel: Bestimme den größten Eigenwert der Matrix X.
> max(X.val)
Fehler in max(X.val) : ungültiger ’type’ (list) des Argumentes
Mit der gewohnten Technik des Subsettings kommen wir nicht zum Ziel. Der Grund:
Mit einfachen eckigen Klammern greifen wir auf Teilbereiche einer Liste zu. Es
ist, als nähmen wir ein Joghurt aus unserem Rucksack heraus, packten es in einen
anderen Rucksack ein und fragten uns: Wo ist unser Joghurt? So weiß die Funktion
max() nicht, was sie mit der Liste X.val anfangen soll.
17 Listen 207
Frage: Was können wir tun, um auf einzelne Listenelemente zuzugreifen? Eine
Verdopplung der Klammern führt zum Ziel.
> max(X.val)
[1] 6
Jetzt hat es funktioniert! Der größte Eigenwert lautet also 6. Alternativ können wir
auch mit dem "$"-Operator arbeiten.
Im rechten Code sehen wir, dass wir beim Zugriff mit dem "$"-Operator auch ein-
deutige Abkürzungen für den Namen nehmen können. val ist eine eindeutige
Abkürzung für values. v hingegen nicht, NULL ist die Folge.
Wollen wir auf die ersten zwei Elemente unserer Liste zugreifen, dann führt folgender
Code nicht zum Ziel:
Der Grund: Listen sind rekursiv. Der Code wird intern in X.eigen[[1]][[2]] um-
gewandelt, womit der Output 3 erklärt ist. In (17.4) auf Seite 209 erläutern wir den
Sinn dahinter anhand eines anderen Beispiels.
Frage: Was schreiben wir, wenn wir auf mehr als ein Element bzw. auf einen
Teilbereich einer Liste zugreifen wollen?
Wollen wir also auf mehrere Elemente einer Liste zugreifen, so bleibt uns nur die
Variante mit den einfachen eckigen Klammern.
Der Zugriff auf einzelne Elemente einer Liste funktioniert mit doppelten
eckigen Klammern "[[ ]]" oder dem "$-Operator. Beim "$-Operator können
wir nur mit Beschriftungen (Names) auf die Elemente zugreifen, dafür dürfen
wir auch eindeutige Abkürzungen für die Beschriftung nehmen.
Der Zugriff auf Teilbereiche einer Liste funktioniert nur mit einfachen eckigen
Klammern "[ ]". Dabei wird immer eine Liste zurückgegeben.
Jetzt schauen wir uns noch an, wie wir Listenelemente umordnen können.
Beispiel: Sortiere die Elemente der Liste X.eigen alphabetisch nach ihrer Beschrif-
tung. Und zwar einmal von A bis Z und einmal von Z bis A.
Eine unbefüllte Liste (mit NULL-Einträgen) erstellen wir mit der Funktion vector().
Mit dieser Funktion können wir übrigens auch Vektoren jeden Modes erzeugen.
vector(mode = "logical", length = 0)
Für mode übergeben wir entweder einen atomaren Datentyp ("logical", "numeric",
"character" etc.) oder "list". Mit dem Parameter length geben wir die Anzahl
der Elemente an.
Die Liste besteht aus lauter NULLs, also aus Objekten ohne jegliche Eigenschaft. Das
NULL-Objekt haben wir schon in (14.2) kennengelernt.
17 Listen 209
Mit der Funktion list() können wir beliebig viele Objekte in eine Liste packen.
Diese Funktion ist c() recht ähnlich.
Beispiel: Erstelle eine Liste, welche für die Matrix X die Berechnungen von eigen(),
die Dimension und den Vektor mit den Diagonalelementen enthält.
Das Objekt X.liste besteht aus 3 Elementen: eigen, dim und diag. Der rechte Code
unterscheiden sich vom linken nur durch ein winziges Detail, nämlich unclass(). Wir
werden in (26.2) genau auf diese Funktion eingehen. Nur so viel an dieser Stelle:
unclass() entfernt die Informationen und Attribute, die eigen() dem Ergebnisob-
jekt hinzufügt. Dadurch ist für uns leichter sichtbar, dass die Liste verschachtelt
ist: Das erste Element der Liste X.liste (eigen) ist wieder eine Liste bestehend aus
den Elementen values und vectors.
Wollen wir etwa die Eigenwerte selektieren, so greifen wir zunächst auf das Element
eigen zu und anschließend auf values.
> X.liste[["eigen"]][["values"]]
[1] 6.0 3.0 0.5
210 D Datenstrukturen
Die Welt von R ist manchmal etwas eigentümlich und birgt manche Tücken. Unsere
Aufgabe ist es, diese Tücken zu erkennen, damit wir die Welt heil durchschreiten.
Erfragen wir einmal mit is.vector(), ob X.liste aus (17.4) ein Vektor ist.
Die Abfrage ergibt TRUE, das Objekt X.liste ist also (auch) ein Vektor!
Wollen wir bei einem Objekt abfragen, ob es wirklich ein Vektor ist, so gibt es
folgende Möglichkeiten.
Um echte Vektoren herauszufiltern, fügen wir die zusätzliche Bedingung hinzu, dass
das Objekt auch atomar ist.
Bei der zweiten Technik fragen wir nicht allgemein nach Vektoren, sondern nach
einem Vektor mit einem bestimmten Mode (hier "numeric").
17 Listen 211
Die Funktion c() verkettet auch Listen, nicht nur Vektoren. Zusätzlich können
wir neue Listenelemente auch mit den Methoden aus (17.3) hinten anhängen.
Betrachten wir Beispiele mit dem Objekt X.liste aus (17.1) (Seite 209).
Beispiel: Hänge an X.liste noch die Spur sowie die Determinante von X an.
Das Ersetzen von Elementen funktioniert wie bei Vektoren (vgl. (3.6)). Wollen
wir dabei einem Listenelement NULL zuweisen, so weisen wir list(NULL) zu.
Beispiel: Ersetze in X.liste das Element spur durch NULL. Ersetze daraufhin das
Element spur durch den Vektor 1:3.
Mit der Funktion unlist() können wir eine Liste in einen Vektor überführen.
Dabei werden die Listenelemente der Reihe nach entnommen und in einen Vektor
gesteckt.
Die Namen der Liste werden (so vorhanden) mitgenommen und gegebenenfalls durch-
nummeriert. Zumindest solange wir nicht am Parameter use.names herumwerken.
Aufpassen müssen wir, wenn wir in der Liste unterschiedliche Modes verwalten!
Die Liste liste verwaltet also ein numerisches und ein logisches Objekt.
17 Listen 213
Da Vektoren nur einen Mode haben können, wird bei Bedarf bei unlist() konver-
tiert (vgl. (11)). Hier wird TRUE in 1 sowie FALSE in 0 konvertiert.
17.8 Abschluss
17.8.2 Ausblick
Listen werden uns noch häufig begegnen. Nicht erst dann, wenn wir uns in (29)
darüber freuen, dass wir beim Schreiben eigener Funktionen mehrere Objekte darin
verpacken können.
214 D Datenstrukturen
Listen treten auch bei vielen Stringfunktionen auf, die wir in (22) besprechen. In (18)
lernen wir, wie wir eine Funktion auf alle Elemente einer Liste anwenden können.
Und in (20) lernen wir mit dem Dataframe einen Spezialfall der Liste kennen, der
in der Statistik von sehr großer Relevanz ist.
17.8.3 Übungen
1. Wir wollen mit der QR-Zerlegung den Rang der Matrix X aus der Einleitung
des Kapitels bestimmen.
a) Bestimme mit Hilfe der Funktion qr() den Rang der Matrix X.
b) Erstelle eine beschriftete Liste mit der Matrix X und ihrem Rang.
c) Füge in diese Liste an 2. Stelle noch die Dimension hinzu.
2. Eine Pizzeria bietet 4 Pizzen an. Die Namen und Zutaten der Pizzen sind:
a) Lese aus dem Output von str(pizzen) ab, wie viele Pizzen die Pizzeria
anbietet und wie viele Zutaten jede Pizza hat.
b) Wie viele unterscheidbare Zutaten braucht die Pizzeria, um alle oben
genannten Pizzen herzustellen? Welche Zutaten sind das?
c) Füge eine weitere Pizza der Liste hinzu, die aus Tomaten, Käse und zwei
weiteren (anderen) zufällig gewählten Zutaten aus 2b) besteht. Wähle
einen kreativen Namen für deine Pizza ;-)
d) Sortiere die Liste alphabetisch nach den Namen der Pizza.
e) Lösche die Pizza mit den wenigsten Zutaten aus der Liste. Wie kann uns
dabei die Funktion lengths() (mit „s“ am Ende) helfen?
18 Wiederholte Funktionsanwendung bei Listen 215
Auf Seite 214 haben wir in Übungsaufgabe 2 die Speisekarte einer Pizzeria in Form
einer Liste erstellt:2
> pizzen
$Margherita
[1] "Tomaten" "Kaese"
$Cardinale
[1] "Tomaten" "Kaese" "Schinken"
$‘San Romio‘
[1] "Tomaten" "Kaese" "Schinken" "Salami" "Mais"
$Provinciale
[1] "Tomaten" "Kaese" "Schinken" "Mais" "Pfefferoni"
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_18
216 D Datenstrukturen
... und elegant? Nein! Bei diesem Code, der Regel 4 des allgemeinen Programmierens
auf Seite 31 mit Füßen tritt, vergeht uns der Appetit auf Pizza!
Es müsste eine Möglichkeit geben, mit der wir bequem die Funktion length() auf
jedes Element der Liste anwenden können. Hier kommt die frohe Botschaft:
Genau das ist mit Hilfe der Funktionen lapply() und sapply() möglich!
Dem Parameter X bzw. FUN übergeben wir das Objekt (oft eine Liste) bzw. die
Funktion, die sodann auf jedes Element von X angewendet wird. Dem Dreipunkte-
argument (...) können wir zusätzliche Parameter übergeben, die in FUN eingesetzt
werden. Wie das funktioniert schauen wir uns in (18.2) in Ruhe an.
Beispiel: Bestimme die Anzahl der Zutaten jeder Pizza in pizzen.
> # Bestimme die Länge von jedem Listenelement (Vektor) von pizza
> sapply(pizzen, FUN = length)
Margherita Cardinale San Romio Provinciale
2 3 5 5
sapply() greift sich der Reihe nach die Elemente der Liste pizzen heraus und wendet
die Funktion length() darauf an. Praktisch eine 1-zu-1 Umsetzung unseres ersten
Codes, nur deutlich besser!
Frage: Wodurch unterscheiden sich sapply() und lapply()?
Beide Funktionen tun im Prinzip dasselbe, sapply() ist jedoch flexibler. Während
lapply() das Ergebnis immer in Form einer Liste zurückgibt, versucht sapply()
die Datenstruktur des Rückgabeobjekts nach Möglichkeit zu vereinfachen.
In obigem Beispiel wird das Ergebnis zu einem (beschrifteten) Vektor vereinfacht.
Mit der Einstellung simplify = FALSE können wir die Vereinfachung der Da-
tenstruktur unterbinden.
18 Wiederholte Funktionsanwendung bei Listen 217
Wie die Vereinfachung der Datenstruktur im Detail funktioniert, schauen wir uns in
(18.4) genau an. Im Moment reicht es aus zu wissen, dass sapply() das Ergebnis
zu einem Vektor vereinfacht, weil length() einen Skalar zurückgibt.
Oft ist es bei sapply() und lapply() notwendig, der Funktion FUN weitere Para-
meter zu übergeben. Stürzen wir uns gleich in ein Beispiel!
Beispiel: Sortiere die Zutaten jeder Pizza in pizzen alphabetisch. Und zwar einmal
aufsteigend (von A bis Z) und einmal absteigend (von Z bis A).
Wir zeigen den Output aus platztechnischen Gründen nur für die ersten beiden
Pizzen. Um die Zutaten jeder Pizza absteigend zu sortieren, müssen wir die Funktion
sort() mit der Option decreasing = TRUE auf jedes Listenelement anwenden, was
wir im rechten Code tun. Intern entspricht der rechte Code also folgenden Aufrufen
(gezeigt für die ersten beiden Elemente von pizzen):
Die einzelnen Elemente von X werden der Funktion FUN immer unbenannt als ers-
tes Argument übergeben. Alle Objekte, die wir dem Dreipunkteargument überge-
ben, werden dann der Funktion FUN als weitere Objekte übergeben.
Bei der Ausführung von FUN kommen bei der Parameterzuweisung dieselben Re-
geln zur Anwendung, wie wir sie in (4.3) gelernt haben. Wenn wir einen Blick auf
sort() werfen, so stellen wir fest, dass wir in obigem rechten Code auch TRUE statt
decreasing = TRUE hätten schreiben können.
Wir können steuern, für welchen Parameter von FUN die einzelnen Elemente von X
eingesetzt werden. Schauen wir uns ein kleines Beispiel an.
Den Regeln der Parameterzuweisung (vgl. (4.3)) folgend wird x = 1:3 dem Para-
meter x von sort() übergeben, da wir x exakt benannt haben. Dann wird das
unbenannte Objekt TRUE bzw. FALSE dem nächsten, noch nicht spezifizierten Pa-
rameter von sort() zugewiesen, nämlich decreasing.
Frage: Was passiert, wenn wir 1:3 unbenannt übergeben?
Sowohl TRUE bzw. FALSE als auch 1:3 werden unbenannt übergeben. Das heißt, dass
in sort() das Objekt TRUE bzw. FALSE für x und 1:3 für decreasing eingesetzt wird.
sort() kommt damit nicht zurecht, da decreasing einen Wahrheitswert verlangt.
18 Wiederholte Funktionsanwendung bei Listen 219
Welche Pizzen sind mit Schinken belegt? Bevor wir die Antwort generieren, wollen
wir einen echt coolen Trick aus der R-Trickkiste auspacken.
> pizzen[["Provinciale"]]
[1] "Tomaten" "Kaese" "Schinken" "Mais" "Pfefferoni"
Wir schreiben den binären Operator in Anführungszeichen und können einen Funkti-
onsaufruf mit zwei Parametern vollziehen! Die Parameternamen entnehmen wir der
R-Hilfe (?"=="). Somit erfragen wir, welche Elemente "Schinken" gleichen. Dersel-
be Trick funktioniert auch für andere Operatoren, Tab. 18.1 verschafft uns einen
Überblick über gängige Operatoren.
Wir legen fest, dass wir jeden Vektor von pizzen elementweise mit "Schinken"
vergleichen wollen. Ein TRUE gibt an, dass eine Übereinstimmung gefunden wurde.
220 D Datenstrukturen
> names(bool.res)[bool.res]
[1] "Cardinale" "San Romio" "Provinciale"
Jetzt überlegen wir uns folgendes: Wir sind an jenen Pizzen interessiert, die sowohl
Schinken als auch Mais enthalten. Solche Pizzen erkennen wir daran, dass der
entsprechende logische Vektor in bool zwei TRUE enthält.
Bevor du weiterliest: Versuche die Aufgabe selbstständig zu vollenden!
Die Idee ist naheliegend: Wir bestimmen in bool für jede Komponente die Anzahl
der TRUE. Ist diese Anzahl gleich 2, so enthält die entsprechende Pizza beide Zutaten.
Wie zählen wir die TRUE? Korrekt, mit sum().
> names(bool.res)[bool.sum == 2]
[1] "San Romio" "Provinciale"
18 Wiederholte Funktionsanwendung bei Listen 221
Die Funktion sapply() wählt (bei simplify = TRUE) die Datenstruktur des Ergeb-
nisobjektes wie folgt:
• matrix, wenn FUN für alle Elemente einen gleichlangen Vektor mit mindestens
2 Elementen zurückgibt.
• list, wenn weder ein Vektor noch eine Matrix generierbar ist.
> sapply(liste, max) # Vektor: max() gibt immer einen Skalar zurück.
A B
4 3
> sapply(liste, rev) # Liste: rev() erzeugt hier ungleich lange Vektoren.
$A
[1] 4 3 2 1
$B
[1] 3 2 1
Die Simplifizierung der Datenstruktur hat Vor- und Nachteile: Der größte Vorteil ist,
dass wir mit einfacheren Datenstrukturen in der Regel effizienter arbeiten können.
Der größte Nachteil ist, dass uns manchmal nicht klar ist, in welche Datenstruk-
tur das Ergebnis vereinfacht wird. Das kann zu unangenehmen Inkompatibilitäten
führen; das Phänomen ist das gleiche wie in (15.2) (drop = FALSE).
Tipp: Wenn bereits vor der Codeausführung klar ist, welche Datenstruktur
herauskommen muss, so vereinfachen wir die Datenstruktur, andernfalls nicht.
Bei max() ist klar, dass immer ein Vektor herauskommen muss. Bei range() kommt
immer eine Matrix (mit zwei Zeilen) heraus. Bei quantile() wüssten wir, dass ent-
weder ein Vektor oder eine Matrix herauskommt, je nachdem, ob wir dem Parameter
probs einen Vektor mit einem oder mehreren Elementen übergeben. Bei rev() kann
jede Datenstruktur herauskommen, je nachdem, wie das Objekt liste aussieht.
222 D Datenstrukturen
Abschließend noch die Bemerkung, dass wir sapply() und lapply() grundsätzlich
auch auf Vektoren und Matrizen anwenden können.
Jeder Eintrag eines Vektors bzw. einer Matrix ist gleichzeitig sein eigenes Element!
Wenden wir also sapply() oder lapply() auf einen Vektor oder eine Matrix an,
dann wird die Funktion auf jeden Eintrag angewendet.
Bei Vektoren gibt es sinnvolle Anwendungen, wenngleich es oft (deutlich) effizien-
tere Alternativen gibt.
Hier wird die Summe eines jeden Eintrages der Matrix M berechnet. Das Ergebnis
ist ein Vektor, weil die Funktion sum() stets einen Skalar zurückgibt.
An diejenigen, die sich Spaltensummen erhofft haben: Es gibt die Funktion colSums()
(vgl. (15.3)) ;-)
18 Wiederholte Funktionsanwendung bei Listen 223
18.6 Abschluss
• Wie wenden wir eine Funktion auf jedes Element eines Objektes an? Was ist
der Unterschied zwischen lapply() und sapply()? Was steuert der Parameter
simplify bei sapply()? (18.1)
• Wie funktioniert die Parameterübergabe an FUN innerhalb von sapply() und
lapply()? Wie werden dabei die Elemente des Objektes X übergeben? (18.2)
• Wie können wir Operatoren als Funktionen anwenden? Wie setzen wir (binäre)
Operatoren innerhalb von lapply() und sapply() ein? (18.3)
• Wie wählt sapply() bei simplify = TRUE die Datenstruktur des Ergebnis-
objekts aus? Ist immer klar, wann ein Vektor, eine Matrix oder eine Liste
herauskommt? (18.4)
• Was passiert, wenn wir lapply() und sapply() auf Vektoren oder Matrizen
anwenden? (18.5)
18.6.2 Ausblick
Die *apply()-Funktionsfamilie ist sehr groß. In der R-Hilfe zu sapply() finden wir
unter anderem Hinweise und Links zu folgenden weiteren Funktionen:
• vapply(): Erweiterung von sapply(), bei der wir den Rückgabetyp mittels
Übergabe eines Templates (FUN.VALUE) steuern können.
• mapply(): Eine multiple Erweiterung von sapply(), bei der wir flexiblere Pa-
rameterübergaben vornehmen können.
So hätten wir zum Beispiel den Code von Seite 222
Sehr gerne darfst du dich in Eigenregie tiefergehend mit diesen Funktionen befassen!
224 D Datenstrukturen
In diesem Buch begegnen wir in (19) der Funktion apply(), die sich für die wie-
derholte Funktionsanwendung auf Zeilen oder Spalten einer Matrix eignet. In (25)
sehen wir uns tapply() an. Es bleibt also spannend!
Schleifen sind eine Alternative zu sapply() und lapply(), die wir in (28) einführen.
Funktionen der *apply()-Familie lassen sich wunderbar mit selbst geschriebenen
Funktionen kombinieren. In (29) lernen wir eigene Funktionen zu schreiben.
18.6.3 Übungen
> n <- 6
> sapply(lapply(1:n, ":", 1), sum)
a) Was wird nach Ausführung dieses Codes auf die Console gedruckt?
b) Finde eine kürzere (und effizientere) Alternative, die inhaltlich genau das-
selbe tut.
2. Wir betrachten das Objekt pizzen aus der Einleitung dieses Kapitels.
> pizzen
$Margherita
[1] "Tomaten" "Kaese"
$Cardinale
[1] "Tomaten" "Kaese" "Schinken"
$‘San Romio‘
[1] "Tomaten" "Kaese" "Schinken" "Salami" "Mais"
$Provinciale
[1] "Tomaten" "Kaese" "Schinken" "Mais" "Pfefferoni"
3. (Fortsetzung von 2.) Wir wollen nun die Zutaten jeder Pizza alphabetisch
sortieren, aber Tomaten und Kaese sollen immer zuerst genannt werden.
a) Erstelle eine Liste pizzen12, welche die ersten beiden Zutaten (hier To-
maten und Kaese) entfernt.
Hinweis: Du darfst davon ausgehen, dass Tomaten und Kaese immer an
den ersten beiden Stellen stehen. Wenn du allgemeiner programmieren
und explizit Tomaten und Kaese finden und ausschließen willst, dann
könntest du viel Freude mit mapply() haben ;-)
b) Sortiere die Zutaten jeder Pizza in pizzen12 alphabetisch.
> pizzen12 # Zwischenstand nach 3b)
$Margherita
character(0)
$Cardinale
[1] "Schinken"
$‘San Romio‘
[1] "Mais" "Salami" "Schinken"
$Provinciale
[1] "Mais" "Pfefferoni" "Schinken"
c) Ein Kollege schlägt jetzt folgenden Code zur Lösung der Aufgabe vor:
> lapply(pizzen12, c, c("Tomaten", "Kaese"))
i. Was wird nach Ausführung dieses Codes auf die Console gedruckt?
ii. Warum funktioniert dieser Ansatz nicht bzw. kann diese Aufgabe mit
c() ohne Zuhilfenahme weiterer Funktionen nicht gelöst werden?
Für interessierte Tüftlerinnen und Tüftler: Tatsächlich können wir diese Auf-
gabe mit den bisher gelernten Techniken auch mit c() (insbesondere ohne
append()) lösen. Wie müssen wir den Code der obigen Teilaufgaben modifi-
zieren, damit es funktioniert?
226 D Datenstrukturen
b) Bei welchen Codes in 4a) ist die Datenstruktur immer eindeutig, auch
wenn wir beliebige Buchstabenlisten mit mindestens zwei Buchstaben
pro Listenelement für x verwenden? Welche Datenstrukturen können bei
den anderen Codes jeweils theoretisch herauskommen? Begründe deine
Antwort.
c) Schreibe einen Code, der aus x folgenden String erzeugt:
> string
[1] "LEA ISST EIER"
19 Wiederholte Funktionsanwendung bei Matrizen 227
In (15.8.3) über Matrizen haben wir festgestellt, dass wir einige Aufgaben noch nicht
(sinnvoll) bewältigen können. So hat uns bis jetzt ein passendes Werkzeug gefehlt,
um beispielsweise jede Spalte einer Matrix aufsteigend zu sortieren.
In diesem Kapitel führen wir die Funktion apply() ein, mit der wir eine Funktion auf
Zeilen oder Spalten einer Matrix anwenden können. Die Anwendung dieser Funktion
gleicht weitgehend jener von sapply(), die wir in (18) kennengelernt haben.
Wir erinnern uns an unsere Minigolfspieler aus (8) und (15).
Es sind noch einige Fragen offen, die wir in diesem Kapitel beantworten:
• Wie bestimmen wir mit apply() die Schlagsummen aller Spieler? (19.1.1)
• Wie bilden wir für jeden Spieler die kumulierte Summe seiner Schlagzahlen?
Nach welchen Bahnen liegt Spieler 1 in Führung. (19.1.2)
• Wie bestimmen wir die kleinste/größte Schlagzahl auf jeder Bahn? (19.1.3)
• Auf welchen Bahnen war jeder Spieler überdurchschnittlich gut? (19.2)
In (19.2) und (19.3) lernen wir unter anderem, wie wir jede Spalte einer Matrix
zentrieren, skalieren und standardisieren. Die Berechnung äußerer Vektorprodukte
rundet dieses Kapitel in (19.4) ab.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_19
228 D Datenstrukturen
Mit der Funktion apply() können wir eine Funktion auf alle Zeilen oder Spal-
ten einer Matrix anwenden. Der Funktionsaufruf gleicht bis auf eine winzige
Ausnahme jenem von lapply() aus (18.1):
Dem Parameter X bzw. FUN übergeben wir die Matrix bzw. die Funktion, die sodann
auf jede Zeile oder Spalte von X angewendet wird. Bei MARGIN = 1 wird FUN auf jede
Zeile von X, bei MARGIN = 2 auf jede Spalte von X angewendet. Dem Dreipunktear-
gument ... können wir optionale Argumente für FUN übergeben.
Bevor wir den linken Code erläutern, sei gesagt, dass colSums(Schlaege) deutlich
effizienter ist! Dieses Beispiel dient also lediglich zur Illustration.
Die Idee gleicht im Prinzip jener von sapply()! Da MARGIN = 2 ist, werden der
Reihe nach die Spalten von Schlaege als Vektor extrahiert und in die Funktion
sum() eingesetzt. Folgender Code verdeutlicht die Arbeitsweise von apply():
Die Funktion apply() wählt die Datenstruktur des Ergebnisobjektes nach denselben
Regeln aus, wie sapply() mit der Option simplify = TRUE (vgl. (18.4)):
• list, wenn weder ein Vektor noch eine Matrix generierbar ist.
Beispiel: Berechne für jeden Spieler die kumulierten Schlagzahlen. Bestimme außer-
dem, nach welchen Bahnen Spieler 1 in Führung liegt.
Die Funktion apply() wendet also cumsum() wegen MARGIN = 2 auf jede Spalte
der Matrix Schlaege an. Das Ergebnisobjekt ist eine Matrix, da cumsum() für jede
Spalte einen gleichlangen Vektor der Länge 6 zurückgibt.
Der zweite Teil der Aufgabe ist tricky, aber für uns angehende R-Profis kein Problem!
> # 2.) Bestimme jene Bahnen, nach denen Spieler 1 in Führung liegt.
> Schlaege.cum.min <- apply(Schlaege.cum, MARGIN = 1, FUN = min)
> Schlaege.cum.min
Bahn1 Bahn2 Bahn3 Bahn4 Bahn5 Bahn6
1 3 6 11 17 18
Wir ermitteln für jede Bahn, wie viele Schläge der nach der entsprechenden Bahn
in Führung liegende Spieler benötigt hat. Dazu wenden wir min() auf jede Zeile der
Matrix Schlaege.cum an. Nun bestimmen wir, wann die kumulierten Schlagzahlen
von Spieler 1 mit diesen Minima übereinstimmen. Bei einer Übereinstimmung liegt
Spieler 1 in Führung.
Nach Bahn Nummer 4 hat für Spieler 1 die Minigolfwelt perfekt ausgesehen.
230 D Datenstrukturen
Wenn das Ergebnisobjekt ein Vektor oder eine Liste ist, dann gibt es keine Diskussion
darüber, wie die Elemente angeordnet werden.
Frage: Wie ordnet apply() die Ergebnisse hingegen an, wenn das Ergebnisobjekt
eine Matrix ist?
Klären wir diese Frage anhand eines Beispiels!
Beispiel: Bestimme für jede Bahn die kleinste und größte Schlagzahl.
Wir erkennen, dass die Bahnen jetzt in den Spalten zu finden sind, sich also die
Semantik der Zeilen und Spalten geändert hat. Der Grund dafür: Wie sapply()
ordnet auch apply() die Ergebnisvektoren immer spaltenweise an, egal, ob wir
MARGIN = 1 oder MARGIN = 2 setzen.
Wir können, wenn wir wollen, die Matrix transponieren.
> t(Schlaege.range)
[,1] [,2]
Bahn1 1 2
Bahn2 2 3
Bahn3 2 3
Bahn4 4 7
Bahn5 4 7
Bahn6 1 3
Selbstverständlich können wir auch bei apply() optionale Argumente für FUN über-
geben. Die Parameterzuweisung funktioniert dabei analog wie bei lapply() oder
sapply() (vgl. (18.2)). Daher beschränken wir uns auf ein Codebeispiel.
> # Das 0%- und 100%-Quantil für die Schlagzahlen jeder Bahn bestimmen
> apply(Schlaege, MARGIN = 1, FUN = quantile, probs = c(0, 1))
Bahn1 Bahn2 Bahn3 Bahn4 Bahn5 Bahn6
0% 1 2 2 4 4 1
100% 2 3 3 7 7 3
Hier wird probs = c(0, 1) als zusätzliches Argument in die Funktion quantile()
gesteckt.
19 Wiederholte Funktionsanwendung bei Matrizen 231
Bei der Zentrierung wird in jeder Spalte einer Matrix der entsprechende Mittelwert
abgezogen, bei der Standardisierung wird überdies durch die Standardabwei-
chung dividiert (vgl. (7.6.2)). Beide Wünsche können wir uns mit apply() erfüllen,
dazu sind jedoch eigene Funktionen nötig (besprechen wir in (29)).
Bis es soweit ist, lernen wir mit der Funktion sweep() eine Alternative kennen; der
vereinfachte Funktionsaufruf:
Dabei übergeben wir x bzw. STATS eine Matrix bzw. einen Vektor. Bei MARGIN = 1
wird jede Zeile von x mit dem entsprechenden Eintrag von STATS via FUN verknüpft.
Für MARGIN = 2 gilt selbiges für die Spalten. Dabei müssen wir sicherstellen, dass
die Länge des Vektors STATS bei MARGIN = 1 der Anzahl der Zeilen von x bzw. bei
MARGIN = 2 der Anzahl der Spalten von x entspricht.
Auf das Recycling sollten wir uns bei sweep() nicht verlassen! ;-)
Beispiel: Bereinige jede Spalte von Schlaege um den Spaltenmittelwert.
Wir wissen jetzt, auf welchen Bahnen jeder Spieler überdurchschnittlich gut (Ein-
träge kleiner 0) bzw. überdurchschnittlich schlecht (Einträge größer 0) war.
Bemerkung: Bei der Zentrierung kommt es (wie auch hier) oft zu Rundungsfehlern
(vgl. (13)).
Bemerkung: Wenn wir schon eigene Funktionen schreiben könnten, würde der Code
für die Zentrierung mit apply() so aussehen:
Bis auf winzige (nicht sichtbare) Rundungsfehler sind alle Spalten standardisiert.
Mit den Parametern center bzw. scale steuern wir, ob wir die Spalten zentrieren
bzw. skalieren (normieren) wollen (TRUE oder FALSE).
19 Wiederholte Funktionsanwendung bei Matrizen 233
Zuerst wird die standardisierte Matrix ausgegeben. Danach werden die Spaltenmit-
telwerte und Standardabweichungen jeder Spalte von Schlaege als Attribute ange-
hängt. Wie wir auf Attribute zugreifen, lernen wir in (26).
Nehmen wir einmal an, wir wollen eine Tabelle mit dem kleinen Einmaleins generie-
ren. Ein Fall für die Funktion outer().
Die Elemente der beiden Vektoren X und Y werden mit der Funktion FUN verknüpft
und die Ergebnisse in eine Matrix gesteckt. In der i. Zeile und j. Spalte steht dabei
das Ergebnis von FUN(X[i], Y[j]).
Findige Personen erkennen, dass es sich bei der letzten Codezeile um ein äußeres
Vektorprodukt handelt. Allgemein: Für zwei Vektoren x = (x1 , x2 , . . . , xn )t und
y = (y1 , y2 , . . . , ym )t bezeichnet der Ausdruck x · y t das äußere Vektorprodukt
von x und y, welches wir mit Hilfe des %o%-Operators kürzer berechnen können.
19.5 Abschluss
• Wie wenden wir eine Funktion auf jede Zeile oder Spalte einer Matrix an?
(19.1), (19.1.1)
• Wie wählt apply() die Datenstruktur des Ergebnisobjektes? Falls eine Matrix
herauskommt: Wie ordnet apply() die Ergebnisvektoren in der Matrix an?
(19.1.2), (19.1.3)
• Wie können wir der auf die Zeilen oder Spalten der Matrix anzuwendenden
Funktion weitere optionale Argumente übergeben? (19.1.4)
• Was kann die Funktion sweep() und wie wenden wir sie korrekt an? Wie kön-
nen wir mit Hilfe von sweep() bzw. scale() Spalten einer Matrix zentrieren,
skalieren oder standardisieren? (19.2), (19.3)
• Wie bilden wir äußere Vektorprodukte? (19.4)
19.5.2 Ausblick
Schleifen sind eine Alternative zu apply(), die wir in (28) einführen. Auch apply()
lässt sich wunderbar mit selbst geschriebenen Funktionen kombinieren, sodass wir
sweep() nicht benötigen. In (29) lernen wir eigene Funktionen zu schreiben.
Lösungen, die auf apply() (oder auch Schleifen) beruhen, sind oft ineffizient. Wir
sehen uns in (33) einige Beispiele zum Thema effizientes Programmieren an.
19 Wiederholte Funktionsanwendung bei Matrizen 235
19.5.3 Übungen
> Pkt <- matrix(c(43, 45, 17, NA, 13, 32, NA, NA, 49, 15),
+ ncol = 2, byrow = TRUE)
> rownames(Pkt) <- paste0("Stud", 1:nrow(Pkt))
> colnames(Pkt) <- paste0("T", 1:ncol(Pkt))
> Pkt
T1 T2
Stud1 43 45
Stud2 17 NA
Stud3 13 32
Stud4 NA NA
Stud5 49 15
Ein Kollege schlägt die folgenden beiden Codes zur Realisierung vor:
> apply(Pkt, 1, sort, decreasing = TRUE)
> apply(Pkt, 1, sort, decreasing = TRUE, na.last = TRUE)
a) Führe bei der Matrix Schlaege eine Median-Bereinigung durch. Mit an-
deren Worten: Ziehe von jeder Spalte den entsprechenden Spaltenmedian
ab.
b) Auf welchen Bahnen hat Spieler 1 weniger Schläge benötigt als jeder sei-
ner Kontrahenten? Schreibe einen Code, der für beliebig viele Spieler
funktioniert.
20 Dataframes 237
20 Dataframes
Matrizen können nur einen Datentyp verwalten. Für die Speicherung von Daten sind
allerdings oft mehrere Typen notwendig. Wir brauchen eine Datenstruktur, die für
jede Spalte (Variable) einen eigenen Mode ermöglicht: Sie heißt Dataframe.
Wir begleiten in diesem Kapitel 6 Studierende, die in einer Lehrveranstaltung einen
Abschlusstest absolviert haben, bei dem 40 Punkte erzielt werden konnten. Es hat
zwei Testgruppen (A und B) gegeben und vor Beginn der Prüfung wurden 6 Test-
bögen ausgeteilt. Ein Student ist nicht angetreten, was wir mit NA markieren.
• Wie verwalten wir die Prüfungsdaten in einem Dataframe? Wie verschaffen wir
uns einen Überblick über das Dataframe und ändern Beschriftungen? (20.1)
• Wie selektieren wir bestimmte Zeilen oder Spalten des Dataframes? (20.3)
• Einige Studierende bekommen die Chance, sich bei einem Nachtest (zweiter
Antritt) zu verbessern. Wie können wir den zweiten Antritt und die Abschluss-
noten an das Dataframe anhängen? Wie löschen wir Spalten? (20.4.1)
> test
Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud1 38 A 12 30 30 2
Stud2 82 B 31 NA 31 2
Stud3 53 B 17 14 17 5
Stud4 72 A 0 13 13 5
Stud5 31 B 28 NA 28 3
Stud6 59 A 39 NA 39 1
Wir klären in (20.2), warum sich eine Matrix nicht zur Verwaltung der Prüfungser-
gebnisse eignet und sehen in (20.5), dass ein Dataframe eng verwandt ist mit Listen,
die wir aus (17) kennen. Am Ende erfahren wir unter anderem, worauf wir achten
müssen, wenn wir sapply() und apply() auf Dataframes anwenden.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_20
238 D Datenstrukturen
Mit der Funktion data.frame() können wir aus Vektoren ein Dataframe generie-
ren. Wir übergeben der Funktion beliebig viele Spalten, wobei wir die Spaltennamen
auch mit übergeben können:
Beispiel: Erstelle ein Dataframe, das neben den Variablen nummer, gruppe und
punkte auch die Information enthält, ob der entsprechende Student die Lehrveran-
staltung bestanden hat. Um die Lehrveranstaltung zu bestehen, sind mindestens 20
Punkte nötig.
Bei nummer und punkte >= 20 haben wir keine Variablennamen übergeben, daher
übernimmt R den Namen der beiden Vektoren. Gewisse Zeichen (wie zum Beispiel
Leerzeichen, Anführungszeichen etc.) werden dabei als Punkt dargestellt.
Gewiss wäre es hübscher gewesen, einen Vektor Bestanden zu erstellen ...
... und diesen Vektor statt punkte >= 20 in der Funktion data.frame() einzusetzen.
Dann hätten wir eine schönere Beschriftung gehabt. So können wir aber (20.1.3)
motivieren, wo wir lernen, Beschriftungen zu ändern.
Bemerkung: Die Funktion data.frame() kann noch viel mehr, als wir an dieser
Stelle zeigen. Dem Parameter row.names können wir beispielsweise direkt Zeilenna-
men übergeben. Sehr spannend ist der Parameter stringsAsFactor, mit dem wir
steuern können, ob Strings als Faktoren interpretiert werden sollen und den wir kurz
in (20.1.4) betrachten. Der vollständige Funktionsaufruf:
Wir können von einem Dataframe wie von Matrizen die Dimensionen abfragen
(vgl. (15.1.3)). Einzig length() verhält sich anders, als einige vielleicht erwarten.
Probieren wir es für das auf Seite 238 erstellte Dataframe test aus.
> # Anzahl der Zeilen > # Anzahl der Spalten > # Anzahl der Spalten
> nrow(test) > ncol(test) > length(test)
[1] 6 [1] 4 [1] 4
Die Anzahl der Komponenten einer Datenstruktur, die wir mit length() abfragen,
ist bei Dataframes die Anzahl der Spalten und nicht die Anzahl der Einträge gesamt
(wie bei Matrizen). Daher sind ncol(test) und length(test) äquivalent.
Die Funktionen colnames() bzw. rownames() geben wie bei Matrizen die Spalten-
bzw. Zeilennamen aus. Statt colnames() können wir auch names() nehmen. Wer-
fen wir einen Blick auf die Beschriftungen des Dataframes test von Seite 238.
Die Zeilen werden standardmäßig mit 1 bis Anzahl der Zeilen beschriftet. Die
Umbenennung von Zeilen und Spalten funktioniert wie bei Matrizen.
Beispiel: Benenne die Variablen nummer und punkte....20 in Nr und Bestanden
um. Benenne die Zeilen von test mit "Stud1", "Stud2", ....
> test
Nr Gruppe Punkte Bestanden
Stud1 38 A 12 FALSE
Stud2 82 B 31 TRUE
Stud3 53 B 17 FALSE
Stud4 72 A NA NA
Stud5 31 B 28 TRUE
Stud6 59 A 39 TRUE
Sieht doch gleich besser aus! Statt names() können wir auch colnames() nehmen.
240 D Datenstrukturen
Jede Spalte steht für eine Variable und jede Zeile für eine Beobachtung (observa-
tion). Um uns einen Überblick über die Daten zu verschaffen, bieten sich die
Funktionen str(), head() sowie tail() an.
Die Funktion head() gibt die ersten n und die Funktion tail() die letzten n
Zeilen eines Dataframes aus. In der Praxis sieht man sich – zwecks Kontrolle – nach
dem Einlesen von Daten immer zumindest die ersten und letzten Zeilen an.
> # Die ersten 3 Zeilen betrachten > # Die letzten 3 Zeilen betrachten
> head(test, n = 3) > tail(test, n = 3)
Nr Gruppe Punkte Bestanden Nr Gruppe Punkte Bestanden
Stud1 38 A 12 FALSE Stud4 72 A NA NA
Stud2 82 B 31 TRUE Stud5 31 B 28 TRUE
Stud3 53 B 17 FALSE Stud6 59 A 39 TRUE
Das Dataframe test enthält also 6 Beobachtungen und 4 Variablen. Die Variablen
Nr und Punkte sind vom Mode numeric, Bestanden vom Typ logical und Gruppe
vom Typ character.
Wir erkennen, dass Gruppe mit dieser Einstellung ein Faktor ist.
20 Dataframes 241
Rein optisch scheinen Matrizen und Dataframes gleich zu sein. Einen elementaren
Unterschied arbeiten wir jedoch jetzt heraus. Mit Hilfe der Funktion as.matrix()
können wir ein Dataframe in eine Matrix umwandeln.
Beim Zugriff auf Zeilen und Spalten eines Dataframes greifen wir auf wohlbekannte
Konzepte zurück, die wir bereits von Listen und Matrizen kennen. Wir beziehen uns
in den folgenden Unterabschnitten auf das Objekt test auf dieser Seite.
Wollen wir aus einem Dataframe eine Variable als Vektor selektieren, so haben
wir drei wohlbekannte Möglichkeiten zur Verfügung.
Beispiel: Selektiere aus dem Dataframe test die Variable Punkte als Vektor.
Wollen wir aus einem Dataframe eine Variable selektieren und gleichzeitig die
Dataframestruktur bewahren, so haben wir zwei Möglichkeiten dafür.
Beispiel: Selektiere aus dem Dataframe test die Variable Punkte. Bewahre die
Dataframestruktur bei der Selektion!
Wollen wir auf mehrere Spalten zugreifen, so gibt es auch hierfür zwei Varianten.
Beispiel: Selektiere aus dem Dataframe test die Variablen Gruppe und Bestanden.
Wir brauchen uns (fast) keine Gedanken über Umwandlungen zu machen. Es kommt
immer ein Dataframe heraus, solange wir mindestens 2 Spalten selektieren.
Erinnern wir uns an die Funktion subset() aus (14.3.4). Falls nicht, holen wir sie
wieder zurück ins Gedächtnis.
Beispiel: Selektiere aus test auf Seite 241 die Spalten Nr, Gruppe und Punkte all
jener Tests (jener Zeilen), deren Gruppe "A" ist und bei denen weniger als 20 Punkte
erzielt wurden.
Mit subset wählen wir die Zeilen, mit select wählen wir die Spalten aus. Wirkt auf
den ersten Blick unspektakulär, aber eine Interessantheit soll uns nicht entgehen: Alle
Spalten eines Dataframes sind innerhalb von subset() verfügbar, wir können also
beispielsweise Gruppe == "A" statt test$Gruppe == "A" schreiben. Gleichlautende
Objekte außerhalb der Funktion werden ignoriert.
Treten innerhalb von subset fehlende Werte auf, so werden die entsprechenden
Zeilen standardmäßig ignoriert, also nicht mitselektiert.
So wurde im letzten Beispiel die Zeile Test4 nicht mitselektiert, da in dieser Zeile
bei Punkte ein NA steht und die Abfrage Gruppe == "A"& Punkte < 20 für diese
Zeile den Wert NA ergibt. Wollen wir dieses Verhalten ändern, so müssen wir das
subset() explizit mitteilen.
Beispiel: Selektiere aus test die Spalten Nr, Gruppe und Punkte all jener Tests
(jener Zeilen), deren Gruppe "A" ist und bei denen weniger als 20 Punkte erzielt
wurden oder die Punkte fehlen (also der Student nicht anwesend war).
Mit folgenden Varianten können wir einem Dataframe eine Variable hinzufügen:
Der Vektor inhalt wird als Spalte mit der Beschriftung name hinten angehängt.
244 D Datenstrukturen
Mit der Funktion cbind() (vgl. (15.1.2)) können wir einem Dataframe auch meh-
rere Variablen hinzufügen.
Wollen wir aus einem Dataframe eine oder mehrere Spalten löschen, so bieten
sich folgende (auf NULL basierende) Varianten an:
Die $-Variante funktioniert nur für eine Variable, während die Variante mit den
einfachen eckigen Klammern auch für mehrere Variablen klappt. Bei zweiterer
Variante wird empfohlen, list(NULL) statt NULL zu verwenden.
Zeit für einige Beispiele. Davor zeigen wir noch unseren Zwischenstand:
> test
Nr Gruppe Punkte Bestanden
Stud1 38 A 12 FALSE
Stud2 82 B 31 TRUE
Stud3 53 B 17 FALSE
Stud4 72 A NA NA
Stud5 31 B 28 TRUE
Stud6 59 A 39 TRUE
Beispiel: Wir wollen jenen Studierenden, welche die Lehrveranstaltung (noch) nicht
bestanden oder beim Test gefehlt haben, die Chance geben, sich bei einem zweiten
Antritt zu verbessern.
1. Simuliere für die besagten Studierenden die erzielten Punkte des zweiten An-
tritts aus dem Intervall [0, 40]. Bei allen anderen Studierenden soll NA stehen.
2. Hänge sodann die Punkte des zweiten Antritts an test an. Die Spalten mit
den Punkten des ersten bzw. zweiten Antritts sollen Pkt1 bzw. Pkt2 heißen.
Falls dich die Idee des obigen Codes an die Generierung von lückenlosen Häufig-
keitstabellen in (12.2.4) erinnert, dann liegst du richtig! Du merkst: Die Konzepte
wiederholen sich ;-)
Beispiel: Bestimme nun die folgenden beiden Variablen und hänge sie an das Da-
taframe test an.
1. Pkt: Das jeweils bessere Ergebnis beider Antritte (Pkt1 und Pkt2).
2. Note: Die Note wird aus Pkt gemäß folgender Tabelle errechnet:
1: [35, 40] 2: [30, 35) 3: [25, 30) 4: [20, 25) 5: [0, 20)
Falls du übrigens in 2.) gerade ein Déjà-vu hattest, könnte es an dem Beispiel aus
(14.3.3) liegen ;-)
Beispiel: Lösche die Variable Bestanden aus dem Dataframe test.
> test
Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud1 38 A 12 30 30 2
Stud2 82 B 31 NA 31 2
Stud3 53 B 17 14 17 5
Stud4 72 A NA 13 13 5
Stud5 31 B 28 NA 28 3
Stud6 59 A 39 NA 39 1
Ein kurzes Beispiel zeigt, wie einfach wir Zeilen umordnen können. Für Spalten
funktioniert das Ganze analog!
Beispiel: Ordne die Zeilen des Dataframes test auf dieser Seite gemäß Gruppe und
dann absteigend nach Pkt.
Ein Fall für order()! Eine Wiederholung des Themas Mehrfachsortierung ((6.2.5)
und (6.2.6)) kann dabei nicht schaden ;-)
Wir sehen, dass jetzt zuerst alle Zeilen mit Gruppe A kommen und anschließend
jene mit Gruppe B. Innerhalb jeder Gruppe sind die Zeilen von test absteigend
nach Punkte (Pkt) sortiert. Da wir in order() entweder alle Vektoren aufsteigend
oder alle Vektoren absteigend sortieren können (und nicht etwa eine auf- und eine
andere absteigend), drehen wir das Vorzeichen von test$Pkt um.
Das Ersetzen von Einträgen oder Variablen funktioniert mit den gleichen Me-
thoden, die wir beim Subsetting schon gesehen haben. Wir beschränken uns daher
auf ein kurzes Beispiel.
Beispiel: Ersetze in test in der Variable Pkt1 alle fehlenden Werte durch 0.
> test
Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud1 38 A 12 30 30 2
Stud2 82 B 31 NA 31 2
Stud3 53 B 17 14 17 5
Stud4 72 A 0 13 13 5
Stud5 31 B 28 NA 28 3
Stud6 59 A 39 NA 39 1
Dataframes sind ein Spezialfall von Listen mit der Einschränkung, dass die Kom-
ponenten des Dataframes (dessen Spalten) alle dieselbe Länge haben müssen. Wir
schauen uns den Zusammenhang zwischen Dataframes und Listen anhand des Objek-
tes test.part an, ein kleineres Teilobjekt von test, damit es übersichtlicher wird.
Insbesondere die fehlenden Werte der Variable Pkt wecken gleich unser Interesse.
Mit der Funktion is.data.frame() fragen wir ab, ob das Objekt ein Dataframe ist.
Ja, test.part ist ein Dataframe. Und auch eine Liste, wie wir sehen. Gehen wir
einen Schritt weiter. Mit class() erfragen wir die Klasse eines Objekts.
Noch genauer: Ein Dataframe ist eine Liste mit dem Klassenattribut data.frame.
Wir werden uns in (26) ausführlicher mit Klassen befassen, wollen uns aber eine sehr
nützliche Funktion bereits jetzt ansehen.
248 D Datenstrukturen
Die Funktion unclass() entfernt das Klassenattribut des übergebenen Objekts. Da-
mit können wir die interne Struktur eines Objektes freilegen; ein nützlicher Befehl!
In (26) erfahren wir auch, was es mit Attributen (attr) auf sich hat.
Eine Überführung von Dataframes in Listen (mit as.list()) ist immer mög-
lich, umgekehrt nur, wenn die Längenbedingung eingehalten wird.
Wenn die Elemente der Liste die Längenbedingung nicht erfüllen, so kommt es
zu Problemen. Im schlimmsten Fall bemerken wir diese Probleme nicht, im besten
Fall gibt R eine Fehlermeldung aus.
> # Die Liste ohne NAs in Pkt2 > # Rücküberführung in ein Dataframe
> test.part.liste > as.data.frame(test.part.liste) # !!
$Pkt1 Pkt1 Pkt2 Pkt
[1] 12 31 17 0 28 39 1 12 30 30
$Pkt2 2 31 14 31
[1] 30 14 13 3 17 13 17
$Pkt 4 0 30 13
[1] 30 31 17 13 28 39 5 28 14 28
6 39 13 39
Hier hat das Recycling gewütet und unsere Daten durcheinander gewirbelt. Wenn
wir nicht genau hinschauen, fällt uns evtl. nicht auf, dass Pkt2 nicht mehr stimmt.
20 Dataframes 249
Wenn kein vollständiges Recycling angewendet werden kann, so wird eine Fehler-
meldung ausgegeben. Diese Fehlermeldung bewahrt uns oft vor Schlimmem!
> as.data.frame(test.part.liste)
Fehler in (function (..., row.names = NULL, check.rows = FALSE,
check.names = TRUE, :
Argumente implizieren unterschiedliche Anzahl Zeilen: 5, 3, 6
Einige bisher gelernten Funktionen lassen sich (unter Umständen) auch bei einem
Dataframe sinnvoll anwenden. Wir studieren in (20.6.1), wie sich sapply(), apply()
sowie colMeans() bei Dataframes verhalten. Und in (20.6.2) erfahren wir, dass wir
is.na() und viele Vergleichsoperatoren auch bei Dataframes anwenden können.
Generell gilt: Probiere Dinge einfach mal aus und schau, was passiert!
Wir stellen uns die Frage, ob und inwieweit wir die Funktionen apply() und sapply()
sowie weitere Funktionen wie colMeans() bei Dataframes anwenden können. Pro-
bieren wir es einfach mit unserem Objekt test aus!
> test
Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud1 38 A 12 30 30 2
Stud2 82 B 31 NA 31 2
Stud3 53 B 17 14 17 5
Stud4 72 A 0 13 13 5
Stud5 31 B 28 NA 28 3
Stud6 59 A 39 NA 39 1
Wir wollen nun den Mittelwert für jede Spalte von test berechnen, um unter
anderem herauszufinden, welcher Test schwieriger und welcher leichter war. Dabei
machen wir einige erstaunliche Entdeckungen!
250 D Datenstrukturen
Wir sehen, dass wir nichts (Sinnvolles) sehen. Alle guten Dinge sind drei?! Probieren
wir es mit sapply().
Besser! Beim Aufruf von apply() wird das Dataframe wie schon bei colMeans()
zuerst in eine Matrix (vom Mode character) umgewandelt. Da Zeichenketten aber
keinen Mittelwert haben (können), gibt mean() für jede Spalte NA zurück.
Die Funktion sapply() hingegen wendet die Funktion mean() separat auf jede
Spalte des Dataframes an. Daher wird nur für die Spalte Gruppe ein NA erzeugt.
Letzteres schaut ein wenig unsauber aus.
Frage: Wie wäre es, wenn wir alle numerischen Spalten selektieren würden,
bevor wir die Spaltenmittelwerte berechnen? Wenn du von dieser Idee auch begeistert
bist, dann werden dir die folgenden Codezeilen viel Freude bereiten!
20 Dataframes 251
> test[bool.pkt]
Pkt1 Pkt2 Pkt
Stud1 12 30 30
Stud2 31 NA 31
Stud3 17 14 17
Stud4 0 13 13
Stud5 28 NA 28
Stud6 39 NA 39
Wenn wir die Anzahl der fehlenden Werte für jede Zeile oder Spalte eines Da-
taframes bestimmen wollen, so können wir das bequem mit der Funktion is.na()
bewerkstelligen. Sie erzeugt eine logische Matrix desselben Ausmaßes, wobei ein TRUE
besagt, dass an der entsprechenden Stelle des Dataframes ein NA vorkommt.
Schauen wir uns das kurz für die Punktespalten von test an. Wie gut, dass wir auf
der vorherigen Seite das passende Objekt bool.pkt erzeugt haben ;-)
Auch viele Vergleichsoperatoren (vor allem "<", "<=", "==", ">=", ">", "!=")
können wir bei Dataframes anwenden!
Beispiel: Zähle nach, wie oft bei jedem Test sowie insgesamt mindestens 20 Punkte
erzielt wurden.
20.7 Abschluss
• Wie erstellen wir ein Dataframe? Wie übergeben wir dabei die gewünschten
Spaltennamen und welcher Spaltenname wird gewählt, wenn wir keine Spal-
tennamen bestimmen? (20.1.1)
• Wie erfragen wir die Dimensionen eines Dataframes? Was gibt die Funktion
length() aus, wenn wir sie auf ein Dataframe anwenden und was ist der Grund
dafür? (20.1.2)
• Wie greifen wir auf die Beschriftungen von Zeilen und Spalten eines Dataframes
zu und wie ändern wir die Beschriftungen? (20.1.3)
• Worüber gibt uns der Output von str() (auf ein Dataframe angewendet)
Auskunft? Wie können wir die ersten bzw. letzten n Zeilen ausgeben? (20.1.4)
• Wie wandeln wir ein Dataframe in eine Matrix um? Was passiert im Zuge
dessen, wenn das Dataframe Spalten mit unterschiedlichen Modes hat? (20.2)
• Wie greifen wir auf eine oder mehrere Spalten eines Dataframes zu? Wie be-
wahren wir dabei die Dataframestruktur? Wie selektieren wir Zeilen eines Da-
taframes? (20.3)
• Wie fügen wir einem Dataframe eine oder mehrere Variablen hinzu? Wie lö-
schen wir eine oder mehrere Variablen? (20.4.1)
• Wie ordnen wir die Zeilen oder Spalten eines Dataframes um? Wie ersetzen
wir Einträge in einem Dataframe? (20.4.2), (20.4.3)
• Wie hängt ein Dataframe mit einer Liste (list) zusammen? Wie wandeln wir
ein Dataframeobjekt in eine Liste um und umgekehrt? Was passiert bei der
Umwandlung einer Liste in ein Dataframe, wenn die Listenelemente ungleich
lange sind? (20.5)
• Was passiert, wenn wir colMeans(), apply(), sapply() und is.na() auf
ein Dataframe anwenden? Wie verhält es sich bei Vergleichsoperatoren wie
beispielsweise "==" und "<"? (20.6)
20.7.3 Ausblick
Dataframes begegnen uns noch häufig, unter anderem in (32), wenn wir lernen, wie
wir Daten in R einlesen. Aber auch in (21), wo wir uns ansehen, wie wir Dataframes
miteinander verknüpfen können. Auch sonst gibt es noch viel Spannendes zu entde-
cken! In (20.1.4) haben wir bemerkt, dass die Variable Gruppe ein Faktor ist. Was
sich dahinter verbirgt, besprechen wir ausführlich in (24). Und in (30) gehen wir
nochmal auf sapply() in Zusammenhang mit Dataframes ein.
254 D Datenstrukturen
20.7.4 Übungen
> geschlecht <- c("w", "m", NA, "m", "w", "w", "w", "m", "m", "m")
> groesse <- c(176, 181, 181, 183, 163, 157, 164, 166, 176, 184)
> gewicht <- c(65, 92, 65, 93, 49, 47, NA, 50, 62, 84)
kg
Der BMI berechnet sich gemäß m2 (Gewicht in kg durch Größe in m zum
Quadrat).
a) Erstelle aus den drei Vektoren das Dataframe daten. Wähle dabei sinn-
volle Variablennamen. Warum ist eine Matrix nicht geeignet?
2. Wir betrachten das Dataframe test aus diesem Kapitel. Du kannst den End-
stand, der auf Seite 237 abgebildet ist, wie folgt laden:
a) Zähle, wie oft bei jedem Test sowie insgesamt zwischen 20 und 29 Punkte
erreicht wurden.
b) Angenommen, es gäbe mehr als zwei Tests und die entsprechenden Spal-
ten sind mit Pkt1, Pkt2, . . . beschriftet. Schreibe einen Code, der für jeden
Studierenden das Ergebnis mit den wenigsten Punkten ermittelt. Dabei
sollen fehlende Werte wie 0 Punkte gewertet werden und das Dataframe
test soll nicht überschrieben werden!
Hinweis: Mit folgendem Code bestimmst du allgemein, welche Spalten
von test mit Pkt1, Pkt2, . . . beschriftet sind.
21 Dataframes verknüpfen
• Dataframes verknüpfen
Eine gute Datenbank zeichnet sich dadurch aus, dass die Daten speicherschonend
und konfliktfrei (konsistent) verwaltet werden. Dabei werden die Daten in der Regel
auf mehrere Tabellen verteilt und bei Bedarf miteinander verknüpft. Die Umsetzung
einiger wichtiger Verknüpfungsoperationen (Joins) ist das Thema dieses Kapitels.
Wir begleiten drei Studierende, die an einer Lehrveranstaltung teilnehmen, in der es
zwei Tests gibt (T1 und T2). Die folgenden beiden Tabellen enthalten die Ergebnisse.
T1 T2
Ben 2 Eva 5
Eva 3 Jan 4
Ben hat nur am 1. Test teilgenommen, ergo suchen wir ihn in der rechten Tabelle
vergebens. Jan war nur beim 2. Test anwesend, weshalb er nur in der rechten Tabelle
aufscheint. Eva hat beide Tests absolviert und kommt daher in beiden Tabellen vor.
Wir gehen unter anderem folgenden Fragen nach:
• Wie erstellen wir eine Tabelle, die alle Studierenden und alle Testleistungen
enthält (ggf. mit fehlenden Werten)? (21.1)
• Wie können wir Testleistungen weiterer Studierenden anhängen? (21.2)
Ein Join ist eine Verknüpfung zweier Tabellen. Mit Hilfe eines Schlüssels (Key)
können wir jede Zeile einer Tabelle eindeutig identifizieren und bei einem Join sollten
die Schlüssel beider zu verknüpfenden Tabellen dieselbe Information enthalten. In
unserem Beispiel ist das der Name. Wir führen vier wichtige Joins ein:
• Right Join alle Zeilen, deren Schlüssel in der rechten Tabelle vorkommen.
Fehlende Tabelleneinträge werden mit fehlenden Werten (NA) aufgefüllt.
Mit der Funktion merge() können wir diese Joins mit R umsetzen.
Den Parametern x bzw. y übergeben wir die linke bzw. rechte Tabelle (als Data-
frame). Mit by steuern wir, welche Spalte bzw. welche Spalten als Schlüssel dienen.
Wenn wir by nicht spezifizieren, werden standardmäßig all jene Spalten als Schlüssel
herangezogen, deren Namen in beiden Tabellen gleich lauten. Beachte, dass das bei
einer sauberen Modellierung nur eine Spalte ist.
Mit den Parametern all, all.x und all.y steuern wir, welcher Join gebildet wird.
Das schauen wir uns sogleich anhand eines Beispiels an und werfen anschließend
einen Blick auf die Parameter by, by.x und by.y.
Beispiel: Bilde mit den Daten der Studierenden aus der Einleitung einen Inner
Join, Outer Join, Left Join und Right Join.
> T1 > T2
Name T1 Name T2
1 Ben 2 1 Eva 5
2 Eva 3 2 Jan 4
Standardmäßig wird ein Inner Join erzeugt. Durch korrekte TRUE-Belegung der Pa-
rameter all, all.x und all.y erzeugen wir die gewünschten Joins.
> # Inner Join > # Outer Join > # Left Join > # Right Join
> # all = FALSE > merge(T1, T2, > merge(T1, T2, > merge(T1, T2,
> merge(T1, T2) + all = TRUE) + all.x = TRUE) + all.y = TRUE)
Name T1 T2 Name T1 T2 Name T1 T2 Name T1 T2
1 Eva 3 5 1 Ben 2 NA 1 Ben 2 NA 1 Eva 3 5
2 Eva 3 5 2 Eva 3 5 2 Jan NA 4
3 Jan NA 4
21 Dataframes verknüpfen 257
Jetzt sehen wir uns den Sinn des Parameters by an. Standardmäßig wird die Schnitt-
menge der Namen beider Tabellen herangezogen:
Es wird also hier die Spalte Name als Schlüssel für beide Tabellen gewählt.
Frage: Was passiert, wenn es keine Namensübereinstimmungen gibt und wir by
nicht spezifizieren? Um diese Frage zu beantworten, ändern wir kurzerhand die Be-
schriftung von T2.
Die Schnittmenge ist also leer, wie uns das character(0) mitteilt. Wenn wir jetzt
obigen Code zur Bildung des Outer Joins anwenden, erleben wir eine Überraschung.
Findige R-Talente und Mathefans sehen vielleicht, dass in diesem Fall das kartesische
Produkt beider Tabellen gebildet wird, also jede Zeile der linken Tabelle (T1) mit
jeder Zeile der rechten Tabelle (T2) kombiniert wird. Um den Outer Join zu erhalten,
müssen wir jetzt merge() die Schlüsselspaltennamen explizit mitteilen.
Jetzt haben wir unseren Outer Join und alle drei Studierenden in einer Tabelle.
Es soll nicht unerwähnt bleiben, dass der Schlüsselname der linken Tabelle (Name)
übernommen wird.
258 D Datenstrukturen
Ziel: Wir wollen die beiden Dataframes Res.outer und Res2 aneinanderhängen.
Dazu kramen wir die Funktion rbind() heraus, die wir aus (15.1.1) kennen, und
mit der wir zwei Dataframes zeilenweise verknüpfen können.
Die Funktion rbind() verlangt, dass die Beschriftungen aller Dataframes über-
einstimmen. Man einigt sich also auf die Beschriftung Name statt Person.
Jetzt klappt es! Es spielt dabei übrigens keine Rolle, wie die Spalten gereiht sind.
Solange die Spaltennamen übereinstimmen, kommt rbind() damit klar.
> # Verdrehe die Spalten von Res2 - rbind() funktioniert immer noch
> Res2 <- Res2[length(Res2):1]
Wir betrachten eine einfache Datenbank zur Verwaltung von Studierenden und Lehr-
veranstaltungen an einer Universität. Es gibt drei Tabellen:
• Stud: zur Verwaltung der Studierenden (Matrikelnummer und Name)
• LV: zur Verwaltung der Lehrveranstaltungen (LV-Nummer und Titel der LV)
• besucht: zur Verwaltung, welche Studierenden welche LV(s) besuchen
Wollen wir etwa die Titel jener Lehrveranstaltungen erfahren, die von Gerda besucht
werden, dann schreiben wir:
Jede relevante Information wird genau einmal gespeichert; nur die Schlüssel Stud$Nr
und LV$Nr kommen mehrfach in besucht vor. Wir blenden datenbanktechnische
Hintergründe aus und nennen nur einige Vorteile dieser Datenbankmodellierung:
1. Wir sparen Speicherplatz. Zeichenketten können sehr lang sein und daher
extrem viel Speicherplatz beanspruchen.
2. Die Datenbank ist leichter aktualisierbar. Ändert sich der Titel einer Lehr-
veranstaltung, so brauchen wir ihn lediglich an einer Stelle ändern.
3. Die Konsistenz (Widerspruchsfreiheit) der Datenbank ist leichter gewährleis-
tet. Dies folgt unmittelbar aus dem vorherigen Punkt.
260 D Datenstrukturen
Unsere Aufgaben:
1. Gib die Titel all jener Lehrveranstaltungen aus, die von Gerda besucht werden.
Finde zwei Varianten, eine mit merge() und eine ohne merge().
2. Gib die Titel jener Lehrveranstaltungen aus, die von mindestens einer Person
besucht werden. Finde nach Möglichkeit zwei Varianten, eine mit merge() und
eine ohne merge().
3. Erstelle eine Häufigkeitstabelle, die angibt, wie viele Lehrveranstaltungen jede
Person besucht. Es soll jede Person in der Häufigkeitstabelle vorkommen.
4. Erstelle für jeden der folgenden Fälle ein Dataframe, das die Matrikelnummern,
die Namen, die LV-Nummern und die LV-Titel enthält.
a) Nur aktive Studierende aber alle Lehrveranstaltungen sollen vorkommen.
b) Nur aktive Studierende und LVs, die von mind. einer Person besucht
werden, sollen vorkommen.
c) Alle Studierenden und alle LVs sollen vorkommen.
Welcher Join kommt in jedem der Fälle zur Anwendung?
5. Isabella möchte auch noch die LV Spieltheorie besuchen. Modifiziere das Data-
frame besucht, sodass Isabellas Wunsch in Erfüllung geht. Suche dabei auto-
matisiert nach der korrekten Matrikelnummer und der korrekten LV-Nummer!
Die erste Aufgabe haben wir oben schon ohne merge() gelöst. Gleich schauen wir
uns eine Variante mit merge() an. Die anderen Aufgaben überlassen wir dir als
Übung in Übungsaufgabe 1 auf der nächsten Seite.
> res$Titel
[1] "Statistik" "Programmieren"
In unserem Fall selektieren wir aus Stud zuerst die Zeile, die zu Gerda gehört
(Stud.gerda). Damit ersparen wir uns die Mitnahme von drei unnötigen Zeilen,
wodurch die folgenden beiden merge()-Aufrufe deutlich schneller und speicherscho-
nender ablaufen.
21 Dataframes verknüpfen 261
21.4 Abschluss
• Was ist ein Inner Join, Outer Join, Left Join und Right Join? Wie setzen wir
diese Joins mit R um? Was ist ein Schlüssel und wie definieren wir in merge()
den Schlüssel bzw. die Schlüssel beider zu verknüpfenden Tabellen? Wie wird
der Schlüssel in merge() standardmäßig bestimmt? (21.1)
• Wie verknüpfen wir zwei Dataframes zeilenweise? Welche Voraussetzungen
müssen die Dataframes hinsichtlich der Beschriftungen erfüllen? (21.2)
In (21.3.1) haben wir eine kleine Beispieldatenbank betrachtet, die andeutet, wie
Verknüpfungen in der Praxis eingesetzt werden. Wir achten bei der Verknüpfung
von Dataframes darauf, dass die Tabellen möglichst klein bleiben.
21.4.2 Übungen
22 Textmanipulation: Stringfunktionen
Die Anwendungspalette der Werkzeuge dieses Kapitels ist riesig! Beim Textmining
geht es darum, wertvolle Informationen aus unstrukturierten Texten herauszufiltern,
was in der Praxis oft sehr herausfordernd ist. Wir betrachten in diesem Kapitel
zunächst die Grundlagen, ehe wir in (23) ans Eingemachte gehen und lernen, wie
wir die Werkzeuge richtig gut verwenden können.
Wir betrachten in diesem Kapitel eine kleine Speisekarte einer Pizzeria, die wir in
der Datei Pizza.RData unter dem Objekt pizza finden.
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
Unser Ziel: Wir wollen aus dem Objekt pizza die Namen, Zutaten und Preise
der Pizzen extrahieren und eine mit den (schön geschriebenen) Namen der Pizzen
beschriftete Liste mit den entsprechenden Zutaten erstellen.
Die Hilfsobjekte namen und zutaten erstellen wir dabei im Laufe des Kapitels.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_22
22 Textmanipulation: Stringfunktionen 265
• Welche Pizzen enthalten Mais? Auf welchen Pizzen ist Schinken oben? (22.2)
• Woran erkennen wir (automatisiert), wo die Namen, Zutaten und Preise im
Text platziert sind? Wie teilen wir das R mit und wie extrahieren wir diese
Informationen? (22.2.2), (22.3), (22.4)
Funktion Bedeutung
nchar(x) zählt die Anzahl der Zeichen von x
tolower(x) wandelt alle Buchstaben in x in Kleinbuchstaben um
toupper(x) wandelt alle Buchstaben in x in Großbuchstaben um
abbreviate(x) nette Funktion zur Abkürzung von Wörtern; sehr nützlich vor
allem zur Beschriftung in Grafiken mit Platzmangel.
Beispiel: Oft passieren bei der Eingabe unhübsche Dinge. Die Feststelltaste ist eine
der vielen möglichen Ursachen dafür. Nehmen wir mal an, wir wären schon bei (22.3)
und hätten die (unschön) geschriebenen Namen der Pizzen extrahiert.
> namen.upper
[1] "MARGHERITA" "VALENTINO" "CARDINALE" "PROVINCIALE"
> namen.lower
[1] "margherita" "valentino" "cardinale" "provinciale"
266 E Tools für Data Science und Statistik
Mit toupper() und tolower() haben wir immerhin schon mal die Ästhetik der
Wörter verbessert. Zur vollen Blüte treiben wir das Ganze dann in (22.3), wenn wir
den ersten Buchstaben groß und die restlichen Buchstaben klein schreiben. Schauen
wir uns noch kurz die beiden anderen Funktionen an.
Beispiel: Wir betrachten das Objekt namen.lower des vorangehenden Beispiels.
> namen.lower
[1] "margherita" "valentino" "cardinale" "provinciale"
1. Welcher Name ist der längste (hat die meisten Buchstaben bzw. Zeichen)?
provinciale hat 11 Zeichen und ist damit der längste der Namen.
2. Kürze alle Namen derart ab, dass sie noch mindestens 4 Zeichen haben und
eindeutig sind.
Der Vektor ist beschriftet, sodass die Originalwörter abrufbar bleiben. Wür-
de die Funktion abbreviate() keine eindeutigen Abkürzungen mit 4 Zeichen
finden, so würde sie wegen der Standardeinstellung strict = FALSE mehr Zei-
chen für die Abkürzungen verwenden.
Wenn wir in einem Text bzw. in einem Stringvektor nach einem Suchmuster (engl.
pattern) wie etwa dem Wort "Mais" oder dem Zeichen ":" suchen, dann gibt es im
Wesentlichen zwei zentrale Fragen, die wir uns stellen könnten:
• Welche Elemente des Stringvektors enthalten das Suchmuster? Wir finden al-
so alle Komponenten des Vektors, die das Suchmuster (mindestens ein Mal)
enthalten. (22.2.1)
• An welcher Stelle bzw. welchen Stellen beginnt das Suchmuster im Text? Wir
finden also in jeder Komponente des Vektors die Position des Zeichens bzw.
der Zeichen, an der das Suchmuster beginnt. (22.2.2)
22 Textmanipulation: Stringfunktionen 267
Tabelle 22.2: Ausgewählte Parameter der Funktionen grepl() und grep(). Mit
(*) markierte Parameter stehen nur in grep() zur Verfügung.
Parameter Bedeutung
pattern Das Suchmuster, für das wir uns interessieren.
x Der zu durchsuchende Stringvektor bzw. Text
ignore.case Steuert, ob die Groß- und Kleinschreibung ignoriert werden soll
(TRUE) oder nicht (FALSE, Standard).
value (*) Sollen Indizes (value = FALSE, Standard) oder die Einträge
selbst (value = TRUE) zurückgegeben werden?
invert (*) TRUE: Es werden jene Indizes/Elemente zurückgegeben, die das
Suchmuster pattern nicht enthalten. FALSE ist Standard.
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
Nur 1 Pizza. Wir erkennen, dass grepl() einen logischen Vektor zurückgibt, während
grep() einen Vektor mit Indizes retourniert. grep() kann aber noch etwas mehr,
wie wir gleich sehen.
Beispiel: Selektiere aus pizza all jene Komponenten, die Mais enthalten bzw. jene
Komponenten, die nicht Mais enthalten.
Dank value = TRUE ist es keine große Sache, die Elemente zu selektieren. Mit
invert = TRUE selektieren wir jene Elemente, die das Suchmuster nicht enthalten.
Beispiel: Welche Pizzen enthalten keinen Schinken?
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
Ein Vegetarier bestellt sich eine Pizza Valentino und ist schwer enttäuscht! Da ist ja
(Roh)schinken drauf!
Frage: Warum findet R Rohschinken nicht und wie modifizieren wir die Suche?
22 Textmanipulation: Stringfunktionen 269
Wir erinnern uns: R unterscheidet zwischen Groß- und Kleinschreibung, ist also case
sensitive (vgl. (10.4)). Das bedeutet in diesem Fall: Schinken ist nicht dasselbe wie
schinken.
Tipp: Kontrolliere deine R-Ergebnisse immer auf Plausibilität!
Jetzt haben wir unter anderem folgende Möglichkeiten:
2. Wir suchen einerseits nach Schinken und andererseits nach schinken und
verodern die Information.
> bool <- grepl(pattern = "Schinken", x = pizza) |
+ grepl(pattern = "schinken", x = pizza)
> bool
[1] FALSE TRUE TRUE TRUE
3. Wir weisen R an, die Groß- und Kleinschreibung zu ignorieren. Dies geschieht
mit Hilfe von ignore.case = TRUE.
> bool <- grepl(pattern = "Schinken", x = pizza, ignore.case = TRUE)
> bool
[1] FALSE TRUE TRUE TRUE
> pizza[!bool]
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
Unser Vegetarier bestellt sich also eine Pizza Margherita und ist zufrieden :-)
Bemerkung: Findige R-Nachwuchstalente bemängeln, dass es Pizzen geben könnte,
die zum Beispiel Speck aber keinen Schinken enthalten und daher einen Vegetarier
nicht glücklich machen. Wenn du zu diesen Personen zählen solltest, dann hast du
völlig recht damit und bist herzlich eingeladen, diesen Mangel auszubessern!
Die Funktionen grepl() und grep() liefern uns die Information, ob ein Muster im
Text vorkommt bzw. in welchen Elementen eines Vektors ein Muster vorkommt.
Oft benötigen wir jedoch die präzisere Information, an welcher Stelle (bei welchem
Zeichen) im Text ein Suchmuster beginnt. Genau diese Information liefert uns die
Funktion regexpr().
270 E Tools für Data Science und Statistik
Weiter unten lernen wir die Funktion gregexpr() kennen, die uns im Gegensatz zu
regexpr() alle Stellen, an denen im Text ein Suchmuster beginnt, zurückgibt. Für
die Bedeutung der Parameter verweisen wir auf Tab. 22.2 auf Seite 267.
Vorsicht: Statt x verwendet regexpr() den Parameternamen text!
Beispiel: An welcher Stelle beginnt im Vektor pizza das Muster Schinken?
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
Die Funktion gibt uns einen Vektor zurück. Dabei bedeutet -1, dass in der entspre-
chenden Zeichenkette Schinken nicht vorkommt. Wird eine Zahl > 0 zurückgegeben,
so ist das die Stelle des ersten Auftretens von Schinken. Die 28 (3. Eintrag) deu-
tet etwa an, dass im 3. Element von pizza das Muster Schinken beim 28. Zeichen
anfängt. Gerne darfst du per Hand nachzählen ;-)
Außerdem sehen wir unter match.length, wie lang die Übereinstimmungen (die
Matches) jeweils sind. Da Schinken aus genau 8 Zeichen besteht, ist es nicht verwun-
derlich, dass zwei Mal eine 8 ausgegeben wird ;-). In (23) definieren wir Suchmuster
variabler Längen, dann wird es spannender!
Bemerkung: match.length ist ein sogenanntes Attribut. Wie wir auf Attribute
zugreifen können, schauen wir uns in (26) an.
Wenn wir darüber hinaus auch Rohschinken matchen wollen, dann können wir wie-
derum den Parameter ignore.case auf TRUE setzen.
Aus dem Output von regexpr() können wir ganz schnell den Output von grepl()
konstruieren. regexpr() ist daher anwendungsmöglichkeitentechnisch eine Verallge-
meinerung von grepl().
Wollen wir nicht nur die erste Übereinstimmung, sondern alle Übereinstimmun-
gen haben, so nehmen wir gregexpr(). Der Aufruf ist derselbe; einziger feiner
Unterschied im Output: Es wird immer eine Liste zurückgegeben. Das ist sinnvoll,
da die Anzahl der Übereinstimmungen in den Elementen zumeist verschieden ist.
Beispiel: An welchen Stellen stehen im Vektor pizza jeweils die Beistriche?
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
Aus übersichtstechnischen Gründen betrachten wir nur die ersten beiden Elemente.
Die [[1]] und [[2]] zeigen uns, dass es sich um eine Liste handelt. Ansonsten
interpretieren wir den Output wie bei regexpr(). Die 20 und 30 sagen uns etwa,
dass im ersten Eintrag von pizza die Beistriche an den Stellen 20 und 30 stehen.
Zeit, um mal wieder unsere Aufmerksamkeit zu schulen! Ein Kollege schlägt folgen-
den Code vor, um im vorangehenden Beispiel auf dieser Seite die Beistriche jeder
Komponente zu zählen:
Frage: In welchem Fall zählt dieser Code nicht korrekt die Anzahl der Beistriche
pro Komponente? Wie korrigieren wir den Ansatz in diesem Fall? Nimm dir zwei
Minuten Zeit um Antworten auf diese Fragen zu finden, bevor du weiterliest!
Wir schauen uns an, was passiert, wenn ein Eintrag keinen Beistrich enthält.
In dem Fall wird -1 zurückgegeben. Mit obigem Code würden wir aber die -1 mit-
zählen, sodass statt 0 das Ergebnis 1 herauskommt.
Erinnern wir uns an Regel 6 auf Seite 57: Hinterfrage kritisch deine (impliziten)
Annahmen! Die implizite Annahme des Kollegen war, dass in jeder Komponente
zumindest ein Beistrich vorkommt.
Lösen wir diese Annahme jetzt mit einem Zwischenschritt auf, indem wir zunächst
überprüfen, ob eine Zahl größer als Null ist und anschließend die Anzahl der TRUE
pro Komponente zählen (vgl. (18.3))
Wenn wir uns lediglich für die Gesamtanzahl der Beistriche interessieren, können
wir auch kürzer folgendes schreiben:
gregexpr() gibt wirklich immer eine Liste zurück, selbst dann, wenn das Suchmus-
ter in jeder Komponente gleich oft vorkommt.
22 Textmanipulation: Stringfunktionen 273
Mit der Funktion substring() können wir Teile von Strings extrahieren.
Mit first und last steuern wir, von welchem Zeichen (inklusive) bis zu welchem
Zeichen (inklusive) wir gehen wollen. Eine tolle Nachricht: Das funktioniert auch
vektorwertig!
Im rechten Code extrahieren wir der Reihe nach die ersten zwei bis fünf Zeichen.
Jetzt wollen wir aus pizza die Namen extrahieren. Da die Namen allesamt ungleich
lang sind, brauchen wir eine Regel, die uns zuverlässig die Spreu vom Weizen trennt.
Bevor du das folgende Beispiel betrachtest, überlege dir: Mit welcher Regel können
wir zuverlässig alle Namen extrahieren?
Beispiel: Extrahiere alle Namen des Vektors pizza. Schreibe sie darüber hinaus
richtig (erster Buchstabe groß, der Rest klein).
Nach Betrachten von pizza stellen wir fest, dass folgende Regel in diesem Fall funk-
tioniert: Die Namen der Pizzen stehen jeweils ganz zu Beginn und werden durch
einen Doppelpunkt vom Rest des Strings getrennt.
Um die Namen korrekt zu extrahieren, führen wir also folgende Schritte aus:
1. Stelle des ersten Doppelpunktes finden
2. Teilbereich bis exklusive des ersten Doppelpunktes extrahieren
3. Namen richtig schreiben
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
274 E Tools für Data Science und Statistik
Wir könnten per Hand nachzählen und uns davon überzeugen, dass es passt, das
ist in der Praxis jedoch fast nie möglich. Daher führen wir zur Kontrolle einen
Plausibilitätscheck durch.
Alle Einträge von pizza enthalten (zumindest einen) Doppelpunkt und die Stellen
des jeweils ersten Doppelpunktes sehen plausibel aus. Sollte also passen.
Bei der Bestimmung von namen.rest wäre nchar(namen) nicht notwendig gewesen.
Aber so haben wir dieser coolen und nützlichen Funktion die Chance gegeben, sich
erneut zu präsentieren :-)
Bemerkung: Leider ist das Auffinden allgemeiner Regeln in der Praxis oft ein sehr
mühsames Unterfangen. Oft greift eine Regel wunderbar, allerdings nur in 90% aller
Fälle. Für die restlichen 10% müssen wir wiederum neue Regeln finden...
Um die Preise in diesem Fall zuverlässig zu selektieren, könnten wir zunächst nach
dem ersten Punkt suchen. Probieren wir es aus!
Die folgenden Sonderzeichen haben in der Welt der Stringsuche magische Kräfte:
. \ | ( ) [ { ^ $ * + ?
Der Punkt steht etwa für ein beliebiges Zeichen. Da regexpr() immer die Stelle der
ersten Übereinstimmung zurückgibt, wird also im letzten Code auf der vorherigen
Seite das erste Zeichen gematcht.
Wie wir uns der Magie dieser Sonderzeichen bedienen können, klären wir bei den
Regular Expressions (23). Für uns ist im Moment nur wichtig, dass wir diese Zeichen
besonders behandeln müssen, wenn wir explizit nach ihnen suchen möchten. Wir
betrachten zwei Möglichkeiten, die uns zur Verfügung stehen.
Jetzt passt es! In der Regel funktionieren bei der expliziten Suche nach obigen Son-
derzeichen beide Varianten, beim Backslash "\" und beim Potenzzeichen "^" gibt
es jedoch Ausnahmen, die wir in (23.1) erörtern.
Beispiel: Extrahiere aus pizza die Preisinformation.
Wir erkennen, dass die Preisinformation in diesem Fall unmittelbar nach dem ersten
Punkt folgt. Was für ein glücklicher Zufall, dass wir gerade gelernt haben, wie wir
nach Punkten suchen können ;-)
> # ... und Euro wollen wir am Ende auch nicht haben.
> preise <- substring(pizza, stelle.punkt + 2, nchar(pizza) - nchar(" Euro"))
> preise
[1] "6,00" "7,50" "7,50" "7,90"
Beachte: Die Annahme, dass am Ende immer Euro steht, kann evtl. nicht zutreffen.
Die Annahme, dass nach dem Punkt genau ein Leerzeichen kommt, bevor der Preis
beginnt, ist ebenfalls mit Vorsicht zu genießen. Und wenn der Punkt fehlt, dann sind
wir mit dieser Extraktionsregel komplett aufgeschmissen :-(
276 E Tools für Data Science und Statistik
Das soll uns aber nicht entmutigen, sondern uns im Gegenteil Motivationsschübe
für die kommenden Seiten dieses Buches liefern! Als kleinen Vorgeschmack auf (23)
zeigen wir dir einen Code, der die Preise in einer Codezeile extrahiert.
Mit sub() können wir ein Muster durch ein anderes ersetzen.
Dabei wird der erste pattern durch replacement im Vektor x ersetzt. Wollen wir
alle Vorkommen von pattern ersetzen, so erfüllt uns die völlig analog anwendbare
Funktion gsub() unseren Wunsch. Ein kleines Codebeispiel:
Beispiel: Wandle die Preise, die wir im Beispiel auf der vorherigen Seite erstellt
haben, in einen numerischen Vektor um.
> preise
[1] "6,00" "7,50" "7,50" "7,90"
> as.numeric(preise)
Warnung in eval(ei, envir) NAs durch Umwandlung erzeugt
[1] NA NA NA NA
Beachte: Nur bei pattern müssen wir die in (22.4) angeführten Sonderzeichen in
eckige Klammern stellen oder einen doppelten Backslash voranstellen. Schauen wir
uns an, was passiert, wenn wir das bei replacement (auch) tun.
Zeit für einen Zwischenstand! Bis jetzt haben wir die Namen und Preise extrahiert.
> namen
[1] "Margherita" "Valentino" "Cardinale" "Provinciale"
> preise
[1] 6.0 7.5 7.5 7.9
> pizza
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 Euro"
[3] "CARDINALE: Tomaten, Kaese, Schinken. 7,50 Euro"
[4] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
Die Extraktionsregel, die hier greift: Die Zutaten stehen zwischen dem ersten Dop-
pelpunkt und dem ersten Punkt. In (22.3) bzw. (22.4) haben wir die Stelle des ersten
Doppelpunktes bzw. ersten Punktes ermittelt.
In zwei Schritten erfüllen wir die Aufgabe:
1. Extrahiere den Teilstring zwischen dem ersten Doppelpunkt und ersten Punkt
(jeweils exklusive)
2. Lösche alle Leerzeichen
Jetzt wollen wir alle Leerzeichen löschen. Ein Fall für gsub(); sub() würde nur das
erste Leerzeichen löschen.
Jetzt wollen wir noch eine beschriftete Liste mit den Zutaten erstellen. Ein Fall für
strsplit() im nächsten Kapitel.
278 E Tools für Data Science und Statistik
Wir kommen zum grande Finale! Jetzt wollen wir eine Liste mit den Zutaten jeder
Pizza erstellen. Dazu spalten wir zutaten aus dem Beispiel auf der vorherigen Seite
bei jedem Komma auf.
> zutaten
[1] "Tomaten,Kaese"
[2] "Tomaten,Kaese,Champignons,Rohschinken,Ananas"
[3] "Tomaten,Kaese,Schinken"
[4] "Tomaten,Kaese,Schinken,Speck,Mais,Pfefferoni"
Dabei ist x ein Vektor von Zeichenketten, der bei jedem Auftreten des Suchmusters
in split getrennt werden soll.
Fakten über strsplit() und split:
• Spannend: Wollen wir den Vektor x in seine einzelnen Zeichen aufspalten, so
setzen wir split = "", also auf einen Leerstring.
• Wichtig: Wollen wir nach einem Punkt splitten, so müssen wir (wie beim
Parameter pattern) split = "[.]" oder split = "\\." setzen. Selbiges gilt
auch für die anderen in (22.4) angeführten Sonderzeichen.
• Wichtig: strsplit() gibt uns immer eine Liste zurück!
Beispiel: Erstelle aus zutaten eine mit den Namen der Pizzen beschriftete Liste,
welche den Vektor mit den jeweiligen Zutaten enthält.
Jetzt noch zu den Beschriftungen. Dazu verwenden wir einfach die Namen, die wir
in (22.3) erstellt haben.
22 Textmanipulation: Stringfunktionen 279
Voilà. An dieser Stelle würde es sich noch anbieten, den Kreis des Beispiels zu
schließen. Also mit Hilfe der Objekte pizza.liste und preise den Vektor pizza
zu rekonstruieren. Das überlassen wir aber lieber dir in Übung 1 ;-) Viel Spaß!
Funktion Bedeutung
paste(..., sep, collapse) Zeichenketten verknüpfen
paste0(..., collapse) wie paste() mit sep = ""
nchar(x) Anzahl der Zeichen zählen
tolower(x) generelle Kleinschreibung
toupper(x) generelle Großschreibung
abbreviate(names.arg, minlength) Abkürzungen erzeugen
grepl(pattern, x, ignore.case) suchen: return logischer Vektor
grep(pattern, x, ignore.case, suchen: return Indizes / Elemente
value, invert)
regexpr(pattern, text, ignore.case) suchen: erste Stelle des Matches
gregexpr(pattern, text, ignore.case) suchen: alle Stellen der Matches
substring(text, first, last) Teilstring extrahieren
sub(pattern, replacement, x) ersetze ersten Match
gsub(pattern, replacement, x) ersetze alle Matches
strsplit(x, split) spaltet x nach split auf
Außerdem bieten alle Funktionen, in denen die pattern und split vorkommen,
auch den Parameter perl an, den wir in (23) kennenlernen.
280 E Tools für Data Science und Statistik
Alle Sonderzeichen aus (22.4), die bei der expliziten Suche speziell behandelt wer-
den müssen:
. \ | ( ) [ { ^ $ * + ?
22.8 Abschluss
• Wie zählen wir die Anzahl der Zeichen eines Strings? Wie wandeln wir Buch-
staben in Groß- bzw. Kleinbuchstaben um? (22.1)
• Wie bestimmen wir, ob ein Suchmuster in einem Text vorkommt? Was ist dabei
der Unterschied zwischen grepl() und grep()? Wie können wir die Groß- und
Kleinschreibung ignorieren und was steuern wir mit den Parametern value und
invert in grep()? (22.2.1)
• Wie bestimmen wir, an welcher Stelle im Text ein Suchmuster beginnt? Was
ist dabei der Unterschied zwischen regexpr() und gregexpr()? Welche Da-
tenstrukturen kommen bei beiden Funktionen heraus und was sagt uns jeweils
der Output? (22.2.2)
• Wie zählen wir korrekt, wie oft ein Suchmuster in jeder Komponente eines
Stringvektors vorkommt? (22.2.2)
• Wie extrahieren wir Teile aus Zeichenketten? (22.3)
• Welche beiden Möglichkeiten haben wir, wenn wir explizit nach bestimmten
Sonderzeichen (wie etwa dem Punkt) suchen wollen? Welche 12 Zeichen gelten
als Sonderzeichen? (22.4)
• Wie ersetzen wir das erste bzw. alle Vorkommen eines Suchmusters durch einen
Ersetzungsstring? Was passiert, wenn wir im Ersetzungstext Sonderzeichen in
eckige Klammern setzen oder zwei Backslashes voranstellen? (22.5)
22.8.3 Ausblick
Die hier betrachteten Funktionen sind der Grundpfeiler für eine erfolgreiche Text-
manipulation. Du kannst bereits viele einfache Anwendungen erfolgreich ausführen
und dennoch wirst du merken, dass du (noch) schnell an Grenzen stößt. Einige Lö-
sungsansätze dieses Kapitels wären gescheitert, wenn bestimmte Voraussetzungen
nicht erfüllt gewesen wären, wie etwa, dass vor dem Preis immer ein Punkt steht. In
(23) lernen wir fortgeschrittene und flexible Suchmuster kennen und erfahren, wie
wir mit solchen Herausforderungen umgehen können.
In (26) lernen wir, was Attribute sind und wie wir auf diese zugreifen. Damit kön-
nen wir auf die Matchlängen von regexpr() und gregexpr() zugreifen und in
Verbindung mit den Anfangsstellen der Übereinstimmungen und substring() ge-
zielt Teile des Textes extrahieren. Schon in (23) lernen wir die Extraktionsfunktion
regmatches() kennen, die das für uns übernimmt.
Stringfunktionen kommen nicht nur im Textmining vor. Auch in der Datenaufberei-
tung oder etwa bei der Selektion von bestimmten Spalten von Dataframes finden sie
ihre Anwendung. Mehr über das Einlesen von Daten und Datenaufbereitung erfahren
wir in (32).
22.8.4 Übungen
1. Rekonstruiere nur mit Hilfe der Objekte pizza.liste und preise das Objekt
pizza.
2. Wir betrachten den Vektor namen dieses Kapitels. Enden alle Pizzanamen auf
einem Selbstlaut?
3. Es war einmal vor sehr sehr vielen Jahren, da konnten Dateinamen aus höchs-
tens 8 Zeichen bestehen. Bestand der Name aus mehr als 8 Zeichen, so wurde
einfach nach der 6. Stelle abgeschnitten und eine 1 angehängt.2
So wurden beispielsweise die Wörter
"Baumhaus" "Reihenhaus" "Stiege" "Stiegenhaus" "Stiegenbauer"
zu folgenden Wörtern umfunktioniert:
"Baumhaus" "Reihen~1" "Stiege" "Stiege~1" "Stiege~1"
a) Schreibe einen Code, der für beliebige Wörter die oben beschriebene Um-
wandlung bewirkt.
b) Überprüfe, ob das Tilde-Zeichen bei all jenen Wörtern, bei denen eine
Abkürzung notwendig war, an 7. Stelle steht.
2 Gabes mehrere Dateien, die mit denselben 6 Buchstaben anfingen, wurde eine fortlaufende
Nummerierung genommen. Dieses Detail blenden wir aber hier aus.
282 E Tools für Data Science und Statistik
4. Gegeben ist der Vektor text, der drei kurze Texte enthält:
text <- c("R ist super. Mit R kann ich so viele tolle Dinge tun.",
"Zeichenketten aufsplitten ist ein Beispiel.",
"Vektoren umdrehen und das erste Element selektieren.")
Dein Code muss auch dann funktionieren, wenn text aus mehr als drei Ein-
trägen besteht!
Hinweis: Überprüfe deine Ergebnisse auf Richtigkeit! Punkte und Leerzeichen
zählen zum Beispiel nicht als Buchstabe ;-)
5. Betrachte beispielhaft folgende Dateinamen:
Extrahiere die Dateiendungen (alles nach dem letzten Punkt). Schreibe deinen
Code so, dass er für beliebige Dateinamen funktioniert.
Hinweis: Die Aufgabe ist mit den bisherigen Techniken lösbar, wenngleich es
etwas trickreich ist. Vielleicht versteckt sich in einer vorangehenden Aufgabe
ein kleiner Hinweis ;-). Solltest du noch nicht auf eine Lösung kommen, verzage
nicht: Spätestens am Ende von (23) findest du einen Weg!
6. Sei x ein beliebiger Vektor vom Mode character. Was tut folgender Code?
Finde eine Alternative, die genau dasselbe tut!
ifelse(substring(x, 1, 1) == " ", substring(x, 2, nchar(x)), x)
Wenn wir nach festen Wörtern suchen möchten, so kommen wir mit dem Wissen aus
(22) aus. Für viele Aufgaben des Textminings reicht unser Wissen jedoch noch nicht
aus. Wie extrahieren wir zum Beispiel (ganz bestimmte) Zahlen aus einem Text?
Um Aufgaben wie diese erfolgreich zu meistern, benötigen wir flexible Suchmuster.
In diesem Kapitel tauchen wir tiefer ein in die Welt der Textsuche und Suchmuster.
Anhand des Vektors namen, der einige (mehr oder weniger gängige) Namen enthält,
entwickeln wir ein Gespür für die Entwicklung flexibler Suchmuster.
• Wie oft kommt in jedem Namen das Muster „Selbstlaut, Mitlaut, Selbstlaut“
vor? Wie gehen wir dabei mit etwaigen Überlappungen um? (23.6.3)
Obwohl der Vektor namen überschaubar ist und die Fragen harmlos wirken, stoßen
wir auf Tücken bei der Textsuche und diskutieren Lösungsmöglichkeiten. Daneben
schauen wir uns viele weitere praxisrelevante Fragestellungen an und begegnen span-
nenden Herausforderungen. Und in den abschließenden Übungsaufgaben bekommst
du die Chance zu zeigen, was du drauf hast! Klingt wie eine Abenteuerreise :-)
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_23
284 E Tools für Data Science und Statistik
. \ | ( ) [ { ^ $ * + ?
Unter ?regex finden wir alle Details dazu. Im Laufe dieses Kapitels schauen wir uns
an, welche besonderen Kräfte in diesen Sonderzeichen stecken.
Wenn wir explizit nach einem Sonderzeichen suchen wollen, so müssen wir
> index.p
[1] 4
.....
Im ersten Code deaktivieren die beiden Backslashes die magische Wirkung des fol-
genden Zeichens ("\\."), im zweiten Code werden die magischen Wirkungen aller
Zeichen, die in eckigen Klammern stehen, abgeschaltet ("[.]").
Doch Vorsicht ist geboten, es gibt Ausnahmen bei der expliziten Suche nach
bestimmten Zeichen:
Bevor wir uns Codebeispiele dazu anschauen: Hast du dir eigentlich schon mal die
Frage gestellt, wie wir in Strings ein Anführungszeichen darstellen können?
Das funktioniert so nicht, weil wir ja die Anführungszeichen zur Definition eines
Strings brauchen. Ein vorangestellter Backslash schafft Abhilfe!
Die Funktion cat() druckt einen String auf die Console (oder in eine Datei). Mit \n
markieren wir einen Zeilenumbruch. Wir gehen in (31) genauer darauf ein. Beachte,
dass die Backslashes nach dem Drucken auf die Console verschwinden.
Bei der Darstellung eines Backslashes in Texten verhält es sich gleich. Wir
müssen dem eigentlichen Backslash einen anderen Backslash voranstellen. Beachte,
dass \" und \\ jeweils ein Zeichen darstellen!
Die erste Abfrage sucht explizit nach dem Escape-Befehl \n, der eine neue Zeile
beginnt. Die zweite Abfrage wirkt wild, ist aber leicht zu interpretieren! Die ers-
ten beiden Backslashes heben die Wirkung des nächsten Zeichens auf. Das nächste
Zeichen ist aber gerade ein Backslash (in R durch zwei Backslashes dargestellt).
Mit unserem bisherigen Wissen können wir bereits nach einfachen Mustern suchen.
So können wir etwa aus namen all jene Namen extrahieren, die ein großes A oder das
Muster Anne enthalten.
> namen
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"
[6] "Anne" "Aurelia" "Elena" "Eugen" "Ida"
[11] "Freia" "Maaouiya" "Marie-Anne" "Otto" "Renee"
Beachte, dass wir nicht nach Namen gesucht haben, die mit A oder Anne beginnen!
Marie-Anne bestätigt das ;-)
Mit dem Punkt "." markieren wir in einem Muster ein beliebiges Zeichen.
> # Alle Namen mit A gefolgt von beliebigem Zeichen gefolgt von a
> grep("A.a", namen, value = TRUE)
[1] "Adam" "Ana"
Bei Adam wird das d und bei Ana wird das n für das beliebige Zeichen eingesetzt.
Alle anderen Namen enthalten das Suchmuster nicht.
23 Komplexe Textsuchmuster: Regular Expressions 287
Oft wollen wir nach einem Zeichen aus einer Zeichenmenge suchen. Das be-
werkstelligen wir, indem wir diese Zeichen in eckige Klammern setzen.
Für [ae] wird also je nach Bedarf entweder ein a oder e eingesetzt.
Selbstverständlich können wir mehrere Zeichenmengen aneinanderhängen. Im folgen-
den Code kombinieren wir einen großen Selbstlaut mit einem beliebigem Zeichen.
Wollen wir Zeichen einer Zeichenmenge ausschließen, so stellen wir einfach ein
Potenzzeichen (ˆ) vor diese Zeichen. So steht etwa im folgenden Code [ˆd] für ein
beliebiges Zeichen außer d.
> # Großer Selbstlaut gefolgt von einem Zeichen außer d gefolgt von a
> grep("[AEIOU][^d]a", namen, value = TRUE)
[1] "Ana"
Wir sehen: Adam wird nicht mehr gematcht, Ida ebenso nicht mehr.
Beispiel: Wie viele Vokale (Selbstlaute) kommen in jedem Namen vor?
Eine Lösungsmöglichkeit: Wir definieren mit [aeiou] die Menge aller (kleingeschrie-
benen) Selbstlaute, suchen alle Stellen, an denen diese Zeichen vorkommen und be-
stimmen die Anzahl. Wie wir diese Idee umsetzen können, haben wir uns bereits in
(22.2.3) angesehen. Da wir die Groß- und Kleinschreibung ignorieren wollen, setzen
wir ignore.case = TRUE.
Wir überzeugen uns stichprobenartig per Hand davon, dass Annabelle tatsächlich
4 und Anna-Maria tatsächlich 5 Selbstlaute hat.
> head(namen, 5)
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"
Wir hätten diese Frage auch schon mit den Techniken des vorangehenden Kapitels
(22) beantworten können:
Da wir über das Ausschließen von Zeichen gesprochen haben, drängt sich eine nahe-
liegende Frage auf.
Frage: Bestimmt der Code gregexpr("[ˆaeiou]", namen, ignore.case = TRUE)
die Stellen aller Konsonanten (Mitlaute)?
Bevor du weiterliest: Überlege dir deine Antwort, um sie dann mit dem folgenden
Code abzugleichen!
Kommen wir zur Auflösung!
Nicht ganz: Bei Anna-Maria wird das Minuszeichen mitgezählt. Um alle Konsonan-
ten zu zählen, ist es also (trotz erhöhten Tippaufwands) sicherer, explizit mittels
[bcdfghjklmnpqrstvwxyz] nach ihnen zu suchen.
> rechnung
[1] "3 + 2 ^ 4 - 1 = 18"
> gregexpr("[3-9]", rechnung)[[1]] # [[1]], weil nur eine Komponente
[1] 1 9 18
.....
Mit [3-9] matchen wir alle Ziffern zwischen 3 und 9, also insbesondere Ziffern größer
oder gleich 3. Dass die 8 an 18. Stelle steht, ist Zufall.
In der Hilfe zu Regular Expressions (?regex) finden wir weitere vordefinierte Zei-
chenbereiche wie etwa alphanumerische Zeichen oder Satzzeichen.
Angenommen, wir wollen all jene Namen selektieren, die drei aufeinanderfolgende
Vokale haben. Dann ginge es beispielsweise wie folgt.
Mit [aeiou] bezeichnen wir die Menge aller Selbstlaute (Vokale). Da wir drei Selbst-
laute in Folge wollen, schreiben wir diese Menge drei Mal hintereinander auf. Ange-
hende R-Profis merken aber schnell: Das muss doch kürzer gehen!
Mit Hilfe von Matchlängenoperatoren können wir einstellen, wie oft das Zeichen
(oder die Zeichenmenge) unmittelbar davor gematcht werden soll. Und zwar:
• {k}: genau k Mal
• {a,b}: zwischen a und b Mal
• {a,}: mindestens a Mal
• {,b}: höchstens b Mal
Beachte: Vermeide es, innerhalb der geschwungenen Klammern Leerzeichen zu ver-
wenden! Die Matchlängenoperatoren können damit zumeist nicht umgehen.
290 E Tools für Data Science und Statistik
Beispiel: Suche nach allen Namen, die Ana oder Anna enthalten.
Das {1,2} im ersten Code besagt, dass das n direkt davor ein bis zwei Mal gematcht
wird. Das ? im zweiten Code bedeutet, dass das (zweite) n optional ist.
Beispiel: Welche Namen enthalten mindestens zwei aufeinanderfolgende Vokale?
Mit {2,} sagen wir, dass das Zeichen davor mindestens zwei Mal vorkommen muss, in
diesem Fall zwei Mal in Folge ein Zeichen der Menge [aeiou]. Mindestens zwei Mal
heißt aber auch: Ein Mal plus mindestens ein weiteres Mal, was wir mit + umsetzen.
Oder zwei Mal und mindestens 0 weitere Male, was wir mit * kennzeichnen. Da
Namen auch mit zwei Selbstlauten beginnen können und der erste Buchstabe groß
geschrieben wird, setzen wir ignore.case = TRUE.
Frage: Wie filtern wir all jene Namen heraus, die genau zwei aufeinanderfolgende
Vokale haben?
Im ersten Reflex würden viele Folgendes versuchen:
Es kommen dieselben Namen heraus wie im letzten Beispiel. Für R ist es näm-
lich irrelevant, was nach den beiden gefundenen Selbstlauten kommt. Freia und
Maaouiya haben nicht genau zwei Vokale in Folge und rutschen durch. So können
wir das Beispiel also noch nicht lösen, in (23.5.3) reparieren wir den Code!
23 Komplexe Textsuchmuster: Regular Expressions 291
R verfolgt das Konzept des greedy Matchings. Das heißt, R dehnt das Suchmuster
so weit wie möglich aus, sodass möglichst viele Zeichen gematcht werden.
Wollen wir dieses gierige Verhalten abschalten, so stellen wir dem Matchlängen-
operator ein Fragezeichen nach. Betrachten wir ein einfaches Codebeispiel.
Im linken Code wird das beliebige Zeichen wegen .+ so lange wie möglich gedehnt.
Das Suchmuster deckt den ganzen String ab, da die mittleren x ja auch beliebige
Zeichen sind. Wir gehen also vom ersten x bis zum letzten x.
Der rechte Code hingegen geht nur vom ersten x bis zum zweiten x. Möglich macht
dies das Fragezeichen, das dem Pluszeichen davor die Anweisung gibt, möglichst
wenige Zeichen zu matchen.
> x <- c("Heute -20%", "Er hat 20.25 Punkte.", "Viele Meter laufen",
+ "Kalte -4.5 Grad")
Wir können zwar mit regexpr() (bzw. gregexpr()) gepaart mit einem geeigneten
Suchmuster herausfinden, an welchen Stellen die Zahlen beginnen und über wie viele
Stellen sie sich erstrecken (Attribut match.length), doch uns fehlt derzeit noch das
Mittel, um auf Attribute zuzugreifen. Das holen wir in (26) nach.
Eine deutlich bequemere Möglichkeit ist die Funktion regmatches(), mit der wir
Teile aus einem String extrahieren können, basierend auf der Information, die
uns regexpr() bzw. gregexpr() zur Verfügung stellt.
Wir übergeben dabei regmatches() einen Stringvektor x und für m den Output von
regexpr() oder gregexpr().
292 E Tools für Data Science und Statistik
Beispiel: Extrahiere aus dem Vektor x auf der vorherigen Seite alle Zahlen.
> x
[1] "Heute -20%" "Er hat 20.25 Punkte." "Viele Meter laufen"
[4] "Kalte -4.5 Grad"
Schritt 1: Wir überlegen uns, wie wir im Vektor x eine Zahl überhaupt korrekt und
vollständig erkennen und übersetzen unsere Erkenntnisse in ein Suchmuster.
Eine Zahl besteht aus einem optionalen Minuszeichen sowie mindestens einer be-
liebigen Ziffer. Danach schließt sich noch optional ein Dezimalpunkt an gefolgt von
gegebenenfalls beliebig vielen (mindestens Null) weiteren Ziffern.
Bemerkung: Die Zahl "1." ist eine Abkürzung für "1.0". Das ist aber nicht der
einzige Grund, warum wir * genommen haben (siehe Übung 5 auf Seite 308).
Schritt 2: Dieses Suchmuster setzen wir in regexpr() ein.
Der String "Viele Meter laufen" enthält keine Zahl! Der Output -4.5 gehört
eigentlich zum 4. Eintrag von x; die Indizes haben sich also verschoben.
Frage: Wie wäre es, wenn wir mit einem NA markieren würden, dass an 3. Stelle
keine Zahl vorkommt?
Bevor du weiterliest, versuche selbst einen Code zu entwickeln, der diese Aufga-
be meistert! Vielleicht erinnert dich die Aufgabenstellung ja an die Erstellung von
lückenlosen Häufigkeitstabellen (vgl. (12.2.4)) ;-)
Schritt 4: (optional). Markiere all jene Stellen, die das Suchmuster nicht enthalten
mit einem fehlenden Wert (NA).
> # Jene Stellen mit NA markieren, in denen das Suchmuster nicht vorkommt
> res.zahl.na <- rep(NA, length(x))
> res.zahl.na[stelle.zahl > 0] <- res.zahl
> res.zahl.na
[1] "-20" "20.25" NA "-4.5"
Das NA an dritter Stelle sagt uns jetzt, dass im 3. Eintrag von x ("Viele Meter
laufen") keine Zahl vorkommt.
23 Komplexe Textsuchmuster: Regular Expressions 293
Bei der Definition von Suchmustern müssen wir aufpassen, dass wir nicht zu viel ex-
trahieren! Was passiert etwa, wenn sich zu x ein Datumstext dazugesellt (x.datum)?
> x.datum <- c("Heute -20%", "Samstag, der 04.01.2019", "Viele Meter laufen",
+ "Er hat 20.25 Punkte.", "Kalte -4.5 Grad")
Das Datum rutscht leider durch. Eine logisch klingende Möglichkeit, um ein Datum
auszuschließen: Nach der Dezimalzahl schließen wir einen weiteren Punkt aus.
Klingt zwar logisch, aber es funktioniert nicht. Nimm dir eine Minute Zeit, eine
Antwort auf folgende Frage zu finden!
Frage: Warum funktioniert dieser Ansatz nicht, obwohl er so logisch klingt?
Abschließend stellen wir noch fest, dass wir von regmatches() immer eine Liste
zurückbekommen, wenn wir den Output von gregexpr() hineinstecken.
> x
[1] "Heute -20%" "Er hat 20.25 Punkte." "Viele Meter laufen"
[4] "Kalte -4.5 Grad"
Wir sehen, dass jene Stellen, an denen das Suchmuster nicht vorkommt, mit einem
leeren Vektor des Typs character markiert werden.
Bis jetzt haben wir uns angesehen, wie wir einzelne Zeichen definieren und deren
Matchlänge steuern können. Wir können aber auch mehrere Zeichen zu einer
Zeichengruppe zusammenfassen, indem wir sie in runde Klammern stecken.
Nein, beides ;-). Mit (le) definieren wir eine Zeichengruppe, die aus den Zeichen l
und e besteht. Das Fragezeichen danach besagt, dass diese Zeichengruppe optional
ist. Somit wird sowohl Fisch, als auch Fleisch gefunden.
Beispiel: Welche Namen enthalten (mindestens) zwei Mal in Folge einen Selbstlaut
gefolgt von einem Mitlaut?
Auf Seite 288 haben wir uns schon überlegt, warum [ˆaeiou] nicht ideal ist, um
nach einem Mitlaut zu suchen.
23 Komplexe Textsuchmuster: Regular Expressions 295
Nach (23.5.3) wird dir klar, warum dieser Output herauskommt. Es gibt einige Work-
arounds dafür, lass dich von den restlichen Abschnitten inspirieren ;-)
Anhand der Frage, ob das ganze Fisch oder Fleisch ist, schauen wir uns an, wie wir
Zeichenmuster verodern können. Folgender Code wirkt lang und schwerfällig.
> x
[1] "Fisch" "Fleisch"
Die Oder-Verknüpfung funktioniert aber auch innerhalb von Suchmustern, was uns
deutlich kürzere Schreibweisen ermöglicht!
Der linke Code sucht explizit nach Fisch oder Fleisch. Der rechte Code sucht nach
einem F gefolgt von einer Zeichengruppe, die entweder aus le oder aus einem Leer-
string besteht, danach isch.
Wir gehen einen Schritt weiter: Welche Namen enthalten Doppelbuchstaben? Also
"aa", "bb", "cc" etc. Schreiben wir einmal das Suchmuster auf.
pattern = "aa|bb|cc|dd|ee|ff...
Das muss doch kürzer und schneller gehen! Bevor du weiterliest: Philosophiere über
eine elegantere und grazilere Möglichkeit. Du kennst bereits alle Techniken, die du
dazu brauchst!
Schon eine Antwort gefunden? Kommen wir zur Auflösung!
Beispiel: Selektiere alle Namen, welche
1. zwei Mal in Folge denselben Selbstlaut haben.
> # 1.) Welche Namen enthalten denselben Selbstlaut zwei Mal in Folge?
> selbstlaute <- c("a", "e", "i", "o", "u")
> pattern <- paste(selbstlaute, selbstlaute, sep = "", collapse = "|")
> grep(pattern, namen, ignore.case = TRUE, value = TRUE)
[1] "Maaouiya" "Renee"
Die Grazilität dieser Variante ist in der Verwendung von paste() begründet (vgl.
(10.3)). Wegen sep = "" werden die Selbstlaute ohne Zwischenraum aneinanderge-
hängt und mit collapse = "|" fügen wir zwischen den entstandenen Doppelselbst-
lauten jeweils ein Oder-Zeichen ein. Die Suche nach Namen mit einem beliebigen
Doppelbuchstaben funktioniert analog.
Den Stringanfang markieren wir mit dem Potenzzeichen ("ˆ"), das Stringende
mit einem Dollarzeichen ("$"). Damit können wir ab sofort die Frage beantworten,
welche Strings mit bestimmten Zeichen beginnen oder enden.
Im zweiten Code setzen wir einen Anker, der den Stringanfang matcht. Damit fällt
Marie-Anne durch das Suchmuster.
Das erste Potenzzeichen steht für den Stringanfang, das zweite in den eckigen Klam-
mern erfüllt die in (23.2.2) gelernte Negationsfunktion.
Wir brauchen zur Beantwortung der Frage eine Fallunterscheidung. Die beiden
aufeinanderfolgenden Vokale (Selbstlaute) können
1. in der Mitte des Strings stehen,
2. zu Beginn des Strings stehen,
3. am Ende des Strings stehen,
4. die einzigen beiden Buchstaben des Strings sein.
Die Idee ist klar: Wir verodern alle diese vier Fälle zu einem Suchmuster.
Falls du dich fragen solltest, warum wir plötzlich mit sep = "|" arbeiten (anstatt
mit collapse), dann legen wir dir Abb. 10.1 auf Seite 129 ans Herz.
Wenn wir uns für Namen interessieren, die genau zwei aufeinanderfolgende Vokale
haben und darüber hinaus keine Folge aus mehr als zwei Vokalen besitzen, so
brauchen wir eine Modifikation. Der Name Freia-Marie hätte in Marie genau zwei
Vokale in Folge, aber eben auch eine Folge von drei Vokalen in Freia. In Übung 4
bekommst du die Chance, einen Code für die Modifikation zu entwerfen.
Den Stringanfang und das Stringende können wir äußerst nützlich beim Trimmen
von Strings einsetzen. Dabei entfernen wir White Space (wie etwa Leerzeichen)
vorne und hinten (am Rand des Strings) aber nicht in der Mitte.
Der Nachteil: Etwaige Tabulatoren werden dabei nicht entfernt. Um White Space zu
matchen, stellt uns R die Zeichenmenge [:space:] zur Verfügung.
Beachte, dass wir [:space:] nochmal in eckige Klammern setzen müssen. Dadurch
ist es uns möglich, der Zeichengruppe noch weitere Zeichen, wie etwa das Rufzeichen,
hinzuzufügen.
Wir betrachten weitere fortgeschrittenere nützliche Tools, mit denen wir noch viel
mächtiger werden!
Angenommen, wir wollen all jene Namen extrahieren, deren 1. und 3. Buchstabe
sich gleichen. Wir könnten das Suchmuster so definieren:
pattern = "^a.a|^b.b|^c.c|..."
Kürzer geht es mit Rückreferenzen. Wir können in ein Suchmuster mittels \k exakt
jene Zeichen einsetzen, die in der k. Gruppe gematcht wurden. Das eröffnet uns viele
spannende Möglichkeiten, die wir uns jetzt ansehen :-)
> # Selektiere all jene Lacher, die aus exakt denselben Silben bestehen
> grep("([Hh])([ai])\\1\\2", x, value = TRUE)
[1] "haha" "hihi"
Wir suchen nach Vorkommen eines H oder h gefolgt von einem a oder i. Anschließend
sollen exakt dieselben Zeichen kommen, die in der 1. Gruppe bzw. 2. Gruppe ge-
matcht wurden. Haha wird demnach nicht gematcht, weil H nicht h ist und hiha auch
nicht, weil i nicht a ist.
23 Komplexe Textsuchmuster: Regular Expressions 299
Auf einzelne Zeichen können wir nicht rückreferenzieren. Das Ganze funktioniert
nur dann, wenn wir Gruppen verwenden!
Wir können Rückreferenzen auch in Ersetzungen verwenden. Das eröffnet uns coole
Möglichkeiten! Wie wäre es zum Beispiel, wenn wir jeden Selbstlaut verdoppeln?
> x
[1] "Haha" "haha" "hiha" "hihi" "Aha"
Für jeden Selbstlaut, der im Suchmuster gematcht wird, wird bei der Ersetzung ex-
akt dasselbe Zeichen zwei Mal eingefügt. Wir sehen anhand von Aha und AAhaa, dass
bei der Ersetzung zwischen Groß- und Kleinschreibung unterschieden wird. Die Ein-
stellung ignore.case = TRUE steuert nämlich lediglich, dass auch Großbuchstaben
mit dem Suchmuster gefunden werden. Mit ignore.case = FALSE verdoppeln wir
hingegen nur die kleingeschriebenen Selbstlaute.
Kein Name?! Mit ignore.case = TRUE geht es nicht, analoger Grund wie oben: AdAm
und Adam passen nicht zusammen. Wir brauchen einen Griff in die R-Trickkiste.
Oftmals wollen wir einen Teilstring eines Textes extrahieren unter der Bedingung,
dass vor oder/und nach diesem Teilstring noch bestimmte Zeichen(gruppen) vor-
kommen. Wir betrachten als Beispiel folgenden Vektor masse.
> masse <- c("50 g", "30 kg", "7kg", "50 km", "27cm")
> masse
[1] "50 g" "30 kg" "7kg" "50 km" "27cm"
Aufgabe: Wir wollen aus dem Vektor masse all jene Zahlen extrahieren, die Maß-
angaben in Gramm (g) oder Kilogramm (kg) beschreiben. Die Gewichtsmaße sollen
dabei aber nicht mitextrahiert werden.
Bevor du weiterliest: Philosophiere über eine Lösungsmöglichkeit, um diese Aufgabe
zu lösen. Es ist mit den bisherigen Techniken bereits möglich.
Wir haben unter anderem zwei Möglichkeiten:
1. Wir extrahieren alle Zahlen, denen ein g oder kg folgt. Anschließend löschen
wir alle Vorkommen von g und kg.
2. Wir benützen Lookaheads.
> # 1. Variante
> # Alle Zahlen extrahieren, die von g oder kg gefolgt werden
> temp <- regmatches(masse, regexpr("[0-9]+ *(g|kg)", masse))
> temp
[1] "50 g" "30 kg" "7kg"
> # Alle g und kg löschen
> gsub(" *(g|kg)", "", temp)
[1] "50" "30" "7"
Hier sehen wir übrigens eine weitere sinnvolle Anwendung für das Matchlängenkürzel
"*": Zwischen der Zahl und der Maßangabe können beliebig viele Leerzeichen (oder
auch keines) stehen. Statt (g|kg) hätten wir auch k?g schreiben können.
Kommen wir zu Variante Nummer 2.
Mit (?= *(g|kg)) definieren wir einen positiven Lookahead über die Zeichenfolge
" *(g|kg)". Was sind aber Lookaheads überhaupt?
Mit [0-9]+ definieren wir eine Zahl. Anschließend kommt der positive Lookahead:
Damit überhaupt ein Match erfolgt, muss die Zeichenfolge " *(g|kg)" nach der
Zahl vorkommen. Sie wird aber in der Matchlänge nicht berücksichtigt, wie wir im
Output von temp erkennen, und daher auch nicht mitextrahiert.
Erinnerst du dich noch an Kapitel (22.2), als wir perl erwähnt haben? Erst wenn
wir perl = TRUE setzen, können wir Gebrauch von Lookaheads machen.
Neben Lookaheads stehen uns analog auch Lookbehinds für Zeichen(gruppen) vor
dem interessanten Teilstring zur Verfügung. In Tab. 23.1 sehen wir eine Übersicht
aller vier Einsatzmöglichkeiten.
Schauen wir uns noch kurz einen negativen Lookahead an – Tücke inklusive.
Funktioniert leider nicht, die 2 ganz zum Schluss kommt von 27cm, und cm wollten
wir ja nicht haben. Der Grund für das Scheitern wiederholt sich: R matcht die 2
als Ziffer ([0-9]+). Die 7 kann jetzt aber nicht mehr als Ziffer gematcht werden,
da nach der 7 bereits das verbotene cm kommt. Damit also ein Match überhaupt
möglich ist, muss R die 7 bereits für den negativen Lookahead verwenden.
Wir reparieren obigen Code dahingehend:
Mit [0-9]* im negativen Lookahead verhindern wir, dass R Ziffern für das verbotene
cm einsetzen kann. Wir zwingen also R, alle Ziffern für [0-9]+ einzusetzen.
Gerne darfst du auch in diesem Beispiel jene Elemente des Ergebnisvektors mit NA
markieren, bei denen keine Übereinstimmung gefunden wurde.
302 E Tools für Data Science und Statistik
Im linken Code wird aba nur einmal gefunden. Das passiert deswegen, weil jedes
Zeichen nur höchstens einem Match zugeordnet werden kann. Und wie wir an der
Matchlänge 3 sehen, wird ba mitgematcht. Daher fehlt insbesondere das mittlere
a für das zweite aba. Im rechten Code hingegen werden beide aba gefunden, weil
Lookaheads nicht zum Match zählen.
Beispiel: Wie oft kommt in jedem Namen ein Selbstlaut, kein Selbstlaut und wieder
ein Selbstlaut in Folge vor?
Im ersten Code (ohne Überlappungen) findet R bei Aurelia das eli nicht mehr,
da das e schon für ure gematcht wurde. Ähnliches gilt für Elena. Im zweiten Code
(mit Überlappungen) hingegen werden die Lookaheads nicht mitgematcht, womit in
beiden Namen beide Vorkommen des Musters gefunden werden.
23 Komplexe Textsuchmuster: Regular Expressions 303
Wir begleiten eine Studentin, die gerade nach Wien gekommen ist und gerne mehr
über die öffentlichen Verkehrsmittel in Wien erfahren möchte, insbesondere über
die U-Bahn-Linie U2. Bevor wir starten, erfahren wir in Infobox 9 einige für dieses
Beispiel relevante Information über die Wiener Linien.
Seit Ende 2012 erklingt in den Wiener öffentlichen Verkehrsmitteln die Stimme
von Angela Schneider, welche die nächste Station und die Umsteigemöglichkei-
ten ansagt. Das Ansageschema sieht (vereinfacht) wie folgt aus:
Ein Sonderfall ist WLB (Wiener Lokal Bahn), die eine eigene Kategorie dar-
stellt. Schnellbahnen, Regionalbusse und weitere Details vernachlässigen wir.
Der Vektor ansagen enthält die vollständigen Ansagen aller U2-Stationen. Die Ob-
jekte stationen und umstieg sind von ansagen abgeleitete Objekte; sie enthalten
die Stationsnamen sowie jenen Ansagenteil, der die Umsteigemöglichkeiten enthält.
> head(ansagen, n = 6)
[1] "Karlsplatz. Umsteigen zu: U1, U4, D, 1, 2, 62, 71, 2A, 4A, 59A, WLB"
[2] "Museumsquartier. Umsteigen zu: 57A"
[3] "Volkstheater. Umsteigen zu: U3, 46, 49, 71, 48A"
[4] "Rathaus. Umsteigen zu: 2"
[5] "Schottentor. Umsteigen zu: D, 1, 37, 38, 40, 41, 42, 43, 44, 71, 1A, 40A"
[6] "Schottenring. Umsteigen zu: U4, 1, 31, 3A"
304 E Tools für Data Science und Statistik
> head(stationen, n = 6)
[1] "Karlsplatz" "Museumsquartier" "Volkstheater" "Rathaus"
[5] "Schottentor" "Schottenring"
> head(umstieg, n = 6)
[1] "U1,U4,D,1,2,62,71,2A,4A,59A,WLB" "57A"
[3] "U3,46,49,71,48A" "2"
[5] "D,1,37,38,40,41,42,43,44,71,1A,40A" "U4,1,31,3A"
Die ersten beiden Fragen beantworten wir sogleich. Die anderen fünf Fragen über-
lassen wir dir als Übung.
> # 1.) Bei welchen Stationen können wir zu keinem Autobus umsteigen?
> pattern.bus <- "[0-9]+[AB]"
> stationen[!grepl(pattern.bus, umstieg)]
[1] "Rathaus"
Eine Autobuslinie erkennen wir an einer Zahl gefolgt von einem A oder B. Wir erfra-
gen, bei welchen Stationen Autobuslinien halten und verneinen dann die Antwort.
Würden wir lediglich pattern = "[AB]" schreiben, so würde beim Karlsplatz die
Wiener Lokalbahn (WLB), ein schienengebundenes Verkehrsmittel, fälschlicherweise
als Autobuslinie durchgehen. Das würde bei der zweiten Frage zu einem falschen
Ergebnis führen! Ein Beispiel, das zeigt, wie aufmerksam wir bei der Auffindung von
korrekten Suchmustern sein müssen. Wir erinnern uns an Regel 15 auf Seite 293.
> # 2.) In wie viele Autobuslinien können wir bei jeder Station umsteigen?
> stellen.bus <- gregexpr(pattern.bus, umstieg)
> temp <- lapply(stellen.bus, ">", 0)
> sapply(temp, sum)
[1] 3 1 1 0 2 1 1 3 1 1 2 3 3 3 3 1 6 3 4 3
Es gibt eine Alternative, die uns in der Praxis solche Aufgaben vereinfachen kann,
und die zeigt, dass wir Stringfunktionen selbstverständlich auch in lapply() und
sapply() anwenden können.
23 Komplexe Textsuchmuster: Regular Expressions 305
R wendet also auf jede Listenkomponente von umstieg.liste die Funktion grep()
an. pattern und value werden dabei als weitere Parameter für grep() übergeben.
23.8 Abschluss
Wir fassen in Tab. 23.2 unsere Werkzeuge zusammen und listen ihre magischen
Fähigkeiten auf. Die 12 Sonderzeichen, die dabei eine besondere Bedeutung haben
und die von uns bei der expliziten Suche besonders behandelt werden müssen:
. \ | ( ) [ { ^ $ * + ?
Wir haben Regular Expressions auf hohem Niveau betrachtet! Für Feinheiten, die
wir uns nicht angesehen haben, verweisen wir auf ?regex. Unter anderem gibt es
dort noch weitere Zeichenbereiche zu entdecken.
306 E Tools für Data Science und Statistik
Sonderzeichen Bedeutung
"." beliebiges Zeichen
"[...]" beliebiges Zeichen aus der Menge ...
"[ˆ...]" beliebiges Zeichen außer den Zeichen aus der Menge ...
"[A-Z]" beliebiger Großbuchstabe
"[a-z]" beliebiger Kleinbuchstabe
"[0-9]" beliebige Ziffer
"{k}" Zeichen/Gruppe davor genau k Mal matchen
"{a,}" Zeichen/Gruppe davor mindestens a Mal matchen
"{a,b}" Zeichen/Gruppe davor zwischen a und b Mal matchen
"{,b}" Zeichen/Gruppe davor höchstens b Mal matchen
"+" Zeichen/Gruppe davor mindestens 1 Mal matchen
"*" Zeichen/Gruppe davor mindestens 0 Mal matchen
"?" Zeichen/Gruppe davor ist optional.
"(...)" Zeichengruppe mit den Zeichen ... definieren
"ab|cd" ab oder cd matchen
"ˆ" bzw. "$" Stringanfang bzw. Stringende matchen
"(?=...)" positiver Lookahead
"(?!...)" negativer Lookahead
"(?<=...)" positiver Lookbehind
"(?<!...)" negativer Lookbehind
• Wie suchen wir explizit nach Sonderzeichen? Welche Ausnahmen gibt es bei der
Suche nach bestimmten Zeichen? Was ist ein Escape-Befehl und wie markieren
wir einen Zeilenumbruch? Wie stellen wir in einem String einen Backslash und
ein Anführungszeichen dar? (23.1)
• Wie matchen wir ein beliebiges Zeichen? Wie definieren wir eine Zeichenmenge
bzw. schließen Zeichen einer Menge aus? Wie matchen wir einen beliebigen
Groß- bzw. Kleinbuchstaben sowie eine beliebige Ziffer? (23.2.1)
• Wie geben wir an, dass ein Zeichen (bzw. eine Gruppe) genau k Mal, mindes-
tens a Mal oder/und maximal b Mal gematcht werden soll? Was bedeuten bei
der Steuerung der Matchlänge die Kürzel "+", "*" und "?"? (23.3.1)
23 Komplexe Textsuchmuster: Regular Expressions 307
• Was bedeutet greedy Matching? Wie stellen wir dieses Verhalten ab? (23.3.2)
• Wie extrahieren wir bestimmte Teile eines Strings mit Hilfe des Outputs von
regexpr() oder gregexpr()? Welche Datenstruktur wird dabei jeweils zu-
rückgegeben? Wie markieren wir jene Elemente eines Stringvektors mit NA,
die ein Suchmuster nicht enthalten? (23.4)
• Was sind Zeichengruppen und wie definieren wir sie? (23.5.1)
• Wie verodern wir Suchmuster bzw. Teile eines Suchmusters? Wie suchen wir
fingerschonend nach Doppelbuchstaben? (23.5.2)
• Wie matchen wir den Anfang sowie das Ende eines Strings? (23.5.3)
• Was ist White Space und wie entfernen wir ihn am Anfang oder/und am Ende
eines Strings? (23.5.4)
• Was sind bzw. was können Rückreferenzen? Wie setzen wir sie in R beim
Suchen und beim Ersetzen ein? (23.6.1)
• Was sind Lookaheads und Lookbehinds? Wie setzen wir sie in R um? (23.6.2)
• Wie führen wir eine überlappende Textsuche durch? (23.6.3)
Und wir beherzigen Regel 15: Definiere Suchmuster sowohl flexibel genug als auch
starr genug um möglichst jene Informationen zu extrahieren, die du haben willst.
23.8.3 Übungen
2. Gegeben ein beliebiger Stringvektor x. Entwickle für jede der folgenden Fragen
einen Code, der sie beantwortet:
a) An welcher Stelle befindet sich in jeder Komponente von x die letzte
schließende runde Klammer?
b) Kommen in x ausschließlich Buchstaben und Leerzeichen vor?
c) Folgt auf jeden Großbuchstaben stets ein Kleinbuchstabe?
308 E Tools für Data Science und Statistik
3. Wir betrachten erneut den Vektor namen aus der Einleitung dieses Kapitels.
Bei allen folgenden Fragen soll die Groß- und Kleinschreibung ignoriert werden.
a) Welche Namen fangen mit einem Selbstlaut an und enden auch auf einem?
6. Schreibe einen Code, der aus folgendem Text alle Zahlen, aber keine Datums-
werte selektiert.
Markiere all jene Stellen, die keine gültige Zahl enthalten, mit einem NA.
7. Ein Beispiel, das andeutet, wie Textmining in der Praxis aussehen könnte.
wettertext <- "Es ist 20.05 Uhr. Zeit für die Wettervorhersage.
Morgen, am 9.01. hat es 5.5 Grad, am 10.01. sind es noch 3 °C.
Der 11.1. kommt mit winterlichen -2.5 Grad und -1°C hat es am 12.1."
Wir betrachten in diesem Kapitel die Datei Vertreter.txt. Den vollständigen Da-
tensatz in der Editoransicht finden wir in Abb. 24.1.
Dort begleiten wir 24 Vertreter, die in vier Gebieten Staubsauger verkaufen. Jeder
Vertreter hat eine von zwei Ausbildungen abgeschlossen.
Die abgebildeten Codierungen stehen dabei für:
Der erzielte Gewinn wird in 1000 Euro angegeben. Darüber hinaus hat jeder Ver-
treter eine Nummer.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_24
24 Kategorielle Variablen: Faktoren 311
• Wie verwalten wir in R sinnvoll die beiden kategoriellen Variablen Gebiet und
Ausbildung? (24.1)
Wir lesen jetzt den Datensatz mit Hilfe von read.table() ein.
Dem Parameter file übergeben wir den Namen (gegebenenfalls inklusive dem Pfad)
der gewünschten Datei. header = TRUE besagt, dass in der ersten Zeile der Datei
die Variablennamen stehen. Mit dec = "," teilen wir R mit, dass in der Datei Dezi-
malzahlen mit einem Komma dargestellt werden. Das Thema Lesen und Schreiben
von Dateien besprechen wir in (32) noch ausführlich.
Es kann sein, dass R bei der Beschriftung der ersten Variablen einen Dummytext
einfügt. Das hängt mit der Codierung der Datei zusammen, worauf wir hier nicht
näher eingehen. Stattdessen bereinigen wir diese Unsauberkeit, indem wir die ersten
drei Zeichen (ï..) löschen.
> # Die ersten Zeilen betrachten > # Die letzten Zeilen betrachten
> head(daten) > tail(daten)
Nummer Gebiet Ausbildung Gewinn Nummer Gebiet Ausbildung Gewinn
1 1 2 1 11.4 19 19 4 2 7.9
2 2 3 1 9.2 20 20 1 1 10.5
3 3 1 1 9.3 21 21 2 1 7.1
4 4 3 2 13.6 22 22 3 2 6.4
5 5 4 1 8.9 23 23 2 2 10.4
6 6 1 2 3.3 24 24 3 1 24.3
Sieht sehr gut aus! Der Datensatz hat vier Variablen: Nummer, Gebiet, Ausbildung
und Gewinn.
Die Variablen Gebiet bzw. Ausbildung sind kategorielle Variablen mit vier bzw.
zwei Ausprägungen (Kategorien). In Abb. 24.2 zeigen wir noch einmal, wofür diese
Codierungen stehen. Wir werden den Nummerncodes diese Beschriftungen zuweisen.
Gebiet Ausbildung
Code Beschriftung Code Beschriftung
1 West 1 Matura
2 Nord 2 Lehre
3 Ost
4 Süd
Faktoren können wir mit der Funktion factor() generieren. Der vereinfachte Aufruf
(ein paar weitere Parameter schauen wir uns noch weiter unten an):
> head(daten, n = 8)
Nummer Gebiet Ausbildung Gewinn
1 1 Nord Matura 11.4
2 2 Ost Matura 9.2
3 3 West Matura 9.3
4 4 Ost Lehre 13.6
5 5 Süd Matura 8.9
6 6 West Lehre 3.3
7 7 Süd Lehre 2.1
8 8 Ost Lehre 0.2
314 E Tools für Data Science und Statistik
Den Parameter levels haben wir nicht spezifiziert, daher greift die Standardpro-
zedur: Die unterscheidbaren Werte in Gebiet und Ausbildung werden vor der Fak-
torbildung sortiert.
Anschließend werden die labels gemäß Abb. 24.2 den levels zugeordnet.
Gebiet Ausbildung
levels 1 2 3 4 levels 1 2
labels West Nord Ost Süd labels Matura Lehre
Zur Wiederholung: Intern werden die Beobachtungen eines Faktors mit numerischen
Werten (den Codes) gespeichert. Die Information über die Beschriftungen wird aus-
gelagert. Dadurch werden die Daten speicherschonend verwaltet. Wie das genau
aussieht, schauen wir uns in (24.1.3) und (24.1.4) an. Und in (24.1.6) nehmen wir
den Parameter levels genauer unter die Lupe!
Schauen wir uns einmal an, wie das Ganze als Vektor aussieht.
> daten$Gebiet
[1] Nord Ost West Ost Süd West Süd Ost Süd Nord West Süd West West
[15] Nord Nord Süd Ost Süd West Nord Ost Nord Ost
Levels: West Nord Ost Süd
Die Funktion unclass() zeigt uns die elementaren Bestandteile, aus denen der Fak-
tor Gebiet besteht, indem das Klassenattribut entfernt wird. Wir sehen, dass ein
Faktor aus einem numerischen Wertevektor (die Codierungen) und einem Vektor
levels (die Beschriftungen) besteht (als Attribut). Rufen wir daten$Gebiet auf
(die Variable Gebiet wird ausgegeben), so wird intern auf die entsprechenden Le-
vels zugegriffen.
Dank unclass() und obigen Überlegungen ist jetzt klar, warum der Mode numerisch
ist. Und wir sehen den Unterschied zwischen mode(daten$Gebiet) == "numeric"
und is.numeric(daten$Gebiet).
Die explizite Abfrage auf einen Faktor setzen wir mit is.factor() um.
Mit levels() können wir auf die Ausprägungsnamen eines Faktors zugreifen.
Mit nlevels() bestimmen wir die Anzahl der Ausprägungen eines Faktors.
Möchten wir einen Faktor in einen Stringvektor umwandeln, so haben wir zwei
Möglichkeiten:
> unclass(daten$Gebiet)
[1] 2 3 1 3 4 1 4 3 4 2 1 4 1 1 2 2 4 3 4 1 2 3 2 3
attr(,"levels")
[1] "West" "Nord" "Ost" "Süd"
Intern ist also daten$Gebiet ein numerischer Vektor mit Einträgen von 1 bis 4.
> factor(gebiet.string)
[1] Nord Ost West Ost Süd West Süd Ost Süd Nord West Süd West West
[15] Nord Nord Süd Ost Süd West Nord Ost Nord Ost
Levels: Nord Ost Süd West
Vergleichen wir einmal den Faktor aus daten mit dem soeben erzeugten:
> daten$Gebiet
[1] Nord Ost West Ost Süd West Süd Ost Süd Nord West Süd West West
[15] Nord Nord Süd Ost Süd West Nord Ost Nord Ost
Levels: West Nord Ost Süd
> factor(gebiet.string)
[1] Nord Ost West Ost Süd West Süd Ost Süd Nord West Süd West West
[15] Nord Nord Süd Ost Süd West Nord Ost Nord Ost
Levels: Nord Ost Süd West
Die Levelreihenfolge hat sich verändert! Da wir der Funktion factor() keine levels
übergeben haben, greift wieder folgender Code, der die Reihenfolge festlegt:
> sort(unique(gebiet.string))
[1] "Nord" "Ost" "Süd" "West"
Das heißt, 1 = Nord, 2 = Ost, 3 = Süd und 4 = West. Wir möchten aber gerne
unsere Codierung, wie wir sie in Abb. 24.2 auf Seite 312 festgelegt haben. Hier
kommt der Parameter levels ins Spiel:
Hierbei müssen wir aufpassen, dass wir alle unterscheidbaren Elemente des
Datenvektors aufschreiben, da ansonsten NA-Werte auftreten.
Nun wollen wir uns der Variable Gewinn näher zuwenden. Eine mögliche Problem-
stellung ist es, den Gewinn gemäß Tab. 24.1 zu kategorisieren.
> daten$Gewinn
[1] 11.4 9.2 9.3 13.6 8.9 3.3 2.1 0.2 10.7 16.7 11.8 7.8 7.4 7.1
[15] 9.4 7.1 12.2 8.1 7.9 10.5 7.1 6.4 10.4 24.3
Gewinn Kategorie
(−∞, 5] wenig
(5, 10] moderat
(10, 15] viel
(15, ∞] Goldgrube
Wir betrachten in den folgenden beiden Unterabschnitten zwei Varianten: eine auf-
wändige (wenngleich coole) und eine einfache.
Eine Möglichkeit, den Gewinnen die gewünschte Kategorie zuzuordnen wäre es, von
Indikatorfunktionen Gebrauch zu machen:
Die Variable gr5 sagt uns, welche Einträge von Gewinn größer als 5 sind, analog für
gr10 und gr15. Erfüllt ein Eintrag der Variable Gewinn viele dieser Bedingungen, so
ist sein Wert tendenziell groß, wodurch er in der Kategorie steigt. Nun bestimmen
wir die Kategorie zunächst als Zahlenwert.
Sieht gut aus! Nun vollenden wir das Ganze, indem wir Beschriftungen hinzufügen.
> head(daten, n = 6)
Nummer Gebiet Ausbildung Gewinn Gewinnstufe
1 1 Nord Matura 11.4 viel
2 2 Ost Matura 9.2 moderat
3 3 West Matura 9.3 moderat
4 4 Ost Lehre 13.6 viel
5 5 Süd Matura 8.9 moderat
6 6 West Lehre 3.3 wenig
Leichter geht es mit der Funktion cut(), welche numerische Werte zu Interval-
len zusammenfasst und uns einen Faktor zurückgibt. Der Funktionsaufruf:
In Tab. 24.2 beschreiben wir kurz einige Parameter. ordered_result verstehen wir,
nachdem wir (24.4) gelesen haben.
Parameter Bedeutung
x numerischer Vektor
breaks Intervallsgrenzen (Breakpoints) oder Anzahl der Intervalle
labels Benennung der Intervalle
right Sollen die Intervalle rechts abgeschlossen (TRUE, Standard) oder
links abgeschlossen (FALSE) sein?
Wir sehen, dass aus den fünf Grenzen die vier dazu passenden (halboffenen) Inter-
valle gebastelt werden. Die Intervalle sind rechts abgeschlossen, da wir right nicht
spezifiziert haben und daher die Standardeinstellung greift.
Als äußerste Grenze empfiehlt es sich, einfach −∞ und ∞ zu nehmen. Wollen wir zu-
sätzlich die Ausprägungen benennen, so können wir die Variable stufen aus (24.2.1)
verwenden.
Beachte: Auch bei cut() entstehen NA-Werte, wenn es für Elemente aus x kein
Intervall gibt, in das sie hineinfallen.
Wir schauen uns im Folgenden an, wie wir Ausprägungen verschmelzen und
umbenennen sowie neue Kategorien hinzufügen. Dabei beziehen wir uns auf
das Objekt daten, das wir auf dieser Seite letztmalig modifiziert haben.
320 E Tools für Data Science und Statistik
Beispiel: Wir wollen die Ausprägung Goldgrube mit der Ausprägung viel zur
Ausprägung viel verschmelzen. Zunächst schauen wir uns den Ist-Zustand an.
Wir betrachten zwei Varianten: Eine Variante, welche die Kategorie Goldgrube be-
wahrt und eine, die das nicht tut.
In der ersten Variante überschreiben wir direkt die Elemente, die Goldgrube glei-
chen, durch "viel". Diese Variante bewahrt die Kategorie Goldgrube in den levels.
Diese Lösung funktioniert, alle Goldgrube-Einträge wurden durch viel ersetzt. Wir
sehen, dass wir bei der Zuweisung "viel" nehmen. Die Zuweisung der entsprechen-
den Codierung (hier 3) würde nicht funktionieren.
Die Kategorie Goldgrube bleibt zur Erinnerung erhalten! Wollen wir diese nicht mehr
in Anwendung befindliche Kategorie entfernen, so können wir eine neuerliche
Faktorbildung durchführen.
> factor(gewinn.cut1)
[1] viel moderat moderat viel moderat wenig wenig wenig viel
[10] viel viel moderat moderat moderat moderat moderat viel moderat
[19] moderat viel moderat moderat viel viel
Levels: wenig moderat viel
In der zweiten Variante überschreiben wir in den levels die Kategorie Goldgrube
mit der Kategorie "viel". Damit wird gleichzeitig die Kategorie Goldgrube vollstän-
dig gelöscht!
Beispiel: Die Vertreter 1, 2, 5 und 6 werden in ein neues Gebiet Zentrum verlegt.
Erweitere den Faktor in geeigneter Weise.
> daten$Gebiet
[1] Nord Ost West Ost Süd West Süd Ost Süd Nord West Süd West West
[15] Nord Nord Süd Ost Süd West Nord Ost Nord Ost
Levels: West Nord Ost Süd
322 E Tools für Data Science und Statistik
> daten$Gebiet
[1] <NA> <NA> West Ost <NA> <NA> Süd Ost Süd Nord West Süd West West
[15] Nord Nord Süd Ost Süd West Nord Ost Nord Ost
Levels: West Nord Ost Süd
Dies funktioniert offenbar nicht. Der Faktor daten$Gebiet kennt nämlich die Aus-
prägung Zentrum nicht und kann daher die Ersetzung nicht korrekt vornehmen.
Wir müssen die Variable Gebiet also zuerst mit der neuen Kategorie Zentrum ver-
traut machen! Genauer gesagt die levels von Gebiet.
> levels(daten$Gebiet)
[1] "West" "Nord" "Ost" "Süd" "Zentrum"
Jetzt kennt der Faktor die Kategorie "Zentrum" und beschwert sich nicht mehr,
wenn wir diese Kategorie zuweisen.
Bis jetzt haben wir lediglich nominalskalierte Variablen betrachtet bzw. so getan, als
wären sie nominalskaliert. Nominalskaliert heißt, dass die Kategorien keine Ord-
nung haben. Haben die Ausprägungen einer Variable hingegen eine Ordnung, so
ist diese Variable ordinalskaliert.
Wir können einem Faktor mitteilen, dass die Kategorien eine Ordnung aufweisen.
Dies geschieht in der Funktion factor(), indem wir den Parameter ordered = TRUE
setzen bzw. mit der Funktion as.ordered().
24 Kategorielle Variablen: Faktoren 323
> # Ausgangssituation
> daten$Gewinnstufe
[1] viel mittel mittel viel mittel wenig wenig wenig viel viel
[11] viel mittel mittel mittel mittel mittel viel mittel mittel viel
[21] mittel mittel viel viel
Levels: wenig mittel viel
Wenn der Faktor ordinalskaliert ist, so können wir auch "<", "<=", ">" und eben
">=" verwenden.
Mit Hilfe der Funktion is.ordered() können wir abfragen, ob ein Faktor ordi-
nalskaliert ist.
Wir verlassen die Welt der Vertreter und betrachten ein paar Tücken von Faktoren.
Folgendes Problem könnte beispielsweise auftreten: Wir lesen einen Datensatz ein,
der eine Variable Note enthält, welche als String gespeichert ist. Also aus Sehr gut,
Gut, Befriedigend, Genügend und Nicht genügend besteht.
Ziel: Wir wollen die Noten in die entsprechenden Zahlen umwandeln, und zwar so,
wie es in Abb. 24.4 (links) gezeigt ist.
Beim Einlesen der Variablen Note passiert aber folgendes: Die unterscheidbaren
Noten werden alphabetisch sortiert und die Noten in dieser Reihenfolge codiert.
Daraus resultiert die falsche Codierung, die wir in Abb. 24.4 (rechts) sehen.
Abbildung 24.4: Noten als Strings und ihre zugehörige Zahl. Links: Korrekte Zu-
ordnung der Noten zur Codierung. Rechts: Die inkorrekte Zu-
ordnung nach dem Einlesen.
Vereinfacht dargestellt:
> noten <- c("Sehr gut", "Gut", "Befriedigend", "Genügend", "Nicht genügend")
> noten
[1] "Sehr gut" "Gut" "Befriedigend" "Genügend"
[5] "Nicht genügend"
Der Ist-Zustand: Die Levels sind offensichtlich falsch sortiert, nämlich alphabetisch.
Daher scheitert folgender Code:
Wir können diesen Umstand leicht mit dem Parameter levels beheben, indem wir
levels die korrekte Reihenfolge übergeben.
Dass Noten ordinalskaliert sind, teilen wir dem Faktor mit ordered = TRUE mit!
Jetzt werden den Noten auch die korrekten Zahlen zugeordnet:
Thematisch ziemlich ähnlich ist folgendes Problem: Angenommen, wir haben ein
paar Leute gefragt, wie viele Mahlzeiten sie an einem normalen Tag zu sich nehmen
und haben das Ergebnis als Faktor gespeichert (Variable mahl).
Ziel: Wir wollen (wieder) einen numerischen Vektor haben.
Beachte, dass niemand angegeben hat, nur eine Mahlzeit am Tag zu haben!
Probieren wir eine naheliegende, aber leider nicht zielführende Möglichkeit. Wer Fak-
toren bereits wirklich verstanden hat, kann bereits selbst erklären, warum folgender
Code nicht das korrekte Ergebnis liefert.
So geht das also offenbar nicht. Wie korrigieren wir diesen Ansatz?
Mit as.numeric() werden nämlich die Codes erzeugt, und die gehen immer von 1
bis Anzahl der Ausprägungen, wie wir schon in (24.1.1) geklärt haben.
326 E Tools für Data Science und Statistik
Tabellarisch:
Code 1 2 3
Ausprägung 2 3 5
Beispiel: Erstelle für die Vektoren mahl und mahl.numeric aus (24.5.2) eine Häu-
figkeitstabelle.
Zunächst schauen wir uns eine Tabelle mit Lücken an:
Wir können also der Funktion table() auch einen Faktor übergeben. Dabei greift
table() auf die levels des Faktors zu und bestimmt für jede dort gefundene Ka-
tegorie die entsprechende Häufigkeit. Für jede dort gefundene Häufigkeit!
Wollen wir eine lückenlose Häufigkeitstabelle (vgl. (12.2.4)) haben, so bieten
Faktoren aus diesem Umstand heraus eine elegante Möglichkeit dafür.
24 Kategorielle Variablen: Faktoren 327
> mahl.numeric
[1] 3 3 2 3 5 5 2
> table(mahl.faktor)
mahl.faktor
0 1 2 3 4 5
0 0 2 3 0 2
Wir übergeben den levels einfach all jene Kategorien, die wir in der Häufigkeitsta-
belle sehen wollen. Dann tut table() sein Übriges.
Wir schauen uns an, wie wir fehlende Werte als eigene Ausprägung definieren
können. Dazu betrachten wir eine Ja/Nein-Frage einer kleinen fiktiven Umfrage.
> is.na(antwort.fak1)
[1] FALSE FALSE FALSE FALSE TRUE
Fall 2: Nun soll aber NA eine eigene Ausprägung sein, getreu dem Motto „Keine
Meinung ist auch eine Meinung“.
In dem Fall entdecken wir in der R-Hilfe zu factor() den Parameter exclude, den
wir schon in (14.3.3) kennengelernt haben.
> antwort.fak2
[1] Ja Nein Ja Ja <NA>
Levels: Ja Nein <NA>
Mit exclude = NULL sagen wir factor(), dass wir keine Kategorie ausschließen
wollen. Nun funktioniert aber is.na() nicht mehr, wir wir am letzten FALSE im
folgenden Code erkennen, da es offiziell keinen fehlenden Wert mehr gibt.
> is.na(antwort.fak2)
[1] FALSE FALSE FALSE FALSE FALSE
Auf der rechten Seite des Vergleiches steht jener Code, der für NA-Werte abgestellt
wurde (hier 3). Auf der linken Seite stehen die Zahlencodes der Umfrage.
Wir können diese NA-Kategorie auch beschriften, zum Beispiel mit Keine Angabe.
Jetzt ist es ganz einfach und unkompliziert auf diese Kategorie zu testen:
24.7 Abschluss
• Warum ist die Verwaltung einer kategoriellen Variable in Form eines Faktors
besser, als in Form von Zeichenketten? Welche Idee steckt hinter der Codierung
und welche Codes verwendet R? (24.1.1)
• Wie generieren wir einen Faktor? Was bewirken die Parameter levels und
labels der Funktion factor()? Was passiert, wenn wir levels nicht spezifi-
zieren? (24.1.2)
• Wie werden Faktoren intern verwaltet? Welchen Mode hat ein Faktor? Wie
erfragen wir, ob ein Objekt ein Faktor ist? (24.1.3)
• Mit welchen Funktionen greifen wir auf die Levels eines Faktors zu bzw. erfah-
ren, wie viele Kategorien ein Faktor hat? (24.1.4)
• Welche zwei Möglichkeiten haben wir gelernt, um einen Faktor in einen String
umzuwandeln? (24.1.5)
• Wie stellen wir die Reihenfolge ein, in der die Kategorien codiert werden sollen?
Was passiert mit Elementen des Datenvektors, die bei den Kategorien nicht
berücksichtigt wurden? (24.1.6)
• Wie kategorisieren wir mit cut() einen numerischen Vektor? Welche Bedeu-
tung haben die Parameter breaks, labels und right und welche Einstellmög-
lichkeiten bieten sie? (24.2)
• Wie verschmelzen wir zwei Kategorien zu einer Kategorie? Wie gehen wir dabei
vor, wenn wir die zu verschmelzende Kategorie bewahren bzw. nicht bewahren
wollen? (24.3.1)
• Wie benennen wir Kategorien um? Wie fügen wir einem Faktor eine neue
Kategorie hinzu? (24.3.2), (24.3.3)
• Wie können wir (in factor()) bestimmen, dass es sich um eine ordinalska-
lierte Variable handelt? Welche Vergleichsoperatoren funktionieren bei ordi-
nalskalierten Faktoren zusätzlich? (24.4)
• Wie ordnen wir Faktorausprägungen um? (24.5.1)
• Wie gewinnen wir aus einem Faktor, der nur Zahlen enthält, die entsprechende
korrekte numerische Variable? (24.5.2)
• Wie erstellen wir mit Faktoren lückenlose Häufigkeitstabellen? (24.5.3)
• Mit welcher Einstellung in factor() bestimmen wir, dass NA eine eigene Ka-
tegorie sein soll? Wie finden wir heraus, ob ein Element eines Faktors ein
fehlender Wert ist, wenn dieser als eigene Kategorie verwaltet wird? (24.6)
330 E Tools für Data Science und Statistik
24.7.3 Ausblick
Es gibt noch einige spannende Fragen, die noch nicht beantwortet wurden! Zum
Beispiel ist es interessant zu erfahren, wie viel Ertrag in jedem Gebiet im Mittel
erzielt wurde. Solche und ähnliche Fragen betrachten wir in (25).
Faktoren ermöglichen es uns, gezielt und unkompliziert Grafiken zu erstellen. R
erkennt etwa automatisch, dass Streudiagramme mit kategoriellen Variablen wenig
Sinn machen und passt das Verhalten bei der Grafikerstellung an. Das schauen wir
uns in (35) an. Faktoren sind auch bei linearen Modellen unverzichtbar. Wieso,
erfahren wir in (38).
24.7.4 Übungen
c) Erstelle nun eine Kreuztabelle mit den kategorisierten Vektoren. Wie viele
Zellen sind leer (haben Häufigkeit 0)?
Hinweis: Wir können table() auch mehrere Vektoren übergeben.
d) Fasse die Ausprägungen jung und mittel zu Erwerbstätiger zusammen.
Benenne zusätzlich weise in Pensionist um.
e) Erstelle nun erneut eine Kreuztabelle. Verifiziere, dass Erwerbstätiger
in dieser Tabelle genauso häufig vorkommt wie die Kategorien jung und
mittel in der in 1c) erstellten Tabelle.
2. Ein abstraktes Beispiel, das viele Tücken der Faktoren aufzeigt und gleichzeitig
einige altbekannte Konzepte wiederholt. Wir gehen von folgendem Code aus:
a) Wie sieht die Bildschirmausgabe von faktor aus? Erkläre anhand dieses
Beispiels, wie die Parameter levels und labels funktionieren.
24 Kategorielle Variablen: Faktoren 331
b) Was passiert bzw. kommt heraus, wenn wir folgende Codezeilen eintip-
pen?
> as.numeric(faktor)
> levels(faktor) * 4
> faktor[faktor]
Offenbar sind die Levels von faktor1 nicht richtig sortiert. Erstelle einen
Faktor, in welchem die Levels richtig sortiert sind (2 < 4 < 6).
a) Wie viele Studierende haben eine Note von 4 oder besser erzielt?
b) Erstelle mit Hilfe eines modifizierten Faktors eine lückenlose Häufigkeits-
tabelle, welche die Häufigkeit aller Noten von 1 bis 5 angibt. Generiere
dazu einen Faktor, der alle Noten als Kategorie enthält.
c) Bestimme mit der Häufigkeitstabelle aus 3b) erneut, wie viele Studieren-
den eine Note von 4 oder besser erreicht haben.
d) Erstelle nun eine Häufigkeitstabelle, die zusätzlich angibt, wie viele Stu-
dierende nicht angetreten sind.
e) Erstelle einen Faktor, der angibt, ob eine Person bestanden hat (Noten
1 bis 4) oder nicht bestanden hat (Note 5 oder nicht angetreten). Wähle
geeignete Beschriftungen für die Kategorien.
332 E Tools für Data Science und Statistik
• Kreuztabellen einführen
Bisher haben wir eine Funktion immer auf einen ganzen Vektor bzw. auf eine ganze
Spalte eines Dataframes angewendet. Funktionen wie etwa sapply() oder lapply()
(siehe (18)) haben uns bei Dataframes dabei geholfen, eine Funktion auf alle Varia-
blen eines Dataframes anzuwenden.
Häufig möchten wir jedoch eine Funktion nicht auf die ganze Variable bzw. einen
ganzen Vektor anwenden. Vielmehr wollen wir die Variable nach einer oder mehreren
kategoriellen Variablen aufsplitten und die Funktion dann separat auf die Elemente
jeder Gruppe anwenden. Durch Faktoren, die wir in (24) kennengelernt haben, wird
dieser Wunsch zusätzlich befeuert. Wir lernen in diesem Kapitel adäquate Techniken
zur Wunscherfüllung kennen.
Wir begleiten in diesem Kapitel erneut unsere Vertreter aus (24) mit allen in dorti-
gem Kapitel vorgenommenen Modifizierungen.
In Abb. 25.1 bilden wir den gesamten Vertreterdatensatz ab. 24 Vertreter versu-
chen in 5 Gebieten (Gebiet: 1 = West, 2 = Nord, 3 = Ost, 4 = Süd, 5 = Zentrum)
Staubsauger zu verkaufen. Jeder Vertreter hat eine von zwei Ausbildungen erhalten
(Ausbildung: 1 = Matura, 2 = Lehre). Die Variable Gewinn enthält den erzielten
Gewinn in 1000 Euro. Die Gewinnstufe ist eine kategorisierte Version des Gewinns.
• Wie hoch ist der durchschnittliche Gewinn pro Gebiet? Wie hoch der kleinste
bzw. größte Gewinn pro Gebiet? (25.1)
• Wie viele Vertreter jeder Ausbildung arbeiten in jedem Gebiet? (25.2.1), (25.2.3)
• Wie viel Gewinn wurde insgesamt in jedem Gebiet bzw. bei jeder Ausbildung
erzielt? (25.2.4), (25.2.5)
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_25
25 Aggregation und Kreuztabellen 333
Nachdem wir uns mit dem Einvariablenfall befasst haben, schauen wir uns in (25.3)
an, wie wir mehrere Variablen gleichzeitig nach Gruppen splitten können. Abschließend
lernen wir in (25.4) Möglichkeiten kennen, wie wir die Spalten von Dataframes in-
nerhalb einer Funktion bzw. global verfügbar machen können.
Wie bestimmen wir den durchschnittlichen Gewinn pro Gebiet bzw. pro Ausbildung?
Fragen wie diese können wir mit der Funktion tapply() elegant beantworten; sie
ermöglicht es uns, eine Funktion getrennt nach einer oder mehreren Gruppen
auf einen Vektor anzuwenden.
In Tab. 25.1 beschreiben wir die Parameter von tapply(). Die Anwendung der
Funktion ist jener von sapply() ähnlich (vgl. (18.1)).
334 E Tools für Data Science und Statistik
Parameter Bedeutung
X Vektor bzw. atomares Objekt
INDEX Faktor oder Liste von Faktoren – falls INDEX (bzw. seine
Listenelemente) kein Faktor ist, wird er in einen solchen
umgewandelt.
FUN die anzuwendende Funktion
... weitere Parameter für FUN
simplify TRUE: Vektor oder Matrix wird zurückgegeben (allerdings nur,
wenn FUN einen Skalar zurückgibt).
FALSE: Liste wird zurückgegeben.
Insbesondere greifen für simplify = TRUE dieselben Regeln für die Wahl der Daten-
struktur des Ergebnisobjektes, wie wir sie in (18.4) gelernt haben. Mit einer einzigen
Ausnahme: Vereinfacht wird nur dann, wenn FUN einen Skalar zurückgibt.
Schauen wir uns zunächst ein Codebeispiel zur Veranschaulichung und Motivation
an. Wir erheben von 6 Personen das Geschlecht und die Körpergröße in cm und
berechnen die mittlere Körpergröße der Männer und der Frauen.
> geschlecht
[1] Mann Frau Frau Mann Frau Mann
Levels: Frau Mann
> groesse
[1] 180 165 175 170 NA 190
Das Problem: Was tun wir, wenn wir nicht zwei, sondern beispielsweise 10 Kategorien
haben? Würden wir dann den Code mit Copy & Paste kopieren und die Kategorie-
namen austauschen? Nein, natürlich nicht, dieser Ansatz ist sehr fehleranfällig und
mühsam. Einfacher und besser geht es mit tapply()!
Die Elemente der Variable groesse werden gemäß geschlecht aufgeteilt und an-
schließend die Funktion mean() auf jede Teilmenge angewendet. na.rm = TRUE wird
an die Funktion mean() als zusätzliche Spezifikation weitergegeben.
Beispiel: Berechne den durchschnittlichen Gewinn für jedes Gebiet und jede Aus-
bildung. Berechne auch den kleinsten und größten Gewinn für jedes Gebiet.
Im Norden und Osten wird tendenziell mehr verdient. Vertreter mit Matura sind
erfolgversprechender als Vertreter mit Lehrabschluss.
Achtung: Wenn wir statt min() und max() die Funktion range() anwenden, so
entsteht eine Liste, da range() keinen Skalar zurückgibt.
Uns ist wohl eine Matrix lieber. Im folgenden Code entnimmt sapply() die Vektoren
von gebiet.range der Reihe nach, identity() gibt diese unverändert zurück und
sapply() ordnet die Vektoren schließlich spaltenweise zu einer Matrix an.
Bemerkung: Die Funktion as.matrix() macht uns in diesem Fall nicht glücklich.
Das Coole: Solltest du mal vergessen, dass man Häufigkeitstabellen mit table()
erstellen kann, so hast du jetzt eine Alternative ;-)
Im Prinzip ist es egal, welchen Vektor wir statt Vektor$Gebiet für X übergeben.
Er muss nur dieselbe Länge haben, wie der Vektor Vektor$Gebiet, den wir an den
Parameter INDEX übergeben.
25.2 Kreuztabellen
Mit der Funktion table() können wir auch Kreuztabellen erstellen, indem wir
zwei (oder mehrere) Faktoren übergeben.
Beispiel: Erstelle eine Kreuztabelle, aus der man ablesen kann, wie viele Vertreter
jeder Ausbildung in jedem Gebiet arbeiten.
So arbeitet etwa nur ein Vertreter mit der Ausbildung Lehre im Zentrum und 3
Vertreter mit der Ausbildung Matura in der Region West.
Wenn wir table() mehr als zwei Faktoren übergeben, so generiert table() ein
Array. Arrays stellen eine Verallgemeinerung von Matrizen dar und können
beliebig viele Dimensionen aufweisen.
25 Aggregation und Kreuztabellen 337
Das Array tab3 hat drei Dimensionen: Die erste Dimension enthält die Gewinnstufe,
die zweite das Gebiet und die dritte die Ausbildung.
Die Selektion von Elementen aus Arrays funktioniert ähnlich wie bei Matrizen.
Wenn wir beispielsweise jenen Teilbereich selektieren wollen, der für die Gewinnstufe
wenig steht, dann schreiben wir:
Wir selektieren wenig aus der ersten Dimension und lassen die beiden anderen Di-
mensionen bei der Selektion leer. Im linken Code wird die Datenstruktur zu einer
Matrix vereinfacht, da wir nur eine Zeile selektieren. Im rechten Code verhindern
wir mit drop = FALSE die Vereinfachung der Datenstruktur.
Wir können mit apply() (siehe (19)) auch Funktionen auf eine oder mehrere
Dimensionen anwenden. Im folgenden Code wird beispielsweise über die letzten
beiden Dimensionen hinweg die Summe gebildet, wobei sich der linke Code vom
rechten Code durch die Reihenfolge unterscheidet, in der die Dimensionen an MARGIN
übergeben werden.
> # apply() über 2. und 3. Dimension > # apply() über 3. und 2. Dimension
> apply(tab3, MARGIN = 2:3, sum) > apply(tab3, MARGIN = 3:2, sum)
Matura Lehre West Nord Ost Süd Zentrum
West 3 2 Matura 3 2 2 2 3
Nord 2 3 Lehre 2 3 3 3 1
Ost 2 3
Süd 2 3
Zentrum 3 1
338 E Tools für Data Science und Statistik
Wir werden Arrays nicht weiter vertiefen, sehr gerne darfst du dir die R-Hilfe dazu
durchlesen (?array)! Gerne darfst du auch die Funktion ftable() auf tab3 anwen-
den, sie macht ein Array flach.
> tab
Matura Lehre
West 3 2
Nord 2 3
Ost 2 3
Süd 2 3
Zentrum 3 1
> # Gesamtsumme
> margin.table(tab)
[1] 24
Mit der Funktion addmargins() können wir auch andere Randfunktionen be-
rechnen und deren Ergebnisse anhängen. Wie das geht, schauen wir uns in Übung 3
auf Seite 346 an.
Wir können Kreuztabellen nicht nur mit Häufigkeiten, sondern auch mit belie-
bigen Maßzahlen befüllen. Für diesen Zweck eignet sich tapply().
Die Variablen, nach denen wir den Datenvektor aufsplitten wollen, übergeben wir
dabei dem Parameter INDEX als Liste.
Beispiel: Wie viel Gewinn haben die Vertreter jeder Ausbildung in jedem Gebiet
insgesamt erzielt?
1. Gegeben ein Gebiet: Welcher Anteil des Gewinns wurde von Vertretern der
Ausbildung Matura bzw. Lehre gemacht (Zeilenprozent)?
2. Gegeben eine Ausbildung: Wie viel Prozent des Gewinns entfallen auf die
einzelnen Gebiete (Spaltenprozent)?
3. Wie viel Prozent des Gewinns entfallen auf jede einzelne Kategorienkombina-
tion von Gebiet und Ausbildung (Totalprozent)?
All diese Fragen können wir bequem mit der Funktion prop.table() beantworten.
So wurden rund 14.2 Prozent des gesamten Gewinns von Vertretern mit der Aus-
bildung Matura im Gebiet West erzielt. Insgesamt entfallen rund 58.3 Prozent auf
Vertreter mit der Ausbildung Matura.
So entfallen beispielsweise 24.3 Prozent des gesamten Gewinns, der von Vertretern
der Ausbildung Matura erzielt wurde, auf das Gebiet West.
Frage 1: Zeilenprozente berechnen
Die Zeilenprozente bestimmen wir völlig analog wie Spaltenprozente.
So entfallen beispielsweise 68.5 Prozent des gesamten Gewinns, der im Gebiet West
erzielt wurde, auf Vertreter mit der Ausbildung Matura.
Mit tapply() können wir einen Vektor nach einem oder mehreren Faktoren aufsplit-
ten und auf jede gewonnene Teilmenge eine Funktion anwenden. Doch was tun wir,
wenn wir eine solche Aufsplittung für mehrere Vektoren gleichzeitig durch-
führen wollen? In dem Fall bietet sich die Funktion aggregate() an. Sie ist in
verschiedenen Varianten verfügbar, wir betrachten die Version für Dataframes.
Wir listen in Tab. 25.2 auf der nächsten Seite die Parameter dieser Funktion auf.
Beachte insbesondere, dass wir für by immer eine Liste benötigen!
Zur Demonstration erweitern wir unseren Vertreterdatensatz um eine Variable Bonus:
Jeder Vertreter bekommt 1000 Euro plus 10 Prozent seines erzielten Gewinns.
Parameter Bedeutung
x Dataframe mit allen Variablen, für die wir eine Berechnung
vornehmen wollen.
by eine Liste von Faktoren bzw. Trennvariablen
FUN Die Funktion, die auf jede Spalte von x angewendet werden soll.
... weitere Parameter für FUN
simplify Soll die Datenstruktur des Ergebnisobjektes vereinfacht werden?
TRUE ist Standard.
drop Sollen Kombinationen, in die keine Beobachtungen fallen,
entfernt werden? TRUE ist Standard.
Beispiel: Berechne für die Variablen Gewinn und Bonus des Dataframes daten auf
der vorherigen Seite den Mittelwert getrennt nach Gebiet.
Mit der Funktion tapply() müssten wir folgendes eingeben:
Wir wollen uns an dieser Stelle lieber nicht vorstellen, wie es aussehen würde, wenn
wir mehr als zwei Variablen hätten. Wenn du in Übungsaufgabe 4 nach demselben
Prinzip vorgehst, dann wärme bitte deine Finger gut auf! Wer bewahrt uns vor
drohendem Muskelkater in den Fingern? aggregate()!
Wir übergeben aggregate() ein Dataframe mit jenen Variablen aus daten, die uns
interessieren. Der Parameter by verlangt immer eine Liste. Da ein Dataframe auch
eine Liste ist, können wir die Variable Gebiet als Dataframe übergeben.
25 Aggregation und Kreuztabellen 343
Alle Kreuzkombinationen aus Gebiet und Ausbildung werden gebildet und die ent-
sprechenden Mittelwerte für die Variablen Gewinn und Bonus berechnet.
Jetzt können wir aus dem Dataframe einfach bestimmte Elemente selektieren:
> # Durchschnittswerte der Gebiete West, Nord und Ost für Ausbildung Matura
> daten.aggr[daten.aggr$Gebiet %in% c("West", "Nord", "Ost") &
+ daten.aggr$Ausbildung == "Matura", ]
Gebiet Ausbildung Gewinn Bonus
1 West Matura 10.53333 2.053333
2 Nord Matura 8.25000 1.825000
3 Ost Matura 16.20000 2.620000
Wegen der Standardeinstellung drop = TRUE werden nur nichtleere Elemente ange-
zeigt. In obigem Beispiel ist keine Kategorienkombination leer.
Wenn wir hingegen nach Ausbildung und Gewinnstufe aufsplitten, so fällt uns auf,
dass die Kombination aus Matura und wenig in folgendem Output fehlt.
Wenn wir auf bestimmte Variablen eines Dataframes zugreifen, so müssen wir diese
Variablen selektieren. Entweder mit dem Dollaroperator oder mit eckigen Klammern.
Wie wäre es, wenn wir uns diese Selektion ersparen und direkt auf die Variablen
zugreifen könnten?
Wenn wir diesen Ausdruck mit der Funktion with() ummanteln und als erstes Ar-
gument das Dataframe daten übergeben, so sind die Spalten von daten innerhalb
von with() lokal verfügbar.
Daneben gibt es eine andere Möglichkeit, Variablen eines Dataframes direkt verfüg-
bar zu machen, die in vielen Fällen die Laufzeit eines Codes senken kann.
> Gewinn
Fehler: Objekt ’Gewinn’ nicht gefunden
> # attach() rückgängig machen und die Variablen von daten wieder einpacken
> detach(daten)
Die Funktion attach() macht alle Subobjekte von daten global verfügbar, mit
detach() packen wir die Objekte wieder ein.
25 Aggregation und Kreuztabellen 345
> Gewinn
[1] 50000
> tapply(Gewinn, Ausbildung, mean)
Fehler in tapply(Gewinn, Ausbildung, mean) :
Argumente müssen die selbe Länge haben
> detach(daten)
Die Variable Gewinn, die wir außerhalb von daten definiert haben, behält die Ober-
hand und drängt die Spalte Gewinn von daten in den Hintergrund!
25.5 Abschluss
• Wie können wir einen Vektor nach einer oder mehreren Variablen aufsplitten
und eine Funktion auf jede Teilmenge anwenden? Wie wählt dabei die Funktion
tapply() die Datenstruktur des Ergebnisobjektes? (25.1)
• Wie erstellen wir einfache Kreuztabellen mit Häufigkeiten? (25.2.1)
• Wenn wir der Funktion table() mehr als zwei Variablen übergeben, entsteht
ein Array. Wie sieht ein Array aus? Wie selektieren wir Elemente aus einem
Array? Wie verhält sich apply() in Zusammenhang mit Arrays? (25.2.2)
• Wie bestimmen wir Randsummen von Kreuztabellen und wie hängen wir die
Randsummen an Tabellen an? (25.2.3)
• Wie erstellen wir Kreuztabellen mit beliebigen Maßzahlen? (25.2.4)
• Wie bestimmen wir die Zeilen-, Spalten- und Totalprozente einer zweidimen-
sionalen Häufigkeitstabelle? Wie hängen wir diese Informationen korrekt an
die Tabelle an? (25.2.5)
346 E Tools für Data Science und Statistik
• Wie können wir mehrere Variablen gleichzeitig nach einer oder mehreren Va-
riablen aufsplitten und eine Funktion auf jede entsprechende Teilmenge an-
wenden? Welche Datenstruktur hat das Ergebnisobjekt? Wie stellen wir ein,
ob leere Kategoriekombinationen angezeigt werden sollen oder nicht? (25.3)
• Was macht die Funktion with() und wie wenden wir sie an? (25.4.1)
• Wie können wir Variablen eines Dataframes global verfügbar machen und sie
wieder einpacken? Was passiert, wenn eine Variable des Dataframes bereits
existiert, es also zu einem Namenskonflikt kommt? (25.4.2)
25.5.2 Ausblick
In (29) und (30) befassen wir uns unter anderem mit Environments und Scoping und
gehen dort der Frage nach, wie R vorgeht, wenn es mehrere gleichnamige Objekte
an unterschiedlichen Stellen gibt. Einen kleinen Vorgeschmack haben wir in (25.4.2)
erlangt.
Die Funktion aggregate() gibt es in vielen Versionen. Eine sehr häufige Variante
benützt Formelobjekte, mit deren Hilfe wir einfach und elegant statistische Modelle
bauen können. Formelobjekte besprechen wir ausführlich in (38.1). Du bist herzlich
eingeladen, dir nach diesem Kapitel die Funktion aggregate.formula() anzusehen!
Als Alternative für aggregate() bietet sich auch die Funktion by() an. Auch diese
Funktion darfst du dir bei Gelegenheit zu Gemüte führen.
25.5.3 Übungen
a) Erstelle eine Kreuztabelle, welche den minimalen Gewinn für jede Kate-
gorienkombination aus Gebiet und Ausbildung enthält.
Die Variablen Month und Day sind selbsterklärend. Die Daten wurden von der
New York State Department of Conservation (Ozondaten) sowie vom National
Weather Service (meteorologische Daten) bezogen.
Implizit sind uns Klassen, generische Funktionen und Attribute schon oft unterge-
kommen. In diesem Kapitel klären wir, was sich hinter diesen Begriffen verbirgt.
Wenn wir etwa Objekte auf die Console drucken wollen, so entscheidet das Objekt,
wie es auf die Console gedruckt werden will. Wie das funktioniert, schauen wir uns
unter anderem an folgenden beiden Beispielvektoren an:
> geschlecht
[1] Mann Frau Frau Mann
Levels: Mann Frau
> alter
[1] 23 26 19 27
Das Objekt geschlecht ist ein Faktor (vgl. (24)), alter ein numerischer Vektor.
Wir klären unter anderem folgende Fragen:
• Was passiert intern, wenn wir die beiden Objekte geschlecht und alter mit
print() auf die Console drucken? (26.1)
• Wie greifen wir auf das Attribut levels des Faktors geschlecht direkt zu?
Und wie legen wir die interne Verwaltung der Objekte offen? (26.2)
Abschließend schauen wir uns in (26.3.1) ein cooles Beispiel an: Run Length Enco-
ding. Dabei erfahren wir, wie wir uns das Attribut match.length, das die Funktion
gregexpr() (vgl. (22.2.2)) erzeugt, zunutze machen können.
Eine Klasse ist ein Schema, das es uns ermöglicht, ähnliche Objekte zusammen-
zufassen. Zwei Objekte sind sich ähnlich, wenn sie dieselben Eigenschaften besitzen.
Wenn wir zum Beispiel zwei Faktoren hernehmen, dann können sie völlig verschiede-
ne Informationen verwalten. Was aber alle Faktoren vereint (und sie damit ähnlich
in obigem Sinne macht): Sie bestehen aus einem numerischen Codevektor und aus
einem Attribut levels, das die Beschriftungen zu den codierten Werten enthält.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_26
26 Klassen, generische Funktionen und Attribute 349
Mit der Funktion class() können wir von einem Objekt die Klasse abfragen.
Jetzt haben wir es blau auf weiß, dass geschlecht ein Objekt der Klasse factor
und dass alter ein Objekt der Klasse numeric ist.
Es scheint klar zu sein, dass ein Faktor anders gedruckt werden muss, als etwa ein
normaler numerischer Vektor. Beim Faktor müssen/sollen die levels mitgedruckt
werden. Bei einem numerischen Vektor hingegen existieren keine levels.
> # Drucke einen Faktor mit levels > # Drucke einen numerischen Vektor
> print(geschlecht) > print(alter)
[1] Mann Frau Frau Mann [1] 23 26 19 27
Levels: Mann Frau
> print
function (x, ...)
UseMethod("print")
<bytecode: 0x0000000016cc7568>
<environment: namespace:base>
generischeFunktion.Klasse()
Mit der Funktion methods() können wir alle spezifischen Funktionen abrufen.
So hat etwa die Funktion print() weit über 100 davon. Ein kleiner Auszug:
Klasse factor (wir verwenden das Objekt geschlecht aus der Einleitung):
> class(geschlecht)
[1] "factor"
Da es eine spezielle Funktion für Faktoren gibt, wird diese auch verwendet.
Klasse data.frame:
Wir lernen schnell die bequeme Anwendbarkeit von generischen Funktionen schät-
zen! Anstatt mehrere Funktionsversionen von print() auswendig lernen zu müssen,
genügt es, die Hauptfunktion zu kennen, den Rest erledigt R.
Bemerkung: Wird in der Console zur Laufzeit geschlecht bzw. alter eingege-
ben, wird intern print(geschlecht) bzw. print(alter) aufgerufen. Bis auf eine
Ausnahme, die wir in (28) sehen werden.
Eine Auflistung einiger wichtiger generischer Funktionen (viele Funktionen führen
wir erst später ein):
Mit unclass() können wir das Klassenattribut entfernen und damit die interne
Verwaltungsstruktur betrachten. Sehen wir uns ein Codebeispiel an.
Wir erkennen ein weiteres Merkmal vieler Objekte: Attribute (attributes). Ein
Faktor hat das Attribut levels, das die Beschriftungen des Faktors speichert.
Mit der Funktion attributes() können wir auf Attribute zugreifen.
Jetzt können wir aus der Liste jenes Attribut selektieren, an dem wir interessiert
sind. Für viele Attribute existieren daneben eigene Zugriffsfunktionen, wie wir im
folgenden Code erkennen.
> # Zugriff auf Attribut class > # Zugriff auf Attribut levels
> attributes(geschlecht)$class > attributes(geschlecht)$levels
[1] "factor" [1] "Mann" "Frau"
Wir werfen eine faire Münze k = 50 Mal und wollen erfahren, wie oft dabei im-
mer Kopf hintereinander geworfen wird, ohne Unterbrechung. Wir sind also an den
sogenannten Run Lengths von Kopf interessiert.
Frage: Wie bestimmen wir, wie oft jeweils hintereinander Kopf geworfen wurde?
Zur Bestimmung dieser Anzahlen gibt es mehrere mögliche Zugänge. Wir präsentie-
ren eine Lösung, die auf gregexpr() beruht (siehe (22.2.2)).
Jetzt kommt die Schlüsselstelle! Mit gregexpr() suchen wir mit dem Suchmuster
"K+" nach allen konsekutiven Vorkommen von Kopf.
Das Attribut match.length gibt hier die Anzahl der unmittelbar aufeinanderfolgen-
den Köpfe an. Genau diese Information wollen wir haben!
Es wurde also 5 Mal nur ein Kopf in Folge geworfen, 3 Mal zwei Köpfe, ein Mal 4
Köpfe und ein Mal 9 Köpfe. Sehr gerne laden wir dich dazu ein, eine lückenlose
Häufigkeitstabelle zu erstellen ;-)
26 Klassen, generische Funktionen und Attribute 353
26.4 Abschluss
• Was ist eine Klasse? Wie erfragen wir die Klasse eines Objekts? Was ist eine
generische Funktion und wie erkennen wir, ob eine Funktion generisch ist? Was
passiert intern beim Aufruf einer generischen Funktion? (26.1)
• Wie entfernen wir das Klassenattribut eines Objektes? Wie können wir auf die
Attribute eines Objektes zugreifen? (26.2)
26.4.2 Ausblick
In (30.6.1) erstellen wir in einem Fallbeispiel die Klasse Bruch zur Verwaltung von
Brüchen. Dabei schreiben wir nicht nur eine maßgeschneiderte print()-Funktion
für unsere Brüche, sondern auch eigene Rechenoperatoren.
Grafikfunktionen sind ein Paradebeispiel dafür, wie sinnvoll das Konzept von gene-
rischen Funktionen ist. Mit Grafiken befassen wir uns in (34), (35) und (36).
26.4.3 Übungen
c) Wende summary() auf die beiden Vektoren alter und geschlecht aus der
Einleitung auf Seite 348 an. Ergeben die Ergebnisse einen Sinn? Erkläre
die internen Abläufe, die zu diesem Ergebnis führen.
a) Finde eine andere Möglichkeiten, die Run Lengths von Kopf zu bestim-
men, die nicht auf Stringfunktionen beruht (und auch nicht auf rle()).
b) Bestimme nun mit Hilfe der Funktion rle() die Run Lengths von Kopf.
a) Finde heraus, welches Attribut für die Beschriftungen der Zeilen und Spal-
ten einer Matrix steht.
Hinweis: Möglicherweise wird das Attribut erst dann sichtbar, sobald
Zeilen- und Spaltenbeschriftungen übergeben wurden.
b) Weise nun mit Hilfe von Attributen der Matrix Zeilen- und Spaltenbe-
schriftungen zu.
Hinweis: Attribute können wir mit Hilfe folgender Syntax ändern bzw.
zuweisen:
attributes(objekt ) <- inhalt
27 Datums- und Uhrzeitobjekte 355
Weißt du, an welchem Wochentag du auf die Welt gekommen bist? Oder in wie
vielen Tagen dein nächster Geburtstag ist? Spätestens nach diesem Kapitel weißt du
es und kannst noch viele weitere spannende Fragen im Zusammenhang mit Datum
und Uhrzeit beantworten.
Wir betrachten in diesem Kapitel unsere Geburtsdaten, also deine und jene der
Autoren dieses Buches.
Ersetze dabei im obigen Code Neujahr98 durch deinen Namen und 01.01.1998
durch deinen Geburtstag. In (27.3.1) erzeugen wir aus diesem Stringvektor das Da-
tumsobjekt gebtag.Date, mit dem wir folgende spannende Fragen beantworten:
• An welchen Wochentagen sind wir jeweils auf die Welt gekommen? (27.3.1)
• Wie alt sind wir jeweils zum heutigen Zeitpunkt? (27.5)
• In wie vielen Tagen ist jeweils unser nächster Geburtstag? (27.5)
Bevor es soweit ist, klären wir einleitend unter anderem folgende Fragen:
• Wie erfragen wir das aktuelle Datum bzw. die aktuelle Uhrzeit? (27.1.1)
• Wie werden Datums- und Uhrzeitobjekte intern verwaltet? (27.1.2)
• Wie formatieren wir Datum und Uhrzeit nach unseren Wünschen? (27.2)
Während auf einem deutschsprachigen Rechner der Wochentag auf Deutsch ausge-
geben wird (z. B. Montag), so wird dieser in anderen Ländern in anderen Sprachen
ausgegeben (z. B. Monday in den USA, Lundi in Frankreich etc.). Darauf müssen wir
bei bestimmten Aufgaben aufpassen; in (27.6) gehen wir darauf ein, wie wir unseren
Code sprachenunabhängig gestalten können.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_27
356 E Tools für Data Science und Statistik
Das heutige Datum können wir mit Sys.Date() abrufen. Möchten wir zusätzlich
die aktuelle Uhrzeit haben, so bietet sich Sys.time() an.
Wir wissen also, wann dieses Kapitel letztmalig vor Drucklegung erstellt wurde.
R orientiert sich bei der Darstellung des Datums und der Uhrzeit an ISO 8601:
YYYY-MM-DD, also Jahr-Monat-Tag. Die Uhrzeit wird im Format hh:mm:ss darge-
stellt, also Stunden:Minuten:Sekunden. Zusätzlich wird die Zeitzone angezeigt: CET
steht für Central European Time und CEST für Central European Summer Time.
Zur Verwaltung des Datumsobjektes date bzw. des Uhrzeitobjektes time werden die
Klassen Date bzw. POSIXct (Portable Operating System Interface calendar time)
verwendet. Betrachten wir, wie diese Klassen die Informationen intern verwalten.
Die Funktion unclass() leistet uns in diesem Zusammenhang wertvolle Dienste.
> # Interne Verwaltung von Date > # Interne Verwaltung von POSIXct
> unclass(date) > unclass(time)
[1] 18498 [1] 1598284211
Folgende Klassen zur Verwaltung von Datums- und Uhrzeitobjekten gibt es:
Damit wissen wir, was die Zahlen am Ende von (27.1.1) bedeuten.
27 Datums- und Uhrzeitobjekte 357
Die dritte Klasse POSIXlt vernachlässigen wir in diesem Buch aus Übersichtsgrün-
den. Alle Anwendungen dieses Kapitels funktionieren mit POSIXlt-Objekten völlig
analog, du bist aber natürlich herzlich eingeladen, dir selbst ein Bild von dieser
Klasse zu machen! as.POSIXlt sowie ?as.POSIXlt sind zwei erste Anlaufstellen ;-)
Mit den drei Funktionen as.Date(), as.POSIXct() und as.POSIXlt() können wir
Datums- und Uhrzeitobjekte in die entsprechende Klasse konvertieren. Bei der
Konvertierung eines Uhrzeitobjektes in die Klasse Date geht die Uhrzeitinformation
verloren.
> time
[1] "2020-08-24 17:50:11 CEST"
Wir sehen, dass nach der Rückkonvertierung die Uhrzeit nicht mehr stimmt. Sie
wurde auf 02:00:00 geändert.
Uns fällt auf, dass das Datum im Format YYYY-MM-DD ausgegeben wird. Da drängt
sich folgende naheliegende Frage auf: Wie können wir Datums- und Uhrzeitobjek-
te in einem beliebigen Format ausgeben? Die Antwort lautet: mit format()
(siehe (27.2.1)). Für die Extraktion bestimmter Datumsinformationen gibt es eigene
Funktionen, die wir uns kurz in (27.2.2) ansehen.
Für die Klassen Date und POSIXct gibt es jeweils eine eigene format()-Funktion
(format() ist generisch). Mit ihr können wir aus einem Date- bzw. POSIXct-Objekt
eine formatierte Zeichenkette generieren, deren Gestalt wir selbst bestimmen.
Für die Klasse POSIXct gibt es die Funktion format.POSIXct().
358 E Tools für Data Science und Statistik
Für x setzen wir ein Objekt der Klasse POSIXct ein. Mit format steuern wir, wie das
Datum und die Zeit ausgegeben werden sollen. Mit den Parametern tz und usetz
können wir die Zeitzone (Time Zone) einstellen. Dieses Detail blenden wir aber aus.
Dem Parameter format übergeben wir eine Zeichenkette mit dem gewünschten For-
mat. Datums- und Uhrzeitinformationen können wir dabei mit Hilfe von Platzhaltern
einfügen. In Tab. 27.1 listen wir einige wichtige Platzhalter auf. Eine vollständige
Liste aller Platzhalter findest du unter ?format.POSIXct.
Tabelle 27.1: Ausgewählte Platzhalter für Datums- und Uhrzeitobjekte. Oben: Da-
tumsplatzhalter. Unten: Uhrzeitplatzhalter
Schauen wir uns für POSIXct-Objekte an, wie das Ganze funktioniert.
Beispiel: Erzeuge aus der aktuellen Uhrzeit Zeichenketten der folgenden Bauart:
Wir orientieren uns in diesem Buch am Objekt time, das wir in (27.1.1) erstellt
haben. Bei dir wird abhängig davon, wann du dieses Beispiel nachprogrammierst,
etwas anderes herauskommen, wenn du die Zeile time <- Sys.time() ausführst.
> # 1.)
> format(time, format = "Es ist %H:%M Uhr.")
[1] "Es ist 17:50 Uhr."
> # 2.)
> format(time, format = "%d.%m.%Y %H:%M:%S")
[1] "24.08.2020 17:50:11"
> # 3.)
> format(time, format = "%A, der %d. %B %Y")
[1] "Montag, der 24. August 2020"
Wir sehen, wie leicht wir mit den Platzhaltern Datums- und Uhrzeitinformationen
einsetzen können.
Die Funktion format.Date() für Date-Objekte funktioniert völlig gleich, lediglich
die Platzhalter für Uhrzeitinformationen sind obsolet.
> # 3.)
> format(time.Date, "%A, der %d. %B %Y")
[1] "Montag, der 24. August 2020"
Mit den Funktionen weekdays(), months() und quarters() können wir aus einem
Datumsobjekt den Wochentagsnamen, Monatsnamen und das Quartal be-
stimmen. Die beiden erstgenannten Funktionen stellen uns jeweils den Parameter
abbreviate zur Verfügung. Setzen wir ihn auf TRUE, so werden die Wochentags-
bzw. Monatsnamen abgekürzt. Die Quartale werden in Form von "Q1" bis "Q4"
zurückgegeben.
Beispiel: Welcher Wochentag bzw. Monat ist heute? In welchem Quartal befinden
wir uns aktuell?
Wir orientieren uns in diesem Buch am Objekt date, das wir in (27.1.1) erstellt
haben. Wenn du die Zeile date <- Sys.Date ausführst, kommt bei dir natürlich
etwas anderes heraus.
Wir betrachten zwei Varianten: Die rechte basiert auf format() während die linke
die soeben erwähnten Datumsfunktionen einsetzt.
Mit den Funktionen as.Date() bzw. as.POSIXct() erzeugen wir Objekte der Klasse
Date bzw. POSIXct. Dabei gibt es zwei Varianten:
Wir betrachten dabei lediglich die Variante für Datumsobjekte der Klasse Date. Für
Objekte der Klasse POSIXct funktioniert das Ganze genauso. Und falls du dich mit
POSIXlt beschäftigen willst, so darfst du dir gerne as.POSIXlt() anschauen.
27 Datums- und Uhrzeitobjekte 361
Mit der für Zeichenketten maßgeschneiderten Version von as.Date() können wir
aus einem Stringvektor ein Datumsobjekt erzeugen.
Für x übergeben wir einen Stringvektor, der die Datumsinformationen enthält. Mit
dem Parameter format zeigen wir R, wo sich welche Datumsinformation befindet.
Der Grund für die Fehlermeldung ist, dass R das Format nicht erkennen kann. R er-
wartet defaultmäßig das Standardformat YYYY-MM-DD oder YYYY/MM/DD, in unserem
Fall ist das Datum jedoch im Format DD.MM.YYYY gegeben.
Ist der String nicht in einem Standardformat gegeben, so müssen wir das R mit Hilfe
des Parameters format mitteilen. Dabei kommen dieselben Platzhalter zum Einsatz,
die wir schon in Tab. 27.1 auf Seite 358 kennengelernt haben.
Bemerkung: Bei as.POSIXct wird die Zeitzone hinzugefügt und bei as.Date()
gehen die Beschriftungen verloren. Wenn uns zweiteres stört, können wir sie aber
leicht wieder hinzufügen.
362 E Tools für Data Science und Statistik
Der Parameter origin beschreibt das Referenzdatum bzw. die Referenzuhrzeit, das
bzw. die idealerweise die entsprechende Klasse hat. Bei as.Date() ist x die Anzahl
der Tage, die wir vom Referenzdatum abweichen möchten (auch negative Werte sind
möglich). Bei as.POSIXct() sind es die Anzahl der Sekunden.
Beispiel: Generiere ausgehend vom heutigen Datum einen Datumsvektor, der den
vorgestrigen Tag, den heutigen Tag und jenen Tag in zwei Wochen enthält.
Wir verwenden in diesem Buch die Objekte date und time aus (27.1.1). Zunächst
erzeugen wir die gewünschten Datumswerte für Datumsobjekte der Klasse Date.
Der Vektor c(-2, 0, 14) enthält die gewünschten Abweichungen vom Referenzda-
tum, das in origin definiert wird.
27 Datums- und Uhrzeitobjekte 363
Bemerkung: Das Ganze funktioniert auch vektorwertig. Das heißt, wir können
sowohl x als auch origin Vektoren mit mehreren Elementen übergeben. Du ahnst
vielleicht schon, was passiert, wenn x und origin nicht gleich lang sind. Stichwort
Recycling (siehe (3.3)) ;-)
Wir können mit den Funktionen sort() und order() auch Datumswerte auf-
oder absteigend sortieren. Das funktioniert genauso, wie in (6.2) erklärt, daher
schauen wir uns nur kurz ein paar Codebeispiele an.
Erinnerst du dich noch an unser Fußballerinnenbeispiel aus (6.2.5)? Dort haben wir
mit Hilfe der Mehrfachsortierung die Namen von österreichischen Fußballspielerinnen
nach ihrem Alter sortiert. Gerne darfst du kurz auf Seite 78 zurückblättern und die
Sortierung mit den Techniken dieses Kapitels durchführen ;-)
Die Vergleichsoperatoren "<", "<=", "==", "!=", ">" und ">=" funktionieren
übrigens auch für Datums- und Uhrzeitobjekte!
Wir können mit Datumswerten auch rechnen. Dabei gibt es drei Varianten:
• Tage bzw. Sekunden zu einem Datum bzw. einer Uhrzeit hinzuaddieren oder
abziehen. (27.5.1)
• Paarweise Zeitdifferenzen berechnen, um die Zeitdifferenzen zweier in einem
Vektor aufeinanderfolgenden Elemente zu bestimmen. (27.5.2)
• Parallele Zeitdifferenzen berechnen, um die elementweisen Zeitdifferenzen zwei-
er Vektoren zu bestimmen. (27.5.3)
Wir können Objekten der Klassen Date bzw. POSIXct Tage bzw. Sekunden hin-
zuaddieren oder abziehen. Vom Prinzip her funktioniert das genauso wie in
(27.3.2) beschrieben.
Für POSIXct-Objekte funktioniert das völlig gleich, nur dass hier Sekunden statt
Tage hinzuaddiert oder abgezogen werden.
Mit der Funktion diff() können wir herausfinden, wie viele Tage zwischen zwei
Datumswerten liegen. Ja, auch diff() ist eine generische Funktion ;-)
> gebtag.Date
Daniel Andreas Neujahr98
"1986-07-07" "1973-09-12" "1998-01-01"
> diff(gebtag.Date)
Time differences in days
Andreas Neujahr98
-4681 8877
Wir erfahren im Output, dass Andreas 4681 Tage vor Daniel auf die Welt gekommen
ist und dass Neujahr98 8877 Tage nach Andreas’ Geburt stattgefunden hat.
27 Datums- und Uhrzeitobjekte 365
Das gleiche Ergebnis würden wir übrigens bekommen, wenn wir dem Datumsobjekt
die Klasse entziehen, bevor wir die Differenzen bilden.
Für Objekte der Klasse POSIXct wird die Zeitdifferenz bei Anwendung von diff()
wiederum in Sekunden angegeben.
Womit wir erfahren, wie viele Tage wir schon auf dieser Welt verbringen dürfen. Bei
Objekten der Klasse POSIXct wären es wiederum Sekunden.
Mit der Funktion difftime() können wir derlei Zeitdifferenzen auch in anderen
Einheiten berechnen. Der Parameter units macht es möglich.
Beachte: difftime() rechnet immer time1 minus time2. Wir übergeben also der
Funktion in der Regel zuerst den späteren Zeitpunkt (für time1).
Beispiel: Falco war ein berühmter österreichischer Musiker, der mit Songs wie
„Rock me Amadeus“ weltweite Erfolge erzielen konnte. Er hat von 19.02.1957 bis
06.02.1998 gelebt.
Wir sehen, dass die Wochen als Dezimalzahl ausgegeben werden. Interessieren wir
uns nur für ganze Wochen, so runden wir einfach ab.
Die 2. Frage ist nicht so einfach zu beantworten, da uns difftime() nicht die Op-
tion units = "years" anbietet. Da Jahre ungleich lang sind (Schaltjahre), würde
das keinen Sinn machen. Selbiges gilt für Monate. Jetzt könnten wir die Jahresinfor-
mation extrahieren und deren Differenz bestimmen, also 1998 − 1957 = 41 rechnen.
> # Extrahiere das Jahr der Geburt und des Todes und berechne Jahresdifferenz
> falco.year <- as.numeric(format(falco, "%Y"))
> falco.year
[1] 1957 1998
> diff(falco.year)
[1] 41
Aufmerksame R-Talente stellen jedoch fest, dass das Alter nicht stimmt.
Frage: Warum scheitert dieser Ansatz? Und wie reparieren wir den Ansatz?
Grund des Scheiterns: Wir nehmen implizit an, dass Falco seinen 41. Geburtstag
im Todesjahr noch erlebt hat. Das ist aber hier nicht der Fall: Falco ist am 06.02.
gestorben, also einige Tage vor seinem Geburtstag am 19.02. Du weißt schon: Achte
immer auf implizite Annahmen (vgl. Regel 6 auf Seite 57)!
Wir könnten überprüfen, ob Falco seinen Geburtstag im Todesjahr noch erlebt hat.
Falls ja, stimmt das Ergebnis, andernfalls müssen wir das Alter um 1 reduzieren.
Dazu berechnen wir zunächst den Geburtstag im Jahr des Todes. Dazu verknüpfen
wir den Tag und Monat der Geburt mit dem Todesjahr.
Jetzt erfragen wir, ob Falco im Jahr seines Todes seinen Geburtstag noch erlebt hat.
Mit anderen Worten: War sein Tod nach dem Geburtstag im Todesjahr?
Falls das Ergebnis FALSE ist (was hier der Fall ist), dann ziehen wir von der Jahres-
differenz eine 1 ab.
Falco ist also nur 40 Jahre alt geworden. Möge er am Wiener Zentralfriedhof in
Frieden ruhen!
Beispiel: Wie alt sind wir drei jeweils heute?
Selbe Idee wie bei Falco im vorherigen Beispiel, nur etwas vektorwertiger ;-)
> gebtag.Date
Daniel Andreas Neujahr98
"1986-07-07" "1973-09-12" "1998-01-01"
> geb.jahr
[1] 1986 1973 1998
> heute.jahr
[1] 2020
Stopp! Es ist wieder einmal an der Zeit, unsere Aufmerksamkeit zu schulen und
Regel 6 in Erinnerung zu rufen (Achte auf implizite Annahmen). Es ist nicht offen-
sichtlich, aber obiger Code funktioniert leider nicht immer.
Frage: Unter welchen Umständen funktioniert obiger Code nicht korrekt? Nimm
dir zwei Minuten Zeit und überlege dir eine Antwort.
Schon eine Idee? Kleiner Tipp: Manche Jahre haben mehr Tage als andere ;-)
Kommen wir zur Auflösung. Implizit nehmen wir oben an, dass niemand am 29.02.
Geburtstag hat. Stell dir einmal vor, du wärst am 29.02.1996 auf die Welt gekommen
und würdest den Code am 17.03.2021 durchlaufen lassen. Dann würde in geb.heuer
der 29.02.2021 generiert werden. Diesen Tag gibt es aber nicht, da 2021 kein Schalt-
jahr ist, wodurch bei der Umwandlung mit as.Date() ein NA herauskäme.
Wie reparieren wir nun diesen Fehler? Zum Beispiel so: Wir wissen, dass jedes NA für
einen 29.02. stehen muss, da der 29.02. der einzige Tag ist, der nur in Schaltjahren
existiert. Wenn wir also NA abfangen und durch den 01.03. ersetzen, dann können
wir das Alter auch für diesen speziellen Tag korrekt bestimmen. Machen wir uns ans
Werk! Um unseren Code zu testen, fügen wir kurzerhand Felix2902 hinzu.
Jetzt kommt die Korrektur. Wir ersetzen in geb.heuer jedes Vorkommen von NA
durch den 01.03. des aktuellen Jahres.
Jetzt können wir das aktuelle Alter auch für jene Personen zuverlässig bestimmen,
die an einem 29.02. Geburtstag haben.
Der Code funktioniert nach der Korrektur auch in Schaltjahren korrekt. Denn in
Schaltjahren existiert ja der 29.02. und es entsteht somit kein NA, womit keine Er-
setzung in geb.heuer vorgenommen wird.
Wir belassen Felix2902 für unser nächstes Beispiel in der Liste.
Beispiel: Fortsetzung des vorangehenden Beispiels. In wie vielen Tagen / Wochen
haben wir jeweils unseren nächsten Geburtstag?
Auch bei dieser Frage müssen wir überprüfen, ob wir noch heuer Geburtstag ha-
ben. Wenn du tagesaktuelle Zahlen haben willst, dann führe zunächst den Code des
letzten Beispiels mit date <- Sys.Date() aus.
Wenn wir heuer schon Geburtstag hatten, so feiern wir (frühestens) im nächsten
Jahr wieder. Jetzt bauen wir uns das Datum des nächsten Geburtstages zusammen.
370 E Tools für Data Science und Statistik
Für Felix2902 kommt NA heraus, selber Grund wie im vorangehenden Beispiel. Die
Reparaturidee hier: Wir bestimmen das Datum des nächsten 29.02. ab dem heutigen
Tag. Dazu müssen wir zunächst bestimmen, ob der 01.03. in diesem Jahr schon
stattgefunden hat. Wenn nicht, so ist der 29.02. in diesem Jahr der Ausgangspunkt
unserer Suche, andernfalls der 29.02. des folgenden Jahres.
Ausgehend von dem soeben bestimmten Ausgangsjahr muss das nächste Schaltjahr
spätestens in sieben Jahren auftreten (manchmal entfallen die Schaltjahre, wie etwa
im Jahr 2000), daher addieren wir noch 0:7. Damit generieren wir alle Kandidaten-
jahre und in weiterer Folge alle Kandidatendatumswerte (date2902).
Der erste Eintrag von date2902, der nicht NA gleicht, ist das gesuchte Datum. Wir
ersetzen nun in geb.next jedes Auftreten von NA durch dieses Datum.
Sollte bei dir 0 herauskommen, dann wünschen wir dir alles Gute zu deinem heutigen
Geburtstag! Solltest du tatsächlich an einem 29.02. Geburtstag haben und dir die
Wartezeit auf den nächsten 29.02. zu lange sein, darfst du natürlich auch gerne nach
dem nächsten 01.03. suchen ;-)
Der linke Code funktioniert nicht weltweit. In anderen Ländern gibt weekdays()
den Wochentag in einer anderen Sprache zurück. So würde in den USA "Tuesday"
herauskommen; die Abfrage weekdays(datum) == "Dienstag" würde daher fälsch-
licherweise FALSE ergeben.
Im rechten Code sind wir abgesichert. Wir wissen aus Tab. 27.1 auf Seite 358, dass
%u für den Wochentag als Zahl steht (von "1" = Montag bis "7" = Sonntag). Die
"2" steht für den Dienstag, und das gilt überall!
Ähnlich verhält es sich beim Monat: Anstatt mit months() zu arbeiten oder den
Platzhalter %B zu verwenden, sollten wir den Platzhalter %m nehmen.
Wir werden auf Locales nicht weiter eingehen; sehr gerne darfst du aber eine Re-
cherche in Eigenregie durchführen!
372 E Tools für Data Science und Statistik
Beispiel: Erstelle einen Datumsvektor, der alle ersten Samstage der Jahre 2021,
2022 und 2023 enthält.
Wir betrachten zwei Strategien, um diese Aufgabe zu bewältigen.
Strategie 1: Selektionsstrategie. Präsentiert in zwei Schritten:
1. Erzeuge einen Datumsvektor mit den ersten 7 Tage der Jahre 2021 bis 2023.
2. Selektiere aus diesem Datumsvektor alle Samstage.
Da eine Woche 7 Tage hat, können wir uns sicher sein, dass es in den ersten 7 Tagen
eines Jahres genau einen Samstag gibt. Setzen wir die Strategie um.
> # 1.) Datumsvektor für die ersten 7 Tage aller Jahre erzeugen.
> jahre <- rep(c(2021, 2022, 2023), each = 7)
> datum.string <- paste(1:7, 1, jahre, sep = ".")
> datum.string
[1] "1.1.2021" "2.1.2021" "3.1.2021" "4.1.2021" "5.1.2021" "6.1.2021"
[7] "7.1.2021" "1.1.2022" "2.1.2022" "3.1.2022" "4.1.2022" "5.1.2022"
[13] "6.1.2022" "7.1.2022" "1.1.2023" "2.1.2023" "3.1.2023" "4.1.2023"
[19] "5.1.2023" "6.1.2023" "7.1.2023"
Hierbei haben wir uns geschickt des Recyclings bedient! 1:7 muss drei Mal repliziert
werden, um dieselbe Länge wie jahre zu haben.
> # Kontrolle
> weekdays(res)
[1] "Samstag" "Samstag" "Samstag"
Damit der Code nicht nur auf deutschsprachigen PCs funktioniert, verwenden wir
den Platzhalter %u, der den Wochentag als Zahl zurückgibt. Jetzt brauchen wir nur
noch abzuzählen, dass die Zahl "6" für den Samstag steht. Fertig :-)
> # 2.) Anzahl der Tage bis zum nächsten Samstag berechnen
> tag <- as.numeric(format(datum, "%u"))
> tag
[1] 5 6 7
Wir suchen den nächsten Samstag, der mit 6 codiert ist. tag gibt an, welcher Wo-
chentag jeweils der 01.01. ist. Ist dieser Tag x kleiner als 6, dann kommt der nächste
Samstag in 6 − x Tagen. Ist dieser Tag x gleich 6, dann ist der 01.01. bereits der
Samstag; der nächste Samstag ist also in 6 − x = 0 Tagen. Ist dieser Tag x größer
als 6, dann kommt der nächste Samstag erst nächste Woche, und zwar in 6 + 7 − x
Tagen.
Wir überzeugen uns, dass der folgende Code alle drei Fälle korrekt abdeckt.
> # Kontrolle
> weekdays(res)
[1] "Samstag" "Samstag" "Samstag"
Die in diesem Kapitel erwähnten drei Klassen folgen hinsichtlich der Anwendungs-
möglichkeiten folgender Hierarchie:
3 Mit diff() können wir paarweise Zeitdifferenzen eines Datumsvektors in Tagen berechnen.
374 E Tools für Data Science und Statistik
Mit POSIXlt könnten wir darüber hinaus auch auf alle Datums- und Uhrzeitat-
tribute direkt zugreifen.
In der statistischen Praxis werden die Klassen Date bzw. POSIXct (falls Tage nicht
genau genug sind) häufig in Dataframes verwendet. Sie verbrauchen weniger Spei-
cherplatz als POSIXlt-Objekte, die Datumsattribute in Listen verwalten.
Zu Weihnachten feiert die christliche Kirche die Geburt von Jesu Christi. Der
Heiligabend ist jedes Jahr am 24.12. In den Wochen vor Weihnachten schließt
sich die Adventzeit an; die vier Sonntage vor Weihnachten sind die vier Advent-
sonntage. Fällt dabei Heiligabend selbst auf einen Sonntag, so ist er gleichzeitig
auch der 4. Adventsonntag.
Wir lassen uns an dieser Stelle von der Berechnungsstrategie auf Seite 372 inspi-
rieren. Du darfst gerne auch die Selektionsstrategie umsetzen!
4 Mit diff() können wir paarweise Zeitdifferenzen eines Uhrzeitvektors in Sekunden berechnen.
27 Datums- und Uhrzeitobjekte 375
Wir überzeugen uns davon, dass tag %% 7 die Anzahl der Tage bestimmt, die wir
vom Heiligabend abziehen müssen, um den 4. Adventsonntag zu erhalten. Fällt der
Heiligabend auf einen Sonntag ("7"), so ist er gleichzeitig auch der 4. Adventsonntag.
Das ist der einzige Grund, warum wir die Modulofunktion brauchen.
Vom 4. Adventsonntag ziehen wir anschließend noch 3 Wochen (= 21 Tage) ab, um
das Datum des 1. Adventsonntages zu berechnen.
27.8 Abschluss
• Wie rufen wir das heutige Datum bzw. die aktuelle Uhrzeit ab? In welchem
Format wird das Datum bzw. die Uhrzeit standardmäßig ausgegeben? (27.1.1)
• Welche drei Klassen für Datums- und Uhrzeitobjekte stehen uns zur Verfü-
gung? Wie werden dabei Datums- und Uhrzeitobjekte intern verwaltet? Wie
konvertieren wir Objekte in eine andere Klasse um? (27.1.2), (27.1.3)
• Wie formatieren wir Datums- und Uhrzeitvektoren? Welche Möglichkeiten ha-
ben wir, den Wochentag, den Monat und das Quartal zu erfragen? Ist die
Ausgabe des Wochentagnamens bzw. des Monatsnamens überall gleich? (27.2)
• Wie erzeugen wir Objekte der Klassen Date und POSIXct aus Strings? Wie
teilen wir dabei R mit, an welchen Stellen sich die relevanten Datums- und
Uhrzeitinformationen im String befinden? (27.3.1)
• Wie erzeugen wir mit Hilfe eines Referenzdatums bzw. einer Referenzuhrzeit
der Klassen Date bzw. POSIXct neue Objekte? (27.3.2)
• Wie sortieren und vergleichen wir Datums- und Uhrzeitvektoren? (27.4)
• Was passiert, wenn wir zu Objekten der Klasse Date bzw. POSIXct Zah-
len addieren? Was ist der konzeptionelle Unterschied zwischen diff() und
difftime()? Wie bestimmen wir Zeitdifferenzen in Sekunden, Minuten, Stun-
den, Tagen oder Wochen? (27.5)
• Wie bestimmen wir korrekt das aktuelle Alter bzw. den nächsten Geburtstag
einer Person? (27.5.3)
• Warum sind Abfragen mittels Wochentags- oder Monatsnamen mit Vorsicht
zu genießen? Wie gestalten wir derlei Abfragen sprachenunabhängig? (27.6)
• Wie funktionieren die Selektionsstrategie und die Berechnungsstrategie zur
Bestimmung bestimmter Datumswerte? (27.6)
Die Konzepte und Beispiele ab (27.5.3) haben es zum Teil in sich. So haben wir gese-
hen, dass die korrekte Bestimmung des aktuellen Alters bzw. des nächsten Geburts-
tags nicht trivial ist. Wir müssen prüfen, ob der Geburtstag noch heuer stattfindet
und Schaltjahre im Auge behalten.
Sollten dir diese Konzepte Kopfzerbrechen bereitet haben, dann lass sie einmal in
Ruhe sacken.
27 Datums- und Uhrzeitobjekte 377
Die Welt des Datums und der Uhrzeit lässt sich prima mit anderen Konzepten
kombinieren. Rückblickend betrachtet helfen uns Stringfunktionen (siehe (22)) und
Regular Expressions (siehe (23)) dabei, Datumswerte aus Texten herauszufiltern.
So, wie wir es zum Beispiel in Übung 7 auf Seite 308 getan haben.
Vorausblickend erleichtern uns Schleifen (siehe (28)) die Beantwortung weiterer span-
nender Fragen. So könnten wir uns zum Beispiel dafür interessieren, wann wir drei
das nächste Mal an einem Samstag Geburtstag haben. Mit unseren bisherigen Mit-
teln können wir das zwar schon herausfinden, der erforderliche Code würde aber
eher schwerfällig wirken. In (28) in Übung 5 auf Seite 398 kommen wir auf diese
Fragestellung zurück :-)
Außerdem haben wir gesehen, dass die Berechnung des Alters mit difftime() leider
nicht direkt möglich ist. Wie wäre es, wenn wir eine Funktion schreiben könnten,
welche diese Aufgabe meistert? Super wäre das? Dann darfst du dich auf (29) freuen,
denn dort lernen wir eigene Funktionen zu schreiben.
27.8.3 Übungen
a) Erstelle aus str1 und str2 einen Vektor der Klasse POSIXct.
b) Welche Wochentage waren das?
c) Wie viele (ganze) Wochen liegen zwischen beiden Tagen?
3. Drei flüchtige Bekannte (Lisa, Herbert und Peter) haben demnächst Geburts-
tag. Alle drei wurden im Jahr 1989 geboren. Folgendes wissen wir:
Bestimme das Geburtsdatum von Lisa, Herbert und Peter und erstelle in R
einen mit den Namen beschrifteten Datumsvektor der Klasse Date.
378 E Tools für Data Science und Statistik
4. Wir betrachten die Umsatzentwicklung (in 100 Euro) einer fiktiven Wiener
Bar zwischen 23.11.2018 und 06.12.2018:
# Die Umsatzdaten
umsatz <- c(15, 19, 16, 14, 36, 48, 16, 13, 19, 19, 15, 54, 57, 11)
a) Erstelle den dazu passenden Datumsvektor als Objekt der Klasse Date
und erstelle ein Dataframe mit dem Datum (als Date-Objekt) und dem
Umsatz.
b) Generiere aus dem Datum einen Faktor, der angibt, ob es sich um einen
Wochenendtag (Freitag, Samstag) oder um einen Wochentag (Sonntag bis
Donnerstag) handelt und hänge diesen an das Dataframe an.
c) Wie viel hat die Bar im Mittel unter der Woche und wie viel am Wochen-
ende verdient?
d) Berechne den durchschnittlichen Umsatz für jeden Wochentag. Achte dar-
auf, dass die Wochentage chronologisch geordnet sind (von Montag bis
Sonntag).
Daniel Obszelka
380 F Eigene Funktionen und Ablaufsteuerung
28 Kontrollstrukturen
• Anweisungsblöcke einführen
• Verzweigungen (if / else) einsetzen lernen
• Schleifen zur wiederholten Ausführung eines Anweisungsblockes kennenlernen
In (2) haben wir in Übung 2 auf Seite 13 die Fibonaccifolge (ak )k≥1 betrachtet, die
wie folgt definiert ist:
(
1 für k ∈ {1, 2}
ak = (28.1)
ak−2 + ak−1 für k ≥ 3
Angenommen, wir hätten keine geschlossene Formel zur Verfügung, mit der wir
das n. Folgenglied auswerten können. Dann brauchen wir ein Mittel, das es uns
ermöglicht, Formel (28.1) solange wiederholt für wachsendes k auszuwerten, bis wir
das n. Folgenglied bestimmt haben.
Die frohe Botschaft: Wir lernen in (28.3) die for-Schleife und while-Schleife kennen,
die uns die Beantwortung der folgenden Fragen ermöglicht:
Schon davor führen wir in (28.2) if- und else-Anweisungen ein. Mit ihrer Hilfe ist
es möglich einen Programmteil nur dann auszuführen, wenn eine Bedingung er-
füllt ist. So könnten wir etwa für den Fall, dass ein Vektor fehlende Werte hat,
eine Warnmeldung ausgeben wollen. In (8.2.2) haben wir die Funktion ifelse()
kennengelernt, mit der wir binäre vektorwertige Fallunterscheidungen durchführen
können. Wodurch sich ifelse() von den in diesem Kapitel eingeführten if- und
else-Anweisungen unterscheidet, erfahren wir in (28.2.3).
Wie Einrückungen die Lesbarkeit unseres Codes verbessern, sehen wir eindrucksvoll
in (28.4.1). In (28.4.2) betrachten wir abschließend ein cooles Beispiel aus der Simu-
lation (Zelluläre Automaten), bei dem wir die neu gelernten Techniken im Rahmen
eines größeren Beispiels einsetzen.
Ganz zu Beginn legen wir in (28.1) mit der Einführung von Anweisungsblöcken
den Grundstein für einen erfolgreichen Umgang mit Kontrollstrukturen, also if- und
else-Anweisungen und Schleifen. Viel Vergnügen!
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_28
28 Kontrollstrukturen 381
28.1 Anweisungsblöcke – { }
Typischerweise werden Anweisungen sofort ausgeführt. Wir können aber auch An-
weisungen zu einem Block zusammenfassen, der als Ganzes ausgeführt wird.
Ein Anweisungsblock wird durch geschwungene Klammern angezeigt:
# Ein Anweisungsblock
{
Anweisung1
Anweisung2
...
}
Wollen wir innerhalb von Anweisungsblöcken Objekte auf die Console drucken,
so versehen wir diese Objekte mit einem print()-Befehl. Ansonsten wird (höchstens)
der letzte Ausdruck des Anweisungsblocks ausgegeben; im linken Code etwa 4:6.
> { > {
+ 1:3 # wird nicht gedruckt + print(1:3)
+ 4:6 # letzter Ausdruck + print(4:6)
+ } + }
[1] 4 5 6 [1] 1 2 3
[1] 4 5 6
Besteht ein Anweisungsblock aus nur einem Befehl, können wir die geschwungenen
Klammern auch weglassen, was wir aber eher nicht empfehlen. Den Grund dafür
erörtern wir im nächsten Abschnitt.
Wollen wir einen Anweisungsblock nur dann ausführen, wenn eine Bedingung
erfüllt ist, so machen wir Gebrauch von einer if-Anweisung:
if (Logischer Wert) {
# Anweisungsblock
}
382 F Eigene Funktionen und Ablaufsteuerung
Der if-Anweisung übergeben wir genau einen logischen Wert (TRUE oder FALSE).
Falls TRUE übergeben wird, so wird der Anweisungsblock ausgeführt, bei FALSE nicht.
Beispiel: Gegeben ist eine Zahl x. Multipliziere x mit −2, wenn sie negativ ist.
> x > x
[1] 2 [1] 1
Im linken Code ist die Bedingung der if-Anweisung (x < 0) erfüllt, da x negativ ist.
Daher wird der sogenannte Rumpf der if-Anweisung (x <- x * (-2)) ausgeführt.
Im rechten Code ist x < 0 nicht erfüllt, daher wird x <- x * (-2) nicht ausgeführt.
Da der Rumpf der if-Anweisung aus nur einem Befehl besteht, können hier die
geschwungenen Klammern entfallen. Allerdings kann das Weglassen der Klammern
zu tückischen Fehlern führen. Nämlich dann, wenn wir später einen zweiten Befehl
in den Rumpf dazuschreiben und die Klammern vergessen.
Wenn die Bedingung nicht erfüllt ist, so möchten wir vielleicht einen anderen An-
weisungsblock ausführen. Dies ist mit der else-Anweisung möglich:
if (Logischer Wert) {
# Anweisungsblock 1
} else {
# Anweisungsblock 2
}
Beispiel: Gegeben ist ein Vektor x. Wenn x NA-Werte enthält, so soll "x enthält
NAs" auf die Console gedruckt werden, andernfalls "x enthält keine NAs".
> if (is.na(x)) {
+ cat("x enthält NAs\n") # Meldung auf Console drucken
+ } else {
+ cat("x enthält keine NAs\n") # Meldung auf Console drucken
+ }
Warnung in if (is.na(x)) {
Bedingung hat Länge > 1 und nur das erste Element wird benutzt
x enthält keine NAs
Frage: Was passiert eigentlich, wenn wir der if-Anweisung mehrere logische Werte
übergeben?
Die Warnung beantwortet uns diese Frage: Es wird nur das erste Element verwendet.
In obigem Code bestimmt also lediglich der erste Eintrag von x, ob entweder der
Rumpf der if- oder der else-Anweisung ausgeführt wird. Da der erste Eintrag 1
(und damit nicht NA) ist, wird der else-Rumpf ausgeführt.
Diese Warnung deutet auf einen Programmierfehler hin! Modifikation:
> if (any(is.na(x))) {
+ cat("x enthält NAs\n") # Meldung auf Console drucken
+ } else {
+ cat("x enthält keine NAs\n") # Meldung auf Console drucken
+ }
x enthält NAs
Jetzt passt es! Mit any() fragen wir, ob x zumindest einen NA-Eintrag enthält. Die
Funktion cat() werden wir in (31) kennenlernen, sie druckt Strings auf die Console.
Der Escape-Befehl \n beginnt eine neue Zeile (vgl. (23.1)).
Eine else-Anweisung benötigt unbedingt eine if-Anweisung! Daher darf die if-
Anweisung noch nicht beendet sein, bevor else aufgerufen wird.
> {
+ if (any(is.na(x))) {
+ cat("x enthält NAs\n")
+ }
+ else {
+ cat("x enthält keine NAs\n")
+ }
+ }
x enthält NAs
Die erste und letzte Klammer deuten den übergeordneten Anweisungsblock an. Der
+-Prompt vor else sagt uns, dass der aktuelle Block noch nicht beendet ist. Daher
findet else die darüber liegende if-Anweisung und alles läuft reibungslos.
Wenn wir die erste und letzte Klammer weglassen, dann ist die if-Anweisung zuende,
bevor else aufgerufen wird, was wir im folgenden Code an dem >-Prompt vor else
erkennen. else weiß nicht, auf welches if es sich beziehen soll.
> if (any(is.na(x))) {
+ cat("x enthält NAs\n")
+ }
x enthält NAs
> else {
Fehler: Unerwartete(s) ’else’ in "else"
384 F Eigene Funktionen und Ablaufsteuerung
Viele fragen sich an dieser Stelle, worin der Unterschied zwischen der if- bzw. else-
Anweisung und der Funktion ifelse() besteht.
Tatsächlich gibt es bedeutende Unterschiede zwischen beiden Konstrukten:
Man entwickelt mit der Zeit ein Gespür dafür, wann if/else und wann ifelse()
sinnvoll ist.
28.3 Schleifen
• for-Schleifen verwenden wir, wenn die Anzahl der Durchläufe bereits zur
Laufzeit bekannt ist. (28.3.1)
• while-Schleifen hingegen kommen zum Einsatz, wenn wir nicht im Vorhinein
wissen, wie oft der Anweisungsblock durchlaufen werden muss. (28.3.4)
Daneben schauen wir uns weitere wichtige Aspekte rund um Schleifen an, wie bei-
spielsweise Endlosschleifen und Ablaufsteuerung innerhalb von Schleifen.
Wir betrachten die Fibonaccizahlen, die zur Wiederholung wie folgt definiert sind:
(
1 für k ∈ {1, 2}
ak =
ak−2 + ak−1 für k ≥ 3
Ziel: Berechne a1 , a2 , . . . , a5 .
28 Kontrollstrukturen 385
Angenommen, wir kennen die geschlossene Formel zur Berechnung der Fibonacci-
zahlen nicht, dann könnten wir zunächst auf folgende Idee kommen:
Bei dem Code stellt es einem regelrecht die Haare auf! In diesem Fall wurde der Code
fibo[3] <- fibo[1] + fibo[2] in die nächste Zeile mit Copy & Paste kopiert und
dort wurden die Zahlen ausgetauscht. Der Code wird durch ein solches Vorgehen
aufgebläht und die Wahrscheinlichkeit, dass sich Fehler einschleichen, ist sehr groß!
Aber: Wir können aus diesem schwachen Ansatz eine bahnbrechende Idee entwi-
ckeln! Schauen wir uns eine modifizierte Variante an:
> i <- 3
> fibo[i] <- fibo[i - 2] + fibo[i - 1]
> i <- 4
> fibo[i] <- fibo[i - 2] + fibo[i - 1]
> i <- 5
> fibo[i] <- fibo[i - 2] + fibo[i - 1]
Zwar ist dieser Code nicht kürzer, aber er hat einen entscheidenden Vorteil: Der
Code fibo[i] <- fibo[i - 2] + fibo[i - 1] ist bei jeder Berechnung immer
derselbe, wir brauchen lediglich die Variable i zu aktualisieren.
Frage: Wie wäre es, wenn uns R diese Aktualisierungsarbeit abnehmen würde?
Super? Dann schauen wir uns zunächst einmal die Syntax für for-Schleifen an:
for (i in vektor ) {
# Anweisungsblock
}
Dabei werden die Einträge von vektor der Reihe nach der Schleifenvariable i zu-
gewiesen und der Anweisungsblock durchlaufen. Erreicht R das Ende der Schleife,
so springt das Programm wieder zum Schleifenbeginn und i wird aktualisiert, also
das nächste Element von vektor wird i zugewiesen. Das passiert solange, bis alle
Elemente von vektor durchlaufen wurden.
386 F Eigene Funktionen und Ablaufsteuerung
Sehen wir, wie uns for-Schleifen bei der Berechnung von Fibonaccizahlen helfen:
Sieht doch gleich besser aus! Die Aktualisierung der Schleifenvariable i übernimmt
jetzt die for-Schleife, indem sie der Variablen i die Zahlen von 3:length(fibo)
der Reihe nach zuweist. Der Code fibo[i] <- fibo[i - 2] + fibo[i - 1] wird
in jeder Iteration (in jedem Schleifendurchlauf) mit dem aktualisierten i ausgeführt.
In beiden Varianten gehen wir den Vektor x elementweise durch, berechnen das
Quadrat jedes Elementes und drucken das Ergebnis auf die Console.
Im linken Code werden dem (einzeiligen) Anweisungsblock die Indizes der Reihe nach
übergeben. Statt 1:length(x) schreiben viele auch seq(along = x). Wir können
nun auf das i. Element von x zugreifen. Im rechten Code übergeben wir dem Anwei-
sungsblock direkt die Elemente von x.
Bemerkung: Wollen wir innerhalb einer Schleife ein Objekt auf die Console dru-
cken, so müssen wir uns das von R explizit wünschen, etwa mit print() oder cat()
(siehe Seite 383). Selbiges gilt für if- und else-Anweisungen.
28 Kontrollstrukturen 387
Wir wollen nun die internen Abläufe der obigen beiden Varianten nachbauen, um
unser Verständnis für die Abläufe zu festigen.
> x
[1] 5 -2 3
Wollen wir einen neuen Vektor in einer Schleife befüllen, so müssen wir diesen vor
der Schleife initialisieren.
Beispiel: Wie das vorige Beispiel, nur speichere die Ergebnisse im Vektor erg ab.
In der linken Variante teilen wir R mit erg <- NULL mit, dass es das Objekt erg
gibt (vgl. Dummy-Initialisierung in (14.2)). In der rechten spezifizieren wir darüber
hinaus die Datenstruktur (Vektor), die Länge und den Mode (numeric).
Die rechte Variante ist effizienter, wie wir in (33) beim Thema Effizienz erläutern
werden! Ohne Initialisierung würden wir eine Fehlermeldung erhalten, da erg nicht
gefunden werden kann.
388 F Eigene Funktionen und Ablaufsteuerung
Mit Schleifen sollten wir sparsam umgehen, da sie in R langsam sind. Wir machen
unseren Code effizienter, wenn wir vektorwertige Funktionen einsetzen.
> x
[1] 5 -2 3
Hier wird die 2 recycelt und der Potenzoperator wird vektorwertig angewendet.
Bemerkung: Wir besprechen in (33) beim Thema Effizienz ausführlich, wie wir
unseren Code mit Hilfe von coolen Tricks drastisch beschleunigen können.
Manchmal lassen sich jedoch for-Schleifen nicht bzw. kaum vermeiden. Betrachten
wir nochmal die Berechnung der Fibonaccizahlen aus (28.3.1).
Um etwa die 5. Fibonaccizahl berechnen zu können, müssen wir die 3. und 4. Fibo-
naccizahl bereits kennen. Daher müssen wir die Fibonaccizahlen sequenziell von
vorne nach hinten berechnen. Eine vektorwertige Funktion verlangt aber, dass die
Ergebnisse parallel berechnet werden können, also die einzelnen Teilergebnisse un-
abhängig voneinander bestimmbar sind, was bei der Berechnung der Fibonac-
cizahlen eine vektorwertige Lösung unmöglich macht – zumindest solange, bis wir
eine geschlossene Formel finden, so wie wir sie in Übungsaufgabe 2 auf Seite 13
aufgeschrieben haben:
√ k √ k !
1 1+ 5 1− 5
ak = √ · −
5 2 2
Die for-Schleife wird solange ausgeführt, bis alle Elemente durchgegangen wurden.
Manchmal wissen wir allerdings nicht, wie oft eine Schleife durchlaufen werden soll.
In diesem Fall bietet sich die while-Schleife an:
while (bedingung ) {
# Anweisungsblock
}
> i <- 1
Wir starten mit i <- 1. Es wird solange i gedruckt und anschließend um den Wert
1 erhöht, bis die Bedingung i < 4 (erstmalig) verletzt ist. Wir bauen die internen
Abläufe des Beispiels nach.
> i <- 1
Beispiel: Wir setzen das Fibonaccizahlenbeispiel von (28.3.1) fort. Ab dem wieviel-
ten Folgenglied übersteigt die Fibonaccifolge ak erstmals den Wert 100?
Wir starten mit den ersten beiden Fibonaccizahlen, die per Definition 1 sind. Der
Iterationszähler iter gibt zu Schleifenbeginn an, wie viele Folgenglieder wir bis jetzt
berechnet haben und fibo[iter] ist die zuletzt bestimmte Fibonaccizahl. Solange
die Bedingung fibo[iter] <= 100 erfüllt ist, müssen wir mindestens eine weitere
Fibonaccizahl berechnen; der Schleifenrumpf wird ausgeführt.
Ist hingegen fibo[iter] <= 100 (erstmalig) nicht erfüllt, so haben wir unser Ziel
erreicht und die Schleife wird abgebrochen. Das 12. Folgenglied ist erstmals größer
als 100.
Bemerkung: Der obige Code hat eine effizienztechnische Schwäche: Der Vektor
fibo muss in jeder Iteration um ein Element erweitert werden. Das Betriebssystem
muss ständig neuen Speicherplatz für das immer größer werdende Objekt fibo fin-
den, was aufwändig ist. In der Praxis versucht man daher häufig, eine (möglichst en-
ge) obere Schranke für die Anzahl der benötigten Schleifendurchläufe zu bestimmen
und den Vektor fibo bereits zu Beginn groß genug zu initialisieren. Wir schneiden
auch diesen Aspekt in (33) beim Thema Effizienz an.
Wird innerhalb einer Schleife der Befehl break ausgeführt, so wird die Schleife um-
gehend beendet. Beim Befehl next wird der aktuelle Schleifendurchlauf beendet und
mit der nächsten Iteration fortgesetzt. Dadurch können wir zusätzliche Bedingungen
in die Schleife packen.
28 Kontrollstrukturen 391
Wenn i <= 4 erfüllt ist, wird der Schleifendurchlauf wegen next abgebrochen und
mit der nächsten Iteration fortgefahren. Sofern i > 4 ist, wird die erste if-Anweisung
nicht durchgeführt. Das Objekt i wird dann mit print(i) gedruckt. Falls i >= 8,
so wird die Schleife wegen break umgehend abgebrochen. Pech für die Elemente 9
und 10 aus dem Vektor 1:10: sie werden nicht mehr berücksichtigt.
Da in dieser Form der while-Schleife die Bedingung TRUE logischerweise immer er-
füllt ist, wird die Schleife nie regulär abgebrochen. Endlosschleifen sollten (daher)
Abbruchsbedingungen innerhalb des Anweisungsblocks enthalten.
Bemerkung: Schleifen können in R manuell mit der Escape-Taste abgebrochen
werden, sofern sie nicht Teil einer anderen Schleife oder Funktion sind.
Beispiel: Wir betrachten das Fibonaccizahlenbeispiel von Seite 390 via Endlos-
schleife. Das wievielte Folgenglied übersteigt erstmals den Wert 100?
> fibo
[1] 1 1 2 3 5 8 13 21 34 55 89 144
> length(fibo)
[1] 12
Dasselbe Ergebnis! Hier haben wir uns aus der Fragestellung eine passende Ab-
bruchsbedingung fibo[n] > 100 gebastelt. Sobald diese Bedingung erfüllt ist, wird
wegen break die Schleife abgebrochen.
> x <- 0
> while (x < 5){if (x <= 1){temp<-1
+ x <- x + 1}else{if(x %in% c(2, 3)){x <- x + 2}else{x <- x - 1}}}
> x <- 0
> while (x < 5) {
+ if (x <= 1) {
+ temp <- 1
+ x <- x + temp
+ }
+ else {
+ if (x %in% c(2, 3)) {
+ x <- x + 2
+ }
+ else {
+ x <- x - 1
+ }
+ }
+ }
28 Kontrollstrukturen 393
Wenn wir mit Verzweigungen und Schleifen arbeiten, so machen wir unseren Code
lesbarer, wenn wir die Befehle der einzelnen Anweisungsblöcke einrücken. Folgende
Regeln hat die übersichtlichere Version beachtet:
• Nach den Wörtern if, else, for, while und repeat fügen wir ein Leerzeichen
ein.
• Befehle, die zum selben Anweisungsblock gehören, stehen direkt untereinander.
Die Befehle temp <- 1 und x <- x + temp gehören zum selben Anweisungs-
block und stehen daher folgerichtig untereinander.
• Ein neuer Anweisungsblock ist um eine konstante Zahl an Leerzeichen (min-
destens 2) oder um einen Tabulator eingerückt.
• Eine öffnende geschwungene Klammern darf entweder am Ende der Zeile oder
zu Beginn der nächsten Zeile (direkt unterhalb der Anweisung, zu der sie ge-
hört) stehen. Wir entscheiden uns für genau eine dieser beiden Varianten und
mischen sie nicht.
• Eine schließende Klammer steht in einer eigenen Zeile und zwar genau unter-
halb der Anweisung, zu der sie gehört. Die grün markierte schließende Klammer
steht beispielsweise genau unter der else-Anweisung, zu der sie gehört.
Ein zellulärer Automat ist ein relativ einfaches Simulationsmodell, das bei vielen
diskreten dynamischen Systemen (zum Beispiel Populationsentwicklung, Räuber-
Beute-Simulation) eingesetzt wird. Wir betrachten einen einfachen eindimensionalen
zellulären Automaten mit n Zellen. Die Zellen sind perlenartig angeordnet, also
X1 − X2 − X3 − . . . − Xn . Jede Zelle kann genau einen von drei Zuständen
annehmen: negativ (-1), neutral (0) oder positiv (+1).
Außerdem verändern Zellen über die Zeit ihre Zustände – dies erfolgt nach einem
gegebenen Regelsatz. Sei X(t) der Vektor, der die Zustände aller Zellen zum Zeit-
punkt t speichert, jedes X(t) besteht also aus n Elementen. Die Anfangszustände
X(1) sind gegeben bzw. werden sie zufällig bestimmt.
Das Update der Zelle Xi (t) erfolge nach folgender Regel: Zunächst wird für jede
Zelle der Zustand des linken und rechten Nachbarn sowie der Zelle selbst addiert
(für die erste und letzte Zelle gibt es nur einen Nachbarn):
X1 (t) + X2 (t)
falls i = 1
Zi (t + 1) = Xn−1 (t) + Xn (t) falls i = n
Xi−1 (t) + Xi (t) + Xi+1 (t) sonst
394 F Eigene Funktionen und Ablaufsteuerung
+1 falls Zi (t + 1) > 0
Xi (t + 1) = 0 falls Zi (t + 1) = 0
−1 falls Zi (t + 1) < 0
Aufgabe: Implementiere den Automaten für beliebiges n. Sobald der Automat kon-
vergiert, also sich alle Zustände in zwei aufeinanderfolgenden Zeitschritten nicht
mehr ändern, soll abgebrochen werden, spätestens jedoch nach tmax Iterationen.
Lösung: In Abb. 28.1 ist ein R-Code abgebildet, der die Aufgabe löst. Betrachten
wir eine Beispielausgabe des Automaten für n = 5 und tmax = 10.
> X
X_1 X_2 X_3 X_4 X_5
t = 1 -1 1 -1 0 1
t = 2 0 -1 0 0 1
t = 3 -1 -1 -1 1 1
t = 4 -1 -1 -1 1 1
Von t = 3 auf t = 4 ändert sich kein Zustand mehr, weshalb der Code mit dem
stabilen Zustand X(4) = (-1, -1, -1, +1, +1) abbricht.
Es folgen jetzt noch nähere Erklärungen zum abgebildeten Code.
Der Automat wird durch die Matrix X repräsentiert. Jede Zeile steht für einen Zeit-
punkt. In 1.) (schwarzer Teil) werden n und tmax eingestellt und der erste Zustand
des Automaten (die erste Zeile von X) zufällig mit sample() ausgewürfelt.
In 2.) (roter Teil) definieren wir die äußere Schleife: Eine for-Schleife, die von 2 bis
tmax läuft, da ja nach höchstens tmax Iterationen Schluss sein soll.
In 3.) (lila Teil) werden die Zustände addiert (also die Z(t)-Werte berechnet): Wir
generieren einen Vektor z, der dann in der inneren Schleife befüllt wird. Hier unter-
scheiden wir Fälle, da die linke und rechte Zelle ja nur zwei Nachbarn haben.
Dann kommt in 4.) die Zustandsanpassung (grüner Teil): Wir prüfen mit ifelse(),
ob z > 0 ist. Falls ja, soll an den entsprechenden Stellen +1 stehen, ansonsten prüfen
wir nochmals mit ifelse(), ob z < 0 ist. Falls ja, so schreiben wir -1, sonst 0. Der
aktuelle Zustand wird an die Matrix angehängt.
In 5.) (blauer Teil) prüfen wir schließlich, ob der Automat bereits einen stabilen
Zustand erreicht hat und brechen die äußere Schleife gegebenenfalls mit break ab.
Bemerkung: Dieses Beispiel soll vor allem den Unterschied zwischen if/else und
ifelse() verdeutlichen und die Anwendung der Schleife aufzeigen. Es ist (bei wei-
tem) nicht die effizienteste Lösung! In (33) bekommst du in Übung 2 auf Seite 501
die Chance, den Code zu beschleunigen.
28 Kontrollstrukturen 395
28.5 Abschluss
• Was ist ein Anweisungsblock und wie definieren wir ihn? Was müssen wir
tun, damit innerhalb eines Anweisungsblocks Objekte sicher auf die Console
gedruckt werden? (28.1)
• Was ist eine if-Anweisung und wie definieren wir sie? Was passiert, wenn wir
der if-Anweisung mehr als einen Wahrheitswert übergeben? (28.2.1)
• Was ist eine Wenn-Dann-Verzweigung und wie setzen wir sie in R um? Wie ge-
währleisten wir, dass die else-Anweisung die zugehörige if-Anweisung findet?
(28.2.2)
• Welche Unterschiede gibt es zwischen if/else-Anweisungen und der Funktion
ifelse()? (28.2.3)
• Welche beiden Arten von Schleifen haben wir gelernt? In welchen Situationen
setzen wir die beiden Schleifen jeweils ein? (28.3)
• Wie definieren wir eine for-Schleife und wie funktioniert sie? Wie sehen in-
dexbasierte bzw. inhaltsbasierte Schleifen aus? (28.3.1), (28.3.2)
• Sind vektorwertige Funktionen oder Schleifen in der Regel schneller? Was ist
der Unterschied zwischen sequenzieller und paralleler Berechnung? Warum ist
es in der Regel nicht möglich, eine sequenzielle Berechnung mit vektorwertigen
Funktionen durchzuführen? (28.3.3)
• Wie definieren wir eine while-Schleife und wie funktioniert sie? (28.3.4)
• Wie können wir die aktuelle Schleife bzw. den aktuellen Schleifendurchlauf
beenden? (28.3.5)
• Was sind Endlosschleifen und wie definieren wir sie? (28.3.6)
28.5.2 Ausblick
Wir haben Schleifen bewusst relativ spät eingeführt, um dir mehr Zeit zu geben,
dich mit vektorwertigen Funktionen anzufreunden. Du wirst in (33) staunen, um
wie viel schneller dein Code wird, wenn du die Anzahl der Schleifendurchläufe klein
hältst und verstärkt (spezialisierte) vektorwertige Funktionen einsetzt!
In (30) klären wir unter anderem, warum wir innerhalb von ifelse() oder auch etwa
sapply() keine Objekte überschreiben können bzw. sollen. Und in (31) erkunden
wir die Welt der Stringformatierung und Consolenausgabe.
28 Kontrollstrukturen 397
28.5.3 Übungen
1. In einem IQ-Test finden wir unter anderem folgende Aufgabe: Setze die folgen-
de Reihe fort: 3 2 5 4 7 6 9 ...
a) Welche Gesetzmäßigkeit steckt in dieser Reihe?
> x
[1] -3 -1 1 4 -3 4
Was wird in den folgenden Unteraufgaben jeweils für res mit dem Vektor x
von oben auf der Console ausgegeben? Finde jeweils einen alternativen Code,
der für beliebige numerische Vektoren x das gleiche Resultat wie res liefert
und ohne Schleifen auskommt.
a) res <- 0
for (i in 2:length(x)) {
res <- res + (x[i] > x[i - 1])
}
res
for (i in 1:length(x)) {
if (x[i] > 0) {
next()
}
if (x[i] > res) {
res <- x[i]
}
}
res
for (i in 2:length(x)) {
res[i] <- res[i - 1] + x[i]
}
res
398 F Eigene Funktionen und Ablaufsteuerung
3. Was kommt bei den folgenden Codes jeweils für res heraus (und warum)?
Schreibe einen Code, der ausrechnet, wann jeder von uns nächstes Mal an
einem Samstag Geburtstag hat.
6. Rekapituliere Übungsbeispiel 5 auf Seite 138. Baue in deine modifizierte Lö-
sung eine if/else-Anweisung ein.
29 Eigene Funktionen: Grundlagen 399
Funktionen machen vor allem dann Sinn, wenn wir Anweisungsblöcke immer wie-
der mit wechselnden Objekten verwenden möchten. Sie tragen dazu bei, dass ein
Programm schneller geschrieben werden kann, übersichtlicher, lesbarer und weniger
fehleranfällig ist.
Ein eindrucksvolles Beispiel dafür haben wir bereits in der Einleitung von (4) gese-
hen. Wenn wir die Wurzel mehrerer Zahlen ziehen möchten, dann schätzen wir es
sehr, dass es die Funktion sqrt() gibt.
Wir lernen in diesem Kapitel, wie wir eigene Funktionen schreiben. Anhand von
mehreren Beispielfunktionen lernen wir die unterschiedlichen Konzepte kennen, die
für uns relevant sind. Dabei entwickeln, entdecken und lernen wir unter anderem
Folgendes:
• Eine Funktion, die uns die Lösungen der quadratischen Gleichung zurückgibt.
Dabei lernen wir, wie wir eine Funktion definieren und welche Möglichkeiten
wir haben, ein Objekt zurückzugeben. (29.1)
• Eine Funktion, die überprüft, ob eine Matrix symmetrisch ist. Dabei befassen
wir uns mit Fehlermeldungen und Warnungen für den Fall, dass keine Matrix
übergeben wird und lernen eine Erweiterung von logischen Verknüpfungen ken-
nen. (29.1.3), (29.2), (29.3)
Pn p 1/p
• Eine Funktion, welche kxkp := ( i=1 |xi | ) , also die p-Norm kxkp eines Vek-
tors x berechnet. Dabei lernen wir, wie wir (statische) Defaultwerte definieren
und befassen uns mit der Frage, wie wir Funktionen in der Praxis sinnvoll
organisieren. (29.4.1)
• Eine Funktion, mit der wir Würfelwürfe generieren können. Und zwar mit
einem kürzeren Code als mit sample(). (29.4.2)
• Eine Funktion, die mehrere Arten von Mittelwerten berechnen kann und den
vom Benutzer gewünschten Mittelwert zurückgibt. (29.4.4)
Darüber hinaus lernen wir in (29.4.5) mehr über das Dreipunkteargument und wie
wir es weiterverarbeiten können. In (29.6) machen wir uns mit Environments und
Scoping bekannt.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_29
400 F Eigene Funktionen und Ablaufsteuerung
Mit Hilfe des Schlüsselwortes function können wir eine eigene Funktion definie-
ren. Das sieht schematisch so aus:
29.1.1 Funktionsname
Zunächst überlegen wir uns einen Funktionsnamen. Dieser sollte bzw. muss folgende
Anforderungen erfüllen:
29.1.2 Parameter
Im Prinzip können wir beliebig viele Parameter verwenden, die alle mit einem Bei-
strich getrennt werden. Die Parameternamen sollten ebenfalls sprechend sein. An-
hand von Beispielen werden wir uns bald die Feinheiten ansehen.
Eine Funktion kann immer nur ein Objekt retournieren. Möchten wir mehrere Ob-
jekte zurückgeben, so können wir diese in eine Liste packen. Die Rückgabe des
Ergebnisobjektes können wir auf zwei primäre Arten vornehmen.
• mittels der Funktion return() – dies ist die Empfehlung für den Beginn.
• letzter Ausdruck der Funktion.
Erreicht die Funktion ein return(), wird die Funktion sofort abgebrochen und ein
Ergebnis zurückgeliefert. Im Zweifel bevorzugen wir die return()-Variante, da sie
sicherer ist. Mittels der Funktion invisible() kann das Ergebnisobjekt in beiden
Varianten auch unsichtbar zurückgegeben werden.
29 Eigene Funktionen: Grundlagen 401
Wir wollen die Rückgabevarianten gegenüberstellen. Dazu schreiben wir unsere erste
Funktion, die einen Parameter x hat, der unverändert zurückgegeben wird.
Wir nennen unsere Funktion quadratgleichung, ein sprechender Name. Der Funkti-
on übergeben wir die drei Parameter a, b und c, die jeweils durch Kommata getrennt
sind und die wir im Rumpf der Funktion verwenden können. Jetzt wird uns auch
klar, warum wir bisher wichtige Parameter in einem Block ganz zu Beginn des Codes
zusammengefasst haben, das war schon eine Vorleistung für dieses Kapitel.
Spannend ist der Ausdruck c(-1, 1) *. Mit ihm können wir das ± in der Lösungs-
formel in einer Zeile ausdrücken.
Statt return(res) hätten wir auch einfach res schreiben können. Hätten wir statt
return(res) den Befehl invisible(res) verwendet, so wäre das Ergebnis nicht
auf die Console gedruckt worden.
402 F Eigene Funktionen und Ablaufsteuerung
Der Hauptvorteil der Funktionsvariante gegenüber jener aus (2.2.1): Wenn wir für
zwei Parameterkonstellationen die Lösungen berechnen wollen, dann brauchen wir
lediglich zwei Mal die Funktion quadratgleichung() aufrufen.
Beispiel: Schreibe eine Funktion, die überprüft, ob eine Matrix symmetrisch ist.
Falls jeder Eintrag von X mit dem entsprechenden Eintrag der transponierten Matrix
übereinstimmt (all(X == t(X)) ist TRUE), dann setze ergebnis auf TRUE, andern-
falls auf FALSE. Wir könnten die Funktion auch kürzer aufschreiben:
Sofern all(X == t(X)) erfüllt ist, wird TRUE zurückgegeben und return(FALSE)
wird nicht mehr ausgeführt. Da der Rumpf der if-Anweisung nur aus einem Befehl
besteht, können die geschwungenen Klammern entfallen.
Wenden wir unsere Funktion einmal mit zwei Beispielmatrizen an.
> M1 <- cbind(c(4, 0), c(0, 3)) > M2 <- cbind(c(4, 0), c(1, 3))
> M1 > M2
[,1] [,2] [,1] [,2]
[1,] 4 0 [1,] 4 1
[2,] 0 3 [2,] 0 3
> is.symmetric(M1) > is.symmetric(M2)
[1] TRUE [1] FALSE
Passt! Wenn wir is.symmetric() eine quadratische Matrix übergeben, haben wir
keine Probleme. Was passiert aber, wenn wir keine quadratische Matrix übergeben?
Ups! Eigentlich sollte es hier kein Ergebnis geben, da wir einen Vektor übergeben
haben. Dies sollten wir abfangen.
29 Eigene Funktionen: Grundlagen 403
Mit den Funktionen stop() und warning() können wir Fehlermeldungen und
Warnungen erzeugen.
• stop() bricht die Funktion ergebnislos ab und druckt den übergebenen Text
als Fehlermeldung aus.
• warning() druckt den übergebenen Text als Warnmeldung aus, die Funktion
läuft allerdings normal weiter.
Andernfalls können wir die Funktionen nrow() und ncol() auf X anwenden, da
das Objekt eine Matrix ist und überprüfen, ob sie nicht quadratisch ist. Ist die
Matrix nicht quadratisch, so brechen wir die Funktion wiederum mit einer passenden
Fehlermeldung ab.
„Überlebt“ der Programmstrang (der sogenannte Thread) beide Abfragen, so können
wir uns schließlich der Überprüfung der Symmetrie zuwenden.
Manche hätten das letzte Beispiel von Seite 403 im ersten Reflex so gelöst:
Sieht auf den ersten Blick gut aus. Auf den zweiten Blick folgt die Ernüchterung:
Die Funktion wird zwar korrekterweise abgebrochen, aber nicht so, wie wir das wollen
(nämlich mit unserem informativeren Fehlertext). Grund des Scheiterns: Die Aus-
drücke is.matrix(X) und nrow(X) == ncol(X) werden beide ausgewertet, bevor
sie mit der Und-Verknüpfung kombiniert werden. Übergeben wir für X einen Vektor,
so existieren aber nrow(X) und ncol(X) nicht.
Wir erhalten also einen leeren logischen Vektor, mit dem die if-Anweisung nichts
anzufangen weiß.
29 Eigene Funktionen: Grundlagen 405
Zeit für eine Reparaturmaßnahme! Zu diesem Zweck schauen wir uns die beiden
Operatoren "&&" und "||" an:
Der Operator "&&" vergleicht genau einen Wahrheitswert der linken Seite mit ge-
nau einem Wahrheitswert der rechten Seite. Im Unterschied zu & wird aber der
rechte Ausdruck nicht mehr herangezogen, wenn der linke bereits FALSE ergibt. Da-
her ist es völlig belanglos, dass nrow(X) == ncol(X) einen leeren Vektor zurückgibt.
Gleiches Prinzip gilt auch für die Oder-Verknüpfung "||": Das Ergebnis des rechten
Ausdrucks wird nicht mehr herangezogen, wenn der linke Ausdruck TRUE ergibt.
Bewaffnet mit dieser neuen Erkenntnis schreiten wir zur Reparatur!
Spannend, dass ein Zeichen so einen großen Unterschied machen kann ;-)
Parameter = Defaultwert
Beispiel: Schreibe eine Funktion, welche die p-Norm eines Vektors berechnet. Ver-
wende defaultmäßig die euklidische Norm (p = 2). Überprüfe, ob es sich beim über-
gebenen Objekt tatsächlich um einen numerischen Vektor handelt.
406 F Eigene Funktionen und Ablaufsteuerung
Die Berechnung der Norm erfolgt ausschließlich in der Funktion norm(). Die Funktio-
nen norm1() und norm2() rufen lediglich die Funktion norm() mit dem passenden
Parameter p auf, werden also auf norm() zurückgeführt. Wenn uns ein Fehler
auffällt, dann brauchen wir ihn nur in norm() zu korrigieren.
Wollen wir die Betragsnorm für einen Vektor x berechnen, so können wir jetzt auch
kürzer norm1(x) statt norm(x, p = 1) schreiben.
29 Eigene Funktionen: Grundlagen 407
Nicht immer werden alle Parameter gebraucht; in diesem Fall müssen Parameter
nicht spezifiziert werden.
Beispiel: Wir wollen eine Funktion schreiben, welche den gewichteten Mittelwert
für einen Vektor x berechnet. Werden keine Gewichte (weights) übergeben, berechne
den gewöhnlichen Mittelwert.
Zwei Möglichkeiten der Umsetzung sind denkbar:
1. weights wird defaultmäßig auf NULL gesetzt. Wir überprüfen in der Funktion
mit is.null(), ob Gewichte übergeben wurden.
2. mittels der Funktion missing(): Gibt TRUE zurück, wenn ein Parameter nicht
spezifiziert wurde.
Die ersten vier Zeilen könnten auch so aussehen (der Rest bleibt gleich):
switch(EXPR, ...)
Wir übergeben der Funktion einen Ausdruck EXPR. Das kann entweder eine Zahl oder
ein String sein. Für das Dreipunkteargument ... können wir beliebig viele Ausdrücke
der Form links = rechts übergeben. Nun sucht die Funktion nach Übereinstim-
mungen auf der linken Seite und gibt die entsprechende rechte Seite zurück.
Beispiel: Schreibe eine Funktion, welche auf Wunsch entweder das arithmetische,
geometrische, harmonische oder quadratische Mittel für einen Vektor berechnet.
Standardmäßig wird das arithmetische Mittel berechnet.
Mit dem Parameter type steuern wir, ob wir das arithmetische (arith), geome-
trische (geom), harmonische (harm) oder quadratische (quadr) Mittel berechnen.
Die Funktion switch() berechnet den dazu passenden Wert und speichert ihn auf
ergebnis. Übergeben wir für type keinen gültigen Wert, so wird in weiterer Folge
eine Warnmeldung ausgegeben und das arithmetische Mittel zurückgegeben.
Bemerkung: Wenn wir die erste Hälfte einer Strecke mit 60 km/h fahren und die
zweite Hälfte mit 120 km/h, so beträgt unsere Durchschnittsgeschwindigkeit 80 km/h
(nicht 90 km/h).
Mit dem flexiblen Dreipunkteargument (vgl. (4.3.3)) können wir viele Funktionen
bequemer implementieren und deren Anwendung stark vereinfachen. Wir sehen uns
ein paar Beispiele an.
Beispiel: Mit der Funktion paste() können wir mehrere Vektoren zu einer Zeichen-
kette verknüpfen. Vor der Anwendung steht die genaue Anzahl der Vektoren nicht
fest. Das ist mit dem Dreipunkteargument kein Problem; wir können ihm beliebig
viele Vektoren übergeben:
> a <- 2
> b <- 5
Beachte: Alle Parameter, die nach dem Dreipunkteargument stehen, müssen exakt
benannt werden (vgl. Regel 5 auf Seite 44)! So muss im Beispiel der Parameter
collapse vollständig bezeichnet werden, da collapse nach ... steht, sonst kommt
ein Murks heraus:
Beispiel: Die Funktion apply() ermöglicht uns die Anwendung einer Funktion auf
Zeilen oder Spalten einer Matrix (vgl. (19)). Jede Funktion weist eine andere Para-
metrisierung auf. Vor der Anwendung stehen die Funktion und damit die benötigten
Parameter noch nicht fest.
Ein Fall für das Dreipunkteargument. Wir können für ... in
nun jene Parameter einsetzen, die in FUN benötigt werden (vgl. (19.1.4)). Also etwa
probs = c(0.2, 0.8) für quantile() oder decreasing = TRUE für sort() in den
folgenden Beispielen.
Wenn wir eine Funktion frisch geschrieben haben, dann wissen wir genau, wie wir
sie anwenden können. Allerdings wissen das andere Benutzer nicht. Und auch bei
uns könnte die Erinnerung nach einigen Monaten verblassen. Um uns gegen den
Erinnerungsverlust abzusichern, empfiehlt es sich, die eigenen Funktionen zu kom-
mentieren. Idealerweise schreiben wir auch einige Anwendungsbeispiele auf.
29 Eigene Funktionen: Grundlagen 413
Während rwuerfel() intuitiv ist, so könnten wir etwa nach einem Jahr die Funk-
tion mittel() von Seite 409 mit type = "harmonisch" aufrufen und würden das
arithmetische Mittel zurückbekommen. Hier sind Kommentare sehr wertvoll!
Beispiel: Kommentiere die Funktion mittel() von Seite 409.
Eine (primitive) Möglichkeit, eine Nullstelle einer stetigen und monotonen Funktion
f zu bestimmen, ist der Bisektionsalgorithmus (Intervallhalbierungsalgorithmus):
Gegeben ist f und ein Intervall [a, b], wobei sign f (a) 6= sign f (b). Diese Bedingung
garantiert, dass in [a, b] sicher eine Nullstelle existiert.
Wir führen die Schritte 1 und 2 solange aus bis |f (m)| < ϵ, wobei ϵ > 0 die Rechen-
genauigkeit steuert, wir also nahe genug an einer Nullstelle liegen.
Wir veranschaulichen die Idee: Angenommen, jemand denkt sich eine Zahl zwischen
1 und 15 aus und wir müssen sie erraten. Egal, welche Zahl er sich ausdenkt, wir
erraten sie nach höchstens 4 Versuchen. Sei im Beispiel 11 die gesuchte Zahl.
Ziel: Schreibe eine Funktion, welche die Nullstelle einer stetigen und monotonen
Funktion f mit dem Bisektionsalgorithmus berechnet.
In Abb. 29.1 bilden wir den R-Code ab. Da wir nicht wissen, wie viele Iteratio-
nen nötig sind, um die gewünschte Genauigkeit zu erreichen, bietet sich hier die
while-Schleife an. Wir wollen die (angenäherte) Nullstelle und den entsprechenden
Funktionswert geliefert bekommen. Da wir aber immer nur ein Objekt retournieren
können, packen wir beide Objekte im Zuge der Rückgabe in eine Liste.
Bei der Gelegenheit sehen wir auch, wie einfach wir Funktionen als Argument über-
geben können. In unserem Beispiel übergeben wir dem Parameter f die Funktion,
für welche die Nullstelle bestimmt werden soll. Innerhalb von bisektion() können
wir f() wie eine Funktion verwenden.
√
Als Anwendungsbeispiel wollen wir mit bisektion() den Ausdruck 2 berechnen.
√
2 = x ⇔ x2 − 2 = 0
Gesucht ist also die Nullstelle der Funktion f (x) = x2 − 2. Klarerweise ist die Null-
stelle größer als 0 und kleiner als 2, womit wir das Startintervall haben.
29 Eigene Funktionen: Grundlagen 415
Environments sind Umgebungen, die Objekte enthalten. Jedes Mal, wenn eine Funk-
tion aufgerufen wird, entsteht eine neue (untergeordnete) Umgebung mit eigenen von
der Außenwelt unabhängigen Objekten. Alle in dieser untergeordneten Umgebung
definierten und manipulierten Objekte sind in der Regel nach außen hin nicht sicht-
bar und werden bei Beendigung der Funktion wieder gelöscht. Wird innerhalb einer
Umgebung auf Variablen verwiesen, die nicht in dieser Umgebung existieren, so wird
nach gleichnamigen Variablen außerhalb der aktuellen Umgebung gesucht (Scoping).
Anhand eines Beispiels studieren wir Environments und Scoping in der Praxis.
Innerhalb von hugo() wird zeilen um 1 erhöht. Da die Variable zeilen innerhalb
der hugo()-Umgebung nicht existiert, sucht R davor außerhalb der Funktion nach
dem Objekt zeilen und wird bei zeilen <- 2 fündig. zeilen wird kopiert und
somit innerhalb von hugo() eine gleichnamige Variable zeilen erzeugt, die ab sofort
von dem äußeren Objekt zeilen völlig unabhängig ist.
> zeilen # Nicht auf 3 erhöht > spalten # Nicht auf 6 erhöht
[1] 2 [1] 5
> M # M nach außen nicht sichtbar: Existiert nur innerhalb von hugo()!
Fehler: Objekt ’M’ nicht gefunden
29 Eigene Funktionen: Grundlagen 417
Wenn wir einen Parameter einer Funktion nicht spezifizieren, so sucht R nicht nach
gleichnamigen Objekten außerhalb der Funktion!
• Objekte, die innerhalb einer Funktion erstellt werden, sind außerhalb der Funk-
tion nicht sichtbar und werden nach Beendigung der Funktion gelöscht.
• Existiert ein Objekt innerhalb einer Funktion nicht, so sucht R außerhalb der
Funktion nach gleichnamigen Objekten. Wird R fündig, so erzeugt R unab-
hängige (tiefe) Kopien. Auch Objekte, die wir einem Parameter übergeben,
werden innerhalb der Funktion kopiert.
• Objekte außerhalb einer Funktion werden innerhalb einer Funktion nicht ver-
ändert. Eine nicht empfohlene Ausnahme dafür sehen wir in (30.1).
Wenn wir innerhalb einer Funktion auf außenstehende Objekte zugreifen, so kann
das tückische Folgen haben. Wir haben nämlich noch nicht geklärt, wo genau R
nach den gleichnamigen Objekten sucht. In (30.6.2) kommen wir darauf anhand
eines Beispiels zurück. Wir legen dir sehr nahe, dich an Regel 16 zu halten!
Schreibe deine Funktionen am besten immer so, dass alle Objekte, die innerhalb
deiner Funktionen verwendet werden, entweder als Parameter übergeben
werden oder innerhalb der Funktion erzeugt werden.
> mean(1:3)
[1] "Mittelwert"
418 F Eigene Funktionen und Ablaufsteuerung
Zur Beantwortung führen wir den Scopingoperator "::" ein. Mit ihm können wir
auf Objekte anderer Pakete zugreifen. Wir brauchen lediglich zu wissen, dass mean()
innerhalb des Pakets base definiert ist. Wenn wir das nicht (mehr) wissen sollten:
Das Paket können wir in der Hilfe in der linken oberen Ecke ablesen.
Wir können unsere eigene Funktion mean() mit der Funktion rm() löschen. Das
funktioniert völlig gleich, wie bei Konstanten (vgl. (14.1.1)).
> mean(1:3)
[1] "Mittelwert"
29.8 Abschluss
• Wie definieren wir eine eigene Funktion? Welche Anforderungen sollten ein
guter Funktionsname und gute Parameternamen erfüllen? Auf welche Arten
können wir ein Objekt zurückgeben und wodurch unterscheiden sie sich? (29.1)
• Wie erzeugen wir Fehlermeldungen und Warnungen? Läuft die Funktion nach
einer Fehlermeldung bzw. einer Warnung weiter? (29.2)
• Wodurch unterscheidet sich "&&" von "&"? Und wie "||" von "|"? In welchen
Fällen ist "&&" sehr nützlich bzw. sogar notwendig? (29.3)
• Was sind statische und dynamische Defaultwerte? Wie definieren wir solche
Defaultwerte in einer eigenen Funktion? Was sind Wrapper-Funktionen und
wann bzw. warum machen sie Sinn? (29.4.1), (29.4.2)
• Wie können wir innerhalb einer Funktion überprüfen, ob ein Parameter spezi-
fiziert wurde? (29.4.3)
• Was kann die Funktion switch() und wie setzen wir sie ein? (29.4.4)
29 Eigene Funktionen: Grundlagen 419
Folgende drei Dinge beherzigen wir, wenn wir eigene Funktionen schreiben:
29.8.2 Ausblick
In (30) vertiefen wir das Thema Funktionen und erörtern, wie wir eigene Operatoren,
generische Funktionen (vgl. (26)) und rekursive Funktionen schreiben.
29.8.3 Übungen
1. Ein Kollege ärgert sich darüber, dass in der Funktion sum() der Parameter
na.rm defaultmäßig auf FALSE gesetzt ist und beschließt, die Funktion sum()
zu überschreiben.
2. Schreibe eine Funktion, die einen numerischen Vektor x wie folgt transformiert:
0
falls x < a
x−a
y = b−a falls a ≤ x ≤ b
1 falls x > b
Prüfe dabei auf sinnvolle Werte für a und b und breche ggf. die Funktion mit
einem geeigneten Fehlertext ab. Verwende als Standardwerte a = 0 und b = 1.
Kommentiere die Funktion.
n
!1/p
p
X
kx − ykp := |xi − yi |
i=1
• Rekursion kennenlernen
• generische Funktionen und Operatoren schreiben
• weitere Einblicke in Environments und Scoping sammeln
In (20) haben wir Studierende durch eine Lehrveranstaltung begleitet und das Da-
taframe test erstellt. Laden wir zunächst die Daten.
Wir haben in (20.6.1) mittels sapply() die Mittelwerte jeder Punktespalte (Pkt1,
Pkt2 und Pkt) berechnet. Das funktioniert prima, aber was machen wir, wenn wir
diese Spalten manipulieren wollen? Zum Beispiel die Punkte auf 100% skalieren
wollen? Dank (29.6) haben wir eventuell schon eine Idee, warum diese Aufgabe
mit sapply() nicht bewältigbar ist. In (30.1) stellen wir Schleifen und *apply()
gegenüber und stellen fest, dass eine solche Manipulation mit Schleifen möglich ist.
In (30.3) führen wir rekursive Funktionen ein, das sind Funktionen, die sich selbst
aufrufen. Nachdem wir mit der Berechnung der Fakultät n! ein klassisches Beispiel
für eine rekursiv definierte Funktion besprochen haben, schauen wir uns zwei coole
Aufgabenstellungen an, die wir mit Rekursion elegant lösen können. Unter anderem
schreiben wir eine Funktion, die alle Permutationen einer Menge bestimmt. Und wir
besprechen, in welchen Fällen Rekursion extrem ineffizient ist.
Wie wäre es, wenn wir Datumsobjekte standardmäßig im Format dd.mm.yyyy aus-
geben könnten? Super? Dann wirst du von (30.4) begeistert sein, wenn wir eine
maßgeschneiderte print()-Funktion für diesen Zweck schreiben. In (30.6.1) erstel-
len wir dann eine neue Klasse zur Verwaltung von Brüchen und definieren dazu ein
paar praktische generische Funktionen und Operatoren.
Abschließend schreiben wir in (30.6.2) eine Funktion, die ein Polynom an mehreren
Stellen auswertet. Dabei erfahren wir mehr über Scoping (vgl. (29.6)).
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_30
422 F Eigene Funktionen und Ablaufsteuerung
apply(), sapply(), lapply() und tapply() sind allesamt Funktionen. Wir haben
in (29.6) beobachtet, dass es nicht möglich ist, Objekte außerhalb von Funktionen
zu manipulieren. Schleifen hingegen sind keine Funktionen im eigentlichen Sinn.
Betrachten wir die wichtigsten Unterschiede zwischen Schleife und *apply():
Mit dem Wissen aus (28.3.3) und (29.6) wird uns klar, warum wir die Fibonaccizah-
len (siehe zum Beispiel Seite 13 für eine Definition) nicht mit Hilfe von sapply()
erzeugen können (bzw. sollten): Wir müssen auf bereits berechnete Folgenglieder
zugreifen und neue Folgenglieder abspeichern, brauchen also Interaktion.
Einen (nicht zu empfehlenden (!)) Ausweg zeigt Punkt 2:
In der linken Version hat das innere Objekt fibo absolut nichts mit dem außerhalb
von sapply() stehenden Objekt fibo zutun. Daher bleibt fibo unangetastet.
In der rechten Version machen wir Gebrauch von "<<-". Das Objekt fibo wird wie
gehabt kopiert und für fibo eingesetzt. Durch die globale Zuweisung wird aber nicht
das lokale Objekt fibo überschrieben, sondern das globale Objekt fibo.
Beachte: Die globale Zuweisung kann zu ganz grauslichen Fehlern (Seiteneffekten)
führen. Daher am besten niemals einsetzen!
In (28.3) haben wir die (hier deutlich bessere) Schleifenversion besprochen.
30 Eigene Funktionen: Ergänzung und Vertiefung 423
> test
Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud1 38 A 12 30 30 2
Stud2 82 B 31 NA 31 2
Stud3 53 B 17 14 17 5
Stud4 72 A 0 13 13 5
Stud5 31 B 28 NA 28 3
Stud6 59 A 39 NA 39 1
Die Spalten Pkt1 und Pkt2 beinhalten die Testergebnisse von zwei Prüfungsantritten
und Pkt enthält das jeweils bessere der beiden Testergebnisse.
Ziel: Wir wollen jede Punktespalte auf 100% skalieren. Dabei entsprechen 40 Punkte
den 100%, da bei jedem Test maximal 40 Punkte erzielt werden konnten.
Wie gehen wir vor? Na ja, zunächst ist es sinnvoll, alle relevanten Spalten zu be-
stimmen. Dann gehen wir in einer for-Schleife diese Spalten durch und skalieren die
Ergebnisse. Los geht’s!
> test
Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud1 38 A 30.0 75.0 75.0 2
Stud2 82 B 77.5 NA 77.5 2
Stud3 53 B 42.5 35.0 42.5 5
Stud4 72 A 0.0 32.5 32.5 5
Stud5 31 B 70.0 NA 70.0 3
Stud6 59 A 97.5 NA 97.5 1
Die Testpunkte sind also auf 100% skaliert. Da Schleifen (wie übrigens auch if- und
else-Anweisungen) keine Funktionen sind, können wir innerhalb der for-Schleife
auf Objekte außerhalb der Schleife zugreifen und sie in der Schleife verändern. In
obigem Code also auf bestimmte Spalten von test.
Gleiches gilt auch umgekehrt und eine Interessantheit soll uns nicht verborgen blei-
ben. Die Schleifenvariable j, die innerhalb der Schleife erstellt wird, existiert auch
außerhalb der Schleife! Sie beinhaltet am Ende das letzte Element von names.pkt.
> j
[1] "Pkt"
424 F Eigene Funktionen und Ablaufsteuerung
Wir können Funktionen auch anonym definieren, zum Beispiel innerhalb von
*apply(). Anonym heißt, dass wir die Funktion nicht als Objekt zwischenspeichern.
Beispiel: Bestimme für die Spalte Pkt von test auf der vorherigen Seite die Inter-
quartilsspannweite (75%-Quantil 25%-Quantil) getrennt für jede Gruppe.
Variante 1: Definierte Funktion einsetzen
Wir definieren die Funktion iqr(), die wir sodann in tapply() einsetzen können.
Dass wir dabei na.rm = TRUE setzen müssen, soll nicht unbeachtet bleiben.
Die Elemente von test$Pkt werden gemäß test$Gruppe aufgeteilt und die resultie-
renden Vektoren getrennt in die Funktion iqr() gesteckt (vgl. (25.1)).
Variante 2: Anonyme Funktion
In dieser Variante definieren wir innerhalb von tapply() eine anonyme Funktion,
welche die Interquartilsspannweite berechnet.
Anonyme Funktionen sollten wir, wenn überhaupt, nur dann einsetzen, wenn die
Funktion kurz ist und nur einmal gebraucht wird.
Beispiel: Wie das vorherige Beispiel, nur für alle Punktespalten.
Die Idee: Wir stülpen ein sapply() über obigen Code.
Wir übergeben sapply() alle Punktespalten von test. Dank names.pkt auf Sei-
te 423 ist die Selektion der Punktespalten einfach. Jetzt entnimmt sapply() der
Reihe die Spalten aus test[names.pkt] und steckt sie als Vektor x in eine anonym
definierte Funktion, die wiederum x in tapply() einsetzt. Der Rest funktioniert wie
im vorherigen Beispiel.
sapply() wählt für das Ergebnisobjekt in diesem Fall eine Matrix, da das Ergebnis
der anonymen Funktion immer aus 2 Elementen besteht: je eine Interquartilsspann-
weite für die Gruppen A und B (vgl. (18.4) und (18.5)).
> fakultaet(4)
[1] 24
Wir setzen (30.1) eins-zu-eins um. Das Problem n! wird solange in n·(n − 1)! zerteilt,
bis wir das Problem direkt lösen können (falls n ≤ 1 gilt). In dem Fall können wir
per Definition den Wert 1 zurückgeben.
426 F Eigene Funktionen und Ablaufsteuerung
Die rechte Funktion benützt Recall(), um sich selbst wiederholt aufzurufen. Diese
Variante ist der linken generell vorzuziehen! Ändern wir nämlich den Namen der
Funktion, so bleiben die wiederholten Funktionsaufrufe davon unberührt.
Bemerkung: Die Funktion factorial() ist deutlich besser, da sie auch vektorwer-
tig funktioniert. Wenn wir an k! für alle k ∈ {1, . . . , n} interessiert sind, dann ist
cumprod() noch besser, wie wir schon in (8.1.2) bemerkt haben.
Beispiel: Schreibe eine Funktion, welche eine Matrix mit allen Permutationen eines
Vektors x bildet.
Wir zerlegen das Problem so lange in kleinere Teilprobleme, bis die Lösung der
Subprobleme direkt möglich ist. Das ist dann der Fall, wenn nur noch höchstens ein
Element übrig ist. Dann wird der Reihe nach das erste Element festgelegt, mit allen
Permutationen der Restmenge spaltenweise verknüpft und an die bisher berechneten
Permutation in Gestalt der Matrix M zeilenweise angehängt.
30 Eigene Funktionen: Ergänzung und Vertiefung 427
Gehen wir den Code schrittweise für obiges Beispiel durch! Die Einrückungen deuten
an, in welcher Ebene wir uns befinden.
Beachte, dass mit jedem Aufruf von Recall() eine neue Umgebung (Environment)
geöffnet wird. Insbesondere sind die Matrizen M in all diesen Aufrufen völlig unab-
hängig voneinander!
Viele Problemstellungen lassen sich elegant mit Rekursion lösen. Oftmals ist Rekur-
sion jedoch extrem ineffizient, wie folgendes Beispiel zeigt:
Beispiel: Wir betrachten erneut die Fibonaccifolge fn :
(
1 für n ∈ {1, 2}
fn =
fn−2 + fn−1 für n ≥ 3
1.) Schleifenvariante
2.) Rekursion
n %% 1 != 0 ergibt TRUE, wenn n nicht ganzzahlig ist. Wir brauchen zwei Oder-
Zeichen, denn n <= 0 kann nur dann sinnvoll überprüft werden, wenn n numerisch
ist (vgl. (29.3)). Rekursion ist in diesem Fall extrem ineffizient. Entferne das
Kommentarzeichen in den folgenden Codes und bemerke den Unterschied.
Bemerkung: Böse Zungen behaupten, dass manche bewusst die rekursive Variante
wählen, um eine Zigarettenpause oder Kaffeepause leichter rechtfertigen zu können.
Die Laufzeit der rekursiven Variante wächst rasend schnell! Für n = 40 würden wir
im rekursiven Fall länger als eine Stunde auf das Ergebnis warten! Der Grund: Wir
führen extrem viele unnötige Mehrfachberechnungen durch.
30 Eigene Funktionen: Ergänzung und Vertiefung 429
Rekursion ist daher vor allem für überschaubare Probleme geeignet, die mit Re-
kursion bequem implementiert werden können. Das abschließende Beispiel dieses
Abschnittes zeigt eine elegante Anwendung der Rekursion.
Beispiel: Vor langer Zeit ist ein Kollege mit folgendem Problem gekommen: Schrei-
be eine Funktion, die eine Sequenz der Länge k nach folgendem Muster generiert:
Dabei sollen die Zeichen frei gewählt werden können (hier die Zeichen a, b und c).
Lösung: Wir schreiben zwei Funktionen: Eine Hauptfunktion, die vom Benutzer
aktiviert wird und eine rekursiv definierte Hilfsfunktion, die bei Bedarf von der
Hauptfunktion aufgerufen wird.
Die Idee dahinter: Wenn k <= length(buch), dann brauchen wir nicht viel zu tun.
In dem Fall schreiben wir einfach die ersten k Zeichen ab, fertig!
Andernfalls erstellen wir eine Liste mit allen einelementigen Gliedern und rufen die
Hilfsfunktion sequenz.sub() auf. Da wir bereits length(buch) viele Glieder be-
stimmt haben, brauchen jetzt nur noch k - length(buch) viele Restglieder erzeugt
werden. Zunächst mit 2 Zeichen (bei Bedarf auch mehr).
Wir haben bereits generische Funktionen und Klassen angesprochen. Eine generische
Funktion ruft je nach Klasse des übergebenen Objektes eine geeignete Subfunktion
auf, die für diese Klasse maßgeschneidert ist. Wir können generische Funktionen
nicht nur anwenden, sondern deren Methoden auch (über)schreiben!
Beispiel: Schreibe eine Funktion print.Date, welche Objekte der Klasse Date im
Format "%d.%m.%Y" ausdruckt.
> # Beispieldaten
> datum <- as.Date(c("1.1.2018", "2.1.2018"), format = "%d.%m.%Y")
> class(datum)
[1] "Date"
In (26) haben wir uns über die internen Abläufe beim Aufruf von generischen Funk-
tionen unterhalten. Dort haben wir festgestellt, dass print() generisch ist.
Die ursprüngliche Funktion print.Date() existiert jedoch noch (im Paket base).
Wir können sie explizit aufrufen:
Mit Hilfe von "::" können wir Inhalte eines Paketes ansprechen (vgl. (29.7)). Hier
rufen wir also print.Date() aus dem Paket base auf.
Wir können sogar eigene Klassen definieren und uns eine geeignete Funktionssamm-
lung dazu programmieren. Das gleiche Prinzip gilt auch für Operatoren. Wir schauen
uns die Nützlichkeit von Klassen und generischen Funktionen und Operatoren an-
hand eines Fallbeispiels in (30.6.1) zum Thema Brüche an.
432 F Eigene Funktionen und Ablaufsteuerung
Da der Funktionsname %in% nicht mit einem Buchstaben beginnt, müssen wir ihn in
Anführungszeichen setzen. Wir bemerken an dieser Stelle erneut, dass wir Operato-
ren auch wie gewöhnliche Funktionen einsetzen können (vgl. (18.3)). Hierzu müssen
wir den Funktionsnamen in Anführungszeichen setzen.
Wie wäre es, wenn wir nicht nur Dezimalzahlen, sondern auch Brüche darstellen
könnten? Wenn wir mit Brüchen rechnen könnten?
Zunächst ein paar Vorüberlegungen. Wir stellen fest, dass Brüche nur aus ganzzah-
ligen Werten bestehen dürfen. Dazu schreiben wir eine Hilfsfunktion, die bestimmt,
ob alle Elemente (annähernd) ganzzahlig sind.
> is.ganzzahlig(-1)
[1] TRUE
> is.ganzzahlig(-1.0000001)
[1] FALSE
> is.ganzzahlig(-1.00000000000000000001) # Annähernd ganzzahlig
[1] TRUE
An dieser Stelle seien Integerzahlen erwähnt. Das sind ganzzahlige Werte. In R wer-
den diese durch die Klasse integer implementiert.
30 Eigene Funktionen: Ergänzung und Vertiefung 433
Wir schreiben gleich eine Funktion zur Erzeugung eines Bruches. Innerhalb dieser
prüfen wir, ob Zähler und Nenner annähernd ganzzahlig sind. Sind sie das, dann
wandeln wir sie in Objekte der Klasse integer um. Schauen wir uns an, wie wir mit
as.integer() Zahlen in ganzzahlige Werte der Klasse integer umwandeln
können.
Wir erkennen im linken Code, dass bei der direkten Umwandlung in Integerzahlen
die Nachkommastellen abgeschnitten werden. In unserem Fall ist das nicht sinnvoll,
besser wir runden mit round() zur nächstgelegenen ganzen Zahl. Dadurch wird 1.99
nicht mehr auf 1 abgeschnitten, sondern auf 2 aufgerundet.
Wir schreiben eine Funktion zur Erzeugung eines Bruchs (einen Konstruktor):
Die print()-Funktion ist mau. Lieber wäre uns wohl eine Ausgabe à la 2/3. Schrei-
ben wir doch eine maßgeschneiderte print()-Funktion für die Klasse Bruch:
Die Funktion cat() druckt Zeichenketten direkt auf die Console, "\n" beginnt
eine neue Zeile. Bevor wir unsere Funktion ausprobieren, überzeugen wir uns davon,
dass print.Bruch() der Methodenliste von print() hinzugefügt wurde.
> # Ruft intern print(b) auf > # Ruft intern print.Bruch(b) auf
> b > print(b)
2/3 2/3
Haut prima hin! Jetzt wollen wir Möglichkeiten bereitstellen, Brüche miteinander
zu multiplizieren und zu dividieren. Was funktioniert schon jetzt?
Die Multiplikation funktioniert bereits. Gerne darfst du eine Funktion schreiben, die
Brüche kürzt (2/6 → 1/3). Die Division hingegen arbeitet noch nicht korrekt, es soll-
te 4/3 herauskommen. Wir definieren daher einen Divisionsoperator für Brüche.
Der erste Bruch wird dabei mit dem Kehrwert des zweiten Bruches multipliziert.
> b1 / b2
4/3
> b1 + b2
3/5
30 Eigene Funktionen: Ergänzung und Vertiefung 435
> b1 + b2
7/6
> b1 + 2
Fehler in Bruch(zaehler, nenner) :
Zaehler und Nenner muessen ganzzahlig sein!
> 2 + b1
Fehler in Bruch(zaehler, nenner) :
Zaehler und Nenner muessen ganzzahlig sein!
Bei der Addition entstehen NA-Werte, was der Funktion Bruch() nicht schmeckt. Wir
schreiben "+.Bruch" daher um: Wir fragen ab, ob das Objekt die Klasse Bruch ent-
hält und wandeln es gegebenenfalls in einen Bruch um (mit Nenner 1). Da ein Bruch
zwei Klassen (Bruch und integer) hat, kommen wir mit class(b) == "Bruch"
nicht ans Ziel. Stattdessen stehen uns unter anderem folgende beiden Varianten zur
Verfügung, wobei inherits() eine Spezialfunktion zur Klassenabfrage ist.
In (29.6) haben wir Environments und Scoping eingeführt und festgestellt: Existieren
innerhalb einer Funktion Objekte nicht, dann sucht R nach gleichnamigen Objekten
außerhalb der Umgebung der Funktion. Offen geblieben ist, wo genau gesucht wird.
Das thematisieren wir jetzt und bei der Gelegenheit legen wir dir nochmals nahe,
dich nach Möglichkeit an Regel 16 auf Seite 417 zu halten.
Ein Polynom kann durch die Koeffizienten β charakterisiert werden:
n
X
f (x) = β1 + β2 · x + β3 · x2 + . . . + βn · xn−1 = βk · xk−1 (30.2)
k=1
Ziel: Schreibe eine Funktion polynom(), der wir den Koeffizientenvektor β eines
Polynoms übergeben können und die uns eine Funktion zurückgibt, mit der wir
dieses Polynom an beliebigen Stellen auswerten können.
30 Eigene Funktionen: Ergänzung und Vertiefung 437
Lösung:
> # polyfun ist eine function > # polyfun hat Mode function
> is.function(polyfun) > mode(polyfun)
[1] TRUE [1] "function"
Gerne darfst du per Hand nachrechnen, dass das Ergebnis stimmt! Innerhalb von fun
wird beta verwendet. Da diese Variable nicht innerhalb dieser Funktion definiert ist,
wird außerhalb nach dieser Variable gesucht. In der Umgebung von polynom() wird
R fündig beta = c(-1, 1, 2). Dieser Vektor wird herangezogen. Die außerhalb von
polynom() definierte Variable beta <- c(0, 1) bleibt unberücksichtigt.
Frage: Was passiert, wenn wir die Funktion fun() außerhalb von polynom() de-
finieren? Schauen wir es uns an!
438 F Eigene Funktionen und Ablaufsteuerung
Die Funktion polynom.neu() gibt lediglich die Funktion fun zurück. Diese ist in
polynom.neu() nicht definiert, also wird die außerhalb definierte Funktion fun her-
angezogen, sobald polynom.neu() aufgerufen wird. Dort kann die benötigte Variable
beta jedoch nicht gefunden werden. Jetzt sucht R in jener Umgebung nach beta,
in der fun definiert wurde! Dieses Vorgehen bezeichnet man als lexical scoping.
R wird fündig (beta <- c(0, 1)), womit beta = c(-1, 1, 2) nicht verwendet
wird und somit nicht das gewünschte bzw. erwartete Ergebnis herauskommt.
Es stellt sich die berechtigte Frage, warum wir eine Funktion zurückgeben mussten.
Warum definieren wir nicht einfach eine Funktion polyfun(), welche das Polynom
direkt für gegebene x und β auswertet?
Die Antwort: Damit wir das Thema Scoping vertiefen konnten. Die Aufgabe ein
Polynom auszuwerten lässt sich nämlich besser (unter Einhaltung der Regel 16 auf
Seite 417) so lösen:
Jetzt gibt es keine Missverständnisse bezüglich des Scopings mehr! Wenn wir zu-
sätzlich noch Gebrauch von Vektorarithmetik machen, können wir diesen Ansatz
beschleunigen.
30 Eigene Funktionen: Ergänzung und Vertiefung 439
> # res <- polyfun(x, beta) > # res <- polyfun.effizient(x, beta)
30.7 Abschluss
In den Fallbeispielen in (30.6.1) und (30.6.2) haben wir gesehen, wie wir eigene
Klassen mit dazu passenden Methoden und Operatoren schreiben können und unsere
Sinne für das Scoping geschärft.
30.7.2 Ausblick
30.7.3 Übungen
# Lösche Objekte
rm(v, w, x, y, z)
w <- 1
x <- 2
y <- 3
z <- 4
2. Inhaltliche Fortsetzung von Übungsbeispiel 1 auf Seite 254. Betrachte das Da-
taframe daten:
> geschlecht <- c("w", "m", NA, "m", "w", "w", "w", "m", "m", "m")
> groesse <- c(176, 181, 181, 183, 163, 157, 164, 166, 176, 184)
> gewicht <- c(65, 92, 65, 93, 49, 47, NA, 50, 62, 84)
Berechne für alle numerischen Spalten den Mittelwert getrennt nach Geschlecht.
Schließe dabei fehlende Werte aus.
3. Implementiere den Bisektionsalgorithmus aus (29.5.2) ab Seite 414 mit Hilfe
von Rekursion.
4. Schreibe für das Bruch-Fallbeispiel (30.6.1) die Funktion "-.Bruch".
G Datenimport und Datenexport
Daniel Obszelka
442 G Datenimport und Datenexport
• Strings formatieren
• Werte über die Console einlesen
• unser erstes interaktives Programm schreiben
An einer Veranstaltung der Kinderuniversität, bei der junge Menschen für die aufre-
gende Welt des statistischen Programmierens begeistert werden, nehmen hunderte
Kinder teil. Damit es übersichtlich bleibt, picken wir uns zwei Kinder heraus:
Unser Ziel ist es, Namen und Alter der Kinder zu formatieren und in Tabellenform
auszugeben. Um unser Vorhaben zu realisieren, fragen wir uns unter anderem:
• Wie drucken wir Namen und Alter auf die Console? Wie bauen wir dabei
Zeilenumbrüche und Tabulatoren ein? (31.1)
• Wie können wir die Namen und die Altersangaben (rechtsbündig) formatieren?
(31.2), (31.3)
• Wie erstellen wir aus den Einzelteilen eine Tabelle? (31.4)
Wir lernen außerdem in (31.5), wie wir Daten über die Console eingeben können und
nutzen dieses Wissen, um in (31.6.1) unser erstes interaktives Programm zu schrei-
ben. Ein Mathequiz, bei dem uns R Multiplikationsaufgaben stellt und in weiterer
Folge von uns über die Console die (hoffentlich richtige) Antwort erhält. Am Ende
gibt es dann eine kleine Auswertung.
Die Ausgabe von Texten oder Objekten auf die R-Console bewerkstelligen
wir mit den folgenden beiden Funktionen:
In (26) haben wir gelernt, dass print() eine generische Funktion ist, also ihr Ver-
halten an das übergebene Objekt anpasst.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_31
31 Stringformatierung, Consoleneingabe und -ausgabe 443
Die Funktion cat() druckt den Text direkt auf die Console. Bei jedem Auftreten
von \n wird dabei ein Zeilenumbruch eingefügt. Der allerletzte Zeilenumbruch sorgt
dafür, dass der folgende "> "-Prompt in einer neuen Zeile beginnt. Würden wir ihn
weglassen, so würde der > -Prompt unmittelbar nach dem Wort Abstand folgen.
Wir können jeden beliebigen String auf diese Art und Weise drucken, insbesondere
auch Zeichenketten, die wir mit paste() oder paste0() erzeugen.
Beispiel: Drucke name und alter derart auf die Console, dass in jeder Zeile ein
Name und das entsprechende Alter steht, jeweils durch einen Tabulator getrennt.
Ein Escape-Befehl zählt als ein Zeichen. Insgesamt besteht text aus 4 (Hugo) + 8
(Brigitte)+ 1 (9) + 2 (10) + 2 (2 × "\t") + 2 (2 × "\n") = 19 Zeichen.
> nchar(text)
[1] 19
Mit der Funktion format() können wir Zeichenketten formatieren. Die folgenden
Beispiele deuten die enorme Vielseitigkeit der Funktion format() lediglich an. Ein
zusätzlicher Blick in die R-Hilfe (?format) lohnt sich allemal!
Die Funktion format() ordnet die Namen standardmäßig linksbündig an und fügt
am Ende so viele Leerzeichen an, bis alle Namen gleich viele Zeichen haben.
444 G Datenimport und Datenexport
Mit dem Parameter width können wir die Textbreite einstellen, also die Anzahl
der Zeichen des formatierten Stringvektors steuern.
Der linke Code klärt uns darüber auf, dass format() keinen String abschneidet.
Für width wird zumindest die Zahl max(nchar(name)) eingesetzt. Im rechten Code
werden zwei Zeichen mehr verwendet als notwendig sind.
> alter
[1] 9 10
Anders als bei format() müssen wir bei formatC() die Breite explizit einstellen.
Mit max(nchar(alter)) bestimmen wir die Breite der breitesten Zahl von alter.
Wie wäre es, wenn wir statt Leerzeichen führende Nullen einsetzen? Gut? Dann
kommt die gute Nachricht: Mit flag = "0" ist das mühelos möglich.
Ideal geeignet, um etwa Geburtstage zu formatieren, sofern wir nicht auf die Kon-
zepte aus (27) zurückgreifen wollen ;-)
31 Stringformatierung, Consoleneingabe und -ausgabe 445
Beispiel: Erstelle aus name und alter eine „hübsche“ Tabelle mit den beiden Spal-
ten Name und Alter. Das Ergebnis könnte zum Beispiel so aussehen:
> cat(tabelle)
Name | Alter
----------------
Hugo | 9
Brigitte | 10
Das erste Element von text enthält die Spaltennamen. Wir wollen diese Spaltenna-
men vom Rest durch einen waagrechten Strich trennen.
Dazu replizieren wir "-" max(nchar(text)) Mal, damit der Strich genau so lang
wird wie die Zeilen unserer Tabelle. Garniert mit adäquaten paste0()-Befehlen und
einem abschließenden Zeilenumbruch, und schon ist unser Strich fertig.
Beim Zusammenbauen müssen wir aufpassen, dass wir die Zeilenumbrüche "\n"
richtig setzen. Nachdem wir die einzelnen Teile korrekt zusammengesetzt und dabei
insbesondere den Stich an der richtigen Stelle eingefügt haben, können wir voller
Stolz unsere Tabelle präsentieren!
Möchten wir die Tabelle in Microsoft Word einfügen, dann glänzt obige Tabelle
allerhöchstens nur dann, wenn wir eine Monospaced-Schriftart (wie etwa Courier
New) verwenden, also eine Schriftart, bei der jedes Zeichen gleich breit ist.
Für Proportional-Schriftarten (wie etwa Arial, Times, Verdana etc.) bietet es sich an,
stattdessen mit Tabulatoren zu arbeiten, die Tabelle in einer txt-Datei zu speichern
und die Funktion Text in Tabelle umwandeln von Word zu verwenden. Dann können
wir in Word die Tabelle nach unseren Wünschen gestalten.
Wie wir mit cat() die Tabelle als Textdatei abspeichern können, schauen wir uns
in (32.5) an.
Wenn wir eine Tabelle in LATEX einfügen wollen, so haben wir unter anderem
folgende Möglichkeit:
1. Wir schreiben eine Funktion, welche aus einem Dataframe eine kompilierbare
LATEX-Tabelle erzeugt. Das schauen wir uns in Übungsaufgabe 3 an.
2. Wir stecken das Dataframe in diese Funktion hinein und speichern den Output
in einer Datei ab.
3. Jetzt können wir die Tabelle in LATEX mit dem Befehl input{} einfügen.
Es gibt Zusatzpakete, die uns die Arbeit von Schritt 1 abnehmen. Allerdings ist
eine derartige Funktion in wenigen Minuten geschrieben und wir können Tabellen
komplett frei nach unseren Vorstellungen gestalten, wenn wir selbst Hand anlegen.
Was in die eine Richtung geht, geht meist auch in die andere! Wir schauen uns an-
hand von Beispielen an, wie wir mit scan() Consoleneingaben auslesen können.
Wir bestätigen die Eingabe jeweils mit ENTER. Der Funktionsaufruf wird beendet,
sobald wir erstmalig einen Leerstring übergeben.
Standardmäßig erwartet scan() die Eingabe von Zahlen. Wollen wir Zeichenketten
eingeben, so setzen wir what = "". Dann brauchen wir bei der Eingabe einzelner
Wörter diese auch nicht in Anführungszeichen setzen.
Wollen wir mehrere Wörter als ein Element einlesen, so müssen wir standardmäßig
diese Wörter in Anführungszeichen setzen. So erkennen wir im linken Code, dass
Hans Peter nicht als ein Wort, sondern als zwei Wörter interpretiert wird. In der
rechten Version wird – den Anführungszeichen sei Dank – "Hans Peter" als ein
Name eingelesen.
Alternative zu den Anführungszeichen: Den Parameter sep auf ein Zeichen setzen,
das sicher nicht verwendet wird (hier zum Beispiel "*").
Lange ist es her, dass wir über Lottozahlen gesprochen haben. Zeit, dies zu ändern!
Bei dieser Gelegenheit sehen wir auch, dass wir mit Hilfe des Parameters n die
Anzahl der Eingaben beschränken können.
Beispiel: In einer Lottoziehung wurden folgende Zahlen gezogen:
3, 19, 24, 23, 7, 34, 16
Lese die Zahlen über die Console ein.
Unser Ziel ist es, ein interaktives Programm zu schreiben, das n Multiplikations-
aufgaben stellt mit jeweils k Faktoren. Der Benutzer wird aufgefordert, die richtige
Antwort einzugeben. Je nachdem, ob die Antwort richtig oder falsch ist, wird ein
Glückwunschtext oder ein Motivationstext ausgegeben. Am Ende soll die Anzahl
der richtigen Antworten ausgegeben werden.
Abb. 31.1 enthält den ausführbaren R-Code und mit mathequiz(n = 10) werden
dir 10 Aufgaben gestellt. Viel Spaß!
In 1.) wird eine Matrix mit den Aufgaben erstellt und in 2.) die Rechnungen als
String zusammengebaut sowie die Ergebnisse berechnet.
In 4.) wird die nächste Aufgabe gedruckt. In 5.) wird unser Tipp entgegengenommen
und in 6.) mit der richtigen Antwort verglichen. Entweder wird ein Glückwunschtext
oder ein Motivationstext ausgegeben. In 7.) wird das Ergebnis verkündet.
Den rot markierten Schritt 5.) sehen wir uns genauer an. In dieser Form ist die Funk-
tion scan() nicht abgesichert gegenüber der Eingaben von Zeichenketten.
Würden wir als Ergebnis "Hugo" eingeben, so würde unser Programm abstürzen.
Eine Möglichkeit, damit umzugehen: Wir bohren so lange nach, bis eine gültige Zahl
eingegeben wird. Sehen wir uns an, wie das gehen könnte!
Wird keine gültige Zahl übergeben, so entsteht bei der Umwandlung ein NA. Genau
das gibt uns die Möglichkeit, eine gültige Zahl zu erkennen!
31 Stringformatierung, Consoleneingabe und -ausgabe 449
for (i in 1:n) {
# 4.) Frage stellen
cat("Wie lautet das Ergebnis der folgenden Rechnung:\n")
cat(paste0(" ", rechnung[i], "\n"))
Idee: Wir schreiben eine while-Schleife, in der wir den Benutzer solange eine Zahl
eingeben lassen, bis er erstmals eine korrekte Zahl eintippt.
Wir modifizieren nun den rot markierten Teil in Abb. 31.1. Abb. 31.2 enthält die
abgesicherte Version, wobei die Änderungen blau hervorgehoben sind. Die Funkti-
on suppressWarnings() erweist sich als nützlich, um die etwaigen Warnungen zu
unterdrücken.
Es kann passieren, dass für tipp ein Leerstring eingelesen wird. In dem Fall hat tipp
Länge 0 und die Abfrage !is.na(as.numeric(tipp)) würde ins Leere greifen. Das
bekommen wir in den Griff, indem wir zuerst abfragen, ob tipp eine Länge größer
0 hat und erst anschließend die Abfrage auf NA ausführen.
Das doppelte Und ("&&") ist notwendig, denn !is.na(as.numeric(tipp)) darf nur
dann ausgewertet werden, wenn length(tipp) > 0 den Wert TRUE liefert, andern-
falls beschwert sich die if-Anweisung:
anz.richtig <- 0
for (i in 1:n) {
cat("Wie lautet das Ergebnis der folgenden Rechnung:\n")
cat(paste0(" ", rechnung[i], "\n"))
suppressWarnings(
while (TRUE) {
tipp <- scan(what = "", n = 1) # what = "" -> character
31.7 Abschluss
• Was ist der Unterschied zwischen print() und cat()? Wie fügen wir bei der
Consolenausgabe Zeilenumbrüche und Tabulatoren ein? (31.1)
• Wir formatieren wir Strings linksbündig, zentriert und rechtsbündig? Wie stel-
len wir die Breite des Strings ein? (31.2)
• Wie formatieren wir Zahlen rechtsbündig? Wie können wir den Zahlen führen-
de Nullen anhängen? (31.3)
• Wie lesen wir über die Console Zahlen und Strings ein? Wie stellen wir beim
Einlesen von Strings sicher, dass mehrere Wörter als ein String interpretiert
werden? (31.5)
In (31.6) haben wir angeschnitten, wie wir Tabellen aufbereiten bzw. in Microsoft
Word und LATEX einfügen können. In (31.6.1) haben wir unter anderem besprochen,
wie wir bei der Consoleneingabe mit Hilfe einer while-Schleife gewährleisten können,
dass eine gültige Zahl eingegeben wird.
31.7.2 Ausblick
In (39.2) schauen wir uns kurz an, wie wir R-Codes in html- und LATEX-Dokumente
einbauen können.
31.7.3 Übungen
Die Zahlen sollen also auf 2 Nachkommastellen gerundet und nach dem Dezi-
malpunkt ausgerichtet bzw. rechtsbündig dargestellt werden.
31 Stringformatierung, Consoleneingabe und -ausgabe 453
2. Schreibe eine Funktion, die ein Dataframe (beliebiger Länge) als Inputpara-
meter hat und es schön formatiert. Als Inspiration kann das Beispiel ab Seite
445 dienen. Berücksichtige dabei ausschließlich Spalten, die numerisch, Strings
oder Faktoren sind, das heißt ignoriere die anderen Spalten.
Du darfst numerische Spalten generell auf zwei Nachkommastellen runden.
Gerne kannst du dir aber auch Gedanken über eine flexiblere Lösung machen
(z. B. mittels signif()). Bei Anteilswerten etwa sind zwei Stellen etwas wenig.
Hinweis: Eine for-Schleife ist nützlich. Überspringe in der Schleife bei Bedarf
Spalten, die ignoriert werden.
3. Schreibe eine Funktion, die ein Dataframe (beliebiger Länge) als Inputpara-
meter hat und es als fertige LATEX-Tabelle formatiert.
Eine einfache LATEX-Tabelle ist wie folgt aufgebaut:
• Ganz zu Beginn steht \begin{tabular} und am Ende \end{tabular}.
• Die einzelnen Spalten der Tabelle werden durch ein & getrennt.
• Das Ende jeder Tabellenzeile muss mit \\ markiert werden.
Erweitere sodann deine Funktion derart, dass man zusätzlich angeben kann,
wie jede Spalte angeordnet werden soll. Wir können dabei für jede Spalte
"l" (linksbündig), "c" (zentriert) sowie "r" (rechtsbündig) auswählen. Diese
Information wird direkt nach \begin{tabular} via { } angehängt.
Das Ergebnis könnte für unsere Daten aus der Einleitung so aussehen:
Hinweis: Ein Blick in (23.1) lohnt sich, falls du mit den Backslashes Schwie-
rigkeiten haben solltest.
Sehr gerne darfst du deiner Funktion weitere Zusatzfeatures hinzufügen!
4. Überlege dir zunächst ein Passwort. Schreibe sodann einen Code, der einen
Benutzer dazu auffordert, das richtige Passwort einzugeben. Er hat dabei drei
Versuche. Gibt er das richtige Passwort ein, so erscheint eine Erfolgsmeldung,
dass das Passwort korrekt war. Hat er nach drei Versuchen noch immer nicht
das richtige Passwort eingegeben, bricht der Code die Passworteingabe mit der
Meldung ab, dass der Benutzer keine weiteren Versuche hat.
454 G Datenimport und Datenexport
Bevor wir mit statistischen Auswertungen beginnen können, müssen wir die Daten
einlesen. Gerade das Einlesen von Daten und die Datenaufbereitung (Korrektur von
Formatierungs- und Datenfehlern etwa) beschäftigen uns manchmal länger.
Wir sehen uns in diesem Kapitel verschiedene Versionen des Vertreterdatensatzes an
(vgl. (24.0) ab Seite 311). Unter anderem:
• Vertreter.txt: Diese Datei haben wir bereits in (24.0) eingelesen. Sie um-
fasst die Nummer, das Gebiet (1 = West, 2 = Nord, 3 = Ost, 4 = Süd), die
Ausbildung (1 = Matura, 2 = Lehre) und den erzielten Gewinn in 1000 Euro
von 24 Vertretern.
• Vertreter1.txt: Dieselben Daten, nur sind Gebiet und Ausbildung als Zei-
chenketten gegeben und in Gebiet gibt es zusätzlich die Kategorie Zentrum.
• Vertreter2.txt: Dieselben Daten erweitert um die Variable Datum, die angibt,
wann der jeweilige Vertreter eingestellt wurde. Zusätzlich haben sich diver-
se Fehler und Unbequemlichkeiten eingeschlichen: Dezimalzahlen und fehlen-
de Werte sind uneinheitlich definiert, ungünstige Leerzeichen und irrelevante
Anfangs- und Schlussbemerkungen sind vorhanden.
4. Wie gehen wir mit den Tücken in Vertreter2.txt um? (32.3), (32.3.2)
Darüber hinaus befassen wir uns in (32.4) und (32.5) damit, wie wir Dateien ohne
rechteckiges Format einlesen und schreiben können. Abschließend lesen wir in (32.6)
Excel- und SPSS-Dateien ein.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_32
32 Datenimport und Datenexport 455
Zwei gängige einfache Dateiformate, die sich hervorragend für rechteckige Da-
tenstrukturen wie etwa Dataframes eignen, sind:
• Text (*.txt): Die einzelnen Spalten sind durch einen Tabulator oder ein Leer-
zeichen getrennt.
• csv (comma separated values, *.csv): Die Spalten sind standardmäßig durch
ein Komma (",") oder durch einen Strichpunkt (";") getrennt.
Die Dateiendung ist an sich von sekundärer Bedeutung, es handelt sich hierbei um
Konventionen, an die wir uns halten sollten. Beide Dateiformate schauen wir uns in
den kommenden Unterabschnitten an.
Mit der Funktion read.table() können wir Daten einlesen, die eine rechtecki-
ge Gestalt aufweisen. Der Übersicht halber werden hier nur ausgewählte wichtige
Parameter aufgelistet, die in Tab. 32.1 kurz beschrieben werden. Über die Codie-
rung von Dateien (Parameter fileEncoding) sprechen wir separat in (32.2). Eine
Übersicht über alle Parameter und Details findest du in der R-Hilfe (?read.table).
Bevor wir uns die Anwendung von read.table() anschauen, wiederholen wir kurz
einige Konzepte aus (5). Wir können für file den absoluten Pfad (vom Laufwerk
bis zur Datei) oder den relativen Pfad übergeben. Übergeben wir für file nur den
Dateinamen, so sucht R im aktuellen Ordner (Arbeitsverzeichnis) nach dieser
Datei. Wird R nicht fündig, wird eine Fehlermeldung ausgegeben. Mit "../" können
wir in den übergeordneten Ordner wechseln (vgl. (5.2)).
Beispiel: Lese die Datei Vertreter1.txt ein. In Tab. 32.1 finden wir einen Auszug
dieser Datei.
456 G Datenimport und Datenexport
Parameter Bedeutung
file Dateiname oder Pfad der einzulesenden Datei
header Enthält die erste einzulesende Zeile die Spaltennamen?
TRUE: ja. FALSE: nein (Standard)
sep Wie sind die Spalten voneinander getrennt?
Standard: sep = "" (White Space – in erster Linie
Leerzeichen und Tabulatoren)
dec Dezimaltrennzeichen (Standard: Punkt ".")
na.strings Wie werden fehlende Werte dargestellt?
Kann auch ein Vektor sein.
nrows Wie viele Zeilen sollen höchstens eingelesen werden?
skip Anzahl der Zeilen, die zu Beginn übersprungen werden.
strip.white Wird nur verwendet, falls sep spezifiziert wurde.
TRUE: White Space wird entfernt.
FALSE: Gegenteil (Standard)
stringsAsFactors FALSE: Strings werden nicht in Faktoren umgewandelt
(Standard seit Version 4.0.1).
TRUE: Strings werden in Faktoren umgewandelt.
fileEncoding Wie ist die Datei codiert (Zeichencodierung)?
Wir nehmen im Folgenden an, dass sich die Datei im Ordner D:/RBuch/Daten be-
findet. Zur Wiederholung schauen wir uns zwei Lösungsvarianten an: eine, die auf
absoluten Pfaden und eine, die auf dem Arbeitsverzeichnis beruht.
Variante 1: Absoluter Pfad
In dieser Variante übergeben wir den absoluten Pfad vom Laufwerk bis zur Datei
Vertreter1.txt an.
Die erste Zeile enthält die Spaltennamen, was wir der Funktion read.table() mit
header = TRUE) mitteilen. Das Dezimaltrennzeichen ist ein Komma, weswegen wir
dec = "," setzen. Was wir nicht erkennen: Die Datei ist im UTF-8-Format co-
diert, daher schreiben wir fileEncoding = "UTF-8". Wir kommen in (32.2) auf das
Thema Zeichencodierung zurück.
Die Spalten sind durch Tabulatoren getrennt und da Tabulatoren White Space sind,
brauchen wir sep nicht zu spezifizieren. Würden aber Leerzeichen bei einzelnen
Einträgen verwendet, so müssten wir explizit sep = "\t" schreiben (vgl. (32.3)).
Variante 2: Verzeichnis mit der Funktion setwd() wechseln.
Mit der Funktion setwd() wechseln wir das Arbeitsverzeichnis in jenen Ordner, in
dem sich die Datei Vertreter1.txt befindet.
> setwd("D:/RBuch/Daten")
> file <- "Vertreter1.txt"
> daten <- read.table(file, header = TRUE, dec = ",", fileEncoding = "UTF-8",
+ stringsAsFactors = TRUE)
> head(daten, n = 7)
Nummer Gebiet Ausbildung Gewinn
1 1 Zentrum Matura 11.4
2 2 Zentrum Matura 9.2
3 3 West Matura 9.3
4 4 Ost Lehre 13.6
5 5 Zentrum Matura 8.9
6 6 Zentrum Lehre 3.3
7 7 Süd Lehre 2.1
458 G Datenimport und Datenexport
Die Funktion write.table() steht uns zur Verfügung, wenn wir Textdateien
schreiben wollen. Sie hat viel weniger Parameter als read.table(). Wir werden
einige von ihnen anhand eines Beispiels betrachten. Für weitere Informationen sei
auf die R-Hilfe verwiesen (?write.table).
write.table(x, file = "", append = FALSE, quote = TRUE, sep = " ",
eol = "\n", na = "NA", dec = ".", row.names = TRUE,
col.names = TRUE, qmethod = c("escape", "double"),
fileEncoding = "")
Beispiel: Hänge an das Dataframe daten auf der vorherigen Seite die Gewinnstufe
an, die wie folgt definiert ist:
Gewinn ≤ 5: wenig, 5 < Gewinn ≤ 10: mittel, Gewinn > 10: viel
Speichere das resultierende Dataframe als Textdatei (Tabulator getrennt) und als
csv-Datei ab. Ein Tabulator wird durch den Escape-Befehl \t dargestellt.
Für das Lesen und Schreiben von csv-Dateien stehen uns auch die Funktionen
read.csv() und write.csv() bzw. read.csv2() und write.csv2() zur Verfügung.
Sie entsprechen den Funktionen read.table() und write.table(), es werden je-
doch csv-typische Defaultwerte verwendet. Die csv2-Varianten sind speziell für
deutschsprachige Dateien (sep = ";", dec = ",") entwickelt.
32 Datenimport und Datenexport 459
32.2 Zeichencodierung
Frage: Was wäre passiert, wenn wir in (32.1.1) die Datei Vertreter.txt ohne
UTF-8-Spezifikation eingelesen hätten? Finden wir es heraus!
Ohne UTF-8-Spezifikation wird das ü in der Kategorie SÃd nicht korrekt eingelesen.
Gleiches passiert auch für andere Umlaute und etwa das scharfe „S“ (ß). Wir erfah-
ren in (32.2.1) den Grund dafür und erfahren anschließend in (32.2.2) und (32.2.3)
(weitere) Möglichkeiten der Handhabung.
32.2.1 Zeichencodierungsstandards
Jedes Zeichen wird intern als Binärzahl, also eine Zahl bestehend aus 0 und 1,
verwaltet. Die Zeichencodierung legt fest, auf welches Zeichen eine bestimmte
Zahl abgebildet wird. Unterschiedliche Zeichencodierungen können allerdings
unter Umständen dieselbe Zahl auf unterschiedliche Zeichen abbilden. Daher
hat man einheitliche Codierungsstandards eingeführt.
!"#\$\%\&’()*+,-./0123456789:;<=>?
@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
‘abcdefghijklmnopqrstuvwxyz{|}~
Umlaute sind also mit ASCII nicht abbildbar, was weitere Codierungs-
standards notwendig macht.
• Latin-1 (nach ISO 8859-1): Druckbare Zeichen, die mit ASCII abbildbar
sind, haben in Latin-1 dieselbe Codierung. Zusätzlich wird ein Teil des
höchsten Bits (das ASCII nicht verwendet) dazu genützt, um typische
westeuropäische Zeichen wie Umlaute zu codieren, die durch ASCII nicht
abgedeckt werden.
Problem: Die Anzahl der codierbaren Zeichen ist nach wie vor begrenzt,
da weiterhin nur 1 Byte verwendet wird. Es sind daher nicht alle Zeichen
mit diesem Standard darstellbar. Deswegen wurden weitere Standards
nach ISO 8859 eingeführt, um das auszugleichen.
Zurück zu unserem Beispiel. Das Zeichen ü kann mit ASCII also nicht abgebildet
werden und in der UTF-8-Zeichencodierung ist ein zweites Byte nötig. Wenn wir
aber keine Codierung angeben, so wird (standardmäßig) jedes Byte einzeln in-
terpretiert. Ob R dabei ASCII oder etwa Latin-1 verwendet, spielt dabei eine
nebensächliche Rolle, das Ergebnis ist in beiden Fällen falsch.
Das erklärt, warum SÃd aus vier Zeichen besteht (S + zwei Zeichen für ü + d).
> temp
[1] "SÃd"
Mit dem Parameter encoding der Funktion options(), die wir aus (6.4) kennen,
können wir den verwendeten Zeichencodierungsstandard umstellen.
Bei allen Dateien, die ab sofort geschrieben oder eingelesen werden, wird jetzt stan-
dardmäßig die UTF-8-Codierung herangezogen. Jetzt brauchen wir beim Einlesen
von Vertreter1.txt den Parameter fileEncoding nicht mehr einstellen, die Ka-
tegorie Süd wird korrekt eingelesen.
Wir vertiefen das Thema Zeichencodierung an dieser Stelle nicht weiter. Du weißt
alles Wichtige, was du wissen musst. Sehr gerne darfst du weitere Recherchen auf
eigene Faust durchführen!
Tipp: Verwende durchgängig UTF-8 für deine Dateien.
462 G Datenimport und Datenexport
Wir beschreiben eine gangbare Strategie beim Einlesen von Dateien. Idealer-
weise ist uns der verwendete Zeichencodierungsstandard bekannt, dann nehmen wir
diesen. Ist die Zeichencodierung unbekannt, so können wir mehrere Standards aus-
probieren (UTF-8, Latin-1 etc.). Führt dies zu keinem Erfolg, können wir versuchen,
die Zeichencodierung extern (außerhalb von R) zu ändern.
Haben wir jetzt immer noch keine korrekt eingelesene Datei, dann können wir die
falsch eingelesenen Zeichen immer noch „per Hand“ korrigieren. Anhand eines
kleinen Codebeispiels schauen wir uns kurz an, wie das funktionieren könnte.
32.3 Datenaufbereitung
Beispiel: Wir betrachten in Abb. 32.2 und Abb. 32.3 die Bildschirmkopien der
beiden Dateien Vertreter2.txt und Vertreter2.csv.
Beide Dateien enthalten wie gesagt dieselben Daten nur in einem anderen Format.
In der csv-Version werden die Spalten durch ein ";" getrennt.
32 Datenimport und Datenexport 463
1. Zu Beginn wird der Datensatz beschrieben. Diese Zeilen sind für uns bedeu-
tungslos und sollen daher nicht eingelesen werden.
Lösung: Dies können wir mit skip = 9 beheben.
2. Ab Zeile 35 sollen die Daten nicht mehr eingelesen werden.
Lösung: nrows = 24 (24 Zeilen nach den Überschriften, da header = TRUE)
3. In der Spalte Gewinn werden fehlende Werte unterschiedlich bezeichnet.
Lösung: na.strings = c(NA, "na", "")) oder alternativ: Ersetzfunktio-
nen benützen, um NA-Strings zu vereinheitlichen.
4. Ebenfalls in der Spalte Gewinn wird ein paar Mal ein Dezimalpunkt statt eines
Dezimalkommas verwendet. Dies würde beim Einlesen zu Problemen führen
(Gewinn würde nicht als numerisch erkannt werden).
Lösung: Wenn die Daten in Excel zur Verfügung stehen, können wir mit der
Ersetzfunktion bequem alle Punkte durch Kommata ersetzen (Vorsicht: Bei
Datum sind die Punkte erwünscht!). Alternative: die Funktion sub() verwen-
den und in einen numerischen Vektor umwandeln.
5. In der Spalte Ausbildung kommen beim 2. und 4. Vertreter ungewollte Leer-
zeichen vor. Dies würde dazu führen, dass für dieselben Kategorien jeweils
eigene Faktorstufen angelegt werden.
Lösung: Mit der Ersetzfunktion Leerzeichen entfernen, strip.white = TRUE
setzen oder Faktorstufen in R verschmelzen.
Ziel: Lese nun sowohl Vertreter2.txt als auch Vertreter2.csv korrekt ein und
gib allen Spalten sinnvolle Modes bzw. Klassen.
Zunächst das csv-File:
Hier ergibt sich folgender interessanter Fall: Eigentlich bräuchten wir sep nicht spe-
zifizieren, denn Tabulatoren sind (auch) White Space. In diesem Fall würden auto-
matisch auch alle Leerzeichen bei Ausbildung wegfallen. Da allerdings bei Vertreter
Nummer 21 nichts bei Gewinn steht, würde das zu einer Fehlermeldung führen.
466 G Datenimport und Datenexport
Daher müssen wir den Tabstopp als Trennzeichen definieren (mit sep = "\t"). Dann
müssen wir jedoch auch strip.white = TRUE setzen, um die ungewollten Leerzei-
chen bei der Ausbildung zu entfernen und Problem Nummer 5 zu beheben. Schauen
wir uns an, was mit der Variable Ausbildung passieren würde, wenn wir die Daten
mit der Option sep = "\t" und ohne strip.white = TRUE einlesen würden.
> head(temp, n = 8)
Nummer Gebiet Ausbildung Gewinn Datum
1 1 Zentrum Matura 11,4 01.04.2018
2 2 Zentrum Matura 9,2 01.06.2018
3 3 West Matura 9,3 01.05.2018
4 4 Ost Lehre 13,6 01.02.2018
5 5 Zentrum Matura <NA> 01.11.2018
6 6 Zentrum Lehre 3.3 01.10.2018
7 7 Süd Lehre 2,1 01.10.2018
8 8 Ost Lehre <NA> 01.12.2018
> temp$Ausbildung
[1] Matura Matura Matura Lehre Matura Lehre Lehre Lehre Lehre
[10] Lehre Matura Matura Lehre Lehre Matura Lehre Matura Matura
[19] Lehre Matura Matura Lehre Lehre Matura
Levels: Matura Lehre Lehre Matura
> levels(temp$Ausbildung)
[1] " Matura" "Lehre" "Lehre " "Matura"
> head(daten, n = 8)
Nummer Gebiet Ausbildung Gewinn Datum
1 1 Zentrum Matura 11,4 01.04.2018
2 2 Zentrum Matura 9,2 01.06.2018
3 3 West Matura 9,3 01.05.2018
4 4 Ost Lehre 13,6 01.02.2018
5 5 Zentrum Matura <NA> 01.11.2018
6 6 Zentrum Lehre 3.3 01.10.2018
7 7 Süd Lehre 2,1 01.10.2018
8 8 Ost Lehre <NA> 01.12.2018
32 Datenimport und Datenexport 467
> daten$Gebiet
[1] Zentrum Zentrum West Ost Zentrum Zentrum Süd Ost Süd
[10] Nord West Süd West West Nord Nord Süd Ost
[19] Süd West Nord Ost Nord Ost
Levels: Nord Ost Süd West Zentrum
> daten$Ausbildung
[1] Matura Matura Matura Lehre Matura Lehre Lehre Lehre Lehre Lehre
[11] Matura Matura Lehre Lehre Matura Lehre Matura Matura Lehre Matura
[21] Matura Lehre Lehre Matura
Levels: Lehre Matura
> daten$Gewinn
[1] 11,4 9,2 9,3 13,6 <NA> 3.3 2,1 <NA> 10,7 16,7 11,8 7,8 7,4 <NA>
[15] 9,4 7,1 12,2 8,1 7,9 10.5 <NA> 6,4 10,4 24,3
20 Levels: 10,4 10,7 10.5 11,4 11,8 12,2 13,6 16,7 2,1 24,3 3.3 6,4 ... 9,4
> daten$Datum
[1] 01.04.2018 01.06.2018 01.05.2018 01.02.2018 01.11.2018 01.10.2018
[7] 01.10.2018 01.12.2018 01.05.2018 01.04.2018 01.07.2018 01.08.2018
[13] 01.07.2018 01.11.2018 01.01.2018 01.06.2018 01.05.2018 01.07.2018
[19] 01.07.2018 01.06.2018 01.11.2018 01.05.2018 01.04.2018 01.02.2018
10 Levels: 01.01.2018 01.02.2018 01.04.2018 01.05.2018 ... 01.12.2018
Bei der Variable Gewinn sehen wir, dass sämtliche fehlende Werte korrekterweise als
NA (bzw. <NA>) markiert werden.
Bemerkung: Hätten wir beim Einlesen stringsAsFactors nicht auf TRUE gesetzt,
dann wären die Variablen Gebiet, Ausbildung, Gewinn und Datum nicht als Fakto-
ren, sondern als character-Vektoren eingelesen worden. Wir erläutern in (32.3.2),
welche Modifikationen bei der Datenaufbereitung sich in diesem Fall ergeben.
Folgende Aufgaben werden wir gleich „per Hand“ lösen:
a) Die Levels von Gebiet und Ausbildung sind nicht korrekt sortiert. Wir wün-
schen uns aber stattdessen folgende Codierungen (vgl. Seite 310):
1 = West, 2 = Nord, 3 = Ost, 4 = Süd und 5 = Zentrum sowie
1 = Matura und 2 = Lehre.
b) Datum ist ein Faktor und soll in ein Objekt der Klasse Date umgewandelt
werden (siehe (27)).
c) In der Variable Gewinn ist das Dezimaltrennzeichen nicht einheitlich. Zwei Mal
wird der Dezimalpunkt anstatt des Dezimalkommas verwendet. Dadurch wird
Gewinn fälschlicherweise als Faktor interpretiert. Das Dezimaltrennzeichen soll
zunächst vereinheitlicht und anschließend Gewinn in einen numerischen Vektor
(mit den richtigen Werten) umgewandelt werden.
Bevor du weiterliest: Löse diese drei Aufgaben zunächst selbst. Du weißt an dieser
Stelle bereits alles, was du zur Bewältigung wissen musst.
468 G Datenimport und Datenexport
Sollten bei dir nicht die korrekten Gewinnwerte herauskommen, könnten evtl. fol-
gende Kapitel inspirieren: (24.5.1), (24.5.2)
Betrachten wir die Auswirkungen unserer Maßnahmen:
> head(daten, n = 8)
Nummer Gebiet Ausbildung Gewinn Datum
1 1 Zentrum Matura 11.4 2018-04-01
2 2 Zentrum Matura 9.2 2018-06-01
3 3 West Matura 9.3 2018-05-01
4 4 Ost Lehre 13.6 2018-02-01
5 5 Zentrum Matura NA 2018-11-01
6 6 Zentrum Lehre 3.3 2018-10-01
7 7 Süd Lehre 2.1 2018-10-01
8 8 Ost Lehre NA 2018-12-01
> daten$Gewinn
[1] 11.4 9.2 9.3 13.6 NA 3.3 2.1 NA 10.7 16.7 11.8 7.8 7.4 NA
[15] 9.4 7.1 12.2 8.1 7.9 10.5 NA 6.4 10.4 24.3
> is.numeric(daten$Gewinn) # TRUE - passt :-)
[1] TRUE
Bis jetzt haben wir uns angesehen, wie wir Standardformate (insbesondere recht-
eckige Datenstrukturen) einlesen können. Oftmals sind die Daten aber nicht in
Form einer rechteckigen Datenstruktur gegeben, sondern weisen komplexere Struk-
turen auf. Ein Beispiel sind html-formatierte Texte.
Mit der Funktion scan() können wir Dateien mit komplexerem Dateiformat
einlesen und sie anschließend in R nachbearbeiten. Wir verweisen für die Beschrei-
bung der Parameter auf die R-Hilfe (?scan()).
Unter Umständen benötigen wir viele Stringmanipulationen, bis wir die Daten in
die gewünschte Form gebracht haben. Wir betrachten daher nur ein kleines Beispiel.
Beispiel: Wir betrachten erneut die Datei Vertreter.txt. Dort sind die Variablen
Gebiet und Ausbildung als Nummerncodes abgespeichert.
470 G Datenimport und Datenexport
Dummerweise haben wir uns nicht gemerkt, welche Beschriftungen zu den Nummern-
codes gehören. Aber wir wissen, dass es eine Datei gibt, welche diese Informationen
enthält. Zwar kennen wir den genauen Namen der Datei nicht mehr, aber wir wissen,
dass im Dateinamen das Wort Labels vorkommt. An dieser Stelle erinnern wir uns
an die Funktion list.files() (siehe (5.4)), die alle Dateien und Ordner auflistet,
welche sich im aktuellen Verzeichnis oder einem bestimmten Ordner befinden.
Die Datei heißt also Vertreter_Labels.txt. Werfen wir in Abb. 32.4 einen Blick
auf diese Datei.
32 Datenimport und Datenexport 471
Die Datei wird in diesem Fall Wort für Wort eingelesen. Und so sieht das Ganze
aus: scan() hat jedes Wort eingelesen und die Wörter als Vektor gespeichert. Nun
können wir mit einfachen Regeln die gewünschten Informationen extrahieren:
a) Die Namen der Variablen stehen rechts vom Schlüsselwort Variable. Wir kön-
nen also nach Variable suchen und den Index um 1 erhöhen.
b) Die Labels stehen rechts vom Gleichheitszeichen.
Jetzt müssen wir noch die Labels den richtigen Variablen zuordnen. Matura und
Lehre gehören zur Variable Ausbildung und die restlichen vier zu Gebiet. Wir
wissen, dass alle Labels, deren Indices zwischen 6 und 14 liegen, zur 1. Variablen
gehören und alle mit einem Index größer als 14 zur 2. Diese Information nützen wir
für eine automatisierte Lösung aus!
Voilà! Nun picken wir uns in einer for-Schleife die Variablen aus variablennamen
der Reihe nach heraus und weisen ihnen Faktoren mit den Labels zu.
> head(daten, n = 5)
Nummer Gebiet Ausbildung Gewinn
1 1 Nord Matura 11.4
2 2 Ost Matura 9.2
3 3 West Matura 9.3
4 4 Ost Lehre 13.6
5 5 Süd Matura 8.9
Was in die eine Richtung geht, funktioniert meistens auch in die andere. Mit der
Funktion cat() können wir Textdateien beliebigen Formates erstellen. Wir
kennen die Funktion bereits aus (31.1).
cat(... , file = "", sep = " ", fill = FALSE, labels = NULL, append = FALSE)
Wird file spezifiziert, so wird die Ausgabe nicht auf die Console, sondern in das
spezifizierte file geleitet. Interessant ist der Parameter append. Setzen wir diesen
auf TRUE, wird keine neue Datei erstellt, sondern der Inhalt von ... an die Textdatei
angehängt, so diese Datei existiert.
32 Datenimport und Datenexport 473
<person>
<name>Tom</name>
<alter>28</alter>
<wohnort>Mödling</wohnort>
</person>
<person>
<name>Anna</name>
<alter>NA</alter>
<wohnort>Wien</wohnort>
</person>
Die Idee: Wir hängen vektorwertig an jede Variable einen <...>-Tag vorne und einen
</...>-Tag an. Anschließend stülpen wir noch <person> und </person> über jedes
Datum. Geschickt platzierte Zeilenumbrüche runden unseren folgenden Code ab.
Wenn wir cat(string) eingeben, so kommt genau der gewünschte Output heraus.
Wir speichern die Datei abschließend in UTF-8-Codierung ab (siehe (32.2.2)).
Zum Einlesen von Excel-Dateien existiert etwa das Paket xlsx, das unter ande-
rem die Funktion read.xlsx() enthält. Um diese Funktion nützen zu können, muss
Java auf dem PC installiert sein. Siehe: https://www.java.com/de/download/
Wir werfen einen Blick auf den Funktionsaufruf. Die Parameternamen sind spre-
chend, für Details verweisen wir auf die R-Hilfe (?read.xlsx).
Beispiel: Lese das Sheet Vertreter2 der Datei Vertreter2.xls ein. Es enthält
dieselben Daten wie die Datei Vertreter2.txt (vgl. Abb. 32.2 auf Seite 463).
Nun können wir die Daten, wie in (32.3) gezeigt, aufbereiten. Zwei mögliche Gründe,
warum es zu Fehlern kommen kann:
Falls das nicht funktioniert, gibt es zur Not immer noch den Ausweg, in Excel die Da-
ten im csv- oder txt-Format abzuspeichern und dann beispielsweise mit der Funktion
read.table() einzulesen.
32 Datenimport und Datenexport 475
Mit der Funktion read.spss() des Pakets foreign können wir SPSS-Dateien
einlesen. Werfen wir einen Blick auf den Funktionsaufruf, für Details verweisen wir
auf die R-Hilfe.
Beispiel: Lese die Datei Vertreter1.sav ein. Es handelt sich um dieselben Daten
wie bei Vertreter1.txt aus (32.1.1).
> daten$Gebiet
[1] Zentrum Zentrum West Ost Zentrum Zentrum Süd Ost Süd
[10] Nord West Süd West West Nord Nord Süd Ost
[19] Süd West Nord Ost Nord Ost
Levels: West Nord Ost Süd Zentrum
Mit to.data.frame = TRUE werden die Daten in ein Dataframe überführt. Die
Funktion read.spss() wählt normalerweise eine sinnvolle Zeichencodierung. Sollten
Sonderzeichen nicht korrekt eingelesen werden, so kann reencode = TRUE Abhilfe
schaffen.
Das Paket foreign bietet auch Möglichkeiten für SAS-Dateien, dazu muss jedoch
SAS installiert sein.
476 G Datenimport und Datenexport
32.7 Abschluss
• Welches Format hat eine Textdatei bzw. eine csv-Datei? Wie lesen wir sol-
che Dateien mit read.table() korrekt ein? Wie teilen wir dieser Funktion
mit, dass die erste Zeile die Variablennamen enthält? Wie definieren wir das
Trennzeichen für die Spalten (insbesondere Tabulatoren) sowie Dezimaltrenn-
zeichen? (32.1), (32.1.1)
• Wie speichern wir ein Dataframe als Textdatei ab? Was bewirken in der Funkti-
on write.table() die Parameter quote, sep, dec, row.names und col.names?
(32.1.2)
• Wie lesen wir csv-Dateien ein? Und wie speichern wir ein Dataframe als csv-
Datei ab? (32.1.1), (32.1.2), (32.1.3)
• Was ist eine Zeichencodierung bzw. ein Zeichencodierungsstandard? Welche
Grundidee steckt hinter den Standards ASCII, Latin-1 und UTF-8? Wie stellen
wir in read.table() den Zeichencodierungsstandard ein? (32.2), (32.2.1)
• Wie stellen wir den Zeichencodierungsstandards bei Strings um? Und wie kön-
nen wir den standardmäßig verwendeten Zeichencodierungsstandard umstel-
len? (32.2.2)
• Wie können wir in read.table() die ersten k Zeilen beim Einlesen übersprin-
gen? Wie legen wir die Anzahl der einzulesenden Zeilen fest? Wie gehen wir
vor, wenn fehlende Werte bzw. Dezimalzahlen unterschiedlich definiert sind?
Wie gehen wir damit um, wenn bei kategoriellen Variablen ungewollte Leer-
zeichen vorkommen? (32.3.1)
• Was bewirkt der Parameter stringsAsFactors in read.table()? (32.3.2)
• Wie lesen wir Textdateien mit beliebigem Format ein? (32.4)
• Wie schreiben wir Textdateien mit beliebigem Format? (32.5)
• Wie lesen wir Excel-Dateien ein? (32.6.1)
• Wie lesen wir SPSS-Dateien ein? (32.6.2)
In diesem Kapitel haben wir auch wichtige Konzepte zu den Themen Arbeitsverzeich-
nis und Dateipfade ((5)), Faktoren ((24)) und Datum ((27)) wiederholt. Außerdem
haben wir in (32.4) ein Beispiel dafür gesehen, wie wir mit Hilfe von Regeln auto-
matisiert bestimmte Informationen aus einem Text extrahieren können, wobei uns
die Stringfunktionen und Regular Expressions aus (22) und (23) helfen.
32 Datenimport und Datenexport 477
32.7.2 Übungen
a) Lies die Datei korrekt in R ein und erstelle aus den Daten ein Dataframe.
Achte darauf, dass jede Variable einen geeigneten Mode hat.
b) Speichere das Dataframe aus 1a) als Textdatei ab. Wähle dabei den UTF-
8-Zeichencodierungsstandard.
c) Lies wiederum die Datei aus 1b) korrekt in R ein. Achte wiederum darauf,
dass jede Variable einen geeigneten Mode hat.
a) Lies (zur Wiederholung) die Daten erneut als Dataframe ein. Bereinige
dabei alle Datenfehler.
b) Hänge an das Dataframe die Variable Effizienz an. Sie enthält den
mittleren Gewinn pro Tag in Euro, wobei die Zeitdifferenz zwischen dem
Einstellungsdatum und dem 31.12.2018 die Anzahl der Tage ist.
c) Sortiere die Zeilen absteigend nach Effizienz und speichere das neue
Dataframe als Textdatei (mit einem geeigneten Trennzeichen) und csv-
Datei ab. Verwende dabei den UTF-8-Zeichencodierungsstandard.
3. Die Datei Daten_EW.sav enthält Daten einer Umfrage unter Studierenden der
Ernährungswissenschaften. Folgende Variablen enthält der Datensatz:
Lies die Daten in R als Dataframe ein und bearbeite folgende Aufgaben:
33 Effizienz
• Laufzeiten messen.
Angenommen, wir wollten die Spaltensummen einer (großen) Matrix berechnen. Das
können wir mit for-Schleifen, apply() oder colSums() tun. Was glaubst du, welche
Variante die schnellste ist? Und wie stark unterscheiden sich die Laufzeiten dieser
Varianten?
Diese Frage beantworten wir in (33.1). Um die Effizienz eines Codes beurteilen zu
können, brauchen wir die Information, wie lange R benötigt, um einen bestimmten
Code auszuführen. Auch das schauen wir uns gleich zu Beginn in (33.1) an.
Anschließend werden wir in (33.2) bis (33.6) unser Wissen in diversen Beispielen
vertiefen und einige Regeln aufstellen, deren Einhaltung im Allgemeinen zu schnel-
leren Programmabläufen führt. Das Potenzial ist jedenfalls enorm, wie wir sehen
werden!
Die Palette reicht von der Bestimmung von Zeilenminima bis hin zur Ermittlung
von erwarteten Flächeninhalten von Dreiecken. Wir implementieren die Beispiele auf
mehrere Arten und vergleichen die Laufzeiten. Dabei merken wir, dass die Laufzeit
tendenziell mit jedem Versuch sinkt.
Schnelle Lösungen sind aber meistens auch etwas komplizierter.
Mit Hilfe der Funktion Sys.time() aus (27.1.1) können wir die Zeit unmittelbar vor
bzw. nach der Ausführung eines Codes bestimmen und die Differenz berechnen.
Beispiel: Generiere eine 100 × 107 Matrix mit beliebigen Zahlen. Berechne die
Spaltenmittelwerte mit den folgenden drei Varianten:
> {
+ zeit1 <- Sys.time()
+ apply(matrix, 2, mean)
+ zeit2 <- Sys.time()
+ }
Wichtig ist, dass wir die Anweisungen in einen Anweisungsblock packen, damit der
Code als Ganzes ausgeführt wird und somit die Laufzeit sauber gemessen wird.
Variante 2a: for-Schleife (mit NULL-Initialisierung)
> {
+ zeit1 <- Sys.time()
+ z <- NULL
+ for (j in 1:ncol(matrix)) {
+ z[j] <- mean(matrix[, j])
+ }
+ zeit2 <- Sys.time()
+ }
> {
+ zeit1 <- Sys.time()
+ z <- numeric(k) # besser als NULL!
+ for (j in 1:ncol(matrix)) {
+ z[j] <- mean(matrix[, j])
+ }
+ zeit2 <- Sys.time()
+ }
Wir können die Laufzeit schon alleine dadurch senken, wenn wir den Ergebnisvektor
z sinnvoll initialisieren. Die NULL-Initialisierung in Variante 2a führt dazu, dass mit
jeder Iteration der Ergebnisvektor verlängert werden muss.
482 H Effizienz und Simulation
Diese Verlängerungen können wir R ersparen! Denn in diesem Fall wissen wir, dass
am Ende k numerische Spaltenmittelwerte berechnet werden.
Wenn du im Vorhinein schon weißt, wie groß dein Objekt wird und von welchem
Typ es ist, dann nutze diese Information bei der Initialisierung! Verzichte nach
Möglichkeit auf NULL-Initialisierungen!
> {
+ zeit1 <- Sys.time()
+ colMeans(matrix)
+ zeit2 <- Sys.time()
+ }
Diese Variante der Laufzeitmessung wird auch Wall clock time genannt. Wir schauen
auf die Uhr und stoppen die Zeit zu Beginn und zu Ende der Codeausführung.
Das Problem dabei: Hintergrundprozesse können das Ergebnis verfälschen. Daher
schauen wir uns im nächsten Unterabschnitt eine ausgefeiltere Methode an.
Alternativ und bequem können wir Laufzeiten mit der Funktion system.time()
messen. Die Funktion misst die CPU-Zeit in Sekunden, die ein Prozess in Anspruch
nimmt.
> system.time(colMeans(matrix))
User System verstrichen
0.09 0.00 0.09
> system.time(apply(matrix, 2, mean))
User System verstrichen
76.24 0.17 76.40
Die gesamte verstrichene Laufzeit wird dabei aufgeteilt in einen vom Benutzenden
verursachten und einen vom System verursachten Teil.
Um die Schleifenvarianten mit system.time() zu messen, empfiehlt es sich, eine
eigene Funktion zu definieren. Das überlassen wir dir als kleine Übung.
Um zuverlässige Aussagen über das Laufzeitverhalten zu erhalten, ist es notwendig,
die Laufzeiten mehrmals zu messen. Wir kommen in (33.3) darauf zurück.
33 Effizienz 483
Das folgende Beispiel wirkt zwar harmlos, trotzdem hat es schon viel zu bieten! Sei
X eine Zufallsvariable mit folgender Dichtefunktion:
0.6
0.4
0.2
0.0
Ziel: Schreibe eine Funktion, die möglichst effizient die Dichtefunktion für gegebene
x auswertet. Kontrolliere deine Funktion(en) mit folgenden ausgewählten x-Werten.
> ddreieck1(x)
[1] 1
Dieser Versuch scheitert kläglich! Das Problem liegt darin, dass der Vektor x als
Ganzes in die Maximumfunktion gesteckt wird. Die Funktion max() sucht sich nun
aus allen Werten den größten heraus und gibt diesen zurück. Eigentlich sollte aber
für jeden Eintrag xi von x das Maximum von 1 − xi und 0 gebildet werden.
484 H Effizienz und Simulation
Variante 2: sapply()
> ddreieck2(x)
[1] 0.0 0.0 0.5 1.0 0.5 0.0 0.0
Nun funktioniert das Ganze so, wie wir es wollten! Innerhalb von sapply() haben
wir eine anonyme Funktion mit Parameter z definiert. Die Elemente von x werden
der Reihe entnommen und an z und weiter max() übergeben.
Variante 3: for-Schleife
Das geht natürlich auch mit einer Schleife.
> ddreieck3(x)
[1] 0.0 0.0 0.5 1.0 0.5 0.0 0.0
Beachte, dass wir vor der for-Schleife den Ergebnisvektor sinnvoll initialisiert haben
(gemäß Regel 17 auf Seite 482).
Variante 4: Transformation und ifelse()
Es geht schneller mit Vektorarithmetik (Benützung von vektorwertigen Funktio-
nen)! Wir schreiben hierfür die Dichtefunktion wie folgt um:
( (
1 − |x| falls −1 ≤ x ≤ 1 1 − |x| falls 1 − |x| ≥ 0
f (x) = =
0 sonst 0 sonst
Die Funktion ddreieck4() arbeitet wesentlich schneller als ddreieck2() und auch
ddreieck3().
Variante 5: pmax()
Mit der Funktion pmax(), die das parallele Maximum mehrerer Vektoren berechnet,
geht es sogar noch schneller.
33.3 Laufzeitvergleiche
Wir führen einen Laufzeitvergleich aller vier korrekt arbeitenden Funktionen aus
(33.2) durch. Dabei schauen wir uns auch unterschiedliche Eingabelängen von x an.
Der R-Code dazu:
Interessant ist, dass wir der anonymen Funktion im letzten sapply() die Funktion
fun übergeben. sapply() übergibt der anonymen Funktion der Reihe nach jede
Funktion aus funktionen. Die Funktion fun können wir sodann normal verwenden.
486 H Effizienz und Simulation
> erg
ddreieck2() ddreieck3() ddreieck4() ddreieck5()
k = 10000 0.01 0.02 0.01 0.00
k = 100000 0.11 0.05 0.01 0.00
k = 1000000 1.61 0.45 0.03 0.00
k = 10000000 28.30 4.55 0.30 0.07
> options(optalt) # Optionen zurücksetzen
Mit der Funktion array() erzeugen wir ein Array Zeit mit drei Dimensionen. Die
ersten beiden enthalten die Problemgrößen laengen und die Funktionen funktionen.
Über diese beiden Dimensionen wollen wir am Ende aggregieren, also die Mittelwer-
te über alle Versuche bilden. Daher ist es sinnvoll, dass die N Versuche in der dritten
Dimension verwaltet werden.
Wir befüllen also unser Array Zeit mit den Laufzeitergebnissen. Jetzt bilden wir für
jede Kombination aus Problemgröße und Funktion den Mittelwert aller N Versuche.
Der Laufzeitunterschied ist für große k enorm, was sich zum Beispiel bei Simulationen
mit mehreren Millionen Berechnungen sehr stark auswirkt. Probiere es einmal aus
für k = 100.000.000. Wir hoffen, dir wird nicht langweilig beim Warten.
Je größer k, desto ineffizienter wird die sapply()-Variante ddreieck2() im Vergleich
zur Schleifenvariante ddreieck3(). Diese Beobachtung lässt sich auch in anderen
Anwendungen anstellen.
Die Funktion pmax() kann weniger als ifelse(). Das, was sie aber kann, kann sie
schnell. Auch diese Beobachtung lässt sich allgemein häufiger anstellen.
Schleifen sind in R generell langsam. Allerdings sind sie nicht ganz so schlecht, wie
ihr Ruf ist. Es kommt darauf an, dass wir Schleifen sinnvoll einsetzen.
Beispiel: Bestimme für eine n × k Matrix (n >> k, also n deutlich größer als k)
die Zeilenminima auf effiziente Weise.
Variante 1: apply()
Dauert lange... Wir gehen die Matrix M zeilenweise durch (MARGIN = 1) und wenden
die Funktion min() an. Wir erhalten also die Zeilenminima.
Variante 2: for-Schleife über die Zeilen
Ebenfalls langsam. Die grundlegende Idee ist die gleiche wie oben.
Variante 3: for-Schleife über die Spalten
Jetzt zu einer merklich schnelleren Variante! Schleifen sind in R generell langsam.
Gleiches gilt auch für schleifenähnliche Funktionen wie etwa sapply() und apply().
Aber es gibt einen coolen Trick, den wir auch gleich als Regel auffassen.
Die Varianten 1 und 2 sind deshalb so langsam, weil wir die Matrix zeilenweise
durchgehen. Bei 107 Zeilen muss R also 10.000.000 Mal die Schleife ausführen.
Frage: Wie wäre es, wenn die Schleife die Matrix nur 10 Mal (Anzahl der Spalten)
durchlaufen müsste? Und geht das überhaupt? Die Antworten:
Die Idee: Wir nehmen zunächst an, dass die 1. Spalte die Zeilenminima enthält
und weisen die Zahlen res zu. Dann betrachten wir die 2. Spalte und weisen res
das parallele Minimum von res (den bis dato beobachteten Zeilenminima) und der
2. Spalte zu.
res enthält jetzt also für jede Zeile das Minimum der ersten beiden Spalten. Wir
versehen unsere Idee noch mit einem schönen for-Schleifchen, fertig!
33 Effizienz 489
Die Schleife muss nur ncol = 10 Mal ausgeführt werden, der Rest geschieht vek-
torwertig mit der Funktion pmin(). Ein genialer Trick, der für viele statistische
Simulationsanwendungen hervorragend eingesetzt werden kann.
Frage: Wie viele Würfe mit einem normalen 6-seitigen Spielwürfel brauchen wir im
Mittel, um eine Augensumme von k = 100 zu erreichen oder zu überschreiten?
Wir werden jetzt viele Varianten studieren. Tendenziell werden sie immer schneller.
Und auch immer komplexer! Ab Variante 3 wird es schwieriger und die Varianten
4 und 5 sind schon Hardcore. Wenn du diese beiden Varianten nicht (auf Anhieb)
verstehst, dann mach dir bitte keine Sorgen!
Variante 1: for-Schleife
Das ist wohl eine der ersten Varianten, die einem in den Sinn kommt. Der Code
ist in Abb. 33.2 dargestellt. Wir simulieren das Spiel n Mal in einer for-Schleife.
Die simulierten Resultate verwalten wir in einem Ergebnisvektor res. Die Variable
iter zählt die Anzahl der benötigten Würfelwürfe mit und summe speichert die
nach iter-vielen Würfelwürfen erreichte Augensumme. Beide müssen natürlich mit
0 initialisiert werden. Solange summe < k ist, würfeln wir weiter. Dies geschieht in
einer while-Schleife. Wir erhöhen in jeder Iteration iter um 1 und addieren zu
summe ein weiteres Wurfergebnis.
Beachte, dass wir mit res.sim1, dem wir das Ergebnis von sim1(n, k) zuweisen, ei-
ne Summary erstellen können, obwohl die Zuweisung innerhalb von system.time()
erfolgt. Weiters bietet es sich hier an, das Ergebnis unsichtbar (invisible()) zu-
rückzugeben. Grund: Es macht keinen Sinn, hunderttausende Zahlen auf die Console
zu drucken (so wir vergessen sollten, sie einem Objekt zuzuweisen).
Variante 1 ist sehr langsam, weil wir viele Iterationen haben. Zeit für diverse Be-
schleunigungsmaßnahmen!
Variante 2: sapply()
Wir ersetzen die for-Schleife durch sapply(). Dabei definieren wir eine temporäre
Funktion tempfun, die das Spiel einmal simuliert und fahren mit sapply() darüber.
Dabei übergeben wir der Funktion tempfun genau n Mal den Wert k.
Der Code ist in Abb. 33.3 dargestellt.
Kommen wir zur ersten bahnbrechenden Idee! Wir wissen, dass wir höchstens k Mal
würfeln müssen, um eine Augensumme von k zu erreichen. Das heißt, wir können
zuerst alle n×k Wurfergebnisse erzeugen und anschließend die Ergebnisse berechnen.
Eine n × k-Matrix Wurf ist Ausgangspunkt für unsere nächste Variante, die in Abb.
33.4 dargestellt ist.
Schon schneller! Wir erläutern die Funktionsweise, indem wir den Funktionsrumpf
von sim3() mit kleinem n und k ausführen – zum Beispiel mit n = 10 und k = 5.
Wir setzen die Zahlen dabei direkt ein, um das oben definierte n und k nicht zu
überschreiben.
> RNGversion("4.0.2")
> set.seed(116952)
492 H Effizienz und Simulation
Die orange markierten Zahlen markieren in jeder Spalte jene Zeile, in der die Au-
gensumme den Wert 5 erstmals erreicht oder überschreitet. Die Anzahl der Wür-
felwürfe bestimmen wir, indem wir zählen, wie lange die Augensumme kleiner als 5
war und den entscheidenden Wurf dazuzählen (+ 1). Jetzt brauchen wir nur noch
ein schleifenähnliches Konstrukt apply(), um die kumulierten Summen jedes Simu-
lationsdurchgangs zu berechnen.
33 Effizienz 493
Die Variante ist deutlich schneller als die bisher angeführten. Grund: Mussten wir
bisher im Schnitt ca. n·29 Schleifendurchläufe ausführen (in einem Schleifendurchlauf
brauchen wir im Mittel n/3.5 = 100/3.5 = 28.6 ≈ 29 Würfelwürfe), so benötigen wir
jetzt nur noch n Iterationen. Ansonsten machen wir lediglich von Vektorarithmetik
Gebrauch.
Folgende Verbesserungspotenziale hat diese Variante jedoch:
1. Die Anzahl der Iterationen ist immer noch an n gekoppelt.
2. Für große n und k kann der Arbeitsspeicher ggf. nicht ausreichen.
Wir entkoppeln in der nächsten Variante die Anzahl der Schleifendurchläufe von n.
Variante 4: Simultanes Simulieren mit while (schon recht anspruchsvoll)
Wir simulieren alle n Ergebnisse simultan (gleichzeitig) mit Vektorarithmetik. Dabei
basteln wir uns eine while-Schleife, die solange ausgeführt wird, bis alle Augensum-
men ≥ k sind. Anders formuliert: Solange noch mindestens ein Durchgang mit einer
Augensumme < k existiert, machen wir weiter.
Anschließend bestimmen wir die Durchgänge, die in der aktuellen Iteration die Au-
gensumme k erreicht oder überschritten haben. Dazu benötigen wir die beiden Be-
dingungen summe < k und summe >= k.
Wie, das erklären wir direkt im R-Code, den wir in Abb. 33.5 sehen.
Diese Variante ist deshalb so genial, weil wir jetzt nur noch höchstens k Iterationen
(im schlimmsten Fall würfeln wir lauter Einsen) benötigen und per Annahme k < n
gilt. Eine erhebliche Einsparung!
Frage: Geht es noch schneller?
Frage: Wie wäre es, wenn wir jede Iteration auch dafür nützen, um unseren Vektor
summe dahingehend auszumisten?
494 H Effizienz und Simulation
Dazu schreiben wir Variante 4 wie folgt um: summe enthält nach jeder Iteration nur
noch jene Durchgänge, die noch weiterer Würfelwürfe bedürfen. Die fertig geworde-
nen Durchgänge schmeißen wir in jeder Iteration raus.
Wir lassen die while-Schleife laufen, solange es noch Durchgänge gibt, die eine zu
kleine Augensumme haben, also solange der Vektor summe nicht leer ist.
Weitere Details erläutern wir wieder direkt im R-Code in Abb. 33.6.
> sim5 <- function(n, k) {
+
+ res <- numeric(n) # Initialisiere Ergebnisvektor
+ summe <- rep(0, n) # Vektor der Augensummen
+ iter <- 0 # Iterationszähler: Anzahl der Würfelwürfe
+
+ anz.fertig <- 0 # Anzahl der fertig simulierten Durchgänge
+
+ while(length(summe) > 0) {
+ # Solange summe Elemente enthält, gibt es noch Durchgänge, die
+ # weiterer Würfe bedürfen.
+
+ # Einen weiteren Würfelwurf hinzuaddieren
+ iter <- iter + 1
+ summe <- summe + sample(1:6, size = length(summe), replace = TRUE)
+
+ # Elemente bestimmen, die (in dieser Iteration) fertig geworden sind.
+ bool <- summe >= k
+ # Anzahl der Durchgänge, die gerade fertig geworden sind.
+ m <- sum(bool)
+
+ if (m > 0) {
+ summe <- summe[!bool]
+
+ # anz.fertig ist die Anzahl der fertig simulierten Durchgänge.
+ # Also fangen wir beim folgenden Eintrag von res an und befüllen
+ # m Einträge mit iter.
+ res[(anz.fertig + 1):(anz.fertig + m)] <- iter
+
+ # m Durchgänge fertig simuliert - zu anz.fertig dazuzählen
+ anz.fertig <- anz.fertig + m
+ }
+ }
+
+ return(invisible(res))
+ }
Jetzt brauchen wir nicht nur höchstens k Iterationen, sondern sind in den einzelnen
Iterationen auch noch tendenziell schneller, da der Vektor summe kleiner wird.
Fazit
Wir schauen uns die Laufzeiten in einer Übersicht nochmal an. Dabei besprechen
wir ein spannendes Konstrukt.
> str <- paste0("c(", paste0("time", 1:5, "[3]", collapse = ", "), ")")
> str
[1] "c(time1[3], time2[3], time3[3], time4[3], time5[3])"
Die Funktion parse() wandelt den Inhalt von Dateien (Parameter file) oder Zei-
chenketten (Parameter text) in eine expression um. Expressions sind quasi ein
Mittelding aus Strings und Funktionen und werden von eval() auswertet. In unse-
rem Fall werden alle fünf gemessenen Zeiten zu einem Vektor verkettet.
Expressions werden wir in diesem Buch nicht mehr betrachten. Interessierte seien
unter anderem auf die R-Hilfen zu diesen obigen Funktionen verwiesen.
Gerne darfst du an dieser Stelle nochmal die ersten Varianten betrachten. Sie werden
dir jetzt ultralahm vorkommen. Und nachdem du jetzt so viele tolle Tricks kennen-
gelernt hast, wirst du kaum noch Lust verspüren, länger als notwendig auf dein
Ergebnis zu warten ;-)
Ein Beispiel zur Abrundung: Die beiden Zufallsvektoren (X1 , Y1 ) und (X2 , Y2 ) seien
unabhängig und identisch gleichverteilt auf [0, 1] × [0, 1]. Sei A der Flächeninhalt des
von (0,0) und den beiden Vektoren aufgespannten Dreiecks. In Abb. 33.7 sind drei
solche zufälligen Dreiecke abgebildet.
Aufgabe: Simuliere E(A) und V ar(A), also den Erwartungswert und die Varianz
von A.
1.0
1.0
1.0
0.8
0.8
0.8
0.6
0.6
0.6
0.4
0.4
0.4
0.2
0.2
0.2
0.0
0.0
0.0
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
Wir können die Fläche des Dreiecks mit Hilfe der Determinante berechnen. Unsere
folgende Überlegung wird von Abb. 33.8 visuell begleitet.
Das grüne Dreieck mit den Eckpunkten ( xy11 ) = ( 0.6 x1 0.2
0.2 ) und ( y1 ) = ( 0.4 ) wirdum
das blaue Dreieck erweitert. Der rechte obere Eckpunkt ist ( xy33 ) = xy11 +x+y2
2
=
0.6+0.2
0.8 0.6 0.2
0.2+0.4 = ( 0.6 ). Der Absolutbetrag der Determinante von ( 0.2 0.4 ) berechnet die
Fläche des Parallelogramms. Die Hälfte davon ist die gesuchte Fläche des grünen
Dreiecks.
Man kann zeigen (nach langer Rechnung und Auswertung eines Vierfachintegrals):
13 229
E(A) = 108 und V ar(A) = 23328 .
Variante 1: Fläche n Mal berechnen
Wir schreiben eine Funktion flaeche(), die uns die Fläche eines zufälligen Dreiecks
liefert. Anschließend befüllen wir in einer for-Schleife einen Ergebnisvektor res.
1.0
0.8
x3
0.6
y3
x2
y2
0.4
0.2
x1
y1
0.0
Abbildung 33.8: Das grüne Dreieck alleine und gemeinsam mit dem blauen Drei-
eck als Parallelogramm
Gute Nacht! Du kannst mit der Esc-Taste links oben abbrechen... Sind die Ergebnisse
wenigstens gut?
Die simulierten Ergebnisse kommen schon ziemlich nahe an die exakten Ergebnisse
heran.
Variante 2: Flächen simultan simulieren
Wir verwenden einen einfachen Trick aus der Mathematiktrickkiste. Die Determi-
nante einer 2 × 2-Matrix lässt sich mit der „Schlaufenregel“ berechnen.
x x2
1
det = x1 · y2 − y1 · x2 (33.1)
y1 y2
Stellen wir uns das Ganze als Vektor vor, dann stoßen wir auf eine effiziente Variante!
33 Effizienz 499
Wir schreiben also eine Funktion flaeche1(), mit der wir nicht nur den Flächen-
inhalt eines zufälligen Dreiecks, sondern von n Dreiecken generieren können. Und
zwar mit Vektorarithmetik!
33.7 Monte-Carlo-Simulation
Die Beispiele (33.6) und (33.5) liefern Beispiele für Monte-Carlo-Simulation. Dabei
geht es darum, analytisch kaum oder schwierig zu bewältigende Problemstellun-
gen mit Hilfe von Zufallsexperimenten näherungsweise zu lösen. Monte-
Carlo-Simulation beruht im Wesentlichen auf dem Gesetz der großen Zahlen.
Schauen wir uns die Grundidee anhand des Dreiecksflächenbeispiels (33.6) an. Sei
A die Zufallsvariable, welche die Fläche (genauer: den Flächeninhalt) eines Zufalls-
dreiecks modelliert. Seien a1 , a2 , . . . an Realisationen der Zufallsvariable A, also eine
Zufallsstichprobe über die Menge aller möglichen Flächeninhalte. Dann gilt:
n
1X
ai −→ E(A)
n i=1 n→∞
Das heißt, dass der Mittelwert der Stichprobe gegen den Erwartungswert konver-
giert, wenn n gegen Unendlich strebt. Daraus folgt natürlich, dass unser simuliertes
Ergebnis umso präziser wird, je größer die Stichprobe ist.
500 H Effizienz und Simulation
33.8 Abschluss
33.8.2 Ausblick
Wenn wir komplexe Sachverhalte simulieren, dann wollen wir die Ergebnisse ger-
ne auch visualisieren. In (34), (35) und (36) widmen wir uns der Grafikerstellung.
Nach diesen Kapiteln kannst du unter anderem die Ergebnisse dieses Kapitels mit
Histogrammen und Boxplots darstellen.
R ist eine Interpretersprache, das heißt, der Programmcode wird erst zur Laufzeit
in maschinenverständlichen Code übersetzt. Im Gegensatz dazu übersetzen (kompi-
lieren) Compilersprachen wie C++ den Code bereits vor der Programmausführung.
Im Zuge der Kompilierung wird der Code auch optimiert, sodass er vom Computer
schneller abgearbeitet werden kann.
Die Codeoptimierung zahlt sich vor allem bei zeitkritischen Programmen wie Simu-
lationen aus, die viele Schleifen und Programmsprünge haben. Interpretersprachen
wie R fehlt dabei die Möglichkeit, optimierend einzugreifen.
33 Effizienz 501
33.8.3 Übungen
1. Wir wollen für jede Zeile der folgenden Matrix M die Summe der quadrierten
Werte berechnen.
Code A Code B
> res <- NULL > res <- apply(M, 1, function(x) {
> i <- 0 + return(sum(x^2))
> while (i < nrow(M)) { + })
+ i <- i + 1
+ res <- c(res, sum(M[i, ]^2))
+ }
Code C Code D
> res <- rep(0, nrow(M)) > res <- rep(0, nrow(M))
> for (i in 1:nrow(M)) { > for (j in 1:ncol(M)) {
+ res[i] <- sum(M[i, ]^2) + res <- res + M[, j]^2
+ } + }
2. Betrachte den zellulären Automaten aus (28.4.2) ab Seite 393. Der Code aus
Abb. 28.1 ist leider an sehr vielen Stellen ineffizient. Schreibe den Code derart
um, dass er schneller läuft.
502 H Effizienz und Simulation
3. Wir würfeln n Mal mit einem Spielwürfel und erzeugen den Vektor x:
n <- 10^6
x <- sample(1:6, size = n, replace = TRUE)
for (i in 1:length(x)) {
mittelwert <- c(mittelwert, mean(x[1:i]))
}
Es dauert sehr sehr lange, bis die Lösung ermittelt wird. Wenn dir lang-
weilig wird, darfst du gerne mit ESC die Berechnung abbrechen.
Finde eine merklich schnellere Möglichkeit, die kumulierten Mittelwerte
zu berechnen. Die effizienteste Lösung benötigt nur wenige Millisekunden.
b) Wir wollen die kumulierten Varianzen berechnen, also einen Vektor
k
T 1 X 2
(v1 , v2 , . . . , vn ) wobei vk := (xi − x̄k ) .
k − 1 i=1
4. Wir erstellen eine n×k-Matrix W mit Würfelwürfen, wobei k deutlich kleiner als
n ist. Unser Ziel: Wir wollen jede Zeile von W aufsteigend sortieren. Folgender
Code existiert:
n <- 10^6
k <- 5
Das dauert sehr lange. Wir wollen eine schnellere Lösung finden. Ein Kollege
schlägt hierfür folgende Variante vor:
W.sort <- W
5. Wir studieren in diesem Beispiel das Sammelbildproblem. Gegeben ist ein Sti-
ckeralbum, für das wir m unterschiedliche Bilder sammeln müssen (bzw. wol-
len). Zu Beginn haben wir noch kein Bild eingeklebt, uns fehlen also am Anfang
alle m Sticker. Wenn uns zu irgendeinem Zeitpunkt noch k < m Bilder fehlen,
so beträgt die Wahrscheinlichkeit, dass das nächste gekaufte Bild kein Duplikat
ist, k/m.
Frage: Wie viele Bilder müssen wir im Mittel kaufen, um das Sammelalbum
zu vervollständigen?
Schreibe einen Code, der diese Frage mit Hilfe einer Simulation möglichst effizi-
ent beantwortet. Die Anzahl der Bilder m und die Anzahl der Simulationsläufe
n soll dabei einstellbar sein.
Hinweis: Überprüfe deine Ergebnisse auf Plausibilität. Das exakte Ergebnis
(der Erwartungswert) lässt sich mit folgender Formel berechnen:
m
X m
k
k=1
504 H Effizienz und Simulation
Wir wollen also für jede Komponente von x die Summe des linken und rech-
ten Nachbarn (so vorhanden) sowie des Elements selbst bestimmen und auf y
zuweisen. Ein Kollege schlägt folgenden Code zur Lösung dieser Aufgabe vor:
> # Beispielvektor
> x <- c(3, 6, -8, 2, 4, -1)
> x
[1] 3 6 -8 2 4 -1
> y <- x
> for (i in 1:length(x)) {
+ if (i == 1) {
+ y[i] <- sum(x[i:(i+1)])
+ }
+ else if (i == length(x)) {
+ y[i] <- sum(x[(i - 1):i])
+ }
+ else {
+ y[i] <- sum(x[(i-1):(i+1)])
+ }
+ }
> y
[1] 9 1 0 -2 5 3
Der Code funktioniert zwar korrekt, ist aber leider sehr ineffizient. Finde eine
vektorwertige Möglichkeit, den Vektor y zu berechnen.
I Visualisierung von Daten
Daniel Obszelka
506 I Visualisierung von Daten
Uns begleitet durch weite Teile dieses Kapitels der berühmte Iris-Datensatz, bei dem
Blüten von Iris-Blumen vermessen wurden. In Infobox 13 finden wir relevante Hin-
tergrundinformationen zu diesem Datensatz. Mit diesen Daten erstellen wir unsere
erste Grafik und dekorieren sie fortlaufend. Bringen wir also im wahrsten Sinne des
Wortes unsere Grafiken zum Erblühen!
Bevor es losgeht, führen wir in (34.1) die Grafikfunktion plot() ein, die von zentra-
ler Bedeutung ist. Wir machen Bekanntschaft mit den ersten elementaren Grafikein-
stellungen und setzen unsere Entdeckungsreise in (34.2) fort. In (34.3) lernen wir,
wie wir Elemente einer Grafik hinzufügen können. Die Palette reicht von Punkten,
Linien, Texten und Legenden bis hin zu Beschriftungen.
Mit Hilfe der Techniken dieses Kapitels ist es sogar schon möglich, Funktion zu
schreiben, die Grafiken nach individuellen Wünschen erzeugt!
Wenn wir Daten visuell darstellen, sprechen wir oft davon, die Daten zu plotten.
Die Funktion plot() ist die vielseitigste Grafikfunktion, die als Basis für viele Gra-
fiken dient. Sie ist im Paket graphics definiert, das noch viele weitere nützliche
Grafikfunktionen enthält. Es lohnt sich definitiv, die Hilfe zu diesem Paket anzuse-
hen (?help(package = graphics)). Der stark vereinfachte (!) Funktionsaufruf von
plot():
Die Funktion plot() ist eine generische Funktion, das heißt, sie passt ihre Funktio-
nalität an die Klasse des übergebenen Objekts an (vgl. (26)). Die hier dargestellte
Variante ist plot.default() (siehe ?plot.default).
Parameter Bedeutung
x, y x- und y-Koordinaten
type Wie werden die Punkte x und y dargestellt?
"p": points (Streudiagramm – Standardwert)
"l": lines (Liniengrafiken)
"b": both (Punkte werden mit Linien verbunden)
"n": none (leerer Plot)
xlim bzw. ylim Zeichnungsbereich der x-Achse bzw. y-Achse als Intervall
(Vektor der Länge 2). Bei NULL wird das Intervall aus den
Daten geschätzt.
main Hauptüberschrift (oben in der Grafik)
sub Subüberschrift (unten in der Grafik)
xlab bzw. ylab Beschriftung der x-Achse bzw. y-Achse
... Weitere Grafikparameter
508 I Visualisierung von Daten
Mit den folgenden Codebeispielen bekommen wir ein erstes Gefühl für plot() und
die oben erwähnten Parameter. In (34.1) erweitern wir dann unsere Parametersamm-
lung. Definieren wir uns zunächst ein paar Beispielpunkte.
Lassen wir type unspezifiziert, so wird ein Streudiagramm für x und y gezeichnet.
Mit den Einstellungen type = "l" und type = "b" werden Liniengrafiken gezeich-
net, wobei in zweiterem Fall noch Punkte mit eingezeichnet werden. Mit dem Para-
meter main stellen wir die Überschrift ein.
Punktegrafik Liniengrafik
5
5
4
4
3
3
y
y
2
2
1
1 2 3 4 5 1 2 3 4 5
x x
5
4
4
3
3
y
y
2
2
1
1 2 3 4 5 1 2 3 4 5
x x
34 Einführung in die 2D-Grafik 509
Bei type = "n" werden die Daten nicht gezeichnet. Wenn du dich fragen solltest,
welchen Sinn das macht, dann gedulde dich bitte noch bis (34.3.4). Da wir keine Be-
schriftungen für die x-Achse und y-Achse übergeben haben, zieht R die Objektnamen
x und y heran.
Treffengrafik S Treppengrafik s
5
5
4
4
3
3
y
y
2
2
1
1 2 3 4 5 1 2 3 4 5
x x
Variante S Variante s
Beachte bei Treppengrafiken den Unterschied zwischen der Groß- und Kleinschrei-
bung "S" bzw. "s": Beim Erreichen des nächsten x-Wertes wird bei "S" zum y-Wert
des nächsten Punktepaares gesprungen, während bei "s" zum y-Wert des aktuel-
len Punktepaares gesprungen wird. Mit sub vergeben wir eine Subüberschrift, die
ganz unten eingeblendet wird.
Stabdiagramm
5
4
3
y
2
1
1 2 3 4 5
x
510 I Visualisierung von Daten
Mit type = "h" erstellen wir Stabdiagramme. Bei der nächsten Grafik überge-
ben wir darüber hinaus für xlab bzw. ylab die gewünschten Achsenbeschriftungen.
Außerdem verändern wir mittels xlim = c(0, max(x)) den Zeichenbereich entlang
der x-Achse: Er soll bei 0 beginnen und bis max(x) laufen.
Damit die Stäbe die richtigen Proportionen haben, wollen wir die y-Achse bei 0
beginnen lassen. Außerdem wollen wir oben einen kleinen Rand einfügen. Das be-
werkstelligen wir mit ylim = c(0, max(y) + 1).
Stabdiagramm
6
5
4
Häufigkeiten
3
2
1
0
0 1 2 3 4 5
Anzahl
Wir können der Funktion plot() aber auch eine n × 2-Matrix übergeben. In dem
Fall enthält die erste Spalte die x-Werte und die zweite Spalte die y-Werte.
x y
[1,] 1 2
y−Achse = 2. Spalte
[2,] 2 5
[3,] 3 4
3
[4,] 4 1
[5,] 5 3
2
> plot(M,
+ main = "Grafik aus Matrix", 1 2 3 4 5
Zeit für unser erstes größeres Beispiel und den ersten Einsatz unserer Iris-Daten aus
der Kapiteleinleitung auf Seite 506!
Beispiel: Zeichne mit den beiden Variablen Petal.Length und Petal.Width von
iris ein Streudiagramm. Wähle sinnvolle Beschriftungen (also kurze und sprechende
Bezeichnungen) für die Achsen und eine passende Überschrift.
1.5
1.0
0.5
1 2 3 4 5 6 7
Petal Length
Die Grafik bietet uns einen guten Überblick über die (bivariate) Verteilung, hat
aber noch Verbesserungspotenzial! Wie wäre es, wenn wir die drei Spezies sowohl
farblich als auch in der Form der Punkte unterscheiden könnten? Super? Dann ist
der nächste Unterabschnitt perfekt anmoderiert ;-)
Wir haben in (34.1) bereits erste Grafiken erstellt, mit dem Parameter type experi-
mentiert und Achsen beschriftet.
Einige Frage kommen auf, zum Beispiel:
Die Antwort auf diese Fragen lautet: Weil es in par() so definiert ist. In par() (plot
arguments) werden Parameter verwaltet, die das Aussehen der Grafiken steuern.
Die Hilfe zu all diesen Parametern können wir mit ?par aufrufen.
Üblicherweise übernehmen die Grafikfunktionen wie etwa plot() die Grafikparame-
ter von par(), sofern sie nicht in der Funktion überschrieben werden (auch mit dem
Dreipunktargument ...). Die derzeitigen Einstellungen rufen wir mit par() auf.
Es handelt sich um eine Liste mit 72 Einträgen (in R-Version 4.0.2). Ein Auszug:
In Tab. 34.2 verschaffen wir uns einen Überblick über einige wichtige Grafikparame-
ter. All diese Parameter können wir auch in plot() verwenden.
Parameter Bedeutung
cex Skalierung von Punkten und Texten (character expansion)
cex.axis Gibt die relative Größe der Achsenlabels bezüglich cex an.
cex.lab Dasselbe für die Achsenbeschriftungen
cex.main Dasselbe für die Hauptüberschrift
cex.sub Dasselbe für die Subüberschrift
col Steuert die Farbe. Wie bei cex gibt es 4 spezielle Parameter:
col.axis, col.lab, col.main, col.sub
lty Linienart (line type)
lwd Linienstärke (line width)
pch Steuert das Aussehen der Punkte (point character)
Auszugsweise schauen wir uns im Folgenden einige dieser Parameter an. Wir ver-
wenden dieselben x- und y-Werte wie im vorangehenden Abschnitt:
Bei cex (character expansion) handelt es sich um eine Zahl, welche die relative
Größe von Punkten und Texten angibt. Den Wert cex = 1 bezeichnen wir als Nor-
malgröße (100%). Ein Wert von cex = 2 bedeutet, dass Punkte und Texte doppelt
so groß dargestellt werden wie normal. Für cex = 0.25 haben Punkte und Texte
ein Viertel der Normalgröße usw.
cex ist ein Basiswert. Daneben gibt es vier spezielle cex-Werte für die Achsen und
Überschriften: cex.axis, cex.lab, cex.main, cex.sub. Diese steuern die relative
Größe bezogen auf den Basiswert cex.
Der Parameter cex ist auf den Standardwert 1 eingestellt, der Parameter cex.main
auf den Standardwert 1.2. Normale Punkte und Texte werden also in Normalgröße
dargestellt, Hauptüberschriften um 20% größer als normale Punkte und Texte.
Setzen wir jetzt den Basiswert cex = 2, dann werden alle Punkte und Texte doppelt
so groß dargestellt wie normal. Hauptüberschriften sind dann darüber hinaus um
20% größer, werden also bezogen auf die Normalgröße um den Faktor 2 · 1.2 = 2.4
vergrößert.
Wichtig zu erwähnen ist, dass sich hier cex.lab (wie auch cex.main, cex.axis und
cex.sub) am cex-Wert orientiert, der in par() gespeichert ist. Wird cex in der
Funktion plot() definiert, so wird weiterhin der par()-Wert herangezogen.
Klingt verwirrend, betrachten wir daher ein Beispiel ;-)
Die Hauptüberschriften sind in beiden Grafiken gleich groß. Links ergibt sich der
Skalierungsfaktor gemäß 2 · 0.6 = 1.2, rechts gemäß 1 · 1.2 = 1.2.
514 I Visualisierung von Daten
Maintext
Maintext
5
4
5
y
3
y
1
2
1 2 3 4 5
1
x 1 2 3 4 5
Auch die Zahlen entlang der Achsen sind in beiden Grafiken gleich groß. Links
ergibt sich der Skalierungsfaktor gemäß 2 · 0.5 = 1, rechts gemäß 1 · 1 = 1. Da wir in
der rechten Grafik cex.axis nicht spezifiziert haben, wird der Wert par()$cex.axis
herangezogen, der standardmäßig 1 ist.
Die Achsenbeschriftungen der linken Grafik sind doppelt so groß wie in der rech-
ten Grafik. Grund: In beiden Grafiken wird für cex.lab der Wert par()$cex.axis
herangezogen, der standardmäßig 1 beträgt, aber par()$cex ist in der linken Grafik
doppelt so groß.
Beachte, dass cex = 2 in der rechten Grafik lediglich für die Größe der Punkte
herangezogen wird, nicht aber für die Bestimmung der Skalierungsfaktoren. Damit
ist auch geklärt, warum die Punkte in beiden Grafiken gleich groß sind.
Der Parameter col steuert die Farbe der Punkte. Es gibt viele Möglichkeiten, wie
col spezifiziert werden kann. In den beiden einfachsten Varianten übergeben wir
entweder eine Zahl oder einen Farbnamen.
Wenn wir nur wenige Farben benötigen und keine allzu hohen Ansprüche haben,
bieten sich Zahlen an. Die Zahlen von 1 bis 8 stehen dabei für die folgenden Farben:
1 2 3 4 5 6 7 8
Das Aussehen der Punkte können wir unter anderem mit Nummerncodes steuern.
Wir bilden die ersten 25 Punktformen ab.
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
21 22 23 24 25
Die Parameter cex, col und pch können wir auch vektorwertig einsetzen.
> # cex, col vektorwertig definieren > # cex, col für alle Punkte gleich
> # pch mit Nummerncodes definieren > # pch mit Zeichen definieren
> plot(x, y, cex = 1:5, col = 1:5, > plot(x, y, cex = 2, col = 4,
+ pch = 1:5) + pch = c("A", "B", "C", "+", "@"))
B
5
C
4
@
3
3
y
A
2
+
1
1 2 3 4 5 1 2 3 4 5
x x
Die beiden Grafiken sehen nicht sehr ansehnlich aus, da die größeren Symbole über
die Ränder der Plots hinausragen. plot() ändert die Zeichnungsbereiche der x- und
y-Achse nicht automatisch in Abhängigkeit der Symbolgrößen. Das müssen wir per
Hand mit den Parametern xlim und ylim einstellen.
1 Aufgerufen am 26.09.2019
516 I Visualisierung von Daten
Den Linientyp (lty – line type) können wir mit Nummerncodes oder Namen defi-
nieren. Im Folgenden sind die Einstellungsmöglichkeiten aufgelistet (Defaultwert ist
lty = 1):
0 = blank
1 = solid
2 = dashed
3 = dotted
4 = dotdash
5 = longdash
6 = twodash
Die Linienstärke (lwd – line width) ist eine ganze Zahl, wobei lwd = 1 die Stan-
dardeinstellung ist.
Die Hintergrundfarbe können wir mit bg (background) umstellen. Das ist auch
deshalb spannend, weil wir bei der Gelegenheit eine neue „Farbe“ kennenlernen!
> par()$bg
[1] "transparent"
Der Hintergrund ist also standardmäßig transparent, wie uns "transparent" mit-
teilt. Natürlich können wir auch andere Farben einstellen.
par() retourniert eine Liste mit den aktuellen Einstellungen unsichtbar mittels
invisible(). Das ist nützlich, wenn wir die geänderten par()-Einstellungen nur
vorübergehend benützen möchten. Das sieht schematisch so aus:
Mit unserem neuen Wissen können wir unser Irisbeispiel aus (34.1.2) fortsetzen.
Beispiel: Fortsetzung des Beispiels auf Seite 511. Stelle die Variablen Petal.Length
und Petal.Width in einem Streudiagramm dar. Die Spezies sollen sich dabei in der
Grafik voneinander unterscheiden. Sowohl farblich als auch in der Form der Punkte.
1.5
1.0
0.5
1 2 3 4 5 6 7
iris$Petal.Length
Zunächst wählen wir drei Farben nach unserem Geschmack aus, eine für jede Spezies.
Da iris$Species ein Faktor ist, funktioniert der Zugriff col.roh[iris$Species].
Wir selektieren also die Farben in der korrekten Reihenfolge, sodass sie zu den Spe-
zies passen. Dasselbe Prinzip auch bei pch.roh[iris$Species].
518 I Visualisierung von Daten
Wir können die Spezies voneinander unterscheiden, aber wir wissen nicht, welche
Punkte für welche Spezies steht. Ein Manko, das wir im nächsten Abschnitt ausbü-
geln werden ;-)
Wenn wir die Funktion plot() aufrufen, so stellen wir fest, dass die Grafik jedes Mal
neu gezeichnet wird. Wir schauen uns jetzt an, wie wir einer existierenden Grafik
Elemente hinzufügen können. Neu eingezeichnete Elemente überdecken dabei ggf.
bereits gezeichnete darunterliegende Elemente.
Hierbei funktionieren points() und lines() genauso wie plot(): Übergebe x- und
y-Koordinaten (bzw. eine n×2-Matrix) und spezifiziere bei Bedarf Grafikparameter.
points(x, y, type = "l") entspricht dabei lines(x, y).
Stark vereinfachte Funktionsaufrufe (nur ausgewählte Parameter sind angeführt) der
anderen beiden Funktionen:
Hierbei definieren wir mit labels bei text() bzw. text bei mtext() die Zeichen-
ketten, die eingezeichnet werden sollen.
Bei mtext() können wir zusätzlich mit side einstellen, auf welcher Seite die
Strings platziert werden sollen. Die folgenden Richtungsangaben finden auch bei
vielen anderen Grafikparametern Anwendung.
• 1 für unten
• 2 für links
• 3 für oben
• 4 für rechts
34 Einführung in die 2D-Grafik 519
Beispiel: Fortsetzung des Beispiels auf Seite 517. Zeichne die Mittelwertsvektoren
für die drei Gattungen gut erkennbar ein.
2.5
setosa
versicolor
2.0
2.0
virginica
iris$Petal.Width
iris$Petal.Width
1.5
1.5
1.0
1.0
0.5
0.5
1 2 3 4 5 6 7 1 2 3 4 5 6 7
iris$Petal.Length iris$Petal.Length
Im rechten Plot haben wir zusätzlich mit text() die Gattungen eingetragen, damit
wir uns auskennen, welche Farbe welcher Gattung entspricht. Das schaut natürlich
nicht schön aus. Mit Legenden beseitigen wir diese ästhetische Katastrophe.
Legenden sind für die Erklärung einer Grafik oft unerlässlich. Der stark vereinfachte
Funktionsaufruf der Funktion legend():
Parameter Bedeutung
x, y x- und y- Koordinaten der Legende. Für x können wir auch
Schlüsselwörter übergeben, zum Beispiel:
"bottomleft", "bottom", "center", "top", "topright" etc.
In diesem Fall wird y ignoriert.
legend Legendenbeschriftungen
bty box type. bty = "n" unterdrückt die Box um die Legende.
bg background (Hintergrundfarbe, vgl. (34.2.5))
pt.cex point character expansion. Größe der Punkte
xjust falls x und y spezifiziert wurden:
0: Linksausrichtung um x (x ist linker Endpunkt der Legende)
0.5: Legende ist horizontal zentriert um x
1: Rechtsausrichtung um x (x ist rechter Endpunkt der Legende)
yjust falls x und y spezifiziert wurden:
0: y ist unterer Endpunkt der Legende
0.5: Legende ist vertikal zentriert um y
1: y ist oberer Endpunkt der Legende
ncol Anzahl der Spalten. Praktisch, wenn viele Labels existieren.
title Überschrift für die Legende
inset relativer Einzug vom Rand gemessen an der Größe der Plotregion
(falls x mit Schlüsselwort spezifiziert wurde). Kann auch ein
zweielementiger Vektor sein (horizontaler Einzug, vertikaler
Einzug).
Beachte: Entweder pch, lwd oder lty müssen spezifiziert werden, sonst bleibt der
entsprechende Platzhalter für die Linien/Punkte leer.
Beispiel: Fortsetzung des Beispiels auf der vorherigen Seite. Zeichne eine hübsche
Legende ein, die uns darüber aufklärt, welche Punkte zu welcher Spezies gehören.
Die Legende soll oben links eingezeichnet werden. Die Legendenbeschriftungen bezie-
hen wir aus den Kategorien von Species. Für col und pch setzen wir die passenden
Vektoren col, col.roh, pch bzw. pch.roh von Seite 517 ein.
34 Einführung in die 2D-Grafik 521
Alternativ können wir die Legende auch rechts unten einzeichnen. Dabei berechnen
wir den rechten unteren Eckpunkt automatisiert aus den Daten. Die Legende richten
wir mit xjust = 1 und yjust = 0 korrekt aus.
2.5
setosa
versicolor
virginica
2.0
2.0
iris$Petal.Width
iris$Petal.Width
1.5
1.5
1.0
1.0
setosa
0.5
0.5
versicolor
virginica
1 2 3 4 5 6 7 1 2 3 4 5 6 7
iris$Petal.Length iris$Petal.Length
2.5
setosa Spezies
versicolor setosa
2.0
2.0
virginica versicolor
iris$Petal.Width
iris$Petal.Width
virginica
1.5
1.5
1.0
1.0
0.5
0.5
1 2 3 4 5 6 7 1 2 3 4 5 6 7
iris$Petal.Length iris$Petal.Length
522 I Visualisierung von Daten
Mit den Funktionen box(...) und grid(...) können wir Boxen und Gitter-
netzlinien einzeichnen. Beiden Funktionen können wir gängige Grafikargumente
(wie jene aus (34.2)) übergeben.
Manchmal möchten wir nicht die Achsen verwenden, die plot() standardmäßig er-
zeugt, sondern eigene Achsen definieren. Das ist mit der Funktion axis() mög-
lich. Davor müssen wir jedoch das Zeichnen der Standardachsen unterbinden,
was wir mit der Option axes = FALSE in plot() bewerkstelligen. In Tab. 34.4 bilden
wir ausgewählte Parameter der Funktion axis() ab.
Parameter Bedeutung
side Auf welcher Seite soll die Achse gezeichnet werden?
1 für unten, 2 für links, 3 für oben und 4 für rechts
at Wo sollen die Häkchen gemacht werden?
NULL: R berechnet günstige Stellen (mit der Funktion pretty())
labels Wie sollen die Häkchen beschriftet werden?
TRUE: R trägt die Häkchenbeschriftungen automatisch ein.
FALSE: keine Beschriftungen
Wir können auch Vektoren derselben Länge wie at übergeben.
tick TRUE: Die Häkchen werden eingezeichnet.
pos An welcher Position soll die Achse eingefügt werden?
Beispiel: Plotte die Tangens Hyperbolicus Funktion (tanh()) im Intervall [−2, 2].
Die Achsen sollen sich im Ursprung kreuzen. Zeichne auch ein Gitternetz ein.
Beachte: Eine Kurve besteht bei Computergrafiken immer aus einer Folge von
Geraden. Wenn wir hinreichend viele Geradenstücke zeichnen, dann ist die Illusion
einer Kurve perfekt. Dafür reichen 201 Bildpunkte in der Regel aus.
Im folgenden rechten Plot zeichnen wir noch eine Box ein, um zu demonstrieren,
dass wir auch Boxen „nett“ gestalten können:
Tangenshyperbolicus Tangenshyperbolicus
1.0
1.0
0.5
0.5
0.0
0.0
y
−2 −1 0 1 2 −2 −1 0 1 2
−0.5
−0.5
−1.0
−1.0
x x
Natürlich ist das Gitternetz viel zu dick, lwd = 1 hätte gereicht. Allerdings können
wir so einen wichtigen Aspekt motivieren: Die Funktionsgerade wird teilweise durch
das Gitternetz und die Achsen überdeckt. Auch wenn das hier nicht so schlimm ist
(so lwd = 1 wäre), könnte das in anderen Fällen die Grafikqualität negativ beein-
trächtigen. Im folgenden Abschnitt sehen wir uns eine Modifikation an.
1. Wir rufen plot() mit dem Parameter type = "n" auf. Dadurch wird die Funk-
tionsgerade nicht gezeichnet.
2. Wir zeichnen das Gitternetz und die Achsen ein.
Beispiel: Fortsetzung des Beispiels auf Seite 522. Die Funktionsgerade soll über das
Gitternetz und über die Achsen gezeichnet werden.
Tangenshyperbolicus Tangenshyperbolicus
1.0
1.0
0.5
0.5
0.0
0.0
y
−2 −1 0 1 2 −2 −1 0 1 2
−0.5
−0.5
−1.0
−1.0
x x
In der rechten Grafik sind das Gitternetz und die Achsen in einer vernünftigen
Größe eingezeichnet (lwd = 1). Interessierte können sich als Ergänzung die Para-
meter panel.first und panel.last von plot.default() anschauen.
Mit Hilfe der Funktion polygon() können wir Polygone zeichnen. In Tab. 34.5
listen wir wichtige Parameter auf. Der Funktionsaufruf:
Parameter Bedeutung
density Dichte der Schraffierung in Linien pro Inch
NULL: keine Schraffierung (Defaultwert).
angle Winkel der Schraffierung. 0: waagrechte Schraffierung, 45:
Drehung um 45 Grad gegen den Uhrzeigersinn (Defaultwert) etc.
col Füllfarbe bzw. Farbe der Schraffierung
NA: keine Füllung (Defaultwert)
border Farbe der Umrandung
NULL: Es wird die Farbe der Schraffierung (falls vorhanden) oder
par("fg") herangezogen (f oreground, Defaultwert).
NA: Umrandung wird nicht gezeichnet.
Mit pty = "s" sorgen wir dafür, dass die x-Achse und y-Achse dieselbe Länge
haben. Mit pty = "m" (Standard) nützen wir die Plotregion maximal aus.
Polygone
5
4
3
2
1
0
0 1 2 3 4 5
526 I Visualisierung von Daten
Oft wollen wir Achsenbeschriftungen näher an die Grafik heranrücken, was wir mit
Adjustierungsparametern wie adj oder padj umsetzen können. Mit par(pty = "s")
stellen wir eine quadratische Plotregion ein, wie schon in (34.3.5) gesehen.
Beispiel: Zeichne ein Schachbrett. Das Ergebnis könnte etwa so aussehen:
A B C D E F G H
8
8
7
7
6
6
5
5
4
4
3
3
2
2
1
A B C D E F G H
Innerhalb der for-Schleifen zeichnen wir die Felder ein. Ganze Zahlen markieren
den Mittelpunkt eines Feldes. Die Eckpunkte eines Feldes bestimmen wir, indem wir
zum Mittelpunkt −0.5 und +0.5 addieren. Die Farbe variiert zwischen 0 (weiß) und
1 (Schwarz). Mit lwd = 0 wird das Zeichnen der Achsenlinien verhindert und mit
padj = -2 werden die Beschriftungen näher an die Grafik herangerückt.
Drei weitere Funktionen bieten sich an, wenn es um Linien und Pfeile geht. Mit
abline() können wir „intelligente“ durchgezogene Linien einzeichnen.
abline() ist sehr flexibel. Horizontale und vertikale Linien können wir mit den
Parametern h und v einzeichnen. Die Parameter a und b können wir für lineare
Funktionen der Form a + b · x verwenden. Alternativ können wir unter anderem
einen Koeffizientenvektor der Länge 2 an coef übergeben.
Mit segments() zeichnen wir vektorwertige Linien ein: Wir ersparen uns dadurch
eine Schleife, in der wir lines() aufrufen. Die Anwendung der Funktion:
x0 und y0 sind die Startpunkte der Linien und x1 und y1 die Endpunkte. Für das
Dreipunkteargument "..." können wir weitere Grafikargumente wie lwd übergeben.
Die Funktion arrows(), mit der wir vektorwertige Pfeile zeichnen, funktioniert
nach demselben Prinzip. Wie wir in Tab. 34.6 sehen, können wir zusätzlich die
Pfeilspitzen anpassen.
Parameter Bedeutung
length Länge der Pfeilspitze in inches
angle Winkel in Grad. Standardmäßig ist angle auf 30 Grad eingestellt.
code 1: Pfeilspitze beim Startpunkt („Rückwärtspfeil“)
2: Pfeilspitze beim Endpunkt („Vorwärtspfeil“ – Default)
3: Pfeilspitzen an beiden Enden („Zweirichtungspfeil“)
> # Plot mit dichteren Achsen > # Vorwärtspfeile (links --> rechts)
> plot(1:10, 1:10, axes = FALSE) > arrows(1, 5:10, 2, 5:10)
> box() > arrows(2, 5:10, 3, 5:10,
> axis(1, 1:10) + angle = 60, length = 0.1)
> axis(2, 1:10)
> # An beiden Enden Pfeile zeichnen
> # Gitternetz einzeichnen > arrows(3, 5:10, 4, 5:10, code = 3)
> # grid() funktioniert hier nicht.
> abline(h = 1:10, > # Blaue strichlierte Pfeile zeichnen
+ col = "grey", lty = "dotted") > arrows(4, 5:10, 5:10, 4, col = 4,
> abline(v = 1:10, + lty = "dashed", lwd = 3)
+ col = "grey", lty = "dotted")
> # Rote Linien und Pfeile zeichnen
> # zeichnet die Gerade a + b*x ein > segments(5:10, 4, 5:10, 2)
> abline(a = 0, b = 1, col = "red") > arrows(5:10, 2, 5:10, 1, col = 2)
9 10
9 10
8
8
7
7
6
6
1:10
1:10
5
5
4
4
3
3
2
2
1
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
1:10 1:10
grid() funktioniert in der Form nicht, weil grid() dort das Gitternetz zeichnet, wo
die Achsen defaultmäßig ihre Häkchen haben – bestimmt durch pretty(). Das heißt,
das Gitternetz würde nicht zu den Achsen passen. Wir könnten mit den Parametern
nx und ny von grid() spielen, aber dank abline() haben wir volle Kontrolle!
Wenn wir einer Grafik Beschriftungen hinzufügen möchten, so bietet sich die
Funktion title() an. In Tab. 34.7 schauen wir uns die Parameter kurz an.
Eine nette Eigenschaft von title() ist, dass wir die Positionierung (Adjustierung)
der Beschriftungen leicht mit Hilfe von line steuern können, wie wir im folgen-
den rechten Codebeispiel sehen. Das funktioniert für andere Beschriftungen völlig
analog. Mit xlab = "" und ylab = "" verhindern wir, dass Achsenbeschriftungen
abgebildet werden. Diese fügen wir im linken Code nachträglich mit title() hinzu.
34 Einführung in die 2D-Grafik 529
Parameter Bedeutung
main Hauptüberschrift
sub Subüberschrift
xlab bzw. ylab Beschriftung der x- bzw. y-Achse
line Auf welcher Höhe soll die Beschriftung gezeichnet werden?
Main Main 2
Main 1
Main 0
1.0
1.0
0.8
0.8
0.6
0.6
ylab
0.4
0.4
0.2
0.2
0.0
0.0
0.0 0.2 0.4 0.6 0.8 1.0 0.0 0.2 0.4 0.6 0.8 1.0
xlab
Vorweg: Das Beispiel hat es in sich und soll dir zeigen, was alles möglich ist!
Ziel: Wir schreiben eine Funktion, welche uns die aktuelle Uhrzeit grafisch anzeigt.
Dabei greifen wir auch auf die Techniken von (27) (Datum- und Uhrzeit) zurück.
Bei Polarkoordinaten wird ein Punkt durch einen Winkel und einen Abstand zum Ur-
sprung beschrieben. Sie erleichtern die Bestimmung der Markierungspositionen der
Uhr enorm! In Abb. 34.1 ist ein R-Code zur Bestimmung von kartesischen Kreisko-
ordinaten abgebildet, die wir aus Polarkoordinaten ableiten.
530 I Visualisierung von Daten
In Abb. 34.3 bilden wir den R-Code der Funktion uhr() zur Darstellung der aktuellen
Uhrzeit ab, den wir dort auch kommentieren. Du kannst den Code der Funktion
auch schrittweise ausführen, um ihn besser nachzuvollziehen. Abb. 34.2 zeigt nähere
Erläuterungen zu Polarkoordinaten und den Output der Funktion uhr().
3π
6
12
4π 2π
11 1
6 0.87 6
5π 1π
10
2
6 1 6
6π φ 0
9
6 0.5 2π
7π 11π
4
8
6 6
8π 10π 7 5
6 9π 6 6
6
# Zeiger einzeichnen
arrows(0, 0, x, y, lwd = 2)
points(0, 0, pch = 16, col = 4, cex = 2)
34.5 Abschluss
• Wie sorgen wir dafür, dass Punkte und Linien über etwaige Gitternetze und
Achsen gezeichnet werden? (34.3.4)
• Wie zeichnen wir Rechtecke und Polygone in eine Grafik ein? Wie stellen wir
die Füllfarbe ein? Wie schraffieren wir Polygone und wie beeinflussen wir die
Dichte der Schraffur? Wie bestimmen wir den Winkel der Schraffur sowie die
Farbe der Umrandung? Wie unterbinden wir, dass eine Umrandung um das
Polygon gezeichnet wird? (34.3.5)
• Wie stellen wir eine quadratische Plotregion ein und was bedeutet das? Wie
verhindern wir, dass Achsenlinien gezeichnet werden? Mit welchen Parametern
können wir Achsenbeschriftungen näher an die Grafik heranrücken? (34.3.6)
• Wie zeichnen wir horizontale und vertikale Linien sowie Linien der Bauart y =
a + b · x ein? Wie fügen wir unserer Grafik mehrere Linien vektorwertig hinzu?
Wie zeichnen wir Pfeile (Vorwärtspfeile, Rückwärtspfeile, Zweirichtungspfeile)
ein und wie steuern wir das Aussehen der Pfeile? (34.3.7)
• Wie können wir im Nachhinein eine Überschrift sowie Achsenbeschriftungen
einfügen? Wie verhindern wir in plot(), dass Achsenbeschriftungen (und eine
Überschrift) abgebildet werden? (34.3.8)
34.5.2 Ausblick
Die Reise durch die Grafikwelt geht in (35) mit Standardgrafiken (z. B. Balkendia-
grammen und Histogrammen) und Farbpaletten sowie in (36) mit Layouting weiter.
34.5.3 Übungen
Zeichne die Dichtefunktion im Intervall [−1.5, 1.5]. Berücksichtige bei der Gra-
fikerstellung folgendes:
• Zeichne an einer passenden Stelle eine Legende ein. Die Legende soll die
Dichtefunktion enthalten. Die Linie in der Legende soll dieselbe Farbe
haben wie die gezeichnete Dichtefunktion.
Hinweis: Gerne kannst du dich von der Grafik auf Seite 483 inspirieren lassen.
534 I Visualisierung von Daten
2. Wir betrachten die Iris-Daten aus der Kapiteleinführung auf Seite 507.
Erstelle nun ein Streudiagramm mit den beiden Variablen Sepal.Length und
Sepal.Width. Deine Grafik soll dabei folgende Punkte erfüllen:
Eignen sich die Kelchblätter oder die Blütenblätter besser dazu, die Spezies
voneinander zu unterscheiden?
3. Zeichne die Grafik am Ende von (16.3.1) ab Seite 199 nach. Das heißt: Er-
stelle ein ansprechend beschriftetes Streudiagramm mit der Größe und dem
Gewicht und zeichne die Regressionsgerade, die Residuen (Abstände zwischen
den Datenpunkten und der Regressionsgeraden) und eine Legende ein.
4. Schreibe den Code zur Erzeugung des Schachbretts im Beispiel auf Seite 526
so um, dass das Schachbrettmuster mit rect() statt mit polygon() erzeugt
wird. Versuche dabei, auf Schleifen komplett zu verzichten und rect() nur ein
Mal aufzurufen.
20
40
60
80
100
Alles, was du über Standardgrafiken lernst, kannst du schon mit den Techniken aus
(34) umsetzen. Wenn du etwa ein Balkendiagramm zeichnen willst, dann kannst
du die Balken als Rechtecke einzeichnen. Passende Achsen dazu, fertig. Das dauert
jedoch etwas länger und daher schauen wir uns in diesem Kapitel schnellere Mög-
lichkeiten an, wie Standardgrafiken erstellt werden können.
Wir betrachten im ersten Teil dieses Kapitels die Wahlergebnisse aller österreichi-
schen Nationalratswahlen von 1945 bis 2017, die in der Datei NRWahlen.txt enthal-
ten sind. Lesen wir zunächst die Daten ein.2
Die Datei ist im UTF-8 Format codiert, daher setzen wir encoding = "UTF-8".
Damit stellen wir sicher, dass die Umlaute (die Ö’s in den Parteinamen) korrekt
eingelesen werden. Schauen wir uns die Wahlergebnisse der letzten 3 Wahlen an.
> tail(wahl, n = 3)
Wahljahr SPÖ ÖVP FPÖ KPÖ Grüne LIF BZÖ MATIN FRANK NEOS PILZ Sonstige
20 2008 29.3 26.0 17.5 0.8 10.4 2.1 10.7 NA NA NA NA 3.2
21 2013 26.8 24.0 20.5 1.0 12.4 NA 3.5 NA 5.7 5.0 NA 1.1
22 2017 26.9 31.5 26.0 0.8 3.8 NA NA NA NA 5.3 4.4 1.3
Ab der zweiten Spalte finden wir die Stimmenanteile der Parteien in Prozent. Ein
NA bedeutet, dass die Partei bei der entsprechenden Wahl nicht angetreten ist.
• Wie können wir den Verlauf der Wahlergebnisse in einer Grafik visualisieren?
(35.1.1)
• Wie stellen wir das Ergebnis einer Wahl als Balkendiagramm dar? Wie ver-
gleichen wir die Ergebnisse zweier Wahlen in einem Balkendiagramm? (35.1.2)
Im zweiten Teil greifen wir den Iris-Datensatz (siehe Infobox 13 auf Seite 506) erneut
auf. In (34.3.2) haben wir bereits ein Streudiagramm für zwei Variablen erstellt und
dekoriert. Boxplots stoßen in (35.1.5) zu unserer Grafiksammlung dazu.
2 Quellen:Statistik Austria, http://wahl13.bmi.gv.at (abgerufen am 11.12.2017), http://
wahl17.bmi.gv.at (abgerufen am 11.12.2017)
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_35
536 I Visualisierung von Daten
Wie wir Histogramme erstellen, erfahren wir in (35.1.3). Bei der Gelegenheit zeichnen
wir auch Dichtekurven mit ein.
Nicht nur Freundinnen und Freunde der Farben werden von (35.2) begeistert sein.
Dort mischen wir uns in (35.2.1) und (35.2.2) eigene Farben zusammen, basierend
auf RGB- und HCL-Farben. Wenn wir in einem Balkendiagramm ordinalskalierte
Merkmale darstellen, dann sind Farbpaletten wertvolle Freundinnen! Wir können in
R auf vorgefertigte Paletten zugreifen oder eigene Farbverläufe kreieren. Wie wäre
es etwa mit einem Farbverlauf von Weiß über Gelb zu Rot? Oder von Rot über Weiß
nach Blau? Nach (35.2.3) und (35.2.4) kein Problem für uns!
35.1 Standardgrafiken
Stellen wir uns vor, wir wollen die Funktion f (x) = k · x für mehrere k im Intervall
[0, 2] plotten. Versuchen wir es mit den bisher gelernten Techniken für k ∈ {1, 2, 3}.
Vielleicht fällt dir bei der folgenden linken Grafik eine Schwäche auf ;-)
6
5
1.5
4
k*x
k*x
1.0
3
2
0.5
1
0.0
0.0 0.5 1.0 1.5 2.0 0.0 0.5 1.0 1.5 2.0
x x
35 Standardgrafiken und Farben 537
Die linke Grafik überzeugt uns wohl nicht, da die rote und grüne Linie über den Rand
hinausragen. Grund: Die Größe der Grafik wird im Zuge des plot()-Befehls fest-
gelegt und da übergeben wir an y den Vektor 1 * x. Danach eingezeichnete Linien
beeinflussen die Plotgröße nicht mehr. Ragen sie hinaus, haben sie Pech gehabt.
Um alle Linien vollständig sichtbar zu machen, müssen wir die Plotregion bereits
vor dem Zeichnen festlegen. Das bewerkstelligen wir im rechten Code, indem wir
zuerst alle auftretenden y-Werte berechnen und sie in der Matrix Y abspeichern.
Danach erstellen wir einen leeren Plot und übergeben plot() die Extremalpunkte.
Damit ist sichergestellt, dass am Ende alle Linien vollständig sichtbar sind. Alterna-
tiv hätten wir auch die Parameter xlim und ylim bemühen können. Abschließend
fügen wir in der for-Schleife für jede Spalte von Y eine Linie in die Grafik ein.
Eine in vielen Fällen anwendbare und bequemere Alternative bietet uns die Funktion
matplot(). Sie arbeitet nach demselben Prinzip wie plot() mit dem Unterschied,
dass x oder y auch Matrizen sein dürfen. Ist x ein Vektor und y eine Matrix, so wird
x gemeinsam mit jeder Spalten von y gezeichnet.
6
5
5
4
4
k*x
k*x
3
3
2
2
1
1
0
0.0 0.5 1.0 1.5 2.0 0.0 0.5 1.0 1.5 2.0
x x
Wir sehen, dass matplot() die Stricharten und Farben defaultmäßig unterschiedlich
wählt. Selbstverständlich können wir diese auch nach unseren eigenen Wünschen
einstellen, das funktioniert wie bei plot() über Grafikparameter (vgl. (34.2)). In
der linken Grafik haben wir die Linien wegen lwd = 3 dicker gezeichnet, in der
rechten Grafik sind alle Linien wegen lty = 1 durchgezogen. Wollen wir im Nach-
hinein Linien und Punkte einzeichnen, so stehen uns die Funktionen matlines()
und matpoints() zur Verfügung.
538 I Visualisierung von Daten
> tail(wahl, n = 3)
Wahljahr SPÖ ÖVP FPÖ KPÖ Grüne LIF BZÖ MATIN FRANK NEOS PILZ Sonstige
20 2008 29.3 26.0 17.5 0.8 10.4 2.1 10.7 NA NA NA NA 3.2
21 2013 26.8 24.0 20.5 1.0 12.4 NA 3.5 NA 5.7 5.0 NA 1.1
22 2017 26.9 31.5 26.0 0.8 3.8 NA NA NA NA 5.3 4.4 1.3
Wir sehen, dass 2 Parteien bei den letzten beiden Wahlen nicht angetreten sind.
Diese bestimmen wir automatisiert, indem wir zunächst die letzten beiden Zeilen
von wahl selektieren und dann jene Spalten bestimmen, die zwei NAs enthalten.
Jetzt machen wir Folgendes: Wir greifen uns diese Parteien gemeinsam mit Sonstige
heraus und bilden die Zeilensummen. Denn diese Parteien sind schon früher einmal
angetreten. Damit haben wir Sonstige aktualisiert und können sodann die nicht
angetretenen Parteien entfernen.
> tail(wahl, n = 3)
Wahljahr SPÖ ÖVP FPÖ KPÖ Grüne BZÖ FRANK NEOS PILZ Sonstige
20 2008 29.3 26.0 17.5 0.8 10.4 10.7 NA NA NA 5.3
21 2013 26.8 24.0 20.5 1.0 12.4 3.5 5.7 5.0 NA 1.1
22 2017 26.9 31.5 26.0 0.8 3.8 NA NA 5.3 4.4 1.3
Die Linien zeichnen wir dabei noch nicht (type = "n"), da wir noch ein Gitternetz
einzeichnen und die Linien nicht von diesem überdeckt werden sollen (vgl. (34.3.4)).
Das Gitternetz können wir hier nicht mit grid() einzeichnen, da wir eine vertikale
Linie bei jedem Wahljahr haben wollen und die Wahljahre nicht äquidistant sind.
Wie gut, dass es Funktionen wie abline() aus (34.3.7) gibt :-)
Die horizontalen Linien erstrecken sich in 5er-Schritten über unsere Grafik. Hat eine
Partei mindestens 50% der gültigen Stimmen bekommen, so hat diese Partei die
absolute Mehrheit erreicht. Diese wollen wir etwas dicker markieren.
Jetzt zeichnen wir die Verläufe der Ergebnisse aller Parteien ein, was wir einfach mit
der Funktion matlines() bewerkstelligen. Davor definieren wir noch für jede Partei
eine geeignete Farbe, wobei wir uns hierbei zum Beispiel von (34.2.2) inspirieren
lassen. Dazu erstellen wir einen mit den Parteinamen beschrifteten Vektor. Da-
durch können wir jederzeit ganz bestimmte Parteifarben aus diesem Vektor in der
gewünschten Reihenfolge selektieren.
Jetzt kommen noch die Achsen und Legende. Damit die Prozentwerte auf der y-
Achse aufgestellt und die Wahljahre auf der x-Achse um 90 Grad verdreht sind,
setzen wir las = 2. Allgemein steuert las die Ausrichtung der Wertbeschriftungen
der Achsen. Herausfordernd ist es, cex.axis so einzustellen, dass möglichst alle
Wahljahre angezeigt werden und sie gut lesbar sind. In unserer Grafik wird das Jahr
1971 nicht angezeigt, weil es zu eng an 1970 liegt.
Die Legende positionieren wir links in der Mitte. In Übung 3 auf Seite 562 überlegen
wir uns, wie wir die Legende oben in der Grafik platzieren können.
Nationalratswahlen in Österreich
55
50
45
Prozent der gültigen Stimmen
40
35
SPÖ BZÖ
30 ÖVP FRANK
FPÖ NEOS
25 KPÖ PILZ
Grüne Sonstige
20
15
10
0
1945
1949
1953
1956
1959
1962
1966
1970
1975
1979
1983
1986
1990
1994
1999
2002
2006
2008
2013
2017
Wahljahr
Mit barplot() können wir Balkendiagramme erstellen. Diese Funktion ist viel-
seitig und es lohnt sich, die Hilfe (?barplot) zu studieren. Wir schauen uns in Tab.
35.1 ausgewählte Parameter an.
Parameter Bedeutung
height Vektor oder Matrix mit den Höhen der Balken
width Breite der Balken. Standardmäßig sind alle Balken gleich breit.
beside nur sinnvoll, falls height eine Matrix ist
FALSE: Die Spalten von height werden gestapelt.
TRUE: Die Spalten von height werden gruppiert.
horiz FALSE: Die Balken werden vertikal gezeichnet (Säulen).
TRUE: Die Balken werden horizontal gezeichnet (quer gelegt).
35 Standardgrafiken und Farben 541
Daneben gibt es die „üblichen Verdächtigen“ wie col (Farbe der Balken), density
und angle (Schraffurstil der Balken). Auch die Abstände zwischen den Balken
können wir einstellen (space) usw.
Beispiel: Fortsetzung des Beispiels auf Seite 538. Stelle die Nationalratswahl 2017
in einem Balkendiagramm dar. Sortiere dabei die Parteien absteigend nach ihrem
Stimmenanteil und stelle nur Parteien dar, die angetreten sind. Sonstige soll dabei
am Schluss stehen.
Schon auf Seite 539 haben wir las = 2 eingesetzt. Die Beschriftungen von wahl17
werden im Balkendiagramm übernommen. Jetzt sortieren wir noch die Parteien. Wie
zuvor passen wir auf, dass wir die Parteifarben korrekt zuordnen!
30 30
25 25
20 20
15 15
10 10
5 5
0 0
Grüne
Grüne
SPÖ
FPÖ
KPÖ
NEOS
PILZ
Sonstige
SPÖ
FPÖ
NEOS
PILZ
KPÖ
Sonstige
ÖVP
ÖVP
542 I Visualisierung von Daten
Beispiel: Stelle nun die Nationalratswahlen 2013 und 2017 in einem gruppierten
Balkendiagramm dar, wobei die Parteien nach dem Ergebnis von 2017 sortiert
sind (Sonstige wieder am Schluss). Das Wahlergebnis von 2013 soll dabei heller
sein als jenes von 2017. Dabei darfst du folgende Funktion verwenden:
Schon im Beispiel ab Seite 538 haben wir all jene Parteien „eliminiert“ (das heißt in
Sonstige gesteckt), die bei den letzten beiden Wahlen nicht angetreten sind. Darum
brauchen wir uns also nicht mehr zu kümmern.
Das Sortieren der Spalten funktioniert jetzt nicht mehr mit sort(), aber mit order()
finden wir einen würdigen Ersatz.
> # Jetzt nach 2017 absteigend sortieren und Sonstige hinten anhängen
> order <- c(order(wahl2[2, colnames(wahl2) != "Sonstige"],
+ decreasing = TRUE, na.last = TRUE), ncol(wahl2))
> order
[1] 2 1 3 8 9 5 4 6 7 10
Die Funktion order() gibt die Indizes in jener Reihenfolge zurück, sodass die Ele-
mente nach der Entnahme sortiert sind (vgl. (6.2.2)). Um Sonstige hinten anzu-
hängen, muss diese Spalte als letzte entnommen werden, was wir mit ncol(wahl2)
markieren. Mit na.last = TRUE schieben wir fehlende Werte nach hinten. Das ist
notwendig, damit nicht jene Parteien verschwinden, die 2013 angetreten sind, aber
2017 nicht mehr.
Jetzt zur Bestimmung der Parteifarben. Damit es funktioniert, müssen wir jede
Farbe duplizieren, wobei wir jede zweite Farbe (beginnend bei der ersten) mit obiger
Funktion lighten() aufhellen. Wir gehen gleich noch auf Details dazu ein.
30
25
20
15
10
0
Grüne
SPÖ
FPÖ
NEOS
PILZ
KPÖ
BZÖ
FRANK
Sonstige
ÖVP
Mit beside = TRUE bestimmen wir, dass ein gruppiertes Balkendiagramm er-
stellt wird. Die Farben werden dabei spaltenweise zugeordnet, also ÖVP 2013,
ÖVP 2017, SPÖ 2013, SPÖ 2017 etc. Deshalb müssen wir bei der Duplizierung der
Parteifarben each = 2 nehmen (und nicht times = 2). Da zuerst die Ergebnisse
aus 2013 abgebildet werden, müssen wir jeweils die erste Farbe aufhellen.
Beispiel: Fortsetzung des letzten Beispiels. Jetzt soll ein gestapeltes Balkendia-
gramm erstellt werden: Links sollen die gestapelten Ergebnisse von 2013 und rechts
jene von 2017 stehen.
Wir beschränken uns auf die notwendigen Modifikationen zum vorherigen Beispiel.
80
60
40
20
2013 2017
Mit Hilfe der Funktion hist() können wir Histogramme erstellen. In Tab. 35.2
listen wir wichtige Parameter auf. In der Hilfe (?hist) und im folgenden Beispiel
gibt es noch viel Spannendes zu entdecken!
Parameter Bedeutung
x Numerischer Vektor
breaks Ein Skalar gibt die (ungefähre) Anzahl der Klassen an, die im
Histogramm gezeigt werden. Es gibt vier weitere Möglichkeiten
(siehe Hilfe).
freq TRUE: Darstellung von absoluten Häufigkeiten (Default).
FALSE: Darstellung von relativen Häufigkeiten.
35 Standardgrafiken und Farben 545
Histogram of x Histogram of x
0.30
15
Frequency
0.20
10
Density
0.10
5
0.00
0
−4 −3 −2 −1 0 1 2 3 −4 −3 −2 −1 0 1 2 3
x x
In der linken Grafik werden absolute Häufigkeiten, in der rechten Grafik hingegen
wegen freq = FALSE relative Häufigkeiten dargestellt.
Jetzt schauen wir uns an, wie wir die Säulen umdesignen können.
Histogram of x Histogram of x
0.30
0.30
0.20
0.20
Density
Density
0.10
0.10
0.00
0.00
−4 −3 −2 −1 0 1 2 3 −4 −3 −2 −1 0 1 2 3
x x
546 I Visualisierung von Daten
Abschließend zeigen wir, wie wir die Anzahl der Klassen grob steuern können.
Beachte, dass es sich dabei nur um Richtwerte handelt: Setzen wir breaks = 4, so
werden ungefähr 4 Klassen genommen. Die Wahl der „richtigen“ Anzahl an Klas-
sen ist tricky; R wählt normalerweise immer eine vernünftige Zahl. 30 Klassen sind
beispielsweise zu viele.
Histogram of x Histogram of x
0.8
0.20
0.6
0.15
Density
Density
0.4
0.10
0.2
0.05
0.00
0.0
−4 −2 0 2 4 −3 −2 −1 0 1 2 3
x x
Beispiel: Fortsetzung des Beispiels von Seite 545. Zeichne jetzt noch die Dichte der
Standardnormalverteilung ein.
> # Zeige die inneren Ränder an > # Zeige die inneren Ränder an
> tmp <- par()$usr > tmp <- par()$usr
> tmp > tmp
[1] -4.28 3.28 -0.68 17.68 [1] -4.2800 3.2800 -0.0136 0.3536
> # x- und y-Werte der Dichte > # x- und y-Werte der Dichte
> xx <- seq(tmp[1], tmp[2], > xx <- seq(tmp[1], tmp[2],
+ length = 201) + length = 201)
> yy <- dnorm(xx) > yy <- dnorm(xx)
Frage: Findest du den Grund, warum im linken Histogramm die Dichte nicht schön
über dem Histogramm liegt? Kleiner Tipp: Der rechte Code unterscheidet sich vom
linken durch genau eine Parameterdefinition) ;-)
35 Standardgrafiken und Farben 547
Histogram of x Histogram of x
0.30
15
Frequency
0.20
10
Density
0.10
5
0.00
0
−4 −3 −2 −1 0 1 2 3 −4 −3 −2 −1 0 1 2 3
x x
Interessant ist par()$usr. Die 4 Zahlen zeigen uns die inneren Grafikränder an: links,
rechts, unten, oben. Der Wert 0.3536 im rechten Code etwa zeigt uns an, dass oben
bei 0.3536 abgeschnitten wird. Das ist genau dort, wo die rote Linie unterbrochen
wird. Wir schauen uns in (35.1.4) eine Technik an, mit der wir gewährleisten können,
dass die Dichte vollständig angezeigt wird.
Im letzten Beispiel des vorherigen Unterabschnitts haben wir gesehen, dass Grafik-
elemente wie Dichtekurven über eine Grafik hinausragen können. Das ist nicht so
hübsch, daher wollen wir das korrigieren.
Versuchen wir einmal, die Rückgabe von hist() auf ein Objekt res.hist zu spei-
chern.
> set.seed(111)
> x <- rnorm(50)
Die meisten Grafikfunktionen aus dem Paket graphics haben den Parameter plot.
Setzen wir diesen auf FALSE, so wird keine Grafik gezeichnet.
548 I Visualisierung von Daten
Die Funktion hist() gibt uns (unsichtbar via invisible()) eine Liste zurück, die
unter anderem folgende Informationen enthält:
Schauen wir uns anhand eines Codebeispiels an, wie wir diese Informationen dazu
nützen, um beispielsweise bestimmte Säulen hervorzuheben.
Histogram of x Histogram of x
15
15
Frequency
Frequency
10
10
5
5
0
−4 −3 −2 −1 0 1 2 3 −4 −3 −2 −1 0 1 2 3
x x
Bei der Schraffierung beginnen wir mit der Ecke links unten und gehen gegen den
Uhrzeigersinn zur Ecke links oben.
Auch andere Grafikfunktionen geben uns relevante Informationen zurück, die wir
weiterverarbeiten können. Da das Grundprinzip gezeigt ist, verzichten wir auf wei-
tere Beispiele. In Übung 2 auf Seite 562 bekommst du die Möglichkeit, mit Hilfe
dieser Techniken dafür zu sorgen, dass im Beispiel ab Seite 546 die Dichtefunktion
vollständig angezeigt wird.
35 Standardgrafiken und Farben 549
7.5
6.5
6.5
cm
cm
5.5
5.5
4.5
4.5
Gehen wir einen Schritt weiter. Wir können der Funktion boxplot() auch ein Da-
taframe übergeben. In dem Fall werden alle Variablen in einem Boxplot dargestellt.
Das macht natürlich nur dann Sinn, wenn die Maßeinheiten aller beteiligten Varia-
blen gleich sind. So, wie es im folgenden Beispiel der Fall ist.
Beispiel: Zeichne für den Iris-Datensatz einen Boxplot für alle Sepal-Variablen.
Die Boxplots sollen in einer Grafik dargestellt werden.
8
7
7
6
6
cm
cm
5
5
4
4
3
3
2
Die Farben in der rechten Grafik sind zu kräftig, generell sollten wir dezentere Far-
ben wie lightblue nehmen. Beachte, dass bei der Farbzuteilung gegebenenfalls das
Recycling greift. Potenzielle Ausreißer werden durch Punkte dargestellt.
Was tun wir, wenn wir einen Boxplot für eine Variable getrennt nach einer katego-
riellen Variable erstellen wollen? Einen gruppierten Boxplot also? Hier haben wir
drei Möglichkeiten, wie wir im nächsten Beispiel sehen werden.
Beispiel: Zeichne für die Variable Sepal.Width einen Boxplot. Unterteile diesen
nach der Spezies (Species).
Wir haben drei Möglichkeiten:
1. boxplot(iris$Sepal.Width ∼ iris$Species)
Hier wird ein formula-Objekt verwendet; es wird intern boxplot.formula()
aufgerufen. Eine einfache Formel ist y ∼ x. Dabei wird y (Sepal.Width) gemäß
x (Species) aufgetrennt. Statistische Modelle und formula-Objekte bespre-
chen wir ausführlich in (38).
2. plot(iris$Sepal.Width ∼ iris$Species)
Hier wird plot.formula() aufgerufen (plot() ist generisch). Da Species ein
Faktor ist, wird ein Boxplot generiert. Ein Streudiagramm würde in diesem
Fall keinen Sinn machen.
3. plot(iris$Species, iris$Sepal.Width)
Hier wiederum wird plot.factor() aufgerufen, da Species (das erste über-
gebene Objekt) ein Faktor ist. Wiederum wird ein Boxplot gezeichnet.
4.0
iris$Sepal.Width
3.5
3.5
y
3.0
3.0
2.5
2.5
2.0
2.0
setosa versicolor virginica setosa versicolor virginica
iris$Species x
In (34.2.2) haben wir bereits einfache Möglichkeiten zur Farbgebung betrachtet. Wir
werden in diesem Abschnitt tiefer in die bunte Welt der Farben eintauchen.
In (35.2.1) und (35.2.2) lernen wir, wie wir mit Hilfe des RGB-Farbraums bzw.
des HCL-Farbraums eigene Farben zusammenmischen können. R stellt uns Stan-
dardfarbpaletten zur Verfügung, einige davon bewundern wir in (35.2.3). Am Ende
lernen wir in (35.2.4), wie wir uns eigene Farbpaletten basteln können.
RGB (Red, Green, Red) ist wohl der bekannteste Farbraum. Die Farbe setzt sich
dabei aus den drei Basisfarben Rot, Grün und Blau zusammen. Damit können wir
andere Farben zusammen mischen.
Mit der Funktion rgb() erzeugen wir Farben aus dem RGB-Farbraum. Schauen
wir sie uns an:
Dabei stehen red, green und blue für den Rot-, Grün-, bzw. Blauanteil der Farbe.
Je höher ein Farbwert ist, desto mehr scheint diese Farbe durch. Der Farbwert jeder
Basisfarbe wird als Zahl zwischen 0 und maxColorValue angegeben. Standardmäßig
ist maxColorValue auf 1 gesetzt, wir verwenden aber in der Praxis sehr oft den Wert
255 als maxColorValue, da Farbwerte in der Regel als ganze Zahlen zwischen 0 und
255 (= 28 − 1) angegeben werden.
552 I Visualisierung von Daten
Für die Darstellung der Farben zweckentfremden wir das Balkendiagramm, indem
wir gleich hohe Balken ohne Abstand (space = 0) definieren.
Ein weiterer Farbraum ist HCL (Hue, Chroma, Luminance). Wir können zwar mit
den Standardfarben gut arbeiten, allerdings sind diese oftmals unangenehm für die
Augen. Einige Farbtöne wie Grün und Gelb stechen grell hervor, während andere
Farben wie Blau und Schwarz sehr kräftig und dunkel sind.
HCL-Farben zielen darauf ab, Farben gleicher Intensität zu generieren. Sie kön-
nen in R mit der Funktion hcl() erzeugt werden. Die Farbe wird mit Hilfe von drei
Parametern bestimmt:
Kein Vorteil ohne Nachteil: Werden Farben mit gleicher Intensität generiert, so ge-
hen unter Umständen Farbtöne verloren. In Abb. 35.1 erkennen wir, dass im HCL-
Farbraum der Farbton Blau (h = 240) für sehr kleine Luminanzwerte und höhere
Chromawerte nicht darstellbar ist.
hcl−Farbpalette für feste luminance (50) hcl−Farbpalette für feste luminance (70)
100
100
80
80
60
60
chroma
chroma
40
40
20
20
0
0 50 100 150 200 250 300 350 0 50 100 150 200 250 300 350
hue hue
hcl−Farbpalette für festes hue (120) hcl−Farbpalette für festes hue (240)
100
100
80
80
60
60
chroma
chroma
40
40
20
20
0
0 20 40 60 80 100 0 20 40 60 80 100
luminance luminance
Einen ähnlichen Farbraum beschreibt HSV (Hue, Saturation, Value) – siehe ?hsv.
554 I Visualisierung von Daten
R stellt uns einige Standardfarbpaletten zur Verfügung. Listen wir ein paar Funk-
tionen auf, die uns Farben aus vorgefertigten Farbpaletten zurückgeben:
• rainbow(): Regenbogenfarben
• heat.colors(): warme Farben von Rot bis Gelb
• cm.colors(): von Cyan bis Magenta
• terrain.colors(): Terrainfarben
• topo.colors(): Freunde von Atlanten werden diese Farben lieben.
Jede der oben angeführten Funktionen hat den Parameter n. Dieser steuert, wie viele
Farben aus der Palette erzeugt werden. Die Funktionen geben uns sodann n Farben
aus der entsprechenden Farbpalette zurück.
Schauen wir uns die Farbpaletten der Reihe nach an! Dabei zweckentfremden wir
wieder das Balkendiagramm analog zu (35.2.1).
> barplot(x, space = 0, axes = FALSE, > barplot(x, space = 0, axes = FALSE,
+ col = heat.colors(n)) + col = cm.colors(n))
> barplot(x, space = 0, axes = FALSE, > barplot(x, space = 0, axes = FALSE,
+ col = terrain.colors(n)) + col = topo.colors(n))
35 Standardgrafiken und Farben 555
colorRampPalette(colors, ...)
> barplot(x, space = 0, axes = FALSE, > barplot(x, space = 0, axes = FALSE,
+ col = fun(n)) + col = fun(n))
wichtig
eher wichtig
0.8
teils/teils
eher nicht wichtig
0.6
nicht wichtig
0.4
0.2
0.0
Gr. 1 Gr. 2
Wir übergeben colorRampPalette() die Farben "blue", "white" und "red" und
erhalten die Funktion palette(), aus der wir in barplot() genau nrow(M) viele
Farben herausziehen.
35 Standardgrafiken und Farben 557
Mit Hilfe der Spaltenprozente haben wir jede x-Achsen-Kategorie auf 100 Prozent
aufgepumpt. Dadurch können wir die relativen Anteile der Gruppen besser verglei-
chen und wir sehen: Für Gruppe 1 ist Statistik tendenziell wichtiger.
Mit legend.text erzeugen wir bequem eine hübsche Legende. Weitere Parameter
für die Legende können wir mittels args.legend spezifizieren.
Da die Legende die Balken standardmäßig überdeckt, müssen wir die x-Achse än-
dern. Eine allgemeine Lösung würde den Rahmen sprengen, daher stellen wir das
Zeichenintervall der x-Achse per Hand auf xlim = c(0, 6) ein. Es ist sehr gut
möglich, dass du bei dir andere Werte einstellen musst, damit es gut aussieht (zum
Beispiel xlim = c(0, 4)). Wenn dir das nicht allgemein genug ist, darfst du dich
sehr gerne etwa mit dem Grafikparameter par()$usr befassen ;-)
Wir wollen für die Variable Petal.Length des Iris-Datensatzes (siehe zum Beispiel
Infobox 13 auf Seite 506) ein Histogramm erstellen, wobei die drei Spezies farblich
unterscheidbar sind. Dabei machen wir uns die Techniken von (35.1.4) zunutze.
Die Farben wählen wir aus dem HCL-Farbraum (siehe (35.2.2)), damit die drei
Farbintensitäten zusammenpassen. Jetzt erstellen wir ein Histogramm, wobei wir es
wegen plot = FALSE nicht zeichnen.
> class(hist.info)
[1] "histogram"
558 I Visualisierung von Daten
Histogram of iris$Petal.Length
30
Frequency
20
10
0
1 2 3 4 5 6 7
iris$Petal.Length
Für Objekte der Klasse histogram gibt es eine maßgeschneiderte plot-Funktion, die
intern beim Aufruf von plot() aufgerufen wird. Diese verarbeitet die Informationen,
welche uns hist() zurückgibt.
Jetzt zu den Farben. Mit folgendem Griff in die R-Trickkiste kommen wir ans Ziel:
1. Wir erstellen getrennt für jede Spezies ein Histogramm und speichern uns die
zurückgegebenen Informationen. Dabei übergeben wir jeweils jene Breaks, die
wir oben erhalten haben. Damit ist sichergestellt, dass die Intervalle der drei
Histogramme zusammenpassen.
2. Die Häufigkeitsinformationen (counts) der drei Listen ordnen wir zu einer
Matrix an. Mit dieser Matrix erstellen wir schließlich ein Balkendiagramm.
> # 1.) Histogramm für jede Gruppe berechnen (mit denselben Breaks!)
> temp <- tapply(iris$Petal.Length, iris$Species, function(u) {
+ hist(u, breaks = hist.info$breaks, plot = FALSE)
+ })
Mit space = 0 stellen wir ein, dass die Balken horizontal ohne Zwischenraum nahtlos
aneinanderhängen.
setosa
versicolor
virginica
Vielleicht fragst du dich, warum wir die Farben für die Legende dunkler gemacht
haben. Die Antwort: Damit die Farben besser zusammenpassen. Denn auf großen
Flächen (hier die Balken) wirken Farben tendenziell dunkler. Wer schon mal Wände
bemalt hat und sich gewundert hat, dass die Farbe auf der kleinen Farbpalette viel
heller gewirkt hat als auf der großen Wand, der weiß, wovon die Rede ist ;-)
Jetzt wollen wir noch gerne Achsen und Beschriftungen einzeichnen.
35
setosa setosa
versicolor versicolor
30
30
virginica virginica
25
25
Anzahl
Anzahl
20
20
15
15
10
10
5
5
0
0 2 4 6 8 10 12 1 2 3 4 5 6 7
cm cm
Die linke Grafik zeigt das Ergebnis nach Ausführung des obigen Codes. Uns fällt auf,
dass die x-Achse nicht zu den Daten passt; sie sollte vielmehr so aussehen, wie in der
rechten Grafik! Das liegt daran, dass in barplot() die Balkenbreite standardmäßig
auf 1 eingestellt ist und der erste Balken bei x = 0 beginnt. Laut den Breaks sollte
aber ein Balken nur 0.5 breit sein.
560 I Visualisierung von Daten
Ein neuerlicher Griff in die R-Trickkiste sorgt dafür, dass die Werte auf der x-Achse
zu den Daten passen, wie im rechten Bild:
• x-Achse verschieben, sodass der erste Balken an der korrekten Stelle beginnt.
Wir finden also mit Hilfe von pretty() geeignete Stellen für die x-Achse. Diese sind
aber noch nicht korrekt beschriftet, da der erste Balken bei 1 beginnen soll. Die
Addition von hist.info$breaks[1] löst das Problem.
Implizit nehmen wir in obigem Code an, dass die Intervalle äquidistant sind, was
in der Praxis in der Regel der Fall ist. Wenn sich die Klassenbreiten unterscheiden,
so müssen wir den Code modifizieren. Engagierte R-Nachwuchstalente sind herzlich
dazu eingeladen, sich über geeignete Adaptierungen Gedanken zu machen ;-)
35.4 Abschluss
• Wie gewährleisten wir bei Verwendung von plot() und lines(), dass alle
Linien zur Gänze abgebildet werden? Mit welchen Funktionen erstellen wir
parallele Liniengrafiken bzw. zeichnen mehrere Linien gleichzeitig in eine Grafik
ein und wie funktioniert das? (35.1.1)
• Wie erstellen wir ein einfaches Balkendiagramm? Wie erstellen wir gruppierte
und gestapelte Balkendiagramme? Wie erfolgt jeweils die Farbzuteilung zu den
Balken? (35.1.2)
35 Standardgrafiken und Farben 561
• Wie erstellen wir Histogramme? Wie schalten wir dabei zwischen absoluten
und relativen Häufigkeiten um? Wie können wir die Balken des Histogramms
umfärben oder/und schraffieren? (35.1.3)
• Mit welcher Einstellung verhindern wir in gängigen Standardgrafiken, dass die
Grafik gezeichnet wird? Wie können wir Rückgaben von Grafikfunktionen spei-
chern? Wofür stehen bei der Rückgabe der Funktion hist() die Listenelemente
breaks, counts, density und mids? Wie setzen wir diese Informationen dazu
ein, um einen bestimmten Balken hervorzuheben? Wie stellen wir sicher, dass
eine darüber gezeichnete Dichtekurve zur Gänze gezeigt wird (unter Verwen-
dung der Informationen von hist() und zum Beispiel ylim)? (35.1.4)
• Wie generieren wir einfache und gruppierte Boxplots? Wie färben wir die Boxen
um? (35.1.5)
• Wie sind RGB-Farben definiert und wie erstellen wir RGB-Farben in R? Was
steuert insbesondere der Parameter maxColorValue? Welche Standardeinstel-
lung hat er und welcher Wert ist in der Praxis üblich? (35.2.1)
• Wie sind HCL-Farben definiert und wie erstellen wir HCL-Farben in R? Wel-
chen Vorteil bieten HCL-Farben gegenüber vielen anderen Farbpaletten? Wie
steuern wir insbesondere den Grauanteil und die Helligkeit der Farben? Wie
müssen wir den Farbton (hue) ungefähr einstellen, damit wir eine rötliche,
grünliche bzw. bläuliche Farbe erhalten? (35.2.2)
• Welche Standardfarbpaletten gibt es unter anderem in R? (35.2.3)
• Wie erzeugen wir mit Hilfe von colorRampPalette() eigene Farbpaletten?
Was gibt uns diese Funktion zurück und wie können wir mit Hilfe dieses zu-
rückgegebenen Objektes eine bestimmte Anzahl an Farben aus unserer Palette
herausziehen? (35.2.4)
35.4.2 Ausblick
Nachdem wir die Welt der Farben erkundet haben, geht es in (36) um das Thema
Layout. Dort lernen wir, wie wir mehrere Plots ansprechend in einer Grafik anordnen
können. In (37.1.2) führen wir mit dem QQ-Plot eine weitere Standardgrafik ein.
35.4.3 Übungen
1. Wir betrachten die Gammaverteilung (siehe (9.3)). Wähle mindestens drei Pa-
rameterkonstellationen aus und zeichne die entsprechenden Dichtekurven in
einer Grafik gut unterscheidbar ein. Wähle ein sinnvolles Intervall für die x-
Achse (und ggf. auch für die y-Achse) und zeichne an geeigneter Stelle eine
Legende ein, die uns über die Parametereinstellungen informiert. Idealerweise
soll auch ein Gitternetz hinter den Dichtekurven eingezeichnet sein.
562 I Visualisierung von Daten
2. Modifiziere den Code des Beispiels ab Seite 546 derart, dass die Dichtefunkti-
on immer (also auch für andere Zufallszahlen) zur Gänze sichtbar ist. Färbe
zusätzlich jenen Balken, der am höchsten ist, in einer dezenten Farbe deiner
Wahl ein. Sollte es mehrere gleich hohe höchste Balken geben, färbe all diese
Balken ein.
3. Positioniere die Legende im Nationalratswahlenbeispiel ab Seite 538 oben in
der Grafik, sodass sie keine Linie überdeckt und möglichst viel Platz für die
Liniengrafik übrig bleibt.
Hinweis: Überlege dir, wie du in der oberen Region der Grafik mehr Platz
schaffen kannst und wie du die Legende derart einpassen kannst, dass möglichst
wenig Raum auf der y-Achse in Anspruch genommen wird.
4. Mehrere Studierende der Ernährungswissenschaften wurden befragt, für wie
gefährlich sie gentechnisch veränderte Lebensmittel halten. Die Antwortmög-
lichkeiten (inkl. Anzahl der abgegebenen Antworten in Klammern) waren:
• Sehr gefährlich (3)
• Gefährlich (10)
• Teils/teils (6)
• Wenig gefährlich (2)
• Ungefährlich (4)
a) Erstelle ein einfaches Balkendiagramm, das uns Aufschluss über die Ein-
schätzung der Studierenden gibt. Versuche dabei die Grafik nach Möglich-
keit derart zu gestalten, dass alle 5 Kategorien auf der x-Achse angezeigt
werden und nicht allzu viel Platz verloren geht.
Hinweis: Die x-Achse ist eine Spielerei. Eine Möglichkeit: cex.names
in barplot() variieren oder/und Zeilenumbrüche in die Kategorienamen
einfügen. Eine andere Möglichkeit: Erstelle zwei separate versetzte x-
Achsen. Unter anderem könnte dich (34.3.6) dabei inspirieren. Frage dich,
ob dir barplot() evtl. die Mittelpunkte der Balken zurückgibt ;-)
Hinweis: Sollte sich die versetzte Achse unten nicht mehr ausgehen,
dann schau dir zum Beispiel den Grafikparameter mar von par() an.
b) Erstelle jetzt ein gestapeltes Balkendiagramm. Wähle für die Kategorien
passende Farben aus einer Palette, die den ordinalskalierten Charakter
widerspiegelt. Zeichne eine Legende ein, die uns über die Kategorien ad-
äquat Aufschluss gibt und die keine Balken überdeckt.
5. Eine Aufgabe für engagierte R-Talente, bei der wir das Thema effizientes Pro-
grammieren (siehe (33)) wiederholen und eine hübsche Grafik zeichnen. Gege-
ben ist eine Folge (ein zeitdiskreter stochastischer Prozess) f (t):
(
f (t − 1) + xt für t > 0
f (t) =
0 für t = 0
35 Standardgrafiken und Farben 563
Dabei seien die xt unabhängig und identisch verteilte Realisierungen einer stan-
dardnormalverteilten Zufallsvariable. Die Folge hat also den Startwert f (0) = 0
und in jedem Zeitschritt t ∈ {1, 2, . . . , T } wird eine (neue) standardnormalver-
teilte Zufallszahl hinzuaddiert, wobei wir T ∈ N Zeitschritte betrachten.
20
20
0
f(t)
f(t)
0
−20
−20
−40
−40
0 20 40 60 80 100 0 20 40 60 80 100
t t
• Grafiken abspeichern
In (34.3.2) haben wir ein Streudiagramm mit den beiden Variablen Petal.Length
und Petal.Width des Iris-Datensatzes (siehe Infobox 13 auf Seite 506) erstellt. Wir
wollen aus der Grafik zusätzlich die Randverteilungen für beide Variablen ablesen
können, wahlweise als Balkendiagramme (analog zu (35.3.1)) oder Dichteschätzer.
Die Erstellung einer solchen Grafik stellt den krönenden Abschluss dieses Kapitels
in (36.4.1) dar.
Bis es soweit ist, brauchen wir noch ein paar Vorleistungen, damit unsere Grafik am
Ende richtig toll wird. Die Fragen zu diesen Vorleistungen lauten:
• Wie stellen wir die äußeren Ränder unserer Plots ein? (36.3.1)
• Wie können wir das Grafikfenster unterteilen und wie definieren wir eigene
Layouts? (36.3.2), (36.3.4)
Wie das Abspeichern von Grafiken funktioniert, sehen wir uns in (36.2) an. Dabei
listen wir auch gängige Grafikdateiformate auf und lernen den Unterschied zwischen
Rastergrafiken und Vektorgrafiken kennen.
Ganz zu Beginn befassen wir uns in (36.1) mit Grafikfenstern (Devices).
Mit folgendem Aufruf können wir die Ausmaße des Grafikfensters umstellen.
dev.new(height, width)
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_36
36 Grafikfenster und Layout 565
Die Parameter height und width werden indirekt über das Dreipunkteargument
angesprochen und müssen daher beim Funktionsaufruf exakt benannt werden. Die
Höhe und Breite wird in inches angegeben; 1 inch entspricht 2.54 cm.
> # Neues Device mit 7 x 7 inches > # Neues Device mit 3 x 8 inches
> dev.new() > dev.new(height = 3, width = 8)
Jedes Device hat seine eigenen par()-Einstellungen. Wird ein neues Device geöff-
net, so werden also die standardmäßigen par()-Einstellungen verwendet. Zumindest
solange, bis wir sie manuell umstellen, wie wir es in (34.2) gelernt haben.
Gezeichnet und geplottet wird immer nur im aktiven Device. Ob ein Device aktiv
ist, sehen wir in der Titelleiste des Grafikfensters: Sie zeigt entweder „active“ oder
„inactive“ an.
Ein neu geöffnetes Device wird automatisch aktiv und gegebenenfalls das derzeit
aktive Device inaktiv. Wenn es kein aktives Device gibt, öffnen Grafikfunktionen
wie plot() automatisch ein neues Device.
Mit der Funktion dev.cur() erfragen wir den Index des aktiven Device und mit
dev.set(which) aktivieren wir den Device mit dem Index which. Den Index des
Devices sehen wir ebenfalls in der Titelleiste des Grafikfensters.
Mit dem Aufruf dev.off() können wir das aktive Grafikfenster schließen und
mit dev.off(which) wird das Fenster mit dem Index which geschlossen. Wollen wir
alle Grafikfenster schließen, so schreiben wir graphics.off().
• Bitmap (.bmp)
• JPEG (Joint Photographic Experts Group, .jpg bzw .jpeg)
• PNG (Portable Network Graphics, .png)
• TIFF (Tagged Image File Format, .tif bzw. .tiff)
566 I Visualisierung von Daten
Im Gegensatz dazu setzen sich die Bilder einer Vektorgrafik aus einfachen geome-
trischen Strukturen (Linien, Kreise, Polygone, Kurven) zusammen. Bei der Darstel-
lung auf einem Computerbildschirm werden Vektorgrafiken gerastert, also in eine
Rastergrafik übersetzt, wobei die Farbwerte der Bildpunkte aus den geometrischen
Strukturen berechnet werden. Vertreterinnen dieser Gruppe sind beispielsweise:
• PostScript (.ps)
• Encapsulated PostScript (.eps)
• Windows Metafile (.wmf) – für Office Anwendungen (Word, Excel etc.)
Zum Speichern von Grafiken steht uns die Funktion savePlot() zur Verfügung:
savePlot(filename = "Rplot",
type = c("wmf", "emf", "png", "jpg", "jpeg", "bmp",
"tif", "tiff", "ps", "eps", "pdf"),
device = dev.cur(), restoreConsole = TRUE)
Für filename übergeben wir den Dateinamen, gegebenenfalls inklusive des Pfades
(siehe (5)). Übergeben wir nur den Dateinamen, so wird die Grafik im aktuellen
Arbeitsverzeichnis abgespeichert. Mit type stellen wir das Dateiformat ein.
Wir demonstrieren den Unterschied zwischen Rastergrafiken und Vektorgrafiken und
führen dabei savePlot() ein.
In Abb. 36.1 stellen wir die eben erzeugte Rastergrafik und Vektorgrafik dar: einmal
in Originalgröße und einmal vergrößert.
Scharf mit
Vektorgrafik
Abbildung 36.1: Rastergrafiken und Vektorgrafiken nach einer Skalierung. Oben:
Die Bilder in Originalgröße. Unten: Nach der Verdoppelung bei-
der Bilddimensionen ist die Rastergrafik (links) unscharf, wäh-
rend die Vektorgrafik (rechts) nach wie vor scharf ist.
Wie wir mehrere Grafiken in ein Grafikfenster packen, schauen wir uns in (36.3.2)
und (36.3.4) an. Damit die Grafiken besser zueinander passen, müssen wir oft an
den Grafikrändern schrauben, was wir uns in (36.3.1) ansehen.
Mit dem Parameter mar der Funktion par() können wir die äußeren Grafikrän-
der verändern. Wir übergeben einen vierelementigen Vektor mit den gewünschten
Abständen, wobei wir unten beginnen und gegen den Uhrzeigersinn vorgehen.
Die Abstände werden in Anzahl der Zeilen definiert, wobei eine Zeile 0.2 inches misst.
Titel
Titel
10
8
10
6
0:10
8
4
6
2
4
0
0 2 4 6 8 10
0
0:10
0 2 4 6 8 10
Damit wir die Unterschiede besser erkennen, sind beide Grafiken mit einer äußeren
Box (nicht in R gezeichnet) versehen. Der obere Rand ist deutlich größer, während
der untere und linke Rand schmäler sind. Insbesondere sind diese beiden Ränder so
schmal, dass sich die Achsenbeschriftungen nicht mehr ausgehen. Der rechte Rand
entfällt komplett.
Äquivalent zu mar ist der Parameter mai, mit dem wir die Abstände der Ränder in
inches definieren können.
Oftmals möchten wir mehrere Bilder in ein Device packen. Im einfachsten Fall
können wir dies mit dem Parameter mfrow der Funktion par() umsetzen.
Mit a stellen wir die Anzahl der Zeilen und mit b die Anzahl der Spalten
ein. Die einzelnen Grafiken werden zeilenweise von links oben nach rechts unten
eingezeichnet und jede Zelle ist gleich groß.
Beispiel: Stelle alle vier numerische Variablen des Iris-Datensatzes als Histogramm
und Boxplot übersichtlich in einem Device dar.
36 Grafikfenster und Layout 569
Mit mfrow = c(2, length(iris.num)) legen wir fest, dass das Device in 2 Zeilen
und 4 Spalten unterteilt wird.
30
30
30
25
25
25
20
Frequency
Frequency
Frequency
Frequency
20
20
20
15
15
15
10
10
10
10
5
5
5
0
4 5 6 7 8 2.0 2.5 3.0 3.5 4.0 1 2 3 4 5 6 7 0.0 0.5 1.0 1.5 2.0 2.5
cm cm cm cm
2.5
7
7.5
4.0
2.0
7.0
5
3.5
6.5
1.5
4
6.0
3.0
1.0
5.5
3
2.5
5.0
0.5
2
4.5
2.0
Mit der Funktion pairs() können wir paarweise Streudiagramme erstellen. Wir
übergeben der Funktion ein Dataframe und pairs() kümmert sich hingebungsvoll
um alles.
Beispiel: Erstelle für alle numerischen Variablen des Iris-Datensatzes paarweise
Streudiagramme in einem Device. Die drei Spezies sollen unterscheidbar sein.
Sepal.Length
5.5
4.5
4.0
Sepal.Width
3.0
2.0
7
6
5
Petal.Length
4
3
2
1
2.5
1.5
Petal.Width
0.5
Mit paarweisen Streudiagrammen verschaffen wir uns einen sehr guten und schnellen
Überblick über die Daten. Die Funktion pairs() ist vielseitig und es lohnt sich,
die R-Hilfe (?pairs) zu studieren, unter anderem auch die dortigen Beispiele! Wir
erläutern ganz kurz, welche Wünsche wir uns mit pairs() erfüllen können.
Die Abstände zwischen den Grafikzellen können wir mit dem Parameter gap
umstellen. Er ist standardmäßig auf 1 eingestellt.
36 Grafikfenster und Layout 571
Wir können auch steuern, was wir in die Diagonalzellen und Nebendiago-
nalzellen zeichnen wollen. Dazu stehen uns die Parameter diag.panel, panel,
lower.panel und upper.panel zur Verfügung, denen wir eigene Zeichenfunktionen
übergeben können.
So könnten wir uns beispielsweise in der Diagonale zusätzlich Histogramme wün-
schen. Das schauen wir uns in Übung 1 auf Seite 586 an.
Bemerkung: Mit dem Wissen der vorherigen Abschnitte hätten wir auch ohne
pairs() paarweise Streudiagramme erstellen können. Sei n die Anzahl der darzu-
stellenden Variablen. Dann können wir das Vorhaben schematisch so realisieren:
1. Unterteile das Device in n Zeilen und n Spalten.
par(mfrow = c(n, n))
2. Zeichne die Grafiken mit einer doppelten for-Schleife ein.
In der Diagonale schreiben wir die Variablennamen auf, abseits der Diagona-
le zeichnen wir die jeweiligen Streudiagramme. Mit dem Parameter mar von
par() können wir die Ränder passend einstellen.
Was können wir tun, wenn wir ein Device in unterschiedlich große Bereiche
unterteilen wollen? Mit dem Parameter mfrow kommen wir leider nicht weit, da
wir das Device so lediglich in gleich große Bereiche unterteilen können. Stattdessen
bietet sich die Funktion layout() an:
Parameter Bedeutung
mat Matrix; steuert die Anzahl der Zeilen und Spalten des Devices
sowie die Reihenfolge, in der die Grafiken in die Zellen eingefügt
werden (mit Indizes).
widths Numerische Werte geben die relativen Höhen an.
Werte von lcm() geben feste Höhen in cm an.
heights analog zu widths für die Breite
respect TRUE: Jede Zelle ist quadratisch.
572 I Visualisierung von Daten
Das aktuelle Device wird durch layout() redefiniert. Daneben stehen uns die beiden
nützlichen Hilfsfunktionen layout.show() und lcm() zur Verfügung:
layout.show(n = 1)
lcm(x)
Mit layout.show(n) können wir uns das Layout ansehen, wobei wir mit n die Anzahl
der anzuzeigenden Zellen einstellen. lcm(x) wandelt einen Zahlenvektor x in Strings
der Form "x cm" um.
Wir werden schrittweise Bekanntschaft mit layout() machen.
Beispiel: Setze die Aufgabe ab Seite 568 mit Hilfe von layout() um.
Die Matrix M enthält alle Informationen zur Einfügereihenfolge der Grafiken. Die
Grafiken werden zeilenweise (von 1 = links oben bis 8 = rechts unten) eingezeichnet.
1 2 1 2 3 4
5 6 7 8
Jetzt können wir das Device mit Leben befüllen. Zur Demonstration zuerst mit
Stichproben einer Standardnormalverteilung ...
36 Grafikfenster und Layout 573
25
20
25
20
20
20
15
15
Frequency
Frequency
Frequency
Frequency
15
15
10
10
10
10
5
5
5
5
0
0
−2 −1 0 1 2 −2 −1 0 1 2 −3 −2 −1 0 1 2 3 −3 −2 −1 0 1 2
x x x x
20
30
30
25
15
15
25
20
Frequency
Frequency
Frequency
Frequency
20
10
10
15
15
10
10
5
5
5
5
0
−4 −2 0 2 4 −3 −2 −1 0 1 2 −3 −2 −1 0 1 2 3 −3 −2 −1 0 1 2
x x x x
... und jetzt mit unseren Iris-Daten. Beachte dabei, dass das Layout von obigem Nor-
malverteilungsbeispiel übernommen und mit unseren Histogrammen und Boxplots
überzeichnet wird.
35
35
30
30
30
30
25
25
25
20
Frequency
Frequency
Frequency
Frequency
20
20
20
15
15
15
10
10
10
10
5
5
5
0
0
4 5 6 7 8 2.0 2.5 3.0 3.5 4.0 1 2 3 4 5 6 7 0.0 0.5 1.0 1.5 2.0 2.5
cm cm cm cm
2.5
7
7.5
4.0
2.0
7.0
5
3.5
6.5
1.5
4
6.0
3.0
1.0
5.5
3
2.5
5.0
0.5
2
4.5
2.0
Unserer Fantasie sind praktisch keine Grenzen gesetzt! Schauen wir uns an, wie wir
Zellen miteinander verbinden bzw. leer lassen können.
2 3 2 3
1 1
4
Wir können benachbarte Zellen verknüpfen, indem wir diesen Zellen denselben
Index zuweisen. Hier erstreckt sich der 1. Bereich über die ganze erste Spalte und
der 4. Bereich über die 2. und 3. Spalte der zweiten Zeile. Zellen, die mit einer 0
markiert sind, bleiben leer.
36 Grafikfenster und Layout 575
1 4 7
1 4 7
2 5 8
2 5 8
3 6 9
3 6 9
Mit widths = c(1, 2, 4) bestimmen wir, dass jede Spalte doppelt so breit sein soll
wie die vorangehende. Im rechten Code gilt wegen heights = c(1, 2, 4) dasselbe
für die Zeilen.
Zu guter Letzt betrachten wir, wie wir Höhen und Breiten fixieren können.
1 4 7 1 4 7
2 5 8 2 5 8
3 6 9 3 6 9
Im linken Code fixieren wir mit lcm() alle Spaltenbreiten. Die Spalten messen genau
2, 3 bzw. 4 cm, egal, wie groß das Device ist. Sollte das Device zu klein sein, so gibt
R eine Warnmeldung aus:
Warnung: Displayliste unvollständig neugezeichnet
576 I Visualisierung von Daten
Die Zeilen werden wie gehabt den Zahlen c(1, 2, 4) gemäß proportional aufgeteilt,
sodass die ganze Devicehöhe ausgenützt wird.
Wir können aber auch einzelne Zeilen oder Spalten fixieren. Im rechten Code
fixieren wir nur die zweite Spalte (2 cm Breite) und die zweite Zeile (3 cm Breite).
Der Effekt der Fixierungen wird besonders dann ersichtlich, wenn wir das Device
skalieren: Die fixierten Zeilen bzw. Spalten behalten ihre Höhe bzw. Breite; der
restliche Platz wird proportional auf die anderen Zeilen bzw. Spalten verteilt.
Mit eigenen Layouts können wir tolle Dinge anstellen. In (36.4.1) etwa zeichnen wir
ein Streudiagramm inkl. Darstellung der Randverteilungen.
setosa setosa
versicolor versicolor
virginica virginica
2.5 2.5
2.0 2.0
Petal.Width
Petal.Width
1.5 1.5
1.0 1.0
0.5 0.5
1 2 3 4 5 6 7 1 2 3 4 5 6 7
Petal.Length Petal.Length
Wir wollen also für den Iris-Datensatz ein Streudiagramm mit den beiden Variablen
Petal.Length und Petal.Width erstellen. Anders als im Beispiel ab Seite 511 wollen
wir zusätzlich die Randverteilungen einzeichnen. Überlegen wir uns zunächst grob
die nötigen Schritte, um unser Ziel zu erreichen:
1. Wir unterteilen die Grafik mit layout() aus (36.3.4) in vier Teilbereiche.
2. Wir zeichnen das Streudiagramm, die Randverteilungen und die Legende ein.
3. Dabei passen wir auf, dass alle Grafiken passende äußere Ränder haben, damit
die Randverteilungen perfekt zum Streudiagramm passen (vgl. (36.3.1)).
36 Grafikfenster und Layout 577
Innerhalb von layout() stellen wir mit widths = c(5, 2) bzw. heights = c(2,
5)) ein, dass das Streudiagramm 5/7 der Gesamtbreite bzw. der Gesamthöhe ein-
nehmen soll. Mit der Einstellung mgp = c(2, .5, 0) stellen wir die Abstände von
den Achsenbeschriftungen zur Grafik adäquat ein bzw. rücken sie etwas näher her-
an (mgp = c(3, 1, 0) ist Standard). Und mit las = 1 bestimmen wir, dass die
Beschriftungen der Achsenhäkchen immer aufgestellt sind.
Die farblichen Histogramme zeichnen wir etwas heller ein. Den Grund dafür haben
wir in (35.3.1) erläutert.
Schauen wir uns den Zwischenstand an! Der obere und rechte Bereich ist noch leer,
das ändert sich aber schon sehr bald.
2.5
2.0
Petal.Width
1.5
1.0
0.5
1 2 3 4 5 6 7
Petal.Length
578 I Visualisierung von Daten
Jetzt vollenden wir das Ganze. Um die Idee hinter unserem Layout zu verdeutlichen,
zeichnen wir zunächst die Randverteilung in einer Farbe ein.
Bei der oberen Randverteilung stellen wir den linken Rand auf 4, damit wir denselben
linken Rand wie beim Streudiagramm haben und den unteren Rand auf 0, damit der
vertikale Abstand zum Streudiagramm kleiner ist. Dieselbe Überlegung gilt bei der
rechten Randverteilung. Hier sorgen wir zusätzlich mit horiz = TRUE dafür, dass die
Balken niedergelegt werden. Die Histogramme zeichnen wir dabei mit barplot()
ein. Dabei greifen wir auf die Techniken von (35.1.4) zurück.
setosa
versicolor
virginica
2.5
2.0
Petal.Width
1.5
1.0
0.5
1 2 3 4 5 6 7
Petal.Length
Sieht schon recht hübsch aus! Jetzt verallgemeinern wir das Ganze :-)
36 Grafikfenster und Layout 579
Neben einem Histogramm (mit oder ohne Farbinformation) soll es auch möglich
sein, die Randdichten einzuzeichnen. Dazu schreiben wir drei Hilfsfunktionen. Die
erste (zeichne.bar()) kümmert sich um die Histogramme.
Übergeben wir nichts für gruppe, so zeichnen wir ein einfärbiges Balkendiagramm.
Andernfalls färben wir die Balken gemäß der Gruppe ein, was wir in (35.3.1) be-
reits gemacht haben. Kernstück dabei: Wir berechnen getrennt für jede Gruppe ein
Histogramm mit denselben Breaks. Testen wir unsere erste Hilfsfunktion.
> zeichne.dichte <- function(x, gruppe, horiz = FALSE, kernel = FALSE, col) {
+ # Hilfsfunktion für die Dichtegrafiken.
+ # kernel ... FALSE: Es wird Normalverteilung angenommen.
+ # TRUE: Es werden Kerndichteschätzer berechnet.
+
+ # 1.) Vorbereitungen
+ lim <- range(x)
+ xx <- seq(lim[1], lim[2], length = 201)
+ Y <- matrix(NA, nrow = length(xx), ncol = nlevels(gruppe))
+
+ # 2.) y-Werte der Dichtekurven berechnen.
+ for (i in 1:nlevels(gruppe)) {
+ temp <- x[gruppe == levels(gruppe)[i]]
+
+ if (kernel)
+ # Kerndichteschätzer bestimmen
+ Y[, i] <- density(temp, from = lim[1], to = lim[2], n = length(xx))$y
+ else
+ # Dichteschätzer unter Normalverteilungsannahme bestimmen
+ Y[, i] <- dnorm(xx, mean(temp, na.rm = TRUE), sd(temp, na.rm = TRUE))
+ }
+
+ # 3.) Dichtekurven zeichnen.
+ if (horiz)
+ matplot(Y, xx, col = col, type = "l", lwd = 2, lty = 1,
+ axes = FALSE, las = 1, xlab = "", ylab = "")
+ else
+ matplot(xx, Y, col = col, type = "l", lwd = 2, lty = 1,
+ axes = FALSE, las = 1, xlab = "", ylab = "")
+ }
Wir erstellen in Teil 1 die Matrix Y, wobei jede Spalte am Ende die geschätzten
Dichtewerte einer Spezies (Gruppe) enthält. Die Auflösung stellen wir dabei auf 201
Punkte ein und die Dichtekurven ragen später für alle Spezies vom Minimum bis
zum Maximum von x (gespeichert im Vektor lim).
Im 2. Teil berechnen wir getrennt für jede Gruppe die Dichtekurve und weisen die
Ergebnisse den Spalten von Y zu. Ein Ansatz mit tapply() scheitert, da wir Y
innerhalb von tapply() nicht manipulieren können (vgl. (30.1)). Daher gehen
wir in einer for-Schleife die Gruppen durch.
Für kernel = TRUE wird ein Kerndichteschätzer3 bestimmt. Mit density() berech-
nen wir die y-Werte eines Kerndichteschätzers zu den Daten der jeweiligen Gruppe
(temp) im Intervall [lim[1], lim[2]]. Für kernel = FALSE wird mit dnorm() die
Dichtekurve unter Annahme der Normalverteilung geschätzt (siehe (9.2)), wobei der
Mittelwert und die Standardabweichung aus temp geschätzt werden.
3 Siehe etwa https://de.wikipedia.org/wiki/Kerndichteschätzer (abgerufen am 06.10.2019)
36 Grafikfenster und Layout 581
Abschließend zeichnen wir in Teil 3 mit matplot() (siehe (35.1.1)) die Dichtekurven
ein. Wenn wir die rechte Randverteilung einzeichnen wollen (horiz = TRUE), dann
müssen wir die Rollen von xx und Y vertauschen.
Testen wir auch unsere zweite Hilfsfunktion.
Unsere dritte Hilfsfunktion plot.auswahl() kümmert sich darum, die richtige Gra-
fik zu zeichnen. Mit dem Parameter type bestimmen wir, wie die Randverteilun-
gen dargestellt werden sollen: Dabei steht "bar" für einfärbige Balkendiagramme,
"bar.col" für mehrfärbige Balkendiagramme, "normal" für Normalverteilungskur-
ven und "kernel" für Kerndichteschätzer. In switch() (siehe (29.4.4)) rufen wir
die passende Funktion auf.
Kommen wir jetzt zu unserer Hauptfunktion plot.streu()! In Abb. 36.2 ist die
fertige Funktion aufgeschrieben und in Abb. 36.3 können wir unsere Grafiken be-
wundern.
582 I Visualisierung von Daten
setosa setosa
versicolor versicolor
virginica virginica
2.5 2.5
2.0 2.0
Petal.Width
Petal.Width
1.5 1.5
1.0 1.0
0.5 0.5
1 2 3 4 5 6 7 1 2 3 4 5 6 7
Petal.Length Petal.Length
Normalverteilungsdichte Kerndichteschätzer
> plot.streu(Petal.Length, > plot.streu(Petal.Length,
+ Petal.Width, Species, + Petal.Width, Species,
+ type = "normal", + type = "kernel",
+ hue = hue, pch = pch) + hue = hue, pch = pch)
setosa setosa
versicolor versicolor
virginica virginica
2.5 2.5
2.0 2.0
Petal.Width
Petal.Width
1.5 1.5
1.0 1.0
0.5 0.5
1 2 3 4 5 6 7 1 2 3 4 5 6 7
Petal.Length Petal.Length
Beim ersten print()-Befehl wird der rohe Ausdruck 3 * 4 gedruckt. Das liegt an der
Lazy Evaluation: Ausdrücke werden erst ausgewertet, wenn sie gebraucht werden.
Nachdem wir x überschrieben haben, haben wir keinen Zugriff mehr auf 3 * 4.
Die Funktion ist noch lange nicht perfekt. Einige mögliche Erweiterungen:
Das Layout hat 3 Zeilen, wobei sich die erste Zeile (Position 5) über beide
Spalten erstreckt und eine feste Höhe (für einen geeigneten Wert k) bekommt.
Jetzt können wir mit plot() und text() die Überschrift einzeichnen.
• Die Funktion könnte flexibler parametrisiert sein.
Wir könnten etwa das Dreipunkteargument einbauen, um Parameter wie pch,
xlab etc. zu übergeben. (29.4.5) könnte diesbezüglich inspirieren.
• Die Farbgebung ist an den HCL-Farbraum gebunden.
Wir könnten eine Funktion einbauen, die uns Farben heller / dunkler macht.
Einen Ansatz haben wir in (35.1.2) im Beispiel ab Seite 542 besprochen.
• Es werden fehlerhafte Eingaben nicht abgefangen.
Ist x ein numerischer Vektor? Ist gruppe tatsächlich ein Faktor derselben Länge
wie x? Fragen wie diesen sollte idealerweise nachgegangen und ggf. sollten
Warnungen und Fehlermeldungen ausgegeben werden.
• Es wird auf Objekte außerhalb der Funktion zugegriffen.
Die Hilfsfunktionen plot.bar(), plot.dichte() und plot.auswahl() sind
nicht innerhalb der Funktion plot.streu() definiert und werden auch nicht als
Parameter übergeben. Werden die Hilfsfunktionen überschrieben, funktioniert
plot.streu() nicht mehr korrekt (vgl. (29.6)).
36 Grafikfenster und Layout 585
36.5 Abschluss
• Wie öffnen wir ein neues Grafikfenster? Wie bestimmen wir die Höhe und
Breite eines neuen Grafikfensters? (36.1.1)
• Wie aktivieren wir ein bestimmtes Grafikfenster? (36.1.2)
• Wie können wir das aktive, ein bestimmtes bzw. alle Grafikfenster schließen?
(36.1.3)
• Wie werden Bildinformationen bei Rastergrafiken und Vektorgrafiken intern
verwaltet? Welche Konsequenz ergibt sich daraus, wenn wir Bilder beider Gra-
fiktypen vergrößern? Wie speichern wir Grafiken in einem bestimmten Datei-
format ab? (36.2)
• Wie stellen wir die äußeren Grafikränder ein? (36.3.1)
• Wie nehmen wir eine einfache Fensterteilung mit Hilfe von par() und mfrow
vor? (36.3.2)
• Was macht die Funktion pairs() und wie wenden wir sie an? (36.3.3)
• Wie können wir mit Hilfe von layout() ein Grafikfenster in Zellen unterteilen?
Wie bestimmen wir, in welcher Reihenfolge die Zellen befüllt werden? Wie
können wir Zellen miteinander verbinden? Wie werden die Zeilenhöhen und
Spaltenbreiten bestimmt? Wie fixieren wir die Breite einer Spalte bzw. die
Höhe einer Zeile? (36.3.4)
36.5.2 Ausblick
Alleine zum Thema Grafik könnte man ganze Bücher schreiben und es gibt noch
viel Spannendes zu entdecken! Schau dir die par()-Einstellungen in Ruhe an. Auch
die Dokumentationen zu den Paketen graphics (help(package = graphics)) bzw.
grDevices (help(package = grDevices)) sind spannend, du findest dort weitere
Funktionen für Standardgrafiken. Ein Beispiel sind Mosaicplots (mosaicplot()).
Zwei weitere spannende Funktionen sind contour() und image(). Mit ihnen kön-
nen wir unter anderem eine (mathematische) Funktion mit zwei Variablen grafisch
darstellen. Ebenso interessant sind identify() und locator(), die auf Mausklicks
innerhalb einer Grafik reagieren.
In (37) bzw. (38) befassen wir uns mit Hypothesentests bzw. statistischen Modellen.
Dabei begegnen uns diagnostische Plots (beispielsweise QQ-Plots), die uns bei der
Überprüfung der Modellannahmen unterstützen.
586 I Visualisierung von Daten
36.5.3 Übungen
1. In (36.3.3) haben wir einen pairs()-Plot für die Iris-Daten erstellt. Erstelle
nun einen pairs()-Plot, der in der Hauptdiagonalen ein Histogramm für die
jeweilige Variable enthält. Der Variablenname soll auch angezeigt werden.
Hinweis: Lasse dich von der R-Hilfe inspirieren ;-)
a) Visualisiere die Funktion f (·, ·) im Bereich [−10, 10]2 mit Hilfe der Funk-
tionen image(), contour() und filled.contour(). Probiere mehrere
Farbpaletten aus und experimentiere mit der Bildauflösung. Wie hoch
muss die Auflösung sein, damit die Grafiken schön glatt wirken?
Hinweis: Zur Bestimmung der Funktionswerte eignet sich zum Beispiel
outer() (siehe (19.4)).
Die Grafiken könnten etwa so aussehen:
image() contour()
10
10
−0.5
0
0
0
0
−1.5 −1.5 −1.5
−1
−1 0.5 −1 0.5 −1 0.5
−0.5 −0.5 −0.5
1.5 1.5 1.5
1
0 0 0
5
−0.5 −0.5
5
0.5 −1 1 1 1
.5
−1.5 −1.5 −0
0
−1.5
−1
0.5 0.5
−1 −1 1
−0.5
1.5 1.5
1
0
0
y
0 0 0
−1.5
−1
0.5
−1
0.5 0.5 −1
−5
−0.5
−5
0
−10
−10
−10 −5 0 5 10 −10 −5 0 5 10
filled.contour()
10 2
5 1
0 0
−5 −1
−10 −2
−10 −5 0 5 10
36 Grafikfenster und Layout 587
b) Erstelle nun zunächst eine Grafik mit image() und zeichne anschließend
mit contour() die Höhenschichtlinien ein. Die Höhenschichtlinien sollen
dabei genau entlang der Farbgrenzen verlaufen.
3. Wir wollen für die Iris-Daten (siehe Infobox 13 auf Seite 506) eine Grafik mit
parallelen Koordinaten zeichnen. Die Spezies soll dabei in unterschiedlichen
Farben dargestellt werden.
F (x) = 1 − e−λx
Die R-Hilfe ?plotmath ist ein sehr guter erster Anhaltspunkt. Finde her-
aus, mit welchem Befehl bzw. Schlüsselwort du griechische Buchstaben
einzeichnen kannst. Erstelle zunächst einen String und wandle ihn mit
parse() in eine expression um. Gehe dabei so vor, wie im Fazit am
Ende von (33.5) ab Seite 496.
588 I Visualisierung von Daten
1.0
0.8
0.6
F(x)
0.4
0.2
λ = 0.5
λ= 1
λ= 2
0.0
0 2 4 6 8 10
x
J Data Science und Statistik in der
Praxis
Andreas Baierl
590 J Data Science und Statistik in der Praxis
In (9.7.1) haben wir per Hand einen t-Test mit einer Apfelstichprobe gerechnet und
Grundbegriffe zu Hypothesentests erläutert. Es existieren in R Funktionen zu nahe-
zu allen statistischen Tests. In diesem Kapitel konzentrieren wir uns auf die Themen
Verteilungs- und Hypothesentests. Die statistischen Hintergründe der Tests bespre-
chen wir nur punktuell, der Fokus liegt auf der programmiertechnischen Anwendung
der Funktionen und der Weiterverarbeitung der Ergebnisse.
Als Beispiel dienen uns simulierte Einkommensdaten von je 200 Männern und Frauen
im Alter von 20 bis 64 Jahren. Folgende Variablen sind gegeben:
• geschlecht: Geschlecht der Person ("weiblich", "männlich")
• bildung: höchste abgeschlossene Bildung ("niedrig", "mittel", "hoch")
• alter: Alter in Jahren
• einkommen: monatliches Bruttoeinkommen in Euro
• einkommen_vorjahr: monatliches Bruttoeinkommen des Vorjahrs in Euro
Die Einkommensdaten werden nicht identisch verteilt simuliert, sondern beinhalten
Effekte für Geschlecht, Bildung und Alter:
> head(inc)
geschlecht bildung alter einkommen einkommen_vorjahr
1 weiblich niedrig 30 1382.3552 2482.9689
2 weiblich niedrig 22 506.9246 492.0906
3 weiblich niedrig 26 1209.9534 298.7344
4 weiblich niedrig 63 4393.5002 796.1567
5 weiblich niedrig 49 1375.0227 2105.7958
6 weiblich niedrig 57 2209.1333 19471.7661
> str(inc, vec.len = 2)
’data.frame’: 400 obs. of 5 variables:
$ geschlecht : Factor w/ 2 levels "weiblich","männlich": 1 1 1 1 1 ...
$ bildung : Factor w/ 3 levels "niedrig","mittel",..: 1 1 1 1 1 ...
$ alter : int 30 22 26 63 49 ...
$ einkommen : num 1382 507 ...
$ einkommen_vorjahr: num 2483 492 ...
• Wie überprüfen wir die Verteilung des Einkommens mittels Grafiken oder/und
statistischer Tests? (37.1)
• Verdient mehr als die Hälfte der Frauen mehr als 2500 Euro? Ist der Anteil
der Frauen, die mehr als 2500 Euro verdienen, niedriger als jener der Männer?
(37.2.1), (37.2.3),
• Wie können wir den Zusammenhang zwischen dem Einkommen des aktuellen
Jahres und des Vorjahres testen und die Annahmen des zugrundeliegenden
Tests untersuchen? (37.2.4), (37.3.1)
• Hat die Bildung einen Einfluss auf das Einkommen? (37.2.5)
• Wie führen wir all diese Tests in R durch und wie können wir das Ergebnis
eines Tests weiterverarbeiten?
• Erhalten wir gemeinsam mit dem Testergebnis auch Konfidenzintervalle für
die entsprechenden Parameterschätzer?
37.1 Verteilungstests
Grafisch überprüfen wir die Verteilung zum Beispiel mittels Histogramm und an-
gepasster Verteilungsdichte (37.1.1), in unserem Fall der Log-Normalverteilung, oder
mittels Quantil-Quantil-Plot (37.1.2). In (37.1.3) betrachten wir den Kolmogorov-
Smirnov-Test, einen allgemein einsetzbaren Verteilungsanpassungstest.
> mean(inc$einkommen)
[1] 2308.547
> exp(mean(log(inc$einkommen)))
[1] 1933.931
> library(psych)
> psych::geometric.mean(inc$einkommen)
[1] 1933.931
> median(inc$einkommen)
[1] 1954.257
Für den Quantil-Quantil-Plot oder kurz QQ-Plot werden mit der Funktion qqplot()
die theoretischen Quantile der hypothetischen Verteilung auf der x-Achse und die
empirischen Quantile (die aus den Daten bestimmten Quantile) des zu untersu-
chenden Merkmals auf der y-Achse aufgetragen. Anschließend kann zur Orientierung
entweder eine 45-Grad Gerade oder mittels qqline() eine Gerade durch zwei
Quantile der theoretischen Verteilung gelegt werden. Standardmäßig sind die bei-
den Quantile das erste und das dritte Quartil (vgl. (7.4)).
Im Fall des Einkommens ist die hypothetische Verteilung die Log-Normalverteilung,
wobei die Parameter Mittelwert und Standardabweichung aus den Daten geschätzt
werden. Die empririschen Quantile bestimmen wir dabei mit qlnorm(). Der QQ-
Plot ist in Abb. 37.2 dargestellt. Die Punkte liegen alle in etwa auf der Gerade, das
heißt, die Verteilung des Einkommens entspricht der Log-Normalverteilung.
594 J Data Science und Statistik in der Praxis
ppoints() erzeugt geeignete äquidistante Werte für den Parameter p von qlnorm().
Beachte, dass wir in der Praxis die Mehrfachberechnung des Logarithmus, Mittel-
werts und der Standardabweichung vermeiden, Regel 9 auf Seite 109 folgend.
10000
8000
Einkommen
6000
4000
2000
0
Die Funktion qqPlot() aus dem Paket car ermöglicht überdies die Darstellung
von Konfidenzintervallen. Die linke Grafik in Abb. 37.3 zeigt einen entsprechen-
den Plot für Einkommen und Quantile der Log-Normalverteilung, die Hilfslinie durch
das erste und dritte Quartil wird standardmäßig eingezeichnet.
> library(car)
381 381
10000
9.0
279
logarithmiertes Einkommen
8.5
8000
8.0
Einkommen
6000
7.5
4000
7.0
2000
6.5
6.0
256
0
> # Standardfunktion
> ks.test(inc$einkommen, "plnorm", mean(log(inc$einkommen)),
+ sd(log(inc$einkommen)))
One-sample Kolmogorov-Smirnov test
data: inc$einkommen
D = 0.035106, p-value = 0.7077
alternative hypothesis: two-sided
596 J Data Science und Statistik in der Praxis
In unserem Fall ergeben beide Methoden einen nicht signifikanten p-Wert, die Null-
hypothese wird also beibehalten.
binom.test(x, n, p = 0.5,
alternative = c("two.sided", "less", "greater"),
conf.level = 0.95)
In Tab. 37.1 erläutern wir die Parameter. In unserem Fall steht x für die Anzahl
der Frauen, die mehr als 2500 Euro verdienen, und die Referenzwahrscheinlich-
keit beträgt p0 = 0.5. Standardmäßig wird ein zweiseitiger Test durchgeführt
(alternative = "two.sided"), mit den Einstellungen alternative = "less" bzw.
alternative = "greater" kann eine der beiden einseitigen Testvarianten ge-
wählt werden. Das Signifikanzniveau α steuern wir indirekt über das Konfidenz-
niveau 1 − α (conf.level); Standardwert für α ist 0.05.
Parameter Bedeutung
x Anzahl der Erfolge
n Anzahl der Versuche, also die Stichprobengröße
p Referenzwahrscheinlichkeit p0
alternative two.sided für zweiseitigen Test (Standard)
"less" und "greater" für einseitigen Test
conf.level Konfidenzniveau 1 − α. Standardwert ist 0.95
37 Verteilungstests und Hypothesentests 597
Für x setzen wir die Anzahl der Frauen ein, die mehr als 2500 Euro verdienen und n
ist die Anzahl der Frauen in der Stichprobe. Die print()-Methode für das Ergebnis
von binom.test() gibt den Anteilswert, den p-Wert und das 95%-Konfidenzintervall
aus. Wir erkennen, dass der Anteil der Frauen, die mehr als 2500 Euro verdient,
29.5% beträgt mit einem 95%-Konfidenzintervall von [23.3, 36.3%].
Die meisten Funktionen für Hypothesentests wie die bereits besprochenen Funktio-
nen ks.test() und binom.test() geben ein Objekt der Klasse htest zurück, das
als Listenelemente alle relevanten Ergebnisse des Hypothesentests enthält.
Das Rückgabeobjekt des binomialtests in (37.2.1) enthält folgende Listenelemente:
• statistic: Anzahl der Erfolge, hier die Anzahl der Frauen mit mehr als 2500
Euro Einkommen.
• parameter: Anzahl der Versuche, hier die Anzahl der betrachteten Frauen.
• p.value: p-Wert des Hypothesentests.
• conf.int: Vektor der Länge 2 mit der unteren und oberen Grenze des Konfi-
denzintervalls.
• estimate: der geschätzte Parameter, hier der Anteil der Frauen mit mehr als
2500 Euro Einkommen.
Mit prop.test() können wir sowohl Ein- als auch Zwei-Stichprobentests für An-
teilswerte durchführen. Der Ein-Stichprobenfall entspricht derselben Hypothese wie
beim Binomialtest, mit dem Unterschied, dass prop.test() anstatt der tatsächli-
chen Binomialverteilung eine Normalverteilungsapproximation verwendet. Die Ap-
proximation ist ausreichend gut, wenn die Bedingung np(1 − p) > 9 erfüllt ist.
598 J Data Science und Statistik in der Praxis
H0 : Der Anteil der Frauen mit einem Einkommen über 2500 Euro ist nicht niedriger
als der entsprechende Anteil der Männer.
H1 : Der Anteil der Frauen mit einem Einkommen über 2500 Euro ist niedriger
als der entsprechende Anteil der Männer.
Als Argument x in der Funktion prop.test() geben wir einen Vektor der Länge 2 an,
der die Anzahl der Frauen und die Anzahl der Männer mit einem Einkommen über
2500 Euro enthält (einkommen_gt_2500). Beachte, dass wir diesen Vektor elegant
mit tapply() berechnen. n spezifiziert die Anzahl der Frauen bzw. Männer. Da
wir unter H1 unterstellen, dass der Anteil bei den Frauen niedriger ist, wählen wir
alternative = "less". Alternativ kann über den Parameter x eine 2 × 2 Matrix
oder eine 2×2 Kreuztabelle übergeben werden, die als Spalten die Anzahl der Erfolge
und die Anzahl der Misserfolge enthält. Der Parameter n entfällt dann.
Die Funktion prop.test() gibt ein Objekt der Klasse htest zurück. Die Listenele-
mente sind gleichlautend wie für den Binomialtest (siehe (37.2.2)), jedoch werden
nun unter statistic der Wert der Teststatistik und unter parameter die Freiheits-
grade der approximativen Verteilung der Teststatistik, nämlich der χ2 -Verteilung,
geführt. Eine Normalverteilungsapproximation trifft beim prop.test() trotzdem
zu, da die quadrierte Normalverteilung der χ2 -Verteilung entspricht.
Den McNemar-Test verwenden wir, wenn ein dichotomes Merkmal zweimal an den-
selben Beobachtungseinheiten gemessen wurde. Als Argument können der Funktion
mcnemar.test() entweder eine 2 × 2 Matrix oder 2 Faktoren in 2 getrennten Argu-
menten übergeben werden.
37 Verteilungstests und Hypothesentests 599
Im folgenden Beispiel untersuchen wir, ob sich der Anteil der Personen mit mehr
als 2500 Euro Einkommen zwischen dem Vorjahr und dem aktuellen Jahr verändert
hat. Da hier wiederholte Messungen derselben 400 Personen vorliegen, wäre ein Test
mit der Funktion prop.test() nicht adäquat.
Im ersten Schritt wird eine 2×2 Kreuztabelle gebildet und anschließend der Funktion
mcnemar.test() übergeben.
Da der p-Wert 0.1425 beträgt, kann die Nullhypothese nicht abgelehnt werden. Die
Nullhypothese besagte, dass der Anteil der Personen mit mehr als 2500 Euro Ein-
kommen zwischen dem Vorjahr und dem aktuellen Jahr sich nicht veränderte. Zu-
rückgegeben wird ein Objekt der Klasse htest, wobei das Listenelement statistic
diesmal den Wert der McNemar-Statistik enthält.
Als Beispiel für den χ2 -Test betrachten wir das Merkmal Bildung mit drei Ausprä-
gungen und das dichotome Merkmal “Einkommen über 2500 Euro”. Wir übergeben
der Funktion chisq.test() die Daten in Form von zwei Faktoren an die Parameter
x und y. Alternativ könnten wir eine zweidimensionale Kreuztabelle an x übergeben.
600 J Data Science und Statistik in der Praxis
Das Ergebnisobjekt der Klasse htest ist umfangreicher als bei den vorherigen Tests.
Es enthält zusätzlich die beobachteten Häufigkeiten (observed) und erwarte-
ten Häufigkeiten (expected) sowie die Pearson Residuen (residuals) und
standardisierten Residuen (stdres). Für die Berechnung der Residuen wird je-
weils die Differenz zwischen erwarteten und beobachteten Häufigkeiten gebildet. Im
Fall der Pearson Residuen wird die Differenz durch die Wurzel der erwarteten Häu-
figkeiten dividiert, für die standardisierten Residuen durch die Standardabweichung
der Residuen.
• Der exakte Test nach Fisher für Kreuztabellen ist eine Alternative zum χ2 -Test.
Der Fisher-Test beruht auf keiner Verteilungsapproximation, was bei kleinen
Stichproben von Vorteil ist. Im Gegensatz zum χ2 -Test setzt er aber einen
datengenerierenden Prozess mit festen Zeilen- und Spaltensummen voraus, was
in der Regel nicht gegeben ist. In R steht zur Durchführung die Funktion
fisher.test() zur Verfügung.
• Der Cochran-Mantel-Haenszel-Test ist ein χ2 -Test für stratifizierte kategorielle
Daten. Im Beispiel für den χ2 -Test untersuchten wird den Zusammenhang
zwischen Bildung und Einkommen über 2500 Euro. Mittels Cochran-Mantel-
Haenszel-Test können wir ein weiteres kategorielles Merkmal, zum Beispiel
das Geschlecht, berücksichtigen. Der Zusammenhang zwischen Bildung und
Einkommen wird getrennt für Männer und Frauen berechnet und anschließend
zusammengeführt. Auf diese Weise kann für ein Merkmal kontrolliert oder eine
Stratifizierung in der Datengenerierung berücksichtigt werden. Durchgeführt
wird der Test in R mit der Funktion mantelhaen.test().
37 Verteilungstests und Hypothesentests 601
Mit dem Korrelationskoeffizienten nach Pearson messen wir, ob ein linearer Zu-
sammenhang zwischen zwei Merkmalen besteht. Die Nullhypothese, dass es keinen
Zusammenhang zwischen zwei Merkmalen gibt, können wir unter der Annahme ei-
ner bivariaten Normalverteilung der beiden Merkmale mittels entsprechendem t-Test
untersuchen. Die Annahme der bivariaten Normalverteilung lässt sich grafisch
mittels Scatterplots untersuchen. Die Punkte sollten innerhalb einer Ellipse liegen.
In unserem Beispiel wollen wir die Korrelation zwischen dem Vorjahrseinkommen
und dem aktuellen Einkommen bestimmen. Für einen Scatterplot inklusive Ellipse,
die den 95%-Bereich der entsprechenden bivariaten Normalverteilung beschreibt,
übergeben wir der Funktion dataEllipse() aus dem Paket car eine Matrix mit
den beiden Spalten "einkommen_vorjahr" und "einkommen".
Abb. 37.4 zeigt die entsprechenden Scatterplots für das nicht logarithmierte Ein-
kommen (linke Grafik) und logarithmierte Einkommen (rechte Grafik).
> library(car)
> car::dataEllipse(x = as.matrix(inc[, c("einkommen_vorjahr", "einkommen")]),
+ level = .95) # mit originalen Daten
9.0
8.5
8000
log(einkommen)
8.0
einkommen
6000
7.5
4000
7.0
2000
6.5
6.0
0
Abbildung 37.4: Scatterplot für das Einkommen mit 95%-Bereich der bivariaten
Normalverteilung. Links: originale Einkommensdaten. Rechts:
logarithmiertes Einkommen
602 J Data Science und Statistik in der Praxis
Es ist deutlich ersichtlich, dass die Annahme der bivariaten Normalverteilung für die
untransformierten Einkommensdaten verletzt ist. Entsprechend berechnen wir den
Test für den Korrelationskoeffizienten für die logarithmierten Einkommensdaten.
Dafür verwenden wir die Funktion cor.test().
Alternativ können wir den Test für den Korrelationskoeffizienten nach Spearman be-
rechnen. Dieser setzt keine bivariate Normalverteilung und keinen linearen, sondern
nur einen monotonen Zusammenhang voraus.
Die Funktion cor.test() gibt ein Objekt der Klasse htest zurück, das neben dem
gewählten Korrelationskoeffizienten im Listenelement estimate auch das Konfidenz-
intervall (Element conf.int) zurückgibt.
Mit dem t-Test für eine Stichprobe untersuchen wir, ob der Erwartungswert einer
Verteilung einem bestimmten Wert entspricht. Im Fall von zwei unabhängigen
Stichproben vergleichen wir die Erwartungswerte der beiden Verteilungen. Die
Erwartungswerte werden jeweils durch das arithmetische Mittel geschätzt und der
t-Test setzt voraus, dass die Daten jeder Stichprobe einer Normalverteilung folgen.
Zuerst untersuchen wir mit einem t-Test für eine Stichprobe, ob sich der Erwar-
tungswert des Einkommens von 2500 Euro unterscheidet. Ein Logarithmieren der
Daten führt dazu, dass wir anstatt des arithmetischen das geometrische Mittel un-
tersuchen (vgl. Infobox 14 auf Seite 593). Im vorliegenden Fall können wir auf Basis
der Stichprobengröße von 400 argumentieren, dass aufgrund des Zentralen Grenz-
wertsatzes die Teststatistik annähernd normalverteilt ist und somit der t-Test gültig
bleibt. Dies muss aber selbst für große Stichproben nicht unbedingt gelten.
37 Verteilungstests und Hypothesentests 603
Für die Durchführung des t-Tests in R verwenden wir die Funktion t.test().
Inwieweit sich das Einkommen zwischen dem Vorjahr und dem aktuellen Jahr ver-
änderte, testen wir mit einem t-Test für gepaarte Stichproben. Es handelt sich
um eine Stichprobe mit wiederholten Beobachtungen. Wir erhalten die Stichprobe,
indem wir die Differenz aus den beiden Einkommensvektoren bilden. Der logi-
sche Referenzwert ist 0 für keine Differenz. Alternativ können wir in R der Funktion
t.test() beide Variablen übergeben und den Parameter paired = TRUE setzen.
Auch hier hat der t-Test für die untransformierten Daten eine andere Interpreta-
tion als der t-Test für logarithmierte Daten: Im ersten Fall wird die Differenz, im
zweiten Fall der Quotient des aktuellen Einkommens und des Vorjahreseinkommens
untersucht.
In einem dritten Fall untersuchen wir nun zwei unabhängige Stichproben, näm-
lich die Einkommen der Frauen und jene der Männer. Als zusätzliches Argument
kann beim t-Test für zwei unabhängige Stichproben angegeben werden, ob die Vari-
anzen in den beiden Stichproben als gleich angenommen werden sollen (var.equal
= TRUE) oder die Welch-Approximation für den t-Test mit ungleichen Varian-
zen angewendet werden soll. Als (sinnvoller) Standardwert geht R von ungleichen
Varianzen (var.equal = FALSE) aus.
In allen drei Fällen gibt die Funktion t.test() eine Liste der Klasse htest zurück.
t.test() verfügt über eine formula-Methode, die es ermöglicht, die Variablen kom-
fortabel in Form einer Formel zu übergeben. Ein Beispiel für eine Formel haben
wir bereits im Boxplotbeispiel auf Seite 550 gesehen. Links von ∼ schreiben wir
das metrische Merkmal, rechts davon die Gruppenvariable. Das formula-Objekt be-
sprechen wir ausführlich in (38.1). Zusätzlich kann über den Parameter data ein
Dataframe oder eine Matrix mit den Variablen der Formel übergeben werden.
• Für den Test auf Gleichheit der Varianzen steht in R die Funktion var.test()
zur Verfügung.
• Der Friedman-Test (friedman.test()) ist ein nichtparametrischer Test auf
Gleichheit des Lageparameters in mehr als zwei Stichproben.
606 J Data Science und Statistik in der Praxis
R-Funktion Testname
binom.test() Binomialtest
prop.test() Test für Anteilswerte
chisq.test() χ2 -Test
mcnemar.test() McNemar-Test
fisher.test() Exakter Test nach Fisher
mantelhaen.test() Cochran-Mantel-Haenszel Test
cor.test() Test für Korrelationskoeffizienten
t.test() t-Test
wilcox.test() Wilcoxon-Vorzeichen-Rang-Test,
Wilcoxon-Rangsummen-Test (Mann-Whitney-U-Test)
friedman.test() Friedman-Test
var.test() Test auf Gleichheit der Varianzen
ks.test() Kolmogorov-Smirnov-Test
37.7 Abschluss
• Wie untersuchen wir, ob ein Merkmal einer bestimmten Verteilung folgt? Wel-
che beiden grafischen Möglichkeiten haben wir dabei und wie funktionieren
diese? Welcher statistische Test steht uns hierfür zur Verfügung? (37.1)
• Welche Tests verwenden wir für kategorielle bzw. metrische Merkmale in R?
Wie sehen die nichtparametrischen Alternativen aus? (37.2), (37.3), (37.4)
• Mit welcher Einstellung können wir einseitige Tests durchführen? Wie steuern
wir das Signifikanzniveau α bzw. das Konfidenzniveau? (37.2.1)
• Wie ist ein Ergebnisobjekt der Klasse htest aufgebaut? Welche Elemente ent-
hält es und wie greifen wir auf sie zu? Wie extrahieren wir insbesondere den
p-Wert und das Konfidenzintervall? (37.2.2)
37 Verteilungstests und Hypothesentests 607
37.7.2 Ausblick
In (37.3.2) und (37.4) haben wir erwähnt, dass wir Hypothesentests alternativ mit
Formeln (formula) befüllen können. In (38) gehen wir detailliert auf Formeln ein
und erläutern, wie wir damit in R statistische Modelle bauen können.
37.7.3 Übungen
38 Statistische Modelle
In diesem Kapitel behandeln wir vor allem die lineare Regression. Komplexere sta-
tistische Verfahren unterscheiden sich hinsichtlich der Programmierung kaum, es
kommen dieselben Syntaxregeln zum Einsatz. Statistische Hintergründe werden in
diesem Kapitel nur punktuell vermittelt, der Fokus liegt auf der programmiertech-
nischen Anwendung der Funktionen und der Weiterverarbeitung der Ergebnisse.
Wir verwenden dieselben Daten wie in (37), nämlich simulierte Einkommensdaten
von 200 Männern und Frauen im Alter von 20 bis 64 Jahren. Folgende Fragen wollen
wir in diesem Kapitel behandeln:
• Wie untersuchen wir den Einfluss des Alters auf das Einkommen mittels linea-
rer Regression? (38.1.1)
• Wie können wir die Ergebnisse der linearen Regression ausgeben, darstellen
und weiterverarbeiten? (38.1.1)
• Wie können wir den Einfluss mehrerer Variablen auf das Einkommen simultan
untersuchen? (38.1.2)
• Welche Möglichkeiten bestehen in R, um das Einkommensmodell zu spezifizie-
ren? (38.1.2)
• Welche Alternativen zur linearen Regression stehen in R zur Verfügung, um
statistische Zusammenhänge zu untersuchen? (38.2)
Die lineare Regression stellt das zentrale Werkzeug der statistischen Analyse dar und
wird in R standardmäßig mit der Funktion lm() (Linear Model; Lineares Modell)
durchgeführt. Über das formula-Argument werden die abhängige Variable und die
unabhängigen Variablen spezifiziert. Zusätzlich kann über das data-Argument ein
Dataframe mit den Variablen der Formel übergeben werden.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_38
38 Statistische Modelle 609
In einem Modell mit nur einer unabhängigen Variable, der sogenannten einfachen
linearen Regression, wollen wir den Einfluss des Alters auf das Einkommen unter-
suchen. Im formula-Argument geben wir zuerst die abhängige und anschließend die
unabhängige Variable an. Getrennt werden die beiden Variablen durch das Tilde-
Symbol ˜.
In (37.1) haben wir bereits die Verteilung des Einkommens untersucht und festge-
stellt, dass das Einkommen einer Log-Normalverteilung folgt. Da die lineare Regres-
sion normalverteilte Residuen voraussetzt, logarithmieren wir das Einkommen. Dies
können wir direkt im formula-Argument durchführen.
Mit der Formel log(einkommen) ˜ alter schätzen wir die Parameter β0 und β1 der
Regressionsgeraden log(einkommen) = β0 + β1 · alter. Die Variablen einkommen
und alter werden dabei wegen data = inc direkt dem Dataframe inc entnommen.
Die print-Methode für die Funktion lm() gibt nur das formula-Objekt und die
Schätzer für den Intercept und den Effekt des Alters in der Console aus. Die geschätz-
te Regressionsgleichung lautet: log(einkommen) = 6.66 + 0.02 · alter. Der In-
tercept von 6.66 entspricht dem logarithmierten Einkommen für alter = 0. Der
Koeffizient für alter von 0.02 gibt den Anstieg des logarithmierten Einkommens
pro Altersjahr an.
Darüber hinaus gibt uns die Funktion lm() aber eine umfangreiche Liste der Klasse
lm zurück, in der die Koeffizienten, die Residuen und vieles mehr enthalten sind. Auf
die einzelnen Elemente der Liste kann wie gewohnt mit $ zugegriffen werden und es
existieren entsprechende generische Funktionen, die wir auf ein lm-Objekt anwenden
können. Unter anderen stehen folgende Funktionen zur Verfügung:
Als Beispiel wollen wir für das obige Modell erweiterte Analysen mit der Funk-
tion summary() ausgeben und die Parameterschätzer inklusive Standardfehler, t-
Statistiken und p-Werten abspeichern.
Die einfache lineare Regression kann zur multiplen linearen Regression mit mehr
als einer unabhängigen Variable erweitert werden. Zum Beispiel können wir das
logarithmierte Einkommen im aktuellen Jahr durch das logarithmierte Einkommen
im Vorjahr und das Alter erklären:
Wir haben die beiden unabhängigen Variablen mit + verbunden. Dadurch wird ein
sogenanntes additives Modell geschätzt, in dem alle (in diesem Fall zwei) Variablen
ausschließlich als Haupteffekte einfließen.
Das formula-Objekt erlaubt die Spezifikation unterschiedlichster Modelle unter Ver-
wendung von Operatoren wie ˜ und +, die wir schon kennengelernt haben. Im Fol-
genden werden anhand einfacher Beispiele mit der abhängigen Variable y und den
unabhängigen Variablen x1, x2 und x3 die Möglichkeiten der Modellspezifikation
im linearen Modell demonstriert. Auf sehr ähnliche Weise können Formeln auch in
anderen statistischen Modellen angegeben werden.
Wir beginnen mit dem additiven Modell analog zum Einkommensbeispiel:
y ˜ x1 + x2
Dieses möchten wir um die Wechselwirkung zwischen den beiden unabhängigen
Variablen x1 und x2 ergänzen. Eine Möglichkeit ist, den Wechselwirkungsterm mit
dem Operator : explizit anzuführen:
y ˜ x1 + x2 + x1:x2
Dieses Modell können wir auch kürzer formulieren:
y ˜ x1 * x2
Mit Hilfe von * werden alle Haupteffekte und (Mehrfach-)Wechselwirkungen
der beteiligten Variablen berücksichtigt. Falls das Modell eine dritte unabhängige
Variable x3 umfasst, dann werden im Modell
y ˜ x1 * x2 * x3
alle Haupteffekte, alle Zweifach-Wechselwirkung und die Dreifach-Wechselwirkung
x1:x2:x3 geschätzt.
Oft möchte man nur die Zweifach-Wechselwirkungen inkludieren. Dies ist ent-
weder mit der Langschreibweise
y ˜ x1 + x2 + x3 + x1:x2 + x1:x3 + x2:x3
Für die Formelbildung stehen darüber hinaus die Operatoren - und / zur Verfügung,
mit denen wir Effektterme ausschließen können. So lässt sich beispielsweise das
Modell
y ˜ x1 + x1:x2
auch folgendermaßen aufschreiben:
y ˜ x1 * x2 - x2
y ˜ x1/x2
Der - Operator erlaubt auch ein Modell ohne Intercept darzustellen:
y ˜ x1 * x2 - 1
Das Nullmodell (ein Modell, das nur den Intercept enthält) lautet:
y ˜ 1
Wird das data-Argument in der lm()-Funktion verwendet, dann steht
y ˜ .
für ein Modell, in dem alle Spalten des in data spezifizierten Datensatzes als
Haupteffekte ins Modell aufgenommen werden.
Wir können auch Berechnungen innerhalb der Formel durchführen. Im Bei-
spiel in (38.1.1) führten wir innerhalb der Formel eine logarithmische Transformation
der abhängigen Variablen durch, so wie im folgenden Beispiel:
log(y) ˜ x1 * x2
Beachte: Werden Operatoren für die Berechnung eingesetzt, die auch für die Defi-
nition der Formel eine Bedeutung haben, müssen wir die Funktion I() verwenden.
y ˜ x1 + I(x1 ˆ 2)
bedeutet, dass x1 als linearer und quadratischer Term in die Formel inkludiert wird.
y ˜ I(x1 + x2)
heißt, dass zuerst die Summe aus den Variablen x1 und x2 gebildet wird und nur
die Summenvariable (und der Intercept) in die Formel einfließt.
Generell gilt, dass die Variablennamen in der Formel existierenden Objektnamen
entsprechen müssen. Es können auch Spalten eines zweidimensionalen Objekts
als Variablennamen verwendet werden, zum Beispiel
y ˜ x[, 1] + x[, 2]
wenn x eine Matrix oder ein Dataframe mit zumindest zwei Spalten darstellt.
38 Statistische Modelle 613
Nicht unerwähnt soll bleiben, dass wir eine Formel auch als String mit paste()
oder paste0() zusammensetzen und anschließend einer Funktion wie lm() überge-
ben können. R versucht automatisch, den String in eine Formel umzuwandeln. Mit
as.formula() können wir explizit einen String in eine Formel umwandeln.
Für die Funktion lm() gilt, dass sowohl die abhängige als auch die unabhängigen
Variablen metrisch sein müssen. Kategorielle unabhängige Variablen müssen zuvor
in metrische Variablen umcodiert werden. In lm() passiert dies automatisch, wenn
die kategorielle Variable als Faktor (siehe (24)) übergeben wird.
Die Art der Umcodierung wird durch die Spezifikation der Kontraste mit der Funk-
tion contrasts() festgelegt. Standardmäßig kommt der sogenannte Treatmentkon-
trast zum Einsatz, der die zweite, dritte und jede weitere Kategorie mit der ersten
Kategorie, der sogenannten Referenzkategorie, vergleicht. Um die automatische
Recodierung nachzuvollziehen, kann die Designmatrix X der linearen Regressi-
on mit der Funktion model.matrix() angezeigt werden.
Anhand des Einkommensbeispiels untersuchen wir den Einfluss des Faktors bildung
auf das logarithmierte Einkommen:
Die Koeffizienten bildungmittel und bildunghoch messen die Differenz der jewei-
ligen Kategorien zur Referenzkategorie niedrig. In unserem Fall sind beide Effekte
positiv, das heißt, sowohl Personen mit mittlerer als auch hoher Bildung weisen ein
höheres Einkommen auf als Personen mit niedriger Bildung.
Die Designmatrix X gibt Aufschluss über die interne Recodierung des Faktors. Wir
zeigen jeweils die erste Zeile der simulierten Bildungsblöcke an:
Mit der Funktion step() können wir ein lineares Modell automatisiert spezifi-
zieren. Es müssen alle potenziellen unabhängigen Variablen mit deren Wechselwir-
kungen angegeben werden. Das schrittweise Verfahren step() eliminiert alle nicht
relevanten Terme auf Basis eines Optimalitätskriteriums, im Standardfall ist dies das
Akaike Informationskriterium (AIC), und gibt das auf diese Weise ermittelte Modell
zurück. Die Funktion step() greift dabei auf die Funktionen add1(), drop1() und
update() zu. Ein Beispiel und kritische Anmerkungen zur Modellselektion finden
sich in (38.1.5).
Zum Vergleich zweier Modelle steht zusätzlich die generische Funktion anova()
mit der Methode anova.lm() zur Verfügung. Standardmäßig wird mittels F-Test die
Reduktion der Residuenquadratsumme durch ein komplexeres Modell im Vergleich
zu einem einfacheren Modell getestet. Wichtig: Das einfachere Modell muss im
komplexeren Modell enthalten (Englisch: nested) sein.
Betrachten wir ein Beispiel mit den Modellen m1 (das einfachere Modell) und m2
(das komplexere Modell). Beachte, dass m1 in m2 enthalten ist!
Der F-Test liefert einen p-Wert (Pr(>F)) von < 2.2e-16 (kleiner als 2.2·10−16 ). Das
komplexere Modell mit der zusätzlichen erklärenden Variable einkommen_vorjahr
(logarithmiert) verbessert also das Modell signifikant.
Wenden wir anova() nur auf ein Modell an, dann wird jeder Term des Modells
getestet:
> anova(m2)
Analysis of Variance Table
Response: log(einkommen)
Df Sum Sq Mean Sq F value Pr(>F)
alter 1 31.900 31.900 144.16 < 2.2e-16 ***
log(einkommen_vorjahr) 1 23.288 23.288 105.25 < 2.2e-16 ***
Residuals 397 87.847 0.221
---
Signif. codes: 0 *** 0.001 ** 0.01 * 0.05 . 0.1 1
Abschließend wollen wir schrittweise jenes Modell für das Einkommen aufbauen, das
für die Simulation am Beginn des Kapitels (37) herangezogen wurde.
38 Statistische Modelle 615
Die Koeffizienten für alter und geschlecht weichen im einfachen Modell stark von
den simulierten Werten ab, der Effekt von bildung stimmt gut überein. Der Grund
dafür liegt im Interaktionsterm für Alter und Geschlecht, den wir im folgenden
Schritt inkludieren:
Durch die Inklusion der Interaktion verändern sich die Koeffizienten und deren In-
terpretation: Der Effekt für alter misst den Zusammenhang mit dem Einkommen,
wenn das Geschlecht der Referenzkategorie (hier weiblich) entspricht. Der Effekt für
geschlecht misst den Geschlechterunterschied, wenn das Alter auf 0 gesetzt wird.
616 J Data Science und Statistik in der Praxis
Bemerkung: Um zu verhindern, dass sich durch die Hinzunahme der Interaktion die
Haupteffekte ändern, können beide Variablen alter und geschlecht mittelwerts-
zentriert werden, wodurch sich ein Mittelwert von 0 ergibt. Auch für kategorielle
Variablen ist dies durch die entsprechende Dummycodierung möglich. Für alter
und geschlecht in unserem Beispiel funktioniert die Zentrierung folgendermaßen:
Alle Koeffizienten, die der Simulation des Einkommens zugrunde liegen, liegen in-
nerhalb der Konfidenzintervalle des geschätzten Modells.
Abschließend erstellen wir eine Reihe von diagnostischen Plots, um den Modellfit zu
überprüfen. Abb. 38.1 zeigt die vier erstellten Plots.
> plot(m4)
Standardized residuals
312 312
0.5
Residuals
−0.5
245
228 245
228
−3
Standardized residuals
228 312
245
1.0
Cook’s
397 distance
382
0.0
Keiner der Plots zeigt Auffälligkeiten, die auf eine Verletzung von Modellannahmen
hindeuten würde.
38 Statistische Modelle 617
Anhand eines einfachen Beispiels mit einer metrischen und einer kategoriellen unab-
hängigen Variable wollen wir uns noch einmal genauer die Modellbildung im Rahmen
der linearen Regression ansehen.
Wir simulieren 150 Beobachtungen mit einer gleichverteilten Variable x und einer
Dummyvariable gruppe.
> n <- 150
> RNGversion("4.0.2")
> set.seed(2^31 - 1)
> x <- runif(n, min = -3, max = 3)
> gruppe <- sample(0:1, size = n, replace = TRUE)
Die abhängige Variable y simulieren wir als quadratische Funktion der erklärenden
Variable x (x und xˆ2), mit einem linearen Term für gruppe und einem Wechsel-
wirkungsterm für gruppe mit x. Der Fehlerterm ist im Vergleich zu den Effekten
gering, um den Zusammenhang gut sichtbar zu machen.
Mit den drei Variablen bilden wir den Datensatz d, den wir nach der Variable x
ordnen, damit wir später grafische Darstellungen mit Linien erstellen können.
In Abb. 38.2 (links) sind die simulierten Daten in einem Streudiagramm dargestellt,
wobei wir x auf der x-Achse und gruppe farblich darstellen.
In Modell 1 schätzen wir nur den Intercept. Dieses Modell wird auch Nullmodell
genannt (vgl. (38.1.2)).
Der Schätzer für den Intercept entspricht dem Gesamtmittelwert der abhängigen
Variable y:
> mean(d$y)
[1] 21.57097
618 J Data Science und Statistik in der Praxis
Wir wollen grafisch die Vorhersagen der bis jetzt geschätzten Modelle 1, 2 und 3
darstellen. Dazu wenden wir die generische Funktion predict() auf alle 3 Modelle
an. Abb. 38.2 (rechts) zeigt das Ergebnis. Die Vorhersagen sind noch unbefriedigend,
da wir die Variable gruppe noch nicht berücksichtigt haben.
80
0 1 0
1 2 1
3
60
60
40
40
y
y
20
20
0
−3 −2 −1 0 1 2 3 −3 −2 −1 0 1 2 3
x x
Modell 4 schätzen wir daher mit einem eigenen additiven Term für gruppe.
> res.lm4 <- lm(y ~ gruppe + x + I(x^2), data = d) # Modell 4
> coef(res.lm4)
(Intercept) gruppe x I(x^2)
4.136655 7.836424 -4.754957 5.093511
Modell 4 stellen wir in Abb. 38.3 (links) dar. Die Vorhersage ist deutlich besser.
In Abb. 38.3 (rechts) stellen wir die Vorhersage durch Modell 6 dar.
Gruppe Gruppe
80
80
0 0
1 1
60
60
40
40
y
y
20
20
0
0
−3 −2 −1 0 1 2 3 −3 −2 −1 0 1 2 3
x x
In der Praxis kennen wir das datengenerierende Modell meist nicht und oft ist un-
bekannt, ob alle erklärenden Variablen zur Verfügung stehen. Mit automatisierten
Modellselektionsalgorithmen versucht man das beste Modell zu finden. Für schritt-
weise Selektionen steht in R wie bereits erwähnt die Funktion step() zur Verfügung.
Der Funktion wird ein lineares Modell mit der maximalen Anzahl an Termen über-
geben. step() vereinfacht das Modell, soweit es das Optimalitätskriterium erlaubt.
In unserem Beispiel übergeben wir ein Modell mit allen drei Haupteffekten und
allen Zweifach-Wechselwirkungen. Die beiden Terme x:I(xˆ2) und gruppe:I(xˆ2)
sind nicht Teil des datengenerierenden Prozesses und sollten im Idealfall durch die
Modellselektion eliminiert werden.
> res.step <- step(lm(y ~ (gruppe + x + I(x^2))^2, data = d), trace = 0)
> coef(res.step)
(Intercept) gruppe x I(x^2) gruppe:x
2.208835 9.986169 1.036930 4.951051 -10.044820
Hier funktioniert die Modellselektion nicht wie erwünscht. Kein einziger Term wird
eliminiert, da bereits die Dreifach-Wechselwirkung gruppe:x:I(xˆ2) beibehalten
wird und daraufhin keine niedrigeren Wechselwirkungen mehr untersucht werden.
Zur Berechnung der Varianzanalyse steht die Funktion aov() zur Verfügung, wo-
bei diese wiederum auf die Funktion lm() zurückgreift. Unterschiede bestehen un-
ter anderen bei der Ausgabe auf die Console und beim Ergebnis der Funktion
summary.aov(). Zusätzlich kann in der Formel mit Error() ein Stratum wie folgt
spezifiziert werden:
y ˜ x1 * x2 + Error(stratum)
> gm1 <- glm(einkommen > 2500 ~ alter + einkommen_vorjahr, data = inc,
+ family = binomial)
> summary(gm1)
Call:
glm(formula = einkommen > 2500 ~ alter + einkommen_vorjahr, family = binomial,
data = inc)
Deviance Residuals:
Min 1Q Median 3Q Max
-3.4540 -0.8236 -0.5246 1.0216 2.3300
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -3.967e+00 4.795e-01 -8.272 < 2e-16 ***
alter 6.257e-02 9.829e-03 6.366 1.94e-10 ***
einkommen_vorjahr 1.644e-04 4.098e-05 4.013 6.00e-05 ***
---
Signif. codes: 0 *** 0.001 ** 0.01 * 0.05 . 0.1 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 510.13 on 399 degrees of freedom
Residual deviance: 430.20 on 397 degrees of freedom
AIC: 436.2
Number of Fisher Scoring iterations: 4
622 J Data Science und Statistik in der Praxis
Der Output für das generalisierte lineare Modell ist ähnlich aufgebaut wie für das li-
neare Modelle (38.1.1) mit formula-Aufruf, Residuenverteilung, Koeffizientenschät-
zer und -tests und einer Zusammenfassung des Modellfits. Da das generalisierte
lineare Modell mittels Maximum-Likelihood-Verfahren geschätzt wird und nicht wie
das lineare Regressionsmodell mit der Methode der kleinsten Quadrate, umfasst der
Output andere Kennzahlen für den Modellfit, nämlich die Devianz und das Akaike
Informationskriterium (AIC).
38.3 Abschluss
In (41) sind Verweise auf Pakete für eine Reihe von typischen statistischen Verfahren
aufgelistet. Dazu zählen Zeitreihenanalyse, Überlebenszeitanalyse (Survival Analy-
sis), Mixed-Effects-Modelle, Korrekturen für multiple Hypothesentests, Poweranaly-
se, Konfirmatorische Faktorenanalyse, Strukturgleichungsmodelle und Bayesianische
Inferenz.
38 Statistische Modelle 623
38.3.3 Übungen
1. Simuliere 1000 Mal zwei normalverteilte Vektoren x und y der Länge 100 und
berechne das lineare Regressionsmodell y ˜ x.
2. Gegeben ist folgender Datensatz mit drei Merkmalen: der abhängigen Variable
y und den beiden erklärenden Variablen x1 und x2:
3. Gegeben sind eine abhängige Variable y und drei unabhängige Variablen x1,
x2 und x3.
a) Finde unter den folgenden sechs Formeln jene drei Paare, die dasselbe
Modell definieren.
i. y ∼ x1 * x2 iv. y ∼ x1 * x2 - x2
ii. y ∼ x1/x2 v. y ∼ x1 + x2
iii. y ∼ x1 + x2 + x1:x2 vi. y ∼ x1 * x2 - x1:x2
Hat er das Modell korrekt in eine Formel übersetzt? Falls nicht, korrigiere
seinen Ansatz.
624 J Data Science und Statistik in der Praxis
39 Programmierpraxis
Neben der übersichtlichen Gestaltung einer Codedatei besteht die Möglichkeit, den
Code auf mehrere Dateien aufzuteilen aber gemeinsam auszuführen. Mit dem Befehl
source() wird Code aus einer beliebigen Datei oder URL eingelesen, geparsed (siehe
?parse für Details) und Zeile für Zeile ausgeführt.
Das Ausführen eines R-Codes in der Console und mittels source() verhält sich etwas
unterschiedlich:
• Im Gegensatz zur zeilenweisen Ausführung des Codes erscheinen bei source()
standardmäßig weder die Befehle noch die Ergebnisse in der Console. Mit den
Optionen echo = TRUE und print.eval = TRUE können sowohl die Befehle
sichtbar gemacht als auch die automatische print-Funktion aktiviert werden.
Möchten wir nur einzelne Ergebnisse drucken, können wir dies mit print- oder
cat-Statements erzielen (siehe (31)).
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_39
39 Programmierpraxis 625
• Enthält die mit source() inkludierte Datei syntaktische Fehler (siehe (39.3)
für deren Definition), wird im Gegensatz zum schrittweisen Ausführen in der
Console kein Code ausgeführt. Bei semantischen Fehlern wird der Code bis
zur fehlerhaften Zeile ausgeführt.
Beim Aufteilen von Code ist es sinnvoll, inhaltlich abgegrenzte Teile in unterschied-
lichen Dateien abzuspeichern. Angenommen unser Projekt besteht aus drei Teilen:
Funktionen.R - Funktionen definieren
bmi <- function(gewicht, groesse) {
return(gewicht / (groesse / 100) ^ 2)
}
Auswertung.R - Auswertungen
print(bmi(gewicht, groesse))
Für das Ausführen des gesamten Codes definiert man üblicherweise eine Masterdatei,
die folgendermaßen aussehen kann:
39.2.1 Einleitung
1. separate Code-Chunks
2. Inline-Code
In Code-Chunks können beliebige Operationen in R durchgeführt werden. Für
jeden Code-Chunk wird über Optionen festgelegt, welche Elemente in den Text in-
tegriert werden sollen und wie. Einige häufig verwendete Optionen mit ihren stan-
dardmäßigen Werten sind:
• echo = TRUE: mit TRUE oder FALSE legen wir fest, ob der R-Code in den Text
übernommen werden soll oder nicht.
• fig.show = ’as.is’: bestimmt, ob und wie Grafiken inkludiert werden. Re-
levante Werte sind ’as.is’ für das Einfügen an Ort und Stelle, mit ’hold’
wird die Grafik am Ende des Code-Chunks eingefügt, ’hide’ generiert den
Plot, fügt ihn aber nicht ein.
• results = ’markup’ legt fest, ob und wie Outputs, die normalerweise in
die Console gedruckt werden, integriert werden sollen. Standardmäßig wird
mit dem Wert ’markup’ das Resultat in der eingestellten Markup-Sprache
(LATEX, HTML etc.) dargestellt. ’asis’ inkludiert das Resultat unformatiert,
mit ’hide’ wird das Resultat unterdrückt, der Code aber ausgeführt. ’hold’
stellt den Output an das Ende des Code-Chunks, mit Markup-Formatierung.
Um mehrere Optionen zu spezifizieren, müssen wir zwischen den Optionen Kommas
setzen. Eine vollständige Liste der Optionen findet sich auf der knitr-Seite:
https://yihui.name/knitr/options/
39 Programmierpraxis 627
\documentclass{article}
\begin{document}
Der Mittelwert aus den Zahlen 3, 7, 8 ist:
<<echo=FALSE>>=
x <- c(3, 7, 8)
mean(x)
@
Die Standardabweichung lautet \Sexpr{round(sd(x), 1)}.
\end{document}
<html>
<body>
Der Mittelwert aus den Zahlen 3, 7, 8 ist:
<!--begin.rcode echo=FALSE
x <- c(3, 7, 8)
mean(x)
end.rcode-->
Die Standardabweichung lautet <!--rinline round(sd(x), 1) -->.
</html>
</body>
---
output: pdf_document
---
Der Mittelwert aus den Zahlen 3, 7, 8 ist:
‘‘‘{r echo=FALSE}
x <- c(3, 7, 8)
mean(x)
‘‘‘
Die Standardabweichung lautet ‘r round(sd(x), 1)‘.
628 J Data Science und Statistik in der Praxis
Mit der Funktion knit() aus dem Paket knitr führen wir die Code-Chunks und
Inline-Codes aus und integrieren das Ergebnis in das Markup-Dokument.
> knitr::knit("beispiel.Rnw")
> knitr::knit("beispiel.Rhtml")
> knitr::knit("beispiel.Rmd")
Das R Markdown-Dokument beispiel.md lässt sich mit dem Befehl render() aus
dem Paket rmarkdown in ein HTML-, PDF- oder Word-Dokument umwandeln. Für
die Umwandlung in ein PDF-Dokument muss wiederum eine TeX-Distribution in-
stalliert sein.
Als Alternative zu den Befehlen knit() und render() existiert in RStudio die Mög-
lichkeit, das R Markdown-Dokument “auf Knopfdruck” in ein HTML-, PDF- oder
Word-Dokument umzuwandeln.
39 Programmierpraxis 629
Der Begriff Fehler oder Error ist beim Programmieren von Beginn an omnipräsent.
Während bei kurzen Programmen der Fehler meist recht schnell behoben ist, kann
sich die Fehlersuche in der Praxis bei längeren Projekten sehr mühsam gestalten.
39.3.1 Fehlertypen
• Logische Fehler: Inhaltliche Fehler, zum Beispiel möchten wir ganzzahlig divi-
dieren, schreiben aber:
> 34 / 7
[1] 4.857143
Syntaktische Fehler sind meist rasch gefunden. Die Ausführung des Codes wird
sofort unterbrochen und oft weist uns die Fehlermeldung bereits in die richtige Rich-
tung. Editoren versuchen während der Eingabe durch automatische Vervollständi-
gung von Befehlen, Anzeigen der offenen Klammern etc. derlei Fehlern vorzubeugen.
Semantische Fehler können erst während der Ausführung des Codes erkannt wer-
den. Speziell bei der Programmierung von komplexen Funktionen ist die Fehlersuche
aufwändiger, siehe dazu (39.3.2).
Logische Fehler können sehr heimtückisch sein. Das Programm läuft ohne Fehler-
meldung, das Ergebnis ist jedoch inhaltlich falsch. Je plausibler das falsche Ergebnis
ist, umso leichter werden Fehler übersehen.
Hinzu kommt, dass bei typischen statistischen Anwendungen, wo Code auf Daten
angewendet wird, inputspezifische Fehler auftreten können. Der Code läuft über
weite Strecken fehlerfrei. Tauchen plötzlich unvorhergesehene Werte (z. B. NA-Werte)
in den Daten auf, kommt es trotz sorgfältiger Überprüfung zu falschen Berechnungen.
Bereits in (7.4) beschäftigten wir uns mit diesem Problem.
630 J Data Science und Statistik in der Praxis
Im Rahmen einer Datenaufbereitung möchten wir beispielsweise die Anzahl der Wer-
te des Vektors x, die größer als 1 sind, bestimmen. In der Regel ergeben die drei
möglichen Codes dasselbe Ergebnis:
> x <- 1:2
Grundsätzlich treten Fehler bei Funktionen entweder bereits bei der Erzeugung der
Funktion oder erst beim Funktionsaufruf auf. Für R-Funktionen gilt die sogenannte
Lazy Evaluation Regel, das heißt, Ausdrücke werden erst ausgewertet, wenn auf sie
erstmalig zugegriffen wird.
39 Programmierpraxis 631
Fehlermeldungen bei der Erzeugung von Funktionen treten nur bei syntaktischen
Fehlern auf. Die Funktion
ohne Fehlermeldung, auch wenn das Objekt z nicht global verfügbar ist. Bei der
Ausführung der Funktion durch f2() erscheint die Fehlermeldung, dass das Objekt
z nicht gefunden wurde.
> f2()
Fehler in f2() : Objekt ’z’ nicht gefunden
Für die Fehlersuche von semantischen und logischen Fehlern in Funktionen gibt
es unterschiedliche Herangehensweisen. Einige wollen wir an folgendem, einfachen
Beispiel demonstrieren.
> f3 <- function(x) {
+ y <- log(x[x > 0])
+ return(t.test(x, y, paired = TRUE))
+ }
> f3(0:4)
Fehler in complete.cases(x, y) : nicht alle Argumente haben gleiche Länge
Beim Funktionsaufruf wird ein Fehler ausgegeben, da der gepaarte t-Test zwei Vekto-
ren gleicher Länge verlangt und y in f3() nur für die positiven Werte von x gebildet
wird. Würden wir f3() nur mit positiven x-Werten aufrufen, würde sie fehlerlos
laufen.
Eine Funktion global machen
Im ersten Schritt müssen alle Inputparameter an Objekte übergeben werden. An-
schließend können die Befehle der Funktion schrittweise ausgeführt und auf Fehler
untersucht werden.
Wir erkennen, dass der Fehler erst beim letzten Befehl entsteht. Zusätzlich ermög-
licht das Globalmachen einer Funktion die temporär erzeugten Objekte zu untersu-
chen, in diesem Fall etwa das Objekt y.
print()-Befehle einbauen
An kritischen Stellen in der Funktion werden mittels print() oder cat() die ak-
tuellen Inhalte von Objekten, die nur während des Funktionsaufrufs existieren, auf
der Console ausgegeben.
Wir erkennen, dass die Längen von x und y unterschiedlich sind. Für komplexere
Funktionen und umfangreichere Objekte ist das Einbauen von print()-Befehlen
nicht praktikabel.
traceback() verwenden
Die angezeigte Fehlermeldung bezieht sich immer auf jene Funktion, die den Fehler
hervorruft. Diese Funktion kann innerhalb anderer Funktionen verschachtelt sein
und somit lässt sich das Problem schwer lokalisieren. In diesem Fall hilft uns die
Funktion traceback(). Sie listet alle Funktionen auf, die bis zur Funktion, die den
Fehler hervorgerufen hat, aufgerufen wurden.
> f3(0:4)
Fehler in complete.cases(x, y) : nicht alle Argumente haben gleiche Länge
> traceback()
4: complete.cases(x, y)
3: t.test.default(x, y, paired = TRUE)
2: t.test(x, y, paired = TRUE) at #3
1: f3(0:4)
Im vorliegenden Fall tritt der Fehler bei der Funktion complete.cases() auf, die
innerhalb der Funktion t.test.default() aufgerufen wird. Diese Methode wird
wiederum durch die generische Funktion t.test() in der Funktion f3() aufgerufen.
browser()-Befehle einbauen
Mit der Funktion browser() können an verschiedenen Stellen der fehlerhaften Funk-
tion Breakpoints gesetzt werden, an denen die Funktion bei der Ausführung stoppt.
39 Programmierpraxis 633
Beim Aufruf der Funktion erscheint in der Console das browser-Prompt Browse[1]>.
Im browser-Prompt können nun Objekte angezeigt werden und R-Befehle eingege-
ben werden, die im Environment an der jeweiligen Stopp-Position ausgeführt werden.
> f3_browser(0:4)
Called from: f3_browser(0:4)
Browse[1]> x
[1] 0 1 2 3 4
Browse[1]> y
Fehler: Objekt ’y’ nicht gefunden
Das Objekt x ist an dieser Stelle vorhanden und wird angezeigt, y wurde noch nicht
erzeugt. Um den Browser zu verlassen, geben wir c im browser-Prompt ein.
Browse[1]> c
Called from: f3_browser(0:4)
Browse[1]>
In unserem Fall haben wir einen zweiten browser()-Befehl in die Funktion integriert.
Entsprechend stoppt die Funktion wieder mit einem browser-Prompt. An diesem
Punkt ist auch das Objekt y erzeugt und wir können darauf zugreifen:
Browse[1]> y
[1] 0.0000000 0.6931472 1.0986123 1.3862944
Browse[1]> length(x) == length(y)
[1] FALSE
Browse[1]> c
Fehler in complete.cases(x, y) : nicht alle Argumente haben gleiche Länge
Ein weiteres Mal verlassen wir den Browser. Die Funktion wird weiter ausgeführt
und es kommt zur Fehlermeldung. Neben dem Steuerungskommando c stehen noch
weitere Kommandos zur Verfügung, die in der browser()-Hilfe aufgelistet sind.
debug() verwenden
Alternativ zum händischen Einfügen der Breakpoints mit browser() kann die feh-
lerhafte Funktion mit debug() gekennzeichnet werden. Beim Funktionsaufruf wird
anschließend nach jeder Codezeile die Ausführung gestoppt.
> f3(0:4)
debugging in: f3(0:4)
debug bei #1:{
y <- log(x[x > 0])
return(t.test(x, y, paired = TRUE))
Browse[2]>
Anschließend rufen wir die zu untersuchende Funktion auf. Es erscheint ein Auswahl-
menü mit der Liste der verschachtelten Funktionsaufrufe, wo der Fehler auftrat.
> f3(0:4)
Fehler in complete.cases(x, y) : nicht alle Argumente haben gleiche Länge
1: f3(0:4)
2: #3: t.test(x, y, paired = T)
3: t.test.default(x, y, paired = T)
4: complete.cases(x, y)
Auswahl: 3
Called from: t.test.default(x, y, paired = T)
Browse[1]> ls()
[1] "alternative" "conf.level" "dname" "mu" "paired"
[6] "var.equal" "x" "y"
Browse[1]> length(x) == length(y)
[1] FALSE
Browse[1]> c
Auswahl: 0
>
Wir wählen die Funktion mit der Zahl 3 aus und zeigen mit ls() alle Objekte an, die
bei diesem Funktionsaufruf zur Verfügung standen. Mit dem Steuerungskommando
c analog zur Funktion browser() kehren wir wieder in das Auswahlmenü zurück,
welches wir mit 0 verlassen können.
39 Programmierpraxis 635
Fehlersuche in RStudio
Die Entwicklungsumgebung RStudio bietet erweiterte Möglichkeiten zur Fehlersuche
und Fehlerkorrektur an. Im Menüpunkt Debug > On Error kann die Option “Error
Inspector” gewählt werden. Bei jedem Funktionsaufruf mit Fehlermeldung werden
anschließend die Fehlermeldung grau hinterlegt und am rechten Rand die Optionen
“Show Traceback” und “Rerun with Debug” eingeblendet. Per Mausklick kann
die Fehlersuche gestartet werden.
Darüber hinaus bietet RStudio eine menübasierte Fehlersuche an. Das R-Script
mit der zu untersuchenden Funktion muss dazu in einer eigenen Datei mit dem
Namen der Funktion abgespeichert werden. Am linken Rand des Codefensters wer-
den per Mausklick rote Punkte an allen Codezeilen der Funktion gesetzt, wo ei-
ne Inspektion durchgeführt werden soll. Anschließend muss die Funktion über den
Source-Button am rechten oberen Rand des Fensters gestartet werden.
In einem neuen R-Script-Fenster wird nun die Funktion aufgerufen und stoppt an
allen festgelegten Positionen mit einem browser-Prompt. Neben den Steuerungs-
kommandos für browser() blendet RStudio auch Steuerung-Buttons am oberen
Rand der Console ein.
39.4 Abschluss
39.4.2 Ausblick
Wer mehr zur Markup-Sprache R Markdown erfahren möchte, findet unter https:
//rmarkdown.rstudio.com ausführliche Informationen inklusive Cheatsheet und
Reference Guide.
39.4.3 Übungen
1. In Abb. 39.1 auf Seite 628 ist das PDF-Dokument zur Datei beispiel.Rnw
dargestellt.
a) Im Dokument ist der R-Code, der das Ergebnis (6) erzeugt, nicht sichtbar.
Wie können wir den R-Code im Dokument sichtbar machen?
b) Uns stören die beiden #-Zeichen vor dem Ergebnis. Wie können wir diese
weglassen?
c) Der Hintergrund des Code-Chunks ist grau eingefärbt. Wir möchten dies
auf farblos ändern!
d) Anstatt der Zahlen 3, 7 und 8 möchten wir den Mittelwert und die Stan-
dardabweichung aus 200 standardnormalverteilten Zufallszahlen berech-
nen. Achte darauf, dass die Maßzahlen mit einer vernünftigen Anzahl an
Dezimalstellen dargestellt werden.
e) Wir möchten die in 1d) erstellten Zahlen in Form eines Histogramms
darstellen. Die Abbildung soll im Bericht inkludiert werden, wir möchten
eine Beschreibung der Abbildung (caption) erstellen und die Höhe der
Abbildung auf ca. 10 cm beschränken.
f) Abschließend wollen wir eine Überschrift vergeben. In LATEXverwenden
wir dazu den Befehl \section{Überschrift }.
2. Führe die Schritte aus Beispiel 1 soweit möglich für die Dateien
a) beispiel.Rhtml und
b) beispiel.Rmd
durch.
Hinweis: wertvolle Hinweise finden sich unter folgendem Link:
https://yihui.name/knitr/options/
39 Programmierpraxis 637
Shiny ist ein R-Paket, mit dem auf einfache Weise R-Programme als interak-
tive Webanwendung gestaltet werden können. Die Webanwendung kann mit
jedem Internetbrowser aufgerufen werden und es ist keine lokale Installati-
on von R erforderlich. Shiny wird von RStudio zur Verfügung gestellt (siehe
https://shiny.rstudio.com), kann aber auch unabhängig von RStudio be-
nutzt werden.
Wir widmen uns der folgenden Aufgabenstellung: Ohne R auf ihrem Rechner in-
stalliert zu haben und ohne Programmierkenntnisse zu besitzen, soll eine Person die
Verteilung einer Stichprobe normalverteilter Zufallszahlen grafisch darstellen kön-
nen. Die Größer der Stichprobe sowie Mittelwert und Standardabweichung der zu-
grundeliegenden Normalverteilung sollen frei wählbar sein.
Die folgenden Fragen zur obigen Aufgabenstellung wollen wir beantworten:
• Wie können wir die Inputwerte für Mittelwert, Standardabweichung und Stich-
probengröße an die Shiny-Anwendung übergeben? (40.3.2)
• Wie integrieren wir den R-Code zur Erstellung der Zufallszahlen und der Gra-
fik? (40.2)
• Wie können wir die Grafik in der Anwendung ausgeben? (40.3.2)
• Wie programmieren wir Shiny-Anwendungen mit Unterseiten? (40.3.1)
Shiny-Anwendungen bestehen aus zwei Teilen: dem User Interface Objekt (UI)
und der Serverfunktion. Beide Teile können entweder gemeinsam in einem R-Skript
gespeichert werden oder in getrennten Dateien mit den Namen ui.R und server.R.
In beiden Fällen müssen die R-Skripts in einem eigenen Ordner abgelegt werden,
dessen Name der Anwendung entspricht. Insbesondere wenn in den Skripts Son-
derzeichen verwendet werden, ist es notwendig das Skript mit UTF-8 Encoding zu
speichern.
Im User Interface Objekt werden das Layout der Webseite und die Inhalte der
Webseitenelemente festgelegt. Das Layout kann zum Beispiel aus zwei nebeneinan-
der oder drei untereinander liegenden Elementen bestehen. Standardmäßig wird die
Webseite als fluidPage definiert, damit die Größen der Elemente automatisch an
die Displaygröße der Geräte der Anwender angepasst werden. Mögliche Inhalte der
Elemente sind Eingabefelder aller Art, Text, Grafiken und Tabellen.
Die Serverfunktion umfasst alle Berechnungen in R, die für die Anwendung durch-
geführt werden sollen. Die Serverfunktion besitzt als Argumente die Objekte input
und output. Innerhalb dieser Funktion kann auf die Elemente von input, die in
den Eingabefeldern des User Interfaces definiert wurden, zugegriffen werden. Dem
output-Objekt können Elemente hinzugefügt werden, auf die im User Interface zu-
gegriffen wird.
Eine minimale Shiny-Anwendung sieht folgendermaßen aus:
library(shiny)
ui <- fluidPage()
server <- function(input, output){}
shinyApp(ui = ui, server = server)
Der letzte Befehl shinyApp() generiert aus dem User Interface Objekt und der Ser-
verfunktion ein Shiny-App Objekt. Der Befehl ist nur notwendig, wenn das User In-
terface Objekt und die Serverfunktion in einem gemeinsamen R-Skript abgespeichert
werden. RStudio erkennt automatisch, dass ein R-Skript eine Shiny-Anwendung ent-
hält, wenn entweder die Funktion shinyApp() vorkommt oder das R-Skript ui.R
oder server.R heißt.
eine Verbindung zu shinyapps.io hergestellt werden. Die Werte für die Parameter
name, token und secret erhält man über das persönliche Profil des shinyapps.io-
Accounts. Anschließend kann die Shiny-Anwendung mit
deployApp(appDir)
installiert werden. Über den Parameter appDir wird das lokale Verzeichnis der Shiny-
Anwendung übergeben.
Aus RStudio heraus können Shiny-Anwendungen direkt über einen Publish-Button
auf http://shinyapps.io bereitgestellt werden. Zuvor muss im Menüpunkt Tools >
Global Options > Publishing die Verbindung zum Shiny-Server eingerichtet werden.
Shiny-Anwendungen sind interaktiv. Das heißt, dass der User bestimmte Elemen-
te der Webseite verändern kann (Inputs) und als Reaktion darauf Berechnungen,
Darstellungen und ähnliches veranlasst werden. Diese erscheinen dann in Form von
Outputs auf der Webseite.
Als R-Shiny Programmierer müssen wir uns nur wenige Gedanken über diese Prozes-
se (das Event Handling) im Hintergrund machen. Ausnahmen sind rechenintensive
Prozesse. Werden die Ergebnisse derselben Berechnung für mehrere Outputs benö-
tigt, wird die Berechnung standardmäßig mehrmals durchgeführt.
Dies kann verhindert werden, indem das Ergebnis der Berechnung mit einem so-
genannten Conductor gespeichert wird. Der Conductor reagiert auf neue Inputs
und übergibt das Ergebnis an alle Outputs. Definiert wird ein Conductor mit der
Funktion reactive(). Ein Beispiel zur Verwendung von reactive() findet sich in
(40.2) und weiterführende Informationen gibt es auf der Webseite
https://shiny.rstudio.com/articles/reactivity-overview.html.
Die Serverfunktion umfasst alle Berechnungen in R, die für die Anwendung durch-
geführt werden sollen. R-Inputs werden vom User Interface erfasst und intern wei-
tergegeben. Auf die Inputwerte kann mit R-Befehlen zugegriffen werden. Bei je-
der Veränderung der Inputwerte im User Interface werden die Berechnungen erneut
durchgeführt.
40 Interaktive Webanwendungen mit Shiny 641
Die Serverfunktion wird also in Form einer eigenen Funktion mit dem Namen server
und den Argumenten input und output definiert. Das Objekt input ist eine Liste,
deren Elemente im User Interface definiert und befüllt werden.
Das output-Objekt ist ebenfalls eine Liste. Die output-Elemente werden mittels
eigener Shiny-Funktionen in der Serverfunktion erstellt.
Im folgenden Beispiel übergeben wir mit der Funktion renderPlot() ein Histo-
gramm mit 10 standardnormalverteilten Zufallszahlen an das User Interface. Das
Output-Listenelement nennen wir p1. Inputs werden keine benötigt.
Tab. 40.1 gibt einen Überblick der vorhandenen Funktionen zur Erstellung von Out-
putelementen, die sogenannten Render-Funktionen. Die Outputs sind ebenfalls re-
aktiv, das heißt, sie werden bei jeder Änderung der Inputwerte neu erstellt.
Render-Funktion Outputelement
renderTable() einfache Tabelle
renderDataTable() Tabelle mit Such-, Sortier-, Filterfunktionen etc.
renderPlot() R-Plot (wird intern in ein Bild umgewandelt)
renderImage() Bilddatei, kann für gespeicherte Plots und für beliebige
Bilder verwendet werden
renderPrint() R-Output über print()
renderText() R-Output über cat()
renderUI() reaktive HTML-Elemente. Gestaltung und Ein- bzw.
Ausblenden von HTML-Elementen in Abhängigkeit von
Inputs. Mit tagList() können mehrere Elemente
gleichzeitig eingefügt werden.
Werden dieselben Berechnungen für mehr als ein Outputelement benötigt, empfiehlt
sich die Verwendung der Funktion reactive(). Im Beispiel stellen wir dieselben
Zufallszahlen mittels Histogramm und Dotplot dar. Beachte, dass reactive() eine
expression zurückgibt, die mit () übergeben werden muss.
Das User Interface ist de facto eine HTML-Webseite, die über das Objekt ui defi-
niert wird. Für die Erstellung dieser Webseite stehen eigene Shiny-Funktionen sowie
die komplette Palette der HTML-Befehle zur Verfügung. Die Shiny-Funktionen sind
komplexe Funktionen, die typische Elemente einer Shiny-Anwendung wie Schiebe-
regler, Tabellen und Plots in HTML-Code übersetzen. Intern wird dabei auf das
Bootstrap-Framework (siehe www.getbootstrap.com) zurückgegriffen, das eine Rei-
he von Gestaltungsvorlagen umfasst. Der HTML-Code des User Interfaces kann wie
bei jeder Webseite im Browser unter “Seitenquelltext anzeigen” eingesehen werden.
40.3.1 Layout
Das Layout der Shiny-Anwendung greift auf das für HTML-Webseiten übliche Grid-
system zurück, das heißt, alle Webseitenelemente werden in Zeilen angeordnet und
jede Zeile kann in Spalten unterteilt werden. Mit dem Befehl fluidRow() können wir
eine neue Zeile generieren. Jede Zeile kann mit column() in bis zu 12 Spalten
unterteilt werden. Das folgende Beispiel generiert ein Layout mit 2 Spalten, die die
Bildschirmbreite im Verhältnis 1:2 aufteilen.
ui <- fluidPage(
fluidRow(
column(4, "Inhalte"),
column(8, "Inhalte")))
Als Inhalte können Inputelemente wie Schieberegler, Eingabefenster etc. oder Out-
putelemente wie Plots, Tabellen etc. eingefügt werden. Die entsprechenden Funkti-
onsaufrufe müssen mittels Komma getrennt werden, da sie Argumente der Funktion
fluidRow() darstellen.
40 Interaktive Webanwendungen mit Shiny 643
Ein typisches Layout ist das Sidebar-Layout, wo links eine schmälere Seitenleiste
positioniert wird, die meist für Inputelemente eingesetzt wird. Der restliche Platz
wird den Outputelementen gewidmet. Dieses Layout kann ebenfalls mit den Gridbe-
fehlen dargestellt werden, es existiert dafür aber auch die Highlevel-Layoutfunktion
sidebarLayout() in Shiny.
ui <- fluidPage(
sidebarLayout(
sidebarPanel("Inhalte"),
mainPanel("Inhalte")))
ui <- fluidPage(
navbarPage(title = "Titel",
tabPanel("Unterseite 1", "Inhalt"),
tabPanel("Unterseite 2", "Inhalt")))
Nachdem wir das Layout der Shiny-Webseite gestaltet haben, werden die einzelnen
Felder mit Inhalten befüllt. Zur Verfügung stehen zahlreiche Funktionen zur Er-
stellung von Input- und Outputelementen, es können aber auch einfache Texte als
Inhalte übergeben werden.
Zur Veranschaulichung befüllen wir im Sidebar-Layout Beispiel aus (40.3.1) das linke
sidebarPanel() mit einem Inputelement, nämlich einem Schieberegler.
644 J Data Science und Statistik in der Praxis
ui <- fluidPage(
sidebarLayout(
sidebarPanel(
sliderInput(
inputId = "x",
label = "Stichprobengröße:",
min = 10,
max = 500,
value = 100)),
mainPanel("Inhalte")))
In Abb. 40.1 sehen wir das linke Panel mit dem Schieberegler.
Tab. 40.2 listet die verfügbaren Inputelemente mit den dazugehörigen Funktionen
auf.
Die Standardparameter für Inputfunktionen sind inputID, label und width.
Eine Ausnahme ist das Element submitButton(), für das keine inputID und kein
label angegeben wird.
inputID und label wurden bereits im Rahmen des Schiebereglerbeispiels beschrie-
ben. width bezieht sich auf die Breite des Elements. Diese muss im Gegensatz zu
inputID und label nicht verpflichtend angegeben werden. Die maximale Element-
breite ist bereits durch das Layout festgelegt.
40 Interaktive Webanwendungen mit Shiny 645
Manche Funktionen sehen verpflichtende zusätzliche Parameter vor, die in Tab. 40.2
in der Spalte Parameter angegeben sind. Darüber hinaus existieren noch optionale
Parameter, die der entsprechenden R-Hilfe entnommen werden können.
Outputfunktion Render-Funktion
tableOutput() renderTable()
dataTableOutput() renderDataTable()
imageOutput() renderImage()
plotOutput() renderPlot()
verbatimTextOutput() renderPrint()
textOutput() renderText()
uiOutput() renderUI()
646 J Data Science und Statistik in der Praxis
Wir wollen eine Shiny-App programmieren, die eine anzugebende Anzahl normal-
verteilter Pseudozufallszahlen mit frei wählbaren Werten für Mittelwert und Stan-
dardabweichung erstellen und die Verteilung der Stichprobe grafisch darstellen soll.
Folgender Code, der das User Interface Objekt und die Serverfunktion in einem
gemeinsamen R-Skript verwaltet, erzeugt die Shiny-App in Abb. 40.2.
40 Interaktive Webanwendungen mit Shiny 647
In einer Version 2 möchten wir die Verteilung als Histogramm und in Form eines
Kerndichteschätzers auf zwei getrennten Unterseiten darstellen. Dazu verwen-
den wir die Layoutfunktion tabsetPanel().
40.5 Abschluss
• Welche R-Skripts müssen wir für eine Shiny-Anwendung erstellen und wie be-
nennen wir diese? Wie sieht eine minimale Shiny-Anwendung aus? Was bedeu-
ten die Objekte input und output und wie verwenden wir diese? (40.1.1)
• Was ist ein Conductor in Shiny? (40.1.3)
• Wie übergeben wir Tabellen bzw. Grafiken an das User Interface? Welche R-
Funktionen kommen dabei zum Einsatz? (40.2)
• Wie erstellen wir eine einfache Seite mit mehreren Spalten? Was ist ein Sidebar-
Layout und wie definieren wir es? Wie generieren wir ein Layout mit mehreren
Unterseiten? (40.3.1)
40 Interaktive Webanwendungen mit Shiny 649
40.5.2 Übungen
1. Erweitere die 2. Version der Shiny-App aus (40.4.2) um eine Checkbox. Wenn
die Box angeklickt wird, soll die theoretische Verteilung im Histogramm und
im Kerndichteschätzerplot eingezeichnet werden.
2. Programmiere eine Shiny-App, bei der ein User Interface Element abhängig
von einem Inputwert ein- bzw. ausgeblendet werden kann. Siehe dazu https:
//shiny.rstudio.com/articles/dynamic-ui.html.
41 Relevante R-Pakete
Im Folgenden wird eine Auswahl von R-Paketen aufgelistet und kurz beschrieben.
Pakete stellen in R Erweiterungen zu den vorhandenen Funktionen dar. Standard-
mäßig werden beim Start von R die folgenden sieben Pakete geladen:
• base: R-Basisfunktionen wie c(), paste() etc.
• datasets: Eine Sammlung von Datensätzen in R
• graphics: Standard-R-Grafikfunktionen
• grDevices: Die R-Grafik-Devices, Farben etc.
• methods: Klassen und generische Funktionen
• stats: Statistische Funktionen
• utils: Sammlung nützlicher Funktion wie data(), citation() etc.
• tidyverse: Eine Sammlung von ca. 50 Paketen, unter anderem zur Daten-
aufbereitung und Visualisierung. Auf einige Pakete gehen wir im Detail ein.
Informationen zu tidyverse finden sich unter https://www.tidyverse.org.
• data.table: Erweitung von data.frame-Objekten. Ermöglicht effiziente Ag-
gregationen und Manipulation von Datensätzen.
• dplyr: Alternative Datenmanipulationsfunktionen, Teil von tidyverse. Zen-
trale Funktionen: mutate(), select(), filter(), summarise(), arrange()
• tibble: Eine Alternative zum Objekttyp data.frame, Teil von tidyverse
• Rcpp: Funktionen für die Integration von C++ in R
41.2 Grafik
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6_41
41 Relevante R-Pakete 651
41.3 Import/Export
41.4 Funktionensammlungen
• car (Companion to Applied Regression): Funktionen aus dem Buch von J. Fox
und S. Weisberg: An R Companion to Applied Regression. Enthält u. a. die
Funktionen qqPlot() für Quantil-Quantil-Plots inklusive Konfidenzintervalle,
Anova() für Quadratsummen des Typs II und III und BoxCox() für Box-Cox
Transformation.
• Hmisc (Harrell Miscellaneous): Harrell bezieht sich auf den bekannten Biosta-
tistiker Frank Harrell. Das Paket umfasst Funktionen zu Datenanalyse, Stich-
probenplanung und Powerberechnung, Imputation fehlender Werte und vieles
mehr.
• MASS (vorinstalliert): Funktionen aus dem Buch von W.N. Venables and B.D.
Ripley “Modern Applied Statistics with S”. Wichtige Funktionen sind z. B.
isoMDS() für Multidimensionale Skalierung, rlm() für robuste Regression,
polr() für Probit-Regression.
• vcd (Visualizing Categorical Data): Funktionen für kategorielle Daten, Be-
gleitung zum Buch “Discrete Data Analysis with R” von M. Friendly und D.
Meyer.
• lubridate: Funktionen für Datum- und Uhrzeitdaten, Teil von tidyverse
• stringi, stringr: Alternative Funktionen für Textmanipulationen. stringr
ist eine Adaption des Pakets stringi für tidyverse.
652 J Data Science und Statistik in der Praxis
42 R-Ressourcen
Stichwortverzeichnis
Symbols C
χ2 -Test . . . . . . . . . . . . . . . . . . . . . . . . . . 599 Case sensitivity. . . . . . . . . .31, 130, 269
Choleskyzerlegung . . . . . . . . . . . . . . . 205
A Cochran-Mantel-Haenszel-Test . . . 600
Abbruchsbedingung . . . . . . . . . . . . . . 391 Codierung . . . . . . . . . . . . . . . . . . . . . . . 312
Absolutbetrag . . . . . . . . . . . . . . . . . . . . 49 Consolenausgabe . . . . . . . . . . . . . . . . . 442
Achsen . . . . . . . . . . . . . . . . . . . . . . . . . . 522 Consoleneingabe . . . . . . . . . . . . . . . . . 446
Additives Modell. . . . . . . . . . . . . . . . .611 Copy & Paste-Fehler . . . . . . . . . . . . . 101
Adjustierungsparameter . . . . . . . . . . 526 Cosinus . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Annähernde Gleichheit . . . . . . . . . . . 151 csv-Datei . . . . . . . . . . . . . . . . . . . . . . . . 455
Anonyme Funktion . . . . . . . . . . . . . . 424
Anweisungsblock . . . . . . . . . . . . . . . . . 381 D
Arbeitsverzeichnis . . . . . . . . . . . . . . . . . 64 Dataframe . . . . . . . . . . . . . . . . . . . . . . . 238
abfragen. . . . . . . . . . . . . . . . . . . . . .64 Datei
wechseln . . . . . . . . . . . . . . . . . . . . . 64 csv-Datei . . . . . . . . . . . . . . . . . . . . 455
Excel-Datei . . . . . . . . . . . . . . . . . 474
Argumente
SAS-Datei. . . . . . . . . . . . . . . . . . .475
formale Argumente . . . . . . . . . . . 41
SPSS-Datei . . . . . . . . . . . . . . . . . 475
Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
Textdatei . . . . . . . . . . . . . . . . . . . 455
Atomares Objekt . . . . . . . . . . . . . . . . 210
Dateipfad . . . . . . . . . . . . . . . . . . . . 64, 455
Attribut . . . . . . . . . . . . . . . . . . . . . . . . . 351
Datentyp
Ausreißer . . . . . . . . . . . . . . . . . . . . . . . . . 94
einfacher Datentyp . . . . . . . . . . 132
Datums- und Uhrzeitklassen . . . . . 356
B Debugging . . . . . . . . . . . . . . . . . . . . . . . 630
Balkendiagramm . . . . . . . . . . . . . . . . . 540 Defaultwert . . . . . . . . . . . . . . . . . . 43, 405
Bedingte Maßzahl. . . . . . . . . . . . . . . .162 dynamischer Defaultwert 43, 407
Befehl. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .5 statischer Defaultwert . . 43 f., 406
unvollständiger Befehl . . . . . . . . . 5 Der exakte Test nach Fisher . . . . . 600
vollständiger Befehl . . . . . . . . . . . . 5 Designmatrix . . . . . . . . . . . . . . . . . . . . 613
Beschriftung . . . . . . . . . . . . . . . . . . . . . 140 Determinante . . . . . . . . . . . . . . . . . . . . 198
ersetzen . . . . . . . . . . . . . . . . . . . . . 142 Device . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
hinzufügen . . . . . . . . . . . . . . . . . . 142 Diagonalmatrix . . . . . . . . . . . . . . . . . . 196
löschen . . . . . . . . . . . . . . . . . 143, 158 Dichtefunktion . . . . . . . . . . . . . . . . . . . 113
Bestimmtheitsmaß . . . . . . . . . . . . . . . 610 Differenzenvektor . . . . . . . . . . . . . . . . 106
Binomialkoeffizient . . . . . . . . . . . . . . . . 82 Differenzmenge . . . . . . . . . . . . . . . . 73, 87
Binomialtest . . . . . . . . . . . . . . . . . . . . . 596 Dimension . . . . . . . . . . . . . . . . . . . . . . . 174
Bisektionsalgorithmus . . . . . . . . . . . . 414 Doppelzuweisung . . . . . . . . . . . . . . . . 199
boolean . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Dreiecksmatrix. . . . . . . . . . . . . . . . . . .203
Boxen . . . . . . . . . . . . . . . . . . . . . . . . . . . 522 Dreipunkteargument . . . . . . . . . 44, 410
Boxplot . . . . . . . . . . . . . . . . . . . . . . . . . . 549 Dynamischer Defaultwert. . . . .43, 407
Breakpoint
Fehlersuche . . . . . . . . . . . . . . . . . 632 E
Kategorisierung . . . . . . . . . . . . . 319 Eigenvektor . . . . . . . . . . . . . . . . . . . . . . 205
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6
Stichwortverzeichnis 657
Matching . . . . . . . . . . . . . . . . . . . . . . . . . 72 sichern . . . . . . . . . . . . . . . . . . . . . . . 65
Matchlängenoperator . . . . . . . . . . . . 289 Oder-Verknüpfung . . . . . . . . . . . . . . . . 23
Mathematische Rundung . . . . . . . . . . 48 Operator . . . . . . . . . . . . . . . . . . . . . . . . 219
Matrixdimension . . . . . . . . . . . . . . . . . 174 als Funktion verwenden . . . . . 219
Matrixinversion . . . . . . . . . . . . . . . . . . 198 Hilfeoperator . . . . . . . . . . . . . . . . . 39
Matrixmultiplikation . . . . . . . . . . . . . 198 Logische Operatoren. . . . . . . . . .21
Matrixplot . . . . . . . . . . . . . . . . . . . . . . . 536 Matchlängenoperator . . . . . . . . 289
Matrixtransponierung . . . . . . . . . . . . 175 Modulooperator . . . . . . . . . . . . . . 46
McNemar-Test . . . . . . . . . . . . . . . . . . . 598 Negationsoperator . . . . . . . . . . . . 19
Median . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Rechenoperator . . . . . . . . . . . . . . . . 9
Mehrfachsortierung . . . . . . . . . . . . . . 78 f. Verknüpfungsoperator . . . 23, 405
Mengenfunktion. . . . . . . . . . . . . . . . . . .71 Zuweisungsoperator. . . . . . . . . . . .7
Messfehler . . . . . . . . . . . . . . . . . . . . . . . . 91 Ordinalskalierte Variable . . . . . . . . . 322
Methode . . . . . . . . . . . . . . . . . . . . . . . . . 349
Min-Max-Transformation . . . . . . . . 124 P
Mittelwert . . . . . . . . . . . . . . . . . . . . . . . . 92
p-Distanz . . . . . . . . . . . . . . . . . . . . . . . . 420
Mode . . . . . . . . . . . . . . . . . . . . . . . . . . 132 f.
p-Norm . . . . . . . . . . . . . . . . . . . . . . . . . . 406
Modell
p-Wert. . . . . . . . . . . . . . . . . . . . . . . . . . .119
additives Modell. . . . . . . . . . . . .611
Paarweise Streudiagramme . . . . . . . 570
generalisiertes lineares M. . . . 621
pairs-Plot . . . . . . . . . . . . . . . . . . . . . . . . 570
lineares Modell . . . . . . . . . . . . . . 608
Paket
Nullmodell . . . . . . . . . . . . . . . . . . 612
installieren . . . . . . . . . . . . . . . . . . . 12
volles Modell . . . . . . . . . . . . . . . . 620
laden . . . . . . . . . . . . . . . . . . . . . . . . . 12
Modellbildung . . . . . . . . . . . . . . . . . . . 611
Parallele Liniengrafik . . . . . . . . . . . . 536
Modulooperator . . . . . . . . . . . . . . . . . . . 46
Paralleles Maximum . . . . . . . . . . . . . 103
Monospaced-Schriftart . . . . . . . . . . . 446
Paralleles Minimum . . . . . . . . . . . . . . 103
Monte-Carlo-Simulation. . . . . . . . . .499
Mosaicplot . . . . . . . . . . . . . . . . . . . . . . . 585 Parameter . . . . . . . . . . . . . . . . . . . . . . . . 41
Multiple lineare Regression . . . . . . 611 Adjustierungsparameter . . . . . 526
Multivariate Normalverteilung . . . 202 Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . 266
Permutation . . . . . . . . . . . . . . . . . . . . . 118
N Pfad
Negationsoperator . . . . . . . . . . . . . . . . 19 absoluter Pfad . . . . . . . . . . . 64, 455
Nominalskalierte Variable . . . . . . . . 322 relativer Pfad . . . . . . . . . . . . 64, 455
NULL-Objekt . . . . . . . . . . . . . . . . . . . . 158 Pfeile . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
Nullmodell . . . . . . . . . . . . . . . . . . . . . . . 612 Platzhalter . . . . . . . . . . . . . . . . . . . . . . 358
Plausibilitätscheck . . . . . . . . . . . . . . . 274
O plotten . . . . . . . . . . . . . . . . . . . . . . . . . . 507
Objekt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Poisson-Regression . . . . . . . . . . . . . . . 621
atomares Objekt . . . . . . . . . . . . 210 Polarkoordinaten . . . . . . . . . . . . . . . . 529
NULL-Objekt . . . . . . . . . . . . . . . 158 Polygon. . . . . . . . . . . . . . . . . . . . . . . . . .524
rekursives Objekt . . . . . . . . . . . 210 Polynom . . . . . . . . . . . . . . . . . . . . . . . . . 436
Objekte Prompt . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
anzeigen . . . . . . . . . . . . . . . . . . . . . . 62 Proportional-Schriftart . . . . . . . . . . . 446
laden . . . . . . . . . . . . . . . . . . . . . . . . . 65 Pseudozufallszahl . . . . . . . . . . . . . . . . 117
löschen . . . . . . . . . . . . . . . . . . . . . . . 63 Punktart . . . . . . . . . . . . . . . . . . . . . . . . 515
660 Stichwortverzeichnis
Q for-Schleife . . . . . . . . . . . . . . . . . . 385
QQ-Plot . . . . . . . . . . . . . . . . . . . . . . . . . 593 repeat-Schleife . . . . . . . . . . . . . . 391
QR-Zerlegung . . . . . . . . . . . . . . . 202, 205 while-Schleife. . . . . . . . . . . . . . . .389
Quadratische Plotregion . . . . . . . . . 526 Schleifenvariable . . . . . . . . . . . . . . . . . 385
Quadratwurzel . . . . . . . . . . . . . . . . . . . . . 7 Schlüssel . . . . . . . . . . . . . . . . . . . . . . . . . 255
Quantil . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Schnittmenge . . . . . . . . . . . . . . . . . . . 73 f.
Quantil-Quantil-Plot . . . . . . . . . . . . . 593 Schriftart
Quantilsfunktion . . . . . . . . . . . . . . . 113 f. Monospaced-Schriftart. . . . . . .446
Quartil . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Proportional-Schriftart . . . . . . 446
Scoping . . . . . . . . . . . . . . . . . . . . . . . . . . 416
R Scopingoperator . . . . . . . . . . . . . . . . . 418
R-Homepage . . . . . . . . . . . . . . . . . . . . . . . 2 Seed . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Rang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 Selektion
Rastergrafik . . . . . . . . . . . . . . . . . . . . . 565 aus einem Dataframe . . . . . . . . 241
Realteil . . . . . . . . . . . . . . . . . . . . . . . . . . 136 aus einem Vektor . . . . . . . . . . . . . 16
Rechenoperator . . . . . . . . . . . . . . . . . . . . 9 aus einer Liste . . . . . . . . . . . . . . 206
Rechteck . . . . . . . . . . . . . . . . . . . . . . . . . 524 aus einer Matrix. . . . . . . . . . . . .176
Rechtszuweisung . . . . . . . . . . . . . . . . . . . 9 Semantischer Fehler . . . . . . . . . . . . . . 629
Recycling . . . . . . . . . . . . . . . . . . . . 20, 248 Sequenz . . . . . . . . . . . . . . . . . . . . . . . 15, 40
Regression Serverfunktion . . . . . . . . . . . . . . . . . . . 640
lineare Regression . . . . . . . . . . . 608 Signifikanzniveau . . . . . . . . . . . . . . . . 119
logistische Regression . . . . . . . . 621 Singulärwertzerlegung . . . . . . . 202, 205
multiple lineare Regression . . 611 Sinus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Poisson-Regression . . . . . . . . . . 621 Skalierung . . . . . . . . . . . . . . . . . . . . . . . 513
Regular Expression . . . . . . . . . . . . . . 284 Skript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Rekursion. . . . . . . . . . . . . . . . . . . . . . . .425 Skriptdatei . . . . . . . . . . . . . . . . . . . . . . . . . 6
Rekursives Objekt . . . . . . . . . . . . . . . 210 sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Render-Funktion . . . . . . . . . . . . . . . . . 641 Spacing . . . . . . . . . . . . . . . . . . . . . . . . . . 189
repeat-Schleife . . . . . . . . . . . . . . . . . . . 391 Spaltenmittelwerte . . . . . . . . . . . . . . . 182
Replikation . . . . . . . . . . . . . . . . . . . . . . . 45 Spaltenprozent . . . . . . . . . . . . . . . . . . . 339
Residuum . . . . . . . . . . . . . . . . . . . . . . . . 199 Spaltensumme . . . . . . . . . . . . . . . . . . . 182
RGB-Farbraum . . . . . . . . . . . . . . . . . . 551 Spannweite . . . . . . . . . . . . . . . . . . . . . . . 91
RGBA-Farbraum . . . . . . . . . . . . . . . . 552 SPSS-Datei . . . . . . . . . . . . . . . . . . . . . . 475
Robustheit . . . . . . . . . . . . . . . . . . . . . . . . 94 Spur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Rumpf . . . . . . . . . . . . . . . . . . . . . . . . . . . 382 Stabdiagramm . . . . . . . . . . . . . . . . . 509 f.
Run Length . . . . . . . . . . . . . . . . . . . . . . 352 Standardabweichung . . . . . . . . . . . . . . 92
Rundung Standardfarbpalette . . . . . . . . . . . . . . 554
Kaufmännische Rundung . . . . . 48 Standardformat . . . . . . . . . . . . . . . . . . 361
Mathematische Rundung . . . . . 48 Standardisierung . . . . . . . . . . . . . 95, 231
Rundungsfehler . . . . . . . . . . . . . . . . . . 150 Statischer Defaultwert . . . . . . . . 43, 406
Rückreferenz . . . . . . . . . . . . . . . . . . . . . 298 Stichprobe ziehen . . . . . . . . . . . . . . . . 118
Streudiagramm . . . . . . . . . . . . . . . . . . 507
S String . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
SAS-Datei . . . . . . . . . . . . . . . . . . . . . . . 475 Stringanfang . . . . . . . . . . . . . . . . . . . . . 296
Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . 384 Stringende . . . . . . . . . . . . . . . . . . . . . . . 296
Endlosschleife . . . . . . . . . . . . . . . 391 Subsetting . . . . . . . . . . . . . . . . . . . . . . . . 16
Stichwortverzeichnis 661
T W
t-Test . . . . . . . . . . . . . . . . . . . . . . . 119, 602 Wahrscheinlichkeitsfunktion . . . . . . 116
Tabulator . . . . . . . . . . . . . . . . . . . . . . . . 443 Wall clock time . . . . . . . . . . . . . . . . . . 482
Tangens . . . . . . . . . . . . . . . . . . . . . . . . . . 50 Warnmeldung . . . . . . . . . . . . . . . . . . . . 403
Teilbarkeit . . . . . . . . . . . . . . . . . . . . . . . . 46 Warnung . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Teile-und-herrsche-Prinzip . . . . . . . 425 while-Schleife . . . . . . . . . . . . . . . . . . . . 389
Teilstring . . . . . . . . . . . . . . . . . . . . . . . . 273 White Space . . . . . . . . . . . . . . . . 298, 456
Test auf Gleichheit der Varianzen 605 Wilcoxon-Vorzeichen-Rang-Test . . 604
Test des Korrelationskoeffizienten 602 Wissenschaftliche Notation . . . . . . . . 82
Test für Anteilswerte. . . . . . . . . . . . .597 Workspace . . . . . . . . . . . . . . . . . . . . . . . . 11
Teststatistik . . . . . . . . . . . . . . . . . . . . . 119 Wrapper-Funktion . . . . . . . . . . . . . . . 406
Textdatei . . . . . . . . . . . . . . . . . . . . . . . . 455
Textsuche. . . . . . . . . . . . . . . . . . . . . . . .266 Z
überlappende Textsuche . . . . . 302 Zeichencodierung . . . . . . . . . . . . . . . . 459
Ties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 Zeichengruppe . . . . . . . . . . . . . . . . . . . 294
Totalprozent . . . . . . . . . . . . . . . . . . . . . 339 Zeichenketten . . . . . . . . . . . . . . . . . . . . 126
Transponierung . . . . . . . . . . . . . . . . . . 175 erstellen . . . . . . . . . . . . . . . . . . . . . 126
Treatmentkontrast . . . . . . . . . . . . . . . 613 extrahieren . . . . . . . . . . . . . . . . . . 291
Treppengrafik . . . . . . . . . . . . . . . . . . . . 509 formatieren. . . . . . . . . . . . . . . . . .443
Trigonometrie . . . . . . . . . . . . . . . . . . . . . 50 sortieren . . . . . . . . . . . . . . . . . . . . 127
Typumwandlung trimmen . . . . . . . . . . . . . . . . . . . . 297
explizite Typumwandlung . . . 135 verknüpfen . . . . . . . . . . . . . . . . . . 127
implizite Typumwandlung . . . 134 zerlegen . . . . . . . . . . . . . . . . . . . . . 278
Zeichenmenge . . . . . . . . . . . . . . . . . . . . 286
U Zeilenmittelwerte . . . . . . . . . . . . . . . . 182
U-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . 605 Zeilenprozent . . . . . . . . . . . . . . . . . . . . 339
Und-Verknüpfung . . . . . . . . . . . . . . . . . 23 Zeilensumme . . . . . . . . . . . . . . . . . . . . . 182
Unendlichkeit . . . . . . . . . . . . . . . . . . . . 164 Zeilenumbruch . . . . . . . . . . . . . . . . . . . 443
Unvollständiger Befehl . . . . . . . . . . . . . 5 Zeilenvektor . . . . . . . . . . . . . . . . . . . . . 176
Zellulärer Automat . . . . . . . . . . . . . . 393
V Zentrierung . . . . . . . . . . . . . . . . . . . . . . 231
Variable. . . . . . . . . . . . . . . . . . . . . . . . . . . .7 Zufallszahl . . . . . . . . . . . . . . . . . . 113, 117
kategorielle Variable . . . . . . . . . 312 Zuweisung . . . . . . . . . . . . . . . . . . . . . . . . . 7
nominalskalierte Variable . . . . 322 globale Zuweisung . . . . . . . . . . . 422
Varianz . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Linkszuweisung . . . . . . . . . . . . . . . . 9
Varianzanalyse . . . . . . . . . . . . . . . . . . . 621 Rechtszuweisung . . . . . . . . . . . . . . . 9
Vektorgrafik . . . . . . . . . . . . . . . . . . . . . 565 Zuweisungsoperator . . . . . . . . . . . . . . . . 7
Vektorwertige Funktion . . . . . . . . . . . 47 Zweiseitiger Hypothesentest . . . . . . 596
Vereinigung . . . . . . . . . . . . . . . . . . . . . 73 f.
Verknüpfungsoperator . . . . . . . . 23, 405
Verteilungsanpassungstest. . . . . . . .595 Äußeres Vektorprodukt . . . . . . . . . . 234
Verteilungsfunktion . . . . . . . . . . . . 113 f. Überlappende Textsuche . . . . . . . . . 302
662 Funktionsverzeichnis
Funktionsverzeichnis
Symbols #. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .7
... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 ˆ. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9
.Machine . . . . . . . . . . . . . . . . . . . . . . . . 153 list(NULL). . . . . . . . . . . . . . . . . . . . . .244
.Machine$double.eps . . . . . . . . . . . 153 <- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
:: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418 B
< . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21, 363 break. . . . . . . . . . . . . . . . . . . . . . . . . . . .390
<= . . . . . . . . . . . . . . . . . . . . . . . . . . . 21, 363
F
== . . . . . . . . . . . . . . . . . . . . . . . . . . . 21, 363
FALSE . . . . . . . . . . . . . . . . . . . . . . . . 19, 157
> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21, 363 function . . . . . . . . . . . . . . . . . . . . . . . . 400
>= . . . . . . . . . . . . . . . . . . . . . . . . . . . 21, 363
? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 I
[ ] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 Inf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
[:space:] . . . . . . . . . . . . . . . . . . . . . . . 298
$ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 L
%*% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 LETTERS . . . . . . . . . . . . . . . . . . . . . . . . . 130
%/% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 letters . . . . . . . . . . . . . . . . . . . . . . . . . 129
%% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
%in% . . . . . . . . . . . . . . . . . . . . . . . . . 72, 220 N
NA . . . . . . . . . . . . . . . . . . . . . . . . . . . 43, 159
%o% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
NaN . . . . . . . . . . . . . . . . . . . . . . . 7, 136, 165
& . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
next . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
&& . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
NULL . . . . . . . . . . . . . . 143, 158, 211, 244
| . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
|| . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405 f. P
*. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 pi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
+. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9
-. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 T
/. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 TRUE . . . . . . . . . . . . . . . . . . . . . . . . . 19, 157
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6
668 Abbildungsverzeichnis
Abbildungsverzeichnis
1.1 R-Homepage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2.1 R-Console und erste Befehle . . . . . . . . . . . . . . . . . . . . . . . 5
2.2 Skript zur Lösung einer quadratischen Gleichung . . . . . . . . . . . 7
2.3 Dialoge beim Beenden von R . . . . . . . . . . . . . . . . . . . . . . 11
3.1 Spielkarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2 Gemischter Kartenstapel . . . . . . . . . . . . . . . . . . . . . . . . . 14
10.1 Die Parameter sep und collapse der Funktion paste() . . . . . . . 129
Tabellenverzeichnis
2.1 Arithmetische Rechenoperatoren . . . . . . . . . . . . . . . . . . . . 9
2.2 Nützliche Tastenkombinationen für die R-Console . . . . . . . . . . . 11
3.1 Logische Operatoren für Vergleiche und Negation . . . . . . . . . . . 21
3.2 Operatoren zur Verknüpfung von Wahrheitswerten . . . . . . . . . . 23
15.1 Funktionen für die Berechnung von Summen und Mittelwerten für
Zeilen und Spalten einer Matrix . . . . . . . . . . . . . . . . . . . . . 182
16.1 Wichtige Aufgaben und Funktionen beim Rechnen mit Matrizen . . 197
18.1 Operatoren als Funktionen verwenden . . . . . . . . . . . . . . . . . 219
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6
Tabellenverzeichnis 671
Infoboxenverzeichnis
1 Lotto 6 aus 45 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
2 Fußball-EM 2017 der Frauen . . . . . . . . . . . . . . . . . . . . . . . . . 78
3 Quantile, Median und Quartile . . . . . . . . . . . . . . . . . . . . . . . . 92
4 Minigolf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5 Jokerziehung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
6 Verteilungsfunktion und Quantilsfunktion . . . . . . . . . . . . . . . . . . 113
7 Begriffe im Zusammenhang mit Hypothesentests . . . . . . . . . . . . . . 119
8 stringsAsFactors vor Version 4.0.1 . . . . . . . . . . . . . . . . . . . . . 240
9 Wiener Linien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
10 Weihnachten und Adventsonntage . . . . . . . . . . . . . . . . . . . . . . 374
11 stringsAsFactors vor Version 4.0.1 . . . . . . . . . . . . . . . . . . . . . 455
12 Zeichencodierungsstandards . . . . . . . . . . . . . . . . . . . . . . . . . . 459
13 Iris Datensatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
14 Mittelwert und Logarithmieren . . . . . . . . . . . . . . . . . . . . . . . . 592
15 Shiny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 638
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6
Regelverzeichnis 673
Regelverzeichnis
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020
D. Obszelka und A. Baierl, Statistisches Programmieren mit R,
https://doi.org/10.1007/978-3-658-28842-6