Leistungsvorteile der Verkettung gegenüber ANDing beim Filtern einer Datentabelle

12

Ich habe die Angewohnheit, ähnliche Aufgaben in einer einzigen Zeile zusammenzufassen. Wenn ich zum Beispiel nach und in einer Datentabelle filtern amuss b, füge cich sie in einer []mit UNDs zusammen. Gestern habe ich festgestellt, dass dies in meinem speziellen Fall unglaublich langsam war und stattdessen Verkettungsfilter getestet. Ich habe unten ein Beispiel aufgenommen.

Zuerst ich den Zufallszahlengenerator, lade und erstelle einen Dummy-Datensatz.

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

Als nächstes definiere ich meine Methoden. Der erste Ansatz kettet Filter zusammen. Das zweite UND setzt die Filter zusammen.

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

Hier überprüfe ich, ob sie die gleichen Ergebnisse liefern.

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

Schließlich vergleiche ich sie.

# 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

Erstellt am 25.10.2019 vom reprex-Paket (v0.3.0)

In diesem Fall reduziert die Verkettung die Laufzeit um ca. 70%. Warum ist das so? Ich meine, was ist unter der Haube in der Datentabelle los? Ich habe keine Warnungen vor der Verwendung gesehen &, daher war ich überrascht, dass der Unterschied so groß ist. In beiden Fällen bewerten sie die gleichen Bedingungen, so dass dies kein Unterschied sein sollte. Im AND-Fall &handelt es sich um einen Schnelloperator, der die Datentabelle nur einmal filtern muss (dh unter Verwendung des aus den ANDs resultierenden logischen Vektors), im Gegensatz zum dreimaligen Filtern im Verkettungsfall.

Bonus-Frage

Gilt dieses Prinzip für Datentabellenoperationen im Allgemeinen? Ist die Modularisierung von Aufgaben immer eine bessere Strategie?

Lyngbakr
quelle
1
Ich auch diese Beobachtung, habe mich das gleiche gefragt. Nach meiner Erfahrung wird die Beschleunigung der Verkettungsgeschwindigkeit im allgemeinen Betrieb beobachtet.
JDG
9
Während data.tavle für solche Fälle einige Optimierungen vornimmt (dies allein ist eine Leistung und eine große Verbesserung gegenüber Basis R!), bewertet A & B & C & D im Allgemeinen alle N logischen Bedingungszeiten, bevor die Ergebnisse kombiniert und gefiltert werden . Während beim Verketten der 2. 3. und 4. logische Aufrufe nur n-mal ausgewertet werden (wobei n <= N die Anzahl der nach jeder Bedingung verbleibenden Zeilen ist)
MichaelChirico
@ MichaelChirico WOW. Das ist überraschend! Ich weiß nicht warum, aber ich habe nur angenommen, dass es wie ein C ++ - Kurzschluss
funktionieren
Wenn Sie den Kommentar von @ MichaelChirico weiterverfolgen, können Sie eine ähnliche baseBeobachtung mit Vektoren machen, indem Sie Folgendes tun: chain_vec <- function() { x <- which(a < .001); x[which(b[x] > .999)] }und and_vec <- function() { which(a < .001 & b > .999) }. (wo aund bsind Vektoren gleicher Länge von runif- ich habe n = 1e7für diese Cutoffs verwendet).
ClancyStats
@ MichaelChirico Ah, ich verstehe. Der große Unterschied besteht also darin, dass in jedem Schritt der Kette die Datentabelle wesentlich kleiner ist und daher die Bedingung und der Filter schneller bewertet werden können. Das macht Sinn. Vielen Dank für Ihre Erkenntnisse!
Lyngbakr

Antworten:

8

Meistens wurde die Antwort bereits in den Kommentaren gegeben: Die "Verkettungsmethode" für data.tableist in diesem Fall schneller als die "Anding-Methode", da die Verkettung die Bedingungen nacheinander ausführt. Da jeder Schritt die Größe des Schrittes verringert data.table, muss für den nächsten weniger bewertet werden. "Anding" wertet jedes Mal die Bedingungen für die Daten in voller Größe aus.

Wir können dies anhand eines Beispiels demonstrieren: Wenn die einzelnen Schritte die Größe von NICHT verringern data.table(dh die zu überprüfenden Bedingungen sind für beide Ansätze gleich):

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

Verwenden Sie dieselben Daten, aber das benchPaket, das automatisch überprüft, ob die Ergebnisse identisch sind:

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

Wie Sie hier sehen können, ist der Anding-Ansatz in diesem Fall 2,43-mal schneller . Das bedeutet, dass das Verketten tatsächlich einen gewissen Overhead verursacht , was darauf hindeutet, dass Anding normalerweise schneller sein sollte. AUSSER wenn die Bedingungen die Größe desdata.table Schrittes Schritt für Schritt verringern . Theoretisch könnte der Verkettungsansatz sogar langsamer sein (selbst wenn der Overhead beiseite gelassen wird), nämlich wenn eine Bedingung die Größe der Daten erhöhen würde. Aber praktisch denke ich, dass dies nicht möglich ist, da das Recycling von logischen Vektoren nicht erlaubt ist data.table. Ich denke, das beantwortet Ihre Bonusfrage.

Zum Vergleich: Originalfunktionen auf meinem Computer mit 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
quelle