Tatort (Quelle: ARD / daserste.de)

Hinter den Kulissen der Tableau Kino Tour – Teil 3: Die Tatorte

Nachdem ich in den beiden vergangenen Teilen dieser Serie gezeigt hatte, wie wir die IMDb-Daten (Teil 1) bzw. die Daten zu Filmreihen (Teil 2) extrahiert und in ein brauchbares Format überführt haben, möchte ich nun noch beleuchten, wie wir die Daten zur Analyse der Krimireihe “Tatort” gewonnen und bearbeitet haben. Für den “Tatort” als Untersuchungsobjekt haben wir uns entschieden, da es dazu Unmengen an Daten gibt (s.u.), da sich die Serie von verschiedenen Aspekten her analysieren lässt (die Serie wird seit vielen Jahrzehnten produziert, findet an unterschiedlichen geographischen Orten statt, involviert eine Menge an Ermittlern, Schauspielern, usw.), und da es eine der, wenn nicht sogar die beliebteste deutsche Fernsehserie ist. Und auch, weil ich selbst ein großer Fan bin und mich das Thema und die Daten auch ganz persönlich interessiert haben…

Ursprünglich war auch diese Datensammlung ein manueller Prozess, den ich aber inzwischen versucht habe, mittels eines R-Skripts weitestgehend zu automatisieren. Wie bei schneller Durchsicht des Codes offenbar werden wird, ist mir dies leider nur teilweise gelungen, was auf Unzulänglichkeiten der verwendeten Datenquellen zurückzuführen ist. Nichtsdestotrotz sollte es mit dem vorliegenden Skript nun möglich sein, tagesaktuelle (bzw. wochenaktuelle) Daten zu den Tatortfolgen und den Details zu den Ermittlern und der Produktion zu extrahieren. Natürlich vorbehaltlich zukünftiger Änderungen an den Online-Datenquellen.

Tatort (Quelle: ARD / daserste.de)

Tatort (Quelle: ARD / daserste.de)

Die Datenquellen

Die Tatortfolgen

Auch wenn Wikipedia keine wirklich vertrauenswürdige Informationsquelle ist (insbesondere für den Wissenschaftler in mir), so stellt sie doch gerade bei Themen des allgemeinen öffentlichen Interesses eine erstaunlich umfangreiche, gut gepflegte und hochaktuelle Quelle dar. So auch im Fall der Tatortfolgen. So finden wir dort nicht nur allgemeine Informationen zur Fernsehserie “Tatort”, sondern auch eine Liste aller jemals produzierten Tatortfolgen. Darin finden sich dann nicht nur inhaltliche Informationen, also beispielsweise der Titel, das Ermittlerteam und die Folgennummer, sondern auch Produktionsdaten wie Erstausstrahlungsdatum, produzierende Sendeanstalt, Produzenten, Autoren, Regisseure und weitere Besonderheiten. Diese Produktionsdaten fanden zwar im Rahmen der Tableau Kino Tour erstmal keine Verwendung, wir haben sie aber dennoch im Datensatz behalten – für spätere Analysen und Artikel…

Die Ermittlerteams

Leider finden sich in den o.g. Daten zu den einzelnen Tatort-Episoden keine Informationen zu den Orten, an denen jeweils ermittelt wurde. Diese ließen sich zwar aus weiteren Tabellen der Wikipedia gewinnen, deren Format zu parsen erschien mir aber zu aufwändig. Deshalb hatte ich mich nach einer weiteren Datenquelle umgesehen, und bin auf der offiziellen Tatort-Seite der ARD fündig geworden. Dort gibt es zwei getrennte Übersichten, die aktuellen Ermittlerteams einerseits, und die ehemaligen andererseits. Auch diese beiden Tabellen sind nicht wirklich prädestiniert zum automatisierten Auslesen, aber es erschien mir doch einfacher.

Das Skript

Das komplette Skript liegt, ebenso wie die anderen Skripte, zum freien Download auf meinem GitHub-Repository bereit. Ich freue mich über Kommentare und Kollaborateure, die das Skript vermutlich an allen Ecken und Enden optimieren und korrigieren können.

Da wir hier Daten aus Tabellen von Webseiten auslesen wollen, bietet sich in R die Verwendung des rvest-Packages an. Die vielen Vektoren zu Beginn des Codes werden später noch eine Rolle spielen und sind Teil des nach wie vor leider etwas manuellen Prozesses.

Die Verarbeitung der Tatortfolgen

Richtig los geht es dann mit dem Statement, das sich von Zeile 39 bis 62 erstreckt:

tatorte <- url %>%
  read_html() %>%
  html_node(xpath='//*[@id="mw-content-text"]/table[1]') %>%
  html_table() %>% 
  ## Daten mittels RegEx verarbeiten
  ### Folge
  mutate(unechter.Tatort = grepl("\\d(?:a|b)\\*", Folge)) %>% 
  filter(!unechter.Tatort) %>% 
  ### Titel
  mutate(Titel = gsub("\\(Folge .+? trägt den(?: gleichen|selben) Titel\\)", 
                      "", 
                      Titel)) %>% 
  ### Sender
  mutate(Sender = gsub("\\n", 
                       "", 
                       Sender)) %>% 
  ### Besonderheiten
  mutate(Besonderheiten = gsub("\\[.+?\\]", 
                               "", 
                               Besonderheiten)) %>% 
  ### Fall
  mutate(Fall = gsub("\\s.*", 
                     "", 
                     Fall))

Hier wird zunächst das Element im Document Object Model der Wikipedia-Seite angesprochen, das die gesuchte Tabelle der Episoden enthält (hier die erste Tabelle), diese in einen Dataframe gespeichert und dann mittels verschiedener regulärer Ausdrücke bearbeitet.

Als “unechte” Tatorte (Zeilen 45-46) habe ich hierbei jene 13 Folgen bezeichnet, die vom ORF außerhalb der Gemeinschaftsproduktion mit der ARD produziert wurden. Sie sind in der Liste mit Suffixen nach den Folgennummern gekennzeichnet, beispielsweise die Episode “168a*”. Der Ausdruck in Zeile 45 sucht diese im Feld “Folge”, Zeile 46 filtert sie heraus. Diese fehlen also in unserem Datensatz. Ich bitte die österreichischen Tatort-Puristen um Nachsicht, aber diese Episoden noch mit in den Kanon einzuordnen hätte die Datenverarbeitung noch weiter verkompliziert. Die Zeilen 60-62 beschäftigen sich ebenfalls mit den Nachwirkungen dieser “unechten” Episoden im Feld “Fall”, da diese auch die Zählung, der wie vielte Fall eines Ermittlerteams eine Folge darstellt, in einigen Fällen mit beeinflusst. Im Feld “Besonderheiten” habe ich zudem die Wikipedia-Fußnoten (bspw. “[2]”) entfernt.

Das Feld “Erstausstrahlung” bedarf auch einer detaillierteren Behandlung (Zeilen 64-81), da hier teilweise mehrfache Daten (je nach Senderanstalt) angegeben werden (bspw. bei Folge 363, die zunächst am 25. Dezember 1996 im NDR ausgestrahlt wurde, ehe sie am 8. Juni 1997 in der ARD lief) – uns interessiert aber zunächst nur das absolut erste Datum. Zudem habe ich die einzelnen Datumsinformationen gleich in ein für Tableau leicht verdauliches Format gebracht (das war noch vor der verbesserten DATE()-Funktion in Version 10.2). Hierbei findet auch der Vektor monate Verwendung, der am Anfang des Skripts initialisiert wurde:

Erstausstrahlung.Datum <- sub("(\\d{1,2})\\.\\s?(\\w+?)\\.?\\s?(\\d{4})(.*)", 
                               "\\1@\\2@\\3@", 
                               tatorte$Erstausstrahlung, 
                               perl = TRUE) %>% 
  strsplit("@")
Erstausstrahlung.Monat <- sapply(Erstausstrahlung.Datum, 
                                 "[",
                                 2) %>% 
  substr(1, 3) %>% 
  match(., monate)
tatorte$Erstausstrahlung <- paste(sapply(Erstausstrahlung.Datum, 
                                         "[",
                                         1), 
                                  Erstausstrahlung.Monat, 
                                  sapply(Erstausstrahlung.Datum, 
                                         "[",
                                         3), 
                                  sep = ".")

Mit den Namen der Ermittler beschäftigen sich die dann folgenden Zeilen 82-177:

### Ermittler (komplett)
tatorte$Ermittler.komplett <- gsub("(\\n.*|\\(.*\\))", 
                                   "", 
                                   tatorte$Ermittler)
### Ermittler
tatorte$Ermittler <- gsub("\\s/.*", 
                          "", 
                          tatorte$Ermittler.komplett)
tatorte <- tatorte %>% 
  mutate(Ermittler = gsub("\\s/.*", 
                          "", 
                          Ermittler)) %>% 
  mutate(Ermittler = gsub(" +$", 
                          "", 
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Bienzle und Gächter$",
                          "Bienzle",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Blum$",
                          "Blum und Perlmann",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Blum und Perlmann, Matteo Lüthi$",
                          "Blum und Perlmann",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Blum und Perlmann, Reto Flückiger$",
                          "Blum und Perlmann",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Borowski und Brandt$",
                          "Borowski",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Borowski und Jung$",
                          "Borowski",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Carlucci und Gertsch$",
                          "Carlucci",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Delius a.D.$",
                          "Delius",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Dellwo$",
                          "Dellwo und Sänger",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Sänger und Dellwo$",
                          "Dellwo und Sänger",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Eisner$",
                          "Eisner und Fellner",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Falke und Grosz$",
                          "Falke",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Falke und Lorenz$",
                          "Falke",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Flemming und Koch$",
                          "Flemming",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Flemming, Koch und Ballauf$",
                          "Flemming",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Flückiger und Lanning$",
                          "Flückiger und Ritschard",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Hellmann und Ritter$",
                          "Hellmann",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Lürsen$",
                          "Lürsen und Stedefreund",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Lutz und Schreitle$",
                          "Lutz",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Marek a. D.$",
                          "Marek",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Odenthal$",
                          "Odenthal und Kopper",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Ritter und Stark$",
                          "Ritter",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Sänger$",
                          "Dellwo und Sänger",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Sieland, Gorniak, Mohr und Schnabel$",
                          "Sieland, Gorniak und Schnabel",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Stark$",
                          "Ritter",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Steier und Mey$",
                          "Steier",
                          Ermittler)) %>% 
  mutate(Ermittler = gsub("^Stoever$",
                          "Stoever und Brockmöller",
                          Ermittler))

Die Zeilen 83-89 sorgen dafür, dass alle über den Namen des Ermittlerteams hinausgehenden Informationen und Zeilenumbrüche entfernt werden. So sieht das Feld “Ermittler” bei Folge 453 beispielsweise aus wie folgt: “Batic und Leitmayr \n(Gastauftritt Odenthal)”. Dass Lena Odenthal hier einen Gastauftritt bei den Münchner Kollegen hatte ist zwar durchaus interessant (die Analyse der Gastauftritte füllt wahrscheinlich einen eigenen Artikel), spielt aber hier zunächst keine Rolle.

Die verbleibenden Zeilen 97-177 beschäftigen sich dann damit, die in unterschiedlichen Formaten auftauchenden Namen der Ermittlerteams zu standardisieren. Innerhalb der Tabelle sind sie zwar einheitlich, sie passen aber nicht immer zu den Bezeichnungen in unserem zweiten Datensatz von der ARD-Website mit den Details zu den Ermittlerteams. Puristen werden bei diesen Korrekturen möglicherweise auch teilweise aufmerken, da ich hier in der Tat einige Sachverhalte ein wenig vereinfacht habe. So habe ich beispielsweise alle Folgen, in denen Moritz Eisner ermittelt, als “Eisner und Fellner” deklariert, obwohl die beiden erst seit seinem 24. Fall zusammen ermitteln und er zuvor mit wechselnden anderen KollegInnen zusammengearbeitet hatte. Es wäre durchaus spannend, sich einmal die genaue Zusammensetzung der einzelnen Ermittlerteams anzusehen, aber für die vorliegenden Analysen war uns dieser vereinfachte Blick ausreichend.

Die Verarbeitung der Ermittlerteams

Die Zeilen 187 bis 215 lesen die Daten zu den nicht mehr aktiven Ermittlerteams von der ARD-Website und bringen sie in ein analysierbares Format:

url <- "http://www.daserste.de/unterhaltung/krimi/tatort/kommissare/kommissare-ausser-dienst-100.html"
kommissare.ehemalig <- url %>%
  read_html() %>%
  html_node(xpath='//*[@id="content"]/div/div[2]/div[1]/div/div/div/div/div[3]/table') %>%
  html_table() %>% 
  ## Spalten umbenennen
  rename(Kommissare = `Die Kommissare`) %>% 
  ## unnötige Spalte 2 entfernen
  select(-2) %>% 
  ## leere Zeilen entfernen
  filter(Kommissare != "") %>% 
  ## Daten mittels RegEx verarbeiten
  ### Kommissare
  mutate(Kommissare = gsub(paste(dienstgrade, 
                                 collapse = "|"), 
                           "", 
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("\\s{2,}", 
                           " ", 
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^ +", 
                           "", 
                           Kommissare)) %>% 
  ### Einsatzort
  mutate(Einsatzort = gsub("^nordd. Kleinstadt$", 
                           "Endwarden", 
                           Einsatzort)) %>% 
  ### fehlenden Kommissar hinzufügen
  rbind(c("Felber", "Frankfurt", "Heinz Schubert", "HR", 1))

Nachdem unnötige Spalten und Zeilen entfernt wurden, werden mittels des zu Beginn des Skripts definierten Vektors dienstgrade die für unsere Zwecke irrelevanten Dienstgrade aus den Namen der Ermittlerteams entfernt. Zudem werden unnötige Leerzeichen entfernt und den recht generischen Handlungsort “norddt. Kleinstadt” der Folge 125 durch den tatsächlich verwendeten Namen der fiktiven Stadt “Endwarden” ersetzt. Unerklärlicherweise fehlt in der Aufzählung der nicht mehr aktiven Ermittler KHK Leo Felber, diesen habe ich also noch manuell hinzugefügt.

In ähnlicher Weise lesen und verarbeiten die Zeilen 220 bis 233 die Daten zu den derzeit aktiven Ermittlerteams:

url <- "http://www.daserste.de/unterhaltung/krimi/tatort/kommissare/tatort-filter-aktuelle-kommissare-100.html"
kommissare.aktuell <- url %>%
  read_html() %>%
  html_nodes(css = '.headline') %>% 
  sapply(function(x){html_text(html_children(x))}) %>% 
  unlist() %>% 
  matrix(ncol = 2, 
         byrow = TRUE) %>% 
  data.frame(stringsAsFactors = FALSE) %>% 
  ## Spalten umbenennen
  rename(Kommissare = X1, 
         Einsatzort = X2) %>% 
  ## Daten mittels RegEx verarbeiten
  mutate(Einsatzort = gsub("\\(|\\)", "", Einsatzort))

Die Kürze des Codes zeigt, dass hier vergleichbar wenig manuelle Nacharbeit nötig war.

Die nachfolgende Zeilen 237 bis 327 fügen dann die Daten der aktuellen und ehemaligen Ermittlerteams und ihrer Einsatzorte zu einem gemeinsamen Dataframe zusammen:

kommissare <- kommissare.ehemalig %>% 
  select(Kommissare, Einsatzort) %>% 
  union(kommissare.aktuell) %>% 
  ## Daten mittels RegEx verarbeiten
  ### Einsatzort
  mutate(Einsatzort = gsub("Franken",
                           "Nürnberg",
                           Einsatzort)) %>%
  mutate(Einsatzort = gsub("Heppenheim",
                           "Heppenheim (Bergstraße)",
                           Einsatzort)) %>%
  mutate(Einsatzort = gsub("Bern",
                           "Berne",
                           Einsatzort)) %>%
  mutate(Einsatzort = gsub("Leipzig und Dresden",
                           "Leipzig",
                           Einsatzort)) %>%
  ### Kommissare
## add Felber (ep.303)
  mutate(Kommissare = gsub("^Batic und Leitmayr mit Team$",
                           "Batic und Leitmayr",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Borowski und Brandt$",
                          "Borowski",
                          Kommissare)) %>% 
  mutate(Kommissare = gsub("^Borowski und Jung$",
                          "Borowski",
                          Kommissare)) %>% 
  mutate(Kommissare = gsub("^Cenk Batu$",
                           "Batu",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Heinz Brammer$",
                           "Brammer",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Marianne Buchmüller$",
                           "Buchmüller",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Casstorff und Oberkommissar Holicek$",
                           "Casstorff und Holicek",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Sänger und Dellwo$",
                           "Dellwo und Sänger",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Falke und Grosz$",
                           "Falke",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Haferkamp$",
                           "Haferkamp und Kreutzer",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Robert Hellmann$",
                           "Hellmann",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Paul Kant und Jakob Varasani$",
                           "Kant und Varanasi",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Ludwig Lenz$",
                           "Lenz",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Dorn und Lessing$",
                           "Lessing und Dorn",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Liersdahl$",
                           "Liersdahl und Schäfermann",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Franz Markowitz$",
                           "Markowitz",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Pfeiffer$",
                           "Pfeifer",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Eva Saalfeld und Andreas Keppler$",
                           "Saalfeld und Keppler",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Schimanski$",
                           "Schimanski und Thanner",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Martin Schmidt$",
                           "Schmidt",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Lea Sommer$",
                           "Sommer",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Frank Steier$",
                           "Steier",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Philipp von Burg und Gertsch$",
                           "von Burg und Gertsch",
                           Kommissare)) %>% 
  mutate(Kommissare = gsub("^Till Ritter$",
                           "Ritter",
                           Kommissare)) 

Hier war leider wieder viel manuelle Nacharbeit nötig, damit diese Daten einerseits später in Tableau auch geokodierbar sind (Zeilen 242-253) und die Namen der Ermittlerteams auch mit den im oben vorgestellten Datensatz der Folgen zusammenpassen (Zeilen 256-327). Zugegeben, die Anpassung der Namen der Ermittlerteams auf zwei Codeblocks aufzuteilen ist etwas verwirrend und kein guter Stil, hat sich aber einfach so ergeben aus dem chronologischen (und recht gehetzten) Aufbau dieses Skripts. Auch hier darf sich gerne ein(e) geneigte(r) Leser(in) gütlich tun.

Zu guter Letzt bleibt noch das Zusammenführen der Daten zu den Tatortfolgen im Dataframe tatorte und der Ermittlerteams im Dataframe kommissare:

komplett <- tatorte %>% 
  full_join(kommissare, 
            by = c("Ermittler" = "Kommissare")) %>% 
  filter(!is.na(Folge)) %>% 
  select(Folge, Titel, Ermittler, Fall, Einsatzort) %>% 
  left_join(Geodaten, 
            by = c("Einsatzort" = "V1")) %>% 
  rename(Stadt = Einsatzort, 
         Bundesland = einsatzbundesländer, 
         Land = einsatzländer) %>% 
  transform(Folge = as.numeric(Folge), 
            Fall = as.numeric(Fall)) %>% 
  arrange(Folge)

Als Exportformat haben wir uns aus rein demonstrativen Zwecken im Rahmen der Tableau Kino Tour für Excel-Dateien entschieden:

write.xlsx(komplett, 
           file = "Folgen.xlsx",
           sheetName = "Tatort", 
           row.names = FALSE)

Fertig!

Viel Spaß bei der Analyse der Tatort-Daten! Wie gesagt verwirft das Skript in seiner derzeitigen Form viele Daten, die eigentlich in den verwendeten Quellen mit angeboten werden. Beispielsweise die exakten Erstausstrahlungsdaten, die Produzenten und produzierenden Sendeanstalten der ARD, Informationen zu Regisseuren usw. Diese mit zu analysieren wäre absolut interessant, und steht auf meiner sehr langen und fast stündlich wachsenden ToDo-Liste. Ich freue mich natürlich über Kommentare hier oder auch Forks des GitHub-Repositories.

2 Comments

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.