r data.table dplyr

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 }