Как стать автором
Обновить

Программирование с dplyr

Время на прочтение 10 мин
Количество просмотров 2.2K
Автор оригинала: Hadley Wickham

Большинство глаголов dplyr так или иначе используют аккуратную оценку (tidy evaluation). Tidy evaluation - это особый тип нестандартной оценки, используемый во всём tidyverse. В dplyr есть две основные формы tidy evaluation:

  • arrange()count()filter()group_by()mutate(), и summarise() используют маскировку данных (data masking), чтобы вы могли обращаться к столбцам таблиц так, как если бы они были переменными глобального окружения (т.е. позволяет опустить название таблицы и обращаться my_variable, вместо df$myvariable).

  • across()relocate()rename()select(), и pull()  используют концепцию tidy selection, позволяющую обращаться к столбцам таблицы по их индексу, имени или типу (например, start_with("x") или is.numeric).

Узнать использует ли аргумент функции маскирование данных или tidy selection, можно посмотрев документацию: в списке аргументов вы увидите <data-masking> или <tidy-select>.

Описанные выше концепции обращения к переменным таблиц делают интерактивное исследование данных быстрым и гибким, но они добавляют некоторые новые проблемы, когда вы пытаетесь использовать их косвенно, например, в теле цикла for или собственной функции. Эта статья поможет вам разобраться как преодолеть эти проблемы. Сначала мы рассмотрим основы концепций data masking и tidy selection, поговорим о том, как их использовать косвенно, а затем рассмотрим ряд рецептов решения наиболее распространенных проблем.

Эта статья даст вам минимум знаний, необходимых для того, чтобы писать эффективный код с использованием tidy evaluation. Если вы хотите больше узнать о лежащей в основе теории или о том, как именно она отличается от нестандартной оценки, я рекомендуем вам прочитать главы о метапрограммировании в книге "Advanced R".

library(dplyr)

Содержание

Если вы интересуетесь анализом данных возможно вам будут полезны мои telegram и youtube каналы. Большая часть контента которых посвящены языку R.

  1. Маскирование данных

    1. Переменные окружения и данных

    2. Косвенное обращение

  2. Концепция Tidy selection

    1. Синтаксис tidyselect DSL

    2. Косвенное обращение

  3. Рецепты

    1. Данные, предоставленные пользователем

    2. Исправление R CMD check NOTEs

    3. Одно или несколько пользовательских выражений

    4. Любое количество пользовательских выражений

    5. Преобразование пользовательских переменных

    6. Перебрать несколько переменных

    7. Использование переменной ввода Shiny

  4. Заключение

Маскирование данных

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

starwars[starwars$homeworld == "Naboo" & starwars$species == "Human", ]

Эквивалент этого кода в dplyr более лаконичен, потому что маскирование данных позволяет вам один раз ввести starwars:

starwars %>% filter(homeworld == "Naboo", species == "Human")

Переменные окружения и данных

Ключевая идея маскирования данных заключается в том, что оно стирает грань между двумя разными значениями слова «переменная»:

  • переменные окружения - это «программные» переменные, которые созданны и доступны в окружении. Обычно они создаются с помощью <-.

  • переменные данных - это «статистические» переменные, которые живут во фрейме данных, т.е. это столбцы таблицы. Обычно они берутся из файлов данных (например  .csv.xls) или создаются с помощью векторов.

Чтобы сделать эти определения более понятными, посмотрите на следующий фрагмент кода:

df <- data.frame(x = runif(3), y = runif(3))
df$x
#> [1] 0.08075014 0.83433304 0.60076089

Он создает переменную окружения df, которая содержит две переменные данных, x и y. Затем он извлекает переменную данных x из переменной окружения df с помощью $.

Я думаю, что это размытие значения «переменной» - хорошая фича для интерактивного анализа данных, потому что она позволяет вам ссылаться на переменные данных как есть, без какого-либо префикса. И это кажется довольно интуитивным, поскольку многие новые пользователи R обычно забывают про дублирование имён таблицы в базовом R, и пишут что-то вродеdiamonds[x == 0 | y == 0, ].

К сожалению, за это преимущество придётся заплатить. Когда вы начнете программировать с помощью описанных в данной статье концепций, вам придется столкнуться с конфликтом имён переменных глобального окружения и данных. Это будет не привычно, потому что вам никогда не приходилось задумываться об этом раньше, поэтому понадобится какое то время, чтобы усвоить эти новые концепции и привыкнуть к ним. Однако, как только вы разделите идею «переменной» на переменную данных и переменную окружения, всё станет намного проще.

Косвенное обращение

Основная проблема программирования с функциями, использующими data masking, возникает, когда вы вводите некоторую косвенность, то есть когда вы хотите получить переменную данных из переменной окружения вместо того, чтобы напрямую вводить имя переменной данных. Есть два основных случая:

  • Когда у вас есть переменная данных в аргументе функции (то есть переменная окружения функции, которая содержит обещание), вам нужно охватить аргумент, заключив его в двойные фигурные скобки, например filter(df, {{ var }}).

    Следующая функция использует охват фигурными скодками для создания обвёртки над summarize(), которая вычисляет минимальное и максимальное значения заданной пользователем переменной, а также общее количество наблюдений каждой группы:

var_summary <- function(data, var) {
  data %>%
    summarise(n = n(), min = min({{ var }}), max = max({{ var }}))
}
mtcars %>% 
  group_by(cyl) %>% 
  var_summary(mpg)
  • Если у вас есть переменная окружения, которая является текстовым вектором, вам необходимо проиндексировать местоимение .data с помощью [[, например, summarize(df, mean = mean(.data[[var]])).

    В следующем примере .data используется для подсчета количества уникальных значений каждого столбца таблицы mtcars:

for (var in names(mtcars)) {
  mtcars %>% count(.data[[var]]) %>% print()
}

Обратите внимание, что .data не является таблицей; это специальная конструкция, местоимение, которое позволяет вам получить доступ к текущим переменным либо напрямую, с помощью .data$x, либо косвенно с помощью .data[[var]]. Этот приём не будет работать в других функциях.

Концепция Tidy selection

Маскирование данных упрощает вычисление значений в наборе данных. Tidy selection - это дополнительный инструмент, который упрощает работу со столбцами набора данных.

Синтаксис tidyselect DSL

Все функции, поддерживающие концепцию tidy select основаны на пакете tidyselect. Он предоставляет миниатюрный предметно-ориентированный язык, который упрощает выбор столбцов по имени, положению или типу (DSL - Data Selection Language). Например:

  • select(df, 1)выбирает первый столбец; select(df, last_col())выбирает последний столбец.

  • select(df, c(a, b, c))выбирает столбцы abи c.

  • select(df, starts_with("a"))выбирает все столбцы, имя которых начинается с «а»; select(df, ends_with("z"))выбирает все столбцы, имена которых заканчиваются на «z».

  • select(df, where(is.numeric)) выбирает все числовые столбцы.

Больше примеров можно посмотреть в официальной справке ?dplyr_tidy_select.

Косвенное обращение

Как и в случае с маскированием данных, tidy select упрощает выполнение общей задачи за счет усложнения менее распространенной задачи. Если вы хотите использовать tidy select косвенно со спецификацией столбца, хранящейся в промежуточной переменной, вам необходимо изучить некоторые новые инструменты. Опять же, есть две формы косвенного обращения:

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

    Следующая функция вычисляет средние значения всех переменных таблицы, выбранных пользователем:

summarise_mean <- function(data, vars) {
  data %>% summarise(n = n(), across({{ vars }}, mean))
}
mtcars %>% 
  group_by(cyl) %>% 
  summarise_mean(where(is.numeric))
  • Когда у вас есть глобальная переменная в виде текстового вектора, с помощью которой вы хотите обратиться к группе столбцов, необходимо использовать all_of() или  any_of() в зависимости от того, хотите ли вы, чтобы функция выдавала ошибку, если одна из переменных, перечисленных в вашем векторе не найдена в таблице.

    Следующий код использует all_of() для выбора всех переменных, заданных в векторе vars; затем мы используем  all_of() в сочетании с !⁣, чтобы исключить из таблицы mtcars все переменные, заданные в векторе vars:

vars <- c("mpg", "vs")
mtcars %>% select(all_of(vars))
mtcars %>% select(!all_of(vars))

Рецепты

Приведённый в этом разделе примеры демонстрируют решение наиболее распространённых проблем. Все примеры включают минимально необходимый объём кода, в решении реальных задач вам придётся писать больший объём кода, и объединять методы приведённые в нескольких примерах.

Данные, предоставленные пользователем

Если вы посмотрите документацию, то увидите, что .data никогда не использует маскировку данных или tidy selection. Это означает, что вам не нужно делать ничего особенного в своей функции:

mutate_y <- function(data) {
  mutate(data, y = a + x)
}

Исправление R CMD check NOTEs

Если вы пишете пакет и у вас есть функция, которая использует переменные данных:

my_summary_function <- function(data) {
  data %>% 
    filter(x > 0) %>% 
    group_by(grp) %>% 
    summarise(y = mean(y), n = n())
}

Вы получите следующее предупреждение при проверке пакета с помощью R CMD CHECK:

N  checking R code for possible problems
   my_summary_function: no visible binding for global variable ‘x’, ‘grp’, ‘y’
   Undefined global functions or variables:
     x grp y

Убрать это предупреждение можно, используя .data$var и импортируя .data из пакета rlang (который реализует аккуратную оценку):

#' @importFrom rlang .data
my_summary_function <- function(data) {
  data %>% 
    filter(.data$x > 0) %>% 
    group_by(.data$grp) %>% 
    summarise(y = mean(.data$y), n = n())
}

Одно или несколько пользовательских выражений

Если вы хотите, чтобы пользователь предоставил выражение, которое передается в аргумент, использующий маскирование данных или tidy selection, оберните аргумент двойными фигурными скобками:

my_summarise <- function(data, group_var) {
  data %>%
    group_by({{ group_var }}) %>%
    summarise(mean = mean(mass))
}

Если необходимо использовать одно предоставленное пользователем выражение в нескольких местах вычислений:

my_summarise2 <- function(data, expr) {
  data %>% summarise(
    mean = mean({{ expr }}),
    sum = sum({{ expr }}),
    n = n()
  )
}

Если вам необходимо передать в тело функции несколько аргументов, оберните каждый из них двойными фигурными скобками:

my_summarise3 <- function(data, mean_var, sd_var) {
  data %>% 
    summarise(mean = mean({{ mean_var }}), sd = sd({{ sd_var }}))
}

Если вы хотите использовать имена переменных в выходных данных, вы можете использовать синтаксис glue в сочетании с оператором :=:

my_summarise4 <- function(data, expr) {
  data %>% summarise(
    "mean_{{expr}}" := mean({{ expr }}),
    "sum_{{expr}}" := sum({{ expr }}),
    "n_{{expr}}" := n()
  )
}
my_summarise5 <- function(data, mean_var, sd_var) {
  data %>% 
    summarise(
      "mean_{{mean_var}}" := mean({{ mean_var }}), 
      "sd_{{sd_var}}" := sd({{ sd_var }})
    )
}

Любое количество пользовательских выражений

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

my_summarise <- function(.data, ...) {
  .data %>%
    group_by(...) %>%
    summarise(mass = mean(mass, na.rm = TRUE), height = mean(height, na.rm = TRUE))
}

starwars %>% my_summarise(homeworld)
#> # A tibble: 49 x 3
#> homeworld mass height
#> <chr> <dbl> <dbl>
#> 1 Alderaan 64 176.
#> 2 Aleen Minor 15 79 
#> 3 Bespin 79 175 
#> 4 Bestine IV 110 180 
#> # … with 45 more rows

starwars %>% my_summarise(sex, gender)
#> `summarise()` has grouped output by 'sex'. You can override using the `.groups` argument.
#> # A tibble: 6 x 4
#> # Groups: sex [5]
#> sex gender mass height
#> <chr> <chr> <dbl> <dbl>
#> 1 female feminine 54.7 169.
#> 2 hermaphroditic masculine 1358 175 
#> 3 male masculine 81.0 179.
#> 4 none feminine NaN 96 
#> # … with 2 more rows

При использовании ... в своих функциях рекомендуется добавлять к именам всех остальных аргументов префикс .. Это позволит снизить (но не исключить полностью) вероятность конфликта аргументов; см. https://design.tidyverse.org/dots-prefix.html для получения более подробной информации.

Преобразование пользовательских переменных

Если ваша функция принимает от пользователя набор переменных данных (столбцов таблицы), которые в результате выполнения функции необходимо преобразовать используйте across():

my_summarise <- function(data, summary_vars) {
  data %>%
    summarise(across({{ summary_vars }}, ~ mean(., na.rm = TRUE)))
}
starwars %>% 
  group_by(species) %>% 
  my_summarise(c(mass, height))

#> # A tibble: 38 x 3
#> species mass height
#> <chr> <dbl> <dbl>
#> 1 Aleena 15 79
#> 2 Besalisk 102 198
#> 3 Cerean 82 198
#> 4 Chagrian NaN 196
#> # … with 34 more rows

Вы можете использовать ту же идею для нескольких наборов входных переменных данных:

my_summarise <- function(data, group_var, summarise_var) {
  data %>%
    group_by(across({{ group_var }})) %>% 
    summarise(across({{ summarise_var }}, mean))
}

Используйте аргумент .names в across() для определения имён столбцов результирующей исходящей таблицы.

my_summarise <- function(data, group_var, summarise_var) {
  data %>%
    group_by(across({{ group_var }})) %>% 
    summarise(across({{ summarise_var }}, mean, .names = "mean_{.col}"))
}

Перебрать несколько переменных

Если вы хотите перебрать вектор имен переменных с помощью цикла for, укажите специальное местоимение .data:

for (var in names(mtcars)) {
  mtcars %>% count(.data[[var]]) %>% print()
}

Этот же метод работает с альтернативами цикла for, такими как семейство функций  apply() и семейство purrr::map():

mtcars %>% 
  names() %>% 
  purrr::map(~ count(mtcars, .data[[.x]]))

Использование переменной ввода Shiny

Большинство элементов ввода в пользовательском интерфейсе Shiny возвращают вектор, поэтому вы можете использовать приведённый в предыдущем примере подход: .data[[input$var]].

library(shiny)
ui <- fluidPage(
  selectInput("var", "Variable", choices = names(diamonds)),
  tableOutput("output")
)
server <- function(input, output, session) {
  data <- reactive(filter(diamonds, .data[[input$var]] > 0))
  output$output <- renderTable(head(data()))
}

См. Https://mastering-shiny.org/action-tidy.html для получения более подробной информации и примеров использования.

Заключение

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

Если интересуетесь анализом данных и языком R наверняка вам будут интересны мои YouTube и Telegram канал, основная часть контента которых посвящена языку R.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+7
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Data Scientist
62 вакансии

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн