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?
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/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, error
. Für alle Iterationen, bei denen ein Listenelement numerisch ist, enthält result
[1] 0.5
[[1]]result
[1] 1
[[2]]result
[1] "This did not quite work out."
[[3]]result
[1] 3
[[4]]result gibt wiederum das Ergebnis unserer Operation zurück
warnings
und result
[1] 0.5
[[1]]warnings
character(0)
[[1]]result
[1] "a"
[[2]]warnings
[1] "Can't be done. Printing this instead."
[[2]]result
[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(.))
Wunderbar – das war eine schnelle Zusammenfassung, wie du deine Funktionen zur Behandlung von Nebenwirkungen in deinen Listenoperationen mit Adverbien von purrr verpackst. Happy Coding!