web-dev-qa-db-fra.com

dplyr mute/replace sur un sous-ensemble de lignes

Je suis en train d'essayer un flux de travail basé sur dplyr (plutôt que d'utiliser principalement data.table, auquel je suis habitué), et j'ai rencontré un problème pour lequel je ne trouve pas de solution dplyr équivalente. . Je rencontre couramment le scénario dans lequel je dois mettre à jour/remplacer conditionnellement plusieurs colonnes en fonction d'une seule condition. Voici un exemple de code, avec ma solution 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-t-il une solution simple à ce problème avec Dplyr? J'aimerais éviter d'utiliser ifelse car je ne souhaite pas avoir à taper la condition plusieurs fois. Il s'agit d'un exemple simplifié, mais il y a parfois de nombreuses assignations basées sur une seule condition. 

Merci d'avance pour l'aide!

58
Chris Newton

Ces solutions (1) maintiennent le pipeline, (2) remplacent pas l'entrée et (3) exigent seulement que la condition soit spécifiée une fois:

1a) mutate_cond Crée une fonction simple pour les blocs de données ou les tables de données pouvant être incorporés dans des pipelines. Cette fonction ressemble à mutate mais n'agit que sur les lignes satisfaisant la condition:

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 Il s'agit d'une fonction alternative pour les trames de données ou les tables de données qui ressemble encore à mutate mais qui n'est utilisée que dans group_by (comme dans l'exemple ci-dessous) et ne fonctionne que sur le dernier groupe plutôt que sur chaque groupe. Notez que TRUE> FALSE donc si group_by spécifie une condition, mutate_last ne fonctionnera que sur les lignes satisfaisant cette condition.

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) factoriser la condition Éliminer la condition en faisant une colonne supplémentaire qui sera supprimée par la suite. Ensuite, utilisez ifelse, replace ou l'arithmétique avec des logiques comme illustré. Cela fonctionne également pour les tables de données.

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 Nous pourrions utiliser SQL update via le paquetage sqldf dans le pipeline pour les trames de données (mais pas les tables de données à moins de les convertir - cela peut représenter un bogue dans dplyr. Voir numéro 1579 de dplyr . Il peut sembler que nous modifions de manière indésirable l'entrée dans ce code en raison de l'existence de la variable update, mais en réalité, la update agit sur une copie de l'entrée dans la base de données générée temporairement et non sur l'entrée réelle.

library(sqldf)

DF %>% 
   do(sqldf(c("update '.' 
                 set 'qty.exit' = qty, cf = 0, 'delta.watts' = 13 
                 where measure = 'exit'", 
              "select * from '.'")))

Note 1: Nous l'avons utilisé comme 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))

Note 2: Le problème de savoir comment spécifier facilement la mise à jour d'un sous-ensemble de lignes est également traité dans dplyr issues 134 , 631 , 1518 et 1573 avec 631 étant le fil principal et 1573 étant une synthèse des réponses ici.

58
G. Grothendieck

Vous pouvez le faire avec le tube à deux voies magrittr_ %<>%:

library(dplyr)
library(magrittr)

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

Cela réduit la quantité de frappe, mais reste beaucoup plus lent que data.table.

18
eipi10

Voici une solution que j'aime bien:

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
}

Il vous permet d’écrire des choses comme par exemple.

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

ce qui est assez lisible - même s’il n’est peut-être pas aussi performant qu’il pourrait l’être.

15
Kevin Ushey

Comme eipi10 le montre ci-dessus, il n’existe pas de moyen simple de remplacer un sous-ensemble dans dplyr, car DT utilise la sémantique passage par référence vs dplyr en utilisant passage par valeur. dplyr nécessite l’utilisation de ifelse() sur l’ensemble du vecteur, alors que DT fera le sous-ensemble et le mettra à jour par référence (en renvoyant l’ensemble du DT). Donc, pour cet exercice, DT sera beaucoup plus rapide.

Vous pouvez également sous-définir d'abord, puis mettre à jour et enfin recombiner:

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

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

Mais DT va être beaucoup plus rapide: (Modifié pour utiliser la nouvelle réponse 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
11
Alex W

Je suis tombé sur ça et j'aime beaucoup mutate_cond() by @G. Grothendieck, mais pensait qu'il serait peut-être utile de gérer également de nouvelles variables. Donc, ci-dessous a deux ajouts:

Non lié: la dernière dernière ligne est un peu plus dplyr en utilisant filter()

Trois nouvelles lignes au début reçoivent les noms de variables à utiliser dans mutate() et initialisent toutes les nouvelles variables dans le bloc de données avant que mutate() ne se produise. Les nouvelles variables sont initialisées pour le reste de data.frame à l'aide de new_init, défini comme manquant (NA) par défaut.

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
}

Voici quelques exemples utilisant les données de l'iris:

Remplacez Petal.Length par 88, où Species == "setosa". Cela fonctionnera dans la fonction d'origine ainsi que dans cette nouvelle version.

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

Comme ci-dessus, mais créez également une nouvelle variable x (NA dans des lignes non incluses dans la condition). Pas possible avant.

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

Comme ci-dessus, mais les lignes non incluses dans la condition de x sont définies sur FALSE. 

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

Cet exemple montre comment new_init peut être défini sur une list pour initialiser plusieurs nouvelles variables avec des valeurs différentes. Ici, deux nouvelles variables sont créées, les lignes exclues étant initialisées à l'aide de valeurs différentes (x initialisé à FALSE, y à NA).

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

mutate_cond est une excellente fonction, mais elle génère une erreur s'il y a un NA dans la ou les colonnes utilisées pour créer la condition. Je pense qu'un mutant conditionnel devrait simplement laisser de telles lignes. Cela correspond au comportement de filter (), qui renvoie des lignes lorsque la condition est VRAI, mais omet les deux lignes avec FALSE et NA.

Avec cette petite modification, la fonction fonctionne à merveille:

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
}
4
Magnus

Avec la création de rlang, une version légèrement modifiée de l'exemple 1a de Grothendieck est possible, éliminant le besoin de l'argument envir, car enquo() capture l'environnement dans lequel .p est créé automatiquement.

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)
3
Davis Vaughan

En fait, je ne vois aucun changement dans dplyr qui rendrait cela beaucoup plus facile. case_when est idéal pour les conditions et les résultats multiples pour une colonne, mais ne vous aide pas dans le cas où vous souhaitez modifier plusieurs colonnes en fonction d'une condition. De même, recode enregistre la saisie si vous remplacez plusieurs valeurs différentes dans une colonne mais ne vous aide pas à le faire dans plusieurs colonnes à la fois. Enfin, mutate_at, etc., n'appliquent que des conditions aux noms de colonne et non aux lignes du cadre de données. Vous pourriez potentiellement écrire une fonction pour mutate_at qui le ferait, mais je ne peux pas comprendre comment vous le feriez se comporter différemment pour différentes colonnes. 

Ceci étant dit, voici comment je l'aborderais en utilisant nest forme tidyr et map à partir de 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()
3
see24

Vous pouvez scinder le jeu de données et effectuer un appel de mutation régulier sur la partie TRUE:

library(tidyverse)
df1 %>%
  split(.,.$measure == "exit") %>%
  modify_at("TRUE",~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 l'ordre des lignes est important, utilisez d'abord tibble::rowid_to_column, puis dplyr::arrange sur rowid et sélectionnez-le à la fin.

En ce qui concerne les nouveaux développements de dplyr, il existe la fonction group_split que vous pouvez trouver dans la version de développement, voir ce problème , il sera divisé en groupes et pourra également être groupé à la volée, le code est ici .

les données

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),
                 stringsAsFactors = F)
2
Moody_Mudskipper

Je pense que cette réponse n'a pas été mentionnée auparavant. Il fonctionne presque aussi vite que la solution 'par défaut' data.table-.

Utilisez 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 recycle la valeur de remplacement. Ainsi, lorsque vous souhaitez entrer les valeurs des colonnes qty dans colums qty.exit, vous devez également sous-définir qty ... d'où le qty[ measure == 'exit'] dans le premier remplacement.

maintenant, vous ne voudrez probablement pas retaper le measure == 'exit' tout le temps ... afin de pouvoir créer un vecteur-index contenant cette sélection et l'utiliser dans les fonctions ci-dessus.

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

repères

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

Au détriment de la syntaxe habituelle dplyr, vous pouvez utiliser within à partir de la base:

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

Il semble bien s’intégrer au tuyau et vous pouvez y faire à peu près tout ce que vous voulez.

1
Jan Hlavacek

Une solution concise consisterait à effectuer la mutation sur le sous-ensemble filtré, puis à rajouter les lignes non sortantes du tableau:

library(dplyr)

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