212,56
рейтинг
11 января в 10:38

Разработка → Использование apply, sapply, lapply в R перевод tutorial

Это вводная статья об использовании 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.

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

Надеюсь, эти примеры были полезны.
Автор: @qc-enior Pete

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

  • 0
    Для простых задач достаточно и семейств *apply функций,
    но для больших размерностей или сложных агрегациях,
    лучше использовать функции из dplyr (они побыстрее и их удобнее использовать).
    • 0
      dplyr только для data.frame. Если ради удобства и отталкиваться от примеров в статье, то можно использовать plyr.
  • +1
    А можно глупый вопрос: почему length(x[x<0]), а не, например, sum(x<0)?
    • 0
      Я бы тоже использовал sum. Оно и быстрей в пару раз и позволяет правильно обработать NA.
    • 0
      Вероятнее всего, что автор оригинального поста не очень хорошо знал R (так как его пример ошибочен в случае с NA),
      впоследствии он же сетует на плохую документацию R.
      Хотя по apply есть и более удачные примеры.
  • 0
    Наверно поэтому у меня низкая карма, я не считаю нужным описывать на хабре фигню, которая описана в каждой книге и в каждом курсе по R.

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

Самое читаемое Разработка