Программирование условия в data.table

Вопрос или проблема

Описание

Я пытаюсь освоить программирование на data.table и испытываю некоторые трудности с пониманием виктиметры по программированию на data.table.

В виктиметре, в разделе Вычисления на языке, приводится следующий пример на базовом R:

my_subset = function(data, col, val) {
  eval(substitute(subset(data, col == val)))
}
my_subset(iris, Species, "setosa")

Я пытаюсь понять, как адаптировать и расширить это, используя аргумент env = list() в data.table. Моя попытка воспроизвести это выглядит следующим образом:

my_subset <- function(data, col, val) {
    data <- data.table::as.data.table(data)
    data[x == val, env = list(x = as.name(col))]
}
my_subset(iris, "Species", "setosa")

1. Это, вероятно, показывает, насколько я не знаю, но почему “Species” должно быть в кавычках, в отличие от предыдущей функции my_subset()? Если я оставлю без кавычек, я получаю Ошибка: объект 'Species' не найден.

Расширяя, я бы хотел, возможно, создать столбец на основе определенного условия.

my_subset <- function(data, col, val, newcol) {
    data <- data.table::as.data.table(data)
    data[,
        y := data.table::fifelse(x == val, TRUE, FALSE),
        env = list(x = as.name(col), y = as.name(newcol))
    ]
}
results <- my_subset(iris, "Species", "setosa", "Test")
head(results)
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species   Test
          <num>       <num>        <num>       <num>  <fctr> <lgcl>
1:          5.1         3.5          1.4         0.2  setosa   TRUE
2:          4.9         3.0          1.4         0.2  setosa   TRUE
3:          4.7         3.2          1.3         0.2  setosa   TRUE
4:          4.6         3.1          1.5         0.2  setosa   TRUE
5:          5.0         3.6          1.4         0.2  setosa   TRUE
6:          5.4         3.9          1.7         0.4  setosa   TRUE

Это также, похоже, работает как ожидалось. Далее, я хотел бы заменить == на оператор, заданный пользователем. Ссылаясь на этот ответ на SO, кажется, что сделать это можно следующим образом:

my_fun <- function(data, col, val, newcol, operator) {
    # к data.table
    data <- data.table::as.data.table(data)

    # класс col
    is_number <- data[, inherits(x, "integer") | inherits(x, "numeric"), env = list(x = as.name(col))]

    # допустимые операторы
    ops <- c("<", ">", "<=", ">=", "==", "!=", "%in%")
    if (!is_number) ops <- ops[-c(1:4)]
    if (!operator %in% ops) stop()

    # делаем дело
    data[,
        y := data.table::fifelse(.cond, TRUE, FALSE),
        env = list(
            y = as.name(newcol),
            .cond = call(operator, as.name(col), val)
        )
    ]

    # возвращаем
    return(data)
}
# работает
results <- my_fun(iris, "Species", c("setosa", "virginica"), "Test", "%in%")

# ошибки как и планировалось
results <- my_fun(iris, "Species", c("setosa", "virginica"), "Test", ">")

# работает
results <- my_fun(iris, "Sepal.Length", 5.2, "GreaterThan5.2", ">")

Дело в том, что это, кажется, чуть ли не возвращает к eval(parse(...)), от чего виктиметра специально предостерегает.

В примере

Для более реалистичного примера, я пытаюсь добавить функциональность в ggplot2 geom_smooths(). Используя некоторую из приведенной логики, я написал эту функцию:

gg_split_smooth_data <- function(model, xvar, split_point, operator) {
    # получение данных
    data <- insight::get_datagrid(model, length = 1000) |>
        data.table::as.data.table()

    # класс col
    is_number <- data[, inherits(x, "integer") | inherits(x, "numeric"), env = list(x = as.name(xvar))]
    if (!is_number) cli::cli_abort(c("x" = "{.val {xvar}} должен быть числовым."))

    # предсказание
    pred <- predict(model, se = TRUE, newdata = data)

    # добавляем предсказания в данные
    data <- data |>
        dplyr::mutate(
            p = pred$fit,
            se = pred$se.fit,
            lower = p - 1.96 * se,
            upper = p + 1.96 * se
        ) |>
        data.table::as.data.table()


    # допустимые операторы
    ops <- c("<", ">", "<=", ">=")
    if (!operator %in% ops) cli::cli_abort(c("x" = "{.val {operator}} не является допустимым оператором."))

    # делаем дело
    data[,
        Group := data.table::fifelse(.cond, TRUE, FALSE),
        env = list(
            .cond = call(operator, as.name(xvar), split_point)
        )
    ]

    # делим по группе
    data <- split(data, by = "Group")

    # построение
    plot <- ggplot2::ggplot() +
        ggplot2::geom_ribbon(
            ggplot2::aes(
                x = .data[[xvar]],
                ymin = lower,
                ymax = upper,
                fill = Group
            ),
            alpha = 0.3,
            data = data[[1]]
        ) +
        ggplot2::geom_ribbon(
            ggplot2::aes(
                x = .data[[xvar]],
                ymin = lower,
                ymax = upper,
                fill = Group
            ),
            alpha = 0.3,
            data = data[[2]]
        ) +
        ggplot2::geom_line(
            ggplot2::aes(
                x = .data[[xvar]],
                y = p,
                color = Group
            ),
            data = data[[1]]
        ) +
        ggplot2::geom_line(
            ggplot2::aes(
                x = .data[[xvar]],
                y = p,
                color = Group
            ),
            data = data[[2]]
        ) +
        ggplot2::geom_vline(xintercept = split_point)

    return(plot)
}
mgcv::gam(hp ~ s(mpg), data = mtcars) |>
    gg_split_smooth_data(
        xvar = "mpg",
        split_point = 23,
        operator = "<"
    )

результат функции

Очевидно, что с этим есть множество проблем, но это иллюстрирует суть. Что ведет к

2. Есть ли альтернативы тому, чтобы пользователь передавал оператор в виде строки?

3. Каковы более эффективные подходы?

Я понимаю, что это скорее философский вопрос, поэтому буду признателен за любые ответы (tidyverse, base и т.д.)!

Ответ или решение

Вопрос, который вы подняли, касается работы с пакетом data.table в R, особенно об использовании функции для программирования условий и манипуляции данными. Давайте перейдем по порядку и постараемся ответить на ваши вопросы.

1. Почему "Species" должен быть в кавычках?

Когда вы используете my_subset(iris, Species, "setosa"), Species без кавычек интерпретируется как переменная в окружении (на уровне глобальной среды), чего не происходит в контексте функции data.table. Причина в том, что во многих функциях (включая data.table) вам нужно передавать имена колонок как строки, чтобы они правильно интерпретировались. Внутри функции my_subset, колонка col передается как строка, и именно поэтому вы должны использовать кавычки. Важно учитывать, что data.table использует специальную синтаксис, прекрасной функциональностью которого является использование строк для обозначения имен колонок.

2. Альтернативы передачи оператора в виде строки

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

my_fun <- function(data, col, val, newcol, operator) {
    # преобразуем в data.table
    data <- data.table::as.data.table(data)

    # проверяем, является ли колонка числовой
    is_number <- data[, inherits(x, "numeric"), env = list(x = as.name(col))]

    # проверка валидности оператора
    if (!is.function(operator)) stop("Неверный оператор. Ожидается функция.")

    # применяем условие
    data[, y := operator(get(col), val), by = .(1:.N)]

    return(data)
}

# Пример использования
results <- my_fun(iris, "Sepal.Length", 5.2, "GreaterThan5.2", `>`)

В данном случае вы передаете > в качестве функции. Это делает ваш код более мощным и универсальным.

3. Лучшие подходы

a. Использование dplyr и rlang. Если ваш основной фокус на работе с условиями и колонками, вы можете рассмотреть возможность использования dplyr, который предлагает очень удобный способ условного манипулирования данными.

library(dplyr)

my_fun_dplyr <- function(data, col, val, newcol, operator) {
    operator_func <- match.fun(operator)  # Преобразуем строку в функцию
    data %>%
        mutate(!!sym(newcol) := case_when(!!sym(col) %in% val ~ TRUE, TRUE ~ FALSE))
}

# Пример использования
results <- my_fun_dplyr(iris, "Species", c("setosa", "virginica"), "Test", "==")

b. Munge-операции с data.table. Используйте базовые операции data.table, которые позволяют работать со столбцами более интуитивно и гибко. Использование функционала eval может быть избегнуто альтернативными подходами.

Заключение

Вы правильно подходите к проблеме и ваши программные конструкции демонстрируют понимание работы с R и пакетом data.table. Переход к более чистым и интуитивным методам обработки данных, таким как применение функций вместо операторов, может значительно улучшить читаемость и поддерживаемость вашего кода. Если вы будете использовать функцию как параметры, это позволит вам писать более модульный и универсальный код.

Если у вас возникнут дополнительные вопросы или потребуется уточнить детали, не стесняйтесь спрашивать!

Оцените материал
Добавить комментарий

Капча загружается...