Data Science, Machine Learning und KI
Kontakt

Errors, Warnings und Meldungen erfassen, während Listenoperationen weiterlaufen

In den ersten beiden Teilen meiner Artikelserie über Textmining habe ich einige Lösungen für das Webscraping der Inhalte unseres STATWORX-Blogs mit Hilfe des purrr-Pakets vorgestellt. Bei den Vorbereitungen für die nächste Folge meiner Serie über Text Mining fiel mir jedoch ein kleines Gimmick ein, das ich auf dem Weg dorthin sehr hilfreich fand. Daher ein kleiner Umweg: Wie erfasse ich Side-Effects und Errors, wenn ich Operationen auf Listen mit purrr durchführe, anstatt eine Schleife zu verwenden?

meme blog loop

Zunächst einmal ein kurzes motivierendes Beispiel: Stell dir vor, wir wollen einen Parser für den Blog unserer STATWORX-Website entwickeln. Wir haben jedoch keine Ahnung, wie viele Einträge in der Zwischenzeit gepostet wurden (der Bienenstock von Data Scientists in unserem Büro ist in der Regel ziemlich schnell mit dem Schreiben solcher Artikel). Daher muss eine solche Funktion robuster sein, d. h. sie muss die Grausamkeiten von „404 – Not found“-Fehlermeldungen überstehen und auch nach einem Fehler noch weiter parsen können.

Wie könnte das possibly() funktionieren?

Lass uns also einige schöne purrr-Adverbien verwenden, um alle unsere Ausgaben, Errors und Warnings furchtlos aufzuzeichnen, anstatt anzuhalten und den User aufzufordern, sich um die Seiteneffekte zu kümmern, sobald Fehler auftauchen. Diese Adverbien erinnern an try(), sind aber etwas praktischer für Operationen auf Listen.

Betrachten wir zunächst ein komplexeres, motivierendes Beispiel, aber keine Sorge – weiter unten auf dieser Seite gibt es offensichtlichere Beispiele, die helfen, die Feinheiten zu erklären. Der folgende R-Code veranschaulicht unsere Verwendung von possibly() für die spätere Verwendung mit puurr::map(). Lass uns zunächst einen Blick darauf werfen, was wir mit unserer Funktion erreichen wollten. Genauer gesagt, was zwischen den geschweiften Klammern unten passiert: Unsere Funktion robust_parse() parst einfach HTML-Webseiten nach anderen Links unter Verwendung von URLs, die wir ihr zur Verfügung stellen. In diesem Fall verwenden wir einfach paste0(), um einen Vektor von Links zu unseren Blog-Übersichtsseiten zu erstellen, extrahieren die Weblinks von jeder dieser Seiten mit XML::xpathSApply(), leiten diese Weblinks in einen data_frame und bereinigen unsere Ergebnisse von Duplikaten mit dplyr::filter() – es gibt verschiedene Übersichtsseiten, die unsere Blogs nach Kategorien gruppieren – und dplyr::distinct().

robust_parse <- possibly(function(value){
        htmlParse(paste0("http://www.statworx.com/de/blog/page/",
                         value, "/")) %>%
          xpathSApply(., "//a/@href") %>%
          data_frame(.) %>%
          filter(., grepl("/blog", .)) %>%
          filter(., !grepl("/blog/|/blog/page/|/data-science/|/statistik/", .)) %>%           distinct()       }, otherwise = NULL) </code></pre>  Lass uns nun untersuchen, wie wir <code>possibly()</code> in diesem Zusammenhang verwenden. <code>possibly()</code> erwartet von uns eine zu modifizierende Funktion sowie das Argument <code>otherwise</code>, das angibt, was sie tun soll, wenn es schiefgeht. In diesem Fall wollen wir <code>NULL</code> als Ausgabewert. Eine andere beliebte Wahl wäre <code>NA</code>, um zu signalisieren, dass wir irgendwo eine Zeichenkette nicht wie vorgesehen erzeugt haben. In unserem Beispiel sind wir jedoch mit <code>NULL</code> zufrieden, da wir nur die existierenden Seiten analysieren wollen und keine spezifische Auflistung der nicht existierenden Seiten benötigen (oder was passiert ist, wenn wir eine Seite nicht gefunden haben).  <pre><code class="language-r" lang="r">webpages <- map_df(0:100, ~robust_parse(.)) %>%  			unlist  > webpages .1 "https://www.statworx.com/de/blog/strsplit-but-keeping-the-delimiter/" .2 "https://www.statworx.com/de/blog/data-science-in-python-vorstellung-von-nuetzlichen-datenstrukturen-teil-1/" .3 "https://www.statworx.com/de/blog/burglr-stealing-code-from-the-web/" .4 "https://www.statworx.com/de/blog/regularized-greedy-forest-the-scottish-play-act-i/"  ... </code></pre>  Drittens verwenden wir unsere neue Funktion <code>robust_parse()</code>, um mit einem Vektor oder einer Liste von ganzen Zahlen von 0 bis 100 (mögliche Anzahl von Unterseiten, die wir analysieren wollen) zu arbeiten, und werfen einen kurzen Blick auf die schönen Links, die wir extrahiert haben. Zur Erinnerung: Unten findest du den Code zum Extrahieren und Bereinigen der Inhalte der einzelnen Seiten mit Hilfe einer weiteren <code>map_df()</code>-basierten Schleife - die im Mittelpunkt eines <a href="https://www.statworx.com/de/blog/furchtlose-grammatiker-textmining-im-tidyverse-teil-2/">anderen Beitrags</a> steht. <pre><code class="language-r" lang="r">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"))) %>%                                  anti_join(data_frame(word = stopwords("en"))) %>%                                  mutate(author = .word[2]))

Aber eigentlich wollen wir zu unseren purrr-Helfern zurückkehren und sehen, was sie für uns tun können. Um genauer zu sein, heißen sie nicht Helfer, sondern Adverbien, da wir sie verwenden, um das Verhalten einer Funktion (d.h. eines Verbs) zu ändern. Unsere derzeitige Funktion robust_parse() erzeugt keine Einträge, wenn die Schleife nicht erfolgreich eine Webseite findet, die sie nach Links durchsucht.

Stell dir die Situation vor, dass du erfolglose Operationen und Fehler, die auf dem Weg dorthin auftreten, verfolgen wilslt. Anstatt die purrr-Adverbien anhand des oben genannten Codes weiter zu erforschen, wollen wir uns ein viel einfacheres Beispiel ansehen, um die möglichen Zusammenhänge zu erkennen, in denen die Verwendung von purrr-Adverbien hilfreich sein kann.

Ein viel einfacheres Beispiel: Versuche, eine Zeichenkette durch 2 zu teilen

Angenommen, es gibt ein Element in unserer Liste, bei dem unsere erstaunlichen Divisionsfähigkeiten nutzlos sind: Wir werden versuchen, alle Elemente in unserer Liste durch 2 zu teilen – aber dieses Mal wollen wir, dass purrr beachtet, wo die Funktion i_divide_things sich weigert, bestimmte Elemente für uns zu teilen. Auch hier hilft uns das otherwise-Argument, unsere Ausgabe in Situationen zu definieren, die außerhalb des Anwendungsbereichs unserer Funktion liegen.

i_divide_things <- possibly(function(value){
						value /2},
                  		otherwise = "I won't divide this for you.")

# Let's try our new function

> purrr::map(list(1, 2, "a", 6), ~ i_divide_things(.))
[[1]]
[1] 0.5

[[2]]
[1] 1

[[3]]
[1] "I won't divide this for you."

[[4]]
[1] 3


Bedenke allerdings den Fall, bei dem „etwas hat nicht funktioniert“ nicht ausreicht und du sowohl mögliche Fehler als auch Warnungen im Auge behalten willst, während du die gesamte Ausgabe beibehältst. Das ist eine Aufgabe für safely(): Wie unten dargestellt, hilft uns die Umrahmung unserer Funktion durch safely(), eine verschachtelte Liste auszugeben. Für jedes Element der Eingabe liefert die Ausgabe zwei Komponenten, result</code> und <code>error. Für alle Iterationen, bei denen ein Listenelement numerisch ist, enthält result</code> eine numerische Ausgabe und ein leeres (<code>= NULL</code>) Fehler-Element. Nur für das dritte Listenelement - wenn unsere Funktion über eine Zeicheneingabe stolpert - haben wir eine Fehlermeldung sowie das Ergebnis, das wir mit <code>otherwise</code> definiert haben, erfasst. <pre><code class="language-r">i_divide_things <- safely(function(value){                       value /2},                       otherwise = "This did not quite work out.")  > purrr::map(list(1, 2, "a", 6), ~ i_divide_things(.)) [[1]] [[1]]result
[1] 0.5

[[1]]error NULL   [[2]] [[2]]result
[1] 1

[[2]]error NULL   [[3]] [[3]]result
[1] "This did not quite work out."

[[3]]error <simpleError in value/2: non-numeric argument to binary operator>   [[4]] [[4]]result
[1] 3

[[4]]error NULL</code></pre> Im vorstehenden Beispiel haben wir unsere Fehler erst aufgedeckt, nachdem wir eine Schleife über alle Elemente unserer Liste durchlaufen haben, indem wir die Ausgabeliste inspiziert haben. Allerdings hat <code>safely()</code> auch das Argument <code>quiet</code>, das standardmäßig auf <code>TRUE</code> gesetzt ist. Wenn wir dies auf <code>FALSE</code> setzen, erhalten wir unsere Fehler genau dann, wenn sie auftreten.  Nun wollen wir einen kurzen Blick auf <code>quietly()</code> werfen. Wir werden eine Warnung und eine Nachricht definieren und einen Output erstellen. Dies soll veranschaulichen, wo <em>purrr</em> die einzelnen Komponenten speichert, die unsere Funktion zurückgibt. Für jedes Element unserer Eingabe liefert die zurückgegebene Liste vier Komponenten: <ul>  	<li><code>result gibt wiederum das Ergebnis unserer Operation zurück

  • output</code> gibt die Ausgabe zurück, die in der Konsole ausgegeben wurde</li>  	<li><code>warnings und message</code> geben die von uns definierten Zeichenketten zurück</li> </ul> <pre><code class="language-r" lang="r">i_divide_things <- purrr::quietly(function(value){   if(is.numeric(value) == TRUE) {           print(value / 2)   }   else{            warning("Can't be done. Printing this instead.")           message("Why would you even try dividing this?")           print(value)   }   })  > purrr::map(list(1, "a", 6), ~i_divide_things(.)) [[1]] [[1]]result
    [1] 0.5

    [[1]]output [1] "[1] 0.5"  [[1]]warnings
    character(0)

    [[1]]messages character(0)   [[2]] [[2]]result
    [1] "a"

    [[2]]output [1] "[1] \"a\""  [[2]]warnings
    [1] "Can't be done. Printing this instead."

    [[2]]messages [1] "Why would you even try dividing this?\n"   [[3]] [[3]]result
    [1] 3

    [[3]]output [1] "[1] 3"  [[3]]warnings
    character(0)

    [[3]]$messages
    character(0)

    Schließlich gibt es noch auto_browse(), das es uns ermöglicht, den RStudio-Browser für die Fehlersuche auszulösen und die Usern an den ungefähren Ort des Fehlers zu bringen. Dieser Fall ist im folgenden Screenshot dargestellt.

    i_divide_things <- purrr::auto_browse(function(value){
    
        print(value / 2)
    })
    
    purrr::map(list(1, "a", 6), ~i_divide_things(.)) 
    

     

    output of auto_browse

     

    Wunderbar – das war eine schnelle Zusammenfassung, wie du deine Funktionen zur Behandlung von Nebenwirkungen in deinen Listenoperationen mit Adverbien von purrr verpackst. Happy Coding! David Schlepps

    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 %
      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 %
      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 %
      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 %
      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.

    David Schlepps

    Die Grundlage

    Das methodische Schaffenswerk Karl Pearsons ist durchaus bekannt – kaum ein Student einer Disziplin mit quantitativen Spielarten wird am Namen des ersten Statistik-Lehrstuhlinhabers der Welt vorbeikommen. Durchaus weniger bekannt ist jedoch Pearsons Werk The Grammar of Science und seine Anschauungen zum – ihm zufolge vornehmlich deskriptiven statt erklärenden – Wesen der wissenschaftlichen Methode. Diese vielfältigen Beiträge zur Erkenntnistheorie inspirierten nachweislich die Arbeit Albert Einsteins, ebenso wie die anderer namenhafter Wissenschaftler.

    Karl Pearson 1910

    Zugegeben, das 540-Seiten-starke Buch liest sich nicht unbedingt in der Mittagspause – können uns moderne Data Science Methoden dabei helfen, uns dem mehr als 200-Jahre-alten Grundsatzwerk schneller anzunähern? Das Manuskript des berühmten UCL-Professors ist frei im Netz zugänglich und stellt damit die ideale Textgrundlage für unsere ersten Textmining-Schritte im tidyverse dar. In diesem Beitrag wollen wir uns um vorbereitende Schritte im Textmining kümmern und unsere ersten Gehversuche im Bereich der Aufbereitung und Visualisierung machen.

    Erste Textaufbereitungsschritte

    Mit dem tidytext-Paket von Julia Silge und David Robinson gehen diese aber wirklich sehr schnell (ihre großartige Einführung in das Paket und das Thema Textmining finden Sie hier). Nach dem Laden (bzw. Installieren) der Pakete tidytext, dplyr (zur allgemeinen Datenaufbereitung) und tibble (gemeinsam enthalten im Paket tidyverse) sind wir bereit, die ersten Textaufbereitungsschritte zu gehen. Ein Download von The Grammar of Science als .txt-File lässt sich ganz angenehm mit R durchführen.

    # load packages 
    library(tidytext) 
    library(tidyverse) 
    
    # tell R where your .txt-File should go, dowload the file and give it a name 
    setwd("/Users/obiwan/jedi_documents") 
    download.file("https://archive.org/stream/grammarofscience00pearuoft/
                   grammarofscience00pearuoft_djvu.txt",
                  destfile = "grammar.txt")
    

    Anschließend lesen wir mit readChar() das .txt-File aus und bereinigen im Text vorhandene Absätze (durch „n“ gekennzeichnet). Wichtig für die weitere Verarbeitung ist auch, dass wir einen tibble mit einer character-Variable erstellen, mit der wir im Folgenden weiterarbeiten.

    # read the .txt contents and clean line breaks 
    grammar_os_text <- readChar('grammar.txt', file.info('grammar.txt')$size) 
    grammar_os_text <- gsub("n", "", paste(grammar_os_text, collapse = "n")) 
    
    # create a tibble with a character vector 
    grammar_os_df <- tibble(content = grammar_os_text)
    

    Bereinigung der Daten

    Im Weiteren zerlegen wir mit der tidytext-Funktion unnest_tokens() den Text in Tokens – in unserem Fall die einzelnen Worte im Text, die Standardeinstellung der Funktion. Die Funktion ist allerdings deutlich vielseitiger und kann beispielsweise Paragraphen, Sätze oder Wortteile nach regulären Ausdrücken zerlegen und ausgeben. Ein Blick auf den Datensatz zeigt, dass wir die ersten Worte der Titelseite (sowie den restlichen Inhalt des Buches) als Zeilen in unserem Datensatz angelegt haben.

    # zerlegen in einzelne Woerter
    grammar_os_df % unnest_tokens(word, content) 
    head(grammar_os_df) 
    
    # Ausgabe
           word 
    1       the 
    1.1 grammar 
    1.2      of 
    1.3 science 
    1.4   first 
    1.5 edition 
    

    Selbstverständlich sind wir mit der Bereinigung der word-Variable noch am Anfang. Nach dem Laden des tokenizers-Paketes, haben wir nun die Möglichkeit, sogenannte Stop Words („a“, „an“, „and“, „are“, etc.) aus dem Datensatz herauszufiltern. Hierbei ist uns die dplyr-Funktion anti_join() behilflich, welche hier alle Reihen aus dem ersten Argument (unser grammar_os_df) ausgibt, welche nicht im zweiten Argument (ein Datensatz aus Stop Words) vorhanden sind.

    # Stop words
    library(tokenizers) 
    
    data(stop_words) 
    grammar_os_df <- anti_join(grammar_os_df, tibble(word = stopwords("en"))) 
    

    Sentiment-Bibliotheken

    Nachdem wir die ersten Bereinigungen unserer Daten vorgenommen haben, sind wir nun bereit, Sentiment-Lexika zu verwenden – wenn man so will Wörterbücher. Praktischerweise steht bei tidytext hierfür direkt der Befehl get_sentiments() zur Verfügung. Dieser erlaubt es uns, vier unterschiedliche Sentiment-Bibliotheken im tidy-Format zu laden, bei denen pro Eintrag eine Zeile besteht.

    Zur Auswahl steht erstens bing, ein Lexikon mit zwei Klassen: Positive und negative Worte. Zweitens afinn, ein Lexikon mit Ratings auf einer Skala von minus fünf (negativ) bis fünf (positiv). Die letzten beiden Möglichkeiten sind nrc und loughran, zwei Lexica welche etwas nuanciertere Klassifikationen bieten. Das Lexikon nrc bietet zehn Kategorien (joy, fear, disgust, anticipation, anger, trust, surprise, sadness, positive, negative), während loughran sechs Kategorien (litigious, constraining, superfluous, uncertainty, sowie positive und negative) bietet. Eine beispielhafte Klassifizierung aus unserem tidytext sieht wie folgt aus:

    word sentiment
    displeased sadness
    superficial negative
    university anticipation

    Eine sehr simple Form der Analyse unserer Textdaten beruht dementsprechend auf einem simplen inner_join() des aufbereiteten Datensatzes und den zuvor genannten Lexika. Mit einem so gematchen Datensatz können wir aufschlussreiche erste Visualisierungen machen. Im Folgenden Code-Snippet erledigen wir folgende Schritte:

    • Aufrufen des nrc-Lexikons, sowie inner_join() von nrc-Lexikon und dem grammar_os_df in einem Schritt
    • Auszählung von Worten gruppiert nach Sentiment (auch dplyr::count() könnte hier als eine Kurzform der Kombination von group_by() und tally() verwendet werden)
    • Erstellung einer Termmatrix, d.h. Konversion des Long- zu einem Wide-Datensatz, in dem eine Spalte pro Sentiment erstellt wird und leere Zeilen mit Nullen gefüllt werden. Da das wordcloud-Paket eine Matrix mit Zeilennamen benötigt, müssen wir den tibble, welchen spread() ausgibt, noch entsprechend umwandeln.
    • Visualisierung der Termmatrix in Form einer Comparison Cloud – eine Wordcloud gruppiert nach Klassen. Als letztere Klassen verwenden wir die zuvor gebildeten Sentimentspalten, die Größe der Worte wird – wie bei Wordclouds üblich – über die Anzahl bestimmt. Als zusätzliche Pakete verwenden wir wordcloud, sowie den RColorBrewer, um eine zehnstufige Farbpalette zu bilden, bei der die benachbarten Farben deutlich genug unterscheidbar sind.
    inner_join(grammar_os_df, get_sentiments("nrc")) %>% 
      group_by(., word, sentiment) %>% tally(., sort = TRUE) %>% 
      spread(., key = sentiment, value = n, fill = 0)  %>%  
      remove_rownames(.) %>%  
      column_to_rownames(., "word") %>%  
      comparison.cloud(data.matrix(.), scale=c(3.5,.75),
                       colors = brewer.pal(10, "Paired"),
                       max.words = 1000)
    

    Wordcloud Grammar of sciende

    Zum Vergleich erstellen wir den selben Plot noch einmal mit dem zuvor genannten loughran-Lexikon:

    Wordcloud Grammar of sciende - loughran

    Die beiden Comparison-Clouds zeigen hierbei bereits die deutlichen Unterschiede in den Lexika auf. Erstere könnte auf eine unterrepräsentierte Furchtdimension im Text hinweisen – eine Darstellung die den eingefleischten Pearson-Fans nicht widerstreben dürfte. Wie korrekt die einzelnen Klassifizierungen wirklich erscheinen und ob sich erstere Wordcloud wirklich für Einschätzungen über die Furchtlosigkeit in Pearsons Werk eignet, das wollen wir an dieser Stelle allerdings offenlassen.

    Fazit

    Soviel zu unseren ersten Gehversuchen im Tidyverse der Textanalyse. Während sich dieser Beitrag nur mit allerersten Schritten der Vorbereitung und Exploration von Textdaten beschäftigt, wollen wir in den nächsten Beiträgen natürlich tiefer in die Materie eindringen: Dabei werden wir – ganz im Sinne des Goethe-begeisterten Pearson – einen Ausblick zur Verarbeitung von deutschen Texten geben und entsprechende deutsche Lexika vorstellen. Zusätzlich werden wir auch in fortgeschrittene Analysemethoden vordringen und versuchen, mit Hilfe statistischer Modelle Bedeutungsstrukturen zu erkunden.

    Wem die Arbeit mit epistemologischer Literatur etwas zu staubig erscheint – dem sei ganz dringend der Blog meines Kollegen Lukas ans Herz gelegt: Dort geht gibt es alles Wissenswerte zum effektiven Anzapfen von Twitter als Datenoase.

    Referenzen

    1. Erich Neuwirth (2014). RColorBrewer: ColorBrewer Palettes. R package version 1.1-2. https://CRAN.R-project.org/package=RColorBrewer
    2. 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
    3. Ian Fellows (2014). wordcloud: Word Clouds. R package version 2.5. https://CRAN.R-project.org/package=wordcloud
    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. Porter, T. (2017). Karl Pearson. In Encyclopædia Britannica. Retrieved from https://www.britannica.com/biography/Karl-Pearson
    7. Silge, J., & Robinson, D. (2017). Text mining with R: a tidy approach. Sebastopol, CA: OReilly Media.

    David Schlepps

  • Die Grundlage

    Das methodische Schaffenswerk Karl Pearsons ist durchaus bekannt – kaum ein Student einer Disziplin mit quantitativen Spielarten wird am Namen des ersten Statistik-Lehrstuhlinhabers der Welt vorbeikommen. Durchaus weniger bekannt ist jedoch Pearsons Werk The Grammar of Science und seine Anschauungen zum – ihm zufolge vornehmlich deskriptiven statt erklärenden – Wesen der wissenschaftlichen Methode. Diese vielfältigen Beiträge zur Erkenntnistheorie inspirierten nachweislich die Arbeit Albert Einsteins, ebenso wie die anderer namenhafter Wissenschaftler.

    Karl Pearson 1910

    Zugegeben, das 540-Seiten-starke Buch liest sich nicht unbedingt in der Mittagspause – können uns moderne Data Science Methoden dabei helfen, uns dem mehr als 200-Jahre-alten Grundsatzwerk schneller anzunähern? Das Manuskript des berühmten UCL-Professors ist frei im Netz zugänglich und stellt damit die ideale Textgrundlage für unsere ersten Textmining-Schritte im tidyverse dar. In diesem Beitrag wollen wir uns um vorbereitende Schritte im Textmining kümmern und unsere ersten Gehversuche im Bereich der Aufbereitung und Visualisierung machen.

    Erste Textaufbereitungsschritte

    Mit dem tidytext-Paket von Julia Silge und David Robinson gehen diese aber wirklich sehr schnell (ihre großartige Einführung in das Paket und das Thema Textmining finden Sie hier). Nach dem Laden (bzw. Installieren) der Pakete tidytext, dplyr (zur allgemeinen Datenaufbereitung) und tibble (gemeinsam enthalten im Paket tidyverse) sind wir bereit, die ersten Textaufbereitungsschritte zu gehen. Ein Download von The Grammar of Science als .txt-File lässt sich ganz angenehm mit R durchführen.

    # load packages 
    library(tidytext) 
    library(tidyverse) 
    
    # tell R where your .txt-File should go, dowload the file and give it a name 
    setwd("/Users/obiwan/jedi_documents") 
    download.file("https://archive.org/stream/grammarofscience00pearuoft/
                   grammarofscience00pearuoft_djvu.txt",
                  destfile = "grammar.txt")
    

    Anschließend lesen wir mit readChar() das .txt-File aus und bereinigen im Text vorhandene Absätze (durch „n“ gekennzeichnet). Wichtig für die weitere Verarbeitung ist auch, dass wir einen tibble mit einer character-Variable erstellen, mit der wir im Folgenden weiterarbeiten.

    # read the .txt contents and clean line breaks 
    grammar_os_text <- readChar('grammar.txt', file.info('grammar.txt')$size) 
    grammar_os_text <- gsub("n", "", paste(grammar_os_text, collapse = "n")) 
    
    # create a tibble with a character vector 
    grammar_os_df <- tibble(content = grammar_os_text)
    

    Bereinigung der Daten

    Im Weiteren zerlegen wir mit der tidytext-Funktion unnest_tokens() den Text in Tokens – in unserem Fall die einzelnen Worte im Text, die Standardeinstellung der Funktion. Die Funktion ist allerdings deutlich vielseitiger und kann beispielsweise Paragraphen, Sätze oder Wortteile nach regulären Ausdrücken zerlegen und ausgeben. Ein Blick auf den Datensatz zeigt, dass wir die ersten Worte der Titelseite (sowie den restlichen Inhalt des Buches) als Zeilen in unserem Datensatz angelegt haben.

    # zerlegen in einzelne Woerter
    grammar_os_df % unnest_tokens(word, content) 
    head(grammar_os_df) 
    
    # Ausgabe
           word 
    1       the 
    1.1 grammar 
    1.2      of 
    1.3 science 
    1.4   first 
    1.5 edition 
    

    Selbstverständlich sind wir mit der Bereinigung der word-Variable noch am Anfang. Nach dem Laden des tokenizers-Paketes, haben wir nun die Möglichkeit, sogenannte Stop Words („a“, „an“, „and“, „are“, etc.) aus dem Datensatz herauszufiltern. Hierbei ist uns die dplyr-Funktion anti_join() behilflich, welche hier alle Reihen aus dem ersten Argument (unser grammar_os_df) ausgibt, welche nicht im zweiten Argument (ein Datensatz aus Stop Words) vorhanden sind.

    # Stop words
    library(tokenizers) 
    
    data(stop_words) 
    grammar_os_df <- anti_join(grammar_os_df, tibble(word = stopwords("en"))) 
    

    Sentiment-Bibliotheken

    Nachdem wir die ersten Bereinigungen unserer Daten vorgenommen haben, sind wir nun bereit, Sentiment-Lexika zu verwenden – wenn man so will Wörterbücher. Praktischerweise steht bei tidytext hierfür direkt der Befehl get_sentiments() zur Verfügung. Dieser erlaubt es uns, vier unterschiedliche Sentiment-Bibliotheken im tidy-Format zu laden, bei denen pro Eintrag eine Zeile besteht.

    Zur Auswahl steht erstens bing, ein Lexikon mit zwei Klassen: Positive und negative Worte. Zweitens afinn, ein Lexikon mit Ratings auf einer Skala von minus fünf (negativ) bis fünf (positiv). Die letzten beiden Möglichkeiten sind nrc und loughran, zwei Lexica welche etwas nuanciertere Klassifikationen bieten. Das Lexikon nrc bietet zehn Kategorien (joy, fear, disgust, anticipation, anger, trust, surprise, sadness, positive, negative), während loughran sechs Kategorien (litigious, constraining, superfluous, uncertainty, sowie positive und negative) bietet. Eine beispielhafte Klassifizierung aus unserem tidytext sieht wie folgt aus:

    word sentiment
    displeased sadness
    superficial negative
    university anticipation

    Eine sehr simple Form der Analyse unserer Textdaten beruht dementsprechend auf einem simplen inner_join() des aufbereiteten Datensatzes und den zuvor genannten Lexika. Mit einem so gematchen Datensatz können wir aufschlussreiche erste Visualisierungen machen. Im Folgenden Code-Snippet erledigen wir folgende Schritte:

    inner_join(grammar_os_df, get_sentiments("nrc")) %>% 
      group_by(., word, sentiment) %>% tally(., sort = TRUE) %>% 
      spread(., key = sentiment, value = n, fill = 0)  %>%  
      remove_rownames(.) %>%  
      column_to_rownames(., "word") %>%  
      comparison.cloud(data.matrix(.), scale=c(3.5,.75),
                       colors = brewer.pal(10, "Paired"),
                       max.words = 1000)
    

    Wordcloud Grammar of sciende

    Zum Vergleich erstellen wir den selben Plot noch einmal mit dem zuvor genannten loughran-Lexikon:

    Wordcloud Grammar of sciende - loughran

    Die beiden Comparison-Clouds zeigen hierbei bereits die deutlichen Unterschiede in den Lexika auf. Erstere könnte auf eine unterrepräsentierte Furchtdimension im Text hinweisen – eine Darstellung die den eingefleischten Pearson-Fans nicht widerstreben dürfte. Wie korrekt die einzelnen Klassifizierungen wirklich erscheinen und ob sich erstere Wordcloud wirklich für Einschätzungen über die Furchtlosigkeit in Pearsons Werk eignet, das wollen wir an dieser Stelle allerdings offenlassen.

    Fazit

    Soviel zu unseren ersten Gehversuchen im Tidyverse der Textanalyse. Während sich dieser Beitrag nur mit allerersten Schritten der Vorbereitung und Exploration von Textdaten beschäftigt, wollen wir in den nächsten Beiträgen natürlich tiefer in die Materie eindringen: Dabei werden wir – ganz im Sinne des Goethe-begeisterten Pearson – einen Ausblick zur Verarbeitung von deutschen Texten geben und entsprechende deutsche Lexika vorstellen. Zusätzlich werden wir auch in fortgeschrittene Analysemethoden vordringen und versuchen, mit Hilfe statistischer Modelle Bedeutungsstrukturen zu erkunden.

    Wem die Arbeit mit epistemologischer Literatur etwas zu staubig erscheint – dem sei ganz dringend der Blog meines Kollegen Lukas ans Herz gelegt: Dort geht gibt es alles Wissenswerte zum effektiven Anzapfen von Twitter als Datenoase.

    Referenzen

    1. Erich Neuwirth (2014). RColorBrewer: ColorBrewer Palettes. R package version 1.1-2. https://CRAN.R-project.org/package=RColorBrewer
    2. 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
    3. Ian Fellows (2014). wordcloud: Word Clouds. R package version 2.5. https://CRAN.R-project.org/package=wordcloud
    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. Porter, T. (2017). Karl Pearson. In Encyclopædia Britannica. Retrieved from https://www.britannica.com/biography/Karl-Pearson
    7. Silge, J., & Robinson, D. (2017). Text mining with R: a tidy approach. Sebastopol, CA: OReilly Media.

    David Schlepps