Bir veri tablosunu filtrelerken ANDing üzerinden zincirlemenin performans avantajları


12

Benzer görevleri tek bir hatta toparlama alışkanlığım var. Örneğin, filtrelemem gerekirse a,b , ve cbir veri tablosunda, ben birinde onları biraraya koyacağım []Eleştiri ile. Dün, özel durumumda bunun inanılmaz derecede yavaş olduğunu ve bunun yerine zincirleme filtrelerini test ettiğini fark ettim. Aşağıda bir örnek ekledim.

İlk olarak, rastgele sayı üretecini , ve sahte bir veri kümesi oluşturuyorum.

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

Sonra, yöntemlerimi tanımlarım. İlk yaklaşım, filtreleri birlikte zincirler. İkincisi VE filtreleri birlikte.

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

Burada, aynı sonuçları verdiklerini kontrol ediyorum.

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

Sonunda, onları karşılaştırıyorum.

# 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

2019-10-25 tarihinde tarafından oluşturuldu reprex paketi (v0.3.0)

Bu durumda zincirleme, çalışma süresini yaklaşık% 70 oranında azaltır. Neden böyle? Yani, veri tablosunda kaputun altında neler oluyor? Kullanmaya karşı herhangi bir uyarı görmedim& , bu yüzden farkın çok büyük olduğuna şaşırdım. Her iki durumda da aynı koşulları değerlendirirler, bu yüzden bir fark olmamalıdır. AND durumunda, &hızlı bir operatördür ve daha sonra, zincirleme durumunda üç kez filtrelemenin aksine, veri tablosunu sadece bir kez filtrelemek zorundadır (yani, AND'lerden kaynaklanan mantıksal vektörü kullanarak).

Bonus soru

Bu ilke genel olarak veri tablosu işlemleri için geçerli midir? Modülerleştirme görevleri her zaman daha iyi bir strateji midir?


1
Bu gözlemin aynısını merak ettim. Deneyimlerime göre, genel operasyonlarda zincirleme hızının toplanması gözlenmektedir.
JDG

9
data.tavle bu gibi durumlar için bazı optimizasyonlar yaparken (bu tek başına bir başarı ve baz R'ye kıyasla büyük bir gelişme!), genel olarak A & B & C & D , sonuçları birleştirmeden ve filtrelemeden önce tüm N mantık koşullarını değerlendirecektir. . zincirleme ile 2. ve 4. mantıksal çağrılar sadece n kez değerlendirilir (burada n <= N her koşuldan sonra kalan satır sayısıdır)
MichaelChirico

@MichaelChirico WOW. Bu şaşırtıcı! Neden bilmiyorum, ama sadece C ++ kısa devre gibi çalışacağını varsaydım
duckmayr

@ MichaelChirico'nun yorumunu takip ederek base, aşağıdakileri yaparak vektörlerle benzer bir gözlem yapabilirsiniz : chain_vec <- function() { x <- which(a < .001); x[which(b[x] > .999)] }ve and_vec <- function() { which(a < .001 & b > .999) }. (nerede ave baynı uzunlukta vektörler runif- Ben n = 1e7bu kesikler için kullanılır ).
ClancyStats

@MichaelChirico Ah, anlıyorum. Yani, büyük fark, zincirin her adımında, veri tablosunun büyük ölçüde daha küçük olması ve bu nedenle durumu ve filtreyi değerlendirmek için daha hızlı olmasıdır. Mantıklı. Görüşleriniz için teşekkürler!
Lyngbakr

Yanıtlar:


8

Çoğunlukla, cevap her zaman yorumlarda verilmiştir: için "zincirleme yöntemi" data.tablezincirleme koşulları birbiri ardına yürüttüğü için "anding yönteminden" daha hızlıdır. Her adımın boyutunu küçültürken data.table, bir sonraki adım için değerlendirilecek daha az şey vardır. "Anding" her seferinde tam boyutlu verilerin koşullarını değerlendirir.

Bunu bir örnekle gösterebiliriz: ayrı adımlar adımların boyutunu küçültmediğinde data.table(yani kontrol koşulları her iki değerlendirme için de aynıdır):

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

Aynı verileri kullanarak, ancak benchsonuçların aynı olup olmadığını otomatik olarak kontrol eden paketi kullanarak :

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

Burada da görebileceğiniz gibi anding yaklaşımı bu durumda 2,43 kat daha hızlı . Bu, zincirlemenin aslında biraz ek yük getirdiği anlamına gelir ve genellikle anding'in daha hızlı olması gerektiğini gösterir. Koşullardata.table adım adım boyutunu küçültüyorsa HARİÇ . Teorik olarak, zincirleme yaklaşımı daha yavaş olabilir (ek yükü bir kenara bıraksa bile), yani bir koşul verilerin boyutunu artırabilirse. Ama pratikte bunun mantıklı vektörlerin geri dönüşümüne izin verilmediği için mümkün olmadığını düşünüyorumdata.table . Bunun bonus sorunuza cevap verdiğini düşünüyorum.

Karşılaştırma için, makinemdeki orijinal işlevler 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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.