Uploaded by jasmin-seuss

2019 Book ModerneDatenanalyseMitR

advertisement
Sebastian Sauer
Moderne
Datenanalyse mit R
Daten einlesen, aufbereiten,
visualisieren, modellieren
und kommunizieren
FOM-Edition
FOM Hochschule für Oekonomie & Management
Reihenherausgeber
FOM Hochschule für Oekonomie & Management, Essen, Deutschland
Dieses Werk erscheint in der FOM-Edition, herausgegeben von der FOM Hochschule für
Oekonomie & Management.
Weitere Bände in der Reihe
http://www.springer.com/series/12753
Sebastian Sauer
Moderne
Datenanalyse mit R
Daten einlesen, aufbereiten,
visualisieren, modellieren
und kommunizieren
Sebastian Sauer
FOM Hochschule für Oekonomie &
Management
Nürnberg, Deutschland
FOM-Edition
ISBN 978-3-658-21586-6
https://doi.org/10.1007/978-3-658-21587-3
ISBN 978-3-658-21587-3 (eBook)
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
Springer Gabler
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
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 Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt
auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichenund Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
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.
Springer Gabler 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
Vorwort
Wir fühlen, dass selbst, wenn alle möglichen wissenschaftlichen Fragen beantwortet sind,
unsere Lebensprobleme noch gar nicht berührt sind.
– Wittgenstein, Tractatus, 6.52
Dystopie eines Datenzeitalters
Wir leben im frühen Zeitalter der Daten und Algorithmen, dem Algorithmozän. Gebetsmühlenartig haben uns Unternehmensberatungen, Politiker und Google-Ingenieure dieses
Mantra vorgetragen, so dass es längt zum Fundus säuberlich abgehefteter Binsenweisheiten gehört (vgl. Chui et al. 2018). Untermalt wird dieses sonore Flüstern durch ein
Stakkato von Eilmeldungen wie kürzlich von AlphaGo Zero (D. Silver et al. 2017b). Das
ist ein Programm, das sehr gut im Brettspiel Go ist (und wohl auch in einigen anderen
Spielen laut D. Silver et al. (2017a)). Wie sein Vorgänger aus dem letzten Jahr, AlphaGo,
basiert das Programm auf sog. neuronalen Netzen, einer wohlbekannten Methode des statistischen Modellierens (Scherer 2013). Das neue Programm spielt deutlich besser als das
alte: Bei einem gemütlichen Treffen schlug der Neue den Alten vernichtend: Mit 100 zu 0
fegte der Frischling den alten Hasen vom Platz. Dabei hatte der Alte einiges vorzuweisen.
Im Vorjahr hatte er einen Meister des Go-Spiels, einen Menschen, klar besiegt (D. Silver
et al. 2017b). Interessant ist, dass AlphaGo Zero ohne Lernmaterial von außen auskam, im
Gegensatz zu früheren Programmen wie AlphaGo. Das legt nahe, dass Maschinen grundsätzlich in der Lage sind, ohne Hilfe von Menschen zu lernen – und „übermenschliche“
Leistung im Go-Spiel und vielleicht auch anderswo zu erzielen.
Ähnlich spektakulär: Eine Reihe von Fachartikeln zeigte, dass Algorithmen – ausreichend mit Daten gefüttert – die Persönlichkeit einer Person besser einschätzen können
als deren Freunde (Kosinski et al. 2013; Quercia, Kosinski, Stillwell, & Crowcroft 2011;
Youyou et al. 2015). Auch hier wurden moderne Modelle des statistischen Modellierens
verwendet. Auf analoge Art schlägt Ihnen ein Algorithmus auf einer Webseite vor, für
welche Produkte Sie sich noch interessieren könnten. Nicht immer ist der Algorithmus
auch nur ansatzweise clever: Wer hat noch nicht erlebt, im Internet einen Gegenstand erworben zu haben, ein Fahrrad zum Beispiel, und danach noch wochenlang von Werbung
für Fahrräder drangsaliert zu werden (mir reicht ein Rad, Google).
V
VI
Vorwort
Bei aller Dystopie, die mit der Digitalisierung zusammenhängt und mit Chinas „Kreditwürdigkeitspunkten“ bisher am konsequentesten weitergedacht wurde (Botsman 2017),
es gibt auch Nutzen. Moderne Daten-Technologien stecken im Smartphone, in medizinischen Anwendungen und in deutschen Autos. Wer möchte auf diesen Fortschritt verzichten? Die wenigsten offenbar.
Diese Beispiele ließen sich noch länger mit Unterhaltungswert fortsetzen. Wie man
den Fortschritt der Algorithmen auch einschätzt – wünschenswert, durchwachsen oder
bedrohlich – man muss zum gleichen Schluss kommen: An der intensiven Beschäftigung
mit dieser Technik kommen wir (jeder Einzelne) nicht vorbei. Ein Baustein, um die unheimliche Bedrohung durch panoptische, orwelleske Überwachung abzuwenden, ist das
Verständnis der modernen Datentechnik. Gleichermaßen gilt: Um die offenbar gewaltigen
ökonomischen Potenziale für die Unternehmen urbar zu machen, müssen wir die Technik verstehen (Brynjolfsson und McAfee 2016). Die Digitalisierung ist der bestimmende
Trend des Wirtschaftslebens – wahrscheinlich (Vorsicht mit Vorhersagen); daher ist es beruflich, gesellschaftlich und politisch geboten, sich dem Algorithmozän zu stellen. Das
heißt nicht, dass jeder Programmierer und Statistiker werden muss. Aber ein gewisses
Grundverständnis sollte zum Bildungsstandard gehören.
Das Statistikcurriculum ist veraltet
Die Lehrpläne der Hochschulen geben sich von dramatischen Meldungen und neuen Technologien noch weitgehend unbescholten. Zumeist gilt in Lehrplänen für Statistik: Über
den t-Test geht nichts. Der Wirklichkeit außerhalb der Alma Mater wird das kaum gerecht.
Die Gründe für diese Gemächlichkeit können darin liegen, dass sich einige Hochschullehrer1 mit neuen Technologien schwertun und mit Daten operieren (wollen), für die die
alten Methoden wie der t-Test geeignet sind. Frei nach Max Planck kann man behaupten,
dass alte Lehrmeinungen dann erst das Zeitliche segnen, wenn das auch die Professoren
tun, die die Lehrmeinungen vertreten. Vielleicht liegt es auch schlicht daran, dass unser
Alltag in den meisten Belangen wenig von der Digitalisierung und von Algorithmen berührt scheint: Beim Bäcker grüßt man wie seit Altvaterzeiten; deutsche Autos rollen vom
Band wie seit dem Wirtschaftswunder; Schüler und Studenten lesen in ihren Büchern, wie
es ihnen in Preußen, als das deutsche Schulwesen seine Anfänge fand, eingebläut wurde
(Foucault 1994). Die Revolution der Daten ist kaum spürbar; sie fühlt sich weit weg an.
Aber ein Wechsel „unter den Talaren“ zeichnet sich ab: Statistiker mit Renommee rufen
dazu auf, das Datenzeitalter im Unterricht einzuläuten (Cobb 2007; Hardin et al. 2015):
Datenanalyse heute ist anders als gestern (s. folgende Abbildung). Immer mehr Lehrbücher zu moderner Statistik und Datenanalysen erscheinen, auch richtig gute (Baumer et al.
2017; z. B. James et al. 2013; McElreath 2015; Wickham und Grolemund 2016). Bislang
zumeist im englischen Sprachraum, aber es gibt auch zunehmend mehr deutschsprachige
Bücher (z. B. Wickham und Grolemund (2017)).
1
Aus Gründen der Lesbarkeit wird in diesem Buch das generische Maskulinum („der Leser“) verwendet; immer sind alle Geschlechter gleichermaßen gemeint.
Vorwort
VII
Früher
Normalverteilung
kleine Daten
Flexible Algorithmen
(sehr) große Daten
keine/kaum Computer
Heute
Computer allgegenwärtig
Das vorliegende Buch versucht, einen Teil der Lücke im deutschsprachigen Raum zu
schließen. Sie werden in diesem Buch die grundlegenden Ideen der modernen Datenanalyse lernen. Ziel ist es, Sie – in Grundzügen – mit moderner Statistik vertraut zu machen.2
Lernziele
Zielgruppe dieses Buches sind Einsteiger; Formeln und mathematische Hintergründe
sucht man meist vergebens. Das liegt zum einen daran, dass keine oder kaum Vorkenntnisse in Datenanalyse vorausgesetzt werden. Zum anderen beruht das Buch auf einem
didaktischen Ansatz, der das Tun vor das Wissen setzt. Das bedeutet nicht, dass Wissen
geringer geschätzt würde als Handeln. Vielmehr steht dahinter die Idee, dass es dem Verstehen und dem statistical thinking (C. J. Wild und Pfannkuch 1999) hilft, sich frühzeitig
mit dem Ausprobieren auseinanderzusetzen. Der Werkzeug- oder Problemlöse-Charakter
des Denkens steht im Mittelpunkt des Lernansatzes dieses Buches (vgl. Trilling und Fadel
(2012)). Kein Inhalt dieses Buches bleibt ohne Umsetzung, ohne Anwendung; es ist ein
Buch für Praktiker. Wer eine tiefere, mathematisch ansprechendere Einführung sucht, sei
an das exzellente Buch von Hastie et al. (2013) verwiesen.
Reines Lesen dieses Buches wird dem Anfänger in etwa so viel bringen wie die Lektüre einer Schwimmfibel. Umgekehrt ist Üben die Grundlage für Fortschritt in der Kunst
der Datenanalyse (s. folgende Abbildung).3 Nutzen Sie die Übungsangebote: die reichhaltige R-Syntax, die Daten, die Aufgaben und die Verweise zu weiterführender Literatur.
Wesentlich ist das Durcharbeiten der Syntax-Beispiele. Die Kapitel sind zum Teil in sich
abgeschlossen; die Grundlagen (bis einschließlich Kap. 7) werden durchgängig benötigt.
Eine Lektüre von vorne nach hinten ist ratsam, aber nicht zwangsläufig nötig, gerade für
fortgeschrittene Leser. Vergleichsweise schwierig sind die Kap. 28 und 29.
2
Statistiker, die diesem Buch als Vorbild Pate standen, sind: Roger D. Peng: http://www.biostat.
jhsph.edu/~rpeng/, Hadley Wickham: http://hadley.nz, Jennifer Bryan: https://github.com/jennybc.
3
Die Abbildung zeigt den Zusammenhang von Klausurerfolg und Vorbereitungsaufwand. Man
sieht, dass der Klausurerfolg (Y-Achse) tendenziell steigt, wenn der Vorbereitungsaufwand (XAchse) steigt.
VIII
Vorwort
Prozent richtiger Lösungen
Mehr Lernen, bessere Noten
Der Zusammenhang von Lernzeit und Klausurerfolg in der Statistik
1.00
0.75
0.50
0.25
0.00
1
2
3
4
5
Lernaufwand
bestanden
ja
nein
n = 1646 Studenten
Nach der Lektüre dieses Buches können Sie:
den Ablauf eines Projekts aus der Datenanalyse in wesentlichen Schritten nachvollziehen
Daten aufbereiten und ansprechend visualisieren
Inferenzstatistik anwenden und kritisch hinterfragen
klassische Vorhersagemethoden (Regression) anwenden
moderne Methoden der angewandten Datenanalyse anwenden (z. B. Textmining)
(wirtschaftliche) Fragestellungen mittels datengetriebener Vorhersagemodelle
beantworten.
Zur Didaktik
Im Gegensatz zu anderen vergleichbaren Kursen steht hier die Umsetzung mit R (R Core
Team 2018) im Vordergrund. Dies hat pragmatische Gründe: Möchte man Daten einer
statistischen Analyse unterziehen, so muss man sie zumeist erst aufbereiten, und zwar
oft mühselig. Selten kann man den Luxus genießen, einfach „nur“, nach Herzenslust sozusagen, ein Feuerwerk an multivariater Statistik abzubrennen. Zuvor gilt es, die Daten
umzuformen, zu prüfen und zusammenzufassen. Für beide Anforderungen ist R bestens
geeignet. Dem Teil des Aufbereitens der Daten ist hier ausführlich Rechnung getragen.
Außerdem spielt in diesem Kurs die Visualisierung von Daten eine große Rolle. Ein Grund
ist, dass Menschen bekanntlich Augentiere sind. Zum anderen bieten Diagramme bei umfangreichen Daten Einsichten, die sonst leicht wortwörtlich übersehen würden.
Lovett und Greenhouse (2000) leiten aus der kognitiven Theorie fünf Prinzipien zur
Didaktik des Statistikunterrichts ab; nach diesen Prinzipien ist dieses Buch ausgerichtet.
(1) Menschen lernen am meisten durch das und von dem, was sie selber ausprobieren:
Das Selber-Tun steht im Zentrum dieses Buches. (2) Wissen ist situiert, kontextspezifisch:
Im Unterricht bzw. in einem Buch sollte daher lebensnah und alltagsrelevantes Wissen
Vorwort
IX
vermittelt werden; die Beispiele und Methoden dieses Buches sind aus typischen oder
verbreiteten Fragestellungen des Wirtschaftslebens entnommen. (3) Direktes Feedback
verbessert das Lernergebnis: Das sofortige Ausprobieren anhand der R-Syntax gibt unmittelbares Feedback, ob ein Plan aufgegangen ist. Wie beim Jonglieren: Wenn ein Ball
zu Boden fällt, weiß man, es ist ein Fehler passiert. Ähnlich verhält es sich, wenn R nichts
oder Kauderwelsch ausspuckt. (4) Lernen geschieht beim Verbinden von Bekanntem mit
Neuem: Der Sprachduktus ist informell, da viele Themen gerade in Bereichen nahe der
Mathematik nicht wegen des Inhalts, sondern wegen der Formalisierung kompliziert werden. Freilich setzt man mit informeller Sprache Genauigkeit und Detailtiefe aufs Spiel.
Da es sich aber um ein für die avisierte Leserschaft neues Thema handelt, neigt sich die
Waage hier zugunsten der informellen, intuitiven Herangehensweise. Für fortgeschrittene
Leser ist dieses Buch daher weniger geeignet. (5) Die mentale Belastung sollte ausgewogen sein: Das Buch beginnt mit grundlegenden Themen; die vergleichsweise schwierigen
warten gen Ende. Jedes Kapitel behandelt nur ein Thema, um geistige Ressourcen effektiv zu nutzen. Die „R-Philosophie“ dieses Buches orientiert sich am „Tidyverse“ (vgl.
Wickham und Grolemund (2017)); alle Kapitel und alle R-Syntax sind diesem Paradigma
verhaftet. Sie werden schnell den ähnlichen Aufbau der Syntax in allen Kapiteln erkennen.
Erfahrenen R-Programmierern wird der ausgiebige Gebrauch der „Pfeife“ aus magrittr
auffallen; genauso wie der ausgiebige Gebrauch von dplyr und anderen Figuren aus dem
„Tidyverse“.
Icons
R spricht zu Ihnen; sie versucht es jedenfalls in diesem Buch, und zwar mit folgenden
Icons (Fonticons 2018).
R-Pseudo-Syntax: An vielen Stellen dieses Buches findet sich R-Syntax. Neue
oder kompliziertere Syntax ist Zeile für Zeile ins Deutsche übersetzt.
Achtung, aufgepasst: Schwierige, merkwürdige oder fehlerträchtige Stellen sind
mit diesem Symbol markiert.
Übungsaufgaben: In jedem Kapitel finden sich Übungsaufgaben. Auf diese wird
mit diesem Icon verwiesen oder die Übungen sind in einem Abschnitt mit einsichtigem Titel zu finden.
Hinweise
Kunstwerke (Bilder) sind genau wie Standard-Literatur im Text zitiert, die verwendeten
R-Pakete nur im Anhang. Alle Werke (auch Daten und Software) finden sich im Literaturverzeichnis. Dieses Buch wurde mit dem R-Paket bookdown basierend auf R (R Core
Team 2018) in RStudio (RStudio 2018) geschrieben. bookdown basiert wiederum u. a.
auf den R-Paketen knitr und rmarkdown. Norman Markgrafs Typografie-Paket hat den
X
Vorwort
Textsatz geschliffen (Markgraf 2018). Diese Pakete stellen großartige Funktionalität zur
Verfügung, kostenlos und quelloffen.
Ein Hinweis für Dozenten: Der Text enthält an vielen Stellen Abbildungen oder kurze
Zusammenfassungen, die sich für Folien eignen. Getreu der Philosophie, nach der Folien
keine Konkurrenz zum Buch sind, sondern nur Wesentliches illustrieren, sollten vor allem
diese Bilder in einen möglichen Foliensatz für eine Lehrveranstaltung eingehen. Das hat
den charmanten Vorteil, dass die Erstellung der Folien einfach von der Hand geht. Im
Anhang findet sich ein Vorschlag für das Curriculum einer Einführungsveranstaltung (s.
Abschn. A.2.4).
Zu diesem Buch gibt es eine Webseite: https://sebastiansauer.github.io/modar/. Dort
finden sich die Abbildungen, die Syntax und mehr. Feedback, Fragen und Hinweise können Sie dort auch einstellen.
Danke
Dieses Buch ist mit der Hilfe Vieler und dank glücklicher Umstände entstanden. Von meinen Kollegen mit ihren Hinweisen, ihrem Ansporn, ihrem Lob und ihrer Kritik habe ich
vielfältig profitiert; das hat dieses Buch geschliffen. Herausgreifen möchte ich Karsten
Lübke, von dem ich viel gelernt habe. Norman Markgraf hat sein Wissen großzügig mit
mir geteilt; für viele, gerade technische Hilfestellungen bin ich ihm dankbar. Weiter danke
ich meiner Frau Sabrina, Christoph Kurz, Felix Bauer, Moritz Körber und Oliver Gansser
für ihr Feedback zum Manuskript. Der Austausch mit den Kollegen vom ifes-Institut war
fruchtbar und schwungvoll. Dieses Buch baut vielfach auf dieser Zusammenarbeit auf.
Ich danke der Hochschulleitung der FOM für ihre Unterstützung und den Vorschuss an
Vertrauen, was mein Arbeiten freudvoll machte; Gleiches gilt für den Dekan der Wirtschaftspsychologie an der FOM, Christoph Berg, der den Weg ebnete für neue Ideen, wie
sie auch in diesem Buch Eingang fanden. Mein Dank gilt außerdem Kai Stumpp für die
Begleitung bei der Erstellung des Buches. Nicht zuletzt danke ich meinen Studenten; ich
weiß nicht, wer von wem mehr gelernt hat: sie von mir oder ich von ihnen, dank vieler Fragen und Hinweise. Vermeintlich „dumme“ Fragen zielen oft in die Mitte des Wesentlichen
und die meisten komplexen Dinge kann man in einfachen Worten erklären, wenn man
sie verstanden hat. Ohne umfangreiche Open-Source-Software wäre dieses Buch nicht
entstanden; viele Menschen haben unentgeltlich mitgewirkt. Solcher Reichtum verblüfft
mich immer wieder.
Ich hoffe, dass Sie mit diesem Buch einiges Handwerkszeug der modernen Datenanalyse lernen; dass Sie Gefallen an der „Kunst und Wissenschaft“ der Datenanalyse finden.
Für Ihre Anregungen, Hinweise zu Fehlern und Ideen bin ich dankbar; am besten stellen
Sie sie hier ein: https://github.com/sebastiansauer/modar/issues.
Nürnberg
im Sommer 2018
Sebastian Sauer
Inhaltsverzeichnis
Teil I
Rahmen
1
Statistik heute . . . . . . . . . . . . . . . . . . . . . . .
1.1
Datenanalyse, Statistik, Data Science und Co.
1.2
Wissensgebiete der Datenanalyse . . . . . . . .
1.3
Einige Grundbegriffe . . . . . . . . . . . . . . .
1.4
Signal und Rauschen . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
4
6
8
9
2
Hallo, R . . . . . . . . . . . . . . . . .
2.1
Eine kurze Geschichte von R
2.2
Warum R? Warum, R? . . . .
2.2.1 Warum R? . . . . . . .
2.2.2 Warum, R? . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13
13
15
15
17
3
R starten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1
R und RStudio installieren . . . . . . . . . . . . . . . . . . .
3.2
Pakete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Pakete von CRAN installieren . . . . . . . . . . .
3.2.2 Pakete installieren vs. Pakete starten (laden) . . .
3.2.3 Pakete wie pradadata von Github installieren
3.3
Hilfe! R startet nicht! . . . . . . . . . . . . . . . . . . . . . .
3.4
Zuordnung von Paketen zu Befehlen . . . . . . . . . . . . .
3.5
R-Skript-Dateien . . . . . . . . . . . . . . . . . . . . . . . . .
3.6
Daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.6.1 Datensätze aus verschiedenen R-Paketen . . . . .
3.6.2 Datensätze aus dem R-Paket pradadata . . . .
3.7
Grundlagen der Arbeit mit RStudio . . . . . . . . . . . . .
3.7.1 Das Arbeitsverzeichnis . . . . . . . . . . . . . . . .
3.7.2 RStudio-Projekte . . . . . . . . . . . . . . . . . . .
3.8
Hier werden Sie geholfen . . . . . . . . . . . . . . . . . . . .
3.8.1 Wo finde ich Hilfe? . . . . . . . . . . . . . . . . . .
3.8.2 Einfache, reproduzierbare Beispiele (ERBies) . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
21
23
23
24
25
25
27
29
29
29
30
30
31
32
33
33
33
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
XI
XII
4
Inhaltsverzeichnis
Erstkontakt . . . . . . . . . . . . . . . . .
4.1
R ist pingelig . . . . . . . . . . . .
4.2
Variablen zuweisen und auslesen
4.3
Funktionen aufrufen . . . . . . . .
4.4
Logische Prüfungen . . . . . . . .
4.5
Vektorielle Funktionen . . . . . .
4.6
Literaturempfehlungen . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
37
37
38
39
40
42
43
5
Datenstrukturen . . . . . . . . . . . . . . . . . . . . .
5.1
Überblick über die wichtigsten Objekttypen
5.2
Objekttypen in R . . . . . . . . . . . . . . . . .
5.2.1 Vektoren . . . . . . . . . . . . . . . . .
5.2.2 Faktoren . . . . . . . . . . . . . . . . .
5.2.3 Listen . . . . . . . . . . . . . . . . . .
5.2.4 Matrizen und Arrays . . . . . . . . .
5.2.5 Dataframes . . . . . . . . . . . . . . .
5.3
Daten auslesen und indizieren . . . . . . . . .
5.3.1 Reine Vektoren . . . . . . . . . . . . .
5.3.2 Matrizen und Arrays . . . . . . . . .
5.3.3 Listen . . . . . . . . . . . . . . . . . .
5.3.4 Dataframes . . . . . . . . . . . . . . .
5.4
Namen geben . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
47
47
49
49
51
53
53
53
55
55
57
57
59
60
6
Datenimport und -export . . . . . . . . . . . . . .
6.1
Daten in R importieren . . . . . . . . . . . .
6.1.1 Excel-Dateien importieren . . . . .
6.1.2 Daten aus R-Paketen importieren .
6.1.3 Daten im R-Format laden . . . . .
6.1.4 CSV-Dateien importieren . . . . .
6.2
Textkodierung . . . . . . . . . . . . . . . . . .
6.3
Daten exportieren . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
63
63
64
64
65
65
68
69
Datenjudo . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.1
Daten aufbereiten mit dplyr . . . . . . . . . . . . .
7.2
Zentrale Bausteine von dplyr . . . . . . . . . . . .
7.2.1 Zeilen filtern mit filter() . . . . . . . .
7.2.2 Fortgeschrittene Beispiele für filter()
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
75
77
78
78
80
Teil II
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Daten einlesen
Teil III
7
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Daten aufbereiten
Inhaltsverzeichnis
7.3
7.4
7.5
7.6
XIII
7.2.3 Spalten wählen mit select() . . . . . . . . . . .
7.2.4 Zeilen sortieren mit arrange() . . . . . . . . . .
7.2.5 Einen Datensatz gruppieren mit group_by() . .
7.2.6 Eine Spalte zusammenfassen mit summarise()
7.2.7 Zeilen zählen mit n() und count() . . . . . . .
Die Pfeife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Spalten berechnen mit mutate() . . . . . . . . . . . . . . .
Bedingte Analysen mit den Suffixen von dplyr . . . . . .
7.5.1 Suffix _if . . . . . . . . . . . . . . . . . . . . . . . .
7.5.2 Suffix _all . . . . . . . . . . . . . . . . . . . . . . .
7.5.3 Suffix _at . . . . . . . . . . . . . . . . . . . . . . . .
Tabellen zusammenführen (join) . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
81
82
84
86
89
91
93
96
96
97
97
99
8
Deskriptive Statistik . . . . . . . . . . . . . . . . .
8.1
Univariate Statistik . . . . . . . . . . . . . . .
8.1.1 Deskriptive Statistik mit mosaic
8.1.2 Deskriptive Statistik mit dplyr .
8.1.3 Relative Häufigkeiten . . . . . . . .
8.2
Korrelationen berechnen . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
103
104
107
108
109
112
9
Praxisprobleme der Datenaufbereitung . . . . . .
9.1
Fehlende Werte . . . . . . . . . . . . . . . . . .
9.1.1 Ursachen von fehlenden Werten . .
9.1.2 Auf fehlende Werte prüfen . . . . . .
9.1.3 Umgang mit fehlenden Werten . . .
9.1.4 Fälle mit fehlenden Werten löschen
9.1.5 Fehlende Werte einer Spalte zählen
9.1.6 Fehlende Werte ersetzen . . . . . . .
9.1.7 -99 in NA umwandeln . . . . . . . . .
9.2
Datenanomalien . . . . . . . . . . . . . . . . .
9.2.1 Doppelte Fälle löschen . . . . . . . .
9.2.2 Nach Anomalien suchen . . . . . . .
9.2.3 Ausreißer identifizieren . . . . . . .
9.2.4 Hochkorrelierte Variablen finden . .
9.2.5 Quasi-Konstante finden . . . . . . . .
9.2.6 Auf Normalverteilung prüfen . . . .
9.3
Daten umformen . . . . . . . . . . . . . . . . .
9.3.1 Aufgeräumte Dataframes . . . . . .
9.3.2 Langes vs. breites Format . . . . . .
9.3.3 z-Standardisieren . . . . . . . . . . .
9.3.4 Spaltennamen ändern . . . . . . . . .
9.3.5 Variablentypen ändern . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
117
118
118
119
119
119
121
122
124
125
125
126
127
128
129
130
130
130
131
133
134
135
XIV
Inhaltsverzeichnis
9.4
9.5
10
Werte umkodieren und partitionieren . . . . . . . . . . . . . . . . . . . .
9.4.1 Umkodieren und partitionieren mit car::recode() . . .
9.4.2 Einfaches Umkodieren mit einer Logik-Prüfung . . . . . . .
9.4.3 Binnen mit cut() . . . . . . . . . . . . . . . . . . . . . . . . .
Vektoren zu Skalaren zusammenfassen . . . . . . . . . . . . . . . . . .
9.5.1 Mittelwerte pro Zeile berechnen . . . . . . . . . . . . . . . . .
9.5.2 Beliebige Statistiken pro Zeile berechnen mit rowwise()
Fallstudie: Datenjudo . . . . . . . . . . . . . . . . . . . . . .
10.1 Deskriptive Statistiken zu den New Yorker Flügen
10.2 Visualisierungen zu den deskriptiven Statistiken . .
10.2.1 Maximale Verspätung . . . . . . . . . . . . .
10.2.2 Durchschnittliche Verspätung . . . . . . . .
10.2.3 Korrelate der Verspätung . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
136
137
138
139
141
141
142
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
145
146
149
149
151
152
11
Datenvisualisierung mit ggplot2 . . . . . . . . . . . . . . . . . . . . . . .
11.1 Einstieg in ggplot2 . . . . . . . . . . . . . . . . . . . . . . . . . .
11.1.1 Ein Bild sagt mehr als 1000 Worte . . . . . . . . . . . . .
11.1.2 Diagramme mit ggplot2 zeichnen . . . . . . . . . . . .
11.1.3 Die Anatomie eines Diagramms . . . . . . . . . . . . . .
11.1.4 Schnell Diagramme erstellen mit qplot() . . . . . . .
11.1.5 ggplot-Diagramme mit mosaic . . . . . . . . . . . . . .
11.2 Häufige Arten von Diagrammen (Geomen) . . . . . . . . . . . . .
11.2.1 Eine kontinuierliche Variable – Histogramme und Co. .
11.2.2 Zwei kontinuierliche Variablen . . . . . . . . . . . . . . .
11.2.3 Eine oder zwei nominale Variablen . . . . . . . . . . . .
11.2.4 Zusammenfassungen zeigen . . . . . . . . . . . . . . . . .
11.3 Die Gefühlswelt von ggplot2 . . . . . . . . . . . . . . . . . . . .
11.4 ggplot(), der große Bruder von qplot() . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
157
158
158
158
159
162
164
166
166
167
169
174
178
179
12
Fortgeschrittene Themen der Visualisierung .
12.1 Farbwahl . . . . . . . . . . . . . . . . . . . .
12.1.1 Die Farben von Cynthia Brewer
12.1.2 Die Farben von Wes Anderson .
12.1.3 Viridis . . . . . . . . . . . . . . . .
12.2 ggplot2-Themen . . . . . . . . . . . . . . .
12.2.1 Schwarz-Weiß-Druck . . . . . . .
12.3 Interaktive Diagramme . . . . . . . . . . .
12.3.1 Plotly . . . . . . . . . . . . . . . . .
12.3.2 Weitere interaktive Diagramme .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
187
187
188
190
193
194
195
197
197
198
Teil IV
.
.
.
.
.
.
.
.
.
.
.
.
.
Daten visualisieren
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Inhaltsverzeichnis
XV
13
Fallstudie: Visualisierung . . . . . . . . . . . . .
13.1 Umfragedaten visualisieren mit „likert“ .
13.2 Umfragedaten visualisieren mit ggplot
13.2.1 Daten aufbereiten . . . . . . . . .
13.2.2 Daten umstellen . . . . . . . . . .
13.2.3 Diagramme für Anteile . . . . . .
13.2.4 Rotierte Balkendiagramme . . .
13.2.5 Text-Labels . . . . . . . . . . . . .
13.2.6 Diagramm beschriften . . . . . .
13.2.7 Balken mit Häufigkeitswerten . .
13.2.8 Sortieren der Balken . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
201
202
203
203
204
204
207
208
211
211
212
14
Geovisualisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14.1 Kartendaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14.1.1 Geo-Daten der deutschen Verwaltungsgebiete . . . . . . . . . .
14.1.2 Daten der Wahlkreise . . . . . . . . . . . . . . . . . . . . . . . . .
14.2 Unterschiede in Kartensegmenten visualisieren . . . . . . . . . . . . . .
14.2.1 Karte der Wahlkreise gefärbt nach Arbeitslosigkeit . . . . . .
14.2.2 Wahlergebnisse nach Wahlkreisen . . . . . . . . . . . . . . . . .
14.2.3 Zusammenhang von Arbeitslosigkeit und AfD-Wahlergebnis
14.2.4 Ein komplexeres Modell . . . . . . . . . . . . . . . . . . . . . . .
14.3 Weltkarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14.3.1 rworldmap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14.3.2 rworldmap mit geom_sf . . . . . . . . . . . . . . . . . . . . . . .
14.4 Anwendungsbeispiel: Konkordanz von Kulturwerten und
Wohlbefinden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14.5 Interaktive Karten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14.5.1 Karten mit „leaflet“ . . . . . . . . . . . . . . . . . . . . . . . . . .
14.5.2 Karten mit googleVis . . . . . . . . . . . . . . . . . . . . . . .
215
216
216
218
219
219
220
222
223
224
224
227
Teil V
15
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
229
234
234
235
Modellieren
Grundlagen des Modellierens . . . . . . . . . . . . . . . . . . . . .
15.1 Was ist ein Modell? Was ist Modellieren? . . . . . . . . . . .
15.2 Abduktion als Erkenntnisfigur im Modellieren . . . . . . . .
15.3 Ein Beispiel zum Modellieren in der Datenanalyse . . . . .
15.4 Taxonomie der Ziele des Modellierens . . . . . . . . . . . .
15.5 Die vier Schritte des statistischen Modellierens . . . . . . .
15.6 Einfache vs. komplexe Modelle: Unter- vs. Überanpassung
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
245
246
248
250
251
254
255
XVI
Inhaltsverzeichnis
15.7
15.8
15.9
15.10
15.11
Bias-Varianz-Abwägung . . . . . . . . . . . . . . . . . . . .
Trainings- vs. Test-Stichprobe . . . . . . . . . . . . . . . . .
Resampling und Kreuzvalidierung . . . . . . . . . . . . . .
Wann welches Modell? . . . . . . . . . . . . . . . . . . . . .
Modellgüte . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15.11.1 Modellgüte in numerischen Vorhersagemodellen
15.11.2 Modellgüte bei Klassifikationsmodellen . . . . .
15.12 Der Fluch der Dimension . . . . . . . . . . . . . . . . . . . .
16
17
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
256
257
259
260
260
261
261
262
Inferenzstatistik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.1 Wozu Inferenzstatistik? . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.2 Der p-Wert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.2.1 Was sagt der p-Wert? . . . . . . . . . . . . . . . . . . . . . . . .
16.2.2 Der zwielichtige Statistiker – ein einführendes Beispiel zur
Inferenzstatistik . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.2.3 Von Männern und Päpsten – Was der p-Wert nicht sagt . . .
16.2.4 Der p-Wert ist eine Funktion der Stichprobengröße . . . . .
16.2.5 Mythen zum p-Wert . . . . . . . . . . . . . . . . . . . . . . . .
16.3 Wann welcher Inferenztest? . . . . . . . . . . . . . . . . . . . . . . . . .
16.4 Beispiele für häufige Inferenztests . . . . . . . . . . . . . . . . . . . . .
16.4.1 2 -Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.4.2 t-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.4.3 Einfache Varianzanalyse . . . . . . . . . . . . . . . . . . . . . .
16.4.4 Korrelationen (nach Pearson) auf Signifikanz prüfen . . . . .
16.4.5 Regression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.4.6 Wilcoxon-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.4.7 Kruskal-Wallis-Test . . . . . . . . . . . . . . . . . . . . . . . . .
16.4.8 Shapiro-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.4.9 Logistische Regression . . . . . . . . . . . . . . . . . . . . . . .
16.4.10 Spearmans Korrelation . . . . . . . . . . . . . . . . . . . . . . .
16.5 Alternativen zum p-Wert . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.5.1 Konfidenzintervalle . . . . . . . . . . . . . . . . . . . . . . . . .
16.5.2 Effektstärke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.5.3 Power-Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16.5.4 Bayes-Statistik . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
267
268
269
269
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
271
274
275
276
277
278
278
280
281
282
283
284
284
285
285
286
286
286
289
292
293
Simulationsbasierte Inferenz . . . . . . . . . .
17.1 Stichproben, Statistiken und Population
17.2 Die Stichprobenverteilung . . . . . . . .
17.3 Der Bootstrap . . . . . . . . . . . . . . . .
17.4 Nullhypothesen auf Signifikanz testen .
.
.
.
.
.
301
301
304
308
311
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Inhaltsverzeichnis
Teil VI
XVII
Geleitetes Modellieren
18
Lineare Modelle . . . . . . . . . . . . . . . . . . . . . . . . . .
18.1 Die Idee der klassischen Regression . . . . . . . . . .
18.2 Modellgüte . . . . . . . . . . . . . . . . . . . . . . . . .
18.2.1 Mittlere Quadratfehler . . . . . . . . . . . . .
18.2.2 R-Quadrat (R2 ) . . . . . . . . . . . . . . . . .
18.3 Die Regression an einem Beispiel erläutert . . . . . .
18.4 Überprüfung der Annahmen der linearen Regression
18.5 Regression mit kategorialen Prädiktoren . . . . . . .
18.6 Multiple Regression . . . . . . . . . . . . . . . . . . . .
18.7 Interaktionen . . . . . . . . . . . . . . . . . . . . . . . .
18.8 Prädiktorenrelevanz . . . . . . . . . . . . . . . . . . . .
18.9 Anwendungsbeispiel zur linearen Regression . . . .
18.9.1 Overfitting . . . . . . . . . . . . . . . . . . . .
18.9.2 Konfidenzintervalle der Parameter . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
321
321
324
325
325
327
329
331
333
335
337
339
339
341
19
Klassifizierende Regression . . . . . . . . . . . . . . . . . . . . . .
19.1 Normale Regression für ein binäres Kriterium . . . . . . .
19.2 Die logistische Funktion . . . . . . . . . . . . . . . . . . . .
19.3 Interpretation des Logits . . . . . . . . . . . . . . . . . . . .
19.4 Kategoriale Prädiktoren . . . . . . . . . . . . . . . . . . . . .
19.5 Multiple logistische Regression . . . . . . . . . . . . . . . .
19.6 Modellgüte . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19.6.1 Vier Arten von Ergebnissen einer Klassifikation
19.6.2 Kennzahlen der Klassifikationsgüte . . . . . . . .
19.7 Vorhersagen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19.8 ROC-Kurven und Fläche unter der Kurve (AUC) . . . . .
19.8.1 Cohens Kappa . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
345
346
347
350
351
352
353
353
355
356
357
360
20
Fallstudie: Titanic . . . . . . . . . . . . .
20.1 Explorative Analyse . . . . . . . .
20.1.1 Univariate Häufigkeiten
20.1.2 Bivariate Häufigkeiten .
20.2 Inferenzstatistik . . . . . . . . . . .
20.2.1 2 -Test . . . . . . . . . . .
20.2.2 Effektstärke . . . . . . . .
20.2.3 Logistische Regression .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
365
366
366
367
368
368
369
373
21
Baumbasierte Verfahren . . . . . . . .
21.1 Entscheidungsbäume . . . . . .
21.1.1 Einführendes Beispiel
21.1.2 Tuningparameter . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
377
378
378
383
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
XVIII
Inhaltsverzeichnis
21.2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
384
388
391
391
391
393
394
394
398
Fallstudie: Kreditwürdigkeit mit caret . . . . . . . . . . . . . . . . . . . . .
22.1 Zwei Arten der prädiktiven Modellierung . . . . . . . . . . . . . . . . .
22.2 Daten aufbereiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22.2.1 Fehlende Werte . . . . . . . . . . . . . . . . . . . . . . . . . . .
22.2.2 Trainings- und Test-Sample aufteilen . . . . . . . . . . . . . .
22.2.3 Variablen ohne Varianz . . . . . . . . . . . . . . . . . . . . . . .
22.2.4 Hochkorrelierte Variablen entfernen . . . . . . . . . . . . . . .
22.2.5 Parallele Verarbeitung . . . . . . . . . . . . . . . . . . . . . . .
22.3 Modelle anpassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22.3.1 Kreuzvalidierung . . . . . . . . . . . . . . . . . . . . . . . . . .
22.3.2 Modell im Trainings-Sample anpassen mit train() . . . .
22.3.3 Ein einfaches Modell . . . . . . . . . . . . . . . . . . . . . . . .
22.3.4 Random Forest . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22.3.5 Support Vector Machines . . . . . . . . . . . . . . . . . . . . .
22.3.6 Penalisierte lineare Modelle . . . . . . . . . . . . . . . . . . . .
22.4 Modellgüte bestimmen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22.4.1 Modellgüte in der Test-Stichprobe . . . . . . . . . . . . . . . .
22.4.2 Modellgüte in der Kreuzvalidierung . . . . . . . . . . . . . . .
22.5 Wichtigkeit der Prädiktoren bestimmen . . . . . . . . . . . . . . . . . .
22.5.1 Modellunabhängige Variablenwichtigkeit für Klassifikation
22.5.2 Modellunabhängige Prädiktorenrelevanz bei numerischen
Vorhersagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22.5.3 Modellabhängige Variablenwichtigkeit . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
401
402
403
403
403
404
405
406
407
407
407
408
411
414
416
418
418
421
426
427
21.3
21.4
21.5
21.6
21.7
21.8
22
Teil VII
23
Entscheidungsbäume mit caret . . . . . .
21.2.1 Vorhersagegüte . . . . . . . . . . . .
Der Algorithmus der Entscheidungsbäume
Regressionsbäume . . . . . . . . . . . . . . .
Stärken und Schwächen von Bäumen . . .
Bagging . . . . . . . . . . . . . . . . . . . . .
Grundlagen von Random Forests . . . . . .
21.7.1 Grundlagen . . . . . . . . . . . . . .
Variablenrelevanz bei Baummodellen . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 429
. 430
Ungeleitetes Modellieren
Clusteranalyse . . . . . . . . . . . . . . . . . . . . . . . . .
23.1 Grundlagen der Clusteranalyse . . . . . . . . . . .
23.1.1 Intuitive Darstellung der Clusteranalyse
23.1.2 Euklidische Distanz . . . . . . . . . . . . .
23.1.3 k-Means-Clusteranalyse . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
437
437
438
440
442
Inhaltsverzeichnis
23.2
XIX
Beispiel für eine einfache Clusteranalyse . . . . . . . . . . . . . . . . . . 443
23.2.1 Distanzmaße berechnen . . . . . . . . . . . . . . . . . . . . . . . 443
23.2.2 Fallstudie: kmeans für den Extraversionsdatensatz . . . . . . . 444
24
Textmining . . . . . . . . . . . . . . . . . . .
24.1 Grundlegende Analyse . . . . . . . .
24.1.1 Tidytext-Dataframes . . . .
24.1.2 Regulärausdrücke . . . . . .
24.1.3 Textdaten einlesen . . . . . .
24.1.4 Worthäufigkeiten auszählen
24.1.5 Visualisierung . . . . . . . .
24.2 Sentimentanalyse . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
449
450
450
453
455
456
457
459
25
Fallstudie: Twitter-Mining . . . . . . . . . . . . . . . . . . . . . .
25.1 Zum Einstieg: Moderne Methoden der Sentimentanalyse
25.2 Grundlagen des Twitter-Minings . . . . . . . . . . . . . . .
25.2.1 Authentifizierung bei der Twitter-API . . . . . . .
25.2.2 Hashtags und Nutzer suchen . . . . . . . . . . . .
25.2.3 Tweets einer Nutzermenge auslesen . . . . . . . .
25.2.4 Aufbau einer Tweets-Datenbank . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
463
464
465
466
467
469
471
Teil VIII
26
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Kommunizieren
RMarkdown . . . . . . . . . . . . . . . . . . . . . . . . . .
26.1 Forderungen an Werkzeuge zur Berichterstellung
26.2 Start mit RMarkdown . . . . . . . . . . . . . . . . .
26.3 RMarkdown in Action . . . . . . . . . . . . . . . .
26.4 Aufbau einer Markdown-Datei . . . . . . . . . . .
26.5 Syntax-Grundlagen von Markdown . . . . . . . .
26.6 Tabellen . . . . . . . . . . . . . . . . . . . . . . . . .
26.7 Zitieren . . . . . . . . . . . . . . . . . . . . . . . . . .
26.8 Format-Vorlagen für RMarkdown . . . . . . . . . .
Teil IX
27
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
475
476
478
480
482
483
484
487
489
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
495
496
497
498
499
Rahmen 2
Projektmanagement am Beispiel
einer Fallstudie . . . . . . . . . . . . . . . . . . . . .
27.1 Was ist Populismus? . . . . . . . . . . . . . .
27.2 Forschungsfrage und Operationalisierung .
27.3 Emotionslexikon . . . . . . . . . . . . . . . .
27.4 Daten, Stichprobe und Analysekontext . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
XX
Inhaltsverzeichnis
27.5
27.6
27.7
Prozess der Datenanalyse . . . . . . . . . . . . . .
Zentrale Ergebnisse . . . . . . . . . . . . . . . . .
Projektmanagement . . . . . . . . . . . . . . . . .
27.7.1 Gliederung eines Projektverzeichnisses
27.7.2 Faustregeln zur Struktur eines Projekts
27.7.3 Versionierung mit Git . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
499
501
504
504
505
508
28
Programmieren mit R . . . . . . . . . . . . . . . . . . . . . . . .
28.1 Funktionen schreiben . . . . . . . . . . . . . . . . . . . . .
28.2 Wiederholungen . . . . . . . . . . . . . . . . . . . . . . . .
28.2.1 Wiederholungen für Elemente eines Vektors . .
28.2.2 Wiederholungen für Spalten eines Dataframes
28.2.3 Dateien wiederholt einlesen . . . . . . . . . . . .
28.2.4 Anwendungsbeispiele für map . . . . . . . . . .
28.2.5 Einige Rechtschreibregeln für map() . . . . .
28.3 Defensives Programmieren . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
511
511
514
515
516
518
520
523
523
29
Programmieren mit dplyr . . . . . . . . . . . . . . . . . . .
29.1 Wie man mit dplyr nicht sprechen darf . . . . . . .
29.2 Standard-Evaluation vs. Non-Standard-Evaluation
29.3 NSE als Backen . . . . . . . . . . . . . . . . . . . . .
29.4 Wie man Funktionen mit dplyr-Verben schreibt . .
29.5 Beispiele für NSE-Funktionen . . . . . . . . . . . . .
29.5.1 Funktionen für ggplot . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
527
527
528
530
534
537
539
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Anhang A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
Literatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Sachverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 559
Der Autor
Professor Dr. habil. Sebastian Sauer arbeitet als Hochschullehrer für Wirtschaftspsychologie an der FOM und versteht sich als Data Scientist. Datenanalyse mit R ist sein zentrales
Thema im Moment. Neben dem „Wie“ der Datenanalyse beschäftigen ihn die Grenzen
sowie die Gefahren, die die moderne Datenwissenschaft mit sich bringt. Außerdem engagiert er sich für das Thema Open Science und interessiert sich für die Frage, wie die
Psychologie zur Klärung von Problemen mit gesellschaftlicher Relevanz beitragen kann.
Sein Blog https://data-se.netlify.com/ dient ihm als Notizbuch sich entwickelnder Gedanken. Data Science für die Wirtschaft bietet er auf https://www.data-divers.com/ an.
XXI
Teil I
Rahmen
1
Statistik heute
Datenanalyse, praktisch betrachtet, kann man in fünf Schritte einteilen (Wickham und
Grolemund 2017), s. Abb. 1.1. Analog zu diesem Modell der Datenanalyse ist dieses Buch
aufgebaut. Zuerst muss man die Daten einlesen, die Daten also in R (oder einer anderen
Software) verfügbar machen (laden). Fügen wir hinzu: In schöner Form verfügbar machen; das man nennt auch Tidy Data (hört sich cooler an). Sobald die Daten in geeigneter
Form in R geladen sind, folgt das Aufbereiten. Das beinhaltet das Zusammenfassen, Umformen oder Anreichern der Daten, je nach Bedarf. Ein nächster wesentlicher Schritt ist
das Visualisieren der Daten. Ein Bild sagt bekanntlich mehr als tausend Worte. Schließlich folgt das Modellieren oder das Prüfen von Hypothesen: Man überlegt sich, wie sich
die Daten erklären lassen könnten. Zu beachten ist, dass diese drei Schritte – Aufbereiten,
Visualisieren, Modellieren – keine starre Abfolge sind, sondern eher ein munteres Hinund-Her-Springen, ein aufeinander aufbauendes Abwechseln. Der letzte Schritt ist das
Kommunizieren der Ergebnisse der Analyse – nicht der Daten. Niemand ist an Zahlenwüsten interessiert; es gilt, spannende Einblicke zu vermitteln. Die Datenanalyse als solche ist
in einen Rahmen eingebettet; das beinhaltet philosophische und technische Grundlagen.
Entsprechend diesen fünf Schritten sowie dem einbettenden Rahmen ist dieses Buch in
Teile gegliedert. Zu Beginn jedes Teiles ist ein Diagramm analog zu 1.1 dargestellt, um
einen Überblick über den jeweiligen Schritt der Datenanalyse zu geben.
Abb. 1.1 Der Rahmen als
Bestandteil der Datenanalyse
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_1
3
4
1
Statistik heute
Lernziele
Wissen, was Statistik ist, bzw. einige Aspekte einer Definition kennen
Statistik zu Data Science und anderen verwandten Begriffen abgrenzen können
Grundkonzepte wie Daten, Variable und Beobachtung definieren können
Die Begriffe Signal und Rauschen in Verbindung bringen können
Die Wissensgebiete der Datenanalyse aufzählen und erläutern können
1.1 Datenanalyse, Statistik, Data Science und Co.
Was ist Statistik? Eine Antwort dazu ist, dass Statistik die Wissenschaft von Sammlung,
Analyse, Interpretation und Kommunikation von Daten mithilfe mathematischer Verfahren ist und zur Entscheidungshilfe beitragen solle (Romeijn 2016; The Oxford Dictionary
of Statistical Terms 2006). E. N. Brown und Kass (2009) rücken die Wahrscheinlichkeitsrechnung ins Zentrum einer Definition von Statistik oder „Statistik-Denken“. Cobb (2015,
S. 3) spricht kurz und bündig von „thinking with and about data“ als dem Wesensmerkmal
von Statistik.
Wie lässt sich Statistik von Datenanalyse abgrenzen? Tukey (1962) definiert Datenanalyse als den Prozess des Erhebens von Daten, ihrer Auswertung und Interpretation.
Unschwer zu sehen, dass sich diese beiden Definitionen nur um die Betonung der stochastischen Modellierung, der Anwendung der Wahrscheinlichkeitsrechnung, unterscheiden.
Betrachtet man Lehrbücher der Statistik (Bortz 2013; Freedman et al. 2007), so fällt der
stärkere Fokus auf mathematische Ableitung und Eigenschaften von Objekten der Statistik ins Auge; Datenanalyse scheint einen stärkeren Anwendungsfokus zu haben (im
Gegensatz zu einem mathematischen Fokus). Statistik wird häufig in die zwei Gebiete
deskriptive und inferierende Statistik eingeteilt (vgl. Abb. 1.2). Erstere fasst viele Zahlen zusammen (s. Kap. 8), so dass wir den Wald statt vieler Bäume sehen. Eine Statistik
bezeichnet dabei die zusammenfassende Kenngröße; eine prototypische Statistik ist der
Mittelwert. Die Inferenzstatistik verallgemeinert von den vorliegenden Daten auf eine
zugrunde liegende Grundgesamtheit (Population; s. Kap. 16). So zieht man etwa eine
Stichprobe von einigen College-Studenten und schließt auf dieser Basis auf alle Menschen
dieser Welt. Ein abenteuerlicher Schluss, aber leider kein seltener (Henrich et al. 2010).
Da Analyse von Daten ein allgemeiner Begriff ist, der wenig mit bestimmten Methoden
aufgeladen ist, wird Datenanalyse im Folgenden als gemeinsamer Kern aller einschlägigen
Disziplinen oder Begrifflichkeiten verwendet; der Fokus ist dabei als angewandt gedacht.
In diesem Buch werden Statistik und Datenanalyse im Folgenden lose als Synonyme betrachtet.
1.1
Datenanalyse, Statistik, Data Science und Co.
Deskriptivstatistik
5
Inferenzstatistik
X
X
Abb. 1.2 Sinnbild für die Deskriptiv- und die Inferenzstatistik
Aufgabe der deskriptiven Statistik ist es primär, Daten prägnant zusammenzufassen.
Aufgabe der Inferenzstatistik ist es, zu prüfen, ob Daten einer Stichprobe auf eine
Grundgesamtheit verallgemeinert werden können.
Liegt der Schwerpunkt der Datenanalyse auf computerintensiven Methoden, so wird
auch von Data Science gesprochen, wobei der Begriff nicht einheitlich verwendet wird
(Hardin et al. 2015; Wickham und Grolemund 2017). Einige Statistiker sehen Data
Science als „Statistik am Computer“, und plädieren für ein „Neudenken“ der Statistik
bzw. des Statistikunterrichts, so dass Computermethoden eine zentrale Rolle spielen
(Cobb 2015). Andere Statistiker wiederum grenzen Data Science von der Statistik ab,
mit dem Argument, dass bei Ersterer Fragen der angewandten Informatik zentral sind,
bei der Statistik nicht (Baumer et al. 2017). Die Popularität von Data Science ist dem
Fortschritt in der Rechen- und Speicherkapazität der Computer zu verdanken. Heutzutage
sind ganz andere Daten und Datenmengen verarbeitbar und von Interesse. Sicherlich
bedingt die technische Machbarkeit auch, welche Forschungsfragen hoch im Kurs rangieren. Eine Echtzeit-Analyse von Twitter-Daten wäre bis vor einiger Zeit kaum möglich
gewesen, da die Hardware nicht leistungsfähig genug war. (Wir übersehen hier geflissentlich, dass die Hardware heute immer noch an Grenzen kommt und dass es früher kein
Twitter gab.) Die Datenmengen erfordern Arbeitsschritte wie z. B. Authentifizierung in
die Twitter-Schnittstelle, wiederholtes Abfragen der Schnittstelle unter Beachtung der
erlaubten Download-Obergrenzen, Umwandeln von einem Datenformat in ein anderes,
Einlesen in eine Datenbank, Prüfung auf Programmfehler, Automatisierung des Prozesses, Verwendung mehrerer Rechenkerne, Bereinigung des Textmaterials von Artefakten,
Aufbereiten der Daten und so weiter. Keine dieser Aufgaben war bei Statistikern vor 100
Jahren verbreitet. Da konnte man sich noch ganz auf die mathematischen Eigenschaften
6
1
Statistik heute
des t-Tests konzentrieren. Die Verschiebung innerhalb der Datenanalyse spiegelt einfach
die technische Entwicklung allgemein in der Gesellschaft wider. Das heißt nicht, dass
heute jeder Programmierer sein muss; auch für die Datenanalyse nicht. Glücklicherweise
gibt es eine Reihe von Werkzeugen, die die Handhabung der Daten einfacher macht. Diese
sind hinzugekommen zum Handwerkskoffer des Datenschreiners. Nichtsdestotrotz ist ein
grundlegendes Verständnis von computergestützter Datenverarbeitung wichtig und wird
zunehmend wichtiger für die Datenanalyse.
Schließlich kursieren u. a. noch die Begriffe Data Mining, maschinelles Lernen und
statistisches Lernen. Data Mining und maschinelles Lernen sind Begriffe, die eher in informatiknahen Gebieten verwendet werden; entsprechend sind die Themen mehr in Richtung
Informatik verschoben. Die technische Repräsentation und technische Aspekte der Datenmanipulation (Data Warehouse, Datenbanken) werden von IT-affinen Autoren stärker
betont als von Autoren, die nicht aus IT-nahen Fakultäten stammen. Beim statistischem
Lernen stehen Konzepte und Algorithmen des Modellierens (s. Kap. 15) im Vordergrund.
Auf der anderen Seite: Vergleicht man die Inhaltsverzeichnisse von Büchern aus allen
diesen Bereichen, so stellt man eine große Überschneidung des Inhaltsverzeichnisses fest.
Typische Themen in allen diesen Büchern sind (Bishop 2006; Han et al. 2011; James et al.
2013; Tan 2013) baumbasierte Verfahren (s. Kap. 21), Support Vector Machines, Dimensionsreduktion, Clusteranalyse, Regression (s. Kap. 18) oder Datenexploration (s. Kap. 11).
Kurz: Die Gebiete überlappen einander beträchtlich; mal stehen IT-nahe Themen im Vordergrund, mal wird die Mathematik betont, mal die Anbindung an empirisches Forschen.
Immer geht es darum, aus Daten Wissen zu generieren bzw. Entscheidungen datenbasiert
und rational zu begründen.
1.2 Wissensgebiete der Datenanalyse
Egal, ob man von Datenanalyse, Statistik, Data Mining, maschinellem Lernen, Data
Science oder statistischem Lernen spricht: In der Schnittmenge des Analysierens von Daten gleichen sich die Anforderungen. Wenn sich die Nuancen zwischen den Fachgebieten
auch verschieben, so sind doch stets die folgenden Wissensgebiete gefragt:
1. Philosophische Grundlagen
Dazu gehören die Annahmen, die im Alltag meist unhinterfragt für bare Münzen genommen werden. Annahmen, die das Fundament des Gebäudes der Datenanalyse stützen. Ist dieses Fundament auf Sand gebaut, so ist nicht zu erwarten, dass das Gebäude
seinen Zweck erfüllt. Zu den Grundlagenfragen gehört die Frage, was Wahrscheinlichkeit, Erkenntnis, Kausalität und Unendlichkeit sind. So wird zum Beispiel kontrovers
diskutiert, ob Wahrscheinlichkeit besser als Grenzwert der relativen Häufigkeit, als
subjektive Angelegenheit oder als Erweiterung der Aussagenlogik zu betrachten ist
(Briggs 2016; Jaynes 2003; Keynes 2013; Rucker 2004). Wichtig ist weiter die Frage,
was eine Messung genau ist, woran man erkennt, ob ein Variable quantitative Aus-
1.2
Wissensgebiete der Datenanalyse
7
sagen erlaubt und woran man die Güte eines Messinstruments festmacht (Saint-Mont
2011).
2. Mathematisch-statistische Anwendungen
Zentrale Theorie für die Statistik oder für Wissenschaft allgemein ist die Wahrscheinlichkeitsrechnung bzw. die Theorie der Wahrscheinlichkeit (Jaynes 2003). In den meisten Lehrbüchern der Statistik, auch in den Einsteigerbüchern, findet sich eine mal
schmalere, mal ausführlichere Einführung in Aspekte verschiedener Verteilungen, die
gewisse Zufallsprozesse, berechenbare Zufallsprozesse, voraussetzen.1 Häufig wird
angenommen, dass sich eine bestimmte Variable nach einem bekannten stochastischen
Modell verhält, so dass aus dem Modell Aussagen ableitbar sind. Besondere Berühmtheit hat die Normalverteilung erlangt, die wohl an keinem Studenten eines empirisch
orientierten Faches vorbeigegangen ist.2 Neben der Stochastik spielen aber noch weitere Felder der angewandten Mathematik eine Rolle; maßgeblich sind das die lineare
Algebra und die Infinitesimalrechnung (J. D. Brown 2015). Ein eigener Zweig, der
stark mit der Wahrscheinlichkeitslehre verbunden ist, ist die Bayes-Statistik (Wagenmakers et al. 2016).
3. Computerwissenschaftliche Anwendungen
Die zunehmende Digitalisierung der Gesellschaft macht vor der Datenanalyse keinen
Halt. Im Gegenteil; es liegt in der Natur der Datenanalyse, computeraffin zu sein –
sind doch Daten Gegenstand sowohl der Statistik als auch der Computerwissenschaft.
War es vor einigen Jahren noch ausreichend oder beeindruckend, den Knopf für den
t-Test zu kennen, sind die Anforderungen bei modernen Daten meist höher. Einlesen,
Aufbereiten und Speichern können leicht den größten Zeitanteil einer Datenanalyse
ausmachen. In einigen Anwendungen kommt noch der Anspruch dazu, dass die Analyse schnell gehen muss: Wir haben gerade viele Kunden auf der Webseite und müssen
wissen, wem wir welches Produkt und welchen Preis vorschlagen müssen. Nein, wir
wollen die Antwort nicht morgen, wir brauchen sie in ein paar Sekunden; Korrektur:
jetzt. Ach ja, der Datensatz ist ein paar Terabyte3 groß.
4. Fach- und Branchenkenntnis
Möchte man die Zufriedenheit eines Kunden vorhersagen, so ist es hilfreich, etwas
über die Ursachen von Kundenzufriedenheit zu kennen – also Wissen über den Gegenstand Kundenzufriedenheit zu haben. Wenn man schon die Ursachen nicht kennt, so ist
zumindest Wissen über zusammenhängende Variablen (Korrelate) sinnvoll. Wenn ein
Arzt aus Erfahrung weiß, was die Risikofaktoren einer Erkrankung sind, dann sollten
diese Informationen in das statistische Modell einfließen. Sach- inkl. Branchenkennt-
1
Zufall wird hier verstanden als ein nicht näher bekannter Prozess, dessen Ergebnisse zwar nicht
sicher sind, aber doch ein gewisses Muster erwarten lassen.
2
Micceri (1989) zeigt auf, dass Normalverteilungen seltener sind als gemeinhin angenommen.
McElreath (2015) bietet einen gut verständlichen Einblick in die Informationsentropie; diese Darstellung zeigt, dass eine Normalverteilung eine konservative Annahme für eine Verteilung darstellt.
3
1 Terabyte sind 1012 Byte.
8
1
Statistik heute
nis ist zentral für gute (genaue) statistische Modelle (Shearer 2000). Wissen über den
Sachgegenstand ist schon deshalb unerlässlich, weil Entscheidungen keine Frage der
Statistik sind: Ob ein Kunde zufrieden ist mit einer Vorhersage durch ein statistisches
Modell oder ein Patient gesund genug ist nach Aussage eines statistisches Modells,
muss der Anwender entscheiden. Ob ein Betrugsversuch mit 90 %, 99 % oder 99.9 %
Sicherheit erkannt werden soll, kann einen Unterschied machen – für einen bestimmten Anwender, in einer bestimmten Situation. Ein wichtiger, vielleicht der wichtigste
Punkt der Datenanalyse ist es, Entscheidungen für Handlungen zu begründen. Daher
muss die Datenanalyse immer wieder auf die Entscheidung und damit auf die Präferenz des Nutzers zurückgeführt werden.
1.3 Einige Grundbegriffe
Daten (die Einzahl Datum ist ungewöhnlich) kann man definieren als Informationen, die
in einem Kontext stehen (Moore 1990), wobei eine numerische Konnotation mitschwingt.
Häufig sind Daten in Tabellen bzw. tabellenähnlichen Strukturen gespeichert; die ExcelTabelle ist der Prototyp davon. Tabellen, so wie sie hier verstanden werden, zeichnen sich
dadurch aus, dass sie rechteckig sind und aus Zeilen und Spalten bestehen. Rechteckig
impliziert, dass alle Zeilen gleich lang sind und alle Spalten gleich lang sind (die Tabelle
muss aber nicht quadratisch sein). Knotenpunkte von Zeilen und Spalten heißen Zellen
oder Elemente; die Zellen dürfen auch leer sein oder mit einem Symbol für „kein Wert
vorhanden“ gefüllt sein.
Daten sind ein Produkt von Variablen (Merkmalen) und Beobachtungseinheiten (Fällen, Beobachtungen). Eine Tabelle mit ihren zwei rechtwinkligen Achsen der Zeilen und
Spalten verdeutlicht das (s. Abschn. 9.3.1 und Abb. 9.6). Die Beobachtungseinheit ist das
Objekt, das die untersuchten Merkmale aufweist. Oft sind es Personen, es können aber
auch Firmen, Filme, Filialen oder Flüge sein. Ein Merkmal einer Beobachtungseinheit
kann ihre Schuhgröße, der Umsatz, die Verspätung oder das Budget sein. Betrachten wir
das Merkmal Schuhgröße der Beobachtungseinheit Person S und finden wir 46, so ist 46
der Wert oder die Ausprägung dieses Merkmals dieser Beobachtungseinheit. Um nicht so
viel schreiben zu müssen, wird der Wert der Beobachtungseinheit i in der Variablen k
häufig als xi k bezeichnet. Die Gesamtheit der verfügbaren und zusammengehörigen Daten eines Sachverhalts bezeichnen wir als Datensatz; meist ist es eine Stichprobe aus einer
Population. Nehmen wir an, es gäbe nur zwei verschiedene Schuhgrößen: 36 und 46. Dann
hat die Variable Schuhgröße zwei Ausprägungen.
Natürlich gibt es auch Daten, die sich nicht (so einfach) in das enge Korsett einer
Tabelle pressen lassen; Textdokumente, Sprachdaten oder Bilder zum Beispiel. Nicht tabellarisierte Daten werden auch als unstrukturiert, Daten in Tabellenform als strukturiert
bezeichnet. Rein mengenmäßig überwiegen unstrukturierte Daten in der Welt. Allerdings
sind strukturierte Daten einfacher zu verarbeiten; wir werden uns auf diese konzentrieren.
1.4
Signal und Rauschen
9
1.4 Signal und Rauschen
Die Aufgabe der Wissenschaft – oder sogar jeglichen Erkennens – kann man als zweistufigen Prozess betrachten: erstens Signale (Phänomene) erkennen und zweitens diese dann
erklären (Bogen und Woodward 1988; Silver 2012). Signale erkennen ist der Versuch,
aus Daten ein Muster, Regularitäten, herauszulesen. Das impliziert, dass Daten dieses
Muster nicht direkt offenbaren und nicht identisch mit dem Muster sind. Mit dem alten
Bild, im Rauschen ein (leises) Geräusch, das Signal, herauszuhören, ist der Sachverhalt
gut beschrieben (Haig 2014). Daten sind vergänglich, veränderlich, vorübergehend – und
für den erkennenden Verstand ohne Interesse. Das Muster ist es, welches von alleinigem
Interesse ist. Schält sich ein Muster heraus, ist es über die Zeit, Erhebungsmethode und
Situation hinweg stabil, so hat man ein Phänomen identifiziert. Im Gegensatz zu Daten ist
ein Phänomen unbeobachtbar; es ist die Abstrahierung der Gemeinsamkeit aus der Konkretheit der Daten. Ein Phänomen könnte sein, dass „Männer, die Windeln kaufen, auch
Bier kaufen“. Ist ein Phänomen identifiziert, so ist die wichtigste Frage zumeist, was die
Ursache des Phänomens ist. Wissenschaftliche und Alltagstheorien untersuchen Phänomene, nicht Daten. Warum ist es wichtig, Ursachen von Phänomen zu kennen? Ein Grund
ist, dass man das Auftreten eines Phänomens beeinflussen kann, wenn man seine Ursache
kennt. Überspitzt formuliert: Die Ursache der Entzündung sind Bakterien? Entferne die
Bakterien, und die Entzündung klingt ab.
Abb. 1.3 stellt den Unterschied (und den Zusammenhang) von Rauschen und Signal dar. Der linke Teil des Diagramms zeigt die Körpergröße einer Reihe von Frauen
und Männern (und damit von zwei Variablen). Die Beobachtungseinheit, als schwarzer
Punkt dargestellt, ist eine Person. Wie man sieht, unterscheiden sich die Körpergrößen
der Personen; einige sind größer, andere kleiner. Auch zwischen den Geschlechtern gibt
es Unterschiede. Unser Auge ist schnell mit der Erkenntnis des Musters, dass „Männer
größer sind als Frauen“. Nicht alle Männer sind größer als alle Frauen; einige Frauen
sind größer als einige Männer. Aber in den meisten Fällen gilt: Der Mann ist größer als
die Frau. Anders betrachtet: Der „mittlere“ Mann ist größer als die „mittlere“ Frau (als
Quadrat bzw. Linie dargestellt im rechten Teil der Abb. 1.4). Als Beobachtung in den Daten existiert aber weder die mittlere Frau noch der mittlere Mann; wir erkennen dies als
Phänomen in den Daten bzw. aus den Daten heraus.
Das Rauschen in den Daten kann vielerlei Ursachen haben: Messfehler, Besonderheiten
der ausgewählten Merkmalsträger oder der Situation. Zufall ist keine Ursache im strengen Sinne des Wortes; in der Regel wird dieser Begriff verwendet, wenn man die wahre
Ursache nicht kennt.4 Experimentieren ist nichts anderes als die Kunst, Rauschen vor
der Messung zu verringern. Genauer gesagt solches Rauschen zu verringern, welches das
Signal sonst überlagert. Analog kann man sagen, dass Datenanalyse das Ziel hat, Rau4
Diesem Gedanken hinterliegt ein deterministisches Weltbild; strittige quantentheoretische Phänomene (Jaynes 2003) und der (meiner Meinung nach) freie menschliche Wille sind davon
ausgeklammert.
1
220
220
200
200
height
height
10
180
160
Statistik heute
180
160
man
woman
man
sex
woman
sex
220
220
200
200
height
height
Abb. 1.3 Muster und Rauschen
180
160
180
160
man
woman
sex
man
woman
sex
Abb. 1.4 Das Ziehen von Stichproben birgt Zufall
schen nach der Messung zu entfernen. Die Entdeckung eines Phänomens hat immer etwas
von einer Erfindung, da ein Phänomen nicht beobachtet, sondern erschlossen wird. Daher
gibt es auch nicht die eine richtige Statistik-Technik, die in einer bestimmten Situation
anzuwenden ist (zumindest gibt es die nicht immer). Vielmehr wird man verschiedene
Methoden, Daten und Philosophien nutzen, um der Phänomene habhaft zu werden.
Es wurde bereits eine gewisse Geringschätzung für Daten ausgedrückt, mit der Begründung, dass nur Phänomene interessant seien. Es stimmt, dass für die Erkenntnis und
für weitere Erklärungen nur Phänomene eine Rolle spielen. Allerdings: Ohne Daten keine
Muster. Als Grundlage für Phänomene sind Daten natürlich unerlässlich. Angenommen,
wir hätten nur einen Mann und eine Frau aus unserer Stichprobe. Wie wahrscheinlich
ist es, dass wir das gleiche Muster (den gleichen Unterschied in der Körpergröße) finden
würden (s. Abb. 1.4, links)? Sicherlich wären wir mit viel mehr Ungewissheit konfrontiert:
Das Phänomen (der wahre Größenunterschied) wäre schwieriger zu entdecken. Kleinere
Stichproben bergen mehr Raum für Zufall als große (unter sonst gleichen Umständen;
s. Abb. 1.4, rechts). Gleichzeitig wären wir gewisser über das Muster, wenn die Männer
1.4
Signal und Rauschen
11
alle ähnlich groß wären (sagen wir zwischen 1.80 und 1.81 m) und auch die Frauen sich
ziemlich in der Größe ähnelten (sagen wir zwischen 1.65 und 1.66 m). Sinkt die Streuung
(Variabilität) der Beobachtungen, steigt die Gewissheit hinsichtlich des Phänomens.
Aufgaben
Richtig oder falsch?5
1.
Eine gängige Unterteilung der Statistik erfolgt in die drei Bereiche deskriptiv,
inferierend und explikativ.
2. Aufgabe der Inferenzstatistik ist es, Daten prägnant zusammenzufassen.
3. Zu den Wissensgebieten der Datenanalyse zählen Wissen um Wahrscheinlichkeit, Kausalität, stochastische Modelle, Normalverteilung.
4. Den Begriff Daten kann man definieren als Information ohne Kontext.
5. Wesentliches Merkmal von Tabellen, wie in diesem Text verstanden, ist eine
Organisation aus Zeilen und Spalten; eine rechteckige Struktur ist nicht nötig.
6. Unter einer Beobachtungseinheit versteht man den Gegenstand, der in den Zeilen einer Tabelle auftauchen soll.
7. Wissenschaft kann man als zweistufigen Prozess verstehen: Signale erkennen
und Daten erklären.
8. Wissenschaftliche Theorien beziehen sich auf Daten, nicht auf Phänomene.
9. Experimentieren ist die Kunst, Rauschen vor der Messung zu verringern.
10. Kleinere Stichproben bergen mehr Raum für Zufall als große.
5
Lösungen: F, F, R, F, F, R, F, F, R, R.
2
Hallo, R
Lernziele
Die Geschichte von R in kurzer Form kennen
Vor- und Nachteile der Verwendung von R unterscheiden
Vor- und Nachteile von Skriptsprachen wie R einem „Klickprogramm“ wie Excel
gegenüberstellen können
2.1
Eine kurze Geschichte von R
Was ist R? Wo kommt es her? Und warum dieser komische Name? Was R ist, ist einfach
zu beantworten: Ein Dialekt der Programmiersprache S, die wiederum von John Chambers und anderen bei der Firma AT&T entwickelt wurde, beginnend im Jahre 1976; schon
etwas her (Peng 2014). S wurde einige Male hin und her verkauft, schließlich unter dem
Namen S-Plus. Das Besondere an S-Plus ist, dass es seine Wurzeln nicht in der typischen Programmierung, sondern in der Datenanalyse hat. S-Plus sollte eine interaktive
Umgebung bieten, in der der Nutzer schnell und unkompliziert einige Analysen ausführen
kann (Peng 2014). Das ist der Grund, warum man in R genauso wie in S-Plus einzelne Zeilen direkt ausführen kann (ein Interpreter), anstatt ein komplettes Programm zu
schreiben, das nur als Ganzes in Computersprache übersetzt (kompilieren) und ausgeführt
werden kann (ein Compiler). Allerdings sollte S-Plus gleichzeitig die Möglichkeit bieten,
größere Programme zu schreiben, und alle wichtigen Eigenschaften einer vollwertigen
Programmiersprache aufweisen, so dass fortgeschrittene Nutzer anspruchsvolle Syntax
(Ausdrücke der Programmiersprache; Code) schreiben können. Man kann S-Plus und R
also als Skriptsprache bzw. Programmiersprache oder (auch) als interaktive Analyseumgebung bezeichnen. Unter einer Skriptsprache versteht man eine Programmiersprache, die
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_2
13
14
2
Hallo, R
für schnelle Entwicklung, kleinere Programme oder eben schnelles Ausprobieren (interaktive Analyse) ausgelegt ist. Ein Interpreter ist praktisch, weil man „mal eben schnell“ eine
Zeile Code bzw. die Änderung einer Zeile Code in einem längeren Programm betrachten
kann. Ein Nachteil eines Interpreters ist die geringere Geschwindigkeit beim Ausführen,
da immer wieder Code in Maschinensprache übersetzt werden muss.
Ein Nachteil von S ist, dass es kommerziell vertrieben wird, wie andere StatistikSoftware auch; das stellt eine Hemmschwelle für Nutzer dar. Im Jahr 1991 entwickelten
Ross Ihaka und Robert Gentleman von der Uni Auckland in Neuseeland [Dank an Ross
und Robert] R explizit für Datenanalyse und Datenvisualisierung (vgl. Ihaka und Gentleman (1996)). R ist frei – frei wie in Freibier und frei wie in Freiheit!1 ) Die Syntax von
R ist der von S-Plus noch immer sehr ähnlich, allerdings hat sich R stark entwickelt seit
seiner Geburt in den 1990er Jahren. Die Freiheit von R ist eine große Stärke: Alle, die sich
berufen fühlen, können die Funktionalität von R erweitern. Wenn Sie eine geniale Idee für
eine neue Methode der Statistik haben, nur zu, Sie können sie einfach für die ganze Welt
verfügbar machen. Und die ganze Welt kann Ihre Idee einfach nutzen. Das führte dazu,
dass es eine unglaublich reichhaltige Fülle an Erweiterungen für R gibt (sog. Pakete), um
eben jene neuen Ideen, wie genial auch immer, für Ihre Analysen nutzbar zu machen.
R ist für alle gängigen Betriebssysteme verfügbar (Windows, Mac, Linux) und steht an
einem zentralen Ort, dem Comprehensive R Archive Network2 , kurz CRAN, zum Download bereit. Dort findet man auch weitere Informationen wie häufige Fragen oder Anleitungen und eine große Zahl an Erweiterungen für R. Genauer gesagt, beinhaltet das
„Standard-R“, wie man es sich von CRAN herunterlädt, schon ein paar Pakete, die sozusagen von Haus aus mitgeliefert werden. Dazu zählt das Paket base, das grundlegende
Funktionen enthält, und noch gut zwei Dutzend weitere.3
Wie jedes Ding, oder wie jede Software, hat R auch Schwächen. Der R-Kern ist vergleichsweise alt und einige Erblasten werden aus Kompatibilitätsgründen mitgeschleppt.
Aber grundlegende Funktionsweisen von R zu ändern, könnte dazu führen, dass einfacher Standard-R-Code auf einmal nicht mehr funktioniert. Wer weiß, was dann passiert?
Studenten lernen dann vielleicht Falsches zum p-Wert, mein Download bricht vorzeitig
ab und Ihr Aktien-Forecast macht Sie doch nicht zum Millionär . . . Das ist der Grund,
warum Neuerungen bei R nicht im „Kern“, in Standard-R, sondern in den Paketen vonstatten gehen.
R wurde (und wird) eher von Statistik-Freunden denn von waschechten Programmierern entwickelt und genutzt; das hat R den Vorwurf eingebracht, nicht so effizient und
breit nutzbar zu sein wie andere Programmiersprachen. Tatsächlich ist in Kreisen, die
1
R ist unter der GNU-Lizenz veröffentlicht; https://en.wikipedia.org/wiki/R_(programming_
language); unter http://www.fsf.org/ finden Sie Details, was freie Software bedeutet.
2
https://cran.r-project.org/.
3
https://stat.ethz.ch/R-manual/R-devel/doc/html/packages.html.
2.2 Warum R? Warum, R?
15
eher aus der Informatik stammen, die Programmiersprache Python4 verbreiteter. In einigen Foren tobt die Diskussion, welche von beiden Sprachen den größten Nutzerkreis
habe, am coolsten sei und so weiter. Python hat auch einige Vorteile, die hier aus Gründen der Befangenheit begrenzten Seitenzahl nicht ausgeführt werden. Es gibt auch einige
Neuentwicklungen, zum Beispiel die Programmiersprache Julia5 , die schneller rechnet
als R. Schwer zu sagen, welche Programmiersprache in ein paar Jahren Platzhirsch sein
wird. Vorhersagen sind bekanntlich schwierig, gerade wenn sie die Zukunft betreffen.
Aber plausibel ist, dass ein so dynamisches Feld wie die Datenanalyse genug Platz für
mehrere Ökosysteme bietet.
2.2 Warum R? Warum, R?
2.2.1
Warum R?
Warum sollte man Daten mit R analysieren und nicht mit . . . Excel, zum Beispiel?
Vielleicht ist die schärfere Unterscheidung zu fragen, ob man eine Programmiersprache
(wie R) verwendet oder eine „klickbare Oberfläche“ (wie Excel). Für beide Kategorien
gibt es einige Vertreter, und die im Folgenden aufgeführten Vor- und Nachteile gelten
allgemein für Programmiersprachen in Abgrenzung zu klickbaren Programmen. Wenn
auch hier vertreten wird, dass eine Programmiersprache wie R für ernsthafte Datenanalyse besser geeignet ist, so ist die Frage, welche Programmiersprache zweitrangig ist.
Neben R ist Python auch sehr stark verbreitet, allerdings ist R für einen Einstieg sehr gut
geeignet: Nach einer aktuellen Analyse basierend auf Textmining von Posts bei https://
www.stackoverflow.com ist R die am meisten geschätzte Programmiersprache (Robinson
2017). Die Unterscheidung Programmiersprache vs. klickbare Oberfläche lässt außen vor,
dass es Zwischenstufen gibt: So kann man mit Excel und SPSS auch Syntax schreiben
und für R gibt es klickbare Oberflächen.6
R ist beliebt und beleibt, was die Anzahl von „Paketen“ angeht, also Erweiterungen im
Sinne von neuen Funktionen von R. Ende 2017 gab es 13201 R-Pakete; Tendenz steigend.
Abb. 2.1 zeigt im linken Teil die Gesamtanzahl von R-Paketen auf CRAN, dem zentralen
Ablageort (Repositorium; eine Art „Webstore“) für R-Pakete (Rickert 2017). Der mittlere
und rechte Teil zeigen die Anzahl der neuen Pakete pro Monat (rechts mit logarithmischer
Achse; da die Steigung rechts linear aussieht, deutet es auf exponentielle Steigung hin).
Unter Daten liebenden Wissenschaftlern ist R die vielleicht am weitesten verbreitete Wahl;
aber auch Unternehmen wie Google, Apple oder Twitter setzen (auch) auf R (Sight 2017).
So hat Microsoft vor einiger Zeit eine führende Firma im R-Umfeld aufgekauft.
4
https://www.python.org/.
https://julialang.org/.
6
Bekannt und hilfreich ist der R-Commander (Fox 2005).
5
16
2
Hallo, R
10000
7500
5000
2500
Anzahl neuer R-Pakete
Anzahl neuer R-Pakete
Anzahl R-Pakete
250
200
150
100
100
50
10
2008
2010
2012
2014
2016
2018
2008
2010
Zeit
2012
2014
2016
2018
2008
Zeit
2010
2012
2014
2016
2018
Zeit
Abb. 2.1 Anzahl (neuer) R-Pakete auf CRAN
Was spricht für R, im Gegensatz zu Excel?
1. R kennt die modernen Verfahren – Excel nicht.
Moderne Verfahren der Datenanalyse, speziell aus der prädiktiven Modellierung sind
in R sehr gut vertreten. In Excel nicht oder viel schwächer. So beinhaltet das R-Paket
caret aktuell 237 verschiedene prädiktive Modelle – normale Regression ist eines
davon. Diese Vielfalt gibt es in Excel nicht. Außerdem sind die Verfahren, die es in
Excel gibt, zumeist nicht die neuesten. In R können neue Verfahren ohne Zeitverlust
der Allgemeinheit zur Verfügung gestellt werden. Daher ist man am Puls der Zeit,
wenn man mit R arbeitet. In Excel nicht.
2. R ist reproduzierbar – Excel nicht.
In R ist der „Rohstoff“ (Daten) von den „Verarbeitungsschritten“ (Analyseverfahren
wie Mittelwert bilden) getrennt; in Excel vermengt.7 In Excel sind Daten und Analysebefehle in einer Tabelle vermengt; teilweise sogar in einer Zelle. Das macht das
Vorgehen intransparent; in welcher Zelle hatte ich noch mal den 78. Arbeitsschritt
geschrieben? Und war es eigentlich der 78. oder doch der 79. Schritt? In dieser vermengten Form ist es schwierig, die Analyse zu dokumentieren. Genauso schwierig
ist es, Fehler zu finden. Komplexe Exceltabellen an andere Menschen (inklusive Ihr
zukünftiges Ich) weiterzugeben, ist daher problematisch. In jüngerer Zeit sind einige
Fehler in Datenanalysen bekannt geworden, die auf der Verwendung von Excel beruhen. So berichten Ziemann et al. (2016), dass etwa ein Fünftel von Fachartikeln in
führenden Zeitschriften Fehler enthalten, die von Excel erzeugt wurden. So wurden
z. B. Gennamen wie „Septin 2“ von Excel unaufgefordert in ein Datum (September)
7
Wir lassen hier außen vor, dass man mit Excel auch Skripte schreiben kann.
2.2 Warum R? Warum, R?
17
umformatiert; damit wurde die eigentliche Information zerstört. Solche Fehler sind
schwer zu finden, wenn Daten und Analyseverfahren vermengt sind, wie in Excel (vgl.
Krugman (2013)). Aktuell wird verstärkt diskutiert über Forschungsergebnisse, die
sich nicht bestätigen lassen (Begley und Ioannidis 2015; Open Science Collaboration
2015); das sind alarmierende und schockierende Nachrichten. Wenn schon in der Wissenschaft, wo die strengsten Maßstäbe an Sorgfalt angelegt werden, so viele Fehler
auftauchen, wie mag es an in der „Praxis“ aussehen, wo häufig „pragmatisch“ vorgegangen wird? Die Möglichkeit, eine Analyse effizient nachzuvollziehen, ist daher
zentral. Reproduzierbarkeit ist daher eine zentrale Forderung zur Sicherung der Qualität einer Datenanalyse. Mit R ist das ohne Weiteres möglich; mit Excel ist es schwierig.
3. R ist automatisierbar – Excel nicht (oder weniger).
Hat man eine Analyse „verschriftlicht“, also ein Skript geschrieben, in dem alle Befehle niedergeschrieben sind, kann man dieses Skript starten, und die Analyse wird
automatisch ausgeführt. Ähnliches ist mit Excel deutlich schwieriger. Problemlos kann
man in R die Analyse parametrisieren, also z. B. sagen „Hey, führe die Analyse mit
den Geschäftszahlen letzter Woche durch und nimm das Stylesheet „mightyDonald“.“
In Excel? Schwierig. Alle 78 Schritte der letzten Datenanalyse noch einmal durchzuklicken, begünstigt Sehnenscheidenentzündung und ist fehleranfällig. Ein R-Skript
mit einem kurzen Befehl oder einem „Run-Button“ auszuführen, ist deutlich einfacher.
4. R ist frei: quelloffen und kostenlos – Excel nicht.
Sie möchten wissen, wie eine bestimmte Berechnung genau durchgeführt wird, sozusagen wissen, was unter der Motorhaube passiert? Mit R kein Problem. Sie möchten
diese Berechnung ändern? Mit R kein Problem. Außerdem möchten Sie diese Berechnung einfach an interessiertes Fachpublikum weitergeben dürfen? Mit R kein Problem.
R ist ein Computerprogramm, das von Freiwilligen gepflegt und weiterentwickelt wird.
Jeder kann mitmachen. Kostet nix. Manch kommerzielles Programm für Datenanalyse
hingegen ist sehr teuer.
5. R erstellt elegante Diagramme – Excel nicht.
Abb. 2.2 zeigt Beispiele für hochwertige Abbildungen, die mit R erstellt wurden (links:
Ein Circlize-Plot mit dem R-Paket circlize; Zuguang (2017); rechts: Kartenmaterial visualisiert; Kashnitsky (2017))
2.2.2
Warum, R?
Die Kehrseite von R ist die aufwändigere Einarbeitung. Es dauert länger mit R als mit
Excel, bis man die Grundlagen beherrscht, also einfache Arbeitsschritte selber ausführen
kann. Die Aufwandskurve ist bei einfachen, grundlegenden Tätigkeiten steiler als bei der
Arbeit mit Excel (s. Abb. 2.3); das kann frustrieren.8 Bei zunehmender Komplexität wird
der Aufwand, ein Problem zu lösen, mit R aber schließlich geringer als mit Excel. Das
8
http://youtube.com/watch?v=PbcctWbC8Q0.
18
2
Hallo, R
Aufwand
Abb. 2.2 Beispiele für Datenvisualisierung mit R
R
Excel
Komplexität des Problems
Abb. 2.3 Schwierigkeiten mit R
liegt daran, dass, wie bereits gesagt, komplexe Analyseverfahren in Excel nicht oder nur
umständlich verfügbar sind.
Eine andere Eigenart einer Programmiersprache kann bisweilen für Frust sorgen: Man
hackt ein paar geniale Befehle ein, freut sich, erwartet Wunderbares, und es passiert . . .
nichts. Oder es kommt eine Fehlermeldung, zumeist präzise, verständlich und konstruktiv.
Ein R-Klassiker ist: „Object of type closure is not subsettable“.9 Diese Fehlermeldung,
9
https://imgflip.com/i/1z7avz.
2.2 Warum R? Warum, R?
19
dem R-Anwärter wenig verständlich, tritt dann auf, wenn man Versucht, eine Funktion
(closure) zu indizieren (s. Abschn. 5.3).
Zum Glück gibt es im Internet einen regen Austausch an Fragen zu R; http://www.
stackoverflow.com ist die bekannteste Anlaufstelle für Fragen zu R (s. Abschn. 3.8.1).
Eine Apologie von Schwierigkeiten beim Lernen von R, angelehnt an Dantes Göttliche Komödie, findet sich bei Burns (2012). Es gibt für R „klickbare Oberflächen“10 , die
die Arbeit vereinfachen können, ohne die Vorteile von R einzuschränken. Dazu zählen
RStudio (s. Kap. 3), das R-Paket R-Commander (Fox 2016) oder eigenständige, auf R
basierende Programme wie Jamovi11 oder Exploratory.12
Aufgaben
Richtig oder falsch?13
1.
2.
3.
4.
5.
6.
7.
R ist ein Dialekt der Programmiersprache Q.
R wurde zu Beginn kommerziell vertrieben.
R ist für Windows, Mac OS und Linux verfügbar.
Alternativen zu R sind Python, Julia und Excel.
Es gibt ca. 1000 R-Pakete auf CRAN.
Excel vermengt Syntax und Daten; R nicht.
Trennung von Syntax und Daten verbessert (potenziell) die Reproduzierbarkeit
einer Analyse.
8. Etwas zugespitzt könnte man formulieren, dass in R komplexe Sachen einfach,
einfache Aufgaben aber komplex sind.
9. CRAN ist ein Ort bzw. eine Webseite, auf der man R kaufen kann.
10. Python ist eine Programmiersprache, die für die Datenanalyse nicht geeignet
ist.
10
GUIs, Grafische Benutzeroberflächen.
https://www.jamovi.org/.
12
https://exploratory.io/.
13
F, F, R, F, F, R, R, R, F, F.
11
3
R starten
Lernziele
In der Lage sein, einige häufige technische Probleme zu lösen
R-Pakete installieren können
Einige grundlegende R-Funktionalitäten verstehen
Als Haupt-Analysewerkzeug nutzen wir R; daneben wird uns die sog. Entwicklungsumgebung (integrated development environment; IDE) RStudio einiges an komfortabler
Funktionalität bescheren.1 Eine ganze Reihe von R-Paketen (Packages; d. h. Erweiterungen) werden wir auch nutzen.
3.1 R und RStudio installieren
Sie können R unter https://cran.r-project.org herunterladen und installieren (für Windows,
Mac oder Linux). RStudio finden Sie auf der gleichnamigen Homepage: https://www.
rstudio.com; laden Sie die Desktop-Version für Ihr Betriebssystem herunter. RStudio ist,
vereinfacht gesagt, „nur“ eine Oberfläche (GUI; Graphical User Interface) für R, mit einer
R von praktischen Zusatzfunktionen.2 Die eigentliche Arbeit verrichtet das „normale“ R,
welches automatisch gestartet wird, wenn Sie RStudio starten (sofern R installiert ist). Die
1
Unter einer IDE versteht man ein Computerprogramm, das eine Reihe hilfreicher Werkzeuge für
die Entwicklung von Software bereitstellt.
2
Ehrlich gesagt ist das schamlos untertrieben; RStudio kann viel. Aber für den Anfang ist es eine
nützliche Vorstellung.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_3
21
22
3 R starten
Abb. 3.1 RStudio
Oberfläche von RStudio sieht unter allen Betriebssystemen etwa so aus wie in Abb. 3.1
dargestellt.
Das Skriptfenster von RStudio ähnelt einem normalen Text-Editor; praktischerweise
finden Sie aber einen Button „run“, der die aktuelle Zeile oder die Auswahl „abschickt“,
d. h. in die Konsole gibt, wo die Syntax ausgeführt wird.3 Wenn Sie ein Skript-Fenster
öffnen möchten, so können Sie das Icon
klicken (Alternativ: Ctrl-Shift-N oder File
> New File > R Script). Aus dem Fenster der Konsole spricht R zu uns bzw. sprechen
wir mit R. Wird ein Befehl (synonym: Funktion) hier eingegeben, so führt R ihn aus.
Es ist aber viel praktischer, Befehle in das Skript-Fenster einzugeben als in die Konsole.
Behalten Sie die Konsole im Blick, wenn Sie eine Antwort von R erwarten. Im Fenster
Globale Umgebung* (engl. environment) zeigt R, welche Variablen (Objekte) vorhanden
sind. Stellen Sie sich die Umgebung wie einen Karpfenteich vor, in dem die Datensätze und andere Objekte herumschwimmen. Was nicht in der Umgebung angezeigt wird,
existiert nicht für R. Kurz gesagt: Die Daten, die Ihr R kennt, werden in der Umgebung
angezeigt. Im Fenster rechts unten werden mehrere Informationen bereitgestellt, z. B. werden Diagramme (Plots) dort ausgegeben; auch Informationen zu Paketen (Packages) und
Hilfeseiten (Help) finden sich dort. Klicken Sie mal die anderen Reiter im Fenster rechts
unten durch.
3
Achten Sie darauf, dass Ihre Skriptdatei die Endung .r oder .R hat.
3.2 Pakete
23
Wenn Sie RStudio starten, startet R automatisch auch. Starten Sie daher, wenn Sie
RStudio gestartet haben, nicht noch extra R. Damit hätten Sie sonst zwei Instanzen von R laufen, was zu Verwirrung (bei R und beim Nutzer) führen kann. Wer
Shortcuts mag, wird in RStudio überschwänglich beschenkt; der Shortcut für die
Shortcuts ist Shift-Alt-K.
3.2 Pakete
Ein Großteil der Neuentwicklungen bei R passiert in sog. Paketen, das sind Erweiterungen für R. Diese Erweiterungen beherbergen zusätzliche Funktionen und manchmal auch
Daten und mehr. Jeder, der sich berufen fühlt, kann ein R-Paket schreiben und es zum
„R-Appstore“ (CRAN4 ) hochladen. Von dort kann es dann frei (das heißt auch kostenlos)
heruntergeladen werden. Einige Pakete werden mit dem Standard-R mitgeliefert, aber ein
Großteil der Funktionalität von R muss bzw. kann nach Belieben nachinstalliert werden.
Am Anfang vergisst man gerne, dass ein Paket installiert sein muss, bevor man seine
Funktionen nutzen kann.
3.2.1 Pakete von CRAN installieren
Am einfachsten installiert man R-Pakete in RStudio über den Button Install im Reiter
Packages (s. Abb. 3.2). Beim Installieren von R-Paketen könnten Sie gefragt werden, welchen „Mirror“ Sie verwenden möchten. Das hat folgenden Hintergrund: R-Pakete sind in
einer Art „App-Store“, mit Namen CRAN (Comprehense R Archive Network) gespei-
Abb. 3.2 So installiert man
Pakete in RStudio
4
https://cran.r-project.org/
24
3 R starten
chert. Damit nicht ein armer, kleiner Server überlastet wird, wenn alle Menschen dieser
Welt just gerade beschließen sollten, ein Paket herunterzuladen, gibt es viele Kopien dieses Servers – seine Spiegelbilder (Mirrors). Suchen Sie sich einfach einen Server aus,
der in der Nähe steht oder der Ihnen vorgeschlagen wird. Sie können Pakete auch per Befehl installieren; das geht so: install.packages("name_des_pakets"). Also zum
Beispiel:
install.packages("tidyverse")
Der Klick auf den Menüpunkt Install löst diesen R-Befehl zur Installation von Pakete aus
(s. Abb. 3.2).
3.2.2 Pakete installieren vs. Pakete starten (laden)
Nicht vergessen: Installieren muss man eine Software nur einmal; starten (laden) muss man
die R-Pakete jedes Mal, wenn man sie vorher geschlossen hat und wieder nutzen möchte.5
Ein Paket, z. B. tidyverse, laden Sie mit diesem Befehl: library(tidyverse). Der
Befehl bedeutet sinngemäß: „Hey R, geh in die Bücherei (library) und hole das Buch
(package) tidyverse!“ Alternativ können Sie in RStudio unter dem Reiter Packages den
Namen des Pakets anklicken.
Wann benutzt man bei R Anführungszeichen? Im Detail ist es Kraut und Rüben,
aber die Grundregel lautet: Wenn man Text anspricht, braucht man Anführungsstriche. Sprechen Sie existierende Daten oder Befehle an, brauchen Sie hingegen keine
Anführungsstriche. Aber es gibt Ausnahmen: Im Beispiel library(tidyverse)
ist tidyverse hier erst mal für R nichts Bekanntes, weil noch nicht geladen.
Demnach müssten eigentlich Anführungsstriche (einfache oder doppelte) stehen.
Allerdings meinte ein Programmierer, dass es doch bequemer sei, so ohne Anführungsstriche – spart man doch zwei Anschläge auf der Tastatur. Recht hat er.
Aber bedenken Sie, dass es sich um die Ausnahme einer Regel handelt. Sie können also auch schreiben: library("tidyverse"), das entspricht der normalen
R-Rechtschreibung.
Zu Beginn jedes Kapitels dieses Buchs stehen die R-Pakete, die in dem jeweiligen
Kapitel verwendet werden. Denken Sie daran, die benötigten Pakete zu installieren und zu
starten, bevor Sie in dem Kapitel arbeiten. Am Anfang vergisst man gerne mal, ein Paket
zu starten, und wundert sich dann, warum eine Funktion R unbekannt ist.
5
Genauer gesagt wird das Paket nicht geladen, sondern ausgewählte Funktionen und Daten werden
dem Suchpfad angefügt. Damit werden diese Objekte gefunden, auch wenn man nicht die Umgebung, d. h. den Namen des Pakets, angibt.
3.3 Hilfe! R startet nicht!
25
Wenn Sie R bzw. RStudio schließen, werden alle gestarteten Pakete ebenfalls geschlossen. Sie müssen die benötigten Pakete beim erneuten Öffnen von RStudio
wieder starten.
Möchte man einen Überblick bekommen, wofür ein Paket, z. B. tidyverse, gut ist,
gibt man am besten ein: help(tidyverse) bzw. ?tidyverse. Daraufhin wird die
Hilfe- bzw. Infoseite des Pakets geöffnet.
3.2.3 Pakete wie pradadata von Github installieren
Github ist ein weiterer Ort, an dem man viele R-Pakete finden kann. Im Unterschied zu
CRAN, wo der Autor eines Paket zu einigen Qualitätschecks gezwungen wird, ist der
Autor bei Github sein eigener Herr. Daher finden sich häufig Pakete, die noch in einer
früheren Entwicklungsphase sind, auf Github. Um Pakete von Github zu installieren, kann
man den Befehl install_github() verwenden, der aus dem Paket devtools kommt.
Die Daten dieses Buches finden sich in einem Paket auf Github (pradadata).6 Daher
installieren Sie bitte zuerst devtools und dann das Paket pradadata:
install.packages("devtools", dependencies = TRUE)
library(devtools)
install_github("sebastiansauer/pradadata")
3.3 Hilfe! R startet nicht!
Manntje, Manntje, Timpe Te,
Buttje, Buttje inne See,
myne Fru de Ilsebill
will nich so, as ik wol will.
– Gebrüder Grimm, Märchen vom Fischer und seiner Frau7
Ihr R startet nicht oder nicht richtig? Die drei wichtigsten Heilmittel sind:
1. Schließen Sie die Augen für eine Minute. Denken Sie an etwas Schönes.
2. Schalten Sie den Rechner aus (komplett herunterfahren) und probieren Sie es morgen
noch einmal.
3. Fragen Sie das Internet.
6
7
Im Abschn. 3.6 finden Sie mehr Hinweise zu den Daten dieses Buches.
https://de.wikipedia.org/wiki/Vom_Fischer_und_seiner_Frau.
26
3 R starten
Entschuldigen Sie die schnoddrigen Tipps. Aber: Es passiert allzu leicht, dass man Fehler
wie diese macht:
OH NO
install.packages(teidyvörse)
install.packages("Tidyverse")
install.packages("tidy verse")
Keine Internet-Verbindung beim Installieren von Paketen
library(tidyverse) # ohne tidyverse vorher zu installieren
Wenn R oder RStudio dann immer noch nicht startet oder nicht richtig läuft, probieren
Sie Folgendes:
Sehen Sie eine Fehlermeldung, die von einem fehlenden Paket spricht (z. B. „Package
‚Rcpp‘ not available“) oder davon, dass ein Paket nicht installiert werden konnte (z. B.
„Package ‚Rcpp‘ could not be installed“ oder „es gibt kein Paket namens ‚Rcpp‘“ oder
„unable to move temporary installation XXX to YYY“), dann tun Sie Folgendes:
– Schließen Sie R und starten Sie es neu.
– Installieren Sie das oder die angesprochenen Pakete.
– Starten Sie das entsprechende Paket mit library(name_des_pakets).
Gerade bei Windows 10 scheinen die Schreibrechte für R (und damit RStudio oder
RCommander) eingeschränkt zu sein. Ohne Schreibrechte kann R aber nicht die Pakete („packages“) installieren, die Sie für bestimmte R-Funktionen benötigen. Daher
schließen Sie R bzw. RStudio und suchen Sie das Icon von R oder wenn Sie RStudio
verwenden von RStudio. Rechtsklicken Sie das Icon und wählen Sie „als Administrator
ausführen“. Damit geben Sie dem Programm Schreibrechte. Jetzt können Sie etwaige
fehlende Pakete installieren.
Ein weiterer Grund, warum R bzw. RStudio die Schreibrechte verwehrt werden könnten (und damit die Installation von Paketen), ist ein Virenscanner. Der Virenscanner
sagt, nicht ganz zu Unrecht: „Moment, einfach hier Software zu installieren, das geht
nicht, zu gefährlich.“ Grundsätzlich gut, in diesem Fall aber unnötig. Schließen Sie
R/RStudio und schalten Sie dann den Virenscanner komplett aus. Öffnen Sie dann R/RStudio wieder und versuchen Sie, fehlende Pakete zu installieren. Danach schalten Sie
den Virenscanner wieder ein.
Verwenden Sie möglichst die neueste Version von R, RStudio und Ihres Betriebssystems. Um R oder R-Studio zu aktualisieren, laden Sie einfach die neueste
3.4 Zuordnung von Paketen zu Befehlen
27
Version herunter und installieren Sie sie. Ältere Versionen führen u. U. zu Problemen; je älter, desto Problem. . . Updaten Sie Ihre Packages regelmäßig z. B. mit
update.packages() oder dem Button Update bei RStudio (Reiter Packages).
3.4 Zuordnung von Paketen zu Befehlen
Woher weiß man, welche Befehle (oder auch Daten) in einem Paket enthalten sind?
Eine einfache Möglichkeit ist es, beim Reiter „Pakete“ auf den Namen eines der installierten Pakete zu klicken. Daraufhin öffnet sich die Dokumentation des Pakets und man
sieht dort alle Befehle und Daten aufgeführt (s. Abb. 3.3). Übrigens sehen Sie dort auch
die Version eines Pakets.8 Für geladene Pakete kann man auch den Befehl help nutzen,
z. B. help(ggplot2).
Und umgekehrt, woher weiß ich, in welchem Paket ein Befehl „wohnt“?
Probieren Sie den Befehl help.search("qplot"), wenn Sie wissen möchten, in
welchem Paket qplot zuhause ist. help.search sucht alle Hilfeseiten von installierten Paketen, in denen der Suchbegriff vorkommt. Um das Paket eines geladenen Befehl zu
finden, hilft der Befehl find("qplot"). Sie können auch den Befehl find_funs aus
dem Paket prada nutzen; dann müssen Sie aber zuerst dieses Paket von Github installieren9 (s. Abschn. 3.2.3): z. B. prada::find_funs("select"). Über die Seite http://
search.r-project.org/nmz.html lassen sich auch Funktionen suchen.
Falls bei einem Befehl kein Paket angegeben ist, heißt das, dass der Befehl im
„Standard-R“ wohnt – Sie müssen kein weiteres Paket laden.10 Also zum Beispiel
ggplot2::qplot: Der Befehl qplot ist im Paket ggplot2 enthalten. Das Zeichen ::
trennt also Paket von Befehl.
Manche Befehle haben Allerweltsnamen (z. B. filter()). Manchmal gibt
es Befehle mit gleichem Namen in verschiedenen Paketen. Befehle mit Allerweltsnamen (wie filter()) sind Kandidaten für solcherlei Verwechslung
(mosaic::filter() vs. dplyr::filter()). Falls Ihnen eine vertraute Funktion wirre Ausgaben oder eine diffuse Fehlermeldung liefert, kann es daran liegen,
dass R einen Befehl mit dem richtigen Namen, aber aus dem „falschen“ Paket zieht.
8
Sagt jemand mal zu Ihnen, „Sie sind ja outdated“, dann schauen Sie mal auf die Paket-Versionen;
das geht übrigens auch mit dem Befehl packageVersion("ggplot2"); in gleicher Manier
liefert getRversion() Ihre R-Version zurück.
9
https://github.com/sebastiansauer/prada.
10
Eine Liste der Pakete, die beim Standard-R enthalten sind (also bereits installiert sind), finden Sie
hier: https://stat.ethz.ch/R-manual/R-devel/doc/html/packages.html.
Abb. 3.3 Hier werden Sie geholfen: Die Dokumentation der R-Pakete
28
3 R starten
3.6 Daten
29
Geben Sie im Zweifel lieber den Namen des Pakets vor dem Paketnamen an, z. B.
so dplyr::filter(). Der doppelte Doppelpunkt trennt den Paketnamen vom
Namen der Funktion.
Woher weiß ich, ob ein Paket geladen ist?
Wenn im Reiter Packages in RStudio (s. Abb. 3.3) der Haken im Kästchen vor dem
Namen des Pakets gesetzt ist, dann ist das Paket geladen. Sonst nicht. Alternativ können
Sie den Befehl search() ausführen.
3.5 R-Skript-Dateien
Ein neues R-Skript in RStudio können Sie z. B. öffnen mit File > New File > R Script.
Schreiben Sie dort Ihre R-Befehle; Sie können die Skriptdatei speichern, öffnen, ausdrucken, übers Bett hängen . . . R-Skripte können Sie speichern (unter File > Save) und
öffnen. R-Skripte sind einfache Textdateien, die jeder Texteditor verarbeiten kann. Nur
statt der Endung .txt, sind R-Skripte stolzer Träger der Endung .R. Es bleibt aber eine schnöde Textdatei. Geben Sie Ihren R-Skript-Dateien die Endung .R; damit erkennt
RStudio, dass es sich um ein R-Skript handelt, und bietet ein paar praktische Funktionen
wie den „Run-Button“, der die aktuelle Zeile bzw. die markierten Zeilen per Mausklick
ausführt.
3.6 Daten
In diesem Buch spielen Datensätze eine große Rolle. Zu Beginn jedes Kapitels werden
die in dem Kapitel verwendeten Datensätze aufgeführt. In diesem Abschnitt erfahren Sie,
wo Sie die Daten finden. Alle Daten sind frei verfügbar; fast alle Daten können aus einem R-Paket geladen werden. Das ist meist am komfortabelsten. Es ist nicht nötig, dass
Sie sich sofort alle Daten auf einmal herunterladen. Aber Sie sollten sicherstellen, dass
Sie für das Kapitel, das Sie als nächstes bearbeiten möchten, die Daten zur Verfügung
(heruntergeladen) haben. Wie man Daten in R importiert, ist im Kap. 6 erläutert.
3.6.1 Datensätze aus verschiedenen R-Paketen
Datensatz profiles aus dem R-Paket okcupiddata (Kim und Escobedo-Land
2015); es handelt sich um Daten von einer Online-Singlebörse.
Datensatz flights aus dem R-Paket nycflights13 (Bureau of transportation statistics RITA 2013); es handelt sich um Abflüge von den New Yorker Flughäfen.
30
3 R starten
Datensatz boston aus dem Paket MASS; hier geht es um Immobilienpreise.
Datensatz GermanCredit aus dem Paket caret; Einflussgrößen von Kreditausfällen
werden untersucht.
Datensatz Affairs aus dem Paket AER (Fair 1978); es handelt sich um eine Umfrage
zu außerehelichen Affären.
Datensatz titanic_train aus dem Paket titanic von kaggle.com11 ; es handelt
sich um Überlebensraten vom Titanic-Unglück.
3.6.2 Datensätze aus dem R-Paket pradadata
Die folgenden Datensätze stehen nach Installation des Pakets pradadata zur Verfügung
(s. Abschn. 3.2.3). Alternativ können Sie die Datensätze auf der Webseite zum Buch12
herunterladen.
stats_test (Ergebnisse einer Statistikklausur, (Sauer 2017d)), wo_men (Körpergröße von Studenten, Sauer (2017c)), extra (https://osf.io/meyhp/; Umfrage zu Extraversion, Sauer (2017e)), Werte (Werte von Konsumenten), Gansser (2017), cultvalues
(https://osf.io/cqtwr/; Kulturwerte nach S. H. Schwartz (1999)), afd (Wahlprogramm
2016 der AfD, AfD (2016)), socec (sozioökonomische Daten von Deutschland zum Jahr
2017, Bundeswahlleiter (2017a)), elec_results (Wahlergebnisse der Bundestagswahl
2017, Bundeswahlleiter (2017b)), wellbeing (Wohlbefinden, OECD (2016)), sentiws
(Sentiment-Wörterbuch für Textmining Remus et al. (2010)), wahlkreise_shp (Kartendaten der deutschen Wahlkreise, Bundeswahlleiter (2017a, b)). In den Hilfeseiten des
Pakets pradadata finden sich weitere Hinweise wie die Erläuterung der Variablen.
Aufgrund der Größe des Datensatzes de_l (Karte von Deutschland, GeoBasis-DE,
BKG (2017)) ist dieser nicht im Paket pradadata enthalten, aber er ist über die URL
https://osf.io/nhy4e/ und auf der Webseite zum Buch verfügbar.
3.7 Grundlagen der Arbeit mit RStudio
Wenn man anfängt, sich mit R intensiver zu beschäftigen, kommt der Tag, an dem man
professioneller arbeiten möchte: Alle Dateien für ein Projekt sollen zusammen an einem
Ort liegen, das Arbeitsverzeichnis zeigt zu diesem Ort und es soll eine vernünftige Ordnerstruktur geben. RStudio bietet für diesen Zweck, den ich hier als Projektmanagement
bezeichne, eine Reihe von Hilfen. Dazu zählen das Spezifizieren des Arbeitsverzeichnisses, die Arbeit mit Projekten, das Mischen von Text und R-Code (s. Kap. 26), einfache
Publikationsmöglichkeiten und Versionierungswerkzeuge. Es ist sinnvoll, wenn alle Da11
12
https://www.kaggle.com/c/titanic/data.
https://sebastiansauer.github.io/modar/.
3.7 Grundlagen der Arbeit mit RStudio
31
Abb. 3.4 Das Arbeitsverzeichnis mit RStudio
auswählen
teien, die zu einem „Projekt“ im weiteren Sinne gehören, in einem gemeinsamen Ordner
liegen. Dateien kann man entlang der Arbeitsschritte in fünf Arten aufteilen13 :
Eingabe: Rohdaten und Erläuterung der Rohdaten (Metadaten)
Skripte: (R-)Syntax zur Verarbeitung der Rohdaten
Aufbereitete Daten: Als Zwischenstufe oder Ergebnis nach Durchlauf von Skripten
Ausgaben: Zumeist Diagramme und Dokumente, aber auch Applikationen, Webseiten
oder Datenbanken
Readme: Informationen über die anderen Dateien bzw. über das Projekt
3.7.1 Das Arbeitsverzeichnis
Man kann man in R ein beliebiges Verzeichnis als Arbeitsverzeichnis (working directory) deklarieren. Führt man dann eine Funktion aus, die z. B. eine Datendatei
einlesen soll, so sucht diese Funktion im Arbeitsverzeichnis (sofern man keinen anderen Pfad angibt). Fügt man einen relativen Pfad zu diesem Dateinamen hinzu, so
versteht R diesen Pfad als Unterordner des Arbeitsverzeichnisses. Ein Beispiel für
einen relativen Pfad ist mein/unterordner/datei.csv. Ein absoluter Pfad benennt den Pfad vom obersten Verzeichnis des Datenträgers und ggf. den Datenträger. Auf Unix-Betriebssystemen wie Mac OS sieht ein absoluter Pfad z. B. so aus:
/Users/sebastiansauer/Documents/datei.csv. Das Arbeitsverzeichnis kann
man mit getwd() erfragen und mit setwd() einstellen. Komfortabler ist es aber, das
aktuelle Verzeichnis per Menü zu ändern (vgl. Abb. 3.4). In RStudio: Session > Set
Working Directory > Choose Directory . . . (oder mit dem Shortcut, der dort angezeigt
wird).
13
Vgl. https://www.projecttier.org/tier-protocol/specifications/.
32
3 R starten
Es ist praktisch, das Arbeitsverzeichnis festzulegen, denn dann kann man z. B. eine Datendatei einlesen, ohne den Pfad eingeben zu müssen: daten_deutsch <read_csv("meine_daten.csv"). Auch bei kleineren Projekten ist es sinnvoll,
einen „Hauptordner“ als Arbeitsverzeichnis festzulegen (z. B. „Statistik_21“). Dort erstellt man sich eine Unterordner-Struktur (vgl. Abschn. 27.7.2) und legen Daten, Syntax
und sonstiges Material dort ab.
Spezifizieren Sie ein Arbeitsverzeichnis, wird dort der Inhalt Ihrer Umgebung (d. h.
alle geladenen Daten/Objekte) gespeichert, wenn Sie RStudio schließen und die
Option Save Workspace on Exit aktiviert haben (Tools > Global Options > Save
Workspace on Exit). Den Workspace (synonym: Umgebung oder global environment) kann man sich als Sammelbecken für alle aktuell in R geladenen bzw.
erzeugten Daten vorstellen. Allerdings sucht RStudio beim Starten nicht zwangsläufig in diesem Ordner. Per Doppelklick auf die Workspace-Datei .Rdata oder
über File > Open File . . . können Sie Ihren Workspace wieder laden. Es kann praktisch sein, die Objekte Ihrer letzten Arbeitssitzung wieder zur Verfügung zu haben.
Ist man aber darauf aus, „schönen“ R-Code zu schreiben, ist es teilweise sinnvoller,
eine Skript-Datei erneut ablaufen zu lassen, um sich die Datenobjekte neu berechnen
zu lassen; Hadley Wickham empfiehlt, nie den Workspace zu sichern bzw. wiederherzustellen (Wickham und Grolemund 2017).
3.7.2
RStudio-Projekte
Unter dem Menüpunkt File > New Projekt kann man ein neues Projekt anlegen. Dabei
wird man gefragt, ob man einen neuen Ordner anlegen oder auf einen vorhandenen Ordner
zurückgreifen möchte. So oder so, in diesem Ordner sollen dann alle relevanten Dateien
für das Projekt abgelegt werden. Dieser Ordner ist dann das Arbeitsverzeichnis Ihres Projekts. Man kann auch ein Projekt öffnen, welches Versionierung mit Git unterstützt (s.
Abschn. 27.7.3). Erstellt man ein neues Projekt, so fragt RStudio, um welche Art von
Projekt es sich handelt. Für spezielle Arten von Projekten – z. B. Pakete, Bücher, Blogs
oder Web-Applikationen – bietet RStudio spezielle Vorlagen an. Wir bleiben hier beim
allgemeinen Feld-Wald-Wiesen-Projekt („Empty Project“). Das Arbeitsverzeichnis von R
wird dabei automatisch auf diesen Ordner gesetzt. RStudio-Projekte sind eine praktische
Angelegenheit, deren Komfort man nicht missen möchte, wenn man mehr macht, als ein
bisschen herumzuspielen.
3.8 Hier werden Sie geholfen
3.8
33
Hier werden Sie geholfen
3.8.1 Wo finde ich Hilfe?
Es ist keine Schande, nicht alle Befehle der ca. 10000 R-Pakete auswendig zu wissen.
Schlauer ist, zu wissen, wo man Antworten findet. Hier eine Auswahl:
Zu einigen Paketen gibt es gute „Spickzettel“ (cheatsheets): z. B. ggplot2, RMarkdown,
dplyr, tidyr. Klicken Sie dazu in RStudio auf Help > Cheatsheets > . . . oder gehen Sie
auf https://www.rstudio.com/resources/cheatsheets/.
In RStudio gibt es eine Reihe (viele) von Tastaturkürzeln (Shortcuts), die Sie hier finden: Tools > Keyboard Shortcuts Help.
Für jeden Befehl aus einem geladenen Paket können Sie mit help() die HilfeDokumentation anschauen, also z. B. help("qplot").
Im Internet finden sich zuhauf Tutorials.
Der Reiter „Help“ bei RStudio verweist auf die Hilfe-Seite des jeweiligen Pakets bzw.
Befehls.
Die bekannteste Seite, um Fragen rund um R zu diskutieren, ist Stackoverflow (http://
stackoverflow.com).
3.8.2 Einfache, reproduzierbare Beispiele (ERBies)
Sagen wir, Sie haben ein Problem. Mit R. Oder R mit Ihnen. Bevor Sie jemanden bitten,
Ihr Problem zu lösen, haben Sie schon drei dreizehn dreißig Minuten recherchiert, ohne
Erfolg. Sie entschließen sich, bei https://www.stackoverflow.com Ihr Problem zu posten.
Außerdem kann eine Mail zu einer Bekannten, einer R-Expertin, die sich auskennen sollte,
nicht schaden, nicht wahr? Sie formulieren also Ihr Problem: „Hallo, mein R startet nicht,
und wenn es startet, dann macht es nicht, was ich soll, außerdem funktioniert der Befehl
‚mean‘ bei mir nicht. Bitte lös’ mein Problem!“ Seltsamerweise reagiert die Empfängerin nicht begeistert. Stattdessen wird nach einer genauen Beschreibung Ihres Problems
verlangt (dreist!), mit dem Hinweis, dass „Ferndiagnosen“ schwierig seien. Genauer gesagt möchte ihr potenzieller Helfer ein „Minimal Reproducible Example“ (MRE) oder,
Deutsch, ein einfaches reproduzierbares Beispiel (ERBie).14 Was sollte alles in einem
ERBie enthalten sein?
Wenn Sie jemanden um R-Hilfe bitten, dann sollten Sie Ihr Problem prägnant beschreiben, das nennen wir ein ERBie. Ein ERBie besteht aus vier Teilen: Syntax,
Daten, Paketen und Infos zum laufenden System (R Version etc.)
14
Oder Erbie; s. dazu fortunes::fortune(385).
34
3 R starten
Wie sollte so ein ERBie aussehen? Ich empfehle, folgende Eckpunkte zu beachten:15
Syntax: Stellen Sie die R-Syntax bereit, die das Problem erzeugt (d. h. die den entsprechenden Fehler liefert).
Als Text, nicht als Screenshot: Wenn Sie einen Screenshot schicken, zwingen Sie Ihre Helfer, Ihre Syntax unnötigerweise abzutippen. Ihre Syntax sollte immer als Text
(„copypastebar“) zur Verfügung stehen.
Einfach: Geben Sie so wenig Syntax wie möglich an. Es bereitet Ihrem Helfer nur
wenig Spaß, sich durch 2000 Zeilen Code zu wühlen, wenn es zehn Zeilen auch getan
hätten.
Reproduzierbar: Stellen Sie sicher, dass Ihre Syntax das Problem auslöst.
Übersichtlich: Schreiben Sie Ihre Syntax übersichtlich, verständlich und kommentiert;
z. B. sollten die Variablennamen informativ sein.
Präzise: Beschreiben Sie den Fehler genau („läuft nicht“ reicht nicht); z. B. ist es hilfreich, den Wortlaut einer Fehlermeldung bereitzustellen.
Pakete laden: Zu Beginn der Syntax sollten die benötigten Pakete geladen werden.
Session-Infos: Zu Ende des ERBie sollte die Ausgabe der Funktion sessionInfo()
einkopiert werden; damit werden Informationen zum laufenden System (wie Version
von R, Betriebssystem etc.) bereitgestellt.
Verfügbare Daten: Beziehen Sie sich möglichst auf Daten, die in R schon „eingebaut
sind“, wie die Datensätze iris oder mtcars.
Natürlich sollte man immer erst selbst nach einer Lösung recherchieren, bevor man jemanden um Hilfe bittet. Viele Fragen wurden schon einmal diskutiert und oft auch gelöst.
Aufgaben
Richtig oder falsch?16
1.
2.
3.
4.
5.
6.
RStudio ist eine Entwicklungsumgebung für R.
Das Skriptfenster von RStudio stellt R-Ausgaben dar.
Die Konsole von RStudio gleicht der Konsole von R.
Nur Objekte, die im Fenster „Umgebung“ dargestellt sind, existieren in der
aktuellen Sitzung.
R-Pakete dürfen keine Daten enthalten.
CRAN stellt R-Pakete gegen Entgelt zur Verfügung.
15
Hier finden Sie weitere Hinweise zu ERBies: https://stackoverflow.com/help/mcve oder https://
gist.github.com/hadley/270442.
16
R, F, R, R, F, F, F, F, R, R.
3.8 Hier werden Sie geholfen
7.
35
Beim
Befehl
install.packages()
bedeutet
das
Argument
dependencies = TRUE, dass frühere Versionen des Pakets mitinstal-
liert werden sollen.
8. Pakete müssen nach jedem Start von R neu installiert werden.
9. Startet man RStudio, so wird R automatisch gestartet.
10. Die Schreibweise mosaic::filter() meint, dass aus dem Paket mosaic
die Funktion filter() verwendet werden soll.
Aufgaben
1.
2.
3.
4.
Installieren Sie das Paket pradadata von Github.17
Wie kann man in R das „Arbeitsverzeichnis“ erfahren?18
Was versteht man unter einem ERBie?19
Öffnen Sie diesen Link https://raw.github.com/sebastiansauer/modar/master/
datasets/stats_test.csv und es öffnet sich eine Textdatei in vager Tabellenform.
Speichern Sie die Datei auf Ihrem Computer.20
17
install.packages(„devtools“, dependencies = TRUE); library(devtools); install_github(„sebasti
ansauer/pradadata“).
18
getwd().
19
ERBies sind Syntax-Beispiele, die ein Problem oder einen Sachverhalt einfach und nachprüfbar
(reproduzierbar) darstellen. Sie sollten alle Informationen einhalten – wie Syntax, Daten oder Hintergrundinformationen (z. B. Version von R), die nötig sind; aber sie sollten keine nicht unbedingt
nötigen Informationen enthalten.
20
In einigen Browsern führt die Tastenkombination Strg-S zum Ziel. In anderen Browsern findet sich ein Hinweis wie „Speichern als . . . “ beim Klick mit der rechten Maustaste oder auf der
Menüleiste.
4
Erstkontakt
Lernziele
4.1
Wissen, wobei R pingelig ist
Die Varianten von „x D y“ in R auseinanderhalten können
Wissen, was ein Argument einer Funktion ist
Verstehen, was man mit vektoriellem Rechnen meint, und wissen, wie man es
anwendet
R ist pingelig
R nimmt einige Dinge recht genau:
R unterscheidet zwischen Groß- und Kleinbuchstaben, d. h. Oma und oma sind zwei
verschiedene Wesen für R.
R verwendet den Punkt . als Dezimaltrennzeichen.
Fehlende Werte werden in R durch NA gekennzeichnet.
Kommentare werden mit dem Rautezeichen (Hashtag) # eingeleitet; der Rest der Zeile
wird von R dann ignoriert.
Variablennamen in R sollten mit Buchstaben beginnen; ansonsten dürfen nur Zahlen
und Unterstriche (_) enthalten sein. Leerzeichen sollte man meiden.
Um den Inhalt einer Variablen auszulesen, geben wir einfach den Namen des Objekts
in der Konsole ein (oder schicken den Befehl aus dem Skriptfenster in die Konsole).
Nur die Daten, die im Fenster Environment (globale Umgebung) aufgeführt sind, existieren für R. Ob ein Reiter mit einem Datensatz offen ist, sagt nichts darüber aus, ob
dieser Datensatz gerade geladen ist (der Reiter wird dahingehend nicht automatisch
aktualisiert).
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_4
37
38
4
Erstkontakt
4.2 Variablen zuweisen und auslesen
Eine Variable ist ein Platzhalter für einen oder mehrere Werte. Man kann sich eine Variable vorstellen als einen Behälter, in den man einen Wert (oder mehrere) „einfüllt“ (zuweist,
bindet). Außerdem bekommt der Behälter einen Namen (es klebt ein Zettel auf dem Behälter); wie könnte man ihn sonst benennen? Voilá: Das ist eine Variable (s. Abb. 4.1).
Eine Variable wird auch als Objekt bezeichnet; mehr dazu in Kap. 5. Gerade wenn man
viele Werte hat, ist es praktisch, diese einer Variable zuzuweisen und dann zu sagen: „Hey
R, berechne den Mittelwert aller Werte in diesem Behälter.“ Dann müssen Sie nicht jedes
Mal alle Werte gebetsmühlenartig aufsagen. In R definiert man Variablen gewöhnlich mit
dem Zuweisungspfeil:
temperatur <- 9
Mit dem Operator <- (Zuweisungspfeil) weisen wir dem Behälter (der Variablen) mit
dem Namen temperatur den Wert 9 zu; wir „binden“ den Wert 9 an das Symbol
temperatur, sagt man. Der Pfeil versinnbildlicht das. Das, worauf der Pfeil zeigt, wird
definiert (hier temperatur). Übrigens darf der Pfeil auch nach rechts zeigen: ->; dann
steht das, was definiert wird, rechts vom Pfeil. In R ist auch das Gleichheitszeichen als Zuweisungsoperator erlaubt; der Pfeil ist aber deutlich. Dafür ist er aufwändiger zu tippen.
In diesem Buch wird nur der Zuweisungspfeil, nicht das Gleichheitszeichen, für Zuweisungen verwendet.
Gibt man den Objektnamen ein, so wird der Inhalt in der Konsole ausgegeben:
temperatur
#> [1] 9
An den meisten Stellen darf man Leerzeichen setzen, wenn man mit R spricht (Befehle erteilt). Der Zuweisungspfeil ist eine Ausnahme: es ist „verboten“, < - zu
schreiben, also ein Leerzeichen zwischen dem Kleinerzeichen und dem Minuszeichen einzufügen. R würde diesen Versuch mit einer Fehlermeldung quittieren, da R
dann etwas von einem Kleinerzeichen und einem Minuszeichen liest und nicht ahnt,
dass Sie ein Zuweisungszeichen schreiben wollten.
temperatur
Abb. 4.1 Sinnbild: Eine Variable definieren
9
4.3 Funktionen aufrufen
39
Wenn man keine Zahlen, sondern Text zuweisen will, so muss man den Text in Anführungszeichen setzen (doppelte oder einfache sind erlaubt):
y <- 'Hallo R!'
Man kann auch einer Variablen eine andere zuweisen:
y <- x
Wird jetzt y mit dem Inhalt von x überschrieben oder umgekehrt? Der Zuweisungspfeil <- macht die Richtung der Zuweisung ganz klar. Wie erwähnt, ist R manchmal
etwas pingelig. Der Aufruf Temperatur wird zu einem Fehler führen, da es das Objekt
Temperatur nicht gibt; wir haben nur das Objekt temperatur definiert. Gerade, wenn
man einem Objekt einen kreativen Namen wie x gegeben hat, kann es schnell vorkommen,
dass man sich vertut und z. B. X eingibt.
4.3
Funktionen aufrufen
Um einen Befehl (präziser, aber hier synonym gebraucht: eine Funktion1 ) aufzurufen, geben wir ihren Namen an und definieren sog. Argumente in einer runden Klammer, z. B.
so:
ein_mittelwert <- mean(temperatur)
Lassen Sie sich den Inhalt des Objekts ein_mittelwert ausgeben. Tatsächlich! Der
Mittelwert von Temperatur ist 9! Funktionen sind häufig ein Stück weit flexibel. Mit
Argumenten gibt man genau an, was die Funktion tun soll. So kann es eine Funktion
wurzel() geben, die ein Argument hat. Die Funktion errechnet die Quadratwurzel der
Zahl, die als Argument übergeben wurde: Das Argument sagt, worauf die Funktion oder
wie die Funktion anzuwenden ist.
Allgemein gesprochen:
funktionsname(argumentname1 = wert1, argumentname2 = wert2, ...)
Die drei Punkte ... sollen andeuten, dass evtl. weitere Argumente2 übergeben werden
könnten. Die Reihenfolge der Argumente ist egal – wenn man die Argumentenamen anführt. Ansonsten muss man sich an die Standard-Reihenfolge, die eine Funktion vorgibt,
halten. Die Hilfeseite einer Funktion gibt Aufschluss über die Namen und StandardReihenfolge der Argumente (z. B. ?mean).
1
Noch genauer könnte man argumentieren, dass eine Funktion in der Informatik eine bestimmte
technische Umsetzung eines Befehls ist, den wir dem Computer geben.
2
Argumente werden auch als Parameter bezeichnet.
40
4
Erstkontakt
#ok:
ein_mittelwert <- mean(x = temperatur, na.rm = FALSE)
ein_mittelwert <- mean(temperatur, FALSE)
# ohno:
ein_mittelwert <- mean(FALSE, temperatur)
In der Hilfe zu einem Befehl findet man die Standard-Syntax inklusive der möglichen
Parameter, ihrer Reihenfolge und Standardwerte (default values) von Argumenten. Zum
Beispiel ist beim Befehl mean() der Standardwert für na.rm mit FALSE als Voreinstellung (schauen Sie mal in der Hilfe nach); eine Tatsache, die schon manchen Anfänger
frustriert hat. Gibt man einen Parameter nicht an, für den ein Standardwert eingestellt ist,
„befüllt“ R den Parameter mit diesem Standardwert.
Einen R-Befehl (synonym: Funktion) erkennt man in R daran, dass er einen Namen
hat, der von runden Klammern gefolgt ist, z. B. mean(x), play_it_again()
oder find_question(42). Es ist bei machen Funktionen auch möglich, dass
nichts (kein Parameter) in den Klammern steht. So, wie Menschen in Häusern wohnen, „wohnen“ R-Funktionen in R-Paketen. Da es viele R-Pakete gibt, passiert es
immer wieder, dass ein Funktionsname in mehreren Paketen existiert. Ein Beispiel
ist die Funktion filter(), die es im Standard-R gibt (Paket base) oder z. B. im
Paket dplyr. Da sind Verwechslungen vorprogrammiert: „Nein, ich meinte den
anderen Schorsch Müller!“ Um zu spezifizieren, aus welchem Paket man eine Funktion beziehen will, benutzt man diese Schreibweise: dplyr::filter(). Damit
weiß R, dass Sie filter() aus dem Paket dplyr meinen. Falls es zu merkwürdigen Fehlermeldungen kommt, kann so eine Funktionen-Verwechslung vorliegen.
Geben Sie dann den Paketnamen mit an, mit Hilfe des doppelten Doppelpunkts.
Abschn. 3.4 erläutert, wie man herausfindet, in welchem Paket (oder welchen Paketen) eine Funktion beheimatet ist. Übrigens: Gibt man den Paketnamen mit an,
so muss das Paket nicht geladen sein, ansonsten schon. Probieren Sie einmal aus:
fortunes::fortune(50) – aber vergessen Sie nicht, das Paket fortunes zu
installieren (s. Abschn. 3.2.1).
4.4 Logische Prüfungen
Eine häufige Tätigkeit in der Datenanalyse (und beim Programmieren allgemein) sind
logische Prüfungen (Vergleiche). Damit ist gemeint, zu prüfen, ob eine Aussage wahr
oder falsch ist. Zum Beispiel ist die Aussage wahr:
2 < 3
#> [1] TRUE
4.4 Logische Prüfungen
Tab. 4.1 Logische Operatoren
in R
41
Logik-Operator
==
!=
<
<=
>
>=
!
&
|
Beschreibung
Prüfung auf Gleichheit
Prüfung auf Ungleichheit
Kleiner als
Kleiner als oder gleich groß
Größer als
Größer als oder gleich groß
Nicht (Negation)
Logisches Und
Logisches Oder
Dafür hätten Sie jetzt nicht R gebraucht, O. K. Anstelle von Zahlen können auch Objekte
Teil der Prüfung sein:
temperatur < 10
#> [1] TRUE
temperatur == 10
#> [1] FALSE
Um Gleichheit zu prüfen, sieht die Rechtschreibung von R das doppelte Gleichheitszeichen vor. Ein häufiger Fehler ist es, Gleichheit mit einem Gleichheitszeichen zu überprüfen, da das einfache Gleichheitszeichen für Zuweisungen vorgesehen ist (aber nicht
empfehlenswert) und außerdem für Funktionsargumente gebraucht wird.
„X gleich Y“ hat in R drei Gesichter:
1. x <- y (oder x = y) weist x den Wert von y zu.
2. x == y prüft, ob x und y identisch sind.
3. fun(x = y) weist innerhalb der Funktion fun() dem Argument x den Wert
der Variablen y zu.
Ist eine logische Aussage wahr, so quittiert R das mit TRUE; falsche Aussagen mit
FALSE. R unterstützt die klassischen Logik-Operationen wie das logische Und sowie das
logische Oder:
# Temperatur zwischen 0 und 10 Grad?
temperatur < 10 & temperatur > 0
#> [1] TRUE
In Tab. 4.1 sind gängige logische Operatoren von R aufgeführt.
42
4
4.5
Erstkontakt
Vektorielle Funktionen
Angenommen, Sie haben eine Liste der Temperaturen jedes Tages Ihres Wohnorts; und
zwar von den letzten 100 Jahren. Aus irgendwelchen Gründen sind die Temperaturen in
Grad Fahrenheit angegeben; Celsius wäre Ihnen lieber. Aufgrund der großen Menge der
Zahlen käme es Ihnen ungelegen, jeden einzelnen Wert umzurechnen. Praktischerweise
sind viele (nicht alle) R-Befehle vektoriell aufgebaut. Das bedeutet: Sie wenden einen Befehl auf einen Vektor (eine Liste von Werten) an; R führt diesen Befehl auf jedes einzelne
Element des Vektors aus. Damit müssen Sie nur einmal den Befehl eingeben, das ist ziemlich praktisch. Sagen wir, das sei unsere Variable, befüllt mit einigen Temperaturwerten:
temperaturen <- c(32, 54, 0)
# Temperaturen in Fahrenheit
Der Befehl c() erstellt einen Vektor, d. h. ein Datenobjekt, das einer Liste gleicht; mehr
dazu in Abschn. 5.1. Hier erstellen wir die Variable temperatur, die aus den drei Werten
32, 54 und 0 besteht. Stellen Sie sich einen Zettel vor, auf dem drei Zahlen aufgelistet
stehen. Jetzt rechnen wir die Werte in Celsius um:
temperaturen_celcius <- (temperaturen - 32) * 5/9
temperaturen_celcius
#> [1]
0.0 12.2 -17.8
Die Spalten einer Tabelle sind in R als Vektor angelegt; da viel mit Tabellen gearbeitet
wird, kommt uns die Fähigkeit des vektoriellen Verarbeitens sehr gelegen. Tab. 4.2 stellt
die Idee des vektoriellen Rechnens bzw. Verarbeitens sinnbildlich dar: Die Rechnung x C
y D z wird für jedes Element der beteiligten Vektoren x, y und z durchgeführt.
Unter vektoriellem Rechnen bzw. vektoriellem Verarbeiten wird die Tatsache verstanden, dass R eine Operation auf alle Elemente eines Vektors ausführt.
Tab. 4.2 Sinnbild für
x Cy D z, vektoriell gerechnet
id
1
2
3
4
5
x
2
3
1
2
3
y
1
5
2
2
3
z
3
8
3
4
6
4.6 Literaturempfehlungen
43
4.6 Literaturempfehlungen
Wer es formal mag, kann sich mit der R-Language-Definition vertraut machen, in der zentrale Punkte von GNU-R erläutert werden (R Core Team 2018); dieser Text ist aber für
fortgeschrittene Nutzer gedacht. Die vielleicht beste und tiefste Erläuterung der R-Sprache
findet sich bei Wickham (2014), dort findet sich alles, was man braucht, um R zu verstehen. Es ist allerdings auch ein Text für hoch motivierte Einsteiger oder fortgeschrittene
Nutzer, die ein tiefes Verständnis erwerben wollen. Ein guter Einstieg in die Datenanalyse
mit R findet sich bei Ligges (2008). Alternativ bietet Luhmann (2015) einen Einstieg, der
Leser aus den Sozialwissenschaften im Blick hat. Beide Bücher haben allerdings keine
modernen R-Ansätze wie das Tidyverse im Blick. Gut ist das Buch von Wickham und
Grolemund (2016); auch erhältlich in einer deutschen Version (Wickham und Grolemund
2017).
Aufgaben
Richtig oder falsch?3
1.
Datenanalyse wird in diesem Buch in folgende fünf Schritte eingeteilt: Daten
einlesen, Daten aufbereiten, Daten visualisieren und modellieren, schließlich
die Ergebnisse kommunizieren.
2. Eine Variable ist ein Platzhalter für einen oder mehrere Werte.
3. Eine Variable ist etwas anderes als ein Objekt.
4. Mit temp = 9 wird der Variablen 9 der Wert temp zugewiesen.
5. Durch Eingabe des Namens eines Objekts wird dieses ausgelesen.
6. Der Zuweisungspfeil <- darf auch (d. h. korrekte R-Schreibweise) so geschrieben werden < -.
7. Das Argument oder die Argumente einer R-Funktion werden stets in eckigen
Klammern geschrieben.
8. Um auf Gleichheit zu prüfen, ist diese R-Schreibweise korrekt: temperatur
= 9.
9. fun(x = y) weist innerhalb der Funktion fun() dem Argument x den Wert
der globalen Variablen y zu.
10. Unter vektoriellem Rechnen bzw. vektoriellem Verarbeiten wird die Tatsache
verstanden, dass R eine Operation auf alle Vektoren in der Arbeitsumgebung
ausführt.
3
R, R, F, F, R, F, F, F, R, F.
Teil II
Daten einlesen
5
Datenstrukturen
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
Lernziele
Die wesentlichen Datenstrukturen von R kennen
Verstehen, warum bzw. inwiefern Vektoren im Zentrum der Datenstrukturen stehen
Einige Unterschiede zwischen den Datenstrukturen kennen
Datenstrukturen erzeugen können
In diesem Kapitel wird folgendes Paket benötigt:
library(tidyverse)
5.1
Überblick über die wichtigsten Objekttypen
R ist ein Programm, um Daten zu analysieren. Da liegt es nahe, dass man diese Daten irgendwie ansprechen können muss. Außerdem sollte R verstehen, ob es sich bei den Daten
z. B. um Zahlen oder um Text handelt. Wir lehnen uns nicht zu weit aus dem Fenster, wenn
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_5
47
48
Tab. 5.1 Zentrale Objektarten
in R
5
Dimension
1
2
Beliebig
Homogen
Reiner Vektor, Faktor
Matrix
Array
Abb. 5.1 Zentrale Objektarten
in R
Heterogen
Liste
Dataframe (Tibble)
Vektoren
1
Eier
1
Salz
2
Butter
2
Schmalz
Datenstrukturen
Dataframe
ID
Name
1 Anna
2 Anna
3 Berta
4 Carla
Note1
1
1
2
2
2
Abb. 5.2 Datenstrukturen
in R; der Vektor steht im Mittelpunkt
Dataframes
Matrizen
Arrays
Vektoren
reine Vektoren
Faktoren
Listen
wir behaupten, dass die Art, wie Daten in R strukturiert (repräsentiert, dargestellt) sind,
eine zentrale Angelegenheit bei der computergestützten Analyse von Daten ist. Datenstrukturen oder synonym Objekte meint also die Art, wie Daten in R dargestellt werden.
Die wichtigsten Datenstrukturen von R kann man anhand zweier Merkmale gliedern: Dimension und Homogenität. Unter Dimension wird hier verstanden, ob das Objekt eine,
zwei oder mehrere Dimensionen besitzt. Mit Homogenität ist gemeint, ob das Objekt „sortenrein“ ist, also nur eine Art von Daten verträgt (z. B. Zahlen oder Text). Daraus lassen
sich die in Tab. 5.1 genannten fünf zentralen Objektarten ableiten (Wickham 2014).
Praktisch ist hier der Befehl str(objektname), der den Aufbau (die Struktur) des
Objekts objektname recht übersichtlich wiedergibt. Für den Einstieg reichen aber zwei
Objektarten: Vektoren und Dataframes (s. Abb. 5.1). Vektoren sind die technische Grundlage der wichtigsten Datenstrukturen in R (s. Abb. 5.2). Dataframes sind der Datentyp,
der in der praktischen Datenanalyse am meisten verwendet wird. Eine neuere Variante
von Dataframes heißen Tibbles (s. Abschn. 6.1.4.3).1
Für Einsteiger sind zwei Datenstrukturen in R zentral: Der Vektor und der Dataframe.
Vielleicht die wichtigste Datenstruktur in R ist der reine (oder „atomare“) Vektor. Auf
dieser Datenstruktur bauen die meisten anderen Objektarten auf (s. Abb. 5.2). Nehmen
wir an, Sie gehen gleich einkaufen und notieren sich auf einem Einkaufszettel: „Eier,
Schmalz, Butter, Salz“. Das ist ein Beispiel für einen Vektor, Ihr Einkaufsvektor. In die1
Übrigens ist in R alles ein Objekt, auch Funktionen oder Argumente von Funktionen.
5.2 Objekttypen in R
49
sem Fall ist die Reihenfolge egal, aber manchmal ist die Reihenfolge wichtig, wie beim
Anziehen: „Unterhose, Unterhemd, Hose, Hemd“. Ein Vektor ist eine geordnete Folge von
Werten. Dabei bezeichnet Wert ein einzelnes Element wie „Schmalz“. Sie kennen den Begriff Vektor vielleicht aus der Algebra oder der Geometrie. Man erstellt einen Vektor mit
der Funktion c() (wie combine). Spalten- und Zeilenvektoren werden von R nicht unterschieden. Wenn man in R von einer Variablen spricht, meint man zumeist einen Vektor.
Vektoren dürfen auch die Länge 1 haben.2
ein_vektor <- c(1, 2, 3)
ein_kurzer_vektor <- c(1)
Dataframes sind, einfach gesprochen, das R-Pendant zu Excel-Tabellen, wobei die Spalten
einen Namen haben müssen (s. Abb. 5.1). Es wird Sie nicht sonderlich beeindrucken,
wenn ich hinzufüge, dass Tabellen aus Spalten und Zeilen bestehen. Und dass die Werte
in den Zellen der Tabelle zu finden sind. Dataframes erstellt man am besten nicht in R,
sondern z. B. in Excel oder man bekommt sie z. B. aus einer Umfrage-Software oder aus
dem Controlling. Um einen Überblick über einen Dataframe zu bekommen, ist der Befehl
glimpse() hilfreich, der über das Paket tidyverse bereitgestellt wird. Möchte man
sich wie in Excel die ganze Tabelle anschauen, so kann man im Reiter Environment auf das
kleine Tabellen-Icon klicken, das hinter dem Namen eines Dataframes steht. Das ganze
Handwerkszeug zum Arbeiten mit Dataframes findet sich in den Kap. 6 (Einlesen), Kap. 7
(Datenjudo) und Kap. 9 (Praxisprobleme der Datenaufbereitung).
5.2 Objekttypen in R
5.2.1
Vektoren
Betrachten wir zunächst reine Vektoren; man könnte auch sagen „normale“ Vektoren.
Solche Vektoren sind eindimensional und bestehen aus einem oder mehreren zusammengehörigen Elementen eines Datentyps (daher „rein“). Ein Beispiel für einen Vektor
– nennen wir ihn „Einkaufvektor“ – ist „Butter, Schmalz, Eier, Salz, Milch, Mehl“. Der
umgangssprachliche Begriff der Liste trifft das Wesen eines Vektor sehr gut; leider gibt
es aber in R eine andere Datenstruktur mit dem Namen Liste. Daher sollte der Begriff
Liste für Vektoren vorsichtig verwendet werden, um Verwechslungen zu vermeiden. Die
wichtigsten Datentypen bzw. Arten reiner Vektoren sind:
Vektoren für reelle Zahlen (numeric, auch double genannt)
Vektoren für ganze Zahlen (integer, in R auch mit L abgekürzt)
2
Genauso gut hätten wir schreiben können: ein_kurzer_vektor <- 1; das Resultat ist das
gleiche. Interessanterweise gibt es in R keine Datenstruktur für Skalaren (einzelne Zahlen); diese
kurzen Geschöpfe werden als Vektoren der Länge 1 dargestellt und können ebenfalls mit c() erstellt
werden.
50
5
Datenstrukturen
Vektoren für Text (character, auch String genannt)
Vektoren für logische Ausdrücke (logical, mit den Werten TRUE und FALSE)
Alle diese vier Arten von Vektoren können mit c():3 erstellt und modifiziert werden
num_vector
int_vector
chr_vector
lgl_vector
<<<<-
c(1, 2.71, 3.14)
c(1L, 2L, 3L) # Das L sorgt für ganze Zahlen (nicht reell)
c("Hallo ", "R")
c(TRUE, FALSE, T, F) # TRUE/T sind gleich, F/FALSE auch
Man kann den Elementen eines Vektors einen Namen geben:
ein_vektor <- c(a = 1, b = 2, c = 3)
str(ein_vektor)
#> Named num [1:3] 1 2 3
#> - attr(*, "names")= chr [1:3] "a" "b" "c"
Hinter den Kulissen bekommt der Vektor durch die Zuweisung von Namen ein Attribut,
nämlich das Attribut names. Mit attributes(objekt) kann man sich die Attribute
des Objekts objekt ausgeben lassen. Man kann Objekten wie z. B. Vektoren oder Dataframes nach Belieben Attribute zuweisen, um Metadaten zu speichern und zu lesen.
attributes(ein_vektor)
#> $names
#> [1] "a" "b" "c"
attr(ein_vektor, "Autor") <- "student"
attr(ein_vektor, "Datum") <- 2017
attr(ein_vektor, "Datum")
#> [1] 2017
Aufgaben
1. Liefern folgende zwei Befehle das gleiche Ergebnis: c(1, c(2, 3)) vs.
c(1, 2, 3)?4
2. Wie hießen noch mal die vier Arten von atomaren Vektoren?5
3. Welche anderen Datenstrukturen außer reinen Vektoren wurden besprochen?6
4. In welche zwei Arten lassen sich Vektoren aufteilen?7
3
Achtung: c(), nicht C().
Ja.
5
Double, Integer, Charakter, Logisch.
6
Listen, Dataframes, Matrizen, Arrays, Faktoren.
7
Reine Vektoren und Faktoren.
4
5.2 Objekttypen in R
5.2.2
51
Faktoren
Faktoren sind Vektoren, die nur vorab festgelegte Elemente speichern; z. B. könnte man
das Geschlecht von Studienteilnehmern oder Kunden in einem Vektor speichern, der nur
mann und frau als Werte zulässt. Faktoren sind technisch gesehen reine Vektoren vom
Typ integer, also ganzzahlig. Allerdings merkt sich R, welche Zahl welchem Text (d. h.
welcher Name bzw. welches Label) zugeordnet ist. Z. B. ist im folgenden Beispiel das Label „frau“ im Faktor sex mit 1 repräsentiert. Man nutzt sie, um nominal skalierte Daten
zu speichern. Man kann Faktoren mit c() anlegen und sie dann mit factor() zu einem Faktor konvertieren. factor() wandelt einen Vektor mit Zahlen oder Text in einen
Faktor-Vektor um:
sex <- factor(c("mann", "frau", "frau", "frau"))
str(sex)
#> Factor w/ 2 levels "frau","mann": 2 1 1 1
Wie str() zeigt, werden die Elemente intern mit ganzen Zahlen abgespeichert; in der
Voreinstellung wird alphabetisch geordnet (also frau vor mann). Die unterschiedlichen
Elemente werden als Stufen (Faktorstufen, Levels) oder Ausprägungen bezeichnet. Meist
werden Faktoren wie Textvariablen verarbeitet; da sie aber technisch Ganzzahl-Vektoren
gleichen, kann es zu Problemen kommen, wenn man versucht, sie mit as.numeric() in
einen numerischen Vektor umzuwandeln:
fieber <- factor(c(41, 40, 40.5, 41))
fieber
#> [1] 41
40
40.5 41
#> Levels: 40 40.5 41
fieber_nichtnum <- as.numeric(fieber)
fieber_nichtnum
#> [1] 3 1 2 3
Das Ergebnis 1 2 3 war wohl nicht das gewünschte! Woran liegt das? Der Grund ist,
dass 40 intern als erste Ausprägung (level), also als 1 gespeichert ist. Möchte man 40
40.5 41 als Zahlen zurückgeliefert bekommen, so geht man so vor: 1) Hey R, wandele
den Faktor-Vektor in einen Text-Vektor um (mit as.character()), 2) Wandele den
Text-Vektor in einen Zahlen-Vektor um (mit as.numeric()):
fieber_chr <- as.character(fieber)
fieber_num <- as.numeric(fieber_chr)
fieber_num
#> [1] 41.0 40.0 40.5 41.0
fieber_num <- as.numeric(as.character(fieber))
# kompakter und identisch
52
5
Datenstrukturen
Manchmal ist es praktisch, mit Faktoren zu arbeiten, z. B. wenn man den Faktorstufen
eine Reihenfolge geben möchte. Sagen wir, uns liegt dieser Vektor vor:
tage <- factor(c("Mittwoch", "Montag", "Mittwoch", "Dienstag", "Dienstag"))
levels(tage)
#> [1] "Dienstag" "Mittwoch" "Montag"
levels() gibt uns die Faktorstufen (levels) aus. Gibt man keine besondere Reihenfolge
der Faktorstufen an, so wird alphanumerisch sortiert.8
Eine typische Anwendung ist die Datenvisualisierung; viele Befehle zur Visualisierung
nutzen Faktoren bzw. die Reihenfolge ihrer Stufen, um die Elemente einer Achse anzuordnen (s. Kap. 11). Wie ist eigentlich die Reihenfolge der Stufen von tage? levels
gibt uns Antwort:
levels(tage)
#> [1] "Dienstag" "Mittwoch" "Montag"
Das ist nicht die übliche Reihenfolge: Im Standard sortiert R die Faktorstufen alphabetisch. So lässt sich die Reihenfolge der Faktorstufen ändern:
tage <- factor(tage, levels = c("Montag", "Dienstag", "Mittwoch"))
levels(tage)
#> [1] "Montag"
"Dienstag" "Mittwoch"
Man kann auch die Faktorstufen nach ihrer Reihenfolge im Vektor sortieren, und zwar mit
forcat::fct_inorder():
fct_inorder(tage)
#> [1] Mittwoch Montag
Mittwoch Dienstag Dienstag
#> Levels: Mittwoch Montag Dienstag
Technisch gesehen sind Faktoren reine Vektoren plus zwei Attribute: levels und class.
Schauen wir uns die Attribute an:
attributes(tage)
#> $levels
#> [1] "Montag"
#>
#> $class
#> [1] "factor"
"Dienstag" "Mittwoch"
levels zeigt die Faktorstufen, also die erlaubten Werte; class zeigt, um welchen Datentyp es sich handelt; factor in diesem Fall. Ein reiner Vektor, wie wir ihn im Vorangegangenen definiert haben, hat keine Attribute.
8
Die Reihenfolge ist: 0; 1; : : : ; 9; a; b; : : : ; z; A; B; : : : ; Z.
5.2 Objekttypen in R
5.2.3
53
Listen
Listen (lists) unterscheiden sich von reinen Vektoren in zwei Hinsichten: Zum einen müssen sie nicht homogen sein und zum anderen können sie aus Elementen unterschiedlicher
Länge bestehen. Listen werden mit dem Befehl list() erstellt.
eine_liste <- list(1, c(1, 2), c(TRUE, FALSE), c("Hallo, R ", "Liste"))
str(eine_liste)
#> List of 4
#> $ : num 1
#> $ : num [1:2] 1 2
#> $ : logi [1:2] TRUE FALSE
#> $ : chr [1:2] "Hallo, R " "Liste"
5.2.4
Matrizen und Arrays
Matrizen sind numerische Vektoren mit zwei Dimensionen; es sind „rechteckige“ Datenstrukturen, Tabellen, aber ohne Spaltenköpfe und nur für Zahlen. Arrays sind die Verallgemeinerung von Matrizen auf n Dimensionen; wir werden uns nicht weiter um Arrays
kümmern. Weist man einem Vektor mit dim() zwei Dimensionen zu (erst Zeilen, dann
Spalten), so entsteht eine Matrix. Das kann man mit dem Befehl class() prüfen, der den
Objekttyp zurückgibt:
eine_matrix <- 1:6
dim(eine_matrix) <- c(3, 2) # erst Zeilenzahl, dann Spaltenzahl
eine_matrix
#>
[,1] [,2]
#> [1,]
1
4
#> [2,]
2
5
#> [3,]
3
6
class(eine_matrix) # Objekttyp?
#> [1] "matrix"
In diesem Buch werden wir so gut wie nie mit Matrizen arbeiten; Dataframes (s.
Abschn. 5.2.5) sind die Standardform von „Tabellen“ bei R. Das Beispiel zeigt, dass
auch Matrizen auf Vektoren aufbauen.
5.2.5 Dataframes
Dataframes sind die zentrale Datenstruktur in R. Unter der Motorhaube ist ein Dataframe
eine Liste, deren Elemente gleich lang sein müssen und die Spalten einer Tabelle repräsentieren. Die Spalten haben jeweils einen Namen. Die Elemente eines Dataframes
sind Vektoren. Damit ist eine Dataframe zweidimensional und eine Art Kreuzung von
54
Tab. 5.2 Ein einfacher Dataframe
5
essen
Suppe
Suppe
Pizza
Datenstrukturen
geschmack
2
2
5
Liste und Matrix. Ein Dataframe lässt sich sowohl als Liste als auch als Matrix ansprechen. Dataframes kann man sich gut als Tabelle vorstellen. Mit data.frame() oder
tibble::data_frame() lassen sich Dataframes erstellen:
ein_df <- data.frame(essen = c("Suppe", "Suppe", "Pizza"),
geschmack = c(2, 2, 5))
str(ein_df)
#> 'data.frame':
3 obs. of 2 variables:
#> $ essen
: Factor w/ 2 levels "Pizza","Suppe": 2 2 1
#> $ geschmack: num 2 2 5
Man betrachte diesen Dataframe (Tab. 5.2) in seiner ganzen Pracht. Wie str() zeigt,
wird essen als Faktor angelegt. Das ist oft nicht praktisch; Textvariablen sind oft praktischer. Mit data_frame() aus tibble werden in der Voreinstellung in diesem Fall
Textvariablen erzeugt:
ein_df2 <- data_frame(essen = c("Suppe", "Suppe", "Pizza"),
geschmack = c(2, 2, 5))
str(ein_df2)
#> Classes 'tbl_df', 'tbl' and 'data.frame':
3 obs. of 2 variables:
#> $ essen
: chr "Suppe" "Suppe" "Pizza"
#> $ geschmack: num 2 2 5
Man beachte den kleinen aber feinen Unterschied: data.frame() vs. data_frame().
Tibbles (tbl bzw. tbl_df) sind auch Dataframes, verhalten sich aber etwas anders. Vor
allem sind sie typstabil, bleiben also Dataframes, wenn man aus ihnen nur einen Teil der
Daten ausliest. Normale Dataframes könnten unerwartet zu reinen Vektoren konvertieren,
wenn man nur eine Spalte ausliest (s. Übungen am Ende dieses Kapitels). Das kann zu
Problemen führen, so dass ich Tibbles für die meisten Fälle empfehle. Mit as_tibble()
kann man ein Objekt zwingen, ein Tibble zu werden:
ein_df <- as_tibble(ein_df)
ein_tbl <- as_tibble(x)
ein_tbl2 <- as_tibble(eine_liste)
Wenn wir im Folgenden von Dataframes sprechen, ist der Oberbegriff gemeint; Tibbles
dürfen sich auch angesprochen fühlen. Laxerweise werde ich auch manchmal von „Tabellen“ sprechen, damit ist etwas gemeint, was ähnlich oder identisch zu Dataframes ist.
5.3 Daten auslesen und indizieren
5.3
55
Daten auslesen und indizieren
Möchte man den Inhalt eines Objekts wissen, so gibt man einfach den Namen des Objekts
an R, und R gibt seinen Inhalt aus:
ein_vektor
#> a b c
#> 1 2 3
#> attr(,"Autor")
#> [1] "student"
#> attr(,"Datum")
#> [1] 2017
Bei dieser Ausgabe hat R auch die Attribute angegeben; das sind Metadaten wie z. B. der
Autor und das Datum. Man kann auch nur einen Teil eines Objekts auslesen; das nennt
man auch Indizieren (subsetting). Wir werden in Kap. 7 komfortable Wege für das Indizieren von Dataframes betrachten, die für den Alltag wichtiger sind als die hier vorgestellten.
In diesem Kapitel geht es um grundlegende Prinzipien, die vor allem für den Umgang mit
Vektoren von Bedeutung sind.
5.3.1 Reine Vektoren
Reine Vektoren können auf mehreren Wegen indiziert werden (vgl. Wickham (2014)).
Betrachten wir den einfachen Vektor x:
x <- c(2.2, 3.3, 4.4)
1. Positive ganze Zahlen geben das Element mit der jeweiligen Position zurück:
x[1]
#> [1] 2.2
Man kann mehrere Positionen abfragen, wenn man diese mit c() kombiniert:
x[c(1,2)]
#> [1] 2.2 3.3
Der Fantasie sind dabei keine Grenzen gesetzt; ob es nützlich ist, muss man selber
entscheiden:
x[c(1, 1, 2, 3, 1)]
#> [1] 2.2 2.2 3.3 4.4 2.2
56
5
Datenstrukturen
2. Negative ganze Zahlen lassen das Element an der angegebenen Stelle aus:
x[-1]
#> [1] 3.3 4.4
x[-c(1,2)]
#> [1] 4.4
3. Logische Vektoren geben die Elemente wieder, deren korrespondierender logischer
Wert TRUE ist. Sie sagen also zu R: „Das erste Objekt, ja, richtig, das will ich; das
zweite, nein, falsch, das will ich nicht; das dritte, ja, richtig, das will ich.“
x[c(TRUE, FALSE, TRUE)] # Ich will das 1. und 3. Objekt sehen
#> [1] 2.2 4.4
Anstelle von TRUE oder FALSE kann man auch Ausdrücke verwenden, die in einen
logischen Wert münden. Folgender Ausdruck prüft für jedes Element von x, ob es
größer als 3 ist. Falls ja, wird TRUE zurückgegeben, ansonsten FALSE:
x > 3
#> [1] FALSE
TRUE
TRUE
Diesen Ausdruck können wir zum Indizieren nutzen, also um nur Elemente auszulesen,
die eine Bedingung erfüllen, z. B. die größer als 3 sind:
x[x > 3]
#> [1] 3.3 4.4
Ihrer Bastelfreude sind dabei kaum Grenzen gesetzt:
pruefung_x <- x > 3
x[pruefung_x]
#> [1] 3.3 4.4
4. Namen: Haben die Elemente einen Namen, so können sie anhand des Namens indiziert
werden:
Noten <- c("Anna" = 1.3, "Berta" = 2.7, "Carla" = 4.3)
Noten["Anna"] # nur ein Element indizieren
#> Anna
#> 1.3
Noten[c("Anna", "Anna", "Carla")] # oder mehrere
#> Anna Anna Carla
#>
1.3
1.3
4.3
5.3 Daten auslesen und indizieren
5.3.2
57
Matrizen und Arrays
Matrizen können genau zu reinen Listen ausgelesen werden. Der Unterschied ist, dass für
beide Dimensionen ein Wert angegeben werden kann, getrennt durch ein Komma:
eine_matrix[1,1]
#> [1] 1
eine_matrix[c(1,2), 1]
#> [1] 1 2
Der erste Wert spricht die Zeile an, der zweite die Spalte. Möchte man alle Zeilen auslesen,
so gibt man vor dem Komma nichts an:
eine_matrix[ ,1]
#> [1] 1 2 3
eine_matrix[1, ]
#> [1] 1 4
Für die Spalte gilt das Gleiche, nur nach dem Komma. Da Matrizen technisch auch eine
Art Vektoren sind, kann man sie als Vektoren ansprechen, also eindimensional indizieren.
eine_matrix[5]
#> [1] 5
So wird der 5. Wert der Matrix wiedergegeben (spaltenweise gezählt).
Arrays sind Matrizen mit nicht nur zwei, sondern beliebig vielen Dimensionen. Sie
werden genau wie Matrizen angesprochen; Arrays werden nicht häufig verwendet. Wir
werden von ihnen keinen Gebrauch machen.
5.3.3 Listen
Eine Liste ist ähnlich wie ein reiner Vektor, nur dass eine Liste Elemente unterschiedlichen
Typs beinhalten kann. Listen können wie reine Vektoren ausgelesen werden:
eine_liste[1] # erstes Element
#> [[1]]
#> [1] 1
eine_liste[c(2,3)] # zweites und drittes Element
#> [[1]]
#> [1] 1 2
#>
#> [[2]]
#> [1] TRUE FALSE
58
5
Datenstrukturen
Indiziert man mit [, so wird eine Liste zurückgegeben. Möchte man nicht eine Liste zurückbekommen, sondern einen Vektor, so verwendet man die Operatoren [[ oder $:
x <- eine_liste[[2]]
str(x)
#> num [1:2] 1 2
Der Dollar-Operator ($) setzt voraus, dass das Element einen Namen hat. Also benennen
wir die Elemente:
eine_liste <- list(L1
L2
L3
L4
eine_liste$L2
#> l2_1 l2_2
#>
1
2
=
=
=
=
1,
c(l2_1 = 1, l2_2 = 2),
c(l3_1 = TRUE, l3_2 = FALSE),
c(l4_1 = "Hallo ", l4_2 = "Liste"))
Auch der Operator [[ kann Elemente bei ihrem Namen ansprechen – genau wie bei reinen
Listen. Und genau wie bei reinen Listen wird dann ein reiner Vektor zurückgegeben:
x <- eine_liste[["L2"]]
str(x)
#> Named num [1:2] 1 2
#> - attr(*, "names")= chr [1:2] "l2_1" "l2_2"
Moment mal: x ist ein reiner Vektor, stimmt’s?9 Und reine Vektoren kann man mit [
indizieren, stimmt’s?10 Und eine_liste[["L2"]] liefert einen reinen Vektor zurück,
stimmt’s?11 Dann kann man die einzelnen Ausdrücke gleichsetzen. Dieser Ausdruck
x[1]
#> l2_1
#>
1
liefert also das Gleiche wie folgender, weil eine_liste[["l2"]] nichts anderes als x
zurückgibt.
eine_liste[["l2"]][1]
#> NULL
Interessanterweise liefert die folgende Indizierung wieder das gleiche Ergebnis, ist also
synonym:
eine_liste[[c(2, 1)]]
#> [1] 1
9
Ja.
Ja.
11
Ja, aber nicht glauben, sondern prüfen.
10
5.3 Daten auslesen und indizieren
59
5.3.4 Dataframes
Dataframes können wie Listen oder wie Matrizen angesprochen werden. Es gibt komfortable Methoden, um gängige Datenoperationen mit Dataframes vorzunehmen. Diese sind
in Kap. 7 dargestellt. Man kann aber auch die klassischen R-Werkzeuge für Dataframes
nutzen; das kann für kompliziertere Operationen wichtig sein. In diesem Abschnitt geht
es um die klassischen Werkzeuge zum Auslesen von Dataframes, also um die Werkzeuge,
die vom Standard-R zur Verfügung gestellt werden. Spricht man Dataframes als Listen an,
so kann man entsprechend die Operatoren [, [[ oder $ verwenden.
# gibt Dataframe zurück:
ein_df[1]
#>
essen
#> 1 Suppe
#> 2 Suppe
#> 3 Pizza
ein_df["essen"]
#>
essen
#> 1 Suppe
#> 2 Suppe
#> 3 Pizza
# gibt Vektor zurück:
ein_df[[1]]
#> [1] Suppe Suppe Pizza
#> Levels: Pizza Suppe
ein_df[["essen"]]
#> [1] Suppe Suppe Pizza
#> Levels: Pizza Suppe
ein_df$essen
#> [1] Suppe Suppe Pizza
#> Levels: Pizza Suppe
ein_df$essen[1]
#> [1] Suppe
#> Levels: Pizza Suppe
Wie man sieht, gibt es einen Unterschied bei der Ansprache mit [ im Gegensatz zu [[: Die
einfache Klammer gibt wieder den Dataframe zurück; die Doppelklammer vereinfacht zu
einem reinen Vektor. Dann gibt es noch den Dollar-Operator $, der etwas Tipparbeit spart
und daher beliebt ist. Er spricht den Dataframe auch als Liste an, gibt also einen reinen
Vektor zurück.
Spricht man einen Dataframe als Matrix an, so muss man beide Dimensionen benennen, Zeilen und Spalten:
ein_df[1, 1] # zuerst die
#> [1] Suppe
#> Levels: Pizza Suppe
Zeile, dann die Spalte
60
5
Datenstrukturen
ein_df[2, c(1,2)]
#>
essen geschmack
#> 2 Suppe
2
ein_df[1, ]
#>
essen geschmack
#> 1 Suppe
2
5.4
Namen geben
Nach einem Bonmot ist es eines der schwierigsten Dinge in der Computerwissenschaft,
einen guten Namen für ein Objekt zu finden. Tatsächlich ist es gar nicht so leicht, Objekte
so zu benennen, dass der Name
treffend ist, den Inhalt des Objekts also genau beschreibt
kurz ist; lange Namen machen die Syntax unübersichtlich und sind umständlich zu
tippen
konsistent ist, so dass analoge Objekte analoge Namensbestandteile haben.
Zum zweiten Punkt, Kürze, ist zu sagen, dass einige Editoren, wie der von RStudio, über
eine automatische Texterkennung von bekannten Objekten verfügen. Insgesamt ist die
Kürze deutlich weniger wichtig als die Klarheit, die immer bevorzugt werden sollte. Frei
nach Donald Knuth: Computerprogramme sollten primär für Menschen geschrieben werden, und nur in zweiter Linie für Computer.
Tippt man in RStudio einen oder mehrere Buchstaben eines Objekts ein, das in
der Arbeitsumgebung existiert, so kann man mit der Tabulator-Taste Vorschläge für
passende Objekte bekommen. Das ist praktisch; man spart Tipparbeit.
Abgesehen von diesen zentralen Richtlinien existieren verschiedene Konventionen mit
lustigen Namen wie „Kamelschreibweise“ (z. B. ichWarEinKamel). Die Vorschläge
hier sind vom Tidyverse Styleguide12 abgeleitet; letztlich ist Konsistenz zentral. Einige
Beispiele für die Benennung von R-Objekten sind:
# gut
gewinn_abt1
arrival_delay
niederschlag_mm
add_row()
filter()
12
http://style.tidyverse.org/.
5.4 Namen geben
61
# schlecht
v1
object_234523
gewinn_abt1_quartal2_woche3_vor_abzug_der_steuern_nach_rueckstellung
do_stuff()
stuff()
Beachten Sie, dass man eine Funktion an den runden Klammern am Ende des Namens
erkennt (z. B. filter()). Da es oft nicht möglich ist, Variablennamen komplett eindeutig zu wählen, sollte man eine Datei anlegen, die jedem Variablennamen eine Erklärung
zuordnet (ein sog. Codebuch). Einige weitere Stil-Richtlinien:
Nur Kleinschreibung verwenden
Unterstrich verwenden (_), um Wörter innerhalb des Namens abzugrenzen (gewinn_
abt1)
Vor Ziffern ist kein Unterstrich nötig
Es sollten nur Buchstaben und Ziffern, keine Umlaute und Sonderzeichen verwendet
werden
Objektnamen sollten nicht mit Ziffern beginnen
Für Namen von Funktionen eignen sich Verben; für Datenobjekte sollten nie Verben
verwendet werden.
Da ein Objekt häufig in mehreren Schritten weiterverarbeitet wird, kann man, wenn man
gewillt ist, längere Namen in Kauf zu nehmen, folgendes Prinzip verwenden. Dazu setzt
man ein Kürzel für den eingesetzten Verarbeitungsschritt hinter den Namen des Objekts:
gewinn
gewinn_avg
gewinn_avg_q1
gewinn_avg_q1_nona
Einige typische Verarbeitungsschritte und mögliche Kürzel sind:
Mittelwert berechnet: _avg (average)
Analog für übrige deskriptive Statistiken: _md, _sd, _min, . . .
Fehlende Werte entfernt: _nona (No NA, keine fehlenden Werte; fehlende Werte bereinigt)
Dataframe in das „lange“ Format umgewandelt: _long
Dataframe mit einem zweiten vereinigt: _joined
Diagrammobjekt erzeugt: _plot
Häufigkeiten für ein Objekt: _count
Anteile für ein Objekt: _prop (proportion)
62
5
Datenstrukturen
Den zentralen Dataframe kann man konsistent mit data, d oder df bezeichnen; für
die Rohdaten fügt man _raw an
Namenssuffixe wie _df oder _list bieten sich meist nicht an, da diese Information
(z. B. über den Environment-Tab in RStudio) direkt ersichtlich ist
Mit der Kunst, Informationen über den Datensatz (Metadaten) in den Namen zu quetschen, kommt man an Grenzen. Eine andere (zusätzliche) Möglichkeit ist es, einem Objekt
Attribute zuzuweisen (s. Abschn. 5.1). Ein Nachteil der Attribute-Methode ist, dass man
es einem Dataframe nicht direkt ansieht, mit welchen Attributen er versehen ist; man übersieht es daher leicht. Außerdem ist dieses Vorgehen nicht weit verbreitet.
Die eben verwendeten Bezeichnungen sind alle auf Englisch. Sollte man nicht die
deutsche Sprache hochhalten? Bei internationalen Arbeitsgruppen erübrigt sich die Frage.
Aber auch in anderen Fällen ist es praktisch, sich an Standards zu halten. Sucht man z. B.
auf http://www.stackoverflow.com nach Hilfe, so sollten alle Variablennamen in Englisch
gewählt sein.
Aufgaben
1. Betrachten Sie den Vektor x <- factor(c("A", "a", "ä")). Wie wird
die Reihenfolge der Faktorstufen (levels) sein?13
2. Wie ist die Reihenfolge der Faktorstufen in diesem Vektor y <factor(c("s", "0", "ß", "Z", "!"))?14
3. Indizieren Sie aus dem Dataframe ein_df die zweite Spalte, und zwar mit Hilfe
des Dollar-Operators $. Welcher Objekttyp wird zurückgeliefert?15
4. Angenommen, Sie indizieren von einem Dataframe eine Spalte, d. h., Sie „verschmälern“ den Dataframe auf eine Spalte. Das zurückgelieferte Objekt sei ein
Vektor, kein Dataframe. Ist die Operation dann typstabil?16
13
levels(x) liefert [1] "a" "A" "ä".
Gibt man den Namen eines Objekts vom Typ Faktor ein, so werden die Stufen in korrekter Reihenfolge ausgegeben: y liefert [1] s 0 ß Z !; Levels: ! 0 s ß Z.
15
ein_df$geschmack; str(ein_df$geschmack). Es wird ein numerischer Vektor zurückgeliefert; Hey, wo ist mein Dataframe hin!
16
Nein.
14
6
Datenimport und -export
Lernziele
Wissen, auf welchen Wegen man Daten in R hineinbekommt
Wissen, was eine CSV-Datei ist
Wissen, was UTF-8 bedeutet
Wissen, mit welchen Datenformaten man häufig in R arbeitet
Erläutern können, was R unter dem „working directory“ versteht
Daten aus R exportieren können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
6.1
Daten in R importieren
In R kann man ohne Weiteres verschiedene Datenformate einlesen, gebräuchliche (wie
Excel oder CSV) oder weniger gebräuchliche.1 In RStudio lässt sich der Datenimport
z. B. durch einen schnellen Klick auf Import Dataset im Reiter Environment erledigen.2
1
Ein neues Datenformat ist Feather https://cran.r-project.org/web/packages/feather/index.html.
Um CSV-Dateien zu laden, wird durch den Klick im Hintergrund das Paket readr verwendet;
die entsprechende Syntax wird in der Konsole ausgegeben, so dass man sie sich anschauen und sie
weiterverwenden kann.
2
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_6
63
64
6
Datenimport und -export
Abb. 6.1 Daten einlesen (importieren) mit RStudio
6.1.1 Excel-Dateien importieren
Am einfachsten ist es, eine Excel-Datei (.xls oder .xlsx) über die RStudio-Oberfläche zu
importieren; das ist mit ein paar Klicks geschehen,3 s. Abb. 6.1. Nach dem gleichen Muster können Sie auch Datendateien aus anderen Statistikprogrammen einlesen wie SPSS,
SAS oder STATA (s. Abb. 6.1).
Es ist für bestimmte Zwecke sinnvoll, nicht zu klicken, sondern die Syntax einzutippen.
Zum Beispiel: Wenn Sie die komplette Analyse als Syntax in einer Datei haben (eine
sog. „Skriptdatei“), dann brauchen Sie (in RStudio) nur alles auszuwählen und auf Run
zu klicken, und die komplette Analyse läuft durch! Die Erfahrung zeigt, dass das ein
praktisches Vorgehen ist.
Daten (CSV, Excel . . . ) können Sie nicht öffnen über File > Open File . . . Dieser
Weg ist Skript-Dateien und R-Daten-Objekten vorbehalten.
6.1.2 Daten aus R-Paketen importieren
In R-Paketen wohnen nicht nur Funktionen, sondern auch Daten. Diese Daten kann man
mit dem Befehl data laden, dem man den Namen des zu ladenden Datensatzes dataset
und seines Heimatpakets paket übergibt: data(dataset, package = "paket").
Natürlich muss das Paket installiert sein (s. Abschn. 3.2.1). So lässt sich der Datensatz
profiles aus dem Paket okcupiddata laden:
data(profiles, package = "okcupiddata")
3
Im Hintergrund wird das Paket readxl verwendet, das i. d. R. schon installiert ist.
6.1 Daten in R importieren
65
Mit glimpse(profiles) bekommt man einen Überblick über den Dataframe. Im Reiter Environment findet sich dann der Eintrag profiles; daran erkennt man, dass die
Daten geladen (und damit verfügbar) sind.
6.1.3 Daten im R-Format laden
Das R-Datenformat erkennt man an der R-Endung rda oder RData. Dateien mit diesem Format kann man in RStudio über File > Open File... öffnen. Oder mit dem
Befehl load(file), wobei file der Dateiname ist, also z. B. extra.RData. Mit
dem Schwesterbefehl save() können Sie ein Objekt im R-Datenformat speichern, z. B.
save(stats_test, file = "stats_test.RData").
6.1.4 CSV-Dateien importieren
Die gebräuchlichste Form von Daten für statistische Analysen ist wahrscheinlich das CSVFormat.4 Das ist ein einfaches Format, eine simple Textdatei. Schauen Sie sich mal diesen
Auszug aus einer CSV-Datei an, wie ihn ein Texteditor zeigt, wenn man eine CSV-Datei
damit öffnet:
date_time,study_time,self_eval,interest,score,row_number,bestanden
19.06.2015 11:14:02,5,40,NA,0.5789473684210527,1,nein
19.06.2015 11:17:06,9,30,NA,0.7,2,nein
19.06.2015 11:18:08,0,10,NA,0.4,3,nein
19.06.2015 11:18:42,7,20,NA,0.5,4,nein
19.06.2015 11:19:39,1,50,NA,0.725,5,ja
19.06.2015 11:19:43,15,60,NA,0.85,6,ja
Erkennen Sie das Muster? Die erste Zeile gibt die „Spaltenköpfe“ wieder, also die Namen der Spalten; jede Spalte entspricht einer Variablen. Hier sind es sieben Spalten; zum
Beispiel heißt die fünfte Spalte „score“ und gibt die Punkte eines Studenten in einer Statistikklausur wieder. Die Spalten sind offenbar durch Kommata (,) voneinander getrennt.
Dezimalstellen sind in amerikanischer Manier mit einem Punkt (.) dargestellt. Die Daten
sind „rechteckig“; alle Spalten haben gleich viele Zeilen und umgekehrt alle Zeilen gleich
viele Spalten. Man kann sich diese Tabelle gut als Excel-Tabelle mit Zellen vorstellen,
in denen z. B. „row_number“ (Zelle oben links) oder „39“ (Zelle unten rechts) steht. An
4
CSV steht für comma separed values; ein Datenformat also, in dem Daten durch Kommata voneinander abgetrennt sind bzw. sein können. Eine neuere Variante von CSV heißt CSVY. In CSVY
werden Metadaten wie Autor, Titel, Datum über die eigentliche Tabelle geschrieben (und zwar im
YAML-Format), vgl. http://csvy.org/. Mit dem R-Paket csvy kann das CSVY-Format verwendet
werden.
66
6
Datenimport und -export
einigen Stellen steht NA. Das ist Errisch für fehlender Wert. NA gibt an, dass in dieser Zelle
nichts steht.
Der Button Import Dataset im Reiter Environment erlaubt Ihnen, CSV-Dateien einzulesen – es ist das gleiche Prinzip wie bei Excel-Tabellen. CSV-Dateien in diesem Format
können auch mit folgendem Befehl eingelesen werden: daten <- read_csv("daten.
csv"). Dafür wird das Paket readr benötigt, welches von tidyverse geladen wird.
6.1.4.1 Vorsicht bei nicht-amerikanisch kodierten Textdateien
Der Befehl read_csv() liest also eine CSV-Datei, was uns jetzt nicht übermäßig überrascht. Aber Achtung: Wenn Sie aus einem Excel mit deutscher Einstellung eine CSVDatei exportieren, wird diese CSV-Datei als Spaltentrennung ; (Strichpunkt) und als
Dezimaltrennzeichen , verwenden. Da der Befehl read_csv() laut amerikanischem
Standard mit Komma als Spaltentrennung und Punkt als Dezimaltrennzeichen arbeitet,
müssen wir die deutschen „Sonderlocken“ explizit angeben, z. B. so:
daten_deutsch <- read_delim("daten_deutsch.csv",
delim = ";",
locale = locale(decimal_mark = ","))
daten_deutsch <- read_csv2("daten_deutsch.csv")
Dabei steht delim (delimiter) für das Trennzeichen zwischen den Spalten und decimal_
mark für das Dezimaltrennzeichen. Die Funktion locale() versucht, alle länderspezifischen Eigenheiten von Datencodierung aufzufangen (z. B. Zeitzonen). Alternativ kann
man anstelle der genannten Syntax die Kurzform read_csv2() verwenden; damit wird
als Spaltentrennzeichen das Semikolon und als Dezimaltrennzeichen der Punkt eingestellt:
read_csv2("daten_deutsch.csv").
Man kommt hier auch mit „Klicken statt Tippen“ zum Ziel; in der Maske von „Import Dataset“ (für CSV-Dateien) gibt es den Auswahlpunkt Delimiter (Trennzeichen; s.
Abb. 6.2). Dort kann man das Komma durch einen Strichpunkt (oder was auch immer)
ersetzen. Es hilft, im Zweifel die Textdatei vorab mit einem Texteditor zu öffnen.
6.1.4.2 Daten von Github importieren
Die Funktion read.csv() ist im Standard-R enthalten; read_csv() kommt aus
readr. Übrigens kann man mit read_csv() auch CSV-Dateien von einer URL (Webseite) aus einlesen. Github ist ein zentraler Ort, wo u. a. R-Code und auch Daten abgelegt
werden. Um von Github Daten einzulesen – oder allgemeiner: Dateien zu öffnen, muss
man Links so aufbauen:
https://raw.github.com/name/projekt/zweig/ordner/datei.endung
Name spezifiziert einen Github-Nutzernamen; ein Projekt (repository) ist eine Art Ordner.
Mit Zweig (branch) ist die Variante des Projekts gemeint: Github baut auf der Versionie-
6.1 Daten in R importieren
67
Abb. 6.2 Trennzeichen einer CSV-Datei in RStudio einstellen
rungssoftware git auf, welche es erlaubt, von einer Datei mehrere (aktuelle, nicht überholte) Varianten gleichzeitig vorzuhalten. Der Standardzweig heißt master. Sie könnten
z. B. den CSV-Datensatz stats_test so einlesen:
prada_stats_test_url <paste0("https://raw.github.com/", # Domain, Webseitenname
"sebastiansauer/", # Nutzer
"Praxis_der_Datenanalyse/", # Projekt/Repositorium
"master/", # Variante
"data/stats_test.csv") # Ordner und Dateinamen
stats_test <- read_csv(prada_stats_test_url)
Der Befehl read_csv() liefert als Ergebnis einen Dataframe zurück; dieser Dataframe
ist aber noch nicht in einem R-Objekt gespeichert. Mit dem Zuweisungspfeil <- lässt sich
dieser Dataframe einem R-Objekt (vom Typ Dataframe) zuordnen. Einfach gesagt: Der
Inhalt der Datei stats_test.csv wird im Dataframe stats_test gespeichert. Falls
es dieses Objekt noch nicht gegeben haben sollte, so „erschafft“ R dieses Objekt. Ist es
schon vorhanden, so wird der alte Inhalt überschrieben.
Der Befehl paste0 „klebt“ (to paste) mehrere Textabschnitte zusammen; die null
dabei heißt, dass zwischen den Textabschnitten nichts steht; die Textabschnitte werden nahtlos zusammengeklebt. Wir hätten die ganze URL auch in einen
Textschnipsel packen können. So ist es nur ein bisschen übersichtlicher.
68
6
Datenimport und -export
6.1.4.3 Besonderheiten von Tibbles
Importiert man Daten mit read_csv(), so werden einige Metadaten als Attribute zum
Dataframe hinzugespeichert. str(stats_test) oder attributes(stats_test)
offenbart diese Attribute. Außerdem liefert read_csv keinen normalen Dataframe zurück, sondern einen Tibble. Tibbles sind Dataframes mit einer Reihe von zusätzlichen
Eigenschaften, dazu gehören: Wählt man nur eine Spalte aus einem Tibble, so bleibt es
ein Tibble; normale Dataframes verwandeln sich in diesem Fall manchmal in einen Vektor. Da ist Verwirrung vorprogrammiert. Probieren Sie mal folgende Syntax aus: Zuerst
machen wir aus stats_test einen normalen Dataframe mit as.data.frame(). Dann
wählen wir mit dem [-Operator zuerst zwei Spalten, dann eine Spalte aus. Im ersten Fall
wird ein Dataframe zurückgeliefert; im zweiten Fall aber ein Vektor, also kein Dataframe!
Ein normaler Dataframe ist auf einmal keine Tabelle mehr, wenn man nur eine Spalte auswählt, sondern er verwandelt sich in einen Vektor. Das kann zu unerwarteten Problemen
führen. Tibbles hingegen sind typstabil, bleiben also immer Tibbles, egal wie viele Spalten
man auswählt.
stats_test_normaler_df <- as.data.frame(stats_test)
str(stats_test_normaler_df[, c("self_eval", "score")]) # Output: df
#> 'data.frame':
1646 obs. of 2 variables:
#> $ self_eval: int 7 8 4 7 6 6 6 7 8 7 ...
#> $ score
: num 0.75 0.825 0.789 0.825 0.8 ...
str(stats_test_normaler_df[, "self_eval"]) # output: Vektor!
#> int [1:1646] 7 8 4 7 6 6 6 7 8 7 ...
Tibbles sind auch Dataframes; das erkennt man daran, dass sie über die Klasse data.
frame verfügen. Aber sie verfügen auch über die Klassen tbl_df5 und tbl, deshalb
sind sie keine normalen Dataframes:
class(stats_test)
#> [1] "tbl_df"
"tbl"
class(data.frame(stats_test))
#> [1] "data.frame"
attr(stats_test, "class")
#> [1] "tbl_df"
"tbl"
"data.frame"
"data.frame"
6.2 Textkodierung
Öffnet man eine Textdatei mit einem Texteditor seiner Wahl, so sieht man . . . Text und
sonst nichts, also keine Formatierung etc. Eine Textdatei besteht aus Text und sonst nichts.
Auch eine R-Skript-Datei (Coole_Syntax.R) oder eine Datendatei im CSV-Format
5
Gesprochen als „Tibblediff“; daher rührt der Name Tibble.
6.3 Daten exportieren
69
(stats_test) ist eine Textdatei. Technisch gesprochen werden in Textdateien nur Informationen gespeichert, die sich in Textzeichen ausgeben lassen; im Gegensatz dazu
speichert z. B. eine Word-Datei noch andere Informationen wie die Formatierung des
Textes. Natürlich werden die Textzeichen in der Computersprache, letztlich in Bits, gespeichert. Sagen wir, Ihr Computer trifft auf die Bitfolge 01010101. Woher weiß er, als
welches Textzeichen diese Bitfolge dargestellt werden soll? Codiert diese Bitfolge A, Ë›,
Å oder ein anderes Zeichen? Die Antwort ist: Er braucht eine Art Übersetzungstabelle,
auch Kodierungstafel genannt, um eine Bitfolge einem Textzeichen zuzuordnen. In einer
solchen Kodierungstafel steht das zugehörige Textzeichen für eine Bitfolge. Mehrere solcher Kodierungstafeln existieren. Die gebräuchlichste im Internet heißt UTF-8;6 Windows
nutzt die Tafel Windows-1252.7 Ich empfehle, Ihre Textdateien stets im UTF-8-Format zu
kodieren. RStudio fragt sie, wie Sie eine Textdatei kodieren möchten. Sie können auch unter File > Save with Encoding... die Kodierung einer Textdatei festlegen. Das
gilt sowohl für Ihre Syntax-Dateien als auch Ihre Daten-Dateien im CSV-Format.
Speichern Sie R-Textdateien wie Skripte stets mit UTF-8-Kodierung ab.
6.3
Daten exportieren
Wie bekommt man seine Daten wieder aus R raus („Ich will zu Excel zurück!“)? Eine
Möglichkeit bietet die Funktion readr::write_csv(); sie schreibt eine CSV-Datei:
write_csv(name_der_tabelle, "Dateiname.csv")
Mit help(write_csv) bekommt man mehr Hinweise dazu. Beachten Sie, dass immer
in das aktuelle Arbeitsverzeichnis geschrieben wird.
Das CSV-Format ist empfehlenswert, denn es ist einfach, menschenlesbar und nicht
von einer einzelnen Organisation beherrscht. Im Zweifel empfehle ich Ihnen, Ihre Daten
im CSV-Format abzuspeichern. Verwenden Sie das Standard-Format (mit einem Komma
als Spaltentrennzeichen und dem Punkt als Dezimaltrennzeichen). Speichern Sie die CSVDatei im UTF-8-Format ab.
6
7
https://de.wikipedia.org/wiki/UTF-8.
https://en.wikipedia.org/wiki/Windows-1252.
70
6
Datenimport und -export
Aufgaben
Richtig oder falsch?8
1.
2.
3.
4.
In CSV-Dateien dürfen Spalten nie durch Komma getrennt sein.
RStudio bietet die Möglichkeit, CSV-Dateien per Klick zu exportieren.
RStudio bietet die Möglichkeit, CSV-Dateien per Klick zu importieren.
„Deutsche“ CSV-Dateien verwenden als Spalten-Trennzeichen einen Strichpunkt.
5. R stellt fehlende Werte mit einem Fragezeichen ? dar.
6. Um Excel-Dateien zu importieren, kann man den Befehl read_csv() verwenden.
7. Im Standard nutzen CSV-Dateien ein Semikolon als Spaltentrennzeichen.
8. Daten im Rda-Format sind keine Textdateien.
9. Tibbles sind auch Dataframes.
10. Mit write_csv() exportiert man einen Dataframe in eine CSV-Datei.
Aufgaben
1. Wie kann man die Attribute des Datensatzes stats_test auslesen?9
2. Handelt es sich bei stats_test um einen Tibble, also eine spezielle Version
eines Dataframes? Tipp: Die Funktion attr() erlaubt es, einzelne Attribute
eines Objekts zu lesen oder zu verändern.10
3. Um die Standardkonfiguration von länderspezifischen Datencodierungen zu
sehen, gibt es die Funktion default_locale() aus readr. Führen Sie
die Funktion aus und interpretieren Sie das Ergebnis. Führen Sie auch
vignette("locales") aus, um weitere Hinweise zu erhalten.
8
F, F, R, R, F, F, F, R, R, R.
Das geht mit attributes(stats_test); Möchte man eines dieser Attribute wieder loswerden, so kann man dieses Attribut mit attr auf NULL setzen: attr(stats_test, "spec")
<- NULL.
10
Die Funktion attr(stats_test, "class") liefert zurück, dass das Objekt stats_test
zu folgenden Objekttypen gehört: tbl_df,tbl,data.frame. tbl_df und tbl lassen sich
nicht nur als „Tibble“ aussprechen, sie zeigen auch an, dass es sich bei diesem Objekt um ein Tibble
handelt. Aber eben auch um einen Dataframe.
9
6.3 Daten exportieren
71
4. Tibbles haben eine schönere Print-Funktion als normale Dataframes. In R werden Objekte am Bildschirm ausgegeben (gedruckt), wenn man ihren Namen
eingibt. Vergleichen Sie mal die Ergebnisse der beiden folgenden Zeilen:
# nur die ersten paar Zeilen zeigen, daher mit head()
as_tibble(head(profiles)) # tibble
as.data.frame(head(profiles)) # normaler Dataframe
5. Über welche Attribute verfügt ein Tibble? Geben Sie die Attribute des Tibbles
stats_test an.11
6. Probieren Sie den Befehl aus und erklären Sie seine Funktion: d <read_csv(file.choose()).12
11
attributes(stats_test).
Es öffnet sich ein Fenster, in dem man die zu ladende CSV-Datei auswählen kann und zwar
komfortabel per Maus. Für den interaktiven Gebrauch ist das komfortabel. Natürlich könnte man
dann gleich den Button „Import Dataset“ verwenden.
12
Teil III
Daten aufbereiten
7
Datenjudo
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
Lernziele
Die zentralen Ideen der Datenanalyse mit dplyr verstehen
Typische Probleme der Datenanalyse schildern können
Zentrale dplyr-Befehle anwenden können
dplyr-Befehle kombinieren können
Die Pfeife anwenden können
Dataframes zusammenführen (joinen) können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(mosaic)
library(tidyverse)
library(stringr)
library(car)
data(stats_test, package = "pradadata")
data(flights, package = "nycflights13")
data(stats_test, package = "pradadata")
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_7
75
76
7
Datenjudo
data(profiles, package = "okcupiddata")
data(airlines, package = "nycflights13")
Das Paket tidyverse lädt dplyr, ggplot2 und weitere Pakete. Daher ist es komfortabler, tidyverse zu laden, damit spart man sich Tipparbeit. Die eigentliche Funktionalität, die wir in diesem Kapitel nutzen, kommt aus dem Paket dplyr. Mit Datenjudo
ist gemeint, die Daten für die eigentliche Analyse „aufzubereiten“. Unter Aufbereiten ist
hier das Umformen, Prüfen, Bereinigen, Gruppieren und Zusammenfassen von Daten zu
verstehen. Die deskriptive Statistik kann man unter die Rubrik Aufbereiten fassen, da eine
zentrale Aufgabe der deskriptiven Statistik die Zusammenfassung von Daten ist.
Ist das Aufbereiten von Daten auch nicht statistisch anspruchsvoll, so ist es trotzdem von Bedeutung und häufig zeitintensiv. Eine Anekdote zur Relevanz der Datenaufbereitung, die (so will es die Geschichte) mir an einer Bar nach einer einschlägigen
Konferenz erzählt wurde (daher keine Quellenangabe, Sie verstehen . . . ): Eine Computerwissenschaftlerin aus den USA (deutschen Ursprungs) hatte einen beeindruckenden
„Track Record“ an Siegen in Wettkämpfen der Datenanalyse. Tatsächlich hatte sie keine besonderen, raffinierten Modellierungstechniken eingesetzt; klassische Regression war
ihre Methode der Wahl. Bei einem Wettkampf, bei dem es darum ging, Krebsfälle aus
Krankendaten vorherzusagen (z. B. auf Basis von Röntgenbildern) fand sie nach langem
Datenjudo heraus, dass in die „ID-Variablen“ Information gesickert war, die dort nicht
hingehörte und die sie nutzen konnte für überraschend (aus Sicht der Mitstreiter) gute
Vorhersagen zu Krebsfällen. Wie war das möglich? Die Daten stammten aus mehreren
Kliniken, jede Klinik verwendete ein anderes System, um IDs für Patienten zu erstellen. Überall waren die IDs stark genug, um die Anonymität der Patienten sicherzustellen,
aber gleichwohl konnte man (nach einigem Judo) unterscheiden, welche ID von welcher
Klinik stammte. Was das bringt? Einige Kliniken waren reine Screening-Zentren, die die
Normalbevölkerung versorgten. Dort sind wenig Krebsfälle zu erwarten. Andere Kliniken
jedoch waren Onkologie-Zentren für bereits bekannte Patienten oder für Patienten mit besonderer Risikolage – wenig überraschend, dass man dann höhere Krebsraten vorhersagen
kann. Eigentlich ganz einfach; besondere Mathematik steht hier (zumindest in dieser Geschichte) nicht dahinter. Und, wenn man den Trick kennt, ganz einfach. Aber wie so oft
ist es nicht leicht, den Trick zu finden. Sorgfältiges Datenjudo hat hier den Schlüssel zum
Erfolg gebracht.
Bevor man seine Statistik-Trickkiste so richtig schön aufklappen kann, muss man die
Daten häufig erst noch in Form bringen. Das ist nicht schwierig in dem Sinne, dass es um
komplizierte Mathematik ginge. Allerdings ist mitunter recht viel Zeit notwendig, und ein
paar (oder viele) handwerkliche Tricks sind notwendig. Hier soll dieses Kapitel helfen.
Typische Probleme, die immer wieder auftreten, sind:
Zeilen sortieren: Welche Kunden haben im letzten Quartal am meisten zum Deckungsbeitrag beigetragen?
Spalten auswählen: Von den 100 Items der Kundenbefragung interessieren uns nur die
ersten fünf, weil die übrigen so niederschlagend beantwortet wurden.
7.1 Daten aufbereiten mit dplyr
77
Deskriptive Statistiken berechnen: Was war die häufigste Antwort auf die Frage nach
der Zufriedenheit mit der Führungskraft?
Neue Variablen (Spalten) berechnen: Ein Student fragt nach seiner Anzahl an richtigen
Aufgaben in der Statistik-Probeklausur. Wir wollen helfen und im entsprechenden Datensatz eine Spalte erzeugen, in der pro Person die Anzahl der richtig beantworteten
Fragen steht.
7.1
Daten aufbereiten mit dplyr
Willkommen in der Welt von dplyr! dplyr hat seinen Namen, weil es sich ausschließlich um Dataframes bemüht; es erwartet einen Dataframe als Eingabe und gibt einen
Dataframe zurück (zumindest bei den meisten Befehlen). Es gibt viele Möglichkeiten,
Daten mit R aufzubereiten; dplyr1 ist ein populäres Paket dafür (wenn Sie tidyverse
laden, wird dplyr auch geladen). dplyr basiert auf zwei Ideen:
1. Lego-Prinzip: Komplexe Datenanalysen in Bausteine zerlegen (vgl. Abb. 7.1).
2. Durchpfeifen: Alle Operationen werden nur auf Dataframes angewendet; jede Operation erwartet einen Dataframe als Eingabe und gibt wieder einen Dataframe aus (vgl.
Abb. 7.2).
Das erste Prinzip von dplyr ist, dass es nur ein paar wenige Grundbausteine geben sollte,
die sich gut kombinieren lassen. Sprich: Wenige grundlegende Funktionen mit eng umgrenzter Funktionalität stellen den dplyr-Werkzeugkoffer. Ein Nachteil dieser Konzeption
kann sein, dass man recht viele dieser Bausteine kombinieren muss, um zum gewünschten
Ergebnis zu kommen. Außerdem muss man die Logik des Baukastens gut verstanden habe
– die Lernkurve ist also erstmal steiler als bei maßgeschneiderten Befehlen. Dafür ist man
dann nicht darauf angewiesen, dass es irgendwo „Mrs Right“ gibt, die genau das kann,
was ich will. Außerdem braucht man sich auch nicht viele Funktionen merken. Es reicht,
einen kleinen Satz an Funktionen zu kennen (die praktischerweise konsistent in Syntax
und Methodik sind). Diese Bausteine sind typische Tätigkeiten im Umgang mit Daten;
nichts Überraschendes. Wir schauen uns diese Bausteine gleich näher an.
Abb. 7.1 Lego-Prinzip: Zerlege eine komplexe Struktur in
einfache Bausteine
1
Komplexe Struktur
https://cran.r-project.org/web/packages/dplyr/index.html.
Einzelne Bausteine
78
7
Eingabe-Tabelle
ID V2 V2
D
Datenjudo
Ausgabe-Tabelle
ID V2 V2
D
Tabellen-Operation 1
ID V2 V2
D
Tabellen-Operation 2...n
Abb. 7.2 Durchpfeifen: Ein Dataframe wird von Operation zu Operation weitergereicht
Das zweite Prinzip von dplyr ist es, einen Dataframe von Operation zu Operation
durchzureichen. Die Standard-Befehle von dplyr arbeiten also nur mit Dataframes. Jeder
Arbeitsschritt bei dplyr erwartet einen Dataframe als Eingabe und gibt im Gegenzug
wieder einen Dataframe aus. Das ist praktisch, da viele Funktionen als Eingabe einen
Dataframe erwarten; allerdings geben viele Funktionen (außerhalb von dplyr) keinen
Dataframe – oder keinen Dataframe in Normalform – zurück.
Aufgaben
1. Kennen Sie R-Funktionen, die als Eingabe einen Dataframe erwarten, aber keinen Dataframe (in Normalform) zurückgeben? Nennen Sie Beispiele.2
2. Woran kann man erkennen, welcher Art (genauer: von welcher Klasse) das Objekt ist, das zurückgegeben wird?3
7.2 Zentrale Bausteine von dplyr
7.2.1
Zeilen filtern mit filter()
Manchmal will man bestimmte Zeilen aus einer Tabelle filtern; der dplyr-Befehl dazu
heißt filter(). Abb. 7.3 zeigt ein Sinnbild für filter(). Beispiele für Situationen, in
denen man Zeilen filtern möchte:
Jemand arbeitet für die Zigarettenindustrie und ist nur an den Rauchern interessiert
(die im Übrigen unser Gesundheitssystem retten laut Krämer (2011)), nicht an NichtRauchern
Es sollen die nur Umsatzzahlen des letzten Quartals untersucht werden, nicht die der
vorherigen Quartale
Es sollen nur die Daten aus Labor X (nicht die aus Labor Y) ausgewertet werden
2
table(stats_test["bestanden"]), favstats(~score, data = stats_test),
summary(stats_test), mean(~score, data = stats_test, na.rm= TRUE),
stats_test["bestanden"].
3
Dazu kann man die Funktion class() verwenden oder die Funktion str(), z. B.
summary(stats_test) %>% str.
7.2 Zentrale Bausteine von dplyr
Abb. 7.3 Zeilen filtern
79
ID
Name
Note1
ID
Name
Note1
1 Anna
1
2 Anna
1
1 Anna
1
3 Berta
2
2 Anna
1
4 Carla
2
5 Carla
2
Merke: Die Funktion filter() filtert Zeilen aus einem Dataframe. Die Zeilen, die
zum Filterkriterium passen, bleiben im Datensatz.
Schauen wir uns einige Beispiele aus dem Datensatz profiles an; nicht vergessen
die Daten vorher zu laden. Achtung: „Wohnen“ die Daten im Paket okcupiddata, muss
dieses Paket installiert sein, damit man auf die Daten zugreifen kann.
df_frauen <- filter(profiles, sex == "f") # nur die Frauen
df_alt <- filter(profiles, age > 70) # nur die alten Menschen
# nur die alten Frauen, d.h. UND-Verknüpfung:
df_alte_frauen <- filter(profiles, age > 70, sex == "f")
# zwischen (between) 35 und 60:
df_mittelalt <- filter(profiles, between(age, 35, 60))
# liefert alle Personen, die Nicht-Raucher *oder* Nicht-Trinker sind:
df_nosmoke_nodrinks <- filter(profiles,
smokes == "no" | drinks == "not at all")
Gar nicht so schwer, oder? Allgemeiner gesprochen werden diejenigen Zeilen gefiltert
(also behalten bzw. zurückgeliefert), für die das Filterkriterium TRUE ist. filter() ist
deutlich einfacher (und klarer) als Standard-R. Vergleichen Sie mal:
# dplyr:
filter(profiles, age > 70, sex == "f", drugs == "sometimes")
# base-R:
profiles[profiles$age > 70 & profiles$sex == "f" &
profiles$drugs == "sometimes", ]
Manche Befehle wie filter() haben einen Allerweltsnamen; gut möglich, dass
ein Befehl mit gleichem Namen in einem anderen (geladenen) Paket existiert. Das
kann dann zu Verwirrung führen – und kryptischen Fehlern. Im Zweifel den Namen
des richtigen Pakets ergänzen, und zwar zum Beispiel so: dplyr::filter(...).
80
7
7.2.2
Datenjudo
Fortgeschrittene Beispiele für filter()
Man kann alle Elemente (Zeilen) filtern, die zu einer Menge gehören, und zwar mit diesem
Operator: %in%:
filter(profiles, body_type %in% c("a little extra", "average"))
Besonders Variablen vom Typ String (Text) laden zu einigen Extra-Überlegungen ein,
wenn es ums Filtern geht. Sagen wir, wir wollen alle Personen filtern, die Katzen bei den
Haustieren erwähnen. Es soll reichen, wenn cat ein Teil des Textes ist; also likes dogs
and likes cats wäre O. K. (soll gefiltert werden); loves cats like crazy soll
auch O. K. sein. Dazu nutzen wir das Paket stringr, das Befehle zur Bearbeitung von
Strings (Textdaten) bereitstellt; es wird ebenfalls von tidyverse geladen; Sie müssen es
nicht extra starten.
filter(profiles, str_detect(pets, "cats"))
Ein häufiger Fall ist, Zeilen ohne fehlende Werte (NAs) zu filtern. Das geht einfach, z. B.
so:
profiles_keine_nas <- drop_na(profiles)
Aber was ist, wenn wir nur bei bestimmten Spalten wegen fehlender Werte besorgt sind?
Sagen wir bei income und bei sex:
profiles_keine_nas2 <- drop_na(profiles, income, sex)
filter(profiles_keine_nas2)
Aufgaben
Richtig oder falsch?4
1.
2.
3.
4.
5.
filter() filtert Spalten.
filter() ist eine Funktion aus dem Paket dplyr.
filter() erwartet als ersten Parameter das Filterkriterium.
filter() lässt nur ein Filterkriterium zu.
Möchte man aus dem Datensatz profiles (okcupiddata) die Frauen filtern,
so ist folgende Syntax korrekt: filter(profiles, sex == "f").
6. filter(profiles, between(age, 35, 60)) filtert die mittelalten
Frauen (zwischen 35 und 60)
4
F, R, F, F, R, R.
7.2 Zentrale Bausteine von dplyr
81
vorher
Abb. 7.4 Spalten auswählen
nachher
ID Name N1 N2 N3
7.2.3
ID Name N1
1 Anna
1
2
3
1 Anna
1
2 Berta
1
1
1
2 Berta
1
3 Carla
2
3
4
3 Carla
2
Spalten wählen mit select()
Das Gegenstück zu filter() ist select(); dieser Befehl liefert die gewählten Spalten
zurück (und entfernt die nicht gewählten). Das ist häufig praktisch, wenn der Datensatz
sehr „breit“ ist, also viele Spalten enthält. Dann kann es übersichtlicher sein, nur die relevanten auszuwählen. Abb. 7.4 zeigt ein Sinnbild für diesen Befehl.
Merke: Die Funktion select() wählt Spalten aus einem Dataframe aus.
Betrachten wir hier als Beispiel den Datensatz stats_test aus dem Paket
pradadata. Dieser Datensatz beinhaltet Daten zu einer Statistikklausur.
select(stats_test, score) # Spalte "score" auswählen
select(stats_test, score, study_time)
# Spalten "score" und "study_time" auswählen
select(stats_test, score:study_time) # dito
select(stats_test, 5:6) # Spalten 5 bis 6 auswählen
Tatsächlich ist der Befehl select() sehr flexibel; es gibt viele Möglichkeiten, Spalten
auszuwählen. Im dplyr-Cheatsheet5 findet sich ein guter Überblick dazu. Probieren Sie
einmal diese Variante aus:
vars <- c("score", "study_time")
select(stats_test, one_of(vars))
5
https://www.rstudio.com/resources/cheatsheets/.
82
7
Datenjudo
Aufgaben
Richtig oder falsch?6
1. select() wählt Zeilen aus.
2. select() ist eine Funktion aus dem Paket knitr.
3. Möchte man zwei Spalten auswählen, so ist folgende Syntax prinzipiell korrekt:
select(df, spalte1, spalte2).
4. Möchte man Spalten 1 bis 10 auswählen, so ist folgende Syntax prinzipiell korrekt: select(df, spalte1:spalte10).
5. Mit select() können Spalten nur bei ihrem Namen, aber nicht bei ihrer Nummer aufgerufen werden.
7.2.4
Zeilen sortieren mit arrange()
Man kann zwei Arten des Umgangs mit R unterscheiden: Zum einen der „interaktive Gebrauch“ und zum anderen „richtiges Programmieren“. Beim interaktiven Gebrauch geht
es uns darum, die Fragen zum aktuell vorliegenden Datensatz (schnell) zu beantworten. Es
geht nicht darum, eine allgemeine Lösung zu entwickeln, die wir in die Welt verschicken
können und die dort ein bestimmtes Problem löst, ohne dass der Entwickler (wir) dabei
Hilfestellung geben muss. Wir wollen keine Applikation entwickeln. „Richtige“ Software,
wie ein R-Paket auf CRAN oder Microsoft PowerPoint, muss diese Erwartung erfüllen;
„richtiges Programmieren“ ist dazu vonnöten. Natürlich sind in diesem Fall die Ansprüche an die Syntax („Code“, hört sich cooler an) viel höher. In dem Fall muss man alle
Eventualitäten voraussehen und sicherstellen, dass das Programm auch beim merkwürdigsten Nutzer brav seinen Dienst tut. Der Code muss auf Herz und Nieren geprüft sein.
Wir haben hier, beim interaktiven Gebrauch, geringere Ansprüche bzw. andere Ziele (aber
s. Kap. 29).
Beim interaktiven Gebrauch von R (oder beliebigen Analyseprogrammen) ist das Sortieren von Zeilen eine recht häufige Tätigkeit. Ein typisches Beispiel wäre der Lehrer,
der eine Tabelle mit Noten hat und wissen will, welche Schüler die schlechtesten oder
die besten sind in einem bestimmten Fach. Oder bei der Prüfung der Umsätze nach Filialen möchten wir die umsatzstärksten sowie -schwächsten Niederlassungen kennen. Ein
R-Befehl hierzu ist arrange(); einige Beispiele zeigen die Funktionsweise am besten:
arrange(stats_test, score) # *schlechteste* Noten zuerst
arrange(stats_test, -score) # *beste* Noten zuerst
arrange(stats_test, interest, score) # zwei Sortierkriterien
6
F, F, R, R, F.
7.2 Zentrale Bausteine von dplyr
Abb. 7.5 Spalten sortieren
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
83
ID
# A tibble: 2 x 5
study_time self_eval interest score
<int>
<int>
<int> <dbl>
1
3
10
4
1
2
3
9
4
1
# A tibble: 2 x 7
date_time
study_time self_eval
<chr>
<int>
<int>
1 29.08.2017 18~
3
6
2 30.09.2017 11~
1
1
Name
Note1
ID
Name
Note1
1 Anna
1
1 Anna
1
2 Anna
5
3 Berta
2
3 Berta
2
5 Carla
3
4 Carla
4
4 Carla
4
5 Carla
3
2 Anna
5
Gute
Noten
zuerst!
bestanden
<chr>
ja
ja
interest score row_number bestanden
<int> <dbl>
<int> <chr>
5 0
2150 nein
1 0.0238
2166 nein
Die generelle Syntax lautet arrange(df, Spalte1, ...), wobei df den Dataframe bezeichnet und Spalte1 die erste zu sortierende Spalte; die Punkte ... geben
an, dass man weitere Parameter übergeben kann. Gibt man zwei Spalten an (Spalte1
und Spalte2), nach denen sortiert werden soll, so wird erst nach Spalte1 sortiert.
Identische Werte werden dann nach Spalte2 sortiert. Man kann sowohl numerische
Spalten als auch Textspalten sortieren. Am wichtigsten ist hier, dass man weitere Spalten
übergeben kann. Standardmäßig sortiert arrange() aufsteigend (weil kleine Zahlen im
Zahlenstrahl vor den großen Zahlen kommen). Möchte man diese Reihenfolge umdrehen
(große Werte zuerst, d. h. absteigend), so kann man ein Minuszeichen vor den Namen der
Spalte setzen. Gibt man zwei oder mehr Spalten an, so werden pro Wert von Spalte1
die Werte von Spalte2 sortiert etc.; man betrachte den Output des bereits genannten
Beispiels dazu. Abb. 7.5 erläutert die Arbeitsweise von arrange().
Merke: Die Funktion arrange() sortiert die Zeilen eines Dataframes.
Ein ähnliches Ergebnis erhält man mit top_n(), welches die n() größten Ränge wiedergibt: top_n(stats_test, 3, interest). Gibt man keine Spalte an (also nur
top_n(stats_test)), so bezieht sich top_n auf die letzte Spalte im Datensatz. Wenn
sich aber, wie hier, mehrere Objekte den größten Rang (Wert 6) teilen, bekommen wir
nicht drei Zeilen zurückgeliefert, sondern entsprechend mehr. dplyr „denkt“ sich: „O. K.,
er will die drei besten Ränge; aber neun Studenten teilen sich den ersten Rang (Interes-
84
7
Datenjudo
se 6), wen sollte ich da ausschließen? Am besten, ich liefere alle neun zurück, sonst wäre
es ja ungerecht, weil alle neun ja gleich interessant sind.“
Aufgaben
Richtig oder falsch?7
1.
2.
3.
4.
5.
arrange() arrangiert Spalten.
arrange() sortiert im Standard absteigend.
arrange() lässt nur ein Sortierkriterium zu.
arrange() kann numerische Werte, aber nicht Zeichenketten sortieren.
top_n(5) liefert immer fünf Werte zurück.
Aufgaben
1. Sortieren Sie den Dataframe profiles nach Alter und nach Körperform.8
2. Sortieren Sie den Datensatz stats_test nach dem Datum; wird das Datum
korrekt erkannt?9
3. Lesen Sie die Spalte date_time als Datums-Zeit-Objekt ein; dazu hilft die
Funktion lubridate::parse_date_time(). Dieser Funktion muss man
das Format der Datum-Zeit-Angabe mitteilen, in diesem Fall (hey, das ist ein
Tipp!) so: "dmY HMS"; kleine Buchstaben stehen für zweistellige Datumsangaben; große Buchstaben für vierstellige Daten bzw. Uhrzeiten im 24-StundenFormat (im Gegensatz zum 12-Stunden-Format).10
4. Recherchieren Sie die weiteren Optionen für Datums-Zeitangaben dieser Funktion.11
7.2.5 Einen Datensatz gruppieren mit group_by()
Einen Datensatz zu gruppieren, ist eine häufige Angelegenheit: Was ist der mittlere Umsatz in Region X im Vergleich zu Region Y? Ist die Reaktionszeit in der Experimentalgruppe kleiner als in der Kontrollgruppe? Können Männer schneller ausparken als Frauen?
7
F, F, F, F, F.
data(profiles, package = "okcupiddata"); arrange(profiles, age,
body_type).
9
arrange(stats_test, date_time); nein, die Sortierreihenfolge stimmt nicht.
10
arrange(stats_test, lubridate::parse_date_time(date_time, orders
= "dmY HMS")); vergessen Sie nicht, das Paket ggf. noch zu installieren und zu laden.
11
?parse_date_time().
8
7.2 Zentrale Bausteine von dplyr
Abb. 7.6 Datensätze nach
Subgruppen aufteilen
85
ID Name Note Fach
1 Anna
1A
2 Berta
1A
3 Carla
2B
ID Name
1 Anna
ID 2Name
Berta
1 Anna
ID Name
2 Berta
1 Anna
ID 2 Name
Berta
1 Anna
2 Berta
Note Fach
1D
Note 1Fach
B
1C
Note Fach
1
1 BA
Note1 Fach
B
1A
1A
Man sieht, dass das Gruppieren v. a. in Verbindung mit Mittelwerten oder anderen Zusammenfassungen sinnvoll ist; dazu im nächsten Abschnitt mehr.
Gruppieren bedeutet, einen Datensatz anhand einer diskreten Variablen (z. B.
Geschlecht) so aufzuteilen, dass Teil-Datensätze entstehen – pro Gruppe ein TeilDatensatz (z. B. ein Datensatz, in dem nur Männer enthalten sind, und einer, in dem
nur Frauen enthalten sind).
In Abb. 7.6 wurde der Datensatz anhand der Spalte (d. h. Variable) Fach in mehrere
Gruppen geteilt (Fach A, Fach B . . . ). Wir könnten uns als Nächstes z. B. Mittelwerte pro
Fach – d. h. pro Gruppe (pro Ausprägung von Fach) – ausgeben lassen; in diesem Fall
vier Gruppen (Fach A bis D).
test_gruppiert <- group_by(stats_test, interest)
test_gruppiert
test_gruppiert <- group_by(stats_test, interest)
select(test_gruppiert, study_time, interest, score) %>% head(4)
#> # A tibble: 4 x 3
#> # Groups:
interest [3]
#>
study_time interest score
#>
<int>
<int> <dbl>
#> 1
3
4 0.75
#> 2
4
5 0.825
#> 3
3
1 0.789
#> 4
4
5 0.825
Schaut man sich nun den Datensatz an, sieht man erstmal wenig Effekt der Gruppierung.
R teilt uns lediglich mit (Groups: interest [3]), dass es drei Gruppen gibt, aber
es gibt keine extra Spalte oder sonstige Anzeichen der Gruppierung. Aber keine Sorge,
wenn wir gleich einen Mittelwert ausrechnen, bekommen wir den Mittelwert pro Gruppe.
group_by() alleine ist nicht nützlich. Nützlich wird es erst, wenn man weitere Funktionen auf den gruppierten Datensatz anwendet – z. B. Mittelwerte pro Gruppe ausrechnet
86
7
Abb. 7.7 Schematische
Darstellung „Gruppieren –
Zusammenfassen – Kombinieren“
ID Name Note Fach
1 Anna
2A
2 Berta
1A
3 Anna
2B
4 Carla
4B
Ausgangs-Tabelle
Nach Gruppen
aufsplitten
Datenjudo
ID Name Note Fach ID Name Note Fach
3 Anna
2B
1 Anna
2A
4 Carla
4B
2 Berta
1A
Jede Gruppe
zusammenfassen
mean(Note)
1.5
Zusammenfassungen in
Tabelle kombinieren
mean(Note)
3
Fach mean(Note)
A
1.5
3
B
(z. B. mit summarise(), s. Abschn. 7.2.6). Die dplyr-Funktionen berücksichtigen nämlich die Gruppierung. So kann man einfach Mittelwerte pro Gruppe ausrechnen. dplyr
kombiniert dann die Zusammenfassungen (z. B. Mittelwerte) der einzelnen Gruppen in
einen Dataframe und gibt diesen mit den Mittelwerten pro Gruppe aus. Die Idee des
„Gruppieren – Zusammenfassen – Kombinieren“ ist flexibel; man kann sie häufig brauchen. Es lohnt sich, diese Idee zu lernen (vgl. Abb. 7.7).
Aufgaben
Richtig oder falsch?12
1. Mit group_by() gruppiert man einen Datensatz.
2. group_by() lässt nur ein Gruppierungskriterium zu.
3. Die Gruppierung durch group_by() wird nur von Funktionen aus dplyr erkannt.
4. group_by() kann sinnvoll mit summarise() kombiniert werden.
Merke: Mit group_by() teilt man einen Datensatz in Gruppen ein, entsprechend
den Werten einer oder mehrerer Spalten.
7.2.6
Eine Spalte zusammenfassen mit summarise()
Vielleicht die wichtigste oder häufigste Tätigkeit bei der Analyse von Daten ist es, eine
Spalte zu einem Wert zusammenzufassen; summarise() leistet dies. Beispiele solcher
12
R, F, R, R.
7.2 Zentrale Bausteine von dplyr
Abb. 7.8 Spalten zu einer
Zahl zusammenfassen
87
Note
1
1
2
Mittelwert
Note
2
4
Zusammenfassungsoperationen sind: einen Mittelwert berechnen, den größten (kleinsten)
Wert heraussuchen oder die Streuung berechnen. Die Gemeinsamkeit dieser Operationen ist, dass sie eine Spalte zu einem Wert zusammenfassen, „aus Spalte mach einzelnen
Wert“, sozusagen. Daher ist der Name des Befehls summarise() ganz passend. Genauer gesagt fasst dieser Befehl eine Spalte zu einer Zahl zusammen anhand einer Funktion
wie mean(), sd() oder max() (vgl. Abb. 7.8). Hierbei ist jede Funktion erlaubt, die
eine Spalte als Input verlangt und eine Zahl zurückgibt; solche Funktionen nennen wir
Zusammenfassungsfunktionen. Andere Funktionen sind bei summarise() nicht erlaubt.
summarise(stats_test, mean(score))
#> # A tibble: 1 x 1
#>
`mean(score)`
#>
<dbl>
#> 1
NaN
Man könnte diesen Befehl so ins Deutsche übersetzen: „Fasse aus Tabelle stats_test die
Spalte score anhand des Mittelwerts zusammen.“ Nicht vergessen, wenn die Spalte score
fehlende Werte hat, wird der Befehl mean() dies standardmäßig mit NA quittieren. Ergänzt man den Parameter nr.rm = TRUE, so ignoriert R fehlende Werte, und der Befehl
mean() liefert ein Ergebnis zurück. Jetzt können wir auch die Gruppierung nutzen:
test_gruppiert <- group_by(stats_test, interest)
summarise(test_gruppiert, mean(score, na.rm = TRUE))
#> # A tibble: 7 x 2
#>
interest `mean(score, na.rm = TRUE)`
#>
<int>
<dbl>
#> 1
1
0.721
#> 2
2
0.744
#> 3
3
0.768
#> 4
4
0.775
#> 5
5
0.815
#> 6
6
0.862
#> 7
NA
0.770
Der Befehl summarise() erkennt also, wenn eine (mit group_by()) gruppierte Tabelle
vorliegt. Jegliche Zusammenfassung, die wir anfordern, wird anhand der Gruppierungsinformation aufgeteilt werden. In dem Beispiel bekommen wir einen Mittelwert für jeden
Wert von interest. Interessanterweise sehen wir, dass der Mittelwert tendenziell größer
wird, je größer interest wird.
88
7
Datenjudo
Alle diese dplyr-Befehle geben einen Dataframe zurück, was praktisch ist für die
weitere Verarbeitung. In diesem Fall heißen die Spalten interest und mean(score).
Letzterer Name ist nicht so schön, daher ändern wir ihn wie folgt:
test_gruppiert <- group_by(stats_test, interest)
summarise(test_gruppiert, mw_pro_gruppe = mean(score, na.rm = TRUE))
#> # A tibble: 7 x 2
#>
interest mw_pro_gruppe
#>
<int>
<dbl>
#> 1
1
0.721
#> 2
2
0.744
#> 3
3
0.768
#> 4
4
0.775
#> 5
5
0.815
#> 6
6
0.862
#> 7
NA
0.770
Nun heißt die zweite Spalte mw_pro_Gruppe. Das Argument na.rm = TRUE veranlasst, bei fehlenden Werten trotzdem einen Mittelwert zurückzuliefern (die Zeilen mit
fehlenden Werten werden in dem Fall ignoriert).
Merke: Mit summarise() kann man eine Spalte eines Dataframes zu einem Wert
zusammenfassen.
Aufgaben
Richtig oder falsch?13
1. Möchte man aus der Tabelle stats_test den Mittelwert für die Spalte
score berechnen, so ist folgende Syntax korrekt: summarise(stats_test,
mean(score)).
2. summarise() liefert eine Tabelle, genauer: einen Tibble, zurück.
3. Die Tabelle, die diese Funktion zurückliefert: summarise(stats_test,
mean(score)), hat eine Spalte mit dem Namen mean(score).
4. summarise() lässt zu, dass die zu berechnende Spalte einen Namen vom Nutzer zugewiesen bekommt.
5. summarise() darf nur verwendet werden, wenn eine Spalte zu einem Wert
zusammengefasst werden soll.
6. Man kann summarise() nicht ohne Weiteres nutzen, um eine Korrelation zu
berechnen.
13
R, R, R, R, R, R.
7.2 Zentrale Bausteine von dplyr
Abb. 7.9 Sinnbild für
89
Gruppe A Gruppe B Gruppe C
count()
X
X
X
Gruppe
5
3
n
A
5
B
3
C
4
4
7.2.7 Zeilen zählen mit n() und count()
Ebenfalls nützlich ist es, Zeilen zu zählen, also Häufigkeiten zu bestimmen (s. Abb. 7.9).
Im Gegensatz zum Standardbefehl14 nrow() versteht der dplyr-Befehl n() auch Gruppierungen, die über group_by() erstellt wurden. n() darf im Pfeifen-Workflow nur
im Rahmen von summarise() oder ähnlichen dplyr-Befehlen verwendet werden.
Warum? Weil n() eine Zusammenfassungsfunktion ist, also einen Vektor zu einem
einzelnen Wert zusammenfasst; nur solche Funktionen sind bei summarise() erlaubt.
summarise(stats_test, n())
#> # A tibble: 1 x 1
#>
`n()`
#>
<int>
#> 1 1646
summarise(test_gruppiert, n())
#> # A tibble: 7 x 2
#>
interest `n()`
#>
<int> <int>
#> 1
1
278
#> 2
2
298
#> 3
3
431
#> 4
4
319
#> 5
5
250
#> 6
6
63
#> 7
NA
7
nrow(stats_test)
#> [1] 1646
14
Standardbefehl bedeutet, dass die Funktion zum Standardrepertoire von R gehört, also nicht über
ein Paket extra geladen werden muss.
90
7
Datenjudo
Außerhalb von gruppierten Datensätzen ist nrow() meist praktischer. Praktisch ist
auch der Befehl count(), der nichts anderes ist als die Hintereinanderschaltung von
group_by() und n(). Mit count() zählen wir die Häufigkeiten nach Gruppen;
Gruppen sind hier zumeist die Werte einer auszuzählenden Variablen (oder mehrerer
auszuzählender Variablen). Das macht count() zu einem wichtigen Helfer bei der
Analyse von Häufigkeitsdaten.
dplyr::count(stats_test, interest)
#> # A tibble: 7 x 2
#>
interest
n
#>
<int> <int>
#> 1
1
278
#> 2
2
298
#> 3
3
431
#> 4
4
319
#> 5
5
250
#> 6
6
63
#> 7
NA
7
Probieren Sie auch diese Varianten:
dplyr::count(stats_test, study_time, sort = TRUE)
dplyr::count(stats_test, interest, study_time)
Allgemeiner formuliert lautet die Syntax: count(df, Spalte1, ...), wobei df der
Dataframe ist und Spalte1 die erste auszuzählende Spalte (es können mehrere sein).
Gibt man z. B. zwei Spalten an, so werden pro Wert der 1. Spalte die Häufigkeiten der 2.
Spalte ausgegeben.
Merke: n und count zählen die Anzahl der Zeilen, d. h. die Anzahl der Fälle.
Aufgaben
Richtig oder falsch?15
1. Mit count() kann man Zeilen zählen.
2. count() ist ähnlich (oder identisch) wie eine Kombination von group_by()
und n().
3. Mit count(() kann man nur eine Gruppe beim Zählen berücksichtigen.
4. count) darf nicht bei nominalskalierten Variablen verwendet werden.
5. n() nimmt einen Vektor als Eingabe und liefert einen Skalar zurück.
15
R, R, F, F, R.
7.3 Die Pfeife
91
Aufgaben
1. Bauen Sie sich einen Weg, um den Modus (den häufigsten Wert) mithilfe von
count() und arrange() zu berechnen.16
2. Liefert count(stats_test, bestanden) einen gruppierten oder ungruppierten Datensatz zurück?17
7.3
Die Pfeife
Die zweite zentrale Idee von dplyr kann man salopp als „durchpfeifen“ oder die „Idee
der Pfeife“ bezeichnen; ikonographisch mit einem pfeifenähnlichen Symbol dargestellt
%>% (Prozent-Größer-Prozent). Der Begriff „Durchpfeifen“ ist frei vom Englischen „to
pipe“ übernommen (und stammt aus dem R-Paket magrittr). Das berühmte Bild von
René Magritte stand dabei Pate (s. Abb. 7.10).
Hierbei ist gemeint, einen Datensatz sozusagen auf ein Fließband zu legen und an jedem Arbeitsplatz einen Arbeitsschritt auszuführen. Der springende Punkt ist, dass ein
Dataframe als „Rohstoff“ eingegeben wird und jeder Arbeitsschritt seinerseits wieder
einen Dataframe ausgibt. Damit kann man sehr schön einen „Fluss“ an Verarbeitung erreichen, außerdem spart man sich Tipparbeit, und die Syntax wird lesbarer. Damit das
Durchpfeifen funktioniert, benötigt man Befehle, die als Eingabe einen Dataframe erwarten und wieder einen Dataframe zurückliefern. Das Schaubild verdeutlicht beispielhaft
eine Abfolge des Durchpfeifens (s. Abb. 7.11).
Die sog. „Pfeife“ (pipe: %>%) in Anspielung an das berühmte Bild von René Magritte verkettet Befehle hintereinander. Das ist praktisch, da es die Syntax vereinfacht.
Tipp: In RStudio gibt es einen Shortcut für die Pfeife: Strg-Shift-M (auf allen Betriebssystemen).
Vergleichen Sie mal diese Syntax
filter(summarise(group_by(filter(stats_test, # oh je
!is.na(score)), interest), mw = mean(score)), mw > 30)
16
stats_count <- count(stats_test, score); stats_count_sortiert <arrange(stats_count, -n).
17
count() liefert ungruppierte Datensätze zurück; für count() wird der Datensatz gruppiert
und zum Schluss wieder „ungruppiert“ mit ungroup().
92
7
Datenjudo
Abb. 7.10 Das ist keine Pfeife
mit dieser
stats_test %>%
# oh ja
filter(!is.na(score)) %>%
group_by(interest) %>%
summarise(mw = mean(score)) %>%
filter(mw > 30)
#> # A tibble: 0 x 2
#> # ... with 2 variables: interest <int>, mw <dbl>
Lassen Sie uns die „Pfeifen-Syntax“ in deutschen Pseudo-Code übersetzen:
Nimm die Tabelle „stats_test“ UND DANN
filtere alle nicht-fehlenden Werte UND DANN
gruppiere die verbleibenden Werte nach „interest“ UND DANN
bilde den Mittelwert (pro Gruppe) für „score“ UND DANN
liefere nur die Werte größer als 30 zurück.
Die Pfeife zerlegt die „russische Puppe“, also ineinander verschachtelten Code, in
sequenzielle Schritte, und zwar in der richtigen Reihenfolge (entsprechend der Abarbei-
ID G V1 V2
D
G
Spalten
wählen
G
V1
1 ..
Zeilen
filten
2 ..
V1
1 ..
2 ..
G
G1
G2
Gruppieren
V1
1 ..
3 ..
Zeilen
zählen
4 ..
Visualisieren
Abb. 7.11 Das „Durchpfeifen“
G
n
1 29
2 7
Zeilen
filten
G
n
Sortieren
1 29
2 7
G
n
1 7
2 29
7.4 Spalten berechnen mit mutate()
93
tung). Wir müssen den Code nicht mehr von innen nach außen lesen (wie das bei einer
mathematischen Formel der Fall ist), sondern können wie bei einem Kochrezept „erstens
. . . , zweitens . . . , drittens . . . “ lesen. Die Pfeife macht die Syntax einfacher. Natürlich
hätten wir die verschachtelte Syntax in viele einzelne Befehle zerlegen und jeweils ein
Zwischenergebnis speichern können mit dem Zuweisungspfeil <- und hätten das Zwischenergebnis dann explizit an den nächsten Befehl weitergeben können. Praktisch macht
die Pfeife etwas Ähnliches18 – nur mit weniger Tipparbeit. Und auch einfacher zu lesen.
„UND DANN“ wird mit %>% ins Errische übersetzt. Zu lang sollten Pfeifenketten nicht
werden, sonst wird es zu unübersichtlich (nicht mehr als zehn Zeilen).
Wenn Sie mit der Pfeife Befehle verketten, sind nur Befehle erlaubt, die einen Datensatz als Eingabe verlangen und einen Datensatz ausgeben. Nur die letzte Funktion
einer Pfeifenkette muss keinen Dataframe zurückliefern, da die Pfeife beendet ist.
In der Praxis wird häufig als letztes Glied einer Pfeifenkette eine Funktion gesetzt,
die ein Diagramm ausgibt (s. Kap. 11). So eine „Plotfunktion“ liefert in der Regel
keinen Dataframe zurück; wenn sie das letzte Glied der Pfeifenkette ist, ist das aber
egal. Pfeifen-Aficionados finden im Paket magrittr, aus dem die Pfeife stammt,
weitere Pfeifen-Varianten.
7.4 Spalten berechnen mit mutate()
Wenn man die Pfeife benutzt, ist der Befehl mutate() ganz praktisch: Er berechnet
eine Spalte. Im „Standard-R“ kann man eine Spalte berechnen mit dem Zuweisungsoperator und dem $-Operator. Zum Beispiel so: df$neue_spalte <- df$spalte1 +
df$spalte2. Innerhalb einer Pfeifen-Syntax geht das aber nicht (so gut). Da ist man mit
der Funktion mutate() besser beraten; mutate() leistet just dasselbe wie die gerade
genannte Pseudo-Syntax:
df %>%
mutate(neue_spalte = spalte1 + spalte2)
In Worten:
Nimm die Tabelle „df“ UND DANN
bilde eine neue Spalte mit dem Namen neue_spalte, die sich berechnet als
Summe von spalte1 und spalte2.
18
Etwas formaler ausgedrückt ist df %>% fun(y) identisch mit fun(df, y).
94
7
Abb. 7.12 Sinnbild für mutate
ID N1 N2 N3 MW
ID N1 N2 N3
1
1
2
3
2
1
1
1
3
2
3
4
Datenjudo
Will Durchschnittsnote (MW) pro
Student wissen!
1
1
2
3
2
2
1
1
1
1
3
2
3
4
3
Allerdings berücksichtigt mutate() auch group_by()-Gruppierungen, das ist praktisch. Der Hauptvorteil ist die bessere Lesbarkeit durch das Auflösen der Verschachtelungen. Ein Beispiel:
stats_test %>%
select(bestanden, interest, score) %>%
mutate(Streber = score > 38) %>%
head()
#> # A tibble: 6 x 4
#>
bestanden interest score Streber
#>
<chr>
<int> <dbl> <lgl>
#> 1 ja
4 0.75 FALSE
#> 2 ja
5 0.825 FALSE
#> 3 ja
1 0.789 FALSE
#> 4 ja
5 0.825 FALSE
#> 5 ja
3 0.8
FALSE
#> 6 ja
4 0.8
FALSE
Diese Syntax erzeugt eine neue Spalte innerhalb von stats_test; diese Spalte prüft
pro Person, ob score > 38 ist. Falls ja (TRUE), dann ist Streber TRUE, ansonsten ist
Streber FALSE (tja). head() zeigt die ersten sechs Zeilen des resultierenden Dataframes an. Abb. 7.12 zeigt ein Sinnbild für mutate(); beachten Sie, dass aus einer Spalte
eine neue Spalte erzeugt wird. Man könnte also sagen, die alten Werte werden zu neuen
transformiert.
Betrachten Sie das Sinnbild von mutate(): Die Idee ist, eine Spalte umzuwandeln
nach dem Motto: „Nimm eine Spalte, mach was damit und liefere die neue Spalte
zurück.“ Die Spalte (und damit jeder einzelne Wert in der Spalte) wird verändert
(„mutiert“, daher mutate()). Man kann auch sagen, die Spalte wird transformiert.
7.4 Spalten berechnen mit mutate()
Aufgaben
1. Entschlüsseln Sie dieses Ungetüm. Übersetzen Sie diese Syntax auf Deutsch:
bestanden_gruppen <filter(
summarise(
group_by(filter(select(stats_test,
-c(row_number, date_time)),
bestanden == "ja"), interest),
Punkte = mean(score), n = n()))
2. Entschlüsseln Sie jetzt diese Syntax bzw. übersetzen Sie sie ins Deutsche:
stats_test %>%
select(-row_number, -date_time) %>%
filter(bestanden == "ja") %>%
group_by(interest) %>%
summarise(Punkte = mean(score),
n = n())
3. Sind die beiden gerade dargestellten Syntax-Beispiele synonym in dem Sinne,
dass sie das gleiche Ergebnis liefern?19
4. Die Pfeife im Klausur-Datensatz: Übersetzen Sie die folgende Pseudo-Syntax
ins ERRRische:
Nimm den Datensatz stats_test UND DANN
Wähle daraus die Spalte score UND DANN
Berechne den Mittelwert der Spalte UND DANN
ziehe vom Mittelwert die Spalte ab UND DANN
quadriere die einzelnen Differenzen UND DANN
bilde davon den Mittelwert.
Lösung
stats_test %>%
select(score) %>%
mutate(score_delta = score - mean(.$score)) %>%
mutate(score_delta_squared = score_delta^2) %>%
summarise(score_var = mean(score_delta_squared)) %>%
summarise(sqrt(score_var))
19
JA.
95
96
7
Datenjudo
Was sagt uns der Punkt . in der Syntax? Der Punkt steht für die Tabelle, wie sie
gerade aufbereitet ist (also laut vorheriger Zeile in der Syntax; d. h. unmittelbar
vor %>%). Warum müssen wir dem Befehl mean() sagen, welche Spalte/Variable score wir meinen? Ist doch klar, welche Spalten wir im Auge haben – die
score im aktuellen, durchgepfiffenen Datensatz. R soll sich bitteschön nicht
so dumm stellen. Leider ist der Befehl mean() unbetrübt von solchen Überlegungen. mean() hat keinerlei Idee von Pfeifen, unseren Wünschen und Sorgen.
mean() denkt sich: „Not my job! Sag mir gefälligst wie immer, in welchem
Dataframe ich die Spalte finde!“ Also sagen wir mean(), wo sich die Spalte
befindet . . .
5. Berechnen Sie die Standardabweichung (mit der Funktion sd()) von score in
stats_test.20
6. Vergleichen Sie sie mit dem Ergebnis der vorherigen Aufgabe.21
7. Was hat die Pfeifen-Syntax aus Aufgabe 4 berechnet?22
7.5
Bedingte Analysen mit den Suffixen von dplyr
Alle Hauptverben von dplyr verfügen über eine Reihe von Suffixen:
_if
_all
_at
Einige Beispiele machen den Nutzen klar: summarise_all() fasst alle Spalten zusammen (daher „all“). mutate_at() formt nur bestimmte Variablen um. summarise_if()
fasst nur Spalten zusammen, auf die eine bestimmte Bedingung zutrifft. In einigen Fällen
sind diese Suffixe praktisch; einige Beispiele erläutern das am einfachsten.
7.5.1
Suffix _if
stats_test %>%
summarise_if(is.numeric, mean, na.rm = TRUE)
#> # A tibble: 1 x 5
#>
study_time self_eval interest score row_number
#>
<dbl>
<dbl>
<dbl> <dbl>
<dbl>
#> 1
2.83
5.52
3.09 0.768
1116.
20
sd(stats_test$score).
Das Ergebnis ist fast identisch.
22
Es wurde die Standardabweichung von score berechnet.
21
7.5 Bedingte Analysen mit den Suffixen von dplyr
97
Diese Syntax lässt sich fast wortwörtlich in Pseudocode übersetzen:
Nimm die Tabelle „stats_test“ UND DANN
fasse eine jede Spalte zusammen WENN
die Spalte numerisch ist ACH JA
fasse diese Spalten mithilfe des Mittelwerts zusammen.
Allgemein gesprochen ist die Syntax summarise_if(df, bedingung, fun,
args), wobei df die Tabelle, bedingung die zu prüfende Bedingung ist, fun die für
jede Spalte auszuführen ist, auf die die Bedingung zutrifft. Etwaige weitere Argumente
args für fun werden per Komma angereiht.
7.5.2
Suffix _all
Recht elegant lassen sich mit dem Suffix _all die Anzahl der fehlenden Werte pro Spalte
zusammenfassen.
stats_test %>%
summarise_all(funs(is.na(.) %>% sum))
#> # A tibble: 1 x 7
#>
date_time study_time self_eval interest score row_number bestanden
#>
<int>
<int>
<int>
<int> <int>
<int>
<int>
#> 1
0
7
0
7
12
0
12
Wir übersetzen wieder:
Nimm die Tabelle „stats_test“ UND DANN
fasse jede Spalte zusammen und zwar
mit der Funktion „funs“ und zwar
mit dem Ergebnis dieser zwei Schritte: (Finde alle NAs und summiere sie).
7.5.3
Suffix _at
Beim Suffix _at gibt man Spalten an. Wie immer bei dplyr kann man die Spaltennummer oder den Spaltennamen angeben. Sagen wir, wir möchten den maximalen Wert von
study_time und von self_eval herausfinden. Das könnte so aussehen:
stats_test %>%
select(-date_time) %>%
summarise_at(.vars = vars(study_time, self_eval),
98
7
Datenjudo
.funs = max,
na.rm = TRUE) %>%
head(3)
#> # A tibble: 1 x 2
#>
study_time self_eval
#>
<int>
<int>
#> 1
5
10
Als ersten Parameter (nach dem Dataframe, der implizit über die Pfeife übergeben wird)
erwarten dplyr-Verben mit dem Suffix _at den Parameter .vars, also den Hinweis, welche Variablen wir einbeziehen wollen. Wie immer, wenn wir die Standard-Reihenfolge
beachten, brauchen wir die Parameter nicht zu benennen (wir könnten also .vars =
weglassen). Hingegen brauchen wir vars(), um dplyr zu sagen, von wo bis wo die
Liste der relevanten Spalten geht. Der nächste Parameter ist dann der oder die Befehle, die wir nutzen möchten z. B. max(). Etwaige Parameter von max(), wie na.rm =
TRUE, werden einfach ganz hinten angefügt. Schauen wir uns ein realistischeres Beispiel
an. Sagen wir, wir möchten die Umfrage-Items auf einer Skala von 0 bis 1 standardisieren.
Dazu können wir jeden Wert durch den (theoretischen) Maximalwert der Skala teilen.23
Mit dplyr kann das so aussehen:
stats_test %>%
drop_na() %>%
mutate_at(.vars = vars(study_time, self_eval, interest),
.funs = funs(prop = ./max(.))) %>%
select(contains("_prop"))
#> # A tibble: 1,627 x 3
#>
study_time_prop self_eval_prop interest_prop
#>
<dbl>
<dbl>
<dbl>
#> 1
0.6
0.7
0.667
#> 2
0.8
0.8
0.833
#> 3
0.6
0.4
0.167
#> # ... with 1,624 more rows
Die Funktion funs erzeugt benannte Funktionen; diese Namen werden dann den zu erstellenden Spalten als Suffix angefügt. drop_na() löscht Zeilen mit fehlenden Werten. Man
kann mit drop_na() die Spalten eingrenzen, die beim Suchen fehlender Werte berücksichtigt werden – im Gegensatz zu na.omit(), welches immer alle Spalten berücksichtigt.
Gibt man bei drop_na() keine Spalten an, so werden alle Spalten durchsucht.
Der Punkt . kann hier übersetzt werden mit „alle gewählten Spalten von
stats_test“. Generell bezeichnet der Punkt den Datensatz, so wie er aus dem
letzten Pfeifenschritt hervorging.
23
Genauer bzw. allgemein lautet die Formel: zi D
= (. - 1)/(max(.) - 1).
xi min.x/
; in R-Code für unser Beispiel: prop
max.x/min.x/
7.6 Tabellen zusammenführen (join)
99
7.6 Tabellen zusammenführen (join)
Angenommen, Sie betreiben einen Webshop, in dem Sie R-Devotionalien verkaufen. Alle Verkäufe halten Sie in einer Tabelle fest (Ihre „Verkaufstabelle“), in der u. a. auch die
Kundennummer des Käufers auftaucht. Die Beobachtungseinheit in dieser Tabelle sei ein
Artikel; besteht ein Einkauf aus n Artikeln, so würde die Verkaufstabelle n Zeilen umfassen. Natürlich könnten Sie in dieser Tabelle auch die Informationen des jeweiligen
Käufers (Name, Adresse, Lieblings-Devotionalien) festhalten. Aber effizienter ist es, diese Käufer-Informationen in einer eigenen Tabelle, in der nur die Käufer-Daten aufgeführt
sind, festzuhalten. Andernfalls würde Ihre Verkaufstabelle viele redundante Informationen aufweisen, was Speicher verschwendet. Da R nicht darauf ausgelegt ist, mehrere
unterschiedliche Tabellen gleichzeitig im Blick zu halten, ist es häufig sinnvoll, für Ihre Analyse die Verkaufstabelle mit der Kundentabelle zusammenzuführen. So könnte es
sinnvoll sein, an die Verkaufstabelle einige Spalten aus der Kundentabelle „anzuhängen“,
um zu wissen, welcher Kunde das Produkt gekauft hat bzw. welches Kundenprofil hinter
dem Kauf steht. Dieses „Zusammenfügen“ von Tabellen (die hier wie immer als Dataframes repräsentiert sind), nennt man auch merge oder join. Betrachten wir als Beispiel den
Datensatz flights aus dem Paket ncyflights13. Dort sind Flüge aufgeführt, aber der
Name der Airline ist nur mit einem Kürzel versehen.
flights %>%
select(carrier) %>%
head(3)
#> # A tibble: 3 x 1
#>
carrier
#>
<chr>
#> 1 UA
#> 2 UA
#> 3 AA
Den kompletten Namen der Airline finden wir im Datensatz airlines.
head(airlines, 3)
#> # A tibble: 3 x 2
#>
carrier name
#>
<chr>
<chr>
#> 1 9E
Endeavor Air Inc.
#> 2 AA
American Airlines Inc.
#> 3 AS
Alaska Airlines Inc.
Unser Ziel ist es jetzt, dem Datensatz flights die Spalte name aus dem Datensatz
airlines hinzuzufügen und natürlich so, dass jedes Kürzel dem richtigen Namen zugeordnet wird. Nach diesem Join wird flights also um eine Spalte breiter sein; man
spricht daher auch von einem mutating join. Um diese beiden Datensätze sozusagen zu
verheiraten, müssen wir angeben, anhand welches Kriteriums, d. h. Spalte, wir die Ta-
100
Abb. 7.13 Diese zwei Tabellen sollen zusammengeführt
werden
7
A ID
A
B ID
B
Datenjudo
C
D
id_01 a
1
id_01 x
10
id_02 b
2
id_02 y
20
id_03 c
3
id_04 z
30
bellen aufeinander beziehen. In diesem Fall ist das Vereinigungskriterium (MatchingKriterium) das Kürzel der Airline (carrier). Man beachte, dass diese Spalte in beiden
Tabellen vorkommen muss.
flights %>%
inner_join(airlines, by = "carrier") -> flights_joined
head(flights_joined$name)
#> [1] "United Air Lines Inc." "United Air Lines Inc."
#> [3] "American Airlines Inc." "JetBlue Airways"
#> [5] "Delta Air Lines Inc."
"United Air Lines Inc."
Wichtig ist noch hinzuzufügen, dass bei inner_join() Zeilen, die keinen (Hochzeits-)
Partner in der anderen Tabellen fanden, nicht übernommen, also fallengelassen werden.
Das macht inner_join() gefährlich; man muss aufpassen, dass man keine Daten verliert.
Bei einem inner join werden nur Zeilen behalten, die in beiden Tabellen vorkommen. Die resultierende Tabelle ist also nie länger, höchstens kürzer als eine oder
beide Ausgangstabellen.
Allgemein kann man die Syntax von inner_join() so schreiben: x %>% inner_
join(y, by = "key"). Dabei ist x die „linke“ Tabelle, y die „rechte“ Tabelle und mit
key wird die Spalte benannt, die das Matching-Kriterium enthält. Würde die MatchingSpalte in X key_X heißen und in Y key_Y, so würde man schreiben: inner_join(y,
by = ("key_X", "key_Y")). Für andere Join-Arten ist die Syntax analog.
Ein anderer Join, der häufig verwendet wird, ist left_join(). Ein left_join()
behält alle Beobachtungen von X, auch wenn es kein Match mit Y gibt. Hingegen
werden Beobachtungen aus Y entfernt, falls sie keine Entsprechung in X haben. Bei
right_join() gilt Ähnliches für Y (die „rechte“ Tabelle). Bei full_join() hingegen werden alle Beobachtungen sowohl aus X als auch Y behalten, auch wenn sie keine
Entsprechung in der jeweils anderen Tabelle haben.
Abb. 7.13 zeigt zwei Tabellen, die zusammengeführt werden sollen. Einige verschiedene Arten von Joins sind in Abb. 7.14 wiedergegeben.
7.6 Tabellen zusammenführen (join)
left_join
ID
A
B
C
1x
id_01 a
2y
id_02 b
101
D
A
B
C
id_01 a
1x
10
20
id_02 b
2y
20
id_03 c
3 NA NA
id_04 NA NA z
inner_join ID
A
B
C
D
10
3 NA NA
id_03 c
fulll_join ID
D
id_01 a
1x
10
id_02 b
2y
20
anti_join ID
A
id_03 c
30
B
3
Abb. 7.14 Einige Arten von Joins
Aufgaben
Richtig oder falsch?24
1. Möchte man mit dplyr eine Spalte zu einem Wert zusammenfassen, so nutzt
man die Funktion mutate().
2. Die Befehlskette „Datensatz nehmen -> Spalten auswählen -> Filtern ->
Gruppieren -> Mittelwert berechnen“ könnte man mit dplyr mit
Pseudo-Syntax so darstellen: df %>% select(col1, col2) %>%
filter(cond == TRUE) %>% group_by(my_groups) %>%
summarise(mean(score)).
3. Mit der Funktion na.rm(df) kann man alle Zeilen aus df entfernen, die feh-
4.
5.
6.
7.
24
lende Werte aufweisen.
Möchte man Tabellen „nebeneinander kleben“, also die gemeinsamen Zeilen
zweier Tabellen zusammenführen, so bietet sich der Befehl join (in einigen
Varianten) an. Mit left_join() werden nur Zeilen behalten, die im zweiten
Datensatz liegen (sofern sie eine Entsprechung im anderen Datensatz haben).
Möchte man mehrere Spalten zu ihrem Mittelwert zusammenfassen, so ist dieser
Befehl hilfreich: summarise_at().
Möchte man von allen numerischen Variablen den Mittelwert wissen, so ist dies
in dplyr mit summarise_if(is.numeric, mean, na.rm = TRUE) umsetzbar.
Der Befehl tidyverse_packages(include_self = TRUE) liefert eine
Aufstellung aller Pakete des Tidyverse.
F, R, F, R, F, R, R, R.
8
Deskriptive Statistik
Lernziele
Die grundlegende Idee der deskriptiven Statistik erläutern können
Die beiden zentralen Arten von Kennwerten der deskriptiven Statistik kennen
Typische Lage- und Streuungsmaße benennen und erläutern können
Deskriptive Statistiken mit mosaic und dplyr berechnen können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(mosaic)
library(tidyverse)
library(skimr)
library(lsr)
library(corrr)
library(GGally)
library(sjmisc)
data(stats_test, package = "pradadata")
Wie in Kap. 1 dargestellt, unterteilt man Statistik oft in zwei Bereiche: Deskriptiv- und
Inferenzstatistik. Die Aufgabe der Deskriptivstatistik ist es, Daten prägnant zusammenzufassen. Oft wird dabei ein Spalten- oder Zeilenvektor einer Tabelle zu einem einzelnen Wert (Skalar oder Statistik) zusammengefasst; typisches Beispiel ist der Mittelwert
(s. Abb. 1.2); man spricht dann von univariater Statistik. Es kann aber auch sein, dass
man mehrere Vektoren zu einer Zahl zusammenfasst, wie bei der Korrelation.1 In dem
1
Gibt es auch Situationen, in denen man einen Vektor in mehreren Kennwerten zusammenfassen
möchte?
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_8
103
104
8
Deskriptive Statistik
Tab. 8.1 Typische Skalenniveaus
Skalenniveau
Nominal (kategorial)
Ordinal (Rangfolge)
Metrisch (numerisch)
Beispiel
Augenfarbe, Geschlecht, Branche
Lieblingsessen, Rankings
Größe, Gewicht, Einkommen
Zulässige Statistiken (Auswahl)
Häufigkeiten
Median, IQR, Minimum, Maximum
Mittelwert, Varianz
Fall spricht man von bi- bzw. multivariaten Statistiken. Hinweise zur deskriptiven Statistik finden sich in vielen Statistikbüchern (Diez et al. 2015). Stark an R orientiert ist die
Einführung bei Wickham und Grolemund (2017). Dieser Abschnitt konzentriert sich nicht
auf konzeptionelle Punkte, sondern auf die Anwendung mit R.2
Weiter kann man Statistiken danach unterscheiden, für welche Arten von Daten sie
geeignet sind. So ist es nicht sinnvoll, für den Vektor „Geschlecht“ den Mittelwert auszurechnen; die Art der Variable – das Skalenniveau – passt nicht zur Statistik Mittelwert.
Gängige Arten von Skalenniveaus sind in Tab. 8.1 aufgeführt (vgl. Eid et al. (2010) für
eine ausführlichere Darstellung).
8.1 Univariate Statistik
Ein zentraler Gedanke der Deskriptivstatistik ist es, dass es beim Zusammenfassen von
Daten nicht reicht, sich auf den Mittelwert oder eine (hoffentlich) „repräsentative“ Zahl zu
verlassen. Man braucht auch einen Hinweis, wie unterschiedlich die Daten sind; wie sehr
sie streuen. Entsprechend spricht man von zwei Hauptbereichen der deskriptiven Statistik:
Lagemaße (der repräsentative, typische Wert) und Streuungsmaße (wie sehr sich die Werte
ähneln bzw. wie unterschiedlich die einzelnen Werte sind).
Aufgabe der deskriptiven Statistik ist es primär, Daten prägnant zusammenzufassen.
Die univariate deskriptive Statistik hat zwei Hauptbereiche: Lagemaße und Streuungsmaße.
Lagemaße geben den „typischen“, „mittleren“ oder „repräsentativen“ Vertreter der Verteilung an. Bei den Lagemaßen denkt man sofort an das arithmetische Mittel (synonym:
Mittelwert; häufig als XN abgekürzt; mean()). Ein Nachteil von Mittelwerten ist, dass
sie nicht robust gegenüber Extremwerten sind: Schon ein vergleichsweise großer Einzelwert kann den Mittelwert stark verändern und damit die Repräsentativität des Mittelwerts
2
Eine interessante Ergänzung und Alternative ist das Programm Exploratory, https://exploratory.
io/, welches eine „klickbare“ Oberfläche für explorative Datenanalyse bietet. Die Software baut auf
R auf, gibt die R-Syntax aus und ist eng an das Tidyverse angelehnt. Ähnliches gilt für die Software
jamovi, https://www.jamovi.org/ und. Beide sind modern und aufgeräumt im Erscheinungsbild.
8.1 Univariate Statistik
105
5
note
4
3
MW = 2.33
2
1
1
2
3
4
id
Richtung der Abweichung
negativ
positiv
Abb. 8.1 Abweichungsbalken und Abweichungsquadrate
für die Gesamtmenge der Daten in Frage stellen. Eine robuste Variante ist der Median
(Md; median()). Ist die Anzahl der (unterschiedlichen) Ausprägungen nicht zu groß
im Verhältnis zur Fallzahl, so ist der Modus eine sinnvolle Statistik; er gibt die häufigste
Ausprägung an.3
MittelwerteP berechnen sich als die Summe aller Elemente xi , geteilt durch deren Anzahl n: XN D nxi . In Abb. 8.1 ist ein Mittelwert der Noten von vier Studenten dargestellt;
der Mittelwert beträgt 2.33. Die vertikalen Balken zeigen die individuelle Abweichung
vom Mittelwert; so ist die Note von Student 1 relativ nah am Mittelwert (und etwas besser,
also geringer). Da einige Studenten besser als der Mittelwert sind und andere schlechter,
haben einige Abweichungswerte ein positives Vorzeichen und andere ein negatives. In
Abb. 8.1 gehen entsprechend einige Balken vom Mittelwert nach unten und andere vom
Mittelwert nach oben. Der Mittelwert hat die Eigenschaft, dass die Summe der AbweiP
chungen di null ergibt: di D 0. Anders gesagt ist die Summe der Abweichungen mit
positivem Vorzeichen gleich groß wie die Summe der Abweichungen mit negativem Vorzeichen. Anschaulich gesprochen: Die Summe der Abweichungsbalken, die nach oben
gehen, ist gleich lang wie die Summe der Abweichungsbalken, die vom Mittelwert aus
nach unten zeigen.
Streuungsmaße geben die Unterschiedlichkeit in den Daten wieder; mit anderen Worten:
Sind die Daten einander ähnlich oder unterscheiden sich die Werte deutlich? Zentrale Statistiken sind der mittlere Absolutabstand (MAA; engl. mean absolute deviation, MAD)4 ,
die Standardabweichung (SD sd()), die Varianz (Var; var()) und der Interquartilsab3
Der Modus ist im Standard-R nicht mit einem eigenen Befehl vertreten. Man kann ihn aber leicht
von Hand bestimmen, z. B. mit count(df, x) %>% arrange(-n) %>% top_n(1), wobei x die relevante Variable und df einen Dataframe darstellt. Es gibt auch einige Pakete, die diese
Funktion anbieten: z. B. modes::modes().
4
Der MAD ist im Standard-R nicht mit einem eigenen Befehl vertreten. Es gibt einige Pakete, die
diese Funktion anbieten: z. B. lsr::aad (absolute average deviation from the mean).
106
8
200
150
Größe
Q2
Q1
Deskriptive Statistik
Q3
Viertel
[150,165]
(165,180]
100
(180,195]
50
(195,210]
0
0
25
50
75
100
Nummer der Person
Abb. 8.2 Verdeutlichung der Quartile
stand (IQR; IQR()). MAA, Varianz und SD sind konzeptionell verwandt und berechnen
P
sich analog. Der MAA ist definiert als mittlere Abweichung d über alle i: MAA D ndi .
Der MAA entspricht dem mittleren Abweichungsbalken in Abb. 8.1 (MAA D 0:57). Dabei wird das Vorzeichen nicht berücksichtigt, sonst ergäbe die Summe der Abweichungsbalken null.
Die Varianz ist definiert als der Mittelwert der quadrierten Abweichungen di :
P 2
di
Var D n . Das entspricht dem mittleren „Abweichungsquadrat“ in Abb. 8.1 (Var D
0:38). Die Varianz hat einige nützliche statistische Eigenschaften; allerdings ist sie nicht
sehr anschaulich. Durch die Quadrierung der Abweichungswerte resultieren andere (größere) Werte; zieht man die Wurzel, so resultieren Zahlen mitpeinem ähnlichen Wertebereich
wie die Rohwerte. Entsprechend ist die SD definiert: SD D Var. Anschaulich gesprochen
gleicht die SD der Seitenlänge des mittleren Abweichungsquadrats.
Quartile und, allgemeiner, Quantile sind Lagemaße, aber aus ihnen können auch Informationen zur Streuung abgeleitet werden. Was sind Quartile und, daraus abgeleitet,
der Interquartilsabstand? Dazu ein Beispiel: Ein kauziger Professor überredet seine 100
Studenten, sich der Größe nach im Vorlesungssaal aufzustellen. Der Professor läuft gemessenen Schrittes die Reihe der 100 Studenten ab, beginnend beim kleinsten (1.50 m).
Als er bei Person 25 ankommt (ca. 1.65 m), ruft er dieser Person zu: „Sie sind Q1!“ Damit
meint er, dass ca. 25 % (ein Viertel) der 100 Studenten kleiner sind als diese Person (und
drei Viertel größer). Dann läuft er weiter; schließlich kommt er bei der 50. Person an (ca.
1.80 m). Das Spiel wiederholt sich: „Sie sind Q2“: Zwei Viertel der Studenten sind kleiner
als diese Person. Bei Person 75: „Sie sind Q3!“ Abb. 8.2 verdeutlicht die Aufstellung des
Professors. Also:
Das erste Quartil (Q1) kennzeichnet den Wert, an dem 25 % der Werte kleiner und
entsprechend 75 % der Werte größer sind.
Das zweite Quartil (Q2) kennzeichnet den Wert, an dem 50 % der Werte kleiner und
entsprechend 50 % der Werte größer sind; diesen Wert nennt man auch Median.
Das dritte Quartil (Q3) kennzeichnet den Wert, an dem 75 % der Werte kleiner und
entsprechend 25 % der Werte größer sind.
8.1 Univariate Statistik
107
Der kauzige Professor hätte die Studentengruppe auch in Drittel aufteilen können anstelle
von Vierteln. Er hätte sie auch in 100 Prozentstufen aufteilen können. Damit sind wir
bei Quantilen: Das 50 %-Quantil ist der Wert, der von 50 % der Studenten (bzw. ihrer
Körpergrößen) nicht überschritten wird (z. B. 1.80 m). Allgemeiner: Das p-Quantil ist der
Wert, der von p der Fälle nicht überschritten wird.
Der IQR ist definiert als die Differenz von Q3 und Q1: IQR D Q3Q1. Da nur der IQR
nicht auf dem Mittelwert basiert und nicht die extremsten Werte des Vektors einbezieht,
ist er robuster als Statistiken, die sich aus dem Mittelwert ergeben oder in die alle Werte
des Vektors bzw. der Verteilung einfließen.
8.1.1 Deskriptive Statistik mit mosaic
Das R-Paket mosaic wurde dafür entwickelt, den Einstieg in die Datenanalyse zu erleichtern. Eine zentrale Idee ist, die Syntax von R-Befehlen anzugleichen. Das ist praktisch,
da viele R-Befehle unterschiedliche Syntax aufweisen, da sie von vielen verschiedenen
Menschen entwickelt wird. Die Stärke von R – Reichtum an Funktionsvielfalt – bringt
daher leider Komplexität mit sich. „Wie war noch mal die Syntax von diesem Befehl?“,
fragt man sich allzu oft. Ist der Datensatz für diesen Befehl mit dem Argument data
oder mit x oder mit df anzusprechen? Da liegt es nahe (im Nachhinein betrachtet), eine
Syntax zu entwickeln, die für einen Kreis von ähnlichen Befehlen ähnlich funktioniert.
mosaic leistet das für Deskriptiv- und Inferenzstatistik; auch für Datenvisualisierung
stellt es Funktionen bereit. Allerdings nicht für grundlegende Datenaufbereitung wie in
Abschn. 7.2 geschildert. Für Datenjudo allgemeiner betrachtet bietet dplyr einen ähnlichen Ansatz, allerdings ist mosaic für Einsteiger vielleicht einfacher. Für prädiktive
Modellierung leistet das R-Paket caret eine ähnliche „Syntax-Schablone“ (s. Kap. 22).
Die Syntax-Schablone von mosaic sieht in einfacher Form so aus:
verfahren(zielvariable ~ gruppierungsvariable,
data = meine_tabelle)
Auf Deutsch liest sich das so:
Berechne das statistische Verfahren mit dem Namen verfahren,
wobei die Zielvariable y heißt und die Gruppierungsvariable
bzw. die zweite relevante Variable x ist. Der Datensatz,
in dem sich diese Variablen finden, heißt meine_tabelle.
Das Tolle ist, dass es reicht, diese Syntax zu kennen, um viele Verfahren der Statistik
nutzen zu können – der Aufbau der Syntax entspricht jedes Mal dieser Blaupause. Die
Schreibweise mit der Tilde (dem Kringel ~) nennt man in R Formelschreibweise (formula
interface). Die Formelschreibweise wird für gängige Modellierungsverfahren der Statis-
108
8
Deskriptive Statistik
tik wie t-Test oder Varianzanalyse (s. Abschn. 16.3) verwendet. Es führt kein Weg daran
vorbeiEs lohnt sich, sich mit der Formelschreibweise vertraut zu machen. Mit y meint
man meist die Ziel- oder Ergebnisvariable; x ist oft eine Gruppierungsvariable oder eine
Einflussgröße. Ein sehr praktischer Befehl ist favstats(). Er berechnet Ihre Lieblingsstatistiken; ja, auch Ihre. Schauen Sie:
favstats(interest ~ bestanden, na.rm = TRUE, data = stats_test)
#>
bestanden min Q1 median Q3 max mean
sd
n missing
#> 1
ja
1 2
3 4
6 3.31 1.42 1030
3
#> 2
nein
1 2
3 4
6 2.73 1.33 598
3
Hier haben wir typische Statistiken für die Variable interest berechnet, aufgeteilt nach
zwei Gruppen (bestanden vs. nicht bestanden). Natürlich geht es mit anderen Verfahren
analog, wie mean(), cor() und sd(), die ebenfalls in mosaic enthalten sind. Mit
tally() kann man sich Häufigkeiten auszählen lassen.
8.1.2 Deskriptive Statistik mit dplyr
dplyr kann man gut gebrauchen, um deskriptive Statistik zu berechnen. Dabei charakterisiert summarise() eine Hauptidee der Deskriptivstatistik: einen Vektor zu einer Zahl
zusammenzufassen. group_by() steht für die Idee, „Zahlensäcke“ (Verteilungen bzw.
Stichprobendaten) in Subgruppen aufzuteilen. mutate() transformiert Daten. n() zählt
Häufigkeiten.
stats_test <- drop_na(stats_test, score)
summarise(stats_test,
mean(score), sd(score), aad(score))
#> # A tibble: 1 x 3
#>
`mean(score)` `sd(score)` `aad(score)`
#>
<dbl>
<dbl>
<dbl>
#> 1
0.768
0.149
0.126
# 'aad' aus Paket 'lsr'
Viele R-Befehle der deskriptiven Statistik sind im Standard so eingestellt, dass sie
NA zurückliefern, falls es in den Daten fehlende Werte gibt. Das ist einerseits informativ, andererseits aber oft unnötig. Mit dem Parameter na.rm = TRUE kann man
dieses Verhalten abstellen.
Tipp: Mit dem Befehl df <- drop_na(df) entfernen Sie alle fehlenden Werte aus df. Der Befehl stammt aus tidyr, welches automatisch geladen wird, wenn
Sie library(tidyverse) ausführen. Mit drop_na() kann man angeben, aus
welchen Spalten die fehlenden Werte gelöscht werden sollen. Das ist praktisch,
wenn es eine Spalte mit fehlenden Werten gibt, die aber für die vorliegende Analyse
nicht so wichtig ist.
8.1 Univariate Statistik
109
summarise() liefert im Unterschied zu mean() etc. immer einen Dataframe zurück.
Da der Dataframe die typische Datenstruktur ist, ist es häufig praktisch, wenn man einen
Dataframe zurückbekommt, mit dem man weiterarbeiten kann. Die dplyr-Verben erlauben
das; mean() und Co. nicht. Außerdem lassen mean() etc. keine Gruppierungsoperationen zu; über group_by() kann man dies aber bei dplyr erreichen.
Möchte man die „üblichen Verdächtigen“ an deskriptiven Statistiken mit einem Befehl
bekommen, so ist der Befehl skimr::skim_to_wide() hilfreich. Mit skim kann man
in gewohnter Weise Subgruppen vergleichen:
stats_test %>%
filter(!is.na(bestanden)) %>%
group_by(bestanden) %>%
skim_to_wide()
dplyr::group_by(iris) %>% skim() -> df
Eine Alternative zu skimr::skim() bietet mosaic::favstats() für einzelne Spalten und mosaic::inspect() für ganze Dataframes:
favstats(~score, data = stats_test)
inspect(stats_test)
8.1.3 Relative Häufigkeiten
Manchmal ist es praktisch, nicht nur die (absoluten) Häufigkeiten von Zeilen zu zählen,
sondern auch ihren Anteil (nach relativer Häufigkeit). Klassisches Beispiel: Wie viel Prozent der Fälle im Datensatz sind Frauen, wie viele sind Männer? Mit mosaic gibt es eine
einfache Lösung: tally(); dabei ergänzt man das Argument format = proportion,
um sich relative statt absolute Häufigkeiten ausgeben zu lassen:
tally(~bestanden, data = stats_test, format = "proportion")
#> bestanden
#>
ja nein
#> 0.632 0.368
Man kann auch (relative) Häufigkeiten für mehr als eine Variable aufgliedern:
stats_test %>%
drop_na() %>%
filter(interest %in% c(1, 6)) %>% # nur die wenig und stark Interessierten
tally(bestanden ~ interest, data = ., format = "proportion")
#>
interest
#> bestanden
1
6
#>
ja
0.484 0.857
#>
nein 0.516 0.143
110
8
Deskriptive Statistik
Wie man sieht, unterscheiden sich die Gering- von den Hochinteressierten in der Bestehensquote. Möchte man die Differenz der Bestehensanteile wissen, so bietet mosaic eine
komfortable Lösung:
stats_test %>%
drop_na() %>%
filter(interest %in% c(1, 6)) %>% # nur die wenig und stark Interessierten
diffprop(bestanden ~ interest, data = ., format = "proportion")
#> diffprop
#>
0.374
diffprop() gibt den Unterschied der Anteile zwischen den Spalten wieder. Der An-
teilsunterschied ist bereits eine bivariate Statistik, da zwei Variablen zu einer Zahl zusammengefasst werden. Praktisch ist auch das Paket sjmisc, das eine Reihe von dplyrfreundlichen Befehlen hat, z. B. frq (frequency):
stats_test %>%
group_by(bestanden) %>%
frq(study_time)
frq() gibt für jede der übergebenen Variablen absolute, relative und kumulierte Häufigkeiten aus. Gruppierungen mit group_by() werden berücksichtigt. Allerdings
werden nur univariate Häufigkeiten ausgegeben. Möchte man eine bi- oder multivariate Häufigkeitsverteilung aufschlüsseln (eine Kontingenztabelle berechnen), so kann
man flat_table() nutzen:
stats_test %>%
flat_table(bestanden, study_time, margin = "row")
#>
study_time
1
2
3
4
5
#> bestanden
#> ja
9.32 16.21 33.98 29.42 11.07
#> nein
29.26 27.26 31.77 9.87 1.84
Mit dem Parameter margin kann man verschiedene Arten von relativen Häufigkeiten
anfordern. Mit row wird die Randhäufigkeit pro Reihe berechnet; mit col pro Spalte und
mit cell wird über alle Zellen hinweg aufaddiert. In der Voreinstellung wird die absolute
Häufigkeit berechnet.
Mit dplyr pur ist die Ausgabe von relativen Häufigkeiten aufwändiger, da es keinen
fertigen Befehl gibt:
stats_test %>%
count(interest) %>%
mutate(interest_prop = n / sum(n))
#> # A tibble: 7 x 3
#>
interest
n interest_prop
#>
<int> <int>
<dbl>
8.1 Univariate Statistik
#>
#>
#>
#>
#>
#>
#>
1
2
3
4
5
6
7
1
2
3
4
5
6
NA
276
296
425
318
250
63
6
111
0.169
0.181
0.260
0.195
0.153
0.0386
0.00367
prop steht hier für „Proportion“, also Anteil. sum(n) liefert die Summe der Fälle zurück.
Etwas komplexer ist es, wenn man zwei Gruppierungsvariablen hat und dann Anteile auszählen möchte:
stats_test %>%
drop_na(interest, bestanden) %>%
mutate(bestanden = score > .7) %>%
group_by(interest, bestanden) %>%
summarise(n = n()) %>%
mutate(interest_prop = n / sum(n)) %>%
head()
#> # A tibble: 6 x 4
#> # Groups:
interest [3]
#>
interest bestanden
n interest_prop
#>
<int> <lgl>
<int>
<dbl>
#> 1
1 FALSE
142
0.514
#> 2
1 TRUE
134
0.486
#> 3
2 FALSE
124
0.419
#> 4
2 TRUE
172
0.581
#> 5
3 FALSE
158
0.372
#> 6
3 TRUE
267
0.628
Synonym zur letzten Syntax könnte man auch schreiben:
stats_test %>%
count(interest, bestanden) %>%
mutate(interest_prop = n / sum(n))
In dem Beispiel wird als „Ganzes“ der komplette Datensatz angesehen: Die Anteile werden relativ zur ganzen Stichprobe berechnet; 100 % entsprechen 1634. Manchmal ist man
aber an Fragen interessiert wie: „Wie viel Prozent der Nicht-Interessierten haben bestanden?“ Dann ist die Bezugsgröße, die 100 %, die Menge der Studenten, die bestanden
haben. Wie kann man dplyr beibringen, was 100 % sein sollen? Dafür gruppiert man den
Datensatz vor der Berechnung der Anteile; jede Gruppe wird dann als „Ganzes“, als 100 %
betrachtet:
stats_test %>%
count(interest, bestanden) %>%
group_by(interest) %>%
112
8
Deskriptive Statistik
mutate(interest_prop = n / sum(n)) %>%
head(2)
#> # A tibble: 2 x 4
#> # Groups:
interest [1]
#>
interest bestanden
n interest_prop
#>
<int> <chr>
<int>
<dbl>
#> 1
1 ja
134
0.486
#> 2
1 nein
142
0.514
8.2 Korrelationen berechnen
Die Korrelation nach Pearson misst die Stärke und Richtung des linearen Zusammenhangs
zweier metrischer Variablen (vgl. Eid et al. (2010) für eine ausführliche Darstellung). Zwei
Variablen X und Y sind (positiv) korreliert, wenn hohe Werte in X mit hohen Werten in Y
einhergehen und umgekehrt. Gehen hohe Werte in X mit geringen Werte in Y einher, so
sind X und Y negativ korreliert. Der Wertebereich der Korrelationskoeffizienten reicht von
1 über 0 bis C1. Ein Wert von 0 zeigt keine (null) Korrelation an; je näher der Wert gegen
C1 bzw. 1 geht, desto stärker ist die Korrelation (in positiver bzw. negativer Richtung).
Abb. 8.3 stellt einige Beispiele für Korrelationen verschiedener Stärke dar. Je enger sich
die Punktewolke an die Regressionsgerade (eine Gerade in der Mitte der Punktewolke)
anschmiegt, desto größer ist der Absolutwert der Korrelation.
Die Korrelation nach Pearson wird häufig mit dem Buchstaben r (für Stichproben)
(und für Populationen) bezeichnet. Sie berechnet sich als durchschnittliches
AbweiP
zxi zxi
. Anschaulich
chungsquadrat der z-transformierten Rohwerte zxi und zyi : r D
n
gesprochen berechnet man für jeden Fall der Stichprobe das Abweichungsquadrat, das
-0.99
-0.4
0
Abb. 8.3 Beispiele für Korrelationskoeffizienten verschiedener Stärke und Richtung
0.8
8.2 Korrelationen berechnen
113
MW
45
Schuhgröße
dxi
dyi
MW
40
160
170
180
190
200
Körpergröße
MW: Mittelwert
Abb. 8.4 Sinnbild zur Berechnung der Korrelation
sich vom Mittelwert jeder der beiden Variablen X und Y zum jeweiligen X- bzw. YWert aufspannt (s. Abb. 8.4). Dann berechnet man aus diesen Abweichungsrechtecken
das mittlere
Rechteck; die Fläche dieses Rechtecks entspricht dem Wert der Korrelation:
P
r D .dxni dyi / .
Korrelationen bzw. Korrelationstabellen lassen sich mit dem R-Standardbefehl cor()
berechnen:
stats_test %>%
select(study_time, self_eval, score) %>%
cor()
#>
study_time self_eval score
#> study_time
1
NA
NA
#> self_eval
NA
1.000 0.617
#> score
NA
0.617 1.000
Oh! Lauter NAs! Besser, wir löschen Zeilen mit fehlenden Werten, bevor wir die Korrelation ausrechnen. Mit dem Argument method, das im Standard auf pearson gesetzt ist,
kann man auch die Koeffizienten von spearman und kendall anfordern:
stats_test %>%
select(study_time, self_eval, score) %>%
drop_na() %>%
cor(method = "spearman")
#>
study_time self_eval score
#> study_time
1.000
0.545 0.431
#> self_eval
0.545
1.000 0.632
#> score
0.431
0.632 1.000
114
8
Deskriptive Statistik
Alternativ zu cor() kann man auch corrr:correlate() verwenden:
stats_test %>%
select(study_time, self_eval, score) %>%
correlate(method = "kendall")
#> # A tibble: 3 x 4
#>
rowname
study_time self_eval score
#>
<chr>
<dbl>
<dbl> <dbl>
#> 1 study_time
NA
0.451 0.336
#> 2 self_eval
0.451
NA
0.489
#> 3 score
0.336
0.489 NA
correlate() hat den Vorteil, dass es bei fehlenden Werten einen Wert ausgibt; die
Korrelation wird paarweise mit den verfügbaren (nicht-fehlenden) Werten berechnet. Außerdem wird ein Dataframe (genauer: tibble) zurückgeliefert, was häufig praktischer ist
zur Weiterverarbeitung. Wir könnten jetzt die resultierende Korrelationstabelle plotten,
vorher „rasieren“ wir noch mit shave()das redundante obere Dreieck ab (da Korrelationstabellen symmetrisch sind). Mit corrr::rplot() kann man die resultierende
Korrelationstabelle visualisieren.
stats_test %>%
select(study_time, self_eval, score, interest) %>%
correlate() %>%
shave() %>%
rplot()
Aufgaben
Richtig oder falsch?5
1.
2.
3.
4.
5.
6.
5
Die „normale“ Korrelation (nach Pearson) setzt ein metrisches Skalenniveau
voraus.
cor() führt in der Voreinstellung die Korrelation nach Pearson aus.
Liegen fehlende Werte vor, so liefert cor() in der Voreinstellung NA zurück.
Möchte man trotz fehlender Werte einen Wert für die Korrelation ausgegeben
bekommen, so fügt man bei cor() das Argument na.rm = TRUE hinzu.
Statistiken, die mehrere Vektoren zu einer Zahl zusammenfassen, bezeichnet
man als multivariat.
Die Varianz ist definiert als Quadratwurzel des mittleren Abweichungsquadrats.
R, R, R, F, F, F, F, R, R.
8.2 Korrelationen berechnen
115
7.
8.
9.
Die Abweichungen zum Mittelwert summieren sich zu 1 auf.
Der Unterschied in zwei Anteilen ist ein Beispiel für eine bivariate Statistik.
Für positive Korrelationen zweier Variablen X und Y gilt: Je größer X, desto
kleiner Y.
10. Ist die Korrelation null, so ist die Trend- bzw. Regressionsgerade parallel zur
X-Achse.
Aufgaben
1. Kann man sich mit mosaic::tally() die Häufigkeitsverteilung von drei Variablen ausgeben lassen? Wenn ja, wie?6
2. Probieren Sie diese Befehle von mosaic aus: sd(), median(), IQR().
3. Führen Sie diesen Befehl aus inspect(stats_test). Erläutern Sie die
Funktionsweise des Befehls. Nicht vergessen, dass mosaic geladen sein muss,
damit Sie diese Befehle nutzen können.
4. Wie kann man sich deskriptive Statistiken wie mean() mit dem Standard-R
ausgeben lassen?7
5. Ist das Berechnen deskriptiver Statistiken mittels Standard-R einfacher als mittels dplyr?8
6. Was ist der Unterschied zwischen na.omit() und drop_na()?9
7. Wird dplyr automatisch geladen, wenn man library(tidyverse) ausführt?10
6
Ja: tally(bestanden ~ interest + self_eval, data = drop_na(stats_test).
Zum Beispiel so: mean(stats_test$score, na.rm = TRUE).
8
Vielleicht ist das Geschmackssache; dplyr vereinheitlicht die Syntax und führt viele Operationen
auf eine Grundmenge an Bausteinen zurück, was in einigen Situationen hilfreich ist.
9
drop_na() bezieht sich nur auf Dataframes; na.omit() kann auch Vektoren verarbeiten.
drop_na() fügt sich in dplyr-Syntax; so können Variablennamen ohne Anführungsstriche übergeben werden.
10
Ja; lädt man tidyverse, so wird angezeigt, welche Pakete geladen werden. Mit
tidyverse_packages() kann man sich explizit aufzeigen lassen, welche Pakete geladen werden. Um genau zu sein, werden die Pakete nicht „geladen“, sondern an den Suchpfad angehängt;
vgl. Wickham (2014).
7
116
8
Deskriptive Statistik
Aufgaben
Zum Datensatz stats_test:
1. Zählen Sie mit Hilfe von mosaic aus, wie viele Studenten bestanden haben in
Abhängigkeit von ihrem Interesse am Fach.11
2. Lassen Sie sich das Ergebnis als relative Häufigkeit ausgeben.12
3. Sehen Sie in der Hilfe nach, wie man das Ergebnis nicht als Anteil, sondern als
Prozentzahl ausgegeben bekommt.13
4. Wie viel Prozent der Studenten, die bestanden haben, haben sehr viel gelernt?14
5. Kann man – und wenn ja, wie – mit favstats() und inspect() auch Subgruppenanalysen vornehmen?15
11
tally(interest ~ bestanden, data = stats_test).
tally(interest ~ bestanden, data = stats_test, format =
"proportion").
13
?tally; das Argument lautet format = "percent".
14
stats_test %>% count(study_time, bestanden) %>%
group_by(bestanden) %>% mutate(prop = n / sum(n).
15
favstats(score ~ interest, data = stats_test); inspect() bietet die
Möglichkeit nicht.
12
9
Praxisprobleme der Datenaufbereitung
Lernziele
Typische Probleme der Datenaufbereitung kennen und bearbeiten können
Grundlegende Herangehensweisen im Umgang mit fehlenden Werte kennen
Datenanomalien aufspüren können
Daten umformen können (z. B. partitionieren)
Wissen, was man unter der Normalform eines Dataframes versteht
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(corrr)
library(car)
library(pradadata)
library(tidyverse)
library(mice)
library(VIM)
library(skimr)
library(ggcorrplot)
library(sjmisc)
data(extra, package = "pradadata")
data(stats_test, package = "pradadata")
Stellen wir einige typische Probleme des Datenjudo (genauer: der Datenaufbereitung)
zusammen. Probleme heißt hier nicht, dass etwas Schlimmes passiert ist, sondern es ist
gemeint, wir schauen uns ein paar typische Aufgabenstellungen an, die im Rahmen der
Datenaufbereitung häufig anfallen. Es ist eine alte Weisheit der Datenanalyse, dass ein
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_9
117
118
9
Praxisprobleme der Datenaufbereitung
Großteil der Zeit in einem Projekt der Datenanalyse mit den „Vorarbeiten“ des Aufbereitens der Daten „draufgeht“. Einiges an R-Judo kann dafür nötig sein.1
9.1
Fehlende Werte
9.1.1 Ursachen von fehlenden Werten
Es gibt viele Gründe, die zu fehlenden Daten führen können: Ein technischer Fehler in
der Datenübertragung, Versuchspersonen erscheinen nicht zur Versuchsdurchführung, der
Hund läuft über die Tastatur, Teilnehmer einer Befragung verweigern die Antwort auf bestimmte Fragen oder übersehen sie aus Unaufmerksamkeit. Fehlende Werte sind für viele
Algorithmen ein Problem; einige Funktionen „verweigern“ den Dienst, wenn Datenpunkte fehlen im Datensatz. Bei der Frage, wie mit den fehlenden Werten umgegangen werden
soll, sollten folgende Fragen berücksichtigt werden (Cohen et al. 2013; Little und Rubin
2014, 2002):
1. Welcher Anteil der Daten fehlt? Offensichtlich sind größere Anteile an fehlenden Daten (z. B. 10, 20 oder 30 %) gravierender als kleinere Anteile (1, 2 oder 3 %).
2. Wie groß ist die Stichprobe? Einige Methoden sollten nur bei größeren Stichproben
(z. B. n > 200) verwendet werden.
3. Warum fehlen die Daten? Eine Gruppe von Ursachen für fehlende Werte bezeichnet
man als „komplett zufällig“: Ihr Hund hat den Zettel, auf den Sie die Daten von Versuchsperson JB007 notierten, aufgefressen. Genauso gut hätte es JB008 erwischen
können. Reiner Zufall, keine Systematik im Muster der fehlenden Werte, wie man
in Ermangelung der Kenntnis des Gedankengangs des Hundes sagt. Diese Situation
bezeichnet man auch als missing completely at random (MCAR). Etwas technischer
gesprochen: Das Fehlen von Werten steht nicht im Zusammenhang mit der Ausprägung dieser Beobachtung in anderen Variablen. Ein anderer Fall liegt vor, wenn in
einer Umfrage z. B. Männer häufiger ihr Gewicht nicht angeben als Frauen. Das Fehlen
bei Gewicht hängt dann mit einer anderen Variablen, Geschlecht, zusammen. Diesen
Fall nennt man auch, etwas missverständlich, missing at random (MAR) bzw. zufallsbedingtes Fehlen. Das Fehlen von Werten bei Gewicht ist insofern zufallsbedingt, als
es nichts mit der Ausprägung der Person zu tun: Ob eine Variable einer Beobachtungseinheit fehlt, ist unabhängig von der Ausprägung der Beobachtungseinheit in dieser
Variablen, zumindest in diesem Beispiel. Nicht zufällige Gründe für fehlende Werte
– Missing not at random (MNAR) – könnte z. B. bedeuten, dass die Antwort für das
Item Gewicht umso häufiger verweigert wird, je schwerer die Person ist. Hängt die
Wahrscheinlichkeit eines fehlenden Werts mit der Ausprägung zusammen, so spricht
man von nicht-zufällig fehlenden Werten. MNAR birgt also Informationen über die
1
fortunes::fortune(52).
9.1 Fehlende Werte
119
Ausprägung des fehlenden Werts; diese Information sollte berücksichtigt werden. Bei
MAR und MCAR gibt es keine solche Information; der fehlende Wert kann einfacher
ignoriert oder ersetzt werden.
9.1.2 Auf fehlende Werte prüfen
Das geht recht einfach mit summary(mein_dataframe). Der Befehl liefert für jede
Spalte des Dataframes mein_dataframe die Anzahl der fehlenden Werte zurück. Kennen Sie Alternativen zu summary()?2 Möchte man wissen, welches Element fehlt, so
kann man z. B. is.na() verwenden. Diese Funktion liefert für jedes Element zurück, ob
es fehlt (TRUE) oder nicht (FALSE).
stats_test %>%
pull(score) %>%
is.na() %>% sum()
#> [1] 12
Mit pull() „zieht“ man einen Vektor aus einem Dataframe. pull() liefert also, im
Gegensatz zu sonstigen dplyr-Befehlen, keinen Dataframe, sondern einen Vektor zurück.
Dieser Befehl ist hier nötig, weil is.na() als Input einen Vektor erwartet. Bei R lassen
sich logische Variablen bzw. deren Werte aufsummieren: TRUE + TRUE ist 2; TRUE wird
mit 1 übersetzt; FALSE mit 0. Daher kann man das Ergebnis von is.na() mit sum()
aufaddieren.
9.1.3 Umgang mit fehlenden Werten
Es gibt zwei Wege, mit fehlenden Werten umzugehen: Löschen oder Ersetzen. Fehlt nur
ein geringer Anteil in einem großen Datensatz, so ist Löschen eine Option. Je nachdem,
ob sich die fehlenden Werte in einer Variablen oder einem Fall (Beobachtung) häufen,
würde man entsprechend eine Zeile bzw. eine Spalte löschen; meist ist eine Zeile eher zu
verschmerzen als eine Spalte.
9.1.4 Fälle mit fehlenden Werten löschen
Löscht man Fälle mit fehlenden Werten, so sollte man prüfen, wie viele Fälle verlorengehen.
# Ursprüngliche Anzahl an Fällen (Zeilen)
nrow(stats_test)
#> [1] 1646
2
mosaic::inspect() oder skimr::skim() bieten sich an.
120
9
Praxisprobleme der Datenaufbereitung
# Nach Umwandlung in neuen Dataframe
stats_test %>%
drop_na() -> stats_test_na_omit
nrow(stats_test_na_omit)
#> [1] 1627
Bei mit der Pfeife verketteten Befehlen darf man für Funktionen die runden Klammern weglassen, wenn man keinen Parameter schreibt. Also ist drop_na (ohne
Klammern) erlaubt bei dplyr, wo es eigentlich drop_na() heißen müsste. Sie
dürfen die Klammern natürlich schreiben, aber Sie müssen nicht. Mit Klammern ist
deutlicher, dass es sich um eine Funktion handelt, nicht um ein Datenobjekt; das
erkennt man u. U. nur an den runden Klammern. Also schreiben Sie Funktionen
besser mit Klammern.
Welche Zeilen verlieren wir eigentlich? So lassen wir uns nur die nicht-kompletten
Fälle anzeigen (und davon nur die ersten paar):
stats_test %>%
filter(!complete.cases(.)) %>%
head()
Man beachte, dass der Punkt . für den Datensatz steht, wie er vom letzten Schritt
weitergegeben wurde. Innerhalb einer dplyr-Befehlskette können wir den Datensatz,
wie er im letzten Schritt beschaffen war, stets mit . ansprechen; ganz praktisch,
weil schnell zu tippen. Natürlich könnten wir diesen Datensatz jetzt als neues Objekt speichern und damit weiterarbeiten. Das Ausrufezeichen ! steht für logisches
„Nicht“. Mit head bekommt man nur die ersten paar Fälle (6 im Standard) angezeigt, was für einen Überblick oft reicht.
In Pseudo-Syntax liest sich das so:
Nimm den Datensatz stats_test UND DANN
filtere die nicht-kompletten Fälle.
9.1 Fehlende Werte
121
9.1.5 Fehlende Werte einer Spalte zählen
Wie viele fehlende Wert weist eine Spalte auf? Der dplyr-Weg sieht so aus:3
stats_test %>%
mutate(self_eval_NA = is.na(self_eval)) %>%
summarise(self_eval_NA_sum = sum(self_eval_NA))
#> # A tibble: 1 x 1
#>
self_eval_NA_sum
#>
<int>
#> 1
0
Nimm die Tabelle stats_test UND DANN
berechne eine Spalte self_eval_NA in der steht, ob bei self_eval ein NA
steht UND DANN
zähle die NAs zusammen.
Praktischer ist es natürlich, mehrere Spalten auf einmal nach fehlenden Werten zu
durchkämmen. Betrachten wir dazu den Datensatz extra.
extra %>%
select(i01:i10) %>%
skim_to_wide() %>%
select(missing)
#> # A tibble: 10 x 1
#>
missing
#>
<chr>
#> 1 6
#> 2 7
#> 3 5
#> # ... with 7 more rows
Eine „lange Pfeife“, also eine lange Sequenz verbundener R-Befehle, kann manchmal kompliziert sein. Wenn Sie unsicher sind, was ein bestimmter Befehl auslöst,
dann markieren Sie nur eine oder eine Auswahl an Zeilen. Führen Sie diesen Teil
aus und arbeiten Sie sich auf diese Weise vor.
3
Prägnanter geht es so: sum(is.na(stats_test$self_eval)).
122
9
Praxisprobleme der Datenaufbereitung
In 72 Zellen fehlen Werte; dabei wurden die Spalten i01 bis i10 (von extra) berücksichtigt. Schauen wir etwas genauer hin, so dass wir wissen, bei welchem Item (Spalte)
und welcher Zeile (Teilnehmer) ein Wert fehlt:
extra %>%
drop_na(code, i01:i10)
#> # A tibble: 608 x 34
#>
timestamp code
i01 i02r
i03
i04
i05 i06r
i07
i08
i09
#>
<chr>
<chr> <int> <int> <int> <int> <int> <int> <int> <int> <int>
#> 1 11.03.20~ HSC
3
3
3
3
4
4
3
2
3
#> 2 11.03.20~ ERB
2
2
1
2
3
2
2
3
3
#> 3 11.03.20~ ADP
3
4
1
4
4
1
3
2
3
#> # ... with 605 more rows, and 23 more variables: i10 <int>,
#> #
n_facebook_friends <dbl>, n_hangover <dbl>, age <int>, sex <chr>,
#> #
extra_single_item <int>, time_conversation <dbl>, presentation <chr>,
#> #
n_party <dbl>, clients <chr>, extra_vignette <chr>, i21 <chr>,
#> #
extra_vignette2 <int>, major <chr>, smoker <chr>, sleep_week <dbl>,
#> #
sleep_wend <int>, clients_freq <dbl>, extra_mean <dbl>,
#> #
extra_md <dbl>, extra_aad <dbl>, extra_mode <dbl>, extra_iqr <dbl>
9.1.6 Fehlende Werte ersetzen
Ist die Anzahl der fehlenden Werte zu groß, als dass wir es verkraften könnten, die Zeilen
zu löschen, so können wir die fehlenden Werte ersetzen (to replace). Man spricht dann
vom Imputieren. Allein, Imputieren ist ein weites Feld (Little und Rubin 2002). Eine einfache, aber nicht die beste Möglichkeit, besteht darin, die fehlenden Werte durch einen
repräsentativen Wert, z. B. den Mittelwert der Spalte, zu ersetzen. Im Tidyverse (Paket
tidyr) gibt es dazu eine genussfertige Funktion, replace_na():
stats_test %>%
select(-date_time, row_number) %>%
tidyr::replace_na(replace = list(interest = mean(.$interest, na.rm = TRUE)))
#> # A tibble: 1,646 x 6
#>
study_time self_eval interest score row_number bestanden
#>
<int>
<int>
<dbl> <dbl>
<int> <chr>
#> 1
3
7
4 0.75
21 ja
#> 2
4
8
5 0.825
22 ja
#> 3
3
4
1 0.789
23 ja
#> # ... with 1,643 more rows
Man übergibt replace_na() eine Liste, in der die zu prüfende Spalte benannt ist; hier
nur interest; dazu gibt man an, mit welchem Wert fehlende Werte zu ersetzen sind.
Alternativ kann man die generische Funktion base::replace() verwenden:
i10
i04
i05
i07
i02r
i06r
i08
i09
i01
i03
Combinations
i10
i04
i05
i07
i02r
i06r
i08
i09
i01
i03
0.000 0.006
Abb. 9.1 Histogramm fehlender Werte (links); Muster
fehlender Werte (rechts)
123
Proportion of missings
9.1 Fehlende Werte
interest_avg <- mean(stats_test$interest, na.rm = TRUE)
stats_test %>%
mutate(interest = replace(x = .$interest,
list = is.na(.$interest),
values = interest_avg)) -> stats_test
replace() ersetzt Werte aus dem Vektor .$interest, für die is.na(.$interest)
wahr ist; d. h., Zeilen mit fehlenden Werten in dieser Spalte werden durch den Mittelwert
ersetzt. Der Punkt . steht für den Datensatz (so wie er aus dem letzten Pfeifenschritt
hervorkam). Der Punkt . ist in dem Fall synonym zum Dataframe (wir hätten auch den
Namen der Tabelle ausschreiben können).
Alternativ gibt es eine Reihe von Paketen, die tieferen Einblick in fehlende Werte
gestatten und bessere Lösungen zur Imputation vorschlagen. VIM bietet z. B. eine gute
Visualisierung von fehlenden Werten (s. Abb. 9.1; erstellt mit der Funktion aggr()).
Links zeigt das Histogramm die Häufigkeiten fehlender Werte pro Variable. Wir sehen,
dass i01 mit gut 1 % die meisten fehlenden Werte aufweist. Rechts im Fliesendiagramm
sehen wir, dass etwa ein halbes Prozent (0.0048) aller Variablen fehlende Werte (rot) aufweist. Ein weiteres Promill fehlender Werte findet sich in der Kombination aller zehn
Items mit Ausnahme von i04 und i03 (obere Zeile). Das Diagramm zeigt uns, dass es
wenig Zusammenhang von fehlenden Werten zwischen den Items zu geben scheint. Das
sind gute Nachrichten: Hängen die fehlenden Werte zusammen, so müssten wir dies beim
Ersetzen berücksichtigen. Hier scheint es einfacher zu sein.
items <- extra %>%
select(i01:i10)
# Erstellt Diagramme zur Inspektion fehlender Werte:
aggr(items, col = c('grey80','red'),
numbers = TRUE,
sortVars = TRUE,
labels = names(data))
#>
#> Variables sorted by number of missings:
#> Variable
Count
#>
i10 0.01090
#>
i04 0.00969
#>
i05 0.00969
124
#>
#>
#>
#>
#>
#>
#>
9
i07
i02r
i06r
i08
i09
i01
i03
Praxisprobleme der Datenaufbereitung
0.00969
0.00847
0.00847
0.00847
0.00847
0.00726
0.00605
Mit mice kann man sich mehrere Varianten von Imputationen (multiple Imputationen)
ausgeben lassen. Dann kann man deren Mittelwerte betrachten oder ein Modell jede Variante der Imputation anpassen und die Ergebnisse vergleichen. Wir lassen uns mit mice()
nur eine Variante (m = 1) von fehlenden Werten ausgeben. Details der Berechnung sollen hier nicht ausgegeben werden (printFlag = FALSE). Mit help(mice) bekommt
man ausführlichere Informationen zu diesem Befehl.
extra %>%
select(i01:i10) %>%
mice(m = 1,
printFlag = FALSE) -> items_imputed
Warum ist die multiple Imputation empfehlenswert? Ein zentraler Vorteil ist, dass durch
das mehrfache Ersetzen mehr Information gewonnen wird als durch einmaliges Ersetzen
(vgl. Buuren (2012)). So, wie eine Stichprobe mit n D 1 weniger Information liefert
als eine Stichprobe mit n D 5, so ist auch multiples Imputieren informationsreicher als
einmaliges Imputieren. Die Vorteile liegen sowohl im Maß der zentralen Tendenz der
Imputation als auch in der Variabilität der Ersatzwerte. Die Streuung der Imputationen sagt
etwas über die Unsicherheit der Imputation aus, und der Mittelwert erlaubt eine bessere
Schätzung als eine einzelne Imputation. Bei Little und Rubin (2002) und Buuren (2012)
finden sich ausführliche Einführungen zu diesem Thema.
9.1.7 -99 in NA umwandeln
Manchmal kommt es vor, dass fehlende Werte nicht durch leere Zellen bzw. Rs Symbol
dafür (NA) kodiert sind, sondern durch ein anderes Symbol. -99 ist ein Kandidat, den man
immer mal wieder trifft. Wie erklären wir R, dass -99 zu NA umkodiert werden soll? So:
stats_test$study_time[1] <- -99
head(stats_test)
stats_test %>%
mutate_if(is_numeric, na_if, -99) %>% head
9.2 Datenanomalien
125
Der Befehl sagt in etwa „Transformiere jede Spalte, wenn sie numerisch ist. Und die
Transformation, die du anwenden sollst, R, ist Erzeuge NA, wenn X der Fall ist. Und
dieses wenn X ist99“.
Aufgaben
1. Probieren Sie zur Identifikation von fehlenden Werten die Funktionen
favstats::inspect(), sjmisc::descr() und skimr::skim() aus.
Versuchen Sie auch die Methode mit summarise_if().
2. Wie viele fehlende Werte weist score im Datensatz stats_test auf? Verwenden Sie die Funktionen sum() und is.na() zur Beantwortung.4
3. Wie viele Fälle bleiben uns eigentlich übrig, wenn wir alle Zeilen mit fehlenden
Werten aus dem Datensatz extra entfernen?5
9.2 Datenanomalien
Unter einer Datenanomalie verstehen wir merkwürdige Daten. Das können Eingabefehler
sein oder schlicht auch ungewöhnliche Werte, wie ein Mensch mit Körpergröße 2.11 m.
Besonders im Fall von solchen Extremwerten gilt, dass es keine mechanische Regel im
Umgang damit gibt, sondern dass im Einzelfall entschieden werden muss, ob es besser ist,
den Fall zu entfernen, ihn zu ändern oder ihn so zu lassen, wie er ist.
9.2.1 Doppelte Fälle löschen
Es kann passieren, dass sich doppelte Fälle (Dubletten) im Datensatz finden; z. B. durch
einen Fehler beim Erstellen oder Einlesen der Daten. In dem Fall wären komplette Zeilen
gleich. Auf ähnliche Weise kann es passieren, dass der Nutzer Alois zweimal auf unsere
Umfrage antwortet. Wenn sein Gedächtnis nicht so gut ist, ist es gut möglich, dass nicht
jede Antwort beim zweiten Mal gleich wie beim ersten Mal beantwortet wurde. In dem
Fall wären nur Teile von Zeilen gleich. In beiden Fällen werden Sie wahrscheinlich die
Dubletten wieder loswerden wollen. Schauen wir uns letzteren Fall zuerst an:
stats_test %>%
mutate(is_duplicate = duplicated(date_time)) %>%
filter(!is_duplicate) %>%
nrow
#> [1] 1645
4
5
sum(is.na(stats_test$score)).
extra %>% select(i01:i10) %>% filter(complete.cases(.)) %>% nrow.
126
9
i02r
i03
1.00
i04
400
300
200
100
0
0.75
Anteil
count
i01
Praxisprobleme der Datenaufbereitung
i05
i06r
i07
i08
400
300
200
100
0
0.50
0.25
0.00
i01 i02r i03
i04
i05 i06r i07
i08
Item
1 2 3 4
1 2 3 4
1 2 3 4
1 2 3 4
value
Antwort
1
2
3
4
NA
Abb. 9.2 Anomalien identifizieren
Die Funktion duplicated gibt für jedes Element eines Vektors zurück, ob das Objekt eine Dublette ist, d. h., ob der betreffende Wert in vorherigen Elementen schon einmal aufgetaucht ist. In dem Datensatz gibt es offenbar keine Dubletten: Alle Werte von date_time
sind verschieden.
9.2.2
Nach Anomalien suchen
Eine Anomalie kann ein echter Fehler sein: Leicht schleichen sich Tippfehler oder sonstige Verarbeitungsfehler ein. Man sollte darauf prüfen; so könnte man sich ein Histogramm
ausgeben lassen pro Variable, um „ungewöhnliche“ Werte gut zu erkennen. Meist geht das
besser als durch das reine Betrachten von Zahlen. Gibt es wenig unterschiedliche Werte,
so kann man sich auch die unterschiedlichen Werte ausgeben lassen. Wer mag, kann sich
für diesen Zweck ein Diagramm ausgeben lassen.
stats_test %>%
dplyr::count(interest, sort = TRUE)
stats_test %>%
dplyr::count(interest, sort = TRUE) %>%
ggplot(aes(x = factor(interest), y = n)) +
geom_point()
Schauen wir uns noch die Verteilungen der Item-Antworten an (s. Abb. 9.2); sehen diese
plausibel aus? Einige Verteilungen sind ordentlich schief. Man könnte räsonieren, dass
man sich damit in guter Gesellschaft befindet – mit Blick auf andere Umfragen. Aber die
ungleiche Fallzahl pro Itemantwort und die Abweichung von der Normalverteilung könnten später Probleme bereiten. Es würde sich anbieten, die schiefen Items zu überarbeiten.
Findet man „merkwürdige“ (unplausible) Werte, so kann es sinnvoll sein, diese Fälle
zu entfernen (im Detail eine schwierige Entscheidung). Besser als die Zeilen zu löschen,
9.2 Datenanomalien
127
1.00
150
0.75
score
100
50
0.50
0.25
0
0.00
0.00
0.25
0.50
0.75
1.00
score
score
score
Abb. 9.3 Ausreißer identifizieren; links: Histogramm; rechts: Boxplot
kann es sein, diese Werte in NA umzuwandeln. Sagen wir, wir möchten im Datensatz
stats_test alle Fälle mit score < .5 in NA umwandeln.
stats_test %>%
mutate(score_bereinigt = replace(.$score,
.$score < .5,
NA)) -> stats_test
9.2.3 Ausreißer identifizieren
Ähnlich wie Fehlern steht man Ausreißern häufig skeptisch gegenüber. Allerdings kann
man nicht pauschal sagen, dass Extremwerte entfernt werden sollen: Vielleicht war jemand in der Stichprobe wirklich nur 1.20 m groß? Hier gilt es, im Einzelfall begründet und
nachvollziehbar zu entscheiden. Histogramme und Boxplots sind ein geeignetes Mittel,
um Ausreißer zu finden (s. Abb. 9.3). Definieren wir Fälle, die mehr als 3 Standardabweichungseinheiten vom Mittelwert entfernt (d. h. z-Werte größer als 3 und kleiner als 3)
sind, als Extremfälle:
stats_test %>%
mutate(score_z = (score - mean(score, na.rm = TRUE)) /
sd(score, na.rm = TRUE)) %>%
mutate(is_extreme = if_else(abs(score_z) > 3, TRUE, FALSE)) %>%
count(is_extreme)
#> # A tibble: 3 x 2
#>
is_extreme
n
#>
<lgl>
<int>
#> 1 FALSE
1632
#> 2 TRUE
2
#> 3 NA
12
128
9
Praxisprobleme der Datenaufbereitung
Die Funktion if_else() kann man für Wenn-Dann-Prüfungen nutzen. Die Funktion
hat drei Argumente: 1) Wie lautet die zu prüfende Bedingung?, 2) Was tun, wenn die
Prüfung ein positives Ergebnis liefert? 3) Was tun im negativen Fall? Im vorliegenden
Fall übersetzt sich die entsprechende Zeile etwa so ins Deutsche: „Wenn der z-Wert einer
Person größer als 3 ist, dann soll die Variable is_extreme für diese Person auf TRUE
gesetzt werden, ansonsten auf FALSE“. Eine (kleine) Zahl an Extremwerte haben wir so
identifiziert; diese könnten wir jetzt näher betrachten und ggf. entfernen oder auf einen
kleineren Wert zurückstutzen.
9.2.4
Hochkorrelierte Variablen finden
Haben zwei Leute die gleiche Meinung, so ist einer von beiden überflüssig, wird manchmal behauptet. Ähnlich bei Variablen; sind zwei Variablen sehr hoch korreliert (>.9 als
grober Richtwert), so bringt die zweite kaum Informationszuwachs zur ersten und kann in
einigen Situationen ausgeschlossen werden. Nehmen wir dazu den Datensatz extra her
und berechnen wir eine Korrelationsmatrix, d. h., wir korrelieren alle Variablen paarweise
miteinander:
extra %>%
dplyr::select(i01:i10) %>% # Wähle die Variablen von i1 bis i10 aus
corrr::correlate() -> km
# Korrelationsmatrix berechnen
km %>%
select(1:3) # der Übersichtlichkeit halber nur ein Auszug
#> # A tibble: 10 x 3
#>
rowname
i01
i02r
#>
<chr>
<dbl> <dbl>
#> 1 i01
NA
0.468
#> 2 i02r
0.468 NA
#> 3 i03
0.117 0.115
#> # ... with 7 more rows
Betrachten Sie die Korrelationstabelle, was fällt Ihnen auf?6 In diesem Beispiel sind keine
Variablen sehr hoch korreliert. Wir leiten keine weiteren Schritte ein, abgesehen von einer
Visualisierung (s. Abb. 9.4, links)
p_corr1 <- km %>%
shave() %>% # Oberes Dreieck ist redundant, wird "abrasiert"
rplot(colors = c("firebrick", "blue4")) # Korrelationsplot
6
Sie ist quadratisch, auf der Hauptdiagonalen steht NA, und gespiegelt an der Hauptdiagonalen,
d. h., das obere Dreieck der Matrix ist redundant.
9.2 Datenanomalien
129
i03
1.0
i04
i05
0.5
i06r
0.0
i07
-0.5
i08
i09
-1.0
i10
i01i02ri03i04i05i06ri07i08i09
i10
i09
i08
i07
i06r
i05
i04
i03
i02r
i01
Corr
1.0
0.5
0.0
-0.5
-1.0
i0
i0 1
2
i0 r
i03
i04
i0 5
6
i0 r
i07
i08
i19
0
i02r
Abb. 9.4 Varianten von Korrelationsplots
Die Funktion correlate() stammt aus dem Paket corrr, welches vorher installiert
und geladen sein muss. Hier ist die Korrelation nicht zu groß, so dass wir keine weiteren
Schritte unternehmen. Hätten wir eine sehr hohe Korrelation gefunden, so hätten wir eine
der beiden beteiligten Variablen aus dem Datensatz löschen können. Alternativ stellt die
Funktion ggcorr:ggcorrplot() schöne Korrelationsplots dar (s. Abb. 9.4, rechts).
extra %>%
dplyr::select(i01:i10) %>%
drop_na() %>%
cor() %>%
ggcorrplot() -> p_corr2
9.2.5 Quasi-Konstante finden
Hier suchen wir nach Variablen (Spalten), die nur einen Wert oder zumindest nur sehr wenige verschiedene Werte aufweisen. Oder, ähnlich: Wenn 99.9 % der Fälle nur von einem
Wert bestritten werden. In diesen Fällen kann man die Variable als „Quasi-Konstante“
bezeichnen. Quasi-Konstanten sind für die Modellierung von keiner oder nur geringer Bedeutung; sie können in der Regel für weitere Analysen ausgeschlossen werden. Haben wir
z. B. nur Männer im Datensatz, so kann das Geschlecht nicht für Unterschiede im Einkommen verantwortlich sein. Besser ist es, die Variable Geschlecht zu entfernen. Auch
hier sind Histogramme oder Boxplots von Nutzen zur Identifikation von (Quasi-)Konstanten. Alternativ kann man sich auch pro Variable die Streuung (numerische Variablen)
oder die Anzahl unterschiedlicher Werte (qualitative Variablen) ausgeben lassen:
IQR(extra$n_facebook_friends, na.rm = TRUE) # keine Konstante
#> [1] 300
n_distinct(extra$sex) # es scheint 3 Geschlechter zu geben...
#> [1] 3
130
9
Praxisprobleme der Datenaufbereitung
4
200
density
count
150
100
sample
0.75
0.50
2
0.25
50
0
3
0.00
1
2
3
4
extra_mean
2
3
extra_mean
4
-2
0
2
theoretical
Abb. 9.5 Visuelles Prüfen der Normalverteilung
9.2.6 Auf Normalverteilung prüfen
Einige statistische Verfahren gehen von normalverteilten Variablen aus; eine Abweichung
von der Normalverteilung ist insofern eine Anomalie. Daher ist es sinnvoll, auf Vorliegen
der Normalverteilung zu prüfen. Perfekte Normalverteilungen sind genau so häufig wie
perfekte Kreise in der Natur: Es gibt sie nicht. Entsprechend werden Signifikanztests, die
auf perfekte Normalverteilung prüfen, immer signifikant sein, sofern die Stichprobe groß
genug ist. Außerdem hat sich gezeigt, dass auch eine einigermaßen normalverteilte Verteilung in der Wirklichkeit seltener ist, als man denken könnte (Micceri 1989). Daher ist es
meist zweckmäßiger, einen graphischen „Test“ durchzuführen: ein Histogramm, ein QQPlot oder ein Dichte-Diagramm als „glatt geschmirgelte“ Variante des Histogramms bieten
sich an (s. Abb. 9.5). Während der mittlere Extraversionswert recht gut normalverteilt ist,
ist die Anzahl der Facebookfreunde ordentlich (rechts-)schief. Bei schiefen Verteilungen
können Transformationen Abhilfe schaffen; ein Thema, auf das wir hier nicht weiter eingehen. Die Erstellung solcher Diagramme ist im Kap. 11 geschildert.
9.3
Daten umformen
9.3.1 Aufgeräumte Dataframes
Dataframes sollten „aufgeräumt“ (tidy; in Normalform) sein, bevor wir weitere Analysen
starten. Unter aufgeräumt verstehen sich folgende Punkte:
Es handelt sich um einen Dataframe, also um eine Tabelle mit Spalten mit Namen und
gleicher Länge; eine Datentabelle in rechteckiger Form und die Spalten haben einen
Namen.
In jeder Zeile steht eine Beobachtung, in jeder Spalte eine Variable.
Fehlende Werte sollten sich in leeren Zellen niederschlagen; diese werden von R automatisch mit NA gekennzeichnet.
9.3 Daten umformen
131
Abb. 9.6 Schematische Darstellung eines Dataframes in Normalform
Daten sollten nicht von Hand mit Farbmarkierungen o. Ä. kodiert werden (z. B. in Excel).
Man sollte in Variablennamen keine Sonderzeichen und keine Leerzeichen verwenden,
sondern nur Ziffern und Buchstaben und Unterstriche.
Variablennamen beginnen nicht mit einer Zahl.
Abb. 9.6 visualisiert die Bestimmungsstücke eines Dataframes (Wickham und Grolemund
2017). Der Punkt jede Zeile eine Beobachtung, jede Spalte eine Variable, jede Zelle ein
Wert verdient besondere Beachtung. Betrachten Sie das Beispiel in Abb. 9.7, links). In
der rechten Tabelle sind die Variablen Quartal und Umsatz klar getrennt; jede hat ihre
eigene Spalte. In der linken Tabelle hingegen sind die beiden Variablen vermischt. Sie
haben nicht mehr ihre eigene Spalte, sondern sind über vier Spalten verteilt. Die rechte
Tabelle ist ein Beispiel für eine Tabelle in Normalform, die linke nicht.
9.3.2 Langes vs. breites Format
Eine der ersten Aktionen einer Datenanalyse sollte das Aufräumen Ihres Dataframes sein.
In R bietet sich dazu das Paket tidyr an, mit dem die Tabelle von Breit- auf Langformat
(und wieder zurück) geschoben werden kann. Abb. 9.7 (links) zeigt ein Beispiel dazu.
Warum ist es wichtig, von der „breiten“ zur „langen“ oder „Normalform“ zu wechseln?
Ganz einfach: viele Befehle (allgemeiner: Tätigkeiten) verlangen die Normalform; hin
und wieder sind aber die Tabellen von ihrem Schöpfer in breiter Form geschaffen worden.
Zum Beispiel erwartet ggplot2 – und viele andere Diagrammbefehle –, dass man einer
Achse eine Spalte (Variable) zuweist, z. B. die Variable „Umsatz“ auf die Y-Achse. Der
X-Achse könnten wir dann z. B. die Variable „Quartal“ packen (s. Abb. 9.7, rechts).
Um von der breiten Form zur langen Form zu kommen, kann man den Befehl
tidyr::gather() nehmen. Von der langen Form zur breiten Form gibt es tidyr::
spread(). Also etwa:
library(tidyr)
df_lang <- gather(df_breit, key = "Quartal", value = "Umsatz")
df_breit <- spread(df_lang, Quartal, Umsatz)
324
343
1
2
3
124
342
342
Q2
456
234
431
Q3
Q4
465
345
675
342
342
124
Q3
124
342
Q2
2
342
Umsatz
value
Q1
Q2
1
3
Quartal
Q1
ID
key
Umsatz
1
3
Quartal
2
Abb. 9.7 „Tidy“ Dataframes. Links: langes vs. breites Format; rechts: eine Abbildung auf Basis einer Normalform-Tabelle
spread
gather
lang (Normalform)
4
9
Werte der Spalte value
Q1
123
ID
Werte der Spalte key
breit
132
Praxisprobleme der Datenaufbereitung
9.3 Daten umformen
133
31 %
50 %
16 %
0% 0%
-3
2%
69 %
84 %
5%
-2
98 % 99 %
-1
0
1
2
3
Abb. 9.8 Flächenanteile und z-Werte bei der Normalverteilung
Dabei baut gather() den Dataframe so um, dass nur zwei Spalten übrig bleiben
(s. Abb. 9.7, links). Es resultiert eine Spalte, die nur Spaltennamen („Q1“, „Q2“, . . . )
enthält; diese Spalte nennt gather() im Standard key. Die zweite Spalte enthält die
Werte (z. B. Umsätze), die vormals über mehrere Spalten verstreut waren. Diese Spalte
heißt per Default value. Im Beispiel macht die Spalte ID bei dem Spiel „Aus vielen
Spalten werden zwei“ nicht mit. Möchte man die Spalte ID aussparen, so schreibt man
das bei gather() so:
df_lang <- gather(df_breit, key = "Quartal", value = "Umsatz", -ID)
9.3.3 z-Standardisieren
Für eine Reihe von Analysen ist es wichtig, die Skalierung der Variablen zu vereinheitlichen. Die z-Standardisierung ist ein übliches Vorgehen. Dabei wird der Mittelwert auf 0
transformiert und die SD auf 1. Bei normalverteilten Variablen haben die resultierenden zWerte den Charme, dass die resultierenden Flächenanteile wohlbekannt sind (s. Abb. 9.8).
Hat man die Werte einer normalverteilten Variablen N z-transformiert, so spricht man
von der Standardnormalverteilung N0 . Der Unterschied der N0 -Werte zweier Fälle A und
B sagt nur etwas aus zur relativen Position von A zu B innerhalb der ursprünglichen Verteilung N . Hinweise zum „echten“ Mittelwert bzw. der echten Streuung wurden durch die
Standardisierung entfernt.
extra %>%
dplyr::select(i01, i02r) %>%
scale() %>% # z-standardisieren
as_data_frame() %>% # von Matrix zurück zu Dataframe
head() # nur die ersten paar Zeilen abdrucken
#> # A tibble: 6 x 2
#>
i01
i02r
#>
<dbl> <dbl>
#> 1 -0.519 -0.134
#> 2 -2.00 -1.38
#> 3 -0.519 1.11
#> 4 -0.519 -0.134
#> 5 0.964 -0.134
#> 6 -0.519 -1.38
134
9
Praxisprobleme der Datenaufbereitung
Dieser Befehl liefert z-standardisierte Spalten zurück. Man beachte, dass scale() eine
Matrix zurückliefert, weswegen wir mit as_data_frame() wieder in einen Dataframe
umwandeln. Kommoder ist es, alle Spalten des Datensatzes nicht auf die zu standardisieren Variablen zu verkleinern; sonst müsste man im Nachgang die von select()
entfernten Variablen wieder hinzufügen.
extra %>%
mutate_if(is.numeric, funs("z" = scale)) %>%
glimpse
Der Befehl mutate() berechnet eine neue Spalte; mutate_if(is.numeric, ...)
tut dies nur für numerische Spalten. Die neue Spalte wird berechnet als z-Transformierung
der alten Spalte; zum Spaltennamen wird ein „z“ hinzugefügt. Natürlich hätten wir auch
mit select „händisch“ die relevanten Spalten auswählen können. Betrachten Sie die
ersten paar Zeilen der neu erstellten Tabelle.7
Da beim z-Transformieren einer Variablen X von jedem Wert xi der Mittelwert von X
abgezogen wird, haben z-transformierte Variablen den Mittelwert 0: XN z D 0. Wenn der
P
Mittelwert 0 ist, so ist auch die Summe der Werte 0: X D 0. Da jeder Wert durch die
X
D 1.
sdX geteilt wird, ist sdXz D sd
sdX
9.3.4 Spaltennamen ändern
Es gibt, wie für fast alles, mehrere Wege in R zu diesem Ziel. Möchte man alle Spaltennamen ändern, so ist der Befehl names() ganz praktisch: names(mein_df) <c("Var1", "Var2"). Möchte man nur einzelne Spaltennamen ändern, so kann man
den Befehl rename() aus dplyr verwenden; die Syntax lautet: rename(mein_df,
neuer_name = alter_name).8
stats_test %>%
rename(Punkte = score) -> stats_test
Übrigens kann man den gleichen Effekt mit dplyr::select() auch erreichen;
genauer den fast gleichen. Probieren Sie mal rename(stats_test, Punkte =
score) und vergleichen Sie das Ergebnis.
7
head(extra).
Es gibt auch weniger schöne Wege, um Spalten umzubenennen:
names(stats_test)[names(stats_test) == "bestanden"] <- "jippiejey".
8
9.3 Daten umformen
135
9.3.5 Variablentypen ändern
Mitunter kommt es vor, dass man eine numerische Variable in eine Faktor- oder in eine
Textvariable ändern will oder umgekehrt. Solange keine Faktorvariablen involviert sind,
ist das einfach. Sagen wir, Sie wollen die Variable interest aus stats_test in eine
Textvariable (character) umwandeln. str(stats_test$interest) zeigt Ihnen,
dass die Variable metrisch ist, genauer gesagt ganzzahlig (Typ integer). So geht’s:
stats_test %>%
mutate(interest_chr = as.character(interest)) -> stats_test
Analog kann man mit factor(interest) in eine Faktorvariable umwandeln (vgl.
Kap. 5). Mit as.numeric() geht es wieder retour. Warum würde man überhaupt den
Variablentyp ändern wollen? Weil manche Befehle bestimmte Erwartungen an den Variablentyp haben, den Sie verarbeiten möchten. Ein Beispiel ist die Farbgebung bei
ggplot2:
stats_test %>%
ggplot(aes(x = interest, fill = as.numeric(bestanden))) +
geom_bar() +
labs(title = "Keine Füllfarben. Traurig.")
Wird als Füllfarbe für geom_bar eine numerische Variable angegeben, so verweigert
ggplot() den Dienst. Wird aber eine nominale Variable (character oder factor)
angegeben, so weiß ggplot(), was zu tun ist. Möchte man eine Faktor-Variable in eine
numerische Variable umwandeln, muss man Folgendes wissen: Die erste Stufe eines Faktors wird intern mit 1 gespeichert, die zweite mit 2 und so weiter; etwa so:
bestanden = 1
durchgefallen = 2
nicht abgegeben = 3
Warum macht R das so? Diese Art von Kodierung spart Speicher (ja, auch heute kann
das nützlich sein). Außerdem ist es manchmal praktisch, wenn vorab definiert ist, welche
Werte zulässig sind. Eine Faktorvariable kann nur Werte speichern, die zu einer ihrer
Stufen passen. Möchten wir für fünf Studenten angeben, ob sie eine Prüfung bestanden
haben, durchgefallen sind oder nicht abgegeben haben, könnte das so aussehen:
Ergebnis <- c(1, 1, 1, 2, 3)
Ergebnis <- factor(Ergebnis, labels = c("bestanden",
"durchgefallen",
"nicht abgegeben"))
Ergebnis
#> [1] bestanden
bestanden
bestanden
durchgefallen
#> [5] nicht abgegeben
#> Levels: bestanden durchgefallen nicht abgegeben
136
9
Praxisprobleme der Datenaufbereitung
Moment, bei Prof. Feistersack ist noch ein Student angetreten; tja, durchgefallen:
Ergebnis <- c(Ergebnis, "durchgefallen"). Das ist eine gültige R-Syntax.
Diese Syntax hingegen erzeugt eine Warnung: Ergebnis[4] <- "fast geschafft"
und fügt an Position 4 NA ein. Natürlich hindert uns niemand, als Stufen Zahlen zu definieren. Ob das sinnvoll ist, müssen Sie selber entscheiden:
Ergebnis <- c(1, 1, 1, 2, 3)
Ergebnis <- factor(Ergebnis, labels = c("10", "20", "30"))
Ergebnis
#> [1] 10 10 10 20 30
#> Levels: 10 20 30
Möchte man jetzt diese Faktorvariable in eine numerische Variable umwandeln, kann es
zu Verwirrung kommen. Denn R gibt die Zahlen zurück, die intern verwendet werden, um
die Faktorstufen zu definieren:
as.numeric(Ergebnis)
#> [1] 1 1 1 2 3
Wahrscheinlich wollte man eher „10, 10, 10, 20, 30“ als Ergebnis zurückgeliefert bekommen. Um das zu erreichen, wandelt man eine Faktorvariable zuerst in eine Textvariable
um, und dann in eine numerische Variable:
Ergebnis %>% as.character() %>% as.numeric()
#> [1] 10 10 10 20 30
9.4 Werte umkodieren und partitionieren
Umkodieren bedeutet, die Werte einer Variablen nach einem Muster zu ändern. Man sieht
immer mal wieder, dass die Variable gender (Geschlecht) mit 1 und 2 kodiert ist. Verwechslungen sind da vorprogrammiert („Ich bin mir 100 % sicher, dass ich wahrscheinlich
1 für Männer kodiert habe.“). Besser wäre es, die Ausprägungen male und female
(„Mann“, „Frau“) o. Ä. zu verwenden (s. Abb. 9.9, links). Eine Ausnahme sind Variablen wie is_female (oder kürzer female); hier sind Werte wie 1 („ja“) und 0 („nein“)
einfach verständlich.
Partitionieren oder Binnen bedeutet, eine kontinuierliche Variable in einige Bereiche
zu „zerschneiden“. Damit macht man aus einer kontinuierlichen Variablen eine diskrete.
Ein Bild erläutert das am einfachsten (s. Abb. 9.9, rechts). Möchte man Häufigkeiten bei
einer metrischen Variablen auszählen (z. B. „Was ist die häufigste Körpergröße?“), bietet
es sich an, die Werte erstmal zu partitionieren (z. B. klein vs. mittel vs. groß). Allgemeiner
gesprochen sind Häufigkeitsauswertungen nur dann sinnvoll, wenn die Gruppen nicht zu
dünn besiedelt sind.
9.4 Werte umkodieren und partitionieren
Abb. 9.9 Links: Umkodieren;
rechts: Binnen
137
Geschlecht
Geschlecht_umkodiert
1
Mann
1
Mann
2
Frau
9.4.1 Umkodieren und partitionieren mit car::recode()
Manchmal möchte man z. B. negativ gepolte Items umdrehen oder bei kategoriellen Variablen kryptische Bezeichnungen in sprechendere umwandeln. Hier gibt es eine Reihe
praktischer Befehle, z. B. bietet recode() aus dem Paket car. dplyr auch einen Befehl recode(), der strenger ist bzw. frühzeitig einen Fehler auswirft. Es ist besser, früher
als später einen Fehler zu entdecken, was für dplyr::recode() spricht. Auf der anderen Seite ist car::recode() flexibler und damit komfortabler. Schauen wir uns ein paar
Beispiele zum Umkodieren an.
stats_test %>%
mutate(study_binned = car::recode(.$study_time,
"5 = 'sehr viel'; 2:4 = 'mittel'; 1 = 'wenig'",
as.factor = TRUE)) -> stats_test
stats_test %>%
mutate(study_binned = car::recode(.$study_time,
"5 = 'sehr viel'; 2:4 = 'mittel'; 1 = 'wenig'",
as.factor = FALSE)) -> stats_test
stats_test %>%
mutate(score_binned = car::recode(.$score,
"40:38 = 'sehr gut'; 37:35 = 'gut'; else = 'Weiterlernen'",
as.factor = TRUE)) -> stats_test
stats_test %>% select(score_binned, study_binned) %>% head
Der Befehl car::recode() ist praktisch; mit : kann man „von bis“ ansprechen (das
ginge mit c() übrigens auch); else für „ansonsten“ ist möglich und mit as.factor.
result kann man entweder einen Faktor oder eine Text-Variable zurückgeliefert bekommen. Der ganze „Wechselterm“ steht in Anführungsstrichen ("..."). Einzelne Teile des
Wechselterms sind mit einem Strichpunkt (;) voneinander getrennt.
138
9
Praxisprobleme der Datenaufbereitung
Wann braucht man einen Punkt . in einer dplyr-Pfeifenkette? Man braucht ihn
nur dann, wenn man innerhalb der Pfeifenkette Nicht-dplyr-Befehle verwendet wie
mean() oder recode(). Diese Befehle wissen nicht, dass sie gerade in einer Pfeifenkette stehen, daher müssen wir ihnen den Namen der Tabelle explizit mitteilen.
Genau genommen steht der Punkt für die Tabelle, so wie sie aus dem letzten Pfeifenschritt herausgekommen ist (z. B. schon mit einigen Zeilen weggefiltert).
Das klassische Umkodieren von Items aus Fragebögen kann man so anstellen; sagen
wir interest soll umkodiert werden:
stats_test %>%
mutate(no_interest = car::recode(.$interest,
"1 = 6; 2 = 5; 3 = 4; 4 = 3; 5 = 2; 6 = 1; else = NA")) -> stats_test
glimpse(stats_test$no_interest)
#> num [1:1646] 3 2 6 2 4 3 4 3 2 2 ...
Beim Wechselterm muss man aufpassen, nichts zu verwechseln; die Zahlen sehen alle
ähnlich aus. Testen ist hier unerlässlich; prüfen Sie die den Erfolg des Umkodierens anhand folgender Syntax:
dplyr::count(stats_test, interest)
dplyr::count(stats_test, no_interest)
Scheint zu passen. Noch praktischer ist, dass man so numerische Variablen in Bereiche
aufteilen kann („binnen“). Hier das Binnen von Prof. Schnaggeldi:
stats_test %>%
mutate(bestanden =
car::recode(.$score,
"0:0.9 = 'durchgefallen';
else = 'bestanden'")) -> stats_test_schnaggeldi
9.4.2
Einfaches Umkodieren mit einer Logik-Prüfung
Nehmen wir an, wir möchten die Anzahl der Punkte in einer Statistikklausur (score)
umkodieren in eine Variable „bestanden“ mit den zwei Ausprägungen „ja“ und „nein“;
der griesgrämige Professor beschließt, dass die Klausur ab 25 Punkten (von 40) bestanden
sei. Die Umkodierung ist also von der Art „viele Ausprägungen in zwei Ausprägungen
umkodieren“. Das kann man z. B. so erledigen:
9.4 Werte umkodieren und partitionieren
139
stats_test %>%
mutate(bestanden = score > 24) -> stats_test
head(stats_test$bestanden)
#> [1] FALSE FALSE FALSE FALSE FALSE FALSE
Genauso könnte man sich die „Grenzfälle“ – die Bemitleidenswerten mit 24 von 40 Punkten – anschauen (knapp daneben sei auch vorbei, so der griesgrämige Professor weiter):
stats_test$Grenzfall <- stats_test$score == 24/40
dplyr::count(stats_test, Grenzfall)
#> # A tibble: 3 x 2
#>
Grenzfall
n
#>
<lgl>
<int>
#> 1 FALSE
1572
#> 2 TRUE
62
#> 3 NA
12
Natürlich könnte man auch hier „durchpfeifen“:
stats_test <stats_test %>%
mutate(Grenzfall = score == 24/40)
9.4.3 Binnen mit cut()
Numerische Werte in Klassen zu gruppieren (to bin, denglisch: „binnen“), kann mit dem
Befehl cut (and friends) erledigt werden. Es lassen sich drei typische Anwendungsformen
unterscheiden. Man möchte eine numerische Variable . . .
1. in k gleich große Intervalle gruppieren
2. so in Klassen gruppieren, dass in jeder Klasse n Beobachtungen sind (gleiche Gruppengrößen)
3. in beliebige Klassen gruppieren
9.4.3.1 Gleichgroße Intervalle
Nehmen wir an, wir möchten die numerische Variable score in drei Gruppen einteilen:
„schlecht“, „mittel“ und „gut“. Der Range von score soll gleichmäßig auf die drei Gruppen aufgeteilt werden. Dazu kann man cut_interval() aus ggplot2 nehmen.9
temp <- cut_interval(x = stats_test$score, n = 3)
levels(temp)
#> [1] "[0,0.333]"
"(0.333,0.667]" "(0.667,1]"
9
ggplot2 muss geladen sein; wenn man tidyverse lädt, wird ggplot2 automatisch auch
geladen.
140
9
Praxisprobleme der Datenaufbereitung
cut_interval() liefert eine Variable vom Typ factor zurück. Hier haben wir das
Punktespektrum in drei gleich große Bereiche unterteilt (d. h. mit jeweils gleichem
Punkte-Range). Mit eckigen Klammern weist cut darauf hin, dass der angezeigte Werte nicht zum Intervall gehört; runde Klammern zeigen an, dass der Wert zum Intervall
gehört.
9.4.3.2 Gleiche Gruppengrößen
Mit cut_number() (aus ggplot2) kann man einen Vektor in n Gruppen mit (etwa) gleich
vielen Observationen einteilen. Hier teilen wir score am Median; teilt man einen Vektor
in zwei gleich große Gruppen, so entspricht das einer Aufteilung am Median (MedianSplit).
score_gruppen <- cut_number(stats_test$score, n = 2)
str(score_gruppen)
#> Factor w/ 2 levels "[0,0.775]","(0.775,1]": 1 2 2 2 2 2 2 2 2 1 ...
9.4.3.3 In beliebige Klassen gruppieren
stats_test %>%
mutate(punkte_gruppe = cut(stats_test$score,
breaks = c(-Inf, .5, .6, .7, .8, .9),
labels = c("5", "4", "3", "2", "1"))) -> stats_test
dplyr::count(stats_test, punkte_gruppe)
#> # A tibble: 6 x 2
#>
punkte_gruppe
n
#>
<fct>
<int>
#> 1 5
62
#> 2 4
220
#> 3 3
319
#> 4 2
339
#> 5 1
346
#> 6 <NA>
360
cut() ist im Standard-R (Paket base) enthalten. Mit breaks gibt man die Intervallgrenzen an. Zu beachten ist, dass man eine Unter- bzw. Obergrenze angeben muss. Das
heißt, der kleinste Wert in der Stichprobe wird nicht automatisch als unterste Intervallgrenze herangezogen, sondern muss explizit als Intervallgrenze benannt werden. Anschaulich
gesprochen ist cut() ein Messer, das ein Seil (die kontinuierliche Variable) mit einem
oder mehreren Schnitten zerschneidet (s. Abb. 9.9, rechts). Wenn wir dreimal schneiden
(breaks), resultieren zwei Teile, wie Abb. 9.9 (rechts) zeigt. Darum müssen wir im Beispiel oben 5 (6 1) labels für die Teile vergeben.
9.5 Vektoren zu Skalaren zusammenfassen
9.5
141
Vektoren zu Skalaren zusammenfassen
Eine der häufigsten Operationen in der angewandten Datenanalyse ist es, einen Vektor
zu einem Skalar (einzelne Zahl) zusammenzufassen: Der Mittelwert von 1, 2 und 3 ist
2. Typische Anwendungsbeispiele von Zusammenfassungsoperationen sind die Berechnung des Mittelwerts meiner Antworten zum Extraversionsfragebogen; die Berechnung
der medianen Verweildauer von Besuchern auf einer Webseite oder die Berechnung des
maximalen Überziehungszeitraums eines Kunden.
9.5.1
Mittelwerte pro Zeile berechnen
Um Umfragedaten auszuwerten, will man häufig einen Mittelwert pro Zeile berechnen.
Ein Beispiel wäre eine Mitarbeiterbefragung, bei der die Teilnehmer fünf Aussagen zu
ihrer Arbeitszufriedenheit bewerten konnten; die Fragen waren jeweils anzukreuzen mit
Antwortoptionen von „stimme überhaupt nicht zu“ bis „stimme voll und ganz zu“. Die
Antworten werden meist mit Punkten von 1 bis 5 (bei fünf Antwortoptionen) versehen.
Für die Auswertung ist man meist an der „durchschnittlichen“ Antwort interessiert; diese
interpretiert man als die Arbeitszufriedenheit. Diese Herangehensweise ist hier natürlich
stark verkürzt dargestellt (s. Bühner (2011) für eine ausführliche Einführung). In dem Fall
wird eine Zeile, das könnten die Antworten eines Mitarbeiters sein, zusammengefasst zu
einer Zahl. Ein Beispiel aus einer Befragung wäre die Berechnung des ExtraversionsMittelwerts jeder Person. Der häufigste Fall ist, wie gesagt, einen Mittelwert zu bilden für
jede Person. Nehmen wir an, wir haben eine Befragung zur Extraversion durchgeführt und
möchten jetzt den mittleren Extraversionswert pro Person (d. h. pro Zeile) berechnen:
items %>%
row_means(n = .9) %>% head(2)
#> # A tibble: 2 x 11
#>
i01 i02r
i03
i04
i05 i06r
i07
i08
i09
i10 rowmeans
#>
<int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
<dbl>
#> 1
3
3
3
3
4
4
3
2
3
1
2.9
#> 2
2
2
1
2
3
2
2
3
3
1
2.1
sjmisc::row_means() ist dplyr-freundlich, als Eingabe werden die aufzuaddierenden
Spalten verlangt. Gibt man nichts an, so wird der ganze Dataframe als Eingabe angenommen. Dazu muss man noch den Parameter n angeben, der die Anzahl (z. B. n=100) oder
den Anteil (z. B. n=.9) der gültigen Zeilen spezifiziert. Die Funktionen row_sums()
und row_counts() arbeiten analog.
142
9
9.5.2
Praxisprobleme der Datenaufbereitung
Beliebige Statistiken pro Zeile berechnen mit rowwise()
rowMeans() ist praktisch, aber nicht sehr flexibel. So gibt es im Standard-R kein
rowMedian():10 Es gibt verschiedene Wege, um zeilenweise eine Statistik zu berechnen. Ein Weg ist dplyr::rowwise()
select(items, i01, i02r, i03) %>%
rowwise() %>%
mutate(extra_md = median(c(i01, i02r, i03)),
max_row = max(c(i01, i02r, i03)))
#> Source: local data frame [826 x 5]
#> Groups: <by row>
#>
#> # A tibble: 826 x 5
#>
i01 i02r
i03 extra_md max_row
#>
<int> <int> <int>
<int>
<int>
#> 1
3
3
3
3
3
#> 2
2
2
1
2
2
#> 3
3
4
1
3
4
#> # ... with 823 more rows
Übersetzen wir die Syntax ins Deutsche:
Wähle aus items folgende drei Items UND DANN
führe den folgenden Befehl zeilenweise aus UND DANN
berechne den Median und das Maximum über die angegebenen Spalten.
Aufgaben
Richtig oder falsch?11
1.
2.
3.
4.
5.
10
11
Zufälliges Fehlen von Daten (MAR) kann man salopp veranschaulichen mit
„Der Hund hat die Daten aufgefressen“.
Auf fehlende Werte kann man z. B. mit is.na() prüfen.
Bei der Frage, ob man fehlende Werte löschen oder ersetzen sollte, gilt die Regel: Wenn mehr als 5 % der Werte fehlen, so muss man auf jeden Fall ersetzen.
Unter multipler Imputation versteht man das mehrfache Prüfen einer Transformation.
z-Werte haben einen Mittelwert von 1.
Allerdings gibt es Pakete mit diesem Befehl, z. B. miscTools.
F, R, F, F, F, R, F, F, F, R, R, R.
9.5 Vektoren zu Skalaren zusammenfassen
6.
Um vor unliebsamen Überraschungen geschützt zu sein, sollte man Faktorvariablen zuerst in Text- und dann erst in numerische Variablen umwandeln.
7. Kategoriale Variablen können mit cut in numerische umgewandelt werden.
8. Man sollte sich hüten, Vektoren in Skalare umzuwandeln.
9. Der R-Befehl cor berechnet Korrelationstabellen; in der Voreinstellung werden fehlende Werte fallweise ignoriert.
10. Deskriptive Statistik beinhaltet das Zusammenfassen von Vektoren zu Skalaren
als zentralen Teil.
11. In einem Dataframe in Normalform steht in jeder Zeile eine Beobachtung.
12. In einem Dataframe in Normalform steht in jeder Spalte eine Variable.
143
Fallstudie: Datenjudo
10
Lernziele
Daten sicher aufbereiten können
Deskriptive Statistiken nach Bedarf berechnen können
Typische Fragestellungen von einfachen Situationen der angewandten Datenanalyse kennen
Grundlegende Funktionen von dplyr anwenden können
Das Konzept der Pfeife in einem echten Datensatz anwenden können
Auch mit relativ großen Daten sicher hantieren können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(viridis)
library(GGally)
library(corrr)
data(flights, package = "nycflights13")
In diesem Kapitel geht es darum, einige typische Aufgaben der Datenaufbereitung mittels dplyr anhand einer Fallstudie einzuüben. Verwendet wird der Datensatz flights
aus dem Package nycflights13. Der Datensatz ist recht groß (~300000 Zeilen und 19
Spalten); wenn man ihn in Excel importiert, kann eine alte Möhre von Computer schon
in die Knie gehen. Beim Import als CSV in R habe ich noch nie von Problemen gehört;
beim Import via data() ebenfalls nicht. Der Befehl data() lädt Daten aus einem mit
package spezifizierten Paket; gibt man kein Paket an, so werden alle geladenen Pakete nach einem Datensatz des angegebenen Namens durchsucht. glimpse() zeigt einen
Überblick über die Daten.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_10
145
146
10
Fallstudie: Datenjudo
10.1 Deskriptive Statistiken zu den New Yorker Flügen
Stellen Sie sich folgende Situation vor: Sie sind der Assistent des Chefs der New Yorker
Flughäfen. Ihr Chef kommt gut gelaunt ins Büro und sagt, dass er diesen Schnarchnasen
einheizen wolle und dass Sie ihm mal schnell die Flüge mit der größten Verspätung raussuchen sollen. Nix Schickes, aber zacki-zacki . . .
flights %>%
arrange(arr_delay)
#> # A tibble: 336,776 x 19
#>
year month
day dep_time sched_dep_time dep_delay arr_time
#>
<int> <int> <int>
<int>
<int>
<dbl>
<int>
#> 1 2013
5
7
1715
1729
-14
1944
#> 2 2013
5
20
719
735
-16
951
#> 3 2013
5
2
1947
1949
-2
2209
#> # ... with 3.368e+05 more rows, and 12 more variables:
#> #
sched_arr_time <int>, arr_delay <dbl>, carrier <chr>, flight <int>,
#> #
tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,
#> #
distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>
Hm, übersichtlicher wäre es wahrscheinlich, wenn wir weniger Spalten anschauen müssten. Am besten neben der Verspätung nur die Information, die wir zur Identifizierung der
Schuldigen der gesuchten Flüge benötigen:
flights %>%
arrange(arr_delay) %>%
select(arr_delay, carrier, month, day, dep_time,
#> # A tibble: 336,776 x 7
#>
arr_delay carrier month
day dep_time flight
#>
<dbl> <chr>
<int> <int>
<int> <int>
#> 1
-86 VX
5
7
1715
193
#> 2
-79 VX
5
20
719
11
#> 3
-75 UA
5
2
1947
612
#> # ... with 3.368e+05 more rows
flight, dest)
dest
<chr>
SFO
SFO
LAX
Da Zahlen in ihrer natürlichen Form von klein nach groß sortiert sind, sortiert arrange()
in ebendieser Richtung. Wir können das umdrehen mit einem Minuszeichen vor der zu
sortierenden Spalte: arrange(-arr_delay).
Oh halt, wir wollen keine Tabelle mit ca. 300000 Zeilen (der Chef ist kein Freund von
Details). Also begrenzen wir die Ausgabe auf die ersten zehn Plätze, d. h., wir filtern nach
Zeilennummern (row_number()) kleiner 11:
flights %>%
arrange(-arr_delay) %>%
select(arr_delay, carrier, month, day, dep_time, tailnum, flight, dest) %>%
filter(row_number() < 11)
10.1 Deskriptive Statistiken zu den New Yorker Flügen
#>
#>
#>
#>
#>
#>
#>
147
# A tibble: 10 x 8
arr_delay carrier month
day dep_time tailnum flight dest
<dbl> <chr>
<int> <int>
<int> <chr>
<int> <chr>
1
1272 HA
1
9
641 N384HA
51 HNL
2
1127 MQ
6
15
1432 N504MQ
3535 CMH
3
1109 MQ
1
10
1121 N517MQ
3695 ORD
# ... with 7 more rows
„Geht doch“, war die Antwort des Chefs, als Sie die Tabelle ausgedruckt übergeben (er
mag auch keine E-Mails). „Hab ich doch gleich gewusst, dass die Fluggesellschaft „HA“
die Schnarchnasen sind . . . “. Ob dieser Schluss wohl gerechtfertigt ist? „Ach ja“, raunt
der Chef, als Sie das Zimmer verlassen wollen, „hatte ich erwähnt, dass ich die gleiche
Auswertung für jeden Carrier brauche? Reicht bis in einer halben Stunde.“
Wir gruppieren also den Datensatz nach der Fluggesellschaft (carrier) und filtern
dann die ersten drei Zeilen (damit die Tabelle für den Chef nicht zu groß wird). Wie jeder
dplyr-Befehl wird die vorherige Gruppierung berücksichtigt und daher die Top-3-Zeilen
pro Gruppe, d. h. pro Fluggesellschaft, ausgegeben.
flights %>%
arrange(-arr_delay) %>%
select(arr_delay, carrier, month, day, dep_time, tailnum, flight, dest) %>%
group_by(carrier) %>%
filter(row_number() < 4)
#> # A tibble: 48 x 8
#> # Groups:
carrier [16]
#>
arr_delay carrier month
day dep_time tailnum flight dest
#>
<dbl> <chr>
<int> <int>
<int> <chr>
<int> <chr>
#> 1
1272 HA
1
9
641 N384HA
51 HNL
#> 2
1127 MQ
6
15
1432 N504MQ
3535 CMH
#> 3
1109 MQ
1
10
1121 N517MQ
3695 ORD
#> # ... with 45 more rows
Vielleicht gefällt dem Chef diese Darstellung (sortiert nach carrier) besser:
flights %>%
arrange(-arr_delay) %>%
select(arr_delay, carrier, month, day, dep_time, tailnum, flight, dest) %>%
group_by(carrier) %>%
filter(row_number() < 4) %>%
arrange(carrier)
#> # A tibble: 48 x 8
#> # Groups:
carrier [16]
#>
arr_delay carrier month
day dep_time tailnum flight dest
#>
<dbl> <chr>
<int> <int>
<int> <chr>
<int> <chr>
#> 1
744 9E
2
16
757 N8940E
3798 CLT
#> 2
458 9E
7
24
1525 N927XJ
3538 MSP
#> 3
421 9E
7
10
2054 N937XJ
3325 DFW
#> # ... with 45 more rows
148
10
Fallstudie: Datenjudo
Da Sie den Chef gut kennen, berechnen Sie gleich noch die durchschnittliche Verspätung
pro Fluggesellschaft:
flights %>%
select(arr_delay, carrier, month, day, dep_time, tailnum, flight, dest) %>%
group_by(carrier) %>%
summarise(delay_mean = mean(arr_delay, na.rm = TRUE)) %>%
arrange(-delay_mean) %>%
head
#> # A tibble: 6 x 2
#>
carrier delay_mean
#>
<chr>
<dbl>
#> 1 F9
21.9
#> 2 FL
20.1
#> 3 EV
15.8
#> 4 YV
15.6
#> 5 OO
11.9
#> 6 MQ
10.8
Als Sie so über den Daten sitzen, fragen Sie sich: Ob wohl längere Flüge mehr Verspätung
haben? Ein Blick in die Daten lässt noch die Frage aufkommen, was „länger“ bedeutet –
länger in der Luft (air_time) oder größere Distanz zum Ziel (distance)? Die Distanz
zum Ziel erscheint Ihnen besser geeignet, da Zeit in der Luft auch durch Stau am Zielflughafen verursacht sein könnte. Also berechnen Sie die Korrelation der Verspätung mit der
Distanz zum Ziel:
cor(flights$arr_delay, flights$distance, use = "complete.obs")
#> [1] -0.0619
Keine oder kaum Korrelation. Aber gilt das für alle Fluglinien?
flights %>%
group_by(carrier) %>%
summarise(cor_delay_dist = cor(arr_delay, distance, use = "complete.obs")) %>%
filter(abs(cor_delay_dist) > .2)
#> # A tibble: 0 x 2
#> # ... with 2 variables: carrier <chr>, cor_delay_dist <dbl>
Ob überhaupt substanzielle Korrelationen mit der Verspätungszeit vorhanden sind? Gibt
es also Variablen im Datensatz, die mit der Verspätung substanziell korrelieren? Dabei
definieren Sie substanziell als jrj D :2 (Bosco et al. 2015).
flights %>%
select(-year) %>%
select_if(is.numeric) %>%
correlate() %>%
focus(arr_delay) %>%
filter(abs(arr_delay) > .2)
#> # A tibble: 2 x 2
10.2 Visualisierungen zu den deskriptiven Statistiken
149
#>
rowname
arr_delay
#>
<chr>
<dbl>
#> 1 dep_time
0.232
#> 2 dep_delay
0.915
Dass die Verspätung des Fluges bei Ankunft mit der Verspätung bei Abflug sehr stark
korreliert, ist nicht weiter überraschend. Ist es interessant, dass Flüge später am Tag mehr
Verspätung haben? Das lässt sich wohl damit begründen, überlegen Sie, dass sich die Verspätungen über den Tag aufaddieren. Wie man diese Hypothese wohl überprüfen könnte?
10.2 Visualisierungen zu den deskriptiven Statistiken
Sie kennen Ihren Chef. Zahlen verstehen ist nicht immer seins; aber Bilder sprechen ihn
meist an. Daher lesen Sie schnell die Kapitel zur Datenvisualisierung (Kap. 11 und 12)
und erstellen einige Diagramme.
10.2.1 Maximale Verspätung
Zuerst erstellen Sie ein Diagramm, welches die Flüge mit den größten Verspätungen zeigt,
auf Basis Ihrer deskriptiven Auswertung (s. Abb. 10.1, linker Teil).
flights %>%
arrange(-arr_delay) %>%
select(arr_delay, carrier, month, day, dep_time, tailnum, flight, dest) %>%
filter(between(row_number(),1, 10)) %>%
ggplot() +
aes(x = reorder(tailnum, -arr_delay), y = arr_delay, fill = arr_delay) +
geom_col() +
scale_fill_viridis(direction = -1)
Die Syntax lässt sich so ins Deutsche übersetzen:
Nimm den Datensatz flights UND DANN
sortiere absteigend nach der Spalte arr_delay UND DANN
wähle die mir genehmen Spalten aus UND DANN
filtere die Zeilen zwischen (between) 1 und 10 UND DANN
male ein Bild mit ggplot UND DANN
definiere die Achsen und Füllfarbe (die aesthetik) wobei
die Flugzeuge (tailnum) nach arr_delay sortiert sein sollen UND DANN
male das Balken-Geom UND DANN
fülle die Balken mit dem Farbschema des Pakets viridis wobei
die Farben in umgekehrter Reihenfolge gezeigt werden sollen. Fertig.
N384HA
N504MQ
N517MQ
N338AA
N665MQ
N959DL
N927DA
N6716C
N5DMAA
0
Verspätung
500
1000
900
1000
1100
1200
arr_delay
5
10
15
20
F9
FL
EV
YV
MQ
Fluggesellschaft
OO
Top-10 der Verspätungs-Airlines
WN
B6
9E
UA
10
Abb. 10.1 Verspätung von Flügen
Flugzeug
N523MQ
Top-10 der verspäteten Flugzeuge
150
Fallstudie: Datenjudo
Verspätung
10.2 Visualisierungen zu den deskriptiven Statistiken
151
Die bunte Füllung ist ehrlicherweise überflüssig bzw. redundant, da schon durch die
Höhe der Balken dargestellt. Aber Sie kennen Ihren Chef; außerdem kommt die Farbabstufung auch bei Schwarz-Weiß-Ausdrucken gut raus. Das hier verwendete Farbschema
heißt Viridis und ist in Abschn. 12.1.3 näher vorgestellt.
10.2.2
Durchschnittliche Verspätung
Als Nächstes möchten Sie ein Diagramm erstellen, um die mittlere Verspätung nach Fluggesellschaft darzustellen. Wo war noch mal Ihre Analyse der relevanten VerspätungsStatistiken? Ach ja, hier:
flights %>%
select(arr_delay, carrier, month, day, dep_time, tailnum, flight, dest) %>%
group_by(carrier) %>%
summarise(delay_md = median(arr_delay, na.rm = TRUE),
delay_mean = mean(arr_delay, na.rm = TRUE),
delay_sd = sd(arr_delay, na.rm = TRUE),
delay_iqr_lower = quantile(arr_delay, na.rm = TRUE, probs = .25),
delay_iqr_upper = quantile(arr_delay, na.rm = TRUE, probs = .75),
delay_count = n()) %>%
ungroup() -> flights_summary
Für jede Fluggesellschaft haben Sie die typischen Statistiken für die Verspätung berechnet;
außerdem noch die Anzahl der Flüge, die pro Fluggesellschaft in die Kennzahl einging.
Das Ergebnis zeigt Abb. 10.1, rechter Teil.
flights_summary %>%
ggplot() +
aes(x = reorder(carrier, -delay_mean), y = delay_mean) +
geom_point() +
labs(title = "Verspätung nach Fluggesellschaft",
x = "Fluggesellschaft",
y = "Verspätung") +
coord_flip() +
theme(title = element_text(size = rel(0.7))) -> p_delay2
Dieses Mal zeichnen Sie keine Balken, sondern nur Punkte, da Punkte die gleiche Information mit weniger Tinte und damit cooler eleganter darstellen; davon hatten Sie mal
gelesen (Cleveland 1993).
p_delay3 <- flights_summary %>%
ggplot() +
aes(x = reorder(carrier, -delay_md),
y = delay_md) +
152
10
arr_delay
air_time
Cor : -0.209
AS: 0.423
F9: 0.174
distance
dep_time
carrier
Cor : -0.301
Cor : 0.305
AS: 0.288
AS: NA
F9: 0.318
F9: NA
arr_delay
Cor : 0.951
Cor : -0.138
S: 0.0702
AS: NA
F9: NA
9: -0.141
air_time
Cor : -0.144
AS: NA
F9: NA
distance
0.015
0.010
0.005
0.000
400
350
300
250
200
2400
2200
2000
1800
1600
Fallstudie: Datenjudo
dep_time
2000
1500
1000
500
0
300
200
100
0
300
200
100
0
carrier
0 250 500 750 200 250 300 350 400
160018002000220024000 500100105020000
AS
F9
Abb. 10.2 Streudiagramm-Matrix
geom_point(aes(size = delay_count,
color = delay_count)) +
geom_errorbar(aes(ymin = delay_iqr_lower,
ymax = delay_iqr_upper),
color = "grey60") +
labs(title = "Verspätung nach Fluggesellschaft",
x = "Fluggesellschaft",
y = "Median der Verspätung",
caption = paste0("Größe und Farbe der Punkte",
"spiegeln die Anzahl der Flüge wider")) +
theme(title = element_text(size = rel(0.7)))
10.2.3 Korrelate der Verspätung
Die Frage nach den Korrelaten der Verspätung hat Ihrem Chef gefallen. Also visualisieren
Sie ihm noch die Korrelation der Verspätung; dafür könnten Sie die schon berechneten Korrelationswerte als Punkte visualisieren. Alternativ und informationsreicher kann
man die Streudiagramme für jede Korrelation visualisieren; sozusagen eine Matrix an
Streudiagrammen. Hier hilft das Paket GGally. Dabei möchten Sie noch zwei Airlines
vergleichen, nämlich eine mit geringer mittlerer Verspätung (AS) und eine mit viel mittlerer Verspätung (F9). Abb. 10.2 zeigt das Ergebnis.
flights %>%
select(arr_delay, air_time, distance, dep_time, carrier) %>%
10.2 Visualisierungen zu den deskriptiven Statistiken
filter(carrier %in% c("F9", "AS")) %>%
ggpairs(aes(color = carrier, fill = carrier))
Der Chef ist zufrieden. Sie können sich wieder wichtigeren Aufgaben zuwenden . . .
praise::praise()
#> [1] "You are bedazzling!"
Aufgaben
Richtig oder falsch?1
1.
Möchte man einen Dataframe sortieren, so kann man anstelle von arrange()
auch count(..., sort = TRUE) verwenden.
2. Möchte man eine Spalte mit der Häufigkeit einer Gruppe group an einen Dataframe anhängen, so kann man den Befehl add_count(group) verwenden,
wie die Hilfeseite von count() zeigt.
3. Möchte man mehrere Spalten mit select() auswählen und stehen die Spalten
im Dataframe nebeneinander, so kann man praktischerweise bei select schreiben: select(df, spalte1:spalte10).
4. Der Befehl filter(row_number() < 4) hat den gleichen Effekt, unabhängig davon, ob zuvor eine Gruppierung (mit group_by()) durchgeführt
wurde.
5. Möchte man nur Flüge der deutschen Lufthansa filtern, so kann man diese Syntax verwenden: filter(flights, str_detect(carrier, "LHH")).
6. Die Lufthasa startete laut unserem Datensatz im Jahr 2013 mehr als einmal aus
New York.
7. Möchte man cor() nahebringen, dass fehlende Werte ignoriert werden sollen,
so ist das Argument rm.na = TRUE zu verwenden.
8. Möchte man mit ggplot ein Balkendiagramm nach absteigenden Balken sortieren, so ist dieser Befehl korrekt: aes(x = reorder(tailnum,
-arr_delay)). Dabei steht das Nummernschild auf der X-Achse; die Balken werden absteigend nach Verpätung sortiert.
9. Der Befehl summarise() erlaubt nicht nur Zusammenfassungen zu einem
einzelnen Wert, sondern es dürfen auch längere Vektoren zurückgegeben werden.
10. Berechnet man die Korrelation von Flugdauer und Zieldistanz, so würde die
Korrelation sinken, wenn man nur Flüge mit ähnlicher Flugdauer berücksichtigte (die Varianz der Flugdauer also deutlich eingeschränkt wäre).
1
R, R, R, F, R, F, F, R, F, R.
153
Teil IV
Daten visualisieren
11
Datenvisualisierung mit ggplot2
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
Lernziele
An einem Beispiel erläutern können, warum/wann ein Bild mehr sagt als 1000
Worte
Häufige Arten von Diagrammen erstellen können
Diagramme bestimmten Zwecken zuordnen können
Die Syntax von ggplot2 in Grundzügen beherrschen
Beispiele für optisch ansprechende und weniger ansprechende Diagramme aufzeigen können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(mosaic)
data(flights, package = "nycflights13")
data(movies, package = "ggplot2movies")
data(profiles, package = "okcupiddata")
data(stats_test, package = "pradadata")
data(wo_men, package = "pradadata")
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_11
157
158
11
Datenvisualisierung mit ggplot2
Dieses Kapitel erläutert das Visualisieren von Daten; zentrales Werkzeug dafür ist das
R-Paket ggplot2.
11.1 Einstieg in ggplot2
11.1.1 Ein Bild sagt mehr als 1000 Worte
Ein Bild sagt bekanntlich mehr als 1000 Worte. Schauen wir uns zur Verdeutlichung Herrn
Anscombes (1973) berühmtes Beispiel an. Es geht hier um vier Datensätze mit zwei Variablen (Spalten X und Y). Berechnen wir typische Statistiken:
#>
x1_mean x2_mean x3_mean x4_mean y1_mean y2_mean y3_mean y4_mean x1_sd
#> 1
9
9
9
9
7.5
7.5
7.5
7.5 3.32
#>
x2_sd x3_sd x4_sd y1_sd y2_sd y3_sd y4_sd
#> 1 3.32 3.32 3.32 2.03 2.03 2.03 2.03
#>
cor1 cor2 cor3 cor4
#> 1 0.816 0.816 0.816 0.817
Offenbar sind die Datensätze praktisch identisch: Alle X haben den gleichen Mittelwert
und die gleiche Standardabweichung; dasselbe gilt für die Y. Die Korrelation zwischen X
und Y ist in allen vier Datensätzen gleich. Allerdings erzählt eine Visualisierung der vier
Datensätze eine ganz andere Geschichte (s. Abb. 11.1).
Erst die Visualisierung zeigt: Es „passieren“ in den vier Datensätzen gänzlich unterschiedliche Dinge. Dies haben die reinen Zahlen (Statistiken) nicht aufgedeckt; die
Visualisierung erhellte uns. Kurz: Die Visualisierung ist ein unverzichtbares Werkzeug,
um zu verstehen, was in einem Datensatz (und damit in der zugrunde liegenden „Natur“)
passiert.1
11.1.2 Diagramme mit ggplot2 zeichnen
Es gibt viele Möglichkeiten, Daten zu visualisieren (in R). Wir werden uns hier auf einen
Weg bzw. ein Paket konzentrieren, der komfortabel, aber mächtig ist und gut zum Prinzip
des Durchpfeifens passt: ggplot2.2 Diagramme mit ggplot2 können sehr ansehnlich
sein. Abb. 11.2 zeigt im linken Diagramm das Wetter in New York im Zeitverlauf (Bresler
1
Eine coole Variante mit der gleichen Botschaft findet sich hier: https://www.autodeskresearch.
com/publications/samestats; bzw. mit einer Animation hier: https://d2f99xq7vri1nk.cloudfront.net/
DinoSequentialSmaller.gif; vgl. Matejka und Fitzmaurice (2017).
2
„gg“ steht für „grammer of graphics“ nach einem Buch von Wilkinson (2006); „plot“ steht für „to
plot“, also ein Diagramm erstellen („plotten“); vgl. https://en.wikipedia.org/wiki/Ggplot2.
11.1
Einstieg in ggplot2
159
Data 1
Data 2
10.0
y2
y1
10.0
7.5
7.5
5.0
5.0
6
9
12
6
9
x1
Data 3
Data 4
12.5
15.0
10.0
12.5
y4
y3
12
x2
7.5
10.0
7.5
5.0
6
9
12
5.0
7.5
10.0
x3
12.5
15.0
17.5
x4
Abb. 11.1 Das Anscombe-Quartett
2015); es ist eine Umsetzung eines berühmten Diagramms von Edward Tufte3 (Tufte 1990,
2001, 2006). Das rechte Diagramm zeigt eine Choroplethen-Karte der Bevölkerungsdichte
der USA („ggcounty“ 2014). Für beide Diagramme ist die R-Syntax öffentlich und über
die Quellenangabe auffindbar.
11.1.3 Die Anatomie eines Diagramms
ggplot2 unterscheidet folgende Bestandteile („Anatomie“) eines Diagramms (vgl.
Abb. 11.3):
Daten
Abbildende Aspekte (Achsen, Farben . . . )
Geome (statistische Bilder wie Punkte, Linien, Boxplots . . . )
3
Edward Tufte gilt als Grand Seigneur der Datenvisualisierung; er hat mehrere lesenswerte Bücher
zu dem Thema geschrieben. Auch Clevelands Ansichten sind weitflächig rezipiert worden (vgl.
Cleveland (1993)).
11
Abb. 11.2 Beispiele für Diagramme mit ggplot2
160
Datenvisualisierung mit ggplot2
11.1
Einstieg in ggplot2
161
Klausurerfolg
Lernzeit
Punkte in
Klausur
Anna
10
30
Berta
30
60
Carla
20
90
Dora
50
120
Daten
Klausurerfolg
Lernzeit
Lernzeit
Achsen
Geom
Abb. 11.3 Anatomie eines Diagramms
Abb. 11.4 Sinnbild der Zuordnung von Spalten zu Attributen
eines Diagramms
Dataframe
Diagramm
Spalten
Attribute
X
X-Achse
Y
Y-Achse
Z
Farbe
Die Daten müssen als Dataframe angegeben werden. Zu den abbildenden Aspekten (in
ggplot2 als „aesthetics“ bzw. aes() bezeichnet) zählen vor allem die Achsen, aber
auch Farben u. a. Was ist mit abbildend gemeint? Weist man einer Achse eine Variable zu, so wird jede Ausprägung der Variablen einer Ausprägung der Achse zugeordnet
(welcher Wert genau, entscheidet ggplot2 für uns, wenn wir es nicht explizieren). Mit
Geom ist die eigentliche Art von „Bild“ gemeint, wie Punkt, Linie oder Boxplot (vgl.
Abschn. 11.2). Erstellt ggplot2 ein Diagramm, so ordnet es Spalten den Bestandteilen
des zu erzeugenden Diagramms zu. Dieser Vorgang wird als Mapping (Zuordnung) bezeichnet (s. Abb. 11.4).
11
2.0e+08
2.0e+08
1.5e+08
1.5e+08
budget
budget
162
1.0e+08
5.0e+07
1.0e+08
5.0e+07
0.0e+00
1890
Datenvisualisierung mit ggplot2
0.0e+00
1920
1950
1980
2010
111181818181
981
981
981
93
1
94
1
95
1
96
1
09
7
1
09
8
1
091
091
09
109
2
1
09
3
1
09
4
1
09
5
1
09
6
19
7
19
8
1919
0
19
19
2
19
3
19
4
19
5
19
6
1
29
7
1
29
8
1
291
29
0
1
29
1291
29
3
1
29
4
1
29
5
1
29
6
1
39
7
1
39
8
1
391
39
0
1
39
139
2
1
391
39
4
1
39
5
1
39
6
1
49
7
1
49
8
1
491
49
0
1
49
149
2
1
49
3
1
491
49
5
1
49
6
1
59
7
1
59
8
1
591
59
0
1
59
159
2
1
59
3
1
59
4
1
591
59
6
19
7
1
69
8
1
691
69
0
1
69
169
2
1
69
3
1
69
4
1
69
5
1
691
791
79
8
1
791
79
0
1
79
179
2
1
79
3
1
79
4
1
79
5
1
79
6
1
89
7
1
891
891
89
0
1
89
189
2
1
89
3
1
89
4
1
89
5
1
89
6
19
7
19
8
1929
0
29
1
29
20
3
2
90
4
2
90
590
60
70
8900102345
year
factor(year)
Abb. 11.5 Mittleres Budget pro Jahr
Aufgaben
Los geht’s! Beginnen wir mit dem Datensatz movies. Wie kann man sich einen
Überblick zu dem Datensatz ausgeben lassen?4 Nicht vergessen, dass der Datensatz
geladen sein muss, bevor Sie auf ihn zugreifen können. Betrachten Sie zum Einstieg
das Diagramm 11.5, linke Seite; hier einige Aufgaben zum Aufwärmen:5
1.
2.
3.
4.
Welche Variable steht auf der X-Achse?
Welche Variable steht auf der Y-Achse?
Was wird gemalt? Linien, Boxplots, Punkte?
Wie heißt der Datensatz, aus dem die Daten gezogen werden?
11.1.4 Schnell Diagramme erstellen mit qplot()
Der Befehl, der dieses Diagramm erzeugte, heißt qplot. Es ist ziemlich genau die Antwort auf die Übungsfragen von gerade eben:
„q“ in qplot steht für „quick“. Tatsächlich hat qplot() einen großen Bruder,
ggplot()6 , der deutlich mehr Funktionen aufweist – und daher auch die umfangreichere
(komplexere) Syntax. Schauen wir uns den Befehl qplot etwas näher an. Wie ist er
aufgebaut?
4
Mit glimpse(movies).
1: year; 2: budget; 3: Punkte; 4: movies.
6
Achtung, man kann sich leicht vertippen: Nicht qqplot(), nicht ggplot2(), nicht gplot(),
sondern qplot() ist der richtige Name des Befehls.
5
11.1
Einstieg in ggplot2
163
qplot: Erstelle schnell (q wie quick in qplot) mal einen Plot (engl. „plot“: Diagramm).
x: Der X-Achse soll die Variable „year“ zugeordnet werden.
y: Der Y-Achse soll die Variable „budget“ zugeordnet werden.
geom: („geometriches Objekt“) Gemalt werden sollen Punkte, und zwar
pro Beobachtung (hier: Film) ein Punkt; nicht etwa Linien oder Boxplots.
data: Als Datensatz bitte movies verwenden.
Sie können auch mehrere Geome übereinander zeichnen; probieren Sie mal geom =
c("point", "smooth"). Betrachten Sie Abb. 11.5, linke Seite: Offenbar ging die
Schere in den Budgets mit der Zeit auseinander; in jüngeren Jahren sind die Budgets
unterschiedlicher geworden. Außerdem scheint das Budget im Lauf des Jahres größer geworden zu sein. Genau kann man es aber schlecht erkennen in diesem Diagramm. Besser
ist es vielleicht, die Daten pro Jahr zusammenzufassen in einem Geom und dann diese
Geome zu vergleichen (s. Abb. 11.5, rechte Seite):
qplot(x = factor(year),
y = budget,
geom = "boxplot",
data = movies)
Übrigens: factor(year) wird benötigt, um aus year eine nominalskalierte Variable
zu machen. Nur bei nominalskalierten Variablen auf der X-Achse zeichnet qplot()
mehrere Boxplots nebeneinander. qplot bzw. ggplot2 denkt sich: „Hey, nur wenn es
mehrere Gruppen gibt, macht es Sinn, die Gruppen anhand von Boxplots zu vergleichen.
Also brauchst du eine Gruppierungsvariable (Faktor oder Text) auf der X-Achse!“ Es
sind zu viele Jahre, das macht das Diagramm unübersichtlich. Besser wäre es, Jahrzehnte
dazustellen. Ein Jahrzehnt ist so etwas wie eine Jahreszahl, von der die letzte Ziffer abgeschnitten (d. h. durch 10 teilen und abrunden) und dann durch eine Null ersetzt wurde
(d. h. mit 10 multiplizieren):
movies %>%
mutate(Jahrzehnt
mutate(Jahrzehnt
mutate(Jahrzehnt
mutate(Jahrzehnt
=
=
=
=
year / 10) %>%
trunc(Jahrzehnt)) %>% # trunkieren, abrunden
Jahrzehnt * 10) %>%
factor(Jahrzehnt)) -> movies
Schauen Sie sich die ersten Werte von Jahrzehnt mal an: movies %>% select
(Jahrzehnt) %>% head. O. K., auf ein neues Bild (Abb. 11.6):
164
11
Datenvisualisierung mit ggplot2
qplot(x = Jahrzehnt,
y = budget,
geom = "boxplot",
group = Jahrzehnt,
data = movies) + geom_smooth(aes(group = 1), se = FALSE)
Weitere Geome kann man auch mit geom_XXX() hinzufügen. Warum haben wir das hier
so gemacht und nicht die kürzere Methode wie eben verwendet? Der Grund ist, dass wir
mit geom_XXX() die Argumente des Geoms, seine Feinheiten also, festlegen können. Mit
dem bisherigen Weg können wir nur die voreingestellten Werte verwenden. Die voreingestellten Werte würden hier dazu führen, dass für jedes Jahrzehnt eine eigene Glättungslinie
erzeugt würde. Die hätte aber Länge null, wäre also nicht sichtbar. Warum würde hier in
der Voreinstellung für jede Ausprägung von Jahrzehnt eine Gruppe angenommen werden,
für die eine eigene Glättungslinie gezeichnet wird? Der Grund ist, dass der Boxplot die
Werte bereits gruppiert hat. Der Boxplot hat alle Filme (Werte) eines Jahrzehnts in eine
Gruppe eingeteilt. Wir müssen die Gruppierung wieder ändern und R klar machen, dass
wir keine Gruppen bzw. nur noch eine Gruppe, die alle Werte enthält, haben möchten,
wenn das Geom smooth gezeichnet wird. Mit se = FALSE schalten wir den grauen „Schleier“ der Glättungslinie ab (SE steht für den Standardfehler). Interessanterweise
sanken die Budgets gegen Ende unserer Datenreihe; das ist aber vielleicht nur ein Zufallsrauschen. Diese Syntax des letzten Beispiels ist recht einfach; allgemein kann man sie in
folgender „Blaupause“ formulieren. Diese Blaupause ist prototypisch für qplot, es lohnt
sich daher, die Syntax zu verstehen:
qplot (x = X_Achse,
y = Y_Achse,
data = mein_dataframe,
geom = "ein_geom")
Wir definieren mit x, welche Variable der X-Achse des Diagramms zugewiesen werden
soll, z. B. Jahrzehnt; analog geschieht das mit der Y-Achse. Mit data sagen wir, in
welchem Dataframe die Spalten (Variablen) „wohnen“, und als „Geom“ ist die Art des
statistischen „geometrischen Objekts“ gemeint, also Punkte, Linien, Boxplots, Balken etc.
11.1.5 ggplot-Diagramme mit mosaic
mosaic bietet eine komfortable Möglichkeit, auf ggplot-Diagramme zuzugreifen.7 Das
Praktische ist, dass man sich auf die Syntax von mosaic verlassen kann; man muss
sich kaum neue Syntax merken (s. Abschn. 8.1.1 für weitere Hinweise zu mosaic). Dia-
7
mosaic bietet auch Diagramme vom Typ lattice.
11.1
Einstieg in ggplot2
165
2.0e+08
budget
1.5e+08
1.0e+08
5.0e+07
0.0e+00
1890
1900
1910
1920
1930
1940
1950
1960
1970
1980
1990
2000
Jahrzehnt
Abb. 11.6 Film-Budgets über die die Jahrzehnte
gramm 11.6 lässt sich mit mosaic so anfordern:
gf_boxplot(budget ~ factor(Jahrzehnt), data = movies)
Generell ist die Syntax gf_XXX, wobei XXX das Geom bezeichnet, das gezeigt werden soll, also z. B. point, boxplot oder histogram. Die Namen der Geome sind
die gleichen wie bei ggplot(). Die Syntax-Blaupause von mosaic bzw. von ggplotDiagrammen mit mosaic lautet:
gf_XXX(y_achse ~ x_achse, data = meine_tabelle)
Praktisch ist auch die Summary-Funktion von gf_XXX(). Was ist das mittlere Budget
pro Jahrzehnt?
gf_boxplot(budget ~ factor(Jahrzehnt), data = movies) %>%
gf_point(stat = "summary", color = "red", shape = 17)
Mit shape kann man die Form des Geoms Punkt auswählen, z. B. Dreiecke oder Quadrate.
Aufgaben
1. Laden Sie den Datensatz mit dem Befehl data(anscombe); wie viele Variablen gibt es und von welchem Typ sind sie?8
2. Mit welchem Befehl kann man sich auf einen Blick einen Überblick über den
Datensatz anscombe verschaffen?9
8
9
8 Variablen, alle numerisch.
mosaic::inspect(anscombe).
166
11
Datenvisualisierung mit ggplot2
3. Berechnen Sie Mittelwerte und SD von x und y in jedem der vier Teil-Datensätze
von anscombe.10
4. Berechnen Sie die Korrelation von x mit y in jedem der vier Datensätze.11
5. Was fällt Ihnen auf, wenn Sie die deskriptiven Statistiken zum Datensatz Anscombe betrachten?12
6. Nennen Sie drei Geome, die mit mosaic über gf_XXX() erstellt werden können.13
7. Legen Sie eine Glättungslinie über das Diagramm in Abb. 11.6.14
8. Ist das Diagramm mit mosaic in diesem Fall komplett identisch?15
9. Bauen Sie das Diagramm in Abb. 11.5 (links) mit gf_XXX nach.16
10. Mit der Pfeife kann man mehrere dieser Diagramme (bzw. Geome) übereinander
legen. Bauen Sie das Diagramm in Abb. 11.5 (rechts) mit der Pfeife nach.17
11.2 Häufige Arten von Diagrammen (Geomen)
Unter den vielen Arten von Diagrammen und vielen Arten, diese zu klassifizieren, greifen wir uns ein paar häufige Diagramme heraus. Die Arten von Diagrammen richten wir
im Folgenden an den beiden Achsen eines typischen Diagramms aus. Mit Diagramm ist
hier die Art der Visualisierung gemeint: Balken, Punkte, Linien und so weiter. ggplot2
bezeichnet diese als Geome.
11.2.1 Eine kontinuierliche Variable – Histogramme und Co.
Schauen wir uns die Verteilung von Filmbudgets aus movies an (s. Abb. 11.7, links).
qplot(x = budget, data = movies)
Weisen wir nur der X-Achse (aber nicht der Y-Achse) eine kontinuierliche Variable
zu, so wählt ggplot2 automatisch als Geom automatisch ein Histogramm; wir müs10
Für jede der relevanten Variablen kann ein Befehl dieser Art verwendet werden: mosaic::sd(
~ y1, data = anscombe).
11
mosaic::cor(x1 ~ y1, data = anscombe).
12
Die deskriptiven Statistiken für x bzw. y sind praktisch identisch zwischen den vier TeilDatensätzen.
13
Zum Beispiel gf_abline(), gf_density(), gf_histogram(), gf_boxplot().
14
%>% gf_smooth(group = ~1).
15
Nein, die Glättungslinie ist eine Gerade bei mosaic.
16
gf_point(budget ~ year, data = movies).
17
gf_point(budget ~ year, data = movies) %>% gf_smooth().
11.2
Häufige Arten von Diagrammen (Geomen)
167
9e-08
2000
6e-08
1000
3e-08
0
0e+00
0.0e+00 5.0e+07 1.0e+08 1.5e+08 2.0e+08
0.0e+00 5.0e+07 1.0e+08 1.5e+08 2.0e+08
budget
budget
Abb. 11.7 Verteilung des Budgets von Filmen mit einem Histogramm
sen daher nicht explizieren, dass wir ein Histogramm als Geom wünschen (aber wir
könnten es hinzufügen). Alternativ wäre ein Dichtediagramm hier von Interesse (s.
Abb. 11.7, rechter Teil). Das bekommen Sie mit der Syntax qplot(x = budget,
geom = "density", data = movies). Beachten Sie, dass das Histogramm auf
der Y-Achse die Häufigkeiten angibt, während das Dichtediagramm die Dichte angibt.
Die Dichte ist ein Maß, das angibt, wie viele Beobachtungen pro Budget-Einheit (Dollar)
vorliegen. Was man sich merken muss, ist, dass hier nur das Geom mit Anführungsstrichen
zu benennen ist, die übrigen Parameter ohne.
Was heißt das kleine „e“, das man bei wissenschaftlichen Zahlen hin und wieder
sieht (wie im Diagramm 11.7)?
Zum Beispiel: 5.0e+07. Das e sagt, wie viele Stellen im Exponenten (zur Basis
10) stehen: hier 1007 . Eine große Zahl – eine 1 gefolgt von sieben Nullen: 10000000.
Die schöne Zahl soll noch mit 5 multipliziert werden: also 50000000. Bei so vielen
Nullern kann man schon mal ein Flimmern vor den Augen bekommen. Daher ist
die „wissenschaftliche“ Notation ganz praktisch, wenn die Zahlen sehr groß (oder
sehr klein) werden. Sehr kleine Zahlen werden mit dieser Notation so dargestellt:
5.0e-07 heißt 1017 . Eine Zahl sehr nahe bei Null. Das Minuszeichen zeigt hier,
dass wir den Kehrwert von 5.0e+07 nehmen sollen.
11.2.2
Zwei kontinuierliche Variablen
Ein Streudiagramm ist die klassische Art, zwei metrische Variablen darzustellen. Das ist mit
qplot einfach: p1 <- qplot(x = budget, y = rating, data = movies).
Das Diagrammobjekt ist jetzt in der Variablen p1 gespeichert. Durch Eingabe des Namens
p1 wird das Diagramm am Bildschirm gedruckt Wir weisen hier wieder der X-Achse und
der Y-Achse eine Variable zu; handelt es sich in beiden Fällen um Zahlen, so wählt
168
11
10.0
2.0e+08
7.5
1.5e+08
Datenvisualisierung mit ggplot2
rating
budget
count
500
400
1.0e+08
300
200
5.0e+07
100
2.5
0.0e+00
0.0e+00 5.0e+07 1.0e+08 1.5e+08 2.0e+08
budget
1920 1950 1980 2010
year
Abb. 11.8 Visualisierung zweier metrischer Variablen
ggplot2 automatisch ein Streudiagramm – d. h. Punkte als Geom (geom = "point").
Es ist nicht wirklich ein Trend erkennbar: Teure Filme sind nicht unbedingt beliebter bzw.
besser bewertet (s. Abb. 11.8, links). Die horizontale Trendgerade (Regressionsgerade)
untermauert das Fehlen eines (starken) Trends.
p_budget_rating_lm <- p_budget_rating + geom_smooth(method = "lm")
Synonym könnten wir auch schreiben:
movies %>%
ggplot() +
aes(x = budget, y = rating) +
geom_point() +
geom_smooth(method = "lm")
Da ggplot() als ersten Parameter die Daten erwartet, kann die Pfeife hier problemlos
durchgereicht werden. Innerhalb eines ggplot()-Aufrufs werden die einzelne Teilbefehle durch ein Pluszeichen + voneinander getrennt. Nachdem wir den Dataframe benannt haben, definieren wir die Zuordnung der Variablen zu den Achsen mit aes(). Ein
„Smooth-Geom“ ist eine Linie, die sich schön an die Punkte anschmiegt, in diesem Falls
als Gerade (lineares Modell, lm()).
Innerhalb einer ggplot-Syntax dürfen Sie die Pfeife nicht verwenden; trennen Sie
die einzelnen Teilbefehle mit einem Pluszeichen.
Bei sehr großen Datensätzen sind Punkte unpraktisch, da sie sich überdecken (Overplotting). Eine Abhilfe ist es, die Punkte nur „schwach“ zu färben. Dazu stellt man die
„Füllstärke“ der Punkte über alpha ein: z. B. geom_point(alpha = 1/100). Um
einen passablen Alpha-Wert zu finden, bedarf es etwas Probierens. Zu beachten ist, dass
es mitunter recht lange dauert, wenn ggplot2 viele (z. B. > 100000) Punkte malen soll.
11.2
Häufige Arten von Diagrammen (Geomen)
169
1990
2000
10.0
rating
7.5
5.0
2.5
0.0e+00
5.0e+07
1.0e+08
1.5e+08
2.0e+08
0.0e+00
5.0e+07
1.0e+08
1.5e+08
2.0e+08
budget
Abb. 11.9 Facettierung eines Diagramms
Bei noch größeren Datenmengen bietet sich an, den Scatterplot als „Schachbrett“ aufzufassen und das Raster einzufärben, je nach Anzahl der Punkte pro Schachfeld; zwei
Geome dafür sind geom_hex() und geom_bin2d() (s. Abb. 11.8, rechts).
p_hex <- ggplot(movies) +
aes(x = year, y = budget) +
geom_hex()
Möchte man ein Diagramm in Subgruppen aufteilen, z. B. Filme nach Jahrzehnten, so
kann man den ggplot-Befehl facet_wrap() verwenden (s. Abb. 11.9). Man beachte die
Tilde ~, die vor die „Gruppierungsvariable“ Jahrzehnt zu setzen ist. Mit geom_rug()
bekommt man Hinweise zur univariaten Häufigkeitsverteilung, d. h. wie häufig jeder Wert
von budget bzw. rating ist. Im linken Teil des Diagrammes sinkt die Trendlinie bei
höheren Budgetwerten ab; allerdings wird die Fallzahl dann auch kleiner. Eine kleinere
Fallzahl macht die zugrunde liegende Population schlechter einschätzbar; der Schätzbereich wird größer bzw. die Schätzung ungenauer; das ist mit dem grauen „Schleier“
(Glättungslinien, Smoother oder LOESS genannt) visualisiert.
movies %>%
filter(Jahrzehnt %in% c("1990", "2000")) %>%
ggplot(aes(x = budget, y = rating, color = Jahrzehnt)) +
geom_point(alpha = .5) +
facet_wrap(~Jahrzehnt) +
geom_smooth() +
geom_rug() +
theme(legend.position = "none")
11.2.3 Eine oder zwei nominale Variablen
Bei nominalen Variablen geht es in der Regel darum, Häufigkeiten auszuzählen. Ein Klassiker: Wie viele Männer und Frauen finden sich in dem Datensatz? Eine andere Frage,
170
11
Datenvisualisierung mit ggplot2
1000
150
750
factor(interest)
100
500
1
6
50
250
0
0
ja
nein
bestanden
ja
nein
bestanden
Abb. 11.10 Visualisierung von Häufigkeiten mit Balkendiagrammen
beruhend auf dem Datensatz stats_test (bitte laden): Wie viele Studenten haben die
Klausur (nicht) bestanden (s. Abb. 11.10, links)?
stats_test %>% drop_na(interest, score) -> stats_test
p_bar1 <- qplot(x = bestanden, data = stats_test)
p_bar1
Falls nur die X-Achse definiert ist und dort eine Faktorvariable oder eine Textvariable steht, dann nimmt qplot automatisch ein Balkendiagramm als Geom (es steht uns
frei, trotzdem geom = bar anzugeben). Wir könnten uns jetzt die Frage stellen, wie
viele Nicht-Interessierte und Hoch-Interessierte es in der Gruppe, die bestanden hat
(bestanden == "ja"), gibt; entsprechend für die Gruppe, die nicht bestanden hat
(s. Abb. 11.10, rechts).
p_bar2 <- qplot(x = bestanden, fill = factor(interest),
data = stats_test)
p_bar2
Für p_bar2 haben wir qplot() mitgeteilt, dass die Balken entsprechend der Häufigkeit
von interest gefüllt werden soll. Damit qplot() (und ggplot()) sich bequemt, die
Füllung umzusetzen, müssen wir aus interest eine nominalskalierte Variable machen –
factor() macht das für uns. Schön wäre noch, wenn die Balken Anteile (Prozentwerte)
angeben würden. Das geht mit qplot() (so einfach) nicht; wir schwenken auf ggplot
um. Und, um die Story zuzuspitzen, schauen wir uns nur die Extremwerte von interest
an (s. Abb. 11.11, links). Eine einfache Möglichkeit, Zusammenhänge nominaler Variablen darzustellen, bietet die Funktion mosaicplot(), die im Standard-R enthalten ist (s.
Abb. 11.11, rechts). Im Unterschied zu normalen Balkendiagrammen wird die Breite der
Balken der Häufigkeit des angezeigten Wertes zugeordnet; so ist der Wert 3 der häufigste
Wert von interest, da der entsprechende Balken der breiteste ist.
Abb. 11.11 Balkendiagramme, um Anteile darzustellen
11.2
Häufige Arten von Diagrammen (Geomen)
171
172
11
Datenvisualisierung mit ggplot2
p_bar3 <- stats_test %>%
filter(interest == 1 | interest == 6) %>%
mutate(interest = factor(interest)) %>%
ggplot() +
aes(x = bestanden, fill = interest) +
geom_bar(position = "fill") +
labs(fill = "Interesse",
y = "Anzahl")
p_bar3
mosaicplot(interest ~bestanden, data = stats_test,
main = NULL)
Der Lehrer freut sich: In der Gruppe, die bestanden hat, ist der Anteil der Freaks HochInteressierten größer als bei den Durchfallern (s. Abb. 11.11, links). Schauen wir uns die
Struktur des Befehls ggplot näher an.
stats_test: Hey R, nimm den Datensatz stats_test UND DANN
ggplot() : Hey R, male ein Diagramm von Typ ggplot (mit dem Datensatz aus
dem vorherigen Pfeifen-Schritt, d. h. aus der vorherigen Zeile, also stats_test)!
filter: wir wollen nur Zeilen (Studenten), für die gilt interest == 1 oder
interest == 6. Der horizontale Strich heißt „oder“.
+: Das Pluszeichen grenzt die Teile eines ggplot-Befehls voneinander ab.
aes: von „aethetics“, also welche Variablen des Datensatzes den sichtbaren
Aspekten (v. a. Achsen, Farben) zugeordnet werden.
x: Der X-Achse (Achtung, x wird klein geschrieben hier) wird die Variable
bestanden zugeordnet.
y: gibt es nicht??? Wenn in einem ggplot-Diagramm keine Y-Achse definiert
wird, wird ggplot automatisch ein Histogramm bzw. ein Balkendiagramm erstellen.
Bei diesen Arten von Diagrammen steht auf der Y-Achse keine eigene Variable,
sondern meist die Häufigkeit des entsprechenden X-Werts (oder eine Funktion der
Häufigkeit, wie relative Häufigkeit).
fill Das Diagramm (die Balken) sollen so gefüllt werden, dass sich die Häufigkeit der Werte von interest darin widerspiegelt.
geom_XYZ: Als „Geom“ soll ein Balken („bar“) gezeichnet werden. Ein Geom
ist in ggplot2 das zu zeichnende Objekt, also ein Boxplot, ein Balken, Punkte, Linien
etc. Entsprechend wird das gewünschte Geom mit geom_bar, geom_boxplot,
geom_point etc. gewählt.
position = fill: position_fill will sagen, dass die Balken alle eine
Höhe von 100 % (1) haben, d. h. gleich hoch sind. Die Balken zeigen also nur die
Anteile der Werte der fill-Variablen.
Häufige Arten von Diagrammen (Geomen)
173
n
NA
used up
thin
skinny
rather not say
overweight
jacked
full figured
fit
curvy
average
athletic
a little extra
10000
7500
5000
NA
very often
socially
rarely
often
not at all
2500
desperately
body_type
11.2
drinks
Abb. 11.12 Visualisierung des Zusammenhangs nominaler Variablen
Die einzige Änderung in den Parametern ist position = "fill". Dieser Parameter weist ggplot an, die Positionierung der Balken auf die Darstellung von Anteilen
auszulegen. Damit haben alle Balken die gleiche Höhe, nämlich 100 % (1). Aber die „Füllung“ der Balken schwankt je nach der Häufigkeit der Werte von interest pro Balken.
Wir sehen, dass die Anteile von hoch bzw. gering interessierten Studenten bei den beiden
Gruppen (bestanden vs. durchgefallen) unterschiedlich hoch sind. Dies spricht für einen
Zusammenhang der beiden Variablen; man sagt, die Variablen sind abhängig (im statistischen Sinne).
Je unterschiedlicher die „Füllhöhe“, desto stärker sind die Variablen (X-Achse
vs. Füllfarbe) voneinander abhängig (d. h. desto stärker der Zusammenhang).
Untersucht man den Zusammenhang zweier nominaler Variablen, so sind Kontingenztabellen das täglich Brot. Mit welchen Befehlen kann man sich Kontingenztabellen ausgeben lassen?18 Typische Beispiele: Welche Produkte wurden wie häufig an welchem
Standort verkauft? Wie viele Narzissten gibt es in welcher Management-Stufe? Wie ist
die Verteilung von Alkoholkonsum und Figur bei Menschen einer Single-Börse? Eine
Kontingenztabelle gibt also Häufigkeiten der Kombination von Merkmalsausprägungen
wieder. Bleiben wir bei dem Datensatz profiles; gibt es einen Zusammenhang zwischen Alkoholkonsum und Figur? Um Kontingenztabellen zu visualisieren, kann man
Fliesendiagramme nutzen (s. Abb. 11.12). Was sofort ins Auge sticht, ist, dass „soziales
Trinken“, nennen wir es mal so, am häufigsten ist, unabhängig von der Figur. Das erkennt
man an der helleren Färbung der Fliesen. Ansonsten scheinen die Zusammenhänge nicht
sehr stark zu sein.
18
Z. B. mit tally() oder count().
174
11
Datenvisualisierung mit ggplot2
profiles %>%
dplyr::count(drinks, body_type) %>%
ggplot() +
aes(x = drinks, y = body_type, fill = n) +
geom_tile() +
theme(axis.text.x = element_text(angle = 90))
Was haben wir gemacht? Also:
Nimm den Datensatz „profiles“ UND DANN
Zähle die Kombinationen von „drinks“ und „body_type“ UND DANN
Erstelle ein ggplot-Plot UND DANN
Weise der X-Achse „drinks“ zu, der Y-Achse „body_type“ und der Füllfarbe „n“
UND DANN
Male Fliesen UND DANN
Passe das Thema so an, dass der Winkel für Text der X-Achse auf 90 Grad steht.
Mit theme() verändert man das „Thema“ des Diagramms; damit sind alle Teile gemeint, die nicht die Daten visualisieren, also z. B. Achsenbeschriftungen, Titel oder Legenden.
11.2.4 Zusammenfassungen zeigen
Manchmal möchten wir nicht die Rohwerte einer Variablen darstellen, sondern z. B. die
Mittelwerte pro Gruppe. Mittelwerte sind eine bestimmte Zusammenfassung einer Spalte; also fassen wir zuerst die Körpergröße zum Mittelwert zusammen – gruppiert nach
Geschlecht.
profiles %>%
group_by(sex) %>%
summarise(income_m = mean(income, na.rm = TRUE),
income_sd = sd(income, na.rm = TRUE)) -> profiles_income_summary
profiles_income_summary
#> # A tibble: 2 x 3
#>
sex
income_m income_sd
#>
<chr>
<dbl>
<dbl>
#> 1 f
86633.
189917.
#> 2 m
110984.
205162.
Diese Tabelle schieben wir jetzt in ggplot2; natürlich hätten wir das gleich in einem
Rutsch durchpfeifen können.
profiles_income_summary %>%
qplot(x = sex, y = income, data = ., color = sex)
Häufige Arten von Diagrammen (Geomen)
110000
income_m
105000
sex
100000
f
m
95000
90000
175
Mittleres Einkommen
11.2
3e+05
2e+05
sex
f
1e+05
m
0e+00
-1e+05
f
f
m
m
sex
Fehlerbalken zeigen die SD
Abb. 11.13 Zusammenfassungen zeigen
Das Diagramm (Abb. 11.13, links) besticht nicht durch Tiefe und Detaillierung. Bereichern wir das Diagramm um Hinweise zur Streuung des Einkommens. Dazu steigen wir
auf ggplot() um, den großen Bruder von qplot(); Abb. 11.13, rechts, zeigt das Resultat. Diese Syntax erstellt zum Diagramm:
profiles_income_summary %>%
ggplot() +
aes(x = sex, y = income_m) +
geom_errorbar(aes(ymin = income_m - income_sd,
ymax = income_m + income_sd)) +
geom_point(aes(color = sex),
size = 5,
show.legend = FALSE) +
labs(caption = "Fehlerbalken zeigen die SD",
x = "",
y = "Mittleres Einkommen")
In Pseudosyntax:
Nimm den Datensatz profiles_income_summary UND DANN
erstelle ein ggplot-Diagramm UND DANN
weise der X-Achse sex und der Y-Achse income_m zu UND DANN
zeichne einen Fehlerbalken UND DANN
zeichne Punkte, die nach Geschlecht gefärbt und von der Größe 5 sind.
Möchte man die Werte-Labels der Achse ändern, so kann man im Dataframe die
Werte umkodieren (s. Abschn. 9.4). Alternativ kann man die Labels nur für die XAchse im Plot ändern: scale_x_discrete(labels = c("f"= "female", "m"
= "male")). Mit scale_x_discrete() spricht man die X-Achse an, die hier diskre-
176
11
Datenvisualisierung mit ggplot2
income
90000
sex
f
60000
m
30000
f
m
sex
Abb. 11.14 Informationsreiche Darstellung des Einkommens
te Daten zeigt. Das Argument labels ändert alte in neue Labels um. Mit show.legend
= FALSE haben wir die Darstellung der Legende unterdrückt.
Beachten Sie die unterschiedlichen Skalierungen der Y-Achsen in den zwei Teilbildern von Abb. 11.13. Der numerische Wert des Unterschieds von Frauen und
Männer hinsichtlich Gehalt ist jedes Mal gleich. Die Achsenskalierung suggeriert
jedoch Gegenteiliges: Im einen Teilbild wirkt der Unterschied groß, im anderen
klein.
Alternativ kann man hier Boxplots oder Violinplots verwenden; beide sind informationsreicher als Punkte. Grundsätzlich gilt: ein Diagramm, das wenig Information vermittelt, ist überflüssig. Daher sind informationsreichere Diagramme – unter sonst gleichen
Umständen – zu bevorzugen. Natürlich muss das Diagramm dabei übersichtlich bleiben.
Abb. 11.14 zeigt, dass es einige Extremwerte im Gehalt gibt; entfernen wir diese und
prüfen, ob das Diagramm dadurch deutlicher wird.
qplot(x = sex,
y = income,
data = profiles,
geom = "violin")
# oder "boxplot""
profiles %>%
filter(between(income, 100, 100000)) %>%
ggplot(aes(x = sex, y = income)) +
geom_violin() +
geom_point(data = profiles_income_summary, aes(color = sex,
y = income_m),
size = 5)
11.2
Häufige Arten von Diagrammen (Geomen)
177
Tab. 11.1 Häufige Diagrammtypen
X-Achse
Kontinuierliche Variable
Kontinuierliche Variable
Nominale Variable
Nominale Variable
Nominale Variable
Nominale Variable
Y-Achse
–
Kontinuierliche Variable
–
Nominale Variable
Metrische Variable
Metrische Variable
Histogramm
Diagrammtyp
Histogramm, Dichtediagramm
Punkte, Schachbrett-Diagramme
Balkendiagramm
Mosaicplot (Fliesendiagramm)
Punktediagramm für Zusammenfassungen
Boxplots (besser)
Histogramm
Streu
10.0
150
2.0
7.5
self_eval
density
count
1.5
100
1.0
50
5.0
0.5
2.5
0
0.0
0.00
0.25
0.50
0.75
1.00
0.00
0.25
0.50
score
Smooth
0.00
1.00
0.25
0.75
n
count
1.00
Balken (Anteile)
750
5.0
0.75
1.00
1000
7.5
0.50
score
Balken (Rohwerte)
10.0
self_eval
0.75
score
500
0.50
0.25
250
2.5
0
0.00
0.25
0.50
0.75
0.00
ja
1.00
score
ja
nein
bestanden
Punkte (Mittelwerte)
nein
bestanden
stats_test
Linie
0.84
score
0.75
0.8
score
score_median_gruppe
1.00
0.50
0.80
0.7
0.76
0.25
0.00
ja
nein
bestanden
0.72
ja
nein
bestanden
2
4
6
interest
Abb. 11.15 Überblick über häufige Diagrammtypen
Tab. 11.1 und Abb. 11.15 fassen die gerade besprochenen Diagrammtypen (Geome) zusammen.
178
11
Datenvisualisierung mit ggplot2
Aufgaben
Verändern Sie die letzte Syntax (von Abb. 11.9, links) folgendermaßen:
1. Färben Sie die Punkte mit Alpha = .01 ein.19
2. Probieren Sie auch Folgendes aus: Fügen Sie bei aes den Parameter color =
Jahrzehnt hinzu.20
3. Ändern Sie die Farbpalette (s. Abschn. 12.1.1 und 12.1.3).
Lösung
movies %>%
filter(Jahrzehnt %in% c("1990", "2000")) %>%
ggplot(aes(x = budget, y = rating, color = Jahrzehnt)) +
geom_point(data = select(movies, -Jahrzehnt), color = "grey80") +
geom_point(alpha = .5) +
facet_wrap(~Jahrzehnt) +
geom_smooth() +
#geom_rug() +
scale_color_viridis(discrete = TRUE)
11.3 Die Gefühlswelt von ggplot2
Geben Sie eine diskrete X-Achse an und keine Y-Achse, so greift qplot im Standard
auf das Geom bar zurück (Balkendiagramm), falls Sie kein Geom angeben.
qplot(x = score, data = stats_test) # identisch zu
qplot(x = score, data = stats_test, geom = "bar")
Geben Sie eine kontinuierliche X-Achse an und keine Y-Achse, so greift qplot im Standard auf das Geom histogram zurück (Histogramm).
qplot(x = score, data = stats_test) # identisch zu
qplot(x = score, data = stats_test, geom = "histogram")
Geben Sie eine kontinuierliche X-Achse und eine kontinuierliche Y-Achse an, so greift
qplot im Standard auf das Geom point zurück (Streudiagramm).
qplot(x = score, y = self-eval, data = stats_test) # identisch zu
qplot(x = score, y= self-eval, data = stats_test, geom = "point")
19
geom_point() wird verändert zu geom_point(alpha = 1/100).
aes(x = budget, y = rating) wird verändert zu aes(x = budget, y =
rating, color = Jahrzehnt).
20
11.4
ggplot(), der große Bruder von qplot()
179
Möchten Sie mehrere Geome für eine Variable darstellen, so muss die Gruppierungsvariable diskret sein:
#oh no:
qplot(x = rating, y = affairs, geom = "boxplot", data = Affairs)
#oh yes:
qplot(x = factor(rating), y = affairs, geom = "boxplot", data = Affairs)
#oh yes:
qplot(x = gender, y = affairs, geom = "boxplot", data = Affairs)
Oft wird gefragt, warum ggplot diesen grauen Hintergrund bevorzugt. Der Vorteil des
Graus ist, dass die Gitternetzlinien zwar vorhanden, aber doch dezent im Hintergrund
bleiben, wo sie hingehören. Außerdem fügt sich das Grau gut in den optischen Gesamteindrucks einer Seite mit viel Text. Zu guter Letzt vermittelt die graue Fläche einen Eindruck
der Zusammengehörigkeit, so dass das Bild als Gestalt sich von der Umgebung abhebt. Sie
können aber, wenn Sie einen anderen Stil bevorzugen, leicht wechseln, s. Abschn. 12.2.
Ähnliches gilt für das Farbschema; alternative Farbschemata werden in Abschn. 12.1 vorgestellt.
11.4 ggplot(), der große Bruder von qplot()
qplot() ist dafür gedacht, schnell ein Diagramm zu plotten. Die Syntax ist einfach ge-
halten, so dass man nicht an allen möglichen Details herumspielen kann bzw. muss. Zur
Kontrolle der Details gibt es ggplot(); mit dieser Funktion hat man Zugriff auf ein
großes Spektrum von Einstellungsmöglichkeiten. Die grundlegende Syntax ist ähnlich.
Die beiden folgenden Zeilen erstellen das gleiche Diagramm:
qplot(data = stats_test, x = bestanden, y = interest, geom = "boxplot")
# ist identisch zu
ggplot(data = stats_test) +
aes(x = bestanden, y = interest) +
geom_boxplot()
Mit aes() wird die „Ästhetik“ bzw. ein „Ästhetikum“ definiert: Bestimmte Spalten des
Dataframes werden den visuellen Merkmalen des Diagramms – wie Achsen und Farben
– zugeordnet. Wir werden im Folgenden hauptsächlich mit ggplot() arbeiten, da der
Funktionsumfang viel größer ist als bei qplot().
Ein Diagramm zu erzeugen, das keine Wünsche offen lässt, ist (auch) mit ggplot2
kein Selbstläufer.21 Viele Details können (und sollten oft) angepasst werden. Die einfa21
https://xkcd.com/1319/.
180
11
Datenvisualisierung mit ggplot2
che Lehre daraus ist, dass man zwischen Diagrammen für „privaten“ und „öffentlichen“
Gebrauch unterscheiden muss: Bei Diagrammen, die man veröffentlichen will, wird man
mehr Augenmerk auf Aspekte wie aussagekräftige Achsenbeschriftung legen. Auch in
diesem Buch sind nicht alle Diagramme auf Hochglanz poliert. Wickhams (2016) Buch
zu ggplot gibt einen umfassenden Überblick über viele praktische Fragen, die sich in der
Arbeit mit ggplot ergeben.
Aufgaben
1.
2.
3.
4.
5.
6.
Erzählen Sie einer vertrauenswürdigen Person jeweils eine „Geschichte“, die
das Zustandekommen der vier Plots von Anscombe (Abb. 11.1) erklärt.22
Abb. 11.6 stellt das mittlerer Budget von Filmen dar; als „Geom“ wird ein
Boxplot verwendet. Andere Geome wären auch möglich – aber wie sinnvoll
wären sie?23
Erstellen Sie ein Diagramm, welches Histogramme für budget verwendet anstelle von Boxplots (Datensatz movies).24
Ist das Histogramm genauso erfolgreich wie der Boxplot, wenn es darum geht,
viele Verteilungen vergleichend zu präsentieren? Warum?25
Erstellen Sie ein sehr grobes und ein sehr feines Histogramm für die Schuhgröße aus dem Datensatz wo_men.26
Bauen Sie das zweite Diagramm aus dem Vorwort nach.
Lösung
#data(stats_test, package = "pradadata")
stats_test %>%
filter(study_time %in% 1:5) %>%
select(bestanden, study_time, score) %>%
na.omit() %>%
22
Wenn Sie niemanden finden, schicken Sie mir eine E-Mail.
Andere Geome, die in Frage kämen, sind Punkte, verwackelte Punkte (geom_jitter), Violinenplots oder Histogramme. Punkte als Geom haben das Problem, dass es zu Overplotting kommen
kann. Violinen sind ähnlich wie Boxplots, aber noch informationsreicher. Histogramme sind weniger gut geeignet, um Gruppen zu vergleichen.
24
qplot(x = budget, geom = "histogram", data = movies, facets =
~factor(Jahrzehnt)).
25
Der Boxplot ist besser geeignet als das Histogramm, um mehrere Verteilungen vergleichend zu
präsentieren. Durch die gleiche Ausrichtung der Boxplots fällt es dem Auge leichter, Vergleiche
anzustellen im Vergleich zu den Histogrammen.
26
data(wo_men, package = "pradadata"); qplot(x = shoe_size, data =
wo_men, bins = 5); qplot(x = shoe_size, data = wo_men, bins = 50).
23
11.4
ggplot(), der große Bruder von qplot()
ggplot(aes(x = factor(study_time), y = score)) +
geom_jitter(aes(color = bestanden), alpha = .56) +
geom_boxplot(alpha = .7) +
geom_smooth(aes(group = 1), method = "lm", se = FALSE) +
theme(legend.position = "bottom") +
labs(y = "Prozent richtiger Lösungen",
x = "Lernaufwand",
title = "Mehr Lernen, bessere Noten",
subtitle = "Der Zusammenhang von Lernzeit und Klausurerfolg",
caption = paste0("n = ", nrow(stats_test), " Studenten"))
7.
Erstellen Sie ein Diagramm, das sowohl eine Zusammenfassung (Mittelwert)
der Körpergrößen nach Geschlecht darstellt als auch die einzelnen Werte.
Lösung
Zuerst erstellen wir einen Dataframe ohne fehlende Werte und ohne Extremwerte:
wo_men %>%
drop_na() %>%
filter(between(height, 150, 210)) %>%
filter(between(shoe_size, 35, 48)) -> wo_men2
Und dann einen Dataframe mit den zusammengefassten Mittelwerten:
wo_men2 %>%
group_by(sex) %>%
summarise(height = mean(height)) -> wo_men3
Dann zeichnen wir das Diagramm auf Basis beider Datensätze:
wo_men2 %>%
ggplot() +
aes(x = sex, y = height) +
stat_summary(fun.data = "mean_cl_normal",
geom = "errorbar",
color = "grey40") +
geom_jitter(color = "grey80") +
geom_point(data = wo_men3, color = "red", size = 8) +
labs(x = "Geschlecht",
y = "Größe",
caption = "Fehlerbalken zeigen das 95%-KI des Mittelwerts")
Der „Trick“ ist hier, erst die zusammengefassten Daten in ein Geom zu stecken (wo_men3). Dann werden die Rohdaten (wo_men2) ebenfalls in ein
181
182
11
Datenvisualisierung mit ggplot2
Geom gepackt. Allerdings muss die Achsen-Beschriftung bei beiden Geomen identisch sein, sonst gibt es eine Fehlermeldung. Mit stat_summary()
bekommt man zusammengefasste Werte wie Mittelwert oder SD. Natürlich
kann man die selber vorher berechnen, aber man kann diese Arbeit auch an
ggplot delegieren. Der Parameter fun.data = "mean_cl_normal" sagt
sinngemäß: „Die Daten, die dem hier verlangten Geom (ein Fehlerbalken) zugrunde liegen, bekommt man, wenn man die Funktion mean_cl_normal()
ausführt. Diese wiederum berechnet den Mittelwert und die Grenzen des 95 %Konfidenzintervalls auf Basis einer t-Verteilung.“
Hmisc::smean.cl.normal(wo_men2$height)
#> Mean Lower Upper
#>
170
168
171
8.
Erstellen Sie ein Diagramm in mehreren Varianten, um den Zusammenhang
von Körper- und Schuhgröße zu visualisieren.
Lösung
p1 <ggplot(wo_men2) +
aes(x = height, y = shoe_size) +
geom_point() +
labs(title = "geom_point")
p2 <ggplot(wo_men2) +
aes(x = height, y = shoe_size) +
geom_jitter() +
labs(title = "geom_jitter -\nverwackelt")
p3 <ggplot(wo_men2) +
aes(x = height, y = shoe_size) +
geom_smooth() +
labs(title = "geom_smooth")
p4 <ggplot(wo_men2) +
aes(x = height, y = shoe_size) +
geom_bin2d(bins = 10) +
labs(title = "geom_bin2d")
grid.arrange(p1, p2, p3, p4, ncol = 2)
11.4
9.
ggplot(), der große Bruder von qplot()
Erstellen Sie ein Diagramm in mehreren Varianten, um den Zusammenhang
von Geschlecht und Körpergröße zu visualisieren.
Lösung
p1 <ggplot(wo_men2) +
aes(x = sex, y = height) +
geom_boxplot() +
labs(title = "geom_boxplot")
p2 <ggplot(wo_men2) +
aes(x = sex, y = height) +
geom_violin() +
labs(title = "geom_violin")
p3 <ggplot(wo_men3, aes(x = sex, y = height)) +
geom_point(size = 7, aes(shape = sex), color = "firebrick",
alpha = .7) +
geom_line(group = 1) +
geom_point(data = wo_men2, aes(x = sex)) +
labs(title = "Mit zusammengefasstem Datensatz") +
theme(legend.position = c(1, 1),
legend.justification = c(1, 1))
grid.arrange(p1, p2, p3, ncol = 1)
10. Erstellen Sie das Diagramm in Abb. 11.15.
Lösung
p0 <- ggplot(data = stats_test) +
theme_minimal() +
theme(text = element_text(size = 6),
title = element_text(size = 8),
legend.position = "none")
p1 <- p0 + aes(x = score) + geom_histogram() +
labs(title = "Histogramm")
p2 <- p0 + aes(x = score) + geom_density() +
labs(title = "Histogramm")
p3 <- p0 + aes(x = score, y = self_eval) +
geom_point() +
183
184
11
Datenvisualisierung mit ggplot2
labs(title = "Streu")
p4 <- p0 + aes(x = score, y = self_eval, fill = "bestanden") +
geom_smooth() +
geom_point(color = "grey80") +
labs(title = "Smooth")
p5 <-p0 + aes(x = bestanden) +
geom_bar() +
labs(title = "Balken (Rohwerte)")
p6 <- stats_test %>%
mutate(interessiert = stats_test$interest > 3) %>%
count(bestanden, interessiert) %>%
ggplot() +
aes(x = bestanden, y = n, fill = interessiert) +
geom_col(position = "fill") +
theme_minimal() +
theme(text = element_text(size = 6),
title = element_text(size = 8),
legend.position = "none") +
labs(title = "Balken (Anteile)")
p7 <- stats_test %>%
mutate(interessiert = stats_test$interest > 3) %>%
group_by(bestanden, interessiert) %>%
summarise(score_median_gruppe = median(score)) %>%
ggplot(aes( x = bestanden, y = score_median_gruppe,
color = interessiert,
shape = interessiert),
size = 6) +
geom_point(alpha = .7) +
labs(title = "Punkte (Mittelwerte)") +
theme_minimal() +
theme(text = element_text(size = 6),
title = element_text(size = 8),
legend.position = "none")
p8 <- p0 + aes(x = bestanden, y = score) +
geom_boxplot() +
labs(title = "stats_test")
p9 <- p0 + aes(x = interest, y = score) +
stat_summary(fun.y = "mean", geom = "line") +
labs(title = "Linie")
grid.arrange(p1, p2, p3, p4, p5, p6, p7, p8, p9, nrow = 3)
11.4
ggplot(), der große Bruder von qplot()
185
11. Bauen Sie Diagramm 11.16, das auf Abb. 11.13 basiert, nach.
Mittelwert des Gehalts nach Geschlecht
Mittleres Einkommen
250000
200000
150000
100000
50000
0
female
male
Geschlecht
Fehlerbalken zeigen die SD
Abb. 11.16 Zusammenfassungen zeigen mit Details
profiles_income_summary %>%
ggplot() +
aes(x = sex, y = income_m) +
geom_jitter(data = profiles,
aes(y = income),
color = "grey60",
alpha = .1,
width = .1) +
geom_errorbar(aes(ymin = income_m - income_sd,
ymax = income_m + income_sd),
width = 0,
color = "grey40") +
geom_line(aes(group = 1)) +
geom_point(aes(color = sex,
shape = sex), size = 5,
show.legend = FALSE) +
labs(title = "Mittelwert des Gehalts nach Geschlecht",
caption = "Fehlerbalken zeigen die SD",
y = "Mittleres Einkommen",
x = "Geschlecht") +
theme(legend.position = "bottom") +
scale_x_discrete(labels = c("f"= "female",
"m" = "male")) +
coord_cartesian(ylim = c(0, 250000))
Einen Unterschied haben wir in diese Versions hineingebaut: Wir haben das
Diagramm gleichsam „herangezoomt“ (coord_cartesian(ylim)). Probie-
186
11
Datenvisualisierung mit ggplot2
ren Sie mal im Unterschied ylim(); mit ylim() wird nicht nur herangezoomt, sondern es werden auch alle nicht mehr sichtbaren Daten aus den
Berechnungen der Geome entfernt. Das kann einen großen Unterschied machen.
Aufgaben
Richtig oder falsch?27
1. Diese Geome gehören zum Standard in ggplot2: bar, histogram, point, density,
jitter, boxplot.
2. qplot() ist eine Funktion im Paket ggplot2.
3. Mit aes definiert man, wie „ästhetisch“ das Diagramm sein soll (z. B. grauer
Hintergrund vs. weißer Hintergrund, Farbe der Achsen c.). Genauer gesagt, man
definiert, welche Variable im Datensatz mit welchem „Ästhetikum“, also welcher sichtbaren Eigenschaft des Diagramms korrespondiert.
4. Diese Geome gehören zum (Standard-)ggplot2: smooth, line, boxwhisker, mosaicplot.
5. Möchte man ein Diagramm erstellen, welches auf der X-Achse total_bill,
auf der Y-Achse tip darstellt, als Geom Punkte verwendet und die Daten aus
der Tabelle tips bezieht, so ist folgende Syntax korrekt: qplot(x = total,
bill, y = tip, geom = "point", data = tips)1. geom_jitter
zeigt „verwackelte“ Punkte.
6. Mit labs() kann man Titel, Achsen und Legenden beschriften.
7. Fliesendiagramme eignen sich, um Kontingenztabellen zu visualisieren.
8. Mit dem Argument alpha kann man die Durchsichtigkeit von Geomen einstellen.
9. Möchte man die Grenzen der X- oder Y-Achse des Diagramms bestimmen, so
führen coord_cartesian() und xlim() zu den gleichen Ergebnissen.
27
R, R, R, F, R, R, R, R, R, F.
Fortgeschrittene Themen der Visualisierung
12
Lernziele
Farbschemata wählen
Nicht-Datenteile eines ggplot-Diagramms ändern
Interaktive Diagramme erstellen
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(gridExtra)
library(wesanderson)
library(RColorBrewer)
library(gridExtra)
library(ggthemes)
library(viridis)
data(flights, package = "nycflights13")
data(movies, package = "ggplot2movies")
12.1 Farbwahl
Erstens: Keinen Schaden anrichten – so könnte hier die Maßgabe bei der Wahl von Farben
sein. Es ist leicht, zu grelle oder wenig kontrastierende Farben auszuwählen. Außerdem
gilt es, Farben an den Zweck bzw. die Art der Darbietung des Diagramms anzupassen.
Für eine Exploration der Daten muss das Diagramm nicht ins Detail schick sein; das wäre
Zeitverschwendung. Für einen Schwarz-Weiß-Druck gelten wiederum andere Regeln als
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_12
187
188
12
Fortgeschrittene Themen der Visualisierung
für eine Präsentation via Beamer. Generell ist eine gute Farbauswahl (Palette) nicht so
leicht und hängt eben vom Zweck der Darstellung ab. Bevor Sie sich an die Zusammenstellung Ihrer Lieblingsfarben stürzen, ein Cave:
Ihre Farbpalette sollte widerspiegeln, . . .
ob es sich um eine diskrete oder um eine kontinuierliche Variable handelt .- wie viele
verschiedene Werte darzustellen sind. Je nach Anzahl der darzustellenden unterschiedlichen Werte sollte die Farbwahl sich ändern (v. a. bei diskreten Variablen). Wenn Sie
nur wenige Farben haben, würden Sie die Farben vielleicht so wählen wollen, dass
sie im Farbkreis weit auseinanderliegen, damit sie unterschiedlich (kontrastreich) genug sind. Bei mehr Farben ändert sich damit die Wahl der Farben. Man benötigt also
am besten für jede Anzahl von Kategorien eine eigene Palette: die Farben sollten so
unterschiedlich wie nötig, aber die Unterschiede so dezent wie möglich sein.
ob Rot-Grün-Blindheit berücksichtigt werden soll, da diese recht verbreitet ist.
ob in schwarz-weiß gedruckt werden soll; Schwarz-Weiß-Drucker können nur Helligkeits-/Graustufen wiedergeben; Menschen können nur eine begrenzte Anzahl von
Graustufen (komfortabel) unterscheiden
Kurz: Die Entwicklung eigener Farbpaletten kann umfangreich sein, wenn man es ordentlich machen will. Glücklicherweise gibt es eine Reihe guter Paletten, die frei verfügbar
sind und viele Szenarien abdecken. Ein Beispiel für gute, handgewählte Paletten ist die
Auswahl von Frau Brewer.
12.1.1 Die Farben von Cynthia Brewer
Cynthia Brewer1 hat einige schöne Farbpaletten zusammengestellt; diese sind in R und
in ggplot2 über das Paket RcolorBrewer verfügbar. Mit brewer.pal.info() bekommen Sie einen Überblick. Je nach Art der darzustellenden Daten sind verschiedene
Farbpaletten sinnvoll.
Kontrastierende Farbpaletten (nominale/qualitative Variablen) – z. B. Männer vs. Frauen (s. Abb. 12.1).
Sequenzielle Farbpaletten (unipolare numerische Variablen) – z. B. Preis oder Häufigkeit (s. Abb. 12.2).
Divergierende Farbpaletten (bipolare numerische Variablen) – z. B. semantische Potenziale oder Abstufungen von „stimme überhaupt nicht zu“ über „neutral“ bis „stimme
voll und ganz zu“ (s. Abb. 12.3).
In ggplot2 können wir Paletten ändern mit dem ggplot-Befehl scale_color_XXX für
Punkt- bzw. Linienfarbe bzw. mit dem Befehl scale_fill_XXX für Füllfarben. Für das
1
http://colorbrewer2.org/.
12.1 Farbwahl
189
Set3
Set2
Set1
Pastel2
Pastel1
Paired
Dark2
Accent
Abb. 12.1 Farbpaletten für nominale Variablen
YlOrRd
YlOrBr
YlGnBu
YlGn
Reds
RdPu
Purples
PuRd
PuBuGn
PuBu
OrRd
Oranges
Greys
Greens
GnBu
BuPu
BuGn
Blues
Abb. 12.2 Farbpaletten für unipolare numerische Variablen
Spectral
RdYlGn
RdYlBu
RdGy
RdBu
PuOr
PRGn
PiYG
BrBG
Abb. 12.3 Farbpaletten für bipolare numerische Variablen
190
12
Fortgeschrittene Themen der Visualisierung
750
dest
500
ATL
BOS
250
LAX
arr_delay
arr_delay
750
dest
500
ATL
BOS
250
0
LAX
0
ATL
BOS
LAX
dest
ATL
BOS
LAX
dest
Abb. 12.4 Brewer-Paletten
XXX gibt es ein paar Möglichkeiten; eine ist brewer. Damit werden die Farbpaletten von
Cynthia Brewer angesprochen (s. Abb. 12.4).
data(flights)
p0 <- flights %>%
filter(dest %in% c("BOS", "ATL", "LAX")) %>%
ggplot() +
aes(x = dest, y = arr_delay, color = dest) +
geom_boxplot()
p_brewer1 <- p0 + scale_color_brewer(palette = "Set1")
p_brewer2 <- p0 + scale_color_brewer(palette = "Set2")
grid.arrange(p_brewer1, p_brewer2, nrow = 1)
scale_color_brewer() bedeutet hier: „Ordne der Variablen, die für „color“ zuständig ist, hier sex, eine Farbe aus der Brewer-Palette „Set1“ zu.“ Die Funktion wählt
automatisch die richtige Anzahl von Farben. Man beachte, dass die Linienfarbe über
color und die Füllfarbe über fill zugewiesen wird. Punkte haben nur eine Linienfarbe,
keine Füllfarbe. Mit grid.arrange() aus gridExtra kann man mehrere Diagramme
in ein Diagramm packen.
12.1.2
Die Farben von Wes Anderson
Auch die Farbpaletten von Wes Anderson sind erbaulich.2 Diese sind nicht „hart verdrahtet“ in ggplot2, sondern werden über scale_XXX_manual zugewiesen (wobei XXX
z. B. color oder fill sein kann). Betrachten wir dazu den Datensatz movies. Zuerst
2
https://github.com/karthik/wesanderson.
12.1 Farbwahl
191
wandeln wir die Spalten, in denen die Genres kodiert sind, in eine einzelne Spalte um. Das
ist nicht ganz koscher, weil ein Film auch mehreren Genres zugehörig sein kann; z. B. der
Film „Avalance“, der sowohl „Action“ als auch „Drama“ zugeordnet ist. Solche Filme
ordnen wir einer neuen Kategorie multiple zu.
movies %>%
gather(key = genre, value = is_true, -c(title:mpaa)) %>%
filter(is_true == 1) %>%
mutate(multiple_genre = duplicated(title)) %>%
mutate(genre = ifelse(multiple_genre, "multiple", genre)) -> movies2
Hat man solcherlei Datenakrobatik vollzogen, lohnt es sich, ausgiebig zu testen, ob das
Gewünschte dem Vorhandenen gleicht. Nun plotten wir verschiedene Farbpaletten von
Wes Anderson (s. Abb. 12.5).
movies2 %>%
filter(genre %in% c("Action", "Drama", "multiple")) %>%
sample_n(5000) %>%
ggplot() +
aes(x = budget, y = rating, color = genre) +
geom_point(alpha = .5) +
theme_void() +
theme(legend.position = "none",
plot.title = element_text(size = rel(.5))) -> p_blank
p_wes1 <- p_blank + scale_color_manual(values = wes_palette("Darjeeling1")) +
labs(title = "Palette: Darjeeling1")
p_wes2 <- p_blank + scale_color_manual(values = wes_palette("GrandBudapest1")) +
labs(title = "Palette: GrandBudapest1")
meine_farben <- c("red", "blue", "#009981")
p_own <- p_blank + scale_color_manual(values = meine_farben) +
labs(title = "Palette: c('red', 'blue', '#009981')")
grid.arrange(p_wes1, p_wes2, p_own, ncol = 3)
Wer sich berufen fühlt, eigene Farben (oder die seiner Organisation) zu
verwenden, kommt auf ähnlichem Weg zu Ziel (s. Abb. 12.5, rechts). Man definiert sich
seine Palette, wobei ausreichend Farben definiert sein müssen. Diese weist man über
scale_XXX_manual() dann zu. Man kann aus den in R definierten Farben auswählen3
oder sich selber die RGB-Nummern (in Hexadezimal-Nummern) heraussuchen. Möch-
3
http://sape.inf.usi.ch/quick-reference/ggplot2/colour.
Palette: GrandBudapest1
Palette: c('red', 'blue', '#009981')
12
Abb. 12.5 Die Farbpaletten von Wes Anderson
Palette: Darjeeling1
192
Fortgeschrittene Themen der Visualisierung
12.1 Farbwahl
193
2.0e+08
2
budget
y
1.5e+08
0
-2
1.0e+08
-2
5.0e+07
0
2
x
0.0e+00
Action
Drama
multiple
count
genre
20
40
60
80
Abb. 12.6 Die Viridis-Farbpalette
te man schlicht zu ggplot2 sagen: „Ich habe drei Boxplots; fülle den ersten rot, den
zweiten blau und den dritten grün“, so kann man das auch so erreichen:
movies2 %>%
filter(genre %in% c("Action", "Drama", "multiple")) %>%
ggplot(aes(x = genre, y = budget)) +
geom_boxplot(fill = c("red", "blue", "green")) -> movies_boxplot
12.1.3 Viridis
Lohnenswert sind auch die Palette im Paket viridis. Diese Palette ist für SchwarzWeiß-Druck und bei Farbenblindheit geeignet. Die entsprechenden Befehle fügen sich in
die gängige Logik: scale_fill_viridis() zur Füllung von Flächen und scale_
color_viridis() zur Färbung von Punkten und Linien. Zu beachten ist, dass Viridis
von kontinuierlichen Farbverläufen ausgeht. Hat man eine begrenzte Anzahl Farben, so
übergibt man den Parameter discrete = TRUE (s. Abb. 12.6). Mit guide = FALSE
schaltet man die Legende (Guide) aus.
p_viridis1 <- movies2 %>%
filter(genre %in% c("Action", "Drama", "multiple")) %>%
ggplot(aes(x = genre, y = budget, color = genre)) +
geom_boxplot() +
scale_color_viridis(discrete = TRUE, guide = FALSE)
p_viridis2 <- movies2 %>%
filter(genre %in% c("Action", "Drama", "multiple")) %>%
ggplot() +
aes(x = genre, fill = genre) +
geom_bar() +
scale_fill_viridis(discrete = TRUE, guide = FALSE) +
theme(legend.position = "right")
194
12
Fortgeschrittene Themen der Visualisierung
p_viridis3 <- ggplot(data.frame(x = rnorm(10000), y = rnorm(10000)),
aes(x = x, y = y)) +
geom_hex() + coord_fixed() +
scale_fill_viridis() +
theme(legend.position = "bottom")
grid.arrange(p_viridis1,
p_viridis3, nrow = 1)
12.2 ggplot2-Themen
Ein „Thema“ bei ggplot2 umfasst die Gestaltung aller Aspekte eines Diagramms, welche nicht die Daten betreffen, also Achsen, Hintergrund, Titel etc.;4 ggplot2 kommt mit
einer Auswahl an „eingebauten“ Themen (s. Abb. 12.7), aber es gibt noch einige weitere
Themen in anderen Paketen.
Betrachten wir zuerst die „Standard-Themen“:
p_flights <- flights %>%
filter(dest %in% c("BOS", "ATL", "LAX")) %>%
ggplot() +
aes(x = dest, y = air_time) +
geom_boxplot() +
theme(legend.position = "none")
p_flights1 <- p_flights +
theme_classic() +
ggtitle("theme_classic")
p_flights2 <- p_flights +
theme_bw() +
ggtitle("theme_bw")
p_flights3 <- p_flights +
theme_minimal() +
ggtitle("theme_minimal")
p_flights4 <- p_flights +
theme_void() +
ggtitle("theme_void")
grid.arrange(p_flights1, p_flights2,
p_flights3, p_flights4, ncol = 2)
Mit theme(legend.position = "none") kann man noch die Legende abschalten.
4
http://docs.ggplot2.org/dev/vignettes/themes.html.
12.2 ggplot2-Themen
195
theme_classic
theme_bw
400
air_time
air_time
400
300
200
100
300
200
100
ATL
BOS
LAX
ATL
dest
BOS
LAX
dest
theme_minimal
theme_void
air_time
400
300
200
100
ATL
BOS
LAX
dest
Abb. 12.7 Themen von ggplot2
Über das Paket ggthemes5 kann man ein gutes Dutzend Themen anfordern. Probieren Sie mal theme_excel(), theme_tufte() und theme_base(). Weitere Themen
(Themes) sind verfügbar.6 Ein recht schönes, weil klares Design (Theme) für ggplot2 bietet das Paket cowplot (s. Abb. 12.8).
library(cowplot)
flights %>%
filter(dest %in% c("BOS", "ATL", "LAX")) %>%
ggplot() +
aes(x = dest, y = air_time, color = dest) +
geom_boxplot()
12.2.1
Schwarz-Weiß-Druck
Möchte man ein Diagramm in Schwarz-Weiß abbilden, so bieten sich folgende Maßnahmen an (vgl. Abb. 12.9):
5
6
https://cran.r-project.org/web/packages/ggthemes/vignettes/ggthemes.html.
https://github.com/ricardo-bion/ggtech.
196
12
Fortgeschrittene Themen der Visualisierung
air_time
400
dest
300
ATL
BOS
LAX
200
100
ATL
BOS
LAX
dest
Abb. 12.8 GGplot mit dem Thema „cowplot“
Füllfarben auf Graustufen setzen; bei ggplot geht das mit fill_color_grey().
Das Farbschema „Schwarz-Weiß“ (theme_bw()) wählen; dieses Thema verzichtet
auf den grauen Hintergrund, der bei Kopien oder beim Ausdrucken Probleme bereiten könnte. – Hat man nur wenige Gruppen, so kann man den Gruppen geometrische
Symbole (wie Dreiecke oder Rechtecke) zuweisen; bei Punkten als Geomen ist das am
sinnvollsten. Ähnliches gilt für Linien; dort kann man dem Linienstil eine Gruppierungsvariable zuordnen.
flights %>%
sample_n(100) %>%
ggplot() +
aes(x = origin, y = arr_delay, shape = origin, fill = origin) +
theme_bw() -> p_bw_base
p_bw1 <- p_bw_base + geom_point() + theme(legend.position = "none")
p_bw2 <- p_bw_base + geom_boxplot() + scale_fill_grey() +
theme(legend.position = "none") + ylab("")
p_bw3 <- p_bw_base + geom_line(aes(x = dep_delay, linetype = origin)) +
ylab("")
arr_delay
grid.arrange(p_bw1, p_bw2, p_bw3, nrow = 1)
150
150
150
100
100
100
50
50
50
0
0
0
origin
EWR
JFK
LGA
-50
-50
EWR
JFK
origin
LGA
-50
EWR
JFK
LGA
origin
Abb. 12.9 Diagramme in Schwarz-Weiß bzw. Graustufen
0 50
101050
origin
12.3 Interaktive Diagramme
197
12.3 Interaktive Diagramme
Erstellt man Diagramme nicht für das Papier, sondern zur Betrachtung am Bildschirm, so
sind interaktive Diagramme eine interessante Option. Mit interaktiv ist gemeint, dass der
Nutzer das Diagramm verändern kann bzw. dass das Diagramm auf Aktionen des Nutzers
„antwortet“. Da immer mehr Diagramme auf einem Monitor bzw. elektronisch dargestellt
werden, kommt der Frage nach der Interaktivität zunehmend Bedeutung zu. Der Vorteil
interaktiver Diagramme ist es, dass sie Einblicke gestatten, die statische Diagramme nicht
(so leicht) erlauben. Mittlerweile existiert eine größere Anzahl von Methoden für interaktive Diagramme; das R-Paket htmlwidgets bringt viele dieser Technologien in R.
Meist bauen solche Technologien auf JavaScript auf; damit kann der Browser die nötigen
Berechnungen selber durchführen und es wird keine weiteren Ressourcen wie ein laufendes R (z. B. auf einem Server) benötigt. So sind z. B. interaktive Karten möglich (s.
Abschn. 14.5).
12.3.1 Plotly
Ein einfaches, aber mächtiges Werkzeug ist das Paket plotly. Der Vorteil dieses Pakets
ist, dass man ein Objekt des Typs ggplot übergeben kann; man braucht keine anderen
Funktionen bzw. muss sich keine neue Herangehensweise merken. Plotly zeigt z. B. den
numerischen Wert des Falls bzw. Objekts, über den sich der Mauszeiger gerade bewegt.
Man kann auch einzelne Gruppen ausblenden und zoomen (s. Abb. 12.10).
p_stats <- ggplot(data = stats_test) +
aes(x = interest, fill = bestanden) +
geom_density(alpha = .3)
p_stats_plotly <- ggplotly(p_stats)
p_stats_plotly
Abb. 12.10 Eine statische
Variante eines dynamischen
Diagramms mit plotly
198
12
Fortgeschrittene Themen der Visualisierung
Abb. 12.11 Ein 3D-Punktediagramm mit Plotly
Eine interessante Option von interaktiven Diagrammen ist es, 3D-Objekte zu rotieren.
Auch das ist mit plotly möglich. plotly bietet auch mehr als „nur“ die Übersetzung
von ggplot2-Objekten in JavaScript; es gibt auch eine eigene Syntax. Um ein zoomund rotierbares 3D-Streudiagramm zu erstellen, definiert man anstelle von zwei einfach
drei Dimensionen; es wird automatisch ein Punktdiagramm in 3D erstellt (s. Abb. 12.11).
Mit add_markers() fügt man eine Legende hinzu. Wir nutzen hier den „eingebauten“
Datensatz mtcars.
plot_ly(mpg, x = ~cty, y = ~hwy, z = ~cyl) %>%
add_markers(color = ~cyl)
12.3.2 Weitere interaktive Diagramme
Ein interessantes Netzwerk-Diagramm zeigt networkD3::forceNetork() (s. Abb.
12.12).
data(MisLinks, MisNodes)
forceNetwork(Links = MisLinks, Nodes = MisNodes, Source = "source",
Target = "target", Value = "value", NodeID = "name",
Group = "group", opacity = 0.2)
12.3 Interaktive Diagramme
199
Abb. 12.12 Ein interaktives Netzwerk-Diagramm
Aufgaben
Richtig oder falsch?7
1.
2.
Das Farbschema Viridis ist für Rot-Grün-Blindheit ungeeignet.
Um eine nominale Variable in Farben abzubilden, werden in den BrewerPaletten andere Farben verwendet als zur Abbildung von quantitativen Farben.
3. Um sich eine eigene Farbpalette zu definieren, ist in ggplot der Befehl
scale_color_manual(values = meine_farben) gedacht.
4. Viridis ist für Schwarz-Weiß-Druck ungeeignet.
5. Bei ggplot ist das Thema mit grauem Hintergrund theme_gray der Standard.
6. Ggplot erlaubt es nicht, den grauen Hintergrund abzustellen bzw. eine andere
Art des Hintergrunds auszuwählen.
7. Mit interaktiv bei Diagrammen ist gemeint, dass der Nutzer das Diagramm
verändern kann.
8. Über ggplotly() kann man interaktive Varianten von ggplot erstellen.
9. Über das Paket htmlwidgets kann man HTML-Applikationen in R einlesen.
10. Das Paket cowplot bietet eine Vielzahl von interaktiven Diagrammen für R.
7
F (vgl. https://cran.r-project.org/web/packages/viridis/vignettes/intro-to-viridis.html), R, R, F, R,
F, R, R, F, F.
Fallstudie: Visualisierung
13
Lernziele
Diagramme für nominale Variablen erstellen können
Balkendiagramme mit Prozentpunkten auf der Y-Achse erstellen können
Balkendiagramme drehen können
Text-Labels an Balkendiagramme anfügen können
Farbschemata von Balkendiagrammen ändern können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(likert)
library(viridis)
library(scales)
library(tidyverse)
data(extra, package = "pradadata")
Eine recht häufige Art Daten in der Wirtschaft stammt aus Umfragen in der Belegschaft
oder von Kunden. Diese Daten gilt es dann aufzubereiten und graphisch wiederzugeben.
Das ist der Gegenstand dieser Fallstudie. Wir nutzen dazu den Datensatz extra. Neben
Extraversion wurden noch ein paar weitere Variablen erhoben (?extra); uns interessieren
hier aber nur die zehn Extraversionsitems, die zusammen Extraversion als Persönlichkeitseigenschaft messen (sollen). Wir werden die Antworten der Befragten darstellen, aber uns
hier keine Gedanken über Messqualität u. Ä. machen. Schauen Sie sich zum Einstieg die
Daten mit glimpse(extra) an.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_13
201
202
13
Fallstudie: Visualisierung
13.1 Umfragedaten visualisieren mit „likert“
Das Paket likert erlaubt komfortables Arbeiten mit likertskalierten Items. Im ersten
Schritt erstellen wir dafür den Dataframe, der die darzustellenden Items enthält. Allerdings
verdaut likert nur klassische Dataframes, keine neumodischen Tibbles, daher müssen
wir extra noch in einen data.frame umwandeln mit as.data.frame(). Außerdem
zieht likert den Variablentyp factor vor:
extra %>%
select(i01:i10) %>%
mutate_all(factor) %>%
as.data.frame() -> extra_items
Die Faktorstufen eines Items kann man sich mit levels(extra_items$i01) etc. anschauen.1 Im zweiten Schritt führen wir die Funktion likert() aus, die unsere Daten
aufbereitet, so dass sie dann einfach geplottet werden können. Betrachten Sie das ausgegebene Objekt, z. B. mit str(extra_items_likert):
extra_items %>%
likert() -> extra_items_likert
Das Objekt extra_items_likert hat das Attribut „likert“ (wie uns z. B. str() verrät). Übergeben wir ein Objekt mit diesem Typ an plot(), so wird automatisch die
„richtige“ Plot-Methode ausgeführt, mit ansehnlichem Ergebnis (s. Abb. 13.1).
plot(extra_items_likert)
Die Hauptarbeit haben wir geschafft; man könnte natürlich noch etwas an der Schönheit feilen. So könnte man die Items und die Antwortskala aussagekräftiger benennen,
einen Titel hinzufügen und die Farben ändern. Die Webseite zu likert2 und die Hilfeseiten (z. B. ?likert.options) dokumentieren die Einstellungen für diese Funktii01
i09
i04
i02r
i05
i06r
i07
i08
i10
i03
9%
10%
16%
21%
23%
25%
25%
29%
66%
77%
100
91%
90%
84%
79%
77%
75%
75%
71%
34%
23%
50
0
50
100
Percentage
Response
1
2
3
4
Abb. 13.1 Umfrageergebnisse visualisieren mit „likert“
1
2
Alle Items auf einmal werden durch die Funktion map(extra_items, levels) gezeigt.
http://jason.bryer.org/likert/.
13.2 Umfragedaten visualisieren mit ggplot
203
on. Schauen wir uns ein paar Optionen an. Zuerst benennen wir die Antwortstufen um;
anstelle von 1 ist ein Label wie „stimme nicht zu“ aussagekräftiger. Dazu kann man
likert::recode() verwenden; diese Funktion erwartet allerdings character als
Eingabe; dazu müssten wir etwas umständlich die Spalten erst in den Typ character
umwandeln; zum Schluss dann wieder zurück in den Typ factor für likert(). Daher
behelfen wir uns mit recode_factor() aus dplyr; zu beachten ist, dass auf der linken
Seite des Gleichheitszeichens die alte Kategorie und rechts die neue steht.
extra_items %>%
mutate_all(recode_factor,
"1" = "stimme nicht zu",
"2" = "stimme eher nicht zu",
"3" = "stimme eher zu",
"4" = "stimme voll und ganz zu") %>%
as.data.frame -> extra_items_r
Mit mutate_all werden die Funktionen, die mit funs() angegeben sind – hier nur
recode() –, auf alle Spalten von extra angewendet. Etwaige Parameter von der anzuwendenden Funktion werden mit Komma angefügt. Das Ergebnis gleicht Abb. 13.1, nur
die Labels der Antwortstufen haben sich geändert. Mit welchem Befehl/welchen Befehlen plotten Sie das Diagramm?3 Mit summary(extra_items_r_likert) kann man
sich eine Zusammenfassung der Item-Antworten geben lassen; ggf. kann der resultierende
Dataframe weiterverarbeitet werden.
13.2 Umfragedaten visualisieren mit ggplot
likert bietet eine starke Vorverarbeitung der Daten an, so dass man als Nutzer nur
noch wenig selber machen muss. Will man allerdings hohe Kontrolle über das Diagramm
und mit vielfach getesteten Werkzeugen arbeiten (likert ist noch neu), so bietet sich
ggplot an.
13.2.1 Daten aufbereiten
Typische Aspekte des Datenaufbereitens für die Auswertung von Umfragen sind:
Mittelwerte (o. Ä.) pro Person über zusammengehörige Items bilden (s. Abschn. 9.5.1)
Fehlende Werte prüfen und ggf. ersetzen (s. Abschn. 9.1.2 und folgende)
Items umpolen (s. Abschn. 9.4.1)
Daten von „breit“ auf „lang“ umformen (s. Abschn. 13.2.2)
Diese und weitere typische Aufräumarbeiten sind ausführlicher in Kap. 9 geschildert.
3
extra_items_r_likert <- likert(items = extra_items_r);
plot(extra_items_r_likert).
204
13
Fallstudie: Visualisierung
13.2.2 Daten umstellen
Führen wir uns ein Diagramm vor Augen (s. Abb. 13.2), bei dem auf der X-Achse die
Items stehen (1, 2, . . . , n) und auf der Y-Achse die Anzahl der Kreuze nach Kategorien.
Viele Grafik-Funktionen sind so aufgebaut, dass auf der X-Achse nur eine Variable steht.
ggplot2, das wir hier verwenden, ist da keine Ausnahme. Wir müssen also die „breite“
Tabelle (zehn Spalten, pro Item eine) in eine „lange Spalte“ umbauen: Eine Spalte heißt
dann „Itemnummer“ und die zweite „Wert des Items“ oder so ähnlich. Wie bisher wählen
wir aus der Fülle der Daten wieder die Spalten, die uns interessieren: die zehn Extraversionsitems (Spalten 3 bis 12).
extra_items <- dplyr::select(extra, 3:12)
Dann stellen wir die Daten von „breit“ nach „lang“ um, so dass die Items eine Variable
bilden und damit für ggplot2 gut zu verarbeiten sind.
extra_items %>%
gather(key = items, value = Antwort) %>%
mutate(items = factor(items),
Antwort = factor(Antwort)) -> extra_items_long
Warum wandeln wir die Item-Antworten in eine Faktorvariable um? Es waren doch so
schöne Zahlen! Betrachten Sie zur Erläuterung Abb. 13.2; wie man sieht, sind die Füllfarben der Balken den Stufen der Variablen Antwort zugeordnet. ggplot wird aber
diese diskrete Farbzuordnung nur vornehmen, wenn die Gruppierungsvariable (Antwort)
nominalskaliert ist (vom Typ factor oder character). Damit haben wir die nötige Vorbereitung geschafft. Jetzt wird gemalt.
13.2.3 Diagramme für Anteile
Stellen wir die Anteile der Antworten anhand von farbig gefüllten Balken dar (s.
Abb. 13.2). Beachten Sie, dass die Balken alle auf 1 (100 %) skaliert sind; es werden
also relative Häufigkeiten dargestellt. Absolute Häufigkeiten bleiben hier außen vor (s.
Abb. 13.2). Die Schriftgröße (aller Textelemente) kann man mit dem Argument text
steuern.
p1 <- ggplot(data = extra_items_long) +
aes(x = items) +
geom_bar(aes(fill = Antwort), position = "fill")
theme(legend.position = "bottom",
text = element_text(size = 4)) +
scale_fill_viridis(discrete = TRUE)
+
0.00
0.25
0.50
0.75
i01
Antwort
i02r
i03
1
i04
2
i05
items
i06r
3
Abb. 13.2 Balkendiagramm, unrotiert und rotiert
count
1.00
i07
4
i08
NA
i09
i10
i01
i02r
i03
i04
i05
i06r
i07
i08
i09
i10
0.00
Antwort
0.25
1
2
0.50
count
3
4
0.75
NA
1.00
13.2 Umfragedaten visualisieren mit ggplot
205
items
206
13
Fallstudie: Visualisierung
Was macht dieser ggplot-Befehl? Schauen wir es uns im Einzelnen an:
ggplot(data = ...): Wir sagen: „Ich möchte gern die Funktion ggplot nutzen, um den Datensatz . . . zu plotten.“
aes(...): Hier definieren wir die „aesthetics“ des Diagramms, d. h. alles
„Sichtbare“. Wir ordnen in diesem Fall der X-Achse die Variable items zu. Per
Standardeinstellung geht ggplot davon aus, dass Sie die Häufigkeiten der X-Werte
auf der Y-Achse haben wollen, wenn Sie nichts über die Y-Achse sagen. Jetzt haben
wir ein Koordinatensystem definiert (das noch leer ist).
geom_bar(): „Hey, R oder ggplot, jetzt male mal einen barplot in den ansonsten
noch leeren plot.“
aes(fill = Antwort): Genauer gesagt nutzen wir aes, um einem sichtbaren Aspekte des Diagramms (wie die X-Achse) eine Variable des Datensatzes
zuzuordnen. Jetzt sagen wir, dass die Füllung (im Balkendiagramm) durch die Werte von Antwort definiert sein sollen (also „1“, „2“ c.).
position = "fill" sagt, dass die Gesamt-Höhe des Balkens aufgeteilt werden soll mit den „Teil-Höhen“ der Gruppen (Antwort-Kategorien 1 bis 4); wir hätten
die Teil-Höhen auch nebeneinanderstellen können.
theme() definiert die Nicht-Datenteile des Diagramms wie Textgröße oder Position der Legende.
scale_fill_viridis() wendet die Viridis-Farbpalette zur Füllung der Geome an (nicht für deren Linien); dazu muss das Paket viridis verfügbar sein. Bei
diskreten Variablen, wie im vorliegenden Fall, muss der Parameter discrete =
TRUE ergänzt werden.
Wichtig ist hier das Argument position = "fill". Damit werden alle Balken
gleich hoch (100 %), so dass nur noch die „Pegelstände“, definiert durch die Füllfarben, informativ sind. Diese Art der Darstellung eignet sich zum Vergleich der Häufigkeit
von Subkategorien, z. B. der Häufigkeit von Antwortstufen zwischen Items. Vielleicht
ist es schöner, die NAs erst zu entfernen. Plotten Sie das Diagramm dann noch mal
extra_items_long <- drop_na(extra_items_long).
extra_items_long <- drop_na(extra_items_long)
ggplot(data = extra_items_long) +
aes(x = items) +
geom_bar(aes(fill = Antwort), position = "fill") +
scale_fill_viridis(discrete = TRUE)
13.2 Umfragedaten visualisieren mit ggplot
207
13.2.4 Rotierte Balkendiagramme
Dazu ergänzen wir die Zeile + coord_flip(); das heißt so viel wie „drehe das Koordinatensystem um 90 Grad“ (s. Abb. 13.2).
p1_flip <- p1 + coord_flip()
Übrigens: Auch bei „geflippten Koordinaten“ bleibt für ggplot2 die X-Achse die XAchse. In unserem Beispiel ist die Variable items der X-Achse zugeordnet, darauf hat
die Achsendrehung keinen Einfluss. Wenn Sie in aes()definieren x = items, dann
bleibt auch nach coord_flip() die X-Achse der Variable items zugeordnet. Halt, ein
Problem haben wir noch. Im Diagramm „liegen“ die größeren Kategorien (z. B. 4) „links“
von den kleineren Kategorien (z. B. 1). Die Reihenfolge der Kategorien sollte aber lieber
aufsteigend von links nach rechts sein. Daher lieber die Reihenfolge umkehren. Konkret
„drehen“ wir die Reihenfolge der Faktorstufen von Antwort mit rev (rev wie reverse).
extra_items_long %>%
mutate(Antwort = factor(Antwort,
levels = rev(levels(Antwort)))) ->
extra_items_long_rev
Um diese Syntax besser zu verstehen, vergleichen Sie einmal die Ausgaben dieser beiden
Zeilen:
levels(extra_items_long_rev$Antwort)
levels(extra_items_long_rev$Antwort) %>% rev
Jetzt plotten Sie die Daten wie gewohnt:4
extra_items_long_rev %>%
ggplot(aes(x = items)) +
geom_bar(aes(fill = Antwort), position = "fill") +
coord_flip()
Standardmäßig platziert ggplot2 geringe Werte auf einem kleineren Skalenwert der
Achse, näher am Achsenursprung also; das ist sinnvoll. Allerdings passt es dann nicht
mehr zur Leserichtung von oben nach unten. Drehen wir also die Reihenfolge der Kategorien um, so dass die Reihenfolge der Leserichtung entspricht (s. Abb. 13.3, links).
4
Unter http://docs.ggplot2.org/current/position_stack.html findet man einige Hinweise dazu aus der
Dokumentation von ggplot2.
208
13
Fallstudie: Visualisierung
Die Ergebnisse
der Kundenbefragung
i01
Ich bin das erste Item
i02r
Das zweite Item
i03
Item 03
i04
Beispiel für
Zeilenumbruch
items
i05
i06r
Item 05
Item 06
i07
Item 07
i08
Item 08
Item 09
i09
Item 10
i10
0.00
0.00
0.25
0.50
0.75
1.00
0.25
0.50
stimme nicht zu
stimme eher nicht zu
Antwort
NA
1
2
3
0.75
1.00
count
stimme eher zu
stimme zu
4
N = 826
Abb. 13.3 Itemnummern von oben nach unten aufsteigend (links); aussagekräftige Item-Labels
13.2.5 Text-Labels
Wir definieren die Texte („Labels“) für die Items:
item_labels <- c("Ich bin das erste Item",
"Das zweite Item",
"Item 3 sdjfkladsjk",
"Ein Couch-Potato UMKODIERT",
"i5 asf", "i6 sdf", "adfjks", "sfjlkd", "sdfkjl", "sdfjkl") %>% factor()
Jetzt hängen wir die Labels an die Items im Diagramm (s. Abb. 13.3, rechts).
ggplot(extra_items_long_rev, aes(x = items)) +
geom_bar(aes(fill = Antwort), position = "fill") +
coord_flip() +
scale_fill_viridis(discrete = TRUE) +
theme(legend.position = "bottom") +
labs(x = "", y = "") +
scale_x_discrete(labels = rev(levels(extra_items_long_rev$items))) +
guides(fill = guide_legend(reverse = TRUE)) -> p_itemnummern
Kommentieren Sie mal die Zeile mit coord_flip() aus, und betrachten Sie den Unterschied. Die Funktion rev(levels(data2$items) dreht die Reihenfolge der Fak-
13.2 Umfragedaten visualisieren mit ggplot
209
torstufen (levels) um (rev wie „reverse“). Dann sagen wir ggplot2, dass wir die
X-Achse (auf der eine diskrete Variable dargestellt ist), anpassen möchten. Genauer gesagt, übergeben wir einen Vektor (item_labels) mit den Bezeichnungen der Kategorien
der Achse. Man kann auch einen Zeilenumbruch in den Item-Labels erzwingen; wobei uns
das schon recht weit führt, aber für die Schönheit . . .
item_labels2 <- fct_inorder(c("Ich bin das erste Item",
"Das zweite Item",
"Item 03 ",
"Beispiel für\nZeilenumbruch",
"Item 05", "Item 06", "Item 07",
"Item 08", "Item 09", "Item 10"))
Mit fct_inorder() haben wir die Reihenfolge der Faktorstufen nach ihrem erstmaligen Auftreten im Vektor sortiert (s. Abschn. 5.2.2). Im rechten Teil der Abb. 13.3 ist
zu Demonstrationszwecken ein anderes Farbschema verwendet. Natürlich kann man auch
eigene Farbschemata definieren:
meine_palette <- c("red", "green", "blue", "yellow")
p2 <- p_itemnummern + scale_fill_manual(values = meine_palette)
Über Geschmack lässt sich streiten. Oder auch nicht: Im Zweifelsfall ist davon abzuraten,
sich eine eigene Palette zusammenzustellen. Es ist gar nicht so leicht, eine gute Auswahl
zu treffen (z. B. gute Kontraste zwischen allen Farben der Paletten). Jedenfalls lohnt sich
ein Blick auf die Auswahl der 657 Farben in R mit colors(). Wer sich die BrewerFarbpaletten anschauen will, kann das mit display.brewer.all() aus dem Paket
rcolorbrewer tun (vgl. Kap. 12).
Außerdem haben wir noch Labels für die Antwortstufen hinzugefügt; dazu haben wir
die Faktorstufen von Antwort entsprechend geändert. Zur Sicherheit überschreiben wir
Antwort nicht, sondern speichern den Vektor mit den umgedrehten Antwortstufen als
Antwort2 ab.5
antwort_labels_rev <- fct_inorder(c("stimme zu",
"stimme eher zu",
"stimme eher nicht zu",
"stimme nicht zu"))
extra_items_long_rev$Antwort2 <- extra_items_long_rev$Antwort
levels(extra_items_long_rev$Antwort2) <- antwort_labels_rev
5
Alternativ hätten wir die Faktorstufen umbenennen können, vgl. ?fct_relevel aus forcats.
210
13
Fallstudie: Visualisierung
Den Titel der Legende der Füllfarben („Antwort“) kann man unterdrücken; der Informationswert ist gering. Im linken Teil von Abb. 13.3 wurde dafür die Zeile, die die Füllfarbe
der Geome beschreibt, wie folgt geändert: scale_fill_XXX(discrete = TRUE,
name = ""). Mit dem Argument name kann man den Namen (d. h. den Titel) der zugehörigen Legende ändern. Hier haben wir ihn auf einen leeren Wert gesetzt. Falls Sie
sich über das „\n“ in der Definition des Titels der Abbildung wundern: Damit wird ein
Zeilenumbruch erzeugt. Ornamente, wie Schriftgröße, werden bei ggplot über theme()
gesteuert. Hier haben wir dem Text der Achsen sowie dem Titel des Plots eine relative
Schriftgröße von 70 % verordnet (in Relation zur Voreinstellung von ggplot).
p_itemlabels <extra_items_long_rev %>%
drop_na() %>%
ggplot(aes(x = items)) +
geom_bar(aes(fill = Antwort2), position = "fill") +
coord_flip() +
scale_fill_grey(name = "",
guide = guide_legend(reverse = TRUE,
nrow = 2,
keywidth = 0.5,
keyheight = 0.5)) +
scale_x_discrete(labels = rev(levels(item_labels2))) +
labs(title = "Die Ergebnisse \nder Kundenbefragung",
caption = paste0("N = ",nrow(extra))) +
theme(axis.text = element_text(size = rel(0.7)),
plot.title = element_text(size = rel(0.7)),
legend.position = "bottom",
legend.text = element_text(size = rel(.5)))
gridExtra::grid.arrange(p_itemnummern, p_itemlabels, nrow = 1)
Die Legende kann man mit guide_legend() ansteuern; das ist nützlich, wenn man
Dinge wie Ausrichtung und Schriftgröße der Legende ändern möchte. Den Namen der
Legende spricht man mit der Funktion scale_XXX_XXX() mit dem Argument name
an. Diese Funktion bezieht sich auf das jeweilige Mapping von Variablen zu einem „Ästhetikum“ wie die Füllfarbe eines Geoms; in diesem Fall wird Antwort2 auf Graustufen abgebildet („gemapt“). Entsprechend haben wir guide_legend() innerhalb von
scale_fill_grey() aufgerufen. Mit ?guide_legend bekommt man einen Überblick über die Argumente. Ein Teil der Feinsteuerung läuft über theme(); so wird die
Lage der Legende im Plot (bottom, right, left, top) über theme() gesteuert. Die fehlenden
Werte sind nur deshalb gelöscht, damit nicht zu viele Graustufen vom Auge unterschieden werden müssen. Fehlende Werte hätten eine optisch abgesetzte Graustufe verdient,
was nicht leicht umzusetzen ist.
13.2 Umfragedaten visualisieren mit ggplot
211
13.2.6 Diagramm beschriften
Überschrift, Unter-Überschrift oder „Figure Caption“ – all diese Betitelungen eines Diagramms kann man in ggplot2 recht komfortabel ändern:
p2 + labs(title = "Häufigkeiten der Antworten",
subtitle = paste0("N = ",nrow(extra)),
caption = "Die Daten wurden 2016 erhoben")
Mit theme kann man alle „Nicht-Daten-Elemente“ des Diagramms einstellen, aber
nicht Text zuweisen wie z. B. Überschriften. Für Letzteres verwendet man labs oder
scale_XXX:
p2 + labs(title = "Häufigkeiten der Antworten",
subtitle = "N = viel",
caption = "Die Daten wurden 2016 erhoben") +
theme_classic() +
theme(axis.ticks = element_blank(),
axis.text = element_text(size = 6))
Mit ?theme bekommt man weitere Informationen zu den „Design-Themen“ von ggplot2.
Wo wir schon dabei sind, haben wir noch die Position und den Titel der Legende geändert.
Wenn man das Seitenverhältnis (Y:X) des Diagramms einstellen möchte, geht das mit
aspect_ratio: z. B. p2 + theme(aspect.ratio = .61).
13.2.7 Balken mit Häufigkeitswerten
Der generelle Eindruck, den das Diagramm gut transportiert, ist wichtiger als die Details, aber manchmal sind die exakten Zahlen doch von Interesse. Darüber hinaus würde
Tufte vielleicht argumentieren, dass man diese Zahlen ruhig (eher klein) auf das Diagramm aufbringen darf (Tufte 2001). Schließlich soll, so Tufte, ein gutes Diagramm mehr
Details offerieren, wenn man gleichsam näher an das Diagramm herangeht; Diagramme sollen reich an Informationen sein, sonst braucht man sie nicht (allerdings sollten sie
übersichtlich sein). Reich an Information ist etwas anderes als reich an Schnörkeln. Um
die Häufigkeitswerte pro Kategorie zu bekommen, müssen wir sie selber berechnen:
extra_items_long_rev %>%
filter(items == "i01") %>%
count(Antwort) %>%
mutate(n_prop = n / sum(n)) -> extra_items_count
Im Folgenden übergeben wir keine Rohwerte, sondern aggregierte Werte (Häufigkeiten).
Damit ändert sich die Syntax. ggplot() muss jetzt nicht mehr selber die relevanten
Zeilen zählen, um die Höhe der Balken zu bestimmen. Vielmehr übergeben wir die richtige Zeilenanzahl an ggplot; diesem Wert soll die Höhe des Balkens jetzt zugeord-
212
13
Fallstudie: Visualisierung
net werden. Das kann man mit geom_col() erreichen (oder mit geom_bar(stat =
"identity", ...)). mit vjust kann man die vertikale Verankerung justieren. Außerdem verringern wir noch die Schriftgröße (s. Abb. 13.4, links).
legend_position <- c(.8, .7)
extra_items_count %>%
ggplot() +
aes(x = Antwort, y = n) +
geom_col(aes(fill = Antwort)) +
geom_text(aes(label = n), vjust = 1.5, size = 2)+
theme(legend.position = legend_position,
text = element_text(size = 6)) -> p5
Bei der Gelegenheit haben wir die Legende in das eigentliche Diagramm gepackt; dem
Argument legend.position wird dabei ein Vektor mit zwei Elementen übergeben, die
der X- und der Y-Koordinate entsprechen. c(0, 0) korrespondiert zur linken unteren
Ecke des Diagramms und c(1, 1) zur rechten oberen. Das eigentliche Darstellen der
numerischen Werte geht mit geom_text, welches Daten einen Text (z. B. eine gedruckte
Zahl im Diagramm) zuordnet.
Statt absoluten Häufigkeiten wären Prozentzahlen genehm (s. Abb. 13.4, Mitte):
extra_items_count %>%
ggplot() +
aes(x = Antwort, y = n_prop) +
geom_col(aes(fill = Antwort)) +
geom_text(aes(label = percent(round(n_prop, 2))), vjust = 1.5, size = 2) +
theme(legend.position = legend_position,
text = element_text(size = 6)) +
scale_y_continuous(labels = percent) -> p6
13.2.8 Sortieren der Balken
Es ist sinnvoll, die Balken nach ihrer Höhe zu sortieren. Allerdings ist die Standardreihenfolge der Balken bei ggplot die, die von der Reihenfolge der Faktorstufen vorgegeben
ist:
levels(extra_items_count$Antwort)
#> [1] "4" "3" "2" "1"
Möchten wir die Balken der Höhe nach sortieren, so bietet es sich an, die Faktorstufen neu zu ordnen: reorder(zu_sortierender_Vektor, neue_Reihenfolge).
Dabei wird der ersten Faktorstufe von zu_sortierender_Vektor der kleinste Wert
von neue_Reihenfolge zugeordnet. Möchte man stattdessen der ersten Faktorstufe
den größten Wert zuordnen, so hilft ein Minuszeichen vor neue_Reihenfolge (s.
Abb. 13.4, rechts):
0
100
200
300
4
371
3
374
Antwort
2
66
1
9
NA
6
NA
1
2
3
4
n_prop
Abb. 13.4 Balkendiagramme mit Zahlen und sortiert
n
Antwort
0.0%
10.0%
20.0%
30.0%
40.0%
4
3
45.0% 45.0%
Antwort
2
8.0%
1
1.0%
NA
1.0%
NA
1
2
3
4
Antwort
0.0%
10.0%
20.0%
30.0%
40.0%
3
2
8.0%
1
1.0%
NA
1
2
3
4
NA
1.0%
reorder(Antwort, -n_prop)
4
45.0% 45.0%
Antwort
13.2 Umfragedaten visualisieren mit ggplot
213
n_prop
214
13
Fallstudie: Visualisierung
library(tidyverse)
extra_items_count %>%
ggplot() +
aes(x = reorder(Antwort, -n_prop), y = n_prop) +
geom_col(aes(fill = Antwort)) +
geom_text(aes(label = percent(round(n_prop, 2))), vjust = 1.5, size = 2) +
theme(legend.position = legend_position,
text = element_text(size = 6)) +
scale_y_continuous(labels = percent) -> p7
Aufgaben
Wie viele numerische Daten gibt es in dem Datensatz extra?6
Wie viele fehlende Werte gibt es?7
Wie ist der Wertebereich (min, max) der Items?8
Ist dieser Wertebereich für alle Items gleich?9
Spielen Sie mit den Füllfarben von Abb. 13.3. Probieren Sie dazu diesen Befehl aus scale_fill_brewer(palette = 17). Dieser Befehl ersetzt die andere Anweisung für Füllfarben, die sich in dem Call befindet
(scale_fill_XXX).10
6. Mit welchem Befehl steuert man die Optionen zur Darstellung der Legende
an?11
7. Mit welchem Befehl kann man die Achsen in ggplot beschriften?12
8. Sie möchten ein Balkendiagramm darstellen, und zwar so, dass die Balken der
Größe nach sortiert sind. Leider macht das ggplot nicht in der Voreinstellung.
Wie können Sie ggplot Ihren Wunsch verklickern?13
9. Wie können Sie das Diagramm (bzw. das Koordinatensystem des Diagramms)
um 90 Grad drehen?14
10. Nennen Sie eine Funktion, die Umfragedaten mit wenig Aufwand recht ansprechend visualisiert.15
1.
2.
3.
4.
5.
6
extra %>% select_if(is.numeric) %>% ncol().
sum(is.na(extra)).
8
extra %>% select(i01:i10) %>% summarise_all(funs(min, max), na.rm
= TRUE).
9
Ja.
10
Diese Palette nutzt erdfarbene Töne.
11
Zum Beispiel über guide_legend().
12
Zum Beispiel mit labs().
13
Zum Beispiel mit aes(x = reorder(Antwort, n_prop)).
14
coord_flip().
15
likert::plot().
7
Geovisualisierung
14
Lernziele
Grundlegende Aspekte der Visualisierung von Kartenmaterial kennen
Kartenmaterial mit anderen Daten assoziieren
Erste Erfahrungen mit Listenspalten sammeln
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(viridis)
library(stringr)
library(gridExtra)
library(sf)
library(rworldmap)
library(leaflet)
library(ggmap)
library(googleVis)
library(datasets)
library(stringr)
library(ISOcodes)
data(socec, package = "pradadata")
data(cult_values, package = "pradadata")
data(wahlkreise_shp, package = "pradadata")
data(wellbeing, package = "pradadata")
data(elec_results, package = "pradadata")
data(countries, package = "pradadata")
Karten zeichnen, um statistische Auswertungen mit geographischen Daten zu verknüpfen
– eine schöne Sache, ist Gegenstand dieses Kapitels.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_14
215
216
14
Geovisualisierung
14.1 Kartendaten
Wenig überraschend ist Kartenmaterial die Grundlage für dieses Thema. Genauer gesagt
benötigen wir drei Arten von Informationen:
1. Zum einen die reinen geographischen Informationen (Geo-Daten1 ), vor allem Grenzen
von Verwaltungseinheiten wie Staaten oder Landkreisen, aber auch andere Geo-Daten
wie Flüsse oder Seen. Diese Daten sind als Vektorarten gespeichert (Linien, Polygone).
2. Zum anderen semantische Informationen, die mit rein geographischen Informationen
verknüpft sind, also z. B. die Hauptstadt eines Landes.
3. Dazu kommen dann noch drittens Strukturdaten und sonstige statistische Informationen, die wiederum mit den semantischen Informationen verknüpft sind.
14.1.1 Geo-Daten der deutschen Verwaltungsgebiete
Die Geo-Daten werden wir im Shape-Format verarbeiten – eines der am weitesten verbreiteten Formate für Geo-Daten. Das Shape-Format besteht aus mindestens drei Dateien
(.shp, .dbf, .shx); achten Sie darauf, dass diese drei Arten von Dateien verfügbar sind (im
gleichen Ordner). Eine Shape-Datei für Deutschland kann vom Bundesamt für Kartografie
und Geodäsie (BKG)2 heruntergeladen werden; dort sind auch die Nutzungsbedingungen3 geregelt. Man kann die Daten dort kostenfrei herunterladen; bereits in aufbereiteter
Form bekommt man sie auch unter diesem Link: https://osf.io/nhy4e/, wobei man noch
?action=download anfügt. Da der Download recht groß ist (ca. 13 MB), prüfen wir
zuerst, ob die Zieldatei dest_file schon existiert (file.exists()); nur falls dem
nicht so ist, laden wir die Zieldatei herunter (download.file()) und lesen die Datei
dann ein:
url <- "https://osf.io/nhy4e/?action=download"
dest_file <- "data/de_L.RData"
if (!file.exists(dest_file)) download.file(url, dest_file)
load(dest_file)
load("data/de_L.RData")
Die Daten sind komplex; ein Blick in die Dokumentation (im Download enthalten) ist
sinnvoll (s. Ordner Dokumentation bei den vom BKG heruntergeladenen Daten). In der
„Linien-Datei“ für Deutschland (De-L) steht die Spalte AGZ für Art der Grenze, wobei 1
für die Staatsgrenze steht (vgl. vg250.pdf). RDG steht für rechtliche Definition der Grenze
mit 1 D festgelegt. GM5 steht für die Grenzmerkmale von Verwaltungsgemeinschaftsgren1
Man spricht auch von Geo-Info-Systemen, GIS.
http://www.bkg.bund.de; Ordner „vg250“.
3
http://www.geodatenzentrum.de/auftrag/pdf/geonutz.pdf; (GeoNutzV). Die Daten wurden am 9.
Okt. 2017 heruntergeladen.
2
14.1 Kartendaten
217
54°N
54°N
52°N
52°N
50°N
50°N
48°N
48°N
6°E
8°E 10°E 12°E 14°E
6°E
8°E 10°E 12°E 14°E
Abb. 14.1 Verwaltungsgrenzen von Bund (links) und Ländern (rechts)
zen (ein Wort im besten Amtsdeutsch; für uns nicht weiter von Belang). Diese Variablen
sind relevant für die Datei, die die Linien (L) kodiert; für Punkte (P) oder Flächen (F) gibt
es weitere Variablen, man konsultiere die Dokumentation dafür. In der „Flächen-Datei“
steht GF für Geofaktor, wobei 1 und 2 Gewässer markieren. BSG steht für besondere Gebiete; für uns ohne Belang. Interessanter ist RS, der Regionalschlüssel, der die einzelnen
Ebenen der Verwaltungseinheiten kodiert.
Hat man Shape-Daten als eigene Dateien vorliegen, so ist die Funktion st_read()
von Nutzen; sie lädt sowohl Geo-Daten als auch sonstige Daten in einen Dataframe. Die
ganze Geo-Info findet sich einer Spalte (geometry) des Dataframes wieder. Interessant
ist, dass in der Spalte geometry (pro Zeile) mehrere Informationen gespeichert sind. Man
spricht von einer Listenspalte. Praktischerweise kümmern sich st_read() und seine
Freunde um diesen speziellen Datentyp. Das Argument quiet = TRUE unterdrückt die
Ausgabe einer Info, die die Funktion sonst redseligerweise von sich geben würde.
Wohlan! Eine Karte von Deutschland, genauer gesagt, aller Verwaltungseinheiten ist
leicht zu erhalten:
ggplot(data = de_L) +
geom_sf(size = .1, color = "lightgrey")
Allein, es dauert lang, denn es gibt doch einiges zu sehen (bzw. einige Grenzen zu ziehen)
in Deutschland. Begrenzen wir uns auf die Bundesländer. Es gibt mehrere Wege dorthin,
z. B. ist in der Flächen-Datei die Staatsgrenze mit AGZ = 1 definiert (s. Abb. 14.1, links);
die Ländergrenzen sind mit AGZ = 2 definiert (s. Abb. 14.1, rechts).
de_L %>%
filter(AGZ %in% c(1)) %>%
ggplot +
geom_sf() -> p_de1
218
14
Geovisualisierung
de_L %>%
filter(AGZ %in% c(1,2)) %>%
ggplot +
geom_sf() -> p_de2
gridExtra::grid.arrange(p_de1, p_de2, nrow = 1)
14.1.2
Daten der Wahlkreise
Alternativ zu den Karten der Verwaltungsgebiete bietet der Bundeswahlleiter (2017b) eine
Karte mit den Wahlkreisen an.4 . Das ist ganz praktisch, weil dort auch sozioökonomische
Daten zur Verfügung stehen, die den Wahlkreisen zugeordnet sind.5 Die Wahlkreise-GeoDaten der Bundestagswahl 2017 sind im Paket pradadata enthalten; sie stammen vom
Bundeswahlleiter und sind nur etwas aufbereitet.
data("wahlkreise_shp")
glimpse(wahlkreise_shp)
#> Observations: 299
#> Variables: 5
#> $ WKR_NR
<int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1...
#> $ LAND_NR
<fct> 01, 01, 01, 01, 01, 01, 01, 01, 01, 01, 01, 13, 13, ...
#> $ LAND_NAME <fct> Schleswig-Holstein, Schleswig-Holstein, Schleswig-Ho...
#> $ WKR_NAME <fct> Flensburg - Schleswig, Nordfriesland - Dithmarschen ...
#> $ geometry <MULTIPOLYGON [m]> MULTIPOLYGON (((543475 6076..., MULTIPO...
In diesem Dataframe gibt es vier Spalten mit Daten zu den Verwaltungsgebieten: WKR_Nr,
die Ordnungsnummer des Wahlkreises; LAND_NR, die Ordnungsnummer des Bundeslandes; LAND_NAME, den Namen des Bundeslandes; WKR_NAME, den Namen des Wahlkreises
(vgl. wahlkreise_shp). In der Spalte geometry sind wiederum die Geo-Daten gespeichert; auch hier handelt es sich um eine Listenspalte, d. h. um eine Spalte, die aus mehreren
„Spalten“ besteht. Laden wir dazu noch die vom Bundeswahlleiter bereitgestellten sozioökonomischen Daten und bringen beides in eine Karte. In socec sind die Spaltennamen
nur mit einer laufenden Nummer kodiert (Probieren Sie glimpse(socec)); ihr Inhalt
kann über ?socec oder über den Dataframe socec_dict ausgelesen werden.
4
https://www.bundeswahlleiter.de/bundestagswahlen/2017/wahlkreiseinteilung/downloads.html
Alle Daten zu den Wahlergebnissen, die hier präsentiert werden, stammen vom Bundeswahlleiter,
Wiesbaden 2017; alle Rechte liegen dort, vgl. https://www.bundeswahlleiter.de/info/impressum.
html.
5
https://www.bundeswahlleiter.de/bundestagswahlen/2017/strukturdaten.html.
14.2 Unterschiede in Kartensegmenten visualisieren
219
14.2 Unterschiede in Kartensegmenten visualisieren
14.2.1
Karte der Wahlkreise gefärbt nach Arbeitslosigkeit
Färben wir nun die Wahlkreise (allgemeiner: Kartensegmente) nach ihrer Arbeitslosigkeit ein; die Arbeitslosenquote findet sich als V47 (zum Stand vom März 2017). Vorher
müssen wir noch die Geo-Daten mit den Strukturdaten vereinen. Dafür benötigen wir aus
beiden Dataframes eine ID-Spalte, die die Wahlkreisnamen (oder -nummern) kodiert. Mit
left_join führen wir dann die Vereinigung durch. Wie Sie in der folgenden Syntax
sehen, kann man select() auch verwenden, um Spalten umzubenennen; rename()
leistet das auch, wählt aber alle Spalten aus; select() nur die angesprochenen (und
dann umbenannten).
socec_short <- socec %>% # Spalten wählen und umbenennen
select(WKR_NAME = V03,
WKR_NR = V02,
wegzug = V11, # Wegzugssalde
migration = V19, # Migrationsanteil
auslaender = V08, # Auslaenderanteil
alq = V47, # Arbeitslosenquote
migration = V19) # Anteil mit Migrationshintergrund
socec_short %>% # Plotten (Arbeitslosigkeit)
left_join(wahlkreise_shp) %>%
ggplot +
aes(fill = alq) +
theme_void()
geom_sf() -> p1
socec_short %>% # Joinen und Plotten (Migration)
left_join(wahlkreise_shp) %>%
ggplot +
aes(fill = migration) +
scale_fill_viridis() +
theme_void() +
geom_sf() -> p2
Wir haben nur Geo-Daten für 299 Wahlkreise, aber 316 Sätze mit Strukturdaten; auf diese
Inkonsistenz macht uns die Funktion aufmerksam (diese Ausgabe ist hier nicht wiedergegeben). Außerdem ist die ID-Spalte einmal als Faktor, einmal als Textvariable definiert.
Beide Warnungen schlagen wir für einen Moment in den Wind und betrachten lieber
das Ergebnis in Abb. 14.2. Im linken Teilbild ist die Arbeitslosenquote abgebildet; im
rechten die Migrationsquote – ein zentrales Gesprächsthema im Wahlkampf 2017. Das
Farbschema im rechten Teilbild greift die Unterschiede dramatischer, akzentuierter und
besser abgrenzbar ab als das linke Farbschema. Das Thema void blendet Achsen (und
Hintergrund) aus; da diese hier nicht von Belang sind. Ähnliche Analysen lassen sich
220
14
Geovisualisierung
Abb. 14.2 Arbeitslosigkeit nach Wahlkreis
analog für die anderen sozioökonomischen Indikatoren durchführen, die sich in socec
finden.
Aufgaben
1. Visualisieren Sie in gleicher Manier die Bevölkerungsdichte pro Wahlkreis.6
2. Verwenden Sie verschiedene Farbpaletten; experimentieren Sie!7
3. Wenn in einer Farbpalette geringe Werte mit Rot, hohe Werte mit Grün assoziiert
sind, kann das für manche Visualisierungen unpassend sein (z. B. Arbeitslosigkeit). Wie kann man diese Zuordnung „umdrehen“?8
14.2.2
Wahlergebnisse nach Wahlkreisen
Nach dem gleichen Prinzip können wir die Wahlergebnisse nach Wahlkreisen visualisieren; nehmen wir den Anteil gültiger Zweitstimmen für die AfD. Diese Daten finden sich
6
Wie in der genannten Syntax nur mit V09.
Hinzufügen von z. B. + scale_fill_gradient(low = "green",high = "red")
oder scale_fill_distiller(palette = "Greens", direction = +1) zum
ggplot-Call.
8
Bei scale_fill_distiller() gibt es den Parameter direction, den man auf 1 setzen
kann, was diese Umkehrung besorgt; vgl. ?scale_fill_distiller.
7
14.2 Unterschiede in Kartensegmenten visualisieren
221
beim Bundeswahlleiter (Bundeswahlleiter, 2017b); das Paket pradadata stellt diese Daten in etwas aufgeräumter Form zur Verfügung.9
Pro Partei finden sich vier Spalten:
1.
2.
3.
4.
Anteil gültiger Erststimmen dieser Wahl
Anteil gültiger Erststimmen in der letzten Wahl
Anteil gültiger Zweitstimmen dieser Wahl
Anteil gültiger Zweitstimmen in der letzten Wahl
Zurück zur Frage, warum es mehr Wahlergebnisse gibt als Wahlkreise. Ein Blick in
wahlkreise_shp zeigt, dass nicht nur die Wahlkreise, sondern auch zusätzlich die
Bundesländer aufgeführt sind. Deren übergeordneter Wahlkreis (parent) ist die Bundesrepublik, mit 99 kodiert. Probieren Sie mal aus:
elec_results %>%
select(parent_district_nr, district_name, district_nr) %>%
filter(parent_district_nr == 99)
Die 16 Bundesländer entfernen wir für die folgende Analyse; dann erstellen wir wieder
die eingefärbte Karte:
elec_results %>%
filter(parent_district_nr != 99) -> elec_results
elec_results %>%
select(WKR_NR = district_nr, AfD_3, votes_3) %>%
mutate(afd_prop = AfD_3 / votes_3) %>%
left_join(wahlkreise_shp) %>%
ggplot() +
aes(fill = afd_prop) +
geom_sf() +
scale_fill_viridis() +
theme_void() +
labs(caption = "Anteil gültiger Zweitstimmen für die AfD",
fill = "") -> p3
Das Farbschema stellt schön die Unterschiede zwischen den Anteilen heraus (vgl.
Abb. 14.3, linke Seite). Interessanterweise ist ein ziemlich klares Muster erkennbar;
Ostdeutschland ist deutlich „heller“, weist also höhere AfD-Quoten auf. Der Westen und
der Norden zeigen die geringsten AfD-Ergebnisse.
9
Der Autor/Urheber und Rechteinhaber ist der Bundeswahlleiter. Mit data(elec_results,
package = "pradadata") können Sie die Daten laden.
222
14
Geovisualisierung
0.2
0.3
0.1
0.2
0.0
0.1
Anteil gültiger Zweitstimmen für die AfD
Abweichung vom geschätzten AfD-Anteil
Abb. 14.3 AfD-Wahlergebnisse nach Wahlkreisen
14.2.3 Zusammenhang von Arbeitslosigkeit und AfD-Wahlergebnis
Man könnte die Hypothese vertreten, dass das AfD-Wahlergebnis mit der Arbeitslosigkeit
anstiege. Überprüfen wir diese Hypothese und stellen das Ergebnis wiederum auf der
Karte dar. Dazu sagen wir die AfD-Quote anhand der Arbeitslosigkeit mit einem linearen
Modell vorher (einfache Regression). Die Wahlkreise färben wir ein, je nachdem, wie
gut sie zur Vorhersage passen (also nach der Größe des Vorhersagefehlers). Mehr zum
linearen Modell findet sich im Kap. 18. Zuerst bereiten wir uns einen passenden Datensatz
(afd_df), dann plotten wir das Ergebnis (vgl. Abb. 14.3, rechte Seite).
elec_results %>%
select(WKR_NR = district_nr, AfD_3, votes_3) %>%
mutate(afd_prop = AfD_3 / votes_3) %>%
inner_join(wahlkreise_shp) %>%
inner_join(socec_short) %>%
mutate(afd_vorhersagefehler1 = lm(afd_prop ~ alq, data = .)$residuals) ->
afd_df
Übersetzen wir die Syntax auf Deutsch:
Nimm den Datensatz elec_results UND DANN
wähle die Spalten Distriktnummer, AfD- und Gesamtstimmen aus UND DANN
vereinige mit den Shape-Daten UND DANN
vereinige mit den Strukturdaten UND DANN
erzeuge eine Spalte mit dem Vorhersagefehler.
14.2 Unterschiede in Kartensegmenten visualisieren
223
Der inner_join() vereinigt Zeilen mit gleicher ID; dabei werden nur Zeilen behalten,
die sich in beiden zu vereinigenden Dataframes finden. Der AfD-Vorhersagefehler
zeigt, um wie viel Prozentpunkte das AfD-Wahlergebnis eines Wahlkreises von der Prognose anhand der lokalen Arbeitslosigkeit abweicht. Die Funktion lm() liefert als Ergebnis
eine Liste mit mehreren Elementen zurück; einer davon heißt residuals. Mit diesem
Wert füllen wir die Wahlkreise nach bekannter Manier:
afd_df %>%
ggplot +
aes(fill = afd_vorhersagefehler1) +
geom_sf() +
labs(caption = "Abweichung vom geschätzten AfD-Anteil",
fill = "") +
scale_fill_viridis() +
theme_void() -> p4
grid.arrange(p3, p4, nrow = 1)
Auch hier zeigt sich wieder ein klares Muster: Der Osten ist farblich erkennbar abgesetzt
vom Rest der Republik. Hier ist der AfD-Anteil höher, als es die Arbeitslosigkeit erwarten ließe. Umgekehrtes gilt für den westlichen Rand der Republik; dort ist der AfD-Anteil
geringer, als eine Vorhersage durch die Arbeitslosigkeit erwarten lassen würde. Die Analyse zeigt, dass mittels Visualisierung einige Überraschungen und erhellende Momente
möglich sind.
14.2.4
Ein komplexeres Modell
Gerade eben haben wir versucht, den AfD-Erfolg anhand einer Variablen (der Arbeitslosigkeit) zu verstehen. Sicher wird unser Verständnis besser, wenn wir mehrere Variablen
zur Vorhersage verwenden? Natürlich basiert die Auswahl an Variablen, die wir gleich
treffen werden, auf ausdifferenzierten und bewährten Theorien . . . Nur ist der Seitenrand
hier zu schmal, um diese aufzuführen . . . Sei’s drum. Nehmen wir folgende Prädiktoren, um den AfD-Anteil vorherzusagen: Anteil der Menschen mit Migrationshintergrund
(V19), Wegzugs-Saldo (V11), Arbeitslosigkeit (V47) und Ausländeranteil (V8).
afd_df %>%
mutate(afd_vorhersagefehler2 =
lm(afd_prop ~ alq + wegzug + migration + auslaender,
data = .)$residuals) -> afd_df
Genau wie beim letzten Modell haben wir wieder den Abweichungsfehler berechnet.
Das Ergebnis ist in Abb. 14.4 (links) zu sehen. Auf den ersten Blick sieht das Ergebnis ähnlich aus wie beim vorherigen Modell. Allerdings sollten wir uns überlegen, ob
das Farbschema für Vorhersagefehler gut passt. Vorhersagefehler können entweder etwa
224
14
Geovisualisierung
0.15
0.15
0.10
0.10
0.05
0.05
0.00
0.00
-0.05
-0.05
-0.10
-0.10
Abb. 14.4 Ein komplexeres Erklärungsmodell zum AfD-Erfolg; mehrere Strukturparameter wurden zur Vorhersage des AfD-Wahlerfolgs herangezogen
null sein – oder positiv oder negativ. Wir brauchen also eine Palette, die einen farblichen
Mittelpunkt hat, und Farben, die Abweichungen nach unten bzw. nach oben markieren.
Man spricht hier von einer divergierenden Farbpalette (s. Abb. 14.4, rechts). Übrigens:
scale_fill_distiller() erweitert ein Farbspektrum mit einer begrenzten Anzahl
von Farben, so dass es für kontinuierliche Variablen verwendbar ist.
afd_df %>%
ggplot +
aes(fill = afd_vorhersagefehler2) +
geom_sf() +
labs(fill = "") +
theme_void() -> p5
grid.arrange(p5 + scale_fill_viridis(),
p5 + scale_fill_distiller(palette = "Spectral"),
nrow = 1)
14.3 Weltkarten
14.3.1 rworldmap
Das R-Paket rworldmap bietet Karten und Strukturdaten für 244 Länder der Erde; der
Datensatz countriesLow beinhaltet diese Informationen. Das Paket hat eine eigene
Plot-Funktion, mit plot(countriesLow) bekommt man eine Weltkarte. Daneben gibt
14.3 Weltkarten
225
Abb. 14.5 Einfaches Choropleth-Diagramm
es eine Funktion, mit der man eigene Daten zu den Ländern des Datensatzes hinzufügen kann, z. B. die Information will_ich_hin. Ländernamen kann man z. B. nach der
Isonorm ISO 3166-1 alpha-310 eingeben. Sagen wir, unser Vektor will_ich_hin sei
bestückt mit folgenden Ländern:
will_ich <- data_frame(
hin = c("TTO", "COK", "MNG", "CAN"),
ja_wirklich = rep("will ich hin", times = 4)
)
# rep wie repeat
Diesen Datensatz mappen (joinen) wir jetzt zu countriesLow (s. Abb. 14.5). Dazu nutzen wir die Funktion joinCountryData2Map(). Mit mapCountryData() wird dann
die eingefärbte Karte gezeichnet:
will_ich_hin_map <- joinCountryData2Map(will_ich,
joinCode = "ISO3",
nameJoinColumn = "hin",
verbose = TRUE)
mapCountryData(will_ich_hin_map, nameColumnToPlot="ja_wirklich",
catMethod = "categorical",
missingCountryCol = gray(.8),
addLegend = FALSE,
mapTitle = "")
10
https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3.
120 W
60 W
0
Abb. 14.6 Geo-Visualisierung mit sf
50 S
0
50 N
60 E
120 E
60 S
40 S
20 S
0
20 N
40 N
60 N
80 N
120 W
60 W
0
60 E
120 E
226
14
Geovisualisierung
14.3 Weltkarten
227
14.3.2 rworldmap mit geom_sf
Das Paket sf und die Funktion geom_sf() lassen jedoch ein komfortableres Arbeiten
zu, und die Darstellung sieht schick aus (s. Abb. 14.6, links). Was ist der Vorteil, wenn wir
das Kartenobjekt in ein Objekt vom Typ sf umwandeln? Das Objekt countriesLow ist
ein kompliziertes, mehrfach verschachteltes Objekt. Wandeln wir es in ein sf-Objekt um,
so können wir mit den gewohnten Schritten arbeiten.
world_sf <- st_as_sf(countriesLow)
p_world_sf1 <- ggplot() +
geom_sf(data = world_sf) +
aes(fill = REGION) + # Füllfarbe entsprechend der Region
theme(legend.position = "none")
Das Objekt world_sf hat 50 Spalten; es lohnt sich, dieses recht komplizierte Objekt
einmal genauer zu betrachten. Z. B. können wir einfach nach Europa filtern (s. Abb. 14.6,
rechts).
p_world_sf2 <- world_sf %>%
filter(REGION == "Europe") %>%
ggplot() +
aes(fill = REGION) +
geom_sf() +
theme(legend.position = "none")
Interessieren uns die EU-Länder, so tragen wir zuerst eine Liste dieser Länder aus dem
Gedächtnis zusammen; oder wir lesen nach. Hier eine Liste der EU-Länder im ISO 3166
alpha-2, einer gebräuchlichen Liste von Länderkürzeln.
eu <- c("BE", "BG", "CZ", "DK", "DE", "EE", "IE", "GR", "ES", "FR",
"HR", "IT", "CY", "LV", "LT", "LU", "HU", "MT", "NL", "AT",
"PL", "PT", "RO", "SI", "SK", "FI", "SE", "GB")
Prüfen wir, ob diese Länder im Datensatz countries enthalten sind:
countries %>%
filter(cca2 %in% eu) %>% nrow
#> [1] 28
Das passt; jeweils 28 EU-Länder wurden identifiziert. Ergänzen wir zu countries noch
eine Spalte is_EU, für etwaige spätere Verwendung:
countries %>%
mutate(is_EU = ifelse(cca2 %in% eu, TRUE, FALSE)) -> countries
14
Abb. 14.7 Visualisierung der Bevölkerungszahlen der EU
228
Geovisualisierung
14.4 Anwendungsbeispiel: Konkordanz von Kulturwerten und Wohlbefinden
229
Dann filtern wir world_sf so, dass nur die Namen aus der EU übrig bleiben (s. Abb. 14.7,
links).
world_sf %>%
filter(ISO_A2 %in% eu) %>%
ggplot() +
aes() +
geom_sf(fill = "steelblue", alpha = .5) +
coord_sf(xlim = c(-20, 40), ylim = c(30, 70))
Mit coord_sf() kann man die Koordinaten begrenzen. Natürlich können wir den Farben
der Länder eine beliebige Variable zuordnen; aes() sorgt dann dafür, dass die Werte
dieser Variablen jeweils einer Farbe zugeordnet (gemappt) werden (s. Abb. 14.7, rechts).
world_sf %>%
filter(ISO_A2 %in% eu) %>%
ggplot() +
aes() +
geom_sf(aes(fill = POP_EST)) +
scale_fill_viridis() +
coord_sf(xlim = c(-20, 40), ylim = c(30, 70))
Das Farbspektrum arbeitet die Bevölkerungszahlen schön heraus; Deutschland sticht mit
einer großen Bevölkerungszahl hervor.
14.4 Anwendungsbeispiel: Konkordanz von Kulturwerten und
Wohlbefinden
Betrachten wir ein weiteres Fallbeispiel, basierend auf Daten zu den Cultural Values
nach Schwartz (S. H. Schwartz 1999) und den Wellbeing Indicators der OECD (Durand
2015). Untersuchen wir die Hypothese, ob „freies Denken“ – intellektuelle Autonomie
in Schwartzens Begriffen – mit Lebenszufriedenheit einhergeht. Anders gesagt: Haben
Länder mit hohen Werten in intellektueller Autonomie tendenziell höhere Werte bei der
Lebenszufriedenheit? Die Frage ist sicherlich interessant; allerdings darf dabei nicht vergessen werden, dass wir ohne tieferes theoretisches Fundament an die Frage herangehen.
Die Art, wie wir die Frage stellen, ist sicherlich naiv; außerdem haben wir uns nicht die
Mühe gemacht, den Stand der Forschung zu verstehen. All das sind Punkte, die es in ernst
gemeinter Datenanalyse als Teil ernst gemeinter Forschung zu beachten gilt. Sei’s drum
– mit welchem Maß können wir fassen, ob ein bestimmtes Land (Absurdistan) ähnlich
„gut“ in beiden Aspekten abschneidet?
Eine Möglichkeit wäre, für beide Maße eine Rangliste zu erstellen; dann könnte man
für Absurdistan den Rangplatz in intellektueller Autonomie mit dem Rangplatz für Lebenszufriedenheit vergleichen. Eine einfache Differenz würde vielleicht reichen. Besser
noch erstellt man eine relative Rangliste von Listenplatz 0 % (der höchste Wert) bis zum
Listenplatz 100 % (der geringste) Wert, um mögliche Unterschiede in der Länge der bei-
230
14
Geovisualisierung
den Listen auszugleichen. Man könnte argumentieren, dass es für unsere Fragestellung nur
darauf ankäme, wie ähnlich die beiden Rangplätze, wie gering also die Rang-Differenz
ist. Dann würde das Vorzeichen keine Rolle spielen; nur der Absolutbetrag der Differenz
spielte eine Rolle. Dieser Ansatz impliziert, dass wir an den Größen der Unterschiede
zwischen den Rangplätzen nicht interessiert sind. Anders gesagt nehmen wir an, dass die
Unterschiede zwischen den Rangplätzen gleich groß sind; wir messen auf ordinalem Niveau, wie man sagt. Eine relative Rangliste kann man so vereinbaren:
ri 1
n1
wobei ri die Position einer Beobachtung ist, wenn die Beobachtungen (aufsteigend) sortiert sind; n ist die Anzahl der Beobachtungen. Dabei stellt sich noch die Frage, wie man
mit gleichen Werten (Rangbindungen; ties) umgeht (s. Agresti (2013) für Details). Eine
Umsetzung in R für die relative Rangliste bietet dplyr::percent_rank():
RD
x <- c(1, 2, 3)
percent_rank(x)
#> [1] 0.0 0.5 1.0
Ein anderer Ansatz wäre, den Wertebereich (Range) beider Maße auf 1 zu standardisieren;
so dass der kleinste Wert 0 und der größte Wert 1 ist (Min-Max-Standardisierung oder 0-1Standardisierung). Dann könnte man, ohne den Umweg über Rangplätze zu gehen, direkt
(Absolutbeträge von) Differenzen berechnen. Hätten beide Maße die gleiche Skalierung, so
könnte man sich diese Standardisierung sparen. Dieser Ansatz setzt voraus, dass die Grenzen der Skalierung (0 und 1) sinnvolle, nicht-willkürliche Werte darstellen, an denen man
sich orientieren kann. Möchte man X min-max-normalisieren, so kann man dies so tun:
S <- (X - min(X)) / (max(X) - min(X))
Als Formel geschrieben:
xi min.x/
max.x/ min.x/
wobei X D .x1 ; x2 ; : : : ; xn / der ursprüngliche Vektor und si der normalisierte Wert der
Beobachtung i ist.
Ein dritter Weg wäre die z-Skalierung beider Maße; dabei wird der relative Abstand
eines Landes vom Mittelwert aller Länder betrachtet. Diese Skalierung nimmt die Skalengrenzen nicht als Orientierungspunkt, sondern den Mittelwert der Länder. Dieser Überlegung liegt zugrunde, dass es bei vielen Maßen der Sozialwissenschaften keinen fixen
Nullpunkt gibt. z-Werte sind so definiert:
si D
zi D
xi xN
sd.x/
xi ist der Wert des zu standardisierenden Maßes der Beobachtung i; sd ist die Standardabweichung.
14.4 Anwendungsbeispiel: Konkordanz von Kulturwerten und Wohlbefinden
231
In R kann man sich z-Werte so ausgeben lassen:
x <- c(1, 2, 3)
scale(x) %>% as.numeric()
scale() gibt eine Matrix zurück; hier eine Matrix mit nur einer Spalte. Möchte man
stattdessen einen Vektor zurückbekommen, so kann man mit as.numeric() das Format
wechseln. Alternativ kommt man mit mosaic::zscore(x) schneller ans Ziel (muss
aber einen weiteren Befehl kennen).
Welche Standardisierung gewählt wird, hängt also von theoretisch-methodischen Annahmen ab. Schließlich wäre noch die Frage zu erörtern, wie es um die Messgüte der
Maße bestellt ist. Gehen wir von vielen Unschärfen aus, denken also pessimistisch, dass
es mehr Rauschen als Signal in den Daten gebe, dann sind wir besser beraten, auf Scheingenauigkeit zu verzichten, und wir sollten die Hoffnung auf metrisches Niveau aufgeben.
Dann sollte man sich für eine Analyse der Rangplätze entscheiden. Ist man optimistischer
und sieht den Messfehler in strenge Ketten gelegt, so darf man sich Aussagen mit höherer
Aussagekraft (auf metrischem Niveau) erlauben.
Führt man sich vor Augen, dass die Daten aus unterschiedlichen Jahren stammen, wohl
von vielen verschiedenen Versuchsleitern erhoben, über Sprach- und Kulturgrenzen hinweg gemessen, mit Instrumenten unbekannter Beschaffenheiten, und weitere mögliche,
hier nicht bekannte Schwächen aufweisen, sollten wir uns mit geringerer Aussagekraft
begnügen oder besser gesagt, keine Scheingenauigkeit produzieren. Es sollte die Maxime gelten, lieber nichts zu sagen, als etwas Falsches. Wir unterwerfen uns also dem
Regime der Rangplatzanalyse; ein Gebiet von respektablem wissenschaftlichem Renommee (Agresti 2013).
Betrachten wir also die Daten (cult_values für die Kulturwerte und well_being
für die OECD-Daten des Wohlbefindens) und standardisieren zu relativen Rangplätzen.
Die intellektuelle Autonomität findet sich in der Spalte cult_values$intel_auton
und die Lebenszufriedenheit in wellbeing$Life_satisfaction (der Datensatz
stellt zwei ähnliche Maße bereit; bleiben wir beim ersten).
names(wellbeing)
names(cult_values)
cult_values %>%
mutate(intel_auton_rank = percent_rank(intel_auton)) -> cult_values
wellbeing %>%
mutate(Life_satisfaction_rank = percent_rank(Life_satisfaction),
Country = tolower(Country)) -> wellbeing
Nun müssen wir die beiden Datensätze zusammenführen, um die Daten aufeinander zu
beziehen (so dass in der Zeile von Absurdistan beide Werte, intellektuelle Autonomie und
Lebenszufriedenheit, stehen). Dabei ist zu beachten, dass die Ländernamen einmal mit
großem Anfangsbuchstaben und einmal mit kleinem Anfangsbuchstaben geschrieben wa-
232
14
Geovisualisierung
ren; das haben wir mit tolower() gerade angeglichen. Außerdem sind die USA einmal
als unitedstates und einmal als United States bezeichnet; für unitedkingdom
gilt Ähnliches. Das sollten wir noch angleichen.
cult_values %>%
mutate(country_long = recode(country_long,
"unitedstates" = "united states", # erst alter Wert, dann neuer Wert
"unitedkingdom" = "united kingdom")) -> cult_values
c("united states", "united kingdom") %in% cult_values$country_long
#> [1] TRUE TRUE
x %in% y prüft, ob x in y enthalten ist; es empfiehlt sich, stets zu prüfen, ob das, wovon
man denkt, dass es funktioniert, auch tatsächlich funktioniert . . . Mit inner_join()
werden nur Zeilen behalten, die sich in beiden Dataframes wiederfinden:
wellbeing %>%
filter(region_type == "country_whole") %>%
inner_join(cult_values,
by = c("Country" = "country_long")) -> df_joined
Das Zusammenführen von Datensätzen unterschiedlicher Herkunft zeigt auf, wie viel Wissen so erzeugt bzw. abgeschöpft werden kann. Hätten wir Daten auf Personenebene, so
würde die Gefahr von De-Anonymisierung beträchtlich steigen; hier ist das kein Problem. Wir können uns erfreuen am Reichtum möglicher Forschungsfragen, die die Daten
zulassen. Betrachten wir die Konkordanz unserer beiden Maße:
df_joined %>%
select(Country, Life_satisfaction_rank, intel_auton_rank) %>%
gather(key = indicator, value = perc_rank, -Country) %>%
ggplot +
aes(x = indicator, y = perc_rank) +
geom_point(size = 5, alpha = .5) +
scale_x_discrete(labels = c("Autonomie", "Zufried")) +
geom_line(aes(group = Country)) -> p_rank_comp
Wie Abb. 14.8 (links) zeigt, scheint die Konkordanz beider Maße nicht sonderlich ausgeprägt zu sein. Wäre sie stark ausgeprägt, so wären die Linien parallel zur X-Achse; je
extremer die Steigung der Linien, desto geringer die Konkordanz. Überprüfen wir diesen
optischen Eindruck an der Rang-Korrelation:
df_joined %>%
select(Life_satisfaction_rank, intel_auton_rank) %>%
cor(method = "spearman")
#>
Life_satisfaction_rank intel_auton_rank
#> Life_satisfaction_rank
1.000
0.313
#> intel_auton_rank
0.313
1.000
14.4 Anwendungsbeispiel: Konkordanz von Kulturwerten und Wohlbefinden
233
1.00
80°N
60°N
0.75
perc_rank
40°N
20°N
0°
0.50
20°S
40°S
60°S
0.25
120°W
60°W
0°
60°E
120°E
konkordanz
0.2 0.4 0.6
0.00
Autonomie
Zufried
indicator
Abb. 14.8 Konkordanz von intellektueller Autonomie und Lebenszufriedenheit
Spearmans Rangkorrelation zeigt einen Wert von 0.31 an; gar nicht so wenig. Berechnen
wir als Nächstes für jedes Land die absolute Differenz der beiden Indikatoren und färben
eine Karte entsprechend ein.
df_joined %>%
mutate(konkordanz = abs(Life_satisfaction_rank - intel_auton_rank)) %>%
select(Country, konkordanz,
Life_satisfaction_rank, intel_auton_rank) -> df_joined_small
Falls Sie die Kartendaten noch nicht geladen haben sollten, tun Sie es bitte jetzt (vgl.
Abschn. 14.3).
data(countriesLow)
world_sf <- st_as_sf(countriesLow)
Dann „joinen“ wir die Kartendaten zu unseren Umfragedaten und erstellen eine Choroplethenkarte, eine Karte also, die Flächen Werte zuweist, welche durch Farbwerte o. Ä.
dargestellt sind:
world_sf %>%
mutate(NAME = tolower(NAME)) %>%
inner_join(df_joined_small,
by = c("NAME" = "Country")) -> world_sf_joined
world_sf_joined %>%
ggplot +
234
14
Geovisualisierung
aes(fill = konkordanz) +
geom_sf() +
theme(legend.position = "bottom") +
scale_fill_viridis() -> p_concordance
grid.arrange(p_rank_comp, p_concordance,
layout_matrix = rbind(c(1, 2, 2), c(1,2, 2)))
Wie man in Abb. 14.8 (rechts) sieht, scheint diese Konkordanz für Zentraleuropa und die
USA zu gelten, aber weniger oder kaum für andere OECD-Länder.
14.5 Interaktive Karten
14.5.1
Karten mit „leaflet“
Mit dem Paket leaflet ist es einfach, Karten beliebiger Orte zu visualisieren; dabei
übergibt man die gewünschten Breiten- und Längengrade, die man z. B. von ggmap::
geocode() bekommt. leaflet bezieht das Kartenmaterial vom OpenStreetMapProjekt (dafür sorgt die Funktion addTiles()); mit addMarkers() kann man eine
Nadel an einer gewünschten Position setzen (s. Abb. 14.9).
geo1 <- geocode("Nuremberg")
m <- leaflet() %>%
addTiles() %>%
addMarkers(lng = geo1$lon[1], lat = geo1$lat[1],
popup = "Wo dieses Buch geschrieben wurde")
Abb. 14.9 Eine interaktive Karte mit „leaflet“
14.5 Interaktive Karten
235
Abb. 14.10 Ein Choropleth-Diagramm mit googleVis
14.5.2
Karten mit googleVis
Auch das Paket googleVis bietet einfache Möglichkeiten zur interaktiven Visualisierung von Karten.11 Die zentrale Funktion ist hier gvisGeoChart(); man übergibt einen
Dataframe (data), der die Namen der Verwaltungseinheiten enthält (locationvar) sowie die Variable zum Einfärben dieser Einheiten (colorvar). Mit options legt man
fest, welche Ebene von Verwaltungseinheiten gezeigt werden soll und wie die übergeordnete Einheit heißt (s. Abb. 14.10).
states <- data.frame(state.name, state.x77)
GeoStates <- gvisGeoChart(data = states,
locationvar = "state.name",
colorvar = "Illiteracy",
options=list(region="US",
displayMode="regions",
resolution="provinces",
width=600, height=400))
plot(GeoStates)
Erstellen wir ein interaktives Diagramm mit den Arbeitslosigkeitszahlen nach Bundesland aus dem Datensatz socec. Die offiziellen Namen von Provinzen von Ländern (d. h.
nach ISO_3166_2) bekommt man vom Paket ISO_3166_2. Wir ziehen daraus alle Provinzen, die mit „DE-“ beginnen. Mit plot(gvisTable(select(ISO_de, Code,
Name))) bekommt man eine interaktive Tabelle (s. Abb. 14.11).
11
Die Nutzungsbedingungen stehen unter https://developers.google.com/terms/.
236
14
Geovisualisierung
Abb. 14.11 Eine interaktive Tabelle mit googleVis
ISO_de <- ISO_3166_2 %>%
filter(str_detect(Code, "DE-")) %>%
arrange()
Tatsächlich sind die offiziellen Namen wenig überraschend: So heißen die deutschen
Bundesländer eben. Allerdings hat gvisGeoChart() einen Bug, der dazu führt, dass
Brandenburg nicht richtig dargestellt wird,12 daher verwenden wir nicht die offiziellen
Namen, sondern den ISO-Code. Dafür fügen wir die ISO-Codes mittels join zum Datensatz socec zu. Dann rufen wir gvisGeoChart() analog wie gerade auf (s. Abb. 14.12).
socec %>%
full_join(ISO_de, by = c("V01" = "Name")) %>%
filter(V01 != "Deutschland") -> socec_ISO
geoChartDE <- list(region="DE",
resolution="provinces",
legend="{numberFormat:'#,###.00'}")
plot(
gvisGeoChart(socec_ISO, locationvar = "Code",
colorvar = "V51",
options=geoChartDE)
)
12
https://github.com/google/google-visualization-issues/issues/707.
14.5 Interaktive Karten
237
Abb. 14.12 Arbeitslosigkeit mit googleVis
Aufgaben
1. Stellen Sie Hypothesen auf zu Prädiktoren des AfD-Wahlerfolgs für die Bundestagswahl 2017.13
2. Suchen Sie nach passenden Variablen im Datensatz socec; ggf. müssen Sie Ihre Hypothesen modifizieren, so dass Sie über Daten zum Testen der Hypothesen
verfügen.14
3. Berechnen Sie einfache, bivariate Korrelationen und lineare Modelle – jeweils
ein Prädiktor und den Wahlerfolg der AfD.
Lösung
data("socec", package = "pradadata")
library(corrr)
13
Je geringer der Ausländeranteil (V08), desto höher der AfD-Wahlerfolg. Je geringer das verfügbare Einkommen der Haushalte (V26), desto höher der Wahlerfolg. Diese Hypothesen sind nur
Beispiele von vielen möglichen; sie sind ziemlich willkürlich gewählt. Es fehlt diesen Hypothesen
eine Anbindung an eine gute Theorie.
14
V08 und V26 könnten interessante Kandidaten sein.
238
14
Geovisualisierung
afd_df %>%
select(afd_prop, alq, migration) %>%
correlate() %>%
shave()
#> # A tibble: 3 x 4
#>
rowname
afd_prop
alq migration
#>
<chr>
<dbl>
<dbl>
<dbl>
#> 1 afd_prop
NA
NA
NA
#> 2 alq
0.181 NA
NA
#> 3 migration
-0.593 -0.0348
NA
lm_a3_1 <- lm(afd_prop ~ alq, data = afd_df)
lm_a3_2 <- lm(afd_prop ~ migration, data = afd_df)
4. Berechnen Sie ein multiples lineares Model (mehrere Prädiktoren) – wie unterscheiden sich die Korrelationen bzw. Einflussgewichte von den Modellen mit
einem Prädiktor?
Lösung
Im Modell mit mehreren Prädiktoren sind die Regressionsgewichte der Prädiktoren jeweils anders als in den einfachen Modellen mit einem Prädiktor.
library(broom)
lm_a4_1 <- lm(afd_prop ~ alq + migration, data = afd_df)
tidy(lm_a3_1)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept) 0.102
0.00832
12.2 4.42e-28
#> 2 alq
0.00401
0.00126
3.17 1.66e- 3
tidy(lm_a3_2)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic
p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept) 0.192
0.00572
33.5 9.14e-103
#> 2 migration
-0.00349 0.000275
-12.7 8.81e- 30
tidy(lm_a4_1)
#> # A tibble: 3 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept) 0.169
0.00851
19.9 2.20e-56
#> 2 alq
0.00356 0.00102
3.50 5.35e- 4
#> 3 migration
-0.00346 0.000270
-12.8 3.74e-30
14.5 Interaktive Karten
239
5. Visualisieren Sie Ihr „bestes“ Modell.
afd_df$lm_a4 <- lm(afd_prop ~ alq + migration,
data = afd_df)$residuals
afd_df %>%
ggplot +
aes(fill = lm_a4) +
geom_sf() +
labs(title = "Abweichung vom geschätzten AfD-Anteil",
fill = "",
caption = "Prädiktoren: Ausländer- und Migrationsquote") +
scale_fill_viridis() +
theme_void()
Begründen Sie, warum es das „beste“ Modell ist.15
Wie finden Sie mehr über die verschiedenen Farbskalen von ggplot heraus?16
Welches Farbschema passt für welche Situationen?17
Welche weiteren Farbschemata sind noch implementiert? Recherchieren Sie im
Paket RColorBrewer.18
10. Bestimmen Sie R2 für das einfache und das komplexere Erklärungsmodell zum
AfD-Erfolg.
6.
7.
8.
9.
Lösung
elec_results %>%
select(WKR_NR = district_nr, AfD_3, votes_3) %>%
mutate(afd_prop = AfD_3 / votes_3) %>%
inner_join(wahlkreise_shp) %>%
inner_join(socec_short) -> afd_data
afd_lm1 <- lm(afd_prop ~ alq, data = afd_data)
broom::tidy(afd_lm1)
#> # A tibble: 2 x 5
15
Diese Frage sollte unter Bezug auf eine Theorie – oder im Vergleich zu konkurrierenden Theorien
– beantwortet werden. In dieser Übung geht es um die technischen Aspekte einer solchen Forschungsfrage; in echter Forschung wäre eine solche Begründung nötig. Natürlich kann und sollte
man auch Koeffizienten zum Vergleich der Modelle heranziehen, etwa die Güte der Vorhersage in
einem Test-Sample.
16
Probieren Sie display.brewer.all().
17
Ein wichtiger Aspekt ist, ob man eine zweipolige Variable wie Höhenprofil darstellt oder eine
einpolige wie Prozent Arbeitslosigkeit.
18
?RColorBrewer.
240
14
Geovisualisierung
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept) 0.102
0.00832
12.2 4.42e-28
#> 2 alq
0.00401
0.00126
3.17 1.66e- 3
broom::glance(afd_lm1)
[Ausgabe aus Platzgründen gekürzt]
afd_rsme_lm1 <- mean(afd_lm1$residuals^2) %>% sqrt()
afd_lm2 <- lm(afd_prop ~ alq + wegzug + migration + auslaender,
data = afd_data)
[Ausgabe aus Platzgründen gekürzt]
broom::glance(afd_lm2)
[Ausgabe aus Platzgründen gekürzt]
afd_rsme_lm2 <- mean(afd_lm2$residuals^2) %>% sqrt()
11. Welche weiteren Prädiktoren könnten für ein Erklärungsmodell genutzt werden?19
12. Erstellen Sie eine Karte Europas nur mit Ländern zwischen dem 50. und 70.
Breitengrad.20
13. Visualisieren Sie in gleicher Manier eine andere Region der Welt, für die Daten
in world_sf vorliegen.
Lösung
world_sf3 <- world_sf %>%
filter(REGION == "Asia") %>%
ggplot() +
aes(fill = GEO3) +
geom_sf() +
theme(legend.position = "none")
world_sf3 + scale_fill_brewer(palette = 2)
19
Variablen, die das Alter des Landkreises umreißen wie V12-V17 in socec, könnten von Interesse
sein; viele andere potenziell auch. Die Auswahl der Prädiktoren sollte von einer Theorie diktiert
werden.
20
Die Syntax entspricht der von Abb. 14.7, nur dass ylim auf c(50,70) gesetzt wird.
14.5 Interaktive Karten
241
14. Passen Sie ebenfalls die Koordinaten an, falls es nötig ist.21
15. Probieren Sie verschiedene Farbpaletten aus.22
16. Lässt man sich Informationen zur Modellgüte mit broom::glance() ausgeben, so wird unter anderem ein Wert für statistic, p.value und sigma
ausgegeben (z. B. im Modell afd_lm1). Worauf beziehen sich diese Koeffizienten?23
21
world_sf3 + coord_sf(xlim = c(60, 120)); das sind willkürlich gewählte Längengrade.
22
Z. B.
world_sf3 + scale_fill_viridis_d()
oder
world_sf3 +
scale_fill_brewer(palette = 2).
23
statistic gibt den F-Wert der gesamten Regression an; p.value den zugehörigen p-Wert
zum F-Wert. sigma gibt die Streuung der Residuen wieder. Die Hilfeseite verrät weitere Hinweise:
?glance.lm.
Teil V
Modellieren
15
Grundlagen des Modellierens
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
Lernziele
Erläutern können, was man unter einem Modell versteht
Die Ziele des Modellieren aufzählen und erläutern können
Die Vor- und Nachteile von einfachen vs. komplexen Modellen vergleichen können
Wissen, was man unter Bias-Varianz-Abwägung versteht
Um die Notwendigkeit von Trainings- und Test-Stichproben wissen
Wissen, was man unter Modellgüte versteht
Um die Schwierigkeiten der Prädiktorenauswahl wissen
In diesem Kapitel benötigen wir folgende Pakete und Daten:
library(tidyverse)
data(stats_test, package = "pradadata")
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_15
245
246
15
Grundlagen des Modellierens
15.1 Was ist ein Modell? Was ist Modellieren?
In diesem Kapitel geht es um Modelle und Modellieren; aber was ist das eigentlich? Seit
dem 16. Jahrhundert wird mit dem italienischen Begriff modelle ein verkleinertes Muster,
Abbild oder Vorbild für ein Handwerksstück benannt (Gigerenzer 1980). Prototypisch für
ein Modell ist – wer hätt’s gedacht – ein Modellauto (s. Abb. 15.1; Spurzem (2017)).
In die Wissenschaft kam der Begriff in der Zeit nach Kant, als man sich klar wurde,
dass (physikalische) Theorien nicht die Wirklichkeit als solche zeigen, sondern ein Modell davon. Modellieren ist eine grundlegende Tätigkeit, derer sich Menschen fortlaufend
bedienen, um die Welt zu verstehen. Denn das Leben ist schwer – oder sagen wir: komplex. Um einen Ausschnitt der Wirklichkeit zu verstehen, erscheint es daher sinnvoll, sich
einige als wesentlich erachtete Aspekte „herauszugreifen“ bzw. auszusuchen und sich nur
noch deren Zusammenspiel näher anzuschauen. Modelle vereinfachen: Es wird nur ein
Ausschnitt der Wirklichkeit in einfacherer Form berücksichtigt. Da wir die Natur bzw. die
Wirklichkeit nicht komplett erfassen, erschaffen wir uns ein vereinfachtes Abbild von der
Wirklichkeit, ein Modell.
Manche Aspekte der Wirklichkeit sind wirklicher als andere: Interessiert man sich für
den Zusammenhang von Temperatur und Grundwasserspiegel, so sind diese Dinge direkt
beobachtbar. Interessiert man sich hingegen für Intelligenz und Zufriedenheit, so muss
man diese Untersuchungsgegenstände erst konstruieren, da Zufriedenheit und Intelligenz
nicht direkt beobachtbar sind. Sprechen wir daher statt von Wirklichkeit lieber vorsichtiger vom Gegenstandsbereich, also dem konstruierten Auszug der Wirklichkeit, für den
sich die forschende Person interessiert. Bestenfalls (er)findet man mit dem Modell eine
Annäherung an die Wirklichkeit, schlechterenfalls eine verzerrte, gar grob falsche Darstellung. Da keine Wiedergabe der Wirklichkeit perfekt ist, sind streng genommen alle
Modelle „falsch“ in diesem Sinne. Gegenstandsbereich und Modelle stehen in einer Beziehung miteinander (vgl. Abb. 15.2; Unrau (2017)).
Abb. 15.1 Ein Modell eines
VW-Käfers als Prototyp eines
Modells
15.1 Was ist ein Modell? Was ist Modellieren?
Abb. 15.2 Modellieren
247
gestaltet
X
Z
vereinfacht
Gegenstandsbereich
Y
Modell
Damit verstehen wir Modellieren als eine typische Aktivität von Menschen (Gigerenzer 1980), genauer eines Menschen mit einem bestimmten Ziel. Wir können gar nicht
anders, als uns ein Modell unserer Umwelt zu machen; entsprechend kann (und muss)
man von mentalen Modellen sprechen. Vielfältige Medien kommen dazu in Frage: Bilder,
Geschichten, Logik, Gleichungen. Wir werden uns hier auf eine bestimmte Art formalisierter Modelle, numerische Modelle, konzentrieren, weil es dort vergleichsweise einfach
ist, die Informationen auf präzise Art und Weise herauszuziehen. Allgemein gesprochen
ist hier mit Modellieren der Vorgang gemeint, einige Beobachtungen (Erfahrungen über
die Welt; „Empirie“) in eine mathematisch-formale Struktur zu übersetzen.
Zusammengehörige Beobachtungen können dabei als empirisches System bezeichnet
werden, welches aus einer oder mehr Mengen von Objekten besteht, die zueinander in
bestimmten Beziehungen stehen. Ein Bespiel wäre eine Reihe von Personen, die in bestimmten Größe-Relationen zueinander stehen, oder eine Reihe von Menschen, bei denen
die Füße mit steigender Körpergröße tendenziell größer werden.
Mit mathematische Struktur ist ein formalisiertes Pendant zum empirischen System
gemeint; man spricht von einem numerischen System. Im Gegensatz zum empirischen
System ist das numerische System rein theoretisch, also ausgedacht, nicht empirisch. Es
existiert nicht in der (äußeren) Welt. Auch hier gibt es eine Reihe von Objekten, aber mathematischer Art, also z. B. Zahlen oder Vektoren. Diese mathematischen Objekte stehen
wiederum in gewissen Beziehungen (Relationen) zueinander. Der springende Punkt ist:
Im Modell sollen die Beziehungen zwischen den mathematischen Objekten die Beziehungen zwischen den empirischen Objekten widerspiegeln. Was heißt das? Stellen wir uns
vor, der Klausurerfolg steigt mit der Lernzeit.1 Fragen wir das Modell, welchen Klausurerfolg Alois hat (er hat sehr viel gelernt), so sollte das Modell erwidern, dass Alois einen
hohen Klausurerfolg hat. Damit würde das Modell korrekt die Empirie widerspiegeln.
Kausale Prozesse, Ursache-Wirkungs-Zusammenhänge also, sind von zentralem Interesse
für Modelle.
Modellieren bedeutet hier, ein Verfahren zu erstellen, welches empirische Sachverhalte adäquat in numerische Sachverhalte umsetzt.
1
Ein Dozent freut sich.
248
15
Grundlagen des Modellierens
Etwas spitzfindig könnte man behaupten, es gibt keine Modelle – es gibt nur Modelle
von etwas; dieser Satz soll zeigen, dass zwar ein empirisches System für sich alleine stehen
kann, aber ein Modell nicht. Ein Modell verweist immer auf ein empirisches System. Eine
formalere Sichtweise des Modellierens ist die Zuordnung des empirischen Systems E
auf das numerische System Z. Dabei besteht E aus einer Menge von Objekten O sowie
einer Menge von Relationen RE (Relationen bezeichnet hier nichts mehr als irgendwelche
Beziehungen zwischen den Elementen von O). Analog besteht Z aus einer Menge von
numerischen Objekten Z sowie einer Menge von Relationen RZ (Relationen zwischen
den Elementen von Z).2
15.2 Abduktion als Erkenntnisfigur im Modellieren
Statistisches Modellieren läuft oft nach folgendem Muster ab (Grolemund und Wickham
2014):
Prämisse 1: Ist Modell M wahr, sollten die Daten das Muster D zeigen.
Prämisse 2: Die Daten zeigen das Muster D.
--Konklusion: Daher ist das Modell M wahr.
Die Konklusion ist nicht zwangsläufig richtig. Es ist falsch zu sagen, dass dieses Argumentationsmuster – Abduktion (Flach und Hadjiantonis 2013) genannt – wahre, sichere
Schlüsse (Konklusionen) liefert. Die Konklusion kann, muss aber nicht, zutreffen.
Ein Beispiel: Jemand lädt Sie zu einem Glücksspiel ein; zehn Mal soll eine Münze
geworfen werden. Sie setzen auf Kopf. Die andere Person wirft die Münze nun zehn Mal
– zehn Mal landet sie auf Zahl. Ist das Ergebnis Anlass genug, um zu glauben, dass das
Spiel nicht mit rechten Dingen vor sich ging?
Eine Argumentation, die wir hier verwenden könnten, lautet: Wenn die Münze fair
ist (Kopf und Zahl gleich wahrscheinlich sind, also Modell M zutrifft), dann wäre diese
Datenlage D (10 von 10 Mal Zahl) unwahrscheinlich – Prämisse 1. Datenlage D ist tatsächlich der Fall; die Münze fiel zehn Mal auf Zahl – Prämisse 2. Die Daten D „passen“
also nicht zum Modell M ; man kann es auf dieser Basis verwerfen und entscheiden, dass
die Münze gezinkt ist.
Wichtig zu erkennen ist, dass Abduktion mit dem Wörtchen wenn beginnt. Also davon
ausgeht, dass ein Modell M der Fall ist (die Münze also tatsächlich fair ist). Das, worüber
wir entscheiden wollen, wird gewissermaßen vorausgesetzt. Es wird also argumentiert:
Falls M gilt – gehen wir mal davon aus – wie gut passen dann die Daten dazu?
2
Diese Sichtweise des Modellierens geht auf der Repräsentationstheorie des Messens nach Suppes
und Zinnes (1963) zurück; vgl. Gigerenzer (1980).
15.2 Abduktion als Erkenntnisfigur im Modellieren
249
Wie gut passen die Daten D zum Modell M ? Das ist die Frage, die hier gestellt wird
bzw. beantwortet werden soll. Natürlich ist keineswegs sicher, dass das Modell gilt. Darüber macht die Abduktion auch keine Aussage. Es könnte also sein, dass ein anderes
Modell zutrifft: Sie haben Pech gehabt.
Viele statistische Modelle beantworten nicht, wie wahrscheinlich es ist, dass ein
Modell gilt. Stattdessen beurteilen sie, wie gut die vorliegenden Daten zu einem
vorab postulierten Modell passen. Den Grad der Übereinstimmung bezeichnet man
als Modellgüte (model fit).
Häufig trifft ein Modell eine Reihe von Annahmen, die nicht immer explizit gemacht
werden, die aber klar sein sollten: Sind die Münzwürfe unabhängig voneinander? Oder
kann es sein, dass sich die Münze „einschießt“ auf eine Seite? Dann wären die Münzwürfe
nicht unabhängig voneinander. In diesem Fall klingt das reichlich unplausibel; in anderen
Fällen kann dies eher der Fall sein. Auch wenn die Münzwürfe unabhängig voneinander sind, ist die Wahrscheinlichkeit für Zahl jedes Mal gleich? Hier ist es wiederum
unwahrscheinlich, dass sich die Münze verändert, ihre Masse verlagert, so dass eine Seite
Unwucht bekommt. In anderen Situationen können sich Untersuchungsobjekte verändern
(Menschen lernen manchmal etwas, sagt man), so dass die Wahrscheinlichkeiten für ein
Ereignis unterschiedlich sein können, man dies aber nicht berücksichtigt.
Neben der Abduktion wird Wissen auf zwei andere Arten erzeugt: durch Induktion und
Deduktion. Das gilt auch für die Statistik. Kurz gesagt bedient man sich induktiven Denkens, wenn man ein Muster, das man in Daten gefunden hat, auf eine Grundgesamtheit
erweitert. In meiner Stichprobe waren Männer im Schnitt größer als Frauen; ich schließe,
dass in meiner Grundgesamtheit Männer im Schnitt größer sind als Frauen. Diese Argumentationsfigur wirft Fragen auf, die hier nicht diskutiert werden sollen (vgl. Saint-Mont
(2011) für eine eingehende Untersuchung). Nehmen wir der Einfachheit halber an, die
Grundgesamtheit seien alle Menschen (die im Moment leben?). Der Schluss, dass „Männer im Schnitt größer“ seien, beinhaltet Aussagen, die über die Beobachtung, die die Daten
liefern, hinausgehen. Es ist eine Verallgemeinerung; von Einzelbeobachtung wird auf ein
Allgemeines, eine allgemeine Regel geschlossen.
Eine Deduktion ist komplementär zur Induktion in dem Sinne, dass dabei von einer
allgemeinen Regel auf beobachtbare Einzelfälle geschlossen wird (Popper 1968). Trifft
eine Theorie eine Behauptung über alle Objekte einer Grundgesamtheit, so muss sich diese
Behauptung an einem bestimmten Objekt beobachten lassen – wenn die Theorie richtig ist
(oder sonst nichts Komisches passiert ist). Damit ist die Deduktion nichts anderes als die
Anwendung von Logik zur Ableitung wahrer Aussagen. Saint-Mont (2011) liefert einen
trefflichen Überblick über diesen und verwandte Aspekte der Wissenschaftstheorie.
250
15
Grundlagen des Modellierens
15.3 Ein Beispiel zum Modellieren in der Datenanalyse
Schauen wir uns ein Beispiel aus der Datenanalyse an (s. Abb. 15.3). Im linken Plot (A)
sehen wir – schon übersetzt in eine Datenvisualisierung – den Gegenstandsbereich, der
uns gerade brennend interessiert. Dort sind einige Objekte zusammen mit ihren Relationen abgebildet (Selbsteinschätzung und Klausurerfolg). Der rechte Plot spezifiziert nun
die Beziehung beider Größen: Es wird ein linearer Zusammenhang (eine Gerade) zwischen Körpergröße und Schuhgröße unterstellt. In Abb. 15.4 sehen wir ein Schema dazu,
ein sog. Pfadmodell. Noch ist das Modell recht unspezifisch; es wird nur postuliert, dass
die Körpergröße auf die Schuhgröße einen Einfluss habe. Fügen wir hinzu, dass der Einfluss linear sei, heißt das, dass der Einfluss der Selbsteinschätzung auf den Klausurerfolg
immer gleich groß ist, also unabhängig vom Wert der Selbsteinschätzung. Allgemeiner
formuliert, haben wir einen oder mehrere Eingabegrößen bzw. Prädiktoren, von denen
wir annehmen, dass sie einen Einfluss haben auf genau eine Zielgröße (Ausgabegröße)
bzw. ein Kriterium.
Einfluss ist hier nicht (notwendigerweise) kausal gemeint, auch wenn es das Wort so
vermuten lässt. Stattdessen ist nur ein statistischer Einfluss gemeint; letztlich nichts
anderes als ein Zusammenhang. In diesem Sinne könnte man postulieren, dass die
Größe des Autos, das man fährt, einen „Einfluss“ auf das Vermögen des Fahrers
habe. Empirisch ist es gut möglich, dass man Belege für dieses Modell findet. Jedoch
wird dieser Einfluss nicht kausal sein.
Modelle, wie wir sie betrachten werden, berechnen einen quantitativen Zusammenhang
zwischen diesen beiden Arten von Größen – Prädiktoren und Kriterien. Damit lassen sich
B
40
35
35
score
score
A
40
30
30
25
25
20
20
2.5
5.0
7.5
self_eval
Abb. 15.3 Ein Beispiel für Modellieren
Abb. 15.4 Ein Beispiel für ein
Pfadmodell
10.0
2.5
5.0
self_eval
7.5
10.0
15.4 Taxonomie der Ziele des Modellierens
251
unsere Modelle in drei Aspekte gliedern. Die Einflussgrößen werden in einer „schwarzen Kiste“, die wir hier noch nicht näher benennen, irgendwie verwurstet, will sagen,
verrechnet, so dass ein geschätzter Wert für das Kriterium, eine Vorhersage „hinten bei
rauskommt“.3 Wir gehen dabei nicht davon aus, dass unsere Modelle perfekt sind, sondern
dass Fehler passieren; in Ermangelung besseren Wissens nennen wir die Fehler zufällig.
Mathematischer ausgedrückt:
Y D f .X/ C Hier stehen Y für das Kriterium, X für den oder die Prädiktoren, f für die „schwarze
Kiste“ und für den Fehler, den wir bei unserer Vorhersage begehen. Die schwarze Kiste
ist unser Pendant für die Kräfte der Natur, die gewisse Prozesse (kausal) bewirken (vgl.
Briggs (2016); Hastie et al. (2013)). Unser Modell erklärt die Beobachtungen nicht perfekt, es macht Fehler, was durch den Fehlerterm dargestellt ist. Die Fehler werden als
informationslos betrachtet, pures Rauschen, nicht erklärbar für unser Modell. Durch den
Fehlerterm in der Gleichung ist das Modell nicht deterministisch, sondern beinhaltet
erstens einen funktionalen Term (Y D f .X/) und zweitens einen stochastischen Term
(); stochastisch heißt, dass diese Größe zufallsbestimmt ist. Die schwarze Kiste könnte
man auch als eine datengenerierende Maschine oder einen datengenerierenden Prozess
bezeichnen.
Übrigens: Auf das Skalenniveau der Eingabe- bzw. Ausgabegrößen (qualitativ vs. quantitativ) kommt es hier nicht grundsätzlich an; es gibt Modelle für verschiedene Skalenniveaus. Ein Beispiel zur Verdeutlichung: „Gehört Herr Bussi-Ness zur Gruppe der
Verweigerer oder der Wichtigmacher?“ (qualitatives Kriterium); „Wie hoch ist der
Wichtigmacher-Score von Herrn Bussi-Ness?“ (quantitatives Kriterium). Ein Modell
mit qualitativem Kriterium bezeichnet man auch als Klassifikation; ein Modell mit quantitativem Kriterium bezeichnet man auch als Regression. Bei letzterem Begriff ist zu
beachten, dass er doppelt verwendet wird. Neben der gerade genannten Bedeutung steht
er auch für eine zentrale Modellierungsmethode (das Allgemeine bzw. Generalisierte
Lineare Modell).
15.4 Taxonomie der Ziele des Modellierens
Modelle kann man auf vielerlei Arten gliedern; für unsere Zwecke ist folgende Taxonomie
der Ziele von Modellieren nützlich.
Geleitetes Modellieren
– Prädiktives Modellieren
– Explikatives Modellieren
Ungeleitetes Modellieren
– Dimensionsreduzierendes Modellieren
– Fallreduzierendes Modellieren
3
Das ist schließlich entscheidend – frei nach Helmut Kohl.
252
15
Grundlagen des Modellierens
Geleitetes Modellieren ist jede Art des Modellierens, bei dem die Variablen in Prädiktoren
und Kriterien unterteilt werden, wie z. B. in Abb. 15.4. Man könnte diese Modelle vereinfacht darstellen als „X führt zu Y “. Dabei müssen dem Modell einige Fälle für X und für
Y bekannt sein.
Prädiktives Modellieren könnte man kurz als Vorhersagen bezeichnen. Hier ist das
Ziel, eine Black Box geschickt zu wählen, so dass der Vohersagefehler möglichst klein
ist. Man zielt also darauf ab, möglichst exakte Vorhersagen zu treffen. Sicherlich wird der
Vorhersagefehler kaum jemals null sein; aber je präziser, desto besser. Das Innenleben der
„schwarzen Kiste“ interessiert uns hier nicht. Wir machen keine Aussagen über UrsacheWirkungs-Beziehungen. Ein Beispiel für ein prädiktives Modell: „Je größer das Auto,
desto höher das Gehalt.“ Dabei werden wir wohl nicht annehmen, dass die Größe des
Autos die Ursache für die Höhe des Gehalts ist.4 Wir sind – in diesem Beispiel – lediglich
daran interessiert, das Gehalt möglichst präzise zu schätzen; die Größe des Autos dient
uns dabei als Prädiktor, wir verstehen sie nicht als Ursache. Ein altbekanntes Lamento der
Statistiklehrer lautet: „Korrelation heißt noch nicht Kausation!“
Explikatives Modellieren oder kurz Erklären bedeutet, verstehen zu wollen, wie oder
warum sich ein Kriteriumswert so verändert, wie er es tut. Auf welche Art werden die Prädiktoren verrechnet, so dass eine bestimmter Kriteriumswert resultiert? Welche Prädiktoren sind dabei (besonders) wichtig? Ist die Art der Verrechnung abhängig von den Werten
der Prädiktoren? Hierbei interessiert uns vor allem die Beschaffenheit der schwarzen Kiste; die Güte der Vorhersage ist zweitrangig. Oft, aber nicht immer, steht ein Interesse an
der Ursache hinter dieser Art der Modellierung. Ursache-Wirkungs-Beziehungen gehören
sicherlich zu den interessantesten und wichtigsten Dingen, die man untersuchen kann. Die
Wissenschaft ist (bzw. viele Wissenschaftler sind) primär an Fragestellungen zur kausalen
Beschaffenheit interessiert (Shmueli 2010). Ein Beispiel für diese Art von Modellierung
wäre, ob Achtsamkeit zu weniger intensiven emotionalen Reaktionen führt – und wenn
ja, auf welche Art und Weise (Sauer et al. 2010). Übrigens: Es ist erlaubt, eine kausale
Theorie zu vertreten, auch wenn das Design einer Studie solche Schlussfolgerungen nur
eingeschränkt oder gar nicht erlaubt (Shmueli 2010). Häufig werden Beobachtungsstudien
auf Korrelationsbasis angeführt, um kausale Theorien zu testen. Natürlich ist der Preis für
eine einfachere Studie, dass man weniger Evidenz für eine Theorie mit Kausalanspruch
einstreichen kann. Aber irgendwo muss man ja anfangen (aber man sollte nicht bei einfachen Studien stehen bleiben).
Findet man, dass zwei Ereignisse (Variablen) zusammenhängen, so heißt das nicht,
dass das eine Ereignis die Ursache des anderen sein muss. Man denke an Störche
und Babies (wo es viele Störche gibt, gibt es viele Babies; so will es die Geschichte).
4
Bitte mir Bescheid geben, falls ich hier etwas übersehen haben sollte.
15.4 Taxonomie der Ziele des Modellierens
Abb. 15.5 Modelle mit
schwarzer Kiste; angeleitetes
Lernen
253
Einflussgrößen
X1
Schwarze Kiste
Vorhersage
X2
Y
X3
Auf der anderen Seite: Impliziert Abwesenheit von Korrelation Abwesenheit von kausalen Zusammenhängen? Nein. Warum ist das so? Zum einen können Zusammenhänge vorliegen, die ein bestimmter Zusammenhangskoeffizient nicht aufdeckt (z. B. nicht-lineare
Zusammenhänge). Oder unbekannte Drittvariablen können einen Zusammenhang überdecken (sog. Suppressorvariablen). Die Metapher von Milton Friedmans Thermostat (Farrell
2012) zeigt das gut: Stellen Sie sich vor, Sie fahren in Ihrem Auto. Je stärker Sie auf das
Gaspedal drücken, umso schneller fährt das Auto (unter sonst gleichen Umständen), richtig? Richtig. Stellen Sie sich vor, das Auto fährt bergauf. Dann wird das Auto langsamer
werden (unter sonst gleichen Umständen), richtig? Richtig. Jetzt stellen Sie sich vor, Sie
sitzen auf dem Beifahrersitz und haben vom Autofahren wenig Ahnung. Die Fahrt geht
durch die Berge; das Auto fährt mit konstanter Geschwindigkeit. Sie könnten denken:
„Ah, wie viel Gas man gibt, hat keinen Einfluss auf die Geschwindigkeit.“ und „Ah, die
Steigung der Straße hat keinen Einfluss auf die Geschwindigkeit.“ Obwohl es tatsächlich
einen kausalen Einfluss beider Größen auf die Geschwindigkeit gibt, sieht man nicht unbedingt einen Zusammenhang in den Daten. Auf einer tieferen Ebene: Datenanalyse allein
kann die „Essenz“ eines Phänomens grundsätzlich nicht aufdecken (Briggs 2016; aber s.
Pearl (2009) für eine Gegenmeinung).
Vorhersagen und Erklären haben gemein, dass Eingabegrößen genutzt werden, um
Aussagen über eine Ausgabegröße zu treffen. Anders gesagt: Es liegt eine Zielgröße mit
bekannten Ausprägungen vor, zumindest für eine Reihe von Fällen. Hat man einen Datensatz, so kann man prüfen, wie gut das Modell funktioniert, also wie genau man die
Ausgabewerte vorhergesagt hat. Das ist also eine Art „Lernen mit Anleitung“ oder angeleitetes Lernen oder geleitetes Modellieren (supervised learning). Abb. 15.5 gibt diesen
Fall wieder.
Beim ungeleiteten Modellieren entfällt die Unterteilung zwischen Prädiktor und Kriterium. Ungeleitetes Modellieren (Reduzieren) bedeutet, dass man die Fülle des Datenmaterials verringert, indem man ähnliche Dinge zusammenfasst (vgl. Abb. 15.6).
Fasst man Fälle zusammen, so spricht man von fallreduzierendem Modellieren. Zum
Beispiel könnte man spektakulärerweise „Britta“, „Carla“ und „Dina“ zu „Frau“ und
„Joachim“, „Alois“ und „Casper“ zu „Mann“ zusammenfassen. Analog spricht man von
dimensionsreduzierendem Modellieren, wenn Variablen zusammengefasst werden. Hat
man z. B. einen Fragebogen zur Mitarbeiterzufriedenheit mit den Items „Mein Chef ist
fair“, „Mein Chef ist kompetent“, „Meinem Chef ist meine Karriere wichtig“, so könnte
254
Abb. 15.6 Die zwei Arten des
ungeleiteten Modellierens
15
Fallreduzierendes
Modellieren
Grundlagen des Modellierens
Dimensionsreduzierendes
Modellieren
man – wenn die Daten dies unterstützen – die Items zu einer Variable „Zufriedenheit mit
Chef“ zusammenfassen. Wenn also das Ziel des Modellierens lautet, die Daten zu reduzieren, also z. B. Kunden nach Persönlichkeit zu gruppieren, so ist die Lage anders als
beim geleiteten Modellieren: Es gibt keine Zielgröße. Wir wissen nicht, was die „wahre Kundengruppe“ ist, zu der Herr Casper Bussi-Ness gehört. Wir sagen eher: „OK, die
drei Typen sind sich irgendwie ähnlich, sie werden wohl zum selben Typ von Kunden
gehören.“ Wir tappen (noch mehr) im Dunkeln, was die „Wahrheit“ ist im Vergleich zum
angeleiteten Modellieren. Unser Modell muss ohne Hinweise darauf, was richtig ist, auskommen. Man spricht daher in diesem Fall von Lernen ohne Anleitung oder ungeleitetem
Modellieren (unsupervised learning).
15.5 Die vier Schritte des statistischen Modellierens
Modellieren ist in der Statistik eine zentrale Tätigkeit. Modelliert man in der Statistik, so
führt man die folgenden Schritte aus:
1. Man wählt eines der vier Ziele des Modellierens (z. B. ein prädiktives Modell).
2. Man wählt ein Modell aus (genauer: eine Modellfamilie), z. B. postuliert man, dass die
Körpergröße einen linearen Einfluss auf die Schuhgröße habe.
3. Man bestimmt (berechnet) die Details des Modells anhand der Daten, z. B.: Wie groß
ist die Steigung der Geraden und wo ist der Achsenabschnitt? Man sagt auch, dass man
die Modellparameter anhand der Daten schätzt (Modellinstantiierung oder Modellanpassung).
4. Dann prüft man, wie gut das Modell zu den Daten passt (Modellgüte); wie gut lässt
sich die Schuhgröße anhand der Körpergröße vorhersagen bzw. wie groß ist der Vorhersagefehler?
15.6 Einfache vs. komplexe Modelle: Unter- vs. Überanpassung
255
15.6 Einfache vs. komplexe Modelle: Unter- vs. Überanpassung
Je komplexer ein Modell, desto besser passt es (unter sonst gleichen Umständen) auf den
Gegenstandsbereich. Eine grobe, holzschnittartige Theorie ist doch schlechter als eine,
die feine Nuancen berücksichtigt, oder nicht? Einiges spricht dafür; aber auch einiges
dagegen. Schauen wir uns ein Problem mit komplexen Modellen an.
Teil A (links) von Abb. 15.7 zeigt den Datensatz ohne Modell; Teil B legt ein lineares
Modell (Gerade) in die Daten. Teil C zeigt ein Modell, welches die Daten exakt erklärt
– die „zittrige“ Linie geht durch alle Punkte. Die 4. Teilabbildung (D) zeigt ein Modell,
welches die Punkte gut beschreibt, aber nicht exakt trifft.
Welchem Modell würden Sie (am meisten) vertrauen? Modell C beschreibt die Daten
sehr gut, aber hat das Modell überhaupt eine „Idee“ vom Gegenstandsbereich, eine „Ahnung“, wie Y und X zusammenhängen bzw. wie X einen Einfluss auf Y ausübt? Offenbar
nicht. Das Modell ist „übergenau“ oder zu komplex. Man spricht von Überanpassung
(Overfitting). Das Modell scheint zufälliges, bedeutungsloses Rauschen zu ernst zu nehmen. Das Resultat ist eine zu wackelige Linie – ein schlechtes Modell, da wir wenig
Anleitung haben, auf welche Y-Werte wir tippen müssten, wenn wir neue, unbekannte
X-Werte bekämen.
Beschreibt ein Modell (wie Modell C hier) eine Stichprobe sehr gut, heißt das noch
nicht, dass es auch zukünftige (und vergleichbare) Stichproben gut beschreiben
wird. Die Güte (Vorhersagegenauigkeit) eines Modells sollte sich daher stets auf
eine neue Stichprobe beziehen (Test-Stichprobe), die nicht in der Stichprobe beim
Anpassen des Modells (Trainings-Stichprobe) enthalten war.
Während Modell C zu detailverliebt ist, ist Modell B zu simpel. Die Gerade beschreibt
die Y-Werte nur sehr schlecht. Man hätte gleich den Mittelwert von Y als Schätzwert für
jedes einzelne Yi hernehmen können. Dieses lineare Modell ist unterangepasst, könnte
A
B
C
D
1.0
1.0
1.0
1.0
0.5
0.5
0.5
0.5
0.0
0.0
0.0
0.0
-0.5
-0.5
-0.5
-0.5
-1.0
-1.0
-1.0
-1.0
2.5 5.0 7.5 10.0
x
2.5 5.0 7.5 10.0
x
2.5 5.0 7.5 10.0
2.5 5.0 7.5 10.0
x
Abb. 15.7 Welches Modell (Teile B–D) passt am besten zu den Daten (Teil A)?
x
Abb. 15.8 Mittlere Komplexität hat die beste
Vorhersagegenauigkeit (am
wenigsten Fehler) in der TestStichprobe
15
Fehler
256
Grundlagen des Modellierens
Test-Stichprobe
Trainings-Stichprobe
Komplexität
man sagen (underfitting). Auch dieses Modell wird uns wenig helfen können, wenn es
darum geht, zukünftige Y-Werte vorherzusagen (gegeben jeweils einen bestimmten XWert).
Ah! Modell D scheint das Wesentliche, die „Essenz“ der „Punktebewegung“ zu erfassen. Nicht die Details, die kleinen Abweichungen, aber die „große Linie“ scheint gut
getroffen. Dieses Modell erscheint geeignet, zukünftige Werte gut zu beschreiben. Das
Modell D ist damit ein Kompromiss aus Einfachheit und Komplexität und würde besonders passen, wenn es darum gehen sollte, zyklische Veränderungen zu erklären.5
Je komplexer ein Modell ist, desto besser kann es einen bekannten Datensatz (Trainings-Stichprobe) beschreiben. Allerdings ist das Modell, welches den
Trainings-Datensatz am besten beschreibt, nicht zwangsläufig das Modell, welches
neue, unbekannte Daten am besten beschreibt. Oft im Gegenteil!
Je komplexer das Modell, desto kleiner wird der Fehler im Trainings-Datensatz tendenziell (James et al. 2013). Allerdings: Die Fehler-Kurve im Test-Datensatz ist U-förmig
(konvex): Mit steigender Komplexität wird der Fehler einige Zeit lang kleiner; ab einer
gewissen Komplexität steigt der Fehler im Test-Datensatz wieder (vgl. Abb. 15.8). Halten
wir fest: Das Modell sollte exakte Vorhersagen (in Test-Datensatz) treffen, aber so einfach
wie möglich sein (sparsam im Sinne von Occams Rasiermesser).
15.7 Bias-Varianz-Abwägung
Einfache Modelle verfehlen oft wesentliche Aspekte des Gegenstandsbereichs; die Wirklichkeit ist häufig zu komplex für einfache Modelle. Die resultierende Verzerrung in den
vorhergesagten Werten nennt man auch Bias. Einfach ausgedrückt: Ist ein Modell zu ein5
Tatsächlich wurden die Y-Werte als Sinus-Funktion plus etwas normalverteiltes Rauschen simuliert.
257
1.0
0.5
0.0
-0.5
-1.0
y
y
15.8 Trainings- vs. Test-Stichprobe
2.5
5.0
7.5
1.0
0.5
0.0
-0.5
-1.0
10.0
x
2.5
5.0
7.5
10.0
x
Abb. 15.9 Der Spagat zwischen Verzerrung und Varianz
fach, passt es zu wenig zu den Daten (underfitting). Auf der anderen Seite ist ein einfaches
Modell aber tendenziell robust in dem Sinne, dass sich die vorhergesagten Werte kaum ändern, falls sich der Trainings-Datensatz etwas ändert. So wird sich die Modellgleichung
von Modell B in Abb. 15.7 nur wenig ändern, wenn man eine Beobachtung hinzufügt; die
Modellgleichung von Modell C könnte sich hingegen stark ändern, wenn ein weiterer Fall
hinzugefügt wird.
Ist das Modell aber zu reichhaltig („komplex“), bildet es alle Details des TrainingsDatensatzes ab, dann wird es auch zufällige Variationen des Datensatzes vorhersagen;
Variation, die nicht relevant ist, die nichts Eigentliches abbildet (vgl. Abb. 15.7, Modell
C). Das Modell ist „überangepasst“ (Overfitting). Geringfügige Änderungen im Datensatz
können das Modell stark verändern: Das Modell ist nicht robust – seine Varianz ist hoch.
Auf der positiven Seite werden die Nuancen der Daten gut abgebildet: Die Verzerrung
durch das Modell (der Bias) ist gering bzw. tendenziell geringer als bei einfachen Modellen. Grundsätzlich gilt: Einfache Modelle: viel Bias, wenig Varianz. Komplexe Modelle:
wenig Bias, viel Varianz. Dieser Sachverhalt ist in Abb. 15.9 dargestellt (vgl. Kuhn und
Johnson (2013)).
Der linke Teil von Abb. 15.9 zeigt ein komplexes Modell;6 der Graph erscheint „zittrig“; kleine Änderungen in den Daten können große Auswirkungen auf die Vorhersagen
(Y-Achse) haben. Darüber hinaus sind einige Details des Modells unplausibel: Es gibt viele kleine „Hügel“, die nicht augenscheinlich plausibel sind. Der rechte Teil von Abb. 15.9
hingegen ist sehr einfach und robust. Änderungen in den Daten werden vergleichsweise
wenig Einfluss auf die Modellvorhersagen (die beiden horizontalen Linien) haben.
15.8
Trainings- vs. Test-Stichprobe
Wie wir gerade gesehen haben, kann man immer ein Modell finden, welches die vorhandenen Daten exakt beschreibt. Das gleicht der Tatsache, dass man im Nachhinein (also bei
vorhandenen Daten) leicht eine Erklärung findet.7 Ob diese Erklärung sich in der Zukunft,
bei unbekannten Daten bewahrheitet, steht auf einem ganz anderen Blatt.
6
7
Genauer gesagt ein Polynom von Grad 15.
Ein Phänomen, das man von Fußballspielen kennt.
258
15
Grundlagen des Modellierens
Daher sollte man, wenn die Stichprobe groß ist, sein Modell an einer Stichprobe entwickeln („trainieren“, „schätzen“ oder auch „anpassen“ genannt) und an einer zweiten Stichprobe testen. Die erste Stichprobe nennt man auch training sample (Trainings-Stichprobe)
und die zweite test sample (Test-Stichprobe). Entscheidend ist, dass das Test-Sample beim
Entwickeln des Modells unbekannt war bzw. nicht verwendet wurde. Die Modellgüte ist
im Trainings-Sample meist deutlich besser als im Test-Sample (vgl. die Fallstudie dazu in
Abschn. 18.9).
Die Güte des Modells sollte bei ausreichend großem Datensatz nur anhand eines –
bislang nicht verwendeten – Test-Samples überprüft werden. Das Test-Sample darf
bis zur Modellüberprüfung nicht analysiert werden.
Ist der Datensatz jedoch klein, so ist es empfehlenswert, auf eine Test-Stichprobe zu
verzichten. Der Grund ist, dass bei knappen Daten jede Information für die Entwicklung
des Modells benötigt wird. Außerdem wird eine kleine Test-Stichprobe nur ungenaue und
daher vielleicht unbrauchbare Ergebnisse liefern (Hawkins et al. 2003; Kuhn und Johnson
2013; Molinaro et al. 2005). Einmal mehr gilt: Viel hilft viel! Zum Aufteilen verfügbarer
Daten in eine Trainings- und eine Test-Stichprobe gibt es mehrere Wege. Ein einfacher
sieht so aus:
train <- slice(stats_test, 1:200)
test <- slice(stats_test, 201:306)
dplyr::slice() schneidet eine „Scheibe“ aus einem Datensatz. Mit anti_join()
kann man einen generellen Ansatz wählen: anti_join() vereinigt die nicht-gleichen
Zeilen zweier Datensätze. Die Gleichheit wird geprüft anhand aller Spalten mit identischem Namen in beiden Datensätzen.
train <- stats_test %>%
sample_frac(.8, replace = FALSE)
# Stichprobe von 80%, ohne Zurücklegen
test <- stats_test %>%
anti_join(train)
# Behalte nur Zeilen, die nicht in "train" vorkommen
Damit haben wir ein Trainings-Sample (train), in dem wir ein oder besser mehrere
Modelle berechnen können. Die Modellgüte beurteilen wir dann im Test-Sample. Der
Nachteil dieses Verfahrens, so bestechend die Einfachheit auch ist, liegt in den ungleichmäßigen Verteilungen, die resultieren können. Daher ist es vorteilhafter, die Verteilung des
Kriteriums und ggf. auch der Prädiktoren zu kontrollieren. Eine Möglichkeit dazu bietet
die Funktion caret::createDataPartition() (vgl. Kap. 22).
15.9 Resampling und Kreuzvalidierung
259
15.9 Resampling und Kreuzvalidierung
So einfach und praktisch das Aufteilen der Daten in eine Trainings- und eine TestStichprobe auch ist: Ein Nachteil ist, dass unsere Modellgüte wohl anders wäre, hätten
wir andere Fälle im Test-Sample erwischt. Würden wir den gesamten Datensatz auf andere Art und Weise in ein Trainings-Sample und ein Test-Sample aufteilen, so hätten wir
wohl andere Ergebnisse. Was, wenn diese Ergebnisse nun deutlich von den ersten abweichen? Dann würde unser Vertrauen in die Modellgüte sinken. Es wäre also hilfreich, wenn
wir das Aufteilen der gesamten Stichprobe in Trainings- und Test-Sample k Mal (z. B.
k D 10) wiederholen,8 um stabilere Schätzungen der Vorhersagegüte im Test-Sample zu
bekommen; Abb. 15.10 versinnbildlicht diese Idee. Solche Verfahren, in denen aus dem
bestehenden Datensatz neue Teil-Stichproben gezogen werden, um die Modellgüte zu
bestimmen, nennt man Resampling-Ansätze. Das Verfahren wie in Abb. 15.10 ist eine Art
von Resampling, bezeichnet als k-fache Kreuzvalidierung (k-fold cross-validation). Jeder
Fall kommt dabei genau 1 Mal in das Test-Sample und k-1 Mal in das Trainings-Sample;
die Trainings-Samples sind in jedem Durchlauf gleich groß. Jeder Durchgang wird auch
als Faltung (fold) bezeichnet. Die Gesamtfehler werden als Durchschnitt der einzelnen
Fehler berechnet. Der Vorteil dieses Verfahrens ist, dass unsere Vorhersagen stabiler werden – und damit im Schnitt besser sind. Ein Nachteil ist, dass ein Modell dann k-mal
berechnet werden muss, was Zeit kostet.
Die Kreuzvalidierung ist eine Methode zur Beurteilung der Modellgüte. Der Prozess
der Aufteilung in Trainings- und Test-Sample wird dabei (mehrfach) vorgenommen.
Die Modellgüte wird anschließend summarisch beurteilt.
Durchlauf 1
Durchlauf 2
Durchlauf 3
Durchlauf 4
Test-Sample
Test-Sample
Test-Sample
Test-Sample
Test-Sample
Test-Sample
Test-Sample
Test-Sample
Abb. 15.10 Beispiel für eine k D 4 Kreuzvalidierung
8
Zehn Mal scheint sich gut zu bewähren, aber auch andere Werte können sinnvoll sein: http://
appliedpredictivemodeling.com/blog/2014/11/27/vpuig01pqbklmi72b8lcl3ij5hj2qm.
260
15
Grundlagen des Modellierens
15.10 Wann welches Modell?
Tja, mit dieser Frage lässt sich ein Gutteil des Kopfzerbrechens im Modellierungsmetier
erfassen. Die einfache Antwort lautet: Es gibt kein „bestes Modell“, kein Modell, das
immer am besten abschneidet, aber es mag für einen bestimmten Gegenstandsbereich, in
einem bestimmten (historisch-kulturellen) Kontext, für ein bestimmtes Ziel und mit einer
bestimmten Stichprobe ein bestmögliches Modell geben. Dazu einige Eckpfeiler:
Unter sonst gleichen Umständen sind einfachere Modelle den komplexeren vorzuziehen. Ein einfaches Modell sollte zu Beginn als Referenzwert getestet werden.
Je nach Ziel der Modellierung ist ein erklärendes Modell oder ein Modell mit reinem
Vorhersage-Charakter vorzuziehen.
Man sollte stets mehrere Modelle vergleichen, um abzuschätzen, welches Modell in
der aktuellen Situation geeigneter ist.
Inhaltliches Wissen über den Gegenstand (z. B. Kundenzufriedenheit) sollte einfließen.
15.11 Modellgüte
Wie „gut“ ist mein Modell? Modelle zu bewerten bzw. vergleichend zu bewerten, ist eine der wichtigsten Aufgaben beim Modellieren. Ein Klassifikationsmodell muss anders
beurteilt werden als ein Modell der numerischen Vorhersage (Regressionsmodell). Die
Frage der Modellgüte hat feine technisch-statistische Verästelungen, aber einige wesentliche Aspekte kann man einfach zusammenfassen:
Kriterium der theoretischen Plausibilität: Ein statistisches Modell sollte theoretisch plausibel sein.
Kriterium der guten Vorhersage: Die Vorhersagen eines Modells sollen präzise
sein.
Kriterium der Neuartigkeit: Die Vorhersagen sollen neues Wissen produzieren,
sie sollen „überraschend“ sein.
Kriterium der Situationsangemessenheit: Die Güte des Modells ist auf die konkrete Situation abzustellen.
Anstelle „alles mit allem“ durchzuprobieren, sollte man sich auf Modelle konzentrieren, die theoretisch plausibel sind. Die Modellwahl ist theoretisch zu begründen. Dass ein
Modell die Wirklichkeit präzise vorhersagen soll, liegt auf der Hand. Hier verdient nur der
Term vorhersagen Beachtung. Es ist einfach, im Nachhinein Fakten (Daten) zu erklären.
Jede Nachbesprechung eines Bundesliga-Spiels liefert reichlich Gelegenheit, post hoc Er-
15.11
Modellgüte
261
klärungen zu hören. Schwieriger sind Vorhersagen.9 Die Modellgüte ist also idealerweise
an in der Zukunft liegenden Ereignissen bzw. deren Vorhersage zu messen. Zur Not kann
man auch schon in der Vergangenheit angefallene Daten hernehmen. Dann müssen diese
Daten aber für das Modell neu sein. Was ist mit überraschend gemeint? Eine Vorhersage,
dass die Temperatur morgen in Nürnberg zwischen 30 und C40 °C liegen wird, ist sicherlich sehr treffend, aber nicht unbedingt präzise und nicht wirklich überraschend. Die
Vorhersage, dass der nächste Chef der Maurer-Innung (wenn es diese geben sollte) ein
Mann sein wird, und nicht eine Frau, kann zwar präzise sein, ist aber nicht überraschend.
Wir werden also unseren Hut dann vor dem Modell ziehen, wenn die Vorhersagen sowohl
präzise als auch überraschend sind.
15.11.1 Modellgüte in numerischen Vorhersagemodellen
Einfach gesagt ist das Ziel eines numerischen Vorhersagemodells – auch als Regressionsmodell bezeichnet –, dass der Wert der Vorhersage nahe dem tatsächlichen Wert ist (auch
als wahrer Wert bezeichnet). Sage ich für morgen eine Höchsttemperatur von 30 Grad
voraus, und es werden 20 Grad, so ist mein Vorhersagefehler 10 Grad (die Richtung des
Fehlers ist meist weniger wichtig). Vorhersagefehler sollen möglichst gering sein. Dabei
muss beachtet werden, dass komplexere Modelle bessere Vorhersagen treffen. Daher „bestrafen“ manche Gütekoeffizienten komplexe Modelle, um diesen Vorteil auszugleichen.
In numerischen Vorhersagemodellen beruht der Vorhersagefehler oft auf der Differenz zwischen tatsächlichen und vorhergesagten Werten.
Der einfache Grundsatz lautet: Je geringer die Vorhersagefehler im Test-Sample, desto
besser; Abb. 15.11 zeigt ein Regressionsmodell mit geringem Vorhersagefehler (A; links)
und ein Regressionsmodell mit hohem Vorhersagefehler B; rechts).
Außerdem kann (und sollte) die Güte eines Regressionsmodells gegen ein Nullmodell
abgeglichen sein. Ein Nullmodell stellt ein sehr einfaches oder „dummes“ Modell dar –
etwa indem es für morgen die Jahresdurchschnittstemperatur vorhersagt (oder den Mittelwert aller Tage, die dem Modell bekannt sind). Ist die Vorhersage unseres Modells nicht
(wesentlich) besser als die des Nullmodells, werden sich andere wenig beeindruckt von
unserem Modell zeigen.
15.11.2
Modellgüte bei Klassifikationsmodellen
Bei Klassifikationsmodellen lautet die Schlüsselfrage: Wie oft war die Vorhersage des
Modells richtig? Wenn Professor Feistersack das Klausurergebnis von fünf Studenten
9
Gerade wenn sie die Zukunft betreffen; ein Bonmot, das Yogi Berra nachgesagt wird.
262
15
B - viel Vorhersagefehler
6
6
3
3
y2
y1
A - wenig Vorhersagefehler
0
-3
Grundlagen des Modellierens
0
-3
-6
-6
-3
-2
-1
0
1
2
-3
-2
-1
x
0
1
2
x
Abb. 15.11 Geringer (links; A) vs. hoher (rechts, B) Vorhersagefehler
Abb. 15.12 Sinnbild für die
Trefferquote eines Klassifikationsmodells
ID
beobachtet
vorhergesagt
A
bestanden
bestanden
B
bestanden
durchgefallen
C
durchgefallen
durchgefallen
D
bestanden
bestanden
E
durchgefallen
durchgefallen
vorhersagt und viermal richtig liegt, so hat er eine Trefferquote (Richtigkeit, Korrektklassifikationsrate) von 4=5 D 80 %; Abb. 15.12 stellt diesen Zusammenhang sinnbildlich
dar.
Zu beachten ist, dass auch bei Klassifikationsmodellen ein Nullmodell zum Vergleich
herangezogen werden sollte. Wenn ich weiß, dass es in Nürnberg im Schnitt an einem von
drei Tagen regnet, und mein Modell sagt für morgen eine Regenwahrscheinlichkeit von
33 % voraus, so ist das nicht gerade beeindruckend. Das Modell wäre nicht besser als ein
plausibles Referenzmodell (Nullmodell).
15.12 Der Fluch der Dimension
Nehmen wir an, wir möchten datenbasiert vorhersagen, ob es morgen regnet oder nicht.
Ein Prädiktor steht uns zur Verfügung: die Niederschlagsmenge von gestern. Wahrscheinlich wäre man zuversichtlich, dass weitere Prädiktoren wie Luftfeuchtigkeit, Jahreszeit
und Temperatur die Genauigkeit der Vorhersage verbessern könnten. Das führt zur einfachen Idee, dass ein prädiktives Modell von der Aufnahme weiterer Prädiktoren gewinnen
könnte (vgl. Bishop (2006), Kapitel 1.4).
15.12
Der Fluch der Dimension
263
2
x2
1
0
-1
-2
-2
-1
0
1
2
x1
Abb. 15.13 Ein einfaches Klassifikationsmodell
Abb. 15.13 verdeutlicht das Szenario für zwei Prädiktoren, x1 und x2, z. B. Niederschlagsmenge und Temperatur gestern. Ein einfaches Vorhersagemodell könnte vorschlagen, alle Punkte „in der Nähe“ zu betrachten und dann das Ereignis (Regen ja vs. Regen
nein) zu wählen, welches am häufigsten unter diesen Punkten vertreten ist. In diesem Fall
wurde der Wertebereich von x1 und x2 in vier gleich große Teile (Rechtecke oder „Zellen“) unterteilt; 4 4 Zellen resultieren. Die Farbe der Zellen entspricht dem in dieser
Region häufigsten Ereignis. Die Form der Punkte symbolisiert ebenfalls das untersuchte
Ereignis: Regen ja (Dreieck) vs. Regen nein (Kreis).
Für den morgigen Tag stehen die entsprechenden Messwerte zur Verfügung; in
Abb. 15.13 ist der resultierende Punkt mit einem Stern gekennzeichnet. Die Punkte
in der Umgebung unseres Punktes sprechen sich mehrheitlich gegen Regen aus; die Zelle ist dunkel gefärbt. Entsprechend lautet die Vorhersage dieses Modells genauso. Man
beachte, dass wir keine Vorhersage treffen könnten, wenn die Zelle leer wäre, wenn also
keine Messwerte zur Verfügung stünden, die zur Vorhersage genutzt werden könnten.
Auch mit wenigen Messwerten kann man an der Qualität der Vorhersage berechtigte
Zweifel anmelden.
Ein Problem dieses Modells ist, dass die Anzahl der Zellen exponentiell mit der Anzahl
der Prädiktoren steigt. Das exponentielle Anwachsen ist in Abb. 15.14 angedeutet: Liegt
die Anzahl der Dimensionen D bei eins, so liegt die Gesamtzahl der Zellen bei k D , wobei
k die Anzahl der Zellen pro Dimension ist. Zur besseren Übersicht sind in Abb. 15.14 nur
ein paar Zellen dargestellt.
264
15
Grundlagen des Modellierens
Abb. 15.14 Die Anzahl der
Zellen wächst exponentiell
x2
x2
x1
x1
D=1
x1
D=2
x3
D=3
Unterteilt man beispielsweise k D 10 Zellen pro Dimension – das könnten 10 verschiedenen Temperaturbereiche sein – so sind das 10 (101 ) Zellen für D D 1, bei D D 2
schon 100 (102 ). Bei 10 Prädiktoren sind es schon 10000000000 (1010 ) Zellen; mehr, als
es aktuell Menschen auf der Erde gibt. Möchte man alle Zellen mit Messwerten füllen,
um für jede Zelle Vorhersagen anstellen zu können, steigt die Zahl der benötigten Messwerte schnell ins Astronomische. Die benötigte Stichprobengröße wird mit steigendem
D schnell unhandlich groß. Dazu kommt erschwerend, dass sich die Messwerte meist
nicht gleichmäßig verteilen, sondern sich in bestimmten Messwertbereichen konzentrieren. Möchte man das ganze Spektrum vorhersagen können, muss man aber auch seltenere
Randbereiche mit (ausreichend) Werten bestücken.
Eine große Anzahl an Prädiktoren (Dimensionen) verlangt im Allgemeinen nach
einer großen Stichprobe. Genauer gesagt steigt die benötigte Stichprobengröße exponentiell mit der Anzahl der Dimensionen. Eine große Prädiktorenzahl kann –
muss aber nicht – problematisch für eine Modellierung sein. Man sollte gut prüfen, ob die Anzahl der Dimensionen nicht verringert werden könnte (vgl. Kuhn und
Johnson (2013)).
So problematisch der „Fluch der Dimension“ auch ist, es gibt ein paar Ansätze zur Lösung oder zumindest Abmilderung. Man sollte immer prüfen, ob nicht auf einige Dimensionen (Prädiktoren) verzichtet werden kann. Häufig kann man auch einige Dimensionen,
wenn sie sich sehr ähnlich sind, zusammenfassen (Dimensionsreduktion). Ein typisches
Verfahren dazu ist die Hauptkomponentenanalyse (James et al. 2013). Außerdem kann
man Lücken im Verlauf von kontinuierlichen Variablen durch Techniken des Extrapolierens schließen: Haben Menschen der Größe x 1 das Gewicht y 1, und Menschen der
Größe x C 1 das Gewicht y C 1, so ist es nicht unplausibel, bei Größe x auf das Gewicht
y zu schließen, auch wenn für den Wert x keine Daten vorliegen.
15.12
Der Fluch der Dimension
265
Aufgaben
Richtig oder falsch?10
1.
Die Aussage „Pro Kilo Schoki steigt der Hüftumfang um einen Zentimeter“
kann als Beispiel für ein deterministisches Modell herhalten.
2. Gruppiert man Kunden nach ähnlichen Kaufprofilen, so ist man insofern am
„Reduzieren“ der Datenmenge interessiert.
3. Es gilt: Je komplexer ein Modell, desto besser.
4. Mit „Bias“ ist gemeint, dass ein Modellgraph „zittrig“ oder „wackelig“ ist –
sich also bei geringer Änderung der Stichprobendaten massiv in den Vorhersagen ändert.
5. In der Gleichung Y D f .x/ C steht für den Teil der Kriteriums, der nicht
durch das Modell erklärt wird.
6. Bei der k-fachen Kreuzvalidierung gilt: Jeder Fall kommt dabei genau einmal
in das Test-Sample und k 1-mal in das Trainings-Sample.
7. Modelle mit hoher Komplexität haben meist den kleinsten Vorhersagefehler in
der Trainings-Stichprobe.
8. Modelle mit hoher Komplexität haben meist den kleinsten Vorhersagefehler in
der Test-Stichprobe.
9. Ein Modell sagt für die nächsten zehn Tage jeden Tag Regen vorher, und
tatsächlich regnet es an sieben von zehn Tagen. Damit hat das Modell eine
Gesamtgenauigkeit von 70 %.
10. Ein Modell sagt für 90 % der nächsten Tage Regen vorher; allerdings regnet es
in dieser Gegend zu dieser Jahreszeit an 90 % der Tage. Die Gesamtgenauigkeit
des Modells ist hoch, aber trotzdem ist der Mehrwert des Modells gering.
Aufgaben
1. Erfolg beim Online-Dating: Lesen Sie diesen11 Artikel (Sauer und Wolff 2016).
Zeichnen Sie ein Pfaddiagramm zum Modell.12
2. Ziele des Modellierens: Welche drei Ziele des Modellierens kann man unterscheiden?13
10
R, R, F, F, R, R, R, F, R, R.
https://thewinnower.com/papers///5202-the-effect-of-a-status-symbol-on-success-in-onlinedating-//an-experimental-study-data-paper?review_it=true.
12
Status ! Erfolg beim Online-Dating.
13
S. Abschn. 15.4.
11
266
15
Grundlagen des Modellierens
3. Bias-Varianz-Abwägung: Betrachten Sie Abb. 15.9. Welches der beiden Modelle (visualisiert im jeweiligen Plot) ist a) mehr bzw. weniger robust gegenüber
Änderungen im Datensatz, b) mehr oder weniger präzise?14
4. Ist eine Trefferquote von 99 % in einem Klassifikationsmodell automatisch
gut?15
5. Möchte man eine Test- von einer Trainings-Stichprobe absondern, so kann man
den R-Befehl slice() nutzen. Welchen Nachteil hat dieses Vorgehen?16
14
a) Rechtes Modell ist robuster; b) linkes Bild ist präziser im Sinne eines geringeren Vorhersagefehlers.
15
Nein, es hängt von der Verteilung des Kriteriums ab.
16
Die Verteilung des Kriteriums wird nicht ausgewogen; es kann also passieren, dass alle „leichten“ Fälle im Trainings-Sample landen und alle „schweren“ Fälle im Test-Sample. Dann würde der
Algorithmus erfolgreich im Trainings-Sample lernen, aber im Test-Sample „überraschend“ schlecht
abschneiden.
16
Inferenzstatistik
Lernziele
Den Unterschied zwischen Stichprobe und Grundgesamtheit verstehen
Die Idee des statistischen Inferierens erläutern können
Den p-Wert erläutern können
Den p-Wert kritisieren können
Alternativen zum p-Wert kennen
Inferenzstatistische Verfahren für häufige Fragestellungen kennen
Gängige Effektstärken kennen und inferenzstatistischen Testverfahren zuordnen
können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(pwr)
library(compute.es)
library(tidyverse)
library(broom)
library(BayesFactor)
library(mosaic)
library(boot)
library(bootES)
library(mosaic)
data(flights, package = "nycflights13")
data(extra, package = "pradadata")
data(wo_men, package = "pradadata")
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_16
267
268
16
Inferenzstatistik
16.1 Wozu Inferenzstatistik?
In bisherigen Kapiteln haben wir einen Datensatz betrachtet und Aussagen über diesen
getroffen. Das ist zwar schön, aber häufig ist man an mehr interessiert: Sie haben zehn Ihrer Freunde zu politischen Präferenzen gefragt? Interessant, aber für eine Bild-Schlagzeile
reicht das nicht. Hätten Sie alle Bundesbürger befragt, würden die Reporter (vielleicht)
bei Ihnen Schlange stehen. Leider ist es aufwändig, alle Wahlberechtigten des Landes zu
befragen. Angenehm ist es dann, wenn die Statistik es einem erlaubt, von der Stichprobe auf eine Gesamtmenge zu schließen – genauer von einer Stichprobenzkennzahl auf
den korrespondierenden Wert einer Grundgesamtheit (synonym: Population). Die Stichprobenkennzahl bezeichnet man auch als eine Statistik und den korrespondierenden Wert
in der Population auch als Parameter oder Populationsparameter. Diese Art, Schlüsse
zu ziehen, bezeichnet man als Inferenzstatistik. Betrachten Sie zur Illustration Abb. 16.1:
Im linken Teil sehen Sie einige Stichproben der PLZ-Gebiete, und rechts sehen Sie das
gesamte Bundesgebiet, auf das Sie in der Regel Ihre Aussage verallgemeinern möchten.
Hat man also „nur“ eine Stichprobe, keine Vollerhebung, so stellt sich die Frage, wie
aussagekräftig eine Stichprobe ist – wie gut die Stichprobe den entsprechenden Sachverhalt in der Population widerspiegelt. Aufgabe der Inferenzstatistik ist es, von einer
Stichprobe auf eine zugrunde liegende Population zu schließen. Genauer gesagt untersucht man eine der folgenden Fragen:
1. Welcher Wert in der Population ist am plausibelsten?
2. Welcher Wertebereich in der Population ist der plausibelste?
3. Kann man ausschließen, dass in der Population ein bestimmter Wert vorliegt?
Dabei gilt immer: Die Antworten auf diese Fragen beziehen sich auf die Population. Und
die Antworten sind immer mit Unsicherheit behaftet. Bei der ersten Frage spricht man
von Punktschätzung; im zweiten Fall von Bereichsschätzung. Die dritte Frage zielt auf
Abb. 16.1 Stichproben vs. Vollerhebung
16.2 Der p-Wert
269
das Testen von Hypothesen ab. Der p-Wert wird oft als Messgröße zur Beantwortung der
dritten Frage herangezogen.1
Die Methoden der Inferenzstatistik beziehen sich auf Zufallsstichproben. Hat man
keine Zufallsstichprobe, so erübrigt sich ein Inferenzschluss auf eine Population.
16.2 Der p-Wert
16.2.1 Was sagt der p-Wert?
Der p-Wert, entwickelt von Sir Ronald Fisher (s. Abb. 16.2; („Fisher, Ronald Aylmer“
2008)), ist die heilige Kuh vieler Forscher. Das ist nicht normativ, sondern deskriptiv
gemeint. Der p-Wert entscheidet (häufig) darüber, was publiziert wird, und damit, was
als Wissenschaft sichtbar ist – und damit, was Wissenschaft ist (wiederum deskriptiv,
nicht normativ gemeint). Kurz: Dem p-Wert kommt eine große Bedeutung zu bzw. ihm
wird eine hohe Bedeutung beigemessen.2 Der p-Wert ist der tragende Ziegelstein in einem Theoriegebäude, das als Nullhypothesen-Signifikanztesten (NHST3 ) bezeichnet wird.
Oder kurz als „Inferenzstatistik“ bezeichnet. Was sagt uns der p-Wert? Eine gute intuitive
Definition ist:
Abb. 16.2 Der größte Statistiker des 20. Jahrhunderts
(p < :05)
1
Die Inferenzstatistik wird oft als Mischung der Ansätze von Fisher (1955) und Neyman und Pearson (1933) dargestellt; besser wäre, die beiden Theorien auseinanderzuhalten. Auch in diesem Buch
wird jedoch der Einfachheit halber nicht zwischen den beiden Ansätzen unterschieden. Eine gute
Aufschlüsselung der Unterschiede findet sich bei Gigerenzer (2004).
2
https://imgflip.com/i/22q4jc.
3
Der Term „Signifikanz-Hypothesen-Inferenz-Testen“ hat sich nicht durchgesetzt.
270
16
Inferenzstatistik
Der p-Wert ist eine Zahl zwischen 0 und 1, die aussagt, wie gut die Daten zur
Nullhypothese passen, wie plausibel die Daten für die Hypothese sind. Häufig wird
5 % als Grenzwert herangezogen; das ist reine Konvention. p-Werte kleiner als 5 %
zeigen, dass die Daten unter der getesteten Hypothese unplausibel sind, und führen
zur Verwerfung der Hypothese. So ein Ergebnis nennt man statistisch signifikant.
Andernfalls wird die Hypothese beibehalten; das Ergebnis ist nicht signifikant.
Die (genauere) Definition des p-Werts ist kompliziert; man kann sie leicht missverstehen.
Der p-Wert – P .T jH / – gibt die Wahrscheinlichkeit P unserer Teststatistik T an
(und noch extremere Ausprägungen von T ), unter der Annahme, dass die getestete
Hypothese H wahr ist (und alle sonstigen Annahmen und wenn wir den Versuch
unendlich oft wiederholen würden, unter identischen Bedingungen und ansonsten
zufällig).
Mit anderen Worten: Je kleiner p, desto schlechter passen die Daten zur getesteten
Hypothese, der sog. Nullhypothese. Als Nullhypothese (H0 ) bezeichnet man die getestete
Hypothese. Der Name Nullhypothese rührt vom Begriff nullifizieren (verwerfen) her, da
(nach dem Falsifikationismus) eine These immer nur verworfen, nie bestätigt werden kann
(Popper 1968). Da viele Menschen die eigene Hypothese nur ungern verwerfen wollen,
wird die „gegnerische“ Hypothese, die man loswerden will, als H0 getestet.4 Fällt p unter die magische Zahl von 5 %, so proklamiert man Erfolg (Signifikanz) und verwirft die
H0 . Natürlich muss es nicht unbedingt 5 % sein; aber viele Forscher halten sich an diese
Konvention. Allgemeiner spricht man vom Alpha-Niveau (Ë›).5
4
Das ist genau das Gegenteil von dem, was Herr Popper mit seiner Idee des Falsifizierens meinte:
Man soll versuchen, seine eigene Hypothese zu falsifizieren, nicht versuchen, sie zu schätzen, indem man die H0 testet. Außerdem sollte es so sein, dass mit steigender Genauigkeit einer Messung
bzw. mit größerer Stichprobe eine Hypothese leichter falsifiziert werden müsste: Meine Hypothese
beharrt auf einen gewissen Wert, 42.701. Experimente mit besserem Design, also genaueren Ergebnissen, sollten eine härtere Prüfung für meine Hypothese sein, also leichter zu falsifizieren sein:
Genauere Prüfung lässt eine Behauptung schneller als falsch erkennen. Mit dem gängigen Vorgehen
verhält es sich aber genau andersherum: Größere Stichproben werden meine Hypothese zunehmend
in einem günstigeren Licht erscheinen lassen. Diese und weitere Probleme mit dem p-Wert in den
Sozialwissenschaften sind im klassischen Artikel von Meehl (1978) herausgearbeitet.
5
Diese Methode, wie sie hier dargestellt und meist gelehrt wird, ist eine Mischung aus der von
Fisher (1955) und der von Neyman und Pearson (1933). Eine genauere Auseinandersetzung findet
sich bei Gigerenzer (2004) oder Eid et al. (2010).
16.2 Der p-Wert
16.2.2
271
Der zwielichtige Statistiker – ein einführendes Beispiel zur
Inferenzstatistik
Nach einem langen Arbeitstag machen Sie sich auf den Weg nach Hause; ihr Weg führt
Sie durch eine dunkle Ecke. Just dort regt sich auf einmal eine Gestalt im Schatten. Die
Person spricht Sie an: „Na, Lust auf ein Spielchen?“ Sie willigen sofort ein. Die Person
stellt sich als ein Statistiker vor, dessen Namen nichts zur Sache tue; das Gesicht kommt
Ihnen vage bekannt vor. „Pass auf“, erklärt der Statistiker, „wir werfen eine Münze, ich
setze auf Zahl.“ Letzteres überrascht Sie nicht. „Wenn ich gewinne“, fährt der Statistiker fort, „bekomme ich 10 Euro von Dir, wenn Du gewinnst, bekommst Du 11 Euro von
mir. Gutes Spiel, oder?“ Sie einigen sich auf zehn Durchgänge, in denen der Statistiker
jedes Mal eine Münze wirft, fängt und dann die oben liegende Seite prüft. Erster Wurf:
Zahl! Der Statistiker gewinnt. Pech für Sie. Zweiter Wurf: Zahl! Schon wieder 10 Euro
für den Statistiker. Hm. Dritter Wurf: . . . Zahl! Schon wieder. Aber kann ja passieren,
bei einer fairen Münze, oder? Vierter Wurf: Zahl! Langsam regen sich Zweifel bei Ihnen.
Kann das noch mit rechten Dingen zugehen? Ist die Münze fair? Insgesamt gewinnt der
zwielichtige Statistiker acht von zehn Durchgängen. Unter leisem Gelächter des Statistikers (und mit leeren Taschen) machen Sie sich von dannen. Hat er falsch gespielt? Wie
plausibel ist es, bei zehn Würfen acht Treffer zu erhalten, wenn die Münze fair ist? Ist
das ein häufiges, ein typisches Ereignis oder ein seltenes, untypisches Ereignis bei einer
fairen Münze? Wenn es ein einigermaßen häufiges Ereignis sein sollte, dann spricht das
für die Fairness der Münze. Zumindest spricht ein Ereignis, welches von einer Hypothese
als häufig vorausgesagt wird und schließlich eintritt, nicht gegen eine Hypothese.
Zuhause angekommen, werfen Sie wie wild Münzen, immer zehn Mal: Sie wollen ausprobieren, ob acht von zehn Mal Zahl (d. h. eine Trefferquote von 80 %) ein übliches oder
unübliches Ereignis ist, wenn die Münze fair ist.6 Die ganze Nacht werfen Sie Münzen
und führen Strichlisten. Am nächsten Morgen sind Sie übernächtigt, aber Sie haben den
Versuch 1000 Mal durchgeführt. Ihre Strichliste, etwas aufgehübscht, ist in Abb. 16.3 dargestellt. Hm, acht Treffer kamen in ca. 4 % aller Fälle vor; das ist selten (ungewöhnlich).
Sie beschließen, dass alles, was seltener als 5 % ist, zu selten ist. Zu selten, als dass Sie
noch an die Fairness der Münze glauben würden. So ein Ereignis nennt man statistisch
signifikant. Natürlich kann ein seltenes Ereignis (wie zehn Treffer bei zehn Versuchen)
vorkommen, wenn die Münze fair ist (d. h. wenn die getestete Hypothese wahr ist), aber
es ist Ihnen eben zu selten. In solchen seltenen Fällen beschließen Sie, nicht an die Hypothese der Fairness zu glauben; es riecht Ihnen zu sehr nach gezinkter Münze. Ereignisse,
die noch seltener sind als acht Treffer bei zehn Versuchen, sprechen noch stärker gegen
die Fairness-Hypothese. Daher betrachten Sie nicht nur acht Treffer bei zehn Versuchen
als Beleg gegen die Fairness-Hypothese, sondern mindestens acht Treffer bei zehn Ver6
Wir sparen uns hier die Diskussion, ob es faire Münzen gibt (vgl. Jaynes (2003)).
272
16
Inferenzstatistik
p-Wert von q=8/10: 0.06
300
0.25
0.2
200
0.19
n
0.14
0.12
100
0
0
0.01
0.0
0.1
0.04
0.04
0.2
0.01
0.3
0.4
0.5
0.6
0.7
0.8
0.9
Trefferquote q
Bereich der Verwerfung (p<.05) beginnt bei q = 0.8
Abb. 16.3 Simulation von Würfen einer fairen Münze
suchen – das sind acht, neun oder zehn Treffer. Diese „5 %-Hürde“ bezeichnet man als
Signifikanzniveau Ë›. Die Wahl von 5 % ist willkürlich; es ist eine Konvention. Das hat
zumindest den Vorteil, dass man nicht argumentieren muss, warum man eine bestimmte
Grenze gewählt hat.7
In Ihrer Versuchsreihe wurden bei 48 der 1000 Versuche acht oder neun Treffer erzielt
(zehn Treffer kamen nicht vor). Sie beschließen auf Basis dieser Überlegung: „Statistiker sind alle gleich. Acht oder mehr Treffer sind selten bei einer fairen Münze. Daher
glaube ich nicht, dass er eine faire Münze geworfen hat. Die Münze war gezinkt!“ Damit
legen Sie sich zur Ruhe; Sie träumen von Ihrer Versuchsreihe. Details zur Simulation von
Verteilungen finden sich in Kap. 17.
Als Sie wieder aufwachen, überlegen Sie: Ab wie vielen Treffern ist ein Ereignis selten? Sie zählen in Ihrer Strichliste nach: Die 5 % unwahrscheinlichsten (also höchsten)
Trefferquoten beginnen bei sieben von zehn möglichen Treffern, also 70 % (die gestrichelte Linie in Abb. 16.4). Aber was wäre, wenn Sie nicht zehn Durchgänge gespielt
hätten, sondern nur fünf? Oder ganze 30 Münzwürfe? Aufgrund einer SehnenscheidenEntzündung (vermutlich zu viel mit Excel gearbeitet) wollen Sie ungern diese Versuche
noch einmal von Hand durchführen. Zum Glück übernimmt der Computer (R) diese Arbeit ohne Murren.
Die Ergebnisse sind in Abb. 16.4 dargestellt. Man sieht die Verteilung der (simulierten)
Stichproben, die Stichprobenverteilung unter der getesteten Hypothese (in diesem Fall:
Die Münze ist fair, d. h. Trefferquote beträgt 50 %). Bei der kleineren Stichprobengröße
(n D 5) ist die Verteilung breiter (größere Streuung) – eine Trefferquote von .8 ist nicht
selten. Bei der größeren Stichprobengröße (n D 30) ist die Verteilung schmaler (geringere
Streuung) – eine Trefferquote von .8 ist erkennbar seltener. Der „Grenzwert“, also der
Beginn des Bereichs der Verwerfung der Fairness-Hypothese ist offenbar abhängig von
der Stichprobengröße. Genauer gesagt ist der Bereich der Verwerfung abhängig von der
7
Die 5 %-Grenze sieht sich auch Kritik ausgesetzt (vgl. Benjamin et al. (2018)).
16.2 Der p-Wert
273
5
30
400
n
300
200
100
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
Trefferquote
Abb. 16.4 Simulation des Münzversuchs mit n D 5 und n D 30
Streuung der Stichprobenverteilung; diese Streuung bezeichnet man als Standardfehler.
Mehr zur Berechnung des Standardfehlers findet sich in Kap. 17.
Der Anteil der simulierten Stichproben, die mindestens so extrem sind wie das echte
(empirische) Stichprobenergebnis, summiert sich zum p-Wert. Die Streuung der
Stichprobenverteilung ist maßgeblich zur Berechnung des p-Werts, daher hat sie
einen eigenen Namen: Standardfehler.
Die getestete Hypothese – hier: „Die Münze ist fair“ bzw. Trefferquote q D :5 – nennt
man die Nullhypothese, kurz H0 . Die komplementäre Hypothese „Die Trefferquote ist
größer als 50 %“ (q > :5) nennt man auch die Alternativhypothese, kurz HA oder HA .
Eine Alternativhypothese kann gerichtet sein wie im Münzbeispiel (q > :5) oder ungerichtet. Ein Beispiel für eine ungerichtete Hypothese ist: „Die Münze ist nicht fair;
sowohl kleine als auch große Trefferquoten erscheinen mir plausibel“, kurz q ¤ :5. Bevor
man weiß, ob der Statistiker auf Kopf oder Zahl setzt, sind beide Richtungen (hohe bzw.
geringe Trefferquote für „Zahl“) denkbar als Alternativhypothese. Egal ob die Alternativhypothese gerichtet oder ungerichtet ist, ein gewähltes Signifikanzniveau (meist 5 %)
bleibt davon unbescholten. Im zweiseitigen Fall werden die 5 % hälftig aufgeteilt. Sowohl
sehr kleine Trefferquoten als auch sehr große Trefferquoten führen dann zu einem signifikanten Ergebnis. Genauer: Gehört das empirische Ergebnis zu den 2.5 % kleinsten oder
2.5 % größten Ergebnissen der Stichprobenverteilung, so ist das Ergebnis signifikant. Bei
der Berechnung des p-Werts im ungerichteten Fall wird die Zahl der extrem kleinen und
großen Stichproben aufaddiert – also beide Randbereiche der Verteilung (vgl. Abb. 16.5,
rechts). Abb. 16.5 stellt im linken und mittleren Teil eine gerichtete Hypothese dar in den
Varianten „größer als“ (q > :5) und „kleiner als“ (q > :5). Im rechten Teil der Abbildung
ist eine ungerichtete Hypothese dargestellt. In allen drei Fällen ist das Signifikanzniveau
(insgesamt) 5 %.
274
16
A
B
Gerichtet, "kleiner als"
Inferenzstatistik
C
Gerichtet, "größer als"
Ungerichtet
Abb. 16.5 Ungerichtete vs. gerichtete Hypothesen
16.2.3 Von Männern und Päpsten – Was der p-Wert nicht sagt
Der p-Wert ist weit verbreitet. Er bietet die Möglichkeit, relativ objektiv zu quantifizieren,
wie stark ein Kennwert, der mindestens so extrem wie der aktuell vorliegende ist, gegen
eine Hypothese H0 spricht. Allerdings hat der p-Wert seine Probleme. Vor allem: Er wird
missverstanden. Jetzt kann man sagen, dass es dem p-Wert nicht anzulasten sei, dass
einige ihn missverstehen. Auf der anderen Seite finde ich, dass sich Technologien dem
Nutzer anpassen sollten (so weit wie möglich) und nicht umgekehrt. Viele Menschen –
auch Statistik-Dozenten – haben Probleme mit dieser Definition (Gigerenzer 2004). Das
ist nicht deren Schuld: Die Definition ist kompliziert. Viele denken, der p-Wert sage das,
was die meisten Praktiker interessiert: die Wahrscheinlichkeit der (getesteten) Hypothese
H , gegeben der Tatsache, dass eine Teststatistik T bestimmter Höhe vorliegt: P .H jT /.
Leider ist das nicht die Definition des p-Werts. Der p-Wert gibt die Wahrscheinlichkeit
einer Teststatistik T an, gegeben dass eine Hypothese H der Fall ist: P .T jH /. Also:
P .T jH / ¤ P .H jT /
Formeln haben die merkwürdige Angewohnheit, vor dem inneren Auge zu verschwimmen; Bilder sind für viele Menschen klarer, scheint’s. Übersetzen wir die Formel in folgenden Satz: Die Wahrscheinlichkeit, ein Mann zu sein, wenn man Papst ist, ist ungleich
der Wahrscheinlichkeit, Papst zu sein, wenn man Mann ist. Kürzer:
P .M jP / ¤ P .P jM /
Abb. 16.6 zeigt den Anteil der Männer an den Päpsten (sehr hoch). Und es zeigt den Anteil
der Päpste von allen Männern (sehr gering). Dabei können wir Anteil mit Wahrscheinlichkeit übersetzen. Kurz: Die beiden Anteile (Wahrscheinlichkeiten) sind nicht gleich. Die
Wahrscheinlichkeit, ein Mann zu sein, wenn man Papst ist, ist vermutlich ganz anders als
die Wahrscheinlichkeit, Papst zu sein, wenn man ein Mann ist.
16.2 Der p-Wert
275
Abb. 16.6 Mann und Papst zu
sein, ist nicht das Gleiche
Die amerikanische Gesellschaft für Statistik8 hat vor einiger Zeit einige Richtlinien zur
Interpretation von p-Werten veröffentlicht, vor dem Hintergrund der regen oder erregten
Diskussion (Wasserstein und Lazar 2016). Einige Kernaussagen dieses Artikels lauten:
1. p-Werte zeigen an, wie inkompatibel Daten mit einer getesteten statistischen Hypothese sind.
2. p-Werte messen nicht die Wahrscheinlichkeit einer getesteten Hypothese und auch
nicht die Wahrscheinlichkeit, dass die Daten durch puren Zufall zu erklären sind.
3. Wissenschaftliche Schlüsse oder Entscheidungen in der Praxis (Geschäftswelt, Politik)
sollten nicht ausschließlich auf der Basis des p-Werts getroffen werden.
4. Angemessene statistische Inferenz verlangt stets lückenlose Berichte und Transparenz
in der Analyse.
5. Ein p-Wert bzw. das Vorliegen von statistischer Signifikanz ist kein Maß für die Stärke
eines Effekts oder die Bedeutung (Relevanz) eines Ergebnisses.
6. Der p-Wert für sich genommen ist kein gutes Maß für die Evidenz eines Modells oder
einer Hypothese.
16.2.4 Der p-Wert ist eine Funktion der Stichprobengröße
Der p-Wert ist für weitere Eigenschaften kritisiert worden (Briggs 2016; Wagenmakers
2007); z. B., dass die Konvention der „5 %-Hürde“ einen zu schwachen Test für die Hypothese bedeute (Benjamin et al. 2017). Letzterer Kritikpunkt ist nicht dem p-Wert anzulasten, denn dieses Kriterium ist beliebig, könnte konservativer gesetzt werden und jegliche
mechanisierte Entscheidungsmethode kann ausgenutzt werden. Ähnliches kann man zum
Thema „p-Hacking“ argumentieren (Head et al. 2015; Wicherts et al. 2016): Andere statistische Verfahren können auch gehackt werden. „Hacken“ soll hier sagen, dass man –
Kreativität und Wille vorausgesetzt – immer Wege finden kann, um einen Kennwert in die
gewünschte Richtung zu lenken.
Ein anderer Anklagepunkt lautet, dass der p-Wert nicht nur eine Funktion der Effektgröße sei, sondern auch der Stichprobengröße. Sprich: Bei großen Stichproben wird jede
Hypothese signifikant. Das ist richtig. Das schränkt die praktische Nützlichkeit ein (vgl.
Abb. 16.7). Egal wie klein die Effektstärke ist, es existiert eine Stichprobengröße, die
diesen Effekt beliebig signifikant werden lässt.
8
ASA: http://www.amstat.org/.
276
Abb. 16.7 Zwei Haupteinflüsse auf den p-Wert
16
Inferenzstatistik
Effektstärke
p-Wert
Stichprobengröße
Die Verteidigung argumentiert hier, dass das „kein Bug, sondern ein Feature“ sei: Wenn
man z. B. die Hypothese prüft, dass der Gewichtsunterschied zwischen Männern und Frauen 0.000000000 kg sei, und man findet einen Unterschied von 0.000000123 kg, ist die
getestete Hypothese falsch. Punkt. Der p-Wert gibt demnach das korrekte Ergebnis. Meiner Ansicht nach ist die Antwort zwar richtig, geht aber an den Anforderungen der Praxis
vorbei.
16.2.5 Mythen zum p-Wert
Falsche Lehrmeinungen sterben erst aus, wenn die beteiligten Professoren in Rente gehen,
heißt es. Jedenfalls hält sich eine Reihe von Mythen zum p-Wert hartnäckig; sie sind meist
falsch.9
1. Wenn der p-Wert kleiner als 5 % ist, dann ist meine Hypothese (H1) sicher richtig.
Falsch. Richtig ist: „Wenn der p-Wert kleines ist als 5 % (oder allgemeiner: kleiner
als Ë›), dann sind die Daten (oder noch extremere) unwahrscheinlich, vorausgesetzt, die
H0 gilt.“
2. Wenn der p-Wert kleiner als 5 % ist, dann ist meine Hypothese (H1) höchstwahrscheinlich richtig.
Falsch. Richtig ist: Wenn der p-Wert kleiner ist als Ë›, dann sind die Daten unwahrscheinlich, falls die H0 gilt. Ansonsten (wenn H0 nicht gilt) können die Daten sehr wahrscheinlich sein.
3. Wenn der p-Wert kleiner als 5 % ist, dann ist die Wahrscheinlichkeit der H0 kleiner
als 5 %.
Falsch. Der p-Wert gibt nicht die Wahrscheinlichkeit einer Hypothese an. Richtig ist:
Ist der p-Wert kleiner als 5 %, dann sind meine Daten (oder noch extremere) unwahrscheinlich, wenn die H0 gilt.
4. Wenn der p-Wert kleiner als 5 % ist, dann habe ich die Ursache eines Phänomens
gefunden.
Falsch. Richtig ist: Keine Statistik kann für sich genommen eine Ursache erkennen
(Briggs 2016; aber s. Pearl (2009)). Bestenfalls kann man sagen: hat man alle konkur9
S. fortunes::fortune(264).
16.3
Wann welcher Inferenztest?
277
rierenden Ursachen ausgeschlossen und sprechen die Daten für die Ursache und sind die
Daten eine plausible Erklärung, so erscheint es der beste Schluss, anzunehmen, dass man
eine Ursache gefunden hat – im Rahmen des Geltungsbereichs einer Studie.
5. Wenn der p-Wert größer als 5 % ist, dann ist das ein Beleg für die H0 .
Falsch. Richtig ist: Ein großer p-Wert ist ein Beleg, dass die Daten plausibel unter
der H0 sind. Wenn es draußen regnet, ist es plausibel, dass es Herbst ist. Das heißt aber
nicht, dass andere Hypothesen nicht auch plausibel sind. Ein großer p-Wert ist also Abwesenheit von klarer Evidenz – nicht Anwesenheit von klarer Evidenz zugunsten der H0 .
Schöner ausgedrückt: „‚No evidence of effect‘ is not the same as ‚evidence of no effect‘“
(Ranganathan et al. 2015). Für die Wissenschaft ist das insofern ein großes Problem, als
sich Zeitschriften weigern, nicht-signifikante Studien aufzunehmen: „Das ist eine unklare
Befundlage. Kein Mehrwert.“, so die Haltung vieler Editoren. Das führt dazu, dass die
wissenschaftliche Literatur einer großen Verzerrung unterworfen ist.
Wenn der p-Wert größer als 5 % ist, dann kann ich meine Studie nicht veröffentlichen.
Zu oft richtig; kein Mythos. Leider wird der p-Wert häufig als zentrales Kriterium zur
Aussagekraft einer Studie (bzw. ihrer statistischen Analyse) herangezogen. Wichtiger wäre zu prüfen, wie „gut“ das Modell ist – wie präzise sind die Vorhersagen? Wie theoretisch
befriedigend ist das Modell?
16.3 Wann welcher Inferenztest?
Ein p-Wert sollte nicht das zentrale Kriterium zur Entscheidung über eine Hypothese sein. Inferenztests sollten mit anderen Kennwerten wie Effektstärken und
Vorhersagegüte angereichert werden (oder von ihnen ersetzt werden), um über die
Annahme oder Ablehnung einer Hypothese zu entscheiden.
In der Praxis ist es eine häufige Frage, wann man welchen statistischen Test verwenden soll. Bei Eid et al. (2010) findet man eine umfangreiche Tabelle dazu; auch online
wird man schnell fündig (z. B. bei der Methodenberatung der Uni Zürich10 oder beim
Ärzteblatt11 , Prel et al. (2010)). Die folgende Auflistung gibt einen kurzen Überblick zu
gebräuchlichen Verfahren. Entscheidungskriterium ist hier (etwas simplifiziert) das Skalenniveau der Variablen (unterschieden in Input- und Outputvariablen).
1.
2.
3.
2 nominale Variablen: 2 -Test – chisq.test()
Output: 1 metrisch, Input: 1 dichotom: t-Test – t.test()
Output: 1 metrisch, 1 oder mehr nominal: Varianzanalyse – aov()
10
http://www.methodenberatung.uzh.ch/de/datenanalyse.html.
https://www.aerzteblatt.de/archiv/74880/Auswahl-statistischer-Testverfahren.
11
278
4.
5.
6.
16
Inferenzstatistik
2 metrische Variablen: Korrelation – cor.test()
Output: 1 metrisch, Input: 1 oder mehr nominal oder metrisch: Regression – lm()
Output: 1 ordinal, Input: 1 dichotom: Wilcoxon (Mann-Whitney-U-Test) – wilcox.
test()
7.
8.
9.
Output: 1 ordinal, Input: 1 nominal: Kruskal-Wallis-Test – kruskal.test()
1 metrisch (Test auf Normalverteilung): Shapiro-Wilk-Test – shapiro.test()
Output: 1 dichotom, Input 1 oder mehr nominal oder metrisch: logistische (klassifikatorische) Regression: glm(..., family = "binomial")
10. 2 ordinal: Spearmans Rangkorrelation – cor.test(x, y, method =
"spearman")
16.4 Beispiele für häufige Inferenztests
Die H0 , die bei den folgenden Testverfahren angenommen wird, ist, dass es keinen Zusammenhang zwischen den getesteten Variablen gibt. Anders ausgedrückt: Es gibt keinen
Effekt in dem getesteten Modell; die Input-Variable hat keinen Einfluss auf die OutputVariable. Das Signifikanzniveau wird auf Ë› D 5 % festgelegt. Das gängige Vorgehen ist,
dass man die H0 testet und den p-Wert berechnet. Ist der p-Wert kleiner als das Signifikanzniveau; so verwirft man die H0 und nimmt H1 an. Im Folgenden sind auch einige
Beispiele dargestellt, wie man die Ergebnisse solcher Tests berichten kann (vgl. American
Psychological Association (2009)).
16.4.1 2 -Test
Betrachten wir den Datensatz extra. Ob es wohl einen Zusammenhang gibt zwischen
Geschlecht und extremem Alkoholgenuss? Definieren wir – auf Basis einer Konsultation
eines Experten – „extrem“ durch mehr als zehn Kater in den letzten zwölf Monaten.12
Damit ist extremer Alkoholgenuss eine qualitative (genauer: dichotome) Variable. mosaic
bietet eine komfortablere Variante des 2 -Tests; dafür muss das Paket natürlich geladen
sein.
Forschungsfrage: Gibt es einen Zusammenhang zwischen Geschlecht und extremem
Alkoholgenuss?
H0 : Es gibt keinen Zusammenhang zwischen Geschlecht und extremem Alkoholgenuss
H1 : Es gibt einen Zusammenhang zwischen Geschlecht und extremem Alkoholgenuss.
Synonym wäre zu fragen, ob sich die Stufen von Geschlecht (die Geschlechter, also
Mann und Frau) hinsichtlich Saufen extremem Alkoholgenuss unterscheiden.13 Wir nut12
Damit haben wir die Variable Kater dichotomisiert; das ist nachteilig, s. fortunes::
fortune(279).
13
Ohne mosaic: chisq.test(x = extra$sex, extra$viel_saeufer).
16.4
Beispiele für häufige Inferenztests
279
zen xchisq.test() aus mosaic. Das Testergebnis spricht gegen die H0 und damit
zugunsten der H1 :
extra$viel_saeufer <- extra$n_hangover > 10
xchisq.test(sex ~ viel_saeufer, data = extra)
#>
#> Pearson's Chi-squared test
#>
#> data: tally(x, data = data)
#> X-squared = 200, df = 4, p-value <2e-16
#>
#>
70
445
14
#> ( 99.27) (413.08) ( 16.65)
#> [ 8.63] [ 2.47] [ 0.42]
#> <-2.94> < 1.57> <-0.65>
#>
#>
84
197
5
#> ( 53.67) (223.33) ( 9.00)
#> [ 17.14] [ 3.10] [ 1.78]
#> < 4.14> <-1.76> <-1.33>
#>
#>
1
3
7
#> ( 2.06) ( 8.59) ( 0.35)
#> [ 0.55] [ 3.64] [127.86]
#> <-0.74> <-1.91> <11.31>
#>
#> key:
#> observed
#> (expected)
#> [contribution to X-squared]
#> <Pearson residual>
Es fand sich ein statistisch signifikanter Effekt zwischen Geschlecht und extremem Alkoholgenuss, 2 .4; n D 826/ D 166; p < :001. Neben dem 2 -Wert und dem p-Wert
sind hier noch die Freiheitsgrade (4) und die Stichprobengröße (826) berichtet. Ihre Daten
liegen in aggregierter Form vor? Etwa so:
saeufer_tab <- table(x = extra$sex, extra$viel_saeufer)
Dann können Sie dieses Objekt des Typs table direkt an den 2 -Test-Befehl übergeben:
xchisq.test(saeufer_tab)
Noch ein paar Hinweise zur Ausgabe: Für jede Zelle von sauefer_tab werden vier
Werte berichtet: 1) Die beobachtete Häufigkeit („observed“) der Zelle (z. B. gibt es 70
weibliche Säufer), 2) die erwartete Häufigkeit („expected“), wenn die H0 gilt (es also
keinen Zusammenhang zwischen Geschlecht und extremem Alkoholgenuss gibt), 3) der
2 -Beitrag der Zelle zum gesamten 2 -Wert („contribution to X-squared“) und 4) das
280
16
Inferenzstatistik
Residuum rj der Zelle j („Pearson residual“) definiert als Funktion zwischen erwarteter
Oj Ej
Häufigkeit E der Zelle j und beobachteter: rj D p
; das Residuum gibt damit einen
Ej
Hinweis, wie stark die Zelle vom erwarteten Wert (laut H0 ) abweicht.
16.4.2 t-Test
Forschungsfrage: Sind Männer im Schnitt extrovertierter als Frauen?
H0 : Es gibt keinen Unterschied in der mittleren Extraversion zwischen Männern und
Frauen.
H1 : Der mittlere Extraversionswert von Männern ist größer als der von Frauen.
t.test(extra_mean ~ sex, data = extra, alternative = "less")
#>
#> Welch Two Sample t-test
#>
#> data: extra_mean by sex
#> t = 1, df = 500, p-value = 0.9
#> alternative hypothesis: true difference in means is less than 0
#> 95 percent confidence interval:
#>
-Inf 0.104
#> sample estimates:
#> mean in group Frau mean in group Mann
#>
2.91
2.86
Es fand sich kein signifikanter Unterschied in Extraversion zwischen den Geschlechtern,
t.507:03/ D 1:35, p D :91. Die H0 kann nicht verworfen werden.
Hinweise:
Der t-Test testet im Standard ungerichtet.
Wird eine Gruppierungsvariable (wie Geschlecht) vom Typ factor angegeben, so
muss diese zwei Faktorstufen haben. Allein durch Filtern wird man zusätzliche Faktorstufen nicht los (im Gegensatz zu Variablen vom Typ character, Text). Man muss
die Faktorvariable neu als Faktorvariable definieren. Dann werden nur die existierenden Werte als Faktorstufen herangezogen.
Bei gerichteten Hypothesen sieht t.test zwei Möglichkeiten vor: less und greater.
Woher weiß man, welches von beiden man nehmen muss? Die Antwort lautet: Bei
Textvariablen sind die Stufen alphabetisch geordnet. R sagt also sozusagen: Frau ?
Mann. Und für ? müssen wir das richtige Ungleichheitszeichen einsetzen (< oder >), so
dass es unserer Hypothese entspricht. In diesem Fall glauben wir laut H1 , dass Frauen
weniger (bzw. Männer mehr) extrovertiert sind, also haben wir less gewählt.
Liegt der Datensatz nicht tidy vor, also gibt es z. B. eine Spalte mit Extraversionswerten für Männer und eine für Frauen, so darf man nicht die Formelsyntax (Kringel,
Tilde „~“) verwenden, sondern benennt die Spalten mit den Argumenten x und y: z. B.
t.test(x = df$extra_maenner, y = df$extra_frauen).
16.4
Beispiele für häufige Inferenztests
281
16.4.3 Einfache Varianzanalyse
Forschungsfrage: Unterscheiden sich Menschen mit unterschiedlich viel Kundenkontakt
in ihrer Extraversion?
H0 : Die mittlere Extraversion ist in allen Gruppen, die sich in ihrem Kundenkontakt unterscheiden, gleich.
H1 : Die mittlere Extraversion ist nicht gleich groß in allen Gruppen, die sich in ihrem
Kundenkontakt unterscheiden.
Der Kundenkontakt (clients_freq) wurde mit einer Likertskala gemessen, die mehrere Stufen von „weniger als einmal pro Quartal“ bis „im Schnitt mehrfach pro Tag“ reichte.
Wir gehen nicht davon aus, dass diese Skala Intervallniveau aufweist. Obwohl Ordinalskalenniveau für clients_freq plausibel ist, bleiben wir zu Demonstrationszwecken bei
der Varianzanalyse (Varianzanalyse, analysis of varience, AOV, ANOVA), die nur nominales Niveau ausschöpft. Beachten Sie, dass die entsprechende Variable clients_freq
als Ganzzahl in R definiert ist, obwohl die Abstände nicht sicher gleich sind. Es ist zwar
erlaubt, den Stufen einer nominalen Variablen Zahlen zuzuordnen, aber wir sollten nicht
vergessen, dass die Zahlen „keine echten Zahlen“ sind, also nicht metrisches Niveau aufweisen (zumindest ist das nicht sicher).14
So sieht die Umsetzung in R aus:
aov(extra_mean ~ clients_freq, data = extra) %>% tidy()
#> # A tibble: 2 x 6
#>
term
df
sumsq meansq statistic p.value
#>
<chr>
<dbl>
<dbl> <dbl>
<dbl>
<dbl>
#> 1 clients_freq
1
0.602 0.602
2.91 0.0884
#> 2 Residuals
688 142.
0.207
NA
NA
aov(extra_mean ~ clients_freq, data = extra) %>% glance()
#> # A tibble: 1 x 11
#>
r.squared adj.r.squared sigma statistic p.value
df logLik
AIC
BIC
#> *
<dbl>
<dbl> <dbl>
<dbl>
<dbl> <int> <dbl> <dbl> <dbl>
#> 1
0.00421
0.00277 0.455
2.91 0.0884
2 -434. 875. 889.
#> # ... with 2 more variables: deviance <dbl>, df.residual <int>
tidy() räumt das Ergebnis der ANOVA etwas auf, so dass die Ausgabe in einem Dataframe dargestellt ist, wobei die relevanten Statistiken jeder Input-Variable in einer Zeile
stehen. Folgende Statistiken sind aufgeführt: Freiheitsgrade pro Input-Variable df; Summe und Mittelwert der Quadratsummen zwischen den Faktorstufen sumsq bzw. meansq,
F-Wert statistic und p-Wert p.value. glance() liefert die „Überblickskoeffizienten“ (daher „glance“, engl. „einen Blick werfen“). glance() zeigt folgende Statistiken: R2 und adjustiertes R2 , den F-Wert (statistic), die Streuung (SD) der Residuen
14
Die verschiedenen Stufen einer Variablen kann man sich so ausgeben lassen: extra %>%
distinct(clients_freq).
282
16
Inferenzstatistik
(sigma), den p-Wert und einige Likelihood-Statistiken zum Modellvergleich sowie die
Freiheitsgrade (df) der Residuen df.residual. Die Gruppen unterscheiden sich nicht
statistisch signifikant, F .1; 688/ D 2:91; p D :09, die H0 wird beibehalten; die Größe
des Effekts (R2 ) ist sehr gering. Beim F-Test berichtet man zwei Freiheitsgrade: Die Freiheitsgrade der Input-Variablen (von glance() mit df bezeichnet) und die Freiheitsgrade
der Residuen (df.residual).
Man kann auch mehrere Input-Variablen in das Modell aufnehmen, diese werden dann
mit + voneinander getrennt: aov(extra_mean ~ clients_freq + sex, data =
extra). Möchte man noch zusätzlich den Interaktionseffekt (vgl. Abschn. 18.7) spezifizieren, z. B. zwischen Geschlecht und extremem Alkoholgenuss, so nutzt man den
Operator ::
aov(extra_mean ~ clients_freq + sex + sex:clients_freq, data = extra) %>% tidy()
16.4.4 Korrelationen (nach Pearson) auf Signifikanz prüfen
Forschungsfrage: Sind Extraversion (Mittelwert) und die Anzahl der Facebook-Freunde
korreliert?
H0 : Die Korrelation (nach Pearson) von Extraversion und Anzahl an Facebook-Freunden
beträgt null.
H1 : Die Korrelation (nach Pearson) von Extraversion und Anzahl an Facebook-Freunden
beträgt nicht null.
cor.test(extra$n_facebook_friends, extra$extra_mean)
#>
#> Pearson's product-moment correlation
#>
#> data: x and y
#> t = 1, df = 700, p-value = 0.2
#> alternative hypothesis: true correlation is not equal to 0
#> 95 percent confidence interval:
#> -0.0303 0.1208
#> sample estimates:
#>
cor
#> 0.0455
extra_cor_test <- cor.test(extra$n_facebook_friends, extra$extra_mean)
str(extra_cor_test)
#> List of 9
#> $ statistic : Named num 1.18
#>
..- attr(*, "names")= chr "t"
#> $ parameter : Named int 669
#>
..- attr(*, "names")= chr "df"
16.4
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
Beispiele für häufige Inferenztests
283
$ p.value
: num 0.239
$ estimate
: Named num 0.0455
..- attr(*, "names")= chr "cor"
$ null.value : Named num 0
..- attr(*, "names")= chr "correlation"
$ alternative: chr "two.sided"
$ method
: chr "Pearson's product-moment correlation"
$ data.name : chr "x and y"
$ conf.int
: num [1:2] -0.0303 0.1208
..- attr(*, "conf.level")= num 0.95
- attr(*, "class")= chr "htest"
Extraversion und Anzahl der Facebook-Freunde sind laut diesen Daten nicht signifikant
korreliert, r.669/ D 0:05, p D 0:239. Da der p-Wert nicht kleiner ist als das Signifikanzniveau, wird die H0 beibehalten: Auf Basis des Testergebnisses kann man nicht
ausschließen, dass es keinen Zusammenhang gibt.15 Man kann auch – wie beim t-Test –
gerichtet testen, vgl. ?cor.test.
16.4.5 Regression
Forschungsfrage: Wie groß ist der Einfluss der Anzahl von Parties auf die Anzahl der
Kater?
H0 : Die Anzahl der Parties hat keinen linearen Einfluss auf die Anzahl an Katern.
H1 : Die Anzahl der Parties hat einen linearen Einfluss auf die Anzahl an Katern.
Dabei ist Einfluss nicht kausal gemeint; vielmehr ist gemeint, wie sehr sich die Anzahl der
Kater in Abhängigkeit von der Anzahl der besuchten Parties verändert. Die Regression ist
ausführlicher im Kap. 18 dargestellt.
lm(n_hangover ~ n_party, data = extra) %>% tidy()
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.159
1.40
0.113 9.10e- 1
#> 2 n_party
0.539
0.0540
9.98 3.62e-22
lm(n_hangover ~ n_party, data = extra) %>% glance()
#> # A tibble: 1 x 11
#>
r.squared adj.r.squared sigma statistic p.value
df logLik
AIC
BIC
#> *
<dbl>
<dbl> <dbl>
<dbl>
<dbl> <int> <dbl> <dbl> <dbl>
#> 1
0.113
0.112 29.2
99.7 3.62e-22
2 -3752. 7510. 7524.
#> # ... with 2 more variables: deviance <dbl>, df.residual <int>
15
Das Paket magrittr stellt die „Dollar-Pfeife“ %$% zur Verfügung, so dass man diesen
Aufruf in gewohnter Pfeifen-Manier schreiben kann: extra %$% cor.test(extra_mean,
n_facebook_friends).
284
16
Inferenzstatistik
Genau wie bei der Ausgabe zur Varianzanalyse liefert glance() bei statistic den
F-Wert und bei sigma die SD der Residualstreuung. Hingegen meint statistic in der
Ausgabe von tidy() den t-Wert des jeweiligen Prädiktors zur H0 . estimate ist die
geschätzte Steigung der Regressionsgeraden in der Population. Anstelle von tidy() und
glance() kann man auch summary() verwenden. Die Anzahl der besuchten Parties hat
einen signifikanten Einfluss auf die Anzahl der Kater: b D 0:16; SE D 1:40; p < :001;
der Anteil der erklärten Varianz (adj. R2 ) lag bei .11, F .1; 781/ D 100.
Sehr kleine oder sehr große Zahlen gibt R in der Exponentialschreibweise an, z. B.
3.615622e-22. Möchte man in die Dezimalschreibweise konvertieren, so hilft
dieser Befehl: format(3.615622e-22, scientific = FALSE). Faustregel:
Wenn eine 3 oder eine größere Zahl im (negativen) Exponenten steht, so ist das
Ergebnis statistisch signifikant; z. B. 2e-03 D 0:002.
16.4.6 Wilcoxon-Test
Die Forschungsfrage und die daraus abgeleiteten Hypothesen sind identisch zum t-Test.
Nur nehmen wir hier nicht an, dass Extraversion metrisch ist, sondern begnügen uns mit
der schwächeren Annahme eines ordinalen Niveaus.
wilcox.test(extra_mean ~ sex, data = extra)
#>
#> Wilcoxon rank sum test with continuity correction
#>
#> data: extra_mean by sex
#> W = 80000, p-value = 0.4
#> alternative hypothesis: true location shift is not equal to 0
16.4.7 Kruskal-Wallis-Test
Die Forschungsfrage und die daraus abgeleiteten Hypothesen sind identisch zur Varianzanalyse. Genau wie beim Wilcoxon-Test gehen wir wieder von ordinalem Niveau bei
Extraversion aus.
extra %>%
kruskal.test(extra_mean ~ factor(sex), data = .)
#>
#> Kruskal-Wallis rank sum test
#>
#> data: extra_mean by factor(sex)
#> Kruskal-Wallis chi-squared = 0.6, df = 1, p-value = 0.4
Zu beachten ist, dass die Inputvariable vom Typ Faktor sein muss.
16.4
Beispiele für häufige Inferenztests
285
16.4.8 Shapiro-Test
Forschungsfrage: Ist Extraversion normalverteilt?
H0 : Die Verteilung von Extraversion gleicht einer Normalverteilung.
H1 : Die Verteilung von Extraversion ist nicht gleich zu einer Normalverteilung.
Wahrscheinlich ist es sinnvoller, diese Frage mit einem Histogramm (oder QQ-Plot) zu
beantworten, weil der Test bei großen Stichproben (zu) schnell signifikant wird. Aber
führen wir den Test einmal durch:
shapiro.test(extra$extra_mean)
#>
#> Shapiro-Wilk normality test
#>
#> data: extra$extra_mean
#> W = 1, p-value = 9e-09
Signifikant. Die Variable ist also nicht (exakt) normalverteilt. Böse Zungen behaupten, die
Normalverteilung sei ungefähr so häufig wie Einhörner (Micceri 1989). Trotzdem setzen
viele Verfahren sie voraus. Glücklicherweise reicht es häufig, wenn eine Variable einigermaßen normalverteilt ist (wobei es hier keine klaren Grenzen gibt).
16.4.9 Logistische Regression
Forschungsfrage: Kann man anhand der Extraversion vorhersagen, ob eine Person Extremtrinker ist?
H0 : Extraversion hat keinen Einfluss auf die Wahrscheinlichkeit, dass eine Person Extremtrinker (wie definiert) ist.
H1 : Extraversion hat einen Einfluss auf die Wahrscheinlichkeit, dass eine Person Extremtrinker (wie definiert) ist.
glm(viel_saeufer ~ extra_mean, data = extra, family = "binomial") %>% tidy()
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
-4.21
0.661
-6.37 1.93e-10
#> 2 extra_mean
0.942
0.218
4.33 1.51e- 5
Der Extraversionswert hat in diesem Modell einen signifikanten Einfluss auf die Wahrscheinlichkeit, dass jemand Extremtrinker ist, b D 0:94; SE D 0:22; p < :001. Die
logistische Regression ist ausführlicher im Kap. 19 dargestellt. Der p-Wert impliziert,
dass die H0 zu verwerfen ist.
286
16
Inferenzstatistik
16.4.10 Spearmans Korrelation
Die Forschungsfrage und die daraus abgeleiteten Hypothesen sind identisch zum Test von
Pearsons Korrelation. Es werden nur Informationen auf Rangniveau verwertet.
cor.test(extra$extra_mean, extra$n_facebook_friends, method = "spearman")
16.5 Alternativen zum p-Wert
16.5.1
Konfidenzintervalle
Was ist ein Kondienzintervall? Einfach, zu einfach gesagt, gibt ein 95 %-Konfidenzintervall an, wie groß der Bereich ist, in dem der gesuchte Parameter (Mittelwert der Größe
des deutschen Mannes) mit 95 % Wahrscheinlichkeit liegt (wenn Ë› D :05). Ein anderes Beispiel: Wir ziehen eine Stichprobe erwachsener deutscher Männer und finden, dass
der Mittelwert bei 183 cm liegt – in der Stichprobe. Dieser Wert ist ein guter Schätzer
für den echten Mittelwert des deutschen Mannes (vorausgesetzt, die Stichprobe ist gut):
Der Mittelwert der Stichprobe ist der Punktschätzer für den Wert in der Population (den
Parameter). Was ist unser Schätzbereich (Konfidenz- oder Schätzintervall) der mittleren
Körpergröße des deutschen Mannes? Praktischerweise kann man sich ein Konfidenzintervall so ausgeben lassen:
wo_men %>%
filter(sex == "man") %>%
select(height) %>%
drop_na() %>%
pull(height) %>%
t.test(.)
#>
#> One Sample t-test
#>
#> data: .
#> t = 80, df = 20, p-value <2e-16
#> alternative hypothesis: true mean is not equal to 0
#> 95 percent confidence interval:
#> 178 188
#> sample estimates:
#> mean of x
#>
183
Der geschätzt Wert (die Intervallschätzung) der mittleren Größe des deutschen Mannes
liegt demnach zwischen 178 und 188 cm; ein Konfidenzintervall ist ein Bereichsschätzer.16
16
Konfidenzintervalle werden meist zweiseitig angegeben; aber auch einseitige sind möglich, vgl.
Eid et al. (2010).
16.5 Alternativen zum p-Wert
287
Es ist verführerisch zu sagen, dass „der mittlere deutsche Mann mit 95 % Wahrscheinlichkeit“ zwischen 178 und 188 cm groß ist.17 Aber so einfach ist es nicht: Würden wir mehrere Stichproben ziehen, würde sich der Schätzbereich jedes Mal ändern, weil immer andere
Personen in die Stichprobe kommen. Der Parameterwert (der „wahre“ Wert) würde sich
natürlich nicht ändern. „Der“ deutsche Mann hat eine fixe Größe, egal wie viele Stichproben Sie ziehen. Für unser Konfidenzintervall gilt: Entweder es enthält den Parameterwert
oder nicht. Zufällig an diesem Verfahren ist nicht der Parameterwert, der ist fix, sondern
das Verfahren. Zöge man viele Stichproben, so würden die Schätzbereiche variieren. Ein
95 %-Konfidenzintervall ist daher definiert als der Bereich, der bei unendlichen vielen
Stichproben („auf Dauer“) in 95 % der Intervalle einen „Treffer“ erzielt, also den gesuchten Wert enthält (Neyman 1937). Eine andere Definition des 95 %-Konfidenzintervalls
lautet, dass es alle Werte beinhaltet, bei denen der zugehörige Inferenztest nicht signifikant würde; vgl. Kruschke (2014). Ein Konfidenzintervall berichtet man unter Angabe der
Konfidenzniveaus (1 Ë›) sowie der unteren und der oberen Grenze.
Das 95 %-Konfidenzintervall ist ein Bereich, so dass bei unendlich häufiger Stichprobenziehung der wahre Wert in 95 % der Intervalle beinhaltet wäre.
Wie man ein Konfidenzintervall missversteht. Das Konfidenzintervall wird oft (miss-)
verstanden als der Bereich, in dem der gesuchte Parameter mit 95 % Wahrscheinlichkeit liegt. Diese Interpretation ist in vielen Situationen falsch („Konfidenzfehler“). Es ist
in der sog. Frequentistischen Theorie der Wahrscheinlichkeit gar nicht möglich, für ein
einzelnes Ereignis eine Wahrscheinlichkeit anzugeben (Clopper und Pearson 1934; Neyman und Pearson 1933). Wenn ich eine Münze hochwerfe und sie auffange, wie groß
ist die Wahrscheinlichkeit, dass sie auf Kopf gelandet ist? 50 %? Falsch, sagen Frequentisten à la Neyman und Pearson. Entweder ist die Münze auf Kopf gelandet, dann kann
man höchstens sagen: p.K/ D 1, oder auf Zahl, dann entsprechend p.Z/ D 1. Eine
Wahrscheinlichkeit ist nach diesem Verständnis nur sinnvoll, wenn man den Versuch (unendlich) oft wiederholt. Die relative Häufigkeit von Kopf ist dann die Wahrscheinlichkeit
dieses Ereignisses. Genauso gilt für ein Konfidenzintervall: Entweder liegt der gesuchte
Wert (der Parameter) in diesem Intervall (p D 1) oder nicht (p D 0). Es ist daher im
Frequentismus nicht sinnvoll, die Wahrscheinlichkeit anzugeben, mit der der Parameter in
diesem Intervall liegt. Dazu Neyman (1937, S. 347):
Consider now the case when a sample is already drawn, and the calculations have given
[untere Grenze: 1, obere Grenze: 2]. Can we say that in this particular case the probability
of the true value [innerhalb dieser Grenzen] is equal to Ë›? The answer is obviously in the
negative. The parameter is an unknown constant, and no probability statement concerning its
value may be made.
17
Nach Gelman (2014) (Kommentar um 08:15 pm) wird das Konfidenzintervall in der Praxis immer
so interpretiert.
288
16
Inferenzstatistik
Die meisten Konfidenzintervalle beinhalten hier den wahren Wert
Größe
190
185
180
0
25
50
75
100
id
Horizontale Linie: Wahrer Wert (183cm) der Größe in der Population
Abb. 16.8 Simulation eines Konfidenzintervalls
Wie man ein Konfidenzintervall richtig interpretiert. Würde man viele Stichproben ziehen, so enthielten ca. 95 % von ihnen den wahren Populationswert. Abb. 16.8 verdeutlicht
dies mit 100 Intervallen; diejenigen, die den wahren Parameterwert (183 cm) nicht überdecken, sind durch eine durchgezogene Linie markiert.18 Dabei berechnet sich ein 95 %Konfidenzintervall so: XN Ë™ 1:96 SE, wobei SE den Standardfehler bezeichnet. Unter
der Annahme, dass sich viele Stichproben des Mittelwerts (annähernd) normalverteilen,
schneidet ein z-Wert von 1.96 die äußeren 5 % ab (jeweils 2.5 % an jeder Seite). Der Standardfehler misst die Streuung der Stichproben, also die „Breite“ der Stichprobenverteilung
(vgl. Abb. 16.3).
Natürlich stellt sich jetzt die Frage, welche Bedeutung der letzte Satz für unser einziges Konfidenzintervall hat. Darüber gibt die Breite des Konfidenzintervalls Auskunft:
Breitere Konfidenzintervalle zeigen eine ungenauere Schätzung auf als schmalere Konfidenzintervalle (unter sonst gleichen Umständen). Aber auch hier gilt: Die Theorie der
Konfidenzintervalle versucht, die Fehlerzahl auf lange Sicht zu verringern; Aussagen für
einzelne Intervalle sind kein Gegenstand dieser Theorie.19 Allerdings ist diese Interpretation von Konfidenzintervallen auch kritisiert worden (Morey et al. 2016); vielleicht ist das
Hauptproblem, dass Menschen ein Konfidenzintervall gerne im Sinne des Konfidenzfehlers interpretieren: „Ich bin zu 95 % sicher, dass der Parameter in diesem Bereich liegt.“
Unter bestimmten Umständen – hier nicht näher erläutert – gleicht ein Konfidenzintervall allerdings einem Bayes-Intervall. Bei einem Bayes-Intervall sind Aussagen der Art:
„Mit 95 % Wahrscheinlichkeit liegt der Wert des Parameters in diesem Bereich“ erlaubt
– allerdings um den Preis, dass man weitere Annahmen treffen muss. Eine tiefere BayesStatistik ist nicht Gegenstand dieses Buches (aber vgl. Abschn. 16.5.4), obwohl lohnend;
bei McElreath (2015) findet sich eine gute Einführung.
18
Hier findet sich eine schöne interaktive Visualisierung zum Konfidenzintervall: http://
rpsychologist.com/d3/CI/.
19
https://imgflip.com/i/22waqa.
16.5 Alternativen zum p-Wert
16.5.2
289
Effektstärke
Eine weitere Alternative zu p-Werten sind Maße der Effektstärke (J. Cohen 1992). Effektstärkemaße geben z. B. an, wie sehr sich zwei Parameter unterscheiden: „Deutsche
Männer sind im Schnitt 13 cm größer als Frauen“ (Wikipedia 2017). Oder: „In Deutschland ist die Korrelation von Gewicht und Größe um 0.12 Punkte höher als in den USA“
(frei erfunden). Im Gegensatz zu p-Werten wird meist keine Art von Wahrscheinlichkeitsaussage angestrebt, sondern die Größe von Parameter(unterschieden) quantifiziert.
Effektstärken sind, im Gegensatz zum p-Wert, nicht abhängig von der Stichprobengröße.
Man kann Effektstärken in nicht-standardisierte (wie Unterschiede in der Körpergröße)
oder standardisierte (wie Unterschiede in der Korrelation) einteilen.
Nicht-standardisierte Effektstärken haben den Vorteil der Anschaulichkeit. Standardisierte Effektgrößen sind meist besser vergleichbar, aber weniger anschaulich. Bei Variablen mit weniger anschaulichen Metriken (wie psychologische Variablen und Umfragen)
ist ein standardisiertes Maß häufig nützlicher. Anschauliche Variablen sind oft mit unstandardisierten Effektstärken adäquat dargestellt. Variablen mit wenig anschaulichen Metriken profitieren von standardisierten Effektstärkemaßen.
Um zwei Mittelwerte zu vergleichen, ist Cohens d gebräuchlich. Es gibt den Unterschied des Mittelwerts standardisiert an der Standardabweichung an (J. Cohen 1988). Das
ist oft sinnvoll, denn 5 C Preisunterschied können viel oder weniger sein: Bei Eiskugeln
wäre der Unterschied enorm (die Streuung ist viel weniger als 5 C), bei Sportwagen wäre
der Unterschied gering (die Streuung ist viel höher als 5 C).
16.5.2.1 Typische Effektstärkemaße
Einige typische Effektstärkemaße sind im Folgenden aufgelistet (vgl. Eid et al. (2010);
J. Cohen (1992)). Eine einfache Faustregel ist, dass man für einen bestimmten Test meist
ein „zugehöriges“ Effektstärkemaß angibt.
d (Cohens d) wird zur Bemessung des Unterschieds der Überlappung zweier Verteilungen verwendet, z. B. um die Effektstärke eines t-Werts zu quantifizieren. d berechnet
2
.
sich im einfachsten Fall als: d D 1sd
r Pearsons R ist ein Klassiker, um die Stärke des linearen Zusammenhangs zweier
metrischer Größen zu quantifizieren. r berechnet sich als: r D mw.zx zy /, wobei mw
für den Mittelwert steht und z für einen z-Wert.
R2 , 2 sind Maße für den Anteil aufgeklärter Varianz; sie finden in der Varianzanalyse
F
,
oder der Regressionsanalyse Verwendung. R2 wird u. a. so berechnet: R2 D QS
QST
wobei QS für die Quadratsummen steht und QSF für die Quadratsumme, die auf den
Faktor (unabhängige Variable) zurückgeht, und QST für die Gesamt-Quadratsumme.
f 2 ist ein Maß, dass aus der erklärten Varianz abgeleitet ist. Es gibt das Verhältnis
von erklärter zu nicht-erklärter Varianz wieder (auch signal-noise-ratio genannt). Es
R2
berechnet sich als f 2 D 1R
2.
290
Tab. 16.1 Überblick über
gängige Effektstärkemaße
16
Name
Cohens d
r
R2 , 2
f
f2
!
OR
Kleiner Effekt
.2–.5
0.1
0.01
0.1
0.02
0.1
1.5
Mittlerer Effekt
.5–.8
0.3
0.06
0.25
0.15
0.3
3
Inferenzstatistik
Großer Effekt
> :8
0.5
0.14
0.4
0.35
0.5
9
f gibt das Verhältnis der Standardabweichung der Mittelwerte der Population zur
Varianz innerhalb der Gruppen m an; es wird für die einfache Varianzanalyse verwendet: f D m .
! (Cohens Omega) ist ein Maß für die Stärke des Zusammenhangs
zweier nominaler
q
2
Variablen, abgeleitet vom 2 -Test. Es berechnet sich als ! D n .
OR (Odds Ratio) ist ebenfalls ein Maß für die Stärke des Zusammenhangs zweier
nominaler Variablen, allerdings binärer (zweistufiger) Variablen. Es berechnet sich als
c
, wobei c die Chancen für ein Ereignis E angibt (z. B. 9:1). OR kann aus !
OR D 1c
abgleitet werden.
Simple Difference ist eine einfachere Variante zum OR. Ein Beispiel: Bei Prof. Feistersack fallen 90 % der Kandidaten durch die Prüfung. Bei Prof. Schnaggeldi hingegen
nur 50 %. Der Wert für die Simple Difference liegt dann bei 90 % 50 % D 40 %.
Doch, so einfach ist das (Kerby 2014); dieses Maß kann nach Kerby (2014) als Realisation der Phi-Korrelation gelten.
Anteil konformer Paarvergleiche oder Cliffs Delta ist ein Effektstärkemaß für den
Wilcoxon- bzw. den Mann-Whitney-U-Test. Ein Beispiel: Studenten zweier Gruppen
müssen wollen sich zu ihrem Statistikwissen testen lassen (Gruppe Prof. F . und Prof.
S.). Für jeden Studenten aus Gruppe F wird nun geprüft, gegen wie viele Studenten
aus Gruppe S er „gewinnt“ (höhere Werte aufweist). Dann wird ausgezählt, wie viele
solcher Paarvergleiche insgesamt von Gruppe F gewonnen wurden. Der Anteil der
hypothesenkonformen Paarvergleiche ist dann das Effektstärkemaß (Kerby 2014).
Tab. 16.1 gibt einen groben Überblick über Effektstärken (nach J. Cohen (1988) und Eid et
al. (2010)). Zu beachten ist, dass die Einschätzung, was ein „großer“ oder „kleiner“ Effekt
ist, nicht pauschal übers Knie gebrochen werden kann. Besser ist es, die Höhe der Effektstärke im eigenen Datensatz mit relevanten anderen Datensätzen bzw. mit Sachverstand
(einschlägigen Theorien, sofern vorhanden) zu vergleichen.
Mit dem Paket pwr kann man sich Cohens (1988) Konventionen der Effektstärkehöhen
in Erinnerung rufen lassen. Das Paket bietet folgende Optionen:
cohen.ES(test = c("p", "t", "r", "anov", "chisq", "f2"),
size = c("small", "medium", "large"))
16.5 Alternativen zum p-Wert
291
Allerdings sind die Richtwerte aus Tab. 16.1 vielleicht zu hoch gegriffen: Neuere Studien
finden, dass Effektstärken in der Praxis viel geringer sind. So berichten Bosco et al. (2015),
dass die mediane Effektstärke bei ca. 150000 Korrelationen aus Studien der angewandten
Psychologie bei jrj D 0:16 lag – deutlich unter dem „mittleren“ Effekt nach J. Cohen
(1988). Nach Bosco et al. (2015) scheint folgende Klassifikation passend:
kleiner Effekt (33. Perzentil): r D 0:09
mittlerer Effekt (50. Perzentil): r D 0:16
mittlerer Effekt (67. Perzentil): r D 0:27.
Diese Art der „Klassifizierung“ von Effektstärken ist normorientiert, mit Blick auf „übliche“ Werte geeicht. Eine andere Art der Klassifizierung ist lebensweltlich orientiert
(kriteriumsorientiert): Man suche sich einen Effekt, der allgemein als „groß“ akzeptiert
wird, und berechne die Effektstärke dafür. Nehmen wir den Größenunterschied zwischen
Männern und Frauen. Garcia und Quintana-Domeque (2007) schätzen die mittlere Körpergröße in Österreich auf 168Ë™6 cm (Frauen) bzw. 179Ë™6 cm (Männer).20 Dieser Effekt
hat den Vorteil, dass er allgemein vertraut ist. Ausgedrückt in Cohens d liegt dieser Unterschied bei .179 168/=6 1:83. Und ist damit, nach Cohens Klassifikation, (sehr)
groß.
16.5.2.2 Effektstärken berechnen
Möchte man sich Effektstärken berechnen lassen, ist das Paket compute.es hilfreich. Im
Folgenden sind Effektstärkeberechnungen für gängige Inferenztests vorgestellt, in Fortsetzung zu den bereits besprochenen Beispielen (s. Tab. 16.2).
2 -Test: compute.es::chies(chi_sq, n), wobei chi_sq den 2 -Wert und n
die Stichprobengröße bezeichnet.
t-Test: compute.es::tes(t, n.1, n.2); hier sind der t-Wert und die Größen
der beiden Stichproben anzugeben.
Varianzanalyse (F-Test): broom::glance(mein_modell) gibt R2 aus;
etaSquared(mein_modell) aus dem Paket lsr ebenfalls. Möchte man f 2 berechnen, so geht das per Hand schnell, z. B. 0.2 / (1-0.2).
Korrelation: Der Korrelationswert r ist schon ein Maß der Effektstärke. Yeah.
Regression: Die Steigung der Regressionsgeraden (b) ist ein (unstandardisiertes) Maß
für die Stärke des („Netto“-)Einflusses eines Prädiktors. R2 hingegen ein Maß für die
relative Varianzaufklärung aller Prädiktoren gemeinsam. MSE ist ein weiteres Maß für
die Vohersagegüte (s. Kap. 18).
20
Vgl. https://en.wikipedia.org/wiki/List_of_average_human_height_worldwide; angegeben ist
hier Mittelwert plus/minus SD.
292
16
Inferenzstatistik
Tab. 16.2 R-Befehle für gängige Effektstärkemaße
Test
t.Test
Varianzanalyse
Regression
Logistische Regression
2 -Test
2 -Test
Wilcoxon.test
Effektstärkemaß
Cohens d
R2
R2
Pseudo-R2
!
OR
Cliffs Delta
R.Befehl
compute.es::tes()
lsr::etaSquared()
broom::glance() oder summary()
BaylorEdPsych::PseudoR2()
sqrt(chisqwert)
computes.es::chies()
effsize::cliff.delta()
Logistische Regression: Mit BaylorEdPsych::PseudoR2(mein_glm_objekt)
kann man eine Art R2 bekommen (s. Abschn. 19.6).
Wilcoxon-Test bzw. Mann-Whitney-U-Test: Anteil der paarweisen Vergleiche, die
hypothesenkonform sind (vgl. Kerby (2014)). Dazu kann man z. B. die Funktion
cliff.delta() aus dem Paket effsize nutzen.
16.5.3 Power-Analyse
Eng verwandt mit Effektstärken ist die Frage nach der Power eines Tests. Damit ist gemeint, wie groß die Wahrscheinlichkeit ist, dass ein Test einen Effekt findet – wenn
wirklich einer da ist. Sonst ist die Sache vergebliche Liebesmüh. Sehr anschaulich gesprochen kann man die Power eines Tests vergleichen mit der Güte der Spürnase einer
Trüffelsau, Trude. Wie hoch ist die Wahrscheinlichkeit, einen Trüffel (Effekt) zu finden?
Einsichtigerweise ist die Frage nur sinnvoll, wenn im Feld wirklich ein Trüffel verborgen
liegt. Es ist auch einsichtig, mit etwas Phantasie, dass diese Wahrscheinlichkeit von der
Größe des Trüffels abhängt: Größere Trüffel sind leichter zu finden als kleine. Außerdem
ist wichtig (jetzt kommt die Metapher ins Wackeln), wie häufig die Trüffelsau schnüffelt.
Ergo: Je größer die Stichprobengröße (die Anzahl der Schnüffler), desto eher wird der Effekt gefunden (unter sonst gleichen Umständen). Schließlich ist noch die Fehlalarmquote
von Belang.
Die Power eines Tests gibt an, wie groß die Wahrscheinlichkeit ist, einen Effekt zu
finden, wenn wirklich einer da ist. Anders gesagt: Die Power gibt die Wahrscheinlichkeit an, die H0 berechtigterweise zu verwerfen. Die Power ist abhängig von der
Effektgröße, von der Stichprobengröße und dem Signifikanzniveau Ë›.
Power-Analysen sind für die Forschung von hoher Bedeutung (Chambers 2017; J. Cohen 1992); vor jeder Untersuchung, die Daten erhebt, sollte eine Power-Analyse durchgeführt werden. Eine geringe Power führt einerseits zu einer geringen Aussicht auf „Erfolg“,
16.5 Alternativen zum p-Wert
293
also auf die Chance, einen Effekt zu finden. Warum bereitet man ein Experiment mit viel
Aufwand vor, wenn es dann wahrscheinlich den Effekt nicht findet? Daher lieber in eine hohe Power investieren. Andererseits führt eine geringe Power dazu, dass der Effekt
ungenau geschätzt wird; die Ergebnisse einer Studie mit geringer Power sind also wenig
zuverlässig. Unabhängig von der Power-Analyse sollte man deshalb die Ergebnisse seiner Schätzwerte immer mit Angaben zur Präzision dieser Schätzwerte versehen; gängige
Kennzahlen dafür sind der Standardschätzfehler und das Konfidenzintervall.21
16.5.4
Bayes-Statistik
Der Bayes-Statistik wird mit diesem kurzem Absatz kaum Rechnung getragen. Gute ausführlichere Einführungen sind vorhanden (für gute Einführungen s. McElreath (2015) und
Kruschke (2014); s. Jaynes (2003) für eine philosophischere Einführung). Kurz gesagt
verrechnet Bayes’ Ansatz zwei Komponenten, um die Wahrscheinlichkeit einer Hypothese im Lichte bestimmter Daten zu berechnen. Der Ansatz entspricht Alltagsdenken und
ist mathematisch sauber (Jaynes 2003). Bayes’ Theorem berechnet das, was die meisten
Praktiker interessiert: die Wahrscheinlichkeit des Zutreffens der getesteten Hypothese, im
Lichte der vorliegenden Teststatistik: p.H jT /. Diesen Wert nennt man auch den Vorhersagewert. Zur Erinnerung: Der p-Wert gibt die Wahrscheinlichkeit der Teststatistik an
unter Annahme der getesteten Hypothese: p.T jH /. Offenbar sind beide Terme nicht identisch. Die Bayes-Statistik zieht zwei Komponenten zur Berechnung von p.H jT / heran.
Zum einen die Grundrate einer Hypothese p.H /, zum anderen die relative Plausibilität
der Teststatistik unter meiner Hypothese im Vergleich zur Plausibilität der Teststatistik
unter konkurrierenden Hypothesen. Der Preis, den man für p.H jT / zahlen muss, ist, dass
man Grundraten spezifizieren muss, also Annahmen über die Plausibilität von Theorien
im Vorfeld (apriori) angeben muss.
Betrachten wir ein Beispiel. Nehmen wir an, wir untersuchen die Hypothese „Ich
bin krank“ (jetzt noch keine vorschnellen Einschätzungen). Die Grundrate der fraglichen
Krankheit sei zehn von 1000 (1 %). Der Test, der zur Diagnose der Krankheit verwendet wird, habe eine Sicherheit von 90 %. Von 100 Kranken wird der Test demnach 90
identifizieren (auch Sensitivität genannt) und zehn werden übersehen (ein Überseh- oder
Betafehler von 10 %). Umgekehrt wird der Test von 100 Gesunden wiederum 90 als gesund und demnach korrekt diagnostizieren (Spezifität); zehn werden fälschlich als krank
eingeschätzt (Fehlalarm oder Alpha-Fehler).
Jetzt Achtung: Der Test sagt, ich sei krank. Die Gretchen-Frage lautet: Wie hoch ist die
Wahrscheinlichkeit, dass diese Hypothese, basierend auf den vorliegenden Daten, korrekt
ist? Abb. 16.9 stellt das Beispiel in Form eines Baumdiagrammes dar. In der Medizin ist
positiv zumeist eine schlechte Nachricht, es soll sagen, dass der Test der Meinung ist, die
getestete Person ist krank (das getestete Kriterium trifft zu). Wie man leicht nachrechnen
21
Wahrscheinlich habe ich gerade einen Konfidenzfehler begangen.
294
16
Inferenzstatistik
1000 untersuchte
Personen
990 gesund
891 Tests
negativ
99 Tests
positiv
10 krank
1 Test
negativ
9 Tests
positiv
Abb. 16.9 Die zwei Stufen der Bayes-Statistik in einem einfachen Beispiel
kann, beträgt die Wahrscheinlichkeit, in Wirklichkeit krank zu sein, wenn der Test positiv
9
8 %. Das überrascht auf den ersten Blick, ist doch
ausfällt, 8 %: 9=.99 C 9/ D 108
der Test so überaus „sicher“ (Sensitivität von 90 %)! Aber die Wahrscheinlichkeit, dass
die Hypothese krank zutrifft, ist eben nicht nur abhängig von der Sicherheit des Tests,
sondern auch von der Grundrate. Beide Komponenten sind nötig, um den Vorhersagewert
zu berechnen. Der p-Wert begnügt sich mit der Aussage, ob der Test positiv oder negativ
ist. Die Grundrate wird nicht berücksichtigt.
Die Bayes-Statistik liefert die Wahrscheinlichkeit p einer Hypothese H im Lichte
einer Teststatistik T (d. h. ein gewisses Stichprobenergebnis): p.H jT /. Damit gibt
die Bayes-Statistik die Antwort, die sich die meisten Anwender wünschen.
Fairerweise muss man hinzufügen, dass die Grundrate für die Wissenschaft oft nicht
einfach zu bestimmen ist. Wer kennt schon die Grundrate der „guten Ideen“? Vielleicht der
liebe Gott, aber der hilft uns nicht22 (God 2016). Wir werden also eine Einschätzung treffen müssen, die subjektiv sein kann. Diese Subjektivität ist von Kritikern moniert worden
– zu Recht, wenn die Grundrate ohne starke bzw. nicht auf Basis allgemein akzeptierter
Annahmen getroffen wurde (Briggs 2016).
Auf der anderen Seite kann man diese Subjektivität umgehen, indem man nur angibt,
um welchen Faktor die H1 wahrscheinlicher ist als die H0 , als Resultat der vorliegenden Daten. Das wird durch den sog. Bayes-Faktor BF ausgedrückt. Liegt BF bei zehn, so
eine gängige Konvention, so ist dies „starke“ Evidenz für H1 (da H1 dann zehnmal wahrscheinlicher als die H0 ist); entsprechend stark ist ein BF von 0.1 (1=10) – zugunsten der
H0 (McElreath 2015; Wagenmakers et al. 2016). Ein BF von 1 räumt beiden Hypothesen
22
https://twitter.com/TheTweetOfGod/status/688035049187454976.
16.5 Alternativen zum p-Wert
295
gleiche Plausibilität ein. Einige Softwarepakete (z. B. JASP23 ) geben den Bayes-Faktor
aus; man kann ihn auch aus gängigen Statistiken ableiten (Wagenmakers 2007). Ein t-Test
à la Bayes kann z. B. so berechnet werden:
extra %>%
group_by(sex) %>%
summarise(mean(extra_mean, na.rm = TRUE))
#> # A tibble: 3 x 2
#>
sex
`mean(extra_mean, na.rm = TRUE)`
#>
<chr>
<dbl>
#> 1 Frau
2.91
#> 2 Mann
2.86
#> 3 <NA>
2.73
extra %>%
filter(sex %in% c("Mann", "Frau")) %>%
mutate(sex = factor(sex)) %>%
as.data.frame %>% # 'ttestBF' verkraftet nur althergebrachte data.frames!
ttestBF(formula = extra_mean ~ sex,
data = .) -> extra_BF
# 'formula' muss explizit hingeschrieben sein,
# sonst droht Fehlermeldung
# Hier kommt der Bayes-Faktor:
extra_BF@bayesFactor
#>
bf
error
time
code
#> Alt., r=0.707 -1.51 9.3e-06 Thu Oct 18 12:04:37 2018 128fb475aad8f
Der Test prüft die Hypothese, dass der Mittelwert bzw. der Mittelwertsunterschied null beträgt. Ein Ergebnis einer bayesianischen Analyse ist der Bayes-Faktor. Der Bayes-Faktor
vergleicht die relative Plausibilität zweier Hypothesen, H1 zu H0 . Schätzt man beide Hypothesen apriori, also bevor man die Daten kennt, als gleich plausibel ein, so gibt der
Bayes-Faktor BF 10 an, wie plausibel die H1 im Vergleich zur H0 ist. Das Ergebnis dieses
Tests zeigt einen BF von 1:51; das ist sehr nahe dem indifferenten Wert von 1. Anders
gesagt zeigt dieser BF nur einen Hauch von Evidenz zugunsten der H1 . Keine überzeugende Evidenz für H0 . Man beachte, dass der Test beiden Hypothesen apriori neutral
gegenüberstand; apriori wurden hier beide Hypothesen als gleich wahrscheinlich angesehen. Jetzt ist unsere Überzeugung für die H1 ein bisschen gestiegen, und zwar um den
Faktor, den der Bayes-Faktor angibt. Das negative Vorzeichen zeigt an, dass der Wert in
der ersten Faktorstufe (Frau) geringer ist als in der zweiten (Mann).
Kaum liest man diese Zahl, steigt der unwiderstehliche Drang nach der Frage auf: „Ist
das signifikant?“ Es ist vielleicht besser, auf diese Frage zu verzichten, aber verschiedene
Autoren haben Antworten vorgelegt. Kass und Raftery (1995) schlagen vor, BF > 20 als
„starken“ Beleg zugunsten der H1 einzuschätzen (vgl. Wagenmakers (2007)). Im vorlie23
https://jasp-stats.org/.
296
16
Inferenzstatistik
genden Fall wäre die korrekte Einstufung „kaum der Rede wert“ – unsere Einschätzung
der relativen Plausibilität der H1 im Verhältnis der H0 hat sich kaum verändert.
Aufgaben
Richtig oder falsch?24
Der p-Wert gibt die Wahrscheinlichkeit der H0 an unter der Annahme der Daten.
2. p.DjH / D p.H jD/
3. Der p-Wert sagt, wie gut die Daten zur Nullhypothese passen.
4. Bei sehr großen Stichproben werden nur sehr große Effekte signifikant.
5. Egal wie klein die Effektstärke ist, es existiert eine Stichprobengröße, die diesen Effekt beliebig signifikant werden lässt – solange die Effektstärke größer
null ist.
6. Wenn der p-Wert kleiner als 5 % ist, dann ist meine Hypothese (H1) höchstwahrscheinlich richtig.
7. Wenn der p-Wert größer als 5 % ist, dann ist das ein Beleg für die H0 .
8. Der p-Wert basiert auf der Idee, dass man ein Experiment unendlich oft wiederholt; und das unter zufälligen, aber ansonsten komplett gleichen Bedingungen.
9. Das 95 %-Konfidenzintervall ist der Bereich, in den der Parameter in 95 % der
Fälle fallen würde bei sehr häufiger Wiederholung des Versuchs.
10. Der Vorhersagewert ist definiert als p.H jD/.
1.
Aufgaben
1. Sie möchten ein Experiment durchführen, indem Sie drei Gruppen hinsichtlich
deren Mittelwerte vergleichen; als Test schwebt Ihnen eine Varianzanalyse vor.
Wie groß muss die Stichprobe sein, um eine Power von 80 % zu erreichen?
Gehen Sie von einem „kleinen“ Effekt nach Cohen aus. Das Alpha-Niveau sei
5 %.25
2. Welche Maße der Effektstärke zieht man heran, wenn man untersucht, ob sich
Männer und Frauen in ihrer Zustimmung (ja/nein) zur Frage „Die Welt ist rund“
unterscheiden?26
3. Mit welchem statistischen Test kann man Frage 2 untersuchen?27
24
F, F, R, F, R, F, F, R, R, R.
library(pwr); pwr.anova.test(k = 3, f = 0.1, power = .8).
26
Z. B. das Odds Ratio.
27
Z. B. mit dem 2 -Test.
25
16.5 Alternativen zum p-Wert
297
4. Eine Forscherin untersucht, ob Geschlecht und Extraversion einen Einfluss auf
die Anzahl von Katern haben (Datensatz extra). Sie berechnet einmal eine
Varianzanalyse und einmal eine Regression. Wie sehr unterscheiden sich die
zentralen Koeffizienten aus beiden Methoden?
Lösung
aov_hangover <- aov(n_hangover ~ sex + extra_mean, data = extra)
broom::glance(aov_hangover)
#> # A tibble: 1 x 11
[Ausgabe aus Platzgründen gekürzt]
lm_hangover <- lm(n_hangover ~ sex + extra_mean, data = extra)
broom::glance(lm_hangover)
#> # A tibble: 1 x 11
[Ausgabe aus Platzgründen gekürzt]
Die zentralen Koeffizienten sind gleich; beide Modelle sind Varianten des sog.
Allgemeinen Linearen Modells (vgl. Eid et al. (2010)).
5. Berechnen Sie Effektstärkemaße für die Überlebensrate in der 1. vs. 3. Klasse
auf der Titanic (Datensatz titanic_train aus dem Paket titanic).
Lösung
data(titanic_train, package = "titanic")
titanic_train %>%
select(Survived, Pclass) %>%
filter(Pclass %in% c(1,3)) -> d
tally(Survived ~ Pclass, data = d, format = "prop")
#>
Pclass
#> Survived
1
3
#>
0 0.370 0.758
#>
1 0.630 0.242
OR <- (.63/.37) / (.24/.76)
OR
#> [1] 5.39
6. Erstellen Sie ein Diagramm zur Schätzung des Standardfehlers der Körpergröße
(Datensatz wo_men).
298
16
Inferenzstatistik
Lösung
# SE (d.h. Histogramm der Stichproben) plotten:
height %>%
ggplot() +
aes(x = height_avg) +
geom_histogram(aes(y = ..density..)) +
geom_density()
7. Erstellen Sie ein Diagramm zur Simulation eines Konfidenzintervalls auf Basis
dieses Schätzfehlers (Körpergröße aus Datensatz wo_men). Sprich: Bauen Sie
die Abb. 16.8 nach.
Lösung
# 100 Stichproben simulieren:
height_avg <- do(100) * mean(rnorm(n = 20, mean = 183, sd = 8))
height_sd <- do(100) * sd(rnorm(n = 20, mean = 183, sd = 8))
height <- height_avg %>%
bind_cols(height_sd) %>%
add_column(id = 1:100) %>%
rename(height_avg = mean,
height_sd = sd)
# Eine Dataframe mit 95%-CI erstellen:
height %>%
mutate(height_sd = sd(height_avg),
lower = height_avg - 2*height_sd,
upper = height_avg + 2*height_sd) %>%
mutate(hit = if_else((183 > lower) & (183 < upper), 1, 0)) -> height
# 100 CIs plotten:
height %>%
ggplot() +
aes(x = id) +
geom_hline(yintercept = 183, linetype = "dashed") +
geom_errorbar(aes(ymin = height_avg - 2*height_sd,
ymax = height_avg + 2*height_sd,
color = as.factor(hit),
linetype = as.factor(hit))) +
labs(y = "Größe",
title = "Die meisten Konfidenzintervalle beinhalten hier
den wahren Wert",
caption = "Horizontale Linie: Wahrer Wert (183cm) der Größe
in der Population") +
16.5 Alternativen zum p-Wert
299
theme(legend.position = "none") +
scale_color_manual(values = c("firebrick", "grey40"))
Hinweise zu Simulationskonzepten und deren Umsetzung in R finden sich in
Kap. 17.
8. Was macht das Paket pwr; wozu ist es gut – in einem Satz?28
9. Bauen Sie Abb. 16.3 nach.
Lösung
set.seed(42) # Zufallszahlen fixieren
sample_size <- 1000
muenz_10 <- rbinom(n = sample_size, size = 10, prob = .5) / 10
muenz_5 <- rbinom(n = sample_size, size = 5, prob = .5) / 5
muenz_30 <- rbinom(n = sample_size, size = 30, prob = .5) / 30
muenz_df <- data_frame(muenz_10 = muenz_10,
muenz_5 = muenz_5,
muenz_30 = muenz_30) %>%
gather(key = Anzahl_Wuerfe, value = Trefferquote) %>%
separate(Anzahl_Wuerfe, sep = "_",
into = c("dummy", "Anzahl_Wuerfe")) %>%
select(-dummy) %>%
filter(Anzahl_Wuerfe == 10) %>%
count(Trefferquote) %>%
mutate(Trefferquote_cum = cumsum(n)/1000,
p_values = lag(1 - Trefferquote_cum),
likelihood = n / sample_size,
sig = if_else(Trefferquote >= .8, 1, 0)) -> muenz_df
muenz_df %>%
filter(p_values < 0.05) %>%
summarise(threshold = min(Trefferquote)) %>%
pull(threshold) -> threshold
muenz_df %>%
filter(Trefferquote >= .8) %>%
summarise(p_value_8_of_10 = sum(p_values)) -> p_value
28
?pwr liefert folgende Hinweise (wenn das Paket vorher geladen wurde): „Basic Functions for
Power Analysis pwr“ und als Beschreibung (Description): „Power calculations along the lines of
Cohen (1988) using in particular the same notations for effect sizes. Examples from the book are
given.“
300
16
Inferenzstatistik
muenz_df %>%
ggplot(aes(x = Trefferquote, y = n)) +
geom_vline(xintercept = threshold, linetype = "dashed") +
geom_col(aes(fill = factor(sig))) +
geom_label(aes(label = round(likelihood, 2))) +
scale_x_continuous(breaks = seq(0,1, by= .1)) +
scale_fill_manual(values = c("grey60", "firebrick")) +
scale_y_continuous(limits = c(0, 250)) +
labs(title = paste0("p-Wert von 8/10 Treffer: ", round(p_value, 2)),
x = "Trefferquote q",
caption = paste0("Bereich der Verwerfung (p<.05) beginnt
bei q = ",
round(threshold, 2))) +
theme(legend.position = "none") -> p_simu1
p_simu1
Simulationsbasierte Inferenz
17
Lernziele
Simulationskonzepte anwenden können, um inferenzstatistische Schlüsse zu ziehen
Wissen, was man unter Bootstrapping versteht
Den Unterschied zwischen einer Bootstrap-Stichprobenverteilung und einer
„normalen“ Verteilung erläutern können
Den p-Wert im Rahmen von Simulationskonzepten erläutern können
Nullhypothesen testen im Rahmen von Simulationskonzepten
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(mosaic)
library(bootES)
library(mosaic)
data(flights, package = "nycflights13")
17.1 Stichproben, Statistiken und Population
Wie gerechtfertigt ist es, die Daten einer Stichprobe auf eine Grundgesamtheit (Population) zu verallgemeinern, also induktiv von einigen Daten eine allgemeine Tatsache
zu inferieren? In diesem Kapitel untersuchen wir diese Frage nicht anhand theoretischer
Überlegungen, sondern durch „Ausprobieren“; wir lassen den Computer viele Stichproben
aus einer definierten Population ziehen. So bekommen wir eine Stichprobenverteilung.
Auf dieser Basis entscheiden wir über das Verwerfen einer Hypothese.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_17
301
302
17
Simulationsbasierte Inferenz
Beginnen wir mit einem Beispiel. Nehmen wir an, Sie beraten einen großen deutschen
Flughafen in der Entstehung. Leider gab es in der Vergangenheit einige Probleme beim
Bau und so sind Zeit und Kosten massiv etwas über den veranschlagten Rahmen hinausgeschossen . . . Jetzt wurde eine findige Unternehmensberatung beauftragt, den Karren aus
dem Dreck zu ziehen, und Sie sind das Masterbrain jener Beratung. Eine erste Aufgabe für
Sie ist es, Flugaufkommen anderer Flughäfen zu analysieren, um Anhaltspunkte für das
Mengengerüst „Ihres“ Flughafens zu bekommen. Auch Verspätungen an dem ReferenzFlughafen interessieren die Projektleitung. Dazu analysieren Sie die Abflüge der New
Yorker Flughäfen des Jahres 2013. Komfortablerweise haben wir die Grundgesamtheit aller Flüge von New York, eine Vollerhebung also. Um das Verhalten von Stichproben zu
untersuchen, ziehen wir eine bzw. einige Stichproben der Größe n D 30 und schauen, wie
zuverlässig verschiedene Statistiken sind. Mit zuverlässig ist hier gemeint, wie genau die
Stichprobe die Zustände der Grundgesamtheit abbildet.
set.seed(1234567)
flights_sample <- flights %>%
drop_na() %>%
sample_n(size = 30)
Mit set.seed() haben wir festgelegt, welche Zufallszahlen gezogen werden sollen; Ihre Stichprobe wird die gleichen Elemente enthalten wie diese hier, wenn Sie den gleichen
Wert bei set.seed() eingeben. Zur Erinnerung: In der Praxis haben wir (fast) immer
nur eine Stichprobe, nicht den Luxus einer Vollerhebung. Der Sinn dieser Übung liegt
in der Untersuchung der Eigenschaften verschiedener Statistiken und des Einflusses der
Stichprobengröße. Wir könnten mit der Stichprobe im nächsten Schritt relevante Verspätungswerte berechnen und dem Kunden mitteilen:
flights_sample %>%
select(arr_delay) %>%
summarise_all(funs(min, max, median, mean, sd, IQR))
#> # A tibble: 1 x 6
#>
min
max median mean
sd
IQR
#>
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1
-28
275
6.5 28.1 65.0 57.2
Laut unseren Stichproben-Daten liegt der Mittelwert der Verspätung bei einer knappen
halben Stunde; der Median aber nur bei 6.5 Minuten. Die maximale Verspätung beläuft sich auf etwa 4.58 Stunden. Möchte der Flughafen also konservativ planen, sollte
er sich auf 4.58 Stunden Verspätung im schlimmsten Fall einstellen (?). Schauen wir
jetzt also, wie gut die Verspätungen der Flüge (arr_delay) in unserer Stichprobe
flights_sample – genauer einige Statistiken dieser Stichprobe – der Grundgesamtheit
aller Flüge (aus 2013 von New York) entsprechen.
17.1
Stichproben, Statistiken und Population
303
flights %>%
select(arr_delay) %>%
drop_na() %>%
summarise_all(funs(min, max, median, mean, sd, IQR))
#> # A tibble: 1 x 6
#>
min
max median mean
sd
IQR
#>
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1
-86 1272
-5 6.90 44.6
31
Der Median unserer Stichprobe entspricht dem Populationswert einigermaßen (6.5 vs.
5, d. h. 11.5 Minuten Differenz); beim Mittelwert sieht es ähnlich aus (etwas größere
Differenz) und beim Maximalwert der Verspätung liegen viele Stunden zwischen dem
Populationswert laut flights und dem Schätzwert der Stichprobe flights_sample.
Einige Statistiken eignen sich also nicht oder weniger als andere, um auf die Population zu schließen. Hier hat der Median am besten abgeschnitten; der Maximalwert
am schlechtesten. Das ist ein typischer Befund.
Eine andere Idee wäre, sich die die 5 % Flüge mit der größten Verspätung anzuschauen und diesen Wert als zu erwartende Verspätung anzunehmen. Mit percent_rank()
können wir die Werte von arr_delay in Prozentränge umwandeln:
flights_sample %>%
select(arr_delay) %>%
mutate(delay_ranking = percent_rank(arr_delay)) %>%
arrange(-delay_ranking)
#> # A tibble: 30 x 2
#>
arr_delay delay_ranking
#>
<dbl>
<dbl>
#> 1
275
1.000
#> 2
167
0.966
#> 3
115
0.931
#> # ... with 27 more rows
Demnach kommen wir auf ca. 160 Minuten Verspätung; man könnte also argumentieren,
dass wir uns auf „normale“ Verspätungen vorbereiten, aber die 5 % extremsten Verspätungen als höhere Macht hinnehmen und demnach Vorkehrungen für höchstens 160 Minuten
Verspätung treffen. Vergleichen wir wieder die Statistik mit dem Wert der Population.
quantile() gibt das Quantil für ein Perzentil (oder mehrere) eines Datensatzes (hier 95)
zurück; das ist präziser als mit percent_rank().
quantile(~arr_delay, probs = .95,
data = flights, na.rm = TRUE)
#> 95%
#> 91
304
17
Simulationsbasierte Inferenz
Führe das Experiment häufig aus; zähle die Treffer
Berechne das Integral der Fläche unter der Kurve
f(x|μ,σ2) =
1
(x-μ)2
2σ2
−
e
relative Häufigkeit
Wahrscheinlichkeit
Extreme
Stichproben
2πσ2
60
30
40
50
60
60
70
Anzahl Treffer bei 100 fairen Münzwürfen
30
40
50
60
70
Anzahl Treffer bei 100 fairen Münzwürfen
Abb. 17.1 Sinnbild für klassische und simulationsbasierte Inferenztechniken
Das ist etwa eine Stunde weniger als in unserer Stichprobe. Ist eine Stunde Abweichung
„schlimm“? Das ist keine Frage der Statistik, sondern eine Frage des Sachgegenstands
(Sie könnten sagen, das muss der Kunde entscheiden). Eine andere Frage, und zwar eine
der Statistik, ist: Wie groß sollte die Stichprobe sein, um eine vom Kunden gewünschte
Genauigkeit zu erreichen? (Zu groß sollte die Stichprobe aus Kostengründen nicht sein.)
Das ist ein Punkt, wo die Inferenzstatistik ins Spiel kommt. Anstelle von Simulationstechniken, die wir in diesem Kapitel verwenden, kann man auch die gesuchten Größen bzw.
deren Funktionen direkt ausrechnen (der „klassische“ Ansatz). Das ist allerdings deutlich
komplizierter und nicht für jede Situation möglich; daher finden Simulationstechniken
zunehmend Verwendung (Diez et al. 2014). Abb. 17.1 versinnbildlicht den Unterschied
zwischen klassischem und simulationsbasiertem Inferenzansatz.
17.2 Die Stichprobenverteilung
Wie wir gesehen haben, ist keineswegs sicher, dass eine Statistik dem Parameter
(d. h. dem analogen Kennwert in der Population) ähnlich ist. Wir brauchen also
einen Kennwert, der uns sagt, wie ähnlich eine Statistik einem Parameter wohl ist.
Das leisten der Standardfehler und die Stichprobenverteilung.
Leider kennen wir in der Praxis nicht den „echten“ Wert in der Population (z. B. die
wirkliche Verspätung über alle Flüge aus New York); wir haben meist nur eine einsame Stichprobe, die einen Ausschnitt der Population darstellt. Würden wir uns die Mühe
17.2
Die Stichprobenverteilung
305
machen, viele Stichproben zu ziehen (Sie hören schon den Kunden schreien wegen der
Kosten, aber mittlerweile würde es bei Ihrem Flughafen darauf auch nicht mehr ankommen), dann könnten wir prüfen, wie stabil die Stichprobenkennwerte sind: Sind sich die
Stichproben hinsichtlich des relevanten Kennwerts ähnlich, so ist das ein Indiz dafür, dass
sie den Wert der Population gut schätzen. In dem Flüge-Beispiel haben wir den Luxus, die
Population zu kennen, deswegen ziehen wir zur Demonstration einmal viele Stichproben
(z. B. 1000; n_replics = 1000). O. K., los geht’s. Bitte nehmen Sie sich einen Stapel
mit Zetteln und schreiben Sie auf jeden die Verspätung eines Flugs (Spalte arr_delay in
flights). Das bitte noch 999 Mal wiederholen. Wie? Sie haben keine Lust? Gut, dann
soll der Computer die Stichproben ziehen, der beschwert sich nicht bei langweiligen Arbeiten. Diesen Vorgang nennt man auch Simulation. So setzen wir die Simulation in R
um:
1. Mit der Funktion sample() ziehen wir eine Stichprobe der Größe sample_size
aus den Verspätungen
2. von dem Vektor, der von sample() zurückgeliefert wird, berechnen wir dann den
Mittelwert.
3. Diese Berechnung wiederholen wir dann n_replics Mal mit Hilfe der Funktion
do() (aus mosaic).
Ist das erledigt, betrachten wir die Streuung der Stichproben. Die Verteilung lässt sich auf
üblichem Wege plotten (vgl. Abb. 17.2).
flights_nona <- drop_na(flights, arr_delay)
n_replics <- 1000
sample_size <- 30
set.seed(42) # Zufallszahlen fixieren zur Reproduzierbarkeit
viele_stipros30 <- do(n_replics) * mean(sample(flights_nona$arr_delay,
size = sample_size))
head(viele_stipros30$mean)
#> [1] 15.83 -3.37 10.53 -4.10 -4.37 -0.70
sd(viele_stipros30$mean)
#> [1] 8.2
Die zentrale Zeile dieser Syntax lässt sich so ins Deutsche übersetzen: „Tue n_replics
Mal das Folgende: Berechne den Mittelwert einer Stichprobe an Verspätungswerten.“
Die Stichprobenverteilung eines Parameters zeigt also die Verteilung der Menge aller Stichproben aus der Population; mit 1000 Stichproben haben wir nur eine Näherung, aber das ist meist ausreichend.1 Eine Stichprobenverteilung ändert sich, wenn man
die Stichprobengröße ändert (vgl. Abb. 17.2). Die Streuung (SD) der Stichprobenvertei1
Es ist vielleicht spitzfindig, aber sind wir natürlich von unendlich vielen Stichproben noch unendlich weit entfernt (vgl. Briggs (2016)).
306
17
n = 005
n = 030
SE = 21
0.08
Simulationsbasierte Inferenz
n = 100
SE = 8
SE = 4
density
0.06
0.04
0.02
0.00
0
40
80
0
40
80
0
40
80
arr_delay_mean
Der horizontale Balken zeigt den Standardfehler (SE).
Abb. 17.2 Die grobe Stichprobenverteilung des Mittelwerts der Verspätungen
lung nennt man den Standardfehler (abgekürzt SE). Der SE ist ein Maß für die Zuverlässigkeit (Schätzgenauigkeit, Stabilität, Robustheit, Effizienz) unseres Kennwerts; bei
viele_stipros100 lag die SD – d. h. die SE – bei gut 4.43 Minuten.
Betrachten Sie Abb. 17.2: Die Abbildungen sehen verdächtig nach einer Normalverteilung aus. Tatsächlich verteilt sich eine Stichprobe von k Stichproben-Mittelwerten der
Größe n zunehmend nach einer Normalverteilung – je größer n, desto ähnlicher (wenn k
ausreichend groß ist, sagen wir k D 1000). Nach einer Faustregel ist die Stichprobenverteilung ab n D 30 normalverteilt (Eid et al. 2010). Man sieht: Je größer n, desto kleiner
die SD der Stichprobenverteilung.
Der Standardfehler (SE) zeigt, wie sehr sich Stichprobenkennwerte ähneln, wenn
man viele Stichproben zieht. Er wird oft als Maß für die Schätzgenauigkeit des
Mittelwerts verstanden. Der SE ist nicht identisch mit der Streuung (SD) in der
Population. Grob gesagt will der SE die Genauigkeit der Schätzung angeben. Je
kleiner der SE, desto präziser die Schätzung (kleinerer Schätzbereich).
Um die Genauigkeit anzugeben, mit der ein Stichprobenkennwert (z. B. der Mittelwert
N
X; ein Schätzer) einen Populationswert schätzt, wird häufig der Bereich von XN mSE bis
XN C mSE angegeben (kürzer: XN Ë™ mSE).2 Diesen Bereich nennt man auch ein Konfidenzintervall (KI). Meist wird das Konfidenzintervall für m D 2 angegeben, d. h. XN Ë™ 2SE.
Wie groß ist das 95 %-Konfidenzintervall für die Verspätung der Flüge? Berechnen wir
die untere und obere Grenze des 95 %-KI für das Beispiel viele_stipros100:
2
Für eine genauere Diskussion des Konfidenzintervalls s. Abschn. 16.5.1.
17.2
Die Stichprobenverteilung
307
delay_avg <- mean(viele_stipros100$mean, na.rm = TRUE) %>% round(2)
delay_se <- sd(viele_stipros100$mean, na.rm = TRUE) %>% round(2)
(UG <- delay_avg - 2 * delay_se) # untere Grenze
#> [1] -1.94
(OG <- delay_avg + 2 * delay_se) # obere Grenze
#> [1] 15.8
Vergleichen Sie den Mittelwert der Stichprobenverteilung (6.92) mit dem Mittelwert der
Population (6.9). Wie man sieht, liegen die Zahlen eng beieinander. Der Mittelwert der
Stichprobenverteilung schätzt den Mittelwert der Population ohne Verzerrung, und umso genauer, je größer die Stichprobe ist (s. Abb. 17.2). Bei einer Stichprobengröße von
n D 100 lag die SD des Mittelwerts in den Stichproben bei 4.43 Minuten. Der Median schneidet hier ähnlich gut ab. Halten wir fest: Mittelwert und Median der Population
wurden von der Stichprobenverteilung gut geschätzt. Schauen wir uns an, wie sehr das
95 %-Quantil zwischen den Stichproben streut.
set.seed(42)
n_replics <- 1000
viele_stipros30_q <- do(n_replics) * quantile(sample(flights_nona$arr_delay,
size = 30),
probs = .95)
mean(viele_stipros30_q$X95.)
#> [1] 78.7
sd(viele_stipros30_q$X95.)
#> [1] 38.3
quantile(~arr_delay, data = flights_nona, probs = 0.95)
#> 95%
#> 91
Die letzte Syntax lässt sich so ins Deutsche übersetzen:
Lege die Zufallszahlen fest.
Tue n_replics Mal das Folgende:
Berechne das 95 %-Quantil für eine Stichprobe an Verspätungswerten.
Das Ergebnis soll als Objekt viele_stipros3 gespeichert werden.
In der Population liegt das 95 %-Quantil für Verspätung bei 91 Minuten. In unserer
Simulation (n D 30) liegt dieser Wert im Mittel bei 78.68 Minuten; das ist eine Differenz von 12.32 Min. Das 95 %-Quantil trifft den Populationswert offenbar nicht so genau,
wie das der Mittelwert schafft; die Differenz zwischen Populationswert und Stichprobenverteilung ist beim 95 %-Quantil größer als beim Mittelwert (12.32 Min. vs. 0.72 n.). Wie
ähnlich sind die Stichprobenwerte für das 95 %-Quantil? Betrachten wir die Streuung der
308
17
Simulationsbasierte Inferenz
Stichproben: Die SD liegt bei 38.27 Minuten. Das ist ein Vielfaches der Streuung des
Mittelwerts von 8.2. Natürlich kennt man die Population nicht in der Praxis; der Zweck
der Übung war, das Verhalten von Stichprobenkennwerten zu studieren. Halten wir einige
Ergebnisse fest:
1. Einige Kennwerte wie der Mittelwert und der Median sind besser geeignet zur Schätzung der Populationsparameter als andere (wie Extremwerte oder das 95 %-Quantil).
2. Größere Stichproben führen zu genaueren Schätzungen der Populationsparameter.
3. Die Stichprobenverteilung zeigt, wie sehr Stichproben aus einer bestimmten Population streuen, und gibt damit die Genauigkeit einer Parameter-Schätzung an.
Ein Hinweis zur Verwendung der Pfeife %>% in diesem Beispiel: Bisher haben wir die
Pfeife genutzt, um einen Dataframe von einem Pfeifenschritt zum nächsten weiterzureichen. Man kann aber genauso einen Vektor von einem Pfeifenschritt zum nächsten
weiterreichen. Präziser ausgedrückt sorgt die Pfeife dafür, dass das Ergebnis des Schritts
vor der Pfeife als erstes Argument der Funktion nach der Pfeife übergeben wird. sd() erwartet als erstes Argument einen Vektor, für den die Streuung (SD) berechnet werden soll.
Dieses Argument haben wir gerade nicht expliziert, weil das die Pfeife für uns übernommen hat. Gleiches gilt für round(), das ebenfalls als ersten Parameter den zu rundenden
Vektor erwartet.
17.3 Der Bootstrap
Wie gerade erläutert, haben wir in der Praxis meist nur eine einzige, einsame Stichprobe
und keine (sicheren) Werte zu Populationsparametern. Ohne Stichprobenverteilung kein
Standardfehler. Ohne Population keine Stichprobenverteilung. Der Standardfehler ist, wie
bereits erörtert, eine gebräuchliche Methode, um vom Stichprobenkennwert auf die Population (genauer: den Populationsparameter) zu schließen. Mit einem Trick, bekannt als
Bootstrap, kann man sich dennoch eine Stichprobenverteilung basteln – und damit einen
Standardfehler bekommen (Efron und Tibshirani 1994). Die Idee des Bootstraps ist, die
Stichprobe als Population zu betrachten: Wir ziehen viele Stichproben aus der Stichprobe
mit Ziehen mit Zurücklegen. Nach Entnahme eines Fluges legen wir ihn wieder zurück
in die Stichprobe, so bleibt unsere Original-Stichprobe immer gleich groß. Außerdem
bleibt so die Wahrscheinlichkeit für einen bestimmten Wert immer gleich. Man könnte
vom „Baron-Münchhausen-Prinzip“ sprechen: Wir erzeugen eine Stichprobenverteilung
zwar nicht aus dem Nichts, aber aus den vorliegenden Stichproben-Daten; man zieht sich
gewissermaßen an den eigenen Haaren aus dem Sumpf.3 Dieses wiederholte Stichprobenziehen nennt man auch Resampling. Ein kleines Beispiel: Ziehen wir eine Stichprobe
mit n D 3 aus der Population; eine normale Stichprobe ohne Zurücklegen (replace =
3
In Amerika nimmt man dazu die Stiefelschlaufen, die boot straps.
17.3
Der Bootstrap
309
FALSE). Übrigens: Bei der Funktion sample() ist replace = FALSE die Voreinstellung (default); führt man das Argument replace nicht an, so geht die Funktion von der
Voreinstellung aus. Probieren wir es aus:
set.seed(42)
echte_kleine_stichprobe <- sample(flights_nona$arr_delay,
size = 3, replace = FALSE)
echte_kleine_stichprobe
#> [1] -14 -1 -21
Die Klammern um den gesamten Ausdruck sorgen dafür, dass das Ergebnis ausgedruckt wird. Jetzt ziehen wir aus dieser kleinen Stichprobe eine neue Stichprobe der
gleichen Größe, dieses Mal mit Zurücklegen (replace = TRUE). Voilá: eine BootstrapStichprobe.
set.seed(42)
bootstrap_sample <- sample(echte_kleine_stichprobe,
size = 3, replace = TRUE)
bootstrap_sample
#> [1] -21 -21 -14
In dieser Stichprobe wurde der Flug mit Verspätung 21 zwei Mal gezogen. Es hätte auch
anders ausgehen können: Ein bestimmter Wert hätte einmal, mehrfach oder gar nicht
gezogen werden können. Mit set.seed() kann man festlegen, welche Zufallszahlen gezogen werden. Bei einem bestimmten Wert von set.seed() werden immer die gleichen
Zufallszahlen gezogen.4 Das ist praktisch, wenn man die Ergebnisse exakt wiederholen
(reproduzieren) möchte. Ahnen Sie, was als Nächstes kommt? Wir wiederholen diese
Stichprobenziehung, genauso wie wir das mit der Population getan haben. Dabei gehen
wir davon aus, dass uns eine Stichprobe der Größe n D 100 zur Verfügung steht.
set.seed(42)
sample_n <- 100
echte_stichprobe <- sample(flights_nona$arr_delay, size = sample_n)
viele_bootstrap_samples <- do(1000) * mean(resample(echte_stichprobe))
sd(viele_bootstrap_samples$mean)
#> [1] 4.31
Anstelle von sample(..., replace = TRUE) kann man auch die Funktion
mosaic::resample() verwenden, das ist kürzer. Dabei wird eine Resample-Stichprobe der Größe der Original-Stichprobe gezogen. Die Variabilität (Unterschiedlichkeit,
Streuung) des Stichprobenkennwerts ist ein Maß für die Robustheit des Kennwerts bzw.
für die Genauigkeit der Schätzung des Populationswertes. Aber wissen wir, wie gut unser
Bootstrap funktioniert hat? Vergleichen wir unsere Bootstrap-Werte (vor allem die SD der
4
Was ist da noch Zufall?
310
17
Simulationsbasierte Inferenz
„gebootstrapten“ Stichprobenverteilung) mit einer normalen Stichprobenverteilung von
„echten“ Stichproben.
favstats(viele_stipros100$mean)
#>
min
Q1 median
Q3 max mean
sd
n missing
#> -4.72 3.85
6.73 9.87 21.7 6.92 4.43 1000
0
Erstaunlich! Der Standardfehler der Bootstrap-Verteilung – das Maß der Schätzgenauigkeit von arr_delay – ist mit 4.31 Min. nah am Standardfehler aus der Verteilung der
Stichproben, 4.43 Min., die wir aus der Population gezogen haben.
Für einigermaßen große Stichproben ist der Bootstrap – bzw. der BootstrapStandardfehler – ein guter Schätzer für den „echten“ Standardfehler (Efron und
Tibshirani 1994). Damit bietet das „Bootstrapping“ eine gute Möglichkeit, die
Schätzgenauigkeit zu quantifizieren.
Jetzt können wir eine Aussage zur Genauigkeit unseres Schätzers (des Stichprobenkennwerts) treffen. Dazu nehmen wir das 94 95 %-Konfidenzintervall, weil man das so
gerne tut:
quantile(viele_bootstrap_samples$mean, p = c(.025, .975))
#> 2.5% 97.5%
#> 1.73 18.88
Wir schneiden also von der Verteilung links und rechts jeweils 2.5 % ab, die verbleibenden, inneren 95 % markieren das 95 %-Konfidenzintervall (s. Abb. 17.4). Wir interpretieren das 95 %-KI als eine Art Schätzbereich, in dem sich die mittlere Verspätung bewegt.5
Bootstrapping ist ein Verfahren, um die Genauigkeit einer Schätzung zu quantifizieren, zum Beispiel in Form eines Konfidenzintervalls. Bootstrapping beruht
auf wiederholtem Ziehen mit Zurücklegen aus der Stichprobe; dadurch wird eine
Verteilung erzeugt, die eine Annäherung an die tatsächliche Stichprobenverteilung
darstellt. Der Bootstrap sollte nicht verwendet werden, wenn die Stichprobe als
unrepräsentativ für die Population eingeschätzt wird bzw. wenn sie klein ist. Laut
Mooney et al. (1993) sollte gelten n > 50.
Eine einfache Umsetzung bietet das R-Paket bootES; bei Kirby und Gerlanc (2013)
oder bei Carsey und Harden (2013) findet man eine zugängliche Einführung in den Bootstrap. Mit dem Paket lassen sich nicht nur Konfidenzintervalle für Mittelwerte, sondern
5
Das ist eine laxe Interpretation des Konfidenzintervalls, obwohl eine übliche (s. Abschn. 16.5.1).
17.4
Nullhypothesen auf Signifikanz testen
311
auch für Regressions-, Korrelations- und Mittelwertsunterschieds-Kennwerte berechnen.
Das Paket boot bietet umfassendere Methoden des Bootstrappings für beliebige Statistiken.
bootES(echte_stichprobe)
#>
#> 95.00% bca Confidence Interval, 2000 replicates
#> Stat
CI (Low)
CI (High)
bias
SE
#> 9.510
2.583
20.464
-0.065
4.382
In der Voreinstellung führt diese Funktion 2000 Resamples durch. Die Ergebnisse ähneln
unserer Simulation; ein geringer Unterschied ist erwartbar, da andere Zufallszahlen gezogen werden können.
17.4 Nullhypothesen auf Signifikanz testen
Anstatt ein Konfidenzintervall zu berechnen, wird häufig eine Hypothese H0 getestet wie:
„Die mittlere Verspätung in der Population beträgt null Minuten, D 0.“ Für Parameter
der Population benutzt man häufig griechische Buchstaben, um deutlich zu machen, dass
es sich nicht um einen empirischen Wert, sondern um einen theoretischen Wert laut Hypothese handelt. Warum wird gerne die Nullhypothese getestet? Besagt Ihre Hypothese, dass
es eine Verspätung gibt, so ist die Idee naheliegend, die Gegenhypothese – dass eine keine
Verspätung gibt – zu verwerfen.6 Wird die H0 verworfen, deutet man das gerne als gute
Nachricht für Ihre Hypothese, die als H1 (oder HA ) bezeichnet wird. Man könnte auch argumentieren, dass die H0 die Position eines Skeptikers einnimmt: „Da ist nichts, da gibt es
nichts Besonderes, nicht der Rede wert, da braucht man keine komplizierte Theorie.“ Mit
dieser skeptischen Haltung sollte sich ein vernünftiger Mensch auseinandersetzen. Wie
bei der Berechnung des Konfidenzintervalls geht es darum, von den Stichproben-Daten
auf den Parameter der Grundgesamtheit zu schließen. Eine häufige Anwendung ist, vom
Mittelwert der Stichprobe (der Teststatistik) auf den Mittelwert7 der Population (der Parameter) zu inferieren. Der zentrale Gedanke dabei ist: „Wenn ich die Stichprobenverteilung
laut H0 betrachte, wie häufig kommt mein Stichprobenergebnis xN darin vor?“ Kommt xN in
der Stichprobenverteilung selten vor, so wird das als Beleg gegen die Hypothese gewertet,
dass die Daten aus der Population laut H0 stammen. Der Ablauf eines Hypothesen-Tests
nach dieser Methode ist so (vgl. Abb. 17.3). Der Name der Nullhypothese rührt daher, dass
diese Hypothese getestet und damit einem Falsifikationsversuch (einer „Nullifizierung“;
to nullify) unterworfen wird.
1. Definiere eine Nullhypothese H0 ; die H0 nimmt an, dass es keinen Effekt in der Population gibt, z. B. dass es keine Verspätung (im Mittel) gibt, D 0. Definiere die H1 .
6
7
Wobei das keine astreine Umsetzung von Poppers Falsifikationismus ist (s. Kap. 16).
Erwartungswert.
312
17
Abb. 17.3 Nullhypothesen auf
Signifikanz testen
Simulationsbasierte Inferenz
H0
Stichprobe
Daten
Test-Statistik
X
X0
X0
X
Verteilung
von X unter H0
p-Wert
X0
2. Berechne einen Stichprobenkennwert, dessen Pendant in der Population getestet werden soll (z. B. die mittlere Verspätung); diesen Kennwert nennt man auch die TeststaN
tistik, z. B. ein Mittelwert, X.
3. Ziehe viele Stichproben aus der Verteilung, die die H0 vorgibt, und berechne jeweils
die Teststatistik, z. B. XN0 . Damit erstellen wir die Stichprobenverteilung.
4. Zähle den Anteil der Stichproben aus der Stichprobenverteilung von H0 , deren TestN Diesen
statistik (XN 0 ) so extrem oder extremer ist als die empirische Teststatistik X.
Wert nennt man p-Wert. Extremer heißt diejenigen Werte, die noch mehr als XN für die
H1 sprechen.
So viel zur Theorie. Probieren wir das einmal aus:
1. Die H0 lautet: „Die mittlere Verspätung in der Grundgesamtheit beträgt null Minuten,
D 0“. Die H1 lautet: > 0.
2. Den Verspätungswert unserer echten, empirischen Stichprobe haben wir schon einmal
bestimmt (mean(echte_stichprobe)).
3. Wir ziehen viele Stichproben aus einer Normalverteilung mit Mittelwert D 0. Das
leistet die Funktion rnorm(). Außerdem brauchen wir noch eine Annahme über die
Streuung. Der Einfachheit halber nehmen wir die Streuung der Stichprobe als Schätzwert für die Streuung in der Population.8 Für jede Stichprobe berechnen wir den Mittelwert; der Mittelwert ist hier die getestete Statistik – die Teststatistik. Der Vorgang
wird mittels mosaic::do() einige Male repliziert. Das Ergebnis ist in Abb. 17.4
dargestellt.
8
Das ist nicht der beste Weg, aber ein einfacher.
17.4
Nullhypothesen auf Signifikanz testen
313
95%-KI: [2,19]
p-Wert: 0.017
75
count
count
75
50
50
x = 9.51
25
25
0
0
0
10
-15
20
Die vertikalen Linien geben das 95%-KI an.
-10
-5
0
5
10
15
x0
Mittlere Verspätung
Der Anteil der roten Fläche entspricht dem p-Wert.
Abb. 17.4 Simulation einer Stichproben-Verteilung
set.seed(42)
viele_stipros_H0<- do(n_replics) * mean(rnorm(n = sample_n, mean = 0,
sd = sd(echte_stichprobe)))
head(viele_stipros_H0)
#>
mean
#> 1 1.449
#> 2 0.104
#> 3 -7.449
#> 4 3.705
#> 5 1.760
#> 6 -6.537
favstats(viele_stipros_H0$mean)
#> min
Q1 median
Q3 max
mean
sd
n missing
#> -14 -3.31 -0.127 2.72 13.2 -0.287 4.41 1000
0
4. Anteil der „extremen“ Stichproben: Abb. 17.4 (rechts) zeigt, dass ein Stichprobenwert
von 9.51 selten ist in der Stichprobenverteilung von H0 . Wenn H0 stimmen sollte, dass
ist das beobachtete Ergebnis aus echte_Stichprobe ungewöhnlich. Auf dieser Basis entscheiden wir uns, H0 zu verwerfen; das Stichprobenergebnis spricht gegen H0 .
Wäre der Stichprobenwert in der Stichprobenverteilung häufig als ein beliebig gewählter
Grenzwert (z. B. häufiger als in 5 % der simulierten Stichproben), so behält man die H0
bei. Der Schwellenwert von 5 % ist eine reine Konvention, die nicht immer passen muss.
Aber eine Konvention bietet den Vorteil, dass sie Subjektivität vorbeugt. Zählen wir also
aus, wie viele Stichproben der H0 gleich groß oder größer sind als der Stichprobenkennwert aus echte_stichprobe. Der Anteil von TRUE wird p-Wert genannt. In diesem
Fall liegt er bei 0.017.
prop(viele_stipros_H0$mean >= mean(echte_stichprobe), format = "proportion")
#> prop_TRUE
#>
0.017
In Abschn. 16.4 sind einige R-Funktionen aufgeführt, die p-Werte für gängige Situationen
berechnen, z. B. für die hier getestete Frage.
314
17
Simulationsbasierte Inferenz
Aufgaben
Richtig oder falsch?9
1.
Unter einer Stichprobenverteilung versteht man die Verteilung von Stichproben
aus einer definierten Hypothese, aber nicht aus der Nullhypothese.
2. Unter einer Teststatistik versteht man eine Statistik, die man in einem Inferenztest untersucht.
3. Die Nullhypothese heißt Nullhypothese, weil sie verworfen, also „nullifiziert“
werden soll.
4. Beim Bootstrap werden Stichproben ohne Zurücklegen gezogen.
5. Das 95 %-Quantil einer Stichprobe ist besser geeignet als das 50 %-Quantil, um
den Wert in der Population zu schätzen.
6. Bei größeren Stichproben wird die Schätzung eines Populationsparameters genauer (unter sonst gleichen Umständen).
7. Der Standardfehler zeigt, wie sehr sich Stichprobenkennwerte ähneln, wenn
man viele Stichproben zieht.
8. Eine gängige Interpretation des Standardfehlers ist, dass ein größerer Standardfehler mit größerer Präzision der Parameterschätzung einhergeht.
9. Der Bereich, der sich zwei Einheiten des Standardfehlers um den Mittelwert herum erstreckt, wird bei einer Normalverteilung auch als 95 %Konfidenzintervall bezeichnet.
10. Der Standardfehler ist eine Standardabweichung – die Standardabweichung der
Stichprobenverteilung.
Aufgaben
1.
2.
3.
9
Mit welcher anderen Funktion bzw. anderen Herangehensweise hätten Sie auch
die deskriptiven Statistiken erhalten können?10
Sind größere Stichproben immer besser als kleinere?11
In welchen Situationen benötigt man keine Inferenzstatistik?12
F, R, R, F, F, R, R, F, R, R.
favstats(~arr_delay, data = flights_sample) oder flights_sample
%>% select(arr_delay) %>% skim().
11
Nein; nur, wenn sie mit zunehmender Größe die Grundgesamtheit besser abbilden. So wird auch
eine sehr große Stichprobe an Statistikern nicht unbedingt repräsentativ sein für die Normalbevölkerung hinsichtlich der Lesepräferenzen.
12
Wenn man nicht daran interessiert ist, auf eine Grundgesamtheit zu verallgemeinern oder wenn
man eine Vollerhebung durchgeführt hat (z. B. alle Kunden des eigenen Unternehmens).
10
17.4
Nullhypothesen auf Signifikanz testen
Berechnen Sie die Streuung von viele_stipros100.13
Probieren Sie verschiedene Werte für sample_size aus: 5, 10, 100, 1000. Berechnen Sie wieder das 95 %-Quantil der Verspätung. Wie verändern sich der
Mittelwert und die SD dieses Parameters in Abhängigkeit von der Stichprobengröße?
6. Welchen Einfluss hat die Streuung in der Population auf die Streuung der Stichprobenverteilung (d. h. SE)? Untersuchen Sie diese Frage anhand einer bzw.
mehrerer Simulation mit mind. drei verschiedenen Werten der SD in der Population.
7. Welchen Einfluss hat die Anzahl der Replikationen (Anzahl der Stichproben;
n_replics, z. B. 10, 50, 100, 1000, 1000) auf den mittleren Wert und die
Streuung des 95 %-Quantils?
8. Wiederholen Sie die Simulation für andere Quantile: 50 %, 99 %, 1 %. Was
beobachten Sie?
9. Das 99 %-KI berechnet sich als XN Ë™ 2:58 SE; überdeckt das 99 %-KI einen
größeren oder kleineren Wertebereich als das 95 %-KI? Beantworten Sie die
Frage mit einer oder mehreren Simulationen.
10. Wie groß ist das 89 %-Konfidenzintervall14 für die Klausurpunkte (Datensatz
stats_test)?
4.
5.
Lösung
data("stats_test", package = "pradadata")
set.seed(42)
n_replics <- 1000
stats_test %>%
drop_na(score) -> d
boot_verteilung <- do(n_replics)* mean(~score, data = resample(d))
qplot(data = boot_verteilung,
x = mean)
11. Berechnen Sie auf Basis einer Simulationsstudie den Zusammenhang von
Ankunftverspätung und Distanz zum Flugziel. Berichten Sie ein 95 %-Konfidenzintervall zu dieser Korrelation. Verwenden Sie eine Stichprobe mit n D
100 aus dem Flights-Datensatz.
13
14
viele_stipros100 %>% sd %>% round(2).
89 ist eine Primzahl. Verklärter Blick in die Ferne.
315
316
17
Simulationsbasierte Inferenz
Lösung
set.seed(42)
flights_sample100 <- drop_na(flights) %>% sample_n(100)
boot_cor <- do(10000) * cor(arr_delay ~ distance, data =
resample(flights_sample100))
boot_cor %>%
ggplot(aes(x = cor)) +
geom_histogram()
quantile(~cor, data = boot_cor, prob = c(.05, .5, .95))
#>
5%
50%
95%
#> -0.2415 -0.0711 0.1497
count
750
500
250
0
-0.25
0.00
0.25
cor
Das 95 %-Konfidenzintervall der Korrelation reicht von :24 bis .15.
12. Wie groß ist der Unterschied der mittleren Verspätung der Flughäfen LGA und
JFK? Berichten Sie ein 95 %-Konfidenzintervall. Gehen Sie von einer Stichprobe mit n D 100 aus (Datensatz flights).
Lösung
set.seed(42)
flights_sample100 %>%
filter(origin %in% c("JFK", "LGA")) -> flights_sample_JFK_LGA
boot_diffmean <- do(1000) * diffmean(arr_delay ~ origin,
data = resample(flights_sample_JFK_LGA))
quantile(~diffmean, data = boot_diffmean,prob = c(.05, .5, .95))
#>
5% 50% 95%
#> 11.6 30.2 50.5
Vergleichen wir zur Kontrolle den echten Unterschied zwischen den beiden
Flughäfen („echt“ soll heißen im gesamten Datensatz flights):
17.4
Nullhypothesen auf Signifikanz testen
diffmean(arr_delay ~ origin,
data = filter(flights, origin %in% c("JFK", "LGA")),
na.rm = TRUE)
#> diffmean
#>
0.232
Der echte Unterschied in der Verspätung passt gut zum simulierten Konfidenzintervall.
317
Teil VI
Geleitetes Modellieren
Lineare Modelle
18
Lernziele
Wissen, was man unter linearen Modellen bzw. linearer Regression versteht
Die Annahmen der Regression überprüfen können
Regression mit kategorialen Prädiktoren durchführen können
Die Modellgüte bei der Regression bestimmen können
Interaktionen erkennen und ihre Stärke einschätzen können
Die Relevanz von Prädiktoren einschätzen können
Für dieses Kapitel benötigen wir folgende Pakete und Daten:
library(caret)
library(tidyverse)
library(gridExtra)
library(modelr)
library(broom)
library(mosaic)
data(stats_test, package = "pradadata")
library(ggrepel)
18.1 Die Idee der klassischen Regression
Regressionsmodelle stellen eine bestimmte Art der Modellierung von Daten dar; synonym und treffender sollten wir besser von einem linearen Modell sprechen. Linear ist das
Modell in diesem Fall deswegen, weil wir eine Gerade „schön mittig“ in die Daten legen.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_18
321
322
18
Lineare Modelle
1.00
score
0.75
0.50
0.25
0.00
1
2
3
4
5
study_time
Abb. 18.1 Beispiel für eine Regression
Damit haben wir ein einfaches, lineares Modell1 (vgl. Abb. 18.1). Die Gerade „erklärt“
die Daten: Für jeden X-Wert liefert sie einen Y-Wert als Vorhersage zurück. Eine Gerade
kann mit y D b 0 C b 1 x beschrieben werden, wobei y der vorherzusagende Wert (das Kriterium) ist, b1 die Steigung der Geraden, b0 der y-Wert bei x D 0 (oft als Achsenabschnitt
bezeichnet) und x den Wert des Prädiktors darstellt. In Abb. 18.1 hieße das score =
b0 + b*study_time. Das Kriterium score ist der Erfolg in einer Statistikklausur (in
Prozentpunkten), und study_time ist die Lernzeit für die Klausur. Anders ausgedrückt:
Die Vorhersage für die Klausurpunkte (score) einer Person berechnet sich als der Wert
des Achsenabschnitts plus dem Produkt aus der Anzahl der gelernten Stunden mal den
Zusatznutzen pro gelernter Stunde.
stats_test %>%
ggplot() +
aes(x = study_time, y = score) +
geom_jitter(alpha = .3) +
geom_smooth(method = "lm")
Mit geom_smooth(method = "lm") wird eine Trendgerade in Form einer Geraden
in den Punkteschwarm gelegt. Wie das genau funktioniert, dazu gleich mehr. Fürs Erste
begnügen wir uns mit der etwas gröberen Beobachtung, dass die Gerade „schön mittig“
in der Punktewolke liegt. Schauen wir uns zunächst die Syntax genauer an.
Nimm stats_test UND DANN
starte ein neues Diagramm mit ggplot UND DANN
definiere das Diagramm (X-Achse, Y-Achse) UND DANN
zeichne das Geom „jitter“ (verwackeltes Punktediagramm) UND DANN
zeichne eine Glättungslinie vom Typ „lineares Modell“ (lm) ein.
Da das Kriterium y als eine Linearkombination der einzelnen Terme b0 C b1 x C : : : gebildet
wird, kann man wegen der Addition der Terme von einer linearen Regression sprechen, selbst wenn
quadrierte Terme wie x 2 in der Modellgleichung vorkommen (J. Cohen et al. 2013).
1
323
1.00
1.00
0.75
0.75
score
score
18.1 Die Idee der klassischen Regression
0.50
0.50
0.25
0.25
0.00
0.00
2.5
5.0
7.5
10.0
2
self_eval
4
6
interest
Abb. 18.2 Zwei weitere Beispiele für Regressionen
Eine Regression zeigt anhand einer Regressionsgeraden einen „Trend“ in den Daten an
(s. weitere Beispiele in Abb. 18.2). Eine Regression lädt förmlich dazu ein, Vorhersagen
zu treffen: Hat man erstmal eine Gerade, so kann man für jeden X-Wert („Prädiktor“) eine
Vorhersage für den Y-Wert („Kriterium“) treffen. Anhand des Diagramms kann man also für jede Person (d. h. jeden Wert innerhalb des Wertebereichs von study_time oder
einem anderen Prädiktor) einen Wert für score vorhersagen. Wie gut (präzise) die Vorhersage ist, steht erstmal auf einem anderen Blatt. Lineare Modelle nutzt man meist für
zwei Zwecke: zur Vorhersage und zur Beschreibung eines Trends. Mit Trend ist meist die
Steigung der Geraden gemeint. Anstelle von „man beschreibt einen Trend“ könnte auch
sagen, man erklärt ein Kriterium anhand einer Funktion des Prädiktors. Die Größe der
(mittleren) Vorhersagefehler ist für beide Fragestellungen von Belang.
Man beachte, dass eine Gerade über ihre Steigung und ihren Achsenabschnitt festgelegt
ist; in Abb. 18.1 ist die Steigung 0.05 und der Achsenabschnitt 0.62. Der Achsenabschnitt
zeigt also an, wie viele Klausurpunkte man „bekommt“, wenn man gar nicht lernt (Probieren kann man es ja mal, nicht wahr?); die Steigung gibt eine Art „Wechselkurs“ (Trend)
an: Wie viele zusätzliche Klausurpunkte (in Prozentpunkten gemessen) bekomme ich für
jede zusätzliche Stufe von study_time? Je stärker die Steigung der Geraden, desto
stärker ist das Einflussgewicht des Prädiktors; auch als b, Regressionsgewicht oder Regressionskoeffizient bezeichnet. In lm1 gilt: Pro Stufe von study_time gewinnt man
etwa 5 Prozentpunkte in der Klausur. Unser Modell ist einfach gehalten: Man könnte argumentieren, dass der Zusatznutzen der 393. Lernstunde geringer ist als der Zusatznutzen
der ersten paar Stunden. Aber dann müssten wir anstelle der Geraden eine andere Funktion nutzen, um die Daten zu modellieren: Im oberen Lernbereich müssten wir die Kurve
abflachen. Modelle solcher Art sind ohne Probleme darstellbar; aber halten wir erst einmal
den Ball flach. In R kann man eine Regression so berechnen:
lm1 <- lm(score ~ study_time, data = stats_test)
tidy(lm1)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.615
0.00880
70.0 0.
#> 2 study_time
0.0537
0.00286
18.8 3.56e-71
324
18
Lineare Modelle
lm steht dabei für „lineares Modell“; allgemeiner gesprochen lautet die Rechtschreibung
für diesen Befehl:
lm(kriterium ~ praediktor, data = meine_datentabelle)
In Worten: kriterium wird modelliert als Funktion von praediktor, wobei sich diese
Variablen im Dataframe meine_datentabelle finden.
Um ausführlichere Informationen über das Regressionsmodell zu bekommen, kann
man die Funktion broom::tidy() nutzen. Alternativ geht auch summary(); aber
tidy() liefert einen übersichtlichen Dataframe zurück, was für die weitere Verarbeitung
zweckdienlicher sein kann.
mein_lm <- lm(kriterium ~ praediktor, data = meine_datentabelle)
tidy(mein_lm)
Natürlich kann man das auch in der Pfeife rauchen mit der Pfeife darstellen:
lm(kriterium ~ praediktor, data = meine_datentabelle) %>% tidy()
18.2 Modellgüte
In einem Regressionsmodell lautet die grundlegende Überlegung zur Modellgüte: Wie
groß ist der Unterschied zwischen Vorhersage und Wirklichkeit? Die Größe des Unterschieds (Differenz, Delta) zwischen vorhergesagtem (geschätztem) Wert und Wirklichkeit
bezeichnet man als Fehler, Residuum oder Vorhersagefehler, häufig mit (griechisches e
wie error) abgekürzt.
Betrachten Sie die beiden Plots in Abb. 15.11. Die Gerade gibt die vorhergesagten
(geschätzten) Werte wieder; die Punkte die beobachteten („echten“) Werte. Je länger die
vertikalen Linien („Abweichungsstrecken“), desto größer die Vorhersagefehler. Je größer
der Vorhersagefehler, desto schlechter das Modell. Und umgekehrt.
Je kürzer die „Abweichungslinien“, desto besser die Vorhersage.
Sagt mein Modell voraus, dass Ihre Schuhgröße 49 ist, aber in Wahrheit liegt sie bei
39, so werden Sie dieses Modell als schlecht beurteilen, wahrscheinlich. Je größer die Abweichung zwischen echtem und vorhergesagtem (geschätztem) Wert, desto schlecht(er)
ist das Modell bzw. die Modellpassung (Modelfit). Leider ist es nicht immer einfach zu
sagen, wie groß der Fehler sein muss, damit das Modell als „gut“ bzw. „schlecht“ gilt.
Man kann argumentieren, dass es keine wissenschaftliche Frage sei, wie viel „viel“ oder
„genug“ ist (Briggs 2016). Das ist zwar plausibel, hilft aber nicht, wenn ich eine Entscheidung treffen muss. Stellen Sie sich vor: Ich zwinge Sie mit der Pistole auf der Brust, meine
Schuhgröße zu schätzen.
18.2 Modellgüte
325
Eine einfache Lösung ist, das beste Modell unter mehreren Kandidaten zu wählen. Ein
anderer Ansatz zur Beurteilung der Güte der Vorhersage ist, die Vorhersage in Bezug zu
einem Kriterium zu setzen. Dieses „andere Kriterium“ könnte sein „einfach die Schuhgröße auf gut Glück raten“. Oder, etwas intelligenter, Sie schätzen meine Schuhgröße auf
einen Wert, der eine gewisse Plausibilität hat, also z. B. die durchschnittliche Schuhgröße
des deutschen Mannes. Auf dieser Basis kann man dann quantifizieren, ob und wie viel
besser die eigene Vorhersage im Vergleich zu diesem Referenzkriterium ist.
18.2.1 Mittlere Quadratfehler
Eine der häufigsten Gütekennzahlen ist der mittlere Quadratfehler (Mean Squared Error,
MSE), wobei Fehler wieder als Differenz zwischen Vorhersage (pred) und Beobachtung
(obs, y) definiert ist. Dieser berechnet für jede Beobachtung den Fehler, quadriert ihn und
bildet dann den Mittelwert dieser „Quadratfehler“.
MSE D
1X
.obs pred/2
n
Konzeptionell ist dieses Maß an die Varianz angelehnt. Zieht man aus diesem Maß die
Wurzel, so erhält man den sog. Root Mean Square Error (RMSE), der die Standardabweichung der Vorhersagefehler darstellt. In Pseudo-R-Syntax:
RMSE <- sqrt(mean((df$pred - df$obs)^2))
Der RMSE hat die gleiche Einheit wie die zu schätzende Variable, also z. B. SchuhgrößenNummern. caret::postResample() ist eine praktische Funktion, um sich den RMSE
ausgeben zu lassen; in Abschn. 18.9 betrachten wir ein Beispiel dafür.
caret::postResample(pred = df$predicted, obs = df$y_werte)
18.2.2
R-Quadrat (R 2 )
R2 , auch Bestimmtheitsmaß oder Determinationskoeffizient genannt, setzt die Höhe unseres Vorhersagefehlers ins Verhältnis zum Vorhersagefehler eines „Nullmodells“. Wenn
das Nullmodell sprechen könnte, würde es hier sagen: „Keine Ahnung, was deine Schuhgröße ist, mich interessieren auch keine Prädiktoren, ich nehme einfach den Mittelwert
der Grundgesamtheit als Schätzwert für deine Schuhgröße. Fertig.“
Analog zum Nullmodell-Fehler spricht man auch von der Gesamtvarianz (sum of squares total, Gesamtstreuung, SST ); sie ist definiert als Summe der quadrierten mittleren
P
N SST D i .yi y/
N 2.
Abweichung der n beobachteten Werte yi von ihrem Mittelwert y:
Der Vorhersagefehler unseres Modells wird auch SSE (sum of squares error; Fehler-,
Residual- oder Reststreuung) genannt und ist definiert als Summe der quadrierten mittleren Abweichung von beobachteten Werten yi und vorhergesagten Werten yOi : SSEn D
326
18
Lineare Modelle
y
y
y
f
x
x
Abb. 18.3 Sinnbild für den Determinationskoeffizienten. (Quelle: Orzetto 2010)
P
P 2
SST SSM D i .yi yOi /2 D
ei . Die Differenz zwischen der Gesamtstreuung und
der Reststreuung ist der Erklärungsbeitrag unseres Modells, die Streuungsreduktion durch
P
N 2.
unser Modell: SSM D i .yOi y/
2
Damit gibt R an, wie gut unsere Vorhersagen im Verhältnis zu den Vorhersagen des
Nullmodells sind. Ein R2 von 25 % (0.25) hieße, dass unser Vorhersagefehler 25 % kleiner
ist als der des Nullmodells. Ein R2 von 100 % (1) heißt also, dass wir den kompletten
Fehler reduziert haben (null Fehler übrig) – eine perfekte Vorhersage. Etwas formaler
kann man R2 so definieren:
P 2
SST SSM
SSE
i ei
2
P
D1
D1
R D1
SST
SST
N 2
i .yi y/
Abb. 18.3 versinnbildlicht R2; das linke Teildiagramm zeigt die (quadrierten) Abweichungen von yN für vier Punkte. Das rechte Teildiagramm zeigt die (quadrierten) Abweichungen
der beobachteten Punkte yi von den vorhergesagten yOi . In Pseudo-R-Syntax:
R2 <- 1 - sum((df$pred - df$obs)^2) / sum((mean(df$obs) - df$obs)^2)
Praktischerweise gibt es einige R-Funktionen, z. B. broom::glance(mein_lm) oder
einfach summary(mein_lm), die die Berechnung von R2 für uns besorgen.
Verwendet man die Korrelation (r) oder R2 als Gütekriterium, so sollte man sich
über folgenden Punkt klar sein: Ändert man die Skalierung der Variablen, ändert
sich die Korrelation nicht; das gilt auch für R2 . Beide Koeffizienten zielen allein
auf das Muster der Zusammenhänge ab – nicht die Größe der Abstände. Aber häufig ist die Größe der Abstände zwischen beobachteten und vorhergesagten Werten
das, was uns interessiert. In dem Fall wäre der (R)MSE vorzuziehen. Auch perfekte
Korrelation heißt nicht, dass vorhergesagte und beobachtete Werte identisch sind.
18.3 Die Regression an einem Beispiel erläutert
327
18.3 Die Regression an einem Beispiel erläutert
Schauen wir uns den Datensatz zur Statistikklausur noch einmal an. Welchen Einfluss hat
die Lernzeit auf den Klausurerfolg? Wie viel bringt es also, zu lernen? Wenn das Lernen
keinen Einfluss auf den Klausurerfolg hat, dann kann man es ja gleich sein lassen . . . Aber
umgekehrt, wenn es viel bringt, gut, dann könnte man sich die Sache (vielleicht) noch
mal überlegen. Aber was heißt „viel bringen“ eigentlich? Wenn für jede Stunde Lernen
viele zusätzliche Punkte herausspringen, dann bringt Lernen viel. Allgemeiner: Je größer
der Zuwachs im Kriterium ist pro zusätzliche Einheit des Prädiktors, desto größer ist der
Einfluss des Prädiktors. Natürlich könnte jetzt jemand argumentieren, dass die ersten paar
Stunden lernen viel bringen, aber dann flacht der Nutzen ab, weil es ja schnell einfach
und trivial werde. Aber wir argumentieren (erstmal) so nicht. Wir gehen davon aus, dass
jede Stunde Lernen gleich viel (oder wenig) Nutzen bringt. Geht man davon aus, dass
jede Einheit des Prädiktors gleich viel Zuwachs bringt, unabhängig von dem Wert des
Prädiktors, so geht man von einem linearen Einfluss aus. Versuchen wir im ersten Schritt,
die Stärke des Einflusses an einem Streudiagramm abzuschätzen (s. Abb. 18.1). Hey R –
berechne uns die „Trendlinie“ (Regressionsgerade)! Dazu nimmt man den Befehl lm():
mein_lm <- lm(score ~ study_time, data = stats_test)
tidy(mein_lm)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.615
0.00880
70.0 0.
#> 2 study_time
0.0537
0.00286
18.8 3.56e-71
lm steht für lineares Modell, eben weil eine Linie (Gerade) als Modell in die Daten gelegt
wird. Mit tidy() bekommt man typische Statistiken für die bs, also das Einflussgewicht
(Regressionsgewicht) des Prädiktors. Der Wert von b0 wird von R als (Intercept)
ausgegeben und kennzeichnet den Wert des Kriteriums, wenn der Prädiktor den Wert 0
hat.
Die Steigung der Geraden beträgt 0.05 – das ist der Einfluss des Prädiktors Lernzeit
auf das Kriterium Klausurerfolg. Man könnte sagen: der „Wechselkurs“ von Lernzeit auf
Klausurpunkte. Für jede Stunde Lernzeit bekommt man offenbar so viele Klausurpunkte
(natürlich viel zu leicht); dabei heißt 1.00 alles richtig, volle Punktzahl. Wenn man nichts
lernt (study_time == 0) hat man in dieser Klausur 62 Prozent der Punkte.
Der Einfluss des Prädiktors study_time steht in der Spalte „estimate“ und der
Zeile study_time. Er wird als „estimate“ bezeichnet, weil er zugleich der Schätzwert ist für den „wahren“ Wert des Regressionsgewichts in der Population (der
Parameter). Der Kriteriumswert, wenn der Prädiktor Null ist, steht in der Zeile „(In-
328
18
Lineare Modelle
tercept)“, ebenfalls in der Spalte estimate. Auf Deutsch spricht man hier vom
Achsenabschnitt. Der Standardfehler std.error schätzt die Genauigkeit des Parameters (des Regressionsgewichts bzw. des Achsenabschnitts). Ob der Parameter
signifikant von null verschieden ist, gibt die Spalte mit dem p-Wert an.
Jetzt kennen wir die Stärke (und Richtung) des Einflusses der Lernzeit. Ob das viel
oder wenig ist, ist am besten im Verhältnis zu einem Referenzwert zu sagen. Anders gesagt: Die Antwort auf diese Frage ist nicht unbedingt eine Frage der Statistik; ob eine
Vorhersage „gut genug“ ist, entscheidet der Anwender. Die Gerade wird übrigens so in
die Punktewolke gelegt, dass die (quadrierten) Abstände der Punkte zur Geraden minimal
sind. Dies wird auch als Kriterium der kleinsten Quadrate (Ordinary Least Squares, OLS)
bezeichnet. Jetzt können wir auch einfach Vorhersagen machen. Sagt uns jemand, ich habe „viel“ gelernt (Lernzeit D 4), so können wir den Klausurerfolg grob im Diagramm
ablesen. Genauer geht es natürlich mit dieser Rechnung:
y D 0:62 C 4 0:05
Oder, komfortabler, mit predict(); schließlich berechnen wir noch die Vorhersagegüte.
predict(mein_lm, data.frame(study_time = 4))
#>
1
#> 0.83
Mit glance() kann man den Modellfit anzeigen lassen:
glance(mein_lm)
#> # A tibble: 1 x 11
#>
r.squared adj.r.squared sigma statistic p.value
df logLik
AIC
#> *
<dbl>
<dbl> <dbl>
<dbl>
<dbl> <int> <dbl> <dbl>
#> 1
0.178
0.177 0.135
352. 3.56e-71
2
947. -1888.
#> # ... with 3 more variables: BIC <dbl>, deviance <dbl>, df.residual <int>
Das Bestimmtheitsmaß R2 liegt bei 0.18, was nach Bosco et al. (2015) ein hoher Wert
(starker Effekt) ist. Dabei gilt, dass es subjektiv ist, wie viel „gut“ ist. Das adjustierte R2
nimmt eine Korrektur für Zufallsrauschen vor und sollte dem normalen R2 gegenüber
bevorzugt werden. AIC, BIC, logLik und Deviance sind Kennzahlen, um die Güte von
Modellen zu vergleichen; wir gehen hier nicht näher darauf ein (s. Hastie et al. (2013)
für Details). Im Zweifel sollte man mehrere Modelle vergleichen, um zumindest sagen zu
können, welches der untersuchten Modelle besser ist. 18 % der Varianz des Klausurerfolgs
werden vom Modell „erklärt“. „Erklärt“ bedeutet hier, dass die Fehlerbalken um diesen
Anteil geringer sind als im Nullmodell. In Abb. 18.4 (links) sind die (quadrierten) Abweichungen des Nullmodells (Abweichungen vom Mittelwert) sowie vom Regressionsmodell
18.4 Überprüfung der Annahmen der linearen Regression
50
Abweichung
0.20
40
density
329
Modell
30
Nullmodell
20
Regression
10
0
0.15
0.10
0.05
0.00
0.00
0.05
0.10
0.15
0.20
Abweichung
Nullmodell
Regressionsmodell
Modell
Abb. 18.4 Verdeutlichung, was „Erklären“ im Rahmen eines Regressionsmodells bedeutet
(Abweichung vom Vorhersagewert) dargestellt. Im rechten Teil von Abb. 18.4 erkennt
man die im Median kürzeren Abweichungen beim Regressionsmodell. Man erkennt also,
dass die Abweichungen beim Regressionsmodell geringer sind als beim Nullmodell, wenn
auch nicht viel. In der Höhe, in der die Abweichungen beim Regressionsmodell geringer
sind als beim Nullmodell, hat das Modell das Kriterium „erklärt“, sagt man. MSE und
RMSE kann man zu Fuß gut berechnen:
MSE <- mean(mein_lm$residuals^2)
MSE
#> [1] 0.0183
sqrt(MSE)
#> [1] 0.135
18.4 Überprüfung der Annahmen der linearen Regression
Statistische Verfahren haben Annahmen, Prämissen also, die als gegeben angenommen
werden. So baut der Mittelwert auf der Annahme auf, dass die Daten metrisches Niveau
haben. Genauso hat ein lineares Modell bestimmte Annahmen. Überprüfen wir einige
Annahmen.
Die Linearität des Zusammenhangs haben wir zu Beginn mit Hilfe des Scatterplots
überprüft. Es schien einigermaßen zu passen.
Zur Überprüfung der Normalverteilung der Residuen zeichnen wir ein Histogramm.
Die Residuen können über den Befehl add_residuals (Paket modelr) zum Datensatz hinzugefügt werden. Dann wird eine Spalte mit dem Namen resid zum Datensatz
hinzugefügt. Hier scheint es zu passen (s. Abb. 18.5, links); das Histogramm ähnelt einer Normalverteilung.
stats_test %>%
add_residuals(mein_lm) %>%
ggplot +
aes(x = resid) +
geom_histogram() -> p_assumption1
330
18
Lineare Modelle
stats_test %>%
add_predictions(mein_lm) %>%
add_residuals(mein_lm) %>%
ggplot() +
aes(y = resid, x = pred) +
geom_jitter(alpha = .2, width = .005) +
geom_boxplot(aes(group = pred), alpha = .7) -> p_assumption2
gridExtra::grid.arrange(p_assumption1, p_assumption2, nrow = 1)
Übrigens kann man das Paket modelr auch nutzen, um sich komfortabel die vorhergesagten Werte zum Datensatz hinzufügen zu lassen (Spalte pred):
stats_test %>%
add_predictions(mein_lm) %>%
select(pred) %>%
head
#> # A tibble: 6 x 1
#>
pred
#>
<dbl>
#> 1 0.776
#> 2 0.830
#> 3 0.776
#> 4 0.830
#> 5 0.723
#> 6 0.723
Konstante Varianz (Homoskedastizität): Dies kann z. B. mit einem Scatterplot der Residuen auf der Y-Achse und den vorhergesagten Werten auf der X-Achse überprüft werden. Bei jedem X-Wert sollte die Streuung der Y-Werte (etwa) gleich sein (s. Abb. 18.5,
rechts). Die geschätzten (angepassten) Werte kann man sich pfeifenfreundlich mit dem
Befehl add_predictions() aus dem Paket modelr ausgeben lassen, die Fehlerwerte entsprechend mit dem Befehl add_residuals (s. Abb. 18.5, rechts). Die Annahme der konstanten Varianz scheint leicht verletzt zu sein: Bei geringen Vorhersagewerten (pred) sind die Residuen (resid) vergleichsweise groß, können also nicht
so genau geschätzt werden. Mit steigendem Wert von pred werden die Residuen
etwas kleiner. Die Verletzung dieser Annahme beeinflusst nicht die Schätzung der Steigung, sondern die Schätzung des Standardfehlers und damit den p-Wert der Einflussgewichte.
Extreme Ausreißer: Extreme Ausreißer scheint es einige wenige zu geben.
Unabhängigkeit der Beobachtungen: Wenn die Studenten in Lerngruppen lernen, kann
es sein, dass die Beobachtungen nicht unabhängig voneinander sind: Wenn ein Mitglied
der Lerngruppe gute Noten hat, ist die Wahrscheinlichkeit für ebenfalls gute Noten bei
den anderen Mitgliedern der Lerngruppe erhöht. Böse Zungen behaupten, dass „Abschreiben“ eine Gefahr für die Unabhängigkeit der Beobachtungen sei.
18.5 Regression mit kategorialen Prädiktoren
331
150
resid
count
0.0
100
-0.4
50
0
-0.8
-0.8
-0.4
0.0
0.4
0.65
0.70
resid
0.75
0.80
0.85
0.90
pred
Abb. 18.5 Prüfung von Annahmen des Regressionsmodells
18.5 Regression mit kategorialen Prädiktoren
Vergleichen wir interessierte und nicht interessierte Studenten. Dazu teilen wir die Variable interest in zwei Gruppen (1–3 vs. 4–6) auf; wir dichotomisieren die Variable.
stats_test$interessiert <- stats_test$interest > 3
Dichotomisieren von quantitativen Variablen wird von Statistikern mit Exkommunikation bestraft (hab ich gehört): Es verschleudert grundlos Information. Wir
dichotomisieren hier nur zu Demonstrationszwecken.
Vergleichen wir die Mittelwerte des Klausurerfolgs zwischen den Interessierten und
Nicht-Interessierten:
stats_test %>%
drop_na(score, interessiert) %>%
group_by(interessiert) %>%
summarise(score = mean(score)) -> score_interesse
score_interesse
#> # A tibble: 2 x 2
#>
interessiert score
#>
<lgl>
<dbl>
#> 1 FALSE
0.748
#> 2 TRUE
0.800
Aha, die Interessierten haben im Schnitt 0.05 mehr Punkte als Nicht-Interessierte; nicht
viel. Stellen wir den Unterschied zwischen den beiden Gruppen in einem Diagramm dar
(s. Abb. 18.6).
332
18
Lineare Modelle
1.00
score
0.75
0.50
0.25
0.00
FALSE
TRUE
interessiert
Abb. 18.6 Der Unterschied im Klausurerfolg zwischen Interessierten und Nicht-Interessierten
stats_test %>%
drop_na() %>%
ggplot() +
aes(x = interessiert, y = score) +
geom_boxplot(width = .1) +
geom_jitter(width = .1, alpha = .1) +
geom_point(data = score_interesse,
color = "red",
size = 5,
shape = 17) +
geom_line(data = score_interesse,
group = 1,
color = "red")
Mit group = 1 bekommt man eine Linie, die alle Punkte verbindet (im Datensatz
score_interesse sind es dieser zwei). Wir haben in dem Fall nur zwei Punkte, die
entsprechend verbunden werden. Berechnen wir nun die Vorhersage von score anhand
des kategorialen Prädiktors interessiert:
lm2 <- lm(score ~ interessiert, data = stats_test)
tidy(lm2)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.748
0.00466
160.
0.
#> 2 interessiertTRUE
0.0516
0.00749
6.89 8.15e-12
glance(lm2)
#> # A tibble: 1 x 11
#>
r.squared adj.r.squared sigma statistic p.value
df logLik
AIC
#> *
<dbl>
<dbl> <dbl>
<dbl>
<dbl> <int> <dbl> <dbl>
#> 1
0.0283
0.0277 0.147
47.4 8.15e-12
2
810. -1613.
#> # ... with 3 more variables: BIC <dbl>, deviance <dbl>, df.residual <int>
18.6 Multiple Regression
333
Das Ergebnis entspricht dem Ergebnis der deskriptiven Analyse von vorhin: Der Schätzwert (estimate) für den Einfluss von interessiertTRUE ist der Unterschied des Werts
der Interessierten (interessiert == TRUE) gegenüber dem Wert der Nicht-Interessierten. interessiert == FALSE wird hier als 0 verstanden und interessiert
== TRUE als 1. Damit ist der Unterschied zwischen beiden Gruppen nichts anderes als
die Steigung der Regressionsgeraden, nur dass es eben auf der X-Achse nur zwei Werte
gibt.
18.6 Multiple Regression
Wie wirken sich mehrere Einflussgrößen zusammen auf den Klausurerfolg aus? Wie stark
ist der Einfluss von Interesse über den Einfluss der Lernzeit hinaus? Anders gefragt: Wenn
sich zwei Studenten gleich viel vorbereiten, um wie viel wird der Interessiertere von beiden besser in der Klausur abschneiden? Findet man einen Unterschied im Klausurerfolg
dieser beider Studenten, so kann der nicht von der Lernzeit abhängig sein – aber vom
Interesse schon. In dem Unterschied würde sich also der Einfluss des Interesses, welcher
unabhängig ist von der Lernzeit, widerspiegeln. Ein entscheidendes Merkmal der multiplen Regression ist, dass der Einfluss eines Prädiktors X1 unabhängig vom Einfluss des
Prädiktors X2 bestimmt wird. Betrachten wir ein Modell mit zwei Prädiktoren:
lm3 <- lm(score ~ study_time + interessiert, data = stats_test)
tidy(lm3)
#> # A tibble: 3 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.614
0.00880
69.8 0.
#> 2 study_time
0.0516
0.00297
17.4 2.85e-62
#> 3 interessiertTRUE
0.0184
0.00714
2.58 9.88e- 3
Die multiple Regression untersucht den „Nettoeinfluss“ jedes Prädiktors: Den Einfluss
eines Prädiktors also, wenn die anderen Prädiktoren konstant gehalten werden. Anders
gesagt: Betrachten wir jeden Wert von study_time separat, so haben die Interessierten
jeweils im Schnitt etwas mehr Punkte.
#>
#>
#>
#>
#>
#>
# A tibble: 3 x 5
term
estimate std.error statistic p.value
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
1 (Intercept)
0.614
0.00880
69.8 0.
2 study_time
0.0516
0.00297
17.4 2.85e-62
3 interessiertTRUE
0.0184
0.00714
2.58 9.88e- 3
334
18
Lineare Modelle
A
0.9
interessiert
score
0.8
nicht interessiert
0.7
0.6
1
2
3
4
5
study_time
B
1
5
1.00
score
0.75
0.50
0.25
0.00
FALSE
TRUE
FALSE
TRUE
interessiert
Nur Werte 1 und 5 von study_time sind dargestellt. Die Rauten symbolisieren den Median.
keine Interaktion
Abb. 18.7 Eine multivariate Analyse (hier ohne Interaktionseffekte) fördert Einsichten zu Tage, die
bei einfacheren Analysen verborgen bleiben
Hier haben wir übrigens dem Modell aufgezwungen, dass der Einfluss von Lernzeit auf
Klausurerfolg bei den beiden Gruppen gleich groß sein soll (d. h. bei Interessierten und
Nicht-Interessierten ist die Steigung der Regressionsgeraden gleich). Das illustriert sich
am einfachsten in einem Diagramm (s. Abb. 18.7). Diese multivariate Analyse (mehr als
zwei Variablen sind beteiligt) zeigt uns, dass die Regressionsgerade nicht gleich ist in
den beiden Gruppen (Interessierte vs. Nicht-Interessierte; s. Abb. 18.7): Im Teildiagramm
A sind die Geraden (leicht) versetzt: Die Gerade der Nicht-Interessierten liegt oberhalb
der Geraden der Interessierten; die Geraden sind parallel. Analog zeigt Teildiagramm B,
dass die Interessierten (interessiert == TRUE) geringere Punktewerte haben als die
Nicht-Interessierten, wenn man die Werte von study_time getrennt betrachtet. Ohne
multivariate Analyse hätten wir dies nicht entdeckt. Daher sind multivariate Analysen
sinnvoll und sollten gegenüber einfacheren Analysen bevorzugt werden.
18.7
Interaktionen
335
Die multivariate Analyse zeigt ein anderes Bild, ein genaueres Bild als die einfachere Analyse mit weniger Prädiktoren. So kann ein Sachverhalt, der für den Datensatz
als Ganzes gilt, in seinen Subgruppen anders sein. Beachten Sie, dass sich die Werte (und Vorzeichen) eines Prädiktors ändern können, wenn sich die Gleichung des
Regressionsmodells ändert; wenn also Prädiktoren hinzukommen oder entfernt werden.
Man könnte sich jetzt noch fragen, warum die Regressionsgeraden in Abb. 18.7 parallel
sein müssen. Gerade hat unser R-Befehl sie noch gezwungen, parallel zu sein. Parallele Regressionsgeraden bedeuten, dass die Stärke des Einflusses des Prädiktors (d. h. die
Steigung der Geraden) in beiden Gruppen gleich ist; der Achsenabschnitt ist allerdings
verschieden. Gleich lassen wir hier die Zügel locker. Wenn die Regressionsgeraden nicht
mehr parallel verlaufen, spricht man von Interaktionseffekten: Der Einfluss des Prädiktors
unterscheidet sich zwischen den Gruppen (Baron und Kenny 1986).2
18.7 Interaktionen
Es könnte ja sein, dass die Stärke des Einflusses von Lernzeit auf Klausurerfolg in der
Gruppe der Interessierten anders ist als in der Gruppe der Nicht-Interessierten. Wenn man
nicht interessiert ist, so könnte man argumentieren, dann bringt eine Stunde Lernen weniger, als wenn man interessiert ist. Dann müssten die Steigungen der Regressionsgeraden
in den beiden Gruppen unterschiedlich sein. In diesem Fall würde ein Interaktionseffekt
vorliegen. Interaktionseffekte werden hier synonym als Moderatoreffekte bezeichnet (vgl.
Baron und Kenny (1986) für eine genauere Erläuterung).
Von einem Interaktionseffekt spricht man, wenn die Wirkung eines Prädiktors (auf
das Kriterium) abhängig ist von einem anderen Prädiktor. Bildlich gesprochen:
Wenn die Regressionsgeraden nicht parallel sind. Umgangssprachlich liegt ein Interaktionseffekt dann vor, wenn die Antwort auf die Frage „Hat X einen Einfluss auf
Y ?“ lautet: „Kommt drauf an.“ Und zwar auf den Wert einer dritten Variablen, Z.
Um R dazu zu bringen, die Regressionsgeraden frei variieren zu lassen, so dass sie
nicht mehr parallel sind, fügen wir die Interaktion als zusätzlichen Term zur Regressions-
2
Idealerweise sollten Moderatoren mit den Prädiktoren und dem Kriterium unkorreliert sein (Baron
und Kenny 1986).
336
18
A
B
1.0
1.0
5
4
3
2
1
0.8
score
0.9
interessiert
nicht interessiert
0.9
score
Lineare Modelle
0.8
0.7
0.6
0.7
0.5
0.6
FALSE
0
2
4
study_time
TRUE
interessiert
6
Jede Linie entspricht einer Stufe von "study_time"
Mit Interaktion
Abb. 18.8 Eine Regressionsanalyse mit Interaktionseffekten
gleichung hinzu. Der Interaktionsterm von interessiert und study_time wird in R
mit interessiert:study_time benannt:3
lm4 <- lm(score ~ interessiert + study_time + interessiert:study_time,
data = stats_test)
tidy(lm4)
#> # A tibble: 4 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.624
0.0108
57.9
0.
#> 2 interessiertTRUE
-0.0108
0.0194
-0.558 5.77e- 1
#> 3 study_time
0.0477
0.00382
12.5
3.12e-34
#> 4 interessiertTRUE:study_time 0.00983
0.00606
1.62 1.05e- 1
Der Interaktionseffekt wird in der Zeile mit interessiertTRUE:study_time angezeigt. Bei den Interessierten ist die Steigung der Geraden um 0.01 Einheiten (Prozentpunkte des Klausurerfolgs) höher als bei den Nicht-Interessierten. Der Effekt ist klein und
nicht statistisch signifikant, so dass wir wohl Zufallsrauschen überinterpretieren: Bei den
Interessierten bringt jede Lernstunde tendenziell mehr Klausurerfolg als bei den NichtInteressierten. Auch hier ist eine Visualisierung wieder hilfreich.
Wie wir in Abb. 18.8 sehen, ist der Einfluss von study_time je nach Gruppe (Wert
von interessiert) unterschiedlich (Teildiagramm A). Analog ist der Einfluss des Interesses (leicht) unterschiedlich, wenn man die fünf Stufen von study_time getrennt
betrachtet. Sind die Regressionsgeraden nicht parallel, so liegt ein Interaktionseffekt (in
der Stichprobe) vor; sind sie parallel, liegt kein Interaktionseffekt vor. Im vorliegenden
Fall von lm4 liegt der Einfluss von study_time bei 0.05 Prozentpunkten, wenn die anderen Prädiktoren 0 sind (FALSE wird ebenfalls als 0 gewertet). Allerdings ist ein Wert
3
Alternativ könnte man auch schreiben score ~ interessiert*study_time, das ist kürzer und synonym.
18.8 Prädiktorenrelevanz
337
von 0 für study_time nur begrenzt sinnvoll, denn der Wertebereich liegt zwischen 1 und 4.
Um den Interaktionseffekt besser interpretieren zu können, hilft es, numerische Prädiktoren zu zentrieren.4 Zentrieren bedeutet, dass man von einer Variablen ihren Mittelwert
abzieht.
stats_test %>%
mutate(study_time_centered = study_time mean(study_time, na.rm = TRUE)) -> stats_test
Mit den zentrierten Variablen berechnen wir die Regression erneut:
lm5 <- lm(score ~ interessiert + study_time_centered +
interessiert:study_time,
data = stats_test)
tidy(lm5)
#> # A tibble: 4 x 5
#>
term
estimate std.error statistic
#>
<chr>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.787
0.0172
45.9
#> 2 interessiertTRUE
-0.0108
0.0194
-0.558
#> 3 study_time_centered
0.0576
0.00471
12.2
#> 4 interessiertFALSE:study_time -0.00983
0.00606
-1.62
p.value
<dbl>
1.40e-295
5.77e- 1
5.86e- 33
1.05e- 1
Der Wert von study_time_centered im Modell lm5 gibt an, um wie viele Prozentpunkte sich der Klausurerfolg erhöht, wenn eine Person einen mittleren Lernaufwand
betreibt. In lm5 liegt study_time_centered bei 0.06 Prozentpunkten. Die Interaktion zweier metrischer Prädiktoren ist komplexer als die Interaktion eines numerischen mit
einem kategorialen Prädiktor. Hilfreiche Erläuterungen sind bei McElreath (2015, Kapitel 7.3) oder Wickham und Grolemund (2017, Kapitel 18) zu finden.
18.8
Prädiktorenrelevanz
Hat man eine Regression berechnet, so stellt sich oft die Frage, welcher Prädiktor wichtig
bzw. am wichtigsten ist.
Effektstärke und Relevanz des Prädiktors sind insofern identisch. Bei Regressionsmodellen kann man zwei Arten von Prädiktorenrelevanz unterscheiden, die auf unterschiedliche Aspekte von Relevanz abzielen. Zum einen gibt es korrelationsbasierte Kriterien, wie
R2 und MSE, die die Präzision oder Güte der Vorhersage quantifizieren, also eine Antwort
geben auf die Frage: „Wie genau sind die Vorhersagen auf Basis dieses Prädiktors?“5 So
kann der Zuwachs an R2 , den ein Prädiktor auf sich zieht, wenn er in ein Modell aufge4
Außerdem lässt sich so Probleme der Multikollinearität begegnen (McElreath 2015; Eid et al.
2010).
5
Präziser: Wie groß ist der Beitrag dieses Prädiktors für genaue Vorhersagen?
338
18
Lineare Modelle
nommen wird, als Relevanz für seine Bedeutung herangezogen werden. Betrachten wir
das R2 von lm2 und lm3, die sich nur um den Prädiktor study_time unterscheiden;
beide Modelle enthalten den Prädiktor interessiert. Wie viel zusätzliche Varianz,
über die Erklärung von interessiert hinaus, bindet study_time? Die Funktion
summary() gibt ein Objekt zurück (eine Liste), in der man auf einen Vektor mit dem
Namen r.squared zugreifen kann:
summary(lm2)$r.squared
#> [1] 0.0283
summary(lm3)$r.squared
#> [1] 0.181
delta <- summary(lm3)$r.squared - summary(lm2)$r.squared
delta
#> [1] 0.153
Der Unterschied (Delta-R2 ) liegt bei ca. 15 %; damit ist die Relevanz von study_time
deutlich höher als die von interessiert.
Zum anderen gibt es die Einflussgewichte, b (unstandardisiert) und ˇ (standardisiert),
diese werden verwendet, um den (angenommenen) kausalen Einfluss eines Prädiktors zu
bemessen (vgl. J. Cohen et al. (2013, Kapitel 5.21)). Das unstandardisierte Regressionsgewicht gibt an, um welchen Wert sich die vorhergesagten Kriteriumswerte ändern, wenn
sich der Prädiktor um eine Einheit ändert (Eid et al. 2010). Unstandardisierte Einflussgewichte sind geeignet, wenn es sich um allgemein bekannte Größen handelt; Geld in Dollar
oder Euro ist ein Beispiel. Es hängt von der Zielgruppe ab, welche Variablen allgemein
bekannt sind – in der Psychologie gehören IQ-Werte zu allseits bekannten Variablen. Variablen, deren Maßeinheit keine intrinsische Bedeutung hat, sind weniger geeignet, um
mit dem unstandardisierten Einflussgewicht berichtet zu werden: Pro Punkt auf Sauers
Maß der Statistikliebe steigt der Klausurerfolg um 0.42 Punkte. Ist das viel oder wenig?
Erschwerend kommt hinzu, dass eine Variable in verschiedenen Einheiten gemessen werden kann, z. B. in Meter vs. Zentimetern oder Kilogramm vs. Gramm. Berechnet man
ein Modell einmal mit einer Variable in Kilogramm skaliert und einmal in Gramm skaliert, so wird das Einflussgewicht beim zweiten Modell 1000-fach größer sein, aber die
Modellgüte (z. B. R2 ) wird gleich bleiben.
Standardisierte Einflussgewichte geben an, um wie viele SD-Einheiten sich das Kriterium verändert, wenn sich der Prädiktor um 1 SD-Einheit erhöht. Aufgrund der Unvergleichbarkeit vieler Variablen ist es häufig einfacher, die standardisierten Einflussgewichte
zu vergleichen. Sie sind besonders dann geeignet, wenn man innerhalb einer Stichprobe
die Einflussstärke der Prädiktoren vergleichen möchte.
Um standardisierte Einflussgewichte zu bekommen, kann man alle Variablen des Modells z-standardisieren und dann eine Regression in gewohnter Manier durchführen. Zur
z-Transformation dient die Funktion base::scale(). Alternativ kann man die Funktion QuantPsyc::lm.beta() verwenden. Dieser Funktion übergibt man als Argument
ein Objekt vom Typ lm:
18.9 Anwendungsbeispiel zur linearen Regression
339
QuantPsyc::lm.beta(lm1)
#> study_time
#>
0.422
stats_test %>%
mutate_at(c("study_time", "score"), funs(z = scale(.))) -> stats_test
lm1_z <- lm(score_z ~ study_time_z, data = stats_test)
tidy(lm1_z)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept) -0.00320
0.0225
-0.142 8.87e- 1
#> 2 study_time_z 0.421
0.0225
18.8
3.56e-71
Die p-Werte zu den Einflussgewichten sind nicht geeignet, um die Relevanz des Prädiktors abzuschätzen. p-Werte messen mehrere Dinge gleichzeitig: die Stichprobengröße,
die Genauigkeit der Parameterschätzung und die Größe des Einflussgewichtes. Allerdings
können p-Werte sehr klein sein, ohne dass das eine praktisch relevante Effektstärke (Relevanz des Prädiktors) anzeigen würde (Nakagawa und Cuthill 2007).
18.9 Anwendungsbeispiel zur linearen Regression
18.9.1 Overfitting
Vergleichen wir im ersten Schritt eine Regression, die die Modellgüte anhand der
Trainingsstichprobe schätzt, mit einer Regression, bei der die Modellgüte in einer TestStichprobe überprüft wird. Betrachten wir nochmal die einfache Regression von Lernzeit
auf Klausurerfolg. Wie lautet das R2 ?
stats_test %>%
select(score, study_time) %>%
drop_na %>%
mutate_if(is_integer, as.numeric) -> stats_test_lm4
lm4 <- lm(score ~ ., data = stats_test_lm4)
glance(lm4)
#> # A tibble: 1 x 11
#>
r.squared adj.r.squared sigma statistic p.value
df logLik
AIC
#> *
<dbl>
<dbl> <dbl>
<dbl>
<dbl> <int> <dbl> <dbl>
#> 1
0.178
0.177 0.135
352. 3.56e-71
2
947. -1888.
#> # ... with 3 more variables: BIC <dbl>, deviance <dbl>, df.residual <int>
Im zweiten Schritt teilen wir die Stichprobe in eine Trainings- und eine Test-Stichprobe
auf. Wir „trainieren“ das Modell anhand der Daten aus der Trainings-Stichprobe:
340
lm4_train <- stats_test_lm4 %>%
sample_frac(.8, replace = FALSE)
18
Lineare Modelle
# Stichprobe von 80%, ohne Zurücklegen
lm4_test <- stats_test_lm4 %>%
anti_join(lm4_train) # Alle Zeilen von "df", die nicht in "train" vorkommen
lm_train <- lm(score ~ study_time, data = lm4_train)
Dann testen wir (die Modellgüte) anhand der Test-Stichprobe. Also los, lm_train, mach
deine Vorhersage:
lm_predict <- predict(lm_train, newdata = lm4_test)
Diese Syntax sagt:
Speichere unter dem Namen lm2_predict das Ergebnis folgender Berechnung:
Mache eine Vorhersage („to predict“) anhand des Modells lm_train,
wobei frische Daten, d. h. die Test-Stichprobe (newdata = lm4_test) verwendet werden sollen.
Als Ergebnis bekommen wir einen Vektor, der für jede Beobachtung des Test-Samples
den geschätzten (vorhergesagten) Klausurpunktewert speichert. Dann lassen wir uns mit
caret::postResample() die Gütewerte ausgeben:
caret::postResample(pred = lm_predict, obs = lm4_test$score)
#>
RMSE Rsquared
MAE
#>
0.2009
0.0575
0.1510
Die Funktion postResample aus dem Paket caret liefert uns zentrale Gütekennzahlen für unser Modell. Wir sehen, dass die Modellgüte im Test-Sample schlechter ist als im
Trainings-Sample. Ein typischer Fall, der uns warnt, nicht vorschnell optimistisch zu sein!
In diesem Fall ist der Unterschied zwischen Trainings- und Test-Stichprobe nicht riesig;
aber wir haben auch nur einen Prädiktor und ein einfaches, nicht flexibles Modell verwendet. In anderen Situationen kann der Unterschied zwischen der Modellgüte des Trainingsund des Test-Samples dramatisch sein.
Die Modellgüte in der Test-Stichprobe ist meist schlechter als in der TrainingsStichprobe. Das warnt uns vor Befunden, die naiv nur die Werte aus der TrainingsStichprobe berichten. Bei flexibleren Modellen ist der Unterschied tendenziell
stärker als bei einfachen Modellen wie der linearen Regression.
18.9 Anwendungsbeispiel zur linearen Regression
341
Intercept
study_time
count
900
600
300
0
0.58
0.60
0.62
0.64
0.040
0.045
0.050
0.055
0.060
0.065
value
Abb. 18.9 Konfidenzintervalle für die Modellparameter eines Regressionsmodells
18.9.2 Konfidenzintervalle der Parameter
Im Abschn. 18.3 haben wir im Modell mein_lm den Einfluss von study_time (0.05)
und des Achsenabschnitts (0.1) geschätzt. Allerdings gibt lm() keine Konfidenzintervalle
aus. Mit einem einfachen Bootstrapping-Ansatz können wir aber (gebootstrapte) Konfidenzintervalle für diese Parameter ausgeben lassen (s. Abb. 18.9).
set.seed(42)
lm4_boot_b <- do(10000) * lm(score ~ study_time,
data = mosaic::resample(stats_test_nona))
lm4_boot_b %>%
select(Intercept, study_time) %>%
gather(key = coefficient, value = value) %>%
ggplot(aes(x = value)) +
geom_histogram() +
facet_wrap(~coefficient, scales = "free_x") -> p_lm_boot1
p_lm_boot1
Intercept
study_time
count
900
600
300
0
0.58
0.60
0.62
0.64
0.040
value
0.045
0.050
0.055
0.060
0.065
342
18
Lineare Modelle
0.9
0.8
0.7
0.6
1
2
3
4
5
value
Abb. 18.10 Visualisierung der 1000 via Bootstrap simulierten Regressionsgeraden
Mit quantile() kann man sich die relevanten Zusammenfassungen ausgeben lassen;
außerdem kann man die Werte der theoretischen Konfidenzintervalle vergleichen:
quantile(~Intercept, data = lm4_boot_b, prob = c(.025, .5, .975))
#> 2.5%
50% 97.5%
#> 0.598 0.615 0.633
quantile(~study_time, data = lm4_boot_b, prob = c(.025, .5, .975))
#>
2.5%
50% 97.5%
#> 0.0481 0.0537 0.0591
confint(mein_lm)
#>
2.5 % 97.5 %
#> (Intercept) 0.5982 0.6327
#> study_time 0.0481 0.0593
Ziemlich ähnlich; gut. Es stellt sich die Frage, ob man mit confint() nicht einfacher
ans Ziel kommt. In diesem einfachen Fall ist es richtig, wenn auch das Bootstrapping
konzeptionell einfacher ist. Bei komplexeren Modellen sind aber die theoretischen Verteilungen kompliziert oder unbekannt. Dann ist das Bootstrapping der einfachere oder sogar
der einzige Weg. Interessant könnte es noch sein, die 1000 Regressionsgeraden zu plotten,
um einen Eindruck ihrer Unterschiedlichkeit zu erhalten (s. Abb. 18.10).
lm4_boot_b %>%
#head() %>%
#select(Intercept, study_time) %>%
#gather(key = coefficient, value = value) %>%
ggplot(aes(x = value)) +
geom_abline(aes(slope = study_time, intercept = Intercept),
alpha = .01) +
scale_x_continuous(limits = c(1, 5)) +
scale_y_continuous(limits = c(.6, .9))
18.9 Anwendungsbeispiel zur linearen Regression
343
Auch für R2 ist ein Konfidenzintervall von Interesse; in lm4_boot_b sind die nötigen
Werte enthalten:
quantile(~r.squared, data = lm4_boot_b, prob = c(.055, .5, .945))
#> 5.5%
50% 94.5%
#> 0.149 0.178 0.208
Aufgaben
Richtig oder falsch?6
1.
2.
X-Wert: Kriterium; Y-Wert: Prädiktor.
Der Y-Wert in der einfachen Regression wird berechnet als Achsenabschnitt
plus x-mal die Geradensteigung.
3. R2 liefert einen relativen Vorhersagefehler (relativ im Sinne eines Anteils) und
MSE einen absoluten.
4. Unter „Ordinary Least Squares“ versteht man eine abschätzige Haltung gegenüber Statistik.
5. Zu den Annahmen der Regression gehört die Normalverteilung der Kriteriumswerte.
6. Die Regression darf nicht bei kategorialen Prädiktoren verwendet werden.
7. Mehrere bivariate Regressionsanalysen (1 Prädiktor, 1 Kriterium) sind einer
multivariaten Regression i. d. R. vorzuziehen (unter sonst gleichen Bedingungen).
8. Interaktionen erkennt man daran, dass die Regressionsgeraden nicht parallel
sind.
9. Im Standard von lm() werden Interaktionseffekte berechnet.
10. Stellen Sie sich vor, Sie haben in zwei linearen Modellen jeweils den Einfluss
eines Prädiktors berechnet; also für jeden der beiden Prädiktoren eine einfache Regression berechnet. Jetzt vergleichen Sie deren Einflussgewichte mit
den beiden Einflussgewichten, die sich aus einer multiplen Regression ergeben, wenn also beide Prädiktoren zugleich in ein lineares Modell eingegeben
werden. Dann ist es möglich, dass sich die Einflussgewichte aus der einfachen
Regression von den Einflussgewichten unterscheiden, die sich aus der multiplen Regression ergeben (wenn sich sonst nichts ändert).
6
F, R, R, F, F, F, F, R, F, R.
344
18
Lineare Modelle
Aufgaben
1.
2.
3.
4.
5.
6.
7.
8.
9.
7
Wie groß ist der Einfluss des Interesses?7
Für wie aussagekräftig halten Sie Ihr Ergebnis aus Aufgabe 1?8
Welcher Einflussfaktor (in unseren Daten) ist am stärksten?9
Visualisieren Sie den Gruppenunterschied von interest für score mit einem
Boxplot.10
Berechnen Sie ein lineares Modell dazu.11
Erstellen Sie eine alternative Visualisierung für Abb. 18.6.12
Wie stark ist der Einfluss von study_time auf den Klausurerfolg, wenn man
study_time auch in zwei Gruppen teilt?13
Wie viel Prozent der Variation des Klausurerfolgs können Sie durch das Interesse
des Studenten modellieren?14
Um wie viele Prozentpunkte hat sich der Einfluss von interessiertTRUE
geändert bei lm3 im Vergleich zu lm2?15
lm_interest <- lm(score ~ interest, data = stats_test);
tidy(lm_interest).
8
Diese Fragen können z. B. über R2 oder über RMSE überprüft werden: glance(lm_
interest); RMSE_interest <- sqrt(mean(lm_interest$residualsˆ2)).
9
Diese Frage kann über den Zuwachs von R2 beantwortet werden (das adjustierte R2 am besten)
oder – das ist ein anderer Blickwinkel – über die Frage welches b, d. h. welcher Regressionskoeffizient, am größten ist (im Absolutbetrag).
10
qplot(x = interessiert, y = score, data = stats_test, geom =
"boxplot").
11
lm2a <- lm(score ~ interessiert, data = stats_test).
12
ggplot(stats_test_nonaaes(x = score, fill = interessiert)) +
geom_density(alpha = .5).
13
lm5 <- lm(score ~ I(interest>4), data = stats_test_nona);
tidy(lm5); glance(lm5).
14
lm(score ~ interest, data = stats_test_nona) %>% glance() %>%
pull(adj.r.squared).
15
glance(lm3)$adj.r.squared - glance(lm2)$adj.r.squared.
Klassifizierende Regression
19
Lernziele
Die Idee der logistischen Regression verstehen
Die Koeffizienten der logistischen Regression interpretieren können
Die Modellgüte einer logistischen Regression einschätzen können
Klassifikatorische Kennzahlen kennen und beurteilen können
Für dieses Kapitel benötigen wir folgende Pakete und Daten:
library(SDMTools)
library(pROC)
library(tidyverse)
library(BaylorEdPsych)
library(broom)
data(stats_test, package = "pradadata")
Hilft Lernen, eine Statistikklausur zu bestehen? Kommt es auf das Interesse an? Darauf,
dass man sich mit dem Dozenten gut versteht? Versuchen wir vorherzusagen, wer eine
Statistikklausur besteht; dabei untersuchen wir, wie wichtig die eben genannten Größen
für den Klausurerfolg sind. Etwas genauer gesagt, sagen wir ein binäres (dichotomes)
Kriterium vorher – Bestehen der Klausur vs. Durchfallen – anhand einer oder mehrerer Variablen (Prädiktoren) mit beliebigem Skalenniveau. Verschaffen Sie sich zu Beginn
einen Überblick über den Datensatz stats_test, den wir hier als Beispiel nutzen, z. B.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_19
345
346
19
Klassifizierende Regression
mit glimpse(). Wie viele fehlende Werte gibt es und in welchen Variablen? Entfernen
Sie Fälle mit fehlenden Werten.1
19.1 Normale Regression für ein binäres Kriterium
In gewohnter Manier nutzen wir als ersten Schritt die normale Regression, um das Kriterium Bestehen anhand der Vorbereitungszeit vorherzusagen. Mit einem kleinen Trick
können wir die dichotome Variable bestanden in eine binäre Variable umwandeln (Werte 0 und 1), damit sie wieder in unser Regressions-Handwerk passt: Wenn bestanden
== "ja", dann sei bestanden_num = 1; ansonsten bestanden_num = 0. Dieses
„wenn-dann“ leistet der Befehl if_else: if_else(bedingung, wenn_erfüllt,
ansonsten). In unserem Fall sieht das so aus:
stats_test %>%
drop_na() %>% #fehlende Werte entfernen
mutate(bestanden_num = if_else(bestanden == "ja", 1, 0)) -> stats_test
Rechnen wir jetzt unsere Regression:
lm1 <- lm(bestanden_num ~ study_time, data = stats_test)
tidy(lm1)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.205
0.0292
7.02 3.29e-12
#> 2 study_time
0.151
0.00950
15.9 9.75e-53
Stellen wir das Ergebnis grafisch dar (vgl. Abb. 19.1). Aha! Mehr Lernen hilft offenbar:
Die Regressionsgerade steigt; der Koeffizient ist positiv. Ein typisches Dozentenbeispiel.
Betrachten Sie diese praktische Eigenschaft der Regression: Obwohl die Kriteriumsvariable (Y-Achse) nur zwei Ausprägungen aufweist (0 und 1), sagt sich die Regression:
„Hey, 0 und 1 sind normale reelle Zahlen und zwischen jedem solcher Zahlenpaare gibt
es Zahlen. Also kann ich meine Regressionsgerade ohne abzusetzen durchmalen.“ Damit
können wir die Werte zwischen 0 und 1 wie Wahrscheinlichkeiten interpretieren: Sagt die
Regressionsgerade für bestimmte Prädiktorwerte hohe Kriteriumswerte voraus, so können wir sagen, die Wahrscheinlichkeit, die Klausur zu bestehen, ist hoch. So weit, so gut.
Aber Moment. Was bedeutet es, wenn die Wahrscheinlichkeit größer 1 ist? Dass der Professor vorher eine eidesstattliche Erklärung für Bestehen geschickt hat? Von so etwas hat
man noch nicht gehört . . . Kurz gesagt: Wahrscheinlichkeiten größer 1 und kleiner 0 sind
Quatsch. Wahrscheinlichkeiten müssen zwischen 0 und 1 liegen.
1
Das geht z. B. so: stats_test <- drop_na(stats_test). Man kann argumentieren,
dass fehlende Werte in der Variablen date_time nicht tragisch sind oder dass diese Variable
für unsere Zwecke hier gleich entfernt werden kann.
bestanden_num
19.2 Die logistische Funktion
347
1.0
0.5
0.0
1
2
3
4
5
study_time
Abb. 19.1 Regressionsgerade für das Bestehen-Modell
19.2 Die logistische Funktion
Die Überlegung zeigt uns: Wir brauchen eine Funktion, die das Ergebnis einer linearen Regression in einen Bereich von 0 bis 1 „umbiegt“. Eine häufig dafür verwendete Funktion
ist die logistische Funktion. Abb. 19.2 (links) stellt den Graphen der Funktion exemplarisch für den Bereich von x D 10 bis x D C10 dar. Der Graph der logistischen Funktion
ähnelt einem lang gestreckten S. (Diese Form wird Ogive genannt.)
Die logistische Regression ist eine Verallgemeinerung des Allgemeinen Linearen Modells. Die Idee dabei ist, eine beliebige Funktion (wie eine Ogive) zur Vorhersage zu
verwenden, nicht nur eine Gerade. Die Modellgleichung soll linear bleiben, wie bei der
„normalen“ linearen Regression, also von der Form y D b0 C b1 x1 C b2 x2 C : : : Eine
Gerade ist doch recht unflexibel; in vielen Situationen, wie im Beispiel eben, passt eine
„gebogene“ Kurve besser. Aber es wäre doch schön, wenn wir keine oder zumindest wenig
neue Methodik lernen müssten, sondern mit dem gewohnten Regressionswerkzeug arbeiten können. Das GLM erreicht beides: Flexible Funktionen bei (einigermaßen) gewohnter
Mechanik. Dazu formuliert man die Regressionsgleichung in gewohnter Manier und sagt
bestanden_num
1.00
y
0.75
0.50
0.25
1.0
0.5
0.0
0.00
-10
-5
0
x
5
10
1
2
3
study_time
Abb. 19.2 Die logistische Regression beschreibt eine „s-förmige“ Kurve
4
5
348
19
Klassifizierende Regression
dann: „Jetzt wende eine Operation an, die aus der Geraden meine gewünschte Funktion, z. B. die Ogive macht.“ Diese Funktion wird der Einfachheit halber als Linkfunktion
L bezeichnet, weil sie eine Verbindung (Link) zwischen der Ausgangsfunktion und der
Zielfunktion herstellt. Als Kriterium wird die Wahrscheinlichkeit für y D 1 modelliert,
z. B. das Ereignis Klausur bestanden. Das erlaubt uns, die Regression in gewohnter Weise zu schreiben (spezifizieren): y ~ study_time, nur nehmen wir nicht die Funktion
lm(), sondern glm(). Die Art der Linkfunktion wird dabei angegeben mit dem Argument family, welche für binäre (zweiwertige) Kriterien für Typ binomial sein muss.
Damit lautet der Funktionsaufruf wie folgt:
glm1 <- glm(bestanden_num ~ study_time,
family = "binomial",
data = stats_test)
Die logistische Regression in R wünscht, dass das Kriterium eine binäre Variable sei (Stufen 0 und 1), eine Faktor-Variable oder eine logische Variable (Stufen
TRUE und FALSE) sei; eine String-Variable (Stufen ja und nein) wird nicht goutiert. Wenn Sie eine Faktor-Variable verwenden, so wird die erste Stufe als 0 und
die zweite Stufe als 1 interpretiert (1 ist das zu modellierende Ereignis). Wenn
nicht anderweitig vorgegeben, sortiert R die Faktorstufen alphanumerisch. Mit
levels(stats_test$bestanden_fct) können Sie die Reihenfolge der Faktorstufen auslesen. So können Sie eine neue Faktorstufe als Referenzstufe (d. h. erste
Faktorstufe) bestimmen: relevel(stats_test$bestanden_fct, "nein")
-> stats_test$bestanden_fct.
Der Graph von glm1 ist in Abb. 19.2 rechts abgebildet. Im Diagramm werden ein
Streudiagramm der beobachteten Werte sowie die s-förmige Regressionskurve ausgegeben. Wir können so z. B. ablesen, dass mit einer Lernzeit von 5 Stunden die Wahrscheinlichkeit für Bestehen bei knapp 100 % liegt; viel zu einfach, diese Klausur . . . Die genauen
Koeffizienten lassen sich – genau wie bei der „normalen“ Regression – z. B. mit tidy()
ausgeben:
glm1_tidy <- tidy(glm1)
glm1_tidy
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
-1.42
0.146
-9.73 2.23e-22
#> 2 study_time
0.722
0.0516
14.0 1.82e-44
Die p-Werte der Koeffizienten können in der Spalte p.value abgelesen werden.
Der Achsenabschnitt ((Intercept)) wird mit 1:42 geschätzt, die Steigung von
19.2 Die logistische Funktion
349
study_time mit 0.72. Allerdings sind die hier dargestellten Werte sogenannte Lo-
gits L.2
Die Koeffizienten der logistischen Regression dürfen nicht so interpretiert werden
wie bei der „normalen“ (linearen) Regression. Aber es gilt nach wie vor: Ist der
Einfluss eines Prädiktors X positiv, so steigt der Wert des Kriteriums Y (z. B. die
Wahrscheinlichkeit für Bestehen), wenn der Prädiktorwert steigt. Analoges gilt für
ein Einflussgewicht mit negativem Vorzeichen. Ist das Einflussgewicht null, so hat
der Prädiktor keinen Einfluss. In ähnlicher Weise gilt: Ist der Wert des Achsenabschnitts b0 positiv (b0 > 0), so ist die Wahrscheinlichkeit von Y (z. B. Bestehen
bzw. für das zu modellierende Ereignis) größer als 50 % bei x D 0.
Aber was sagt der Logit denn nun aus? Der Logit ist die Umrechnungsfunktion von
einer Geraden zu einer Ogive. Diese Form der Darstellung (Parametrisierung) erlaubt
uns, Modelle der logistischen Regression genau wie die der normalen Regression auszudrücken; auch die Ausgaben (von R) sehen ähnlich aus. Betrachten wir den Logit genauer:
p
L D ln
1p
Zugeben, dass klingt erstmal opaque; der Logit ist der Logarithmus der Chancen; unter
Chance versteht man den Quotienten der Wahrscheinlichkeit p des modellierten Ereignisses A zu seiner Gegenwahrscheinlichkeit :A mit Wahrscheinlichkeit 1 p; die Chancen
werden auch Odds oder Wettquotient genannt. Das Praktische ist, dass wir die Koeffizienten in Logitform in gewohnter Manier verrechnen dürfen. Wollen wir zum Beispiel
wissen, wie wahrscheinlich das Ereignis „Bestehen“ für eine Person mit einer Lernzeit
von 3 Stunden ist, können wir einfach rechnen: y = intercept + 3*study_time;
natürlich rechnet das R für uns bequemer aus.
predict(glm1, newdata = data.frame(study_time = 3))
#>
1
#> 0.745
Das Berechnen des Kriteriumswerts funktioniert genau wie bei der normalen Regression. Aber beachten Sie, dass das Ergebnis in Logits angegeben ist. Um zur „normalen“
Wahrscheinlichkeit zu kommen, muss man also erst delogarithmieren. Delogarithmieren
bedeutet, die e-Funktion anzuwenden, exp() auf Errisch:
exp(glm1_tidy$estimate[2])
#> [1] 2.06
2
Das L ist ein schnödes L wie in Ludwig.
350
19
Klassifizierende Regression
Im Objekt glm1_tidy, einem Dataframe, finden wir die Koeffizienten in der Spalte
estimate. In der ersten Zeile steht immer der Achsenabschnitt ((Intercept)); in
der zweiten Zeile dann der Prädiktor (in folgenden Zeilen würden weitere Prädiktoren
stehen). Nach dem Delogarithmieren haben wir also Chancen. Wie rechnet man Chancen in Wahrscheinlichkeiten um? Ein Beispiel zur Illustration. Bei Prof. Schnaggeldi
fallen von 10 Studenten 9 durch. Die Durchfallchance ist also 9:1 oder 9. Die Durchfallwahrscheinlichkeit 9=10 oder .9. Also kann man so umrechnen:
wskt D 9=.9 C 1/ D 9=10 D :9
In unserem Fall sind die Chancen etwa 2:1; also lautet die Umrechnung:
(wskt <- 2 / (2+1))
#> [1] 0.667
Die äußeren Klammern führen dazu, dass das Ergebnis der Zuweisung ausgegeben wird
(wir hätten das gleiche Ergebnis erreicht, wenn wir in der zweiten Zeile wskt eingegeben
hätten). Diesen Ritt kann man sich merklich kommoder bereiten, wenn man die predictFunktion um das Argument type = "response" erweitert:3
predict(glm1, newdata = data.frame(study_time = 3), type = "response")
#>
1
#> 0.678
Übrigens: Die logistische Funktion kann so definiert werden:
p.y D 1/ D
ex
ex
D
D
1 C ex
e x . e1x C 1/
1
ex
1
1
D x
e C1
C1
19.3 Interpretation des Logits
Ist ein Logit L größer als 0, so ist die zugehörige Wahrscheinlichkeit größer als 50 % (und
umgekehrt). Je größer der Logit, desto höher die Wahrscheinlichkeit. Eine Wahrscheinlichkeit von 1 entspricht einem unendlich großen Logit. Ist der Wert des Achsenabschnitts
(intercept) positiv (> 0), dann ist das Ereignis bestanden wahrscheinlicher als das
Gegenereignis (nicht bestanden), d. h. p.y D 1/ > :5. Dies gilt unter der Annahme, dass
die Prädiktoren alle den Wert 0 haben. Das können Sie auch so nachprüfen: Ein Logit
von 0 entspricht einem Chancen-Verhältnis von 1 (da e 0 D 1). Die Chance zu bestehen läge also bei 1 : 1. Für jeden Studenten, den Professor Feistersack durchwinkt, lässt er einen
durchrasseln . . . Von zwei Studenten schafft es einer; die Wahrscheinlichkeit zu bestehen
liegt also bei 50 %.
3
Dieses Argument soll heißen: „Die Art (Typ) der Vorhersage soll im Format der ResponseVariablen, also des Kriteriums, erfolgen.“
19.4
Kategoriale Prädiktoren
351
Der Wert estimate in der Zeile von study_time gibt die Steigung der Regressionsgeraden, den Einfluss des Prädiktors also, an. Hier gilt analog zum Achsenabschnitt:
Werte größer als null stehen für einen positiven Einfluss des Prädiktors: Je mehr gelernt
wird, desto höher ist die Wahrscheinlichkeit zu bestehen, und umgekehrt. Größere Werte
lassen sich mit einer stärker steigenden Kurve veranschaulichen (s. Abb. 19.2).
19.4 Kategoriale Prädiktoren
Wie in der linearen Regression können auch in der logistischen Regression kategoriale Variablen als unabhängige Variablen genutzt werden. Betrachten wir als Beispiel die Frage,
ob die kategoriale (genauer: dichotome) Variable interessiert einen Einfluss auf das Bestehen in der Klausur hat, also die Wahrscheinlichkeit für Bestehen erhöht. Die Variable
interessiert definieren wir auf schamlose Weise willkürlich:
stats_test$interessiert <- stats_test$interest > 3
Erstellen Sie zum Aufwärmen ein Diagramm, dass die Bestehensquoten zwischen Interessierten und Nicht-Interessierten vergleicht.4 Los geht’s, probieren wir die logistische
Regression mit einem kategorialen Prädiktor aus:
glm2 <- glm(bestanden_num ~ interessiert,
family = "binomial",
data = stats_test)
tidy(glm2)
#> # A tibble: 2 x 5
#>
term
estimate std.error statistic
p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.299
0.0641
4.67 0.00000298
#> 2 interessiertTRUE
0.666
0.110
6.07 0.00000000127
Der Einflusswert (die Steigung) von interessiert ist positiv: Wenn man interessiert
ist, steigt die Wahrscheinlichkeit zu bestehen. Gut. Aber wie groß ist die Wahrscheinlichkeit für jede Gruppe, d. h. die Gruppe der Interessierten vs. die Gruppe der NichtInteressierten? Am einfachsten lässt man sich das von R ausrechnen:
predict(glm2, newdata = data.frame(interessiert = FALSE),
type = "response")
#>
1
#> 0.574
predict(glm2, newdata = data.frame(interessiert = TRUE),
type = "response")
4
stats_test %>% ggplot() + aes(x = interessiert) +
geom_bar(aes(fill = bestanden), position = "fill") +
geom_jitter(width = .1).
352
19
Klassifizierende Regression
#>
1
#> 0.724
Die Bestehenswahrscheinlichkeit für die Gruppe der Nicht-Interessierten ist 0.57; für die
Interessierten liegt die Wahrscheinlichkeit bei 0.72. Interessiert zu sein, scheint dem Bestehen durchaus zuträglich zu sein.
Wichtig ist noch zu wissen, dass bei der logistischen Regression genau wie bei der
„normalen“ Regression kategoriale Prädiktoren dichotom sein müssen, also nur zwei Stufen haben dürfen. Das liegt daran, dass kategoriale Prädiktoren in das Zahlenwerk 0 und
1 umgemünzt werden; darauf wird dann die normale Regression angewandt. Aber keine
Bange: Hat Ihr kategorialer Prädiktor mehr als zwei Stufen, so wandelt glm() einen Prädiktor p mit k Stufen in k 1 binäre Prädiktoren um, genau wie lm() es macht. Warum
gibt es einen binären Prädiktor weniger, als der Prädiktor p Stufen hat? Diese verlorene
Stufe dient als Vergleichsstufe; der Effekt jeder anderen Stufe wird im Kontrast zu dieser
Stufe berechnet (wiederum genau wie bei lm()).
glm3 <- glm(bestanden_num ~ factor(interest), data = stats_test)
tidy(glm3) %>% head(3)
#> # A tibble: 3 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept)
0.484
0.0285
17.0 1.79e-59
#> 2 factor(interest)2
0.0974
0.0396
2.46 1.40e- 2
#> 3 factor(interest)3
0.145
0.0366
3.95 8.17e- 5
19.5 Multiple logistische Regression
Können wir unser Modell glm1 mit nur einer erklärenden Variablen verbessern, indem
weitere Prädiktoren hinzugefügt werden? Verbessern heißt hier: Können wir die Präzision
der Vorhersage verbessern durch Hinzunahme weiterer Prädiktoren?
glm4 <- glm(bestanden_num ~ study_time + interessiert + self_eval,
family = binomial,
data = stats_test)
tidy(glm4) %>% str()
#> Classes 'tbl_df', 'tbl' and 'data.frame':
4 obs. of 5 variables:
#> $ term
: chr "(Intercept)" "study_time" "interessiertTRUE" "self_eval"
#> $ estimate : num -2.7752 0.3168 -0.0269 0.476
#> $ std.error: num 0.1913 0.0615 0.1307 0.0337
#> $ statistic: num -14.51 5.149 -0.206 14.126
#> $ p.value : num 1.04e-47 2.62e-07 8.37e-01 2.64e-45
b_glm4 <- round(broom::tidy(glm4)$p.value[3], 3)
19.6
Modellgüte
353
Hm, die Interessierten schneiden jetzt – unter Konstanthaltung anderer Einflussfaktoren – schlechter ab als die Nicht-Interessierten; das Regressionsgewicht von
interessiertTRUE ist kleiner als null. Als Statistik-Dozent bin ich (fast) der Meinung, dieses Ergebnis sollte in der Schublade verschwinden (böse Zungen behaupten, das
sei kein ungewöhnlicher Umgang mit unliebsamen Ergebnissen, vgl. Franco et al. (2014)
und Simonsohn et al. (2014)). Allerdings ist der Koeffizient nahe null, so dass es sein kann,
dass wir Zufallsrauschen interpretieren. Der p-Wert, dass der Regressionskoeffizient null
beträgt, liegt bei 0.837.
19.6 Modellgüte
Aber wie gut ist das Modell? Und welches Modell ist besser? Es gibt kein R2 im Sinne einer erklärten Streuung der y-Werte, da keine Abweichungsquadrate berechnet werden im
GLM. Ein Gütemaß bei der (logistischen) Regression ist das Akaike Information Criterion
(AIC). Hier gilt: Je kleiner der AIC, desto besser der Fit. Das AIC ist ein sog. informationstheoretisches Maß; es ist eine Funktion der Wahrscheinlichkeit der Daten unter der
Annahme, dass das Modell gilt (vgl. Hastie et al. (2013)). Richtlinien, was ein „guter“
AIC-Wert ist, gibt es nicht. AIC-Werte helfen nur beim Vergleichen verschiedener Modelle. Mit glance() bekommt man den AIC ausgegeben (neben anderen Werten).
glance(glm3)
#> # A tibble: 1 x 7
#>
null.deviance df.null logLik
AIC
BIC deviance df.residual
#>
<dbl>
<int> <dbl> <dbl> <dbl>
<dbl>
<int>
#> 1
378.
1626 -1088. 2190. 2227.
363.
1621
Man kann beim GLM also kein R2 ausrechnen, zumindest nicht ohne Tricks. Einige
findige Statistiker haben sich aber Umrechnungswege einfallen lassen, wie man auch
ohne Abweichungsquadrate ein R2 berechnen kann; weil es kein „echtes“ R2 ist, nennt
man es auch Pseudo-R2 . Eine Reihe von R-Paketen bieten die Berechnung an, z. B.
BaylorEdPsych::PseudoR2(mein_glmmodell). Es gibt ein paar Varianten von
Pseudo-R2 , wir bleiben bei der Variante von Herrn McFadden: 0.041.
19.6.1 Vier Arten von Ergebnissen einer Klassifikation
Logistische Regressionsmodelle werden häufig zur Klassifikation verwendet; man versucht, Beobachtungen den richtigen Klassen zuzuordnen, z. B.:
Ein medizinischer Test soll Kranke als krank und Gesunde als gesund klassifizieren.
Ein statistischer Test soll wahre Hypothesen als wahr und falsche Hypothesen als falsch
klassifizieren.
Ein Personaler soll geeignete Bewerber als geeignet und nicht geeignete Bewerber als
nicht geeignet einstufen.
354
19
Klassifizierende Regression
Tab. 19.1 Vier Arten von Ergebnissen von Klassifikationen
Wahrheit
In Wahrheit negativ ()
In Wahrheit positiv (C)
Summe
Als negativ () vorhergesagt
Richtig negativ (RN)
Falsch negativ (FN)
N*
Als positiv (C) vorhergesagt
Falsch positiv (FP)
Richtig positiv (RN)
P*
Summe
N
P
NCP
Dabei kann man zwei Arten von Fehlern machen: 1) Kranke als gesund und 2) Gesunde als krank zu klassifizieren (bei den anderen Beispielen analog). Umgekehrt kann man
sagen, man muss zwei Sachen gut machen: Gesunde richtig zu klassifizieren und Kranke
richtig zu klassifizieren. Diese beiden Arten von Klassifikationen können unterschiedlich
gut sein. Im Extremfall könnte ein Test alle Menschen als krank („positiv“) einstufen. Mit
Sicherheit wurden dann alle Kranken korrekt als krank diagnostiziert. Dummerweise würde der Test „auf der anderen Seite“ viele Fehler machen: Gesunde als gesund („negativ“)
zu klassifizieren. Etwas genauer kann man folgende vier Arten von Ergebnisse aus einem
Test erwarten (s. Tab. 19.1, vgl. James et al. (2013)).
Ein Test, der alle positiven Fälle korrekt als positiv klassifiziert, muss deshalb noch
lange nicht alle negativen Fälle als negativ klassifizieren. Die Richtig-Positiv-Rate
und die Richtig-Negativ-Rate können unterschiedlich sein.
Die logistische Regression gibt uns für jeden Fall die Wahrscheinlichkeit (bzw. den
Logit) zurück, dass der Fall zum Ereignis 1 gehört (d. h. dem zu modellierenden Ereignis
wie bestanden). Wir müssen dann einen Schwellenwert (Threshold) auswählen – einen
Wert also, der bestimmt, ob bzw. ab wann der Fall zum Ereignis 1 gehört. Häufig nimmt
man 0:5. Liegt die Wahrscheinlichkeit unter dem Schwellenwert, so ordnet man den Fall
dem Ereignis 0 zu (dem Gegenereignis, z. B. nicht bestanden).
Ein Beispiel: Alois’ Wahrscheinlichkeit, die Klausur zu bestehen, wird vom Regressionsmodell auf 51 % geschätzt. Unser Schwellenwert sei 50 %; wir ordnen Alois der Klasse
„bestehen“ zu. Alois freut sich. Das Modell sagt also bestanden (1) für Alois voraus. Man
sagt auch, der geschätzte Wert (Fitted Value) von Alois sei 1. Die aus dem Modell ermittelten (vorhergesagten; pred) Wahrscheinlichkeiten werden dann in einer sogenannten
Konfusionsmatrix (Confusion Matrix) mit den beobachteten (obs) Häufigkeiten verglichen:
(cm <- confusion.matrix(stats_test$bestanden_num,
glm4$fitted.values))
#>
obs
#> pred
0
1
#>
0 343 164
#>
1 255 865
#> attr(,"class")
#> [1] "confusion.matrix"
19.6
Modellgüte
355
Tab. 19.2 Geläufige Kennwerte der Klassifikation. Anmerkungen: F: Falsch. R: Richtig. P: Positiv.
N: Negativ
Name
FP-Rate
RP-Rate
FN-Rate
RN-Rate
Pos. Vorhersagewert
Neg. Vorhersagewert
Richtigkeit
Definition
FP=N
RP=N
FN=N
RN=N
RP=P*
RN=N*
(RPCRN)=(NCP)
Synonyme
Alphafehler, Typ-1-Fehler, 1-Spezifität, Fehlalarm
Power, Sensitivität, 1-Betafehler, Recall
Fehlender Alarm, Befafehler
Spezifität, 1-Alphafehler
Präzision, Relevanz
Segreganz
Korrektklassifikationsrate, Gesamtgenauigkeit
Wie häufig hat unser Modell richtig geschätzt? Genauer: Wie viele echte 1 hat unser
Modell als 1 vorausgesagt, und wie viele echte 0 hat unser Modell als 0 vorausgesagt?
Berechnen wir Kennzahlen zur Klassifikation, um diese Fragen zu beantworten. Schauen wir zuerst die 598 Studenten an, die in Wirklichkeit nicht bestanden haben (obs=0);
von denen wurden 343 richtig als „nicht bestanden“ (Ereignis 0) klassifiziert; der Rest
wurde fälschlich als „bestanden“ klassifiziert. Bei den 1029 Studenten, die bestanden haben (obs=1), wurden 865 richtig als bestanden klassifiziert; der Rest fälschlich als nicht
bestanden.
19.6.2 Kennzahlen der Klassifikationsgüte
In der Literatur und Praxis herrscht eine Vielfalt an Begriffen zur Klassifikationsgüte,
deswegen stellt Tab. 19.2 einen Überblick vor. Alle Kennwerte haben einen Wertebereich
von 0 bis 1 (0–100 %). Zu beachten ist, dass die Gesamtgenauigkeit einer Klassifikation
an sich wenig aussagekräftig ist: Ist eine Krankheit sehr selten, werde ich durch die einfache Strategie „diagnostiziere alle als gesund“ insgesamt kaum Fehler machen. Meine
Gesamtgenauigkeit wird sehr genau sein – trotzdem lassen Sie sich davon wohl kaum zu
einem Applaus hinreißen. Besser ist, die Richtig-Positiv- und die Richtig-Negativ-Raten
getrennt zu beurteilen. Aus dieser Kombination leitet sich der Youden-Index ab. Er berechnet sich als: RP-Rate + RN-Rate - 1. Der Youden-Index ist etwas Ähnliches wie die
durchschnittliche Genauigkeit von Sensitivität und Spezifität.
Die Gegenüberstellung von dem, was das Modell behauptet (häufig mit pred abgekürzt), und dem, was wahr ist (bzw. was beobachtet wurde; häufig mit obs abgekürzt), nennt man eine Konfusionsmatrix. Sie können die Konfusionsmatrix mit dem Paket
confusion.matrix() aus dem Paket SDMTools berechnen. Das gleiche Paket stellt
auch Funktionen bereit, die die Sensitivität und die Spezifität aus der Konfusionsmatrix
berechnen.
(cm <- confusion.matrix(stats_test$bestanden_num,
glm4$fitted.values))
356
19
Klassifizierende Regression
#>
obs
#> pred
0
1
#>
0 343 164
#>
1 255 865
#> attr(,"class")
#> [1] "confusion.matrix"
SDMTools::sensitivity(cm)
#> [1] 0.841
SDMTools::specificity(cm)
#> [1] 0.574
Das Modell glm4 weist eine recht hohe Sensitivität (0.84) auf. Um die Spezifität ist es
allerdings deutlich schlechter bestellt (0.57). Wie gesagt, es ist ein Leichtes, in einer der
beiden Kennzahlen hohe Werte zu erreichen: „klassifiziere alle Studenten als bestanden“
(die Studenten jubilieren) oder „klassifiziere alle als durchgefallen“ (Prof. Feistersack
lacht schrill).
Prof. Feistersack könnte jetzt argumentieren, dass er im Zweifel lieber eine Person als
Nicht-Besteher einschätzt (um die Lernschwachen noch besser unterstützen zu können,
erklärt Prof. Feistersack). Dazu würden wir den Schwellenwert von 50 % auf z. B. 80 %
heraufsetzen (das geht mit dem Argument threshold = .8).5 Erst bei Erreichen des
Schwellenwerts klassifizieren wir die Beobachtung als „bestanden“ (1).
19.7 Vorhersagen
Eine zentrale Aufgabe der logistischen Regression ist es, einzelne Fälle aufgrund ihrer
Prädiktoren zu klassifizieren: „Aufgrund ihres Profils gehe ich davon aus, dass diese
Person einen Kreditausfall produzieren wird.“ Allgemeiner gesprochen schätzt man das
Kriterium auf Basis der Prädiktoren. Dafür wird in R meist die Funktion predict()
verwendet; so auch für glm(). Nutzen wir das Modell glm4 zur Vorhersage; wir blenden jetzt aus, dass wir den Datensatz schon zur Anpassung des Modells verwendet haben.
Sinnvoller wäre, die Güte des Modells an neuen Daten zu testen.
glm4_pred <- predict(glm4, newdata = stats_test)
head(glm4_pred)
#>
1
2
3
4
5
6
#> 1.4803 2.2732 0.0793 1.7972 0.7144 0.6875
In der Voreinstellung liefert predict() hier Werte in der Standardform der logistischen
Form – in Logits. Übergibt man predict() das Argument type = "response", so
werden die Vorhersagen in der Skalierung der Response-Variablen (d. i. das Kriterium)
ausgegeben. Die Response-Variable ist von 0 bis 1 skaliert; entsprechend versteht man
diese Werte als Wahrscheinlichkeiten:
5
Z. B. cm2 <- confusion.matrix(stats_test$bestanden_num, glm4$fitted.
values, threshold = .8).
19.8 ROC-Kurven und Fläche unter der Kurve (AUC)
357
stats_test$glm4_pred <- predict(glm4, newdata = stats_test, type = "response")
head(glm4_pred)
#>
1
2
3
4
5
6
#> 1.4803 2.2732 0.0793 1.7972 0.7144 0.6875
Möchte man die Wahrscheinlichkeiten bestimmten Klassen zuordnen, so ist etwas Handarbeit gefragt. Sagen wir, unsere Regel ist: „Ist die Wahrscheinlichkeit für „Good“ kleiner
als 50 %, so ordnen wir den Fall der Klasse „Bad“ zu; ansonsten zu „Good“.“ So kann
man diese Zuordnung vornehmen:
stats_test$glm_pred_class <- ifelse(stats_test$glm4_pred < .5, "Bad", "Good")
head(stats_test$glm_pred_class)
#> [1] "Good" "Good" "Good" "Good" "Good" "Good"
count(stats_test, glm_pred_class)
#> # A tibble: 2 x 2
#>
glm_pred_class
n
#>
<chr>
<int>
#> 1 Bad
507
#> 2 Good
1120
Viele R-Nutzer schreiben anstelle von ifelse() gerne so etwas:
stats_test$glm_pred_class <- "Good"
stats_test$glm_pred_class[stats_test$glm4_pred < .5] <- "Bad"
head(stats_test$glm_pred_class)
#> [1] "Good" "Good" "Good" "Good" "Good" "Good"
count(stats_test, glm_pred_class)
#> # A tibble: 2 x 2
#>
glm_pred_class
n
#>
<chr>
<int>
#> 1 Bad
507
#> 2 Good
1120
Beide Wege führen zum gleichen Ergebnis.
19.8 ROC-Kurven und Fläche unter der Kurve (AUC)
Mit Hilfe von ROC-Kurven können wir jetzt viele verschiedene Schwellenwerte vergleichen. Hierzu wird der Grenzwert (Cutpoint) zwischen 0 und 1 variiert und die RichtigPositiv-Rate (Sensitivität) gegen die Falsch-Positiv-Rate (1-Spezifität) abgetragen. Das
Paket pROC hilft uns hier weiter. Zuerst berechnen wir für viele verschiedene Schwellenwerte jeweils die beiden Fehler (Falsch-Positiv-Rate und Falsch-Negativ-Rate). Trägt man
diese in ein Diagramm ab, so bekommt man Abb. 19.3, eine sog. ROC-Kurve.6
6
Von receiver operating characteristic; ein Ausdruck aus dem Funkwesen.
358
19
Klassifizierende Regression
Ein Test ist dann gut, wenn er für alle möglichen Schwellenwerte insgesamt wenig
Fehler produziert.
lets_roc <- roc(response = stats_test$bestanden_num,
predictor = glm4$fitted.values)
Da die Sensitivität determiniert ist, wenn die Falsch-Positiv-Rate bekannt ist (1FP-Rate),
kann man statt Sensitivität auch die FP-Rate abbilden. Für die Spezifität und die FalschNegativ-Rate gilt das Gleiche. In Abb. 19.3 steht auf der X-Achse Spezifität, aber die
Achse ist „rückwärts“ (absteigend) skaliert, so dass die X-Achse identisch ist mit der FPRate (normal skaliert; d. h. aufsteigend). Die Abbildung zeigt die ROC-Kurven von drei
Modellen.
plot(lets_roc, main = "ROC-Kurve von glm4")
lets_roc1 <- roc(response = stats_test$bestanden_num,
predictor = glm1$fitted.values)
#plot(lets_roc1, main = "ROC-Kurve von glm1")
lets_roc3 <- roc(response = stats_test$bestanden_num,
predictor = glm3$fitted.values)
#plot(lets_roc3, main = "ROC-Kurve von glm3")
Die Fläche unter der Kurve (area under curve, AUC) ist ein Maß für die Güte des Tests.
Abb. 19.4 stellt drei Beispiele von Klassifikationsgüten dar: sehr gute (A), gute (B) und
schlechte (C). Ein hohe Klassifikationsgüte zeigt sich daran, dass eine hohe RichtigPositiv-Rate mit einer geringen Fehlalarmquote einhergeht: Wir finden alle Kranken, aber
nur die Kranken. Die AUC-Kurve „hängt oben links an der Decke“. Ein schlechter Klassifikator trifft so gut wie ein Münzwurf: Ist das Ereignis selten, hat er eine hohe FalschPositiv-Rate und eine geringe Falsch-Negativ-Rate. Ist das Ereignis hingegen häufig, liegen die Fehlerhöhen genau umgekehrt: Eine hohe Richtig-Positiv-Rate geht dann mit einer
hohen Falsch-Positiv-Rate einher.
Fragt sich noch, wie man den besten Schwellenwert herausfindet. Den besten Schwellenwert kann man als besten Youden-Index-Wert verstehen. Im Paket pROC gibt es dafür
den Befehl coords(), der uns im ROC-Diagramm die Koordinaten des besten Schwellenwerts und den Wert dieses besten Schwellenwerts liefert:
coords(lets_roc, "best")
#>
threshold specificity sensitivity
#>
0.764
0.911
0.568
1.0
0.8
0.6
0.4
0.2
ROC-Kurve von glm1
0.0
1.0
0.8
0.6
0.4
0.2
ROC-Kurve von glm3
0.0
Sensitivity
Sensitivity
1.0
Abb. 19.3 Eine ROC-Kurve
Sensitivity
0.8
0.6
Specificity
0.2
0.0
0.4
1.0
0.8
0.6
0.2
0.0
0.4
1.0
0.8
0.6
0.4
0.2
0.0
Specificity
1.0
0.8
0.4
Specificity
0.6
0.2
ROC-Kurve von glm4
0.0
19.8 ROC-Kurven und Fläche unter der Kurve (AUC)
359
0.50
0.25
0.10
0.00
0.000.100.25 0.50 0.750.910.00
False positive fraction
B
1.00
0.90
0.75
0.50
0.25
0.10
0.00
0.000.100.25 0.50 0.750.910.00
False positive fraction
True positive fraction
A
1.00
0.90
0.75
True positive fraction
19
True positive fraction
360
Klassifizierende Regression
C
1.00
0.90
0.75
0.50
0.25
0.10
0.00
0.000.100.25 0.50 0.750.910.00
False positive fraction
Abb. 19.4 Beispiel für eine sehr gute (A), gute (B) und schlechte (C) Klassifikation
19.8.1 Cohens Kappa
Ein weiteres gebräuchliches Gütemaß für binäre Klassifikationen ist Cohens Kappa ()
(Kuhn und Johnson 2013). Es beseitigt einen Makel des globalen Richtigkeitsmaßes (Korrektklassifikationsrate): Es setzt die beobachtete Richtigkeit O ins Verhältnis zur erwarteten Richtigkeit E. Unter erwarteter Richtigkeit wird hier verstanden, dass alle Fälle
„zufällig“ einer Kategorie zugordnet werden: Gibt es z. B. zwei Klassen, die jeweils 50 %
der Fälle stellen, so wird eine Korrektklassifkationsrate von 50 % erwartet. Gleicht die
beobachtete Richtigkeit der erwarteten, so resultiert ein Kappa-Wert von 0. Je weiter der
Kappa-Wert von 0 abweicht, desto mehr übertrifft die Richtigkeit des Modells die erwartete Richtigkeit. Werte größer 0 gehen in die erwartete Richtung; Werte kleiner 1 spiegeln
Werte wider, die weniger häufig sind als per Zufall zu erwarten. Die absoluten Maximalwerte liegen bei 1. Sind die Klassen etwa gleich häufig, so liefern Kappa und die
Korrektklassifikationsrate ein ähnliches Ergebnis. Je ungleicher die Klassenhäufigkeiten,
desto mehr weichen Kappa und Korrektklassifikationsrate voneinander ab. Die KappaStatistik ist so definiert:
O E
D
1E
Aufgaben
Richtig oder falsch?7
1.
2.
3.
4.
7
Die logistische Regression ist eine Regression für dichotome Kriterien.
Unter einer OliveOgive versteht man eine „s-förmige“ Kurve.
Berechnet man eine „normale“ (OLS-)Regression bei einem dichotomen Kriterium, so kann man Wahrscheinlichkeiten < 0 oder > 1 erhalten, was unlogisch
ist.
Ein Logit ist definiert als der Einfluss eines Prädiktors in der logistischen Regression. Der Koeffizient berechnet sich als Logarithmus des Wettquotienten.
R, R, R, R, F, R, R, R, F, F.
19.8 ROC-Kurven und Fläche unter der Kurve (AUC)
361
5.
Das AIC ein Gütemaß, welches man bei der logistischen Regression generell
vermeidet.
6. Eine Klassifikation kann vier Arten von Ergebnissen bringen – gemessen an
der Richtigkeit des Ergebnisses.
7. Der positive Vorhersagewert ist definiert als der Anteil aller richtig-positiven
Klassifikationen an allen als positiv klassifizierten Objekten.
8. Man möchte eine einfache logistische Regression (d. h. einen Prädiktor) visualisieren. Stellt man auf der X-Achse den Prädiktor und auf der Y-Achse die
Logits des Kriteriums dar, so wird eine Gerade abgebildet werden, wenn man
die Vorhersagewerte einzeichnet.
9. Cohens Kappa > 1 ist ein Indiz für eine gute Vorhersage.
10. Je größer die Fläche unter der ROC-Kurve, desto schlechter ist die Vorhersage
des Modells.
Aufgaben
1.
2.
3.
4.
5.
Konvertieren Sie das Kriterium von lm1 (bestanden) in eine Faktorvariable
(bestanden_fct) und berechnen Sie das Modell erneut. Kommt es zu einer
Fehlermeldung?8
Wenn es zu keiner Fehlermeldung kommt, wie lautet R2 ? Ist es identisch mit
R2 in lm1?9
Konvertieren Sie das Kriterium in eine logische Variable und berechnen Sie R2
erneut.10
Lassen Sie sich von R die Stufen des Faktors bestanden_fct ausgeben.11
Berechnen Sie den Zuwachs an Wahrscheinlichkeit für unser Beispielmodell,
wenn sich die study_time von 1 auf 2 erhöht. Vergleichen Sie das Ergebnis
mit der Punktprognose für study_time D 7 im Vergleich zu study_time
D 8.
Lösung
wskt1 <- predict(glm1, data.frame(study_time = 1), type = "response")
wskt2 <- predict(glm1, data.frame(study_time = 2), type = "response")
wskt2 - wskt1
#>
1
#> 0.174
8
So
könnte
man
umkodieren:
stats_test %>% mutate(bestanden_fct =
factor(bestanden)) -> stats_test; es resultiert keine Fehlermeldung.
9 2
R ist identisch mit lm1.
10
...bestanden_lgl = bestanden == "ja"; R2 ist identisch zu lm1.
11
levels(stats_test$bestanden_fct) gibt die Stufen dieser Faktor-Variablen aus.
362
19
Klassifizierende Regression
Anders gesagt: „Mit jedem Punkt mehr study_time steigt der Logit (die logarithmierten Chancen) für Bestehen um 0.722.“ Die Erhöhung der BestehensWahrscheinlichkeit zwischen zwei Werten von study_time kann unterschiedlich sein, da die Steigung der S-Kurve nicht immer gleich ist.
# mit dem vollständigen Modell berechnet
predict(glm1, data.frame(study_time = 1),
type = "response")
#>
1
#> 0.332
predict(glm1, data.frame(study_time = 5),
type = "response")
#>
1
#> 0.899
6.
Bei einem Wert von study_time = 4 beträgt die Wahrscheinlichkeit für
y D 1, d. h. für das Ereignis „Bestehen“ 0.81. Bei einem Wert von 5 liegt
diese Wahrscheinlichkeit bei 0.9.
Wenn Sie die Regression von glm4 mit einem Kriterium als Faktorvariable
(bestanden_fct) berechnen; welche Stufe wird als null und welche als eins
verstanden?12
Hilfestellung:
stats_test %>%
mutate(bestanden_fct = factor(bestanden)) -> stats_test
glm4_fct <- glm(bestanden_fct ~ study_time + interessiert + self_eval,
family = binomial,
data = stats_test)
broom::tidy(glm4_fct)
7.
12
Würde ein GLM mit dem Kriterium factor(bestanden_num) – und im
Übrigen keine Unterschiede zu glm4 – zu dem gleichen Ergebnis führen wie
glm4?13
Die Ausgabe von levels(stats_test$bestanden_fct) zeigt, dass „ja“ als null und
„nein“ als eins verstanden wird. Das ist genau umgekehrt wie im Modell mit bestanden_num.
Im Modell glm_fct wird also das Nicht-Bestehen modelliert; Nicht-Bestehen ist hier das zu modellierende Ereignis.
13
Ja, dieses Modell ist identisch mit glm4.
19.8 ROC-Kurven und Fläche unter der Kurve (AUC)
363
8.
Wie kann man R explizit sagen, welche Faktorstufe des Faktors
stats_test$bestanden die Referenzstufe darstellt – und damit die
null in der Regression?14
9. Setzen Sie den Schwellenwert auf .8 herauf; wie verändert sich die Sensitivität
und wie die Spezifität?15
10. Welcher von beiden Schwellenwert ist insgesamt besser?16
11. Berechnen Sie Pseudo-R2 für glm1, glm2 und glm3.17
14
Zuerst
prüfe
man
die
aktuelle
Reihenfolge
der
Faktorstufen:
levels(stats_test$bestanden_fct). Mit dem Befehl relevel() kann man die Referenzstufe definieren; das Argument ref bezeichnet die neue Referenz-Faktorstufe, d. h. die erste
Faktorstufe: stats_test$bestanden <- relevel(stats_test$bestanden_fct,
ref = "nein").
15
(cm2 <- confusion.matrix(stats_test$bestanden_num,
glm3$fitted.values, threshold = .8)); sensitivity(cm2);
specificity(cm2).
16
Der Youden-Index ist bei einem Schwellenwert von .5 besser als bei .8.
17
PseudoR2(glm1);PseudoR2(glm2);PseudoR2(glm3).
Fallstudie: Titanic
20
Lernziele
Praktische Erfahrung mit angewandter Datenanalyse sammeln
Typische Analysen des geleiteten Modellierens anwenden
Lineare Modelle berechnen an einem echten Datensatz
Die typischen Schritte einer Datenanalyse durchlaufen
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(broom)
library(corrplot)
library(compute.es)
library(viridis)
library(mosaic)
data(titanic_train, package = "titanic")
In dieser YACSDA1 geht es um die beispielhafte Analyse nominaler Daten anhand des
„klassischen“ Falls zum Untergang der Titanic. Eine polemische Frage, die sich hier aufdrängt, lautet: Kann (konnte) man sich vom Tod freikaufen? Oder neutraler: Hängt die
Überlebensquote von der Klasse, in der der Passagiers reist, ab? Die zentrale Frage dieser
Fallstudie ist also: Was sind Prädiktoren des Überlebens auf der Titanic? Und: Wie groß
ist der Einfluss der Passagierklasse auf das Überleben? Eine erste Feststellung dazu ist,
dass Überleben eine dichotome Angelegenheit ist; Graustufen gibt es nicht. Dichotome
1
Yet-another-case-study-on-data-analysis.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_20
365
366
20 Fallstudie: Titanic
Variablen werden häufig als numerische Variablen mit den Stufen 0 und 1 kodiert; wenig
überraschend steht 0 für nein und 1 für ja; so auch hier. Hilfe zum Datensatz kann man übrigens mit help(titanic_train) bekommen, wenn man das Paket titanic geladen
hat.
Werfen Sie einen ersten Blick in die Daten mit glimpse(titanic_train), sobald
geladen. Wie viele Variablen und wie viele Fälle gibt es im Datensatz?2 Lassen Sie sich
dann einige deskriptive Statistiken ausgeben;3 gleich betrachten wir die Häufigkeiten genauer.
20.1
Explorative Analyse
Lesen Sie die Beschreibungen der Variablen nach;4 von den zwölf Variablen des Datensatzes interessieren uns offenbar Pclass und Survived. Diese beiden Variablen sind
kategorial (nicht-metrisch), wobei sie in diesem Dataframe mit Zahlen kodiert sind. Natürlich ändert die Art der Codierung (hier als Zahl) nichts am eigentlichen Skalenniveau.
Genauso könnte man „Mann“ mit 1 und „Frau“ mit 2 kodieren; ein Mittelwert bliebe
genauso (wenig) aussagekräftig. Zu beachten ist hier nur, dass sich manche R-Befehle
verunsichern lassen, wenn nominale Variablen mit Zahlen kodiert sind. Daher ist es oft
besser, nominale Variablen mit Text-Werten zu benennen (wie survived vs. drowned c.).
Wir kommen später auf diesen Punkt zurück.
20.1.1 Univariate Häufigkeiten
Bevor wir uns in kompliziertere Fragestellungen stürzen, halten wir fest: Wir untersuchen
zwei nominale Variablen. Sprich: Wir werden Häufigkeiten auszählen. Häufigkeiten (und
relative Häufigkeiten, also Anteile oder Quoten) sind das, was uns hier beschäftigt. Zählen
wir zuerst die univariaten Häufigkeiten aus: Wie viele Passagiere gab es pro Klasse? Wie
viele Passagiere gab es pro Wert von Survived (die also überlebten bzw. nicht überlebten)?
c1
c1
#>
#>
#>
#>
#>
#>
2
<- dplyr::count(titanic_train, Pclass)
# A tibble: 3 x 2
Pclass
n
<int> <int>
1
1
216
2
2
184
3
3
491
12 Variablen bzw. 891 Fälle.
Z. B. mit titanic_train %>% count(Survived) oder titanic_train %>%
summarise(Ticketpreis = mean(Fare, na.rm = TRUE)).
4
Sie finden sie in der Hilfeseite des Datensatzes: help(titanic_train).
3
20.1 Explorative Analyse
367
Man beachte, dass der Befehl count stets einen Dataframe verlangt und zurückliefert.
Wir weisen die Ausgabe von count einer Variable zu, um im Folgenden damit weiterzuarbeiten (genauer gesagt, ein Diagramm zu erstellen).
Aha. Zur besseren Anschaulichkeit können Sie die univariaten Häufigkeiten plotten
(ein Diagramm dazu malen). Wie?5 Zählen Sie als Nächstes, wie viele Menschen überlebt
haben und wie viele gestorben sind.6
Achtung – Namenskollision! Sowohl im Paket mosaic als auch im Paket dplyr
gibt es einen Befehl count(). Für select() gilt Ähnliches – und für eine Reihe
anderer Befehle auch. Das arme R weiß nicht, welchen von beiden wir meinen, und
entscheidet sich im Zweifel für den falschen. Da hilft es, zu sagen, aus welchem
Paket wir den Befehl beziehen wollen. Das macht der Operator ::.
20.1.2
Bivariate Häufigkeiten
Jetzt kennen wir die Häufigkeiten pro Wert von Survived und für Pclass. Eigentlich
interessiert uns aber die Frage, ob sich die relativen Häufigkeiten der Stufen von Pclass
innerhalb der Stufen von Survived unterscheiden. Einfacher gesagt: Ist der Anteil der
Überlebenden in der 1. Klasse größer als in der 3. Klasse? Noch anders gesagt: Gibt es
einen Zusammenhang zwischen Passagierklasse und Überleben? Ist das Überleben von
der Klasse abhängig? Zählen wir zuerst die Häufigkeiten für alle Kombinationen von
Survived und Pclass:
c3
c3
#>
#>
#>
#>
#>
#>
#>
#>
#>
5
<- dplyr::count(titanic_train, Pclass, Survived)
# A tibble: 6 x 3
Pclass Survived
n
<int>
<int> <int>
1
1
0
80
2
1
1
136
3
2
0
97
4
2
1
87
5
3
0
372
6
3
1
119
Z. B. qplot(x = Pclass, y = n, data = c1); Der Befehl qplot zeichnet automatisch Punkte, wenn auf beiden Achsen „Zahlen-Variablen“ stehen (also Variablen, die keinen „Text“,
sondern nur Zahlen beinhalten. In R sind das Variablen vom Typ int (integer), also ganze Zahlen
oder vom Typ num (numeric), also reelle Zahlen).
6
c2 <- dplyr::count(titanic_train, Survived).
368
20 Fallstudie: Titanic
1.00
300
0.50
n
n
0.75
200
0.25
100
0.00
1
2
3
1
factor(Pclass)
factor(Survived)
2
3
factor(Pclass)
0
1
size
5
factor(Survived)
0
Abb. 20.1 Überlebensraten auf der Titanic in Abhängigkeit von der Passagierklasse
Da Pclass drei Stufen hat (1., 2. und 3. Klasse) und es innerhalb jeder dieser drei
Klassen die Gruppe der Überlebenden und der Nicht-Überlebenden gibt, gibt es insgesamt 3 2 D 6 Gruppen. Es ist hilfreich, sich diese Häufigkeiten wiederum zu plotten;
probieren Sie qplot(x = Pclass, y = n, data = c3). Hm, nicht so hilfreich.
Schöner wäre, wenn wir (optisch) erkennen könnten, welcher Punkt für „überlebt“ und
welcher Punkt für „nicht überlebt“ steht. Mit qplot geht das recht einfach: Wir sagen der
Funktion qplot, dass die Farbe (color) und die Form (shape) der Punkte den Stufen
von Survived zugeordnet werden sollen: qplot(x = Pclass, y = n, color =
Survived, shape = Survied, data = c3).
Viel besser. Was noch stört, ist, dass Survived als metrische Variable verstanden
wird. Das Farbschema lässt Nuancen, feine Farbschattierungen, zu. Für nominale Variablen ist das nicht sinnvoll; es gibt da keine Zwischentöne: Tot ist tot. Wir sollten daher der
Funktion sagen, dass es sich um nominale Variablen handelt (s. Abb. 20.1, links).
Besser; die Balkendiagramme zeigen deutlich, dass es einen Zusammenhang zwischen
Passagierklasse und Überleben gibt. Das Punktdiagramm (s. Abb. 20.1, rechts) macht das
weniger deutlich. Fügen Sie noch Titel und Achsenbeschriftungen hinzu.7
20.2 Inferenzstatistik
20.2.1
2 -Test
Wir könn(t)en hier noch einen Signifikanztest berechnen; allerdings stellt sich die Frage,
auf welche Grundgesamtheit wir verallgemeinern wollen. Auf die Population aller jemals
untergegangenen Schiffe? Sei’s drum, berechnen wir einen p-Wert. Es gibt mehrere statistische Tests, die sich hier potenziell anböten und unterschiedliche Ergebnisse liefern
7
p2 + labs(x = "Klasse", title = "Überleben auf der Titanic", fill
= "Überlebt?", y = Änteil").
20.2 Inferenzstatistik
369
können (was die Frage nach der Objektivität von statistischen Tests in ein ungünstiges
Licht rückt (Briggs 2008)). Nehmen wir den 2 -Test, in der Version des Pakets mosaic.
xchisq.test(Survived ~ Pclass, data = titanic_train)
#>
#> Pearson's Chi-squared test
#>
#> data: tally(x, data = data)
#> X-squared = 100, df = 2, p-value <2e-16
#>
#>
80
97
372
#> (133.09) (113.37) (302.54)
#> [21.18] [ 2.36] [15.95]
#> <-4.60> <-1.54> < 3.99>
#>
#>
136
87
119
#> ( 82.91) ( 70.63) (188.46)
#> [34.00] [ 3.80] [25.60]
#> < 5.83> < 1.95> <-5.06>
#>
#> key:
#> observed
#> (expected)
#> [contribution to X-squared]
#> <Pearson residual>
Der p-Wert ist kleiner als 5 % (2.2e-16), daher entscheiden wir uns, entsprechend der
üblichen Gepflogenheit, gegen die H0 und zugunsten der H1 : „Es gibt einen Zusammenhang von Überlebensrate und Passagierklasse.“ Präziser wäre natürlich zu sagen: „Daten,
die mindestens so extrem sind wie diese, sind selten, wenn die H0 richtig sein sollte, daher
glauben wir nicht an die H0 , sondern tun so, als ob die H1 richtig ist.“ Natürlich kann sich
das kein Mensch merken. Hinweise zur Interpretation des 2 -Tests bzw. der R-Ausgabe
finden sich im Abschn. 16.4.
20.2.2
Effektstärke
Abgesehen von der Signifikanz kann und sollte man untersuchen, wie stark die Variablen zusammenhängen. Für Häufigkeitsanalysen mit 2 2-Feldern bietet sich das Odds
Ratio (OR; Chancenverhältnis) an. Diese Statistik beantwortet die Frage: „Um welchen
Faktor ist die Überlebenschance in der einen Klasse größer als in der anderen Klasse?“.
Eine interessante Frage; gehen wir der Sache nach. Das OR ist standardgemäß definiert
für 2 2-Häufigkeitstabellen, daher müssen wir die Anzahl der Passagierklassen von 3
auf 2 verringern. Nehmen wir nur 1. und 3. Klasse, um den vermuteten Effekt deutlich
herauszuschälen:
t2 <- filter(titanic_train, Pclass != 2)
# "!=" heißt "nicht"
370
20 Fallstudie: Titanic
Alternativ (synonym) könnten wir auch schreiben:
t2 <- filter(titanic_train, Pclass == 1 | Pclass == 3)
# "|" heißt "oder"
Und dann zählen wir zur Sicherheit wieder die Häufigkeiten aus pro Gruppe von Pclass:
(c4 <- dplyr::count(t2, Pclass))
#> # A tibble: 2 x 2
#>
Pclass
n
#>
<int> <int>
#> 1
1
216
#> 2
3
491
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
Pearson's Chi-squared test with Yates' continuity correction
data: tally(x, data = data)
X-squared = 100, df = 1, p-value <2e-16
80
372
(138.09) (313.91)
[24.02] [10.57]
<-4.94> < 3.28>
136
119
( 77.91) (177.09)
[42.58] [18.73]
< 6.58> <-4.37>
key:
observed
(expected)
[contribution to X-squared]
<Pearson residual>
Berechnen Sie nochmal p-Wert an, da wir jetzt ja mit einer veränderten Datentabelle operieren.8 Dann berechnen wir die Effektstärke (OR) mit dem Paket compute.es (es muss
ebenfalls installiert und geladen sein). Die Funktion chies() berechnet die Effektstärke
(„es“) für einen 2 -Wert; die Stichprobengröße n wird ebenfalls benötigt:
t2_or <- compute.es::chies(chi.sq = 96, n = 711)
Das OR beträgt also etwa 4.18. Die Chance zu überleben ist damit in der 1. Klasse etwa 4mal so hoch wie in der 3. Klasse. Es scheint: Mit Geld kann man viel kaufen. Netterweise
gibt die Funktion auch das Konfidenzintervall zum OR an.
t2_chi <- xchisq.test(Survived ~ Pclass, data = t2); Ein 2 -Wert von 96
resultiert. Mit sum(c4) kann man sich die Stichprobengröße ausgeben lassen.
8
20.2 Inferenzstatistik
371
Betrachten wir noch die Syntax der Visualisierung (s. Abb. 20.1).
qplot(x = factor(Pclass),
y = n,
color = factor(Survived),
shape = factor(Survived),
data = c3) -> p1
ggplot(c3, aes(x = factor(Pclass),
y = n,
fill = factor(Survived))) +
geom_col(position = "fill") +
theme(legend.position = "bottom") +
scale_fill_viridis(discrete = TRUE) -> p2
In beiden Plot-Objekten (p1 und p2) wurde als Datensatz c3 verwendet; dort sind bereits
aggregierte Häufigkeiten vorhanden. Die Plot-Funktion muss also keine Zusammenfassung berechnen, sondern das Geom einfach den Werten zuordnen, die in c3 aufgeführt
sind. Zu beachten ist, dass ggpplot2 nur dann die Geome einfärbt, wenn die zugeordnete Farbe nicht numerisch, sondern kategorial (vom Typ Faktor oder Text) ist. Im ersten
Plot wurden als Geom Balken verwendet, im zweiten Punkte. geom_col() erwartete
bereits aggregierte Häufigkeiten, so wie sie in c3 vorliegen. Im Unterschied dazu würde
geom_bar() Rohdaten erwarten (in der Voreinstellung zumindest) und das Zusammenzählen selber vornehmen. Mit position = "fill" wird die Höhe der Balken auf 1
vereinheitlicht; das bietet sich immer an, wenn man die „Füllstände“ vergleichen möchte.
Das Farbschema viridis ist eine Alternative zum Standard-Farbschema von ggplot2
und ist für Schwarz-Weiß-Druck besser geeignet.9
Eine alternative Darstellung zeigt Abb. 20.2 (links). Hier werden die vier „Fliesen“
gleich groß dargestellt; die (relative) Fallzahl wird durch die Füllfarbe dargestellt. Hier
wurde vorab der Anteil der Überlebenden pro Passagierklasse berechnet, um den Vergleich der Überlebenswahrscheinlichkeit pro Gruppe zu ermöglichen. Dafür wurde mit
group_by(Pclass) pro Passagierklasse eine Gruppe gebildet. Wie üblich werden dann
die folgenden Befehle pro Gruppe ausgerechnet. Hier war der nächste Befehl die Berechnung des Anteils – aufgrund der vorherigen Gruppierung der Anteile pro Passagierklasse.
c3 %>%
group_by(Pclass) %>%
mutate(prop = n / sum(n)) -> c3_grouped
c3_grouped %>%
ggplot +
aes(x = factor(Pclass), y = factor(Survived), fill = prop) +
geom_tile() #+ scale_fill_viridis()
9
Auf der Webseite von Cynthia Brewer kann man sich Farbpaletten zusammenstellen lassen, auch
nach der Maßgabe kopiererfreundlich (s. auch Abschn. 12.1.1).
372
20 Fallstudie: Titanic
0.00
1
0.7
0.6
0.5
0.4
0
0.3
1
2
3
Survived_diff
factor(Survived)
prop
-0.05
-0.10
-0.15
-0.20
1
factor(Pclass)
2
3
factor(Pclass)
Abb. 20.2 Überlebenshäufigkeiten anhand eines Fliesendiagramms dargestellt
Noch eine alternative Darstellungsweise zielt auf die Unterschiede der Überlebenswahrscheinlichkeit ab; das hebt den Effekt der Stufen des Prädiktors hervor (s. Abb. 20.2
(rechts)). Diese Art der Abbildung könnte geeignet sein, wenn es darum geht, die Stärke des Zusammenhangs bzw. die Höhe des Einflusses des Prädiktors zu untersuchen. In
diesem Fall dient die 1. Klasse als Referenzstufe (wir könnten die 1. Klasse daher auch
nicht abdrucken); die 2. Klasse weist im Vergleich zur 1. Klasse eine um ca. 15 % geringere Überlebensrate auf; in der 3. Klasse sind es mehr als 20 % weniger als in der 1.
Klasse.
c3 %>%
group_by(Pclass) %>%
mutate(prop = n / sum(n)) %>%
ungroup() %>%
filter(Survived == 1) %>%
mutate(Survived_diff = prop - lag(prop)) %>%
replace_na(list(Survived_diff = 0)) %>%
ggplot(aes(x = factor(Pclass), y = Survived_diff)) +
geom_point()
Die Syntax der Abb. 20.2 (rechts) ist komplizierter und deshalb ausführlicher erläutert:
Nimm den Datensatz c3 UND DANN
Gruppiere nach Passagierklassen UND DANN
berechne für jede Klasse die Überlebenswahrscheinlichkeit UND DANN
löse die Gruppierung wieder auf, die folgenden Befehle (mutate) sollen keine
Gruppierung berücksichtigen UND DANN
filtere nur die Werte für Überleben, nicht für Sterben, da das Diagramm nur Überlebenswahrscheinlichkeit darstellen soll UND DANN
20.2 Inferenzstatistik
373
berechne den Unterschied zwischen einer Klasse und der vorherigen (also z. B. 2
vs. 1) UND DANN
Ersetze NA in der Spalte Survived_diff durch 0 UND DANN
plotte das Diagramm.
lag() gibt den Wert des vorherigen Elements eines Vektors zurück. Man kann es gut
verwenden, um die Veränderung von „gestern“ zu „heute“ zu berechnen:
x <- c(10, 20, 30)
lag(x)
#> [1] NA 10 20
20.2.3 Logistische Regression
Modellieren wir zum Abschluss noch die Überlebenswahrscheinlichkeit als Funktion der
Passagierklasse. Ignorieren Sie fürs Erste die folgende Syntax und schauen Sie sich die
Abb. 20.3 an. Hier sehen wir die (geschätzten) Überlebenswahrscheinlichkeiten p für Passagiere der 1. Klasse vs. Passagiere der 2. vs. der 3. Klasse.
glm_titanic <- glm(data = titanic_train,
formula = Survived ~ Pclass,
family = "binomial")
coef(glm_titanic)
#> (Intercept)
Pclass
#>
1.45
-0.85
exp(coef(glm_titanic))
#> (Intercept)
Pclass
#>
4.249
0.427
titanic_train$pred_prob <- predict(glm_titanic, type = "response")
Wir sehen, dass die Überlebenswahrscheinlichkeit p in der 1. Klasse höher ist als in der
3. Klasse. Optisch grob geschätzt, 60 % in der 1. Klasse und 25 % in der 3. Klasse. Der
Graph ist keine Gerade, sondern leicht gebogen. Die „reine Form“ der Ogive kommt in
diese Daten nicht prägnant zum Vorschein.
Gehen wir im nächsten Schritt durch die Syntax und die Ausgabe der logistischen Regression von glm_titanic: Zuerst haben wir mit glm und family = "binomial"
eine logistische Regression angefordert. Man beachte, dass der Befehl sehr ähnlich der
normalen Regression (lm) ist. Die Einflussgewichte der Prädiktoren bzw. des Prädiktors
sowie des Achsenabschnittes (Intercept) kann man sich mit coef() ausgeben lassen.
Da die Koeffizienten in der Logit-Form zurückgegeben werden, haben wir sie mit der
374
20 Fallstudie: Titanic
1.0
p
0.8
0.6
0.4
0.2
0.0
1
2
3
Pclass
Abb. 20.3 Logistische Regression zur Überlebensrate nach Passagierklasse
Exponential-Funktion exp() in die „normale“ Odds-Form gebracht (delogarithmiert); es
resultiert eine Chance (c D p=.1 p/). Wir sehen, dass die Überlebenschance etwa um
den Faktor 0.43 sinkt pro zusätzlicher Stufe der Passagierklasse. Der Achsenabschnitt
(Intercept) sagt uns: Würde jemand in der „nullten“ Klasse fahren, wäre seine Überlebenschance ca. 4.25. In jedem linearen Modell (d. h. in jeder Regression) gilt y D
b0 C b1 x1 C : : : Nimmt man die Überlebenschance als y, so ist die Überlebenschance in der 1. Klasse etwa 4:25 0:43, also 1.83; grob 2 : 1. Alternativ können wir nach
dem gleichen Prinzip den „Überlebenslogit“ L berechnen: L D 1:45 0:85 D 0:60;
logarithmiert man diesen Wert, so resultiert wieder die Überlebenschance. Was ist dann
die Überlebenswahrscheinlichkeit p? Ein Beispiel: Angenommen, die Überlebenschance c beträgt 2 (d. h. 2 : 1). Wenn auf zwei Menschen, die überleben, einer kommt, der
ertrinkt, so überleben zwei von drei. Die Überlebenswahrscheinlichkeit liegt dann bei
c
. Komfortabler können wir uns die Überlebenswahrscheinlichkeiten mit
2=3: p D 1Cc
der Funktion predict ausgeben lassen.
predict(glm_titanic, newdata = data.frame(Pclass = 1), type = "response")
#>
1
#> 0.645
predict(glm_titanic, newdata = data.frame(Pclass = 2), type = "response")
#>
1
#> 0.437
predict(glm_titanic, newdata = data.frame(Pclass = 3), type = "response")
#>
1
#> 0.249
Ergänzend kann man die tatsächlichen (beobachteten) Häufigkeiten auch noch „per Hand“
bestimmen. Der Unterschied von beobachtet und geschätzt (vom Modell) gibt Hinweise
auf die Modellgüte.
tally(Survived ~ Pclass, data = titanic_train,
format = "proportion")
20.2 Inferenzstatistik
#>
Pclass
#> Survived
1
2
3
#>
0 0.370 0.527 0.758
#>
1 0.630 0.473 0.242
Aufwändiger (aber dafür in Form eines Tidy-Dataframes) geht es mit dplyr:
titanic_train %>%
dplyr::select(Survived, Pclass) %>%
group_by(Pclass, Survived) %>%
summarise(n = n() ) %>%
mutate(Anteil = n / sum(n)) -> c5
c5
#> # A tibble: 6 x 4
#> # Groups:
Pclass [3]
#>
Pclass Survived
n Anteil
#>
<int>
<int> <int> <dbl>
#> 1
1
0
80 0.370
#> 2
1
1
136 0.630
#> 3
2
0
97 0.527
#> 4
2
1
87 0.473
#> 5
3
0
372 0.758
#> 6
3
1
119 0.242
Übersetzen wir diese Syntax auf Deutsch:
Nimm den Datensatz „titanic_train“ UND DANN
Filtere nur die 1. und die 3. Klasse heraus UND DANN
wähle nur die Spalten „Survived“ und „Pclass“ UND DANN
gruppiere nach „Pclass“ und „Survived“ UND DANN
zähle die Häufigkeiten für jede dieser Gruppen aus UND DANN
berechne den Anteil an Überlebenden bzw. Nicht-Überlebenden
für jede der beiden Passagierklassen.
Aufgaben
Richtig oder falsch?10
1.
10
Um die Überlebenschancen zu berechnen, hätte man anstelle von count()
auch mosaic::tally() oder sjmisc::frq() verwenden können.
R, R, R, F, F, F, R, R, R, F.
375
376
2.
20 Fallstudie: Titanic
Ist diese Syntax korrekt sjmisc::frq(titanic_train, Pclass), um
die Überlebenschancen zu berechnen?
3. Die Analyse bivariater Häufigkeitsverteilungen wird häufig als Analyse von
Kontingenztabellen bezeichnet.
4. Balkendiagramme sollten nie zur Darstellung von Häufigkeiten verwendet werden.
5. Balkendiagramme sollten nie zur Darstellung von Mittelwerten verwendet werden.
6. 2 -Quadrat-Tests werden nicht nur Analyse nominaler Zusammenhänge verwendet.
7. Ein Effektstärkemaß für nominale Zusammenhänge ist Odds Ratio.
8. Ein Odds Ratio von 0.1 zeigt einen gleich starken Zusammenhang an wie ein
Odds Ratio von 10.
9. Ein Odds Ratio von 1.0 zeig keinen Zusammenhang an.
10. Eine logistische Regression darf nur binäre (dichotome) Prädiktoren beinhalten.
21
Baumbasierte Verfahren
Lernziele
Die wichtigsten Varianten baumbasierter Modelle kennen und unterscheiden
können
Die grundlegende Funktionsweise (den Algorithmus) von Entscheidungsbäumen
erklären können
Die Erweiterung von Entscheidungsbäumen zu Bagging und Random Forests
kennen
Um die Stärken und Schwächen von baumbasierten Verfahren wissen
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(rpart)
library(partykit)
library(caret)
library(gridExtra)
library(mosaic)
data(Affairs, package = "AER")
In diesem Kapitel betrachten wir Varianten von baumbasierten Verfahren. Dazu zählen
Entscheidungsbäume, Bagging und Random Forests („Zufallswälder“) – neben weiteren
Familienmitgliedern, die wir außen vor lassen (s. James et al. (2013); Hastie et al. (2013)
für eine vertiefte Darstellung). Baumbasierte Verfahren sind sowohl für die Klassifikation
(nominale Kriterien) als auch für numerische Vorhersagemodelle (quantitative Kriterien) einsetzbar. Technisch bedeutet das zumeist, dass Kriterien vom Typ factor oder
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_21
377
378
21 Baumbasierte Verfahren
character eine Klassifikation auslösen, wohingegen numerische Kriterien eine Regression nach sich ziehen. In diesem Kapitel betrachten wir ein Klassifikationsmodell. Als
Prädiktoren können bei baumbasierten Modellen sowohl numerische als auch nominale
Variablen verwendet werden.
21.1 Entscheidungsbäume
Entscheidungsbäume stellen, allgemein gesprochen, Entscheidungen und deren Konsequenzen in einer baumähnlichen Struktur dar. Dabei stellen die Prüfungen die „Astgabeln“
oder „Knoten“ (nodes) dar, die „Äste“ die Entscheidungen der Prüfungen, und die „Blätter“ des Baumes repräsentieren die Entscheidung des Modells; am besten sieht man dies in
einem Bespiel (s. Abb. 21.1). Entscheidungsbäume können sowohl für Klassifikations- als
auch für numerische Vorhersagemodelle verwendet werden. Ein Sammelbegriff für solche
Modelle ist CART (Classification and Regression Trees) (Breiman et al. 1984).
21.1.1 Einführendes Beispiel
Betrachten wir ein einfaches Beispiel; zuerst ist etwas Datenjudo nötig. Wir trennen einen
Datensatz in eine Trainings- und eine Test-Stichprobe auf. Als Datenbeispiel dient uns
der Affären-Datensatz; Ziel unserer Analyse soll sein, anhand der verfügbaren Variablen
vorherzusagen, ob jemand außereheliche Affären eingegangen ist oder nicht. Dazu erstellen wir eine binäre Zielvariable (nennen wir sie is_halodrie). Dabei dürfen wir nicht
vergessen, die Variable affairs zu löschen, nachdem wir is_halodrie erstellt haben,
sonst bliebe ja die vorherzusagende Information im Datensatz.
Erstellt man ein Modell, um eine Zielgröße zu erklären bzw. vorauszusagen, so
sollte man prüfen, ob nicht „unerlaubte“ Informationen als Prädiktoren verwendet
werden. Möchten wir z. B. vorhersagen, ob jemand ein Hallodri ist, so darf die Anzahl der eingegangenen Affären nicht als Prädiktor verwendet werden. Das hört sich
trivial an, aber es kann subtilere Fälle solcher Informationslecks geben (vgl. Kap. 7,
erster Absatz).
set.seed(42) # Zufallszahlen festlegen
index_train <- sample(x = 1:601,
size = trunc(.8 * 601))
Affairs <- Affairs %>%
mutate(is_halodrie = ifelse(affairs > 0, "ja", "nein"),
is_halodrie = as.factor(is_halodrie))
21.1 Entscheidungsbäume
379
train_df <- Affairs %>%
filter(row_number() %in% index_train) %>%
select(-affairs)
test_df <- Affairs %>%
filter(!row_number() %in% index_train)
rm(index_train)
# Das Objekt wieder löschen, da nicht mehr benötigt
Das Kriterium is_halodrie haben wir als Faktor definiert, weil die Funktionen, die wir
nutzen werden, anhand des Typs der Variable entscheiden, ob eine Klassifikation oder Regression durchgeführt wird: Bei Faktoren kommt eine Klassifikation zum Einsatz und bei
numerischen Werten eine Regression. index_train erstellt eine Liste von Fällen, die
zur Trainingsstichprobe gehören sollen. filter() wählt dann die Zeilen (Fälle) aus, die
in dieser Liste enthalten sind (für die Trainingsstichprobe) bzw. nicht enthalten sind (für
die Test-Stichprobe). Nach dieser Vorarbeit lassen wir uns den Entscheidungsbaum berechnen (mit der Funktion rpart::rpart()) und plotten (mit partykit::plot()).
Betrachten wir der Einfachheit halber erst einmal einen Baum mit nur zwei Prädiktoren
(Ehezufriedenheit und Geschlecht).
baum1 <- rpart(is_halodrie ~ rating+gender, data = train_df)
plot(as.party(baum1))
Der Baum (s. Abb. 21.1) ist recht einfach: Er besteht nur aus einem Knoten (dem „Wurzelknoten“; Knoten 1) und zwei Blättern (Endknoten; Knoten 2 und 3). Die einzige Prüfung,
die das Modell durchgeführt hat, ist, ob die Ehezufriedenheit kleiner als 2.5 ist oder nicht.
Wie kommt unser Baum-Modell darauf? Warum diese Variable (und nur diese) und dieser Schwellenwert? Hier zur Einführung nur so viel: Dieser Wert trennt Hallodris von
Nicht-Hallodris offenbar am deutlichsten (zumindest ist das die Meinung des Modells);
mehr dazu in Abschn. 21.3. In der Gruppe der Unzufriedenen (rating<2.5) liegt der
Hallodri-Anteil bei ca. 60 %; in der Gruppe der Zufriedenen bei ca. 20 %. Hätten wir keine Unterteilung durchgeführt, hätten wir einen Hallodri-Anteil von 25 % festgestellt:
train_df %>%
count(is_halodrie) %>%
mutate(prop = n / sum(n))
#> # A tibble: 2 x 3
#>
is_halodrie
n prop
#>
<fct>
<int> <dbl>
#> 1 ja
122 0.254
#> 2 nein
358 0.746
Durch diese Aufsplittung (auch Gruppierung oder (Partionierung) genannt) anhand einer
Prüfung (rating<2.5) in zwei Gruppen haben wir „reinere“, also homogenere Gruppen
bekommen als bei Betrachtung der Hallodri-Häufigkeit ohne Partionierung. Bei einer per-
380
21 Baumbasierte Verfahren
1
rating
< 2.5
Node 3 (n = 417)
ja
1
0.8
0.6
0.4
0.2
0
nein
nein
ja
Node 2 (n = 63)
≥ 2.5
1
0.8
0.6
0.4
0.2
0
Abb. 21.1 Ein einfacher Entscheidungsbaum, der die Stichprobe in zwei Gruppen aufteilt
fekten Klassifizierung fänden sich in einer Gruppe nur Hallodris und in der anderen nur
treue Herzen (Nicht-Hallodris).
Eine homogenere Teilmenge, d. h. ein sehr hoher oder ein sehr geringer HallodriAnteil, lässt uns sichere (genauere) Vorhersagen treffen, erhöht unser Wissen. Es ist
erstrebenswert, den Datensatz anhand der Prädiktoren so aufzuteilen, dass homogene Gruppen hinsichtlich des Kriteriums entstehen. Entscheidungsbäume wählen bei
jeder Astgabelung den Prädiktor und den Trennwert, der die Homogenität insgesamt
erhöht.
Interessanterweise wurde der Prädiktor gender nicht in das Modell aufgenommen.
Der Grund ist, dass dieser Prädiktor die Homogenität der Gruppen nicht (ausreichend)
erhöht. Wir lernen daraus, dass gender in diesem Fall nicht viel (zusätzliche) Information birgt. Alternative Visualisierungen für diesen einfachen Entscheidungsbaum sind
im linken Teil der Abb. 21.2 dargestellt. Die Y-Achse zeigt den wahren Hallodri-Status
(is_halodrie) an, die Form der Punkte den vom Modell geschätzten Hallodri-Status
(pred_halodrie). Die gestrichelte Linie gibt den Grenzwert an, der zwischen den beiden Klassen den geschätzten Hallodri-Status (FALSE, TRUE) unterteilt. Der rechte Teil
der Abbildung visualisiert einen komplexeren Entscheidungsbaum, wie er in Abb. 21.3
dargestellt ist. Erweitern wir das Beispiel und lassen alle Prädiktoren des (Trainings-)
Datensatzes zu; wie sich wohl der Baum verändert (s. Abb. 21.3)?
baum2 <- rpart(is_halodrie ~ ., data = train_df)
plot(as.party(baum2))
Betrachten wir unseren Baum in Abb. 21.3; die „Äste“ zeigen nach unten; der Baum steht
Kopf. Phantasievolle Mitmenschen sehen statt Ästen Wurzeln, dann stimmt die Ausrich-
21.1 Entscheidungsbäume
381
female
nein
male
age
is_halodrie
50
40
30
ja
20
1
2
3
4
5
1
2
3
4
rating
pred_halodrie
5
1
2
3
4
5
rating
FALSE
is_halodrie
TRUE
ja
n
nein
10
20
Abb. 21.2 Alternative Visualisierungen für Entscheidungsbäume
1
rating
< 2.5
≥ 2.5
2
age
< 44.5
≥ 44.5
3
age
≥ 29.5
< 29.5
5
gender
male female
ja
ja
ja
ja
ja
nein
nein
nein
nein
nein
Node 4 (n = 32)Node 6 (n = 8)Node 7 (n = 14)Node 8 (n = 9)Node 9 (n = 417)
1
1
1
1
1
0.8
0.8
0.8
0.8
0.8
0.6
0.6
0.6
0.6
0.6
0.4
0.4
0.4
0.4
0.4
0.2
0.2
0.2
0.2
0.2
0
0
0
0
0
Abb. 21.3 Ein komplexerer Entscheidungsbaum
382
21 Baumbasierte Verfahren
tung des Baumes wieder . . . Das Modell baum2 gibt also folgendes Regelwerk (Algorithmus) zur Entscheidung wieder, ob jemand Hallodri ist:
Prüfung: Ist die Ehezufriedenheit größer oder gleich 2.5? (Astgabel 1)
Wenn die Entscheidung „ja“ lautet, ist die Hallodri-Quote (Konsequenz) ca. .20.
(Blatt 9)
Ansonsten Prüfung: Ist das Alter größer oder gleich 44.5? (Astgabel 2)
Wenn ja, ist die Hallodri-Quote ca. .20. (Blatt 8)
Ansonsten Prüfung: Ist das Alter größer oder gleich 29.5? (Astgabel 3)
Wenn ja, ist die Hallodri-Quote ca. .75. (Blatt 4)
Ansonsten Prüfung: Ist es ein Mann? (Astgabel 5)
Wenn ja, ist die Hallodri-Quote ca. .75. (Blatt 6)
Ansonsten ist die Hallodri-Quote ca. .30. (Blatt 7)
Es handelt sich also um ein einfaches Entscheidungsregelwerk; man denke an einen
Arzt in der Notaufnahme: Ist Puls da? Wenn nein, beginne Wiederbelebung. Ansonsten Prüfung: Ist Atmung erkennbar usw. Eine praktische, gut verständliche und schön
visualisierbare Angelegenheit. Die Blätter des Baumes (die Knoten ohne weitere Verzweigungen) spiegeln den Anteil der Hallodris wider, die den Weg bis zu diesem Blatt
durchlaufen haben. Anstelle des Graphen können wir uns natürlich auch die numerischen
Werte ausgeben lassen:
baum2
#> n= 480
#>
#> node), split, n, loss, yval, (yprob)
#>
* denotes terminal node
#>
#> 1) root 480 122 nein (0.254 0.746)
#>
2) rating< 2.5 63 28 ja (0.556 0.444)
#>
4) age< 44.5 54 21 ja (0.611 0.389)
#>
8) age>=29.5 32
9 ja (0.719 0.281) *
#>
9) age< 29.5 22 10 nein (0.455 0.545)
#>
18) gender=male 8
2 ja (0.750 0.250) *
#>
19) gender=female 14
4 nein (0.286 0.714) *
#>
5) age>=44.5 9
2 nein (0.222 0.778) *
#>
3) rating>=2.5 417 87 nein (0.209 0.791) *
So liest sich die Ausgabe: An der „Wurzel“ (root) des Baumes (Astgabel/Knoten 1) haben
wir 480 Fälle, von denen 122 Hallodris sind, was 25.4 % entspricht. Die Mehrheit sind also
keine Hallodris; unser Tipp bzw. unsere Vorhersage (yval) an diesem Punkt ist also „nein“
(kein Hallodri). Der Ast 2) mit rating< 2.5 führt zu einem Blatt mit n D 63 Observationen; wobei die Mehrheit (55 %) Hallodris sind. Entsprechend werden alle Fälle, die
21.1 Entscheidungsbäume
383
in diese Gruppe fallen, als Hallodris abgestempelt (ja). Dabei tun wir 28 Menschen Unrecht (loss). Loss bzw. Verlieren heißt, dass die Klassifikation falsch ist, da die verlorenen
Fälle nicht der Mehrheit angehören und die Vorhersage immer der Mehrheitskategorie
entspricht Für diese Gruppe wäre die Vorhersage nein, kein Hallodri. Leider passt die
Nummer der Knoten in der Abb. 21.3 nicht zur Nummer der Knoten der Textausgabe des
Objekts baum2. Die Richtigkeit der Ergebnisse können wir in gewohnter Manier nachprüfen:
count(train_df, is_halodrie)
#> # A tibble: 2 x 2
#>
is_halodrie
n
#>
<fct>
<int>
#> 1 ja
122
#> 2 nein
358
count(train_df, rating>=2.5)
#> # A tibble: 2 x 2
#>
`rating >= 2.5`
n
#>
<lgl>
<int>
#> 1 FALSE
63
#> 2 TRUE
417
# Hallodris insgesamt
# Prüfung in Knoten 1
train_df %>%
# Prüfung in Knoten 9
filter(rating>=2.5) %>%
count(is_halodrie)
#> # A tibble: 2 x 2
#>
is_halodrie
n
#>
<fct>
<int>
#> 1 ja
87
#> 2 nein
330
21.1.2
Tuningparameter
Abb. 21.2 (rechter Teil) zeigt eine alternative Visualisierung für baum2. Auch in diesem Baum fanden nicht alle Variablen Eingang, da sie die Homogenität nicht (ausreichend) verbesserten. Was „ausreichend“ ist, wird durch einen sog. Komplexitätsparameter (cp) gesteuert; in der Voreinstellung liegt dieser bei 1 % Verbesserung im Vergleich zum vorherigen Knoten. Ist die Verbesserung der Homogenität geringer als dieser Wert (cp), so wird der Split nicht durchgeführt. Mit printcp(baum2) kann man
sich den Einfluss verschiedener Werte des Komplexitätsparameters ausgeben lassen. Mit
plotcp(baum2) bekommt man ein Diagramm dazu zurückgeliefert. Mit dem Argument
control = rpart.control(cp = .05) kann man den Komplexitätsparameter auf
z. B. 5 % setzen:
baum5 <- rpart(is_halodrie ~ ., data = train_df,
control = rpart.control(cp = .05))
384
21 Baumbasierte Verfahren
Für Entscheidungsbäume ist der Komplexitätsparameter eine Art Hebel, der die Arbeitsweise der Maschine beeinflusst. Man spricht von Tuningparametern. Es gibt
keinen vorab bekannten „richtigen“ Wert für einen Tuningparameter; der Anwender entscheidet über den Wert. Verschiedene Werte von Tuningparametern dürfen
ausprobiert werden, solange dafür nur die Trainingsstichprobe, nicht die TestStichprobe, verwendet wird. Dann verwendet man den Wert des Tuningparameters,
der im Trainings-Sample zu den besten Ergebnissen führte, im Test-Sample. Im
Kap. 22 betrachten wir ein praktisches Beispiel zur Wahl von Tuningparametern.
21.2 Entscheidungsbäume mit caret
Das R-Paket caret bietet eine einheitliche Syntax – eine Idee analog zum Paket mosaic
– für viele statistische Modelle; im Kap. 22 setzen wir uns genauer mit caret auseinander. Ein Vorteil dieses Pakets, den wir hier auch schon nutzen, ist, dass Kreuzvalidierungstechniken einfach mit caret bewerkstelligt werden können (vgl. Abschn. 15.9). Berechnen wir also baum3, ein Entscheidungsbaum-Modell mit dem Kriterium is_halodrie,
das alle Variablen als (potenzielle) Prädiktoren berücksichtigt. caret probiert für uns
auch in der Voreinstellung einige Werte der oder (in diesem Fall) des Tuningparameters
aus.
my_train_control <- trainControl(
method = "cv",
number = 10)
baum3 <- train(is_halodrie ~ .,
data = train_df,
method = "rpart",
trControl = my_train_control)
baum3
#> CART
#>
#> 480 samples
#>
8 predictor
#>
2 classes: 'ja', 'nein'
#>
#> No pre-processing
#> Resampling: Cross-Validated (10 fold)
#> Summary of sample sizes: 432, 431, 432, 432, 432, 432, ...
#> Resampling results across tuning parameters:
#>
#>
cp
Accuracy Kappa
#>
0.0246 0.764
0.2210
21.2 Entscheidungsbäume mit caret
385
#>
0.0410 0.739
0.1311
#>
0.0574 0.731
0.0547
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was cp = 0.0246.
Mit dieser Syntax wurde für die Trainingsstichprobe in zehn Kreuzvalidierungsstichproben jeweils ein Baum-Modell berechnet. Die Kreuzvalidierung wurde mit dem Argument
trControl in train() bzw. mit der Funktion trainControl festgelegt. Jedes Mal
wurde am Zehntel der Trainingsstichprobe, das nicht in das Modell einging, die Modellgüte berechnet (s. Abschn. 15.9). Dann hat die Funktion train() den Baum mit der besten
Vorhersagegüte ausgewählt. Diesen werden wir gleich verwenden, um am bisher nicht
angerührten Test-Sample test_df die Vorhersagegüte zu berechnen. Die Ausgabe zeigt
u. a., welches Modell berechnet wurde (CART), welche Resampling-Technik zum Einsatz
kam (zehnfache Kreuzvalidierung) und welche Werte des Komplexitätsparameters (cp),
der einzige Tuningparameter bei Entscheidungsbäumen, verwendet wurde; weiter werden
Gütekennzahlen dazu aufgeführt. Weitere Gütekennzahlen sind im Dataframe results,
Teil des caret-Modells, in diesem Fall baum3, abgelegt; das beste Modell ist im Objekt
finalModel beschrieben. Mit summary(baum3) bekommt man noch detailliertere Informationen.
baum3$results
#>
cp Accuracy Kappa AccuracySD KappaSD
#> 1 0.0246
0.764 0.2210
0.0511
0.190
#> 2 0.0410
0.739 0.1311
0.0393
0.148
#> 3 0.0574
0.731 0.0547
0.0244
0.108
baum3$finalModel
#> n= 480
#>
#> node), split, n, loss, yval, (yprob)
#>
* denotes terminal node
#>
#> 1) root 480 122 nein (0.254 0.746)
#>
2) rating< 2.5 63 28 ja (0.556 0.444)
#>
4) age< 44.5 54 21 ja (0.611 0.389) *
#>
5) age>=44.5 9
2 nein (0.222 0.778) *
#>
3) rating>=2.5 417 87 nein (0.209 0.791) *
Interessant ist die Frage, wie unterschiedlich die zehn Bäume sind; werden ihre Vorhersagen stark variieren, wird uns das kein großes Vertrauen in das Modell schenken. Die
Situation würde einer Waage gleichen, die jedes Mal ein komplett anderes Gewicht anzeigt, wenn Sie sie betreten. Werden die Gewichtswerte Ihrer Waage sich stets ähneln,
haben Sie mehr Vertrauen in die Waage. Betrachten wir also die Variabilität der Vorhersagen. Das Objekt, das train() zurückliefert, beinhaltet das Objekt resample; dort sind
die Gütekoeffizienten von jedem der zehn Kreuzvalidierungsdurchläufe festgehalten:
386
21 Baumbasierte Verfahren
0.84
0.8
0.80
value
value
0.6
0.4
model
glm
0.76
tree
0.2
0.72
0.0
Accuracy
Kappa
coefficient
Gütekennzahlen der zehn Folds des Baummodells
glm
tree
Accuracy
Kreuze geben den Mittelwert an
Abb. 21.4 Vergleich der Kreuzvalidierungsergebnisse für das Affären-Modell
baum3$resample %>%
head()
#>
Accuracy Kappa Resample
#> 1
0.837 0.5088
Fold02
#> 2
0.729 0.1613
Fold01
#> 3
0.833 0.4667
Fold03
#> 4
0.750 0.0769
Fold06
#> 5
0.812 0.4194
Fold05
#> 6
0.729 0.0370
Fold04
Formen wir diesen Dataframe in Langform, so dass wir ihn einfach visualisieren können:
baum3$resample %>%
gather(key = coefficient, value = value, - Resample) -> baum_folds
Nun plotten wir diesen Dataframe (s. Abb. 21.4, links), um die Genauigkeit und das Kappa
der zehn Resamples („Folds“) des Baummodells zu vergleichen:
baum_folds %>%
ggplot +
aes(x = coefficient, y = value) +
labs(caption = "Gütekennzahlen der zehn Folds des Baummodells") +
geom_point(position = "jitter") -> baum_folds_p
Aber ist die Variation, die wir hier sehen, groß oder klein? Schwer zu sagen; klarer wird
die Zuverlässigkeit des Modells, wenn wir sie in Relation zu einem anderen Modell setzen.
Ein gutes Vergleichsmodell, da einfach und bewährt, ist die (logistische) Regression. Auch
das GLM, in Form der logistischen Regression, kann man mit caret berechnen. Analog
zum Baummodell baum3 berechnen wir ein zehnfach kreuzvalidiertes GLM:
my_train_control <- trainControl(
method = "cv",
number = 10)
21.2 Entscheidungsbäume mit caret
387
glm_halodrie <- train(is_halodrie ~ .,
data = train_df,
method = "glm",
family = "binomial",
trControl = my_train_control)
glm_halodrie$resample %>%
gather(key = coefficient, value = value, - Resample) %>%
mutate(model = "glm") -> glm_folds
Die beiden Dataframes baum_folds und glm_folds fassen wir zu einem Dataframe
zusammen, um die Genauigkeit (accuracy) der beiden Modelle dann vergleichend zu plotten. Dazu hilft die Funktion gather(), die aus dem Dataframe glm1$resample einen
Dataframe in Langform faltet, welcher für ein Diagramm besser geeignet ist. Dann heften
wir einfach beide Dataframes untereinander. In der Sprechweise von dplyr nehmen wir
den Dataframe baum_folds und binden unten Zeilen an mit bind_rows, und zwar die
Zeilen des Dataframes glm_folds.
baum_folds %>%
mutate(model = "tree") %>%
bind_rows(glm_folds) -> folds
Der resultierende Dataframe folds ist jetzt zur Visualisierung geeignet (vgl. Abb. 21.4,
rechts). Die Kreuze geben jeweils den Mittelwert an.
folds %>%
filter(coefficient == "Accuracy") %>%
ggplot() +
aes(x= model, y = value) +
geom_point(position = "jitter",
size = 3,
aes(color = model,
shape = model)) +
labs(x = "Accuracy") +
stat_summary(fun.y = "mean",
geom = "point",
shape = 4,
size = 4) +
labs(caption = "Kreuze geben den Mittelwert an") -> p_folds
Mit stat_summary() haben wir uns zusätzlich zu den Punkten, die jeweils für einen
Resampling-Durchlauf stehen, den Mittelwert der jeweiligen Punkte (Kreuze) angeben
lassen (vgl. Wickham (2016)). Insgesamt weist das Baumodell mehr Varianz in den Vorhersagen auf, was keine wünschenswerte Eigenschaft ist. Ein Blick in die deskriptive
Statistik belegt dies:
388
21 Baumbasierte Verfahren
folds %>%
group_by(model, coefficient) %>%
summarise(sd = sd(value))
#> # A tibble: 4 x 3
#> # Groups:
model [?]
#>
model coefficient
sd
#>
<chr> <chr>
<dbl>
#> 1 glm
Accuracy
0.0168
#> 2 glm
Kappa
0.0805
#> 3 tree Accuracy
0.0511
#> 4 tree Kappa
0.190
21.2.1
Vorhersagegüte
Wie „gut“ ist unser Baum bzw. unser Vorhersagemodell? Genauer gesagt: Wie viele Hallodris (positive Fälle) wurden als solche von unserem Modell erkannt, und wie viele
Nicht-Hallodris (negative Fälle) wurden als solche erkannt? Dazu betrachten wir eine
Konfusionsmatrix; zuerst für die Trainings-, danach für die Test-Stichprobe.
train_df <- train_df %>%
mutate(halodrie_predict = predict(baum3,
type = "raw",
newdata = train_df))
train_df %>%
# Konfusionsmatrix
count(is_halodrie, halodrie_predict) -> conf_matrix_baum3_train
conf_matrix_baum3_train
#> # A tibble: 4 x 3
#>
is_halodrie halodrie_predict
n
#>
<fct>
<fct>
<int>
#> 1 ja
ja
33
#> 2 ja
nein
89
#> 3 nein
ja
21
#> 4 nein
nein
337
Dabei berechnet sich die Richtigkeit (Korrektklassifikationsrate oder Richtigkeit; Accuracy) als der Anteil aller Hallodris, die als solche erkannt wurden (33) plus alle NichtHallodris, die als solche erkannt wurden (337), bezogen auf die Anzahl aller Fälle im
Trainings-Sample (480).
Mit einer Richtigkeit von 0.77 ist das Modell baum3 wenig besser als das Nullmodell,
also ein Baum „ohne Äste“. Als Nullmodell definieren wir das Modell, in dem jeder Fall
der häufigsten Klasse zugeordnet wird (sicher keine doofe Strategie, wenn man sonst keine Informationen hat). Das Nullmodell hat eine Richtigkeit von 0.75 (wie kann man die
21.2 Entscheidungsbäume mit caret
389
Richtigkeit des Nullmodells berechnen?1 ). Das waren die Gütekennzahlen der Trainingsstichprobe; wichtiger sind aber die Gütewerte der Trainingsstichprobe. Vergleichen wir
die Werte Trainingsstichprobe als Nächstes mit Güte in der Test-Stichprobe; dort ist die
Richtigkeit zumeist geringer als in der Trainingsstichprobe.
test_df <- test_df %>%
mutate(halodrie_predict = predict(baum3,
type = "raw",
newdata = test_df))
confusionMatrix(
data = test_df$halodrie_predict,
reference = test_df$is_halodrie)
#> Confusion Matrix and Statistics
#>
#>
Reference
#> Prediction ja nein
#>
ja
6
9
#>
nein 22
84
#>
#>
Accuracy : 0.744
#>
95% CI : (0.656, 0.819)
#>
No Information Rate : 0.769
#>
P-Value [Acc > NIR] : 0.7772
#>
#>
Kappa : 0.14
#> Mcnemar's Test P-Value : 0.0311
#>
#>
Sensitivity : 0.2143
#>
Specificity : 0.9032
#>
Pos Pred Value : 0.4000
#>
Neg Pred Value : 0.7925
#>
Prevalence : 0.2314
#>
Detection Rate : 0.0496
#>
Detection Prevalence : 0.1240
#>
Balanced Accuracy : 0.5588
#>
#>
'Positive' Class : ja
#>
Die Funktion confusionMatrix aus caret berechnet bequem alle relevanten Statistiken für uns. Unser Baum kommt in diesem Beispiel nicht gut weg; das Nullmodell hat
eine Richtigkeit (Accuracy) von 0.77 (No Information Rate), was besser ist als unser
baum3 mit 0.74. Die Vorhersagen sind nicht besser als Raten; entsprechend ist der Kappa-
1
Z. B. mit tally(~is_halodrie, data = train_df, format = "percent"); der
Anteil der häufigsten Kategorie ist die Vorhersage des Nullmodells.
390
21 Baumbasierte Verfahren
Wert nahe null. Wer vorsichtig ist, der rechne per Hand die Sensitivität und Spezifität des
Nullmodells nach.
# Sensitivität und Spezifität des Nullmodells:
test_df %>%
count(is_halodrie, halodrie_predict) %>%
group_by(is_halodrie) %>%
mutate(prop = n / sum(n))
#> # A tibble: 4 x 4
#> # Groups:
is_halodrie [2]
#>
is_halodrie halodrie_predict
n
prop
#>
<fct>
<fct>
<int> <dbl>
#> 1 ja
ja
6 0.214
#> 2 ja
nein
22 0.786
#> 3 nein
ja
9 0.0968
#> 4 nein
nein
84 0.903
# Hallodri-Anteil:
mosaic::tally(~is_halodrie, data = test_df, format = "proportion")
#> is_halodrie
#>
ja nein
#> 0.231 0.769
Die Gesamtgenauigkeit des Baummodells (baum3) entspricht in etwa der des
Nullmodells, sowohl hinsichtlich Sensitivität als auch Spezifität. Unser Modell war
offenbar wenig informiert. Man sieht, dass die Gesamtgenauigkeit von baum3 nicht
absolut, sondern im Vergleich zum Nullmodell beurteilt werden muss.
Wie steht es mit der Gütekennzahl von glm_halodrie? Berechnen wir auch hier
wieder die Vorhersagen und gleichen sie mit den beobachteten Werten ab:
test_df <- test_df %>%
mutate(halodrie_predict_glm = predict(glm_halodrie,
type = "raw",
newdata = test_df))
confusionMatrix(
data = test_df$halodrie_predict_glm,
reference = test_df$is_halodrie) -> conf_matrix_glm_test
conf_matrix_glm_test$overall[["Kappa"]]
#> [1] 0.0711
Betrachten wir nur den Wert von Kappa als generelles Gütemaß; dieser Wert liegt bei 0.07
– geringer als das Kappa des Baummodells, das bei 0.14 liegt.
21.5 Stärken und Schwächen von Bäumen
391
21.3 Der Algorithmus der Entscheidungsbäume
Grob gesagt ist der Ablauf bzw. der Algorithmus der Entscheidungsbäume so (vgl. Han et
al. (2011)):
1. Starte mit der gesamten Stichprobe als „Wurzelknoten“.
2. Partioniere den aktuellen Knoten so, dass die resultierenden zwei Knoten insgesamt homogener sind als der aktuelle Knoten.
3. Weise den resultierenden zwei Knoten die entsprechende Teilstichprobe zu.
4. Wiederhole Schritte 2 und 3, bis ein Stop-Kriterium erreicht wird.
5. Nimm als Vorhersagewert die häufigste Klasse eines Endknotens.
Typische Stop-Kriterien sind a), dass ein Knoten 100 % homogen ist (nur noch Fälle
einer Klasse), b), dass die Stichprobengröße einer Klasse unter einen Grenzwert (z. B.
n D 5) fällt, oder c), dass der Homogenitätszuwachs unter einen Grenzwert (z. B. < 1 %)
fällt.
Das Partionieren wird so ausgeführt, dass die resultierenden zwei Klassen immer disjunkt (überlappungsfrei) sind. Da dieses Partionieren wiederholt (rekursiv) ausgeführt
wird, spricht man bei baumbasierten Modellen auch von rekursivem Partionieren. Wie
geht der Algorithmus vor, um zu wissen, welche Variable und welcher Schnittpunkt die
höchste Homogenität verspricht? Es werden einfach alle Variablen und alle relevanten
Schnittpunkte ausprobiert. Das hört sich viel an, aber es ist nicht so schlimm, da bei n
Beobachtungen nur n 1 Schnittpunkte ausprobiert werden müssen: Gibt es zwei Beobachtungen, so wird die (metrische) Variable in der Mitte zwischen den Werten der beiden
Beobachtungen geteilt.
21.4 Regressionsbäume
Regressionsbäume sind den Klassifikationsbäumen ähnlich; allerdings wird hier unter
Homogenität nicht „sortenreine“ Aufteilung verstanden (z. B. in Form eines geringen
Klassifikationsfehlers), sondern eine möglichst geringe Abweichung der Vorhersage vom
beobachteten Wert. Für alle Fälle, die im gleichen Blatt landen, wird wiederum der gleiche Wert vorhergesagt; dieses Mal der Mittelwert der Fälle dieses Blattes. Bei James et al.
(2013) oder Kuhn und Johnson (2013) findet sich eine umfassendere Einführung.
21.5 Stärken und Schwächen von Bäumen
Bäume sind ein verbreitetes Werkzeug der statistischen Modellierung; man kann sie als
nonparametrische Regression verstehen (Strobl und Malley 2009). Im Gegensatz zu linearen Modellen, wo Prädiktoren linear kombiniert werden, wird hier der Prädiktorenraum
392
21 Baumbasierte Verfahren
B. Rekursive Partionierung schneidet schlecht ab
y
y
A. Lineare Klassifizierung schneidet gut ab
x
x
D. Rekursive Partionierung schneidet gut ab
y
y
C. Lineare Klassifizierung schneidet schlecht ab
x
x
Abb. 21.5 Vergleich von linearen Modellen und Entscheidungsbäumen
in „rechteckige“ Regionen geteilt. Abb. 21.5 veranschaulicht ein Modell mit zwei Prädiktoren (vgl. James et al. (2013, S. 315)). Je nachdem, ob die Entscheidungsgrenze besser
mit Rechtecken oder mit einer Geraden (bzw. einer Polynomfunktion) zu modellieren ist,
wird entweder ein lineares Modell oder ein Entscheidungsbaum besser passen, also zu
einer besseren Vorhersage führen.
Die Stärken von Entscheidungsbäumen sind ihre intuitive Plausibilität, die sich auch
in der guten Visualisierbarkeit niederschlägt. Menschliche Entscheidungsprozesse könnten dem von Entscheidungsbäumen in einigen Aspekten ähnlich sein (James et al. 2013).
Qualitative Variablen können ohne weitere Vorverarbeitung problemlos eingegeben werden. Der Nachteil von Entscheidungsbäumen ist ihre Instabilität; nur wenig Änderung
in der zugrunde liegenden Stichprobe kann den Baum massiv verändern. Außerdem ist
die prädiktive Güte von Entscheidungsbäumen nicht so hoch wie die anderer Verfahren.
Ein weiteres Problem betrifft die Frage der Überanpassung (Overfitting): Zu viele Knoten führen dazu, dass Rauschen, bedingt durch die Zufälligkeit der Stichprobenziehung,
in das Modell einfließt. Eine Gegenmaßnahme dazu ist das sog. Pruning, das „Zurückschneiden“ der Baumäste, um den Baum wieder einfacher (weniger Knoten) zu gestalten
(s. James et al. (2013) für Details dazu). Der Effekt von Überanpassung ist in Abb. 21.6
dargestellt: Die Richtigkeit im Trainings-Sample nimmt mit der Anzahl der Knoten zu;
für das Test-Sample gilt das nicht: Bei drei Knoten erreicht die Richtigkeit ihren Maximalwert, danach steigt sie nicht weiter. Im Gegenteil: häufig sinkt die Test-Richtigkeit mit
zunehmender Komplexität des Modells, da Zufallsrauschen im Modell für bare Münze
genommen wird, was schlechte Vorhersagen produzieren muss. Ein ähnliches Bild zeigt
21.6 Bagging
393
Richtigkeit
0.85
0.80
Genauigkeit kreuzvalidiert
Genauigkeit Test-Sample
0.75
Genauigkeit Trainings-Sample
0.70
1
2
3
4
5
6
7
8
9
10
Anzahl der Knoten
Abb. 21.6 Klassifikationsgüte in Abhängigkeit von der Anzahl der Knoten im Baum
die Richtigkeitskurve für die kreuzvalidierten Daten: Ab einer gewissen Komplexität des
Modells (hier vier Knoten) steigt die Richtigkeit nicht weiter.
Abb. 21.6 stellt die Werte der Gesamtgenauigkeit in drei Varianten dar: Für die Trainingsstichprobe (Rechtecke), für die Test-Stichprobe (Dreiecke) und für die zehnfach
kreuzvalidierten Stichproben im Trainings-Sample (Kreise). Wie man sieht, verbessert
sich die Richtigkeit bis zu zwei Knoten, danach fällt sie ab. Mehr Knoten, komplexere
Modelle also, bringen (hier) offenbar keine relevante Verbesserung. Dieser Effekt zeigt
sich nicht im Trainings-Sample: Wie zu erwarten, steigt die Richtigkeit mit der Anzahl
der Knoten, bis eine Sättigung erreicht ist. Dieser Anstieg an Genauigkeit ist aber nur
Rauschen – Überanpassung an die Daten – und sollte mit Skepsis betrachtet werden. Das
kreuzvalidierte Modell ist relativ robust gegenüber dieser Verzerrung; seine Richtigkeit ist
ähnlich dem Modell im Test-Sample.
21.6 Bagging
Ein Nachteil von Entscheidungsbäumen ist ihre hohe Varianz im Vergleich zu anderen
Modellen: Ändert sich die Beschaffenheit der Stichprobe ein wenig, kann der Entscheidungsbaum deutlich anders aussehen; die Vorhersagen variieren relativ stark. Sie können
das nachvollziehen, indem Sie einen Datensatz zufällig in zwei Hälften aufteilen und jeweils einen Entscheidungsbaum anpassen – die Bäume der beiden Hälften können sich
deutlich unterscheiden (James et al. 2013, Kapitel 8.2.1). Die resultierenden Vorhersagen
der zwei Bäume für denselben Test-Datensatz könnten sich deutlich unterscheiden. Das ist
wenig vertrauenserweckend. Ein Hilfsmittel, die Varianz der Vorhersagen im Test-Sample
zu verringern, ist es, mehrere Stichproben zu ziehen. Für jede Stichprobe ist ein Baum zu
berechnen; die Vorhersagen würden dann einfach gemittelt. Damit erreicht man stabilere
Vorhersagen; die Varianz des Modells sinkt. Leider ist es unpraktisch, viele Stichproben
zu ziehen: 1000-mal eine Stichprobe an Hallodris zu ziehen . . . anstrengend. Daher wendet man einen Trick an: Man betrachtet eine einzelne Stichprobe als Population. Dann
394
21 Baumbasierte Verfahren
zieht man aus dieser „Pseudopopulation“ einfach viele Stichproben; dabei legt man nach
jeder Ziehung die Beobachtungen wieder zurück. Dabei werden einige Beobachtungen
mehrfach und andere vielleicht gar nicht gezogen (s. Abschn. 17.3). Mit diesem Vorgehen wird Zufall in das Baummodell injiziert: Die Stichproben variieren; diese Idee heißt
Bootstrapping. Für jede dieser Bootstrap-Samples können wir dann ein Modell (z. B. Entscheidungsbaum) berechnen. Zum Schluss nehmen wir den Mittelwert aller Modelle als
Vorhersagewert für jede Beobachtung. Dieses Prinzip heißt Bagging. Durch Bagging verringern wir die Varianz der Schätzung, das ist der Vorteil. Ein Nachteil ist, dass das Modell
nicht mehr gut zu interpretieren ist: Einen Baum kann man einfach beschreiben, aber wie
soll man zu einem „mittleren Baum“ zusammenfassen? Das geht nicht; die Interpretierbarkeit leidet.
Durch das Ziehen mit Zurücklegen werden im Schnitt nur etwa zwei Drittel der Beobachtungen gezogen. Das verbleibende Drittel wird also nicht zum Training des Baumes
verwendet. Diesen Teil an nicht gezogenen Beobachtungen nennt man die Out-of-BagBeobachtungen (OOB) oder das OOB-Sample. Es bietet sich an, die Vorhersagen jedes
einzelnen Baumes des Bagging-Modells anhand der OOB-Beobachtungen durchzuführen, um so genauere Einschätzungen der Modellgüte zu bekommen. Im Paket caret sind
einige Bagging-Methoden implementiert;2 man übergibt dafür der Funktion train() für
das Argument method den Wert treebag. So kann man ein einfaches Bagging-Modell
mit Syntax von caret schreiben:
bag1 <- train(is_halodrie ~ .,
data = train_df,
method = "treebag",
trControl = my_train_control)
21.7 Grundlagen von Random Forests
21.7.1
Grundlagen
Random Forests, „Zufallswälder“ an Entscheidungsbäumen, führen noch eine zweite
Schicht Zufall in das Modell ein. Wie beim Bagging werden viele (z. B. k D 1000)
Bäume aus gebootstrappten Stichproben gezogen (s. Abb. 21.7). Aber im Unterschied
zum Bagging wird bei jeder Astgabelung eine kleine Zufallsstichprobe an m Prädiktoren
gezogen, die zur Bestimmung der Gabelungen herangezogen wird. Nicht alle Prädiktoren
des Datensatzes werden berücksichtigt, sondern nur m zufällige Prädiktoren. Bei jeder
Astgabelung wird wieder eine neue Stichprobe an m Prädiktoren gezogen. Das hat natürlich den Effekt, dass Variablen mit viel Einfluss auf das Kriterium weniger häufig zum
Zuge kommen. Was sich wie ein Nachteil anhört, stellt sich als ein Vorteil heraus: Ohne
2
https://topepo.github.io/caret/train-models-by-tag.html#bagging.
21.7
Grundlagen von Random Forests
395
Abb. 21.7 Intuitive
Darstellung des RandomForest-Modells
diese zufällige Begrenzung würden stets ähnliche Prädiktoren – die einflussreichsten
– ausgewählt werden. Die Bäume würden einander stark ähneln. Anders ausgedrückt:
Ihre Vorhersagen wären korreliert. Leider reduziert die Aggregation von korrelierten
Beobachtungen die Variabilität nicht so stark wie die Aggregation von unkorrelierten
Beobachtungen. Korrelierte Bäume bringen nicht viel zusätzliche Information. Man kann
Random Forests demnach als eine Methode zur Dekorrelation der Bäume verstehen. In
vielen Fällen funktioniert die Methode erstaunlich gut: Die Vorhersagegüte von RandomForest-Modellen ist oft gut; besser als bei einzelnen Bäumen und bei Bagging.
Der Unterschied zum Bagging ist also, dass nur m Prädiktoren pro Astgabelung berücksichtigt werden, nicht alle p Prädiktoren. Wie viele Prädiktoren werden gewählt, wie
p
groß soll m sein? Häufig nimmt man m D p, wobei es sich anbietet, verschiedene
Werte auszuprobieren. m ist damit ein Tuningparameter (Hyper-, Metaparameter) des Modells; ein Parameter, der vom Anwender bestimmt, d. h. nicht berechnet wird. Würde man
m D p setzen, so gliche das Random-Forest-Modell dem Bagging; damit kann Bagging
als Spezialfall des Random-Forest-Modells gesehen werden. Natürlich sollte jedes Ausprobieren im Trainings-Sample stattfinden, sofern die Stichprobe insgesamt nicht zu klein
ist. Erst das finale Modell sollte das Test-Sample sehen. Mit method = "rf" weisen
wir train() an, ein Random-Forest-Modell zu berechnen. In Software-Umsetzungen
von Random Forest wird m meist als mtry bezeichnet und die Anzahl der Bäume mit
ntree, so auch bei caret und dem zugrunde liegenden Paket randomForest. mtry
ist der Tuningparameter für Random Forests (zumindest für die Umsetzung, die wir verwenden). ntree wird von caret nicht als Tuningparameter gezählt, da es meist eine
Zahl von Bäumen gibt, bis zu der die Leistung des Modells monoton steigt. Ab einer ge-
396
21 Baumbasierte Verfahren
wissen Zahl an Bäumen wird ein Plateau erreicht, und die Leistung steigt nicht weiter an.3
Wir definieren noch, dass wir das Ergebnis kreuzvalidieren möchten (s. Abschn. 15.9); dazu nutzen wir die Funktion trainControl() aus caret. Mit dem Argument method
kann man angeben, welche Methode zur Kreuzvalidierung man verwenden möchte (hier
die normale Kreuzvalidierung, cv); wir fordern zehn Faltungen (number = 10) an. Die
caret-Syntax ist wie gesagt für alle Modelle gleich; caret probiert auch selbständig ein
paar Werte für die Metaparameter durch; im Standard sind das insgesamt 3k , wobei k die
Anzahl der Metaparameter ist (in unserem Fall ist k D 1).
rf1 <- train(is_halodrie ~ .,
data = train_df,
method = "rf",
trControl = my_train_control)
rf1
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
Random Forest
480 samples
8 predictor
2 classes: 'ja', 'nein'
No pre-processing
Resampling: Cross-Validated (10 fold)
Summary of sample sizes: 432, 432, 432, 432, 432, 433, ...
Resampling results across tuning parameters:
mtry
2
5
8
Accuracy
0.738
0.715
0.723
Kappa
0.156
0.163
0.188
Accuracy was used to select the optimal model using the largest value.
The final value used for the model was mtry = 2.
Die Ausgabe gleicht der des Baummodells; zentrale Informationen sind die ausprobierten Werte der Tuningparameter mit ihren zugehörigen Gütekennzahlen wie Genauigkeit
(accuracy) und Kappa. Das Modell mit mtry = 2 schnitt also hinsichtlich Genauigkeit
am besten ab. Kappa wäre hier eine alternative Entscheidungsgröße. Man kann train
auch die Werte des oder der Tuningparameter vorgeben, dann probiert caret brav alle
diese Werte durch. Dazu legt man eine Tabelle an, in der alle Werte der Tuningparameter miteinander kombiniert werden. Hat man z. B. zwei Tuningparameter mit jeweils drei
Werten, so müssten 32 D 9 Werte durchprobiert werden. Das Anlegen dieser Tabelle kann
3
Man könnte ntree daher eher als Schokoladenparameter bezeichnen: Eine gewisse Menge
wünscht man sich, aber mehr bringt keinen Zusatznutzen; randomforest und damit caret als
Schnittstelle zu randomforest nimmt ntree = 500 als Standard, vgl. ?randomForest.
21.7
Grundlagen von Random Forests
397
expand.grid() übernehmen. Man übergibt die Tuningparameter und ihre Werte, und
expand.grid() erstellt eine Tabelle mit allen Kombinationen. Da es hier nur einen Tu-
ningparameter gibt, ist die Kombination recht witzlos, aber andere Modelle können mehr
Tuningparameter haben.
my_tuning_grid <- expand.grid(mtry = 1:10)
Sehen wir nach, ob die Erweiterung des Suchraums die Vorhersagegüte verbessert; wie
Sie vielleicht merken, kann es etwas dauern, bis das Modell durchgerechnet ist. Zwar darf
ntree nicht als Tuningparameter übergeben werden, aber wir können train() einen
konstanten Wert übergeben:
rf2 <- train(is_halodrie ~ .,
data = train_df,
method = "rf",
trControl = my_train_control,
ntree = 1000,
importance = TRUE,
tuneGrid = my_tuning_grid)
Mit importance = TRUE weisen wir die Funktion an, die Wichtigkeit der Prädiktoren
zu schätzen; Näheres dazu in Abschn. 21.8.
rf2$results %>%
top_n(n = 1, wt = Accuracy)
#>
mtry Accuracy
Kappa AccuracySD KappaSD
#> 1
1
0.746 0.00691
0.00801
0.038
Mit nur m D 1 Prädiktor wird die beste Vorhersagegüte im Trainings-Sample erzielt; die
Accuracy liegt in dem Fall bei 0.746 und das Kappa bei 0.007. Das ist allerdings eine
schlechte Nachricht, wenn wir uns daran erinnern, dass der Anteil der Nicht-Hallodris
etwa genauso hoch ist. Prüfen wir, wie gut unsere Vorhersagen im Test-Sample sind. Die
Funktion postResample() erledigt das Berechnen der Gütekennzahlen sehr bequem:
rf2_predict <- predict(rf2, newdata = test_df, type = "raw")
postResample(pred = rf2_predict, obs = test_df$is_halodrie)
#> Accuracy
Kappa
#>
0.769
0.000
Wie man sieht, ist die Vorhersagegüte ähnlich schlecht wie im Trainings-Sample. Auf den
Punkt gebracht muss man resümieren: Beide Modelle, sowohl im Trainings-Sample als
auch im Test-Sample, zeigen eine schlechte Leistung, kaum besser als das Nullmodell. Es
gibt verschiedene Richtwerte, was ein kleines bzw. ein großes Kappa ist (Banerjee et al.
1999; Brennan und Prediger 1981), aber D :04 gehört immer zu „schlecht“! Ein KappaWert von 0 ist von blindem Raten (Zufallsrauschen) nicht unterscheidbar. Unser Modell
ist also nicht oder kaum besser als blindes Raten. Kann eigentlich nur noch besser werden.
398
21.8
21 Baumbasierte Verfahren
Variablenrelevanz bei Baummodellen
Ein Vorteil von linearen Modellen ist, dass die Relevanz der Prädiktoren gut zu berechnen ist (vgl. Abschn. 18.8). Wie ist das bei Random Forests? Kann man dort auch eine
Einschätzung zur Variablenrelevanz (variable importance) bekommen? Ja. Eine typische
Überlegung dazu ist die folgende: Angenommen, wir würden die Werte einer Variablen
wild durchmischen und dann mit dieser gemischten (permutierten) Variablen das Modell
neu berechnen – dann müsste die Vorhersage schlechter werden, wenn die Variable vorher
wichtige Informationen zur Vorhersage enthielt. Das Durchmischen hat jeden Zusammenhang zerstört, der vorhanden war. Durch die Höhe der Verringerung der Vorhersagegüte
können wir die Relevanz einer Variablen abschätzen. Gibt man bei train() das Argument importance = TRUE an (was wir getan haben), so werden die Relevanzwerte
mitberechnet. Mit varImp() kann man sich die Werte ausgeben lassen, wenn man das
Modellobjekt an die Funktion übergibt:
varImp(rf2)
#> rf variable importance
#>
#>
Importance
#> rating
100.00
#> religiousness
38.69
#> yearsmarried
33.93
#> gendermale
14.95
#> age
13.70
#> childrenyes
11.17
#> education
6.57
#> occupation
0.00
Ehezufriedenheit wurde als wichtigster Prädiktor identifiziert; die Werte sind so skaliert,
dass der beste Prädiktor (mit der größten Verringerung der Vorhersagegüte) einen Wert von
100 bekommt. Die übrigen Prädiktoren sind relativ dazu skaliert. Die unstandardisierten
Werte bekommt man so: varImp(rf2, scale = FALSE). Dieser Wert berechnet sich
als die Verringerung der Genauigkeit durch das Permutieren der Variable pro Baum. Dieser Wert wird dann über alle Bäume gemittelt und durch die Standardabweichung geteilt
(A. Liaw und Wiener 2002). Allerdings raten vorsichtigere Gemüter dazu, die Abstände
dieser Werte mit Vorsicht zu genießen und nur die Rangfolge zu beachten. Weiterhin haben verschiedene Modelle verschiedene Maße der Variablenrelevanz, so dass ein absoluter
Vergleich kaum möglich ist, auf Rangebene schon. Mit plot(varImp(rf2)) kann man
sich einen Plot zur Variablenrelevanz ausgeben lassen.
21.8 Variablenrelevanz bei Baummodellen
399
Aufgaben
1.
2.
3.
4.
Verändern Sie den cp-Wert des Arguments rpart.control, z. B. auf 0.001
oder 0.1; wie verändert sich der Baum?4
Nehmen Sie nur zwei Prädiktoren in den Baum auf und berechnen Sie den
Baum neu. Wie verändert sich der Baum?5
Betrachten Sie die Tuning-Parameter dieses Modells; das Objekt, das von
rpart() zurückgegeben wird, beherbergt eine Liste mit dem Namen
control. Wie kann man diese Liste auslesen?6
Definieren Sie das Kriterium is_halodrie als String-Variable. Wird dieser
Datentyp von rpart() akzeptiert bzw. werden sinnvolle Ergebnisse zurückgemeldet?
Lösung
Ja, die Funktion akzeptiert auch String-Variablen als Kriterium; das Ergebnis
ist identisch. Man sieht z. B., dass die Anzahl der Fälle pro Blatt identisch ist:
train_df$is_halodrie_chr <- as.character(train_df$is_halodrie)
baum2b <- rpart(is_halodrie_chr ~ rating + age, data = train_df)
plot(as.party(baum2b))
str(baum2b)
identical(baum2b$frame, baum2a$frame)
5.
6.
7.
4
Die Funktion rpart() berechnet automatisch Werte zur Variablenwichtigkeit.
Wie kann man auf diese Werte zugreifen?7
Geben Sie eine Syntax-Blaupause an, um einen Baum mit caret berechnen
zu lassen.8
Zählen in einer Konfusionsmatrix sowohl die richtig negativen als auch die
richtig positiven zusammen, wenn man die Gesamtrichtigkeit (Accuracy) berechnet?9
Vgl. Abschn. 21.1.2.
baum2a <- rpart(is_halodrie ~ rating + age, data = train_df);
plot(as.party(baum2a)); es resultiert ein anderer Baum. Interessant ist, dass die Variable
age mehr als einmal in den Baum einging, wie im baum2 auch.
6
Z. B. so: baum2a[["control"]].
7
baum2a[["variable.importance"]].
8
train(kiterium ~ praediktor, data = meine_daten, method =
"rpart").
9
Ja.
5
400
21 Baumbasierte Verfahren
8.
Bei steigender Anzahl der Knoten in einem Baumodell steigt die Richtigkeit
im Test-Sample. Richtig?10
9. Der Tuning-Parameter mtry in einem Random-Forest-Modell sollte etwa auf
die Anzahl der Variablen im Modell gesetzt werden. Richtig?11
10. Abb. 21.2 (rechter Teil) entspricht baum2; Abb. 21.3 stellt das gleiche Modell
dar. In letzterer Abbildung kann man im Knoten 7 erkennen, dass für diese und
nur diese Gruppe das Geschlecht einen großen Einfluss hat. Wie groß ist dieser
Einfluss (in Prozentpunkten) und welche „Kachel“, also welches Rechteck in
Abb. 21.2 korrespondiert mit diesem Knoten?12
10
Nein; wird die Anzahl der Knoten im Baum zu groß, so sinkt die Modellgüte im Test-Sample
wieder.
p
11
Nein; in der Regel sind viel kleinere Werte besser. Als Faustregel kann man mit .k/ beginnen,
wobei k die Anzahl der Prädiktoren im Modell bezeichnet.
12
Ca. 50 Prozentpunkte; s. Abb. 21.3 im Vergleich von Knoten 6 und Knoten 7. In Abb. 21.2 kennzeichnet das linke, mittlere Rechteck (sowohl bei Männern als auch bei Frauen) diesen Unterschied
(also Menschen mit geringer Ehezufriedenheit und mittlerem Alter).
Fallstudie: Kreditwürdigkeit mit caret
22
Lernziele
Wissen, welche Aufgaben mit dem R-Paket caret erledigt werden können
Die grundlegende Syntax von caret kennen
Prädiktive Modelle mit caret berechnen können
Resampling-Methoden mit caret anwenden können
Einen Datensatz für eine prädiktive Modellierung vorbereiten können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(caret)
library(modelr)
library(broom)
library(doMC)
data(GermanCredit, package = "caret")
data(stats_test, package = "pradadata")
Das R-Paket caret (Classification And REgression Training) bietet eine komfortable
Syntax, um eine Vielzahl – aktueller Stand: 232 – von statistischen Modellen zu berechnen. Es gibt ein empfehlenswertes Lehrbuch, welches eng auf diesem Paket aufbaut (Kuhn
und Johnson 2013). Was ist der Nutzen dieses Pakets? Kein Mensch ist scharf drauf,
sich die unterschiedliche Syntax einer großen Zahl an Modellen (die in verschiedensten R-Paketen hausen) zu merken. Das ist der Vorteil von caret: Für alle Modelle wird
die gleiche Syntax verwendet. Zweitens sind Resampling-Methoden in caret auf komfortable Weise eingebaut. Da caret „nur“ eine Oberfläche für bestehende Modelle zur
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_22
401
402
22
Fallstudie: Kreditwürdigkeit mit caret
Verfügung stellt, müssen die eigentlichen Pakete noch nachinstalliert werden; das übernimmt caret. Einen Überblick zu den verfügbaren Modellen bekommt man z. B. mit
names(getModelInfo()).
Einige Funktionen, auch bei caret, verkraften (noch) keine Tibbles. Merkwürdige Fehlermeldungen können die Ursache haben, dass sich eine Funktion an einem
Tibble verschluckt. Im Zweifel sollte man einen Tibble mit as.data.frame() in
einen klassischen Dataframe umwandeln.
Schauen wir ein einfaches Beispiel einer Analyse mit caret an. Sagen wir, wir
möchten vorhersagen, welche Kunden einer Bank „kreditwürdig“ sind; Im Datensatz
GermanCredit ist diese Information in der Variable Class gespeichert. Unter kreditwürdig verstehen wir ein geringes Risiko, dass der Kunde seinen Kredit nicht zurückzahlt.
Wie hoch das Risiko ist, dass wir (oder unser Auftraggeber) verschmerzen können oder
wollen, ist keine statistische Frage, sondern beinhaltet Nutzen-Kosten-Überlegungen,
mithin Gegenstand der Entscheidungstheorie (Jaynes 2003). Solche Aspekte ignorieren
wir in dieser Fallstudie und vereinbaren, dass ein Ausfallrisiko von weniger als 50 % als
kreditwürdig zu klassifizieren ist.
22.1
Zwei Arten der prädiktiven Modellierung
Grundsätzlich und bei caret können zwei Arten von prädiktiven Modellen unterschieden werden: Vorhersage numerischer Werte (Regression) und Vorhersage nominaler Werte
(Klassifikation). Möchte man eine Regression durchführen, muss das Kriterium numerisch
sein; möchte man eine Klassifikation durchführen, so sollte das Kriterium vom Typ Faktor
sein. Definiert man eine Faktorvariable, so definiert der erste Level das „Nicht-Ereignis“
(z. B. „nicht bestanden“, 0) und die zweite Stufe (Level) das Ereignis von Interesse (z. B.
„bestanden“ oder 1). Ist hingegen das Kriterium eine numerische Variable, so wird caret
automatisch eine Regression durchführen. Nicht jedes Modell ist für beide Varianten des
Modellierens – Klassifikation und numerische Vorhersage – geeignet. In diesem Fall werden wir klassifizieren, da die Zielvariable Class dieser Vorgabe entspricht:
str(GermanCredit$Class)
#> Factor w/ 2 levels "Bad","Good": 2 1 2 2 1 2 2 2 2 1 ...
levels(GermanCredit$Class)
#> [1] "Bad" "Good"
Bad ist die erste Stufe (Nicht-Ereignis, 0) und Good die zweite Stufe (das zu modellierende Ereignis, 1). Mit ?GermanCredit bekommt man mehr Information zu diesem
Datensatz (wenn das Paket caret geladen ist).
22.2 Daten aufbereiten
22.2
Daten aufbereiten
22.2.1
Fehlende Werte
403
Viele Modelle kriegen Bauchschmerzen bei fehlenden Werten (NAs). Daher sollte ein
Datensatz von fehlenden Werten bereinigt werden. In Wirklichkeit ist dieser Schritt des
Datenaufbereitens häufig recht langwierig, da es nicht immer einfach ist, zu entscheiden,
was man mit fehlenden Werten macht. Die ganze Zeile löschen? Mit Mittelwert, Median,
häufigstem Wert ersetzen? Ähnliche Fälle suchen und deren Wert eintragen? Verschiedene
Verfahren ausprobieren und deren Mittelwert hernehmen? Eine einfache Strategie ist, alle
Fälle mit fehlenden Werten zu löschen. Solange auf diese Weise noch genug Daten übrig
bleiben, kann das ein sinnvoller Weg sein. Es kann aber auch leicht passieren, dass man
mit dieser Methode keine Daten mehr übrig hat. Prüfen wir zuerst, ob überhaupt fehlende
Werte vorliegen. caret bietet eine praktische Funktion dafür:
anyNA(GermanCredit)
#> [1] FALSE
Im Datensatz gibt es keine fehlenden Werte. Wie man mit fehlenden Werten umgehen
kann, ist in Abschn. 9.1.6 nachzulesen.
22.2.2
Trainings- und Test-Sample aufteilen
Teilen wir zuerst die Daten (n D 1000) in ein Trainings- und ein Test-Sample auf; eine
gängige Aufteilung ist 80:20. Die einfachste Möglichkeit dazu ist, die ersten 80 % der
Stichprobe dem Trainings-Sample zuzuweisen (n1 D 800) und den Rest dem Test-Sample
(n2 D 200):
n_train <- round(.8 * nrow(GermanCredit), digits = 0)
train_index <- sample(1:nrow(GermanCredit), size = n_train)
train <- GermanCredit[train_index, ]
test <- GermanCredit[-train_index, ]
Einfach; aber schöner wäre natürlich, wenn der Anteil des vorherzusagenden Werts
(Good) im Trainings-Sample gleich groß ist wie im Test-Sample. Sprich: Wir hätten gerne eine nach der Kreditwürdigkeit stratifizierte Stichprobe. Praktischerweise stellt caret
eine Funktion zur Verfügung, createDataPartition(), die diesen Wunsch erfüllt:
Trainings_Faelle <- createDataPartition(GermanCredit$Class, p = .8)
Trainings_Faelle_vec <- unlist(Trainings_Faelle)
GermanCredit %>%
filter(row_number() %in% train_index) -> train
GermanCredit %>%
filter(!(row_number() %in% train_index)) -> test
404
22
Fallstudie: Kreditwürdigkeit mit caret
Die Funktion createDataPartition() liefert ein Objekt vom Typ list (Liste) zurück, in dem ein Vektor enthalten ist. Dieser Vektor enthält die Zeilennummern von den
80 % (p = .8) der Fälle, die in das Trainings-Sample gehören. Die Funktion stellt sicher,
dass der Anteil von Class == Good in Trainings- und Test-Sample etwa gleich groß ist.
Da filter() keine Listen verarbeitet, sondern nur Vektoren, müssen wir den Vektor aus
der Liste erst herausschälen, „entlisten“ sozusagen, mit unlist. Um die Fälle für den
Testvektor zu bestimmen, negieren wir die Filteraussage mit dem Minus-Operator.
Schauen wir uns die Verteilung des Kriteriums Class in beiden Stichprobenteilen an:
mosaic::tally(~Class, data = train, format = "proportion")
#> Class
#>
Bad Good
#> 0.294 0.706
mosaic::tally(~Class, data = test, format = "proportion")
#> Class
#>
Bad Good
#> 0.325 0.675
Die Aufteilung hat offenbar funktioniert; die Verteilung von Class ist in den beiden
Stichproben ähnlich.
22.2.3
Variablen ohne Varianz
Weist eine Variable (in einem Datensatz) nur einen Wert auf, so ist sie konstant. Allgemeiner gesprochen: Liegt eine Bedingung für alle Fälle in gleicher Weise vor, so kann
diese Bedingung keine Unterschiede produzieren. Sind alle Läufer im Sprint zweibeinig,
so ist die Aufnahme der Variable hat_zwei_Beine wenig nützlich, um die Sprintzeit
vorherzusagen. Wir sollten daher Variablen mit (fast) keiner Varianz (Near Zero Variance;
NZV) entfernen.
train %>%
nearZeroVar(saveMetrics = TRUE) %>%
rownames_to_column() %>%
filter(nzv == TRUE) %>%
pull(rowname) -> train_nzv
length(train_nzv)
#> [1] 14
Insgesamt wurden 14 Variablen mit wenig oder keiner Varianz identifiziert. Davon sind
zwei Variablen konstant (null Varianz; Purpose.Vacation und Personal.Female.
Single – beide mit Häufigkeit null); außerdem einige Variablen mit fast keiner Varianz;
diese Variablen haben wir im Vektor train_nzv gespeichert. pull() zieht einen Vektor
22.2 Daten aufbereiten
405
aus einem Dataframe; die Funktion liefert – im Gegensatz zu anderen dplyr-Funktionen –
also einen Vektor zurück. Die Variablen dieses Vektors (train_nzv) entfernen wir aus
dem Trainingsdatensatz mit dem Befehl select(-one_of(train_nzv)):
train %>%
select(-one_of(train_nzv)) -> train
attr(train, "nzv checked") <- TRUE
Wir dokumentieren in den Attributen des Dataframes, dass Variablen ohne bzw. mit wenig
Varianz entfernt bzw. nicht vorhanden sind. Mit one_of() kann man select() einen
Text-Vektor mit Variablen (Spalten) übergeben; eine praktische Angelegenheit – beachten
Sie aber, dass diese Spalten in Anführungsstrichen stehen müssen (man übergibt einen
Text-Vektor), im Gegensatz zur sonstigen dplyr-Gepflogenheit, in der auf Anführungsstriche weitgehend verzichtet wird.
22.2.4
Hochkorrelierte Variablen entfernen
Hoch korrelierte (redundante) Variablen bringen per Definition wenig neue Information
in das Modell. Schlimmer noch, sie können dazu führen, dass einige Modelle wie lineare
Modelle unter bestimmten Umständen ins Trudeln geraten und arg verzerrte Schätzungen
produzieren (Kuhn und Johnson 2013). Man spricht in solchen Fällen von Multikollinearität. Andere Modelle, wie Partial Least Squares, die korrelierte Prädiktoren zu sog.
Komponenten zusammenfassen, haben wiederum keine Probleme mit Multikollinearität.1
Möchte man Probleme mit Multikollinearität vermeiden, entfernt man redundante Variablen:
train %>%
select_if(is.numeric)
-> train_num
findCorrelation(cor(train_num, use = "complete.obs"),
names = TRUE)
#> character(0)
Die Funktion untersucht die Korrelation aller Paare von Variablen. Findet sich ein Paar
mit einer Korrelation, die größer oder gleich dem cutoff ist, so wird diejenige Variable von beiden, deren Mittelwert an Korrelationswerten an höchsten ist, angezeigt. Der
Cutoff-Wert ist in der Voreinstellung .9 und daher im Befehl nicht extra erwähnt. Wä1
Allerdings ist Multikollinearität für Vorhersagemodelle, wo es darum geht, einen Wert möglichst
exakt vorherzusagen, weniger ein Problem als für Erklärungsmodelle, wo es darum geht, die Art
und Stärke des Einfluss von Prädiktoren zu ermitteln (vgl. Hyndman (2014), Punkt 3).
406
22
Fallstudie: Kreditwürdigkeit mit caret
ren redundante Variablen angezeigt worden, so hätten wir sie aus dem Datensatz entfernt.
Korrelationen nach Pearson sind nur für metrische Daten sinnvoll; daher wählen wir eine Variable nur (select_if), wenn sie numerisch (metrisch) ist (is.numeric). Hier
wurde keine verdächtige Variable identifiziert.
22.2.5
Parallele Verarbeitung
Hat ein Computer mehr als einen Rechenkern, was heutzutage bei vielen Computern
der Fall ist, so können Berechnungen parallel verarbeitet werden. Dazu hilft doMC::
registerDoMC(). Aber die parallele Verarbeitung hat auch Nachteile: Da mehr Berechnungen gleichzeitig durchgeführt werden, wird auch mehr Speicher benötigt. Grob
gesagt verdoppelt sich die Menge des benötigten Speichers, wenn doppelt so viele Kerne verwendet werden. Bei großen Datenmengen und aufwändigen Modellen kann der
Arbeitsspeicher schnell aufgebraucht sein. Unter Linux und Mac kann man parallel die
Verarbeitung mit dem Paket doMC einschalten:
registerDoMC(cores = 2)
Allerdings ist das Paket doMC nicht unter Windows verfügbar. Dort kann man z. B. die
Pakete doParallel and doSNOW nutzen. Weiteres findet sich z. B. beim CRAN Task
View für High-Performance und Parallelverarbeitung.2
Aufgaben
1. Fügen Sie fehlende Werte in eine Variable ein, um zu prüfen, ob die Funktion
anyNA() funktioniert.
2. Geht man davon aus, dass keine fehlenden Werte vorliegen, weil man z. B. fehlende Werte schon ersetzt hat, ist es sinnvoll zu prüfen, ob überhaupt fehlende
Werte vorliegen, ohne Berücksichtigung in welcher Spalte. Wie kann man das
einfach in R prüfen?3
3. Wem dieser Weg nicht genehm ist, kann sich z. B. mit skimr::skim() die
Anzahl der fehlenden Werte für jede Variable ausgeben lassen. Wie?4
4. Wie viele Fälle vom Typ bad gibt es? Wie groß ist deren Anteil an allen Fällen?5
2
https://cran.r-project.org/web/views/HighPerformanceComputing.html.
anyNA() bietet dazu einen Weg.
4
skimr(meine_tabelle).
5
count(GermanCredit, Class).
3
22.3 Modelle anpassen
22.3
Modelle anpassen
22.3.1
Kreuzvalidierung
407
Um Überanpassung zu vermeiden, sollte man beim prädiktiven Modellieren eine Validierungsstichprobe verwenden. Besser noch ist es, ein mehrfaches Validierungskonzept –
Kreuzvalidierung – zu verwenden. Im Rahmen von caret kann man mittels der Funktion
trainControl() definieren, ob und welche Kreuzvalidierung man einsetzen möchte.
my_crossval <- trainControl(method = "repeatedcv",
number = 10, # k
repeats = 5, # r
allowParallel = TRUE)
Hier sagen wir mit repeatedcv, dass wir eine wiederholte Kreuzvalidierung mit k D
10 durchführen wollen. Mit dem Argument repeats werden r Wiederholungen der k
Kreuzvalidierungen unternommen. Diese Wiederholung kann weitere Stabilität in die Gütekennzahlen bringen;6 natürlich erhöht sich die Rechenzeit; und zwar etwa linear um den
entsprechenden Faktor. allowParralel erlaubt die Verwendung mehrerer Rechenkerne, sofern initialisiert (s. Abschn. 22.2.5).
22.3.2
Modell im Trainings-Sample anpassen mit train()
Anstelle vom „Berechnen“ spricht man gerne vom „Anpassen“ (to fit) eines Modells, weil
man versucht, das Modell so gut es geht den Daten anzupassen (und sonst keine Fehler zu
machen). Egal welches Modell man anpassen möchte, die Syntax von train() ist dabei
praktisch gleich:
train(ziel ~ .,
data = daten,
method = "methode",
trControl = my_crossval)
train() kann man mit der Formelschreibweise füttern (wie hier geschehen) oder mit
x und y die Prädiktoren bzw. das Kriterium explizieren. Letzteres kann zu schnellerer
Berechnung führen, da weniger Zwischenschritte nötig sind. Die Formelschreibweise wiederum wandelt alle kategoriellen Variablen in Dummy-Variablen um – Variablen mit nur
zwei Ausprägungen, die als 0 bzw. 1 kodiert sind. Dazu wird im Hintergrund der Befehl model.matrix() ausgeführt (Kuhn und Johnson 2013, Kapitel 14.7). Natürlich
kann man die Variablen auch schon in Dummy-Form übergeben. Verwendet man statt
6
http://appliedpredictivemodeling.com/blog/2014/11/27/vpuig01pqbklmi72b8lcl3ij5hj2qm.
408
22
Fallstudie: Kreditwürdigkeit mit caret
der Formelform x und y, so muss man kategorielle Variablen selber noch in Dummyform bringen. Analog zur Syntax von mosaic wird bei der Formelschreibweise erst die
„Zielvariable“ bzw. das Kriterium angegeben; die Tilde ~ trennt von den Prädiktoren ab.
Schreibt man einen Punkt nach der Tilde, so bedeutet das „nimm alle Variablen, nur nicht
die Zielvariable“. Mit method gibt man den Namen des statistischen Modells an, z. B.
ein Lineares Modell (lm), Generalisiertes Lineares Modell (glm) oder Random Forests
(rf). Die Namen der verfügbaren Modelle findet man in Dokumentation von caret,7 mit
?train_model_list oder mit modelLookup().
Vorsicht: Hat eine Variable zehn Stufen, so erstellt dummyVars() zehn Variablen;
das kann die Dimensionalität des Datensatzes über die Maßen aufblähen. Unter
Umständen ist man besser beraten, auf einige dieser Variablen zu verzichten. Eine
Möglichkeit ist, nur einige vom Vorwissen (Theorie) favorisierte Stufen herauszupicken und nur diese Kontraste zu betrachten.
Zentrales Argument ist method; dort gibt man den Namen des auszuführenden Modells an. Übrigens verkraften viele Modelle bei train() keine fehlenden Werte. Allerdings kann man mit dem Argument preProcess bei train() fehlende Werte imputieren (z. B. so: preProcess = "knnImpute"). Die Dokumentation online8 ist hilfreich
und schildert viele Details.
22.3.3 Ein einfaches Modell
Berechnen wir zu Beginn ein einfaches Modell – eine logistische Regression (s. Kap. 19)
mit glm(). Ach ja: Ein wesentlicher Punkt beim Modellieren ist, Wissen über den Gegenstand einzubringen. Was sind echte Ursachen, wenn jemand einen Kredit nicht zurückzahlt? Wenn man die Ursachen nicht kennt, welche Größen korrelieren zumindest
mit dem Kriterium? Nehmen wir an, Sie führen einen umfassenden Selbstversuch dazu
durch (für die Wissenschaft). Außerdem sprechen Sie mit mehreren Betroffenen. Auf dieser Basis kommen Sie zu dem Schluss, dass die Höhe des Kredits (Amount), die Dauer
der Überfälligkeit (Duration) sowie das Alter (Age) des Kreditnehmers wichtig sind.
Diese Größen nehmen Sie daraufhin als Prädiktoren in Ihr Modell. train() berechnet
das Modell für uns und nimmt – gesteuert über trControl – auch die Kreuzvalidierung
vor.
7
8
https://topepo.github.io/caret/available-models.html.
https://topepo.github.io/caret/index.html.
22.3 Modelle anpassen
409
Es ist davon abzuraten, stumpf alle Variablen des Datensatzes als Prädiktoren in
ein lineares Modell aufzunehmen. Besser ist es, eine Vorauswahl zu treffen; entweder anhand theoretischer Überlegungen oder aufgrund von statistisch basierten
Auswahlstrategien. Eine Möglichkeit dazu ist es, sich z. B. über Random Forests (s.
Abschn. 22.3.4) die Variablenwichtigkeit ausgeben zu lassen und nur diese „wichtigen“ Variablen als Prädiktoren in das lineare Modell aufzunehmen. Näheres zur
Auswahl von Prädiktoren findet sich bei James et al. (2013) oder bei Kuhn und
Johnson (2013).
set.seed(42) # Zufallszahlen fixieren
glm_fit1 <- train(Class ~ Amount + Age + Duration, # Modellgleichung
data = train, # Daten
method = "glm", # Modell
family = "binomial", # Modelldetails
trControl = my_crossval) # Kreuzvalidierung
glm_fit1
#> Generalized Linear Model
#>
#> 800 samples
#>
3 predictor
#>
2 classes: 'Bad', 'Good'
#>
#> No pre-processing
#> Resampling: Cross-Validated (10 fold, repeated 5 times)
#> Summary of sample sizes: 720, 721, 719, 720, 721, 719, ...
#> Resampling results:
#>
#>
Accuracy Kappa
#>
0.701
0.036
Mit set.seed() stellen wir sicher, dass ein reproduzierbarer Satz an Zufallszahlen gezogen wird. Das ist nützlich, wenn man die Ergebnisse nachprüfen oder verschiedene
Modelle vergleichen möchte. Beachten Sie, dass die Gesamtgenauigkeit Accuracy irreführend sein kann: Angenommen, 70 % aller Fälle sind Good, also keine Ausfälle, so kann
man ohne viel Federlesens eine Genauigkeit von 70 % erreichen – indem man für jeden
Fall auf Good tippt (d. h. immer das häufigere Ereignis vorhersagt). In dem Fall wäre zwar
die Gesamtgenauigkeit hoch, aber die Sensitivität gering (oder die Spezifität, je nachdem,
was Ereignis und was Nicht-Ereignis ist). Ein Blick auf die Details der Konfusionsmatrix
könnte dann offenbaren, dass alle Fälle nur einer Kategorie (der häufigeren) zugeordnet wurden. Für so ein Modell sind keine besonders schlauen Algorithmen notwendig;
es bietet wenig Nutzen. Allerdings ist ein solches Modell wichtig als Referenzmodell oder
Nullmodell, an dem sich Ihr Modell messen lassen muss. Hier schneidet unser GLM kaum
besser ab als das Nullmodell; Kappa liegt etwas über null.
410
22
Fallstudie: Kreditwürdigkeit mit caret
Entspricht die Genauigkeitskennzahl Accuracy in etwa dem Anteil des gesuchten Ereignisses oder seines Gegenereignisses, so ist Vorsicht geboten: Häufig wird
entweder die Sensitivität oder die Spezifität der Vorhersage schlecht ausfallen.
Analog hätten wir statt caret() die Standard-Funktion glm() verwenden können
glm_fit2 <- glm(Class ~ ., data = train, family = "binomial").
Die Modellierung nur mit glm geht meist schneller als mit caret::train(). Woran
liegt das? Der Hintergrund ist, dass caret eine Menge mehr Berechnungen anstellt als
glm(). Wie die Ausgabe von glm_fit1 zeigt, wurde das Modell kreuzvalidiert, da bei
train_control() k Faltungen, die r Mal wiederholt wurden, angegeben waren. Falls
es Tuningparameter gibt (was bei glm() nicht der Fall ist), werden in den Voreinstellungen drei Tuningwerte ausprobiert, für die jeweils kreuzvalidiert wird.
Aufgaben
1. Angenommen, die Funktion nearZeroVar() hat redundante Variablen identifiziert. Wie könnte man diese Variablen aus dem Datensatz entfernen?9
2. Wie kann man diesen Arbeitsschritt als Metadatum protokollieren? Anders gefragt: Wie kann man sicherstellen, dass die Information „Redundante Variablen
wurden aus diesem Datensatz entfernt“ an geeigneter Stelle protokolliert ist?10
3. Nehmen wir an, wir wollen die Variable x zum Datensatz GermanCredit
hinzufügen. Die Variable x sei so definiert: GermanCredit$x <- seq(1).
Handelt es sich bei x um eine Konstante, d. h. eine Spalte mit nur einer Ausprägung?11
4. Überprüfen Sie die Aufteilung der Stichprobe in ein Trainings- und ein TestSample anhand von sample().
Lösung
Erstellen wir einen einfachen Dataframe, so dass wir die Plausibilität unseres
Vorgehens in Augenschein nehmen können. Dann führen wir die exakt gleichen
Schritte durch, wie als wir das Trainings- vom Test-Sample abgespaltet haben.
Einige Tests bieten sich an; so kann man mit length() überprüfen, ob ein
Objekt die richtige Länge hat, in einigen Situationen ist das ein nützlicher Test.
Mit any(x %in% y) kann man prüfen, ob irgendein (any) Element von Vektor
9
train %>% select(-name_der_variable) -> train.
attr(train, "high corr checked") <- TRUE.
11
Ja.
10
22.3 Modelle anpassen
411
x in Vektor y zu finden ist. Das soll nicht der Fall sein in unserem Beispiel, da
die Trainings- von der Test-Stichprobe disjunkt (überlappungsfrei) sein soll.
d <- data_frame(id = 1:10)
n_train <- round(.8 * nrow(d), digits = 0)
train_index <- sample(1:nrow(d), size = n_train)
length(train_index)
d_train <- d[train_index, ]
d_test <- d[-train_index, ]
any(d_train$id %in% d_test$id)
22.3.4
Random Forest
Ein beliebtes Modell ist das Random-Forest-Modell (s. Abschn. 21.7). caret bietet mehrere Implementierungen von Random-Forest-Methoden; rf ist eine bekannte (A. Liaw
und Wiener 2002). Die aktuell neueste Implementierung ist ranger (Wright und Ziegler
2017). Die Syntax von caret bleibt weitgehend gleich, nur, dass wir Tuningparameter
von Random Forests berücksichtigen müssen. Wir spezifizieren also, wie viele Variablen in einen Baum jeweils eingehen sollen (.mtry). Außerdem geben wir an, wie viele
verschiedene
Werte von mtry ausprobiert werden sollen. Eine Faustregel für mtry ist
p
k, wobei k für die Anzahl der (gesamt verfügbaren) Prädiktoren steht. Weiter möchte
ranger gerne wissen, anhand welches Kriteriums die Aufteilung von Ästen vorgenommen werden soll. Der Gini-Koeffizient ist hier die Voreinstellung für das Entscheidungskriterium (.splitrule). Schließlich geben wir noch an, wie viele Fälle mindestens in
einem Ast verbleiben sollen (.min.node.size). Mit expand.grid() wird eine Tabelle aller Kombinationen der Tuningparameterwerte
erstellt (hier 4 2). Probieren wir
p
aufs Geratewohl einige Werte um k herum aus; einige Erfahrungswerte zeigen, dass
weniger Prädiktoren zu besseren Ergebnissen führen können, daher wählen wir den Wert
von .mtry etwas kleiner.
rf_grid <- expand.grid(.mtry = c(5, 6, 7, 8),
.splitrule = "gini",
.min.node.size = c(10, 20))
Dann spezifizieren wir den Aufruf von train() genau wie beim letzten Mal, nur dass
wir noch auf die verschiedenen Werte der (bzw. des) Tuningparameter hinweisen mit
dem Argument tuneGrid; stattdessen hätten wir auch angeben können, wie viele verschiedene Werte von Tuningparametern bzw. deren Kombinationen ausprobiert werden
sollen. So würde tuneLength = 10 zu zehn verschiedenen Wertkombinationen führen.
Mit importance = "permutation" lässt man die Variablenwichtigkeit berechnen
(s. Abschn. 22.5). Schließlich geben wir noch an, wie viele Bäume in jedem Durch-
412
22
Fallstudie: Kreditwürdigkeit mit caret
lauf berechnet werden sollen (num.trees). Möchte man Wahrscheinlichkeiten der
Klassenzugehörigkeit haben, so muss man bei trainControl() noch den Parameter
classProbs = TRUE übergeben.
set.seed(123)
rf_fit1 <- train(Class ~ .,
data = train,
method = "ranger", # Random Forest
trControl = trainControl(method = "repeatedcv",
number = 10, # k
repeats = 5, # r
allowParallel = TRUE,
verboseIter = T,
classProbs = T),
tuneGrid = rf_grid,
num.trees = 500,
importance = "permutation", # Variablenwichtigkeit
verbose = TRUE) # ausführlicher Output
rf_fit1
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
#>
Random Forest
800 samples
48 predictor
2 classes: 'Bad', 'Good'
No pre-processing
Resampling: Cross-Validated (10 fold, repeated 5 times)
Summary of sample sizes: 720, 720, 720, 720, 720, 720, ...
Resampling results across tuning parameters:
mtry min.node.size Accuracy Kappa
5
10
0.748
0.282
5
20
0.742
0.247
6
10
0.750
0.299
6
20
0.741
0.259
7
10
0.747
0.297
[ reached getOption("max.print") -- omitted 3 rows ]
Tuning parameter 'splitrule' was held constant at a value of gini
Accuracy was used to select the optimal model using the largest value.
The final values used for the model were mtry = 6, splitrule = gini
and min.node.size = 10.
Die mittleren Performanzwerte für Accuracy waren für die verschiedenen Werte von
mtry ähnlich. Das beste Modell ist im Objekt rf_fit1 unter rf_fit1$bestTune
zugänglich. Wir erfahren so, dass der beste Wert von mtry bei 6 liegt. Übergibt man
22.3 Modelle anpassen
413
das Argument metric = "Kappa", wäre Kappa als Entscheidungskriterium herangezogen worden, was zu einer anderen Entscheidung führen kann hinsichtlich der Wahl der
besten Tuningparameter. Wollen wir jetzt neue, unverbrauchte Werte vorhersagen (mit
predict()), wird das Modell wie in bestTune notiert verwendet werden. Schauen
wir also, wie es um die Genauigkeit im Test-Sample bestellt ist; postResample() gibt
Genauigkeit (Accuracy) und Cohens Kappa wieder.
rf_pred1 <- predict(rf_fit1, newdata = test)
postResample(pred = rf_pred1 , obs = test$Class)
#> Accuracy
Kappa
#>
0.930
0.832
Details inklusive Konfusionsmatrix bekommt man mit confusionMatrix(data =
rf_pred1, reference = test$Class). Ein Blick in diese detailliertere Ausgabe
zeigt, dass die Sensitivität relativ gering (0.8) und die Spezifität (0.99) hoch ist.
In diesem Fall benötigte die Berechnung relativ wenig Zeit. Je nach Art des zu berechnenden Modells und der Datengröße kann die Rechenzeit einige oder sogar viel Zeit in
Anspruch nehmen und Speicher benötigen. Random Forests sind besonders rechenintensiv. Mögliche Auswege aus einer Zeit- und Speicherkostenmisere sind:
Eine Zufallsstichprobe aus dem Trainings-Sample ziehen
Die Berechnung auf mehrere Kerne des Rechners aufteilen
Die Rechen- und Speicherkapazität des Rechners erhöhen
Effizientere Algorithmen verwenden
Weniger Faltungen und/oder Wiederholungen bei der Kreuzvalidierung vornehmen
Eine Rundum-Sorglos-Lösung gibt es wieder einmal nicht; meist tauscht man Rechenzeit gegen Genauigkeit. Natürlich kann man das Objekt, hat man es einmal berechnet,
in einer Datei speichern und von dort wieder einlesen; das spart Zeit, da man die
aufwändige Neuberechnung umgeht. Speichert man einzelne Objekte, ist die Standard-RFunktion saveRDS(rf_fit1, file = "data/rf_fit1.rds") empfehlenswert,
die ein Objekt rf_fit1 in die Datei data/rf_fit1.rds schreibt. Mit rf_fit1 <readRDS("data/rf_fit1.rds") kann man das Objekt wieder laden.
Aufgaben
1. Sagen wir, es gibt drei Geschlechter (m, w, x) und fünf Altersstufen (jj, j, m, a,
aa). Erstellen Sie einen Dataframe mit allen Kombinationen der beiden Variablen
(Geschlecht und Alter); in jeder Zeile soll eine Kombination vorkommen.12
12
expand.grid(sex = c("w", "m", "x"), age = c("jj", "j", "m", "a",
"aa")).
414
22
Fallstudie: Kreditwürdigkeit mit caret
2. Wie viele Zeilen enthält der eben erstelle Dataframe?13
3. Von welchem Datentyp ist rf_fit1?14
4. Enthält rf_fit1 Listen?15
22.3.5 Support Vector Machines
Support Vector Machines (SVM) sind eine weitere Art der prädiktiven Modellierung
(James et al. 2013) . Bei vielen Modellen ist es ratsam, die Anzahl der Prädiktoren gering
zu halten. SVM gehen einen anderen Weg: Um eine klare Abgrenzung der Fälle verschiedener Klassen zu finden, werden aus den vorhandenen Prädiktoren zusätzliche Variablen
gebildet. Es gibt mehrere Varianten dieser Methode; wir verwenden hier einen Radialen
Kernel, das impliziert eine nonlineare Transformation der Prädiktoren (Kuhn und Johnson
2013, S. 155).
Wenn auch die statistisch-mathematischen Hintergründe nicht trivial sind, so ist es
doch die Umsetzung via caret. Dieses Modell kennt zwei Tuningparameter C und Sigma.16 Doch dieses Mal kümmern wir uns nicht um die Tuningparameter, sondern weisen
train() nur an, zehn verschiedene Kombinationen auszuprobieren. Da SVM distanzbasierte Modelle sind, sollten die Variablen gleich skaliert sein; ansonsten erzeugen die
unterschiedlichen Skalierungen verzerrte Effekte. Wenn nötig, kann man die Skalierung
durch z-Standardisierung angleichen (s. Abschn. 9.3.3); train() bietet dazu das Argument preProcess.
set.seed(1056)
svm_fit1 <- train(Class ~ ., data = train,
method = "svmRadial", # Radial kernel
trControl = my_crossval,
preProcess = c("center", "scale"),
tuneLength = 10,
verboseIter = TRUE)
#svm_fit1
Die verschiedenen Werte der Tuningparameter haben einigen Einfluss auf die Modellgüte;
Kappa liegt im Bereich von 0 bis 0.35, wie ein Blick auf svm_fit1 zeigt (hier nicht
3 5 D 15.
Es ist eine Liste.
15
Ja.
16
Möchte man herausfinden, welche Tuningparameter ein Modell hat, so kann man die Funktion
caret::modelLookup("svmRadial") verwenden; man übergibt der Funktion den Namen
des Modells.
13
14
22.3 Modelle anpassen
415
abgedruckt). Offenbar lohnt es sich, die Tuningparameter gut einzustellen. Probieren wir
einige Werte um den C -Wert herum, die zum besten Kappa-Wert führten:
svm_grid <- expand.grid(C = c(3.5, 3.75, 4.00, 4.25),
sigma = c(0.01, 0.015, .02, .05, .1))
Mit diesem 4 5-Grid passen wir das SVM-Modell noch einmal an:
set.seed(123)
svm_fit2 <- train(Class ~ ., data = train,
method = "svmRadial", # Radial kernel
trControl = my_crossval,
preProcess = c("center", "scale"),
tuneGrid = svm_grid)
#svm_fit2
Ruft man das Ausgabeobjekt svm_fit2 auf, so bekommt man einen Überblick über die
Modellgüte im Trainings-Sample. Die beste Kombination von Tuningparametern ist im
Objekt bestTune gespeichert:
svm_fit2$bestTune
#>
sigma C
#> 11 0.01 4
Diese Tuningwerte werden zur Vorhersage neuer Werte verwendet, wenn die Funktion
predict() mit dem Output-Objekt von train() aufgerufen wird; das schauen wir uns
in Abschn. 22.4.1 an.
Achtung! Das Ausprobieren von verschiedenen Werten der Modellparameter ist
nur im Trainings-Sample erlaubt. Die Modellgüte muss anhand eines Datensatzes
geprüft werden, der nicht zum Ausprobieren verwendet wurde (das Test-Sample).
Andernfalls ist die Gefahr, Zufallsrauschen zu interpretieren, hoch.
Aufgaben
1. Wie kann man sich die Sensitivität und Spezifität von svm_fit2 ausgeben lassen?17
17
cm_svm2 <- confusionMatrix(data = svm_pred2, reference =
test$Class).
416
22
Fallstudie: Kreditwürdigkeit mit caret
2. Ist es sinnvoll, für jede Kombination von Tuningparametern die Modellgüte im
Test-Sample zu überprüfen?18
3. Wie kann man herausfinden, welche Tuningparameter ein Modell hat?19
4. Wie kann man herausfinden, ob SVM-Varianten von caret angeboten werden?20
5. Wie viele Zeilen (Kombinationen) hat das Tune-Grid svm_grid?21
6. Betrachten Sie das Objekt svm_fit1; handelt es sich um eine Liste?22
7. Betrachten Sie die Struktur von svom_fit1 und prüfen Sie, ob wirklich insgesamt zehn verschiedene Modelle berechnet bzw. Tuningparameter durchprobiert
wurden.23
22.3.6
Penalisierte lineare Modelle
Eine wichtige Variante von linearen Modellen sind sog. penalisierte oder regularisierte
Modelle (penalized models; „bestrafte Modelle“ hört sich komisch an). Dabei handelt es
sich um lineare Modelle, bei denen ein Strafterm zum zu optimierenden Term addiert wird
(James et al. 2013). Jedes Modell hat ein Ziel, das es möglichst gut erreichen möchte; formaler ausgedrückt, gibt es eine Funktion, deren Ergebnis optimiert werden soll. In diesem
Fall ist es die Likelihood des Modells. Die Likelihood gibt an, wie plausibel eine Stichprobe unter einer Hypothese bzw. unter Annahme bestimmter Modellparameter ist (James
et al. 2013). Die Modellparameter werden dann so gewählt, dass die Likelihood möglichst
gut ist.24 Der Strafterm, der bei penalisierten Modellen zum Likelihoodwert addiert wird,
hat zur Konsequenz, dass der Absolutbetrag der Einflussgewichte der Prädiktoren kleiner
wird bzw. gegen Null geht. Im Gegensatz zu normalen linearen Regressionen, bei denen
die Auswahl der Prädiktoren eine vorausgeschaltete Frage ist, bekommt man mit diesen
Modellen eine Prädiktorenauswahl mitgeliefert. Mehrere Varianten dieser Modelle existieren; eines davon heißt „Lasso“ (kommt aus dem Westen der USA). Darüber hinaus sind
Ridge Regression und Elastic Net bekannt. Die Tuningparameter sind (Lambda), was
bestimmt, wie stark große Koeffizienten bestraft werden (Default ist 100), und Ë›, welches
zwischen den Extremen Lasso und Ridge abwägt. Das Elastic Net ist eine Mischung der
beiden anderen Arten.
18
Nein, die Gefahr, Zufallsrauschen überzubewerten, ist zu groß. Das Test-Sample sollte nur einmal
verwendet werden. Das Modell ist immer an anderen Daten zu berechnen als zu testen.
19
modelLookup("model_name").
20
Z. B. kann man in dieser Tabelle suchen: https://topepo.github.io/caret/available-models.html.
21
4 5 D 20.
22
Ja.
23
str(svm_fit1$results).
24
Die besten Likelihood-Werte werden bestimmt durch bestimmte mathematische Verfahren wie
Ableiten und Nullsetzen der Ableitung oder durch Ausprobieren.
22.3 Modelle anpassen
417
Definieren wir das Tuning-Grid, hier mit 10 10 verschiedenen Elementen:
glmnet_grid = expand.grid(alpha = seq(0, 1, length = 10),
lambda = 10^seq(-2, 2, length = 10))
Dann rufen wir wie gewohnt train() auf:
set.seed(123)
glmnet_fit1 <- train(Class ~ .,
method = "glmnet",
data = train,
trControl = my_crossval,
tuneGrid = glmnet_grid)
#glmnet_fit1
Aufgaben
1. Wie kann man herausfinden, was die erste Stufe (der erste Level) des Faktors
train$Class ist?25
2. Wie viel Prozent der Fälle im Trainings- bzw. im Test-Level sind vom Typ
Good?26
3. Wie würde man die logistische Regression mit Methoden des Standard-R in diesem Fall berechnen?27
4. Die erste Faktorstufe eines Faktors nennt man auch die Referenzstufe; wird die
Referenzstufe als das dazu modellierende Ereignis in einer (binären) Klassifikation verstanden, z. B. von glm?28
5. Berechnen Sie das Model glm_fit1 mit der Funktion glm() und vergleichen
Sie das Ergebnis. Beziehen Sie sich auf den Datensatz train.
Lösung
glm_fit2 <- glm(Class ~ Amount + Age + Duration,
data = train,
family = "binomial")
broom::tidy(glm_fit2)
25
Z. B. so attr(x = train$Class, which = "levels")[1]; mit str(train$Class)
bekommt man die Information in ähnlicher Form. Schließlich kann man auch
levels(test$Class) verwenden.
26
Mit count(train, Class) bekommt man die absoluten Häufigkeiten; die Anteile bekommt man einfach mit mosaic::tally(~Class, data = train, format =
"proportion").
27
glm1 <- glm(Class ~ Amount + Age + Duration, family =
"binomial", data = train).
28
Nein, die erste Faktorstufe wird als null verstanden; bei test$Class ist das Bad. Die zweite
Faktorstufe ist das zu modellierende Ereignis; hier ist das Good.
418
22
Fallstudie: Kreditwürdigkeit mit caret
#> # A tibble: 4 x 5
#>
term
estimate std.error statistic p.value
#>
<chr>
<dbl>
<dbl>
<dbl>
<dbl>
#> 1 (Intercept) 0.947
0.307
3.08 0.00207
#> 2 Amount
-0.0000624 0.0000327
-1.91 0.0564
#> 3 Age
0.0181
0.00758
2.38 0.0171
#> 4 Duration
-0.0216
0.00799
-2.70 0.00693
broom::glance(glm_fit2)
#> # A tibble: 1 x 7
#>
null.deviance df.null logLik
AIC
BIC deviance df.residual
#>
<dbl>
<int> <dbl> <dbl> <dbl>
<dbl>
<int>
#> 1
969.
799 -468. 943. 962.
935.
796
train$pred_glm_fit2 <- predict(glm_fit2, type = "response")
threshold <- 0.5
confusionMatrix(factor(train$pred_glm_fit2 > threshold),
factor(train$Class == "Good"),
positive = "FALSE")
#> Confusion Matrix and Statistics
#>
#>
Reference
#> Prediction FALSE TRUE
#>
FALSE
13
17
#>
TRUE
222 548
#>
#>
Accuracy : 0.701
#>
95% CI : (0.668, 0.733)
#>
No Information Rate : 0.706
#>
P-Value [Acc > NIR] : 0.638
#>
#>
Kappa : 0.034
#> Mcnemar's Test P-Value : <2e-16
#>
#>
Sensitivity : 0.0553
#>
Specificity : 0.9699
#> [ reached getOption("max.print") -- omitted 9 rows ]
22.4
22.4.1
Modellgüte bestimmen
Modellgüte in der Test-Stichprobe
Prüfen wir nun die Vorhersagen jedes Modells; damit können wir beurteilen, wie „gut“
jedes Modell abgeschnitten hat. Im ersten Schritt berechnen wir die Vorhersagen jedes
Modells (mit predict()); für jedes Modell entsteht so eine neue Spalte. In dieser Spalte
22.4 Modellgüte bestimmen
419
„sagt“ das Modell, welchen Wert (bzw. welche Klasse) es für jede Person im Test-Sample
vorhersagt:
test %>%
mutate(glm1_pred = predict(glm_fit1, newdata = test),
rf_pred = predict(rf_fit1, newdata = test),
svm_pred1 = predict(svm_fit1, newdata = test),
svm_pred2 = predict(svm_fit2, newdata = test),
glmnet_pred = predict(glmnet_fit1, newdata = test)) -> test
test %>%
select(contains("_pred")) %>%
tail()
#>
glm1_pred rf_pred svm_pred1 svm_pred2 glmnet_pred
#> 195
Good
Good
Good
Good
Good
#> 196
Good
Good
Good
Good
Good
#> 197
Good
Good
Good
Good
Good
#> 198
Good
Good
Good
Good
Good
#> [ reached getOption("max.print") -- omitted 2 rows ]
In der Voreinstellung gibt predict() (für ein Objekt von train()) die vorhergesagte
Klasse wieder. Möchte man lieber die Wahrscheinlichkeiten, so kann man das Argument
type = "prob" an predict() übergeben. Zu beachten ist, dass nicht jedes Modell
Klassenwahrscheinlichkeiten ausgibt.
Mit confusionMatrix() kann man sich einen breiteren Überblick über die Klassifikationskennzahlen ausgeben lassen; als Eingabe verlangt die Funktion die vorhergesagten Werte (data) und die Referenz- bzw. beobachteten Werte (reference). Diese
Funktion liefert ein Objekt zurück, von dem die Ausgabe am Bildschirm abgeleitet wird.
Mit str(cm_GermanCredit)kann man herausfinden, welche Informationen unter welchem Namen in dem Objekt gespeichert sind.
cm_GermanCredit <- confusionMatrix(data = test$rf_pred, reference = test$Class)
cm_GermanCredit
#> Confusion Matrix and Statistics
#>
#>
Reference
#> Prediction Bad Good
#>
Bad
52
1
#>
Good 13 134
#>
#>
Accuracy : 0.93
#>
95% CI : (0.885, 0.961)
#>
No Information Rate : 0.675
#>
P-Value [Acc > NIR] : < 2e-16
#>
#>
Kappa : 0.832
#> Mcnemar's Test P-Value : 0.00328
420
22
Fallstudie: Kreditwürdigkeit mit caret
0.75
Wert
coefficient
Accuracy
0.50
Kappa
0.25
glm1_pred
glmnet_pred
svm_pred1
svm_pred2
rf_pred
reorder(Modell, Wert)
Abb. 22.1 Performanzwerte der getesteten Modelle (Test-Sample)
#>
#>
#>
#>
Sensitivity : 0.800
Specificity : 0.993
Betrachten wir die Genauigkeitswerte (Accuracy und Kappa) der getesteten Modelle. Dazu wählen wir die Spalten mit den vorhergesagten Werten aus und übergeben jede dieser
Spalten an confusionMatrix().
test %>%
select(contains("_pred")) %>%
map_df(~confusionMatrix(data = .,
reference = test$Class)$overall[c("Accuracy",
"Kappa")]) %>%
mutate(coefficient = c("Accuracy", "Kappa")) -> test_accuracies
test_accuracies
#> # A tibble: 2 x 6
#>
glm1_pred rf_pred svm_pred1 svm_pred2 glmnet_pred coefficient
#>
<dbl>
<dbl>
<dbl>
<dbl>
<dbl> <chr>
#> 1
0.685
0.93
0.86
0.88
0.755 Accuracy
#> 2
0.0611
0.832
0.662
0.717
0.357 Kappa
Wie man sieht, schneidet das GLM am schlechtesten ab. Die Gütekennzahlen kann man
natürlich visualisieren (s. Abb. 22.1).
test_accuracies %>% # in langes Format umwandeln
gather(key = "Modell", value = "Wert", -coefficient) %>%
ggplot() +
aes(x = reorder(Modell, Wert),
y = Wert,
color = coefficient,
shape = coefficient
) +
geom_point(size = 3)
22.4 Modellgüte bestimmen
421
Accuracy (Repeated Cross-Validation)
0.750
0.747
Minimal Node Size
10
20
0.745
0.742
5
6
7
8
#Randomly Selected Predictors
Abb. 22.2 Der Einfluss des Tuningparameters beim Random-Forest-Modell auf die Modellgüte in
der Kreuzvalidierung
22.4.2
Modellgüte in der Kreuzvalidierung
In diesem Fall ist die Stichprobe relativ groß, wobei es schwer ist, „groß“ bzw. „groß
genug“ zu definieren. Daher betrachten wir auch noch die (Variabilität der) Modellgüte
in den verschiedenen Stichproben im Rahmen der Kreuzvalidierung. caret bietet einige
Funktionen, um die Modellgüte innerhalb der Kreuzvalidierung zu betrachten – sowohl
innerhalb eines Modells als auch zwischen Modellen. Möchte man innerhalb eines Modells die verschiedenen Stichproben der Kreuzvalidierung vergleichen, so kann man sich
die Einfluss der Tuningparameter auf die Modellgüte mit plot() visualisieren lassen (s.
Abb. 22.2).
ggplot(rf_fit1)
Da glm_fit1 keinen Tuningparameter aufweist, kann man es nicht in dieser Form darstellen. Hingegen ist es möglich, sich ohne Berücksichtigung der oder des Tuningparameters die Modellgüte zu veranschaulichen (s. Abb. 22.3).
densityplot(glm_fit1)
Eine andere Möglichkeit besteht darin, die Kreuzvalidierungen zwischen den Modellen zu
vergleichen; wie gesagt, ist dieses Vorgehen besonders bei kleinen Stichproben empfehlenswert. Zu diesem Zweck gibt es in caret die Funktion resamples(); die Ergebnisse
422
22
Fallstudie: Kreditwürdigkeit mit caret
Density
15
10
5
0
0.65
0.70
0.75
Accuracy
Abb. 22.3 Das Profil der Modellgüte des GLM-Modells in den Stichproben der Kreuzvalidierung
0.0
0.2
Accuracy
0.4
0.6
0.8
Kappa
svm2
rf
glmnet
svm1
glm
0.0
0.2
0.4
0.6
0.8
Abb. 22.4 Modellgüte zwischen den Modellen in der Kreuzvalidierung
kann man sich z. B. in einem Boxplot ausgeben lassen (s. Abb. 22.4). Man beachte, dass
die Kreuzvalidierung sich auf das Trainings-Sample bezieht. Die Werte aus Abb. 22.4
decken sich im Großen und Ganzen mit denen des Test-Samples in Abb. 22.1.
model_results <- resamples(list(glm = glm_fit1,
rf = rf_fit1,
svm1 = svm_fit1,
svm2 = svm_fit2,
glmnet = glmnet_fit1))
bwplot(model_results)
Aufgaben
1. Vergleichen
Sie die Gesamtgenauigkeit im Test-Sample (Datensatz
GermanCredit) für ein Modell mit vs. ein Modell ohne Kreuzvalidierung. Ziehen Sie dafür eine Stichprobe von n D 100 und teilen Sie 20 Fälle für
die Test-Stichprobe ab. Nehmen Sie nur diese Prädiktoren auf: Amount, Age
und Duration.
Lösung
set.seed(1234567)
data_small <- sample_n(drop_na(GermanCredit),
size = 100)
22.4 Modellgüte bestimmen
423
Trainings_Faelle <- createDataPartition(data_small$Class,
p = .79)
Trainings_Faelle <- unlist(Trainings_Faelle)
train_small <- data_small %>%
dplyr::filter(row_number() %in% Trainings_Faelle)
test_small <- data_small %>%
dplyr::filter(!(row_number() %in% Trainings_Faelle))
# glm ohne Kreuzvalidierung
glm1_small <- glm(Class ~ Amount + Age + Duration,
data = train_small,
family = "binomial")
glm1_small_pred_prob <- predict(glm1_small,
newdata = test_small,
type = "response")
glm1_small_pred_class <- factor("Bad", levels = c("Bad", "Good"))
glm1_small_pred_class[glm1_small_pred_prob > .5] <- "Good"
confusionMatrix(glm1_small_pred_class,
reference = test_small$Class)$overall["Accuracy"]
# glm mit Kreuzvalidierung
glm_fit1_small <- train(Class ~ Amount + Age + Duration,
data = train_small,
method = "glm",
family = "binomial",
trControl = my_crossval)
glm_fit1_small_pred_class <- predict(glm_fit1_small,
newdata = test_small)
confusionMatrix(glm_fit1_small_pred_class,
test_small$Class)$overall["Accuracy"]
2. Wie verändert sich die Vorhersagegüte für Class, wenn die Stichprobengröße
sich verändert? Untersuchen Sie folgende Stichprobengrößen: n D 50, n D 100,
n D 500 und n D 1000; davon ist jeweils die Trainings- und die Test-Stichprobe
abzuteilen. Untersuchen Sie dabei für jede Stichprobengröße, ob – und wenn ja,
wie viel – die kreuzvalidierte Variante überlegen ist.
Lösung
Zuerst definieren wir eine Funktion, die die Hauptarbeit verrichtet (s.
Abschn. 28.1). Die Funktion, nennen wir sie compare_accuracies(), berechnet die Genauigkeit der Klassifikation im Test-Sample einmal ohne und
einmal mit Resampling. Als Argument kann man die Stichprobengröße einsetzen, wobei n D 100 als Voreinstellung gesetzt wird. Der Datensatz der Funktion
424
22
Fallstudie: Kreditwürdigkeit mit caret
ist „hardverdrahtet“; besser bzw. allgemeiner wäre es, den Datensatz als Argument (ggf. mit Voreinstellung) zu übernehmen. Aber wir machen es uns hier aus
Gründen der schnellen Zugänglichkeit etwas einfacher:
compare_accuracies <- function(sample_size = 100){
set.seed(1234567) # Zufallszahlen fixieren
data_small <- sample_n(drop_na(GermanCredit),
size = sample_size)
Trainings_Faelle <- createDataPartition(data_small$Class,
p = .79)
Trainings_Faelle <- unlist(Trainings_Faelle)
train_small <- data_small %>% dplyr::filter(row_number() %in%
Trainings_Faelle)
test_small <- data_small %>% dplyr::filter(!(row_number() %in%
Trainings_Faelle))
# glm ohne Kreuzvalidierung
glm1_small <- glm(Class ~ Amount + Age + Duration,
data = train_small,
family = "binomial")
glm1_small_pred_prob <- predict(glm1_small,
newdata = test_small,
type = "response")
glm1_small_pred_class <- factor("Bad", levels = c("Bad", "Good"))
glm1_small_pred_class[glm1_small_pred_prob > .5] <- "Good"
# glm mit Kreuzvalidierung
glm_fit1_small <- train(Class ~ Amount + Age + Duration,
data = train_small,
method = "glm",
family = "binomial",
trControl = my_crossval)
glm_fit1_small_pred_class <- predict(glm_fit1_small,
newdata = test_small)
# Accuracies:
cm_no_resampling <- confusionMatrix(glm_fit1_small_pred_class,
test_small$Class)
Accuracy_no_resampling <- cm_no_resampling$overall["Accuracy"] %>%
unname()
cm_with_resampling <- confusionMatrix(glm1_small_pred_class,
reference = test_small$Class)
Accuracy_with_resampling <- cm_with_resampling$overall["Accuracy"] %>%
unname()
22.4 Modellgüte bestimmen
425
results <- list(cm_no_resampling,
Accuracy_no_resampling,
cm_with_resampling,
Accuracy_with_resampling)
return(results)
}
Als Nächstes lassen wir die Funktion für verschiedenen Stichprobengrößen
durchlaufen:29
run_50 <- compare_accuracies(50)
run_100 <- compare_accuracies(100)
run_500 <- compare_accuracies(500)
run_1000 <- compare_accuracies(1000)
0.76
accuracies
0.74
resampling
0.72
no
yes
0.70
0.68
50 100
500
1000
sample_size
Abb. 22.5 Vergleich der Vorhersagegüte eines Modells mit Kreuzvalidierung und eines
ohne
Schließlich fassen wir die Ergebnisse in einem Dataframe zusammen und visualisieren diesen (s. Abb. 22.5).
simu_results <- data.frame(
accuracies = c(run_50[[2]], run_50[[4]],
run_100[[2]], run_100[[4]],
run_500[[2]], run_500[[4]],
run_1000[[2]], run_1000[[4]]),
resampling = rep(c("no", "yes"), times = 4),
sample_size = c(50, 50, 100, 100, 500, 500, 1000, 1000))
29
Mit debug(compare_accuracies) kann man eine Funktion debuggen, d. h. auf Fehler prüfen.
426
22
Fallstudie: Kreditwürdigkeit mit caret
simu_results %>%
ggplot(aes(x = sample_size,
y = accuracies,
color = resampling,
shape = resampling)) +
geom_point(size = 4) +
geom_line(aes(group = resampling), alpha = .7) +
scale_x_continuous(breaks = c(50, 100, 500, 1000))
Das Ergebnis zeigt uns, dass das kreuzvalidierte Modell in drei von vier Fällen
besser abschnitt als das Modell ohne Kreuzvalidierung. Für starke Schlüsse war
die kurze Simulationsstudie sicher zu schwach, aber sie zeigt Wege auf, um den
Nutzen einer Methode, wie Kreuzvalidierung, zu überprüfen.
22.5
Wichtigkeit der Prädiktoren bestimmen
Neben den Vorhersagen bzw. der Genauigkeit der Vorhersage gilt das Interesse häufig der
Frage, welche Variablen die vorherzusagende Größe maßgeblich oder besonders stark beeinflussen. Da kausale Aussagen zumeist nicht möglich sind, lautet die Frage vorsichtiger
formuliert, welche Variablen das Kriterium besonders gut erklären im Sinne von statistisch beeinflussen. Solche Erklärungsansätze lassen zumeist die Größe der Stichprobe und
stochastische Überlegungen außen vor. Es interessiert natürlich der unique Erklärungsbeitrag einer Variablen, also ein Erklärungsbeitrag einer Variablen, der nicht durch andere
Variablen im Modell erklärt wird. Dabei darf man sich nicht der Illusion hingeben, dass
es keine beeinflussenden oder erklärenden Variablen außerhalb des eigenen Modells gäbe. Was sich trivial anhört, lässt sich in der Praxis aber immer wieder als übergewisse
Behauptungen (Übergewissheit, Overcertainty); vgl. Briggs (2016) finden.
Im Rahmen klassischer Statistik spricht man von Effektstärke; in der Datenwissenschaft (Data Science) oder im Maschinenlernen eher von Variablenwichtigkeit (variable
importance). Die unterschiedliche Begrifflichkeit rührt wohl daher, dass die neuen Modelle der Datenwissenschaft zumindest zum Teil nicht mit den herkömmlichen Konzepten
der Effektstärke zu fassen sind. Hier wird der Begriff der Variablenwichtigkeit verwendet
(Kuhn und Johnson 2013; Zumel et al. 2014). Grundsätzlich kann man zwei Arten von
Variablenwichtigkeit unterscheiden: Einerseits allgemeine oder modellunabhängige Koeffizienten und andererseits Koeffizienten, die sich aus einem Modell ergeben. Dann muss
noch zwischen Regression und Klassifikation unterschieden werden; außerdem spielt es
eine Rolle, ob ein Prädiktor kategoriell oder numerisch ist (Kuhn und Johnson 2013).
Bei kategoriellen Prädiktoren kann man noch fragen, ob zwei oder mehr Klassen zu unterscheiden sind. Im Folgenden betrachten wir nur modellunabhängige (oder generische)
Koeffizienten, der allgemeineren Gültigkeit wegen.
22.5 Wichtigkeit der Prädiktoren bestimmen
22.5.1
427
Modellunabhängige Variablenwichtigkeit für Klassifikation
Bei numerischen Prädiktoren bietet es sich an, die AUC (s. Kap. 19.8) zu berechnen,
jeweils pro Prädiktor und pro Klasse (Kuhn und Johnson 2013, Kapitel 18.3). caret
bietet dazu eine Funktion, filterVarImp(), die auf das Paket pRoc zurückgreift. Bei
nur zwei Kategorien (z. B. Bad und Good) sind die Gütewerte zwischen den Kategorien
identisch.
filterVarImp(x = select(test, -Class),
y = test$Class) %>%
head()
#>
Bad Good
#> Duration
0.723 0.723
#> Amount
0.578 0.578
#> InstallmentRatePercentage 0.576 0.576
#> ResidenceDuration
0.554 0.554
#> Age
0.588 0.588
#> NumberExistingCredits
0.553 0.553
Bei dichotomen Prädiktoren bietet sich der Odds Ratio (OR) an, der definiert ist als
p=.1 p/, wobei p die Wahrscheinlichkeit (relative Häufigkeit) eines Ereignisses angibt. Über die Funktion stats::fisher.test() kann man sich das Odds Ratio sowie
den p-Wert über den exakten Fisher-Permutationstest ausgeben lassen.
tab1 <- table(test$EmploymentDuration.Unemployed,
test$Class)
fi_test1 <- fisher.test(tab1)
Diese Funktion gibt ein Objekt zurück, in der das Odds Ratio unter dem Namen
estimate gespeichert ist:
str(fi_test1)
#> List of 7
#> $ p.value
: num 0.141
#> $ conf.int
: num [1:2] 0.177 1.369
#>
..- attr(*, "conf.level")= num 0.95
#> $ estimate
: Named num 0.49
#>
..- attr(*, "names")= chr "odds ratio"
#> $ null.value : Named num 1
#>
..- attr(*, "names")= chr "odds ratio"
#> $ alternative: chr "two.sided"
#> $ method
: chr "Fisher's Exact Test for Count Data"
#> $ data.name : chr "tab1"
#> - attr(*, "class")= chr "htest"
428
22
Fallstudie: Kreditwürdigkeit mit caret
Dieses Prozedere wiederholt man dann mit allen Prädiktoren. Etwas schicker ist es, eine
Art kleine Schleife zu schreiben. Dazu legt man sich zuerst einen Vektor mit den binären
Prädiktoren an:
binary_predictors <- test %>%
select(8:62, -Class) %>%
names()
head(binary_predictors)
#> [1] "Telephone"
#> [3] "CheckingAccountStatus.lt.0"
#> [5] "CheckingAccountStatus.gt.200"
"ForeignWorker"
"CheckingAccountStatus.0.to.200"
"CheckingAccountStatus.none"
Genau wie beim Trainingsdatensatz entfernt man noch die Variablen ohne Varianz:
test %>%
nearZeroVar(saveMetrics = TRUE) %>%
rownames_to_column() %>%
filter(nzv == TRUE) %>%
pull(rowname) -> test_nzv
Dann wendet man auf jedes Element von binary_predictors den Fisher-Test an
(map(~fisher.test(.))). Schließlich zieht man dann von jedem Element das Objekt estimate heraus. Im letzten Schritt wird der Datensatz noch etwas frisiert, von breit
auf lang umgebaut und nach den Werten des Odds Ratios sortiert:
test %>%
select(one_of(binary_predictors)) %>%
select(-one_of(test_nzv)) %>%
map(~table(., test$Class)) %>%
map(~fisher.test(.)) %>%
map_df("estimate") %>%
gather(key = "predictor", value = "OR") %>%
arrange(-OR)
#> # A tibble: 42 x 2
#>
predictor
OR
#>
<chr>
<dbl>
#> 1 SavingsAccountBonds.gt.1000
6.21
#> 2 CheckingAccountStatus.gt.200 5.09
#> 3 CheckingAccountStatus.none
3.84
#> # ... with 39 more rows
map() ordnet jeder Spalte von test einen Befehl zu (z. B. table(). Mehr zu map()
findet sich im Abschn. 28.2.
22.5 Wichtigkeit der Prädiktoren bestimmen
429
Aufgaben
1. Suchen Sie oder schreiben Sie eine Funktion is_binary(), die aus einem
Dataframe alle binären Variablen (mit nur zwei Ausprägungen) identifiziert.
Lösung
is_binary <- function(col){
n_unique <- length(unique(col))
result <- ifelse(n_unique == 2, TRUE, FALSE)
return(result)
}
Die Helferfunktion is_binary() erwartet einen Vektor (Spalte eines Dataframes) als Eingabe und gibt TRUE oder FALSE zurück, je nachdem, ob die
Spalte eine binäre Variable darstellt (d. h. genau zwei Ausprägungen aufweist)
oder nicht. Dann übergeben wir jede binäre Spalte an den Fishertest und ziehen
das Odds Ratio heraus.
2. Nutzen Sie die Funktion is_binary(), um alle binären Variablen aus dem
Datensatz test zu identifizieren.30
3. Wie kann man aus einem Dataframe alle die Variablen auswählen, die von
is_binary() gefunden wurden?31
22.5.2
Modellunabhängige Prädiktorenrelevanz bei numerischen
Vorhersagen
Auch bei der Vorhersage von numerischen Kriterien kann man zwei Fälle unterscheiden,
wenn es darum geht, die Wichtigkeit der Prädiktoren zu untersuchen: Erstens numerische Prädiktoren und zweitens kategoriale Prädiktoren. In beiden Fällen wird eine generische, nicht modellspezifische Berechnung (bzw. Schätzung) der Prädiktorenrelevanz
vorgenommen; es ist nicht nötig, das Modell mit train() vorher anzupassen. Die Funktion filterVarImp() berechnet die Kennwerte aus den Rohdaten.
Bei numerischen Prädiktoren berechnet caret::train() den Absolutbetrag des
t-Werts des Prädiktors32 als das Maß der Variablenwichtigkeit; das ist nicht ideal
30
test %>% summarise_all(is_binary) %>% gather %>% filter(value ==
TRUE) -> test_binaries.
31
test %>% select(one_of(test_binaries$key)).
32
https://topepo.github.io/caret/variable-importance.html.
430
22
Fallstudie: Kreditwürdigkeit mit caret
(s. Abschn. 18.8). Ein Problem dieser Art, die Variablenwichtigkeit zu bemessen, ist,
dass etwaige Verzerrungen durch Drittvariablen unberücksichtigt bleiben. Ein verwandtes
Problem ist, dass solche univariaten Einfluss-Statistiken keine Interaktionen berücksichtigen: Zwei Prädiktoren können jeweils mit dem Kriterium unkorreliert sein, aber ihre
Interaktion kann mit dem Kriterium korreliert sein. Anstelle des linearen Korrelationskoeffizienten kann es Sinn machen, Spearmans Korrelationskoeffizienten (s. Abschn. 8.2)
heranzuziehen oder eine lokal gewichtete Polynomregression (LOESS) zu verwenden (Kuhn und Johnson 2013, Kapitel 18.1). filterVarImp() mit dem Argument
nonpara = TRUE (für nonparametrische Regression) berechnet den LOESS. Betrachten wir dazu ein Beispiel aus dem Datensatz stats_test:
stats_test %>%
select_if(is.numeric) %>%
drop_na() %>%
filterVarImp(x = .,
y = .$score,
nonpara = TRUE)
#>
Overall
#> study_time 0.1778
#> self_eval
0.3831
#> interest
0.0502
#> score
1.0000
#> row_number 0.0116
Bei kategorialen Prädiktoren ist es erhellend, den Kriteriumswert pro Gruppe zu vergleichen: Wie ist das Interesse am Fach (Kriterium) bei Studenten, die bestanden bzw.
nicht bestanden haben? Mit einem d -Wert (s. Kap. 16.5.2.1) können die Größe des Unterschieds und damit die Wichtigkeit der Gruppierungsvariablen quantifiziert werden. Hat
die Variable mehrere Stufen, so kann eine Varianzanalyse (s. Kap. 16.4.3) zum Einsatz
kommen. Alternativ oder nachgeschaltet kann man die mehrstufige Variable in DummyVariablen aufteilen; zum Aufteilen in Dummy-Variablen hilft model.matrix() oder
caret::dummyVars(). caret berechnet in diesem Fall die Fläche unter der Kurve
(AUC; s. Abschn. 19.8). Auf der Hilfeseite der Funktion finden sich weitere Hinweise
(filterVarImp()).
22.5.3
Modellabhängige Variablenwichtigkeit
Einige Modelle lassen eigene Ansätze zu, um die Wichtigkeit von Variablen zu bestimmen. Ein typisches Maß zur Wichtigkeit von Variablen bei Random-Forest-Modellen
ist es, die Werte eines Prädiktors zu permutieren (wild durchzumischen) und dann zu
schauen, um wie viel sich die Klassifikation im Schnitt über alle Bäume hinweg dadurch
22.5 Wichtigkeit der Prädiktoren bestimmen
431
verschlechtert. Die Idee dabei ist, dass die Höhe der Verschlechterung in direkter Beziehung zur Wichtigkeit der Variablen steht. Entsprechend nennt man dieses Maß auch Mean
Decrease in Accuracy. Komfortablerweise hat caret::varImp() für viele Modelle deren eigene bzw. passende Wichtigkeitskoeffizienten schon implementiert. Die Benutzung
ist einfach: varImp(caret_objekt).
varImp(rf_fit1)$importance %>%
rownames_to_column %>%
arrange(-Overall) %>%
head
#>
rowname Overall
#> 1 CheckingAccountStatus.none
100.0
#> 2 CheckingAccountStatus.lt.0
61.3
#> 3
Duration
46.7
#> 4
Amount
44.8
#> 5
CreditHistory.Critical
33.6
#> 6 SavingsAccountBonds.lt.100
27.7
varImp(glm_fit1)
#> glm variable importance
#>
#>
Overall
#> Duration
100.0
#> Age
60.2
#> Amount
0.0
Aufgaben
Sie sind dazu gezwungen worden, herauszufinden, welche Faktoren den Hauspreis
in einer nordamerikanischen Metropolregion in die Höhe treiben. Dazu nutzen Sie
den Datensatz Boston aus dem Paket MASS. Bearbeiten Sie dazu die folgenden
Aspekte:
1.
2.
3.
4.
5.
Laden Sie den Datensatz und verschaffen Sie sich einen Überblick.
Erstellen Sie ein Histogramm für jede numerische Variable.
Bereiten Sie eine zehnfache Kreuzvalidierung mit fünffacher Wiederholung vor.
Teilen Sie den Datensatz 80:20 in eine Trainings- und eine Test-Stichprobe auf.
Berechnen Sie ein Random-Forest-Modell mit fünf verschiedenen Werten von
.mtry. Die vorherzusagende Variable ist der Hauswert; nehmen Sie alle übrigen
Variablen als Prädiktor auf.
6. Berichten Sie relevante Gütekoeffizienten des Modells.
7. Benennen Sie die wichtigsten Einflussgrößen (laut diesem Modell).
432
22
Fallstudie: Kreditwürdigkeit mit caret
Lösung
1. Laden Sie den Datensatz und verschaffen Sie sich einen Überblick.
# Laden und Betrachten:
data(Boston, package = "MASS")
glimpse(Boston)
# Nur numeric/ integer betrachten:
Boston %>%
select_if(is.numeric)
# nur *nicht* numerische Variablen betrachten:
Boston %>%
select_if(Negate(is.numeric))
# Deskriptive Statistiken für alle Variablen:
Boston %>%
skimr::skim()
# Anzahl fehlender Werte:
sum(is.na(Boston))
2. Erstellen Sie ein Histogramm für jede numerische Variable.
Da alle Variablen numerisch sind (s. vorherige Aufgabe), können wir alle Variablen wählen, ohne eine Vorauswahl zu treffen.
Boston_long <- tidyr::gather(Boston)
ggplot(Boston_long, aes(value)) +
geom_density() + facet_wrap(~key, scales = 'free')
Alternativ, aber komplizierter geht es so:
Boston %>%
map2(., names(.),
~{ggplot(data_frame(.x), aes(.x)) +
geom_histogram() +
labs(x = .y,
title = .y)
})
1. Nimm den Datensatz Boston UND DANN
2. Ordne jede Spalte und den Namen (names()) jeder Spalte folgendem
Befehl zu:
3. dl ggplot(); die ggplot-Gruppe soll als Ganze geplottet werden,
daher die geschweifte Klammer f. Mit .x werden die Spalten angesprochen und mit .y die Namen der Spalten.
22.5 Wichtigkeit der Prädiktoren bestimmen
433
3. Bereiten Sie eine zehnfache Kreuzvalidierung mit fünffacher Wiederholung vor.
k <- 10
r <- 5
my_crossval <- trainControl(method = "repeatedcv",
number = k,
repeats = r)
4. Teilen Sie den Datensatz 80 : 20 in eine Trainings- und eine Test-Stichprobe auf.
n_train <- round(.8 * nrow(Boston), digits = 0)
train_index <- sample(1:nrow(Boston), size = n_train)
train <- Boston[train_index, ]
test <- Boston[-train_index, ]
5. Berechnen Sie ein Random-Forest-Modell mit fünf verschiedenen Werten von
.mtry. Die vorherzusagende Variable ist der Hauswert; nehmen Sie alle übrigen
Variablen als Prädiktor auf.
Boston_rf_fit <- train(medv ~ ., data = train,
method = "rf", # Random Forest
trControl = my_crossval,
tuneLength = 5,
importance = TRUE)
Boston_rf_fit
6. Berichten Sie relevante Gütekoeffizienten des Modells.
test$Boston_rf_pred <- predict(Boston_rf_fit, newdata = test)
postResample(pred = test$Boston_rf_pred, obs = test$medv)
7. Benennen Sie die wichtigsten Einflussgrößen (laut diesem Modell).
varImp(Boston_rf_fit)
Teil VII
Ungeleitetes Modellieren
Clusteranalyse
23
Lernziele
Das Ziel einer Clusteranalyse erläutern können
Das Konzept der euklidischen Abstände verstehen
Eine k-Means-Clusteranalyse berechnen und interpretieren können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(cluster)
library(broom)
data(extra, package = "pradadata")
23.1 Grundlagen der Clusteranalyse
Das Ziel einer Clusteranalyse ist es, Gruppen von Beobachtungen (d. h. Cluster oder Typen) zu finden, die innerhalb der Cluster möglichst homogen (ähnlich) und zwischen den
Clustern möglichst heterogen (unähnlich) sind. Um die Ähnlichkeit von Beobachtungen
zu bestimmen, können verschiedene Distanzmaße herangezogen werden. Für metrische
Merkmale wird z. B. häufig die euklidische Metrik verwendet, d. h., Ähnlichkeit und Distanz werden auf Basis des euklidischen Abstands bestimmt. Aber auch andere Abstände
wie „Manhattan“ oder „Gower“ sind möglich (Han et al. 2011; Tan 2013). Letztere haben
den Vorteil, dass sie nicht nur für metrische Daten, sondern auch für gemischte Variablentypen verwendet werden können. Wir werden uns hier auf den euklidischen Abstand
konzentrieren.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_23
437
23 Clusteranalyse
2
Klausurpunkte
Klausurpunkte
438
0
-2
-4
2
cluster
1
0
2
3
-2
-4
-5
0
5
-5
Lernzeit
0
5
Lernzeit
Abb. 23.1 Ein Streudiagramm – ohne und mit gekennzeichneten Clustern
Zwei Arten der Clusteranalyse werden häufig verwendet: die hierarchischen und die
partitionierenden Verfahren (Han et al. 2011). Bei den hierarchischen Verfahren gibt es
zwei Herangehensweisen: agglomerativ (bottom-up) und divisiv (top-down). Bei den agglomerativen Verfahren beginnt man mit so vielen Clustern, wie es Fälle gibt. In jedem
Schritt werden dann zwei Cluster verschmolzen, bis ein Stopkriterium erreicht wird. Bei
den divisiven Clusteranalysen geht es andersherum.
Bei den partitionierenden Verfahren wird eine Anzahl von Clustern vom Nutzer vorgegeben; dann wird in mehreren Schritten versucht, die Fälle bestmöglich den Clustern
zuzuordnen. Ein bekannter Vertreter dieser Verfahren ist die sog. k-Means-Clusteranalyse,
auf die wir uns hier konzentrieren. Auch wenn bei der k-Means-Analyse die Anzahl
der Cluster vorgegeben werden muss, gibt es Ansätze zur datengetriebenen Auswahl der
„richtigen“ Anzahl von Clustern (vgl. Bortz (2013); s. Abschn. 23.2.2).
23.1.1 Intuitive Darstellung der Clusteranalyse
Betrachten Sie das Streudiagramm in Abb. 23.1 (die Daten sind frei erfunden; „simuliert“,
sagt der Statistiker). Es stellt den Zusammenhang von Lernzeit (wie viel ein Student für
eine Statistikklausur lernt) und dem Klausurerfolg (wie viele Punkte ein Student in der
Klausur erzielt) dar. Sehen Sie Muster? Lassen sich Gruppen von Studenten mit bloßem
Auge abgrenzen (Abb. 23.1, links)? Eine Lösung zeigt der rechte Teil der Abb. 23.1.
Nach dieser probatorischen Aufteilung in drei Gruppen, sind folgende „Cluster“,
„Gruppen“ oder „Typen“ von Studenten augenscheinlich:
„Rechteck-Gruppe“: Fälle dieser Gruppe lernen wenig und haben wenig Erfolg in der
Klausur. Tja.
„Kreis-Gruppe“: Fälle dieser Gruppe lernen viel; der Erfolg ist recht durchwachsen.
„Dreieck-Gruppe“: Fälle dieser Gruppe lernen mittel viel und erreichen einen vergleichsweise großen Erfolg in der Klausur.
23.1 Grundlagen der Clusteranalyse
439
1
2
0
-2
-4
2
x
x
x2
4
7
-5
x
0
5
-5
1
2
6
3
x
x x xxx x x xxx
x xxxx xxxxx xx
x
x
x
x x
8
xxx x
x
xx
2
0
-2
-4
.cluster
xx x
5
x
x x x
2
0
-2
-4
3
0
9
5
-5
0
4
5
6
7
8
9
5
x1
Summe Varianz within
Abb. 23.2 Unterschiedliche Anzahlen von Clustern im Vergleich
3000
2000
1000
1
2
3
4
5
6
7
8
9
Anzahl der Cluster
Abb. 23.3 Die Summe der Innerhalb-Varianz in Abhängigkeit von der Anzahl von Clustern. Ein
Screeplot
Drei Gruppen scheinen ganz gut zu passen. Wir hätten auch mehr oder weniger Gruppen unterteilen können. Die Clusteranalyse gibt keine definitive Anzahl an Gruppen vor;
vielmehr gilt es, aus theoretischen und statistischen Überlegungen heraus die richtige
Anzahl auszuwählen (dazu gleich noch mehr). Unterteilen wir zur Illustration den Datensatz einmal in bis zu neun Cluster (Abb. 23.2). Das „X“ soll jeweils den „Mittelpunkt“
des Clusters zeigen. Der Mittelpunkt ist so gewählt, dass die Distanz von jedem Punkt
zum Mittelpunkt möglichst kurz ist. Dieser Abstand wird auch als „Varianz innerhalb des
Clusters“ oder kurz Varianz innerhalb (Innerhalb-Varianz; Variance Within) bezeichnet.
Natürlich wird diese Innerhalb-Varianz immer kleiner, je größer die Anzahl der Cluster
wird. Das führt zur Abwägung, wie man Innerhalb-Varianz mit der Anzahl der Cluster abwägen soll. Dazu gibt es keine einzelne richtige Antwort, aber ein paar gängige Verfahren.
Die vertikale gestrichelte Linie in Abb. 23.3 zeigt an, wo die Einsparung an Varianz auf
einmal „sprunghaft“ weniger wird – just an jenem Knick bei x D 3; dieser „Knick“ wird
auch „Ellbogen“ genannt (da sage einer, Statistiker haben keine Phantasie). Man kann jetzt
440
23 Clusteranalyse
Interesse am Fach
Klausurpunkte
e
kt
Berta
e
4
Anna
d
c
A
3
B
ur
s
au
Kl
n
pu
D
b
a
Lernzeit
C
Lernze
it
Abb. 23.4 Pythagoras in der Ebene (links) und in 3D (rechts)
sagen, dass drei Cluster eine gute Lösung seien, weil mehr Cluster die Varianz innerhalb
der Cluster nur noch wenig verringern. Diese Art von Diagramm wird als Screeplot oder
Struktogramm bezeichnet (Bortz 2013). Damit haben wir eine Antwort auf die Frage,
wie viele Cluster zu wählen sind. Dieses Verfahren ist vielleicht nicht das beste, aber mit
Sicherheit eines der einfachsten. Eine andere Antwort könnte theoriegeleitet sein.
23.1.2 Euklidische Distanz
Aber wie weit liegen zwei Punkte voneinander entfernt? Betrachten wir ein Beispiel. Anna
und Berta sind zwei Studentinnen, die eine Statistikklausur geschrieben habenschreiben
mussten (bedauernswert). Die beiden unterscheiden sich sowohl hinsichtlich Lernzeit als
auch Klausurerfolg. Aber wie unterschiedlich sind Anna und Berta? Wie groß ist der „Abstand“ zwischen ihnen (s. Abb. 23.4, links)?
Eine Möglichkeit, die Distanz zwischen zwei Punkten in der Ebene (2D) zu bestimmen,
ist der Satz des Pythagoras (leise Trompetenfanfare). Generationen von Schülern haben
diese Gleichung ähmm . . . geliebt:
c 2 D a2 C b 2
In unserem Beispiel (s. Abb. 23.4, links) lernt Anna drei Zeiteinheiten weniger als Berta,
und Berta hat vier Klausurpunkte mehr erzielt als
Es gilt also: c 2 D 32 C 42 D 25;
pAnna.p
2
das Dreieck ACD ist rechtwinklig. Folglich ist c D 25 D 5 D c. Der Abstand oder
der Unterschied zwischen Anna und Berta beträgt also 5 – diese Art von „Abstand“ nennt
man den euklidischen Abstand.
Aber kann man den euklidischen Abstand auch in 3D (Raum) verwenden? Oder gar in
Räumen mehr Dimensionen? Betrachten wir den Versuch, zwei Dreiecke in 3D zu zeichnen (s. Abb. 23.4, rechts). Stellen wir uns vor, zusätzlich zu Lernzeit und Klausurerfolg
23.1 Grundlagen der Clusteranalyse
441
Abb. 23.5 Pythagoras in Reihe geschaltet
hätten wir als drittes Merkmal der Studentinnen noch „Statistikliebe“ erfasst (Bertas Statistikliebe ist um zwei Punkte höher als Annas). Sie können sich Punkt A als Ecke eines
Zimmers vorstellen; Punkt B schwebt dann in der Luft, in einiger Entfernung zu A.
Wieder suchen wir den Abstand zwischen den Punkten A und B. Wenn wir die Länge
e wüssten, dann hätten wir die Lösung; e ist der Abstand zwischen A und B im 3D-Raum.
Wie lang ist e, der „Abstand“ von Anna und Berta in 3D?
Abb. 23.5 zeigt die Lösung: Im orangefarbenen Dreieck (M ABD) gilt wiederum der
Satz von Pythagoras: c 2 C d 2 D e 2 . Wenn wir also c und d wüssten, so könnten wir e
berechnen . . . c haben wir ja gerade berechnet (5) und d ist einfach der Unterschied in
Statistikliebe zwischen Anna und Berta (2). Also
e2 D c2 C d 2
e 2 D 52 C 22
e 2 D 25 C 4
p
e D 29 5:4
Ah! Der Unterschied zwischen den beiden Studentinnen beträgt also 5.4.
Intuitiv gesprochen, „schalten wir mehrere Pythagoras-Sätze hintereinander“: Der euklidische Abstand berechnet sich mit dem Satz des Pythagoras (Azad 2013). Das geht
nicht nur für „zwei Dreiecke hintereinander“; der Algebra ist es wurscht, wie viele Dreiecke es sind.
Um den Abstand zweier Objekte mit k Merkmalen zu bestimmen, kann der euklidische Abstand berechnet werden mit. Bei k D 3 Merkmalen lautet die Formel
dann e 2 D a2 C b 2 C d 2 . Bei mehr als drei Merkmalen erweitert sich die Formel
entsprechend.
442
23 Clusteranalyse
Dieser Gedanken ist nützlich: Wir können von allen möglichen Objekten den Unterschied bzw. die (euklidische) Distanz ausrechnen. Betrachten wir drei Professoren, die
befragt wurden, wir sehr sie bestimmte Filme mögen (1: gar nicht; 10: sehr). Die Filme waren: „Die Sendung mit der Maus“, „Bugs Bunny“, „Rambo Teil 1“, „Vom Winde
verweht“ und „MacGyver“.
profs <- data_frame(
film1 = c(9, 1, 8),
film2 = c(8, 2, 7),
film3 = c(1, 8, 3),
film4 = c(2, 3, 2),
film5 = c(7, 2, 6))
Wir könnten einen „fünffachen Pythagoras“ zu Rate ziehen. Praktischerweise gibt es aber
eine R-Funktion, die uns die Rechnerei abnimmt:
dist(profs)
#>
1
2
#> 2 13.23
#> 3 2.65 10.77
Offenbar ist der (euklidische) Abstand zwischen Professor 1 und 2 groß (13.2); zwischen
Professor 2 und 3 ist er auch recht groß (10.8). Aber der Abstand zwischen Professor 1
und 3 ist relativ klein. Endlich hätten wir diese Frage auch geklärt.
Distanzen (d ) zu berechnen, ist nur für metrische Merkmale sinnvoll. Allerdings lässt
sich die Logik auch auf nicht-metrische Variablen übertragen. Bei dichotomen Variablen
kann man von d D 0 sprechen, wenn beide Objekte den gleichen Wert in der dichotomen Variablen haben. Mehrkategoriale Variablen kann man mittels Dummy-Kodierung in
mehrere dichotome Variablen aufteilen. Entsprechende Verfahren und viele Erweiterungen existieren; bei Bortz (2013) oder Tan (2013) kann man mehr dazu erfahren.
23.1.3 k-Means-Clusteranalyse
Beim k-Means Clusterverfahren handelt es sich um eine bestimmte Form von Clusteranalysen; zahlreiche Varianten der Clusteranalyse existieren (Han et al. 2011); die k-MeansClusteranalyse ist häufig anzutreffen. Im Gegensatz zur hierarchischen Clusteranalyse
handelt es sich um ein partitionierendes Verfahren. Die Daten werde in k Cluster aufgeteilt – dabei muss die Anzahl der Cluster im Vorhinein feststehen. Ziel ist es, dass die
Quadratsumme der Abweichungen der Beobachtungen (die Varianz der Abweichungen)
im Cluster zum Clusterzentrum minimiert wird.
Der Ablauf des Verfahrens ist wie folgt:
1. Zufällige Fälle als Clusterzentrum wählen
2. Fälle dem nächsten Clusterzentrum (geringste euklidische Distanz) zuordnen
3. Clusterzentren als Mittelwert der dem Cluster zugeordneten Fälle neu berechnen
23.2 Beispiel für eine einfache Clusteranalyse
443
Dabei werden die Schritte 2. und 3. so lange wiederholt, bis sich keine Änderung der
Zuordnung mehr ergibt – oder eine maximale Anzahl an Iterationen erreicht wurde. Aufgrund von (1.) hängt das Ergebnis einer k-Means-Clusteranalyse vom Zufall ab. Aus
Gründen der Reproduzierbarkeit sollte daher der Zufallszahlengenerator gesetzt werden
(mit set.seed()). Außerdem bietet es sich an, verschiedene Startkonfigurationen zu
versuchen. In der Funktion kmeans() erfolgt dies durch das Argument nstart.
23.2 Beispiel für eine einfache Clusteranalyse
Nehmen wir uns noch einmal den Extraversionsdatensatz vor. Kann man die Personen anhand von Ähnlichkeiten wie Facebook-Freunde, Partyfrequenz und Katerhäufigkeit clustern? Probieren wir es aus. Verschaffen Sie sich zuerst einen Überblick über den Datensatz
extra mit der Funktion glimpse().
23.2.1 Distanzmaße berechnen
Auf Basis der drei metrischen Merkmale (d. h. Alter, Einkommen und Kinder), die
wir hier aufs Geratewohl auswählen, ergeben sich für die ersten sechs Beobachtungen
folgende Abstände:
extra %>%
dplyr::select(n_facebook_friends, n_hangover, extra_single_item) %>%
head() %>%
dist()
#>
1
2
3
4
5
#> 2 144.01
#> 3 35.01 109.00
#> 4 51.93 95.19 21.24
#> 5 150.00
6.08 115.00 101.12
#> 6 126.00 270.00 161.00 176.56 276.00
Sie können erkennen, dass die Beobachtungen 2 und 5 einen kleinen Abstand haben, während 5 und 6 den größten haben. Allerdings hängen die Abstände von der Skalierung der
Variablen ab (n_facebook_friends streut stärker als extra_single_item). Daher
sollten wir die Variablen vor der Analyse standardisieren (z. B. über scale()).
Dann können wir uns mit der Funktion daisy() aus dem Paket cluster den Abstand zwischen den Objekten ausgeben lassen. Die Funktion errechnet Abstandsmaße
auch dann, wenn die Objekte aus Variablen mit unterschiedlichen Skalenniveaus bestehen; in diesem Fall wird als Abstandsmaß nicht das euklidische Maß, sondern Gowers
Maß verwendet (Gower 1971). Allerdings mag daisy() Variablen vom Typ chr nicht,
daher sollten wir sex zuerst in eine Faktorvariable umwandeln.
444
23 Clusteranalyse
extra %>%
dplyr::select(n_facebook_friends, sex, extra_single_item) %>%
drop_na() %>%
mutate(sex = factor(sex)) %>%
mutate_if(is.numeric, .funs = funs(scale)) %>%
head() %>%
cluster::daisy(.)
#> Dissimilarities :
#>
1
2
3
4
5
#> 2 0.5072
#> 3 0.0423 0.4650
#> 4 0.3937 0.1135 0.3514
#> 5 0.1812 0.3406 0.1389 0.4541
#> 6 0.4855 0.9928 0.5278 0.8792 0.6667
#>
#> Metric : mixed ; Types = I, N, I
#> Number of objects : 6
Mehr Hinweise zu daisy() und ihrer Ausgabe findet sich unter ?daisy und
?dissimilarity.object.
23.2.2
Fallstudie: kmeans für den Extraversionsdatensatz
Versuchen wir mithilfe einer k-Means-Clusteranalyse, einige Variablen in vier Cluster zu
clustern.
set.seed(42)
extra %>%
mutate(Frau = sex == "Frau") %>%
dplyr::select(n_facebook_friends, Frau, extra_single_item) %>%
drop_na() %>%
scale -> extra_cluster
kmeans_extra_4 <- kmeans(extra_cluster, centers = 4, nstart = 10)
Lassen Sie sich das Objekt extra_cluster ausgeben und betrachten Sie die Ausgabe; auch str(kmeans_extra_4) ist interessant. Neben der Anzahl Beobachtungen pro
Cluster (z. B. 1 in Cluster 2) werden auch die Clusterzentren ausgegeben. Diese können
dann direkt verglichen werden. Schauen wir mal, in welchem Cluster die Anzahl der Facebookfreunde im Schnitt am kleinsten ist:
kmeans_extra_4$centers
#>
n_facebook_friends
Frau extra_single_item
#> 1
-0.0291 0.712
0.6117
#> 2
25.6707 -1.402
1.3792
#> 3
-0.0614 0.712
-1.1929
#> 4
-0.0368 -1.402
-0.0587
23.2 Beispiel für eine einfache Clusteranalyse
445
Abb. 23.6 Schematische
Darstellung zweier einfacher
Clusterlösungen; links: geringe
Varianz innerhalb der Cluster;
rechts: hohe Varianz innerhalb
der Cluster
Betrachten Sie auch die Mittelwerte der anderen Variablen, die in die Clusteranalyse eingegangen sind. Wie „gut“ ist diese Clusterlösung? Vielleicht wäre ja eine andere Anzahl
von Clustern besser? Eine Antwort darauf liefert die Varianz (Streuung) innerhalb der
Cluster: Sind die Summen der quadrierten Abweichungen vom Clusterzentrum gering, so
ist die Varianz „innerhalb“ der Cluster gering; die Cluster sind homogen, und die Clusterlösung ist „gut“ (vgl. Abb. 23.6).
Je größer die Varianz innerhalb der Cluster, umso schlechter ist die Clusterlösung.
In zwei Dimensionen kann man Cluster gut visualisieren (Abb. 23.2); in drei Dimensionen wird es schon unübersichtlich. Mehr Dimensionen sind schwierig. Daher ist es
oft sinnvoll, die Anzahl der Dimensionen durch Verfahren der Dimensionsreduktion zu
verringern. Die Hauptkomponentenanalyse oder die Faktorenanalyse bieten sich dafür an
(vgl. für Details James et al. (2013)). Vergleichen wir ein paar verschiedene Lösungen,
um zu sehen, welche Lösung am besten zu sein scheint:
kmeans_extra_2
kmeans_extra_3
kmeans_extra_5
kmeans_extra_6
<<<<-
kmeans(extra_cluster,
kmeans(extra_cluster,
kmeans(extra_cluster,
kmeans(extra_cluster,
centers
centers
centers
centers
=
=
=
=
2,
3,
5,
6,
nstart
nstart
nstart
nstart
=
=
=
=
10)
10)
10)
10)
Dann nehmen wir die Gesamtstreuung jeder Lösung und erstellen daraus erst einen Vektor
und dann einen Dataframe:
streuung_innerhalb <- c(kmeans_extra_2$tot.withinss,
kmeans_extra_3$tot.withinss,
kmeans_extra_4$tot.withinss,
kmeans_extra_5$tot.withinss,
kmeans_extra_6$tot.withinss)
23 Clusteranalyse
streuung_innerhalb
446
1000
500
0
2
3
4
5
6
anzahl_cluster
Abb. 23.7 Abnahme der Innerhalb-Streuung als Funktion der Clusterzahl
streuung_df <- data_frame(
streuung_innerhalb,
anzahl_cluster = 2:6
)
Jetzt plotten wir die Höhe der Streuung pro Clusteranalyse, um einen Hinweis zu bekommen, welche Lösung am besten passen könnte (s. Abb. 23.7; auch als Struktogramm
bezeichnet von Bortz (2013)). Bei den paar Informationen hätte eine kleine Tabelle auch
gereicht.
ggplot(streuung_df) +
aes(x = anzahl_cluster,
y = streuung_innerhalb) +
geom_col() +
geom_line()
Nach der Lösung mit vier Clustern kann man (vage) einen „Knick“ oder „Ellbogen“1 ausmachen: Noch mehr Cluster verbessern die Streuung innerhalb der Cluster (und damit ihre
Homogenität) nur noch unwesentlich oder zumindest deutlich weniger. Daher entscheiden
wir uns für eine Lösung mit vier Clustern.
Aufgaben
Richtig oder falsch?2
1.
1
Die Clusteranalyse wird gemeinhin dazu verwendet, Objekte nach Ähnlichkeit
zu Gruppen zusammenzufassen.
Phantasievolle Zeitgenossen sehen einen steilen Berghang, zu dessen Fuß Geröll liegt; daher
spricht man auch von einem „Geröll-Plot“ (Screeplot); doch, im Ernst.
2
R, R, F, F, R, R, R, R, F, R.
23.2 Beispiel für eine einfache Clusteranalyse
447
2.
Die Varianz innerhalb eines Clusters kann als Maß für die Anzahl der zu extrahierenden Cluster herangezogen werden.
3. Unter euklidischer Distanz versteht man jedes Maß, welches den Abstand zwischen Punkten in der Ebene misst.
4. Bei der k-Means-Clusteranalyse darf man die Anzahl der zu extrahierenden
Cluster nicht vorab festlegen.
5. Cluster einer k-Means-Clusteranalyse werden so bestimmt, dass die Cluster
möglichst homogen sind, d. h. möglichst wenig Streuung aufweisen (also möglichst nah am Cluster-Zentrum liegen).
6. Der Satz des Pythagoras ist auch im 3D-Raum anwendbar.
7. Der Satz des Pythagoras ist auch im nD-Raum anwendbar.
8. Mit der Funktion dist() kann man sich die euklidische Distanz zwischen
Objekten mit mehreren Attributen ausgeben lassen.
9. Bei einer Clusteranalyse müssen alle Attribute metrisch sein.
10. Ein Ellbogen-Diagramm ist eine Methode, um die Anzahl der „richtigen“ Cluster in einer Clusteranalyse zu bestimmen.
Aufgaben
1. Laden Sie den Datensatz extra zur Extraversion. Unter Berücksichtigung der
zehn Extraversionsitems: Lassen sich die Teilnehmer der Umfrage in eine Gruppe oder in mehrere Gruppen einteilen? Wenn in mehrere Gruppen, wie viele
Gruppen passen am besten?
Lösung
set.seed(42)
extra %>%
select(i01:i10) %>%
drop_na() -> extra_items
kmeans(extra_items,
kmeans(extra_items,
kmeans(extra_items,
kmeans(extra_items,
centers
centers
centers
centers
=
=
=
=
1)
2)
3)
4)
streuung_items_innerhalb <- c(c1
c2
c3
c4
->
->
->
->
=
=
=
=
kmeans_items_1
kmeans_items_2
kmeans_items_3
kmeans_items_4
kmeans_items_1$tot.withinss,
kmeans_items_2$tot.withinss,
kmeans_items_3$tot.withinss,
kmeans_items_4$tot.withinss)
448
23 Clusteranalyse
streuung_items_innerhalb %>%
as_tibble %>%
add_column(n_centers = 1:4) %>%
ggplot(aes(x = n_centers, y = value)) +
geom_line(color = "grey20") +
geom_point(size = 3)
2. Testen Sie ein bis zehn Cluster auf die Veränderung der Within-Varianz; verwenden Sie die Funktion map() (s. Abschn. 28.2) oder eine gleichwertige Funktion.
Lösung
centers <- 1:10
centers %>%
map(~kmeans(extra_items, centers = .)) -> kmeans_extra_list
kmeans_extra_list %>%
map("tot.withinss") %>%
map_dfr(~data_frame(tot_wihin_ss = .)) %>%
add_column(n_centers = centers) %>%
ggplot(aes(x = n_centers, y = tot_wihin_ss, group = 1)) +
geom_line(color = "grey40") +
geom_point(size = 3) +
scale_x_continuous(breaks = centers)
Man kann einen Knick (Ellbogen) erkennen nach der zweiten Cluster-Lösung;
das Ergebnis ist aber nicht eindeutig.
Textmining
24
Lernziele
Sie kennen zentrale Ziele und Begriffe des Textminings
Sie wissen, was ein Tidytext-Dataframe ist
Sie können Worthäufigkeiten auszählen
Sie können Worthäufigkeiten anhand einer Wordcloud visualisieren
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(stringr)
library(tidytext)
library(lsa)
library(SnowballC)
library(wordcloud)
library(skimr)
library(pdftools)
data(afd, package = "pradadata")
data(stopwords_de, package = "lsa")
data(sentiws, package = "pradadata")
Ein großer Teil der zur Verfügung stehenden Daten liegt nicht als brave Tabellen vor,
sondern in „unstrukturierter“ Form, z. B. in Form von Fließtexten. Im Gegensatz zur Analyse von numerischen Daten ist die Analyse von Texten bisher weniger verbreitet. In
Anbetracht der Menge und der Informationsreichhaltigkeit von Text erscheint die Analyse von Text vielversprechend. In gewisser Weise ist das Textmining eine Ergänzung der
klassischen qualitativen Verfahren der Sozialforschung: Geht es in der qualitativen Sozial© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_24
449
450
24
Textmining
forschung primär um das Verstehen eines Textes, so kann man für das Textmining ähnliche
Ziele formulieren. Textmining kann (dem Menschen) beim Verstehen eines Textes helfen;
allerdings ist es wesentlich schwächer und beschränkter in der Tiefe des Verstehens als
z. B. Arbeiten mit der Grounded Theory (Strauss und Corbin 1990). Der Computer ist
einfach noch wesentlich dümmer als ein Mensch, was das Verstehen von Texten betrifft.
Allerdings ist er auch wesentlich schneller als ein Mensch, was das Lesen betrifft. Daher
bietet sich das Textmining für das Lesen großer Textmengen an, in denen eine geringe Informationsdichte vermutet wird; sozusagen maschinelles Sieben im großen Stil. Da fällt
viel durch die Maschen, aber es werden Tonnen von Sand bewegt.
In der Regel wird das Textmining als gemischte Methode verwendet: Sowohl quantitative als auch qualitative Aspekte spielen eine Rolle. Damit vermittelt das Textmining auf
konstruktive Art und Weise zwischen den manchmal antagonierenden Schulen der qualitativ-idiographischen und der quantitativ-nomothetischen Sichtweise auf die Sicht der
Welt (Flick 2007). Man könnte es auch als qualitative Forschung mit moderner Technik
bezeichnen – mit den skizzierten Einschränkungen wohlgemerkt. Dieses Kapitel gibt eine
Einführung in solche Analysen.
Die computergestützte Analyse von Texten speiste (und speist) sich reichhaltig aus
Quellen der Linguistik; entsprechende Fachtermini finden Verwendung:
Ein Corpus bezeichnet die Menge der zu analysierenden Dokumente; das könnten z. B.
alle Reden der Bundeskanzlerin Angela Merkel sein oder alle Tweets von „@realDonaldTrump“.
Ein Token (Term) ist ein elementarer Baustein eines Texts, die kleinste Analyseeinheit,
häufig ein Wort.
Unter einem Tidytext-Dataframe versteht man einen Dataframe, in dem pro Zeile nur
ein Token (z. B. Wort) steht (Silge und Robinson 2016). Synonym könnte man von
einem „langen“ Dataframe sprechen, so wie wir ihn in Kap. 7 kennengelernt haben.
Abb. 24.1 stellt einen Tidytext-Dataframe dar.
24.1
Grundlegende Analyse
24.1.1 Tidytext-Dataframes
Wozu ist es nützlich, einen Text-Dataframe in einen Tidytext-Dataframe umzuwandeln?
Der Grund ist, dass wir gängige R-Werkzeuge wie die von dplyr einfacher ausführen können, wenn nur ein Wort (Token) pro Zeile steht. Z. B. kann man mit TidytextDataframe count() nutzen, um zu zählen, wie häufig ein Wort vorkommt. Sprich: Sobald
wir einen Dataframe in einen Tidytext-Dataframe (d. h. einen langen Dataframe) umgewandelt haben, können wir unsere bekannten Methoden einsetzen. Basteln wir uns daher
zuerst einen Tidytext-Dataframe. Wir gehen dabei von einem Vektor mit mehreren Text-
24.1 Grundlegende Analyse
Tab. 24.1 Gedicht von Erich
Kästner
451
Zeile
1
2
3
4
Abb. 24.1 Illustration eines
Tidytext-Dataframes (rechts)
text
Wir haben die Frauen zu Bett gebracht,
als die Männer in Frankreich standen.
Wir hatten uns das viel schöner gedacht.
Wir waren nur Konfirmanden.
Breiter Dataframe
Zeile
text
Wir haben die Frauen zu
1
Bett gebracht,
2
...
Langer Dataframe
Zeile Wort
1
Wir
1
haben
als die Männer in
Frankreich standen.
1
die
1
Frauen
...
1
zu
...
...
tidy text Dataframe
Elementen aus, das ist ein realistischer Startpunkt. Unser Text-Vektor besteht aus vier
Elementen.1
text <- c("Wir
"als
"Wir
"Wir
haben die Frauen zu Bett gebracht,",
die Männer in Frankreich standen.",
hatten uns das viel schöner gedacht.",
waren nur Konfirmanden.")
Als Nächstes machen wir daraus einen Dataframe (s. Tab. 24.1).
text_df <- data_frame(Zeile = 1:4,
text = text)
Übrigens, falls Sie eine beliebige Textdatei einlesen möchten, können Sie das mit
read_lines() tun. Der Befehl read_lines (aus readr) liest Zeilen (Zeile für
Zeile) aus einer Textdatei:
text <- read_lines("Brecht.txt")
Dann „dehnen“ wir den Dataframe zu einem Tidytext-Dataframe (s. Abb. 24.1); das besorgt die Funktion unnest_tokens(). „unnest“ heißt dabei so viel wie „Entschachteln“, also von breit auf lang dehnen. Mit tokens sind hier einfach die Wörter gemeint (es
könnten aber auch andere Analyseeinheiten sein, Sätze zum Beispiel).
text_df %>%
unnest_tokens(output = wort, input = text) -> tidytext_df
tidytext_df %>% head()
1
Nach dem Gedicht „Jahrgang 1899“ von Erich Kästner.
452
#>
#>
#>
#>
#>
#>
#>
#>
#>
24
Textmining
# A tibble: 6 x 2
Zeile wort
<int> <chr>
1
1 wir
2
1 haben
3
1 die
4
1 frauen
5
1 zu
6
1 bett
Der Parameter output sagt, wie die neue, „saubere“ („lange“) Spalte heißen soll; input
sagt der Funktion, welche Spalte sie als ihr Futter (Input) betrachten soll (welche Spalte
in Tidytext umgewandelt werden soll); in Abb. 24.1 heißt die Output-Spalte „Wort“
und die Input-Spalte „text“. Die Syntax ist unnest_tokens(Ausgabespalte,
Eingabespalte). Nebenbei werden übrigens alle Buchstaben auf Kleinschreibung
getrimmt. Übersetzen wir die Syntax ins Deutsche:
Nimm den Datensatz „text_df“ UND DANN
dehne die einzelnen Elemente der Spalte „text“, so dass jedes Element seine eigene Spalte bekommt.
Ach ja: Diese „gedehnte“ Spalte soll „wort“ heißen (weil nur einzelne Wörter
drinnen stehen) UND DANN
speichere den resultierenden Dataframe ab als tidytext_df.
In einem Tidytext-Dataframe steht in jeder Zeile ein Wort (Token) und die Häufigkeit dieses Worts im Dokument.
Als Nächstes filtern wir die Satzzeichen heraus, da die Wörter für die Analyse wichtiger
(oder zumindest einfacher) sind.
tidytext_df %>%
filter(str_detect(wort, "[a-z]")) -> tidytext_df_lowercase
In Pseudo-Code heißt dieser Abschnitt:
Nimm den Datensatz „tidytext_df“ UND DANN
filtere die Spalte „wort“, so dass nur noch Kleinbuchstaben übrig bleiben (keine
Ziffern c.). FERTIG.
24.1 Grundlegende Analyse
453
24.1.2 Regulärausdrücke
Das "[a-z]" in der Syntax steht für „alle Buchstaben von a-z“. Diese flexible Art von
„String-Verarbeitung mit Jokern“ nennt man Regulärausdrücke (Regular Expressions). Es
gibt eine ganze Reihe von diesen Regulärausdrücken, die die Verarbeitung von Textdaten
erleichtert. Mit dem Paket stringr geht das – mit etwas Übung – gut von der Hand;
das Paket wird über tidyverse mitgestartet. Nehmen wir als Beispiel den Text eines
Tweets:
string <- paste0("Correlation of unemployment and #AfD votes",
"at #btw17: ***r = 0.18***",
"https://t.co/YHyqTguVWx")
Gibt es mindestens eine Ziffer in dem String?
Möchte man Ziffern identifizieren, so hilft der Regulärausdruck [:digit:]; er steht
für eine Ziffer.
str_detect(string, "[:digit:]")
#> [1] TRUE
Dieser Befehl prüft, ob sich in der Stringvariablen string mindestens eine Ziffer befindet; Ziffern werden „dedektiert“ im String (string detect).
Finde die Position der ersten Ziffer! Welche Ziffer ist es?
str_locate(string, "[:digit:]")
#>
start end
#> [1,]
50 50
str_extract(string, "[:digit:]")
#> [1] "1"
Mit str_locate wird lokalisiert, von welcher bis welcher Position sich der erste Treffer
für [:digit] befindet; das erste Zeichen im String ist Position 1. str_detect gibt
nicht die Position, sondern den Wert (d. h. die Ziffer) zurück.
Finde alle Ziffern!
str_extract_all(string, "[:digit:]")
#> [[1]]
#> [1] "1" "7" "0" "1" "8"
Finde alle Stellen, an denen genau zwei Ziffern hintereinander folgen!
str_extract_all(string, "[:digit:]{2}")
#> [[1]]
#> [1] "17" "18"
454
24
Textmining
Der Quantitätsoperator fng findet alle Stellen, an denen der gesuchte Ausdruck genau n
mal auftaucht.
Gib die Hashtags des Tweets zurück!
str_extract_all(string, "#[:alnum:]+")
#> [[1]]
#> [1] "#AfD"
"#btw17"
Der Operator [:alnum:] steht für „alphanumerischer Charakter“ – also eine Ziffer oder
einen Buchstaben; synonym hätte man auch \\w schreiben können (w wie word). Warum
werden zwei Backslashes gebraucht? Mit \\w wird signalisiert, dass nicht der String
\w, sondern etwas Besonderes, eben der Regex-Operator \w, gesucht wird. Der RegexOperator + steht für „der Ausdruck vor mir, hier ein alphanumerisches Zeichen, darf
einmal oder häufiger gefunden werden“. Es werden also alle Zeichenketten zurückgeliefert, die mit einem Hashtag beginnen und dann von einem oder mehr alphanumerischen
Zeichen gefolgt werden. Punkte, Sonder- und Leerzeichen sind keine alphanumerischen
Zeichen.
Gib URLs zurück!
str_extract_all(string, "https?://[:graph:]+")
#> [[1]]
#> [1] "https://t.co/YHyqTguVWx"
Das Fragezeichen ? ist eine Quantitätsoperator, der einen Treffer liefert, wenn das vorherige Zeichen (hier s) null oder einmal gefunden wird. [:graph:] ist die Summe von
[:alpha:] (Buchstaben, groß und klein), [:digit:] (Ziffern) und [:punct:] (Satzzeichen und Sonderzeichen wie / oder :).
Zähle die Wörter im String!
str_count(string, boundary("word"))
#> [1] 12
Liefere nur Buchstabenfolgen zurück, lösche alles Übrige!
str_extract_all(string, "[:alpha:]+")
#> [[1]]
#> [1] "Correlation" "of"
"unemployment" "and"
#> [5] "AfD"
"votesat"
"btw"
"r"
#> [9] "https"
"t"
"co"
"YHyqTguVWx"
Der Quantitätsoperator + liefert alle Stellen zurück, an denen der gesuchte Ausdruck
einmal oder häufiger vorkommt. Die Ergebnisse werden als Vektor von Wörtern zurückgegeben. Ein anderer Quantitätsoperator ist *, der für 0 oder mehr Treffer steht. Möchte
man einen Vektor, der aus String-Elementen besteht, zu einem String zusammenfügen,
hilft paste(string) oder str_c(string, collapse = " ").
24.1 Grundlegende Analyse
455
Ersetze die Leerzeichen!
str_replace_all(string, "[^[:alpha:]+]", "")
#> [1] "CorrelationofunemploymentandAfDvotesatbtwrhttpstcoYHyqTguVWx"
Mit dem Negationsoperator [ˆx] wird der Regulärausdruck x negiert; die Syntax heißt
also: „Ersetze in string alles außer Buchstaben durch Nichts.“ Mit „Nichts“ sind hier
Strings der Länge Null gemeint; ersetzt man einen beliebigen String durch einen String der
Länge null, so hat man den String gelöscht. Das Cheatsheet zu Strings bzw. zu stringr
von RStudio gibt einen guten Überblick über Regex;2 im Internet finden sich viele Beispiele.
24.1.3 Textdaten einlesen
Nun lesen wir Textdaten ein; das können beliebige Daten sein.3 Eine gewisse Reichhaltigkeit des Textes ist von Vorteil. Nehmen wir das Parteiprogramm der Partei AfD.4 Vor dem
Hintergrund des Erstarkens des (extremen) Populismus weltweit und der großen Gefahr,
die davon ausgeht – man blicke auf die Geschichte Europas in der ersten Hälfte des 20.
Jahrhunderts – verdienterfordern der politische Prozess und speziell Neuentwicklungen
darin unsere besondere Beachtung. Das Parteiprogramm (Dataframe afd) ist im Paket
pradadata enthalten.
Ein häufiges Format von Text-Fundorten sind PDF-Dateien. Um Text aus PDF-Dateien
zu lesen, bieten sich in R das Paket pdftools und die Funktion pdf_text() an, die
den Inhalt einer PDF-Datei einliest. Jede Seite wird dabei als ein Element eines Vektors
abgespeichert. Hätten wir die PDF-Datei vorliegen, so könnten wir sie mit pdf_text
einlesen. In diesem Beispiel hat der resultierende Vektor afd_raw 96 Elemente (entsprechend der Seitenzahl des Dokuments).5 Wandeln wir als Nächstes den Vektor in einen
Dataframe um.
afd_pfad <- "data/afd_programm.pdf"
content <- pdf_text(afd_pfad)
afd <- data_frame(Seite = 1:96,
content)
Im Ergebnis haben wir einen Dataframe, genauso wie er in pradadata vorhanden ist. Mit
head(afd_raw) können Sie sich den Beginn dieses Dataframes anzeigen lassen. Als
2
https://www.rstudio.com/resources/cheatsheets/.
Ggf. benötigen Sie Administrator-Rechte, um Dateien auf Ihrem Computer zu speichern.
4
Heruntergeladen am 1. März 2017 von https://www.alternativefuer.de/wp-content/uploads/sites/7/
2016/05/2016-06-27_afd-grundsatzprogramm_web-version.pdf.
5
Das PDF-Dokument können Sie auch unter https://osf.io/6d3hq/ herunterladen.
3
456
24
Textmining
Nächstes konvertieren wir in einen Tidytext-Dataframe und filtern alles bis auf Buchstaben
weg:
afd %>%
unnest_tokens(output = token, input = content) %>%
dplyr::filter(str_detect(token, "[a-z]")) -> afd_long
head(afd_long)
#> # A tibble: 6 x 2
#>
page token
#>
<int> <chr>
#> 1
1 programm
#> 2
1 für
#> 3
1 deutschland
#> 4
1 das
#> 5
1 grundsatzprogramm
#> 6
1 der
Insgesamt 26396 Wörter; eine substanzielle Menge von Text. Was wohl die häufigsten
Wörter sind?
24.1.4 Worthäufigkeiten auszählen
afd_long %>%
na.omit() %>% # fehlende Werte löschen
count(token, sort = TRUE)
#> # A tibble: 7,087 x 2
#>
token
n
#>
<chr> <int>
#> 1 die
1151
#> 2 und
1147
#> 3 der
870
#> # ... with 7,084 more rows
Die häufigsten Wörter sind inhaltsleere Partikel, Präpositionen, Artikel . . . Solche sogenannten „Stopwörter“ sollten wir besser herausfischen, um zu den inhaltlich tragenden
Wörtern zu kommen. Praktischerweise gibt es frei verfügbare Listen an deutschen Stopwörtern, z. B. im Paket lsa.
stopwords_de <- data_frame(word = stopwords_de)
afd_long %>%
anti_join(stopwords_de, by = c("token" = "word") ) -> afd_no_stop
Mit by geben wir an, wie die Spalten heißen, anhand derer zusammengeführt wird (s.
Abschn. 7.6). Der resultierende Datensatz afd_no_stop hat jetzt viel weniger Zeilen; wir haben also durch anti_join() Zeilen gelöscht (herausgefiltert). Das ist die
24.1 Grundlegende Analyse
Tab. 24.2 Die häufigsten
Wörter – mit Stemming (links)
und ohne Stemming (rechts)
457
token_stem
deutschland
afd
deutsch
polit
staat
programm
europa
woll
burg
soll
n
219
171
119
88
85
81
80
67
66
63
token
deutschland
afd
programm
wollen
bürger
euro
dafür
eu
deutsche
deutschen
n1
190
171
80
67
57
55
53
53
47
47
Funktion von anti_join(): Die Zeilen, die in beiden Dataframes vorkommen, werden
herausgefiltert. Es verbleiben also nicht „Nicht-Stopwörter“ in unserem Dataframe. Damit
wird es schon interessanter, welche Wörter häufig sind.
afd_no_stop %>%
count(token, sort = TRUE) -> afd_count
Tab. 24.2 zeigt das Ergebnis. Ganz interessant; aber es gibt mehrere Varianten des Themas
„deutsch“. Es ist wohl sinnvoller, diese auf den gemeinsamen Wortstamm zurückzuführen
und diesen nur einmal zu zählen. Dieses Verfahren nennt man Stemming oder Trunkieren.
afd_no_stop %>%
mutate(token_stem = wordStem(.$token, language = "de")) %>%
count(token_stem, sort = TRUE) -> afd_count_stemmed
Das ist schon informativer. Dem Befehl SnowballC::wordStem() füttert man einen
Vektor an Wörtern ein und gibt die Sprache an (Default ist Englisch). Denken Sie daran,
dass . bei dplyr den Datensatz meint, wie er im letzten Pfeifenschritt definiert war. Mit
.$token wählen wir also die Variable token aus afd_raw aus.
24.1.5 Visualisierung
Zum Abschluss noch eine Visualisierung mit einer „Wordcloud“ dazu (s. Abb. 24.2).
wordcloud(words = afd_count_stemmed$token_stem,
freq = afd_count_stemmed$n,
max.words = 100,
scale = c(2,.5),
colors=brewer.pal(6, "Dark2"))
458
Abb. 24.2 Eine Wordwolke
zum AfD-Parteiprogramm
24
gross
Textmining
deutschland
landlich beruffordert
einwander
schul partei aufgabwirtschaft
mensch
dies schutz
gesetz der
neu gen sowi bankprivat
parlament ziel
dafur
elt
derzeit grund sprach ford
interess lich
darf
geb
deutsch
woll
demokrati
burg
raum
damit
kein
stark
land hohgesellschaft
setzt islam
natur
arbeit
recht
freiheit entscheid
kost
schaff
erhalt
soll
freie
mittel
will eig bereit dabei fall
zahl
schen offent entwickl
bess
durf
asyl national lehnt jahr
identitat
beend
sich eu integration
staatlicheegall kultur
steu gend
international
demokrat
euro
polit kind
programm familiinsbesondvolk
sozial
staat europa forschungausland
Man kann die Anzahl der Wörter, Farben und einige weitere Formatierungen der Wortwolke beeinflussen.6 Weniger verspielt ist eine schlichte visualisierte Häufigkeitsauszählung
dieser Art, z. B. mit Balkendiagrammen (gedreht, s. Abb. 24.3).
Warnung: Eine Wordcloud hat keinen inhaltlichen Mehrwert; im besten Fall ist sie
schick. Nutzen Sie sie nicht, um wichtige Informationen zu vermitteln, sondern
höchstens, um optisch zu schmeicheln.
afd_count_stemmed %>%
top_n(30) %>%
ggplot() +
aes(x = reorder(token_stem, n), y = n) +
geom_col() +
labs(title = "mit Trunkierung") +
coord_flip() -> p1
afd_count %>%
top_n(30) %>%
ggplot() +
aes(x = reorder(token, n), y = n) +
geom_col() +
labs(title = "ohne Trunkierung") +
coord_flip() -> p2
Die beiden Diagramme vergleichen die trunkierten Wörter mit den nicht trunkierten Wörtern. Mit reorder() ordnen wir die Spalte token nach der Spalte n. coord_flip()
dreht die Abbildung um 90°, d. h., die Achsen sind vertauscht.
6
https://cran.r-project.org/web/packages/wordcloud/index.html.
24.2 Sentimentanalyse
459
ohne Trunkierung
reorder(token, n)
reorder(token_stem, n)
mit Trunkierung
deutschland
afd
deutsch
polit
staat
programm
europa
woll
burg
soll
stark
euro
land
kind
eu
dafur
wirtschaft
sich
recht
offent
moglich
will
famili
staatlich
erhalt
mensch
national
einwander
sowi
all
0
50
100
150
deutschland
afd
programm
wollen
bürger
euro
eu
dafür
deutschen
deutsche
stärken
soll
kinder
will
staaten
erhalten
sowie
staat
menschen
darf
damit
europäischen
einwanderung
dabei
integration
gen
sprache
dies
deutschlands
recht
kultur
200
n
0
50
100
150
n
Abb. 24.3 Worthäufigkeiten im AfD-Parteiprogramm
24.2
Sentimentanalyse
Eine weitere interessante Analyse ist, die „Stimmung“ oder „Emotionen“ (Sentiments)
eines Textes auszulesen. Die Anführungszeichen deuten an, dass hier ein Maß an Verständnis suggeriert wird, welches nicht (unbedingt) von der Analyse eingehalten wird.
Jedenfalls ist das Prinzip der Sentimentanalyse im einfachsten Fall so:
1. Schau dir jeden Token aus dem Text an.
2. Prüfe, ob sich das Wort im Lexikon der Sentiments wiederfindet.
3. Wenn ja, dann addiere den Sentimentwert dieses Tokens zum bestehenden Sentimentwert.
4. Wenn nein, dann gehe weiter zum nächsten Wort.
5. Liefere zum Schluss die Summenwerte pro Sentiment zurück.
Es gibt Sentimentlexika, die lediglich einen Punkt für „positive Konnotation“ bzw.
„negative Konnotation“ geben; andere Lexika weisen differenzierte Gefühlskonnotationen auf. Z. B. kann man dem Wort „Hochzeit“ einen höheren Score geben als dem Wort
„Chillen“ (wenn man möchte). Oder man kann nicht nur ein globales, undifferenziertes hedonistisches Differenzial von negativ zu positiv ausgeben, sondern verschiedene
Emotionen wie Überraschung, Freude oder Angst differenzieren. Wir nutzen hier das
460
24
Textmining
Tab. 24.3 Auszug aus SentiwS
neg_pos
neg
neg
neg
neg
neg
neg
word
Abbau
Abbruch
Abdankung
Abdämpfung
Abfall
Abfuhr
Tab. 24.4 Zusammenfassung
von SentiWS
value
0:058
0:005
0:005
0:005
0:005
0:337
neg_pos
neg
pos
inflections
Abbaus,Abbaues,Abbauen,Abbaue
Abbruches,Abbrüche,Abbruchs,Abbrüchen
Abdankungen
Abdämpfungen
Abfalles,Abfälle,Abfalls,Abfällen
Abfuhren
polarity_sum
52:6
29.6
polarity_count polarity_prop
219
0.27
586
0.73
Sentimentlexikon von Remus et al. (2010). Tab. 24.3 zeigt einen Ausschnitt aus deren
Sentimentlexikon SentiWS.
Nun können wir jedes Token des Textes mit dem Sentimentlexikon abgleichen; dabei
zählen wir die Treffer für positive bzw. negative Terme. Zuvor müssen wir aber noch
die Daten (afd_long) mit dem Sentimentlexikon zusammenführen (joinen). Das geht
nach bewährter Manier mit inner_join(); „inner“ sorgt dabei dafür, dass nur Zeilen
behalten werden, die in beiden Dataframes vorkommen. Tab. 24.4 zeigt Summe, Anzahl
und Anteil der Emotionswerte.
afd_long %>%
inner_join(sentiws, by = c("token" = "word")) %>%
select(-inflections) -> afd_senti # die Spalte brauchen wir nicht
afd_senti %>%
group_by(neg_pos) %>%
summarise(polarity_sum = sum(value),
polarity_count = n()) %>%
mutate(polarity_prop = (polarity_count / sum(polarity_count)) %>%
round(2)) -> afd_senti_tab
Die Analyse zeigt, dass die emotionale Bauart des Textes durchaus interessant ist: Es
gibt viel mehr positiv getönte Wörter als negativ getönte. Allerdings sind die negativen
Wörter offenbar deutlich stärker emotional aufgeladen, denn der Absolutwert der Summe
an Emotionswert der negativen Wörter ist deutlich größer als die der positiven (obwohl
es mehr positive Wörter gibt im Text). Betrachten wir also die intensivsten negativ und
positiv konnotierten Wörter näher.
afd_senti %>%
distinct(token, .keep_all = TRUE) %>%
mutate(value_abs = abs(value)) %>%
top_n(20, value_abs) %>%
24.2 Sentimentanalyse
pull(token)
#> [1] "ungerecht"
#> [5] "behindern"
#> [9] "gemein"
#> [13] "falsch"
#> [17] "belasten"
461
"besonders"
"gefährden"
"verletzt"
"vermeiden"
"schädlich"
"gefährlich"
"brechen"
"zerstören"
"zerstört"
"töten"
"überflüssig"
"unzureichend"
"trennen"
"schwach"
"verbieten"
Diese „Hitliste“ wird zumeist (19/20) von negativ polarisierten Begriffen aufgefüllt, wobei
„besonders“ ein Intensivierwort ist, welches das Bezugswort verstärkt („besonders gefährlich“). Das Argument keep_all = TRUE sorgt dafür, dass alle Spalten zurückgegeben
werden, nicht nur die durchsuchte Spalte token. Mit pull haben wir aus dem Dataframe,
der von den dplyr-Verben übergeben wird, die Spalte token „herausgezogen“; hier nur
um Platz zu sparen bzw. der Übersichtlichkeit halber.
Nun könnte man noch den erzielten „Netto-Sentimentwert“ des Corpus ins Verhältnis zum Sentimentwert des Lexikons setzen: Wenn es insgesamt im Sentimentlexikon
sehr negativ zuginge, wäre ein negativer Sentimentwert in einem beliebigen Corpus nicht
überraschend. skimr::skim() gibt uns einen Überblick der üblichen deskriptiven Statistiken. Alternativ könnten wir z. B. summarise() verwenden:
sentiws %>%
select(value, neg_pos) %>%
skim()
sentiws %>%
select(value, neg_pos) %>%
group_by(neg_pos) %>%
skim_to_wide()
#> # A tibble: 2 x 13
#>
type neg_pos variable missing
#>
<chr> <chr>
<chr>
<chr>
#> 1 nume~ neg
value
0
#> 2 nume~ pos
value
0
#> # ... with 3 more variables: p50
complete n
mean sd
p0
<chr>
<chr> <chr> <chr> <chr>
1818
1818 "-0.~ "0.2~ "-1 ~
1650
1650 " 0.~ 0.13 " 0.~
<chr>, p75 <chr>, p100 <chr>
p25
<chr>
"-0.~
" 0.~
Insgesamt ist das Lexikon ziemlich ausgewogen; negative Werte sind leicht in der Überzahl im Lexikon. Unser Corpus hat eine ähnliche mittlere emotionale Konnotation wie das
Lexikon:
afd_senti %>%
summarise(senti_sum = mean(value) %>% round(2))
#> # A tibble: 1 x 1
#>
senti_sum
#>
<dbl>
#> 1
-0.03
462
24
Textmining
Damit könnte man die emotionale Konnotation unseres Corpus als „mittel“ einstufen –
mittel ist dabei definiert als ähnlich ausgeprägt wie das Wörterbuch SentiWS.
Aufgaben
Richtig oder falsch?7
1.
2.
3.
Unter einem Token versteht man die größte Analyseeinheit in einem Text.
In einem Tidytext-Dataframe steht jedes Wort in einer (eigenen) Zeile.
Eine hinreichende Bedingung für einen Tidytext-Dataframe ist es, dass in jeder Zeile ein Wort steht (beziehen Sie sich auf den Tidytext-Dataframe wie in
diesem Kapitel erörtert).
4. Gibt es „Stop-Wörter“ in einem Dataframe, dessen Text analysiert wird, so
kommt es – per definitionem – zu einem Stop.
5. Mit dem Befehl unnest_tokens kann man einen Tidytext-Dataframe erstellen.
6. Balkendiagramme sind sinnvolle und auch häufige Diagrammtypen, um die
häufigsten Wörter (oder auch Tokens) in einem Corpus darzustellen.
7. In einem „Tidytext-Dataframe“ steht in jeder Zeile ein Wort (token), aber nicht
die Häufigkeit des Worts im Dokument.
8. Unter Stemming versteht man (bei der Textanalyse), die Etymologie eines
Worts (Herkunft) zu erkunden.
9. Der Datensatz tidytext_df ist tidy.
10. Die folgende Syntax ist geeignet, um die Anzahl der Wörter im Dataframe afd
auszulesen, stimmt’s? afd %>% unnest_tokens(output = token,
input = content) %>% dplyr::filter(str_detect(token,
"[a-z]")) %>% count().
7
F, R, F, F, R, R, F, F, R, R.
25
Fallstudie: Twitter-Mining
Lernziele
Grundlagen des Twitter-Minings lernen
Grundlegende Konzepte des Text-Minings und speziell des Sentiment-Minings
anwenden
Die Polarität der Emotion in einem Textcorpus anwenden
Die Bedeutung und die Konsequenzen für die Organisationsdiagnose einschätzen
Laden wir zuerst die benötigten Pakete:
library(pradadata)
library(tidyverse)
library(tidytext)
library(twitteR)
library(tidytext)
Twitter ist ein Dienst, dessen Nutzer öffentlich einsehbare Kurznachrichten auf die
Twitter-Seite einstellen; der Dienst ist für Nutzer kostenlos. Das zentrale Datenelement
bei Twitter ist eine Twitter-Nachricht, Tweet genannt. Tweets sind aber, aus Sicht der
Datenanalyse, weit mehr als 140280 Textzeichen. Mit einem Tweet ist der Nutzername
verknüpft, andere Nutzer, die angesprochen oder auf die geantwortet wird, oder #Hashtags (Schlagwörter). Darüber hinaus auch Zeit, Datum, Betriebssystem, Ort und einiges
mehr. Rechtlich ist der Zugriff auf Daten so geregelt, dass man kostenlos auf eine gewisse
Menge Tweets und andere Informationen zugreifen darf, nach vorheriger Anmeldung.
Allerdings darf man die Tweets nicht weiter verbreiten, das verbieten die Nutzungsbedingungen von Twitter. Insgesamt bietet Twitter eine hervorragende Möglichkeit zur
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_25
463
464
25 Fallstudie: Twitter-Mining
Analyse von Daten, die eine Vielzahl von Themen mit breitem gesellschaftlichem Belang
berühren. Einige Beispiele für Forschungsfragen, die Twitter-Daten erlauben, sind:
Themen: Welche Themen werden häufig gepostet?
Trends: Welche Themen werden zunehmen häufig gepostet?
Geo-Trends: Wie unterscheiden sich Trends zwischen geographischen Gebieten?
Netzwerke: Wer bezieht sich auf wen? Welche Nutzerkonten sind besonders stark vernetzt?
Multimodale Daten: Liegen von einzelnen Nutzern neben den Tweets weitere Daten
vor, so können die Daten vernetzt werden, was naturgemäß reichere Informationen
birgt. Es können dabei aber Probleme des Datenschutzes (Entanonymisierung) auftreten.
Sentiments: Welche emotionale Polarität (emotional positiv vs. negativ getönt) findet
sich in Tweets?
25.1
Zum Einstieg: Moderne Methoden der Sentimentanalyse
Exemplarisch sei die Analyse von Sentiments verdeutlicht. Unter einem Sentiment (deutsche Aussprache) versteht man eine emotionale Bewertung eines Objekts, z. B. einer Person oder eines Ereignisses (Stevenson 2010). Es wird somit die affektive Komponente
einer Einstellung im psychologischen Sinne beschrieben (Aronson et al. 2010), wobei bei
einer Einstellung eine Konnotation der Stabilität mitschwingt, was beim Begriff des Sentiments nicht zwingend ist.
Typische Forschungsgegenstände in diesem Umfeld sind die Arbeitsmotivation von
Mitarbeitern, die Servicezufriedenheit von Kunden oder die Beurteilung neuer Funktionen
von Nutzern eines Online-Dienstes. Ein konkretes Beispiel ist die Organisationsdiagnose. Darunter versteht man die Erhebung des Ist-Zustands einer Organisation, häufig auch
unter expliziter Einbeziehung „weicher“ Kriterien wie Zufriedenheit und Arbeitsmotivation (Schuler 2015). Konkreter gesagt wird häufig eine Mitarbeiterbefragung durchgeführt,
in der Multiple-Choice-Items zur Einschätzung solcher weicher Kriterien erfragt werden:
„Ich sehe Führungsvorbilder in meinem Umfeld.“ Die anzukreuzenden Antworten könnten von „soll das ein Witz sein“ oder „stimme überhaupt nicht zu“ bis hin zu „stimme voll
und ganz zu“ reichen. Oft werden vier, fünf oder sechs Antwortstufen vorgegeben, die eine
zunehmende Zustimmung messen sollen.1 Dieses Vorgehen, bewährt oder zumindest lange bekannt, wird in sehr vielen Unternehmen mehr oder weniger häufig durchgeführt. Die
Vorteile liegen in der einfachen Quantifizierung, der einfachen Auswertung und Darstellung sowie in der recht einfachen Durchführung. Nachteilig sind der Aufwand (Arbeitszeit
1
Außerdem wird angenommen, dass die Zunahme der Zustimmung bei jedem Schritt gleich groß
ist.
25.2 Grundlagen des Twitter-Minings
465
der Mitarbeiter zum Antworten plus die Auswertung) sowie die Subjektivität der Antworten. Außerdem liegt ein Zeitverzug zwischen der Meinungsbildung, der Abfrage der
Meinung, der Auswertung und der Beschäftigung mit etwaigen Konsequenzen.
Ein anderer, neuer Ansatz der Analyse von Sentiments liegt in der maschinellen Analyse von Informationen, die Rückschlüsse auf die Gestimmtheit oder die Einstellung der
relevanten Personengruppe (z. B. der Belegschaft oder die Kundschaft eines Unternehmens oder die Kunden) erlaubt. Naheliegend ist die Idee, Texte der Personengruppe zu
untersuchen, da diese Texte unmittelbar zur Verfügung stehen und insofern „nah“ an den
entsprechenden Menschen sind, da die Texte ja von diesen Menschen verfasst sind. Wird
der Text maschinell (computergestützt) untersucht, so sind (viel) größere Textmengen in
kurzer Zeit bewältigbar, in objektiver d. h. in grundsätzlich exakt reproduzierbarer Form.
Ein Nachteil der maschinellen Textanalyse ist die geringere Verarbeitungstiefe – „zwischen den Zeilen“ zu lesen, subtile Wortspiele, Ironie, Witz und Anklang auf verwandte
Themen, all dass ist einem Computer schwer zu erklären. Meist beschränken sich entsprechende Textanalysen auf einfache Konzepte; einige grundlegende erproben wir praktisch
in diesem Kapitel.
Tweets eignen sich für Sentimentanalysen auch deswegen, weil sie in großer Zahl vorliegen, so dass eine automatisierte Auswertung nötig ist, wenn man einen Überblick über
die Tweet-Landschaft bekommen möchte. Natürlich kann man für Sentimentanalysen einer Organisation auch beliebige andere Texte hernehmen. Hier gibt es noch Luft für viel
kreative Forschung.
25.2
Grundlagen des Twitter-Minings
Der Kurznachrichtendienst Twitter ist für eine Sentimentanalyse gut geeignet: Die Tweets
(Kurznachrichten) sind über eine strukturierte Schnittstelle (Application Programming Interface, API) einigermaßen frei und kostenlos verfügbar. Frei verfügbar sind die Tweets
in dem Sinne, dass jeder Tweet öffentlich einsehbar ist, solange der Autor des Tweets
dagegen nichts aktiv unternimmt (den Tweet löscht). Twitter erfreut sich recht großer Beliebtheit; etliche Millionen Menschen nutzen Twitter – aktuell gibt es gut 300 Millionen
Nutzer. Bei der Freude über die Datenfülle muss angemerkt werden, dass über die API
nur eine bestimmte Menge an Tweets abgerufen werden dürfen. Das Auslesen von Tweets
mit anderen Mitteln und besonders das Auslesen einer großen Zahl von Tweets sind nicht
erlaubt. Außerdem dürfen die Tweets nicht weiter verbreitet werden.2 Als Autor eines
Tweets behält man die Rechte an den Inhalten, die man einstellt. Allerdings gestattet man
Twitter die Nutzung dieser Inhalte, und zwar kostenfrei und unbegrenzt. Möchte man
Tweets weitergeben, so ist das nur in „dehydrierter“ Form erlaubt: Man speichert die eindeutige ID eines Tweets, über die API können dann der zugehörige Tweet und Metadaten
2
https://twitter.com/de/tos.
466
25 Fallstudie: Twitter-Mining
(Autor, Datum . . . ) rekonstruiert werden, sofern der Tweet noch verfügbar ist und nicht
etwa gelöscht wurde. Diese Regelung ist (auch) Datenschutz: Der Autor eines Tweets hat
das Recht, dass Twitter seinen Tweet „vergisst“ (oder zumindest nicht mehr öffentlich
zugänglich macht).
25.2.1
Authentifizierung bei der Twitter-API
Um Twitter nutzen zu können, benötigt man ein Benutzerkonto (einen Account, kostenlos). Mit einem Account kann man Tweets schreiben (posten) oder auch das Archiv seiner
Tweets in tutto herunterladen. Um die Twitter-API3 nutzen zu können, benötigt man zusätzlich ein Entwicklerkonto.4 Um die folgende Analyse selber durchzuführen, benötigen
Sie so ein Konto (kostenlos). Meldet man sich dort an, so gibt es die Möglichkeit, eine
neue Applikation anzulegen („Create New App“). Legt man eine Applikation an, hat man
die Möglichkeit, über R Daten aus der Twitter-API auszulesen. Man könnte auch „richtige Apps“ programmieren, die eine Schnittstelle zu Twitter haben. Das haben wir nicht
vor, aber dieser Hintergrund erklärt, warum auf dieser Webseite von der Erstellung einer
Applikation die Rede ist. Geben Sie in der sich öffnenden Maske den Namen der Applikation an, „meine_app“ oder „Politiker-Mining“ erfüllt den Zweck vollauf. Weiter wird
man nach einer kurzen Beschreibung gefragt, die etwaigen Endnutzern Ihrer App gezeigt
wird. Da unsere App keinen Nutzern gezeigt werden soll, ist der Inhalt dieses Felds eher
als Beschreibung für Sie hilfreich. Es wird auch verlangt, eine gültige URL einzugeben,
das kann Ihre persönliche Homepage sein. Schließlich bleibt noch die Geschäftsbedingungen zu lesen und per Klick zu bestätigen. Auf der folgenden Seite werden dann einige
Zugangsdaten gezeigt (s. Abb. 25.1); evtl. in mehreren Reitern. Ein weiterer Klick ist nötig, um „Access Tokens“ zu erstellen. Alle diese Zugangsdaten benötigen wir, um Daten
mittels der API abzufragen.
Mit diesen Daten können Sie sich jetzt über R in der Twitter-API anmelden (das geht
über das Paket twitteR):
appname <- "meine_app"
requestURL <- "https://api.twitter.com/oauth/request_token"
accessURL <- "http://api.twitter.com/oauth/access_token"
authURL <- "http://api.twitter.com/oauth/authorize"
consumerKey <- "Abfolge_von_Buchstaben_und_Zahlen"
consumerSecret <- "lange_Abfolge_von_Buchstaben_und_Zahlen"
accessToken = "Abfolge_von_Buchstaben_und_Zahlen"
accessSecret = "Abfolge_von_Buchstaben_und_Zahlen"
3
API steht für Application Programming Interface; kurz gesagt handelt sich um eine Schnittstelle
zwischen zwei Programmen. In unserem Fall handelt es sich um eine Schnittstelle zwischen R und
Twitter. Die Schnittstelle versteht und äußert gewisse Informationen im Zusammenhang mit den
Daten und Funktionen, die der Server, hier Twitter, bereitstellt.
4
S. https://apps.twitter.com/.
25.2 Grundlagen des Twitter-Minings
467
Abb. 25.1 Authentifizierung bei Twitters Entwicklungsumgebung
Sie können diese Daten auch in eine Datei auslagern und dann mit source() einlesen:
source("~/Documents/Div/credentials/twitter_oauth.R")
setup_twitter_oauth(consumer_key = consumerKey,
consumer_secret = consumerSecret,
access_token = NULL,
access_secret = NULL)
25.2.2
Hashtags und Nutzer suchen
Jetzt können wir z. B. nach Schlagwörtern (Hashtags) suchen mit searchTwitter();
schauen Sie sich die Dokumentation (mit ?) für Argumente dieser Funktion an. Suchen
wir nach 100 Treffern in deutscher Sprache zum Hashtag Populismus.
tweets <- searchTwitter("populismus", lang = "de", n = 100)
Twitter speichert die Tweet-Daten als JSON-Objekt;5 unsere Funktion gibt eine Liste von
Objekten des Typs status zurück. Für eine komfortable Verarbeitung bietet es sich an,
die Daten in einen Dataframe zu konvertieren:
tweets %>%
twListToDF() -> tweets_df
5
So wie bei CSV, XML und YAML handelt es sich bei JSON um eine einfache und verbreite
Datenstruktur, die in Rohtext abgespeichert ist, s. https://en.wikipedia.org/wiki/JSON.
468
25 Fallstudie: Twitter-Mining
Um die Tweets eines oder mehrerer Nutzer herunterzuladen, kann man userTimeline()
nutzen.
tweets_s <- userTimeline("sauer_sebastian",
n = 100)
tweets_s %>%
twListToDF() -> tweets_s_df
Nicht immer ist der Text der Tweets als UTF-8 gespeichert, was Probleme (Fehlermeldungen) bereiten kann. Daher definieren wir den Text der Tweets zunächst als UTF-8:
Encoding(tweets_df$text) <- "UTF8"
Encoding(tweets_s_df$text) <- "UTF8"
Diese Daten können wir nun untersuchen. Worüber spricht dieser Sebastian Sauer?
tweets_s_df %>%
dplyr::select(text) %>%
unnest_tokens(output = Wort, input = text) %>%
filter(str_detect(Wort, "[a-z]")) %>%
count(Wort, sort = TRUE) %>%
head()
#> # A tibble: 6 x 2
#>
Wort
n
#>
<chr> <int>
#> 1 https
88
#> 2 t.co
88
#> 3 rstats
35
#> 4 in
25
#> 5 to
25
#> 6 for
20
Offenbar über unverständliches Zeugs. „rstats“ könnte für R stehen. In gewohnter Manier können mit Daten dieser Art Worthäufigkeiten und Sentiments analysiert werden (s.
Kap. 24). Entfernen wir alle Strings, die von URLS stammen, wie „t.co“:
tweets_s_df_cleared <- tweets_s_df %>%
mutate(text = str_replace_all(string = text,
pattern = "[:blank:]*https://[:graph:]+",""))
Mit diesem Aufruf wurden die URLS entfernt; vergleichen Sie einmal folgende beiden
Aufrufe:
tweets_s_df$text %>% head()
tweets_s_df_cleared$text %>% head()
25.2 Grundlagen des Twitter-Minings
469
Betrachten wir diese Regex-Syntax genauer:
[:blank:]*: Jede Stelle mit null oder mehr Leerzeichen (vgl. Abschn. 24.1.2) ist
angesprochen.
https://: Das ist kein Regex-Operator, sondern identifiziert Strings, die dieser Zeichenkette entsprechen. Mit dieser Zeichenkette beginnen URLS.
[:graph:]+: graph ist die Summe von Buchstaben, Ziffern und Punktzeichen (wie
., ,, . . . ); der Quantitätsoperator + steht für Stellen, die mindestens einmal vorkommen.
Insgesamt kann man diese Regex-Syntax also grob so lesen. „Finde Strings dieser Art:
Beginnt man mit 0 oder mehr Leerzeichen, gefolgt von „https:// “, gefolgt von einem
oder mehr graph-Symbolen“. Im Abschn. 24.1.2 finden Sie weitere Hinweise zur RegexSyntax.
25.2.3
Tweets einer Nutzermenge auslesen
Eine andere interessante Herangehensweise an ein Twitter-Mining ist es, eine Reihe von
relevanten Nutzern (bzw. deren Nutzerkonten) auszumachen und die Tweets dieser Nutzermenge auszulesen. Wir greifen auf einen Vektor mit Twitter-Nutzernamen deutscher
Politiker zurück:
data(polits_twitter, package = "pradadata")
head(polits_twitter)
#> # A tibble: 6 x 2
#>
party screenName
#>
<chr> <chr>
#> 1 afd
Alice_Weidel
#> 2 afd
ArminPaulHampel
#> 3 afd
Beatrix_vStorch
#> 4 afd
FraukePetry
#> 5 afd
Georg_Pazderski
#> 6 afd
Joerg_Meuthen
Die Daten sind nicht mehr ganz aktuell; die Dokumentation (?polits_twitter) erzählt
uns, dass die Daten im August 2017 gesammelt wurden. Damals war z. B. Frau Petry noch
Mitglied der AfD. Je nachdem, wie viele Accounts und wie viele Tweets pro Account wir
über die Twitter-API auslesen, kann das etwas Zeit in Anspruch nehmen. Daher messen
wir im Folgenden die vergangene Zeit folgendermaßen:
start_time <- Sys.time()
# tue etwas Wichtiges...
end_time <- Sys.time()
end_time - start_time
470
25 Fallstudie: Twitter-Mining
twitteR scheint keine Tibbles zu verkraften, daher bleiben wir bei altbewährten
data.frames.
usernames <- c("realDonaldTrump", "sauer_sebastian", "TweetOfGod")
start_time <- Sys.time()
usernames %>%
map(userTimeline, n = 50) -> tweets_strange3
end_time <- Sys.time()
end_time - start_time
Übersetzen wir diese Syntax ins Deutsche:
Schaue auf die Uhr.
Nimm den Vektor usernames UND DANN
ordne jedem Element dieses Vektors (d. h. jedem Nutzerkonto) die Funktion
userTimeline() zu, welche die Tweets ausliest UND DANN
speichere den resultierenden Dataframe als tweets_strange3. Schau zum
Schluss wieder auf die Uhr und sag, wie viel Zeit verstrichen ist.
userTimeline gibt die Ergebnisse als Liste zurück. Wandeln wir das Ergebnis in
einen Dataframe um:
tweets_strange3 %>%
map_dfr(twListToDF) -> tweets_strange3_df
map_dfr nimmt jedes Listenelement von tweets_strange3 und wendet jeweils die
Funktion twListToDF an, was aus jedem Listenelement einen Dataframe mit Tweets
macht. Das Suffix _dfr von map_dfr() steht für „Dataframe rowwise“; normalerweise
würde map() eine Liste zurückgeben, aber jetzt wird ein Dataframe zurückgegeben, der
die einzelnen Elemente (selber schon Dataframes) zeilenweise zusammenfügt. Dasselbe
Ergebnis erreicht man mit einer „For-Schleife“; das erfordert mehr Tipparbeit, ist weniger
kompakt, aber dafür vielleicht einfacher zu verstehen:
tweets <- data.frame()
for (i in seq_along(usernames)){
temp_df <- twListToDF(userTimeline(usernames[i], n = 5))
tweets <- rbind(tweets_df, temp_df)
}
seq_along(usernames) ist ganz praktisch: Es erzeugt einen Index für die Elemente
von usernames:
seq_along(usernames)
#> [1] 1 2 3
25.2 Grundlagen des Twitter-Minings
471
Nach diesem Prinzip könnten Sie eine größere Menge an Nutzerkonten auslesen. Interessant wäre z. B., wie sich Politiker einer Partei, Mitarbeiter einer Firma oder Bürger einer
Gegend zu bestimmten Themen äußern. Denken Sie daran, dass es nötig ist, bei Twitter
angemeldet zu sein, z. B. mit setup_twitter_oauth(). Außerdem müssen Sie online
sein, um sich anzumelden und um Daten auszulesen.
25.2.4
Die
Aufbau einer Tweets-Datenbank
Twitter-API
begrenzt die Anzahl der Abfragen pro Zeitintervall;
mit
getCurRateLimitInfo() bekommt man einen Überblick, wie viele Abfragen man
noch machen darf bzw. wie viele Elemente man in einem gewissen Zeitraum noch auslesen darf. Natürlich ist unser Tweet-Vorrat noch nicht groß; durch wiederholte Abfragen
der API kann man mit der Zeit eine größere Menge an Tweets anlegen. twitteR stellt
einen Mechanismus zur Verfügung, um Daten in einer SQLite-Datenbank zu speichern.
So legt man eine einfache Datenbank an:
tweets_db <- tempfile()
register_sqlite_backend(tweets_db)
Da unser Objekt tweets kein Objekt mit Twitter-Daten (Objekttyp status) ist, sondern
ein Liste mit Objekten des Typ status, müssen wir jedes Element (d. h. jedes Objekt
vom Typ status) einzeln speichern:
tweets %>%
map(store_tweets_db)
Dies kann man, sooft man Daten heruntergeladen hat, wiederholen. Möchte man die
Tweets auslesen, so geht das z. B. so:
tweets_aus_db <- load_tweets_db(as.data.frame = TRUE)
glimpse(tweets_aus_db)
Aufgaben
Richtig oder falsch?6
1.
2.
6
Twitter gibt nicht preis, wer der Autor eines Tweets ist (aus Datenschutzgründen).
Man darf Tweets unter bestimmten Bedingungen speichern und weitergeben,
vorausgesetzt, es handelt sich um „dehydrierte“ Tweets.
F, R, F, R, F, R, R, F, T, F.
472
3.
25 Fallstudie: Twitter-Mining
Aus Datenschutzgründen macht Twitter nie die Geo-Daten (Längen- und Breitengrade) eines Tweets öffentlich.
4. Löscht ein Nutzer einen Tweet, so ist dieser aus Datenschutzgründen nicht mehr
über die API abrufbar.
5. Über die Twitter-API können nicht mehr als 1000 Tweets pro Tag kostenlos
heruntergeladen werden.
6. Man kann Twitter über die API sowohl nach Themen, Trends als auch nach
Nutzernamen durchsuchen.
7. Diese Regex „\d\d\d_\w+.Rmd“ wird zu diesem String „112_NSE.Rmd“ passen. Hilfestellung: str_detect(string, pattern).
8. Diese Regex „\d\d\d_\w+.Rmd“ wird zu diesem String „12_NSE.Rmd“ passen.
9. Diese Regex „\d\d\d_\w+.Rmd“ wird zu diesem String „_112.Rmd“ passen.
10. Diese Regex „ˆ\d\d\d_\w+.Rmd“ wird zu diesem String „_112_NSE.Rmd“ passen.
Teil VIII
Kommunizieren
26
RMarkdown
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
Lernziele
Anforderungen an ein gutes Werkzeug zur Textverarbeitung benennen können
Die Grundlagen der Markdown-Syntax kennen
Wissen, was YAML ist
Die Ablaufschritte der Texterstellung mit RMarkdown kennen
Tabellen mit Markdown erzeugen können
Zitationen und Referenzen im Text mit Markdown erstellen können
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(knitr)
library(tidyverse)
library(xtable)
library(stargazer)
library(kableExtra)
library(formattable)
library(apaTables)
data(stats_test, package = "pradadata")
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_26
475
476
26 RMarkdown
Nehmen wir an, Sie möchten die Ergebnisse Ihrer Datenanalyse niederschreiben. Sei es,
dass Sie einen Professor beglücken müssen wollen; sei es, dass Sie einem Kunden einen
Bericht der Quartalszahlen inkl. Forecast erstellen oder mit einem Kollegen Ideen austauschen. Welche Anforderungen stellen Sie an ein Werkzeug (d. h. Software), mit dem Sie
die Ergebnisse niederschreiben?
26.1
Forderungen an Werkzeuge zur Berichterstellung
Einige Anforderungen an Werkzeuge zur Berichterstellung sind im Folgenden aufgelistet:
1.
R-Syntax und -Ausgaben sollten mit dem eigentlichen Text in einem Dokument integriert sein.
2. Man sollte sich beim Schreiben auf den Inhalt konzentrieren können – ohne sich auf
die Formatierung konzentrieren zu müssen.
3. Das Werkzeug sollte einfach zu erlernen (und zu bedienen) sein.
4. Es sollte verschiedene Ausgabeformate (PDF, HTML, . . . ) unterstützen.
5. Das (fertige) Dokument sollte optisch ansprechend formatiert sein.
6. Das Werkzeug sollte übergreifend (Betriebssysteme, Zeiten, Versionen) arbeiten.
7. Das Werkzeug sollte mächtig (Funktionsreich) sein.
8. Das Werkzeug sollte frei (quelloffen sowie kostenfrei) sein.
9. Das Werkzeug sollte Zusammenarbeit mit anderen leicht machen.
10. Das Werkzeug sollte Versionierungen und Änderungsverfolgung unterstützen.
Mit Werkzeug ist hier die Software zur Erstellung des Berichts gemeint; viele Menschen
denken sofort an Microsoft Word oder PowerPoint. Betrachten wir die einzelnen Forderungen näher und überlegen, welches Schreibwerkzeug gut passen könnte.
R-Synax und -Ausgaben sollten mit dem eigentlichen Text in einem Dokument integriert
sein.
Sind das Werkzeug der Datenanalyse und das Werkzeug zur Berichterstellung voneinander getrennt, dann resultiert eine Schnittstelle, die wie eine Sollbruchstelle wirkt. Dieser
Graben ist stets zu überwinden. Zwei Dinge, die dann fast zwangsläufig passieren: CopyPaste und manuelles Aktualisieren. Copy-Paste ist mühsam und fehleranfällig. Mitunter
weicht man vielleicht auf Neu-Eintippen aus: „Ok, der Mittelwert war doch 12.34 . . . “;
Fehler sind dann vorprogrammiert. Zum manuellen Aktualisieren: Stellen Sie sich vor, Ihr
Bericht beinhaltet sagen wir 50 Diagramme, eine Reihe Tabellen und sonstige Zahlen –
pro Kunde, pro Berichtszeitraum. Sie haben 100 Kunden, die einen Bericht pro Woche
verlangen . . . Und jetzt für jedes Diagramm etc. in der Berichts-Software auf „Einfügen
. . . “ klicken? Effizientes Arbeiten geht anders. Kurz: Die Analyse sollte sich nahtlos in Ihren Bericht einfügen. Das ist vor allem für die Ergebnisse (Zahlen, Diagramme, Tabellen
. . . ) wichtig, aber sekundär auch für die Syntax der Datenanalyse.
26.1 Forderungen an Werkzeuge zur Berichterstellung
477
Man sollte sich beim Schreiben auf den Inhalt konzentrieren können – ohne sich auf
die Formatierung konzentrieren zu müssen.
Entscheidend sind die Inhalte, die Formatierung ist zweitrangig und außerdem zeitlich
nachgelagert. Bietet das Werkzeug reichhaltige Möglichkeiten zur Formatierung, besteht
die Gefahr, dass verfrüht der geistige Fokus von den Inhalten zur Dekoration der Inhalte
wandert. Besser kann es sein, sich in gestalterischer Askese zu üben und die mentalen Ressourcen komplett auf die Inhalte zu konzentrieren. Außerdem ist es wünschenswert, wenn
das Werkzeug sich selbständig um das Formatieren kümmert. Damit soll dem Autor nicht
die Möglichkeit verwehrt sein, selber zu gestalten; doch sollte ihm optisch ansprechende
Standards automatisch angeboten werden.
Das Werkzeug sollte einfach zu erlernen (und zu bedienen) sein.
Nimmt mir das Werkzeug die Formatierung weitgehend ab, so bleibt (fast nur) das
reine Schreiben von Text übrig. Das ist einfach zu bewerkstelligen (oder sollte es sein).
Aufwändiges Einarbeiten in neue Werkzeuge sollte entfallen. Ein Werkzeug zur Texterstellung ist LATEX. Bei Latex ist das nicht unbedingt gegeben; Latex zu lernen und in Latex
zu schreiben, kann recht aufwändig sein.
Das Werkzeug sollte verschiedene Ausgabeformate (PDF, HTML . . . ) unterstützen.
Die wichtigsten Formate, um Berichte zu erstellen sind sicherlich PDF, DOC, PPT
und neuerdings zunehmend HTML und EPUB-Formate. Diese Formate sollten unterstützt
werden. HTML und EPUB gewinnen mit der Verbreitung elektronischer Lesegeräte an
Bedeutung. So ist z. B. HTML an einem Tablet oder Handy besser zu lesen als eine PDFDatei. Ein wesentlicher Grund ist, dass bei HTML Zeilenumbrüche flexibel sind, z. B. je
nach Größe des Displays. Bei PDF-Dateien sind Zeilenumbrüche auf ein Seitenformat
festgelegt.
Das (fertige) Dokument sollte optisch ansprechend formatiert sein bzw. formatiert werden können.
Das Werkzeug sollte – idealerweise als Standard ohne Eingriffe des Nutzers – optisch
ansprechende Dokumente erzeugen. Dazu gehört vor allem schöner Schriftsatz: keine
hässlichen Löcher zwischen Wörtern oder zwischen Buchstaben, elegante Schriftsätze
oder schöne Formeln. Für HTML-Dateien sind „coole“ Designs – z. B. mit benutzerfreundlicher Navigationsspalte – wünschenswert.
Das Werkzeug sollte übergreifend (Betriebssysteme, Zeiten, Versionen) arbeiten.
Ärgerlich ist es, wenn das Werkzeug nur für bestimmte Betriebssysteme (oder Versionen davon) funktioniert. Nicht so schlimm, aber auch nervig ist, wenn die Varianten
eines Werkzeugs zwar übergreifend funktionieren, aber nicht ganz gleich sind – wenn die
resultierenden Dokumente unterschiedlich aussehen, je nachdem, mit welcher Variante
des Werkzeugs man arbeitet, so ist das von Nachteil. Auch in ein paar Jahren sollten die
Dateien noch lesbar sein.
Das Werkzeug sollte mächtig (funktionsreich) sein.
Die Quadratur des Kreises: Ein Werkzeug sollte einfach zu bedienen sein, aber mächtig, also einen großen Funktionsumfang besitzen. Beide Ziele sind nicht leicht unter einen
Hut zu bringen und vielleicht das Grundproblem der Benutzerfreundlichkeit von Compu-
478
26 RMarkdown
tersystemen. Eine pragmatische Lösung ist es (oder sollte sein), dem Nutzer vernünftige
Standardwerte anzubieten bzw. diese ungefragt anzuwenden, aber gleichzeitig dem Nutzer die Möglichkeit zu geben, in viele Details einzugreifen. Letzteres ist häufig kompliziert(er), aber für viele Nutzer nicht nötig, wenn die Standards gut sind.
Das Werkzeug sollte frei (quelloffen sowie kostenfrei) sein.
Ein „Normalnutzer“ denkt vielleicht: „Ist mir doch egal, ob das Werkzeug von Firma
XYZ angeboten wird, kauf ich mir halt!“ Das ist in einigen Fällen stimmig. Aber: Die Erfahrung zeigt, dass quelloffene Software sich tendenziell schneller weiterentwickelt wird
(oder Fehler beseitigt werden). Außerdem ist Transparenz die Grundlage von Vertrauen:
Wer mag (und kann), schaut sich den Quellcode eines Werkzeugs an, und bildet sich selber ein Urteil über die Qualität und Datensicherheit. Weniger schön ist es, wenn man nur
das Wort eines Anbieters hat; möglicherweise sind dessen Interesse nicht deckungsgleich
mit Ihren. Bei quelloffener Software können Nutzer (mit den entsprechenden Kenntnissen) ihre Ideen der Allgemeinheit zur Verfügung stellen. Dieser Weg kann schneller sein
als die Produktentwicklung bei einem Software-Konzern (R vs. SPSS ist ein Beispiel).
Außerdem: Manche Werkzeuge der Datenanalyse sind sehr teuer; da ist es schön, wenn
man (bessere) Leistung kostenfrei bekommt. Nicht jeder ist mit ausreichend Finanzmitteln
gesegnet . . . „Offen“ beinhaltet in dem Zusammenhang idealerweise auch, dass die vom
Werkzeug erstellten Quelldateien menschenlesbar sind, also reine Text-Dateien (im Gegensatz zu Dateien des Typs .doc). Damit kann jeder, auch ohne ein bestimmtes Werkzeug
zu nutzen, die Quelldatei lesen. Reine Textdateien haben wohl von allen Datei-Formaten
die beste Aussicht, in zehn, 20, 50 oder 100 Jahren noch lesbar zu sein.
Das Werkzeug sollte Zusammenarbeit mit anderen leicht machen.
Einfache Werkzeuge, um gemeinsam ein Dokument zu schreiben, sind z. B. Google
Docs, Microsoft SharePoint, Authorea oder Dropbox. Mächtiger (aber komplexer in der
Bedienung) sind spezielle Werkzeuge wie Git;1 und Github (s. Abschn. 27.7.3).
Das Werkzeug sollte Versionierungen und Änderungsverfolgung unterstützen.
Der Änderungsmodus von MS-Word und ähnlichen Werkzeugen ist hilfreich für eine begrenzte Anzahl von Änderungen. Wurde aber eine bestimmte Passage mehrfach
geändert, so kommt diese Funktionalität an ihre Grenzen. Kurz: Bei wichtigeren oder
komplexeren Dokumenten ist eine professionelle Lösung sinnvoll. Daher werden bei größeren Projekten, gerade in der Software-Entwicklung, spezielle Versionierungswerkzeuge
wie Git verwendet (s. Abschn. 27.7.3).
26.2
Start mit RMarkdown
RMarkdown erfüllt diese Forderungen. Nicht perfekt, aber besser wohl als viele andere
Werkzeuge. Daher ist es ein sehr wertvolles Werkzeug, wenn Sie Texte mit Datenanalysen
(Syntax und Ausgaben) schreiben. Dieses Buch wurde mit RMarkdown in RStudio ge1
https://de.wikipedia.org/wiki/Git.
26.2 Start mit RMarkdown
479
schrieben. RMarkdown wird als Teil von RStudio mitgeliefert, kostenlos und quelloffen.
Es ist bereits installiert, wenn Sie RStudio installiert haben. Der Name RMarkdown suggeriert, dass R hier eine Rolle spielt. Das ist richtig. Doch was heißt „Markdown“? Der Name
rührt von der Idee von Auszeichnungssprachen (Markup Languages) her. Auszeichnungssprachen sind, einfach gesagt, ein paar Befehle, die man in einen Text hineinschreibt, um
den Text zu formatieren. Ein Beispiel wäre „Überschrift(Vorwort)“. Diese (ausgedachte)
Auszeichnung soll „Vorwort“ als Überschrift formatieren. Wie ein Text vom Typ Überschrift formatiert werden soll, muss an anderer Stelle definiert sein (mit den Parametern
wie Schriftgröße, Schriftart c.). Ein Textdokument einer Auszeichnungssprache ist also
eine Mischung aus Inhalt und Formatierungszeichen. HTML oder Latex sind bekannte
Auszeichnungssprachen. Beide sind aber recht komplex; oft sieht der Text aus wie Kraut
und Rüben vor lauter Auszeichnungsbefehlen. Markdown will anders sein, einfacher.
Markdown-Texte sollen (fast) so aussehen wie Text ohne Auszeichnungen; die Auszeichnungen sollen zurückhaltend sein, den Textfluss möglichst wenig stören. In Abschn. 26.5
lernen Sie die Syntax von Markdown kennen. Einige andere Auszeichnungssprachen wie
HTML oder Latex sind vor lauter Auszeichnungen kaum lesbar. Daher ist Markdown keine „Markup-“, sondern eine „Markdown-Sprache“. Von „Sprache“ braucht man sich hier
nicht beunruhigen zu lassen. Die „Sprache“ Markdown ist in zehn Minuten gelernt. Wie
Sie sicher schon geahnt haben, hat das R in RMarkdown die Bedeutung, dass R in das
Markdown „eingestrickt“ wird. Damit kann man dann einfach Dokumente erzeugen, in
denen Daten analysiert werden und zusätzlich Text vorkommt (nennen wir es ein R-TextDokument). Stellen Sie sich vor, wie eine E-Mail aussieht, wenn man sie im einfachen
Texteditor schreibt. Das gibt nach John Gruber, dem Entwickler von Markdown, einen
guten Eindruck von Markdown.
Eine wesentliche Design-Idee von Markdown ist es, dass im Text keine Details der
Formatierung – wie Schriftgröße oder -art – vorkommen sollten. Im Text wird definiert,
dass eine Textstelle eine Überschrift ist, aber nicht, wie sie deshalb formatiert werden
soll. Letztere Information ist an eine Vorlagen-Datei (Stylesheet; z. B. eine CSS-Datei)
ausgelagert.
RMarkdown-Dateien haben sinnvollerweise die Endung .Rmd. Rmd-Dateien sind, genau wie normale Markdown-Dateien, schnöde Text-Dateien und können entsprechend von
jedem Text-Editor (auch mit RStudio, das hier mit Syntax-Highlighting verwöhnt) geöffnet werden. Erstellt man eine Rmd-Datei, so wird das Verzeichnis, in dem diese Datei
liegt, von RStudio als Arbeitsverzeichnis betrachtet.2
Doch wie sehen die Arbeitsschritte mit RMarkdown aus? Wo fange ich an, und was
„kommt hinten bei raus“? Kurz gesagt, sind zwei Konvertierungen bzw. zwei technische
Schritte beteiligt (s. Abb. 26.1). Man beginnt mit einem R-Markdown-Dokument (d. h.
2
Möchte man auf eine Datei verweisen, die in einem Unterverzeichnis liegt (z. B. ein Bild laden), so ist es meist am einfachsten, einen relativen Pfad vom Arbeitsverzeichnis anzugeben:
Bilder/mein_bild.png. Dieser Pfad sucht nach Bilder/mein_bild.png innerhalb des
aktuellen Arbeitsverzeichnisses.
480
26 RMarkdown
R+Text
Reintext
knitr
PANDOC
PDF
HTML
weitere
Formate
Abb. 26.1 Die zwei Arbeitsschritte bei der Konvertierung von RMarkdown-Dokumenten
eine Datei vom Typ .Rmd). In diesem Dokument finden sich sowohl R-Befehle als auch
normaler Text. Im ersten Schritt werden die R-Befehle, die sich in diesem Rmd-Dokument
finden, ausgeführt und deren Ergebnis in ein neues Dokument geschrieben. Dieses resultierende Dokument ist eine (reine) Markdown-Datei (M #); die R-Befehle wurden
ausgeführt und ihre Ausgaben (z. B. Diagramme) wurden in das Dokument eingefügt.
Dieser Arbeitsschritt wird vom R-Paket knitr geleistet.
RMarkdown unterstützt drei wesentliche Ausgabeformate: HTML-, Word- und
PDF-Dokumente; aber auch andere Format sind möglich. Um PDF-Dokumente erstellen zu können, muss Latex auf Ihrem Computer installiert sein. Wenn Sie eine
Rmd-Datei in ein anderes Auszeichnungsformat (wie HTML) übersetzen, so wird
(in der Voreinstellung) der Ablageort dieser Datei als Arbeitsverzeichnis verwendet,
unabhängig davon, auf welchen Ordner Ihr Arbeitsverzeichnis gesetzt ist.
26.3
RMarkdown in Action
Werden wir praktisch! Ein Beispiel zur Arbeit mit Markdown. Öffnen Sie RStudio. Klicken Sie das Icon für „neue Datei“ und wählen Sie „R Markdown . . . “, um eine neue
RMarkdown-Datei zu erstellen (File > New File > R Markdown). Dann öffnet sich eine einfache Rmd-Datei, die nicht ganz leer ist, sondern aus didaktischen Gründen ein
paar einfache, aber typische Rmd-Befehle enthält. Schauen Sie sich den Inhalt kurz an;
Sie sehen eine Mischung aus „Prosa“, Rmd-Steuerbefehlen und R. Jetzt „verstricken“ (to
knit) wir die R-Teile und die Text-Teile zu einer Datei; als Ausgabe-Format wählen wir
HTML (das ist die Voreinstellung). „Verstricken“ bedeutet dabei, dass die Ergebnisse des
R-Codes in den Text „gestrickt“ werden. Anders gesagt: Der Code wird ausgeführt; die Er-
26.3 RMarkdown in Action
481
gebnisse werden in das Markdown-Dokument eingefügt. Die Ausgabedatei enthält dann
sowohl Text als auch R-Ausgaben; auf Wunsch des Nutzers kann auch die ausgeführte R-Syntax im Ausgabedokument beinhaltet sein. Das Paket knitr führt den R-Code
aus und fügt ihn sowie die Ausgabe (z. B. Diagramme) in den Text ein. Dafür klicken
Sie auf das Stricknadel-Icon oder drücken die Tasten Strg-Shift-K. Ach ja, RStudio bittet Sie noch, die Rmd-Datei zu speichern. Tun Sie RStudio den Gefallen. Als Ergebnis
müsste sich im Reiter „Viewer“ das Ergebnis zeigen. Sie haben eine, vielleicht Ihre erste,
HTML-Datei aus einer Rmd-Datei erstellt. Verweilen Sie in Andacht. Sie können sich das
Ergebnis, die HTML-Datei, im Browser anschauen, indem Sie betreffende HTML-Datei
im Browser öffnen (diese liegt dort, wo auch die zugehörige Rmd-Datei liegt). Wenn Sie
Ihr Markdown-Dokument in ein Latex-Dokument übersetzen und als PDF-Datei ausgeben lassen wollen, muss Latex (oder eine andere TeX-Distribution) auf Ihrem Computer
installiert sein; RStudio findet Latex in der Regel zu diesem Schritt. Hat man schließlich
die Struktur und zentrale Ideen erarbeitet, so kann man den Text – jetzt mit wohlgeformter(er) Sprache und rotem Faden – schreiben. Dabei ist es oft so, dass man die Beiträge
von Koautoren integrieren möchte. Dies kann mit einer (zusätzlichen) Software wie „Git“
bzw. „Github“ erreicht werden, die ebenfalls quelloffen, kostenlos (und weit verbreitet
bei Tekkies) ist („Coautoren integrieren“). Diese Software erlaubt nicht nur Versionierung lokal an einem eigenen Computer, sondern auch in Zusammenarbeit mit Coautoren
(„Änderungen nachverfolgen“). Parallel zum Schreiben des Textes sind die Daten zu analysieren; eine Aufgabe, die prinzipiell unabhängig vom Schreiben des Textes ist („Daten
analysieren“). Gehen wir davon aus, dass Ihr Dokument statistische Analysen auf Basis
von R enthält. Die Ergebnisse der Analyse sollen in den Text eingefügt werden (daher
„R+Text“ als Ausgangspunkt in Abb. 26.1). Um Schnittstellen zu vermeiden, ist es sinnvoll, den Inhalt-Text sowie die R-Syntax in einer Datei zu „verstricken“ (mit knitr).
Knitr führt die R-Syntax im Dokument aus und liefert das Ergebnis im Markdown-Format
zurück. Aus R+Markdown wird reines Markdown. Die Markdown-Datei kann nun in fast
jedes denkbare andere Markup-Format übersetzt werden, die wichtigsten sind HTML und
Latex. Die Übersetzung führt RStudio wiederum, genau wie das „Knittern“, auf Wunsch
komfortabel im Hintergrund aus. Dazu wird auf ein Programm namens pandoc (http://
pandoc.org) zurückgegriffen. Pandoc übersetzt eine Markup-Sprache in eine andere. Haben wir z. B. in Latex übersetzt, so können wir das Dokument – vorausgesetzt, Latex o. Ä.
ist installiert – in PDF umwandeln lassen. Ist Tex auf Ihrem Rechner installiert, so wird
Ideen
sammeln
Änderungen
kontrollieren
Text
schreiben
R
ausführen
Text
formatieren
Coautoren
integrieren
Daten
analysieren
Vorlagen
nutzen
Abb. 26.2 Die Arbeitsschritte mit RMarkdown
Ausgabe
wählen
482
26 RMarkdown
diese Übersetzung wiederum automatisch vorgenommen. Abb. 26.2 stellt den eben skizzierten Ablauf dar.
26.4
Aufbau einer Markdown-Datei
Eine Markdown-Datei (.md) ist eine reine Text-Datei, genau wie eine Rmd-Datei. Eine
Rmd-Datei ist auch eine Markdown-Datei, nur eben mit zusätzlichem R-Code. Mit jedem
Texteditor können Sie Rmd- und md-Dateien öffnen und ändern. So kann eine Rmd-Datei
aussehen:
--title: "Mein Titel"
author: "Sebastian Sauer"
hinweis: "Das ist der YAML-Header"
--# Einleitung
Das ist ein RMarkdown-Dokument. Jetzt kommt ein Chunk mit R-Syntax:
```{r}
summary(cars)
```
Typischerweise besteht eine Markdown-Datei aus drei Teilen:
1. Einem (optionalen) YAML-Header, abgegrenzt durch ---.
2. R-Chunks (R-Syntax-Abschnitten), abgegrenzt durch ```.
3. Normalem Reintext (Prosa) mit einigen Steuerzeichen wie *kursiv* für Kursivschrift.
Der YAML-Header definiert einige Meta-Daten für Ihre Datei; Dinge wie Autor, Titel,
Datum oder Ausgabeformat werden hier festgelegt. Sie müssen keine YAML-Header angeben, dann werden einige Standards verwendet. YAML steht übrigens, falls Sie sich das
gerade fragen, für „yet another markup language“, also eine Auszeichnungssprache, die
sehr einfach ist und (in unserem Fall) für Metadaten zuständig ist. Warum ist das praktisch? YAML definiert auf einfache Weise die Metadaten Ihres Dokuments. Wenn Sie auf
diese Weise festlegen, was Titel, Datum, Autor etc. ist, so kann die Formatierung automatisch (vom „Stylesheet“) übernommen werden. Das Stylesheet „weiß“ dann, „ah das ist
der Titel, da erstelle ich jetzt eine schön formatierte Titelseite“. Online finden sich einige
Tutorials zu YAML, die die Details der Sprache erläutern.3
R-Syntax wird (innerhalb von Rmd-Dateien) nur innerhalb der „Chunks“ als R verstanden; außerhalb der eingezäunten R-Abschnitte werden sie als normaler Text verstanden.
3
http://www.yaml.org/spec/1.2/spec.html; https://de.wikipedia.org/wiki/YAML.
26.5 Syntax-Grundlagen von Markdown
483
Sie können Chunks auch nochmal ausführen, in dem Sie in RStudio z. B. den Button
„Run“ klicken. Um das ganze Dokument zu „übersetzen“, d. h. in ein Ausgabeformat wie
PDF umzuwandeln, reicht ein Klick auf das Stricksymbol. Der Text bei Markdown sieht
im Prinzip so aus, wie man eine Plain-Text-E-Mail früher (oder manchmal heute noch)
geschrieben hätte. Auch ohne dass man Markdown kennt, kann man ihn ohne Probleme
erfassen.
Übrigens bedeutet ein Zeilenumbruch in einer Rmd- oder md-Datei keinen Zeilenumbruch im Ausgabe-Dokument. Der einfache Zeilenumbruch dient primär der
Übersichtlichkeit in der Textdatei. Eine Faustregel besagt, dass eine Textzeile nicht
länger als 70 bis 80 Zeichen sein sollte, damit sie gut zu lesen ist. Möchten Sie im
fertigen Dokument einen (sichtbaren) Zeilenumbruch einfügen, so fügen Sie in Ihrem Markdown-Dokument eine Leerzeile mit der Returntaste ein, bevor Sie Enter
drücken.
26.5
Syntax-Grundlagen von Markdown
In RStudio wird Pandocs Markdown verwendet; die Übersetzung von Markdown in eine
andere Auszeichnungssprache wird komplett von Pandoc abgewickelt. Man braucht etwas
Übung, um sich die Syntax zu merken, aber im Grunde ist es ganz einfach. Schauen Sie
selbst:
Text formatieren
-----------------------------------------------------------*kursiv* oder so _kursiv_
__fett__
**fett**
`R-Syntax`
hochgestellt^2^ und tiefgestellt~2~
Überschriften
-----------------------------------------------------------# 1. Ebene
## 2. Ebene
### 3. Ebene
Aufzählungen
------------------------------------------------------------
484
26 RMarkdown
*
Listenpunkt 1
*
Listenpunkt 2
* Listenpunkt 2a
* Listenpunkt 2b
1.
Element 1 einer nummerierten Liste
1.
Element 2. Die korrekte Nummer wird automatisch erstellt.
Links und Bilder
-----------------------------------------------------------<http://Beispiel.com>
[Bezeichnung des Links](http://Beispiel.com)
![Optionale Bildbezeichnung](pfad/zum/bild.png)
Tabellen
-----------------------------------------------------------Spaltenkopf1
------------Zelleninhalt
Zelleninhalt
|
|
|
|
Spaltenkopf1
------------Zelleninhalt
Zelleninhalt
Wenn man mal etwas vergisst, kann man in RStudio hier nachschauen: Help > Markdown
Quick Reference.
26.6
Tabellen
Praktisch ist, dass man sich Dataframes in (R)Markdown einfach als Tabellen ausgeben
lassen kann. Normalerweise werden in RMarkdown Dataframes in gewohnter Manier ausgegeben:
mtcars %>%
slice(1:3)
#>
mpg cyl disp hp drat
wt qsec vs am gear carb
#> 1 21.0
6 160 110 3.90 2.62 16.5 0 1
4
4
#> [ reached getOption("max.print") -- omitted 2 rows ]
26.6 Tabellen
485
Tab. 26.1 Eine Tabelle mit kable aus knitr
mpg
21.0
21.0
22.8
cyl
6
6
4
disp
160
160
108
hp
110
110
93
drat
3.90
3.90
3.85
wt
2.62
2.88
2.32
qsec
16.5
17.0
18.6
vs
0
0
1
am
1
1
1
gear
4
4
4
carb
4
4
1
slice() filtert die angegebenen Zeilen; hier 1 bis 3. Möchte man eine „richtige“ Tabelle,
so kann man z. B. mit dem Befehl knitr::kable() arbeiten. Die Tab. 26.1 wurde mit
folgender Syntax erstellt:
mtcars %>%
slice(1:3) %>%
kable(caption = "Eine Tabelle mit kable aus knitr")
Mittlerweile gibt es eine Reihe von Paketen, mit denen man komplexe Tabellen einfach erzeugen kann. Gerade für HTML-Dokumente hat sich einiges getan in letzter Zeit. Mit dem
Paket xtable kann man ansprechende Tabellen für PDF-Dokumente und für HTMLDokumente erstellen. Das Paket bietet auch, im Gegensatz zu knitr::kable() die
Möglichkeit, die Ergebnisse eines linearen Modells (lm(), glm(), aov()) in eine ansprechende Tabelle zu gießen. Das Paket stargazer bietet ähnliche Funktionalität. Mit
dem Paket apaTables kann man z. B. die Ergebnisse eines linearen Modells oder eine
Varianzanalyse in eine APA-konforme Tabelle in ein Word-Dokument (sic!) packen:
lm1 <- lm(score ~ interest + study_time + interest:study_time,
data = stats_test)
print(xtable(lm1), type = "html") # oder: type = "latex"
Neu und schön ist das Paket kableExtra, welches dem Spieltrieb zur Tabellenformatierung kaum Grenzen setzt. Allerdings bietet kableExtra keine automatische TabellenFormatierung, die verbreiteten Richtlinien folgen der American Psychological Association (APA). kableExtra baut auf dem Paket knitr auf und unterstützt Ausgaben in
HTML- und PDF-Dokumente; das Paket fügt sich gefällig in Abfolgen des tidyverse
ein (Zhu 2017). Spezifiziert man kein Ausgabeformat, so wird eine Ausgabe in Markdown erzeugt, und es bleibt der „automatischen“ Konvertierung von Pandoc überlassen,
in welches Ausgabeformat übersetzt wird. Das ist flexibel und einfach; allerdings unterstützt Markdown bei Weitem nicht alle Formatierungsmöglichkeiten, die HTML und
Latex/PDF bieten. Um die ganze Formatierungsmöglichkeiten zu nutzen, sollte man für
kableExtra daher angeben, in welches Format man umsetzen möchte. Am einfachsten
legt man die grundsätzliche Wahl am Anfang des Rmd-Dokuments fest:
options(knitr.table.format = "html")
# oder knitr.table.format = "latex"
486
26 RMarkdown
Abb. 26.3 Eine HTML-Tabelle mit kableExtra
So erstellt man eine ansprechend (Format „Bootstrap“) formatierte HTML-Tabelle:
kable(head(stats_test), "html") %>%
kable_styling(bootstrap_options = "striped", full_width = F)
Mit kable_styling() wird auf die Formatierung der Tabelle Einfluss genommen. Hier
verlangen wir eine Tabelle mit Zeilen in sanft abgesetzten Grautönen ("striped"), die
nicht die ganze Seitenbreite ausfüllt (full_width = FALSE). Hat man das Ausgabeformat „html“ schon vorab definiert, muss es nicht noch einmal an kable() als Argument
übergeben werden (aus Gründen der einfachen Reproduzierbarkeit ist es hier noch einmal aufgeführt). Man kann die Zeilen und Spalten jeweils feingliedrig spezifizieren (mit
column_spec für die Spalten und row_spec für die Zeilen). Abb. 26.3 zeigt das Ergebnis folgender Syntax:
kable(head(stats_test), "html") %>%
kable_styling(bootstrap_options = "striped", full_width = F) %>%
column_spec(7, bold = T) %>%
row_spec(3:4, bold = T, color = "white", background = "red")
Ist man an HTML-Dokumenten interessiert, so kann man das Paket kableExtra mit
formattable kombinieren, was Effekte wie bedingte Formatierung von Zellen (wie bei
Excel) erlaubt.
df %>%
mutate(bestanden = ifelse(bestanden == "nein",
cell_spec(bestanden, "html",
color = "red", bold = T),
cell_spec(bestanden, "html",
color = "green", italic = T)),
score = color_bar("lightblue")(score),
interest = color_tile("orange", "green")(interest)
) %>%
26.7 Zitieren
487
Abb. 26.4 Eine HTMLTabelle mit kableExtra und
formattable
kable("html", escape = F) %>%
kable_styling("hover", full_width = F) %>%
column_spec(3, width = "3cm")
Übersetzen wir die Syntax ins Deutsche; das Ergebnis zeigt Abb. 26.4.
Nimm den Datensatz df UND DANN
mutiere (hier: formatiere) die Variable bestanden in Abhängigkeit von ihrem
Wert in rote oder grüne Schrift.
mutiere (hier: formatiere) score mit einem „Farbbalken“.
mutiere (hier: formatiere) interest mit einem farbigen Hintergrund UND
DANN
Erstelle die Tabelle in HTML, wobei HTML-Steuerzeichen als solche erkannt
werden sollen UND DANN
formatiere im Stil „hover“ und nicht seitenbreit UND DANN
spezifiziere die Breite von Spalte 3 auf 3 cm.
26.7
Zitieren
Zitate sind bei (R)Markdown, genau wie Bilder und Verweise, eine Art von Links: Innerhalb von eckigen Klammern steht der Name des zu verlinkenden (zu zitierenden) Objekts.
So wurden auch in diesem Buch die Zitationen vorgenommen.
Das ist furchtbar wichtig [@WeisOis2017]. @Feistersack2017 widerspricht vehement.
Innerhalb der eckigen Klammern, die einen Linktext kennzeichnen, wird ein Klammeraffe geschrieben (@); dieser ist die Bezeichnung für eine Zitation. Dann folgt die ID der
Zitation; diese findet sich in der Datei, in der die eigentlichen Zitationen stehen (z. B.
„bibliography.bib“), bei der jeweiligen Zitation. Lässt man die eckigen Klammern weg,
488
26 RMarkdown
so wird eine In-Text-Zitation ausgegeben. Fügt man ein Minuszeichen vor den Klammeraffen, so wird nur die Jahreszahl ausgegeben (wenn der Zitationsstil sonst den Namen
hinzufügen würde). „Übersetzt“ in einem Ausgabedokument erscheinen dann formatierte
Zitationen, z. B. so: (Xie 2015).
Einige Varianten des Zitierens:
Mehrere Zitationen werden mit Strichpunkt getrennt: So steht es geschrieben
[@Weis-Ois2017; @FeisterSack1914].
Man kann beliebige Kommentare in die Zitation aufnehmen: So war es schon
immer [vgl. @Weis-Ois2017, S. 1-999].
Will man eine Zitation ohne Klammern im Text einfügen, so lässt man die eckigen Klammern weg: @Feistersack1917 sagt, es sei schon immer so
gewesen.
Den Namen des Autors/der Autoren kann man unterdrücken, in dem man ein vor die Zitation einfügt. Dann wird nur die Jahreszahl abgedruckt: Der gleiche
Autor [-@WeisOis2017] wusste es schon immer. Darauf weist er
andauernd hin.
Eine Reihe von bibliographischen Formaten wird unterstützt; darunter BibLatex, BibTeX,
endnote und medline. Die bibliographischen Einträge alle Zitationen fügt man in eine
Textdatei (z. B. mit Namen bibliography.bib); im YAML-Header der Rmd-Datei
gibt man dann den Dateinamen mit den bibliographischen Informationen an. Anhand der
Endung ordnet RMarkdown (d. h. Pandoc) dann zu, um welche Art von Format (z. B.
Bibtex) es sich bei der Literaturdatei handelt.
bibliography: bibliography.bib
csl: apa.csl
Innerhalb der Bibliographie-Datei werden dann die bibliografischen Informationen geschrieben. Bibtex bietet sich als Format an; ein bibliographischer Eintrag könnte so aussehen:
@article{Gigerenzer2004, % ID des Eintrags
doi = {10.1016/j.socec.2004.09.033}, % DOI
year = {2004},
volume = {33},
number = {5},
pages = {587--606},
author = {Gerd Gigerenzer},
title = {Mindless statistics},
journal = {The Journal of Socio-Economics}
}
Solche bibliographischen Daten kann man sich z. B. vom Verlag, wo eine bestimmte Literaturquelle wie ein Fachartikel oder Buch erschienen ist, herunterladen. Ein anderer Weg
26.8 Format-Vorlagen für RMarkdown
489
ist eine wissenschaftliche Suchmaschine wie Google Scholar. Es gibt auch Webseiten, die
ISBNs (von Büchern) in Bibtex-Einträge übersetzen.4
Bestimmte Verlage und bestimmte Reihen oder Zeitschriften verlangen einen bestimmten Stil, wie Zitationen zu formatieren sind. Bekannt sind Harvard- und Chicago-Stile.
Den Zitationsstil kann man ebenfalls im YAML-Header Ihrer Rmd-Datei mit der Variable csl definieren. Dort verweist man auf eine CSL-Stil-Datei; diese Dateien definieren
Zitationsstile. Es handelt sich um Text-Dateien mit einer einigermaßen intuitiven Syntax;
man könnte daran also rumbasteln, wenn man denn wollte. CSL-Dateien sind quelloffen.5
Böse Zungen sagen, dass es mehr Zitationsstile gäbe als wissenschaftliche Zeitschriften,
und von Letzteren gibt es Tausende.
Geben Sie für die Literatur- und die CSL-Datei keinen Pfad an, so geht R davon aus,
dass die Dateien im Verzeichnis der jeweiligen Rmd-Datei liegen.
26.8
Format-Vorlagen für RMarkdown
Eine zentrale Idee von Markdown ist es, sich beim Schreiben nicht um das Formatieren zu kümmern. Die ganze Konzentration soll beim Schreiben sein; das Formatieren ist
nachgelagert. Das hat den Vorteil, dass das Formatieren gut an eine Format-Vorlage ausgelagert werden kann. Mit Hilfe solcher Format-Vorlagen muss sich der Nutzer nicht um
das Formatieren kümmern (aber er kann, wenn er möchte). RStudio bietet für die Ausgabeformate HTML und PDF jeweils eine Format-Vorlage an, die bereits installiert ist und
automatisch verwendet wird. Möchte man Text in Latex schreiben, so hat man den zusätzlichen Vorteil, dass Markdown viel einfacher ist als Latex. Bei Markdown bleibt man
von den bei Latex allgegenwärtigen geschweiften Klammern verschont. Schreiben in Latex kann anstrengend sein, wie jeder weiß, dessen Präambel schon mal wie ein Dschungel
gewuchert ist. Markdown-Dateien sind vergleichsweise frei von Steuerungsformalitäten;
saubere, schlanke, übersichtliche Dokumente – schlicht: ein Vergnügen.
Es gibt weitere Formatvorlagen; besonders für Dokumente mit hohem Anspruch
an den Aufbau bzw. die Formatierung ist dies wichtig. Beispiele für solche Dokumente sind Manuskripte wissenschaftlicher Aufsätze sowie standardisierte Berichte
wie Qualifizierungsarbeiten in Hochschulen. Praktischerweise gibt es für diese Zwecke RMarkdown-Vorlagen. Allen diesen Vorlagen ist gemein, dass die Steuerung von
Dokument-Parametern wie Autor, Abstract, Titel oder Schriftart über den YAML-Header
kontrolliert wird – eine einfache und transparente Angelegenheit. Da diese Vorlagen
auf Pandoc basieren, bieten sie alle dessen breites Feature-Repertoire: Zitieren, Abbil4
5
https://manas.tungare.name/software/isbn-to-bibtex/.
Man kann sie sich z. B. hier herunterladen: https://github.com/citation-style-language/styles.
Abb. 26.5 Ein Screenshot von PDF-Dokumenten mit der yart-Vorlage
490
26 RMarkdown
26.8 Format-Vorlagen für RMarkdown
491
dungen/Tabellen referenzieren, nahtlose TeX-Einbindung oder Gliederungen einbinden.
RStudio bringt einige Vorlagen mit (File > New File > R Markdown > From Template).
Für Manuskripte von Fachartikeln im Stil der American Psychological Assocation
(APA) ist vor allem die Vorlage papaja aus dem gleichnamigen R-Paket zu nennen (Aust
und Barth 2017). Diese Vorlage ist mächtig und erzeugt ansprechende Dokumente getreu den minutiösen Vorgaben der APA. papaja ist über CRAN zu beziehen. papaja
übernimmt auch das Formatieren von Tabellen und Abbildungen in APA-Form. Es gibt
Einführungen zu papaja im Internet.6 Weitere Rmd-Vorlagen für andere Dokumenttypen
finden sich im Internet. Für standardisierte Berichte wie Seminar- oder Abschlussarbeiten
kann das Paket yart nützlich sein, obgleich deutlich weniger ausgebaut und noch in der
Entwicklungsphase.7 Es bietet eine Vorlage mit typischen Steuerungsmöglichkeiten wie
individuelle Titelseite und Einbindung eines Logos. Abb. 26.5 wirft einen Blick auf die
„yart-Syntax“ und das ausgegebene PDF auf Basis dieser Vorlage. Einen Blick verdient
auch das Paket komaletter, mit dem man Briefe (z. B. Geschäftskorrespondenz) auf
Basis von Markdown schreiben kann.
Aufgaben
Richtig oder falsch?8
1.
Schreibt man Dokumente in Markdown, so können diese in andere MarkupFormate wie HTML oder Latex übersetzt werden; RStudio stellt diese Funktionalität via Pandoc zur Verfügung. (In dieser und in den folgenden Fragen ist
stets die Markdown-Version von RStudio gemeint.)
2. RMarkdown-Dokumente sind keine Textdateien.
3. Pandoc ist Software, mit der man Markdown-Texte in andere Markup-Sprachen
übersetzen kann.
4. Das R-Paket knitr „strickt“ Markdown und andere Auszeichnungssprachen
in ein Dokument zusammen.
5. Markdown-Dateien dürfen nicht mit einem YAML-Teil beginnen.
6. Möchte man eine Zeile als Überschrift der Ebene 1 formatieren, so könnte man
schreiben „#Überschrift“.
7. Möchte man R-Syntax in eine Markdown-Datei integrieren, so kennzeichnet
man den Beginn eines R-Blocks mit ”’frg.
8. Auszeichnungen dieser Art „[WeisOis2017]“ kennzeichnen eine Zitation in
Markdown.
9. Im Moment unterstützt Markdown nur einen Zitationsstil.
10. Die Markdown-Vorlage papaja ist nützlich zur Gestaltung von optisch aufwändigen Briefen.
6
https://crsh.github.io/papaja_man/introduction.html.
https://github.com/sebastiansauer/yart.
8
R, F, R, F, F, R, R, F, F, F.
7
Teil IX
Rahmen 2
27
Projektmanagement am Beispiel
einer Fallstudie
Modellieren
Einlesen
Aufbereiten
Kommunizieren
Visualisieren
Rahmen
Lernziele
Eine Definition von Populismus berichten und diskutieren können
Die Eckpunkte eines größeren Datenanalyse-Projekts kennen
Grundlegende Methoden des Projektmanagements größerer DatenanalyseProjekte kennen
Grundlagen der Versionierung mit Git erläutern können
In diesem Kapitel untersuchen wir ein größeres Analyseprojekt aus der Vogelperspektive. Genauer gesagt handelt es sich um eine Fallstudie zur Frage, wie populistisch deutsche
Politiker tweeten. Anders als in den meisten anderen Kapiteln geht es hier nicht um Syntax
und Details der Analyse. Stattdessen sollen hier Aspekte des Projektmanagements größerer R-Projekte zu Sprache kommen. Darüber hinaus betrachten wir einige theoretische
Inhalte; schließlich braucht man für Datenanalyse nicht nur Computer- und Mathewissen,
sondern auch inhaltliches Wissen.
Fragt man nach dem Populismus deutscher Politiker, wie er sich in deren Tweets manifestiert, so muss man zwangsläufig einige Annahmen treffen. Anders gesagt, man baut auf
gewissen Theorien auf. Die wichtigste Frage ist sicherlich, was mit „Populismus“ gemeint
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_27
495
496
27 Projektmanagement am Beispiel einer Fallstudie
ist? Um diese Frage zu beantworten, berufen wir uns auf Poppers Theorie der „Urhorde“
– der Idee, dass die menschliche Zivilisation auf patriarchalischen, auf Verwandtschaft
basierenden Stämmen, die lokal abgegrenzte Territorien besiedelten, zurückgeht (Popper
1972).
27.1
Was ist Populismus?
Was kennzeichnet eine Urhorde aus Poppers Sicht? Eine solche Gesellschaft ist hierarchisch und patriarchalisch gegliedert. Wesentlich ist die Idee, dass es richtige und einfache
Antworten auf zentrale Fragen der Welt gibt. Aus der Gegebenheit der Antworten wird
ein Verbot des Hinterfragens gefolgert, was der Zementierung sozialer Machtstrukturen
dient. Um Zentrifugalkräften im Innern weiter Einhalt zu gebieten, spielen Feinde im Äußeren eine große Rolle; ein Narrativ des „wir gegen die“ ist gängig. Ähnlich und daraus
abgeleitet sind hochgehandelte Werte wie Kampfesmut, Ehre, Gehorsamkeit, Eintreten
für Gruppenmitglieder (und Nicht-Eintreten für Nicht-Mitglieder), Beharren auf Althergebrachtem, Widerwille gegenüber Veränderung, Hochschätzung des universalistischen
Weltbildes sowie generell eines nicht- oder antirationalen, mystisch verklärten Weltbilds.
Geringgeschätzt werden die Gegenentwürfe: Rationalität, Hinterfragen, Kritik, Zweifeln,
die Methode des Versuchens und Verwerfens, das Aushandeln des „richtigen“ Weges,
Kompromissbereitschaft, Toleranz gegenüber widerstreitenden Sichtweisen. Popper sieht
die Entwicklung der Menschheit als eine Entwicklungstrajektorie fort von dieser Urhorde
oder geschlossenen Gesellschaft hin zu einer offenen oder offeneren Gesellschaft. Allerdings ist die Entwicklung von Rückschritten bedroht; faktisch verläuft sie keineswegs
immer nur geradewegs in Richtung einer offenen Gesellschaft.
Populismus ist vor dieser Bühne nichts anderes als das Auftreten von Protagonisten –
mit oder ohne Maske, um beim Bild der Bühne zu bleiben – die zurück zur alten Welt
der Urhorde hin rufen. Diesen Rufern zum Gestern kommt zu passe, dass sie mit ihrem
Programm einen Nerv treffen. Richtig ist, dass Menschen unter der Unsicherheit der offenen Gesellschaft leiden: Das Unbehagen in der Kultur. In einer offenen Gesellschaft ist
niemand mehr da, der die sichere Antwort weiß. Keiner da, der garantieren kann, dass
am Ende alles gut ausgeht. Es ist nicht mal sicher, ob „wir“ die „Guten“ sind. Was in
der Welt der Urhorde ein unverrückbarer Felsblock der Sicherheit war, erscheint in der
offenen Gesellschaft mitunter als Rauchschleier, die man nicht mit den Händen fassen
kann. Gerade zu Zeiten von Krisen in der offenen Gesellschaft oder zu Zeiten, da es besonders viel Unsicherheit gibt, steigt die Angst vor und das Unbehagen in der modernen
Gesellschaft. Solche Zeiten bedeuten Rückenwind in die Segel der Populisten. Allerdings
ist der Weg zurück zur Urhorde zwar nicht praktisch verstellt, aber humanistisch ungangbar: Die Urhorde ist eine zutiefst antihumane Gesellschaft. Die geschlossene Gesellschaft
ist eine Gesellschaft, in der das Wohl des Einzelnen nicht gilt. Totalitär, der Willkür der
Herrschenden werden keine Grenzen gesetzt. Die Urhorde ist damit das Gegenteil der
27.2 Forschungsfrage und Operationalisierung
497
modernen, offenen Gesellschaft. Sicherlich hat Popper diese Ideen in einer Zeit geschrieben, in der die Tage vollends vom „Höllensturz“ Europas (Kershaw 2015) überschattet
waren. Da aber auch heute die gleichen Ursachen zu den gleichen Wirkungen führen, ist
die Gefahr des Populismus nicht gering zu schätzen.
27.2
Forschungsfrage und Operationalisierung
Vor dieser Theorie untersuchen wir die Frage, wie sich deutsche Politiker in Indikatoren des Populismus unterscheiden. Natürlich ist es nicht möglich, Populismus direkt zu
messen; wir müssen uns mit Indikatoren zufriedengeben. In dieser Fallstudie wurde Populismus anhand acht Indikatoren, teilweise zusammenhängend, operationalisiert.
1. Wortkürze: Wortkürze wird als Indikator für das Bevorzugen einfacher Erklärungen
hergenommen; einfache Erklärungen, die sich in Wörtern niederschlagen wie „ja“,
„so“, „machen“. Differenzierte Darstellungen erfordern tendenziell längere Wörter.
2. Verhältnis von negativ zu positiv gefärbten Wörtern: Ein hoher Wert in diesem Indikator deutet auf Inhalte des „Gegeneinanders“ hin und stellt insofern ein Schlaglicht auf
Populismus dar.
3. Anteil negativ gefärbter Wörter zur Gesamtwortzahl: Unabhängig von der Häufigkeit
positiv konnotierter Wörter wird hier untersucht, wie stark Themen der Gegeneinanders und der Destruktivität eine Rolle spielen.
4. Anteil emotionaler Wörter: Hier wird nach dem Anteil von sowohl positiv als auch
negativ konnotierten Wörtern an der Gesamtwortzahl gefragt. Damit steht die Höhe
der Emotionalität als solche im Blick. Hohe Emotionalität wird als Gegenpol von Rationalität verstanden; damit ist Emotionalität ein Ausdruck von Populismus.
5. Grad an Negativität: Hierfür werden die Emotionswerte von Textstücken aufaddiert,
nicht die Anzahl der negativen Wörter. Die Aussage ist ähnlich wie die Anzahl der
negativ gefärbten Wörter eine Aussage zu Narrativen der Destruktivität und damit des
Populismus, allerdings sind in diesem Indikator die Wörter gewichtet nach ihrer Negativität.
6. Grad der Emotionalität: Wie stark ist der Emotionswert eines Textstücks? Hierfür werden die Emotionswerte der Texteinheiten aufaddiert. Die Idee hierbei ist, dass geringe
Emotionalität ein Indikator für rationales Argumentieren ist. Nicht-rationales, emotionales Sprechen wird demgegenüber als Indikator für Populismus aufgefasst.
7. Der Anteil der Wörter in GROSSBUCHSTABEN: Wörter in Großbuchstaben werden
als schriftliches Pendant zum Schreien gesehen. Schreien ist im Wesentlichen keine
Form der rationalen Auseinandersetzung, sondern eine irrationale, und damit dem Formenkreis des Populismus zuzuordnen.
8. Methode der Ursachenattribuierung: Wird das Wesen der Menschen oder deren Verhalten zur Ursachenattribuierung herangezogen? „Er ist gut“ ist ein Versuch, die un-
498
27 Projektmanagement am Beispiel einer Fallstudie
veränderliche Essenz einer Person zu beschreiben. Im Gegensatz dazu beschreibt die
Formulierung „Sie macht das gut“ änderbares Verhalten.
Diese Indikatoren sind sicherlich fehlerbehaftet. Die Hoffnung ist, dass sie weniger fehlerbehaftet sind als bloßes Raten; dass sie also Information enthalten. Einige Schwächen,
die die Auswahl dieser Indikatoren beinhaltet, ist, dass die Nicht-Exaktheit der zugrunde
liegenden Theorie andere Indikatoren ebenfalls erlauben würde. Außerdem ist unplausibel, dass alle Indikatoren die gleiche Wichtigkeit für Populismus innehaben. Durch das
Ausmitteln der Werte wird ihnen aber faktisch die gleiche Wichtigkeit zugeordnet. In ähnlicher Weise impliziert das Mitteln, dass die Indikatoren ein metrisches Niveau aufweisen;
nur dann kann die Operation des Berechnens des Mittelwerts bedeutungsvoll angewendet
werden. Allerdings ist der metrische Gehalt der Variablen nicht gesichert.
Ein anderer Kritikpunkt betrifft potenziell den unterschiedlichen Wertebereich der Variablen. Mittelt man zwei Variablen X und Y, von denen X einen großen Wertebereich und
Y einen kleinen Wertebereich aufweist, so wird der resultierende Mittelwert maßgeblich
von X, aber nicht von Y bestimmt sein. Allein die Skalierung, die letztlich willkürlich
ist, da durch Wahl der Bezugseinheit festlegbar, würde das Einflussgewicht einer Variable
bestimmen. Vor diesem Hintergrund müssen in solchen Situationen Variablen mit unterschiedlichen Wertebereichen standardisiert werden. Das wurde hier getan; es wurde eine
z-Transformierung (s. Abschn. 9.3.3) durchgeführt.
27.3
Emotionslexikon
Ein wesentlicher Aspekt der Analyse betrifft den Abgleich mit einem Emotionslexikon.
Ein Ausschnitt aus diesem Lexikon ist nützlich, um das Vorgehen besser einschätzen zu
können. Tab. 27.1 stellt einen Ausschnitt aus dem Lexikon dar (Remus et al. 2010). Der
Emotionswert einer Texteinheit ist nichts anderes als die Summe aller Wörter der Texteinheit, die im Emotionslexikon gefunden werden. Positive und negative Wörter gleichen
sich aus. Absolutwerte der Emotionswerte würden stattdessen den Grad der GesamtEmotionalität anzeigen.
Tab. 27.1 Ausschnitt aus dem
SentiWS-Emotionslexikon
neg_pos
neg
neg
neg
pos
pos
pos
word
Gefahr
Schuld
unnötig
Lob
gelungen
perfekt
value
1:000
0:969
0:946
0.725
1.000
0.730
27.5 Prozess der Datenanalyse
27.4
499
Daten, Stichprobe und Analysekontext
Als Beobachtungseinheit wird eine Person, ein deutscher Politiker, herangezogen. Für
jeden Indikatorwert wird dabei der Median über alle seine Tweets berechnet.
Eine Liste von k D 199 deutschen Politikern sowie ihrer Parteizugehörigkeit zum
Stand August 2017 findet sich im Paket pradadata; diese Liste wurde zum großen Teil
aus der Liste der Deutschen Welle (2017) bezogen. Da in dieser Liste aus unbekannten
Gründen aber kaum Politiker der AfD und der FDP vertreten waren, wurden alle Mitglieder dieser beiden Parteien, sofern Sie Twitter-Accounts besaßen, mit aufgenommen.1
Es wurden nur persönliche Accounts, also keine Gruppen-Accounts wie spdde aufgenommen. Aus datenrechtlichen Gründen dürfen nur dehydrierte Tweets, also nur die IDs
der Tweets, bereitgestellt werden. Diese Daten sowie die Syntax sind in einem GithubRepositorium öffentlich zugänglich.2
Insgesamt umfasst das Datenmaterial etwa 400000 Tweets mit insgesamt etwa 6.3
Millionen Wörtern. Sieben deutsche Parteien wurden berücksichtigt (AfD, CDU, CSU,
FDP, Grüne, Linke, SPD) sowie eine fraktionslose Person. Zu Vergleichszwecken wurde
Donald Trump hinzugenommen. Die Daten wurden im August 2017 gesammelt und ausgewertet. Die meisten Accounts fanden sich bei der SPD (s. Abb. 27.1, links). Am meisten
twitterte Donald Trump, gefolgt von einer einzelnen Person, die die Gruppe der Fraktionslosen ausmachte und nicht weiter berücksichtigt wurde. Danach folgen die Grünen
(s. Abb. 27.1, rechts); am wenigsten twittern die Politiker der CSU. Um Vergleichbarkeit
zu gewährleisten, ist hier die mediane Zahl an Tweets pro Tag dargestellt. Die Abfolge zur Berechnung der Populismuswerte kann wie folgt beschrieben werden (vgl. auch
Abb. 27.2):
1.
2.
3.
4.
5.
Berechne für jeden Tweet jeden Indikatorwert.
Aggregiere die Indikatorwerte der Tweets pro Person (Median).
Berechne die z-Werte für jeden Indikatorwert jeder Person.
Aggregiere die Indikatorwerte jeder Person zum Populismuswert der Person (Median).
Aggregiere die Indikatorwerte und den Populismuswert der Personen auf Parteiebene
(Median).
27.5
Prozess der Datenanalyse
Grob gesagt, bestand die Datenanalyse aus sieben Schritten:
1. Erstellung eines Entwicklerkontos bei Twitter
2. Identifikation der Nutzernamen (Politiker mit Twitter-Account)
1
2
Markus Söder hat es auch noch auf diese Liste geschafft.
https://github.com/sebastiansauer/polits_tweet_mining.
0
20
40
Anzahl Accounts
csu
fraktionslos
cdu
spd
afd
linke
gruene
fraktionslos
trump
fdp
trump
spd
linke
gruene
fraktionslos
fdp
csu
cdu
afd
party
Partei
trump
afd
csu
linke
gruene
fdp
cdu
Abb. 27.1 Anzahl von Accounts und Tweets pro Tag
Partei
spd
2.5
5.0
7.5
10.0
Mediane Anzahl an Tweets pro Tag
0.0
trump
spd
linke
gruene
fraktionslos
fdp
csu
cdu
afd
party
500
27 Projektmanagement am Beispiel einer Fallstudie
501
pe
rs
on
s
27.6 Zentrale Ergebnisse
id
id
id
tweet
tweet
tweet
Tweets pro Person
Person party I1 I2 I3
Md
Party
1
A
A
2
A
B
3
B
Populismusindikatoren (I1, I2,...)
pro Person (z-Werte)
I1
I2 I3
Md
Populismusindikatoren (I1, I2,...)
pro Partei (z-Werte)
Abb. 27.2 Berechnung der Populismuswerte
3.
4.
5.
6.
7.
Herunterladen der Tweets
Aufbereiten der Tweets (Entfernen von Stopwörtern und Umformen zur Normalform)
Berechnung von Tweet-Häufigkeiten
Berechnung der Indikatoren und des Populismuswertes
Visualisierung der Ergebnisse
27.6
Zentrale Ergebnisse
Die Idee, Trump als Referenzwert für Populismus zu referenzieren, findet sich in den Daten gut bestätigt: Sein Populismuswert ist mit Abstand der höchste (s. Abb. 27.3, links).
Ohne Trump wird die Liste von der AfD, gefolgt von der Linken, angeführt. Interessanterweise sind diese beiden Parteien auch am homogensten, was den Populismus betrifft; die
Streuung dieser Parteien ist am geringsten. Die geringsten Populismuswerte finden sich
bei den Volksparteien CDU, SPD und CSU.
Informationsreicher ist die Darstellung der (relativen) Ausprägungen der acht einzelnen Indikatoren (s. Abb. 27.4). Einige Aspekte fallen ins Auge; so sind die „Zitterkurven“
von CDU und CSU ähnlich. Ein zweiter Blick bescheinigt, dass auch SPD und FDP den
Unionsparteien ähneln. Eine zweite Korrespondenz findet sich auch bei den Kurven der
Grünen und der Linken. Nur die AfD hat ein uniques Muster. Zu beachten ist, dass die farbigen großen Punkte die Mediane pro Indikator pro Partei darstellen. Die kleineren grauen
Punkte stellen die Werte einzelner Politiker dar. Betrachtet man die grauen Punkte, so stellt
man fest, dass alle Parteien eine gewisse Streuung ihrer Politiker für die Indikatoren aufweisen; vielleicht die AfD wiederum am meisten. Die Syntax dieser Abbildung, da etwas
komplexer und daher interessanter, sei hier herausgegriffen.3
3
Quelle: https://github.com/sebastiansauer/polits_tweet_mining/blob/master/code/05_analyses_
03_populism_score.Rmd.
27 Projektmanagement am Beispiel einer Fallstudie
Abb. 27.3 Populismuswerte der Parteien
502
27.6 Zentrale Ergebnisse
503
AfD
CDU
CSU
FDP
B90/Grüne
Die Linke
−1 0
−1 0
SPD
Wortkürze
Verhältnis negativer/positiver Wörter
Indikator
Anteil negativer Wörter
Anteil emotionaler Wörter
Wert an negativer Emotion
Wert an Emotionalität
Anteil GROSSBUCHSTABEN
Methode der Ursachenattribuierung
−1 0
1
−1 0
1
−1 0
1
−1 0
1
1
1
−1 0
1
z−Wert
dargestellt sind Mediane über alle Tweets aller Politiker
Abb. 27.4 Populismus-Indikatoren deutscher Parteien
polits_df_long %>%
ggplot +
aes(x = Indikator, y = z_Wert,) +
geom_point(color = "grey80", position = "jitter") +
facet_wrap(~party, nrow = 1) +
scale_color_manual(values = party_pal) +
scale_x_discrete(labels = pop_vars_de) +
scale_y_continuous(limits = c(-1.5,1.5), breaks = c(-1,0,1)) +
theme(legend.position = "none") +
coord_flip() +
labs(caption =
"dargestellt sind Mediane über alle Tweets aller Politiker",
y = "*z*-Wert",
color = "Partei") +
geom_point(data = party_pop_scores_md_long, aes(color = party), size = 4) +
geom_line(data = party_pop_scores_md_long, aes(color = party), group = 1)
polits_df_long ist ein Dataframe in Langform, der für jeden Politiker den z-Wert
jedes Indikators wiedergibt; außerdem noch die Partei jedes Politikers. Zuerst wird für
diese Detaildaten (198 Politiker à 8 Indikatoren) jeweils ein hellgrauer Punkt geplottet
(geom_point()), in „verwackelter“ Form (jittered). Die Ergebnisse werden dann in Facetten (Teilbilder) entsprechend den Parteien aufgegliedert (facet_wrap()). Die Farbpalette wurde vorab anhand der für eine Partei üblichen Farbe definiert (party_pal) und
die Farbzuordnung manuell anhand dieses Farbschemas vorgenommen (scale_color_
504
27 Projektmanagement am Beispiel einer Fallstudie
manual()). Für die Indikatoren wurden nicht die Variablennamen, sondern optisch ansprechendere Bezeichnungen in Textvariablen gespeichert (labels = pop_vars_de).
Die Grenzwerte der z-Wert-Achse und die Achsenwerte wurden expliziert (scale_y_
continuous()). Neben einigen anderen Details ist ein wesentlicher Punkt, dass ein
weiterer Datensatz herangezogen wird (party_pop_scores_md_long), der die Mediane pro Indikator pro Partei enthält. Mit diesen Daten werden dann die großen farbigen
Punkte und Linien geplottet. Das Diagramm gewinnt gerade durch die Kontextualisierung
der Medianwerte (in bunten Farben) vor dem Hintergrund der grauen Punkte. Die „Zitterkurve“ hilft dem Auge, eine Konfiguration der Punkte pro Partei zu sehen.
27.7
27.7.1
Projektmanagement
Gliederung eines Projektverzeichnisses
Der Gliederung Ihres Projektverzeichnisses kommt großer Bedeutung zu. Ein OnlineProjektordner, auf den mehrere Menschen zugreifen, wird auch als Repositorium bezeichnet. Im Repositorium zum Projekt polits_tweet_mining4 finden sich folgende
Ordner.
1. data-raw: Ein Ordner, der die Rohdaten fasst. Eine Grundregel lautet, dass in diesem Ordner nicht geschrieben, sondern nur gelesen wird. Im Github-Repositorium ist
der Ordner aus rechtlichen Gründen nicht enthalten. Allerdings ist das auch ein Problem: Ohne die Daten ist es nicht möglich, die Analyse nachzuvollziehen. Hier ist es so
gelöst, dass die IDs der Tweets bereitgestellt sind, die mittels der Twitter-API wieder
„hydriert“ werden können. Sofern keine rechtlichen Fragen dagegensprechen, sollten
die Daten im Repositorium vorhanden sein, um die Reproduzierbarkeit zu gewährleisten: Ohne Daten ist die Analyse nicht reproduzierbar.
2. data: Hier finden sich die aufbereiteten Daten und zusätzliche Daten wie das Emotionslexikon u. Ä.
3. code: Dieser Ordner umfasst die einzelnen Schritte der Datenanalyse (s. Abb. 27.2);
so ist eine sinnvolle Gliederung innerhalb dieses Ordners etwa 01_parse_data.Rmd,
02_prepare_data.Rmd, 03_visualize.Rmd, 04_build_models_01.Rmd
und so weiter. Hier beinhalten die einzelnen Syntax-Dateien jeweils einen wesentlichen Schritt im Rahmen der Analyse und sind gemäß ihrer Abfolge nummeriert.
4. img: Diagramme (images) finden sich in diesem Ordner. Zumeist wurden die Bilder als PDF abgespeichert, so dass die Diagramme als Vektorgrafiken zur Verfügung
stehen. Da aber solche Diagramme z. B. im Webbrowser häufig nicht reibungslos abgebildet werden können, sind auch PNG-Versionen der Diagramme vorhanden. Der
Name der Dateien entspricht ihrem R-Namen; das erleichtert die Zuordnung.
4
https://github.com/sebastiansauer/polits_tweet_mining.
27.7 Projektmanagement
505
5. talk_polit_twitter: Hier finden sich Medien zur Ergebniskommunikation; in
diesem Fall sind es Folien für eine Fachkonferenz.
6. div: Die Restefalle; ein Ordner für Übriges.
Außerdem findet sich eine Datei LICENSE, die die Verwendungsrechte dieses Repositoriums (MIT-Lizenz) expliziert. Die Readme.md gibt einen Überblick über das Projekt
und dient als erste Anlaufstelle, wenn man sich über das vorliegende Projekt informieren möchte. Die bib-Datei beinhaltet die Zitationen der verwendeten R-Pakete, und die
Rproj-Datei regelt die projektbezogenen Einstellungen von RStudio, das für dieses Projekt verwendet wurde. .gitignore regelt, welche Dateien nicht bei der Versionskontrolle überwacht werden sollen.
27.7.2
Faustregeln zur Struktur eines Projekts
Die genannte Ordnergliederung ist ein Beispiel, wie ein größeres Analyseprojekt aufgebaut sein kann; andere Gliederungen sind möglich. Grundsätzlich ist eine Orientierung
entlang der Arbeitsschritte sinnvoll (vgl. Abb. 1.1). Wenn auch verschiedene Strukturen
möglich sind, so sollte man doch bei einer einmal gewählten Struktur bleiben und seine
Projektordner gleich oder ähnlich aufbauen.
Funktionen. Neben einzelnen Quelldateien entsprechend den Schritten der Analyse
kann es sinnvoll sein, Funktionen in eine eigene Datei, etwa funs.R, auszugliedern.
Das bietet sich v. a. dann an, wenn es sich um generelle Funktionen handelt, die nicht
einem bestimmten Schritt in der Datenanalyse zugeordnet sind. Sonst ist die jeweilige
Funktion besser dort aufgehoben, wo sie Verwendung findet. Generell ist es von zentraler
Bedeutung, die Syntax zu parzellieren und in einzelne, in sich abgeschlossene Pakete –
die Funktionen – zu packen. Eine Funktion sollte nur eine Sache erledigen; die aber gut.
Für jede Funktion sollte klar sein, was Eingabe und was Ausgabe ist.
Notizbücher. Es hat Vorteile, statt reiner R-Skripte notizbuchähnliche Dateien zu erstellen, die sowohl die Syntax als auch Ergebnisse wie Diagramme und Tabellen enthalten
– und Erläuterungen, Kommentare, Gedanken; Text also. Rmd-Dateien sind solche Notizbücher (s. Kap. 26).
Makefile. Was häufig sinnvoll ist, ist eine makefile-Datei; ruft man diese Datei auf,
so wird die ganze Analyse oder ein gewählter zentraler Haupt-Schritt ausgeführt. Eine Makefile-Datei kann eine R-Datei sein, muss aber nicht; GNU Make ist ein übliches
Tool in der Unix-Welt. Wenn nicht alle Schritte in R ablaufen, sondern verschiedene Programme benutzt werden, kann Make ein guter „Dirigent“ sein. Etwas vereinfacht, könnte
ein Makefile so aussehen:
# Einige Erläuterung und Hinweise finden sich in Readme.md
load_data(compute_all = TRUE)
prepare_data()
train_models(all = TRUE)
506
27 Projektmanagement am Beispiel einer Fallstudie
test_models(all = TRUE)
plot_figs(format = "PDF")
print_tables(format = "LateX")
Hat man eine zeitaufwändige Berechnung durchgeführt, so bietet es sich an, deren Ausgabe in einer Datei zu speichern, so dass man zum Weiterarbeiten einfach diese Datei laden
kann. Das spart Zeit, da nicht jedes Mal wieder die langwierige Berechnung durchgeführt
werden muss. Es gibt auch Software, wie knitr, die Ergebnisse selbständig im Hintergrund
in eine Datei schreibt, um Zeit zu sparen (Caching).
Es passiert immer wieder, dass man denkt, man „habe fertig“, und doch noch einige Berechnungen wiederholen muss.5 Hat man den Ablauf automatisiert, so ist es ein Leichtes,
auf Änderungen z. B. in den Daten zu reagieren. Das Gegenteil: Gleicht Ihr Projekt einem
Rübenfeld, wird die Arbeit ineffizient. Man ist mehr mit Suchen auf lokalen Lösungen
beschäftigt als mit der eigentlichen Arbeit.
Dokumentation. Pflegt man Notizbücher, so hat man Wesentliches zur Dokumentation
eines Projekts getan. Dokumentation ist wesentlich; man bezweifelt das gerne, wenn man
im Eifer des Gefechts bastelt. Spätestens einige Monate später hat man dann keine Ahnung mehr, was sich hinter dem Objekt p_emo_words_ratio verbirgt. Wohl dem, der
sorgfältig dokumentiert hat, was eine Funktion macht, was sie als Eingabe verlangt und
was sie zurückgibt. Außerdem benötigen die Daten eine Dokumentation, ein Codebuch
(Code Book), in dem für jedes Objekt der Typ (z. B. Integer) und eine Beschreibung des
Objekts („Alter der Versuchspersonen“) aufgeführt ist.
Relative Pfade. Man kann in R relative oder absolute Pfade angeben. Arbeitet man
in einem RStudio-Projekt, so bezieht sich ein relativer Pfad auf das Projektverzeichnis.
Der relative Pfad wird dann als Unterordner relativ zum Arbeitsverzeichnis des Projekts
verstanden. Das Projektverzeichnis ist der Ordner, in dem sich die Datei mit der Endung
Rproj findet; das ist eine Textdatei, die einige Einstellungen Ihres Projekts vorhält.
Wenn Sie in RStudio ein neues Projekt anlegen (z. B. mit File > New Project . . . ), so
werden Sie nach einem Projektordner gefragt. Ein absoluter Pfad gibt den kompletten
Pfad an, beginnend mit dem Wurzelverzeichnis auf Ihrem Rechner (oder einem Server).
Relative Pfade sind viel praktischer, da leichter portabel. Möchten Sie mir Ihr Projekt
übergeben, so werden die relativen Pfade auch bei mir funktionieren (sofern Sie die komplette Ordnerstruktur des Projekts weitergeben). Absolute Pfade werden bei mir nicht
funktionieren. Ein Beispiel für einen relativen Pfad wäre code/prepare_data.Rmd:
relativ zum Arbeitsverzeichnis wird das Unterverzeichnis code angesprochen, worin
sich die Datei prepare_data.Rmd befindet. Ein absoluter Pfad (MacOS) könnte so
aussehen: ~/Documents/Projects/Fermat_solution/all_solved.Rmd. Bei
5
„Lieber Sebastian, ich habe noch 232 Versuchspersonen entdeckt in meiner Schublade. Könntest
Du bitte die Analyse mit diesen Daten wiederholen? Ach ja, morgen würden wir den Bericht gerne
einreichen“.
27.7 Projektmanagement
507
Unix-artigen Betriebssystem wie dem MacOS bedeutet die Tilde ~ das Stammverzeichnis
(home) des Nutzers. In diesem Stammverzeichnis befindet sich der angegebene Pfad.
Alleine schon weil mein Stammverzeichnis sebastiansauer wohl anders heißen wird
als Ihres, wird dieser Pfad bei Ihnen nicht funktionieren. Durch relative Pfade werden
Portabilität und damit Reproduzierbarkeit Ihrer Arbeit sichergestellt.
Objektlandschaft im Zaum halten. Im Abschn. 5.4 haben wir einige Grundregeln für
die Benennung von Objekten betrachtet; je größer das Projekt, desto mehr Objekte und
desto wichtiger deren Benennung. Ein anderer Punkt ist, dass ein Wildwuchs von Objekten in der Arbeitsumgebung (globale Umgebung, Global Environment) vermieden werden
sollte – sonst verliert man früher oder später den Durchblick. Besser ist es, nebensächliche
Objekte wie Zwischenergebnisse in Funktionen zu packen, so dass sie die Arbeitsumgebung nicht zuwuchern. Hadley Wickham empfiehlt, die globale Umgebung regelmäßig zu
löschen und nicht wiederherzustellen, wenn Sie RStudio öffnen.
Übergabe von Objekten zwischen Funktionen. Um die Arbeitsumgebung schlank zu
halten, sollten Objekte nach Möglichkeit in Funktionen verschoben werden. Eine Ausnahme stellen Objekte dar, die für einen oder den nächsten Schritt benötigt werden. Solche
Objekte können entweder als Datei gespeichert werden oder auch als Objekt verfügbar
gehalten werden für die nächste Funktion im Arbeitsablauf. Man kann Funktionen mit
einem Argument versehen, das prüft, ob das Ausgabeobjekt als Datei gespeichert werden
soll, etwa:
prepare_data <- function(input_df, save_to_disk = FALSE){
# komplizierte Syntax hier
if (save_to_disk = TRUE) save(ausgabe_objekt,
file = "data/data_prepared.Rda")
return(ausgabe_objekt)
}
Hier ist für save_to_disk die Voreinstellung FALSE gewählt. Entsprechend kann man
der in der Abfolge nächsten Funktion einen Schalter einbauen, der abfragt, ob mit dem
Objekt aus der Datei oder aus der Arbeitsumgebung weitergearbeitet werden soll:
train_models(input_df, read_from_disk = FALSE, save_to_disk = FALSE){
if (read_from_disk = TRUE) read("data/data_prepared.Rmd")
# berechne wilde Modelle
return(models)
}
Übergabe von Objekten zwischen Dateien. Die Abfolge der Funktionen wird häufig unterbrochen durch die Grenze zwischen Dateien, wie load_data.R und prepare_data.R
(s. Abb. 27.5). Das ist auch gut so. Genau wie Funktionen zur Gliederung von Syntax beitragen, tragen auch einzeln Dateien zur Gliederung von Funktionen bei. Das entspricht
der Gliederung eines Buches, die auch (oft) mehrere Ebenen aufweist. Im Prinzip gilt bei
508
27 Projektmanagement am Beispiel einer Fallstudie
Abb. 27.5 Übergabe von
Datenobjekten zwischen Funktionen und zwischen Dateien
Funktion 1
Funktion 2
prepare_data.R
der Abfolge von Skript-Dateien die gleiche Regel wie bei der Übergabe von Objekten bei
der Abfolge von Funktionen innerhalb einer Skript-Datei. Allerdings ist es häufig sinnvoll, das oder die Ausgabeobjekte der letzten Funktion einer Datei als Datei zu speichern.
Die erste Funktion der nächsten Skriptdatei lädt dieses Objekt aus der Datei. Das hat den
praktischen Hintergrund, dass man bei größeren Projekten gerade z. B. in das Thema Modellieren eingedacht ist und es dann praktischer ist, sich nicht noch in die Funktionen des
Aufbereitens eindenken zu müssen. Praktischer ist es, wenn Daten in einer Datei vorliegen, die man für die erste Funktion im neuen Abschnitt lädt.
27.7.3
Versionierung mit Git
bericht_0304_v02a_sauer_final_v02_korrigiert_final2_a.doc – Haben Sie schon mal eine Datei per E-Mail gesendet (bekommen), die so hieß? Ich kenne
jemanden, der jemanden kennt, dem das passiert ist. Ist man selber Autor der Datei, so
mag man sich nach einer konsistenteren Art der Versionierung sehnen. Eine einfache
Lösung wäre- den Dateinamen mit einem Suffix der Versionsnummer zu versehen, etwa
bericht_v04; das bezeichnet die vierte Überarbeitung dieser Datei. Bei jeder Bearbeitung (durch jede Person) wird die Versionsnummer um eins erhöht. Das Datum zu wählen,
ist nicht optimal, da das Betriebssystem dieses Metadatum ohnehin zur Verfügung stellt.
Standen Sie, wie ich, auch schon vor dieser Situation: Sie haben einen Bericht geschrieben, der an eine Reihe von Kollegen gehen soll, die noch einen Teil beitragen, z. B.
Korrektur lesen? Auf! Sie senden den Bericht an fünf geschätzte Kollegen. Was passiert?
Zwei Kollegen haben sich nach einem Monat noch nicht gemeldet; einer der beiden entschuldigt sich, dass sein Hund den Computer angefressen habe und er sich erst gegen
Weihnachten melden könne. Einer sendet am nächsten Tag gleich seine Version zurück
und die Woche darauf noch eine. Der vierte bzw. der fünfte Kollege schickt nach einer
bzw. nach zwei Wochen eine Version. Sie müssen jetzt diese Versionen zusammenführen.
Viel Spaß. E-Mails mit Dokumenten durch den Äther zu schicken, ist alles andere als ideal. Besser ist da schon, wenn der Bericht auf dem Server liegt und die Kollegen auf das
gleiche Dokument auf dem Server zugreifen. Dann gibt es keine verschiedenen Versionen,
die man „verheiraten“ muss (zumindest in der Theorie).
Hat man aber ein Projekt, welches eine längere Schreibphase mit vielen Änderungen
beinhaltet, so ist es wichtig, einzelne Veränderungen nachvollziehen zu können. Gerade
27.7 Projektmanagement
509
in der Software-Entwicklung ist das von zentraler Bedeutung. Datenanalyse kann man als
eine Art Software-Entwicklung betrachten. Größere Analysen beinhalten manchmal ein
paar Tausend Zeilen oder mehr an Code. Änderungen nachzuverfolgen, gewinnt an Bedeutung. Da kommt man mit Werkzeugen aus dem Microsoft-Office-Umfeld schnell an
Grenzen. Ein typisches Werkzeug zur Versionierung heißt git6 . Git ist eine Software, die
den Stand eines Projektes (Ordners) jeweils zu einem Zeitpunkt „fotografiert“, sich also
den jeweiligen Stand merkt. Dieses Fotografieren nennt man auch Comitten. Committet
man mehrere Male, kann man die Änderung von Commit zu Commit vergleichen – und
gegebenenfalls rückgängig machen. Auch die Zusammenarbeit mit mehreren Kollegen ist
problemlos möglich; wenn allerdings mehrere Menschen gleichzeitig eine Datei ändern,
kann es haarig werden. Sie schreiben: „Die Antwort lautet 42“; ich schreibe zur gleichen
Zeit: „Die Antwort lautet Krebskanon.“ Wer hat Recht? Auf wessen Meinung soll git vertrauen? Zum Glück ist git recht hart im Nehmen. Solange die Änderungen nicht in der
gleichen Zeile der Datei vorgenommen werden, kommt git nicht durcheinander. Wird allerdings die gleiche Zeile zur gleichen Zeit geändert, so entsteht ein „Konflikt“. git mag da
keine Entscheidung treffen; in dem Fall muss man sich selber für eine Version entscheiden. Praktischerweise zeigt git die verschiedenen Varianten an, so dass Sie sich in Ruhe
entscheiden können („Die Antwort ist „MU““). Der Begriff Zeile macht deutlich, dass git
ein System ist, welches auf Textdateien aufbaut. Die Änderungen in Textdateien werden
verfolgt; zwar speichert git im Prinzip die Änderungen in jeder Datei, aber das Betrachten des Unterschieds zweier aufeinanderfolgender Commits ist bei git nur mit Textdateien
möglich.
Github7 ist ein Anbieter, der Server für Projekte (Ordner) mit git zur Verfügung stellt.
Man legt dort einen Projektordner (ein sog. Repositorium) an, lädt Kollegen ein und arbeitet im Team fleißig an dem Projekt. Über das Github-Repositorium werden die Dateien
bzw. die Änderungen an alle Kollegen verteilt. Man zieht (pull) sich den aktuellen Stand
vom Repositorium herunter auf seinen lokalen Rechner oder drückt (push) seine Änderungen vom eigenen Rechner in das Github-Repositorium.
RStudio bietet eine Integration von Git und Github an, so dass man komfortabel seine
Änderungen nachverfolgen kann. Für viele Situationen reicht ein Klick auf „Commit“ und
„Push“. Allerdings darf nicht verschwiegen werden, dass es auch kompliziertere Situationen gibt, in denen eine Reihe von Befehlen in der Kommandozeile nötig werden kann.
Wirft man einen Blick in das git-Handbuch8 (Chacon und Straub 2014), so macht alleine
der Umfang schon klar, dass dieses Programm mächtig ist – und es Aufwand bedeutet,
dass Programm in der Tiefe zu verstehen.9
6
https://de.wikipedia.org/wiki/Git.
https://github.com/; https://de.wikipedia.org/wiki/GitHub.
8
https://git-scm.com/book/de/v1.
9
https://xkcd.com/1319/.
7
510
27 Projektmanagement am Beispiel einer Fallstudie
Aufgaben
Richtig oder falsch?10
1.
Der Grad der Emotionalität eines Textes wird in der Studie dieses Kapitels als
ein Indikator für Populismus definiert.
2. Poppers Buch „Logik der Forschung“ skizziert eine Theorie der Urhorde, auf
die sich die hier vorliegende Konzeption von Populismus bezieht.
3. Für jeden Populismus-Indikator wurde pro Person der Mittelwert berechnet.
4. Um die Indikatoren vergleichbar zu machen, wurden sie auf den Bereich von
null bis eins standardisiert.
5. Ein Projektverzeichnis für Datenanalyse sollte einen Ordner enthalten, der sowohl Rohdaten als auch erzeugte Datenobjekte enthält.
6. Ein Make-File ist eine Datei, die eine Abfolge von Befehlen ausführt, oft, um
ein (Analyse-)Projekt in Gänze auszuführen.
7. Pfade sollten (in R) nie relativ, sondern stets absolut angegeben werden.
8. Das Nachverfolgen von Änderungen mit der Software Git basiert primär auf
Textdateien.
9. RStudio bietet eine Integration zentraler Git-Befehle.
10. Ein Emotionslexikon, im Sinne der vorliegenden Studie, umfasst eine Anzahl
von Wörtern, denen jeweils ein oder mehrere Emotionswerte zugeordnet werden.
10
R, F, F, F, F, R, F, R, R, F.
Programmieren mit R
28
Lernziele
Wissen, wie man in R eine Funktion schreibt
Das Iterationskonzept mit purrr::map() in den Grundzügen verstehen
Grundideen des defensiven Programmierens kennen
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(tidyverse)
library(magrittr)
library(gridExtra)
data(extra, package = "pradadata")
data(Affairs, package = "AER")
tidyverse lädt auch das Paket purrr, das in diesem Kapitel eine zentrale Rolle spielt.
28.1
Funktionen schreiben
Ähnlich wie Objekte (Variablen) Platzhalter für einen oder mehrere Werte sind, sind Funktionen Platzhalter für eine oder mehrere andere Funktionen (der Begriff Befehl wird hier
synonym verwendet). Funktionen werden in R aufgerufen, indem man ihren Namen und
ein Paar von runden Klammern eingibt,1 etwa fun() oder sd(temperatur). Das haben Sie wahrscheinlich schon oft getan. Eine Funktion kann, muss aber nicht, Parameter
1
Beim Durchpfeifen darf man auf die runden Klammern bei Funktionsnamen verzichten, was einen
zwei Anschläge auf der Tastatur einspart, aber die Syntax verschwommener macht.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_28
511
512
28 Programmieren mit R
Abb. 28.1 Funktionen definieren: Die Funktion als Gefäß für
andere Befehle
R
fun
x %>%
subtract(mean(x)) %>%
raise_to_power(2) %>%
sum %>%
divide_by(length(x)) %>%
sqrt
besitzen, mit denen die Funktion „gefüttert“ wird. Eine Funktion liefert ein Ergebnis, ein
Objekt, zurück.
Nehmen wir zur Verdeutlichung an, Sie haben eine Reihe unglaublich schlauer RFunktionen gefunden, die Sie in unglaublich schlauer Art kombinieren, um ein sagenhaftes Ergebnis zu bekommen. Nehmen wir an, Ihre Abfolge bestehe aus fünf bis sechs
Schritten. Jetzt wenden Sie natürlich diese tolle, fünfschrittige Abfolge immer und überall – oder zumindest immer mal wieder – an. Nach einiger Zeit meldet sich eine alte
Krankheit, Sehnenscheidenentzündung, wieder – all diese Tipperei! Mit Excel wäre das
nicht passiert . . . Um Sie vor Schlimmerem als Excel einer Sehnenscheidenentzündung zu
bewahren, gibt es Funktionen, die nichts anderes tun, als eine Reihe von Funktionen zu einem Bündel zu schnüren und mit einem Namen, z. B. fun(), zu versehen (s. Abb. 28.1).
Leider wissen wir nicht, wie Ihre tolle Abfolge aussieht; begnügen wir uns mit dieser hier:
x <- c(1, 2, 3)
x %>%
subtract(mean(x)) %>%
raise_to_power(2) %>%
sum() %>%
divide_by(length(x)) %>%
sqrt()
#> [1] 0.816
Auf Deutsch heißt diese Syntax etwa:
Nimm den Vektor x UND DANN
ziehe von jedem Element von x den Mittelwert von x ab UND DANN
nimm jedes Element hoch 2 (quadriere es) UND DANN
bilde die Summe der Elemente UND DANN
teile durch die Anzahl der Elemente von x UND DANN
ziehe von dieser Zahl die Wurzel.
In Statistik-Lehrbüchern wird diese Abfolge eher so dargestellt:
r
1X
.x x/
N 2
n
28.1 Funktionen schreiben
513
Was auf das Gleiche hinausläuft. Welche Darstellung finden Sie eingängiger? Vermutlich
hängt die Antwort von Ihrer Mathe- und Informatik-Vorerfahrung ab. Persönlich glaube
ich, dass die R-Syntax-Darstellung einfacher ist, da sie von links nach rechts zu lesen ist,
und damit nacheinander. Die Formel muss von innen nach außen und von rechts nach links
gelesen werden. Damit muss sie gleichsam „auf einen Happs“ verstanden werden.
Diese Abfolge wird auch als „Standardabweichung“ bezeichnet; übrigens liefert
sd(x) nicht den gleichen Wert, da dort durch n 1 geteilt wird, hier wurde durch n
geteilt. Wir könnten unsere Abfolge also mit sd2() bezeichnen (sehr kreativ). Um diese
Abfolge in die Form einer Funktion zu gießen, gilt diese Rechtschreibung:
sd2 <- function(eingabevektor){
# hier unsere Rechenschritte, der "Körper" der Funktion
}
Wir erklären, dass sd2() eine Funktion ist, mit einem Parameter (eingabevektor) und
dem „Körper“ (body), wie er zwischen den beiden geschweiften Klammern definiert ist.
In diesem Fall:
sd2 <- function(eingabevektor){
eingabevektor %>%
subtract(mean(eingabevektor)) %>%
raise_to_power(2) %>%
sum() %>%
divide_by(length(eingabevektor)) %>%
sqrt()
}
Natürlich darf der Parameter heißen, wie es uns beliebt; x wäre auch in Ordnung gewesen;
nützlich ist, wenn die Parameter prägnante Namen haben. Schicken wir unsere Funktion
zur Arbeit:
sd2(x)
#> [1] 0.816
Der Aufruf des Namens einer Funktion f führt eine Reihe von anderen Funktionen
aus (nämlich die, die im Körper von f angeführt sind). Die von f aufgerufenen
Funktionen können parametrisiert sein: so können die Werte der Argumente von f
an die aufgerufenen Funktionen weitergegeben werden. Das, was f erledigt, hängt
also von Ihren Parameter(werten) ab.
Die Sehnenscheidenentzündung (und Excel) konnte noch mal abgewendet werden. Interessanterweise gilt bei R: (Fast) alles, was passiert, ist eine Funktion. Auch Operatoren
wie +, <- und [ sind Funktionen. Hört sich unglaubwürdig an; ist aber so. Um Funktionen
mit „komischen“ Namen wie + anzusprechen, schreibt man:
514
28 Programmieren mit R
`+`(1, 1)
#> [1] 2
Normalerweise hätten Sie einfach geschrieben 1 + 1.2 Entsprechend kann man unsere
Funktion sd2() synonym auch so schreiben:
sd2 <- function(eingabevektor){
eingabevektor %>%
`-`(mean(eingabevektor)) %>%
`^`(2) %>%
sum() %>%
`/`(length(eingabevektor)) %>%
sqrt() -> ausgabe
return(ausgabe)
}
sd2_von_x <- sd2(x)
str(sd2_von_x)
#> num 0.816
Der letzte Wert, der berechnet wird, ist der, der von der Funktion zurückgegeben wird. Es
ist guter Stil, explizit anzugeben (mit return()), welches Objekt zurückgegeben wird.
Ein Wermutstropfen zum Schluss: Funktionen von dpylr lassen sich nicht so einfach
in Funktionen packen. Generell gilt, dass Funktionen, die wie dplyr mit nonstandard
evaluation (NSE) arbeiten, leider eine kompliziertere Behandlung verlangen. Dazu später
mehr (s. Kap. 29).
28.2 Wiederholungen
Häufig möchte oder muss man Dinge wiederholen; beim Analysieren von Daten ist es genauso. Allgemeiner gesprochen bedeutet „Wiederholen“ in diesem Zusammenhang, dass
wir ein Objekt mit mehreren Elementen vor uns haben und wir eine Aktion (Funktion)
auf jedes Element anwenden möchten (s. Abb. 28.2); man könnte auch sagen, wir ordnen
(map) jedem ei die Funktion f zu oder wir führen die Funktion f für jedes Element ei
von E aus. Typische Anwendungsfälle von solchen Wiederholungen sind:
1. Für jedes Element einer Spalte soll eine Berechnung durchgeführt werden (z. B. von
Fahrenheit in Celsius umrechnen; s. Abschn. 7.2.6).
2. Für jede Spalte eines Dataframes soll eine Berechnung durchgeführt werden (z. B. der
Mittelwert berechnet werden; s. Abschn. 7.4).
3. Jede Datei aus einem Verzeichnis soll eingelesen werden (s. Abschn. 7.4 und 28.2.3).
Eine Operation der Form Cab nennt man eine Prefix-Operator-Funktion; eine Operation der Form
a C b eine Infix-Operator-Funktion.
2
28.2 Wiederholungen
515
Funktion f
Abb. 28.2 Die Funktion f
wird auf jedes Element von E
angewendet
E
28.2.1
e1
e2
e3
en
Wiederholungen für Elemente eines Vektors
Der gerade angeführte Anwendungsfall 1 bezieht sich auf Wiederholungen für Elemente
eines Vektors; Anwendungsfall 2 auf Elemente eines Dataframes; Anwendungsfall 3 wiederum auf Elemente einer Liste. Betrachten wir die Beispiele nacheinander. In Abschn. 4.5
haben wir festgestellt, dass R vektoriell rechnet; viele Funktionen wenden das, was sie tun
sollen, auf jedes Element des Vektors an (Peng 2014):
x <- 1:3
y <- 4:6
x+y
#> [1] 5 7 9
In diesem Fall wurde jedes Element von x zu jedem korrespondierenden Element von y
addiert:
x:
1 2 3
+ + +
y: 4 5 6
----------5 7 9
Ohne diese Eigenschaft wären wir gezwungen, solche Ungetüme zu schreiben:3
z <- vector(mode = "numeric", length = length(x))
for(i in seq_along(x)) {
z[i] <- x[i] + y[i]
}
z
#> [1] 5 7 9
Viele Funktionen für statistische Berechnungen sind vektorisiert: mean(), sum(), sd(),
sqrt(); zum Glück: Meist will man eine Statistik für alle Beobachtungen berechnen. Im
Folgenden (Abschn. 28.2.3) betrachten wir nicht-vektorisierte Funktionen. Im Normalfall
heißt das: Gute Nachricht, keine besondere Aktion nötig.
3
Aber es festigt den Charakter.
516
28.2.2
28 Programmieren mit R
Wiederholungen für Spalten eines Dataframes
Sagen wir, Sie haben eine Umfrage zur Extraversion junger Erwachsener durchgeführt
und möchten nun wissen, wie die deskriptiven Statistiken der Items 1 und 2 aussehen. Eine
komfortable Lösung bietet dplyr; dazu verwenden wir die Daten der Umfrage extra.
Sagen wir, wir möchten für mehrere Spalten des Dataframes jeweils ein paar Statistiken
berechnen. Das kann so aussehen:
extra %>%
summarise_at(vars(i01:i02r), funs(max, median, sd, IQR), na.rm = TRUE)
#> # A tibble: 1 x 8
#>
i01_max i02r_max i01_median i02r_median i01_sd i02r_sd i01_IQR i02r_IQR
#>
<dbl>
<dbl>
<dbl>
<int> <dbl>
<dbl>
<dbl>
<dbl>
#> 1
4
4
3
3 0.674
0.801
1
1
summarise_at() führt eine Zusammenfassung für die mit dem Argument vars gewählten Spalten durch; dabei kommen die mit funs() gewählten Funktionen zum Zug.
Dabei gilt die Einschränkung, dass summarise() nur Funktionen ausführen kann, die als
Ergebnis einen Skalar (eine einzelne Zahl) zurückliefern. Dadurch, dass man mit funs()
mehrere Funktionen wählen kann, ist man oft ausreichend gerüstet. summarise_at()
führt also eine Wiederholung von funs() bei (at) den gewünschten Spalten durch.
Möchte man eine Funktion wiederholt ausführen, die mehr als ein einzelnes atomares Element zurückliefert, muss man sich anders behelfen. Das Paket purrr bietet dazu
eine Wiederholungsfunktion, map(E, f), die eine Funktion f auf die Elemente von E
anwendet (zuordnet, daher map):
extra %>%
select(i01, i02r) %>%
map(mosaic::favstats)
#> $i01
#> min Q1 median Q3 max
#>
1 3
3 4
4
#>
#> $i02r
#> min Q1 median Q3 max
#>
1 3
3 4
4
mean
sd
n missing
3.35 0.674 820
6
mean
sd
n missing
3.11 0.801 819
7
Warum haben wir nur einen Parameter (favstats() aus mosaic) angegeben? Die Pfeife impliziert, dass wir mit den Daten wie im vorherigen Schritt definiert arbeiten möchten.
Genauer: Das Ergebnis der letzten Zeile wird als erster Parameter an map() übergeben.
Ein anderes Beispiel für map():
extra %>%
select(i01:i02r) %>%
map(mean, na.rm = TRUE)
28.2 Wiederholungen
#>
#>
#>
#>
#>
517
$i01
[1] 3.35
$i02r
[1] 3.11
Auf Deutsch heißt diese Syntax in etwa:
Nimm den Datensatz extra UND DANN
wähle Spalten i01 bis i02r aus UND DANN
wende den Befehl mean auf alle diese Spalten an.
Die Parameter von mean() werden, abgetrennt von jeweils einem Komma, angereiht.
Übrigens darf man sich die Klammern, die sonst eine Funktion kennzeichnen, in diesem
Fall sparen: mean und mean() sind beide erlaubt. Hier besteht keine Verwechslungsgefahr mit einer Variablen, da an dieser Stelle eine Funktion stehen muss. Praktischerweise
„versteht“ mean() bzw. map(), dass wir mean() auf die Liste (hier in Form eines
Dataframes) so wie aus der letzten Zeile bzw. aus dem letzten Schritt hervorgegangen,
anwenden wollen. Alternativ (synonym aus Sicht von purrr) hätten wir die Formelschreibweise (~) verwenden können:
extra %>%
select(i01:i10) %>%
map(~mean(., na.rm = TRUE))
Der Punkt . bezeichnet hier das Datenobjekt, wie es aus der letzten Zeile hervorging, so
wie wir es bei dplyr kennen gelernt haben. Benutzt man allerdings diese Schreibweise
(die sog. Formel-Schreibweise), so muss man die Tilde ~ an den Beginn des Funktionsnamens stellen; dann schreibt man die Funktion wie gehabt. Wie erwähnt, liefert map eine
Liste zurück. Möchte man einen numerischen Vektor, so kann man map_dbl() verwenden:
extra %>%
select(i01:i02r) %>%
map_dbl(~mean(., na.rm = TRUE))
#> i01 i02r
#> 3.35 3.11
Gibt man bei map() kein Suffix (wie map_dbl()) an, so wird eine Liste zurückgeliefert.
Es gibt Varianten von map() für z. B. int, lgl, chr;4 auch Suffixe wie bei dplyr
existieren in der Art _if, _at. Alternativ zu map() kann man die Funktion apply()
4
Z. B. map_dbl(), map_dfr(); s. ?map.
518
28 Programmieren mit R
und Freunde aus dem Standard-R verwenden; allerdings sind diese Funktionen nicht alle
typstabil und haben weniger Komfort-Funktionen im Vergleich zu map.
Jedes gewählte Element von extra (allgemeiner: der vorausgehenden Liste) wird
an die Funktion angegeben, die bei map() als Argument angeführt ist.
28.2.3 Dateien wiederholt einlesen
Sagen wir, Sie haben von „baugleiche“ Datensätze (gleiche Spaltennamen und Datentypen) in einem Verzeichnis liegen; diese sollen eingelesen, zu einem Datensatz zusammengeführt und schließlich einer geheimen Analyse unterzogen werden. Um dieses Szenario
nachspielen zu können, erstellen wir uns ein paar Datensätze und schreiben diese in ein
Verzeichnis auf der Festplatte.
1:3 %>%
map(~filter(extra, row_number() == .)) %>%
map(~select(., 1:3)) %>%
imap(~write_csv(., path = paste0("dummy/df", .y, ".csv")))
#> [[1]]
#> # A tibble: 1 x 3
#>
timestamp
code
i01
#>
<chr>
<chr> <int>
#> 1 11.03.2015 19:17:48 HSC
3
#>
#> [[2]]
#> # A tibble: 1 x 3
#>
timestamp
code
i01
#>
<chr>
<chr> <int>
#> 1 11.03.2015 19:18:05 ERB
2
#>
#> [[3]]
#> # A tibble: 1 x 3
#>
timestamp
code
i01
#>
<chr>
<chr> <int>
#> 1 11.03.2015 19:18:09 ADP
3
Das erste map() erzeugt drei Dataframes, jeweils mit einer Zeile (nämlich Zeilen 1, 2
bzw. 3 aus extra). Alternativ hätten wir vorab eine Liste mit drei Dataframes erstellen
können, was aber wohl komplizierter gewesen wäre. Das zweite map() wählt dann für
den 1. Dataframe die 1. Spalte aus dem einzeiligen Dataframe, für den 2. Dataframe die
2. Spalte und für den 3. Dataframe die 3. Spalte. Die Funktion imap() gleicht map(),
nur dass sie über die von ihr erzeugte Variable .y den Index, also die Nummer jedes
28.2 Wiederholungen
519
Elements, das gemappt wird, verfügt. Damit erstellen wir den Dateinamen. Lassen wir
uns diese Abfolge bis zur Erstellung der Dateinamen ausgeben:
1:3 %>%
map(~filter(extra, row_number() == .)) %>%
map(~select(., 1:3)) %>%
imap(~paste0("dummy/df", .y, ".csv"))
#> [[1]]
#> [1] "dummy/df1.csv"
#>
#> [[2]]
#> [1] "dummy/df2.csv"
#>
#> [[3]]
#> [1] "dummy/df3.csv"
Puh, geschafft. In der Praxis ist dieser Schritt oft nicht nötig, da Sie schon die 100 Datensätze in einem Ordner liegen haben. So, jetzt lesen wir diese 100 Datensätze ein. Um
einer Sehnenscheidenentzündung vorzubeugen, entscheiden wir uns nach kurzem Zögern
gegen folgendes – zugegeben einfaches – Vorgehen:
df1 <- read_csv("dummy/df1.csv")
df2 <- read_csv("dummy/df2.csv")
df3 <- read_csv("dummy/df3.csv")
Neben der Gefahr des Viel-Tippens ist dieses Vorgehen fehleranfällig und außerdem
schlecht wiederverwendbar für ähnliche Situation. Besser geht es mit map(). Zuerst
erstellen wir einen Vektor mit den Namen aller CSV-Dateien im betreffenden Ordner, das
geht mit dir():
df_filenames <- dir(path = "dummy", pattern = "*.csv")
str(df_filenames)
#> chr [1:3] "df1.csv" "df2.csv" "df3.csv"
Diesen Vektor übergeben wir an map(), der auf jedes Element die Funktion read_csv()
anwenden soll. Liegen die Dateien im Arbeitsverzeichnis, so ist die Sache einfach:
df_filenames %>%
map(read_csv)
Liegen die Datei in einem anderen Ordner, so muss man read_csv() sagen, wo zu
suchen ist:
df_filenames %>%
map_df(~read_csv(file = paste0("dummy/", .))) -> df
df
#> # A tibble: 3 x 3
520
28 Programmieren mit R
#>
timestamp
#>
<chr>
#> 1 11.03.2015 19:17:48
#> 2 11.03.2015 19:18:05
#> 3 11.03.2015 19:18:09
28.2.4
code
i01
<chr> <int>
HSC
3
ERB
2
ADP
3
Anwendungsbeispiele für map
28.2.4.1 Ein lineares Modell für jede Gruppe berechnen
Ein weiteres Exempel für map() ist dieses hier: Wir berechnen das gleiche lineare Modell
einmal für Frauen und einmal für Männer.
extra %>%
select(i01, i02r, i03, n_facebook_friends, sex) %>%
split(.$sex) %>%
map(~lm(n_facebook_friends ~ i01 + i02r + i03, data = .)) -> extra_models
Wieder ist . nur eine Art Steno, das das Datenobjekt bezeichnet, so wie es im letzten
Schritt berechnet wurde. split() teilt den Datensatz in zwei Listen auf, die jeweils ein
Datensatz sind – einer für Männer, einer für Frauen. Das ist übrigens ein Unterschied
zu group_by(), der zwar auch einen Datensatz in Gruppen aufteilt, aber eben nicht in
mehrere Listen, sondern in einen speziellen gruppierten Dataframe, was etwas anderes
ist. In der nächsten Zeile wird ein lineares Modell mit lm ausgeführt, wobei die Anzahl
der Facebook-Freunde einer Person das Kriterium ist und die Items 1bis 3 die Prädiktoren. Da unsere Daten jetzt aus zwei Elementen (einem Datensatz für Frauen, einem
für Männer) bestehen, wird diese Zeile zweimal ausgeführt bzw. für jedes Element einmal ausgeführt. Im nächsten Schritt wäre es interessant, die Ergebnisse des Modells zu
betrachten. Weist man einem Objekt das Ergebnis von lm() zu, so kann man sich mit
der Funktion summary() Ergebnisse des Modells ausgeben lassen (probieren Sie mal
str(extra_models)). Führen wir summary() im Rahmen von map() aus, so wird
mit jedem (der beiden) Elemente von extra_models der Befehl summary() ausgeführt:
extra_models %>%
map(summary) %>%
map_dbl("r.squared")
#>
Frau
Mann
#> 0.0421 0.0115
Führt man summary() aus, so wird eine Liste erstellt, die einige Zusammenfassungen für
das jeweilige Modell enthält. Im Rahmen von map() wird diese „Zusammenfassungsliste“ für jedes Element von extra_models erstellt (d. h. einmal für Frauen und einmal für
Männer). Enthält eine Liste benannte Elemente, so kann man diese beim Namen anspre-
28.2 Wiederholungen
521
chen; aus jedem Element der Liste wird das Objekt mit dem entsprechenden Namen dann
ausgelesen. Da unter r.squared nur eine (reelle) Zahl gespeichert ist, lassen wir uns
anstelle einer Liste eine einfachere Struktur, nämlich einen numerischen Vektor ausgeben
mit map_dbl().
28.2.4.2 Mehreren Spalten jeweils einen t-Test zuordnen
Sagen wir, wir wären brennend an der Frage interessiert, wer häufiger fremdgeht: Männer
oder Frauen. Und diese Frage soll für uns nicht eher entschieden sein, als wir einen pWert dazu sehen. Außerdem wollen wir noch wissen, ob sich Frauen und Männer in den
anderen Variablen des Datensatzes Affairs noch unterscheiden. Also los geht’s; werfen
Sie zu Beginn einen Blick in den Datensatz Affairs; wir sind dabei nur an numerischen
Spalten interessiert, da wir diese einem Test zum Vergleich des Mittelwerts (hinsichtlich
Frauen und Männer) unterziehen wollen.
Affairs %>%
select_if(is.numeric) %>% head
#>
affairs age yearsmarried religiousness education occupation rating
#> 4
0 37
10.00
3
18
7
4
#> 5
0 27
4.00
4
14
6
4
#> [ reached getOption("max.print") -- omitted 4 rows ]
Wir „mappen“ jede dieser Spalten zu einer Funktion, hier einem t-Test, welcher die Spalte
hinsichtlich der Geschlechter vergleicht:
Affairs %>%
select_if(is.numeric) %>%
map(~t.test(. ~ Affairs$gender)) %>%
map_dbl("p.value")
#>
affairs
age yearsmarried religiousness
#>
7.74e-01
2.85e-06
4.58e-01
8.51e-01
#>
occupation
rating
#>
8.89e-35
8.53e-01
education
9.77e-24
Am Anfang kann die Funktion map() obskur wirken. Daher schlüsseln wir sie nochmal
auf; der letzte Code-Abschnitt heißt auf Pseudo-Deutsch also:
Nimm die Tabelle Affairs UND DANN
wähle alle numerischen Spalten UND DANN
wende auf alle Spalten einen t-Test an, wobei die Gruppierungsvariable
Affairs$gender ist UND DANN
wende die Funktion „extrahiere das Element mit dem Namen p.value“ an; das
Ergebnis soll vom Typ „reelle Zahl“ (double) sein.
28 Programmieren mit R
Untersuchte Variablen
522
rating
religiousness
signif
affairs
ns
yearsmarried
significant
age
education
occupation
0.0
0.2
0.4
0.6
0.8
p-Wert
Abb. 28.3 Multiple t-Tests und deren p-Werte
Ein „klassischer“ R-Ansatz würde vielleicht so aussehen:
lapply(Affairs[c("affairs", "age", "yearsmarried")],
function(x) t.test(x ~ Affairs$gender))
Wir lassen diese Syntax andächtig so stehen. Schließlich können wir die Ergebnisse noch
visualisieren, der Mensch ist halt ein Augentier (s. Abb. 28.3).
Affairs %>%
select_if(is.numeric) %>%
map_df(~t.test(. ~ Affairs$gender)$p.value) %>%
gather() %>%
mutate(signif = ifelse(value < .05, "significant", "ns")) %>%
ggplot(aes(x = reorder(key, value), y = value)) +
geom_point(aes(color = signif, shape = signif)) +
coord_flip() +
labs(x = "Untersuchte Variablen",
y = "p-Wert")
Und die Daten sagen uns: Die mittlere Anzahl der Seitensprünge sowie die mittlere Ehezufriedenheit sind nicht statistisch signifikant unterschiedlich zwischen Männern und Frauen. Ein anderer Weg wäre, nur die signifikanten Testergebnisse zu behalten bzw. weiter
zu analysieren; dafür gibt es die Funktion keep() in purrr. Man übergibt keep() eine
Bedingung (z. B. .$p.value < .05); dann werden nur die Elemente behalten, auf die
die Bedingung zutrifft:
Affairs %>%
select_if(is.numeric) %>%
map(~t.test(. ~ Affairs$gender)) %>%
keep(~.$p.value < .05) %>%
map("p.value")
#> $age
#> [1] 2.85e-06
28.3 Defensives Programmieren
#>
#>
#>
#>
#>
#>
523
$education
[1] 9.77e-24
$occupation
[1] 8.89e-35
28.2.5 Einige Rechtschreibregeln für map()
Wiederholungsbefehle wie map() sind anfangs vielleicht verwirrend, daher hier noch ein
Überblick zur Gefühlswelt von map():
1. Die allgemeine Syntax lautet map(E, f): Auf jedes Element des Vektors E wird die
Funktion f angewendet.
2. Verwendet man map() in einer Pfeifensyntax, so braucht man E nicht anzugeben,
dass wird von der Pfeife implizit getan, also %>% map(f).
3. Man kann für f den Namen einer Funktion angeben, etwa: %>% map(mean, na.rm
= TRUE); etwaige Parameter werden mit Komma aufgereiht.
4. Man kann auch die Formel-Schreibweise verwenden: %>% map(~mean(., na.rm
= TRUE)), dann schreibt man die zu wiederholenden Funktion in normaler Schreibweise.
5. Man kann map() auch den Namen eines Elements von E übergeben (was voraussetzt,
dass die Elemente von E ein Element dieses Namens besitzen); in diesem Fall wird der
Wert dieses Elements zurückgeliefert: %>% map("p.value")
28.3
Defensives Programmieren
Eine Weisheit beim Programmieren lautet: Es dauert immer länger, als man denkt, bis
etwas funktioniert (selbst wenn man diese Regel berücksichtigt). Dinge, die man zum
ersten Mal (in R) schreibt, dauern besonders lange. Das kann frustrieren, aber man kann
sich darauf einstellen und genug Zeit einplanen. Professionelle Softwareentwicklung ist
keine Kleinigkeit, aber das Berücksichtigen einiger Grundregeln kann viel Zeit und Nerven sparen. Einen vorausschauenden Programmierstil bezeichnet man auch als defensiv
(Hunt und Thomas 1999). Einige Grundregeln des defensiven Programmierens sind im
Folgenden aufgeführt:
1. Wiederhole dich nicht (Don’t repeat yourself; DRY): Wenn Sie merken, dass sie den
gleichen Code mehrfach eingeben/einkopieren, machen Sie besser eine Funktion daraus. Es ist einfacher zu sagen: „links abbiegen“, als zu sagen: „Blicke links über die
Schulter, blinke links, links lenken.“ Auf ähnliche Weise ist es besser, mehrere Einzeloperationen in eine Funktion zu packen:
524
28 Programmieren mit R
80
Was für ein Histogramm
Aber was ist mit den Farben?
60
60
count
count
80
40
20
0
1
2
3
4
sex
Frau
40
Mann
NA
20
extra_mean
0
sex
sex
1
2
3
4
extra_mean
Abb. 28.4 Den Code zu vereinfachen, hilft, Fehler zu finden
abbiegen <- function(richtung, krass = FALSE){
schulterblick(richtung)
blinke(richtung)
zustand <- lenke(richtung)
return(zustand)
}
2. Vereinfache den Code: Besonders wenn Sie auf der Suche nach einem Fehler sind,
sollten Sie die fraglichen Zeilen so weit wie möglich kürzen und vereinfachen. So ist
der Fehler einfacher zu finden. Betrachten Sie die Syntax für Abb. 28.4; die Syntax
für das linke Diagramm ist etwa doppelt so lang wie die für das rechte. Es fällt daher
schwerer, den Fehler zu finden, der dafür sorgt, dass die Histogramme nicht jeweils für
jedes Geschlecht ausgegeben werden.5
extra %>%
select(extra_mean) %>%
drop_na(extra_mean) %>%
ggplot +
aes(x = extra_mean, fill = "sex") +
geom_histogram() +
labs(title = "Was für ein Histogramm",
subtitle = "Aber was ist mit den Farben?") +
theme(legend.position = "bottom") -> p1
extra %>%
ggplot() +
aes(x = extra_mean, color = sex) +
geom_histogram() -> p2
grid.arrange(p1, p2, nrow = 1)
5
Die Variable sex darf nicht in Anführungsstrichen stehen, da es eine Variable ist; die Anführungsstriche werden als String aufgefasst. Außerdem könnte fill statt color angebrachter sein.
28.3 Defensives Programmieren
525
3. Testen Sie: Oft denkt man, R werde das tun, was man denkt. Leider ist dem meist
nicht immer so. Tatsächlich scheint es gerade dort Unterschiede zwischen Wunsch
und Wirklichkeit zu geben, wo man sie am wenigsten erwartet. Daher hilft es, auf solche Abweichungen zu prüfen. Man sollte solche Tests nicht übertreiben, gerade wenn
man „nur“ ein bisschen „rumanalysiert“. Aber je ernsthafter die Analyse ist und gerade
wenn man eine Funktion schreibt, die fehlersicher zum Ziel führen soll, desto wichtiger sind Tests. Möchten Sie zum Beispiel die Korrelation zweier Variablen (Extraversion und Anzahl Parties) berechnen, so kann es sinnvoll sein, vorab zu prüfen, ob die
Variablen metrisch sind und ob es fehlende Werte gibt. stopifnot(bedingung)
ist dafür hilfreich. Diese Funktion produziert eine Fehlermeldung, falls bedingung
nicht zutrifft. Probieren Sie folgende Beispiele aus:
stopifnot(is.numeric(extra$sex))
stopifnot(is.numeric(extra$n_party))
stopifnot(sum(is.na(extra$n_party)) == 0)
sum(is.na(extra$n_party))
Aufgaben
Richtig oder falsch?6
1.
In Funktionen darf die Pfeife (aus dem Paket magrittr) nicht verwendet werden.
2. Eine Funktion kann über Argumente (Parameter) verfügen.
3. Eine Funktion muss über Argumente (Parameter) verfügen.
4. +(1, 1) schreibt man gewöhnlich als 1+1.
5. Die Funktion sd2() liefert identische Wert wie sd().
6. Wenn x <- 1:3; y <- 4:6, dann gibt z <- x + y zurück: 5 7 9.
s <- c("Hallo", "R", "wie", "geht's");
führt
man
7. Sei
paste0(s, "!") aus, wird man sehen, dass paste0() vektoriell rechnet.
8. Mit summarise_at() kann man mehrere Spalten eines Dataframes zu einer
oder mehreren Statistiken zusammenfassen.
9. Mit map() kann man mehrere Elemente eines Vektors einer Funktion zuführen; die Funktion wird auf jedes Element angewendet.
10. Der Funktion map() dürfen auch Dataframes eingegeben werden; in dem Fall
wird die zugeordnete Funktion auf jede Spalte angewendet.
s <- c("Hallo", "R", "wie", "geht's")
paste0(s, "!")
#> [1] "Hallo!"
6
F, R, F, R, F, R, R, F, R, R.
"R!"
"wie!"
"geht's!"
Programmieren mit dplyr
29
Lernziele
Ein Grundverständnis von non-standard evaluation (NSE) erwerben.
Wissen, wie man Funktionen für dplyr schreibt.
Funktionen, die NSE verwenden, erkennen.
Wissen, was man unter Zitieren (Quotation) versteht.
In diesem Kapitel werden folgende Pakete und Daten benötigt:
library(rlang)
library(pryr)
library(tidyverse)
library(gridExtra)
data(extra, package = "pradadata")
29.1
Wie man mit dplyr nicht sprechen darf
Wie wir in Abschn. 28.1 gesehen haben, sind Funktionen eine praktische Sache. Da wir
in diesem Buch ausgiebig von dplyr Gebrauch gemacht haben, liegt die Idee nahe, unser
Wissen über das Schreiben von Funktionen auf dplyr anzuwenden. Probieren wir es aus;
schreiben wir eine Funktion, welche von einer Spalte den Median und den IQR berechnet.
Erstmal zur Erinnerung: Ohne eine Funktion zu programmieren, würden wir so vorgehen
(nehmen wir den Datensatz extra als Beispiel):
extra %>%
drop_na(i01) %>%
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3_29
527
528
29 Programmieren mit dplyr
summarise(median(i01),
IQR(i01))
#> # A tibble: 1 x 2
#>
`median(i01)` `IQR(i01)`
#>
<dbl>
<dbl>
#> 1
3
1
Das war einfach; aber schön wäre natürlich, wenn wir eine Funktion hätten, nennen wir
sie stats(), die das gleiche Ergebnis liefern würde, die also folgenden Befehl erlaubt:
extra %>%
stats(i01)
Wir ignorieren hier geflissentlich, dass dieses Problem schon von anderen gelöst wurde;
z. B. würden mosaic::inspect(extra), summary(extra), jsmisc::descr()
oder skimr:skim(extra) helfen. Man könnte meinen, so sollte man diese Funktion
stats() schreiben:
stats_ohno <- function(df, spalte){
df %>%
summarise(median(spalte, na.rm = TRUE),
IQR(spalte, na.rm = TRUE))
}
Geht aber nicht.
stats_ohno(extra, i01)
#> Error in summarise_impl(.data, dots): Evaluation error: object 'i01'
not found.
Führt man diese Funktion aus, wird man enttäuscht. Um den Grund zu verstehen, müssen wir Funktionen noch genauer verstehen. Und wir müssen erkennen, welche Funktion
„normal“ funktioniert und welche „nicht normal“ funktioniert. Bei letzteren Funktionen
spricht man von non-standard evaluation (NSE) (Wickham 2014).
29.2
Standard-Evaluation vs. Non-Standard-Evaluation
Goldene Regeln des guten Tons bei R beinhalten: „Du sollst Text in Anführungsstriche
setzen“ und „Benenne den Dataframe eines Vektors“. Soll R einen Text schreiben, so
kann das so aussehen:
print("Hallo, R")
#> [1] "Hallo, R"
29.2 Standard-Evaluation vs. Non-Standard-Evaluation
529
Hingegen funktioniert folgender Code nicht, da er einen Text nicht in Anführungsstriche
setzt.
print(Hallo)
#> Error in print(Hallo): object 'Hallo' not found
Das ist R-Grundwissen. Aber es gibt ein paar Funktionen, die diese Regel brechen:
library(ggplot2)
Dieser Code wird ohne Murren ausgeführt (evaluiert, sagt man auch). Aber: „ggplot2“ ist
keine Variable (kein Objekt) in meiner Arbeitsumgebung (Global Environment). In meiner
Arbeitsumgebung gibt es kein Objekt mit dem Namen „ggplot2“. Warum funktioniert
dann dieser Code? Die Antwort lautet: Die normalen Evaluationsregeln von R werden
nicht verwendet; stattdessen wird die „Nicht-Standard-Evaluation“ (NSE) verwendet. In
einem Moment schauen wir uns das genauer an. Zuerst noch ein paar Beispiele:
data(Affairs, package = "AER")
select(Affairs, affairs: age, education, rating, children)
Auch hier wieder: „Affairs“ ist zur Zeit des Aufrufs dieser Funktion kein Objekt, das
meinem R bekannt ist; es existiert kein Objekt mit diesem Namen in meiner Arbeitsumgebung. Die Funktionen library() und select() verwenden NSE in gleicher Weise.
Wie erkennt man, ob eine Funktion NSE verwendet? Angenommen, Sie fragen sich,
ob library(ggplot2) ein Funktionsaufruf ist, der NSE verwendet. Um das zu
überprüfen, evaluieren Sie den Ausdruck ggplot2 (geben Sie den Ausdruck in
R ein). Wird ein Objekt erkannt, dann ist es vermutlich eine reguläre Evaluation,
eine „normale“ Funktion. Wird kein Objekt gefunden, so wurde vermutlich NSE
verwendet.
Was ist der Vorteil von NSE? Vielleicht der Hauptvorteil ist Komfort. Es wäre umständlicher, wenn man schreiben müsste:
data("Affairs", package = "AER")
select("Affairs", "Affairs$affairs", "Affairs$age", "Affairs$education",
"Affairs$rating", "Affairs$children")
Funktionen bei dplyr und in einigen anderen Paketen auch machen ausgiebigen Gebrauch
von NSE, weil es komfortabel für den Nutzer ist. Die zentrale Idee ist, ein Funktionsargument nur als R-Code zu lesen und sozusagen für die spätere Verwendung zu speichern
und aufzubereiten. Diesen Vorgang nennt man Zitieren (Quotation). Das zitierte Objekt
nennt man einen Ausdruck (Expression, Zitation). Wird der Ausdruck später einem Be-
530
29 Programmieren mit dplyr
fehl zugeführt, wird er in diesem Zuge evaluiert (ausgeführt). Der Nachteil ist: NSEFunktionen sind schwierig zu programmieren; möchte man eine Funktion schreiben, die
NSE-Funktionen nutzt, muss man einen Umweg in Kauf nehmen. Bevor wir lernen, wie
man NSE-Funktionen schreibt, ist es hilfreich, die Arbeitsweise von NSE zu verstehen.
Dazu hilft die Analogie, die NSE mit Backen bzw. einem Backrezept vergleicht.
Ein Ausdruck ist ein Objekt, das R-Code enthält. Erstellt man einen Ausdruck, so
spricht man von Zitieren.
29.3
NSE als Backen1
Stellen Sie sich vor, Sie wollen einen Apfelkuchen backen. Hier ist das Rezept:
Omas Bester Apfel
1. Folgen Sie dem Rezept „Rührkuchen“, s. S. 42
2. Geben Sie ein Pfund klein geschnittene Äpfel unter
3. Rühren Sie den Zuckerguss an, s. S. 982
Sie backen den Kuchen – herrlich, was für ein Kuchen. Ihre Gäste lecken die Teller ab.
Nun ist es einsichtig, dass ein Rezept nicht das Gleiche ist wie ein Kuchen, auch wenn
es einen wichtigen Zusammenhang gibt. Der springende Punkt: Das Rezept ist für den
Kuchen das Gleiche, was ein Ausdruck (Zitation) für die Evaluation eines Ausdrucks ist.
Lesen Sie diesen Satz noch einmal in Ruhe.
Zur Verdeutlichung lassen wir R den Rührkuchen backen:
ruehrkuchen <- eier + salz + milch + mehl
#> Error in eval(expr, envir, enclos): object 'eier' not found
R will sofort loslegen mit dem Backen; anders gesagt: In diesem Beispiel ist eier +
salz + milch + mehl ein Ausdruck, und R versucht, diesen zu evaluieren. Allerdings gibt es eine Fehlermeldung, wenn R diese Objekte nicht kennt. In meiner Arbeitsumgebung gibt es keine Objekte dieser Namen. Beim Backen muss man zuerst die Zutaten
besorgen; im Analogieschluss bedeutet das, diese Objekte in R zu definieren. Wir weisen
den Objekten eine Zahl zu, die den Kalorien (pro Stück Kuchen) entsprechen soll:
eier <- 120
salz <- 1
milch <- 100
mehl <- 80
1
Dieser Abschnitt orientiert sich an Salam (2017).
29.3 NSE als Backen
531
Jetzt stehen die Zutaten im Regal (d. h. in der globalen Umgebung): Los, R, Kuchen backen!
ruehrkuchen <- eier + salz + milch + mehl
ruehrkuchen
#> [1] 301
Ah, der Kuchen ist gebacken; sehr lecker. Hat ordentlich Kalorien. Das normale Vorgehen
von R ist, Ausdrücke sofort zu evaluieren; das geht natürlich nur, wenn die Objekte existieren. Benennt man keinen Ort, so sucht R in der aktuellen Umgebung, das ist hier die globale Umgebung. Und zwar am richtigen Ort; Wäre mehl im Dataframe speisekammer
abgespeichert, so hätte R das Objekt in diesem Fall nicht gefunden. R sucht ratlos in der
Arbeitsumgebung nach den Zutaten, ohne zu wissen, dass das Mehl anderswo (in der
Speisekammer) liegt, um im Bild zu bleiben. Erklären wir nun R den Unterschied zwischen einem Rezept und dem Kuchen bzw. dem Backen des Kuchens. Dafür ziehen wir
die Funktion rlang::quo() heran, die einen Ausdruck zitiert, also den Ausdruck speichert.
Zitieren bedeutet, den Ausdruck (den R-Code als Objekt) zu speichern – nicht das
Ergebnis des Ausdrucks.
Um in der Backmetapher zu bleiben, benennen wir die Funktion um:
rezept <- quo
Schreiben wir das Rezept auf Errisch auf:
ruehrkuchen_rezept <- rezept(eier + salz + milch + mehl)
ruehrkuchen_rezept
#> <quosure>
#>
expr: ^eier + salz + milch + mehl
#>
env: global
Keine Fehlermeldung; R hat das Rezept notiert; die Eingabe wurde zitiert. Aber was bedeutet die merkwürdige Ausgabe? Zum ersten sagt uns R, dass es sich um ein Objekt des
Typs quosure handelt.2 Das ist ein Ausdruck (Zitation), der speichert, wo er sich befindet. Zumeist befindet sich der Ausdruck in der Arbeitsumgebung (hier mit env: global
gekennzeichnet); er könnte sich aber auch in einer Funktion oder einem Dataframe befinden. Die eigentliche Expression wird durch expr kenntlich gemacht. Außerdem wird
2
Der Begriff quosure ähnelt dem Begriff closure, was eine Funktion kennzeichnet, die sich die ihr
zugehörige Umgebung gemerkt hat. Außerdem drückt quosure aus, dass zitiert wird (quoting); ein
Quosure ist demnach eine Mischung aus beiden.
532
29 Programmieren mit dplyr
noch durch das Hütchen ˆ auf die Expression hingewiesen. Das Hütchen will sagen: „Achtung, hier kommt etwas Ungewöhnliches.“
Zur Wiederholung: R hat das Rezept nicht ausgeführt – nur notiert. Der Kuchen wurde
nicht gebacken, nur das Rezept notiert. Das ist bemerkenswert, denn die Objekte wie eier
existieren ja, haben einen (numerischen) Wert. Der Ausdruck eier + salz + milch
+ mehl könnte also problemlos ausgeführt werden – wird er aber nicht von rezept.
Man könnte sagen: Rezepte haben keine Kalorien, im Gegensatz zum Kuchen.
R kennt jetzt also das Rezept und den Unterschied zum Backen. Dann los – aus dem
Rezept wird gebacken. Um einen Ausdruck (Rezept) zu evaluieren (backen), können wir
die Funktion eval_tidy() nutzen. Wieder benennen wir sie um, um in der Metapher zu
bleiben:
backen <- eval_tidy
ruehrkuchen2 <- backen(ruehrkuchen_rezept)
ruehrkuchen2
#> [1] 301
Hervorragend gelungen, auch dieses Mal. Oh, die Äpfel haben wir vergessen. Naja,
schmeckt auch so. Der Zuckerguss fehlt auch noch; erstellen wir in R das Rezept und
bereiten den Zuckerguss zu:
zuckerguss_rezept <- rezept(butter + zitrone + zucker)
zuckerguss_rezept
#> <quosure>
#>
expr: ^butter + zitrone + zucker
#>
env: global
zuckerguss <- backen(zuckerguss_rezept)
#> Error in backen(zuckerguss_rezept): object 'butter' not found
Hoppla – ein Fehler. Wir haben keine Butter und keine Zitrone im Vorratsregal. Aber:
Natürlich konnten wir ein Rezept erstellen. Beim Rezeptschreiben hat R keinen Fehler
bemäkelt. Das macht Sinn, oder? Um ein Rezept zu schreiben, braucht man kein gefülltes Vorratsregal; zum Backen schon. Kaufen wir schnell noch die fehlenden Zutaten und
bereiten den Zuckerguss zu:
butter <- 130
zitrone <- 20
aepfel <- 70
zucker <- 110
zuckerguss <- backen(zuckerguss_rezept)
29.3 NSE als Backen
533
Schauen Sie sich noch einmal das Rezept oben, Omas Bester Apfel an; backen wir es jetzt
nach:
kuchen <- backen(ruehrkuchen_rezept + aepfel + zuckerguss_rezept)
#> Error in ruehrkuchen_rezept + aepfel: non-numeric argument to binary operator
Oh nein, ein Fehler: Der Kuchen ist missraten. Was ist bloß passiert? Oma, bitte hilf. Eine
Funktion, die uns hilft, indem sie aufschlüsselt, welche Ausdrücke verarbeitet wurden,
heißt qq_show:
oma <- qq_show
Oma, bitte schau dir an, was beim Backen passiert ist:
oma(backen(ruehrkuchen_rezept + aepfel + zuckerguss_rezept))
#> backen(ruehrkuchen_rezept + aepfel + zuckerguss_rezept)
Oma sagt, sinngemäß, die Äpfel sind schön klein geschnitten bereit. Aber anstelle der
Zutaten des Rührkuchens wurde einfach das Rezept für den Rührkuchen in den Ofen geschoben; für den Zuckerguss genauso. Nur das Rezept liegt in der Backform! Oma schaut
kritisch. So wird das nie ein Kuchen!
Das Problem ist, dass zuckerguss_rezept nicht das Gleiche ist wie Zutaten: Es
ist ein Verweis auf die Zutaten. Wir müssen R sagen, was sich hinter dem Teilrezept
zuckerguss_rezept verbirgt, sonst kann kein Kuchen gebacken werden. Das ist wie
beim Rezept, von Omas Bester Apfel: Auch da müssen wir den begeisterten Gästen aufschlüsseln, was sich hinter den Teilrezepten „Rührkuchen“ und „Zuckerguss“ verbirgt.
Nicht so bei den Äpfeln: Die sind eine echte Zutat, die nicht ersetzt werden muss. Dieses
Ersetzen nennt man im Fachjargon unquoting, die Umkehroperation vom Zitieren; nennen
wir es Unzitieren. Die R-Funktion dafür sieht lustig aus und hat einen lustigen Namen: !!,
gesprochen „Bang-Bang“. Heißt wirklich so. Wenn Sie es nicht glauben, fragen Sie Oma:
oma(!!zuckerguss_rezept)
#> ^butter + zitrone + zucker
Oma erkennt sofort: zuckerguss_rezept ist ein Rezept aus Butter, Zitrone und Zucker. Damit können wir jetzt die Teilrezepte auflösen und ein Gesamtrezept erstellen. Nur
mit diesem Gesamtrezept können Ihre Gäste den köstlichen Kuchen nachbacken:
kuchen_gesamtrezept <- rezept(!!ruehrkuchen_rezept + aepfel +
!!zuckerguss_rezept)
kuchen_gesamtrezept
#> <quosure>
#>
expr: ^(^eier + salz + milch + mehl) + aepfel + (^butter
#>
+ zitrone + zucker)
#>
env: global
534
29 Programmieren mit dplyr
Oma, schau nicht so streng, jetzt gelingt der Kuchen:
kuchen <- backen(kuchen_gesamtrezept)
kuchen
#> [1] 631
Oma ist zufrieden. Ein bisschen mächtig, aber dafür auch mit Äpfeln. Auf zur nächsten
Feier. Fassen wir zusammen:
Ein Rezept notieren: Einen Ausdruck zitieren
Aus dem Rezept einen Kuchen backen: Einen Ausdruck (oder mehrere) evaluieren
Ein Teilrezept in seinen Zutaten aufschlüsseln: Einen Ausdruck unzitieren.
Das sind die wesentlichen Ideen von NSE. Damit sind wir gerüstet, eine NSE-Funktion
mit dplyr zu schreiben.
29.4
Wie man Funktionen mit dplyr-Verben schreibt
Erstellen wir eine Funktion, die den Median einer beliebigen Spalte eines beliebigen Dataframes berechnet und zwar mithilfe der dplyr-Werkzeuge. Ach ja, ein Hinweis noch:
anstelle von quo() nehmen wir hier die Schwesterfunktion enquo(), die immer dann
zum Einsatz kommt, wenn man innerhalb einer Funktion einen Ausdruck zitieren möchte.
Betrachten Sie den Unterschied der beiden Funktionen in diesem Beispiel:
my_median <- function(df, col){
col_q = enquo(col)
df %>%
summarise(median(!!(col_q), na.rm = TRUE))
}
my_median(extra, i01)
#> # A tibble: 1 x 1
#>
`median(i01, na.rm = TRUE)`
#>
<dbl>
#> 1
3
Würde man innerhalb einer Funktion anstelle von enquo() die Funktion quo() verwenden, so würden wir mit einem Fehler bestraft:
debug(my_median_quo)
#> Error in debug(my_median_quo): object 'my_median_quo' not found
my_median_quo <- function(df, col){
col_q = quo(col)
df %>%
summarise(median(!!(col_q), na.rm = TRUE))
}
29.4 Wie man Funktionen mit dplyr-Verben schreibt
535
my_median_quo(extra, i01)
#> Error in summarise_impl(.data, dots): Evaluation error: object 'i01' not
found.
quo(col) liefert col zurück – aber wir möchten i01 zurückgeliefert bekommen!
quo() ist zu wörtlich. Das ist der Grund, warum wir enquo() innerhalb einer Funktion
verwenden müssen, nicht quo(). Warum muss man df nicht zitieren? Weil df mit dem
Objekt extra ersetzt wird, das beim normalen Aufruf der Funktion my_median() ge-
funden wird (sofern es in der globalen Umgebung liegt); Standard-Evaluation liefert das
gewünschte Ergebnis für df. i01 hingegen wird in der globalen Umgebung oder in der
Umgebung der Funktion my_median() nicht gefunden. Und warum der Bang-BangOperator bei col_q? dplyr-Funktionen wie summarise() zitieren ihre Argumente;
würden wir das Argument nicht vorab unzitieren, resultierte eine doppelte Zitierung.
Doppelte Zitierungen funktionieren nicht:
quo(quo(aepfel))
#> <quosure>
#>
expr: ^quo(aepfel)
#>
env: global
Dieser Aufruf liefert den Ausdruck quo(aepfel) zurück; was wir aber bräuchten, ist
der Ausdruck aepfel. Eine der beiden Zitierungen muss unzitiert werden:
quo(!!quo(aepfel))
#> <quosure>
#>
expr: ^aepfel
#>
env: global
Dieser Aufruf hat funktioniert, da nur eine Zitierung vorlag; die Doppel-Zitierung wurde
durch eine Unzitierung (Bang-Bang) zurückgenommen. Entsprechendes ist nötig, wenn
man Funktionen mit den dplyr-Werkzeugen schreibt.
Kaum hat man einen Schritt gemeistert, erwachen schon neue Wünsche: Die Spaltennamen, die unsere Funktion my_median() von sich gibt, sind nicht sehr ansprechend.
Benennen wir sie um:
my_median <- function(df, col){
col_q = enquo(col)
df %>%
summarise(md_spalte = median(!!(col_q),
na.rm = TRUE))
}
my_median(extra, i01)
#> # A tibble: 1 x 1
#>
md_spalte
536
#>
#> 1
29 Programmieren mit dplyr
<dbl>
3
Noch schöner, aber auch komplizierter geht es mit dem Operator: :=. Dieser Operator
wird benötigt, weil R nicht erlaubt, dass links von einem Gleichheitszeichen eine Funktion ausgeführt (evaluiert) wird. R erlaubt links vom Gleichheitszeichen nur einfache
Variablen. Abgesehen davon ist := ein normales Gleichheitszeichen, das eine Zuweisung
innerhalb einer Funktion vornimmt:
my_median <- function(df, col){
col_q = enquo(col)
col_name_md_q = paste0(quo_name(col_q),"_md")
df %>%
summarise(!!(col_name_md_q) := median(!!(col_q),
na.rm = TRUE))
}
my_median(extra, i01)
#> # A tibble: 1 x 1
#>
i01_md
#>
<dbl>
#> 1
3
Neu ist noch die Funktion quo_name(); sie konvertiert einen Ausdruck in einen String:
string <- "Das ist ein Text"
string2 <- quo_name(quo(string))
string2
#> [1] "string"
Ein wichtiger Test für unsere Funktion my_median() steht noch aus: Fügt sie sich klaglos in eine Abfolge anderer dplyr-Befehle, mit der Pfeife verbunden, wie üblich?
extra %>%
group_by(sex) %>%
my_median(extra_mean)
#> # A tibble: 3 x 2
#>
sex
extra_mean_md
#>
<chr>
<dbl>
#> 1 Frau
2.9
#> 2 Mann
3
#> 3 <NA>
3
Gut, das funktioniert.
29.5 Beispiele für NSE-Funktionen
29.5
537
Beispiele für NSE-Funktionen
Erstellen wir eine Funktion modus(), die die häufigste Ausprägung eines Vektors zurückliefert. Im Gegensatz zu anderen dplyr-Funktionen soll diese Funktion einen Vektor als Eingabe und einen Skalar als Aufgabe verlangen, damit sie sich in die Funktion
summarise() einfügt.
modus <- function(df, col){
col_q <- enquo(col)
df %>%
count(!!col_q, sort = TRUE) %>%
slice(1) %>%
pull(1)
}
extra %>%
summarise(modus_i1 = modus(., i01))
#> # A tibble: 1 x 1
#>
modus_i1
#>
<int>
#> 1
3
Übersetzen wir diese Funktion ins Deutsche:
Definiere modus() als Funktion mit den Parametern df und col
Zitiere col und weise das Ergebnis col_q zu
Nimm df UND DANN
zähle die Ausprägungen von der evaluierten Variablen col_q und sortiere das
Ergebnis UND DANN
schneide die 1. Zeile heraus UND DANN
ziehe die erste Spalte als Vektor heraus.
Manche Funktionen sind so flexibel, dass sie mit (praktisch) beliebig vielen Argumenten umgehen können. group_by() ist so ein Beispiel: Man kann die Funktion mit nur
einem Argument bestücken, z. B. group_by(sex), aber auch mit zwei oder mehr Argumenten, etwa group_by(sex, smoker). Wie programmiert man eine Funktion, die
diese Flexibilität berücksichtigt? Ein Weg sieht so aus:
my_median_grouped <- function(df, col, ...){
col_q <- enquo(col)
groups_q <- enquos(...)
538
29 Programmieren mit dplyr
df %>%
group_by(!!!groups_q) %>%
summarise(md = median(!!(col_q), na.rm = TRUE))
}
extra %>%
my_median_grouped(i01, sex, smoker)
#> # A tibble: 7 x 3
#> # Groups:
sex [?]
#>
sex
smoker
md
#>
<chr> <chr>
<dbl>
#> 1 Frau Gelegentlich (z. B. auf Partys)
3
#> 2 Frau Ja
4
#> 3 Frau Nein
3
#> 4 Frau <NA>
3
#> 5 Mann Ja
4
#> 6 Mann <NA>
3
#> 7 <NA> <NA>
3
Man beachte, dass enquos() ein Plural-S am Ende hat; damit wird ausgedrückt, dass die
Funktionen mehrere Ausdrücke zitiert. So wie !! (Bang-Bang) einen Ausdruck unzitiert,
unzitiert !!!() (Bang-Bang-Bang) mehrere Ausdrücke, die jeweils als Argument der
aufrufenden Funktion übergeben werden.
Eine andere Sache, die mir schon häufiger gefehlt hat, ist, dass count() keine Anteile
ausgibt, nur absolute Häufigkeiten. Schreiben wir eine Funktion, die neben den absoluten
Häufigkeiten auch die relativen Häufigkeiten (Anteile) ausgibt:
count_prop <- function(df, ...){
groups_q <- enquos(...)
df %>%
count(!!!groups_q) %>%
mutate(prop = round(n / sum(n), 2))
}
extra %>%
count_prop(sex, smoker)
#> # A tibble: 7 x 4
#>
sex
smoker
n prop
#>
<chr> <chr>
<int> <dbl>
#> 1 Frau Gelegentlich (z. B. auf Partys)
1 0
#> 2 Frau Ja
1 0
#> 3 Frau Nein
8 0.01
#> 4 Frau <NA>
519 0.63
#> 5 Mann Ja
2 0
#> 6 Mann <NA>
284 0.34
#> 7 <NA> <NA>
11 0.01
29.5 Beispiele für NSE-Funktionen
539
80
Frau
Mann
NA
50
60
40
count
count
40
30
20
20
10
0
0
1
2
3
4
extra_mean
1
2
3
4 1
2
3
4 1
2
3
4
extra_mean
Abb. 29.1 ggplot-Diagramme erzeugt mit NSE
29.5.1
Funktionen für ggplot
Auch ggplot2 nutzt NSE; möchte man eine Funktion mit ggplot schreiben, muss man
die hier diskutierten Verfahren anwenden. Sagen wir, wir möchten eine Funktion schreiben, die ein Histogramm für eine numerische Variable col ausgibt. Das kann so aussehen
wie in Abb. 29.1, linker Teil.
gg_fun <- function(data, col)
{
col_q <- enquo(col)
p <- ggplot(data,
aes_q(x = col_q))
p + geom_histogram()
}
p1 <- gg_fun(extra, extra_mean)
aes_q definiert das Mapping (Zuordnung von Spalten zu Achsen etc.) über eine Zitierung
(quote, daher _q). Wiederum müssen wir die Variable, die der Nutzer tippte, zitieren (mit
enquo()) und diesen Ausdruck dann evaluieren – genau, wie wir es schon einmal getan haben. Möchten wir das Histogramm noch in Facetten aufbrechen entsprechend einer
Gruppierungsvariablen group, kann das so geschehen (Abb. 29.1, rechter Teil):
gg_fun2 <- function(data, col, group)
{
p <- ggplot(data,
aes_q(x = enquo(col))) +
facet_wrap(as.formula(enquo(group))) +
geom_histogram()
}
p2 <- gg_fun2(extra, extra_mean, sex)
540
29 Programmieren mit dplyr
Bei facet_wrap() haben wir das gleiche Prinzip wie eben angewendet, mit dem einzigen Unterschied, das Objekt noch in eine Formel umzuwandeln, da facet_wrap()
Objekte vom Formel-Typ versteht.
Aufgaben
Richtig oder falsch?3
1.
Leider funktionieren die dplyr-Befehle nicht, wenn man sie programmiert, d. h.
innerhalb von Funktionen verwendet.
2. dplyr-Befehle arbeiten nach dem Prinzip der Nonstandard Evaluation, was immer das ist.
3. Sei ggplot2 <- "dplyr"; dann startet der Aufruf library(ggplot2)
dplyr, nicht ggplot2.
4. quo(eier+schmalz) wird nicht zu einem Fehler führen, wenn die Objekte
eier und schmalz unbekannt sind.
5. Unter einer Quosure versteht man einen (zitierten) Ausdruck, der sich die zugehörige Umgebung merkt.
6. Mit dem Bang-Bang-Operator kann man den Namen eines „Rezepts“ in seine
„Zutaten“ übersetzen.
7. Wenn wir Oma fragen oma(!!zuckerguss_rezept), antwortet sie
ˆbutter + zitrone + zucker.
8. Das Hütchen (Circumflex) in ˆbutter + zitrone + zucker zeigt an,
dass es sich bei der Ausgabe um eine Zitierung handelt.
9. In Funktionen muss man anstelle von enquo() stets quo() verwenden.
10. Möchte man einen Ausdruck in einen String umwandeln, so kann man
quo_name(expr) verwenden.
3
R, R, F, F, R, R, R, R, F, F.
Anhang A
A.1 Grundlagenthemen
Möchte man einen Einstieg in die moderne Datenanalyse finden oder lehren, so ist ein
Überblick über Grundlagenthemen sinnvoll. Diese Themen erscheinen zum Einstieg ratsam:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
Kap. 1 Statistik heute
Kap. 3 R starten
Kap. 4 Erstkontakt
Abschn. 5.1 Überblick über die wichtigsten Objekttypen
Kap. 6 Datenimport und -export
Kap. 7 Datenjudo
Kap. 8 Deskriptive Statistik
Kap. 11 Grundlagen der Datenvisualisierung mit ggplot2
Kap. 15 Grundlagen des Modellierens
Kap. 16 Inferenzstatistik
Kap. 18 Lineare Modelle
Kap. 24 Textmining
Natürlich kann es im Einzelfall sinnvoll sein, nicht alle Abschnitte dieser Kapitel zu lernen
bzw. lehren. So könnte man zum Beispiel auf die Abschn. 7.5 und 7.6 verzichten. Letztlich
ist eine Auswahl ein Stück weit subjektiv. Diese Auswahl berücksichtigt grundlegende
Themen zu R, der Datenanalyse, der Inferenzstatistik, des statistischen Modellierens sowie „moderne“ Aspekte wie Textmining. Die übrigen Kapitel dieses Buches sind demnach
fortgeschrittenere Themen. Am anspruchsvollsten sind die Kapitel des Teils Rahmen 2:
27, 28 und 29.
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2019
S. Sauer, Moderne Datenanalyse mit R, FOM-Edition,
https://doi.org/10.1007/978-3-658-21587-3
541
542
Anhang A
A.2 Übungsaufgaben
Die folgenden Aussagen sind entweder als „richtig“ oder als „falsch“ zu bewerten. Offene
Fragen verlangen einen Text als Antwort.
A.2.1 Fragen
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
Bei install.packages spielt der Parameter dependencies = TRUE in der
Praxis keine Rolle.
Dateien mit der Endung .R sind keine Textdateien.
Der Befehl read.csv kann auch Dateien einlesen, die nicht lokal, sondern auf einem
Server im Internet gespeichert sind.
Fehlende Werte werden in R durch NA kodiert.
Um Variablen einen Wert zuzuweisen, kann man in R den Zuweisungspfeil <- verwenden.
Die deutsche Version von R verwendet im Standard das Komma als Dezimaltrennzeichen.
Statistisches Modellieren verwendet die Abduktion als zentrale Denkfigur.
Eine Abduktion führt zu sicheren Schlüssen.
Das CSV-Format ist identisch mit dem Excel-Format, was sich auch darin zeigt, dass
Excel CSV-Dateien oft problemlos öffnet.
Das Arbeitsverzeichnis (engl. working directory) ist der Ort, an dem R eine Datei, die
Sie aufrufen, vermutet – sofern kein anderer Pfad angegeben ist.
In einer Tabelle in Normalform steht in jeder Zeile eine Variable und in jeder Spalte
eine Beobachtung.
Die Funktion filter() filtert Spalten aus einer Tabelle.
Die Funktion select() lässt Spalten sowohl anhand ihres Namens als auch ihrer
Nummer (Position in der Tabelle) auswählen.
Die Funktion group_by() gruppiert eine Tabelle anhand der Werte einer diskreten
Variablen.
Die Funktion group_by() akzeptiert nur Faktorvariablen als Gruppierungsvariablen.
Die Funktion summarise() darf nur für Funktionen verwendet werden, welche genau einen Wert zurückliefern.
Was sind drei häufige Operationen der Datenaufbereitung?
Um Korrelationen mit R zu berechnen, kann man die Funktion corrr::
correlate() verwenden.
corrr::correlate() liefert stets einen Dataframe zurück.
Anhang A
543
20. Tibbles sind eine spezielle Art von Dataframes.
21. Was zeigt uns „Anscombes Quartett“?
22. ggplot() unterscheidet drei Bestandteile eines Diagramms: Daten, Geome und
Transformationen.
23. Um eine kontinuierliche Variable zu plotten, wird häufig ein Histogramm verwendet.
24. Das Geom tile zeigt drei Variablen.
25. Geleitetes Modellieren kann unterteilt werden in prädiktives und explikatives Modellieren.
26. Der Befehl scale() verschiebt den Mittelwert einer Verteilung auf 0 und skaliert
die SD auf 1.
27. Mit „Binnen“ im Sinne der Datenanalyse ist gemeint, eine kategoriale Variable in
eine kontinuierliche zu überführen.
28. Die Gleichung y = ax + b lässt sich in R darstellen als y ~ ax + b.
29. R2 , auch Bestimmtheitsmaß oder Determinationskoeffizient genannt, gibt die Vorhersagegüte im Verhältnis zu einem Nullmodell an.
30. Bei der logistischen Regression gilt: Bei ˇ0 > 0 ist die Wahrscheinlichkeit kleiner
als 50 %, dass das modellierte Ereignis eintritt, wenn alle anderen Prädiktoren 0 sind.
31. Die logistische Regression sollte nicht verwendet werden, wenn die abhängige Variable dichotom ist.
32. Die logistische Regression stellt den Zusammenhang zwischen Prädiktor und Kriterium nicht mit einer Geraden, sondern mit einer s-förmigen Kurve dar.
33. Bevor die Koeffizienten der logistischen Regression als Odds Ratio interpretiert werden können, müssen sie delogarithmiert werden.
34. Unter delogarithmieren versteht man, die Umkehrfunktion der e-Funktion auf eine
Gleichung anzuwenden.
35. Wendet man die normale Regression an, um eine dichotome Variable als Kriterium
zu modellieren, so kann man Wahrscheinlichkeiten größer als 1 und kleiner als 0
bekommen.
36. Eine typische Idee der Clusteranalyse lautet, die Varianz innerhalb der Cluster jeweils
zu maximieren.
37. Bei einer k-means-Clusteranalyse darf man nicht die Anzahl der Cluster vorab festlegen; vielmehr ermittelt der Algorithmus die richtige Anzahl der Cluster.
38. Für die Wahl der „richtigen“ Anzahl der Cluster kann das Ellbogen-Kriterium als
Entscheidungsgrundlage herangezogen werden.
39. Ein Screeplot stellt die Varianz innerhalb der Cluster als Funktion der Anzahl der
Cluster dar (im Rahmen der Clusteranalyse).
40. Die euklidische Distanz zwischen zwei Objekten in der Ebene lässt sich mit dem Satz
des Pythagoras berechnen.
544
Anhang A
A.2.2 Lösungen
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.
Falsch
Falsch
Richtig
Richtig
Richtig
Falsch
Richtig
Falsch
Falsch
Richtig
Falsch
Falsch
Falsch
Richtig
Richtig
Falsch
Richtig
Auf fehlende Werte prüfen, Fälle mit fehlenden Werten löschen, fehlende Werte ggf.
ersetzen, nach Fehlern suchen, Ausreißer identifizieren, hochkorrelierte Variablen finden, z-standardisieren, Quasi-Konstante finden, auf Normalverteilung prüfen, Werte
umkodieren und „binnen“.
Richtig
Richtig
Richtig
Es geht hier um vier Datensätze mit zwei Variablen (Spalten; X und Y). Offenbar sind
die Datensätze praktisch identisch: Alle X haben den gleichen Mittelwert und die
gleiche Varianz; dasselbe gilt für die Y. Die Korrelation zwischen X und Y ist in allen
vier Datensätzen gleich. Allerdings erzählt eine Visualisierung der vier Datensätze
eine ganz andere Geschichte.
Falsch
Richtig
Richtig
Falsch
Richtig
Falsch
Richtig
Richtig
Falsch
Falsch
Anhang A
545
33. Richtig
34. Richtig
35. Falsch. Richtig wäre: Die Umkehrfunktion des Logarithmus, also die e-Funktion, auf
eine Gleichung anzuwenden.
36. Richtig
37. Falsch
38. Falsch. Richtig wäre: Man gibt die Anzahl der Cluster vor. Dann vergleicht man die
Varianz innerhalb der verschiedenen Lösungen.
39. Richtig
40. Richtig
41. Richtig
A.2.3 Hinweise
RStudio und Shiny sind eingetragene Markennamen von RStudio, Inc.
A.2.4 Beispiel-Curriculum
Für eine Einführungsveranstaltung könnte der Stoff wie folgt in zwölf Termine zu je drei
Zeit-Stunden aufgeteilt werden (s. Abschn. A.1). Mit diesem Zeitumfang kann nur ein Teil
des Buches vermittelt werden.
Tab. A.1 Zuordnung von
Themen zu Terminen
Termin
1
1
1
1
2
2
3
4
5
6
7
8
9
10
11
12
Thema (Kapitel)
Organisatorisches
Einführung
Arbeiten mit R
Daten einlesen
Datenjudo
Deskriptive Statistik
Daten visualisieren
Fallstudie (z. B. zu „movies“)
Daten modellieren
Inferenzstatistik
Lineare Regression – metrisch
Lineare Regression – kategorial
Fallstudie (z. B. zu „titanic“)
Clusteranalyse
Textmining
Wiederholung
546
Anhang A
A.3 Verwendete R-Pakete
Die folgenden R-Pakete wurden in diesem Buch verwendet:
Stanley (2018), Morey und Rouder (2015), Beaujean (2012), Canty und Ripley (2017),
Gerlanc und Kirby (2015), Robinson (2018), Fox und Weisberg (2011), Jed Wing et
al. (2018), Maechler et al. (2018), Wei und Simko (2017), Jackson (2016), Wilke
(2017), Wickham et al. (2018), Analytics & Weston (2017), Wickham et al. (2017),
Winston Chang (2014), Ren und Russell (2016), Schloerke et al. (2017), Kahle und Wickham (2013), Jeppson et al. (2017), Wickham (2016), Wickham (2015), Arnold (2018),
Gesmann und de Castillo (2011), Warnes et al. (2016), Auguie (2017), Hugh-Jones
(2018), Buchta und Hornik (2017), Zhu (2018), Xie (2018), Meschiari (2015), Cheng
et al. (2017), Bryer und Speerschneider (2016), Wild (2015), Navarro (2015), Bache
und Wickham (2014), W. N. Venables und Ripley (2002), van Buuren und GroothuisOudshoorn (2011), Wickham (2017a), Pruim et al. (2017), Raiche (2010), Daróczi und
Tsegelskyi (2017), Aust und Barth (2017), Hothorn und Zeileis (2015), Ooms (2018),
Soetaert (2017), Sachs (2017), Urbanek (2013), Sauer (2017a), Sauer (2018), Robin et
al. (2011), Wickham (2018a), Revelle (2018), Champely (2018), Neuwirth (2014), Henry
und Wickham (2018), Therneau und Atkinson (2018), South (2011), Wickham (2018b),
VanDerWal et al. (2014), Pebesma (2018), McNamara et al. (2018), Bouchet-Valat (2014),
Hlavac (2018), Wickham (2018c), Wickham und Henry (2018), Wickham (2017b), Gentry
(2015), Kowarik und Templ (2016), Garnier (2018), Ram und Wickham (2016), Fellows
(2014), Dahl (2016), Sauer (2017b).
Literatur
AfD (2016). Party platform („Parteiprogramm“) of the AfD party („Alternative for Germany“) as of
2016-05-01. Web page. https://www.afd.de/.
Agresti, A. (2013). Categorical data analysis. Hoboken, N.J: Wiley.
American Psychological Association (2009). Publication Manual of the American Psychological
Association, 6th Edition. American Psychological Association (APA). https://www.amazon.com/
Publication-Manual-American-Psychological-Association/dp/1433805618?SubscriptionId=0JY
N1NVW651KCA56C102&tag=techkie-20&linkCode=xm2&camp=2025&creative=165953&cr
eativeASIN=1433805618.
Analytics R., & Weston, S. (2017). doMC: Foreach Parallel Adaptor for ’parallel’. https://CRAN.
R-project.org/package=doMC.
Anscombe, F. J. (1973). Graphs in Statistical Analysis. The American Statistician, 27(1), 17–21.
https://doi.org/10.1080/00031305.1973.10478966.
Arnold, J. B. (2018). ggthemes: Extra Themes, Scales and Geoms for ’ggplot2’. https://CRAN.Rproject.org/package=ggthemes.
Aronson, E., Akert, R. M., & Wilson, T. D. (2010). Sozialpsychologie. München: Pearson.
Auguie, B. (2017). gridExtra: Miscellaneous Functions for „Grid“ Graphics. https://CRAN.Rproject.org/package=gridExtra.
Aust, F., & Barth, M. (2017). papaja: Create APA manuscripts with R Markdown. https://github.
com/crsh/papaja.
Azad, K. (2013). Math, Better Explained. Amazon.
Bache, S. M., & Wickham, H. (2014). magrittr: A Forward-Pipe Operator for R. https://CRAN.Rproject.org/package=magrittr.
Banerjee, M., Capozzoli, M., McSweeney, L., & Sinha, D. (1999). Beyond kappa: A review of
interrater agreement measures. Canadian Journal of Statistics, 27(1), 3–23. https://doi.org/10.
2307/3315487.
Baron, R. M., & Kenny, D. A. (1986). The moderator-mediator variable distinction in social psychological research: Conceptual, strategic, and statistical considerations. Journal of Personality and
Social Psychology, 51(6), 1173–1182.
Baumer, B., Kaplan, D. T., & Horton, N. J. (2017). Modern Data Science with R. Boca Raton, Florida: Chapman; Hall/CRC.
Beaujean, A. A. (2012). BaylorEdPsych: R Package for Baylor University Educational Psychology
Quantitative Courses. https://CRAN.R-project.org/package=BaylorEdPsych.
Begley, C. G., & Ioannidis, J. P. (2015). Reproducibility in Science. Circulation Research, 116(1),
116–126. https://doi.org/10.1161/CIRCRESAHA.114.303819.
547
548
Literatur
Benjamin, D. J., Berger, J., Johannesson, M., Nosek, B., Wagenmakers, E.-J., Berk, R., Johnson,
V., et al. (2017). Redefine Statistical Significance. Nature Human Behaviour. https://doi.org/10.
1038/s41562-017-0189-z.
Benjamin, D. J., Berger, J. O., Johannesson, M., Nosek, B. A., Wagenmakers, E.-J., Berk, R., et al.
(2018). Redefine statistical significance. Nature Human Behaviour, 2(1), 6. https://doi.org/10.
1038/s41562-017-0189-z.
Bishop, C. M. (2006). Pattern Recognition and Machine Learning. New York: Springer.
Bogen, J., & Woodward, J. (1988). Saving the phenomena. The Philosophical Review, 97(3), 303–
352.
Bortz, J. (2013). Statistik: Für Sozialwissenschaftler. Heidelberg: Springer.
Bosco, F. A., Aguinis, H., Singh, K., Field, J. G., & Pierce, C. A. (2015). Correlational effect size
benchmarks. Journal of Applied Psychology, 100(2), 431–449. https://doi.org/10.1037/a0038047.
Botsman, R. (2017, Oktober). Big data meets Big Brother as China moves to rate its citizens. http://
www.wired.co.uk/article/chinese-government-social-credit-score-privacy-invasion.
Bouchet-Valat, M. (2014). SnowballC: Snowball stemmers based on the C libstemmer UTF-8 library. https://CRAN.R-project.org/package=SnowballC.
Breiman, L., Friedman, J., Stone, C. J., & Olshen, R. A. (1984). Classification and regression trees.
Boca Raton, Florida: CRC press.
Brennan, R. L., & Prediger, D. J. (1981). Coefficient Kappa: Some Uses, Misuses, and Alternatives. Educational and Psychological Measurement, 41(3), 687–699. https://doi.org/10.1177/
001316448104100307.
Bresler, A. (2015, Januar). „Recreating Edward Tufte’s New York City Weather Visualization“.
https://gist.github.com/abresler/46c36c1a88c849b94b07.
Briggs, W. M. (2008). Breaking the Law of Averages: Real-Life Probability and Statistics in Plain
English. Morrisville, NC: Lulu.com.
Briggs, W. M. (2016). Uncertainty: The Soul of Modeling, Probability & Statistics. New York:
Springer.
Brown, J. D. (2015). Linear Models in Matrix Form: A Hands-On Approach for the Behavioral
Sciences. New York: Springer.
Brown, E. N., & Kass, R. E. (2009). What is statistics? The American Statistician, 63(2), 105–110.
Bryer, J., & Speerschneider, K. (2016). likert: Analysis and Visualization Likert Items. https://
CRAN.R-project.org/package=likert.
Brynjolfsson, E., & McAfee, A. (2016). The Second Machine Age: Work, Progress, and Prosperity
in a Time of Brilliant Technologies. New York, NY: W. W. Norton & Company. https://www.
amazon.com/Second-Machine-Age-Prosperity-Technologies/dp/0393350649?SubscriptionId=0
JYN1NVW651KCA56C102&tag=techkie-20&linkCode=xm2&camp=2025&creative=165953
&creativeASIN=0393350649.
Buchta, C., & Hornik, K. (2017). ISOcodes: Selected ISO Codes. https://CRAN.R-project.org/
package=ISOcodes.
Bundeswahlleiter (2017a). Strukturdaten für die Wahlkreise. https://www.bundeswahlleiter.de/dam/
jcr/f7566722-a528-4b18-bea3-ea419371e300/btw17_strukturdaten.csv.
Bundeswahlleiter (2017b). Ergebnisse der Bundestagswahl 2017. https://www.bundeswahlleiter.de/
bundestagswahlen/2017.html. Zugegriffen: 1. Okt. 2017.
Bureau of transportation statistics RITA (2013). nycflights13. http://www.transtats.bts.gov/DL_
SelectFields.asp?Table_ID=236.
Burns, P. (2012). The R Inferno. Morrisville, NC: lulu.com.
van Buuren, S. (2012). Flexible Imputation of Missing Data. Boca Raton, Fl: Chapman; Hall/CRC.
van Buuren, S., & Groothuis-Oudshoorn, K. (2011). mice: Multivariate Imputation by Chained
Equations in R. Journal of Statistical Software, 45(3), 1–67. http://www.jstatsoft.org/v45/i03/.
Literatur
549
Bühner, M. (2011). Einführung in die Test- und Fragebogenkonstruktion. Hallbergmoos: Pearson.
Canty, A., & Ripley, B. D. (2017). boot: Bootstrap R (S-Plus) Functions.
Carsey, T. M., & Harden, J. J. (2013). Monte Carlo simulation and resampling methods for social
science. Los Angeles: SAGE.
Chacon, S., & Straub, B. (2014). Pro Git. New York City, NY: Apress.
Chambers, C. (2017). The Seven Deadly Sins of Psychology: A Manifesto for Reforming the Culture
of Scientific Practice. Princeton, CA: Princeton University Press.
Champely, S. (2018). pwr: Basic Functions for Power Analysis. https://CRAN.R-project.org/
package=pwr.
Cheng, J., Karambelkar, B., & Xie, Y. (2017). leaflet: Create Interactive Web Maps with the JavaScript ’Leaflet’ Library. https://CRAN.R-project.org/package=leaflet.
Chui, M., Manyika, J., Miremadi, M., Henke, N., Chung, R., Nel, P., & Malhotra, A. S. (2018).
Notes from the AI frontier. Report by McKinsey & Company. https://www.mckinsey.com/
featured-insights/artificial-intelligence/notes-from-the-ai-frontier-applications-and-value-ofdeep-learning.
Cleveland, W. S. (1993). Visualizing Data. Summit, NJ: Hobart Press.
Clopper, C. J., & Pearson, E. S. (1934). The use of confidence or fiducial limits illustrated in the case
of the binomial. Biometrika, 26(4), 404–413.
Cobb, G. W. (2007). The introductory statistics course: a Ptolemaic curriculum? Technology Innovations in Statistics Education, 1(1).
Cobb, G. W. (2015). Mere Renovation is Too Little Too Late: We Need to Rethink Our Undergraduate Curriculum from the Ground Up. The American Statistician, 69(4), 266–282. https://doi.
org/10.1080/00031305.2015.1093029.
Cohen, J. (1988). Statistical Power Analysis for the Behavioral Sciences. Abingdon-on-Thames,
UK: Routledge. https://doi.org/10.4324/9780203771587.
Cohen, J. (1992). A power primer. Psychological Bulletin, 112(1), 155–159.
Cohen, J., Cohen, P., West, S. G., & Aiken, L. S. (2013). Applied multiple regression/correlation
analysis for the behavioral sciences. Boca Raton, Florida: Routledge.
Dahl, D. B. (2016). xtable: Export Tables to LaTeX or HTML. https://CRAN.R-project.org/
package=xtable.
Daróczi, G., & Tsegelskyi, R. (2017). pander: An R ’Pandoc’ Writer. https://CRAN.R-project.org/
package=pander.
Deutsche Welle (2017). Liste von Twitter-Accounts deutscher Politiker. https://twitter.com/dw_
politics/lists/german-politicians/members?lang=en.
Diez, D. M., Barr, C. D., & Cetinkaya-Rundel, M. (2014). Introductory Statistics with Randomization and Simulation. North Charleston, SC: CreateSpace.
Diez, D. M., Barr, C. D., & Cetinkaya-Rundel, M. (2015). OpenIntro statistics. CreateSpace. https://
www.openintro.org/.
Dodge, Y. (Hrsg.) (2006). The Oxford Dictionary of Statistical Terms. Oxford University Press.
Durand, M. (2015). The OECD Better Life Initiative: How’s Life? and the Measurement of WellBeing. Review of Income and Wealth, 61(1), 4–17. https://doi.org/10.1111/roiw.12156.
Efron, B., & Tibshirani, R. (1994). An Introduction to the Bootstrap. Boca Raton, FL: Taylor &
Francis. https://books.google.de/books?id=gLlpIUxRntoC.
Eid, M., Gollwitzer, M., & Schmitt, M. (2010). Statistik und Forschungsmethoden. Göttingen: Hogrefe.
Fair, R. C. (1978). A theory of extramarital affairs. Journal of Political Economy, 86(1), 45–61.
Farrell, H. (2012, Juli). Milton Friedman’s Thermostat. Blog Post. http://themonkeycage.org/2012/
07/milton-friedmans-thermostat/.
Fellows, I. (2014). wordcloud: Word Clouds. https://CRAN.R-project.org/package=wordcloud.
550
Literatur
Fisher, R. A. (1955). Statistical methods and scientific induction. Journal of the Royal Statistical
Society. Series B (Methodological), 17(1), 69–78.
Fisher, R. A. (2008). Photograph. http://www.swlearning.com/quant/kohler/stat/biographical_
sketches/Fisher_3.jpeg.
Flach, P., & Hadjiantonis, A. (2013). Abduction and Induction: Essays on their Relation and Integration. https://books.google.de/books?id=E7fnCAAAQBAJ.
Flick, U. (2007). Qualitative Sozialforschung: eine Einführung. Berlin: Rowohlt. http://books.
google.de/books?id=zvISAQAAMAAJ.
Fonticons (2018). Icons ‘check-square-regular’, ‘laptop-solid’, and ‘bell-regular’ from Font Awesome. https://fontawesome.com/license/free
Foucault, M. (1994). Überwachen und Strafen. Die Geburt des Gefängnisses. Frankfurt: Suhrkamp.
Fox, J. (2005). The R Commander: A Basic Statistics Graphical User Interface to R. Journal of
Statistical Software, 14(9), 1–42. http://www.jstatsoft.org/v14/i09.
Fox, J. (2016). Using the R Commander: A Point-and-Click Interface for R. Chapman & Hall/CRC
The R Series. Cambridge, MA: Chapman; Hall/CRC.
Fox, J., & Weisberg, S. (2011). An R Companion to Applied Regression (Second.). Thousand Oaks
CA: SAGE. http://socserv.socsci.mcmaster.ca/jfox/Books/Companion.
Franco, A., Malhotra, N., & Simonovits, G. (2014). Publication bias in the social sciences:
Unlocking the file drawer. Science, 345(6203), 1502–1505. https://doi.org/10.1126/science.
1255484.
Freedman, D., Pisani, R., & Purves, R. (2007). Statistics (4. Aufl.). New York City, NY: W. W.
Norton & Company.
Gansser, O. (2017). Data for Principal Component Analysis and Common Factor Analysis. Data set.
Open Science Framework. osf.io/zg89r.
Garcia, J., & Quintana-Domeque, C. (2007). The evolution of adult height in Europe: a brief note.
Economics and human biology, 5(2), 340–349.
Garnier, S. (2018). viridis: Default Color Maps from ’matplotlib’. https://CRAN.R-project.org/
package=viridis.
Gelman, A. (2014). The Fallacy of Placing Confidence in Confidence Intervals. Blog Post. http://
andrewgelman.com/2014/12/11/fallacy-placing-confidence-confidence-intervals/.
Gentry, J. (2015). twitteR: R Based Twitter Client. https://CRAN.R-project.org/package=twitteR.
GeoBasis-DE, B. K. G. (2017). Verwaltungsgebiete 1 : 250 000. Web page. http://www.bkg.bund.
de.
Gerlanc, D., & Kirby, K. (2015). bootES: Bootstrap Effect Sizes. https://CRAN.R-project.org/
package=bootES.
Gesmann, M., & de Castillo, D. (2011). googleVis: Interface between R and the Google Visualisation API. The R Journal, 3(2), 40–44. https://journal.r-project.org/archive/2011-2/RJournal_20112_Gesmann+de~Castillo.pdf.
Gigerenzer, G. (1980). Messung und Modellbildung in der Psychologie. Stuttgart: UTB.
Gigerenzer, G. (2004). Mindless statistics. The Journal of Socio-Economics, 33(5), 587–606. https://
doi.org/10.1016/j.socec.2004.09.033.
God (2016). I don’t care about you. Please share this with friends. TheTweetOfGod. Twitter Tweet.
https://twitter.com/TheTweetOfGod/status/688035049187454976.
Gower, J. C. (1971). A general coefficient of similarity and some of its properties. Biometrics, 27(4),
857–871.
Grolemund, G., & Wickham, H. (2014). A cognitive interpretation of data analysis. International
Statistical Review, 82(2), 184–204.
Haig, B. D. (2014). Investigating the psychological world. Cambridge, MA: MIT Press.
Literatur
551
Han, J., Kamber, M., & Pei, J. (2011). Data Mining: Concepts and Techniques (3. Aufl.). Burlington,
Massachusetts: Morgan Kaufmann.
Hardin, J., Hoerl, R., Horton, N. J., Nolan, D., Baumer, B., Hall-Holt, O., et al. (2015). Data science
in statistics curricula: Preparing students to ’Think with Data’. The American Statistician, 69(4),
343–353.
Hastie, T., Tibshirani, R., & Friedman, J. (2013). The Elements of Statistical Learning: Data Mining,
Inference, and Prediction. Bd. 1. New York City: Springer. https://doi.org/10.1007/b94608.
Hawkins, D. M., Basak, S. C., & Mills, D. (2003). Assessing model fit by cross-validation. Journal
of chemical information and computer sciences, 43(2), 579–586.
Head, M. L., Holman, L., Lanfear, R., Kahn, A. T., & Jennions, M. D. (2015). The Extent and Consequences of P-Hacking in Science. PLOS Biology, 13(3), e1002106. https://doi.org/10.1371/
journal.pbio.1002106.
Henrich, J., Heine, S. J., & Norenzayan, A. (2010). The Weirdest People in the World? Behavioral
and Brain Sciences, 33(2-3), 61–83.
Henry, L., & Wickham, H. (2018). rlang: Functions for Base Types and Core R and ’Tidyverse’
Features. https://CRAN.R-project.org/package=rlang.
Hlavac, M. (2018). stargazer: Well-Formatted Regression and Summary Statistics Tables. Bratislava, Slovakia: Central European Labour Studies Institute (CELSI). https://CRAN.R-project.org/
package=stargazer.
Hothorn, T., & Zeileis, A. (2015). partykit: A Modular Toolkit for Recursive Partytioning in R.
Journal of Machine Learning Research, 16, 3905–3909. http://jmlr.org/papers/v16/hothorn15a.
html.
Hugh-Jones, D. (2018). huxtable: Easily Create and Style Tables for LaTeX, HTML and Other
Formats. https://CRAN.R-project.org/package=huxtable.
Hunt, A., & Thomas, D. (1999). The Pragmatic Programmer: From Journeyman to Master. Boston,
MA: Addison-Wesley Professional.
Hyndman, R. J. (2014). To explain or predict? Blog Post. https://robjhyndman.com/hyndsight/toexplain-or-predict/.
Ihaka, R., & Gentleman, R. (1996). R: A Language for Data Analysis and Graphics. Journal of
Computational and Graphical Statistics, 5(3), 299–314. https://doi.org/10.1080/10618600.1996.
10474713.
Jackson, S. (2016). corrr: Correlations in R. https://CRAN.R-project.org/package=corrr.
James, G., Witten, D., Hastie, T., & Tibshirani, R. (2013). An Introduction to Statistical Learning.
Bd. 6. New York City, New York: Springer.
Jaynes, E. T. (2003). Probability theory: The logic of science. Cambridge, MA: Cambridge University Press.
from Jed Wing, M. K. C., Weston, S., Williams, A., Keefer, C., Engelhardt, A., Cooper, T., Hunt,
T., et al. (2018). caret: Classification and Regression Training. https://CRAN.R-project.org/
package=caret.
Jeppson, H., Hofmann, H., & Cook, D. (2017). ggmosaic: Mosaic Plots in the ’ggplot2’ Framework.
https://CRAN.R-project.org/package=ggmosaic.
Kahle, D., & Wickham, H. (2013). ggmap: Spatial Visualization with ggplot2. The R Journal, 5(1),
144–161. http://journal.r-project.org/archive/2013-1/kahle-wickham.pdf.
Kashnitsky, I. (2017). Subplots in maps with ggplot2. Blog post. https://ikashnitsky.github.io/2017/
subplots-in-maps/.
Kass, R. E., & Raftery, A. E. (1995). Bayes Factors. Journal of the American Statistical Association,
90(430), 773–795. https://doi.org/10.1080/01621459.1995.10476572.
Kerby, D. S. (2014). The Simple Difference Formula: An Approach to Teaching Nonparametric
Correlation. Comprehensive Psychology, 3(11.IT.3.1) https://doi.org/10.2466/11.IT.3.1.
552
Literatur
Kershaw, I. (2015). To Hell and Back: Europe, 1914–1949 (Alan Lane History). London: Allen
Lane.
Keynes, J. M. (2013). A treatise on probability. London: Courier Corporation.
Kim, A. Y., & Escobedo-Land, A. (2015). OkCupid Data for Introductory Statistics and Data
Science Courses. Journal of Statistics Education, 23(2), n2.
Kirby, K. N., & Gerlanc, D. (2013). BootES: An R package for bootstrap confidence intervals on
effect sizes. Behavior Research Methods, 45(4), 905–927. https://doi.org/10.3758/s13428-0130330-5.
Kosinski, M., Stillwell, D., & Graepel, T. (2013). Private traits and attributes are predictable from
digital records of human behavior. Proceedings of the National Academy of Sciences, 110(15),
5802–5805.
Kowarik, A., & Templ, M. (2016). Imputation with the R Package VIM. Journal of Statistical Software, 74(7), 1–16. https://doi.org/10.18637/jss.v074.i07.
Krämer, W. (2011). Wie wir uns von falschen Theorien täuschen lassen. Berlin University Press.
https://books.google.de/books?id=HWUKaAEACAAJ.
Krugman, P. (2013). The Excel Depression. The New York Times, A31.
Kruschke, J. (2014). Doing Bayesian Data Analysis, Second Edition: A Tutorial with R, JAGS, and
Stan. Cambridge, MA: Academic Press.
Kuhn, M., & Johnson, K. (2013). Applied predictive modeling. New York: Springer.
Liaw, A., & Wiener, M. (2002). Classification and Regression by randomForest. R News, 2(3), 18–
22. http://CRAN.R-project.org/doc/Rnews/.
Ligges, U. (2008). Programmieren mit R (Statistik und ihre Anwendungen) (German Edition). Berlin: Springer.
Little, R., & Rubin, D. B. (2002). Statistical Analysis with Missing Data. Wiley.
Little, R., & Rubin, D. (2014). Statistical Analysis with Missing Data. New York City, NY: Wiley.
https://books.google.de/books?id=AyVeBAAAQBAJ.
Lovett, M. C., & Greenhouse, J. B. (2000). Applying Cognitive Theory to Statistics Instruction. The
American Statistician, 54(3), 196–206. https://doi.org/10.1080/00031305.2000.10474545.
Luhmann, M. (2015). R für Einsteiger. Beltz: Nordhausen.
Maechler, M., Rousseeuw, P., Struyf, A., Hubert, M., & Hornik, K. (2018). cluster: Cluster Analysis
Basics and Extensions.
Markgraf, N. (2018). typography.py. Software. https://github.com/NMarkgraf/typography.py.
Matejka, J., & Fitzmaurice, G. (2017). Same stats, different graphs: Generating datasets with varied
appearance and identical statistics through simulated annealing. In Proceedings of the 2017 CHI
Conference on Human Factors in Computing Systems (S. 1290–1294). ACM.
McElreath, R. (2015). Statistical Rethinking: A Bayesian Course with Examples in R and Stan. Boca
Raton, FL: Chapman; Hall/CRC.
McNamara, A., Arino de la Rubia, E., Zhu, H., Ellis, S., & Quinn, M. (2018). skimr: Compact and
Flexible Summaries of Data. https://CRAN.R-project.org/package=skimr.
Meehl, P. E. (1978). Theoretical risks and tabular asterisks: Sir Karl, Sir Ronald, and the slow progress of soft psychology. Journal of consulting and clinical Psychology, 46(4), 806.
Meschiari, S. (2015). latex2exp: Use LaTeX Expressions in Plots. https://CRAN.R-project.org/
package=latex2exp.
Micceri, T. (1989). The unicorn, the normal curve, and other improbable creatures. Psychological
Bulletin, 105(1), 156–166. https://doi.org/10.1037/0033-2909.105.1.156.
Molinaro, A. M., Simon, R., & Pfeiffer, R. M. (2005). Prediction error estimation: a comparison of
resampling methods. Bioinformatics, 21(15), 3301–3307.
Mooney, C., Duval, R., & Duvall, R. (1993). Bootstrapping: A Nonparametric Approach to Statistical Inference. SAGE. https://books.google.de/books?id=ZxaRC4I2z6sC.
Literatur
553
Moore, D. (1990). Uncertainty. In N. R. Council (Hrsg.), On the shoulders of giants: New approaches to numeracy (S. 95–137). Washington, DC: ERIC. https://doi.org/10.17226/1532.
Morey, R. D., & Rouder, J. N. (2015). BayesFactor: Computation of Bayes Factors for Common
Designs. http://bayesfactorpcl.r-forge.r-project.org/.
Morey, R. D., Hoekstra, R., Rouder, J. N., Lee, M. D., & Wagenmakers, E.-J. (2016). The fallacy
of placing confidence in confidence intervals. Psychonomic Bulletin & Review, 23(1), 103–123.
https://doi.org/10.3758/s13423-015-0947-8.
Nakagawa, S., & Cuthill, I. C. (2007). Effect size, confidence interval and statistical significance:
a practical guide for biologists. Biological Reviews, 82(4), 591–605. https://doi.org/10.1111/j.
1469-185X.2007.00027.x.
Navarro, D. (2015). Learning statistics with R: A tutorial for psychology students and other beginners. (Version 0.5). Adelaide, Australia: University of Adelaide. http://ua.edu.au/ccs/teaching/lsr.
Neuwirth, E. (2014). RColorBrewer: ColorBrewer Palettes. https://CRAN.R-project.org/package=
RColorBrewer.
Neyman, J. (1937). Outline of a Theory of Statistical Estimation Based on the Classical Theory of
Probability. Phil. Trans. R. Soc. Lond. A, 236(767), 333–380.
Neyman, J., & Pearson, E. S. (1933). On the problem of the most efficient tests of statistical hypotheses. Philosophical Transactions of the Royal Society of London A: Mathematical, Physical
and Engineering Sciences, 231, 289–337. https://doi.org/10.1098/rsta.1933.0009.
OECD (2016). Data from the OECD Regional Wellbeing study (RWB_03112017172414431).
https://www.oecdregionalwellbeing.org/ (Erstellt: 06.2016).
Ooms, J. (2018). pdftools: Text Extraction, Rendering and Converting of PDF Documents. https://
CRAN.R-project.org/package=pdftools.
Open Science Collaboration (2015). Estimating the reproducibility of psychological science.
Science, 349(6251) https://doi.org/10.1126/science.aac4716.
Orzetto (2010). Coefficient of Determination. https://en.wikipedia.org/wiki/Coefficient_of_
determination#/media/File:Coefficient_of_Determination.svg Zugegriffen: 18.02.2017.
Pearl, J. (2009). Causality. Cambridge: Cambridge University Press.
Pebesma, E. (2018). sf: Simple Features for R. https://CRAN.R-project.org/package=sf.
Peng, R. (2014). R Programming for data science. Victoria: Canada: Leanpub.
Popper, K. (1968). Logik der Forschung. Tübingen: Mohr.
Popper, K. (1972). Die Offene Gesellschaft und ihre Feinde. Bern: Francke UTB.
du Prel, J.-B., Roehrig, B., Hommel, G., & Blettner, M. (2010). Deutsches Aerzteblatt Online.
https://doi.org/10.3238/arztebl.2010.0343.
Pruim, R., Kaplan, D. T., & Horton, N. J. (2017). The mosaic Package: Helping Students to ’Think
with Data’ Using R. The R Journal, 9(1), 77–102. https://journal.r-project.org/archive/2017/RJ2017-024/index.html.
Quercia, D., Kosinski, M., Stillwell, D., & Crowcroft, J. (2011). Our twitter profiles, our selves:
Predicting personality with twitter. In Privacy, Security, Risk and Trust (PASSAT) and 2011 IEEE
Third Inernational Conference on Social Computing (SocialCom), 2011 IEEE Third International
Conference on (S. 180–185). IEEE.
R Core Team (2018). R language definition. Vienna, Austria: R foundation for statistical computing.
Raiche, G. (2010). an R package for parallel analysis and non graphical solutions to the Cattell scree
test. http://CRAN.R-project.org/package=nFactors.
Ram, K., & Wickham, H. (2016). wesanderson: A Wes Anderson Palette Generator. https://github.
com/karthik/wesanderson.
Ranganathan, P., Pramesh, C., & Buyse, M. (2015). Common pitfalls in statistical analysis: „No
evidence of effect“ versus „evidence of no effect“. Perspectives in Clinical Research, 6(1), 62–
63. https://doi.org/10.4103/2229-3485.148821.
554
Literatur
Remus, R., Quasthoff, U., & Heyer, G. (2010). SentiWS – a Publicly Available German-language
Resource for Sentiment Analysis. In Proceedings of the 7th International Language Resources
and Evaluation (LREC’10) (S. 1168–1171).
Ren, K., & Russell, K. (2016). formattable: Create ’Formattable’ Data Structures. https://CRAN.Rproject.org/package=formattable.
Revelle, W. (2018). psych: Procedures for Psychological, Psychometric, and Personality Research.
Evanston, Illinois: Northwestern University. https://CRAN.R-project.org/package=psych.
Rickert, J. (2017). 10,000 CRAN Packages. R-Views. https://rviews.rstudio.com/2017/01/06/10000cran-packages/.
Robin, X., Turck, N., Hainard, A., Tiberti, N., Lisacek, F., Sanchez, J.-C., & Müller, M. (2011).
pROC: an open-source package for R and S+ to analyze and compare ROC curves. BMC Bioinformatics, 12, 77.
Robinson, D. (2017). What are the Most Disliked Programming Languages? Blog post. https://
stackoverflow.blog/2017/10/31/disliked-programming-languages/.
Robinson, D. (2018). broom: Convert Statistical Analysis Objects into Tidy Data Frames. https://
CRAN.R-project.org/package=broom.
Romeijn, J.-W. (2016). Philosophy of Statistics. In E. N. Zalta (Hrsg.), The Stanford Encyclopedia
of Philosophy. http://plato.stanford.edu/archives/win2016/entries/statistics/.
RStudio (2018). RStudio Desktop. Software. https://www.rstudio.com/.
Rucker, R. (2004). Infinity and the Mind. Princeton University Press.
Rudis, B. (2014). ggcounty. https://github.com/hrbrmstr/ggcounty.
Sachs, M. C. (2017). plotROC: A Tool for Plotting ROC Curves. Journal of Statistical Software,
Code Snippets, 79(2), 1–19. https://doi.org/10.18637/jss.v079.c02.
Saint-Mont, U. (2011). Statistik im Forschungsprozess: eine Philosophie der Statistik als Baustein
einer integrativen Wissenschaftstheorie. Heidelberg: Springer.
Salam, J. (2017). Quasi-Quotation By Analogy. Blog Post. http://blog.jalsalam.com/posts/2017/
quasi-quotation-as-meta-recipe/.
Sauer, S. (2017a). prada: Data and R functions as a companion to my book „Statistik_21“.
Sauer, S. (2017b). yart: A RMarkdown Template for writing PDF reports. https://github.com/
sebastiansauer/yart.
Sauer, S. (2017c). Height, sex, and shoe size of some students. Data Set. https://doi.org/10.17605/
OSF.IO/JA9DW.
Sauer, S. (2017d). Results from an exam in inferential statistics. Data Set. https://doi.org/10.17605/
OSF.IO/SJHUY.
Sauer, S. (2017e). Results from a survey on extraversion. Data Set. https://doi.org/10.17605/OSF.
IO/4KGZH.
Sauer, S. (2018). pradadata: Data Sets for Practical Data Analysis. https://github.com/sebastiansauer/
pradadata.
Sauer, S., & Wolff, A. (2016). The effect of a status symbol on success in online dating: an experimental study (data paper). The Winnower. 3:e147241.13309. https://doi.org/10.15200/winn.
147241.13309.
Sauer, S., Walach, H., & Kohls, N. (2010). Gray’s Behavioural Inhibition System as a mediator of
mindfulness towards well-being. Personality and Individual Differences, 50(4), 506–551. https://
doi.org/10.1016/j.paid.2010.11.019.
Scherer, A. (2013). Neuronale Netze: Grundlagen und Anwendungen. Berlin: Springer.
Schloerke, B., Crowley, J., Cook, D., Briatte, F., Marbach, M., Thoen, E., Larmarange, J., et al.
(2017). GGally: Extension to ’ggplot2’. https://CRAN.R-project.org/package=GGally.
Schuler, H. (2015). Lehrbuch Organisationspsychologie. Bern: Huber Hans.
Literatur
555
Schwartz, S. H. (1999). A Theory of Cultural Values and Some Implications for Work. Applied
Psychology, 48(1), 23–47. https://doi.org/10.1111/j.1464-0597.1999.tb00047.x.
Shearer, C. (2000). The CRISP-DM model: the new blueprint for data mining. Journal of data
warehousing, 5(4), 13–22.
Shmueli, G. (2010). To Explain or to Predict? Statistical Science, 25(3), 289–310. https://doi.org/
10.1214/10-STS330.
Sight, S. (2017). Data Science Crash-Course. Sharp Sight.
Silge, J., & Robinson, D. (2016). tidytext: Text Mining and Analysis Using Tidy Data Principles in
R. The Journal of Open Source Software, 1(3) https://doi.org/10.21105/joss.00037.
Silver, D., Hubert, T., Schrittwieser, J., Antonoglou, I., Lai, M., Guez, A., Hassabis, D., et al.
(2017a). Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm. CoRR, abs/1712.01815. http://arxiv.org/abs/1712.01815.
Silver, D., Schrittwieser, J., Simonyan, K., Antonoglou, I., Huang, A., Guez, A., Hassabis, D., et
al. (2017b). Mastering the game of Go without human knowledge. Nature, 550(7676), 354–359.
https://doi.org/10.1038/nature24270.
Silver, N. (2012). The signal and the noise: the art and science of prediction. London: Penguin UK.
Simonsohn, U., Nelson, L. D., & Simmons, J. P. (2014). P-curve: a key to the file-drawer. Journal of
experimental psychology. General, 143(2), 534–547. https://doi.org/10.1037/a0033242.
Soetaert, K. (2017). plot3D: Plotting Multi-Dimensional Data. https://CRAN.R-project.org/package
=plot3D.
South, A. (2011). rworldmap: A New R package for Mapping Global Data. The R Journal, 3(1),
35–43. http://journal.r-project.org/archive/2011-1/RJournal_2011-1_South.pdf.
Spurzem, L. (2017). VW 1303 von Wiking in 1:87. Photograph. https://de.wikipedia.org/wiki/
Modellautomobil#/media/File:Wiking-Modell_VW_1303_(um_1975).JPG.
Stanley, D. (2018). apaTables: Create American Psychological Association (APA) Style Tables.
https://CRAN.R-project.org/package=apaTables.
Stevenson, A. (2010). Oxford Dictionary of English. Oxford, UK: OUP. https://books.google.de/
books?id=anecAQAAQBAJ.
Strauss, A., & Corbin, J. M. (1990). Basics of qualitative research: Grounded theory procedures and
techniques. Thousand Oaks, Ca: SAGE.
Strobl, C., & Malley, J. (2009). An Introduction to Recursive Partitioning. Bagging and Random Forests: Rationale, Application and Characteristics. Psychological Methods, 14(4), 323–348. https://
doi.org/10.1037/a0016973.
Suppes, P., & Zinnes, J. (1963). Basic Measurement Theory. In D. Luce (Hrsg.), Journal of Symbolic
Logic (S. 1–1). John Wiley & Sons.
Tan, P.-N. (2013). Introduction to Data Mining. Boston, Ma: Addison-Wesley.
Therneau, T., & Atkinson, B. (2018). rpart: Recursive Partitioning and Regression Trees. https://
CRAN.R-project.org/package=rpart.
Trilling, B., & Fadel, C. (2012). 21st Century Skills: Learning for Life in Our Times. San Francisco,
CA: Jossey-Bass.
Tufte, E. R. (1990). Envisioning Information. Ceshire, CO: Graphics Press.
Tufte, E. R. (2001). The Visual Display of Quantitative Information. Ceshire, CO: Graphics Press.
Tufte, E. R. (2006). Beautiful Evidence. Ceshire, CO: Graphics Press.
Tukey, J. W. (1962). The future of data analysis. The annals of mathematical statistics, 33(1), 1–67.
Unrau, S. (2017). No Title. Photograph. https://unsplash.com/photos/CoD2Q92UaEg.
Urbanek, S. (2013). png: Read and write PNG images. https://CRAN.R-project.org/package=png.
VanDerWal, J., Falconi, L., Januchowski, S., Shoo, L., & Storlie, C. (2014). SDMTools: Species Distribution Modelling Tools: Tools for processing data associated with species distribution
modelling exercises. https://CRAN.R-project.org/package=SDMTools.
556
Literatur
Venables, W. N., & Ripley, B. D. (2002). Modern Applied Statistics with S (Fourth.). New York:
Springer. http://www.stats.ox.ac.uk/pub/MASS4.
Wagenmakers, E.-J. (2007). A practical solution to the pervasive problems of p values. Psychonomic
Bulletin & Review, 14(5), 779–804. https://doi.org/10.3758/bf03194105.
Wagenmakers, E.-J., Morey, R. D., & Lee, M. D. (2016). Bayesian benefits for the pragmatic researcher. Current Directions in Psychological Science, 25(3), 169–176.
Warnes, G. R., Bolker, B., Bonebakker, L., Gentleman, R., Liaw, W. H. A., Lumley, T., & . . . Venables, B. (2016). gplots: Various R Programming Tools for Plotting Data. https://CRAN.R-project.
org/package=gplots.
Wasserstein, R. L., & Lazar, N. A. (2016). The ASA’s Statement on p-Values: Context, Process,
and Purpose. The American Statistician, 70(2), 129–133. https://doi.org/10.1080/00031305.2016.
1154108.
Wei, T., & Simko, V. (2017). R package „corrplot“: Visualization of a Correlation Matrix. https://
github.com/taiyun/corrplot.
Wicherts, J. M., Veldkamp, C. L. S., Augusteijn, H. E. M., Bakker, M., van Aert, R. C. M., & van
Assen, M. A. L. M. (2016). Degrees of Freedom in Planning, Running, Analyzing, and Reporting
Psychological Studies: A Checklist to Avoid p-Hacking. Frontiers in Psychology, 7 https://doi.
org/10.3389/fpsyg.2016.01832.
Wickham, H. (2014). Advanced R. Boca Raton, Florida: CRC Press.
Wickham, H. (2015). ggplot2movies: Movies Data. https://CRAN.R-project.org/package=ggplot2
movies.
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis (Use R!). New York: Springer.
Wickham, H. (2017a). modelr: Modelling Functions that Work with the Pipe. https://CRAN.Rproject.org/package=modelr.
Wickham, H. (2017b). tidyverse: Easily Install and Load the ’Tidyverse’. https://CRAN.R-project.
org/package=tidyverse.
Wickham, H. (2018a). pryr: Tools for Computing on the Language. https://CRAN.R-project.org/
package=pryr.
Wickham, H. (2018b). scales: Scale Functions for Visualization. https://github.com/hadley/scales.
Wickham, H. (2018c). stringr: Simple, Consistent Wrappers for Common String Operations. https://
CRAN.R-project.org/package=stringr.
Wickham, H., & Grolemund, G. (2016). R for Data Science: Visualize, Model, Transform, Tidy, and
Import Data. O’Reilly Media.
Wickham, H., & Grolemund, G. (2017). R für Data Science. Sebastopol, CA: O’Reilly.
Wickham, H., & Henry, L. (2018). tidyr: Easily Tidy Data with ’spread()’ and ’gather()’ Functions.
https://CRAN.R-project.org/package=tidyr.
Wickham, H., Francois, R., Henry, L., & Müller, K. (2017). dplyr: A Grammar of Data Manipulation. https://CRAN.R-project.org/package=dplyr.
Wickham, H., Hester, J., & Chang, W. (2018). devtools: Tools to Make Developing R Packages
Easier. https://CRAN.R-project.org/package=devtools.
Wikipedia (2017). Körpergröße — Wikipedia, Die freie Enzyklopädie. https://de.wikipedia.org/w/
index.php?title=K%C3%B6rpergr%C3%B6%C3%9Fe&oldid=165047921.
Wild, C. J., & Pfannkuch, M. (1999). Statistical thinking in empirical enquiry. International Statistical Review, 67(3), 223–248.
Wild, F. (2015). lsa: Latent Semantic Analysis. https://CRAN.R-project.org/package=lsa.
Wilke, C. O. (2017). cowplot: Streamlined Plot Theme and Plot Annotations for ’ggplot2’. https://
CRAN.R-project.org/package=cowplot.
Wilkinson, L. (2006). The grammar of graphics. New York City, NY: Springer.
Literatur
557
Winston Chang (2014). extrafont: Tools for using fonts. https://CRAN.R-project.org/package=
extrafont.
Wright, M. N., & Ziegler, A. (2017). ranger: A Fast Implementation of Random Forests for High
Dimensional Data in C++ and R. Journal of Statistical Software, 77(1), 1–17. https://doi.org/10.
18637/jss.v077.i01.
Xie, Y. (2015). Dynamic Documents with R and knitr (2. Aufl.). Boca Raton, Florida: Chapman;
Hall/CRC. http://yihui.name/knitr/.
Xie, Y. (2018). knitr: A General-Purpose Package for Dynamic Report Generation in R. https://
yihui.name/knitr/.
Youyou, W., Kosinski, M., & Stillwell, D. (2015). Computer-based personality judgments are more
accurate than those made by humans. Proceedings of the National Academy of Sciences, 112(4),
1036–1040.
Zhu, H. (2017). Create Awesome HTML Table with knitr::kable and kableExtra. https://cran.rproject.org/web/packages/kableExtra/vignettes/awesome_table_in_html.html.
Zhu, H. (2018). kableExtra: Construct Complex Table with ’kable’ and Pipe Syntax. https://CRAN.
R-project.org/package=kableExtra.
Ziemann, M., Eren, Y., & El-Osta, A. (2016). Gene name errors are widespread in the scientific
literature. Genome Biology, 17(1), 177. https://doi.org/10.1186/s13059-016-1044-7.
Zuguang, G. (2017). Add legends to circlize plot. http://zuguang.de/blog/html/add_legend_to_
circlize.html.
Zumel, N., Mount, J., & Porzak, J. (2014). Practical data science with R. Shelter Island, NY: Manning.
Sachverzeichnis
::, doppelter Doppelpunkt, 40
A
Abduktion, 248
Abweichungsbalken, 105
Abweichungsstecken, Abweichungslinien, 324
Achsenabschnitt, 322
Achsenabschnitt, Intercept, 328
agglomerative Clusteranalyse, 438
Akaike Information Criterion, AIC, 353
Algorithmus, 391
Allgemeines Lineares Modell, Generalisiertes
Lineares Modell, 251
Allgemeines Lineares Modells, Generalisiertes
Lineares Modell, General Linear Model,
GLM, 347
Alpha-Wert, 168
Alternativhypothese, H1, HA, 273
angeleitetes Lernen, 253
ANOVA, Varianzanalyse, AOV, 281
Anscombe, 158
Application Programming Interface, API, 466
Arbeitsverzeichnis, 31
Argument, 39
Ästhetikum, 179, 210
Attribute, 50, 55, 62
AUC, Fläche unter der Kurve, Area Under
Curve, 358
Ausdruck, Expression, Zitation, 529
Ausprägungen, 8, 51
Auszeichnungssprache, Markup Language, 479
B
Bagging, 377, 394
baumbasierte Verfahren, Bäume, 377
Befehl, 511
Befehl, Funktion, Anweisung, 39
Befehle, Funktionen, 22
Beobachtungseinheiten, Beobachtung, Fälle, 8
Bereichsschätzer, Intervallschätzer, 286
Bestimmtheitsmaß, 325
Bias, 256
Bibtex, 488
Binnen, 136
bivariat, multivariat, 104
Bootstrap, Boostrapping, 308
Bootstrapping, 394
C
CART, Classification and Regression Trees,
378
Chancen, 349
Chi-Quadrat-Test, 278
Choroplethenkarte, 233
Closure, 531
Clusteranalyse, Cluster, Typen, 437
Codebuch, 61
Codebuch, Code Book, 506
Cohens d, 289
Committen, Committing, 509
Copy-Paste, 476
Corpus, 450
CRAN, 14, 23
CSL-Stil, 489
CSS-Datei, 479
CSV, CSV-Datei, 65
D
Dataframes, 53, 59
Daten, 8
datengenerierende Maschine, 251
Datenjudo, 76
559
560
Datensatz, 8
Datenstrukturen, 48
Deduktion, 249
Delogarithmieren, 349, 350
Determinationskoeffizient, 325
deterministisch, 251
Dichotomisieren, 331
dimensionsreduzierendes Modellieren, 253
Dokumentation, 506
Dollar-Operator, 58
dplyr::arrange, 82
dplyr::count, 90
dplyr::filter, 78
dplyr::select, 81
dplyr::summarise, 86
DRY, 523
Dublette, 125
Dummy-Variablen, 407
E
Effektstärke, 289, 426
einfaches reproduzierbares Beispiel, 33
Einflussgewicht, b, Beta, Regressionsgewicht,
327
Einflussgewicht, Regressionsgewicht,
Regressionskoeffizient, 323
Einflussgrößen, 250
Elastic Net, 416
Entschachteln, unnest, 451
Entscheidungsbäume, 377
Entwicklungsumgebung, Integrated
Development Environment, IDE, 21
Environment, Umgebung, Arbeitsumgebung,
globale Umgebung, 22
Erklären, 252
euklidischer Abstand, 440
Evaluation, 530
Explikatives Modellieren, 252
F
Faktoren, 51
Faktorstufen, levels, 52
fallreduzierendes Modellieren, 253
Faltung, fold, 259
Farbpalette divergierend, 188, 224
Farbpalette kontrastierend, 188
Farbpalette sequenziell, 188
Fehlerterm, 251
Fläche unter der Kurve, AUC, 430
Sachverzeichnis
Formelschreibweise, 107
Funktionen, 511
G
Gegenstandsbereich, 246
geleitetes Modellieren, 252
Geome, 161, 166
Git, 478, 509
Github, 509
Glättungslinie, 164
Grundgesamtheit, Population, 301
H
Hashtag, Schlagwort, 467
hierarchische Clusteranalyse, 438
Homoskedastizität, 330
I
Imputieren, 122
Indizieren, 55
Induktion, 249
Informationstheoretisches Maß, 353
Innerhalb-Varianz, Varianz innerhalb, 439
Interaktionseffekt, 335, 336
Interaktionsterm, 336
Interquartilsabstand, IQR, 106
ISO 3166-1 alpha-3, 225
Item-Labels, Labels, 208
J
JSON, 467
K
Kausation, 252
Klassifikation, 251, 353
Kommentare, 37
Komplexitätsparameter, 383
Konfidenzintervalle, 286, 306
Konfusionsmatrix, 354, 355
Konsole, 22
Kontingenztabelle, 110
Korrelation, Zusammenhang, linearer
Zusammenhang, 112
Kreuzvalidierung, 259
Kriterium der Kleinsten Quadrate, 328
L
Lagemaße, 104
LaTeX, TeX, 477
Sachverzeichnis
Lernen ohne Anleitung, unsupervised learning,
254
Lieblingsstatistiken, 108
Likelihood, 416
lineare Modelle, 321, 327
Linkfunktion, 348
Listen, 53, 57
Listenspalte, 217
logistische Funktion, 347
Logit, 349
lokal gewichtete Polynomregression, LOESS,
430
M
Makefile, 505
Mapping, Zuordnung, 161
Markdown, 479
Markup-Format, Auszeichnungssprache, 481
Masterbrain, 302
Matrizen, 53
Md, Median, 105
Mean Descrease in Accuracy, 431
Metadaten, 62
Mitarbeiterbefragung, 464
Mittelwert, arithmetisches Mittel, 104
Mittlere Absolutabstand, MAA, MAD, 105
Mittlerer Quadratfehler, Mean Squared Error,
MSE, 325
Modell, 246
Modellanpassung, 254
Modellfit, Modellpassung, Modellgüte, 324
Modellgüte, 254
Modellgüte, Model Fit, 249
Modus, 105
Multikollinearität, 405
multivariat, 334
N
NHST, 269
Non-standard Evaluation, NSE,
Non-Standard-Evaluation, 528
Normalform eines Dataframes, 130
Nullhypothese, 270
Nullhypothese, H0, 273
Nullhypothesen-Signifikanztesten, 269
Nullmodel, No Information Rate, 389
Nullmodell, 261, 409
O
Objekte, 38, 48
561
Objekte, Variablen, 511
Odds, 349
Odds Ratio, OR, 427
Ogive, 349
OOB, Out-of-Bag-Beobachtungen, out of bag,
OOB-Sample, 394
Ordinary Least Squares, 328
Organisationsdiagnose, 464
Overfitting, 255
Overplotting, 168
P
Pakete, 14, 21, 23
pandoc, 481
Parameter, 39, 286
Partionierung, 379
partitionierende Clusteranalyse, 438
Partitionierung, 136
penalisierte Modelle, regularisierte Modelle,
penalized models, 416
Pfadmodell, 250
Pfeife, 91
Population, Grundgesamtheit, 301
Populationsparameter, 268
Populismus, 495
Portabilität, 507
Power, 292
prädiktives Modellieren, 16, 252
Prädiktoren, 250
Prädiktorenrelevanz, Prädiktorenwichtigkeit,
Wichtigkeit von Prädiktoren, 429
Pruning, Zurückschneiden eines Baumastes,
392
Punktschätzer, 286
p-Wert, 269, 313
Q
Quantil, 106
Quosure, 531
R
Random Forest, Random-Forest-Modell, 411
Random Forests, 377, 394
R-Befehl, Funktion, 40
Reduzieren, 253
Referenzstufe, 363
Referenzstufe einer Faktorvariablen, 417
Regression, 251, 321
Regressionsgerade, Regressionslinie,
Trendgerade, 327
562
Regulärausdrücke, Regex, 453
reiner Vektor, 49
rekursives Partionieren, 391
Relation, 248
Relative Pfade, 506
Repositorium, 504, 509
Reproduzierbarkeit, 17, 504, 507
Resampling, 259, 308
Residuen, 329
Residuum, Fehler, 324
Richtigkeit, Korrektklassifikationsrate,
Accuracy, 388
Ridge Regression, 416
RMarkdown, 479
robust, 257
ROC-Kurve, ROC, 357
R-Skript, 29
S
Schätzer, 306
Screeplot, 440, 446
Sentiment, 464
Sentimentanalyse, 465
Sentimentlexikon, 460
Shape-Daten, 217
signal noise ratio, 289
Signal, Phänomen, 9
Signfifikanz, 271
Signifikanz, 270
Signifikanzniveau, 272
Simulation, 305
Skalenniveau, 104
Skriptfenster, 22, 37
SQLite-Datenbanken, 471
Standardabweichung, SD, 105
Standardfehler, 273, 304, 306
Standardnormalverteilung, 133
Standardwerte, Default Values, Voreinstellung,
40
Steigung, 323
Stichprobenverteilung, 272, 304, 305
stochastisch, 251
Stop-Kriterien, 391
Strafterm, 416
Streuungsmaße, 105
Stufen, Faktorstufen, levels, 51
Support Vector Machines, 414
Syntax, Code, 13
T
Themes, Themen, 195
Sachverzeichnis
Tibble, tbl-df, tbl, 68
Tidy Data, Daten in Normalform, 3
Tidytext-Dataframe, 450
Token, Term, 450
Transformieren, 94
Trendgerade, Regressionsgerade, 322
Trunkieren, Stemming, 457
Tuningparameter, 384, 395, 414
Twitter, 463
U
Überanpassung, 255
Übergewissheit, Overcertainty, 426
Umkodieren, 136
Ungeleitetes Modellieren, 253
univariat, 103
Unquoting, Unzitieren, 533
Unteranpassung, Underfitting, 255
Urhorde, 496
V
Variable, 38
variable importance, 398
Variablen, Merkmale, 8, 37
Variablenrelevanz, 398
Variablenwichtigkeit, variable importance, 426
Varianz, Var, 105
Vektor, 42, 48
vektoriell, 42
vektorielles Verarbeiten, vektorielles Rechnen,
42
vektorisiert, 515
Vorhersagefehler, 324, 325
Vorhersagen, 252
W
Wert, 8
Y
YAML, Yaml, YAML-Header, 482
Youden-Index, 355
Z
Zellen, Elemente, 8
zentrieren, 337
Zielgröße, Output-Variable, Kriterium,
Ausgabegröße, 250
Zitieren, Quotation, Backen, 529
Zufallszahlen, set.seed(), 409
Zusammenfassungsfunktionen, 87
Download