dplyr na data.table, czy naprawdę używam data.table?

91

Jeśli używam składni dplyr na szczycie datatable , czy uzyskam wszystkie korzyści związane z szybkością datatable, nadal używając składni dplyr? Innymi słowy, czy niewłaściwie używam datatable, jeśli wykonuję zapytanie za pomocą składni dplyr? Czy też muszę używać czystej składni datatable, aby wykorzystać całą jego moc.

Z góry dziękuję za wszelkie rady. Przykład kodu:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

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

Wyniki:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

Oto równoważność danych, którą wymyśliłem. Nie jestem pewien, czy jest zgodny z dobrą praktyką DT. Ale zastanawiam się, czy kod jest naprawdę wydajniejszy niż składnia dplyr za kulisami:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]
Polimeraza
źródło
7
Dlaczego nie użyłbyś składni tabeli danych? Jest również elegancki i wydajny. Na to pytanie nie da się odpowiedzieć, ponieważ jest bardzo szerokie. Tak, istnieją dplyrmetody dla tabel danych, ale tabela danych ma również swoje własne porównywalne metody
Rich Scriven,
7
Potrafię używać składni datatable lub kursu. Ale jakoś uważam, że składnia dplyr jest bardziej elegancka. Niezależnie od moich preferencji co do składni. To, co naprawdę chcę wiedzieć, to: czy muszę używać czystej składni datatable, aby uzyskać 100% korzyści z mocy datatable.
Polymerase
3
Aby zapoznać się z najnowszym benchmarkiem, w którym dplyrjest używany na data.frames i odpowiadających data.tables, patrz tutaj (i odnośniki tam).
Henrik
2
@Polymerase - Myślę, że odpowiedź na to pytanie brzmi zdecydowanie „Tak”
Rich Scriven
1
@Henrik: Później zdałem sobie sprawę, że źle zinterpretowałem tę stronę, ponieważ wyświetlała tylko kod konstrukcji dataframe, ale nie kod, którego użyli do konstrukcji data.table. Kiedy zdałem sobie z tego sprawę, usunąłem swój komentarz (mając nadzieję, że go nie widziałeś).
IRTFM

Odpowiedzi:

77

Nie ma prostej / prostej odpowiedzi, ponieważ filozofie obu tych pakietów różnią się w pewnych aspektach. Dlatego niektórych kompromisów nie da się uniknąć. Oto niektóre z problemów, którymi możesz się zająć / rozważyć.

Operacje obejmujące i(== filter()i slice()w dplyr)

Załóżmy, że DTpowiedzmy 10 kolumn. Rozważ następujące wyrażenia data.table:

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1) podaje liczbę wierszy, w DTktórych kolumna a > 1. (2) zwraca mean(b)pogrupowane według c,dtego samego wyrażenia w i(1).

Powszechnie używanymi dplyrwyrażeniami byłyby:

DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)

Oczywiście kody data.table są krótsze. Ponadto są również bardziej wydajne pod względem pamięci 1 . Czemu? Ponieważ w obu (3) i (4) najpierw filter()zwraca wiersze dla wszystkich 10 kolumn , gdy w (3) potrzebujemy tylko liczby wierszy, aw (4) potrzebujemy tylko kolumn b, c, ddo kolejnych operacji. Aby temu zaradzić, musimy do select()kolumn apriori:

DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)

Konieczne jest podkreślenie głównej różnicy filozoficznej między tymi dwoma pakietami:

  • W programie data.tablelubimy trzymać te powiązane operacje razem, co pozwala spojrzeć na j-expression(z tego samego wywołania funkcji) i zdać sobie sprawę, że nie ma potrzeby stosowania żadnych kolumn w (1). Wyrażenie w ijest obliczane i .Njest po prostu sumą tego wektora logicznego, który daje liczbę wierszy; cały podzbiór nigdy nie jest realizowany. W (2) tylko kolumny b,c,dsą materializowane w podzbiorze, inne kolumny są ignorowane.

  • Ale dplyrfilozofia ma mieć funkcję zrobić dokładnie jedną rzecz dobrze . Nie ma (przynajmniej obecnie) sposobu, aby stwierdzić, czy operacja po filter()wymaga wszystkich tych kolumn, które przefiltrowaliśmy. Jeśli chcesz efektywnie wykonywać takie zadania, musisz myśleć z wyprzedzeniem. Osobiście uważam, że w tym przypadku jest to sprzeczne z intymnością.

Zauważ, że w (5) i (6) nadal mamy podzbiór kolumny, aktórej nie wymagamy. Ale nie jestem pewien, jak tego uniknąć. Gdyby filter()funkcja miała argument do wybierania kolumn do zwrócenia, moglibyśmy uniknąć tego problemu, ale wtedy funkcja nie wykona tylko jednego zadania (co jest również wyborem projektu dplyr).

Przypisanie podrzędne przez odniesienie

dplyr nigdy nie będzie aktualizować przez odniesienie. To kolejna ogromna (filozoficzna) różnica między tymi dwoma pakietami.

Na przykład w data.table możesz:

DT[a %in% some_vals, a := NA]

która aktualizuje kolumnę a przez odwołanie tylko w tych wierszach, które spełniają warunek. W tej chwili dplyr deep kopiuje wewnętrznie całą tabelę data.table, aby dodać nową kolumnę. @BrodieG już o tym wspomniał w swojej odpowiedzi.

Ale głęboka kopia może zostać zastąpiona płytką kopią, gdy wdrożony jest FR # 617 . Dotyczy również: dplyr: FR # 614 . Zwróć uwagę, że nadal modyfikowana kolumna będzie zawsze kopiowana (w związku z tym odrobinę wolniejsza / mniej wydajna pamięć). Nie będzie możliwości aktualizowania kolumn przez odniesienie.

Inne funkcjonalności

  • W data.table można agregować podczas łączenia, co jest łatwiejsze do zrozumienia i wydajne pod względem pamięci, ponieważ wynik łączenia pośredniego nigdy nie jest materializowany. Zobacz przykład w tym poście . Nie możesz (w tej chwili?) Tego zrobić, używając składni data.table / data.frame programu dplyr.

  • Funkcja łączenia obrotowego data.table nie jest również obsługiwana w składni dplyr.

  • Niedawno zaimplementowaliśmy łączenie nakładające się w data.table, aby łączyć w zakresach interwałów ( tutaj jest przykład ), co jest obecnie oddzielną funkcją foverlaps()i dlatego może być używane z operatorami potoków (magrittr / pipeR? - nigdy tego nie próbowałem).

    Ale ostatecznie naszym celem jest zintegrowanie go [.data.table, abyśmy mogli zebrać inne funkcje, takie jak grupowanie, agregowanie podczas łączenia itp., Które będą miały te same ograniczenia, które opisano powyżej.

  • Od wersji 1.9.4 data.table implementuje automatyczne indeksowanie przy użyciu kluczy pomocniczych do szybkiego wyszukiwania binarnego podzbiorów opartych na zwykłej składni języka R. Np .: DT[x == 1]i DT[x %in% some_vals]automatycznie utworzy indeks przy pierwszym uruchomieniu, który będzie następnie używany w kolejnych podzbiorach z tej samej kolumny do szybkiego podzbioru przy użyciu wyszukiwania binarnego. Ta funkcja będzie ewoluować. Sprawdź to streszczenie, aby uzyskać krótki przegląd tej funkcji.

    Ze sposobu, w jaki filter()jest zaimplementowany dla data.tables, nie wykorzystuje tej funkcji.

  • Cechą dplyr jest to, że zapewnia również interfejs do baz danych używających tej samej składni, której data.table obecnie nie ma.

Będziesz więc musiał rozważyć te (i prawdopodobnie inne punkty) i zdecydować na podstawie tego, czy te kompromisy są dla Ciebie akceptowalne.

HTH


(1) Należy pamiętać, że wydajność pamięci bezpośrednio wpływa na szybkość (zwłaszcza gdy dane stają się większe), ponieważ w większości przypadków wąskim gardłem jest przenoszenie danych z pamięci głównej do pamięci podręcznej (i maksymalne wykorzystanie danych w pamięci podręcznej - zmniejsz liczbę braków w pamięci podręcznej) - aby ograniczyć dostęp do pamięci głównej). Nie wchodząc tutaj w szczegóły.

Bieg
źródło
4
Absolutnie genialne. Dzięki za to
David Arenburg,
6
To dobra odpowiedź, ale byłoby możliwe (jeśli nie jest prawdopodobne) dla dplyr zaimplementowanie wydajnego filter()plusa summarise()przy użyciu tego samego podejścia, którego używa dplyr dla SQL - tj. Zbudowanie wyrażenia, a następnie wykonanie tylko raz na żądanie. Jest mało prawdopodobne, że zostanie to zaimplementowane w najbliższej przyszłości, ponieważ dplyr jest dla mnie wystarczająco szybki, a implementacja planowania zapytań / optymalizatora jest stosunkowo trudna.
Hadley
Wydajność pamięci pomaga również w innym ważnym obszarze - faktycznie wykonaniu zadania przed wyczerpaniem pamięci. Podczas pracy z dużymi zbiorami danych napotkałem ten problem zarówno z dplyr, jak i pandami, podczas gdy data.table wykonałaby zadanie z wdziękiem.
Zaki
25

Po prostu spróbuj.

library(rbenchmark)
library(dplyr)
library(data.table)

benchmark(
dplyr = diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair", 
                        list(AvgPrice = mean(price),
                             MedianPrice = as.numeric(median(price)),
                             Count = .N), by = cut][order(-Count)])[1:4]

W przypadku tego problemu wygląda na to, że data.table jest 2,4x szybsza niż dplyr używająca data.table:

        test replications elapsed relative
2 data.table          100    2.39    1.000
1      dplyr          100    5.77    2.414

Zmieniono na podstawie komentarza Polymerase.

G. Grothendieck
źródło
2
Korzystając z microbenchmarkpakietu stwierdziłem, że uruchomienie dplyrkodu OP na oryginalnej (ramce danych) wersji programu diamondszajęło średnio 0,012 sekundy, podczas gdy mediana czasu zajęła 0,024 sekundy po konwersji diamondsdo tabeli danych. Uruchomienie data.tablekodu G. Grothendiecka zajęło 0,013 sekundy. Przynajmniej na moim systemie wygląda dplyri data.tablema mniej więcej taką samą wydajność. Ale dlaczego miałoby dplyrbyć wolniej, gdy ramka danych jest najpierw konwertowana na tabelę danych?
eipi10
Drogi G. Grothendieck, to jest wspaniałe. Dziękuję za pokazanie mi tego narzędzia wzorcowego. BTW zapomniałeś [order (-Count)] w wersji datatable, aby uzyskać równoważność aranżacji dplyr (desc (Count)). Po dodaniu datatable jest nadal szybszy o około x1,8 (zamiast 2,9).
Polymerase
@ eipi10 Czy możesz ponownie uruchomić swoją ławkę z wersją z możliwością datowania tutaj (dodane sortowanie według malejącej Licznik na ostatnim kroku): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (mediana (cena)), Count = .N), by = cut] [order (-Count)]
Polymerase
Nadal 0,013 sekundy. Operacja zamawiania zajmuje niewiele czasu, ponieważ polega tylko na zmianie kolejności stołu finałowego, który ma tylko cztery wiersze.
eipi10
1
Konwersja ze składni dplyr na składnię tabeli danych wiąże się z pewnym stałym obciążeniem, dlatego warto wypróbować różne rozmiary problemów. Mogłem też nie zaimplementować najbardziej wydajnego kodu tabeli danych w dplyr; łatki są zawsze mile widziane
Hadley
22

Aby odpowiedzieć na Twoje pytania:

  • Tak, używasz data.table
  • Ale nie tak wydajnie, jak w przypadku czystej data.tableskładni

W wielu przypadkach będzie to akceptowalny kompromis dla tych, którzy chcą dplyrskładni, chociaż prawdopodobnie będzie wolniejszy niż w dplyrprzypadku zwykłych ramek danych.

Wydaje się, że jednym z ważnych czynników jest to, że podczas grupowania domyślnie dplyrkopiuje data.table. Rozważ (używając microbenchmark):

Unit: microseconds
                                                               expr       min         lq    median
                                diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                          diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
 diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                               diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609

Filtrowanie ma porównywalną szybkość, ale grupowanie nie. Uważam, że winowajcą jest ta linia w dplyr:::grouped_dt:

if (copy) {
    data <- data.table::copy(data)
}

gdzie copydomyślnie TRUE(i nie można go łatwo zmienić na FALSE, co widzę). To prawdopodobnie nie stanowi 100% różnicy, ale sam ogólny narzut dotyczący czegoś, diamondsco najprawdopodobniej jest wielkości, nie jest pełną różnicą.

Problem polega na tym, że aby mieć spójną gramatykę, dplyrgrupuje się w dwóch etapach. Najpierw ustawia klucze na kopii oryginalnej tabeli danych, które pasują do grup, a dopiero później grupuje. data.tablepo prostu przydziela pamięć dla największej grupy wynikowej, która w tym przypadku jest tylko jednym wierszem, więc to robi dużą różnicę w tym, ile pamięci należy przydzielić.

FYI, jeśli kogoś to obchodzi, znalazłem to za pomocą treeprof( install_github("brodieg/treeprof")), eksperymentalnej (i wciąż bardzo alfa) przeglądarki drzew do Rprofwyjścia:

wprowadź opis obrazu tutaj

Zauważ, że powyższe działa obecnie tylko na komputerach Mac AFAIK. Ponadto niestety Rprofrejestruje połączenia tego typu packagename::funnamejako anonimowe, więc w rzeczywistości mogą to być wszystkie datatable::połączenia wewnętrzne, grouped_dtktóre są odpowiedzialne, ale po szybkich testach wyglądało na datatable::copyto, że jest to duże.

To powiedziawszy, możesz szybko zobaczyć, że wokół [.data.tablepołączenia nie ma zbyt dużego narzutu , ale istnieje również całkowicie oddzielna gałąź dla grupowania.


EDYCJA : potwierdzenie kopiowania:

> tracemem(diamondsDT)
[1] "<0x000000002747e348>"    
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
Source: local data table [5 x 2]

        cut AvgPrice
1      Fair 4358.758
2      Good 3928.864
3 Very Good 3981.760
4   Premium 4584.258
5     Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
         cut       V1
1:     Ideal 3457.542
2:   Premium 4584.258
3:      Good 3928.864
4: Very Good 3981.760
5:      Fair 4358.758
> untracemem(diamondsDT)
BrodieG
źródło
To jest niesamowite, dzięki. Czy to oznacza, że ​​dplyr :: group_by () podwoi wymagania dotyczące pamięci (w porównaniu z czystą składnią datatable) z powodu wewnętrznego kroku kopiowania danych? Oznacza to, że rozmiar mojego obiektu datatable wynosi 1 GB i używam składni połączonej z łańcuchem dplyr podobnej do tej w oryginalnym poście. Potrzebuję co najmniej 2 GB wolnej pamięci, aby uzyskać wyniki?
Polymerase
2
Czuję, że naprawiłem to w wersji deweloperskiej?
Hadley
@hadley, pracowałem z wersją CRAN. Patrząc na dev, wygląda na to, że częściowo rozwiązałeś problem, ale rzeczywista kopia pozostaje (nie została przetestowana, wystarczy spojrzeć na wiersze c (20, 30:32) w R / grouped-dt.r. Prawdopodobnie jest teraz szybsza, ale Założę się, że powolny krok to kopia.
BrodieG,
3
Czekam też na płytką funkcję kopiowania w data.table; do tego czasu myślę, że lepiej być bezpiecznym niż szybko.
Hadley
2

Możesz teraz użyć dtplyr , który jest częścią tidyverse . Pozwala na używanie instrukcji w stylu dplyr jak zwykle, ale wykorzystuje leniwą ocenę i tłumaczy twoje instrukcje na kod data.table pod maską. Narzut związany z tłumaczeniem jest minimalny, ale możesz czerpać wszystkie, jeśli nie, większość korzyści z data.table. Więcej szczegółów na oficjalnym repozytorium git tutaj i na stronie tidyverse .

Czarne mleko
źródło