Большинство глаголов 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.
Маскирование данных
Маскирование данных ускоряет манипуляции с данными, поскольку требует меньшего набора текста. В большинстве (но не во всех) базовых функциях 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))
выбирает столбцыa
,b
и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.