Рассмотрим следующий DataFrame:

    DF = structure(list(c_number = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 
1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 2L, 3L, 3L, 
3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 3L, 
3L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 4L, 5L, 5L, 5L, 5L, 
5L, 5L, 5L, 5L, 5L), date = c("2001-01-06", "2001-01-07", "2001-01-08", 
"2001-01-09", "2001-01-10", "2001-01-11", "2001-01-12", "2001-01-13", 
"2001-01-14", "2001-01-15", "2001-01-16", "2001-01-17", "2001-01-18", 
"2001-01-19", "2001-01-20", "2001-01-21", "2001-01-22", "2001-01-23", 
"2001-01-24", "2001-01-25", "2001-01-26", "2001-01-11", "2001-01-12", 
"2001-01-13", "2001-01-14", "2001-01-15", "2001-01-16", "2001-01-17", 
"2001-01-18", "2001-01-19", "2001-01-20", "2001-01-21", "2001-01-22", 
"2001-01-23", "2001-01-24", "2001-01-25", "2001-01-26", "2001-01-27", 
"2001-01-28", "2001-01-12", "2001-01-13", "2001-01-14", "2001-01-15", 
"2001-01-16", "2001-01-17", "2001-01-18", "2001-01-19", "2001-01-20", 
"2001-01-21", "2001-01-22", "2001-01-23", "2001-01-24", "2001-01-25", 
"2001-01-26", "2001-01-27", "2001-01-28", "2001-01-29", "2001-01-30", 
"2001-01-21", "2001-01-22", "2001-01-23", "2001-01-24", "2001-01-25", 
"2001-01-26", "2001-01-27", "2001-01-28", "2001-01-29", "2001-01-30", 
"2001-01-31", "2001-01-24", "2001-01-25", "2001-01-26", "2001-01-27", 
"2001-01-28", "2001-01-29", "2001-01-30", "2001-01-31", "2001-02-01"
), value = c(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)), .Names = c("c_number", 
"date", "value"), row.names = c(NA, -78L), class = "data.frame")

У меня есть данные о продажах для 5 клиентов в последовательные даты; Для клиента 1 у меня есть данные о продажах за 21 день подряд ... для клиента # 5 у меня есть данные о продажах за 9 дней подряд ...:

> table(DF[, 1])

 1  2  3  4  5 
21 18 19 11  9

Для каждого клиента я хочу выбрать дополнительный DF из 15 последовательных дней (если у меня есть хотя бы 15 последовательных дат для этого клиента) или все даты для этого клиента (если у меня нет 15 последовательных дат для этого клиента).

Ключевая часть состоит в том, что в случае 1 (если у меня есть хотя бы 15 последовательных дат для этого клиента), эти 15 последовательных дней должны иметь случайную дату начала (например, не всегда первые или последние 15 дат для клиента), чтобы избежать введения уклон в анализе.

В простой R я бы сделал:

library(dplyr)

slow_function <- function(i, DF, length_out = 15){
  sub_DF = DF[DF$c_number == i, ]
  if(nrow(sub_DF) <= length_out){
    out_DF = sub_DF
  } else {
    random_start = sample.int(nrow(sub_DF) - length_out, 1)
    out_DF = sub_DF[random_start:(random_start + length_out - 1), ]
  }
}
a_out = lapply(1:nrow(a_1), slow_function, DF = DF, length_out = 15)
a_out = dplyr::bind_rows(a_out)


table(a_out[, 1])
 1  2  3  4  5 
15 15 15 11  9 

Но мои данные намного больше, а операция выше невыносимо медленная. Есть ли быстрый способ получить тот же результат в data.table / dplyr?

Редактировать: код для генерации данных.

num_customer = 10
m   = 2 * num_customer
a_0 = seq(as.Date("2001-01-01"), as.Date("2001-12-31"), by = "day")
a_1 = matrix(sort(sample(as.character(a_0), m)), nc = 2)
a_2 = list()
for(i in 1:nrow(a_1)){
  a_3 = seq(as.Date(a_1[i, 1]), as.Date(a_1[i, 2]), by = "day")
  a_4 = data.frame(i, as.character(a_3), round(runif(length(a_3), 1)))
  colnames(a_4) = c("c_number", "date", "value")
  a_2[[i]] = a_4
}
DF = dplyr::bind_rows(a_2)
dim(DF)
table(DF[, 1])
dput(DF)

Edit2 :

На 100 000 клиентов DF решение Кристофа Волка является самым быстрым. Далее идет Г. Гротендик (примерно в 4 раза больше времени), затем Натан Верт (еще в 2 раза медленнее Г. Гротендика). Другие решения заметно медленнее. Тем не менее, все предложения быстрее, чем моя предварительная «медленная функция», так что спасибо всем!

1
user189035 1 Сен 2017 в 22:36

5 ответов

Лучший ответ

Попробуй это:

sample15consecutive <- function(DF) {
runs <- rle(DF$c_number)$lengths
start <- ifelse(runs > 15, sapply(pmax(runs-15, 1), sample.int, size=1), 1)
end <- ifelse(runs >= 15, 15, runs)
previous <- cumsum(c(0, head(runs, -1)))
DF[unlist(mapply(seq, previous + start, previous + start + end - 1), length),]
}

Это примерно в 4 раза быстрее, согласно микробенчмарку. C_numbers и даты должны быть отсортированы.

2
Christoph Wolk 1 Сен 2017 в 20:13

Это довольно просто для пакетов tidyverse (в частности, dplyr и tidyr ).

library(tidyverse)

df.sample <- arrange(DF, date) %>% 
  group_by(c_number) %>% 
  do(head(., 15))

Выход (первые 30 строк / 2 сотрудника):

# A tibble: 65 x 3
   c_number       date value
      <int>      <chr> <dbl>
 1        1 2001-01-06     1
 2        1 2001-01-07     1
 3        1 2001-01-08     1
 4        1 2001-01-09     1
 5        1 2001-01-10     1
 6        1 2001-01-11     1
 7        1 2001-01-12     1
 8        1 2001-01-13     1
 9        1 2001-01-14     1
10        1 2001-01-15     1
11        1 2001-01-16     1
12        1 2001-01-17     1
13        1 2001-01-18     1
14        1 2001-01-19     1
15        1 2001-01-20     1
16        2 2001-01-11     1
17        2 2001-01-12     1
18        2 2001-01-13     1
19        2 2001-01-14     1
20        2 2001-01-15     1
21        2 2001-01-16     1
22        2 2001-01-17     1
23        2 2001-01-18     1
24        2 2001-01-19     1
25        2 2001-01-20     1
26        2 2001-01-21     1
27        2 2001-01-22     1
28        2 2001-01-23     1
29        2 2001-01-24     1
30        2 2001-01-25     1
# ... with 35 more rows

Изменить: следующее выбирает случайную дату начала для каждого сотрудника, а затем выбирает до 15 последовательных дней после случайно выбранной точки:

df.sample <- arrange(DF, date) %>% 
  group_by(c_number) %>% 
  mutate(date = as.Date(date), start = sample(date, 1)) %>% 
  filter(date >= start & date <= (start + 14))
1
jdobres 1 Сен 2017 в 20:02

Попробуй это:

library(data.table)

setDT(DF)

DF[
  ,
  {
    if (.N <= 15) {
      # 15 or fewer rows? Grab them all.
      .SD
    } else {
      # Grab a random starting row not too close to the end
      random_start <- sample(seq_len(.N - 14), size = 1)
      .SD[random_start + 0:14]
    }
  },
  by = c_number
]
1
Nathan Werth 1 Сен 2017 в 20:00

samp генерирует вектор из 1 (в выборке) и 0 (из выборки), и мы поднаменяем этим. Я не тестировал его, но он не разбивает DF на подкадры данных, а только разбивает вектор c_number, а затем делает одно подмножество в исходном DF.

samp <- function(x) {
  n <- length(x)
  replace(0*x, seq(sample(max(n - 15, 1), 1), length = min(n, 15)), 1)
}
s <- subset(DF, ave(c_number, c_number, FUN = samp) == 1)
1
G. Grothendieck 1 Сен 2017 в 19:55

Способ ускорения в базе R может состоять в том, чтобы просто работать с индексами, а не со всем data.frame до поднабора.

output = DF[unlist(lapply(
            split(1:NROW(DF), DF$c_number),  #Split indices along rows of DF
            function(x){
                if(length(x) < 15){          #Grab all indices if there are less than 15
                    x
                } else{
                    #Grab an index randomly such that there will be 14 more left after it
                    x[sample(0:(length(x) - 15), 1) + sequence(15)]
                }
            })),
            ]

sapply(split(output, output$c_number), NROW)
# 1  2  3  4  5 
#15 15 15 11  9 
2
d.b 1 Сен 2017 в 20:18