colonnes Pandas GroupBy avec des valeurs NaN (manquantes)

147

J'ai un DataFrame avec de nombreuses valeurs manquantes dans les colonnes que je souhaite regrouper:

import pandas as pd
import numpy as np
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})

In [4]: df.groupby('b').groups
Out[4]: {'4': [0], '6': [2]}

voir que Pandas a supprimé les lignes avec des valeurs cibles NaN. (Je veux inclure ces lignes!)

Comme j'ai besoin de nombreuses opérations de ce type (de nombreux cols ont des valeurs manquantes) et que j'utilise des fonctions plus compliquées que de simples médianes (généralement des forêts aléatoires), je veux éviter d'écrire des morceaux de code trop compliqués.

Aucune suggestion? Dois-je écrire une fonction pour cela ou existe-t-il une solution simple?

Gyula Sámuel Karli
la source
1
@PhillipCloud J'ai édité cette question pour n'inclure que la question, qui est en fait assez bonne, relative à l' amélioration des pandas ouverts de Jeff.
Andy Hayden
1
Le fait de ne pas pouvoir inclure (et propager) des NaN dans des groupes est assez aggravant. Citer R n'est pas convaincant, car ce comportement n'est pas cohérent avec beaucoup d'autres choses. Quoi qu'il en soit, le hack factice est également assez mauvais. Cependant, la taille (inclut les NaN) et le nombre (ignore les NaN) d'un groupe diffèrent s'il y a des NaN. dfgrouped = df.groupby (['b']). a.agg (['sum', 'size', 'count']) dfgrouped ['sum'] [dfgrouped ['size']! = dfgrouped ['count ']] = None
Brian Preslopsky
Pouvez-vous résumer ce que vous essayez précisément d'accomplir? c'est-à-dire que nous voyons une sortie, mais quelle est la sortie "souhaitée"?
ca
3
Avec 1.1 pandas géants vous serez bientôt en mesure de préciser dropna=Falsedans groupby()pour obtenir le résultat souhaité. En savoir plus
CS95

Réponses:

131

Ceci est mentionné dans la section Données manquantes de la documentation :

Les groupes NA de GroupBy sont automatiquement exclus. Ce comportement est cohérent avec R, par exemple.

Une solution de contournement consiste à utiliser un espace réservé avant d'effectuer le groupby (par exemple -1):

In [11]: df.fillna(-1)
Out[11]: 
   a   b
0  1   4
1  2  -1
2  3   6

In [12]: df.fillna(-1).groupby('b').sum()
Out[12]: 
    a
b    
-1  2
4   1
6   3

Cela dit, cela semble assez horrible hack ... peut-être qu'il devrait y avoir une option pour inclure NaN dans groupby (voir ce problème github - qui utilise le même hack d'espace réservé).

Andy Hayden
la source
4
C'est une solution logique mais une sorte de solution amusante à laquelle j'ai pensé plus tôt, Pandas crée des champs NaN à partir des champs vides, et nous devons les changer. C'est la raison pour laquelle je pense chercher d'autres solutions comme exécuter un serveur SQL et interroger les tables à partir de là (cela semble un peu trop compliqué), ou chercher une autre bibliothèque malgré Pandas, ou utiliser la mienne (que je veux se débarrasser de). Thx
Gyula Sámuel Karli
@ GyulaSámuelKarli Pour moi, cela semble être un petit bogue (voir le rapport de bogue ci-dessus), et ma solution est une solution de contournement. Je trouve étrange que vous supprimiez toute la bibliothèque.
Andy Hayden
1
Je ne veux pas écrire les Pandas, mais cherchez simplement l'outil qui correspond le plus à mes demandes.
Gyula Sámuel Karli
1
Jetez un œil à ma réponse ci-dessous, je pense avoir trouvé une solution assez bonne (plus propre et probablement plus rapide). stackoverflow.com/a/43375020/408853
ca
4
Non, cela n'est pas cohérent avec R. df%>% group_by donnera également des résumés NA avec un avertissement qui peut être évité en passant la colonne de regroupement par fct_explicit_na, puis un niveau (manquant) est créé.
Ravaging Care
40

Sujet ancien, si quelqu'un trébuche encore dessus - une autre solution de contournement consiste à convertir via .astype (str) en chaîne avant le regroupement. Cela conservera les NaN.

in:
df = pd.DataFrame({'a': ['1', '2', '3'], 'b': ['4', np.NaN, '6']})
df['b'] = df['b'].astype(str)
df.groupby(['b']).sum()
out:
    a
b   
4   1
6   3
nan 2
M. Kiewisch
la source
@ K3 --- rnc: Voir le commentaire sur votre lien - l'auteur du message dans votre lien a fait quelque chose de mal.
Thomas
@Thomas, oui, exactement comme dans l'exemple ci-dessus. Veuillez modifier si vous pouvez rendre l'exemple sûr (et aussi trivial).
K3 --- rnc
Le sumof aest une concaténation de chaînes ici, pas une somme numérique. Cela "fonctionne" uniquement parce que 'b' se composait d'entrées distinctes. Vous avez besoin de 'a' pour être numérique et 'b' pour être une chaîne
BallpointBen
30

pandas> = 1,1

Depuis pandas 1.1 vous avez un meilleur contrôle sur ce comportement, les valeurs NA sont désormais autorisées dans le groupeur en utilisant dropna=False:

pd.__version__
# '1.1.0.dev0+2004.g8d10bfb6f'

# Example from the docs
df

   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2

# without NA (the default)
df.groupby('b').sum()

     a  c
b        
1.0  2  3
2.0  2  5
# with NA
df.groupby('b', dropna=False).sum()

     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4
cs95
la source
4
Espérons que cette réponse fasse une marche progressive vers le sommet. C'est la bonne approche.
kdbanman le
Je ne pense pas que la version 1.1 soit encore sortie. Vérifié sur conda et pip et les versions il y a encore 1.0.4
sammywemmy
@sammywemmy pandas 1.1 est sorti le 28 juillet 20, je crois.
cs95 il y a
@ cs95 haha. Merci. J'ai déjà mis à jour.
sammywemmy il y a
9

Je ne peux pas ajouter de commentaire à M. Kiewisch car je n'ai pas assez de points de réputation (en ai seulement 41 mais il en faut plus de 50 pour commenter).

Quoi qu'il en soit, je tiens simplement à souligner que la solution de M. Kiewisch ne fonctionne pas telle quelle et peut nécessiter davantage de modifications. Considérez par exemple

>>> df = pd.DataFrame({'a': [1, 2, 3, 5], 'b': [4, np.NaN, 6, 4]})
>>> df
   a    b
0  1  4.0
1  2  NaN
2  3  6.0
3  5  4.0
>>> df.groupby(['b']).sum()
     a
b
4.0  6
6.0  3
>>> df.astype(str).groupby(['b']).sum()
      a
b
4.0  15
6.0   3
nan   2

ce qui montre que pour le groupe b = 4.0, la valeur correspondante est 15 au lieu de 6. Ici, il concatène simplement 1 et 5 sous forme de chaînes au lieu de l'ajouter sous forme de nombres.

Kamaraju Kusumanchi
la source
13
C'est parce que vous avez converti l'intégralité du DF en str, au lieu de la seule bcolonne
Korem
Notez que cela a été corrigé dans la réponse mentionnée maintenant.
Shaido - Réintègre Monica
1
La nouvelle solution est meilleure mais toujours pas sûre, à mon avis. Prenons un cas où l'une des entrées de la colonne 'b' est identique à np.NaN stringifié. Ensuite, ces choses sont matraquées ensemble. df = pd.DataFrame ({'a': [1, 2, 3, 5, 6], 'b': ['foo', np.NaN, 'bar', 'foo', 'nan']}) ; df ['b'] = df ['b']. astype (str); df.groupby (['b']). sum ()
Kamaraju Kusumanchi
6

Un petit point à la solution d'Andy Hayden - cela ne fonctionne pas (plus?) Parce que np.nan == np.nancède False, donc la replacefonction ne fait rien.

Ce qui a fonctionné pour moi, c'est ceci:

df['b'] = df['b'].apply(lambda x: x if not np.isnan(x) else -1)

(Du moins c'est le comportement de Pandas 0.19.2. Désolé de l'ajouter comme réponse différente, je n'ai pas assez de réputation pour commenter.)

Tuetschek
la source
12
Il y a aussi df['b'].fillna(-1).
K3 --- rnc
6

Toutes les réponses fournies jusqu'à présent entraînent un comportement potentiellement dangereux car il est tout à fait possible que vous sélectionniez une valeur fictive qui fait en fait partie de l'ensemble de données. Cela est de plus en plus probable lorsque vous créez des groupes avec de nombreux attributs. En termes simples, l'approche ne se généralise pas toujours bien.

Une solution moins hacky consiste à utiliser pd.drop_duplicates () pour créer un index unique de combinaisons de valeurs, chacune avec son propre identifiant, puis grouper sur cet identifiant. Il est plus verbeux mais fait le travail:

def safe_groupby(df, group_cols, agg_dict):
    # set name of group col to unique value
    group_id = 'group_id'
    while group_id in df.columns:
        group_id += 'x'
    # get final order of columns
    agg_col_order = (group_cols + list(agg_dict.keys()))
    # create unique index of grouped values
    group_idx = df[group_cols].drop_duplicates()
    group_idx[group_id] = np.arange(group_idx.shape[0])
    # merge unique index on dataframe
    df = df.merge(group_idx, on=group_cols)
    # group dataframe on group id and aggregate values
    df_agg = df.groupby(group_id, as_index=True)\
               .agg(agg_dict)
    # merge grouped value index to results of aggregation
    df_agg = group_idx.set_index(group_id).join(df_agg)
    # rename index
    df_agg.index.name = None
    # return reordered columns
    return df_agg[agg_col_order]

Notez que vous pouvez maintenant simplement faire ce qui suit:

data_block = [np.tile([None, 'A'], 3),
              np.repeat(['B', 'C'], 3),
              [1] * (2 * 3)]

col_names = ['col_a', 'col_b', 'value']

test_df = pd.DataFrame(data_block, index=col_names).T

grouped_df = safe_groupby(test_df, ['col_a', 'col_b'],
                          OrderedDict([('value', 'sum')]))

Cela renverra le résultat réussi sans avoir à vous soucier d'écraser des données réelles qui sont confondues avec une valeur fictive.

Grant Langseth
la source
C'est la meilleure solution pour le cas général, mais dans les cas où je connais une chaîne / un numéro invalide que je peux utiliser à la place, je vais probablement suivre la réponse d'Andy Hayden ci-dessous ... J'espère que les pandas corrigent ce problème bientôt.
Sarah Messer le
4

J'ai déjà répondu à cela, mais pour une raison quelconque, la réponse a été convertie en commentaire. Néanmoins, c'est la solution la plus efficace:

Le fait de ne pas pouvoir inclure (et propager) les NaN dans des groupes est assez aggravant. Citer R n'est pas convaincant, car ce comportement n'est pas cohérent avec beaucoup d'autres choses. Quoi qu'il en soit, le hack factice est également assez mauvais. Cependant, la taille (inclut les NaN) et le nombre (ignore les NaN) d'un groupe diffèrent s'il y a des NaN.

dfgrouped = df.groupby(['b']).a.agg(['sum','size','count'])

dfgrouped['sum'][dfgrouped['size']!=dfgrouped['count']] = None

Lorsque ceux-ci diffèrent, vous pouvez redéfinir la valeur sur Aucun pour le résultat de la fonction d'agrégation pour ce groupe.

Brian Preslopsky
la source
1
Cela m'a été très utile, mais cela répond à une question légèrement différente de celle d'origine. IIUC, votre solution propage les NaN dans la somme, mais les éléments NaN de la colonne «b» sont toujours supprimés en tant que lignes.
Andrew
0

Installé Pandas 1.1 dans Anaconda

Je ne suis pas en mesure de commenter la réponse de cs95 mais il m'a aidé à résoudre le problème.

J'ai essayé d'installer Pandas 1.1 mais cela a échoué en utilisant son code, donc j'ai cherché sur Google et j'ai pu installer.

Je lance d'abord l'invite anaconda en tant qu'administrateur et je colle le code suivant:

pip install pandas==1.1.0rc0

Après cela, utilisez dropna = False

Lien: https://libraries.io/pypi/pandas

EzrealReal
la source
0

df = df.fillna("") cela a fonctionné pour moi

Vineet Kumar
la source