3 Transformation und Analyse mit dplyr

3.1 Die Stammdaten des 17. - 19. Bundestags

Dieses und das folgende Kapitel behandeln die Bereinigung, Transformation und deskriptive statistische Analyse von Daten mit dplyr sowie die graphische Analyse mit ggplot2. Zur Veranschaulichung analysieren wir eine Auswahl biographischer Daten der Bundestagsabgeordneten des 19. Bundestags. Die Daten stammen aus einer vom Bundestag bereitgestellten “.xml” Datei und wurden am 24.03.2022 unter folgender URL heruntergeladen: https://www.bundestag.de/services/opendata

Das Script zur Extraktion der interessierenden Variablen aus den XML Daten und der Konstruktion des hier genutzten tibbles, finden Sie in meinem GitHub: https://github.com/JakobTures/r-intro/blob/master/scripts/stammdaten_extract.R

Das Script speichert eine Tibble mit allen genutzten Daten in der Datei “stammdaten_17_19.RData”, die von Seminarteilnehmer*innen auch in Moodle heruntergeladen werden kann. Laden wir diese Daten zunächst in R. Da es sich bei dem Datenobjekt um eine Tibble handelt, müssen wir auch zunächst das tidyverse package laden. R kennt ohne dieses keine Tibbles und interpretiert das Objekt als Dataframe.

library(tidyverse)

load("stammdaten_17_19.RData")

Die Datei enthielt das Objekt stammdaten_17_19, eine Tibble, welche die Stammdaten aller Bundestagsabgeordneten der 17. - 19. Legislaturperiode enthält. Schauen wir uns die Tibble einmal an:

stammdaten_17_19
## # A tibble: 2,060 × 9
##       wp nachname  vorname   praefix geschlecht alter geb        Titel fraktion 
##    <int> <chr>     <chr>     <chr>   <chr>      <int> <chr>      <chr> <chr>    
##  1    19 Abercron  Michael   von     männlich      64 17.11.1952 Dr.   Union    
##  2    19 Achelwilm Doris     <NA>    weiblich      40 30.11.1976 <NA>  DIE LINK…
##  3    17 Ackermann Jens      <NA>    männlich      34 02.07.1975 <NA>  FDP      
##  4    19 Aggelidis Grigorios <NA>    männlich      52 19.08.1965 <NA>  FDP      
##  5    17 Ahrendt   Christian <NA>    männlich      46 07.05.1963 <NA>  FDP      
##  6    17 Aigner    Ilse      <NA>    weiblich      44 07.12.1964 <NA>  Union    
##  7    19 Akbulut   Gökay     <NA>    weiblich      34 16.11.1982 <NA>  DIE LINK…
##  8    17 Aken      Jan       van     männlich      48 01.05.1961 <NA>  DIE LINK…
##  9    18 Aken      Jan       van     männlich      52 01.05.1961 <NA>  DIE LINK…
## 10    18 Albani    Stephan   <NA>    männlich      45 03.06.1968 <NA>  Union    
## # … with 2,050 more rows

Die Tibble enthält 2060 Beobachtungen. Zu jeder Beobachtung stehen 9 Variablen bereit. Wahlperiode, Nachname, Vorname, Namenspräfix (bspw. “von”), Geschlecht, Alter, Geburtsdatum, akademischer Titel sowie die Fraktion in welcher der/die MdB die Legislaturperiode begonnen hat. Der Datensatz ist damit so angelegt, dass für eine Abgeordnete eine Beobachtung für jede Wahlperiode vorliegt, in der sie im Bundestag vertreten war. War eine Abgeordnete in allen drei Wahlperioden MdB, liegen entsprechend auch drei separate Beobachtungen vor. Dies entspricht den Prinzipien der tidy data.

Wie Sie sehen, werden bei Tibbles standardmäßig nur die ersten 10 Zeilen gedruckt. Die Darstellung ist dadurch kompakt und übersichtlich. Eventuell könnten wir aber daran interessiert sein mehr als 10 Zeilen zu sehen. Ein Weg ist das Anklicken des Objekts im “Environment” Tab oder das Schreiben von View(objektname) in der console. Nun öffnet sich das Objekt in einem neuen Tab und kann komplett betrachtet werden. Ein anderer Weg ist es, R zu zwingen mehr Zeilen in die console abzudrucken. Dies ist mit der Funktion print() möglich. Als zweites Argument können wir mit n = angeben, wie viele Zeilen wir sehen möchten.

print(stammdaten_17_19, n = 20)
## # A tibble: 2,060 × 9
##       wp nachname   vorname       praefix geschlecht alter geb    Titel fraktion
##    <int> <chr>      <chr>         <chr>   <chr>      <int> <chr>  <chr> <chr>   
##  1    19 Abercron   Michael       von     männlich      64 17.11… Dr.   Union   
##  2    19 Achelwilm  Doris         <NA>    weiblich      40 30.11… <NA>  DIE LIN…
##  3    17 Ackermann  Jens          <NA>    männlich      34 02.07… <NA>  FDP     
##  4    19 Aggelidis  Grigorios     <NA>    männlich      52 19.08… <NA>  FDP     
##  5    17 Ahrendt    Christian     <NA>    männlich      46 07.05… <NA>  FDP     
##  6    17 Aigner     Ilse          <NA>    weiblich      44 07.12… <NA>  Union   
##  7    19 Akbulut    Gökay         <NA>    weiblich      34 16.11… <NA>  DIE LIN…
##  8    17 Aken       Jan           van     männlich      48 01.05… <NA>  DIE LIN…
##  9    18 Aken       Jan           van     männlich      52 01.05… <NA>  DIE LIN…
## 10    18 Albani     Stephan       <NA>    männlich      45 03.06… <NA>  Union   
## 11    19 Albani     Stephan       <NA>    männlich      49 03.06… <NA>  Union   
## 12    18 Albsteiger Katrin        <NA>    weiblich      29 20.11… <NA>  Union   
## 13    17 Alpers     Agnes         <NA>    weiblich      48 29.06… <NA>  DIE LIN…
## 14    18 Alpers     Agnes         <NA>    weiblich      52 29.06… <NA>  DIE LIN…
## 15    19 Alt        Renata        <NA>    weiblich      52 27.08… <NA>  FDP     
## 16    19 Altenkamp  Norbert Maria <NA>    männlich      45 27.07… <NA>  Union   
## 17    17 Altmaier   Peter         <NA>    männlich      51 18.06… <NA>  Union   
## 18    18 Altmaier   Peter         <NA>    männlich      55 18.06… <NA>  Union   
## 19    19 Altmaier   Peter         <NA>    männlich      59 18.06… <NA>  Union   
## 20    19 Amthor     Philipp       <NA>    männlich      24 10.11… <NA>  Union   
## # … with 2,040 more rows

3.2 Datentransformation mit dplyr

Das dplyr package enthält eine Reihe von Funktionen zur Bereinigung, Transformation und deskriptiven Analyse von Daten. Wir beginnen damit, Zeilen und Spalten, die wir nicht benötigen, aus der Tibble zu entfernen, bevor wir die Daten in einigen bestehenden Spalten transformieren und neue Spalten aus den vorhandenen Daten erzeugen.

Empfohlene weiterführende Resourcen zu dplyr sind das entsprechende RStudio Cheatsheet: https://raw.githubusercontent.com/rstudio/cheatsheets/master/data-transformation.pdf sowie das Kapitel “Data transformation” in “R for Data Science”: https://r4ds.had.co.nz/transform.html

3.2.1 filter()

filter() dient dazu, Beobachtungen – also Zeilen – aus einer Tibble zu filtern. Die Funktion nimmt dabei die Daten, auf die sie angewandt werden soll, als erstes und eine oder mehrere expressions als zweites Argument. Diese Ausdrücke sind letztlich die Regeln, nach denen die Funktion filtern soll.

Zum Schreiben der Ausdrücke benötigen wir die in Kapitel 1.2.4 bereits eingeführten Vergleichsoperatoren. Diese vergleichen den Wert auf der linken Seite des Operators mit dem auf der rechten und geben als Ergebnis ein TRUE zurück, wenn der Test bestanden wurde, beziehungsweise FALSE, wenn er nicht bestanden wurde.

Wir könnten nun beispielsweise daran interessiert sein, nur die Daten bestimmter Fraktionen zu analysieren. Filtern wir zunächst die Stammdaten der Unions-Abgeordneten heraus. Wir können dazu in filter() testen, ob der Wert der Spalte “fraktion” gleich dem Wert “Union” ist. Bitte denken Sie daran, dass wir zum Vergleich zweier Werte immer == schreiben müssen, nicht =.

stammdaten_17_19 %>% 
  filter(fraktion == "Union")
## # A tibble: 830 × 9
##       wp nachname   vorname       praefix geschlecht alter geb    Titel fraktion
##    <int> <chr>      <chr>         <chr>   <chr>      <int> <chr>  <chr> <chr>   
##  1    19 Abercron   Michael       von     männlich      64 17.11… Dr.   Union   
##  2    17 Aigner     Ilse          <NA>    weiblich      44 07.12… <NA>  Union   
##  3    18 Albani     Stephan       <NA>    männlich      45 03.06… <NA>  Union   
##  4    19 Albani     Stephan       <NA>    männlich      49 03.06… <NA>  Union   
##  5    18 Albsteiger Katrin        <NA>    weiblich      29 20.11… <NA>  Union   
##  6    19 Altenkamp  Norbert Maria <NA>    männlich      45 27.07… <NA>  Union   
##  7    17 Altmaier   Peter         <NA>    männlich      51 18.06… <NA>  Union   
##  8    18 Altmaier   Peter         <NA>    männlich      55 18.06… <NA>  Union   
##  9    19 Altmaier   Peter         <NA>    männlich      59 18.06… <NA>  Union   
## 10    19 Amthor     Philipp       <NA>    männlich      24 10.11… <NA>  Union   
## # … with 820 more rows

Möchten wir mehrere Ausdrücke kombinieren, benötigen wir dazu logische Operatoren. &UND – testet, ob beide verbundenen Ausdrücke gleichzeitig TRUE sind und gibt nur dann TRUE zurück. |ODER – testet, ob wenigstens einer der verbundenen Ausdrücke TRUE ist und gibt in diesem Fall TRUE zurück. Dies sind alle möglichen Kombinationen für 2 Ausdrücke:

  • TRUE & TRUE gibt TRUE zurück
  • TRUE & FALSE, FALSE & TRUE und FALSE & FALSE geben alle FALSE zurück
  • TRUE | TRUE, TRUE | FALSE und FALSE | TRUE geben alle TRUE zurück
  • FALSE | FALSE gibt FALSE zurück

Möchten wir nun beispielsweise alle Unions-Abgeordneten des 19. Bundestags filtern, können wir dazu den & Operator einsetzen:

stammdaten_17_19 %>% 
  filter(fraktion == "Union" & wp == 19)
## # A tibble: 261 × 9
##       wp nachname    vorname       praefix geschlecht alter geb   Titel fraktion
##    <int> <chr>       <chr>         <chr>   <chr>      <int> <chr> <chr> <chr>   
##  1    19 Abercron    Michael       von     männlich      64 17.1… Dr.   Union   
##  2    19 Albani      Stephan       <NA>    männlich      49 03.0… <NA>  Union   
##  3    19 Altenkamp   Norbert Maria <NA>    männlich      45 27.0… <NA>  Union   
##  4    19 Altmaier    Peter         <NA>    männlich      59 18.0… <NA>  Union   
##  5    19 Amthor      Philipp       <NA>    männlich      24 10.1… <NA>  Union   
##  6    19 Auernhammer Artur         <NA>    männlich      54 09.0… <NA>  Union   
##  7    19 Aumer       Peter         <NA>    männlich      41 17.0… <NA>  Union   
##  8    19 Bär         Dorothee      <NA>    weiblich      39 19.0… <NA>  Union   
##  9    19 Bareiß      Thomas        <NA>    männlich      42 15.0… <NA>  Union   
## 10    19 Barthle     Norbert       <NA>    männlich      65 01.0… <NA>  Union   
## # … with 251 more rows

Möchten wir die MdBs der großen Koalition aus dem Datensatz filtern, also Abgeordnete der Union oder der SPD, können wir dies mit | realisieren:

stammdaten_17_19 %>% 
  filter(fraktion == "Union" | fraktion == "SPD")
## # A tibble: 1,353 × 9
##       wp nachname   vorname       praefix geschlecht alter geb    Titel fraktion
##    <int> <chr>      <chr>         <chr>   <chr>      <int> <chr>  <chr> <chr>   
##  1    19 Abercron   Michael       von     männlich      64 17.11… Dr.   Union   
##  2    17 Aigner     Ilse          <NA>    weiblich      44 07.12… <NA>  Union   
##  3    18 Albani     Stephan       <NA>    männlich      45 03.06… <NA>  Union   
##  4    19 Albani     Stephan       <NA>    männlich      49 03.06… <NA>  Union   
##  5    18 Albsteiger Katrin        <NA>    weiblich      29 20.11… <NA>  Union   
##  6    19 Altenkamp  Norbert Maria <NA>    männlich      45 27.07… <NA>  Union   
##  7    17 Altmaier   Peter         <NA>    männlich      51 18.06… <NA>  Union   
##  8    18 Altmaier   Peter         <NA>    männlich      55 18.06… <NA>  Union   
##  9    19 Altmaier   Peter         <NA>    männlich      59 18.06… <NA>  Union   
## 10    19 Amthor     Philipp       <NA>    männlich      24 10.11… <NA>  Union   
## # … with 1,343 more rows

Es lassen sich auch mehr als zwei Ausdrücke mit & bzw. | kombinieren. Beispielsweise um die die Abgeordneten der Oppositionsfraktionen zu filtern:

stammdaten_17_19 %>% 
  filter(fraktion == "AfD" | fraktion == "FDP" | fraktion == "Die Grünen" | fraktion == "DIE LINKE.")
## # A tibble: 705 × 9
##       wp nachname  vorname   praefix geschlecht alter geb        Titel fraktion 
##    <int> <chr>     <chr>     <chr>   <chr>      <int> <chr>      <chr> <chr>    
##  1    19 Achelwilm Doris     <NA>    weiblich      40 30.11.1976 <NA>  DIE LINK…
##  2    17 Ackermann Jens      <NA>    männlich      34 02.07.1975 <NA>  FDP      
##  3    19 Aggelidis Grigorios <NA>    männlich      52 19.08.1965 <NA>  FDP      
##  4    17 Ahrendt   Christian <NA>    männlich      46 07.05.1963 <NA>  FDP      
##  5    19 Akbulut   Gökay     <NA>    weiblich      34 16.11.1982 <NA>  DIE LINK…
##  6    17 Aken      Jan       van     männlich      48 01.05.1961 <NA>  DIE LINK…
##  7    18 Aken      Jan       van     männlich      52 01.05.1961 <NA>  DIE LINK…
##  8    17 Alpers    Agnes     <NA>    weiblich      48 29.06.1961 <NA>  DIE LINK…
##  9    18 Alpers    Agnes     <NA>    weiblich      52 29.06.1961 <NA>  DIE LINK…
## 10    19 Alt       Renata    <NA>    weiblich      52 27.08.1965 <NA>  FDP      
## # … with 695 more rows

Die Daten enthalten aktuell noch alle Abgeordneten der 17. - 19. Legislaturperioden. Im rahmen dieser Einführung, arbeiten wir nur mit den MdBs des 19. Bundestags. Mit filter() können wir den Datensatz auf diese reduzieren.

stammdaten_19 <- stammdaten_17_19 %>% 
  filter(wp == 19)

Ab hier arbeiten wir mit dem Objekt stammdaten_19 weiter.

3.2.2 select()

Möchten wir ein Subset von Variablen bilden, könne wir dazu select() nutzen. Die Funktion nimmt die Daten, auf die sie angewendet werden soll, als erstes Argument – hier weitergegeben durch die Pipe – und dann einen oder mehrere Spaltennamen, die in der Tibble verbleiben sollen. Folgen die zu behaltenden Spalten aufeinander, können wir auch eine “von:bis” Notation nutzen. Nehmen wir an, wir möchten die Spalten “nachname”, “vorname”, “praefix” und “fraktion” extrahieren:

stammdaten_19 %>% 
  select(nachname, vorname, praefix, fraktion)
## # A tibble: 750 × 4
##    nachname  vorname       praefix fraktion  
##    <chr>     <chr>         <chr>   <chr>     
##  1 Abercron  Michael       von     Union     
##  2 Achelwilm Doris         <NA>    DIE LINKE.
##  3 Aggelidis Grigorios     <NA>    FDP       
##  4 Akbulut   Gökay         <NA>    DIE LINKE.
##  5 Albani    Stephan       <NA>    Union     
##  6 Alt       Renata        <NA>    FDP       
##  7 Altenkamp Norbert Maria <NA>    Union     
##  8 Altmaier  Peter         <NA>    Union     
##  9 Amthor    Philipp       <NA>    Union     
## 10 Amtsberg  Luise         <NA>    Die Grünen
## # … with 740 more rows

stammdaten_19 %>% 
  select(nachname:praefix, fraktion)
## # A tibble: 750 × 4
##    nachname  vorname       praefix fraktion  
##    <chr>     <chr>         <chr>   <chr>     
##  1 Abercron  Michael       von     Union     
##  2 Achelwilm Doris         <NA>    DIE LINKE.
##  3 Aggelidis Grigorios     <NA>    FDP       
##  4 Akbulut   Gökay         <NA>    DIE LINKE.
##  5 Albani    Stephan       <NA>    Union     
##  6 Alt       Renata        <NA>    FDP       
##  7 Altenkamp Norbert Maria <NA>    Union     
##  8 Altmaier  Peter         <NA>    Union     
##  9 Amthor    Philipp       <NA>    Union     
## 10 Amtsberg  Luise         <NA>    Die Grünen
## # … with 740 more rows

Wir können select()auch nutzen, um Variablen zu entfernen. Dazu schreiben wir - vor den zu entfernenden Spaltennamen. Da das genaue Geburtsdatum der Abgeordneten im Folgenden nicht mehr benötigt wird, können wir die Spalte “geb” entfernen.

stammdaten_19 <- stammdaten_19 %>% 
  select(-geb)

stammdaten_19
## # A tibble: 750 × 8
##       wp nachname  vorname       praefix geschlecht alter Titel fraktion  
##    <int> <chr>     <chr>         <chr>   <chr>      <int> <chr> <chr>     
##  1    19 Abercron  Michael       von     männlich      64 Dr.   Union     
##  2    19 Achelwilm Doris         <NA>    weiblich      40 <NA>  DIE LINKE.
##  3    19 Aggelidis Grigorios     <NA>    männlich      52 <NA>  FDP       
##  4    19 Akbulut   Gökay         <NA>    weiblich      34 <NA>  DIE LINKE.
##  5    19 Albani    Stephan       <NA>    männlich      49 <NA>  Union     
##  6    19 Alt       Renata        <NA>    weiblich      52 <NA>  FDP       
##  7    19 Altenkamp Norbert Maria <NA>    männlich      45 <NA>  Union     
##  8    19 Altmaier  Peter         <NA>    männlich      59 <NA>  Union     
##  9    19 Amthor    Philipp       <NA>    männlich      24 <NA>  Union     
## 10    19 Amtsberg  Luise         <NA>    weiblich      33 <NA>  Die Grünen
## # … with 740 more rows

3.2.3 rename()

Ihnen wird eventuell bereits aufgefallen sein, dass alle Spaltennamen außer “Titel” mit einem kleingeschriebenen Buchstaben beginnen. Da habe ich anscheinend beim Erstellen des Datenobjekts meine eigenen Namenskonventionen – alles beginnt mit kleinen Buchstaben – außer Acht gelassen. Um diese nun umzubenennen, können wir rename() nutzen. Erneut werden als erstes Argument die Daten durch die Pipe weitergegeben, gefolgt von einem oder mehreren Argumenten in der Form neuer_name = alter_name.

stammdaten_19 <- stammdaten_19 %>% 
  rename(titel = Titel)

stammdaten_19
## # A tibble: 750 × 8
##       wp nachname  vorname       praefix geschlecht alter titel fraktion  
##    <int> <chr>     <chr>         <chr>   <chr>      <int> <chr> <chr>     
##  1    19 Abercron  Michael       von     männlich      64 Dr.   Union     
##  2    19 Achelwilm Doris         <NA>    weiblich      40 <NA>  DIE LINKE.
##  3    19 Aggelidis Grigorios     <NA>    männlich      52 <NA>  FDP       
##  4    19 Akbulut   Gökay         <NA>    weiblich      34 <NA>  DIE LINKE.
##  5    19 Albani    Stephan       <NA>    männlich      49 <NA>  Union     
##  6    19 Alt       Renata        <NA>    weiblich      52 <NA>  FDP       
##  7    19 Altenkamp Norbert Maria <NA>    männlich      45 <NA>  Union     
##  8    19 Altmaier  Peter         <NA>    männlich      59 <NA>  Union     
##  9    19 Amthor    Philipp       <NA>    männlich      24 <NA>  Union     
## 10    19 Amtsberg  Luise         <NA>    weiblich      33 <NA>  Die Grünen
## # … with 740 more rows

3.2.4 mutate()

mutate() dient dazu, Daten in bestehenden Spalten zu transformieren und neue Spalten aus den Daten bestehender Spalten zu generieren. Dabei schreiben wir innerhalb der Klammern den Namen der zu transformierenden oder neu zu erstellenden Spalte = einem Ausdruck der beschreibt, wir die Daten transformiert oder generiert werden sollen. Dies kann eine einfache Berechnung wie mutate(neue_spalte = spalte_a + spalte b) sein, aber auch weitere Funktionen beinhalten. Auch in mutate() können wir innerhalb der Klammern mehrere mutate-Schritte der Form alte/neue_Spalte = Ausdruck durch Kommata “aneinanderhängen”.

Hier erstellen wir eine kleine Tibble mit Beispieldaten um die Funktionsweise von mutate() zu veranschaulichen:

exmpl_tbl <- tibble(x1 = 1:5,
                    x2 = 5:1)

exmpl_tbl
## # A tibble: 5 × 2
##      x1    x2
##   <int> <int>
## 1     1     5
## 2     2     4
## 3     3     3
## 4     4     2
## 5     5     1

exmpl_tbl %>% 
  mutate(x1 = sqrt(x1),
         x3 = x1 * x2,
         x4 = round(x3, digits = 2))
## # A tibble: 5 × 4
##      x1    x2    x3    x4
##   <dbl> <int> <dbl> <dbl>
## 1  1        5  5     5   
## 2  1.41     4  5.66  5.66
## 3  1.73     3  5.20  5.2 
## 4  2        2  4     4   
## 5  2.24     1  2.24  2.24

In diesem Beispiel wird der Inhalt der Spalte “x1” durch die Quadratwurzel der enthaltenen Werte ersetzt – sqrt() ist eine base R Funktion zum Ziehen der Quadratwurzel. Die Originalwerte werden also mit den transformierten Werten ersetzt. Die Werte in “x2” werden hier nicht ersetzt. Die Spalten “x3” und “x4” werden beide neu aus den Werten der bestehenden Spalten erstellt. Bitte beachten Sie dabei, dass bei der Erstellung von “x3” bereits die transformierten Werte aus “x1” in die Berechnung eingehen, nicht die Originalwerte. Die Funktion round(), welche bei der Erstellung von “x4” genutzt wird, rundet die Werte aus “x3” auf zwei Nachkommastellen. Die Anzahl Nachkommastellen wird über das optionale Argument digits = kontrolliert. Geben wir dies nicht an, wird auf Null Nachkommastellen, also Ganzzahlen, gerundet.

Gehen wir nun zur Transformation unseres Datensatzes über. Die Spalte “titel” listet aktuell die akademischen Titel der Abgeordneten. Liegt ein Titel vor, wird dieser genau benannt. Liegt keiner vor enthält die Spalte ein NA, die Repräsentation fehlender Werte in R. Für diese Analyse interessiert uns nur, ob ein Titel vorliegt, aber nicht welcher. Dazu könnten wir eine neue Spalte generieren, welche einen logischen Vektor enthält, der angibt, ob ein Titel vorliegt (TRUE) oder nicht (FALSE). Dies ist eine sogenannte Dummyvariable. Zum Generieren können wir die dplyr Funktion if_else() nutzen. Wir geben als erstes Argument eine Bedingung an, die getestet wird. Hier !is.na(titel), welche testet ob der Inhalt von “titel” nicht – angezeigt durch das ! – missing ist. Das zweite und dritte Argument geben an, was eingesetzt werden soll wenn die getestete Bedingung erfüllt beziehungsweise nicht erfüllt ist. Hier soll im ersten Fall TRUE, im zweiten FALSE eingesetzt werden.

stammdaten_19 <- stammdaten_19 %>% 
  mutate(titel_dummy = if_else(!is.na(titel), TRUE, FALSE))

stammdaten_19 %>% 
  select(titel, titel_dummy)
## # A tibble: 750 × 2
##    titel titel_dummy
##    <chr> <lgl>      
##  1 Dr.   TRUE       
##  2 <NA>  FALSE      
##  3 <NA>  FALSE      
##  4 <NA>  FALSE      
##  5 <NA>  FALSE      
##  6 <NA>  FALSE      
##  7 <NA>  FALSE      
##  8 <NA>  FALSE      
##  9 <NA>  FALSE      
## 10 <NA>  FALSE      
## # … with 740 more rows

Die Spalte “fraktion” ist aktuell als Character Vektor abgelegt. Bei der Fraktionszugehörigkeit handelt es sich aber um eine kategoriale Variable. So gibt es nur eine begrenzte Anzahl möglicher Ausprägungen, nämlich die Namen der im 19. Bundestag vertretenen Fraktionen sowie die Kategorie “Flos” für Fraktionslose. Die Repräsentation kategorialer Variablen in R nennt sich factor. Das core tidyverse Package forcats ermöglicht es uns, Faktorvariablen anzulegen und zu manipulieren.

Der einfachste Weg ist es, R selbst auslesen zu lassen, welche Kategorien in einer Variable vorhanden sind. Dazu wenden wir einfach die Funktion factor() innerhalb von mutate() auf eine Spalte an.

stammdaten_19 <- stammdaten_19 %>% 
  mutate(fraktion = factor(fraktion))

Um herauszufinden, welche Kategorien ein factor vector umfasst, nutzen wir levels().

levels(stammdaten_19$fraktion)
## [1] "AfD"        "Die Grünen" "DIE LINKE." "FDP"        "Flos"      
## [6] "SPD"        "Union"

Dies scheint funktioniert zu haben. R hat automatisch und korrekt alle Ausprägungen der Varibale erkannt und jede einem factor zugeordnet. Aktuell sind die Kategorien alphabetisch geordnet, inhaltlicher sinnvoller wäre aber beispielsweise eine Ordnung nach der Größe der Fraktionen im 19. Bundestag.

Um die Kategorien manuell neu zu ordnen, könnten wir fct_relevel() nutzen. Dazu müssten wir aber die gewünschte Reihenfolge per Hand notieren. Bequemer geht es mit fct_infreq(), welche die Kategorien automatisch nach der Häufigkeit Ihres Auftretens in den Daten ordnet. Also in unserem Fall von der Kategorie mit den meisten Abgeordneten bis zu der mit den wenigsten.

stammdaten_19 <- stammdaten_19 %>% 
  mutate(fraktion = fct_infreq(fraktion))

levels(stammdaten_19$fraktion)
## [1] "Union"      "SPD"        "AfD"        "FDP"        "Die Grünen"
## [6] "DIE LINKE." "Flos"

Dass die Grünen vor der Linken eingeordnet wurden obwohl die Linken im 19. Bundestag zwei Mandate mehr hatten, liegt daran, dass der Datensatz auch ausgeschiedene MdBs umfasst. Von diesen hatten die Grünen drei mehr, so dass sie sich in der Gesamtzahl von Beobachtungen im Datensatz knapp vor die Linke setzen.

Die Spalte “geschlecht” liegt ebenfalls als Character Vektor vor und kann auf dem selben Weg in einen Factor umgewandelt werden.

stammdaten_19 <- stammdaten_19 %>% 
  mutate(geschlecht = factor(geschlecht))

levels(stammdaten_19$geschlecht)
## [1] "männlich" "weiblich"

Damit sind die Daten zur weiteren Analyse vorbereitet. Die Spalte “titel” ist allerdings nicht mehr notwendig, da wir die enthaltenen Informationen in die neue Dummyvariable überführt haben. Wir können diese also mit select() entfernen.

stammdaten_19 <- stammdaten_19 %>% 
  select(-titel)

stammdaten_19
## # A tibble: 750 × 8
##       wp nachname  vorname       praefix geschlecht alter fraktion   titel_dummy
##    <int> <chr>     <chr>         <chr>   <fct>      <int> <fct>      <lgl>      
##  1    19 Abercron  Michael       von     männlich      64 Union      TRUE       
##  2    19 Achelwilm Doris         <NA>    weiblich      40 DIE LINKE. FALSE      
##  3    19 Aggelidis Grigorios     <NA>    männlich      52 FDP        FALSE      
##  4    19 Akbulut   Gökay         <NA>    weiblich      34 DIE LINKE. FALSE      
##  5    19 Albani    Stephan       <NA>    männlich      49 Union      FALSE      
##  6    19 Alt       Renata        <NA>    weiblich      52 FDP        FALSE      
##  7    19 Altenkamp Norbert Maria <NA>    männlich      45 Union      FALSE      
##  8    19 Altmaier  Peter         <NA>    männlich      59 Union      FALSE      
##  9    19 Amthor    Philipp       <NA>    männlich      24 Union      FALSE      
## 10    19 Amtsberg  Luise         <NA>    weiblich      33 Die Grünen FALSE      
## # … with 740 more rows

3.3 Speichern der transformierten Daten

Wir werden im nächsten Kapitel mit den hier transformierten Daten weiterarbeiten. Dazu sollten Sie entweder zu Beginn des Kapitels das Script aus diesem Kapitel nochmals durchlaufen lassen, oder Sie speichern die bearbeiteten Daten:

save(stammdaten_19, file = "stammdaten_19.RData")

3.4 Deskriptive Datenanalyse mit dplyr

3.4.1 summarise()

Die Funktion summarise() ermöglicht es uns, zusammenfassende Maßzahlen zu einer Tibble zu berechnen. Die Syntax funktioniert dabei ähnlich zu mutate(). Wir definieren einen Namen für die Spalte, in der die Maßzahlen gelistet werden sollen, und nach dem = eine entsprechende Berechnung oder Funktion. Das Ergebnis ist ein neue Tibble, welche die angeforderten Statistiken enthält.

Betrachten wir zunächst die Spalte “alter”. Wir könnten uns für das arithmetische Mittel des Alters der MdBs interessieren. Dazu definieren wir innerhalb von summarise() den Namen der neuen Spalte und nach dem = nutzen wir die base R Funktion mean():

stammdaten_19 %>% 
  summarise(arith_mittel = mean(alter))
## # A tibble: 1 × 1
##   arith_mittel
##          <dbl>
## 1         49.4

Wir können uns auch mehrere Statistiken gleichzeitig ausgeben, indem wir die entsprechenden Anfragen mit , verbinden:

stammdaten_19 %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            median = median(alter),
            maximum = max(alter))
## # A tibble: 1 × 4
##   minimum arith_mittel median maximum
##     <int>        <dbl>  <dbl>   <int>
## 1      24         49.4     50      77

Das sieht alles schon ordentlich aus, ist aber auch auf einfacherem Wege erzielbar, beispielsweise mit der praktischen base R Funktion summary(). Wirklich mächtig wird summarise() erst durch die Kombination mit group_by(). So können wir die Daten gruppieren und die zusammenfassenden Statistiken für jede dieser Gruppen separat berechnen.

3.4.2 summarise() & group_by()

Unsere Daten bieten sich für eine Analyse nach Fraktionszugehörigkeit an. Als ersten Schritt können wir die Daten also nach dem Inhalt der Spalte “fraktion” gruppieren und zunächst einmal zählen, wie viele Abgeordnete pro Fraktion im Bundestag vertreten sind. Hier bitte erneut beachten, dass sowohl ausgeschiedene Abgeordnete sowie deren NachrückerInnen im Datensatz enthalten sind. Die Analyseergebnisse werden dementsprechend nicht exakt mit der Mandatszahl übereinstimmen. Innerhalb von summarise() nutzen wir hier die dplyr Funktion n(), welche die Anzahl Beobachtungen pro Gruppe, also die Gruppengröße, zurückgibt.

stammdaten_19 %>% 
  group_by(fraktion) %>% 
  summarise(abgeordnete = n())
## # A tibble: 7 × 2
##   fraktion   abgeordnete
##   <fct>            <int>
## 1 Union              261
## 2 SPD                165
## 3 AfD                 94
## 4 FDP                 85
## 5 Die Grünen          72
## 6 DIE LINKE.          71
## 7 Flos                 2

Eben haben wir das Durchschnittsalter aller Abgeordneten berechnet. Dies können wir natürlich auch gruppiert nach Fraktionen machen.

stammdaten_19 %>% 
  group_by(fraktion) %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            std_abweichung = sd(alter),
            median = median(alter),
            maximum = max(alter),
            anzahl = n())
## # A tibble: 7 × 7
##   fraktion   minimum arith_mittel std_abweichung median maximum anzahl
##   <fct>        <int>        <dbl>          <dbl>  <dbl>   <int>  <int>
## 1 Union           24         50.0           9.72   51        75    261
## 2 SPD             27         50.3           9.35   52        68    165
## 3 AfD             26         50.9          12.2    50        77     94
## 4 FDP             24         45.6          10.7    46        76     85
## 5 Die Grünen      26         46.9           9.90   47.5      64     72
## 6 DIE LINKE.      27         50.4           9.72   52        69     71
## 7 Flos            42         42             0      42        42      2

Oft ist es hilfreich, die Daten nach einer Statistik, die uns interessiert, zu ordnen. Möchten wir beispielsweise wissen, welche Fraktion den höchsten beziehungsweise niedrigsten Mittelwert des Alters hat, können wir die Ergebnisse in einem weiteren Schritt mit der Funktion arrange() nach den Werten dieser Variable ordnen. Die zusätzliche Funktion desc() bewirkt, dass die Daten absteigend geordnet werden.

stammdaten_19 %>% 
  group_by(fraktion) %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            std_abweichung = sd(alter),
            median = median(alter),
            maximum = max(alter),
            anzahl = n()) %>% 
  arrange(desc(arith_mittel))
## # A tibble: 7 × 7
##   fraktion   minimum arith_mittel std_abweichung median maximum anzahl
##   <fct>        <int>        <dbl>          <dbl>  <dbl>   <int>  <int>
## 1 AfD             26         50.9          12.2    50        77     94
## 2 DIE LINKE.      27         50.4           9.72   52        69     71
## 3 SPD             27         50.3           9.35   52        68    165
## 4 Union           24         50.0           9.72   51        75    261
## 5 Die Grünen      26         46.9           9.90   47.5      64     72
## 6 FDP             24         45.6          10.7    46        76     85
## 7 Flos            42         42             0      42        42      2

Für die Mehrzahl der Fraktionen liegt der Mittelwert des Alters bei ca. 50 Jahren. Dabei beträgt die Differenz des arithmetischen Mitels zwischen AfD und Union etwa 1 Jahr. Wir sehen für die AfD aber auch eine höhere Standardabweichung und einen niedrigeren Median. Wir könnten also vermuten, dass in der AfD einige besonders alte Abgeordnete vertreten sind, welche den Mittelwert nach oben verzerren. Die FDP ist dem Mittelwert nach die jüngste, die Grünen die zweitjüngste Fraktion. Während sich das Alter der jüngsten Fraktionsmitglieder relativ wenig unterscheidet, bestehen beim Alter der ältesten Mitglieder deutliche Unterschiede. Während es bei den Grünen keine MdBs über 64 gibt, geht das Maximalalter bei AfD, Union und FDP bis in die mittleren 70er. Die Statistiken für die Gruppe der fraktionslosen sind nicht besonders belastbar, da sie nur aus zwei Personen besteht, und lassen sich nicht sinnvoll analysieren. Diesen ersten Befunden werden wir bei der graphischen Analyse im folgenden Kapitel weiter auf den Grund gehen.

Die Spalte “titel_dummy” ist ein logischer Vektor der TRUE enthält, wenn ein MdB einen akademischen Titel besitzt und FALSE, wenn keiner vorliegt. Hinter einem logischen Vektor stehen Zahlenwerte. TRUE ist die Repräsentation von 1, FALSE von 0. Wir können also direkt mit den Werten des Vektors rechnen. Um die absolute Anzahl an Titeln zu berechenn, reicht es so die Summe des Vektors zu bilden. Alle Werte werden aufaddiert und da wir nur Einsen und Nullen haben, ist das Ergebnis gleich der Anzahl der Abgeordneten, die einen akademischen Titel tragen. Um den relativen Anteil zu berechnen, können wir das arithmetische Mittel nutzen. Alle Einsen und Nullen werden aufaddiert und durch die Anzahl der Beobachtungen geteilt. Dies ist der Anteil der Abgeordneten mit Titel.

stammdaten_19 %>% 
  summarise(absolut = sum(titel_dummy),
            relativ = mean(titel_dummy)
  )
## # A tibble: 1 × 2
##   absolut relativ
##     <int>   <dbl>
## 1     147   0.196

Wir könnten uns dafür interessieren, ob sich das Alter der Abgeordneten mit Titel von denen unterscheidet die keinen Titel tragen. Dazu gruppieren wir nach der Spalte “titel_dummy” und berechnen die Statistiken zum Alter.

stammdaten_19 %>% 
  group_by(titel_dummy) %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            std_abweichung = sd(alter),
            median = median(alter),
            maximum = max(alter),
            anzahl = n())
## # A tibble: 2 × 7
##   titel_dummy minimum arith_mittel std_abweichung median maximum anzahl
##   <lgl>         <int>        <dbl>          <dbl>  <int>   <int>  <int>
## 1 FALSE            24         49.1           10.1     50      77    603
## 2 TRUE             28         50.8           10.6     50      76    147

Abgeordnete mit Titel sind im Durchschnitt etwas älter. Dies macht Sinn, da sowohl das Erlangen eines Titels als auch eines Bundestagmandats hohe Zeitinvestitionen darstellen.

Wir können auch nach mehreren Spalten gleichzeitig gruppieren. In diesem Fall werden die Statistiken für jede Gruppenkombination berechnet.

stammdaten_19 %>% 
  group_by(fraktion, titel_dummy) %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            std_abweichung = sd(alter),
            median = median(alter),
            maximum = max(alter),
            anzahl = n())
## `summarise()` has grouped output by 'fraktion'. You can override using the `.groups` argument.
## # A tibble: 14 × 8
## # Groups:   fraktion [7]
##    fraktion   titel_dummy minimum arith_mittel std_abweichung median maximum
##    <fct>      <lgl>         <int>        <dbl>          <dbl>  <dbl>   <int>
##  1 Union      FALSE            24         49.7           9.83   51        70
##  2 Union      TRUE             32         50.9           9.32   50        75
##  3 SPD        FALSE            27         50.2           9.45   52        68
##  4 SPD        TRUE             33         50.9           8.95   49.5      67
##  5 AfD        FALSE            26         49.8          11.7    49        77
##  6 AfD        TRUE             28         54.8          13.5    55.5      76
##  7 FDP        FALSE            24         44.9           9.77   46        66
##  8 FDP        TRUE             31         48.4          13.5    44.5      76
##  9 Die Grünen FALSE            26         47.8          10.1    49        64
## 10 Die Grünen TRUE             34         43.8           8.79   42.5      64
## 11 DIE LINKE. FALSE            27         49.0           9.74   50        66
## 12 DIE LINKE. TRUE             48         56.9           6.67   56        69
## 13 Flos       FALSE            42         42            NA      42        42
## 14 Flos       TRUE             42         42            NA      42        42
## # … with 1 more variable: anzahl <int>

Wir sehen an den Mittelwerten, dass der oben für den gesamten Bundestag analysierte Unterschied im mittleren Alter von MdBs mit und ohne Titel nicht für alle Fraktionen gleichermaßen gilt. Während Titelträger in AfD, FDP und LINKE im Mittel klar älter sind als Abgeordnete ohne Titel, ist die Differenz für die Union und die SPD relativ gering. Bei den Grünen sind Titelträger im Mittel sogar deutlich jünger. Auch dies werden wir in der graphischen Analyse erneut betrachten.

An dieser Stelle müssen wir noch über eine Besonderheit in der Zusammenarbeit von group_by() und summarise() sprechen, welche zu Problemen führen kann, wenn Sie sich ihr nicht bewusst sind. Grundsätzlich wird jeder Aufruf von summarise() nach einem group_by() Befehl eine Ebene der Gruppierung auflösen. Für die Beispiele in denen wir nur nach Fraktion oder Titel gruppiert haben, bedeutet dies also, dass die Daten nach dem summarise() Befehl nicht mehr gruppiert waren. Wir hatten eine Gruppierungsebene und diese wurde automatisch entfernt. Im letzten Beispiel hatten wir zwei Gruppierungsebenen. Von diesen wurde nur die niedrigere Gruppierungsebene – die Reihenfolge der Variablen in group_by() bestimmt auch die Reihenfolge der Gruppierung –, also die Gruppierung nach dem Titel aufgelöst. Die Gruppierung nach der Fraktionsmitgliedschaft ist weiterhin aktiv. Dies ist auch im Output anhand der Zeile # Groups: fraktion [7] erkennbar. Die Daten sind nach der Fraktion gruppiert und wir haben 7 Gruppen. Möchten wir mit den Ergebnissen dieser Berechnungen weiterarbeiten, müssen wir uns auch über die Gruppierung bewusst sein. Sollen die Daten weiterhin gruppiert sein? Dann ist alles gut, sollen sie es nicht sein müssen wir die Gruppierung vollständig auflösen. Dies machen wir mit der Funktion ungroup(). Solange Sie sich nicht sicher sind, dass Sie die Gruppierung wirklich weiterhin brauchen, würde ich immer zu einem ungroup() am Ende einer group_by() und summarise() Operation mit mehreren Gruppen tendieren. So verhindern sie versehentliche Berechnungen auf Gruppenbasis und die Gruppierung lässt sich jederzeit schnell wieder herstellen, sollte dies notwendig werden.

stammdaten_19 %>% 
  group_by(fraktion, titel_dummy) %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            std_abweichung = sd(alter),
            median = median(alter),
            maximum = max(alter),
            anzahl = n()) %>% 
  ungroup()
## `summarise()` has grouped output by 'fraktion'. You can override using the `.groups` argument.
## # A tibble: 14 × 8
##    fraktion   titel_dummy minimum arith_mittel std_abweichung median maximum
##    <fct>      <lgl>         <int>        <dbl>          <dbl>  <dbl>   <int>
##  1 Union      FALSE            24         49.7           9.83   51        70
##  2 Union      TRUE             32         50.9           9.32   50        75
##  3 SPD        FALSE            27         50.2           9.45   52        68
##  4 SPD        TRUE             33         50.9           8.95   49.5      67
##  5 AfD        FALSE            26         49.8          11.7    49        77
##  6 AfD        TRUE             28         54.8          13.5    55.5      76
##  7 FDP        FALSE            24         44.9           9.77   46        66
##  8 FDP        TRUE             31         48.4          13.5    44.5      76
##  9 Die Grünen FALSE            26         47.8          10.1    49        64
## 10 Die Grünen TRUE             34         43.8           8.79   42.5      64
## 11 DIE LINKE. FALSE            27         49.0           9.74   50        66
## 12 DIE LINKE. TRUE             48         56.9           6.67   56        69
## 13 Flos       FALSE            42         42            NA      42        42
## 14 Flos       TRUE             42         42            NA      42        42
## # … with 1 more variable: anzahl <int>

3.5 Export von Tabellen

Ein flexibler Weg zum direkten Export von Tabellen ist es, diese als “.csv” abzuspeichern. Dieses Format kann wiederum direkt in Excel importiert werden.

CSV steht für “comma-separated values”. Da diese Dateien sehr einfach gehalten sind und von einer großen Bandbreite an Software verarbeitet werden können, ist CSV eines der meistgenutzten Formate für zweidimensionale Datenstrukturen.

Hier fehlt der Platz um ins Detail zu gehen, aber wir können kurz die Grundstruktur einer CSV Datei betrachten:

column1, column2, column3
data1_1, data1_2, data1_3
data2_1, data2_2, data2_3
data3_1, data3_2, data3_3

Die Zeilen einer Tabelle sind durch Zeilenumbrüche getrennt, die Spalten durch Kommata. In der ersten Zeile stehen dabei häufig, aber nicht immer, die Spaltennamen. Damit wissen Sie bereits das Wichtigste. Mehr zu “.csv” Dateien finden sie unter: https://jakobtures.github.io/web-scraping/files.html

Wir können beispielsweise das Ergebnis einer summarise() Operation mit write_csv() aus readr (core tidyverse) abspeichern.

stammdaten_19 %>% 
  group_by(fraktion, titel_dummy) %>% 
  summarise(minimum = min(alter),
            arith_mittel = mean(alter),
            std_abweichung = sd(alter),
            median = median(alter),
            maximum = max(alter),
            anzahl = n()) %>% 
  ungroup() %>% 
  write_csv(file = "alter_fraktion_titel_1.csv")
## `summarise()` has grouped output by 'fraktion'. You can override using the `.groups` argument.

Aus Gründen, die vermutlich nur Microsoft selbst bekannt sind – ich tippe auf missverstandene “Nutzerfreundlichkeit” –, können “.csv” Dateien, in denen die Spalten durch Kommata getrennt sind – der internationale Standard –, auf deutschen Windows Systemen oft nicht ohne weiteres importiert werden. Nutzen Sie statt Microsoft Office die Open Source Alternative LibreOffice, ist das Problem bereits behoben. Möchten Sie aber Microsoft Office auf einem deutschen System nutzen, sollten Sie die Datei eventuel im Deutschen Standard speichern. Dabei werden statt Kommata Semikolons als Trennzeichen der Spalten verwendet, da die Kommata in Deutschland zur Darstellung von Dezimalzahlen benötigt werden. Dies ist mit write_csv2() auf einfachem Wege möglich.

3.5.0.1 Weiterführende Resourcen

Es existiert eine Vielzahl von Packages, die Funktionen zur Formatierung und dem Export von Tabellen mitbringen. Aus Platzgründen können wir nicht auf die Details eingehen, hier aber einige Startpunkte:

Möchten sie direkt nach “.xlsx” exportieren, wären zwei der Optionen:

Tabellen direkt im “.docx” Format zu erstellen ermöglicht flextable: https://davidgohel.github.io/flextable/

huxtable kann Tabellen im “.tex” Format zur Arbeit in LaTeX ausgeben, beherrscht aber auch HTML sowie Word, Excel und Powerpoint Output: https://hughjonesd.github.io/huxtable/

Möchten sie externe Software zum Schreiben von Hausarbeiten und Papers ganz umgehen, könnte R Markdown für Sie von Interesse sein. Dies ermöglicht das gleichzeitige Schreiben von Text und Code in einem Dokument. Die Ausgabe lässt sich in unterschiedliche Formate exportieren und kann neben dem Text auch Code, der dargestellt werden soll, sowie Tabellen und Grafiken enthalten. Einige Resourcen zu R Markdown finden Sie hier:

Die Website, die Sie aktuell betrachten, sowie viele der verlinkten Resourcen sind mit bookdown geschrieben, welches auf R Markdown aufbaut und sich vor allem für umfangreichere Projekte eignet.