web-dev-qa-db-fra.com

Passer des arguments aux fonctions dplyr

Je veux paramétrer le calcul suivant en utilisant dplyr qui trouve quelles valeurs de Sepal.Length sont associés à plusieurs valeurs de Sepal.Width:

library(dplyr)

iris %>%
    group_by(Sepal.Length) %>%
    summarise(n.uniq=n_distinct(Sepal.Width)) %>%
    filter(n.uniq > 1)

Normalement, j'écrirais quelque chose comme ceci:

not.uniq.per.group <- function(data, group.var, uniq.var) {
    iris %>%
        group_by(group.var) %>%
        summarise(n.uniq=n_distinct(uniq.var)) %>%
        filter(n.uniq > 1)
}

Cependant, cette approche génère des erreurs car dplyr utilise évaluation non standard . Comment cette fonction doit-elle être écrite?

50
asnr

Vous devez utiliser les versions d'évaluation standard des fonctions dplyr (ajoutez simplement '_' aux noms de fonction, c'est-à-dire group_by_ & summarise_) Et passez des chaînes à votre fonction , que vous devez ensuite transformer en symboles. Pour paramétrer l'argument de summaryise_, vous devrez utiliser interp(), qui est défini dans le package lazyeval. Concrètement:

library(dplyr)
library(lazyeval)

not.uniq.per.group <- function(df, grp.var, uniq.var) {
    df %>%
        group_by_(grp.var) %>%
        summarise_( n_uniq=interp(~n_distinct(v), v=as.name(uniq.var)) ) %>%
        filter(n_uniq > 1)
}

not.uniq.per.group(iris, "Sepal.Length", "Sepal.Width")

Notez que dans les versions récentes de dplyr les versions d'évaluation standard des fonctions dplyr ont été "soft deprecated" en faveur d'une évaluation non standard.

Voir Programmation avec dplyr vignette pour plus d'informations sur l'utilisation d'une évaluation non standard.

48
asnr

Comme les anciennes versions de dplyr jusqu'à 0,5, le nouveau dplyr dispose d'installations pour l'évaluation standard (SE) et l'évaluation non standard (NSE). Mais ils s'expriment différemment qu'auparavant.

Si vous voulez une fonction NSE, vous passez des expressions nues et utilisez enquo pour les capturer en tant que quosures . Si vous voulez une fonction SE, passez simplement les quosures (ou symboles) directement, puis supprimez-les dans les appels dplyr. Voici la solution SE à la question:

library(tidyverse)
library(rlang)

f1 <- function(df, grp.var, uniq.var) {
   df %>%
       group_by(!!grp.var) %>%
       summarise(n_uniq = n_distinct(!!uniq.var)) %>%
       filter(n_uniq > 1)  
}

a <- f1(iris, quo(Sepal.Length), quo(Sepal.Width))
b <- f1(iris, sym("Sepal.Length"), sym("Sepal.Width"))
identical(a, b)
#> [1] TRUE

Notez comment la version SE vous permet de travailler avec des arguments de chaîne - transformez-les simplement en symboles en utilisant sym(). Pour plus d'informations, consultez la vignette programmation avec dplyr .

18
Paul

Dans la version de développement de dplyr (bientôt disponible 0.6.0), nous pouvons également utiliser une syntaxe légèrement différente pour passer les variables.

f1 <- function(df, grp.var, uniq.var) {
   grp.var <- enquo(grp.var)
   uniq.var <- enquo(uniq.var)

   df %>%
       group_by(!!grp.var) %>%
       summarise(n_uniq = n_distinct(!!uniq.var)) %>%
       filter(n_uniq >1)  


}

res2 <- f1(iris, Sepal.Length, Sepal.Width) 
res1 <- not.uniq.per.group(iris, "Sepal.Length", "Sepal.Width")
identical(res1, res2)
#[1] TRUE

Ici enquo prend les arguments et retourne la valeur sous la forme d'un quosure (similaire au substitut dans la base R) en évaluant les arguments de la fonction paresseusement et à l'intérieur du résumé, nous lui demandons de ne pas citer (!! ou UQ) pour qu'il soit évalué.

11
akrun

Dans la version actuelle de dplyr (0.7.4), l'utilisation des versions de fonction d'évaluation standard (ajoutée '_' au nom de la fonction, par exemple group_by_) est obsolète. Au lieu de cela, vous devez compter sur tidyeval lors de l'écriture des fonctions.

Voici un exemple de l'apparence de votre fonction:

# definition of your function
not.uniq.per.group <- function(data, group.var, uniq.var) {
  # enquotes variables to be used with dplyr-functions
  group.var <- enquo(group.var)
  uniq.var <- enquo(uniq.var)

  # use '!!' before parameter names in dplyr-functions
  data %>%
    group_by(!!group.var) %>%
    summarise(n.uniq=n_distinct(!!uniq.var)) %>%
    filter(n.uniq > 1)
}

# call of your function
not.uniq.per.group(iris, Sepal.Length, Sepal.Width)

Si vous voulez tout savoir sur les détails, il y a une excellente vignette par l'équipe dplyr sur la façon dont cela fonctionne.

5

J'ai écrit une fonction dans le passé qui fait quelque chose de similaire à ce que vous faites, sauf qu'elle explore toutes les colonnes en dehors de la clé primaire et recherche plusieurs valeurs uniques par groupe.

find_dups = function(.table, ...) {
  require(dplyr)
  require(tidyr)
  # get column names of primary key
  pk <- .table %>% select(...) %>% names
  other <- names(.table)[!(names(.table) %in% pk)]
  # group by primary key,
  # get number of rows per unique combo,
  # filter for duplicates,
  # get number of distinct values in each column,
  # gather to get df of 1 row per primary key, other column,
  # filter for where a columns have more than 1 unique value,
  # order table by primary key
  .table %>%
    group_by(...) %>%
    mutate(cnt = n()) %>%
    filter(cnt > 1) %>%
    select(-cnt) %>%
    summarise_each(funs(n_distinct)) %>%
    gather_('column', 'unique_vals', other) %>%
    filter(unique_vals > 1) %>%
    arrange(...) %>%
    return
  # Final dataframe:
  ## One row per primary key and column that creates duplicates.
  ## Last column indicates how many unique values of
  ## the given column exist for each primary key.
}

Cette fonction fonctionne également avec l'opérateur de tuyauterie:

dat %>% find_dups(key1, key2)
3

Vous pouvez éviter lazyeval en utilisant do pour appeler une fonction anonyme, puis en utilisant get. Cette solution peut être utilisée plus généralement pour utiliser plusieurs agrégations. J'écris habituellement la fonction séparément.

library(dplyr)

not.uniq.per.group <- function(df, grp.var, uniq.var) {
  df %>%
    group_by_(grp.var) %>%
    do((function(., uniq.var) {
      with(., data.frame(n_uniq = n_distinct(get(uniq.var))))
    }      
  )(., uniq.var)) %>%
  filter(n_uniq > 1)
}

not.uniq.per.group(iris, "Sepal.Length", "Sepal.Width")
2
bmayer

Voici la façon de le faire à partir de rlang 0.4 en utilisant curly curly{{ pseudo-opérateur:

library(dplyr)

not.uniq.per.group <- function(data, group.var, uniq.var) {
  data %>%
    group_by({{group.var}}) %>%
    summarise(n.uniq=n_distinct({{uniq.var}})) %>%
    filter(n.uniq > 1)
}

iris %>% not.uniq.per.group(Sepal.Length, Sepal.Width)
#> # A tibble: 25 x 2
#>    Sepal.Length n.uniq
#>           <dbl>  <int>
#>  1          4.4      3
#>  2          4.6      4
#>  3          4.8      3
#>  4          4.9      5
#>  5          5        8
#>  6          5.1      6
#>  7          5.2      4
#>  8          5.4      4
#>  9          5.5      6
#> 10          5.6      5
#> # ... with 15 more rows
2
Moody_Mudskipper