Grammar of Graphics und ggplot2

Grammar of Graphics

Hadley Wickhams Paket ggplot2 (Wickham, 2016) versucht, die Erstellung von Grafiken in einer einheitlichen Grammatik, der “grammar of graphics”, auszudrücken. Das Ziel hier ist es, nicht mehr in “Scatterplot” und “Boxplot” als einzelne Kategorien zu denken und diese einzeln erstellen lernen zu müssen, sondern alle Abbildungen mit derselben Logik erstellen zu können.

In Seinem Paper (Wickham, 2010) werden die folgenden Komponenten als grundlegende Bausteine einer Grafik eingeführt:

  • a default dataset and set of mappings from variables to aesthetics,
  • one or more layers, with each layer having one geometric object, one statistical trans- formation, one position adjustment, and optionally, one dataset and set of aesthetic mappings,
  • one scale for each aesthetic mapping used,
  • a coordinate system,
  • the facet specification. (Wickham, 2010, p. 8)

Komponenten eines Plots

Wir müssen für einen Plot also überlegen:

  1. welche Daten wir auf welche Aesthetics mappen
  2. welche geometrischen Objekte wir in welcher Reihenfolge auf die Grafik layer wollen und ob diese optionale andere Daten benötigen
  3. welche Skala wir für die Mappings nutzen wollen
  4. welches Koordinatensystem wir nutzen wollen
  5. in welchen Facetten wir die Daten darstellen wollen

Komponenten in ggplot2

Beispieldaten

Pinguine im Eis

Im palmerpenguins-Paket werden Pinguin-Beobachtungen der Palmer-Station in der Antarktis zur Verfügung gestellt:

palmerpenguins::penguins %>% 
  head()
# A tibble: 6 × 8
  species island    bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
  <fct>   <fct>              <dbl>         <dbl>             <int>       <int>
1 Adelie  Torgersen           39.1          18.7               181        3750
2 Adelie  Torgersen           39.5          17.4               186        3800
3 Adelie  Torgersen           40.3          18                 195        3250
4 Adelie  Torgersen           NA            NA                  NA          NA
5 Adelie  Torgersen           36.7          19.3               193        3450
6 Adelie  Torgersen           39.3          20.6               190        3650
# ℹ 2 more variables: sex <fct>, year <int>

1. Daten und Aesthetics - ggplot() + aes()

Wir wollen den Zusammenhang zwischen Körpergewicht und Schnabellänge über die Spezies betrachten. Dafür legen wir die “Leinwand” des Plots mit den zentralen mappings an:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species))

Dabei können natürlich je nach geom(s), die aufgeschaltet werden, unterschiedliche mappings relevant sein. Welche jeweils nötig sind, lässt sich auf der Hilfeseite des entsprechenden geoms nachlesen.

Beispiele für mögliche Werte für die meisten mappings lassen sich in der ggplot2-Vignette zum Thema (vignette("ggplot2-specs")) finden.

2. Geometrische Objekte - geom_*

Diesem Plot fügen wir Punkte als geometrische Objekte hinzu, die uns zu einem Scatterplot führen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point()

3. Skalen - scale_*

Die Symbole und Farben haben genau wie x- und y- Koordinaten als ästhetische Mappings eigene Skalen. Wenn uns also die Farben nicht passen, können wir einfach eine andere Skala setzen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point() +
  scale_color_viridis_d()

4. Koordinatensystem coord_*

Das Koordinatensystem passt von der Auflösung erstmal, aber wir wollen eine direkte Zuordnung von 10 mm Schnabellänge zu 1000 g Körpermasse. Dazu fügen wir eine coord_*-Spezifikation an:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point() +
  scale_color_viridis_d()+
  coord_fixed(ratio = 10/1000)

5. Facetten - facet_*

Als letzte Komponente überlegen wir uns, dass die verschiedenen Beobachtungspunkte als Einteilung interessant sein könnten und wir diese getrennt betrachten wollen. Mit der facet-Familie können wir den Graphen nach Indel facettieren:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point() +
  scale_color_viridis_d()+
  coord_fixed(ratio = 10/1000) +
  facet_wrap(~island)

Grammar of Graphics in ggplot2

Einfache Grafiken

Neben den dem Point-geom gibt es in ggplot2 natürlich auch so gut wie alle anderen geoms, die für gängige Plots nötig sind. 1

1 Daneben gibt es einen riesigen Stamm an Paketen, die weniger gängige Plot-Typen als geoms implementieren, z.B. ggwordcloud für wordclouds, ggalluvial für Alluvial und Sankey-Diagramme, ggnet für Netzwerk-Plots und das bei meinen Studis sehr beliebte ggpubr für ‘publication ready plots’. Auf cran sind im Moment 213 Pakete gelistet, die mit “gg” anfangen und ggplot2 imoprtieren oder von ggplot2 abhängig sind.

Die folgende Auswahl ist nach dem System des sehr zu empfehlenden Cheat-Sheets von posit zu ggplot2 sortiert und nur ein Bruchteil der in R angelegten geoms.

Für die Darstellung einer numerischen Variable bieten Histogramme und ähnliche Darstellungen der Verteilungen einer Variable an.

In Abbildung 6.1 sind vier Beispiele, wie sich mit ggplot2 eine numerische Variable darstellen ließe. Alle diese Plots haben den folgenden ggplot-Call als Grundlage:

palmerpenguins::penguins %>% 
  ggplot(aes(x = flipper_length_mm))
(a) + geom_histogram()
(b) + geom_dotplot()
(c) + geom_area(stat= ‘bin’)
(d) + geom_freqpoly()
Abb 6.1: Eine Auswahl der in ggplot2 angelegten geoms, die die Verteilung einer Variable abbilden.

Alle vier Beispiele berechnen dabei die bins in die die Beobachtungen einsortiert werden als Standard so, dass 30 Gruppen entstehen.

Welches der auf der geom_histogram()-Hilfeseite genannten Argumente hilft mir am ehesten, die Bin-Breite auf 5mm flipper-Length zu setzen?

binwidth
The width of the bins. Can be specified as a numeric value or as a function that calculates width from unscaled x. Here, “unscaled x” refers to the original x values in the data, before application of any scale transformation. When specifying a function along with a grouping structure, the function will be called once per group. The default is to use the number of bins in bins, covering the range of the data. You should always override this value, exploring multiple widths to find the best to illustrate the stories in your data.

palmerpenguins::penguins %>% 
  ggplot(aes(x = flipper_length_mm)) +
  geom_histogram(binwidth=5)

Im Hilfetext wird auch die Möglichkeit eines Funktionsaufrufs genannt. Wenn wir zum Beispiel 5% des Abstands zwischen Maximum und Minimum des Wertebereichs als Binwidth setzen wollen, können wir den folgenden Call mit Lambda-Funktion benutzen. Lambda-Funktionen sind anonyme Funktionen, für die in R 4.1.0 die Kurzschreibweise \(x) <body> statt function(x) <body> eingeführt wurde.

palmerpenguins::penguins %>% 
  ggplot(aes(x = flipper_length_mm)) +
  geom_histogram(binwidth=\(x) .05 * (max(x, na.rm=T) - min(x, na.rm=T)))

Antwort aufdecken

Für eine diskrete Variable kann mit einem Barchart die Verteilung illustriert werden:

palmerpenguins::penguins %>% 
  ggplot(aes(x = island)) +
  geom_bar()

In Abbildung 6.2 wird die Basis des geom_point-Blocks aus dem Anfang des ggplot-Abschnitts für alle vier Plots genutzt. Die Basis ist also in jedem Fall der folgende Code-Schnipsel:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g))
(a) + geom_point()
(b) + geom_quantile()
(c) + geom_smooth(method = ‘lm’)
(d) + geom_rug(sides = ‘bl’)
Abb 6.2: Eine Auswahl der in ggplot2 angelegten geoms, die die Verteilung einer Variable abbilden.

Typische Darstellungen, die diskrete und kontinuierliche Variablen kombinieren, sind Darstellungen von Verteilungsvergleichen. Zum Beispiel könnte der Vergleich der Verteilungen der Schnabel-Länge zwischen den Spezies mit folgendem Call angelegt werden:

palmerpenguins::penguins %>% 
  ggplot(aes(x = species, 
             y = bill_length_mm,
             color = species))

In Abbildung 6.3 sind drei Möglichkeiten abgetragen, wie sich diese Basis für eine Darstellung nutzen ließe.

(a) + geom_boxplot()
(b) + geom_dotplot(binaxis = ‘y’, stackdir = ‘center’)
(c) + geom_violin(scale = ‘area’)
Abb 6.3: Eine Auswahl der in ggplot2 angelegten geoms, die die Verteilung einer Variable abbilden.

Natürlich lassen sich diese beide Variablen auch mit dem Psychologen-Favourite der Mittelwert-Barcharts darstellen.

Dazu gibt es zwei Möglichkeiten. Entweder wir bauen eine Pipeline, die gruppierte Mittelwerte berechnet und darstellt wie im folgenden Beispiel:

palmerpenguins::penguins %>%
  group_by(species) %>% 
  summarise(bill_length_mm = mean(bill_length_mm, na.rm = T)) %>% 
  ggplot(aes(x = species, 
             y = bill_length_mm,
             fill = species)) +
  geom_col()

Alternativ können wir das ggplot-Feature nutzen, Berechnungen im data-Argument eines geoms anzugeben:

palmerpenguins::penguins %>%
  ggplot(aes(x = species, 
             y = bill_length_mm,
             fill = species)) +
  geom_col(data = ~ group_by(.x, species) %>% 
             summarise(bill_length_mm = mean(bill_length_mm, na.rm=T)))

Natürlich sind viele der schon genannten geoms auch nutzbar, um drei Variablen darzustellen. Mit unserem Beispiel vom Anfang hatten wir z.B. ja schon drei Variablen (species, bill_length_mm und body_mass_g) auf jeweils eine “Ästhetische Dimension” gemapped.

Für zum Beispiel Korrelationsdarstellungen könnte ergänzend aber noch geom_tile ganz interessant sein. Dazu berechnen wir einmal die Korrelation zwischen allen numerischen Variablen in unserem Datensatz:

palmerpenguins::penguins %>% 
  select(where(is.numeric)) %>% 
  cor(., use = 'p')
                  bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
bill_length_mm        1.00000000   -0.23505287         0.6561813  0.59510982
bill_depth_mm        -0.23505287    1.00000000        -0.5838512 -0.47191562
flipper_length_mm     0.65618134   -0.58385122         1.0000000  0.87120177
body_mass_g           0.59510982   -0.47191562         0.8712018  1.00000000
year                  0.05454458   -0.06035364         0.1696751  0.04220939
                         year
bill_length_mm     0.05454458
bill_depth_mm     -0.06035364
flipper_length_mm  0.16967511
body_mass_g        0.04220939
year               1.00000000

Mit ein bisschen R-Magie machen wir daraus einen ggplot-kompatiblen Datensatz mit den drei Variablen x-Dimension, y-Dimension und Korrelation:

Code
r <- palmerpenguins::penguins %>%
  select(where(is.numeric)) %>%
  cor(., use = 'p')

r <- tibble(x = rep(row.names(r), nrow(r)),
       y = rep(colnames(r), each=nrow(r)),
       r = c(r))

r
# A tibble: 25 × 3
   x                 y                    r
   <chr>             <chr>            <dbl>
 1 bill_length_mm    bill_length_mm  1     
 2 bill_depth_mm     bill_length_mm -0.235 
 3 flipper_length_mm bill_length_mm  0.656 
 4 body_mass_g       bill_length_mm  0.595 
 5 year              bill_length_mm  0.0545
 6 bill_length_mm    bill_depth_mm  -0.235 
 7 bill_depth_mm     bill_depth_mm   1     
 8 flipper_length_mm bill_depth_mm  -0.584 
 9 body_mass_g       bill_depth_mm  -0.472 
10 year              bill_depth_mm  -0.0604
# ℹ 15 more rows

Diese drei Dimensionen können wir mit geom_tile darstellen:

r %>% 
  ggplot(aes(x, y, fill = r)) +
  geom_tile() 

Alternativ gibt es selbstverständlich auch ein Paket, das ggplot2-basierte Korrelationsmatrizen darstellt:

r <- palmerpenguins::penguins %>%
  select(where(is.numeric)) %>%
  cor(., use = 'p')


ggcorrplot::ggcorrplot(r)

Aufgaben

Aufgabe 1

Lesen Sie den Datensatz “worldbank_indicators.xlsx” ein. In ihm sind Daten von der worldbank zu verschiedenen Indikatoren in mehreren Ländern für die Jahre von 2000 bis 2021 vorgehalten.

Wählen Sie ein Jahr aus und filtern Sie den Datensatz, so dass nur noch dieses Jahr enthalten ist.

Erstellen Sie mit den Daten

  1. eine Grafik mit einer kontinuierlichen Variable

  2. eine Grafik mit zwei kontinuierlichen Variablen

  3. eine Grafik mit dem Länder-Namen und einer kontinuierlichen Variable Ihrer Wahl

library(readxl)

worldbank_indicators <- read_excel("data/worldbank_indicators.xlsx")

one_year <- worldbank_indicators %>% 
  filter(Year == 2019)

one_year %>% 
  ggplot(aes(x = `Life expectancy at birth, total (years)`)) +
  geom_histogram(fill = 'white',
                 color = 'darkblue')
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

one_year %>% 
  ggplot(aes(`Life expectancy at birth, total (years)`,
             `GDP per capita (current US$)`,
             color = `Country Name`)) +
  geom_point()

one_year %>% 
  ggplot(aes(`Country Name`,
             `Total alcohol consumption per capita (liters of pure alcohol, projected estimates, 15+ years of age)`,
             fill = `Country Name`)) +
  geom_col() +
  coord_flip()

Antwort aufdecken

Aufgabe 2

Lesen Sie die Hilfeseite für geom_line. Erstellen Sie ein Liniendiagramm mit der Zeit auf der x-Achse und einer Variable Ihrer Wahl auf der y-Achse.

Sollten Sie noch Zeit haben, fügen Sie dem Datensatz eine Variable hinzu, die den Mittelwert des Alkoholkonsums über die Jahre pro Land enthält. Nutzen Sie diese Variable um eine neue Variable zu erstellen, die einen Median-Split enthält. Sie müssen dafür wahrscheinlich einmal ungroup aufrufen.

Facettieren Sie das Liniendiagramm nach diesem Split.

worldbank_indicators %>% 
  ggplot(aes(Year,
             `Life expectancy at birth, total (years)`,
             color = `Country Name`)) +
  geom_line()

worldbank_indicators %>% 
  group_by(`Country Name`) %>% 
  mutate(mean_consumption = mean(`Total alcohol consumption per capita (liters of pure alcohol, projected estimates, 15+ years of age)`, na.rm = T)) %>% 
  ungroup() %>% 
  mutate(median_split = case_when(mean_consumption > median(mean_consumption) ~ 'Hoher Konsum',
                                  mean_consumption <= median(mean_consumption) ~ 'Weniger hoher Konsum')) %>% 
  ggplot(aes(Year,
             `Life expectancy at birth, total (years)`,
             color = `Country Name`)) +
  geom_line() +
  facet_wrap(~median_split)

Antwort aufdecken

Aufgabe 3

Erstellen Sie eine Grafik mit geom_tile auf Basis des Original-Datensatz. Auf der x-Achse sollen die Jahre zu sehen sein, auf der y-Achse die Länder. Die Flächen sollen so eingefärbt werden, dass der Quotient des GDPs und der Lebenserwartung sichtbar wird.

worldbank_indicators %>% 
  mutate(quotient = `GDP per capita (current US$)` / `Life expectancy at birth, total (years)`) %>% 
  ggplot(aes(Year,
             `Country Name`,
             fill = quotient)) +
  geom_tile()

Antwort aufdecken

Kombination mehrerer geoms

Plots müssen nicht nur aus einem geom bestehen.

Ein möglicher Fall ist der Wunsch nach der Darstellung von Summary-Statistics in Grafiken mit Rohdaten.

Im Pinguin-Scatter-Plot vom Anfang könnten wir uns zum Beispiel wünschen, die Mittelwerte pro Gruppe darzustellen. Dazu müssen wir zuerst diesen neuen Datensatz berechnen. :

penguin_means <- palmerpenguins::penguins %>% 
  group_by(species) %>% 
  summarise(across(c(bill_length_mm, 
                     body_mass_g), 
                   ~mean(., na.rm=T)))

…und auf den Plot in einem neuen Layer hinzufügen2:

2 alternativ würde natürlich wieder eine (Lambda-)Funktion im data-Argument funktionieren.

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point() +
  geom_point(data=penguin_means)+
  scale_color_viridis_d()

Für den Layer können wir dann auch wieder spezifische geometrische Eigenschaften einfügen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point() +
  geom_point(data=penguin_means, shape = 3)+
  scale_color_viridis_d()

Oder direkt ein neues Mapping einführen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original')) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'))+
  scale_color_viridis_d()

Und auch beide Varianten kombinieren:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3)+
  scale_color_viridis_d()

Die Kombination von mehreren geoms kann außerdem interessant sein, wenn wir verschiedene kombinieren wollen, um mehrere Aspekte abzubilden. So könnte man mehrere der in Einfache Grafiken besprochenen geoms zur Darstellung gleicher Daten auf einem Plot zusammenfügen.

In Abbildung 6.4 sind Beispiele dargestellt, die mit dem eingeklappten Chunk erstellt wurden, um mit Hilfe mehrerer geoms mehr Informationen abzubilden.

Code
palmerpenguins::penguins %>% 
  ggplot(aes(x = flipper_length_mm, 
             y = bill_length_mm,
             color = species)) +
  geom_point(alpha = .5) +
  geom_smooth(method = 'lm') +
  geom_quantile(lty = 3)

palmerpenguins::penguins %>% 
  ggplot(aes(x = species, 
             y = bill_length_mm,
             color = species)) +
  geom_violin(scale = 'area',
              aes(fill = species),
              alpha = .25) +
  geom_boxplot(width = 0.25,
               position = position_nudge(x = -0.125)) +
  geom_dotplot(aes(fill = species),
               binaxis = 'y', 
               stackdir = 'up',
               dotsize = 0.1)
(a) Zwei kontinuierliche Variablen (+1 diskrete)
(b) Je eine diskrete und eine kontinuierliche Variable
Abb 6.4: Abbildungen mit Kombinationen mehrerer geoms

Mehrere scales

Genauso wie die Verwendung mehrerer geoms, können natürlich auch mehrere Skalen gesetzt werden. So können wir im Scatter-Plot mit den Mittwelwerten zum Beispiel noch die Symbole für die unterschiedlichen dargestellten Daten anpassen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))

Aufgabe

Für diese Aufgabe benötigen Sie wieder den Worldbank-Datensatz.

Erstellen Sie ein Liniendiagramm, das für drei von Ihnen ausgewählte Länder die Entwicklung der Lebenserwartung für Frauen und Männer darstellt.

Die Länder sollen in eigenen Facetten zu sehen sein.

Passen Sie die scale der Farbe so an, dass Ihnen die Farben gefallen.

worldbank_indicators %>% 
  filter(`Country Name` %in% c('Germany', 'United States', 'United Kingdom')) %>% 
  ggplot(aes(x = Year)) +
  geom_line(aes(y = `Life expectancy at birth, female (years)`,
                color = 'female')) +
  geom_line(aes(y = `Life expectancy at birth, male (years)`,
                color = 'male')) +
  facet_wrap(~`Country Name`) +
  scale_color_manual(values = c('female' = 'royalblue1', 'male' = 'darkslategrey'))

Antwort aufdecken

Berechnen Sie dann die mittlere Lebenserwartung im ganzen Datensatz pro Jahr und Geschlecht.

Fügen Sie diese Verläufe nach der Facettierung der Grafik hinzu.

Passen Sie die Linientypen (linetype) so an, dass die Mittelwerte schraffiert und die tatsächlichen Werte durchgezogen sind.

mean_life_expectancy <- worldbank_indicators %>% 
  group_by(Year) %>% 
  summarise(across(matches('male'), mean))

worldbank_indicators %>% 
  filter(`Country Name` %in% c('Germany', 'United States', 'United Kingdom')) %>% 
  ggplot(aes(x = Year)) +
  geom_line(aes(y = `Life expectancy at birth, female (years)`,
                color = 'female',
                lty = 'country')) +
  geom_line(aes(y = `Life expectancy at birth, male (years)`,
                color = 'male',
                lty = 'country')) +
  facet_wrap(~`Country Name`)+
  geom_line(data = mean_life_expectancy,
            aes(y = `Life expectancy at birth, female (years)`,
                color = 'female',
                lty = 'average')) +
  geom_line(data = mean_life_expectancy,
            aes(y = `Life expectancy at birth, male (years)`,
                color = 'male',
                lty = 'average'))+
  scale_color_manual(values = c('female' = 'royalblue1', 'male' = 'darkslategrey')) +
  scale_linetype_manual(values = c('country' = 1, 'average' = 2))

Antwort aufdecken

Berechnen Sie zuletzt pro Land einen Mittelwert der Lebenserwartung pro Geschlecht über den Messzeitraum. Lesen Sie sich die Hilfeseite der geom_hline-Funktion durch und fügen Sie die Mittelwerte für die drei von Ihnen ausgewählten Länder zu der Grafik hinzu.

mean_per_country <- worldbank_indicators  %>% 
  filter(`Country Name` %in% c('Germany', 'United States', 'United Kingdom')) %>% 
  group_by(`Country Name`) %>% 
  summarise(across(matches('male'), mean))


worldbank_indicators %>% 
  filter(`Country Name` %in% c('Germany', 'United States', 'United Kingdom')) %>% 
  ggplot(aes(x = Year)) +
  geom_line(aes(y = `Life expectancy at birth, female (years)`,
                color = 'female',
                lty = 'country')) +
  geom_line(aes(y = `Life expectancy at birth, male (years)`,
                color = 'male',
                lty = 'country')) +
  geom_hline(data = mean_per_country,
             aes(yintercept = `Life expectancy at birth, female (years)`,
                color = 'female',
                lty = 'average')) +
  geom_hline(data = mean_per_country,
             aes(yintercept = `Life expectancy at birth, male (years)`,
                color = 'male',
                lty = 'average')) +
  facet_wrap(~`Country Name`)+
  geom_line(data = mean_life_expectancy,
            aes(y = `Life expectancy at birth, female (years)`,
                color = 'female',
                lty = 'average')) +
  geom_line(data = mean_life_expectancy,
            aes(y = `Life expectancy at birth, male (years)`,
                color = 'male',
                lty = 'average'))+
  scale_color_manual(values = c('female' = 'royalblue1', 'male' = 'darkslategrey')) +
  scale_linetype_manual(values = c('country' = 1, 'average' = 2))

Antwort aufdecken

Fortgeschrittene Grafiken mit ggplot2

Styling

ggplot2 hat neben den in der Grammar of Graphics beschriebenen Komponenten außerdem noch Gruppen von Funktionen die die Usability verbessern oder die Einstellung zur optischen Erscheinung neben den scales ermöglichen.

Die wohl wichtigste dieser Funktionen ist die labs-Funktion, mit der sich die Beschriftungen des Graphen anpassen lassen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))+
  coord_fixed(ratio = 10/1000) +
  facet_wrap(~island) +
  labs(x = 'Schnabellänge (mm)',
       y = 'Körpergewicht (g)',
       color = 'Pinguin-Spezies',
       shape = 'Aggregations-Niveau')

Für eine top-Level-Anpassung der optischen Erscheinung gibt es außerdem die Familie der theme_ Funktionen. In Abbildung 7.1 ist der Pinguin-Plot von eben mit allen in ggplot2 definierten themes kombiniert.

(a) theme_grey
(b) theme_bw
(c) theme_linedraw
(d) theme_light
(e) theme_dark
(f) theme_minimal
(g) theme_classic
(h) theme_void
(i) theme_test
Abb 7.1: Alle in ggplot2 angelegten Themes

Mir gefallen aus dieser Aufstellung “light” und “minimal” am Besten, die anderen sind aber je nach Anlass auch gute Startpunkte.

Unserem Plot können wir das theme wie alle bisherigen layer hinzufügen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))+
  labs(x = 'Schnabellänge (mm)',
       y = 'Körpergewicht (g)',
       color = 'Pinguin-Spezies',
       shape = 'Aggregations-Niveau') +
  theme_light()

In diesem Zusammenhang können wir auch gleich Base-Font und Schriftgröße setzen. theme_light setzt die kleinste Schriftgröße auf .8 mal die base_size3, wenn wir minimal 8pt große Schrift haben wollen, wie es zum Beispiel von der APA gefordert wird. Außerdem lässt sich eine APA-konforme Schriftart auswählen.

3 Diese Info findet man nicht auf der Hilfeseite, wenn man aber in RStudio eine Funktion markiert und F2 drückt, kann man den Quellcode einsehen. theme_light ist dabei auch ein gutes Beispiel als Ausgangpunkt für ein eigenes theme

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))+
  labs(x = 'Schnabellänge (mm)',
       y = 'Körpergewicht (g)',
       color = 'Pinguin-Spezies',
       shape = 'Aggregations-Niveau') +
  theme_light(base_family = 'Helvetica',
              base_size = 10)

Für genauere Kontrolle der optischen Eigenschaften kann die theme-Funktion benutzt werden. Wir kratzen nur mal an der Oberfläche der Möglichkeiten und verschieben die Legende an den unteren Rand des Graphen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))+
  labs(x = 'Schnabellänge (mm)',
       y = 'Körpergewicht (g)',
       color = 'Pinguin-Spezies',
       shape = 'Aggregations-Niveau') +
  theme_light(base_family = 'Helvetica',
              base_size = 10) +
  theme(legend.position = 'bottom')

Um die zu breite Beschriftung zu reparieren nutzen wir die guides-Funktion 4

4 ggplot2 hat eine ganze Familie an guide_-Funktionen, die unterschiedliche Legenden-Arten und Legenden-Anpassungen bieten - aus Zeitgründen sei hier aber nur auf deren Existenz verwiesen.

palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))+
  labs(x = 'Schnabellänge (mm)',
       y = 'Körpergewicht (g)',
       color = 'Pinguin-Spezies',
       shape = 'Aggregations-Niveau') +
  theme_light(base_family = 'Helvetica',
              base_size = 10) +
  theme(legend.position = 'bottom') +
  guides(color = guide_legend(nrow = 3),
         shape = guide_legend(nrow = 2))

Export

Zum Abschluss wollen wir die Grafiken natürlich exportieren.

Die Textgröße ist in pt gesetzt, deswegen sollten wir nach dem Export die Größe im besten Fall nicht mehr viel ändern.

Eine Din A4-Seite ist 8.2 x 11.6 Zoll groß. Wenn wir eine Grafik auf 80% der Seitenbreite haben wollen, brauchen wir also eine 6.56 Zoll breite Grafik.

Zum Speichern setzen wir unsere Grafik und die Maße in ggsave ein:

p <- palmerpenguins::penguins %>% 
  ggplot(aes(x = bill_length_mm, 
             y = body_mass_g,
             color = species)) + 
  geom_point(aes(shape = 'Original'),
             alpha = .5) +
  geom_point(data=penguin_means,
             aes(shape = 'Mittelwert'),
             size = 3) +
  scale_color_viridis_d(end = 0.7) +
  scale_shape_manual(values = c(Original = 20, Mittelwert = 3))+
  labs(x = 'Schnabellänge (mm)',
       y = 'Körpergewicht (g)',
       color = 'Pinguin-Spezies',
       shape = 'Aggregations-Niveau') +
  theme_light(base_family = 'Helvetica',
              base_size = 10) +
  theme(legend.position = 'bottom') +
  guides(color = guide_legend(nrow = 3),
         shape = guide_legend(nrow = 2))

ggsave(plot = p,
       filename = 'imgs/penguin_scatter.png',
       width = 6.56,units = 'in')
Saving 6.56 x 5 in image

Der Export sieht so aus:

Exportierte Grafik

Der Plot ist für meinen Geschmack etwas zu sehr vertikal geraten, das können wir im Export mit “hight” anpassen:

ggsave(plot = p,
       filename = 'imgs/shorter_penguin_scatter.png',
       width = 6.56, height = 6, units = 'in')

Convenient Standards

Die beiden theme-Funktionen müssten wir so an jede Grafik anfügen. Solche Wiederholungen sind schlechter Stil und stören beim Lesen des Skripts, deswegen bietet ggplot2 convenience-Funktionen um allgemeine Einstellungen zu setzen. Mit dem folgenden Snippet am Anfang des Skripts werden die Standards für alle Grafiken genutzt:

my_theme <-  theme_light(base_family = 'Helvetica',
              base_size = 10) +
  theme(legend.position = 'bottom')

theme_set(my_theme)

So kann ich jetzt beispielsweise einfach ein eingefärbtes Histogramm für die Flossen-Länge mit den gesetzten Standards erstellen:

palmerpenguins::penguins %>% 
  ggplot(aes(x = flipper_length_mm,
             fill = sex)) +
  geom_histogram(binwidth = 5)

Summary-Layer

Um einer Grafik deskriptive Statistiken hinzuzufügen, gibt es mehrere Möglichkeiten. Eine haben wir bereits genutzt, indem wir ein geom erstellt haben, dass einen anderen Wert für das data-Argument5 übergeben bekommt:

5 Oder halt wieder eine Lambda-Funktion als data-Argument

penguin_means <- palmerpenguins::penguins %>% 
  group_by(species) %>% 
  summarise(across(c(bill_length_mm, 
                     body_mass_g), 
                   ~mean(., na.rm=T)))
palmerpenguins::penguins %>% 
  ggplot(aes(x = species, 
             y = body_mass_g,
             color = species))+ 
  geom_boxplot(alpha = .5) +
  geom_point(data=penguin_means,
             size = 3)+
  scale_color_viridis_d()

Auf diese Art und Weise können wir natürlich beliebig viele Statistiken hinzufügen, so zum Beispiel Mittelwerte +/- SEMs und Text, der Anzahl der in die Mittelwerte einfließenden Werte beziffert:

penguin_means <- palmerpenguins::penguins %>% 
  group_by(species) %>% 
  summarise(across(c(bill_length_mm, 
                     body_mass_g), 
                   .fns = list(mean = ~mean(., na.rm=T),
                               n = ~sum(!is.na(.)),
                               sem = ~sd(., na.rm = T)/sqrt(sum(!is.na(.))))))

penguin_means %>% 
1  ggplot(aes(color = species,
                 x = species,
                 y = body_mass_g_mean)) +
2  geom_boxplot(data = palmerpenguins::penguins,
               aes(y = body_mass_g)) +
  geom_point(size = 3) +
3  geom_errorbar(aes(ymin = body_mass_g_mean - body_mass_g_sem,
                     ymax = body_mass_g_mean + body_mass_g_sem),
                width = 0.5, lty = 2) +
4  geom_text(aes(label = paste('N =', bill_length_mm_n)),
5            position = position_nudge(x = 0.175, y = 30),
            color = 'black') +
  scale_color_viridis_d()
1
Da nur noch der erste geom_point-Layer die Koordinaten aus dem original-Datensatz zieht ist das wirklich einheitliche Mapping auf den gerade erstellten Datensatz bezogen.
2
Alle Layer nach diesem nutzen den erstellten Datensatz - so rum muss nur dem ersten Layer ein extra-Datenargument übergeben werden.
3
geom_linerange und geom_errorbar brauchen Start- und Endpunkte der Fehlerbalken. Um die Koordinaten zu berechnen, können wir direkt im Mapping eine Funktion, hier den +-Operator aufrufen.
4
Mit paste fügen wir den String zusammen.
5
Mit position_nudge können wir die labels verschieben. Die genauen Werte kann man durch ausprobieren finden. Oder man nutzt das Paket ggrepel, das genau dieses Problem löst.

Ab einem bestimmten Punkt werden diese Operationen aber natürlich etwas unübersichtlich. Für diesen Fall bietet ggplot2 die stat_-Layer und die Lambda-Funktionen im data-Argument.

Derselbe Graph lässt sich damit auch wie folgt erstellen:

palmerpenguins::penguins %>% 
  ggplot(aes(color = species,
                 x = species,
                 y = body_mass_g)) + 
  geom_boxplot() +
  stat_summary() +
  geom_text(data = ~ group_by(., species) %>%  
              summarise(n = sum(!is.na(body_mass_g)),
                        body_mass_g = mean(body_mass_g, na.rm = T)),
            aes(label = paste('N =', n)),
            position = position_nudge(x = 0.175, y = 30),
            color = 'black') +
  scale_color_viridis_d()
No summary function supplied, defaulting to `mean_se()`

Dieser Graph ist auch ein gutes Beispiel, um Faktoren zur Gruppierung von kategorialen aesthetics zu besprechen.

Neben den bisher besprochenen atomaren Variablentypen logical, numeric und character gibt es in R noch das etwas speziellere Format der factors.

factoren sind kategoriale Variablen, deren levels eine Ordnung zugewiesen werden kann. ggplot2 sortiert kategoriale Variablen erstmal nach Alphabet, Faktor-levels werden aber berücksichtigt.

Wir können uns zum Beispiel wünschen, dass die Spezies in der Reihenfolge Gentoo, Chinstrap, Adelie aufgelistet werden, also genau umgekehrt.

Dazu können wir die species-Variable in einer Pipeline in einen Faktor umwandeln und die levels entsprechend unserer Vorstellung sortieren:

palmerpenguins::penguins %>%
  mutate(species = factor(species, levels = c('Gentoo', 'Chinstrap', 'Adelie'))) %>% 
  ggplot(aes(color = species,
                 x = species,
                 y = body_mass_g)) + 
  geom_boxplot() +
  stat_summary() +
  geom_text(data = ~ group_by(., species) %>%  
              summarise(n = sum(!is.na(body_mass_g)),
                        body_mass_g = mean(body_mass_g, na.rm = T)),
            aes(label = paste('N =', n)),
            position = position_nudge(x = 0.175, y = 30),
            color = 'black') +
  scale_color_viridis_d()
No summary function supplied, defaulting to `mean_se()`

Wie (so gut wie) immer bietet das tidyverse aber auch hier natürlich eine bequemere und flexiblere Lösung.

Mit dem forcats-Paket (Wickham, 2023) lassen sich Faktoren mit einer Reihe von Ordnungsfunktionen anordnen:

(a) forcats::fct_infreq(species) - nach Anzahl
(b) forcats::fct_reorder(species, body_mass_g) - nach Median einer anderen Variable
(c) forcats::fct_rev(species) - umgedreht
Abb 7.2: Drei Möglichkeiten, Faktoren mit forcats umzusortieren.

Aufgabe

Sie benötigen wieder die Worldbank-Daten.

Erstellen Sie einen Boxplot mit den Jahren von 2015 aufwärts auf der x-Achse und der Prevalence of severe food insecurity in the population (%) auf der y-Achse. Wandeln Sie dazu die Jahreszahlen in einen Faktor um.

Wenden Sie auf diesen Graphen ein theme Ihrer Wahl an.

worldbank_indicators %>% 
  filter(Year > 2015) %>% 
  mutate(Year = factor(Year)) %>% 
  ggplot(aes(x = Year,
             y = `Prevalence of severe food insecurity in the population (%)`)) +
  geom_boxplot() +
  theme_light()

Antwort aufdecken

Fügen Sie dem Graphen Mittelwerte als Punkte und Streuungen mit geom_errorbar hinzu.

Tun sie dies entweder mit einem neuen Datensatz oder gehen Sie die etwas anspruchsvollere Route, die Funktion (fun.data) der stat_summary-Funktion anzupassen.

means_and_sds <- worldbank_indicators %>% 
  filter(Year > 2015) %>% 
  mutate(Year = factor(Year)) %>% 
  group_by(Year) %>% 
  summarise(m = mean(`Prevalence of severe food insecurity in the population (%)`, na.rm = T),
            sd = sd(`Prevalence of severe food insecurity in the population (%)`, na.rm = T))

worldbank_indicators %>% 
  filter(Year > 2015) %>% 
  mutate(Year = factor(Year)) %>% 
  ggplot(aes(x = Year)) +
  geom_boxplot(aes(y = `Prevalence of severe food insecurity in the population (%)`)) +
  geom_point(data = means_and_sds,
             aes(y = m,
                 pch = 'mean')) +
  geom_errorbar(data = means_and_sds,
                aes(ymin = m - sd,
                    ymax = m + sd),
                lty = 3) +
  theme_light()

## alternativ:

range_fun <- \(x)tibble(y = mean(x, na.rm = T),
                                     ymin = y - sd(x, na.rm = T),
                                     ymax = y + sd(x, na.rm = T))

worldbank_indicators %>% 
  filter(Year > 2015) %>% 
  mutate(Year = factor(Year)) %>% 
  ggplot(aes(x = Year,
             y = `Prevalence of severe food insecurity in the population (%)`)) +
  geom_boxplot() +
  stat_summary(fun.data = range_fun,
               geom = "errorbar") +
  stat_summary(fun.data = range_fun,
               geom = "point") +
  theme_light()

Antwort aufdecken

Färben Sie die hinzugefügten Statistiken rot ein. Übersetzen Sie die Achsen-Beschriftungen auf Deutsch und setzen Sie die Überschrift der Farb-Skala auf einen leeren String (’’).

range_fun <- \(x)tibble(y = mean(x, na.rm = T),
                                     ymin = y - sd(x, na.rm = T),
                                     ymax = y + sd(x, na.rm = T))

worldbank_indicators %>% 
  filter(Year > 2015) %>% 
  mutate(Year = factor(Year)) %>% 
  ggplot(aes(x = Year,
             y = `Prevalence of severe food insecurity in the population (%)`)) +
  geom_boxplot() +
  stat_summary(fun.data = range_fun,
               geom = "errorbar",
               aes(color = 'M+/-SD')) +
  stat_summary(fun.data = range_fun,
               geom = "point",
               aes(color = 'M+/-SD')) +
  theme_light() +
  scale_color_manual(values = c('M+/-SD' = 'firebrick4')) +
  labs(color = '',
       y = 'Prävalenz von schwerer Ernährungsunsicherheit in der Bevölkerung (%)',
       x = 'Jahr')

Antwort aufdecken

Hier ist ein etwas angepasstes theme, das ich für mich erstellt habe:

my_theme <- \(){
  theme_minimal(base_size = 12) +
    theme(
      # Beschriftung allgemein:
      text = element_text(colour = "#335272", family = "DejaVu Sans",
                          lineheight = 1.1),
      
      # spezifische Beschriftungen:
      plot.title = element_text(colour = '#01284F', family = "DejaVu Sans", 
                                size = rel(1.6), margin = margin(12, 0, 8, 0)),
      plot.subtitle = element_text(size = rel(1.1), margin = margin(4, 0, 0, 0)),
      axis.text.y = element_text(colour = "#667E95", size = rel(0.8)),
      axis.title.y = element_text(size = rel(1), margin = margin(0, 4, 0, 0)),
      axis.text.x = element_text(colour = "#667E95", size = rel(0.8)),
      axis.title.x = element_text(size = rel(1), margin = margin(0, 4, 0, 0)),
      plot.caption = element_text(size = rel(0.8), margin = margin(8, 0, 0, 0)),
      
      # Formatierung der Legenden:
      legend.position = "top",
      legend.justification = 1,
      
      # Formatierung des Orientierungs-grids:
      panel.grid = element_line(colour = "#F3F4F5"),
      panel.grid.major.x = element_line(colour = "#F5F6F7", linetype = 1, linewidth = 0.2),
      panel.grid.minor.x = element_blank(),
      panel.grid.major.y =  element_line(colour = "#F5F6F7", linetype = 1, linewidth = 0.2),
      panel.grid.minor.y = element_blank(),
      
      # Formatierung der Einzüge und des Umrisses:
      plot.margin = margin(0.25, 0.25, 0.25, 0.25,"cm"),
      panel.border = element_rect(colour = "#F3F4F5", fill = NA, linetype = 1),
      panel.background = element_rect(fill = NA))
}

Benutzen Sie dieses Beispiel und die Hilfeseite der theme-Funktion als Orientierung, um Ihre Grafik so unansehnlich wie möglich zu gestalten.

worldbank_indicators %>% 
  filter(Year > 2015) %>% 
  mutate(Year = factor(Year)) %>% 
  ggplot(aes(x = Year,
             y = `Prevalence of severe food insecurity in the population (%)`)) +
  geom_boxplot() +
  stat_summary(fun.data = range_fun,
               geom = "errorbar",
               aes(color = 'M+/-SD')) +
  stat_summary(fun.data = range_fun,
               geom = "point",
               aes(color = 'M+/-SD')) +
  scale_color_manual(values = c('M+/-SD' = 'firebrick4')) +
  labs(color = '',
       y = 'Prävalenz von schwerer Ernährungsunsicherheit in der Bevölkerung (%)',
       x = 'Jahr') +
    theme(
      # Beschriftung allgemein:
      text = element_text(colour = "plum", family = "Zapf Dingbats",
                          lineheight = 1.1),
      
      # spezifische Beschriftungen:
      plot.title = element_text(colour = 'olivedrab1', family = "Zapf Dingbats", 
                                size = rel(1.6), margin = margin(12, 0, 8, 0)),
      plot.subtitle = element_text(size = rel(1.1), margin = margin(4, 0, 0, 0)),
      axis.text.y = element_text(colour = "hotpink", size = rel(0.8)),
      axis.title.y = element_text(size = rel(1), margin = margin(0, 4, 0, 0)),
      axis.text.x = element_text(colour = "black", size = rel(0.8)),
      axis.title.x = element_text(size = rel(1), margin = margin(0, 4, 0, 0)),
      plot.caption = element_text(size = rel(0.8), margin = margin(8, 0, 0, 0)),
      
      # Formatierung der Legenden:
      legend.position = "top",
      legend.justification = 1,
      
      # Formatierung des Orientierungs-grids:
      panel.grid = element_line(colour = "black"),
      panel.grid.major.x = element_line(colour = "black", linetype = 3, linewidth = 8),
      panel.grid.minor.x = element_blank(),
      panel.grid.major.y =  element_line(colour = "orange", linetype = 1, linewidth = 16),
      panel.grid.minor.y = element_blank(),
      
      # Formatierung der Einzüge und des Umrisses:
      plot.margin = margin(0.25, 0.25, 0.25, 0.25,"cm"),
      panel.border = element_rect(colour = "purple", fill = NA, linetype = 1),
      panel.background = element_rect(fill = "greenyellow"))

Antwort aufdecken

Literatur

Grolemund, G., & Wickham, H. (2016). R for Data Science. https://r4ds.had.co.nz/
Wickham, H. (2010). A layered grammar of graphics. Journal of Computational and Graphical Statistics, 19(1), 3–28.
Wickham, H. (2016). ggplot2: Elegant graphics for data analysis. Springer-Verlag New York. https://ggplot2.tidyverse.org
Wickham, H. (2023). Forcats: Tools for working with categorical variables (factors). https://CRAN.R-project.org/package=forcats