Я пытаюсь ускорить конкретный расчет объекта data.table. Таблица содержит столбец значений и один или несколько столбцов группировки. Для каждой комбинации групп, если сумма значений больше единицы, я хотел бы пропорционально уменьшить значения, чтобы их сумма стала единицей. Можно предположить, что значения неотрицательны.
Вот пример настройки с полумиллионом строк:
library(data.table)
n_rows <- 5e5
dt <- data.table(
# grouping variables
group1 = sample(letters, size = n_rows, replace = TRUE),
group2 = sample(letters, size = n_rows, replace = TRUE),
group3 = sample(letters, size = n_rows, replace = TRUE),
group4 = sample(letters, size = n_rows, replace = TRUE),
# non-negative values
value = runif (n_rows)
)
И решение у меня есть:
scale_values <- function(values) {
values / sum(values)
}
dt[, {value := if (sum(value) > 1) {
scale_values(value)
} else {
value
}},
by = list(group1, grpup2, group3, group4)]
Это самый быстрый вариант, который я когда-либо нашел (после того, как немного поигрался с синтаксисом data.table и несколькими альтернативами scale_values), но мне бы хотелось сделать это быстрее. При фактическом использовании dt будет иметь примерно 20 миллионов строк и пять группирующих переменных.
Заранее благодарим вас за любые идеи о том, как это улучшить.
Обновлено: оказывается, что двухэтапное решение намного быстрее:
dt[, sum_values := sum(values), by = list(group1, group2, group3, group4)][
sum_values > 1, value := scale_values(value), by = list(group1, group2, group3, group4)]
Хотя мне не очень понятно, почему.





Вы хотите использовать оптимизацию Gforce. Используйте verbose = TRUE, чтобы проверить, используется ли он.
library(data.table)
n_rows <- 5e5
set.seed(42)
dt <- data.table(
group1 = sample(letters, size = n_rows, replace = TRUE),
group2 = sample(letters, size = n_rows, replace = TRUE),
group3 = sample(letters, size = n_rows, replace = TRUE),
group4 = sample(letters, size = n_rows, replace = TRUE),
value = runif (n_rows)
)
dt1 <- copy(dt)
scale_values <- function(values) {
values / sum(values)
}
system.time(
dt[, value := if (sum(value) > 1) {
scale_values(value)
} else {
value
},
by = list(group1, group2, group3, group4), verbose = TRUE])
#user system elapsed
#0.39 0.09 0.39
system.time({
dt1[, sum := sum(value), by = list(group1, group2, group3, group4), verbose = TRUE]
dt1[sum > 1, value := value / sum, verbose = TRUE]
})
#user system elapsed
#0.11 0.00 0.04
all.equal(dt[["value"]], dt1[["value"]])
#[1] TRUE
Я удалил ненужное by = .... Это делает его еще быстрее.
Да, я думаю, это определенно лучший способ
dt[, sum_value := sum(value), by = .(group1, group2, group3, group4)]
dt[, value := fifelse(sum_value > 1, value / sum_value, value), by = .(group1, group2, group3, group4)]
dt[, sum_value := NULL] # Clean up the temporary column
Использование fifelse выполняется быстрее, чем if-else. А удаление ненужных столбцов может повысить производительность.
Вы можете переключиться на соответствующий пакет свернуть
dt = dt |>
fgroup_by(group1, group2, group3, group4) |>
fmutate(sum = fsum(value))
# dt[sum > 1, value := value/sum, verbose = TRUE]
Спасибо! Это действительно значительно ускоряет процесс. Я не знал, стоит ли проверять
GForce, спасибо, что указали на это