Snack Wars – XML-Logfiles und Prozessdaten analysieren mit R und tidyverse

David Schlepps Blog, Data Science, Statistik

Neuen Mitarbeitern bei STATWORX fällt unweigerlich das großzügige Angebot und die ebenbürtig große Nachfrage nach Snacks von der Banane bis zum Schokoriegel auf. Da liegt es für Datenliebhaber sehr nah, (anonymisierte) Daten über den Snack-Konsum in der Firma zu sammeln. Im Folgenden soll es um eine beliebte Form des Sammelns und der Analyse von Prozessdaten gehen: XML-Logfiles.

Für die Erstellung von XML-Logfiles gibt es allerdings keine etablierten Standards, weshalb Logfiles in freier Wildbahn ziemlich divers und komplex sein können. Da Logfiles deshalb nicht nach einem einheitlichen Schema mit unveränderten Code-Blöcken analysiert werden können, ist es eine gute Idee, sich für das Erlernen der Grundfertigkeiten ein einfacheres Beispiel zu suchen. An dieser Stelle wollen wir ein einfaches Beispiel darstellen – ein Logfile zum Snackkonsum bei STATWORX.

Logfile Aufbau

Generell bieten XML-Logfiles mehrere Möglichkeiten, strukturiert Informationen abzuspeichern bzw. wieder abzurufen. Während Informationen direkt in Elementen abgespeichert werden können, lassen sich thematisch untergeordnete Informationen beispielsweise in sogenannten Children abspeichern. Informationen, die auf der gleichen Ebene wie dieses Child abgespeichert werden sollen, lassen sich als Sibling bezeichnen.

Wir wollen uns das kurz am Beispiel eines Logfiles über den Snack-Konsum der STATWORX-Mitarbeiter veranschaulichen. Legen wir beispielsweise einen Logfile-Eintrag darüber an, dass ein Snack (Element) vom Typ Banane (Child) aus der Ablage entnommen wird:

<snack> 
<type>Banana</type> 
</snack> 

Um das Beispiel nun durch Zusatzinformationen zu erweitern, könnten wir, um den besonders gesundheitsbewussten STATWORX-Mitarbeitern gerecht zu werden, auch den Zuckergehalt grams_sugar als Sibling des Snackstyps eintragen:

<snack> 
<type>Banana</type> 
<grams_sugar>20.6</grams_sugar> 
</snack>

Eine weitere Art, Informationen abzuspeichern ist das Attribute eines Elements. In unserem Snack-Log fügen wir an das Element snack Attributes an, in denen wir den Namen des hungrigen Kollegen (in diesem Fall leicht anonymisiert) und einen Timestamp (eine Art, Zeiten abzuspeichern) angeben:

<snack timestamp = "2017-10-17 18:50:09 CEST" colleague = "Chewie"> 
<type>Banana</type> 
<grams_sugar>20.6</grams_sugar> 
</snack> 

Logfile Analyse

Im Folgenden wollen wir versuchen, in diesem Logfile mit Hilfe der bereits im tidyverse vorhandenen Werkzeuge im Logfile zum Snackkonsum bei STATWORX zu navigieren und Analysen anzustellen. Zur Analyse unseres Beispiels nutzen wir hier die Pakete xml2, lubridate und dplyr. Laden der Pakete und der Datei snackworx.xml:

library(dplyr) 
library(lubridate) 
library(xml2) 
snacks <- read_xml("snackworx.xml")

In einem ersten Schritt können wir uns einen Überblick über unseren Snack-Log verschaffen:

<SNACKWORX> 
 [1] <snack timestamp="2017-10-17 18:50:09 CEST" colleague="C3PO">\n  <type>Pick Up</type>\n  <grams_sugar>9.8</grams_sugar>\n</snack> 
 [2] <snack timestamp="2017-10-21 14:32:08 CEST" colleague="R2D2">\n  <type>Pick Up</type>\n  <grams_sugar>9.8</grams_sugar>\n</snack> 
 [3] <snack timestamp="2017-09-17 08:50:11 CEST" colleague="Luke">\n  <type>Banana</type>\n  <grams_sugar>20.6</grams_sugar>\n</snack> 
… 

In allen Einträgen ist ein Snacktyp hinterlegt. Eine erste interessante Frage könnte also sein, welche Arten von Snacks es bei STATWORX überhaupt gibt. Hilfreich sind hierfür die xml2-Funktionen xml_find_all() und xml_text() – Erstere durchsucht alle XML-Nodes nach einem String, Zweitere extrahiert den gespeicherten Text:

snacks %>%  
  xml_find_all("//type") %>%  
  xml_text() %>% unique() 
[1] "Pick Up"  "Banana"   "Balisto"  "Apple"    "Snickers" "Mars"

Ähnlich können wir vorgehen, um Attribute zu untersuchen. Wir können beispielsweise mit xml_children(), die Children des obersten XML-Elements abbilden und aus diesen anschließend mit xml_attr() die (anonymisierten) Namen der hungrigen Kollegen extrahieren:

snacks %>% xml_children %>% xml_attr("colleague") %>% unique() 
 [1] "C3PO"   "R2D2"   "Luke"   "Ben"    "Kylo"   "Anakin" "Padme"  "Solo"   "Boba"   "Jabba"  "Chewie" 

XML-Strukturen lassen sich mit as_list() auch bequem in ein Listenobjekt umwandeln – sehr hilfreich, um komplexere XML-Strukturen weiterzuverarbeiten. Nachdem wir unsere ersten Navigationsversuche im XML-Revier gemacht haben, wollen wir nun einen Datensatz erstellen. Letzteres ist der wahrscheinlich wichtigste Punkt bei der Analyse von XML-Prozessdaten: Für interessante Analysen müssen wir vom (meist sehr unübersichtlichen) XML-File zu einem geordneten Datensatz mit Spalten und Reihen gelangen und bereits zu Anfang eine gute Idee haben, wie dieser am Ende (und in den Zwischenschritten) aussehen soll.

Abbildung xml zu dataframe

In unserem einfachen Beispiel werden wir nun die Zeit der Snackaufnahme (snack_time), den Namen des Kollegen (hungry_colleague), die Art des Snacks (meal) und den Zuckergehalt den Snacks (sugar_boost) als Vektoren bilden und anschließend in einem Datensatz zusammensetzen.

snack_time       <- snacks %>% xml_children %>%
                      xml_attr("timestamp") %>%
                      as_datetime(tz = "CET") 
hungry_colleague <- snacks %>%
                      xml_children %>%
                      xml_attr("colleague")  
meal             <- snacks %>%
                      xml_children %>%
                      xml_child("type")
                      %>% xml_text() 
sugar_boost      <- snacks %>%
                      xml_children %>%
                      xml_child("grams_sugar") %>%
                      xml_text() %>% as.numeric() 
snackworx        <- data.frame(hungry_colleague, meal, snack_time, sugar_boost)

Mit dem neuen data.frame snackworx lassen sich nun interessante Analysen anstellen und beispielsweise herausfinden, wen es auch nachts noch einmal an die Süßigkeitenschale drängt oder welcher Mitarbeiter den im Durchschnitt höchsten Zuckerbedarf zu decken hatte. Hierbei kommen uns die lubridate-Funktionen hour() und as_datetime() sehr gelegen:

  
# look at night-time snackers 
snackworx %>%  
filter(hour(snack_time) >  0 & hour(snack_time) < 6)

  hungry_colleague   meal          snack_time sugar_boost 
1              Ben  Apple 2017-10-17 01:50:03        12.8 
2             Kylo   Mars 2017-10-28 05:29:22        37.5 
3            Padme   Mars 2017-10-10 03:08:12        37.5 
4             Solo Banana 2017-09-08 02:50:11        20.6 

# look at October top scorers 
snackworx %>%  
  filter(snack_time > as_datetime("2017-09-30") &
    snack_time < as_datetime("2017-10-31")) %>%
  group_by(hungry_colleague) %>%
  summarise_at(c("sugar_boost"), mean) %>%  
  arrange(desc(sugar_boost)) 

# A tibble: 11 x 2 
   hungry_colleague sugar_boost 
                     
 1           Chewie    37.50000 
 2            Padme    34.04000 
 3           Anakin    27.62000 
 4             C3PO    26.93333 
 5             Kylo    23.85000 
 6             Boba    22.16000 
 7            Jabba    21.35714 
 8             Solo    19.36667 
 9             R2D2    17.47500 
10              Ben    15.05000 
11             Luke    13.48182

Visualisierung

Für Visualisierungen des Snackkonsums steht uns im tidyverse das beliebte Paket ggplot2 zur Verfügung:

library(ggplot2) 
snackworx <- snackworx %>%
               group_by(hungry_colleague) %>%
               mutate(snackcount = n()) 
SnackPlot <- ggplot(snackworx,
                    aes(x = reorder(hungry_colleague, snackcount), fill = meal)) +
               geom_bar(alpha = 0.8) +
               labs(title = "STATWORX October Snack Habits",
                    x = "Hungry Colleague",
                    y = "Number of Snacks",
                    fill = "Meal") 
ggsave("snackplot_gg.jpg", plot = last_plot(), device = NULL,
       width = 12, height = 8, dpi = 300)

Abbildung der gestapelten Balkendiagrammen

ggplot(snackworx, aes(x = hour(snack_time), fill = meal)) + 
  geom_density(alpha = 0.7) +
  labs(title = "Snack Popularity by Day Time",
       x = "Hour of the Day",
       y = "Density",
       fill = "Meal") 
ggsave("snackplot_gg_density.jpg", plot = last_plot(), device = NULL,
       width = 12, height = 8, dpi = 300)

Abbildung der Snack Dichten

Selbstverständlich sind Prozessdaten in der Realität deutlich komplexer gestaltet und erfordern komplexere Strategien, um zu einem zur Analyse geeigneten Datensatz zu gelangen. An dieser Stelle haben wir ein paar wenige, nützliche Tools angesprochen, die den Einstieg in das Verarbeiten von XML-Logfiles erleichtern. Die alte Statistik-Weisheit, im Sinne derer der größte Teil der Arbeit im Ordnen und Bereinigen der Daten besteht, bewahrheitet sich auch bei XML-Prozessdaten.

Referenzen

  1. Hadley Wickham, James Hester and Jeroen Ooms (2017). xml2: Parse XML. R package version 1.1.1. https://CRAN.R-project.org/package=xml2
  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. Garrett Grolemund, Hadley Wickham (2011). Dates and Times Made Easy with lubridate. Journal of Statistical Software, 40(3), 1-25. URL http://www.jstatsoft.org/v40/i03/.
  4. H. Wickham. ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York, 2009.
Ü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.