Druckbuchstaben in Schublade

Furchtlose Grammatiker – Textmining im tidyverse Teil 2

David Schlepps Blog, Data Science

In unserem ersten Blog-Beitrag zum Textmining im tidyverse haben wir uns mit den ersten Schritten zum Einlesen und Bereinigen von Texten mit den Mitteln des tidyverse befasst und bereits erste Sentimentanalysen begonnen. Die Grundlage hierzu bildete das epistemologische Werk The Grammar of Science von Karl Pearson. Im zweiten Teil wollen wir auf diesen Grundlagen aufbauen und damit ein weiteres von Pearsons vielfältigen Interessensgebieten anschneiden: Die deutsche Sprache. Pearson, der nach einem Studienaufenthalt in Heidelberg Karl anstelle Carl genannt werden wollte, verlieh seinem Interesse an Goethe auch in seinem Buch The New Werther Ausdruck.

Um den deutschsprachigen Korpora des Internets gerecht zu werden, wollen wir an dieser Stelle Lexika vorstellen, welche sich für die Bedeutungsanalyse von Texten eignen. Hierzu eignet sich der Sentimentwortschatz SentiWS der Universität Leipzig. In diesem Worschatz finden sich Ratings auf einer Skala von -1 (negatives Sentiment) bis 1 (positives Sentiment). Die aktuellste Version kann als .zip-File hier heruntergeladen werden.

Wie im ersten Blog der Serie beschrieben, ist der Weg zu ersten Analysen relativ kurz: Nach etwas Datenbereinigung und Zerlegung unseres Character-Strings in einzelne Tokens verbinden wir unsere Textdaten (in diesem Fall ein nach Autoren gruppierter Korpus unseres STATWORX-Blogs) mit dem Lexikon unserer Wahl, wodurch wir einen Datensatz von nach Sentiment bewerteten Wörtern erhalten, welche sowohl in unserem Datensatz als auch im Lexikon enthalten sind.

Deutschsprachige Blogs scrapen

Bevor wir beginnen, müssen wir uns allerdings zuerst der Erstellung eines Textdatensatzes widmen. Da wir als Beispiel einen vornehmlich deutschen, aber überschaubaren Korpus wählen möchten und uns das Befinden der STATWORX-Blogger verständlicher Weise sehr am Herzen liegt, möchten wir den STATWORX-Blog als Grundlage nutzen.

Nach dem Laden der relevanten Pakete (auch in diesem Eintrag möchte ich wieder Pakete aus dem tidyverse empfehlen), konstruieren wir mit Hilfe des purrr-Paketes zwei aufgeräumte, kompakte Code-Blöcke zum Sammeln der entsprechenden Blog-Links und zum Auslesen und Präparieren selbiger.

# load packages
library(XML)
library(xml2)
library(tidyverse)
library(tidytext)
library(tokenizers)

Im folgenden Code-Block durchsuchen wir die fünf bisher existierenden Blog-Übersichten auf der STATWORX-Homepage nach Links zu den einzelnen Blogs. Dafür nutzen wir hmtlParse und xpathSApply aus dem XML-Paket um die Übersichtsseiten einzulesen und nach Links zu durchforsten. Mit Hilfe von filter und distinct aus dem dplyr-Paket trennen wir daraufhin Übersichten von den eigentlichen Artikeln und filtern Duplikate aus den Links heraus.

# Extraction of first five pages of Statworx-Blogs
# Extraction of all links that contain "blog", but filter the overview pages
# get unique blog posts

webpages <- map_df(1:5,
                   ~htmlParse(paste0("http://www.statworx.com/de/blog/page/",
                                     ., "/")) %>%
  xpathSApply(., "//a/@href") %>%
  data_frame(.) %>%
  filter(., grepl("/blog", .)) %>%
  filter(., !grepl("/blog/$|/blog/page/|/data-science/|/statistik/", .)) %>%
  distinct()) %>% unlist

Nun sind wir bereit, mit den entsprechenden Links, den bereits angesprochenen xpathSApply und htmlParse, sowie read_html aus dem xml2-Paket die eigentlichen Blogeinträge auszulesen. Mit Hilfe von paste und gsub bereinigen wir die Absätze im Text. Anschließend nutzen wir unnest_tokens aus dem tidytext-Paket, um einzelne Worte aus den Blogeinträgen zu isolieren. Weiterhin nutzen wir dplyr und das tokenizers-Paket, um mit anti_join und stopwords(„de“) deutschsprachige Stopwords aus dem Text zu entfernen (für genauere Beschreibungen der Begrifflichkeiten und der Natur dieser Bereinigungen möchte ich an dieser Stelle noch einmal auf den ersten Teil unserer Serie Textmining im tidyverse verweisen). Zuletzt fügen wir noch eine Spalte zum Dataframe hinzu (da dieser Block in purrr::map_df eingewickelt ist, erhalten wir als Output unserer Pipe einen Dataframe), welcher den Nachnamen des jeweiligen STATWORX-Bloggers angibt.

# read in blog posts, output should be a dataframe
# parse HTML, extract text, clean line breaks
# unnest tokens (in this case terms) and remove stop words
# add a column with the author name

tidy_statworx_blogs <- map_df(webpages, ~read_html(.) %>%
  htmlParse(., asText = TRUE) %>%
  xpathSApply(., "//p", xmlValue) %>%
  paste(., collapse = "\n") %>%
  gsub("\n", "", .) %>%
  data_frame(text = .) %>%
  unnest_tokens(word, text) %>%
  anti_join(data_frame(word = stopwords("de"))) %>%
  mutate(author = .$word[2]))

Im nächsten Schritt wollen wir unseren Blog-Datensatz mit dem oben genannten Leipziger Sentimentwortschatz verbinden. Wir lesen sowohl die negativen, als auch die positiven Sentimentrating-txt-Files ein, beachten dabei \t als Trennzeichen und setzen fill = TRUE. Mit bind_rows aus dem dplyr-Paket verbinden wir beide Rating-Datensätze, selektieren nur die ersten beiden Spalten und benennen diese mit word und value.

setwd("/Users/obiwan/jedi_documents")
sentis <- bind_rows(
  read.table("SentiWS_v1.8c_Negative.txt", sep = "\t", fill = TRUE),
  read.table("SentiWS_v1.8c_Positive.txt", sep = "\t", fill = TRUE)) %>%
  dplyr::select(., 1:2)
names(sentis) <- c("word", "value")

Anschließend nutzen wir str_to_lower aus dem stringr-Paket, um die character-Daten im Ratingdatensatz komplett in Kleinbuchstaben umzuwandeln und gsub, um die Worttypbeschreibungen aus den Strings zu entfernen. Mit inner_join aus dem dplyr-Paket verbinden wir nun die Blog-Eintragsdaten mit den Sentimentratings und zwar nur für jene Worte, welche sowohl in den Blogs vorkommen, als auch im Leipziger Sentimentwortschatz geratet sind. Für weitere Analysen können wir auch noch in der gleichen Pipe eine Spalte hinzufügen, welche dichotom beschreibt, ob einem Wort ein positives, oder ein negatives Sentiment zugeordnet wird – dazu mehr beim nächsten Mal.

tidy_statworx_blogs_sentis <- sentis %>%
  mutate(word = stringr::str_to_lower(word)) %>%
  mutate(word = gsub("\\Dnn", "", word)) %>%
  inner_join(., tidy_statworx_blogs, by = "word") %>%
  mutate(sent_bin = ifelse(value >= 0, "positive", "negative"))

Wir erhalten einen Datensatz mit Ratings, welcher wie folgt aussieht:

tbl_df(tidy_statworx_blogs_sentis)
# A tibble: 360 x 4
word value author sent_bin
   
1 abhängigkeit -0.3653 darrall    negative
2 abhängigkeit -0.3653 darrall    negative
3 abhängigkeit -0.3653 darrall    negative
4 abhängigkeit -0.3653 moreau     negative
5 absturz      -0.4739 krabel     negative
6 abweichung   -0.3462 aust       negative
7 abweichung   -0.3462 moreau     negative
8 abweichung   -0.3462 gepp       negative
9 angriff      -0.2120 bornschein negative
10 auflösung   -0.0048 heinz      negative
# ... with 350 more rows

Durchschnittliche Sentimentratings - Ein Stimmungsbarometer?

Da uns nun interessieren könnte, welchem Blogger aus dem Team wir besser nicht krumm kommen sollten, könnten wir nun zu unserer Sicherheit das durchschnittliche Sentimentrating pro Autor visualisieren. Wir gruppieren unsere Analyse pro Autor, aggregieren die Sentimentratings als arithemtische Mittel auf Gruppenebene und pipen den entstehenden Dataframe in eine ggplot-Funktion. Letztere erstellt für uns absteigend geordnete Säulen mit dem mittleren Sentimentrating pro Blogger, zeichnet das mittlere Sentimentrating des gesamten STATWORX-Teams ein, dreht die Koordinaten und ändert das ggplot-Theme zu theme_minimal für den optischen Feinschliff.

tidy_statworx_blogs_sentis %>%
  group_by(author) %>%
  summarise(mean_senti = mean(value, na.rm = TRUE)) %>%
  ggplot(.) +
    geom_bar(aes(x = reorder(author, mean_senti), y = mean_senti),
             stat = "identity", alpha = 0.8, colour = "Darkgrey") +
    labs(title = "Mean Sentiment Rating by Author",
         x = "Author", y = "Mean Sentiment") +
    geom_hline(yintercept = mean(sentis$value, na.rm = TRUE),
               linetype = "dashed", colour = "Grey30", alpha = 0.7) +
    coord_flip() +
    theme_minimal()

Mean Sentiment Rating by Author

Eine andere Darstellung, welche für uns interessant ist, ist die Verteilung der Sentimentratings pro Autor. An dieser Stelle wählen wir einen gruppierten Densitiyplot, obwohl durchaus viele Darstellungen hier hilfreich sein können:

tidy_statworx_blogs_sentis %>% ggplot(.) +
  geom_density(aes(value, fill = author), alpha = 0.7, bw = 0.08) +
  xlim(-1,1) +
  labs(title = "Densities of Sentiment Ratings by Author",
       x = "Sentiment Rating") + theme_minimal()

Densities of Sentiment Ratings by Author

Mit diesen wenigen Handgriffen haben wir nun auch ein paar erste Analysen zu einem deutschsprachigen Textkorpus gemacht. Meine Formulierung verrät wohl bereits: Wir stehen mit dem Textmining trotz ersten Fortschritten noch ziemlich am Anfang. Allerdings haben wir uns nun für deutlich komplexere Aufgaben ausgerüstet: Der näheren Erfassung von Inhalt und Semantik in unseren Korpora. Im nächsten Teil befassen wir uns Term-Dokument-Matrizen, Dokument-Term-Matrizen, sowie der Latent Dirichlet Allocation und verwandten Techniken.

Referenzen

  1. Duncan Temple Lang and the CRAN Team (2017). XML: Tools for Parsing and Generating XML Within R and S-Plus. R package version 3.98-1.9. https://CRAN.R-project.org/package=XML
  2. Hadley Wickham, James Hester and Jeroen Ooms (2017). xml2: Parse XML. R package version 1.1.1. https://CRAN.R-project.org/package=xml2
  3. Hadley Wickham, Romain Francois, Lionel Henry and Kirill Müller (2017). dplyr: A Grammar of Data Manipulation. R package version 0.7.4. https://CRAN.R-project.org/package=dplyr
  4. Kirill Müller and Hadley Wickham (2017). tibble: Simple Data Frames. R package version 1.3.4. https://CRAN.R-project.org/package=tibble
  5. Lincoln Mullen (2016). tokenizers: A Consistent Interface to Tokenize Natural Language Text. R package version 0.1.4. https://CRAN.R-project.org/package=tokenizers
  6. Lionel Henry and Hadley Wickham (2017). purrr: Functional Programming Tools. R package version 0.2.3. https://CRAN.R-project.org/package=purrr
  7. Pearson, Karl (1880). The New Werther. C. Kegan & Co. https://archive.org/stream/newwertherbylok00peargoog#page/n6/mode/2up
  8. Pearson, Karl (1892). The Grammar of Science. London: Walter Scott. Dover Publications.
  9. https://archive.org/stream/grammarofscience00pearrich#page/n9/mode/2up
  10. Porter, T. (2017). Karl Pearson. In Encyclopædia Britannica. Retrieved from https://www.britannica.com/biography/Karl-Pearson
  11. Silge, J., & Robinson, D. (2017). Text mining with R: a tidy approach. Sebastopol, CA: OReilly Media.
Über den Autor
David Schlepps

David Schlepps

I am a data scientist at STATWORX and while I am working on machine learning problems during the daytime, I dream about the beauty of Shiny and the Tidyverse at night.