181 votes

GroupBy pandas DataFrame et sélection de la valeur la plus courante

J'ai un cadre de données avec trois colonnes de chaînes. Je sais que la seule valeur de la troisième colonne est valable pour chaque combinaison des deux premières. Pour nettoyer les données, je dois grouper le cadre de données par les deux premières colonnes et sélectionner la valeur la plus courante de la troisième colonne pour chaque combinaison.

Mon code :

import pandas as pd
from scipy import stats

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                  'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                  'Short name' : ['NY','New','Spb','NY']})

print source.groupby(['Country','City']).agg(lambda x: stats.mode(x['Short name'])[0])

La dernière ligne de code ne fonctionne pas, elle indique "Key error 'Short name'" et si j'essaie de regrouper uniquement par ville, j'obtiens un AssertionError. Que puis-je faire pour corriger cela ?

237voto

coldspeed Points 111053

Pandas >= 0.16

pd.Series.mode est disponible !

Utilice groupby , GroupBy.agg et appliquer le pd.Series.mode à chaque groupe :

source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

Si l'on a besoin d'un DataFrame, utiliser

source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode).to_frame()

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY

Ce qui est utile dans Series.mode est qu'il retourne toujours une série, ce qui le rend très compatible avec agg y apply notamment lors de la reconstruction de la sortie groupby. Il est également plus rapide.

# Accepted answer.
%timeit source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])
# Proposed in this post.
%timeit source.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)

5.56 ms ± 343 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.76 ms ± 387 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Gérer les modes multiples

Series.mode fait également un bon travail lorsqu'il y a multiple modes :

source2 = source.append(
    pd.Series({'Country': 'USA', 'City': 'New-York', 'Short name': 'New'}),
    ignore_index=True)

# Now `source2` has two modes for the 
# ("USA", "New-York") group, they are "NY" and "New".
source2

  Country              City Short name
0     USA          New-York         NY
1     USA          New-York        New
2  Russia  Sankt-Petersburg        Spb
3     USA          New-York         NY
4     USA          New-York        New

source2.groupby(['Country','City'])['Short name'].agg(pd.Series.mode)

Country  City            
Russia   Sankt-Petersburg          Spb
USA      New-York            [NY, New]
Name: Short name, dtype: object

Ou, si vous voulez une ligne séparée pour chaque mode, vous pouvez utiliser GroupBy.apply :

source2.groupby(['Country','City'])['Short name'].apply(pd.Series.mode)

Country  City               
Russia   Sankt-Petersburg  0    Spb
USA      New-York          0     NY
                           1    New
Name: Short name, dtype: object

Si vous je m'en fiche dont le mode est retourné tant que c'est l'un ou l'autre, alors vous aurez besoin d'une lambda qui appelle mode et extrait le premier résultat.

source2.groupby(['Country','City'])['Short name'].agg(
    lambda x: pd.Series.mode(x)[0])

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

Alternatives à (ne pas) considérer

Vous pouvez également utiliser statistics.mode de python, mais...

source.groupby(['Country','City'])['Short name'].apply(statistics.mode)

Country  City            
Russia   Sankt-Petersburg    Spb
USA      New-York             NY
Name: Short name, dtype: object

...il ne fonctionne pas bien lorsqu'il faut faire face à des modes multiples ; une StatisticsError est soulevée. Ceci est mentionné dans la documentation :

Si les données sont vides, ou s'il n'y a pas exactement une valeur la plus courante, StatisticsError est soulevé.

Mais vous pouvez voir par vous-même...

statistics.mode([1, 2])
# ---------------------------------------------------------------------------
# StatisticsError                           Traceback (most recent call last)
# ...
# StatisticsError: no unique mode; found 2 equally common values

196voto

HYRY Points 26340

Vous pouvez utiliser value_counts() pour obtenir une série de comptage, et obtenir la première ligne :

import pandas as pd

source = pd.DataFrame({'Country' : ['USA', 'USA', 'Russia','USA'], 
                  'City' : ['New-York', 'New-York', 'Sankt-Petersburg', 'New-York'],
                  'Short name' : ['NY','New','Spb','NY']})

source.groupby(['Country','City']).agg(lambda x:x.value_counts().index[0])

Si vous vous demandez comment exécuter d'autres fonctions agg dans la fonction .agg() essayez ceci.

# Let's add a new col,  account
source['account'] = [1,2,3,3]

source.groupby(['Country','City']).agg(mod  = ('Short name', \
                                        lambda x: x.value_counts().index[0]),
                                        avg = ('account', 'mean') \
                                      )

24voto

abw333 Points 668

Je suis arrivé un peu tard, mais j'ai rencontré des problèmes de performances avec la solution de HYRY, et j'ai donc dû en trouver une autre.

Il fonctionne en trouvant la fréquence de chaque clé-valeur, puis, pour chaque clé, en ne conservant que la valeur qui apparaît avec elle le plus souvent.

Il existe également une solution supplémentaire qui prend en charge plusieurs modes.

Sur un test à l'échelle représentative des données avec lesquelles je travaille, le temps d'exécution a été réduit de 37,4 à 0,5 secondes !

Voici le code de la solution, quelques exemples d'utilisation et le test à l'échelle :

import numpy as np
import pandas as pd
import random
import time

test_input = pd.DataFrame(columns=[ 'key',          'value'],
                          data=  [[ 1,              'A'    ],
                                  [ 1,              'B'    ],
                                  [ 1,              'B'    ],
                                  [ 1,              np.nan ],
                                  [ 2,              np.nan ],
                                  [ 3,              'C'    ],
                                  [ 3,              'C'    ],
                                  [ 3,              'D'    ],
                                  [ 3,              'D'    ]])

def mode(df, key_cols, value_col, count_col):
    '''                                                                                                                                                                                                                                                                                                                                                              
    Pandas does not provide a `mode` aggregation function                                                                                                                                                                                                                                                                                                            
    for its `GroupBy` objects. This function is meant to fill                                                                                                                                                                                                                                                                                                        
    that gap, though the semantics are not exactly the same.                                                                                                                                                                                                                                                                                                         

    The input is a DataFrame with the columns `key_cols`                                                                                                                                                                                                                                                                                                             
    that you would like to group on, and the column                                                                                                                                                                                                                                                                                                                  
    `value_col` for which you would like to obtain the mode.                                                                                                                                                                                                                                                                                                         

    The output is a DataFrame with a record per group that has at least one mode                                                                                                                                                                                                                                                                                     
    (null values are not counted). The `key_cols` are included as columns, `value_col`                                                                                                                                                                                                                                                                               
    contains a mode (ties are broken arbitrarily and deterministically) for each                                                                                                                                                                                                                                                                                     
    group, and `count_col` indicates how many times each mode appeared in its group.                                                                                                                                                                                                                                                                                 
    '''
    return df.groupby(key_cols + [value_col]).size() \
             .to_frame(count_col).reset_index() \
             .sort_values(count_col, ascending=False) \
             .drop_duplicates(subset=key_cols)

def modes(df, key_cols, value_col, count_col):
    '''                                                                                                                                                                                                                                                                                                                                                              
    Pandas does not provide a `mode` aggregation function                                                                                                                                                                                                                                                                                                            
    for its `GroupBy` objects. This function is meant to fill                                                                                                                                                                                                                                                                                                        
    that gap, though the semantics are not exactly the same.                                                                                                                                                                                                                                                                                                         

    The input is a DataFrame with the columns `key_cols`                                                                                                                                                                                                                                                                                                             
    that you would like to group on, and the column                                                                                                                                                                                                                                                                                                                  
    `value_col` for which you would like to obtain the modes.                                                                                                                                                                                                                                                                                                        

    The output is a DataFrame with a record per group that has at least                                                                                                                                                                                                                                                                                              
    one mode (null values are not counted). The `key_cols` are included as                                                                                                                                                                                                                                                                                           
    columns, `value_col` contains lists indicating the modes for each group,                                                                                                                                                                                                                                                                                         
    and `count_col` indicates how many times each mode appeared in its group.                                                                                                                                                                                                                                                                                        
    '''
    return df.groupby(key_cols + [value_col]).size() \
             .to_frame(count_col).reset_index() \
             .groupby(key_cols + [count_col])[value_col].unique() \
             .to_frame().reset_index() \
             .sort_values(count_col, ascending=False) \
             .drop_duplicates(subset=key_cols)

print test_input
print mode(test_input, ['key'], 'value', 'count')
print modes(test_input, ['key'], 'value', 'count')

scale_test_data = [[random.randint(1, 100000),
                    str(random.randint(123456789001, 123456789100))] for i in range(1000000)]
scale_test_input = pd.DataFrame(columns=['key', 'value'],
                                data=scale_test_data)

start = time.time()
mode(scale_test_input, ['key'], 'value', 'count')
print time.time() - start

start = time.time()
modes(scale_test_input, ['key'], 'value', 'count')
print time.time() - start

start = time.time()
scale_test_input.groupby(['key']).agg(lambda x: x.value_counts().index[0])
print time.time() - start

L'exécution de ce code imprimera quelque chose comme :

   key value
0    1     A
1    1     B
2    1     B
3    1   NaN
4    2   NaN
5    3     C
6    3     C
7    3     D
8    3     D
   key value  count
1    1     B      2
2    3     C      2
   key  count   value
1    1      2     [B]
2    3      2  [C, D]
0.489614009857
9.19386196136
37.4375009537

J'espère que cela vous aidera !

19voto

eumiro Points 56644

Para agg la fonction lambba reçoit un Series qui n'a pas de 'Short name' attribut.

stats.mode renvoie un tuple de deux tableaux, vous devez donc prendre le premier élément du premier tableau dans ce tuple.

Avec ces deux simples changements :

source.groupby(['Country','City']).agg(lambda x: stats.mode(x)[0][0])

renvoie à

                         Short name
Country City                       
Russia  Sankt-Petersburg        Spb
USA     New-York                 NY

13voto

Josh Friedlander Points 3255

Les deux premières réponses suggèrent :

df.groupby(cols).agg(lambda x:x.value_counts().index[0])

ou, de préférence

df.groupby(cols).agg(pd.Series.mode)

Cependant, ces deux méthodes échouent dans des cas limites simples, comme le montre l'exemple suivant :

df = pd.DataFrame({
    'client_id':['A', 'A', 'A', 'A', 'B', 'B', 'B', 'C'],
    'date':['2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01', '2019-01-01'],
    'location':['NY', 'NY', 'LA', 'LA', 'DC', 'DC', 'LA', np.NaN]
})

Le premier :

df.groupby(['client_id', 'date']).agg(lambda x:x.value_counts().index[0])

donne IndexError (à cause de la série vide renvoyée par le groupe C ). La seconde :

df.groupby(['client_id', 'date']).agg(pd.Series.mode)

renvoie à ValueError: Function does not reduce puisque le premier groupe renvoie une liste de deux (puisqu'il y a deux modes). (Comme documenté aquí si le premier groupe renvoyait un mode unique, cela fonctionnerait !)

Deux solutions sont possibles dans ce cas :

import scipy
x.groupby(['client_id', 'date']).agg(lambda x: scipy.stats.mode(x)[0])

Et la solution qui m'a été donnée par cs95 dans les commentaires aquí :

def foo(x): 
    m = pd.Series.mode(x); 
    return m.values[0] if not m.empty else np.nan
df.groupby(['client_id', 'date']).agg(foo)

Cependant, toutes ces méthodes sont lentes et ne sont pas adaptées aux grands ensembles de données. Une solution que j'ai fini par utiliser qui a) peut traiter ces cas et b) est beaucoup, beaucoup plus rapide, est une version légèrement modifiée de la réponse d'abw33 (qui devrait être plus élevée) :

def get_mode_per_column(dataframe, group_cols, col):
    return (dataframe.fillna(-1)  # NaN placeholder to keep group 
            .groupby(group_cols + [col])
            .size()
            .to_frame('count')
            .reset_index()
            .sort_values('count', ascending=False)
            .drop_duplicates(subset=group_cols)
            .drop(columns=['count'])
            .sort_values(group_cols)
            .replace(-1, np.NaN))  # restore NaNs

group_cols = ['client_id', 'date']    
non_grp_cols = list(set(df).difference(group_cols))
output_df = get_mode_per_column(df, group_cols, non_grp_cols[0]).set_index(group_cols)
for col in non_grp_cols[1:]:
    output_df[col] = get_mode_per_column(df, group_cols, col)[col].values

Essentiellement, la méthode travaille sur une colonne à la fois et produit un df, donc au lieu de concat ce qui est intensif, on traite le premier comme un df, puis on ajoute itérativement le tableau de sortie ( values.flatten() ) comme une colonne dans le df.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X