R: Count number of times B follows A using dplyr

Viewed 96

I have a data.frame of monthly averages of radon measured over a few months. I have labeled each value either "below" or "above" a threshold and would like to count the number of times the average value does: "below to above", "above to below", "above to above" or "below to below".

df <- data.frame(value = c(130, 200, 240, 230, 130),
                 level = c("below", "above","above","above", "below"))

A bit of digging into Matlab answer on here suggests that we could use the Matrix package:

require(Matrix)
sparseMatrix(i=c(2,2,2,1), j=c(2,2,2))

Produces this result which I can't yet interpret.

[1,] | |
[2,] | .

Any thoughts about a tidyverse method?

5 Answers

Sure, just use group by and count the values

library(dplyr)

df <- data.frame(value = c(130, 200, 240, 230, 130),
                 level = c("below", "above","above","above", "below"))
df %>% 
  group_by(grp = paste(level, lead(level))) %>% 
  summarise(n = n()) %>% 
  # drop the observation that does not have a "next" value
  filter(!grepl(pattern = "NA", x = grp))
#> # A tibble: 3 × 2
#>   grp             n
#>   <chr>       <int>
#> 1 above above     2
#> 2 above below     1
#> 3 below above     1

You could use table from base R:

table(df$level[-1], df$level[-nrow(df)])

        above below
  above     2     1
  below     1     0

EDIT in response to @HCAI's comment: applying table to multiple columns: First, generate some data:

set.seed(1)
U = matrix(runif(4*20),nrow = 20)
dfU=data.frame(round(U))
library(plyr) # for mapvalues
df2 = data.frame(apply(dfU, 
                       FUN = function(x) mapvalues(x, from=0:1, to=c('below','above')),
                       MARGIN=2))

so that df2 contains random 'above' and 'below':

      X1    X2    X3    X4
1  below above above above
2  below below above below
3  above above above below
4  above below above below
5  below below above above
6  above below above below
7  above below below below
8  above below below above
9  above above above below
10 below below above above
11 below below below below
12 below above above above
13 above below below below
14 below below below below
15 above above below below
16 below above below above
17 above above below above
18 above below above below
19 below above above above
20 above below below above

Now apply table to each column and vectorize the output:

apply(df2, 
      FUN=function(x) as.vector(table(x[-1], 
                                      x[-nrow(df2)])),
      MARGIN=2)

which gives us

     X1 X2 X3 X4
[1,]  5  2  7  2
[2,]  5  6  4  6
[3,]  6  5  3  6
[4,]  3  6  5  5

All that's left is a bit of care in labeling the rows of the output. Maybe someone can come up with a clever way to merge/join the data frames resulting from apply(df2, FUN=function(x) melt(table(x[-1],x[-nrow(df2)])),2), which would maintain the row names. (I spent some time looking into it but couldn't work out how to do it easily.)

not run, so there may be a typo, but you get the idea. I'll leave it to you to deal with na and the first obs. Single pass through the vector.

library(dplyr)
summarize(increase = sum(case_when(value > lag(value) ~ 1, T ~ 0)),
          decrease = sum(case_when(value > lag(value) ~ 1, T ~ 0)),
          constant = sum(case_when(value = lag(value) ~ 1, T ~ 0))
         )

Another possible solution, based on tidyverse:

library(tidyverse)

df<-data.frame(value=c(130,200, 240, 230, 130),level=c("below", "above","above","above", "below"))

df %>% 
  mutate(changes = str_c(lag(level), level, sep = "_")) %>% 
  count(changes) %>% drop_na(changes)

#>       changes n
#> 1 above_above 2
#> 2 above_below 1
#> 3 below_above 1

Yet another solution, based on data.table:

library(data.table)

dt<-data.table(value=c(130,200, 240, 230, 130),level=c("below", "above","above","above", "below"))

dt[, changes := paste(shift(level), level, sep = "_")
][2:.N][,.(n = .N), keyby = .(changes)]

#>        changes n
#> 1: above_above 2
#> 2: above_below 1
#> 3: below_above 1

A slightly different version:

library(dplyr)
library(stringr)
df %>% 
  group_by(level = str_c(level, lead(level), sep = " ")) %>% 
  count(level) %>% 
  na.omit()
 level           n
  <chr>       <int>
1 above above     2
2 above below     1
3 below above     1
Related