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!
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.
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
.
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.
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
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))
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
}
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)
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()
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 .
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)
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
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.
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'))