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