[Перевод] Использование apply, sapply, lapply в R

Это вводная статья об использовании apply, sapply и lapply, она лучше всего подходит для людей, которые недавно работают с R или незнакомы с этими функциями. Я приведу несколько примеров использования функций семейства apply, поскольку они часто применяются при работе в R.

Я сравнивал эти три метода на наборе данных. Была сгенерирована выборка, и они к ней применялись. Хотелось посмотреть, чем отличаются результаты их применения.

Также использовался тестовый стенд, который возвращал матрицу. В ней было три колонки и около 30 строк. Выглядело примерно так:

method1  method2    method3 
[1,] 0.05517714 0.014054038 0.017260447
[2,] 0.08367678 0.003570883 0.004289079
[3,] 0.05274706 0.028629661 0.071323030
[4,] 0.06769936 0.048446559 0.057432519
[5,] 0.06875188 0.019782518 0.080564474 
[6,] 0.04913779 0.100062929 0.102208706


Такие данные можно симулировать с помощью rnorm, чтобы создать три набора. Первый — со средним, равным 0, второй — со средним 2, третий — со средним 5, и 30 строк.

m <- matrix(data=cbind(rnorm(30, 0), rnorm(30, 2), rnorm(30, 5)), nrow=30, ncol=3)


Apply


Когда применять apply? Если у нас есть большой объем упорядоченных данных для обработки. Например, набор средних значений, в виде некоторой матрицы. Какие операции предполагается применять: для получения информации, возможно, преобразование, выделение подмножества, любые операции над данными.

Если вы используете блок данных (тип data frame), все данные должны иметь один и тот же тип, в противном случае будет применено преобразование. Это может оказаться именно тем, что нужно, а может и нет. Если в блоке данных есть строковые/буквенные и числовые данные, числовые данные будут приведены к строкам, и операции над числами могут выдавать не совсем ожидаемые результаты.

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

Во-первых, я хочу убедиться, что правильно создал матрицу с тремя колонками со средними 0, 2 и 5 соответственно. Мы используем apply и базовую функцию mean, чтобы убедиться в этом. Вторым аргументом мы указываем apply, к какому измерению применить функцию — колонкам или строкам. В данном случае в конце мы хотим получить три числа, поэтому укажем apply работать с колонками, передав 2 в качестве второго аргумента. Но давайте поступим неправильно для иллюстрации:

apply(m, 1, mean)

# [1] 2.408150 2.709325 1.718529 0.822519 2.693614 2.259044 1.849530 2.544685 2.957950 2.219874
#[11] 2.582011 2.471938 2.015625 2.101832 2.189781 2.319142 2.504821 2.203066 2.280550 2.401297
#[21] 2.312254 1.833903 1.900122 2.427002 2.426869 1.890895 2.515842 2.363085 3.049760 2.027570


Передавая 1 как второй аргумент, мы получаем 30 значений, среднее по каждой строке. Не три числа, которые мы хотели. Попробуем снова:

apply(m, 2, mean)

#[1] -0.02664418  1.95812458  4.86857792


Отлично. Как можно увидеть, среднее для каждого столбца — примерно 0, 2 и 5, как и ожидалось.

Своя собственная функция


Давайте представим, что увидев это отрицательное число, я понял, что хотел бы работать только с положительными. Давайте узнаем, сколько отрицательных чисел в каждой колонке, снова применив apply:

apply(m, 2, function(x) length(x[x<0]))

#[1] 14  1  0


Итак, 14 отрицательных чисел в первой колонке, одно во второй и ни одного в третьей. Более-менее ожидаемо для трех нормальных распределений с заданными выше средними и единичным стандартным отклонением.

Здесь мы использовали простую функцию, которая была определена непосредственно в вызове apply, а не какую-то встроенную. Обратите внимание, в функции мы не задали возвращаемое значение. Фактически функция использует разбиение на подмножества, чтобы выбрать все элементы х меньше 0, а потом подсчитать их с помощью length. Функция принимает один аргумент, который я произвольно обозначил через х. В данном случае х — одна из колонок матрицы. Это матрица из одной колонки или просто вектор? Давайте посмотрим:

apply(m, 2, function(x) is.matrix(x))

#[1] FALSE FALSE FALSE


Не матрица. Здесь определение функции не требуется, можно было просто передать функцию is.matrix, поскольку она принимает один аргумент и уже была создана. Давайте убедимся, что это векторы, как и ожидалось:

apply(m, 2, is.vector)

#[1] TRUE TRUE TRUE


Почему же тогда нужно было оборачивать в функцию length? Когда мы хотим определить свой собственный обработчик для apply, мы должны как минимум задать имя входной переменной, чтобы использовать его в функции:

apply(m, 2, length(x[x<0]))

#Error in match.fun(FUN) : object ‘x’ not found


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

apply(m, 2, function(x) mean(x[x>0]))

#[1] 0.4466368 2.0415736 4.8685779

Использование sapply и lapply


Эти две функции работают похожим образом, представляя набор данных как список или вектор и применяя заданную функцию к каждому элементу.

Иногда нам требуется что-то больше, чем линейное преобразование данных. Например, мы захотели бы сравнить текущее значение со значением пять отрезков времени назад. Возможно, стоит применить rollapply для этого, но быстрый, хотя и не совсем красивый способ — запустить sapply или lapply, передав набор индексированных значений.

Здесь мы применим sapply, который работает со списком или вектором данных.

sapply(1:3, function(x) x^2)

#[1] 1 4 9


lapply очень похожа, но возвращает список, а не вектор:

lapply(1:3, function(x) x^2)

#[[1]]
#[1] 1
#
#[[2]]
#[1] 4
#
#[[3]]
#[1] 9


Передав в sapply simplify=FALSE, также получите список:

sapply(1:3, function(x) x^2, simplify=F)

#[[1]]
#[1] 1
#
#[[2]]
#[1] 4
#
#[[3]]
#[1] 9


Также можно применить unlist с lapply, чтобы получить вектор.

unlist(lapply(1:3, function(x) x^2))

#[1] 1 4 9


Лучше всего использовать lapply и sapply, если это имеет смысл для ваших данных и ожидаемого результата. Если вы хотите получить список, примените lapply. Если вектор — sapply.

Обходные пути


В любом случае, простой способ — передать sapply вектор индексов и написать свою функцию, делая предположение о структуре входных данных. Давайте еще раз взглянем на пример с mean:

sapply(1:3, function(x) mean(m[,x]))

[1] -0.02664418  1.95812458  4.86857792


В нашу функцию мы передаем индексы колонок (1, 2, 3), что предполагает наличие переменной m с нашими данными. Хорошо как быстрое решение, но в целом не очень, и с большой вероятностью в дальнейшем превратится в большую проблему при поддержке.

Можно сделать немножко лучше, передавая наши данные как аргумент функции и используя специальный аргумент »…», который принимают все функции apply для передачи дополнительных параметров:

sapply(1:3, function(x, y) mean(y[,x]), y=m)

#[1] -0.02664418  1.95812458  4.86857792


На этот раз у нашей функции два аргумента, х и у. Переменная х, как и раньше, будет обозначать данные, которые перебирает sapply, что бы это ни было. Переменную у мы передадим, используя необязательные аргументы sapply.

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

Настоятельно не рекомендуется передавать индексированные аргументы таким образом, это источник ошибок и трудно воспринимается, когда другие люди будут читать ваш код.

Надеюсь, эти примеры были полезны.

© Habrahabr.ru