# Episode V: Es geht ans Eingemachte # Wir beginnen wie gehabt. library(tidyverse) library(tidycomm) # Als Datensatz kommen dieses Mal Tweets aus dem US-Wahlkampf 2016 zum Einsatz. # Es handelt sich um eine leicht umformatierte Datensammlung von www.inhaltsanalyse-mit-r.de # Der Datensatz besteht aus zahlreichen Tweets von Donald Trump und von Hillary Clinton. tweets <- read_csv('tweets_trump-clinton.csv') str(tweets) tweets # Wir beginnen mit einer (inzwischen sehr einfachen) Fingerübung: # Wieviele Tweets wurden im Laufe der Zeit von den KandidatInnen abgesetzt? # Dazu müssen wir allerdings auf Zeiteinheiten aggregieren, zum Beispiel einzelne Wochen. # Dafür eignet sich das Paket lubridate ganz hervorragend. # Und lubridate wurde praktischerweise mit dem tidyverse bereits mit installiert. library(lubridate) vignette('lubridate') # Der gesuchte Befehl heißt offenbar "floor_date". ?floor_date # Anschließend können wir nach KandidatIn und Woche gruppieren. # Und Tweets zählen. # Und visualisieren. # Aber das kennen wir ja schon. tweets %>% mutate(woche = floor_date(created_at, 'week')) %>% group_by(candidate, woche) %>% count() %>% ggplot(aes(x = woche, y = n, color = candidate)) + geom_line() # Schön machen sparen wir uns an der Stelle, denn auch das ist ja mittlerweile ein alter Hut: # - Achsenbeschriftungen # - bessere Farben # - Überschrift und Untertitel # - theme # Wir haben für einige dieser Tweets eine manuelle Inhaltsanalyse durchgeführt. # Dabei wurden zwei Kategorien codiert -- Positivität und Negativität. # 3 CodiererInnen haben außerdem 100 identische Tweets codiert, für die Intercoderreliabilität. # Die drei CodiererInnen haben uns jeweils eine CSV-Datei zukommen lassen. # Die können wir einlesen und jeweils eine Spalte mit einer Nummer der CodiererIn versehen. tweets_codiert_coder1 <- read_csv('tweets_trump-clinton_coded-coder1.csv') %>% mutate(coder = 1) tweets_codiert_coder2 <- read_csv('tweets_trump-clinton_coded-coder2.csv') %>% mutate(coder = 2) tweets_codiert_coder3 <- read_csv('tweets_trump-clinton_coded-coder3.csv') %>% mutate(coder = 3) # Jetzt können wir die drei mit bind_rows aneinander hängen. ?bind_rows tweets_codiert <- tweets_codiert_coder1 %>% bind_rows(tweets_codiert_coder2) %>% bind_rows(tweets_codiert_coder3) # Für die Intercoderreliabilität gibt es zahlreiche Koeffizienten. # Glücklicherweise hilft uns auch hier tidycomm ganz maßgeblich. ?test_icr tweets_codiert %>% test_icr(unit_var = id, coder_var = coder) # So richtig gut haben offenbar beide Kategorien nicht funktioniert. # Die Negativität-Kategorie erzeugte aber offenbar mehr Übereinstimmung. # Alternativ (oder ergänzend) zur manuellen Inhaltsanalyse lassen sich einige Aspekte auch automatisieren. # Dafür nutzen wir das großartige quanteda-Package (das für QUantitative ANalysis of TExtual DAta steht). # Also installieren und dann hier entsprechend laden. library(quanteda) browseVignettes('quanteda') # quanteda arbeitet mit sogenannten Korpora (Singular: Korpus). # Ein Korpus ist eine Sammlung an Texten. ?corpus tweets_corpus <- corpus(tweets, text_field = 'tweet') summary(tweets_corpus) # Types sind in unserem Fall einzigartige Begriffe (zB 1x "well" in "well earned & well deserved") # Tokens sind Wörter (oder: Begriffe im Kontext, zB 2x "well" in "well earned & well deserved") # Sentences sind, nun ja, Sätze # Im nächsten Schritt arbeiten Textanalysen mit sogenannten "Document-Feature Matrices" (DFM). # Eine DFM enthält alle Types (einzigartige Begriffe) als Spalten und alle Dokumente (Texte) als Zeilen. # In den Zellen steht die jeweilige Zahl, wie oft ein Type im jeweiligen Dokument vorkommt. tweets_dfm <- dfm(tweets_corpus) # Das dauert schon etwas länger. # Erkennbar ist das daran, dass die Konsole gerade "nicht bereit" ist (kein ">" in der letzten Zeile). # Nach einigen Sekunden (abhängig vom eigenen Computer) ist die DFM aber erstellt. # Und wie sieht die jetzt aus (Achtung: Es wird nur ein Ausschnitt gezeigt)? tweets_dfm # Aha, also "Wörter" in den Spalten und die Textbezeichnungen (text1, text2 ...) in den Zeilen. # Insgesamt 17932 Dokumente (das entspricht unseren Tweets) und 26301 Features (einzigartige Wörter). # Ganz schön viel, zumal ein Tweets ja kaum Wörter enthält (Remember: max. 140 Zeichen im Jahr 2016). # Entsprechend viele Zellen sind hier "0". # Erkennbar ist aber auch, dass etwa ":" ein "Wort" darstellt. Das wollen wir nicht. # Also nachgelesen, remove in tokens entdeckt und nochmals probiert. ?dfm ?tokens tweets_dfm <- dfm(tweets_corpus, remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE) tweets_dfm # Auf Basis der DFM können wir jetzt zahlreiche Wort- und Textmetriken berechnen. # Etwa die häufigsten Wörter. topfeatures(tweets_dfm) # Die sind aber so offensichtlich, dass wir die eigentlich loswerden könnten. # Un da stand doch was dazu auch in der dfm-Hilfe ... ?dfm tweets_dfm <- dfm(tweets_corpus, remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE, remove = stopwords('en')) topfeatures(tweets_dfm) # Wir können auch nach bestimmten Begriffen suchen. dfm_select(tweets_dfm, pattern = 'crook*') # Dabei stellen wir fest, dass Konjunktionen und Beugungen separat behandeln werden. # Das geht besser. Das Zauberwort heißt "stemming", also Wörter auf ihren Wortstamm reduzieren. # (Spätestens an dieser Stelle sei angemerkt, dass diese Analysen sprachabhängig sind.) # (quanteda kann standardmäßig englisch und ein bisschen deutsch; andere Sprachen können ergänzt werden.) ?dfm tweets_dfm <- dfm(tweets_corpus, remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE, remove = stopwords('en'), stem = TRUE) # Okay, mit neuem DFM also nochmals nach den "crook"-Versionen gesucht ... dfm_select(tweets_dfm, pattern = 'crook*') tweets_dfm # Können wir das nur für Trump machen? tweets_dfm_trump <- dfm_subset(tweets_dfm, candidate == 'Trump') dfm_select(tweets_dfm, pattern = 'crook*') # Schön, und was machen wir jetzt damit? # Wortwolken zum Beispiel!!1elf! textplot_wordcloud(tweets_dfm, max_words = 100) # Cool. Aber was hilft uns das -- abgesehen von etwas Einblick in die Daten? # Zugegeben, Spielerei. # Aber DFMs können noch mehr. Die Positivität/Negativität von vorhin zum Beispiel. # Dafür legt man für zu codierende Kategorien Wörterlisten fest. # Man bestimmt also, dass sich Negativität durch Begriffe wie "abyss", "attack", oder "awful" auszeichnet. # In der Praxis sind diese Listen natürlich sehr viel länger. # Und sie entstammen häufig Befragungen zur affektiven Reaktion der Befragten. # Man nennt diese Verfahren auch diktionärsbasierte Verfahren. # Ein solches Diktionär für englische positive und negative Begriffe stammt von Bing Liu. # Die Listen sind frei verfügbar und recht lang. diktionaer_positiv <- read_lines('dictionary_bingliu_pos.txt', skip = 34) str(diktionaer_positiv) diktionaer_negativ <- read_lines('dictionary_bingliu_neg.txt', skip = 34) str(diktionaer_negativ) # quanteda kann mit diesen Wortlisten nun direkt umgehen. ?dictionary diktionaer <- dictionary(list(positiv = diktionaer_positiv, negativ = diktionaer_negativ)) # Und sie beim Erstellen einer DFM einbeziehen. tweets_dfm_diktionaer <- dfm(tweets_corpus, remove_punct = TRUE, remove_symbols = TRUE, remove_numbers = TRUE, remove = stopwords('en'), stem = TRUE, dictionary = diktionaer) tweets_dfm_diktionaer # Huch, was ist jetzt passiert? # Nur noch 2 features? # Ah, ja, klar: Alle Wörter aus dem Positiv-Diktionär überführt in ein "positiv"-Feature! # Okay, zusammengefasst: # - Wir haben Tweets von Trump und Clinton. # - Wir haben Stoppwörter, Interpunktion, Zahlen und Symbole entfernt. # - Wir haben Wörter auf ihre Wortstämme reduziert. # - Wir haben ein Sentiment-Diktionär für positive/negative Begriffe angewendet. # Done? Done! # Okay, fast. Eine Kleinigkeit noch ... # Momentan zählen wir die absolute Häufigkeit der positiven/negativen Begriffe. # Gerade wenn mit Texten unterschiedlicher Länge gearbeitet wird (zB Nachrichtenbeiträge), # kann die absolute Zahl an Begriffen stark verzerren. # Anders ausgedrückt: # Natürlich gibt es mehr negative Wörter in 3000 Wörtern Reportage als in 150 Wörtern Meldung. # Um dem zu begegnen, lassen sich die absoluten Häufigkeiten je Textlänge gewichten. ?dfm_weight tweets_dfm_diktionaer_gewichtet <- dfm_weight(tweets_dfm_diktionaer, scheme = 'prop') tweets_dfm_diktionaer_gewichtet # Feddich! # Wie kriegen wir die gewichteten Resultate jetzt da raus? ?convert tweets_codiert <- tweets_dfm_diktionaer_gewichtet %>% convert(to = 'data.frame') %>% as_tibble() %>% bind_cols(docvars(tweets_dfm_diktionaer_gewichtet)) str(tweets_codiert) # Und jetzt können wir das Ganze auch wieder wie gehabt visualisieren. # Hier: die Negativität beider KandidatInnen über die Zeit. # Weil die Farbgebung arg verwirrend ist, wird sie hier ausnahmsweise manuell gesetzt. tweets_codiert %>% mutate(woche = floor_date(created_at, 'week')) %>% group_by(candidate, woche) %>% summarise(negativ = sum(negativ)) %>% ggplot(aes(x = woche, y = negativ, color = candidate)) + geom_line() + scale_color_manual(values = c('blue', 'red')) # Übrigens: Die Negativitätswerte lassen sich an der Stelle nur relativ interpretieren. # Trump hat also über weite Strecken deutlich mehr Negativität in seinen Tweets als Clinton.