Узнайте, как писать эффективный код R
R широко используется в бизнесе и науке в качестве инструмента анализа данных. Язык программирования является важным инструментом для задач, управляемых данными. Для многих статистиков и специалистов по данным R является первым выбором для статистических вопросов.
Специалисты по данным часто работают с большими объемами данных и сложными статистическими задачами. Память и время выполнения играют здесь центральную роль. Вам нужно написать эффективный код для достижения максимальной производительности. В этой статье мы представляем советы, которые вы можете использовать непосредственно в своем следующем проекте R.
Используйте профилирование кода
Специалисты по данным часто хотят оптимизировать свой код, чтобы сделать его быстрее. В некоторых случаях вы доверитесь своей интуиции и попробуете что-нибудь. Этот подход имеет тот недостаток, что вы, вероятно, оптимизируете неправильные части своего кода. Так вы зря потратите время и силы. Вы можете оптимизировать свой код только в том случае, если знаете, где он работает медленно. Решение — профилирование кода. Профилирование кода помогает найти медленные части кода!
Rprof() — встроенный инструмент для профилирования кода. К сожалению, Rprof() не очень удобен для пользователя, поэтому мы не рекомендуем его прямое использование. Мы рекомендуем пакет profvis. Profvis позволяет визуализировать данные профилирования кода из Rprof(). Вы можете установить пакет через консоль R с помощью следующей команды:
install.packages("profvis")
На следующем этапе мы делаем профилирование кода на примере.
library("profvis")
profvis({
y <- 0
for (i in 1:10000) {
y <- c(y,i)
}
})
Если вы запустите этот код в своем RStudio, вы получите следующий вывод.

Вверху вы можете увидеть свой код R с гистограммами для памяти и времени выполнения для каждой строки кода. Этот экран дает вам обзор возможных проблем в вашем коде, но не помогает определить точную причину. В столбце памяти можно увидеть, сколько памяти (в МБ) было выделено (полоска справа) и освобождено (полоска слева) для каждого вызова. Столбец времени показывает время выполнения (в мс) для каждой строки. Например, вы можете видеть, что строка 4 занимает 280 мс.
Внизу вы можете увидеть Flame Graph с полным стеком вызовов. Этот график дает вам обзор всей последовательности вызовов. Вы можете перемещать указатель мыши по отдельным вызовам, чтобы получить дополнительную информацию. Также заметно, что сборщик мусора (‹GC›) занимает много времени. Но почему? В столбце памяти вы можете увидеть в строке 4, что требования к памяти повышены. Много памяти выделяется и освобождается в строке 4. Каждая итерация создает еще одну копию y, что приводит к увеличению использования памяти. Пожалуйста, избегайте таких задач копирования-изменения!
Вы также можете использовать вкладку Данные. Вкладка Данные дает вам краткий обзор всех вызовов и особенно подходит для сложных вложенных вызовов.

Если вы хотите узнать больше о provis, вы можете посетить страницу Github.
Векторизуйте свой код
Возможно, вы слышали о векторизации. Но что это? Векторизация заключается не только в том, чтобы избежать циклов for(). Это делает еще один шаг вперед. Вы должны думать в терминах векторов, а не скаляров. Векторизация очень важна для ускорения кода R. Векторизованные функции используют циклы, написанные на C, а не на R. Циклы на C имеют меньше накладных расходов, что делает их намного быстрее. Векторизация означает поиск существующей функции R, реализованной в C, которая точно соответствует вашей задаче. Функции rowSums(), colSums(), rowMeans() и colMeans() удобны для ускорения кода R. Эти векторизованные матричные функции всегда быстрее, чем функция apply().
Для измерения времени выполнения мы используем пакет R microbenchmark. В этом пакете вычисления всех выражений выполняются на C, чтобы минимизировать накладные расходы. На выходе пакет предоставляет обзор статистических показателей. Вы можете установить пакет microbenchmark через консоль R с помощью следующей команды:
install.packages("microbenchmark")
Теперь сравним время выполнения функции apply() с функцией colMeans(). Следующий пример кода демонстрирует это.
install.packages("microbenchmark")
library("microbenchmark")
data.frame <- data.frame (a = 1:10000, b = rnorm(10000))
microbenchmark(times=100, unit="ms", apply(data.frame, 2, mean), colMeans(data.frame))
# example console output:
# Unit: milliseconds
# expr min lq mean median uq max neval
# apply(data.frame, 2, mean) 0.439540 0.5171600 0.5695391 0.5310695 0.6166295 0.884585 100
# colMeans(data.frame) 0.183741 0.1898915 0.2045514 0.1948790 0.2117390 0.287782 100
В обоих случаях мы вычисляем среднее значение каждого столбца фрейма данных. Для обеспечения достоверности результата делаем 100 прогонов (times=10) с помощью пакета microbenchmark. В итоге мы видим, что функция colMeans() работает примерно в три раза быстрее.
Мы рекомендуем онлайн-книгу R Advanced, если вы хотите узнать больше о векторизации.
Матрицы и фреймы данных
Матрицы имеют некоторое сходство с фреймами данных. Матрица — это двумерный объект. Кроме того, некоторые функции работают аналогичным образом. Отличие: все элементы матрицы должны иметь один и тот же тип. Матрицы часто используются для статистических расчетов. Например, функция lm() преобразует входные данные в матрицу. Затем подсчитываются результаты. В общем, матрицы быстрее, чем фреймы данных. Теперь мы рассмотрим различия во времени выполнения между матрицами и фреймами данных.
library("microbenchmark")
matrix = matrix (c(1, 2, 3, 4), nrow = 2, ncol = 2, byrow = 1)
data.frame <- data.frame (a = c(1, 3), b = c(2, 4))
microbenchmark(times=100, unit="ms", matrix[1,], data.frame[1,])
# example console output:
# Unit: milliseconds
# expr min lq mean median uq max neval
# matrix[1, ] 0.000499 0.0005750 0.00123873 0.0009255 0.001029 0.019359 100
# data.frame[1, ] 0.028408 0.0299015 0.03756505 0.0308530 0.032050 0.220701 100
Мы выполняем 100 прогонов с использованием пакета microbenchmark для получения значимой статистической оценки. Известно, что доступ матрицы к первой строке примерно в 30 раз быстрее, чем для фрейма данных. Впечатляет! Матрица значительно быстрее, поэтому ее следует предпочесть фрейму данных.
is.na() и anyNA
Вы, наверное, знаете функцию is.na() для проверки наличия в векторе пропущенных значений. Существует также функция anyNA() для проверки наличия в векторе пропущенных значений. Теперь мы проверяем, какая функция имеет более быстрое время выполнения.
library("microbenchmark")
x <- c(1, 2, NA, 4, 5, 6, 7)
microbenchmark(times=100, unit="ms", anyNA(x), any(is.na(x)))
# example console output:
# Unit: milliseconds
# expr min lq mean median uq max neval
# anyNA(x) 0.000145 0.000149 0.00017247 0.000155 0.000182 0.000895 100
# any(is.na(x)) 0.000349 0.000362 0.00063562 0.000386 0.000393 0.022684 100
Оценка показывает, что anyNA() в среднем значительно быстрее, чем is.na(). Вы должны использовать anyNA(), если это возможно.
if() … else() против ifelse()
if() ... else() — это стандартная функция потока управления, а ifelse() более удобна для пользователя.
Ifelse() работает по следующей схеме:
# test: condition, if_yes: condition true, if_no: condition false ifelse(test, if_yes, if_no)
С точки зрения многих программистов, ifelse() более понятна, чем многострочная альтернатива. Недостатком является то, что ifelse() не так эффективен в вычислительном отношении. Следующий тест показывает, что if() ... else() работает более чем в 20 раз быстрее.
library("microbenchmark")
if.func <- function(x){
for (i in 1:1000) {
if (x < 0) {
"negative"
} else {
"positive"
}
}
}
ifelse.func <- function(x){
for (i in 1:1000) {
ifelse(x < 0, "negative", "positive")
}
}
microbenchmark(times=100, unit="ms", if.func(7), ifelse.func(7))
# example console output:
# Unit: milliseconds
# expr min lq mean median uq max neval
# if.func(7) 0.020694 0.020992 0.05181552 0.021463 0.0218635 3.000396 100
# ifelse.func(7) 1.040493 1.080493 1.27615668 1.163353 1.2308815 7.754153 100
Вам следует избегать использования ifelse() в сложных циклах, так как это значительно замедляет вашу программу.
Параллельные вычисления
Большинство компьютеров имеют несколько процессорных ядер, что позволяет выполнять параллельные задачи. Эта концепция называется параллельными вычислениями. Пакет R parallel позволяет выполнять параллельные вычисления в приложениях R. Пакет предварительно установлен с базой R. С помощью следующих команд вы можете загрузить пакет и посмотреть, сколько ядер у вашего компьютера:
library("parallel")
no_of_cores = detectCores()
print(no_of_cores)
# example console output:
# [1] 8
Параллельная обработка данных идеально подходит для моделирования методом Монте-Карло. Каждое ядро независимо имитирует реализацию модели. В конце подводятся итоги. Следующий пример основан на онлайн-книге Эффективное программирование на R. Во-первых, нам нужно установить пакет devtools. С помощью этого пакета мы можем скачать эффективный пакет с GitHub. В консоли RStudio необходимо ввести следующие команды:
install.packages("devtools")
library("devtools")
devtools::install_github("csgillespie/efficient", args = "--with-keep.source")
В эффективном пакете есть функция snakes_ladders(), которая имитирует одиночную игру «Змеи и лестницы». Мы будем использовать моделирование для измерения времени выполнения функций sapply() и parSapply(). parSapply() — это распараллеленный вариант sapply().
library("parallel")
library("microbenchmark")
library("efficient")
N = 10^4
cl = makeCluster(4)
microbenchmark(times=100, unit="ms", sapply(1:N, snakes_ladders), parSapply(cl, 1:N, snakes_ladders))
stopCluster(cl)
# example console output:
# Unit: milliseconds
# expr min lq mean median uq max neval
# sapply(1:N, snakes_ladders) 3610.745 3794.694 4093.691 3957.686 4253.681 6405.910 100
# parSapply(cl, 1:N, snakes_ladders) 923.875 1028.075 1149.346 1096.950 1240.657 2140.989 100
Оценка показывает, что parSapply() симуляция вычисляет в среднем примерно в 3,5 раза быстрее, чем sapply() функция. Вау! Вы можете быстро интегрировать этот совет в свой существующий проект R.
Интерфейс R для других языков
Бывают случаи, когда R просто медленный. Вы используете всевозможные приемы, но ваш код R все еще слишком медленный. В этом случае вам следует подумать о переписывании кода на другом языке программирования. Для других языков в R есть интерфейсы в виде пакетов R. Примерами являются Rcpp и rJava. Писать код на C++ легко, особенно если у вас есть опыт разработки программного обеспечения. Затем вы можете использовать его в R.
Во-первых, вам нужно установить Rcpp с помощью следующей команды:
install.packages("Rcpp")
Следующий пример демонстрирует подход:
library("Rcpp")
cppFunction('
double sub_cpp(double x, double y) {
double value = x - y;
return value;
}
')
result <- sub_cpp(142.7, 42.7)
print(result)
# console output:
# [1] 100
C++ — это мощный язык программирования, который лучше всего подходит для ускорения кода. Для очень сложных вычислений мы рекомендуем использовать код C++.
Заключение
В этой статье мы узнали, как анализировать код R. Пакет provis поддерживает вас при анализе вашего кода R. Вы можете использовать векторизованные функции, такие как rowSums(), colSums(), rowMeans() и colMeans(), чтобы ускорить вашу программу. Кроме того, по возможности следует отдавать предпочтение матрицам вместо фреймов данных. Используйте anyNA() вместо is.na(), чтобы проверить, есть ли в векторе пропущенные значения. Вы ускорите свой код R, используя if() ... else() вместо ifelse(). Кроме того, вы можете использовать параллельные функции из пакета parallel для сложных симуляций. Вы можете добиться максимальной производительности для сложных участков кода, используя пакет Rcpp.
Есть несколько книг для изучения R. Вы найдете три книги, которые, по нашему мнению, очень хороши для изучения эффективного программирования R:
- Эффективное программирование на R: Практическое руководство по более разумному программированию*
- Практическое программирование на R: написание собственных функций и симуляций*
- R для науки о данных: импорт, упорядочивание, преобразование, визуализация и моделирование данных*
👉🏽 Используйте Рабочее пространство MLflow для своего следующего проекта по науке о данных. Это бесплатно!
👉🏽 Подпишитесь на нашу бесплатную еженедельную рассылку новостей Magic AI, чтобы быть в курсе последних обновлений AI!
Вам понравилась наша статья и вы хотите поддержать нас как автора? Станьте участником Medium, чтобы продолжать обучение без ограничений. Мы получим небольшую часть вашего членского взноса, когда вы воспользуетесь ссылкой. Никаких дополнительных затрат для вас нет.
Не пропустите наши новые новости:
Спасибо за прочтение. Если вам понравилась эта статья, не стесняйтесь поделиться ею. Следуйте за нами для получения дополнительной информации. Хорошего дня!
* Раскрытие информации: ссылки являются партнерскими ссылками, что означает, что мы будем получать комиссию, если вы совершаете покупку по этим ссылкам. Никаких дополнительных расходов для вас не требуется.