dplyr muta / reemplaza varias columnas en un subconjunto de filas

85

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 para el que no puedo encontrar una solución dplyr equivalente . Comúnmente me encuentro con el escenario en el que 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 simple dplyr 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!

Chris Newton
fuente

Respuestas:

81

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í.

G. Grothendieck
fuente
1
Excelente respuesta, gracias! Tu mutate_cond y el mutate_when de @Kevin Ushey son buenas soluciones a este problema. Creo que tengo una ligera preferencia por la legibilidad / flexibilidad de mutate_when, pero le daré a esta respuesta la "verificación" de minuciosidad.
Chris Newton
Realmente me gusta el enfoque mutate_cond. También me parece que esta función o algo muy cercano a ella merece su inclusión en dplyr y sería una mejor solución que VectorizedSwitch (que se discute en github.com/hadley/dplyr/issues/1573 ) para el caso de uso que la gente está pensando por aquí ...
Magnus
Me encanta mutate_cond. Las diversas opciones deberían haber sido respuestas independientes.
Holger Brandl
Han pasado un par de años y los problemas de github parecen cerrados y bloqueados. ¿Existe una solución oficial a este problema?
static_rtti
27

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.

eipi10
fuente
En realidad, ahora que tuve la oportunidad de probar esto, preferiría una solución que evite la necesidad de crear subconjuntos usando la notación dt [dt $ Measure == 'exit',], ya que eso puede volverse difícil de manejar con más dt nombres.
Chris Newton
Solo un FYI, pero esta solución solo funcionará si data.frame/ tibbleya contiene la columna definida por mutate. No funcionará si está intentando agregar una nueva columna, por ejemplo, la primera vez que ejecuta un bucle y modifica un archivo data.frame.
Ursus Frost
@UrsusFrost agregar una nueva columna que es solo un subconjunto del conjunto de datos me parece extraño. ¿Agrega NA a filas que no están subconjuntadas?
Baraliuh
@Baraliuh Sí, puedo apreciar eso. Es parte de un ciclo en el que incremento y agrego datos sobre una lista de fechas. Las primeras fechas deben tratarse de manera diferente a las fechas posteriores, ya que están replicando procesos comerciales del mundo real. En iteraciones posteriores, dependiendo de las condiciones de las fechas, los datos se calculan de manera diferente. Debido a la condicionalidad, no quiero cambiar inadvertidamente fechas anteriores en el data.frame. FWIW, simplemente volví a usar en data.tablelugar de dplyrporque su iexpresión maneja esto fácilmente, además, el ciclo general se ejecuta mucho más rápido.
Ursus Frost
18

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.

Kevin Ushey
fuente
14

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 deifelse() 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   b
Alex W
fuente
10

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 que mutate()ocurra. Las nuevas variables se inicializan durante el resto del data.frameuso new_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 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( 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 a listpara inicializar múltiples variables nuevas con valores diferentes. Aquí, se crean dos nuevas variables con filas excluidas que se inicializan con valores diferentes ( xinicializados como FALSE, ycomo NA)

iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
                  x = TRUE, y = Sepal.Length ^ 2,
                  new_init = list(FALSE, NA))
Simon Jackson
fuente
Su 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.
RHA
¿Ha puesto esto en una biblioteca o lo ha formalizado como una función? Parece una obviedad, especialmente con todas las mejoras.
Ortiga
1
No. Creo que el mejor enfoque con dplyr en este momento es combinar mutate con if_elseo case_when.
Simon Jackson
¿Puede proporcionar un ejemplo (o enlace) a este enfoque?
Ortiga
6

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
}
Magnus
fuente
¡Gracias Magnus! Estoy usando esto para actualizar una tabla que contiene acciones y tiempos para todos los objetos que componen una animación. Llegué al problema de NA porque los datos son tan variados que algunas acciones no tienen sentido para algunos objetos, así que tengo NA en esas celdas. El otro mutate_cond de arriba se bloqueó, pero su solución funcionó a la perfección.
Phil van Kleur
Si esto te resulta útil, esta función está disponible en un pequeño paquete que escribí, "zulutils". No está en CRAN, pero puede instalarlo usando remotos :: install_github ("torfason / zulutils")
Magnus
¡Excelente! Muchas gracias. Todavía lo estoy usando.
Phil van Kleur
4

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, se recodeahorra escribir 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 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 nestform tidyry mapfrom 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()
ver24
fuente
1
Lo único que sugiero es usar nest(-measure)para evitar elgroup_by
Dave Gruenewald
Editado para reflejar la sugerencia de @DaveGruenewald
24
4

Una 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'))
Bob Zimmermann
fuente
3

Con la creación de rlang, es posible una versión ligeramente modificada del ejemplo 1a de Grothendieck, eliminando la necesidad del envirargumento, ya que enquo()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)
Davis Vaughan
fuente
2

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én base::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.000000000

Si el orden de las filas es importante, utilícelo tibble::rowid_to_columnprimero, luego dplyr::arrangeenciéndalo rowidy 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)
Moody_Mudskipper
fuente
2

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 subconjuntos qty ... 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   100
Wimpel
fuente
1

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.

Jan Hlavacek
fuente
Esto no funciona como está escrito porque la segunda tarea en realidad no ocurre. Pero si lo hace, dt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })entonces funciona
24 de