데이터 테이블을 필터링 할 때 AND에 대한 체인의 성능 이점


12

비슷한 작업을 한 줄에 모으는 습관이 있습니다. 내가 필터링해야하는 경우 예를 들어 a, bc데이터 테이블에, 나는 하나에 함께 넣어 것이다 []AND 연산과 함께. 어제, 나는 내 특별한 경우에 이것이 매우 느리고 체인 필터를 테스트 한 것으로 나타났습니다. 아래에 예제를 포함 시켰습니다.

먼저 난수 생성기를 시드하고 로드 하고 더미 데이터 세트를 만듭니다.

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

다음으로 메소드를 정의합니다. 첫 번째 접근 체인은 함께 필터링합니다. 두 번째는 필터를 함께 AND합니다.

# 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

reprex 패키지 (v0.3.0)로 2019-10-25에 작성

이 경우 체인을 사용하면 실행 시간이 약 70 % 단축됩니다. 왜 이런 경우입니까? 데이터 테이블에서 어떤 일이 벌어지고 있습니까? 사용에 대한 경고를 보지 못했기 &때문에 차이가 너무 크다는 것에 놀랐습니다. 두 경우 모두 동일한 조건을 평가하므로 차이가 없어야합니다. AND의 경우, &빠른 연산자이며 체인의 경우 세 번 필터링하는 것과 달리 데이터 테이블을 한 번만 필터링하면됩니다 (즉, AND에서 생성 된 논리 벡터 사용).

보너스 질문

이 원칙은 일반적으로 데이터 테이블 작업에 적용됩니까? 모듈화 작업이 항상 더 나은 전략입니까?


1
나는이 관찰을 똑같이 궁금해했다. 내 경험상 체인 속도 픽업은 일반적인 작업에서 관찰됩니다.
JDG

9
data.tavle은 이와 같은 경우에 대해 일부 최적화를 수행 하지만 (이것은 기본 R에 비해 위업이며 큰 개선입니다!) 일반적으로 A & B & C & D는 결과와 필터링을 결합하기 전에 모든 N 논리 조건 시간을 평가 합니다 . 2 3,4 논리 호출 체인 만 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) }. (여기서 ab같은 길이의 벡터 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 배 더 빠릅니다 . 즉, 체인은 실제로 약간의 오버 헤드를 추가 하여 일반적으로 anding이 더 빨라야 함을 나타냅니다. 조건이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.