dplyr мутировать/заменить несколько столбцов в подмножестве строк
Я пытаюсь выполнить рабочий процесс на основе dplyr (вместо того, чтобы использовать в основном data.table, к которому я привык), и я столкнулся с проблемой, что я не могу найти эквивалент dplyr решение. Я обычно запускаю сценарий, где мне нужно условно обновлять/заменять несколько столбцов на основе одного условия. Вот пример кода с моим решением data.table:
library(data.table)
# Create some sample data
set.seed(1)
dt <- data.table(site = sample(1:6, 50, replace=T),
space = sample(1:4, 50, replace=T),
measure = sample(c('cfl', 'led', 'linear', 'exit'), 50,
replace=T),
qty = round(runif(50) * 30),
qty.exit = 0,
delta.watts = sample(10.5:100.5, 50, replace=T),
cf = runif(50))
# Replace the values of several columns for rows where measure is "exit"
dt <- dt[measure == 'exit',
`:=`(qty.exit = qty,
cf = 0,
delta.watts = 13)]
Есть ли простое решение dplyr для этой же проблемы? Я бы хотел избежать использования ifelse, потому что я не хочу вводить условие несколько раз - это упрощенный пример, но иногда есть много назначений, основанных на одном условии.
Заранее благодарим за помощь!
Ответы
Ответ 1
Эти решения (1) поддерживают конвейер, (2) не перезаписывают ввод и (3) требуют только одно условие:
1a) mutate_cond Создайте простую функцию для фреймов данных или таблиц данных, которые могут быть включены в конвейеры. Эта функция похожа на mutate
, но действует только на строки, удовлетворяющие условию:
mutate_cond <- function(.data, condition, ..., envir = parent.frame()) {
condition <- eval(substitute(condition), .data, envir)
.data[condition, ] <- .data[condition, ] %>% mutate(...)
.data
}
DF %>% mutate_cond(measure == 'exit', qty.exit = qty, cf = 0, delta.watts = 13)
1b) mutate_last Это альтернативная функция для фреймов данных или таблиц данных, которая снова похожа на mutate
, но используется только в group_by
(как в примере ниже) и работает только последней группы, а не каждой группы. Обратите внимание, что TRUE > FALSE, поэтому, если group_by
указывает условие, то mutate_last
будет работать только с строками, удовлетворяющими этому условию.
mutate_last <- function(.data, ...) {
n <- n_groups(.data)
indices <- attr(.data, "indices")[[n]] + 1
.data[indices, ] <- .data[indices, ] %>% mutate(...)
.data
}
DF %>%
group_by(is.exit = measure == 'exit') %>%
mutate_last(qty.exit = qty, cf = 0, delta.watts = 13) %>%
ungroup() %>%
select(-is.exit)
2) факторное условие Измените условие, добавив дополнительный столбец, который впоследствии будет удален. Затем используйте ifelse
, replace
или арифметику с логическими строками, как показано на рисунке. Это также работает для таблиц данных.
library(dplyr)
DF %>% mutate(is.exit = measure == 'exit',
qty.exit = ifelse(is.exit, qty, qty.exit),
cf = (!is.exit) * cf,
delta.watts = replace(delta.watts, is.exit, 13)) %>%
select(-is.exit)
3) sqldf Мы могли бы использовать SQL update
через пакет sqldf в конвейере для фреймов данных (но не таблицы данных, если мы их не конвертируем), это может представлять ошибку в dplyr. dplyr issue 1579). Может показаться, что мы нежелательно модифицируем ввод в этом коде из-за существования update
, но на самом деле update
действует на копию ввода во временно сгенерированной базе данных, а не на фактический ввод.
library(sqldf)
DF %>%
do(sqldf(c("update '.'
set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13
where measure = 'exit'",
"select * from '.'")))
Примечание 1: Мы использовали это как DF
set.seed(1)
DF <- data.frame(site = sample(1:6, 50, replace=T),
space = sample(1:4, 50, replace=T),
measure = sample(c('cfl', 'led', 'linear', 'exit'), 50,
replace=T),
qty = round(runif(50) * 30),
qty.exit = 0,
delta.watts = sample(10.5:100.5, 50, replace=T),
cf = runif(50))
Примечание 2: Проблема того, как легко указать обновление подмножества строк, также обсуждается в проблемах dplyr 134, 631, 1518 и 1573 с 631, являющийся основным потоком и 1573 является обзором ответов здесь.
Ответ 2
Вы можете сделать это с помощью magrittr
двухсторонней трубы %<>%
:
library(dplyr)
library(magrittr)
dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
cf = 0,
delta.watts = 13)
Это уменьшает количество ввода, но все еще намного медленнее, чем data.table
.
Ответ 3
Вот решение, которое мне нравится:
mutate_when <- function(data, ...) {
dots <- eval(substitute(alist(...)))
for (i in seq(1, length(dots), by = 2)) {
condition <- eval(dots[[i]], envir = data)
mutations <- eval(dots[[i + 1]], envir = data[condition, , drop = FALSE])
data[condition, names(mutations)] <- mutations
}
data
}
Он позволяет писать такие вещи, как например.
mtcars %>% mutate_when(
mpg > 22, list(cyl = 100),
disp == 160, list(cyl = 200)
)
который является вполне читаемым, хотя он может быть не таким производительным, каким он мог быть.
Ответ 4
Как показано выше в eipi10, нет простого способа выполнить замену подмножества в dplyr, поскольку DT использует семантику pass-by-reference vs dplyr, используя pass-by-value. dplyr требует использования ifelse()
для всего вектора, тогда как DT будет выполнять подмножество и обновлять по ссылке (возвращая весь DT). Таким образом, для этого упражнения DT будет значительно быстрее.
Вместо этого вы можете сначала подмножить, затем обновить и, наконец, рекомбинировать:
dt.sub <- dt[dt$measure == "exit",] %>%
mutate(qty.exit= qty, cf= 0, delta.watts= 13)
dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])
Но DT будет значительно быстрее:
(отредактирован для использования нового ответа eipi10)
library(data.table)
library(dplyr)
library(microbenchmark)
microbenchmark(dt= {dt <- dt[measure == 'exit',
`:=`(qty.exit = qty,
cf = 0,
delta.watts = 13)]},
eipi10= {dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
cf = 0,
delta.watts = 13)},
alex= {dt.sub <- dt[dt$measure == "exit",] %>%
mutate(qty.exit= qty, cf= 0, delta.watts= 13)
dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])})
Unit: microseconds
expr min lq mean median uq max neval cld
dt 591.480 672.2565 747.0771 743.341 780.973 1837.539 100 a
eipi10 3481.212 3677.1685 4008.0314 3796.909 3936.796 6857.509 100 b
alex 3412.029 3637.6350 3867.0649 3726.204 3936.985 5424.427 100 b
Ответ 5
Я просто наткнулся на это и очень похож на mutate_cond()
на @G. Гротендик, но подумал, что это может пригодиться и для обработки новых переменных. Итак, ниже есть два дополнения:
Несвязанный: вторая последняя строка сделана немного больше dplyr
, используя filter()
Три новые строки в начале получают имена переменных для использования в mutate()
и инициализируют любые новые переменные в кадре данных до mutate()
. Новые переменные инициализируются для остальной части data.frame
с помощью new_init
, который по умолчанию считается отсутствующим (NA
).
mutate_cond <- function(.data, condition, ..., new_init = NA, envir = parent.frame()) {
# Initialize any new variables as new_init
new_vars <- substitute(list(...))[-1]
new_vars %<>% sapply(deparse) %>% names %>% setdiff(names(.data))
.data[, new_vars] <- new_init
condition <- eval(substitute(condition), .data, envir)
.data[condition, ] <- .data %>% filter(condition) %>% mutate(...)
.data
}
Ниже приведены примеры использования данных диафрагмы:
Измените Petal.Length
на 88, где Species == "setosa"
. Это будет работать в исходной функции, а также в этой новой версии.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88)
То же, что и выше, но также создает новую переменную x
(NA
в строках, не включенных в условие). Невозможно раньше.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE)
То же, что и выше, но строки, не включенные в условие для x
, установлены в FALSE.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)
В этом примере показано, как new_init
можно установить в list
для инициализации нескольких новых переменных с разными значениями. Здесь создаются две новые переменные с инициализированными исключенными строками с использованием разных значений (x
инициализируется как FALSE
, y
как NA
)
iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
x = TRUE, y = Sepal.Length ^ 2,
new_init = list(FALSE, NA))
Ответ 6
mutate_cond - отличная функция, но она дает ошибку, если в столбце (столбцах), используемом для создания условия, существует NA. Я чувствую, что условный мутат должен просто оставлять только такие ряды. Это соответствует поведению filter(), который возвращает строки, когда условие TRUE, но не содержит обе строки с FALSE и NA.
При этом небольшом изменении функция работает как шарм:
mutate_cond <- function(.data, condition, ..., envir = parent.frame()) {
condition <- eval(substitute(condition), .data, envir)
condition[is.na(condition)] = FALSE
.data[condition, ] <- .data[condition, ] %>% mutate(...)
.data
}
Ответ 7
С созданием rlang
возможна слегка измененная версия примера Grothendieck 1a, что устраняет необходимость в аргументе envir
, так как enquo()
захватывает среду, в которой автоматически создается .p
.
mutate_rows <- function(.data, .p, ...) {
.p <- rlang::enquo(.p)
.p_lgl <- rlang::eval_tidy(.p, .data)
.data[.p_lgl, ] <- .data[.p_lgl, ] %>% mutate(...)
.data
}
dt %>% mutate_rows(measure == "exit", qty.exit = qty, cf = 0, delta.watts = 13)
Ответ 8
На самом деле я не вижу никаких изменений в dplyr
которые могли бы сделать это намного проще. case_when
подходит для case_when
, когда существует несколько разных условий и результатов для одного столбца, но это не помогает в этом случае, когда вы хотите изменить несколько столбцов на основе одного условия. Точно так же recode
экономит ввод, если вы заменяете несколько разных значений в одном столбце, но не помогает делать это одновременно в нескольких столбцах. Наконец, mutate_at
и т.д. Применяют условия только к именам столбцов, а не к строкам в кадре данных. Вы могли бы потенциально написать функцию для mutate_at, которая бы это делала, но я не могу понять, как бы вы заставили ее вести себя по-разному для разных столбцов.
Тем не менее, вот как я tidyr
к этому, используя nest
tidyr
и map
из purrr
.
library(data.table)
library(dplyr)
library(tidyr)
library(purrr)
# Create some sample data
set.seed(1)
dt <- data.table(site = sample(1:6, 50, replace=T),
space = sample(1:4, 50, replace=T),
measure = sample(c('cfl', 'led', 'linear', 'exit'), 50,
replace=T),
qty = round(runif(50) * 30),
qty.exit = 0,
delta.watts = sample(10.5:100.5, 50, replace=T),
cf = runif(50))
dt2 <- dt %>%
nest(-measure) %>%
mutate(data = if_else(
measure == "exit",
map(data, function(x) mutate(x, qty.exit = qty, cf = 0, delta.watts = 13)),
data
)) %>%
unnest()
Ответ 9
Вы могли бы разделить набор данных и сделать обычный вызов мутирования на TRUE
части.
В dplyr 0.8 есть функция group_split
которая разбивается на группы (и группы могут быть определены непосредственно в вызове), поэтому мы будем использовать ее здесь, но base::split
работает.
library(tidyverse)
df1 %>%
group_split(measure == "exit", keep=FALSE) %>% # or 'split(.$measure == "exit")'
modify_at(2,~mutate(.,qty.exit = qty, cf = 0, delta.watts = 13)) %>%
bind_rows()
# site space measure qty qty.exit delta.watts cf
# 1 1 4 led 1 0 73.5 0.246240409
# 2 2 3 cfl 25 0 56.5 0.360315879
# 3 5 4 cfl 3 0 38.5 0.279966850
# 4 5 3 linear 19 0 40.5 0.281439486
# 5 2 3 linear 18 0 82.5 0.007898384
# 6 5 1 linear 29 0 33.5 0.392412729
# 7 5 3 linear 6 0 46.5 0.970848817
# 8 4 1 led 10 0 89.5 0.404447182
# 9 4 1 led 18 0 96.5 0.115594622
# 10 6 3 linear 18 0 15.5 0.017919745
# 11 4 3 led 22 0 54.5 0.901829577
# 12 3 3 led 17 0 79.5 0.063949974
# 13 1 3 led 16 0 86.5 0.551321441
# 14 6 4 cfl 5 0 65.5 0.256845013
# 15 4 2 led 12 0 29.5 0.340603733
# 16 5 3 linear 27 0 63.5 0.895166931
# 17 1 4 led 0 0 47.5 0.173088800
# 18 5 3 linear 20 0 89.5 0.438504370
# 19 2 4 cfl 18 0 45.5 0.031725246
# 20 2 3 led 24 0 94.5 0.456653397
# 21 3 3 cfl 24 0 73.5 0.161274319
# 22 5 3 led 9 0 62.5 0.252212124
# 23 5 1 led 15 0 40.5 0.115608182
# 24 3 3 cfl 3 0 89.5 0.066147321
# 25 6 4 cfl 2 0 35.5 0.007888337
# 26 5 1 linear 7 0 51.5 0.835458916
# 27 2 3 linear 28 0 36.5 0.691483644
# 28 5 4 led 6 0 43.5 0.604847889
# 29 6 1 linear 12 0 59.5 0.918838163
# 30 3 3 linear 7 0 73.5 0.471644760
# 31 4 2 led 5 0 34.5 0.972078100
# 32 1 3 cfl 17 0 80.5 0.457241602
# 33 5 4 linear 3 0 16.5 0.492500255
# 34 3 2 cfl 12 0 44.5 0.804236607
# 35 2 2 cfl 21 0 50.5 0.845094268
# 36 3 2 linear 10 0 23.5 0.637194873
# 37 4 3 led 6 0 69.5 0.161431896
# 38 3 2 exit 19 19 13.0 0.000000000
# 39 6 3 exit 7 7 13.0 0.000000000
# 40 6 2 exit 20 20 13.0 0.000000000
# 41 3 2 exit 1 1 13.0 0.000000000
# 42 2 4 exit 19 19 13.0 0.000000000
# 43 3 1 exit 24 24 13.0 0.000000000
# 44 3 3 exit 16 16 13.0 0.000000000
# 45 5 3 exit 9 9 13.0 0.000000000
# 46 2 3 exit 6 6 13.0 0.000000000
# 47 4 1 exit 1 1 13.0 0.000000000
# 48 1 1 exit 14 14 13.0 0.000000000
# 49 6 3 exit 7 7 13.0 0.000000000
# 50 2 4 exit 3 3 13.0 0.000000000
Если порядок имеет значение строки, используйте tibble::rowid_to_column
первый, а затем dplyr::arrange
на rowid
и выберите его в конце концов.
данные
df1 <- data.frame(site = sample(1:6, 50, replace=T),
space = sample(1:4, 50, replace=T),
measure = sample(c('cfl', 'led', 'linear', 'exit'), 50,
replace=T),
qty = round(runif(50) * 30),
qty.exit = 0,
delta.watts = sample(10.5:100.5, 50, replace=T),
cf = runif(50),
stringsAsFactors = F)
Ответ 10
За счет разрыва с обычным синтаксисом dplyr вы можете использовать within
из базы:
dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'],
delta.watts[measure == 'exit'] <- 13)
Кажется, что он хорошо интегрируется с трубой, и вы можете делать почти все, что захотите внутри.
Ответ 11
Я думаю, что этот ответ не был упомянут ранее. Он работает почти так же быстро, как и data.table
умолчанию -solution.
Используйте base::replace()
df %>% mutate( qty.exit = replace( qty.exit, measure == 'exit', qty[ measure == 'exit'] ),
cf = replace( cf, measure == 'exit', 0 ),
delta.watts = replace( delta.watts, measure == 'exit', 13 ) )
replace повторяет значение замены, поэтому, когда вы хотите, чтобы значения столбцов qty
введены в столбцы qty.exit
, вы также должны задать qty
... поэтому в первой замене следует qty.exit
qty[ measure == 'exit']
.
теперь вам, вероятно, не захочется постоянно вводить measure == 'exit'
... поэтому вы можете создать индекс-вектор, содержащий этот выбор, и использовать его в приведенных выше функциях.
#build an index-vector matching the condition
index.v <- which( df$measure == 'exit' )
df %>% mutate( qty.exit = replace( qty.exit, index.v, qty[ index.v] ),
cf = replace( cf, index.v, 0 ),
delta.watts = replace( delta.watts, index.v, 13 ) )
тесты
# Unit: milliseconds
# expr min lq mean median uq max neval
# data.table 1.005018 1.053370 1.137456 1.112871 1.186228 1.690996 100
# wimpel 1.061052 1.079128 1.218183 1.105037 1.137272 7.390613 100
# wimpel.index 1.043881 1.064818 1.131675 1.085304 1.108502 4.192995 100
Ответ 12
Одним из кратких решений было бы сделать мутацию на отфильтрованном подмножестве, а затем добавить обратно строки без выхода из таблицы:
library(dplyr)
dt %>%
filter(measure == 'exit') %>%
mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
rbind(dt %>% filter(measure != 'exit'))