dplyr muta/reemplaza varias columnas en un subconjunto de filas
data.table (12)
Estoy en el proceso de probar un flujo de trabajo basado en dplyr (en lugar de usar principalmente data.table, a lo que estoy acostumbrado), y me he encontrado con un problema al que no puedo encontrar una solución dplyr equivalente. . Comúnmente me encuentro con el escenario donde necesito actualizar / reemplazar condicionalmente varias columnas en función de una sola condición. Aquí hay un código de ejemplo, con mi solución 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)]
¿Existe una solución dplyr simple para este mismo problema? Me gustaría evitar usar ifelse porque no quiero tener que escribir la condición varias veces; este es un ejemplo simplificado, pero a veces hay muchas asignaciones basadas en una sola condición.
Gracias de antemano por la ayuda!
A expensas de romper con la sintaxis habitual de dplyr, puede usar
within
de la base:
dt %>% within(qty.exit[measure == ''exit''] <- qty[measure == ''exit''],
delta.watts[measure == ''exit''] <- 13)
Parece integrarse bien con la tubería, y puede hacer casi cualquier cosa que desee dentro de ella.
Aquí hay una solución que me gusta:
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
}
Te permite escribir cosas como por ejemplo
mtcars %>% mutate_when(
mpg > 22, list(cyl = 100),
disp == 160, list(cyl = 200)
)
lo cual es bastante legible, aunque puede que no sea tan eficiente como podría ser.
Como muestra eipi10 arriba, no hay una manera simple de hacer un reemplazo de subconjunto en dplyr porque DT usa semántica de paso por referencia vs dplyr usando paso por valor.
dplyr requiere el uso de
ifelse()
en todo el vector, mientras que DT realizará el subconjunto y lo actualizará por referencia (devolviendo todo el DT).
Entonces, para este ejercicio, DT será sustancialmente más rápido.
Alternativamente, puede subconjunto primero, luego actualizar y finalmente recombinar:
dt.sub <- dt[dt$measure == "exit",] %>%
mutate(qty.exit= qty, cf= 0, delta.watts= 13)
dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])
Pero DT será sustancialmente más rápido: (editado para usar la nueva respuesta de 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
Con la creación de
rlang
, es posible una versión ligeramente modificada del ejemplo 1a de Grothendieck, eliminando la necesidad del argumento
envir
, ya que
enquo()
captura el entorno en el que se crea
.p
automáticamente.
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()
Creo que esta respuesta no se ha mencionado antes.
Se ejecuta casi tan rápido como el ''default''
data.table
..
Use
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 recicla el valor de reemplazo, por lo tanto, cuando desea que los valores de las columnas
qty
qty.exit
en las columnas
qty.exit
, también debe subconjuntar
qty
... de ahí la
qty[ measure == ''exit'']
en el primer reemplazo.
ahora, probablemente no querrá volver a escribir la
measure == ''exit''
todo el tiempo ... por lo que puede crear un vector índice que contenga esa selección y usarlo en las funciones anteriores.
#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 ) )
puntos de referencia
# 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
En realidad, no veo ningún cambio en
dplyr
que haga esto mucho más fácil.
case_when
es ideal para cuando hay múltiples condiciones y resultados diferentes para una columna, pero no ayuda en este caso en el que desea cambiar varias columnas en función de una condición.
Del mismo modo,
recode
guarda la escritura si está reemplazando varios valores diferentes en una columna, pero no ayuda a hacerlo en varias columnas a la vez.
Finalmente,
mutate_at
, etc., solo aplica condiciones a los nombres de columna, no a las filas en el marco de datos.
Potencialmente, podría escribir una función para mutate_at que lo haría, pero no puedo entender cómo haría que se comportara de manera diferente para diferentes columnas.
Dicho esto, así es como lo abordaría usando
tidyr
form
tidyr
y
map
from
purrr
.
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)
Estas soluciones (1) mantienen la tubería, (2) no sobrescriben la entrada y (3) solo requieren que la condición se especifique una vez:
1a) mutate_cond
Crea una función simple para marcos de datos o tablas de datos que se pueden incorporar a las canalizaciones.
Esta función es como
mutate
pero solo actúa en las filas que satisfacen la condición:
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
Esta es una función alternativa para marcos de datos o tablas de datos que nuevamente es como
mutate
pero solo se usa dentro de
group_by
(como en el ejemplo a continuación) y solo opera en el último grupo en lugar de todos los grupos.
Tenga en cuenta que TRUE> FALSE, por lo que si
group_by
especifica una condición,
mutate_last
solo funcionará en filas que cumplan esa condición.
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) condición de
factorización Factoriza la condición convirtiéndola en una columna adicional que luego se elimina.
Luego use
ifelse
,
replace
o aritmética con lógicas como se ilustra.
Esto también funciona para tablas de datos.
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
Podríamos usar la
update
SQL a través del paquete sqldf en la tubería para marcos de datos (pero no tablas de datos a menos que los convirtamos, esto puede representar un error en dplyr. Consulte el
problema 1579 de dplyr
).
Puede parecer que estamos modificando indeseablemente la entrada en este código debido a la existencia de la
update
pero de hecho la
update
está actuando en una copia de la entrada en la base de datos generada temporalmente y no en la entrada real.
library(sqldf)
DF %>%
do(sqldf(c("update ''.''
set ''qty.exit'' = qty, cf = 0, ''delta.watts'' = 13
where measure = ''exit''",
"select * from ''.''")))
Nota 1:
Usamos esto como
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))
Nota 2: El problema de cómo especificar fácilmente la actualización de un subconjunto de filas también se discute en los problemas dplyr 134 , 631 , 1518 y 1573 siendo 631 el hilo principal y 1573 una revisión de las respuestas aquí.
Me encontré con esto y realmente me gusta
mutate_cond()
de @G.
Grothendieck, pero pensó que podría ser útil manejar también nuevas variables.
Entonces, a continuación tiene dos adiciones:
Sin relación: la segunda última línea hizo un poco más
dplyr
usando
filter()
Tres nuevas líneas al principio obtienen nombres de variables para usar en
mutate()
e inicializan cualquier variable nueva en el marco de datos antes de que ocurra
mutate()
.
Las nuevas variables se inicializan para el resto del
data.frame
usando
new_init
, que se establece en falta (
NA
) como valor predeterminado.
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
}
Aquí hay algunos ejemplos que usan los datos del iris:
Cambie
Petal.Length
a 88 donde
Species == "setosa"
.
Esto funcionará tanto en la función original como en esta nueva versión.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88)
Igual que el anterior, pero también crea una nueva variable
x
(
NA
en filas no incluidas en la condición).
No es posible antes.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE)
Igual que el anterior, pero las filas no incluidas en la condición para
x
se establecen en FALSO.
iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)
Este ejemplo muestra cómo
new_init
se puede establecer en una
list
para inicializar múltiples variables nuevas con diferentes valores.
Aquí, se crean dos nuevas variables con filas excluidas que se inicializan usando valores diferentes (
x
inicializado como
FALSE
,
y
como
NA
)
iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
x = TRUE, y = Sepal.Length ^ 2,
new_init = list(FALSE, NA))
Puede dividir el conjunto de datos y hacer una llamada de mutación regular en la parte
TRUE
.
dplyr 0.8
presenta la función
group_split
que se divide por grupos (y los grupos se pueden definir directamente en la llamada), así que la usaremos aquí, pero
base::split
funciona.
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
Si el orden de las filas es importante, use
tibble::rowid_to_column
, luego
dplyr::arrange
en
rowid
y selecciónelo al final.
datos
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)
Puede hacer esto con la
magrittr
de
magrittr
%<>%
:
library(dplyr)
library(magrittr)
dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
cf = 0,
delta.watts = 13)
Esto reduce la cantidad de escritura, pero sigue siendo mucho más lento que
data.table
.
Una solución concisa sería hacer la mutación en el subconjunto filtrado y luego volver a agregar las filas sin salida de la tabla:
library(dplyr)
dt %>%
filter(measure == ''exit'') %>%
mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
rbind(dt %>% filter(measure != ''exit''))
mutate_cond es una gran función, pero da un error si hay un NA en la columna (s) utilizada para crear la condición. Siento que una mutación condicional simplemente debería dejar esas filas en paz. Esto coincide con el comportamiento de filter (), que devuelve filas cuando la condición es VERDADERA, pero omite ambas filas con FALSO y NA.
Con este pequeño cambio, la función funciona de maravilla:
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
}