Другое

Полное руководство по функциям apply в R

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

Понимание семейства функций apply и функций группировки в R

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

Текущее понимание

На основе моего текущего (вероятно, неверного/неполного) понимания:

  1. sapply(vec, f):

    • Входные данные - это вектор
    • Выходные данные - это вектор/матрица, где элемент i равен f(vec[i])
    • Возвращает матрицу, если f имеет выходные данные из нескольких элементов
  2. lapply(vec, f):

    • То же, что и sapply, но выходные данные - это список
  3. apply(matrix, 1/2, f):

    • Входные данные - это матрица
    • Выходные данные - это вектор, где элемент i равен f(строка/столбец i матрицы)
  4. tapply(vector, grouping, f):

    • Выходные данные - это матрица/массив
    • Элемент в матрице/массиве - это значение f для группы g вектора
    • g становится именами строк/столбцов
  5. by(dataframe, grouping, f):

    • Пусть g будет группой
    • Применяет f к каждому столбцу группы/фрейма данных
    • Красиво выводит группу и значение f для каждого столбца
  6. aggregate(matrix, grouping, f):

    • Похож на by, но вместо красивого вывода результатов, aggregate объединяет всё в фрейм данных

Основной вопрос

Может кто-нибудь объяснить, какую функцию apply использовать в каком случае?

Дополнительный вопрос

Я всё ещё не изучал plyr или reshape - заменят ли plyr или reshape все эти функции полностью?

Семейство функций apply в R

Семейство функций apply в R предоставляет различные способы применения функций к структурам данных, где каждая функция служит определенным целям: lapply и sapply работают с векторами/списками и возвращают списки/упрощенные результаты, apply operates on matrices along rows or columns, tapply и by обрабатывают групповые операции над векторами и data frame соответственно, а aggregate создает свод data frame из сгруппированных данных. Понимание этих различий помогает выбрать правильную функцию для вашей структуры данных и желаемого формата вывода.

Содержание

Понимание семейства apply

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

Основные функции apply

Основные функции apply можно классифицировать по их основным случаям использования:

  1. Операции с векторами/списками: lapply(), sapply(), vapply()
  2. Операции с матрицами/массивами: apply()
  3. Операции группировки: tapply(), by(), aggregate()
  4. Специализированные операции: mapply(), eapply(), rapply()

Каждая функция имеет специфические характеристики относительно типов входных данных, форматов вывода и характеристик производительности, что делает их подходящими для разных сценариев.

Операции с векторами и списками

lapply() - Построитель списков

lapply() является наиболее фундаментальной функцией apply. Она применяет функцию к каждому элементу списка или вектора и возвращает список.

r
# Базовый синтаксис
lapply(X, FUN, ...)

# Пример: Возведение в квадрат каждого числа вектора
numbers <- 1:5
result <- lapply(numbers, function(x) x^2)
print(result)  # Список: [[1]] 1, [[2]] 4, [[3]] 9, [[4]] 16, [[5]] 25

Ключевые характеристики:

  • Всегда возвращает список
  • Работает с векторами и списками в качестве входных данных
  • Наиболее предсказуемое поведение среди функций apply
  • Хорошо подходит, когда нужен последовательный вывод в виде списка

sapply() - Упрощающая

sapply() по сути является lapply() с дополнительным шагом упрощения результата.

r
# Пример с sapply
numbers <- 1:5
result <- sapply(numbers, function(x) x^2)
print(result)  # Вектор: 1 4 9 16 25

# Пример, где sapply возвращает матрицу
complex_result <- sapply(list(c(1,2), c(3,4), c(5,6)), rbind)
print(complex_result)
#      [,1] [,2] [,3]
# [1,]    1    3    5
# [2,]    2    4    6

Ключевые характеристики:

  • Пытается упростить вывод списка в вектор или матрицу
  • Удобнее, чем lapply, когда упрощение работает корректно
  • Может быть непредсказуемой, когда функция возвращает выводы разной длины
  • Часто предпочтительна благодаря более чистому выводу, когда это возможно

vapply() - Безопасная версия

vapply() является более строгой версией sapply(), которая требует указания ожидаемого типа вывода.

r
# Синтаксис
vapply(X, FUN, FUN.VALUE, ...)

# Пример
numbers <- 1:5
result <- vapply(numbers, function(x) x^2, numeric(1))
print(result)  # Вектор: 1 4 9 16 25

Ключевые характеристики:

  • Требует указания типа возвращаемого значения (FUN.VALUE)
  • Более безопасна с точки зрения типов, чем sapply
  • Предотвращает неожиданные форматы вывода
  • Немного более многословна, но безопаснее для производственного кода
  • Лучше производительность, чем у sapply для больших наборов данных

Операции с матрицами и массивами

apply() - Обработчик матриц

apply() работает с матрицами или массивами, применяя функции вдоль указанных границ (строк или столбцов).

r
# Синтаксис
apply(X, MARGIN, FUN, ...)

# Пример с операциями по строкам/столбцам
matrix_data <- matrix(1:12, nrow = 3, ncol = 4)

# Суммы по строкам
row_sums <- apply(matrix_data, 1, sum)
print(row_sums)  # [1] 22 26 30

# Средние значения по столбцам
col_means <- apply(matrix_data, 2, mean)
print(col_means)  # [1]  2.5  5.5  8.5 11.5

# Пользовательская функция
apply(matrix_data, 1, function(row) {
  mean(row[row > 5])
})

Ключевые характеристики:

  • Предназначена для матриц и массивов
  • Аргумент MARGIN: 1 для строк, 2 для столбцов, c(1,2) для обоих
  • Возвращает массив с соответствующими размерностями
  • Может быть медленнее векторизованных операций для простых вычислений
  • Необходима, когда требуются операции по размерностям матрицы

Функции группировки и подмножеств

tapply() - Специалист по группировке

tapply() применяет функцию к подмножествам вектора, сгруппированным по другому вектору.

r
# Синтаксис
tapply(X, INDEX, FUN, ...)

# Пример
data <- c(10, 20, 15, 25, 30, 35)
groups <- factor(c("A", "B", "A", "B", "A", "B"))

# Расчет среднего по группам
result <- tapply(data, groups, mean)
print(result)
#   A   B 
# 18.3 26.7

# Множественные факторы группировки
groups2 <- factor(c("X", "X", "Y", "Y", "X", "Y"))
result2 <- tapply(data, list(groups, groups2), mean)

Ключевые характеристики:

  • Работает с векторами и факторами группировки
  • Возвращает массив с именами групп в качестве размерностей
  • Обрабатывает множественные факторы группировки
  • Идеально для статистических сводок по категориям
  • Вывод можно преобразовать в data frame при необходимости

by() - Функция группировки для DataFrame

by() применяет функцию к подмножествам data frame, сгруппированным по факторам.

r
# Синтаксис
by(data, INDICES, FUN, ...)

# Пример
df <- data.frame(
  value = c(10, 20, 15, 25, 30, 35),
  group = factor(c("A", "B", "A", "B", "A", "B")),
  category = factor(c("X", "X", "Y", "Y", "X", "Y"))
)

# Группировка по 'group' и расчет статистик
result <- by(df$value, df$group, function(x) {
  list(mean = mean(x), sd = sd(x), n = length(x))
})

# Группировка по множественным факторам и применение функции
result2 <- by(df, list(df$group, df$category), function(sub_df) {
  sub_df[sub_df$value == max(sub_df$value), ]
})

Ключевые характеристики:

  • Предназначена для data frame
  • Возвращает список data frame
  • Обрабатывает множественные факторы группировки
  • Более гибкая, чем tapply для сложных операций
  • Вывод является списком, что может потребовать дополнительной обработки

aggregate() - Создатель сводок

aggregate() создает свод data frame из сгруппированных данных.

r
# Синтаксис
aggregate(x, by, FUN, ...)

# Пример с использованием формульного интерфейса
df <- data.frame(
  value = c(10, 20, 15, 25, 30, 35),
  group = factor(c("A", "B", "A", "B", "A", "B")),
  category = factor(c("X", "X", "Y", "Y", "X", "Y"))
)

# Агрегация одной переменной
result1 <- aggregate(value ~ group, data = df, FUN = mean)

# Множественные переменные
result2 <- aggregate(cbind(value, value) ~ group, data = df, FUN = mean)

# Множественные факторы группировки
result3 <- aggregate(value ~ group + category, data = df, FUN = function(x) c(mean = mean(x), sd = sd(x)))

Ключевые характеристики:

  • Возвращает data frame со столбцами группировки и результатами
  • Доступен формульный интерфейс
  • Естественно обрабатывает множественные факторы группировки
  • Удобно для дальнейшего анализа
  • Наиболее удобно для создания таблиц сводных статистик

Когда использовать каждую функцию

Краткое руководство по выбору

  1. Используйте lapply(), когда:

    • Вам нужен последовательный вывод в виде списка
    • Работаете с векторами или списками
    • Функция возвращает объекты разной длины
  2. Используйте sapply(), когда:

    • Вы хотите получить более чистый вывод, чем список
    • Функция возвращает объекты одинаковой длины
    • Вы работаете в режиме интерактивного анализа
  3. Используйте vapply(), когда:

    • Вам нужна безопасность типов в производственном коде
    • Вы знаете ожидаемый тип возвращаемого значения
    • Производительность критически важна
  4. Используйте apply(), когда:

    • Работаете с матрицами или массивами
    • Требуются операции по строкам или столбцам
    • Применяете функции к границам матрицы
  5. Используйте tapply(), когда:

    • Работаете с векторными данными и факторами группировки
    • Требуются статистические сводки по группам
    • Нужен вывод в виде массива с именами групп
  6. Используйте by(), когда:

    • Работаете с data frame и факторами группировки
    • Требуются сложные операции над сгруппированными подмножествами
    • Нужен вывод в виде списка результатов
  7. Используйте aggregate(), когда:

    • Создаете свод data frame
    • Работаете с множественными факторами группировки
    • Нужен табличный вывод для отчетности

Вопросы производительности

r
# Сравнение производительности
library(microbenchmark)

# Тестовые данные
vec <- 1:10000
mat <- matrix(1:10000, nrow = 100)

# Сравнение методов apply
mb_results <- microbenchmark(
  lapply_result = lapply(vec, sqrt),
  sapply_result = sapply(vec, sqrt),
  vapply_result = vapply(vec, sqrt, numeric(1)),
  apply_result = apply(mat, 1, sum),
  times = 1000
)

print(mb_results)

Выводы о производительности:

  • vapply() обычно быстрее, чем sapply()
  • apply() может быть медленнее векторизованных операций для простых функций
  • Для больших наборов данных рассмотрите возможность использования векторизованных альтернатив, когда это возможно
  • lapply() имеет последовательную производительность независимо от сложности вывода

Сравнительная таблица

Функция Тип входных данных Тип вывода Лучше всего подходит Ключевые особенности
lapply() Вектор/Список Список Последовательный вывод списков Всегда возвращает список, обрабатывает любой тип вывода
sapply() Вектор/Список Вектор/Матрица Чистый вывод Пытается упростить, удобно, но непредсказуемо
vapply() Вектор/Список Вектор/Матрица Безопасность типов Требует указания типа возвращаемого значения, безопаснее sapply
apply() Матрица/Массив Массив Операции с матрицами Работает с границами (строки/столбцы), возвращает массив
tapply() Вектор + Группы Массив Сводки по группам Операции сгруппированных векторов, вывод массивом
by() DataFrame + Группы Список Группировка DataFrame Операции сгруппированных data frame, вывод списком
aggregate() DataFrame + Группы DataFrame Таблицы сводок Создает свод data frame, формульный интерфейс

Альтернативы: plyr и reshape2

Пакеты plyr и reshape2 (и их преемники) действительно заменяют многие функции apply, но с разными подходами:

Пакет plyr

Пакет plyr предоставляет последовательное соглашение об именовании и более предсказуемое поведение:

r
# Эквиваленты plyr
library(plyr)

# Эквивалент lapply
ldply(vec, function(x) data.frame(result = x^2))

# Эквивалент sapply
laply(vec, function(x) x^2)

# Эквивалент apply
aaply(matrix, 1, sum)

# Эквивалент tapply
ddply(df, .(group), summarise, mean_value = mean(value))

# Эквивалент aggregate
dcast(dcast(df, group ~ variable, mean), group ~ variable)

Ключевые преимущества:

  • Последовательная схема именования (тип входных данных + тип вывода)
  • Лучшее обработка ошибок
  • Более предсказуемое поведение
  • Отчет о прогрессе для долгих операций
  • Легче связывать операции

Пакет reshape2

Пакет reshape2 фокусируется на преобразовании данных и агрегации:

r
library(reshape2)

# Формат широкий-длинный
long_data <- melt(wide_data, id.vars = "id")

# Формат длинный-широкий
wide_data <- dcast(long_data, id ~ variable, mean)

# Агрегация
summary_data <- dcast(data, group + category ~ variable, mean)

Когда использовать базовый R и пакеты

Используйте базовый R apply, когда:

  • Вам нужна максимальная производительность
  • Работаете в минимальных окружениях
  • Функция проста и хорошо понятна
  • Вы поддерживаете устаревший код

Используйте plyr/reshape2, когда:

  • Вам нужно последовательное именование и поведение
  • Работаете со сложными преобразованиями данных
  • Вы хотите более понятные сообщения об ошибках
  • Вы создаете конвейеры анализа данных
  • Вам нужно связывать несколько операций

Практические примеры

Пример 1: Конвейер очистки данных

r
# Пример данных с пропущенными значениями
sales_data <- data.frame(
  product = rep(c("A", "B", "C"), each = 4),
  region = rep(c("North", "South"), 6),
  sales = c(100, 120, 110, NA, 200, 180, NA, 220, 150, 160, 140, 170),
  quarter = rep(1:4, 3)
)

# Использование базового R
# Заполнение пропущенных значений средним по группе
sales_data$sales_filled <- with(sales_data, ave(sales, product, FUN = function(x) {
  x[is.na(x)] <- mean(x, na.rm = TRUE)
  x
}))

# Использование plyr
library(plyr)
sales_data_plyr <- ddply(sales_data, .(product), transform,
  sales_filled = ifelse(is.na(sales), mean(sales, na.rm = TRUE), sales))

Пример 2: Статистический анализ

r
# Генерация тестовых данных
set.seed(123)
study_data <- data.frame(
  treatment = rep(c("Drug", "Placebo"), each = 20),
  age = rnorm(40, 50, 10),
  response = c(rnorm(20, 10, 2), rnorm(20, 8, 2))
)

# Подход базового R
tapply(study_data$response, study_data$treatment, function(x) {
  c(mean = mean(x), sd = sd(x), n = length(x))
})

# Подход aggregate
aggregate(response ~ treatment, study_data, 
  function(x) c(mean = mean(x), sd = sd(x), n = length(x)))

# Подход plyr
library(plyr)
ddply(study_data, .(treatment), summarise,
  mean_response = mean(response),
  sd_response = sd(response),
  n = length(response))

Пример 3: Обработка временных рядов

r
# Создание данных временного ряда
dates <- seq(as.Date("2020-01-01"), by = "month", length.out = 12)
time_series <- data.frame(
  date = dates,
  value = cumsum(rnorm(12, 0, 1)),
  category = rep(c("A", "B"), each = 6)
)

# Расчет скользящих средних по категориям
library(zoo)

# Подход базового R
time_series$rolling_mean <- ave(time_series$value, time_series$category,
  FUN = function(x) rollmean(x, k = 3, fill = NA, align = "right"))

# Подход dplyr
library(dplyr)
time_series %>%
  group_by(category) %>%
  mutate(rolling_mean = zoo::rollmean(value, k = 3, fill = NA, align = "right"))

Лучшие практики

Оптимизация производительности

  1. Предпочитайте векторизованные операции, когда это возможно:
r
# Медленно
result <- sapply(1:1000, function(x) x^2)

# Быстро
result <- (1:1000)^2
  1. Используйте подходящие функции apply для вашей структуры данных:
r
# Для операций с векторами/списками
lapply(my_list, my_function)

# Для операций с матрицами
apply(my_matrix, 2, my_function)

# Для операций группировки
tapply(my_vector, my_groups, my_function)
  1. Учитывайте эффективность памяти для больших наборов данных:
r
# Для очень больших векторов рассмотрите обработку по частям
chunk_size <- 10000
result <- list()
for (i in seq(1, length(large_vector), chunk_size)) {
  result[[length(result) + 1]] <- lapply(large_vector[i:(i+chunk_size-1)], my_function)
}
final_result <- unlist(result)

Читаемость кода

  1. Выбирайте осмысленные имена функций при использовании анонимных функций:
r
# Менее читаемо
sapply(data, function(x) sum(x, na.rm = TRUE))

# Более читаемо
sapply(data, sum_na)
  1. Используйте последовательное отступление и форматирование:
r
# Хорошее форматирование
result <- lapply(data, function(x) {
  # Очистка данных
  cleaned <- x[x > 0]
  # Расчет статистик
  stats <- list(mean = mean(cleaned), sd = sd(cleaned))
  return(stats)
})
  1. Добавляйте комментарии, объясняющие сложные операции:
r
# Расчет скользящих средних для каждой группы по категории
grouped_results <- by(data, data$category, function(group_data) {
  # Используем rollapply из zoo для эффективных расчетов скользящих окон
  rollapply(group_data$value, width = 5, FUN = mean, align = "right", fill = NA)
})

Обработка ошибок

  1. Используйте vapply() для производственного кода, когда это возможно:
r
# Безопаснее в производственном коде
vapply(data, calculate_stat, numeric(1), na.rm = TRUE)

# Менее предсказуемо
sapply(data, calculate_stat, na.rm = TRUE)
  1. Добавляйте проверку ошибок для граничных случаев:
r
# Надежная функция
safe_mean <- function(x) {
  if (length(x) == 0) return(NA)
  if (all(is.na(x))) return(NA)
  mean(x, na.rm = TRUE)
}

# Использование
vapply(data, safe_mean, numeric(1))
  1. Корректно обрабатывайте пропущенные значения:
r
# С явным параметром na.rm
lapply(data, function(x) mean(x, na.rm = TRUE))

# Или внутри функции
mean_with_na <- function(x) {
  if (all(is.na(x))) return(NA)
  mean(x, na.rm = TRUE)
}

Заключение

Семейство функций apply в R предоставляет мощный набор инструментов для манипулирования данными и анализа, где каждая функция служит определенным целям в зависимости от структуры данных и желаемого формата вывода. lapply() является вашим выбором для последовательного вывода в виде списка, sapply() предлагает более чистые результаты, когда упрощение работает корректно, vapply() обеспечивает безопасность типов для производственного кода, apply() обрабатывает операции с матрицами вдоль строк или столбцов, tapply() отлично справляется с операциями группировки векторов, by() работает с подмножествами data frame, а aggregate() создает свод data frame. Хотя пакеты plyr и reshape2 предлагают более последовательные интерфейсы и легкость связывания операций, базовые функции apply остаются незаменимыми благодаря своей производительности и универсальности в программировании на R.

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

Источники

  1. Команда разработчиков R - Документация по семейству функций apply
  2. R Inferno - Мучения apply
  3. R-bloggers - Понимание семейства apply
  4. Stack Overflow - Различия между функциями apply
  5. CRAN - Документация пакета plyr
  6. CRAN - Документация пакета reshape2
Авторы
Проверено модерацией
Модерация