Pandas groupby avec des catégories avec des Nan redondantes

j'ai des problèmes avec pandas groupby avec des données nominales. Théoriquement, il devrait être super efficace: vous groupez et indexez via des entiers plutôt que des chaînes. Mais il insiste sur le fait que, en regroupant par catégories multiples,chaque combinaison de catégories doit être comptabilisé.

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 qu'elles sauvegardent la mémoire / améliorent performance. Il y a parfois des milliers de catégories dans chaque colonne. En regroupant par 3 colonnes,pandas nous oblige à tenir les résultats pour 1000 groupes^3.

ma question: y a-t-il une façon pratique d'utiliser <!-Avec des catégories tout en évitant ce comportement fâcheux? Je ne cherche aucune de ces solutions:

  • recréer toutes les fonctionnalités via numpy.
  • conversion continue en chaînes/codes avant groupby, le retour à catégories plus tard.
  • création d'une colonne tuple à partir des colonnes de groupe, puis groupe par la colonne tuple.

j'espère qu'il y a un moyen de modifier juste ce particulier pandas idiosyncrasie. Un exemple simple est ci-dessous. Au lieu de 4 catégories que je veux dans la sortie, je finis 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

mise à jour des primes

la question est mal abordée par l'équipe de développement pandas (cf github.com/pandas-dev/pandas/issues/17594). Par consà © quent, je cherche des rà © ponses qui traitent de l'un ou l'autre des aspects suivants:

  1. pourquoi, en ce qui concerne le code source pandas, les données catégoriques sont-elles traitées différemment dans les opérations groupby?
  2. pourquoi préférerait-on la mise en oeuvre actuelle? Je comprends que cela soit subjectif, mais je me bats pour trouver une réponse à cette question. Comportement actuel est prohibitif dans de nombreuses situations sans solutions de rechange encombrantes, potentiellement coûteuses.
  3. Existe-t-il une solution propre pour remplacer le traitement pandas des données catégoriques dans les opérations groupby? Remarque le 3 no-go routes (descendre à numpy, la conversion vers/à partir de codes; la création et le regroupement par tuple colonnes). Je préférerais une solution "conforme pandas" pour minimiser / éviter la perte d'autres fonctionnalités catégoriques pandas.
  4. une réponse de l'équipe de développement pandas pour soutenir et clarifier les traitement. En outre, pourquoi devrait considérer toutes les combinaisons de catégorie ne pas être configurable comme un paramètre booléen?

mise à jour des primes #2

Pour être clair, je ne suis pas attendre de réponses aux 4 questions. La question principale que je pose est de savoir s'il est possible ou souhaitable d'écraser pandas méthodes de bibliothèque pour que les catégories soient traitées d'une manière qui facilite groupby / set_index opérations.

12
demandé sur jpp 2018-01-27 04:12:47

5 réponses

depuis Pandas 0.23.0, le groupby méthode peut maintenant prendre un paramètre observed qui corrige ce problème si elle est définie à True (False par défaut). Ci-dessous se trouve le même code que dans la question avec juste observed=True ajout de :

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()

enter image description here

6
répondu ismax 2018-09-24 14:26:53

j'ai pu trouver une solution qui devrait très bien fonctionner. Je vais éditer mon post avec une meilleure explication. Mais dans le même temps, 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 à cette question ressemblait plus à une bonne programmation qu'à une question normale de Pandas. Sous le capot, toutes les séries catégoriques sont juste un tas de nombres qui indexent dans un nom de catégories. J'ai fait un groupby sur ces chiffres, car ils n'ont pas le même problème que catégorique colonnes. Après avoir fait ceci j'ai dû renommer les colonnes. J'ai ensuite utilisé le constructeur from_codes pour créer efficacement retourner 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

donc je comprends que ce n'est pas exactement votre réponse mais j'ai fait de ma solution Une petite fonction pour les gens qui ont ce problème dans le futur.

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

l'appeler comme ceci:

df.pipe(categorical_groupby,group_cols)
4
répondu Gabriel A 2018-02-01 17:46:03

j'ai trouvé le comportement similaire à ce qui est documenté dans la section opérations de Données Catégoriques.

En particulier, similaire à

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 relatif dans Series et groupby. Il y a aussi un exemple de table de pivotement à la fin de la section.

en dehors de la Série.min(), de la Série.max() et de la Série.mode (mode), les éléments suivants les opérations sont possibles avec catégorique données:

méthodes de série comme la série.value_counts() utilisera toutes les catégories, même si certaines catégories ne sont pas présentes dans les données:

Groupby affichera aussi les catégories "non utilisées":

Les paroles et l'exemple sont cités à partir de Données Catégoriques.

4
répondu Tai 2018-02-02 07:06:56

il y a un beaucoup de questions auxquelles il faut répondre ici.

Commençons par comprendre ce qu'est une "catégorie"...

Définition de l'Catégorique dtype

citation de pandas docs pour "Categorical Data":

Categoricals sont une pandas type de données, qui correspondent à des variables catégoriques dans les statistiques: une variable, qui peut prendre seulement un nombre limité, et généralement fixe, de valeurs possibles ( catégories; niveaux en R). Les exemples sont le sexe, la classe sociale, les groupes sanguins, les affiliations Nationales, le temps d'observation ou les cotes selon les échelles de Likert.

il y a deux points sur lesquels je veux me concentrer ici:

  1. définition de La categoricals comme une variable statistique:

    fondamentalement, cela signifie que nous avons de les regarder d'un point de vue statistique, pas celui de la programmation "régulière". c'est à dire qu'ils ne sont pas "énumère'. Les variables statistiques catégoriques ont des opérations et des usecases spécifiques, vous pouvez en lire plus à leur sujet dans wikipédia.

    J'en reparlerai après le deuxième point.

  2. les catégories sont des niveaux dans R:

    Nous pouvons en savoir plus sur les categoricals si nous lisons à propos de R niveaux et les facteurs.

    Je n'ai pas j'en sais beaucoup sur R, mais j'ai trouvé source simple et suffisant. 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
    

fondamentalement, 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 habituels d'utilisation de variables catégoriques dans les statistiques. Presque dans tous les cas, vous toutes les catégories, même si ils ne sont pas utilisés. Prenons par exemple la fonction pandas couper.

GroupBy sur les Variables Catégorielles

pourquoi ne groupby envisager toutes les combinaisons de catégories: je ne peux pas dire pour sûr, mais ma meilleure supposition basée sur un examen rapide du code source (et le github problème vous avez mentionné), est-ce qu'ils considèrent le groupby sur les variables catégorielles interaction entre eux. Par conséquent, il devrait considérer tous les couples/tuples (comme un produit cartésien). AFAIK, cela aide beaucoup quand 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 de type SQL.

des Solutions?

Ok, mais que faire si vous ne voulez pas cela le comportement?

À ma connaissance, et compte tenu du fait que j'ai passé la dernière nuit à le tracer en code source pandas, vous ne pouvez pas le "désactiver". Il est codé en dur dans chaque étape critique.

Toutefois, en raison de la façon dont groupby fonctionne, la véritable "expansion" ne se produit pas jusqu'à ce que ce soit nécessaire. Par exemple, en appelant sum sur les groupes ou en essayant de les imprimer.

Par conséquent, vous pouvez faire l'un des suivants pour obtenir seulement le nécessaire groupes:

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 c'est un non-go pour vous, mais je suis sûr à 99% qu'il n'y a pas de moyen direct de le faire.

Je suis d'accord qu'il devrait y avoir une variable booléenne pour désactiver ce comportement et utiliser le SQL "régulier" comme un.

3
répondu Qusai Alothman 2018-02-02 19:21:26

j'ai trouvé ce post en déboguant quelque chose de similaire. Très bon post, et j'aime vraiment l'inclusion de conditions aux limites!

voici le code qui réalise le but initial:

r = df.groupby(group_cols, as_index=False).agg({'Value': 'sum'})

r.columns = ['_'.join(col).strip('_') for col in r.columns]

enter image description here

L'inconvénient de cette solution est qu'il en résulte une hiérarchie index de colonne que vous pouvez les aplatir (surtout si vous avez plusieurs statistiques). J'ai inclus l'aplatissement de l'index de colonne dans le code surtout.

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 catégoriques, tandis que le .apa() méthode:

df.groupby(group_cols).agg(['count', 'sum', 'mean', 'std']) 

ne tient pas compte des combinaisons de niveaux inutilisées des groupes. Cela semble incohérent. Juste heureux que nous pouvons utiliser la .agg () et ne pas avoir à se soucier d'une explosion de combinaison cartésienne.

en outre, je pense qu'il est très commun d'avoir un nombre de cardinalités unique beaucoup plus bas contre le cartésien produit. Pensez à tous les cas où les données ont des colonnes comme "État", "Comté","Zip"... ce sont toutes des variables imbriquées et de nombreux ensembles de données ont des variables qui ont un haut degré de nidification.

dans notre cas, la différence entre le produit cartésien des variables de regroupement et les combinaisons naturelles est supérieure à 1000x (et l'ensemble de données de départ est supérieur à 1 000 000 de lignes).

par conséquent, j'aurais voté pour faire observer=True la valeur par défaut comportement.

0
répondu Randall Goodwin 2018-07-03 15:28:26