24 ноября 2012

Векторизованные вычисления в R с использованием apply-функций



Характерной особенностью R является векторизация вычислений. Векторизация представляет собой один из способов выполнения параллельных вычислений, при котором программа  определенным образом модифицируется для выполнения нескольких однотипных операций одновременно. Очевидно, что такой подход потенциально может привести к значительному ускорению однотипных вычислений над большими массивами данных.

Рассмотрим простейший пример векторизованных вычислений в R. Допустим, у нас имеется вектор из 10 положительных чисел и мы хотим извлечь квадратный корень из каждого из них. Вместо написания цикла для поочередного выполнения этой операции над каждым элементом вектора, мы просто подаем весь этот вектор на функцию sqrt(), которая возвращает вектор с результатами вычислений:

x <- 1:10
 
sqrt(x)
[1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751
[8] 2.828427 3.000000 3.162278

Так же легко мы можем, например, поэлементно умножить исходный вектор на вектор с результатами вычисления квадратного корня:

x*sqrt(x)
[1]  1.000000  2.828427  5.196152  8.000000 11.180340 14.696938
[7] 18.520259 22.627417 27.000000 31.622777

Принцип векторизованных вычислений применим не только к векторам как таковым, но и к более сложным объектам R - матрицам, спискам и таблицам данных (для R разницы между последними двумя типами объектов не существует - фактически таблица данных является списком из нескольких компонентов-векторов одинакового размера). В базовой комплектации R имеется целое семейство функций, предназначенных для организации векторизованных вычислений над такими объектами. В названии всех этих функций имеется слово apply (англ. применить), которому предшествует буква, указывающая на принцип работы той или иной функции (см. подробнее в справочном файле - ?apply). В ответе на один из вопросов, опубликованных на stackoverflow.com (сайт, который я постоянно использую для поиска информации по R) был дан замечательный обзор apply-функций с примерами их использования. Ниже я привожу перевод этого сообщения (с некоторыми изменениями и дополнениями).

1. Функция apply() - используется в случаях, когда необходимо применить какую-либо функцию ко всем строкам или столбцам матрицы (или массивов большей размерности):

# Создадим обычную двумерную матрицу:
M <- matrix(seq(1,16), 4, 4)
 
# Найдем минимальные значения в каждой строке матрицы
apply(M, 1, min)
[1] 1 2 3 4
 
# Найдем минимальные значения в каждом столбце матрицы
apply(M, 2, max)
[1]  4  8 12 16
 
# Пример с трехмерным массивом:
M <- array(seq(32), dim = c(4,4,2))
 
# Применим функцию sum() (суммирование) к кадому элементу M[*, , ],
# т.е. выполним суммирование по измерениями 2 и 3:
apply(M, 1, sum)
 
# Результат - одномерный вектор:
[1] 120 128 136 144
 
# Применим функцию sum() (суммирование) к кадому элементу  M[*, *, ],
#  - т.е. выполним суммирование по третьему измерению:
apply(M, c(1,2), sum)
 
# Результат - матрица:
     [,1] [,2] [,3] [,4]
[1,]   18   26   34   42
[2,]   20   28   36   44
[3,]   22   30   38   46
[4,]   24   32   40   48

При необходимости вычисления сумм и средних значений по строкам или столбцам матриц рекомендуется также использовать очень быстрые и специально оптимизированные для этого функции colSums(), rowSums(), colMeans и rowMeans().


2. Функция lapply() - используется в случаях, когда необходимо применить какую-либо функцию к каждому компоненту списка и получить результат также в виде списка (буква "l" в названии lapply означает list - "список").

# Создадим список с тремя компонентами-векторами:
x <- list(a = 1, b = 1:3, c = 10:100) 
 
# Установим размер каждого компонента из списка х (функция length()):
lapply(x, FUN = length) 
$a 
[1] 1
$b 
[1] 3
$c 
[1] 91
 
# Выполним суммирование всех элементов в каждом компоненте списка х:
lapply(x, FUN = sum) 
$a 
[1] 1
$b 
[1] 6
$c 
[1] 5005


3. Функция sapply()используется в случаях, когда необходимо применить какую-либо функцию к каждому компоненту списка, но результат вывести в виде вектора (буква "s" в названии sapply означает simplify - "упрощать").

# Список из трех компонентов:
x <- list(a = 1, b = 1:3, c = 10:100)
 
# Выясним размер каждого компонента списка х:
sapply(x, FUN = length)
a  b  c   
1  3 91 # результат возвращен в виде вектора
 
# Суммирование всех элементов в каждом компоненте списка х:
sapply(x, FUN = sum)   
a    b    c    
1    6 5005 # результат возвращен в виде вектора

В некоторых более "продвинутых" случаях sapply() может выдать результат в виде многомерного массива. Например, если применяемая нами функция возвращает векторы одинаковой длины, sapply() объединит эти векторы в матрицу (по столбцам):

sapply(1:5,function(x) rnorm(3,x))
         [,1]     [,2]     [,3]     [,4]     [,5]
[1,] 1.699876 1.898511 2.655672 2.908397 3.841733
[2,] 1.309984 3.279127 2.720922 4.794944 4.723460
[3,] 1.755939 2.780790 2.781063 4.193394 4.220807

Если применяемая нами функция возвращает матрицу, то sapply() преобразует каждую матрицу в вектор и объединит такие векторы в одну большую матрицу (звучит не очень понятно, но пример хорошо поясняет эту идею):

sapply(1:5,function(x) matrix(x,2,2))
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    1    2    3    4    5
[3,]    1    2    3    4    5
[4,]    1    2    3    4    5

Поведение sapply(), продемонстрированное в последнем примере, можно отменить при помощи аргумента simplify = "array" - в этом случае матрицы будут объединены в один многомерный массив:

sapply(1:5,function(x) matrix(x,2,2), simplify = "array")
, , 1
 
     [,1] [,2]
[1,]    1    1
[2,]    1    1
 
, , 2
 
     [,1] [,2]
[1,]    2    2
[2,]    2    2
 
, , 3
 
     [,1] [,2]
[1,]    3    3
[2,]    3    3
 
, , 4
 
     [,1] [,2]
[1,]    4    4
[2,]    4    4
 
, , 5
 
     [,1] [,2]
[1,]    5    5
[2,]    5    5


4. Функция vapply() - схожа с sapply(), но работает несколько быстрее за счет того, что пользователь однозначно указывает тип возвращаемых значений (буква "v" в названии vapply означает velocity - "скорость"; пример сравнения sapply() и vapply() можно найти здесь). Такой подход позволяет также избегать сообщений об ошибках (и прерывания вычислений), возникающих при работе с sapply() в некоторых ситуациях.

При вызове vapply() пользователь должен привести пример ожидаемого типа возвращаемых значений. Для этого служит аргумент FUN.VALUE:

# Аргументу FUN.VALUE присвоено логическое значение FALSE.
# Этим задается тип возвращаемых функцией значений,
# который ожидает пользователь
 
a <- vapply(NULL, is.factor, FUN.VALUE = FALSE)
 
# Функция sapply() применена к тому же объекту нулевого размера:
 
b <- sapply(NULL, is.factor)
 
# Проверка типа переменных:
is.logical(a)
[1] TRUE
 
is.logical(b)
[1] FALSE


5. Функция mapply() - используется в случаях, когда необходимо поэлементно применить какую-либо функцию одновременно к нескольким объектам (например, получить сумму первых элементов векторов, затему сумму вторых элементов векторов, и т.д.). Результат возвращается в виде вектора или массива другой размерности (см. примеры для sapply() выше). Буква "m" в названии mapply означает multivariate - "многомерный" (имеется в виду одновременное выполнение вычислений над элементами нескольких объектов).

mapply(sum, 1:5, 1:5, 1:5) 
[1]  3  6  9 12 15

6. Функция rapply() - используется в случаях, когда необходимо применить какую-либо функцию к компонентам вложенного списка (буква "r" в названии rapply означает recursively - "рекурсивно").

# Пользовательская функция, которая добавляет ! к элементу
# объекта x если этот элемент является текстовым выражением
# или добавляет 1 если этот элемент является числом:
 
myFun <- function(x){
    if (is.character(x)){
    return(paste(x,"!",sep=""))
    }
    else{
    return(x + 1)
    }
}
 
 
# Пример вложенного списка:
l <- list(a = list(a1 = "Boo", b1 = 2, c1 = "Eeek"), 
          b = 3, c = "Yikes", 
          d = list(a2 = 1, b2 = list(a3 = "Hey", b3 = 5)))
 
l
$a
$a$a1
[1] "Boo"
 
$a$b1
[1] 2
 
$a$c1
[1] "Eeek"
 
 
$b
[1] 3
 
$c
[1] "Yikes"
 
$d
$d$a2
[1] 1
 
$d$b2
$d$b2$a3
[1] "Hey"
 
$d$b2$b3
[1] 5
 
 
# Рекурсивное применение функции myFun к списку l:
rapply(l,myFun)
    a.a1     a.b1     a.c1        b        c     d.a2  d.b2.a3  d.b2.b3 
  "Boo!"      "3"  "Eeek!"      "4" "Yikes!"      "2"   "Hey!"      "6"
 
# Если необходимо вернуть результат в виде вложенного
# списка, можно воспользоваться аргументом how = "replace".
# В этом случае исходные значения в списке l будут
# заменены на новые:
rapply(l, myFun, how = "replace")
$a
$a$a1
[1] "Boo!"
 
$a$b1
[1] 3
 
$a$c1
[1] "Eeek!"
 
 
$b
[1] 4
 
$c
[1] "Yikes!"
 
$d
$d$a2
[1] 2
 
$d$b2
$d$b2$a3
[1] "Hey!"
 
$d$b2$b3
[1] 6


7. Функция tapply()используется в случаях, когда необходимо применить какую-либо функцию к отдельным группам элементов вектора, заданным в соответствии с уровнями какого-либо фактора.

Примеры использования tapply() я приводил ранее в сообщении, посвященном расчету параметров описательной статистики.



7 комментариев :

Дмитрий Колодезев комментирует...

Особенно они удобны тем, что существуют их многопроцессорные варианты - mclapply, например, из пакета parallel

Артём Клевцов комментирует...

Только для mapply и lapply.

Артём Клевцов комментирует...
Этот комментарий был удален автором.
Артём Клевцов комментирует...

В свете изложенного очень интересным выглядит пакет plyr, который в некоторых случаях может быть более удобным и эффективным. А также в нём содержатся уникальные функции (например, each).

Сергей Мастицкий комментирует...

Артём, plyr - весьма мощная вещь, без сомнений (как и все, что делает Hadley Wickham). В этом сообщении идея была обсудить стандартные функции. О plyr'е я собираюсь писать отдельно. Спасибо за коментарий, в любом случае!

Артём Клевцов комментирует...

В черновом варианте моего поста как раз содержалось предложение сделать обзор по этому пакету. В последнее время всё чаще им пользуюсь.

Анатолий Вершинин комментирует...

Спасибо автору, написавшему пост, и Провиденью, что вывело меня на него!

Отправить комментарий