Data Science, Machine Learning und KI
Kontakt

The tidyverse is making the life of a data scientist a lot easier. That’s why we at STATWORX love to execute our analytics and data science with the tidyverse. Its user-centered approach has many advantages. Instead of the base R version df_test[df_test$x > 10], we can write df_test %>% filter(x>10)), which is a lot more readable – especially if our data pipeline gets more complex and nested. Also, as you might have noticed, we can use the column names directly instead of referencing the Data Frame before. Because of those advantages, we want to use dplyr-verbs for writing our function. Imagine we want to write our own summary-function my_summary(), which takes a grouping variable and calculates some descriptive statistics. Let’s see what happens when we wrap a dplyr-pipeline into a function:

my_summary <- function(df, grouping_var){
 df %>%
  group_by(grouping_var) %>% 
  summarise(
   avg = mean(air_time),
   sum = sum(air_time),
   min = min(air_time),
   max = max(air_time),
   obs = n()
  )
}
my_summary(airline_df, origin)
Error in grouped_df_impl(data, unname(vars), drop) : 
 Column `grouping_var` is unknown 

Our new function uses group_by(), which is searching for a grouping variable grouping_var, and not for origin, as we intended. So, what happened here? group_by() is searching within its scope for the variable grouping_var, which it does not find. group_by() is quoting its arguments, grouping_var in our example. That’s why dplyr can implement custom ways of handling its operation. Throughout the tidyverse, tidy evaluation is used. Therefore we can use column names, as it is a variable. However, our data frame has no column grouping_var.

Non-Standard Evaluation

Talking about whether an argument is quoted or evaluated is a more precise way of stating whether or not a function uses non-standard evaluation (NSE).

– Hadley Wickham

The quoting used by group_by() means, that it uses non-standard evaluation, like most verbs you can find in dplyr. Nonetheless, non-standard evaluation is not only found and used within dplyr and the tidyverse.

non-standard-evaluation

Because dplyr quotes its arguments, we have to do two things to use it in our function:

  • First, we have to quote our argument
  • Second, we have to tell dplyr, that we already have quoted the argument, which we do with unquoting

We will see this quote-and-unquote pattern consequently through functions which are using tidy evaluation.

my_summary <- function(df, grouping_var){
  df %>%
    group_by(!!grouping_var) %>% 
    summarise(
      avg = mean(air_time),
      sum = sum(air_time),
      min = min(air_time),
      max = max(air_time),
      obs = n()
    )
}
my_summary(airline_df, quo(origin))

Therefore, as input in our function, we quote the origin-variable, which means that R doesn’t search for the symbol origin in the global environment, but holds evaluation. The quotation takes place with the quo() function. In order to tell group_by(), that the variable was already quoted we need to use the !!-Operator; pronounced Bang-Bang (if you wondered about the title).

If we are not using !!, group_by() at first searches for the variable within its scope, which are the columns of the given data frame. As mentioned before, throughout the tidyverse, tidy evaluation is used with its eval_tidy()-function. Whereby, it also introduces the concept of data mask, which makes data a first class object in R.

Data Mask

data-mask

Generally speaking, the data mask approach is much more convenient. However, on the programming site, we have to pay attention to some things, like the quote-and-unquote pattern from before.

As a next step, we want the quotation to take place inside of the function, so the user of the function does not have to do it. Sadly, using quo() inside the function does not work.

my_summary <- function(df, grouping_var){
  quo(grouping_var)
  df %>%
    group_by(!!grouping_var) %>% 
    summarise(
      avg = mean(air_time),
      sum = sum(air_time),
      min = min(air_time),
      max = max(air_time),
      obs = n()
    )
}
my_summary(airline_df, origin)
Error in quos(...) : object 'origin' not found 

We are getting an error message because quo() is taking it too literal and is quoting grouping_var directly instead of substituting it with origin as desired. That’s why we use the function enquo() for enriched quotation, which creates a quosure. A quosure is an object which contains an expression and an environment. Quosures redefine the internal promise object into something that can be used for programming. Thus, the following code is working, and we see the quote-and-unquote pattern again.

my_summary <- function(df, grouping_var){
  grouping_var <- enquo(grouping_var)
  df %>%
    group_by(!!grouping_var) %>% 
    summarise(
      avg = mean(air_time),
      sum = sum(air_time),
      min = min(air_time),
      max = max(air_time),
      obs = n()
    )
}
my_summary(airline_df, origin)
# A tibble: 2 x 6
  origin   avg    sum   min   max   obs
  <fct>  <dbl>  <int> <int> <int> <int>
1 JFK     166. 587966    19   415  3539
2 LAX     132. 850259     1   381  6461

All R code is a tree

To better understand what’s happening, it is useful to know that every R code can be represented by an Abstract Syntax Tree (AST) because the structure of the code is strictly hierarchical. The leaves of an AST are either symbols or constants. The more complex a function call gets, the deeper an AST is getting with more and more levels. Symbols are drawn in dark-blue and have rounded corners, whereby constants have green borders and square corners. The strings are surrounded by quotes so that they won’t be confused with symbols. The branches are function calls and are depicted as orange rectangles.

a(b(4, "s"), c(3, 4, d()))
abstract-syntax-tree

To understand how an expression is represented as an AST, it helps to write it in its prefix form.

y <- x * 10
`<-`(y, `*`(x, 10))
prefix-tree

There is also the R package called lobstr, which contains the function ast() to create an AST from R Code.

The code from the first example lobstr::ast(a(b(4, "s"), c(3, 4, d()))) results in this:

lobstr-ast

It looks as expected and just like our hand-drawn AST. The concept of ASTs helps us to understand what is happening in our function. So, if we have the following simple function, !!` introduces a placeholder (promise) for x.

x <- expr(-1)
f(!!x, y)

Due to R’s lazy evaluation, the function f() is not evaluated immediately, but once we called it. At the moment of the function call, the placeholder x is replaced by an additional AST, which can get arbitrary complex. Furthermore, it keeps the order of the operators correct, which is not the case when we use parse() and paste() with strings. So the resulting AST of our code snippet is the following:

promise-tree

Furthermore, !! also works with symbols, functions, and constants.

Perfecting our function

Now, we want to add an argument for the variable we are summarizing to refine our function. At the moment we have air_time hardcoded into it. Thus, we want to replace it with a general summary_var as an argument in our function. Additionally, we want the column names of the final output data frame to be adjusted dynamically, depending on the input variable. For adding summary_var, we follow the quote and unquote pattern from above. However, for the column-naming, we need two additional functions.

Firstly, quo_name(), which converts a quoted symbol into a string. Therefore, we can use normal string operations on it and, e.g. use the base paste command for manipulating it. However, we also need to unquote it, which would be on the Left-Hand-Side, where R is not allowing any computations. Thus, we need the second function, the vestigial operator := instead of the normal =.

my_summary <- function(df, grouping_var, summary_var){
  grouping_var <- enquo(grouping_var)
  summary_var <- enquo(summary_var)
  summary_nm <- quo_name(summary_var)
  summary_nm_avg <- paste0("avg_",summary_nm)
  summary_nm_sum <- paste0("sum_",summary_nm)
  summary_nm_obs <- paste0("obs_",summary_nm)

  df %>%
    group_by(!!grouping_var) %>% 
    summarise(
      !!summary_nm_avg := mean(!!summary_var),
      !!summary_nm_sum := sum(!!summary_var),
      !!summary_nm_obs := n()
    )
}
my_summary(airline_df, origin, air_time)
# A tibble: 2 x 4
  origin avg_air_time sum_air_time obs_air_time
  <fct>         <dbl>        <int>        <int>
1 JFK            166.       587966         3539
2 LAX            132.       850259         6461

Tidy Dots

In the next step, we want to add the possibility to summarize an arbitrary number of variables. Therefore, we need to use tidy dots (or dot-dot-dot) . E.g. if we call the documentation for select(), we get

Usage

select(.data, ...)

Arguments

... One or more unquoted expressions separated by commas.

In select() we can use any number of variables we want to select. We will use tidy dots ... in our function. However, there are some things we have to account for.

Within the function, ... is treated as a list. So we cannot use !! or enquo(), because these commands are made for single variables. However, there are counterparts for the case of .... In order to quote several arguments at once, we can use enquos(). enquos() gives back a list of quoted arguments. In order to unquote several arguments we need to use !!!, which is also called the big bang-Operator. !!! replaces arguments one-to-many, which is called unquote-splicing and respects hierarchical orders.

splicing

With using purrr, we can neatly handle the computation with our list entries provided by ... (for more information ask your Purrr-Macist). So, putting everything together, we finally arrive at our final function.

my_summary <- function(df, grouping_var, ...) {
  grouping_var <- enquo(grouping_var)

  smry_vars <- enquos(..., .named = TRUE)

  smry_avg <- purrr::map(smry_vars, function(var) {
    expr(mean(!!var, na.rm = TRUE))
  })
  names(smry_avg) <- paste0("avg_", names(smry_avg))

  smry_sum <- purrr::map(smry_vars, function(var) {
    expr(sum(!!var, na.rm = TRUE))
  })
  names(smry_sum) <- paste0("sum_", names(smry_sum))

  df %>%
    group_by(!!grouping_var) %>%
    summarise(!!!smry_avg, !!!smry_sum, obs = n())
}

my_summary(airline_df, origin, dep_delay, arr_delay)
# A tibble: 2 x 6
  origin avg_dep_delay avg_arr_delay sum_dep_delay sum_arr_delay   obs
  <fct>          <dbl>         <dbl>         <int>         <int> <int>
1 JFK            12.9          11.8          45792         41625  3539
2 LAX             8.64          5.13         55816         33117  6461

And the tidy evaluation goes on and on

As mentioned in the beginning, tidy evaluation is not only used within dplyr but within most of the packages in the tidyverse. Thus, to know how tidy evaluation works is also helpful if one wants to use ggplot in order to create a function for a styled version of a grouped scatter plot. In this example, the function takes the data, the values for the x and y-axes as well as the grouping variable as inputs:

scatter_by <- function(.data, x, y, z=NULL) {
  x <- enquo(x)
  y <- enquo(y)
  z <- enquo(z)

  ggplot(.data) + 
    geom_point(aes(!!x, !!y, color = !!z)) +
    theme_minimal()
}
scatter_by(airline_df, distance, air_time, origin) 
scatter-1

Another example would be to use R Shiny Inputs in a sparklyr-Pipeline. input$ cannot be used directly within sparklyr, because it would try to resolve the input list object on the spark side.

server.R

library(shiny)
library(dplyr)
library(sparklyr)

# Define server logic required to filter numbers
shinyServer(function(input, output) {
    tbl_1 <- tibble(a = 1:5, b = 6:10)
    sc <- spark_connect(master = "local")

    tbl_1_sp <-
        sparklyr::copy_to(
            dest = sc,
            df = tbl_1,
            name = "tbl_1_sp",
            overwrite = TRUE
        )

    observeEvent(input$select_a, {

        number_b <- tbl_1_sp %>%
            filter(a == !!input$select_a) %>%
            collect() %>%
            pull()

        output$text_b <- renderText({
            paste0("Selected number : ", number_b)
        })
    })
})

ui.R

library(shiny)
library(dplyr)
library(sparklyr)


# Define UI for application t
shinyUI(fluidPage(
    # Application title
    titlePanel("Select Number Example"),

    # Sidebar with a slider input for number
    sidebarLayout(sidebarPanel(
        sliderInput(
            "select_a",
            "Number for 1:",
            min = 1,
            max = 5,
            value = 1
        )
    ),

    # Show a text as output
    mainPanel(textOutput("text_b")))
))

Conclusion

There are many use cases for tidy evaluation, especially for advanced programmers. With the tidyverse getting bigger by the day, knowing tidy evaluation gets more and more useful. For getting more information about the metaprogramming in R and other advanced topics, I can recommend the book Advanced R by Hadley Wickham.

[author class=“mtl” title=“Über den Autor”]

Introduction

At STATWORX we love beautiful plots. One of my favorite plotting libraries is plotly. It’s being developed by the company of the same name since 2012. Plotly.js is a high-level javascript library for interactive graphics and offers wrappers for a diverse range of languages, like Python, R or Matlab. Furthermore, it is open source and licensed under the MIT license, therefore it can be used in a commercial context. Plotly offers more than 30 different chart types. Another reason we at STATWORX use Plotly extensively is that it can be easily integrated into web-based frameworks, like Dash or R Shiny.

How does it work?

A Plotly plot is based on the following three main elements: Data, Layout and Figure.

Data

The Data object can contain several traces. For example, in a line chart with several lines, each line is represented by a different trace. According to that, the data object contains the data which should be plotted but also the specification of how the data should be plotted.

Layout

The Layout object defines everything, that is not related to the data. It contains elements, like the title, axis titles or background-color. However, you can also add annotations or shapes with the layout object.

Figure

The Figure object includes both data and layout. It creates our final figure for plotting, which is just a simple dictionary-like object. All figures are built with plotly.js, so in the end, the Python API only interacts with the plotly.js library.

Application

Let’s visualize some data. For that purpose we will use the LA Metro Bike Share dataset, which is hosted by the city of Los Angeles and contains anonymized Metro Bike Share trip data. In the following section we will use Plotly for Python and compare it later on with the R implementation.

Creating our first line plot with Plotly

First, we will generate a line plot which shows the number of rented bikes over different dates differentiated over the passholder type. Thus, we first have to aggregate our data before we can plot it. As shown above, we define our different traces. Each trace contains the number of rented bikes for a specific passholder type. For line plots, we use the Scatter()– function from plotly.graph_objs. It is used for scatter and line plots, however, we can define how it is displaced by setting the mode parameter accordingly. Those traces are unified as a list in our data object. Our layout object consists of a dictionary, where we define the main title and the axis titles. At last, we put our data and layout object together as a figure-object.

import pandas as pd
import plotly.graph_objs as go
import plotly.plotly as py

df = pd.read_pickle(path="LA_bike_share.pkl")

rental_count = df.groupby(["Start_Date", "Passholder_Type"]).size().reset_index(name ="Total_Count")

trace0 = go.Scatter(
    x=rental_count.query("Passholder_Type=='Flex Pass'").Start_Date,
    y=rental_count.query("Passholder_Type=='Flex Pass'").Total_Count,
    name="Flex Pass",
    mode="lines",
    line=dict(color="#013848")
)
trace1 = go.Scatter(
    x=rental_count.query("Passholder_Type=='Monthly Pass'").Start_Date,
    y=rental_count.query("Passholder_Type=='Monthly Pass'").Total_Count,
    name="Monthly Pass",
    mode="lines",
    line=dict(color="#0085AF")
)
trace2 = go.Scatter(
    x=rental_count.query("Passholder_Type=='Walk-up'").Start_Date,
    y=rental_count.query("Passholder_Type=='Walk-up'").Total_Count,
    name="Walk-up",
    mode="lines",
    line=dict(color="#00A378")
)
data = [trace0,trace1,trace2]

layout = go.Layout(title="Number of rented bikes over time",
                   yaxis=dict(title="Number of rented bikes", 
                              zeroline=False),
                   xaxis=dict(title="Date",
                              zeroline = False)
                  )

fig = go.Figure(data=data, layout=layout)

Understanding the structure behind graph_objs

If we output the figure-object, we will get the following dictionary-like object.

Figure({
    'data': [{'line': {'color': '#013848'},
              'mode': 'lines',
              'name': 'Flex Pass',
              'type': 'scatter',
              'uid': '5d8c0781-4592-4d19-acd9-a13a22431ccd',
              'x': array([datetime.date(2016, 7, 7), datetime.date(2016, 7, 8),
                          datetime.date(2016, 7, 9), ..., datetime.date(2017, 3, 29),
                          datetime.date(2017, 3, 30), datetime.date(2017, 3, 31)], dtype=object),
              'y': array([ 61,  93, 113, ...,  52,  36,  40])},
             {'line': {'color': '#0085AF'},
              'mode': 'lines',
              'name': 'Monthly Pass',
              'type': 'scatter',
              'uid': '4c4c76b9-c909-44b7-8e8b-1b0705fa2491',
              'x': array([datetime.date(2016, 7, 7), datetime.date(2016, 7, 8),
                          datetime.date(2016, 7, 9), ..., datetime.date(2017, 3, 29),
                          datetime.date(2017, 3, 30), datetime.date(2017, 3, 31)], dtype=object),
              'y': array([128, 251, 308, ..., 332, 312, 301])},
             {'line': {'color': '#00A378'},
              'mode': 'lines',
              'name': 'Walk-up',
              'type': 'scatter',
              'uid': '8303cfe0-0de8-4646-a256-5f3913698bd9',
              'x': array([datetime.date(2016, 7, 7), datetime.date(2016, 7, 8),
                          datetime.date(2016, 7, 12), ..., datetime.date(2017, 3, 29),
                          datetime.date(2017, 3, 30), datetime.date(2017, 3, 31)], dtype=object),
              'y': array([  1,   1,   1, ..., 122, 133, 176])}],
    'layout': {'title': {'text': 'Number of rented bikes over time'},
               'xaxis': {'title': {'text': 'Date'}, 'zeroline': False},
               'yaxis': {'title': {'text': 'Number of rented bikes'}, 'zeroline': False}}
})

In theory, we could build those dictionaries or change the entries by hand without using plotly.graph_objs. However, it is much more convenient to use graph_objs than to write dictionaries. In addition, we can call help on those functions and see which parameters are available for which chart type and it also raises an error with more details if something went wrong. There is also the possibility to export the fig-figure object as a JSON and import it for example in R.

Displaying our plot

Nonetheless, we don’t want a JSON-File but rather an interactive graph. We now have two options, either we publish it online, as Plotly provides a web-service for hosting graphs including a free plan, or we create the graphs offline. This way, we can display them in a jupyter notebook or save them as a standalone HTML.

In order to display our plot in a jupyter notebook, we need to execute the following code

from plotly.offline import iplot, init_notebook_mode
init_notebook_mode(connected=True)

at the beginning of each Notebook. Finally, we can display our plot with iplot(fig).

Before publishing it online, we first need to set our credentials with

plotly.tools.set_credentials_file(username='user.name', api_key='api.key')

and use py.plot(fig, filename = 'basic-plot', auto_open=True) instead of iplot(fig). The following graph is published online on Plotly’s plattform and embedded as an inline frame.

The chart above is fully interactive, which has multiple advantages:

  • Select and deselect different lines
  • Automatical scaling of the y-scale in case of deselected lines
  • Hover-informations with the exact numbers and dates
  • Zoom in and out with self-adjusting date ticks
  • Different chart-modes and the ability to toggle additional options, like spike lines
  • Possibility to include a range-slider or buttons

The graph shows a fairly clear weekly pattern, with Monthly Passholders having their high during the workweek, while Walk-ups are more active on the weekend. Apart from some unusual spikes, the number of rented bikes is higher for Monthly Passholders than for Walk-ups.

Visualizing the data as a pie chart

The next question is: how does the total duration look for the different passholder types? First, we need to aggregate our data accordingly. This time we will build a pie chart in order to get the share of the total duration for each passholder type. As we previously did with the line chart, we must first generate a trace object and use Pie() from graph_objs. The arguments we use are different now: we have labels and values instead of x and y. We’re also able to determine, which hover-information we want to display and can add with hovertext custom information, or completely customize it with hovertemplate. Afterward, the trace object goes into go.Figure() in form of a list.

share_duration = df.groupby("Passholder_Type").sum().reset_index()
colors = ["#013848", "#0085AF", "#00A378"]
trace = go.Pie(labels=share_duration.Passholder_Type,
               values=share_duration.Duration,
               marker=dict(colors=colors,
                           line=dict(color='white', width=1)),
               hoverinfo="label+percent"
              )
fig = go.Figure(data=[trace])

The pie chart shows us, that 59% of the total duration is caused by Walk-ups. Thus, we could assume that the average duration for Walk-ups is higher than for Monthly Passholders.

There is one more thing: figure factory

Now, let’s plot the distribution of the average daily duration. For that we use the create_distplot()-function from the figure_factory. The figure factory module contains wrapper functions that create unique chart types, which are not implemented in the nativ plotly.js library, like bullet charts, dendrograms or quiver plots. Thus, they are not available for other languages, like R or Matlab. However, those functions also deviate from the structure for building a Plotly graph we discussed above and are also not consistent within figure_factory. create_distplot() creates per default a plot with a KDE-curve, histogram, and rug, respectively those plots can be removed with show_curve, show_hist and show_rug set to False. First, we create a list with our data as hist_data, in which every entry is displayed as a distribution plot on its own. Optionally, we can define group labels, colors or a rug text, which is displayed as hover information on every rug entry.

import plotly.figure_factory as ff

mean_duration=df.groupby(["Start_Date", "Passholder_Type"]).mean().reset_index()

hist_data = [mean_duration.query("Passholder_Type=='Flex Pass'").Duration,
             mean_duration.query("Passholder_Type=='Monthly Pass'").Duration,
             mean_duration.query("Passholder_Type=='Walk-up'").Duration]

group_labels = ["Flex Pass", "Monthly Pass", "Walk-up"]

rug_text = [mean_duration.query("Passholder_Type=='Flex Pass'").Start_Date,
            mean_duration.query("Passholder_Type=='Monthly Pass'").Start_Date,
            mean_duration.query("Passholder_Type=='Walk-up'").Start_Date]

colors = ["#013848", "#0085AF", "#00A378"]


fig = ff.create_distplot(hist_data, group_labels, show_hist=False, 
                         rug_text=rug_text, colors=colors)

As we assumed, Walk-ups have a higher average duration than monthly or flex passholders. The average daily duration for Walk-ups is peaking at around 0.6 hours and for Monthly and Flex Passholders already at 0.18, respectively 0.2 hours. Also, the distribution for Walk-ups is much flatter with a fat right tail. Thanks to the rug, we can see that for Flex Pass, there are some days with a very high average duration and due to the hover-information, we can immediately detect, which days have an unusually high average renting duration. The average duration on February 2, 2017, was 1.57 hours. Next, we could dig deeper and have a look on the possible reasons for such an unusual activity, for example a special event or the weather.

Plotly with R

As mentioned in the beginning, Plotly is available for many languages. At STATWORX, we’re using Plotly mainly in R, especially if we’re creating a dashboard with R Shiny. However, the syntax is slightly different, as the R implementation utilizes R’s pipe-operator. Below, we create the same barplot in Python and in R. In Python, we aggregate our data with pandas, create different traces for every unique characteristic of Trip Route Category, specify that we want to create a stacked bar chart with our different traces and assemble our data and layout object with go.Figure().

 total_count = df.groupby(["Passholder_Type", "Trip_Route_Category"]).size().reset_index(name="Total_count")

 trace0 = go.Bar(
   x=total_count.query("Trip_Route_Category=='Round Trip'").Passholder_Type,
   y=total_count.query("Trip_Route_Category=='Round Trip'").Total_count,
   name="Round Trip",
   marker=dict(color="#09557F"))
trace1 = go.Bar(
   x=total_count.query("Trip_Route_Category=='One Way'").Passholder_Type,
   y=total_count.query("Trip_Route_Category=='One Way'").Total_count,
   name="One Way",
   marker=dict(color="#FF8000"))
data = [trace0, trace1]

layout = dict(barmode="stack")

fig = go.Figure(data=data, layout=layout)

With R, we can aggregate the data with dplyr and already start our pipe there. Afterward, we pipe the plotly function to it, in the same way we already specified which data frame we want to use. Within plot_ly(), we can directly address the column name. We don’t have to create several traces and add them with add_trace(), but can define the separation between the different Trip Route Category with the color argument. In the end, we pipe the layout()-function and define it as a stacked bar chart. Thus, with using the pipe-operator, the code looks slightly tidier. However, in comparison to the Python implementation, we are losing the neat functions of the figure factory.

basic_bar_chart <- df %>% 
  group_by(Passholder_Type, Trip_Route_Category) %>% 
  summarise( Total_count = n()) %>%
  plot_ly(x = ~Passholder_Type, 
          y = ~Total_count,
          color = ~Trip_Route_Category , 
          type = 'bar', 
          marker=list(color=c(rep("#FF8000",3),rep("#09557F",3)))) %>%
  layout( barmode = 'stack')

The bar plot shows that Walk-ups use their rented bikes more often for Round Trips in comparison to Monthly Passholders, which could be a reason for their higher average duration.

Conclusion

I hope I could motivate you to have a look at interactive graphs with Plotly instead of using static seaborn or ggplot plots, especially in case of hands-on sessions or dashboards. But there is also the possibility to create an interactive Plotly chart from a ggplot or Matplotlib object with one additional line of code.

With version 3.0 of plotly.py there have been many interesting new features like Jupyter Widgets, the implementation of imperative methods for creating a plot and the possibility to use datashader. Soon you’ll find a blog post on here on how to implement zoomable histograms with Plotly and Jupyter Widgets and why automatic rebinning makes sense by a colleague of mine.

[author class=“mtl“ title=“Über den Autor“]

Daten-Visualisierung und -Verständnis sind wichtige Faktoren bei der Durchführung eines Data Science Projekts. Eine visuelle Exploration der Daten unterstützt den Data Scientist beim Verständnis der Daten und liefert häufig wichtige Hinweise über Datenqualität und deren Besonderheiten. Bei STATWORX wenden wir im Bereich Datenvisualsierung eine Vielzahl von unterschiedlichen Tools und Technologien an, wie z.B. Tableau, Qlik, R Shiny oder D3. Seit 2014 hat QlikTech zwei Produkte im Angebot: QlikView und Qlik Sense. Für viele unserer Kunden ist die Entscheidung, ob sie QlikView oder QlikSense einsetzen sollen nicht einfach zu treffen. Was sind die Vor- und Nachteile der beiden Produkte? In diesem Blogbeitrag erläutern wir die Unterschiede zwischen den beiden Tools.

sense-vs-view

Geschichte und Aufbau von Qlik

Die Erfolgsgeschichte der Firma QlikTech begann in den 90er Jahren mit dem Produkt QlikView, das im Jahre 2014 durch QlikSense offiziell abgelöst werden sollte. Aktuell sind noch beide Produkte am Markt, QlikTech fokussiert sich jedoch stark auf die Weiterentwicklung von QlikSense. Zwischen den Veröffentlichungsterminen der beiden Produkte stieg die Anzahl der Mitarbeiter von QlikTech von 35 auf über 2000. Beiden Produkten liegt dieselbe Kerntechnologie zu Grunde; die Qlik Associative Engine. Diese ermöglicht eine einfache Verknüpfung von unterschiedlichen Datensätzen, die alle In-Memory (im Arbeitsspeicher des Rechners) gehalten werden. Dies ermöglicht einen extrem schnellen Datenzugriff, da der Arbeitsspeicher gegenüber normalen Datenträgern eine erheblich schnellere Lese- und Schreibgeschwindigkeit aufweist. Weiterhin kann die Qlik Associative Engine viele Nutzer und große Datenmengen managen was einer der Hauptgründe ist, warum sich Qlik erfolgreich gegen seine Mitbewerber durchsetzen konnte.

QlikView

Qlikview

QlikView orientiert sich grundsätzlich am Thema „Guided Analytics“. Hierbei wird dem Endanwender eine App von einem QlikView-Entwickler bereitgestellt, die die notwendigen Überlegungen und Implementierungen rund um das Datenmodell, den inhaltlichen und optischen Aufbau der App sowie die unterschiedlichen Visualisierunge enthält. Der Anwender wiederum hat die komplette Freiheit die Daten durch Filtern, Auswählen, Drill-Down und Cycle Groups zu erkunden, um neue Erkentnisse zu gewinnen und Antworten auf seine Business-Fragen zu finden. Hierbei steht allerdings das Erstellen von eigenen Visualisierungen für den Endanwender nicht im Fokus.
Die Entwicklung von QlikView Applikationen erfordert Erfahrung und Expertise, da die Dashboarderstellung nicht über Drag und Drop möglich ist. Der Endanwender erhält hingegen eine fertige und einsatzbereite Applikation und kann somit umgehend mit seinen BI Analysen starten.
Die fertigen Datenmodelle und die daraus resultierende QVDs können ebenfalls von Qlik Sense geöffnet werden, dies ist allerdings andersherum nicht möglich.

Weitere wichtige Eckpunkte zu QlikView sind:

  • eine Vielzahl an unterschiedlichen Datenverbindungsmöglichkeiten vorhanden
  • keine Cloud-Lösung erhältlich, QlikView läuft lokal oder auf einem On-Premise Server
  • benutzerfreundliche Entwicklungsoberfläche
  • baut auf C++ und C# auf
  • PDF Reporting durch NPrinting möglich
  • Pixel genaues Erstellen von Applikationen
  • 2000er Retro-Charme
  • schnelle Entwicklungsprozesse und einfache Anpassungsmöglichkeiten

Qlik Sense

Qliksense

Qlik Sense wurde mit dem Fokus auf Self-Service BI entwickelt und ist Qliks Antwort auf den größten Mitbewerber Tableau. Hierbei wird der Anwender der Applikation weniger gerichtet geleitet sondern ihm die Möglichkeit gegeben seine eigenen Daten zu integrieren, um Apps selbständig zu kreieren. Einer der Vorteile von Self-Service BI ist, dass der Anwender eigenständig neue Visualisierungen erstellen kann, die sich konkret an seinen Fragestellungen orientieren. Allerdings erfordert dies engagierte und neugierige Anwender, die Lust haben ihre Daten zu erkunden. Tools wie QlikSense vereinfachen den Prozess bei der Erstellung von Visualisierungen erheblich, sodass auch unerfahrene Anwender innerhalb kürzester Zeit sinnvolle Darstellungen aus ihren Daten generieren können.
Die Erstellung von Visualisierungen und Layout erfolgt einfach über Drag & Drop. Kennzahlen und Dimensionen können ebenfalls in die Visualisierung gezogen werden. Im Gegensatz zu QlikView stehen nativ moderne Datenvisualisierungsmöglichkeiten zu Verfügung wie beispielsweiße Karten für Geoanalysen.
Jedoch spielen Qlik Experten weiterhin eine wichtige Rolle, da falls es sich nicht um Ad-hoc Analysen handelt eine Anbindung an die bestehende Dateninfrastruktur notwendig ist. Folglich ist es bei großen Datenmengen ebenfalls wichtig, dass das Datenmodell effizient und performant ausgestalltet sind. Um unterschiedliche Berechnung von KPIs zu vermeiden und die Kommunikation von widersprüchlichen Ergebnissen zwischen Abteilungen zu verhindern ist die Bereitstellung von Masterkennzahlen und Dimensionen entscheident. Zusätzlich vermeidet dies die ineffiziente Berechnung von Kennziffern, was bei großen Datenmengen an Bedeutung gewinnt bezüglich Ladezeit. Oft sind komplexere Analysen notwendig, welche nicht durch Drag & Drop möglich sind, sondern Erfahrung und Expertise in weitergehende Funktionen wie Set Analysen erfordert. Nur dadurch kann das volle Potential von Qlik Sense ausgeschöpft werden.
Im Vergleich zu QlikView ist Qlik Sense benutzerfreundlicher gestaltet worden, allerdings schränkt dies die Individualisierungsmöglichkeiten ein, was für erfahrene QlikView-Benutzer beim Umstieg frustrierend sein kann.

Weiterhin gibt es aus der Anwendung von QlikSense heraus noch folgende wichtige Punkte:

  • Vielzahl von möglichen Datenverbindungen und Integration eines Data Marketplace
  • Cloud Option vorhanden
  • vereinfachtes Lizenzmodell
  • kostenfreie Desktop-Version
  • PDF Reporting durch NPrinting
  • verwendet HTML und JavaScript als Grundlage
  • anwenderfreundliche, offene API
  • Vielzahl an kostenlosen und kostenpflichtigen Extensions, allerdings ist hierbei nicht immer eine Kompatibilität mit der nächsten Qlik Sense Version gewährleistet
  • responsive, dadurch geeignet für mobile Devices und touch-freundliche Bedienung

Fazit

Schaut man sich die letzten Updates von QlikView und Qlik Sense an, erkennt man deutlich, dass der Fokus von QlikTech auf Qlik Sense liegt. QlikView soll weiterhin mit Updates versorgt werden, allerdings wird es vorrausichtlich keine neuen Features geben, diese bleiben Qlik Sense vorbehalten. Lange Zeit war der grundlegende Funktionsumfang gleich und die größten Unterschiede gab es hinsichtlich Bedienung und Visualisierungsmöglichkeiten. Allerdings gibt es beispielsweise die neuen In… Funktionen, die Year-to-date Berechnungen vereinfachen, nur in Qlik Sense.

Die Entwicklung von komplexeren Dashboards gestaltet sich unter Qlik Sense im Vergleich zu QlikView als schwieriger. Hier spielt QlikView eindeutig seine Stärke aus. Allerdings erweitern sich die Möglichkeiten diesbezüglich für Qlik Sense mit jedem Update. In den neusten Versionen wurden Optionen integriert um die Grid-Size anzupassen, die Seitenlänge zu erweitern und Scrollen zu ermöglichen oder ein Dashboard mit CI-konformen Farben zu gestalten mittels Custom-Themes. Außerdem bietet Qlik Sense für komplexe Dashboards die Möglichkeit Mashups zu erstellen. Hierbei handelt es sich um Webseiten oder Applikationen in die Qlik Sense Objekte integriert werden. Die Lücke zu QlikView hinsichtlich der Erstellung von Dashboards verkleinert sich somit stetig.

Beide Lösungen werden noch für längere Zeit ihre Daseinsberechtigung haben. Daher wird es für manche Unternehmen weiterhin Sinn machen QlikView und Qlik Sense parallel zu Betreiben und sich anwendungsbezogen für ein Produkt zu entscheiden.

Infografik

Machine Learning (ML) is still an underdog in the field of economics. However, it gets more and more recognition in the recent years. One reason for being an underdog is, that in economics and other social sciences one is not only interested in predicting but also in making causal inference. Thus many „off-the-shelf“ ML algorithms are solving a fundamentally different problem. We here at STATWORX are also facing a variety of problems e.g. dynamic pricing optimization.

„Prediction by itself is only occasionally sufficient. The post office is happy with any method that predicts correct addresses from hand-written scrawls…[But] most statistical surveys have the identification of causal factors as their ultimate goal.“ – Bradley Efron

Introduction

However, the literature of combining ML and casual inferencing is growing by the day. One common problem of causal inference is the estimation of heterogeneous treatment effects. So, we will take a look at three interesting and different approaches for it and focus on a very recent paper by Athey et al. which is forthcoming in „The Annals of Statistics“1.

Model-based Recursive Partitioning

One of the earlier papers about causal trees is by Zeileis et al., 20082. They describe an algorithm for Model-based Recursive Partitioning (MOB), which looks at recursive partitioning for more complex models. They fit at first a parametric model to the data set, while using Maximum-Likelihood, then test for parameter instability for a set of predefined variables and lastly split the model with the variable regarding the highest parameter instability. Those steps are repeated in each of the daughter nodes till a stopping criterion is reached. However, they do not provide statistical properties for the mob and the partitions are still quite large.

Bayesian Additive Regression Tree

Another paper uses Bayesian Additive Regression Tree (BART) for the estimation of heterogeneous treatment effects3. Hereby, one advantage of this approach is, that BART can detect and handle interactions and non-linearity in the response surface. It uses a Sum-of-Tree Model. First, a weak-learning tree is grown, whereby the residuals are calculated and the next tree is fitted according to these residuals. Similar to Boosting Algorithms, BART wants do avoid overfitting. This is achieved by using a regularization prior, which restricts overfitting and the contribution of each tree to the final result.

Generalized Random Forest

However, this and the next blog post will be mainly focused on the Generalized Random Forest (GRF) by Athey et al., who have already been exploring the possibilities of ML in economics before. It is a method for non-parametric statistical estimation, which uses the basic ideas of the Random Forest. Therefore, it keeps the recursive partitioning, subsampling and random split selection. Nevertheless, the final outcome is not estimated via simple averaging over the trees. The Forest is used to estimate an adaptive weighting function. So, we grow a set of trees and each observation gets weighted equalling how often it falls into the same leaf as the target observation. Those weights are used to solve a „local GMM“ model.

Another important piece of the GRF is the split selection algorithm, which emphasizes maximizing heterogeneity. With this framework, a wide variety of applications is possible like quantile regressions but also the estimation of heterogeneous treatment effects. Therefore, the split selection must be suitable for a lot of different purposes. As in Breiman’s Random Forest, splits are selected greedily. However, in the case of general moment estimation, we don’t have a direct loss criterion to minimize. So instead we want to maximize a criterion ∆ , which favors splits that are increasing the heterogeneity of our in-sample estimation. Maximizing ∆ directly on the other side would be computationally costly, therefore Athey et al. are using a gradient-based approximation for it. This results in a computational performance, similar to standard CART- approaches.

Comparing the regression forest of GRF to standard random forest

Athey et al. are claiming in their paper that in the special case of a regression forest, the GRF gets the same results as the standard random forest by Breiman (2001). So, one already implemented estimation method in the grf-package4 is a regression forest. Therefore, I will compare those results, with the random forest implementations of the randomForest-package as well as the implementation of the ranger-packages. For tuning porpuses, I will use a random search with 50 iterations for the randomForest and ranger-package and for the grf the implemented tune_regression_forest()-function. The Algorithms will be benchmarked on 3 data sets, which have been already used in another blog post, while using the RMSE to compare the results. For easy handling, I implemented the regression_forest() into the caret framework, which can be found on my GitHub.

Data Set Metric grf ranger randomForest
air RMSE 0.25 0.24 0.24
bike RMSE 2.90 2.41 2.67
gas RMSE 36.0 32.6 34.4

The GRF performs a little bit worse in comparison with the other implementations. However, this could be also due to the tuning of the parameters, because there are more parameters to tune. According to their GitHub, they are planning on improving the tune_regression_forest()-Function.
One advantage of the GRF is, that it produces unbiased confidence intervals for each estimation point. In order to do so, they are performing honest tree splitting, which was first described in their paper about causal trees5. With honest stree splitting, one sample is used to make the splits and another distinct sample is used to estimate the coefficients.

However, standard regression is not the exciting part of the Generalized Random Forest. Therefore, I will take a look at how the GRF performs in estimating heterogeneous treatment effects with simulated data and compare it to the estimation results of the MOB and the BART in my next blog post.

References

  1. Athey, Tibshirani, Wager. Forthcoming.“Generalized Random Forests“
  2. Zeileis, Hothorn, Hornik. 2008.“Model-based Recursive Partitioning“
  3. Hill. 2011.“Bayesian Nonparametric Modeling for Causal Inference“
  4. https://github.com/swager/grf
  5. Athey and Imbens. 2016.“Recursive partitioning for heterogeneous causal effects.“

„All code is guilty until proven innocent.“

Testing ist ein wichtiger Teil in der Entwicklung von stabilem R Code. Testing stellt sicher, dass der Code wie beabsichtigt funktioniert. Allerdings ist dies ein zusätzlicher Schritt im bisherigen Workflow. Oft besteht der übliche „Testing“-Workflow in R darin, nach dem Schreiben einer neuen Funktion, diese zuerst informell in der Konsole zu testen und zu schauen, ob der Code wie angestrebt funktioniert. Dieser Beitrag soll aufzeigen, wie man mit Hilfe des testthat-Packages strukturierte Unit-Tests schreibt.

Motivation für Unit Testing

  • Geringere Anzahl an Bugs: Dadurch, dass das Verhalten des Codes an zwei Stellen festgehalten wird – einmal im Code selbst und einmal in den Tests – kann man sicherstellen, dass er wie beabsichtigt funktioniert und dadurch im besten Fall keine Fehler im Code sind. Inbesondere kann es nützlich sein, im Falle eines behobenen Bugs im Anschluss einen entsprechenden Test zu schreiben, welcher den Bug identifiziert hätte. Dies stellt sicher, dass wenn man sich nach einiger Zeit dem Code widmet, nicht einen alten Fehler erneut hinzufügt.
  • Bessere Code-Struktur: Für sinnvolle Tests ist es wichtig, dass der Code übersichtlich gestaltet ist. Damit dieser gut getestet werden kann, ist es hilfreich, anstelle von einer komplexen, verschachtelten Funktion, den Code in mehrere, simplere Funktionen aufzuteilen. Hierdurch wird die Fehleranfälligkeit zusätzlich verringert.
  • Robuster Code: Da die komplette Funktionalität des Codes schon einmal überprüft worden ist, kann man einfacher größere Änderungen am Code vornehmen ohne in diesen (unabsichtlich) Fehler einzubauen. Dies ist vor allem hilfreich, wenn man zu einem späteren Zeitpunkt denkt, dass es einen effizienteren Weg gibt, um dies zu bewerkstelligen, aber einen zuvor berücksichtigten Randfall vergisst.

Package: testthat

Ein fantastisches Paket in R für Unit-Testing ist das testthat-Paket von Hadley Wickham. Kennt man sich mit Testing aus anderen Programmiersprachen aus, wird man feststellen, dass es einige signifikante Unterschiede gibt. Dies liegt zum größten Teil daran, dass es sich bei R stärker um eine funktionale Programmiersprache handelt, als um eine objektorientierte Programmiersprache. Daher macht es wenig Sinn, Tests um Objekte und Methoden zu bauen, anstatt um Funktionen.
Eine Alternative zu testthat ist das RUnit-Paket, wobei einer der Vorteile von testthat ist, dass es aktiv weiterentwickelt wird.
Der generelle Testaufbau mit testthat ist, dass mehrere zusammenhängende Expectations in ein test_that-Statement zusammengefügt werden, welches wiederrum in ein context-File gruppiert wird.

Workflow mit Beispielen

Im Folgenden werden wir die Funktion im Skript quadratic_function.R testen, welche die Nullstellen einer quadratischen Gleichung berechnet. Dieses Skript, sowie die Tests sind ebenfalls auf unserer Github-Seite zu finden.

quadratic_equation <- function(a, b, c)
{
  if (a == 0)
    stop("Leading term cannot be zero")
  # Calculate determinant
  d <- b * b - 4 * a * c
  
  # Calculate real roots
  if (d < 0)
    rr <- c()
  else if (d == 0)
    rr <- c(-b / (2 * a))
  else
    rr <- c((-b - sqrt(d)) / (2 * a), 
            (-b + sqrt(d)) / (2 * a))
  
  return(rr)
}

Zu Beginn eines Test-Skriptes wird das jeweilige R-Skript, welches die zu testenden Funktionen enthält, geladen.

source("quadratic_function.R")

Der Name des Test-Skripts muss mit test beginnen und ist strukturell das höchste Element im Testing. Jedes File sollte einen context()Aufruf beinhalten, welches eine kurze Beschreibung über den Inhalt zur Verfügung stellt. Hierbei sollte man beachten, dass man ein gesundes Mittelmaß für den Umfang eines jeden Files findet. Es ist schlecht, wenn die gesamten Tests für ein komplexes Paket sich in einem File befinden, aber gleichermaßen, wenn jeder Test sein eigenes File hat. Oft ist es eine gute Idee, dass jede komplexe Funktion sein eigenes File besitzt.

Expectations

Expectations stellen das kleinste Element dar und beschreiben, was das erwartete Ergebnis einer Berechnung ist, beispielsweise die richtige Klasse oder Wert. Expectations sind Funktionen, die mit expect_ beginnen.

# Expectations
calculated_root <- quadratic_equation(1, 7, 10)

expect_is(calculated_root, "numeric")
expect_length(calculated_root, 2)
expect_lt(calculated_root[1], calculated_root[2])

Es gibt verschiedene, vordefinierte expect_-Funktionen, welche unterschiedliche Bedingungen überprüfen. Beispielsweise überprüft die erste Expectation ob der zurückgegebenen Werte numerisch ist, die zweite ob zwei Wurzeln zurückgegeben werden und die dritte, ob die erste berechnete Wurzel kleiner als die zweite ist. Es besteht ebenfalls die Möglichkeit, mit expect() eigene Expectations zu schreiben, falls eine Expectation häufiger verwendet wird oder mit expect_true() einfache True/Falls Bedingungen zu überprüfen. Letzteres sollte allerdings nur verwendet werden, falls es keine vordefinierte Expectation gibt, da diese eine bessere Fehlerbeschreibung beinhalten als expect_true(). Außerdem sollte beachtet werden, dass expect_that() veraltet ist und nicht mehr benutzt werden sollte.

Tests

Ein Test verbindet mehrere Expectation um beispielsweiße die Ausgabe einer simplen Funktion, eine Reihe von möglichen Eingabewerten einer komplexeren Funktion oder stark verbundene Funktionalitäten von mehreren unterschiedlichen Funktionen zu testen. Daher wird diese Art von Tests Unit-Tests genannt, da sie eine Einheit (Unit) der Funktionalität überprüfen.
Ein neuer Test wird mit Hilfe der test_that()-Funktion kreiert und beinhaltet den Testname sowie einen Codeblock. Hierbei sollte der Testname den Satz „Test that…“ beenden. Außerdem sollte die Beschreibung informativ gehalten werden, damit ein möglichen Fehler zügig gefunden werden kann auch für den Fall, dass man längere Zeit nicht damit gearbeitet hat. Zusätzlich sollte ein Test einen nicht zu großen Bereich an Expectations abdecken, was eine schnelle Lokalisierung des Fehlers ermöglicht.

# Beispiel test_that
calculated_root <- quadratic_equation(1, 7, 10)

expect_is(calculated_root, "numeric")
expect_length(calculated_root, 2)
expect_lt( calculated_root[1], calculated_root[2])

In diesem Beispiel haben wir die obigen Expectations in einen neuen Test zusammengefast, welcher testet, ob die Funktion distinkte Werte zurückgibt.

Einzelne Testfiles können mit test_file() aufgerufen werden und mehrere Tests, deren Dateinamen mit test_ beginnen und sich im gleichen Ordner befinden mit test_dir(). Dies ermöglicht im Gegensatz zu einem einfachen source(), dass die weiteren Tests ebenfalls durchlaufen, falls ein vorheriger abbricht.
Bevor wir die Tests einmal durchlaufen lassen, habe ich noch weitere hinzugefügt, unter anderem einen Test welcher absichtlich nicht passiert. Dieser testet ob die Funktion eine Warnung ausgibt, falls a = 0 gesetzt wurde. Allerdings wurde in der Funktion zuvor eine error-Ausnahme definiert statt einer warning. Über die Handhabung von Ausnahmen dreht sich auch mein vorheriger Blogbeitrag.
Die Ausgabe für die Tests sieht folgendermaßen aus:

unit test output

Ein grüner Punkt bedeutet, dass der jeweilige Test erfolgreich bestanden wurde. Eine rote Zahl hingegen bedeutet, dass der Test nicht bestanden wurde. Darunter befinden sich zusätzliche Informationen, wie beispielweise welcher Test nicht bestanden wurde. Daher ist es wichtig, diese eindeutig zu benennen und nicht zu umfangreich zu gestalten.

good code or shity unit test

Was sollte getestet werden?

Es ist schwer, die richtige Balance zu finden, wenn es um das Schreiben von Tests geht. Auf der einen Seite verhindern diese, dass man unbeabsichtigt etwas im Code verändert, jedoch müssen bei einer gewollten Änderung ebenfalls alle betroffenen Tests angepasst werden.
Einige hilfreiche Punkte hierfür sind:

  • Teste lieber das äußere Interface einer Funktion, als den inneren Code, da dadurch die Flexibilität beibehalten wird, diesen später zu ändern.
  • Schreibe für jede Funktionalität deiner Funktionen nur einen einzelnen Test. Dadurch muss später, falls sich diese verändert, nur ein Test angepasst werden.
  • Konzentriere dich darauf, komplizierten Code mit vielen Abhängigkeiten zu testen und Randfälle statt dem Offensichtlichen. Jeder hat wahrscheinlich schon einmal mehrere Stunden mit Debugging verbracht, nur um einen simplen Fehler zu finden.
  • Schreibe nach jedem erfolgreich gefixten Bug einen Test.

Der vorherige Teil der Reihe drehte sich um die Handhabung von unerwarteten Fehlern und Bugs. Doch manchmal erwartet man das Auftreten von Fehlern, beispielsweiße falls man das gleiche Modell für mehrere Datensätze anwenden möchte. Dabei kann unter anderem der Fehler auftreten, dass das Modell aufgrund von fehlender Varianz nicht geschätzt werden kann. In diesem Fall möchte man nicht, dass durch diesen einen Fehler die komplette Schätzung abbricht, sondern, dass mit der nächsten Schätzung fortgefahren wird.

In R gibt es drei unterschiedliche Methoden um dies zu erreichen:

  • try() ignoriert den Fehler und führt die Berechnung fort.
  • trycatch() lässt eine zusätzlich Fehlermeldung/Aktion zuweisen.
  • withCallingHandlers() ist eine besondere Variante von trycatch() , welches die Ausnahme lokal handhabt. Es wird nur selten benötigt, daher fokussieren wir uns auf die beiden erstgenannten.

try

Mit try() wird der Code weiterhin ausgeführt unbeachtet von auftretenden Fehlern.
Im folgenden Beispiel tritt ein Fehler auf, wodurch der Prozess abgebrochen wird und es wird kein Wert zurückgegeben.

Bespiel Fehlermeldung

Fügen wir die fehlererzeugende Funktion in ein try() ein, wird die Fehlermeldung weiterhin angezeigt, jedoch wird der restliche Code ausgeführt und wir bekommen weiterhin einen Return.

Beispiel try

Aktivieren wir die Option silent = TRUE innerhalb der try()-Funktion, wird nicht einmal mehr die Fehlermeldung angezeigt.

Code-Blöcke werden innerhalb von try() in der geschweiften Klammer {} zusammengefasst.

Beispiel Fehlermeldung try

Zusätzlich ist es möglich die Klasse von try() abzufragen. Taucht kein Fehler auf, ist es die Klasse des letzten Returns, taucht jedoch einer auf, ist es eine eigene „try-error”-Klasse.

Bespiel Klasse von try

Dadurch kann im Nachhinein überprüft werden, ob die Funktion erfolgreich ausgeführt wurde, dies ist besonders hilfreich, wenn man eine Funktion auf mehrere Objekte anwendet.

trycatch

Im Unterschied zu try() können mit trycatch() nicht nur Fehler gehandhabt werden, sondern auch Warnungen, Messages und Abbrüche. Eine weitere Besonderheit ist, dass je nach auftretender Ausnahme unterschiedliche Funktion aufgerufen werden können. In der Regel werden hierbei Default-Werte übergeben oder bedeutsamere Meldungen erzeugt. Im folgenden Beispiel wollen wir die Funktion über einen Vektor mit den Ausprägungen Vektor = data.frame(4, 2, -3, 10, "hallo") loopen. Wobei die log-Funktion bei negativen Werten eine Warnung ausgibt und für Factors und Strings einen Fehler.

Beispiel trycatch

Das letzte, wichtige Argument von trycatch() ist finally. Die dort angegebene Funktion wird als letztes ausgeführt, ungeachtet ob der vorherige Code erfolgreich durchlief oder abgebrochen ist. Dies ist nützlich um nicht mehr benötigte Objekte zu löschen oder Verbindungen zu schließen.

Der nächste Artikel der Reihe wird sich um Unit-Testing drehen.

Referenzen

  1. Advanced R by Hadley Wickham

Nachdem der erste Teil die unterschiedlichen Aktivierungsmöglichkeiten für den Debugger behandelt hat, dreht sich der zweite Teil um effizientes Debugging.

„Debugging is like being the detective in a crime movie where you are also the murderer.”(1)

… und manchmal erinnert man sich nicht mal die Tat begangen zu haben.

Im Folgenden werden wir die Debugging-Features von RStudio an einem einfachen Beispiel behandeln. Die folgenden Funktionen sollen die Buchstaben der einzelnen Wörter umdrehen, aber nicht den kompletten Satz.

# drehe ein Wort um
stringrev <- function(str) {
  vec <- strsplit(str, "")
  vec <- rev(unlist(vec))
  paste(vec, collapse = "")
}

# trenne einen Satz in einzelne Woerter
crazify <- function(str) {
  vec <- strsplit(str, " ")
  vec <- lapply(unlist(vec), stringrev)
  paste(vec, collapse = " ")
}

Für einzelne Sätze funktioniert der Code problemlos. Darauf aufbauend möchten wir die Funktion auf einen Abschnitt eines anderen Blogbeitrags anwenden, welcher in einem Data Frame gespeichert wurde.

test_it <- function() {
  sentences <- data.frame(
  titles = c("first", "second"),
  text = c("Bokaj, der beste Ingenieur im nicht-parametrischen
            Universum und glücklicherweise Leiter unseres
            Maschinenraums, hat eine Idee!",
           "Wir demontieren von einem der anderen Schiffe den
            Antrieb und verstärken damit unseren."))
  sentences$text <- vapply(sentences$text, crazify, "character")
  return(sentences)
}

Doch hierbei taucht diese Fehlermeldung auf:
Error in strsplit(str, " ") : non-character argument
Um dieser Fehlermeldung auf den Grund zu gehen aktivieren wir die Option „Debugging on Error“, wie im vorherigen Blog-Post erklärt.

Zuallererst fällt einem auf, dass sich das Aussehen von RStudio verändert, sobald man sich im Debugging-Modus befindet.

Environment-Pane

Debug Environment Pane

Normalerweise werden im Environment-Pane die globalen Objekte angezeigt. Befindet man sich jedoch im Debugging-Modus, werden stattdessen die Objekte im Environment der jeweiligen Funktion angezeigt. Über der Auflistung der Objekte befindet sich ein Dropdown-Menü, welches die Möglichkeit bietet zwischen den unterschiedlichen Environments zu wechseln. Sind Variablen ausgegraut, bedeutet dies, dass es sich um Objekte handelt, die momentan noch nicht im Environment sind, jedoch zukünftig der Funktion zur Verfügung stehen werden.

Traceback

Debug Traceback

Im Traceback befinden sich alle aufgerufenen Funktionen, um zur aktuellen, fehlerverursachenden Funktion zu gelangen. Über dieses Fenster kann ebenfalls zwischen den vorherigen Funktionsaufrufen gewechselt werden, dadurch wechselt ebenfalls der angezeigte Code und die im Environment angezeigten Objekte. Es ist jedoch zu beachten, dass dies nicht das aktuelle Environment verändert. Unten in der Liste befindet sich die erste aufgerufene Funktion, welche in unserem Fall test_it() ist, und geht weiter bis zu crazify(), welche die R-Funktion strsplit() aufruft.

Konsole

Debug Konsole

Im Debugging-Modus gibt es zwei prominente Veränderungen der Konsole. Zum einen steht in der Eingabe nun

Browse[1]>

Dies zeigt an, dass man sich im Environment-Browser von R befindet. Größtenteils verhält sich die Konsole im Debugging-Modus wie die normale Konsole, mit ein paar Ausnahmen:

  1. Objekte werden anhand des aktuellen Environments evaluiert. Beispielsweiße wird das Text-Data Frame ausgegeben, wenn man x eingibt, es ist ebenfalls möglich x einen neuen Wert zuzuweisen.
  2. Mit der Eingabe-Taste wird das aktuelle Statement ausgeführt und man springt zum nächsten Statement. Dadurch lässt sich der Code bequem Schritt für Schritt durchgehen.
  3. Es stehen eine Vielzahl von Debugging-Funktionen zu Verfügung auf die Später genauer eingegangen wird.

Die zweite große Veränderung innerhalb der Konsole ist die Funktionsleiste, welche sich über der Konsole befindet. Diese bietet praktische Schalter, um spezielle Debugging-Funktionen direkt an die R-Konsole zu senden. Es gibt hierbei keinen Unterschied, ob man die Befehle in die R-Konsole eingibt oder die Schalter verwendet. Die Schalter bedeuten von links nach rechts mit den entsprechenden Konsolen-Befehlen:

Befehl Funktion
n Ausführen des nächsten Statements
s In den Funktionsaufruf springen
f Funktion/Schleife beenden
c Bis zum nächsten Breakpoint fortfahren
Q Debugging beenden

Um das aktuelle Environment zu wechseln, verwendet man die Funktion recover(). Dies listet die Environments der vorausgegangenen Funktionen auf. Aus dieser Liste kann das passende Environment gewählt werden.

Fehlerfindung

Doch wo liegt der Fehler in unserem Beispiel? Im Environment-Pane sieht man, dass es sich bei x, welches in die strsplit()-Funktion übergeben wird, um einen Faktor und nicht um einen Character handelt. Dies liegt daran, dass die Standardeinstellung bei data.frame stringsAsFactors = default.stringsAsFactors() ist. Fügen wir in unserem Ausgangscodeblock der test_it-Funktion stringsAsFactors = FALSE hinzu, sehen wir, dass die Funktion durchläuft und unser gewünschtes Ergebnis ausgibt.

Ergebnis vom Buchstabendrehen

Referenzen

  1. https://twitter.com/fortes/status/399339918213652480
  2. Introduction to Debugging in R
  3. https://github.com/rstudio/webinars/tree/master/15-RStudio-essentials/2-Debugging

In der Blog-Reihe „Fehlerbehandlung in R“ geht es um effizientes und systematisches Überprüfen von R-Code. Den Beginn macht das Finden von Fehlern durch Debugging, weiter geht es mit der Handhabung von Fehlern und endet mit Unit-Testing, das zum Überprüfen von korrekter Funktionalität von R-Code dient.
Die Reihe startet mit Debugging in R, wobei Debugging ein breitgefächertes Thema ist. Dieser Artikel fokussiert sich daher auf die Möglichkeiten, die RStudio bietet.

Debugging hilft dabei herauszufinden an welcher Stelle im Code sich ein Fehler befindet oder an welcher Stelle der Code sich anders verhält als erwartet. Dies beinhaltet im Generellen drei Schritte:

  • den Code laufen lassen
  • Code an der Stelle stoppen an welcher vermutet wird, dass sich dort der Fehler befindet
  • Schritt für Schritt durch den Code gehen und diesen dabei überprüfen.

In den Debugging-Modus eintreten

Um in den Debugging-Modus zu kommen, muss RStudio mitgeteilt werden, wann es die Berechnungen stoppen soll. Es gibt keinen „Pause-Button“ mit welchem man eine laufende Berechnung stoppen kann um in den Debugging-Modus einzutreten. Dies ist in so gut wie allen anderen Programmiersprachen der Fall, da die Berechnungen in der Regel zu schnell von statten gehen als dass es möglich wäre an der richtigen Stelle zu stoppen. Stattdessen bestimmt man zuvor an welcher Stelle der Code angehalten werden soll. Dies ist nicht zu verwechseln mit dem „Stop“-Button über der der Konsole, welcher die Berechnung komplett abbricht.

Vor einer Zeile stoppen

Der einfachste und meist genutzte Weg um in den Debugging-Modus zu gelangen, ist es einen Breakpoint im Code-Editor zu setzen. Dies kann auf einfache Weise in RStudio gemacht werden indem man links neben die Zeilennummer klickt oder durch das Drücken von Shift+F9 auf der Tastatur und zeitgleiches Klicken mit der Maus in der gewünschten Zeile.

aktivierter Breakpoint in Rstudio

Hierbei wird eine Tracing-Funktion innerhalb der eigentlichen Funktion eingefügt. Der Breakpoint wird durch einen ausgefüllten roten Kreis im Editor gekennzeichnet. Außerdem kann man einen schnellen Überblick erlangen in welcher Funktion sich ein Breakpoint befindet, indem man in das Environment-Fenster schaut, welche Funktion ebenfalls durch einen roten Kreis gekennzeichnet wird.

Falls die Funktion noch nicht existiert, zu einem weil man das File noch nicht gesourced hat oder zu anderem weil sich die Funktion im Environment und im Editor unterscheiden, kann der Breakpoint noch nicht aktiviert werden. Dies wird durch einen nicht-ausgefüllten Kreis kenntlich gemacht.

nicht aktivierter Breakpoint in RStudio

In der Regel hilft es das File einmal zu sourcen, wodurch die Tracking-Funktion eingefügt wird und der Breakpoint startbereit ist.

Setzt man den Breakpoint per RStudio-Editor ist es nicht notwendig die Funktion zu bearbeiten und zusätzlichen Code per Hand einzufügen. Allerdings gibt es bestimmte Situationen in denen diese Breakpoints nicht funktionieren wie beispielsweise komplexere Funktionssyntaxen. Außerdem wird konditionelles Debugging bisher nicht von RStudio unterstützt. Hier schafft die browser()-Funktion Abhilfe. Da es sich hierbei um eine tatsächliche Funktion handelt, muss sie in den Code geschrieben werden, kann aber an so gut wie jeder Stelle hinzugefügt werden. Sind sie erstmal aktiv und aufgerufen verhalten sich die Editor-Breakpoins und browser() sehr ähnlich.

Stoppen bevor eine Funktion ausgeführt wird

Der Editor-Breakpoint oder browser() eigenen sich optimal für Funktionen für welche der Source-Code vorliegt. Hat man jedoch nicht das .R File zur Hand kann alternativ eine ganze Funktion mit debug() bzw. debugonce() gedebugged werden. Hierfür wird die jeweilige Funktion innerhalb von debug() bzw. debugonce() geschrieben , beispielsweise debugonce(mean) . Dies ändert nicht die Funktion an sich, aber es startet den Debugger direkt nach dem Funktionsaufruf, man kann es sich vorstellen als würde man einen Breakpoint direkt zu Beginn der Funktion setzen würde. debugonce() aktiviert den Debug-Modus nur ein einziges Mal für die jeweilige Funktion zum nächstmöglichen Zeitpunkt zu welchem diese aufgerufen wird. debug() hingegen aktiviert jedes Mal den Debugger, wenn die Funktion aufgerufen wird, was schlimmsten Falls in einer endlosen Schleife resultieren kann. Daher ist es in der Regel zu empfehlen debugonce() zu benutzen. Das Gegenstück zu debug() ist undebug(), welches benutzt wird wenn man nicht mehr jedes Mal die Funktion bei Aufruf debuggen möchte.

Bei Fehler stoppen

Die dritte Möglichkeit in den Debugging-Modus zu gelangen ist die Einstellung, dass der Debugger jedes Mal automatisch aktiviert wird, wenn ein Fehler auftaucht. Dadurch stoppt die Funktion automatisch und der Debugging-Modus startet direkt von selbst. Diese Funktionalität wird über die RStudio Oberfläche aktiviert in dem man Debug -> On Error von „Error Inspector“ zu „Break in Code“ ändert.

Debug on Error in RStudio

Allerdings wird der Debugger per Default nur aktiviert, wenn ein Fehler im eigenen Code auftaucht. Falls man einen Fehler finden möchte, welcher ebenfalls Code von Dritten beinhaltet, kann diese Einstellung unter Tools -> Global Options und dem abwählen von „Use debug error handler only when my code contains errors“ abgeändert werden. Alternativ kann die Option dauerhaft mit options(error = browser()) überschrieben werden. Es kann jedoch schnell störend werden, dass jedes Mal der Debugger aktiviert wird. Daher sollte nicht vergessen werden diese Option wieder mit options(error = NULL) rückgängig zu machen sobald das Debugging beendet ist.

Der nächste Teil der Reihe „Fehlerbehandlung in R“ dreht sich um effektives Debugging in R nachdem der Debugger aktiviert wurde.

In der Blog-Reihe „Fehlerbehandlung in R“ geht es um effizientes und systematisches Überprüfen von R-Code. Den Beginn macht das Finden von Fehlern durch Debugging, weiter geht es mit der Handhabung von Fehlern und endet mit Unit-Testing, das zum Überprüfen von korrekter Funktionalität von R-Code dient.
Die Reihe startet mit Debugging in R, wobei Debugging ein breitgefächertes Thema ist. Dieser Artikel fokussiert sich daher auf die Möglichkeiten, die RStudio bietet.

Debugging hilft dabei herauszufinden an welcher Stelle im Code sich ein Fehler befindet oder an welcher Stelle der Code sich anders verhält als erwartet. Dies beinhaltet im Generellen drei Schritte:

In den Debugging-Modus eintreten

Um in den Debugging-Modus zu kommen, muss RStudio mitgeteilt werden, wann es die Berechnungen stoppen soll. Es gibt keinen „Pause-Button“ mit welchem man eine laufende Berechnung stoppen kann um in den Debugging-Modus einzutreten. Dies ist in so gut wie allen anderen Programmiersprachen der Fall, da die Berechnungen in der Regel zu schnell von statten gehen als dass es möglich wäre an der richtigen Stelle zu stoppen. Stattdessen bestimmt man zuvor an welcher Stelle der Code angehalten werden soll. Dies ist nicht zu verwechseln mit dem „Stop“-Button über der der Konsole, welcher die Berechnung komplett abbricht.

Vor einer Zeile stoppen

Der einfachste und meist genutzte Weg um in den Debugging-Modus zu gelangen, ist es einen Breakpoint im Code-Editor zu setzen. Dies kann auf einfache Weise in RStudio gemacht werden indem man links neben die Zeilennummer klickt oder durch das Drücken von Shift+F9 auf der Tastatur und zeitgleiches Klicken mit der Maus in der gewünschten Zeile.

aktivierter Breakpoint in Rstudio

Hierbei wird eine Tracing-Funktion innerhalb der eigentlichen Funktion eingefügt. Der Breakpoint wird durch einen ausgefüllten roten Kreis im Editor gekennzeichnet. Außerdem kann man einen schnellen Überblick erlangen in welcher Funktion sich ein Breakpoint befindet, indem man in das Environment-Fenster schaut, welche Funktion ebenfalls durch einen roten Kreis gekennzeichnet wird.

Falls die Funktion noch nicht existiert, zu einem weil man das File noch nicht gesourced hat oder zu anderem weil sich die Funktion im Environment und im Editor unterscheiden, kann der Breakpoint noch nicht aktiviert werden. Dies wird durch einen nicht-ausgefüllten Kreis kenntlich gemacht.

nicht aktivierter Breakpoint in RStudio

In der Regel hilft es das File einmal zu sourcen, wodurch die Tracking-Funktion eingefügt wird und der Breakpoint startbereit ist.

Setzt man den Breakpoint per RStudio-Editor ist es nicht notwendig die Funktion zu bearbeiten und zusätzlichen Code per Hand einzufügen. Allerdings gibt es bestimmte Situationen in denen diese Breakpoints nicht funktionieren wie beispielsweise komplexere Funktionssyntaxen. Außerdem wird konditionelles Debugging bisher nicht von RStudio unterstützt. Hier schafft die browser()-Funktion Abhilfe. Da es sich hierbei um eine tatsächliche Funktion handelt, muss sie in den Code geschrieben werden, kann aber an so gut wie jeder Stelle hinzugefügt werden. Sind sie erstmal aktiv und aufgerufen verhalten sich die Editor-Breakpoins und browser() sehr ähnlich.

Stoppen bevor eine Funktion ausgeführt wird

Der Editor-Breakpoint oder browser() eigenen sich optimal für Funktionen für welche der Source-Code vorliegt. Hat man jedoch nicht das .R File zur Hand kann alternativ eine ganze Funktion mit debug() bzw. debugonce() gedebugged werden. Hierfür wird die jeweilige Funktion innerhalb von debug() bzw. debugonce() geschrieben , beispielsweise debugonce(mean) . Dies ändert nicht die Funktion an sich, aber es startet den Debugger direkt nach dem Funktionsaufruf, man kann es sich vorstellen als würde man einen Breakpoint direkt zu Beginn der Funktion setzen würde. debugonce() aktiviert den Debug-Modus nur ein einziges Mal für die jeweilige Funktion zum nächstmöglichen Zeitpunkt zu welchem diese aufgerufen wird. debug() hingegen aktiviert jedes Mal den Debugger, wenn die Funktion aufgerufen wird, was schlimmsten Falls in einer endlosen Schleife resultieren kann. Daher ist es in der Regel zu empfehlen debugonce() zu benutzen. Das Gegenstück zu debug() ist undebug(), welches benutzt wird wenn man nicht mehr jedes Mal die Funktion bei Aufruf debuggen möchte.

Bei Fehler stoppen

Die dritte Möglichkeit in den Debugging-Modus zu gelangen ist die Einstellung, dass der Debugger jedes Mal automatisch aktiviert wird, wenn ein Fehler auftaucht. Dadurch stoppt die Funktion automatisch und der Debugging-Modus startet direkt von selbst. Diese Funktionalität wird über die RStudio Oberfläche aktiviert in dem man Debug -> On Error von „Error Inspector“ zu „Break in Code“ ändert.

Debug on Error in RStudio

Allerdings wird der Debugger per Default nur aktiviert, wenn ein Fehler im eigenen Code auftaucht. Falls man einen Fehler finden möchte, welcher ebenfalls Code von Dritten beinhaltet, kann diese Einstellung unter Tools -> Global Options und dem abwählen von „Use debug error handler only when my code contains errors“ abgeändert werden. Alternativ kann die Option dauerhaft mit options(error = browser()) überschrieben werden. Es kann jedoch schnell störend werden, dass jedes Mal der Debugger aktiviert wird. Daher sollte nicht vergessen werden diese Option wieder mit options(error = NULL) rückgängig zu machen sobald das Debugging beendet ist.

Der nächste Teil der Reihe „Fehlerbehandlung in R“ dreht sich um effektives Debugging in R nachdem der Debugger aktiviert wurde.