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) ]
r
data.table
dplyr
Polimeraza
źródło
źródło
dplyr
metody dla tabel danych, ale tabela danych ma również swoje własne porównywalne metodydplyr
jest używany nadata.frame
s i odpowiadającychdata.table
s, patrz tutaj (i odnośniki tam).Odpowiedzi:
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()
islice()
w dplyr)Załóżmy, że
DT
powiedzmy 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
DT
których kolumnaa > 1
. (2) zwracamean(b)
pogrupowane wedługc,d
tego samego wyrażenia wi
(1).Powszechnie używanymi
dplyr
wyraż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 kolumnb, c, d
do kolejnych operacji. Aby temu zaradzić, musimy doselect()
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)
Zauważ, że w (5) i (6) nadal mamy podzbiór kolumny,
a
której nie wymagamy. Ale nie jestem pewien, jak tego uniknąć. Gdybyfilter()
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
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]
iDT[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.
źródło
filter()
plusasummarise()
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.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.
źródło
microbenchmark
pakietu stwierdziłem, że uruchomieniedplyr
kodu OP na oryginalnej (ramce danych) wersji programudiamonds
zajęło średnio 0,012 sekundy, podczas gdy mediana czasu zajęła 0,024 sekundy po konwersjidiamonds
do tabeli danych. Uruchomieniedata.table
kodu G. Grothendiecka zajęło 0,013 sekundy. Przynajmniej na moim systemie wyglądadplyr
idata.table
ma mniej więcej taką samą wydajność. Ale dlaczego miałobydplyr
być wolniej, gdy ramka danych jest najpierw konwertowana na tabelę danych?Aby odpowiedzieć na Twoje pytania:
data.table
data.table
składniW wielu przypadkach będzie to akceptowalny kompromis dla tych, którzy chcą
dplyr
składni, chociaż prawdopodobnie będzie wolniejszy niż wdplyr
przypadku zwykłych ramek danych.Wydaje się, że jednym z ważnych czynników jest to, że podczas grupowania domyślnie
dplyr
kopiujedata.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
copy
domyślnieTRUE
(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ś,diamonds
co najprawdopodobniej jest wielkości, nie jest pełną różnicą.Problem polega na tym, że aby mieć spójną gramatykę,
dplyr
grupuje się w dwóch etapach. Najpierw ustawia klucze na kopii oryginalnej tabeli danych, które pasują do grup, a dopiero później grupuje.data.table
po 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 doRprof
wyjścia:Zauważ, że powyższe działa obecnie tylko na komputerach Mac AFAIK. Ponadto niestety
Rprof
rejestruje połączenia tego typupackagename::funname
jako anonimowe, więc w rzeczywistości mogą to być wszystkiedatatable::
połączenia wewnętrzne,grouped_dt
które są odpowiedzialne, ale po szybkich testach wyglądało nadatatable::copy
to, że jest to duże.To powiedziawszy, możesz szybko zobaczyć, że wokół
[.data.table
połą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)
źródło
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 .
źródło