Sie sind auf Seite 1von 698

Daniel Obszelka

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.

Exklusiv für Leser von Springer-Fachbüchern: Testen Sie Springer


für Professionals 30 Tage unverbindlich. Nutzen Sie dazu im
Bestellverlauf Ihren persönlichen Aktionscode C0005406 auf
www.springerprofessional.de/buchaktion/

www.ATZonline.de

Automobiltechnische Zeitschrift

03
März 2012 | 114. Jahrgang
03

FormoPtimierung in der
Fahrzeugentwicklung

Leichte und geräuschoptimierte


Festsattelbremse

geräuschwahrnehmung von 11 | 2012


Elektroautos
www.jot-oberflaeche.de

/// 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

Springer für Professionals.


Digitale Fachbibliothek. Themen-Scout. Knowledge-Manager.
Zugriff auf tausende von Fachbüchern und Fachzeitschriften
Selektion, Komprimierung und Verknüpfung relevanter Themen
durch Fachredaktionen
Tools zur persönlichen Wissensorganisation und Vernetzung
www.entschieden-intelligenter.de

Springer für Professionals


Daniel Obszelka · Andreas Baierl

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

ISBN 978-3-658-28841-9 ISBN 978-3-658-28842-6  (eBook)


https://doi.org/10.1007/978-3-658-28842-6

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.

Planer: Sybille Thelen


Springer Vieweg ist ein Imprint der eingetragenen Gesellschaft Springer Fachmedien Wiesbaden GmbH und ist
ein Teil von Springer Nature.
Die Anschrift der Gesellschaft ist: Abraham-Lincoln-Str. 46, 65189 Wiesbaden, Germany
V

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)

Danksagung von Daniel Obszelka

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

Danksagung von Andreas Baierl

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!

Didaktisches Konzept hinter diesem Buch

Nachhaltigkeit

Programmiersprachen entwickeln sich laufend weiter. Neue Sprachen und Erweite-


rungen entstehen und eröffnen uns neue Möglichkeiten. Lösungen, die heute angesagt
sind, können in wenigen Jahren schon wieder veraltet sein. In einer schnelllebigen
Zeit zahlt es sich umso mehr aus, sich die Zeit für grundlegende und fundamentale
Konzepte zu nehmen und sich mit ihnen gründlich auseinanderzusetzen, diese zu
verstehen und dabei eine stabile und nachhaltige Wissensbasis aufzubauen, um für
Weiterentwicklungen gut gerüstet zu sein.

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

Problemlösungskompetenz und Problemlösungskreativität

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.

Wie sind das Buch und die Kapitel gegliedert?

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“

• aufwändigere Beispiele, die einerseits deinen Horizont erweitern und ande-


rerseits deine Aufmerksamkeit und Problemlösungskompetenz schulen.
• Programmierregeln, deren Einhaltung deinen Programmcode lesbarer oder
besser machen.

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

Auf der Homepage zum Buch http://www.andreasbaierl.at/RBuch.html findest


du

• die Datensätze zum Buch,


• R-Codes zu den einzelnen Kapiteln und
• Musterlösungen zu den Übungsaufgaben am Ende der Kapitel.

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!

• Karteikarten: Schreibe die Befehle und Codestücke auf Karteikarten. Du


kannst zum Beispiel auf der Vorderseite einen Befehl aufschreiben und auf
der Rückseite Anwendungsbeispiele. Oder du schreibst auf die Vorderseite be-
stimmte Aufgabenstellungen und auf der Rückseite die entsprechenden Lösun-
gen. Karteikarten kannst du immer und überall durchgehen und zum Beispiel
im Bus, in der Straßenbahn oder U-Bahn wiederholen.
XI

• Cheatsheets erstellen: Viele Studierende haben eine Art Zusammenfas-


sungscode geschrieben, in dem sie die Befehle und Codestücke anhand ganz
simpler Beispiele anwenden. Dabei haben sie wichtige Schlüsselwörter farb-
lich hervorgehoben und manche haben ihre Cheatsheets sogar mit Skizzen und
Bildern garniert. Du kannst auch interessante Lösungskonzepte oder typische
Abläufe niederschreiben oder/und aufzeichnen.

• Übungsaufgaben lösen: Wir laden dich ein, die Übungsaufgaben am Ende


eines jeden Kapitels zu lösen. Einige von ihnen sind etwas herausfordernd und
werden mehr Zeit erfordern. Aber wenn du sie einmal gelöst hast, dann hast
du einen großen Schritt gemacht. Übung macht den Meister!
• Ausprobieren: Probiere Dinge einfach aus! Experimentiere mit R und schau
dir an, was passiert.

Starten wir los!


XII Inhaltsverzeichnis

Inhaltsverzeichnis

A Erste Schritte mit R 1


1 R startklar machen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1 R-Homepage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 R herunterladen und installieren . . . . . . . . . . . . . . . . . . . . 3
1.3 Onlinematerialien zum Buch . . . . . . . . . . . . . . . . . . . . . . . 3
2 Los geht’s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1 RGui: die Oberfläche von R . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.1 RGui und Prompts – "> ", "+ " . . . . . . . . . . . . . . . . 5
2.1.2 Das Skriptfenster: Skripte öffnen, speichern und laden . . . . 6
2.2 Aus der guten Programmierpraxis . . . . . . . . . . . . . . . . . . . 6
2.2.1 Fallbeispiel: Lösung einer quadratischen Gleichung . . . . . . 6
2.2.2 Programmierstil: Gliederung, Lesbarkeit und Kommentierung 8
2.3 Kommentare und Zuweisungen – "#", "<-" . . . . . . . . . . . . . . 9
2.4 R als Taschenrechner – "+", "-", "*", /", "ˆ", "( )", sqrt() . . . . 9
2.5 Nützliche Tastenkombinationen . . . . . . . . . . . . . . . . . . . . . 11
2.6 R beenden: Workspace und Skripte sichern . . . . . . . . . . . . . . 11
2.7 Pakete installieren und laden – install.packages(), library() . . 12
2.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.8.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 12
2.8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3 Vektoren und logische Abfragen . . . . . . . . . . . . . . . . . . . . . . . . 14
3.1 Vektoren generieren . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.1.1 Elemente zu Vektoren verketten – c() . . . . . . . . . . . . . 15
3.1.2 Indizierung und Länge eines Vektors – length() . . . . . . . 15
3.1.3 Einfache Sequenzen – ":" . . . . . . . . . . . . . . . . . . . . 15
3.2 Elemente selektieren: Subsetting – "[ ]" . . . . . . . . . . . . . . . . 16
3.2.1 Subsetting mit Indizes und Indizes ausschließen – "-" . . . . 16
3.2.2 Subsetting mit Wahrheitswerten – TRUE und FALSE, "!" . . . 19
3.3 Recycling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4 Logische Bedingungen und Operatoren . . . . . . . . . . . . . . . . . 21
3.4.1 Elemente vergleichen – "==", "!", "!=", "<", "<=", ">", ">=" 21
3.4.2 Bedingungen verknüpfen – "&", "|" . . . . . . . . . . . . . . 23
Inhaltsverzeichnis XIII

3.5 Rechnen mit Wahrheitswerten . . . . . . . . . . . . . . . . . . . . . . 24


3.5.1 Absolute und relative Häufigkeiten – sum(), mean() . . . . . 24
3.5.2 Indexwerte bestimmen – which() . . . . . . . . . . . . . . . 25
3.6 Ersetzen und Tauschen von Werten . . . . . . . . . . . . . . . . . . . 27
3.7 Aus der guten Programmierpraxis . . . . . . . . . . . . . . . . . . . 30
3.7.1 Programmierstil: Sprechende Objektnamen und Sonderzeichen 30
3.7.2 Programmierstil: Allgemein und automatisiert programmieren 31
3.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.8.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 34
3.8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
B Vektorfunktionen für Data Science und Statistik 37
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen . . . . . . . . . . . . . 38
4.1 R-Hilfe – "?", help() . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2 Sequenzen generieren – seq() . . . . . . . . . . . . . . . . . . . . . . 40
4.3 Regelwerk für Funktionsaufrufe . . . . . . . . . . . . . . . . . . . . . 41
4.3.1 Objektübergabe und Parameterzuordnung . . . . . . . . . . . 41
4.3.2 Dynamische und statische Defaultwerte . . . . . . . . . . . . 43
4.3.3 Dreipunkteargument und fehlende Werte – ..., NA, na.rm . . 43
4.4 Objekte und Elemente replizieren – rep() . . . . . . . . . . . . . . . 45
4.5 Ganzzahlige Division, Rest und Teilbarkeit – "%/%", "%%" . . . . . . 46
4.6 Komponentenweises Rechnen . . . . . . . . . . . . . . . . . . . . . . 46
4.7 Nützliche Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.7.1 Exponentialfunktion und Logarithmus – exp(), log() . . . . 47
4.7.2 Runden – round(), floor(), ceiling(), trunc(), digits . 48
4.7.3 Absolutbetrag und Vorzeichenfunktion – abs(), sign() . . . 49
4.7.4 Trigonometrie – sin(), cos(), tan(), pi . . . . . . . . . . . 50
4.7.5 Aggregierende Logikfunktionen – all(), any(), xor() . . . . 50
4.8 Aus der guten Programmierpraxis . . . . . . . . . . . . . . . . . . . 52
4.8.1 Programmierstil: Funktionssammlung erweitern . . . . . . . . 52
4.8.2 Fallbeispiel: Lottotippscheine . . . . . . . . . . . . . . . . . . 53
4.9 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.9.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 58
4.9.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.9.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5 Arbeitsverzeichnis, Objekte und Dateiordner . . . . . . . . . . . . . . . . 62
5.1 Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.1.1 Objekte auflisten – ls() . . . . . . . . . . . . . . . . . . . . . 62
5.1.2 Objekte löschen – rm(), rm(list = ls()) . . . . . . . . . . 63
5.2 Pfade und Arbeitsverzeichnis – getwd(), setwd(), ".." . . . . . . . 64
5.3 Objekte speichern und laden – save(), load() . . . . . . . . . . . . 65
5.4 Namen von Ordnern und Dateien abrufen – list.files() . . . . . 66
5.5 Funktionen zur Manipulation von Dateien und Ordnern . . . . . . . 67
XIV 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

8.2 Parallele Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . 103


8.2.1 Parallele Minima und Maxima – pmin(), pmax() . . . . . . . 103
8.2.2 Vektorwertige binäre Fallunterscheidungen – ifelse() . . . . 104
8.3 Differenzieren – diff() . . . . . . . . . . . . . . . . . . . . . . . . . 105
8.4 Kovarianz und Korrelation – cov(), cor() . . . . . . . . . . . . . . . 106
8.5 Aus der guten Programmierpraxis . . . . . . . . . . . . . . . . . . . 107
8.5.1 Fallbeispiel: Gewinnrangermittlung beim Joker . . . . . . . . 107
8.5.2 Fallbeispiel: Das Geburtstagsproblem . . . . . . . . . . . . . 108
8.5.3 Programmierstil: Wiederholte Berechnungen vermeiden . . . 109
8.6 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
8.6.1 Objekte sichern . . . . . . . . . . . . . . . . . . . . . . . . . . 109
8.6.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 109
8.6.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
8.6.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
9 Verteilungen und Zufallszahlen . . . . . . . . . . . . . . . . . . . . . . . . 113
9.1 Namenskonventionen – d, p, q, r . . . . . . . . . . . . . . . . . . . . 113
9.2 Normalverteilung – dnorm(), pnorm(), qnorm(), rnorm() . . . . . . 114
9.3 Verteilungen – ein Überblick . . . . . . . . . . . . . . . . . . . . . . . 116
9.4 Gleichverteilte Zufallszahlen – runif() . . . . . . . . . . . . . . . . . 116
9.5 Zufall reproduzieren: Seeding – set.seed(), RNGversion() . . . . . 117
9.6 Stichproben ziehen – sample() . . . . . . . . . . . . . . . . . . . . . 118
9.7 Aus der guten Statistikpraxis . . . . . . . . . . . . . . . . . . . . . . 119
9.7.1 Fallbeispiel: t-Test für das mittlere Gewicht von Äpfeln . . . 119
9.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
9.8.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 122
9.8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
9.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
C Wichtige Hilfsmittel 125
10 Texte, Zeichenketten und Strings . . . . . . . . . . . . . . . . . . . . . . . 126
10.1 Zeichenketten erstellen – . . . . . . . . . . . . . . . . . . . . . . . . 126
10.2 Zeichenketten sortieren – sort(), order(), rank() . . . . . . . . . 127
10.3 Zeichenketten verknüpfen – paste(), paste0() . . . . . . . . . . . . 127
10.4 Buchstaben und Case sensitivity – letters und LETTERS . . . . . . 129
10.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
10.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 130
10.5.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
10.5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
11 Einfache Datentypen, Modes und Typumwandlung . . . . . . . . . . . . . 132
11.1 Einfache Datentypen und ihre Hierarchie . . . . . . . . . . . . . . . . 132
11.2 Mode abfragen – mode(), is.datentyp () . . . . . . . . . . . . . . . 133
11.3 Implizite Typumwandlung . . . . . . . . . . . . . . . . . . . . . . . . 134
11.4 Explizite Typumwandlung – as.datentyp () . . . . . . . . . . . . . 135
11.5 Komplexe Zahlen – complex, Re(), Im(), NaN . . . . . . . . . . . . . 136
XVI Inhaltsverzeichnis

11.6 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137


11.6.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 137
11.6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
11.6.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
12 Beschriftungen, Names und Häufigkeitstabellen . . . . . . . . . . . . . . . 139
12.1 Beschriftungen bei Vektoren . . . . . . . . . . . . . . . . . . . . . . . 140
12.1.1 Zugriff auf Beschriftungen – names() . . . . . . . . . . . . . . 140
12.1.2 Subsetting mit Names . . . . . . . . . . . . . . . . . . . . . . 140
12.1.3 Übergabe von Beschriftungen in c() . . . . . . . . . . . . . . 141
12.1.4 Beschriftungen hinzufügen, ersetzen und löschen – names()<-,
NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
12.2 Häufigkeitstabellen und Tücken von Names . . . . . . . . . . . . . . 143
12.2.1 Häufigkeitstabellen erstellen – table() . . . . . . . . . . . . 143
12.2.2 Gewichtete Mittelwerte berechnen aus Häufigkeitstabellen . . 144
12.2.3 Häufigkeiten selektieren aus Häufigkeitstabellen . . . . . . . . 145
12.2.4 Lückenlose Häufigkeitstabellen erstellen . . . . . . . . . . . . 146
12.3 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
12.3.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 147
12.3.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
12.3.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
13 Computerarithmetik und Rundungsfehler . . . . . . . . . . . . . . . . . . 150
13.1 Dezimalzahlen und Rundungsfehler – options(digits) . . . . . . . 150
13.2 Prüfung auf annähernde Gleichheit . . . . . . . . . . . . . . . . . . . 151
13.2.1 Absolute Abweichung messen . . . . . . . . . . . . . . . . . . 151
13.2.2 Funktionsbasierte Prüfung – all.equal() . . . . . . . . . . . 152
13.3 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
13.3.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 153
13.3.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
13.3.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
14 Konstante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
14.1 Eingebaute Konstante – Constants . . . . . . . . . . . . . . . . . . . 156
14.1.1 Konstante zurückgewinnen – rm(Konstante ) . . . . . . . . . 156
14.1.2 TRUE und FALSE . . . . . . . . . . . . . . . . . . . . . . . . . 157
14.2 Das NULL-Objekt – NULL . . . . . . . . . . . . . . . . . . . . . . . . . 158
14.2.1 Beschriftungen löschen – names() <- NULL . . . . . . . . . . 158
14.2.2 Abfrage auf NULL – is.null() . . . . . . . . . . . . . . . . . 159
14.3 Fehlende Werte – NA . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
14.3.1 Auf Gleichheit mit NA abfragen – is.na() . . . . . . . . . . . 160
14.3.2 Berechnung von Maßzahlen, unbedingter Fall . . . . . . . . . 160
14.3.3 Umgang mit fehlenden Werten in Funktionen . . . . . . . . . 161
14.3.4 Berechnung von Maßzahlen, bedingter Fall – subset() . . . 162
14.3.5 Logische Werte und NA . . . . . . . . . . . . . . . . . . . . . . 163
14.4 Unendlichkeit – Inf, is.finite(), is.infinite() . . . . . . . . . . 164
14.5 Not A Number – NaN, is.nan() . . . . . . . . . . . . . . . . . . . . 165
Inhaltsverzeichnis XVII

14.6 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166


14.6.1 Zusammenfassung der Konstanten . . . . . . . . . . . . . . . 166
14.6.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 166
14.6.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
14.6.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
D Datenstrukturen 169
15 Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
15.1 Matrizen generieren und manipulieren . . . . . . . . . . . . . . . . . 171
15.1.1 Vektoren zu Matrizen anordnen – matrix() . . . . . . . . . . 171
15.1.2 Vektoren aneinanderhängen – rbind(), cbind() . . . . . . . 172
15.1.3 Dimension von Matrizen – nrow(), ncol(), dim(), length() 174
15.1.4 Beschriftungen bei Matrizen – colnames(), rownames() . . . 174
15.1.5 Matrizen transponieren – t() . . . . . . . . . . . . . . . . . . 175
15.1.6 Matrix oder Vektor? – is.matrix(), is.vector() . . . . . . 176
15.2 Selektion: Subsetting – "[ ]", "[ , , drop]" . . . . . . . . . . . . 176
15.2.1 Selektion von Zeilen und Spalten mit Indizes und Names . . 177
15.2.2 Selektion von Zeilen und Spalten mit logischen Abfragen . . . 178
15.2.3 Elemente selektieren – "[ ]" . . . . . . . . . . . . . . . . . . 180
15.2.4 Subsetting mit Matrizen . . . . . . . . . . . . . . . . . . . . . 180
15.3 Summen/Mittelwerte für Zeilen/Spalten – rowSums(), colSums(),
rowMeans(), colMeans() . . . . . . . . . . . . . . . . . . . . . . . . 181
15.4 Rechnen mit Wahrheitswerten . . . . . . . . . . . . . . . . . . . . . . 184
15.5 Matrix vs. Vektor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
15.5.1 Matrix in Vektor umwandeln – as.vector() . . . . . . . . . 185
15.5.2 Bedeutung von drop = FALSE beim Subsetting . . . . . . . . 185
15.6 Ersetzen von Werten . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
15.6.1 Ersetzungen in der ganzen Matrix . . . . . . . . . . . . . . . 186
15.6.2 Ersetzungen von fehlenden Werten – NA, is.na() . . . . . . . 188
15.6.3 Ersetzungen in Teilbereichen – row(), col() . . . . . . . . . 188
15.7 Aus der guten Programmierpraxis . . . . . . . . . . . . . . . . . . . 189
15.7.1 Programmierstil: Lesbarkeit und Spacing . . . . . . . . . . . 189
15.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
15.8.1 Objekte sichern . . . . . . . . . . . . . . . . . . . . . . . . . . 190
15.8.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 191
15.8.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
15.8.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
16 Rechnen mit Matrizen und Lineare Algebra . . . . . . . . . . . . . . . . . 195
16.1 Diagonalmatrizen und Diagonalelemente – diag() . . . . . . . . . . 195
16.2 Rechnen mit Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . 197
16.2.1 Elementweises Rechnen – "+", "-", "*", /", "ˆ" . . . . . . . 197
16.2.2 Matrixmultiplikation – "%*%", crossprod(), tcrossprod() . 198
16.2.3 Invertierung und Determinante – solve(), det() . . . . . . . 198
XVIII Inhaltsverzeichnis

16.3 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 198


16.3.1 Fallbeispiel: Lineare Regression . . . . . . . . . . . . . . . . . 199
16.3.2 Fallbeispiel: Lineare Gleichungssysteme lösen – solve() . . . 201
16.4 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
16.4.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 202
16.4.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
16.4.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
17 Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
17.1 Eigenwerte und Eigenvektoren – eigen() . . . . . . . . . . . . . . . 205
17.2 Überblick über Listen – is.list(), mode(), str(), length() . . . . 205
17.3 Subsetting und Names – "$", "[ ]", "[[ ]]", names() . . . . . . . 206
17.4 Listen erstellen und initialisieren – list(), vector("list") . . . . 208
17.5 Atomare und rekursive Objekte – is.atomic(), is.recursive(),
is.vector() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
17.6 Elemente hinzufügen, löschen und ersetzen – c(), NULL . . . . . . . . 211
17.7 Listen zu Vektoren vereinfachen – unlist() . . . . . . . . . . . . . . 212
17.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
17.8.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 213
17.8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
17.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
18 Wiederholte Funktionsanwendung bei Listen . . . . . . . . . . . . . . . . 215
18.1 Wiederholte Funktionsanwendung auf Elemente einer Datenstruktur
– lapply(), sapply() . . . . . . . . . . . . . . . . . . . . . . . . . . 216
18.2 Parameterübergabe innerhalb von sapply() und lapply() . . . . . 217
18.3 Operatoren als Funktion verwenden . . . . . . . . . . . . . . . . . . . 219
18.4 Vereinfachung der Datenstruktur bei sapply() . . . . . . . . . . . . 221
18.5 sapply() und lapply() bei Vektoren und Matrizen . . . . . . . . . 222
18.6 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
18.6.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 223
18.6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
18.6.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
19 Wiederholte Funktionsanwendung bei Matrizen . . . . . . . . . . . . . . . 227
19.1 Wiederholte Funktionsanwendung auf Zeilen und Spalten einer Ma-
trix – apply() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
19.1.1 Erstes Anwendungsbeispiel für apply() . . . . . . . . . . . . 228
19.1.2 Vereinfachung der Datenstruktur bei apply() . . . . . . . . . 229
19.1.3 Anordnung der Ergebnisse bei apply() . . . . . . . . . . . . 230
19.1.4 Parameterübergabe innerhalb von apply() . . . . . . . . . . 230
19.2 Über Zeilen und Spalten fegen – sweep() . . . . . . . . . . . . . . . 231
19.3 Zentrierung und Standardisierung – scale() . . . . . . . . . . . . . 232
19.4 Äußere Vektorprodukte – outer(), "%o%" . . . . . . . . . . . . . . . 233
19.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
19.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 234
19.5.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
19.5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Inhaltsverzeichnis XIX

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

22.7 Zusammenfassung der Stringfunktionen . . . . . . . . . . . . . . . . 279


22.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 280
22.8.1 Objekte sichern . . . . . . . . . . . . . . . . . . . . . . . . . . 280
22.8.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 280
22.8.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
22.8.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
23 Komplexe Textsuchmuster: Regular Expressions . . . . . . . . . . . . . . . 283
23.1 Sonderzeichen und Escape-Befehle – ?regex, [ ], \\, \n, \" . . . . . 284
23.2 Zeichenmengen und flexible Suchmuster definieren . . . . . . . . . . 286
23.2.1 Beliebiges Zeichen – "." . . . . . . . . . . . . . . . . . . . . . 286
23.2.2 Zeichen ein- und ausschließen – [ ], [ˆ] . . . . . . . . . . . . 287
23.2.3 Zeichenbereiche definieren – [A-Z], [a-z], [0-9] . . . . . . . 288
23.3 Matchlänge steuern . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
23.3.1 Matchlängenoperatoren und Kürzel – { }, +, *, ? . . . . . . . 289
23.3.2 Greedy Matching unterbinden – ? . . . . . . . . . . . . . . . 291
23.4 Teilstrings extrahieren – regmatches() . . . . . . . . . . . . . . . . 291
23.5 Nützliche Tools zum Ersten . . . . . . . . . . . . . . . . . . . . . . . 294
23.5.1 Zeichengruppen definieren – ( ) . . . . . . . . . . . . . . . . 294
23.5.2 Zeichen und Zeichengruppen verodern – | . . . . . . . . . . . 295
23.5.3 Stringanfang und Stringende – "ˆ", "$" . . . . . . . . . . . . 296
23.5.4 Strings trimmen und White Space – [:space:] . . . . . . . . 297
23.6 Nützliche Tools zum Zweiten . . . . . . . . . . . . . . . . . . . . . . 298
23.6.1 Vorherige Zeichengruppe erneut matchen – \\k . . . . . . . . 298
23.6.2 Bedingte Extraktion: Lookaheads und Lookbehinds – perl,
(?= ), (?! ), (?<= ), (?<! ) . . . . . . . . . . . . . . . . . 300
23.6.3 Überlappende Suche – perl, (?= ) . . . . . . . . . . . . . . . 302
23.7 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
23.7.1 Fallbeispiel: Stationsansagen der Wiener Linien . . . . . . . . 303
23.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
23.8.1 Zusammenfassung der Sonderzeichen und der Syntax . . . . . 305
23.8.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 306
23.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 307
24 Kategorielle Variablen: Faktoren . . . . . . . . . . . . . . . . . . . . . . . 310
24.0 Die Daten des Kapitels einlesen – read.table() . . . . . . . . . . . 311
24.1 Faktoren: Sinn, Generierung und Verwaltung . . . . . . . . . . . . . 312
24.1.1 Codierung von Ausprägungen . . . . . . . . . . . . . . . . . . 312
24.1.2 Faktoren generieren – factor() . . . . . . . . . . . . . . . . 313
24.1.3 Interne Verwaltung – unclass(), mode(), is.factor() . . . 314
24.1.4 Zugriff auf die Ausprägungen – levels(), nlevels() . . . . 315
24.1.5 Faktoren und Strings – as.character() . . . . . . . . . . . . 315
24.1.6 Codierungsreihenfolge bestimmen mit levels . . . . . . . . . 316
24.2 Kategorisierung von numerischen Variablen . . . . . . . . . . . . . . 317
24.2.1 Bedingungen aufsummieren . . . . . . . . . . . . . . . . . . . 317
24.2.2 Kategorisierung mit cut() . . . . . . . . . . . . . . . . . . . 318
Inhaltsverzeichnis XXI

24.3 Ausprägungen manipulieren . . . . . . . . . . . . . . . . . . . . . . . 319


24.3.1 Verschmelzen von Faktorausprägungen . . . . . . . . . . . . . 320
24.3.2 Umbenennung von Ausprägungen . . . . . . . . . . . . . . . . 321
24.3.3 Neue Ausprägungen hinzufügen . . . . . . . . . . . . . . . . . 321
24.4 Ordinalskalierung – factor(ordered), as.ordered(), is.ordered() 322
24.5 Tücken im Zusammenhang mit Faktoren . . . . . . . . . . . . . . . . 324
24.5.1 Umordnen von Faktorausprägungen . . . . . . . . . . . . . . 324
24.5.2 Das as.numeric-Problem . . . . . . . . . . . . . . . . . . . . . 325
24.5.3 Lückenlose Häufigkeitstabellen revisited – table() . . . . . . 326
24.6 Fehlende Werte als Kategorie – factor(exclude), is.na() . . . . . 327
24.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
24.7.1 Objekte sichern . . . . . . . . . . . . . . . . . . . . . . . . . . 328
24.7.2 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 329
24.7.3 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
24.7.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
25 Aggregation und Kreuztabellen . . . . . . . . . . . . . . . . . . . . . . . . 332
25.1 Funktionen auf Gruppen anwenden: Teil 1 – tapply() . . . . . . . . 333
25.2 Kreuztabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336
25.2.1 Einfache Kreuztabellen – table() . . . . . . . . . . . . . . . 336
25.2.2 Mehrdimensionale Kreuztabellen – table(), apply(), ftable()336
25.2.3 Randsummen bestimmen und anhängen – margin.table(),
addmargins() . . . . . . . . . . . . . . . . . . . . . . . . . . 338
25.2.4 Allgemeine Kreuztabellen – tapply() . . . . . . . . . . . . . 339
25.2.5 Zeilen-, Spalten- und Totalprozent – prop.table() . . . . . 339
25.3 Funktionen auf Gruppen anwenden: Teil 2 – aggregate() . . . . . . 341
25.3.1 Eine Gruppenvariable . . . . . . . . . . . . . . . . . . . . . . 342
25.3.2 Mehrere Gruppenvariablen . . . . . . . . . . . . . . . . . . . 343
25.4 Verfügbarkeit von Variablen . . . . . . . . . . . . . . . . . . . . . . . 344
25.4.1 Lokale Verfügbarkeit – with() . . . . . . . . . . . . . . . . . 344
25.4.2 Globale Verfügbarkeit – attach(), detach() . . . . . . . . . 344
25.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
25.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 345
25.5.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
25.5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
26 Klassen, generische Funktionen und Attribute . . . . . . . . . . . . . . . . 348
26.1 Klassen, generische Funktionen – class(), UseMethod(), methods() 348
26.2 Attribute erfragen und Klassenattribut entfernen – attributes(),
unclass() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
26.3 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
26.3.1 Fallbeispiel: Run Length Encoding . . . . . . . . . . . . . . . 352
26.4 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
26.4.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 353
26.4.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
26.4.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
XXII Inhaltsverzeichnis

27 Datums- und Uhrzeitobjekte . . . . . . . . . . . . . . . . . . . . . . . . . 355


27.1 Erste Einblicke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
27.1.1 Datum und Uhrzeit abfragen – Sys.Date(), Sys.time() . . 356
27.1.2 Klassenübersicht – Date, POSIXct, POSIXlt . . . . . . . . . . 356
27.1.3 Konvertierung – as.Date(), as.POSIXct(), as.POSIXlt() . 357
27.2 Datum und Uhrzeit formatieren . . . . . . . . . . . . . . . . . . . . . 357
27.2.1 Formatierungsfunktionen und Platzhalter – format.Date(),
format.POSIXct() . . . . . . . . . . . . . . . . . . . . . . . . 357
27.2.2 Datumsfunktionen – weekdays(), months(), quarters() . . 359
27.3 Datums- und Uhrzeitobjekte erzeugen – as.Date(), as.POSIXct() . 360
27.3.1 Erzeugung aus Zeichenketten . . . . . . . . . . . . . . . . . . 361
27.3.2 Erzeugung aus Referenzdatum bzw. Referenzuhrzeit . . . . . 362
27.4 Datums- und Uhrzeitvektoren sortieren – sort(), order() . . . . . 363
27.5 Rechnen mit Datumswerten . . . . . . . . . . . . . . . . . . . . . . . 364
27.5.1 Tage und Sekunden addieren bzw. subtrahieren . . . . . . . . 364
27.5.2 Paarweise Zeitdifferenzen bestimmen – diff() . . . . . . . . 364
27.5.3 Parallele Zeitdifferenzen bestimmen – "-", difftime() . . . 365
27.6 Spezielle Datumswerte erzeugen und Locales . . . . . . . . . . . . . . 371
27.7 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
27.7.1 Anwendungshierarchie der drei Klassen . . . . . . . . . . . . 373
27.7.2 Fallbeispiel: Berechnung von Adventsonntagen . . . . . . . . 374
27.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 376
27.8.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 376
27.8.2 Ausblick und Rückblick . . . . . . . . . . . . . . . . . . . . . 377
27.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
F Eigene Funktionen und Ablaufsteuerung 379
28 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
28.1 Anweisungsblöcke – { } . . . . . . . . . . . . . . . . . . . . . . . . . 381
28.2 Bedingte Anweisung und Verzweigung . . . . . . . . . . . . . . . . . 381
28.2.1 Bedingte Anweisung – if . . . . . . . . . . . . . . . . . . . . 381
28.2.2 Wenn/Dann-Verzweigung – if, else . . . . . . . . . . . . . . 382
28.2.3 if/else vs. ifelse() . . . . . . . . . . . . . . . . . . . . . . 384
28.3 Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384
28.3.1 for-Schleife – for . . . . . . . . . . . . . . . . . . . . . . . . . 384
28.3.2 Zwei Anwendungsvarianten der for-Schleife . . . . . . . . . . 386
28.3.3 Sequenzielle vs. parallele Berechnung, Schleifenvermeidung . 388
28.3.4 while-Schleife – while . . . . . . . . . . . . . . . . . . . . . . 389
28.3.5 Schleifensteuerung – break, next . . . . . . . . . . . . . . . . 390
28.3.6 Endlosschleifen – while (TRUE), repeat . . . . . . . . . . . 391
28.4 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 392
28.4.1 Programmierstil: Einrückungen . . . . . . . . . . . . . . . . . 392
28.4.2 Fallbeispiel: Zelluläre Automaten . . . . . . . . . . . . . . . . 393
Inhaltsverzeichnis XXIII

28.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396


28.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 396
28.5.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
28.5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
29 Eigene Funktionen: Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . 399
29.1 Eigene Funktionen schreiben – function . . . . . . . . . . . . . . . . 400
29.1.1 Funktionsname . . . . . . . . . . . . . . . . . . . . . . . . . . 400
29.1.2 Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 400
29.1.3 Rückgabe – return(), invisible() . . . . . . . . . . . . . . 400
29.2 Fehlermeldungen und Warnungen – stop(), warning() . . . . . . . 403
29.3 Bedingungen verknüpfen zum Zweiten – "&&", "||" . . . . . . . . . 404
29.4 Parameter und Argumente . . . . . . . . . . . . . . . . . . . . . . . . 405
29.4.1 Statische Defaultwerte und Wrapper-Funktionen . . . . . . . 405
29.4.2 Dynamische Defaultwerte . . . . . . . . . . . . . . . . . . . . 407
29.4.3 Unspezifizierte Parameter . . . . . . . . . . . . . . . . . . . . 408
29.4.4 Objekte switchen – switch() . . . . . . . . . . . . . . . . . . 409
29.4.5 Das Dreipunkteargument – ..., list(...) . . . . . . . . . . 410
29.5 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
29.5.1 Programmierstil: Funktionskommentare . . . . . . . . . . . . 412
29.5.2 Fallbeispiel: Bisektion . . . . . . . . . . . . . . . . . . . . . . 414
29.6 Environments und Scoping . . . . . . . . . . . . . . . . . . . . . . . . 416
29.7 Funktionen überschreiben – "::", rm() . . . . . . . . . . . . . . . . 417
29.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418
29.8.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 418
29.8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
29.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
30 Eigene Funktionen: Ergänzung und Vertiefung . . . . . . . . . . . . . . . 421
30.1 *apply() vs. Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . 422
30.2 Anonyme Funktionen – function() . . . . . . . . . . . . . . . . . . 424
30.3 Rekursion – Recall() . . . . . . . . . . . . . . . . . . . . . . . . . . 425
30.4 Methoden für generische Funktionen schreiben . . . . . . . . . . . . 431
30.5 Eigene Operatoren schreiben . . . . . . . . . . . . . . . . . . . . . . 432
30.6 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
30.6.1 Fallbeispiel: Klasse Bruch . . . . . . . . . . . . . . . . . . . . 432
30.6.2 Fallbeispiel: Auswertung von Polynomen . . . . . . . . . . . . 436
30.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
30.7.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 439
30.7.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439
30.7.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 440
G Datenimport und Datenexport 441
31 Stringformatierung, Consoleneingabe und -ausgabe . . . . . . . . . . . . . 442
31.1 Consolenausgabe – cat(), print(), "\n", "\t" . . . . . . . . . . . . 442
31.2 Strings formatieren – format() . . . . . . . . . . . . . . . . . . . . . 443
31.3 Zahlen formatieren – formatC() . . . . . . . . . . . . . . . . . . . . 444
XXIV Inhaltsverzeichnis

31.4 Tabellen erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445


31.5 Consoleneingabe – scan() . . . . . . . . . . . . . . . . . . . . . . . . 446
31.6 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 448
31.6.1 Fallbeispiel: Mathequiz . . . . . . . . . . . . . . . . . . . . . 448
31.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
31.7.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 452
31.7.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
31.7.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 452
32 Datenimport und Datenexport . . . . . . . . . . . . . . . . . . . . . . . . 454
32.1 Einfache Dateiformate . . . . . . . . . . . . . . . . . . . . . . . . . . 455
32.1.1 Textdateien einlesen – read.table() . . . . . . . . . . . . . 455
32.1.2 Textdateien schreiben – write.table() . . . . . . . . . . . . 458
32.1.3 csv-Dateien einlesen & schreiben – read.csv(), write.csv() 458
32.2 Zeichencodierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459
32.2.1 Zeichencodierungsstandards . . . . . . . . . . . . . . . . . . . 459
32.2.2 Zeichencodierung umstellen – Encoding(), options(encoding)461
32.2.3 Reparaturen per Hand . . . . . . . . . . . . . . . . . . . . . . 462
32.3 Datenaufbereitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462
32.3.1 Herausforderungen beim Einlesen meistern . . . . . . . . . . 462
32.3.2 Strings nicht als Faktoren einlesen – stringsAsFactors . . . 469
32.4 Textdateien mit beliebigem Format einlesen – scan() . . . . . . . . 469
32.5 Textdateien mit beliebigem Format schreiben – cat() . . . . . . . . 472
32.6 Einlesen aus anderen Quellen . . . . . . . . . . . . . . . . . . . . . . 474
32.6.1 Excel-Dateien einlesen – read.xlsx() . . . . . . . . . . . . . 474
32.6.2 SPSS- und SAS-Dateien einlesen – read.spss(), read.sas() 475
32.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 476
32.7.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 476
32.7.2 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477
H Effizienz und Simulation 479
33 Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 480
33.1 Laufzeitmessung: Spaltenmittelwerte einer Matrix . . . . . . . . . . . 480
33.1.1 Messung mittels Stoppuhr – Sys.time() . . . . . . . . . . . 480
33.1.2 Messung der CPU-Zeit: system.time() . . . . . . . . . . . . 482
33.2 Beispiel: Dreiecksdichte auswerten . . . . . . . . . . . . . . . . . . . 483
33.3 Laufzeitvergleiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
33.4 Beispiel: Zeilenminima einer Matrix . . . . . . . . . . . . . . . . . . . 487
33.5 Beispiel: Würfelwurf – Augensumme erreichen . . . . . . . . . . . . . 489
33.6 Beispiel: Flächeninhalt zufälliger Dreiecke . . . . . . . . . . . . . . . 497
33.7 Monte-Carlo-Simulation . . . . . . . . . . . . . . . . . . . . . . . . . 499
33.8 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
33.8.1 Zusammenfassung der Effizienztipps . . . . . . . . . . . . . . 500
33.8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 500
33.8.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501
Inhaltsverzeichnis XXV

I Visualisierung von Daten 505


34 Einführung in die 2D-Grafik . . . . . . . . . . . . . . . . . . . . . . . . . . 506
34.1 Basisfunktion zur Grafikerstellung – plot() . . . . . . . . . . . . . . 507
34.1.1 Erste Grafiken und Einstellungen . . . . . . . . . . . . . . . . 507
34.1.2 Streudiagramme zum Ersten – plot() . . . . . . . . . . . . . 511
34.2 Grafikparameter steuern – par() . . . . . . . . . . . . . . . . . . . . 511
34.2.1 Skalierung – cex . . . . . . . . . . . . . . . . . . . . . . . . . 513
34.2.2 Farben – col . . . . . . . . . . . . . . . . . . . . . . . . . . . 514
34.2.3 Punktarten – pch . . . . . . . . . . . . . . . . . . . . . . . . . 515
34.2.4 Linienart und Linienstärke – lty und lwd . . . . . . . . . . . 516
34.2.5 Hintergrundfarbe – bg, "transparent" . . . . . . . . . . . . 516
34.2.6 Umgang mit par() . . . . . . . . . . . . . . . . . . . . . . . . 516
34.2.7 Streudiagramme zum Zweiten . . . . . . . . . . . . . . . . . . 517
34.3 Grafikelemente hinzufügen . . . . . . . . . . . . . . . . . . . . . . . . 518
34.3.1 Punkte, Linien, Texte – points(), lines(), text(), mtext() 518
34.3.2 Legenden – legend() . . . . . . . . . . . . . . . . . . . . . . 519
34.3.3 Boxen, Gitter, Achsen – box(), grid(), axis(), pretty() . 522
34.3.4 Zeichenreihenfolge steuern – type = "n" . . . . . . . . . . . 523
34.3.5 Rechtecke und Polygone – rect(), polygon() . . . . . . . . 524
34.3.6 Adjustierungen und quadratische Plots – adj, padj, par(pty
= "s") . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
34.3.7 Linien und Pfeile – abline(), segments(), arrows() . . . . 527
34.3.8 Titel und Überschriften – title() . . . . . . . . . . . . . . . 528
34.4 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 529
34.4.1 Fallbeispiel: Uhr . . . . . . . . . . . . . . . . . . . . . . . . . 529
34.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 532
34.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 532
34.5.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
34.5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
35 Standardgrafiken und Farben . . . . . . . . . . . . . . . . . . . . . . . . . 535
35.1 Standardgrafiken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 536
35.1.1 Parallele Liniengrafiken, Matrixplot – matplot() . . . . . . . 536
35.1.2 Balkendiagramme – barplot() . . . . . . . . . . . . . . . . . 540
35.1.3 Histogramme – hist() . . . . . . . . . . . . . . . . . . . . . 544
35.1.4 Rückgabeobjekte von Grafikfunktionen verwerten . . . . . . . 547
35.1.5 Boxplots – boxplot() . . . . . . . . . . . . . . . . . . . . . . 549
35.2 Die Welt der Farben . . . . . . . . . . . . . . . . . . . . . . . . . . . 551
35.2.1 RGB-Farben – rgb() . . . . . . . . . . . . . . . . . . . . . . 551
35.2.2 HCL-Farben – hcl() . . . . . . . . . . . . . . . . . . . . . . . 552
35.2.3 Standardfarbpaletten – Palettes . . . . . . . . . . . . . . . . 554
35.2.4 Eigene Farbpaletten – colorRamp(), colorRampPalette() . 555
35.3 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 557
35.3.1 Fallbeispiel: Farblich markierte Balkendiagramme . . . . . . . 557
XXVI Inhaltsverzeichnis

35.4 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 560


35.4.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 560
35.4.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 561
35.4.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 561
36 Grafikfenster und Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . 564
36.1 Grafikfenster: Devices . . . . . . . . . . . . . . . . . . . . . . . . . . 564
36.1.1 Neues Device öffnen – dev.new() . . . . . . . . . . . . . . . . 564
36.1.2 Aktives Device abfragen und setzen – dev.cur(), dev.set() 565
36.1.3 Device schließen – dev.off(), graphics.off() . . . . . . . . 565
36.2 Grafiken speichern . . . . . . . . . . . . . . . . . . . . . . . . . . . . 565
36.2.1 Rastergrafiken vs. Vektorgrafiken . . . . . . . . . . . . . . . . 565
36.2.2 Speichern von Grafiken . . . . . . . . . . . . . . . . . . . . . 566
36.3 Fenstereinteilung und Layout . . . . . . . . . . . . . . . . . . . . . . 567
36.3.1 Äußere Grafikränder – par()$mar, par()$mai . . . . . . . . 567
36.3.2 Einfache Fensterteilung – par()$mfrow . . . . . . . . . . . . 568
36.3.3 Paarweise Streudiagramme – pairs() . . . . . . . . . . . . . 570
36.3.4 Eigene Layouts definieren – layout(), layout.show(), lcm() 571
36.4 Aus der guten Praxis . . . . . . . . . . . . . . . . . . . . . . . . . . . 576
36.4.1 Fallbeispiel: Streudiagramme mit Randverteilungen – density(),
substitute() . . . . . . . . . . . . . . . . . . . . . . . . . . 576
36.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 585
36.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 585
36.5.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 585
36.5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 586
J Data Science und Statistik in der Praxis 589
37 Verteilungstests und Hypothesentests . . . . . . . . . . . . . . . . . . . . . 590
37.1 Verteilungstests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 592
37.1.1 Histogramme mit Anpassungslinie – hist() . . . . . . . . . . 592
37.1.2 Quantil-Quantil-Plot – qqplot(), qqline(), qqPlot() . . . 593
37.1.3 Kolmogorov-Smirnov-Test – ks.test(), LcKS() . . . . . . . 595
37.2 Hypothesentests für kategorielle Merkmale . . . . . . . . . . . . . . . 596
37.2.1 Binomialtest – binom.test() . . . . . . . . . . . . . . . . . . 596
37.2.2 Rückgabeobjekt von Testfunktionen – htest . . . . . . . . . 597
37.2.3 Tests für Anteilswerte – prop.test() . . . . . . . . . . . . . 597
37.2.4 McNemar-Test – mcnemar.test() . . . . . . . . . . . . . . . 598
37.2.5 χ2 -Test – chisq.test() . . . . . . . . . . . . . . . . . . . . . 599
37.2.6 Weitere Tests für kategorielle Merkmale . . . . . . . . . . . . 600
37.3 Hypothesentests für metrische Merkmale . . . . . . . . . . . . . . . . 601
37.3.1 Test für Korrelationskoeffizienten – cor.test() . . . . . . . 601
37.3.2 t-Test für eine und zwei Stichproben – t.test() . . . . . . . 602
37.4 Nichtparametrische Hypothesentests – wilcox.test() . . . . . . . . 604
37.5 Weitere Tests für metrische Merkmale . . . . . . . . . . . . . . . . . 605
37.6 Überblick über wichtige Hypothesentests in R . . . . . . . . . . . . . 606
Inhaltsverzeichnis XXVII

37.7 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 606


37.7.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 606
37.7.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 607
37.7.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 607
38 Statistische Modelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 608
38.1 Lineare Regression – lm(), formula . . . . . . . . . . . . . . . . . . 608
38.1.1 Einfache lineare Regression . . . . . . . . . . . . . . . . . . . 609
38.1.2 Mehrere metrische unabhängige Variablen, Modellbildung . . 611
38.1.3 Kategorielle Variablen – model.matrix(), contrasts() . . 613
38.1.4 Modellselektion und Modelldiagnostik – step(), anova() . 614
38.1.5 Zusatzbeispiel zur linearen Regression – predict(), step() . 617
38.2 Weitere statistische Verfahren . . . . . . . . . . . . . . . . . . . . . . 621
38.2.1 Varianzanalyse – aov(), Error() . . . . . . . . . . . . . . . . 621
38.2.2 Generalisierte lineare Modelle – glm() . . . . . . . . . . . . . 621
38.3 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 622
38.3.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 622
38.3.2 Ausblick: Funktionen zu weiteren statistischen Verfahren . . 622
38.3.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 623
39 Programmierpraxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 624
39.1 R-Code in mehrere Dateien teilen – source() . . . . . . . . . . . . . 624
39.2 R-Code und Dokumentation integrieren . . . . . . . . . . . . . . . . 625
39.2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 625
39.2.2 Einfügen des R-Codes in ein Dokument . . . . . . . . . . . . 626
39.2.3 Dokument mit R-Code ausführen – knit(), render() . . . . 628
39.3 Fehlertypen und Fehlersuche (Debugging) . . . . . . . . . . . . . . . 629
39.3.1 Fehlertypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629
39.3.2 Fehlersuche und -korrektur – browser(), recover() . . . . 630
39.4 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 635
39.4.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 635
39.4.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 636
39.4.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 636
40 Interaktive Webanwendungen mit Shiny . . . . . . . . . . . . . . . . . . . 638
40.1 Grundlegendes zur Programmierung einer Shiny-Anwendung . . . . 638
40.1.1 Struktur einer Shiny-Anwendung . . . . . . . . . . . . . . . . 639
40.1.2 Bereitstellung einer Shiny-Anwendung . . . . . . . . . . . . . 639
40.1.3 Interaktives Verhalten steuern – reactive() . . . . . . . . . 640
40.2 Serverfunktion und Render-Funktionen . . . . . . . . . . . . . . . . . 640
40.3 User Interface Objekt (UI) . . . . . . . . . . . . . . . . . . . . . . . . 642
40.3.1 Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 642
40.3.2 Input- und Outputfunktionen . . . . . . . . . . . . . . . . . . 643
40.4 Shiny-Beispiel: Darstellung einer Normalverteilungsstichprobe . . . . 646
40.4.1 Version 1: sidebarLayout . . . . . . . . . . . . . . . . . . . . 646
40.4.2 Version 2: sidebarLayout mit Unterseiten . . . . . . . . . . 647
XXVIII Inhaltsverzeichnis

40.5 Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648


40.5.1 Zusammenfassende Kontrollfragen . . . . . . . . . . . . . . . 648
40.5.2 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 649
41 Relevante R-Pakete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 650
41.1 Konzeptionelle Erweiterungen von Base-R . . . . . . . . . . . . . . . 650
41.2 Grafik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 650
41.3 Import/Export . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 651
41.4 Funktionensammlungen . . . . . . . . . . . . . . . . . . . . . . . . . 651
41.5 Statistische Modelle und Methoden . . . . . . . . . . . . . . . . . . . 652
41.6 Spezielle Anwendungsbereiche . . . . . . . . . . . . . . . . . . . . . . 652
42 R-Ressourcen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653
K Verzeichnisse 655
Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 656
Funktionsverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 662
Operatoren- und Konstantenverzeichnis . . . . . . . . . . . . . . . . . . . . . 667
Abbildungsverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 668
Tabellenverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 670
Infoboxenverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 672
Regelverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673
A Erste Schritte mit R
Daniel Obszelka
2 A Erste Schritte mit R

1 R startklar machen

Dort wollen wir in diesem Kapitel hin:

• die R-Homepage ergründen

• 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

Der Link für die R-Homepage lautet https://www.r-project.org/. In Abb. 1.1


zeigen wir einen Ausschnitt.

Abbildung 1.1: R-Homepage (abgerufen am 27.08.2020)

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

1.2 R herunterladen und installieren

Die derzeit aktuelle Version (Stand 27.08.2020) ist R 4.0.2. Um R herunterzula-


den und zu installieren, gehen wir wie folgt vor:

• 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.

• Bei Subdirectories klicken wir auf base und anschließend auf


Download R 4.0.2 for Windows (bzw. die entsprechend neueste Version)
• Anschließend werden wir gefragt, ob wir die Datei
R-4.0.2-win.exe
speichern wollen. Wir bejahen mit einem Klick auf Datei speichern.
• Wir führen die heruntergeladene Datei R-4.0.2-win.exe aus und folgen ein-
fach den Bildschirmanweisungen.

1.3 Onlinematerialien zum Buch

Auf der Homepage zum Buch http://www.andreasbaierl.at/RBuch.html findest


du die Datensätze zum Buch, R-Codes zu den einzelnen Kapiteln und Muster-
lösungen zu den Übungsaufgaben am Ende der Kapitel.
Nachdem wir R installiert haben, sind wir bereit für unser erstes Programm, das
uns in (2) bereits sehnsüchtig erwartet!
4 A Erste Schritte mit R

2 Los geht’s

Dort wollen wir in diesem Kapitel hin:

• mit der R-Console und dem Skriptfenster Bekanntschaft machen

• erste Sprachkonstrukte lernen: Kommentare, Variablen und Zuweisungen


• R als Taschenrechner verwenden
• erste Einblicke in die Programmierästhetik sammeln
• Pakete installieren und laden

Weißt du die Lösungsformel für die quadratische Gleichung


a · x2 + b · x + c = 0 (2.1)
auswendig? Falls nicht, dann haben wir eine gute Nachricht für dich: Wir schreiben
in (2.2.1) unser allererstes Programm, das die quadratische Gleichung (2.1) löst.
Dabei beantworten wir unter anderem folgende Fragen:

• Wie schaut ein solches Programm in R aus? (2.2.1)


• Wie gestalten wir dieses und andere Programme übersichtlich? (2.2.2)
• Wie speichern wir dieses Programm ab, um es später wieder laden zu können?
(2.1.2)

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.

2.1 RGui: die Oberfläche von R

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

2.1.1 RGui und Prompts – "> ", "+ "

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.

Abbildung 2.1: R-Console und erste Befehle

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

2.1.2 Das Skriptfenster: Skripte öffnen, speichern und laden

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.

Die Bequemlichkeiten und Vorteile des Skriptfensters sind jene:

• 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.

Skriptdateien haben die Dateiendung R, also etwa Beispiel.R.


Im Prinzip können wir jeden beliebigen Texteditor verwenden. Es gibt aber auch
Programme, die den Programmcode grafisch aufbereiten und damit übersichtlicher
gestalten, insbesondere IDEs (Integrated Development Environment).
Ein Beispiel ist RStudio (https://www.rstudio.com/). Um R effektiver zu ler-
nen, ist es besser, auf gewisse Annehmlichkeiten, wie zum Beispiel die automatische
Codevervollständigung, zu verzichten.

2.2 Aus der guten Programmierpraxis

2.2.1 Fallbeispiel: Lösung einer quadratischen Gleichung

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

für gegebene a, b, c ∈ R berechnet. Die Lösungsformel lautet:



−b ± b2 − 4ac
x1,2 =
2a
2 Los geht’s 7

Die Situation könnte am Ende so aussehen, wie in Abb. 2.2 gezeigt.

Abbildung 2.2: Skript zur Lösung einer quadratischen Gleichung

Jetzt der Reihe nach:

• 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.

Wir können unser Skript zum Beispiel unter Quadratgleichung.R abspeichern.


8 A Erste Schritte mit R

2.2.2 Programmierstil: Gliederung, Lesbarkeit und Kommentierung

In diesem Buch wird R-Code nach drei Gesichtspunkten gegliedert:

• Input: Befehlszeilen, die R ausführt. Input wird rot markiert.


• Output: Resultate, die R auf der Console ausgibt. Output wird blau markiert.
• Kommentar: Zeilen, die der besseren Übersicht dienen, den Code beschreiben
und von R nicht ausgeführt werden. Kommentare werden grau markiert.

Anhand des Beispiels aus (2.2.1) zeigen wir diese farbliche Gliederung.

> # Ermittle die Lösungen für die quadratische Gleichung


> # a x^2 + b x + c = 0

> # Festlegen der Koeffizienten


> a <- 2
> b <- -5
> c <- 3

> # Ermittlung der Lösungen


> x1 <- (-b + sqrt(b^2 - 4 * a * c)) / (2 * a)
> x2 <- (-b - sqrt(b^2 - 4 * a * c)) / (2 * a)

> # Anzeigen der Lösungen


> x1
[1] 1.5
> x2
[1] 1

Was macht (außer der farblichen Gestaltung) den Code so übersichtlich?

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!

Regel 1: Kommentiere deinen R-Code!

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!

2.3 Kommentare und Zuweisungen – "#", "<-"

In (2.2.1) haben wir bereits die Variablenzuweisung kennengelernt. Genauer gesagt


handelt es sich bei a <- 2 um eine Linkszuweisung: Der Wert 2 wird nach links
auf das Objekt a zugewiesen.
Es gibt in R auch die Rechtszuweisung sowie die Zuweisung mit dem "="-Zeichen.
Letztgenannte Zuweisungsmöglichkeit ist in vielen Programmiersprachen gebräuch-
lich, nicht aber in R.

> # Linkszuweisung > # Rechtszuweisung > # Zuweisung mit "="


> # Empfohlen! > # Nicht empfohlen! > # Unüblich in R!
> a <- 2 > 2 -> a > a = 2
> a > a > a
[1] 2 [1] 2 [1] 2

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.

> a <- 2 # Parameter a definieren

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.

Tabelle 2.1: Arithmetische Rechenoperatoren

Operator Bedeutung
+ Addition
- Subtraktion
* Multiplikation
/ Division
ˆ Potenz

Die Quadratwurzelfunktion sqrt() kennen wir bereits aus (2.2.1).


10 A Erste Schritte mit R

Wir schauen uns ein paar Codebeispiele an.

> # Klammersetzung beeinflusst Punkt- vor Strichrechnung


> 4 * 2 + 3
[1] 11
> 4 * (2 + 3)
[1] 20

> # Klammersetzung bei Multiplikation mit einer negativen Zahl


> (2 + 3) * (-4)
[1] -20
> (2 + 3) * -4
[1] -20

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.

> # Einfache Potenzen und Quadratwurzeln


> 3 ^ 2
[1] 9
> sqrt(9) # Quadratwurzel aus 9
[1] 3
> 9 ^ 0.5 # ebenfalls die Quadratwurzel aus 9
[1] 3

> # Beachte die Klammersetzung!


> 9 ^ (1/2) # entspricht Wurzel aus 9
[1] 3
> 9 ^ 1 / 2 # das nicht (entspricht (9 ^ 1) / 2
[1] 4.5

> # Wurzeln höherer Ordnung


> 27 ^ (1/3) # 3. Wurzel aus 27
[1] 3
> 3 ^ 3 # Kontrolle
[1] 27

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.

Beispiel: Berechne das Verhältnis des „Goldenen Schnittes“ γ:



5−1
γ=
2
> # Verhältnis des Goldenen Schnittes
> gamma <- (sqrt(5) - 1) / 2
> gamma
[1] 0.618034
2 Los geht’s 11

2.5 Nützliche Tastenkombinationen

In Tab. 2.2 zeigen wir einige nützliche Tastenkombinationen für die R-Console.

Tabelle 2.2: 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.

2.6 R beenden: Workspace und Skripte sichern

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

2.7 Pakete installieren und laden – install.packages(),


library()

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:

1. Paket installieren (einmalig): install.packages()


Wir schreiben folgenden Befehl in die R-Console, um ein Paket zu installieren,
wobei wir Paketname durch den Namen des zu installierenden Pakets ersetzen.

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

2.8.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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).

Eine Person wiegt 65 kg und ist 1.76 m groß.

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.

2. Wir betrachten die Fibonaccifolge (an )n≥1 mit


(
1 für n ∈ {1, 2}
an =
an−2 + an−1 für n ≥ 3

Eine geschlossene Formel für diese Folge lautet


√ n  √ n !
1 1+ 5 1− 5

an = √ · −
5 2 2

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

3 Vektoren und logische Abfragen

Dort wollen wir in diesem Kapitel hin:

• unsere erste Datenstruktur (Vektor) kennenlernen


• logische Abfragen erstellen und mit Wahrheitswerten rechnen
• Bekanntschaft mit dem Recycling machen
• weitere Einblicke in die Programmierästhetik sammeln

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

Abbildung 3.1: Die Spielkarten unseres Kartensets

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 verwalten wir in R den (gemischten) Kartenstapel? (3.1.1)


• Wie zählen wir die Anzahl der Karten im Stapel? (3.1.2), (3.1.3)

• 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)

Legen wir los!


© 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_3
3 Vektoren und logische Abfragen 15

3.1 Vektoren generieren

Vektoren sind die einfachste Datenstruktur in R und ideal geeignet, um unseren


gemischten Kartenstapel zu verwalten. Machen wir R mit unserem Kartenstapel
vertraut!

3.1.1 Elemente zu Vektoren verketten – c()

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

3.1.2 Indizierung und Länge eines Vektors – length()

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:

> # Zähle die Anzahl der Karten des Stapels


> # Bestimme die Anzahl der Elemente des Vektors stapel
> length(stapel)
[1] 6

3.1.3 Einfache Sequenzen – ":"

Einfache Sequenzen mit Schrittweite 1 erzeugen wir mit dem Doppelpunkt (":").

> # Einfache Sequenzen generieren


> 1:6
[1] 1 2 3 4 5 6
16 A Erste Schritte mit R

> # Funktioniert auch in die andere Richtung


> 6:1
[1] 6 5 4 3 2 1

> # Funktioniert auch mit negativen Einträgen - beachte die Klammern!


> -2:3
[1] -2 -1 0 1 2 3
> -(2:3)
[1] -2 -3

> # Geht auch mit Dezimalzahlen > # Unvollständige Sequenz


> 1.5:6.5 > 1:4.5
[1] 1.5 2.5 3.5 4.5 5.5 6.5 [1] 1 2 3 4

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.

> # Generiere einen Indexvektor für die Variable stapel


> 1:length(stapel)
[1] 1 2 3 4 5 6

3.2 Elemente selektieren: Subsetting – "[ ]"

Auf Elemente eines Vektors können wir mit "[ ]" zugreifen (Subsetting). Wir un-
terscheiden im Moment zwei Möglichkeiten für den Zugriff:1

• Subsetting mit Indizes in (3.2.1)


• Subsetting mit Wahrheitswerten in (3.2.2)

Wir nützen dieses Wissen dazu, uns bestimmte Karten des Stapels anzusehen.

3.2.1 Subsetting mit Indizes und Indizes ausschließen – "-"

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

> # Schaue die oberste Karte des Kartenstapels an


> # Selektiere das 1. Element von stapel
> stapel[1]
[1] 4
1 Subsetting mit names ist eine dritte Möglichkeit. Diese besprechen wir in (12.1.2).
3 Vektoren und logische Abfragen 17

> # Schaue die 2. und 4. Karte des Kartenstapels an


> # Selektiere das 2. und 4. Element von stapel
> stapel[c(2, 4)]
[1] 7 2

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

> # Schaue die oberste und unterste Karte des Stapels an


> # Selektiere das 1. und letzte Element von stapel
> stapel[c(1, length(stapel))]
[1] 4 5

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.

> # Betrachte die 3. bis 5. Karte des Stapels


> stapel[3:5]
[1] 6 2 3
> stapel
[1] 4 7 6 2 3 5

> # Speichere die gewünschten Elemente auf temp ab


> temp <- stapel[3:5]
> temp
[1] 6 2 3

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.

> # Verschachtelter Zugriff > # Ersetze stapel[3:5] durch temp


> stapel[3:5][1] > temp[1]
[1] 6 [1] 6

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.

> # Indizes zwischenspeichern


> ind <- 3:5
> ind
[1] 3 4 5

> # Gewünschte Elemente extrahieren


> stapel[ind]
[1] 6 2 3

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.

> # Zugriff auf nicht existierende Einträge


> stapel[c(2, 9, 2)]
[1] 7 NA 7

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

> # Selektiere alle Elemente außer dem 1. sowie 3. bis 5. Element


> stapel[-c(1, 3:5)]
[1] 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.

> # Verteile die Karten auf zwei Spieler


> stapel
[1] 4 7 6 2 3 5

> # Indizes jener Karten, die für Spieler 1 bestimmt sind.


> ind <- c(1, 3, 5)
> ind
[1] 1 3 5
3 Vektoren und logische Abfragen 19

> # Hand des 1. Spielers > # Hand des 2. Spielers


> hand1 <- stapel[ind] > hand2 <- stapel[-ind]
> hand1 > hand2
[1] 4 6 3 [1] 7 2 5

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.

3.2.2 Subsetting mit Wahrheitswerten – TRUE und FALSE, "!"

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

> # Selektiere jedes zweite Element (beginnend beim ersten)


> bool1 <- c(TRUE, FALSE, TRUE, FALSE, TRUE, FALSE)
> bool1
[1] TRUE FALSE TRUE FALSE TRUE FALSE
> hand1 <- stapel[bool1]
> hand1
[1] 4 6 3

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:

> stapel[c(TRUE, FALSE, TRUE, FALSE, TRUE, FALSE)]


[1] 4 6 3

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

> # Selektiere jedes vierte Element (beginnend beim ersten)


> stapel[c(TRUE, FALSE, FALSE, FALSE)]
[1] 4 3

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:

> stapel[c(TRUE, FALSE, FALSE, FALSE, TRUE, FALSE)]


[1] 4 3

Recycling kommt auch bei arithmetischen Rechenoperationen zum Tragen. Im


Gegensatz zum Subsetting gibt uns hier R jedoch eine Warnmeldung aus, wenn kein
vollständiges Recycling möglich ist!

> # Recycling bei arithmetischen Rechenoperationen


> y <- 1:4 * c(-1, 1) # 2 ist ein Teiler von 4 => vollständiges Recycling
> y
[1] -1 2 -3 4
3 Vektoren und logische Abfragen 21

> 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)

> # Karten des 1. Spielers > # Karten des 2. Spielers


> hand1 <- stapel[bool1] > hand2 <- stapel[!bool1]
> hand1 > hand2
[1] 4 6 3 [1] 7 2 5

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 Logische Bedingungen und Operatoren

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.

Tabelle 3.1: Logische Operatoren für Vergleiche und Negation

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

> # 1.) Sind die Werte gleich 5?


> stapel == 5
[1] FALSE FALSE FALSE FALSE FALSE TRUE

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:

> # 2.) Sind die Werte größer als 5?


> stapel > 5
[1] FALSE TRUE TRUE FALSE FALSE FALSE

> # 3.) Sind die Werte ungleich 5?


> stapel != 5
[1] TRUE TRUE TRUE TRUE TRUE FALSE

Beispiel: Ziehe alle Karten aus stapel, die einen Wert von mindestens 5 haben.

> # Selektiere alle Elemente >= 5


> stapel[stapel >= 5]
[1] 7 6 5

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.

> # Selektiere alle Elemente, die nicht kleiner als 5 sind


> stapel[!(stapel < 5)] # mit Klammern
[1] 7 6 5

> # Selektiere alle Elemente, die nicht kleiner als 5 sind


> stapel[! stapel < 5] # ohne Klammern
[1] 7 6 5

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.

> # Selektiere alle negativen Zahlen


> stapel[stapel < 0] # Ergebnis ist ein leerer (numerischer) Vektor
numeric(0)

> # Ein leerer Vektor hat Länge 0. > # Ist obiger Vektor leer?
> length(stapel[stapel < 0]) > length(stapel[stapel < 0]) == 0
[1] 0 [1] TRUE

3.4.2 Bedingungen verknüpfen – "&", "|"

Logische Bedingungen können wir auch miteinander verknüpfen. Hierzu eignen sich
die in Tab. 3.2 dargestellten Verknüpfungsoperatoren "&" bzw. "|".

Tabelle 3.2: Operatoren zur Verknüpfung von Wahrheitswerten

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.

> # Verhalten der Und-Verknüpfung > # Verhalten der Oder-Verknüpfung


> TRUE & TRUE > TRUE | TRUE
[1] TRUE [1] TRUE
> TRUE & FALSE > TRUE | FALSE
[1] FALSE [1] TRUE
> FALSE & FALSE > FALSE | FALSE
[1] FALSE [1] FALSE

Beispiel: Ziehe aus stapel alle Karten größer 2 und kleiner oder gleich 5 heraus.

> stapel
[1] 4 7 6 2 3 5

> stapel > 2


[1] TRUE TRUE TRUE FALSE TRUE TRUE
> stapel <= 5
[1] TRUE FALSE FALSE TRUE TRUE TRUE
> stapel > 2 & stapel <= 5
[1] TRUE FALSE FALSE FALSE TRUE TRUE

> # Selektiere alle Elemente > 2 und <= 5


> stapel[stapel > 2 & stapel <= 5]
[1] 4 3 5
24 A Erste Schritte mit R

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 > 2 & <= 5


Fehler: Unerwartete(s) ’<=’ in "stapel > 2 & <="

R erkennt nicht, dass sich <= 5 auf stapel beziehen soll.


Beispiel: Ziehe aus stapel alle Karten kleiner als 4 oder größer als 5 heraus.

> stapel
[1] 4 7 6 2 3 5

> stapel < 4


[1] FALSE FALSE FALSE TRUE TRUE FALSE
> stapel > 5
[1] FALSE TRUE TRUE FALSE FALSE FALSE
> stapel < 4 | stapel > 5
[1] FALSE TRUE TRUE TRUE TRUE FALSE

> # Selektiere alle Elemente < 4 oder > 5


> stapel[stapel < 4 | stapel > 5]
[1] 7 6 2 3

3.5 Rechnen mit Wahrheitswerten

3.5.1 Absolute und relative Häufigkeiten – sum(), mean()

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?

> # Karten des 1. Spielers > # Karten des 2. Spielers


> hand1 > hand2
[1] 4 6 3 [1] 7 2 5
> hand1 >= 5 > hand2 >= 5
[1] FALSE TRUE FALSE [1] TRUE FALSE TRUE

> # 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

Dieses Konzept greift auch bei anderen arithmetischen Rechenoperatoren.

> # entspricht 1 + 0 + 1 > # entspricht 2 * 1 + 1 * 0


> TRUE + FALSE + TRUE > 2 * TRUE + 1 * FALSE
[1] 2 [1] 2

Wir werden in (11) das zugrundeliegende Konzept der Typumwandlung genauer


beleuchten. Im Moment genügt es zu wissen, dass TRUE in 1 und FALSE in 0 umge-
wandelt wird, wenn wir arithmetische Rechenoperationen anwenden.
Interessieren wir uns für den Anteil der TRUE, also für den Anteil der Elemente,
die eine Bedingung erfüllen, so arbeiten wir einfach mit der Funktion mean():

> # Bestimme den Anteil der Karten im Stapel mit einem Wert >= 5
> mean(stapel >= 5)
[1] 0.5

Also 0.5 bzw. 50 Prozent.


Beispiel: Hat Spieler 1 mehr Karten mit einem Wert von mindestens 5 bekommen
als Spieler 2?

> # 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

Also nein, wie uns das FALSE mitteilt.

3.5.2 Indexwerte bestimmen – which()

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 ]

Es werden die zu bool passenden Indexwerte aus 1:length(vektor ) selektiert. Wir


erfahren also, an welchen Stellen sich die TRUE befinden.
26 A Erste Schritte mit R

Beispiel: Die wievielte Karte im Stapel ist die 7?

> 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

> # Ermittle den Index des Eintrags mit dem Wert 7


> index <- (1:length(stapel))[stapel == 7]
> index
[1] 2

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

> # Welchen Wert hat dieses Element?


> stapel[ind1]
[1] 7

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?

> # Kein Element < 0 => leerer Vektor


> which(stapel < 0)
integer(0)

> # Zugriff auf nicht existierendes Element => NA


> which(stapel < 0)[1]
[1] NA

> # Zugriff auf Element(e) mit Index/Wahrheitswert NA


> stapel[which(stapel < 0)[1]]
[1] NA

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().

> # Alle Elemente >= 5 selektieren: Ineffiziente Variante


> stapel[which(stapel >= 5)]
[1] 7 6 5

> # Alle Elemente >= 5 selektieren: Effiziente Variante


> stapel[stapel >= 5]
[1] 7 6 5

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.

3.6 Ersetzen und Tauschen von Werten

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

> # Vertausche 1. und letztes Element - mit Fehler!


> stapel.neu <- stapel[c(length(stapel), 2:(length(stapel - 1)), 1)]
> stapel.neu
[1] 5 7 6 2 3 5 4

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

> # Vertausche 1. und letztes Element - fehlerfrei


> stapel.neu <- stapel[c(length(stapel), 2:(length(stapel) - 1), 1)]
> stapel.neu
[1] 5 7 6 2 3 4

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:

vektor [position ] <- ersetzung

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

> # Verdopple in x alle positiven Einträge


> x[x > 0] <- x[x > 0] * 2
> x
[1] 4 -5 6 8 -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

> # Ersetze alle negativen Werte von x durch 0


> x[x < 0] <- 0
> x
[1] 4 0 6 8 0

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 kopieren


> stapel.neu <- stapel
> stapel.neu
[1] 4 7 6 2 3 5

> # Vertausche 1. und letztes Element - mit Fehler


> stapel.neu[1] <- stapel.neu[length(stapel.neu)]
> stapel.neu[length(stapel.neu)] <- stapel.neu[1]

> 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

2. Wir nützen einen Zwischenpuffer.

Schauen wir uns jetzt den 2. Lösungsansatz an.


30 A Erste Schritte mit R

> stapel.neu <- stapel


> stapel.neu
[1] 4 7 6 2 3 5

> # Vertausche 1. und letztes Element - fehlerfrei


> temp <- stapel.neu[1]
> stapel.neu[1] <- stapel.neu[length(stapel.neu)]
> stapel.neu[length(stapel.neu)] <- temp

> stapel.neu
[1] 5 7 6 2 3 4

Wir speichern den ursprünglichen Eintrag von stapel.neu[1] (4) im Zwischenpuffer


temp ab. Damit geht dieser Eintrag nicht verloren.

3.7 Aus der guten Programmierpraxis

3.7.1 Programmierstil: Sprechende Objektnamen und Sonderzeichen

Frage: Was sollten wir bei der Benennung unserer Objekte beachten?

Regel 2: Verwende kurze und sprechende Objektnamen!

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

Weitere Beispiele für nicht empfohlene bzw. nicht erlaubte Variablennamen:

• x: beliebt, kurz, aber leider nicht sprechend


• 1.hand oder 1.blatt: Variablennamen dürfen nicht mit einer Zahl beginnen.
• hand 1: Leerzeichen sind tabu.

• kartenstoß: Sonderzeichen ("ß") sollten wir vermeiden.


• blätter1: Umlaute sollten wir auch vermeiden.
• gezogen1: Objekte sollten wir mit Substantiven bezeichnen.
• s oder ks: Nur die wenigsten Leute erkennen, dass es sich um eine Abkürzung
für (Karten)Stapel handelt.
Leider werden Umlaute und das "ß" oft nicht richtig interpretiert. Das liegt unter
anderem daran, dass unterschiedliche Betriebssysteme oft unterschiedliche Zeichen-
codierungen verwenden. Es gibt zwar Lösungen dafür, dennoch machen wir es uns
leichter, wenn wir erst gar nicht auf diese zurückgreifen müssen.

Regel 3: Verwende möglichst keine Umlaute (ä, ö, ü) sowie ß.

Verwende sie insbesondere nicht in Codeteilen, die ausgeführt werden. Verwen-


de stattdessen ae, oe, ue bzw. ss.

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.

3.7.2 Programmierstil: Allgemein und automatisiert programmieren

Wenn du folgende Regel 4 beachtest, dann machst du deinen R-Code flexibler, mäch-
tiger und robuster!

Regel 4: Programmiere möglichst allgemein und automatisiert!

Programmiere nach Möglichkeit so, dass R möglichst viele Informationen auto-


matisiert bestimmt und dein Code mit möglichst wenigen Änderungen funkti-
onstüchtig bleibt, wenn sich das Problem (leicht) verändert. Schreibe allfällige
Parameter deines Codes idealerweise ganz oben auf.
32 A Erste Schritte mit R

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

Ausbaufähige Variante Allgemeine, automatisierte Variante


> # Letzte Karte selektieren > # Letzte Karte selektieren
> stapel[6] > stapel[length(stapel)]
[1] 5 [1] 5

> # Jede 2. Karte selektieren > # Jede 2. Karte selektieren


> stapel[c(1, 3, 5)] > stapel[c(TRUE, FALSE)]
[1] 4 6 3 [1] 4 6 3

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?

> # Stapel verlängern


> stapel <- c(stapel, 9, 8)
> stapel
[1] 4 7 6 2 3 5 9 8

Ausbaufähige Variante Allgemeine, automatisierte Variante

> # Letzte Karte selektieren > # Letzte Karte selektieren


> stapel[6] > stapel[length(stapel)]
[1] 5 [1] 8

> # Jede 2. Karte selektieren > # Jede 2. Karte selektieren


> stapel[c(1, 3, 5)] > stapel[c(TRUE, FALSE)]
[1] 4 6 3 [1] 4 6 3 9

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.

> # Stapel wieder zurücksetzen


> stapel <- stapel[-c(length(stapel) - 1, length(stapel))]
> stapel
[1] 4 7 6 2 3 5
3 Vektoren und logische Abfragen 33

Beispiel: Wir wollen für a = 2, b = -5 und c = 3 die Lösungen der quadratischen


Gleichung der Form a · x2 + b · x + c = 0 berechnen. Die Lösungsformel lautet

−b ± b2 − 4ac
x1,2 =
2a
Im Gegensatz zum Code in (2.2.1) setzen wir jetzt die Koeffizienten direkt in die
Formel ein. Der resultierende Albtraum lässt nicht lange auf sich warten.

> # Ermittle die Lösungen für die quadratische Gleichung


> # 2 x^2 - 5 x + 3 = 0

> # Ermittlung der Lösungen


> x1 <- (-(-5) + sqrt((-5)^2 - 4 * 2 * 3)) / (2 * 2)
> x2 <- (-(-5) - sqrt((-5)^2 - 4 * 2 * 3)) / (2 * 2)

> # Anzeigen der Lösungen


> x1
[1] 1.5
> x2
[1] 1

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.

> # Ermittle die Lösungen für die quadratische Gleichung


> # a x^2 + b x + c = 0

> # Festlegen der Koeffizienten


> a <- 2
> b <- -5
> c <- 3

> # Ermittlung der Lösungen


> x1 <- (-b + sqrt(b^2 - 4 * a * c)) / (2 * a)
> x2 <- (-b - sqrt(b^2 - 4 * a * c)) / (2 * a)

> # Anzeigen der Lösungen


> x1
[1] 1.5
> x2
[1] 1

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

3.8.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Wir betrachten den Vektor stapel dieses Kapitels:

> 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!

a) sum(stapel >= 4) e) sum(! (stapel < 4))


b) length(stapel >= 4) f) length(! (stapel < 4))
c) sum(stapel[stapel >= 4]) g) sum(which(stapel >= 4))
d) length(stapel[stapel >= 4]) h) length(which(stapel >= 4))

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

3. Gegeben ist ein beliebiger numerischer Vektor x mit mindestens 3 Elementen.


Unser Ziel ist es, alle Elemente bis auf die letzten beiden zu selektieren.

a) Füge den folgenden beiden fehlerbehafteten Lösungsansätzen Klammern


hinzu, sodass sie die Aufgabe korrekt erfüllen!
> # Beispielvektor
> x <- 1:5
> x
[1] 1 2 3 4 5

> # Ansatz Nr. 1


> x[1:length(x) - 2]
Fehler in x[1:length(x) - 2] :
nur Nullen dürfen mit negativen Indizes gemischt werden

> # Ansatz Nr. 2


> x[-length(x):length(x) - 1]
Fehler in x[-length(x):length(x) - 1] :
nur Nullen dürfen mit negativen Indizes gemischt werden

b) Jetzt wollen wir den Code verallgemeinern. Sei k ∈ N = {1, 2, 3, . . .} und


enthalte der Vektor x mindestens k + 1 Elemente. Modifiziere beide Lö-
sungsansätze, sodass alle Elemente bis auf die letzten k selektiert werden.

4. Der Vektor alter enthält das Alter einiger Personen:

> alter <- c(16, 18, 19, 24, 28, 32, 32, 45)

a) Wie viele Einträge hat der Vektor alter?


b) Wie viel Prozent der Personen sind zwischen 18 und 27 Jahre alt?
c) Wie viele Personen sind jünger als 20 oder älter als 30?

5. Wir betrachten die ausgeteilten Karten aus (3.3) auf Seite 21.

> # Hand von Spieler 1 > # Hand von Spieler 2


> hand1 > hand2
[1] 4 6 3 [1] 7 2 5

Beide Spieler geben jeweils ihre erste Karte an den Mitspieler ab. Folgender
Ansatz existiert bereits:

> # Tausche die erste Karte aus


> hand1[1] <- hand2[1]
> hand2[1] <- hand1[1]

Funktioniert dieser Ansatz? Warum (nicht)? Korrigiere gegebenenfalls den An-


satz, damit er funktioniert.
B Vektorfunktionen für Data Science
und Statistik
Daniel Obszelka
38 B Vektorfunktionen für Data Science und Statistik

4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen

Dort wollen wir in diesem Kapitel hin:

• Funktionen richtig anwenden


• Bekanntschaft mit der R-Hilfe zu Funktionen, Operatoren und Paketen machen
• Sequenzen und Muster erzeugen sowie nützliche Funktionen kennenlernen
• ganzzahlige Divisionen durchführen und deren Rest bestimmen

• komponentenweises Rechnen und vektorwertige Funktionen einführen


• Schärfung der Sensibilität für Problemstellungen

Funktionen ermöglichen es uns, wiederkehrende Aufgaben übersichtlich auszuführen.


Stellen wir uns vor, uns stünde die Funktion sqrt() nicht zur Verfügung. Dann
müssten wir jedes Mal, wenn wir die Wurzel einer Zahl bestimmen wollten, einen
riesigen Codeblock einfügen, der zum Beispiel so aussehen könnte:

> # Die Zahl, von der wir die Wurzel bestimmen wollen
> n <- 9

> # Hilfsvariablen
> a <- 0
> b <- n

> # Ergebnis: wird schrittweise verfeinert


> res <- (a + b) / 2

> while(abs(res^2 - n) >= 10^(-9)) {


+ if (res^2 < n) {
+ a <- res
+ }
+ else {
+ b <- res
+ }
+ res <- (a + b) / 2
+ }

> # Ergebnis ausgeben


> res
[1] 3

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

Wir beantworten in diesem Kapitel unter anderem folgende Fragen:

• Wie bekommen wir nähere Informationen zu einer Funktion? (4.1)


• Wie generieren wir ausgefeilte Sequenzen? (4.2)
• Was müssen wir alles beim Aufruf einer Funktion beachten? (4.3)
• Wie können wir Objekte oder Elemente replizieren? (4.4)
• Wie bestimmen wir, ob eine Zahl durch eine andere teilbar ist? (4.5)

Komponentenweises Rechnen thematisieren wir ausführlich in (4.6). In (4.7) lernen


wir nützliche Funktionen kennen, die uns oft gute Dienste leisten. Die Palette er-
streckt sich über Logarithmen, Rundungsfunktionen, Trigonometrie und aggregieren-
de Logikfunktionen. Wie wir unsere Funktionssammlung erweitern können, bespre-
chen wir in (4.8.1). Abschließend betrachten wir in (4.8.2) unser erstes „Hardcore-
Beispiel“, in dem wir lernen, aufmerksam zu programmieren.

4.1 R-Hilfe – "?", help()

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

> # Hilfe zu der Funktion sum und dem Operator + aufrufen


> # ?sum
> # ?"+"

Es empfiehlt sich, Codezeilen auszukommentieren, welche die Hilfe aufrufen. Also


ein "#" voranstellen, damit beim Codedurchlauf nicht die Hilfe aufgerufen wird.
Die Hilfe zu Funktionen und Operatoren enthält typischerweise folgende Abschnitte:

• Description: Was tut diese Funktion?


• Usage: Informationen über die Parametrisierung, insbesondere die Namen und
die Reihenfolge der Parameter (siehe (4.3.1))
• Arguments: Beschreibung der formalen Argumente (Parameter) der Funktion

• Details: nähere Information zur Funktion und zu den Parametern (welches


Verfahren liegt zugrunde, Wechselwirkungen zwischen den Parametern etc.)
• Value: nähere Informationen zum Output der Funktion
• See Also: Links zu ähnlichen bzw. verwandten Funktionen

• Examples: Anwendungsbeispiele für die Funktion


40 B Vektorfunktionen für Data Science und Statistik

Wollen wir die Hilfe zu einem Paket aufrufen, so schreiben wir:


help(package = Paketname )
Einstweilen kommen wir mit dem Paket base aus.

> # Hilfe zum Paket base aufrufen


> # help(package = base)

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.

4.2 Sequenzen generieren – seq()

Mit der Funktion seq() können wir ausgefeilte Sequenzen generieren. Mit ?seq
erhalten wir Informationen über diese Funktion.

> # Hilfe zur Funktion seq() aufrufen.


> # ?seq

Ein kleiner Auszug aus der Rubrik Usage:

## Default S3 method:
seq(from = 1, to = 1, by = ((to - from)/(length.out - 1)),
length.out = NULL, along.with = NULL, ...)

Wir konzentrieren uns auf die in Tab. 4.1 dargestellten Parameter.

Tabelle 4.1: Parameter der Funktion seq()

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.

> # Aufsteigende Sequenz mit Schrittweite 2


> seq(from = 0, to = 20, by = 2) # length.out ist automatisch 11.
[1] 0 2 4 6 8 10 12 14 16 18 20

> # Absteigende Sequenz mit 5 Elementen und Schrittweite -4


> seq(from = 10, by = -4, length.out = 5) # to ist automatisch 6.
[1] 10 6 2 -2 -6
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 41

> # Abgeschnittene Sequenz


> seq(from = 0, to = 20, by = 3) # length.out ist automatisch 7.
[1] 0 3 6 9 12 15 18

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).

4.3 Regelwerk für Funktionsaufrufe

R verfolgt ein auf den ersten Blick eigenartiges Regelwerk, was den Funktionsaufruf
angeht. Auf den zweiten Blick erleichtert dieses Konzept die Bedienung aber enorm!

4.3.1 Objektübergabe und Parameterzuordnung

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:

> seq(from = 0, to = 100, length.out = 6)


[1] 0 20 40 60 80 100

Hier sprechen wir alle formalen Argumente namentlich exakt an. Was passiert im
folgenden Befehl?

> seq(0, 100, 6)


[1] 0 6 12 18 24 30 36 42 48 54 60 66 72 78 84 90 96

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 2. formale Argument ist to, es bekommt den Wert 100.


• Das 3. formale Argument ist by, das schließlich eine 6 zugewiesen bekommt.
42 B Vektorfunktionen für Data Science und Statistik

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:

> # length.out exakt ansprechen > # by (3. Argument) überspringen


> seq(0, 100, length.out = 6) > seq(0, 100, , 6)
[1] 0 20 40 60 80 100 [1] 0 20 40 60 80 100

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?

> seq(leng = 6, 100, from = 0)


[1] 0 20 40 60 80 100

Hier erfolgt der Aufruf wie folgt:

1. R überprüft, ob die übergebenen Argumente leng und from exakt einem


formalen Argument zugeordnet werden können. Das ist bei from = 0 der Fall,
folglich wird from auf 0 gesetzt.
seq(leng = 6, 100, from = 0)
2. Nun überprüft R, ob es abgekürzte Argumente gibt, die eindeutig einem
formalen Argument zugeordnet werden können. Das Argument leng ist eine
Abkürzung für das formale Argument length.out und kann diesem eindeutig
zugeordnet werden, also wird length.out auf 6 gesetzt.
seq(leng = 6, 100, from = 0)
Das formale Argument length.out wird also namentlich eindeutig abge-
kürzt mittels leng angesprochen. Gäbe es zum Beispiel ein weiteres formales
Argument namens length.in, so käme eine Fehlermeldung der Bauart:
Argument 1 passt auf mehrere formale Argumente
R wüsste nicht, ob das Argument leng dem formalen Argument length.out
oder length.in gelten soll. Abkürzungen müssen also immer eindeutig sein!

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)

Voilà! Das ist jedoch noch nicht die ganze Wahrheit.


4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 43

4.3.2 Dynamische und statische Defaultwerte

Bei der Funktion seq() dürfen wir maximal drei Parameter spezifizieren. Wenn wir
mehr als drei Parameter spezifizieren, folgt eine Fehlermeldung.

> # Spezifiziere 4 formale Argumente - Fehlermeldung


> seq(from = 0, to = 10, by = 2, length.out = 6)
Fehler in seq.default(from = 0, to = 10, by = 2, length.out = 6) :
zu viele Argumente

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!

> seq(from = 1, to = 10) > seq(from = 10, to = 1)


[1] 1 2 3 4 5 6 7 8 9 10 [1] 10 9 8 7 6 5 4 3 2 1

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.

> seq(length.out = 10) > seq(length.out = 10, by = 2)


[1] 1 2 3 4 5 6 7 8 9 10 [1] 1 3 5 7 9 11 13 15 17 19

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.

4.3.3 Dreipunkteargument und fehlende Werte – ..., NA, na.rm

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.

> x <- c(1:4, NA) # Beispielvektor mit einem NA


> x
[1] 1 2 3 4 NA

> sum(x)
[1] NA

Fehlende Werte werden nicht ignoriert. Wir schauen in der Hilfe nach (?sum) ...

sum(..., na.rm = FALSE)

... 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:

> sum(x, TRUE)


[1] NA

Warum nicht 10 herauskommt, erläutern wir anhand des folgenden Codes:

> sum(1:4, TRUE)


[1] 11

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?

Regel 5: Exakte Benennung nach dem Dreipunkteargument

Alle formalen Argumente, die in einer Funktion nach dem Dreipunktear-


gument definiert sind, müssen wir exakt benennen.

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

4.4 Objekte und Elemente replizieren – rep()

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

Tabelle 4.2: Parameter der Funktion rep()

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.

Lernen wir rep() anhand von Codebeispielen kennen!

> # 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.

> # Einsatz von length.out > # Alternativer Einsatz von times


> rep(0:2, length.out = 10) > rep(x = 0:2, times = 3:1)
[1] 0 1 2 0 1 2 0 1 2 0 [1] 0 0 0 1 1 2

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

4.5 Ganzzahlige Division, Rest und Teilbarkeit – "%/%", "%%"

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:

> # Ganzzahlige Division > # Rest der ganzzahligen Division


> 7 %/% 3 > 7 %% 3
[1] 2 [1] 1

> # Kontrollrechnung > # Ist 7 durch 3 teilbar?


> (7 %/% 3) * 3 + (7 %% 3) > (7 %% 3) == 0
[1] 7 [1] FALSE

7 ist nicht durch 3 teilbar, wie uns das FALSE mitteilt.

4.6 Komponentenweises Rechnen

Arithmetische Rechenoperationen werden in R komponentenweise bzw. elementweise


durchgeführt. Das heißt, dass der i. Eintrag des einen Vektors mit dem i. Eintrag
des anderen Vektors verknüpft wird. Sind die Vektoren nicht gleich lang, so greift
das Recycling, das wir in (3.3) kennengelernt haben.
Beispiel: Bestimme mit den Techniken aus (4.5) alle einstelligen und positiven
Zahlen, die durch 3 teilbar sind.

> x <- 1:9


> x
[1] 1 2 3 4 5 6 7 8 9

> # Ganzzahlige Division > # Rest der ganzzahligen Division


> ganz <- x %/% 3 > rest <- x %% 3
> ganz > rest
[1] 0 0 1 1 1 2 2 2 3 [1] 1 2 0 1 2 0 1 2 0

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

> # Rekonstruktion des Vektors x:


> ganz * 3 + rest # Die 3 wird vor der Multiplikation recycelt.
[1] 1 2 3 4 5 6 7 8 9

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

Die Addition der Vektoren ganz * 3 und rest erfolgt komponentenweise.


Jetzt selektieren wir noch alle Zahlen aus x, die durch 3 teilbar sind.

> x[rest == 0] # Durch 3 teilbar <=> Rest == 0


[1] 3 6 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.

4.7 Nützliche Funktionen

4.7.1 Exponentialfunktion und Logarithmus – exp(), log()

Mit Hilfe von exp() bzw. log() können wir die Exponentialfunktion anwenden
bzw. Logarithmen berechnen. Die Anwendung ist ganz einfach.

> exp(1) # Eulersche Zahl e^1


[1] 2.718282
> exp(1:3) # vektorwertige Anwendung: e^1, e^2, e^3
[1] 2.718282 7.389056 20.085537
> log(exp(1:3)) # der natürliche Logarithmus
[1] 1 2 3

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:

> # 10er-Logarithmus von 1000 > # Kontrolle: Es passt!


> log(1000, base = 10) > 10 ^ (log(1000, base = 10))
[1] 3 [1] 1000
48 B Vektorfunktionen für Data Science und Statistik

4.7.2 Runden – round(), floor(), ceiling(), trunc(), digits

Uns stehen unter anderem folgende 4 Funktionen zum Runden zur Verfügung:

• round() – mathematische Rundung


• floor() – generell abrunden
• ceiling() – generell aufrunden
• trunc() – stets Richtung 0 runden

Wir betrachten ein Zahlenbeispiel, um die feinen Unterschiede aufzuzeigen.

> x <- -4.5:4.5


> x
[1] -4.5 -3.5 -2.5 -1.5 -0.5 0.5 1.5 2.5 3.5 4.5

> round(x) # rundet mathematisch


[1] -4 -4 -2 -2 0 0 2 2 4 4
> floor(x) # rundet generell ab
[1] -5 -4 -3 -2 -1 0 1 2 3 4
> ceiling(x) # rundet generell auf
[1] -4 -3 -2 -1 0 1 2 3 4 5
> trunc(x) # rundet Richtung 0
[1] -4 -3 -2 -1 0 0 1 2 3 4

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.

> x <- c(4.9, 5.49, 5.5, 5.51)


> x
[1] 4.90 5.49 5.50 5.51

> # 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

Leider bieten uns die anderen Rundungsfunktionen diesen bequemen digits-Service


nicht an. Zeit für einen hübschen Workaround bzw. für einen Griff in die Trickkiste!

Trick: Beliebige Rundung

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.

Setzen wir diesen Trick in die Tat um!


Beispiel: Runde die Zahlen 114, 221, 428, 545 und 555 auf ganze 10.

> zahlen <- c(114, 221, 428, 545, 555)


> zahlen
[1] 114 221 428 545 555

> k <- 10

> # Runde auf ganze k


> k * round(zahlen / k)
[1] 110 220 430 540 560

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.

4.7.3 Absolutbetrag und Vorzeichenfunktion – abs(), sign()

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.

> x <- -3:3


> x
[1] -3 -2 -1 0 1 2 3

> # Absolutbetrag > # Vorzeichen bestimmen


> abs(x) > sign(x)
[1] 3 2 1 0 1 2 3 [1] -1 -1 -1 0 1 1 1

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

4.7.4 Trigonometrie – sin(), cos(), tan(), pi

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.

> # Sequenz von 0 bis 2*pi > round(x, digits = 3)


> x <- seq(0, 2 * pi, by = pi / 2) [1] 0.000 1.571 3.142 4.712 6.283

> round(sin(x), digits = 3) > round(cos(x), digits = 3)


[1] 0 1 0 -1 0 [1] 1 0 -1 0 1

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).

4.7.5 Aggregierende Logikfunktionen – all(), any(), xor()

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.

Tabelle 4.3: Aggregierende Logikfunktionen

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

Beispiel: Gegeben ist der Vektor x:

> x <- c(3, 4, 9, 2)


> x
[1] 3 4 9 2

Beantworte folgende Fragen:

1. Sind alle Einträge von x positiv?

2. Gibt es in x zumindest eine Zahl, die durch 5 teilbar ist?


3. Wie viele Einträge von x sind durch drei teilbar oder größer gleich 4, erfüllen
also mindestens eine dieser Bedingungen?
4. Wie viele Einträge von x sind entweder durch drei teilbar oder größer gleich 4,
erfüllen also genau eine dieser Bedingungen?

Lösung: Grün markierte Elemente erfüllen die Bedingung(en), orange markierte


Elemente erfüllen sie nicht.

> # 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.

> # Durch 3 teilbar? > # Größer oder gleich 4?


> bool.tb3 <- x %% 3 == 0 > bool.ge4 <- x >= 4
> bool.tb3 > bool.ge4
[1] TRUE FALSE TRUE FALSE [1] FALSE TRUE TRUE FALSE
> x > x
[1] 3 4 9 2 [1] 3 4 9 2

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?

4.8 Aus der guten Programmierpraxis

4.8.1 Programmierstil: Funktionssammlung erweitern

Wie können wir unser Repertoire an Funktionen erweitern? Einige Möglichkeiten:

• dieses Buch genau durcharbeiten


• R-Hilfen zu Funktionen und Paketen studieren
• Internetrecherchen (z. B. https://www.r-project.org/search.html)
• weitere Bücher über R lesen. In (42) findest du neben Buchempfehlungen auch
eine thematisch sortierte Liste mit weiteren interessanten R-Paketen.

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

> # Indexvektor mit Sequenzoperator > # Indexvektor mit seq_along()


> 1:length(stapel) > seq_along(stapel)
[1] 1 2 3 4 5 6 [1] 1 2 3 4 5 6
4 Funktionsaufrufe, R-Hilfe und nützliche Funktionen 53

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.

4.8.2 Fallbeispiel: Lottotippscheine

Wir betrachten einen Lottotippschein der österreichischen Lotterien. In Infobox 1


erläutern wir Lotto 6 aus 45. Gleich vorweg: Dieses Beispiel hat es in sich! Solltest
du nicht alles auf Anhieb verstehen, mach dir bitte keine Sorgen!

Infobox 1: Lotto 6 aus 45

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.

Wir verwalten unseren Tipp in R im Vektor tipp:


> tipp <- c(4, 7, 15, 19, 20, 38)
54 B Vektorfunktionen für Data Science und Statistik

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.

> # Generiere Spalten- und Zeilenindizes für die Zahlen


> spalte <- rep(1:6, length.out = 45)
> zeile <- rep(1:8, each = 6)[1:45]

> 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.

> zeile[tipp] # Zeilen der getippten Zahlen selektieren


[1] 1 2 3 4 4 7
> spalte[tipp] # Spalten der getippten Zahlen selektieren
[1] 4 1 3 1 2 2

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...

> # Die Daten der Lotterie


> nzahlen <- 45 # Anzahl der Zahlen der Lotterie
> ncol <- 6 # Anzahl der Spalten des Tippscheins

> # 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

> # Generiere Spalten- und Zeilenindizes für die Zahlen


> zeile <- rep(1:nrow, each = ncol)[1:nzahlen]
> spalte <- rep(1:ncol, length.out = nzahlen)

> zeile[tipp] # Zeilen der getippten Zahlen selektieren


[1] 1 2 3 4 4 7
> spalte[tipp] # Spalten der getippten Zahlen selektieren
[1] 4 1 3 1 2 2

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

Abbildung 4.3: Lottoschein der Lotterie 6 aus 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:

> # Die Daten der Lotterie


> nzahlen <- 49 # Anzahl der Zahlen der Lotterie
> ncol <- 7 # Anzahl der Spalten (columns) des Tippscheins

> # Bestimme die Anzahl der Zeilen (rows) des Tippscheins


> nrow <- nzahlen %/% ncol + 1
> nrow
[1] 8

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.

> # Ist die letzte Zeile des Lottoscheins voll?


> c(45, 49) %% c(6, 7) == 0
[1] FALSE TRUE

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

> # 1 hinzuaddieren, wenn die letzte Zeile nicht voll ist.


> + (c(45, 49) %% c(6, 7) != 0)
[1] 1 0

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 :-)

> # Korrekte und saubere Lösung des Beispiels

> # Die Daten der Lotterie


> nzahlen <- 45 # Anzahl der Zahlen der Lotterie
> ncol <- 6 # Anzahl der Spalten (columns) des Tippscheins

> # Bestimme die Anzahl der Zeilen (rows) des Tippscheins


> nrow <- nzahlen %/% ncol + (nzahlen %% ncol != 0)
> nrow
[1] 8

> # Generiere Spalten- und Zeilenindizes für die Zahlen


> zeile <- rep(1:nrow, each = ncol)[1:nzahlen]
> spalte <- rep(1:ncol, length.out = nzahlen)

> zeile[tipp] # Zeilen der getippten Zahlen selektieren


[1] 1 2 3 4 4 7
> spalte[tipp] # Spalten der getippten Zahlen selektieren
[1] 4 1 3 1 2 2

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:

Regel 6: Hinterfrage kritisch deine (impliziten) Annahmen!

Implizite Annahmen können sich in manchen Konstellationen als falsch er-


weisen und zu Fehlern führen. Überlege dir, welche Annahmen deinem Code
zugrunde liegen und mache deinen Code möglichst unabhängig von ihnen!
58 B Vektorfunktionen für Data Science und Statistik

4.9 Abschluss

4.9.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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. Generiere die folgenden Vektoren ohne die Funktion c() zu verwenden:

• 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

a) den Parameter length.out,


b) den Parameter to,
c) die Parameter length.out und to

spezifizieren. Ziehe dazu die R-Hilfe zu seq() zurate!

4. Generiere mit Hilfe der Funktion sample()


a) eine Permutation der Zahlen von 1 bis 6.
b) einen zufälligen Tipp für Lotto 6 aus 45 (vgl. Infobox 1 auf Seite 53).
c) 30 Würfelwürfe eines herkömmlichen 6-seitigen Würfels.
d) 20 Zufallszahlen aus folgender diskreter Verteilung (k ∈ N ∪ {0}):

1/4 für k = 0 oder k = 2

P (X = k) = 1/2 für k = 1

0 sonst

Welches Experiment könnte dieser Verteilung zugrunde liegen?


60 B Vektorfunktionen für Data Science und Statistik

5. Gegeben ist ein Vektor x <- c(1:4, NA). Studiere, was bei folgenden Abfra-
gen jeweils passiert. Was beobachtest du?

a) x[NA] e) sum(x[x < 3])


b) x[c(1, NA)] f) sum(x < 3, na.rm = TRUE)
c) x < 3 g) sum(x[x < 3], na.rm = TRUE)
d) x[x < 3] h) length(x[x < 3])

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)?

a) all(x > 0) c) prod(x > 0) == 1

b) any(x <= 0) d) sum(x <= 0) == 0

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

10. Betrachte folgenden Code:

> x <- c(-3, 0, 0.5, 2)


> (x > 0) - (x < 0)

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:

> x <- c(6.49, 6.5, 6.51)


> x
[1] 6.49 6.50 6.51

> # Gewünschte Ausgabe


> res
[1] 6 7 7

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.

> sum(2, rm.na = TRUE, 3:4)


> sum(2, TRUE, NA, na.rm = TRUE, 3:4)

Hinweis: Schau genau!


62 B Vektorfunktionen für Data Science und Statistik

5 Arbeitsverzeichnis, Objekte und Dateiordner

Dort wollen wir in diesem Kapitel hin:

• Objekte auflisten und löschen


• Dateipfade kennenlernen und Arbeitsverzeichnis wechseln
• Objekte speichern und laden

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:

• Wie können wir alle erstellten Objekte auflisten? (5.1.1)


• Wann und warum ist es sinnvoll, (alle) Objekte zu löschen? (5.1.2)
• Wie greifen wir auf Ordner zu und wie wechseln wir jenes Verzeichnis, auf das
R beim Speichern und Laden von Objekten und Skripten zugreift? (5.2)
• Wie sichern und laden wir Objekte in R und welche Dateien und Ordner be-
finden sich im aktuellen Verzeichnis? (5.3), (5.4)

In (5.5) listen wir Funktionen zur Manipulation von Dateien und Ordnern auf.

5.1 Objekte

5.1.1 Objekte auflisten – ls()

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:

> # Zeige alle erstellten Objekte an


> ls()
[1] "hand1" "hand2" "stapel" "x"

Auf existierende Objekte können wir zugreifen, während beim Zugriff auf nicht exis-
tierende Objekte eine Fehlermeldung erscheint.

> # Zugriff auf ein existierendes Objekt


> stapel
[1] 4 7 6 2 3 5

# Zugriff auf ein nicht existierendes Objekt - Fehlermeldung


mich.gibt.es.nicht
Fehler: Objekt ’mich.gibt.es.nicht’ nicht gefunden
© 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_5
5 Arbeitsverzeichnis, Objekte und Dateiordner 63

5.1.2 Objekte löschen – rm(), rm(list = ls())

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"

> hand1 > hand2


[1] 4 6 3 [1] 7 2 5

Die Objekte hand1 und hand2 existieren, wir können auf diese Objekte zugreifen.
Jetzt löschen wir diese beiden Objekte.

> # Lösche die Objekte hand1 und hand2


> rm(hand1, hand2)

> ls()
[1] "stapel" "x"

> hand1
Fehler: Objekt ’hand1’ nicht gefunden
> hand2
Fehler: Objekt ’hand2’ nicht gefunden

Die Objekte hand1 und hand2 sind nun gelöscht.


Wollen wir alle Objekte löschen, so können wir Gebrauch von ls() machen.

> # Lösche alle erstellten Objekte


> rm(list = ls())

> ls()
character(0)

Wie uns das character(0) mitteilt, sind keine Objekte vorhanden.


Frage: Wann und warum ist das Löschen sämtlicher Objekte sinnvoll?
Zum Beispiel dann, wenn wir einen R-Code schreiben, den wir anderen präsentie-
ren und sichergehen wollen, dass keine Fehlermeldungen erscheinen, weil bestimmte
Objekte nicht existieren. Oftmals benennen wir Objekte am Ende noch um und
vergessen, die Namenskorrekturen an allen Stellen vorzunehmen.

Regel 7: Führe den Befehl rm(list = ls()) ganz zu Beginn aus!

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

5.2 Pfade und Arbeitsverzeichnis – getwd(), setwd(), ".."

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:

> # Arbeitsverzeichnis anzeigen


> getwd()
[1] "C:/Users/Daniel/Documents"

Die Struktur eines absoluten Dateipfades sieht wie folgt aus:


"Laufwerk :/Ordner1 /Ordner2 /..."
In R werden Ordner durch einen Schrägstrich ("/") getrennt und nicht durch einen
Backslash ("\"). Wenn wir den Backslash verwenden wollen, dann müssen wir zwei
Backslashes nehmen. Wir klären in (23), warum zwei Backslashes notwendig sind.
Außerdem müssen wir den Pfad in Anführungszeichen setzen.
Wollen wir das Arbeitsverzeichnis wechseln, so machen wir Gebrauch von der
Funktion setwd() und geben den gewünschten Pfad in obiger Bauart an. Wenn wir
beispielsweise ins Verzeichnis D:/Studium/Statistisches Programmieren wech-
seln wollen, so schreiben wir:

> # Arbeitsverzeichnis wechseln


> setwd(dir = "D:/Studium/Statistisches Programmieren")

Erscheint keine Fehlermeldung, so hat alles geklappt! Existiert der Ordner nicht, so
wird eine Fehlermeldung ausgegeben.

> # Arbeitsverzeichnis wechseln - nicht existierender Ordner


> setwd(dir = "D:/Dieser Ordner existiert nicht")
Fehler in setwd(dir = "D:/Dieser Ordner existiert nicht") :
kann Arbeitsverzeichnis nicht wechseln

Neben absoluten Pfaden gibt es auch relative Dateipfade. R erkennt einen relativen
Pfad daran, dass keine Laufwerkinformation angegeben wird.

> # Arbeitsverzeichnis wechseln


> setwd("D:/Studium") # Absoluter Pfad
> getwd()
[1] "D:/Studium"

Wir befinden uns also jetzt im Ordner D:/Studium (= Arbeitsverzeichnis). Wollen


wir innerhalb dieses Ordners in den Unterordner Statistisches Programmieren
wechseln, so können wir Gebrauch von relativen Pfaden machen.
5 Arbeitsverzeichnis, Objekte und Dateiordner 65

> # Wechsle in den Unterordner Statistisches Programmieren


> setwd("Statistisches Programmieren") # Relativer Pfad
> getwd()
[1] "D:/Studium/Statistisches Programmieren"

Im zweiten setwd()-Aufruf wird "Statistisches Programmieren" als relativer


Pfad erkannt (da keine Laufwerkinformation vorhanden). Daher hängt R diesen
relativen Pfad an das derzeitige Arbeitsverzeichnis an. Wiederum erscheint eine Feh-
lermeldung, wenn der Zielordner nicht existiert.
Wollen wir in den darüberliegenden Ordner wechseln, so übergeben wir der
Funktion setwd() zwei Punkte ("..").

> getwd()
[1] "D:/Studium/Statistisches Programmieren"

> # Wechsle in den darüberliegenden Ordner


> setwd("..")
> getwd()
[1] "D:/Studium"

5.3 Objekte speichern und laden – save(), load()

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.

> # Arbeitsverzeichnis festlegen


> setwd("D:/Studium/Statistisches Programmieren")

> # Kartenstapel erzeugen


> stapel <- c(4, 7, 6, 2, 3, 5)

> # Karten an 2 Spieler verteilen


> bool1 <- c(TRUE, FALSE)
> hand1 <- stapel[bool1]
> hand2 <- stapel[!bool1]

> # Diverse Berechnungen anstellen

> # Daten sichern


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> save(stapel, hand1, hand2, file = "Karten.RData")

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.

> # Kartenspieldaten laden


> load("Karten.RData")

> # Kartenspieldaten laden - mit den Namen der geladenen Objekte


> objekte <- load("Karten.RData")
> objekte
[1] "stapel" "hand1" "hand2"

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.

5.4 Namen von Ordnern und Dateien abrufen – list.files()

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:

> # Arbeitsverzeichnis anzeigen


> getwd()
[1] "D:/Studium/Statistisches Programmieren"

> # Ordner und Objekte des Verzeichnisses anzeigen


> list.files()
[1] "Neuer Ordner" "Karten.RData" "Quadratgleichung.R"

Im derzeitigen Arbeitsverzeichnis "D:/Studium/Statistisches Programmieren"


finden wir den Ordner "Neuer Ordner" sowie die beiden Dateien "Karten.RData"
und "Quadratgleichung.R". Dateien erkennen wir in erster Linie an vorhandenen
Dateiendungen, hier ".RData" und ".R". In Übung 2 auf Seite 69 schauen wir uns
an, wie wir ausschließlich auf Ordner zugreifen.
5 Arbeitsverzeichnis, Objekte und Dateiordner 67

5.5 Funktionen zur Manipulation von Dateien und Ordnern

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

Wir wenden diese Funktionen in einem kleinen Workflow an.

> # Arbeitsverzeichnis anzeigen


> getwd()
[1] "D:/Studium/Statistisches Programmieren"

> # Datei im Arbeitsverzeichnis erzeugen


> file.create("AlteDatei.txt")
[1] TRUE
> # Datei im Arbeitsverzeichnis kopieren
> file.copy(from = "AlteDatei.txt", to = "KopierteDatei.txt")
[1] TRUE
> # Datei im Arbeitsverzeichnis umbenennen
> file.rename(from = "AlteDatei.txt", to = "NeueDatei.txt")
[1] TRUE

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

> dateien <- c("AlteDatei.txt", "NeueDatei.txt", "KopierteDatei.txt")

> file.exists(dateien) # Existieren die Dateien?


[1] FALSE TRUE TRUE

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.

> file.remove(dateien) # Dateien löschen


Warnung in file.remove(dateien)
kann Datei ’AlteDatei.txt’ nicht löschen. Grund ’No such file or directory’
[1] FALSE TRUE TRUE

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.

> file.exists(dateien) # Existieren die Dateien?


[1] FALSE FALSE FALSE

Wir sehen uns in Übung 1 auf Seite 69 die Erstellung von Ordnern an.

5.6 Abschluss

5.6.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Wie listen wir alle existierenden Objekte auf? (5.1.1)


• Wie löschen wir einzelne bzw. alle Objekte? Wann und warum ist es sinnvoll,
alle Objekte zu löschen? (5.1.2)
• Was ist der Unterschied zwischen absoluten und relativen Pfaden? Woran er-
kennt R den Unterschied? Was ist das Arbeitsverzeichnis und wie können wir
es abfragen und wechseln? Wie können wir in den überliegenden Ordner wech-
seln? (5.2)
• Wie sichern wir erstellte Objekte? Was müssen wir beim Funktionsaufruf bei
der Übergabe des Dateinamens beachten? Welche Dateiendung sollten wir ver-
wenden? Wie laden wir Objekte und wie können wir die Namen der geladenen
Objekte einsehen? (5.3)
• Wie listen wir alle Ordner und Dateien eines Ordners auf? (5.4)

• 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 weitreichende Automatisierung des Codes ist ein wichtiges Qualitätsmerkmal!


Natürlich könnten wir zum Beispiel per Hand überprüfen, ob ein Ordner existiert,
aber je mehr wir R überlassen, desto besser! In (39) lernen wir, wie wir Skripte
ausführen können ohne die Maus verwenden zu müssen.

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

1. Eine kleine Fingerübung um Routine zu bekommen. Bevor du startest, wähle


einen Ordner deiner Wahl aus, wobei es sich nicht um ein Laufwerk handeln
soll, zum Beispiel "D:/Studium/Statistisches Programmieren".
Führe jetzt folgende Aufgaben der Reihe nach aus:

a) Wechsle mit Hilfe eines absoluten Pfades das Arbeitsverzeichnis auf


den Ordner deiner Wahl.
b) Erstelle im Ordner deiner Wahl einen neuen Ordner, der noch nicht exis-
tiert.
c) Wechsle nun das Arbeitsverzeichnis in den darüber liegenden Ordner und
erstelle von dort aus mit Hilfe eines relativen Pfades im Ordner deiner
Wahl einen weiteren Ordner, der noch nicht existiert.
d) Frage ab, ob im Ordner deiner Wahl die beiden gerade erstellten Ord-
ner existieren. und zwar einmal mit file.exists() und einmal ohne
file.exists() (zum Beispiel mit list.files()). Das Ergebnis soll ent-
weder ein TRUE oder ein FALSE sein.

Lösche die beiden Ordner am besten wieder per Hand.


2. Finde mit der R-Hilfe zur Funktion list.files() folgendes heraus:
a) Was bewirkt der Parameter recursive?
b) Wie können wir lediglich die Ordner (ohne Dateien) anzeigen lassen?
70 B Vektorfunktionen für Data Science und Statistik

6 Mengen, Sortieren und Kombinatorik

Dort wollen wir in diesem Kapitel hin:

• nützliche Mengenfunktionen kennenlernen

• die Welt des Sortierens erkunden


• Bekanntschaft mit elementaren kombinatorischen Funktionen machen

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

Abbildung 6.1: Ausgefüllter Lottoschein von Lotto 6 aus 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.

Ob wir gewonnen haben?


Wir verwalten in R die Daten als Vektoren:

> # Lotto-Ziehungsergebnis und Tipp verwalten


> lotto <- c(3, 19, 24, 23, 7, 34, 16)
> tipp <- c(4, 7, 15, 19, 20, 38)

© 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

Folgende Fragen drängen sich unter anderem auf:

• 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

6.1.1 Matchings bestimmen – "%in%"

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

Im ersten Impuls würden viele an dieser Stelle folgendes probieren:

> tipp == lotto


Warnung in tipp == lotto
Länge des längeren Objektes
ist kein Vielfaches der Länge des kürzeren Objektes
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE

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.

> # Matchings bestimmen


> tipp %in% lotto
[1] FALSE TRUE FALSE TRUE FALSE FALSE

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!

> sum(tipp %in% lotto)


[1] 2

Wenn wir die Anzahl der Übereinstimmungen zählen wollen, so ist es übrigens egal,
welcher der beiden Vektoren links steht.

> # Vertausche tipp und lotto


> lotto %in% tipp
[1] FALSE TRUE FALSE FALSE TRUE FALSE FALSE
> sum(lotto %in% tipp)
[1] 2

Die orange bzw. grün markierten TRUE gehören jeweils zu 7 bzw. 19.

Beachte: Wenn wir die übereinstimmenden Elemente jedoch selektieren wollen,


dann müssen wir aufpassen! Hier haben wir genau zwei Möglichkeiten:

> # Selektion: Möglichkeit 1 > # Selektion: Möglichkeit 2


> tipp[tipp %in% lotto] > lotto[lotto %in% tipp]
[1] 7 19 [1] 19 7

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

6.1.2 Mehrfacheinträge streichen – unique()

Mit der Funktion unique() können wir aus einem Vektor mehrfach vorkommen-
de Elemente entfernen. Betrachten wir ein kleines Beispiel:

> x <- c(2, 1, 2, 3, 2, 1)


> x
[1] 2 1 2 3 2 1

> # Bilde Vektor mit den unterscheidbaren Elementen von x


> unique(x)
[1] 2 1 3

6.1.3 Schnittmenge und Vereinigung mit dem %in%-Operator

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.

> # Mengen als Vektor definieren


> A <- c(2, 3, 4, 5, 7)
> B <- c(1, 2, 6, 7)

> A > B
[1] 2 3 4 5 7 [1] 1 2 6 7

> # Vereinigung von A und B


> c(A, B)
[1] 2 3 4 5 7 1 2 6 7
> unique(c(A, B)) # Wollen nur unterscheidbare Elemente
[1] 2 3 4 5 7 1 6

Wir benötigen hier unique(), um die mehrfach vorkommende 2 und 7 zu streichen.

> # Schnittmenge von A und B > # Schnittmenge von B und A


> A[A %in% B] > B[B %in% A]
[1] 2 7 [1] 2 7

Wir betrachten die linke Schnittmenge tabellarisch:

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!

6.1.4 Schnittmenge und Vereinigung – intersect(), union()

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

> # Vereinigung von A und B > # Schnittmenge von A und B


> union(A, B) > intersect(A, B)
[1] 2 3 4 5 7 1 6 [1] 2 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)

6.2.1 Sortierung der Einträge – sort()

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.

> # Zu sortierender Vektor


> x
[1] 5 7 5 3 4

> # Aufsteigend sortieren > # Absteigend sortieren


> sort(x) > sort(x, decreasing = TRUE)
[1] 3 4 5 5 7 [1] 7 5 5 4 3

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

> # Sortiere die Lottozahlen


> sort(lotto)
[1] 3 7 16 19 23 24 34

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.

> # Sortiere die Lottozahlen. Die Zusatzzahl bleibt an letzter Stelle.


> c(sort(lotto[-length(lotto)]), lotto[length(lotto)])
[1] 3 7 19 23 24 34 16

6.2.2 Generierung der sortierenden Entnahmereihenfolge – order()

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

order(..., na.last = TRUE, decreasing = FALSE,


method = c("auto", "shell", "radix"))

Bei method verweisen wir auf die R-Hilfe und na.last betrachten wir in (14.3.3).

> # Indexreihenfolge bestimmen (aufsteigend)


> x
[1] 5 7 5 3 4
> order(x)
[1] 4 5 1 3 2

Um den Vektor x (aufsteigend) zu sortieren, entnehme zunächst den 4. Eintrag (3),


anschließend den 5. (4), dann den 1. (5), den 3. (5) und schlussendlich den 2. (7).
In Abb. 6.3 veranschaulichen wir das Prozedere.

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.

> # Indexreihenfolge bestimmen (absteigend)


> x
[1] 5 7 5 3 4
> order(x, decreasing = TRUE)
[1] 2 1 3 5 4

6.2.3 Ränge bestimmen – rank()

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

rank(x, na.last = TRUE,


ties.method = c("average", "first", "last", "random", "max", "min"))

Den Parameter na.last betrachten wir (wie du evtl. schon ahnst) in (14.3.3).

> x
[1] 5 7 5 3 4

> # Bestimme die Ränge


> rank(x)
[1] 3.5 5.0 3.5 1.0 2.0

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

> # Ties nach der Reihenfolge des Auftretens auflösen


> rank(x, ties.method = "first")
[1] 3 5 4 1 2

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

Tabelle 6.1: Steuerung des Tie-Verhaltens bei rank()

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

6.2.4 Vektoren umdrehen – rev()

Mit der Funktion rev() können wir einen Vektor umdrehen.

> 1:5 > rev(1:5)


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

6.2.5 Mehrfachsortierung – order()

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.

Infobox 2: Fußball-EM 2017 der Frauen

Bei der Fußball-Europameisterschaft 2017 sind die österreichischen Fußballerin-


nen sensationell bis ins Halbfinale vorgestoßen und haben den hervorragenden
3. Platz geschafft. Grund genug, diesem Team das folgende Beispiel zu widmen!

Tab. 6.2 enthält die Namen und die Geburtsdaten von einigen bei der Fußball-EM
2017 eingesetzten österreichischen Fußballspielerinnen.3

Tabelle 6.2: Namen und Geburtsdaten einiger österreichischer Fußballspielerinnen

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

3 Quelle: ÖFB:http://www.oefb.at/das-frauennationalteam-pid878 (24.09.2017)


6 Mengen, Sortieren und Kombinatorik 79

Bemerkung: Der didaktischen Reduktion (mache Beispiele einfach) ist es geschul-


det, dass andere wichtige Spielerinnen im Beispiel nicht vorkommen.
Beispiel: Wir wollen die Namen der Spielerinnen nach dem Geburtsdatum sortieren
von jung (vorne) bis alt (hinten).
Wir geben zunächst gewissenhaft und mit viel Hingabe die Daten in R ein. Die
Namen der Spielerinnen müssen wir dabei unter Anführungszeichen setzen!

> # Die Daten der Fußballspielerinnen eingeben.


> name <- c("Manuela Zinsberger", "Carina Wenninger", "Viktoria Schnaderbeck",
+ "Katharina Schiechtl", "Laura Feiersinger", "Sarah Zadrazil")
> jahr <- c(1995, 1991, 1991, 1993, 1993, 1993)
> monat <- c(10, 2, 1, 2, 4, 2)
> tag <- c(19, 6, 4, 27, 5, 19)

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!

> # Sortiere name nach jahr aufsteigend


> name[order(jahr)]
[1] "Carina Wenninger" "Viktoria Schnaderbeck" "Katharina Schiechtl"
[4] "Laura Feiersinger" "Sarah Zadrazil" "Manuela Zinsberger"

Carina Wenninger hat das niedrigste Geburtsjahr und wäre somit die Älteste. Nun
sortieren wir die Namen der Spielerinnen in umgekehrter Reihenfolge.

> # Sortiere name nach jahr absteigend


> name[order(jahr, decreasing = TRUE)]
[1] "Manuela Zinsberger" "Katharina Schiechtl" "Laura Feiersinger"
[4] "Sarah Zadrazil" "Carina Wenninger" "Viktoria Schnaderbeck"

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 ;-)

> # Sortiere name nach dem Geburtsdatum absteigend


> reihenfolge <- order(jahr, monat, tag, decreasing = TRUE)
> name[reihenfolge]
[1] "Manuela Zinsberger" "Laura Feiersinger" "Katharina Schiechtl"
[4] "Sarah Zadrazil" "Carina Wenninger" "Viktoria Schnaderbeck"

Wir überzeugen uns in Tab. 6.3, dass unsere Idee wirklich gut war.

Tabelle 6.3: Namen und Geburtsdaten einiger österreichischer Fußballspielerinnen


nach Alter sortiert

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.

6.2.6 Mehrfachsortierung und Ränge – rank()

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.

Trick: Auf- und absteigende Mehrfachsortierung und Ränge

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.

Machen wir uns ans Werk!

> geburtszahl <- jahr * 10000 + monat * 100 + tag


> geburtszahl
[1] 19951019 19910206 19910104 19930227 19930405 19930219

> # Bestimme den Altersrang


> rank(-geburtszahl)
[1] 1 5 6 3 2 4

Wir sehen, dass geburtszahl tatsächlich das Geburtsdatum ordentlich abbildet. Je


höher die Zahl, desto jünger die Spielerin. Drehen wir die Vorzeichen um, so gilt das
Gegenteil. Natürlich könnten wir diesen Trick auch bei order() verwenden. Dort
hat uns die flexible Parametrisierung jedoch noch den Griff in die Trickkiste erspart.
Ein kurzer Check mit Hilfe von Tab. 6.4 zeigt: Es passt!

Tabelle 6.4: Namen und Geburtsdaten einiger österreichischer Fußballspielerinnen


und die dazu passenden Altersränge von 1 (jung) bis 6 (erfahren)

Name Geburtsdatum Rang


Manuela Zinsberger 19.10.1995 1
Carina Wenninger 06.02.1991 5
Viktoria Schnaderbeck 04.01.1991 6
Katharina Schiechtl 27.02.1993 3
Laura Feiersinger 05.04.1993 2
Sarah Zadrazil 19.02.1993 4
82 B Vektorfunktionen für Data Science und Statistik

6.3 Kombinatorik – factorial(), choose(), prod()

Mit der Funktion factorial() können wir Fakultäten à la n! berechnen und die
Funktion choose() ermöglicht uns die Bestimmung des Binomialkoeffizienten nk .


Beide Funktionen arbeiten vektorwertig (vgl. Seite 47).

> # 3! und 4! > # 3 über 2 und 4 über 2


> factorial(c(3, 4)) > choose(c(3, 4), 2)
[1] 6 24 [1] 3 6

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.

> # Produkt von c(1, 2, 3, 4) = 1 * 2 * 3 * 4


> prod(c(1, 2, 3, 4))
[1] 24

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.

6.4 Wissenschaftliche Notation – options(scipen)

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?

> # Anzahl der Möglichkeiten, 52 Karten auf 2 Spieler zu verteilen


> choose(52, 26)
[1] 4.959185e+14

Die Anzahl beträgt ungefähr 4.959185 · 1014 = 495918500000000. Ungefähr deshalb,


weil R die Zahl bei der Ausgabe unter Umständen abschneidet. Intern wird die Zahl
mit der maximalen Genauigkeit verwaltet.
6 Mengen, Sortieren und Kombinatorik 83

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 ;-)

> # Optionen setzen: Wissenschaftliche Notation unterdrücken


> opt <- options(scipen = 10000)
> choose(52, 26)
[1] 495918532948104

> # Zurücksetzen der Optionen


> options(opt)
> choose(52, 26)
[1] 4.959185e+14

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.

6.5 Aus der guten Praxis

6.5.1 Fallbeispiel: Unterscheidbare Lottoziehungsergebnisse

Beispiel: Wie viele unterscheidbare Lottoziehungsergebnisse gibt es in der öster-


reichischen Lotterie 6 aus 45 (siehe Infobox 1 auf Seite 53) bzw. in der deutschen
Lotterie 6 aus 49?4
Wir stellen eine Vorüberlegung für Lotto 6 aus 45 an, die wir anschließend ganz
leicht verallgemeinern können: Wirziehen zunächst 6 Zahlen, wobei es nicht auf die
Reihenfolge ankommt. Das sind 45 6 Kombinationen. Zu jeder dieser Kombinationen
gibt es 45 − 6 = 39 Zusatzzahlen.
Macht also zusammen 45

6 · 39 unterscheidbare Ziehungsergebnisse. Wir ersetzen

• 45 durch die Anzahl der Zahlen in der Lotterie,


• 6 durch die Anzahl der gezogenen Zahlen (exkl. Zusatzzahl/Superzahl) und
• 39 durch die Differenz der beiden anderen Zahlen.

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.

> # Die Lotteriedaten


> nzahlen <- c(45, 49) # Anzahl der Zahlen der Lotterien
> nziehung <- c(6, 6) # Anzahl der gezogenen Zahlen ohne Zusatzzahl

> # Bestimme die Anzahl der unterscheidbaren Lottoziehungen


> choose(nzahlen, nziehung) * (nzahlen - nziehung)
[1] 317657340 601304088

Wenn wir uns lediglich für die Anzahl der unterscheidbaren Lottosechser interessie-
ren, dann lassen wir die Zusatzzahl einfach weg.

> # Bestimme die Anzahl der möglichen Lottosechser


> choose(nzahlen, nziehung)
[1] 8145060 13983816

6.5.2 Fallbeispiel: Unterscheidbare Playlists einer Musik-CD

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.

> # Bestimme die Anzahl der möglichen Playlists


> 5 * 3 * 3 * 4 * 1 * 2 * factorial(6)
[1] 259200

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

> # Die Anzahl der Lieder pro Tanz


> nlieder <- c(5, 3, 3, 4, 1, 2)

> # Bestimme die Anzahl der möglichen Playlists


> nplaylists <- prod(nlieder) * factorial(length(nlieder))
> nplaylists
[1] 259200

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

6.6.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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-


nen wir das Produkt eines numerischen Vektors? (6.3)


• Was bedeuten die Outputs 2e+04 bzw. 2e-04? Wie können wir die wissen-
schaftliche Notation abschalten bzw. forcieren? (6.4)
86 B Vektorfunktionen für Data Science und Statistik

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

> tipp == lotto


Warnung in tipp == lotto
Länge des längeren Objektes
ist kein Vielfaches der Länge des kürzeren Objektes
[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE

a) Erkläre, warum dieser Lösungsversuch nicht funktioniert.


b) Welcher Operator eignet sich anstelle des "=="-Operators besser?

2. Ebenfalls in (6.1.1) hatten wir folgende Anweisung:

> # 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?

> tipp[tipp %in% lotto] # Zahlen vertauscht


[1] 7 19

> lotto[tipp %in% lotto] # Falsche Zahlen


[1] 19 23
6 Mengen, Sortieren und Kombinatorik 87

3. Gegeben sind die Mengen A = {2, 3, 4, 5, 7} und B = {1, 2, 6, 7} aus (6.1.2).

a) Bestimme die Differenzmenge A\B. Also jene Elemente, die in A aber


nicht in B vorkommen.

b) Bestimme die symmetrische Differenz A△B = (A\B) ∪ (B\A). Also jene


Elemente, die nur in A oder nur in B vorkommen.

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:

> a <- c(2, 1, 4, 3) # Seitenlänge a


> b <- c(4, 5, 2, 3) # Seitenlänge b
> area <- a * b # Fläche

> a > b > area


[1] 2 1 4 3 [1] 4 5 2 3 [1] 8 5 8 9

a) Sortiere den Vektor area absteigend.

b) Bestimme die Fläche jenes Rechtecks, das die zweitgrößte Seitenlänge a


hat. Folgender Ansatz existiert bereits:
> area[sort(a)][2]

i. Welches (nicht korrekte) Ergebnis wird nach Ausführung dieser Code-


zeile auf die R-Console gedruckt?
ii. Finde einen Code, der die Aufgabe korrekt löst.

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?

i. a[order(area[order(b)])] iii. a[order(b[order(area)])]

ii. a[order(area, b)] iv. a[order(b, area)]


88 B Vektorfunktionen für Data Science und Statistik

6. Gegeben ist beispielhaft der Vektor x:

> x <- c(2, 4, 1, 3)

a) Betrachte folgenden R-Code:


> x1 <- x
> x1[rank(x)] <- x

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. Sei X ∼ Binom(n, p) eine binomialverteilte Zufallsvariable mit den Parametern


n ∈ N und p ∈ (0, 1). Bestimme für n = 30 und p = 0.2 die komplette
Wahrscheinlichkeitsverteilung der Binomialverteilung, also P (X = k) für alle
k ∈ {0, 1, . . . , n}.  
n
P (X = k) = · pk · (1 − p)n−k
k
Und zwar
a) durch Auswertung der Formel.
b) mit Hilfe der Funktion dbinom().
7 Deskriptive Statistik 89

7 Deskriptive Statistik

Dort wollen wir in diesem Kapitel hin:

• die Welt der deskriptiven Statistik erobern

• den Umgang mit fehlenden Werten vertiefen


• Standardisierung, Messfehler und Robustheit von Maßen diskutieren

Was schätzt du, wie viel ein Apfel im Mittel wiegt?


Wir gehen in diesem Kapitel unter anderem dieser spannenden Frage nach. In der
Datei "Apfel.RData" finden wir drei Varianten einer Stichprobe mit dem Gewicht
von Äpfeln in Gramm, die von einer Schar von wissbegierigen Studierenden erhoben
wurde. In (5) haben wir besprochen, wie wir die Daten in R laden können.

> # Ggf. Arbeitsverzeichnis wechseln


> # setwd(...)
> objekte <- load("Apfel.RData")
> objekte
[1] "apfel" "apfel.teil" "apfel.voll"

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:

• Wie viel wiegt der leichteste bzw. schwerste Apfel? (7.2)


• Sind alle Messungen plausibel? Wie markieren wir etwaige Messfehler? (7.2)
• Wie viel wiegt ein Apfel im Mittel? Wie stark streut das Gewicht? (7.3)
• Wie groß ist das mediane Gewicht eines Apfels? Wie sieht es mit den Quartilen
aus? (7.4)

Darüber hinaus diskutieren wir in (7.6.1), welchen Einfluss Ausreißer haben. In


(7.6.2) schauen wir uns das in der Statistik wichtige Konzept der Standardisierung
an und in (7.7.2) bekommst du einen klitzekleinen Vorgeschmack auf Grafiken.

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

7.1 Funktionsübersicht und fehlende Werte ausschließen – na.rm

Bevor wir loslegen, geben wir in Tab. 7.1 eine Übersicht über gängige Funktionen
der deskriptiven Statistik.

Tabelle 7.1: Wichtige 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.

7.2 Minimum und Maximum – min(), max(), range()

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

> # Minimum > # Maximum > # Minimum und Maximum


> min(apfel.teil) > max(apfel.teil) > range(apfel.teil)
[1] 19 [1] 199 [1] 19 199

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

Sortieren wir einmal die Elemente.

> # Sortiere die Stichprobe aufsteigend


> sort(apfel.teil)
[1] 19 119 134 142 143 149 150 155 165 198 199

Es handelt sich höchst wahrscheinlich um einen Messfehler oder Eingabefehler. Viel-


leicht sind es in Wahrheit 190 g, 119 g, 199 g oder 91 g. Da wir das nicht wissen, ist
es sinnvoll, diesen Wert als fehlenden Wert (NA: Not Available) zu deklarieren.

> # 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.

> range(apfel, na.rm = TRUE) # Fehlende Werte entfernen


[1] 119 199

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.

> # Spannweite des Vektors apfel


> apfel.range <- range(apfel, na.rm = TRUE)
> apfel.range[2] - apfel.range[1]
[1] 80

Es liegen also 80 Gramm zwischen dem schwersten und dem leichtesten Apfel.
92 B Vektorfunktionen für Data Science und Statistik

7.3 Mittelwert, Varianz und Standardabweichung – mean(),


var(), sd()

Mit Hilfe der Funktionen mean(), var() und sd() berechnen wir den Mittelwert x̄,
die Varianz s2 und die Standardabweichung s unserer Apfelstichprobe.

> # Mittelwert und Varianz > # Standardabweichung


> mean(apfel, na.rm = TRUE) > sd(apfel, na.rm = TRUE)
[1] 155.4 [1] 25.80784
> var(apfel, na.rm = TRUE) > sqrt(var(apfel, na.rm = TRUE))
[1] 666.0444 [1] 25.80784

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.

7.4 Median und Quantile – median(), quantile()

Kommen wir zu den allseits beliebten Quantilen, die wir in Infobox 3 auffrischen.

Infobox 3: Quantile, Median und Quartile

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

Den Median können wir mit der Funktion median() berechnen.

> 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().

quantile(x, probs = seq(0, 1, 0.25), na.rm = FALSE,


names = TRUE, type = 7, ...)

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!

> # Bestimme die defaultmäßig eingestellten Quantile


> quantile(apfel, na.rm = TRUE)
0% 25% 50% 75% 100%
119.00 142.25 149.50 162.50 199.00
> # Bestimme das 2.5%- und 97.5%-Quantil
> quantile(apfel, probs = c(0.025, 0.975), na.rm = TRUE)
2.5% 97.5%
122.375 198.775

Bemerkung: Normalerweise beschriftet R wegen names = TRUE die Elemente des


Ergebnisvektors – im Output oben grün markiert.
Beispiel: Berechne die Interquartilsdistanz IQR (Inter Quartile Range) von apfel.
Das ist die Differenz zwischen dem 75 %-Quantil und dem 25 %-Quantil.

> temp <- quantile(apfel, probs = c(0.25, 0.75), na.rm = TRUE)


> temp
25% 75%
142.25 162.50

> # 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

7.5 Summarys – summary()

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.

7.6 Aus der guten Statistikpraxis

7.6.1 Robustheit und Ausreißer

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

Frage: Welche Auswirkungen haben Ausreißer auf die besprochenen Maßzahlen?

> # Maßzahlen OHNE Ausreißer > # Maßzahlen MIT Ausreißer


> min(apfel, na.rm = TRUE) > min(apfel.teil)
[1] 119 [1] 19
> mean(apfel, na.rm = TRUE) > mean(apfel.teil)
[1] 155.4 [1] 143
> sd(apfel, na.rm = TRUE) > sd(apfel.teil)
[1] 25.80784 [1] 47.8623
> median(apfel, na.rm = TRUE) > median(apfel.teil)
[1] 149.5 [1] 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.

Regel 8: Achte immer auf Ausreißer!

Ausreißer können erheblichen Einfluss auf deskriptive Maßzahlen haben. Über-


prüfe Ausreißer stets auf Plausibilität!
7 Deskriptive Statistik 95

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

> # Mittelwert und Standardabweichung berechnen und speichern


> xquer <- mean(apfel.teil)
> s <- sd(apfel.teil)

> # Variante 1: Intervall xquer +/- 2 * Standardabweichung


> c(xquer - 2 * s, xquer + 2 * s)
[1] 47.2754 238.7246

> # Anzahl der Extremwerte bestimmen


> sum(apfel.teil < xquer - 2 * s | apfel.teil > xquer + 2 * s)
[1] 1

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

> # Anzahl der Extremwerte bestimmen


> sum(abs(apfel.teil.scale) > 2)
[1] 1

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

0 50 100 150 200 250

Abbildung 7.1: Visualisierung der Apfelstichprobe, apfel.teil oben und apfel


unten. Der Mittelwert ist jeweils rot eingezeichnet. Die grünen bzw.
blauen Striche sind je eine bzw. zwei Standardabweichungen vom
jeweiligen Mittelwert entfernt.

Bemerkung: Die Standardisierung ist in vielen statistischen Verfahren von großer


Bedeutung. Als Beispiel sei die Clusteranalyse genannt.
Abschließend wollen wir kontrollieren, ob der standardisierte Vektor wirklich Mit-
telwert 0 und Standardabweichung 1 hat.

> mean(apfel.teil.scale) > sd(apfel.teil.scale)


[1] -1.734723e-17 [1] 1

Der Mittelwert des standardisierten Vektors ist rundungsbedingt nicht ganz exakt
0. Über die wissenschaftliche Notation haben wir in (6.4) gesprochen.

7.7 Abschluss

7.7.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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).

a) Bestimme eine Summary für groesse. Sind alle Werte plausibel?


b) Berechne den BMI für alle Studierenden.
c) Ist der mittlere BMI größer als der mediane BMI?
d) Ab einem BMI von 25 gilt eine Person als übergewichtig. Wie viele Stu-
dierende sind übergewichtig?
e) Liegt der BMI einer Person im Intervall [18.5, 25), so gilt sie als normal-
gewichtig. Bestimme den Anteil der normalgewichtigen Studierenden.

f) Standardisiere den BMI.

7. In einer anderen Stichprobe wurde nach der Körpergröße in cm gefragt:

> groesse <- c(164, 1.83, 176, 480, 0, 167, 1.62)

Leider haben einige ihre Größe nicht in cm, sondern in m angegeben. Andere
haben offensichtlich unplausible Werte angegeben.

a) Rechne all jene Größenangaben, die offensichtlich in m gemacht wurden,


automatisiert in cm um.
b) Markiere etwaig verbleibende unplausible Werte mit NA.
c) Selektiere mit Hilfe der Funktion is.na() alle gültigen Elemente aus
groesse, also jene Elemente ungleich NA.

Dein Code soll auch für andere ähnlich geartete Vektoren funktionieren!
8 Kumulieren und Parallelisieren 99

8 Kumulieren und Parallelisieren

Dort wollen wir in diesem Kapitel hin:

• die Magie von kumulierenden Funktionen spüren

• parallele Berechnungen vornehmen


• erste vektorwertige Fallunterscheidungen durchführen
• Zusammenhänge studieren
• weitere Einblicke in die Programmierästhetik sammeln

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 Schlagzahlen der beiden Spieler nach 6 Bahnen


> schlaege1 <- c(2, 3, 2, 4, 7, 3)
> schlaege2 <- c(1, 3, 3, 6, 4, 2)

Die beiden Spieler (wie auch wir) fragen sich unter anderem:

• Nach welchen Bahnen liegt Spieler 1 in Führung? (8.1.1)

• 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)

Kumulierende Funktionen eröffnen uns viele geniale Anwendungsmöglichkeiten! Mit


dem Geburtstagsproblem und der Gewinnrangermittlung beim Joker schauen wir
uns zwei davon in (8.5.2) und (8.5.1) 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_8
100 B Vektorfunktionen für Data Science und Statistik

8.1 Kumulierende Funktionen

Wir betrachten in den folgenden Unterabschnitten vier interessante kumulierende


Funktionen, nämlich kumulierte Summen in (8.1.1), kumulierte Produkte in (8.1.2)
sowie kumulierte Maxima und Minima in (8.1.3). Auf geht’s ;-)

8.1.1 Kumulierte Summen – cumsum()

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.

> sum(schlaege1) # Gesamtschlagzahl für Spieler 1


[1] 21
> sum(schlaege2) # Gesamtschlagzahl für Spieler 2
[1] 19

> # Liegt Spieler 1 in Führung?


> sum(schlaege1) <= sum(schlaege2)
[1] FALSE

> # Differenz der Gesamtschlagzahlen beider Spieler


> sum(schlaege1) - sum(schlaege2)
[1] 2

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!

> schlaege1 > schlaege2


[1] 2 3 2 4 7 3 [1] 1 3 3 6 4 2
> cumsum(schlaege1) > cumsum(schlaege2)
[1] 2 5 7 11 18 21 [1] 1 4 7 13 17 19

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.

> cumsum(schlaege1) - cumsum(schlaege2)


[1] 1 1 0 -2 1 2

> # Nach welchen Bahnen war Spieler 1 in Führung?


> which(cumsum(schlaege1) - cumsum(schlaege2) <= 0)
[1] 3 4

> # Wie viele Runden lang war Spieler 1 in Führung?


> sum(cumsum(schlaege1) - cumsum(schlaege2) <= 0)
[1] 2
102 B Vektorfunktionen für Data Science und Statistik

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.

8.1.2 Kumulierte Produkte – cumprod()

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!

8.1.3 Kumulierte Minima und Maxima – cummin(), cummax()

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.

> schlaege1 > schlaege2


[1] 2 3 2 4 7 3 [1] 1 3 3 6 4 2
> # Kumuliertes Maximum für Spieler 1 > # Kumuliertes Maximum für Spieler 2
> cummax(schlaege1) > cummax(schlaege2)
[1] 2 3 3 4 7 7 [1] 1 3 3 6 6 6

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

8.2 Parallele Funktionen

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.

8.2.1 Parallele Minima und Maxima – pmin(), pmax()

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.

> min(schlaege1, schlaege2)


[1] 1

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

> # Bestimme für jede Bahn die kleinere Schlagzahl


> pmin(schlaege1, schlaege2)
[1] 1 3 2 4 4 2

pmin() retourniert also einen Vektor, der an jeder Stelle das jeweils kleinste Element
der Vektoren schlaege1 und schlaege2 enthält.

pmin(..., na.rm = FALSE)

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.

> # Sortiere die Bahnnummern nach Schwierigkeit


> order(schlaege1 + schlaege2, pmax(schlaege1, schlaege2))
[1] 1 3 6 2 4 5
104 B Vektorfunktionen für Data Science und Statistik

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!

8.2.2 Vektorwertige binäre Fallunterscheidungen – ifelse()

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.

ifelse(test, yes, no)

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

> # Bestimme das parallele Minimum


> ifelse(test = schlaege1 < schlaege2, yes = schlaege1, no = schlaege2)
[1] 1 3 2 4 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.

> ifelse(test = schlaege1 == schlaege2, yes = 0, no =


+ ifelse(test = schlaege1 < schlaege2, yes = 1, no = 2))
[1] 2 0 1 1 2 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

0 schlaege1 < schlaege2

TRUE FALSE

1 2

Abbildung 8.2: Visualisierung der Funktion ifelse() beim Minigolfbeispiel

8.3 Differenzieren – diff()

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.

Anschaulich in Tab. 8.3 dargestellt für die Schlagzahlen von Spieler 1.

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

Abbildung 8.3: Benachbarte Elemente vergleichen (links) und paarweise Differen-


zen zweier benachbarter Elemente (rechts)
106 B Vektorfunktionen für Data Science und Statistik

Mit der Funktion diff() können wir einen Vektor differenzieren und somit
elegant die 2. Variante umsetzen.

> # Differenziere den Vektor der Schlagzahlen


> diff(schlaege1)
[1] 1 -1 2 3 -4

> # Zähle, wie oft die Differenzen <= 0 sind


> diff(schlaege1) <= 0
[1] FALSE TRUE FALSE FALSE TRUE
> sum(diff(schlaege1) <= 0)
[1] 2

Gerne darfst du dieselbe Berechnung auch für Spieler 2 vornehmen!


Die Differenzenbildung ist das Gegenstück zur Bildung von kumulierten Summen.

> schlaege1
[1] 2 3 2 4 7 3

> cumsum(schlaege1) > diff(cumsum(schlaege1))


[1] 2 5 7 11 18 21 [1] 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.

8.4 Kovarianz und Korrelation – cov(), cor()

Wir untersuchen, ob es einen Zusammenhang zwischen schlaege1 und schlaege2


gibt. Unsere Intuition erwartet einen positiven Zusammenhang, da wir annehmen,
dass schwierige Bahnen für beide Spieler schwierig sind.
Die Funktionen cov() und cor() ermöglichen uns diese Studie. Sie ermitteln die
Kovarianz sowie Korrelation; standardmäßig nach Pearson.5

> # Gibt es einen positiven Zusammenhang zwischen den Schlagzahlen?


> cov(schlaege1, schlaege2) / (sd(schlaege1) * sd(schlaege2))
[1] 0.5275705
> cor(schlaege1, schlaege2)
[1] 0.5275705

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

8.5 Aus der guten Programmierpraxis

8.5.1 Fallbeispiel: Gewinnrangermittlung beim Joker

Wir präsentieren eine spannende Einsatzmöglichkeit für die Funktion cummin()!

Infobox 5: Jokerziehung

Bei der Jokerziehung der österreichischen Lotterien werden mit Zurücklegen 6


Zahlen von 0 bis 9 gezogen. Für den Gewinnrang werden von hinten beginnend
die Übereinstimmungen mit unserem Tipp bis zur ersten Abweichung gezählt.
Beispiele:

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 Ziehung und der Tipp


> jokerziehung <- c(4, 3, 9, 1, 0, 3)
> jokertipp <- c(4, 3, 9, 0, 0, 3)

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.

> jokertipp == jokerziehung


[1] TRUE TRUE TRUE FALSE TRUE TRUE
> rev(jokertipp == jokerziehung)
[1] TRUE TRUE FALSE TRUE TRUE TRUE
> cummin(rev(jokertipp == jokerziehung))
[1] 1 1 0 0 0 0
> sum(cummin(rev(jokertipp == jokerziehung)))
[1] 2

Zunächst prüfen wir komponentenweise auf Übereinstimmung. Jetzt wird es span-


nend: Wir machen uns die Typumwandlung beim Rechnen mit logischen Werten
zunutze. cummin() gibt so lange 1 zurück, bis die Funktion auf das erste FALSE
stößt. Dieses FALSE wird in 0 konvertiert. Ab diesem Moment folgen lauter Nullen.
Da cummin() mit dem Kumulieren von vorne beginnt, wir aber von hinten beginnen
müssen, drehen wir davor noch den Vektor mit rev() um. Der Rest ist Formsache
(sum()).
108 B Vektorfunktionen für Data Science und Statistik

8.5.2 Fallbeispiel: Das Geburtstagsproblem

Nähere Informationen zum Geburtstagsproblem bzw. Geburtstagsparadoxon findest


du zum Beispiel unter http://de.wikipedia.org/wiki/Geburtstagsparadoxon.
Frage: Wie viele (für uns unbekannte) Leute müssen wir befragen, damit die Wahr-
scheinlichkeit, dass mindestens zwei von ihnen am selben Tag Geburtstag haben,
erstmals die 50%-Marke überschreitet?
Lösung: Sei An = {mind. 2 von n Leuten haben am selben Tag Geburtstag} das
gesuchte Ereignis. Dann folgt nach simpler Wahrscheinlichkeitsrechnung:

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:

> n <- 30 # Berechnung für n = 1, 2, ..., 30

> # 1-zu-1-Umsetzung der obigen Formel


> pk <- (365 - 0:(n - 1)) / 365
> res <- 1 - cumprod(pk)

> round(res, digits = 3) # Runden, um es übersichtlicher zu machen


[1] 0.000 0.003 0.008 0.016 0.027 0.040 0.056 0.074 0.095 0.117 0.141 0.167
[13] 0.194 0.223 0.253 0.284 0.315 0.347 0.379 0.411 0.444 0.476 0.507 0.538
[25] 0.569 0.598 0.627 0.654 0.681 0.706

> # Ersten Index finden, bei dem die Wahrscheinlichkeit 50% ueberschreitet.
> index <- which(res > 0.5)[1]
> index
[1] 23

> # Entsprechende Wahrscheinlichkeit ausgeben.


> res[index]
[1] 0.5072972

Schon bei 23 Personen übersteigt die Wahrscheinlichkeit erstmals die 50%-Marke.


8 Kumulieren und Parallelisieren 109

8.5.3 Programmierstil: Wiederholte Berechnungen vermeiden

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

> # Nach welchen Bahnen war Spieler 1 in Führung?


> which(fuehrung1)
[1] 3 4

> # Wie viele Runden lang war Spieler 1 in Führung?


> sum(fuehrung1)
[1] 2

Regel 9: Vermeide wiederholte Berechnungen!

Führe Berechnungen, deren Ergebnisse du häufig benötigst, ein Mal aus und
speichere das Resultat in einer Variablen ab.

8.6 Abschluss

8.6.1 Objekte sichern

> # Daten sichern


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> save(schlaege1, schlaege2, file = "Minigolf.RData")

8.6.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

Verwende zum Test beispielsweise folgenden Vektor:


> x <- c(2, 8, 5, 1)

> # Kumulierte Mittelwerte: Gewünschtes Ergebnis


> res
[1] 2 5 5 4

2. Sei x ein numerischer Vektor. Was tun folgende R-Codes? Finde für jeden Code
eine kürzere Schreibweise.

a) ifelse(x %% 2 != 0, FALSE, TRUE)


b) ifelse(x > 0, floor(x), ceiling(x))
c) x[-1] - x[-length(x)]
8 Kumulieren und Parallelisieren 111

3. In einer Schisaison wetteifern Wintersportlerinnen und Wintersportler um Welt-


cuppunkte. Die Punkteverteilung pro Rennen ist wie folgt:

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

a) Generiere den Vektor der Punkteverteilung, ohne sämtliche Zahlen einzeln


einzugeben. Probiere mehrere Möglichkeiten aus.
b) Berechne für alle k ∈ {1, 2, . . . , 30}: Welcher Anteil der insgesamt zu
vergebenden Punkte entfällt auf die ersten k Plätze?

Ein Rennläufer hat in einer Saison folgende Platzierungen erreicht:


6, 10, 26, 5, 7, 5, 14, 8

c) Wie viele Punkte hat der Sportler in Summe erreicht?


d) Nach dem wievielten Rennen hat er erstmals die 200-Punkte-Schranke
überschritten?
e) Wie oft konnte der Rennläufer seine Platzierung gegenüber dem vorheri-
gen Rennen verbessern?
f) Angenommen, es gibt zwei Streichresultate (die zwei schlechtesten Er-
gebnisse werden aus der Wertung genommen). Wie viele Punkte hat er
in diesem Fall in Summe erreicht?

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:

a) Alle Personen, die mindestens 300 Euro ausgegeben haben, bekommen


pauschal 50 Euro. Die anderen Personen bekommen nichts.
b) Alle Personen, die mindestens 300 Euro ausgegeben haben, bekommen
10% ihres Umsatzes. Die anderen Personen bekommen nichts.
c) Alle Personen bekommen 10% ihres Umsatzes, zumindest aber 20 Euro.
d) Alle Personen bekommen 10% ihres Umsatzes, mindestens jedoch 20 Euro
und höchstens 50 Euro.

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:

• -1, falls Spieler 1 mehr Schläge gebraucht hat als Spieler 2.


• 0, falls beide Spieler gleich viele Schläge gebraucht haben.

• 1, falls Spieler 1 weniger Schläge gebraucht hat als Spieler 2.

Schreibe einen Code, der diesen Vektor erzeugt und zwar

a) mit Hilfe der Funktion ifelse().


b) ohne Verwendung der Funktion ifelse().

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:

> x <- c(-1, 1, 2, 2, -2) # Gewünschter Output


> which(diff(x) >= 0) [1] 2 3

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

Bearbeite folgende Aufgaben für die Beispielkonstellation n = 12, p = 1/3 und


α = 0.05.
a) Die Verteilungsfunktion von X ist definiert als:
k  
X n
P (X ≤ k) = · pj · (1 − p)n−j
j=0
j

Werte P (X ≤ k) für alle k ∈ {0, 1, . . . , n} aus.


b) Bestimme den kleinsten Wert für k, sodass folgende Ungleichung erfüllt
ist:
n  
X n
P (X ≥ k) = · pj · (1 − p)n−j < α
j
j=k

Hinweis: (8.5.1) könnte inspirierend sein ;-)

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

9 Verteilungen und Zufallszahlen

Dort wollen wir in diesem Kapitel hin:

• Dichtefunktion, Verteilungsfunktion, Quantilsfunktion für einige wichtige Ver-


teilungen auswerten sowie Zufallszahlen aus ihnen ziehen

• Stichproben mit und ohne Zurücklegen ziehen


• einen Einblick in das Seeding erhaschen
• einen ersten Hypothesentest rechnen

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:

• Wie teilen wir R mit, ob wir die Dichtefunktion, Verteilungsfunktion oder


Quantilsfunktion auswerten wollen oder wir Zufallszahlen ziehen wollen? Wie
machen wir das speziell bei der Normalverteilung? (9.1), (9.2)

• Wie machen wir simulierte Ergebnisse reproduzierbar? (9.5)


• Wie ziehen wir Stichproben mit und ohne Zurücklegen? (9.6)

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

In R implementierte Verteilungen verfügen in der Regel über vier Funktionen: Je eine


für die Dichtefunktion, Verteilungsfunktion, Quantilsfunktion und eine Funktion,
welche Zufallszahlen aus der Verteilung zieht.

© 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

Infobox 6: Verteilungsfunktion und Quantilsfunktion

Für eine Zufallsvariable X bezeichnet die Funktion F (x) = P (X ≤ x) die


Verteilungsfunktion von X. Mit ihr werten wir die Wahrscheinlichkeit aus,
dass X einen Wert kleiner oder gleich x annimmt.

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

In Tab. 9.1 listen wir die vier Buchstabenkürzel auf.

Tabelle 9.1: Konventionelle Buchstabenkürzel für Verteilungen in R

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!

9.2 Normalverteilung – dnorm(), pnorm(), qnorm(), rnorm()

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:

• dnorm() : Dichtefunktion der Normalverteilung


• pnorm() : Verteilungsfunktion der Normalverteilung
• qnorm() : Quantilsfunktion der Normalverteilung
• rnorm() : generiert normalverteilte Zufallszahlen

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

Beispiel: Sei X ∼ N (µ = 7, σ 2 = 4) eine normalverteilte Zufallsvariable mit Er-


wartungswert 7 und Varianz 4. Berechne

1. f (5) und f (9) – also die Dichte an den Stellen 5 und 9


2. P (X ≤ 5) – also die Verteilungsfunktion an der Stelle 5
3. P (X > 5) = 1 − P (X ≤ 5)
4. F −1 ( α2 ) und F −1 (1 − α
2) – also die Quantilsfunktion an den Stellen α
2 und
1 − α2 für α = 0.05

> # Parameter festlegen


> mu <- 7
> sigma <- sqrt(4) # Achtung: sigma statt sigma^2 nehmen!

> # 1.) Dichten bestimmen - aus Symmetriegründen kommt dasselbe heraus.


> dnorm(c(5, 9), mean = mu, sd = sigma)
[1] 0.1209854 0.1209854

> # 2.) Verteilungsfunktion an der Stelle 5 auswerten


> pnorm(5, mean = mu, sd = sigma)
[1] 0.1586553

> # 3.) P(X > 5) = 1 - P(X <= 5)


> 1 - pnorm(5, mean = mu, sd = sigma)
[1] 0.8413447
> pnorm(5, mean = mu, sd = sigma, lower.tail = FALSE) # Alternative
[1] 0.8413447

Mit lower.tail steuern wir, ob wir P (X ≤ x) (TRUE) oder P (X > x) (FALSE)


berechnen wollen, wobei lower.tail = TRUE die Standardeinstellung ist.

> # 4.) Quantile bestimmen


> alpha <- 0.05
> quantil <- qnorm(c(alpha / 2, 1 - alpha / 2), mean = mu, sd = sigma)
> quantil
[1] 3.080072 10.919928

> # Probe - Es muss alpha/2 und 1 - alpha/2 herauskommen.


> pnorm(quantil, mean = mu, sd = sigma)
[1] 0.025 0.975

Beispiel: Generiere n = 6 Zufallszahlen aus N (µ = 100, σ 2 = 100).

> # Parameter einstellen


> n <- 6
> mu <- 100
> sigma <- sqrt(100)

> rnorm(n = n, mean = mu, sd = sigma)


[1] 94.39524 97.69823 115.58708 100.70508 101.29288 117.15065
116 B Vektorfunktionen für Data Science und Statistik

9.3 Verteilungen – ein Überblick

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.

Tabelle 9.2: Übersicht über einige wichtige Verteilungen in R

Verteilung R-Name Parameter


Beta beta shape1, shape2
Binomial binom size, prob
Cauchy cauchy location, scale
Chi-Quadrat chisq df
Exponential exp rate
F f df1, df2
Gamma gamma shape, rate
Geometrische geom p
Gleichverteilung unif min, max
Hypergeometrisch hyper m, n, k
Log-normal lnorm meanlog, sdlog
Logistisch logis location, scale
Negativ binomial nbinom size, prob
Normal norm mean, sd
Poisson pois pois, lambda
Student t t df
Weibull weibull shape, scale

9.4 Gleichverteilte Zufallszahlen – runif()

Mit runif() generieren wir Zufallszahlen aus einer stetigen Gleichverteilung.


Standardmäßig werden die Zufallszahlen aus dem Intervall [0, 1) gezogen. Wenn wir
ein anderes Intervall wünschen, stehen uns die Parameter min und max zur Verfü-
gung.

> # 6 Zufallszahlen aus einer Gleichverteilung auf [0, 1)


> runif(6)
[1] 0.67757064 0.57263340 0.10292468 0.89982497 0.24608773 0.04205953

> # 6 Zufallszahlen aus einer Gleichverteilung auf [-10, 10)


> runif(6, min = -10, max = 10)
[1] -3.441586 9.090073 7.790786 3.856068 2.810136 9.885396
9 Verteilungen und Zufallszahlen 117

9.5 Zufall reproduzieren: Seeding – set.seed(), RNGversion()

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.

> runif(3) # 3 Zufallszahlen > runif(3) # Und nocheinmal


[1] 0.1076894 0.4615334 0.9144012 [1] 0.7818036 0.1223051 0.4582620

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!

> set.seed(123) # Seed festlegen > set.seed(123) # Seed festlegen


> runif(3) # 3 Zufallszahlen > runif(3) # Und nocheinmal
[1] 0.2875775 0.7883051 0.4089769 [1] 0.2875775 0.7883051 0.4089769

Dieselben Zahlen! Die Möglichkeit, simulierte Ergebnisse rekonstruieren und nach-


vollziehen zu können, ist in der (wissenschaftlichen) Praxis von hoher Bedeutung!
Beachte: Zufallszahlengeneratoren sind nicht versionsunabhängig! Das heißt, in
künftigen R-Versionen können trotz set.seed() andere Zufallszahlen herauskom-
men. Wir empfehlen daher, vor dem set.seed()-Befehl folgendes zu schreiben:

> # Version des Zufallszahlengenerators einstellen


> RNGversion(vstr = "4.0.2")

RNGversion() setzt den Zufallszahlengenerator (engl. Random Number Generator)


auf jene Einstellungen, wie sie in einer bestimmten R-Version verwendet wurden. Für
vstr setzen wir den Namen jener R-Version ein, mit der wir die Zufallszahlen erstel-
len ("4.0.2" für R-Version 4.0.2). Damit kommen auch in künftigen R-Versionen
dieselben Zufallszahlen heraus.

Regel 10: Baue bei Simulationen set.seed() und RNGversion() ein!

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.

Unter ?Random erfährst du mehr über Seeding und Zufallszahlengeneratoren. Du


findest dort auch entsprechende Literaturhinweise.
118 B Vektorfunktionen für Data Science und Statistik

9.6 Stichproben ziehen – sample()

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:

sample(x, size, replace = FALSE, prob = NULL)

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.

> # Permutation der Zahlen 1:5


> sample(1:5) # size ist automatisch 5; replace ist defaultmäßig FALSE
[1] 5 2 1 4 3

> # Stichprobe von 1:5 (mit Zurücklegen)


> sample(1:5, replace = TRUE)
[1] 2 2 4 1 2

> # Stichprobe von 1:5 der Größe 10.


> sample(1:5, replace = TRUE, size = 10)
[1] 2 2 4 2 2 1 1 1 3 3

Hier müssen wir replace = TRUE setzen, sonst beschwert sich R:

> sample(1:5, size = 10)


Fehler in sample.int(length(x), size, replace, prob) :
kann keine Stichprobe gröer als die Grundgesamtheit nehmen
wenn ’replace = FALSE’

Betrachten wir weitere Beispiele.

> # 10 Würfe eines normalen 6-seitigen Würfels


> sample(1:6, size = 10, replace = TRUE)
[1] 1 4 1 6 4 3 4 6 4 5

> # 10 Würfe einer unfairen Münze: P(Kopf) = 2/3, P(Zahl) = 1/3


> sample(c("Kopf", "Zahl"), size = 10, replace = TRUE, prob = c(2/3, 1/3))
[1] "Kopf" "Kopf" "Kopf" "Kopf" "Kopf" "Zahl" "Kopf" "Kopf" "Zahl" "Kopf"

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

9.7 Aus der guten Statistikpraxis

9.7.1 Fallbeispiel: t-Test für das mittlere Gewicht von Äpfeln

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)

Infobox 7: Begriffe im Zusammenhang mit Hypothesentests

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


steht für density of f reedom) ausgewertet an der Stelle 1 − α2 .


120 B Vektorfunktionen für Data Science und Statistik

Es gibt drei Entscheidungsregeln, die immer zur selben Testentscheidung führen.


Wir können H0 in diesem (zweiseitigen) Testsetting verwerfen, wenn
(t)
1. |T | > Qdf =n−1 1 − α2 .


2. der Testwert c nicht im (1 − α)-Konfidenzintervall liegt.

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.

> # Ggf. Arbeitsverzeichnis wechseln mit setwd()


> load("Apfel.RData") # Datei Apfel.RData laden
> apfel.voll
[1] 104 176 119 133 203 186 224 132 163 178 172 228 120 127 161 134 121 148
[19] 180 116 155 161 102 135 143 160 169 131 180 166 162 133 112 140 177 119
[37] 200 142 156 208 165 150 137 106 199 200 142 143 198 125 149 162 175

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)).

> # Hilfsgrößen definieren


> c <- 160 # Der Testwert
> n <- sum(!is.na(apfel.voll)) # Stichprobengröße
> xquer <- mean(apfel.voll, na.rm = TRUE) # Mittelwert der Stichprobe
> s <- sd(apfel.voll, na.rm = TRUE) # Standardabweichung der Stichprobe

Der Code sum(!is.na(apfel.voll)) ermittelt die Anzahl der gültigen Beobachtun-


gen. Da apfel.voll keine fehlenden Werte enthält, könnten wir stattdessen auch
length(apfel.voll) schreiben. In (14) besprechen wir is.na() genau!
1. Entscheidungsregel: Wir berechnen mit Hilfe von Formel (9.1) die Teststatistik
und vergleichen sie mit dem Quantil der t-Verteilung mit n − 1 Freiheitsgraden
ausgewertet an der Stelle 1 − α/2.
In Tab. 9.2 auf Seite 116 erfahren wir, dass die t-Verteilung den Namen t trägt und
wir die Freiheitsgrade mit df einstellen können. Dank (9.1) wissen wir, dass wir den
Prefix q anhängen müssen, um das Quantil zu bestimmen.

> # Teststatistik berechnen > # Quantil der t-Verteilung


> T <- (xquer - c) / s * sqrt(n) > qt(1 - alpha / 2, df = n - 1)
> T [1] 2.006647
[1] -1.11957
9 Verteilungen und Zufallszahlen 121

> # 1.) Ist |T| größer als das Quantil?


> abs(T) > qt(1 - alpha / 2, df = n - 1)
[1] FALSE

Wir können H0 also nicht verwerfen.


2. Entscheidungsregel: Mit Formel (9.2) berechnen wir das Konfidenzintervall
und erfragen dann, ob der Testwert c = 160 nicht in diesem Intervall liegt.

> # Konfidenzintervall berechnen


> conf.int <- xquer + c(-1, 1) * qt(1 - alpha / 2, df = n - 1) * s / sqrt(n)
> conf.int
[1] 146.6705 163.7823
> # 2.) Liegt der Testwert c nicht in diesem Intervall?
> c < conf.int[1] | c > conf.int[2]
[1] FALSE

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:

P (X ≤ −|T | oder X ≥ |T |) = 2 · P (X ≥ |T |) = 2 · (1 − P (X ≤ |T |))

Dabei ist X eine t-verteilte Zufallsvariable mit n − 1 Freiheitsgraden. Da wir die


Verteilungsfunktion auswerten wollen, brauchen wir den Prefix p.

> # p-Wert berechnen


> p.value <- 2 * (1 - pt(abs(T), df = n - 1))
> p.value
[1] 0.2680419
> # 3.) Ist der p-Wert < alpha?
> p.value < alpha
[1] FALSE

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.

> t.test(apfel.voll, mu = c, conf.level = 1 - alpha)


One Sample t-test
data: apfel.voll
t = -1.1196, df = 52, p-value = 0.268
alternative hypothesis: true mean is not equal to 160
95 percent confidence interval:
146.6705 163.7823
sample estimates:
mean of x
155.2264
122 B Vektorfunktionen für Data Science und Statistik

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

9.8.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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.

1. Sei X ∼ exp(λ). Die Verteilungsfunktion von X ist gegeben durch:


(
1 − e−λ·x falls x ≥ 0
P (X ≤ x) =
0 sonst
 
ln(2)
Bestimme für mehrere λ (mind. 3) deiner Wahl P X ≤ λ und zwar

a) mit der passenden Funktion aus Tab. 9.2 auf Seite 116.
b) ohne die Funktion aus 1a).

Fällt dir etwas auf?

2. Ein Kollege möchte 6 normalverteilte Zufallszahlen mit Erwartungswert 10


und Varianz 4 erzeugen und ist verzweifelt, weil sein Code nicht das korrekte
Ergebnis liefert. Helfe ihm, indem du seinen Code korrigierst!

> dnorm(6, 10, 4)

3. Seeding ist für die Reproduzierbarkeit sehr wichtig. Ein Kollege schlägt folgen-
de Idee für die Bestimmung des Seeds vor:

> seed <- round(runif(1, min = -2^31 + 1, max = 2^31 - 1))


> set.seed(seed)

> # Diverse Berechnungen

Warum sind Ergebnisse, die auf dieser Idee beruhen, nicht reproduzierbar?

4. Wir verwenden für dieses Beispiel herkömmliche 6-seitige Spielwürfel.

a) Bestimme n = 10000 Augensummen von jeweils zwei Würfeln.

b) Wie oft wurde die Augensumme 7 gewürfelt?


c) Wie oft wurde die Augensumme 2 oder 12 gewürfelt?

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

6. Gegeben ist ein numerischer Vektor x = (x1 , x2 , ..., xn )t .

Die Min-Max-Transformation des Vektors x sieht wie folgt aus:

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.

Mit welcher Verteilungsfunktion könnten wir die beiden Transformationen (eben-


so) durchführen?
C Wichtige Hilfsmittel

Daniel Obszelka
126 C Wichtige Hilfsmittel

10 Texte, Zeichenketten und Strings

Dort wollen wir in diesem Kapitel hin:

• Zeichenketten (Strings) offiziell willkommen heißen


• erste Erfahrungen mit Stringfunktionen sammeln

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:

"x1 = 1.5 und x2 = 1"

Und wenn wir schon dabei sind, möchten wir vielleicht auch die dazu gehörige Pa-
rameterkonstellation darstellen:

"a = 2, b = -5, c = 3"

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).

10.1 Zeichenketten erstellen – ""

Texte (auch Zeichenketten bzw. Strings genannt) sind in R vom Typ character. R
erkennt bei der Eingabe Strings an den Anführungszeichen.

> namen <- c("Walter", "Alex", "Julia")


> namen
[1] "Walter" "Alex" "Julia"

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.

> namen <- c(Walter, Alex, Julia)


Fehler: Objekt ’Walter’ nicht gefunden
© 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_10
10 Texte, Zeichenketten und Strings 127

10.2 Zeichenketten sortieren – sort(), order(), rank()

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"

> # Alphabetische Sortierung > # Absteigende Sortierung


> sort(namen) > sort(namen, decreasing = TRUE)
[1] "Alex" "Julia" "Walter" [1] "Walter" "Julia" "Alex"

> # 1. Name im Alphabet > # Letzter Name im Alphabet


> min(namen) > max(namen)
[1] "Alex" [1] "Walter"

Das klingt unspektakulär. Prickelnd wird das Ganze aber, wenn wir Zahlen nach der
Größe sortieren wollen, diese aber als Text gegeben sind.

> # Zahlen als Zahlen > # Zahlen als Text


> # Sortierung nach Größe > # Alphabetische Sortierung
> sort(c(12, 9, 314)) > sort(c("12", "9", "314"))
[1] 9 12 314 [1] "12" "314" "9"

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.

10.3 Zeichenketten verknüpfen – paste(), paste0()

Mit der Funktion paste() können wir Zeichenketten verknüpfen.

paste(..., sep = " ", collapse = NULL)

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".

> paste("x", 1:2) # Erster Versuch


[1] "x 1" "x 2"

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!

> paste("x", 1:2, sep = "")


[1] "x1" "x2"

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.

> paste("x", 1:2, sep = "", collapse = " und ")


[1] "x1 und x2"

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:

"x1 = 1.5 und x2 = 1"

> # Variablen basteln > # Ergebnisse basteln


> var <- paste("x", 1:2, sep = "") > res <- c(x1, x2)
> var > res
[1] "x1" "x2" [1] 1.5 1.0

> # var und res geeignet zusammenbauen


> paste(var, res, sep = " = ", collapse = " und ")
[1] "x1 = 1.5 und x2 = 1"

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

[1] "x1 = 1.5 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.

paste0(..., collapse = NULL)

> paste0("x", 1:2) # sep = ""


[1] "x1" "x2"

10.4 Buchstaben und Case sensitivity – letters und LETTERS

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:

> letters # Alle Kleinbuchstaben


[1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r"
[19] "s" "t" "u" "v" "w" "x" "y" "z"

> # Der wievielte Buchstabe im Alphabet ist c?


> which(letters == "c")
[1] 3

> # 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.

> LETTERS # Alle Großbuchstaben


[1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R"
[19] "S" "T" "U" "V" "W" "X" "Y" "Z"

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.

> "A" == "a" > "A" == "A"


[1] FALSE [1] TRUE

10.5 Abschluss

10.5.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Woran erkennt R bei der Eingabe einen String? (10.1)


• Wie sortieren wir einen Zeichenkettenvektor alphabetisch? (10.2)
• Mit welcher Funktion verknüpfen wir Strings? Was stellen wir mit den Pa-
rametern sep und collapse der Funktion paste() ein? Warum müssen wir
beide Parameter beim Funktionsaufruf exakt benennen? Worin besteht der
Unterschied zwischen paste() und paste0()? (10.3)
• Mit welchen vordefinierten Variablen greifen wir auf alle Klein- bzw. Groß-
buchstaben zu? (10.4)

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:

> z1 <- c(3, 8, 4, 1) > z2 <- c(5, 9, 2, 5)


> z1 > z2
[1] 3 8 4 1 [1] 5 9 2 5

a) Erzeuge einen String der folgenden Form:

[1] [1] "3 + 5 = 8" "8 + 9 = 17" "4 + 2 = 6" "1 + 5 = 6"

b) Erzeuge einen String der folgenden Form:


[1] "3 + 5 = 8 und 8 + 9 = 17 und 4 + 2 = 6 und 1 + 5 = 6"

2. In (6) ab Seite 70 haben wir Lotto gespielt. Der abgegebene Tipp und das
Ziehungsergebnis haben gelautet:

> tipp <- c(4, 7, 15, 19, 20, 38)


> lotto <- c(3, 19, 24, 23, 7, 34, 16)

Die letzte Zahl 16 ist die Zusatzzahl.

a) Erstelle einen String, der uns in Satzform mitteilt, ob unsere getippten


Zahlen gezogen wurden. Das Resultat könnte etwa so aussehen:
[1] "Die Zahl 4 wurde nicht gezogen."
[2] "Die Zahl 7 wurde gezogen."
[3] "Die Zahl 15 wurde nicht gezogen."
[4] ...

b) Ästhetikfanatiker bemängeln, dass die Zahlen nicht bündig angeordnet


sind. Modifiziere deinen Code aus 2a), sodass er die ein- bis zweistelligen
Zahlen rechtsbündig anordnet. Das Resultat könnte etwa so aussehen:
[1] "Die Zahl 4 wurde nicht gezogen."
[2] "Die Zahl 7 wurde gezogen."
[3] "Die Zahl 15 wurde nicht gezogen."
[4] ...

Hinweis: Führe eine vektorwertige Fallunterscheidung durch.


c) Erweitere deinen Code derart, dass auch angegeben wird, ob die gezogene
Zahl die Zusatzzahl ist.

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

11 Einfache Datentypen, Modes und Typumwandlung

Dort wollen wir in diesem Kapitel hin:

• die drei Modes (logical, numeric, character) in ihrer Hierarchie studieren


• den Mode abfragen und Typumwandlungen vornehmen

Was meinst du: Ist 12 kleiner als "9"? Nein (FALSE) sagst du? Fragen wir einmal R:

> 12 < "9"


[1] TRUE

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.

11.1 Einfache Datentypen und ihre Hierarchie

Vektoren können nur einen einfachen Datentyp (oder Mode, wie es in R heißt) auf-
weisen1 . Folgende Modes kennen wir bereits:

• logical – TRUE und FALSE


• numeric – alle Zahlen
• character – Zeichenketten, Text

Frage: Was passiert, wenn wir in der Funktion c() gemischte Modes verwenden?
Um diese Frage zu beantworten, schauen wir uns folgende Hierarchie an:

logical ⊆ numeric ⊆ character

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.

Werden gemischte Typen verwendet, so konvertiert R alle Einträge implizit in jenen


Mode, mit dem sie alle ohne Informationsverlust dargestellt werden können.
1 Es gibt Datenstrukturen, die auch unterschiedliche Modes speichern 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_11
11 Einfache Datentypen, Modes und Typumwandlung 133

11.2 Mode abfragen – mode(), is.datentyp ()

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

> mode(x) # Abfrage des Modes


[1] "numeric"
> is.numeric(x) # Abfrage auf numeric
[1] TRUE
> mode(x) == "numeric" # Alternative Abfrage auf numeric
[1] TRUE

TRUE wird also in 1 konvertiert, FALSE in 0.


Bemerkung: is.numeric(x) und mode(x) == "numeric" sind nicht ganz iden-
tisch. Wir kommen beim Thema Faktoren in (24.1.3) darauf zu sprechen.
Frage: Was passiert, wenn wir Strings mit anderen Modes mischen?

> # 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"

> mode(x) # Abfrage des Modes


[1] "character"
> is.character(x) # Abfrage auf character
[1] TRUE
> mode(x) == "character" # Alternative Abfrage auf character
[1] TRUE

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

11.3 Implizite Typumwandlung

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.

> TRUE + FALSE > 3 * TRUE > sum(c(TRUE, FALSE))


[1] 1 [1] 3 [1] 1

Zeichenketten vertragen sich nicht mit Rechenoperatoren und Rechenfunktionen.


Auch dann nicht, wenn der Text ausschließlich Zahlen beinhaltet.

> "2" + "3"


Fehler in "2" + "3" : nicht-numerisches Argument für binären Operator
> sum(c("2", "3"))
Fehler in sum(c("2", "3")) : ungültiger ’type’ (character) des Argumentes

Verknüpfungsoperatoren wie etwa "|" machen nur für logische Vektoren Sinn.
Ein numerischer Vektor x wird zu einem logischen konvertiert (mittels x != 0).

> 1976 | 1977 # intern: (1976 != 0) | (1977 != 0)


[1] TRUE
> 2 & 3 # intern: (2 != 0) & (3 != 0)
[1] TRUE

Bei Zeichenketten wird wiederum eine Fehlermeldung ausgegeben.

> "FALSE" | TRUE


Fehler in "FALSE" | TRUE :
Operationen sind nur für numerische, logische oder komplexe Typen möglich

Wie verhält es sich bei Vergleichsoperatoren? Schauen wir uns dazu folgenden
interessanten Fall an:

> 12 < 9 > "12" < 9


[1] FALSE [1] TRUE

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

11.4 Explizite Typumwandlung – as.datentyp ()

Mit den Funktionen as.logical(), as.numeric() und sowie as.character() er-


zwingen wir Umwandlungen in den jeweiligen Datentyp.
Eine typische Anwendung ist die Umwandlung von Zahlen, die als Strings gegeben
sind. Zum Beispiel, nachdem wir die Zahlen aus einem Text extrahiert haben. Die
entsprechenden Techniken lernen wir in den (22) und (23).

> text <- c("5", "2", "6", "3")


> text
[1] "5" "2" "6" "3"
> mean(text) # Kann keinen Mittelwert berechnen, da character.
Warnung in mean.default(text)
Argument ist weder numerisch noch boolesch: gebe NA zurück
[1] NA

> # Explizite Umwandlung in Mode numeric


> zahlen <- as.numeric(text)
> zahlen
[1] 5 2 6 3
> mean(zahlen) # Jetzt funktioniert der Mittelwert, da numeric.
[1] 4

Während Konvertierungen in übergeordnete Modes kein Problem darstellen, treten


umgekehrt unter Umständen NA-Werte (fehlende Werte) auf.
Frage: Was passiert, wenn wir einen Vektor vom Mode character umwandeln?

> x <- c("A", "1/5", 1, TRUE)


> x
[1] "A" "1/5" "1" "TRUE"

> as.numeric(x) # "A", "1/5" und "TRUE" nicht (direkt) konvertierbar


Warnung in eval(ei, envir) NAs durch Umwandlung erzeugt
[1] NA NA 1 NA
> as.logical(x) # "A", "1/5" und "1" nicht (direkt) konvertierbar
[1] NA NA NA TRUE

Frage: Was passiert, wenn wir einen Vektor vom Mode numeric in einen Vektor
vom Mode logical umwandeln?

> y <- -1:2


> y
[1] -1 0 1 2

> as.logical(y) > y != 0 # in diesem Fall äquivalent


[1] TRUE FALSE TRUE TRUE [1] TRUE FALSE TRUE TRUE
136 C Wichtige Hilfsmittel

11.5 Komplexe Zahlen – complex, Re(), Im(), NaN

Wir wollen die quadratische Gleichung

a · x2 + b · x + c = 0

für die Parameterkonstellation a = 0.5, b = −3, c = 6.5 lösen.

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 ;-)

> # Koeffizienten definieren


> a <- 0.5
> b <- -3
> c <- 6.5

> # Eingabe der komplexwertigen Lösungen


> x <- c(3 - 2i, 3 + 2i)
> x
[1] 3-2i 3+2i

> # 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.

> # Realteile von x > # Imaginärteile von x


> Re(x) > Im(x)
[1] 3 3 [1] -2 2

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

11.6.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Welche Hierarchie herrscht zwischen den Datentypen logical, numeric und


character? (11.1)

• 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!

TRUE == 1 1 == "TRUE" TRUE & "1"


TRUE == 2 1 == "1" 0 & 2
TRUE <= 2 TRUE + "TRUE" == 2 0 | 2
TRUE == "TRUE" TRUE + 1 == 2 "FALSE" | TRUE
TRUE == "1" TRUE + 1 == "2" (0 & 1) == FALSE
138 C Wichtige Hilfsmittel

2. Wir wollen aus dem folgenden Vektor x all jene Elemente selektieren, die dem
Wert 2 oder 4 gleichen:

> x <- c(1, 2, 3, 5)

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)]

Leider ist auch dieser Ansatz nicht korrekt.

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.

3. Was kommt bei folgenden Codezeilen heraus (und warum)?

a) as.logical(c(c(TRUE, 0), "FALSE"))


b) a <- 4
b <- 2
"b" < letters[a]

c) as.logical(paste(c("T", "UE"), collapse = "R"))

4. Gegeben sind die beiden komplexen Zahlen x1 = 3 + 2i und x2 = 3 − 2i.


Überprüfe mit einer Abfrage, ob der Imaginärteil von x1 · x2 gleich 0 ist. Das
Ergebnis soll dabei entweder ein TRUE oder ein FALSE sein.
5. Wir betrachten nochmal die quadratische Gleichung
a · x2 + b · x + c = 0
aus (11.5). Modifiziere den Code zur Bestimmung der Lösung derart, dass
• die reellwertige Lösung bestimmt wird, wenn es eine gibt und
• andernfalls die komplexwertige Lösung berechnet wird.
Hinweis: Eine Fallunterscheidung an der richtigen Stelle hat schon so manche
Probleme gelöst ;-)
In (28) lernen wir if-Anweisungen kennen, mit der wir das Beispiel noch einen
Tick eleganter lösen können.
12 Beschriftungen, Names und Häufigkeitstabellen 139

12 Beschriftungen, Names und Häufigkeitstabellen

Dort wollen wir in diesem Kapitel hin:

• die Möglichkeiten von Beschriftungen erleben

• einfache (lückenlose) Häufigkeitstabellen erstellen


• die Tücken dieser Möglichkeiten kennenlernen und damit umgehen lernen

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

> # Die Anzahl der Geschwister


> ngeschwister <- c(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.

Tabelle 12.1: Lückenhafte Häufigkeitstabelle für die Anzahl der Geschwister

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:

• Wie viele Geschwister haben die Studierenden im Mittel? (12.2.2)


• Wie viele Studierende haben mindestens 3 Geschwister? (12.2.3)

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

12.1 Beschriftungen bei Vektoren

12.1.1 Zugriff auf Beschriftungen – names()

Wir haben in (7.4) gesehen, dass quantile() standardmäßig Beschriftungen hinzu-


fügt. Mit der Funktion names() greifen wir auf diese Beschriftungen zu.

> # Quantile berechnen


> ngeschwister.quant <- quantile(ngeschwister)
> ngeschwister.quant
0% 25% 50% 75% 100%
0.00 0.00 1.00 2.75 6.00

> # Die Beschriftungen des Vektors erfragen


> names(ngeschwister.quant)
[1] "0%" "25%" "50%" "75%" "100%"

Jedes Element von ngeschwister.quant ist also beschriftet.

12.1.2 Subsetting mit Names

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.

> # Variante 1: Indizes abzählen > # Variante 2: Sicherer!


> ngeschwister.quant[c(2, 4)] > ngeschwister.quant[c("25%", "75%")]
25% 75% 25% 75%
0.00 2.75 0.00 2.75

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?

> # Berechne andere Quantile


> ngeschwister.quant <- quantile(ngeschwister, probs = c(0.25, 0.5, 0.75))
> ngeschwister.quant
25% 50% 75%
0.00 1.00 2.75

> # Diverse Berechnungen


> # ...
12 Beschriftungen, Names und Häufigkeitstabellen 141

> # Variante 1: Indizes abzählen > # Variante 2: Funktioniert!


> ngeschwister.quant[c(2, 4)] > ngeschwister.quant[c("25%", "75%")]
50% <NA> 25% 75%
1 NA 0.00 2.75

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!

> # Berechne erneut andere Quantile


> ngeschwister.quant <- quantile(ngeschwister, probs = seq(0, 1, by = 0.1))
> ngeschwister.quant
0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%
0.0 0.0 0.0 0.1 0.8 1.0 1.2 1.9 3.8 5.3 6.0

> # Variante 1: Indizes abzählen > # Variante 2: Abgesichert!


> ngeschwister.quant[c(2, 4)] > ngeschwister.quant[c("25%", "75%")]
10% 30% <NA> <NA>
0.0 0.1 NA NA

Im linken Code würden wir im Glauben, mit ngeschwister.quant[c(2, 4)] das


25%- und 75%-Quantil zu selektieren, für die Interquartilsdistanz ein falsches Er-
gebnis von 0.1 - 0.0 = 0.1 bekommen. Im rechten Code sind wir dank NA vor einem
falschen Ergebnis geschützt.
Das funktioniert aber nur sicher, solange die Beschriftungen eindeutig sind! Exis-
tiert eine Beschriftung mehrfach, so wird nur das erste Element selektiert. Wir schau-
en uns das kurz in (12.1.3) an.

Regel 11: Bevorzuge beim Subsetting Names gegenüber Indizes!

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!

12.1.3 Übergabe von Beschriftungen in c()

Wir können schon direkt in der Funktion c() Beschriftungen übergeben:

> ngeschwister.range <- range(ngeschwister)


> ngeschwister.range
[1] 0 6
142 C Wichtige Hilfsmittel

> c(Min = ngeschwister.range[1], Max = ngeschwister.range[2])


Min Max
0 6

Wenn wir mehrelementige Vektoren übergeben, nummeriert R entsprechend durch:

> c(x = 1:3, y = 1:5)


x1 x2 x3 y1 y2 y3 y4 y5
1 2 3 1 2 3 4 5

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:

> # Vergebe nicht eindeutige Beschriftungen


> x <- c(a = 2, a = 3)
> x
a a
2 3

> # Selektion bei nicht eindeutiger Beschriftung


> x[c("a", "a")]
a a
2 2

Der zweite Eintrag mit der Beschriftung "a" (3) wird nicht selektiert. Wir beherzigen
also immer Regel 11 und wählen eindeutige Beschriftungen.

12.1.4 Beschriftungen hinzufügen, ersetzen und löschen – names()<-, NULL

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

> # Füge Beschriftungen hinzu


> names(ngeschwister.range) <- c("Min", "Max")
> ngeschwister.range
Min Max
0 6

Wunderbar! Wir können auch nur einen Teil der Beschriftung ändern.
12 Beschriftungen, Names und Häufigkeitstabellen 143

> # Beschriftung des ersten Eintrags ändern


> names(ngeschwister.range)[1] <- "Minimum"
> ngeschwister.range
Minimum Max
0 6

Mit folgender Syntax können wir Beschriftungen löschen:


names(vektor ) <- NULL

> # Lösche Beschriftungen


> names(ngeschwister.range) <- NULL
> ngeschwister.range
[1] 0 6

12.2 Häufigkeitstabellen und Tücken von Names

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

1. eine Häufigkeitstabelle erstellen (12.2.1),


2. mit ihrer Hilfe die mittlere Anzahl der Geschwister bestimmen (12.2.2),
3. bestimmte Häufigkeiten selektieren (12.2.3) und

4. eine lückenlose Häufigkeitstabelle erstellen. (12.2.4)


Das erste Ziel erreichen wir ganz schnell. Die anderen drei Ziele beschäftigen uns
dann ein wenig länger. Gehen wir es an ;-)

12.2.1 Häufigkeitstabellen erstellen – table()

Einfache Häufigkeitstabellen erstellen wir mit der Funktion table().

> ngeschwister
[1] 0 1 0 1 6 5 2 0

> # Häufigkeitstabelle erstellen


> tab.geschwister <- table(ngeschwister)
> tab.geschwister
ngeschwister
0 1 2 5 6
3 2 1 1 1

Bei tab.geschwister handelt es sich im Prinzip um einen beschrifteten Vektor, wie


wir ihn schon in (12.1.1) bei quantile() kennengelernt haben.
144 C Wichtige Hilfsmittel

12.2.2 Gewichtete Mittelwerte berechnen aus Häufigkeitstabellen

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

Oder auch nicht. Wo liegt der Fehler?


Beachte: Names sind immer vom Mode character!

> names(tab.geschwister) > mode(names(tab.geschwister))


[1] "0" "1" "2" "5" "6" [1] "character"

Jetzt gibt es ja noch die Typumwandlung. Können wir da etwas machen?

> # Versuche den Mode der Names zu ändern


> as.numeric(names(tab.geschwister))
[1] 0 1 2 5 6
> mode(as.numeric(names(tab.geschwister)))
[1] "numeric"

> names(tab.geschwister) <- as.numeric(names(tab.geschwister))


> mode(names(tab.geschwister))
[1] "character"

Nein: Names sind immer vom Mode character!

Wir müssen also names(tab.geschwister) immer wieder erneut in numeric kon-


vertieren, bevor wir mit den Beschriftungen rechnen können.

> # Gewichtetes Mittel berechnen - So funktioniert es!


> sum(as.numeric(names(tab.geschwister)) * tab.geschwister) /
+ sum(tab.geschwister)
[1] 1.875

> # Kontrolle
> mean(ngeschwister)
[1] 1.875

Jetzt hat alles prima geklappt :-)


12 Beschriftungen, Names und Häufigkeitstabellen 145

Ü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.

> # Gewichtetes Mittel berechnen - Alternative


> weighted.mean(as.numeric(names(tab.geschwister)), w = tab.geschwister)
[1] 1.875

12.2.3 Häufigkeiten selektieren aus Häufigkeitstabellen

Kommen wir zu einer weiteren Tücke der Names.


Beispiel: Wie viele Studierende haben genau ein Geschwisterchen?

> tab.geschwister
0 1 2 5 6
3 2 1 1 1

> # Häufigkeit von einem Geschwisterchen selektieren - so nicht!


> tab.geschwister[1]
0
3

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:

> # Häufigkeit von einem Geschwisterchen selektieren - besser!


> tab.geschwister["1"]
1
2

Es haben also 2 Studierende genau ein Geschwisterchen.


Was tun wir, wenn wir aus der Häufigkeitstabelle einen Bereich selektieren wol-
len? Wie gut, dass wir schon über explizite Typumwandlung gesprochen haben!
Beispiel: Wie viele Studierende haben mindestens 3 Geschwister?

> # Selektiere Häufigkeiten mit Beschriftung 3 oder größer


> temp <- tab.geschwister[as.character(3:max(ngeschwister))]
> temp
<NA> <NA> 5 6
1 1

Wir müssen also die gewünschten Zahlen in Zeichenketten konvertieren, ehe wir mit
sum() die Anzahlen addieren.
146 C Wichtige Hilfsmittel

> # Anzahl aus Häufigkeitstabelle > # Kontrolle


> sum(temp, na.rm = TRUE) > sum(ngeschwister >= 3)
[1] 2 [1] 2

Da niemand angegeben hat 3 oder 4 Geschwister zu haben, entstehen in temp NAs.


Das stört uns aber nicht weiter, na.rm kümmert sich liebevoll darum! Rechts zur
Kontrolle die Variante, die wir schon aus (3) gekannt haben.

12.2.4 Lückenlose Häufigkeitstabellen erstellen

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!

> # 1.) Initialisiere Nullvektor mit gewünschter Länge


> tab.geschwister.voll <- rep(0, max(ngeschwister) + 1)
> tab.geschwister.voll
[1] 0 0 0 0 0 0 0

> # 2.) Füge die gewünschten Beschriftungen zu


> names(tab.geschwister.voll) <- 0:max(ngeschwister)
> tab.geschwister.voll
0 1 2 3 4 5 6
0 0 0 0 0 0 0

Wir generieren einen Vektor tab.geschwister.voll mit max(ngeschwister) + 1


(= length(0:max(ngeschwister))) Nullen und weisen sodann die korrekten Be-
schriftungen (0:max(ngeschwister)) zu.

> # Selektiere jenen Teilbereich von tab.geschwister.voll ...


> names(tab.geschwister)
[1] "0" "1" "2" "5" "6"
> tab.geschwister.voll[names(tab.geschwister)]
0 1 2 5 6
0 0 0 0 0

> # ... 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

> # 3.) Werte ersetzen


> tab.geschwister.voll[names(tab.geschwister)] <- tab.geschwister
> tab.geschwister.voll
0 1 2 3 4 5 6
3 2 1 0 0 1 1

Die nichtselektierten Einträge von tab.geschwister.voll bleiben 0, womit der Sinn


unserer Initialisierung von tab.geschwister.voll als Nullvektor geklärt ist.
Wir fassen diesen genialen Trick zusammen.

Trick: Lückenlose Häufigkeitstabellen erstellen

Gegeben ist eine lückenhafte Häufigkeitstabelle tab.alt. Eine lückenlose Häu-


figkeitstabelle tab.neu erstellen wir mit den folgenden drei Schritten:
1. Initialisiere tab.neu als Nullvektor mit der gewünschten Länge der neuen
Häufigkeitstabelle.
tab.neu <- rep(0, gewünschteLänge )
2. Füge tab.neu die gewünschten Beschriftungen der neuen Häufigkeitsta-
belle hinzu.
names(tab.neu) <- gewünschteBeschriftungen
3. Ersetze die Werte (Häufigkeiten) in tab.neu mit den Werten (Häufigkei-
ten) aus tab.alt an jenen Stellen, die in tab.alt definiert sind.
tab.neu[names(tab.alt)] <- tab.alt

Bemerkung: Mit dem Wort Initialisierung bezeichnet man in der Programmierung


allgemein die Zuweisung von Anfangswerten auf ein Objekt.
In (24) lernen wir Faktoren kennen, die uns eine alternative Möglichkeit zur Erstel-
lung von lückenlosen Häufigkeitstabellen anbieten.

12.3 Abschluss

12.3.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

• Wie erstellen wir eine einfache Häufigkeitstabelle? (12.2.1)

• 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)

• Wie erstellen wir lückenlose Häufigkeitstabellen? (12.2.4)

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

1. Wir betrachten die Häufigkeitstabelle tab.geschwister aus (12.2):

> tab.geschwister
0 1 2 5 6
3 2 1 1 1

a) Welche der folgenden Codezeilen selektieren korrekt die Anzahlen jener


Studierenden, die zwischen 1 und 5 Geschwister haben?
tab.geschwister[tab.geschwister %in% 1:5]

tab.geschwister[names(tab.geschwister) %in% 1:5]

tab.geschwister[names(tab.geschwister) %in% as.character(1:5)]

tab.geschwister[1:5]

tab.geschwister[as.character(1:5)]

b) Folgender Code soll tab.geschwister absteigend nach der Anzahl der


Geschwister (hier also von 6 bis 0) sortieren:
> tab.geschwister[sort(names(tab.geschwister), decreasing = TRUE)]
6 5 2 1 0
1 1 1 2 3
12 Beschriftungen, Names und Häufigkeitstabellen 149

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.

c) Erstelle mit Hilfe von tab.geschwister eine Häufigkeitstabelle, welche


die Anzahl der Einzelkinder (0 Geschwister) und Geschwisterkinder
(mindestens ein Geschwisterchen) angibt. Beschrifte die Elemente der Ta-
belle mit den beiden genannten Begriffen.

2. Der Vektor alter enthält das Alter von drei Personen:

> alter <- c(Max = 31, Alex = 37, Christian = 26)


> alter
Max Alex Christian
31 37 26

a) Sortiere die Namen der Personen aufsteigend nach dem Alter.


b) Sortiere den Vektor alter alphabetisch nach den Namen.

3. Bei einer Vorlesungsprüfung wurden folgende Noten vergeben:

> noten <- c(3, 2, 4, 4, 2, 4, 5, 3) # Die einzelnen Noten


> tab.note <- table(noten) # Häufigkeitstabelle der Noten
> tab.note
noten
2 3 4 5
2 2 3 1

a) Bestimme ausschließlich mit Hilfe von tab.note den Anteil derjenigen,


die die Note 5 bekommen haben.
b) Erstelle eine lückenlose Häufigkeitstabelle, in der wir auch die Anzahl der
1er (leider 0 an der Zahl) korrekt ablesen können.
c) Welche Note(n) wurde(n) höchstens ein Mal vergeben?
d) Welche Note(n) wurde(n) am häufigsten vergeben?
150 C Wichtige Hilfsmittel

13 Computerarithmetik und Rundungsfehler

Dort wollen wir in diesem Kapitel hin:

• Einblicke in die Welt der Computerarithmetik bekommen

• mit Rundungsfehlern umgehen lernen

Was denkst du, was bei folgender Abfrage herauskommt:

> 2.05 - 0.05 == 2

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.

> mean(apfel.teil.scale) > sd(apfel.teil.scale)


[1] -1.734723e-17 [1] 1
> mean(apfel.teil.scale) == 0 > sd(apfel.teil.scale) == 1
[1] FALSE [1] TRUE

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.

13.1 Dezimalzahlen und Rundungsfehler – options(digits)

In R können (wie in anderen Programmiersprachen oder Skriptsprachen auch) schon


bei einfachsten Rechnungen teils grausame Rundungsfehler auftreten.

Beispiel: Prüfe nach, ob das Ergebnis der Rechnung 2.05 − 0.05 exakt 2 ergibt.

> 2.05 - 0.05 > 2.05 - 0.05 == 2


[1] 2 [1] FALSE

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.

> # Anzahl der angezeigten Nachkommastellen umstellen.


> opt <- options(digits = 20)
> 2.05 - 0.05 # Jetzt sehen wir den Rundungsfehler.
[1] 1.9999999999999998

> options(opt) # Optionen wieder zurücksetzen

© 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

Frage: Warum ist das so?

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

13.2 Prüfung auf annähernde Gleichheit

13.2.1 Absolute Abweichung messen

Wie gehen wir also mit derlei Rundungsfehlern um?


Ein Ausweg: Wir prüfen nicht auf exakte Gleichheit mit "==", sondern lassen eine
kleine (absolute) Abweichung zwischen dem Istwert und Sollwert zu. Dazu definieren
wir ein kleines ϵ > 0 und fragen auf annähernde Gleichheit ab:

|Istwert − Sollwert| < ϵ

Machen wir R mit unserer Idee vertraut!

> istwert <- 2.05 - 0.05


> sollwert <- 2

> # Prüfung auf exakte Gleichheit


> istwert == sollwert # Rundungsfehler, daher FALSE
[1] FALSE

> # Prüfung auf annähernde Gleichheit


> eps <- 10^-8 # Kleine Fehlerschranke
> abs(istwert - sollwert) < eps # Lassen kleine Abweichung zu: TRUE
[1] TRUE

Den Absolutbetrag dürfen wir dabei natürlich nicht vergessen ;-)


Selbstverständlich funktioniert diese Idee auch vektorwertig, das heißt wir können
für istwert und sollwert auch Vektoren der Länge größer 1 einsetzen.
152 C Wichtige Hilfsmittel

13.2.2 Funktionsbasierte Prüfung – all.equal()

Eine funktionsbasierte Alternative bietet uns die Funktion all.equal():

all.equal(target, current, ...)

## S3 method for class ’numeric’


all.equal(target, current,
tolerance = sqrt(.Machine$double.eps), scale = NULL,
..., check.attributes = TRUE)

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).

> all.equal(2, 2.05 - 0.05)


[1] TRUE

> # Vorgabe einer Toleranzschranke


> eps <- 10^-8
> all.equal(2, 2.05 - 0.05, tolerance = eps)
[1] TRUE

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.

> # Apfeldaten laden


> # ggf. Arbeitsverzeichnis wechseln mit setwd(...)
> load("Apfel.RData")
> apfel.teil
[1] 134 165 155 19 199 142 119 143 150 198 149

> # Standardisierten Vektor berechnen


> apfel.teil.scale <- (apfel.teil - mean(apfel.teil)) / sd(apfel.teil)
> 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

> # Baue die zu vergleichenden Objekte zusammen


> ist <- c(mean(apfel.teil.scale), sd(apfel.teil.scale))
> soll <- c(0, 1)

> all.equal(target = soll, current = ist)


[1] TRUE

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.

> names(ist) <- c("mean", "sd") # ist beschriften


> ist
mean sd
-1.734723e-17 1.000000e+00

> all.equal(soll, ist)


[1] "names for current but not for target"

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

> all.equal(soll, ist)


[1] "Names: 2 string mismatches"

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.

> all.equal(soll, ist, check.attributes = FALSE)


[1] TRUE

Wenn wir tolerance nicht spezifizieren, so setzt R für tolerance standardmäßig


die Zahl .Machine$double.epsˆ0.5 ein. Bei .Machine$double.eps handelt es sich
um die kleinste darstellbare positive Zahl x, sodass die Abfrage 1 + x != 1 ein TRUE
liefert. Von dieser Zahl wird die Wurzel gezogen, um nicht zu streng zu sein.
Bemerkung: .Machine enthält noch weitere interessante Informationen über die
numerische Charakteristik. Sehr gerne darfst du dich via ?.Machine einlesen!

13.3 Abschluss

13.3.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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?

> 5.375 - 0.375 == 5

2. Wir betrachten den Vektor x = (x1 , x2 , . . . , xn )t mit folgenden Elementen:


 √ k
k
xk = 2 ∀k ∈ {1, 2, . . . , n}

a) Berechne für n = 1000 den Vektor x in R.


b) Überprüfe mit Hilfe von all.equal(), ob alle Elemente von x dem Wert
2 annähernd gleichen.
c) Wie 2b), nun aber ohne die Funktion all.equal(). Verwende dabei idea-
lerweise denselben Toleranzwert, den all.equal() standardmäßig ver-
wendet.

Was schätzt du, was bei sum(x == 2) herauskommt?


14 Konstante 155

14 Konstante

Dort wollen wir in diesem Kapitel hin:

• Bedeutende R-Konstante kennenlernen und ihren Sinn erfahren

• Verstehen, wie wir mit diesen Konstanten umzugehen haben

Vielleicht hast du ja schon mal probiert, fehlende Werte mittels == NA abzufragen:

> x <- c(1:4, NA)


> x == NA
[1] NA NA NA NA NA

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:

• Wie erstellen und löschen wir Beschriftungen? (14.2)


• Wie viele Studierende sind nicht angetreten? (14.3.1)
• Wie viele Punkte haben jene Studierenden im Mittel erhalten, die zur Prüfung
angetreten sind? (14.3.2)
• Welche Möglichkeiten haben wir, die fehlenden Werte beim Sortieren oder in
Häufigkeitstabellen zu berücksichtigen? (14.3.3)
• Wie hoch ist der Punktedurchschnitt unter all jenen Studierenden, die min-
destens 20 Punkte geschafft haben? (14.3.4)

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

14.1 Eingebaute Konstante – Constants

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):

> LETTERS # Alle Großbuchstaben


[1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R"
[19] "S" "T" "U" "V" "W" "X" "Y" "Z"
> letters # Alle Kleinbuchstaben
[1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r"
[19] "s" "t" "u" "v" "w" "x" "y" "z"
> pi # Die Kreiszahl pi
[1] 3.141593

14.1.1 Konstante zurückgewinnen – rm(Konstante )

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 # Die Zahl pi, wie sie sein sollte.


[1] 3.141593

> # 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

14.1.2 TRUE und FALSE

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.

> T # T enthält die eingebaute Konstante TRUE.


[1] TRUE

> # Überschreibe T
> T <- FALSE
> T # (!!!)
[1] FALSE

> # Erstelle den Kartenstapel und mische die Karten


> karten <- 1:6
> stapel <- sample(karten)
> stapel
[1] 3 6 2 4 5 1

> # Verteile die Karten auf zwei Spieler


> bool.1 <- c(T, F)
> bool.1
[1] FALSE FALSE

> # Karten von Spieler 1 > # Karten von Spieler 2


> hand.spieler1 <- stapel[bool.1] > hand.spieler2 <- stapel[!bool.1]
> hand.spieler1 > hand.spieler2
integer(0) [1] 3 6 2 4 5 1

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

Im Gegensatz zu T und F sind TRUE und FALSE vor Überschreibungen geschützt.

> TRUE <- FALSE


Fehler in TRUE <- FALSE : ungültige (do_set) linke Seite in Zuweisung

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.

14.2 Das NULL-Objekt – NULL

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.

14.2.1 Beschriftungen löschen – names() <- NULL

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.

> # Momentane Beschriftung erfragen


> names(pkt)
NULL

Das Ergebnis ist NULL, das heißt, es existieren derzeit keine Beschriftungen. Fügen
wir jetzt Beschriftungen hinzu!

> # Beschriftungen zuweisen


> names(pkt) <- paste0("Nr", 1:length(pkt))
> pkt
Nr1 Nr2 Nr3 Nr4 Nr5 Nr6 Nr7 Nr8 Nr9 Nr10
11 38 NA 24 24 31 19 35 NA 22
14 Konstante 159

Wollen wir Beschriftungen löschen, so weisen wir den Names NULL zu.

> # Beschriftung löschen


> names(pkt) <- NULL
> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22

14.2.2 Abfrage auf NULL – is.null()

Wenn wir wissen wollen, ob Beschriftungen existieren, so kommen wohl viele zu-
nächst auf die folgende naheliegende Idee:

> names(pkt) != NULL


logical(0)

Klappt nicht: Es wird ein leerer logischer Vektor zurückgegeben. Mit der Funktion
is.null() funktioniert es hingegen!

> # Abfrage auf Null > # Existieren Beschriftungen?


> is.null(names(pkt)) > !is.null(names(pkt))
[1] TRUE [1] FALSE

Um die Frage, ob Beschriftungen existieren, korrekt zu beantworten, müssen wir die


Antwort von is.null(names(pkt)) negieren, was wir im rechten Code tun. Denn
es existiert eine Beschriftung genau dann, wenn die Beschriftung nicht NULL ist.

14.3 Fehlende Werte – NA

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

> # Bestimme die Differenz der Längen beider Vektoren


> length(pkt) - length(sort(pkt))
[1] 2

Irgendwie genial!
Im folgenden Unterabschnitt schauen wir uns die effizientere Variante an.
160 C Wichtige Hilfsmittel

14.3.1 Auf Gleichheit mit NA abfragen – is.na()

Um auf Gleichheit mit NA abzufragen, ist es verlockend, folgendes zu schreiben:

> 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.

> is.na(pkt) # TRUE, falls der Eintrag NA ist, FALSE sonst


[1] FALSE FALSE TRUE FALSE FALSE FALSE FALSE FALSE TRUE FALSE
> sum(is.na(pkt)) # so gehts!
[1] 2

Es haben also 2 Studierende bei der Prüfung gefehlt.


Bemerkung: NA ist nicht gleich "NA"!

> is.na("NA") > is.na(NA) > "NA" == NA


[1] FALSE [1] TRUE [1] NA

14.3.2 Berechnung von Maßzahlen, unbedingter Fall

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.

> # Elemente ungleich NA selektieren


> pkt.valid <- pkt[!is.na(pkt)]
> pkt.valid
[1] 11 38 24 24 31 19 35 22

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

Im Mittel haben die Studierenden also 25.5 Punkte erzielt.


14 Konstante 161

14.3.3 Umgang mit fehlenden Werten in Funktionen

Viele Funktionen haben eigene Parameter zum Umgang mit fehlenden Werten. Den
Parameter na.rm kennen wir schon aus (7).

> mean(pkt, na.rm = TRUE) # Fehlende Werte nicht berücksichtigen


[1] 25.5

In sort() finden wir den Parameter na.last, mit dem wir steuern, ob und wo wir
fehlende Werte anordnen wollen.

> sort(pkt, na.last = NA) # Fehlende Werte entfernen (Standard)


[1] 11 19 22 24 24 31 35 38
> sort(pkt, na.last = TRUE) # Fehlende Werte hinten anhängen
[1] 11 19 22 24 24 31 35 38 NA NA
> sort(pkt, na.last = FALSE) # Fehlende Werte vorne aufschreiben
[1] NA NA 11 19 22 24 24 31 35 38

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!

> # Punkte in eine Note umrechnen


> pkt
[1] 11 38 NA 24 24 31 19 35 NA 22
> note <- pmin(pmax(ceiling(8 - pkt / 40 * 8), 1), 5)
> note
[1] 5 1 NA 4 4 2 5 1 NA 4

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.

> # Nichts exkludieren > # NA wird exkludiert


> table(note, exclude = NULL) > table(note, exclude = NA)
note note
1 2 4 5 <NA> 1 2 4 5
2 1 3 2 2 2 1 3 2

Standardmäßig werden NA und NaN (siehe (14.5)) ausgeschlossen, was exclude =


c(NA, NaN) entspricht.
162 C Wichtige Hilfsmittel

Eine weitere Entdeckung ist useNA. Mit useNA können wir einstellen, wann NA an-
gezeigt werden soll:

• useNA = "no": NA wird nicht angezeigt (Standard).

• 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.

> # NAs immer anzeigen > # Wenn es NAs gibt, mitnehmen


> table(note, useNA = "always") > table(note, useNA = "ifany")
note note
1 2 4 5 <NA> 1 2 4 5 <NA>
2 1 3 2 2 2 1 3 2 2

14.3.4 Berechnung von Maßzahlen, bedingter Fall – subset()

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

> sum(pkt >= 20, na.rm = TRUE) # 1.)


[1] 6
> mean(pkt, na.rm = TRUE) # 2.)
[1] 25.5
> mean(pkt[pkt >= 20 & !is.na(pkt)]) # 3.) Möglichkeit 1
[1] 29

Die dritte Fragestellung ist so etwas fummelig zu beantworten. Zunächst selektie-


ren wir alle Tests, die mindestens 20 Punkte wert waren und abgegeben wurden.
Anschließend können wir den Mittelwert berechnen.
Natürlich können wir auch das Objekt pkt.valid aus (14.3.2) verwenden:

> pkt.valid <- pkt[!is.na(pkt)]


> mean(pkt.valid[pkt.valid >= 20]) # 3.) Möglichkeit 2
[1] 29
14 Konstante 163

Eine bequeme Alternative bietet uns die Funktion subset(), mit der wir (unter
anderem) Teilmengen aus Vektoren selektieren können.

subset(x, subset, ...)

Dem gleichnamigen Parameter subset können wir eine Bedingung übergeben, die
dann automatisch mit !is.na(pkt) mit der Und-Verknüpfung kombiniert wird.

> mean(subset(pkt, subset = pkt >= 20)) # 3.) Möglichkeit 3


[1] 29

Der Punkteschnitt aller abgegebenen Tests mit mindestens 20 Punkten beträgt also
29 Punkte.

14.3.5 Logische Werte und NA

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.

> # NA mit Und-Verknüpfung > # NA mit Oder-Verknüpfung


> TRUE & NA # NA > TRUE | NA # TRUE
[1] NA [1] TRUE
> FALSE & NA # FALSE > FALSE | NA # NA
[1] FALSE [1] NA

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.

Gleiche Überlegung für die Oder-Verknüpfung (rechte Spalte): Mindestens ein


Ausdruck muss TRUE sein. Bei TRUE | NA ist der Ausdruck definitiv TRUE. Hinge-
gen können wir bei FALSE | NA wegen des fehlenden Wertes nicht beurteilen, was
herauskommt, was NA zur Folge hat.
164 C Wichtige Hilfsmittel

14.4 Unendlichkeit – Inf, is.finite(), is.infinite()

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.

> 1 / 0 > -1 / 0 > 1 / Inf > -1 / Inf > Inf / Inf


[1] Inf [1] -Inf [1] 0 [1] 0 [1] NaN

Anhand eines Codebeispiels schauen wir uns die Abfragemöglichkeiten auf Un-
endlichkeit an.

> z <- c(-Inf, -7, 0, Inf)


> z
[1] -Inf -7 0 Inf

> # Abfrage auf Gleichheit mit Inf


> z == Inf
[1] FALSE FALSE FALSE TRUE

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.

> z %in% c(-Inf, Inf) > is.infinite(z)


[1] TRUE FALSE FALSE TRUE [1] TRUE FALSE FALSE TRUE
> abs(z) == Inf > !is.finite(z)
[1] TRUE FALSE FALSE TRUE [1] TRUE FALSE FALSE TRUE
14 Konstante 165

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.

> x <- c(NA, NaN, Inf, -Inf, 0)


> x
[1] NA NaN Inf -Inf 0

> # Studiere is.finite()


> is.finite(x)
[1] FALSE FALSE FALSE FALSE TRUE

> # Studiere !is.infinite()


> !is.infinite(x)
[1] TRUE TRUE FALSE FALSE TRUE

14.5 Not A Number – NaN, is.nan()

Die Konstante NaN (Not a Number) entsteht


√ bei undefinierten Rechenopera-
tionen, wie zum Beispiel 0/0, ∞ − ∞ oder −2.

> x <- c(0 / 0, 2 - Inf, Inf - Inf, 3 / -Inf, NA, sqrt(-2))


Warnung in sqrt(-2) NaNs wurden erzeugt
> x
[1] NaN -Inf NaN 0 NA NaN

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:

> # Abfrage auf Gleichheit mit NA oder NaN


> is.na(x) # TRUE, falls NA oder NaN
[1] TRUE FALSE TRUE FALSE TRUE TRUE

Möchten wir lediglich auf reine NA prüfen, böte sich folgende Möglichkeit an:

> # Explizite Abfrage auf Gleichheit mit NA


> is.na(x) & !is.nan(x) # TRUE, falls NA und nicht NaN
[1] FALSE FALSE FALSE FALSE TRUE FALSE
166 C Wichtige Hilfsmittel

14.6 Abschluss

14.6.1 Zusammenfassung der Konstanten

Die Konstanten NULL, NA, Inf und NaN wollen von uns besondere Zuwendung. Wir
fassen diese in Tab. 14.1 zusammen.

Tabelle 14.1: Bedeutende Konstante und Abfrage auf Gleichheit

Konstante Bedeutung Abfrage auf Gleichheit


NULL Das "Nichts"-Objekt is.null()
NA Not Available is.na() (liefert TRUE auch bei NaN)
Inf Infinity is.infinite(), is.finite(), "== Inf",
beachte die Unterschiede und Vorzeichen!
NaN Not a Number is.nan()

14.6.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

Dort wollen wir in diesem Kapitel hin:

• unsere zweite Datenstruktur (die Matrix) kennenlernen


• wichtige und nützliche Vektorkonzepte auf Matrizen hochziehen
• weitere Einblicke in die Programmierästhetik sammeln
• die Grenzen des momentan Machbaren ausloten

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:

> # Schlagzahlen des 1. Spielers > # Schlagzahlen des 2. Spielers


> schlaege1 > schlaege2
[1] 2 3 2 4 7 3 [1] 1 3 3 6 4 2

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.

> # Daten laden


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> objekte <- load("Minigolf_Matrizen.RData")
> objekte
[1] "schlaege1" "schlaege2" "schlaege3" "Schlaege"

Wir fragen uns unter anderem:

• Wie erstellen wir eine Matrix mit den Schlagzahlen? (15.1.2)


• Wie beschriften wir die Zeilen und Spalten dieser Matrix? (15.1.4)
• Wie selektieren wir bestimmte Zeilen oder Spalten dieser Matrix? (15.2.1)
• Wie oft hat Spieler 3 mehr Schläge benötigt als Spieler 2? (15.2.2)
• Wie viele Schläge haben die 3 Spieler in Summe benötigt? Welche Platzierung
haben die Spieler dabei erreicht? (15.3)
• Wie oft wurden bei jeder Bahn mehr als zwei Schläge benötigt? (15.4)
• Wie korrigieren wir bestimmte Schlagzahlen in der Matrix? (15.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_15
15 Matrizen 171

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.

15.1 Matrizen generieren und manipulieren

15.1.1 Vektoren zu Matrizen anordnen – matrix()

Mit der Funktion matrix() können wir eine Matrix erstellen. Die Funktion sieht
(vereinfacht) so aus:

matrix(data = NA, nrow = 1, ncol = 1, byrow = FALSE)

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

Die Elemente werden normalerweise spaltenweise (byrow = FALSE ist Standard)


in die Matrix eingefüllt. Alternativ können wir die Elemente auch zeilenweise in
die Matrix einfügen, indem wir byrow = TRUE setzen.

> # Möglichkeit 2: Elemente zeilenweise einfügen


> X <- matrix(data = c(3, 1, -1, 2, 0, 5), nrow = 2, ncol = 3, byrow = TRUE)
> 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

In Abb. 15.1 visualisieren wir die Einstellungen des Parameters byrow.


byrow = FALSE (Standard) byrow = TRUE

Abbildung 15.1: Visualisierung des Parameters byrow der Funktion matrix().


Links: byrow = FALSE (Standard). Rechts: byrow = TRUE.

15.1.2 Vektoren aneinanderhängen – rbind(), cbind()

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.

> x <- 1:4 > y <- 5:8 > z <- 9:12


> x > y > z
[1] 1 2 3 4 [1] 5 6 7 8 [1] 9 10 11 12

> # 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

Bemerkung: Normalerweise werden die Namen der zugrundeliegenden Objekte


mitgenommen. Mit Hilfe von deparse.level = 0 in cbind() bzw. rbind() kön-
nen wir die Mitnahme der Namen unterbinden, wenn wir wollen.
15 Matrizen 173

Gegebenenfalls werden die Vektoren recycelt. Ist kein vollständiges Recycling mög-
lich, so gibt R eine Warnmeldung aus.

> # kein vollständiges Recycling möglich


> rbind(1:4, 1:3)
Warnung in rbind(1:4, 1:3)
number of columns of result is not a multiple of vector length (arg 2)
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 1 2 3 1

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.

> # Erzeuge eine Matrix mit den Schlagzahlen


> Schlaege <- cbind(schlaege1, schlaege2)
> Schlaege
schlaege1 schlaege2
[1,] 2 1
[2,] 3 3
[3,] 2 3
[4,] 4 6
[5,] 7 4
[6,] 3 2

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.

> schlaege3 <- c(1, 2, 3, 7, 4, 1)

> # Füge einen dritten Spieler hinzu


> Schlaege <- cbind(Schlaege, schlaege3)
> 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
174 D Datenstrukturen

Wollen wir Matrizen kombinieren, so müssen unbedingt die Dimensionen der


beteiligten Matrizen passen (gleiche Anzahl an Spalten bei rbind() bzw. gleiche
Anzahl an Zeilen bei cbind()).

> A <- matrix(1:6, nrow = 3) > B <- matrix(1:6, ncol = 3)


> A > B
[,1] [,2] [,1] [,2] [,3]
[1,] 1 4 [1,] 1 3 5
[2,] 2 5 [2,] 2 4 6
[3,] 3 6

> # Klappt nicht, da die Anzahl der Zeilen nicht übereinstimmt.


> cbind(A, B)
Fehler in cbind(A, B) :
Anzahl der Zeilen der Matrizen muss übereinstimmen (siehe Argument 2)

15.1.3 Dimension von Matrizen – nrow(), ncol(), dim(), length()

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

> nrow(Schlaege) # Anzahl der Zeilen


[1] 6
> ncol(Schlaege) # Anzahl der Spalten
[1] 3
> dim(Schlaege) # 1. Eintrag: Anzahl Zeilen, 2. Eintrag: Anzahl Spalten
[1] 6 3
> length(Schlaege) # Anzahl der Elemente in der Matrix
[1] 18

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.

15.1.4 Beschriftungen bei Matrizen – colnames(), rownames()

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.

> # Der Ist-Zustand


> 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

> # Spalten- und Zeilennamen erstellen


> paste0("Spieler", 1:ncol(Schlaege))
[1] "Spieler1" "Spieler2" "Spieler3"
> paste0("Bahn", 1:nrow(Schlaege))
[1] "Bahn1" "Bahn2" "Bahn3" "Bahn4" "Bahn5" "Bahn6"

> # Spalten- und Zeilennamen zuweisen


> colnames(Schlaege) <- paste0("Spieler", 1:ncol(Schlaege))
> rownames(Schlaege) <- paste0("Bahn", 1:nrow(Schlaege))

> 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

Beachte, dass wir mit paste0() sowie 1:ncol(Schlaege) und 1:nrow(Schlaege)


diese Aufgabe wunderbar allgemein, also für eine beliebige Anzahl an Zeilen und
Spalten lösen können!
Bemerkung: Leerzeichen sollten wir bei Beschriftungen vermeiden.

15.1.5 Matrizen transponieren – t()

Mit der Funktion t() können wir eine Matrix transponieren.

> # Originale Matrix > # Transponierte Matrix


> X > t(X)
[,1] [,2] [,3] [,1] [,2]
[1,] 3 1 -1 [1,] 3 2
[2,] 2 0 5 [2,] 1 0
[3,] -1 5
176 D Datenstrukturen

15.1.6 Matrix oder Vektor? – is.matrix(), is.vector()

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

15.2 Selektion: Subsetting – "[ ]", "[ , , drop]"

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

> # Die ersten 3 Zeilen und ersten beiden Spalten selektieren


> Schlaege[1:3, 1:2]
Spieler1 Spieler2
Bahn1 2 1
Bahn2 3 3
Bahn3 2 3

Wollen wir ganze Zeilen (Spalten) selektieren, so lassen wir die Spaltenindizes
(Zeilenindizes) einfach leer. Wir betrachten einfache Codebeispiele.

> # Die ersten 3 Zeilen selektieren - lasse Spaltenindizes leer


> Schlaege[1:3, ]
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3

> # Die ersten beiden Spalten selektieren - lasse Zeilenindizes leer


> Schlaege[, 1:2]
Spieler1 Spieler2
Bahn1 2 1
Bahn2 3 3
Bahn3 2 3
Bahn4 4 6
Bahn5 7 4
Bahn6 3 2
178 D Datenstrukturen

> # Die ersten 3 Zeilen und letzten beiden Spalten selektieren


> Schlaege[1:3, (ncol(Schlaege) - 1):ncol(Schlaege)]
Spieler2 Spieler3
Bahn1 1 1
Bahn2 3 2
Bahn3 3 3

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.

> # Selektiere alle Spalten > # Selektiere die ersten 3 Zeilen


> # der ersten 3 Zeilen > # der Spalte "Spieler2"
> temp <- Schlaege[1, ] > temp <- Schlaege[1:3, "Spieler2"]
> temp > temp
Spieler1 Spieler2 Spieler3 Bahn1 Bahn2 Bahn3
2 1 1 1 3 3

> is.vector(temp) > is.vector(temp)


[1] TRUE [1] TRUE
> is.matrix(temp) > is.matrix(temp)
[1] FALSE [1] FALSE

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

> # Selektiere die ersten beiden Zeilen der Spalte "Spieler2"


> Schlaege[1:2, "Spieler2", drop = FALSE]
Spieler2
Bahn1 1
Bahn2 3

Gerne darfst du dich davon überzeugen, dass in beiden Codezeilen tatsächlich eine
Matrix herauskommt!

15.2.2 Selektion von Zeilen und Spalten mit logischen Abfragen

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?

> # Hat Spieler 3 mehr Schläge als Spieler 2 gebraucht?


> bool <- Schlaege[, "Spieler3"] > Schlaege[, "Spieler2"]
> bool
Bahn1 Bahn2 Bahn3 Bahn4 Bahn5 Bahn6
FALSE FALSE FALSE TRUE FALSE FALSE

> Schlaege[bool, , drop = FALSE] # Matrixstruktur bewahren zur Sicherheit


Spieler1 Spieler2 Spieler3
Bahn4 4 6 7

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

> # 2.) Auf wie vielen Bahnen?


> sum(bool)
[1] 1

Frage: Was passiert, wenn keine Zeile/Spalte die Bedingung erfüllt?


Beispiel: Selektiere alle Bahnen (also Zeilen), auf denen Spieler 1 mehr als 7 Schläge
benötigt hat. Auf wie vielen Bahnen war dies der Fall?

> # 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

> # Zeilen selektieren - drop = FALSE zur Sicherheit


> temp <- Schlaege[bool, , drop = FALSE]
> temp
Spieler1 Spieler2 Spieler3

> 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.

> # Möglichkeit 1 > # Möglichkeit 2


> nrow(temp) > sum(bool)
[1] 0 [1] 0

15.2.3 Elemente selektieren – "[ ]"

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.

> Schlaege > Schlaege >= 5


Spieler1 Spieler2 Spieler3 Spieler1 Spieler2 Spieler3
Bahn1 2 1 1 Bahn1 FALSE FALSE FALSE
Bahn2 3 3 2 Bahn2 FALSE FALSE FALSE
Bahn3 2 3 3 Bahn3 FALSE FALSE FALSE
Bahn4 4 6 7 Bahn4 FALSE TRUE TRUE
Bahn5 7 4 4 Bahn5 TRUE FALSE FALSE
Bahn6 3 2 1 Bahn6 FALSE FALSE FALSE

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:

> Schlaege[Schlaege >= 5]


[1] 7 6 7

15.2.4 Subsetting mit Matrizen

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?

> # Definiere Zeilen- und Spaltenindizes


> zeilen <- c(2, 5, 1)
> spalten <- c(1, 2, 3)

> Schlaege[zeilen, spalten]


Spieler1 Spieler2 Spieler3
Bahn2 3 3 2
Bahn5 7 4 4
Bahn1 2 1 1
15 Matrizen 181

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.

> # Baue Indexmatrix zusammen


> Indizes <- cbind(zeilen, spalten)
> Indizes
zeilen spalten
[1,] 2 1
[2,] 5 2
[3,] 1 3

> # Selektion mittels n x 2 - Matrix


> Schlaege[Indizes]
[1] 3 4 1

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.

> # Unhandliche Alternative


> c(Schlaege[2, 1], Schlaege[5, 2], Schlaege[1, 3])
[1] 3 4 1

15.3 Summen/Mittelwerte für Zeilen/Spalten – rowSums(),


colSums(), rowMeans(), colMeans()

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

> # Wende Vektorfunktionen auf Matrizen an


> mean(Schlaege) # Bildet den Mittelwert über alle Elemente der Matrix
[1] 3.222222
> sum(Schlaege) # Die Summe aller Elemente der Matrix
[1] 58
182 D Datenstrukturen

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.

> # Bestimme die Schlagsummen für jeden Spieler


> schlagsummen <- colSums(Schlaege)
> schlagsummen
Spieler1 Spieler2 Spieler3
21 19 18

Mit colSums() keine große Sache! Bei der Beantwortung der anderen Fragen haben
wir die Chance, das Thema Sortieren (siehe (6.2)) zu wiederholen :-)

> # Bestimme die Platzierungsreihenfolge


> names(schlagsummen)[order(schlagsummen)]
[1] "Spieler3" "Spieler2" "Spieler1"

> # Sortiere die Schlagsummen aufsteigend


> sort(schlagsummen)
Spieler3 Spieler2 Spieler1
18 19 21
> names(sort(schlagsummen))
[1] "Spieler3" "Spieler2" "Spieler1"

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

> # Bestimme die Platzierung für jeden Spieler


> rank(schlagsummen, ties.method = "min")
Spieler1 Spieler2 Spieler3
3 2 1

Bei rank() verwenden wir ties.method = "min", um im Falle eines Unentschiedens


jedem Spieler den besseren Rang zu ermöglichen.

Wollen wir alle Informationen in einem Objekt vereinen, können wir so vorgehen:

> # Alle Informationen vereinen


> Res <- rbind(Platzierung = rank(schlagsummen, ties.method = "min"),
+ Schlagsumme = schlagsummen)
> Res
Spieler1 Spieler2 Spieler3
Platzierung 3 2 1
Schlagsumme 21 19 18

> # Sortiere die Spalten aufsteigend nach schlagsumme


> Res[, order(schlagsummen)]
Spieler3 Spieler2 Spieler1
Platzierung 1 2 3
Schlagsumme 18 19 21

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.

> Temp <- rbind(Schlaege, Summe = schlagsummen)


> Temp
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
Summe 21 19 18

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.

> # Jetzt ohne die Schlagsummen


> which(Temp[-nrow(Temp), 3] < Temp[-nrow(Temp), 1])
Bahn1 Bahn2 Bahn5 Bahn6
1 2 5 6

> # Schont die Nerven


> which(Schlaege[, 3] < Schlaege[, 1])
Bahn1 Bahn2 Bahn5 Bahn6
1 2 5 6

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.

15.4 Rechnen mit Wahrheitswerten

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?

> Schlaege > Schlaege > 2


Spieler1 Spieler2 Spieler3 Spieler1 Spieler2 Spieler3
Bahn1 2 1 1 Bahn1 FALSE FALSE FALSE
Bahn2 3 3 2 Bahn2 TRUE TRUE FALSE
Bahn3 2 3 3 Bahn3 FALSE TRUE TRUE
Bahn4 4 6 7 Bahn4 TRUE TRUE TRUE
Bahn5 7 4 4 Bahn5 TRUE TRUE TRUE
Bahn6 3 2 1 Bahn6 TRUE FALSE FALSE

> # 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

15.5 Matrix vs. Vektor

15.5.1 Matrix in Vektor umwandeln – as.vector()

Die Umwandlung in einen Vektor erfolgt mittels as.vector(). Dabei werden


die Elemente der Matrix immer spaltenweise entnommen (analog zu byrow =
FALSE in der Funktion matrix(), vgl. (15.1.1)).
Möchten wir stattdessen die Elemente zeilenweise entnehmen, so transponieren
wir die Matrix davor mit der Funktion t(). Wir demonstrieren das kurz anhand der
Matrix X aus (15.1.1).

> X > t(X)


[,1] [,2] [,3] [,1] [,2]
[1,] 3 1 -1 [1,] 3 2
[2,] 2 0 5 [2,] 1 0
[3,] -1 5

> # Elemente von X > # Elemente von X


> # spaltenweise entnehmen > # zeilenweise entnehmen
> as.vector(X) > as.vector(t(X))
[1] 3 2 1 0 -1 5 [1] 3 1 -1 2 0 5

Die Elemente einer Matrix X zeilenweise zu entnehmen ist also dasselbe, wie die
Elemente der transponierten Matrix t(X) spaltenweise zu entnehmen.

15.5.2 Bedeutung von drop = FALSE beim Subsetting

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.

> # Selektion mit drop = FALSE


> temp <- Schlaege[Schlaege[, 1] == 7, , drop = FALSE]

> is.matrix(temp) # ja, ist eine Matrix


[1] TRUE
> dim(temp) # ist daher definiert
[1] 1 3

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

> # Selektion ohne drop = FALSE


> temp1 <- Schlaege[Schlaege[, 1] == 7, ]
> temp1
Spieler1 Spieler2 Spieler3
7 4 4

> is.matrix(temp1) # nein, keine Matrix


[1] FALSE
> dim(temp1) # dim ist für Vektoren nicht definiert, gibt NULL zurück
NULL
> is.vector(temp1) # ja, ist ein Vektor
[1] TRUE

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 ;-)

15.6 Ersetzen von Werten

15.6.1 Ersetzungen in der ganzen Matrix

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

> # Wie oft wurde 7 eingetragen?


> sum(Schlaege == 7)
[1] 2

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.

> bool7 <- Schlaege == 7

> # Zufallszahlen ziehen


> RNGversion("4.0.2") # Version des Zufallszahlengenerators setzen
> set.seed(12345) # Friert den Zufall ein
> temp.schlaege <- sample(8:11, size = sum(bool7), replace = TRUE)
> temp.schlaege
[1] 9 10

> # Ersetze alle 7er durch Zufallszahlen zwischen 8 und 11


> Schlaege[bool7] <- temp.schlaege
> Schlaege
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 3 3 2
Bahn3 2 3 3
Bahn4 4 6 10
Bahn5 9 4 4
Bahn6 3 2 1

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.

> # paralleles Minimum > # wird nicht überschrieben


> pmin(Schlaege, 7) > Schlaege
Spieler1 Spieler2 Spieler3 Spieler1 Spieler2 Spieler3
Bahn1 2 1 1 Bahn1 2 1 1
Bahn2 3 3 2 Bahn2 3 3 2
Bahn3 2 3 3 Bahn3 2 3 3
Bahn4 4 6 7 Bahn4 4 6 10
Bahn5 7 4 4 Bahn5 9 4 4
Bahn6 3 2 1 Bahn6 3 2 1

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

15.6.2 Ersetzungen von fehlenden Werten – NA, is.na()

Da mehr als 7 Schläge nicht möglich (sprich ungültig) sind, könnten wir Werte größer
als 7 durch NA ersetzen.

> # Werte größer 7 durch NA ersetzen


> Schlaege[Schlaege > 7] <- NA

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 ;-).

> Schlaege > is.na(Schlaege)


Spieler1 Spieler2 Spieler3 Spieler1 Spieler2 Spieler3
Bahn1 2 1 1 Bahn1 FALSE FALSE FALSE
Bahn2 3 3 2 Bahn2 FALSE FALSE FALSE
Bahn3 2 3 3 Bahn3 FALSE FALSE FALSE
Bahn4 4 6 NA Bahn4 FALSE FALSE TRUE
Bahn5 NA 4 4 Bahn5 TRUE FALSE FALSE
Bahn6 3 2 1 Bahn6 FALSE FALSE FALSE

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.

> # Ersetze fehlende Werte durch 7


> Schlaege[is.na(Schlaege)] <- 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

15.6.3 Ersetzungen in Teilbereichen – row(), col()

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

> # Matrix mit Zeilenindizes > # Matrix mit Spaltenindizes


> row(X) > col(X)
[,1] [,2] [,3] [,1] [,2] [,3]
[1,] 1 1 1 [1,] 1 2 3
[2,] 2 2 2 [2,] 1 2 3

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.

> # Ersetze in den Spalten 2 und 3 alle positiven Werte durch -9


> X[X > 0 & col(X) %in% c(2, 3)] <- -9
> X
[,1] [,2] [,3]
[1,] 3 -9 -1
[2,] 2 0 -9

15.7 Aus der guten Programmierpraxis

15.7.1 Programmierstil: Lesbarkeit und Spacing

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.

Empfohlenes Spacing Nicht empfohlenes Spacing


> 1 + 3 + 5 + 7 > 1+3+5+7
[1] 16 [1] 16

> zahl <- 5 > zahl<-5

> 5 * (2 + 3) > 5*(2+3)


[1] 25 [1] 25

Vor und nach binären Operatoren sollten Leerzeichen eingefügt werden. Ausnah-
me: Beim Sequenzoperator fügen wir keinen Leerraum ein.

Empfohlenes Spacing Nicht empfohlenes Spacing


> 1:5 > 1 : 5
[1] 1 2 3 4 5 [1] 1 2 3 4 5

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

Empfohlenes Spacing Nicht empfohlenes Spacing


> seq(from = 0, to = 10, by = 2) > seq(from=0,to=10,by=2)
[1] 0 2 4 6 8 10 [1] 0 2 4 6 8 10

> seq( from = 0, to = 10, by = 2 )


[1] 0 2 4 6 8 10

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.

Ähnlich verhält es sich auch beim Subsetting.

Empfohlenes Spacing Nicht empfohlenes Spacing


> X[, 1] > X[,1]
[1] 3 2 [1] 3 2

> X[1, ] > X[1,]


[1] 3 -9 -1 [1] 3 -9 -1

> X[, 1, drop = FALSE] > X[,1,drop = FALSE]


[,1] [,1]
[1,] 3 [1,] 3
[2,] 2 [2,] 2

> X[1, , drop = FALSE] > X[1,,drop = FALSE]


[,1] [,2] [,3] [,1] [,2] [,3]
[1,] 3 -9 -1 [1,] 3 -9 -1

In allen vier nicht empfohlenen Varianten fehlen Leerzeichen nach den Beistrichen.
Wir fassen in Regel 13 die besprochenen Empfehlungen zusammen.

Regel 13: Verwende Leerzeichen, um die Lesbarkeit zu verbessern!

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

15.8.1 Objekte sichern

> # Daten sichern


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> save(schlaege1, schlaege2, schlaege3, Schlaege,
+ file = "Minigolf_Matrizen.RData")
15 Matrizen 191

15.8.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

Folgende Aufgabenstellungen wären unter anderem auch sehr von Interesse:

1. Bilde für jeden Spieler die kumulierten Schlagzahlen


2. Bestimme für jeden Spieler den Median der Schlagzahlen
3. Allgemein: Sortiere jede Spalte aufsteigend
192 D Datenstrukturen

Probieren wir ein paar Sachen aus.

> # Die Schlagzahlen der Spieler


> 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

> # 1.) cumsum() kumuliert alle Schlagzahlen!


> cumsum(Schlaege)
[1] 2 5 7 11 18 21 22 25 28 34 38 40 41 43 46 53 57 58

> # 2.) median() berechnet Median aller Schlagzahlen!


> median(Schlaege)
[1] 3

> # 3.) sort() sortiert alle Einträge!


> sort(Schlaege)
[1] 1 1 1 2 2 2 2 3 3 3 3 3 4 4 4 6 7 7

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?

> Z <- cbind(c(3, 4, 2, 1), c(1, 2))


> Z

> Z <- Z[order(Z[, 1]), ]


> Z

> Z <- Z * c(-1, 1)


> Z

> Z <- rbind(Z, colSums(Z))


> Z <- cbind(Z, Z[, 1] + Z[, 2])
> Z

2. Erkläre, wofür folgender R-Code gut sein könnte:

> which(Schlaege >= 5, arr.ind = TRUE)

Wie würdest du jetzt die Aufgabe in Fallbeispiel (4.8.2) lösen?


15 Matrizen 193

3. Eine Lehrveranstaltung wird von n Studierenden besucht. Es werden k Tests


geschrieben, wobei jeweils 50 Punkte erreicht werden können. Jeder Test wird
gleich gewichtet. Die Lehrveranstaltung gilt als bestanden, wenn in Summe
mindestens die Hälfte der erreichbaren Punkte erzielt wurden. Die Resultate
werden in der Matrix Pkt verwaltet. NA heißt: nicht angetreten.
Zur Illustration betrachten wir ein Beispiel mit n = 5 und k = 2.

> 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

Dein Code soll für beliebige n und beliebige k funktionieren!

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

Dabei entspricht 1 einem weißen und 2 einem schwarzen Feld. Passionierte


Schachspieler wissen, dass die Farben keinesfalls vertauscht werden dürfen.

a) Folgender Code existiert bereits:

> S <- matrix(0, ncol = 8, nrow = 8)


> S <- (col(S) + row(S) %% 2 == 0) + 1
> S

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).

5. Dieses Beispiel ist eine Herausforderung!


Generiere ausschließlich mit den bis hierher gelernten Techniken (insbesondere
ohne Schleifen und schleifenähnliche Konstrukte) n ≥ 1 zufällige Lottotipps
für Lotto 6 aus 45 (siehe Infobox 1 auf Seite 53). Ordne die Tipps in einer
n × 6-Matrix an. Dein Code soll für beliebige n funktionieren.
Kontrolliere, ob die Tipps gültig sind! Es darf zum Beispiel keine Zahl in einem
Tipp doppelt vorkommen. Das Ergebnis könnte für n = 3 etwa so aussehen:

> 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!

> Tipps # Zahlen in jeder Zeile sind sortiert


[,1] [,2] [,3] [,4] [,5] [,6]
Tipp1 6 15 18 30 35 41
Tipp2 6 9 17 29 35 40
Tipp3 1 8 23 26 38 39
16 Rechnen mit Matrizen und Lineare Algebra 195

16 Rechnen mit Matrizen und Lineare Algebra

Dort wollen wir in diesem Kapitel hin:

• die Welt der Diagonalmatrizen erobern

• Formeln der linearen Algebra auswerten

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.

Du siehst: Es gibt es viel Spannendes zu entdecken!

16.1 Diagonalmatrizen und Diagonalelemente – diag()

Die Funktion diag() ist extrem vielseitig. Folgende Dinge können wir mit ihr an-
stellen:

1. Einheitsmatrizen und Diagonalmatrizen generieren.

2. Diagonalelemente einer Matrix extrahieren.


3. Diagonalelemente einer Matrix ersetzen.

Funktionsaufruf:

diag(x = 1, nrow, ncol)


diag(x) <- value

© 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

Einheitsmatrizen generieren wir, indem wir dem Parameter x einen Skalar1


übergeben und Diagonalmatrizen erzeugen wir, indem wir dem Parameter x
einen Vektor mit den gewünschten Diagonalelementen übergeben.

> # 3-dimensionale Einheitsmatrix > # Diagonalmatrix mit 1:3


> diag(3) > diag(1:3)
[,1] [,2] [,3] [,1] [,2] [,3]
[1,] 1 0 0 [1,] 1 0 0
[2,] 0 1 0 [2,] 0 2 0
[3,] 0 0 1 [3,] 0 0 3

Diagonalelemente selektieren wir, indem wir für x eine Matrix einsetzen.

> # Generiere eine Matrix D ...


> D <- matrix(1:9, ncol = 3)
> D
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9

> # ... und selektiere ihre Diagonalelemente


> diag(D)
[1] 1 5 9

Diagonalelemente einer Matrix ersetzen wir wie folgt:

> # Ersetzen der Diagonalelemente


> diag(D) <- c(10, 11, 12)
> D
[,1] [,2] [,3]
[1,] 10 4 7
[2,] 2 11 8
[3,] 3 6 12

Beispiel: Generiere aus der Matrix D eine Diagonalmatrix bestehend aus den Dia-
gonalelementen.
Mit diag() keine große Sache!

> # Diagonalmatrix aus den Diagonalelementen generieren


> diag(diag(D))
[,1] [,2] [,3]
[1,] 10 0 0
[2,] 0 11 0
[3,] 0 0 12

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

16.2 Rechnen mit Matrizen

In Tab. 16.1 listen wir wichtige Rechenoperationen auf.

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

16.2.1 Elementweises Rechnen – "+", "-", "*", "/", "ˆ"

Auch bei Matrizen erfolgen binäre Rechenoperationen elementweise (vgl. (4.6)).

> A + B > A * B > B ^ A > A * c(2, 3)


[,1] [,2] [,1] [,2] [,1] [,2] [,1] [,2]
[1,] -1 5 [1,] -2 6 [1,] -2 8 [1,] 2 6
[2,] 3 1 [2,] 2 -2 [2,] 2 1 [2,] 3 6

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

16.2.2 Matrixmultiplikation – "%*%", crossprod(), tcrossprod()

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.

> # Matrixmultiplikation > # A transponiert mal B > # A mal B transponiert


> A %*% B > t(A) %*% B > A %*% t(B)
[,1] [,2] [,1] [,2] [,1] [,2]
[1,] 4 -1 [1,] 0 1 [1,] 4 -1
[2,] 2 0 [2,] -2 4 [2,] 2 0
> crossprod(A, B) > tcrossprod(A, B)
[,1] [,2] [,1] [,2]
[1,] 0 1 [1,] 4 -1
[2,] -2 4 [2,] 2 0

Dabei ist die Berechnung mittels crossprod(A, B) und tcrossprod(A, B) ein we-
nig effizienter als t(A) %*% B und A %*% t(B).

16.2.3 Invertierung und Determinante – solve(), det()

Mit solve() können wir Matrizen invertieren.

> # Die Matrix A > # A invertieren > # A mal A^(-1)


> A > solve(A) > A %*% solve(A)
[,1] [,2] [,1] [,2] [,1] [,2]
[1,] 1 3 [1,] -2 3 [1,] 1 0
[2,] 1 2 [2,] 1 -1 [2,] 0 1

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.

> # Determinante von A


> det(A)
[1] -1

16.3 Aus der guten Praxis

Jetzt haben wir das nötige Rüstzeug, um uns zwei coole Beispiele anzusehen!
16 Rechnen mit Matrizen und Lineare Algebra 199

16.3.1 Fallbeispiel: Lineare Regression

Wir befragen 8 Studentinnen nach ihrer Größe und ihrem Gewicht.

Größe 184 168 160 168 170 168 163 170


Gewicht 65 70 48 52 53 56 56 59

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

> # Matrix X erstellen: Erste Spalte modelliert das Interzept.


> X <- cbind(1, x)
> X
x
[1,] 1 184
[2,] 1 168
[3,] 1 160
[4,] 1 168
[5,] 1 170
[6,] 1 168
[7,] 1 163
[8,] 1 170

> # OLS-Schätzer berechnen laut Formel (16.6)


> beta.hat <- solve(crossprod(X)) %*% crossprod(X, y)
> as.vector(beta.hat) # Interzept und Steigung
[1] -39.2032432 0.5718919

> # Prognostizierte Werte berechnen laut Formel (16.7)


> y.hat <- as.vector(X %*% beta.hat)
> y.hat
[1] 66.02486 56.87459 52.29946 56.87459 58.01838 56.87459 54.01514 58.01838

> # Residuen berechnen laut Formel (16.8)


> u.hat <- y - y.hat
> round(u.hat, digits = 4)
[1] -1.0249 13.1254 -4.2995 -4.8746 -5.0184 -0.8746 1.9849 0.9816

Die Grafiken in Abb. 16.1 zeigen, was wir soeben ausgerechnet haben. Im Ausblick
(16.4.2) findest du den zugrundeliegenden Code dazu.

Regressionsbeispiel: Gewicht vs. Größe Regressionsbeispiel: Gewicht vs. Größe


70

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

Abbildung 16.1: Visualisierung der Regression aus (16.3.1)


16 Rechnen mit Matrizen und Lineare Algebra 201

16.3.2 Fallbeispiel: Lineare Gleichungssysteme lösen – solve()

Wir wollen das lineare Gleichungssystem A · x = b mit


" # !
3 4 2
A= und b=
1 2 0

(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)

> # Parameter definieren


> A <- matrix(c(3, 1, 4, 2), ncol = 2)
> b <- c(2, 0)

> # Löse das lineare Gleichungssystem A x = b laut Formel (16.9)


> res <- solve(A) %*% b
> res
[,1]
[1,] 2
[2,] -1

> # Lösung als Vektor ausgeben


> as.vector(res)
[1] 2 -1

Wir können die eindeutige Lösung x = (2, −1)t bequem ablesen.


Ist die Matrix A nicht invertierbar, so gibt R eine Fehlermeldung aus:

> S <- matrix(rep(1, 4), ncol = 2)


> S # S ist offensichtlich nicht invertierbar.
[,1] [,2]
[1,] 1 1
[2,] 1 1

> # Inverse einer nicht invertierbaren Matrix


> solve(S)
Fehler in solve.default(S) :
Lapackroutine dgesv: System ist genau singulär: U[2,2] = 0

Tipp: Wenn wir ?solve eingeben, stellen wir fest, dass wir so schneller ans Ziel
kommen:

> # Löse Gleichungssystem Ax = b direkt mit solve()


> solve(A, b)
[1] 2 -1
202 D Datenstrukturen

16.4 Abschluss

16.4.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Wie erstellen wir Einheitsmatrizen und Diagonalmatrizen? Wie extrahieren


und ersetzen wir Diagonalelemente einer Matrix? (16.1)
• Wie addieren, subtrahieren, multiplizieren, dividieren und potenzieren wir zwei
Matrizen miteinander elementweise? Was passiert, wenn wir eine Matrix mit
einem Vektor addieren, subtrahieren etc.? (16.2.1)
• Wie führen wir eine Matrixmultiplikation durch? Wie berechnen wir für zwei
adäquate Matrizen A und B die Ausdrücke A−1 · B und A · B −1 ? (16.2.2)
• Wie invertieren wir eine Matrix? Warum ergibt A · A−1 oft nicht die Einheits-
matrix? Wie bestimmen wir die Determinante einer Matrix? (16.2.3)

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)

# Regressionsgerade und Projektionslinien einzeichnen


lines(sort(x), y.hat[order(x)], col = 2)
segments(x, y, x, y.hat, col = 4)
16 Rechnen mit Matrizen und Lineare Algebra 203

16.4.3 Übungen

1. Gegeben ist die Matrix D:

> D <- matrix(1:16, ncol = 4)


> D
[,1] [,2] [,3] [,4]
[1,] 1 5 9 13
[2,] 2 6 10 14
[3,] 3 7 11 15
[4,] 4 8 12 16

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.

c) Finde eine alternative Schreibweise für folgenden Code:


> D * (row(D) == col(D))

d) Bei der oberen Dreiecksmatrix sind alle Elemente unterhalb der Hauptdia-
gonalen gleich 0. Erstelle aus der der Matrix D eine obere Dreiecksmatrix.

2. Betrachte die folgende Beispielmatrix X:


" #
1 5
X=
3 7

a) Berechne die Inverse X −1 .


b) Überprüfe, ob der Ausdruck X · X −1 exakt einer Einheitsmatrix gleicht.
Es soll ein TRUE oder FALSE herauskommen.
c) Überprüfe, ob der Ausdruck X · X −1 annähernd einer Einheitsmatrix
gleicht. Und zwar
• mit der Funktion all.equal().

• ohne die Funktion all.equal(). Verwende dabei jene Toleranz, die


all.equal() standardmäßig verwendet.
204 D Datenstrukturen

17 Listen

Dort wollen wir in diesem Kapitel hin:

• Listen kennenlernen und mit ihnen umgehen lernen

• Die Flexibilität von Listen lieben lernen

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!

1. Wir können mehrere unterschiedliche Objekte in eine Liste hineinpacken.


2. Insbesondere können diese Objekte unterschiedliche Modes haben.

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:

> X <- matrix(c(2, 1, -2, 0, 0.5, 0, 2, 4, 7), ncol = 3)


> X
[,1] [,2] [,3]
[1,] 2 0.0 2
[2,] 1 0.5 4
[3,] -2 0.0 7

Und gehen sodann unter anderem folgenden Fragen nach:


• Wie lauten die Eigenwerte und Eigenvektoren der Matrix X? (17.1)

• 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

17.1 Eigenwerte und Eigenvektoren – eigen()

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.

> # Berechne Eigenwerte und Eigenvektoren von X


> X.eigen <- eigen(X)
> X.eigen
eigen() decomposition
$values
[1] 6.0 3.0 0.5
$vectors
[,1] [,2] [,3]
[1,] -0.3608983 -0.6097108 0
[2,] -0.5905608 -0.7316529 1
[3,] -0.7217966 -0.3048554 0

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).

17.2 Überblick über Listen – is.list(), mode(), str(), length()

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.

> # Abfrage auf list > # Alternative Abfrage auf list


> is.list(X.eigen) > mode(X.eigen) == "list"
[1] TRUE [1] TRUE

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

> # Eigenschaften von X.eigen abfragen


> str(X.eigen)
List of 2
$ values : num [1:3] 6 3 0.5
$ vectors: num [1:3, 1:3] -0.361 -0.591 -0.722 -0.61 -0.732 ...
- attr(*, "class")= chr "eigen"

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.

> # Names einer Liste extrahieren


> names(X.eigen)
[1] "values" "vectors"

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.

> # Eigenwerte extrahieren und Maximum bestimmen - so klappt es nicht.


> X.val <- X.eigen["values"] # tendenziell robuster als X.eigen[1]
> X.val
$values
[1] 6.0 3.0 0.5

> 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.

> # Eigenwerte selektieren und Maximum bestimmen - so geht’s.


> X.val <- X.eigen[["values"]] # tendenziell robuster als x.eigen[[1]]
> X.val
[1] 6.0 3.0 0.5

> 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.

> # Zugriff auf vectors > # Zugriff auf values (val)


> X.eigen$vectors > X.eigen$val
[,1] [,2] [,3] [1] 6.0 3.0 0.5
[1,] -0.3608983 -0.6097108 0 > # Keine eindeutige Abkürzung
[2,] -0.5905608 -0.7316529 1 > X.eigen$v
[3,] -0.7217966 -0.3048554 0 NULL

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:

> X.eigen[[1:2]] # So geht es nicht!


[1] 3

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.

> # Zugriff mit Indizes > # Alternative mit Names


> X.eigen[1:2] > X.eigen[c("values", "vectors")]
$values $values
[1] 6.0 3.0 0.5 [1] 6.0 3.0 0.5
$vectors $vectors
[,1] [,2] [,3] [,1] [,2] [,3]
[1,] -0.3608983 -0.6097108 0 [1,] -0.3608983 -0.6097108 0
[2,] -0.5905608 -0.7316529 1 [2,] -0.5905608 -0.7316529 1
[3,] -0.7217966 -0.3048554 0 [3,] -0.7217966 -0.3048554 0
208 D Datenstrukturen

Regel 14: Zugriffsregeln bei Listen

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.

> # Sortierung von A bis Z > # Sortierung von Z bis A


> temp <- order(names(X.eigen)) > temp <- order(names(X.eigen),
+ decreasing = TRUE)
> X.eigen[temp] > X.eigen[temp]
$values $vectors
[1] 6.0 3.0 0.5 [,1] [,2] [,3]
$vectors [1,] -0.3608983 -0.6097108 0
[,1] [,2] [,3] [2,] -0.5905608 -0.7316529 1
[1,] -0.3608983 -0.6097108 0 [3,] -0.7217966 -0.3048554 0
[2,] -0.5905608 -0.7316529 1 $values
[3,] -0.7217966 -0.3048554 0 [1] 6.0 3.0 0.5

17.4 Listen erstellen und initialisieren – list(), vector("list")

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.

> # Erstelle eine unbefüllte Liste mit 2 Elementen


> vector(mode = "list", length = 2)
[[1]]
NULL
[[2]]
NULL

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.

> X.liste <- list(eigen = eigen(X), > X.liste <- list(


+ dim = dim(X), diag = diag(X)) + eigen = unclass(eigen(X)),
> X.liste + dim = dim(X), diag = diag(X))
$eigen > X.liste
eigen() decomposition $eigen
$values $eigen$values
[1] 6.0 3.0 0.5 [1] 6.0 3.0 0.5
$vectors $eigen$vectors
[,1] [,2] [,3] [,1] [,2] [,3]
[1,] -0.3608983 -0.6097108 0 [1,] -0.3608983 -0.6097108 0
[2,] -0.5905608 -0.7316529 1 [2,] -0.5905608 -0.7316529 1
[3,] -0.7217966 -0.3048554 0 [3,] -0.7217966 -0.3048554 0
$dim $dim
[1] 3 3 [1] 3 3
$diag $diag
[1] 2.0 0.5 7.0 [1] 2.0 0.5 7.0
> length(X.liste) > length(X.liste)
[1] 3 [1] 3

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.

> # Selektiere aus X.liste die Eigenwerte


> X.liste$eigen$values
[1] 6.0 3.0 0.5

Alternativ können wir rekursiv zugreifen.

> # Rekursiver Zugriff auf die Eigenwerte


> X.liste[[c("eigen", "values")]]
[1] 6.0 3.0 0.5

Der Code entspricht intern dem folgenden Code:

> X.liste[["eigen"]][["values"]]
[1] 6.0 3.0 0.5
210 D Datenstrukturen

17.5 Atomare und rekursive Objekte – is.atomic(),


is.recursive(), is.vector()

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.

> # Ist X.liste ein Vektor?


> is.vector(X.liste)
[1] TRUE

Die Abfrage ergibt TRUE, das Objekt X.liste ist also (auch) ein Vektor!

R unterscheidet zwischen atomaren (atomic) und rekursiven (recursive) Objekten.


Listen sind rekursiv, da eine Liste Teil einer anderen Liste sein kann. Vektoren sind
atomar, ein Vektor kann nicht Teil eines anderen Vektors sein.
Die Information, ob ein Objekt atomar bzw. rekursiv ist, erhalten wir mit Hilfe
der Funktionen is.atomic() bzw. is.recursive().

> # Extrahiere den Vektor mit den Eigenwerten


> X.val <- X.eigen$val
> X.val
[1] 6.0 3.0 0.5

> is.atomic(X.liste) > is.atomic(X.val)


[1] FALSE [1] TRUE
> is.recursive(X.liste) > is.recursive(X.val)
[1] TRUE [1] FALSE

Wollen wir bei einem Objekt abfragen, ob es wirklich ein Vektor ist, so gibt es
folgende Möglichkeiten.

> # Abfrage auf "echten" Vektor - 1. Technik


> is.vector(X.val) & is.atomic(X.val)
[1] TRUE
> is.vector(X.liste) & is.atomic(X.liste)
[1] FALSE

Um echte Vektoren herauszufiltern, fügen wir die zusätzliche Bedingung hinzu, dass
das Objekt auch atomar ist.

> # Abfrage auf "echten" Vektor - 2. Technik


> is.vector(X.val, mode = "numeric")
[1] TRUE
> is.vector(X.liste, mode = "numeric")
[1] FALSE

Bei der zweiten Technik fragen wir nicht allgemein nach Vektoren, sondern nach
einem Vektor mit einem bestimmten Mode (hier "numeric").
17 Listen 211

17.6 Elemente hinzufügen, löschen und ersetzen – c(), NULL

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.

> # Spur hinzufügen > # Determinante zusätlich hinzufügen


> X.liste <- c(X.liste, > X.liste[["det"]] <- det(X)
+ spur = sum(diag(X.liste$diag))) > X.liste$det <- det(X) # Alternative
> X.liste > X.liste
$eigen $eigen
$eigen$values $eigen$values
[1] 6.0 3.0 0.5 [1] 6.0 3.0 0.5
$eigen$vectors $eigen$vectors
[,1] [,2] [,3] [,1] [,2] [,3]
[1,] -0.3608983 -0.6097108 0 [1,] -0.3608983 -0.6097108 0
[2,] -0.5905608 -0.7316529 1 [2,] -0.5905608 -0.7316529 1
[3,] -0.7217966 -0.3048554 0 [3,] -0.7217966 -0.3048554 0
$dim $dim
[1] 3 3 [1] 3 3
$diag $diag
[1] 2.0 0.5 7.0 [1] 2.0 0.5 7.0
$spur $spur
[1] 9.5 [1] 9.5
$det
[1] 9

Wollen wir Listenelemente löschen, so weisen wir den entsprechenden Elementen


NULL zu.
Beispiel: Lösche aus X.liste die Elemente eigen, dim und diag.

> # Lösche zunächst eigen und dim


> X.liste[c("eigen", "dim")] <- NULL
> X.liste
$diag
[1] 2.0 0.5 7.0
$spur
[1] 9.5
$det
[1] 9

Mehrere Elemente gleichzeitig zu löschen funktioniert nur mit den einfachen


eckigen Klammern. Einzelne Elemente können wir darüber hinaus auch mit dem
"$"-Operator und den doppelten eckigen Klammern löschen.
Wie gut, dass wir „vergessen“ haben diag zu löschen, dadurch können wir diese
Variante auch noch demonstrieren ;-)
212 D Datenstrukturen

> # Lösche noch diag


> X.liste$diag <- NULL # oder X.liste[["diag"]] <- NULL
> X.liste
$spur
[1] 9.5
$det
[1] 9

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.

> X.liste["spur"] <- list(NULL) > X.liste[["spur"]] <- 1:3


> X.liste > X.liste
$spur $spur
NULL [1] 1 2 3
$det $det
[1] 9 [1] 9

17.7 Listen zu Vektoren vereinfachen – unlist()

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.

unlist(x, recursive = TRUE, use.names = TRUE)

> # Die Liste "unlisten" (make a list "flat")


> X.vektor <- unlist(X.liste)
> X.vektor
spur1 spur2 spur3 det
1 2 3 9

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!

> liste <- list(Zahl = c(1, 2, 7, 3), Bool = c(TRUE, FALSE))


> liste
$Zahl
[1] 1 2 7 3
$Bool
[1] TRUE FALSE

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.

> # Konvertierung bei Überführung in eine Liste


> unlist(liste)
Zahl1 Zahl2 Zahl3 Zahl4 Bool1 Bool2
1 2 7 3 1 0

17.8 Abschluss

17.8.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Wozu brauchen wir Listen? (17)


• Wie berechnen wir die Eigenwerte und Eigenvektoren einer Matrix? Wie wer-
den die Eigenwerte und Eigenvektoren verwaltet? (17.1)
• Wie fragen wir ab, ob ein Objekt eine Liste ist? Wie bestimmen wir die Anzahl
der Elemente einer Liste? Was gibt die Funktion str() aus? (17.2)
• Wie greifen wir auf die Beschriftungen einer Liste zu und wie können wir sie
ändern bzw. löschen? Wie selektieren wir einzelne Listenelemente bzw. einen
Teilbereich einer Liste? Was ist der Unterschied zwischen den Selektionsme-
thoden "[ ]", "[[ ]]" und "$"? (17.3)
• Wie erstellen wir eine unbefüllte Liste und woraus besteht sie? Wie packen
wir mehrere Objekte in eine Liste? Was ist eine verschachtelte Liste und wie
greifen wir auf Elemente zu, die tiefer verschachtelt sind? (17.4)
• Was sind atomare und rekursive Objekte? Warum ist eine Liste rekursiv und
ein Vektor atomar? Wie erfragen wir, ob ein Objekt atomar bzw. rekursiv ist?
Wie können wir Vektoren von Listen unterscheiden? (17.5)
• Wie fügen wir einer Liste neue Elemente hinzu? Wie löschen bzw. ersetzen
wir Elemente einer Liste? Welche Möglichkeiten haben wir dabei jeweils? Wie
weisen wir einem Listenelement NULL zu? (17.6)
• Wie führen wir eine Liste in einen Vektor über? Was passiert dabei, wenn die
Elemente der Liste unterschiedliche Modes haben? (17.7)

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:

• Margherita: Tomaten und Käse


• Cardinale: Tomaten, Käse und Schinken
• San Romio: Tomaten, Käse, Schinken, Salami und Mais
• Provinciale: Tomaten, Käse, Schinken, Mais und Pfefferoni

Wir packen die Pizzen in die Liste pizzen.2

pizzen <- list(


Margherita = c("Tomaten", "Kaese"),
Cardinale = c("Tomaten", "Kaese", "Schinken"),
"San Romio" = c("Tomaten", "Kaese", "Schinken", "Salami", "Mais"),
Provinciale = c("Tomaten", "Kaese", "Schinken", "Mais", "Pfefferoni"))

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

18 Wiederholte Funktionsanwendung bei Listen

Dort wollen wir in diesem Kapitel hin:

• Funktionen auf alle Elemente einer Datenstruktur anwenden

Auf Seite 214 haben wir in Übungsaufgabe 2 die Speisekarte einer Pizzeria in Form
einer Liste erstellt:2

> pizzen <- list(


+ Margherita = c("Tomaten", "Kaese"),
+ Cardinale = c("Tomaten", "Kaese", "Schinken"),
+ "San Romio" = c("Tomaten", "Kaese", "Schinken", "Salami", "Mais"),
+ Provinciale = c("Tomaten", "Kaese", "Schinken", "Mais", "Pfefferoni"))

> 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"

Einige spannende Fragen haben wir in dieser Übungsaufgabe bereits beantwortet.


Solltest du diese Übungsaufgabe ausgelassen haben, so ist es noch nicht zu spät, sie
jetzt nachzuholen ;-). In diesem Kapitel wenden wir uns unter anderem folgenden
Fragen zu:
• Wie viele Zutaten hat jede Pizza (revisited)? (18.1)
• Wie sortieren wir die Zutaten jeder Pizza alphabetisch? (18.2)
• Auf welchen Pizzen befindet sich Schinken? Auf welchen Schinken und Mais?
(18.3)
Wir lernen in diesem Kapitel die beiden Funktionen sapply() und lapply() kennen,
mit denen wir obige Fragen einfach und erfolgreich beantworten können!
Die Funktion sapply() ist etwas flexibler als lapply(), wie wir sehen werden. Fle-
xibilität bedeutet aber auch erhöhte Vorsicht in bestimmten Situationen, wie wir in
(18.4) bemerken.
In den abschließenden Übungsaufgaben erhältst du wieder einmal die Chance, dich
interessanten Herausforderungen zu stellen.
2 Leerzeichen
in Beschriftungen sind in der Regel keine gute Idee! Verwenden wir sie trotzdem, so
müssen wir sowohl bei der Namensübergabe, als auch beim Subsetting mit dem $-Operator den
Namen immer unter Anführungszeichen setzen. Also etwa pizzen$"San Romio", wenn wir die
Zutaten von „San Romio“ wissen wollen.

© 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

18.1 Wiederholte Funktionsanwendung auf Elemente einer


Datenstruktur – lapply(), sapply()

Wie viele Zutaten hat jede Pizza? Ganz einfach ...

> # Anzahl der Zutaten jeder Pizza bestimmen


> anz.zutaten <- c(length(pizzen[[1]]), length(pizzen[[2]]),
+ length(pizzen[[3]]), length(pizzen[[4]]))
> names(anz.zutaten) <- names(pizzen)
> anz.zutaten
Margherita Cardinale San Romio Provinciale
2 3 5 5

... 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!

lapply(X, FUN, ...)


sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)

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

> # lapply() gibt immer > # sapply(): Vereinfachung abstellen


> # eine Liste zurück! > sapply(pizzen, FUN = length,
> lapply(pizzen, FUN = length) + simplify = FALSE)
$Margherita $Margherita
[1] 2 [1] 2
$Cardinale $Cardinale
[1] 3 [1] 3
$‘San Romio‘ $‘San Romio‘
[1] 5 [1] 5
$Provinciale $Provinciale
[1] 5 [1] 5

> # Vereinfachung des Ergebnisobjektes (hier in einen Vektor)


> sapply(pizzen, FUN = length)
Margherita Cardinale San Romio Provinciale
2 3 5 5

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.

18.2 Parameterübergabe innerhalb von sapply() und lapply()

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).

> # Aufsteigend sortieren > # Absteigend sortieren


> lapply(pizzen, FUN = sort,
> lapply(pizzen, FUN = sort) + decreasing = TRUE)
$Margherita $Margherita
[1] "Kaese" "Tomaten" [1] "Tomaten" "Kaese"
$Cardinale $Cardinale
[1] "Kaese" "Schinken" "Tomaten" [1] "Tomaten" "Schinken" "Kaese"
..... .....

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):

> sort(pizzen[[1]], decreasing = TRUE) # Aufruf für die erste Pizza


[1] "Tomaten" "Kaese"
> sort(pizzen[[2]], decreasing = TRUE) # Aufruf für die zweite Pizza
[1] "Tomaten" "Schinken" "Kaese"
218 D Datenstrukturen

Betrachten wir die internen Abläufe auf einer allgemeineren Ebene.

lapply(X, FUN, ...)


sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE)

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.

sort(x, decreasing = FALSE, ...)

Wir können steuern, für welchen Parameter von FUN die einzelnen Elemente von X
eingesetzt werden. Schauen wir uns ein kleines Beispiel an.

> lapply(list(TRUE, FALSE), FUN = sort, x = 1:3)


[[1]]
[1] 3 2 1
[[2]]
[1] 1 2 3

Hier wird innerhalb von lapply() folgendes ausgeführt:

> # Aufruf für 1. Element > # Aufruf für 2. Element


> sort(TRUE, x = 1:3) > sort(FALSE, x = 1:3)
[1] 3 2 1 [1] 1 2 3

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?

> lapply(list(TRUE, FALSE), FUN = sort, 1:3)


Fehler in FUN(X[[i]], ...) :
’decreasing’ muss ein boolescher Vektor der Länge 1 sein.
’partial’ absichtlich gesetzt?

In dem Fall wird intern folgendes ausgeführt:

> # Aufruf für 1. Element > # Aufruf für 2. Element


> sort(TRUE, 1:3) > sort(FALSE, 1:3)

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

18.3 Operatoren als Funktion verwenden

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"

> # "==" als Operator anwenden


> pizzen[["Provinciale"]] == "Schinken"
[1] FALSE FALSE TRUE FALSE FALSE

> # "==" als Funktion anwenden


> "=="(x = pizzen[["Provinciale"]], y = "Schinken")
[1] FALSE FALSE TRUE FALSE FALSE

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.

Tabelle 18.1: Operatoren als Funktionen verwenden

Als Operator Als Funktion Als Operator Als Funktion


x <- value "<-"(x, value) x %in% table "%in%"(x, table)
x[i] "["(x, i) x == y "=="(x, y)
x[[i]] "[["(x, i) x != y "!="(x, y)
x + y "+"(x, y) x < y "<"(x, y)
x * y "*"(x, y) !x "!"(x)
x %% y "%%"(x, y) x & y "&"(x, y)
from:to ":"(from, to) x | y "|"(x, y)

Dieser Trick ermöglicht uns folgenden eleganten Code.

> lapply(pizzen, FUN = "==", y = "Schinken")


$Margherita
[1] FALSE FALSE
$Cardinale
[1] FALSE FALSE TRUE
$‘San Romio‘
[1] FALSE FALSE TRUE FALSE FALSE
$Provinciale
[1] FALSE FALSE TRUE FALSE FALSE

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

Beispiel: Auf welchen Pizzen befindet sich Schinken?

> bool.schinken <- lapply(pizzen, FUN = "==", y = "Schinken")

Eine Pizza enthält Schinken, wenn in bool.schinken der entsprechende logische


Vektor ein TRUE enthält. Um die Aufgabe zu vollenden, bestimmen wir also, ob es
zumindest ein TRUE gibt. Ein Fall für sapply() und any()!

> # Auf welchen Pizzen befindet sich Schinken?


> bool.res <- sapply(bool.schinken, any)
> bool.res
Margherita Cardinale San Romio Provinciale
FALSE TRUE TRUE TRUE

> names(bool.res)[bool.res]
[1] "Cardinale" "San Romio" "Provinciale"

Beispiel: Welche Pizzen sind mit Schinken und Mais belegt?


In diesem Fall brauchen wir den %in%-Operator, da wir prüfen wollen, ob die Zutaten
in der Menge c("Schinken", "Mais") enthalten sind.

> bool <- lapply(pizzen, FUN = "%in%", table = c("Schinken", "Mais"))


> bool
$Margherita
[1] FALSE FALSE
$Cardinale
[1] FALSE FALSE TRUE
$‘San Romio‘
[1] FALSE FALSE TRUE FALSE TRUE
$Provinciale
[1] FALSE FALSE TRUE TRUE FALSE

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().

> # Welche Pizzen sind mit Schinken und Mais belegt?


> bool.sum <- sapply(bool, sum)
> bool.sum
Margherita Cardinale San Romio Provinciale
0 1 2 2

> names(bool.res)[bool.sum == 2]
[1] "San Romio" "Provinciale"
18 Wiederholte Funktionsanwendung bei Listen 221

18.4 Vereinfachung der Datenstruktur bei sapply()

Die Funktion sapply() wählt (bei simplify = TRUE) die Datenstruktur des Ergeb-
nisobjektes wie folgt:

• vector, wenn FUN für alle Elemente einen Skalar zurückgibt.

• 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.

Wir betrachten ein einfaches Codebeispiel.

> liste <- list(A = 1:4, B = 1:3)


> liste
$A
[1] 1 2 3 4
$B
[1] 1 2 3

> sapply(liste, max) # Vektor: max() gibt immer einen Skalar zurück.
A B
4 3

> sapply(liste, range) # Matrix: range() gibt 2-elementigen Vektor zurück.


A B
[1,] 1 1
[2,] 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

18.5 sapply() und lapply() bei Vektoren und Matrizen

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.

> # sapply() bei Vektoren


> sapply(-2:2, max, 0) # pmax(-2:2, 0) ist deutlich effizienter!
[1] 0 0 0 1 2

Eine Anwendung angelehnt an das Codebeispiel von Seite 218.

> sapply(c(TRUE, FALSE), sort, x = 1:3)


[,1] [,2]
[1,] 3 1
[2,] 2 2
[3,] 1 3

In diesem Fall vereinfacht sapply() das Ergebnisobjekt zu einer Matrix, da sowohl


für TRUE als auch FALSE ein Vektor derselben Länge (3) erzeugt wird.
Wir bemerken auch, dass die einzelnen Ergebnisvektoren spaltenweise angeordnet
werden. Das heißt in diesem Fall: Die erste Spalte der Ergebnismatrix steht für das
Ergebnis, das sich für TRUE ergibt und die zweite Spalte für jenes von FALSE.
Bei Matrizen ist die Menge der sinnvollen Anwendungen sehr klein. Und zwar so
klein, dass den Autoren dieses Buches keine sinnvolle Anwendung bekannt ist.

> # sapply() bei Matrizen


> M <- matrix(1:9, ncol = 3)
> M
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9

> sapply(M, sum)


[1] 1 2 3 4 5 6 7 8 9

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

18.6.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

> sapply(c(TRUE, FALSE), sort, x = 1:3)


[,1] [,2]
[1,] 3 1
[2,] 2 2
[3,] 1 3

alternativ auch so formulieren können:

> vapply(c(TRUE, FALSE), sort, FUN.VALUE = c(0, 0, 0), x = 1:3)


> mapply(sort, decreasing = c(TRUE, FALSE), MoreArgs = list(x = 1:3))

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

1. Betrachte folgenden R-Code:

> 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"

a) Bestimme, wie viele fleischhaltige Zutaten (= Schinken und Salami) jede


Pizza in pizzen hat.
b) Wie viele vegetarische Pizzen (das sind Pizzen ohne fleischhaltige Zuta-
ten) bietet die Pizzeria an?

c) Erstelle aus pizzen einen Stringvektor der folgenden Form:


> # Gewünschter Output
> pizzen.string
[1] "Margherita: Tomaten, Kaese"
[2] "Cardinale: Tomaten, Kaese, Schinken"
[3] "San Romio: Tomaten, Kaese, Schinken, Salami, Mais"
[4] "Provinciale: Tomaten, Kaese, Schinken, Mais, Pfefferoni"

d) Überprüfe mit einer geeigneten Abfrage, ob die ersten beiden Zutaten


jeder Pizza in pizzen Tomaten und Kaese sind.
18 Wiederholte Funktionsanwendung bei Listen 225

3. (Fortsetzung von 2.) Wir wollen nun die Zutaten jeder Pizza alphabetisch
sortieren, aber Tomaten und Kaese sollen immer zuerst genannt werden.

> pizzen.sort # Gewünschtes Ergebnis ganz am Ende dieser Aufgabe


$Margherita
[1] "Tomaten" "Kaese"
$Cardinale
[1] "Tomaten" "Kaese" "Schinken"
$‘San Romio‘
[1] "Tomaten" "Kaese" "Mais" "Salami" "Schinken"
$Provinciale
[1] "Tomaten" "Kaese" "Mais" "Pfefferoni" "Schinken"

Wir zerlegen unser Ziel in vier Teilaufgaben.

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?

d) Füge jetzt in pizzen12 an jede Pizza Tomaten und Kaese zu Beginn


ein. Verwende dazu die Funktion append().

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

Bemerkung: Eine Möglichkeit, mit c() den Ansatz in 3c) zu korrigieren,


besteht darin, eine eigene Funktion zu schreiben, was wir allerdings erst in
(29) lernen. Ein kleiner Vorgeschmack:

> lapply(pizzen12, function(x) c(c("Tomaten", "Kaese"), x))

4. Gegeben ist die Buchstabenliste x.

> x <- list(c("L", "E", "A"), c("I", "S", "S", "T"),


+ c("E", "I", "E", "R"))
> x
[[1]]
[1] "L" "E" "A"
[[2]]
[1] "I" "S" "S" "T"
[[3]]
[1] "E" "I" "E" "R"

a) Welche Datenstruktur kommt für das gegebene x bei folgenden R-Codes


jeweils heraus? Begründe deine Antworten.

Code vector matrix list


sapply(x, unique) O O O
sapply(x, sample) O O O
sapply(x, sample, size = 1) O O O
sapply(x, sample, size = 2) O O O

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

19 Wiederholte Funktionsanwendung bei Matrizen

Dort wollen wir in diesem Kapitel hin:

• Funktionen auf Zeilen und Spalten einer Matrix anwenden

• Spalten von Matrizen zentrieren, skalieren und standardisieren


• Äußere Vektorprodukte berechnen

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).

> # Die Daten dieses Kapitels


> # Ggf. Arbeitsverzeichnis wechseln oder Pfad angeben
> objekte <- load("Minigolf_Matrizen.RData")
> objekte
[1] "schlaege1" "schlaege2" "schlaege3" "Schlaege"

> # Die Schlagzahlen der 3 Spieler


> 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

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

19.1 Wiederholte Funktionsanwendung auf Zeilen und Spalten


einer Matrix – apply()

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):

apply(X, MARGIN, FUN, ...)

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.

19.1.1 Erstes Anwendungsbeispiel für apply()

Beispiel: Bestimme die Schlagsummen aller 3 Spieler.

> # Schlagsummen mit apply() > # Schlagsummen mit colSums()


> apply(Schlaege, MARGIN = 2, > # Deutlich effizienter!
+ FUN = sum) > colSums(Schlaege)
Spieler1 Spieler2 Spieler3 Spieler1 Spieler2 Spieler3
21 19 18 21 19 18

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():

> # Arbeitsweise des obigen Codes


> temp1 <- sum(Schlaege[, 1]) # sum() auf 1. Spalte anwenden
> temp2 <- sum(Schlaege[, 2]) # sum() auf 2. Spalte anwenden
> temp3 <- sum(Schlaege[, 3]) # sum() auf 3. Spalte anwenden

> # Ergebnisobjekt zusammenbauen


> res <- c(temp1, temp2, temp3)
> names(res) <- colnames(Schlaege)
> res
Spieler1 Spieler2 Spieler3
21 19 18

Wir sehen, dass ein (beschrifteter) Vektor herauskommt. Im Unterschied zu sapply()


vereinfacht apply() immer die Datenstruktur des Ergebnisobjektes, wobei apply()
dabei denselben Regeln folgt, wie sapply() mit der Option simplify = TRUE. Wir
widmen uns im folgenden Unterabschnitt (19.1.2) kurz diesem Thema.
19 Wiederholte Funktionsanwendung bei Matrizen 229

19.1.2 Vereinfachung der Datenstruktur bei apply()

Die Funktion apply() wählt die Datenstruktur des Ergebnisobjektes nach denselben
Regeln aus, wie sapply() mit der Option simplify = TRUE (vgl. (18.4)):

• vector, wenn FUN für alle Elemente einen Skalar zurückgibt.


• matrix, wenn FUN für alle Elemente einen gleichlangen Vektor mit mindestens
zwei Elementen zurückgibt.

• 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.

> # 1.) Kumulierte Schlagzahlen berechnen


> Schlaege.cum <- apply(Schlaege, MARGIN = 2, FUN = cumsum)
> Schlaege.cum
Spieler1 Spieler2 Spieler3
Bahn1 2 1 1
Bahn2 5 4 3
Bahn3 7 7 6
Bahn4 11 13 13
Bahn5 18 17 17
Bahn6 21 19 18

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.

> bool.fuehrung1 <- Schlaege.cum[, "Spieler1"] == Schlaege.cum.min


> names(bool.fuehrung1)[bool.fuehrung1]
[1] "Bahn4"

Nach Bahn Nummer 4 hat für Spieler 1 die Minigolfwelt perfekt ausgesehen.
230 D Datenstrukturen

19.1.3 Anordnung der Ergebnisse bei apply()

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.

> # Kleinste und größte Schlagzahl bestimmen


> Schlaege.range <- apply(Schlaege, MARGIN = 1, FUN = range)
> Schlaege.range
Bahn1 Bahn2 Bahn3 Bahn4 Bahn5 Bahn6
[1,] 1 2 2 4 4 1
[2,] 2 3 3 7 7 3

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

19.1.4 Parameterübergabe innerhalb von apply()

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

19.2 Über Zeilen und Spalten fegen – sweep()

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:

sweep(x, MARGIN, STATS, FUN = "-", ...)

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.

> # Spalten zentrieren


> center <- colMeans(Schlaege)
> Schlaege.centered <- sweep(Schlaege, MARGIN = 2, STATS = center, FUN = "-")
> Schlaege.centered
Spieler1 Spieler2 Spieler3
Bahn1 -1.5 -2.1666667 -2
Bahn2 -0.5 -0.1666667 -1
Bahn3 -1.5 -0.1666667 0
Bahn4 0.5 2.8333333 4
Bahn5 3.5 0.8333333 1
Bahn6 -0.5 -1.1666667 -2

Anschaulich auf drei Dezimalstellen gerundet:

Spieler1 Spieler2 Spieler3


Bahn1 2.000 - 3.500 1.000 - 3.167 1.000 - 3.000
Bahn2 3.000 - 3.500 3.000 - 3.167 2.000 - 3.000
Bahn3 2.000 - 3.500 3.000 - 3.167 3.000 - 3.000
Bahn4 4.000 - 3.500 6.000 - 3.167 7.000 - 3.000
Bahn5 7.000 - 3.500 4.000 - 3.167 4.000 - 3.000
Bahn6 3.000 - 3.500 2.000 - 3.167 1.000 - 3.000
----- ----- -----
center 3.500 3.167 3.000

> # Kontrolliere, ob die Spalten tatsächlich zentriert sind


> colMeans(Schlaege.centered)
Spieler1 Spieler2 Spieler3
0.000000e+00 1.480297e-16 0.000000e+00
232 D Datenstrukturen

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:

> # Zentriere Spalten einer Matrix


> apply(Schlaege, MARGIN = 2, FUN = function(x) x - mean(x))
Spieler1 Spieler2 Spieler3
Bahn1 -1.5 -2.1666667 -2
Bahn2 -0.5 -0.1666667 -1
Bahn3 -1.5 -0.1666667 0
Bahn4 0.5 2.8333333 4
Bahn5 3.5 0.8333333 1
Bahn6 -0.5 -1.1666667 -2

Beispiel: Standardisiere die Spalten der Matrix Schlaege.centered.

> # Spalten skalieren (auf Standardabweichung 1 bringen)


> temp <- apply(Schlaege.centered, MARGIN = 2, FUN = sd)
> Schlaege.scaled <- sweep(Schlaege.centered, 2, STATS = temp, FUN = "/")
> Schlaege.scaled
Spieler1 Spieler2 Spieler3
Bahn1 -0.8017837 -1.25793362 -0.877058
Bahn2 -0.2672612 -0.09676412 -0.438529
Bahn3 -0.8017837 -0.09676412 0.000000
Bahn4 0.2672612 1.64499012 1.754116
Bahn5 1.8708287 0.48382062 0.438529
Bahn6 -0.2672612 -0.67734887 -0.877058

> # Kontrolliere, ob die Spalten tatsächlich normiert sind


> apply(Schlaege.scaled, MARGIN = 2, FUN = sd)
Spieler1 Spieler2 Spieler3
1 1 1

Bis auf winzige (nicht sichtbare) Rundungsfehler sind alle Spalten standardisiert.

19.3 Zentrierung und Standardisierung – scale()

Für die Zentrierung und Standardisierung von Matrixspalten gibt es eine


bequeme Funktion: scale() mit dem Aufruf:

scale(x, center = TRUE, scale = TRUE)

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

Beispiel: Standardisiere Schlaege mit Hilfe der Funktion scale().

> Schlaege.scaled <- scale(Schlaege)


> Schlaege.scaled
Spieler1 Spieler2 Spieler3
Bahn1 -0.8017837 -1.25793362 -0.877058
Bahn2 -0.2672612 -0.09676412 -0.438529
Bahn3 -0.8017837 -0.09676412 0.000000
Bahn4 0.2672612 1.64499012 1.754116
Bahn5 1.8708287 0.48382062 0.438529
Bahn6 -0.2672612 -0.67734887 -0.877058
attr(,"scaled:center")
Spieler1 Spieler2 Spieler3
3.500000 3.166667 3.000000
attr(,"scaled:scale")
Spieler1 Spieler2 Spieler3
1.870829 1.722401 2.280351

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).

19.4 Äußere Vektorprodukte – outer(), "%o%"

Nehmen wir einmal an, wir wollen eine Tabelle mit dem kleinen Einmaleins generie-
ren. Ein Fall für die Funktion outer().

outer(X, Y, FUN = "*", ...)

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]).

> outer(1:10, 1:10, FUN = "*") # Das kleine Einmaleins


[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,] 1 2 3 4 5 6 7 8 9 10
[2,] 2 4 6 8 10 12 14 16 18 20
[3,] 3 6 9 12 15 18 21 24 27 30
[4,] 4 8 12 16 20 24 28 32 36 40
[5,] 5 10 15 20 25 30 35 40 45 50
[6,] 6 12 18 24 30 36 42 48 54 60
[7,] 7 14 21 28 35 42 49 56 63 70
[8,] 8 16 24 32 40 48 56 64 72 80
[9,] 9 18 27 36 45 54 63 72 81 90
[10,] 10 20 30 40 50 60 70 80 90 100

In der R-Hilfe findest du weitere spannende Anwendungen zu dieser Funktion. Unter


anderem können wir für FUN auch binäre Operatoren einsetzen (vgl. (18.3)).
234 D Datenstrukturen

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.

> # Zwei Beispielvektoren


> x <- c(1, 2, 3)
> y <- c(2, 4)

> # Äußeres Vektorprodukt x y’ > # Alternative für x y’


> x %o% y > x %*% t(y)
[,1] [,2] [,1] [,2]
[1,] 2 4 [1,] 2 4
[2,] 4 8 [2,] 4 8
[3,] 6 12 [3,] 6 12

19.5 Abschluss

19.5.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. (Revision bzw. Ergänzung von Übungsaufgabe 3 auf Seite 193)


Eine Lehrveranstaltung wird von n Studierenden besucht. Es werden k Tests
geschrieben, wobei jeweils 50 Punkte erreicht werden können. Jeder Test wird
gleich gewichtet. Die Lehrveranstaltung gilt als bestanden, wenn in Summe
mindestens die Hälfte der erreichbaren Punkte erzielt wurde. Die Resultate
werden in der Matrix Pkt verwaltet. NA heißt: nicht angetreten.
Zur Illustration betrachten wir ein Beispiel mit n = 5 und k = 2.

> 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

Dein Code soll für beliebige n und beliebige k funktionieren!

a) Bestimme für jeden Test das erreichte Punktemaximum. Schließe dabei


fehlende Werte aus.
b) Ordne die Zeilen der Matrix absteigend nach der Anzahl der insgesamt
erreichten Punkte (Zeilensumme). Schließe dabei fehlende Werte aus.
c) Wir wollen für jeden Studierenden die Testleistungen absteigend sortie-
ren, also jede Zeile der Matrix Pkt absteigend sortieren.

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)

i. Welche Datenstruktur hat jeweils bei beiden Codes das Ergebnisob-


jekt für das oben gegebene Pkt?
ii. Falls eine Matrix herauskommt: Sind die Studierenden in den Zeilen
oder in den Spalten der Ergebnismatrix abgebildet?

d) Wie viele Studierende haben die Lehrveranstaltung bestanden, wenn es


ein Streichresultat gibt? Streichresultat bedeutet, dass die Leistung mit
den wenigsten erreichten Punkten aus der Wertung genommen wird. Be-
achte, dass die Anzahl der erreichbaren Punkte um 50 sinkt.
236 D Datenstrukturen

2. Wir betrachten das Minigolfbeispiel aus diesem Kapitel.

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

Dort wollen wir in diesem Kapitel hin:

• Dataframes kennenlernen und mit ihnen umgehen lernen

• die Unterschiede zu Matrizen und Listen verstehen


• die Beziehung zu apply() und sapply() vertiefen

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.

> # Die Testergebnisse


> nummer <- c(38, 82, 53, 72, 31, 59) # Matrikelnummern
> gruppe <- c("A", "B", "B", "A", "B", "A") # Testgruppe
> punkte <- c(12, 31, 17, NA, 28, 39) # Testergebnisse

Wir fragen uns unter anderem:

• 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)

Am Ende werden wir folgendes Dataframe erstellt haben:

> 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

20.1 Allgemeines zu Dataframes

20.1.1 Dataframes generieren – data.frame()

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:

data.frame(Spaltenname1 = vektor1 , Spaltenname2 = vektor2 , ...)

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.

> # Dataframe erstellen


> test <- data.frame(nummer, Gruppe = gruppe, Punkte = punkte, punkte >= 20)
> test
nummer Gruppe Punkte punkte....20
1 38 A 12 FALSE
2 82 B 31 TRUE
3 53 B 17 FALSE
4 72 A NA NA
5 31 B 28 TRUE
6 59 A 39 TRUE

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 ...

> Bestanden <- punkte >= 20


> Bestanden
[1] FALSE TRUE FALSE NA TRUE TRUE

... 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:

data.frame(..., row.names = NULL, check.rows = FALSE,


check.names = TRUE, fix.empty.names = TRUE,
stringsAsFactors = default.stringsAsFactors())
20 Dataframes 239

20.1.2 Dimension von Dataframes – nrow(), ncol(), length()

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.

20.1.3 Beschriftungen ändern – rownames(), colnames(), names()

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.

> rownames(test) # Zeilennamen


[1] "1" "2" "3" "4" "5" "6"
> colnames(test) # Spaltennamen
[1] "nummer" "Gruppe" "Punkte" "punkte....20"
> names(test) # Spaltennamen
[1] "nummer" "Gruppe" "Punkte" "punkte....20"

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", ....

> # Spalten und Zeilen umbenennen


> bool <- names(test) %in% c("nummer", "punkte....20")
> names(test)[bool] <- c("Nr", "Bestanden")
> rownames(test) <- paste0("Stud", 1:nrow(test))

> 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

20.1.4 Überblick über das Dataframe – str(), head(), tail()

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

> # Die Struktur des Dataframes betrachten


> str(test)
’data.frame’: 6 obs. of 4 variables:
$ Nr : num 38 82 53 72 31 59
$ Gruppe : chr "A" "B" "B" "A" ...
$ Punkte : num 12 31 17 NA 28 39
$ Bestanden: logi FALSE TRUE FALSE NA TRUE 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.

Infobox 8: stringsAsFactors vor Version 4.0.1

Vor Version 4.0.1 wurden Strings innerhalb von data.frame() standardmäßig


in Faktoren (siehe (24)) umgewandelt. Wurde ein R-Code vor Version 4.0.1
geschrieben und der Parameter stringsAsFactors nicht spezifiziert, so ist
der Code nicht mehr mit der aktuellen R-Version kompatibel! Sollen Faktoren
erzeugt werden, so muss man explizit stringsAsFactors = TRUE setzen!

> temp <- data.frame(Nr = nummer, Gruppe = gruppe, Punkte = punkte,


+ Bestanden = punkte >= 20, stringsAsFactors = TRUE)
> str(temp)
’data.frame’: 6 obs. of 4 variables:
$ Nr : num 38 82 53 72 31 59
$ Gruppe : Factor w/ 2 levels "A","B": 1 2 2 1 2 1
$ Punkte : num 12 31 17 NA 28 39
$ Bestanden: logi FALSE TRUE FALSE NA TRUE TRUE

Wir erkennen, dass Gruppe mit dieser Einstellung ein Faktor ist.
20 Dataframes 241

20.2 Zusammenhang mit Matrizen – as.matrix()

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.

> # Dataframe test > # Umwandlung in Matrix


> test.matrix <- as.matrix(test)
> test > test.matrix
Nr Gruppe Punkte Bestanden Nr Gruppe Punkte Bestanden
Stud1 38 A 12 FALSE Stud1 "38" "A" "12" "FALSE"
Stud2 82 B 31 TRUE Stud2 "82" "B" "31" "TRUE"
Stud3 53 B 17 FALSE Stud3 "53" "B" "17" "FALSE"
Stud4 72 A NA NA Stud4 "72" "A" NA NA
Stud5 31 B 28 TRUE Stud5 "31" "B" "28" "TRUE"
Stud6 59 A 39 TRUE Stud6 "59" "A" "39" "TRUE"

> length(test.matrix) > mode(test.matrix)


[1] 24 [1] "character"

Die Matrixversion test.matrix besteht aus 6 · 4 = 24 Elementen, im Gegensatz


zur Dataframeversion test, die aus 4 Elementen (Spalten) besteht. Prickelnd ist
der Umstand, dass eine Matrix nur einen Mode verwalten kann. Da der Variablen
Gruppe eine textuelle Information zugrunde liegt, wandelt as.matrix() alle Einträge
in Strings um und erzeugt eine Matrix des Modes character (vgl. (11)).

20.3 Zugriff auf Zeilen und Spalten: Subsetting

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.

20.3.1 Zugriff auf Spalten/Variablen

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.

> # Zugriff mit [[ ]] > # Zugriff mit $-Operator


> test[["Punkte"]] > test$Punkte
[1] 12 31 17 NA 28 39 [1] 12 31 17 NA 28 39

> # Selektion mit [, ]


> test[, "Punkte"]
[1] 12 31 17 NA 28 39
242 D Datenstrukturen

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!

> # Zugriff mit [ ] > # Zugriff mit [ , , drop = FALSE]


> test["Punkte"] > test[, "Punkte", drop = FALSE]
Punkte Punkte
Stud1 12 Stud1 12
Stud2 31 Stud2 31
Stud3 17 Stud3 17
Stud4 NA Stud4 NA
Stud5 28 Stud5 28
Stud6 39 Stud6 39

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.

> # Zugriff mit [ ] > # Zugriff mit [ , , drop = FALSE]


> test[, c("Gruppe", "Punkte"),
> test[c("Gruppe", "Punkte")] + drop = FALSE]
Gruppe Punkte Gruppe Punkte
Stud1 A 12 Stud1 A 12
Stud2 B 31 Stud2 B 31
Stud3 B 17 Stud3 B 17
Stud4 A NA Stud4 A NA
Stud5 B 28 Stud5 B 28
Stud6 A 39 Stud6 A 39

20.3.2 Zugriff auf Zeilen/Beobachtungen

Zeilen eines Dataframes zu selektieren funktioniert wie bei Matrizen.


Beispiel: Selektiere all jene Tests (jene Zeilen) von test, deren Gruppe "B" ist, bei
denen mindestens 25 Punkte erreicht wurden und die Person anwesend war.

> test[test$Gruppe == "B" & test$Punkte >= 25 & !is.na(test$Punkte), ]


Nr Gruppe Punkte Bestanden
Stud2 82 B 31 TRUE
Stud5 31 B 28 TRUE

Wir brauchen uns (fast) keine Gedanken über Umwandlungen zu machen. Es kommt
immer ein Dataframe heraus, solange wir mindestens 2 Spalten selektieren.

> test[1, ] # Eine Zeile selektieren


Nr Gruppe Punkte Bestanden
Stud1 38 A 12 FALSE
20 Dataframes 243

20.3.3 Flexibler Zugriff – subset()

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.

> subset(test, subset = Gruppe == "A" & Punkte < 20,


+ select = c("Nr", "Gruppe", "Punkte"))
Nr Gruppe Punkte
Stud1 38 A 12

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).

> subset(test, subset = Gruppe == "A" & (Punkte < 20 | is.na(Punkte)),


+ select = c("Nr", "Gruppe", "Punkte"))
Nr Gruppe Punkte
Stud1 38 A 12
Stud4 72 A NA

20.4 Manipulation von Dataframes

20.4.1 Variablen anfügen und löschen – cbind(), NULL, list(NULL)

Mit folgenden Varianten können wir einem Dataframe eine Variable hinzufügen:

data.frame$name <- inhalt


data.frame[name ] <- inhalt

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.

data.frame <- cbind(data.frame, name1 = inhalt1 , name2 = inhalt2 , ...)

Wollen wir aus einem Dataframe eine oder mehrere Spalten löschen, so bieten
sich folgende (auf NULL basierende) Varianten an:

data.frame$name <- NULL


data.frame[namen ] <- list(NULL)

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.

> # 1.) Punkte des zweiten Antritts bestimmen


> # Jene Studierende herausgreifen, die antreten dürfen
> bool <- test$Punkte < 20 | is.na(test$Punkte)
> bool
[1] TRUE FALSE TRUE TRUE FALSE FALSE

> # Punkte des zweiten Antritts mit NA initialisieren


> punkte2 <- rep(NA, nrow(test))
> punkte2
[1] NA NA NA NA NA NA

> # Punkte simulieren und ersetzen


> punkte2[bool] <- sample(0:40, size = sum(bool), replace = TRUE)
> punkte2
[1] 30 NA 14 13 NA NA
20 Dataframes 245

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 ;-)

> # 2.) Zweiten Antritt anhängen und Spalte Punkte umbenennen


> test$Pkt2 <- punkte2
> names(test)[names(test) == "Punkte"] <- "Pkt1"
> test
Nr Gruppe Pkt1 Bestanden Pkt2
Stud1 38 A 12 FALSE 30
Stud2 82 B 31 TRUE NA
Stud3 53 B 17 FALSE 14
Stud4 72 A NA NA 13
Stud5 31 B 28 TRUE NA
Stud6 59 A 39 TRUE NA

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)

> # 1.) Pkt bestimmen


> pkt <- pmax(test$Pkt1, test$Pkt2, na.rm = TRUE)

> # 2.) Note berechnen


> note <- pmin(pmax(ceiling(8 - pkt / 40 * 8), 1), 5)

> # Beide Variablen anhängen


> test <- cbind(test, Pkt = pkt, Note = note)
> test
Nr Gruppe Pkt1 Bestanden Pkt2 Pkt Note
Stud1 38 A 12 FALSE 30 30 2
Stud2 82 B 31 TRUE NA 31 2
Stud3 53 B 17 FALSE 14 17 5
Stud4 72 A NA NA 13 13 5
Stud5 31 B 28 TRUE NA 28 3
Stud6 59 A 39 TRUE NA 39 1

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.

> # Spalte Bestanden löschen


> test["Bestanden"] <- list(NULL)
> # Alternative
> # test$Bestanden <- NULL
246 D Datenstrukturen

Unser Dataframe test nach allen Manipulationen.

> 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

20.4.2 Zeilen und Spalten umordnen

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 ;-)

> test[order(test$Gruppe, -test$Pkt), ]


Nr Gruppe Pkt1 Pkt2 Pkt Note
Stud6 59 A 39 NA 39 1
Stud1 38 A 12 30 30 2
Stud4 72 A NA 13 13 5
Stud2 82 B 31 NA 31 2
Stud5 31 B 28 NA 28 3
Stud3 53 B 17 14 17 5

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.

20.4.3 Einträgen oder Variablen ersetzen

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[, "Pkt1"][is.na(test[, "Pkt1"])] <- 0


> test$Pkt1[is.na(test$Pkt1)] <- 0 # Alternative
20 Dataframes 247

Unser Dataframe nach der Ersetzung.

> 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

20.5 Zusammenhang mit Listen – is.list(), as.list(),


is.data.frame(), as.data.frame(), class(), unclass()

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.

> test.part <- test[c("Pkt1", "Pkt2", "Pkt")]


> test.part
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

Mit der Funktion is.data.frame() fragen wir ab, ob das Objekt ein Dataframe ist.

> is.data.frame(test.part) # Abfrage auf Dataframe


[1] TRUE
> is.list(test.part) # Ein Dataframe ist tatsächlich auch eine Liste,
[1] TRUE
> is.matrix(test.part) # aber keine Matrix.
[1] FALSE

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.

> class(test.part) # Klassenattribut abfragen


[1] "data.frame"

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!

> # Entferne das Klassenattribut


> unclass(test.part)
$Pkt1
[1] 12 31 17 0 28 39
$Pkt2
[1] 30 NA 14 13 NA NA
$Pkt
[1] 30 31 17 13 28 39
attr(,"row.names")
[1] "Stud1" "Stud2" "Stud3" "Stud4" "Stud5" "Stud6"

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.

> # Umwandlung in eine Liste > # Rückumwandlung in ein Dataframe


> test.part.liste <- > # ist problemlos möglich.
+ as.list(test.part) > as.data.frame(test.part.liste)
> test.part.liste Pkt1 Pkt2 Pkt
$Pkt1 1 12 30 30
[1] 12 31 17 0 28 39 2 31 NA 31
$Pkt2 3 17 14 17
[1] 30 NA 14 13 NA NA 4 0 13 13
$Pkt 5 28 NA 28
[1] 30 31 17 13 28 39 6 39 NA 39

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.

> # Entferne fehlende Werte in Pkt2


> temp <- test.part.liste[["Pkt2"]][!is.na(test.part.liste[["Pkt2"]])]
> test.part.liste[["Pkt2"]] <- temp

> # 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!

> # Lösche alle 0-Einträge in Pkt1


> test.part.liste$Pkt1 <- test.part.liste$Pkt1[test.part.liste$Pkt1 != 0]
> test.part.liste
$Pkt1
[1] 12 31 17 28 39
$Pkt2
[1] 30 14 13
$Pkt
[1] 30 31 17 13 28 39

> 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

Wir lernen also (erneut): Programmiere immer aufmerksam!

20.6 Funktionen revisited

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!

20.6.1 apply() und sapply() bei Dataframes

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

> # Berechne Spaltenmittelwerte mit colMeans() - so leider nicht!


> colMeans(test, na.rm = TRUE)
Fehler in colMeans(test, na.rm = TRUE) : ’x’ muss numerisch sein

Frage: Warum funktioniert das nicht?


Antwort: Die Funktion colMeans() wandelt ein Dataframe mittels as.matrix()
in eine Matrix um, bevor sie die Spaltenmittelwerte berechnet. In (20.2) haben wir
gesehen, dass dabei gegebenenfalls eine Typumwandlung vorgenommen wird (Ma-
trizen können nur einen Mode haben), und im Falle von test eine Matrix vom Mode
character entsteht – und mit Zeichenketten kann R nicht rechnen.
Probieren wir es mit apply().

> apply(test, 2, mean, na.rm = TRUE)


Warnung in mean.default(newX[, i], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Warnung in mean.default(newX[, i], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Warnung in mean.default(newX[, i], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Warnung in mean.default(newX[, i], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Warnung in mean.default(newX[, i], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Warnung in mean.default(newX[, i], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Nr Gruppe Pkt1 Pkt2 Pkt Note
NA NA NA NA NA NA

Wir sehen, dass wir nichts (Sinnvolles) sehen. Alle guten Dinge sind drei?! Probieren
wir es mit sapply().

> sapply(test, mean, na.rm = TRUE)


Warnung in mean.default(X[[i]], ...)
Argument ist weder numerisch noch boolesch: gebe NA zurück
Nr Gruppe Pkt1 Pkt2 Pkt Note
55.83333 NA 21.16667 19.00000 26.33333 3.00000

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

> # Bestimme, welche Spalten numerisch sind


> bool.numeric <- sapply(test, is.numeric)
> bool.numeric
Nr Gruppe Pkt1 Pkt2 Pkt Note
TRUE FALSE TRUE TRUE TRUE TRUE

Zunächst erfragen wir mit sapply(test, is.numeric), welche Spalten numerisch


sind. Die Variable Gruppe ist nicht numerisch, wie uns das FALSE mitteilt. Jetzt
können wir die numerischen Spalten selektieren, diese in eine weitere sapply()-
Funktion oder in colMeans() einsetzen und die Spaltenmittelwerte bestimmen.

> # Spaltenmittelwerte aller numerischen Spalten berechnen


> sapply(test[bool.numeric], mean, na.rm = TRUE)
Nr Pkt1 Pkt2 Pkt Note
55.83333 21.16667 19.00000 26.33333 3.00000
> colMeans(test[bool.numeric], na.rm = TRUE) # Funktioniert jetzt tadellos
Nr Pkt1 Pkt2 Pkt Note
55.83333 21.16667 19.00000 26.33333 3.00000

Bemerkung: Mit test[bool.numeric] würde auch die apply()-Variante funktio-


nieren. sapply() ist allerdings effizienter, da die zeitfressende Matrixumwandlung
ausbleibt.
Abschließend bemerken wir, dass der Mittelwert etwa für die Spalte Nr keinen Sinn
macht. Wenn wir nur an den Mittelwerten jener Spalten interessiert sind, die In-
formationen über erzielte Punkte (im Objekt test sind das Pkt1, Pkt2 und Pkt)
enthalten, so wäre es naheliegend, nur diese Spalten zu selektieren.
Die schlechte Nachricht: Uns fehlen noch die Techniken, um diese Spalten automa-
tisiert zu bestimmen. Die gute Nachricht: Du bekommst einen exklusiven Vorge-
schmack auf (22) und (23). Dort lernen wir allgemein, wie wir automatisiert Infor-
mationen aus Texten extrahieren können.

> # Bestimme, welche Spalten Punktinformationen enthalten


> bool.pkt <- grepl("Pkt[0-9]*", names(test))
> bool.pkt
[1] FALSE FALSE TRUE TRUE TRUE FALSE

> 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

> colMeans(test[bool.pkt], na.rm = TRUE)


Pkt1 Pkt2 Pkt
21.16667 19.00000 26.33333
252 D Datenstrukturen

20.6.2 is.na() und Vergleichsoperatoren bei Dataframes

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 ;-)

> test[bool.pkt] > is.na(test[bool.pkt])


Pkt1 Pkt2 Pkt Pkt1 Pkt2 Pkt
Stud1 12 30 30 Stud1 FALSE FALSE FALSE
Stud2 31 NA 31 Stud2 FALSE TRUE FALSE
Stud3 17 14 17 Stud3 FALSE FALSE FALSE
Stud4 0 13 13 Stud4 FALSE FALSE FALSE
Stud5 28 NA 28 Stud5 FALSE TRUE FALSE
Stud6 39 NA 39 Stud6 FALSE TRUE FALSE

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.

> test[bool.pkt] > test[bool.pkt] >= 20


Pkt1 Pkt2 Pkt Pkt1 Pkt2 Pkt
Stud1 12 30 30 Stud1 FALSE TRUE TRUE
Stud2 31 NA 31 Stud2 TRUE NA TRUE
Stud3 17 14 17 Stud3 FALSE FALSE FALSE
Stud4 0 13 13 Stud4 FALSE FALSE FALSE
Stud5 28 NA 28 Stud5 TRUE NA TRUE
Stud6 39 NA 39 Stud6 TRUE NA TRUE

> colSums(test[bool.pkt] >= 20, na.rm = TRUE)


Pkt1 Pkt2 Pkt
3 1 4

20.7 Abschluss

20.7.1 Objekte sichern

> # Daten sichern


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> # setwd(...)
> save(test, file = "Test.RData")
20 Dataframes 253

20.7.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Angelehnt an Übungsaufgabe 6 auf Seite 98. Von 10 Studierenden wurden das


Geschlecht, die Körpergröße in cm und das Gewicht in kg erfragt:

> 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?

b) Berechne den BMI und hänge ihn an daten an.


c) Selektiere alle vollständigen Zeilen von daten, also jene Zeilen, in denen
keine Angabe fehlend (NA) ist.
d) Erstelle eine Häufigkeitstabelle die angibt, wie viele Männer bzw. Frauen
keine Angaben beim Gewicht gemacht haben.
e) Berechne für jede numerische Spalte von daten die Spannweite.
f) Sortiere die Zeilen von daten absteigend nach dem Gewicht.
g) Lösche aus daten die Größe und das Gewicht idealerweise mit einem
Befehl.

2. Wir betrachten das Dataframe test aus diesem Kapitel. Du kannst den End-
stand, der auf Seite 237 abgebildet ist, wie folgt laden:

# Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben


# setwd(...)
load("Test.RData")

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.

> bool.test <- grepl("Pkt[0-9]+", names(test))


21 Dataframes verknüpfen 255

21 Dataframes verknüpfen

Dort wollen wir in diesem Kapitel hin:

• Gängige Tabellenverknüpfungsoperationen (Joins) kennenlernen

• 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)

In (21.3.1) betrachten wir abschließend eine kleine Beispieldatenbank.

21.1 Joins – merge()

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:

Inner Join Outer Join Left Join Right Join


T1 T2 T1 T2 T1 T2 T1 T2
Eva 3 5 Ben 2 NA Ben 2 NA Eva 3 5
Eva 3 5 Eva 3 5 Jan NA 4
Jan NA 4
© 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_21
256 D Datenstrukturen

Die jeweilige Tabelle enthält beim

• Inner Join alle Zeilen, deren Schlüssel in beiden Tabellen vorkommen.


• Outer Join alle Zeilen, deren Schlüssel in mind. einer Tabelle vorkommen.
• Left Join alle Zeilen, deren Schlüssel in der linken Tabelle vorkommen.

• 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.

merge(x, y, by = intersect(names(x), names(y)), by.x = by, by.y = by,


all = FALSE, all.x = all, all.y = all, sort = TRUE,
suffixes = c(".x",".y"), no.dups = TRUE, incomparables = NULL, ...)

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.

> # Erstelle die Dataframes


> T1 <- data.frame(Name = c("Ben", "Eva"), T1 = c(2, 3))
> T2 <- data.frame(Name = c("Eva", "Jan"), T2 = c(5, 4))

> 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:

> names(T1) > names(T2)


[1] "Name" "T1" [1] "Name" "T2"

> intersect(names(T1), names(T2))


[1] "Name"

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.

> names(T2)[1] <- "Person"


> T2
Person T2
1 Eva 5
2 Jan 4

> intersect(names(T1), names(T2))


character(0)

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.

> merge(T1, T2, all = TRUE)


Name T1 Person T2
1 Ben 2 Eva 5
2 Eva 3 Eva 5
3 Ben 2 Jan 4
4 Eva 3 Jan 4

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.

> # Outer Join


> Res.outer <- merge(T1, T2, by.x = "Name", by.y = "Person", all = TRUE)
> Res.outer
Name T1 T2
1 Ben 2 NA
2 Eva 3 5
3 Jan NA 4

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

21.2 Zeilenweise Verknüpfung – rbind()

Jetzt kommt der Lehrveranstaltungsleiter der Parallelgruppe (Gruppe 2) mit seinen


Schützlingen und ihren Resultaten.

> Res2 <- data.frame(


+ Person = c("Ina", "Tom"), T1 = c(5, 1), T2 = c(5, NA))

> # Unsere Studis > # Studis der Parallelgruppe 2


> Res.outer > Res2
Name T1 T2 Person T1 T2
1 Ben 2 NA 1 Ina 5 5
2 Eva 3 5 2 Tom 1 NA
3 Jan NA 4

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.

> rbind(Res.outer, Res2)


Fehler in match.names(clabs, names(xi)) :
Namen passen nicht zu den vorhergehenden Namen

Die Funktion rbind() verlangt, dass die Beschriftungen aller Dataframes über-
einstimmen. Man einigt sich also auf die Beschriftung Name statt Person.

> names(Res2)[names(Res2) == "Person"] <- "Name" # Beschriftung ändern

> Res.outer > Res2 > rbind(Res.outer, Res2)


Name T1 T2 Name T1 T2 Name T1 T2
1 Ben 2 NA 1 Ina 5 5 1 Ben 2 NA
2 Eva 3 5 2 Tom 1 NA 2 Eva 3 5
3 Jan NA 4 3 Jan NA 4
4 Ina 5 5
5 Tom 1 NA

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]

> Res.outer > Res2 > rbind(Res.outer, Res2)


Name T1 T2 T2 T1 Name Name T1 T2
1 Ben 2 NA 1 5 5 Ina 1 Ben 2 NA
2 Eva 3 5 2 NA 1 Tom 2 Eva 3 5
3 Jan NA 4 3 Jan NA 4
4 Ina 5 5
5 Tom 1 NA
21 Dataframes verknüpfen 259

21.3 Aus der guten Praxis

21.3.1 Fallbeispiel: Verwaltung einfacher Datenbanken

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

> # Die Tabellen mit Beispieldaten


> Stud <- data.frame(Nr = 1:4, Name = c("Fritz", "Gerda", "Hubert",
+ "Isabella"), stringsAsFactors = FALSE)

> LV <- data.frame(Nr = 11:14, Titel = c("Statistik", "Programmieren",


+ "Analysis", "Spieltheorie"), stringsAsFactors = FALSE)

> besucht <- data.frame(Student = c(1, 2, 2, 4, 4),


+ LV = c(12, 11, 12, 12, 13))

> Stud > LV > besucht


Nr Name Nr Titel Student LV
1 1 Fritz 1 11 Statistik 1 1 12
2 2 Gerda 2 12 Programmieren 2 2 11
3 3 Hubert 3 13 Analysis 3 2 12
4 4 Isabella 4 14 Spieltheorie 4 4 12
5 4 13

Wollen wir etwa die Titel jener Lehrveranstaltungen erfahren, die von Gerda besucht
werden, dann schreiben wir:

> gerda.nr <- Stud$Nr[Stud$Name == "Gerda"]


> LV$Titel[LV$Nr %in% besucht$LV[besucht$Student == gerda.nr]]
[1] "Statistik" "Programmieren"

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.

> # 1.) Gerdas LVs - mit merge()


> Stud.gerda <- Stud[Stud$Name == "Gerda", ]
> temp <- merge(Stud.gerda, besucht, by.x = "Nr", by.y = "Student")
> res <- merge(temp, LV, by.x = "LV", by.y = "Nr")

> Stud.gerda > temp > res


Nr Name Nr Name LV LV Nr Name Titel
2 2 Gerda 1 2 Gerda 11 1 11 2 Gerda Statistik
2 2 Gerda 12 2 12 2 Gerda Programmieren

> res$Titel
[1] "Statistik" "Programmieren"

Tipp: Versuche bei Datenbankselektionen die Tabellen möglichst klein zu halten!

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

21.4.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Wir betrachten die Beispieldatenbank aus (21.3.1).


Löse die noch offenen Aufgaben 2 bis 5.
E Tools für Data Science und
Statistik
Daniel Obszelka
264 E Tools für Data Science und Statistik

22 Textmanipulation: Stringfunktionen

Dort wollen wir in diesem Kapitel hin:

• in die Welt der Textmanipulation eintauchen


• Zeichenketten zusammenfügen, zerlegen, ersetzen
• in Zeichenketten suchen, Teilbereiche von Zeichenketten extrahieren

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.

> # Die Daten dieses Kapitels


> # Ggf. Arbeitsverzeichnis wechseln oder Pfad angeben
> objekte <- load("Pizza.RData")
> objekte
[1] "pizza" "namen" "zutaten" "preise" "pizza.liste"

> 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.

> pizza.liste # Ziel: Beschriftete Liste mit den Zutaten


$Margherita
[1] "Tomaten" "Kaese"
$Valentino
[1] "Tomaten" "Kaese" "Champignons" "Rohschinken" "Ananas"
$Cardinale
[1] "Tomaten" "Kaese" "Schinken"
$Provinciale
[1] "Tomaten" "Kaese" "Schinken" "Speck" "Mais"
[6] "Pfefferoni"

> preise # Ziel: Die Preise der Pizzen als Zahlen


[1] 6.0 7.5 7.5 7.9

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

Um unser Ziel zu erreichen, stellen wir uns folgende Zwischenfragen:

• 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)

• Wie wandeln wir die Preise in Zahlen um? (22.5)


• Wie erstellen wir aus den Zutaten eine Liste? (22.6)
Davor führen wir in (22.1) einige einfache und sehr nützliche Funktionen ein und in
(22.4) erfahren wir, dass es einige Zeichen gibt, die besonderer Zuwendung bedürfen,
wenn wir explizit nach ihnen suchen wollen.

22.1 Simple Stringfunktionen – nchar(), tolower(), toupper()

Wir betrachten in Tab. 22.1 zunächst ein paar einfache Stringfunktionen.

Tabelle 22.1: Einfache Stringfunktionen

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.

> # Vektor mit unschön geschriebenen Pizzanamen


> namen <- c("MArgherita", "valentino", "CARDINALE", "Provinciale")

Schreibe alle Namen zur Gänze in Großbuchstaben sowie in Kleinbuchstaben.

> # Generelle Groß- und Kleinschreibung


> namen.upper <- toupper(namen) # in Großbuchstaben umwandeln
> namen.lower <- tolower(namen) # in Kleinbuchstaben umwandeln

> 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)?

> # Länge der Namen bestimmen = Anzahl der Zeichen zählen


> namen.nchar <- nchar(namen.lower)
> namen.nchar
[1] 10 9 9 11

> # Alle längsten Namen extrahieren


> namen.lower[namen.nchar == max(namen.nchar)]
[1] "provinciale"

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.

> abbreviate(namen.lower, minlength = 4)


margherita valentino cardinale provinciale
"mrgh" "vlnt" "crdn" "prvn"

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.

22.2 Nach Mustern in Texten suchen

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

22.2.1 Elemente des Vektors finden – grepl(), grep()

Um herauszufinden ob die bzw. welche Vektorkomponenten ein Suchmuster enthal-


ten, bietet uns R die Funktionen grepl() und grep() an.1

grepl(pattern, x, ignore.case = FALSE, perl = FALSE,


fixed = FALSE, useBytes = FALSE)

grep(pattern, x, ignore.case = FALSE, perl = FALSE, value = FALSE,


fixed = FALSE, useBytes = FALSE, invert = FALSE)

Beide Funktionen unterscheiden sich im Wesentlich nur durch ihre Ausgabe.


• grepl() gibt uns einen logischen Vektor (das "l" zum Schluss steht für
logical) zurück; dabei heißt TRUE, dass das entsprechende Element des String-
vektors das Suchmuster enthält.
• grep() hingegen gibt uns standardmäßig die Indizes jener Elemente des
Stringvektors zurück, welche das gesuchte Suchmuster enthalten. Wenn wir
value = TRUE setzen, werden die entsprechenden Elemente zurückgegeben.
Bevor wir uns in einige Beispiele stürzen und die Möglichkeiten ausloten, schauen
wir uns in Tab. 22.2 ausgewählte Parameter an. Den Parameter perl schauen wir
uns dann noch in (23.6.2) kurz an.

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.

Beispiel: Wie viele Pizzen enthalten Mais?

> 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"

1 grep steht übrigens für global regular expression print.


268 E Tools für Data Science und Statistik

> # Möglichkeit 1: mit grepl() - liefert TRUE/FALSE-Vektor


> bool.mais <- grepl(pattern = "Mais", x = pizza)
> bool.mais
[1] FALSE FALSE FALSE TRUE
> sum(bool.mais)
[1] 1

> # Möglichkeit 2: mit grep() - liefert Vektor mit Indizes


> ind.mais <- grep(pattern = "Mais", x = pizza)
> ind.mais
[1] 4
> length(ind.mais)
[1] 1

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.

> grep(pattern = "Mais", x = pizza, value = TRUE)


[1] "Provinciale: Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni. 7,90 Euro"
> grep(pattern = "Mais", x = pizza, value = TRUE, invert = TRUE)
[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"

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"

> !grepl("Schinken", pizza)


[1] TRUE TRUE FALSE FALSE
> grep("Schinken", pizza, value = TRUE, invert = TRUE)
[1] "MArgherita: Tomaten, Kaese. 6,00 Euro"
[2] "valentino: Tomaten, Kaese, Champignons, Rohschinken, Ananas. 7,50 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:

1. Wir schreiben alles in Kleinbuchstaben und suchen nach schinken.

> bool <- grepl(pattern = "schinken", x = tolower(pizza))


> bool
[1] FALSE TRUE TRUE TRUE

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

In allen drei Fällen kommt dasselbe heraus.

> 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!

22.2.2 Stellen in Texten finden – regexpr(), gregexpr()

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

regexpr(pattern, text, ignore.case = FALSE, perl = FALSE,


fixed = FALSE, useBytes = FALSE)

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"

> # Stelle des ersten Auftretens von Schinken


> ind.schinken <- regexpr(pattern = "Schinken", text = pizza)
> ind.schinken
[1] -1 -1 28 30
attr(,"match.length")
[1] -1 -1 8 8
.....

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().

> grepl("Schinken", pizza)


[1] FALSE FALSE TRUE TRUE
> regexpr("Schinken", pizza) > 0 # Nachbau von grepl()
[1] FALSE FALSE TRUE TRUE
22 Textmanipulation: Stringfunktionen 271

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"

> # Die Stellen aller Beistriche


> stellen.beistrich <- gregexpr(",", pizza)
> head(stellen.beistrich, n = 2)
[[1]]
[1] 20 30
attr(,"match.length")
[1] 1 1
attr(,"index.type")
[1] "chars"
attr(,"useBytes")
[1] TRUE
[[2]]
[1] 19 26 39 52 63
attr(,"match.length")
[1] 1 1 1 1 1
attr(,"index.type")
[1] "chars"
attr(,"useBytes")
[1] TRUE

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.

22.2.3 Anzahl der Übereinstimmungen zählen

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:

> sapply(stellen.beistrich, length)


[1] 2 5 3 6

Das funktioniert hier zwar korrekt, aber leider nicht immer.


272 E Tools für Data Science und Statistik

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.

> gregexpr(",", "Text ohne Beistrich")


[[1]]
[1] -1
attr(,"match.length")
[1] -1
.....

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.

> sapply(gregexpr(",", "Text ohne Beistrich"), length)


[1] 1

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))

> temp <- lapply(stellen.beistrich, ">", 0)


> temp
[[1]]
[1] TRUE TRUE
[[2]]
[1] TRUE TRUE TRUE TRUE TRUE
[[3]]
[1] TRUE TRUE TRUE
[[4]]
[1] TRUE TRUE TRUE TRUE TRUE TRUE

> # Wie viele Beistriche pro Eintrag?


> sapply(temp, sum)
[1] 2 5 3 6

Wenn wir uns lediglich für die Gesamtanzahl der Beistriche interessieren, können
wir auch kürzer folgendes schreiben:

> # Wie viele Beistriche insgesamt?


> sum(unlist(stellen.beistrich) > 0)
[1] 16

gregexpr() gibt wirklich immer eine Liste zurück, selbst dann, wenn das Suchmus-
ter in jeder Komponente gleich oft vorkommt.
22 Textmanipulation: Stringfunktionen 273

22.3 Extraktion von Teilen aus Zeichenketten – substring()

Mit der Funktion substring() können wir Teile von Strings extrahieren.

substring(text, first, last = 1000000)

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!

> # Die ersten fünf Zeichen > # Vektorwertige Anwendung


> substring(pizza, 1, 5) > substring(pizza, 1, 2:5)
[1] "MArgh" "valen" "CARDI" "Provi" [1] "MA" "val" "CARD" "Provi"

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

> # 1.) Stelle des ersten Doppelpunktes finden


> stelle.doppelpunkt <- regexpr(pattern = ":", pizza)
> stelle.doppelpunkt
[1] 11 10 10 12
attr(,"match.length")
[1] 1 1 1 1
.....

> 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.

> all(stelle.doppelpunkt > 0) > range(stelle.doppelpunkt)


[1] TRUE [1] 10 12

Alle Einträge von pizza enthalten (zumindest einen) Doppelpunkt und die Stellen
des jeweils ersten Doppelpunktes sehen plausibel aus. Sollte also passen.

> # 2.) Teilbereich bis exklusive des ersten Doppelpunktes extrahieren


> substring(pizza, first = 1, last = stelle.doppelpunkt)
[1] "MArgherita:" "valentino:" "CARDINALE:" "Provinciale:"

Hoppala! Den Doppelpunkt wollen wir nicht haben, wir korrigieren.

> namen <- substring(pizza, first = 1, last = stelle.doppelpunkt - 1)


> namen
[1] "MArgherita" "valentino" "CARDINALE" "Provinciale"

> # 3.) Namen richtig schreiben


> namen.anfang <- toupper(substring(namen, 1, 1))
> namen.rest <- tolower(substring(namen, 2, nchar(namen)))

> namen.anfang # 1. Zeichen groß


[1] "M" "V" "C" "P"
> namen.rest # Rest klein
[1] "argherita" "alentino" "ardinale" "rovinciale"

> namen <- paste0(namen.anfang, namen.rest)


> namen
[1] "Margherita" "Valentino" "Cardinale" "Provinciale"

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!

> regexpr(pattern = ".", pizza)


[1] 1 1 1 1
attr(,"match.length")
[1] 1 1 1 1
.....

Warum an erster Stelle? Erfahren wir gleich in (22.4) mehr!


22 Textmanipulation: Stringfunktionen 275

22.4 Sonderzeichen mit besonderen Fähigkeiten – [ ], "\\"

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.

> # Explizite Suche nach dem Punkt


> stelle.punkt <- regexpr("\\.", pizza) # doppelten Backslash voranstellen
> stelle.punkt <- regexpr("[.]", pizza) # in eckige Klammern setzen
> stelle.punkt
[1] 27 60 36 63
attr(,"match.length")
[1] 1 1 1 1
.....

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 ;-)

> substring(pizza, stelle.punkt)


[1] ". 6,00 Euro" ". 7,50 Euro" ". 7,50 Euro" ". 7,90 Euro"

> # Hoppala, 2 Zeichen zu Beginn zu viel ...


> substring(pizza, stelle.punkt + 2)
[1] "6,00 Euro" "7,50 Euro" "7,50 Euro" "7,90 Euro"

> # ... 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.

> # Extrahiere das erste Vorkommen einer Dezimalzahl


> regmatches(pizza, regexpr("[0-9]+,[0-9]+", pizza))
[1] "6,00" "7,50" "7,50" "7,90"

22.5 Ersetzungen in Zeichenketten – sub(), gsub()

Mit sub() können wir ein Muster durch ein anderes ersetzen.

sub(pattern, replacement, x, ignore.case = FALSE, perl = FALSE,


fixed = FALSE, useBytes = FALSE)

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:

> # Erstes A durch X ersetzen > # Alle A durch X ersetzen


> sub("A", "X", x = "ABCABC") > gsub("A", "X", x = "ABCABC")
[1] "XBCABC" [1] "XBCXBC"

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

Die direkte Umwandlung in einen numerischen Vektor scheitert, da R keine Dezimal-


zahlen mit Komma kennt. Wir müssen davor also den Beistrich durch einen Punkt
ersetzen.

> preise <- sub(pattern = ",", replacement = ".", preise)


> preise <- as.numeric(preise)
> preise
[1] 6.0 7.5 7.5 7.9

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.

> sub(pattern = ",", replacement = "[.]", x = "5,9")


[1] "5[.]9"
22 Textmanipulation: Stringfunktionen 277

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

Fehlen noch die Zutaten, denen wir uns jetzt widmen!


Beispiel: Extrahiere die Zutaten aus pizza. Am Ende sollen sie lediglich durch
Beistriche getrennt sein.

> 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

> # 1.) Teilstring zwischen Doppelpunkt und Punkt extrahieren


> zutaten <- substring(pizza, stelle.doppelpunkt + 1, stelle.punkt - 1)
> zutaten
[1] " Tomaten, Kaese"
[2] " Tomaten, Kaese, Champignons, Rohschinken, Ananas"
[3] " Tomaten, Kaese, Schinken"
[4] " Tomaten, Kaese, Schinken, Speck, Mais, Pfefferoni"

Jetzt wollen wir alle Leerzeichen löschen. Ein Fall für gsub(); sub() würde nur das
erste Leerzeichen löschen.

> # 2.) Alle Leerzeichen löschen


> zutaten <- gsub(pattern = " ", replacement = "", zutaten)
> zutaten
[1] "Tomaten,Kaese"
[2] "Tomaten,Kaese,Champignons,Rohschinken,Ananas"
[3] "Tomaten,Kaese,Schinken"
[4] "Tomaten,Kaese,Schinken,Speck,Mais,Pfefferoni"

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

22.6 Zerlegung von Zeichenketten – strsplit()

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"

Das Zerlegen/Aufspalten von Zeichenketten bewerkstelligen wir mittels strsplit().


strsplit() ist die Umkehrfunktion zu paste().

strsplit(x, split, fixed = FALSE, perl = FALSE, useBytes = FALSE)

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.

> # Bei jedem Komma splitten


> pizza.liste <- strsplit(zutaten, split = ",")
> pizza.liste
[[1]]
[1] "Tomaten" "Kaese"
[[2]]
[1] "Tomaten" "Kaese" "Champignons" "Rohschinken" "Ananas"
[[3]]
[1] "Tomaten" "Kaese" "Schinken"
[[4]]
[1] "Tomaten" "Kaese" "Schinken" "Speck" "Mais"
[6] "Pfefferoni"

Jetzt noch zu den Beschriftungen. Dazu verwenden wir einfach die Namen, die wir
in (22.3) erstellt haben.
22 Textmanipulation: Stringfunktionen 279

> # Beschriftungen hinzufügen


> names(pizza.liste) <- namen
> pizza.liste
$Margherita
[1] "Tomaten" "Kaese"
$Valentino
[1] "Tomaten" "Kaese" "Champignons" "Rohschinken" "Ananas"
$Cardinale
[1] "Tomaten" "Kaese" "Schinken"
$Provinciale
[1] "Tomaten" "Kaese" "Schinken" "Speck" "Mais"
[6] "Pfefferoni"

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ß!

22.7 Zusammenfassung der Stringfunktionen

Tab. 22.3 fasst die Stringfunktionen zusammen.

Tabelle 22.3: Zusammenfassung der Stringfunktionen

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

22.8.1 Objekte sichern

> # Daten sichern


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> save(pizza, namen, zutaten, preise, pizza.liste, file = "Pizza.RData")

22.8.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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)

• Wie zerlegen wir Zeichenketten nach einem Splitmuster? Welche Datenstruktur


gibt uns die besprochene Funktion immer zurück? Wie zerlegen wir einen Text
in seine einzelnen Zeichen? Was müssen wir tun, wenn wir einen Text nach
Sonderzeichen splitten wollen? (22.6)
22 Textmanipulation: Stringfunktionen 281

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.")

a) Zähle die Anzahl der Wörter in jedem Eintrag von text.


b) Bestimme die durchschnittliche Wortlänge in jedem Eintrag von text.
Beachte dabei, dass Satzzeichen nicht zu einem Wort gehören.
c) Erstelle eine lückenlose Häufigkeitstabelle, die angibt, wie oft jeder Buch-
stabe von A bis Z insgesamt in text vorkommt. Groß- und Kleinbuchsta-
ben sollen dabei zusammengefasst werden.

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:

dateien <- c("Version_2.1.txt", "Liste_2019.08.01.xlsx", "Run.R")

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)

Kontrolliere, ob deine Alternative auch wirklich dasselbe tut!


23 Komplexe Textsuchmuster: Regular Expressions 283

23 Komplexe Textsuchmuster: Regular Expressions

Dort wollen wir in diesem Kapitel hin:

• noch tiefer in die Welt der Stringmanipulation eintauchen

• flexible Suchmuster definieren


• die richtige Behandlung von Sonderzeichen verstehen

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.

> namen <- c("Adam", "Ana", "Anna", "Annabelle", "Anna-Maria",


+ "Anne", "Aurelia", "Elena", "Eugen", "Ida",
+ "Freia", "Maaouiya", "Marie-Anne", "Otto", "Renee")
> namen
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"
[6] "Anne" "Aurelia" "Elena" "Eugen" "Ida"
[11] "Freia" "Maaouiya" "Marie-Anne" "Otto" "Renee"

Wir fragen uns unter anderem:


• Wie viele Selbstlaute kommen in jedem Namen vor? (23.2.2)
• Welche Namen enthalten Ana oder Anna? Welche Namen enthalten mindestens
zwei aufeinanderfolgende Selbstlaute? (23.3)
• Welche Namen enthalten genau zwei aufeinanderfolgende Selbstlaute (Voka-
le)? (23.3.1), (23.5.3)
• Welche Namen haben zwei Mal in Folge einen Selbstlaut gefolgt von einem
Mitlaut? (23.5.1)
• Welche Namen enthalten Doppelbuchstaben? (23.5.2)

• 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

23.1 Sonderzeichen und Escape-Befehle – ?regex, [ ], \\, \n, \"

Regular Expressions bieten eine Möglichkeit, nach flexibleren Mustern zu suchen.


Die unten genannten Sonderzeichen, die wir schon aus (22.4) kennen, erfüllen hierbei
einen ganz bestimmten Zweck; in den Parameter pattern eingebettet, entfalten sie
wundersame Möglichkeiten.

. \ | ( ) [ { ^ $ * + ?
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

• dem Sonderzeichen entweder zwei Backslashes voranstellen oder


• das Sonderzeichen in eckige Klammern setzen.

> # Explizite Suche nach dem (ersten) Punkt


> index.p <- regexpr(pattern = "\\.", "R 4.0.2") # doppelter Backslash
> index.p <- regexpr(pattern = "[.]", "R 4.0.2") # mit eckigen Klammern

> 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:

1. Der Backslash "\" markiert in R einen Escape-Befehl. Das sind spezielle


Befehle zur Formatierung von Textausgaben. Der Befehl \n etwa steht für einen
Zeilenumbruch (neue Zeile). Wir lernen in (31) beim Thema Bildschirmausgabe
mehr darüber.
Für uns ist im Moment wichtig, dass wir in R zwei Backslashes brauchen, um
im Text einen Backslash darzustellen und somit vier Backslashes benötigen,
um effektiv nach einem Backslash im Text zu suchen. Klingt lustig, ist es auch.
2. Das Potenzzeichen "^" hat innerhalb der eckigen Klammern eine Negations-
funktion, wenn es an erster Stelle steht ((23.2.2)). Es darf also innerhalb der
eckigen Klammern nicht an erster Stelle stehen, wenn wir explizit nach diesem
Zeichen suchen wollen.
3. Das Minuszeichen "-" darf in eckigen Klammern nicht zwischen zwei anderen
Zeichen stehen. Den Grund dafür lernen wir in (23.2.3).
23 Komplexe Textsuchmuster: Regular Expressions 285

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?

> # Anführungszeichen in Strings


> "Hugo "Nimmersatt" Mustermann"
Fehler: unerwartetes Symbol in ""Hugo "Nimmersatt"

Das funktioniert so nicht, weil wir ja die Anführungszeichen zur Definition eines
Strings brauchen. Ein vorangestellter Backslash schafft Abhilfe!

> "Hugo \"Nimmersatt\" Mustermann"


[1] "Hugo \"Nimmersatt\" Mustermann"

> # Text auf die Console drucken


cat("Hugo \"Nimmersatt\" Mustermann\n")
Hugo "Nimmersatt" Mustermann

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!

> nchar(c("\"", "\\")) # jeweils 1 Zeichen!!


[1] 1 1

So, jetzt aber zu unseren Codebeispielen.

> # 1.) Escapebefehle und Backslashes


> text <- c("Der Escapebefehl \\n beginnt eine neue Zeile.",
+ "Der Escapebefehl \n beginnt eine neue Zeile.")
> grepl("\n", text) # Kommt der Escape-Befehl \n vor?
[1] FALSE TRUE
> grepl("\\\\n", text) # Kommt die Zeichenkette \n vor?
[1] TRUE FALSE

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).

> text.collapse <- paste0(text, "\n", collapse = "")


> cat(text.collapse) # für \n wird neue Zeile eingefügt, für \\n nicht.
Der Escapebefehl \n beginnt eine neue Zeile.
Der Escapebefehl
beginnt eine neue Zeile.

Im zweiten Text wird \n durch einen Zeilenumbruch ersetzt.


286 E Tools für Data Science und Statistik

> # 2.) Stelle des ersten Potenzzeichens suchen


> rechnung <- "3 + 2 ^ 4 - 1 = 18"
> rechnung
[1] "3 + 2 ^ 4 - 1 = 18"

> regexpr("[^]", rechnung) # Potenzzeichen suchen - so nicht!


Fehler in regexpr("[^]", rechnung) :
ungültiger regulärer Ausdruck ’[^]’, Grund ’Missing ’]’’
> regexpr("\\^", rechnung) # Potenzzeichen suchen - so geht’s!
[1] 7
.....

23.2 Zeichenmengen und flexible Suchmuster definieren

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"

> # Alle Namen, die ein großes A enthalten


> grep("A", namen, value = TRUE)
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"
[6] "Anne" "Aurelia" "Marie-Anne"

> # Alle Namen, die Anne enthalten


> grep("Anne", namen, value = TRUE)
[1] "Anne" "Marie-Anne"

Beachte, dass wir nicht nach Namen gesucht haben, die mit A oder Anne beginnen!
Marie-Anne bestätigt das ;-)

Wir beginnen jetzt damit, unsere Techniksammlung zu erweitern!

23.2.1 Beliebiges Zeichen – "."

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

23.2.2 Zeichen ein- und ausschließen – [ ], [ˆ]

Oft wollen wir nach einem Zeichen aus einer Zeichenmenge suchen. Das be-
werkstelligen wir, indem wir diese Zeichen in eckige Klammern setzen.

> # Suche nach Ann gefolgt von einem a oder e


> grep("Ann[ae]", namen, value = TRUE)
[1] "Anna" "Annabelle" "Anna-Maria" "Anne" "Marie-Anne"

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.

> # Großer Selbstlaut gefolgt von beliebigem Zeichen gefolgt von a


> grep("[AEIOU].a", namen, value = TRUE)
[1] "Adam" "Ana" "Ida"

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.

> # Selbstlaute zählen


> anz.vokal <- gregexpr("[aeiou]", namen, ignore.case = TRUE)
> sapply(lapply(anz.vokal, ">", 0), sum)
[1] 2 2 2 4 5 2 5 3 3 2 3 6 5 2 3

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"

Alternativ hätten wir zum Beispiel auch gregexpr("[AEIOUaeiou]", namen) schrei-


ben können, um die Groß- und Kleinschreibung zu ignorieren.
288 E Tools für Data Science und Statistik

Wir hätten diese Frage auch schon mit den Techniken des vorangehenden Kapitels
(22) beantworten können:

> # Selbstlaute zählen: Alternative


> temp1 <- strsplit(tolower(namen), "")
> temp2 <- lapply(temp1, "%in%", c("a", "e", "i", "o", "u"))
> sapply(temp2, sum)
[1] 2 2 2 4 5 2 5 3 3 2 3 6 5 2 3

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-Selbstlaute zählen


> anz <- gregexpr("[^aeiou]", namen, ignore.case = TRUE)
> sapply(lapply(anz, ">", 0), sum)
[1] 2 1 2 5 5 2 2 2 2 1 2 2 5 2 2
> head(namen, n = 5)
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"

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.

23.2.3 Zeichenbereiche definieren – [A-Z], [a-z], [0-9]

Angenommen, wir wollen in einem Text nach beliebigen Großbuchstaben suchen.


Vielleicht ist dir jetzt auch folgende Codezeile in den Sinn gekommen:

> # Suchmuster für alle Großbuchstaben zusammenbauen


> paste0("[", paste0(LETTERS, collapse = ""), "]")
[1] "[ABCDEFGHIJKLMNOPQRSTUVWXYZ]"

Für bestimmte Zeichenkonstellationen gibt es elegante Kurzschreibweisen:


• [A-Z]: ein beliebiger Großbuchstabe
• [a-z]: ein beliebiger Kleinbuchstabe

• [A-Za-z]: ein beliebiger (groß oder klein geschriebener) Buchstabe


• [0-9]: eine beliebige Ziffer zwischen 0 und 9.
23 Komplexe Textsuchmuster: Regular Expressions 289

> # Haben alle Namen (mindestens) einen Großbuchstaben?


> all(grepl("[A-Z]", namen))
[1] TRUE

Ja! Dank [A-Z] haben wir eine fingerschonende Antwortmöglichkeit.

Wir können aber auch die Zeichenbereiche abschneiden.


Beispiel: An welchen Stellen stehen in rechnung Ziffern größer oder gleich 3?

> 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.

23.3 Matchlänge steuern

Angenommen, wir wollen all jene Namen selektieren, die drei aufeinanderfolgende
Vokale haben. Dann ginge es beispielsweise wie folgt.

> grep("[aeiou][aeiou][aeiou]", namen, ignore.case = TRUE, value = TRUE)


[1] "Freia" "Maaouiya"

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!

23.3.1 Matchlängenoperatoren und Kürzel – { }, +, *, ?

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

Zusätzlich gibt es drei Kürzel für bestimmte Fälle:

• +: mindestens 1 Mal (entspricht {1,})


• *: mindestens 0 Mal (entspricht {0,})
• ?: mindestens 0 und höchstens 1 Mal (entspricht {0,1})

Beispiel: Suche nach allen Namen, die Ana oder Anna enthalten.

> grep("An{1,2}a", namen, value = TRUE)


[1] "Ana" "Anna" "Annabelle" "Anna-Maria"
> grep("Ann?a", namen, value = TRUE)
[1] "Ana" "Anna" "Annabelle" "Anna-Maria"

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?

> # Mindestens zwei aufeinanderfolgende Vokale?


> grep("[aeiou]{2,}", namen, ignore.case = TRUE, value = TRUE)
[1] "Anna-Maria" "Aurelia" "Eugen" "Freia" "Maaouiya"
[6] "Marie-Anne" "Renee"

> # Alternativen, die zum selben Ergebnis führen


> grep("[aeiou][aeiou]+", namen, ignore.case = TRUE, value = TRUE)
> grep("[aeiou][aeiou][aeiou]*", namen, ignore.case = TRUE, value = TRUE)

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:

> grep("[aeiou]{2}", namen, ignore.case = TRUE, value = TRUE)


[1] "Anna-Maria" "Aurelia" "Eugen" "Freia" "Maaouiya"
[6] "Marie-Anne" "Renee"

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

23.3.2 Greedy Matching unterbinden – ?

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.

> text <- "x--x--x--x"


> text
[1] "x--x--x--x"

> # Greedy Matching (default) > # Greedy Matching abschalten


> regexpr("x.+x", text) > regexpr("x.+?x", text)
[1] 1 [1] 1
attr(,"match.length") attr(,"match.length")
[1] 10 [1] 4
..... .....

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.

23.4 Teilstrings extrahieren – regmatches()

Angenommen, wir wollten aus folgendem String alle Zahlen herausfiltern:

> 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.

regmatches(x, m, invert = FALSE)

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.

> # Suchmuster für Zahlen definieren


> pattern.zahl <- "-?[0-9]+[.]?[0-9]*"

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.

> stelle.zahl <- regexpr(pattern.zahl, x) # Suche Positionen der Zahlen

Schritt 3: Den Output von regexpr() setzen wir in regmatches() ein.

> res.zahl <- regmatches(x, stelle.zahl) # Extrahiere die Zahlen


> res.zahl
[1] "-20" "20.25" "-4.5"

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")

> # Zahlen ohne Datumswerte extrahieren - Fehlschlag Nummer 1


> stelle.zahl <- regexpr(pattern.zahl, x.datum)
> regmatches(x.datum, stelle.zahl)
[1] "-20" "04.01" "20.25" "-4.5"

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.

> # Zahlen ohne Datumswerte extrahieren - Fehlschlag Nummer 2


> stelle.zahl <- regexpr("-?[0-9]+[.]?[0-9]*[^.]", x.datum)
> regmatches(x.datum, stelle.zahl)
[1] "-20%" "04.01" "20.25 " "-4.5 "

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?

> regmatches(x.datum, stelle.zahl)


[1] "-20%" "04.01" "20.25 " "-4.5 "

R versucht, eine möglichst lange Übereinstimmung zu finden. Würde R aber für


[0-9]* sowohl 0 als auch 1 einsetzen, dann bekäme R ein Problem mit dem darauf
folgenden Punkt (jenem vor 2019): Er ist weder eine Ziffer noch kein Punkt. Damit
also überhaupt eine Übereinstimmung gefunden wird, muss [0-9]* auf ein Zeichen
(0) reduziert werden. Dann wird die 1 bereits für [ˆ.] eingesetzt und die Überein-
stimmung endet. Der zweite Punkt im Datum kommt nicht mehr zur Geltung.

Regel 15: Achte auf korrekte Suchmuster

Wenn du aus einem Text automatisiert ganz bestimmte Informationen extra-


hieren möchtest, dann muss das Suchmuster flexibel genug sein, damit alle
gewünschten Teile extrahiert werden, aber auch starr genug, dass keine un-
erwünschten Teile mitextrahiert werden.

Das Auffinden eines korrekten Suchmusters ist oft herausfordernd! Im Data-Mining


bzw. Text-Mining haben wir es zum Teil mit mehreren Tausend (oder gar Millio-
nen) Datenzeilen zutun. Tippfehler und Formatfehler ausfindig zu machen und zu
korrigieren, kann aufwändig sein. Hier benötigen wir viel Geduld und Erfindergeist.
In Übung 6 auf Seite 308 entwickelst du einen Code, der aus x.datum tatsächlich
nur die Zahlen extrahiert.
294 E Tools für Data Science und Statistik

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"

> stellen.zahl <- gregexpr(pattern.zahl, x)


> regmatches(x, stellen.zahl)
[[1]]
[1] "-20"
[[2]]
[1] "20.25"
[[3]]
character(0)
[[4]]
[1] "-4.5"

Wir sehen, dass jene Stellen, an denen das Suchmuster nicht vorkommt, mit einem
leeren Vektor des Typs character markiert werden.

23.5 Nützliche Tools zum Ersten

23.5.1 Zeichengruppen definieren – ( )

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.

> # Ist das weder Fisch noch Fleisch?


> x <- c("Fisch", "Fleisch")
> grep("F(le)?isch", x, value = TRUE)
[1] "Fisch" "Fleisch"

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?

> grep("([aeiou][bcdfghjklmnpqrstvwxyz]){2}", namen, ignore.case = TRUE,


+ value = TRUE)
[1] "Adam" "Annabelle" "Aurelia" "Elena" "Eugen"

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

Bemerkung: Zeichengruppen können leider nicht mit dem Negationsoperator "ˆ"


ausgeschlossen werden.

> # Fisch und Fleisch ausschließen! - so nicht!


> grep("^(F(le)?isch)", c("Fisch", "Fleisch"), value = TRUE)
[1] "Fisch" "Fleisch"

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 ;-)

23.5.2 Zeichen und Zeichengruppen verodern – |

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"

> # Fisch oder Fleisch?


> x[grepl("Fisch", x) | grepl("Fleisch", x)]
[1] "Fisch" "Fleisch"

Die Oder-Verknüpfung funktioniert aber auch innerhalb von Suchmustern, was uns
deutlich kürzere Schreibweisen ermöglicht!

> grep("Fisch|Fleisch", x, val = TRUE) > grep("F(le|)isch", x, val = TRUE)


[1] "Fisch" "Fleisch" [1] "Fisch" "Fleisch"

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.

2. einen Doppelbuchstaben haben.


296 E Tools für Data Science und Statistik

> # 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.

> # 2.) Welche Namen enthalten einen Doppelbuchstaben?


> pattern <- paste(letters, letters, sep = "", collapse = "|")
> grep(pattern, namen, ignore.case = TRUE, value = TRUE)
[1] "Anna" "Annabelle" "Anna-Maria" "Anne" "Maaouiya"
[6] "Marie-Anne" "Otto" "Renee"

23.5.3 Stringanfang und Stringende – "ˆ", "$"

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.

> # Welche Namen enthalten ein großes A?


> grep("A", namen, value = TRUE)
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"
[6] "Anne" "Aurelia" "Marie-Anne"

> # Welche Namen fangen mit einem großen A an?


> grep("^A", namen, value = TRUE)
[1] "Adam" "Ana" "Anna" "Annabelle" "Anna-Maria"
[6] "Anne" "Aurelia"

Im zweiten Code setzen wir einen Anker, der den Stringanfang matcht. Damit fällt
Marie-Anne durch das Suchmuster.

> # Welche Namen fangen nicht mit einem Selbstlaut an?


> grep("^[^AEIOU]", namen, value = TRUE)
[1] "Freia" "Maaouiya" "Marie-Anne" "Renee"

Das erste Potenzzeichen steht für den Stringanfang, das zweite in den eckigen Klam-
mern erfüllt die in (23.2.2) gelernte Negationsfunktion.

> # Welche Namen enden nicht auf einem Selbstlaut?


> grep("[^aeiou]$", namen, value = TRUE)
[1] "Adam" "Eugen"

Hier markieren wir mit dem Dollarzeichen das Stringende.


23 Komplexe Textsuchmuster: Regular Expressions 297

Beispiel: Welche Namen haben genau zwei aufeinanderfolgende Selbstlaute?

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.

> # Die Suchmuster zu den vier Fällen


> pattern1 <- "[^aeiou][aeiou]{2}[^aeiou]"
> pattern2 <- "^[aeiou]{2}[^aeiou]"
> pattern3 <- "[^aeiou][aeiou]{2}$"
> pattern4 <- "^[aeiou]{2}$"

> # Veroderung der vier Suchmuster


> pattern <- paste(pattern1, pattern2, pattern3, pattern4, sep = "|")

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.

> # Namen extrahieren


> grep(pattern, namen, ignore.case = TRUE, value = TRUE)
[1] "Anna-Maria" "Aurelia" "Eugen" "Marie-Anne" "Renee"

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.

23.5.4 Strings trimmen und White Space – [:space:]

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.

Betrachten wir ein einfaches Codebeispiel.

> # Beispieltext mit Leerzeichen und Rufzeichen


> text <- c(" Hugo lacht! ", " Haha! Haha! ")
> text
[1] " Hugo lacht! " " Haha! Haha! "
298 E Tools für Data Science und Statistik

> # Alle Leerzeichen löschen > # Leerzeichen am Rand löschen


> gsub(" ", "", text) > gsub("^ *| *$", "", text)
[1] "Hugolacht!" "Haha!Haha!" [1] "Hugo lacht!" "Haha! Haha!"

Der Nachteil: Etwaige Tabulatoren werden dabei nicht entfernt. Um White Space zu
matchen, stellt uns R die Zeichenmenge [:space:] zur Verfügung.

> # White Space zu Beginn und am Ende löschen


> gsub("^[[:space:]]*|[[:space:]]*$", "", text)
[1] "Hugo lacht!" "Haha! Haha!"

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.

> # White Space und Rufzeichen zu Beginn und am Ende löschen


> gsub("^[[:space:]!]*|[[:space:]!]*$", "", text)
[1] "Hugo lacht" "Haha! Haha"

23.6 Nützliche Tools zum Zweiten

Wir betrachten weitere fortgeschrittenere nützliche Tools, mit denen wir noch viel
mächtiger werden!

23.6.1 Vorherige Zeichengruppe erneut matchen – \\k

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 :-)

> x <- c("Haha", "haha", "hiha", "hihi", "Aha")


> x
[1] "Haha" "haha" "hiha" "hihi" "Aha"

> # 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!

> grep("ha\\1\\2", x, value = TRUE) # Bei Zeichen keine Rückreferenz!


Fehler in grep("ha\\1\\2", x, value = TRUE) :
ungültiger regulärer Ausdruck .ha\1\2’, Grund .Invalid back reference’
> grep("(h)(a)\\1\\2", x, value = TRUE) # Muss Zeichen in Gruppe stecken!
[1] "haha"

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"

> # Verdopple alle Selbstlaute


> gsub("([aeiou])", "\\1\\1", x, ignore.case = TRUE)
[1] "Haahaa" "haahaa" "hiihaa" "hiihii" "AAhaa"

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.

> gsub("([aeiou])", "\\1\\1", x, ignore.case = FALSE)


[1] "Haahaa" "haahaa" "hiihaa" "hiihii" "Ahaa"

Stürzen wir uns ins Namensgetümmel!


Beispiel: Welche Namen haben an 1. und 3. Stelle denselben Buchstaben? Die
Groß- und Kleinschreibung soll dabei ignoriert werden.

> # 1. und 3. Buchstabe gleich - So nicht!


> grep("^([a-z]).\\1", namen, ignore.case = TRUE, value = TRUE)
character(0)

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.

> # 1. und 3. Buchstabe gleich - So schon! # Kleinschreibweise


> grep("^([a-z]).\\1", tolower(namen), value = TRUE)
[1] "adam" "ana" "elena"
> namen[grepl("^([a-z]).\\1", tolower(namen))] # Originalschreibweise
[1] "Adam" "Ana" "Elena"

Wir wandeln einfach alle Buchstaben mittels tolower(namen) in Kleinbuchstaben


um. Wollen wir die Namen in der ursprünglichen Schreibweise (mit Großbuchstaben)
haben, so selektieren wir die Namen zum Beispiel mittels grepl() aus namen.
300 E Tools für Data Science und Statistik

23.6.2 Bedingte Extraktion: Lookaheads und Lookbehinds – perl, (?= ),


(?! ), (?<= ), (?<! )

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.

> # 2. Variante: Lookaheads nehmen


> temp <- regexpr("[0-9]+(?= *(g|kg))", masse, perl = TRUE)
> temp
[1] 1 1 1 -1 -1
attr(,"match.length")
[1] 2 2 1 -1 -1
.....

> regmatches(masse, temp)


[1] "50" "30" "7"
23 Komplexe Textsuchmuster: Regular Expressions 301

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.

Tabelle 23.1: Lookaheads und Lookbehinds im Überblick

Syntax Name Bedeutung


X(?=ABC) positiver Lookahead ABC muss X folgen
X(?!ABC) negativer Lookahead ABC darf X nicht folgen
(?<=ABC)X positiver Lookbehind ABC muss X voranstehen
(?<!ABC)X negativer Lookbehind ABC darf X nicht voranstehen

Schauen wir uns noch kurz einen negativen Lookahead an – Tücke inklusive.

> # Extrahiere alle Zahlen, die keine cm beschreiben - so noch nicht!


> regmatches(masse, regexpr("[0-9]+(?! *(cm))", masse, perl = TRUE))
[1] "50" "30" "7" "50" "2"

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:

> # Extrahiere alle Zahlen, die keine cm beschreiben - so geht es!


> regmatches(masse, regexpr("[0-9]+(?![0-9]* *(cm))", masse, perl = TRUE))
[1] "50" "30" "7" "50"

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

23.6.3 Überlappende Suche – perl, (?= )

Lookaheads sind notwendig, um eine überlappende Suche durchzuführen.


Stürzen wir uns zunächst in ein Codebeispiel! Wir oft kommt im String ababa das
Muster aba vor?

> x <- "ababa" > x <- "ababa"


> x > x
[1] "ababa" [1] "ababa"

> # Nicht überlappende Suche > # Überlappende Suche


> gregexpr("aba", x) > gregexpr("a(?=ba)", x, perl = TRUE)
[[1]] [[1]]
[1] 1 [1] 1 3
attr(,"match.length") attr(,"match.length")
[1] 3 [1] 1 1
..... .....

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?

> # 1.) Ohne Überlappungen


> temp1 <- gregexpr("[aeiou][^aeiou][aeiou]", namen, ignore.case = TRUE)
> res1 <- sapply(lapply(temp1, ">", 0), sum)
> res1
[1] 1 1 0 1 1 0 1 1 1 1 0 1 2 0 1

> # 2.) Mit Überlappungen (Lookahead)


> temp2 <- gregexpr("[aeiou](?=[^aeiou][aeiou])", namen, ignore.case = TRUE,
+ perl = TRUE)
> res2 <- sapply(lapply(temp2, ">", 0), sum)
> res2
[1] 1 1 0 1 1 0 2 2 1 1 0 1 2 0 1

Wir erkennen an zwei Stellen ein unterschiedliches Zählergebnis.

> namen[res1 != res2] > namen[res1 != res2] > namen[res1 != res2]


[1] "Aurelia" "Elena" [1] "Aurelia" "Elena" [1] "Aurelia" "Elena"

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

23.7 Aus der guten Praxis

23.7.1 Fallbeispiel: Stationsansagen der Wiener Linien

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.

Infobox 9: 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:

[Haltestellenname]. Umsteigen zu: [U-Bahn-Linien],


[Straßenbahnlinien], [Autobuslinien]

Wir erkennen eine


• U-Bahn-Linie an einem U gefolgt von einer Ziffer (z. B. U1).
• Autobuslinie an einer Zahl gefolgt von einem A oder B (z. B. 5B, 94A).
• Straßenbahnlinie an einer Zahl oder genau einem Großbuchstaben
(z. B. 43, D).

Ein Sonderfall ist WLB (Wiener Lokal Bahn), die eine eigene Kategorie dar-
stellt. Schnellbahnen, Regionalbusse und weitere Details vernachlässigen wir.

Wir laden die Stationsansagen der U-Bahn-Linie U2 (Stand August 2019).

> # Ggf. Arbeitsverzeichnis wechseln


> # setwd(...)
> objekte <- load("U2.RData")
> objekte
[1] "stationen" "umstieg" "ansagen"

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"

Unsere Studentin möchte Antworten auf folgende Fragen bekommen:


1. Bei welchen Stationen können wir zu keinem Autobus umsteigen?
2. Zu wie viele Autobuslinien können wir bei jeder Station umsteigen?
3. In welchen Stationen können wir zu mindestens einer U-Bahn-Linie umsteigen?
4. In wie viele Linien können wir allgemein bei jeder Station umsteigen?

5. Bei welchen Stationen können wir zur Straßenbahnlinie 2 umsteigen?


6. Wie viele Straßenbahnlinien halten bei jeder Station?
7. Wie viele unterscheidbare Straßenbahnlinien halten bei Stationen der U2?

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

> # Alternative: Erstelle eine beschriftete Liste


> umstieg.liste <- strsplit(umstieg, " *, *") # Nach Komma trennen
> names(umstieg.liste) <- stationen # Liste beschriften
> head(umstieg.liste, n = 4)
$Karlsplatz
[1] "U1" "U4" "D" "1" "2" "62" "71" "2A" "4A" "59A" "WLB"
$Museumsquartier
[1] "57A"
$Volkstheater
[1] "U3" "46" "49" "71" "48A"
$Rathaus
[1] "2"

> # Die Autobuslinien in jeder Listenkomponente extrahieren


> temp <- sapply(umstieg.liste, grep, pattern = pattern.bus, value = TRUE)
> head(temp, n = 4)
$Karlsplatz
[1] "2A" "4A" "59A"
$Museumsquartier
[1] "57A"
$Volkstheater
[1] "48A"
$Rathaus
character(0)

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.

> # Länge jeder Komponente bestimmen


> anz.bus <- sapply(temp, length)
> head(anz.bus, n = 4)
Karlsplatz Museumsquartier Volkstheater Rathaus
3 1 1 0

23.8 Abschluss

23.8.1 Zusammenfassung der Sonderzeichen und der Syntax

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

Tabelle 23.2: Zusammenfassung der Syntax bei Regular Expressions

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

23.8.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Der Vektor rechnungen enthält einige einfache Rechnungen:

rechnungen <- c("9 + 10 = 19, 2 - 5 = -3,11^2 = 121, 8-7=1")

a) Wie viele Plus-Zeichen kommen vor?


b) Wie oft kommen die Ziffern 3, 4 und 5 in Summe vor?
c) Wie oft kommt als Ergebnis eine Zahl ≥ 0 heraus?
d) Wie oft lautet das Rechenergebnis 1?
e) Wie oft kommen die Zahlen 10, 12, ..., 20 vor?

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?

b) Welche Namen bestehen aus nur einem unterscheidbaren Selbstlaut?


c) Wie viele Namen bestehen aus mind. 3 unterschiedlichen Selbstlauten?
d) Welche Namen beginnen und enden mit dem gleichen Buchstaben?

Versuche, jeweils mehrere Lösungsmöglichkeiten zu finden.


4. Schreibe einen Code, der aus einer Menge von Wörtern all jene selektiert, die
genau zwei Selbstlaute in Folge haben und darüber hinaus keine Folge aus drei
oder mehr Selbstlauten besitzen.
Teste deinen Code mit folgendem Beispielvektor:

> wort <- c("Augenbraue", "Auto", "Baum", "Ei", "Eiertanz",


+ "Feierlaune", "Igel", "Pfau")

> # Gewünschtes Ergebnis


> res
[1] "Auto" "Baum" "Ei" "Pfau"

5. Welche (reellen) Zahlen werden mit folgendem Suchmuster nicht gefunden?

pattern.zahl <- "-?[0-9]+[.]?[0-9]+"

6. Schreibe einen Code, der aus folgendem Text alle Zahlen, aber keine Datums-
werte selektiert.

x.datum <- c("Heute -20%", "Samstag, der 04.01.2019",


"Viele Meter laufen", "Er hat 20.25 Punkte.", "Kalte -4.5 Grad")

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."

Lese aus wettertext automatisiert alle Datumsangaben sowie Gradangaben


heraus und erstelle daraus ein Dataframe mit den beiden Spalten Datum und
Temperatur.
8. Wir betrachten das Fallbeispiel (23.7.1) zu den Wiener Linien ab Seite 303.
Beantworte die Fragen 3 bis 7 der Studentin.
23 Komplexe Textsuchmuster: Regular Expressions 309

9. Gegeben ist ein beliebiger Stringvektor x. Sind folgende Behauptungen richtig


oder falsch? Begründe jeweils deine Antwort und gebe ggf. ein Gegenbeispiel
an, das die Behauptung widerlegt.
a) grep("a[ˆa]*a", x, value = TRUE) selektiert all jene Elemente von x,
die genau zwei a enthalten.
b) grepl("ˆ.{5}", x) und nchar(x) >= 5 liefern dasselbe Ergebnis.
c) grepl(".+[aeiou]$", x) liefert genau dann TRUE, wenn das entspre-
chende Element von x auf einen kleinen Selbstlaut endet.
d) grepl("[aeiou](.*\\1)*", tolower(x)) liefert genau dann TRUE, wenn
das entsprechende Element von x aus genau einem unterscheidbaren Selbst-
laut besteht.
e) Wenn wir alle Vorkommen eines Suchmusters suchen, kann es keine Über-
lappungen geben, wenn alle Zeichen des Musters unterschiedlich sind.
10. Ein Gen ist (als Nichtfachmann gesagt) eine Folge von Basen, bestehend aus
Adenin (A), Cytosin (C), Thymin (T) und Guanin (G). Ein Beispielobjekt mit
2 Genen zu je 50 Basen:

gene <- c("CCTACCGCTATGCCGTACCTTTATATCAGGTCGCGTGATGACGGTCGGAC",


"CAACTCAATACTGTCCCCATCCTGTTGGGACTGGATTACTGACCGCTAAG")

Generiere zufällig n = 10 Gene mit jeweils k = 100 Basen als Stringvektor


(analog zum Beispielobjekt). Beantworte sodann folgende Fragen für jedes Gen:
a) Wie oft kommt das Muster ACT vor?
b) Wie oft kommt das Muster TAT vor?
c) Wie oft kommt jede Base vor?
d) Wie oft kommen nach jeder Base zumindest vier andere Basen?
Hinweis: Denke an etwaige Überlappungen und mache ggf. Gebrauch von
Lookaheads ;-)
Dein Code soll auch für andere n und k funktionieren!
310 E Tools für Data Science und Statistik

24 Kategorielle Variablen: Faktoren

Dort wollen wir in diesem Kapitel hin:

• Faktoren kennenlernen und mit ihnen umgehen lernen


• einige Gefahren im Zusammenhang mit Faktoren erkennen
• Variablen kategorisieren
• erste Einblicke im Einlesen von Daten sammeln

Wir betrachten in diesem Kapitel die Datei Vertreter.txt. Den vollständigen Da-
tensatz in der Editoransicht finden wir in Abb. 24.1.

Abbildung 24.1: Der vollständige Datensatz Vertreter.txt in der Editoransicht

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:

• Gebiet: 1 = West, 2 = Nord, 3 = Ost, 4 = Süd


• Ausbildung: 1 = Matura (Abitur), 2 = Lehre

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

Wir stellen uns unter anderem folgende Fragen:

• Wie verwalten wir in R sinnvoll die beiden kategoriellen Variablen Gebiet und
Ausbildung? (24.1)

• Wie können wir den Gewinn kategorisieren? (24.2)


• Wie fassen wir Gewinnkategorien im Nachhinein zusammen? Wie teilen wir R
mit, wenn wir manche Vertreter in ein neues Gebiet verlegen? (24.3)

Faktoren stellen eine elegante Möglichkeit dar, kategorielle Variablen zu verwalten.


Lernen wir sie also im Laufe des Kapitels näher kennen! Dabei lösen wir viele Stan-
dardardaufgaben aus der Praxis. Wenn wir Faktoren verstanden und verdaut haben,
schauen wir in (24.5) einige Tücken im Zusammenhang mit Faktoren an!
Schon in (24.0) erhaschen wir außerdem einen kurzen Einblick darin, wie wir einfache
Textdateien in R einlesen können.

24.0 Die Daten des Kapitels einlesen – read.table()

Wir lesen jetzt den Datensatz mit Hilfe von read.table() ein.

> # Einlesen der Datei "Vertreter.txt"


> # Arbeitsverzeichnis in jenen Ordner wechseln, in dem Vertreter.txt ist.
> # setwd(...)
> daten <- read.table(file = "Vertreter.txt", header = TRUE, dec = ",")

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.

> # Überblick über die Daten


> str(daten) # Struktur des Dataframes
’data.frame’: 24 obs. of 4 variables:
$ ï..Nummer : int 1 2 3 4 5 6 7 8 9 10 ...
$ Gebiet : int 2 3 1 3 4 1 4 3 4 2 ...
$ Ausbildung: int 1 1 1 2 1 2 2 2 2 2 ...
$ Gewinn : num 11.4 9.2 9.3 13.6 8.9 3.3 2.1 0.2 10.7 16.7 ...

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.

> # Bereinige Beschriftung der 1. Variable


> names(daten)[1] <- substring(names(daten)[1], 4)
312 E Tools für Data Science und Statistik

> # 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.

24.1 Faktoren: Sinn, Generierung und Verwaltung

24.1.1 Codierung von Ausprägungen

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

Abbildung 24.2: Codierung im Datensatz Vertreter.txt Links: Codierung für


Gebiet. Rechts: Codierung für Ausbildung.

Frage: Warum verwenden wir in der Praxis (fast immer) Nummerncodes?


Der Grund ist einfach: Würden wir statt der Nummerncodes die Ausprägungsnamen
verwenden, bräuchten wir mehr Speicherplatz! Bei Gebiet braucht sich R nur 24 × 1
Zeichen (Codes) und 4 Ausprägungsnamen merken, anstatt 24 Ausprägungsnamen.
Wir können uns das wie in einem Verzeichnis vorstellen: Wir verwalten die Daten
in Form von speicherschonenden Codes und blättern bei Bedarf im Verzeichnis die
entsprechende Beschriftung nach. Faktoren setzen diese Idee 1-zu-1 um.
Beachte: R verwendet immer die Codes von 1 bis Anzahl der Ausprägungen! Das
Beispiel in Abb. 24.3 zeigt die (internen) Konsequenzen. Anwendungstechnisch be-
trachtet hat diese interne Umcodierung jedoch keine nennenswerten Konsequenzen
für uns, da die Reihenfolge dieselbe bleibt.
24 Kategorielle Variablen: Faktoren 313

Echte Codierung Codierung in R


Code Beschriftung Code Beschriftung
-1 Weiß nicht 1 Weiß nicht
0 Nein 2 Nein
1 Ja 3 Ja
9 Fehlender Wert 4 Fehlender Wert

Abbildung 24.3: Umcodierung bei der Faktorbildung in R. Links: Ursprüngliche


Codierung. Rechts: Codierung nach der Faktorbildung.

24.1.2 Faktoren generieren – factor()

Faktoren können wir mit der Funktion factor() generieren. Der vereinfachte Aufruf
(ein paar weitere Parameter schauen wir uns noch weiter unten an):

factor(x, levels, labels)

Dabei ist x der zugrunde liegende Datenvektor.


Mit levels beeinflussen wir die Codierungsreihenfolge (1. Eintrag von levels
bekommt Code 1, der zweite den Code 2 usw.). Standardmäßig werden alle unter-
scheidbaren Werte in x sortiert (entspricht im Prinzip sort(unique(x))). Kommen
gewisse Elemente von x nicht in levels vor, so entstehen NA-Werte.
Mit labels geben wir die gewünschten Ausprägungsnamen an, die den levels
zugeordnet werden. Übergeben wir nichts, wird labels = levels gesetzt.
Beispiel: Weise den Variablen Gebiet und Ausbildung einen Faktor mit den in
Abb. 24.2 angegebenen Ausprägungen zu.

> # Faktoren bilden und auf die Variablen zuweisen


> daten$Ausbildung <- factor(daten$Ausbildung, labels = c("Matura", "Lehre"))
> daten$Gebiet <- factor(daten$Gebiet,
+ labels = c("West", "Nord", "Ost", "Süd"))

> 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.

> sort(unique(daten$Gebiet)) > sort(unique(daten$Ausbildung))


[1] 1 2 3 4 [1] 1 2

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!

24.1.3 Interne Verwaltung – unclass(), mode(), is.factor()

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

> # Offenlegung der internen Verwaltung


> 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"

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.

> mode(daten$Gebiet) # numerischer Mode...


[1] "numeric"
> is.numeric(daten$Gebiet) # aber nicht numerisch,
[1] FALSE
24 Kategorielle Variablen: Faktoren 315

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.

> # Abfrage auf Faktor


> is.factor(daten$Gebiet)
[1] TRUE

24.1.4 Zugriff auf die Ausprägungen – levels(), nlevels()

Mit levels() können wir auf die Ausprägungsnamen eines Faktors zugreifen.
Mit nlevels() bestimmen wir die Anzahl der Ausprägungen eines Faktors.

> # Die Ausprägungen abfragen > # Anzahl der Ausprägungen abfragen


> levels(daten$Gebiet) > nlevels(daten$Gebiet)
[1] "West" "Nord" "Ost" "Süd" [1] 4

24.1.5 Faktoren und Strings – as.character()

Möchten wir einen Faktor in einen Stringvektor umwandeln, so haben wir zwei
Möglichkeiten:

> # Umwandlung in einen Stringvektor - Möglichkeit 1


> as.character(daten$Gebiet)
[1] "Nord" "Ost" "West" "Ost" "Süd" "West" "Süd" "Ost" "Süd" "Nord"
[11] "West" "Süd" "West" "West" "Nord" "Nord" "Süd" "Ost" "Süd" "West"
[21] "Nord" "Ost" "Nord" "Ost"

Eine etwas effizientere Möglichkeit ist die folgende:

> 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"

> # Umwandlung in einen Stringvektor - Möglichkeit 2


> levels(daten$Gebiet)[daten$Gebiet]
[1] "Nord" "Ost" "West" "Ost" "Süd" "West" "Süd" "Ost" "Süd" "Nord"
[11] "West" "Süd" "West" "West" "Nord" "Nord" "Süd" "Ost" "Süd" "West"
[21] "Nord" "Ost" "Nord" "Ost"

Hier werden die Einträge aus levels(daten$Gebiet) in jener Reihenfolge selektiert,


wie in daten$Gebiet vorgegeben. Der 1. Eintrag von daten$Gebiet ist 2. R blättert
im Attribut levels nach und findet dort an 2. Stelle die Beschriftung "Nord". Der
2. Eintrag ist 3. R findet an 3. Stelle in levels die Beschriftung "Ost" usw.
316 E Tools für Data Science und Statistik

Intern ist also daten$Gebiet ein numerischer Vektor mit Einträgen von 1 bis 4.

Möchten wir umgekehrt einen Stringvektor in einen Faktor umwandeln, so


nehmen wir die Funktion factor().

> # Zeichenkette generieren und auf x zuweisen


> gebiet.string <- levels(daten$Gebiet)[daten$Gebiet]
> gebiet.string
[1] "Nord" "Ost" "West" "Ost" "Süd" "West" "Süd" "Ost" "Süd" "Nord"
[11] "West" "Süd" "West" "West" "Nord" "Nord" "Süd" "Ost" "Süd" "West"
[21] "Nord" "Ost" "Nord" "Ost"

> 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

24.1.6 Codierungsreihenfolge bestimmen mit levels

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:

> # Faktor erzeugen und Codierungsreihenfolge steuern


> factor(gebiet.string, levels = c("West", "Nord", "Ost", "Süd"))
[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

Alternativ können wir statt gebiet.string auch factor(gebiet.string) überge-


ben. Wir haben also in levels die Codierungsreihenfolge definiert.
24 Kategorielle Variablen: Faktoren 317

Hierbei müssen wir aufpassen, dass wir alle unterscheidbaren Elemente des
Datenvektors aufschreiben, da ansonsten NA-Werte auftreten.

> factor(gebiet.string, levels = "Nord")


[1] Nord <NA> <NA> <NA> <NA> <NA> <NA> <NA> <NA> Nord <NA> <NA> <NA> <NA>
[15] Nord Nord <NA> <NA> <NA> <NA> Nord <NA> Nord <NA>
Levels: Nord

24.2 Kategorisierung von numerischen Variablen

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

Tabelle 24.1: Gewinnkategorien beim Vertreterbeispiel

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.

24.2.1 Bedingungen aufsummieren

Eine Möglichkeit, den Gewinnen die gewünschte Kategorie zuzuordnen wäre es, von
Indikatorfunktionen Gebrauch zu machen:

> # Welche Einträge der Variable Gewinn erfüllen die Bedingungen?


> gr5 <- daten$Gewinn > 5
> gr10 <- daten$Gewinn > 10
> gr15 <- daten$Gewinn > 15

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.

> # Je mehr Bedingungen erfüllt werden, desto höher die Kategorie


> gewinn.indikator <- 1 + gr5 + gr10 + gr15
318 E Tools für Data Science und Statistik

> # Zur Kontrolle:


> head(cbind(Gewinn = daten$Gewinn, Kategorie = gewinn.indikator), n = 6)
Gewinn Kategorie
[1,] 11.4 3
[2,] 9.2 2
[3,] 9.3 2
[4,] 13.6 3
[5,] 8.9 2
[6,] 3.3 1

Sieht gut aus! Nun vollenden wir das Ganze, indem wir Beschriftungen hinzufügen.

> # gewinn.indikator in Faktor verwandeln und Ausprägungen benennen


> stufen <- c("wenig", "moderat", "viel", "Goldgrube")
> daten$Gewinnstufe <- factor(gewinn.indikator, labels = stufen)

> 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

Coole Idee, aber mühsam. Kommen wir zur bequemeren Variante.

24.2.2 Kategorisierung mit cut()

Leichter geht es mit der Funktion cut(), welche numerische Werte zu Interval-
len zusammenfasst und uns einen Faktor zurückgibt. Der Funktionsaufruf:

cut(x, breaks, labels = NULL,


include.lowest = FALSE, right = TRUE, dig.lab = 3,
ordered_result = FALSE, ...)

In Tab. 24.2 beschreiben wir kurz einige Parameter. ordered_result verstehen wir,
nachdem wir (24.4) gelesen haben.

In unserem Beispiel würde das Ganze so aussehen:

> punkte <- c(-Inf, 5, 10, 15, Inf)


> gewinn.cut <- cut(daten$Gewinn, breaks = punkte)
> gewinn.cut
[1] (10,15] (5,10] (5,10] (10,15] (5,10] (-Inf,5] (-Inf,5]
[8] (-Inf,5] (10,15] (15, Inf] (10,15] (5,10] (5,10] (5,10]
[15] (5,10] (5,10] (10,15] (5,10] (5,10] (10,15] (5,10]
[22] (5,10] (10,15] (15, Inf]
Levels: (-Inf,5] (5,10] (10,15] (15, Inf]
24 Kategorielle Variablen: Faktoren 319

Tabelle 24.2: Parameter der Funktion cut()

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.

> # Gewinn kategorisieren und Variable Gewinnstufe erstellen


> gewinn.cut <- cut(daten$Gewinn, breaks = punkte, labels = stufen)
> daten$Gewinnstufe <- gewinn.cut
> 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

> # Der erstellte Faktor Gewinnstufe


> daten$Gewinnstufe
[1] viel moderat moderat viel moderat wenig wenig
[8] wenig viel Goldgrube viel moderat moderat moderat
[15] moderat moderat viel moderat moderat viel moderat
[22] moderat viel Goldgrube
Levels: wenig moderat viel Goldgrube

Beachte: Auch bei cut() entstehen NA-Werte, wenn es für Elemente aus x kein
Intervall gibt, in das sie hineinfallen.

24.3 Ausprägungen manipulieren

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

24.3.1 Verschmelzen von Faktorausprägungen

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.

> # Der Ist-Zustand der Gewinnstufe


> levels(daten$Gewinnstufe) # Die derzeitigen Kategorien
[1] "wenig" "moderat" "viel" "Goldgrube"
> nlevels(daten$Gewinnstufe) # Zähle Anzahl der Levels
[1] 4

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.

> # Kopiere die Variable Gewinnstufe


> gewinn.cut1 <- daten$Gewinnstufe
> gewinn.cut1
[1] viel moderat moderat viel moderat wenig wenig
[8] wenig viel Goldgrube viel moderat moderat moderat
[15] moderat moderat viel moderat moderat viel moderat
[22] moderat viel Goldgrube
Levels: wenig moderat viel Goldgrube

> # Ersetzung vornehmen


> gewinn.cut1[gewinn.cut1 == "Goldgrube"] <- "viel"
> 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 Goldgrube

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

Die Kategorie Goldgrube ist verschwunden.


24 Kategorielle Variablen: Faktoren 321

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!

> # Füge Goldgrube mit viel zusammen


> levels(daten$Gewinnstufe)[levels(daten$Gewinnstufe) == "Goldgrube"] <-
+ "viel"
> daten$Gewinnstufe
[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

> # Das modifizierte Dataframe


> 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

24.3.2 Umbenennung von Ausprägungen

Das Umbenennen von Ausprägungen funktioniert über die levels.


Beispiel: Benenne in der Variable Gewinnstufe des Objekts daten auf dieser Seite
die Ausprägung moderat in mittel um.

> levels(daten$Gewinnstufe)[levels(daten$Gewinnstufe) == "moderat"] <-


+ "mittel"
> 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

24.3.3 Neue Ausprägungen hinzufügen

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

Probieren wir es aus!

> # Weise den Beobachtungen die neue Kategorie Zentrum zu.


> daten$Gebiet[c(1, 2, 5, 6)] <- "Zentrum"
Warnung in ‘[<-.factor‘(‘*tmp*‘, c(1, 2, 5, 6), value = "Zentrum")
ungültiges Faktorniveau, NA erzeugt

> 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.

> # Wir machen Gebiet mit dem "Zentrum" vertraut


> levels(daten$Gebiet) <- c(levels(daten$Gebiet), "Zentrum")

> # Alternative: Neuerliche Faktorbildung


> # daten$Gebiet <- factor(daten$Gebiet,
> # levels = c(levels(daten$Gebiet), "Zentrum"))

> 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.

> daten$Gebiet[c(1, 2, 5, 6)] <- "Zentrum"


> 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

24.4 Ordinalskalierung – factor(ordered), as.ordered(),


is.ordered()

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

Beispiel: Die Variable Gewinnstufe im Objekt daten ist ordinalskaliert. Erstelle


aus dieser Variable einen ordinalskalierten Faktor.

> # 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

> # Ordinalskalierten Faktor erstellen


> daten$Gewinnstufe <- factor(daten$Gewinnstufe, ordered = TRUE)
> 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

Wir erkennen einen ordinalskalierten Faktor an den Kleiner-Zeichen, die in den


Levels zwischen den Ausprägungen stehen. Alternativ hätten wir stattdessen auch
daten$Gewinnstufe <- as.ordered(daten$Gewinnstufe) schreiben können.
Den größten Vorteil schauen wir uns im folgenden Beispiel an, denn ordinalskalierte
Faktoren erleichtern uns die Bestimmung von Häufigkeiten.
Beispiel: Wie viele Vertreter haben zumindest einen mittleren Gewinn erzielt?

> # Varianten, die auch vor diesem Abschnitt funktionieren


> sum(daten$Gewinnstufe == "mittel" | daten$Gewinnstufe == "viel")
[1] 21
> sum(daten$Gewinnstufe %in% c("mittel", "viel"))
[1] 21

> # Variante, die erst dann funktioniert ...


> daten$Gewinnstufe >= "mittel"
[1] TRUE TRUE TRUE TRUE TRUE FALSE FALSE FALSE TRUE TRUE TRUE TRUE
[13] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE

> # ... wenn der Faktor als ordered markiert ist.


> sum(daten$Gewinnstufe >= "mittel")
[1] 21

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.

> # Abfrage auf Ordinalskalierung


> is.ordered(daten$Gewinnstufe)
[1] TRUE
324 E Tools für Data Science und Statistik

24.5 Tücken im Zusammenhang mit Faktoren

Wir verlassen die Welt der Vertreter und betrachten ein paar Tücken von Faktoren.

24.5.1 Umordnen von Faktorausprägungen

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.

Note Codierung Note Codierung


Sehr gut 1 Befriedigend 1
Gut 2 Genügend 2
Befriedigend 3 Gut 3
Genügend 4 Nicht genügend 4
Nicht genügend 5 Sehr gut 5

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"

> noten.faktor <- factor(noten)


> noten.faktor
[1] Sehr gut Gut Befriedigend Genügend
[5] Nicht genügend
Levels: Befriedigend Genügend Gut Nicht genügend Sehr gut

Der Ist-Zustand: Die Levels sind offensichtlich falsch sortiert, nämlich alphabetisch.
Daher scheitert folgender Code:

> as.numeric(noten.faktor) # Keine korrekte Zuordnung


[1] 5 3 1 2 4
24 Kategorielle Variablen: Faktoren 325

Scheitern deshalb, weil folgendes hätte herauskommen sollen:


[1] 1 2 3 4 5

Wir können diesen Umstand leicht mit dem Parameter levels beheben, indem wir
levels die korrekte Reihenfolge übergeben.

> # Umwandlung in Faktor - verwende eigene Codierungsreihenfolge


> noten.faktor1 <- factor(noten.faktor, levels = c("Sehr gut", "Gut",
+ "Befriedigend", "Genügend", "Nicht genügend"), ordered = TRUE)
> noten.faktor1
[1] Sehr gut Gut Befriedigend Genügend
[5] Nicht genügend
Levels: Sehr gut < Gut < Befriedigend < Genügend < Nicht genügend

Dass Noten ordinalskaliert sind, teilen wir dem Faktor mit ordered = TRUE mit!
Jetzt werden den Noten auch die korrekten Zahlen zugeordnet:

> as.numeric(noten.faktor1) # Korrekte Zuordnung


[1] 1 2 3 4 5

24.5.2 Das as.numeric-Problem

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.

> # Der zugrundeliegende Faktor


> mahl <- factor(c(3, 3, 2, 3, 5, 5, 2))
> mahl
[1] 3 3 2 3 5 5 2
Levels: 2 3 5

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.

> falsch <- as.numeric(mahl)


> falsch
[1] 2 2 1 2 3 3 1

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

Eine Möglichkeit der Lösung in zwei Schritten:


1. Wir erzeugen einen String.
2. Diesen String wandeln wir in einen numerischen Vektor um.
Wie wir einen Faktor in eine Zeichenkette umfunktionieren können, haben wir uns
bereits in (24.1.5) angesehen.

> # 1.) Faktor in einen String umwandeln


> levels(mahl)[mahl]
[1] "3" "3" "2" "3" "5" "5" "2"

> # 2.) String in einen numerischen Vektor umwandeln


> mahl.numeric <- as.numeric(levels(mahl)[mahl])
> mahl.numeric
[1] 3 3 2 3 5 5 2

> # 2.) Alternative: etwas ineffizienter aber möglicherweise intuitiver


> as.numeric(as.character(mahl))
[1] 3 3 2 3 5 5 2

24.5.3 Lückenlose Häufigkeitstabellen revisited – table()

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:

> # table() mit numerischem Vektor > # table() mit Faktor


> mahl.numeric > mahl
[1] 3 3 2 3 5 5 2 [1] 3 3 2 3 5 5 2
Levels: 2 3 5
> table(mahl.numeric) > table(mahl)
mahl.numeric mahl
2 3 5 2 3 5
2 3 2 2 3 2

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

> # Faktor mit selbstbestimmten Kategorien von 0 bis zum Maximum


> mahl.faktor <- factor(mahl.numeric, levels = 0:max(mahl.numeric))
> mahl.faktor
[1] 3 3 2 3 5 5 2
Levels: 0 1 2 3 4 5

> 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.

24.6 Fehlende Werte als Kategorie – factor(exclude), is.na()

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.

> # Die Antworten auf die Ja/Nein-Frage


> antwort <- c("Ja", "Nein", "Ja", "Ja", NA)
> antwort
[1] "Ja" "Nein" "Ja" "Ja" NA

Fall 1: Wir möchten NA nicht als Level haben.


In dem Fall erstellen wir einen Faktor wie gehabt.

> antwort.fak1 <- factor(antwort) # entspricht factor(antwort, exclude = NA)


> antwort.fak1
[1] Ja Nein Ja Ja <NA>
Levels: Ja Nein

Hier ist NA keine Faktorausprägung und is.na() funktioniert problemlos.

> 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.

> # NAs als eigene Kategorie definieren


> antwort.fak2 <- factor(antwort, exclude = NULL)
328 E Tools für Data Science und Statistik

> 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

Um NA-Einträge in diesem Fall anzusprechen, machen wir zum Beispiel folgendes:

> # Betrachte die Levels


> levels(antwort.fak2)
[1] "Ja" "Nein" NA
> is.na(levels(antwort.fak2))
[1] FALSE FALSE TRUE

> # Zahlencode von NA herausfinden


> which(is.na(levels(antwort.fak2)))
[1] 3

> as.numeric(antwort.fak2) == which(is.na(levels(antwort.fak2)))


[1] FALSE FALSE FALSE FALSE TRUE

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.

> # Beschrifte NA mit "Keine Angabe"


> levels(antwort.fak2)[is.na(levels(antwort.fak2))] <- "Keine Angabe"
> antwort.fak2
[1] Ja Nein Ja Ja Keine Angabe
Levels: Ja Nein Keine Angabe

Jetzt ist es ganz einfach und unkompliziert auf diese Kategorie zu testen:

> antwort.fak2 == "Keine Angabe"


[1] FALSE FALSE FALSE FALSE TRUE

24.7 Abschluss

24.7.1 Objekte sichern

> # Daten sichern


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> save(daten, file = "Vertreter.RData")
24 Kategorielle Variablen: Faktoren 329

24.7.2 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. 50 Personen haben an einem Intelligenztest teilgenommen. Das Alter der Per-


sonen ist gleichverteilt in [18, 80] und die IQs (Intelligenzquotienten) sind nor-
malverteilt mit Erwartungswert 100 und Standardabweichung 15.
a) Generiere die Vektoren alter und iq nach obigen Verteilungen zufällig.
Hinweis: (9) könnte inspirieren.
b) Kategorisiere alter und iq wie folgt:

alter ≤ 30 jung iq ≤ 85 gering


30 < alter ≤ 65 mittel 85 < iq ≤ 115 mittel
65 < alter weise 115 < iq hoch

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:

> x <- c("A", "B", "C")


> faktor <- factor(x, levels = c("B", "C", "A"), labels = c(4, 2, 6))

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]

c) Wir schreiben nun folgende Codezeilen:


> faktor1 <- factor(faktor, ordered = TRUE)
> faktor1
[1] 6 4 2
Levels: 4 < 2 < 6

Offenbar sind die Levels von faktor1 nicht richtig sortiert. Erstelle einen
Faktor, in welchem die Levels richtig sortiert sind (2 < 4 < 6).

3. Bei einer Prüfung haben Studierende folgende Noten erreicht:

> noten <- c(4, 5, NA, 3, 1, 3, NA, 3)

> noten.faktor <- factor(noten, ordered = TRUE)


> noten.faktor
[1] 4 5 <NA> 3 1 3 <NA> 3
Levels: 1 < 3 < 4 < 5

Ein NA bedeutet, dass eine Person nicht angetreten ist.

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

25 Aggregation und Kreuztabellen

Dort wollen wir in diesem Kapitel hin:

• Funktionen getrennt nach Gruppen anwenden

• 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.

> # Daten laden


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> objekte <- load("Vertreter.RData")
> objekte
[1] "daten"

> # Die Variablen des Vertreterdatensatzes


> names(daten)
[1] "Nummer" "Gebiet" "Ausbildung" "Gewinn" "Gewinnstufe"

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.

Wir fragen uns unter anderem:

• 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

> # Die Vertreterdaten


> daten
Nummer Gebiet Ausbildung Gewinn Gewinnstufe
1 1 Zentrum Matura 11.4 viel
2 2 Zentrum Matura 9.2 mittel
3 3 West Matura 9.3 mittel
4 4 Ost Lehre 13.6 viel
5 5 Zentrum Matura 8.9 mittel
6 6 Zentrum Lehre 3.3 wenig
7 7 Süd Lehre 2.1 wenig
8 8 Ost Lehre 0.2 wenig
9 9 Süd Lehre 10.7 viel
10 10 Nord Lehre 16.7 viel
11 11 West Matura 11.8 viel
12 12 Süd Matura 7.8 mittel
13 13 West Lehre 7.4 mittel
14 14 West Lehre 7.1 mittel
15 15 Nord Matura 9.4 mittel
16 16 Nord Lehre 7.1 mittel
17 17 Süd Matura 12.2 viel
18 18 Ost Matura 8.1 mittel
19 19 Süd Lehre 7.9 mittel
20 20 West Matura 10.5 viel
21 21 Nord Matura 7.1 mittel
22 22 Ost Lehre 6.4 mittel
23 23 Nord Lehre 10.4 viel
24 24 Ost Matura 24.3 viel

Abbildung 25.1: Das Dataframe mit den Vertreterdaten

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.

25.1 Funktionen auf Gruppen anwenden: Teil 1 – tapply()

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.

tapply(X, INDEX, FUN = NULL, ..., simplify = TRUE)

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

Tabelle 25.1: Parameter der Funktion tapply()

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 <- factor(c("Mann", "Frau", "Frau", "Mann", "Frau", "Mann"))


> groesse <- c(180, 165, 175, 170, NA, 190)

> geschlecht
[1] Mann Frau Frau Mann Frau Mann
Levels: Frau Mann
> groesse
[1] 180 165 175 170 NA 190

Der folgende Code ist alles andere als elegant.

> mean(groesse[geschlecht == "Mann"], na.rm = TRUE)


[1] 180
> mean(groesse[geschlecht == "Frau"], na.rm = TRUE)
[1] 170

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()!

> # Berechne mittlere Größe getrennt nach Geschlecht


> tapply(X = groesse, INDEX = geschlecht, FUN = mean, na.rm = TRUE)
Frau Mann
170 180
25 Aggregation und Kreuztabellen 335

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.

> # 1.) Durchschnittlicher Gewinn nach Gebiet und Ausbildung


> tapply(daten$Gewinn, INDEX = daten$Gebiet, FUN = mean)
West Nord Ost Süd Zentrum
9.22 10.14 10.52 8.14 8.20
> tapply(daten$Gewinn, INDEX = daten$Ausbildung, FUN = mean)
Matura Lehre
10.833333 7.741667

Im Norden und Osten wird tendenziell mehr verdient. Vertreter mit Matura sind
erfolgversprechender als Vertreter mit Lehrabschluss.

> # 2.) Kleinster und größter Gewinn pro Gebiet


> tapply(daten$Gewinn, INDEX = daten$Gebiet, FUN = min)
West Nord Ost Süd Zentrum
7.1 7.1 0.2 2.1 3.3
> tapply(daten$Gewinn, INDEX = daten$Gebiet, FUN = max)
West Nord Ost Süd Zentrum
11.8 16.7 24.3 12.2 11.4

Achtung: Wenn wir statt min() und max() die Funktion range() anwenden, so
entsteht eine Liste, da range() keinen Skalar zurückgibt.

> gebiet.range <- tapply(daten$Gewinn, INDEX = daten$Gebiet, FUN = range)


> gebiet.range
$West
[1] 7.1 11.8
$Nord
[1] 7.1 16.7
$Ost
[1] 0.2 24.3
$Süd
[1] 2.1 12.2
$Zentrum
[1] 3.3 11.4

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.

> sapply(gebiet.range, identity)


West Nord Ost Süd Zentrum
[1,] 7.1 7.1 0.2 2.1 3.3
[2,] 11.8 16.7 24.3 12.2 11.4
336 E Tools für Data Science und Statistik

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 ;-)

> # Häufigkeitstabelle für Variable Gebiet erstellen


> table(daten$Gebiet)
West Nord Ost Süd Zentrum
5 5 5 5 4

> tapply(daten$Gebiet, daten$Gebiet, length)


West Nord Ost Süd Zentrum
5 5 5 5 4

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

25.2.1 Einfache Kreuztabellen – table()

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.

> # Kreuztabelle erstellen


> tab <- table(daten$Gebiet, daten$Ausbildung)
> tab
Matura Lehre
West 3 2
Nord 2 3
Ost 2 3
Süd 2 3
Zentrum 3 1

So arbeitet etwa nur ein Vertreter mit der Ausbildung Lehre im Zentrum und 3
Vertreter mit der Ausbildung Matura in der Region West.

25.2.2 Mehrdimensionale Kreuztabellen – table(), apply(), ftable()

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

> # Erstelle eine Kreuztabelle (ein Array) mit 3 Dimensionen


> tab3 <- table(daten$Gewinnstufe, daten$Gebiet, daten$Ausbildung)
> tab3
, , = Matura
West Nord Ost Süd Zentrum
wenig 0 0 0 0 0
mittel 1 2 1 1 2
viel 2 0 1 1 1
, , = Lehre
West Nord Ost Süd Zentrum
wenig 0 0 1 1 1
mittel 2 1 1 1 0
viel 0 2 1 1 0

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:

> tab3["wenig", , ] > tab3["wenig", , , drop = FALSE]


Matura Lehre , , = Matura
West 0 0 West Nord Ost Süd Zentrum
Nord 0 0 wenig 0 0 0 0 0
Ost 0 1 , , = Lehre
Süd 0 1 West Nord Ost Süd Zentrum
Zentrum 0 1 wenig 0 0 1 1 1

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

> # apply() über die 2. Dimension


> apply(tab3, MARGIN = 2, sum)
West Nord Ost Süd Zentrum
5 5 5 5 4

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.

25.2.3 Randsummen bestimmen und anhängen – margin.table(),


addmargins()

Mit der Funktion margin.table() können wir Randsummen berechnen und


addmargins() berechnet defaultmäßig die Randsummen und hängt diese an.
Beispiel: Berechne für die Kreuztabelle tab aus (25.2.1) die Zeilen- und Spalten-
summen und hänge sie an diese an.

> tab
Matura Lehre
West 3 2
Nord 2 3
Ost 2 3
Süd 2 3
Zentrum 3 1

> # Zeilenweise Randsumme


> margin.table(tab, margin = 1)
West Nord Ost Süd Zentrum
5 5 5 5 4

> # Spaltenweise Randsumme


> margin.table(tab, margin = 2)
Matura Lehre
12 12

> # Gesamtsumme
> margin.table(tab)
[1] 24

> # Zeilen- und Spaltensummen anhängen


> addmargins(tab)
Matura Lehre Sum
West 3 2 5
Nord 2 3 5
Ost 2 3 5
Süd 2 3 5
Zentrum 3 1 4
Sum 12 12 24
25 Aggregation und Kreuztabellen 339

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.

25.2.4 Allgemeine Kreuztabellen – tapply()

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?

> tabsum <- tapply(daten$Gewinn, list(daten$Gebiet, daten$Ausbildung), sum)


> tabmargin <- addmargins(tabsum)
> tabmargin
Matura Lehre Sum
West 31.6 14.5 46.1
Nord 16.5 34.2 50.7
Ost 32.4 20.2 52.6
Süd 20.0 20.7 40.7
Zentrum 29.5 3.3 32.8
Sum 130.0 92.9 222.9

Der Vektor daten$Gewinn wird nach daten$Gebiet und daten$Ausbildung auf-


gesplittet und die Elemente jeder Kategorienkombination aufsummiert. Die beiden
Faktoren Gebiet und Ausbildung packen wir dazu in eine Liste. Beachte, dass die
Zeilen- und Spaltensummen bei einigen anderen Anwendungen sinnbefreit sind.

25.2.5 Zeilen-, Spalten- und Totalprozent – prop.table()

Nun könnten wir uns für folgendes interessieren:

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.

prop.table(x, margin = NULL)


340 E Tools für Data Science und Statistik

> # Die Gewinnsummen getrennt nach Gebiet und Ausbildung


> tabsum
Matura Lehre
West 31.6 14.5
Nord 16.5 34.2
Ost 32.4 20.2
Süd 20.0 20.7
Zentrum 29.5 3.3

Wir beantworten unsere drei Fragen von hinten nach vorne.


Frage 3: Totalprozente berechnen
Bei der Berechnung von Totalprozenten genügt es, die Funktion prop.table()
ohne Spezifikation des Parameters margin auf die Tabelle anzuwenden.

> # Totalprozente berechnen > # Randsummen anhängen


> tab.total <- 100 * > round(addmargins(tab.total),
+ prop.table(tabsum) + digits = 1)
> tab.total Matura Lehre Sum
Matura Lehre West 14.2 6.5 20.7
West 14.176761 6.505159 Nord 7.4 15.3 22.7
Nord 7.402423 15.343203 Ost 14.5 9.1 23.6
Ost 14.535666 9.062360 Süd 9.0 9.3 18.3
Süd 8.972633 9.286676 Zentrum 13.2 1.5 14.7
Zentrum 13.234634 1.480485 Sum 58.3 41.7 100.0

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.

> # Alternativen für tab.total:


> totalprozent <- 100 * tabmargin / sum(tabsum)
> totalprozent <- 100 * tabmargin / margin.table(tabsum)

Frage 2: Spaltenprozente berechnen


Um Spaltenprozente zu berechnen, setzen wir in prop.table() den Parameter
margin auf den Wert 2. Es macht in diesem Fall nur Sinn, die Randsummen an die
letzte Zeile anzuhängen, daher setzen wir margin = 1 in addmargins().

> # Spaltenprozente berechnen


> tab.spalte <- 100 * prop.table(tabsum, margin = 2)
> round(addmargins(tab.spalte, margin = 1), digits = 1)
Matura Lehre
West 24.3 15.6
Nord 12.7 36.8
Ost 24.9 21.7
Süd 15.4 22.3
Zentrum 22.7 3.6
Sum 100.0 100.0
25 Aggregation und Kreuztabellen 341

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.

> # Zeilenprozente berechnen


> tab.spalte <- 100 * prop.table(tabsum, margin = 1)
> round(addmargins(tab.spalte, margin = 2), digits = 1)
Matura Lehre Sum
West 68.5 31.5 100
Nord 32.5 67.5 100
Ost 61.6 38.4 100
Süd 49.1 50.9 100
Zentrum 89.9 10.1 100

So entfallen beispielsweise 68.5 Prozent des gesamten Gewinns, der im Gebiet West
erzielt wurde, auf Vertreter mit der Ausbildung Matura.

25.3 Funktionen auf Gruppen anwenden: Teil 2 – aggregate()

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.

## S3 method for class ’data.frame’


aggregate(x, by, FUN, ..., simplify = TRUE, drop = TRUE)

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.

> bonus <- 1 + 0.1 * daten$Gewinn


> daten$Bonus <- bonus
> head(daten)
Nummer Gebiet Ausbildung Gewinn Gewinnstufe Bonus
1 1 Zentrum Matura 11.4 viel 2.14
2 2 Zentrum Matura 9.2 mittel 1.92
3 3 West Matura 9.3 mittel 1.93
4 4 Ost Lehre 13.6 viel 2.36
5 5 Zentrum Matura 8.9 mittel 1.89
6 6 Zentrum Lehre 3.3 wenig 1.33

In Übungsaufgabe 4 auf Seite 347 betrachten wir einen größeren Datensatz.


342 E Tools für Data Science und Statistik

Tabelle 25.2: Parameter der Funktion aggregate()

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.

25.3.1 Eine Gruppenvariable

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:

> tapply(daten$Gewinn, daten$Gebiet, mean)


West Nord Ost Süd Zentrum
9.22 10.14 10.52 8.14 8.20

> tapply(daten$Bonus, daten$Gebiet, mean)


West Nord Ost Süd Zentrum
1.922 2.014 2.052 1.814 1.820

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()!

> daten.aggr <- aggregate(x = daten[c("Gewinn", "Bonus")],


+ by = daten["Gebiet"], mean)
> daten.aggr
Gebiet Gewinn Bonus
1 West 9.22 1.922
2 Nord 10.14 2.014
3 Ost 10.52 2.052
4 Süd 8.14 1.814
5 Zentrum 8.20 1.820

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

25.3.2 Mehrere Gruppenvariablen

Wir erweitern die Sache auf mehrere Gruppierungsvariablen.


Beispiel: Berechne für die Variablen Gewinn und Bonus des Dataframes daten auf
Seite 341 den Mittelwert getrennt nach Gebiet und Ausbildung.

> daten.aggr <- aggregate(x = daten[c("Gewinn", "Bonus")],


+ by = daten[c("Gebiet", "Ausbildung")], mean)
> daten.aggr
Gebiet Ausbildung Gewinn Bonus
1 West Matura 10.533333 2.053333
2 Nord Matura 8.250000 1.825000
3 Ost Matura 16.200000 2.620000
4 Süd Matura 10.000000 2.000000
5 Zentrum Matura 9.833333 1.983333
6 West Lehre 7.250000 1.725000
7 Nord Lehre 11.400000 2.140000
8 Ost Lehre 6.733333 1.673333
9 Süd Lehre 6.900000 1.690000
10 Zentrum Lehre 3.300000 1.330000

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.

> aggregate(x = daten[c("Gewinn", "Bonus")],


+ by = daten[c("Ausbildung", "Gewinnstufe")], mean)
Ausbildung Gewinnstufe Gewinn Bonus
1 Lehre wenig 1.866667 1.186667
2 Matura mittel 8.542857 1.854286
3 Lehre mittel 7.180000 1.718000
4 Matura viel 14.040000 2.404000
5 Lehre viel 12.850000 2.285000
344 E Tools für Data Science und Statistik

25.4 Verfügbarkeit von Variablen

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?

25.4.1 Lokale Verfügbarkeit – with()

Betrachten wir als Motivation folgenden Code.

> tapply(daten$Gewinn, daten$Ausbildung, mean)


Matura Lehre
10.833333 7.741667

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.

> with(daten, tapply(Gewinn, Ausbildung, mean))


Matura Lehre
10.833333 7.741667

25.4.2 Globale Verfügbarkeit – attach(), detach()

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

> # Spalten von daten global verfügbar machen


> attach(daten)

> # Jetzt sind Gewinn und Ausbildung verfügbar.


> 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
> tapply(Gewinn, Ausbildung, mean)
Matura Lehre
10.833333 7.741667

> # 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

Gegebenenfalls kann es bei attach() zu Namenskollisionen mit existierenden


Objekten kommen. Schauen wir uns an, welche Konsequenzen das hat.

> # Definiere eine globale Variable Gewinn


> Gewinn <- 50000
> Gewinn
[1] 50000

> # Namenskollision beim Objekt Gewinn


> attach(daten)
The following object is masked _by_ .GlobalEnv:
Gewinn

> 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

25.5.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Erkläre die Unterschiede zwischen tapply() und aggregate().


2. Ein Student kommt verzweifelt zu dir: Er will für den Vertreterdatensatz dieses
Kapitels für jede numerische Variable von daten den Mittelwert getrennt nach
Gebiet berechnen. Folgenden Code hat er bereits geschrieben:

> aggregate(daten[lapply(daten, is.numeric)], daten$Gebiet, mean)

Korrigiere seinen Code, damit er das gewünschte Ergebnis liefert.


3. Wir betrachten den Vertreterdatensatz dieses Kapitels.

a) Erstelle eine Kreuztabelle, welche den minimalen Gewinn für jede Kate-
gorienkombination aus Gebiet und Ausbildung enthält.

b) Hänge an die Kreuztabelle aus 3a) die Zeilen- und Spaltenmaxima an


Und zwar
• ohne die Funktion addmargins()
• mit der Funktion addmargins()
25 Aggregation und Kreuztabellen 347

4. Der R-Datensatz airquality enthält tägliche Luftgütemessungen von New


York zwischen Mai und September 1973.

> # Datensatz laden und Hilfe zum Datensatz abrufen


> data(airquality)
> # ?airquality

> head(airquality) # Überblick über den Datensatz


Ozone Solar.R Wind Temp Month Day
1 41 190 7.4 67 5 1
2 36 118 8.0 72 5 2
3 12 149 12.6 74 5 3
4 18 313 11.5 62 5 4
5 NA NA 14.3 56 5 5
6 28 NA 14.9 66 5 6

Der Datensatz enthält folgende Variablen:

• Ozone: der Ozonwert in ppb (parts per billion)


• Solar.R: die Sonnenstrahlung in lang (langley)
• Wind: die Windgeschwindigkeit in mph (miles per hour)
• Temp: die Temperatur in Fahrenheit

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.

a) Wie viele fehlende Werte hat jede Variable?


b) Wie viele Zeilen mit mindestens einem fehlenden Wert gibt es?
c) Bestimme für die Variablen Ozone, Solar.R, Wind und Temp die monat-
lichen Durchschnittswerte. Fehlende Werte sollen ausgeschlossen werden.
d) Erstelle aus der Variable Temp eine neue Variable Temp30 mit folgenden
beiden Kategorien:
• <=30GradC: Temperaturen von höchstens 30 Grad Celsius.
• >30GradC: Temperaturen von mehr als 30 Grad Celsius.
30 Grad Celsius entsprechen dabei 86 Grad Fahrenheit.

e) Bestimme automatisiert mit Hilfe eines R-Codes, welche Kategoriekom-


binationen aus Temp30 und Month leer sind.
f) Berechne für die Variablen Ozone, Solar.R, Wind und Temp den Mittel-
wert getrennt nach jeder Kategoriekombination aus Temp30 und Month.
Schließe wiederum fehlende Werte aus.
348 E Tools für Data Science und Statistik

26 Klassen, generische Funktionen und Attribute

Dort wollen wir in diesem Kapitel hin:

• einen Einblick in Klassen und generische Funktionen bekommen

• mit Attributen umgehen lernen

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 <- factor(c(1, 2, 2, 1), labels = c("Mann", "Frau"))


> alter <- c(23, 26, 19, 27)

> 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.

26.1 Klassen, generische Funktionen – class(), UseMethod(),


methods()

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.

> class(geschlecht) > class(alter)


[1] "factor" [1] "numeric"

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

Wir brauchen also für unterschiedliche Klassen jeweils maßgeschneiderte Funk-


tionen (sogenannte Methoden), welche die Spezialeigenschaften der Objekte einer
Klasse berücksichtigen. Genau hier kommen generische Funktionen ins Spiel, sie
zeichnen sich dadurch aus, dass sie sich beim Funktionsaufruf an die Klasse des
übergebenen Objekts anpassen.
Die Funktion print() ist eine solche generische Funktion. Allgemein erkennen wir
eine generische Funktion daran, dass sie UseMethod() aufruft.

> print
function (x, ...)
UseMethod("print")
<bytecode: 0x0000000016cc7568>
<environment: namespace:base>

Dabei sucht UseMethod("print") jene Funktion print.klasse(), welche zur Klasse


des übergebenen Objekts passt. Dieser Aufruf besteht also aus dem Namen der
generischen Funktion und einer Klassenerweiterung, die durch einen Punkt getrennt
sind, was allgemein so aussieht:

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:

> # Einige Methoden der Funktion print()


> methods(print)[auszug]
[1] "print.data.frame" "print.default" "print.factor"
[4] "print.table"

So gibt es für Faktoren die maßgeschneiderte Funktion print.factor(). Gibt es


keine spezielle print()-Methode, die zur Klasse des übergebenen Objektes passt, so
wird standardmäßig print.default() verwendet.
350 E Tools für Data Science und Statistik

Beispiel: Wir betrachten einige Klassen und dazugehörige print()-Funktionen.

Klasse integer (eine Klasse für ganzzahlige numerische Werte):

> x <- 1:5


> class(x)
[1] "integer"

> # Gibt es eine spezielle Methode print.integer()?


> "print.integer" %in% methods(print)
[1] FALSE

Es gibt keine Methode print.integer(), daher wird print.default() verwendet.

> # Generischer print()-Aufruf > # Äquivalent in diesem Fall


> print(x) > print.default(x)
[1] 1 2 3 4 5 [1] 1 2 3 4 5

Klasse factor (wir verwenden das Objekt geschlecht aus der Einleitung):

> class(geschlecht)
[1] "factor"

> # Gibt es eine spezielle Methode print.factor()?


> "print.factor" %in% methods(print)
[1] TRUE

Da es eine spezielle Funktion für Faktoren gibt, wird diese auch verwendet.

> # Generischer print()-Aufruf > # Äquivalent in diesem Fall


> print(geschlecht) > print.factor(geschlecht)
[1] Mann Frau Frau Mann [1] Mann Frau Frau Mann
Levels: Mann Frau Levels: Mann Frau

Klasse data.frame:

> x <- data.frame(Geschlecht = geschlecht, Alter = alter)


> class(x)
[1] "data.frame"

> "print.data.frame" %in% methods(print)


[1] TRUE

> # Generischer print()-Aufruf > # Äquivalent in diesem Fall


> print(x) > print.data.frame(x)
Geschlecht Alter Geschlecht Alter
1 Mann 23 1 Mann 23
2 Frau 26 2 Frau 26
3 Frau 19 3 Frau 19
4 Mann 27 4 Mann 27
26 Klassen, generische Funktionen und Attribute 351

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):

• print() • lines() • sort() • aggregate()


• predict() • points() • diff()
• plot() • summary() • by()

26.2 Attribute erfragen und Klassenattribut entfernen –


attributes(), unclass()

Mit unclass() können wir das Klassenattribut entfernen und damit die interne
Verwaltungsstruktur betrachten. Sehen wir uns ein Codebeispiel an.

> unclass(geschlecht) # Ruft intern print(unclass(geschlecht)) auf


[1] 1 2 2 1
attr(,"levels")
[1] "Mann" "Frau"

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.

> attributes(geschlecht) # Eine Liste mit den Attributen


$levels
[1] "Mann" "Frau"
$class
[1] "factor"

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"

> class(geschlecht) > levels(geschlecht)


[1] "factor" [1] "Mann" "Frau"
352 E Tools für Data Science und Statistik

26.3 Aus der guten Praxis

26.3.1 Fallbeispiel: Run Length Encoding

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)).

> # Anzahl der Münzwürfe


> k <- 50

> # Münzwürfe simulieren


> RNGversion("4.0.2")
> set.seed(29)
> wurf <- sample(c("Z", "K"), size = k, replace = TRUE)

> # Münzwürfe in String umwandeln


> string <- paste(wurf, collapse = "")
> string
[1] "ZKZKKZZZKKKKZKKKKKKKKKZZKZZZZKKZZZKZKZZZZZZZKZKKZZ"

Jetzt kommt die Schlüsselstelle! Mit gregexpr() suchen wir mit dem Suchmuster
"K+" nach allen konsekutiven Vorkommen von Kopf.

> # Matchlängen bestimmen: Suchmuster K, KK, KKK, KKKK, ...


> res <- gregexpr("K+", string)[[1]]
> res
[1] 2 4 9 14 25 30 35 37 45 47
attr(,"match.length")
[1] 1 2 4 9 1 2 1 1 1 2
.....

Das Attribut match.length gibt hier die Anzahl der unmittelbar aufeinanderfolgen-
den Köpfe an. Genau diese Information wollen wir haben!

> # Attribut match.length selektieren und Häufigkeitstabelle erstellen


> anz.kopf <- attributes(res)$match.length
> table(anz.kopf)
anz.kopf
1 2 4 9
5 3 1 1

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

26.4.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Wir betrachten die Funktion summary().

a) Zeige, dass summary() generisch ist.


b) Liste alle summary()-Funktionen auf.

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.

2. Wir rekapitulieren das Fallbeispiel (26.3.1).

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.

3. Erstelle zunächst eine beliebige Matrix, zum Beispiel:

> M <- matrix(1:12, ncol = 3)


354 E Tools für Data Science und Statistik

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

27 Datums- und Uhrzeitobjekte

Dort wollen wir in diesem Kapitel hin:

• Klassen zum Umgang mit Datums- und Uhrzeitwerten kennenlernen

• Datums- und Uhrzeitwerte generieren und formatieren


• mit Datumswerten rechnen sowie nützliche Datumsinformationen gewinnen

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.

> gebtag <- c(Daniel = "07.07.1986", Andreas = "12.09.1973",


+ Neujahr98 = "01.01.1998")
> gebtag
Daniel Andreas Neujahr98
"07.07.1986" "12.09.1973" "01.01.1998"

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

27.1 Erste Einblicke

27.1.1 Datum und Uhrzeit abfragen – Sys.Date(), Sys.time()

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.

> # Aktuelles Datum > # Aktuelle Datums- und Uhrzeitinfo


> date <- Sys.Date() > time <- Sys.time()
> date > time
[1] "2020-08-24" [1] "2020-08-24 17:50:11 CEST"

> class(date) > class(time)


[1] "Date" [1] "POSIXct" "POSIXt"

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

Was bedeuten diese Zahlen? Sehen wir es uns an!

27.1.2 Klassenübersicht – Date, POSIXct, POSIXlt

Folgende Klassen zur Verwaltung von Datums- und Uhrzeitobjekten gibt es:

1. Date: Klasse zur Speicherung von Datumswerten (z. B. Objekt date)


Speicherung als Zahl: Anzahl der Tage seit 01.01.1970 (= Tag 0)
2. POSIXct: Klasse für Datum und Uhrzeit (z. B. Objekt time)
Speicherung als Zahl: Anzahl der Sekunden seit 01.01.1970 um 01:00 Uhr
(MEZ) (= Sekunde 0)
3. POSIXlt: Klasse für Datum und Uhrzeit
Speicherung als Liste: direkter Zugriff auf Merkmale möglich

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 ;-)

27.1.3 Konvertierung – as.Date(), as.POSIXct(), as.POSIXlt()

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"

> # Konvertierung in Klasse Date


> time.Date <- as.Date(time)
> time.Date
[1] "2020-08-24"

> # Rückkonvertierung in Klasse POSIXct - Uhrzeit geht verloren!


> as.POSIXct(time.Date)
[1] "2020-08-24 02:00:00 CEST"

Wir sehen, dass nach der Rückkonvertierung die Uhrzeit nicht mehr stimmt. Sie
wurde auf 02:00:00 geändert.

27.2 Datum und Uhrzeit formatieren

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.

27.2.1 Formatierungsfunktionen und Platzhalter – format.Date(),


format.POSIXct()

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

## S3 method for class ’POSIXct’


format(x, format = "", tz = "", usetz = FALSE, ...)

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

Zeichen Bedeutung Darstellung


%Y Jahr als Zahl vierstellig, zum Beispiel "2019"
%m Monat als Zahl von "01" bis "12"
%B Monatsname abhängig von den Ländereinstellungen
%d Tag des Monats als Zahl von "01" bis maximal "31"
%u Wochentag als Zahl von "1" (Montag) bis "7" (Sonntag)
%A Wochentagsname abhängig von den Ländereinstellungen
%j Tag des Jahres als Zahl von "001" bis (maximal) "366"
%H Stunde als Zahl von "00" bis "23" (24-Stunden)
%M Minute als Zahl von "00" bis "59"
%S Sekunde als Zahl von "00" bis "59"

Schauen wir uns für POSIXct-Objekte an, wie das Ganze funktioniert.
Beispiel: Erzeuge aus der aktuellen Uhrzeit Zeichenketten der folgenden Bauart:

1. "Es ist 17:50 Uhr."


2. "24.08.2020 17:50:11"
3. "Montag, der 24. August 2020"

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.

> # Aktuelle Uhrzeit erfragen


> # time <- Sys.time()
> time
[1] "2020-08-24 17:50:11 CEST"
27 Datums- und Uhrzeitobjekte 359

> # 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.

> # Umwandlung in ein Date-Objekt


> time.Date <- as.Date(time)
> time.Date
[1] "2020-08-24"

> # 3.)
> format(time.Date, "%A, der %d. %B %Y")
[1] "Montag, der 24. August 2020"

27.2.2 Datumsfunktionen – weekdays(), months(), quarters()

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.

> # Heutiges Datum abfragen


> # date <- Sys.Date()
> date
[1] "2020-08-24"
360 E Tools für Data Science und Statistik

Wir betrachten zwei Varianten: Die rechte basiert auf format() während die linke
die soeben erwähnten Datumsfunktionen einsetzt.

> # 1.) Wochentag bestimmen > # 1.) Wochentag bestimmen


> weekdays(date) > format(date, "%A")
[1] "Montag" [1] "Montag"

> # Abgekürzte Version > # Abgekürzte Version


> weekdays(date, abbreviate = TRUE) > format(date, "%a")
[1] "Mo." [1] "Mo."

> # 2.) Monat bestimmen > # 2.) Monat bestimmen


> months(date) > format(date, "%B")
[1] "August" [1] "August"

> # Quartale bestimmen


> quarters(date)
[1] "Q3"

Für Quartale gibt es leider (derzeit) keinen Formatierungsplatzhalter. Wir können


Quartale aber trotzdem auch ohne quarters() mit einem kleinen Workaround be-
stimmen. Das überlassen wir aber dir in Übung 6 ;-)
Beachte: Ob Montag oder Monday oder Lundi etc. gedruckt wird, hängt von den
Ländereinstellungen ab. In (27.6) gehen wir kurz darauf ein, was in diesem Zusam-
menhang Locales sind und wie wir unseren Code bei bestimmten Aufgaben spra-
chenunabhängig gestalten.
Bemerkung: Das Beispiel würde mit einem POSIXct-Objekt genauso funktionieren.
Damit haben wir alles beisammen um zu erfahren, an welchen Wochentagen wir
geboren sind. Das heißt: Fast alles. Davor müssen wir aus den Strings noch ein
Datumsobjekt erzeugen. Genau das tun wir im nächsten Kapitel!

27.3 Datums- und Uhrzeitobjekte erzeugen – as.Date(),


as.POSIXct()

Mit den Funktionen as.Date() bzw. as.POSIXct() erzeugen wir Objekte der Klasse
Date bzw. POSIXct. Dabei gibt es zwei Varianten:

• Erzeugung aus Zeichenketten (27.3.1)


• Erzeugung aus einem Referenzdatum bzw. einer Referenzuhrzeit (27.3.2)

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

27.3.1 Erzeugung aus Zeichenketten

Mit der für Zeichenketten maßgeschneiderten Version von as.Date() können wir
aus einem Stringvektor ein Datumsobjekt erzeugen.

## S3 method for class ’character’


as.Date(x, format, ...)

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 String mit den Geburtstagen


> gebtag
Daniel Andreas Neujahr98
"07.07.1986" "12.09.1973" "01.01.1998"

> # Umwandlung in ein Objekt der Klasse Date


> gebtag.Date <- as.Date(gebtag)
Fehler in charToDate(x) :
Zeichenkette ist nicht in einem eindeutigen Standardformat

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.

> # Erzeuge Objekt der Klasse Date


> gebtag.Date <- as.Date(gebtag, format = "%d.%m.%Y")
> gebtag.Date
[1] "1986-07-07" "1973-09-12" "1998-01-01"

Jetzt hat es geklappt!


Da wir uns hier nicht für die genauen Geburtsuhrzeiten interessieren, genügt die
Klasse Date. Wir bemerken aber, dass wir mit as.POSIXct() auch ein Objekt der
Klasse POSIXct hätten erzeugen können.

> # Erzeuge Objekt der Klasse POSIXct


> as.POSIXct(gebtag, format = "%d.%m.%Y")
Daniel Andreas Neujahr98
"1986-07-07 CEST" "1973-09-12 CET" "1998-01-01 CET"

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

> # Beschriftungen übernehmen


> names(gebtag.Date) <- names(gebtag)
> gebtag.Date
Daniel Andreas Neujahr98
"1986-07-07" "1973-09-12" "1998-01-01"

Jetzt aber zu unserer Frage aus der Einleitung :-)


Beispiel: An welchen Wochentagen war jeweils unsere Geburt?

> format(gebtag.Date, format = "%A")


Daniel Andreas Neujahr98
"Montag" "Mittwoch" "Donnerstag"

27.3.2 Erzeugung aus Referenzdatum bzw. Referenzuhrzeit

Manchmal möchten wir aus einem Referenzdatum bzw. einer Referenzzeit


ein neues Datum generieren. Dies können wir mit einer maßgeschneiderten Variante
der Funktionen as.Date() bzw. as.POSIXct() für numerische Vektoren bewerkstel-
ligen. In dieser Variante spielt der Parameter origin eine wichtige Rolle.

## S3 method for class ’numeric’


as.Date(x, origin, ...)

## S3 method for class ’numeric’


as.POSIXct(x, tz = "", origin, ...)

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.

> # Heutiges Datum abfragen


> # date <- Sys.Date()
> date
[1] "2020-08-24"

> as.Date(c(-2, 0, 14), origin = date)


[1] "2020-08-22" "2020-08-24" "2020-09-07"

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

Bei POSIXct-Objekten müssen wir die Tage in Sekunden umrechnen.

> # Aktuelle Uhrzeit abfragen


> # time <- Sys.time()
> time
[1] "2020-08-24 17:50:11 CEST"

> as.POSIXct(c(-2, 0, 14) * 24 * 60 * 60, origin = time)


[1] "2020-08-22 17:50:11 CEST" "2020-08-24 17:50:11 CEST"
[3] "2020-09-07 17:50:11 CEST"

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)) ;-)

27.4 Datums- und Uhrzeitvektoren sortieren – sort(), order()

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.

> # Sortiere unsere Geburtstage aufsteigend (von erfahren bis jung)


> sort(gebtag.Date)
Andreas Daniel Neujahr98
"1973-09-12" "1986-07-07" "1998-01-01"

> # Sortiere unsere Geburtstage absteigend (von jung bis erfahren)


> sort(gebtag.Date, decreasing = TRUE)
Neujahr98 Daniel Andreas
"1998-01-01" "1986-07-07" "1973-09-12"

> # Namen aufsteigend nach dem Geburtsdatum sortieren


> names(gebtag.Date)[order(gebtag.Date)]
[1] "Andreas" "Daniel" "Neujahr98"

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!

> # Ist Daniel nach Andreas geboren?


> gebtag.Date["Daniel"] > gebtag.Date["Andreas"]
Daniel
TRUE
364 E Tools für Data Science und Statistik

27.5 Rechnen mit Datumswerten

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)

27.5.1 Tage und Sekunden addieren bzw. subtrahieren

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.

> # Heutiges Datum abfragen


> # date <- Sys.Date()
> date
[1] "2020-08-24"

> # Morgen und Übermorgen > # Vorgestern und Gestern


> date + c(1, 2) > date - c(2, 1)
[1] "2020-08-25" "2020-08-26" [1] "2020-08-22" "2020-08-23"

Für POSIXct-Objekte funktioniert das völlig gleich, nur dass hier Sekunden statt
Tage hinzuaddiert oder abgezogen werden.

27.5.2 Paarweise Zeitdifferenzen bestimmen – diff()

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.

> unclass(gebtag.Date) > diff(unclass(gebtag.Date))


Daniel Andreas Neujahr98 Andreas Neujahr98
6031 1350 10227 -4681 8877

Für Objekte der Klasse POSIXct wird die Zeitdifferenz bei Anwendung von diff()
wiederum in Sekunden angegeben.

27.5.3 Parallele Zeitdifferenzen bestimmen – "-", difftime()

Findige R-Nachwuchstalente haben vielleicht schon entdeckt, dass wir Zeitdifferen-


zen auch mit "-" berechnen können.

> # Das heutige Datum abfragen


> # date <- Sys.Date()
> date - gebtag.Date
Time differences in days
Daniel Andreas Neujahr98
12467 17148 8271

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.

difftime(time1, time2, tz,


units = c("auto", "secs", "mins", "hours", "days", "weeks"))

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.

1. Wie viele Tage bzw. Wochen hat Falco gelebt?

2. Wie alt ist er geworden (in ganzen Jahren)?

> # Geburts- und Todesdatum erzeugen


> falco <- as.Date(c("1957-02-19", "1998-02-06"))
> falco
[1] "1957-02-19" "1998-02-06"

Die 1. Frage ist schnell und einfach beantwortet.


366 E Tools für Data Science und Statistik

> # 1.) Wie viele Tage / Wochen hat Falco gelebt?


> diff(falco)
Time difference of 14962 days
> difftime(falco[2], falco[1], units = "days")
Time difference of 14962 days

> difftime(falco[2], falco[1], units = "weeks")


Time difference of 2137.429 weeks

Wir sehen, dass die Wochen als Dezimalzahl ausgegeben werden. Interessieren wir
uns nur für ganze Wochen, so runden wir einfach ab.

> floor(difftime(falco[2], falco[1], units = "weeks"))


Time difference of 2137 weeks

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.

> # Geburtstag im Jahr des Todes erstellen


> temp <- paste0(format(falco[1], format = "%d.%m."),
+ format(falco[2], format = "%Y"))
> temp
[1] "19.02.1998"

> falco.geb.tod <- as.Date(temp, format = "%d.%m.%Y")


> falco.geb.tod
[1] "1998-02-19"
27 Datums- und Uhrzeitobjekte 367

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?

> # War der Tod nach dem Geburtstag im Todesjahr?


> falco[2] >= falco.geb.tod
[1] FALSE

Falls das Ergebnis FALSE ist (was hier der Fall ist), dann ziehen wir von der Jahres-
differenz eine 1 ab.

> # Alter korrigieren


> diff(falco.year) - !(falco[2] >= falco.geb.tod)
[1] 40

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 ;-)

> # Heutiges Datum abfragen


> # date <- Sys.Date()
> date
[1] "2020-08-24"

> gebtag.Date
Daniel Andreas Neujahr98
"1986-07-07" "1973-09-12" "1998-01-01"

> # Geburtsjahre und heutiges Jahr extrahieren


> geb.jahr <- as.numeric(format(gebtag.Date, format = "%Y"))
> heute.jahr <- as.numeric(format(date, format = "%Y"))

> geb.jahr
[1] 1986 1973 1998
> heute.jahr
[1] 2020

> # Die Geburtstage von uns in diesem Jahr generieren


> geb.heuer <- paste0(format(gebtag.Date, format = "%d.%m."), heute.jahr)
> geb.heuer <- as.Date(geb.heuer, format = "%d.%m.%Y")
> geb.heuer
[1] "2020-07-07" "2020-09-12" "2020-01-01"

> # Das korrekte Alter zum heutigen Zeitpunkt berechnen


> alter <- heute.jahr - geb.jahr - !(date >= geb.heuer)
> names(alter) <- names(gebtag.Date)
> alter
Daniel Andreas Neujahr98
34 46 22
368 E Tools für Data Science und Statistik

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.

> datum2902 <- as.Date(c("29.02.1996", "29.02.2021"), format = "%d.%m.%Y")


> datum2902
[1] "1996-02-29" NA

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.

> # Füge zu Testzwecken eine neue Person hinzu


> gebtag.Date <- c(gebtag.Date, Felix2902 = as.Date("1996-02-29"))
> gebtag.Date
Daniel Andreas Neujahr98 Felix2902
"1986-07-07" "1973-09-12" "1998-01-01" "1996-02-29"

> # Generiere den 17.03.2021 zu Testzwecken als heutiges Datum


> date <- as.Date("2021-03-17")
> # Zur Bestimmung des aktuellen Alters durch Sys.Date() ersetzen
> # date <- Sys.Date()
> date
[1] "2021-03-17"

> # Geburtsjahre und heutiges Jahr extrahieren


> geb.jahr <- as.numeric(format(gebtag.Date, format = "%Y"))
> heute.jahr <- as.numeric(format(date, format = "%Y"))

> geb.jahr > heute.jahr


[1] 1986 1973 1998 1996 [1] 2021

> # Die Geburtstage von uns in diesem Jahr generieren


> geb.heuer <- paste0(format(gebtag.Date, format = "%d.%m."), heute.jahr)
> geb.heuer <- as.Date(geb.heuer, format = "%d.%m.%Y")
> geb.heuer
[1] "2021-07-07" "2021-09-12" "2021-01-01" NA
27 Datums- und Uhrzeitobjekte 369

Jetzt kommt die Korrektur. Wir ersetzen in geb.heuer jedes Vorkommen von NA
durch den 01.03. des aktuellen Jahres.

> # Korrektur für Geburtstage in Schaltjahren


> bool.na <- is.na(geb.heuer)
> bool.na
[1] FALSE FALSE FALSE TRUE

> geb.heuer[bool.na] <- as.Date(paste0("01.03.", heute.jahr),


+ format = "%d.%m.%Y")
> geb.heuer
[1] "2021-07-07" "2021-09-12" "2021-01-01" "2021-03-01"

Jetzt können wir das aktuelle Alter auch für jene Personen zuverlässig bestimmen,
die an einem 29.02. Geburtstag haben.

> # Das korrekte Alter zum heutigen Zeitpunkt berechnen


> alter <- heute.jahr - geb.jahr - !(date >= geb.heuer)
> names(alter) <- names(gebtag.Date)
> alter
Daniel Andreas Neujahr98 Felix2902
34 47 23 25

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.

> date # das heutige Datum


[1] "2021-03-17"
> geb.heuer # Datumsvektor mit den heurigen Geburtstagen
[1] "2021-07-07" "2021-09-12" "2021-01-01" "2021-03-01"

> geb.heuer < date # Heuer schon Geburtstag gehabt?


[1] FALSE FALSE TRUE TRUE

> # Jahr des nächsten Geburtstages bestimmen


> jahr <- heute.jahr + (geb.heuer < date)
> jahr
[1] 2021 2021 2022 2022

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

> # Nächsten Geburtstag ermitteln (basierend auf gebtag.Date!)


> geb.next <- paste0(format(gebtag.Date, format = "%d.%m."), jahr)
> geb.next <- as.Date(geb.next, format = "%d.%m.%Y")
> names(geb.next) <- names(gebtag.Date)
> geb.next
Daniel Andreas Neujahr98 Felix2902
"2021-07-07" "2021-09-12" "2022-01-01" NA

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).

> # Reparatur für Schaltjahre


> date0103 <- as.Date(paste0(heute.jahr, "-03-01"))
> date >= date0103 # Hat heuer der 01.03. schon stattgefunden?
[1] TRUE

> kandidatenjahre <- heute.jahr + (date >= date0103) + 0:7


> kandidatenjahre
[1] 2022 2023 2024 2025 2026 2027 2028 2029

> # Kandidatendatumswerte generieren


> date2902 <- as.Date(paste0("29.02.", kandidatenjahre), format = "%d.%m.%Y")
> date2902
[1] NA NA "2024-02-29" NA NA
[6] NA "2028-02-29" NA

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.

> # Nächsten 29.02. extrahieren ...


> date2902.next <- date2902[!is.na(date2902)][1]
> date2902.next
[1] "2024-02-29"

> # ... und NA-Werte damit ersetzen


> geb.next[is.na(geb.next)] <- date2902.next
> geb.next
Daniel Andreas Neujahr98 Felix2902
"2021-07-07" "2021-09-12" "2022-01-01" "2024-02-29"

Jetzt können wir unsere eigentliche Frage beantworten :-)


27 Datums- und Uhrzeitobjekte 371

> # In wie vielen Tagen / Wochen sind unsere nächsten Geburtstage?


> difftime(geb.next, date, units = "days")
Time differences in days
Daniel Andreas Neujahr98 Felix2902
112 179 290 1079

> difftime(geb.next, date, units = "weeks")


Time differences in weeks
Daniel Andreas Neujahr98 Felix2902
16.00000 25.57143 41.42857 154.14286

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 ;-)

27.6 Spezielle Datumswerte erzeugen und Locales

Im Locale werden länderspezifische Einstellungen verwaltet. Die Einstellungen steu-


ern unter anderem, in welcher Sprache Wochentagsnamen oder Monatsnamen ge-
druckt werden.
Warum ist das relevant? Angenommen, wir wollen überprüfen, ob der 01.01.2019
wirklich ein Dienstag war. Dann funktioniert einer der folgenden beiden Codes nicht
weltweit. Höchstens zwei Mal darfst du raten, welcher ;-)

> # 1.) Über Wochentagsnamen > # 2.) Über Zahlen


> datum <- as.Date("2019-01-01") > datum <- as.Date("2019-01-01")
> weekdays(datum) > format(datum, format = "%u")
[1] "Dienstag" [1] "2"

> weekdays(datum) == "Dienstag" > format(datum, format = "%u") == "2"


[1] TRUE [1] TRUE

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"

> # In Datumsobjekt umwandeln


> datum <- as.Date(datum.string, format = "%d.%m.%Y")

Hierbei haben wir uns geschickt des Recyclings bedient! 1:7 muss drei Mal repliziert
werden, um dieselbe Länge wie jahre zu haben.

> # 2.) Alle Samstage selektieren


> res <- datum[format(datum, "%u") == "6"]
> res
[1] "2021-01-02" "2022-01-01" "2023-01-07"

> # 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 :-)

Strategie 2: Berechnungsstrategie. Präsentiert in drei Schritten:


1. Generiere einen Datumsvektor mit den 01.01. der Jahre 2021 bis 2023.
2. Berechne aus den Wochentagen dieses Datumsvektors jeweils die Anzahl der
Tage bis zum nächsten Samstag.

3. Addiere sodann diese Anzahlen zu den 01.01. dazu.


27 Datums- und Uhrzeitobjekte 373

> # 1.) Die 01.01. aller Jahre generieren


> datum.string <- paste(1, 1, 2021:2023, sep = ".")
> datum <- as.Date(datum.string, format = "%d.%m.%Y")
> datum
[1] "2021-01-01" "2022-01-01" "2023-01-01"

> # 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.

> # Anzahl der Tage bis zum nächsten Samstag bestimmen


> (6 + 7 - tag) %% 7
[1] 1 0 6

> # 3.) Diese Anzahlen zu den 01.01. addieren


> res <- datum + (6 + 7 - tag) %% 7
> res
[1] "2021-01-02" "2022-01-01" "2023-01-07"

> # Kontrolle
> weekdays(res)
[1] "Samstag" "Samstag" "Samstag"

27.7 Aus der guten Praxis

27.7.1 Anwendungshierarchie der drei Klassen

Die in diesem Kapitel erwähnten drei Klassen folgen hinsichtlich der Anwendungs-
möglichkeiten folgender Hierarchie:

Date ⊂ POSIXct ⊂ POSIXlt

Mit Date können wir folgende Aufgaben lösen:

• Zeitdifferenzen zweier Datumsvektoren in Tagen und Wochen berechnen


Wie? Mit difftime() und dem Parameter units ("days", "weeks")3

3 Mit diff() können wir paarweise Zeitdifferenzen eines Datumsvektors in Tagen berechnen.
374 E Tools für Data Science und Statistik

• Zeichenketten in beliebiger Form erstellen, die keine Uhrzeit beinhalten


Wie? mit format() und den in Tab. 27.1 auf Seite 358 oberhalb des Striches
bzw. ?format.POSIXct aufgelisteten Platzhaltern.
• Wochentage, Monate, Quartale extrahieren
Wie? Mit weekdays(), months() und quarters()

Mit POSIXct können wir darüber hinaus folgendes tun:

• Zeitdifferenzen zweier Datums- und Uhrzeitvektoren in Sekunden, Minuten


und Stunden berechnen
Wie? Mit difftime() und dem Parameter units ("secs", "mins", "hours")4
• Zeichenketten in beliebiger Form erstellen
Wie? mit format() und den in Tab. 27.1 auf Seite 358 bzw. ?as.POSIXlt
aufgelisteten Platzhaltern.

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.

27.7.2 Fallbeispiel: Berechnung von Adventsonntagen

Kommen wir zu einem spannenden und herausfordernden Beispiel. Gerne darfst du


zunächst versuchen, die folgende Aufgabe selbst zu lösen!
Aufgabe: Erstelle eine Liste, die für die Jahre 2022 bis 2024 das Datum aller Ad-
ventsonntage enthält. In Infobox 10 findest du alle notwendigen Hintergrundinfor-
mationen, die du für diese Aufgabe benötigst.

Infobox 10: Weihnachten und Adventsonntage

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

> # Jahre, für die wir uns interessieren.


> jahre <- 2022:2024

> # Datum für Heiligabend aller gesuchten Jahre generieren


> heiligabend <- as.Date(paste(jahre, 12, 24, sep = "-"))
> heiligabend
[1] "2022-12-24" "2023-12-24" "2024-12-24"

> # Wochentag aller Heiligabende bestimmen


> tag <- as.numeric(format(heiligabend, "%u"))
> tag # Wochentag von Heiligabend
[1] 6 7 2

> tag %% 7 # Tage vom 4. Adventsonntag bis Heiligabend


[1] 6 0 2

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.

> # Datum des 1. Adventsonntages berechnen


> advent1 <- heiligabend - (tag %% 7) - 21
> advent1
[1] "2022-11-27" "2023-12-03" "2024-12-01"

> # Kontrolle, ob alle diese Tage an einem Sonntag sind


> all(format(advent1, "%u") == "7")
[1] TRUE

Jetzt wird es spannend! Um alle vier Adventsonntage zu bekommen, addieren wir


zu jedem 1. Adventsonntag 0, 7, 14 und 21 Tage und zwar gleichzeitig für alle
gewünschten Jahre. An dieser Stelle kommt lapply() ins Spiel! Und das Tolle:
lapply() erzeugt dabei auch gleich eine Liste. Welch glückliche Fügung :-)

> # Liste aller vier Adventsonntage aller Jahre erstellen ...


> advent <- lapply(advent1, "+", seq(from = 0, by = 7, length = 4))
> names(advent) <- paste("Adventsonntage", jahre, sep = "_")

> # ... und ausgeben


> advent
$Adventsonntage_2022
[1] "2022-11-27" "2022-12-04" "2022-12-11" "2022-12-18"
$Adventsonntage_2023
[1] "2023-12-03" "2023-12-10" "2023-12-17" "2023-12-24"
$Adventsonntage_2024
[1] "2024-12-01" "2024-12-08" "2024-12-15" "2024-12-22"
376 E Tools für Data Science und Statistik

27.8 Abschluss

27.8.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

27.8.2 Ausblick und Rückblick

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

1. Erzeuge aus dem Objekt geb.posix folgenden String:


"07. Juli 1986 um 07:22 Uhr"
geb.posix <- as.POSIXct("1986-07-07 07:22:00 CET")
2. Gegeben sind folgende zwei Strings, die Datum und Uhrzeit beschreiben:

str1 <- "13 Uhr am 7. Jänner 2019"


str2 <- "32. Tag des Jahres 2019 um 15:30 Uhr"

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:

• Peter hat am 20.01. Geburtstag.


• Herbert hat genau eine Woche nach Peter Geburtstag.
• Bei Lisa erinnern wir uns noch daran, dass sie im Jahr 2015 am Freitag
zwischen Peter und Herbert Geburtstag hatte.

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).

Funktioniert dein Code auch auf PCs mit anderen Spracheinstellungen?


5. Wir erweitern das Fallbeispiel aus (27.7.2).
a) Erstelle eine Häufigkeitstabelle, aus der ablesbar ist, wie oft zwischen 2020
und 2059 Heiligabend auf welchen Wochentag fällt. Achte darauf, dass die
Wochentage chronologisch geordnet sind (von Montag bis Sonntag).
b) Wie oft findet der 1. Adventsonntag zwischen 2020 und 2059 im November
statt? Dein Code funktioniert idealerweise auch in anderen Sprachen.
6. Schreibe einen Code, der für beliebige Datumsvektoren das Quartal bestimmt
und ohne quarters() auskommt.
7. Die Studienvertretung Statistik der Universität Wien veranstaltet in der Regel
am ersten Donnerstag eines jeden Monats einen Statistikstammtisch. Ausnah-
men und vereinfachende Annahmen:
• Im Februar, Juli, August und September findet kein Stammtisch statt.

• Im Jänner findet der Stammtisch ausnahmsweise am 2. Donnerstag statt


(egal, wie der Dreikönigstag fällt).
• Der April-Stammtisch ist am 1. Donnerstag (egal, wann Osterferien sind).
• Auch der Juni-Stammtisch ist am 1. Donnerstag (egal, wie Pfingsten,
Christi Himmelfahrt und Fronleichnam fallen).
Bestimme für mindestens zwei Jahre deiner Wahl alle Stammtischtermine.
F Eigene Funktionen und
Ablaufsteuerung

Daniel Obszelka
380 F Eigene Funktionen und Ablaufsteuerung

28 Kontrollstrukturen

Dort wollen wir in diesem Kapitel hin:

• Anweisungsblöcke einführen
• Verzweigungen (if / else) einsetzen lernen
• Schleifen zur wiederholten Ausführung eines Anweisungsblockes kennenlernen

• die Lesbarkeit des Codes weiter erhöhen

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:

• Wie berechnen wir die ersten n Fibonaccizahlen? (28.3.1)


• Ab dem wievielten Folgenglied übersteigt die Fibonaccifolge erstmals den Wert
100? (28.3.4), (28.3.6)

Diejenigen, die bei wiederholter Auswertung an sapply() denken, müssen wir an


dieser Stelle leider enttäuschen. Das funktioniert in diesem Fall nicht.

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
...
}

Anweisungsblöcke sind vor allem in folgenden Situationen notwendig:

1. Wenn/Dann-Verzweigung (if/else): Wenn eine Bedingung erfüllt ist,


dann wird Anweisungsblock 1 ausgeführt, sonst Anweisungsblock 2. (28.2)
2. Schleifen (for und while): Der ganze Anweisungsblock wird in jedem Schlei-
fendurchlauf (in jeder Iteration) durchlaufen. (28.3)
3. Funktionen: Der Anweisungsblock wird pro Funktionsaufruf als Ganzes ab-
gearbeitet. Funktionen besprechen wir in (29).

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.

28.2 Bedingte Anweisung und Verzweigung

28.2.1 Bedingte Anweisung – if

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 <- -1 > x <- 1

> if (x < 0) { > if (x < 0) {


+ x <- x * (-2) + x <- x * (-2)
+ } + }

> 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.

28.2.2 Wenn/Dann-Verzweigung – if, else

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".

> x <- c(1, 2, 3, NA) # Beispielvektor mit NA

> 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

Hoppala! Dieser Ausgabe wollen wir nicht ganz trauen!


28 Kontrollstrukturen 383

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:

> x <- c(1, 2, 3, NA) # Beispielvektor mit NA

> 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

28.2.3 if/else vs. ifelse()

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:

• if/else ist im Gegensatz zu ifelse() nicht vektorwertig, es kann im-


mer nur ein Wahrheitswert verwendet werden. Man könnte quasi if/else als
Spezialfall von ifelse() auffassen, wobei der Parameter test aus nur einem
Wahrheitswert bestehen darf.
• if/else arbeitet schneller als ifelse(), wenn wir nur einen Wahrheitswert
verwenden. Der Grund dafür: Während ifelse() stets beide Fälle auswertet
und dann den zum Wahrheitswert passenden Fall zurückgibt, wird bei if/else
lediglich der zum Wahrheitswert passende Fall bearbeitet.
• Innerhalb von if/else können wir Objekte überschreiben, innerhalb der Funk-
tion ifelse() ist das nicht möglich. Diesen Aspekt schauen wir uns beim
Thema Funktionen in (29.6) und (30.1) genau an.

Man entwickelt mit der Zeit ein Gespür dafür, wann if/else und wann ifelse()
sinnvoll ist.

28.3 Schleifen

Schleifen ermöglichen es, Anweisungsblöcke mehrmals zu durchlaufen. Es gibt zwei


Arten von Schleifen, die wir lernen werden:

• 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.

28.3.1 for-Schleife – for

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:

> fibo <- numeric(5) # Numerischen Vektor der Länge 5 erstellen


> fibo[1:2] <- 1 # 1. und 2. Fibonaccizahl per Definition

> fibo[3] <- fibo[1] + fibo[2] # 3. Fibonaccizahl berechnen


> fibo[4] <- fibo[2] + fibo[3] # 4. Fibonaccizahl berechnen
> fibo[5] <- fibo[3] + fibo[4] # 5. Fibonaccizahl berechnen

> fibo # Ergebnis ausgeben


[1] 1 1 2 3 5

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:

> fibo <- numeric(5) # Numerischen Vektor der Länge 5 erstellen


> fibo[1:2] <- 1 # 1. und 2. Fibonaccizahl per Definition

> 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]

> fibo # Ergebnis ausgeben


[1] 1 1 2 3 5

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:

> fibo <- numeric(5) # Numerischen Vektor der Länge 5 erstellen


> fibo[1:2] <- 1 # 1. und 2. Fibonaccizahl per Definition

> for (i in 3:length(fibo)) {


+ fibo[i] <- fibo[i - 2] + fibo[i - 1]
+ }

> fibo # Ergebnis ausgeben


[1] 1 1 2 3 5

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.

28.3.2 Zwei Anwendungsvarianten der for-Schleife

Wir stürzen uns gleich in ein Beispiel.


Beispiel: Gegeben ist ein Vektor x. Quadriere jedes Element und drucke das Er-
gebnis auf die Console.

> x <- c(5, -2, 3)

Wir betrachten zwei Varianten:

> # Indexbasierte Schleife > # Inhaltsbasierte Schleife


> for (i in 1:length(x)) { > for (y in x) {
+ print(x[i]^2) + print(y^2)
+ } + }
[1] 25 [1] 25
[1] 4 [1] 4
[1] 9 [1] 9

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

Indexbasierte Schleife Inhaltsbasierte Schleife


> # Iteration Nummer 1 > # Iteration Nummer 1
> i <- 1 # nächstes Element > y <- x[1] # nächstes Element
> x[i] > y
[1] 5 [1] 5
> x[i]^2 > y^2
[1] 25 [1] 25

> # Iteration Nummer 2 > # Iteration Nummer 2


> i <- 2 # nächstes Element > y <- x[2] # nächstes Element
> x[i] > y
[1] -2 [1] -2
> x[i]^2 > y^2
[1] 4 [1] 4

> # Iteration Nummer 3 > # Iteration Nummer 3


> i <- 3 # nächstes Element > y <- x[3] # nächstes Element
> x[i] > y
[1] 3 [1] 3
> x[i]^2 > y^2
[1] 9 [1] 9

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.

> # Initialisierung mit NULL > # Bessere Initialisierung


> erg <- NULL > erg <- numeric(length(x))

> for (i in 1:length(x)) { > for (i in 1:length(x)) {


+ erg[i] <- x[i]^2 + erg[i] <- x[i]^2
+ } + }

> erg > erg


[1] 25 4 9 [1] 25 4 9

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

28.3.3 Sequenzielle vs. parallele Berechnung, Schleifenvermeidung

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

> # Effiziente Berechnung der quadrierten Werte


> erg <- x ^ 2
> erg
[1] 25 4 9

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).

> fibo <- numeric(5) # Numerischen Vektor der Länge 5 erstellen


> fibo[1:2] <- 1 # 1. und 2. Fibonaccizahl per Definition

> for (i in 3:length(fibo)) {


+ fibo[i] <- fibo[i - 2] + fibo[i - 1]
+ }

> fibo # Ergebnis ausgeben


[1] 1 1 2 3 5

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

> # Berechne die 1. bis 5. Fibonaccizahl mit der geschlossenen Formel


> k <- 1:5
> fibo <- 1 / sqrt(5) * (((1 + sqrt(5)) / 2)^k - ((1 - sqrt(5)) / 2)^k)
> fibo
[1] 1 1 2 3 5
28 Kontrollstrukturen 389

28.3.4 while-Schleife – while

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
}

Solange bedingung erfüllt ist, wird die while-Schleife durchlaufen.


Betrachten wir zum Einstieg ein einfaches Codebeispiel:

> i <- 1

> while (i < 4) {


+ print(i)
+ i <- i + 1
+ }
[1] 1
[1] 2
[1] 3

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

> # Iteration Nummer 1 > # Iteration Nummer 2


> i # i zu Iterationsbeginn > i # i zu Iterationsbeginn
[1] 1 [1] 2
> i < 4 > i < 4
[1] TRUE [1] TRUE
> # TRUE => Schleifenrumpf ausführen > # TRUE => Schleifenrumpf ausführen
> print(i) # drucke i aus > print(i) # drucke i aus
[1] 1 [1] 2
> i <- i + 1 # i hochzählen > i <- i + 1 # i hochzählen

> # Iteration Nummer 3 > # Iteration Nummer 4


> i # i zu Iterationsbeginn > i # i zu Iterationsbeginn
[1] 3 [1] 4
> i < 4 > i < 4
[1] TRUE [1] FALSE
> # TRUE => Schleifenrumpf ausführen > # FALSE => Schleife abbrechen
> print(i) # drucke i aus
[1] 3
> i <- i + 1 # i hochzählen
390 F Eigene Funktionen und Ablaufsteuerung

Beispiel: Wir setzen das Fibonaccizahlenbeispiel von (28.3.1) fort. Ab dem wieviel-
ten Folgenglied übersteigt die Fibonaccifolge ak erstmals den Wert 100?

> # Ergebnisvektor initialisieren


> fibo <- c(1, 1)

> # Der Iterationszähler


> iter <- 2

> while (fibo[iter] <= 100) {


+ # Iterationszähler hochzählen und neue Fibonaccizahl anhängen
+ iter <- iter + 1
+ fibo[iter] <- fibo[iter - 2] + fibo[iter - 1]
+ }
> fibo
[1] 1 1 2 3 5 8 13 21 34 55 89 144

> # Anzahl der Folgenglieder ausgeben


> iter
[1] 12
> length(fibo)
[1] 12

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.

28.3.5 Schleifensteuerung – break, next

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

Wir betrachten ein simples Codebeispiel:

> for (i in 1:10) {


+ if (i <= 4) {
+ next
+ }
+ print(i)
+ if (i >= 8) {
+ break
+ }
+ }
[1] 5
[1] 6
[1] 7
[1] 8

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.

28.3.6 Endlosschleifen – while (TRUE), repeat

Endlosschleifen sind Schleifen ohne Abbruchsbedingung, das heißt sie würden


theoretisch unendlich lange laufen. Endlosschleifen können wir als spezielle while-
Schleife mit der Schleifenbedingung TRUE aufschreiben. Oder wir nehmen alternativ
eine repeat-Schleife. Wir skizzieren beide Varianten:

while (TRUE) { repeat {


# Anweisungsblock # Anweisungsblock
} }

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?

> # Ergebnisvektor initialisieren


> fibo <- c(1, 1)
> fibo
[1] 1 1
392 F Eigene Funktionen und Ablaufsteuerung

> while (TRUE) {


+ # n ... Die wievielte Fibonaccizahl berechnen wir jetzt?
+ n <- length(fibo) + 1
+ fibo[n] <- fibo[n - 2] + fibo[n - 1]
+
+ # Abbruchsbedingung
+ if (fibo[n] > 100) {
+ break
+ }
+ }

> 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.

28.4 Aus der guten Praxis

28.4.1 Programmierstil: Einrückungen

Selbst R-Profis bekommen bei folgendem Code einen kleinen Schwächeanfall:

> 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}}}

Es ist kaum möglich, den Programmablauf nachzuvollziehen. Der folgende äquiva-


lente Code sieht dagegen viel übersichtlicher aus:

> 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.

In Ausnahmefällen können wir manchmal von diesen Regeln abweichen.

28.4.2 Fallbeispiel: Zelluläre Automaten

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

Dann erfolgt die Zustandsanpassung:


+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

# 1.) Definiere Parameter


n <- 5 # Anzahl der Zellen
tmax <- 10 # Maximale Anzahl an Iterationen

# Die Matrix X (T x n) speichert die Zustände, wobei T nicht fix.


# Zeile t: Zustände zum Zeitpunkt t. Spalte i: Zustandsentwicklung von Zelle i.
# definiere X(1); können auch selbst Startzustand vorgeben
x <- sample(-1:1, size = n, replace = TRUE)
X <- t(x)

# 2.) Äußere Schleife definieren


for (t in 2:tmax) {

# 3.) Z(t) berechnen


z <- rep(NA, n)

# Innere Schleife definineren


for (j in 1:n) {
if (j == 1) {
# Linke Zelle
z[1] <- X[t - 1, 1] + X[t - 1, 2]
}
else {
if (j == n) {
# Rechte Zelle
z[n] <- X[t - 1, n - 1] + X[t - 1, n]
}
else {
# Zelle in der Mitte
z[j] <- sum(X[t - 1, (j - 1):(j + 1)])
}
}
}

# 4.) Zustände updaten und an die Matrix anhängen


xt <- ifelse(z > 0, 1, ifelse(z < 0, -1, 0))
X <- rbind(X, xt)

# 5.) Prüfe auf Konvergenz: Wenn alle Zellen unverändert sind,


# breche die (äußere) Schleife ab.
if (all(X[t - 1, ] == X[t, ])) break
}

# 6.) Ausgabe des Automaten


colnames(X) <- paste0("X_", 1:ncol(X))
rownames(X) <- paste("t =", 1:nrow(X))
X

Abbildung 28.1: R-Code für den zellulären Automaten


396 F Eigene Funktionen und Ablaufsteuerung

28.5 Abschluss

28.5.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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?

b) Generiere in R mit Hilfe der in diesem Kapitel genannten Methoden diese


Reihe (20 Zahlen insgesamt).
c) Können wir diese Reihe auch ohne Schleifen generieren? Wenn ja, wie?
2. Gegeben ist ein beliebiger numerischer Vektor x, zum Beispiel:

> 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

b) res <- -Inf

for (i in 1:length(x)) {
if (x[i] > 0) {
next()
}
if (x[i] > res) {
res <- x[i]
}
}

res

c) res <- rep(x[1], length(x))

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)?

a) res <- 1:4 b) res <- 1


for (j in 2:4) { for (j in 2:4) {
if (j == 3) if (j == 3)
res[j] <- res[j - 1] res[j] <- res[j - 1]
else else
res[j] <- sum(res) res[j] <- sum(res)
} }

4. Unser Spielwürfel ist gefragt.


a) Würfle solange mit einem 6-seitigen fairen Würfel, bis die Augensumme
20 überschritten wird. Gib die Anzahl der benötigten Würfe aus.
b) Wie 4a). Jetzt wird aber jedes Mal der Wert 1 von der aktuellen Augen-
summe abgezogen, wenn diese eine gerade Zahl ist.
c) Erzeuge nun für die Aufgabenstellungen von 4a) und 4b) jeweils einen
Vektor, der die Realisierungen von n = 10000 Versuchen enthält. Versuche
dabei nach Möglichkeit auf (weitere) Schleifen zu verzichten.

Hinweis: Sei kreativ. Wenn du noch keine Möglichkeit findest, Schleifen


zu vermeiden bzw. den Anteil an vektorwertigen Funktionen zu erhöhen,
so wird dich (33) inspirieren ;-)
5. In (27) über Datumsklassen haben wir die Geburtsdaten von uns dreien be-
trachtet (ersetze dazu Neujahr98 durch deinen Namen und 01.01.1998 durch
dein Geburtsdatum). Zusätzlich haben wir in (27.5.3) Felix2902 hinzugefügt.

> gebtag <- c(Daniel = "07.07.1986", Andreas = "12.09.1973",


+ Neujahr98 = "01.01.1998", Felix2902 = "29.02.1996")
> gebtag
Daniel Andreas Neujahr98 Felix2902
"07.07.1986" "12.09.1973" "01.01.1998" "29.02.1996"

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

29 Eigene Funktionen: Grundlagen

Dort wollen wir in diesem Kapitel hin:

• eigene Funktionen schreiben

• die Feinheiten von Funktionsdeklarationen kennenlernen


• Einblicke in Environments und Scoping erlangen

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

29.1 Eigene Funktionen schreiben – function

Mit Hilfe des Schlüsselwortes function können wir eine eigene Funktion definie-
ren. Das sieht schematisch so aus:

funktionsname <- function(Parameter ) {


# Kommentar der Funktion: Was tut sie? Was bedeuten die Parameter?
# Was wird zurückgegeben

Diverse Anweisungen und Befehle


Rückgabe
}

29.1.1 Funktionsname

Zunächst überlegen wir uns einen Funktionsnamen. Dieser sollte bzw. muss folgende
Anforderungen erfüllen:

• Er muss (bis auf weiteres) mit einem Buchstaben anfangen.


• Er sollte sprechend sein, das heißt, man erkennt idealerweise anhand des
Namens, was die Funktion tut.
• Vorhandene Objekte sollen nicht überschrieben werden.

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.

29.1.3 Rückgabe – return(), invisible()

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.

> # return-Rückgabe > # letzter Ausdruck > # invisible-Rückgabe


> f1 <- function(x) { > f2 <- function(x) { > f3 <- function(x) {
+ return(x) + mode(x) + mode(x)
+ print("unreachable") + x + invisible(x)
+ } + } + }
> f1(1:5) > f2(1:5) > f3(1:5)
[1] 1 2 3 4 5 [1] 1 2 3 4 5
> res <- f1(1:3) > res <- f2(1:3) > res <- f3(1:3)
> res > res > res
[1] 1 2 3 [1] 1 2 3 [1] 1 2 3

Im linken Code wird print("unreachable") nicht mehr ausgeführt, da die Funktion


zuvor bereits wegen return(x) beendet und das Objekt x zurückgegeben wird. Im
mittleren und rechten Code wird der letzte Ausdruck x zurückgegeben, mode(x)
wird dabei weder auf die Console gedruckt (vgl. (28.1)), noch zurückgegeben.
Wenn das retournierte Objekt nicht auf eine Variable zugewiesen wird, so wird es
normalerweise auf die Console gedruckt. So geschehen bei f1(1:5) und f2(1:5).
Wird das Rückgabeobjekt mit invisible() ummantelt, so wird das Rückgabeobjekt
nicht auf die Console gedruckt. So geschehen bei f3(1:5). Weisen wir das Ergebnis
auf eine Variable (hier res) zu, so können wir in allen Fällen darauf zugreifen.
Beispiel: Schreibe eine Funktion, welche die beiden Lösungen der quadratischen
Gleichung ax2 + bx + c = 0 berechnet (vgl. (2.2.1) auf Seite 6).

> quadratgleichung <- function(a, b, c) {


+ res <- (-b + c(-1, 1) * sqrt(b^2 - 4 * a * c)) / (2 * a)
+ return(res)
+ }

> # Funktion für a = 2, b = -5, c = 3 aufrufen


> quadratgleichung(2, -5, 3)
[1] 1.0 1.5

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.

> is.symmetric <- function(X) {


+ if (all(X == t(X))) {
+ ergebnis <- TRUE
+ }
+ else {
+ ergebnis <- FALSE
+ }
+ return(ergebnis)
+ }

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:

> is.symmetric <- function(X) {


+ if (all(X == t(X)))
+ return(TRUE)
+ return(FALSE)
+ }

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?

> # Übergebe is.symmetric() einen Vektor


> is.symmetric(1:10)
[1] TRUE

Ups! Eigentlich sollte es hier kein Ergebnis geben, da wir einen Vektor übergeben
haben. Dies sollten wir abfangen.
29 Eigene Funktionen: Grundlagen 403

29.2 Fehlermeldungen und Warnungen – stop(), warning()

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.

Beispiel: Überprüfe in der Funktion is.symmetric() auf der vorherigen Seite, ob


das Argument X eine quadratische Matrix ist und breche gegebenenfalls die Funktion
ergebnislos (mit einer Fehlermeldung) ab.
Die Idee: Zuerst fragen wir ab, ob X keine Matrix ist. Ist X keine Matrix, so brechen
wir die Funktion ab und geben einen schönen Fehlertext zurück.

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.

> is.symmetric <- function(X) {


+ text <- "\nX muss eine quadratische Matrix sein."
+
+ if (!is.matrix(X)) {
+ stop(text) # Keine Matrix => Fehler!
+ }
+
+ # Wir wissen: X ist eine Matrix => ncol und nrow existieren
+ if (ncol(X) != nrow(X)) {
+ stop(text) # Keine quadratische Matrix => Fehler!
+ }
+
+ if (all(X == t(X))) {
+ return(TRUE)
+ }
+ return(FALSE)
+ }

Kontrollieren wir einmal, ob unsere Funktion funktioniert.

> # Übergebe einen Vektor


> is.symmetric(1:10)
Fehler in is.symmetric(1:10) :
X muss eine quadratische Matrix sein.
404 F Eigene Funktionen und Ablaufsteuerung

> Temp <- rbind(1:3, 2:4)


> Temp
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 2 3 4

> # Übergebe nichtquadratische Matrix


> is.symmetric(Temp)
Fehler in is.symmetric(Temp) :
X muss eine quadratische Matrix sein.

29.3 Bedingungen verknüpfen zum Zweiten – "&&", "||"

Manche hätten das letzte Beispiel von Seite 403 im ersten Reflex so gelöst:

> is.symmetric <- function(X) {


+ if (is.matrix(X) & nrow(X) == ncol(X)) {
+ if (all(X == t(X))) {
+ return(TRUE)
+ }
+ else {
+ return(FALSE)
+ }
+ }
+ stop("\nX muss eine quadratische Matrix sein.")
+ }

Sieht auf den ersten Blick gut aus. Auf den zweiten Blick folgt die Ernüchterung:

> # Übergebe einen Vektor


> is.symmetric(1:10)
Fehler in if (is.matrix(X) & nrow(X) == ncol(X)) { : Argument hat Länge 0

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.

> X <- 1:10


> nrow(X)
NULL
> nrow(X) == ncol(X)
logical(0)
> is.matrix(X) & nrow(X) == ncol(X)
logical(0)

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:

> X <- 1:10


> is.matrix(X) && nrow(X) == ncol(X)
[1] FALSE

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!

> is.symmetric <- function(X) {


+ if (is.matrix(X) && nrow(X) == ncol(X)) {
+ if (all(X == t(X))) {
+ return(TRUE)
+ }
+ else {
+ return(FALSE)
+ }
+ }
+ stop("\nX muss eine quadratische Matrix sein.")
+ }

> # Übergebe einen Vektor


> is.symmetric(1:10)
Fehler in is.symmetric(1:10) :
X muss eine quadratische Matrix sein.

Spannend, dass ein Zeichen so einen großen Unterschied machen kann ;-)

29.4 Parameter und Argumente

29.4.1 Statische Defaultwerte und Wrapper-Funktionen

Parameter können mit Defaultwerten versehen werden, also Standardwerten,


die verwendet werden, wenn der Parameter nicht spezifiziert wird. Die Syntax:

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 p-Norm ist wie folgt definiert:


n
!1/p
p
X
kxkp := |xi | (29.1)
i=1

Als Funktionsname wählen wir norm aus.

> norm <- function(x, p = 2) {


+ if (!is.vector(x) || !is.numeric(x))
+ stop("\nx muss ein numerischer Vektor sein!")
+ return(sum(abs(x)^p) ^ (1/p))
+ }

In der Funktion norm() definieren wir mit p = 2 einen statischen Defaultwert,


der Defaultwert hängt also nicht von anderen Parametern ab (vgl. (4.3.2)). Außerdem
sehen wir in !is.vector(x) || !is.numeric(x) eine schöne Anwendung für den
||-Operator. Zeit, um unsere Funktion zu testen!

> # Verwende Defaultwert p = 2 > # Spezifiziere p = 1


> norm(1:4) > norm(1:4, p = 1)
[1] 5.477226 [1] 10

Bemerkung: Im Fall p = 1 sprechen wir von der Betragsnorm (oder auch L1 -


Norm). Im Fall p = 2 von der euklidischen Norm (oder auch L2 -Norm).
Dies ist ein gutes Beispiel dafür, wie sinnvoll es ist, Funktionen möglichst allgemein
zu programmieren. Wir hätten auch eine Funktion ausschließlich für die euklidische
Norm schreiben können. Mit dieser hätten wir allerdings keine Betragsnorm berech-
nen können, was eine zweite Funktion erfordern würde.
Wir können Spezialfunktionen (auch Wrapper-Funktionen genannt) zum Beispiel
für p = 1 und p = 2 ableiten, die uns in Spezialfällen die Anwendung erleichtern.

> # Wrapper für Betragsnorm > # Wrapper für euklidische Norm


> norm1 <- function(x) { > norm2 <- function(x) {
+ return(norm(x, p = 1)) + return(norm(x, p = 2))
+ } + }

> # norm1() > # norm() mit p = 1


> norm1(1:4) > norm(1:4, p = 1)
[1] 10 [1] 10

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

Manchmal macht es aber auch Sinn, für Spezialfälle unabhängige Funktionen zu


definieren und sie in eine Masterfunktion einzubauen. Nämlich dann, wenn es für die-
se Spezialfälle eine einfachere Formel oder/und eine schnellere Auswertungsstrategie
gibt. Schauen wir uns auch diese Möglichkeit für p = 1 und p = 2 an.

> # Wrapper für p = 1 > # Wrapper für p = 2


> norm1 <- function(x) { > norm2 <- function(x) {
+ return(sum(abs(x))) + return(sum(x^2)^(1/2))
+ } + }

> # Masterfunktion norm


> norm <- function(x, p = 2) {
+ if (!is.vector(x) || !is.numeric(x))
+ stop("\nx muss ein numerischer Vektor sein!")
+ if (p == 1) return(norm1(x))
+ if (p == 2) return(norm2(x))
+ return(sum(abs(x)^p) ^ (1/p))
+ }

Für p = 1 und p = 2 definieren wir die unabhängigen Spezialfunktionen norm1()


und norm2() und bauen diese in die Masterfunktion norm()Pein. Für die Betrags-
n
norm können wir Formel (29.1) beispielsweise zu kxk1 := i=1 |xi | vereinfachen,
wir ersparen das Potenzieren und die Berechnung der Wurzel. Für p ∈
/ {1, 2} wird
die allgemeine Formel herangezogen.

29.4.2 Dynamische Defaultwerte

Wir können auch dynamische Defaultwerte definieren. Also Defaultwerte, die


von anderen Parametern abhängen.
Beispiel: Schreibe eine Funktion, welche die bequeme Generierung von Würfelwür-
fen ermöglicht. Die Anzahl der Seiten des Würfels soll frei gewählt werden können
(Defaultwert 6). Standardmäßig soll jede Seite dieselbe Wahrscheinlichkeit haben.

> rwuerfel <- function(n, seiten = 6, prob = rep(1, seiten)) {


+ return(sample(1:seiten, size = n, replace = TRUE, prob = prob))
+ }

Hier wird jeder Seite defaultmäßig das gleiche Wahrscheinlichkeitsgewicht zugewie-


sen. Dazu greifen wir in prob = rep(1, seiten) auf den Parameter seiten zurück,
prob hat also einen dynamischen Defaultwert. Den Rest erledigt die Funktion
sample(). Fertig!

> # n = 10, 6 Seiten > # n = 2, 2 Seiten


> rwuerfel(10) > rwuerfel(10, seiten = 2)
[1] 3 6 4 1 1 2 5 1 5 4 [1] 1 2 1 1 2 1 2 2 2 1
408 F Eigene Funktionen und Ablaufsteuerung

29.4.3 Unspezifizierte Parameter

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.

Lösung – Variante 2 mit missing()

> mittelwert <- function(x, weights, na.rm = TRUE) {


+ # Keine Gewichte -> gewöhnliches Mittel
+ if (missing(weights))
+ return(mean(x, na.rm = na.rm))
+
+ # Jetzt wissen wir: weights ist spezifiziert.
+ # Längen müssen übereinstimmen.
+ if (length(weights) != length(x)) {
+ stop("\nweights hat nicht die selbe Länge wie x!")
+ }
+
+ # Falls na.rm TRUE ist, entferne fehlende Werte
+ if (na.rm) {
+ weights <- weights[!is.na(x)]
+ x <- x[!is.na(x)]
+ }
+
+ # Ergebnis berechnen und zurückgeben
+ return(sum(x * weights / sum(weights)))
+ }

Betrachten wir kurz einige Anwendungsbeispiele.

> # Ohne Gewichte und mit NA > # Mit Gewichten


> mittelwert(c(1:3, NA)) > mittelwert(1:3, c(4, 1, 1))
[1] 2 [1] 1.5

> # x und gewichte ungleich lang


> mittelwert(1:3, c(4, 1, 1, 1))
Fehler in mittelwert(1:3, c(4, 1, 1, 1)) :
weights hat nicht die selbe Länge wie x!
29 Eigene Funktionen: Grundlagen 409

Lösung – Variante 1 mit NULL und is.null()

Die ersten vier Zeilen könnten auch so aussehen (der Rest bleibt gleich):

> mittelwert <- function(x, weights = NULL, na.rm = TRUE) {


# Keine Gewichte -> gewöhnliches Mittel
if (is.null(weights))
return(mean(x, na.rm = na.rm))

Diese Funktion gibt es bereits in R und trägt den Namen weighted.mean().

29.4.4 Objekte switchen – switch()

Fallunterscheidungen können wir mit if- und else-Anweisungen durchführen, diese


sind dann aber immer binär. Das kann manchmal unübersichtlich werden. Die Funk-
tion switch() ermöglicht es, viele mögliche Fälle gleichzeitig abzuprüfen:

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.

> mittel <- function(x, type = "arith", na.rm = TRUE) {


+ if (na.rm) {
+ x <- x[!is.na(x)]
+ }
+
+ ergebnis <- switch(type,
+ arith = mean(x),
+ geom = prod(x)^(1/length(x)),
+ harm = length(x) / sum(1/x),
+ quadr = sqrt(mean(x^2)),
+ NULL # falls type zu keiner linken Seite passt
+ )
+
+ if (is.null(ergebnis)) {
+ warning("type ungültig! Gebe arithmetisches Mittel zurück.")
+ ergebnis <- mean(x)
+ }
+
+ return(ergebnis)
+ }
410 F Eigene Funktionen und Ablaufsteuerung

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.

> # Beispielanwendungen für unsere Funktion mittel()


> x <- c(60, 120)

> mittel(x) > mittel(x, "harm")


[1] 90 [1] 80
> mittel(x, "geom") > mittel(x, "quadr")
[1] 84.85281 [1] 94.86833

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).

> # Übergebe ungültigen type


> mittel(x, "Mich gibt es nicht")
Warnung in mittel(x, "Mich gibt es nicht")
type ungültig! Gebe arithmetisches Mittel zurück.
[1] 90

Wenn wir in switch() Zahlen zur Fallunterscheidung verwenden, so müssen wir


diese auf der linken Seite unter Anführungszeichen stellen.

> fun <- function(type) {


+ switch(type, "1" = "typ1", "2" = "typ2", "typUnbekannt")
+ }

> fun(1) > fun(2) > fun(3)


[1] "typ1" [1] "typ2" [1] "typUnbekannt"

29.4.5 Das Dreipunkteargument – ..., list(...)

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:

paste(..., sep = " ", collapse = NULL)


29 Eigene Funktionen: Grundlagen 411

> a <- 2
> b <- 5

> # Übergebe 6 Elemente


> paste("Das Ergebnis von ", a, " + ", b, " ist ", a + b, sep = "")
[1] "Das Ergebnis von 2 + 5 ist 7"

> # Übergebe 2 Elemente


> paste(c("a", "b"), c(a, b), sep = " = ", collapse = " und ")
[1] "a = 2 und 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:

> # collapse muss exakt benannt werden!


> paste(c("a", "b"), c(a, b), sep = " = ", coll = " und ")
[1] "a = 2 = und " "b = 5 = und "

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

apply(X, MARGIN, FUN, ...)

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.

> M <- matrix(1:6, ncol = 3)


> M
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6

> # 20%- und 80%-Quantil für jede Spalte


> apply(M, 2, quantile, probs = c(0.2, 0.8))
[,1] [,2] [,3]
20% 1.2 3.2 5.2
80% 1.8 3.8 5.8

> # Jede Spalte absteigend sortieren


> apply(M, 2, sort, decreasing = TRUE)
[,1] [,2] [,3]
[1,] 2 4 6
[2,] 1 3 5
412 F Eigene Funktionen und Ablaufsteuerung

Schreiben wir eine Funktion, in der das Dreipunkteargument ... vorkommt, so


können wir das Dreipunkteargument entweder einer anderen Funktion weiterreichen
(siehe Übungsbeispiel 1 in (29.8.3)) oder die Elemente mittels list(...) in eine
Liste packen, mit deren Hilfe wir sodann die Argumente weiterverarbeiten können.
Zweiteres sehen wir uns jetzt an.

> fun <- function(...) {


+ args <- list(...) # packe alle Elemente in eine Liste
+ print(args) # drucke die Liste auf die Console
+
+ cat("Folgende Argumente für ... übergeben:\n")
+ cat(paste0(1:length(args), ".) ", names(args), collapse = "\n"))
+ cat("\n")
+ }

> fun(x = 1:5, na.rm = TRUE)


$x
[1] 1 2 3 4 5
$na.rm
[1] TRUE
Folgende Argumente für ... übergeben:
1.) x
2.) na.rm

> fun(x = 2, y = 5:8, f = sum)


$x
[1] 2
$y
[1] 5 6 7 8
$f
function (..., na.rm = FALSE) .Primitive("sum")
Folgende Argumente für ... übergeben:
1.) x
2.) y
3.) f

Bemerkung: args ist ein weit verbreitetes Kürzel für arguments.

29.5 Aus der guten Praxis

29.5.1 Programmierstil: Funktionskommentare

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

Beispiel: Kommentiere die Funktion rwuerfel() von Seite 407.

> rwuerfel <- function(n, seiten = 6, prob = rep(1, seiten)) {


+
+ # Gibt n zufaellige Wuerfelwuerfe zurueck.
+ # n ........ Die Anzahl der gewuenschten Wuerfelwuerfe.
+ # seiten ... Die Anzahl der Seiten des Wuerfels.
+ # prob ..... Die Wahrscheinlichkeitsgewichte. Standardgemaess
+ # hat jede Seite die gleiche Wahrscheinlichkeit.
+
+ return(sample(1:seiten, size = n, replace = TRUE, prob = prob))
+ }

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.

> mittel <- function(x, type = "arith", na.rm = TRUE) {


+
+ # Diese Funktion berechnet verschiedene Mittelwerte
+ # für den Vektor x
+ # x ....... numerischer Datenvektor
+ # type .... Welcher Mittelwert soll berechnet werden?
+ # "arith": arithmetisches Mittel (default)
+ # "geom": geometrisches Mittel
+ # "harm": harmonisches Mittel
+ # "quadr": quadratisches Mittel
+ # na.rm ... TRUE: NA-Einträge werden entfernt, sonst nicht
+
+ if (na.rm) {
+ x <- x[!is.na(x)]
+ }
+
+ ergebnis <- switch(type,
+ arith = mean(x),
+ geom = prod(x)^(1/length(x)),
+ harm = length(x) / sum(1/x),
+ quadr = sqrt(mean(x^2)),
+ NULL
+ )
+
+ if (is.null(ergebnis)) {
+ warning("type ungültig! Gebe arithmetisches Mittel zurück.")
+ ergebnis <- mean(x)
+ }
+
+ return(ergebnis)
+ }
414 F Eigene Funktionen und Ablaufsteuerung

29.5.2 Fallbeispiel: Bisektion

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.

Der Bisektionsalgorithmus basiert auf den folgenden beiden Schritten:

1. Werte f in der Intervallsmitte m = (a + b)/2 aus.

2. Falls in [a, m] kein Vorzeichenwechsel stattfindet, setze a = m, sonst setze


b = m. Soll heißen: Ist sign f (a) = sign f (b) ⇔ f (a) · f (b) > 0, so setze a = m,
sonst b = m.

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.

Rechnung Vermutung Antwort


1. Rateversuch: (1 + 15)/2 8 größer
2. Rateversuch: (9 + 15)/2 12 kleiner
3. Rateversuch: (9 + 11)/2 10 größer
4. Rateversuch: (11 + 11)/2 11 korrekt

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

> bisektion <- function(f, intervall, eps = 10^(-9)) {


+ # Gibt eine Liste mit einer Nullstelle von f und dem Funktionswert
+ # an dieser Stelle zurueck.
+ # f ........... Objekt vom Typ function, gibt reelle Zahl zurück
+ # intervall ... Intervall, in dem gesucht werden soll.
+ # eps ......... Rechengenauigkeit
+
+ links <- min(intervall)
+ rechts <- max(intervall)
+
+ # Checke Vorzeichenbedingung
+ if (f(links) * f(rechts) > 0)
+ stop("Kein Vorzeichenwechsel im übergebenen Intervall!")
+
+ mitte <- (links + rechts) / 2
+
+ # Solange die absolute Abweichung von f(mitte) > eps
+ while(abs(f(mitte)) > eps) {
+ if (f(mitte) * f(links) > 0) {
+ # Vorzeichen wechselt nicht
+ links <- mitte
+ }
+ else {
+ # Vorzeichen wechselt
+ rechts <- mitte
+ }
+ mitte <- (links + rechts) / 2
+ }
+
+ # Gebe Liste zurück mit der näherungsweisen Nullstelle und
+ # dem Funktionswert.
+ return(list(Nullstelle = mitte, Funktionswert = f(mitte)))
+ }

> # Berechne die Wurzel aus 2


> fun <- function(x) x^2 - 2
> bisektion(fun, intervall = c(0, 2))
$Nullstelle
[1] 1.414214
$Funktionswert
[1] 3.154454e-11

> # Zur Kontrolle


> sqrt(2)
[1] 1.414214

Abbildung 29.1: R-Code und Anwendungsbeispiel für den Bisektionsalgorithmus


416 F Eigene Funktionen und Ablaufsteuerung

29.6 Environments und Scoping

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.

> zeilen <- 2


> spalten <- 5

> # Funktion, die eine Matrix generiert und zurückgibt.


> hugo <- function(spalten) {
+ zeilen <- zeilen + 1
+ spalten <- spalten + 1
+ M <- matrix(ncol = spalten, nrow = zeilen)
+ return(M)
+ }

> hugo(spalten = spalten) # 2 + 1 = 3 Zeilen, 5 + 1 = 6 Spalten


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

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.

Dann wird spalten um 1 erhöht. Im Gegensatz zu zeilen existiert spalten inner-


halb von hugo() als Parameter. Allerdings wird auch hier das übergebene spalten
kopiert, wodurch in hugo() ein eigenständiges Objekt spalten erzeugt wird.
Wir überzeugen uns davon, dass die äußeren Objekte zeilen und spalten nach
der Funktionsausführung unverändert geblieben sind und dass das Objekt M, das
innerhalb von hugo() erstellt wurde, nach außen nicht sichtbar 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!

> hugo() # spalten hat keinen Standardwert.


Fehler in hugo() : Argument "spalten" fehlt (ohne Standardwert)

R greift also nicht auf spalten <- 5 zurück.


Wir fassen unsere Beobachtungen zusammen:

• 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!

Regel 16: Greife in Funktionen nicht auf Objekte außerhalb zu!

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.

29.7 Funktionen überschreiben – "::", rm()

Angenommen, wir hätten in (29.4.3) unsere Funktion nicht mittelwert(), sondern


mean() genannt. Wir zeigen, welche Konsequenzen das gehabt hätte.

> # Die wohlbekannte Funktion mean() aus dem Paket base


> mean(1:3)
[1] 2

> # Überschreibe die Funktion mean()


> mean <- function(x) {
+ return("Mittelwert")
+ }

> mean(1:3)
[1] "Mittelwert"
418 F Eigene Funktionen und Ablaufsteuerung

Frage: Wie können wir auf die wohlbekannte Funktion zugreifen?

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.

> # Unsere Funktion > # Funktion des Pakets base


> mean(1:3) > base::mean(1:3)
[1] "Mittelwert" [1] 2

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"

> # Lösche unsere Funktion


> rm(mean)

> # Jetzt wieder Zugriff auf Funktion im Paket base


> mean(1:3)
[1] 2

29.8 Abschluss

29.8.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

• Wie können wir das Dreipunkteargument innerhalb von Funktionen weiterver-


arbeiten und auf Argumente des Dreipunktearguments zugreifen? (29.4.5)
• Was passiert, wenn wir innerhalb einer Funktion auf ein Objekt zugreifen, das
in der Funktion nicht existiert? Können Objekte, die außerhalb einer Funktion
definiert sind, während der Funktionsausführung überschrieben werden? War-
um (nicht)? Sind Objekte, die innerhalb einer Funktion neu erstellt werden,
außerhalb der Funktion sichtbar? (29.6)
• Wie können wir auf Objekte eines bestimmten Paketes (zum Beispiel base)
zugreifen? Wie löschen wir eine selbstgeschriebene Funktion? (29.7)

Folgende drei Dinge beherzigen wir, wenn wir eigene Funktionen schreiben:

• Schreibe deine Funktionen möglichst allgemein (mach sie flexibel). (29.4.1)


• Kommentiere deine Funktionen! Beschreibe, was sie tun, welche Parameter wie
eingesetzt werden und was deine Funktion zurückgibt. (29.5.1)
• Greife innerhalb von selbstgeschriebenen Funktionen nach Möglichkeit nicht
auf Objekte außerhalb zu. (29.6)

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.

> sum <- function(..., na.rm = TRUE) {


+ # ... und na.rm werden an base::sum() weitergereicht.
+ base::sum(..., na.rm)
+ }

Voller Freude probiert er seinen Code aus:

> sum(1:4) > sum(c(1:4, NA))


[1] 11 [1] NA

Und prompt folgt die Ernüchterung! Wo liegt sein Fehler?

> rm(sum) # ACHTUNG: Lösche unsere Funktion wieder!


420 F Eigene Funktionen und Ablaufsteuerung

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.

3. Schreibe eine Funktion, welche die p-Distanz zweier numerischer Vektoren x


und y berechnet. Diese ist wie folgt definiert:

n
!1/p
p
X
kx − ykp := |xi − yi |
i=1

Berücksichtige dabei Folgendes:


• Verwende defaultmäßig die euklidische Norm (p = 2).
• Überprüfe, ob zwei numerische Vektoren der selben Länge übergeben wur-
den und breche gegebenenfalls mit einer sprechenden Fehlermeldung ab.
• Wird für y nichts übergeben, so soll die Distanz von x zum Ursprung
(zum Nullvektor) berechnet werden.
• Idealerweise soll die Funktion die Formel nicht selbst auswerten, sondern
auf die Funktion norm() aus dem Beispiel aus (29.4.1) ab Seite 405 zu-
rückgeführt werden.
Welchen Vorteil könnte die Rückführung auf die Funktion norm() haben?
4. Schreibe eine Funktion, die aus einem Datumsvektor die Jahre, Monate und
Tage als Zahlen zurückgibt. Wähle eine geeignete Datenstruktur für das Rück-
gabeobjekt.
30 Eigene Funktionen: Ergänzung und Vertiefung 421

30 Eigene Funktionen: Ergänzung und Vertiefung

Dort wollen wir in diesem Kapitel hin:

• Schleifen und Funktionen gegenüberstellen

• 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.

> # Daten laden


> # Evtl. Arbeitsverzeichnis wechseln bzw. absoluten/relativen Pfad angeben
> load("Test.RData")
> 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 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

30.1 *apply() vs. Schleifen

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():

1. *apply() wertet die zugrundeliegende Funktion für jede Komponente unab-


hängig aus; eine Interaktion zwischen den Komponenten ist nicht möglich.
2. Da *apply() eine Funktion ist, können wir hierarchisch übergeordnete Objekte
(Objekte, die außerhalb von *apply() stehen) nicht überschreiben. Außer wir
verwenden die globale Zuweisung "<<-" (Bitte NICHT einsetzen!)
3. Schleifen sind flexibler als apply() und sapply(). Es gibt einige Anwendun-
gen, für die Schleifen notwendig sind.

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:

> n <- 8 # berechne 8 Folgenglieder > n <- 8 # berechne 8 Folgenglieder


> fibo <- rep(1, n) > fibo <- rep(1, n)
> fibo > fibo
[1] 1 1 1 1 1 1 1 1 [1] 1 1 1 1 1 1 1 1

> sapply(3:n, function(i) { > sapply(3:n, function(i) {


+ fibo[i] <- fibo[i-1] + fibo[i-2] + fibo[i] <<- fibo[i-1] + fibo[i-2]
+ return(fibo[i]) + return(fibo[i])
+ }) + })
[1] 2 2 2 2 2 2 [1] 2 3 5 8 13 21

> fibo # nichts passiert > fibo # überschrieben


[1] 1 1 1 1 1 1 1 1 [1] 1 1 2 3 5 8 13 21

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

Beispiel: Wir betrachten das Dataframe test aus der Einleitung.

> 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!

> # Punktespalten bestimmen und in einer for-Schleife manipulieren


> names.pkt <- grep("Pkt", names(test), value = TRUE)
> names.pkt
[1] "Pkt1" "Pkt2" "Pkt"

> for (j in names.pkt) {


+ # Spalte j skalieren
+ test[[j]] <- test[[j]] / 40 * 100
+ }

> 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

30.2 Anonyme Funktionen – function()

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

> iqr <- function(x, na.rm = FALSE) {


+ # Berechnet die Interquartilsdistanz für einen Vektor x
+ return(diff(quantile(x, prob = c(0.25, 0.75), na.rm = na.rm)))
+ }

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.

> # Interquartilsdistanz von Pkt getrennt nach Gruppe berechnen


> tapply(test$Pkt, test$Gruppe, iqr, na.rm = TRUE)
A B
32.5 17.5

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.

> # Interquartilsdistanz von Pkt getrennt nach Gruppe berechnen


> tapply(test$Pkt, test$Gruppe, function(x)
+ return(diff(quantile(x, prob = c(0.25, 0.75), na.rm = TRUE)))
+ )
A B
32.5 17.5

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.

> sapply(test[names.pkt], function(x)


+ tapply(x, test$Gruppe, iqr, na.rm = TRUE)
+ )
Pkt1 Pkt2 Pkt
A 48.75 21.25 32.5
B 17.50 0.00 17.5
30 Eigene Funktionen: Ergänzung und Vertiefung 425

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)).

30.3 Rekursion – Recall()

Viele Problemstellungen lassen sich nach dem Teile-und-herrsche-Prinzip (engl. di-


vide and conquer) lösen: Gegeben ist ein Problem. Anstatt dieses Problem direkt zu
lösen, zerlege es in kleinere Teilprobleme (rekursiver Abstieg). Sofern das Teil-
problem klein genug ist, löse es direkt. Baue aus den Lösungen der Teilprobleme die
Lösung des Hauptproblems zusammen.
Beispiel: Schreibe eine Funktion, welche n! für ganzzahliges n ≥ 0 berechnet.
Klarerweise könnten wir die Funktion factorial() oder prod() bzw. cumprod()
verwenden. Aber nehmen wir an, wir haben diese Funktionen vergessen (sollte das
bei dir tatsächlich der Fall sein, dann ist es Zeit für eine Auffrischung ;-)).
Stattdessen machen wir Folgendes: Die Fakultätsfunktion kann mittels der folgenden
rekursiven Beziehung dargestellt werden:
(
1 falls n ≤ 1
n! = (30.1)
n · (n − 1)! sonst

So ist etwa 4! = 4 · 3! = 4 · 3 · 2! = 4 · 3 · 2 · 1! = 4 · 3 · 2 · 1 = 24. Aus dieser rekursiven


Beziehung basteln wir die folgenden Codes.

> fakultaet <- function(n) { > fakultaet <- function(n) {


+ # Abbruchsbedingung + # Abbruchsbedingung
+ if (n <= 1) + if (n <= 1)
+ return(1) + return(1)
+ +
+ return(n * fakultaet(n - 1)) + return(n * Recall(n - 1))
+ } + }

> 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.

> fakultaet(1:4) # Funktioniert nicht wie gewünscht


Warnung in if (n <= 1) return(1)
Bedingung hat Länge > 1 und nur das erste Element wird benutzt
[1] 1

> # Besser, da vektorwertig > # Hier noch besser


> factorial(1:4) > cumprod(1:4)
[1] 1 2 6 24 [1] 1 2 6 24

Beispiel: Schreibe eine Funktion, welche eine Matrix mit allen Permutationen eines
Vektors x bildet.

> permutation <- function(x) {


+ # Gibt eine Matrix mit allen Permutationen der Menge x zurück.
+
+ # Abbruchbedingung
+ if (length(x) <= 1)
+ return(t(x)) # t(x), damit eine Matrix erzeugt wird.
+
+ # Rekursiver Abstieg
+ M <- NULL # Initialisierung notwendig
+ for (i in 1:length(x)) {
+ M <- rbind(M, cbind(x[i], Recall(x[-i])))
+ }
+ return(M)
+ }

> permutation(1:3) > permutation(LETTERS[1:3])


[,1] [,2] [,3] [,1] [,2] [,3]
[1,] 1 2 3 [1,] "A" "B" "C"
[2,] 1 3 2 [2,] "A" "C" "B"
[3,] 2 1 3 [3,] "B" "A" "C"
[4,] 2 3 1 [4,] "B" "C" "A"
[5,] 3 1 2 [5,] "C" "A" "B"
[6,] 3 2 1 [6,] "C" "B" "A"

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.

permutation(x = c(1, 2, 3))


x = 1 2 3
length(x) <= 1 ? FALSE
i = 1
M = rbind(NULL, cbind(1, Recall(c(2, 3))))
permutation(x = c(2, 3))
x = 2 3
length(x) <= 1 ? FALSE
i = 1
M = rbind(NULL, cbind(2, Recall(3)))
permutation(3)
x = 3
length(x) <= 1 ? TRUE
return 3
M = 2 3
i = 2
M = rbind(2 3, cbind(3, Recall(2)))
permutation(2)
x = 2
length(x) <= 1 ? TRUE
return 2
M = 2 3
3 2
M = cbind(1, 2 3)
3 2
M = 1 2 3
1 3 2 # Beachte das Recycling von 1!
i = 2
...

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

Schreibe zwei Funktionen, welche die n. Fibonaccizahl berechnet:


1. mittels Schleife
2. mittels Rekursion
428 F Eigene Funktionen und Ablaufsteuerung

1.) Schleifenvariante

> fibonacci_iterativ <- function(n) {


+ if (!is.numeric(n) || n <= 0 || n %% 1 != 0)
+ stop("n muss eine positive ganze Zahl sein!")
+
+ # Abbruchbedingung
+ if (n <= 2)
+ return(1)
+
+ # Initialisierung und Berechnung
+ fibo <- rep(1, n)
+
+ for (i in 3:n) {
+ fibo[i] <- fibo[i - 2] + fibo[i - 1]
+ }
+ return(fibo[n])
+ }
> fibonacci_iterativ(7)
[1] 13

2.) Rekursion

> fibonacci_rekursiv <- function(n) {


+ if (!is.numeric(n) || n <= 0 || n %% 1 != 0)
+ stop("n muss eine positive ganze Zahl sein!")
+
+ # Abbruchbedingung
+ if (n <= 2)
+ return(1)
+
+ # Rückführung
+ return(Recall(n - 2) + Recall(n - 1))
+ }
> fibonacci_rekursiv(7)
[1] 13

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.

> # fibonacci_rekursiv(30) > # fibonacci_iterativ(30)

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

Bemerkung: Die Anzahl der Funktionsaufrufe folgt wiederum einer Fibonaccifolge!


Für n = 30 gilt: Die 30. und 29. Fibonaccizahl wird ein Mal berechnet, die 28. zwei
Mal, die 27. drei Mal, die 26. fünf Mal, die 25. acht Mal usw.
In Übungsaufgabe 2 auf Seite 13 haben wir angemerkt, dass es eine geschlossene
Formel für die Fibonaccifolge gibt.
In vielen Fällen bietet sich Rekursion aufgrund der Fragestellung an. Manchmal ist
Rekursion schneller als *apply() bzw. for-Schleifen, allerdings gibt es Grenzen:

• Rekursion verursacht oft viele unnötige Mehrfachberechnungen.


• Rekursion beansprucht viel Arbeitsspeicher. Ist das Problem so umfangreich,
dass der Arbeitsspeicher nicht ausreicht, so meldet sich R:
Fehler: Auswertung zu tief verschachtelt: unendliche Rekursion

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:

a b c aa ab ac ba bb bc ca cb cc aaa aab aac aba abb abc ...

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.

> # Hauptfunktion (wird vom Benutzer aufgerufen)


> sequenz <- function(buch, k) {
+
+ # Bestimmt eine Sequenz der Länge k mit den Zeichen von buch.
+ # Fuer buch = letters[1:3]: a b c aa ab ac ba bb bc ca bc ...
+ # buch ... Zeichensatz (zum Beispiel c("a", "b", "c"))
+ # k ...... Wie lange soll die Sequenz sein.
+
+ # Abbruchsbedingung
+ if (k <= length(buch)) {
+ # Hier reicht ein Zeichen fuer alle k Elemente aus.
+ return(buch[1:k])
+ }
+
+ # Aufruf der rekursiven Subfunktion: Hier brauchen wir mehr Zeichen.
+ liste <- list(buch)
+ liste1 <- sequenz.sub(buch, k - length(buch), stellen = 2, liste = liste)
+ return(unlist(liste1))
+ }
430 F Eigene Funktionen und Ablaufsteuerung

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).

> # Hilfsfunktion (wird von sequenz() aufgerufen)


> sequenz.sub <- function(buch, k, stellen, liste) {
+
+ # buch ...... Zeichensatz
+ # k ......... Laenge der Restsequenz
+ # stellen ... Anzahl der Zeichen
+ # liste ..... Liste, an welche die naechsten
+ # Sequenzglieder angehaengt werden.
+
+ # Maximal moegliche Anzahl der Sequenzglieder mit genau stellen Zeichen
+ anz <- length(buch)^stellen
+
+ # Alle stellen-langen Sequenzglieder erzeugen
+ temp <- paste0(rep(liste[[stellen - 1]], each = length(buch)), buch)
+
+ # Abbruchsbedingung
+ if (k <= anz) {
+ # Können alle restlichen Glieder abdecken
+ liste[[stellen]] <- temp[1:k]
+ return(liste)
+ }
+
+ # Rekursiver Abstieg
+ liste[[stellen]] <- temp
+ return(Recall(buch, k - anz, stellen + 1, liste))
+ }

Wir können genau length(buch)ˆstellen Glieder im aktuellen Durchlauf erzeugen.


Und zwar indem wir jede Zeichenfolge mit genau stellen - 1 Zeichen mit jedem
Zeichen der Menge buch verknüpfen.
Gilt wiederum k <= anz, dann geben wir die ersten k Sequenzglieder mit stellen-
vielen Elementen zurück. Andernfalls folgt der rekursive Abstieg mit k - anz Rest-
gliedern und mit (mindestens) stellen + 1 Zeichen).

> sequenz(c("A", "B", "C", "D"), k = 10)


[1] "A" "B" "C" "D" "AA" "AB" "AC" "AD" "BA" "BB"

> sequenz(c("a", "b", "c"), k = 24)


[1] "a" "b" "c" "aa" "ab" "ac" "ba" "bb" "bc" "ca" "cb" "cc"
[13] "aaa" "aab" "aac" "aba" "abb" "abc" "aca" "acb" "acc" "baa" "bab" "bac"
30 Eigene Funktionen: Ergänzung und Vertiefung 431

30.4 Methoden für generische Funktionen schreiben

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"

> print(datum) # Interner Aufruf: print.Date(datum)


[1] "2018-01-01" "2018-01-02"

> # Überschreibe die Funktion print.Date()


> print.Date <- function(x) {
+ res <- format(x, format = "%d.%m.%Y")
+ print.default(res)
+ }

> print(datum) # Verwendet jetzt unsere Funktion print.Date()


[1] "01.01.2018" "02.01.2018"

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:

> # Zugriff auf die print.Date-Funktion der Klasse base


> base::print.Date(datum)
[1] "2018-01-01" "2018-01-02"

Mit Hilfe von "::" können wir Inhalte eines Paketes ansprechen (vgl. (29.7)). Hier
rufen wir also print.Date() aus dem Paket base auf.

> # Lösche abschließend unsere Funktion print.Date()


> rm(print.Date)
> print(datum) # Jetzt wird wieder wie früher gedruckt.
[1] "2018-01-01" "2018-01-02"

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

30.5 Eigene Operatoren schreiben

Abschließend schreiben wir noch einen (nicht generischen) Operator.


Beispiel: Schreibe einen Operator %out%, der die Wahrheitswerte des %in%-Operators
umkehrt.

> "%out%" <- function(a, b) {


+ return(!(a %in% b))
+ }

> c(6, 7) %in% 1:6 > c(6, 7) %out% (1:6)


[1] TRUE FALSE [1] FALSE TRUE

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.

> "%out%"(c(6,7), 1:6) # Entspricht c(6,7) %out% 1:6


[1] FALSE TRUE

30.6 Aus der guten Praxis

30.6.1 Fallbeispiel: Klasse Bruch

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 <- function(x) {


+ # Prüft, ob alle Elemente des Vektors x annähernd ganzzahlig sind.
+
+ return(all((x %% 1)^2 < .Machine$double.eps))
+ }

> 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.

> z <- c(1.99, 2, 2.01)


> z
[1] 1.99 2.00 2.01

> # Umwandlung in integer > # Umwandlung in integer


> # (ohne Rundung) > # (mit Rundung)
> z.int <- as.integer(z) > z.int <- as.integer(round(z))
> z.int > z.int
[1] 1 2 2 [1] 2 2 2
> class(z.int) > class(z.int)
[1] "integer" [1] "integer"

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):

> Bruch <- function(zaehler, nenner) {


+ # Erzeugt aus zwei annaehernd ganzzahligen Skalaren zaehler und nenner
+ # ein Objekt der Klasse Bruch
+
+ # Pruefe auf numerische Skalare
+ if (!is.numeric(zaehler) || !is.numeric(nenner) ||
+ length(zaehler) > 1 || length(nenner) > 1) {
+ stop("Zaehler und Nenner muessen numerische Skalare sein!")
+ }
+
+ # Ergebnisobjekt erstellen
+ res <- c(zaehler, nenner)
+
+ # Pruefe auf gültige Werte und annaehernde Ganzzahligkeit
+ if (any(is.na(res)) || !is.ganzzahlig(res)) {
+ stop("Zaehler und Nenner muessen ganzzahlig sein!")
+ }
+
+ # In Integer (ganzzahlige Werte) umwandeln
+ res <- as.integer(round(res))
+
+ # Klasse "Bruch" hinzufügen und Bruch zurückgeben
+ class(res) <- c("Bruch", class(res))
+ return(res)
+ }
434 F Eigene Funktionen und Ablaufsteuerung

> b <- Bruch(2, 3) # Erzeuge den Bruch 2/3


> print(b)
[1] 2 3
attr(,"class")
[1] "Bruch" "integer"

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:

> print.Bruch <- function(bruch) {


+ cat(paste0(bruch[1], "/", bruch[2], "\n"))
+ }

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.

> "print.Bruch" %in% methods(print)


[1] TRUE

> # 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?

> b1 <- Bruch(2, 3) > b2 <- Bruch(1, 2)


> b1 > b2
2/3 1/2

> b1 * b2 # funktioniert schon > b1 / b2 # funktioniert nicht!


2/6 2/1.5

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.

> "/.Bruch" <- function(bruch1, bruch2) {


+ bruch1 * rev(bruch2)
+ }

> b1 / b2
4/3

Bingo! Wie sieht es mit der Addition aus?

> b1 + b2
3/5
30 Eigene Funktionen: Ergänzung und Vertiefung 435

Es sollte (2 · 2 + 1 · 3)/(2 · 3) = 7/6 herauskommen. Offenbar werden jedoch nur die


Zähler und Nenner jeweils miteinander addiert. Wir müssen die Brüche also auf den
gleichen Nenner bringen!

> "+.Bruch" <- function(bruch1, bruch2) {


+ nenner <- bruch1[2] * bruch2[2]
+ zaehler <- bruch1[1] * bruch2[2] + bruch2[1] * bruch1[2]
+ return(Bruch(zaehler, nenner))
+ }

> b1 + b2
7/6

Besser! Was passiert, wenn wir Brüche mit Nicht-Brüchen mischen?

> 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.

> "Bruch" %in% class(b) > inherits(b, "Bruch")


[1] TRUE [1] TRUE

Jetzt zur Funktion:

> "+.Bruch" <- function(bruch1, bruch2) {


+ if (!inherits(bruch1, "Bruch")) {
+ bruch1 <- Bruch(bruch1, nenner = 1) # Versuche Bruch zu erstellen
+ }
+ if (!inherits(bruch2, "Bruch")) {
+ bruch2 <- Bruch(bruch2, nenner = 1) # Versuche Bruch zu erstellen
+ }
+
+ nenner <- bruch1[2] * bruch2[2]
+ zaehler <- bruch1[1] * bruch2[2] + bruch2[1] * bruch1[2]
+ return(Bruch(zaehler, nenner))
+ }

> b1 + 2 # klappt einwandfrei > 2 + b1 # klappt einwandfrei


8/3 8/3
436 F Eigene Funktionen und Ablaufsteuerung

Die Standardfunktion zur Addition wird dabei nicht überschrieben:

> 5.5 + 2/1 # Funktioniert immer noch!


[1] 7.5

Folgender Code ruft wiederum "+".Bruch() auf.

> 5.5 + b1 # Fehlermeldung: 5.5 ist nicht ganzzahlig.


Fehler in Bruch(bruch1, nenner = 1) :
Zaehler und Nenner muessen ganzzahlig sein!

Mögliche Auswege: Versuche 5.5 in einen Bruch umzuwandeln oder wandle b1 in


eine Dezimalzahl um. An dieser Stelle lassen wir es mit der Addition beruhen...
Andere Operatoren schreiben sich nach dem gleichen Prinzip, zum Beispiel:

> "<.Bruch" <- function(bruch1, bruch2) {


+ return(bruch1[1] * bruch2[2] < bruch2[1] * bruch1[2])
+ }

Elegant: Wir können ">=.Bruch" auf "<.Bruch" zurückführen! b1 ≥ b2 ⇔ b2 < b1 .

> ">=.Bruch" <- function(bruch1, bruch2) {


+ return(bruch2 < bruch1)
+ }

Der folgende Test zeigt: Es schaut super aus!

> b1 > b2 > b1 < b2 > b1 >= b2


2/3 1/2 [1] FALSE [1] TRUE

30.6.2 Fallbeispiel: Auswertung von Polynomen

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:

> polynom <- function(beta) {


+
+ # Gibt eine Funktion zurueck, die das Polynom
+ # f(x) = beta[1] + beta[2] * x +
+ # beta[3] * x^2 + ... + beta[n] * x^(n-1)
+ # auswertet.
+
+ # Definiere die Funktion, die zurueckgegeben wird.
+ fun <- function(x) {
+
+ # Berechnet die Funktionswerte des zugrundeliegenden
+ # Polynoms fuer jedes x.
+
+ # Berechne fuer jedes x den Funktionswert laut Formel (30.2)
+ res <- sapply(x, function(u) {
+ y <- u ^ (0:(length(beta) - 1)) # u enthält ein Element von x
+ return(sum(beta * y)) # y enthält u^0, u^1, u^2 etc.
+ })
+
+ # Gebe die Funktionswerte zurueck
+ return(res)
+ }
+
+ # Gebe die Funktion zurueck
+ return(fun)
+ }

> beta <- c(0, 1)

> # Kann jetzt Polynom berechnen.


> polyfun <- polynom(beta = c(-1, 1, 2))

> # polyfun ist eine function > # polyfun hat Mode function
> is.function(polyfun) > mode(polyfun)
[1] TRUE [1] "function"

> polyfun(x = 0:2)


[1] -1 2 9

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

> fun <- function(x) {


+ res <- sapply(x, function(u) {
+ y <- u ^ (0:(length(beta) - 1))
+ return(sum(beta * y))
+ })
+
+ return(res)
+ }

> polynom.neu <- function(beta) {


+ return(fun)
+ }

> beta <- c(0, 1)


> polyfun.neu <- polynom.neu(beta = c(-1, 1, 2))

> polyfun.neu(x = 0:2) # beta <- c(0, 1) wird herangezogen


[1] 0 1 2
> polyfun(x = 0:2) # beta = c(-1, 1, 2) wird herangezogen
[1] -1 2 9

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:

> polyfun <- function(x, beta) {


+ res <- sapply(x, function(u) {
+ y <- u ^ (0:(length(beta) - 1))
+ return(sum(beta * y))
+ })
+
+ return(res)
+ }

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

> polyfun.effizient <- function(x, beta) {


+ y <- rep(x, each = length(beta)) ^ (0:(length(beta) - 1))
+ M <- matrix(beta * y, ncol = length(beta), byrow = TRUE)
+ return(rowSums(M))
+ }

> beta <- c(-1, 1, 2)

> polyfun(x = 0:2, beta) > polyfun.effizient(x = 0:2, beta)


[1] -1 2 9 [1] -1 2 9

Überzeuge dich davon, dass polyfun.effizient() dasselbe Ergebnis wie polyfun()


in deutlich kürzerer Laufzeit liefert, indem du die Kommentarzeichen entfernst!

> x <- sample(-10:10, size = 10000000, replace = TRUE)

> # res <- polyfun(x, beta) > # res <- polyfun.effizient(x, beta)

30.7 Abschluss

30.7.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Welche Unterschiede gibt es zwischen *apply() und Schleifen? In welchen


Situationen können/sollen wir *apply() nicht einsetzen? (30.1)
• Was ist eine anonyme Funktion? Wie verschachteln wir einen sapply()- und
einen tapply()-Aufruf? (30.2)
• Was ist eine rekursive Funktion und wie schreiben wir eine rekursive Funktion
in R? In welchen Fällen ist Rekursion elegant einsetzbar und in welchen Fällen
ist Rekursion ineffizient? (30.3)
• Wie (über)schreiben wir eine Methode einer generischen Funktion, wie bei-
spielsweise print()? (30.4)
• Wie schreiben wir eigene Operatoren? (30.5)

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

In (30.6.2) haben wir am Ende zwei Auswertungsstrategien aufgeschrieben, die zum


selben Ergebnis führen, aber große Laufzeitunterschiede aufweisen. In (33) gehen wir
ausführlich auf das Thema Effizienz ein.
440 F Eigene Funktionen und Ablaufsteuerung

30.7.3 Übungen

1. Betrachte folgenden Code:

# Lösche Objekte
rm(v, w, x, y, z)

w <- 1
x <- 2
y <- 3
z <- 4

fun <- function(x = 5, y) {


v <- 0
w <- -1
return(v + w + x + y + z)
}

a) Was kommt bei folgenden Funktionsaufrufen heraus? Erkläre dabei auch


die Abläufe.

i. fun() iii. fun(y = 7)


ii. fun(7) iv. fun(7, x = 6)

b) Was wird für die Objekte v, w, x, y, z nach Ausführung von fun(7, x = 6)


auf die Console gedruckt?
c) Schreibe fun() derart um, dass alle benötigten Objekte als Parameter
übergeben werden.

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)

> daten <- data.frame(geschlecht, groesse, gewicht)

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

31 Stringformatierung, Consoleneingabe und -ausgabe

Dort wollen wir in diesem Kapitel hin:

• Strings und Objekte auf die Console drucken

• 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:

> # Name und Alter der herausgepickten Kinder


> name <- c("Hugo", "Brigitte")
> alter <- c(9, 10)

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.

31.1 Consolenausgabe – cat(), print(), "\n", "\t"

Die Ausgabe von Texten oder Objekten auf die R-Console bewerkstelligen
wir mit den folgenden beiden Funktionen:

• cat(): druckt Texte auf die Console


• print(): druckt Objekte

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

In (23.1) haben wir bereits Escape-Befehle kennengelernt. Das sind Zeichenkombi-


nationen mit spezieller Bedeutung bei der Textausgabe. Zwei sehr nützliche davon
sind "\n" (Zeilenumbruch) und "\t" (Tabulator).

cat("Erste Zeile\nZweite Zeile\n\nDritte Zeile mit Abstand\n")


Erste Zeile
Zweite Zeile

Dritte Zeile mit Abstand

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.

> # Trenne Spalten mit Tabulator und Zeilen mit Zeilenumbruch


> text <- paste0(name, "\t", alter, collapse = "\n")
> text <- paste0(text, "\n")
> cat(text)
Hugo 9
Brigitte 10

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

31.2 Strings formatieren – format()

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!

> # Unformatiert > # Linksbündige Formatierung


> name > format(name)
[1] "Hugo" "Brigitte" [1] "Hugo " "Brigitte"
> nchar(name) > nchar(format(name))
[1] 4 8 [1] 8 8

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

Wollen wir die Namen stattdessen rechtsbündig oder zentriert anordnen, so


bedienen wir uns einfach des Parameters justify, den wir entweder auf, "right"
(rechtsbündig) oder "centre" (zentriert) einstellen können. Die Einstellung "left"
(linksbündig) ist Standard.

> # Rechtsbündig ausrichten > # Zentriert ausrichten


> format(name, justify = "right") > format(name, justify = "centre")
[1] " Hugo" "Brigitte" [1] " Hugo " "Brigitte"

Mit dem Parameter width können wir die Textbreite einstellen, also die Anzahl
der Zeichen des formatierten Stringvektors steuern.

> format(name, > format(name,


+ width = 6) + width = max(nchar(name)) + 2)
[1] "Hugo " "Brigitte" [1] "Hugo " "Brigitte "

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.

31.3 Zahlen formatieren – formatC()

Um Zahlen sauber zu formatieren, steht uns formatC() zur Verfügung. Auch


diese Funktion ist extrem vielseitig und wir betrachten in den folgenden Codebei-
spielen nur die Spitze des formatC()-Eisberges.
Beispiel: Formatiere die Altersangaben in alter rechtsbündig.

> alter
[1] 9 10

> # Formatiere alter rechtsbündig


> formatC(alter, width = max(nchar(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.

> # Formatiere alter mit führenden Nullen


> formatC(alter, width = max(nchar(alter)), flag = "0")
[1] "09" "10"

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

31.4 Tabellen erstellen

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

Bauen wir die Tabelle schrittweise zusammen!

> # 1.) Variablennamen und Werte zusammenketten.


> name1 <- c("Name", name)
> alter1 <- c("Alter", alter)

> # 2.) Zeilen der Tabelle erstellen


> text <- paste0(format(name1), " | ", format(alter1, justify = "right"))
> text
[1] "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.

> # 3.) Waagrechten Trennstrich erzeugen


> strich <- paste0(paste0(rep("-", max(nchar(text))), collapse = ""), "\n")
> strich
[1] "----------------\n"

> # 4.) Teile korrekt zusammenbauen


> titel <- paste0(text[1], "\n")
> rest <- paste0(paste0(text[-1], collapse = "\n"), "\n")
> tabelle <- paste0(titel, strich, rest)

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!

> # Ausgabe unserer Tabelle


> cat(tabelle)
Name | Alter
----------------
Hugo | 9
Brigitte | 10
446 G Datenimport und Datenexport

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.

> text <- paste0(paste0(name1, "\t", alter1, collapse = "\n"), "\n")


> cat(text)
Name Alter
Hugo 9
Brigitte 10

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.

31.5 Consoleneingabe – scan()

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.

namen <- scan(what = "") namen <- scan(what = "")


1: Hugo 1: Hugo
2: Hans Peter 2: "Hans Peter"
4: Anna 3: Anna
5: 4:
Read 4 items Read 3 items

> namen > namen


[1] "Hugo" "Hans" "Peter" "Anna" [1] "Hugo" "Hans Peter" "Anna"
31 Stringformatierung, Consoleneingabe und -ausgabe 447

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 "*").

> namen <- scan(what = "", sep = "*")


1: Hugo
2: Hans Peter
3: Anna
4:
Read 3 items
> namen
[1] "Hugo" "Hans Peter" "Anna"

Wollen wir (ausschließlich) Zahlen eingeben, so lassen wir what unspezifiziert.

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.

# n = 7 heißt, dass nach (spätestens) 7 Eingaben Schluss ist.


> lottozahlen <- scan(n = 7) # 6 + Zusatzzahl
1: 3
2: 19
3: 24
4: 23
5: 7
6: 34
7: 16
Read 7 items

> # Die eingelesenen Zahlen > # Es sind tatsächlich Zahlen


> lottozahlen > is.numeric(lottozahlen)
[1] 3 19 24 23 7 34 16 [1] TRUE
448 G Datenimport und Datenexport

31.6 Aus der guten Praxis

31.6.1 Fallbeispiel: Mathequiz

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.

> n <- 2; k <- 3


> M <- matrix(sample(1:10, size = n * k, replace = TRUE), ncol = k)
> M
[,1] [,2] [,3]
[1,] 9 7 2
[2,] 4 1 7

> # Rechnungen erstellen > # Korrekte Ergebnisse berechnen


> rechnung <- apply(M, 1, paste, > erg <- apply(M, 1, prod)
+ collapse = " * ")
> rechnung > erg
[1] "9 * 7 * 2" "4 * 1 * 7" [1] 126 28

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!

> as.numeric("Keine Zahl")


Warnung in eval(ei, envir) NAs durch Umwandlung erzeugt
[1] NA
> is.na(as.numeric("Keine Zahl"))
Warnung in eval(ei, envir) NAs durch Umwandlung erzeugt
[1] TRUE

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

mathequiz <- function(n, k = 2) {


# Stellt n Multiplikationsaufgaben mit je k Faktoren.
# Gibt eine Liste mit der Anzahl der gestellten Aufgaben und der
# Anzahl der richtig gelösten Aufgaben zurück.
# n ... Anzahl der Aufgaben
# k ... Schwierigkeitsgrad der Aufgaben = Anzahl der Faktoren
# Defaultwert: 2

# 1.) Erstelle Matrix mit den Faktoren


M <- matrix(sample(1:10, size = n * k, replace = TRUE), ncol = k)

# 2.) Erstelle die Rechnungen und die Ergebnisse


rechnung <- apply(M, 1, paste, collapse = " * ")
erg <- apply(M, 1, prod)

# 3.) Zähler für die Anzahl der richtigen Antworten


anz.richtig <- 0

for (i in 1:n) {
# 4.) Frage stellen
cat("Wie lautet das Ergebnis der folgenden Rechnung:\n")
cat(paste0(" ", rechnung[i], "\n"))

# 5.) Auf Eingabe des Benutzers warten


tipp <- scan(n = 1) # Bei numerischen Werten bleibt what frei

# 6.) Vergleiche tipp mit der richtigen Loesung


if (tipp == erg[i]) {
# korrekt! Gebe Glückwunschtext aus.
cat("Super, weiter so!\n")
anz.richtig <- anz.richtig + 1
}
else {
# Falsch! Gebe "Motivationstext" aus.
cat("Falsch! Streng dich an, du Penner!\n")
}
}

# 7.) Ergebnis ausdrucken und als Liste zurückgeben


cat(paste0("Ergebnis: ", anz.richtig, " geloeste Aufgaben.\n"))
return(list(Aufgaben = n, Richtig = anz.richtig))
}

Abbildung 31.1: R-Code für das Mathequiz


450 G Datenimport und Datenexport

Idee: Wir schreiben eine while-Schleife, in der wir den Benutzer solange eine Zahl
eingeben lassen, bis er erstmals eine korrekte Zahl eintippt.

> # Annahme: Es werden folgende "Zahlen" eingegeben:


> eingabe <- c("Null", "Eins", "2", "3")

> while (TRUE) {


+ # Entnehme nächste Eingabe
+ zahl <- as.numeric(eingabe[1])
+ eingabe <- eingabe[-1]
+
+ if (!is.na(zahl)) {
+ # Gültige Zahl: Abbruch
+ break
+ }
+
+ # Keine gültige Zahl: Hinweistext ausgeben
+ cat("Bitte eine gueltige Zahl eingeben!\n")
+ }
Warnung in eval(ei, envir) NAs durch Umwandlung erzeugt
Bitte eine gueltige Zahl eingeben!
Warnung in eval(ei, envir) NAs durch Umwandlung erzeugt
Bitte eine gueltige Zahl eingeben!
> zahl # Enthält die erste gültige Zahl von eingabe
[1] 2

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:

> tipp <- character(0) # leeren Vektor erzeugen


> length(tipp) > 0 & !is.na(as.numeric(tipp))
logical(0)
> if (length(tipp) > 0 & !is.na(as.numeric(tipp))) print("Hallo")
Fehler in if (length(tipp) > 0 & !is.na(as.numeric(tipp))) print("Hallo") :
Argument hat Länge 0

Bemerkung: In der zweiten Version wurden einige Kommentare entfernt, damit


sich der Code auf einer Seite ausgeht. Dieses Vorgehen soll keine Schule machen ;-)
31 Stringformatierung, Consoleneingabe und -ausgabe 451

mathequiz <- function(n, k = 2) {


# Stellt n Multiplikationsaufgaben mit je k Faktoren.
# Gibt eine Liste mit der Anzahl der gestellten Aufgaben und der
# Anzahl der richtig gelösten Aufgaben zurück.

M <- matrix(sample(1:10, size = n * k, replace = TRUE), ncol = k)


rechnung <- apply(M, 1, paste, collapse = " * ")
erg <- apply(M, 1, prod)

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

if (length(tipp) > 0 && !is.na(as.numeric(tipp))) {


# Jetzt wurde eine zulässige Zahl eingegeben
break
}

# Es wurde keine zulässige Zahl eingegeben.


cat("Bitte eine gueltige Zahl eingeben!\n")
}
)

# Umwandeln, da String übergeben wurde


tipp <- as.numeric(tipp)

# Vergleiche tipp mit der richtigen Loesung


if (tipp == erg[i]) {
cat("Super, weiter so!\n")
anz.richtig <- anz.richtig + 1
}
else {
cat("Falsch! Streng dich an, du Penner!\n")
}
}

cat(paste0("Ergebnis: ", anz.richtig, " geloeste Aufgaben.\n"))


return(list(Aufgaben = n, Richtig = anz.richtig))
}

Abbildung 31.2: R-Code für das Mathequiz mit Absicherung


452 G Datenimport und Datenexport

31.7 Abschluss

31.7.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Wir ziehen 10 gleichverteilte Zufallszahlen aus [−20, 20] mittels

> x <- runif(10, -20, 20)

Schreibe einen R-Code, der die 10 Zahlen in folgender Bauart ausgibt:

Zufallszahl Nr. 1: 0.05


Zufallszahl Nr. 2: -4.22
Zufallszahl Nr. 3: 16.20
...
Zufallszahl Nr. 9: -5.97
Zufallszahl Nr. 10: -12.49

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:

> df <- data.frame(Name = name, Alter = alter)


> res <- createLatexTable(df, justify = c("l", "r"))

> cat(res) # 1. Spalte linksbündig, 2. Spalte zentriert


\begin{tabular}{lr}
Name & Alter \\
Hugo & 9 \\
Brigitte & 10 \\
\end{tabular}

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

32 Datenimport und Datenexport

Dort wollen wir in diesem Kapitel hin:

• Einblicke in gängige Datenformate sammeln

• Daten einlesen und speichern


• Probleme beim Einlesen erkennen und lösen
• unformatierte Dateien einlesen und speichern

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.

• Vertreter2.csv: Dieselben Daten wie in Vertreter2.txt, nur in einem an-


deren Dateiformat.

Wir klären in diesem Kapitel unter anderem folgende Fragen:

1. Wie lesen wir die Datei Vertreter1.txt sauber ein? (32.1.1)


2. Wie speichern wir den Vertreterdatensatz als Datei ab? (32.1.2), (32.1.3)
3. Wie gehen wir beim Einlesen mit Sonderzeichen wie dem ü in Süd um? Was
können wir tun, um diese Zeichen korrekt einzulesen? (32.2)

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

32.1 Einfache Dateiformate

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.

32.1.1 Textdateien einlesen – read.table()

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).

read.table(file, header = FALSE, sep = "", dec = ".", na.strings = "NA",


nrows = -1, skip = 0, strip.white = FALSE,
stringsAsFactors = default.stringsAsFactors(), fileEncoding = "")

Infobox 11: stringsAsFactors vor Version 4.0.1

Vor Version 4.0.1 wurden Strings innerhalb von read.table() standardmäßig


in Faktoren umgewandelt (vgl. Infobox 8 auf Seite 240). Wurde ein R-Code
vor Version 4.0.1 geschrieben und der Parameter stringsAsFactors nicht
spezifiziert, so ist der Code nicht mehr mit der aktuellen R-Version kompatibel!
Sollen Faktoren erzeugt werden, so muss man explizit stringsAsFactors =
TRUE setzen!

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

Tabelle 32.1: Ausgewählte Parameter der Funktion read.table()

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)?

Abbildung 32.1: Auszug aus der Datei Vertreter1.txt


32 Datenimport und Datenexport 457

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.

> file <- "D:/RBuch/Daten/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

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

32.1.2 Textdateien schreiben – write.table()

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.

> # Gewinnstufe berechnen und an daten anhängen


> gewinn.kat <- cut(daten$Gewinn, breaks = c(-Inf, 5, 10, Inf),
+ labels = c("wenig", "mittel", "viel"), ordered_result = TRUE)
> daten$Gewinnstufe <- gewinn.kat

> # Speichern als txt-Datei mit Tabulator als Trennzeichen


> write.table(daten, file = "Vertreter_Gewinnstufe.txt", quote = FALSE,
+ sep = "\t", dec = ",", row.names = FALSE, fileEncoding = "UTF-8")

> # Speichern als csv-Datei mit ; als Trennzeichen


> write.table(daten, file = "Vertreter_Gewinnstufe.csv", quote = FALSE,
+ sep = ";", dec = ",", row.names = FALSE, fileEncoding = "UTF-8")

Mit quote = FALSE verhindern wir, dass Anführungszeichen mitgespeichert wer-


den. Die Standardeinstellung ist TRUE. Weiters setzen wir row.names = FALSE, da
wir nicht daran interessiert sind die Zeilenbeschriftungen mit auszugeben. Stan-
dardmäßig werden wegen col.names = TRUE die Spaltennamen in die erste Zeile
der Datei geschrieben, was in diesem Fall Sinn macht.

32.1.3 csv-Dateien einlesen & schreiben – read.csv(), write.csv()

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!

> # Ggf. Arbeitsverzeichnis wechseln


> # setwd("...")
> daten.roh <- read.table(file = "Vertreter1.txt", header = TRUE, dec = ",",
+ stringsAsFactors = TRUE)
> daten.utf8 <- read.table(file = "Vertreter1.txt", header = TRUE, dec = ",",
+ fileEncoding = "UTF-8", stringsAsFactors = TRUE)

> # Ohne UTF-8-Codierung > # Mit UTF-8 Codierung


> head(daten.roh, n = 7) > head(daten.utf8, n = 7)
Nummer Gebiet Ausbildung Gewinn Nummer Gebiet Ausbildung Gewinn
1 1 Zentrum Matura 11.4 1 1 Zentrum Matura 11.4
2 2 Zentrum Matura 9.2 2 2 Zentrum Matura 9.2
3 3 West Matura 9.3 3 3 West Matura 9.3
4 4 Ost Lehre 13.6 4 4 Ost Lehre 13.6
5 5 Zentrum Matura 8.9 5 5 Zentrum Matura 8.9
6 6 Zentrum Lehre 3.3 6 6 Zentrum Lehre 3.3
7 7 SÃd Lehre 2.1 7 7 Süd Lehre 2.1

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

In Infobox 12 erfahren wir wesentliche Hintergründe über Zeichencodierungsstan-


dards. Dieses Wissen hilft uns dabei, den obigen Output zu verstehen.

Infobox 12: 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.

Unter anderem existieren folgende gängige Zeichencodierungsstandards:


• ASCII (American Standard Code for Information Interchange): Ein
Zeichen ist genau 1 Byte (= 8 Bit) lang, davon werden 7 Bit gesetzt; das
höchste Bit bleibt frei. Das Zeichen Nummer 65 (binär (0)1000001) steht
etwa für „A“.
460 G Datenimport und Datenexport

Folgende druckbare Zeichen sind mit ASCII abbildbar:

!"#\$\%\&’()*+,-./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.

• UTF-8 (UCS Transformation Format, wobei UCS für Universal Coded


Character Set steht): Druckbare Zeichen, die mit ASCII abbildbar sind,
haben in UTF-8 dieselbe Codierung. Um Sonderzeichen wie Umlaute
verfügbar zu machen, geht UTF-8 einen anderen Weg als Latin-1.
Das höchste Bit wird verwendet, um anzudeuten, ob es sich um ein
ASCII-Zeichen handelt (0) oder nicht (1). Für Sonderzeichen, die nicht
durch ASCII abgedeckt sind, wird eine (flexible) zusätzliche Anzahl an
Bytes herangezogen. Durch ein ausgeklügeltes System weiß der Interpre-
ter genau, wie viele Bytes zu einem Zeichen zusammengefasst werden
müssen. Umlaute benötigen bei UTF-8 beispielsweise 2 Byte.

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).

> # Extrahiere die falsch geschriebene Kategorie Süd


> temp <- grep("S.+d", levels(daten.roh$Gebiet), value = TRUE)
> temp
[1] "SÃd"

> # Die falsch codierte Version hat 4 Zeichen.


> nchar(temp)
[1] 4
32 Datenimport und Datenexport 461

32.2.2 Zeichencodierung umstellen – Encoding(), options(encoding)

Mit Encoding() können wir die Zeichencodierung eines Stringvektors um-


stellen. Zum Beispiel auf "UTF-8".

> temp
[1] "SÃd"

> # Encoding auf UTF-8 umstellen


> Encoding(temp) <- "UTF-8"
> temp
[1] "Süd"

> # Die richtig codierte Version hat 3 Zeichen.


> nchar(temp)
[1] 3

Mit dem Parameter encoding der Funktion options(), die wir aus (6.4) kennen,
können wir den verwendeten Zeichencodierungsstandard umstellen.

> # Standardmäßigen Zeichencodierungsstandard umstellen


> opt <- options(encoding = "UTF-8")

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.

> # Ggf. Arbeitsverzeichnis wechseln


> # setwd("...")
> daten.utf8 <- read.table(file = "Vertreter1.txt", header = TRUE, dec = ",",
+ stringsAsFactors = TRUE)
> head(daten.utf8, 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

> # Optionen wieder zurücksetzen


> options(opt)

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

32.2.3 Reparaturen per Hand

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.

> # Extrahiere die falsch geschriebene Kategorie Süd


> temp <- grep("S.+?d", levels(daten.roh$Gebiet), value = TRUE)
> temp
[1] "SÃd"

> # Extrahiere falsch interpretierte Zeichenkombination per Hand


> zeichen <- substring(temp, 2, 3)
> zeichen
[1] "Ã"

> # Korrigiere alle Vorkommen


> gsub(zeichen, "ü", temp)
[1] "Süd"

32.3 Datenaufbereitung

Wir werden unser Vertreterbeispiel etwas ausweiten. Die Dateien Vertreter.txt


und Vertreter1.txt kennen wir bereits. Vertreter2.txt und Vertreter2.csv
enthalten jeweils dieselben Daten, erweitert um eine Variable Datum, welche das Ein-
stellungsdatum des Vertreters angibt. Außerdem halten sie ein paar (leider ungute)
Überraschungen für uns bereit.
In der Praxis benötigt man manchmal (sehr) viel Zeit, bis die Daten endlich in R
korrekt verfügbar sind und Datenfehler bzw. Inkonsistenzen ausgebügelt sind. Wir
betrachten folgend die Spitze des Eisberges.

32.3.1 Herausforderungen beim Einlesen meistern

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

Abbildung 32.2: Datei Vertreter2.txt


464 G Datenimport und Datenexport

Abbildung 32.3: Datei Vertreter2.csv


32 Datenimport und Datenexport 465

Wir listen alle problematischen Dinge mitsamt adäquater Lösungsmöglichkeiten auf.

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:

> daten <- read.table("Vertreter2.csv", skip = 9, sep = ";",


+ dec = ",", header = TRUE, nrows = 24, na.strings = c("NA", "na", ""),
+ strip.white = TRUE, fileEncoding = "UTF-8", stringsAsFactors = TRUE)

Jetzt das txt-File:

> daten <- read.table("Vertreter2.txt", skip = 9, sep = "\t",


+ dec = ",", header = TRUE, nrows = 24, na.strings = c("NA", "na", ""),
+ strip.white = TRUE, fileEncoding = "UTF-8", stringsAsFactors = TRUE)

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

Probiere es mal ohne sep = "\t" aus ;-)

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.

> temp <- read.table("Vertreter2.txt", skip = 9, sep = "\t",


+ dec = ",", header = TRUE, nrows = 24, na.strings = c("NA", "na", ""),
+ fileEncoding = "UTF-8", stringsAsFactors = TRUE)

> 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"

Da sich bei den Ausprägungen von Ausbildung Leerzeichen eingeschlichen haben


und wir diese nicht mehr mit strip.white = TRUE entfernen, werden " Matura"
und "Matura" als zwei separate Kategorien aufgefasst. Selbiges gilt für "Lehre "
und "Lehre". Wir müssten jetzt diese Kategorien zusammenfassen, was wir dir als
kleine Übung überlassen.
Nach diesem Ausflug in die Welt von strip.white schauen wir uns den Zwischen-
stand von daten an. Beide Versionen führen dabei zum selben Ergebnis.

> 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

a) Kategorien in Gebiet und Ausbildung umreihen

> daten$Gebiet <- factor(daten$Gebiet,


+ levels = c("West", "Nord", "Ost", "Süd", "Zentrum"))
> daten$Ausbildung <- factor(daten$Ausbildung,
+ levels = c("Matura", "Lehre"))

b) Datum die Klasse Date zuweisen:

> daten$Datum <- as.Date(daten$Datum, format = "%d.%m.%Y")

c) Gewinn in numerischen Vektor umwandeln

> # Gewinn in String umwandeln


> daten$Gewinn <- as.character(daten$Gewinn)

> # Ersetze alle "," durch "."


> daten$Gewinn <- sub(pattern = ",", replacement = ".", daten$Gewinn)

> # Wandle Gewinn in numerischen Vektor um


> daten$Gewinn <- as.numeric(daten$Gewinn)

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

> levels(daten$Gebiet) # Kategorien nach Wunsch gereiht - passt :-)


[1] "West" "Nord" "Ost" "Süd" "Zentrum"
> levels(daten$Ausbildung) # Kategorien nach Wunsch gereiht - passt :-)
[1] "Matura" "Lehre"

> 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

> class(daten$Datum) # Date - passt :-)


[1] "Date"
32 Datenimport und Datenexport 469

32.3.2 Strings nicht als Faktoren einlesen – stringsAsFactors

Wir setzen das Beispiel aus (32.3) fort.


Mit der Einstellung stringsAsFactors = FALSE (Standard seit Version 4.0.1) in
read.table() werden aus Zeichenketten keine Faktoren erzeugt. Das kann in
manchen Fällen sehr nützlich sein, wie in unserem Beispiel bei Gewinn, der aufgrund
der Vermischung von Dezimalkomma und -punkt als Faktor eingelesen wurde.

> daten <- read.table("Vertreter2.txt", skip = 9, sep = "\t",


+ dec = ",", header = TRUE, nrows = 24,
+ na.string = c("NA", "na", ""), strip.white = TRUE,
+ fileEncoding = "UTF-8", stringsAsFactors = FALSE)

> # Variablen aufbereiten


> daten$Gebiet <- factor(daten$Gebiet, levels =
+ c("West", "Nord", "Ost", "Süd", "Zentrum"))
> daten$Datum <- as.Date(daten$Datum, format = "%d.%m.%Y")
> daten$Ausbildung <- factor(daten$Ausbildung, levels = c("Matura", "Lehre"))

> # daten$Gewinn <- as.character(daten$Gewinn) # entfällt


> daten$Gewinn <- sub(",", ".", daten$Gewinn)
> daten$Gewinn <- as.numeric(daten$Gewinn)

32.4 Textdateien mit beliebigem Format einlesen – scan()

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()).

scan(file = "", what = double(), nmax = -1, n = -1, sep = "",


quote = if(identical(sep, "\n")) "" else "’¨
", dec = ".",
skip = 0, nlines = 0, na.strings = "NA",
flush = FALSE, fill = FALSE, strip.white = FALSE,
quiet = FALSE, blank.lines.skip = TRUE, multi.line = TRUE,
comment.char = "", allowEscapes = FALSE,
fileEncoding = "", encoding = "unknown", text, skipNul = FALSE)

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.

Folgende Ziele verfolgen wir gleich:

1. Lese die Datei Vertreter.txt ein.


2. Suche nach der Datei, welche die Labelinformationen speichert. Der Dateiname
enthält das Wort Labels. Wie heißt sie genau?
3. Lese diese Datei sodann ein: Ziel ist es, zwei Vektoren zu generieren, welche
die Labels der beiden Faktoren enthalten.
4. Wandle Gebiet und Ausbildung in Faktoren um, wobei die richtigen Labels
verwendet werden sollen.

Gehen wir es an!

> # Verzeichnis wechseln


> # setwd("...")

> # 1. Vertreter.txt einlesen


> daten <- read.table("Vertreter.txt", sep = "\t", dec = ",", header = TRUE)
> names(daten)[1] <- gsub("^.+[.]+", "", names(daten)[1])
> head(daten, n = 5)
Nummer Gebiet Ausbildung Gewinn
1 1 2 1 11.4
2 2 3 1 9.2
3 3 1 1 9.3
4 4 3 2 13.6
5 5 4 1 8.9

> # 2. Auflistung aller Dateien, die sich im Verzeichnis befinden


> files <- list.files()
> files
[1] "Vertreter.RData" "Vertreter.txt" "Vertreter_Labels.txt"
[4] "Vertreter1.txt" "Vertreter2.csv" "Vertreter2.txt"

> # Suche nach dem Wort Labels


> bool <- grepl("Labels", files, ignore.case = TRUE)
> file <- files[bool]
> file
[1] "Vertreter_Labels.txt"

Die Datei heißt also Vertreter_Labels.txt. Werfen wir in Abb. 32.4 einen Blick
auf diese Datei.
32 Datenimport und Datenexport 471

Abbildung 32.4: Datei Vertreter_Labels.txt

> # 3. Einlesen mit scan()


> # what = "" ist notwendig
> rohdaten <- scan(file, what = "", encoding = "UTF-8")
Read 29 items
> rohdaten
[1] "Labels" "für" "die" "Vertreterdaten"
[5] "Variable" "Ausbildung" "1" "="
[9] "Matura" "2" "=" "Lehre"
[13] "Variable" "Gebiet" "1" "="
[17] "West" "2" "=" "Nord"
[21] "3" "=" "Ost" "4"
[25] "=" "Süd" "Ende" "der"
[29] "Labeldefinition"

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.

Machen wir uns ans Werk!

> # Index der Variablennamen


> indvar <- grep("Variable", rohdaten) + 1
> indvar
[1] 6 14

> # Variablennamen extrahieren


> variablennamen <- rohdaten[indvar]
> variablennamen
[1] "Ausbildung" "Gebiet"
472 G Datenimport und Datenexport

> # Einträge rechts von "=" sind die gewünschten Labels


> indlabel = grep("=", rohdaten) + 1
> indlabel
[1] 9 12 17 20 23 26

> label <- rohdaten[indlabel]


> label
[1] "Matura" "Lehre" "West" "Nord" "Ost" "Süd"

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!

> # Werte zu den richtigen Variablen zuordnen


> nr <- as.numeric(cut(indlabel, breaks = c(indvar, Inf)))
> nr
[1] 1 1 2 2 2 2

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.

> for (i in 1:length(variablennamen)) {


+ daten[[variablennamen[i]]] <- factor(daten[[variablennamen[i]]],
+ labels = label[nr == i])
+ }

> 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

32.5 Textdateien mit beliebigem Format schreiben – cat()

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

Beispiel: Folgende kleine Datenbank ist gegeben:

> personen <- data.frame(Name = c("Tom", "Anna"), Alter = c(28, NA),


+ Wohnort = c("Mödling", "Wien"), stringsAsFactors = FALSE)
> personen
Name Alter Wohnort
1 Tom 28 Mödling
2 Anna NA Wien

Speichere die Daten in personen in folgendem xml-Format ab:

<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.

> # xml-Tags an jede Variable anhängen


> namen <- paste0(" <name>", personen$Name, "</name>\n")
> alter <- paste0(" <alter>", personen$Alter, "</alter>\n")
> ort <- paste0(" <wohnort>", personen$Wohnort, "</wohnort>\n")

> # <person> und </person> an jedes Datum anhängen


> string <- paste0("<person>\n", namen, alter, ort, "</person>\n",
+ collapse = "")

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)).

> opt <- options(encoding = "UTF-8")

> # Datei abspeichern


> # Ggf. Verzeichnis wechseln
> # setwd(...)
> cat(string, file = "Personen.xml")

> # Optionen zurücksetzen


> options(opt)

Nun sollte sich im aktuellen Arbeitsverzeichnis die Datei Personen.xml befinden.


474 G Datenimport und Datenexport

32.6 Einlesen aus anderen Quellen

32.6.1 Excel-Dateien einlesen – read.xlsx()

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).

read.xlsx(file, sheetIndex, sheetName=NULL, rowIndex=NULL,


startRow=NULL, endRow=NULL, colIndex=NULL,
as.data.frame=TRUE, header=TRUE, colClasses=NA,
keepFormulas=FALSE, encoding="unknown", password=NULL, ...)

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).

> # Paket xlsx einmalig installieren


> # install.packages("xlsx")

> # Paket xlsx laden


> library(xlsx)

> daten.xls <- read.xlsx(file = "Vertreter2.xls", sheetName = "Vertreter2",


+ encoding = "UTF-8", startRow = 7, endRow = 31)
> head(daten.xls, n = 8)
Nummer Gebiet Ausbildung Gewinn Datum
1 1 5 Matura 11,4 01.04.2018
2 2 5 Matura 9,2 01.06.2018
3 3 1 Matura 9,3 01.05.2018
4 4 3 Lehre 13,6 01.02.2018
5 5 5 Matura NA 01.11.2018
6 6 5 Lehre 3.3 01.10.2018
7 7 4 Lehre 2,1 01.10.2018
8 8 3 Lehre NA 01.12.2018

Nun können wir die Daten, wie in (32.3) gezeigt, aufbereiten. Zwei mögliche Gründe,
warum es zu Fehlern kommen kann:

• Java ist nicht installiert (siehe Link oben).


• Auf 64-Bit-Rechnern kann es zu Kompatibilitätsproblemen kommen. Teste den
Code in der 32-Bit-Version und der 64-Bit-Version von R.

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

32.6.2 SPSS- und SAS-Dateien einlesen – read.spss(), read.sas()

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.

read.spss(file, use.value.labels = TRUE, to.data.frame = FALSE,


max.value.labels = Inf, trim.factor.names = FALSE,
trim_values = TRUE, reencode = NA, use.missings = to.data.frame,
sub = ".", add.undeclared.levels = c("sort", "append", "no"),
duplicated.value.labels = c("append", "condense"),
duplicated.value.labels.infix = "_duplicated_", ...)

Beispiel: Lese die Datei Vertreter1.sav ein. Es handelt sich um dieselben Daten
wie bei Vertreter1.txt aus (32.1.1).

> # Paket einmalig installieren


> # install.packages("foreign")

> # Paket foreign laden


> library(foreign)

> # Ggf. Arbeitsverzeichnis wechseln


> # setwd(...)

> daten <- read.spss("Vertreter1.sav", to.data.frame = TRUE)


re-encoding from UTF-8
> 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

> 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

32.7.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Wir betrachten die im Beispiel ab Seite 473 erstellte Datei personen.xml.

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.

2. Wir betrachten erneut die Datei Vertreter2.txt aus (32.3).

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:

• id: Die Identifikationsnummer des Fragebogens


• obst: Das Lieblingsobst (offene Antwort)
• genfood: Wie gefährlich findest du gentechnisch veränderte Lebensmittel?
• ttip: Für oder gegen TTIP (ein Freihandelsabkommen)?
• geschlecht, alter, größe, gewicht, bmi: Geschlecht, Alter, Größe in
cm, Gewicht in kg und der BMI

Lies die Daten in R als Dataframe ein und bearbeite folgende Aufgaben:

a) Überprüfe, ob die Kategorien der Variable genfood sinnvoll gereiht sind.


b) Bei ttip haben einige keine Antwort abgegeben (NA). Erstelle aus den
fehlenden Werten eine neue Kategorie Keine Meinung.
c) Bereite die Variable obst auf. Fasse dazu die Antworten sinnvoll zusam-
men. Unter anderem haben manche das Obst in Einzahl und andere in
Mehrzahl aufgeschrieben. Außerdem: Wie gehen wir mit den unterschied-
lichen Beeren- und Melonensorten um? Fasse selten genannte Obstsorten
am Ende zu einer Kategorie Sonstige zusammen.
d) Speichere die modifizierte Datei als csv-Datei ab.
H Effizienz und Simulation
Daniel Obszelka
480 H Effizienz und Simulation

33 Effizienz

Dort wollen wir in diesem Kapitel hin:

• Laufzeiten messen.

• Tipps & Tricks zur effizienten Programmierung kennenlernen.


• Einblicke in die Monte-Carlo-Simulation bekommen.

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.

33.1 Laufzeitmessung: Spaltenmittelwerte einer Matrix

33.1.1 Messung mittels Stoppuhr – Sys.time()

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:

1. mit Hilfe von apply()


2. mit einer for-Schleife
3. mit colMeans()

Vergleiche die Laufzeiten der drei Varianten.


© 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_33
33 Effizienz 481

> n <- 10 # Anzahl der Zeilen


> k <- 10^7 # Anzahl der Spalten

> # Matrix generieren


> matrix <- matrix(sample(1:100, size = n * k, replace = TRUE), ncol = k)

Variante 1: mit apply()

> {
+ zeit1 <- Sys.time()
+ apply(matrix, 2, mean)
+ zeit2 <- Sys.time()
+ }

> zeit2 - zeit1 # Zeitdifferenz bestimmen


Time difference of 1.322403 mins

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()
+ }

> zeit2 - zeit1 # Zeitdifferenz bestimmen


Time difference of 1.025146 mins

Variante 2b: for-Schleife (mit besserer Initialisierung)

> {
+ zeit1 <- Sys.time()
+ z <- numeric(k) # besser als NULL!
+ for (j in 1:ncol(matrix)) {
+ z[j] <- mean(matrix[, j])
+ }
+ zeit2 <- Sys.time()
+ }

> zeit2 - zeit1 # Zeitdifferenz bestimmen


Time difference of 42.31873 secs

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.

Regel 17: Initialisiere deine Objekte sinnvoll!

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!

Variante 3: mit colMeans()

> {
+ zeit1 <- Sys.time()
+ colMeans(matrix)
+ zeit2 <- Sys.time()
+ }

> zeit2 - zeit1 # Zeitdifferenz bestimmen


Time difference of 0.098737 secs

Wir sehen, dass colMeans() mit Abstand am schnellsten ist!

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.

33.1.2 Messung der CPU-Zeit: system.time()

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

33.2 Beispiel: Dreiecksdichte auswerten

Das folgende Beispiel wirkt zwar harmlos, trotzdem hat es schon viel zu bieten! Sei
X eine Zufallsvariable mit folgender Dichtefunktion:

f (x) = max(0, 1 − |x|) ∀x ∈ R

Es handelt sich hierbei um eine spezielle symmetrische Dreiecksverteilung. Die Dichte


stellen wir in Abb. 33.1 dar.

Dichte einer symmetrischen Dreiecksverteilung


1.2

f(x) = max(0, 1−|x|)


1.0
0.8
f(x)

0.6
0.4
0.2
0.0

−1.5 −1.0 −0.5 0.0 0.5 1.0 1.5

Abbildung 33.1: Dichtefunktion einer symmetrischen Dreiecksverteilung

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.

> x <- seq(-1.5, 1.5, by = 0.5)


> x
[1] -1.5 -1.0 -0.5 0.0 0.5 1.0 1.5

Variante 1: max() (funktioniert nicht)

> ddreieck1 <- function(x) {


+ return(max(0, 1 - abs(x)))
+ }

> ddreieck1(x)
[1] 1

Bemerkung: Das erste d im Funktionsnamen ddreieck steht für Dichte (density).

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 <- function(x) {


+ res <- sapply(x, function(z) {
+ max(0, 1 - abs(z))
+ } )
+
+ return(res)
+ }

> 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 <- function(x) {


+ res <- numeric(length(x))
+
+ for (i in 1:length(x))
+ res[i] <- max(0, 1 - abs(x[i]))
+
+ return(res)
+ }

> 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

Diese Formel können wir einfach mit ifelse() umsetzen.

> ddreieck4 <- function(x) {


+ z <- 1 - abs(x)
+ return(ifelse(z >= 0, z, 0))
+ }
33 Effizienz 485

> ddreieck4(seq(-1, 1, length = 5))


[1] 0.0 0.5 1.0 0.5 0.0

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.

> ddreieck5 <- function(x) {


+ pmax(0, 1 - abs(x)) # 0 wird ggf. recycelt
+ }

> ddreieck5(seq(-1, 1, length = 5))


[1] 0.0 0.5 1.0 0.5 0.0

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:

> # Liste der Funktionen und Vektor der Vektorlängen.


> funktionen <- list(ddreieck2, ddreieck3, ddreieck4, ddreieck5)
> laengen <- c(10000, 100000, 1000000, 10000000)

> # Wissenschaftliche Notation unterdrücken.


> optalt <- options(scipen = 10000000)

> # Gehe alle Vektorlängen durch


> erg <- sapply(laengen, function(k) {
+
+ # Erstelle Testvektor
+ werte <- seq(-2, 2, length = k + 1)
+
+ # Gehe alle Funktionen durch
+ res <- sapply(funktionen, function(fun) {
+ return(system.time(fun(werte))[3])
+ })
+
+ return(res)
+ })

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 <- t(erg) # Hier muss transponiert werden


> colnames(erg) <- paste("ddreieck", 2:5, "()", sep = "")
> rownames(erg) <- paste("k =", laengen)

> 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

Beachte, dass für eine fundierte Laufzeitanalyse mehrere Wiederholungen not-


wendig sind. Wir werden an dieser Stelle eine Möglichkeit anführen und dabei etwas
über Arrays lernen. Eine Datenstruktur, die wir in (25.2.2) angeschnitten haben.

> # Wissenschaftliche Notation unterdrücken.


> optalt <- options(scipen = 1000000000)

> N <- 20 # Anzahl der Durchgänge (je mehr desto genauer)

> # Array mit 3 Dimensionen erstellen


> Zeit <- array(dim = c(length(laengen), length(funktionen), N))
> colnames(Zeit) <- paste("ddreieck", 2:5, "()", sep = "")
> rownames(Zeit) <- paste("k =", laengen)

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.

> for (i in 1:N) {


+ # Erzeuge den i. von N Versuchen
+ for (k in 1:length(laengen)) {
+ # Gehe alle Problemgrößen durch
+ werte <- seq(-2, 2, length = laengen[k] + 1)
+ for (j in 1:length(funktionen)) {
+ # Gehe alle Funktionen durch
+ Zeit[k, j, i] <- system.time(funktionen[[j]](werte))[3]
+ }
+ }
+ }

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.

> # Aggregiere über die ersten beiden Dimensionen


> mittelwerte <- apply(Zeit, MARGIN = 1:2, mean, simplify = FALSE)
33 Effizienz 487

> # Darstellung der mittleren Laufzeiten


> round(mittelwerte, 2)
ddreieck2() ddreieck3() ddreieck4() ddreieck5()
k = 10000 0.01 0.01 0.00 0.00
k = 100000 0.12 0.04 0.00 0.00
k = 1000000 1.42 0.46 0.03 0.01
k = 10000000 27.93 4.59 0.30 0.08

> # Optionen zurücksetzen


> options(optalt)

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.

Regel 18: Verwende vektorwertige und spezialisierte Funktionen!

Vermeide Schleifen, wenn es geht und verwende stattdessen (spezialisierte) vek-


torwertige Funktionen. Im Allgemeinen gilt folgender Grundsatz: Je weniger
eine Funktion kann, desto besser (schneller) kann sie das, was sie kann.

33.4 Beispiel: Zeilenminima einer Matrix

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.

> nrow <- 10^7 # Anzahl der Zeilen


> ncol <- 10 # Anzahl der Spalten

> # Matrix mit gleichverteilten Zufallszahlen generieren


> M <- matrix(runif(nrow * ncol), nrow = nrow, ncol = ncol)

Variante 1: apply()

> zeit1 <- system.time(apply(M, MARGIN = 1, min))


> zeit1
User System verstrichen
27.96 0.14 28.10
488 H Effizienz und Simulation

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

> zeit2 <- system.time({


+ res <- numeric(nrow) # Denke an eine sinnvolle Initialisierung ;-)
+ for (i in 1:nrow) {
+ res[i] <- min(M[i, ]) # Minimum der i. Zeile berechnen
+ }
+ })
> zeit2
User System verstrichen
7.87 0.07 7.94

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.

Regel 19: Senke die Anzahl der Schleifendurchläufe!

Versuche, bei Verwendung von Schleifen die Anzahl der Schleifendurchläufe so


weit wie möglich zu reduzieren!

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:

1. Großartig wäre das!


2. Ja, das geht!

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 <- M[, 1]


res <- pmin(res, M[, 2])

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

> zeit3 <- system.time({


+ res <- M[, 1]
+
+ for (j in 2:ncol) {
+ res <- pmin(res, M[, j])
+ }
+ })
> zeit3
User System verstrichen
1.11 0.17 1.29

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.

33.5 Beispiel: Würfelwurf – Augensumme erreichen

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?

> # Die Parameter


> n <- 10^6 # Anzahl der Simulationsläufe
> k <- 100 # gewünschte Augensumme

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.

> time1 <- system.time(res.sim1 <- sim1(n, k))


> time1
User System verstrichen
131.20 0.68 131.91

> summary(res.sim1) # Plausibilitätscheck


Min. 1st Qu. Median Mean 3rd Qu. Max.
20.00 27.00 29.00 29.05 31.00 44.00
490 H Effizienz und Simulation

> sim1 <- function(n, k) {


+ res <- numeric(n) # Ergebnisvektor initialisieren
+
+ for (i in 1:n) {
+ iter <- 0
+ summe <- 0
+
+ while (summe < k) {
+ iter <- iter + 1
+ summe <- summe + sample(1:6, size = 1)
+ }
+
+ res[i] <- iter
+ }
+
+ return(invisible(res))
+ }

Abbildung 33.2: Funktion sim1() für das Würfelwurfbeispiel (33.5)

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.

> time2 <- system.time(res.sim2 <- sim2(n, k))


> time2
User System verstrichen
140.20 0.82 141.03

> summary(res.sim2) # Plausibilitätscheck


Min. 1st Qu. Median Mean 3rd Qu. Max.
20.00 27.00 29.00 29.05 31.00 43.00

In der nächsten Variante packen wir die erste Beschleunigungsmaßnahme aus.


33 Effizienz 491

> sim2 <- function(n, k) {


+
+ # Definiere temporäre Funktion - wie oben
+ tempfun <- function(k) {
+ iter <- 0 # zählt Würfelwürfe
+ summe <- 0 # aktuelle Augensumme
+
+ while (summe < k) {
+ iter <- iter + 1
+ summe <- summe + sample(1:6, size = 1)
+ }
+
+ res <- iter
+ return(res)
+ }
+
+ # n Mal tempfun() ausführen und Ergebnisse zurückgeben
+ res <- sapply(rep(k, n), tempfun)
+ return(invisible(res))
+ }

Abbildung 33.3: Funktion sim2() für das Würfelwurfbeispiel (33.5)

Variante 3: Strukturinformation und Matrizen nützen

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.

> time3 <- system.time(res.sim3 <- sim3(n, k))


> time3
User System verstrichen
11.31 0.41 11.73

> summary(res.sim3) # Plausibilitätscheck


Min. 1st Qu. Median Mean 3rd Qu. Max.
19.00 27.00 29.00 29.05 31.00 45.00

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

> sim3 <- function(n, k) {


+
+ # n x k Matrix mit Würfelwürfen
+ Wurf <- matrix(sample(1:6, size = n * k, replace = TRUE), ncol = k)
+
+ # Berechne kumulierte (Augen)Summen
+ # Ergebnis ist k x n - Matrix (Warum?)
+ temp <- apply(Wurf, 1, cumsum)
+
+ # Zählen für jeden Durchgang, wie lange die Augensumme unter k bleibt
+ # und zählen den entscheidenden Wurf mit + 1 dazu.
+ res <- colSums(temp < k) + 1
+
+ return(invisible(res))
+ }

Abbildung 33.4: Funktion sim3() für das Würfelwurfbeispiel (33.5)

> Wurf <- matrix(sample(1:6, size = 10 * 5, replace = TRUE), ncol = 5)


> Wurf
[,1] [,2] [,3] [,4] [,5]
[1,] 3 6 2 2 6
[2,] 4 6 3 5 2
[3,] 1 4 5 5 3
[4,] 4 3 4 4 2
[5,] 5 4 1 3 2
[6,] 4 2 4 6 3
[7,] 2 1 5 1 1
[8,] 2 2 1 2 3
[9,] 4 1 5 4 3
[10,] 2 5 6 6 6
> temp <- apply(Wurf, 1, cumsum)
> temp
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,] 3 4 1 4 5 4 2 2 4 2
[2,] 9 10 5 7 9 6 3 4 5 7
[3,] 11 13 10 11 10 10 8 5 10 13
[4,] 13 18 15 15 13 16 9 7 14 19
[5,] 19 20 18 17 15 19 10 10 17 25
> colSums(temp < 5) + 1
[1] 2 2 2 2 1 2 3 3 2 2

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.

> time4 <- system.time(res.sim4 <- sim4(n, k))


> time4
User System verstrichen
3.50 0.11 3.61

> summary(res.sim4) # Plausibilitätscheck


Min. 1st Qu. Median Mean 3rd Qu. Max.
19.00 27.00 29.00 29.05 31.00 44.00

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?

Variante 5: Paralleles Simulieren + Ausmisten mit while (Hardcore!)


Verbesserungspotenzial gibt es jetzt noch dahingehend, dass wir in Variante 4 stets
bei allen Durchgängen Würfelwürfe hinzuaddieren, auch wenn die Augensumme
vieler Durchgänge k bereits längst überschritten hat. Derlei Durchgänge fühlen sich
allerdings wie unnötiger Ballast an, den wir gerne loswerden wollen.

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

> sim4 <- function(n, k) {


+
+ res <- numeric(n) # Initialisiere Ergebnisvektor
+ summe <- rep(0, n) # Vektor der Augensummen
+ iter <- 0 # Iterationszähler: Anzahl der Würfelwürfe
+
+ while(any(summe < k)) {
+ # bool markiert jene Elemente von summe, die noch kleiner als k sind
+ # und bei denen noch mind. ein Würfelwurf erfolgen muss.
+ bool <- summe < k
+
+ # Einen weiteren Wurf hinzuaddieren
+ iter <- iter + 1
+ summe <- summe + sample(1:6, size = n, replace = TRUE)
+
+ # bestimme jene Durchgänge, die vor dem Durchgang noch nicht fertig
+ # waren (bool == TRUE) und die in diesem Durchgang die Augensumme von
+ # k erreicht oder überschritten haben (summe >= k).
+ fertig <- bool & (summe >= k)
+ res[fertig] <- iter
+ }
+
+ return(invisible(res))
+ }

Abbildung 33.5: Funktion sim4() für das Würfelwurfbeispiel (33.5)


33 Effizienz 495

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))
+ }

Abbildung 33.6: Funktion sim5() für das Würfelwurfbeispiel (33.5)


496 H Effizienz und Simulation

> time5 <- system.time(res.sim5 <- sim5(n, k))


> time5
User System verstrichen
2.10 0.00 2.09

> summary(res.sim5) # Plausibilitätscheck


Min. 1st Qu. Median Mean 3rd Qu. Max.
19.00 27.00 29.00 29.05 31.00 43.00

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])"

> # Umwandlung in eine expression


> expr <- parse(text = str)
> expr
expression(c(time1[3], time2[3], time3[3], time4[3], time5[3]))

> # Ausführung der Expression


> zeit <- eval(expr)
> names(zeit) <- paste0("Variante", 1:length(zeit))
> zeit
Variante1 Variante2 Variante3 Variante4 Variante5
131.91 141.03 11.73 3.61 2.09

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 ;-)

> # Sortiere die Laufzeiten absteigend


> zeit.sort <- sort(zeit, decreasing = TRUE)
> zeit.sort
Variante2 Variante1 Variante3 Variante4 Variante5
141.03 131.91 11.73 3.61 2.09
33 Effizienz 497

33.6 Beispiel: Flächeninhalt zufälliger Dreiecke

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

Abbildung 33.7: Drei zufällige Dreiecke

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.

> n <- 10^6

> # flaeche() gibt einen Flächeninhalt zurück.


> flaeche <- function() {
+ M <- matrix(runif(4), ncol = 2)
+ res <- abs(det(M)) / 2
+ return(res)
+ }
498 H Effizienz und Simulation

1.0
0.8
x3

0.6
 
y3

x2
 
y2
0.4
0.2

x1
 
y1
0.0

0.0 0.2 0.4 0.6 0.8 1.0

Abbildung 33.8: Das grüne Dreieck alleine und gemeinsam mit dem blauen Drei-
eck als Parallelogramm

> # Ergebnisse simulieren


> res <- rep(NA, n)

> for (i in 1:n) {


+ res[i] <- flaeche()
+ }

Gute Nacht! Du kannst mit der Esc-Taste links oben abbrechen... Sind die Ergebnisse
wenigstens gut?

> # Erwartungswert schätzen > # Varianz schätzen


> 13 / 108 # exaktes Ergebnis > 229 / 23328 # exaktes Ergebnis
[1] 0.1203704 [1] 0.009816529

> mean(res) # simuliert > var(res) # simuliert


[1] 0.1203554 [1] 0.009824283

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

> flaeche1 <- function(n) {


+ temp <- runif(n) * runif(n) - runif(n) * runif(n)
+ res <- abs(temp) / 2
+ return(res)
+ }

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!

> # Ergebnisse simulieren


> res <- flaeche1(n)

Zack, viel schneller! Probieren wir mehr Werte aus...

> res <- flaeche1(n = 10 * n)

Immer noch sehr schnell!

> # Erwartungswert schätzen > # Varianz schätzen


> 13 / 108 # exaktes Ergebnis > 229 / 23328 # exaktes Ergebnis
[1] 0.1203704 [1] 0.009816529

> mean(res) # simuliert > var(res) # simuliert


[1] 0.1203326 [1] 0.009811722

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.1 Zusammenfassung der Effizienztipps

Wir fassen unsere gesammelten (Effizienz)Tipps zusammen.

• Überprüfe deinen Code stets auf Plausibilität und teste ihn!


• Wenn du for-Schleifen verwendest, dann initialisiere den Ergebnisvektor in
geeigneter Art und Weise.
• Wenn du Schleifen oder schleifenähnliche Konstrukte (zum Beispiel apply()
oder sapply()) verwendest, dann achte darauf, dass die Anzahl der Iterationen
möglichst klein ist.
• Versuche auf Schleifen und schleifenähnliche Konstrukte zu verzichten. Verwen-
de stattdessen besser vektorwertige Funktionen wie beispielsweise ifelse(),
cumsum(), colSums() etc.
• Faustregel: Je weniger eine Funktion kann, desto besser (schneller) kann sie
das, was sie kann.
• Oft bieten sich geschickte Transformationen an, welche effiziente Lösungen
ermöglichen. So gesehen im Dreiecksflächenbeispiel in (33.6).
• Nicht angesehen aber dennoch erwähnenswert: Der Zugriff auf Spalten eines
Dataframes kann langsam sein, da beim Zugriff die Spalte des Dataframes
kopiert wird. In Aufgaben, bei denen wir häufig auf Spalten eines Dataframes
zugreifen, bietet es sich an, die Spalten mittels attach() global verfügbar zu
machen (vgl. (25.4)).

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

Daher ist es ratsam, zeitkritische Simulationsprozesse in andere schnellere Program-


miersprachen auszulagern. Eine Möglichkeit: Wir simulieren beispielsweise in C++
oder Java einen Prozess, speichern die Daten ab, lesen sie in R ein und werten sie
ebenda aus. Wie können alternativ auch C++-Code in R einbinden, beispielsweise
mit dem Paket Rcpp. Auch für andere Programmiersprachen gibt es entsprechende
Pakete.

33.8.3 Übungen

1. Wir wollen für jede Zeile der folgenden Matrix M die Summe der quadrierten
Werte berechnen.

> # Matrix generieren


> n <- 10^6 # Anzahl der Zeilen
> k <- 10 # Anzahl der Spalten
> M <- matrix(sample(1:6, size = n * k, replace = TRUE), ncol = k)

Wir betrachten vier Varianten, welche die Aufgabe korrekt lösen:

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
+ } + }

a) Warum ist Code C schneller als Code A?


b) Warum ist Code D schneller als Code C?
c) Wo wird sich Code B ungefähr von der Laufzeit einreihen?
d) Finde einen Code, der schneller ist als alle oben genannten und zum selben
Ergebnis führt.

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)

a) Wir wollen die kumulierten Mittelwerte berechnen, also einen Vektor


k
T 1X
(x̄1 , x̄2 , . . . , x̄n ) wobei x̄k := xi .
k i=1

Ein Kollege schlägt folgende Lösung vor:


mittelwert <- NULL

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

x̄k wurde in 3a) definiert. Der selbe Kollege schlägt vor:


varianz <- NULL
for (i in 1:length(x)) {
varianz <- c(varianz, var(x[1:i]))
}

Finde eine deutlich effizientere Möglichkeit, die kumulierten Varianzen zu


berechnen.
Hinweis: Der Verschiebungssatz könnte sich als sehr nützlich erweisen!

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

W <- matrix(sample(1:6, size = n * k, replace = TRUE), ncol = k)


W.sort <- t(apply(W, 1, sort))
33 Effizienz 503

Das dauert sehr lange. Wir wollen eine schnellere Lösung finden. Ein Kollege
schlägt hierfür folgende Variante vor:

W.sort <- W

for (i in 1:(ncol(W.sort) - 1)) {


for (j in ((i + 1) : ncol(W.sort))) {
bool <- W.sort[, i] < W.sort[, j]
W.sort[, i] <- ifelse(bool, W.sort[, i], W.sort[, j])
W.sort[, j] <- ifelse(bool, W.sort[, j], W.sort[, i])
}
}

a) Erkläre, warum die vorgeschlagene Variante in dieser Form noch nicht


einwandfrei funktioniert.
Tipp: Schaue dir die Situation für kleine n an. Vollziehe auf einem Blatt
Papier den Fall W <- t(c(5, 6, 5, 1, 2)) nach.
b) Verbessere den obigen Code, sodass er funktioniert.
Tipp: Vergleiche das Ergebnis mit der korrekten Lösung der apply()-
Variante.
c) Bei dem verwendeten Sortieralgorithmus handelt es sich um einen nicht
effizienten Sortieralgorithmus. Trotzdem ist die Variante mit der Doppel-
schleife schneller als die apply()-Version. Warum?

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

6. Gegeben ist ein numerischer Vektor x mit n ≥ 2 Elementen (der Einfachheit


halber ohne fehlende Werte). Wir wollen nun einen Vektor y derselben Länge
wie folgt bestimmen:

x1 + x2
 falls i = 1
yi = xi−1 + xi + xi+1 falls 1 < i < n

xn−1 + xn falls i = n

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

34 Einführung in die 2D-Grafik

Dort wollen wir in diesem Kapitel hin:

• erste Grafiken erstellen und einfache Grafikeinstellungen kennenlernen

• die Welt der Streudiagramme erkunden


• Grafikelemente hinzufügen: Punkte, Linien, Texte, Boxen, Gitter, Achsen, Le-
genden, Polygone, intelligente Linien, Pfeile und Beschriftungen

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!

Infobox 13: Iris Datensatz

Der Iris-Datensatz wurde erstmals vom englischen Statistiker Ronald Fisher


1936 zur Demonstration der Diskriminanzanalyse verwendet. Erfasst wurden
die Daten vom amerikanischen Botaniker Edgar Anderson. Der Datensatz um-
fasst Messungen von 150 Irisblüten dreier unterschiedlicher Spezies (Species):
setosa, virginica und versicolor. Für jede Blüte hat Anderson die Länge
und Breite der Kelchblätter (Sepal.Length, Sepal.Width) sowie Blütenblätter
(Petal.Length, Petal.Width) erfasst, jeweils in cm.

> # Iris-Datensatz laden


> data(iris)
> head(iris, n = 4)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa

Wir fragen uns unter anderem:

• Wie erstellen wir aus Petal.Length und Petal.Width ein Streudiagramm?


(34.1.2)
• Wie können wir in diesem Streudiagramm die drei Spezies durch unterschied-
liche Farben und Symbole unterscheidbar machen? (34.2.7)
• Wie zeichnen wir Mittelwertsvektoren ein? (34.3.1)
• Wie fügen wir eine Legende hinzu, die uns Aufschluss darüber gibt, zu welcher
Spezies jeder Punkt gehört? (34.3.2)
© 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_34
34 Einführung in die 2D-Grafik 507

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!

34.1 Basisfunktion zur Grafikerstellung – plot()

34.1.1 Erste Grafiken und Einstellungen

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():

plot(x, y = NULL, type = "p", xlim = NULL, ylim = NULL,


main = NULL, sub = NULL, xlab = NULL, ylab = NULL, ...)

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).

Tabelle 34.1: Wichtige Parameter der Funktion plot()

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.

> # Definiere Beispielpunkte


> x <- c(1, 2, 3, 4, 5) # x-Koordinaten der Beispielpunkte
> y <- c(2, 5, 4, 1, 3) # y-Koordinaten der 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.

> plot(x = x, y = y, > plot(x, y, type = "l",


+ main = "Punktegrafik") + main = "Liniengrafik")

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

> plot(x, y, type = "b", > plot(x, y, type = "n",


+ main = "Punkte und Linien") + main = "Leere Grafik")

Punkte und Linien Leere Grafik


5

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.

> plot(x, y, type = "S", > plot(x, y, type = "s",


+ main = "Treffengrafik S", + main = "Treppengrafik s",
+ sub = "Variante S") + sub = "Variante s")

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.

> # Stabdiagramme: angelehnt an Histogramme


> plot(x, y, type = "h", main = "Stabdiagramm")

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).

> # Beschrifte die Achsen und verändere den Zeichnungsbereich


> plot(x, y, type = "h", main = "Stabdiagramm",
+ xlab = "Anzahl", ylab = "Häufigkeiten",
+ xlim = c(0, max(x)), 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 und y verknüpfen


> M <- cbind(x, y) Grafik aus Matrix
> M
5

x y
[1,] 1 2
y−Achse = 2. Spalte

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

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

> # plot() mit n x 2 Matrix


1

> plot(M,
+ main = "Grafik aus Matrix", 1 2 3 4 5

+ xlab = "x-Achse = 1. Spalte", x−Achse = 1. Spalte


+ ylab = "y-Achse = 2. Spalte")
34 Einführung in die 2D-Grafik 511

34.1.2 Streudiagramme zum Ersten – plot()

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.

> # Streudiagramm zeichnen


> plot(x = iris$Petal.Length, y = iris$Petal.Width,
+ main = "Darstellung der iris-Daten",
+ xlab = "Petal Length", ylab = "Petal Width")

Darstellung der iris−Daten


2.5
2.0
Petal Width

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 ;-)

34.2 Grafikparameter steuern – par()

Wir haben in (34.1) bereits erste Grafiken erstellt, mit dem Parameter type experi-
mentiert und Achsen beschriftet.
Einige Frage kommen auf, zum Beispiel:

• Warum werden die Punkte als Ringe gezeichnet?


• Warum sind die Punkte schwarz?

• Warum haben die Punkte diese Größe?


512 I Visualisierung von Daten

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:

> # pch (point character) speichert die Form der Punkte


> par()$pch # Defaultwert 1 entspricht den Ringen
[1] 1

> # col (color) steuert die Farbe der Punkte


> par()$col # Defaultwert "black" entspricht Schwarz
[1] "black"

> # cex (character expansion) regelt die Größe der Punkte


> par()$cex # Defaultwert 1 entspricht der Normalgröße
[1] 1

In Tab. 34.2 verschaffen wir uns einen Überblick über einige wichtige Grafikparame-
ter. All diese Parameter können wir auch in plot() verwenden.

Tabelle 34.2: Ausgewählte Grafikparameter von par()

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:

> # Definiere Beispielpunkte


> x <- c(1, 2, 3, 4, 5) # x-Koordinaten der Beispielpunkte
> y <- c(2, 5, 4, 1, 3) # y-Koordinaten der Beispielpunkte
34 Einführung in die 2D-Grafik 513

34.2.1 Skalierung – cex

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.

> # Betrachte die Standardwerte für die 5 cex-Parameter


> par()[c("cex", "cex.axis", "cex.lab", "cex.main", "cex.sub")]
$cex
[1] 1
$cex.axis
[1] 1
$cex.lab
[1] 1
$cex.main
[1] 1.2
$cex.sub
[1] 1

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 ;-)

> # cex in par() umstellen > # cex in par() umstellen


> par(cex = 2) > par(cex = 1)
> par()$cex > par()$cex
[1] 2 [1] 1
> plot(x, y, main = "Maintext", > plot(x, y, main = "Maintext",
+ cex.main = 0.6, cex.axis = 0.5) + cex = 2, cex.main = 1.2)

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.

34.2.2 Farben – col

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

Standardmäßig ist col = 1 (Schwarz) gesetzt.


Wir können auch Farbnamen übergeben. Viele Farben lassen sich aus der Helligkeit
und der eigentlichen Farbe zusammenstellen. So erkennt R beispielsweise "blue"
(„normales“ Blau) genauso wie "lightblue" oder "darkblue".
34 Einführung in die 2D-Grafik 515

Eine ausführliche Farbpalette findest du zum Beispiel unter1


http://www.stat.columbia.edu/~tzheng/files/Rcolor.pdf
In (35.2) lernen wir weitere coole Möglichkeiten zur Farbgebung kennen.

34.2.3 Punktarten – pch

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

34.2.4 Linienart und Linienstärke – lty und lwd

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.

34.2.5 Hintergrundfarbe – bg, "transparent"

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.

34.2.6 Umgang mit par()

par() ist eine „normale“ Funktion. Anpassungen der par()-Einstellungen erfolgen


durch einen einfachen Funktionsaufruf:

> par(lwd = 1) # setzt lwd auf 1


> par(cex = 1.5, col = 2) # setzt cex auf 1.5 und die Farbe auf Rot

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:

> # Speichere aktuelle Einstellungen und überschreibe lwd und cex


> par.alt <- par(lwd = 2, cex = 2)
> par()$lwd # Auf 2 eingestellt
[1] 2

> # Diverser Code zur Grafikerstellung und -bearbeitung


34 Einführung in die 2D-Grafik 517

> # Zurücksetzen der par()-Einstellungen


> par(par.alt)
> par()$lwd # Wieder auf 1 zurückgesetzt
[1] 1

Praxistipp: Wenn du dir nicht sicher bist, ob du Grafikparameter in par() oder in


Funktionen wie plot() einstellen sollst, dann wähle im Zweifel die zweite Variante.
Wenn du ein Grafikfenster schließt, werden die par()-Einstellungen automatisch auf
die Standardwerte zurückgesetzt (siehe auch (36.1)).

34.2.7 Streudiagramme zum Zweiten

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.

> # Definiere Farben und Punktformen für jede Spezies


> col.roh <- c(2, 3, 4)
> pch.roh <- c(1, 2, 3)

> # Weise den Punkten die korrekten Farben und Symbole zu


> col <- col.roh[iris$Species]
> pch <- pch.roh[iris$Species]

> # Grafik zeichnen


> plot(iris$Petal.Length, iris$Petal.Width, col = col, pch = pch)
2.5
2.0
iris$Petal.Width

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 ;-)

34.3 Grafikelemente hinzufügen

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.

34.3.1 Punkte, Linien, Texte – points(), lines(), text(), mtext()

Schauen wir uns gleich eine Übersicht an:

• points(): zeichnet Punkte ein


• lines(): zeichnet Linien ein
• text(): fügt Texte in der Grafik ein
• mtext(): zeichnet Erklärungstexte am Rand ein

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:

text (x, y = NULL, labels = seq_along(x), ...)


mtext(text, side = 3, ...)

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.

> # Grafik zeichnen


> plot(iris$Petal.Length, iris$Petal.Width,
+ main = "Iris: Petal.Width ~ Petal.Length", col = col, pch = pch)

> # Erklärungstext hinzufügen (side = 3)


> mtext("Mit Mittelwertsvektoren")

> # Mittelwerte getrennt nach Spezies berechnen


> mw.length <- tapply(iris$Petal.Length, iris$Species, mean, na.rm = TRUE)
> mw.width <- tapply(iris$Petal.Width, iris$Species, mean, na.rm = TRUE)

> # Mittelwertsvektoren einzeichnen


> points(mw.length, mw.width, col = col.roh, pch = 16, cex = 2)

> text(2, c(2.5, 2.25, 2), labels = levels(iris$Species), col = col.roh)

Iris: Petal.Width ~ Petal.Length Iris: Petal.Width ~ Petal.Length


Mit Mittelwertsvektoren Mit Mittelwertsvektoren
2.5

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.

34.3.2 Legenden – legend()

Legenden sind für die Erklärung einer Grafik oft unerlässlich. Der stark vereinfachte
Funktionsaufruf der Funktion legend():

legend(x, y = NULL, legend, col = par("col"), lty, lwd, pch,


bty = "o", bg = par("bg"), box.lwd = par("lwd"),
box.lty = par("lty"), box.col = par("fg"), cex = 1, pt.cex = cex,
xjust = 0, yjust = 1, text.col = par("col"), ncol = 1,
title = NULL, inset = 0)
520 I Visualisierung von Daten

Wir listen in Tab. 34.3 ausgewählte Parameter von legend() auf.

Tabelle 34.3: Ausgewählte Parameter 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.

> # Grafik zeichnen


> plot(iris$Petal.Length, iris$Petal.Width,
+ main = "Iris: Petal.Width ~ Petal.Length", col = col, pch = pch)

> # Legende einzeichnen (links oben)


> legend("topleft", legend = levels(iris$Species),
+ col = col.roh, pch = pch.roh)

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

> # Legende einzeichnen (rechts unten)


> legend(x = max(iris$Petal.Length), y = min(iris$Petal.Width),
+ xjust = 1, yjust = 0, col = col.roh, pch = pch.roh,
+ legend = levels(iris$Species))

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.

Iris: Petal.Width ~ Petal.Length Iris: Petal.Width ~ Petal.Length


2.5

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

Schauen wir uns abschließend zwei weitere Möglichkeiten an.

> plot(iris$Petal.Length, > plot(iris$Petal.Length,


+ iris$Petal.Width, + iris$Petal.Width,
+ col = col, pch = pch) + col = col, pch = pch)

> legend("topleft", inset = 0.05, > legend("topleft",


+ legend = levels(iris$Species), + inset = c(0.02, 0.05),
+ pch = pch.roh, col = col.roh, + legend = levels(iris$Species),
+ bg = "lightgray", + pch = pch.roh, col = col.roh,
+ text.col = col.roh) + title = "Spezies", bty = "n")
2.5

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

34.3.3 Boxen, Gitter, Achsen – box(), grid(), axis(), pretty()

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.

axis(side, at = NULL, labels = TRUE, tick = TRUE, pos = NA,


lty = "solid", lwd = 1, lwd.ticks = lwd, col = NULL,
col.ticks = NULL, ...)

Tabelle 34.4: Ausgewählte Parameter der Funktion axis()

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.

> # Definiere die x|y Koordinaten


> x <- seq(-2, 2, length = 201) # Bildgenauigkeit
> y <- tanh(x)

> # axes = FALSE => keine Box und keine Achsen


> plot(x, y, type = "l", axes = FALSE, lwd = 2,
+ col = 4, main = "Tangenshyperbolicus")

> # Gitternetz einzeichnen


> grid(lwd = 20, lty = 1) # lwd = 20 ist zu dick, siehe gleich ;-)

> # Achsen einzeichnen


> axis(side = 1, pos = 0) # Zeichne horizontale Achse bei y = 0
> axis(side = 2, pos = 0) # Zeichne vertikale Achse bei x = 0
34 Einführung in die 2D-Grafik 523

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:

> # Zeichne eine "nette" Box ein


> box(col = 2, lty = 2)

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.

34.3.4 Zeichenreihenfolge steuern – type = "n"

Wir haben gerade eben gesehen, dass existierende Grafikelemente gegebenenfalls


durch neue Grafikelemente überzeichnet werden.

Im Beispiel des vorhergehenden Abschnitts sollte die Funktionsgerade über das


Gitternetz und die Achsen gezeichnet werden. Um dieses Vorhaben umzusetzen,
müssen wir also sicherstellen, dass die Funktionsgerade erst am Ende gezeichnet
wird. Das können wir mit folgenden Schritten umsetzen:

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.

3. Abschließend zeichnen wir die Funktionsgerade (mit lines()) ein.


524 I Visualisierung von Daten

Beispiel: Fortsetzung des Beispiels auf Seite 522. Die Funktionsgerade soll über das
Gitternetz und über die Achsen gezeichnet werden.

> # Definiere die x|y Koordinaten


> x <- seq(-2, 2, length = 201)
> y <- tanh(x)

> # Schritt 1: Verhindere das Zeichnen der Funktionsgeraden


> plot(x, y, type = "n", axes = FALSE, lwd = 2,
+ col = 4, main = "Tangenshyperbolicus")

> # Schritt 2: Gitter und Achsen einzeichnen


> grid(lwd = 20, lty = 1)
> # Achsen einzeichnen
> axis(side = 1, pos = 0) # Zeichne horizontale Achse bei y = 0
> axis(side = 2, pos = 0) # Zeichne vertikale Achse bei x = 0

> # Schritt 3: Funktionsgerade einzeichnen


> lines(x, y, lwd = 2, col = 4)

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.

34.3.5 Rechtecke und Polygone – rect(), polygon()

Mit Hilfe der Funktion polygon() können wir Polygone zeichnen. In Tab. 34.5
listen wir wichtige Parameter auf. Der Funktionsaufruf:

polygon(x, y = NULL, density = NULL, angle = 45,


border = NULL, col = NA, lty = par("lty"), ...)

Wir betrachten ein Codebeispiel.


34 Einführung in die 2D-Grafik 525

Tabelle 34.5: Ausgewählte Parameter der Funktion polygon()

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.

> # Plotregion vorbereiten


> par(pty = "s") # Quadratische Plotregion einstellen
> plot(0:5, 0:5, type = "n", xlab = "", ylab = "", main = "Polygone")

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.

> # Rotes Polygon zeichnen


> x1 <- c(0, 0, 1, 1, 5, 5)
> y1 <- c(0, 5, 5, 1, 1, 0)
> polygon(x1, y1, col = 2, density = 8, angle = 90, border = NA)

> # Blaues Polygon zeichnen > # Oranges Polygon zeichnen


> x2 <- c(2, 2, 3, 3, 5, 5) > x3 <- c(4, 4, 5, 5)
> y2 <- c(2, 5, 5, 3, 3, 2) > y3 <- c(4, 5, 5, 4)
> polygon(x2, y2, col = 4, > polygon(x3, y3, col = "orange")
+ density = 2)

Polygone
5
4
3
2
1
0

0 1 2 3 4 5
526 I Visualisierung von Daten

Beim roten Polygon werden Schraffierungen gezeichnet, weil wir density = 8


gesetzt haben. Die Schraffur verläuft senkrecht, da wir angle = 90 gewählt haben.
border = NA unterdrückt die Umrandung.
Beim blauen Polygon ist die Schraffur weniger dicht als beim roten Polygon, da
density kleiner ist. Die Farbe der Umrandung orientiert sich an der Farbe der
Schraffierung, da wir für border nichts übergeben haben.
Beim orangen Polygon werden wegen des Defaultwerts density = NULL keine
Schraffierungen gezeichnet. Hätten wir col nicht spezifiziert, würde nur die Umran-
dung gezeichnet werden. Die Farbe der Umrandung ist Schwarz (wegen par()$fg).
Die Funktion rect() ist eine einfache Funktion zum Zeichnen von Rechtecken.
Im Gegensatz zu polygon() funktioniert sie auch vektorwertig.

34.3.6 Adjustierungen und quadratische Plots – adj, padj, par(pty = "s")

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

> # Grafik vorbereiten


> par(pty = "s") # Quadratische Plotregion
> plot(0.5:8.5, 0.5:8.5, axes = FALSE, xlab = "", ylab = "", type = "n")

> # Schachbrettmuster einzeichnen


> for (i in 1:8) {
+ for (j in 1:8) {
+ x <- c(i - 0.5, i + 0.5, i + 0.5, i - 0.5)
+ y <- c(j + 0.5, j + 0.5, j - 0.5, j - 0.5)
+ polygon(x, y, col = (i + j + 1) %% 2)
+ }
+ }
34 Einführung in die 2D-Grafik 527

> # Achsen einzeichnen


> axis(1, at = 1:8, LETTERS[1:8], lwd = 0, padj = -2)
> axis(2, at = 1:8, 1:8, lwd = 0, padj = 2)
> axis(3, at = 1:8, LETTERS[1:8], lwd = 0, padj = 2)
> axis(4, at = 1:8, 1:8, lwd = 0, padj = -2)

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.

34.3.7 Linien und Pfeile – abline(), segments(), arrows()

Drei weitere Funktionen bieten sich an, wenn es um Linien und Pfeile geht. Mit
abline() können wir „intelligente“ durchgezogene Linien einzeichnen.

abline(a = NULL, b = NULL, h = NULL, v = NULL, reg = NULL,


coef = NULL, untf = FALSE, ...)

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:

segments(x0, y0, x1, y1, ...)

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.

Tabelle 34.6: Ausgewählte Parameter der Funktion arrows()

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“)

Zeit für ein Codebeispiel zur Veranschaulichung!


528 I Visualisierung von Daten

> # 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!

34.3.8 Titel und Überschriften – title()

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.

title(main = NULL, sub = NULL, xlab = NULL, ylab = NULL,


line = NA, outer = FALSE, ...)

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

Tabelle 34.7: Parameter der Funktion title()

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?

> plot(0:1, 0:1, > plot(0:1, 0:1,


+ xlab = "", ylab = "") + xlab = "", ylab = "")

> # Titel und Achsenbeschriftungen > # line ändern


> # hinzufügen > title(main = "Main 0", line = 0)
> title(main = "Main", > title(main = "Main 1", line = 1)
+ xlab = "xlab", ylab = "ylab") > title(main = "Main 2", line = 2)

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

34.4 Aus der guten Praxis

34.4.1 Fallbeispiel: Uhr

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

circle <- function(mx = 0, my = 0, radius = 1, prec = 360) {


# Diese Funktion berechnet Kreiskoordinaten
# mx und my ... Mittelpunkt des Kreises
# radius ...... Radius des Kreises
# prec ........ Anzahl der Punkte (Präzision)
# Gibt eine (prec + 1) Mal 2 - Matrix mit kartesischen Koordinaten zurück

# Berechnung der kartesischen Koordinaten


winkel <- seq(0, 2 * pi, length = prec + 1)
x <- radius * cos(winkel)
y <- radius * sin(winkel)

# Unsichtbare Rückgabe der kartesischen Koordinaten


invisible(cbind(x = x + mx, y = y + my))
}

Abbildung 34.1: Funktion zur Bestimmung von kartesischen Kreiskoordinaten

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().


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

Abbildung 34.2: Links: Erläuterung der Polarkoordinaten. Der Ursprung ist


durch den Punkt in der Mitte markiert. Ausgehend vom Win-
kel 0 = 2π (entspricht 3 Uhr) erhöht sich zu jeder vollen Stunde
der Winkel um genau π6 , wobei die Richtung gegen den Uhr-
zeigersinn verläuft. 1 Uhr entspricht somit dem Winkel 2π 6 . Die
Polarkoordinate (ϕ = 2π 6 , r = 1) entspricht der kartesischen Ko-
ordinate (x = 0.5, y = 0.87). Um die Umrechnung kümmert sich
circle(). Rechts: Output von uhr().
34 Einführung in die 2D-Grafik 531

uhr <- function() {


# Grafikfenster vorbereiten und Umriss einzeichnen
dev.new() # Neues Grafikfenster öffnen
par(pty = "s", mar = rep(0.5, 4)) # Grafikränder verkleinern
plot(circle(), type = "l", axes = FALSE, xlab = "", ylab = "",
xlim = c(-1.1, 1.1), ylim = c(-1.1, 1.1))

# Segmente vorbereiten: Jede der 60 Minuten bekommt ein Häkchen ...


p1 <- circle(prec = 60)
p2 <- circle(prec = 60, radius = 0.95)

# ... wobei alle 5 Minuten deutlich gekennzeichnet und verbunden werden.


bool.5 <- rep(rep(c(TRUE, FALSE), times = c(1, 4)), length = 60)
seg <- p1[bool.5, ]
segments(seg[1:6,1], seg[1:6,2], seg[7:12,1], seg[7:12,2], col = "grey")

# Häkchen für jede Minute einzeichnen


segments(p1[, 1], p1[, 2], p2[, 1], p2[, 2],
lwd = 2 * bool.5 + 2, col = c(2, 4)[bool.5 + 1])

# Systemzeit abfragen und in Winkel umrechnen


time <- as.POSIXlt(Sys.time()) # Uhrzeit als Liste time
stunde <- -(time$hour %% 12) * pi / 6 + pi/2 - (time$min / 60) * pi/6
minute <- -(time$min) * pi/30 + pi/2

# Umrechnung in kartesische Koordinaten


# Koordinaten des Stundenzeigers: x[1] und y[1]
# Koordinaten des Minutenzeigers: x[2] und y[2]
# Der Minutenzeiger soll länger sein als der Stundenzeiger.
x <- c(0.6, 0.85) * cos(c(stunde, minute))
y <- c(0.6, 0.85) * sin(c(stunde, minute))

# Zeiger einzeichnen
arrows(0, 0, x, y, lwd = 2)
points(0, 0, pch = 16, col = 4, cex = 2)

# Beschrifte die Stunden der Uhr


temp <- circle(prec = 12, radius = 1.1)
w <- temp[c(4:1, 12:5), ]
text <- c(12, 1:11)

# Rotiere die Stundenzahlen mit srt (String RoTation)


srt <- seq(360, 1, by = -30) # srt funktioniert leider nicht vektorwertig!
for (i in 1:length(srt)) {
text(w[i, 1], w[i, 2], text[i], srt = srt[i], cex = 1.7)
}
}

Abbildung 34.3: Funktion zur Anzeige der aktuellen Systemzeit


532 I Visualisierung von Daten

34.5 Abschluss

34.5.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Wie zeichnen wir mit plot() (eigentlich plot.default()) ein Streudiagramm?


Was steuern die Parameter type, xlim, ylim, main, sub, xlab und ylab und
wie setzen wir sie ein? Welche Einstellung müssen wir insbesondere vornehmen,
um Liniengrafiken zu zeichnen oder eine leere Grafik zu erzeugen? (34.1)
• Was macht bzw. ist par() und wie rufen wir die Hilfe dazu auf? Wie werden
die Grafikparameter in par() verwaltet und wie greifen wir auf sie zu? (34.2)
• Wie stellen wir die Größe der Punkte, der Überschrift und der Achsenbeschrif-
tungen um? (34.2.1)
• Wie färben wir die Punkte eines Streudiagramms ein? Wie beeinflussen wir
die Farbe der Überschrift und der Achsenbeschriftungen? Welche einfachen
Möglichkeiten der Farbauswahl haben wir? (34.2.2)
• Wie bestimmen wir die Darstellung der Punkte eines Streudiagramms (Sym-
bole)? Wie werden die übergebenen Objekte für Grafikparameter wie cex, col
und pch den Punkten zugeordnet? (34.2.3)
• Wie stellen wir die Strichart und Strichstärke von Linien ein? Wie können
wir Linien durchgezogen, gestrichelt, punktiert und strichpunktiert zeichnen?
(34.2.4)
• Wie stellen wir die Hintergrundfarbe einer Grafik um? Welche Bezeichnung
hat die transparente Farbe? (34.2.5)
• Wie greifen wir auf die aktuellen Grafikeinstellungen zu? Wie können wir Gra-
fikeinstellungen zwischenspeichern und sie später wieder laden? (34.2.6)
• Wie fügen wir Punkte, Linien, Texte und Erklärungstexte in Grafiken ein? Wie
bestimmen wir die Seite (unten, links, oben, rechts), auf der ein Erklärungstext
gedruckt werden soll? (34.3.1)
• Wie können wir mit legend() eine Legende in eine Grafik einzeichnen? Welche
Möglichkeiten haben wir, die Legende zu positionieren? Welche Rollen spielen
dabei die Parameter x, y, xjust, yjust und inset? (34.3.2)
• Wie zeichnen wir Boxen, Gitter und Achsen in eine Grafik ein? Wie verhindern
wir in der Funktion plot(), dass Achsen gezeichnet werden? Wie teilen wir
R mit, auf welcher Seite (unten, links, oben, rechts) wir die Achse zeichnen
wollen? Wie können wir die vertikale bzw. horizontale Positionierung der x-
Achse bzw. y-Achse einstellen? Wie werden Kurven in Grafiken dargestellt
bzw. angenähert? (34.3.3)
34 Einführung in die 2D-Grafik 533

• 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

1. Folgende Dichtefunktion kennen wir bereits aus (33.2):

f (x) = max(0, 1 − |x|) ∀x ∈ R

Zeichne die Dichtefunktion im Intervall [−1.5, 1.5]. Berücksichtige bei der Gra-
fikerstellung folgendes:

• Wähle eine Farbe für die Dichtefunktion aus.


• Zeichne ein Gitternetz ein, das die Dichtefunktion nicht überdeckt.
• Wähle geeignete Beschriftungen für die Überschrift, x-Achse und y-Achse.

• 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:

• Die Spezies sollen sich in der Grafik voneinander unterscheiden, sowohl


farblich als auch in der Form der Punkte.
• Zeichne die Mittelwertsvektoren gut erkennbar ein.
• Füge an geeigneter Stelle eine hübsche Legende ein, die uns darüber auf-
klärt, zu welcher Spezies die Punkte gehören.
• Die Legende soll keinen Punkt überdecken!

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.

5. Zeichne folgende Zielscheibe nach:

Triffst du ins Schwarze?

20
40
60
80

100

Du darfst dabei die Funktion circle() aus (34.4.1) verwenden.


6. Schreibe die Funktion uhr() aus (34.4.1) derart um, dass die Uhrzeit nicht
mehr im 12-Stunden-Format, sondern im 24-Stunden-Format gezeigt wird.
35 Standardgrafiken und Farben 535

35 Standardgrafiken und Farben

Dort wollen wir in diesem Kapitel hin:

• Liniengrafiken, Balkendiagramme, Histogramme und Boxplots erstellen

• in die Welt der Farben und Farbpaletten eintauchen

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

> # Ggf. Arbeitsverzeichnis wechseln


> # setwd(...)
> wahl <- read.table("NRWahlen.txt", na.string = ".",
+ dec = ",", header = TRUE, skip = 5, encoding = "UTF-8")

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.

Wir fragen uns unter anderem:

• 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

35.1.1 Parallele Liniengrafiken, Matrixplot – matplot()

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 ;-)

> # x-Werte erzeugen > # Alle Linien berechnen


> x <- seq(0, 2, length = 201) > Y <- cbind(1 * x, 2 * x, 3 * x)

> # Leeren Plot erzeugen


> # Erste Linie zeichnen
> plot(range(x), range(Y),
> plot(x, y = 1 * x, type = "l",
+ type = "n",
+ xlab = "x", ylab = "k * x")
+ xlab = "x", ylab = "k * x")

> # Weitere Linien einzeichnen > # Alle Linien einzeichnen


> for (k in 2:3) { > for (k in 1:ncol(Y)) {
+ lines(x, k * x, col = k) + lines(x, Y[, k], col = k)
+ } + }
2.0

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.

> # x-Werte erzeugen und alle y-Werte berechnen


> x <- seq(0, 2, length = 201)
> Y <- cbind(1 * x, 2 * x, 3 * x)

> # Alle Linien zeichnen > # Alle Linien zeichnen


> matplot(x, Y, type = "l", lwd = 3, > matplot(x, Y, type = "l", lty = 1,
+ xlab = "x", ylab = "k * x") + xlab = "x", ylab = "k * x")
6

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

Beispiel: Stelle den Verlauf der Ergebnisse der österreichischen Nationalratswahlen


in einer Grafik dar. Alle Parteien, die bei den letzten beiden Wahlen nicht angetreten
sind, sollen (zusätzlich) in die Kategorie Sonstige gesteckt werden.
Gehen wir es an! In der Einleitung zu diesem Kapitel haben wir die Daten bereits
eingelesen und im Objekt wahl abgespeichert.
Schritt 1: Nicht angetretene Parteien in Sonstige stecken

> 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.

> temp <- is.na(wahl[c(nrow(wahl) - 1, nrow(wahl)), ])


> bool.na <- colSums(temp) == 2
> which(bool.na)
LIF MATIN
7 9

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.

> # Sonstige updaten und nicht angetretene Parteien entfernen


> temp <- wahl[c(names(which(bool.na)), "Sonstige")]
> wahl$Sonstige <- rowSums(temp, na.rm = TRUE)
> wahl[bool.na] <- list(NULL)

> 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

Schritt 2: Verlauf der Wahlen grafisch darstellen


Die fertige Grafik kannst du auf Seite 540 bewundern. Wir bereiten zunächst die
Plotregion vor. Mit dev.new() öffnen wir ein neues Grafikfenster. Diese Funktion
lernen wir offiziell in (36.1) kennen. Man muss in der Regel die Breite und Höhe
mehrmals variieren, bis es am Ende gut aussieht.

> # Neues Fenster mit 8 inches Breite und 6 inches Höhe


> dev.new(width = 8, height = 6)
35 Standardgrafiken und Farben 539

> # Plot vorbereiten: Dimension und Beschriftungen


> matplot(wahl$Wahljahr, wahl[-1], type = "n", axes = FALSE,
+ xlab = "Wahljahr", ylab = "Prozent der gültigen Stimmen", ylim = c(0, 55),
+ main = "Nationalratswahlen in Österreich")

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 :-)

> # Gitternetz einzeichnen


> abline(v = wahl$Wahljahr, col = "grey") # Vertikale Linien
> abline(h = seq(0, 55, by = 5), col = "grey") # Horizontale Linien
> abline(h = 50, col = "grey", lwd = 3) # Absolute Mehrheit

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.

> # Parteifarben definieren und Ergebnisverläufe einzeichnen


> parteifarben <- c(SPÖ = "red", ÖVP = "black", FPÖ = "blue", KPÖ = "brown",
+ Grüne = "chartreuse4", BZÖ = "orange", FRANK = "gray40",
+ NEOS = "deeppink2", PILZ = "chartreuse", Sonstige = "gray80")

> matlines(wahl$Wahljahr, wahl[-1], lty = 1, type = "b", pch = 1,


+ col = parteifarben[names(wahl)[-1]], lwd = 1)

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.

> # Achsen und Legende einzeichnen


> box()
> axis(1, at = wahl$Wahljahr, las = 2, cex.axis = 0.8)
> axis(2, at = seq(0, 55, by = 5), las = 2)

> legend(min(wahl$Wahljahr), 55/2, legend = names(wahl)[-1],


+ col = parteifarben[names(wahl)[-1]], lwd = 2, bg = "white",
+ yjust = 0.5, ncol = 2, inset = c(0.03, 0))
540 I Visualisierung von Daten

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

35.1.2 Balkendiagramme – barplot()

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.

Tabelle 35.1: Ausgewählte Parameter der Funktion barplot()

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.

> # Selektiere Ergebnisse aller 2017 angetretenen Parteien als Vektor


> wahl17 <- unlist(wahl[wahl$Wahljahr == 2017, -1])
> wahl17 <- wahl17[!is.na(wahl17)]
> wahl17
SPÖ ÖVP FPÖ KPÖ Grüne NEOS PILZ Sonstige
26.9 31.5 26.0 0.8 3.8 5.3 4.4 1.3

> # Linkes Balkendiagramm zeichnen


> barplot(wahl17, main = "Nationalratswahl 2017",
+ col = parteifarben[names(wahl17)], las = 2)

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!

> # Jetzt absteigend sortieren und "Sonstige" hinten anhängen


> wahl17.sort <- c(sort(wahl17[names(wahl17) != "Sonstige"],
+ decreasing = TRUE), wahl17["Sonstige"])
> wahl17.sort
ÖVP SPÖ FPÖ NEOS PILZ Grüne KPÖ Sonstige
31.5 26.9 26.0 5.3 4.4 3.8 0.8 1.3

> # Rechtes Balkendiagramm zeichnen


> barplot(wahl17.sort, main = "Nationalratswahl 2017",
+ col = parteifarben[names(wahl17.sort)], las = 2)

Nationalratswahl 2017 Nationalratswahl 2017

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:

lighten <- function(color, faktor = 0.5) {


# Hellt die übergebenen Farben auf
# color .... Vektor mit Farben
# faktor ... Wert zwischen 0 und 1. Je größer, desto heller

# Die Farben Richtung Weiß verschieben


col <- col2rgb(color)
col <- col + (255 - col) * faktor
return(rgb(col[1, ], col[2, ], col[3, ], max = 255))
}

> # Selektiere die letzten beiden Wahlen als Matrix


> wahl2 <- as.matrix(wahl[(nrow(wahl) - 1):nrow(wahl), -1])
> wahl2
SPÖ ÖVP FPÖ KPÖ Grüne BZÖ FRANK NEOS PILZ Sonstige
21 26.8 24.0 20.5 1.0 12.4 3.5 5.7 5.0 NA 1.1
22 26.9 31.5 26.0 0.8 3.8 NA NA 5.3 4.4 1.3

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.

> # Parteifarben wählen (inkl. duplizieren und aufhellen)


> farben <- rep(parteifarben[colnames(wahl2)[order]], each = 2)
> farben[c(TRUE, FALSE)] <- lighten(farben[c(TRUE, FALSE)], 0.7)
35 Standardgrafiken und Farben 543

> # Gruppiertes Balkendiagramm zeichnen


> barplot(wahl2[, order], main = "Nationalratswahl 2013 vs. 2017",
+ col = farben, las = 2, beside = TRUE)

Nationalratswahl 2013 vs. 2017

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.

> # wahl2 transponieren


> wahl2t <- t(wahl2[, order])
> colnames(wahl2t) <- c(2013, 2017)
> wahl2t
2013 2017
ÖVP 24.0 31.5
SPÖ 26.8 26.9
FPÖ 20.5 26.0
NEOS 5.0 5.3
PILZ NA 4.4
Grüne 12.4 3.8
KPÖ 1.0 0.8
BZÖ 3.5 NA
FRANK 5.7 NA
Sonstige 1.1 1.3

Beachte: Bei gestapelten Balkendiagrammen müssen wir fehlende Werte durch 0


ersetzen, sonst funktioniert es nicht.
544 I Visualisierung von Daten

> # Fehlende Werte durch 0 ersetzen


> wahl2t[is.na(wahl2t)] <- 0

> # Farben auswählen


> farben <- parteifarben[rownames(wahl2t)]

> # Gestapeltes Balkendiagramm zeichnen


> barplot(wahl2t, main = "Nationalratswahl 2013 vs. 2017",
+ col = farben, las = 1, beside = FALSE)

Nationalratswahl 2013 vs. 2017


100

80

60

40

20

2013 2017

Sieht nicht besonders übersichtlich aus. Gestapelte Balkendiagramme machen vor


allem bei ordinalskalierten Merkmalen Sinn! Ein Beispiel schauen wir uns in (35.2.4)
an. Die Farben werden nach wie vor spaltenweise zugewiesen. Im Gegensatz zum
gruppierten Balkendiagramm können wir jedoch die Ergebnisse von 2013 nicht heller
darstellen.

35.1.3 Histogramme – hist()

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!

Tabelle 35.2: Ausgewählte Parameter der Funktion hist()

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

Beispiel: Ziehe 50 standardnormalverteilte Zufallszahlen und erstelle ein Histo-


gramm. Dekoriere das Histogramm und probiere mehrere Möglichkeiten dabei aus.

> # Zufallszahlen ziehen


> set.seed(111)
> x <- rnorm(50)

> # Histogramm zeichnen > # Histogramm zeichnen


> hist(x) > hist(x, freq = FALSE)

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.

> # Säulen einfärben > # Säulen schraffieren


> hist(x, freq = FALSE, > hist(x, freq = FALSE,
+ col = "lightblue") + density = 6, col = "black")

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.

> # Histogramm mit 4 Säulen > # Histogramm mit 30 Säulen


> hist(x, freq = FALSE, > hist(x, freq = FALSE,
+ breaks = 4) + breaks = 30)

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.

> # Histogramm zeichnen > # Histogramm zeichnen


> hist(x, col = "white") > hist(x, freq = FALSE, col = "white")

> # 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)

> # Dichte zeichnen > # Dichte zeichnen


> lines(xx, yy, col = "red") > lines(xx, yy, col = "red")

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.

35.1.4 Rückgabeobjekte von Grafikfunktionen verwerten

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)

> res.hist <- hist(x, plot = FALSE)


> res.hist
$breaks
[1] -4 -3 -2 -1 0 1 2 3
$counts
[1] 1 2 11 13 17 4 2
$density
[1] 0.02 0.04 0.22 0.26 0.34 0.08 0.04
$mids
[1] -3.5 -2.5 -1.5 -0.5 0.5 1.5 2.5
.....

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:

• $breaks: Die Trennstellen (Intervallsenden) auf der x-Achse


• $counts: Die absolute Anzahl der Beobachtungen, die in die einzelnen Inter-
valle fallen.
• $density: Die relative Anzahl der Beobachtungen, die in die einzelnen Inter-
valle fallen.
• $mids: Die Mittelpunkte der Intervalle

Schauen wir uns anhand eines Codebeispiels an, wie wir diese Informationen dazu
nützen, um beispielsweise bestimmte Säulen hervorzuheben.

> # Histogramm zeichnen > # 5. Säule schraffieren


> res.hist <- hist(x, col = "white") > x1 <- res.hist$breaks[5:6]
> y1 <- res.hist$counts[5]
> # Mittelpunkt und Höhe der
> # 3. Säule bestimmen > x2 <- c(x1, rev(x1))
> x1 <- res.hist$mids[3] > y2 <- c(0, 0, y1, y1)
> y1 <- res.hist$counts[3] > x2
[1] 0 1 1 0
> # Rote Linie in der 3. Säule > y2
> lines(c(x1, x1), c(0, y1), [1] 0 0 17 17
+ col = 2, lwd = 5) > polygon(x2, y2, density = 5)

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

35.1.5 Boxplots – boxplot()

Mittels boxplot() können wir Boxplots zeichnen. Auf eine Parameterauflistung


verzichten wir, stattdessen schauen wir uns direkt Beispiele an.
Dazu betrachten wir unseren Iris-Datensatz, den wir in (34) eingeführt haben.

> # Iris-Daten laden


> data(iris)
> head(iris)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5.0 3.6 1.4 0.2 setosa
6 5.4 3.9 1.7 0.4 setosa

Beispiel: Zeichne für die Variable Sepal.Length einen Boxplot.

> # Boxplot zeichnen > # Mit hellblauer Box


> boxplot(iris$Sepal.Length, > boxplot(iris$Sepal.Length,
+ main = "Boxplot von Sepal.Length", + main = "Boxplot von Sepal.Length",
+ ylab = "cm") + ylab = "cm", col = "lightblue")

Boxplot von Sepal.Length Boxplot von Sepal.Length


7.5

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.

> # Alle Sepal-Spalten selektieren


> iris.sepal <- iris[grepl("Sepal", names(iris))]
550 I Visualisierung von Daten

> # Boxplot zeichnen > # Mit unterschiedlichen Farben


> boxplot(iris.sepal, > boxplot(iris.sepal,
+ main = "Boxplots für iris", + main = "Boxplots für iris",
+ ylab = "cm") + ylab = "cm", col = c(2, 4))

Boxplots für iris Boxplots für iris


8

8
7

7
6

6
cm

cm
5

5
4

4
3

3
2

Sepal.Length Sepal.Width 2 Sepal.Length Sepal.Width

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.

Abgesehen von den Achsenbeschriftungen unterscheiden sich die Grafiken nicht.


35 Standardgrafiken und Farben 551

> # 1. Möglichkeit > # 3. Möglichkeit


> boxplot( > plot(
+ iris$Sepal.Width ~ iris$Species) + iris$Species, iris$Sepal.Width)
4.0

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

35.2 Die Welt der Farben

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.

35.2.1 RGB-Farben – rgb()

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:

rgb(red, green, blue, alpha, names = NULL, maxColorValue = 1)

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

> # Farben definieren und zeichnen


> col1 <- rgb( 0, 0, 0, max = 255) # Schwarz
> col2 <- rgb(127, 127, 127, max = 255) # Grau
> col3 <- rgb(255, 255, 255, max = 255) # Weiß
> col4 <- rgb(255, 0, 0, max = 255) # Rot
> col5 <- rgb(0 , 255, 255, max = 255) # Türkis (Mische Grün und Blau)
> col6 <- rgb(0 , 127, 127, max = 255) # dunkleres Türkis
> col7 <- rgb(255, 255, 0, max = 255) # Gelb (Mische Rot und Grün)
> col <- c(col1, col2, col3, col4, col5, col6, col7)

Für die Darstellung der Farben zweckentfremden wir das Balkendiagramm, indem
wir gleich hohe Balken ohne Abstand (space = 0) definieren.

> # Plotregion vorbereiten


> dev.new(width = 7, height = 1) # Neues Fenster der Breite 7 und Höhe 1
> par(mar = rep(0, 4)) # Ränder entfernen

> # Farben darstellen


> barplot(rep(1, length(col)), space = 0, col = col, axes = FALSE)

Der RGBA-Farbraum erweitert RGB um den Transparenzparameter alpha. Je


niedriger der Wert, desto durchsichtiger ist die Farbe. Transparenz kann jedoch nicht
in allen Grafiken korrekt interpretiert werden, daher mit Bedacht einsetzen!

35.2.2 HCL-Farben – hcl()

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:

• h (Hue): der Farbton (zwischen 0 und 360). Werte um 0 erzeugen rötliche,


Werte um 120 grüne und Werte um 240 blaue Farben.
• c (Chroma): der Grauanteil (zwischen 0 und 100). Je höher, desto kräftiger
und klarer ist die Farbe.
• l (Luminance): die Helligkeit der Farben (zwischen 0 und 100). Je höher,
desto heller die Farben

Wir schauen uns in Abb. 35.1 einige Konstellationen an.


35 Standardgrafiken und Farben 553

Erlangen wir ein erstes Gepür für den Farbton (Hue):

0 30 60 90 120 150 180 210 240 270 300 330

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

Abbildung 35.1: Darstellung des HCL-Farbraumes

Einen ähnlichen Farbraum beschreibt HSV (Hue, Saturation, Value) – siehe ?hsv.
554 I Visualisierung von Daten

35.2.3 Standardfarbpaletten – Palettes

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).

> # Plotregion vorbereiten


> dev.new(width = 7, height = 1) # Neues Fenster der Breite 7 und Höhe 1
> par(mar = rep(0, 4)) # Ränder entfernen

Wir ziehen jeweils 15 Farben aus jeder Palette.

> x <- rep(1, 15)


> n <- length(x)

> # Ohne Farbübergabe > barplot(x, space = 0, axes = FALSE,


> barplot(x, space = 0, axes = FALSE) + col = rainbow(n))

> 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

35.2.4 Eigene Farbpaletten – colorRamp(), colorRampPalette()

Mit der Funktion colorRampPalette() können wir sogar eigene Farbpaletten


kreieren, unserer Phantasie sind keine Grenzen gesetzt!

colorRampPalette(colors, ...)

Wir brauchen lediglich einige Wunschfarben an den Parameter colors zu übergeben.


colorRampPalette() gibt uns sodann eine Funktion zurück, mit deren Hilfe wir wie
bei den Standardfarbpaletten n Farben erzeugen können.
Die Funktion ruft intern die Funktion colorRamp() auf, die sehr vielseitig ist. Ein
Blick in die R-Hilfe (?colorRamp) lohnt sich!
Schauen wir uns Beispiele an! Wiederum zweckentfremden wir das Balkendiagramm
und erneut ziehen wir 15 Farben aus jeder Palette.

> # Plotregion vorbereiten


> dev.new(width = 7, height = 1) # Neues Fenster der Breite 7 und Höhe 1
> par(mar = rep(0, 4)) # Ränder entfernen

> x <- rep(1, 15)


> n <- length(x)

> # von Weiß bis Schwarz > # Blau - Weiß - Rot


> fun <- colorRampPalette( > fun <- colorRampPalette(
+ c("white", "black")) + c(4, "white", 2))

> barplot(x, space = 0, axes = FALSE, > barplot(x, space = 0, axes = FALSE,
+ col = fun(n)) + col = fun(n))

Derlei Farbpaletten sind besonders praktisch, wenn wir ordinalskalierte Variablen


nett darstellen wollen.
Beispiel: Wir führen eine fiktive Umfrage durch und befragen Personen zweier
Gruppen: „Wie wichtig ist Ihnen die Statistik?“ Die Antwortmöglichkeiten:
• nicht wichtig
• eher nicht wichtig
• teils/teils
• eher wichtig
• wichtig
556 I Visualisierung von Daten

> # Die Daten der fiktiven Umfrage


> M <- matrix(c(0:4, (1:5) + 1), ncol = 2) # absolute Häufigkeiten
> M <- prop.table(M, 2) # Spaltenprozente
> M
[,1] [,2]
[1,] 0.0 0.10
[2,] 0.1 0.15
[3,] 0.2 0.20
[4,] 0.3 0.25
[5,] 0.4 0.30

> # Zeilen und Spalten beschriften


> colnames(M) <- c("Gr. 1", "Gr. 2")
> rownames(M) <- c("nicht wichtig", "eher nicht wichtig", "teils/teils",
+ "eher wichtig", "wichtig")
> M
Gr. 1 Gr. 2
nicht wichtig 0.0 0.10
eher nicht wichtig 0.1 0.15
teils/teils 0.2 0.20
eher wichtig 0.3 0.25
wichtig 0.4 0.30

> # Eigene Farbpalette definieren:


> # Von Blau (nicht wichtig) über Weiß (teils/teils) bis Rot (wichtig)
> palette.fun <- colorRampPalette(c(4, "white", 2))

> # Grafik zeichnen


> barplot(M, col = palette.fun(nrow(M)), main = "Wichtigkeit der Statistik",
+ legend.text = rownames(M), xlim = c(0, 6))

Wichtigkeit der Statistik


1.0

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 ;-)

35.3 Aus der guten Praxis

35.3.1 Fallbeispiel: Farblich markierte Balkendiagramme

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.

> # Iris-Daten laden


> data(iris)

> # Farben für die Spezies aussuchen: Rot, Grün, Blau


> hue <- c(0, 120, 240)
> col.bar <- hcl(hue, 70, 70) # Farben für die Balken
> col.legend <- hcl(hue, 70, 50) # Farben für die Legende (dunkler!)

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.

> # Speichere Histogrammdaten


> hist.info <- hist(iris$Petal.Length, plot = FALSE)
> hist.info[1:3]
$breaks
[1] 1.0 1.5 2.0 2.5 3.0 3.5 4.0 4.5 5.0 5.5 6.0 6.5 7.0
$counts
[1] 37 13 0 1 4 11 21 21 17 16 5 4
$density
[1] 0.49333333 0.17333333 0.00000000 0.01333333 0.05333333 0.14666667
[7] 0.28000000 0.28000000 0.22666667 0.21333333 0.06666667 0.05333333

> class(hist.info)
[1] "histogram"
558 I Visualisierung von Daten

> # Farbloses Histogramm zeichnen. Interner Aufruf: plot.histogram()


> plot(hist.info)

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)
+ })

> # 2.) Ordne die counts zu einer Matrix an.


> mat <- t(sapply(temp, function(x) x$counts))
> mat
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10] [,11] [,12]
setosa 37 13 0 0 0 0 0 0 0 0 0 0
versicolor 0 0 0 1 4 11 20 13 1 0 0 0
virginica 0 0 0 0 0 0 1 8 16 16 5 4

> # Balkendiagramm zeichnen und Legende einzeichnen


> barplot(mat, col = col.bar, axes = FALSE, space = 0)
> legend("topright", legend = levels(iris$Species),
+ pch = 15, col = col.legend, text.col = col.legend, bty = "n")
35 Standardgrafiken und Farben 559

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.

> # Beschriftungen und y-Achse einzeichnen


> title(main = "Histogramm von Petal.Length", ylab = "Anzahl", xlab = "cm")
> axis(side = 2) # y-Achse einzeichnen
> axis(side = 1) # x-Achse einzeichnen

Histogramm von Petal.Length Histogramm von Petal.Length


35

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:

• Balkenbreite einstellen, sodass sie zu den Daten passt.

• x-Achse verschieben, sodass der erste Balken an der korrekten Stelle beginnt.

> # Breite der Balken bestimmen


> width <- hist.info$breaks[2] - hist.info$breaks[1]

> # Balkendiagramm zeichnen und Legende einzeichnen


> barplot(mat, width = width, col = col.bar, axes = FALSE, space = 0)
> legend("topright", legend = levels(iris$Species),
+ pch = 15, col = col.legend, text.col = col.legend, bty = "n")

> # Beschriftungen und y-Achse einzeichnen


> title(main = "Histogramm von Petal.Length", ylab = "Anzahl", xlab = "cm")
> axis(side = 2)

> # x-Achse einzeichnen


> xx <- pretty((0:ncol(mat)) * width)
> xx
[1] 0 1 2 3 4 5 6
> axis(side = 1, at = xx, labels = xx + hist.info$breaks[1])

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

35.4.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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.

a) Schreibe eine Funktion, die n ∈ N Folgen des gegebenen Prozesses mit


frei wählbarem T ∈ N möglichst effizient simuliert. Nimm dabei an, dass
n (deutlich) größer als T ist. Wähle eine geeignete Datenstruktur für das
Rückgabeobjekt.
Simuliere für die folgenden Aufgaben n = 106 Prozesse mit T = 100
Zeitschritten.
b) Berechne für jeden Zeitpunkt t ∈ {1, 2, . . . , T } möglichst effizient das 0%,
10%, 20%, ..., 100%-Quantil aller n Folgen.
c) Erstelle eine Grafik, in der man die Entwicklung der Quantile aus 5b)
nachvollziehen kann. Wähle dabei sinnvolle Farben (zum Beispiel aus ei-
ner eigenen Farbpalette) aus.

Als Inspiration für die Grafikerstellung:

Laufende Quantile des Prozesses Laufende Quantile des Prozesses


1e+06 Folgen 1e+06 Folgen
40
40

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

Zusatzaufgaben bzw. weitere Anforderungen:

• Ergänze die Simulationsfunktion um die Möglichkeiten, den Seed einzu-


stellen und auch andere Quantile zu berechnen.
• Erweitere die Funktion auf beliebige Verteilungen (zum Beispiel Gleich-
verteilung). Die entsprechende Zufallszahlenfunktion wird als Parameter
übergeben und den xt zugrunde gelegt. Was hat dieses Beispiel mit dem
Zentralen Grenzwertsatz zu tun?
• Ergänze die Grafik um eine Legende, welche uns über die Farben adäquat
aufklärt.
564 I Visualisierung von Daten

36 Grafikfenster und Layout

Dort wollen wir in diesem Kapitel hin:

• Grafiken abspeichern

• eigene Layouts erstellen

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).

36.1 Grafikfenster: Devices

Grafiken werden in Grafikfenstern, den sogenannten Devices, abgebildet (genauer


R-Graphics Devices). Wir sehen uns in den folgenden Abschnitten an, wie wir Gra-
fikfenster öffnen und schließen sowie das aktive Grafikfenster bestimmen.

36.1.1 Neues Device öffnen – dev.new()

Oftmals möchten wir gleichzeitig mehrere Grafiken betrachten und bearbei-


ten. Das können wir bewerkstelligen, indem wir mit Hilfe der Funktion dev.new()
ein neues Grafikfenster aufmachen. Die Höhe und Breite eines neuen Grafikfensters
beträgt standardmäßig 7 inches bzw. 17.78 cm.

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.

36.1.2 Aktives Device abfragen und setzen – dev.cur(), dev.set()

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.

36.1.3 Device schließen – dev.off(), graphics.off()

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().

36.2 Grafiken speichern

36.2.1 Rastergrafiken vs. Vektorgrafiken

Wir unterscheiden zwei grundlegende Grafiktypen: Rastergrafiken und Vektorgrafi-


ken. Bei Rastergrafiken wird das Bild in Bildpunkte unterteilt und jedem Punkt
ein Farbwert zugeordnet. Einige gängige Dateiformate dafür sind:

• 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.)

Frage: Was passiert, wenn wir Rastergrafiken und Vektorgrafiken skalieren?


Vektorgrafiken können wir beliebig skalieren, ohne die Bildqualität negativ zu
beeinträchtigen. Das Bild bleibt scharf, da bei der Rasterung die Bildpunkte aus
den geometrischen Formen neu berechnet werden und die Darstellung der Formen
somit auf die neue Bildgröße optimiert werden kann.
Bei Rastergrafiken hingegen müssen im Zuge der Skalierung neue Bildpunkte er-
zeugt bzw. „erfunden“ werden, was zu einer unscharfen Darstellung führt. Daher
sind für statistische Anwendungen Vektorgrafiken in aller Regel vorzuziehen!

Im kommenden Abschnitt demonstrieren wir den Unterschied zwischen Rastergrafi-


ken und Vektorgrafiken.

36.2.2 Speichern von Grafiken

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.

> # Plotregion vorbereiten


> dev.new(height = 0.5, width = 0.7) # Neues Grafikfenster
> par(mar = c(0, 0, 0, 0)) # keine Ränder

> # Rechte Grafik erstellen


> plot(0, 0, type = "n", axes = FALSE) # Leere Grafik
> text(0, 0, " Scharf mit \n Vektorgrafik ", cex = 1)
36 Grafikfenster und Layout 567

> # Grafik als PostScript (Vektorgrafik) abspeichern


> savePlot("Vektorgrafik", type = "ps")

> # Linke Grafik erstellen


> plot(0, 0, type = "n", axes = FALSE) # Leere Grafik
> text(0, 0, " Unscharf mit \n Rastergrafik ", cex = 1)

> # Grafik als png (Rastergrafik) abspeichern


> savePlot("Rastergrafik", type = "png")

In Abb. 36.1 stellen wir die eben erzeugte Rastergrafik und Vektorgrafik dar: einmal
in Originalgröße und einmal vergrößert.

Unscharf mit Scharf mit


Rastergrafik Vektorgrafik

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.

> # Alle Grafikdevices schließen


> graphics.off()

36.3 Fenstereinteilung und Layout

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.

36.3.1 Äußere Grafikränder – par()$mar, par()$mai

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.

par(mar = c(bottom, left, top, right))


568 I Visualisierung von Daten

Standardmäßig sind die Ränder (margins) wie folgt eingestellt:

par(mar = c(5, 4, 4, 2) + 0.1)

Die Abstände werden in Anzahl der Zeilen definiert, wobei eine Zeile 0.2 inches misst.

> # Standardeinstellungen > # Ränder umstellen


> dev.new() > par(mar = c(2, 2, 8, 0))
> plot(0:10, 0:10, main = "Titel") > plot(0:10, 0:10, main = "Titel")

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.

36.3.2 Einfache Fensterteilung – par()$mfrow

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.

par(mfrow = c(a, b))

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

> # Alle numerischen Spalten selektieren


> iris.num <- iris[sapply(iris, is.numeric)]
> head(iris.num, n = 3)
Sepal.Length Sepal.Width Petal.Length Petal.Width
1 5.1 3.5 1.4 0.2
2 4.9 3.0 1.4 0.2
3 4.7 3.2 1.3 0.2

> # Device vorbereiten


> dev.new(width = 12, height = 8)
> par(mfrow = c(2, length(iris.num)))

> # 1. Zeile: Histogramme


> for (i in 1:length(iris.num)) {
+ hist(iris.num[[i]], main = names(iris.num)[i], xlab = "cm")
+ }

> # 2. Zeile: Boxplots


> for (i in 1:length(iris.num)) {
+ boxplot(iris.num[[i]], main = names(iris.num)[i])
+ }

Mit mfrow = c(2, length(iris.num)) legen wir fest, dass das Device in 2 Zeilen
und 4 Spalten unterteilt wird.

Sepal.Length Sepal.Width Petal.Length Petal.Width


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

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

Sepal.Length Sepal.Width Petal.Length Petal.Width


8.0

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

> # Alle Devices schließen


> graphics.off()
570 I Visualisierung von Daten

36.3.3 Paarweise Streudiagramme – pairs()

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.

> # Alle numerischen Spalten selektieren


> iris.num <- iris[sapply(iris, is.numeric)]

> # Definiere Farben und Punktformen für jede Spezies


> col.roh <- c(2, 3, 4)
> pch.roh <- c(1, 2, 3)

> # Paarweise Streudiagramme zeichnen


> pairs(iris.num, main = "Anderson’s Iris Data",
+ col = col.roh[iris$Species], pch = pch.roh[iris$Species])

Anderson’s Iris Data


2.0 3.0 4.0 0.5 1.5 2.5
7.5
6.5

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

4.5 5.5 6.5 7.5 1 2 3 4 5 6 7

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.

Gerne darfst du diese Idee in die Tat umsetzen!

36.3.4 Eigene Layouts definieren – layout(), layout.show(), lcm()

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:

layout(mat, widths = rep(1, ncol(mat)),


heights = rep(1, nrow(mat)), respect = FALSE)

In Tab. 36.1 werfen wir einen Blick auf die Parameter.

Tabelle 36.1: Parameter der Funktion layout()

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.

> # Alle numerischen Spalten selektieren


> iris.num <- iris[sapply(iris, is.numeric)]
> k <- length(iris.num) # Anzahl der numerischen Variablen

> # Generiere ein Layout mit 2 Zeilen und k Spalten.


> M <- matrix(1:(2 * k), nrow = 2, byrow = TRUE)
> M
[,1] [,2] [,3] [,4]
[1,] 1 2 3 4
[2,] 5 6 7 8

> # Layout für M erstellen


> n <- layout(M) # Layout gibt die Anzahl der Bereiche zurück.
> n
[1] 8

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.

> # Zeige die ersten 2 Zellen an > # Zeige alle n Zellen an


> layout.show(2) > layout.show(n)

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

> # Zufallszahlengenerator konfigurieren


> RNGversion("4.0.2")
> set.seed(1)

> # Grafikfenster vorbereiten und Layout erstellen


> dev.new(width = 12, height = 8)
> n <- layout(M)

> # n Stichproben der Größe 100 einer Standardnormalverteilung


> for (i in 1:n) {
+ x <- rnorm(100)
+ hist(x, main = paste("Stichprobe", i))
+ }

Stichprobe 1 Stichprobe 2 Stichprobe 3 Stichprobe 4

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

Stichprobe 5 Stichprobe 6 Stichprobe 7 Stichprobe 8


20
35

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.

> # 1. Zeile: Histogramme


> for (i in 1:length(iris.num)) {
+ hist(iris.num[[i]], main = names(iris.num)[i], xlab = "cm")
+ }

> # 2. Zeile: Boxplots


> for (i in 1:length(iris.num)) {
+ boxplot(iris.num[[i]], main = names(iris.num)[i])
+ }
574 I Visualisierung von Daten

Sepal.Length Sepal.Width Petal.Length Petal.Width

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

Sepal.Length Sepal.Width Petal.Length Petal.Width


8.0

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.

> M <- matrix(c(1, 1, 2, 4, 3, 4), > M <- matrix(c(1, 1, 2, 0, 3, 0),


+ ncol = 3) + ncol = 3)
> M > M
[,1] [,2] [,3] [,1] [,2] [,3]
[1,] 1 2 3 [1,] 1 2 3
[2,] 1 4 4 [2,] 1 0 0

> n <- layout(M) > n <- layout(M)


> layout.show(n) > layout.show(n)

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

Wenden wir uns den Höhen- und Breiteneinstellungen zu.

> M <- matrix(1:9, ncol = 3)


> M
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9

> n <- layout(M, > n <- layout(M,


+ widths = c(1, 2, 4)) + widths = c(1, 2, 4),
+ heights = c(1, 2, 4))
> layout.show(n) > layout.show(n)

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.

> n <- layout(M, > n <- layout(M,


+ widths = lcm(c(2, 3, 4)), + widths = c(1, "2 cm", 2),
+ heights = c(1, 2, 4)) + heights = c(1, "3 cm", 2))
> layout.show(n) > layout.show(n)

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.

36.4 Aus der guten Praxis

36.4.1 Fallbeispiel: Streudiagramme mit Randverteilungen – density(),


substitute()

Jetzt haben wir das Rüstzeug, um unser Iris-Streudiagramm mit Randverteilungen


zu erstellen! Am Ende dieses Abschnitts sehen wir folgende Grafiken:

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

Setzen wir unseren Plan in die Tat um!

> # Spalten des Iris-Datensatzes global verfügbar machen


> attach(iris)

> # Farben aussuchen: Rot, Grün, Blau


> hue <- c(0, 120, 240)
> col.hell <- hcl(hue, 70, 70)
> col.dunkel <- hcl(hue, 70, 50)

> # Layout definieren


> dev.new()
> n <- layout(matrix(c(2, 1, 4, 3), ncol = 2),
+ widths = c(5, 2), heights = c(2, 5))
> layout.show(n)

> # Streudiagramm zeichnen (Position 1)


> par(mar = c(4, 4, 1, 1)) # Äußere Grafikränder einstellen
> par(mgp = c(2, .5, 0)) # Achsentitel und Achsenlabels näher rücken

> plot(Petal.Length, Petal.Width, col = col.dunkel[Species], las = 1)

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.

> # Obere Randverteilung zeichnen (Position 2)


> par(mar = c(0, 4, 1, 1))

> h <- hist(Petal.Length, plot = FALSE)


> barplot(h$density, axes = FALSE, space = 0)

> # Rechte Randverteilung zeichnen (Position 3)


> par(mar = c(4, 0, 1, 1))

> h <- hist(Petal.Width, plot = FALSE)


> barplot(h$density, horiz = TRUE, axes = FALSE, space = 0)

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.

> # Legende einzeichnen (Position 4)


> par(mar = c(0, 0, 1, 1))
> plot(0, 0, type = "n", axes = FALSE)

> # Legende zentriert einzeichnen


> legend(x = "center", legend = levels(Species), pch = 1,
+ col = col.dunkel, text.col = col.dunkel, cex = 1, bty = "n")

> # iris wieder einpacken


> detach(iris)

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.

> zeichne.bar <- function(x, gruppe, horiz = FALSE, col) {


+ # Hilfsfunktion für das Histogramm. Falls gruppe (Klasse factor)
+ # spezifiziert wurde, so wird die Gruppe farblich gemäß col markiert.
+
+ h <- hist(x, plot = FALSE)
+
+ if (missing(gruppe)) {
+ # Einfärbiges Balkendiagramm
+ barplot(h$density, axes = FALSE, horiz = horiz, space = 0)
+ }
+ else {
+ # Mehrfärbiges Balkendiagramm
+ temp <- tapply(x, gruppe, function(u) {
+ hist(u, breaks = h$breaks, plot = FALSE)
+ })
+
+ mat <- t(sapply(temp, function(y) y$counts))
+ barplot(mat, col = col, horiz = horiz, axes = FALSE, space = 0)
+ }
+ }

Ü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.

> graphics.off() # Alle Devices schließen.

> # einfärbig, horizontale Balken > # mehrfärbig, vertikale Balken


> zeichne.bar(iris$Petal.Length, > zeichne.bar(iris$Petal.Length,
+ horiz = TRUE) + gruppe = iris$Species, col = 2:4)
580 I Visualisierung von Daten

Passt! Nun zur zweiten Hilfsfunktion zeichne.dichte() für die Dichtegrafiken.

> 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.

> graphics.off() # Alle Devices schließen.

> # Normalverteilung, vertikal > # Kerndichten, horizontal


> zeichne.dichte(iris$Petal.Length, > zeichne.dichte(iris$Petal.Length,
+ gruppe = iris$Species, + gruppe = iris$Species,
+ horiz = FALSE, kernel = FALSE, + horiz = TRUE, kernel = TRUE,
+ col = 2:4) + col = 2:4)

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.

> plot.auswahl <- function(x, type, gruppe, col, horiz = FALSE) {


+ # Hilfsfunktion, welche die zum type passende Zeichenfunktion aufruft.
+ switch(type,
+ bar = zeichne.bar(x, horiz = horiz),
+ bar.col = zeichne.bar(x, gruppe, horiz, col = col),
+ normal = zeichne.dichte(x, gruppe, horiz, kernel = FALSE, col),
+ kernel = zeichne.dichte(x, gruppe, horiz, kernel = TRUE, col),
+ {
+ warning("type unbekannt, zeichne einfaerbige Balken!");
+ zeichne.bar(x, horiz = horiz)
+ }
+ )
+ }

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

> plot.streu <- function(x, y, gruppe, type = "bar", hue, pch) {


+ # Zeichnet ein Streudiagramm mit diversen Randverteilungen
+ # x, y ..... x und y-Variable
+ # gruppe ... Objekt der Klasse factor
+ # type ..... "bar" (default): Einfärbiges Balkendiagramm
+ # "bar.col": Balkendiagramm; gruppe wird eingefärbt
+ # "normal": Normalverteilungsdichten
+ # "kernel": Kerndichten
+ # hue ...... Farbton aus dem HCL-Farbraum
+ # pch ...... pch-Werte für die Gruppen
+
+ xlab <- substitute(x) # Name für die x-Achse extrahieren
+ ylab <- substitute(y) # Name für die y-Achse extrahieren
+
+ # Farben definieren
+ col1 <- hcl(hue, 70, 70) # hell (Balken, Dichten)
+ col2 <- hcl(hue, 70, 50) # dunkel (Legenden, Punkte)
+
+ # Layout definieren
+ dev.new()
+ n <- layout(matrix(c(2, 1, 4, 3), ncol = 2),
+ widths = c(5, 2), heights = c(2, 5))
+ layout.show(n)
+
+ # Streudiagramm zeichnen (Position 1)
+ par(mar = c(4, 4, 1, 1)) # Äußere Grafikränder einstellen.
+ par(mgp = c(2, .5, 0)) # Abstand der Achsenbeschriftung.
+ plot(x, y, col = col2[gruppe], las = 1, pch = pch[gruppe],
+ xlab = xlab, ylab = ylab)
+
+ # Obere Randverteilung zeichnen (Position 2)
+ par(mar = c(0, 4, 1, 1))
+ plot.auswahl(x, type, gruppe, col1, horiz = FALSE)
+
+ # Rechte Randverteilung zeichnen (Position 3)
+ par(mar = c(4, 0, 1, 1))
+ plot.auswahl(y, type, gruppe, col1, horiz = TRUE)
+
+ # Legende zentriert einzeichnen (Position 4)
+ par(mar = c(0, 0, 1, 1))
+ plot(0, 0, type = "n", axes = FALSE)
+ legend(x = "center", legend = levels(gruppe), pch = pch,
+ col = col2, text.col = col2, cex = 1, bty = "n")
+ }

Abbildung 36.2: Funktion für Streudiagrammen mit diversen Randverteilungen


36 Grafikfenster und Layout 583

Einfärbiges Balkendiagramm Mehrfärbiges Balkendiagramm


> plot.streu(Petal.Length, > plot.streu(Petal.Length,
+ Petal.Width, Species, + Petal.Width, Species,
+ type = "bar", + type = "bar.col",
+ 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

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

Abbildung 36.3: Streudiagramme mit diversen Randverteilungen. Vor der Code-


ausführung müssen attach(iris), hue <- c(0, 120, 240)
und pch <- 1:3 ausgeführt werden. Am Ende iris wieder mit
detach(iris) einpacken.
584 I Visualisierung von Daten

Wir klären kurz, warum im Streudiagramm die Variablennamen Petal.Length und


Petal.Width verwendet werden. Mit Hilfe von substitute() können wir den ei-
nem Objekt zugrunde liegenden „rohen Ausdruck“ extrahieren. Wenn wir etwa x =
Petal.Length setzen, so ist Petal.Length der rohe (unausgewertete) Ausdruck von
x. Dieser wird mit substitute(x) entnommen. Das funktioniert solange, bis wir das
Objekt x in der Funktion nicht überschreiben. Ein kleines Codebeispiel dazu.

> fun <- function(x) { > # Aufruf mit rohem Ausdruck 3 * 4


+ print(substitute(x)) > fun(3 * 4)
+ x <- 5 3 * 4
+ print(substitute(x)) [1] 5
+ }

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:

• Die Grafik könnte eine Hauptüberschrift haben.


Zur Umsetzung können wir zum Beispiel das Layout umdefinieren:
> n <- layout(matrix(c(5, 2, 1, 5, 4, 3), ncol = 2),
+ widths = c(5, 2), heights = c(lcm(k), 2, 5))

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

36.5.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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 ;-)

2. Gegeben ist eine Funktion f : R2 7→ R mit f (x, y) = sin(x) + cos(y). Wir


wollen diese Funktion grafisch darstellen.

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

−0.5 −0.5 1.5


0.5 1 1 −1
0.5 .5
−1.5 −1.5 −0
0

−1.5
−1
0.5
−1
0.5 0.5 −1
−5

−0.5
−5

1.5 1.5 1.5


1

−0.5 −0.5 −0.5


0.5 1 1 1
−1 −1.5 −1 −1.5 −1 −1.5
−1
−0.5
0

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.

a) Führe eine Internetrecherche zu parallele Koordinaten durch. Was ist


die Idee hinter dieser Visualisierungstechnik?
b) Finde heraus, wie du in R Daten mit parallelen Koordinaten visualisieren
kannst und zeichne eine derartige Grafik mit allen numerischen Variablen
von iris.
c) Schreibe eine eigene Funktion, welche beliebige numerische Daten mit par-
allelen Koordinaten visualisiert. Mache dir unter anderem über folgende
Punkte Gedanken:
• Wie bilde ich jede Variable auf das Minimum und Maximum ab?
• Wie kann ich die Linien einzeichnen?
• Wie kann ich die Variablennamen bzw. die Werte sinnvoll und gut
lesbar einzeichnen?
• Wie kann ich die Spezies (bzw. allgemein die Gruppe) sinnvoll ein-
zeichnen?

4. Sei X ∼ exp(λ) eine exponentialverteilte Zufallsvariable. Die Verteilungsfunk-


tion von X ist gegeben durch:

F (x) = 1 − e−λx

a) Visualisiere die Verteilungsfunktion der Exponentialverteilung für drei


Werte für λ deiner Wahl. Wähle einen sinnvollen Wertebereich für die x-
Achse. Die drei Kurven sollen sich gut voneinander unterscheiden. Zeichne
auch ein Gitternetz hinter die Verteilungskurven ein.
b) Zeichne nun eine Legende ein, die uns über die konkreten Werte für λ
aufklärt. Der Legendentext soll dabei folgende Form haben: λ = 0.5,
λ = 1, λ = 2.

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

Die Grafik könnte am Ende so aussehen:

Verteilungsfunktion der Exponentialverteilung

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

37 Verteilungstests und Hypothesentests

Dort wollen wir in diesem Kapitel hin:

• einen Überblick zu statistischen Hypothesentests in R gewinnen

• Hypothesentests an konkreten Daten durchführen


• die Ergebnisse der Tests weiterverarbeiten

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:

> # Anzahlen und Zufallszahlengenerator einstellen


> m <- 20 # Bildungsstufen in Blöcken der Größe m
> n <- m * 20 # Gesamtanzahl der Personen
> RNGversion("4.0.2")
> set.seed(4)

> # Datensatz generieren


> inc <- data.frame(geschlecht = factor(rep(0:1, each = m * 10),
+ labels = c("weiblich", "männlich")))
> inc$bildung <- factor(c(rep(0:2, c(2 * m, 5 * m, 3 * m)),
+ rep(0:2, c(3 * m, 6 * m, 1 * m))),
+ labels = c("niedrig", "mittel", "hoch"))
> inc$alter <- sample(20:64, size = n, replace = TRUE)
> inc$einkommen <- 10^(-0.25 * (inc$geschlecht == "männlich") +
+ 0.005 * inc$alter +
+ 0.3 * (inc$bildung == "hoch") +
+ 0.007 * inc$alter * (inc$geschlecht == "männlich") +
+ rnorm(n, sd = .2) + 3)
> inc$einkommen_vorjahr <- 10^(log10(inc$einkommen) +
+ rnorm(n, mean = -.05, sd = .4))
© 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_37
37 Verteilungstests und Hypothesentests 591

> 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 ...

Folgende Fragen wollen wir in diesem Kapitel behandeln:

• 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)

Aus programmiertechnischer Sicht begleiten uns stets folgende Fragen:

• 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?

In (37.2.2) erläutern wir, wie das Rückgabeobjekt von Hypothesentests aufgebaut


ist und welche Informationen es bereitstellt. Im Zuge von (37.2.1) erläutern wir
kurz, wie wir zweiseitig bzw. einseitig testen können. In (37.3.2) bzw. (37.4) be-
sprechen wir den t-Test bzw. Wilcoxontest, und zwar jeweils für eine Stichprobe,
zwei abhängige/gepaarte Stichproben und zwei unabhängige Stichproben. Weitere
häufig angewendete Testverfahren für kategorielle bzw. metrische Merkmale führen
wir kurz in (37.2.6) bzw. (37.5) an, ehe wir schließlich in (37.6) eine Übersicht über
gängige Testverfahren geben.
Wir gehen grundsätzlich von einem Signifikanzniveau von α = 0.05 bzw. von einem
Konfidenzlevel von 1 − α = 0.95 aus.
592 J Data Science und Statistik in der Praxis

37.1 Verteilungstests

Einkommensverteilungen sind in der Regel rechtsschief, da in den meisten Grundge-


samtheiten wenige Personen ein hohes Einkommen erzielen. Im vorliegenden Daten-
satz folgt das Einkommen einer Log-Normalverteilung und somit sind die logarith-
mierten Daten normalverteilt.

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.

37.1.1 Histogramme mit Anpassungslinie – hist()

Für die Log-Normalverteilungsdichte müssen Mittelwert und Standardabweichung


der entsprechenden Dichtefunktion dlnorm() geschätzt werden. Dazu logarithmieren
wir das Einkommen und berechnen anschließend das arithmetische Mittel und die
Standardabweichung. Alternativ steht auch die Funktion fitdistr() zur Schätzung
von Verteilungsparametern zur Verfügung.
Bereits in (35.1.3) haben wir besprochen, wie wir mit hist() ein Histogramm
erstellen und Dichtefunktionen einzeichnen. In Abb. 37.1 sehen wir, dass das
Einkommen gut mit einer Log-Normalverteilung beschrieben werden kann.

> x <- seq(0, max(inc$einkommen), by = 1)


> y <- dlnorm(x, mean(log(inc$einkommen)), sd(log(inc$einkommen)))
> hist(inc$einkommen, freq = FALSE, yaxt = "n", main = "", ylab = "",
+ xlab = "Einkommen", ylim = c(0, max(y)))
> lines(x, y, col = 2)

0 2000 4000 6000 8000 10000


Einkommen

Abbildung 37.1: Histogramm für das Einkommen mit Log-Normalverteilung


37 Verteilungstests und Hypothesentests 593

Infobox 14: Mittelwert und Logarithmieren

Beachte beim Logarithmieren einer Variable, dass das arithmetische Mittel


der entlogarithmierten Variable nicht dem rücktransformierten Mittelwert der
logarithmierten Variable entspricht.

> mean(inc$einkommen)
[1] 2308.547
> exp(mean(log(inc$einkommen)))
[1] 1933.931

Für den rücktransformierten Mittelwert der logarithmierten Daten gibt es zwei


Interpretationen. Zum einen entspricht dieser Wert exakt dem geometrischen
Mittel der entlogarithmierten Variable, siehe Funktion geometric.mean() aus
dem Paket psych.

> library(psych)
> psych::geometric.mean(inc$einkommen)
[1] 1933.931

Sofern das Logarithmieren zu einer annähernd symmetrischen Verteilung führt,


entspricht zum anderen der rücktransformierte Mittelwert der logarithmierten
Daten in etwa dem Median der entlogarithmierten Daten (sowie der logarith-
mierten Daten, da eine monotone Transformation keine Auswirkung auf die
Reihenfolge der Beobachtungen und somit auf den Median hat).

> median(inc$einkommen)
[1] 1954.257

37.1.2 Quantil-Quantil-Plot – qqplot(), qqline(), qqPlot()

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

> # QQ-Plot und QQ-Linie durch 1. und 3. Quartil zeichnen


> qqplot(qlnorm(ppoints(nrow(inc)), mean = mean(log(inc$einkommen)),
+ sd = sd(log(inc$einkommen))), inc$einkommen,
+ xlab = "theoretisches Quantil der Log-Normalverteilung",
+ ylab = "Einkommen")
> qqline(inc$einkommen, distribution = function(p)
+ qlnorm(p, mean(log(inc$einkommen)), sd(log(inc$einkommen))), col = 2)

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

0 2000 4000 6000 8000 10000


theoretisches Quantil der Log−Normalverteilung

Abbildung 37.2: QQ-Plot für das Einkommen mit Log-Normalverteilung

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)

> car::qqPlot(inc$einkommen, "lnorm", mean = mean(log(inc$einkommen)),


+ sd = sd(log(inc$einkommen)), ylab = "Einkommen",
+ xlab = "theoretisches Quantil der Log-Normalverteilung",
+ col.lines = 2, lwd = 1)
[1] 381 279

Analog können wir überprüfen, ob die logarithmierten Daten einer Normalverteilung


folgen, siehe rechte Grafik in Abb. 37.3.

> car::qqPlot(log(inc$einkommen), "norm", ylab = "logarithmiertes Einkommen",


+ col.lines = 2, lwd = 1,
+ xlab = "theoretisches Quantil der Normalverteilung")
[1] 381 256
37 Verteilungstests und Hypothesentests 595

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

0 2000 4000 6000 8000 10000 −3 −2 −1 0 1 2 3


theoretisches Quantil der Log−Normalverteilung theoretisches Quantil der Normalverteilung

Abbildung 37.3: QQ-Plot für das Einkommen mit 95%-Konfidenzintervallen (Pa-


ket car). Links: originale Einkommensdaten. Rechts: logarith-
miertes Einkommen.

37.1.3 Kolmogorov-Smirnov-Test – ks.test(), LcKS()

Es existieren auch statistische Tests zur Überprüfung der Verteilungsanpassung, ins-


besondere der Kolmogorov-Smirnov-Test. Die Hypothesen des Tests lauten:
H0 : Die Daten stimmen mit einer bestimmten Verteilung überein.
H1 : Die Daten stimmen nicht mit einer bestimmten Verteilung überein.
Beachte: Entsprechend der Theorie des Hypothesentestens ist eine Beibehaltung
der Nullhypothese kein Hinweis darauf, dass diese zutrifft. Insbesondere bei kleinen
Stichproben wird die Nullhypothese, dass die Daten einer bestimmten Verteilung
folgen, kaum abgelehnt. Deshalb wird ein Verteilungstest immer nur in Kombination
mit einer grafischen Überprüfung empfohlen.
Für den Kolmogorov-Smirnov-Test existieren in R zwei Versionen: In der Standard-
funktion ks.test() werden die Verteilungsparameter als gegeben angenommen.
Im realistischeren Szenario, in dem die Parameter der Verteilung aus den Daten
geschätzt werden, wird die korrigierte Version LcKS() (Lilliefors-corrected) aus
dem Paket KScorrect empfohlen. In beiden Fällen können wir den Namen der Ver-
teilungsfunktion "plnorm" als String übergeben.

> # 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

> # Korrigierte Version


> library(KScorrect)

> KScorrect::LcKS(inc$einkommen, "plnorm", nreps = 1000)$p.value


[1] 0.2767233

In unserem Fall ergeben beide Methoden einen nicht signifikanten p-Wert, die Null-
hypothese wird also beibehalten.

37.2 Hypothesentests für kategorielle Merkmale

37.2.1 Binomialtest – binom.test()

Mit dem Binomialtest wollen wir folgende Hypothesen testen:


H0 : Der Anteil der Frauen, die mehr als 2500 Euro verdienen, gleicht 50%.
(p = 0.5)
H1 : Der Anteil der Frauen, die mehr als 2500 Euro verdienen, gleicht nicht 50%.
(p 6= 0.5)
Dabei steht p für den wahren Anteil der Frauen, die mehr als 2500 Euro verdienen.
In R steht dafür die Funktion binom.test() zur Verfügung:

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.

Tabelle 37.1: Parameter der Funktion binom.test()

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

> # Binomialtest durchführen


> inc$weiblich <- inc$geschlecht == "weiblich"
> binom.test(x = sum(inc$einkommen[inc$weiblich] > 2500),
+ n = sum(inc$weiblich), p = 0.5)
Exact binomial test
data: sum(inc$einkommen[inc$weiblich] > 2500) and sum(inc$weiblich)
number of successes = 59, number of trials = 200, p-value =
6.308e-09
alternative hypothesis: true probability of success is not equal to 0.5
95 percent confidence interval:
0.2327688 0.3634231
sample estimates:
probability of success
0.295

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%].

37.2.2 Rückgabeobjekt von Testfunktionen – htest

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.

37.2.3 Tests für Anteilswerte – prop.test()

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

Als Beispiel für einen 2-Stichprobentest untersuchen wir folgende Hypothesen:

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.

> einkommen_gt_2500 <- tapply(inc$einkommen > 2500, inc$geschlecht, sum)


> einkommen_gt_2500
weiblich männlich
59 75

> # Anteilstest durchführen


> prop.test(x = einkommen_gt_2500,
+ n = table(inc$geschlecht), correct = FALSE, alternative = "less")
2-sample test for equality of proportions without continuity
correction
data: einkommen_gt_2500 out of table(inc$geschlecht)
X-squared = 2.8729, df = 1, p-value = 0.04504
alternative hypothesis: less
95 percent confidence interval:
-1.000000000 -0.002643774
sample estimates:
prop 1 prop 2
0.295 0.375

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.

37.2.4 McNemar-Test – mcnemar.test()

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.

> # 1.) 2 x 2 Tabelle bilden


> einkommen_vgl <- table(inc$einkommen_vorjahr > 2500, inc$einkommen > 2500)
> dimnames(einkommen_vgl) <- list("Einkommen Vorjahr" = c("<=2500", ">2500"),
+ "Einkommen aktuell" = c("<=2500", ">2500"))
> einkommen_vgl
Einkommen aktuell
Einkommen Vorjahr <=2500 >2500
<=2500 198 51
>2500 68 83

> # 2.) McNemar-Test durchführen


> mcnemar.test(x = einkommen_vgl)
McNemar’s Chi-squared test with continuity correction
data: einkommen_vgl
McNemar’s chi-squared = 2.1513, df = 1, p-value = 0.1425

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.

37.2.5 χ2 -Test – chisq.test()

Der Pearson χ2 -Test untersucht, ob die beobachteten Häufigkeiten einer beliebig


großen, zweidimensionalen Kreuztabelle signifikant von den unter Unabhängigkeit zu
erwarteten Häufigkeiten abweichen. Die erwartete Häufigkeit jeder Zelle der Kreuz-
tabelle errechnet sich durch Multiplizieren der jeweiligen Zeilensumme mit der Spal-
tensumme und anschließender Division durch die Gesamtsumme.
Im Spezialfall von zwei dichotomen Merkmalen, d. h. einer 2 × 2 Kreuztabelle, ent-
spricht der χ2 -Test dem Test für Anteilswerte, wobei der χ2 -Test im Gegensatz zum
Test für Anteilswerte nur zweiseitige Hypothesen erlaubt.

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

> # Chi-Quadrat-Test durchführen


> res <- chisq.test(x = inc$bildung, y = inc$einkommen > 2500)

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.

> res$observed > res$expected


inc$einkommen > 2500 inc$einkommen > 2500
inc$bildung FALSE TRUE inc$bildung FALSE TRUE
niedrig 72 28 niedrig 66.5 33.5
mittel 170 50 mittel 146.3 73.7
hoch 24 56 hoch 53.2 26.8

> res$residuals > res$stdres


inc$einkommen > 2500 inc$einkommen > 2500
inc$bildung FALSE TRUE inc$bildung FALSE TRUE
niedrig 0.6744533 -0.9502553 niedrig 1.345547 -1.345547
mittel 1.9594139 -2.7606707 mittel 5.046584 -5.046584
hoch -4.0033820 5.6404721 hoch -7.733207 7.733207

37.2.6 Weitere Tests für kategorielle Merkmale

Zwei weitere relevante Tests für kategorielle Merkmale sind:

• 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

37.3 Hypothesentests für metrische Merkmale

37.3.1 Test für Korrelationskoeffizienten – cor.test()

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

> car::dataEllipse(x = log(as.matrix(


+ inc[, c("einkommen_vorjahr", "einkommen")])),
+ xlab = "log(einkommen_vorjahr)", ylab = "log(einkommen)",
+ level = .95) # mit logarithmierten Daten
10000

9.0
8.5
8000

log(einkommen)
8.0
einkommen
6000

7.5
4000

7.0
2000

6.5
6.0
0

0 10000 20000 30000 4 5 6 7 8 9 10


einkommen_vorjahr log(einkommen_vorjahr)

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().

> cor.test(log(inc$einkommen), log(inc$einkommen_vorjahr))


Pearson’s product-moment correlation
data: log(inc$einkommen) and log(inc$einkommen_vorjahr)
t = 11.513, df = 398, p-value < 2.2e-16
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
0.4224901 0.5699541
sample estimates:
cor
0.4998357

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.

> cor.test(inc$einkommen, inc$einkommen_vorjahr, method = "spearman")


Spearman’s rank correlation rho
data: inc$einkommen and inc$einkommen_vorjahr
S = 5709094, p-value < 2.2e-16
alternative hypothesis: true rho is not equal to 0
sample estimates:
rho
0.4647691

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.

37.3.2 t-Test für eine und zwei Stichproben – t.test()

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().

> # t-Test für eine Stichprobe rechnen


> t.test(inc$einkommen, mu = 2500)
One Sample t-test
data: inc$einkommen
t = -2.5919, df = 399, p-value = 0.009895
alternative hypothesis: true mean is not equal to 2500
95 percent confidence interval:
2163.333 2453.761
sample estimates:
mean of x
2308.547

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.

> # t-Test für zwei gepaarte Stichproben rechnen


> t.test(inc$einkommen, inc$einkommen_vorjahr, paired = TRUE)
Paired t-test
data: inc$einkommen and inc$einkommen_vorjahr
t = -4.1941, df = 399, p-value = 3.378e-05
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-1068.1724 -386.3728
sample estimates:
mean of the differences
-727.2726

> t.test(log(inc$einkommen), log(inc$einkommen_vorjahr), paired = TRUE)


Paired t-test
data: log(inc$einkommen) and log(inc$einkommen_vorjahr)
t = 1.4354, df = 399, p-value = 0.1519
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-0.02394253 0.15350890
sample estimates:
mean of the differences
0.06478318
604 J Data Science und Statistik in der Praxis

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.

> # t-Test für zwei unabhängige Stichproben rechnen


> inc$weiblich <- inc$geschlecht == "weiblich"
> t.test(x = inc$einkommen[inc$weiblich], y = inc$einkommen[!inc$weiblich])
Welch Two Sample t-test
data: inc$einkommen[inc$weiblich] and inc$einkommen[!inc$weiblich]
t = -1.8481, df = 362.33, p-value = 0.06541
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-561.83551 17.44951
sample estimates:
mean of x mean of y
2172.451 2444.644

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.

> t.test(einkommen ~ geschlecht, data = inc) # t.test.formula()


Welch Two Sample t-test
data: einkommen by geschlecht
t = -1.8481, df = 362.33, p-value = 0.06541
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-561.83551 17.44951
sample estimates:
mean in group weiblich mean in group männlich
2172.451 2444.644

37.4 Nichtparametrische Hypothesentests – wilcox.test()

Als nichtparametrische Alternative zum t-Test steht uns der Wilcoxon-Vorzeichen-


Rang-Test mit der Funktion wilcox.test() zur Verfügung. Er testet im Einstich-
probenfall, ob die Verteilung symmetrisch um den Referenzwert liegt. Im Fall ge-
paarter Stichproben wird zuerst die Differenz für jedes Paar gebildet.
37 Verteilungstests und Hypothesentests 605

Anhand des Datensatzes untersuchen wir, ob sich das Einkommen symmetrisch um


2500 Euro verteilt bzw. ob sich die Differenz zwischen dem aktuellen und dem Vor-
jahreseinkommen symmetrisch um 0 verteilt.

> # Wilcoxon-Test für eine Stichprobe rechnen


> wilcox.test(inc$einkommen, mu = 2500)
Wilcoxon signed rank test with continuity correction
data: inc$einkommen
V = 27272, p-value = 2.955e-08
alternative hypothesis: true location is not equal to 2500

> # Wilcoxon-Test für zwei gepaarte Stichproben rechnen


> wilcox.test(inc$einkommen, inc$einkommen_vorjahr, paired = TRUE)
Wilcoxon signed rank test with continuity correction
data: inc$einkommen and inc$einkommen_vorjahr
V = 36502, p-value = 0.12
alternative hypothesis: true location shift is not equal to 0

Der Wilcoxon-Rangsummen-Test für zwei unabhängige Stichproben, in diesem


Fall auch Mann-Whitney-U-Test genannt, untersucht, ob die Verteilungen der beiden
Zufallsvariablen ident oder in ihrer Lage verschoben sind. Der Test geht dabei von
identen Varianzen der beiden Stichproben aus.

> # Mann-Whitney-U-Test rechnen


> inc$weiblich <- inc$geschlecht == "weiblich"
> wilcox.test(x = inc$einkommen[inc$weiblich], y = inc$einkommen[!inc$weiblich])
Wilcoxon rank sum test with continuity correction
data: inc$einkommen[inc$weiblich] and inc$einkommen[!inc$weiblich]
W = 19049, p-value = 0.411
alternative hypothesis: true location shift is not equal to 0

Auch für wilcox.test() existiert eine formula-Methode (vgl. (37.3.2)).

> wilcox.test(einkommen ~ geschlecht, data = inc) # wilcox.test.formula()


Wilcoxon rank sum test with continuity correction
data: einkommen by geschlecht
W = 19049, p-value = 0.411
alternative hypothesis: true location shift is not equal to 0

So wie alle Standardfunktionen für Hypothesentests gibt uns wilcox.test() eine


Liste der Klasse htest zurück.

37.5 Weitere Tests für metrische Merkmale

• 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

37.6 Überblick über wichtige Hypothesentests in R

Tab. 37.2 enthält eine Übersicht mit den wichtigsten Hypothesentests in R.

Tabelle 37.2: Hypothesentests in R

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

37.7.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• 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

1. Im Einkommensdatensatz ist der Effekt des Geschlechts mit -0.25 festgelegt. In


(37.3.2) untersuchen wir den Geschlechterunterschied im Einkommen mittels
t-Test und erhalten dabei einen p-Wert von 0.065. Verändere den Effekt für
Geschlecht in den simulierten Einkommensdaten. Für welche Wertebereiche
ergeben sich p-Werte von < 0.05?
2. Untersuche den Zusammenhang zwischen p-Wert und Konfidenzintervall an-
hand eines zweiseitigen Ein-Stichproben-t-Tests mit Referenzwert 0 für 1000
Realisierungen eines standardnormalverteilten Zufallsvektors der Größe 100.
Welche p-Werte erhalten wir, wenn das 95%-Konfidenzintervall den Wert 0
nicht überlappt?
3. Vergleiche die Ergebnisse für parametrische und nichtparametrische Tests an-
hand simulierter Daten mit Ausreißern. Erstelle dazu 1000 Mal zwei unabhän-
gige Stichproben. Die beiden Stichproben sollen zum einen Teil aus Realisie-
rungen zweier normalverteilter Zufallsvariablen mit unterschiedlichen Erwar-
tungswerten µ1 und µ2 aber der gleichen Standardabweichung bestehen, zum
anderen Teil aus Ausreißern. Diese folgen ebenfalls Normalverteilungen mit den
Erwartungswerten µ1 und µ2 , aber mit einer größeren Standardabweichung.
Vergleiche die p-Werte für die beiden Tests für unterschiedliche Effektgrößen
µ1 - µ2 , Anzahl an Ausreißern und Standardabweichungen der Ausreißer.
608 J Data Science und Statistik in der Praxis

38 Statistische Modelle

Dort wollen wir in diesem Kapitel hin:

• die Durchführung komplexerer statistischer Verfahren anhand der linearen Re-


gression kennen lernen
• Modelle für statistische Verfahren spezifizieren
• die Resultate statistischer Verfahren weiterverarbeiten

Es existieren in R Funktionen zu nahezu allen statistischen Verfahren. Die Webseite


http://cran.at.r-project.org/web/views/ gibt einen thematisch gegliederten
Überblick, der stets aktualisiert wird. In (41) sind Verweise auf Pakete für eine
Reihe von typischen statistischen Verfahren aufgelistet.

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)

38.1 Lineare Regression – lm(), formula

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

38.1.1 Einfache lineare Regression

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.

> lm(formula = log(einkommen) ~ alter, data = inc)


Call:
lm(formula = log(einkommen) ~ alter, data = inc)
Coefficients:
(Intercept) alter
6.66086 0.02134

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:

• summary() gibt Standardfehler, p-Werte, R2 , F-Test und Residuenstatistiken


in die Console aus und als Objekt der Klasse summary.lm zurück.
• plot() erstellt diagnostische Plots für die Regressionsanalyse.
• predict() berechnet Vorhersagewerte mit Standardfehlern und Konfidenzin-
tervallen.
• vcov() gibt die Varianz-Kovarianz-Matrix zurück.
• coef() extrahiert die Koeffizienten eines Modells.
• confint() berechnet Konfidenzintervalle für die Koeffizienten.
610 J Data Science und Statistik in der Praxis

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.

> # Modell berechnen und Summary erzeugen


> m1 <- lm(log(einkommen) ~ alter, data = inc)
> summary(m1)
Call:
lm(formula = log(einkommen) ~ alter, data = inc)
Residuals:
Min 1Q Median 3Q Max
-1.46167 -0.39312 0.01529 0.36924 1.44590
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 6.660858 0.088828 74.99 <2e-16 ***
alter 0.021338 0.001996 10.69 <2e-16 ***
---
Signif. codes: 0 *** 0.001 ** 0.01 * 0.05 . 0.1 1
Residual standard error: 0.5284 on 398 degrees of freedom
Multiple R-squared: 0.223, Adjusted R-squared: 0.2211
F-statistic: 114.2 on 1 and 398 DF, p-value: < 2.2e-16

Im Output wird zuerst das formula-Objekt ausgegeben und anschließend Maßzah-


len für die Residuenverteilung. Für die Koeffizienten des Modells werden neben den
Schätzern (Estimate) die Standardfehler der Schätzer (Std. Error) sowie Ergeb-
nisse von Hypothesentests ausgegeben. Folgendes Hypothesenpaar wird dabei jeweils
getestet:

H0 : Der tatsächliche Wert des Koeffizienten ist 0.


H1 : Der tatsächliche Wert des Koeffizienten unterscheidet sich von 0.
Der Output umfasst die Teststatistik, die mit t value benannt ist, da die Teststa-
tistik einer t-Verteilung folgt, sowie den p-Wert Pr(>|t|) inklusive Klassifikation
mittels Sternen. p-Werte unter 2 · 10−16 werden als <2e-16 ausgegeben. Abgespei-
chert werden die p-Werte und die anderen Werte in voller Präzession als Matrix:

> summary(m1)$coef # coef als Abkürzung für coefficients


Estimate Std. Error t value Pr(>|t|)
(Intercept) 6.66085783 0.088827951 74.98606 6.926019e-237
alter 0.02133833 0.001996411 10.68835 1.303856e-23

In unserem Beispiel verwerfen wir in beiden Fällen die Nullhypothese.


Am Ende des summary()-Outputs werden Statistiken für den Modellfit ausgege-
ben: der Standardfehler der Residuen (Residual standard error), das Bestimmt-
heitsmaß (Multiple R-squared) sowie das adjustierte R2 (Adjusted R-squared),
das die Anzahl der im Modell geschätzten Parameter berücksichtigt. Mit dem ab-
schließenden F-Test (F-statistic) wird untersucht, ob das Modell einen signifikan-
ten Erklärungswert hat. In unserem Fall ist der F-Test ident mit dem t-Test für
alter, da wir nur eine erklärende Variable inkludierten.
38 Statistische Modelle 611

38.1.2 Mehrere metrische unabhängige Variablen, Modellbildung

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:

> m2 <- lm(log(einkommen) ~ alter + log(einkommen_vorjahr), data = inc)

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

oder zum Beispiel mit folgender Kurzschreibweise möglich:


y ˜ (x1 + x2 + x3) ˆ 2
612 J Data Science und Statistik in der Praxis

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 <- paste0("y ~ ", paste0("x", 1:3, collapse = " * "))

> f > as.formula(f)


[1] "y ~ x1 * x2 * x3" y ~ x1 * x2 * x3

38.1.3 Kategorielle Variablen – model.matrix(), contrasts()

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:

> m3 <- lm(log(einkommen) ~ bildung, data = inc)


> coef(m3)
(Intercept) bildungmittel bildunghoch
7.43399533 0.02338515 0.60226505

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:

> model.matrix(m3)[cumsum(c(0, 2 * m, 7 * m, 3 * m, 6 * m, 1 * m)) + 1, ]


(Intercept) bildungmittel bildunghoch
1 1 0 0
41 1 1 0
181 1 0 1
241 1 0 0
361 1 1 0
381 1 0 1
614 J Data Science und Statistik in der Praxis

38.1.4 Modellselektion und Modelldiagnostik – step(), anova()

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!

> anova(m1, m2)


Analysis of Variance Table
Model 1: log(einkommen) ~ alter
Model 2: log(einkommen) ~ alter + log(einkommen_vorjahr)
Res.Df RSS Df Sum of Sq F Pr(>F)
1 398 111.135
2 397 87.847 1 23.288 105.25 < 2.2e-16 ***
---
Signif. codes: 0 *** 0.001 ** 0.01 * 0.05 . 0.1 1

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

Das wahre Modell lautete:

inc$einkommen <- 10^(-0.25 * (inc$geschlecht == "männlich") +


0.005 * inc$alter +
0.3 * (inc$bildung == "hoch") +
0.007 * inc$alter * (inc$geschlecht == "männlich") +
rnorm(n, sd = .2) + 3)

Wir verwendeten für den exponentiellen Zusammenhang die Basis 10 statt e, um


den Einkommenswert in Relation zum Exponenten besser einschätzen zu können.
In allen Regressionsmodellen transformieren wir deshalb das Einkommen mit dem
Logarithmus zur Basis 10.
Wir beginnen mit einfachen Regressionen für die drei erklärenden Variablen alter,
geschlecht und bildung mit der abhängigen Variable einkommen. Wir betrachten
die entsprechenden Koeffizienten und 95%-Konfidenzintervalle mit der generischen
Funktion confint(). Die Intercepts interessieren uns hier nicht.

> tmp <- lm(log10(einkommen) ~ alter, data = inc) # Alter


> c(tmp$coef[2], confint(tmp)[2, ])
alter 2.5 % 97.5 %
0.009267120 0.007562588 0.010971651

> tmp <- lm(log10(einkommen) ~ geschlecht, data = inc) # Geschlecht


> c(tmp$coef[2], confint(tmp)[2, ])
geschlechtmännlich 2.5 % 97.5 %
0.02002409 -0.03112197 0.07117015

> tmp <- lm(log10(einkommen) ~ bildung, data = inc) # Bildung


> cbind(Estimate = tmp$coef[2:3], confint(tmp)[2:3, ])
Estimate 2.5 % 97.5 %
bildungmittel 0.01015604 -0.04669399 0.06700608
bildunghoch 0.26156039 0.19085405 0.33226672

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:

> tmp <- lm(log10(einkommen) ~ alter * geschlecht, data = inc)


> cbind(Estimate = tmp$coef[2:4], confint(tmp)[2:4, ])
Estimate 2.5 % 97.5 %
alter 0.005274570 0.002984262 0.007564877
geschlechtmännlich -0.364030717 -0.512595830 -0.215465603
alter:geschlechtmännlich 0.008485221 0.005150755 0.011819687

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:

> inc$alter_c <- scale(inc$alter, scale = FALSE)


> inc$geschlecht_r <- inc$geschlecht
> contrasts(inc$geschlecht_r) <- c(-.5, .5)

Im letzten Schritt inkludieren wir noch die Variable bildung:

> m4 <- lm(log10(einkommen) ~ alter * geschlecht + bildung, data = inc)


> cbind(Estimate = m4$coef[2:6], confint(m4)[2:6, ])
Estimate 2.5 % 97.5 %
alter 0.004351373 0.002324788 0.006377957
geschlechtmännlich -0.354198427 -0.485289234 -0.223107620
bildungmittel 0.002091135 -0.044641760 0.048824030
bildunghoch 0.275224902 0.215374928 0.335074876
alter:geschlechtmännlich 0.009566948 0.006619443 0.012514454

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

Residuals vs Fitted Normal Q−Q


0 2 4

312 312
0.5
Residuals
−0.5

245
228 245
228
−3

3.0 3.4 3.8 −3 −1 1 3


Fitted values Theoretical Quantiles
Standardized residuals

Standardized residuals

Scale−Location Residuals vs Leverage


312
−2 0 2 4

228 312
245
1.0

Cook’s
397 distance
382
0.0

3.0 3.4 3.8 0.000 0.020


Fitted values Leverage

Abbildung 38.1: Diagnostische Plots für lineares Regressionsmodell

Keiner der Plots zeigt Auffälligkeiten, die auf eine Verletzung von Modellannahmen
hindeuten würde.
38 Statistische Modelle 617

38.1.5 Zusatzbeispiel zur linearen Regression – predict(), step()

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.

> y <- 2 + x + 5 * x^2 + 10 * (gruppe == 1) -


+ 10 * (gruppe == 1) * x + rnorm(n)

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.

> d <- data.frame(x, gruppe, y)


> d <- d[order(d$x), ]

In Abb. 38.2 (links) sind die simulierten Daten in einem Streudiagramm dargestellt,
wobei wir x auf der x-Achse und gruppe farblich darstellen.

> plot(d$x, d$y, col = d$gruppe + 1, pch = d$gruppe + 1,


+ xlab = "x", ylab = "y")
> legend("topright", legend = 0:1, title = "Gruppe", col = 1:2, pch = 1:2)

In Modell 1 schätzen wir nur den Intercept. Dieses Modell wird auch Nullmodell
genannt (vgl. (38.1.2)).

> res.lm1 <- lm(y ~ 1, data = d) # Modell 1


> coef(res.lm1)
(Intercept)
21.57097

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

In Modell 2 erweitern wir Modell 1 um einen linearen Term für x.

> res.lm2 <- lm(y ~ x, data = d) # Modell 2


> coef(res.lm2)
(Intercept) x
22.375406 -4.625762

In Modell 3 folgt zusätzlich ein quadratischer Term für x.

> res.lm3 <- lm(y ~ x + I(x^2), data = d) # Modell 3


> coef(res.lm3)
(Intercept) x I(x^2)
7.351130 -5.088865 5.325251

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.

> plot(d$x, d$y, col = d$gruppe + 1, pch = d$gruppe + 1,


+ xlab = "x", ylab = "y")
> legend("topright", legend = 0:1, title = "Gruppe", col = 1:2, pch = 1:2)

> lines(d$x, predict(res.lm1), col = "black")


> lines(d$x, predict(res.lm2), col = "green")
> lines(d$x, predict(res.lm3), col = "blue")

> legend("top", title = "Modell", legend = paste(1:3),


+ col = c("black", "green", "blue"), lty = 1)

Gruppe Modell Gruppe


80

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

Abbildung 38.2: Streudiagramm für den Zusammenhang im Regressionsbeispiel.


Links: Das rohe Streudiagramm. Rechts: Vorhersagen der Mo-
delle 1 bis 3.
38 Statistische Modelle 619

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.

> plot(d$x, d$y, col = d$gruppe + 1, pch = d$gruppe + 1,


+ xlab = "x", ylab = "y")
> legend("topright", legend = 0:1, title = "Gruppe", col = 1:2, pch = 1:2)
> yhat4 <- predict(res.lm4)
> for (i in 0:1) lines(d$x[d$gruppe == i], yhat4[d$gruppe == i], col = i + 1)

Um die Daten optimal zu beschreiben, benötigen wir noch Interaktionseffekte. In


Modell 5 bauen wir Wechselwirkungen zwischen gruppe und x bzw. xˆ2 ein.
> res.lm5 <- lm(y ~ gruppe * (x + I(x^2)), data = d) # Modell 5
> summary(res.lm5)$coef
Estimate Std. Error t value Pr(>|t|)
(Intercept) 2.1482939 0.15796584 13.5997368 1.297872e-27
gruppe 10.1105383 0.23475933 43.0676738 3.840018e-84
x 1.0315864 0.07456531 13.8346689 3.186733e-28
I(x^2) 4.9767608 0.04579157 108.6828968 4.371063e-140
gruppe:x -10.0383286 0.09803148 -102.3990306 2.079315e-136
gruppe:I(x^2) -0.0452785 0.06076900 -0.7450921 4.574304e-01

In Modell 5 haben wir sowohl die Zweifach-Wechselwirkungen zwischen gruppe und


dem linearen Term x als auch dem quadratischen Term xˆ2 inkludiert. Entsprechend
der Simulation von y benötigen wir nur die Interaktion mit dem linearen Term x.
Auch die statistischen Tests für die Koeffizienten von Modell 5 zeigen an, dass die
Interaktion zwischen gruppe und dem quadratischen Term xˆ2 nicht signifikant ist.
Während Modell 5 die Daten optimal beschreibt, aber redundante Terme beinhaltet,
schätzen wir in Modell 6 jene Regression, die die Daten mit möglichst wenigen
Parametern bestmöglich beschreibt.
> res.lm6 <- lm(y ~ gruppe * x + I(x^2), data = d) # Modell 6
> coef(res.lm6)
(Intercept) gruppe x I(x^2) gruppe:x
2.208835 9.986169 1.036930 4.951051 -10.044820

In Abb. 38.3 (rechts) stellen wir die Vorhersage durch Modell 6 dar.

> plot(d$x, d$y, col = d$gruppe + 1, pch = d$gruppe + 1,


+ xlab = "x", ylab = "y")
> legend("topright", legend = 0:1, title = "Gruppe", col = 1:2, pch = 1:2)
> yhat6 <- predict(res.lm6)
> for (i in 0:1) lines(d$x[d$gruppe == i], yhat6[d$gruppe == i], col = i + 1)
620 J Data Science und Statistik in der Praxis

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

Abbildung 38.3: Bessere Vorhersagemodelle im Regressionsbeispiel. Links: Mo-


dell 4. Rechts: Modell 6.

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

Tatsächlich funktioniert die Modellselektion im vorliegenden Fall. Modellselektion


ist ein sehr wichtiges und kontroversiell diskutiertes Thema der Statistik. Anhand
unseres einfachen Beispiels lässt sich eines der Problemfelder demonstrieren: Wir
übergeben step() zusätzlich noch die Dreifach-Wechselwirkung gruppe:x:I(xˆ2)
und somit das sogenannte volle Modell mit allen möglichen Termen:
> res.step <- step(lm(y ~ gruppe * x * I(x^2), data = d), trace = 0)
> coef(res.step)
(Intercept) gruppe x I(x^2)
2.10813674 10.14052995 1.26344137 4.98322990
gruppe:x gruppe:I(x^2) x:I(x^2) gruppe:x:I(x^2)
-10.45265290 -0.05186861 -0.04658058 0.07804515
38 Statistische Modelle 621

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.

38.2 Weitere statistische Verfahren

38.2.1 Varianzanalyse – aov(), Error()

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)

38.2.2 Generalisierte lineare Modelle – glm()

Generalisierte lineare Modelle wie logistische Regression oder Poisson-Regression


können analog zu linearen Modellen mit glm() spezifiziert werden. Im family-
Argument wird die entsprechende Verteilung angegeben: binomial für logistische
Regression, poisson für Poisson-Regression etc. Das folgende Beispiel zeigt ein lo-
gistisches Regressionsmodell für die Variable “Einkommen größer 2500 Euro”.

> 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

38.3.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Wie können wir eine einfache Regressionsgerade schätzen? (38.1.1)


• Mit welchen Funktionen können wir für ein geschätztes lineares Regressionsmo-
dell Elemente extrahieren und weitere Berechnungen durchführen? In welcher
Datenstruktur ist das Rückgabeobjekt von lm() (wie auch summary.lm()) or-
ganisiert? (38.1.1)
• Wie können wir ein lineares Regressionsmodell mit mehreren erklärenden me-
trischen Variablen schätzen? (38.1.2)
• Wie ist eine Formel syntaktisch aufgebaut? Welche Möglichkeiten stehen uns
zur Spezifikation eines formula-Objekts zur Verfügung? Was ist ein additi-
ves Modell und wie definieren wir es in R? Wie bauen wir einzelne bzw. alle
Zweifach- bzw. Mehrfachwechselwirkungen ins Modell ein? Wie schätzen wir
Modelle ohne Intercept bzw. das Nullmodell? Wie können wir Berechnungen
innerhalb einer Formel vornehmen? (38.1.2)
• Wie lassen sich kategorielle erklärende Variablen in einem linearen Regressi-
onsmodell berücksichtigen? (38.1.3)
• Welche einfachen Möglichkeiten zur Modellselektion gibt es in R? (38.1.4)
• Wie schätzen wir ein logistisches Regressionsmodell in R? (38.2.2)

38.3.2 Ausblick: Funktionen zu weiteren statistischen Verfahren

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.

a) Extrahiere den Koeffizienten für x und den entsprechenden p-Wert für


jede Simulation und stelle die beiden Werte grafisch dar. Für die Dar-
stellung von p-Werten wird oft der negative Werte des 10er Logarithmus
verwendet. Warum macht das Sinn?
b) Die Funktion summary.lm() gibt unter F-statistic einen p-Wert aus.
Speichere diesen p-Wert für jeden Simulationsdurchgang ab. Wie viel Pro-
zent der p-Werte sind kleiner als 0.05?

2. Gegeben ist folgender Datensatz mit drei Merkmalen: der abhängigen Variable
y und den beiden erklärenden Variablen x1 und x2:

x1 <- rnorm(100, mean = 10)


x2 <- 2 + .5 * x1 + rnorm(100, sd = .2)
y <- 3 + .3 * x1 + .7 * x2 + rnorm(100, sd = .5)

a) Vergleiche die Koeffizienten und Teststatistiken für x1 und x2 aus den


beiden Einfachregressionen mit dem additiven Modell. Um mehr über die
Hintergründe der Unterschiede zu erfahren, lohnt sich eine Recherche zum
Thema Omitted Variable Bias.
b) Vergleiche die Koeffizienten und Teststatistiken für x1 und x2 aus dem ad-
ditiven Modell mit jenen des vollen Modells (inklusive Wechselwirkung).
c) Wiederhole die Analyse aus 2b) mit mittelwertzentrierten 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

b) Ein Kollege möchte das Modell y = β0 + β1 · x1 + β2 · x21 + β3 · x2 schätzen.


Folgendes formula-Objekt hat er erstellt:
> formel <- as.formula("y ~ x1 + x1^2 + 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

Dort wollen wir in diesem Kapitel hin:

• R-Code in mehrere Dateien aufteilen

• R-Code und Dokumentation integrieren


• effiziente Fehlersuche in R

Folgende Fragen wollen wir in diesem Kapitel behandeln:

• Wir haben im Rahmen vorangegangener Datenanalyseprojekte eine Reihe von


eigenen Funktionen programmiert, auf die wir auch bei künftigen Projekten
zurückgreifen wollen. Wir möchten aber nicht die aus vielen Zeilen bestehen-
den Codes in unser aktuelles R-Skript inkludieren und vor jeder Datenanalyse
ausführen. Welche Möglichkeiten gibt es dazu in R? (39.1)
• Wir erstellen einen schön formatierten Analysebericht, in dem wir aus R Gra-
fiken, Tabellen und Einzelergebnisse im Text integrieren wollen. Wir möchten
vermeiden, Outputs und Grafiken aus R zu kopieren und in den Bericht hän-
disch einzufügen, da uns diese Vorgehensweise zu umständlich und fehleran-
fällig erscheint. Welche besseren Möglichkeiten gibt es dafür? (39.2)
• Wir programmieren gerade eine umfangreiche Funktion. Leider erscheint bei
jedem Aufruf eine Fehlermeldung, die wir nicht zuordnen können. Da viele
Objekte nur während des Funktionsaufrufs existieren, kommen wir mit der
Fehlersuche nicht voran. Gibt es Funktionen, die uns bei der Fehlersuche hel-
fen? (39.3)

39.1 R-Code in mehrere Dateien teilen – source()

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)
}

Daten.R - Daten erfassen


gewicht <- c(56, 79, 103, 45, 68)
groesse <- c(1.54, 1.80, 1.81, 1.40, 1.65)

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:

> # Parameter, Input-/Output-Ordern, etc. definieren


> # Arbeitsverzeichnis mit setwd() festlegen
> source("Funktionen.R")
> source("Daten.R")
> source("Auswertung.R")

Eine weitere Möglichkeit, größere Analyse- oder Programmierprojekte zu organisie-


ren, bietet die Entwicklungsumgebung RStudio durch sogenannte “Projects”. Hier
werden neben den Codedateien auch dazugehörige Datendateien, der Workspace, die
History und das Arbeitsverzeichnis verwaltet. Mehr dazu unter
https://support.rstudio.com/hc/en-us/articles/200526207-Using-Projects.

39.2 R-Code und Dokumentation integrieren

39.2.1 Einleitung

In einer typischen statistischen Analyse werden Daten eingelesen, verarbeitet, auf-


bereitet und analysiert. Alle diese Schritte können problemlos in Form von R-Code
umgesetzt werden. Eine Herausforderung stellt die übersichtliche Dokumentation der
Analyseschritte und die Darstellung der Ergebnisse dar. Wir können zwar Ergebnis-
objekte und Grafiken abspeichern und den Inhalt der Console in Dateien umleiten,
für einen Analyse- und Ergebnisbericht bedarf es aber des “händischen” Zusammen-
626 J Data Science und Statistik in der Praxis

stellens in einem separaten Dokument, zum Beispiel in einem Textverarbeitungspro-


gramm. Dieses Vorgehen ist mühsam und fehleranfällig, besonders wenn Ergebnisse
reproduziert oder mit aktualisierten Daten wiederholt werden sollen.
Genau hier setzt das R-Paket Sweave und seine Erweiterung und Überarbeitung
knitr an. Es ermöglicht die Integration von formatiertem Text, R-Code und Output
in einem Dokument. Der Text wird in einer einfachen Textdatei verfasst, die R-
Codestücke werden entsprechend gekennzeichnet in den Text integriert. knitr führt
anschließend die R-Codestücke aus und fügt sie in den Text ein.
Für das Verfassen eines ansprechenden Berichts mit schön formatiertem Text stehen
sogenannte Markup-Sprachen wie HTML, LATEXoder das in RStudio verfügbare R
Markdown (https://rmarkdown.rstudio.com/) zur Verfügung. Markup-Sprachen
erlauben Formatierungen wie Überschriften, Aufzählungen, Schriftstil etc. zu inklu-
dieren, die anhand von Tags vom inhaltlichen Text eindeutig unterscheidbar sind.

39.2.2 Einfügen des R-Codes in ein Dokument

Der R-Code muss entsprechend der Markup-Sprache unterschiedlich gekennzeichnet


werden. Für alle Markup-Sprachen stehen zwei Varianten für die Integration von
Codestücken zur Verfügung:

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

Inline-Code verwenden wir, um dynamisch Werte von R-Objekten oder Ergebnisse


kurzer Berechnungen in R in den Fließtext zu integrieren.
Code-Chunks und Inline-Code werden in LATEX, HTML und R Markdown mit un-
terschiedlichen Codewörten begrenzt. Im Folgenden sehen wir dasselbe Beispiel mit
Code-Chunk und Inline-Code für die drei Markup-Sprachen. Der Code-Chunk in-
kludiert den Mittelwert aus drei Zahlen in das Dokument. Der Code selbst wird
nicht inkludiert, da die Option echo auf FALSE gesetzt ist. Der Inline-Code gibt die
Standardabweichung der drei Zahlen auf eine Dezimalstelle gerundet zurück.
Die Codewörter zur Begrenzung von Code-Chunks sind grün, jene für Inline-Code
lila gekennzeichnet.
In LATEX (Dateiname beispiel.Rnw):

\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}

In HTML (Dateiname beispiel.Rhtml):

<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>

In R Markdown (Dateiname beispiel.Rmd):

---
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

Die Dokumente in den entsprechenden Markup-Sprachen inklusive Code-Chunks und


Inline-Codes werden abgespeichert, wobei standardmäßig die Dateiendungen .Rnw
für LATEX-Dokumente, .Rhtml für HTML-Dokumente und .Rmd für R Markdown-
Dokumente verwendet werden.

39.2.3 Dokument mit R-Code ausführen – knit(), render()

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")

Die erstellten Dokumente heißen beispiel.tex, beispiel.html bzw. beispiel.md.


Während die HTML-Datei ohne weitere Verarbeitung in jedem Browser dargestellt
werden kann, wird aus dem erstellten LATEX-Dokument beispiel.tex im nächs-
ten Schritt typischerweise ein druckbares PDF- oder PostScript-Dokument erzeugt.
Dazu muss eine TeX-Distribution, zum Beispiel TeX Live (www.tug.org/texlive)
oder MiKTeX (www.miktex.org), am Rechner verfügbar sein. Die Erzeugung des
Ausgabedokuments kann entweder über eine eigene LATEX-Entwicklungsumgebung
passieren oder in RStudio.
In Abb. 39.1 sehen wir einen Screenshot des erstellten PDF-Dokuments.

Abbildung 39.1: PDF-Dokument für beispiel.Rnw

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

39.3 Fehlertypen und Fehlersuche (Debugging)

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

Fehler treten in unterschiedlichsten Formen auf. Wir unterscheiden

• Syntaktische Fehler: Eine Syntaxregel wurde missachtet, zum Beispiel:


paste(1 2)
Fehler: unerwartete numerische Konstante in "paste(1 2"

• Semantische Fehler: Falsche Verwendung von Objekten bei korrekter Syntax,


zum Beispiel:
a * 2
Fehler: Objekt ’a’ nicht gefunden

• Logische Fehler: Inhaltliche Fehler, zum Beispiel möchten wir ganzzahlig divi-
dieren, schreiben aber:
> 34 / 7
[1] 4.857143

Die korrekte Syntax hätte gelautet:


> 34 %/% 7
[1] 4

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

> # Möglichkeit 1.) > # Möglichkeit 2.) > # Möglichkeit 3.)


> sum(x > 1) > length(x[x > 1]) > length(which(x > 1))
[1] 1 [1] 1 [1] 1

Treten NA-Werte auf, sieht dies anders aus:


> x <- c(1:2, NA)

> # Fehlerhafte Codes > # Korrekte bzw. korrigierte Codes


> sum(x > 1) # 1.) > sum(x > 1, na.rm = TRUE) # 1.)
[1] NA [1] 1
> length(x[x > 1]) # 2.) > x1 <- x[!is.na(x)] # 2.)
[1] 2 > length(x1[x1 > 1])
[1] 1
> length(which(x > 1)) # 3.)
[1] 1

Sorgfältiges Programmieren und eine umfassende Überprüfung der Dateninhalte be-


wahren uns hier vor möglichen Fehlern. Insbesondere wenn wir beim Erstellen des
Codes implizit annehmen, dass es keine fehlenden Werte gibt, müssen wir diese An-
nahme stets überprüfen. Wir erinnern an Regel 6 auf Seite 57.

39.3.2 Fehlersuche und -korrektur – browser(), recover()

Fehlersuche (auch Debugging) in einzelnen Codezeilen unterscheidet sich grundsätz-


lich von der Fehlersuche in Funktionen oder Schleifen. Bei einzelnen Codezeilen kann
vor und nach dem Ausführen des Codes jeder Variablenwert überprüft werden. In
Schleifen, wo dieselben Codezeilen mit veränderten Variablenwerten wiederholt aus-
geführt werden, muss zuerst jener Schleifendurchlauf identifiziert werden, bei dem
der Fehler auftrat. Dies gelingt am leichtesten über den Wert der Laufvariable beim
Abbruch der Schleife.
Die Fehlersuche in Funktionen ist aufwändiger. Wie bereits in (29.6) erläutert, wer-
den Objekte, die innerhalb einer Funktion erzeugt werden, in einem untergeordneten
Environment erzeugt und nach Beendigung der Funktion wieder gelöscht. Somit kön-
nen wir die Inhalte der Objekte beim Abbruch der Funktion nicht überprüfen.

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

> f1 <- function(a, b) a b


Fehler: unerwartetes Symbol in "f1 <- function(a, b) a b"

produziert unmittelbar beim Erstellen eine Fehlermeldung, da ein Operator zwischen


a und b fehlt. Somit ist das Auffinden eines syntaktischen Fehlers oft relativ einfach.
Hingegen erstellt R die Funktion
> z <- 5
> rm(z)
> f2 <- function() z

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.

> x <- 0:4 # Parameter x global machen.


> y <- log(x[x > 0]) # Parameter y global machen.
> t.test(x, y, paired = TRUE) # return() muss entfernt werden.
Fehler in complete.cases(x, y) : nicht alle Argumente haben gleiche Länge

> y # y ist jetzt global verfügbar.


[1] 0.0000000 0.6931472 1.0986123 1.3862944
632 J Data Science und Statistik in der Praxis

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.

> f3_print <- function(x) {


+ cat("x:", x, "\n")
+ y <- log(x[x > 0])
+ cat("y:", y, "\n")
+ return(t.test(x, y, paired = TRUE))
+ }
> f3_print(0:4)
x: 0 1 2 3 4
y: 0 0.6931472 1.098612 1.386294
Error in complete.cases(x, y) : nicht alle Argumente haben gleiche Länge

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

> f3_browser <- function(x) {


+ browser()
+ y <- log(x[x > 0])
+ browser()
+ return(t.test(x, y, paired = T))
+ }

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.

# Funktion f3() mit debug() kennzeichnen


> debug(f3)
634 J Data Science und Statistik in der Praxis

> 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]>

Um das debug-Kennzeichen aufzuheben, verwenden wir undebug(). Möchten wir


eine Funktion nur für einen einzigen Funktionsaufruf kennzeichnen, können wir
debugonce() einsetzen.
recover() verwenden
Die Funktion recover() aktiviert browser() genau an jener Stelle, bei der der
Fehler auftritt. Bei verschachtelten Funktionsaufrufen wie in unserem Beispiel der
Funktion f3() muss jene Funktion ausgewählt werden, die inspiziert werden soll.
Für die Anwendung wird recover() der error-Option als Wert übergeben.

> old <- getOption("error") # ursprünglichen Wert der Option speichern


> options(error = recover)

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

Enter a frame number, or 0 to exit

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

Abschließend müssen wir die error-Option wieder zurücksetzen.

> options(error = old) # Option auf alten Wert zurücksetzen

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.1 Zusammenfassende Kontrollfragen

Folgende Konzepte beherrschen wir unter anderem nach diesem Kapitel:

• Wie integrieren wir ausgelagerte R-Dateien in eine Masterdatei? (39.1)


• Was sind Code-Chunks und Inline-Code? Wie legen wir fest, ob R-Code in den
Text übernommen werden soll? Welche Möglichkeiten der Grafikeinbindung
haben wir und wie stellen wir sie ein? Wie legen wir fest, ob und wie Outputs
des R-Codes mitgedruckt werden? (39.2.2)
• Bei welchen Markup-Sprachen lässt sich R-Code integrieren und wie funktio-
niert dies? (39.2.2)
• Wie erzeugen wir aus Rnw-, Rhtml- und Rmd-Dateien jeweils eine pdf-Dateien?
(39.2.3)
• Welche drei Fehlertypen unterscheiden wir beim Programmieren und wann
treten sie auf? Welcher dieser Fehlertypen ist dabei besonders tückisch und
warum? (39.3.1)
• Welche Möglichkeiten bzw. Funktionen stehen uns in R für die Fehlersuche zur
Verfügung? Wie funktionieren sie? (39.3.2)
636 J Data Science und Statistik in der Praxis

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

3. Der folgende Code generiert eine Reihe von Objekten.

s <- c(1, sample(2:10))


for(i in 1:10)
cat("f", s[i], " <- function(x) if (x < 0) stop(’x kleiner 0’) else
f", s[i + 1], "(sqrt(x) - rnorm(1, .5))\n",
sep = "", file = "tmp", append = TRUE)
source("tmp")
unlink("tmp")

Bemerkung: In stop() verwenden wir "’" anstelle von Anführungszeichen.

a) Überprüfe, welche Objekte generiert wurden.


b) Was bewirkt die Codezeile source("tmp")?
c) Führe den Funktionsaufruf
f1(x = 1)

wiederholt aus. Dieser generiert unterschiedliche Fehlermeldungen.


d) Inwieweit helfen uns debug(), traceback() und recover() den Fehler
aufzuklären?
e) Ermittle für unterschiedliche Funktionsaufrufe, wo der Fehler auftritt!
f) Welchen Wert hat der Parameter x beim Auftreten des Fehlers?
g) Angeblich hat sich ein semantischer Fehler im Code eingeschlichen. Wel-
cher ist das und warum wird dieser nicht bei jedem Funktionsaufruf aus-
gegeben?
638 J Data Science und Statistik in der Praxis

40 Interaktive Webanwendungen mit Shiny

Dort wollen wir in diesem Kapitel hin:


• die Struktur einer Shiny-Anwendung kennenlernen
• verfügbare Shiny-Elemente überblicksartig kennenlernen
• eine einfache Shiny-Anwendung programmieren
Zunächst klären wir in Infobox 15 kurz, was Shiny überhaupt ist.

Infobox 15: Shiny

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)

• Wie können wir Shiny-Anwendungen veröffentlichen? (40.1.2)

40.1 Grundlegendes zur Programmierung einer Shiny-Anwendung

Shiny-Anwendungen können sowohl in RStudio als auch in einem beliebigen Editor


oder einer Programmierumgebung erstellt und installiert werden. RStudio stellt er-
weiterte Funktionalitäten zur Verfügung: Die Anwendung kann unter anderem per
Knopfdruck (“Run App”) ausgeführt und somit getestet 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_40
40 Interaktive Webanwendungen mit Shiny 639

40.1.1 Struktur einer Shiny-Anwendung

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.

40.1.2 Bereitstellung einer Shiny-Anwendung

Für die Bereitstellung einer Shiny-Anwendung ist ein Shiny-Webserver erforder-


lich. Verschiedene Accounts (kostenfrei und professionell) können auf der von RStu-
dio zur Verfügung gestellten Infrastruktur angelegt werden (http://shinyapps.io).
Es können aber auch eigene Shiny-Server gekauft bzw. selbst gebaut werden, siehe
dazu www.rstudio.com/products/shiny-server.
640 J Data Science und Statistik in der Praxis

Shiny-Anwendungen können auf einem shinyapps.io-Account mit Hilfe des Pakets


rsconnect direkt aus R installiert werden. Zuerst muss mit der Funktion
setAccountInfo(name, token, secret)

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.

40.1.3 Interaktives Verhalten steuern – reactive()

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.

40.2 Serverfunktion und Render-Funktionen

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

Im Minimalbeispiel in (40.1.1) ist die Serverfunktion folgendermaßen definiert:

server <- function(input, output){}

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.

server <- function(input, output) {


output$p1 <- renderPlot(hist(rnorm(10)))
}

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.

Tabelle 40.1: Render-Funktionen zur Erstellung von Outputelementen

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.

R-Befehle, die in Zwischenschritten die Inputs verarbeiten, werden innerhalb der


dazugehörigen Render-Funktion platziert. Umfasst die Outputfunktion mehr als ei-
ne Befehlszeile, müssen die Codezeilen mit geschwungenen Klammern umschlossen
werden. Im folgenden Beispiel werden zuerst 10 normalverteilte Zufallszahlen dem
Objekt x zugewiesen und anschließend ein Histogramm erstellt.
642 J Data Science und Statistik in der Praxis

server <- function(input, output) {


output$p1 <- renderPlot({
x <- rnorm(10)
hist(x)})
}

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.

server <- function(input, output) {


x <- reactive(rnorm(10))
output$plot1 <- renderPlot(hist(x()))
output$plot2 <- renderPlot(plot(x()))
}

40.3 User Interface Objekt (UI)

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")))

Den Funktionen sidebarPanel() und mainPanel() werden die entsprechenden Input-


und Outputfunktionen übergeben.
Layouts mit Unterseiten und dazugehörenden Navigationsmenüs lassen sich
in Shiny einfach realisieren. Die Funktion navbarPage() erzeugt ein Layout mit
Navigationsleiste in der Kopfzeile. Titel und Inhalte der Unterseiten werden mit der
Funktion tabPanel() festgelegt. Folgendes Codebeispiel zeigt ein Layout mit zwei
Unterseiten:

ui <- fluidPage(
navbarPage(title = "Titel",
tabPanel("Unterseite 1", "Inhalt"),
tabPanel("Unterseite 2", "Inhalt")))

Alternativ zu navbarPage() können die Funktionen navlistPanel() für vertika-


le Navigationsleisten und tabsetPanel() für horizontale Navigationsleisten
ohne Titel verwendet werden.
Weitere Layoutfunktionen sind titlePanel() und wellPanel(). Mit titlePanel()
wird ein Webseitentitel definiert, an entsprechender Stelle dargestellt und ein title-
HTML-Tag mit dem spezifizierten Titel generiert. wellPanel() generiert ein Feld
mit etwas eingerücktem Rahmen und standardmäßig grauem Hintergrund. Alterna-
tive Farb- und Schriftarten können mit dem Zusatzpakte shinythemes ausgewählt
werden. Mehr Details zum Layout von Shiny-Webseiten finden sich unter
https://shiny.rstudio.com/articles/layout-guide.html

40.3.2 Input- und Outputfunktionen

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

Zur Erstellung des Schiebereglers verwenden wir die Funktion sliderInput()


mit den folgenden Parametern:
• inputID spezifiziert den Namen des Listenelements des Objekts input. Über
diesen Namen können die Inputdaten, in unserem Fall die Stichprobengröße,
in der Serverfunktion angesprochen werden.
• In label geben wir den Text an, der auf der Webseite über dem Schieberegler
erscheint.
• min und max sind die Begrenzungen des Schiebereglers.
• value gibt die voreingestellte Position des Schiebereglers an.

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.

Abbildung 40.1: Schieberegler in einem Sidebar-Panel

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.

Tabelle 40.2: Liste der Inputelemente und der dazugehörigen Funktionen

Inputelement Inputfunktion Parameter


Aktionsfeld actionButton()
Checkbox checkboxInput()
Checkbox-Gruppe checkboxGroupInput() choices
nummerische Eingabe numericInput() value
Schieberegler sliderInput() min, max, value
Datumsfeld dateInput()
Datumsbereich-Feld dateRangeInput()
Texteingabe textInput()
Textfeldeingabe textAreaInput()
Dateieingabe fileInput()
Radiofeld (Optionsfeld) radioButtons() choices
Dropdown-Auswahl selectInput() choices
Senden-Feld submitButton()
Variablenauswahl varSelectInput() data

Neben Inputfunktionen gibt es eine Reihe an Outputfunktionen. Diese korrespon-


dieren mit den entsprechenden Render-Funktionen zur Erstellung des Outputobjekts
in Tab. 40.1 auf Seite 641. Welche Render-Funktion zu welcher Outputfunktionen
gehört, ist in Tab. 40.3 dargestellt.

Tabelle 40.3: Outputfunktionen mit entsprechenden Render-Funktionen

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

40.4 Shiny-Beispiel: Darstellung einer


Normalverteilungsstichprobe

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.

40.4.1 Version 1: sidebarLayout

In Version 1 der Shiny-App wählen wir ein sidebarLayout() mit titlePanel().


Im linken sidebarPanel() inkludieren wir Inputelemente für die drei Merkmale:
• Stichprobengröße: Schieberegler mit sliderInput()
• Mittelwert: numerisches Eingabefeld mit numericInput()
• Standardabweichung: numerisches Eingabefeld mit numericInput()
Die vorgegebenen Werte müssen für alle drei Felder über den Parameter value
angegeben werden. Der Schieberegler benötigt zusätzlich noch die Parameter min
und max.
Im (rechten) mainPanel() stellen wir die Verteilung mittels Histogramm dar. Das
Histogramm wird mit der Render-Funktion renderPlot() über das Listenelement
hist von output an die Outputfunktion plotOutput() übergeben.

Abbildung 40.2: Shiny-Anwendung zum Beispiel (Version 1)

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

> # Paket shiny laden


> library(shiny)

> ui <- fluidPage(


+ titlePanel("Normalverteilungssimulation"),
+ sidebarLayout(
+ sidebarPanel(
+ sliderInput(inputId = "N",
+ label = "Stichprobengröe:",
+ min = 10,
+ max = 500,
+ value = 100),
+ numericInput(inputId = "mean",
+ label = "Mittelwert:",
+ value = 0),
+ numericInput(inputId = "sd",
+ label = "Standardabweichung:",
+ value = 1)),
+ mainPanel(
+ plotOutput("hist"))
+ )
+ )

> server <- function(input, output) {


+ output$hist <- renderPlot({
+ x <- rnorm(input$N, mean = input$mean, sd = input$sd)
+ hist(x, main = "", xlab = "", ylab = "Dichte", freq = FALSE)
+ })
+ }

> shinyApp(ui = ui, server = server)

40.4.2 Version 2: sidebarLayout mit Unterseiten

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().

Beachte: Da beide Grafiken auf dieselben simulierten Daten zugreifen, erzeugen


wir die Zufallszahlen außerhalb der Funktion renderPlot(). Dazu müssen wir
die Funktion reactive() einsetzen. Wie bereits in (40.2) erwähnt, müssen wir in
weiterer Folge x() schreiben.
Der folgende Code erzeugt die erweiterte Version unserer Shiny-App in Abb. 40.3.

> # Paket shiny laden


> library(shiny)
648 J Data Science und Statistik in der Praxis

> ui <- fluidPage(


+ titlePanel("Normalverteilungssimulation"),
+ sidebarLayout(
+ sidebarPanel(
+ sliderInput(inputId = "N",
+ label = "Stichprobengröe:",
+ min = 10,
+ max = 500,
+ value = 100),
+ numericInput(inputId = "mean",
+ label = "Mittelwert:",
+ value = 0),
+ numericInput(inputId = "sd",
+ label = "Standardabweichung:",
+ value = 1)),
+ mainPanel(
+ tabsetPanel(
+ tabPanel("Histogramm", plotOutput("hist")),
+ tabPanel("Kerndichteschätzer", plotOutput("kern"))))
+ )
+ )

> server <- function(input, output) {


+ x <- reactive(rnorm(input$N, mean = input$mean, sd = input$sd))
+ output$hist <- renderPlot(hist(x(), main = "", xlab = "",
+ ylab = "Dichte", freq = FALSE))
+ output$kern <- renderPlot(plot(density(x()), main = "", xlab = "",
+ ylab = "Dichte", bty= "l"))
+ }

> shinyApp(ui = ui, server = server)

40.5 Abschluss

40.5.1 Zusammenfassende Kontrollfragen

• 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

Abbildung 40.3: Shiny-Anwendung zum Beispiel (Version 2)

• Welche Dateneingabeelemente stehen in Shiny zur Verfügung? Wie bauen wir


im User Interface Eingabeelemente ein? Wie funktioniert die Kommunikation
zwischen User Interface und der Serverfunktion? Wie greifen wir in der Server-
funktion auf Eingaben zu? Welchen wichtigen Zweck hat dabei die inputId?
(40.3.2), (40.4)

In Tab. 40.2 listen wir wichtige Input-Widgets auf.

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.

3. Zeige eine Verwendung des Inputelements checkboxGroupInput().


4. Programmiere ein Beispiel, in dem der HTML-Tag h1 verwendet wird.
650 J Data Science und Statistik in der Praxis

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.

41.1 Konzeptionelle Erweiterungen von Base-R

• 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

• ggplot2: Grafiken basierend auf “The Grammar of Graphics” von L. Wilkin-


son, Teil von tidyverse
• lattice (Trellis Graphics for R, vorinstalliert): Visualisierung von multivaria-
ten Daten

© 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

• XLConnect: Lesen, Schreiben und Gestalten (Formatierungen, Formeln etc.)


von Microsoft Excel-Dateien. Eine Installation von Microsoft Excel ist nicht
erforderlich, jedoch eine aktuelle Version der Java Runtime Environment.
• foreign (vorinstalliert): Lesen und Schreiben von Daten u. a. aus den Pro-
grammen Minitab, SAS, SPSS, Stata. In (32.6.2) haben wir ein Beispiel mit
SPSS vorgestellt.
• readr: Lesen von tabellenähnlichen Daten, Teil von tidyverse
• RODBC: Zugriff auf Datenbanken über eine ODBC-Verbindung aus R

• shiny: Interaktive Webanwendungen. Wird im Detail in (40) behandelt.


• xtable: Verwandelt Daten in R zu LaTeX- oder HTML-Tabellen
• knitr: Integration von R-Code und Dokumentation. Wird im Detail in (39.2)
behandelt.

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

41.5 Statistische Modelle und Methoden

• survival (vorinstalliert): Enthält die wesentlichen Funktionen zur Überle-


benszeitanalyse (Survival Analysis) inklusive Kaplan-Meier Schätzer und Cox-
Regression. Überlebenszeitdaten werden im eigenen Objekttyp Surv abgespei-
chert.

• zoo (Z’s ordered obersverations): Erweiterung der Funktionen zu Zeitreihen


aus dem stats-Paket um irreguläre Zeitreihen. stats umfasst Funktionen zu
Zeitreihenmodellen wie arima() und die Klasse ts für Zeitreihendaten.
• nlme (Linear and Nonlinear Mixed Effects Models, vorinstalliert): Schätzung
von linearen und nicht-linearen Mixed-Effects Modellen
• lme4 (Linear Mixed-Effects Models using ’Eigen’ and S4): Schätzung von li-
nearen und generalisierten linearen Mixed-Effects-Modellen
• lavaan (Latent Variable Analysis): Schätzung von Modellen mit latenten Va-
riablen wie Konfirmatorische Faktorenanalyse und Strukturgleichungsmodellen
• caret (Classification and Regression Training): Funktionen für Klassifikati-
onsmethoden wie k-Nearest Neighbour, Partial Least Squares Discriminant
Analysis und Bagging
• e1071 Funktionssammlung, die unter anderem Latent Class Analysis, Fuzzy
Clustering, Support Vector Machines und Naive Bayes Classifier umfasst.
• multcomp: Durchführung und Korrekturen für multiple Hypothesentests. Das
stats-Paket umfasst die Funktion p.adjust() für die Adjustierung von p-
Werten.
• rstan: R-Funktionen zur Schätzung von Stan-Modellen. Stan ist eine eige-
ne statistische Programmiersprache für Bayesianische Inferenz mit Hilfe von
Markov Chain Monte Carlo.

41.6 Spezielle Anwendungsbereiche

• vegan (Community Ecology Package): Statistische Modelle, die unter anderem


in der Ökologie verwendet werden, wie Korrespondenzanalyse und Multidimen-
sionale Skalierung.
• psych: Umfasst vor allem multivariate Methoden, die in der Psychologie an-
gewendet werden.
• Bioconductor: Kein einzelnes R-Paket, sondern eine Sammlung von ca. 1200
Paketen für Anwendungen im Bereich Bioinformatik. Für Details siehe:
http://bioconductor.org
42 R-Ressourcen 653

42 R-Ressourcen

Bereits in den einzelnen Kapiteln haben wir weiterführende Informationen erwähnt.


Hier möchten wir noch einmal eine Auswahl an R-Ressourcen zusammenstellen.
Die offizielle R-Webseite https://www.r-project.org enthält neben dem Down-
load von R zahlreiche Informationen:
• Offizielle R-Manuals: https://cran.r-project.org/manuals.html
• Liste an R-Büchern: https://www.r-project.org/doc/bib/R-books.html
• Das Open Access R-Journal: https://journal.r-project.org
• Ankündigungen von R-Konferenzen, insb. der jährlichen UseR!-Konferenz.
• Einen aktuellen und redaktionell betreuten thematischen Überblick zu verfüg-
baren R-Paketen: https://cran.r-project.org/web/views
• R Mailing Lists: https://www.r-project.org/mail.html
Auch die Webseite der Programmierumgebung RStudio umfasst eine Ressourcen-
Sektion mit Links zu Webinars, Videos, Büchern, Cheatsheets etc.:
https://resources.rstudio.com. Der Fokus liegt stärker auf Funktionen der Paket-
Sammlung tidyverse als auf klassischen R-Funktionen. Insbesondere erwähnens-
wert sind die beiden frei verfügbaren Bücher:
• Hadley Wickham & Garrett Grolemund (2017) R for Data Science. O‘Reilly.
https://r4ds.had.co.nz
• Hadley Wickham (2019) Advanced R 2nd Edition. CRC Press.
http://adv-r.hadley.nz
Auf der Frage-und-Antwort Plattform Stack Overflow finden sich sowohl auf der
allgemeinen Seite zahlreiche Beiträge zu R: https://stackoverflow.com/tags/R,
als auch auf der Stack Exchange Seite Cross Validated:
https://stats.stackexchange.com/tags/R
Eine durchsuchbare Sammlung aller R-Funktion findet sich unter
https://www.rdocumentation.org/.
Die Verlage Springer und Chapman & Hall veröffentlichen Buchserien zu R:

• Springer Use R! Series https://link.springer.com/bookseries/6991


• Chapman & Hall R Series
https://www.crcpress.com/Chapman--HallCRC-The-R-Series/book-series/
CRCTHERSER
Online-Kurse zu R werden u. a. von den Firmen DataCamp (https://www.datacamp.
com) und Coursera (https://www.coursera.org) angeboten.
© 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_42
K Verzeichnisse
656 Stichwortverzeichnis

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

Eigenwert . . . . . . . . . . . . . . . . . . . . . . . . 205 Quantilsfunktion . . . . . . . . . . . . 113


Einfacher Datentyp . . . . . . . . . . . . . . 132 Render-Funktion . . . . . . . . . . . . 641
Eingabefehler . . . . . . . . . . . . . . . . . . . . . 91 Serverfunktion . . . . . . . . . . . . . . 640
Einheitsmatrix . . . . . . . . . . . . . . . . . . . 196 vektorwertige Funktion . . . . . . . 47
Einseitiger Hypothesentest . . . . . . . 596 Verteilungsfunktion . . . . . . . . . 113
Elementweises Rechnen. . . . . . .46, 197 Vorzeichenfunktion . . . . . . . . . . . 49
else-Anweisung. . . . . . . . . . . . . . . . . . .382 Wrapper-Funktion . . . . . . . . . . . 406
Endlosschleife . . . . . . . . . . . . . . . . . . . . 391
Environment . . . . . . . . . . . . . . . . . . . . . 416 G
Error . . . . . . . . . . . . . . . . . . . . . . . . . . . . 629 Ganzzahlige Division . . . . . . . . . . . . . . 46
Ersetzfunktion . . . . . . . . . . . . . . . . . . . . 28 Generalisiertes lineares Modell . . . 621
Escape-Befehl . . . . . . . . . . . . . . . 284, 443 Generische Funktion . . . . . . . . 349, 431
Excel-Datei . . . . . . . . . . . . . . . . . . . . . . 474 Gitternetz . . . . . . . . . . . . . . . . . . . . . . . 522
Explizite Typumwandlung . . . . . . . 135 Gleichungssystem . . . . . . . . . . . . . . . . 201
Exponentialfunktion . . . . . . . . . . . . . . 47 Gleichverteilung . . . . . . . . . . . . . . . . . 116
Globale Verfügbarkeit . . . . . . . . . . . . 344
F Globale Zuweisung . . . . . . . . . . . . . . . 422
Faktor . . . . . . . . . . . . . . . . . . . . . . . . . . . 312 Grafik . . . . . . . . . . . . . . . . . . . . . . . . . . . 507
Fakultät . . . . . . . . . . . . . . . . . . . . . . . . . . 82 Balkendiagramm . . . . . . . . . . . . 540
Farbpalette . . . . . . . . . . . . . . . . . . . . . . 554 Boxplot . . . . . . . . . . . . . . . . . . . . . 549
definieren . . . . . . . . . . . . . . . . . . . 555 Histogramm . . . . . . . . . . . . . . . . . 544
Farbraum Liniengrafik . . . . . . . . . . . . . . . . . 507
HCL-Farbraum . . . . . . . . . . . . . . 552 Matrixplot . . . . . . . . . . . . . . . . . . 536
HSV-Farbraum . . . . . . . . . . . . . . 553 Mosaicplot . . . . . . . . . . . . . . . . . . 585
RGB-Farbraum . . . . . . . . . . . . . 551 paarweise Streudiagramme . . 570
RGBA-Farbraum . . . . . . . . . . . . 552 Parallele Liniengrafik . . . . . . . . 536
Fehlender Wert. . . . . . . . . . . . . . .43, 159 QQ-Plot . . . . . . . . . . . . . . . . . . . . 593
ausschließen . . . . . . . . . . . . . . . . . . 90 Quantil-Quantil-Plot . . . . . . . . 593
Fehler . . . . . . . . . . . . . . . . . . . . . . . . . . . 629 Rastergrafik . . . . . . . . . . . . . . . . . 565
logischer Fehler. . . . . . . . . . . . . .629 speichern . . . . . . . . . . . . . . . . . . . . 566
semantischer Fehler . . . . . . . . . 629 Stabdiagramm . . . . . . . . . . . . . . 509
syntaktischer Fehler . . . . . . . . . 629 Streudiagramm . . . . . . . . . . . . . . 507
Fehler 1. Art . . . . . . . . . . . . . . . . . . . . . 119 Vektorgrafik . . . . . . . . . . . . . . . . . 565
Fehlermeldung . . . . . . . . . . . . . . . . . . . 403 Grafikelemente hinzufügen . . . . . . . 518
Fehlersuche . . . . . . . . . . . . . . . . . . . . . . 630 Grafikfenster. . . . . . . . . . . . . . . . . . . . .564
Fensterteilung . . . . . . . . . . . . . . . . . . . 567 Greedy Matching . . . . . . . . . . . . . . . . 291
for-Schleife. . . . . . . . . . . . . . . . . . . . . . .385 Großschreibung . . . . . . . . . . . . . . . . . . 265
Formale Argumente . . . . . . . . . . . . . . . 41
Formelobjekt . . . . . . . . . . . . . . . . 609, 611 H
Friedman-Test . . . . . . . . . . . . . . . . . . . 605 HCL-Farbraum . . . . . . . . . . . . . . . . . . 552
Funktion Hilfeoperator. . . . . . . . . . . . . . . . . . . . . .39
anonyme Funktion . . . . . . . . . . 424 Hintergrundfarbe . . . . . . . . . . . . . . . . 516
Dichtefunktion . . . . . . . . . . . . . . 113 Histogramm . . . . . . . . . . . . . . . . . . . . . 544
generische Funktion . . . . 349, 431 HSV-Farbraum . . . . . . . . . . . . . . . . . . 553
Grafikfunktion . . . . . . . . . . . . . . 507 html-Formatierung . . . . . . . . . . . . . . . 469
Mengenfunktion . . . . . . . . . . . . . . 71 Häufigkeitstabelle . . . . . . . . . . . . . . . . 143
658 Stichwortverzeichnis

lückenlose . . . . . . . . . . . . . . 146, 326 nach Spearman . . . . . . . . . . . . . . 602


Kovarianz. . . . . . . . . . . . . . . . . . . . . . . .106
I Kreuztabelle . . . . . . . . . . . . . . . . . . . . . 336
IDE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Kumulierte Mittelwerte . . . . . . . . . . 110
if-Anweisung . . . . . . . . . . . . . . . . . . . . . 381 Kumulierte Summe . . . . . . . . . . . . . . 100
Imaginäre Einheit . . . . . . . . . . . . . . . . 136 Kumuliertes Maximum . . . . . . . . . . . 102
Imaginärteil . . . . . . . . . . . . . . . . . . . . . 136 Kumuliertes Minimum . . . . . . . . . . . 102
Implizite Typumwandlung . . . . . . . 134
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 L
Indexing . . . . . . . . . . . . . . . . . . . . . . . . . . 25 Laufzeiten messen . . . . . . . . . . . . . . . 482
Indexreihenfolge . . . . . . . . . . . . . . . . . . 75 Layout. . . . . . . . . . . . . . . . . . . . . . . . . . . 567
Indexvektor . . . . . . . . . . . . . . . . . . . . . . . 16 Lazy Evaluation . . . . . . . . . . . . . . . . . 584
Indikatorfunktion . . . . . . . . . . . . . . . . 317 Leere Liste. . . . . . . . . . . . . . . . . . . . . . .208
Initialisierung . . . . . . . . . . . . . . . . . . 146 f. Leere Matrix. . . . . . . . . . . . . . . . . . . . .179
Integer. . . . . . . . . . . . . . . . . . . . . . . . . . .432 Leerer Plot . . . . . . . . . . . . . . . . . . . . . . 507
Interquartilsdistanz . . . . . . . . . . . . . . . 93 Leerer Vektor . . . . . . . . . . . . . . . . . 23, 27
Invertierung . . . . . . . . . . . . . . . . . . . . . 198 Leerstring . . . . . . . . . . . . . . . . . . . . . . . 128
Iris-Datensatz. . . . . . . . . . . . . . . . . . . .506 Legende . . . . . . . . . . . . . . . . . . . . . . . . . 519
Iteration . . . . . . . . . . . . . . . . . . . . 381, 386 Lexical Scoping . . . . . . . . . . . . . . . . . . 438
Lineare Regression . . . . . . . . . . 199, 608
J Lineares Gleichungssystem . . . . . . . 201
Join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 Lineares Modell . . . . . . . . . . . . . . . . . . 608
Linien einzeichnen . . . . . . . . . . . . . . . 527
K Liniengrafik . . . . . . . . . . . . . . . . . . . . 507 f.
Kartesisches Produkt . . . . . . . . . . . . 257 Linienstärke . . . . . . . . . . . . . . . . . . . . . 516
Kategorielle Variable . . . . . . . . . . . . . 312 Linientyp . . . . . . . . . . . . . . . . . . . . . . . . 516
Kategorisierung . . . . . . . . . . . . . . . . . . 317 Linkszuweisung . . . . . . . . . . . . . . . . . . . . 9
Kaufmännische Rundung . . . . . . . . . . 48 Liste
Kerndichteschätzer . . . . . . . . . . . . . . . 580 leere Liste . . . . . . . . . . . . . . . . . . . 208
Key. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .255 verschachtelte Liste. . . . . . . . . .209
Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . 348 Locale . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
Datums- und Uhrzeitklassen . 356 Logarithmus . . . . . . . . . . . . . . . . . . . . . . 47
Klassenattribut . . . . . . . . . . . . . . . . . . 247 Logische Operatoren . . . . . . . . . . . . . . 21
Kleinschreibung . . . . . . . . . . . . . . . . . . 265 Logischer Fehler . . . . . . . . . . . . . . . . . 629
Kolmogorov-Smirnov-Test. . . . . . . . 595 Logischer Vektor . . . . . . . . . . . . . . . . . . 19
Kommandozeile . . . . . . . . . . . . . . . . . . . . 5 Logistische Regression . . . . . . . . . . . 621
Kommentar . . . . . . . . . . . . . . . . . . . . . . . . 7 Lokale Verfügbarkeit . . . . . . . . . . . . . 344
Komplexe Zahl . . . . . . . . . . . . . . . . . . 136 Lookahead . . . . . . . . . . . . . . . . . . . . . . . 301
Komponentenweises Rechnen . . . . . . 46 Lookbehind . . . . . . . . . . . . . . . . . . . . . . 301
Konfidenzintervall. . . . . . . . . . . . . . . .119 Lückenlose Häufigkeitstabelle146, 326
Konstruktor . . . . . . . . . . . . . . . . . . . . . 433
Kontrast M
Treatmentkontrast . . . . . . . . . . 613 Mann-Whitney-U-Test . . . . . . . . . . . 605
Korrelation . . . . . . . . . . . . . . . . . . . . . . 106 Markup-Sprache . . . . . . . . . . . . . . . . . 626
Korrelationskoeffizient Masterdatei . . . . . . . . . . . . . . . . . . . . . . 625
nach Pearson . . . . . . . . . . . . . . . . 601 Masterfunktion . . . . . . . . . . . . . . . . . . 407
Stichwortverzeichnis 659

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

Suchmuster . . . . . . . . . . . . . . . . . . . . . . 266 Volles Modell . . . . . . . . . . . . . . . . . . . . 620


Symmetrische Differenz. . . . . . . .73, 87 Vollständiger Befehl . . . . . . . . . . . . . . . . 5
Syntaktischer Fehler . . . . . . . . . . . . . 629 Vorzeichenfunktion . . . . . . . . . . . . . . . . 49

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

A cbind {base}. . . . . . . . . . . . . . . .172, 244


abbreviate {base} . . . . . . . . . . . . . . 265 ceiling {base} . . . . . . . . . . . . . . . . . . . 48
abline {graphics}. . . . . . . . . . . . . . . .527 chisq.test {stats} . . . . . . . . . . . . . . 599
abs {base} . . . . . . . . . . . . . . . . . . . . . . . . 49 choose {base} . . . . . . . . . . . . . . . . . . . . 82
add1 {stats} . . . . . . . . . . . . . . . . . . . . . 614 class {base}. . . . . . . . . . . . . . . .247, 349
addmargins {stats} . . . . . . . . . . . . . . 338 cm.colors {grDevices} . . . . . . . . . . . 554
aggregate {stats} . . . . . . . . . . . . . . . 341 coef.lm {stats} . . . . . . . . . . . . . . . . . . 609
all {base} . . . . . . . . . . . . . . . . . . . . . . . . 50 col {base} . . . . . . . . . . . . . . . . . . . . . . . 188
all.equal {base}. . . . . . . . . . . . . . . .152 colMeans {base} . . . . . . . . . . . . 182, 249
anova {stats} . . . . . . . . . . . . . . . . . . . . 614 colnames {base} . . . . . . . . . . . . 174, 239
any {base} . . . . . . . . . . . . . . . . . . . . . . . . 50 colorRamp {grDevices} . . . . . . . . . . . 555
aov {stats} . . . . . . . . . . . . . . . . . . . . . . 621 colorRampPalette {grDevices} . . 555
append {base} . . . . . . . . . . . . . . . . . . . 225 colSums {base} . . . . . . . . . . . . . . . . . . 182
apply {base} . . . . . . . . . . . 228, 249, 337 column {shiny} . . . . . . . . . . . . . . . . . . 642
array {base} . . . . . . . . . . . . . . . . . . . . 486 confint.lm {stats} . . . . . . . . . . . . . . 609
arrows {graphics}. . . . . . . . . . . . . . . .527 contour {graphics} . . . . . . . . . . . . . . 585
as.character {base} . . . . . . . . . . . . 135 contrasts {stats} . . . . . . . . . . . . . . . 613
as.Date {base} . . . . . . . . . . . . . 357, 360 cor {stats} . . . . . . . . . . . . . . . . . . . . . . 106
as.formula {stats} . . . . . . . . . . . . . . 613 cor.test {stats} . . . . . . . . . . . . . . . . 602
as.integer {base} . . . . . . . . . . . . . . 433 cos {base} . . . . . . . . . . . . . . . . . . . . . . . . 50
as.list {base} . . . . . . . . . . . . . . . . . . 248 cov {stats} . . . . . . . . . . . . . . . . . . . . . . 106
as.logical {base} . . . . . . . . . . . . . . 135 crossprod {base}. . . . . . . . . . . . . . . .198
as.matrix {base}. . . . . . . . . . . . . . . .241 cummax {base} . . . . . . . . . . . . . . . . . . . 102
as.numeric {base} . . . . . . . . . . . . . . 135 cummin {base} . . . . . . . . . . . . . . 102, 107
as.ordered {base} . . . . . . . . . . . . . . 322 cumprod {base} . . . . . . . . . . . . . . . . . . 102
as.POSIXct {base} . . . . . . . . . . 357, 360 cumsum {base} . . . . . . . . . . . . . . . . . . . 101
as.POSIXlt {base} . . . . . . . . . . 357, 360 cut {base} . . . . . . . . . . . . . . . . . . . . . . . 318
as.vector {base}. . . . . . . . . . . . . . . .185
attach {base} . . . . . . . . . . . . . . . . . . . 344 D
attributes {base} . . . . . . . . . . . . . . 351 data.frame {base} . . . . . . . . . . . . . . 238
axis {graphics} . . . . . . . . . . . . . . . . . . 522 dataEllipse {car}. . . . . . . . . . . . . . .601
debug {base} . . . . . . . . . . . . . . . . . . . . 633
B debugonce {base}. . . . . . . . . . . . . . . .634
barplot {graphics} . . . . . . . . . . . . . . 540 density {stats} . . . . . . . . . . . . . . . . . . 580
binom.test {stats} . . . . . . . . . . . . . . 596 deployApp {rsconnect} . . . . . . . . . . . 640
box {graphics} . . . . . . . . . . . . . . . . . . . 522 det {base} . . . . . . . . . . . . . . . . . . . . . . . 198
boxplot {graphics} . . . . . . . . . . . . . . 549 detach {base} . . . . . . . . . . . . . . . . . . . 344
browser {base} . . . . . . . . . . . . . . . . . . 632 dev.cur {grDevices} . . . . . . . . . . . . . 565
by {base} . . . . . . . . . . . . . . . . . . . . . . . . 346 dev.new {grDevices} . . . . . . . . 538, 564
dev.off {grDevices} . . . . . . . . . . . . . 565
C dev.set {grDevices} . . . . . . . . . . . . . 565
c {base} . . . . . . . . . . . . . . . . . . . . . 15, 211 diag {base}. . . . . . . . . . . . . . . . . . . . . .195
cat {base} . . . . . . . . 285, 434, 442, 472 diff {base} . . . . . . . . . . . . . . . . . 106, 364
© 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
Funktionsverzeichnis 663

difftime {base} . . . . . . . . . . . . . . . . . 365 hist {graphics} . . . . . . . . . . . . . 544, 592


dim {base} . . . . . . . . . . . . . . . . . . . . . . . 174
dir.create {base}. . . . . . . . . . . . . . . .67 I
dir.exists {base}. . . . . . . . . . . . . . . .67 identify {graphics} . . . . . . . . . . . . . 585
dnorm {stats} . . . . . . . . . . . . . . . . . . . . 114 identity {base} . . . . . . . . . . . . . . . . . 335
drop1 {stats} . . . . . . . . . . . . . . . . . . . . 614 ifelse {base} . . . . . . . . . . . . . . 104, 384
Im {base} . . . . . . . . . . . . . . . . . . . . . . . . 136
E image {graphics} . . . . . . . . . . . . . . . . . 585
eigen {base} . . . . . . . . . . . . . . . . . . . . 205 inherits {base} . . . . . . . . . . . . . . . . . 435
Encoding {base} . . . . . . . . . . . . . . . . . 461 install.packages {utils}. . . . . . . . .12
eval {base}. . . . . . . . . . . . . . . . . . . . . .496 intersect {base} . . . . . . . . . . . . . . . . . 74
exp {base} . . . . . . . . . . . . . . . . . . . . . . . . 47 invisible {base}. . . . . . . . . . . . . . . .400
is.atomic {base}. . . . . . . . . . . . . . . .210
F
is.character {base} . . . . . . . . . . . . 133
factor {base} . . . . . . . . . . . . . . 313, 327
is.data.frame {base} . . . . . . . . . . . 247
factorial {base} . . . . . . . . . . . . . . . . . 82
is.factor {base}. . . . . . . . . . . . . . . .315
file.copy {base} . . . . . . . . . . . . . . . . . 67
is.finite {base} . . . . . . . . . . . . . . 164 f.
file.crate {base}. . . . . . . . . . . . . . . .67
is.infinite {base} . . . . . . . . . . . 164 f.
file.exists {base} . . . . . . . . . . . . . . 67
is.list {base} . . . . . . . . . . . . . . . . . . 205
file.remove {base} . . . . . . . . . . . . . . 67
is.logical {base} . . . . . . . . . . . . . . 133
file.rename {base} . . . . . . . . . . . . . . 67
is.matrix {base}. . . . . . . . . . . . . . . .176
fisher.test {stats} . . . . . . . . . . . . . 600
is.na {base} . . . . . . 160, 188, 252, 327
fitdistr {MASS} . . . . . . . . . . . . . . . 592
is.nan {base} . . . . . . . . . . . . . . . . . . . 165
floor {base} . . . . . . . . . . . . . . . . . . . . . 48
is.null {base} . . . . . . . . . . . . . . . . . . 159
fluidRow {shiny} . . . . . . . . . . . . . . . . 642
is.numeric {base} . . . . . . . . . . . . . . 133
format {base} . . . . . . . . . . . . . . 357, 443
is.ordered {base} . . . . . . . . . . . . . . 323
format.Date {base} . . . . . . . . . . . . . 359
is.recursive {base} . . . . . . . . . . . . 210
format.POSIXct {base} . . . . . . . . . . 357
is.vector {base} . . . . . . . . . . . 176, 210
formatC {base} . . . . . . . . . . . . . . . . . . 444
friedman.test {stats} . . . . . . . . . . 605
K
ftable {stats} . . . . . . . . . . . . . . . . . . . 338
knit {knitr} . . . . . . . . . . . . . . . . . . . . . 628
G ks.test {stats} . . . . . . . . . . . . . . . . . . 595
geometric.mean {psych} . . . . . . . . 593
getwd {base} . . . . . . . . . . . . . . . . . . . . . 64 L
glm {stats} . . . . . . . . . . . . . . . . . . . . . . 621 lapply() {base} . . . . . . . . . . . . . . . . . 216
graphics.off {grDevices} . . . . . . . 565 layout {graphics}. . . . . . . . . . . . . . . .571
gregexpr {base} . . . . . . . . . . . . . . . . . 271 layout.show {graphics}. . . . . . . . . .572
grep {base}. . . . . . . . . . . . . . . . . . . . . .267 LcKS {KScorrect} . . . . . . . . . . . . . . . . 595
grepl {base} . . . . . . . . . . . . . . . . . . . . 267 lcm {graphics} . . . . . . . . . . . . . . . . . . . 572
grid {graphics} . . . . . . . . . . . . . . . . . . 522 legend {graphics}. . . . . . . . . . . . . . . .519
gsub {base}. . . . . . . . . . . . . . . . . . . . . .276 length {base} . . . . . . 15, 174, 206, 239
lengths {base} . . . . . . . . . . . . . . . . . . 214
H levels {base} . . . . . . . . . . . . . . . . . . . 315
hcl {grDevices} . . . . . . . . . . . . . . . . . . 552 library {base} . . . . . . . . . . . . . . . . . . . 12
head {utils} . . . . . . . . . . . . . . . . . . . . . . 240 lines {graphics} . . . . . . . . . . . . . . . . . 518
heat.colors {grDevices} . . . . . . . . 554 list {base}. . . . . . . . . . . . . . . . . . . . . .209
664 Funktionsverzeichnis

list.files {base} . . . . . . . . . . . 66, 470 P


lm {stats} . . . . . . . . . . . . . . . . . . . . . . . . 608 pairs {graphics} . . . . . . . . . . . . . . . . . 570
load {base} . . . . . . . . . . . . . . . . . . . . . . . 65 par {graphics} . . . . . . . . . . . . . . 512, 567
locator {graphics} . . . . . . . . . . . . . . 585 parse {base} . . . . . . . . . . . . . . . . . . . . 496
log {base} . . . . . . . . . . . . . . . . . . . . . . . . 47 paste {base}. . . . . . . . . . . . . . . .127, 296
ls {base} . . . . . . . . . . . . . . . . . . . . . . . . . 62 paste0 {base} . . . . . . . . . . . . . . . . . . . 129
plot {graphics} . . . . . . . . . . . . . . . . . . 507
M plot.default {base} . . . . . . . . . . . . 507
mainPanel {shiny} . . . . . . . . . . . . . . . 643 plot.lm {stats} . . . . . . . . . . . . . . . . . . 609
mantelhaen.test {stats} . . . . . . . . 600 pmax {base}. . . . . . . . . . . . . . . . . . . . . .103
mapply {base} . . . . . . . . . . . . . . . . . . . 223 pmin {base} . . . . . . . . . . . . . . . . . 103, 187
margin.table {base} . . . . . . . . . . . . 338 pnorm {stats} . . . . . . . . . . . . . . . . . . . . 114
matlines {graphics} . . . . . . . . . . . . . 537 points {graphics}. . . . . . . . . . . . . . . .518
matplot {graphics} . . . . . . . . . . . . . . 537 polygon {graphics} . . . . . . . . . . . . . . 524
matpoints {graphics} . . . . . . . . . . . . 537 predict.lm {stats} . . . . . . . . . 609, 618
matrix {base} . . . . . . . . . . . . . . . . . . . 171 pretty {base} . . . . . . . . . . . . . . . . . . . 522
max {base} . . . . . . . . . . . . . . . . . . . . . . . . 90 print {base} . . . . . . . . . . . . . . . . . . . . 442
mcnemar.test {stats} . . . . . . . . . . . . 598 prod {base} . . . . . . . . . . . . . . . . . . . . . . . 82
mean {base} . . . . . . . . . . . . . . . . . . . 25, 92 prop.table {base} . . . . . . . . . . . . . . 339
median {stats} . . . . . . . . . . . . . . . . . . . . 93 prop.test {stats} . . . . . . . . . . . . . . . 597
merge {base} . . . . . . . . . . . . . . . . . . . . 256
Q
methods {utils} . . . . . . . . . . . . . . . . . . 349
qnorm {stats} . . . . . . . . . . . . . . . . . . . . 114
min {base} . . . . . . . . . . . . . . . . . . . . . . . . 90
qqline {stats} . . . . . . . . . . . . . . . . . . . 593
missing {base} . . . . . . . . . . . . . . . . . . 408
qqPlot {car} . . . . . . . . . . . . . . . . . . . . 594
mode {base}. . . . . . . . . . . . . . . . . . . . . .133
qqplot {stats} . . . . . . . . . . . . . . . . . . . 593
model.matrix {stats} . . . . . . . . . . . . 613
qr {base} . . . . . . . . . . . . . . . . . . . 202, 214
months {base} . . . . . . . . . . . . . . . . . . . 359
quantile {stats}. . . . . . . . . . . . . . . . . . 93
mosaicplot {graphics} . . . . . . . . . . . 585
quarters {base} . . . . . . . . . . . . . . . . . 359
mtext {graphics} . . . . . . . . . . . . . . . . . 518
mvrnorm {MASS} . . . . . . . . . . . . . . . . 202 R
rainbow {grDevices} . . . . . . . . . . . . . 554
N range {base} . . . . . . . . . . . . . . . . . . . . . 90
na.omit {base} . . . . . . . . . . . . . . . . . . 167 rank {base} . . . . . . . . . . . . . . . . . . 76, 127
names {base} . . . . . . . . . . . 140, 206, 239 rbind {base}. . . . . . . . . . . . . . . .172, 258
navbarPage {shiny} . . . . . . . . . . . . . . 643 Re {base} . . . . . . . . . . . . . . . . . . . . . . . . 136
navlistPanel {shiny} . . . . . . . . . . . 643 reactive {shiny} . . . . . . . . . . . . . . . . 640
nchar {base} . . . . . . . . . . . . . . . . . . . . 265 read.csv {utils} . . . . . . . . . . . . . . . . . 458
ncol {base}. . . . . . . . . . . . . . . . . . . . . .174 read.csv2 {utils} . . . . . . . . . . . . . . . . 458
nlevels {base} . . . . . . . . . . . . . . . . . . 315 read.spss {foreign} . . . . . . . . . . . . . 475
nrow {base}. . . . . . . . . . . . . . . . . . . . . .174 read.table {utils} . . . . . . . . . . 311, 455
read.xlsx {xlsx} . . . . . . . . . . . . . . . . 474
O Recall {base} . . . . . . . . . . . . . . . . . . . 426
options {base}. . . . . . . . . .83, 150, 461 recover {utils} . . . . . . . . . . . . . . . . . . 634
order {base} . . . . . . . . . . . . 75, 127, 363 rect {graphics} . . . . . . . . . . . . . . . . . . 526
outer {base} . . . . . . . . . . . . . . . . . . . . 233 regexpr {base} . . . . . . . . . . . . . . . . . . 269
Funktionsverzeichnis 665

regmatches {base} . . . . . . . . . . . . . . 291 subset {base} . . . . . . . . . . . . . . 163, 243


render {rmarkdown} . . . . . . . . . . . . . 628 substitute {base} . . . . . . . . . . . . . . 584
renderPlot {shiny} . . . . . . . . . . . . . . 641 substring {base}. . . . . . . . . . . . . . . .273
rep {base} . . . . . . . . . . . . . . . . . . . . . . . . 45 sum {base} . . . . . . . . . . . . . . . . . . . . . . . . 24
return {base} . . . . . . . . . . . . . . . . . . . 400 summary {base} . . . . . . . . . . . . . . . . . . . 94
rev {base} . . . . . . . . . . . . . . . . . . . . . . . . 78 summary.lm {stats} . . . . . . . . . . . . . . 609
rgb {grDevices} . . . . . . . . . . . . . . . . . . 551 suppressWarnings {base} . . . . . . . 450
rle {base} . . . . . . . . . . . . . . . . . . . . . . . 353 svd {base} . . . . . . . . . . . . . . . . . . . . . . . 202
rm {base} . . . . . . . . . . . . . . . . . . . . 63, 418 sweep {base} . . . . . . . . . . . . . . . . . . . . 231
RNGversion {base} . . . . . . . . . . . . . . 117 switch {base} . . . . . . . . . . . . . . . . . . . 409
rnorm {stats} . . . . . . . . . . . . . . . . . . . . 114 Sys.Date {base} . . . . . . . . . . . . . . . . . 356
round {base} . . . . . . . . . . . . . . . . . . . . . 48 Sys.time {base} . . . . . . . . . . . . 356, 480
row {base} . . . . . . . . . . . . . . . . . . . . . . . 188 system.time {base} . . . . . . . . . . . . . 482
rowMeans {base} . . . . . . . . . . . . . . . . . 182
rownames {base} . . . . . . . . . . . . . . . . . 174 T
rowSums {base} . . . . . . . . . . . . . . . . . . 182 t {base} . . . . . . . . . . . . . . . . . . . . . . . . . 175
runif {stats} . . . . . . . . . . . . . . . . . . . . 116 t.test {stats} . . . . . . . . . . . . . . 121, 603
table {base} . . . . . . 143, 161, 326, 336
S tabPanel {shiny} . . . . . . . . . . . . . . . . 643
sample() {base} . . . . . . . . . . . . . . . . . 118 tabsetPanel {shiny} . . . . . . . . 643, 647
sapply {base}. . . . . . . . . .216, 221, 249 tail {utils} . . . . . . . . . . . . . . . . . . . . . . 240
save {base} . . . . . . . . . . . . . . . . . . . . . . . 65 tan {base} . . . . . . . . . . . . . . . . . . . . . . . . 50
savePlot {grDevices} . . . . . . . . . . . . 566 tapply {base} . . . . . . . . . . . . . . 333, 339
scale {base} . . . . . . . . . . . . . . . . . . . . 232 tcrossprod {base} . . . . . . . . . . . . . . 198
scan {base} . . . . . . . . . . . . . . . . . 446, 469 terrain.colors {grDevices} . . . . . 554
sd {stats} . . . . . . . . . . . . . . . . . . . . . . . . . 92 text {graphics} . . . . . . . . . . . . . . . . . . 518
segments {graphics} . . . . . . . . . . . . . 527 title {graphics} . . . . . . . . . . . . . . . . . 528
seq {base} . . . . . . . . . . . . . . . . . . . . . . . . 40 titlePanel {shiny} . . . . . . . . . 643, 646
seq_along {base} . . . . . . . . . . . . . . . . . 52 tolower {base} . . . . . . . . . . . . . . . . . . 265
set.seed() {base} . . . . . . . . . . . . . . 117 topo.colors {grDevices} . . . . . . . . 554
setAccountInfo {rsconnect} . . . . . 640 toupper {base} . . . . . . . . . . . . . . . . . . 265
setwd {base} . . . . . . . . . . . . . . . . . 64, 457 traceback {base}. . . . . . . . . . . . . . . .632
shinyApp {shiny} . . . . . . . . . . . . . . . . 639 trunc {base} . . . . . . . . . . . . . . . . . . . . . 48
sidebarLayout {shiny} . . . . . 643, 646
sidebarPanel {shiny} . . . . . . . . . . . 643 U
sign {base} . . . . . . . . . . . . . . . . . . . . . . . 49 unclass {base} . . . . . . . . 248, 314, 351
sin {base} . . . . . . . . . . . . . . . . . . . . . . . . 50 undebug {base} . . . . . . . . . . . . . . . . . . 634
sliderInput {shiny}. . . . . . . . . . . . .644 union {base} . . . . . . . . . . . . . . . . . . . . . 74
solve {base} . . . . . . . . . . . . . . . . . . . . 198 unique {base} . . . . . . . . . . . . . . . . . . . . 73
sort {base} . . . . . . . . 75, 127, 161, 363 unlist {base} . . . . . . . . . . . . . . . . . . . 212
source {base} . . . . . . . . . . . . . . . . . . . 624 update {stats} . . . . . . . . . . . . . . . . . . . 614
step {stats} . . . . . . . . . . . . . . . . 614, 620 UseMethod {base}. . . . . . . . . . . . . . . .349
stop {base}. . . . . . . . . . . . . . . . . . . . . .403
str {utils} . . . . . . . . . . . . . . . . . . 205, 240 V
strsplit {base} . . . . . . . . . . . . . . . . . 278 vapply {base} . . . . . . . . . . . . . . . . . . . 223
sub {base} . . . . . . . . . . . . . . . . . . . . . . . 276 var {stats}. . . . . . . . . . . . . . . . . . . . . . . .92
666 Funktionsverzeichnis

var.test {stats} . . . . . . . . . . . . . . . . 605 which {base} . . . . . . . . . . . . . . . . . . . . . 26


vcov.lm {stats} . . . . . . . . . . . . . . . . . . 609 wilcox.test {stats} . . . . . . . . . . . . . 604
vector {base} . . . . . . . . . . . . . . . . . . . 208 with {base}. . . . . . . . . . . . . . . . . . . . . .344
write.csv {utils} . . . . . . . . . . . . . . . . 458
W write.csv2 {utils}. . . . . . . . . . . . . . . 458
warning {base} . . . . . . . . . . . . . . . . . . 403 write.table {utils} . . . . . . . . . . . . . 458
weekdays {base} . . . . . . . . . . . . . . . . . 359
weighted.mean {stats} . . . . . . 145, 409 X
wellPanel {shiny} . . . . . . . . . . . . . . . 643 xor {base} . . . . . . . . . . . . . . . . . . . . . . . . 50
Operatoren- und Konstantenverzeichnis 667

Operatoren- und Konstantenverzeichnis

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

4.1 Blanker und ausgefüllter Lottoschein von Lotto 6 aus 45 . . . . . . . 53


4.2 Tippscheine von Lotto 6 aus 45 und Lotto 60 aus 450 . . . . . . . . 54
4.3 Lottoschein der Lotterie 6 aus 49 . . . . . . . . . . . . . . . . . . . . 56
6.1 Ausgefüllter Lottoschein von Lotto 6 aus 45 . . . . . . . . . . . . . . 70
6.2 Gewinnzahlen der Lotterie 6 aus 45 . . . . . . . . . . . . . . . . . . . 70
6.3 Veranschaulichung der Funktion order() . . . . . . . . . . . . . . . 76
6.4 Veranschaulichung der Funktion rank() . . . . . . . . . . . . . . . . 77
7.1 Visualisierung der Apfelstichprobe . . . . . . . . . . . . . . . . . . . 96
8.1 Kumulierte Summen der Schlagzahlen beider Spieler . . . . . . . . . 100
8.2 Visualisierung der Funktion ifelse() beim Minigolfbeispiel . . . . . 105
8.3 Vergleich und Differenzen zweier benachbarter Elemente . . . . . . . 105

10.1 Die Parameter sep und collapse der Funktion paste() . . . . . . . 129

15.1 Visualisierung des Parameters byrow der Funktion matrix() . . . . 172


16.1 Visualisierung der Regression aus (16.3.1) . . . . . . . . . . . . . . . 200

24.1 Der vollständige Datensatz Vertreter.txt in der Editoransicht . . . 310


24.2 Codierungen im Datensatz Vertreter.txt . . . . . . . . . . . . . . 312
24.3 Umcodierung bei der Faktorbildung in R . . . . . . . . . . . . . . . . 313
24.4 Noten als Strings und ihre zugehörige Zahl . . . . . . . . . . . . . . 324
25.1 Das Dataframe mit den Vertreterdaten . . . . . . . . . . . . . . . . . 333

28.1 R-Code für den zellulären Automaten . . . . . . . . . . . . . . . . . 395


29.1 R-Code und Anwendungsbeispiel für den Bisektionsalgorithmus . . . 415

31.1 R-Code für das Mathequiz . . . . . . . . . . . . . . . . . . . . . . . . 449


31.2 R-Code für das Mathequiz mit Absicherung . . . . . . . . . . . . . . 451
32.1 Auszug aus der Datei Vertreter1.txt . . . . . . . . . . . . . . . . . 456
32.2 Datei Vertreter2.txt . . . . . . . . . . . . . . . . . . . . . . . . . . 463
32.3 Datei Vertreter2.csv . . . . . . . . . . . . . . . . . . . . . . . . . . 464
32.4 Datei Vertreter_Labels.txt . . . . . . . . . . . . . . . . . . . . . . 471

33.1 Dichtefunktion einer symmetrischen Dreiecksverteilung . . . . . . . . 483


© 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
Abbildungsverzeichnis 669

33.2 Funktion sim1() für das Würfelwurfbeispiel (33.5) . . . . . . . . . . 490


33.3 Funktion sim2() für das Würfelwurfbeispiel (33.5) . . . . . . . . . . 491
33.4 Funktion sim3() für das Würfelwurfbeispiel (33.5) . . . . . . . . . . 492
33.5 Funktion sim4() für das Würfelwurfbeispiel (33.5) . . . . . . . . . . 494
33.6 Funktion sim5() für das Würfelwurfbeispiel (33.5) . . . . . . . . . . 495
33.7 Drei zufällige Dreiecke . . . . . . . . . . . . . . . . . . . . . . . . . . 497
33.8 Flächeninhalt von Dreiecken . . . . . . . . . . . . . . . . . . . . . . . 498

34.1 Funktion zur Bestimmung von kartesischen Kreiskoordinaten . . . . 530


34.2 Polarkoordinaten und Darstellung der aktuellen Uhrzeit . . . . . . . 530
34.3 Funktion zur Anzeige der aktuellen Systemzeit . . . . . . . . . . . . 531
35.1 Darstellung des HCL-Farbraumes . . . . . . . . . . . . . . . . . . . . 553
36.1 Rastergrafiken und Vektorgrafiken nach einer Skalierung . . . . . . . 567
36.2 Funktion für Streudiagrammen mit diversen Randverteilungen . . . 582
36.3 Streudiagramme mit diversen Randverteilungen . . . . . . . . . . . . 583

37.1 Histogramm für das Einkommen mit Log-Normalverteilung . . . . . 592


37.2 QQ-Plot für das Einkommen mit Log-Normalverteilung . . . . . . . 594
37.3 QQ-Plot für das Einkommen mit Konfidenzintervallen . . . . . . . . 595
37.4 Scatterplot mit 95%-Bereich der bivariaten Normalverteilung . . . . 601
38.1 Diagnostische Plots für lineares Regressionsmodell . . . . . . . . . . 616
38.2 Streudiagramm und simple Vorhersagemodelle im Regressionsbeispiel 618
38.3 Bessere Vorhersagemodelle im Regressionsbeispiel . . . . . . . . . . . 620
39.1 PDF-Dokument für beispiel.Rnw . . . . . . . . . . . . . . . . . . . 628
40.1 Schieberegler in einem Sidebar-Panel . . . . . . . . . . . . . . . . . . 644
40.2 Shiny-Anwendung zum Beispiel (Version 1) . . . . . . . . . . . . . . 646
40.3 Shiny-Anwendung zum Beispiel (Version 2) . . . . . . . . . . . . . . 649
670 Tabellenverzeichnis

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

4.1 Parameter der Funktion seq() . . . . . . . . . . . . . . . . . . . . . 40


4.2 Parameter der Funktion rep() . . . . . . . . . . . . . . . . . . . . . 45
4.3 Aggregierende Logikfunktionen . . . . . . . . . . . . . . . . . . . . . 50
5.1 Funktionen zur Manipulation von Dateien und Ordnern . . . . . . . 67
6.1 Steuerung des Tie-Verhaltens bei rank() . . . . . . . . . . . . . . . . 78
6.2 Fußballspielerinnen: Namen und Geburtsdaten . . . . . . . . . . . . 78
6.3 Fußballspielerinnen: Namen und Geburtsdaten nach Alter sortiert . . 80
6.4 Fußballspielerinnen: Namen und Geburtsdaten sowie Altersränge . . 81
7.1 Wichtige Funktionen der deskriptiven Statistik . . . . . . . . . . . . 90
9.1 Konventionelle Buchstabenkürzel für Verteilungen in R . . . . . . . . 114
9.2 Übersicht über einige wichtige Verteilungen in R . . . . . . . . . . . 116

12.1 Lückenhafte Häufigkeitstabelle für die Anzahl der Geschwister . . . . 139


14.1 Bedeutende Konstante und Abfrage auf Gleichheit . . . . . . . . . . 166

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

22.1 Einfache Stringfunktionen . . . . . . . . . . . . . . . . . . . . . . . . 265


22.2 Ausgewählte Parameter der Funktionen grepl() und grep() . . . . 267
22.3 Zusammenfassung der Stringfunktionen . . . . . . . . . . . . . . . . 279
23.1 Lookaheads und Lookbehinds im Überblick . . . . . . . . . . . . . . 301
23.2 Zusammenfassung der Syntax bei Regular Expressions . . . . . . . . 306
24.1 Gewinnkategorien beim Vertreterbeispiel . . . . . . . . . . . . . . . . 317
24.2 Parameter der Funktion cut() . . . . . . . . . . . . . . . . . . . . . 319
25.1 Parameter der Funktion tapply() . . . . . . . . . . . . . . . . . . . 334
25.2 Parameter der Funktion aggregate() . . . . . . . . . . . . . . . . . 342
27.1 Ausgewählte Platzhalter für Datums- und Uhrzeitobjekte . . . . . . 358

32.1 Ausgewählte Parameter der Funktion read.table() . . . . . . . . . 456

34.1 Wichtige Parameter der Funktion plot() . . . . . . . . . . . . . . . 507


34.2 Ausgewählte Grafikparameter von par() . . . . . . . . . . . . . . . . 512

© 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

34.3 Ausgewählte Parameter der Funktion legend() . . . . . . . . . . . . 520


34.4 Ausgewählte Parameter der Funktion axis() . . . . . . . . . . . . . 522
34.5 Ausgewählte Parameter der Funktion polygon() . . . . . . . . . . . 525
34.6 Ausgewählte Parameter der Funktion arrows() . . . . . . . . . . . . 527
34.7 Parameter der Funktion title() . . . . . . . . . . . . . . . . . . . . 529
35.1 Ausgewählte Parameter der Funktion barplot() . . . . . . . . . . . 540
35.2 Ausgewählte Parameter der Funktion hist() . . . . . . . . . . . . . 544
36.1 Parameter der Funktion layout() . . . . . . . . . . . . . . . . . . . 571

37.1 Parameter der Funktion binom.test() . . . . . . . . . . . . . . . . . 596


37.2 Hypothesentests in R . . . . . . . . . . . . . . . . . . . . . . . . . . . 606
40.1 Render-Funktionen zur Erstellung von Outputelementen . . . . . . . 641
40.2 Liste der Inputelemente und der dazugehörigen Funktionen . . . . . 645
40.3 Outputfunktionen mit entsprechenden Render-Funktionen . . . . . . 645
672 Infoboxenverzeichnis

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

1 Kommentiere deinen R-Code! . . . . . . . . . . . . . . . . . . . . . . . . . 8


2 Verwende kurze und sprechende Objektnamen! . . . . . . . . . . . . . . . 30
3 Verwende möglichst keine Umlaute (ä, ö, ü) sowie ß. . . . . . . . . . . . . 31
4 Programmiere möglichst allgemein und automatisiert! . . . . . . . . . . . 31
5 Exakte Benennung nach dem Dreipunkteargument . . . . . . . . . . . . . 44
6 Hinterfrage kritisch deine (impliziten) Annahmen! . . . . . . . . . . . . . 57
7 Führe den Befehl rm(list = ls()) ganz zu Beginn aus! . . . . . . . . . . 63
8 Achte immer auf Ausreißer! . . . . . . . . . . . . . . . . . . . . . . . . . . 94
9 Vermeide wiederholte Berechnungen! . . . . . . . . . . . . . . . . . . . . . 109
10 Baue bei Simulationen set.seed() und RNGversion() ein! . . . . . . . . 117
11 Bevorzuge beim Subsetting Names gegenüber Indizes! . . . . . . . . . . . 141
12 Verwende stets TRUE und FALSE anstelle von T und F! . . . . . . . . . . . 158
13 Verwende Leerzeichen, um die Lesbarkeit zu verbessern! . . . . . . . . . . 190
14 Zugriffsregeln bei Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
15 Achte auf korrekte Suchmuster . . . . . . . . . . . . . . . . . . . . . . . . 293

16 Greife in Funktionen nicht auf Objekte außerhalb zu! . . . . . . . . . . . . 417


17 Initialisiere deine Objekte sinnvoll! . . . . . . . . . . . . . . . . . . . . . . 482
18 Verwende vektorwertige und spezialisierte Funktionen! . . . . . . . . . . . 487
19 Senke die Anzahl der Schleifendurchläufe! . . . . . . . . . . . . . . . . . . 488

© 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

Das könnte Ihnen auch gefallen