ประโยชน์ด้านประสิทธิภาพของการโยงกับ ANDing เมื่อกรองตารางข้อมูล


12

ฉันติดนิสัยที่คล้ายกันในการทำงานเป็นแถวเดียว ตัวอย่างเช่นถ้าฉันต้องกรองa, bและcในตารางข้อมูลผมจะนำพวกเขาเข้าด้วยกันในหนึ่ง[]กับ ANDs เมื่อวานนี้ฉันสังเกตเห็นว่าในกรณีของฉันนี่เป็นตัวกรองช้าและเหลือเชื่อแทนการทดสอบ ฉันได้รวมตัวอย่างด้านล่างแล้ว

ก่อนอื่นฉันจะสร้างตัวสร้างตัวเลขสุ่มโหลดและสร้างชุดข้อมูลจำลอง

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

ต่อไปฉันจะกำหนดวิธีการของฉัน วิธีแรกโซ่กรองด้วยกัน อันที่สอง ANDs ตัวกรองเข้าด้วยกัน

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

ที่นี่ฉันตรวจสอบพวกเขาให้ผลลัพธ์เดียวกัน

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

ในที่สุดฉันมาตรฐานพวกเขา

# 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 โดยแพ็คเกจ reprex (v0.3.0)

ในกรณีนี้การผูกมัดจะลดเวลาทำงานประมาณ 70% ทำไมเป็นกรณีนี้ ฉันหมายถึงสิ่งที่เกิดขึ้นภายใต้ประทุนในตารางข้อมูล? ฉันไม่ได้เห็นคำเตือนใด ๆ เกี่ยวกับการใช้&ดังนั้นฉันแปลกใจว่าความแตกต่างใหญ่มาก ในทั้งสองกรณีพวกเขาประเมินเงื่อนไขเดียวกันดังนั้นจึงไม่ควรแตกต่างกัน ในกรณี AND &เป็นตัวดำเนินการด่วนและจากนั้นจะต้องกรองตารางข้อมูลเพียงครั้งเดียว (เช่นการใช้เวกเตอร์แบบลอจิคัลที่เกิดจาก ANDs) ซึ่งตรงข้ามกับการกรองสามครั้งในกรณีการผูกมัด

คำถามโบนัส

หลักการนี้มีไว้สำหรับการทำงานของตารางข้อมูลโดยทั่วไปหรือไม่? ภารกิจการทำให้เป็นโมดูลเป็นกลยุทธ์ที่ดีกว่าเสมอหรือไม่?


1
ฉันก็เช่นเดียวกันการสังเกตนี้ได้สงสัยเหมือนกัน จากประสบการณ์ของฉันพบว่าการรับสายเร็วนั้นเกิดขึ้นจากการใช้งานทั่วไป
JDG

9
ในขณะที่ data.tavle ทำการเพิ่มประสิทธิภาพบางอย่างสำหรับกรณีเช่นนี้ (สิ่งนี้เป็นความสำเร็จและการปรับปรุงที่ดีเทียบกับฐาน R!) โดยทั่วไป A & B & C & D จะประเมินเงื่อนไขทางตรรกะN ทั้งหมดก่อนที่จะรวมผลลัพธ์และการกรอง . ขณะที่การโยงสายลอจิคัลที่ 2 และ 3 จะถูกประเมินเพียง n ครั้ง (โดยที่ n <= N คือจำนวนแถวที่เหลือหลังจากแต่ละเงื่อนไข)
MichaelChirico

@MichaelChirico WOW นั่นเป็นเรื่องน่าประหลาดใจ! ฉันไม่รู้ว่าทำไม แต่ฉันเพิ่งสันนิษฐานว่ามันจะทำงานเหมือนการลัดวงจร C ++
duckmayr

ติดตาม @ คิดเห็น MichaelChirico ของคุณสามารถทำให้คล้ายbaseสังเกตกับเวกเตอร์โดยการทำต่อไปนี้: และchain_vec <- function() { x <- which(a < .001); x[which(b[x] > .999)] } and_vec <- function() { which(a < .001 & b > .999) }(ที่ไหนaและbมีเวกเตอร์ที่มีความยาวเท่ากันจากrunif- ฉันใช้n = 1e7สำหรับการตัดยอดเหล่านี้)
ClancyStats

@MichaelChirico อ่าเข้าใจแล้ว ดังนั้นความแตกต่างที่สำคัญคือในแต่ละขั้นตอนของโซ่ตารางข้อมูลมีขนาดเล็กลงอย่างมากและรวดเร็วกว่าในการประเมินสภาพและตัวกรอง นั่นทำให้รู้สึก ขอบคุณสำหรับข้อมูลเชิงลึกของคุณ!
Lyngbakr

คำตอบ:


8

ส่วนใหญ่คำตอบที่ได้รับในความคิดเห็นอวดดี: "วิธีการผูกมัด" สำหรับdata.tableจะเร็วกว่าในกรณีนี้กว่า "วิธีการและวิธีการ" ในขณะที่การผูกมัดทำงานเงื่อนไขหนึ่งหลังจากที่อื่น ในขณะที่แต่ละขั้นตอนลดขนาดของที่data.tableมีน้อยกว่าที่จะประเมินสำหรับอีกขั้น "Anding" ประเมินเงื่อนไขสำหรับข้อมูลขนาดเต็มทุกครั้ง

เราสามารถสาธิตสิ่งนี้ด้วยตัวอย่าง: เมื่อแต่ละขั้นตอนไม่ลดขนาดของdata.table(เช่นเงื่อนไขในการตรวจสอบจะเหมือนกันสำหรับผู้ประเมินราคาทั้งสอง):

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

ใช้ข้อมูลเดียวกัน แต่benchแพ็คเกจซึ่งตรวจสอบโดยอัตโนมัติว่าผลลัพธ์เหมือนกันหรือไม่:

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

อย่างที่คุณเห็นนี่คือวิธี anding เร็วกว่า 2.43 เท่าในกรณีนี้ นั่นหมายถึงการผูกมัดเพิ่มค่าใช้จ่ายจริงแนะนำว่าปกติและควรจะเร็ว ยกเว้นในกรณีที่เงื่อนไขกำลังลดขนาดของdata.tableทีละขั้นตอน ในทางทฤษฎีวิธีการผูกมัดอาจจะช้ากว่า (แม้จะทิ้งเหนือหัวไว้) นั่นคือถ้าเงื่อนไขจะเพิ่มขนาดของข้อมูล data.tableแต่ในทางปฏิบัติผมคิดว่าเป็นไปไม่ได้ตั้งแต่การรีไซเคิลของเวกเตอร์ตรรกะไม่ได้รับอนุญาตใน ฉันคิดว่านี่เป็นคำตอบสำหรับคำถามโบนัสของคุณ

สำหรับการเปรียบเทียบฟังก์ชั่นดั้งเดิมบนเครื่องของฉันด้วย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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.