данные.таблица vs dplyr: может ли один сделать что-то хорошо, а другой не может или делает плохо?


обзор

я относительно знаком с data.table, не так много с dplyr. Я прочитал некоторые dplyr виньеток и примеры, которые выскочили на SO, и до сих пор мои выводы таковы:

  1. data.table и dplyr сопоставимы по скорости, за исключением тех случаев, когда есть много (т. е. >10-100K) групп, а также в некоторых других обстоятельствах (см. тесты ниже)
  2. dplyr имеет более доступными синтаксис
  3. dplyr абстракты (или воля) потенциальные взаимодействия с БД
  4. есть некоторые незначительные функциональные различия (см. раздел "примеры использования" ниже)

в моем сознании 2. не несет большого веса, потому что я довольно хорошо знаком с ним data.table, хотя я понимаю, что для новичков в обоих это будет большой фактор. Я хотел бы избежать аргумента о том, что является более интуитивным, поскольку это не имеет отношения к моему конкретному вопросу, заданному из перспектива кого-то уже знакомого с data.table. Я также хотел бы избежать обсуждения того, как" более интуитивный " приводит к более быстрому анализу (конечно, верно, но опять же, не то, что меня больше всего интересует здесь).

вопрос

то, что я хочу знать:

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

один недавний вопрос SO заставил меня задуматься об этом немного больше, потому что до этого момента я не думал dplyr предложил бы гораздо больше того, что я уже могу сделать в data.table. Вот это dplyr решение (данные в конце Q):

dat %.%
  group_by(name, job) %.%
  filter(job != "Boss" | year == min(year)) %.%
  mutate(cumu_job2 = cumsum(job2))

что было намного лучше, чем моя попытка взломать data.table решение. Тем не менее, хорошо data.table решения также довольно хороши (спасибо Жан-Роберу, Аруну и обратите внимание, что здесь я предпочел одно утверждение строго наиболее оптимальному решению):

setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

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

в идеале я хотел бы увидеть несколько хороших примеров были те dplyr или data.table путь существенно более сжат или выполняет существенно более лучше.

примеры

Использование
  • dplyr не позволяет группировать операции, возвращающие произвольное количество строк (от Эдди вопрос, Примечание: похоже, что это будет реализовано в dplyr 0.5, кроме того, @beginneR показывает потенциальную работу с использованием do в ответ на @реитсталл вопрос.)
  • data.table поддерживает подвижного соединения (спасибо @dholstius), а также перекрытие присоединяется
  • data.table внутренне оптимизирует выражения вида DT[col == value] или DT[col %in% values] на скорость через автоматическое индексирование использует бинарный поиск в то время как, используя тот же базовый синтаксис Р. посмотреть здесь для получения более подробной информации и крошечного эталона.
  • dplyr предлагает стандартные оценочные версии функций (например regroup,summarize_each_), что может упростить программное использование dplyr (обратите внимание на программное использование data.table определенно возможно, просто требует некоторых тщательных размышлений, замены / цитирования и т. д., По крайней мере, насколько мне известно)
Стандарты

сведения

это первый пример, который я показал в разделе Вопрос.

dat <- structure(list(id = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L), name = c("Jane", "Jane", "Jane", "Jane", 
"Jane", "Jane", "Jane", "Jane", "Bob", "Bob", "Bob", "Bob", "Bob", 
"Bob", "Bob", "Bob"), year = c(1980L, 1981L, 1982L, 1983L, 1984L, 
1985L, 1986L, 1987L, 1985L, 1986L, 1987L, 1988L, 1989L, 1990L, 
1991L, 1992L), job = c("Manager", "Manager", "Manager", "Manager", 
"Manager", "Manager", "Boss", "Boss", "Manager", "Manager", "Manager", 
"Boss", "Boss", "Boss", "Boss", "Boss"), job2 = c(1L, 1L, 1L, 
1L, 1L, 1L, 0L, 0L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L)), .Names = c("id", 
"name", "year", "job", "job2"), class = "data.frame", row.names = c(NA, 
-16L))
3 590

3 ответа:

мы должны охватить по крайней мере эти аспекты, чтобы дать исчерпывающий ответ/сравнение (без особого порядка важности): Speed,Memory usage,Syntax и Features.

мое намерение состоит в том, чтобы охватить каждый из них как можно более четко из данных.таблица перспектива.

Примечание: если явно не указано иное, ссылаясь на dplyr, мы ссылаемся на данные dplyr.интерфейс фрейма, внутренние элементы которого находятся в C++ с использованием Rcpp.


данные.синтаксис таблицы непротиворечив в своей форме -DT[i, j, by]. Чтобы сохранить i,j и by вместе с дизайном. Сохраняя связанные операции вместе, это позволяет оптимизировать операции скорость и что еще более важно использование памяти, а также мощные функции, все при сохранении согласованности в синтаксисе.

1. Скорость

довольно много тестов (хотя в основном по операциям группировки) были добавлены к вопросу, уже показывающему данные.таблица получает быстрее чем dplyr как количество групп и/или строк в группе, в том числе бенчмарки от Matt по группировке из от 10 миллионов до 2 миллиардов строк (100 ГБ в оперативной памяти) на 100 - 10 миллионов участников!--123--> и различные столбцы группировки, которые также сравниваются pandas.

на бенчмарках было бы здорово охватить и эти оставшиеся аспекты:

  • группировка операций с участием подмножество строк - то есть,DT[x > val, sum(y), by = z] операции типа.

  • тест другие операции, такие как обновление и соединения.

  • также Тест объем памяти для каждой операции в дополнение к времени выполнения.

2. Использование памяти

  1. операции filter() или slice() в dplyr может быть неэффективна память (на обоих данных.кадры и данные.таблицы.) этот пост.

    обратите внимание, что Хэдли переговоры о скорость (что dplyr в изобилии быстро для него), в то время как основная проблема здесь .

  2. данные.интерфейс таблицы на данный момент позволяет изменять/обновлять столбцы по ссылке (обратите внимание, что нам не нужно повторно присваивать результат переменной).

    # sub-assign by reference, updates 'y' in-place
    DT[x >= 1L, y := NA]
    

    но dplyr никогда обновление ПО ССЫЛКЕ. Эквивалент dplyr будет (обратите внимание, что результат должен быть повторно назначен):

    # copies the entire 'y' column
    ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))
    

    беспокойство по этому поводу референциальной прозрачности. Обновление данных.объект таблицы по ссылке, особенно в пределах функции, может быть не всегда желательным. Но это невероятно полезная функция: см.этой и этой сообщения для интересных случаев. И мы хотим его сохранить.

    поэтому мы работаем в направлении экспорта в данных.таблица, которая предоставит пользователю обе возможности. Для например, если желательно не изменять входные данные.таблица внутри функции, затем можно сделать:

    foo <- function(DT) {
        DT = shallow(DT)          ## shallow copy DT
        DT[, newcol := 1L]        ## does not affect the original DT 
        DT[x > 2L, newcol := 2L]  ## no need to copy (internally), as this column exists only in shallow copied DT
        DT[x > 2L, x := 3L]       ## have to copy (like base R / dplyr does always); otherwise original DT will 
                                  ## also get modified.
    }
    

    не используя shallow(), старая функциональность сохраняется:

    bar <- function(DT) {
        DT[, newcol := 1L]        ## old behaviour, original DT gets updated by reference
        DT[x > 2L, x := 3L]       ## old behaviour, update column x in original DT.
    }
    

    создать мелкая копия используя shallow(), мы понимаем, что вы не хотите изменять исходный объект. Мы заботимся обо всем внутренне, чтобы гарантировать, что в то же время обеспечивая копирование столбцов, которые вы изменяете только когда это абсолютно необходимо. При реализации, это должно урегулировать референциальной прозрачности проблема в целом, предоставляя пользователю обе возможности.

    кроме того, один раз shallow() экспортируются данные dplyr.интерфейс таблицы должен избегать почти всех копий. Поэтому те, кто предпочитает синтаксис dplyr, могут использовать его с данными.таблицы.

    но ему все равно будет не хватать многих функций этих данных.таблица предусматривает, в том числе (sub)-присвоение по ссылка.

  3. агрегат при присоединении:

    Предположим, у вас есть два данных.таблицы следующим образом:

    DT1 = data.table(x=c(1,1,1,1,2,2,2,2), y=c("a", "a", "b", "b"), z=1:8, key=c("x", "y"))
    #    x y z
    # 1: 1 a 1
    # 2: 1 a 2
    # 3: 1 b 3
    # 4: 1 b 4
    # 5: 2 a 5
    # 6: 2 a 6
    # 7: 2 b 7
    # 8: 2 b 8
    DT2 = data.table(x=1:2, y=c("a", "b"), mul=4:3, key=c("x", "y"))
    #    x y mul
    # 1: 1 a   4
    # 2: 2 b   3
    

    и вы хотели бы получить sum(z) * mul на каждой строке DT2 при соединении по столбцам x,y. Мы можем либо:

    • 1) совокупность DT1 и sum(z), 2) выполнить соединение и 3) умножить (или)

      # data.table way
      DT1[, .(z = sum(z)), keyby = .(x,y)][DT2][, z := z*mul][]
      
      # dplyr equivalent
      DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
          right_join(DF2) %>% mutate(z = z * mul)
      
    • 2) сделать все это за один раз (используя by = .EACHI характеристика):

      DT1[DT2, list(z=sum(z) * mul), by = .EACHI]
      

    в чем преимущество?

    • нам не нужно выделять память для промежуточных результатов.

    • нам не нужно группировать/хэшировать дважды (один для агрегации, а другой для присоединения).

    • и что еще более важно, операция, которую мы хотели выполнить, понятна, глядя на j in (2).

    Регистрация этот пост подробное описание by = .EACHI. Промежуточные результаты не материализуются, и агрегат join+выполняется за один раз.

    посмотреть этой,этой и этой сообщения для реальных сценариев использования.

    In dplyr вы должны сначала присоединиться и агрегировать или агрегировать, а затем присоединиться, ни из которых столь же эффективны, с точки зрения памяти (что в свою очередь переводится на скорость).

  4. обновление и присоединяется:

    считать данные.код таблицы, показанный ниже:

    DT1[DT2, col := i.mul]
    

    добавляет/обновления С col С mul С DT2 на тех строках, где DT2ключевой столбец соответствует DT1. Я не думаю, что есть точный эквивалент этой операции в dplyr, т. е., не избежать *_join операции, которые придется скопировать весь DT1 просто добавить новый столбец к нему, что не нужно.

    Регистрация этот пост для реального сценария использования.

подводя итог, важно понимать, что каждый кусочек имеет значение оптимизации. Как Грейс Хоппер хочу сказать, следите за своими наносекундами!

3. Синтаксис

Давайте теперь посмотрим на синтаксис. Хэдли прокомментировал здесь:

таблицы данных очень быстро, но я думаю, что их краткость делает его труднее учиться и код, который использует его труднее читать после написания ...

я нахожу это замечание бессмысленным, потому что оно очень субъективно. То, что мы, возможно, можем попытаться противопоставить последовательность в синтаксисе. Мы сравним данные.таблица и синтаксис dplyr бок о бок.

мы будем работать с фиктивными данными показано ниже:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. основные операции агрегации / обновления.

    # case (a)
    DT[, sum(y), by = z]                       ## data.table syntax
    DF %>% group_by(z) %>% summarise(sum(y)) ## dplyr syntax
    DT[, y := cumsum(y), by = z]
    ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
    
    # case (b)
    DT[x > 2, sum(y), by = z]
    DF %>% filter(x>2) %>% group_by(z) %>% summarise(sum(y))
    DT[x > 2, y := cumsum(y), by = z]
    ans <- DF %>% group_by(z) %>% mutate(y = replace(y, which(x > 2), cumsum(y)))
    
    # case (c)
    DT[, if(any(x > 5L)) y[1L]-y[2L] else y[2L], by = z]
    DF %>% group_by(z) %>% summarise(if (any(x > 5L)) y[1L] - y[2L] else y[2L])
    DT[, if(any(x > 5L)) y[1L] - y[2L], by = z]
    DF %>% group_by(z) %>% filter(any(x > 5L)) %>% summarise(y[1L] - y[2L])
    
    • данные.синтаксис таблицы компактен и dplyr довольно многословен. Вещи более или менее эквивалентны в случае (а).

    • в случае (b), мы должны были использовать filter() в dplyr пока подводя итоги. Но пока обновление, мы должны были переместить логику внутри mutate(). В данных.таблица однако, мы выражаем обе операции с той же логикой-работать на строках, где x > 2, но в первом случае, получить sum(y), тогда как во втором случае обновить эти строки y с накопленной суммой.

      вот что мы имеем в виду, когда говорим DT[i, j, by] форма последовательно.

    • аналогично в случае (c), когда у нас есть if-else условие, мы можем выразить логику "в" в обоих данных.стол и dplyr. Однако, если мы хотели бы вернуть только те строки, где if условию удовлетворяет и пропустить в противном случае, мы не можем использовать summarise() непосредственно (AFAICT). Мы должны filter() сначала подведем итоги, потому что summarise() всегда ожидает одно значение.

      в то время как он возвращает тот же самый результат, используя filter() здесь делает фактическая работа менее очевидна.

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

  2. агрегация / обновление по нескольким столбцам

    # case (a)
    DT[, lapply(.SD, sum), by = z]                     ## data.table syntax
    DF %>% group_by(z) %>% summarise_each(funs(sum)) ## dplyr syntax
    DT[, (cols) := lapply(.SD, sum), by = z]
    ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
    
    # case (b)
    DT[, c(lapply(.SD, sum), lapply(.SD, mean)), by = z]
    DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
    
    # case (c)
    DT[, c(.N, lapply(.SD, sum)), by = z]     
    DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
    
    • в случае (a) коды более или менее эквивалентны. данные.таблица использует знакомую базовую функцию lapply(), а dplyr вводит *_each() вместе с кучей функций funs().

    • данные.стол-это := требует, чтобы имена столбцов были предоставлены, в то время как dplyr генерирует его автоматически.

    • в случае (b) синтаксис dplyr относительно прост. Улучшение агрегации / обновления по нескольким функциям находится на данных.список таблиц.

    • в случае (c) хотя, dplyr вернется n() столько раз, сколько столбцов, а не только один раз. В данных.таблица, все, что нам нужно сделать, это вернуть список в j. Каждый элемент списка станет столбцом в результате. Таким образом, мы можем использовать, еще раз, знакомую базовую функцию c() для объединения .N до list который возвращает a list.

    Примечание: еще раз, в данные.стол, все, что нам нужно сделать, это вернуть список в j. Каждый элемент списка станет столбцом в результате. Вы можете использовать c(), as.list(),lapply(),list() etc... базовые функции для достижения этой цели, без необходимости изучать какие-либо новые функции.

    вам нужно будет узнать только специальные переменные -.N и .SD по крайней мере. Эквивалентом в dplyr являются n() и .

  3. соединения

    dplyr предоставляет отдельные функции для каждого типа соединения, где как данные.таблица позволяет соединять с использованием того же синтаксиса DT[i, j, by] (и с причина.) Он также предоставляет эквивалент в данных.таблица, которая позволяет очень быстро переупорядочить данные.таблицы по ссылке.

  4. dplyr обеспечивает интерфейс к базам данных используя тот же синтаксис, что данные.таблицы нет на данный момент.

  5. data.table обеспечивает более быстрые эквиваленты set operations от v1.9. 7+ (написано Яном Горецким) - fsetdiff,fintersect,funion и fsetequal дополнительные

вот моя попытка дать исчерпывающий ответ с точки зрения dplyr, следуя широкому контуру ответа Аруна (но несколько перестроенного на основе различных приоритетов).

синтаксис

есть некоторая субъективность в синтаксисе, но я стою на своем утверждении, что сжатие данных.таблица делает его труднее учиться и труднее читать. Отчасти это связано с тем, что dplyr решает гораздо более простую проблему!

одна очень важная вещь, которую делает dplyr для вас это так и есть ограничения ваши варианты. Я утверждаю, что большинство проблем с одной таблицей могут решается всего с помощью пяти ключевых глаголов фильтр, выберите, мутировать, упорядочить и резюмируйте вместе с наречием" по группе". Это ограничение является большой помощью когда вы изучаете манипуляции с данными, потому что это помогает упорядочить ваши думая о проблеме. В dplyr каждый из этих глаголов сопоставляется с a одна функция. Каждая функция выполняет одну работу, и ее легко понять в изоляции.

вы создайте сложность путем пронзать эти простые деятельности вместе с %>%. Вот пример из одного из постов Аруна связаны к:

diamonds %>%
  filter(cut != "Fair") %>%
  group_by(cut) %>%
  summarize(
    AvgPrice = mean(price),
    MedianPrice = as.numeric(median(price)),
    Count = n()
  ) %>%
  arrange(desc(Count))

даже если вы никогда не видели dplyr раньше (или даже R!), вы все еще можете получить суть того, что происходит, потому что функции все английские глаголы. Недостатком английских глаголов является то, что они требуют больше ввода, чем [, но я думаю, что это может быть в значительной степени смягчено лучшим автозаполнением.

здесь эквивалентные данные.столик код:

diamondsDT <- data.table(diamonds)
diamondsDT[
  cut != "Fair", 
  .(AvgPrice = mean(price),
    MedianPrice = as.numeric(median(price)),
    Count = .N
  ), 
  by = cut
][ 
  order(-Count) 
]

это труднее следовать этому коду, если вы уже знакомы с данные.стол. (Я также не мог понять, как отступить повторенный [ в том смысле, что это хорошо выглядит на мой взгляд). Лично, когда я смотрю на код I написал 6 месяцев назад, это как смотреть на код, написанный незнакомцем, поэтому я предпочитаю простой, хотя и многословный код.

два других незначительных фактора, которые я думаю, немного уменьшаются читаемость:

  • так как почти каждая операция таблицы данных использует [ вам нужна дополнительная контекст, чтобы выяснить, что происходит. Например, это x[y] объединение двух таблиц данных или извлечение столбцов из фрейма данных? Это только небольшая проблема, потому что в хорошо написанном коде имена переменных должны подсказывать, что происходит.

  • мне нравится group_by() это отдельная операция в dplyr. Оно принципиально меняет вычисление так Я думаю, должно быть очевидно при просмотре кода, и это легче обнаружить group_by() чем элемент до [.data.table.

мне тоже нравится, что трубы не ограничивается только одним пакетом. Вы можете начать с уборки данные с tidyr, и закончите с сюжетом в ggvis. И ты не ограничиваясь пакетами, которые я пишу - любой может написать функцию это образует бесшовную часть данных труба манипуляции. На самом деле, я скорее отдайте предпочтение предыдущим данным.код таблицы переписан с помощью %>%:

diamonds %>% 
  data.table() %>% 
  .[cut != "Fair", 
    .(AvgPrice = mean(price),
      MedianPrice = as.numeric(median(price)),
      Count = .N
    ), 
    by = cut
  ] %>% 
  .[order(-Count)]

и идея трубопровода с %>% не ограничивается только фреймами данных и легко обобщается на другие контексты:интерактивные веб графика, web выскабливание, логи,выполнить времени контракты, ...)

память и производительность

я объединил вместе, потому что для меня, они не так важны. Большинство пользователей R работают с более чем 1 миллионом строк данных, а dplyr-это достаточно быстро для такого размера данных, о котором вы не знаете время обработки. Мы оптимизируем dplyr для выразительности на средних данных; не стесняйтесь использовать данные.таблица для необработанной скорости на больших данных.

гибкость dplyr также означает, что вы можете легко настроить производительность характеристики, использующие тот же синтаксис. Если производительность dplyr с бэкэнд фрейма данных не очень хорош достаточно для вас, вы можете использовать данные.бэкэнд таблицы (хотя и с несколько ограниченным набором функций). Если данные, с которыми вы работаете, не помещаются в память, вы можете использовать бэкэнд базы данных.

все, что сказал, производительность dplyr станет лучше в долгосрочной перспективе. Мы определенно реализовать некоторые из великих идей данных.таблица как представление упорядочение и использование одного и того же индекса для соединений и фильтров. Мы тоже работа над распараллеливанием, чтобы мы могли воспользоваться несколькими сердце.

особенности

несколько вещей, над которыми мы планируем работать в 2015 году:

  • the readr пакета, чтобы сделать его легко получить файлы с диска и в на память, аналогично fread().

  • более гибкие соединения, включая поддержку неэкви-соединений.

  • более гибкая группировка, такая как образцы bootstrap, rollups и многое другое

я тоже вкладывают время на улучшение R

в прямом ответе на Заголовок Вопрос...

dplyrнаверняка делает вещи, которые data.table не может.

ваша точка зрения #3

dplyr абстрагирует (или будет) потенциальные взаимодействия с БД

является прямым ответом на ваш собственный вопрос, но не повышен до достаточно высокого уровня. dplyr действительно расширяемый интерфейс для нескольких механизмов хранения данных, где как data.table это расширение до одного.

посмотреть dplyr как внутренний агностический интерфейс, со всеми целями, использующими тот же самый Граммер, где вы можете расширить цели и обработчики по желанию. data.table С dplyr перспектива, одна из этих целей.

вы никогда (я надеюсь) не увидите день, что data.table пытается перевести ваши запросы для создания инструкций SQL, которые работают с дисковыми или сетевыми хранилищами данных.

dplyr можем сделать вещи data.table не будет или не может сделать, как хорошо.

основанный на дизайне работы в-памяти,data.table может быть гораздо сложнее расширить себя в параллельную обработку запросов, чем dplyr.


в ответ на внутренние вопросы...

использование

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

это может показаться плоскодонкой, но реальный ответ-нет. Люди знакомый с инструментами, похоже, используют либо тот, который наиболее знаком им, либо тот, который на самом деле является правильным для работы под рукой. С учетом сказанного, иногда вы хотите представить определенную читаемость, иногда уровень производительности, а также когда у вас есть потребность в достаточно высоком уровне обоих вам может просто понадобиться еще один инструмент, чтобы идти вместе с тем, что у вас уже есть, чтобы сделать более четкие абстракции.

производительность

существуют ли аналитические задачи, которые выполняются существенно (т. е. более чем в 2 раза) более эффективно в одном пакете по сравнению с другим.

опять же, нет. data.table преуспевает в том, чтобы быть эффективным во всем это место dplyr получает бремя ограничения в некоторых отношениях базового хранилища данных и зарегистрированных обработчиков.

это означает, когда вы сталкиваетесь с проблемой производительности с data.table вы можете быть уверены, это в функции запроса и, если это и на самом деле узкое место с data.table тогда вы выиграли себе радость подачи отчета. Это также верно, когда dplyr использует data.table как бэк-энд; вы мая посмотреть некоторые накладные расходы от dplyr но скорее всего, это ваш запрос.

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

Также см. принятый ответ на когда plyr лучше, чем данные.стол?