Преобразование/преобразование SQL в левое соединение dplyr с несколькими критериями

Вот установка (это на самом деле не так сложно...):

Таблица Названия должностей

| PersonID | JobTitle | StartDate | EndDate |
|----------|----------|-----------|---------|
| A        | A1       | 1         | 5       |
| A        | A2       | 6         | 10      |
| A        | A3       | 11        | 15      |
| B        | B1       | 2         | 4       |
| B        | B2       | 5         | 7       |
| B        | B3       | 8         | 11      |
| C        | C1       | 5         | 12      |
| C        | C2       | 13        | 14      |
| C        | C3       | 15        | 18      |

Таблица Транзакции:

| PersonID | TransDate | Amt |
|----------|-----------|-----|
| A        | 2         | 5   |
| A        | 3         | 10  |
| A        | 12        | 5   |
| A        | 12        | 10  |
| B        | 3         | 5   |
| B        | 3         | 10  |
| B        | 10        | 5   |
| C        | 16        | 10  |
| C        | 17        | 5   |
| C        | 17        | 10  |
| C        | 17        | 5   |

Желаемый результат:

| PersonID | JobTitle | StartDate | EndDate | Amt |
|----------|----------|-----------|---------|-----|
| A        | A1       | 1         | 5       | 15  |
| A        | A2       | 6         | 10      | 0   |
| A        | A3       | 11        | 15      | 15  |
| B        | B1       | 2         | 4       | 15  |
| B        | B2       | 5         | 7       | 0   |
| B        | B3       | 8         | 11      | 5   |
| C        | C1       | 5         | 12      | 0   |
| C        | C2       | 13        | 14      | 0   |
| C        | C3       | 15        | 18      | 30  |

Этот SQL дает мне желаемый результат:

select jt.PersonID, jt.JobTitle, jt.StartDate, jt.EndDate, coalesce(sum(amt), 0) as amt
from JobTitles jt left join
     Transactions t
     on jt.PersonId = t.PersonId and
        t.TransDate between jt.StartDate and jt.EndDate
group by jt.PersonID, jt.JobTitle, jt.StartDate, jt.EndDate;

Таблицы для R:

JobTitles <- structure(list(PersonID = structure(c(1L, 1L, 1L, 2L, 2L, 2L, 
3L, 3L, 3L), .Label = c("A", "B", "C"), class = "factor"), JobTitle = structure(1:9, .Label = c("A1", 
"A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3"), class = "factor"), 
    StartDate = c(1L, 6L, 11L, 2L, 5L, 8L, 5L, 13L, 15L), EndDate = c(5L, 
    10L, 15L, 4L, 7L, 11L, 12L, 14L, 18L)), .Names = c("PersonID", 
"JobTitle", "StartDate", "EndDate"), class = "data.frame", row.names = c(NA, 
-9L))
Transactions <- structure(list(PersonID = structure(c(1L, 1L, 1L, 1L, 2L, 2L, 
2L, 3L, 3L, 3L, 3L), .Label = c("A", "B", "C"), class = "factor"), 
    TransDate = c(2L, 3L, 12L, 12L, 3L, 3L, 10L, 16L, 17L, 17L, 
    17L), Amt = c(5L, 10L, 5L, 10L, 5L, 10L, 5L, 10L, 5L, 10L, 
    5L)), .Names = c("PersonID", "TransDate", "Amt"), class = "data.frame", row.names = c(NA, 
-11L))

Как перевести SQL в рабочий код dplyr? Я застреваю на left_join:

left_join(JobTitles, Transactions, 
          by = c("PersonID" = "PersonID", 
                 "StartDate" < "TransDate",
                 "EndDate" >= "TransDate"))
# Error: cannot join on columns 'TRUE' x '' : index out of bounds

person JasonAizkalns    schedule 09.12.2014    source источник
comment
Пожалуйста, объясните вашу инструкцию SQL словами для тех, кто не владеет SQL.   -  person talat    schedule 09.12.2014
comment
Недостатком вашего синтаксиса SQL, а также ответов dplyr является то, что мы должны сначала выполнить (левое) соединение, чтобы получить все данные, после чего идет фильтрация, группировка и суммирование. Это будет очень неэффективно (с точки зрения памяти и скорости), если у вас большие данные. Если вы не привязаны к dplyr, извлеките ?foverlaps из data.table или ?findOverlaps из пакетов IRanges (если вы работаете в памяти).   -  person Arun    schedule 09.12.2014
comment
Почему бы просто не использовать пакет sqldf?   -  person zx8754    schedule 09.12.2014
comment
@ zx8754 Я, конечно, мог бы, но я пытаюсь понять это с точки зрения dplyr, и на самом деле я надеялся, что кто-то упомянет что-то об эффективности, как только что сделал @Arun.   -  person JasonAizkalns    schedule 09.12.2014
comment
@ Арун, я сейчас изучаю foverlaps. Глядя на пример в руководстве CRAN, кажется, что вам нужны как начало, так и конец в двух объектах data.table. Это правильно? В этом вопросе фрейм данных Transactions не имеет начала и конца. Он содержит только даты транзакций, которые могут находиться между начальной и конечной датами в другом фрейме данных, JobTitles. Можно ли в этом случае использовать forverlaps?   -  person jazzurro    schedule 10.12.2014
comment
В настоящее время невозможно элегантно выразить это с помощью dplyr.   -  person hadley    schedule 10.12.2014
comment
@arun, но оптимизатор запросов может не реализовать это в базе данных таким образом   -  person hadley    schedule 10.12.2014
comment
@jazzurro, да, создав фиктивный столбец. проверьте это, это или этот пост..   -  person Arun    schedule 10.12.2014
comment
@hadley, ты знаешь, как это делается внутри? Либо он должен сравнивать диапазоны + подмножество для каждой строки, что будет эффективно использовать память, но очень-очень медленно (проверьте этот пост) или он должен сначала объединиться + подмножество + суммировать, что будет немного быстрее, но будет неэффективно с точки зрения использования памяти. Я не понимаю, как это может быть эффективно с точки зрения скорости и памяти.   -  person Arun    schedule 10.12.2014
comment
@Arun Большое спасибо за ссылку. Я проверю их завтра. :)   -  person jazzurro    schedule 10.12.2014
comment
@arun Я не знаю, мне было интересно, есть ли у вас какие-то специальные знания о том, что в этом случае не было возможной оптимизации или что БД определенно не применяла известные оптимизации   -  person hadley    schedule 11.12.2014
comment
@hadley, я уже указал вам на ссылку, которая оценивает (как скорость, так и память) код sqldf вместе с foverlaps (77 с против 3,8 с, 1,4 ГБ против 1 ГБ) только в части соединения (что похоже на запрос OP здесь). И, насколько мне известно, задача соединения интервалов не может быть выполнена более эффективно, чем алгоритмы, предназначенные для эффективной работы с интервалами. Я не знаю баз данных, способных оптимизировать более эффективно, чем интервальные соединения. Кажется, вы говорите об оптимизации, но не знаете, какие оптимизации могут сделать ее (более) эффективной, что сбивает с толку.   -  person Arun    schedule 11.12.2014
comment
@Arun sqldf использует sqlite, у которого нет особенно хорошего механизма запросов. Я не понимаю, почему вы думаете, что база данных не может реализовать эффективный алгоритм интервальных соединений.   -  person hadley    schedule 11.12.2014
comment
@hadley, мой комментарий был о конкретном запросе OP (отсюда ваш синтаксис SQL). Я выполнил тот же запрос, используя sqldf с PostgreSQL - это было в 3 раза медленнее. Опять же, если у вас есть конкретные оптимизации, дайте мне знать, и я буду рад попробовать (и изменить свое мнение). Я не знаю, это сильно отличается от того, что я думаю, что это невозможно реализовать. Мои тесты пока не показали никакой разницы. Пожалуйста, покажите пример, если вы считаете, что его можно улучшить.   -  person Arun    schedule 11.12.2014
comment
@hadley (и другие), я задал вопрос здесь, на SO, с просьбой о максимально эффективной оптимизации (но не еще не добился успеха). Но я нашел этот пост после публикации, где OP, похоже, прибегал к интервальным деревьям (но не ясно, если это в SQL). Я довольно много искал на SO без успеха. Кажется, я не могу найти оптимизированный запрос для обработки этого случая. Было бы неплохо, если бы вы могли указать мне ссылку или предоставить решение о том, как эффективно оптимизировать этот запрос в SQL.   -  person Arun    schedule 12.12.2014


Ответы (1)


Подобно идее @zx8754, я придумал следующее. Я пытался использовать между, потому что это было в сценарии SQL. Но результат в основном идентичен тому, что есть у @zx8754. Далее я сделал расчет и получил результат (т.е. foo). Затем я объединил его с двумя столбцами (например, PersonID JobTitle) из JobTitles, чтобы получить ожидаемый результат.

foo <- left_join(JobTitles, Transactions) %>%
       rowwise() %>%
       mutate(check = between(TransDate, StartDate, EndDate)) %>%
       filter(check == TRUE) %>%
       group_by(PersonID, JobTitle) %>%
       summarise(total = sum(Amt))

### Merge the master frame including all combs of PersonID and JobTitle, and foo
foo2 <- left_join(JobTitles[,c(1,2)], foo)

### NA to 0 
foo2$total[which(foo2$total %in% NA)] <- 0

#  PersonID JobTitle total
#1        A       A1    15
#2        A       A2     0
#3        A       A3    15
#4        B       B1    15
#5        B       B2     0
#6        B       B3     5
#7        C       C1     0
#8        C       C2     0
#9        C       C3    30

Или чуть короче и в одну трубу:

left_join(JobTitles, Transactions) %>%
  filter(TransDate > StartDate & TransDate < EndDate) %>%
  group_by(PersonID, JobTitle) %>%
  summarise(total = sum(Amt)) %>%
  left_join(JobTitles[,c(1,2)], .) %>%
  mutate(total = replace(total, is.na(total), 0))
person jazzurro    schedule 09.12.2014
comment
надеюсь вы не против, что я добавил версию в один пайп - person talat; 09.12.2014
comment
@beginneR Эй, чувак, извини за поздний ответ. Я лег спать сразу после публикации ответа. Ваша идея великолепна! Это путь, если вы делаете все в трубопроводе. Я тоже думал об использовании replace, но не стал заморачиваться. Спасибо Вам за Ваш вклад! - person jazzurro; 10.12.2014