web-dev-qa-db-fra.com

groupby colonnes avec des valeurs NaN (manquantes)

J'ai un DataFrame avec beaucoup de valeurs manquantes dans les colonnes que je souhaite grouper par:

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]}

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

Comme j'ai besoin de nombreuses opérations de ce type (beaucoup de colonnes ont des valeurs manquantes) et que j'utilise des fonctions plus compliquées que les médianes (forêts aléatoires, par exemple), je veux éviter d'écrire des morceaux de code trop compliqués.

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

106
Gyula Sámuel Karli

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

Les groupes NA dans GroupBy sont automatiquement exclus. Ce comportement est compatible avec R, par exemple.

Une solution de contournement consiste à utiliser un espace réservé avant de procéder à 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

Ceci dit, cela semble assez horrible ... peut-être qu'il devrait y avoir une option pour inclure NaN dans groupby (voir cette question de github - qui utilise le même hack de marque de réservation ).

101
Andy Hayden

Ancien sujet, si quelqu'un trébuche encore sur ce point - une autre solution consiste à convertir via .astype (str) en chaîne avant le regroupement. Cela préservera 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
25
M. Kiewisch

Je ne suis pas en mesure d’ajouter un commentaire à M. Kiewisch car je n’ai pas assez de points de réputation (je n’ai que 41 points, mais il en faut plus de 50 pour commenter).

Quoi qu’il en soit, je tiens à souligner que la solution de M. Kiewisch ne fonctionne pas telle quelle et peut nécessiter davantage de peaufinage. Considérons 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 s'agit simplement de concaténer 1 et 5 sous forme de chaînes au lieu de les ajouter sous forme de nombres.

7

Un petit point à la solution d'Andy Hayden - ça ne marche plus (parce que?) Parce que np.nan == np.nan donne False, donc la fonction replace ne fait rien.

Ce qui a fonctionné pour moi a été ceci:

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

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

6
Tuetschek

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

Une solution moins complexe consiste à utiliser pd.drop_duplicates () pour créer un index unique de combinaisons de valeurs, chacune avec son propre ID, puis un groupe 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')]))

Le résultat obtenu sera renvoyé sans que vous ayez à vous soucier d'écraser des données réelles qui sont confondues avec une valeur factice.

3
Grant Langseth

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

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 ne correspond pas à beaucoup d'autres choses. Quoi qu'il en soit, le bidouillage factice est également très mauvais. Cependant, la taille (incluant les NaN) et le nombre (ignorant les NaN) d'un groupe seront différents s'il existe 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.

2
Brian Preslopsky