[Перевод] Использование 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
.
Настоятельно не рекомендуется передавать индексированные аргументы таким образом, это источник ошибок и трудно воспринимается, когда другие люди будут читать ваш код.
Надеюсь, эти примеры были полезны.