Getting started with slider

library(slider)
library(dplyr, warn.conflicts = FALSE)
library(lubridate, warn.conflicts = FALSE)

This vignette is meant to serve as an introduction to {slider}. In it, you’ll learn about the three core functions in the package: slide(), slide_index(), and slide_period(), along with their many variants.

slider is a package for rolling analysis using window functions. “Window functions” is a term that I’ve borrowed from SQL that means that some function is repeatedly applied to different “windows” of your data as you step through it. Typical examples of applications of window functions include rolling averages, cumulative sums, and more complex things such as rolling regressions.

slide()

To better understand window functions, we’ll turn to our first core function, slide(). slide() is a bit like purrr::map(). You supply a vector to slide over, .x, and a function to apply to each window, .f. With those two things alone, slide() is almost identical to map().

slide(1:4, ~.x)
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] 2
#> 
#> [[3]]
#> [1] 3
#> 
#> [[4]]
#> [1] 4

On top of this, you can control the size and placement of the window by using the additional arguments to slide(). For example, you can ask for a window of size 3 containing “the current element, as well as the 2 before it” like this:

slide(1:4, ~.x, .before = 2)
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] 1 2
#> 
#> [[3]]
#> [1] 1 2 3
#> 
#> [[4]]
#> [1] 2 3 4

You’ll notice that the first two elements of the list contain partial or “incomplete” windows. By default, slide() assumes that you want to compute on these windows anyways, but if you don’t care about them, you can change the .complete argument.

slide(1:4, ~.x, .before = 2, .complete = TRUE)
#> [[1]]
#> NULL
#> 
#> [[2]]
#> NULL
#> 
#> [[3]]
#> [1] 1 2 3
#> 
#> [[4]]
#> [1] 2 3 4

slide() is size stable, so you always get an output that is the same size as your input. Because of that, the partial results have been replaced by the corresponding missing value. For a list, that is NULL.

Sometimes, changing the placement of the window is a critical part of your calculation. For example, you might want a “center alignment” where you have an equal number of values before and after the current element. To accomplish this, you can combine the .before argument with .after to get a centered window. Here we ask for a window of size 3 containing “the current element, as well as 1 element before and 1 element after”. It is “centered” because in position 2 we have a complete window of the current element (2), along with one element before (1) and one after (3).

slide(1:4, ~.x, .before = 1, .after = 1)
#> [[1]]
#> [1] 1 2
#> 
#> [[2]]
#> [1] 1 2 3
#> 
#> [[3]]
#> [1] 2 3 4
#> 
#> [[4]]
#> [1] 3 4

slide() can also perform expanding windows. These are the type that allow cumulative operations to work. In prose, an expanding window would be “the current element, along with every element before this one”. To construct this kind of window, you can set .before to Inf.

slide(1:4, ~.x, .before = Inf)
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] 1 2
#> 
#> [[3]]
#> [1] 1 2 3
#> 
#> [[4]]
#> [1] 1 2 3 4

slide() is type-stable, meaning that it always returns an object of the same type, and the base form of slide() always returns a list. So far, this is all that we have used to illustrate how it works, but practically you are more likely to use one of the suffixed forms like slide_dbl() or slide_int(). For example, you might have a vector of sales data that you want to compute a 3 value moving average on.

sales_vec <- c(2, 4, 6, 2)

slide_dbl(sales_vec, mean, .before = 2)
#> [1] 2 3 4 4

slide_index()

To make things a bit more interesting, let’s assume that the sales vector from the example above is also tied to some “index”, like a date vector of when the sale actually occurred.

index_vec <- as.Date("2019-08-29") + c(0, 1, 5, 6)
wday_vec <- as.character(wday(index_vec, label = TRUE))

company <- tibble(
  sales = sales_vec,
  index = index_vec,
  wday = wday_vec
)

company
#> # A tibble: 4 × 3
#>   sales index      wday 
#>   <dbl> <date>     <chr>
#> 1     2 2019-08-29 Thu  
#> 2     4 2019-08-30 Fri  
#> 3     6 2019-09-03 Tue  
#> 4     2 2019-09-04 Wed

This index is increasing but irregular, meaning that we “jumped” from Friday to Tuesday because there were no sales between those dates. For the purpose of this example, let’s assume that this is an online company where it is perfectly reasonable that you could have sales on both Saturday and Sunday (If your use case requires that you “skip over” weekends and even holidays, you might like {almanac}).

A reasonable business question to ask would be to compute a 3 day moving average. Is this different from the 3 value moving average we computed before? Here is the expected result, side by side with the 3 value one computed using slide_dbl() from before.

#> # A tibble: 4 × 5
#>   sales index      wday  roll_val roll_day
#>   <dbl> <date>     <chr>    <dbl>    <dbl>
#> 1     2 2019-08-29 Thu          2        2
#> 2     4 2019-08-30 Fri          3        3
#> 3     6 2019-09-03 Tue          4        6
#> 4     2 2019-09-04 Wed          4        4

The difference shows up in the third row, when computing the 3 day moving average looking back from Tuesday. To understand why they are different, consider what slide_dbl() does. It uses the sales column and looks at the “current row, along with two rows before it” to compute the result. When you are on row 3, this would select rows 1-3 giving the date range of [Thu, Tue], which isn’t 3 days. The correct answer would have been to look back 2 days from Tuesday, not 2 rows from row 3. This would have given us the date window of [Sun, Tue], and only values in that range should be included in the moving average calculation for row 3. The only row in that range is row 3, so we should just be averaging the single value of 6 to get our result.

slide_dbl() doesn’t give us what we want because it is unaware of the index column. It just looks back a set number of values. What we need is a function that “knows” about the index and can adjust accordingly. For that, you can use slide_index(.x, .i, .f, ...) which has a .i argument to pass an index vector through.

To understand how slide_index() works, take a look at the following comparison to slide(). For illustration, the current window of the weekday vector is printed out. Notice that in position 3, slide() gives us the “wrong” result of Thursday, Friday and Tuesday, because it just looks back 2 values.

wday_vec
#> [1] "Thu" "Fri" "Tue" "Wed"

slide(wday_vec, ~.x, .before = 2)
#> [[1]]
#> [1] "Thu"
#> 
#> [[2]]
#> [1] "Thu" "Fri"
#> 
#> [[3]]
#> [1] "Thu" "Fri" "Tue"
#> 
#> [[4]]
#> [1] "Fri" "Tue" "Wed"

On the other hand, slide_index() can be “aware” of the irregular index vector. By passing it through as .i, and by swapping a look back period of 2 for the lubridate object of days(2), the start of the range is computed as .i - days(2), which correctly computes a date window of [Sun, Tue] for the third element, so that we only capture Tuesday in the window.

slide_index(wday_vec, index_vec, ~.x, .before = days(2))
#> [[1]]
#> [1] "Thu"
#> 
#> [[2]]
#> [1] "Thu" "Fri"
#> 
#> [[3]]
#> [1] "Tue"
#> 
#> [[4]]
#> [1] "Tue" "Wed"

Knowing this, we can swap out slide_dbl() for slide_index_dbl() to see how to correctly compute our 3 day rolling average.

mutate(
  company, 
  roll_val = slide_dbl(sales, mean, .before = 2),
  roll_day = slide_index_dbl(sales, index, mean, .before = days(2))
)
#> # A tibble: 4 × 5
#>   sales index      wday  roll_val roll_day
#>   <dbl> <date>     <chr>    <dbl>    <dbl>
#> 1     2 2019-08-29 Thu          2        2
#> 2     4 2019-08-30 Fri          3        3
#> 3     6 2019-09-03 Tue          4        6
#> 4     2 2019-09-04 Wed          4        4

slide_period()

With slide_index(), we always returned a vector of the same size as .x, and the idea was to build indices to slice .x with using “the current element of .i + some number of elements before/after it”. slide_period() works a bit differently. It first breaks .i up into “time blocks” by some period (like monthly), and then uses those blocks to define how to slide over .x.

To see an example, let’s expand out our company sales data frame.

big_index_vec <- c(
  as.Date("2019-08-30") + 0:4,
  as.Date("2019-11-30") + 0:4
)

big_sales_vec <- c(2, 4, 6, 2, 8, 10, 9, 3, 5, 2)

big_company <- tibble(
  sales = big_sales_vec,
  index = big_index_vec
)

big_company
#> # A tibble: 10 × 2
#>    sales index     
#>    <dbl> <date>    
#>  1     2 2019-08-30
#>  2     4 2019-08-31
#>  3     6 2019-09-01
#>  4     2 2019-09-02
#>  5     8 2019-09-03
#>  6    10 2019-11-30
#>  7     9 2019-12-01
#>  8     3 2019-12-02
#>  9     5 2019-12-03
#> 10     2 2019-12-04

Now say we want to compute the monthly sales, and just return 1 value per month. Since we have 4 months, we should get 4 values back. What we really want to do here is break the index up into “time blocks” of 1 month, and then slide over those. That’s what slide_period() does.

slide_period(big_company, big_company$index, "month", ~.x)
#> [[1]]
#> # A tibble: 2 × 2
#>   sales index     
#>   <dbl> <date>    
#> 1     2 2019-08-30
#> 2     4 2019-08-31
#> 
#> [[2]]
#> # A tibble: 3 × 2
#>   sales index     
#>   <dbl> <date>    
#> 1     6 2019-09-01
#> 2     2 2019-09-02
#> 3     8 2019-09-03
#> 
#> [[3]]
#> # A tibble: 1 × 2
#>   sales index     
#>   <dbl> <date>    
#> 1    10 2019-11-30
#> 
#> [[4]]
#> # A tibble: 4 × 2
#>   sales index     
#>   <dbl> <date>    
#> 1     9 2019-12-01
#> 2     3 2019-12-02
#> 3     5 2019-12-03
#> 4     2 2019-12-04

Since this returns 4 values, and not the same number of values as there are in .x, it won’t fit naturally in a mutate() or summarise() statement. I find the easiest way to do this is to create a helper function that takes a data frame and returns one with the summary result for one time block, and then call that with slide_period_dfr().

monthly_summary <- function(data) {
  summarise(data, index = max(index), sales = sum(sales))
}

slide_period_dfr(
  big_company,
  big_company$index,
  "month",
  monthly_summary
)
#> # A tibble: 4 × 2
#>   index      sales
#>   <date>     <dbl>
#> 1 2019-08-31     6
#> 2 2019-09-03    16
#> 3 2019-11-30    10
#> 4 2019-12-04    19

Now you might be thinking, “I can do that with dplyr and lubridate!”, and you’d be right:

big_company %>%
  mutate(monthly = floor_date(index, "month")) %>%
  group_by(monthly) %>%
  summarise(sales = sum(sales))
#> # A tibble: 4 × 2
#>   monthly    sales
#>   <date>     <dbl>
#> 1 2019-08-01     6
#> 2 2019-09-01    16
#> 3 2019-11-01    10
#> 4 2019-12-01    19

But here is where things get interesting! Now what if we want to compute those monthly sales, but we want the time blocks to be made of the “current month block, plus 1 month block before it”. For example, for the month of 2019-09, it would include 2019-08 and 2019-09 together in the rolling summary. There isn’t an easy way to do this in dplyr alone. With slider, there are two ways to do this.

The first is with slide_period_dfr(), and it is as easy as adding .before = 1, to select the current month block and 1 before it.

slide_period_dfr(
  big_company,
  big_company$index,
  "month",
  monthly_summary,
  .before = 1
)
#> # A tibble: 4 × 2
#>   index      sales
#>   <date>     <dbl>
#> 1 2019-08-31     6
#> 2 2019-09-03    22
#> 3 2019-11-30    10
#> 4 2019-12-04    29

Depending on your use case, you might want to append these results as a new column in big_company. To do this, we can instead go back to using floor_date() to generate monthly groupings, and slide over them using slide_index_dbl() with a lookback period of 1 month.

big_company %>%
  mutate(
    monthly = floor_date(index, "month"),
    sales_summary = slide_index_dbl(sales, monthly, sum, .before = months(1))
  )
#> # A tibble: 10 × 4
#>    sales index      monthly    sales_summary
#>    <dbl> <date>     <date>             <dbl>
#>  1     2 2019-08-30 2019-08-01             6
#>  2     4 2019-08-31 2019-08-01             6
#>  3     6 2019-09-01 2019-09-01            22
#>  4     2 2019-09-02 2019-09-01            22
#>  5     8 2019-09-03 2019-09-01            22
#>  6    10 2019-11-30 2019-11-01            10
#>  7     9 2019-12-01 2019-12-01            29
#>  8     3 2019-12-02 2019-12-01            29
#>  9     5 2019-12-03 2019-12-01            29
#> 10     2 2019-12-04 2019-12-01            29