Переваги продуктивності роботи ланцюга над ANDing під час фільтрації таблиці даних


12

Мені властиво з’єднувати подібні завдання разом в один рядок. Наприклад, якщо мені потрібно буде фільтрувати на a, bі cв таблиці даних, я []з'єднаю їх в одне з 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))

Далі я визначаю свої методи. Перший підхід ланцюги фільтрує разом. Другий І фільтрує разом.

# 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 &- швидкий оператор, і тоді йому потрібно лише один раз відфільтрувати таблицю даних (тобто, використовуючи логічний вектор, отриманий від 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) }. (де aі bє вектори однакової довжини від runif- я використовував n = 1e7для цих обрізів).
ClancyStats

@MichaelChirico Ах, бачу. Отже, велика різниця полягає в тому, що на кожному кроці ланцюга таблиця даних суттєво менша і тому швидше оцінювати стан та фільтрувати? Що має сенс. Дякуємо за вашу думку!
Lyngbakr

Відповіді:


8

Переважно, відповідь була дана в коментарях алеади: "метод прив'язки" для data.tableцього в цьому випадку швидший, ніж "метод андинга", оскільки ланцюжок виконує умови одна за одною. Оскільки кожен крок зменшує розмір, data.tableто для наступного слід менше оцінювати. "Ендінг" кожен раз оцінює умови отримання даних про повний розмір.

Ми можемо продемонструвати це на прикладі: коли окремі кроки НЕ зменшують розмір 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

Як ви бачите, тут підхід анджінгу в цьому випадку у 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.