Wie auch einige andere hier behandelte Verfahren hat die Sentimentanalyse ihre Wurzeln in der Computerlinguistik und Informatik, wird aber seit einigen Jahren auch zunehmend in den Sozialwissenschaften angewandt, um ganz unterschiedliche Texte automatisch zu klassifizieren, etwa Parlamentsdebatten, Freitextantworten in Befragungen, oder Social Media-Diskurse. Ziel der Sentimentanalyse ist die Bestimmung der Polarität eines Textes, womit gemeint ist, ob die darin zum Ausdruck gebrachten Emotionen eher positiv oder negativ sind. Dies geschieht häufig durch Wortlisten und über das Auszählen von Begriffen, die zuvor den Kategorien positiv oder negativ zugeordnet wurden. In vielen Verfahren wird das Resultat anachließend skaliert, oder es werden andere Schritte angewandt, um fehlerhafte Klassifizierungen zu vermeiden. Diese treten vor allem dann auf, wenn Negation oder Ironie verwendet werden, aber auch, wenn der Gegenstand der positiven oder negativen Ausdrücke wechselt oder unklar ist. Unsprünglich wurde die Sentimentanalyse auf Produktbewertungen auf E-Commerence-Plattformen wie Amazon.com getestet, wo diese Probleme eine relativ geringe Rolle spielen. Bei Pressetexten oder Diskursen in den sozialen Medien hingegen, ist oft schwerer zu bewerten, auf was sich eine Sentimentbewertung bezieht, oder welches Sentimenniveau etwa als ‘normal’ betrachtet werden sollte. So kommen beispielsweise in Pressetexten generell wenig Emotionen zum Ausdruck, und die negativen Begriffe überwiegen häufig, ohne dass dies notwendigerweise auf einen schlechten Zustand der Welt zurückzuführen wäre. Schließlich sollte man sich vor Augen führen, dass die Sentimentanalyse ein heuristisches Verfahren ist, dass immer auch fehlerhafte Einzelklassifikationen produziert, was aber idealerweise nicht zu stark ins Gewicht fällt, wenn man etwas Veränderungen im Sentimentverlauf über die Zeit untersucht.

Was die technische Umsetzung angeht, so gehören dieser Abschnitt und das nächste zu spezialisierten Lexika insofern zusammen, als dass es sich bei beiden Ansätzen um ganz ähnliche Verfahren handelt. In beiden Fällen wird ein Lexikon (‘sentiment/topic dictionary’) verwendet, um eine Reihe von Einzelbegriffen in einer Kategorie zusammenzufassen.

Wir verwenden in diesem Kapitel sechs unterschiedliche Sentimentlexika, davon vier in englischer Sprache und zwei für Deutsch:

Diese Lexika sind lediglich Listen von Wörtern, welche wie oben beschrieben den Kategorien postiv oder negativ zugeordnet sind. Zum Teil existiert auch noch eine dritte Kategorie neutral, desweiteren können Begriffe auch mehreren Kategorien zugeordnet sein, oder neben einer Zuordnung der Polarität auch noch eine Sentimentstärke zugeschrieben bekommen. Die hier vorgestellte Technik ist vergleichsweise primitiv, weil sie lediglich Wörter auszählt, allerdings lassen sich die Verfahren leicht noch verfeinern (vgl. bspw. diesen Beitrag von Christian Rauh zur Validierung politischer Sentiment-Lexika). Auch Verfahren die gewichten oder andere Kniffe für die Verringerung der Fehlerrate einsetzen, funktionieren so — die Sentimentanalye ist effektiv, aber auch alles andere als Hexenwerk.

Diese Lexika wenden wir folgend auf fünf Datensätze an: das bereits bekannte Sherlock Holmes—Korpus, einen Datensatz aus Tweets von Donald Trump und Hillary Clinten, einen Kommentar-Korpus aus der Diskussionsplattform Reddit, ein Korpus Schweizer Tageszeitugen mit Artikeln zur Finanzkrise, die zwischen 2007 und 2012 verfasst wurden, und schließlich noch einen Debattenkorpus des 18. Deutschen Bundestags (2013 bis 2017). Auf die Zusammenstellung der Korpora gehen wir später noch ein.

Installation und Laden der benötigten R-Bibliotheken, Laden des Korpus

Zunächst werden wieder die notwendigen Bibliotheken geladen. Neu ist die Bibliothek scales die bei der Normalisierung von Sentiment—Scores zum Einsatz kommt. Dann wird in einem zweiten Schritt das Sherlock-Korpus geladen, welches wir ja bereits zuvor nebst Metadaten im RData-Format gespeichert haben.

if(!require("quanteda")) {install.packages("quanteda"); library("quanteda")}
if(!require("readtext")) {install.packages("readtext"); library("readtext")}
if(!require("tidyverse")) {install.packages("tidyverse"); library("tidyverse")}
if(!require("scales")) {install.packages("scales"); library("scales")}
theme_set(theme_minimal())
load("daten/sherlock/sherlock.korpus.RData")

Erstellung eines Lexikons in quanteda

Wir beginnen zunächst mit einer Sentimentanalyse der Sherlock Holmes-Erzählungen, um bei einem bereits aus Kapitel 1 und 2 vertrauten Korpus zu bleiben. In einem ersten Schritt erstellen wir ein sehr einfaches Ad hoc-Lexikon aus nur sechs Begriffen, um die Struktur eines Lexikons in quanteda zu illustrieren. Dies geschieht mit dem quanteda-Befehl dictionary. Dictionary() akzeptiert eine Reihe von Standardformaten (dazu später noch mehr), aber auch Vektoren, welche die Begriffe enthalten, die eine abstrakte Kategorie operationalisieren. Beliebig viele Kategorien können so definiert und dann mit tausenden von Begriffen ‘befüllt’ werden. Auch Kategorien mit mehreren hierarchischen Ebenen sind möglich — dazu im nächsten Kapitel noch etwas mehr.

test.lexikon <- dictionary(list(posititive.begriffe = c("glück", "freude", "licht"), negative.begriffe = c("trauer", "wut", "dunkelheit")))
test.lexikon
Dictionary object with 2 key entries.
- [posititive.begriffe]:
  - glück, freude, licht
- [negative.begriffe]:
  - trauer, wut, dunkelheit

Erste Sentiment-Analyse mit dem Sherlock Holmes-Korpus

Mit diesem Lexikon können wir mit unserem englischsprachigen Korpus wenig konkretes anfangen, daher wechseln wir besser zu einem echten Sentimentlexikon. In einem zweiten Schritt lesen wir mit dem Befehl scan das Bing Liu Sentiment Lexikon in R ein. Dieses Lexikon umfasst über 6.700 englischsprachige Begriffe die in zwei einfachenn Textdateien abgelegt sind, die jeweils ein Wort je Zeile enthalten. Wir überspringen mit dem Argument skip die ersten 35 Zeilen, da diese Metainformationen über das Lexikon enthalten. Das Argument quiet verhindet die Ausgabe einer Statusmeldung.

positive.woerter.bl <- scan("lexika/bingliu-positive-words.txt", what = "char", sep = "\n", skip = 35, quiet = T)
negative.woerter.bl <- scan("lexika/bingliu-negative-words.txt", what = "char", sep = "\n", skip = 35, quiet = T)

Nun erstellen wir das Lexikon mithilfe der gerade eingelesenen Textvektoren. Dies erfolgt wieder mit der Funktion dictionary(), diesmal mit den gerade eingelesenen Vektoren als Argument.

sentiment.lexikon <- dictionary(list(positive = positive.woerter.bl, negative = negative.woerter.bl))
str(sentiment.lexikon)
Formal class 'dictionary2' [package "quanteda"] with 2 slots
  ..@ .Data:List of 2
  .. ..$ :List of 1
  .. .. ..$ : chr [1:2006] "a+" "abound" "abounds" "abundance" ...
  .. ..$ :List of 1
  .. .. ..$ : chr [1:4783] "2-faced" "2-faces" "abnormal" "abolish" ...
  ..@ meta :List of 3
  .. ..$ system:List of 5
  .. .. ..$ package-version:Classes 'package_version', 'numeric_version'  hidden list of 1
  .. .. .. ..$ : int [1:3] 2 1 2
  .. .. ..$ r-version      :Classes 'R_system_version', 'package_version', 'numeric_version'  hidden list of 1
  .. .. .. ..$ : int [1:3] 4 0 2
  .. .. ..$ system         : Named chr [1:3] "Darwin" "x86_64" "cp"
  .. .. .. ..- attr(*, "names")= chr [1:3] "sysname" "machine" "user"
  .. .. ..$ directory      : chr "/Users/cp/Documents/GitHub/inhaltsanalyse-mit-r.de"
  .. .. ..$ created        : Date[1:1], format: "2021-01-23"
  .. ..$ object:List of 2
  .. .. ..$ valuetype: chr "glob"
  .. .. ..$ separator: chr " "
  .. ..$ user  : list()

Wie man sieht, sind nun mehrere tausend Begriffe den beiden Kategorien des Lexikons zugeordnet worden. Jetzt können wir eine DFM berechnen, welche das erstellte Lexikon auf das Korpus anwendet.

meine.dfm.sentiment <- dfm(korpus, dictionary = sentiment.lexikon)
meine.dfm.sentiment
Document-feature matrix of: 12 documents, 2 features (0.0% sparse) and 1 docvar.
                              features
docs                           positive negative
  A Scandal in Bohemia              245      201
  The Red-headed League             272      216
  A Case of Identity                198      194
  The Boscombe Valley Mystery       194      293
  The Five Orange Pips              148      213
  The Man with the Twisted Lip      191      297
[ reached max_ndoc ... 6 more documents ]

Was ist geschehen? Alle tatsächlich vorkommenden Nennungen der rund 6.700 im Bing Liu-Lexikon enthaltenen Begriffe in den zwölf Sherlock Holmes—Romanen sind jeweils durch die ihnen zugeordnete Kategorie ersetzt worden. Sämtliche Begriffe, die nicht im Lexikon vorkommen, fallen dabei einfach weg. Dadurch bleibt eine Tabelle zurück, die nur noch zwei Spalten enthält — die Summe aller positiven und negativen Begriffe pro Roman. Wir werden darauf später noch im Detail zu sprechen kommen, aber vielleicht haben Sie schon bemerkt, dass mittels dictionary die Spalten einer DFM zusammengefasst werden (also die Wörter), während das Argument group der Funktion dfm() die Zeilen zusammenfasst (also die Texte). Diese dimensionale Reduzierung gehört zu den nützlichsten Eigenschaften von quanteda.

Das folgende Plot zeigt die Sentiment-Verteilung in den zwölf Sherlock Holmes-Erzählungen.

sentiment <- convert(meine.dfm.sentiment, "data.frame") %>%
  gather(positive, negative, key = "Polarität", value = "Wörter") %>% 
  mutate(doc_id = as_factor(doc_id)) %>% 
  rename(Roman = doc_id)
ggplot(sentiment, aes(Roman, Wörter, colour = Polarität, group = Polarität)) + 
  geom_line(size = 1) + 
  scale_colour_brewer(palette = "Set1") + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + 
  ggtitle("Sentiment-Scores in zwölf Sherlock Holmes-Romanen")

Gewichtung von Sentiment-Scores

Erinnert man sich an die im vorherigen Kapitel adressierten Probleme absoluter Wortfrequenzen, so möchte man vielleicht lieber relative Frequenzen berechnen. Das bedeutet beim Einsatz von Lexika in der Regel nicht nur, dass man das Auftreten der Lexikon-Begriffe relativ zur Gesamtwortfrequenz misst, sondern deren Anteil relativ zu einander (also das Verhältnis positiver und negativer Begriffe). Dies hat den Vorteil, dass man die große Zahl aller Begriffe, die weder positiv noch negativ sind, unberücksichtigt lassen kann, was durchaus Sinn ergibt, wenn man sich eben nur für das Sentiment interessiert.

Das folgenden Beispiel verdeutlicht dieses Vorgehen.

meine.dfm.sentiment.prop <- dfm_weight(meine.dfm.sentiment, scheme = "prop")
meine.dfm.sentiment.prop
Document-feature matrix of: 12 documents, 2 features (0.0% sparse) and 1 docvar.
                              features
docs                            positive  negative
  A Scandal in Bohemia         0.5493274 0.4506726
  The Red-headed League        0.5573770 0.4426230
  A Case of Identity           0.5051020 0.4948980
  The Boscombe Valley Mystery  0.3983573 0.6016427
  The Five Orange Pips         0.4099723 0.5900277
  The Man with the Twisted Lip 0.3913934 0.6086066
[ reached max_ndoc ... 6 more documents ]

Auch diese DFM lässt sich natürlich leicht plotten.

sentiment <- convert(meine.dfm.sentiment.prop, "data.frame") %>%
  gather(positive, negative, key = "Polarität", value = "Sentiment") %>% 
  mutate(doc_id = as_factor(doc_id)) %>% 
  rename(Roman = doc_id)
ggplot(sentiment, aes(Roman, Sentiment, colour = Polarität, group = Polarität)) + 
  geom_line(size = 1) + 
  scale_colour_brewer(palette = "Set1") + 
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + 
  ggtitle("Sentiment-Scores in zwölf Sherlock Holmes-Romanen (relativ)")