dplyr trên data.table, tôi có thực sự đang sử dụng data.table không?


91

Nếu tôi sử dụng dplyr cú pháp trên đầu trang của một DataTable , tôi nhận được tất cả những lợi ích tốc độ của DataTable trong khi vẫn sử dụng cú pháp của dplyr? Nói cách khác, tôi có sử dụng sai dữ liệu nếu tôi truy vấn nó bằng cú pháp dplyr không? Hay tôi cần sử dụng cú pháp dữ liệu thuần túy để khai thác tất cả sức mạnh của nó.

Cảm ơn trước cho tất cả lời khuyên. Ví dụ về mã:

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

Các kết quả:

#         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

Đây là sự tương đương về dữ liệu tôi đã nghĩ ra. Không chắc chắn nếu nó tuân thủ DT tốt. Nhưng tôi tự hỏi liệu mã có thực sự hiệu quả hơn cú pháp dplyr đằng sau cảnh hay không:

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

7
Tại sao bạn không sử dụng cú pháp bảng dữ liệu? Nó cũng thanh lịch và hiệu quả. Câu hỏi không thực sự trả lời được vì nó rất rộng. Có, có dplyrphương pháp cho các bảng dữ liệu, nhưng bảng dữ liệu cũng có phương pháp so sánh riêng của mình
Giàu Scriven

7
Tôi có thể sử dụng cú pháp hoặc khóa học có thể dữ liệu được. Nhưng bằng cách nào đó, tôi thấy cú pháp dplyr thanh lịch hơn. Bất kể sở thích của tôi đối với cú pháp. Điều tôi thực sự muốn biết là: tôi có cần sử dụng cú pháp lập trình dữ liệu thuần túy để nhận được 100% lợi ích của sức mạnh dữ liệu không.
Polymerase

3
Để biết điểm chuẩn gần đây dplyrđược sử dụng trên data.frames và data.tablecác s tương ứng , hãy xem tại đây (và các tài liệu tham khảo trong đó).
Henrik

2
@Polymerase - Tôi nghĩ rằng câu trả lời cho câu hỏi đó chắc chắn là "Có"
Giàu Scriven

1
@Henrik: Sau đó, tôi nhận ra rằng tôi đã hiểu sai trang đó vì họ chỉ hiển thị mã cho cấu trúc khung dữ liệu chứ không hiển thị mã mà họ sử dụng để xây dựng data.table. Khi tôi nhận ra điều đó, tôi đã xóa bình luận của mình (hy vọng bạn đã không nhìn thấy nó).
IRTFM

Câu trả lời:


77

Không có câu trả lời đơn giản / dễ hiểu vì triết lý của cả hai gói này khác nhau ở một số khía cạnh nhất định. Vì vậy, một số thỏa hiệp là khó tránh khỏi. Dưới đây là một số mối quan tâm bạn có thể cần giải quyết / xem xét.

Các hoạt động liên quan đến i(== filter()slice()trong dplyr)

Giả sử DTvới giả sử 10 cột. Hãy xem xét các biểu thức data.table sau:

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

(1) cho biết số hàng trong DTcột nơi a > 1. (2) trả về mean(b)được nhóm theo c,dcho cùng một biểu thức trong i(1).

Các dplyrbiểu thức thường được sử dụng sẽ là:

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

Rõ ràng, mã data.table ngắn hơn. Ngoài ra chúng cũng hiệu quả hơn về bộ nhớ 1 . Tại sao? Bởi vì trong cả (3) và (4), filter()trả về các hàng cho tất cả 10 cột trước, khi ở (3) chúng ta chỉ cần số hàng và trong (4), chúng ta chỉ cần các cột b, c, dcho các hoạt động kế tiếp. Để khắc phục điều này, chúng ta phải select()cột 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)

Điều cần thiết là làm nổi bật sự khác biệt chính về triết học giữa hai gói:

  • Trong đó data.table, chúng tôi muốn giữ các thao tác liên quan này lại với nhau, và điều đó cho phép xem xét j-expression(từ cùng một lệnh gọi hàm) và nhận ra rằng không cần bất kỳ cột nào trong (1). Biểu thức trong iđược tính toán và .Nchỉ là tổng của vectơ logic đó cung cấp số hàng; toàn bộ tập hợp con không bao giờ được nhận ra. Trong (2), chỉ cột b,c,dđược thực hiện trong tập hợp con, các cột khác bị bỏ qua.

  • Nhưng trong dplyr, triết lý là phải có một chức năng làm chính xác một điều tốt . Không có (ít nhất là hiện tại) không có cách nào để biết liệu hoạt động sau đó có filter()cần tất cả các cột mà chúng tôi đã lọc hay không. Bạn sẽ cần phải suy nghĩ trước nếu muốn thực hiện những công việc như vậy một cách hiệu quả. Cá nhân tôi thấy nó phản cảm trong trường hợp này.

Lưu ý rằng trong (5) và (6), chúng tôi vẫn đặt cột con amà chúng tôi không yêu cầu. Nhưng tôi không chắc làm thế nào để tránh điều đó. Nếu filter()hàm có một đối số để chọn các cột để trả về, chúng ta có thể tránh được vấn đề này, nhưng sau đó hàm sẽ không chỉ thực hiện một tác vụ (cũng là một lựa chọn thiết kế dplyr).

Chỉ định phụ bằng tham chiếu

dplyr sẽ không bao giờ cập nhật bằng cách tham khảo. Đây là một khác biệt lớn (triết học) giữa hai gói.

Ví dụ, trong data.table, bạn có thể thực hiện:

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

cập nhật cột a bằng cách tham chiếu chỉ trên những hàng thỏa mãn điều kiện. Hiện tại, dplyr sao chép sâu toàn bộ data.table trong nội bộ để thêm một cột mới. @BrodieG đã đề cập đến điều này trong câu trả lời của anh ấy.

Nhưng bản sao sâu có thể được thay thế bằng bản sao nông khi FR # 617 được triển khai. Cũng có liên quan: dplyr: FR # 614 . Tuy nhiên, lưu ý rằng cột bạn sửa đổi sẽ luôn được sao chép (do đó, bộ nhớ chậm hơn / kém hiệu quả hơn). Sẽ không có cách nào để cập nhật các cột bằng cách tham chiếu.

Các chức năng khác

  • Trong data.table, bạn có thể tổng hợp trong khi tham gia và điều này dễ hiểu hơn và hiệu quả về bộ nhớ vì kết quả nối trung gian không bao giờ được thực hiện. Kiểm tra bài đăng này cho một ví dụ. Bạn không thể (vào lúc này?) Làm điều đó bằng cú pháp data.table / data.frame của dplyr.

  • Tính năng kết hợp cuộn của data.table cũng không được hỗ trợ trong cú pháp của dplyr.

  • Gần đây chúng tôi đã triển khai các phép nối chồng chéo trong data.table để nối trên các phạm vi khoảng thời gian ( đây là một ví dụ ), đây là một hàm riêng biệt foverlaps()tại thời điểm này và do đó có thể được sử dụng với các toán tử ống (magrittr / pipeR? - chưa bao giờ tôi tự thử).

    Nhưng cuối cùng, mục tiêu của chúng tôi là tích hợp nó vào [.data.tableđể chúng tôi có thể thu thập các tính năng khác như nhóm, tổng hợp trong khi tham gia, v.v. sẽ có những hạn chế tương tự như đã nêu ở trên.

  • Kể từ 1.9.4, data.table thực hiện lập chỉ mục tự động bằng cách sử dụng các khóa phụ cho các tập con dựa trên tìm kiếm nhị phân nhanh trên cú pháp R thông thường. Ví dụ: DT[x == 1]DT[x %in% some_vals]sẽ tự động tạo chỉ mục trong lần chạy đầu tiên, chỉ mục này sau đó sẽ được sử dụng trên các tập con liên tiếp từ cùng một cột đến tập con nhanh bằng cách sử dụng tìm kiếm nhị phân. Tính năng này sẽ tiếp tục phát triển. Kiểm tra ý chính này để biết tổng quan ngắn gọn về tính năng này.

    Từ cách filter()được triển khai cho data.tables, nó không tận dụng được tính năng này.

  • Một tính năng của dplyr là nó cũng cung cấp giao diện cho cơ sở dữ liệu bằng cách sử dụng cùng một cú pháp, điều mà data.table không có vào lúc này.

Vì vậy, bạn sẽ phải cân nhắc những điểm này (và có thể là những điểm khác) và quyết định dựa trên việc liệu những đánh đổi này có thể chấp nhận được với bạn hay không.

HTH


(1) Lưu ý rằng hiệu quả về bộ nhớ sẽ ảnh hưởng trực tiếp đến tốc độ (đặc biệt là khi dữ liệu ngày càng lớn), vì nút cổ chai trong hầu hết các trường hợp là di chuyển dữ liệu từ bộ nhớ chính vào bộ nhớ đệm (và tận dụng dữ liệu trong bộ nhớ đệm càng nhiều càng tốt - giảm bỏ lỡ bộ nhớ cache - để giảm truy cập bộ nhớ chính). Không đi vào chi tiết ở đây.


4
Hoàn toàn rực rỡ. Cảm ơn vì điều đó
David Arenburg

6
Đó là một câu trả lời hay, nhưng có thể (nếu không có khả năng) dplyr triển khai một phép filter()cộng hiệu quả summarise()bằng cách sử dụng cùng một cách tiếp cận mà dplyr sử dụng cho SQL - tức là xây dựng một biểu thức và sau đó chỉ thực thi một lần theo yêu cầu. Điều này không chắc sẽ được thực hiện trong tương lai gần vì dplyr đủ nhanh đối với tôi và việc triển khai một công cụ lập kế hoạch / tối ưu hóa truy vấn là tương đối khó.
hadley

Bộ nhớ hiệu quả cũng giúp ích trong một lĩnh vực quan trọng khác - thực sự hoàn thành nhiệm vụ trước khi hết bộ nhớ. Khi làm việc với các tập dữ liệu lớn, tôi đã gặp phải vấn đề đó với dplyr cũng như gấu trúc, trong khi data.table sẽ hoàn thành công việc một cách duyên dáng.
Zaki

25

Hãy thử nó.

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]

Về vấn đề này, có vẻ như data.table nhanh hơn 2,4 lần so với dplyr bằng cách sử dụng data.table:

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

Đã sửa đổi dựa trên nhận xét của Polymerase.


2
Bằng cách sử dụng microbenchmarkgói, tôi thấy rằng việc chạy dplyrmã OP trên phiên bản gốc (khung dữ liệu) diamondsmất thời gian trung bình là 0,012 giây, trong khi thời gian trung bình là 0,024 giây sau khi chuyển đổi diamondssang bảng dữ liệu. Chạy data.tablemã của G. Grothendieck mất 0,013 giây. Ít nhất trên hệ thống của tôi, nó trông giống dplyrdata.tablecó cùng hiệu suất. Nhưng tại sao lại dplyrchậm hơn khi khung dữ liệu lần đầu tiên được chuyển đổi thành bảng dữ liệu?
eipi10

Grothendieck thân mến, điều này thật tuyệt vời. Cảm ơn bạn đã cho tôi xem tiện ích điểm chuẩn này. BTW bạn quên [order (-Count)] trong phiên bản có thể dữ liệu để tạo ra sự tương đương của sắp xếp của dplyr (desc (Count)). Sau khi thêm điều này, dữ liệu vẫn nhanh hơn khoảng x1,8 (thay vì 2,9).
Polymerase

@ eipi10 bạn có thể chạy lại băng ghế của mình một lần nữa với phiên bản có thể dữ liệu ở đây (đã thêm sắp xếp theo desc Count ở bước cuối cùng): diamondDT [cut! = "Fair", list (AvgPrice = mean (price), MedianPrice = as.numeric (median) (price)), Count = .N), by = cut] [order (-Count)]
Polymerase

Vẫn là 0,013 giây. Thao tác sắp xếp hầu như không mất thời gian vì nó chỉ sắp xếp lại bảng cuối cùng, chỉ có bốn hàng.
eipi

1
Có một số chi phí cố định cho việc chuyển đổi từ cú pháp dplyr sang cú pháp bảng dữ liệu, vì vậy có thể đáng để thử các kích thước vấn đề khác nhau. Ngoài ra, tôi có thể đã không triển khai mã bảng dữ liệu hiệu quả nhất trong dplyr; các bản vá luôn được chào đón
hadley

22

Để trả lời câu hỏi của bạn:

  • Có, bạn đang sử dụng data.table
  • Nhưng không hiệu quả như bạn làm với data.tablecú pháp thuần túy

Trong nhiều trường hợp, đây sẽ là một thỏa hiệp có thể chấp nhận được đối với những người muốn dplyrcú pháp, mặc dù nó có thể sẽ chậm hơn so dplyrvới các khung dữ liệu thuần túy.

Một yếu tố lớn dường như dplyrsẽ sao chép data.tabletheo mặc định khi nhóm. Cân nhắc (sử dụng 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

Quá trình lọc có tốc độ tương đương, nhưng việc phân nhóm thì không. Tôi tin rằng thủ phạm là dòng này trong dplyr:::grouped_dt:

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

trong đó copymặc định là TRUE(và không thể dễ dàng thay đổi thành FALSE mà tôi có thể thấy). Điều này có thể không chiếm 100% sự khác biệt, nhưng chỉ riêng chi phí chung trên một thứ có kích thước tương đối diamondskhông phải là sự khác biệt hoàn toàn.

Vấn đề là để có một ngữ pháp nhất quán, hãy dplyrphân nhóm theo hai bước. Đầu tiên nó đặt các khóa trên bản sao của bảng dữ liệu gốc khớp với các nhóm và chỉ sau này nó mới nhóm lại. data.tablechỉ cấp phát bộ nhớ cho nhóm kết quả lớn nhất, trong trường hợp này chỉ là một hàng, do đó, điều đó tạo ra sự khác biệt lớn về lượng bộ nhớ cần được cấp phát.

FYI, nếu ai đó quan tâm, tôi đã tìm thấy điều này bằng cách sử dụng treeprof( install_github("brodieg/treeprof")), một trình xem cây thử nghiệm (và vẫn còn rất nhiều alpha) cho Rprofđầu ra:

nhập mô tả hình ảnh ở đây

Lưu ý rằng điều trên hiện chỉ hoạt động trên máy tính AFAIK. Ngoài ra, thật không may, Rprofbản ghi các cuộc gọi thuộc loại packagename::funnameẩn danh nên nó thực sự có thể là bất kỳ và tất cả các datatable::cuộc gọi bên trong grouped_dtchịu trách nhiệm, nhưng từ thử nghiệm nhanh, nó có vẻ datatable::copylà một cuộc gọi lớn.

Điều đó nói rằng, bạn có thể nhanh chóng thấy rằng không có quá nhiều chi phí xung quanh [.data.tablecuộc gọi, nhưng cũng có một nhánh hoàn toàn riêng biệt cho việc nhóm.


CHỈNH SỬA : để xác nhận việc sao chép:

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

Điều này là tuyệt vời, cảm ơn. Điều đó có nghĩa là, dplyr :: group_by () sẽ tăng gấp đôi yêu cầu bộ nhớ (so với cú pháp dữ liệu thuần túy) vì bước sao chép dữ liệu nội bộ? Có nghĩa là nếu kích thước đối tượng có thể dữ liệu của tôi là 1GB và tôi sử dụng cú pháp chuỗi dplyr tương tự như cú pháp trong bài đăng gốc. Tôi sẽ cần ít nhất 2GB bộ nhớ trống để nhận kết quả?
Polymerase

2
Tôi cảm thấy như tôi đã sửa lỗi đó trong phiên bản dành cho nhà phát triển?
hadley

@hadley, tôi đang làm việc từ phiên bản CRAN. Nhìn vào nhà phát triển, có vẻ như bạn đã giải quyết được một phần vấn đề, nhưng bản sao thực sự vẫn còn (chưa được kiểm tra, chỉ nhìn vào dòng c (20, 30:32) trong R / grouped-dt.r. Hiện tại có thể nhanh hơn, nhưng tôi đặt cược bước chậm là sao chép.
BrodieG

3
Tôi cũng đang chờ một hàm sao chép nông trong data.table; cho đến lúc đó tôi nghĩ tốt hơn là an toàn hơn là nhanh chóng.
hadley

2

Bạn có thể sử dụng dtplyr ngay bây giờ, là một phần của ngăn nắp . Nó cho phép bạn sử dụng các câu lệnh kiểu dplyr như bình thường, nhưng sử dụng tính năng đánh giá lười biếng và dịch các câu lệnh của bạn thành mã data.table. Chi phí dịch thuật là tối thiểu, nhưng bạn thu được tất cả, nếu không, hầu hết các lợi ích của data.table. Thông tin chi tiết tại repo git chính thức ở đâytrang tiện ích .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.