data.table vs dplyr: czy można zrobić coś dobrze, a drugiego nie, lub źle?

758

Przegląd

Jestem względnie obeznany data.table, ale nie bardzo dplyr. Przeczytałem niektóre dplyrwiniety i przykłady, które pojawiły się na SO, i jak dotąd moje wnioski są następujące:

  1. data.tablei dplyrsą porównywalne pod względem prędkości, z wyjątkiem sytuacji, gdy istnieje wiele (tj.> 10–100 tys.) grup oraz w niektórych innych okolicznościach (patrz punkty odniesienia poniżej)
  2. dplyr ma bardziej dostępną składnię
  3. dplyr streszcza (lub będzie) potencjalne interakcje DB
  4. Istnieją pewne niewielkie różnice w funkcjonalności (patrz „Przykłady / użycie” poniżej)

Moim zdaniem 2. nie ma większego znaczenia, ponieważ jestem dość dobrze z tym zaznajomiony data.table, choć rozumiem, że dla nowych użytkowników obu będzie to duży czynnik. Chciałbym uniknąć kłótni, która jest bardziej intuicyjna, ponieważ nie ma to znaczenia dla mojego konkretnego pytania zadanego z perspektywy kogoś, kogo już znasz data.table. Chciałbym również uniknąć dyskusji na temat tego, w jaki sposób „bardziej intuicyjny” prowadzi do szybszej analizy (z pewnością prawda, ale znowu nie to, co mnie najbardziej interesuje).

Pytanie

Chcę wiedzieć:

  1. Czy istnieją zadania analityczne, które są o wiele łatwiejsze do zakodowania w jednym lub drugim pakiecie dla osób zaznajomionych z pakietami (tj. Wymagana jest kombinacja kombinacji klawiszy w porównaniu z wymaganym poziomem ezoteryzmu, gdzie mniejsza część jest dobra).
  2. Czy istnieją zadania analityczne, które są wykonywane znacznie (tj. Ponad 2x) bardziej efektywnie w jednym pakiecie w porównaniu do drugiego.

Jedno ostatnie pytanie SO spowodowało, że pomyślałem o tym trochę więcej, ponieważ do tego momentu nie sądziłem, dplyrże zaoferuje coś więcej niż to, co już mogę zrobić data.table. Oto dplyrrozwiązanie (dane na końcu Q):

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

Co było znacznie lepsze niż moja próba hackowania data.tablerozwiązania. To powiedziawszy, dobre data.tablerozwiązania są również całkiem dobre (dzięki Jean-Robert, Arun, i zauważcie, że faworyzowałem jedno stwierdzenie nad najbardziej optymalnym rozwiązaniem):

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

Składnia tego drugiego może wydawać się bardzo ezoteryczna, ale w rzeczywistości jest dość prosta, jeśli jesteś do tego przyzwyczajony data.table(tzn. Nie używa niektórych bardziej ezoterycznych sztuczek).

Idealnie chciałbym zobaczyć kilka dobrych przykładów, w których dplyrlub data.tablesposób jest znacznie bardziej zwięzły lub działa znacznie lepiej.

Przykłady

Stosowanie
  • dplyrnie zezwala na zgrupowane operacje, które zwracają dowolną liczbę wierszy (z pytania eddi , uwaga: wygląda na to, że zostanie zaimplementowane w dplyr 0.5 , również @beginneR pokazuje potencjalne obejście przy użyciu doodpowiedzi na pytanie @ eddi).
  • data.tableobsługuje połączenia toczne (dzięki @dholstius), a także połączenia nakładające się
  • data.tablewewnętrznie optymalizuje wyrażenia formie DT[col == value]lub DT[col %in% values]na prędkości poprzez automatyczną indeksowania który wykorzystuje przeszukiwanie binarne stosując taką samą składnię zasady R. Zobacz tutaj, aby uzyskać więcej szczegółów i mały test.
  • dplyroferty standardowych wersji oceny funkcji (np regroup, summarize_each_), które mogą uprościć programową wykorzystania dplyr(uwaga programowej użytku z data.tablejest z pewnością możliwe, tylko wymaga starannego myśli, podstawienie / cytowania itp, przynajmniej według mojej wiedzy)
Benchmarki

Dane

To jest pierwszy przykład, który pokazałem w sekcji pytań.

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))
BrodieG
źródło
9
Rozwiązanie podobne do czytania dplyrto:as.data.table(dat)[, .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], by = list(name, job)]
eddi
7
W przypadku nr 1 zarówno zespół, jak dplyri data.tablezespoły pracują nad testami porównawczymi, więc w pewnym momencie pojawi się odpowiedź. # 2 (składnia) imO jest ściśle fałszywe, ale to wyraźnie wkracza na terytorium opinii, więc głosuję również za zamknięciem.
eddi
13
cóż, znowu imO, zestaw problemów, które są bardziej wyraziście wyrażone, (d)plyrma miarę 0
eddi
28
@BrodieG jedna rzecz, która naprawdę błędy mną w obu dplyri plyrjeśli chodzi o składnię i jest w zasadzie główny powód, dlaczego nie lubię ich składni, jest to, że muszę nauczyć się zbyt wiele (więcej niż 1) dodatkowe funkcje (z nazwami, które nadal nie mają dla mnie sensu), pamiętajcie, co robią, jakie argumenty biorą itp. To zawsze było dla mnie wielkim odejściem od filozofii plyr.
eddi
43
@eddi [język w policzek] jedyną rzeczą, która naprawdę mnie wkurza w składni data.table, jest to, że muszę nauczyć się, w jaki sposób działa zbyt wiele argumentów funkcyjnych i co oznaczają (np .SD.) tajemnicze skróty . [poważnie] Myślę, że są to uzasadnione różnice projektowe, które przypadną do
gustu

Odpowiedzi:

532

Musimy zajmować co najmniej te elementy, aby zapewnić kompleksową odbierania / porównanie (w przypadkowej kolejności ważności): Speed, Memory usage, Syntaxi Features.

Moim zamiarem jest objęcie każdego z nich tak jasno, jak to możliwe z perspektywy tabeli data.tab.

Uwaga: chyba że wyraźnie zaznaczono inaczej, odnosząc się do dplyr, odnosimy się do interfejsu data.frame dplyr, którego elementy wewnętrzne są w C ++ przy użyciu Rcpp.


Składnia data.table jest spójna w swojej formie - DT[i, j, by]. Aby utrzymać i, ja byrazem jest zgodne z projektem. Utrzymując powiązane operacje razem, pozwala łatwo zoptymalizować operacje pod kątem szybkości i, co ważniejsze, wykorzystania pamięci , a także zapewnia pewne zaawansowane funkcje , a wszystko to przy zachowaniu spójności składni.

1. Prędkość

Do pytania, które już pokazuje dane, dodano kilka testów porównawczych (choć głównie dotyczących operacji grupowania). Tabela staje się szybsza niż dplyr wraz ze wzrostem liczby grup i / lub wierszy do grupowania, w tym testy porównawcze według Matta przy grupowaniu od 10 milionów do 2 miliardy wierszy (100 GB w pamięci RAM) w 100–10 milionach grup i różnych kolumnach grupujących, co również jest porównywalne pandas. Zobacz także zaktualizowane testy porównawcze , które obejmują Sparki pydatatablerównież.

Jeśli chodzi o testy porównawcze, dobrze byłoby objąć również pozostałe aspekty:

  • Operacje grupowania obejmujące podzbiór wierszy - tj DT[x > val, sum(y), by = z]. Operacje typu.

  • Benchmark innych operacji, takich jak aktualizacja i dołączenia .

  • Poza tym środowisko wykonawcze stanowi również ślad pamięci dla każdej operacji.

2. Wykorzystanie pamięci

  1. Operacje obejmujące filter()lub slice()w dplyr mogą być nieefektywne pamięci (zarówno na data.frames, jak i data.tables). Zobacz ten post .

    Zauważ, że komentarz Hadleya mówi o prędkości (ten dplyr jest dla niego bardzo szybki), podczas gdy głównym problemem jest pamięć .

  2. Interfejs data.table pozwala w tej chwili modyfikować / aktualizować kolumny przez odniesienie (zauważ, że nie musimy ponownie przypisywać wyniku do zmiennej).

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

    Ale dplyr nigdy nie będzie aktualizowany przez odniesienie. Odpowiednikiem dplyr byłoby (zauważ, że wynik musi zostać ponownie przypisany):

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

    Problemem jest przejrzystość referencyjna . Aktualizacja obiektu data.table przez odwołanie, szczególnie w obrębie funkcji, może nie zawsze być pożądana. Ale jest to niezwykle przydatna funkcja: zapoznaj się z tym i tymi postami w interesujących przypadkach. I chcemy to zachować.

    Dlatego pracujemy nad shallow()funkcją eksportu do data.table, która zapewni użytkownikowi obie możliwości . Na przykład, jeśli pożądane jest, aby nie modyfikować tabeli danych wejściowych w ramach funkcji, można wtedy:

    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.
    }

    Nieużywanie shallow()powoduje zachowanie starej funkcjonalności:

    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.
    }

    Tworząc płytką kopię przy użyciu shallow(), rozumiemy, że nie chcesz modyfikować oryginalnego obiektu. Dbamy o wszystko wewnętrznie, aby zapewnić, że jednocześnie kopiując kolumny, modyfikujesz tylko wtedy, gdy jest to absolutnie konieczne . Po wdrożeniu powinno to całkowicie rozwiązać kwestię przejrzystości odniesienia, jednocześnie zapewniając użytkownikowi obie możliwości.

    Ponadto po shallow()wyeksportowaniu interfejs data.table dplyr powinien unikać prawie wszystkich kopii. Tak więc ci, którzy preferują składnię dplyr, mogą używać jej z data.tables.

    Ale nadal będzie brakowało wielu funkcji, które zapewnia data.table, w tym (pod) przypisanie przez odniesienie.

  3. Agreguj podczas dołączania:

    Załóżmy, że masz dwie tabele danych w następujący sposób:

    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

    I chciałbyś dostać się sum(z) * muldo każdego wiersza DT2podczas łączenia kolumn x,y. Możemy:

    • 1) agreguj, DT1aby uzyskać sum(z), 2) wykonaj łączenie i 3) pomnóż (lub)

      # 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) zrób to wszystko za jednym razem (używając by = .EACHIfunkcji):

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

    Jaka jest zaleta?

    • Nie musimy przydzielać pamięci dla wyniku pośredniego.

    • Nie musimy grupować / mieszać dwa razy (jeden w celu agregacji, a drugi w celu dołączenia).

    • A co ważniejsze, operacja, którą chcieliśmy wykonać, jest jasna, patrząc na j(2).

    Sprawdź ten post, aby uzyskać szczegółowe wyjaśnienie by = .EACHI. Nie pojawiają się żadne wyniki pośrednie, a agregacja + agregacja jest wykonywana za jednym razem.

    Spójrz na to , to i to posty na prawdziwe scenariusze użytkowania.

    W dplyrmusisz najpierw połączyć i agregować lub agregować, a następnie łączyć , z których żadne nie jest tak wydajne pod względem pamięci (co z kolei przekłada się na szybkość).

  4. Zaktualizuj i dołącza:

    Rozważ poniższy kod data.table:

    DT1[DT2, col := i.mul]

    dodaje / aktualizuje DT1kolumnę colz mulz DT2tych wierszy, w których DT2kluczowa kolumna pasuje DT1. Nie sądzę, że istnieje dokładny odpowiednik tej operacji w dplyr, tj. Bez unikania *_joinoperacji, która musiałaby skopiować całą DT1tylko, aby dodać do niej nową kolumnę, co jest niepotrzebne.

    Sprawdź ten post, aby zobaczyć prawdziwy scenariusz użytkowania.

Podsumowując, ważne jest, aby zdać sobie sprawę, że każda optymalizacja ma znaczenie. Jak powiedziałaby Grace Hopper : Uważaj na swoje nanosekundy !

3. Składnia

Spójrzmy teraz na składnię . Hadley skomentował tutaj :

Tabele danych są niezwykle szybkie, ale myślę, że ich dokładność utrudnia naukę, a kod, który używa, jest trudniejszy do odczytania po napisaniu ...

Uważam tę uwagę za bezcelową, ponieważ jest ona bardzo subiektywna. Być może możemy spróbować porównać spójność w składni . Porównamy składnię data.table i dplyr obok siebie.

Będziemy pracować z danymi atrapa pokazanymi poniżej:

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)
  1. Podstawowe operacje agregacji / aktualizacji.

    # 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])
    • Składnia data.table jest zwarta, a dplyr dość gadatliwy. Sprawy są mniej więcej równoważne w przypadku (a).

    • W przypadku (b) musieliśmy użyć filter()w dplyr podczas podsumowania . Ale podczas aktualizacji musieliśmy przenieść logikę do środka mutate(). Jednak w data.table wyrażamy obie operacje z tą samą logiką - operujemy na wierszach, gdzie x > 2, ale w pierwszym przypadku, otrzymujemy sum(y), podczas gdy w drugim przypadku aktualizujemy te wiersze yo ich skumulowaną sumę.

      To mamy na myśli, gdy mówimy, że DT[i, j, by]forma jest spójna .

    • Podobnie w przypadku (c), kiedy mamy if-elsewarunek, jesteśmy w stanie wyrazić logikę „taką, jaka jest” zarówno w data.table, jak i dplyr. Jeśli jednak chcielibyśmy zwrócić tylko te wiersze, w których ifwarunek jest spełniony, i pominąć inaczej, nie możemy użyć summarise()bezpośrednio (AFAICT). Musimy filter()najpierw, a następnie podsumować, ponieważ summarise()zawsze oczekuje jednej wartości .

      Chociaż zwraca ten sam wynik, użycie filter()tutaj powoduje, że faktyczna operacja jest mniej oczywista.

      Równie dobrze może być możliwe zastosowanie filter()w pierwszym przypadku (nie wydaje mi się to oczywiste), ale chodzi mi o to, że nie powinniśmy.

  2. Agregacja / aktualizacja w wielu kolumnach

    # 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))
    • W przypadku (a) kody są mniej więcej równoważne. data.table korzysta ze znanej funkcji bazowej lapply(), natomiast dplyrwprowadza *_each()wraz z szeregiem funkcji do funs().

    • data.table :=wymaga podania nazw kolumn, podczas gdy dplyr generuje je automatycznie.

    • W przypadku (b) składnia dplyr jest stosunkowo prosta. Poprawianie agregacji / aktualizacji wielu funkcji znajduje się na liście data.table.

    • W przypadku (c) dplyr zwróci n()tyle razy tyle kolumn, ile tylko raz. W data.table wszystko, co musimy zrobić, to zwrócić listę j. Każdy element listy stanie się kolumną w wyniku. Możemy więc ponownie użyć znanej funkcji bazowej, c()aby połączyć .Nsię z wartością, listktóra zwraca a list.

    Uwaga: Po raz kolejny w tabeli data.t wszystko, co musimy zrobić, to zwrócić listę j. W rezultacie każdy element listy stanie się kolumną. Można użyć c(), as.list(), lapply(), list()funkcje itp ... bazowe do osiągnięcia tego celu, bez konieczności uczenia się nowych funkcji.

    Musisz nauczyć się tylko specjalnych zmiennych - .Ni .SDprzynajmniej. Odpowiednikami w dplyr są n()i.

  3. Łączy się

    dplyr zapewnia osobne funkcje dla każdego rodzaju złączenia, w którym as data.table umożliwia sprzężenia przy użyciu tej samej składni DT[i, j, by](i z uzasadnieniem). Zapewnia również równoważną merge.data.table()funkcję jako alternatywa.

    setkey(DT1, x, y)
    
    # 1. normal join
    DT1[DT2]            ## data.table syntax
    left_join(DT2, DT1) ## dplyr syntax
    
    # 2. select columns while join    
    DT1[DT2, .(z, i.mul)]
    left_join(select(DT2, x, y, mul), select(DT1, x, y, z))
    
    # 3. aggregate while join
    DT1[DT2, .(sum(z) * i.mul), by = .EACHI]
    DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
        inner_join(DF2) %>% mutate(z = z*mul) %>% select(-mul)
    
    # 4. update while join
    DT1[DT2, z := cumsum(z) * i.mul, by = .EACHI]
    ??
    
    # 5. rolling join
    DT1[DT2, roll = -Inf]
    ??
    
    # 6. other arguments to control output
    DT1[DT2, mult = "first"]
    ??
    • Niektórzy mogą znaleźć osobną funkcję dla każdego połączenia o wiele ładniejszą (lewą, prawą, wewnętrzną, anty, pół itd.), Podczas gdy inni mogą lubić tabelę danych. DT[i, j, by]Lub merge()podobny do bazy R.

    • Jednak dołączenia dplyr właśnie to robią. Nic więcej. Nic mniej.

    • data.tables może wybierać kolumny podczas łączenia (2), aw dplyr musisz select()najpierw połączyć obie data.frame przed dołączeniem, jak pokazano powyżej. W przeciwnym razie zmaterializujesz połączenie z niepotrzebnymi kolumnami, aby usunąć je później, a to będzie nieefektywne.

    • data.tables można agregować podczas łączenia (3), a także aktualizować podczas łączenia (4), używając by = .EACHIfunkcji. Po co materializować cały wynik łączenia, aby dodać / zaktualizować tylko kilka kolumn?

    • data.table ma możliwość toczenia złączeń (5) - roll do przodu, LOCF , roll do tyłu, NOCB , najbliższe .

    • data.table ma również mult =argument, który wybiera pierwsze , ostatnie lub wszystkie dopasowania (6).

    • data.table ma allow.cartesian = TRUEargument chroniący przed przypadkowymi nieprawidłowymi połączeniami.

Po raz kolejny składnia jest spójna DT[i, j, by]z dodatkowymi argumentami pozwalającymi na dalsze sterowanie wyjściem.

  1. do()...

    Podsumowanie dplyr jest specjalnie zaprojektowane dla funkcji, które zwracają jedną wartość. Jeśli funkcja zwraca wiele / nierówne wartości, będziesz musiał skorzystać z tej opcji do(). Musisz wcześniej wiedzieć o wszystkich zwracanych wartościach wszystkich funkcji.

    DT[, list(x[1], y[1]), by = z]                 ## data.table syntax
    DF %>% group_by(z) %>% summarise(x[1], y[1]) ## dplyr syntax
    DT[, list(x[1:2], y[1]), by = z]
    DF %>% group_by(z) %>% do(data.frame(.$x[1:2], .$y[1]))
    
    DT[, quantile(x, 0.25), by = z]
    DF %>% group_by(z) %>% summarise(quantile(x, 0.25))
    DT[, quantile(x, c(0.25, 0.75)), by = z]
    DF %>% group_by(z) %>% do(data.frame(quantile(.$x, c(0.25, 0.75))))
    
    DT[, as.list(summary(x)), by = z]
    DF %>% group_by(z) %>% do(data.frame(as.list(summary(.$x))))
    • .SDekwiwalentem jest .

    • W data.table możesz wrzucić prawie wszystko j- jedyną rzeczą do zapamiętania jest zwrócenie listy, aby każdy element listy został przekonwertowany na kolumnę.

    • W Dplyr nie mogę tego zrobić. do()Musisz uciekać się do tego, czy masz pewność, że funkcja zawsze zwróci jedną wartość. I to jest dość powolne.

Po raz kolejny składnia data.table jest zgodna z DT[i, j, by]. Możemy po prostu dodawać wyrażenia, jnie martwiąc się o te rzeczy.

Spójrz na to SO pytanie i to . Zastanawiam się, czy byłoby możliwe wyrażenie odpowiedzi tak prosto, używając składni dplyr ...

Podsumowując, szczególnie zwróciłem uwagę na kilka przypadków, w których składnia dplyr jest albo nieefektywna, ograniczona albo nie sprawia, że ​​operacje są proste. Dzieje się tak szczególnie dlatego, że data.table ma sporo luzu na temat składni „trudniej czytać / uczyć się” (jak ta wklejona / połączona powyżej). Większość postów dotyczących dplyr mówi o najprostszych operacjach. I to jest świetne. Ale ważne jest, aby zdać sobie sprawę z jego ograniczeń składniowych i funkcji, a ja jeszcze nie widzę postu na ten temat.

data.table ma również swoje dziwactwa (niektóre z nich wskazałem, że próbujemy to naprawić). Staramy się również poprawić połączenia danych w pliku data.table, o czym tu wspomniałem .

Ale należy również wziąć pod uwagę liczbę funkcji, których brakuje dplyr w porównaniu do data.table.

4. Funkcje

Wskazałem większość funkcji tutaj, a także w tym poście. Dodatkowo:

  • fread - szybki czytnik plików jest już dostępny od dawna.

  • fwrite - parallelised szybko plik pisarz jest już dostępna. Zobacz ten post, aby uzyskać szczegółowe wyjaśnienie dotyczące implementacji i # 1664, aby śledzić dalsze zmiany.

  • Automatyczne indeksowanie - kolejna przydatna funkcja do optymalizacji wewnętrznej składni R.

  • Grupowanie ad-hoc : dplyrautomatycznie sortuje wyniki poprzez grupowanie zmiennych podczas summarise(), co może nie zawsze być pożądane.

  • Liczne zalety złączeń data.table (dla szybkości / wydajności pamięci i składni) wspomnianych powyżej.

  • Złączenia<=, <, >, >= inne niż Equi : Umożliwia łączenia za pomocą innych operatorów wraz ze wszystkimi innymi zaletami połączeń data.table.

  • Niedawno zaimplementowano nakładające się łączenia zakresów w data.table. Sprawdź ten post, aby zapoznać się z testami porównawczymi.

  • setorder() funkcja w data.table, która pozwala na naprawdę szybkie zmienianie kolejności data.tables przez odniesienie.

  • dplyr zapewnia interfejs do baz danych przy użyciu tej samej składni, czego nie ma obecnie data.table.

  • data.tablezapewnia szybsze odpowiedniki zestaw operacji (napisany przez Jana Góreckiego) - fsetdiff, fintersect, funiona fsetequalz dodatkowym allargumentem (jak w SQL).

  • data.table obciążenia czysto bez ostrzeżenia maskowania i posiada mechanizm opisany tutaj dla [.data.framekompatybilności kiedy przeszedł do dowolnego pakietu R. dplyr zmiany funkcji bazowych filter, lagi [która może być przyczyną problemów; np. tu i tutaj .


Wreszcie:

  • W bazach danych - nie ma powodu, dla którego data.table nie może zapewnić podobnego interfejsu, ale teraz nie jest to priorytet. Może się zdarzyć, że użytkownicy bardzo polubią tę funkcję ... nie jestem pewien.

  • O równoległości - wszystko jest trudne, dopóki ktoś nie zrobi tego. Oczywiście będzie to wymagało wysiłku (bezpieczeństwo wątku).

    • Obecnie poczyniono postępy (w wersji 1.9.7) w kierunku równoległych znanych czasochłonnych części w celu zwiększenia przyrostu wydajności OpenMP.
Arun
źródło
9
@bluefeet: Nie sądzę, abyś zrobił nam resztę wspaniałej usługi, przenosząc tę ​​dyskusję na czat. Miałem wrażenie, że Arun był jednym z programistów, co mogło przynieść użyteczne spostrzeżenia.
IRTFM,
2
Kiedy poszedłem na czat za pomocą linku, okazało się, że wszystkie materiały po komentarzu rozpoczynającym się od „Powinieneś użyć filtra”… zniknęły. Czy brakuje mi czegoś w mechanizmie SO-chat?
IRTFM,
6
Myślę, że wszędzie tam, gdzie używasz przypisania przez referencję ( :=), dplyrodpowiednik powinien być również używany <-jak DF <- DF %>% mutate...zamiast zamiastDF %>% mutate...
David Arenburg
4
Odnośnie składni. Uważam, że dplyrmoże to być łatwiejsze dla użytkowników, którzy używali plyrskładni, ale data.tablemoże być łatwiejsze dla użytkowników, którzy używali zapytań o podobną składnię językową SQL, oraz związaną z tym algebrę relacyjną, która dotyczy transformacji danych tabelarycznych. @Arun należy zauważyć, że operatory zestawu są bardzo łatwe do wykonania dzięki data.tablefunkcji owijania i oczywiście przynosi znaczne przyspieszenie.
jangorecki
9
Przeczytałem ten post wiele razy i bardzo pomogło mi to w zrozumieniu data.table i możliwości lepszego korzystania z niego. W większości przypadków wolę tabelę danych niż dplyr lub pandy lub PL / pgSQL. Nie mogłem jednak przestać myśleć o tym, jak to wyrazić. Składnia nie jest łatwa, jasna ani pełna. W rzeczywistości, nawet po tym, jak często korzystałem z table.tablet, często wciąż mam problem ze zrozumieniem własnego kodu, który napisałem dosłownie tydzień temu. To życiowy przykład języka tylko do zapisu. en.wikipedia.org/wiki/Write-only_language Więc miejmy nadzieję, że pewnego dnia będziemy mogli używać dplyr na data.table.
Ufos
385

Oto moja próba wyczerpującej odpowiedzi z perspektywy duszy, zgodnie z ogólnym zarysem odpowiedzi Aruna (ale nieco zmieniona w oparciu o różne priorytety).

Składnia

Składnia jest subiektywna, ale podtrzymuję moje stwierdzenie, że zwięzłość tabeli data.t utrudnia naukę i jest trudniejsza do odczytania. Wynika to częściowo z tego, że dplyr rozwiązuje znacznie łatwiejszy problem!

Jedną z naprawdę ważnych rzeczy, które robi dla ciebie dplyr, jest to, że ogranicza twoje opcje. Twierdzę, że większość problemów z pojedynczą tabelą można rozwiązać za pomocą tylko pięciu czasowników filtrujących, wybierających, mutujących, aranżujących i podsumowujących, wraz z przysłówkiem „według grupy”. To ograniczenie jest dużą pomocą podczas nauki manipulacji danymi, ponieważ pomaga uporządkować myślenie o problemie. W dplyr każdy z tych czasowników jest odwzorowany na pojedynczą funkcję. Każda funkcja wykonuje jedno zadanie i jest łatwa do zrozumienia w izolacji.

Złożoność tworzysz, łącząc ze sobą te proste operacje %>%. Oto przykład jednego z postów połączonych z Arun :

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

Nawet jeśli nigdy wcześniej nie widziałeś dplyr (lub nawet R!), Nadal możesz uzyskać sedno tego, co się dzieje, ponieważ wszystkie funkcje są angielskimi czasownikami. Wadą angielskich czasowników jest to, że wymagają więcej pisania niż [, ale myślę, że można to w dużej mierze złagodzić dzięki lepszemu autouzupełnianiu.

Oto równoważny kod data.table:

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

Trudniej jest podążać za tym kodem, chyba że znasz już dane.tabela. (Nie mogłem też wymyślić, jak wciąć powtórzenie [ w sposób, który wygląda dobrze dla mojego oka). Osobiście, kiedy patrzę na kod, który napisałem 6 miesięcy temu, to tak, jakby patrzeć na kod napisany przez nieznajomego, więc wolę prosty, jeśli szczegółowy, kod.

Dwa inne drobne czynniki, które moim zdaniem nieznacznie zmniejszają czytelność:

  • Ponieważ wykorzystuje się prawie każdą operację na tabeli danych [, potrzebny jest dodatkowy kontekst, aby dowiedzieć się, co się dzieje. Na przykład, czy x[y] łączysz dwie tabele danych czy wyodrębniasz kolumny z ramki danych? To tylko niewielki problem, ponieważ w dobrze napisanym kodzie nazwy zmiennych powinny sugerować, co się dzieje.

  • Podoba mi się, że group_by()to osobna operacja w Dplyr. Zasadniczo zmienia obliczenia, więc myślę, że powinno być oczywiste podczas przeglądania kodu, i łatwiej jest go dostrzec group_by()niż byargument [.data.table.

Podoba mi się również to, że fajka nie ogranicza się tylko do jednej paczki. Możesz zacząć od uporządkowania swoich danych za pomocą tidyr , a skończyć z fabułą w ggvis . I nie jesteś ograniczony do pakietów, które piszę - każdy może napisać funkcję, która stanowi płynną część potoku manipulacji danymi. W rzeczywistości wolę poprzedni kod data.table przepisany za pomocą %>%:

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

A pomysł z rurociągów %>%nie jest ograniczone tylko do ramek danych i jest łatwo uogólnić na innych kontekstach interaktywnych grafik internetowych , web skrobanie , GIST , umów run-time , ...)

Pamięć i wydajność

Zebrałem je razem, ponieważ dla mnie nie są one tak ważne. Większość użytkowników R pracuje z mniej niż 1 milionem wierszy danych, a dplyr jest wystarczająco szybki, aby uzyskać taki rozmiar danych, że nie jesteś świadomy czasu przetwarzania. Optymalizujemy dplyr pod kątem ekspresji na średnich danych; nie krępuj się użyć data.table, by uzyskać surową prędkość na większych danych.

Elastyczność programu dplyr oznacza również, że można łatwo dostosować parametry wydajności przy użyciu tej samej składni. Jeśli wydajność dplyr z backendem ramki danych nie jest dla ciebie wystarczająco dobra, możesz użyć backendu data.table (choć z nieco ograniczonym zestawem funkcji). Jeśli dane, z którymi pracujesz, nie mieszczą się w pamięci, możesz użyć zaplecza bazy danych.

To powiedziawszy, wydajność dplyr poprawi się w dłuższej perspektywie. Zdecydowanie zaimplementujemy niektóre z świetnych pomysłów dotyczących data.table, takich jak porządkowanie radix i używanie tego samego indeksu dla połączeń i filtrów. Pracujemy również nad równoległością, abyśmy mogli skorzystać z wielu rdzeni.

funkcje

Kilka rzeczy, nad którymi planujemy pracować w 2015 roku:

  • readrpakiet, aby go łatwo dostać plików z dysku iw pamięci, analogicznie do fread().

  • Bardziej elastyczne połączenia, w tym obsługa połączeń innych niż równorzędne.

  • Bardziej elastyczne grupowanie, takie jak próbki bootstrap, rollupy i więcej

Inwestuję też czas w ulepszanie łączników bazy danych R , możliwość komunikowania się z aplikacjami internetowymi i ułatwianie skrobania stron HTML .

Hadley
źródło
27
Na marginesie, zgadzam się z wieloma twoimi argumentami (chociaż ja osobiście wolę data.tableskładnię), ale możesz łatwo użyć %>%, aby potokować data.tableoperacje, jeśli nie lubisz [stylu. %>%nie jest specyficzne dplyr, raczej pochodzi z osobnego pakietu (którego jesteś też współautorem), więc nie jestem pewien, czy rozumiem, co próbujesz powiedzieć w większości akapitów składni .
David Arenburg
11
@DavidArenburg dobry punkt. Ponownie napisałem składnię, aby, mam nadzieję, wyjaśnić, jakie są moje główne punkty, i aby podkreślić, że można używać %>%z data.table
hadley
5
Dzięki, Hadley, to przydatna perspektywa. Jeśli chodzi o wcięcia, zwykle robię DT[\n\texpression\n][\texpression\n]( gist ), co w rzeczywistości działa całkiem dobrze. Trzymam odpowiedź Arun jako odpowiedź, kiedy bardziej bezpośrednio odpowiada na moje pytania szczegółowe, które nie są tak bardzo o dostępności składni, ale myślę, że to dobre rozwiązanie dla ludzi, którzy próbują uzyskać ogólne wyczucie różnic / podobieństw pomiędzy dplyri data.table.
BrodieG
33
Po co pracować na fastread, gdy już jest fread()? Czy nie lepiej byłoby spędzać czas na ulepszaniu fread () lub pracowaniu nad innymi (słabo rozwiniętymi) rzeczami?
EDi
10
Interfejs API data.tableopiera się na masowym nadużywaniu []notacji. To jest jego największa siła i największa słabość.
Paul
65

W bezpośredniej odpowiedzi na tytuł pytania ...

dplyr zdecydowanie robi rzeczy, data.tablektórych nie można.

Twój punkt # 3

dplyr streszcza (lub będzie) potencjalne interakcje DB

jest bezpośrednią odpowiedzią na twoje pytanie, ale nie jest podniesiony do wystarczająco wysokiego poziomu. dplyrjest naprawdę rozszerzalnym interfejsem do wielu mechanizmów przechowywania danych, gdzie data.tablejest rozszerzeniem do jednego.

Spójrz na dplyrinterfejs agnostyczny zaplecza, w którym wszystkie cele używają tego samego gramatyki, gdzie możesz dowolnie rozszerzać cele i programy obsługi. data.tablez dplyrperspektywy jest jednym z tych celów.

Nigdy (mam nadzieję) nie zobaczysz dnia, w którym data.tablespróbujesz przetłumaczyć twoje zapytania, aby utworzyć instrukcje SQL działające z dyskowymi lub sieciowymi magazynami danych.

dplyrmoże zrobić rzeczy data.table, których nie zrobi lub nie.

Opierając się na konstrukcji działającej w pamięci, data.tablemoże mieć znacznie trudniejszy czas, rozszerzając się na równoległe przetwarzanie zapytań niż dplyr.


W odpowiedzi na pytania w ciele ...

Stosowanie

Czy istnieją zadania analityczne, które są o wiele łatwiejsze do zakodowania w jednym lub drugim pakiecie dla osób zaznajomionych z pakietami (tj. Wymagana jest kombinacja kombinacji klawiszy w porównaniu z wymaganym poziomem ezoteryzmu, gdzie mniejsza część jest dobra).

To może wydawać się wykopem, ale prawdziwa odpowiedź brzmi „nie”. Ludzie zaznajomieni z narzędziami wydają się korzystać z tego, który jest im najbardziej znany, lub takiego, który jest właściwy dla danego zadania. Biorąc to pod uwagę, czasami chcesz zaprezentować konkretną czytelność, czasem poziom wydajności, a gdy potrzebujesz wystarczająco wysokiego poziomu obu, możesz potrzebować innego narzędzia, aby dostosować się do tego, co już musisz zrobić wyraźniejsze abstrakty .

Wydajność

Czy istnieją zadania analityczne, które są wykonywane znacznie (tj. Ponad 2x) bardziej efektywnie w jednym pakiecie w porównaniu do drugiego.

Znowu nie. data.tablewyróżnia się wydajnością we wszystkim, co robi, gdzie dplyrogranicza się pod pewnymi względami do bazowego magazynu danych i zarejestrowanych procedur obsługi.

Oznacza to, że gdy napotkasz problem z wydajnością data.table, możesz być całkiem pewien, że jest to funkcja zapytania, a jeśli faktycznie jest to wąskie gardło data.table, to zyskałeś radość ze złożenia raportu. Dotyczy to również sytuacji, gdy dplyrjest używany data.tablejako back-end; Państwo mogą zobaczyć jakiś narzut z dplyrale kursy są to zapytanie.

Gdy dplyrwystępują problemy z wydajnością back-endów, można je obejść, rejestrując funkcję do oceny hybrydowej lub (w przypadku baz danych) manipulując wygenerowanym zapytaniem przed wykonaniem.

Zobacz także zaakceptowaną odpowiedź na pytanie, kiedy plyr jest lepszy niż data.table?

Thell
źródło
3
Nie możesz zawijać tabeli data.tt za pomocą tbl_dt? Dlaczego nie skorzystać z tego, co najlepsze z obu światów?
aaa90210,
22
Zapomniałeś wspomnieć o zwrotnym stwierdzeniu „data.table zdecydowanie robi rzeczy, których dplyr nie może”, co również jest prawdą.
jangorecki
25
Odpowiedź Aruna wyjaśnia to dobrze. Najważniejsze (pod względem wydajności) byłyby fread, aktualizacja przez odniesienie, zwijanie złączeń, nakładanie się złączeń. Uważam, że nie ma żadnego pakietu (nie tylko dplyr), który mógłby konkurować z tymi funkcjami. Dobrym przykładem może być ostatni slajd z tej prezentacji.
jangorecki
15
Całkowicie, data.table jest powodem, dla którego wciąż używam R. W przeciwnym razie użyłbym pand. Jest nawet lepszy / szybszy niż pandy.
marbel
8
Podoba mi się data.table ze względu na jego prostotę i podobieństwo do struktury składni SQL. Moja praca polega na codziennym przeprowadzaniu bardzo intensywnej analizy danych doraźnych i grafik do modelowania statystycznego, a naprawdę potrzebuję narzędzia wystarczająco prostego do robienia skomplikowanych rzeczy. Teraz mogę zredukować zestaw narzędzi do tabeli data.tab dla danych i sieci graficznej w mojej codziennej pracy. Podaj przykład, że mogę nawet wykonywać takie operacje: $ DT [grupa == 1, y_hat: = przewiduj (fit1, dane = .SD),] $, co jest naprawdę fajne i uważam to za wielką zaletę SQL w klasyczne środowisko R.
xappppp