Reputation: 25
I have the following dataset (more than 30.000 rows in reality):
dt <- tibble::tibble(x=seq.Date(as.Date("2019-01-01"), as.Date("2019-01-10"), "days"), y=c(1,2,3,2,1,1,3,1,2,1))
A tibble: 10 x 2
x y
<date> <dbl>
1 2019-01-01 1
2 2019-01-02 2
3 2019-01-03 3
4 2019-01-04 2
5 2019-01-05 1
6 2019-01-06 1
7 2019-01-07 3
8 2019-01-08 1
9 2019-01-09 2
10 2019-01-10 1
I would like to identify date-ranges above a given threshold, for example y >= 2. The first date of appearance of the range should be saved in a separate tibble as "start", the last date of appearance as "end". Each "start"/"end"-combination should be saved in a separate line. The ranges are separated from each other by "y"-values that are below the threshold (e.g. y < 2). The result should look like the following table:
result <- tibble::tibble(start=as.Date(c("2019-01-02", "2019-01-07", "2019-01-09")), end=as.Date(c("2019-01-04", "2019-01-07", "2019-01-09")))
A tibble: 3 x 2
start end
<date> <date>
1 2019-01-02 2019-01-04
2 2019-01-07 2019-01-07
3 2019-01-09 2019-01-09
My current solution is using for-loops. However, this leads to very slow execution.
Any ideas how to improve the performance and solve this issue more elegant?
Thank you for your ideas in advance.
Upvotes: 1
Views: 77
Reputation: 46886
Here's a function that identifies groups of consecutive values of a vector statisfing a condition
f = function(x, min) {
## 'run length encoding' of values satisfying the condition
r = rle(x > min)
## replace TRUE values with a grouping variable; FALSE values are coerced to 0
## (probably better to also replace r$values[!r$values] = NA)
r$values[r$values] = seq_len(sum(r$values))
## expand the modified run length encoding to the shape of the original vector
inverse.rle(r)
}
For your data, we have
> mutate(dt, grp = f(y, 1))
# A tibble: 10 x 3
x y grp
<date> <dbl> <int>
1 2019-01-01 1 0
2 2019-01-02 2 1
3 2019-01-03 3 1
4 2019-01-04 2 1
5 2019-01-05 1 0
6 2019-01-06 1 0
7 2019-01-07 3 2
8 2019-01-08 1 0
9 2019-01-09 2 3
10 2019-01-10 1 0
and we can then use standard dplyr operations
mutate(dt, grp = f(y, 1)) %>%
filter(grp != 0) %>%
group_by(grp) %>%
summarize(start = min(x), end = max(x), n = n())
with the output
# A tibble: 3 x 4
grp start end n
<int> <date> <date> <int>
1 1 2019-01-02 2019-01-04 3
2 2 2019-01-07 2019-01-07 1
3 3 2019-01-09 2019-01-09 1
Upvotes: 1
Reputation: 11255
This should work and is based on the idea that the diff of the dates minus 1 will have a cumulative sum equal to each other. That's why we can group by it.
dt%>%
filter(y >= 2)%>%
group_by(grouping = cumsum(c(0, diff.Date(x) - 1)))%>%
summarize(start = min(x)
, end = max(x))%>%
select(-grouping)
# A tibble: 3 x 2
start end
<date> <date>
1 2019-01-02 2019-01-04
2 2019-01-07 2019-01-07
3 2019-01-09 2019-01-09
Upvotes: 1