dplyr muta / sostituisce diverse colonne su un sottoinsieme di righe


86

Sono in procinto di provare un flusso di lavoro basato su dplyr (piuttosto che utilizzare principalmente data.table, a cui sono abituato), e ho riscontrato un problema a cui non riesco a trovare una soluzione dplyr equivalente . Di solito mi imbatto nello scenario in cui ho bisogno di aggiornare / sostituire in modo condizionale diverse colonne in base a una singola condizione. Ecco un po 'di codice di esempio, con la mia soluzione 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)]

Esiste una semplice soluzione dplyr a questo stesso problema? Vorrei evitare di utilizzare ifelse perché non voglio dover digitare la condizione più volte: questo è un esempio semplificato, ma a volte ci sono molte assegnazioni basate su una singola condizione.

Grazie in anticipo per l'aiuto!

Risposte:


83

Queste soluzioni (1) mantengono la pipeline, (2) non sovrascrivono l'input e (3) richiedono solo che la condizione sia specificata una volta:

1a) mutate_cond Crea una semplice funzione per frame di dati o tabelle di dati che possono essere incorporati nelle pipeline. Questa funzione è simile mutatema agisce solo sulle righe che soddisfano la condizione:

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 Questa è una funzione alternativa per i frame di dati o le tabelle di dati che di nuovo è simile mutatema viene utilizzata solo all'interno group_by(come nell'esempio sotto) e opera solo sull'ultimo gruppo piuttosto che su ogni gruppo. Notare che TRUE> FALSE quindi se group_byspecifica una condizione, mutate_lastfunzionerà solo sulle righe che soddisfano quella condizione.

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) Fattorizzare la condizione Fattorizzare la condizione rendendola una colonna aggiuntiva che verrà successivamente rimossa. Quindi usa ifelse, replaceo aritmetica con logiche come illustrato. Questo funziona anche per le tabelle di dati.

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 Potremmo usare SQL updatetramite il pacchetto sqldf nella pipeline per i frame di dati (ma non le tabelle di dati a meno che non li convertiamo - questo potrebbe rappresentare un bug in dplyr. Vedi dplyr problema 1579 ). Può sembrare che stiamo modificando indesiderabilmente l'input in questo codice a causa dell'esistenza del updatema in realtà updatesta agendo su una copia dell'input nel database generato temporaneamente e non sull'input effettivo.

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 Verifica anche row_case_whendefinita in Restituzione di una tabella: come vettorializzare con case_when? . Usa una sintassi simile a case_whenma si applica alle righe.

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: abbiamo usato questo fileDF

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: Il problema di come specificare facilmente l'aggiornamento di un sottoinsieme di righe è discusso anche nei numeri 134 , 631 , 1518 e 1573 di dplyr con 631 come thread principale e 1573 come revisione delle risposte qui.


1
Ottima risposta, grazie! Il tuo mutate_cond e mutate_when di @Kevin Ushey sono entrambe buone soluzioni a questo problema. Penso di avere una leggera preferenza per la leggibilità / flessibilità di mutate_when, ma darò a questa risposta il "controllo" per completezza.
Chris Newton

Mi piace molto l'approccio mutate_cond. Mi sembra che anche a me piaccia questa funzione o qualcosa di molto vicino ad essa merita l'inclusione in dplyr e sarebbe una soluzione migliore di VectorizedSwitch (che è discussa in github.com/hadley/dplyr/issues/1573 ) per il caso d'uso che le persone stanno pensando circa qui ...
Magnus

Adoro mutate_cond. Le varie opzioni avrebbero dovuto essere risposte separate.
Holger Brandl

Sono passati un paio d'anni e i problemi di GitHub sembrano chiusi e bloccati. Esiste una soluzione ufficiale a questo problema?
static_rtti

27

Puoi farlo con magrittril tubo a due vie di %<>%:

library(dplyr)
library(magrittr)

dt[dt$measure=="exit",] %<>% mutate(qty.exit = qty,
                                    cf = 0,  
                                    delta.watts = 13)

Ciò riduce la quantità di digitazione, ma è ancora molto più lento di data.table.


In realtà, ora che ho avuto la possibilità di testarlo, preferirei una soluzione che eviti la necessità di sottoinsiemi utilizzando la notazione dt [dt $ measure == 'exit',], poiché può diventare ingombrante con più tempo nomi dt.
Chris Newton

Solo un FYI, ma questa soluzione funzionerà solo se data.frame/ tibblecontiene già la colonna definita da mutate. Non funzionerà se stai cercando di aggiungere una nuova colonna, ad esempio, la prima volta che esegui un ciclo e modifichi un file data.frame.
Ursus Frost

@ UrsusFrost l'aggiunta di una nuova colonna che è solo un sottoinsieme del set di dati mi sembra strano. Aggiungete NA alle righe che non sono sottoposte a sottoinsiemi?
Baraliuh

@Baraliuh Sì, lo posso apprezzare. Fa parte di un ciclo in cui incremento e accodo i dati su un elenco di date. Le prime date devono essere trattate in modo diverso rispetto alle date successive in quanto replica i processi aziendali del mondo reale. In ulteriori iterazioni, a seconda delle condizioni delle date, i dati vengono calcolati in modo diverso. A causa della condizionalità, non voglio modificare inavvertitamente le date precedenti in data.frame. FWIW, sono appena tornato a usare data.tableinvece di dplyrperché la sua iespressione lo gestisce facilmente, inoltre il ciclo complessivo viene eseguito molto più velocemente.
Ursus Frost

19

Ecco una soluzione che mi piace:

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
}

Ti consente di scrivere cose come ad es

mtcars %>% mutate_when(
  mpg > 22,    list(cyl = 100),
  disp == 160, list(cyl = 200)
)

che è abbastanza leggibile, anche se potrebbe non essere così performante come potrebbe essere.


14

Come mostra eipi10 sopra, non c'è un modo semplice per sostituire un sottoinsieme in dplyr perché DT utilizza la semantica pass-by-reference rispetto a dplyr usando il valore pass-by. dplyr richiede l'uso diifelse() sull'intero vettore, mentre DT eseguirà il sottoinsieme e aggiornerà per riferimento (restituendo l'intero DT). Quindi, per questo esercizio, DT sarà sostanzialmente più veloce.

In alternativa, potresti prima creare un sottoinsieme, quindi aggiornare e infine ricombinare:

dt.sub <- dt[dt$measure == "exit",] %>%
  mutate(qty.exit= qty, cf= 0, delta.watts= 13)

dt.new <- rbind(dt.sub, dt[dt$measure != "exit",])

Ma DT sarà sostanzialmente più veloce: (modificato per utilizzare la nuova risposta di 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

10

Mi sono appena imbattuto in questo e mi piace molto mutate_cond() da @G. Grothendieck, ma ha pensato che potrebbe tornare utile anche per gestire nuove variabili. Quindi, di seguito ha due aggiunte:

Non correlato: la penultima riga ha fatto un po 'di più dplyr usandofilter()

Tre nuove righe all'inizio ottengono i nomi delle variabili da utilizzare mutate()e inizializzano tutte le nuove variabili nel frame di dati prima che si mutate()verifichi. Le nuove variabili vengono inizializzate per il resto data.framedell'utilizzo new_init, che è impostato su missing ( NA) come impostazione predefinita.

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
}

Di seguito sono riportati alcuni esempi che utilizzano i dati dell'iride:

Cambia Petal.Lengthin 88 dove Species == "setosa". Funzionerà nella funzione originale così come in questa nuova versione.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88)

Come sopra, ma crea anche una nuova variabile x( NAnelle righe non incluse nella condizione). Non era possibile prima.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE)

Come sopra, ma le righe non incluse nella condizione per xsono impostate su FALSE.

iris %>% mutate_cond(Species == "setosa", Petal.Length = 88, x = TRUE, new_init = FALSE)

Questo esempio mostra come new_initpuò essere impostato su a listper inizializzare più nuove variabili con valori diversi. Qui, vengono create due nuove variabili con righe escluse inizializzate utilizzando valori diversi ( xinizializzate come FALSE, ycome NA)

iris %>% mutate_cond(Species == "setosa" & Sepal.Length < 5,
                  x = TRUE, y = Sepal.Length ^ 2,
                  new_init = list(FALSE, NA))

La tua mutate_condfunzione genera un errore sul mio set di dati e la funzione di Grothendiecks no. Error: incorrect length (4700), expecting: 168Sembra essere correlato alla funzione di filtro.
RHA

Lo hai messo in una libreria o lo hai formalizzato come funzione? Sembra un gioco da ragazzi, in particolare con tutti i miglioramenti.
Ortica

1
No. Penso che l'approccio migliore con dplyr in questo momento sia combinare mutate con if_elseo case_when.
Simon Jackson

Potete fornire un esempio (o un collegamento) a questo approccio?
Ortica

6

mutate_cond è un'ottima funzione, ma restituisce un errore se è presente un NA nella colonna o nelle colonne utilizzate per creare la condizione. Ritengo che una modifica condizionale dovrebbe semplicemente lasciare tali righe da sole. Ciò corrisponde al comportamento di filter (), che restituisce righe quando la condizione è TRUE, ma omette entrambe le righe con FALSE e NA.

Con questa piccola modifica la funzione funziona come un fascino:

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
}

Grazie Magnus! Lo sto usando per aggiornare una tabella contenente azioni e tempi per tutti gli oggetti che compongono un'animazione. Ho incontrato il problema NA perché i dati sono così vari che alcune azioni non hanno senso per alcuni oggetti, quindi ho NA in quelle celle. L'altro mutate_cond sopra si è bloccato, ma la tua soluzione ha funzionato a meraviglia.
Phil van Kleur

Se questo ti è utile, questa funzione è disponibile in un piccolo pacchetto che ho scritto, "zulutils". Non è su CRAN ma puoi installarlo usando i telecomandi :: install_github ("torfason / zulutils")
Magnus

4

In realtà non vedo alcun cambiamento dplyrche lo renderebbe molto più semplice. case_whenè ottimo per quando ci sono più condizioni e risultati diversi per una colonna, ma non aiuta in questo caso in cui vuoi cambiare più colonne in base a una condizione. Allo stesso modo, recodesalva la digitazione se stai sostituendo più valori diversi in una colonna ma non aiuta a farlo in più colonne contemporaneamente. Finalmente,mutate_at ecc. Applicano solo condizioni ai nomi delle colonne e non alle righe nel dataframe. Potresti potenzialmente scrivere una funzione per mutate_at che lo farebbe, ma non riesco a capire come faresti a comportarsi diversamente per colonne diverse.

Detto questo, ecco come lo approccerei usando nestform tidyre 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()

1
L'unica cosa che suggerirei è di usare nest(-measure)per evitare ilgroup_by
Dave Gruenewald

Modificato per riflettere il suggerimento di
@DaveGruenewald

4

Una soluzione concisa sarebbe quella di eseguire la mutazione sul sottoinsieme filtrato e quindi aggiungere nuovamente le righe di non uscita della tabella:

library(dplyr)

dt %>% 
    filter(measure == 'exit') %>%
    mutate(qty.exit = qty, cf = 0, delta.watts = 13) %>%
    rbind(dt %>% filter(measure != 'exit'))

3

Con la creazione di rlang, è possibile una versione leggermente modificata dell'esempio 1a di Grothendieck, eliminando la necessità envirdell'argomento, poiché enquo()cattura l'ambiente che .pviene creato automaticamente.

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)

2

È possibile dividere il set di dati ed eseguire una normale chiamata di mutazione sulla TRUEparte.

dplyr 0.8 presenta la funzione group_splitche divide per gruppi (e i gruppi possono essere definiti direttamente nella chiamata) quindi lo useremo qui, ma base::splitfunziona anche.

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

Se l'ordine delle righe è importante, usa tibble::rowid_to_columnprima, poi dplyr::arrangesu rowide selezionalo alla fine.

dati

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)

2

Penso che questa risposta non sia stata menzionata prima. Funziona quasi alla stessa velocità della soluzione "predefinita" data.table..

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

sostituire ricicla il valore di sostituzione, quindi quando si desidera inserire i valori delle colonne qtynelle colonne qty.exit, è necessario anche un sottoinsieme qty ... da qui il qty[ measure == 'exit']nella prima sostituzione ..

ora, probabilmente non vorrai ridigitare measure == 'exit'sempre ... quindi puoi creare un vettore indice contenente quella selezione e usarlo nelle funzioni sopra.

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

punti di riferimenti

# 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

1

A scapito della rottura con la solita sintassi dplyr, puoi usare withindalla base:

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

Sembra integrarsi bene con il tubo e puoi fare praticamente tutto ciò che vuoi al suo interno.


Questo non funziona come scritto perché il secondo incarico non avviene effettivamente. Ma se lo fai, dt %>% within({ delta.watts[measure == 'exit'] <- 13 ; qty.exit[measure == 'exit'] <- qty[measure == 'exit'] ; cf[measure == 'exit'] <- 0 })allora funziona
vedi 24
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.