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 similar mutatepero solo actúa en las filas que cumplen 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 similar mutatepero solo se usa dentro group_by(como en el ejemplo a continuación) y solo opera en el último grupo en lugar de en cada grupo. Tenga en cuenta que TRUE> FALSE, por lo que si group_byespecifica una condición mutate_last, solo funcionará en las 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) factorizar la condición Factorizar la condición convirtiéndola en una columna adicional que luego se elimina. Luego use ifelse, replaceo 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 SQL a updatetravés del paquete sqldf en la canalización para marcos de datos (pero no tablas de datos a menos que las convirtamos; esto puede representar un error en dplyr. Consulte el número 1579 de dplyr ). Puede parecer que estamos modificando indeseablemente la entrada en este código debido a la existencia de updatepero, de hecho, updateestá actuando sobre una copia de la entrada en la base de datos generada temporalmente y no sobre la entrada real.
library(sqldf)
DF %>%
do(sqldf(c("update '.'
set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13
where measure = 'exit'",
"select * from '.'")))
4) row_case_when También consulte row_case_whendefinido en
Devolver un tibble: ¿cómo vectorizar con case_when? . Utiliza una sintaxis similar case_whenpero se aplica a las filas.
library(dplyr)
DF %>%
row_case_when(
measure == "exit" ~ data.frame(qty.exit = qty, cf = 0, delta.watts = 13),
TRUE ~ data.frame(qty.exit, cf, delta.watts)
)
Nota 1: utilizamos esto comoDF
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 cuestiones dplyr 134 , 631 , 1518 y 1573 con 631 siendo el hilo principal y 1573 siendo un examen de las respuestas aquí.
Puede hacer esto con
magrittrla tubería de dos vías%<>%: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.fuente
data.frame/tibbleya contiene la columna definida pormutate. No funcionará si está intentando agregar una nueva columna, por ejemplo, la primera vez que ejecuta un bucle y modifica un archivodata.frame.data.frame. FWIW, simplemente volví a usar endata.tablelugar dedplyrporque suiexpresión maneja esto fácilmente, además, el ciclo general se ejecuta mucho más rápido.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 que no sea tan eficaz como podría ser.
fuente
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 frente a dplyr usando paso por valor. 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.Alternativamente, podría crear un 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 bfuente
Me encontré con esto y realmente me gustó
mutate_cond()@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 se hizo un poco más
dplyrusandofilter()Tres nuevas líneas al principio obtienen nombres de variables para su uso
mutate()e inicializan cualquier nueva variable en el marco de datos antes de quemutate()ocurra. Las nuevas variables se inicializan durante el resto deldata.frameusonew_init, que se establece en missing (NA) de forma predeterminada.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 }A continuación, se muestran algunos ejemplos que utilizan los datos del iris:
Cambie
Petal.Lengtha 88 dondeSpecies == "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(NAen filas no incluidas en la condición). No era 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
xse establecen en FALSE.iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)Este ejemplo muestra cómo
new_initse puede establecer en alistpara inicializar múltiples variables nuevas con valores diferentes. Aquí, se crean dos nuevas variables con filas excluidas que se inicializan con valores diferentes (xinicializados comoFALSE,ycomoNA)iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5, x = TRUE, y = Sepal.Length ^ 2, new_init = list(FALSE, NA))fuente
mutate_condfunción arroja un error en mi conjunto de datos y la función de Grothendiecks no.Error: incorrect length (4700), expecting: 168Parece estar relacionado con la función de filtro.if_elseocase_when.mutate_cond es una gran función, pero da un error si hay un NA en las columnas usadas para crear la condición. Siento que un mutado 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 a las mil maravillas:
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 }fuente
En realidad, no veo ningún cambio en
dplyreso que lo haría mucho más fácil.case_whenes ideal para cuando hay varias 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, serecodeahorra escribir si está reemplazando varios valores diferentes en una columna, pero no ayuda a hacerlo en varias columnas a la vez. Finalmente,mutate_atetc. solo aplican condiciones a los nombres de las columnas, no a las filas del 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, aquí es cómo lo abordaría usando
nestformtidyrymapfrompurrr.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()fuente
nest(-measure)para evitar elgroup_byUna solución concisa sería hacer la mutación en el subconjunto filtrado y luego volver a agregar las filas que no son de salida de la tabla:
library(dplyr) dt %>% filter(measure == 'exit') %>% mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>% rbind(dt %>% filter(measure != 'exit'))fuente
Con la creación de
rlang, es posible una versión ligeramente modificada del ejemplo 1a de Grothendieck, eliminando la necesidad delenvirargumento, ya queenquo()captura el entorno que.pse crea automáticamente.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)fuente
Puede dividir el conjunto de datos y hacer una llamada mutada regular en la
TRUEpieza.dplyr 0.8 presenta la función
group_splitque se divide por grupos (y los grupos se pueden definir directamente en la llamada), así que la usaremos aquí, pero tambiénbase::splitfunciona.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.000000000Si el orden de las filas es importante, utilícelo
tibble::rowid_to_columnprimero, luegodplyr::arrangeenciéndalorowidy 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)fuente
Creo que esta respuesta no se ha mencionado antes. Funciona casi tan rápido como la
data.tablesolución "predeterminada" .Utilizar
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 ) )reemplazar recicla el valor de reemplazo, por lo que cuando desee que los valores de las columnas se
qtyingresen en columnasqty.exit, también tenga que subconjuntosqty... de ahí elqty[ measure == 'exit']el primer reemplazo ...ahora, probablemente no querrá volver a escribir el
measure == 'exit'todo el tiempo ... así que puede crear un vector de í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 100fuente
A expensas de romper con la sintaxis habitual de dplyr, puede usar
withindesde la base:dt %>% within(qty.exit[measure == 'exit'] <- qty[measure == 'exit'], delta.watts[measure == 'exit'] <- 13)Parece integrarse bien con la tubería y puedes hacer prácticamente todo lo que quieras dentro de ella.
fuente
dt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })entonces funciona