Dplyr: Conserver les groupes de longueur nulle

Créé le 20 mars 2014  ·  44Commentaires  ·  Source: tidyverse/dplyr

http://stackoverflow.com/questions/22523131

Je ne sais pas quelle devrait être l'interface pour cela - devrait probablement être par défaut drop = FALSE.

feature wip

Commentaire le plus utile

+1 - c'est une rupture pour de nombreuses analyses

Tous les 44 commentaires

Merci d'avoir ouvert ce problème Hadley.

:+1: rencontré le même problème aujourd'hui, drop = FALSE serait d'une grande aide pour moi !

Une idée du délai pour mettre un équivalent .drop = FALSE dans dplyr? J'en ai besoin pour que certains rCharts s'affichent correctement.

En attendant, j'ai obtenu la réponse dans votre lien pour travailler.
http://stackoverflow.com/questions/22523131

J'ai regroupé par deux variables.

+1 pour l'option de ne pas supprimer les groupes vides

Peut-être un chevauchement avec #486 et #413.

Ne pas supprimer les groupes vides serait très utile. Souvent nécessaire lors de la création de tableaux récapitulatifs.

+1 - c'est une rupture pour de nombreuses analyses

Je suis d'accord avec tout ce qui précède - ce serait très utile.

@romainfrancois Actuellement build_index_cpp() ne respecte pas l'attribut drop :

t1 <- data_frame(
  x = runif(10),
  g1 = rep(1:2, each = 5),
  g2 = factor(g1, 1:3)
)
g1 <- grouped_df(t1, list(quote(g2)), drop = FALSE)
attr(g1, "group_size")
# should be c(5L, 5L, 0L)
attr(g1, "indices")
# shoud be list(0:4, 5:9, integer(0))

L'attribut drop s'applique uniquement lors du regroupement par facteur, auquel cas nous devons avoir un groupe par niveau de facteur, que le niveau s'applique ou non aux données.

Cela affectera également les verbes à table unique des manières suivantes :

  • select() : aucun effet
  • arrange() : aucun effet
  • summarise() : les fonctions appliquées aux groupes de lignes zéro doivent recevoir des entiers de niveau 0. n() devrait retourner 0, mean(x) devrait retourner NaN
  • filter() : l'ensemble des groupes doit rester constant, même si certains groupes n'ont désormais plus de lignes
  • mutate() : pas besoin d'évaluer les expressions pour les groupes vides

Finalement, drop = FALSE sera la valeur par défaut, et s'il est difficile d'écrire à la fois les branches drop = FALSE et drop = TRUE , j'abandonnerais volontiers le support pour drop = FALSE ( puisque vous pouvez toujours reniveler le facteur vous-même, ou utiliser un vecteur de caractère à la place).

Cela a-t-il du sens? Si c'est beaucoup de travail, nous pouvons pousser à 0.4

@statwonk , @wsurles , @jennybc , @slackline , @mcfrank , @eipi10 Si vous souhaitez aider, la meilleure chose à faire serait de travailler sur un ensemble de cas de test qui exerce toutes les façons dont les différents verbes peuvent interagir avec des groupes de longueur nulle.

Ah. Je pense que je ne savais tout simplement pas ce que drop était censé faire. C'est clair. Je ne pense pas que ce soit beaucoup de travail.

J'ai ouvert la pull request #833 qui teste si les verbes de table unique ci-dessus gèrent correctement les groupes de longueur nulle. La plupart des tests sont commentés, car dplyr les échoue actuellement, bien sûr.

+1 , des mises à jour de statut ici ? résumé de l'amour, besoin de garder des niveaux vides!

@ebergelson , Voici mon hack actuel pour obtenir des groupes de longueur nulle. J'en ai souvent besoin pour que mes graphiques à barres s'empilent.

Ici, df a 3 colonnes : nom, groupe et métrique

df2 <- expand.grid(name = unique(df$name), group = unique(df$group)) %>%
    left_join(df, by=c("name","group")) %>%
    mutate(metric = ifelse(is.na(metric),0,metric))

Je fais quelque chose de similaire : vérifiez les groupes manquants, puis s'il y en a, générez toutes les combinaisons et left_join .

Malheureusement, il ne semble pas que ce problème suscite beaucoup d'intérêt... peut-être parce qu'il existe cette solution de contournement simple.

@wsurles , @bpbond merci, oui j'ai utilisé une solution de contournement similaire à ce que vous suggérez ! J'adorerais voir un correctif intégré comme .drop.

Juste pour ajouter et être d'accord avec tout le monde ci-dessus - c'est un aspect super critique de nombreuses analyses. J'adorerais voir une implémentation.

Quelques détails supplémentaires nécessaires ici :

Si j'ai ça :

> df <- data_frame( x = c(1,1,1,2,2), f = factor( c(1,2,3,1,1) ) )
> df
Source: local data frame [5 x 2]

  x f
1 1 1
2 1 2
3 1 3
4 2 1
5 2 1

Et je regroupe par x puis f , je me retrouve avec 6 (2x3) groupes où les groupes (2, 2) et (2,3) sont vides. C'est bon. Je peux réussir à mettre en œuvre que je pense.

maintenant, et si j'ai ceci:

> df <- data_frame( f = factor( c(1,1,2,2), levels = 1:3), x = c(1,2,1,4) )
> df
Source: local data frame [4 x 2]

  f x
1 1 1
2 1 2
3 2 1
4 2 4

et je veux grouper par f puis x . Quels seraient les groupes ? @hadley

Les deux stats::aggregate et plyr::ddply renvoient 4 groupes dans ce cas (1,1; 1,2; 2,1; et 2,4), donc je suggère que c'est le comportement à se conformer .

Ne devrait-il pas être d'accord avec table() place, c'est-à-dire retourner 9 groupes ?

> table(df$f, df$x)
  1 2 4
1 1 1 0
2 1 0 1
3 0 0 0

Je m'attendrais df %>% group_by(f, x) %>% tally ce que with(df, as.data.frame(table(f, x))) et ddply(df, .(f, x), nrow, .drop=FALSE) .

Je pensais que notre comportement souhaité était de préserver les groupes de longueur nulle s'ils sont des facteurs (comme .drop in plyr), donc j'imagine que nous voudrions la suggestion de @huftis . Je suggérerais cependant que la valeur par défaut soit drop = TRUE, afin que le comportement par défaut ne change pas, concernant la suggestion de

Hmmm, il est difficile de comprendre exactement ce que le comportement devrait être. Ces expériences de pensée très simples semblent-elles correctes ?

df <- data_frame(x = 1, y = factor(1, levels = 2))
df %>% group_by(x) %>% summarise(n())
#> x n
#> 1 1  

df %>% group_by(y) %>% summarise(n())
#> y n
#> 1 1
#> 2 0

df %>% group_by(x, y) %>% summarise(n()
#> x y n
#> 1 1 1
#> 1 2 0

Mais que se passe-t-il si x a plusieurs valeurs ? Est-ce que ça doit fonctionner comme ça ?

df <- data_frame(x = 1:2, y = factor(1, levels = 2))
df %>% group_by(x, y) %>% summarise(n()
#> x y n
#> 1 1 1
#> 2 1 1
#> 1 1 0
#> 2 2 0

Peut-être que la préservation des groupes vides n'a de sens que lors du regroupement par une seule variable ? Si nous l'encadrons de manière plus réaliste, par exemple data_frame(age_group = c(40, 60), sex = factor(M, levels = c("F", "M")) voudriez-vous vraiment le nombre de femmes ? Je pense que parfois vous le feriez et parfois non. Développer toutes les combinaisons me semble être une opération quelque peu différente (et indépendante de l'utilisation de facteurs).

Peut-être que group_by besoin à la fois des arguments drop et expand ? drop = FALSE conserverait tous les groupes de taille zéro générés par les niveaux de facteurs qui n'apparaissent pas dans les données. expand = TRUE conserverait tous les groupes de taille zéro générés par des combinaisons de valeurs qui n'apparaissent pas dans les données.

@hadley Vos exemples me semblent levels = 1:2 , pas levels = 2 ). Et je pense que la préservation des groupes vides a du sens même lors du regroupement par plusieurs variables. Par exemple, si les variables étaient sex ( male et female ) et answer (sur un questionnaire, avec les niveaux disagree , neutral , agree ), et vous vouliez compter la fréquence de chaque réponse pour chaque sexe (par exemple pour un tableau, ou pour un tracé ultérieur), vous ne voudriez pas simplement supprimer une catégorie de réponse car aucune femelle n'y a répondu.

Je m'attendrais également à ce que les variables factorielles restent des variables factorielles dans les data_frame résultants (non convertis en chaînes) et avec les _niveaux d'origine_. (Donc, lors du traçage des données, les catégories de réponses seraient dans le bon ordre, pas l'ordre alphabétique agree , disagree , neutral ).

Pour votre dernier exemple, il serait _dans certains cas_ naturel de supprimer la variable sex (par exemple, si _intentionnellement_ aucune femme n'a été interrogée), et _dans d'autres cas_ pas (par exemple, lors du comptage du nombre de malformations congénitales stratifié par sexe (et peut-être année)). Mais cela peut (et doit) être facilement traité _après_ l'agrégation des données. (Une solution différente serait d'accepter un argument _vecteur_valué_ .drop . Ce serait bien, mais je suppose que cela pourrait compliquer les choses ?)

(Une solution différente serait d'accepter un argument .drop à valeur vectorielle. Ce serait bien, mais je suppose que cela pourrait compliquer les choses ?)

Oui, probablement trop compliqué. Sinon je suis d'accord avec les commentaires de @huftis .

@hadley
je pense
YES développe toutes les combinaisons de valeurs dans group_by si elles existent dans les données.
NON, ne développez pas les niveaux de facteurs qui n'existent pas.

Mon cas d'utilisation le plus souvent est la préparation d'un ensemble de données résumées pour un graphique (pendant l'exploration). Et les graphiques doivent avoir toutes les combinaisons de valeurs. Mais ils n'ont pas besoin d'avoir des niveaux de facteur qui ont 0 pour tous les groupes. Par exemple, vous ne pouvez pas empiler un graphique à barres sans toutes les combinaisons. Mais vous n'avez pas besoin de valeurs de facteur qui n'existent pas dans les données, celles-ci seraient simplement 0 lorsqu'elles sont empilées et une valeur vide dans la légende.

Je pense que l'extension de toutes les valeurs à group_by devrait être la valeur par défaut car il est beaucoup plus facile (et beaucoup plus intuitif) de filtrer 0 cas après le groupe si nécessaire. Je ne pense pas qu'un argument .drop soit nécessaire, car il est assez facile de filtrer 0 cas après. Nous n'utilisons pas d'arguments supplémentaires pour les autres fonctions, ce qui briserait le moule. La valeur par défaut devrait simplement être d'afficher les résultats de toutes les combinaisons de valeurs existantes basées sur group_by.

Je pense que ce serait le comportement par défaut correct. Ici, l'unique s'étendra uniquement sur les valeurs existantes dans le facteur, pas sur tous les niveaux de facteur. (C'est ce que je lance après avoir exécuté un group_by qui laisse tomber 0 valeurs)

## Expand data so plot groups works correctly
  df2 <- expand.grid(name = unique(df$name), group = unique(df$group)) %>%
    left_join(df, by=c("name","group")) %>%
    mutate(
      measure = ifelse(is.na(measure),0,measure)
    )

Le seul cas où je peux voir où vous voudriez une valeur même si tous les groupes avaient zéro est avec les données de temps. Peut-être qu'une journée de données manque quelque part au milieu. Ici, une extension et une jointure sur une plage de dates seraient toujours nécessaires. Le cas du niveau de facteur ne s'appliquerait pas. Je pense qu'il est juste que le Data Cruncher gère lui-même les dates manquantes.

Merci pour tout votre excellent travail sur cette bibliothèque. 90% de mon travail utilise dplyr. : )

Je suis tout à fait d'accord avec @huftis.

Je ne pense pas que la baisse des niveaux ou des combinaisons de niveaux devrait avoir quoi que ce soit à voir avec les données. Vous êtes peut-être en train de prototyper une fonction ou une figure à l'aide d'un petit échantillon. Ou faire une opération de division-application-combinaison, auquel cas vous voulez avoir la garantie que la sortie de chaque groupe sera conforme à tout le reste.

Adoucir ma position : je pense qu'il vaut la peine de se demander si le comportement par défaut doit différer lorsque la variable de regroupement est déjà un facteur approprié par rapport au moment où elle est contrainte de le prendre en compte. Je peux voir que l'obligation de conserver les niveaux inutilisés pourrait être moindre dans le cas de la coercition. Mais si je me suis donné la peine de mettre en place quelque chose comme facteur et de prendre le contrôle des niveaux... il y a généralement une bonne raison et je ne veux pas me battre constamment pour la préserver.

Pour info, j'aimerais aussi voir cette fonctionnalité. J'ai un scénario similaire à celui décrit par @huftis et

Je suis venu de SO. N'est-ce pas ce que complete de "tidyr" est censé aider ?

Oui. En fait, je viens d'apprendre l'existence de « compléter » récemment et cela semble accomplir cela de manière réfléchie.

L'implémentation de cela pour les backends SQL semble difficile, car ils supprimeront par défaut tous les groupes. Devons-nous en rester là et peut-être implémenter tidyr::complete() pour SQL ?

J'ai créé le numéro 3033 sans me rendre compte que ce problème existait déjà - excuses pour le doublon. Pour ajouter ma propre humble suggestion, j'utilise actuellement pull() et forcats::fct_count() comme solution de contournement à ce problème.

Je ne suis pas un fan de cette méthode car fct_count() trahit le principe tidyverse de faire une sortie qui est toujours du même type que l'entrée (c'est-à-dire que cette fonction crée un tibble à partir d'un vecteur), et j'ai pour renommer les colonnes dans la sortie. Cela crée 3 étapes ( pull() %>% fct_count() %>% rename() ) alors que dplyr::count() était censé en couvrir une. Ce serait fantastique si forcats::fct_count() et dplyr::count() pouvaient être fusionnés d'une manière ou d'une autre et déprécier forcats::fct_count() .

Est-ce que tidyr::complete() fonctionne pour les facteurs ?

Tous les niveaux de facteurs et combinaisons de niveaux de facteurs doivent être conservés par défaut. Ce comportement peut être contrôlé par des paramètres tels que drop , expand , etc. Ainsi, le comportement par défaut de dplyr::count() devrait ressembler à ceci :

df <- data.frame(x = 1:2, y = factor(c(1, 1), levels = 1:2))
df %>% dplyr::count(x, y)
#>  # A tibble: 4 x 3
#>       x        y       n
#>     <int>   <fct>    <int>
#> 1     1        1       1
#> 2     2        1       1
#> 3     1        2       0
#> 4     2        2       0

Les groupes de longueur nulle (combinaisons de groupes) peuvent être filtrés ultérieurement. Mais pour une analyse exploratoire, nous devons avoir une vue d'ensemble.

  1. Y a-t-il des mises à jour de statut sur la solution à ce problème ?
  2. Existe-t-il des plans pour résoudre complètement ce problème ?

2: oui définitivement
1 : Il y a quelques difficultés techniques de mise en œuvre à propos de ce problème, mais j'y reviendrai dans les prochaines semaines.

Nous pourrions nous en sortir en développant les données après coup, quelque chose comme ceci :

library(tidyverse)

truly_group_by <- function(data, ...){
  dots <- quos(...)
  data <- group_by( data, !!!dots )

  labels <- attr( data, "labels" )
  labnames <- names(labels)
  labels <- mutate( labels, ..index.. =  attr(data, "indices") )

  expanded <- labels %>%
    tidyr::expand( !!!dots ) %>%
    left_join( labels, by = labnames ) %>%
    mutate( ..index.. = map(..index.., ~if(is.null(.x)) integer() else .x ) )

  indices <- pull( expanded, ..index..)
  group_sizes <- map_int( indices, length)
  labels <- select( expanded, -..index..)

  attr(data, "labels")  <- labels
  attr(data, "indices") <- indices
  attr(data, "group_sizes") <- group_sizes

  data
}

df  <- data_frame(
  x = 1:2,
  y = factor(c(1, 1), levels = 1:2)
)
tally( truly_group_by(df, x, y) )
#> # A tibble: 4 x 3
#> # Groups:   x [?]
#>       x y         n
#>   <int> <fct> <int>
#> 1     1 1         1
#> 2     1 2         0
#> 3     2 1         1
#> 4     2 2         0
tally( truly_group_by(df, y, x) )
#> # A tibble: 4 x 3
#> # Groups:   y [?]
#>   y         x     n
#>   <fct> <int> <int>
#> 1 1         1     1
#> 2 1         2     1
#> 3 2         1     0
#> 4 2         2     0

évidemment sur toute la ligne, cela serait géré en interne, sans utiliser tidyr ou ronronner.

Cela semble répondre à la question d'origine sur ainsi :

> df = data.frame(a=rep(1:3,4), b=rep(1:2,6))
> df$b = factor(df$b, levels=1:3)
> df %>%
+   group_by(b) %>%
+   summarise(count_a=length(a), .drop=FALSE)
# A tibble: 2 x 3
  b     count_a .drop
  <fct>   <int> <lgl>
1 1           6 FALSE
2 2           6 FALSE
> df %>%
+   truly_group_by(b) %>%
+   summarise(count_a=length(a), .drop=FALSE)
# A tibble: 3 x 3
  b     count_a .drop
  <fct>   <int> <lgl>
1 1           6 FALSE
2 2           6 FALSE
3 3           0 FALSE

La clé ici étant ceci

 tidyr::expand( !!!dots ) %>%

ce qui signifie élargir toutes les possibilités, que les variables soient des facteurs ou non.

Je dirais que nous soit :

  • développez tout lorsque drop=FALSE , ayant potentiellement beaucoup de groupes de longueur 0
  • faire ce que nous faisons maintenant si drop=TRUE

peut-être avoir une fonction pour basculer la dropness.

C'est une opération relativement bon marché je dirais car elle implique uniquement de manipuler les métadonnées, donc peut-être est-il moins risqué de le faire en R en premier ?

Vouliez-vous dire crossing() au lieu de expand() ?

En regardant les éléments internes, êtes-vous d'accord pour dire que nous devons « seulement » modifier build_index_cpp() , en particulier la génération de la trame de données labels , pour que cela se produise ?

Pouvons-nous peut-être commencer par développer uniquement des facteurs avec drop = FALSE ? J'ai considéré une syntaxe "naturelle", mais cela peut être trop déroutant au final (et peut-être même pas assez puissant):

group_by(data, crossing(col1, col2), col3)

Sémantique : Utilisation de toutes les combinaisons de col1 et col2 , et des combinaisons existantes avec col3 .

Oui, je dirais que cela n'affecte que build_index_cpp et la génération des attributs labels , indices et group_sizes que je voudrais écraser dans un structure ordonnée dans le cadre de #3489

La partie "seuls facteurs d'expansion" de cette discussion est ce qui a pris si longtemps.

Quels seraient les résultats de ceux-ci :

library(dplyr)

d <- data_frame(
  f1 = factor( rep( c("a", "b"), each = 4 ), levels = c("a", "b", "c") ),
  f2 = factor( rep( c("d", "e", "f", "g"), each = 2 ), levels = c("d", "e", "f", "g", "h") ),
  x  = 1:8,
  y  = rep( 1:4, each = 2)
)

f <- function(data, ...){
  group_by(data, !!!quos(...))  %>%
    tally()
}
f(d, f1, f2, x)
f(d, x, f1, f2)

f(d, f1, f2, x, y)
f(d, x, f1, f2, y)

Je pense que f(d, f1, f2, x) devrait donner les mêmes résultats que f(d, x, f1, f2) , si l'ordre des lignes est ignoré. Idem pour les deux autres.

Intéressant aussi :

f(d, f2, x, f1, y)
d %>% sample_frac(0.3) %>% f(...)

J'aime l'idée de mettre en œuvre une expansion complète uniquement pour les facteurs. Pour les données non-caractères (y compris les données logiques), nous pourrions définir/utiliser une classe de type facteur qui hérite du type de données respectif. Peut-être fourni par forcats ? Cela rend plus difficile de se tirer une balle dans le pied.

mise en œuvre en cours au #3492

library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
df <- data_frame( f = factor( c(1,1,2,2), levels = 1:3), x = c(1,2,1,4) )

( res1 <- tally(group_by(df,f,x, drop = FALSE)) )
#> # A tibble: 9 x 3
#> # Groups:   f [?]
#>   f         x     n
#>   <fct> <dbl> <int>
#> 1 1        1.     1
#> 2 1        2.     1
#> 3 1        4.     0
#> 4 2        1.     1
#> 5 2        2.     0
#> 6 2        4.     1
#> 7 3        1.     0
#> 8 3        2.     0
#> 9 3        4.     0
( res2 <- tally(group_by(df,x,f, drop = FALSE)) )
#> # A tibble: 9 x 3
#> # Groups:   x [?]
#>       x f         n
#>   <dbl> <fct> <int>
#> 1    1. 1         1
#> 2    1. 2         1
#> 3    1. 3         0
#> 4    2. 1         1
#> 5    2. 2         0
#> 6    2. 3         0
#> 7    4. 1         0
#> 8    4. 2         1
#> 9    4. 3         0

all.equal( res1, arrange(res2, f, x) )
#> [1] TRUE

all.equal( filter(res1, n>0), tally(group_by(df, f, x)) )
#> [1] TRUE
all.equal( filter(res2, n>0), tally(group_by(df, x, f)) )
#> [1] TRUE

Créé le 2018-04-10 par le package reprex (v0.2.0).

Quant à savoir si complete() résout le problème - non, pas vraiment. Quels que soient les résumés calculés, leurs comportements sur les vecteurs vides doivent être préservés, et non corrigés après coup. Par exemple:

data.frame(x=factor(1, levels=1:2), y=4:5) %>%
     group_by(x) %>%
     summarize(min=min(y), sum=sum(y), prod=prod(y))
# Should be:
#> x       min   sum  prod
#> 1         4     9    20
#> 2       Inf     0     1

sum et prod (et dans une moindre mesure, min ) (et diverses autres fonctions) ont une sémantique très bien définie sur les vecteurs vides, et ce n'est pas génial d'avoir à venez ensuite avec complete() et redéfinissez ces comportements.

@kenahoo Je ne suis pas sûr de comprendre. C'est ce que vous obtenez avec la version de développement actuelle. Donc la seule chose que vous n'obtenez pas est l'avertissement de min()

library(dplyr)

data.frame(x=factor(1, levels=1:2), y=4:5) %>%
  group_by(x) %>%
  summarize(min=min(y), sum=sum(y), prod=prod(y))
#> # A tibble: 2 x 4
#>   x       min   sum  prod
#>   <fct> <dbl> <int> <dbl>
#> 1 1         4     9    20
#> 2 2       Inf     0     1

min(integer())
#> Warning in min(integer()): no non-missing arguments to min; returning Inf
#> [1] Inf
sum(integer())
#> [1] 0
prod(integer())
#> [1] 1

Créé le 2018-05-15 par le package reprex (v0.2.0).

@romainfrancois Oh cool, je ne savais pas que tu étais déjà si avancé sur cette implémentation. Ça a l'air super !

Cet ancien problème a été automatiquement verrouillé. Si vous pensez avoir trouvé un problème connexe, veuillez déposer un nouveau problème (avec reprex) et créer un lien vers ce problème. https://reprex.tidyverse.org/

Cette page vous a été utile?
0 / 5 - 0 notes