Преимущества повышения производительности по сравнению с ANDing при фильтрации таблицы данных

12

У меня есть привычка объединять похожие задачи в одну строку. Например, если мне нужно отфильтровать a, bи cв таблице данных, я положу их вместе в одном[] с Андами. Вчера я заметил, что в моем конкретном случае это было невероятно медленно, и вместо этого проверил фильтры цепочки. Я включил пример ниже.

Сначала я генератор случайных чисел, загружаю и создаю фиктивный набор данных.

# 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 по пакету представитель (v0.3.0)

В этом случае сцепление сокращает время выполнения примерно на 70%. Почему это так? Я имею в виду, что происходит под капотом в таблице данных? Я не видел никаких предупреждений против использования &, поэтому я был удивлен, что разница настолько велика. В обоих случаях они оценивают одни и те же условия, поэтому разницы не должно быть. В случае И,& это быстрый оператор, и тогда он должен фильтровать таблицу данных только один раз (т. Е. Используя логический вектор, полученный из AND), в отличие от фильтрации три раза в случае цепочки.

Бонусный вопрос

Действует ли этот принцип для операций с таблицами данных в целом? Всегда ли модульные задачи - лучшая стратегия?

Lyngbakr
источник
1
Я тоже это замечание удивляюсь тому же. По моему опыту, увеличение скорости цепочки наблюдается во всех основных операциях.
JDG
9
в то время как data.tavle выполняет некоторую оптимизацию для подобных случаев (это само по себе подвиг и большое улучшение по сравнению с базой R!), в целом A & B & C & D оценит все N логических условий, прежде чем объединять результаты и фильтровать , тогда как с 2 - й цепочкой 3 и 4 логических вызовов только оценивал п раз (где п <= 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

Главным образом, ответ был дан в комментариях aleady: «метод цепочки» для 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

Как вы можете видеть здесь, подход в этом случае быстрее в 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
JBGruber
источник