Вопрос или проблема
Описание
Я пытаюсь освоить программирование на 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
. Переход к более чистым и интуитивным методам обработки данных, таким как применение функций вместо операторов, может значительно улучшить читаемость и поддерживаемость вашего кода. Если вы будете использовать функцию как параметры, это позволит вам писать более модульный и универсальный код.
Если у вас возникнут дополнительные вопросы или потребуется уточнить детали, не стесняйтесь спрашивать!