wake-up-neo.net

dplyr mutiert/ersetzt in einer Teilmenge von Zeilen

Ich bin gerade dabei, einen auf Dplyr basierenden Workflow auszuprobieren (und benutze meistens nicht die Daten.tabelle, die ich gewohnt bin), und ich bin auf ein Problem gestoßen, für das ich keine entsprechende Dplyr-Lösung finde . Ich stelle häufig das Szenario vor, in dem ich mehrere Spalten basierend auf einer einzigen Bedingung bedingt aktualisieren/ersetzen muss. Hier ist ein Beispielcode mit meiner Lösung 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)]

Gibt es eine einfache dplyr-Lösung für dasselbe Problem? Ich möchte die Verwendung von ifelse vermeiden, da ich die Bedingung nicht mehrmals eingeben muss - dies ist ein vereinfachtes Beispiel, aber es gibt manchmal viele Zuweisungen, die auf einer einzigen Bedingung basieren. 

Vielen Dank im Voraus für die Hilfe!

58
Chris Newton

Diese Lösungen (1) behalten die Pipeline bei, (2) do not überschreiben die Eingabe und (3) erfordern nur eine einmalige Bedingung:

1a) mutate_cond Erstellt eine einfache Funktion für Datenrahmen oder Datentabellen, die in Pipelines integriert werden können. Diese Funktion ist wie mutate, wirkt aber nur auf die Zeilen, die die Bedingung erfüllen:

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 Dies ist eine alternative Funktion für Datenrahmen oder Datentabellen, die wiederum mutate ist, jedoch nur innerhalb von group_by (wie im folgenden Beispiel) verwendet wird und nur für die letzte Gruppe und nicht für jede Gruppe gilt. Beachten Sie, dass TRUE> FALSE. Wenn group_by eine Bedingung angibt, wird mutate_last nur für Zeilen ausgeführt, die diese Bedingung erfüllen.

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) Bedingung ausfiltern Die Bedingung herausfiltern, indem eine zusätzliche Spalte erstellt wird, die später entfernt wird. Verwenden Sie dann ifelse, replace oder Arithmetik mit logischen Logiken, wie in der Abbildung dargestellt. Dies funktioniert auch für Datentabellen.

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 Wir könnten SQL update über das sqldf -Paket in der Pipeline für Datenrahmen verwenden (nicht jedoch Datentabellen, sofern wir diese nicht konvertieren. Dies kann einen Fehler in dplyr darstellen. Siehe dplyr issue 1579 ). Es kann den Anschein haben, dass wir die Eingabe in diesem Code unerwünscht ändern, da update vorhanden ist. Tatsächlich wirkt sich update jedoch auf eine Kopie der Eingabe in der temporär generierten Datenbank und nicht auf die tatsächliche Eingabe aus.

library(sqldf)

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

Note 1: Wir haben dies als DF verwendet.

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

Hinweis 2: Das Problem, wie das Aktualisieren einer Teilmenge von Zeilen einfach angegeben werden kann, wird auch in den Dplyr-Ausgaben 134 , 631 , 1518 und _ behandelt. 1573 mit 631 als Hauptthread und 1573 als Überprüfung der Antworten hier.

58
G. Grothendieck

Dies können Sie mit magrittrs bidirektionaler Pipe %<>% tun:

library(dplyr)
library(magrittr)

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

Dies reduziert die Anzahl der Texteingaben, ist jedoch immer noch viel langsamer als data.table.

18
eipi10

Hier ist eine Lösung, die ich mag:

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
}

Damit können Sie Dinge wie z.

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

das ist durchaus lesbar - obwohl es nicht so performant ist, wie es sein könnte.

15
Kevin Ushey

Wie eipi10 oben zeigt, gibt es keine einfache Möglichkeit, eine Teilmenge in dplyr zu ersetzen, da DT Pass-by-Reference-Semantik gegen Dplyr mit Pass-by-Value verwendet. dplyr erfordert die Verwendung von ifelse() für den gesamten Vektor, wohingegen DT die Teilmenge ausführt und anhand des Verweises aktualisiert wird (wobei das gesamte DT zurückgegeben wird). Für diese Übung ist DT also wesentlich schneller.

Sie können alternativ zuerst einen Teilsatz erstellen, dann aktualisieren und schließlich rekombinieren:

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

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

Aber DT wird wesentlich schneller sein: (Für die neue Antwort von 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

Ich bin gerade darüber gestolpert und mag mutate_cond() von @G wirklich. Grothendieck, hielt es jedoch für nützlich, auch mit neuen Variablen umzugehen. Im Folgenden gibt es zwei Ergänzungen:

Nicht verbunden: Die vorletzte Zeile wurde mit filter() etwas mehr dplyr erstellt.

Drei neue Zeilen am Anfang erhalten Variablennamen zur Verwendung in mutate() und initialisieren alle neuen Variablen im Datenrahmen, bevor mutate() auftritt. Neue Variablen werden für den Rest des data.frame mit new_init initialisiert, der standardmäßig als fehlend (NA) festgelegt ist.

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
}

Hier einige Beispiele für die Verwendung der Iris-Daten:

Ändern Sie Petal.Length in 88, wo Species == "setosa". Dies funktioniert sowohl in der ursprünglichen Funktion als auch in dieser neuen Version.

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

Wie oben, aber erstellen Sie auch eine neue Variable x (NA in Zeilen, die nicht in der Bedingung enthalten sind). Vorher nicht möglich.

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

Wie oben, aber Zeilen, die nicht in der Bedingung für x enthalten sind, werden auf FALSE gesetzt. 

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

Dieses Beispiel zeigt, wie new_init auf eine list gesetzt werden kann, um mehrere neue Variablen mit unterschiedlichen Werten zu initialisieren. Hier werden zwei neue Variablen erstellt, wobei ausgeschlossene Zeilen mit unterschiedlichen Werten initialisiert werden (x initialisiert als FALSE, y als 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 ist eine großartige Funktion, gibt jedoch einen Fehler aus, wenn in den Spalten, die zum Erstellen der Bedingung verwendet werden, eine NA vorhanden ist. Ich denke, ein bedingter Mutat sollte solche Reihen einfach in Ruhe lassen. Dies entspricht dem Verhalten von filter (), das Zeilen zurückgibt, wenn die Bedingung TRUE ist, aber beide Zeilen mit FALSE und NA weglassen.

Mit dieser kleinen Änderung funktioniert die Funktion wie ein Zauber:

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

Mit der Erstellung von rlang ist eine geringfügig modifizierte Version von Grothendieks Beispiel 1a möglich, wodurch das Argument envir nicht mehr erforderlich ist, da enquo() die Umgebung erfasst, in der .p automatisch erstellt wird.

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

Ich sehe keine Änderungen an dplyr, die das wesentlich erleichtern würden. case_when ist ideal, wenn für eine Spalte mehrere unterschiedliche Bedingungen und Ergebnisse vorliegen. In diesem Fall, in dem Sie mehrere Spalten basierend auf einer Bedingung ändern möchten, ist dies nicht hilfreich. Ähnlich erspart recode die Eingabe, wenn Sie mehrere verschiedene Werte in einer Spalte ersetzen, dies jedoch nicht in mehreren Spalten gleichzeitig hilft. Schließlich gelten mutate_at usw. nur für die Spaltennamen und nicht für die Zeilen im Datenrahmen. Sie könnten möglicherweise eine Funktion für mutate_at schreiben, die dies tun würde, aber ich kann nicht herausfinden, wie Sie es für verschiedene Spalten unterschiedlich machen würde. 

Das ist hier gesagt, wie ich es mit nest form tidyr und map von purrr ansprechen würde. 

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

Sie könnten die Datenmenge aufteilen und einen regelmäßigen mutierten Aufruf für den Teil TRUE ausführen:

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

Wenn die Reihenfolge der Zeilen von Bedeutung ist, verwenden Sie zuerst tibble::rowid_to_column, dann dplyr::arrange für rowid und wählen Sie sie am Ende aus.

In Bezug auf neue Dplyr-Entwicklungen gibt es die Funktion group_split, die Sie in der Entwicklungsversion finden können, siehe this issue , sie wird nach Gruppen aufgeteilt und kann sich auch im laufenden Betrieb gruppieren, der Code ist Hier .

daten

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

Ich denke, diese Antwort wurde bisher nicht erwähnt. Es läuft fast so schnell wie die "Default" data.table- Lösung.

Verwenden Sie 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 ersetzt den Ersetzungswert. Wenn Sie also die Werte der Spalten qty in Spalten qty.exit eingeben möchten, müssen Sie auch qty als Teilmenge festlegen ... daher der qty[ measure == 'exit'] in der ersten Ersetzung.

jetzt möchten Sie wahrscheinlich measure == 'exit' nicht ständig erneut eingeben ... Sie können also einen Indexvektor erstellen, der diese Auswahl enthält, und ihn in den oben genannten Funktionen verwenden.

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

Benchmarks

# 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

Auf Kosten der Bruch mit der üblichen Dplyr-Syntax können Sie within von der Basis aus verwenden:

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

Es scheint sich gut in die Pipe zu integrieren, und Sie können so ziemlich alles tun, was Sie möchten.

1
Jan Hlavacek

Eine prägnante Lösung wäre, die Mutation in der gefilterten Teilmenge auszuführen und dann die Nicht-Exit-Zeilen der Tabelle wieder hinzuzufügen:

library(dplyr)

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