Совместное использование data.table и tidy eval: почему group by не работает должным образом, почему вставлено ~?

У меня нет важного варианта использования, но я хотел бы понять, как аккуратный eval и data.table могут работать вместе.

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

Как заставить data.table + tidy eval работать с group by?

В следующих примерах я использовал разрабатываемую версию rlang.

Обновить

Я обновил свой исходный вопрос на основе ответа Стефана Ф и моих дальнейших исследований: я больше не думаю, что вставленный ~ является важной частью вопроса, поскольку он также присутствует в коде dplyr, но у меня есть конкретный код: data.table + group by + quo, почему я не понял, не работает.

# setup ------------------------------------

suppressPackageStartupMessages(library("data.table"))
suppressPackageStartupMessages(library("rlang"))
suppressPackageStartupMessages(library("dplyr"))
#> Warning: package 'dplyr' was built under R version 3.5.1

dt <- data.table(
    num_campaign = 1:5,
    id = c(1, 1, 2, 2, 2)
)
df <- as.data.frame(dt)

# original question ------------------------

aggr_expr <- quo(sum(num_campaign))

q <- quo(dt[, aggr := !!aggr_expr][])

e <- quo_get_expr(q)
e
#> dt[, `:=`(aggr, ~sum(num_campaign))][]
dt[, `:=`(aggr, ~sum(num_campaign))][]
#> Error in `[.data.table`(dt, , `:=`(aggr, ~sum(num_campaign))): RHS of assignment is not NULL, not an an atomic vector (see ?is.atomic) and not a list column.
eval_tidy(e, data = dt)
#>    num_campaign id aggr
#> 1:            1  1   15
#> 2:            2  1   15
#> 3:            3  2   15
#> 4:            4  2   15
#> 5:            5  2   15

использование выражения вместо quo в этом случае нецелесообразно, так как переменные в пользовательском выражении могут не оцениваться в хорошей среде:

# updated question --------------------------------------------------------

aggr_dt_expr <- function(dt, aggr_rule) {
    aggr_expr <- enexpr(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_expr][])
    eval_tidy(q, data = dt)
}

x <- 1L
# expression is evaluated with x = 2
aggr_dt_expr(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1   17
#> 2:            2  1   17
#> 3:            3  2   17
#> 4:            4  2   17
#> 5:            5  2   17

aggr_dt_quo <- function(dt, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo][])
    eval_tidy(q, data = dt)
}

x <- 1L
# expression is evaluated with x = 1
aggr_dt_quo(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1   16
#> 2:            2  1   16
#> 3:            3  2   16
#> 4:            4  2   16
#> 5:            5  2   16

У меня явная проблема с использованием группы по:

# using group by --------------------------------

grouped_aggr_dt_expr <- function(dt, aggr_rule) {
    aggr_quo <- enexpr(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo, by = id][])
    eval_tidy(q, data = dt)
}

# group by has effect but x = 2 is used
grouped_aggr_dt_expr(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1    5
#> 2:            2  1    5
#> 3:            3  2   14
#> 4:            4  2   14
#> 5:            5  2   14

grouped_aggr_dt_quo <- function(dt, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo, by = id][])
    eval_tidy(q, data = dt)
}

# group by has no effect
grouped_aggr_dt_quo(dt, sum(num_campaign) + x)
#>    num_campaign id aggr
#> 1:            1  1   16
#> 2:            2  1   16
#> 3:            3  2   16
#> 4:            4  2   16
#> 5:            5  2   16


# using dplyr works fine ------------------------------------------------------------

grouped_aggr_df_quo <- function(df, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(mutate(group_by(df, id), !!aggr_quo))
    eval_tidy(q)
}
grouped_aggr_df_quo(df, sum(num_campaign) + x)
#> # A tibble: 5 x 3
#> # Groups:   id [2]
#>   num_campaign    id `sum(num_campaign) + x`
#>          <int> <dbl>                   <int>
#> 1            1     1                       4
#> 2            2     1                       4
#> 3            3     2                      13
#> 4            4     2                      13
#> 5            5     2                      13

Я понимаю, что извлечение выражений из запросов - это не способ работать с аккуратным eval, но я надеялся использовать его в качестве инструмента отладки: (пока не очень удачно)

# returning expression in quo for debugging --------------

grouped_aggr_dt_quo_debug <- function(dt, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(dt[, aggr := !!aggr_quo, by = id][])
    quo_get_expr(q)
}

grouped_aggr_dt_quo_debug(dt, sum(num_campaign) + x)
#> dt[, `:=`(aggr, ~sum(num_campaign) + x), by = id][]

grouped_aggr_df_quo_debug <- function(df, aggr_rule) {
    aggr_quo <- enquo(aggr_rule)
    x <- 2L
    q <- quo(mutate(group_by(df, id), !!aggr_quo))
    quo_get_expr(q)
}
# ~ is inserted in this case as well so it is not the problem
grouped_aggr_df_quo_debug(df, sum(num_campaign) + x)
#> mutate(group_by(df, id), ~sum(num_campaign) + x)

Создано 12 августа 2018 г. пакетом REPEX (v0.2.0).

Исходная формулировка вопроса:

Почему вставлен ~ и почему это не проблема аккуратного eval, если это проблема с базовым eval и все находится в глобальной среде?

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


person Ildi Czeller    schedule 11.08.2018    source источник
comment
Судя по принятому ответу, то, что вы хотите, в настоящее время невозможно с rlang (но должно быть). Возможно, вы могли бы отправить проблему в систему отслеживания проблем rlang.   -  person Stefan F    schedule 12.08.2018


Ответы (2)


TL; DR: запросы реализованы в виде формул из-за ошибки, которая затрагивает все версии R до 3.5.1. Специальное определение rlang для ~ доступно только с eval_tidy(). Вот почему Quosures не так совместимы с функциями non-tidyeval, как хотелось бы.

Изменить: Тем не менее, вероятно, существуют другие проблемы, чтобы сделать API-интерфейсы маскирования данных, такие как data.table, совместимыми с запросами.


Котировки в настоящее время реализованы в виде формул:

library("rlang")

q <- quo(cat("eval!\n"))

is.call(q)
#> [1] TRUE

as.list(unclass(q))
#> [[1]]
#> `~`
#>
#> [[2]]
#> cat("eval!\n")
#>
#> attr(,".Environment")
#> <environment: R_GlobalEnv>

Сравните с обычными формулами:

f <- ~cat("eval?\n")

is.call(f)
#> [1] TRUE

as.list(unclass(f))
#> [[1]]
#> `~`
#>
#> [[2]]
#> cat("eval?\n")
#>
#> attr(,".Environment")
#> <environment: R_GlobalEnv>

Так в чем разница между запросом и формулой? Первый оценивает себя, а второй цитирует себя, т.е. возвращает себя.

eval_tidy(q)
#> eval!

eval_tidy(f)
#> ~cat("eval?\n")

Механизм цитирования реализован примитивом ~:

`~`
#> .Primitive("~")

Одна из важных задач этого примитива - записать среду при первом вычислении формулы. Например, формула в quote(~foo) не оценивается и не записывает среду, в то время как eval(quote(~foo)) это делает.

В любом случае, когда вы оцениваете вызов ~, определение для ~ ищется обычным способом и обычно находит примитив ~. Так же, как при вычислении 1 + 1, ищется определение + и обычно находится .Primitive("+"). Причина, по которой Quosures самооценка, а не самооценка, заключается просто в том, что eval_tidy() создает специальное определение для ~ в своей среде оценки. Вы можете получить это особое определение с помощью eval_tidy(quote(`~`)).

Итак, почему мы реализовали quosures как формулы?

  1. Он лучше отходит и печатает. Эта причина теперь устарела, потому что у нас есть собственный депарсер выражений, в котором предложения печатаются с начальным ^, а не с ~.

  2. Из-за ошибки во всех версиях R до 3.5.1 выражения с классом оцениваются на рекурсивных отпечатках. Вот пример классифицированного вызова:

    x  <- quote(stop("oh no!"))
    x <- structure(x, class = "some_class")
    

    Сам объект печатает нормально:

    x
    #> stop("oh no!")
    #> attr(,"class")
    #> [1] "some_class"
    

    Но если вы поместите его в список, он будет оценен!

    list(x)
    #> [[1]]
    #> Error in print(stop("oh no!")) : oh no!
    

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

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

c <- as.call(list(toupper, "a"))
c
#> (function (x)
#> {
#>     if (!is.character(x))
#>         x <- as.character(x)
#>     .Internal(toupper(x))
#> })("a")

Самым большим преимуществом встраивания функций в вызовы является то, что их можно выполнять где угодно. Даже в пустой среде!

eval(c, emptyenv())
#> [1] "A"

Если бы мы реализовали запросы со встроенными функциями, их можно было бы вычислить где угодно. eval(q) будет работать, вы можете убрать кавычки внутри вызовов data.table и т. Д. Но заметили ли вы, насколько шумно выводится встроенный вызов из-за встраивания? Чтобы обойти это, нам нужно дать вызову класс и метод печати. Но помните об ошибке R ‹= 3.5.0. При печати списков вопросов на консоли мы получали странные нетерпеливые оценки. Вот почему quosures до сих пор реализуются как формулы и не так совместимы с функциями non-tidyeval, как хотелось бы.

person Lionel Henry    schedule 12.08.2018

Вам нужно использовать expr() вместо quo()

expr() фиксирует выражение, quo() фиксирует выражение + среду, в которой выражение должно быть вычислено ("запрос").

Quosures - это специфическая вещь для rlang / tidyeval, поэтому вам нужно использовать tidyeval для их оценки.

Что касается ~: тильда используется для формул в R. Формулы - это специальные объекты R, которые были разработаны для определения моделей в R (например, lm()), но они обладают некоторыми интересными свойствами, которые делают их полезными и для других целей. Очевидно, rlang использует их для представления запросов (но я не очень много знаю о внутреннем устройстве).

base::eval() думает, что вы предоставляете формулу, и не знает, что с ней делать в этом контексте, в то время как eval_tidy() знает, что вы на самом деле передаете вопрос. У вас нет этой проблемы с rlang::expr(), потому что он возвращает объекты, которые также базовый R знает, как обрабатывать.

person Stefan F    schedule 12.08.2018
comment
спасибо, ~ действительно не проблема, просто было странно видеть. Выражения не всегда являются решением, я обновил свой вопрос, включив явные примеры. - person Ildi Czeller; 12.08.2018