Regrouper par plusieurs colonnes dans dplyr, en utilisant une entrée vectorielle de chaîne

157

J'essaie de transférer ma compréhension de plyr dans dplyr, mais je ne peux pas comprendre comment grouper par plusieurs colonnes.

# make data with weird column names that can't be hard coded
data = data.frame(
  asihckhdoydkhxiydfgfTgdsx = sample(LETTERS[1:3], 100, replace=TRUE),
  a30mvxigxkghc5cdsvxvyv0ja = sample(LETTERS[1:3], 100, replace=TRUE),
  value = rnorm(100)
)

# get the columns we want to average within
columns = names(data)[-3]

# plyr - works
ddply(data, columns, summarize, value=mean(value))

# dplyr - raises error
data %.%
  group_by(columns) %.%
  summarise(Value = mean(value))
#> Error in eval(expr, envir, enclos) : index out of bounds

Que me manque-t-il pour traduire l'exemple plyr en une syntaxe dplyr-esque?

Edit 2017 : Dplyr a été mis à jour, donc une solution plus simple est disponible. Voir la réponse actuellement sélectionnée.

sharoz
la source
3
Je viens juste d'arriver car c'était top google. Vous pouvez utiliser group_by_maintenant expliqué dansvignette("nse")
James Owers
3
@kungfujam: Cela semble regrouper uniquement par la première colonne, pas la paire de colonnes
sharoz
1
Vous devez utiliser .dots. Voici la solution adaptée de la réponse de @hadley ci-dessous:df %>% group_by_(.dots=list(quote(asihckhdoydk), quote(a30mvxigxkgh))) %>% summarise(n = n())
James Owers
1
Ont mis le code complet dans une réponse ci
James Owers
1
Comme quelqu'un l'a souligné dans une réponse au commentaire, le but est de ne pas exiger des noms de colonne codés en dur.
sharoz

Réponses:

52

Depuis que cette question a été publiée, dplyr a ajouté des versions étendues de group_by( documentation ici ). Cela vous permet d'utiliser les mêmes fonctions que vous utiliseriez avec select, comme ceci:

data = data.frame(
    asihckhdoydkhxiydfgfTgdsx = sample(LETTERS[1:3], 100, replace=TRUE),
    a30mvxigxkghc5cdsvxvyv0ja = sample(LETTERS[1:3], 100, replace=TRUE),
    value = rnorm(100)
)

# get the columns we want to average within
columns = names(data)[-3]

library(dplyr)
df1 <- data %>%
  group_by_at(vars(one_of(columns))) %>%
  summarize(Value = mean(value))

#compare plyr for reference
df2 <- plyr::ddply(data, columns, plyr::summarize, value=mean(value))
table(df1 == df2, useNA = 'ifany')
## TRUE 
##  27 

La sortie de votre exemple de question est comme prévu (voir la comparaison avec plyr ci-dessus et la sortie ci-dessous):

# A tibble: 9 x 3
# Groups:   asihckhdoydkhxiydfgfTgdsx [?]
  asihckhdoydkhxiydfgfTgdsx a30mvxigxkghc5cdsvxvyv0ja       Value
                     <fctr>                    <fctr>       <dbl>
1                         A                         A  0.04095002
2                         A                         B  0.24943935
3                         A                         C -0.25783892
4                         B                         A  0.15161805
5                         B                         B  0.27189974
6                         B                         C  0.20858897
7                         C                         A  0.19502221
8                         C                         B  0.56837548
9                         C                         C -0.22682998

Notez que comme dplyr::summarizene supprime qu'une couche de regroupement à la fois, vous avez toujours du regroupement en cours dans le tibble résultant (qui peut parfois surprendre les gens plus tard sur la ligne). Si vous voulez être absolument à l'abri d'un comportement de regroupement inattendu, vous pouvez toujours ajouter %>% ungroupà votre pipeline après avoir résumé.

Empiromancien
la source
fait la mise à jour pour 0.7.0rendre le système quote-unquote disponible avec plusieurs colonnes également?
JelenaČuklina
4
Vous pouvez également utiliser les .dotsarguments en group_by()tant que tel: data %>% group_by(.dots = columns) %>% summarize(value = mean(value)).
Paul Rougieux
L'appel à one_of()faire quelque chose ici? Je pense que c'est redondant dans ce contexte, car l'expression est enveloppée dans un appel à vars().
knowah
@Khashir oui, cette réponse fonctionne toujours @knowah Vous avez raison, l'appel à one_of()est redondant dans ce contexte
Empiromancer
2
@Sos Pour appliquer une fonction sur plusieurs colonnes en utilisant la selectsyntaxe, voir la nouvelle acrossfonction: dplyr.tidyverse.org/reference/across.html Dans votre cas, cela ressemblerait à quelque chose commesummarize(across(all_of(c(''value_A", "value_B")), mean))
Empiromancer
102

Juste pour écrire le code dans son intégralité, voici une mise à jour de la réponse de Hadley avec la nouvelle syntaxe:

library(dplyr)

df <-  data.frame(
    asihckhdoydk = sample(LETTERS[1:3], 100, replace=TRUE),
    a30mvxigxkgh = sample(LETTERS[1:3], 100, replace=TRUE),
    value = rnorm(100)
)

# Columns you want to group by
grp_cols <- names(df)[-3]

# Convert character vector to list of symbols
dots <- lapply(grp_cols, as.symbol)

# Perform frequency counts
df %>%
    group_by_(.dots=dots) %>%
    summarise(n = n())

production:

Source: local data frame [9 x 3]
Groups: asihckhdoydk

  asihckhdoydk a30mvxigxkgh  n
1            A            A 10
2            A            B 10
3            A            C 13
4            B            A 14
5            B            B 10
6            B            C 12
7            C            A  9
8            C            B 12
9            C            C 10
James Owers
la source
1
Cela semble encore coder en dur les noms des colonnes, juste dans une formule à la place. Le point de la question est de savoir comment utiliser des chaînes pour ne pas avoir à taper asihckhdoydk...
Gregor Thomas
1
Avoir mis à jour la solution en utilisant dots <- lapply(names(df)[-3], function(x) as.symbol(x))pour créer l' .dotsargument
James Owers
4
essayer de trier ces réponses .dots=était l'étape cruciale. si quelqu'un a une bonne idée de la raison pour laquelle cela est requis dans l' group_byappel, pouvez-vous modifier cette réponse? en ce moment, c'est un peu impénétrable.
Andrew
12
vignette("nse")indique qu'il existe trois façons de citer qui sont acceptables: formule, citation et caractère. À moins que vous ne vous inquiétiez de l'environnement dont il tirera, vous pourrez probablement vous en sortirgroup_by_(.dots=grp_cols)
Ari B. Friedman
58

Le support pour cela dans dplyr est actuellement assez faible, finalement je pense que la syntaxe sera quelque chose comme:

df %.% group_by(.groups = c("asdfgfTgdsx", "asdfk30v0ja"))

Mais cela ne sera probablement pas là avant un certain temps (car j'ai besoin de réfléchir à toutes les conséquences).

En attendant, vous pouvez utiliser regroup(), qui prend une liste de symboles:

library(dplyr)

df <-  data.frame(
  asihckhdoydk = sample(LETTERS[1:3], 100, replace=TRUE),
  a30mvxigxkgh = sample(LETTERS[1:3], 100, replace=TRUE),
  value = rnorm(100)
)

df %.%
  regroup(list(quote(asihckhdoydk), quote(a30mvxigxkgh))) %.%
  summarise(n = n())

Si vous avez un vecteur de caractères de noms de colonnes, vous pouvez les convertir dans la bonne structure avec lapply()et as.symbol():

vars <- setdiff(names(df), "value")
vars2 <- lapply(vars, as.symbol)

df %.% regroup(vars2) %.% summarise(n = n())
hadley
la source
6
as.symbolle résout. Merci! Au cas où cela aiderait au développement: ce scénario est très courant pour moi. Agréger un résultat numérique sur chaque combinaison des autres variables.
sharoz le
apparemment, cela ne fonctionne que pour cet exemple particulier et aucun autre.
Paulo E. Cardoso
3
J'ai initialement marqué cela comme la réponse, mais les mises à jour de dplyr permettent à la réponse de kungfujam de fonctionner.
sharoz
regroupest également obsolète (au moins à partir de la version 0.4.3).
Berk U.
27

La spécification de chaîne de colonnes dans dplyrest désormais prise en charge via des variantes des dplyrfonctions dont les noms se terminent par un trait de soulignement. Par exemple, correspondant à la group_byfonction, il existe une group_by_fonction qui peut prendre des arguments de chaîne. Cette vignette décrit en détail la syntaxe de ces fonctions.

L'extrait suivant résout proprement le problème que @sharoz posait à l'origine (notez la nécessité d'écrire l' .dotsargument):

# Given data and columns from the OP

data %>%
    group_by_(.dots = columns) %>%
    summarise(Value = mean(value))

(Notez que dplyr utilise maintenant l' %>%opérateur, et %.%est obsolète).

Edward
la source
17

Jusqu'à ce que dplyr ait un support complet pour les arguments de chaîne, peut-être que l'essentiel est utile:

https://gist.github.com/skranz/9681509

Il contient un tas de fonctions wrapper comme s_group_by, s_mutate, s_filter, etc. qui utilisent des arguments de chaîne. Vous pouvez les mélanger avec les fonctions normales de dplyr. Par exemple

cols = c("cyl","gear")
mtcars %.%
  s_group_by(cols) %.%  
  s_summarise("avdisp=mean(disp), max(disp)") %.%
  arrange(avdisp)
Sebastian Kranz
la source
11

Cela fonctionne si vous lui passez les objets (enfin, vous n'êtes pas, mais ...) plutôt que comme vecteur de caractères:

df %.%
    group_by(asdfgfTgdsx, asdfk30v0ja) %.%
    summarise(Value = mean(value))

> df %.%
+   group_by(asdfgfTgdsx, asdfk30v0ja) %.%
+   summarise(Value = mean(value))
Source: local data frame [9 x 3]
Groups: asdfgfTgdsx

  asdfgfTgdsx asdfk30v0ja        Value
1           A           C  0.046538002
2           C           B -0.286359899
3           B           A -0.305159419
4           C           A -0.004741504
5           B           B  0.520126476
6           C           C  0.086805492
7           B           C -0.052613078
8           A           A  0.368410146
9           A           B  0.088462212

dfétait votre data.

?group_by dit:

 ...: variables to group by. All tbls accept variable names, some
      will also accept functons of variables. Duplicated groups
      will be silently dropped.

ce que j'interprète comme ne signifiant pas les versions de caractère des noms, mais comment vous y feriez référence foo$bar; barn'est pas cité ici. Ou comment vous faites référence à des variables dans une formule: foo ~ bar.

@Arun mentionne également que vous pouvez faire:

df %.%
    group_by("asdfgfTgdsx", "asdfk30v0ja") %.%
    summarise(Value = mean(value))

Mais tu ne peux pas transmettre quelque chose qui n'a pas été évalué n'est pas n'est pas le nom d'une variable dans l'objet de données.

Je suppose que cela est dû aux méthodes internes que Hadley utilise pour rechercher les choses que vous transmettez via l' ...argument.

Gavin Simpson
la source
1
@Arun Merci pour cela. Je n'avais pas remarqué cela, mais c'est aussi logique. J'ai ajouté une note à ce sujet, citant vous et votre commentaire.
Gavin Simpson
4
Malheureusement, je ne peux pas me fier au codage en dur des noms de colonne. J'essaye de le faire sans avoir à les spécifier.
sharoz
4
data = data.frame(
  my.a = sample(LETTERS[1:3], 100, replace=TRUE),
  my.b = sample(LETTERS[1:3], 100, replace=TRUE),
  value = rnorm(100)
)

group_by(data,newcol=paste(my.a,my.b,sep="_")) %>% summarise(Value=mean(value))
Jordan
la source
4

Un (minuscule) cas qui manque dans les réponses ici, que je voulais rendre explicite, est celui où les variables à regrouper sont générées dynamiquement au milieu d'un pipeline:

library(wakefield)
df_foo = r_series(rnorm, 10, 1000)
df_foo %>% 
  # 1. create quantized versions of base variables
  mutate_each(
    funs(Quantized = . > 0)
  ) %>% 
  # 2. group_by the indicator variables
  group_by_(
    .dots = grep("Quantized", names(.), value = TRUE)
    ) %>% 
  # 3. summarize the base variables
  summarize_each(
    funs(sum(., na.rm = TRUE)), contains("X_")
  )

Cela montre essentiellement comment utiliser grepen conjonction avec group_by_(.dots = ...)pour y parvenir.

tchakravarty
la source
3

Exemple général d'utilisation de l' .dotsargument comme entrée de vecteur de caractère dans la dplyr::group_byfonction:

iris %>% 
    group_by(.dots ="Species") %>% 
    summarise(meanpetallength = mean(Petal.Length))

Ou sans nom codé en dur pour la variable de regroupement (comme demandé par l'OP):

iris %>% 
    group_by(.dots = names(iris)[5]) %>% 
    summarise_at("Petal.Length", mean)

Avec l'exemple de l'OP:

data %>% 
    group_by(.dots =names(data)[-3]) %>% 
    summarise_at("value", mean)

Voir aussi la vignette de dplyr sur la programmation qui explique les pronoms, la quasiquotation, les quosures et tidyeval.

Paul Rougieux
la source