Używaj dynamicznych nazw zmiennych w „dplyr”

168

Chcę użyć dplyr::mutate()do utworzenia wielu nowych kolumn w ramce danych. Nazwy kolumn i ich zawartość powinny być generowane dynamicznie.

Przykładowe dane z tęczówki:

library(dplyr)
iris <- tbl_df(iris)

Utworzyłem funkcję do mutowania moich nowych kolumn ze Petal.Widthzmiennej:

multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    df <- mutate(df, varname = Petal.Width * n)  ## problem arises here
    df
}

Teraz tworzę pętlę do budowania moich kolumn:

for(i in 2:5) {
    iris <- multipetal(df=iris, n=i)
}

Jednak ponieważ mutate myśli, że nazwa_zmiennej jest dosłowną nazwą zmiennej, pętla tworzy tylko jedną nową zmienną (nazywaną nazwa_zmiennej) zamiast czterech (nazywana płatkiem.2 - płatek.5).

Jak mogę mutate()użyć mojej dynamicznej nazwy jako nazwy zmiennej?

Timm S.
źródło
1
Nie nalegam na mutację, pytam, czy to możliwe. Może to tylko mała sztuczka, której nie znam. Jeśli jest inny sposób, posłuchajmy.
Timm S.
Uważam, że jest przestrzeń do obejrzenia w pakiecie lazyeval
Baptiste
1
W tym momencie dplyrma całą winietę na niestandardową ocenę
Gregor Thomas
16
Winieta nawet nie wspomina mutate_, a z innych funkcji naprawdę nie wynika, jak z niej korzystać.
nacnudus

Odpowiedzi:

191

Ponieważ dynamicznie budujesz nazwę zmiennej jako wartość znakową, bardziej sensowne jest wykonywanie przypisania przy użyciu standardowego indeksowania data.frame, które pozwala na wartości znakowe dla nazw kolumn. Na przykład:

multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    df[[varname]] <- with(df, Petal.Width * n)
    df
}

mutateFunkcja sprawia, że bardzo łatwo wymienić nowe kolumny za pośrednictwem nazwanych parametrów. Ale to zakłada, że ​​znasz nazwę po wpisaniu polecenia. Jeśli chcesz dynamicznie określić nazwę kolumny, musisz także zbudować nazwany argument.


wersja dplyr> = 0,7

Najnowsza wersja dplyr(0.7) robi to, używając :=do dynamicznego przypisywania nazw parametrów. Możesz zapisać swoją funkcję jako:

# --- dplyr version 0.7+---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    mutate(df, !!varname := Petal.Width * n)
}

Aby uzyskać więcej informacji, zobacz dostępny formularz dokumentacji vignette("programming", "dplyr").


dplyr (> = 0,3 i <0,7)

Nieco wcześniejsza wersja dplyr(> = 0.3 <0.7), zachęcała do stosowania alternatyw „standardowej oceny” dla wielu funkcji. Zobacz winietę oceny niestandardowej, aby uzyskać więcej informacji ( vignette("nse")).

Więc tutaj odpowiedź brzmi: użyj mutate_()zamiast mutate()i wykonaj:

# --- dplyr version 0.3-0.5---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    varval <- lazyeval::interp(~Petal.Width * n, n=n)
    mutate_(df, .dots= setNames(list(varval), varname))
}

dplyr <0,3

Zauważ, że jest to również możliwe w starszych wersjach, dplyrktóre istniały, gdy pierwotnie zadawano pytanie. Wymaga starannego użytkowania quotei setName:

# --- dplyr versions < 0.3 ---
multipetal <- function(df, n) {
    varname <- paste("petal", n , sep=".")
    pp <- c(quote(df), setNames(list(quote(Petal.Width * n)), varname))
    do.call("mutate", pp)
}
MrFlick
źródło
24
Dziękuję, to pomocne. przy okazji, zawsze tworzę naprawdę dramatyczne zmienne.
Timm S.
27
Hehe. to prawdopodobnie jedna z moich ulubionych literówek, które popełniłem od jakiegoś czasu. Myślę, że zostawię to.
MrFlick
1
do.call()prawdopodobnie nie robi tego, co myślisz, że robi: rpubs.com/hadley/do-call2 . Zobacz także winietę nse w wersji deweloperskiej programu dplyr.
Hadley
4
Więc jeśli rozumiem twój punkt @hadley, zaktualizowałem do.callpowyższe, aby używać do.call("mutate")i cytować dfna liście. Czy to właśnie sugerowałeś? A kiedy lazyevalwersja dplyrjest wydaną wersją, mutate_(df, .dots= setNames(list(~Petal.Width * n), varname))czy byłoby lepszym rozwiązaniem?
MrFlick
1
A jeśli potrzebuję nagłówka kolumny zmiennej nie tylko po lewej stronie przypisania, ale także po prawej stronie? np. mutate(df, !!newVar := (!!var1 + !!var2) / 2)nie działa :(
Mario Reutter
55

W nowej wersji dplyr( 0.6.0oczekiwanej w kwietniu 2017) możemy również wykonać przypisanie ( :=) i przekazać zmienne jako nazwy kolumn przez unquoting ( !!), aby ich nie oceniać

 library(dplyr)
 multipetalN <- function(df, n){
      varname <- paste0("petal.", n)
      df %>%
         mutate(!!varname := Petal.Width * n)
 }

 data(iris)
 iris1 <- tbl_df(iris)
 iris2 <- tbl_df(iris)
 for(i in 2:5) {
     iris2 <- multipetalN(df=iris2, n=i)
 }   

Sprawdzanie wyjścia w oparciu o @ MrFlick multipetalzastosowane na 'iris1'

identical(iris1, iris2)
#[1] TRUE
akrun
źródło
26

Po wielu próbach i błędach stwierdziłem, że wzorzec jest UQ(rlang::sym("some string here")))naprawdę przydatny do pracy z ciągami znaków i czasownikami dplyr. Wydaje się, że działa w wielu zaskakujących sytuacjach.

Oto przykład z mutate. Chcemy stworzyć funkcję, która zsumuje dwie kolumny, w której przekazujesz funkcję obie nazwy kolumn jako ciągi. Aby to zrobić, możemy użyć tego wzorca wraz z operatorem przypisania :=.

## Take column `name1`, add it to column `name2`, and call the result `new_name`
mutate_values <- function(new_name, name1, name2){
  mtcars %>% 
    mutate(UQ(rlang::sym(new_name)) :=  UQ(rlang::sym(name1)) +  UQ(rlang::sym(name2)))
}
mutate_values('test', 'mpg', 'cyl')

Wzór działa również z innymi dplyrfunkcjami. Oto filter:

## filter a column by a value 
filter_values <- function(name, value){
  mtcars %>% 
    filter(UQ(rlang::sym(name)) != value)
}
filter_values('gear', 4)

Lub arrange:

## transform a variable and then sort by it 
arrange_values <- function(name, transform){
  mtcars %>% 
    arrange(UQ(rlang::sym(name)) %>%  UQ(rlang::sym(transform)))
}
arrange_values('mpg', 'sin')

Na select, nie trzeba korzystać z wzorca. Zamiast tego możesz użyć !!:

## select a column 
select_name <- function(name){
  mtcars %>% 
    select(!!name)
}
select_name('mpg')
Tom Roth
źródło
Twoje wskazówki działają bardzo dobrze, ale mam mały problem. Zmieniam początkową kolumnę myColna adres URL (na przykład) i myColInitialValuekopiuję starą kolumnę na końcu ramki danych dfz nową nazwą. Ale which(colnames(df)=='myCol')odeślij col # z myColInitialValue. Nie napisałem jeszcze problemu, ponieważ nie znalazłem reprexu. Moim celem jest dla escapeparametru DT::datatable(). Używam escape=FALSEw oczekiwaniu, że. Ze stałymi to również nie działa, ale pakiet DT wydaje się również mieć złą kolumnę #. :)
phili_b
Wygląda na to, że przyczyną nie są zmienne dynamiczne. (dodano reprex)
phili_b
Dzięki za tę odpowiedź! Oto bardzo prosty przykład tego, jak go użyłem:varname = sym("Petal.Width"); ggplot(iris, aes(x=!!varname)) + geom_histogram()
najwyżej
To działało dla mnie wewnątrz formuły, w której !! nazwa_zmiennej nie działała.
daknowles
12

Oto inna wersja, prawdopodobnie nieco prostsza.

multipetal <- function(df, n) {
    varname <- paste("petal", n, sep=".")
    df<-mutate_(df, .dots=setNames(paste0("Petal.Width*",n), varname))
    df
}

for(i in 2:5) {
    iris <- multipetal(df=iris, n=i)
}

> head(iris)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species petal.2 petal.3 petal.4 petal.5
1          5.1         3.5          1.4         0.2  setosa     0.4     0.6     0.8       1
2          4.9         3.0          1.4         0.2  setosa     0.4     0.6     0.8       1
3          4.7         3.2          1.3         0.2  setosa     0.4     0.6     0.8       1
4          4.6         3.1          1.5         0.2  setosa     0.4     0.6     0.8       1
5          5.0         3.6          1.4         0.2  setosa     0.4     0.6     0.8       1
6          5.4         3.9          1.7         0.4  setosa     0.8     1.2     1.6       2
user2946432
źródło
8

Dzięki temu rlang 0.4.0mamy operatory curly-curly ( {{}}), co bardzo ułatwia to zadanie.

library(dplyr)
library(rlang)

iris1 <- tbl_df(iris)

multipetal <- function(df, n) {
   varname <- paste("petal", n , sep=".")
   mutate(df, {{varname}} := Petal.Width * n)
}

multipetal(iris1, 4)

# A tibble: 150 x 6
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species petal.4
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>     <dbl>
# 1          5.1         3.5          1.4         0.2 setosa      0.8
# 2          4.9         3            1.4         0.2 setosa      0.8
# 3          4.7         3.2          1.3         0.2 setosa      0.8
# 4          4.6         3.1          1.5         0.2 setosa      0.8
# 5          5           3.6          1.4         0.2 setosa      0.8
# 6          5.4         3.9          1.7         0.4 setosa      1.6
# 7          4.6         3.4          1.4         0.3 setosa      1.2
# 8          5           3.4          1.5         0.2 setosa      0.8
# 9          4.4         2.9          1.4         0.2 setosa      0.8
#10          4.9         3.1          1.5         0.1 setosa      0.4
# … with 140 more rows

Możemy również przekazywać nazwy zmiennych w cudzysłowie / niecytowane, aby były przypisane jako nazwy kolumn.

multipetal <- function(df, name, n) {
   mutate(df, {{name}} := Petal.Width * n)
}

multipetal(iris1, temp, 3)

# A tibble: 150 x 6
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  temp
#          <dbl>       <dbl>        <dbl>       <dbl> <fct>   <dbl>
# 1          5.1         3.5          1.4         0.2 setosa  0.6  
# 2          4.9         3            1.4         0.2 setosa  0.6  
# 3          4.7         3.2          1.3         0.2 setosa  0.6  
# 4          4.6         3.1          1.5         0.2 setosa  0.6  
# 5          5           3.6          1.4         0.2 setosa  0.6  
# 6          5.4         3.9          1.7         0.4 setosa  1.2  
# 7          4.6         3.4          1.4         0.3 setosa  0.900
# 8          5           3.4          1.5         0.2 setosa  0.6  
# 9          4.4         2.9          1.4         0.2 setosa  0.6  
#10          4.9         3.1          1.5         0.1 setosa  0.3  
# … with 140 more rows

Działa tak samo z

multipetal(iris1, "temp", 3)
Ronak Shah
źródło
4

Dodam też odpowiedź, która trochę to powiększa, bo doszedłem do tego wpisu szukając odpowiedzi, a ten miał prawie to, czego potrzebowałem, ale potrzebowałem trochę więcej, co dostałem przez odpowiedź @MrFlik i R leniwe winiety.

Chciałem stworzyć funkcję, która mogłaby pobierać ramkę danych i wektor nazw kolumn (jako ciągi znaków), które chcę przekonwertować z ciągu na obiekt Date. Nie mogłem wymyślić, jak zrobić as.Date()argument, który jest ciągiem i przekonwertować go na kolumnę, więc zrobiłem to, jak pokazano poniżej.

Poniżej jest jak to zrobiłem za pomocą SE mutate ( mutate_()) i .dotsargument. Krytyka, która to poprawia, jest mile widziana.

library(dplyr)

dat <- data.frame(a="leave alone",
                  dt="2015-08-03 00:00:00",
                  dt2="2015-01-20 00:00:00")

# This function takes a dataframe and list of column names
# that have strings that need to be
# converted to dates in the data frame
convertSelectDates <- function(df, dtnames=character(0)) {
    for (col in dtnames) {
        varval <- sprintf("as.Date(%s)", col)
        df <- df %>% mutate_(.dots= setNames(list(varval), col))
    }
    return(df)
}

dat <- convertSelectDates(dat, c("dt", "dt2"))
dat %>% str
mpettis
źródło
3

Chociaż lubię używać dplyr do użytku interaktywnego, uważam to za niezwykle trudne za pomocą dplyr, ponieważ musisz przejść przez obręcze, aby użyć obejść lazyeval :: interp (), setNames itp.

Oto prostsza wersja wykorzystująca bazę R, w której wydaje mi się bardziej intuicyjne, przynajmniej dla mnie, umieszczenie pętli wewnątrz funkcji i która rozszerza rozwiązanie @ MrFlicks.

multipetal <- function(df, n) {
   for (i in 1:n){
      varname <- paste("petal", i , sep=".")
      df[[varname]] <- with(df, Petal.Width * i)
   }
   df
}
multipetal(iris, 3) 
hackR
źródło
2
+1, chociaż nadal dplyrdużo używam w ustawieniach nieinteraktywnych, używanie go z zmiennym wejściem wewnątrz funkcji wymaga bardzo niezgrabnej składni.
Paul Hiemstra,
3

Możesz cieszyć się pakietem, friendlyevalktóry zawiera uproszczone, uporządkowane API i dokumentację dla nowszych / zwykłych dplyrużytkowników.

Tworzysz łańcuchy, które chcesz mutatetraktować jako nazwy kolumn. Więc używając friendlyevalmożesz napisać:

multipetal <- function(df, n) {
  varname <- paste("petal", n , sep=".")
  df <- mutate(df, !!treat_string_as_col(varname) := Petal.Width * n)
  df
}

for(i in 2:5) {
  iris <- multipetal(df=iris, n=i)
}

Które pod maską wywołuje rlangfunkcje, które sprawdzają, czy varnamejest legalne jako nazwa kolumny.

friendlyeval kod można w dowolnym momencie przekonwertować na równoważny zwykły, uporządkowany kod eval za pomocą dodatku RStudio.

MilesMcBain
źródło