Plenarprotokolle 19.Legislaturperiode, RegExp

15.05.2019 - Valentin Gold - Reading time ~154 Minutes


Plenarprotokolle 19.Legislaturperiode

Die Protokolle der 19.Legislaturperiode sind unter https://www.bundestag.de/services/opendata verfügbar. Im Gegesatz zu den bisherigen bekannten Dateiformaten, liegen die Daten im .xml-Format vor. Das ist durchaus vorteilhaft, da zahlreiche Auszeichnungen vorhanden sind und diese “nur” korrekt eingelesen werden müssen. Hierfür gibt es eine Reihe von nützlichen Paketen, z.B. xml, xml2 und xmltools. Meinen zweiten Versuch, die Daten korrekt(er) mittels den dafür bereit gestellten xml-Paketen zu extrahieren finden Sie im Eintrag Plenarprotokolle 19.Legislaturperiode, XML.

Anhand der ersten 10 Sitzungen demonstriere ich hier noch einmal, wie man mit regulären Ausdrücken ebenso die relevaten Variablen extrahieren kann. Im Vergleich zu den xml-Paketen dauert das vergleichsweise lange. In der Zeit könnte man nicht nur einen Kaffee trinken\(\ldots\) Zudem muss der Datensatz aufwendig bereinigt werden. Der Vorteil ist jedoch: Fast alle Befehle sollten Ihnen bekannt vorkommen!

Das Problem an den zur Verfügung gestellten Daten ist, dass sie meines Erachtens nach nicht alle exakt gleich formatiert sind. Zum Beispiel ist in den meisten Fällen eine Rede-Id vorhanden, nicht aber für die Eröffnungsreden der jeweiligen Sitzungen (Ausnahme: 1.Sitzung). Hier müssen die Rednernamen aus dem Text extrahiert werden (also z.B. “Präsident Dr. Wolfgang Schäuble:”). Daher ist das Verfahren dann doch nicht so trivial, wie es vielleicht zuerst den Anschein erweckt.

Importieren

#Pakete laden
library(tibble)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(magrittr)
library(stringr)
library(tidyr)
## 
## Attaching package: 'tidyr'
## The following object is masked from 'package:magrittr':
## 
##     extract
#Auflistung aller Files
files <- list.files("../../../daten/bundestag19/", 
                    pattern = ".xml")
files

#Import über eine Schleife
lp19 <- NULL

#Test
i <- files[10]

for (i in files){
  #Generiere Pfad zur Datei
  file_path <- str_c("../../../daten/bundestag19/", i)
  
  #Einlesen mit write
  source_file <- readLines(file_path, encoding="UTF-8")
  
  #Variablen extrahieren
  
  l <- 253 #test
  
  temp <- NULL
  
  for (l in 1:length(source_file)){
    #rowid
    rowid <- l
    
    #top
    top <- source_file[l] %>% 
      str_extract('(?<=top-id=\\").*?\\"') %>% 
      str_replace_all('[:punct:]', '')
    
    #Rede ID
    rede_id <- source_file[l] %>% 
      str_extract('(?<=rede id=\\").*?\\"') %>% 
      str_replace_all('[:punct:]', '')
    
    #Redner ID
    redner_id <- source_file[l] %>% 
      str_extract('(?<=redner id=\\").*?\\"') %>% 
      str_replace_all('[:punct:]', '')
    
    #Name
    name <- source_file[l] %>% 
      str_extract('(?<=name\\>).*?(?=\\</name\\>)') %>% 
      str_replace_all('[:punct:]', '')
      
    vorname <- source_file[l] %>% 
      str_extract('(?<=vorname\\>).*?(?=\\<)') %>% 
      str_replace_all('[:punct:]', '')
    
    nachname <- source_file[l] %>% 
      str_extract('(?<=nachname\\>).*?(?=\\<)') %>% 
      str_replace_all('[:punct:]', '')
    
    #Rolle
    rolle_lang <- source_file[l] %>% 
      str_extract('(?<=rolle_lang\\>).*?(?=\\<)') %>% 
      str_replace_all('[:punct:]', '')
    
    rolle_kurz <- source_file[l] %>% 
      str_extract('(?<=rolle_kurz\\>).*?(?=\\<)') %>% 
      str_replace_all('[:punct:]', '')
    
    #Fraktion
    fraktion <- source_file[l] %>% 
      str_extract('(?<=fraktion\\>).*?(?=\\<)') %>% 
      str_replace_all('[:punct:]', '')
    
    #Ortszusatz
    ortszusatz <- source_file[l] %>% 
      str_extract('(?<=ortszusatz\\>).*?(?=\\<)') %>% 
      str_replace_all('[:punct:]', '')
    
    #Klasse der Reden
    klasse <- source_file[l] %>% 
      str_extract('(?<=klasse=\\").*?(?=\\")')
    
    #Text der Rede
    text <- source_file[l] %>% 
      str_extract('(?<=\\>).*?(?=\\</p\\>)')
    
    kommentar <- source_file[l] %>% 
      str_extract('(?<=\\>).*?(?=\\</kommentar\\>)')
  
    #Zusammenfügen
    temp <- bind_rows(temp, tibble(i, rowid, top, rede_id, redner_id, name, vorname, nachname, rolle_lang, rolle_kurz, fraktion, ortszusatz, text, kommentar))
    
    #Leere Zeilen löschen
    dim(temp)
    temp <- temp %>%
      mutate(number_nas = rowSums(is.na(temp))) %>%
      filter(number_nas!=13) %>%
      arrange(rowid)
    dim(temp)
  }
  
  lp19 <- bind_rows(lp19, data.frame(temp))

}

save(lp19, file = "../../../daten/bundestag19/lp19_raw.rda")

Aufräumen

Das Datenaufbereiten kann durchaus viel Zeit in Anspruch nehmen. Hier habe ich einmal ein “Quick-and-Dirty”-Verfahren angewandt, d.h. ohne Anspruch auf Vollständigkeit. In anderen Worten: Wenn Sie eine genaue Inspektion der Daten vornehmen, werden Sie sicherlich noch einige viele Aufbereitungsfehler feststellen. Der Datensatz ist alles andere als tidy. Und in manchen Fällen muss man bestimmt auch noch einmal an der obigen Schleife arbeiten und die regulären Ausdrücke anpassen. Ein Beispiel: Doppelnamen (“Müller-Schmidt”) werden derzeit abgekürzt in “MüllerSchmidt”, da der obige reguläre Ausdruck Punktuationen im Namen löscht. Das könnte man relativ einfach anpassen – im Vergleich zu manchen anderen fehlerhaft kodierten Einträgen.

#Datensatz laden
load("../../../daten/bundestag19/lp19_raw.rda")

#Inspektion
library(knitr)
lp19 %>% 
  #group_by(i) %>% 
  filter(i == "19002-data.xml") %>% 
  select(rede_id, nachname, name, text, kommentar) %>% 
  slice(50:200) %>% 
  DT::datatable(.)

#Problem: Äußerungen der Bundestags-Präsidenten sind nicht
#mit einer Rede ID gekennzeichnet, sondern mit dem Tag <name>
lp19 <- lp19 %>% 
  mutate(nachname = case_when(
    str_detect(name, "[Pp]räsident") & is.na(nachname) ~ name, 
    str_detect(text, "[Pp]räsident") & str_count(text, "\\S+") < 5 & is.na(nachname) ~ text,
    TRUE ~ nachname
  ))

#Variablen weiterführen
lp19 <- lp19 %>% 
  group_by(i) %>% 
  fill(top, 
       rede_id, 
       redner_id, 
       name,
       vorname, 
       nachname, 
       rolle_lang, 
       rolle_kurz, 
       fraktion, 
       ortszusatz)

#Erneute Inspektion
lp19 %>% 
  select(rede_id, nachname, text, kommentar) %>% 
  filter(i == "19002-data.xml") %>% 
  slice(50:200) %>% 
  DT::datatable(.)
## Adding missing grouping variables: `i`

#nicht relevante Beobachtungen löschen
lp19 %>% 
  ungroup() %>% 
  mutate(spec_chars = str_count(text, "\\<")) %>% 
  count(spec_chars)
## # A tibble: 14 x 2
##    spec_chars     n
##         <int> <int>
##  1          0 20498
##  2          1   406
##  3          4    15
##  4         10   705
##  5         11    23
##  6         12   265
##  7         13    12
##  8         14   181
##  9         15     6
## 10         16    85
## 11         17     7
## 12         18    28
## 13         19     2
## 14         NA  9796

dim(lp19)
## [1] 32029    15
lp19 <- lp19 %>% 
  mutate(spec_chars = str_count(text, "\\<"), 
         spec_chars = case_when(
           is.na(spec_chars) ~ 0L, 
           TRUE ~ spec_chars
         )) %>% 
  filter(!spec_chars > 1) %>% 
  filter(text!="" | kommentar!="") %>% 
  mutate(text = str_replace_all(text, '\\<.*?\\>', '')) %>% 
  mutate(kommentar = str_replace_all(kommentar, '\\<.*?\\>', ''))
dim(lp19)
## [1] 26014    16

#Erneute Inspektion
lp19 %>% 
  select(rede_id, nachname, text, kommentar) %>% 
  filter(i == "19002-data.xml") %>% 
  slice(1:200) %>% 
  DT::datatable(.)
## Adding missing grouping variables: `i`

Aggregieren

Der letzte Schritt besteht nun im Aggregieren des Datensatzes. Eine Frage hierbei ist wieder, welche Rolle die Kommentare, also z.B. “Beifall” oder “Lachen”, spielen. Benötigen Sie diese Kommentare, um Unterbrechungen oder zumindest Hintergrundaktivitäten abbilden zu können, oder nicht? Hier werden die Kommentare nicht weiter berücksichtigt\(\dots\)

#Sprecher*innenwechsel markieren
lp19_agg <- lp19 %>% 
  filter(text!="") %>% 
  group_by(i) %>% 
  mutate(speaker_change = case_when(
    nachname != lag(nachname) ~ 1, 
    TRUE ~ 0
  )) %>% 
  mutate(speaker_change = as.numeric(speaker_change)) %>% 
  mutate(speaker_change = cumsum(speaker_change)) %>%
  group_by(i, speaker_change) %>%
  summarise(rowid = first(rowid),
            top = first(top),
            rede_id = first(rede_id),
            redner_id = first(redner_id),
            name = first(name),
            vorname = first(vorname),
            nachname = first(nachname),
            rolle_lang = first(rolle_lang),
            rolle_kurz = first(rolle_kurz),
            fraktion = first(fraktion),
            ortszusatz = first(ortszusatz),
            text = str_c(text, collapse=" ")) %>%
  arrange(rowid)

#Finale Inspektion?
lp19_agg %>%
  select(i, nachname, text) %>%
  slice(1:5) %>%
  DT::datatable(.)

save(lp19_agg, file="../../../daten/bundestag19/lp19_agg.rda")