Stand: 4. April 2022

Erforderliche Installationen und Initialisierung

Der Foliensatz nutzt das polmineR-Paket und das GermaParl-Korpus. Die Installation wurde im vorhergehenden Foliensatz ausführlicher erläutert.

Beachte: Die Funktionalität für einen Teil des hier beschriebenen Codes steht erst mit polmineR-Version 0.8.6 zur Verfügung steht. Bitte updaten, falls erforderlich!

Für die folgenden Beispiele laden wir zunächst polmineR. Außerdem wird für die Beispiele das data.table-Paket benötigt.

library(polmineR)
library(data.table)

Zum Anfang etwas Terminologie

  • Die im PolMine-Projekt aufbereiteten Korpora werden aus Rohdaten (pdf, plain text, html) in standardisierte XML-Formate übersetzt. Die Standardisierung entspricht Vorgaben der Text Encoding Initiative (TEI).

  • Das TEI-XML des GermaParl-Korpus kann als Beispiel dienen. Es ist über ein GitHub-Repositorium offen zugänglich. Know your data - es ist sinnvoll, dieses Ausgangsformat zu kennen!

  • Das XML-TEI ist geeignet für die dauerhafte Datenhaltung und zur Sicherung von Interoperabilität, nicht jedoch für eine effiziente Analyse. Als indexing and query engine nutzt das PolMine Projekt (das polmineR-Paket) die Corpus Workbench (CWB)

  • CWB-indizierte Korpora können insbesondere auch linguistische Annotationen speichern und für die Analyse verfügbar machen. Diese werden über positionale Attribute (p-attributes) verfügbar.

  • Metadaten sind in der Terminologie der CWB als strukturelle Attribute (s-attributes) verfügbar. Entsprechend den Elementen von XML-Dokumenten können s-attributes hierarchisch ineinander geschachtelt sein: s-attributes sind nicht auf die Dokumentebene beschränkt, sondern können auch darin eingelagert Passagen von Text (z.B. Annotationen, Named Entities, in Parlamentsprotokollen: Zwischenrufe) auszeichnen.

registry-Verzeichnis und registry-Dateien

  • Korpora im CWB-Datenformat werden in registry-Dateien beschreiben, die in einem registry-Verzeichnis liegen. Die registry-Dateien deklarieren insbesondere die p- und s-attributes eines Korpus. Außerdem definieren corpus properties allgemeine Merkmale eines Korpus. Das ist inbesondere die Zeichenkodierung der Daten. Angaben zur Version, zum Datentypus oder zum Indizierungsdatum (build date) kommen optional hinzu.

  • Die Deklarationen der registry-Datei eines Korpus werden beim einmaligen Laden eines Korpus im Speicher gehalten, so dass diese Datei nicht immer wieder verarbeitet werden muss.

  • Die Umgebungsvariable CORPUS_REGISTRY definiert die zentrale “Korpusregistratur” eines Systems. Die in diesem Verzeichnis definierten Korpora werden beim Laden von polmineR geladen.

  • Die CORPUS_REGISTRY-Umgebungsvariable lässt sich wie folgt definieren. Wichtig: Dies muss vor dem Laden von polmineR erfolgen. Tip: Umgebungsvariablen für R-Sitzungen können dauerhaft über die Datei .Renviron definiert werden. Durch Aufruf der Hilfe zu den Routinen beim Start von R erfahren Sie mehr (?Startup).

Sys.setenv(CORPUS_REGISTRY = "/PFAD/ZU/REGISTRY/VERZEICHNIS")

Das temporäre registry-Verzeichnis

  • Ein besonderer Fall ist ein temporäres registry-Verzeichnis, das von polmineR beim Laden immer angelegt wird. Diese Sitzungs-Korpusregistrytur wird mit registry() abgefragt.
registry()
## /var/folders/fw/qwt11pjx1qs83dl2jwltcvmr0000gn/T/Rtmp1C3VVN/polmineR_registry
  • Diese temporäre registry wird insbesondere für modifizierte Kopien von registry-Dateien zu Beispielkorpora in R-Paketen genutzt. Diese Modifikationen sind erforderlich, um Pfadangaben zu aktualisieren.

  • Korpora in R-Paketen werden mit use() verfügbar gemacht, die insbesondere in Code-Beispielen genutzt werden. Daher sehen Sie im Beispiel-Code immer wieder diese beiden Zeilen:

use("polmineR")
use("RcppCWB")

Anzeige der vefügbaren Korpora

  • Die corpus()-Methode (ohne Argumente) gibt eine Liste der Korpora an, auf die in Analysen zugegriffen werden kann. Die Spalte size gibt die Korpusgröße an.
corpus()[, c("corpus", "encoding", "type", "template", "size")]
##          corpus encoding type template      size
## 1     GERMAPARL   latin1 plpr     TRUE 101013708
## 2 GERMAPARLMINI   latin1 plpr     TRUE    222201
## 3       REUTERS   latin1 <NA>     TRUE      4050
## 4          UNGA     utf8 <NA>    FALSE    127082
  • Aus Platzgründen haben wir in dieser Tabelle die Spalte mit dem jeweiligen registry-Verzeichnis ausgeschlossen.

  • corpus() ist auch nützlich als Vergewisserung, ob Korpora wie erwartet verfügbar sind. In unserem Fall sehen wir die Verfügbarkeit von GERMAPARL. GERMAPARLMINI ist als Beispielkorpus im polmineR-Paket enthalten, die Korpora REUTERS und UNGA sind in RcppCWB enthalten und sind hier ebenfalls verfügbar. Beachte: Entsprechend den Konventionen der CWB werden Korpora immer in Großbuchschraben geschrieben!

Korpora

  • Jede Analyse mit polmineR bezieht sich auf Korpora - auch wo Subkorpora analysiert werden, muss dieses zunächst als Subset eines Korpus gewonnen werden.

  • Grundsätzlich sind verschiedene auf Korpora anwendbare Methoden bezogen auf corpus-Objekte als Input, die mit der corpus()-Methode instantiert werden.

gparl <- corpus("GERMAPARL")
  • Seit polmineR v0.8.6 können Korpora geladen werden, deren registry-Dateien in beliebigen Verzeichnissen liegen. Hierfür muss dann zusätzlich das Argument registry_dir verwendet werden.

  • Eine grundlegende, einfache Methode für Korpora ist size(). Man erhält die Zahl der Token im Korpus. Sie wird wie folgt auf ein corpus-Objekt angewendet, wie wir es gerade mit gparl angelegt haben.

size(gparl)
## [1] 101013708

Es muss nicht immer gleich corpus() sein

  • Alle für corpus-Objekte verfügbaren Methoden können auch auf einen character-Vektor (der Länge 1) mit der (großgeschriebenen) Korpus-ID angewendet werden. Das corpus-Objekt wird dann intern instantiert. Das Ergebnis ist identisch.
size("GERMAPARL")
## [1] 101013708
  • Die Lesbarkeit von Code lässt sich oft durch die Verwendung von “pipes” verbessern. Pipes haben in R mit dem Paket magrittr Verbreitung gefunden. Sie dazu die folgende Schreibweise.
corpus("GERMAPARL") %>% size()
## [1] 101013708
  • Sie sehen die Nutzung von Pipes an vielen Stellen der polmineR-Dokumentation und auch in diesen Folien. Zwingend ist das nicht. Man sollte den Code einfach so schreiben, wie er am besten lesbar erscheint.

Liniguistische Annotationen: Positionale Attribute

  • Korpora werden in die CWB in tokenisierter Form importiert (Tokenisierung = Zergliederung des ursprünglichen Fließtextes in Worte / “Token”).

  • Jedem Token des Korpus wird bei der Indizierung ein eindeutiger numerischer Wert zugewiesen (“corpus position”, Abkürzung “cpos”).

  • Ergänzend zu der ursprüngliche Wortform im Ursprungstext, wird bei linguistisch annotierten Korpora (im Regelfall) eine Wortarterkennung (“part-of-speech”-Annotation, kurz “pos”) und eine Lemmatisierung der Token (Rückführung des Worts auf Grundform ohne Flektion, “lemma”) durchgeführt.

  • Mit der p_attributes()-Methode frägt man die p-Attribute eines Korpus ab.

p_attributes("GERMAPARL")
## [1] "lemma" "pos"   "word"

Die Tabelle auf der folgenden Seite vermittelt die Datenstruktur mit positionalen Attributen (p-attributes) und Korpus-Positionen (cpos). Der Text kann von oben nach unten gelesen werden.

CWB-Datenstruktur: Tokenstream

cpos word pos lemma
0 Liebe NN lieb
1 Kolleginnen NN Kollegin
2 und KON und
3 Kollegen NN Kollege
4 , $, ,
5 die ART d
6 Sitzung NN Sitzung
7 ist VAFIN sein
8 eröffnet VVPP eröffnen
9 . $. .

Grundsätzlich ist diese Datenstruktur vergleichbar mit jener jener des tidytext-Ansatzes.

Strukturelle Attribute (‘s-attributes’)

Metadaten eines Korpus werden als strukturelle Attribute (s-attributes) bezeichnet. Welche s-attributes bei einem Korpus verfügbar sind, fragt man mit s_attributes() ab.

s_attributes("GERMAPARL")
##  [1] "party"               "parliamentary_group" "speaker"            
##  [4] "lp"                  "session"             "date"               
##  [7] "role"                "interjection"        "agenda_item"        
## [10] "agenda_item_type"    "src"                 "url"                
## [13] "year"

Die Dokumentation eines Korpus sollte erklären, was die S-Attribute bedeuten. Um zu ermitteln, welche Ausprägungen es für ein S-Attribute gibt, wird das Argument s_attribute definiert.

s_attributes("GERMAPARL", s_attribute = "year")
##  [1] "1996" "1997" "1998" "1999" "2000" "2001" "2002" "2003" "2004" "2005"
## [11] "2006" "2007" "2008" "2009" "2010" "2011" "2012" "2013" "2014" "2015"
## [21] "2016"

Korpusgröße

Oben wurde schon erwähnt, dass man mit der size()-Methode die Größe eines Korpus abfragen kann.

size("GERMAPARL")
## [1] 101013708

Wird zusätzlich das Argument s_attribute definiert, werden die Subkorpus-Größen angegeben, die bei einer Aufteilung des Korpus entsprechend dem s-attribute entstehen.

size("GERMAPARL", s_attribute = "lp")
##    lp     size
## 1: 13 11676618
## 2: 14 19349263
## 3: 15 12785509
## 4: 16 18412812
## 5: 17 23418060
## 6: 18 15371446

Rezept: Balkendiagramm mit Korpusumfang

In einem kleinen Beispiel wollen wir mit einem Balkendiagramm visualisieren, wie die Zahl der Worte in den Plenarprotokollen variiert. Zunächst ermitteln wir mit der s_attributes()-Methode die Größe des Korpus differenziert nach Jahren.

s <- size("GERMAPARL", s_attribute = "year")

Dann machen wir daraus ein Balkendiagramm, wobei wir auf der Y-Achse die Größe des Korpus in Tausend Token angeben.

barplot(
  height = s$size / 1000,
  names.arg = s$year,
  main = "Größe GermaParl nach Jahr",
  ylab = "Token (in Tausend)", xlab = "Jahr",
  las = 2
  )

Die damit erzeugt Grafik kommt auf der folgenden Folie.

In den Jahren 1998, 2002, 2005, 2009 und 2013 sehen wir jeweils geringere Korpusumfänge. Welchen systematischen Grund hat das?

Korpusgröße: Zwei S-Attribute

Bei der size()-Methode kann auch ein zweites S-Attribut angegeben werden, dann wird eine Tabelle mit Korpusgrößen differenziert nach den beiden Merkmalen ausgegeben.

Beachte: Der Rückgabewert ist hier ein data.table, nicht ein data.frame, das Standard-Datenformat von R für Tabellen. Viele Operationen können mit data.tables weitaus schneller als mit data.frames durchgeführt werden. Daher nutzt das polmineR-Paket intern intensiv data.tables. Ein Umwandlung in data.frames erfolgt nicht, ist aber problemlos möglich.

dt <- size("GERMAPARL", s_attribute = c("speaker", "party"))
df <- as.data.frame(dt) # Umwandlung in data.frame
df_min <- subset(df, speaker != "") # In wenigen Fällen wurde Sprecher nicht erkannt
head(df_min)
##                 speaker party   size
## 6        Achim Großmann   SPD 113226
## 7            Achim Post   SPD   4785
## 8  Adelheid D. Tröscher   SPD  29462
## 9        Adolf Ostertag   SPD  44002
## 10           Adolf Roth   CDU  24900
## 11         Agnes Alpers LINKE  18824

Redeanteile

Korpusgröße: Zwei Dimensionen

In einem zweiten Beispiel zur Arbeit mit den Ergebnissen einer Untergliederung des Korpus nach zwei Kriterien stellen wir die Frage, wie die Redeanteile der Fraktionen zwischen den Legislaturperioden geschwankt haben.

dt <- size("GERMAPARL", s_attribute = c("parliamentary_group", "lp"))
dt_min <- subset(dt, parliamentary_group != "") # Bearbeitung data.table wie data.frame

Die Tabelle, die wir jetzt haben, ist in einer sogenannten “extensiven” Form. Sie kann folgendermaßen in eine Normalform gebracht werden.

tab <- dcast(parliamentary_group ~ lp, data = dt_min, value.var = "size")
setnames(tab, old = "parliamentary_group", new = "Fraktion") # Umbenennung

Das schauen wir uns an, wobei wir ein ‘widget’ benutzen, das mit der JavaScript-Bibliothek DataTable (nicht verwechseln mit data.table!) erzeugt wird. (Die Ausgabe lässt sich auch in Folien einbeziehen, die - wie diese - mit R Markdown geschrieben wurden.)

DT::datatable(tab)

Wortzahl nach Fraktion und Jahr

Vorbereitungen für den barplot

Für den gruppierten barplot brauchen wir eine Matrix, welche die Höhe der Balken angibt.

pg <- tab[["Fraktion"]] # Für Beschriftung des barplot "retten" wir die Fraktionen
tab[["Fraktion"]] <- NULL # Spalte "Fraktionen" wird an dieser Stelle beseitigt
m <- as.matrix(tab) # Umwandlung des data.table in Matrix
m[is.na(m)] <- 0 # Wo NA-Werte in der Tabelle sind, ist die Korpusgröße 0

Der letzte “Dreh” ist ein Vektor mit den Farben, die den Fraktionen üblicherweise zugeordnet sind. Dieser ist benannt, so dass über eine Indizierung die Zuweisung der Farben erfolgen kann, ohne dass man versehentlich verrutschen könnte.

colors <- c(
  "CDU/CSU" = "black", FDP = "yellow",
  SPD = "red", GRUENE = "green", LINKE = "pink", PDS = "pink",
  fraktionslos = "lightgrey", parteilos = "darkgrey"
  )

Let’s go

Den barplot auszugeben, ist nun keine Zauberei mehr.

barplot(
  m / 1000, # Höhe der Balken - Zahl Worte, in Tausend
  ylab = "Worte (in Tausend)", # Beschriftung der Y-Achse
  beside = TRUE, # Gruppierung
  col = colors[pg] # Farben der Balken, Indizierung gewährleistet richtige Reihenfolge
  )
# Um die Legende zweispaltig gestalten zu können, erstellen wir die Legende gesondert.
legend(
  x = "top", # Platzierung Legende oben mittig
  legend = pg, # Beschriftung mit Benennung Fraktion
  fill = colors[pg], # Indizierung gewährleistet, dass nichts verrutschen kann
  ncol = 2, # zweispaltige Legende
  cex = 0.7 # kleine Schrift
  )

Korpus nach Legislaturperiode und Fraktion

Kenne deine Daten!

Das Beispiel einer Visualisierung der Korpusgröße nach Fraktionszugehörigkeit und Legislaturperiode ist nicht ganz zufällig gewählt. In der 15. Wahlperiode gibt es einen gar nicht so kleinen Redeanteil von Sprechern, die “fraktionslos” sind. Wenn Sie die gleiche Analyse auf Ebene von Parteizugehörigkeit durchführen: Was sehen Sie da? Die fraktionslosen Abgeordneten der 15. Wahlperiode sind Angehörige der PDS.

Dies ist keine Einführung in das GermaParl-Korpus, aber der richtige Ort für den Hinweis, dass jede gute Analyse ein gutes Verständnis der Daten zur Voraussetzung hat.

Lesen Sie die Dokumentation der Daten und sehen Sie sich die Daten an, in diesem Fall das TEI-XML. Was für jede andere Datenart eine Selbstverständlichkeit ist, gilt auf für Korpora: Wenn man zu wenig über die Daten weiß, ist die Wahrscheinlichkeit schlechter Forschung groß.

Diskussion und Ausblick

Zunächst eine Ermutigung: Der Einstieg in die Arbeit mit data.tables erfordert Umdenken, lohnt sich aber, nicht nur wegen der Effizienz dieser Datenstruktur. Als Beispiel dient das folgende “snippet”.

size("GERMAPARL", s_attribute = "speaker")[speaker == "Angela Merkel"]
##          speaker   size
## 1: Angela Merkel 701455

Nochmal das Stichwort “know your data”: Wenn Sie eine Blick in das TEI-XML des GermaParl-Korpus geworfen haben: Zwischenrufe sind - bewusst! - Teil der “XMLifizierung” der Protokolle, in der sie ausgezeichnet sind. Für saubere Analysen muss man also mit Subkorpora arbeiten, die Zwischenrufe aus der Analyse ausschließen. Wie das geht, ist Gegenstand des nächsten Foliensatzes.

Literatur

Baker, Paul. 2008. Using Corpora in Discourse Analysis, Ch. 1, 3. Continuum Discourse Series. London: Continuum International Publishing.

Blätte, Andreas. 2018a. “6 Interaktion Und Dialog Mit Großen Textdaten: Korpusanalyse Mit Dem „polmineR.” In Computational Social Science, 1st ed., 119–38. Nomos Verlagsgesellschaft mbH & Co. KG.

———. 2018b. “7 Zum Verwechseln Ähnlich? Eine Klassifikationsanalyse Par-Lamentarischen Diskursverhaltens Auf Basis Des PolMine-Plenarprotokollkorpus.” In Computational Social Science, 1st ed., 139–62. Nomos Verlagsgesellschaft mbH & Co. KG.

FERNANDES, JORGE M, MARC DEBUS, and HANNA BÄCK. 2021. “Unpacking the Politics of Legislative Debates.” European Journal of Political Research 60 (4): 1032–45.

Manderscheid, Katharina. 2019. “Text Mining.” In Handbuch Methoden Der Empirischen Sozialforschung, 1103–16. Wiesbaden: Springer Fachmedien Wiesbaden.

Proksch, Sven-Oliver. 2020. “Computergestützte Textanalysen.” In Handbuch Methoden Der Politikwissenschaft, 817–35. Wiesbaden: Springer Fachmedien Wiesbaden.