Menu

apply, lapply, sapply в R: семейство apply

Семейство apply — apply, lapply, sapply, vapply, mapply и tapply — прогоняет функцию по каждому элементу ваших данных. Вот что делает каждая из них и когда за какой браться.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Семейство apply в одном предложении

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

  • apply(m, MARGIN, FUN) — строки или столбцы матрицы.
  • lapply(x, FUN) — каждый элемент списка или вектора; всегда возвращает список.
  • sapply(x, FUN) — то же, что lapply, но затем упрощает до вектора или матрицы, когда может.
  • vapply(x, FUN, FUN.VALUE)sapply с объявленным и проверяемым типом возврата.
  • mapply(FUN, x, y, ...) — идёт по нескольким входам параллельно, элемент за элементом.
  • tapply(values, groups, FUN) — применяет FUN внутри каждой группы: один результат на группу.

Все они принимают функции, которые вы пишете сами, включая анонимные, — отсюда и берётся сила семейства.

apply(): строки и столбцы матрицы

apply принимает матрицу, MARGIN (1 для строк, 2 для столбцов) и функцию:

Матрица заполняется по столбцам, поэтому строки это 1 3 5 и 2 4 6: суммы по строкам печатаются как 9 12, а по столбцам — как 3 7 11. Для двух самых частых случаев базовый R поставляет специальные, более быстрые помощники — rowSums, colSums, rowMeans, colMeans, — так что берегите apply для функций, у которых такого помощника нет, вроде apply(m, 2, max).

Одна ловушка: apply на датафрейме молча сначала преобразует его в матрицу, что приводит все столбцы к одному общему типу. Для датафреймов относитесь к столбцам как к списку и используйте lapply/sapply.

lapply() всегда даёт список, sapply() упрощает

lapply отображает функцию на каждый элемент и возвращает список той же длины, что бы ни случилось:

Одно и то же вычисление, две формы: lapply возвращает список с 85 и 80; sapply замечает, что каждый результат имеет длину 1, и упрощает до именованного числового вектора — гораздо приятнее читать и подавать дальше в вычисления.

Однако у удобства острый край: тип возврата sapply зависит от данных. Если хотя бы один элемент даст другую длину, упрощение не удастся, и вы молча снова получите список:

В интерактивной работе на это можно пожать плечами; внутри скрипта это означает, что код, работавший весь год, ломается в день, когда данные сменили форму. Ровно ради этого и существует vapply.

vapply(): типобезопасный sapply

vapply добавляет третий аргумент, FUN.VALUE, — шаблон, объявляющий, как должен выглядеть один результат. integer(1) означает «каждый вызов возвращает ровно одно целое число»:

Вы получаете именованный целочисленный вектор — 5 6 6 — и, что важнее, гарантию: если бы nchar когда-нибудь вернула два значения или строку, vapply остановилась бы с ошибкой прямо в месте вызова, а не пустила бы результат неправильной формы дальше по коду. Объявление заодно служит документацией:

vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'

Правило большого пальца: sapply в консоли, vapply в функциях и скриптах, которые должны работать без присмотра.

mapply() и tapply(): параллельные входы и группы

lapply идёт по одной структуре. Когда каждому вызову нужен элемент из нескольких структур на совпадающих позициях, используйте mapply — обратите внимание, что функция идёт первой:

Каждый элемент вывода объединяет первые значения, затем вторые и так далее: 10 200 3000 40000.

tapply — групповой член семейства: она разбивает values по groups и применяет функцию внутри каждой группы, возвращая по одному результату на группу:

Группа a в среднем даёт 30, группа b40. Это ответ базового R на вопрос «среднее по категории» — тот же тип вопроса, на который для датафреймов отвечает group_by + summarize; если вы уже в мире dplyr, используйте его, а tapply блистает, когда у вас два голых вектора и нужна одна строка.

Два удобства работают во всём семействе. Дополнительные аргументы после функции пробрасываются в неё на каждом вызове:

А анонимные функции вставляются всякий раз, когда готовая функция не подходит: sapply(x, \(v) max(v) - min(v)) вычисляет размах для каждого элемента, не требуя сначала называть вспомогательную функцию.

Семейство apply против циклов for

Вам скажут, что циклы for в R медленные, а семейство apply быстрое. По большей части это миф. По-настоящему медленным является наращивание результата внутри циклаresult <- c(result, new_value) копирует весь вектор на каждом проходе. Цикл, заранее выделяющий свой выход, работает нормально:

Так что выбирайте по читаемости, а не по производительности. Версия с apply говорит что в одну строку — «возведи каждый элемент в квадрат» — и берёт выделение памяти на себя, поэтому она и является идиомой для прямолинейной поэлементной работы. Цикл for окупается, когда итерации зависят от предыдущих результатов, когда нужны досрочные выходы через break или когда тело настолько длинное, что лямбда только навредит. Оба варианта — законный R.

Что вы уносите с собой

  • Всё семейство — одна идея: прогнать функцию по каждому элементу и собрать результаты.
  • apply предназначена для строк матрицы (MARGIN = 1) и столбцов (MARGIN = 2); предпочитайте rowSums/colMeans, когда они существуют.
  • lapply всегда возвращает список; sapply упрощает, когда может, — а значит, её тип возврата может меняться вместе с данными.
  • vapply фиксирует тип возврата; используйте её в коде, который не должен вас удивлять.
  • mapply сшивает несколько входов вместе; tapply агрегирует значения внутри групп.
  • Циклы for не медленные по своей природе — медленно растут векторы. Выбирайте написание, которое читается лучше.

Дальше: пайпы — оператор, который сцепляет эти вызовы в читаемые пошаговые конвейеры.

Часто задаваемые вопросы

В чём разница между lapply и sapply в R?

lapply всегда возвращает список, без исключений. sapply выполняет то же вычисление, а затем пытается упростить результат — до вектора, если каждый элемент имеет длину 1, до матрицы, если длины совпадают, и обратно до списка, если упростить не удаётся. sapply приятнее в интерактивной работе; lapply (или vapply) безопаснее в скриптах, потому что её тип возврата никогда не меняется.

Что делает apply() в R?

apply(m, MARGIN, FUN) прогоняет FUN по матрице: MARGIN = 1 применяет её к каждой строке, MARGIN = 2 — к каждому столбцу. apply(m, 1, sum) даёт суммы по строкам, apply(m, 2, mean) — средние по столбцам. Она предназначена для матриц и массивов — для списков и векторов используйте lapply/sapply.

Быстрее ли семейство apply, чем цикл for в R?

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

Для чего нужен vapply в R?

vapply — это sapply с контрактом: вы объявляете тип и длину каждого результата, например vapply(x, nchar, integer(1)). Если функция вернёт что-то другое, R немедленно выдаст ошибку, а не подсунет вам молча неожиданную структуру. Предпочитайте её в коде, который должен работать без присмотра.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ