Hiệu suất của việc kết nối ANDing khi lọc bảng dữ liệu


12

Tôi có thói quen gộp các nhiệm vụ tương tự lại với nhau thành một dòng duy nhất. Ví dụ, nếu tôi cần phải lọc trên a, bctrong một bảng dữ liệu, tôi sẽ đặt chúng lại với nhau trong một []với AND. Hôm qua, tôi nhận thấy rằng trong trường hợp cụ thể của mình, điều này cực kỳ chậm và đã thử nghiệm các bộ lọc xích thay thế. Tôi đã bao gồm một ví dụ dưới đây.

Đầu tiên, tôi chọn trình tạo số ngẫu nhiên, tải và tạo một tập dữ liệu giả.

# Set RNG seed
set.seed(-1)

# Load libraries
library(data.table)

# Create data table
dt <- data.table(a = sample(1:1000, 1e7, replace = TRUE),
                 b = sample(1:1000, 1e7, replace = TRUE),
                 c = sample(1:1000, 1e7, replace = TRUE),
                 d = runif(1e7))

Tiếp theo, tôi xác định phương pháp của mình. Cách tiếp cận đầu tiên chuỗi các bộ lọc với nhau. AND thứ hai các bộ lọc với nhau.

# Chaining method
chain_filter <- function(){
  dt[a %between% c(1, 10)
     ][b %between% c(100, 110)
       ][c %between% c(750, 760)]
}

# Anding method
and_filter <- function(){
  dt[a %between% c(1, 10) & b %between% c(100, 110) & c %between% c(750, 760)]
}

Ở đây, tôi kiểm tra họ cho kết quả tương tự.

# Check both give same result
identical(chain_filter(), and_filter())
#> [1] TRUE

Cuối cùng, tôi điểm chuẩn chúng.

# Benchmark
microbenchmark::microbenchmark(chain_filter(), and_filter())
#> Unit: milliseconds
#>            expr      min        lq      mean    median        uq       max
#>  chain_filter() 25.17734  31.24489  39.44092  37.53919  43.51588  78.12492
#>    and_filter() 92.66411 112.06136 130.92834 127.64009 149.17320 206.61777
#>  neval cld
#>    100  a 
#>    100   b

Được tạo vào ngày 2019-10-25 bởi gói reprex (v0.3.0)

Trong trường hợp này, chuỗi làm giảm thời gian chạy khoảng 70%. Tại sao điều này là trường hợp? Ý tôi là, những gì đang diễn ra dưới mui xe trong bảng dữ liệu? Tôi chưa thấy bất kỳ cảnh báo nào chống lại việc sử dụng &, vì vậy tôi đã ngạc nhiên rằng sự khác biệt quá lớn. Trong cả hai trường hợp, họ đánh giá các điều kiện giống nhau, do đó không nên là một sự khác biệt. Trong trường hợp AND, &là một toán tử nhanh và sau đó nó chỉ phải lọc bảng dữ liệu một lần (nghĩa là sử dụng vectơ logic kết quả từ AND), trái ngược với việc lọc ba lần trong trường hợp chuỗi.

Câu hỏi thưởng

Liệu nguyên tắc này giữ cho các hoạt động bảng dữ liệu nói chung? Là nhiệm vụ mô đun hóa luôn luôn là một chiến lược tốt hơn?


1
Tôi quan sát điều này, đã tự hỏi tương tự. Theo kinh nghiệm của tôi, việc chọn tốc độ chuỗi được quan sát trong các hoạt động chung.
JDG

9
trong khi data.tavle thực hiện một số tối ưu hóa cho các trường hợp như thế này (riêng điều này là một kỳ công và là một cải tiến lớn so với cơ sở R!), nói chung A & B & C & D sẽ đánh giá tất cả N điều kiện logic trước khi kết hợp kết quả và lọc . trong khi với chuỗi các cuộc gọi logic thứ 3 và 4 chỉ được đánh giá n lần (trong đó n <= N là số hàng còn lại sau mỗi điều kiện)
MichaelChirico

@MichaelChirico WOW. Thật đáng ngạc nhiên! Tôi không biết tại sao, nhưng tôi chỉ cho rằng nó sẽ hoạt động như ngắn mạch C ++
duckmayr

Theo dõi bình luận của @ MichaelChirico, bạn có thể thực hiện một basequan sát tương tự với các vectơ bằng cách thực hiện như sau: chain_vec <- function() { x <- which(a < .001); x[which(b[x] > .999)] }and_vec <- function() { which(a < .001 & b > .999) }. (trong đó ablà các vectơ có cùng độ dài từ runif- Tôi đã sử dụng n = 1e7cho các điểm cắt này).
ClancyStats

@MichaelChirico À, tôi hiểu rồi. Vì vậy, sự khác biệt lớn là trong mỗi bước của chuỗi, bảng dữ liệu nhỏ hơn đáng kể và do đó nhanh hơn để đánh giá điều kiện và bộ lọc trên? Điều đó có ý nghĩa. Cảm ơn những hiểu biết của bạn!
Lyngbakr

Câu trả lời:


8

Hầu hết, câu trả lời đã được đưa ra trong các bình luận: "phương pháp xâu chuỗi" data.tabletrong trường hợp này nhanh hơn trong "phương pháp anding" khi chuỗi này chạy các điều kiện lần lượt. Vì mỗi bước làm giảm kích thước của nên data.tablecó ít hơn để đánh giá cho bước tiếp theo. "Anding" đánh giá các điều kiện cho dữ liệu kích thước đầy đủ mỗi lần.

Chúng ta có thể chứng minh điều này bằng một ví dụ: khi các bước riêng lẻ KHÔNG làm giảm kích thước của data.table(tức là các điều kiện để kiểm tra là giống nhau cho cả hai thẩm định):

chain_filter <- function(){
  dt[a %between% c(1, 1000) # runs evaluation but does not filter out cases
     ][b %between% c(1, 1000)
       ][c %between% c(750, 760)]
}

# Anding method
and_filter <- function(){
  dt[a %between% c(1, 1000) & b %between% c(1, 1000) & c %between% c(750, 760)]
}

Sử dụng cùng một dữ liệu nhưng benchgói sẽ tự động kiểm tra nếu kết quả giống hệt nhau:

res <- bench::mark(
  chain = chain_filter(),
  and = and_filter()
)
summary(res)
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 chain         299ms    307ms      3.26     691MB     9.78
#> 2 and           123ms    142ms      7.18     231MB     5.39
summary(res, relative = TRUE)
#> # A tibble: 2 x 6
#>   expression   min median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <dbl>  <dbl>     <dbl>     <dbl>    <dbl>
#> 1 chain       2.43   2.16      1         2.99     1.82
#> 2 and         1      1         2.20      1        1

Như bạn có thể thấy ở đây, cách tiếp cận anding nhanh hơn 2,43 lần trong trường hợp này . Điều đó có nghĩa là chuỗi thực sự thêm một số chi phí , cho thấy rằng thường thì nên nhanh hơn. NGOẠI TRỪ nếu các điều kiện đang giảm kích thước củadata.table từng bước. Về mặt lý thuyết, cách tiếp cận chuỗi thậm chí có thể chậm hơn (thậm chí bỏ phần trên cao sang một bên), cụ thể là nếu một điều kiện sẽ làm tăng kích thước của dữ liệu. Nhưng thực tế tôi nghĩ rằng điều đó là không thể vì việc tái chế các vectơ logic không được phép thực hiện data.table. Tôi nghĩ rằng điều này trả lời câu hỏi tiền thưởng của bạn.

Để so sánh, các chức năng ban đầu trên máy của tôi với bench:

res <- bench::mark(
  chain = chain_filter_original(),
  and = and_filter_original()
)
summary(res)
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 chain        29.6ms   30.2ms     28.5     79.5MB     7.60
#> 2 and         125.5ms  136.7ms      7.32   228.9MB     7.32
summary(res, relative = TRUE)
#> # A tibble: 2 x 6
#>   expression   min median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <dbl>  <dbl>     <dbl>     <dbl>    <dbl>
#> 1 chain       1      1         3.89      1        1.04
#> 2 and         4.25   4.52      1         2.88     1
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.