データテーブルをフィルタリングする場合のANDingを介したチェーンのパフォーマンス上の利点


12

私は同様のタスクを1行にまとめる癖があります。例えば、私は上のフィルタに必要がある場合はab、およびcデータテーブルに、私は1つにそれらを一緒に出してあげる[]論理積を持ちます。昨日、私は特定のケースではこれが信じられないほど遅く、代わりにチェイニングフィルターをテストしたことに気付きました。以下に例を示します。

まず、乱数ジェネレータをシードし、ロードして、ダミーデータセットを作成します。

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

次に、メソッドを定義します。最初のアプローチでは、フィルターを連鎖させます。2番目は、フィルターを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の場合、&は迅速な演算子であり、チェーンの場合は3回フィルタリングするのではなく、データテーブルを1回(つまり、ANDの結果の論理ベクトルを使用して)フィルタリングするだけで済みます。

ボーナス質問

この原則は、データテーブル操作全般に当てはまりますか?タスクのモジュール化は常により良い戦略ですか?


1
私はこの観察に同感です、同じことを疑問に思いました。私の経験では、チェーンのピックアップ速度は一般的な操作全体で観察されています。
JDG、

9
data.tavle は、このような場合にいくつかの最適化を行いますが(これだけで、ベースRに比べて大きな改善です)、一般に、A&B&C&Dは、結果を組み合わせてフィルタリングする前に、すべてのN論理条件時間を評価ます。一方、2番目、3番目、および4番目の論理呼び出しは連鎖して評価されるのはn回だけです(n <= Nは各条件の後に残っている行の数です)
MichaelChirico

@MichaelChiricoすごい。それは驚くべきことです!理由はわかりませんが、C ++のショートサーキットのように機能すると思いました
duckmayr '25 / 10/25

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この場合、「チェーン方式」は「AND方式」よりも高速です。チェーンは次々に条件を実行するためです。各ステップでのサイズ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.