1. Einleitung

In diesem Tutorial werden wir die ersten Schritte in der quantitativen Textanalyse machen. Wir werden hauptsächlich das quanteda-Paket und seine Nebenpakete (quanteda.textplots und quanteda.textstats) nutzen und vor allem drei Aspekte besprechen: das Einlesen von Textdaten in R, das Erstellen eines Korpus und das Hinzufügen von Variablen.

2. Installation und Laden der relevanten R-Pakete

Für dieses Tutorial benötigen wir vor allem quanteda, readtext, tidyverse und ggplot2. Zu allen Paketen gibt es auch sehr hilfreiche CheatSheets.

install.packages("tidyverse")
install.packages("quanteda")
install.packages("quanteda.textplots")
install.packages("quanteda.textstats")

install.packages("readtext")
library(tidyverse) 

library(quanteda)
library(quanteda.textplots)
library(quanteda.textstats)

library(readtext)
library(ggplot2)

Andere Pakete, die im Laufe dieses Seminars noch eine wichtige Rolle spielen werden sind u.a. topicmodels stm (Themenmodelle), RTextTools (überwachtes maschinelles Lernen) und spacyr (POS-Tagging und Named-Entity-Erkennung).

3. Einlesen von Textdaten

Für die Arbeit mit quanteda werden Textdaten mit der Funktion readtext() aus dem gleichnamigen Paket eingelesen. Eine ausführliche Einführung zu den Funktionalitäten und der Anwendung von readtext findet ihr in der offiziellen Vignette des Paktes: [readtext vignette]https://cran.r-project.org/web/packages/readtext/vignettes/readtext_vignette.html.

Mit readtext lassen sich unterschiedliche Dateiformate importieren (u.a. txt, PDF, csv und auch Word-Dateien). Grundsätzlich sind aber Plaintext-Dateien (in der Regel mit der Endung ".txt" versehen) und Daten in Tabellenform (csv Dateien) zu bevorzugen. Bei (Text)Daten aus Tabellen ist es jedoch wichtig genau zu definieren welche Felder die Primär-(also Text-) und welche die Metadaten beinhalten.

Für dieses Tutorial lesen wir alle Entscheidungen des Bundesverfassungsgerichts der Jahre 2015-2019 ein. Die Textdaten könnt ihr als ZIP-Datei hier herunterladen: https://phimeyer.github.io/teaching/bverfg_15-19.zip.

Sobald ihr die ZIP-Datei entpackt habt, könnt ihr sehen, dass jede Datei einem Text entspricht. Das macht den Import sehr umkompliziert. Wir müssen lediglich den Computerpfad kopieren und damit R mitteilen, wo es die zu importierenden Texte findet ("bverfg_15-19" ist der letzte Ordner in dieser Befehlskette. Dort befinden sich die einzelnen txt.Dateien):

daten_bverfg <- readtext("/Users/PhMeyer/Seafile/Seafile/Meine Bibliothek/Lehre/Seminare Uni Hannover/12 SoSe 2021/Quantitative Textanalyse/data/bverfg_15-19")
head(daten_bverfg)
## readtext object consisting of 6 documents and 0 docvars.
## # Description: df[,2] [6 × 2]
##   doc_id                  text                  
##   <chr>                   <chr>                 
## 1 20150108_2bvr241913.txt "\"\n      \n\n \"..."
## 2 20150112_2bvq005314.txt "\"\n      \n\n \"..."
## 3 20150113_1bvr047214.txt "\"\n      \n\n \"..."
## 4 20150113_1bvr332314.txt "\"\n      \n\n \"..."
## 5 20150113_2bve000113.txt "\"\n      \n\n \"..."
## 6 20150113_2bvr239514.txt "\"\n      \n\n \"..."

Wenn alles geklappt hat, solltet ihr den Datensatz daten_bverfg rechts oben in eurem Environment finden und sehen, dass er 1616 Beobachtungen und zwei Variablen aufweist.

3. Dokumentenvariablen bzw. Metavariablen

Dieser Datensatz ist noch sehr roh und hat, wie gesagt, nur zwei Variablen: doc_id und text. Die erste der beiden, doc_id, ist lediglich der Name der Dateien. Die zweite Variable beinhaltet die Volltexte (Entscheidungen des Bundesverfassungsgericht). Die \n die ihr dort seht machen nicht den textuellen Inhalt aus. Das ist der html-Befehl für das Generieren eines neuen Absatzes. Die Dateien wurden direkt von der Internetseite des Bundesverfassungsgerichts heruntergeladen.

Wie auch immer, zwei Variablen sind natürlich nicht genug für unsere Bedürfnisse. Zum Glück können wir bereits beim Einlesen der Textdaten zwei weitere Variablen erstellen. Wie ihr in der doc_id Variable seht, bestehen die Dateinamen aus zwei Teilen die mit einem Unterstrich getrennt sind: 1) einem Datum und 2) einem Aktenzeichen (das Gericht gibt jeder Entscheidung ein Aktenzeichen), welches den jeweiligen Senat und Verfahrensart angibt (z.B. 2bvr bedeutet, dass der zweite Senat des Gerichts die Entscheidung getroffen hat und das die Entscheidung eine Verfassungsbeschwerde (bvr) behandelt hat). Diese beiden Bestandteile können wir verwenden um weitere Variablen zu erstellen (kontrolliert bitte ?readtext um die Argumente von readtext zu erlernen). Dafür nutzen wir die Argumente docvarsfrom und docvarnames.

daten_bverfg <- readtext("/Users/PhMeyer/Seafile/Seafile/Meine Bibliothek/Lehre/Seminare Uni Hannover/12 SoSe 2021/Quantitative Textanalyse/data/bverfg_15-19"
                         , docvarsfrom = "filenames"  # Hier sagen wir R wo es nach den Namen für den Befehl (docvarnames) suchen soll
                         , docvarnames = c("date","docket_nr")
                         )
head(daten_bverfg)
## readtext object consisting of 6 documents and 2 docvars.
## # Description: df[,4] [6 × 4]
##   doc_id                  text                       date docket_nr 
##   <chr>                   <chr>                     <int> <chr>     
## 1 20150108_2bvr241913.txt "\"\n      \n\n \"..." 20150108 2bvr241913
## 2 20150112_2bvq005314.txt "\"\n      \n\n \"..." 20150112 2bvq005314
## 3 20150113_1bvr047214.txt "\"\n      \n\n \"..." 20150113 1bvr047214
## 4 20150113_1bvr332314.txt "\"\n      \n\n \"..." 20150113 1bvr332314
## 5 20150113_2bve000113.txt "\"\n      \n\n \"..." 20150113 2bve000113
## 6 20150113_2bvr239514.txt "\"\n      \n\n \"..." 20150113 2bvr239514

Wie ihr seht erkennt readtext durch diese Argumente die Struktur der Dateiennamen und parst sie dementsprechend. Unsere Aufgabe war es im Grunde nur, passende Variablennamen (docvarnames) zu definieren.

Hinweis: Dieses Beispiel zeigt, wie wichtig es ist Textdateien funkionale Dateinamen zu geben.

So weit so gut! Aber wir können natürlich noch weitere Metavariablen erstellen. Zum Beispiel können wir die date-Variable weiter aufspalten um das Jahr und Monat als einzelne Variable zu erhalten. Weiterhin können wir den jeweiligen Senat und den Verfahrenstyp extrahieren. Für diese Aufgabe verwenden wir die base R-Funktion substr (wie immer: checkt die Dokumentation der Funktion mit ?!):

daten_bverfg$year <-  substr(daten_bverfg$date, 1,4) # Ich sage R, dass es die ersten vier Elemente von "date" (1,4) in die neue Variable "year" einfügen soll

daten_bverfg$year <-  as.numeric(daten_bverfg$year) # Da "str_extract" eine character Variable erstellt, sagen wir R das die Variable in eine numerische Varibale transformieren soll

daten_bverfg$month <-  substr(daten_bverfg$date, 5,6) 
daten_bverfg$month <-  as.numeric(daten_bverfg$month) 

daten_bverfg$senat <- substr(daten_bverfg$docket_nr, 1,1) # substr brauch einen Anfangswert und einen Endwert. Da wir hier lediglich die erste Ziffer aus "docket_nr" brauchen, sagen wir der Funktion das sie an Position 1 starten und enden soll

daten_bverfg$senat  <-  as.numeric(daten_bverfg$senat)

daten_bverfg$proceeding <- substr(daten_bverfg$docket_nr, 2,4)  # hier ziehen wir uns die verfahrensart aus dem aktenzeichen (das sind immer drei buchstaben)

Schauen wir uns den Datensatz an:

head(daten_bverfg)
## readtext object consisting of 6 documents and 6 docvars.
## # Description: df[,8] [6 × 8]
##   doc_id           text              date docket_nr  year month senat proceeding
##   <chr>            <chr>            <int> <chr>     <dbl> <dbl> <dbl> <chr>     
## 1 20150108_2bvr24… "\"\n      \n…  2.02e7 2bvr2419…  2015     1     2 bvr       
## 2 20150112_2bvq00… "\"\n      \n…  2.02e7 2bvq0053…  2015     1     2 bvq       
## 3 20150113_1bvr04… "\"\n      \n…  2.02e7 1bvr0472…  2015     1     1 bvr       
## 4 20150113_1bvr33… "\"\n      \n…  2.02e7 1bvr3323…  2015     1     1 bvr       
## 5 20150113_2bve00… "\"\n      \n…  2.02e7 2bve0001…  2015     1     2 bve       
## 6 20150113_2bvr23… "\"\n      \n…  2.02e7 2bvr2395…  2015     1     2 bvr

4. Einen Korpus erstellen

Jetzt haben wir einen Datensatz mit insgeamt sieben Variablen, wovon mindestens drei auch für weitere inhaltlische Analysen verwendet werden können. Um eine Textanalyse durchführen zu können müssen wir einen Textkorpus erstellen. Hierfür verwenden wir die quanteda-Funktion corpus:

gerichts_korpus <- corpus(daten_bverfg,docid_field = "doc_id") # mit docid_field definieren wir die Identifikationsnummer eines jeden Texts
gerichts_korpus
## Corpus consisting of 1,616 documents and 6 docvars.
## 20150108_2bvr241913.txt :
## "                                                            ..."
## 
## 20150112_2bvq005314.txt :
## "                                                            ..."
## 
## 20150113_1bvr047214.txt :
## "                                                            ..."
## 
## 20150113_1bvr332314.txt :
## "                                                            ..."
## 
## 20150113_2bve000113.txt :
## "                                                            ..."
## 
## 20150113_2bvr239514.txt :
## "                                                            ..."
## 
## [ reached max_ndoc ... 1,610 more documents ]

Mit den quanteda-Funktionen ndoc, ntoken, ntype und nsentence können wir die Anzahl der Dokumente, Tokens, Types und Sätze identifizieren. Da wir als R-Nutzerinnen aber faul sind, nutzen wir die summary-Funktion um diese Statistiken mit nur einem Befehl zu erstellen.

korpus_stats <- summary(gerichts_korpus, n = 1000000) # Das Funktionsargument n = 1000000 wird hier nur deshalb verwendet, weil die Funktion summary per default nur maximal 100 Texte zusammenfasst.
head(korpus_stats)
##                      Text Types Tokens Sentences     date  docket_nr year month
## 1 20150108_2bvr241913.txt   703   1849       103 20150108 2bvr241913 2015     1
## 2 20150112_2bvq005314.txt   149    268        15 20150112 2bvq005314 2015     1
## 3 20150113_1bvr047214.txt   118    177         8 20150113 1bvr047214 2015     1
## 4 20150113_1bvr332314.txt   210    426        26 20150113 1bvr332314 2015     1
## 5 20150113_2bve000113.txt   786   2133       104 20150113 2bve000113 2015     1
## 6 20150113_2bvr239514.txt   356    976        57 20150113 2bvr239514 2015     1
##   senat proceeding
## 1     2        bvr
## 2     2        bvq
## 3     1        bvr
## 4     1        bvr
## 5     2        bve
## 6     2        bvr

Diese Variablen fügen wir jetzt unserem ursprünglichen Metadatensatz hinzu:

daten_bverfg$types <- korpus_stats$Types
daten_bverfg$tokens <- korpus_stats$Tokens
daten_bverfg$sentences <- korpus_stats$Sentences

Achtung: Die Unterscheidung zwischen Token und Type wird in der Ontologie vorgenommen, um zwischen einem einzelnen Vorkommnis und dem allgemeinen Vorkommnistyp zu unterscheiden. Ein Beispiel: Auf die Frage, wie viele Ziffern sich in der Reihe 2200999 befinden, gibt es zwei korrekte Antworten. Zählt man die Token (die Vorkommnisse), so befinden sich sieben Ziffern in der Reihe. Zählt man hingegen die Typen, so sind es nur drei, '2', '0' und '9' (Quelle: Wikipedia, "Token und Type").

4.1. Allgemeine Statistiken zum Korpus

Die oben gespeicherten Korpusstatistiken lassen sich natürlich auch visualisieren. So bekommt man relativ schnell einen guten Einblick in dessen Beschaffenheit. Im folgenden werden wir die Anzahl der Token, der Typen und die Sätze pro Gerichtsentscheidungen visualiseren. Am Ende plotten wir noch das Verhältnis von Typen zu Tokens.

Die Basis aller jetzt folgenden Plots ist unser Datensatz daten_bverfg. Um die Plots übersichtlicher zu gestalten, werden wir nur Entscheidungen vom Juni 2019 plotten (das subsetting mit filter() haben wir ja bereits gelernt). Natürlich nutzen wir wieder ggplot2.

bverfg_19_06 <- filter(daten_bverfg, year == 2019 & month == 6)
ggplot(bverfg_19_06, aes(doc_id, tokens, group = 1)) + 
  geom_line() + 
  geom_point() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + 
  ggtitle("Tokens pro Entscheidung")

ggplot(bverfg_19_06, aes(doc_id, types, group = 1)) + 
  geom_line() + 
  geom_point() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + 
  ggtitle("Types pro Entscheidung")

ggplot(bverfg_19_06, aes(doc_id, sentences, group = 1)) + 
  geom_line() + 
  geom_point() + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + 
  ggtitle("Sätze pro Entscheidung")

ggplot(bverfg_19_06, aes(doc_id, types, group = 1, label = docket_nr)) + 
  geom_smooth(method = "lm", se = FALSE) + 
  geom_text(check_overlap = T) + 
  ggtitle("Typ-Token-Relation pro Entscheidung")

Diese Graphen sind nicht wirklich informativ. Wir sehen lediglich, dass der Entscheidungstext zu 1 BvR 587/17 deutlich länger ist als die anderen Entscheidungstexte. Natürlich hat das Einfluss auf alle drei Ebenen (Tokens, Types, Sätze). Auch die Typ-Token-Relation ist wenig Aussagekräftig (Über die Typ-Token-Relation lassen sich Rückschlüsse auf die Informationsdichte von Texten ziehen).

4.2. Arbeiten mit dem Korpus

In quanteda lassen sich Korpora leicht samplen und umformen. Auch lassen sich wichtige Einblicke in die Texte gewinnen. Der folgenden Aufruf zeigt die ersten 1000 Zeichen der ersten Entscheidung in unserem Korpus:

str_sub(gerichts_korpus[1], start = 1, end = 1000)
## [1] "\n      \n\n                    \n                    \n                    \n                         \n                        \n                            \n                                \n                                    BUNDESVERFASSUNGSGERICHT \n                                 \n                             \n                         \n                     \n                    \n                         \n                        \n                            \n                                 - 2 BvR 2419/13 - \n                             \n                         \n                     \n                    \n                         \n                        \n                            \n                                 In dem Verfahren  über  die Verfassungsbeschwerde \n                             \n                         \n                     \n                    \n                         \n                        \n                             \n                            \n                "

Hinweis: Jeder Text lässt sich mittels seiner Indizierung identifizieren (gerichts_korpus[1]). Der Nachteil ist aber, dass ihr, sobald ihr einen speziellen Text sehen wollt genau wissen müsst an welcher Stelle dieser im Korpus zu finden ist.

Mit der Funktion corpus_reshape könnt ihr euren Korpus umformen. Im Beispiel unten werden wir aus jedem Satz ein Dokument machen (optional kann man auch Absätze auswählen). Solche Satz-Korpora sind z.B. bei Sentimentanalysen wichtig.

gerichts_saetze <- corpus_reshape(gerichts_korpus, to = "sentences") # welche Argumente ihr mit "to =" definieren könnt findet ihr mit ?corpus_reshape heraus
gerichts_saetze[15]
## Corpus consisting of 1 document and 6 docvars.
## 20150108_2bvr241913.txt :
## "M. und verneinte auch in diesem Fall die medizinische Notwen..."

Mit der Funktion corpus_sample() könnt ihr ein zufälliges Sample aus eurem Korpus ziehen:

zufallssatz <- corpus_sample(gerichts_saetze, size = 1)
zufallssatz
## Corpus consisting of 1 document and 6 docvars.
## 20160419_1bvr330913.txt :
## "Die vom Gesetzgeber gewählte Lösung, kein isoliertes Abstamm..."

Schließlich lassen sich Korpora mithilfe von corpus_segment() nach bestimmten Kriterien aufspalten. Probiert es mal aus!

Bis hierher habt ihr jetzt gelernt, wie man Texte einliest, relevante Variablen und einen Korpus erstellt. An den gezeigten Beispielen wird aber auch deutlich, dass wir an den Entscheidungstexten noch vieles verändern müssen, damit wir eine gute Grundlage für unsere Textanalyse haben. Zum Beispiel müssen wir die Sonderzeichen, die Leerzeichen und Absätze (und damit vor allem die \n!) und die nummerischen Elemente entfernen. Diese Aspekte werden im kommenden Tutorial besprochen.