Цикл for в R может быть очень медленным, если он применяется в чистом виде, без оптимизации, особенно когда приходится иметь дело с большими наборами данных. Есть ряд способов сделать ваш код быстрее, и вы, вероятно, будете удивлены, узнав насколько.

Эта статья описывает несколько подходов, в том числе простые изменения в логике, параллельную обработку и Rcpp, увеличивая скорость на несколько порядков, так что можно будет нормально обрабатывать 100 миллионов строк данных или даже больше.

Давайте попробуем ускорить код с циклом for и условным оператором (if-else) для создания колонки, которая добавляется к набору данных (data frame, df). Код ниже создает этот начальный набор данных.
# Создание набора данных
col1 <- runif (12^5, 0, 2)
col2 <- rnorm (12^5, 0, 2)
col3 <- rpois (12^5, 3)
col4 <- rchisq (12^5, 2)
df <- data.frame (col1, col2, col3, col4)

В первой части: векторизация, только истинные условия, ifelse.
В этой части: which, apply, побайтовая компиляция, Rcpp, data.table, результаты.


Использование which()


Используя команду which() для выбора строк, можно достичь одной третьей скорости Rcpp.
# Спасибо Гейб Бекер
system.time({
  want = which(rowSums(df) > 4)
  output = rep("less than 4", times = nrow(df))
  output[want] = "greater than 4"
}) 

# количество строк = 3 миллиона (примерно)
   user  system elapsed 
  0.396   0.074   0.481 


Используйте семейство функций apply вместо циклов for


Используем функцию apply() для реализации этой же логики и сравним с векторизированным циклом for. Результаты растут с увеличением количества порядков, но они медленнее, чем ifelse() и версии, где проверка делалась за пределами цикла. Это может быть полезно, но возможно, потребуется определенная изобретательность для сложной бизнес-логики.
# семейство apply
system.time({
  myfunc <- function(x) {
    if ((x['col1'] + x['col2'] + x['col3'] + x['col4']) > 4) {
      "greater_than_4"
    } else {
      "lesser_than_4"
    }
  }
  output <- apply(df[, c(1:4)], 1, FUN=myfunc)  # применить 'myfunc' к каждой строке
  df$output <- output
})


Использование apply и цикла for в R

Используйте побайтовые компиляции для функций cmpfun() из пакета compiler вместо собственно функции


Это, наверное, не лучший пример для иллюстрации эффективности побайтовой компиляции, поскольку полученное время немного выше, чем обычная форма. Однако, для более сложных функций побайтовая компиляция доказала свою эффективность. Думаю, стоит попробовать при случае.
# побайтовая компиляция кода
library(compiler)
myFuncCmp <- cmpfun(myfunc)
system.time({
  output <- apply(df[, c (1:4)], 1, FUN=myFuncCmp)
})


Apply, цикл for и побайтовая компиляция кода

Используйте Rcpp


Давайте выйдем на новый уровень. До этого мы увеличивали скорость и производительность с помощью различных стратегий и обнаружили, что использование ifelse() наиболее эффективно. Что если мы добавим еще один ноль? Ниже мы реализуем эту же логику с Rcpp, с набором данных в 100 миллионов строк. Мы сравним скорости Rcpp и ifelse().
library(Rcpp)
sourceCpp("MyFunc.cpp")
system.time (output <- myFunc(df)) # функция Rcpp ниже

Ниже приведена та же логика, реализованная на С++ с помощью пакета Rcpp. Сохраните код ниже как «MyFunc.cpp» в вашей рабочей директории сессии R (или вам придется применять sourceCpp, используя полный путь). Обратите внимание, комментарий // [[Rcpp::export]] обязателен, и его нужно поместить непосредственно перед функцией, которую вы хотите выполнить из R.
// Источник для MyFunc.cpp
#include 
using namespace Rcpp;
// [[Rcpp::export]]
CharacterVector myFunc(DataFrame x) {
  NumericVector col1 = as(x["col1"]);
  NumericVector col2 = as(x["col2"]);
  NumericVector col3 = as(x["col3"]);
  NumericVector col4 = as(x["col4"]);
  int n = col1.size();
  CharacterVector out(n);
  for (int i=0; i 4){
      out[i] = "greater_than_4";
    } else {
      out[i] = "lesser_than_4";
    }
  }
  return out;
}


Производительность Rcpp и ifelse

Используйте параллельную обработку, если у вас многоядерный компьютер


Параллельная обработка:
# параллельная обработка
library(foreach)
library(doSNOW)
cl <- makeCluster(4, type="SOCK") # for 4 cores machine
registerDoSNOW (cl)
condition <- (df$col1 + df$col2 + df$col3 + df$col4) > 4
# параллелизация с векторизацией
system.time({
  output <- foreach(i = 1:nrow(df), .combine=c) %dopar% {
    if (condition[i]) {
      return("greater_than_4")
    } else {
      return("lesser_than_4")
    }
  }
})

df$output <- output


Удаляйте переменные и очищайте память как можно раньше


Удаляйте больше ненужные объекты в коде с помощью rm(), как можно раньше, особенно перед длинными циклами. Иногда может помочь применение gc() в конце каждой итерации цикла.

Используйте структуры данных, занимающие меньше памяти


Data.table() — отличный пример, поскольку не перегружает память. Это позволяет ускорять операции, подобные объединению данных.
dt <- data.table(df)  # создать data.table
system.time({
  for (i in 1:nrow (dt)) {
    if ((dt[i, col1] + dt[i, col2] + dt[i, col3] + dt[i, col4]) > 4) {
      dt[i, col5:="greater_than_4"]  # присвоить значение в 5-й колонке
    } else {
      dt[i, col5:="lesser_than_4"]  # присвоить значение в 5-й колонке
    }
  }
})


Dataframe и data.table

Скорость: результаты


Метод: Скорость, количество строк в df / затраченное время = n строк в секунду
Исходный: 1X, 120000/140.15 = 856.2255 строк в секунду (нормализировано к 1)
Векторизированный: 738X, 120000/0.19 = 631578.9 строк в секунду
Только истинные условия: 1002X, 120000/0.14 = 857142.9 строк в секунду
ifelse: 1752X, 1200000/0.78 = 1500000 строк в секунду
which: 8806X, 2985984/0.396 = 7540364 строк в секунду
Rcpp: 13476X, 1200000/0.09 = 11538462 строк в секунду

Числа, приведенные выше, приблизительны и основаны на случайных запусках. Нет расчетов результатов для data.table(), побайтовой компиляции кода и параллелизации, поскольку они будут сильно отличаться в каждом конкретном случае и в зависимости от того, как вы их применяете.

Комментарии (0)