dplyr mutate / replace en un subconjunto de filas

Estoy en el proceso de probar un flujo de trabajo basado en dplyr (en lugar de usar principalmente data.table, que estoy acostumbrado), y me he encontrado con un problema que no puedo encontrar una solución dplyr equivalente a . Normalmente me encuentro con el escenario en el que necesito actualizar / reemplazar condicionalmente varias columnas en base a una única 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)] 

¿Hay una solución simple de Dplyr para este mismo problema? Me gustaría evitar el uso de 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!

Estas soluciones (1) mantienen la canalización, (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 tuberías. 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 una vez más es mutate pero solo se utiliza dentro de group_by (como en el ejemplo a continuación) y solo opera en el último grupo en lugar de cada grupo. Tenga en cuenta que VERDADERO> FALSO, entonces si group_by especifica una condición, mutate_last solo operará en filas que satisfagan 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 Factorizar la condición convirtiéndola en una columna adicional que luego se elimina. Luego use ifelse , replace o arithmetic con logicals 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 los marcos de datos (pero no tablas de datos a menos que los conviertamos, esto puede representar un error en dplyr. Ver dplyr problema 1579 ). 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 analiza en los números de dplyr 134 , 631 , 1518 y 1573, siendo 631 el hilo principal y 1573 una revisión de las respuestas aquí.

Puedes 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 tipeo, pero aún es mucho más lento que data.table .

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) ) 

que es bastante legible, aunque puede no ser tan eficiente como podría ser.

Me encontré con esto y realmente me gusta mutate_cond() por @G. Grothendieck, pero pensó que podría ser útil manejar también nuevas variables. Entonces, a continuación tiene dos adiciones:

No relacionado: la segunda última línea hizo un poco más dplyr usando el filter()

Tres nuevas líneas al principio obtienen nombres de variables para usar en mutate() e inicializa cualquier variable nueva en el dataframe antes de que se produzca mutate() . Las nuevas variables se inicializan para el rest del data.frame usando new_init , que está configurado como faltante ( 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:

Cambia 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 arriba, 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) 

Lo mismo que arriba, 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 valores diferentes. Aquí, se crean dos nuevas variables con filas excluidas que se inicializan utilizando diferentes valores ( 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)) 

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 versus dplyr usando pass-by-value. dplyr requiere el uso de ifelse() en todo el vector, mientras que DT hará el subconjunto y actualizará por referencia (devolviendo el DT completo). Entonces, para este ejercicio, DT será sustancialmente más rápido.

De manera alternativa, puede subconjuntar primero, luego actualizar y finalmente recombinarse:

 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 

mutate_cond es una gran función, pero da un error si hay una NA en la (s) columna (s) utilizada (s) para crear la condición. Siento que un mutate condicional simplemente debería dejar esas filas en paz. Esto coincide con el comportamiento de filter (), que devuelve filas cuando la condición es TRUE, pero omite ambas filas con FALSE y NA.

Con este pequeño cambio, la función funciona como un encanto:

 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 } 

Con la creación de rlang , es posible una versión ligeramente modificada del ejemplo 1a de Grothendieck, eliminando la necesidad del argumento de envir , ya que enquo() captura el entorno en el que se crea .p forma automática.

 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) 

A expensas de romper con la syntax dplyr habitual, puede usar desde la base:

 dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'], delta.watts[measure == 'exit'] <- 13) 

Parece que se integra bien con la tubería, y puedes hacer prácticamente todo lo que quieras dentro de ella.