J'ai des problèmes avec pandas groupby avec des données catégoriques. Théoriquement, cela devrait être super efficace: vous regroupez et indexez via des entiers plutôt que des chaînes. Mais il insiste sur le fait que, lorsqu’on regroupe plusieurs catégories, il faut prendre en compte toutes les combinaisons de catégories.
J'utilise parfois des catégories même lorsqu'il y a une faible densité de chaînes communes, simplement parce que ces chaînes sont longues et que cela économise de la mémoire/améliore les performances. Parfois, il y a des milliers de catégories dans chaque colonne. Lorsque vous groupez par 3 colonnes, pandas
nous oblige à conserver les résultats pour 1000 ^ 3 groupes.
Ma question: existe-t-il un moyen pratique d’utiliser groupby
avec des catégories tout en évitant ce comportement indésirable? Je ne cherche aucune de ces solutions:
numpy
.groupby
, pour revenir aux catégories ultérieurement.J'espère qu'il y a un moyen de modifier uniquement cette idiosyncrasie pandas
. Un exemple simple est ci-dessous. Au lieu de 4 catégories que je veux dans la sortie, je me retrouve avec 12.
import pandas as pd
group_cols = ['Group1', 'Group2', 'Group3']
df = pd.DataFrame([['A', 'B', 'C', 54.34],
['A', 'B', 'D', 61.34],
['B', 'A', 'C', 514.5],
['B', 'A', 'A', 765.4],
['A', 'B', 'D', 765.4]],
columns=(group_cols+['Value']))
for col in group_cols:
df[col] = df[col].astype('category')
df.groupby(group_cols, as_index=False).sum()
Group1 Group2 Group3 Value
# A A A NaN
# A A C NaN
# A A D NaN
# A B A NaN
# A B C 54.34
# A B D 826.74
# B A A 765.40
# B A C 514.50
# B A D NaN
# B B A NaN
# B B C NaN
# B B D NaN
Bounty update
L’équipe de développement des pandas s’occupe mal du problème (cf github.com/pandas-dev/pandas/issues/17594 ). Par conséquent, je recherche des réponses aux questions suivantes:
Bounty update # 2
Pour être clair, je ne m'attends pas à des réponses à toutes les 4 questions ci-dessus. La question principale que je pose est de savoir s’il est possible ou souhaitable de remplacer les méthodes de la bibliothèque pandas
afin que les catégories soient traitées de manière à faciliter les opérations groupby
/set_index
.
Depuis Pandas 0.23.0, la méthode groupby
peut désormais prendre un paramètre observed
qui résout ce problème s’il est défini sur True
(False par défaut) . Ci-dessous se trouve le même code que dans la question avec juste observed=True
ajouté:
import pandas as pd
group_cols = ['Group1', 'Group2', 'Group3']
df = pd.DataFrame([['A', 'B', 'C', 54.34],
['A', 'B', 'D', 61.34],
['B', 'A', 'C', 514.5],
['B', 'A', 'A', 765.4],
['A', 'B', 'D', 765.4]],
columns=(group_cols+['Value']))
for col in group_cols:
df[col] = df[col].astype('category')
df.groupby(group_cols, as_index=False, observed=True).sum()
J'ai trouvé le comportement similaire à ce qui est documenté dans la section Opérations de Données catégoriques .
En particulier, semblable à
In [121]: cats2 = pd.Categorical(["a","a","b","b"], categories=["a","b","c"]) In [122]: df2 = pd.DataFrame({"cats":cats2,"B":["c","d","c","d"], "values":[1,2,3,4]}) In [123]: df2.groupby(["cats","B"]).mean() Out[123]: values cats B a c 1.0 d 2.0 b c 3.0 d 4.0 c c NaN d NaN
Quelques autres mots décrivant le comportement associé dans Series
et groupby
. Il y a aussi un exemple de tableau croisé dynamique à la fin de la section.
Outre Series.min (), Series.max () et Series.mode (), le suivant les opérations sont possibles avec des données catégorielles:
Les méthodes de série telles que Series.value_counts () utiliseront toutes les catégories, même si certaines catégories ne sont pas présentes dans les données:
Groupby affichera également les catégories «non utilisées»:
Les mots et l'exemple sont cités dans Données catégoriques .
J'ai pu obtenir une solution qui devrait vraiment bien fonctionner. Je vais éditer mon post avec une meilleure explication. Mais entre-temps, est-ce que cela fonctionne bien pour vous?
import pandas as pd
group_cols = ['Group1', 'Group2', 'Group3']
df = pd.DataFrame([['A', 'B', 'C', 54.34],
['A', 'B', 'D', 61.34],
['B', 'A', 'C', 514.5],
['B', 'A', 'A', 765.4],
['A', 'B', 'D', 765.4]],
columns=(group_cols+['Value']))
for col in group_cols:
df[col] = df[col].astype('category')
result = df.groupby([df[col].values.codes for col in group_cols]).sum()
result = result.reset_index()
level_to_column_name = {f"level_{i}":col for i,col in enumerate(group_cols)}
result = result.rename(columns=level_to_column_name)
for col in group_cols:
result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
result
Donc, la réponse à cela ressemblait plus à une programmation appropriée qu'à une question normale des Pandas. Sous le capot, toutes les séries catégorielles ne sont qu'un groupe de chiffres indexés dans un nom de catégorie. J'ai fait un groupe sur ces nombres sous-jacents car ils n'ont pas le même problème que les colonnes catégoriques. Après cela, j'ai dû renommer les colonnes. J'ai ensuite utilisé le constructeur from_codes pour créer efficacement transformer la liste des entiers dans une colonne catégorique.
Group1 Group2 Group3 Value
A B C 54.34
A B D 826.74
B A A 765.40
B A C 514.50
Je comprends donc que ce n’est pas tout à fait votre réponse, mais j’ai transformé ma solution en une petite fonction pour les personnes confrontées à ce problème à l’avenir.
def categorical_groupby(df,group_cols,agg_fuction="sum"):
"Does a groupby on a number of categorical columns"
result = df.groupby([df[col].values.codes for col in group_cols]).agg(agg_fuction)
result = result.reset_index()
level_to_column_name = {f"level_{i}":col for i,col in enumerate(group_cols)}
result = result.rename(columns=level_to_column_name)
for col in group_cols:
result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
return result
appelez ça comme ça:
df.pipe(categorical_groupby,group_cols)
Il y a beaucoup de questions à résoudre ici.
Commençons par comprendre ce qu’est une "catégorie" ...
Citant de pandas docs pour "Données catégoriques":
Les catégoriels sont un type de données pandas, qui correspondent aux variables catégorielles dans les statistiques: une variable qui ne peut prendre qu'un nombre limité, et généralement fixe, de valeurs possibles (catégories; niveaux dans R). Les exemples sont le sexe, la classe sociale, les groupes sanguins, les affiliations à un pays, le temps d'observation ou les notations via les échelles de Likert.
Il y a deux points sur lesquels je veux me concentrer:
La définition des catégoriels en tant que variable statistique:
En gros, cela signifie que nous devons les examiner d’un point de vue statistique et non sous celui de la programmation "normale". c'est-à-dire qu'ils ne sont pas "énumérés". Les variables catégoriques statistiques ont des opérations et des cas d'utilisation spécifiques, vous pouvez en savoir plus à leur sujet dans wikipedia .
Je parlerai plus à ce sujet après le deuxième point.
Les catégories sont des niveaux en R:
Nous pouvons en savoir plus sur les catégories si nous lisons à propos des niveaux et des facteurs R
.
Je ne connais pas grand chose à propos de R, mais j’ai trouvé cette source simple et suffisante. En citant un exemple intéressant:
When a factor is first created, all of its levels are stored along with the factor, and if subsets of the factor are extracted, they will retain all of the original levels. This can create problems when constructing model matrices and may or may not be useful when displaying the data using, say, the table function. As an example, consider a random sample from the letters vector, which is part of the base R distribution. > lets = sample(letters,size=100,replace=TRUE) > lets = factor(lets) > table(lets[1:5]) a b c d e f g h i j k l m n o p q r s t u v w x y z 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1 Even though only five of the levels were actually represented, the table function shows the frequencies for all of the levels of the original factors. To change this, we can simply use another call to factor > table(factor(lets[1:5])) a k q s z 1 1 1 1 1
En gros, cela nous indique que l’affichage/l’utilisation de toutes les catégories, même si elles ne sont pas nécessaires, n’est pas si rare. Et en fait, c'est le comportement par défaut!
Cela est dû aux cas d'utilisation habituels des variables catégorielles dans les statistiques. Presque dans tous les cas, vous faites vous souciez de toutes les catégories, même si elles ne sont pas utilisées. Prenons par exemple la fonction pandas cut .
J'espère par là que vous avez compris pourquoi ce comportement existe chez les pandas.
En ce qui concerne pourquoi groupby
considère toutes les combinaisons de catégories: je ne peux pas le dire avec certitude, mais ma meilleure hypothèse basée sur un examen rapide du code source (et du problème de github que vous avez mentionné), est qu'ils considèrent groupby
sur les variables qualitatives et une interaction entre elles. Par conséquent, il devrait considérer toutes les paires/n-uplets (comme un produit cartésien). Si je comprends bien, cela aide beaucoup lorsque vous essayez de faire quelque chose comme ANOVA .
Cela signifie également que, dans ce contexte, vous ne pouvez pas y penser dans la terminologie habituelle semblable à SQL.
Ok, mais si vous ne voulez pas ce comportement?
Au meilleur de ma connaissance, et compte tenu du fait que j’ai passé la nuit dernière à rechercher cela dans le code source de pandas, vous ne pouvez pas le "désactiver". C'est codé en dur à chaque étape critique.
Cependant, en raison de la façon dont fonctionne groupby
, le "développement" réel ne se produit pas tant que cela n’est pas nécessaire. Par exemple, lorsque vous appelez sum
sur les groupes ou essayez de les imprimer.
Par conséquent, vous pouvez utiliser l’une des méthodes suivantes pour obtenir uniquement les groupes requis:
df.groupby(group_cols).indices
#{('A', 'B', 'C'): array([0]),
# ('A', 'B', 'D'): array([1, 4]),
# ('B', 'A', 'A'): array([3]),
# ('B', 'A', 'C'): array([2])}
df.groupby(group_cols).groups
#{('A', 'B', 'C'): Int64Index([0], dtype='int64'),
# ('A', 'B', 'D'): Int64Index([1, 4], dtype='int64'),
# ('B', 'A', 'A'): Int64Index([3], dtype='int64'),
# ('B', 'A', 'C'): Int64Index([2], dtype='int64')}
# an example
for g in df.groupby(group_cols).groups:
print(g, grt.get_group(g).sum()[0])
#('A', 'B', 'C') 54.34
#('A', 'B', 'D') 826.74
#('B', 'A', 'A') 765.4
#('B', 'A', 'C') 514.5
Je sais que vous ne pouvez pas y aller, mais je suis sûr à 99% qu'il n'y a pas de moyen direct de le faire.
Je conviens qu'il devrait y avoir une variable booléenne pour désactiver ce comportement et utiliser la variable "normale" semblable à SQL.
J'ai trouvé ce post en déboguant quelque chose de similaire. Très bon article et j'aime beaucoup l'inclusion de conditions aux limites!
Voici le code qui réalise l'objectif initial:
r = df.groupby(group_cols, as_index=False).agg({'Value': 'sum'})
r.columns = ['_'.join(col).strip('_') for col in r.columns]
L'inconvénient de cette solution est qu'il en résulte un index de colonne hiérarchique que vous pouvez éventuellement aplatir (en particulier si vous disposez de plusieurs statistiques). J'ai inclus l'aplatissement de l'index de la colonne dans le code ci-dessus.
Je ne sais pas pourquoi les méthodes d'instance:
df.groupby(group_cols).sum()
df.groupby(group_cols).mean()
df.groupby(group_cols).stdev()
utilisez toutes les combinaisons uniques de variables qualitatives, tandis que la méthode .agg ():
df.groupby(group_cols).agg(['count', 'sum', 'mean', 'std'])
ignore les combinaisons de niveau inutilisées des groupes. Cela semble incohérent. Juste heureux de pouvoir utiliser la méthode .agg () sans avoir à s'inquiéter d'une explosion de combinaison cartésienne.
De plus, je pense qu’il est très courant d’avoir un nombre de cardinalités uniques bien inférieur au produit cartésien. Pensez à tous les cas où les données ont des colonnes telles que "Etat", "Comté", "Zip" ... ce sont toutes des variables imbriquées et de nombreux ensembles de données ont des variables qui ont un degré d'imbrication élevé.
Dans notre cas, la différence entre le produit cartésien des variables de regroupement et les combinaisons naturelles est supérieure à 1000x (et le jeu de données initial est supérieur à 1 000 000 lignes).
Par conséquent, j'aurais voté pour que le comportement par défaut soit observé = True.