Spróbuję dać moje najlepsze przewodniki, ale nie jest to łatwe, ponieważ trzeba znać wszystkie {data.table}, {dplyr}, {dtplyr}, a także {base R}. Używam {data.table} i wielu pakietów {tidy-world} (z wyjątkiem {dplyr}). Uwielbiam oba, chociaż wolę składnię data.table od dplyr's. Mam nadzieję, że wszystkie paczki Tidy-world będą używać {dtplyr} lub {data.table} jako backend, gdy będzie to konieczne.
Jak w przypadku każdego innego tłumaczenia (pomyśl dplyr-to-sparkly / SQL), istnieją rzeczy, które można, lub nie można przetłumaczyć, przynajmniej na razie. To znaczy, może pewnego dnia {dtplyr} sprawi, że będzie w 100% przetłumaczony, kto wie. Poniższa lista nie jest wyczerpująca ani nie jest w 100% poprawna, ponieważ postaram się jak najlepiej odpowiedzieć na podstawie mojej wiedzy na temat powiązanych tematów / pakietów / problemów / itp.
Co ważne, w przypadku odpowiedzi, które nie są do końca dokładne, mam nadzieję, że zawiera on wskazówki dotyczące tego, na jakie aspekty {data.table} należy zwrócić uwagę i porównać je z {dtplyr} i samemu znaleźć odpowiedzi. Nie bierz tych odpowiedzi za pewnik.
I mam nadzieję, że ten post może być wykorzystany jako jeden z zasobów dla wszystkich {dplyr}, {data.table} lub {dtplyr} użytkowników / twórców do dyskusji i współpracy, dzięki czemu #RStats jest jeszcze lepszy.
{data.table} służy nie tylko do szybkich i wydajnych operacji pamięciowych. Wiele osób, w tym ja, preferuje elegancką składnię {data.table}. Zawiera także inne szybkie operacje, takie jak funkcje szeregów czasowych, takie jak rodzina krocząca (tj. frollapply
) Napisana w C. Może być używany z dowolnymi funkcjami, w tym tidyverse. Często używam {data.table} + {purrr}!
Złożoność operacji
Można to łatwo przetłumaczyć
library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)
# dplyr
diamonds %>%
filter(cut != "Fair") %>%
group_by(cut) %>%
summarize(
avg_price = mean(price),
median_price = as.numeric(median(price)),
count = n()
) %>%
arrange(desc(count))
# data.table
data [
][cut != 'Fair', by = cut, .(
avg_price = mean(price),
median_price = as.numeric(median(price)),
count = .N
)
][order( - count)]
{data.table} jest bardzo szybki i efektywny pod względem pamięci, ponieważ (prawie?) wszystko jest zbudowane od podstaw z C z kluczowymi pojęciami aktualizacji przez odniesienie , kluczem (myśl SQL) i ich nieustanną optymalizacją wszędzie w pakiecie (to znaczy fifelse
, fread/fread
postanowienie sortowanie pozycyjne przyjęty przez bazowej R), przy jednoczesnym zapewnieniu, że składnia jest zwięzły i spójny, dlatego myślę, że to eleganckie.
Od wprowadzenia do tabeli data.the główne operacje manipulacji danymi, takie jak podzbiór, grupa, aktualizacja, łączenie itp. Są przechowywane razem dla
zwięzła i spójna składnia ...
przeprowadzanie analizy płynnie, bez obciążenia poznawczego związanego z mapowaniem każdej operacji ...
automatycznie optymalizuje operacje wewnętrzne i bardzo skutecznie, dokładnie znając dane wymagane dla każdej operacji, co prowadzi do bardzo szybkiego i wydajnego pamięci kodu
Ostatni punkt jako przykład
# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
.(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
Najpierw dokonujemy podzbioru w i, aby znaleźć pasujące indeksy wierszy, w których lotnisko początkowe równa się „JFK”, a miesiąc równy 6L. Nie składamy jeszcze całej tabeli data.t odpowiadającej tym wierszom.
Teraz patrzymy na j i stwierdzamy, że używa on tylko dwóch kolumn. Musimy obliczyć ich średnią (). Dlatego dzielimy tylko te kolumny odpowiadające pasującym wierszom i obliczamy ich średnią ().
Ponieważ trzy główne elementy zapytania (i, j i by) są razem w [...] , data.table może zobaczyć wszystkie trzy i zoptymalizować zapytanie całkowicie przed oceną, a nie oddzielnie . Jesteśmy zatem w stanie uniknąć całego podzbioru (tj. Podzbiór kolumn oprócz arr_delay i dep_delay), zarówno pod względem szybkości, jak i wydajności pamięci.
Biorąc to pod uwagę, aby skorzystać z {data.table}, tłumaczenie {dtplr} musi być poprawne pod tym względem. Im bardziej złożone operacje, tym trudniejsze tłumaczenia. W przypadku prostych operacji, takich jak powyżej, z pewnością można go łatwo przetłumaczyć. W przypadku skomplikowanych lub nieobsługiwanych przez {dtplyr} musisz się dowiedzieć, jak wspomniano powyżej, należy porównać przetłumaczoną składnię i test porównawczy oraz zapoznać się z powiązanymi pakietami.
W przypadku skomplikowanych operacji lub nieobsługiwanych operacji może być w stanie podać kilka przykładów poniżej. Znów staram się jak najlepiej. Bądź dla mnie łagodny.
Aktualizacja przez odniesienie
Nie będę wchodził w wprowadzenie / szczegóły, ale oto kilka linków
Główny zasób: Semantyka odniesienia
Więcej informacji: Dokładne zrozumienie, kiedy data.table jest odniesieniem do (zamiast kopii) innego data.table
Aktualizacja według referencji , moim zdaniem, najważniejszą cechą {data.table}, dzięki czemu jest tak szybka i wydajna pamięć. dplyr::mutate
domyślnie nie obsługuje tego. Ponieważ nie znam {dtplyr}, nie jestem pewien, ile i jakie operacje mogą być obsługiwane przez {dtplyr}. Jak wspomniano powyżej, zależy to również od złożoności operacji, co z kolei wpływa na tłumaczenia.
Istnieją dwa sposoby korzystania z aktualizacji według odwołania w {data.table}
operator przypisania {data.table} :=
set
-family: set
, setnames
, setcolorder
, setkey
, setDT
, fsetdiff
, i wiele więcej
:=
jest częściej używany w porównaniu do set
. W przypadku złożonego i dużego zbioru danych kluczem do uzyskania najwyższej prędkości i wydajności pamięci jest aktualizacja przez odniesienie . Łatwy sposób myślenia (nie w 100% dokładny, ponieważ szczegóły są o wiele bardziej skomplikowane, ponieważ obejmuje twardą / płytką kopię i wiele innych czynników), powiedzmy, że masz do czynienia z dużym zbiorem danych o wielkości 10 GB, z 10 kolumnami i 1 GB każdy . Aby manipulować jedną kolumną, musisz poradzić sobie tylko z 1 GB.
Kluczową kwestią jest to, że w przypadku aktualizacji przez odniesienie wystarczy tylko poradzić sobie z wymaganymi danymi. Dlatego podczas korzystania z {data.table}, szczególnie w przypadku dużych zbiorów danych, zawsze, gdy to możliwe , korzystamy z aktualizacji przez odniesienie . Na przykład manipulowanie dużym zestawem danych modelowania
# Manipulating list columns
df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)
# data.table
dt [,
by = Species, .(data = .( .SD )) ][, # `.(` shorthand for `list`
model := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
summary := map(model, summary) ][,
plot := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
geom_point())]
# dplyr
df %>%
group_by(Species) %>%
nest() %>%
mutate(
model = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
summary = map(model, summary),
plot = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
geom_point())
)
Operacja zagnieżdżania list(.SD)
może nie być obsługiwana przez {dtlyr}, jak używają użytkownicy Tidyverse tidyr::nest
? Nie jestem więc pewien, czy kolejne operacje można przetłumaczyć, ponieważ sposób {data.table} jest szybszy i zajmuje mniej pamięci.
UWAGA: wynik data.table jest wyrażony w „milisekundach”, dplyr w „minutach”
df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))
bench::mark(
check = FALSE,
dt[, by = Species, .(data = list(.SD))],
df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
# expression min median `itr/sec` mem_alloc `gc/sec` n_itr n_gc
# <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms 2.49 705.8MB 1.24 2 1
# 2 df %>% group_by(Species) %>% nest() 6.85m 6.85m 0.00243 1.4GB 2.28 1 937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# # gc <list>
Istnieje wiele przypadków użycia aktualizacji przez odniesienie, a nawet użytkownicy {data.table} nie będą używać jej zaawansowanej wersji przez cały czas, ponieważ wymaga ona więcej kodów. Niezależnie od tego, czy {dtplyr} obsługuje te gotowe urządzenia, musisz się o tym przekonać.
Wiele aktualizacji według odniesień dla tych samych funkcji
Główny zasób: Eleganckie przypisywanie wielu kolumn w data.table za pomocą lapply ()
Obejmuje to albo najczęściej używane, :=
albo set
.
dt <- data.table( matrix(runif(10000), nrow = 100) )
# A few variants
for (col in paste0('V', 20:100))
set(dt, j = col, value = sqrt(get(col)))
for (col in paste0('V', 20:100))
dt[, (col) := sqrt(get(col))]
# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])
Twórca {data.table} Matt Dowle
(Uwaga: może być bardziej powszechne, że zestaw pętli jest ustawiony na dużej liczbie wierszy niż na dużej liczbie kolumn).
Dołącz + setkey + update-by-reference
Potrzebowałem ostatnio szybkiego łączenia ze stosunkowo dużymi danymi i podobnymi wzorami łączenia, więc używam mocy aktualizacji przez odniesienie , zamiast zwykłych połączeń. Ponieważ wymagają one więcej kodów, pakuję je w pakiet prywatny z niestandardową oceną przydatności do ponownego użycia i czytelności tam, gdzie to nazywam setjoin
.
Zrobiłem tutaj pewien test porównawczy: data.table join + update-by-reference + setkey
Podsumowanie
# For brevity, only the codes for join-operation are shown here. Please refer to the link for details
# Normal_join
x <- y[x, on = 'a']
# update_by_reference
x_2[y_2, on = 'a', c := c]
# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]
UWAGA: dplyr::left_join
został również przetestowany i jest najwolniejszy z ~ 9 000 ms, zużywa więcej pamięci niż oba {data.table} update_by_reference
i setkey_n_update
, ale zużywa mniej pamięci niż normal_join {data.table}. Zużyło około 2,0 GB pamięci. Nie uwzględniłem go, ponieważ chcę skupić się wyłącznie na {data.table}.
Kluczowe wnioski
setkey + update
i update
są ~ 11 i około 6,5 razy większa niż normal join
, odpowiednio
- przy pierwszym złączeniu wydajność
setkey + update
jest podobna do update
narzutu, który w setkey
dużej mierze równoważy wzrost wydajności
- przy drugim i kolejnych łączeniach, jak
setkey
nie jest to wymagane, setkey + update
jest szybszy niż update
~ 1,8 razy (lub szybszy niż normal join
~ 11 razy)
Przykłady
Aby uzyskać połączenia wydajne i wydajne pod względem pamięci, użyj jednego update
lub setkey + update
, gdy ten ostatni jest szybszy kosztem większej liczby kodów.
Zobaczmy pseudo- kody dla zwięzłości. Logika jest taka sama.
Dla jednej lub kilku kolumn
a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)
# `update`
a[b, on = .(x), y := y]
a[b, on = .(x), `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x), `:=` (y = y, z = z, ...) ]
Dla wielu kolumn
cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]
Owijarka do szybkich i wydajnych połączeń ... wiele z nich ... z podobnym wzorem łączenia, owiń je jak setjoin
wyżej - z update
- z lub bezsetkey
setjoin(a, b, on = ...) # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
setjoin(...) %>%
setjoin(...)
Za pomocą setkey
argumentu on
można pominąć. Może być również uwzględniony w celu zwiększenia czytelności, szczególnie w przypadku współpracy z innymi.
Duża operacja rzędowa
- jak wspomniano powyżej, użyj
set
- wstępnie wypełnij tabelę, użyj technik aktualizacji według referencji
- podzbiór za pomocą klucza (tj.
setkey
)
Zasób pokrewny: Dodaj wiersz przez odniesienie na końcu obiektu data.table
Podsumowanie aktualizacji przez odniesienie
To tylko niektóre przypadki użycia aktualizacji przez odniesienie . Jest o wiele więcej.
Jak widać, w przypadku zaawansowanego korzystania z dużych danych istnieje wiele przypadków użycia i technik wykorzystujących aktualizację przez odniesienie dla dużego zestawu danych. Nie jest to takie łatwe w użyciu w {data.table} i czy {dtplyr} to obsługuje, możesz się przekonać.
W tym poście skupiam się na aktualizacji przez odniesienie, ponieważ uważam, że jest to najmocniejsza funkcja {data.table} do szybkich operacji i wydajności pamięci. To powiedziawszy, istnieje wiele, wiele innych aspektów, które czynią go tak wydajnym i myślę, że nie są natywnie obsługiwane przez {dtplyr}.
Inne kluczowe aspekty
To, co jest / nie jest obsługiwane, zależy również od złożoności operacji i tego, czy wiąże się z natywną funkcją data.table, taką jak aktualizacja przez referencję lub setkey
. I to, czy przetłumaczony kod jest bardziej wydajny (taki, który napisaliby użytkownicy data.table) jest również innym czynnikiem (tj. Kod jest tłumaczony, ale czy jest to wersja wydajna?). Wiele rzeczy jest ze sobą powiązanych.
setkey
. Zobacz Klucze i szybki podzbiór oparty na wyszukiwaniu binarnym
- Wtórne indeksy i automatyczne indeksowanie
- Korzystanie z .SD do analizy danych
- funkcje szeregów czasowych: pomyśl
frollapply
. funkcje walcowania, agregaty kroczące, okno przesuwne, średnia krocząca
- walcowanie przyłączenia , nie-równo przyłączenia , (część) "krzyża" join
- {data.table} zbudował fundament szybkości i wydajności pamięci, w przyszłości może zostać rozszerzony o wiele funkcji (np. jak implementują funkcje szeregów czasowych wspomniane powyżej)
- Ogólnie rzecz biorąc, bardziej złożone operacje na data.table-tych
i
, j
lub by
operacji (można używać prawie wszystkich wyrażeń tam), myślę, że tym trudniej tłumaczenia, zwłaszcza gdy łączą się z aktualizacja przez referencję , setkey
i innych rodzimych data.table działa jakfrollapply
- Kolejny punkt dotyczy używania podstawy R lub tidyverse. Używam obu data.table + tidyverse (oprócz dplyr / readr / tidyr). W przypadku dużych operacji często przeprowadzam testy porównawcze, na przykład
stringr::str_*
funkcje rodziny vs. podstawy R i stwierdzam, że podstawa R jest do pewnego stopnia szybsza i korzystam z nich. Chodzi o to, nie trzymaj się tylko tidyverse lub data.table lub ..., sprawdź inne opcje, aby wykonać zadanie.
Wiele z tych aspektów jest powiązanych ze wspomnianymi powyżej punktami
Możesz dowiedzieć się, czy {dtplyr} obsługuje te operacje, zwłaszcza gdy są one połączone.
Kolejne przydatne sztuczki podczas radzenia sobie z małym lub dużym zbiorem danych, podczas interaktywnej sesji, {data.table} naprawdę spełnia obietnicę znacznego skrócenia czasu programowania i obliczeń .
Ustawienie klucza dla powtarzalnie używanej zmiennej zarówno dla prędkości, jak i „doładowanych nazw” (podzbiór bez określania nazwy zmiennej).
dt <- data.table(iris)
setkey(dt, Species)
dt['setosa', do_something(...), ...]
dt['virginica', do_another(...), ...]
dt['setosa', more(...), ...]
# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders.
# It's simply elegant
dt['setosa', do_something(...), Species, ...]
Jeśli twoje operacje obejmują tylko proste, jak w pierwszym przykładzie, {dtplyr} może wykonać zadanie. W przypadku złożonych / nieobsługiwanych można skorzystać z tego przewodnika, aby porównać przetłumaczone {dtplyr} z tym, w jaki sposób doświadczeni użytkownicy data.table kodują w szybki i wydajny sposób przy użyciu eleganckiej składni data.table. Tłumaczenie nie oznacza, że jest to najbardziej efektywny sposób, ponieważ mogą istnieć różne techniki radzenia sobie z różnymi przypadkami dużych danych. W przypadku jeszcze większego zestawu danych możesz połączyć {data.table} z {disk.frame} , {fst} i {drake} i innymi niesamowitymi pakietami, aby uzyskać to, co najlepsze. Istnieje również {big.data.table}, ale obecnie jest nieaktywny.
Mam nadzieję, że to pomoże wszystkim. Miłego dnia ☺☺
dplyr
czym nie da się dobrzedata.table
? Jeśli nie, przełączenie nadata.table
będzie lepsze niżdtplyr
.