dplyr di data.table, apakah saya benar-benar menggunakan data.table?

91

Jika saya menggunakan sintaks dplyr di atas datatable , apakah saya mendapatkan semua manfaat kecepatan dari datatable saat masih menggunakan sintaks dplyr? Dengan kata lain, apakah saya salah menggunakan datatable jika saya menanyakannya dengan sintaks dplyr? Atau apakah saya perlu menggunakan sintaks datatable murni untuk memanfaatkan semua kekuatannya.

Terima kasih sebelumnya atas saran apa pun. Contoh Kode:

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

Hasil:

#         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

Berikut adalah persamaan data yang saya dapatkan. Tidak yakin apakah itu sesuai dengan praktik DT yang baik. Tapi saya ingin tahu apakah kodenya benar-benar lebih efisien daripada sintaks dplyr di belakang layar:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]
Polimerase
sumber
7
Mengapa Anda tidak menggunakan sintaks tabel data? Elegan dan efisien juga. Pertanyaan ini sebenarnya tidak dapat dijawab karena sangat luas. Ya, ada dplyrmetode untuk tabel data, tetapi tabel data juga memiliki metode yang sebanding
Rich Scriven
7
Saya dapat menggunakan sintaks atau kursus yang dapat didata. Tapi entah bagaimana, menurut saya sintaks dplyr lebih elegan. Terlepas dari preferensi saya untuk sintaks. Apa yang benar-benar ingin saya ketahui adalah: apakah saya perlu menggunakan sintaks datatable murni untuk mendapatkan 100% manfaat dari daya dataable.
Polymerase
3
Untuk tolok ukur terbaru di mana dplyrdigunakan pada data.frames dan data.tables yang sesuai , lihat di sini (dan referensi di dalamnya).
Henrik
2
@Polymerase - Saya pikir jawaban atas pertanyaan itu pasti "Ya"
Rich Scriven
1
@Henrik: Saya kemudian menyadari bahwa saya telah salah menafsirkan halaman itu karena mereka hanya menampilkan kode untuk konstruksi dataframe tetapi bukan kode yang mereka gunakan untuk konstruksi data.table. Ketika saya menyadarinya, saya menghapus komentar saya (berharap Anda tidak melihatnya).
IRTFM

Jawaban:

77

Tidak ada jawaban yang lugas / sederhana karena filosofi kedua paket ini berbeda dalam aspek tertentu. Jadi beberapa kompromi tidak bisa dihindari. Berikut adalah beberapa masalah yang mungkin perlu Anda tangani / pertimbangkan.

Operasi yang melibatkan i(== filter()danslice() dalam dplyr)

Asumsikan DTdengan katakanlah 10 kolom. Pertimbangkan ekspresi data.table ini:

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

(1) memberikan jumlah baris di DTmana kolom a > 1. (2) mengembalikan mean(b)dikelompokkan oleh c,duntuk ekspresi yang sama iseperti (1).

dplyrEkspresi yang umum digunakan adalah:

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

Jelasnya, kode data.table lebih pendek. Selain itu mereka juga lebih hemat memori 1 . Mengapa? Karena di kedua (3) dan (4), filter()mengembalikan baris untuk semua 10 kolom terlebih dahulu, ketika di (3) kita hanya membutuhkan jumlah baris, dan di (4) kita hanya perlu kolom b, c, duntuk operasi berturut-turut. Untuk mengatasinya, kita harus select()kolom 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)

Penting untuk menyoroti perbedaan filosofis utama antara kedua paket:

  • Di data.table, kami ingin menjaga operasi terkait ini bersama-sama, dan itu memungkinkan untuk melihat j-expression(dari pemanggilan fungsi yang sama) dan menyadari bahwa tidak diperlukan kolom apa pun di (1). Ekspresi dalam idihitung, dan .Nhanya jumlah dari vektor logika yang memberikan jumlah baris; seluruh bagian tidak pernah terwujud. Dalam (2), hanya kolom b,c,dyang terwujud dalam subset, kolom lain diabaikan.

  • Tapi dalam dplyr, filosofi adalah memiliki fungsi melakukan tepat satu hal dengan baik . Tidak (setidaknya saat ini) tidak ada cara untuk mengetahui apakah operasi setelahnya filter()membutuhkan semua kolom yang kami filter. Anda harus berpikir jauh ke depan jika ingin melakukan tugas tersebut secara efisien. Saya pribadi merasa kontra-intutitif dalam kasus ini.

Perhatikan bahwa di (5) dan (6), kami masih subset kolom ayang tidak kami butuhkan. Tapi saya tidak yakin bagaimana menghindarinya. Jikafilter() fungsi memiliki argumen untuk memilih kolom yang akan dikembalikan, kita dapat menghindari masalah ini, tetapi kemudian fungsi tersebut tidak hanya melakukan satu tugas (yang juga merupakan pilihan desain dplyr).

Sub-tugaskan dengan referensi

dplyr tidak akan pernah diperbarui dengan referensi. Ini adalah perbedaan besar (filosofis) antara kedua paket.

Misalnya, di data.table Anda dapat melakukan:

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

yang memperbarui kolom a dengan referensi hanya pada baris-baris yang memenuhi kondisi. Saat ini dplyr deep menyalin seluruh data.table secara internal untuk menambahkan kolom baru. @BrodieG sudah menyebutkan ini dalam jawabannya.

Tetapi salinan dalam dapat diganti dengan salinan dangkal ketika FR # 617 diimplementasikan. Juga relevan: dplyr: FR # 614 . Perhatikan bahwa tetap saja, kolom yang Anda ubah akan selalu disalin (oleh karena itu anak laki-laki lebih lambat / kurang efisien memori). Tidak ada cara untuk memperbarui kolom dengan referensi.

Fungsi lainnya

  • Dalam data.table, Anda dapat menggabungkan saat bergabung, dan ini lebih mudah dipahami dan hemat memori karena hasil gabungan antara tidak pernah terwujud. Lihat posting ini sebagai contoh. Anda tidak dapat (saat ini?) Melakukan itu menggunakan sintaks data.table / data.frame dplyr.

  • Fitur rolling join data.table juga tidak didukung dalam sintaks dplyr.

  • Kami baru-baru ini mengimplementasikan gabungan tumpang tindih di data.table untuk menggabungkan rentang interval ( berikut adalah contoh ), yang merupakan fungsi terpisahfoverlaps() saat ini, dan oleh karena itu dapat digunakan dengan operator pipa (magrittr / pipeR? - tidak pernah mencobanya sendiri).

    Tetapi pada akhirnya, tujuan kami adalah untuk mengintegrasikannya ke dalam [.data.tablesehingga kami dapat memanen fitur lain seperti pengelompokan, menggabungkan saat bergabung, dll .. yang akan memiliki batasan yang sama seperti yang diuraikan di atas.

  • Sejak 1.9.4, data.table mengimplementasikan pengindeksan otomatis menggunakan kunci sekunder untuk subset berbasis pencarian biner cepat pada sintaks R. Contoh: DT[x == 1]dan DT[x %in% some_vals]secara otomatis akan membuat indeks saat pertama kali dijalankan, yang kemudian akan digunakan pada subset berturut-turut dari kolom yang sama ke subset cepat menggunakan pencarian biner. Fitur ini akan terus berkembang. Periksa inti ini untuk gambaran singkat tentang fitur ini.

    Dari cara filter()diimplementasikan untuk data.tables, tidak memanfaatkan fitur ini.

  • Fitur dplyr adalah ia juga menyediakan antarmuka ke database menggunakan sintaks yang sama, yang saat ini tidak dimiliki data.table.

Jadi, Anda harus mempertimbangkan ini (dan mungkin poin lainnya) dan memutuskan berdasarkan apakah trade-off ini dapat Anda terima.

HTH


(1) Perhatikan bahwa efisiensi memori secara langsung berdampak pada kecepatan (terutama saat data semakin besar), karena hambatan dalam banyak kasus adalah memindahkan data dari memori utama ke cache (dan memanfaatkan data dalam cache sebanyak mungkin - kurangi cache yang hilang - sehingga mengurangi pengaksesan memori utama). Tidak membahas detailnya di sini.

Arun
sumber
4
Sangat brilian. Terima kasih untuk itu
David Arenburg
6
Itu jawaban yang bagus, tapi akan mungkin (jika tidak mungkin) untuk dplyr untuk mengimplementasikan filter()plus efisien summarise()menggunakan pendekatan yang sama yang digunakan dplyr untuk SQL - yaitu membangun ekspresi dan kemudian hanya mengeksekusi satu kali sesuai permintaan. Ini tidak mungkin ini akan diterapkan dalam waktu dekat karena dplyr cukup cepat untuk saya dan menerapkan perencana / pengoptimal kueri relatif sulit.
hadley
Menjadi hemat memori juga membantu di area penting lainnya - benar-benar menyelesaikan tugas sebelum kehabisan memori. Saat bekerja dengan kumpulan data besar, saya menghadapi masalah itu dengan dplyr serta panda, sedangkan data.table akan menyelesaikan pekerjaan dengan baik.
Zaki
25

Cobalah.

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]

Pada masalah ini tampaknya data.table 2.4x lebih cepat daripada dplyr menggunakan data.table:

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

Direvisi berdasarkan komentar Polymerase.

G. Grothendieck
sumber
2
Menggunakan microbenchmarkpaket, saya menemukan bahwa menjalankan dplyrkode OP pada versi asli (bingkai data) diamondsmembutuhkan waktu rata-rata 0,012 detik, sementara itu membutuhkan waktu median 0,024 detik setelah mengonversi diamondske tabel data. Menjalankan data.tablekode G. Grothendieck membutuhkan 0,013 detik. Setidaknya di sistem saya, sepertinya dplyrdan data.tablememiliki kinerja yang hampir sama. Tetapi mengapa akan dplyrlebih lambat ketika bingkai data pertama kali diubah ke tabel data?
eipi10
Dear G. Grothendieck, ini luar biasa. Terima kasih telah menunjukkan kepada saya utilitas patokan ini. BTW Anda lupa [order (-Count)] dalam versi datatable untuk membuat ekuivalen dari dplyr's arranger (desc (Count)). Setelah menambahkan ini, dataTable masih lebih cepat sekitar x1.8 (bukan 2.9).
Polymerase
@ eipi10 dapatkah Anda menjalankan kembali bangku Anda lagi dengan versi yang dapat diperbarui di sini (menambahkan pengurutan berdasarkan desc Hitung pada langkah terakhir): diamondsDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median (harga)), Hitung = .N), oleh = potong] [pesanan (-Hitung)]
Polimerase
Masih 0,013 detik. Operasi pemesanan tidak memakan waktu lama karena hanya menyusun ulang tabel akhir, yang hanya memiliki empat baris.
eipi10
1
Ada beberapa overhead tetap untuk konversi dari sintaks dplyr ke sintaks tabel data, jadi mungkin ada baiknya mencoba berbagai ukuran masalah. Juga saya mungkin belum menerapkan kode tabel data yang paling efisien di dplyr; tambalan selalu disambut
hadley
22

Untuk menjawab pertanyaan Anda:

  • Ya, Anda menggunakan data.table
  • Tetapi tidak seefisien yang Anda lakukan dengan data.tablesintaks murni

Dalam banyak kasus, ini akan menjadi kompromi yang dapat diterima bagi mereka yang menginginkan dplyrsintaks, meskipun mungkin akan lebih lambat dibandingkan dplyrdengan bingkai data biasa.

Satu faktor besar tampaknya dplyrakan menyalin data.tablesecara default saat pengelompokan. Pertimbangkan (menggunakan 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

Pemfilteran memiliki kecepatan yang sebanding, tetapi pengelompokannya tidak. Saya yakin pelakunya adalah baris ini di dplyr:::grouped_dt:

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

di mana copydefaultnya ke TRUE(dan tidak bisa dengan mudah diubah menjadi FALSE yang bisa saya lihat). Ini mungkin tidak memperhitungkan 100% perbedaan, tetapi overhead umum saja pada sesuatu yang ukurannya diamondspaling mungkin bukanlah perbedaan penuh.

Persoalannya adalah agar memiliki tata bahasa yang konsisten, dplyrlakukan pengelompokan dalam dua tahap. Ini pertama-tama menetapkan kunci pada salinan tabel data asli yang cocok dengan grup, dan baru kemudian mengelompokkan. data.tablehanya mengalokasikan memori untuk grup hasil terbesar, yang dalam hal ini hanya satu baris, sehingga membuat perbedaan besar dalam berapa banyak memori yang perlu dialokasikan.

FYI, jika ada yang peduli, saya menemukan ini dengan menggunakan treeprof( install_github("brodieg/treeprof")), penampil pohon eksperimental (dan masih sangat banyak alfa) untuk Rprofkeluaran:

masukkan deskripsi gambar di sini

Catatan di atas saat ini hanya berfungsi di macs AFAIK. Selain itu, sayangnya, Rprofmerekam panggilan jenis ini packagename::funnamesebagai anonim sehingga sebenarnya bisa menjadi salah satu dan semua datatable::panggilan di dalamnya grouped_dtyang bertanggung jawab, tetapi dari pengujian cepat sepertinya datatable::copyitu yang paling penting.

Meskipun demikian, Anda dapat dengan cepat melihat bagaimana tidak ada banyak overhead di sekitar [.data.tablepanggilan, tetapi ada juga cabang yang benar-benar terpisah untuk pengelompokan.


EDIT : untuk mengkonfirmasi penyalinan:

> 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
sumber
Ini luar biasa, terima kasih. Apakah itu berarti, dplyr :: group_by () akan menggandakan kebutuhan memori (dibandingkan dengan sintaks datatable murni) karena langkah penyalinan data internal? Artinya jika ukuran objek data saya adalah 1GB, dan saya menggunakan sintaks dplyr chained mirip dengan yang ada di posting asli. Saya memerlukan setidaknya memori kosong 2GB untuk mendapatkan hasilnya?
Polymerase
2
Saya merasa seperti saya memperbaikinya di versi dev?
hadley
@adley, saya bekerja dari versi CRAN. Melihat dev, sepertinya Anda telah mengatasi sebagian masalah, tetapi salinan sebenarnya tetap (belum diuji, lihat saja baris c (20, 30:32) di R / grouped-dt.r. Mungkin sekarang lebih cepat, tapi Saya yakin langkah lambat adalah salinannya.
BrodieG
3
Saya juga menunggu fungsi salin dangkal di data.table; sampai saat itu saya pikir lebih baik aman daripada berpuasa.
hadley
2

Anda dapat menggunakan dtplyr sekarang, yang merupakan bagian dari tidyverse . Ini memungkinkan Anda untuk menggunakan pernyataan gaya dplyr seperti biasa, tetapi menggunakan evaluasi malas dan menerjemahkan pernyataan Anda ke kode data.table di bawah tenda. Overhead dalam penerjemahan minimal, tetapi Anda mendapatkan semua, jika tidak, sebagian besar manfaat data.table. Detail lebih lanjut di repo git resmi di sini dan halaman tidyverse .

Susu Hitam
sumber