Czego nie mogę zrobić z dtplyr, że mogę w data.table

9

Czy powinienem inwestować wysiłki w uczenie się na przekręcaniu danych w R, szczególnie pomiędzy dplyr, dtplyra data.table?

  • Używam dplyrgłównie, ale gdy dane są za duże na to, skorzystam data.table, co jest rzadkim zjawiskiem. Teraz, gdy dtplyrwersja 1.0 jest dostępna jako interfejs data.table, na pierwszy rzut oka wydaje się, że nigdy nie muszę się martwić o korzystanie z data.tableinterfejsu.

  • Więc jakie są najbardziej użyteczne funkcje lub aspekty data.table, które nie mogą być wykonane przy użyciu dtplyrw tej chwili, i że prawdopodobnie nigdy nie zostaną wykonane z dtplyr?

  • Na jego twarzy dplyrz zaletami data.tablesprawia, że ​​brzmi on jak dtplyrwyprzedzanie dplyr. Czy będzie jakiś powód do korzystania, dplyrgdy dtplyrjuż całkowicie dojrzeje?

Uwaga: nie pytam o dplyrvs data.table(jak w table.table vs dplyr: czy jeden może zrobić coś dobrze, a drugi nie może lub robi źle? ), Ale biorąc pod uwagę, że jeden jest lepszy od drugiego ze względu na konkretny problem, dlaczego nie „ T dtplyrjest narzędzie do użycia.

dule arnaux
źródło
1
Czy jest coś, co można zrobić dobrze, w dplyrczym nie da się dobrze data.table? Jeśli nie, przełączenie na data.tablebędzie lepsze niż dtplyr.
sindri_baldur
2
Z dtplyrpliku Readme: „Niektóre data.tablewyrażenia nie mają bezpośredniego dplyrodpowiednika. Na przykład nie ma możliwości wyrażenia łączeń krzyżowych lub kroczących dplyr”. i „Aby dopasować dplyrsemantykę, mutate() domyślnie się nie zmienia. Oznacza to, że większość wyrażeń obejmujących mutate()musi wykonać kopię, która nie byłaby konieczna, gdybyś używał data.tablebezpośrednio ”. Jest pewien sposób na obejście tej drugiej części, ale biorąc pod uwagę, jak często mutatejest używana, jest to dość duży minus w moich oczach.
ClancyStats

Odpowiedzi:

15

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/freadpostanowienie 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::mutatedomyś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_joinzostał również przetestowany i jest najwolniejszy z ~ 9 000 ms, zużywa więcej pamięci niż oba {data.table} update_by_referencei 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 + updatei updatesą ~ 11 i około 6,5 razy większa niż normal join, odpowiednio
  • przy pierwszym złączeniu wydajność setkey + updatejest podobna do updatenarzutu, który w setkeydużej mierze równoważy wzrost wydajności
  • przy drugim i kolejnych łączeniach, jak setkeynie jest to wymagane, setkey + updatejest szybszy niż update~ 1,8 razy (lub szybszy niż normal join~ 11 razy)

Wizerunek

Przykłady

Aby uzyskać połączenia wydajne i wydajne pod względem pamięci, użyj jednego updatelub 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 setjoinwyż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ą setkeyargumentu onmoż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.

Wiele z tych aspektów jest powiązanych ze wspomnianymi powyżej punktami

  • złożoność operacji

  • aktualizacja przez odniesienie

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 ☺☺

K22
źródło
2

Przychodzą mi na myśl połączenia nierównoramienne i połączenia toczne. Wydaje się, że nie ma żadnych planów włączenia równoważnych funkcji w dplyr, więc dtplyr nie ma nic do przetłumaczenia.

Istnieje również przekształcanie (zoptymalizowany rzut i stopienie równoważne z tymi samymi funkcjami w reshape2), którego również nie ma w dplyr.

Wszystkie funkcje * _if i * _at obecnie nie mogą być tłumaczone za pomocą dtplyr, ale są one w przygotowaniu.

EdTeD
źródło
0

Zaktualizuj kolumnę po dołączeniu Niektóre sztuczki .SD Wiele funkcji f I Bóg wie co jeszcze, ponieważ #rdatatable to coś więcej niż prosta biblioteka i nie można jej streścić kilkoma funkcjami

Sam w sobie jest to cały ekosystem

Nigdy nie potrzebowałem dplyr od dnia, w którym zacząłem R. Ponieważ tabela danych jest tak cholernie dobra

Vikram
źródło