“make a hundred charts and pick one” — Amanda Cox

Vorbereitung

  1. Workshop-Material herunterladen: material.zip
  2. ZIP-Archiv an einem beliegen Ort entpacken
  3. Anschließend die Datei workshop-teil2.Rmd in RStudio öffnen

Daten einlesen

Zunächst laden wir die benötigten R-Pakete. readr stellt Funktionen zum einlesen von CSV Dateien bereit, dplyr liefert praktische Werkzeuge zum analysieren und transformieren von Tabellen, und ggplot2 ist das Visualisierungs-Framework das wir heute benutzen.

Die folgenen Code Blöcke (genannt “chunks”) lassen sich in RStudio durch Klick auf den grünen Pfeil oben rechts im Block ausführen.

needs(readr, dplyr, ggplot2)

# falls das nicht klappt, muss needs noch installiert werden
# install.packages('needs')
# library(needs)

theme_set(theme_light())

Nun laden wir den im ersten Teil vorbereiteten Datensatz btw-final.csv und speichern ihn in der Variable btw. Die mutate Anweisung in der zweiten Zeile können wir erstmal ignorieren (sie legt nur die Reihenfolge fest, in der die Werte in der Spalte gruppe nachher in den Diagrammen auftauchen).

btw <- read_csv('btw-final.csv') %>% 
  mutate(gruppe=factor(gruppe, c('L_SG', 'C_SG', 'C_RRG', 'L_RRG')))
## Parsed with column specification:
## cols(
##   .default = col_double(),
##   Nr = col_integer(),
##   Wahlkreis = col_character(),
##   Land = col_integer(),
##   Total = col_integer(),
##   CDU = col_integer(),
##   SPD = col_integer(),
##   CSU = col_integer(),
##   FDP = col_integer(),
##   GRÜNE = col_integer(),
##   LINKE = col_integer(),
##   RRG = col_integer(),
##   SG = col_integer(),
##   winner = col_character(),
##   landslide = col_logical(),
##   gruppe = col_character(),
##   Gemeinden = col_character()
## )
## See spec(...) for full column specifications.

Nach dem Ausführen des Blocks erscheint in RStudio oben links im “Environment” Fenster ein neuer Eintrag “btw”. Durch klick auf den Eintrag können wir uns den Datensatz genauer ansehen.

Fingerübungen mit ggplot

Kommen wir zum spannenden Teil: Daten visualisieren mit ggplot2. Die Syntax ist etwas gewöhnungsbedürftig, aber sehr mächtig wenn man erstmal den Dreh raus hat.

Die Funktion ggplot() macht allein erst mal nichts, außer einen leeren Plot zu erstellen und ihm einen Datensatz zuzuordnen.

ggplot(btw)

Über den zweiten Parameter mapping kann man Spalten aus dem Datensatz zu Plot-Eigenschaften (den sogenannten “aesthetics”) zuordnen, wie z.B. x, y oder color. ggplot erstellt dann ggf. schon automatisch Achsen und Gitternetzlinien.

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD))

Um nun Datenpunkte auf den Plot zu bekommen müssen wir weitere Ebenen zum Plot hinzufügen. Dazu “addieren” wir weitere ggplot-Funktionen zu dem Ergebnis des ersten ggplot() Aufrufs. In diesem Fall wollen wir Punkte, also benutzen wir geom_point():

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point()

Plot-Eigenschaften anpassen

Jede ggplot Funktion hat selbst eine Reihe von Parametern über die das Aussehen der Plot-Ebene angepasst werden kann.

Im Falle von geom_point lassen sich Form, Farbe, Größe und viele weitere visuelle Eigenschaften der Symbole ändern (für eine Liste aller möglichen Parameter können wir die Hilfefunktion benutzen):

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red', shape=2)

Anstatt die Eigenschaften nur “statisch” für alle Punkte gleichermaßen zu verändern kann man auch weitere Spalten aus dem Datensatz verwenden und so neue Dimensionen zu dem Plot hinzufügen.

Im folgenden Beispiel färben wir die Symbole je nach dem Wert der Spalte landslide, und wir machen die Symbolgröße abhängig von dem Wert in der Spalte Ohne.Hauptschulabschluss.

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(aes(color=landslide, size=Ohne.Hauptschulabschluss), alpha=0.5)

Weitere Ebenen hinzufügen

Man kann auch noch weitere Ebenen zum Plot hinzufügen, indem man noch mal geom_point() hinzufügt und die y aesthetic überschreibt.

Hier fügen wir pro Wahlkreis noch einen weiteren Punkt zum Plot hinzu aber benutzen das CDU Wahlergebnis für die y-Koordinaten:

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDU), color='black', shape=3)
## Warning: Removed 45 rows containing missing values (geom_point).

Die Warnung “Removed 45 rows containing missing values” kommt daher, das in den bayerischen Wahlkreisen nicht CDU sondern CSU gewählt wird. Wir können das Problem lösen, in dem wir eine neue Spalte CDUCSU anlegen, in der beide Parteien zusammengefasst werden:

btw <- btw %>% 
  mutate(CDUCSU=coalesce(CDU, CSU))

Wenn wir den obigen Plot nun noch einmal für die neue CDUCSU Spalte erzeugen ist die Warnung verschwunden:

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDUCSU), color='black', shape=3)

Logarithmische Achse

Da wir feststellen dass sich fast alle Punkte sich an den linken Rand des Plots drängen, bietet sich vielleicht eine logarithmische x-Achse an. Und auch das können wir mit einer weiteren “Addition” dem Plot hinzufügen:

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDUCSU), color='black', shape=3) +
  scale_x_log10()

Trend-Linien hinzufügen

Wie wäre es mit Trend-Linien für unsere beiden Punktmengen? Kein Problem mit der ggplot-Funktion geom_smooth(). Auch hier fügen wir einen zweiten Aufruf für die CDU Punkte ein und überschreiben die y Eigenschaft.

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDUCSU), color='black', shape=3) +
  geom_smooth(color='red') +
  geom_smooth(aes(y=CDUCSU), color='black') +
  scale_x_log10()
## `geom_smooth()` using method = 'loess'
## `geom_smooth()` using method = 'loess'

Standardmäßig benutzt geom_smooth() eine Loess-Funktion zur Annäherung der Messwerte und zeigt im Hintergrund einen grauen Bereich für das Vertrauensinterval an. Mit dem Parameter method='lm' legen wir fest das wir eine gerade (lineare) Trendlinie möchten, und mit se=FALSE schalten das Vertrauensinterval ab.

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDUCSU), color='black', shape=3) +
  geom_smooth(color='red', method='lm', se=FALSE) +
  geom_smooth(aes(y=CDUCSU), color='black', method='lm', se=FALSE) +
  scale_x_log10()

Vertikale min/max Linien

Oder wie wäre es mit vertikalen Linien die die für jeden Wahlkreis beiden Punkte verbinden? Dazu können wir z.B. die Funktion geom_linerange verwenden, der wir nur die Parameter ymin und ymax mitteilen müssen.

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDUCSU), color='black') +
  geom_linerange(aes(ymin=SPD, ymax=CDUCSU), alpha=0.2) +
  scale_x_log10()

Small multiples

Es kommt noch besser: mit facet_grid können wir den Plot in mehrere Panels unterteilen, z.b. nach landslide Wahlkreisen und nicht-landslide Wahlkreisen:

ggplot(btw, aes(x=Bevölkerungsdichte, y=SPD)) +
  geom_point(color='red') +
  geom_point(aes(y=CDUCSU), color='black') +
  geom_linerange(aes(ymin=SPD, ymax=CDUCSU), alpha=0.2) +
  scale_x_log10() +
  facet_grid(. ~ landslide)

Wir wir sehen sind die Möglichkeiten fast endlos. Doch nun zurück zu dem was wir eigentlich machen wollten: Barcodes!

Barcodes visualisieren!

Barcodes sind nichts anderes als vertikale Linien, also fangen wir an mit geom_vline. geom_vline benötigt die aesthetic xintercept, die wir erstmal auf die Spalte Bevölkerungsdichte mappen.

ggplot(btw) +
  geom_vline(aes(xintercept=Bevölkerungsdichte, color=gruppe))

Zunächst mal möchten wir bessere Farben benutzen, damit wir die Gruppen besser lesen können. Dazu definieren wir zunächst ein paar Farben unserer Wahl (schwarz für schwarz-gelb und rot für rot-rot-grün) und fügen diese dann per scale_colour_manual dem Plot hinzu:

mycolors <- c(L_SG='#333333', C_SG='#999999', L_RRG='#cc0000', C_RRG='#ee9999')

ggplot(btw) +
  geom_vline(aes(xintercept=Bevölkerungsdichte, color=gruppe)) +
  scale_colour_manual(values = mycolors)

Leider ist immer noch alles etwas zu unübersichtlich im Plot. Also benutzen wir wieder unseren Freund facet_grid(), um die Barcodes in vier Gruppen zu gliedern.

Mit guide=FALSE schalten wir in scale_colour_manual() die Farblegende ab, die nun überflüssig geworden ist.

ggplot(btw) +
  geom_vline(aes(xintercept=Bevölkerungsdichte, color=gruppe)) +
  scale_colour_manual(values = mycolors, guide=FALSE) +
  facet_grid(gruppe ~ .)

Natürlich können wir auch hier wieder eine logarithmische Achse benutzen, in dem wir die scale_x_log10() hinzufügen:

ggplot(btw) +
  geom_vline(aes(xintercept=Bevölkerungsdichte, color=gruppe)) +
  scale_colour_manual(values = mycolors, guide=FALSE) +
  facet_grid(gruppe ~ .) +
  scale_x_log10()

Die grauen Ränder um den Plot können wir entfernen in dem wir das theme_minimal() verwenden:

ggplot(btw) +
  geom_vline(aes(xintercept=Bevölkerungsdichte, color=gruppe)) +
  scale_colour_manual(values = mycolors, guide=FALSE) +
  facet_grid(gruppe ~ .) +
  scale_x_log10() +
  theme_minimal()

Damit wir in Zukunft nicht jedes Mal wieder das Theme hinzufügen müssen, setzen wir es jetzt mit theme_set() als neues Standard-Theme fest:

theme_set(theme_minimal())

Mittelwerte darüberlegen

Um unseren Plot abzuschließen möchten wir mit weiteren Linien die Gruppenmittelwerte des dargestellten Wertes zum Diagramm hinzufügen.

Dazu berechnen wir zunächst die Gruppenmittelwerte mit den dplyr Funktionen group_by() und summarize() und speichern sie in einem neuen Datensatz mittelwerte. Anschließend können wir die neuen Linien durch addieren eines weiteren geom_vline() Aufrufs zum Plot hinzufügen, wobei wir mit data=mittelwerte den Bezug zum neu erstellten Datensatz erzeugen:

mittelwerte <- btw %>% 
  group_by(gruppe) %>% 
  summarize(mittel=mean(Bevölkerungsdichte))

ggplot(btw) +
    geom_vline(aes(xintercept=Bevölkerungsdichte, color=gruppe)) +
    scale_colour_manual(values = mycolors, guide=FALSE) +
    facet_grid(gruppe ~ ., drop=T) +
    scale_x_log10() +
    geom_vline(aes(xintercept=mittel), data=mittelwerte, 
               color='blue', size=1.5)

Jetzt ist unser Plot soweit erstmal fertig!

Plot als benutzerdefinierte Funktion wiederverwenden

Damit wir den Plot einfacher wiederverwenden können möchten wir ihn in einer benutzerdefinierten Funktion ablegen, der wir einfach den Namen der zu visualisierenden Spalte übergeben können.

Im Prinzip passiert in der Funktion genau das, was wir bis hier hin alles gemacht haben. Was sich ändert ist, das wir zu Beginn der Funktion einen neuen Datensatz anlegen, in dem nur die Spalten gruppe und wert vorhanden sind, wobei letztere den Wert der übergebenden Spalte enthält.

Mit ggtitle() und xlab() steuern wir die Beschriftung der Diagramme und mit dem abschließenden theme() Aufruf passen wir die Textgröße sowie die Abstände rum um den Plot an.

barcode_plot <- function(spalte) {
  
  mycolors <- c(L_SG='#333333', C_SG='#999999',
                L_RRG='#cc0000', C_RRG='#ee9999')

  data <- btw %>% 
    filter(landslide) %>% 
    select_('gruppe', wert=as.name(spalte))
  
  mittelwerte <- data %>% 
    group_by(gruppe) %>% 
    summarize(mittel=mean(wert))

  data %>% 
    mutate(wert2=wert+rnorm(nrow(data), 0, 0.02)) %>%  # ein bisschen jittering
    ggplot() +
      geom_vline(aes(xintercept=wert2, color=gruppe)) +
      scale_colour_manual(values = mycolors, guide=FALSE) +
      facet_grid(gruppe ~ ., drop=T) +
      geom_vline(aes(xintercept=mittel), data=mittelwerte, 
                 color='blue', size=1.5) +
      ggtitle('', subtitle=paste0(spalte, '\n')) +
      xlab('') +
      theme(strip.text = element_text(size=7),
            plot.margin = margin(0,5,3,5,'mm'))
}

Nun können wir die Funktion einfach bequem aufrufen, z.B. für die Spalte Arbeitslosenquote:

barcode_plot('Arbeitslosenquote')

Oder Bergbau:

barcode_plot('Bergbau')

PDF mit allen Barcode Varianten speichern

Als krönenden Abschluss können wir mit wenigen weiteren Zeilen Code ein PDF mit Barcodes von allen Spalten in unserem Datensatz erzeugen.

Zunächst stellen wir sicher, das das Paket gridExtra geladen wird:

needs(gridExtra)

Dann legen wir einen Vektor von Spaltennamen an, die sich für die Visualisierung als Barcode eignen:

spalten <- colnames(btw)[18:55]

Mit lapply führen wir unsere barcode_plot Funktion dann mit einem Rutsch für alle Spalten in unserem Vektor auf. Wir leiten das Ergebnis direkt in die marrangeGrob Funktion weiter, die die einzelnen Plots auf mehrere Seiten verteilt anordnet (in einem 7-zeiligen und 1-spaltigen Layout):

plots <- lapply(spalten, barcode_plot) %>% 
  marrangeGrob(nrow=7, ncol=1, padding=unit(10, "mm"))

Anschließend benutzen wir ggsave um die arrangierten Plots in einer PDF-Datei im DIN A4 Format zu speichern.

ggsave("plots.pdf", plots, width=210, height=297, units='mm')

Extra: Weitere nützliche ggplot Funktionen

Falls wir noch Zeit am Ende haben, oder zum Weiterüben zu Hause:

Histogramme von Variablen verschiedener Gruppen vergleichen:

btw %>%
    ggplot(aes(Alter.unter18, fill=gruppe)) +
    geom_histogram(bins=40) +
    scale_fill_manual(values = mycolors, guide=FALSE) +
    facet_grid(gruppe ~ .)

Eine Alternative zu Histogrammen sind Density-Kurven:

btw %>%
    ggplot(aes(Alter.unter18, fill=gruppe)) +
    geom_density(bw=0.2, color=FALSE, alpha=0.8) +
    scale_fill_manual(values = mycolors, guide=FALSE) +
    facet_grid(gruppe ~ .)

Boxplots sind auch sehr beliebt:

btw %>%
    ggplot(aes(gruppe, Alter.unter18, fill=gruppe)) +
    geom_boxplot() +
    scale_fill_manual(values = mycolors, guide=FALSE) +
    coord_flip()

Violin Plots sind eine Mischung aus Boxplot und Density-Kurve:

btw %>%
    ggplot(aes(gruppe, Alter.unter18, fill=gruppe)) +
    geom_violin(adjust=0.5, color='black', alpha=0.8,
                draw_quantiles=c(0.25, 0.5, 0.75), trim=FALSE) +
    scale_fill_manual(values = mycolors, guide=FALSE) +
    coord_flip()