Data Science, Machine Learning und KI
Kontakt
Content Hub
Blog Post

Purrr Tutorial – Bei Risiken und Nebenwirkungen… Konsultieren Sie Ihren Purrr-Mazeutiker

  • Expert:innen David Schlepps
  • Datum 04. Mai 2018
  • Thema CodingRTutorial
  • Format Blog
  • Kategorie Technology
Purrr Tutorial – Bei Risiken und Nebenwirkungen… Konsultieren Sie Ihren Purrr-Mazeutiker

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 David Schlepps

  • Erfahre mehr!

    Als eines der führenden Unternehmen im Bereich Data Science, Machine Learning und KI begleiten wir Sie in die datengetriebene Zukunft. Erfahren Sie mehr über statworx und darüber, was uns antreibt.
    Über uns