-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path07_dplyr.Rmd
469 lines (312 loc) · 18.9 KB
/
07_dplyr.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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# Mehr zu `dplyr` {#dplyr-single}
```{r include = FALSE}
source("common.R")
```
## Wo stehen wir?
In Kapitel \@ref(dplyr-intro) haben wir bereits zwei sehr wichtige Verben und den Pipe Operator kennengelernt und verwendet:
* `filter()` zur Auswahl spezieller Zeilen eines Datensatzes
* `select()` zur Auswahl spezieller Variablen eines Datensatzes
* der Pipe-Operator `|>` bzw. `%>%` überführt das Objekt auf der linken Seite des Operators in das erste Funktionsargument der Funktion auf der rechten Seite des Aufrufs
Wir haben zudem auch noch die Rolle von `dplyr` innerhalb des tidyverse besprochen:
:::: {.content-box-gray}
[dplyr] ist ein Kernpaket der [tidyverse] Kollektion von Paketen. Da wir die anderen oft beiläufig benutzen, werden wir stets dplyr und die anderen über `library(tidyverse)` laden.
:::
Wir starten wieder mit dem Laden von `dplyr` (über `tidyverse`)
```{r start_dplyr_single}
library(tidyverse)
```
und `gapminder`
```{r message = FALSE, warning = FALSE}
library(gapminder)
```
## Mit `mutate()` neue Variablen erstellen
Wir starten mit dem Anlegen einer Kopie von `gapminder`, die wir dann nach unseren Vorstellungen verändern (es wäre aber auch nichts passiert, wenn wir alles mit `gapminder` durchführen würden; mit dem Befehl `gapminder::gapminder` können wir immer auf die Originalversion zurückgreifen).
```{r}
my_gap <- gapminder
```
Unser __Ziel__ ist es, dass GDP pro Land anzugeben. Das sollte machbar sein, da schließlich das Pro-Kopf-GDP wie auch die Bevölkerungszahl im Datensatz enthalten sind. Multiplizieren beider Variablen liefert uns das gewünschte Ergebnis.
`mutate()` ist eine Funktion, die neue Variablen definiert und in ein tibble einfügt. Dabei können wir auf bestehende Variablen einfach über ihren Namen zugreifen.
```{r}
my_gap |>
mutate(gdp = pop * gdpPercap)
```
Hmmmm ... diese GDP-Zahlen sind ziemlich groß und abstrakt. In dem Zusammenhang, bedenke den Ratschlag von [Randall Munroe](https://fivethirtyeight.com/features/xkcd-randall-munroe-qanda-what-if/):
>One thing that bothers me is large numbers presented without context... "If I added a zero to this number, would the sentence containing it mean something different to me?" If the answer is "no", maybe the number has no business being in the sentence in the first place.
Vielleicht wäre es doch sinnvoller, wenn wir beim Pro-Kopf-GDP bleiben. Aber was wäre, wenn wir das Pro-Kopf-GDP angeben würden _in Relation zu irgendeinem Vergleichsland_. Wir könnten alles in Bezug auf die entsprechenden Daten aus Deutschland angeben.
Dazu müssen wir eine neue Variable erstellen, die aus den `gdpPercap` Werten , geteilt durch die deutschen `gdpPercap` Werte, besteht. Beim Erstellen der Variable müssen wir aber darauf achten, dass wir immer zwei Zahlen teilen, die sich auf dasselbe Jahr beziehen.
__Wie können wir das schaffen?__
1. Beobachtungen für Deutschland in einem Objekt `ger_gap` speichern
1. Erstellen einer neue _temporären_ Variable `tmp` in `my_gap`, die definiert wird durch:
i) die `gdpPercap`-Variable aus `ger_gap` aufrufen
i) mit `rep()` die `gdpPercap` Wert aus `ger_gap` einmal pro Land im `my_gap` reproduzieren, damit ein Vektor entsteht, der die gleiche Anzahl an Beobachtungen wie `my_gap` hat
1. Dividieren der `gdpPercap` Werte durch die deutschen Zahlen
1. Löschen der temporären Variable `tmp` in `my_gap`
```{r}
ger_gap <- my_gap |>
filter(country == "Germany")
my_gap <- my_gap |>
mutate(tmp = rep(ger_gap$gdpPercap, nlevels(country)),
gdpPercapRel = gdpPercap / tmp,
tmp = NULL)
```
Beachte, dass `mutate()` neue Variablen sequentiell erstellt, so dass man auf frühere Variablen (wie `tmp`) verweisen kann um spätere Variablen (wie `gdpPercapRel`) zu definieren. Nachdem eine Variable nicht mehr benötigt wird, kann man sie einfach auf `NULL` setzen.
Bleibt die Frage ob das alles so richtig war. Um diese Frage zu beantworten, können wir uns aber einfach mal die Werte von `gdpPercapRel` für Deutschland anschauen. Die sollten besser alle 1 sein!
```{r}
my_gap |>
filter(country == "Germany") |>
select(country, year, gdpPercapRel)
```
Ich glaube, wir können annehmen, dass Deutschland ein Land mit einem "hohen GDP" pro Kopf ist. Daher sollte die Verteilung von `gdpPercapRel` auf Werten unter 1 konzentriert sein, möglicherweise sogar weit darunter. Aber besser mal nachschauen ob dem so ist:
```{r}
summary(my_gap$gdpPercapRel)
```
Die Zahlen des relativen Pro-Kopf-GDP liegen deutlich unter 1. Wir sehen, dass die meisten Länder, die in diesem Datensatz erfasst werden, über den gesamten Zeitraum im Vergleich zu Deutschland ein wesentlich niedrigeres Pro-Kopf-GDP aufweisen.
:::: {.content-box-yellow}
__Tipp:__ Vertraut niemandem - einschließlich (besonders?) euch selbst. Versucht immer, einen Weg zu finden, um zu überprüfen, ob ihr das gemacht habt, was ihr tun wolltet. Seid aber nicht schockiert, wenn ihr manchmal feststellen müsst, dass dem nicht so ist. `r emo::ji("wink")`
::::
## Mit `arrange()` die Zeilenreihenfolge ändern
`arrange()` ordnet die Zeilen in einem data frame neu an. Stellen wir uns mal vor, dass wir die Daten nach Jahr und Land und nicht nach Land und Jahr ordnen wollen.
```{r}
my_gap |>
arrange(year, country)
```
Oder vielleicht interessieren euch nur die Daten aus 2007, angeordnet entsprechend der Lebenserwartung.
```{r}
my_gap |>
filter(year == 2007) |>
arrange(lifeExp)
```
Das war nun aber nicht das Ergebnis, welches ihr sehen wolltet. Ihr wolltet eigentlich nach absteigender Lebenserwartung sortieren. Dazu müsst ihr `desc()` verwenden.
```{r}
my_gap |>
filter(year == 2007) |>
arrange(desc(lifeExp))
```
:::: {.content-box-yellow}
__Tipp:__ Verlasst euch NIEMALS darauf, dass Zeilen oder Variablen in einer bestimmten Reihenfolge stehen. Aber manchmal will man Tabellen anderen präsentieren und dabei macht es durchaus Sinn die Zeilenreihenfolge je nach Fragestellung anzupassen.
::::
## Mit `rename()` "schöne" Namen vergeben
Ein paar der Namen in `gapminder` sind nicht besonders hübsch, wie z.B. `lifeExp`. life expectancy wären ja schließlich zwei Worte und daher finde ich (persönliche Meinung) es schöner, dies auch im Variablennamen zu sehen
```{r}
my_gap |>
rename(life_exp = lifeExp,
gdp_percap = gdpPercap,
gdp_percap_rel = gdpPercapRel)
```
Die Änderungen haben wir jetzt aber nicht abgespeichert (auch wenn sie schön waren), da wir den nachfolgenden Code auch weiterhin ausführen möchten, ohne die Variablennamen entsprechend zu ändern.
:::: {.content-box-grey}
__Bemerkung:__ Mit `select()` könnten wir bei der Auswahl von Variablen auch deren Namen ändern
```{r}
my_gap |>
filter(country == "Burundi", year > 1996) |>
select(yr = year, lifeExp, gdpPercap) |>
select(gdpPercap, everything())
```
`everything()` wählt alle übrigen (außer `gdpPercap`) Variablen. Da `gdpPercap` an erster Stelle gewählt wurde, wird die Variable auch zur ersten Spalte.
::::
## `summarise()` in Kombination mit `group_by()`
Nehmen wir mal an, dass uns die Antwort auf die Frage
:::: {.content-box-green}
"In welchem Land ist die Lebenserwartung innerhalb von 5 Jahren am stärksten gesunken?"
::::
interessiert.
`dplyr` bietet uns mächtige Hilfsmittel zur Beantwortung der Frage:
* `group_by()` fügt dem Datensatz eine zusätzliche Struktur hinzu -- Gruppierungsinformationen -- die die Grundlage für Berechnungen innerhalb der Gruppen bilden.
* `summarise()` nimmt einen Datensatz mit $n$-Beobachtungen, berechnet die angeforderten Zusammenfassungen und gibt einen Datensatz mit einer Beobachtung (falls nur eine Zusammenfassung angefordert wurde) zurück.
* `mutate()` und `summarise()` berücksichtigen Gruppen.
Kombiniert mit den Verben, die wir bereits kennen, könnt ihr mit diesen neuen Werkzeugen eine extrem vielfältige Reihe von Problemen relativ einfach lösen.
### Dinge aufzählen
Beginnen wir mit dem einfachen Zählen. Wie viele Beobachtungen haben wir pro Kontinent?
```{r}
my_gap |>
group_by(continent) |>
summarise(n = n())
```
Lasst uns hier kurz innehalten und über das tidyverse nachdenken. Ihr könntet die gleichen absoluten Häufigkeiten natürlich auch mit `table()` berechnen.
```{r}
table(gapminder$continent)
str(table(gapminder$continent))
```
Das Ergebnis ist ein Objekt der Klasse `table`. Dies macht nachfolgende Berechnungen leider etwas kniffliger, als es euch lieb ist. Zum Beispiel ist es schade, dass die Namen der Kontinente nur als *Attribute* und nicht als richtiger Faktor zusammen mit den berechneten Werten zurückgegeben werden.
Dies ist ein Beispiel dafür, wie das tidyverse Übergänge glättet, bei denen die Ausgabe von Schritt `i` die Eingabe von Schritt `i + 1` werden soll.
Die `tally()` Funktion ist eine Komfortfunktion, die weiß, wie man Zeilen zählt und dabei Gruppen berücksichtigt.
```{r}
my_gap |>
group_by(continent) |>
tally()
```
Die Funktion `count()` bietet noch mehr Komfort. Sie kann sowohl gruppieren als auch zählen.
```{r}
my_gap |>
count(continent)
```
Was wäre, wenn uns nicht nur die Anzahl an Beobachtungen pro Kontinent interessiert, sondern auch die Anzahl an unterschiedlichen Ländern pro Kontinent. Dazu bestimmen wir einfach mehrere Zusammenfassungen innerhalb von `summarise()`. Dabei verwenden wir die Funktion `n_distinct()`, um die Anzahl der einzelnen Länder innerhalb jedes Kontinents zu zählen.
```{r}
my_gap |>
group_by(continent) |>
summarise(n = n(),
n_countries = n_distinct(country))
```
### Deskriptive Statistiken mit `summarise()`
In Kombination mit `summarise()` können wir eine Vielzahl an verschiedenen Funktionen verwenden. Einige davon berechnen klassische __deskriptive Statistiken__:
In allen betrachteten Fällen seien $x_1,\dots,x_n$ numerische Beobachtungen.
+ `mean()` berechnet das arithmetische Mittel der Beobachtungen
$$\overline x_n = \frac{1}{n} \sum_{i=1}^n x_i\,.$$
+ `median()` berechnet den Median
$$x_{0.5} = \begin{cases}
x_{\left(\frac{n+1}{2}\right)}, & n\ \text{ungerade},\\
\frac{1}{2}\left(x_{\left(\frac{n}{2}\right)} + x_{\left(\frac{n}{2}+1\right)}\right), & n\ \text{gerade}
\end{cases}\,.$$
+ `var()` berechnet die empirische Varianz
$$s_n^2 = \frac{1}{n-1} \sum_{i=1}^n (x_i - \overline x_n)^2\,.$$
+ `sd()` berechnet die empirische Standardabweichung
$$s_n = \sqrt{s_n^2}\,.$$
+ `IQR()` berechnet den Interquartilsabstand
$$IQR = x_{0.75} - x_{0.25}\,,$$
wobei $x_{0.25}$ und $x_{0.75}$ das empirische 0.25 bzw. 0.75 Quantil bezeichnen.
+ `min()` berechnet das Minimum
$$x_{(1)} = \min(x_1,\dots,x_n)\,.$$
+ und `max()` berechnet demnach das Maximum
$$x_{(n)} = \max(x_1,\dots,x_n)\,.$$
Auch wenn dies statistisch gesehen unklug sein mag, lasst uns die durchschnittliche Lebenserwartung pro Kontinent berechnen.
```{r}
my_gap |>
group_by(continent) |>
summarise(avg_lifeExp = mean(lifeExp))
```
`summarise_at()` wendet die gleiche(n) Zusammenfassungs-Funktion(en) auf mehrere Variablen an. Lasst uns die durchschnittliche Lebenserwartung sowie den Median und das Pro-Kopf-GDP nach Kontinenten pro Jahr berechnen... aber nur für 1952 und 2007.
```{r}
my_gap |>
filter(year %in% c(1952, 2007)) |>
group_by(continent, year) |>
summarise_at(vars(lifeExp, gdpPercap), list(mean, median))
```
Im nächsten Schritt konzentrieren wir uns nur auf Asien. Wie hoch ist die minimale und maximale Lebenserwartung pro Jahr?
```{r}
my_gap |>
filter(continent == "Asia") |>
group_by(year) |>
summarise(min_lifeExp = min(lifeExp), max_lifeExp = max(lifeExp))
```
Natürlich wäre es viel interessanter zu sehen, *welches* Land bzw. welche Länder diese extremen Beobachtungen beigetragen haben. Kommt das Minimum (Maximum) immer aus dem gleichen Land? Wir gehen dem in Kürze mithilfe von Window Funktionen nach.
## Gruppierte Veränderungen
Manchmal möchte man die $n$-Zeilen für jede Gruppe nicht zu einer Zeile zusammenfassen. Stattdessen möchte man die Gruppen behalten, aber innerhalb dieser Gruppen rechnen.
### Berechnungen innerhalb der Gruppen
Lasst uns eine neue Variable definieren, die die gewonnenen (verlorenen) Lebenserwartungsjahre im Vergleich zu 1952 für jedes einzelne Land angibt. Wir gruppieren nach Ländern und verwenden `mutate()`, um eine neue Variable zu erstellen. Die Funktion `first()` extrahiert dabei den ersten Wert aus einem Vektor. Beachtet, dass `first()` mit dem Vektor der Lebenserwartungen *in jeder Ländergruppe* arbeitet.
```{r}
my_gap |>
group_by(country) |>
select(country, year, lifeExp) |>
mutate(lifeExp_gain = lifeExp - first(lifeExp)) |>
filter(year < 1963)
```
Innerhalb eines Landes nehmen wir die Differenz zwischen der Lebenserwartung im Jahr $i$ und der Lebenserwartung im Jahr 1952. Daher sehen wir für 1952 immer Nullen und für die meisten Länder eine Folge von positiven und steigenden Zahlen.
### Window Funktionen {#window-functions}
Window Funktionen nehmen eine Eingabe der Länge $n$ und berechnen eine Ausgabe derselben Länge. Diese Ausgabewerte hängen dabei von allen Eingabewerten ab. So ist z.B. `rank()` eine Window Funktion, aber `log()` ist es nicht.
Betrachten wir noch einmal die schlechtesten und besten Lebenserwartungen in Asien im Laufe der Zeit, behalten aber Informationen darüber bei, *welches* Land diese Extremwerte beisteuert.
```{r}
my_gap |>
filter(continent == "Asia") |>
select(year, country, lifeExp) |>
group_by(year) |>
filter(min_rank(desc(lifeExp)) < 2 | min_rank(lifeExp) < 2) |>
arrange(year) |>
print(n = Inf) # erzwingt eine Ausgabe aller Zeilen
```
Wir sehen, dass (min = Afghanistan, max = Japan) das häufigste Ergebnis ist, aber Kambodscha und Israel tauchen auch jeweils mindestens einmal als min bzw. max auf.
> Aber wäre es nicht schön eine Zeile pro Jahr zu haben?
Zuerst sollten wir uns aber vielleicht nochmal fragen wie das eigentlich funktioniert hat? Dazu schauen wir uns die Beobachtungen aus Asien mal direkt an.
```{r}
(asia <- my_gap |>
filter(continent == "Asia") |>
select(year, country, lifeExp) |>
group_by(year))
```
Jetzt wenden wir die Window Funktion `min_rank()` an. Da `asia` nach Jahren gruppiert ist, operiert `min_rank()` innerhalb von Mini-Datensätzen. Auf die Variable `lifeExp` angewandt, liefert `min_rank()` den Rang der beobachteten Lebenserwartung jedes Landes.
:::: {.content-box-gray}
__Bemerkung:__ Der `min`-Teil im Funktionsnamen `min_rank()` gibt nur an, wie im Fall von gleichen Beobachtungswerten die Ränge bestimmt werden.
```{r}
rank(c(1,3,3,5), ties.method = "min")
```
Neben dem Minimum gibt es aber auch noch eine Reihe weiterer Alternativen, wie z.B. den Durchschnitt
```{r}
rank(c(1,3,3,5))
```
::::
Im nächsten Schritt schauen wir uns die Ränge der Lebenserwartung innerhalb eines Jahres mal explizit für ein paar Länder (Afghanistan, Japan und Thailand) an - sowohl in der (Standard-) aufsteigenden als auch in der absteigenden Reihenfolge.
```{r}
asia |>
mutate(le_rank = min_rank(lifeExp),
le_desc_rank = min_rank(desc(lifeExp))) |>
filter(country %in% c("Afghanistan", "Japan", "Thailand"), year > 1995)
```
Afghanistan neigt dazu, 1 in der `le_rank`-Variablen zu haben, Japan neigt dazu, 1 in der `le_desc_rank`-Variablen zu haben und andere Länder, wie Thailand, zeigen deutlich weniger extreme Ränge.
Damit sollte der ursprüngliche `filter()` Befehl
```{r eval = FALSE}
filter(min_rank(desc(lifeExp)) < 2 | min_rank(lifeExp) < 2)
```
auch klar sein.
Diese beiden Sätze von Rängen werden on-the-fly, innerhalb der Jahresgruppe, gebildet, und `filter()` behält alle Zeilen, die einen Rangwert kleiner als 2 haben. Da wir dies für aufsteigende und absteigende Ränge machen, erhalten wir sowohl die Beobachtungen mit dem minimalen als auch dem maximalen Rang.
Wenn wir nur das Minimum ODER das Maximum gewollt hätten, hätte auch ein alternativer Ansatz mit `slice_min()` bzw. `slice_max()` funktioniert.
```{r}
my_gap |>
filter(continent == "Asia") |>
select(year, country, lifeExp) |>
arrange(year) |>
group_by(year) |>
# slice_min(lifeExp, n = 1) ## für das Minimum
slice_max(lifeExp, n = 1) ## bzw. das Maximum
```
## Großes Finale
Beantworten wir also die Frage:
:::: {.content-box-green}
"In welchem Land ist die Lebenserwartung innerhalb von 5 Jahren am stärksten gesunken?"
::::
Die Beobachtungsfrequenz im Datensatz ist fünf Jahre, d.h. wir haben Daten für 1952, 1957 usw. Dies bedeutet also, dass die Veränderungen der Lebenserwartung zwischen benachbarten Zeitpunkten betrachtet werden müssen. Dazu verwenden wir die `lag()` Funktion. Diese veschiebt die Einträge des Inputvektors um ein Lag `k`.
Wir können aber noch mehr erreichen. Lasst uns die Frage pro Kontinenten beantworten.
```{r end_dplyr_single}
my_gap |>
group_by(continent, country) |>
# für jedes Land werden die Unterschiede berechnet
mutate(delta = lifeExp - lag(lifeExp, n = 1)) |>
## für jedes Land wird nur der kleinste Wert behalten und alle Werten mit fehlendem Wert für delta werden nicht berücksichtigt
summarise(worst_delta = min(delta, na.rm = TRUE)) |>
## nun wird noch pro Kontinent, die Zeile mit dem kleinsten Wert ausgegeben
slice_min(worst_delta, n = 1) |>
arrange(worst_delta)
```
Auch folgendes Code würde dasselbe Ergebnis liefern
```{r end_dplyr_single_2}
my_gap |>
group_by(continent, country) |>
# für jedes Land werden die Unterschiede berechnet
mutate(delta = lifeExp - lag(lifeExp, n = 1)) |>
## wir entfernen alle Beobachtungen, wo der Wert von delta fehlt
filter(!is.na(delta)) |>
## für jedes Land wird nur der kleinste Wert behalten
summarise(worst_delta = min(delta)) |>
## nun wird noch pro Kontinent, die Zeile mit dem kleinsten Wert ausgegeben
slice_min(worst_delta, n = 1) |>
arrange(worst_delta)
```
Wenn wir auch die Information bezüglich des Jahres behalten möchten, können wir eine weitere Variable zum `summarize()` hinzufügen
```{r end_dplyr_single_3}
my_gap |>
group_by(continent, country) |>
# für jedes Land werden die Unterschiede berechnet
mutate(delta = lifeExp - lag(lifeExp, n = 1)) |>
## wir entfernen alle Beobachtungen, wo der Wert von delta fehlt
filter(!is.na(delta)) |>
## für jedes Land wird nur der kleinste Wert behalten
summarise(worst_delta = min(delta), year = year[which.min(delta)]) |>
## nun wird noch pro Kontinent, die Zeile mit dem kleinsten Wert ausgegeben
slice_min(worst_delta, n = 1) |>
arrange(worst_delta)
```
Denkt ruhig eine Weile über das Ergebnis nach. Hier sieht man in trockenen Statistiken über die durchschnittliche Lebenserwartung, wie Völkermord aussieht.
Um den Code besser zu verstehen, unterteilt ihn, beginnend von oben, in Stücke und überprüft die einzelnen Zwischenergebnisse. So wurde der Code auch geschrieben/entwickelt, mit Fehlern und Verfeinerungen auf dem Weg.
## Literatur
An dieser Stelle sei noch auf die [dplyr Webseite](https://dplyr.tidyverse.org/) und das Kapitel
[Data transformation][r4ds-transform] in [R for Data Science] [@wickham2016] verwiesen.
```{r links, child="links.md"}
```