1. Einleitung

Viele QTA-Studien analysieren den "Ton" bzw. Sentiment eines Textes. Eine Sentimentanalyse ist daran interessiert, den emotionalen Inhalt eines Text, Facebookpost oder eines Tweet zu analysieren. Ist ein Text positiv oder negativ in Bezug auf ein bestimmtes Thema (z.B. Pro- oder Anti-Brexit)? Ist ein Akteur für oder gegen einen bestimmten politischen Vorschlag?

In diesem Tutorial werden wir die Wörterbuchmethode (Dictionary approach) verwenden und Sentiment-Analysen durchführen. Diese Methode hat sich als sehr effizient herausgestellt, z.B. um zu entscheiden, ob eine Rezension auf Amazon positiv oder negativ ist. Sentiment-Analysen sind jedoch nicht für jedes Forschungsproblem geeignet. Zeitungsartikel z.B. können verschiedene und sich wiedersprechende Aussagen und Tonlagen zu einem Thema beinhalten. Die Wahl der Methode ist also immer kontextabhängig zu treffen.

Ein wichtiger Teil dieses Tutorials wird sich mit der Validierung unseres Wörterbuches befassen. Damit möchte ich euch zeigen, wie wichtig es ist, die verwendeten Methoden und gefunden Ergebnisse auf ihre Gültigkeit hin zu überprüfen. Lexika bzw. Wörterbücher zum Beispiel sollten immer an die jeweilige Aufgabe angepasst werden, indem der Kontext der zu interessierenden Kernkonzepte/-wörter untersucht wird.

2. Eine Dokument-Feature-Matrix (DFM) erstellen

Die Grundlage für eine dictionary-Analyse ist die Dokument-Feature-Matrix (DFM) bzw. Dokument-Term-Matrix (DTM).

Für dieses Tutorial werden wir nicht den Gerichtskorpus verwenden, da es noch kein etabliertes Wörterbuch für Verfassungsgerichtstexte gibt (Wink mit dem Zaunpfahl: Vielleicht ein gutes Thema für eine Hausarbeit?).

Als Alternative werden wir die bereits bekannten Antrittsreden der US Präsidenten aus dem quanteda-Paket verwenden (eine Liste aller in quanteda gespeicherten Korpora findet ihr hier: quanteda.corpora).

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


inaug_speeches <- data_corpus_inaugural 

Jetzt erstellen wir eine DTM mittels einer Pipeline:

library(tidyverse) 

dtm_speeches <-  inaug_speeches %>% 
  tokens(remove_numbers = TRUE, remove_punct = TRUE,remove_symbols = TRUE) %>% 
  tokens_remove(pattern = stopwords("english")) %>% 
  tokens_tolower() %>%
  dfm() # wir entfernen alle Füllwörter, die Interpunktion, Symbole, Zahlen
dtm_speeches
## Document-feature matrix of: 59 documents, 9,212 features (92.66% sparse) and 4 docvars.
##                  features
## docs              fellow-citizens senate house representatives among
##   1789-Washington               1      1     2               2     1
##   1793-Washington               0      0     0               0     0
##   1797-Adams                    3      1     0               2     4
##   1801-Jefferson                2      0     0               0     1
##   1805-Jefferson                0      0     0               0     7
##   1809-Madison                  1      0     0               0     0
##                  features
## docs              vicissitudes incident life event filled
##   1789-Washington            1        1    1     2      1
##   1793-Washington            0        0    0     0      0
##   1797-Adams                 0        0    2     0      0
##   1801-Jefferson             0        0    1     0      0
##   1805-Jefferson             0        0    2     0      0
##   1809-Madison               0        0    1     0      1
## [ reached max_ndoc ... 53 more documents, reached max_nfeat ... 9,202 more features ]

Ohne Pipeline-Operator sieht der Code wie folgt aus:

inaug_speeches_tokens <- tokens(inaug_speeches, remove_numbers = TRUE, remove_punct = TRUE,remove_symbols = TRUE)

inaug_speeches_tokens <- tokens_remove(inaug_speeches_tokens, pattern = stopwords("english"))
inaug_speeches_tokens <- tokens_tolower(inaug_speeches_tokens)

dtm_speeches <- dfm(inaug_speeches_tokens)

dtm_speeches
## Document-feature matrix of: 59 documents, 9,212 features (92.66% sparse) and 4 docvars.
##                  features
## docs              fellow-citizens senate house representatives among
##   1789-Washington               1      1     2               2     1
##   1793-Washington               0      0     0               0     0
##   1797-Adams                    3      1     0               2     4
##   1801-Jefferson                2      0     0               0     1
##   1805-Jefferson                0      0     0               0     7
##   1809-Madison                  1      0     0               0     0
##                  features
## docs              vicissitudes incident life event filled
##   1789-Washington            1        1    1     2      1
##   1793-Washington            0        0    0     0      0
##   1797-Adams                 0        0    2     0      0
##   1801-Jefferson             0        0    1     0      0
##   1805-Jefferson             0        0    2     0      0
##   1809-Madison               0        0    1     0      1
## [ reached max_ndoc ... 53 more documents, reached max_nfeat ... 9,202 more features ]

Mittels einer einfachen Word cloud können wir unsere DTM inspizieren:

textplot_wordcloud(dtm_speeches, max_words=100)

3. Sentimentanalyse mit dem Dictionary-Approach

3.1 Das Wörtbuch

Um eine Sentimentanalyse mit einem Wörterbuch durchzuführen, können wir die dictionary-Funktion von quanteda verwenden. Natürlich benötigen wir aber zu allererst ein Wörterbuch. Hierfür gibt es verschiedene Wege.

  1. Entweder ihr erstellt ein eigenes Lexika, mit eigenen Kategorien und Themen. Der Vorteil ist, dass ihr ein Werkzeug kreieren könnt, welches passgenau auf euer spezielles Forschungsinteresse und Theoriekonzept passt. Der Nachteil ist, dass das Erstellen eines validen und reliablen Wörterbuches sehr arbeitsintensiv ist.

  2. Oder ihr verwendet bereits bestehende Wörterbücher. Im Internet gibt es eine Vielzahl an unterschiedlichen Lexika die ihr herunterladen könnt. Zum Beispiel stellt Johann Gründl im Kontext seines Papers Populist ideas on social media: A dictionary-based measurement of populist communication ein eigenes Wörterbuch vor und zum herunterladen bereit. Oder ihr ladet euch das Wörterbuch für Sentimentanalysen in deutscher Sprache von Haselmayer/Jenny (2020) herunter.

  3. Alternativ stellen auch etliche R-Pakete Wörterbücher bereit. Zum Beispiel enthält das Paket SentimentAnalysis drei verschiedene Wörterbücher: 1) DictionaryGI ist ein allgemeines Sentiment-Wörterbuch das auf The General Inquirer basiert, und 2) DictionaryHE und 3) DictionaryLM sind Wörterbücher mit einem speziellem Fokus auf Finanz- und Wirtschaftpolitik. Das Paket qdapDictionaries enthält ebenfalls eine Reihe von Wörterbüchern, darunter eine Liste von positive.words und negative.words und spezifische Listen für strong.words, weak.words, power.words und submit.words aus dem Harvard IV-Wörterbuch.

Ihr könnt die Wörterbücher von jedem Paket einsehen indem ihr sie installiert, in R ladet und die help-Page nutzt:

library(SentimentAnalysis)
?DictionaryGI
names(DictionaryGI)
## [1] "negative" "positive"
head(DictionaryGI$negative)
## [1] "abandon"     "abandonment" "abate"       "abdicate"    "abhor"      
## [6] "abject"
library(qdapDictionaries)
?positive.words
head(positive.words)
## [1] "a plus"     "abound"     "abounds"    "abundance"  "abundant"  
## [6] "accessable"

Natürlich könnt ihr auch Wörterbücher als CSV herunterladen. Zum Beispiel gibt es das VADER lexicon, welches speziell für Social Media Analysen erstellt wurde und Wörter wie "lol", "rofl" und "meh" enthält. Das Lexikon könnt ihr auf der Github-Seite von C.J. Hutto herunterladen. Dafür nutzen wir die Schritte, die ihr bereits aus dem tidyR Tutorial kennt:

url <-  "https://raw.githubusercontent.com/cjhutto/vaderSentiment/master/vaderSentiment/vader_lexicon.txt" # Die Warnungen von R können hier übersehen werden. 
vader <-  read_delim(url, col_names=c("word","sentiment", "details"),  col_types="cdc",  delim="\t") # delim = "\t" definiert wie wir die Informationen aus der Tablle separieren wollen (da die Datei eine Tabelle ist, müssen wir \t nutzen)
head(vader)
## # A tibble: 6 x 3
##   word     sentiment details
##   <chr>        <dbl> <chr>  
## 1 $:            -1.5 0.80623
## 2 %)            -0.4 1.0198 
## 3 %-)           -1.5 1.43178
## 4 &-:           -0.4 1.42829
## 5 &:            -0.7 0.64031
## 6 ( '}{' )       1.6 0.66332

Die Auswahl des Wörterbuchs hängt immer von der eigenen Forschungsfrage ab. Zum Beispiel benötigt eine Analyse von bestimmten Politikfeldern in Pressemitteilungen von politischen Parteien ein politikfeldspezifisches Wörterbuch wie es zum Beispiel Langer/Sagarzazu (2017) für ihre Analyse zur britischen Finanzpolitik entwickelt haben. Egal für welches Wörterbuch ihr euch entscheidet, oder ob ihr (theoriegeleitet) ein eigenes Wörterbuch entwickelt, die zentrale Aufgabe bei dictionary-Ansätzen ist die Kontrolle, ob das Wörterbuch tatsächlich das misst, was es messen soll!

3.2 Ein Wörterbuch in quanteda nutzen und erstellen

Wie bereits angedeutet können die oben aufgeführten Wörterbücher mit der dictionary-Funktion des quanteda-Pakets angewendet werden.

GI_dict <- dictionary(DictionaryGI) # Wir verwenden hier das Wörterbuch "DictionaryGI" aus dem SentimentAnalysis-Paket (welches wir ja bereits installiert und in `R` geladen haben)
GI_dict
## Dictionary object with 2 key entries.
## - [negative]:
##   - abandon, abandonment, abate, abdicate, abhor, abject, abnormal, abolish, abominable, abrasive, abrupt, abscond, absence, absent, absent-minded, absentee, absurd, absurdity, abuse, abyss [ ... and 1,985 more ]
## - [positive]:
##   - abide, ability, able, abound, absolve, absorbent, absorption, abundance, abundant, accede, accentuate, accept, acceptable, acceptance, accessible, accession, acclaim, acclamation, accolade, accommodate [ ... and 1,617 more ]

Für die Wortlisten könnt ihr z. B. ein Wörterbuch mit definierten Kategorien erstellen:

HL_dict <-  dictionary(list(positive=positive.words, negative=negation.words)) # Wir verwendetn hier die zwei Wörterbücher "positive.words" und "negative.words" aus dem qdapDictionaries-Paket und definieren die Kategorien "positive" und "negative"
HL_dict
## Dictionary object with 2 key entries.
## - [positive]:
##   - a plus, abound, abounds, abundance, abundant, accessable, accessible, acclaim, acclaimed, acclamation, accolade, accolades, accommodative, accomodative, accomplish, accomplished, accomplishment, accomplishments, accurate, accurately [ ... and 1,983 more ]
## - [negative]:
##   - ain't, aren't, can't, couldn't, didn't, doesn't, don't, hasn't, isn't, mightn't, mustn't, neither, never, no, nobody, nor, not, shan't, shouldn't, wasn't [ ... and 3 more ]

Eine weitere Möglichkeit für das Erstelles eines Wörterbuchs ist die Nutzung eines Sentiment-Wertes. Zum Beispiel der Sentiment-Wert des VADER lexicon misst wie positiv oder negativ ein Wort bzw. Emojicon zu bewerten ist. Werte über 0.05 gelten als positiv und unter -0,05 als negativ. Mit diesem Wissen können wir unter zu Hilfe nahme von logischen Operatoren ein eigenes Wörterbuch erstellen:

vader_pos <-  vader$word[vader$sentiment >= 0.05] # alle Wörter die einen Sentiment score über 0.05 haben werden in dem neuen Objekt "vader_pos" gespeichert
vader_neut <-  vader$word[vader$sentiment > -0.05 & vader$sentiment < 0.05]
vader_neg <-  vader$word[vader$sentiment <= -0.05]
Vader_dict <-  dictionary(list(positive=vader_pos, neutral=vader_neut, negative=vader_neg))

Vader_dict
## Dictionary object with 3 key entries.
## - [positive]:
##   - ( '}{' ), ('-:, (':, ((-:, (*, (-*, (-:, (-:0, (-:o, (-:o, (-:|>*, (-;, (-;|, (8, (:, (:0, (:o, (:o, (;, (;< [ ... and 3,317 more ]
## - [neutral]:
## - [negative]:
##   - $:, %), %-), &-:, &:, (%, (-%, (-:<, (-:{, (:<, )':, )-':, )-:, )-:<, )-:{, ):, ):<, ):{, );<, .-: [ ... and 4,151 more ]

Ein weitere Möglichkeit eine Analyse mit einem Wörterbuch durchzuführen ist das Erstellen eines eigenen Wörterbuchs. Hierbei müsst ihr theoriegeleitet vorgehen und die Reliabilität und Validität eures Wörterbuches stets prüfen.

Nehmen wir an, dass wir beispielsweise untersuchen wollen wie Kongressabgeordnete der republikanischen und der demokratischen Partei in Pressemitteilungen die Diskussion um die Einführung eines Mindestlohns framen. Dabei gehen wir davon aus, dass republikanische Abgeordnete eher wirtschaftsfreundlich argumentieren und demokratische Abgeordnete eher wohlfahrtsstaatlich. Der Einfachheit halber legen wir im Beispiel kein Werte für die Wörter fest (kontextabhängig müsst ihr die (theoriegeleitete!!) Entscheidung treffen, ob manche Wörter ein größeres Gewicht haben als andere). Wir erstellen unser eigenes Wörterbuch wie folgt:

# Definition der Wörter die wir durch die bestehende Forschung als relevant für das Thema Mindeslohn identifiziert haben
wordsReps <- c("tax relief", "small business*","job*","creat*","cost*","grow*") # der * erlaubt uns alle Wörter zu inkludieren, welche mit "job", "creat" etc, beginnen
wordsDems <- c("poverty", "fair*","famil*","hard work*","low income","protect*")

MinWage_dict <-  dictionary(list(Republicans=wordsReps, Democrats=wordsDems))

MinWage_dict
## Dictionary object with 2 key entries.
## - [Republicans]:
##   - tax relief, small business*, job*, creat*, cost*, grow*
## - [Democrats]:
##   - poverty, fair*, famil*, hard work*, low income, protect*

Ihr seht: Nicht das technische Erstellen eines Wörterbuchs in R ist schwierig, sondern die richtigen Wörter zu finden.

3.2 Anwendung des Wörterbuchs

Zu Beginn dieses Tutorials haben wir eine DTM der Antrittsreden der US Präsidenten erstellt. In einem zweiten Schritt haben wir mehrere Wörterbücher heruntergeladen, von denen wir jetzt das GI_dict für unsere Sentiment-Analyse der Antrittsreden verwenden.

Die Anwendung des Wörterbuchs ist einfach: 1) wir nutzen die Funktion dfm_lookup um das Wörterbuch auf die DTM anzuwenden, 2) wir konvertieren das Ergebnis in einen tibble-Datensatz mit dem Namen "result". Beide Schritte verbinden wir in einer Pipeline. Der zweite Schritt ist rein optional, aber er erleichtert die nachfolgenden Schritte.

result <-  dtm_speeches %>% dfm_lookup(GI_dict) %>% convert(to = "data.frame") %>% as_tibble
result
## # A tibble: 59 x 3
##    doc_id          negative positive
##    <chr>              <dbl>    <dbl>
##  1 1789-Washington       43      119
##  2 1793-Washington        1        7
##  3 1797-Adams            75      234
##  4 1801-Jefferson        57      178
##  5 1805-Jefferson        71      171
##  6 1809-Madison          39      117
##  7 1813-Madison          54       93
##  8 1817-Monroe          110      311
##  9 1821-Monroe          120      316
## 10 1825-Adams            78      232
## # … with 49 more rows

Jetzt fügen wir die Textlänge hinzu, damit wir die Ergebnisse für die Länge der Dokumente normalisieren können. Hierfür verwenden die Funktion ntoken und legen damit eine neue Variable mit dem Namen "length" an:

result <-  result %>% mutate(length=ntoken(dtm_speeches))

Anschließend können wir einen Gesamt-Sentiment-Score errechnen. Dadurch brauchen wir nicht mit zwei Werten hantieren. Für diese Aufgabe gibt es verschiedene Möglichkeiten: die Anzahl der negativen Sentiments mit der Anzahl der positiven Sentiments zu substrahieren und entweder 1) durch die Gesamtzahl der Sentiments oder 2) durch die Textlänge zu dividieren:

result <-  result %>% mutate(sentiment1=(positive - negative) / (positive + negative)) # 1. Möglichkeit

result <- result %>% mutate(sentiment2=(positive - negative) / length) #  2. Möglichkeit

result
## # A tibble: 59 x 6
##    doc_id          negative positive length sentiment1 sentiment2
##    <chr>              <dbl>    <dbl>  <int>      <dbl>      <dbl>
##  1 1789-Washington       43      119    652      0.469     0.117 
##  2 1793-Washington        1        7     62      0.75      0.0968
##  3 1797-Adams            75      234   1070      0.515     0.149 
##  4 1801-Jefferson        57      178    813      0.515     0.149 
##  5 1805-Jefferson        71      171   1011      0.413     0.0989
##  6 1809-Madison          39      117    526      0.5       0.148 
##  7 1813-Madison          54       93    546      0.265     0.0714
##  8 1817-Monroe          110      311   1561      0.477     0.129 
##  9 1821-Monroe          120      316   2010      0.450     0.0975
## 10 1825-Adams            78      232   1365      0.497     0.113 
## # … with 49 more rows

Diese zwei Scores können als Maßzahlen für die Stimmung pro Dokument definiert werden. Für eine substanzielle Analyse können wir jetzt die Scores mit den Metadaten/Docvars der DTM verknüpfen und z. B. die Stimmung pro Antrittsrede oder über Zeit berechnen.

3.3 Visualisierung

Natürlich können wir auch eine schnelle Visualisierung eines der errechneten Sentiment-Scores in den Antrittsreden der US-Präsidenten realisieren. Hierfür nutzen wir das ggplot2-Paket:

library(ggplot2)

ggplot(result, aes(doc_id, sentiment1, group = 1)) + # Die Reden bilden die x-Achse und der Subjektivitätsmaß bildet die y-Achse
  geom_line(size = 1) +
  theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) +  # Hier definieren wir das Aussehen der Lables 
  ggtitle("Sentiment/Ton in den Antrittsreden der US-Präsidenten") # Titel

Wollen wir die positiven und negativen Wörter getrennt visualisieren (also wie viele positive und wie viele negative Wörter eine Antrittsrede enthält), dann können wir beispielhaft wie folgt vorgehen:

dtm_speeches <- dfm_lookup(dtm_speeches,GI_dict) # Hier wiederholen wir die Anwendung des Wörterbuchs

sentiment <- convert(dtm_speeches, "data.frame") %>%
  gather(positive, negative, key = "Polarität", value = "Wörter") %>% 
  mutate(doc_id = as_factor(doc_id)) %>% 
  rename(Reden = doc_id)  # Wir erstellen einen eigenen Datensatz mit dem Namen "Sentiment" und generieren neue Variablen (Polarität, Wörter) bzw. benennen Variablen um (doc_id wird zu "Reden")
head(sentiment)
##             Reden Polarität Wörter
## 1 1789-Washington  positive    119
## 2 1793-Washington  positive      7
## 3      1797-Adams  positive    234
## 4  1801-Jefferson  positive    178
## 5  1805-Jefferson  positive    171
## 6    1809-Madison  positive    117
ggplot(sentiment, aes(Reden, 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 den Antrittsreden der US-Präsidenten")

4. Validierung

Die Validierung eines Wörterbuchs ist eine zentrale Aufagbe für jede Sentimentanalyse.

Um ein Maß für die Validität eines Wörterbuches zu erhalten, müssen wir eine Zufallstichprobe von Dokumenten manuell kodieren und die Kodierung mit den Eregbnissen des Wörterbuchs vergleichen. Manuelle Kodierungen gelten als Goldstandard, an dem sich automatische Kodierungen messen lassen müssen. Die Validierung einer jeden Textanalysemethode muss im Methodenabschnitt einer Forschungsarbeit berichtet und diskutiert werden.

Wie groß eine Zufallsstichprobe für eine Validierung sein sollte ist in der Wissenschaft umstritten. Hier gilt jedoch: je größer, desto genauer die Validierung. Das größte Problem bei manueller Kodierung ist die Ressourcenknappheit, weshalb kontextspezifisch ein gutes und realisierbares Maß gefunden (und in der Arbeit auch stichhaltig verargumentiert) werden muss.

Um eine Zufallsstichprobe aus der ursprünglichen DTM zu ziehen, können wir die Funktion sample anwenden:

sample_speeches = sample(docnames(dtm_speeches), size=20) # wir wählen zufällig 20 Reden aus

Jetzt transformieren wir unseren Ausgangskorpus in einen Datensatz, fügen die Volltexte und Dokumentennamen hinzu. Danach filtern wir unsere zufällig ausgewählten Antrittsreden und kreieren eine csv.Datei. Diese Datei können wir anschließend für die manuelle Kodierung nutzen:

# Korpus in Datensatz umwandeln
docs <- docvars(inaug_speeches) # Dokumentenvariablen 
docs$doc_id <-  docnames(inaug_speeches) # Textnamen
docs$text <-  texts(inaug_speeches) # Volltexte der Reden

# Wir filtern unsere Zufallsstichprobe, kreieren die neue Variable "manual_coding" und schreiben dann eine csv.Datei (diese finden sich dann in dem Ordner von eurem R-Projekt oder dort wo ihr euer Skript abgespeichert habt)
docs %>% filter(document %in% sample_speeches) %>% mutate(manual_coding="") %>% write_csv("to_code.csv")

Jetzt können wir die csv.Datei in Excel/Pages (you name it) öffnen und die 20 zufällig ausgewählten Dokumente manuell kodieren. Anschließend müssen wir das Ergebnis wieder in R einlesen und mit den automatisch generierten Ergebnissen unserer Wörterbuchanalyse konbinieren:

library(readr) # wir brauchen das paket um die csv zu speichern (vorher müsst ihr vielleicht install.packages("readr") ausführen)
validation <-  read_csv("to_code.csv") %>% mutate(doc_id=as.character(document)) %>% inner_join(result) # wir kombinieren hier unsere manuell kodieren reden mit unseren automatischen ergebnissen (das result-objekt)

Nun kontrollieren wir, wie gut meine (zugegebenermaßen völlig zufällige) manuelle Kodierung mit dem automatischen Sentiment-Score übereinstimmt. Zum Beispiel konnen wir eine Korrelation berechnen:

cor.test(validation$manual_coding, validation$sentiment1)
## 
##  Pearson's product-moment correlation
## 
## data:  validation$manual_coding and validation$sentiment1
## t = 1.0177, df = 18, p-value = 0.3223
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  -0.2333447  0.6125536
## sample estimates:
##       cor 
## 0.2332628
cor.test(validation$manual_coding, validation$sentiment2)
## 
##  Pearson's product-moment correlation
## 
## data:  validation$manual_coding and validation$sentiment2
## t = 1.0446, df = 18, p-value = 0.31
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  -0.2275209  0.6163817
## sample estimates:
##       cor 
## 0.2390701

Die Korrelationen zeigen keinen signifikanten Zusammenhang zwischen meiner manuellen Kodierung und der automatischen Kodierung. Hätte ich die Dokumente ernsthaft kodiert, dann wäre das ein schlechtes Zeichen für meine Wörterbuchanalyse und es würde bedeuten, dass ich mein Wörterbuch verbessern müsste.

Wir können auch eine 'Konfusionsmatrix' berechnen, wenn wir mit der Funktion cut einen Nominalwert aus dem Sentiment erzeugen:

validation <-  validation %>% 
  mutate(sent_nom = cut(sentiment1, breaks=c(0, 0.49, 0.51, 1), labels=c("-", "0", "+")))
con_matrix <-  table(manual = validation$manual_coding, dictionary = validation$sent_nom)
con_matrix
##         dictionary
## manual   - 0 +
##   0.123  1 0 0
##   0.134  1 0 0
##   0.222  1 0 0
##   0.245  1 0 0
##   0.31   0 0 1
##   0.312  1 0 0
##   0.45   1 0 0
##   0.457  1 0 0
##   0.5    0 0 1
##   0.523  0 0 1
##   0.5264 1 0 0
##   0.531  1 0 0
##   0.54   1 0 0
##   0.67   1 0 0
##   0.671  1 0 0
##   0.7    1 0 0
##   0.767  1 0 0
##   0.786  1 0 0
##   0.86   0 0 1
##   0.99   1 0 0

Die Konfusionsmatrix zeigt die Anzahl der Fehler in jeder Kategorie. Zum Beispiel wurden 9 Dokumente durch den Computer als negativ ("-") klassifiziert, aber manuell von mir als positiv kodiert. Die Gesamtgenauigkeit ist die Summe der Diagonalen der Matrix (1+0+0) geteilt durch den gesamten Stichprobenumfang (20), also sehr niedrige 0.05%.

sum(diag(con_matrix)) / sum(con_matrix)
## [1] 0.05

Die Ergebnisse würden bedeuten, dass unser Wörterbuch für die zu analysierenden Texte nicht funktioniert und das eine Verbesserung des Wörterbüches notwendig ist. Da ich die manuelle Kodierung jedoch vollkommen zufällig gemacht habe, sind die gefunden Validierungsergebnisse nicht aussagekräftig. Sie dienen lediglich als Anschauungsmaterial. Bitte kodiert eure 20 Texte selber und schaut dann inwiefern sich die Validationsmaße verändern!

5. Optimierung

Um ein Wörterbuch zu optimieren, ist es wichtig die Wörter im Wörterbuch zu identifizieren, die den größten Einfluss auf die Ergebnisse haben. Für diese Aufgabe eignet sich die textstat_frequency-Funktion aus quanteda in Verbindung mit filter-Funktion.

freqs <-  textstat_frequency(dtm_speeches)
freqs %>% as_tibble() %>% filter(feature %in% HL_dict$positive)
## # A tibble: 1 x 5
##   feature  frequency  rank docfreq group
##   <chr>        <dbl> <dbl>   <dbl> <chr>
## 1 positive     11794     1      59 all

Es zeigt sich, dass die am häufigsten auftretenden "positiven" Wörter "great" und "peace" sind. Natürlich ist es möglich, dass diese Wörter tatsächlich in einem positiven Sinne verwendet wurden ("this great country"), aber es ist ebenso möglich, dass sie neutral oder aber auch negativ verwendet wurden.

Um das zu kontrollieren bietet sich vor allem die "Key-Word-in-Context"-Methode an. Bei dieser Methode definieren wir ein Zielwort und lassen uns eine Textsequenz vor und nach diesem Wort anzeigen (Die Länge der Sequenz ist optimierbar. Wie? Das könnt ihr mittels der help-Page herausfinden ?kwic):

inaug_speeches_toks <- tokens(inaug_speeches)
head(kwic(inaug_speeches_toks, "great"))
## Keyword-in-context with 6 matches.                                                                    
##  [1789-Washington, 471]       tendering this homage to the | Great |
##  [1789-Washington, 735]               than to refer to the | great |
##  [1789-Washington, 862]           ought to watch over this | great |
##       [1797-Adams, 415] and insurrection, threatening some | great |
##       [1797-Adams, 553]                   , I read it with | great |
##       [1797-Adams, 602]      In its general principles and | great |
##                                         
##  Author of every public and             
##  constitutional charter under which you 
##  assemblage of communities and interests
##  national calamity. In this             
##  satisfaction, as the result            
##  outlines it was conformable to
head(kwic(inaug_speeches_toks, "peace"))
## Keyword-in-context with 6 matches.                                                                         
##       [1797-Adams, 862]                 in its effects upon the | peace |
##      [1797-Adams, 1486]          of liberty to independence and | peace |
##      [1797-Adams, 1605]         secret enemies of his country's | peace |
##      [1797-Adams, 2038] an inflexible determination to maintain | peace |
##      [1797-Adams, 2268]               all nations, and maintain | peace |
##  [1801-Jefferson, 1229]               , religious or political; | peace |
##                               
##  , order, prosperity,         
##  , to increasing wealth and   
##  . This example has been      
##  and inviolable faith with all
##  , friendship, and benevolence
##  , commerce, and honest

Daraus geht hervor, dass das Wort "peace" eher als neutraler Begriff verwendet wird und deswegen vermutlich unsere Positive/Negative-zentrierte Sentimentanalyse verzerrt. Um es aus der Liste der positiven Wörter zu entfernen, können wir die Funktion setdiff (Differenz zwischen zwei Mengen) verwenden:

positive.cleaned <- setdiff(positive.words, c("peace"))
HL_dict2 <-  dictionary(list(positive=positive.cleaned, negative=negation.words))

Schauen wir uns jetzt die nochmal die wichtigsten positiven Wörter an:

freqs_pos <- freqs %>% filter(feature %in% HL_dict2$positive)
head(freqs_pos)
##    feature frequency rank docfreq group
## 1 positive     11794    1      59   all

Die Überprüfung der Top-25-Wörter kann die Gültigkeit eines Wörterbuchs bereits gut abbilden, da diese Wörter oft einen großen Teil der Ergebnisse bestimmen.

Für weitere nützliche Hinweise und Beispiele empfehle ich euch die Internetseite von Cornelius Puschmann: http://inhaltsanalyse-mit-r.de/sentiment.html