Manfaat kinerja dari rantai lebih dari ANDing saat memfilter tabel data

12

Saya memiliki kebiasaan menggabung tugas-tugas serupa menjadi satu baris. Sebagai contoh, jika saya perlu menyaring a, bdan cdalam tabel data, saya akan menempatkan mereka bersama-sama dalam satu []dengan ANDs. Kemarin, saya perhatikan bahwa dalam kasus khusus saya ini adalah filter chaining yang sangat lambat dan diuji. Saya telah memasukkan contoh di bawah ini.

Pertama, saya seed generator nomor acak, memuat , dan membuat kumpulan data dummy.

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

Selanjutnya, saya mendefinisikan metode saya. Rantai pendekatan pertama menyaring bersama. ANDs kedua menyaring bersama.

# 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)]
}

Di sini, saya periksa mereka memberikan hasil yang sama.

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

Akhirnya, saya membandingkannya.

# 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

Dibuat pada 2019-10-25 oleh paket reprex (v0.3.0)

Dalam hal ini, perangkaian mengurangi waktu lari sekitar 70%. Mengapa demikian? Maksudku, apa yang terjadi di bawah tenda data? Saya belum melihat adanya peringatan untuk tidak menggunakan &, jadi saya terkejut bahwa perbedaannya sangat besar. Dalam kedua kasus mereka mengevaluasi kondisi yang sama, sehingga seharusnya tidak menjadi perbedaan. Dalam kasus AND, &adalah operator cepat dan kemudian hanya perlu memfilter tabel data sekali (yaitu, menggunakan vektor logis yang dihasilkan dari ANDs), sebagai lawan untuk memfilter tiga kali dalam chaining case.

Pertanyaan bonus

Apakah prinsip ini berlaku untuk operasi tabel data secara umum? Apakah tugas modularising selalu merupakan strategi yang lebih baik?

Lyngbakr
sumber
1
Saya sudah observasi ini, bertanya-tanya sama. Dalam pengalaman saya, peningkatan kecepatan chaining diamati di seluruh operasi umum.
JDG
9
sementara data.tavle memang melakukan beberapa optimasi untuk kasus-kasus seperti ini (ini saja adalah suatu prestasi dan peningkatan yang hebat vs basis R!), secara umum A & B & C & D akan mengevaluasi semua kondisi N kali logis sebelum menggabungkan hasil dan penyaringan . sedangkan dengan chaining ke-2 dan ke-4 panggilan logis hanya dievaluasi n kali (di mana n <= N adalah jumlah baris yang tersisa setelah setiap kondisi)
MichaelChirico
@MichaelChirico WOW. Itu mengejutkan! Saya tidak tahu mengapa, tapi saya hanya berasumsi itu akan bekerja seperti hubungan pendek C ++
duckmayr
Menindaklanjuti komentar @ MichaelChirico, Anda dapat melakukan basepengamatan serupa dengan vektor dengan melakukan hal berikut: chain_vec <- function() { x <- which(a < .001); x[which(b[x] > .999)] }dan and_vec <- function() { which(a < .001 & b > .999) }. (Di mana adan bmerupakan vektor dengan panjang yang sama dari runif- saya gunakan n = 1e7untuk cutoff ini).
ClancyStats
@MichaelChirico Ah, begitu. Jadi, perbedaan besar adalah bahwa dalam setiap langkah rantai, tabel data secara substansial lebih kecil dan karenanya lebih cepat untuk mengevaluasi kondisi dan memfilter? Itu masuk akal. Terima kasih atas wawasan Anda!
Lyngbakr

Jawaban:

8

Sebagian besar, jawabannya diberikan dalam komentar aleady: "metode chaining" untuk data.tablelebih cepat dalam hal ini daripada "metode anding" karena chaining menjalankan kondisi satu demi satu. Karena setiap langkah mengurangi ukuran, data.tableada lebih sedikit untuk mengevaluasi untuk langkah selanjutnya. "Anding" mengevaluasi kondisi untuk data ukuran penuh setiap kali.

Kami dapat menunjukkan ini dengan sebuah contoh: ketika langkah-langkah individual TIDAK mengurangi ukuran data.table(yaitu kondisi untuk memeriksa adalah sama untuk kedua appraoches):

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

Menggunakan data yang sama tetapi benchpaket, yang secara otomatis memeriksa apakah hasilnya identik:

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

Seperti yang Anda lihat di sini pendekatan anding adalah 2,43 kali lebih cepat dalam kasus ini . Itu berarti chaining sebenarnya menambah beberapa overhead , menunjukkan bahwa biasanya anding harus lebih cepat. KECUALI jika kondisinya mengurangi ukurandata.table langkah demi langkah. Secara teoritis, pendekatan rantai bahkan bisa lebih lambat (bahkan menyisakan overhead), yaitu jika suatu kondisi akan meningkatkan ukuran data. Tetapi praktis saya pikir itu tidak mungkin karena daur ulang dari vektor logis tidak diperbolehkan dalam data.table. Saya pikir ini menjawab pertanyaan bonus Anda.

Sebagai perbandingan, fungsi asli pada mesin saya dengan 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
JBGruber
sumber