-
Notifications
You must be signed in to change notification settings - Fork 5
/
11-Data-Munging.Rmd
372 lines (277 loc) · 13.5 KB
/
11-Data-Munging.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
---
output: html_document
editor_options:
chunk_output_type: console
---
```{r, include = FALSE}
knitr::opts_knit$set(unnamed.chunk.label = "data_munging_")
knitr::opts_chunk$set(comment = "", warning = FALSE, error = FALSE)
library(dplyr)
library(car)
library(tidyr)
library(ggplot2)
library(sjmisc)
```
# Data Munging
Ah ja, die wunderbare Welt des *Data Munging* – dem Dammelbegriff für "*Zeug mit Daten machen damit wir da besser mit arbeiten können oder so*". Hierzu gehören so spaßige Themen wie "recoding", "reformatting", "restructuring", "labelling", "transforming", "reshaping" und viele andere lustige Begriffe die teilweise kongruent sind, und auch sonst ist das eher so ein Bereich á la *learning by doing*.
Wie schon beim [Datenimport] erwähnt gibt es in der Regel kein Patentrezept zru Datenbereinigung, aber es *gibt* gängige Anwendungsfälle, und dementsprechend auch populäre Lösungswege für selbige Fälle.
In diesem Kapitel widmen wir uns also einigen dieser gängigen Aufgaben und probieren das Ganze anhand unserer Beispieldatensätze aus.
## Vorab: Pipes!
Lasst mich euch euer neues Stück Lieblingsyntax vorstellen: ` %>% `.
Das ist die *pipe*, im Code gesprochen als "*dann*". Nichts verbessert die Lesbarkeit und Nachvollziehbarkeit von Code so nachhaltig wie großzügige Verwendungen dieses kleinen Operators.
Die pipe steht zwischen zwei Funktionen, und setzt das linke Element als erstes Argument in die rechte Funktion. Sprich: `f(x) %>% g()` ist äquivalent zu `g(f(x))`.
Ich sehe schon, wir brauchen Beispiele:
Mal angenommen wir haben einen Vektor von Zahlen, und wollen diese zuerst quadrieren, dann aufsummieren, dann die Wurzel aus dem Ergebnis ziehen und dann auf zwei Nachkommastellen runden. Wieso sollten wir das tun? Für Übungszwecke. Alles andere wäre ja albern.
```{r pipe_1}
x <- c(1, 2, 5, 4, 3, 7, 6, 8, 4, 3, 5, 7, 8)
round(sqrt(sum(x^2)), 2)
```
Das sieht ziemlich unübersichtlich aus, oder? Wir müssen den Code praktisch von `x` angefangen von innen nach außen lesen, um zu verstehen, was da eigentlich passiert.
Eine Möglichkeit das zu umgehen, wäre die Erstellung von Zwischenergebnissen:
```{r pipes_2}
x <- x^2
x <- sum(x)
x <- sqrt(x)
x <- round(x, 2)
```
Das ist… möglich, aber auch das wird irgendwann unübersichtlich, und solange ihr nicht jedem Zwischenergebnis einen anderen Namen gebt, wird das auch irgendwann schwer nachvollziehbar, insbesondere wenn ihr einen Fehler in eurem Code habt und Zwischenergebnisse nachvollziehen wollt.
Mit der pipe sähe das dann so aus:
```{r pipe_3}
library(magrittr)
x^2 %>% sum() %>% sqrt() %>% round(2)
```
Das sieht jetzt erstmal noch nicht so besonders nach Verbesserung aus, aber achtet darauf, wie wir den Prozess jetzt ganz einfach von links nach rechts lesen können, oder mit mehrzeiliger Formatierung:
```{r pipe_4}
x^2 %>%
sum() %>%
sqrt() %>%
round(2)
```
Pipelines in dieser Art werdet ihr noch sehr viele sehen, und früher oder später werdet ihr sie zu schätzen lernen, just trust me on this one.
Ein komplexeres Beispiel aus einem meiner alten Projekte sieht etwa so aus:
```{r pipe_advanced_example, eval=FALSE}
library(rvest)
library(dplyr)
library(stringr)
happiness <- read_html("https://en.wikipedia.org/wiki/World_Happiness_Report") %>%
html_table(fill = TRUE, trim = TRUE) %>%
extract2(1) %>%
select(Country, Score) %>%
mutate(Country = str_trim(Country, "both")) %>%
set_colnames(c("country", "happiness_score"))
```
Was da passiert ist etwas Folgendes:
1. Wir erstellen ein Objekt `happiness`, dann…
2. lesen eine Wikipedia-Seite ein via `read_html`, dann…
3. holen wir da Tabellen raus mit `html_table`, dann…
4. extrahieren wir das erste Element via `extract2`, dann…
5. wählen wir via `select` zwei Spalten der Tabelle aus, dann…
6. wenden wir `str_trim` auf eine Variable an in `mutate`, dann…
7. setzen wir die Variablennamen via `set_colnames`.
Fertig!
Und das alles in nur einer Pipeline.
Ihr müsst den Code oben nicht inhaltlich nachvollziehen können, aber ihr seht vermutlich, dass die Struktur deutlich einfacher zu verstehen ist, als eine lange Verschachtelung mehrer Funktionen oder eine Reihe von Befehlen mit mehreren Zwischenschritten.
Und _das_ ist die Stärke der pipe, und _das_ ist das zentrale Prinzip in allen `tidyverse`-packages.
### `magrittr`-Boni
Der `%>%`-Operator kommt ja ursprünglich aus dem `magrittr`-package, und auch wenn viele packages wie `dplyr`, `tidyr` oder auch `tadaatoolbox` den Operator auch mitbringen, hat *pures* `magrittr` noch einige nette Boni für Pipe-Konstruktionen
```{r pipe_boni_1}
library(magrittr)
c(1, 4, 7, 4, 8, 19, 33, 42, 12, 4, 16) %>%
divide_by(5) %>%
add(9) %>%
raise_to_power(2)
```
Die Funktionen `add`, `divide_by` und `raise_to_power` sind nur andere Versionen der Rechenoperatoren mit entsprechenden Namen, `+`, `/` und `^`, die sich besser für Pipelines eignen.
## Variablen verändern oder anlegen
Als Beispiel lesen wir mal den `gotdeaths_books`-Datensatz ein:
```{r}
library(readr)
gotdeaths_books <- read_csv("data/got_character-deaths.csv")
```
Ihr seht, dass Spalten Namen haben wie `Death Year`, was wegen des Leerzeichens dazwischen etwas unhandlich ist. Zusätzlich haben wir Spalten `Gender` und `Nobility`, die mit `1` und `0` kodiert sind, die wären mit Labels lesbarer.
### Spaltennamen
Spaltennamen lassen sich einfach mit `names()` anzeigen und ändern:
```r
# Spaltennamen anzeigen
names(gotdeaths_books)
# Spaltennamen ändern
names(gotdeaths_books) <- c("Name", "Allegiances", "Death_Year", "Book_of_Death",
"Death_Chapter", "Book_Introduced", "Gender", "Nobility",
"GameOfThrones", "ClashOfKings", "SongOfStorms", "FeastForCrows",
"DanceWithDragons")
```
...aber so müssen wir einen neuen Vektor mit der gleichen Anzahl an Elementen wie Spalten im Datensatz angeben, das ist ziemlich nervig, wenn wir nur einzelne Spalten umbenennen wollen.
Zum glück gibt's da was von `dplyr`:
```{r}
library(dplyr)
gotdeaths_books <- gotdeaths_books %>%
rename(Death_Year = 'Death Year',
Death_Chapter = 'Death Chapter',
Death_Book = 'Book of Death',
Book_Intro = 'Book Intro Chapter')
```
So müssen wir nur die Variablen angeben, die wir umbenennen wollen. Der neue Variablennamen steht links in `rename`, dann rechts der Name der aktuellen Variable, in diesem Fall in `' '` wegen der Leerzeichen.
Wir speichern das Ganze auch gleich wieder via `gotdeaths_books <- ` in das gleiche Objekt.
### Rekodieren
Als nächsten wollen wir die Variablen `Gender` und `Nobility` rekodieren, damit die numerischen Werte durch was aussagekräftigeres ersetzt werden. Das können wir am einfachsten mit `mutate` aus `dplyr` machen:
```{r}
gotdeaths_books <- gotdeaths_books %>%
mutate(Gender = factor(Gender, levels = c(0, 1), labels = c("Female", "Male")),
Nobility = factor(Nobility, levels = c(0, 1), labels = c("No", "Yes")))
```
Die Funktion `mutate` funktioniert nach dem gleichen Schema wie `rename`: Links steht der Namen der Spalte die wir erstellen (in diesem Fall überschreiben) wollen, und rechts neben dem `=` steht ein Ausdruck, der eine Variable mit gleicher Anzahl an Elementen zurückgibt. In diesem Fall ist die Funktion `factor`, angewandt auf die jeweils zu rekodierende Variable.
`factor()` erstellt einen Vektor des Typs, well, `factor`, mit numerischen Werten (`levels`), praktisch den Merkmalsausprägungen, und mit `character` Labels (`labels`). Beide nennen wir in der Funktion explizit. Das Resultat ist, dass die Variable `Gender` immer noch die `levels` `0, 1` hat, aber jetzt zusätzlich die `labels` `"Männlich", "Weiblich"`.
Nachdem wir den Befehl oben ausgeführt haben können wir uns die Variablen in der Konsole angucken:
```{r}
class(gotdeaths_books$Gender)
class(gotdeaths_books$Nobility)
# Spalten anzeigen:
gotdeaths_books %>% select(Gender, Nobility)
```
Dann ist da noch eine Sache mit den `Allegiances`. Wenn ihr euch die Variable anschaut, seht ihr, dass da manchmal "*Stark*" und manchmal "*House Stark*" etc. steht. Wenn wir jetzt aber nach der Zugehörigkeit gruppieren wollen in Tabellen und Plots, dann wären das ja Duplikate.
Das zu beheben ist leider etwas komplizierter, wenn wir's sauber machen wollen, aber haltet durch.
```{r}
library(stringr)
gotdeaths_books <- gotdeaths_books %>%
mutate(Allegiances = str_replace(Allegiances, pattern = "House\\ ", replacement = ""))
gotdeaths_books %>% count(Allegiances)
```
Fixed it.
Okay, was ist da passiert?
1. Anwendung von `mutate` so wie eben. Variable `Allegiances` ersetzen durch eine modifizierte Version
2. Wir haben das package `stringr` geladen und benutzt für die Funktion `str_replace`, darin...
2.1. Benutzen wir die Variable `Allegiances`...
2.2. Suchen das "Muster" `"House\\ "`, das steht für "Das wort `House` mit einem Leerzeichen danach"
2.3. Ersetzen das gesuchte Muster durch `""`, also leeren Text
3. Das Resultat ist die Variable `Allegiances`, aber überall wurde `House ` entfernt
Das was wir hier gemacht haben fällt unter die Themen *string manipulation* und *regular expressions*.
Das müsst ihr nicht sofort verstehen oder jetzt recherschieren, aber es kann helfen das zu können. Kommt alles mit der Zeit und lässt sich prima googlen, weil das in vielen Bereichen häufig vorkommt.
### Klassieren
Für diesen Anwendungsfall nehmen wir am besten wieder den `qmsurvey`-Datensatz, weil es bei den *Game of Thrones*-Daten so wenig zu klassieren gibt.
```{r}
qmsurvey <- readRDS("data/qm_survey_ss2017.rds")
```
Zum klassieren (also Sonderfall des Rekodierens) haben wir mehrere Optionen in R.
Die erste ist aus `car`:
```{r}
library(dplyr) # Für mutate und %>%
library(car) # Für recode
qmsurvey <- qmsurvey %>%
mutate(schlaf_k = recode(schlafstunden, "lo:7 = 1; 7.5:9 = 2; 9:hi = 3"))
```
Leider benutzt sich `recode` etwas umständlich, aber der Befehl liest sich etwa so:
- "Alle Werte vom niedrigsten (`lo`, sprich "low") bis `7` sollen zu `1` werden"
- "Alle Werte von `7.5` bis `9` sollen zu `2` werden"
- "Alle Werte von `9` bis zum höchsten (`hi`, sprich "high") sollen zu `3` werden"
Eine Alternative Möglichkeit aus `sjmisc` ist `split_var`:
```{r}
library(sjmisc)
qmsurvey <- qmsurvey %>%
mutate(schlaf_k = split_var(schlafstunden, n = 3))
```
Hier steht `n = 3` für die Anzahl der Gruppen, die wir gerne hätten. Das Resultat hat in diesem Fall allerdings nur 2 Gruppen, vermutlich weil die Spannweite der Werte relativ klein ist.
Eine *dritte* Variante wäre `dplyr` mit `case_when`, und erfordert Logik:
```{r}
qmsurvey <- qmsurvey %>%
mutate(schlaf_k = case_when(
schlafstunden >= 9 ~ 3,
schlafstunden < 9 ~ 2,
schlafstunden <= 7 ~ 1
))
```
Das liest sich so:
- Alle Werte *größer gleich* `9` sollen zu `3` werden
- Alle Werte *kleiner als* `9` sollen zu `2` werden
- Alle Werte *kleiner gleich* `7` sollen zu `1` werden
Benutzt eine diese Varianten, je nachdem welche ihr am Verständlichsten findet.
Und ja, es gibt noch viele andere Möglichkeiten, aber irgendwann ist auch mal gut.
## Summary Statistics
```{r}
qmsurvey %>%
summarize(m_alter = mean(alter),
sd_alter = sd(alter),
median_alter = median(alter))
qmsurvey %>%
group_by(rauchen) %>%
summarize(m_alter = mean(alter),
sd_alter = sd(alter),
median_alter = median(alter))
```
## Der `dplyr`-Workflow
```{r}
gotdeaths_books %>%
filter(!is.na(Death_Year)) %>%
group_by(Allegiances) %>%
tally() %>%
arrange(n)
```
Als plot:
```{r, eval = FALSE}
gotdeaths_books %>%
filter(!is.na(Death_Year)) %>%
group_by(Allegiances) %>%
tally() %>%
ggplot(aes(x = reorder(Allegiances, n), y = n)) +
geom_col() +
coord_flip()
```
## Format: Wide vs. Long
### Beispiel 1
```{r}
gotdeaths_books %>%
select(GoT, CoK, SoS, FfC, DwD)
```
```{r}
library(tidyr)
gotdeaths_books %>%
select(GoT, CoK, SoS, FfC, DwD) %>%
gather(key = Book, value = Appearance)
```
```{r}
gotdeaths_books %>%
select(GoT, CoK, SoS, FfC, DwD) %>%
gather(key = Book, value = Appearance) %>%
filter(Appearance > 0)
```
```{r}
gotdeaths_books %>%
select(GoT, CoK, SoS, FfC, DwD) %>%
gather(key = Book, value = Appearance) %>%
filter(Appearance > 0) %>%
group_by(Book) %>%
summarize(Character_Appearances = sum(Appearance))
```
```{r}
library(ggplot2)
gotdeaths_books %>%
select(GoT, CoK, SoS, FfC, DwD) %>%
gather(key = Book, value = Appearance) %>%
filter(Appearance > 0) %>%
group_by(Book) %>%
summarize(Appearances = sum(Appearance)) %>%
ggplot(aes(x = reorder(Book, Appearances), y = Appearances)) +
geom_col(color = "black", alpha = .75) +
labs(title = "A Song of Ice and Fire",
subtitle = "Character Appearances per Book",
x = "Book", y = "Character Appearances")
```
### Beispiel 2
```{r}
gotdeaths_books %>%
gather(key = Book, value = Appearance, GoT, CoK, SoS, FfC, DwD) %>%
select(Name, Book, Appearance)
```
```{r}
gotdeaths_books %>%
gather(key = Book, value = Appearance, GoT, CoK, SoS, FfC, DwD) %>%
filter(Appearance > 0) %>%
group_by(Name) %>%
summarize(Appearances = sum(Appearance)) %>%
ggplot(aes(x = Appearances)) +
geom_bar(alpha = .75, color = "black") +
scale_y_continuous(breaks = seq(0, 1000, 100),
minor_breaks = seq(0, 1000, 50)) +
labs(title = "A Song of Ice and Fire",
subtitle = "Number of Books Characters Appear in",
x = "Number of Books", y = "Abs. Frequency")
```